feat: update PDF stamp picker and server CORS settings for enhanced UI rendering
This commit is contained in:
+6
-6
@@ -131,12 +131,12 @@ public class CyberCoreSignerApplication {
|
|||||||
private static void printBanner() {
|
private static void printBanner() {
|
||||||
System.out.println("""
|
System.out.println("""
|
||||||
============================================================
|
============================================================
|
||||||
____ _ ____
|
____ _ ____ \s
|
||||||
/ ___| _ _ __| | ___ _ __ / ___|___ _ __ ___
|
/ ___| _ _ | |__ ___ _ __ / ___|___ _ __ ___ \s
|
||||||
| | | | | |/ _` | / _ \\| '__| | | / _ \\ | '__|/ _ \\
|
| | | | | | | '_ \\ / _ \\ '__| | | / _ \\| '__/ _ \\ \s
|
||||||
| |___ | |_| | (_| || __/| | | |__| (_) || | | __/
|
| |___ | |_| | | |_) | __/ | | |__| (_) | | | __/ \s
|
||||||
\\____| \\__, |\\__,_| \\___||_| \\____\\___/ |_| \\___|
|
\\____| \\__, | |_.__/ \\___|_| \\____\\___/|_| \\___| \s
|
||||||
|___/
|
|___/ \s
|
||||||
SIGNER AGENT v0.1.0
|
SIGNER AGENT v0.1.0
|
||||||
============================================================
|
============================================================
|
||||||
""");
|
""");
|
||||||
|
|||||||
+2
-1
@@ -990,7 +990,8 @@ public class CcspServer {
|
|||||||
|
|
||||||
exchange.getResponseHeaders().set("Access-Control-Allow-Origin", origin);
|
exchange.getResponseHeaders().set("Access-Control-Allow-Origin", origin);
|
||||||
exchange.getResponseHeaders().set("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
exchange.getResponseHeaders().set("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
||||||
exchange.getResponseHeaders().set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-CyberCore-AppId, X-CyberCore-Nonce");
|
exchange.getResponseHeaders().set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-CyberCore-AppId, X-CyberCore-Nonce, Accept");
|
||||||
|
exchange.getResponseHeaders().set("Access-Control-Allow-Private-Network", "true");
|
||||||
exchange.getResponseHeaders().set("Access-Control-Max-Age", "86400");
|
exchange.getResponseHeaders().set("Access-Control-Max-Age", "86400");
|
||||||
|
|
||||||
if ("OPTIONS".equalsIgnoreCase(exchange.getRequestMethod())) {
|
if ("OPTIONS".equalsIgnoreCase(exchange.getRequestMethod())) {
|
||||||
|
|||||||
+1
@@ -96,6 +96,7 @@ public class CyberCoreDesktopApplication extends Application {
|
|||||||
|
|
||||||
// Inicializa serviços centrais reais da plataforma
|
// Inicializa serviços centrais reais da plataforma
|
||||||
this.certificateManager = CertificateManager.createDefault();
|
this.certificateManager = CertificateManager.createDefault();
|
||||||
|
this.signingService = new SigningService(this.certificateManager);
|
||||||
|
|
||||||
if (developmentMode) {
|
if (developmentMode) {
|
||||||
logger.info("Modo de desenvolvimento ativo (utilizando certificados reais da máquina).");
|
logger.info("Modo de desenvolvimento ativo (utilizando certificados reais da máquina).");
|
||||||
|
|||||||
+29
-10
@@ -57,21 +57,27 @@ public class SignaturePromptDialog {
|
|||||||
|
|
||||||
private void initLayout(String appName, String origin, ManagedCertificate cert, SigningContext context,
|
private void initLayout(String appName, String origin, ManagedCertificate cert, SigningContext context,
|
||||||
String documentName, String documentType, boolean isAuthentication) {
|
String documentName, String documentType, boolean isAuthentication) {
|
||||||
VBox root = new VBox(18);
|
VBox root = new VBox(16);
|
||||||
root.getStyleClass().add("dialog-container");
|
root.getStyleClass().add("dialog-container");
|
||||||
root.setPrefWidth(540);
|
root.setPadding(new Insets(20, 24, 20, 24));
|
||||||
|
root.setPrefWidth(560);
|
||||||
|
|
||||||
HBox headerBox = new HBox(12);
|
HBox headerBox = new HBox(14);
|
||||||
headerBox.setAlignment(Pos.CENTER_LEFT);
|
headerBox.setAlignment(Pos.CENTER_LEFT);
|
||||||
|
|
||||||
VBox titleBox = new VBox(2);
|
VBox titleBox = new VBox(3);
|
||||||
Label header = new Label(isAuthentication ? "SOLICITAÇÃO DE LOGIN DIGITAL" : "SOLICITAÇÃO DE ASSINATURA");
|
HBox.setHgrow(titleBox, Priority.ALWAYS);
|
||||||
|
titleBox.setMaxWidth(Double.MAX_VALUE);
|
||||||
|
|
||||||
|
Label header = new Label(isAuthentication ? "SOLICITAÇÃO DE LOGIN" : "SOLICITAÇÃO DE ASSINATURA");
|
||||||
header.getStyleClass().add("dialog-header");
|
header.getStyleClass().add("dialog-header");
|
||||||
|
|
||||||
Label desc = new Label(isAuthentication
|
Label desc = new Label(isAuthentication
|
||||||
? "Uma aplicação web está solicitando autenticação de identidade com seu Certificado Digital ICP-Brasil."
|
? "Uma aplicação web está solicitando autenticação de identidade com seu Certificado Digital ICP-Brasil."
|
||||||
: "Uma aplicação web está solicitando a assinatura digital de um documento.");
|
: "Uma aplicação web está solicitando a assinatura digital de um documento.");
|
||||||
desc.getStyleClass().add("brand-subtitle");
|
desc.getStyleClass().add("brand-subtitle");
|
||||||
|
desc.setWrapText(true);
|
||||||
|
desc.setMaxWidth(480);
|
||||||
titleBox.getChildren().addAll(header, desc);
|
titleBox.getChildren().addAll(header, desc);
|
||||||
|
|
||||||
headerBox.getChildren().addAll(
|
headerBox.getChildren().addAll(
|
||||||
@@ -145,7 +151,8 @@ public class SignaturePromptDialog {
|
|||||||
"O PIN deste dispositivo é gerenciado diretamente pelo driver nativo (SafeSign / Windows Security). " +
|
"O PIN deste dispositivo é gerenciado diretamente pelo driver nativo (SafeSign / Windows Security). " +
|
||||||
"Caso o token ainda não esteja autenticado, a janela segura do driver será aberta automaticamente."
|
"Caso o token ainda não esteja autenticado, a janela segura do driver será aberta automaticamente."
|
||||||
);
|
);
|
||||||
winDesc.setStyle("-fx-text-fill: -fx-text-secondary; -fx-font-size: 12px;");
|
winDesc.getStyleClass().add("dialog-subtitle");
|
||||||
|
winDesc.setStyle("-fx-font-size: 12px;");
|
||||||
winDesc.setWrapText(true);
|
winDesc.setWrapText(true);
|
||||||
|
|
||||||
winBox.getChildren().addAll(winTitle, winDesc);
|
winBox.getChildren().addAll(winTitle, winDesc);
|
||||||
@@ -177,6 +184,7 @@ public class SignaturePromptDialog {
|
|||||||
if (nowVisible) {
|
if (nowVisible) {
|
||||||
cbSavePin.setSelected(true);
|
cbSavePin.setSelected(true);
|
||||||
}
|
}
|
||||||
|
stage.sizeToScene();
|
||||||
});
|
});
|
||||||
|
|
||||||
rememberedBox.getChildren().addAll(rememberedBadge, btnChangePin);
|
rememberedBox.getChildren().addAll(rememberedBadge, btnChangePin);
|
||||||
@@ -184,25 +192,26 @@ public class SignaturePromptDialog {
|
|||||||
} else {
|
} else {
|
||||||
VBox inputWrapper = new VBox(8);
|
VBox inputWrapper = new VBox(8);
|
||||||
Label pinPromptLabel = new Label("Informe o PIN do dispositivo para autorizar a chave privada:");
|
Label pinPromptLabel = new Label("Informe o PIN do dispositivo para autorizar a chave privada:");
|
||||||
pinPromptLabel.setStyle("-fx-text-fill: -fx-text-secondary; -fx-font-size: 12px;");
|
pinPromptLabel.getStyleClass().add("dialog-subtitle");
|
||||||
|
pinPromptLabel.setStyle("-fx-font-size: 12px;");
|
||||||
|
|
||||||
cbSavePin.setSelected(false);
|
cbSavePin.setSelected(false);
|
||||||
inputWrapper.getChildren().addAll(pinPromptLabel, pinField, cbSavePin);
|
inputWrapper.getChildren().addAll(pinPromptLabel, pinField, cbSavePin);
|
||||||
pinContainer.getChildren().add(inputWrapper);
|
pinContainer.getChildren().add(inputWrapper);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
detailsBox.getChildren().add(pinContainer);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Label de Alerta de Validação
|
// Label de Alerta de Validação
|
||||||
Label validationLabel = new Label();
|
Label validationLabel = new Label();
|
||||||
validationLabel.setStyle("-fx-text-fill: #f87171; -fx-font-size: 12px;");
|
validationLabel.setStyle("-fx-text-fill: #f87171; -fx-font-size: 12px;");
|
||||||
validationLabel.setVisible(false);
|
validationLabel.setVisible(false);
|
||||||
|
validationLabel.setManaged(false);
|
||||||
|
|
||||||
// Botões de Ação
|
// Botões de Ação
|
||||||
HBox actionsRow = new HBox(12);
|
HBox actionsRow = new HBox(12);
|
||||||
actionsRow.setAlignment(Pos.CENTER_RIGHT);
|
actionsRow.setAlignment(Pos.CENTER_RIGHT);
|
||||||
|
actionsRow.setPadding(new Insets(4, 0, 0, 0));
|
||||||
|
|
||||||
Button btnCancel = new Button("Cancelar");
|
Button btnCancel = new Button("Cancelar");
|
||||||
btnCancel.getStyleClass().add("btn-cancel");
|
btnCancel.getStyleClass().add("btn-cancel");
|
||||||
@@ -222,6 +231,8 @@ public class SignaturePromptDialog {
|
|||||||
if (!hasStoredPin && !pinProvided) {
|
if (!hasStoredPin && !pinProvided) {
|
||||||
validationLabel.setText("Atenção: O PIN do token é obrigatório para assinar via PKCS#11.");
|
validationLabel.setText("Atenção: O PIN do token é obrigatório para assinar via PKCS#11.");
|
||||||
validationLabel.setVisible(true);
|
validationLabel.setVisible(true);
|
||||||
|
validationLabel.setManaged(true);
|
||||||
|
stage.sizeToScene();
|
||||||
pinField.requestFocus();
|
pinField.requestFocus();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -242,12 +253,18 @@ public class SignaturePromptDialog {
|
|||||||
|
|
||||||
actionsRow.getChildren().addAll(btnCancel, btnConfirm);
|
actionsRow.getChildren().addAll(btnCancel, btnConfirm);
|
||||||
|
|
||||||
root.getChildren().addAll(headerBox, detailsBox, validationLabel, actionsRow);
|
if (isA3) {
|
||||||
|
root.getChildren().addAll(headerBox, detailsBox, pinContainer, validationLabel, actionsRow);
|
||||||
|
} else {
|
||||||
|
root.getChildren().addAll(headerBox, detailsBox, validationLabel, actionsRow);
|
||||||
|
}
|
||||||
|
|
||||||
Scene scene = new Scene(root);
|
Scene scene = new Scene(root);
|
||||||
ThemeManager.applyTheme(scene);
|
ThemeManager.applyTheme(scene);
|
||||||
|
|
||||||
stage.setScene(scene);
|
stage.setScene(scene);
|
||||||
|
stage.sizeToScene();
|
||||||
|
stage.centerOnScreen();
|
||||||
stage.setOnCloseRequest(e -> approvalFuture.complete(false));
|
stage.setOnCloseRequest(e -> approvalFuture.complete(false));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -263,6 +280,8 @@ public class SignaturePromptDialog {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public CompletableFuture<Boolean> showAndAwait() {
|
public CompletableFuture<Boolean> showAndAwait() {
|
||||||
|
stage.sizeToScene();
|
||||||
|
stage.centerOnScreen();
|
||||||
stage.show();
|
stage.show();
|
||||||
stage.toFront();
|
stage.toFront();
|
||||||
return approvalFuture;
|
return approvalFuture;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
.root {
|
.root {
|
||||||
-fx-font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
-fx-font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||||
-fx-font-size: 13px;
|
-fx-font-size: 13px;
|
||||||
|
-fx-text-secondary: #64748b;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ==========================================================================
|
/* ==========================================================================
|
||||||
@@ -17,6 +18,7 @@
|
|||||||
-fx-background: #f1f5f9;
|
-fx-background: #f1f5f9;
|
||||||
-fx-control-inner-background: #ffffff;
|
-fx-control-inner-background: #ffffff;
|
||||||
-fx-text-fill: #0f172a;
|
-fx-text-fill: #0f172a;
|
||||||
|
-fx-text-secondary: #64748b;
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme-light.cyber-root,
|
.theme-light.cyber-root,
|
||||||
@@ -438,6 +440,7 @@
|
|||||||
-fx-background: #0b0f17;
|
-fx-background: #0b0f17;
|
||||||
-fx-control-inner-background: #131c2e;
|
-fx-control-inner-background: #131c2e;
|
||||||
-fx-text-fill: #f1f5f9;
|
-fx-text-fill: #f1f5f9;
|
||||||
|
-fx-text-secondary: #94a3b8;
|
||||||
}
|
}
|
||||||
|
|
||||||
.theme-dark.cyber-root,
|
.theme-dark.cyber-root,
|
||||||
@@ -855,7 +858,7 @@
|
|||||||
|
|
||||||
/* Botões Base e Proteção contra Truncamento com Reticências */
|
/* Botões Base e Proteção contra Truncamento com Reticências */
|
||||||
.button {
|
.button {
|
||||||
-fx-min-width: -fx-pref-size;
|
-fx-min-width: -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary {
|
.btn-primary {
|
||||||
@@ -865,7 +868,6 @@
|
|||||||
-fx-padding: 8px 18px;
|
-fx-padding: 8px 18px;
|
||||||
-fx-background-radius: 6px;
|
-fx-background-radius: 6px;
|
||||||
-fx-cursor: hand;
|
-fx-cursor: hand;
|
||||||
-fx-min-width: -fx-pref-size;
|
|
||||||
-fx-alignment: center;
|
-fx-alignment: center;
|
||||||
-fx-effect: dropshadow(three-pass-box, rgba(2, 132, 199, 0.25), 4, 0, 0, 1);
|
-fx-effect: dropshadow(three-pass-box, rgba(2, 132, 199, 0.25), 4, 0, 0, 1);
|
||||||
}
|
}
|
||||||
@@ -880,7 +882,6 @@
|
|||||||
-fx-font-weight: bold;
|
-fx-font-weight: bold;
|
||||||
-fx-padding: 8px 18px;
|
-fx-padding: 8px 18px;
|
||||||
-fx-background-radius: 6px;
|
-fx-background-radius: 6px;
|
||||||
-fx-min-width: -fx-pref-size;
|
|
||||||
-fx-alignment: center;
|
-fx-alignment: center;
|
||||||
-fx-cursor: hand;
|
-fx-cursor: hand;
|
||||||
}
|
}
|
||||||
@@ -910,7 +911,6 @@
|
|||||||
-fx-text-fill: #475569;
|
-fx-text-fill: #475569;
|
||||||
-fx-font-weight: bold;
|
-fx-font-weight: bold;
|
||||||
-fx-padding: 8px 18px;
|
-fx-padding: 8px 18px;
|
||||||
-fx-min-width: -fx-pref-size;
|
|
||||||
-fx-alignment: center;
|
-fx-alignment: center;
|
||||||
-fx-cursor: hand;
|
-fx-cursor: hand;
|
||||||
}
|
}
|
||||||
@@ -933,14 +933,12 @@
|
|||||||
/* Botões Padronizados para Diálogos e Modais */
|
/* Botões Padronizados para Diálogos e Modais */
|
||||||
.btn-dialog {
|
.btn-dialog {
|
||||||
-fx-min-width: 110px;
|
-fx-min-width: 110px;
|
||||||
-fx-pref-width: -fx-pref-size;
|
|
||||||
-fx-pref-height: 34px;
|
-fx-pref-height: 34px;
|
||||||
-fx-alignment: center;
|
-fx-alignment: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-dialog-wide {
|
.btn-dialog-wide {
|
||||||
-fx-min-width: 220px;
|
-fx-min-width: 220px;
|
||||||
-fx-pref-width: -fx-pref-size;
|
|
||||||
-fx-pref-height: 36px;
|
-fx-pref-height: 36px;
|
||||||
-fx-alignment: center;
|
-fx-alignment: center;
|
||||||
}
|
}
|
||||||
@@ -949,13 +947,12 @@
|
|||||||
.btn-compact {
|
.btn-compact {
|
||||||
-fx-padding: 5px 12px;
|
-fx-padding: 5px 12px;
|
||||||
-fx-font-size: 11.5px;
|
-fx-font-size: 11.5px;
|
||||||
-fx-min-width: -fx-pref-size;
|
|
||||||
-fx-alignment: center;
|
-fx-alignment: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Botão de Perigo / Exclusão / Remoção */
|
/* Botão de Perigo / Exclusão / Remoção */
|
||||||
.btn-danger {
|
.btn-danger {
|
||||||
-fx-background-color: #fee2e2;
|
-fx-background-color: #fee2e8;
|
||||||
-fx-text-fill: #b91c1c;
|
-fx-text-fill: #b91c1c;
|
||||||
-fx-border-color: #fca5a5;
|
-fx-border-color: #fca5a5;
|
||||||
-fx-border-radius: 6px;
|
-fx-border-radius: 6px;
|
||||||
@@ -965,7 +962,6 @@
|
|||||||
-fx-font-weight: bold;
|
-fx-font-weight: bold;
|
||||||
-fx-padding: 6px 14px;
|
-fx-padding: 6px 14px;
|
||||||
-fx-alignment: center;
|
-fx-alignment: center;
|
||||||
-fx-min-width: -fx-pref-size;
|
|
||||||
-fx-cursor: hand;
|
-fx-cursor: hand;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+50
-18
@@ -667,9 +667,9 @@
|
|||||||
<!-- Configurações do Selo e da Assinatura -->
|
<!-- Configurações do Selo e da Assinatura -->
|
||||||
<div id="pdfOptionsSection" style="margin-top: 16px; display: none; background: var(--bg-surface); padding: 16px; border-radius: var(--radius-sm); border: 1px solid var(--border-subtle);">
|
<div id="pdfOptionsSection" style="margin-top: 16px; display: none; background: var(--bg-surface); padding: 16px; border-radius: var(--radius-sm); border: 1px solid var(--border-subtle);">
|
||||||
<h4 style="font-size: 13px; font-weight: 700; color: #fff; margin-bottom: 12px;">🎨 Configurações do Selo Visual</h4>
|
<h4 style="font-size: 13px; font-weight: 700; color: #fff; margin-bottom: 12px;">🎨 Configurações do Selo Visual</h4>
|
||||||
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 14px;">
|
<div style="display: grid; grid-template-columns: minmax(240px, 360px); gap: 14px;">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="form-label">Tema do Selo do Agente:</label>
|
<label class="form-label">Tema do Selo Visual:</label>
|
||||||
<select id="pdfSealThemeSelect" class="cert-bar-select">
|
<select id="pdfSealThemeSelect" class="cert-bar-select">
|
||||||
<option value="icp-brasil">ICP-Brasil Institucional (Padrão)</option>
|
<option value="icp-brasil">ICP-Brasil Institucional (Padrão)</option>
|
||||||
<option value="gov-br">Governamental (Gov.br / ITI)</option>
|
<option value="gov-br">Governamental (Gov.br / ITI)</option>
|
||||||
@@ -678,16 +678,6 @@
|
|||||||
<option value="custom">🎨 Selo Customizado do Designer</option>
|
<option value="custom">🎨 Selo Customizado do Designer</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="form-label">Razão / Motivo da Assinatura:</label>
|
|
||||||
<input type="text" id="pdfReasonInput" value="Documento aprovado e assinado digitalmente" placeholder="Ex: De acordo com os termos">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
|
||||||
<label class="form-label">Localização:</label>
|
|
||||||
<input type="text" id="pdfLocationInput" value="Curitiba, PR - Brasil" placeholder="Ex: Brasília, DF">
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="margin-top: 16px; display: flex; gap: 10px; flex-wrap: wrap;">
|
<div style="margin-top: 16px; display: flex; gap: 10px; flex-wrap: wrap;">
|
||||||
@@ -1062,6 +1052,13 @@
|
|||||||
certTypeBadge.textContent = cert.type || 'A1';
|
certTypeBadge.textContent = cert.type || 'A1';
|
||||||
certTypeBadge.className = `badge-tag ${cert.type === 'A3' ? 'badge-a3' : 'badge-a1'}`;
|
certTypeBadge.className = `badge-tag ${cert.type === 'A3' ? 'badge-a3' : 'badge-a1'}`;
|
||||||
certValidityText.textContent = `Validade: ${cert.valid_to ? new Date(cert.valid_to).toLocaleDateString() : 'N/D'}`;
|
certValidityText.textContent = `Validade: ${cert.valid_to ? new Date(cert.valid_to).toLocaleDateString() : 'N/D'}`;
|
||||||
|
|
||||||
|
if (pdfStampPicker) {
|
||||||
|
pdfStampPicker.updateSealTheme(null, {
|
||||||
|
signerName: cert.common_name || cert.alias,
|
||||||
|
signerDoc: cert.cpf_cnpj ? `CPF: ${cert.cpf_cnpj}` : (cert.type === 'A3' ? 'Token Físico A3' : 'ICP-Brasil')
|
||||||
|
});
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
certTypeBadge.style.display = 'none';
|
certTypeBadge.style.display = 'none';
|
||||||
certValidityText.textContent = '';
|
certValidityText.textContent = '';
|
||||||
@@ -1171,22 +1168,56 @@
|
|||||||
stampPickerMount.style.display = 'block';
|
stampPickerMount.style.display = 'block';
|
||||||
pdfOptionsSection.style.display = 'block';
|
pdfOptionsSection.style.display = 'block';
|
||||||
|
|
||||||
|
const activeCert = currentCertificates.find(c => c.id === selectedCertificateId);
|
||||||
|
const currentThemeVal = pdfSealThemeSelect ? pdfSealThemeSelect.value : 'icp-brasil';
|
||||||
|
const initialTheme = (currentThemeVal === 'custom' && currentCustomSealConfig)
|
||||||
|
? currentCustomSealConfig
|
||||||
|
: currentThemeVal;
|
||||||
|
|
||||||
|
const rawCertName = activeCert ? (activeCert.common_name || activeCert.alias) : '';
|
||||||
|
const cleanCertName = rawCertName.includes(':') ? rawCertName.split(':')[0].trim() : rawCertName;
|
||||||
|
const initialSignerInfo = activeCert ? {
|
||||||
|
signerName: cleanCertName,
|
||||||
|
signerDoc: activeCert.cpf_cnpj ? `CPF: ${activeCert.cpf_cnpj}` : (activeCert.type === 'A3' ? 'Token Físico A3' : 'ICP-Brasil')
|
||||||
|
} : {
|
||||||
|
signerName: 'Nome do Titular do Certificado',
|
||||||
|
signerDoc: 'CPF: ***.456.789-**'
|
||||||
|
};
|
||||||
|
|
||||||
if (!pdfStampPicker) {
|
if (!pdfStampPicker) {
|
||||||
pdfStampPicker = new PdfStampPicker({
|
pdfStampPicker = new PdfStampPicker({
|
||||||
container: stampPickerMount,
|
container: stampPickerMount,
|
||||||
pdfSource: buffer,
|
pdf: buffer,
|
||||||
|
sealTheme: initialTheme,
|
||||||
|
signerInfo: initialSignerInfo,
|
||||||
initialScale: 1.0,
|
initialScale: 1.0,
|
||||||
onPositionChange: (pos) => {
|
onPositionChange: (pos) => {
|
||||||
console.log('Posição do selo atualizada:', pos);
|
console.log('Posição do selo atualizada:', pos);
|
||||||
|
},
|
||||||
|
onSignRequest: () => {
|
||||||
|
btnSignPdfFull.click();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
pdfStampPicker.updateSealTheme(initialTheme, initialSignerInfo);
|
||||||
pdfStampPicker.loadPdf(buffer);
|
pdfStampPicker.loadPdf(buffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateButtonStates();
|
updateButtonStates();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sincroniza alteração de tema com o selo sobre o PDF
|
||||||
|
pdfSealThemeSelect.addEventListener('change', () => {
|
||||||
|
const themeVal = pdfSealThemeSelect.value;
|
||||||
|
if (pdfStampPicker) {
|
||||||
|
if (themeVal === 'custom' && currentCustomSealConfig) {
|
||||||
|
pdfStampPicker.updateSealTheme(currentCustomSealConfig);
|
||||||
|
} else {
|
||||||
|
pdfStampPicker.updateSealTheme(themeVal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Assinatura Completa de PDF (Devolve PDF Assinado Base64)
|
// Assinatura Completa de PDF (Devolve PDF Assinado Base64)
|
||||||
btnSignPdfFull.addEventListener('click', async () => {
|
btnSignPdfFull.addEventListener('click', async () => {
|
||||||
btnSignPdfFull.disabled = true;
|
btnSignPdfFull.disabled = true;
|
||||||
@@ -1201,13 +1232,11 @@
|
|||||||
|
|
||||||
const signParams = {
|
const signParams = {
|
||||||
certificateId: selectedCertificateId,
|
certificateId: selectedCertificateId,
|
||||||
pdf: currentPdfBytes,
|
pdf: currentPdfFile || currentPdfBytes,
|
||||||
page: stampConfig.page,
|
page: stampConfig.page,
|
||||||
x: stampConfig.x,
|
x: stampConfig.x,
|
||||||
y: stampConfig.y,
|
y: stampConfig.y,
|
||||||
scale: stampConfig.scale,
|
scale: stampConfig.scale
|
||||||
reason: document.getElementById('pdfReasonInput').value || 'Assinado Digitalmente',
|
|
||||||
location: document.getElementById('pdfLocationInput').value || 'Brasil'
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (selectedTheme === 'custom' && currentCustomSealConfig) {
|
if (selectedTheme === 'custom' && currentCustomSealConfig) {
|
||||||
@@ -1324,9 +1353,12 @@
|
|||||||
document.getElementById('btnApplySealToPdf').addEventListener('click', () => {
|
document.getElementById('btnApplySealToPdf').addEventListener('click', () => {
|
||||||
currentCustomSealConfig = pdfSealDesigner.getConfig();
|
currentCustomSealConfig = pdfSealDesigner.getConfig();
|
||||||
pdfSealThemeSelect.value = 'custom';
|
pdfSealThemeSelect.value = 'custom';
|
||||||
|
if (pdfStampPicker) {
|
||||||
|
pdfStampPicker.updateSealTheme(currentCustomSealConfig);
|
||||||
|
}
|
||||||
// Alterna para aba PDF
|
// Alterna para aba PDF
|
||||||
document.querySelector('[data-tab="tab-pdf"]').click();
|
document.querySelector('[data-tab="tab-pdf"]').click();
|
||||||
alert('Selo customizado selecionado para a assinatura do PDF!');
|
alert('Selo customizado selecionado e aplicado ao posicionador de PDF!');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -209,6 +209,9 @@ export class PdfSealDesigner {
|
|||||||
padding: 20px;
|
padding: 20px;
|
||||||
color: #f8fafc;
|
color: #f8fafc;
|
||||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||||
|
max-width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
.designer-header {
|
.designer-header {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
@@ -227,12 +230,13 @@ export class PdfSealDesigner {
|
|||||||
}
|
}
|
||||||
.designer-body {
|
.designer-body {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
|
||||||
gap: 24px;
|
gap: 24px;
|
||||||
|
min-width: 0;
|
||||||
}
|
}
|
||||||
@media (max-width: 860px) {
|
@media (max-width: 860px) {
|
||||||
.designer-body {
|
.designer-body {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: minmax(0, 1fr);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.form-section-title {
|
.form-section-title {
|
||||||
@@ -364,12 +368,17 @@ export class PdfSealDesigner {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
|
min-width: 0;
|
||||||
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
.preview-card, .json-export-card {
|
.preview-card, .json-export-card {
|
||||||
background: #131c31;
|
background: #131c31;
|
||||||
border: 1px solid #24304f;
|
border: 1px solid #24304f;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
|
min-width: 0;
|
||||||
|
max-width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
.preview-card h4, .json-export-card h4 {
|
.preview-card h4, .json-export-card h4 {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
@@ -385,9 +394,14 @@ export class PdfSealDesigner {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
box-shadow: 0 4px 15px rgba(0,0,0,0.3);
|
box-shadow: 0 4px 15px rgba(0,0,0,0.3);
|
||||||
|
max-width: 340px;
|
||||||
|
margin: 0 auto;
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
.seal-live-preview-box svg {
|
.seal-live-preview-box svg {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
|
width: 100%;
|
||||||
height: auto;
|
height: auto;
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
@@ -426,8 +440,22 @@ export class PdfSealDesigner {
|
|||||||
font-size: 10.5px;
|
font-size: 10.5px;
|
||||||
color: #38bdf8;
|
color: #38bdf8;
|
||||||
max-height: 180px;
|
max-height: 180px;
|
||||||
overflow: auto;
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-all;
|
||||||
|
word-wrap: break-word;
|
||||||
|
max-width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.theme-json-box code {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-all;
|
||||||
|
word-wrap: break-word;
|
||||||
|
display: block;
|
||||||
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
document.head.appendChild(style);
|
document.head.appendChild(style);
|
||||||
@@ -541,7 +569,7 @@ export class PdfSealDesigner {
|
|||||||
|
|
||||||
let iconGraphic = '';
|
let iconGraphic = '';
|
||||||
if (t.customLogoBase64) {
|
if (t.customLogoBase64) {
|
||||||
iconGraphic = `<image href="${t.customLogoBase64}" x="12" y="14" width="34" height="34" preserveAspectRatio="xMidYMid meet" />`;
|
iconGraphic = `<image xlink:href="${t.customLogoBase64}" href="${t.customLogoBase64}" x="12" y="14" width="34" height="54" preserveAspectRatio="xMidYMid meet" />`;
|
||||||
} else {
|
} else {
|
||||||
switch (t.iconStyle) {
|
switch (t.iconStyle) {
|
||||||
case 'star':
|
case 'star':
|
||||||
@@ -578,7 +606,7 @@ export class PdfSealDesigner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const svg = `
|
const svg = `
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 82" width="320" height="82" style="border-radius:6px; background:${t.backgroundColor};">
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 320 82" width="320" height="82" style="border-radius:6px; background:${t.backgroundColor};">
|
||||||
<rect x="1" y="1" width="318" height="80" rx="6" fill="${t.backgroundColor}" stroke="${t.borderColor}" stroke-width="1.5"/>
|
<rect x="1" y="1" width="318" height="80" rx="6" fill="${t.backgroundColor}" stroke="${t.borderColor}" stroke-width="1.5"/>
|
||||||
<rect x="1" y="1" width="6" height="80" rx="3" fill="${t.primaryColor}"/>
|
<rect x="1" y="1" width="6" height="80" rx="3" fill="${t.primaryColor}"/>
|
||||||
|
|
||||||
@@ -591,13 +619,20 @@ export class PdfSealDesigner {
|
|||||||
<text x="54" y="19" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="8.5" font-weight="bold" fill="${t.primaryColor}" letter-spacing="0.3">${t.customHeader || 'DOCUMENTO ASSINADO DIGITALMENTE'}</text>
|
<text x="54" y="19" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="8.5" font-weight="bold" fill="${t.primaryColor}" letter-spacing="0.3">${t.customHeader || 'DOCUMENTO ASSINADO DIGITALMENTE'}</text>
|
||||||
<text x="54" y="34" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="12" font-weight="bold" fill="${t.textColor}">MARIA DA SILVA</text>
|
<text x="54" y="34" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="12" font-weight="bold" fill="${t.textColor}">MARIA DA SILVA</text>
|
||||||
<text x="54" y="47" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="8" fill="#475569">${[docText, extraText].filter(Boolean).join(' • ')}</text>
|
<text x="54" y="47" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="8" fill="#475569">${[docText, extraText].filter(Boolean).join(' • ')}</text>
|
||||||
<text x="54" y="58" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="8" fill="#475569">${[dateText, 'AC SERPRO v5'].filter(Boolean).join(' • ')}</text>
|
<text x="54" y="58" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="8" fill="#475569">${dateText ? `Data: ${dateText}` : ''}</text>
|
||||||
<text x="54" y="70" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="7.5" fill="#64748b">${t.customFooter || 'ICP-Brasil • MP nº 2.200-2/2001'}</text>
|
<text x="54" y="70" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="7.5" fill="#64748b">${t.customFooter || 'ICP-Brasil • MP nº 2.200-2/2001'}</text>
|
||||||
</svg>
|
</svg>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
this.previewBox.innerHTML = svg;
|
this.previewBox.innerHTML = svg;
|
||||||
this.jsonBox.textContent = JSON.stringify(this.getThemeConfig(), null, 2);
|
|
||||||
|
// Na visualização do código, trunca se o Base64 for gigante para preservar performance da UI
|
||||||
|
const displayConfig = { ...this.getThemeConfig() };
|
||||||
|
if (displayConfig.customLogoBase64 && displayConfig.customLogoBase64.length > 100) {
|
||||||
|
const lenKb = (displayConfig.customLogoBase64.length / 1024).toFixed(1);
|
||||||
|
displayConfig.customLogoBase64 = `${displayConfig.customLogoBase64.substring(0, 32)}... [${lenKb} KB]`;
|
||||||
|
}
|
||||||
|
this.jsonBox.textContent = JSON.stringify(displayConfig, null, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -625,6 +660,13 @@ export class PdfSealDesigner {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Alias conveniente para getThemeConfig().
|
||||||
|
*/
|
||||||
|
getConfig() {
|
||||||
|
return this.getThemeConfig();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Carrega um tema para edição.
|
* Carrega um tema para edição.
|
||||||
*/
|
*/
|
||||||
|
|||||||
+234
-71
@@ -25,7 +25,11 @@ export class PdfStampPicker {
|
|||||||
|
|
||||||
this.onPositionChange = options.onPositionChange || null;
|
this.onPositionChange = options.onPositionChange || null;
|
||||||
this.onSignRequest = options.onSignRequest || null;
|
this.onSignRequest = options.onSignRequest || null;
|
||||||
this.sealTheme = options.sealTheme || null;
|
this.sealTheme = options.sealTheme || 'icp-brasil';
|
||||||
|
this.signerInfo = options.signerInfo || {
|
||||||
|
signerName: 'Nome do Titular do Certificado',
|
||||||
|
signerDoc: 'CPF: ***.456.789-**'
|
||||||
|
};
|
||||||
|
|
||||||
// Estado interno
|
// Estado interno
|
||||||
this.pdfDocument = null;
|
this.pdfDocument = null;
|
||||||
@@ -42,8 +46,8 @@ export class PdfStampPicker {
|
|||||||
|
|
||||||
// Estado do selo
|
// Estado do selo
|
||||||
this.sealScale = options.initialScale || 1.0;
|
this.sealScale = options.initialScale || 1.0;
|
||||||
this.baseSealWidth = 230; // pixels base no visualizador
|
this.baseSealWidth = 240; // Proporção 320x82 pt
|
||||||
this.baseSealHeight = 60;
|
this.baseSealHeight = 61.5;
|
||||||
this.sealPosX = null; // pixel relativo ao container da folha
|
this.sealPosX = null; // pixel relativo ao container da folha
|
||||||
this.sealPosY = null;
|
this.sealPosY = null;
|
||||||
this.isDragging = false;
|
this.isDragging = false;
|
||||||
@@ -98,20 +102,9 @@ export class PdfStampPicker {
|
|||||||
<div class="picker-sheet-wrapper">
|
<div class="picker-sheet-wrapper">
|
||||||
<div class="sheet-container">
|
<div class="sheet-container">
|
||||||
<canvas class="pdf-canvas"></canvas>
|
<canvas class="pdf-canvas"></canvas>
|
||||||
<!-- Selo Visual Interativo (Drag & Drop) -->
|
<!-- Selo Visual Interativo Oficial (Drag & Drop) -->
|
||||||
<div class="stamp-overlay" style="display:none;">
|
<div class="stamp-overlay" style="display:none;" title="Clique e arraste para posicionar o selo na folha">
|
||||||
<div class="stamp-content">
|
<div class="stamp-svg-container"></div>
|
||||||
<div class="stamp-icon-box">
|
|
||||||
<svg viewBox="0 0 24 24" class="stamp-svg-icon" fill="currentColor">
|
|
||||||
<path d="M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4zm-1 6h2v2h-2V7zm0 4h2v6h-2v-6z"/>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div class="stamp-info-box">
|
|
||||||
<div class="stamp-badge-text">ASSINADO DIGITALMENTE • ICP-BRASIL</div>
|
|
||||||
<div class="stamp-signer-name">NOME DO TITULAR DO CERTIFICADO</div>
|
|
||||||
<div class="stamp-signer-meta">CPF: ***.456.789-** • VALIDADE JURÍDICA</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="stamp-resize-handle" title="Arraste para redimensionar o selo"></div>
|
<div class="stamp-resize-handle" title="Arraste para redimensionar o selo"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -208,6 +201,8 @@ export class PdfStampPicker {
|
|||||||
this.payloadCode = this.root.querySelector('.payload-code-box code');
|
this.payloadCode = this.root.querySelector('.payload-code-box code');
|
||||||
this.btnCopyPayload = this.root.querySelector('.btn-copy-payload');
|
this.btnCopyPayload = this.root.querySelector('.btn-copy-payload');
|
||||||
this.btnTriggerSign = this.root.querySelector('.btn-trigger-sign');
|
this.btnTriggerSign = this.root.querySelector('.btn-trigger-sign');
|
||||||
|
|
||||||
|
this._renderSealGraphic();
|
||||||
}
|
}
|
||||||
|
|
||||||
_injectStyles() {
|
_injectStyles() {
|
||||||
@@ -291,21 +286,22 @@ export class PdfStampPicker {
|
|||||||
}
|
}
|
||||||
.picker-body {
|
.picker-body {
|
||||||
display: flex;
|
display: flex;
|
||||||
min-height: 520px;
|
min-height: 560px;
|
||||||
height: 600px;
|
height: 680px;
|
||||||
}
|
}
|
||||||
.picker-viewport {
|
.picker-viewport {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
background: #060911;
|
background: #060911;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
padding: 30px;
|
||||||
align-items: center;
|
display: block;
|
||||||
justify-content: center;
|
box-sizing: border-box;
|
||||||
padding: 24px;
|
|
||||||
}
|
}
|
||||||
.picker-sheet-wrapper {
|
.picker-sheet-wrapper {
|
||||||
display: none;
|
display: none;
|
||||||
|
margin: 0 auto;
|
||||||
|
width: fit-content;
|
||||||
box-shadow: 0 12px 35px rgba(0,0,0,0.65);
|
box-shadow: 0 12px 35px rgba(0,0,0,0.65);
|
||||||
border: 1px solid rgba(255,255,255,0.1);
|
border: 1px solid rgba(255,255,255,0.1);
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
@@ -322,64 +318,53 @@ export class PdfStampPicker {
|
|||||||
}
|
}
|
||||||
.stamp-overlay {
|
.stamp-overlay {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 230px;
|
width: 240px;
|
||||||
height: 60px;
|
height: 61.5px;
|
||||||
border: 2px dashed #0284c7;
|
border: 1.5px solid #0284c7;
|
||||||
background: rgba(13, 71, 161, 0.08);
|
background: transparent;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
box-shadow: 0 4px 15px rgba(0,0,0,0.25);
|
box-shadow: 0 4px 18px rgba(0,0,0,0.3);
|
||||||
cursor: grab;
|
cursor: grab;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
z-index: 20;
|
z-index: 20;
|
||||||
|
user-select: none;
|
||||||
transition: box-shadow 0.2s, border-color 0.2s;
|
transition: box-shadow 0.2s, border-color 0.2s;
|
||||||
}
|
}
|
||||||
|
.stamp-overlay:hover {
|
||||||
|
border-color: #38bdf8;
|
||||||
|
box-shadow: 0 6px 22px rgba(56, 189, 248, 0.4);
|
||||||
|
}
|
||||||
.stamp-overlay:active {
|
.stamp-overlay:active {
|
||||||
cursor: grabbing;
|
cursor: grabbing;
|
||||||
border-color: #38bdf8;
|
border-color: #0ea5e9;
|
||||||
box-shadow: 0 6px 20px rgba(56, 189, 248, 0.4);
|
box-shadow: 0 8px 26px rgba(14, 165, 233, 0.55);
|
||||||
}
|
}
|
||||||
.stamp-content {
|
.stamp-svg-container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
display: block;
|
||||||
align-items: center;
|
|
||||||
padding: 4px 8px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
gap: 8px;
|
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
.stamp-icon-box {
|
.stamp-svg-container svg {
|
||||||
width: 34px;
|
|
||||||
height: 34px;
|
|
||||||
color: #0d47a1;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
.stamp-svg-icon {
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
display: block;
|
||||||
|
border-radius: 6px;
|
||||||
}
|
}
|
||||||
.stamp-info-box {
|
.stamp-resize-handle {
|
||||||
display: flex;
|
position: absolute;
|
||||||
flex-direction: column;
|
bottom: 0;
|
||||||
justify-content: center;
|
right: 0;
|
||||||
line-height: 1.15;
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
background: linear-gradient(135deg, transparent 50%, #0284c7 50%);
|
||||||
|
cursor: se-resize;
|
||||||
|
z-index: 25;
|
||||||
|
border-bottom-right-radius: 5px;
|
||||||
}
|
}
|
||||||
.stamp-badge-text {
|
.stamp-resize-handle:hover {
|
||||||
font-size: 7.5px;
|
background: linear-gradient(135deg, transparent 50%, #38bdf8 50%);
|
||||||
font-weight: 800;
|
|
||||||
color: #0d47a1;
|
|
||||||
letter-spacing: 0.3px;
|
|
||||||
}
|
|
||||||
.stamp-signer-name {
|
|
||||||
font-size: 10px;
|
|
||||||
font-weight: 800;
|
|
||||||
color: #0f172a;
|
|
||||||
margin: 1px 0;
|
|
||||||
}
|
|
||||||
.stamp-signer-meta {
|
|
||||||
font-size: 7.5px;
|
|
||||||
color: #475569;
|
|
||||||
}
|
}
|
||||||
.picker-empty-state {
|
.picker-empty-state {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
@@ -676,14 +661,16 @@ export class PdfStampPicker {
|
|||||||
this.pdfBase64 = this._bufferToBase64(arrayBuffer);
|
this.pdfBase64 = this._bufferToBase64(arrayBuffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.pdfBytes = new Uint8Array(arrayBuffer);
|
this.pdfBytes = new Uint8Array(arrayBuffer.slice(0));
|
||||||
this.docNameLabel.textContent = this.documentName;
|
this.docNameLabel.textContent = this.documentName;
|
||||||
this.docNameLabel.title = this.documentName;
|
this.docNameLabel.title = this.documentName;
|
||||||
|
|
||||||
// Garante suporte ao PDF.js
|
// Garante suporte ao PDF.js
|
||||||
await this._ensurePdfJsLoaded();
|
await this._ensurePdfJsLoaded();
|
||||||
|
|
||||||
const loadingTask = window.pdfjsLib.getDocument({ data: this.pdfBytes });
|
// Envia uma fatia clonada ao Worker do PDF.js para não descolar (detach) o buffer original
|
||||||
|
const workerData = new Uint8Array(arrayBuffer.slice(0));
|
||||||
|
const loadingTask = window.pdfjsLib.getDocument({ data: workerData });
|
||||||
this.pdfDocument = await loadingTask.promise;
|
this.pdfDocument = await loadingTask.promise;
|
||||||
this.totalPages = this.pdfDocument.numPages;
|
this.totalPages = this.pdfDocument.numPages;
|
||||||
this.totalPagesLabel.textContent = this.totalPages;
|
this.totalPagesLabel.textContent = this.totalPages;
|
||||||
@@ -740,6 +727,15 @@ export class PdfStampPicker {
|
|||||||
const ctx = this.canvas.getContext('2d');
|
const ctx = this.canvas.getContext('2d');
|
||||||
await page.render({ canvasContext: ctx, viewport: viewport }).promise;
|
await page.render({ canvasContext: ctx, viewport: viewport }).promise;
|
||||||
|
|
||||||
|
// Ajusta o tamanho base do selo (320x82 pt) proporcionalmente aos pixels da tela
|
||||||
|
const pixelScale = viewport.width / this.pagePdfWidth;
|
||||||
|
this.baseSealWidth = 320 * pixelScale;
|
||||||
|
this.baseSealHeight = 82 * pixelScale;
|
||||||
|
|
||||||
|
const stampW = this.baseSealWidth * this.sealScale;
|
||||||
|
const stampH = this.baseSealHeight * this.sealScale;
|
||||||
|
this.stampOverlay.style.width = `${stampW}px`;
|
||||||
|
this.stampOverlay.style.height = `${stampH}px`;
|
||||||
this.stampOverlay.style.display = 'block';
|
this.stampOverlay.style.display = 'block';
|
||||||
|
|
||||||
// Recalcula limites se já houver posição definida
|
// Recalcula limites se já houver posição definida
|
||||||
@@ -881,9 +877,7 @@ export class PdfStampPicker {
|
|||||||
scale: Number(this.sealScale.toFixed(2)),
|
scale: Number(this.sealScale.toFixed(2)),
|
||||||
visible_seal: true,
|
visible_seal: true,
|
||||||
seal_theme_id: this.sealTheme?.id || 'icp-brasil',
|
seal_theme_id: this.sealTheme?.id || 'icp-brasil',
|
||||||
seal_custom: this.sealTheme || null,
|
seal_custom: typeof this.sealTheme === 'object' ? this.sealTheme : null,
|
||||||
reason: 'Assinatura Digital ICP-Brasil',
|
|
||||||
location: 'Brasil',
|
|
||||||
pdf: this.pdfBase64 ? `${this.pdfBase64.substring(0, 32)}...[${this.pdfBytes?.length || 0} bytes]` : null
|
pdf: this.pdfBase64 ? `${this.pdfBase64.substring(0, 32)}...[${this.pdfBytes?.length || 0} bytes]` : null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -904,13 +898,182 @@ export class PdfStampPicker {
|
|||||||
scale: Number(this.sealScale.toFixed(2)),
|
scale: Number(this.sealScale.toFixed(2)),
|
||||||
visible_seal: true,
|
visible_seal: true,
|
||||||
seal_theme_id: this.sealTheme?.id || 'icp-brasil',
|
seal_theme_id: this.sealTheme?.id || 'icp-brasil',
|
||||||
seal_custom: this.sealTheme || null,
|
seal_custom: typeof this.sealTheme === 'object' ? this.sealTheme : null,
|
||||||
reason: 'Assinatura Digital ICP-Brasil',
|
|
||||||
location: 'Brasil',
|
|
||||||
pdf: this.pdfBase64
|
pdf: this.pdfBase64
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retorna os dados geométricos essenciais do posicionamento do selo.
|
||||||
|
*/
|
||||||
|
getStampConfig() {
|
||||||
|
return {
|
||||||
|
page: this.currentPageNumber,
|
||||||
|
x: this.calculatedPdfX,
|
||||||
|
y: this.calculatedPdfY,
|
||||||
|
scale: Number(this.sealScale.toFixed(2)),
|
||||||
|
position: this.selectedPositionEnum
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Atualiza o tema visual ou os dados do titular estampados no selo em tempo real.
|
||||||
|
* @param {string|Object} [theme] ID do tema ('icp-brasil', 'gov-br', etc.) ou objeto completo de tema
|
||||||
|
* @param {Object} [signerInfo] Dados do titular: { signerName, signerDoc }
|
||||||
|
*/
|
||||||
|
updateSealTheme(theme, signerInfo) {
|
||||||
|
if (theme) {
|
||||||
|
this.sealTheme = theme;
|
||||||
|
}
|
||||||
|
if (signerInfo) {
|
||||||
|
this.signerInfo = { ...(this.signerInfo || {}), ...signerInfo };
|
||||||
|
}
|
||||||
|
this._renderSealGraphic();
|
||||||
|
}
|
||||||
|
|
||||||
|
_renderSealGraphic() {
|
||||||
|
const theme = typeof this.sealTheme === 'object' && this.sealTheme !== null
|
||||||
|
? this.sealTheme
|
||||||
|
: this._getPresetTheme(this.sealTheme);
|
||||||
|
|
||||||
|
const signer = this.signerInfo || {};
|
||||||
|
const rawName = signer.signerName || 'Nome do Titular do Certificado';
|
||||||
|
// Remove sufixo :CPF ou :CNPJ para exibir apenas o nome limpo do titular, idêntico ao assinador Java
|
||||||
|
const name = rawName.includes(':') ? rawName.split(':')[0].trim() : rawName.trim();
|
||||||
|
const doc = signer.signerDoc || 'CPF: ***.456.789-**';
|
||||||
|
const dateText = new Date().toLocaleDateString('pt-BR') + ' ' + new Date().toLocaleTimeString('pt-BR');
|
||||||
|
|
||||||
|
// Ajusta o tamanho da fonte proporcionalmente ao comprimento do nome
|
||||||
|
let nameFontSize = 11.5;
|
||||||
|
if (name.length > 36) nameFontSize = 8.8;
|
||||||
|
else if (name.length > 26) nameFontSize = 10.0;
|
||||||
|
|
||||||
|
let iconGraphic = '';
|
||||||
|
if (theme.customLogoBase64) {
|
||||||
|
iconGraphic = `<image xlink:href="${theme.customLogoBase64}" href="${theme.customLogoBase64}" x="12" y="14" width="34" height="54" preserveAspectRatio="xMidYMid meet" />`;
|
||||||
|
} else {
|
||||||
|
switch (theme.iconStyle || theme.id) {
|
||||||
|
case 'star':
|
||||||
|
case 'gov-br':
|
||||||
|
iconGraphic = `
|
||||||
|
<circle cx="28" cy="30" r="15" fill="${theme.primaryColor}" opacity="0.12" />
|
||||||
|
<circle cx="28" cy="30" r="15" fill="none" stroke="${theme.primaryColor}" stroke-width="1.5" />
|
||||||
|
<path d="M28 18 L31 26 L39 26 L33 31 L35 39 L28 34 L21 39 L23 31 L17 26 L25 26 Z" fill="${theme.secondaryColor || '#fbbf24'}" />
|
||||||
|
`;
|
||||||
|
break;
|
||||||
|
case 'crystal':
|
||||||
|
case 'cybercore-modern':
|
||||||
|
iconGraphic = `
|
||||||
|
<polygon points="28,14 42,22 42,38 28,46 14,38 14,22" fill="${theme.primaryColor}" opacity="0.15" />
|
||||||
|
<polygon points="28,14 42,22 42,38 28,46 14,38 14,22" fill="none" stroke="${theme.primaryColor}" stroke-width="1.5" />
|
||||||
|
<line x1="28" y1="14" x2="28" y2="46" stroke="${theme.secondaryColor || '#10b981'}" stroke-width="1.5" />
|
||||||
|
`;
|
||||||
|
break;
|
||||||
|
case 'notary':
|
||||||
|
case 'minimal-clean':
|
||||||
|
iconGraphic = `
|
||||||
|
<circle cx="28" cy="30" r="16" fill="none" stroke="${theme.primaryColor}" stroke-width="2" stroke-dasharray="3,2" />
|
||||||
|
<circle cx="28" cy="30" r="12" fill="${theme.primaryColor}" opacity="0.1" />
|
||||||
|
<path d="M22 30 L26 34 L34 26" fill="none" stroke="${theme.secondaryColor || '#64748b'}" stroke-width="2" stroke-linecap="round" />
|
||||||
|
`;
|
||||||
|
break;
|
||||||
|
case 'shield':
|
||||||
|
case 'icp-brasil':
|
||||||
|
default:
|
||||||
|
iconGraphic = `
|
||||||
|
<path d="M28 12 L40 17 V28 C40 38 33 46 28 49 C23 46 16 38 16 28 V17 Z" fill="${theme.primaryColor}" opacity="0.12" />
|
||||||
|
<path d="M28 12 L40 17 V28 C40 38 33 46 28 49 C23 46 16 38 16 28 V17 Z" fill="none" stroke="${theme.primaryColor}" stroke-width="1.8" />
|
||||||
|
<rect x="23" y="27" width="10" height="8" rx="2" fill="${theme.secondaryColor || theme.primaryColor}" />
|
||||||
|
<path d="M25 27 V23.5 C25 21.8 26.3 20.5 28 20.5 C29.7 20.5 31 21.8 31 23.5 V27" fill="none" stroke="${theme.secondaryColor || theme.primaryColor}" stroke-width="1.5" />
|
||||||
|
`;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const header = theme.customHeader || (theme.id === 'cybercore-modern' ? 'CYBERCORE SIGNER • ASSINATURA ELETRÔNICA' : (theme.id === 'minimal-clean' ? 'Assinado Digitalmente' : 'DOCUMENTO ASSINADO DIGITALMENTE'));
|
||||||
|
const footer = theme.customFooter || (theme.id === 'minimal-clean' ? 'Conforme MP 2.200-2/2001 e ICP-Brasil' : 'ICP-Brasil • MP nº 2.200-2/2001');
|
||||||
|
|
||||||
|
const svg = `
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 320 82" width="100%" height="100%" style="background:${theme.backgroundColor || '#ffffff'}; border-radius:6px;">
|
||||||
|
<rect x="1" y="1" width="318" height="80" rx="6" fill="${theme.backgroundColor || '#ffffff'}" stroke="${theme.borderColor || '#cbd5e1'}" stroke-width="1.5"/>
|
||||||
|
<rect x="1" y="1" width="6" height="80" rx="3" fill="${theme.primaryColor || '#0d47a1'}"/>
|
||||||
|
|
||||||
|
<!-- Emblema / Brasão -->
|
||||||
|
<g id="seal-icon-group">
|
||||||
|
${iconGraphic}
|
||||||
|
</g>
|
||||||
|
|
||||||
|
<!-- Tipografia do Selo -->
|
||||||
|
<text x="54" y="19" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="8.5" font-weight="bold" fill="${theme.primaryColor || '#0d47a1'}" letter-spacing="0.3">${header}</text>
|
||||||
|
<text x="54" y="34" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="${nameFontSize}" font-weight="bold" fill="${theme.textColor || '#0f172a'}">${name}</text>
|
||||||
|
<text x="54" y="47" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="8" fill="#475569">${doc}</text>
|
||||||
|
<text x="54" y="58" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="8" fill="#475569">Data: ${dateText}</text>
|
||||||
|
<text x="54" y="70" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="7.5" fill="#64748b">${footer}</text>
|
||||||
|
</svg>
|
||||||
|
`;
|
||||||
|
|
||||||
|
if (this.stampOverlay) {
|
||||||
|
const container = this.stampOverlay.querySelector('.stamp-svg-container');
|
||||||
|
if (container) {
|
||||||
|
container.innerHTML = svg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_getPresetTheme(themeId) {
|
||||||
|
switch (themeId) {
|
||||||
|
case 'gov-br':
|
||||||
|
return {
|
||||||
|
id: 'gov-br',
|
||||||
|
primaryColor: '#005696',
|
||||||
|
secondaryColor: '#ffcc00',
|
||||||
|
backgroundColor: '#ffffff',
|
||||||
|
borderColor: '#93c5fd',
|
||||||
|
textColor: '#0f172a',
|
||||||
|
iconStyle: 'star',
|
||||||
|
customHeader: 'DOCUMENTO ASSINADO DIGITALMENTE • GOV.BR',
|
||||||
|
customFooter: 'Conforme Decreto nº 10.543/2020 e ICP-Brasil'
|
||||||
|
};
|
||||||
|
case 'cybercore-modern':
|
||||||
|
return {
|
||||||
|
id: 'cybercore-modern',
|
||||||
|
primaryColor: '#0284c7',
|
||||||
|
secondaryColor: '#10b981',
|
||||||
|
backgroundColor: '#ffffff',
|
||||||
|
borderColor: '#38bdf8',
|
||||||
|
textColor: '#0f172a',
|
||||||
|
iconStyle: 'crystal',
|
||||||
|
customHeader: 'CYBERCORE SIGNER • ASSINATURA ELETRÔNICA',
|
||||||
|
customFooter: 'Validação criptográfica com fé pública'
|
||||||
|
};
|
||||||
|
case 'minimal-clean':
|
||||||
|
return {
|
||||||
|
id: 'minimal-clean',
|
||||||
|
primaryColor: '#334155',
|
||||||
|
secondaryColor: '#64748b',
|
||||||
|
backgroundColor: '#fafafa',
|
||||||
|
borderColor: '#cbd5e1',
|
||||||
|
textColor: '#1e293b',
|
||||||
|
iconStyle: 'notary',
|
||||||
|
customHeader: 'Assinado Digitalmente',
|
||||||
|
customFooter: 'Conforme MP 2.200-2/2001 e ICP-Brasil'
|
||||||
|
};
|
||||||
|
case 'icp-brasil':
|
||||||
|
default:
|
||||||
|
return {
|
||||||
|
id: 'icp-brasil',
|
||||||
|
primaryColor: '#0d47a1',
|
||||||
|
secondaryColor: '#0284c7',
|
||||||
|
backgroundColor: '#ffffff',
|
||||||
|
borderColor: '#93c5fd',
|
||||||
|
textColor: '#0f172a',
|
||||||
|
iconStyle: 'shield',
|
||||||
|
customHeader: 'ASSINADO DIGITALMENTE • ICP-BRASIL',
|
||||||
|
customFooter: 'ICP-Brasil • MP nº 2.200-2/2001'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_bufferToBase64(buffer) {
|
_bufferToBase64(buffer) {
|
||||||
const bytes = new Uint8Array(buffer);
|
const bytes = new Uint8Array(buffer);
|
||||||
let binary = '';
|
let binary = '';
|
||||||
|
|||||||
+64
-1
@@ -691,8 +691,71 @@ export class CyberCoreSigner {
|
|||||||
// Métodos Utilitários Internos
|
// Métodos Utilitários Internos
|
||||||
// =========================================================================
|
// =========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executa requisições HTTP REST ao servidor local CCSP/1.
|
||||||
|
*
|
||||||
|
* @param {string} endpoint Caminho do endpoint (ex: '/ccsp/v1/status').
|
||||||
|
* @param {Object} [options={}] Opções do fetch (method, body, timeoutMs, etc).
|
||||||
|
* @returns {Promise<Object>} Resposta JSON decodificada.
|
||||||
|
*/
|
||||||
|
async _fetch(endpoint, options = {}) {
|
||||||
|
const url = `${this.baseUrl}${endpoint}`;
|
||||||
|
const timeout = options.timeoutMs || 15000;
|
||||||
|
const controller = typeof AbortController !== 'undefined' ? new AbortController() : null;
|
||||||
|
const timeoutId = controller ? setTimeout(() => controller.abort(), timeout) : null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const headers = {
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'X-CyberCore-AppId': this.applicationId,
|
||||||
|
...(options.headers || {})
|
||||||
|
};
|
||||||
|
|
||||||
|
if (options.body && !headers['Content-Type']) {
|
||||||
|
headers['Content-Type'] = 'application/json';
|
||||||
|
}
|
||||||
|
|
||||||
|
const fetchOptions = {
|
||||||
|
...options,
|
||||||
|
headers
|
||||||
|
};
|
||||||
|
|
||||||
|
if (controller) {
|
||||||
|
fetchOptions.signal = controller.signal;
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await fetch(url, fetchOptions);
|
||||||
|
const data = await res.json().catch(() => ({}));
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
const errorCode = data.error_code || `HTTP_${res.status}`;
|
||||||
|
const message = data.message || `Erro retornado pelo agente (${res.status})`;
|
||||||
|
this._throwMappedError(errorCode, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
} catch (err) {
|
||||||
|
if (err.name === 'AbortError') {
|
||||||
|
throw new CyberCoreError(`Tempo limite esgotado (${timeout}ms) na comunicação com o agente.`, 'TIMEOUT_ERROR');
|
||||||
|
}
|
||||||
|
if (err instanceof CyberCoreError) {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
throw new AgentNotRunningError(`Falha ao conectar com o CyberCore Signer em ${this.baseUrl}: ${err.message}`);
|
||||||
|
} finally {
|
||||||
|
if (timeoutId) {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async _fileToBuffer(file) {
|
async _fileToBuffer(file) {
|
||||||
if (file instanceof ArrayBuffer) return file;
|
if (file instanceof ArrayBuffer) {
|
||||||
|
if (file.byteLength === 0) {
|
||||||
|
throw new Error('O buffer binário está vazio ou foi desacoplado (detached). Forneça o objeto File ou Blob original.');
|
||||||
|
}
|
||||||
|
return file;
|
||||||
|
}
|
||||||
if (ArrayBuffer.isView(file)) {
|
if (ArrayBuffer.isView(file)) {
|
||||||
return file.buffer.slice(file.byteOffset, file.byteOffset + file.byteLength);
|
return file.buffer.slice(file.byteOffset, file.byteOffset + file.byteLength);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user