fix(pdf): co-assinatura incremental com preservação de assinaturas anteriores e trava anti-salto no redimensionamento
This commit is contained in:
+23
-16
@@ -195,18 +195,16 @@ public class PdfSigningService {
|
|||||||
effectiveExtraInfo
|
effectiveExtraInfo
|
||||||
);
|
);
|
||||||
|
|
||||||
// 4. Carrega o PDF e estampa o selo se solicitado
|
// 4. Carrega o PDF original uma única vez, estampa o selo se solicitado
|
||||||
byte[] intermediatePdf;
|
// e aplica a nova assinatura PAdES via atualização incremental (Incremental Update),
|
||||||
|
// preservando intactas quaisquer assinaturas digitais pré-existentes.
|
||||||
|
ByteArrayOutputStream finalOut = new ByteArrayOutputStream();
|
||||||
|
try (PDDocument doc = Loader.loadPDF(pdfBytes)) {
|
||||||
if (options.visibleSeal()) {
|
if (options.visibleSeal()) {
|
||||||
byte[] sealPng = PdfSealRenderer.renderToPngBytes(theme, sealData);
|
byte[] sealPng = PdfSealRenderer.renderToPngBytes(theme, sealData);
|
||||||
intermediatePdf = applyVisualSealToPdf(pdfBytes, sealPng, theme, options);
|
applyVisualSealToDocument(doc, sealPng, theme, options);
|
||||||
} else {
|
|
||||||
intermediatePdf = pdfBytes;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Aplica a assinatura PAdES criptográfica via PDFBox e Bouncy Castle
|
|
||||||
ByteArrayOutputStream finalOut = new ByteArrayOutputStream();
|
|
||||||
try (PDDocument doc = Loader.loadPDF(intermediatePdf)) {
|
|
||||||
PDSignature signature = new PDSignature();
|
PDSignature signature = new PDSignature();
|
||||||
signature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE);
|
signature.setFilter(PDSignature.FILTER_ADOBE_PPKLITE);
|
||||||
signature.setSubFilter(PDSignature.SUBFILTER_ADBE_PKCS7_DETACHED);
|
signature.setSubFilter(PDSignature.SUBFILTER_ADBE_PKCS7_DETACHED);
|
||||||
@@ -224,13 +222,11 @@ public class PdfSigningService {
|
|||||||
return finalOut.toByteArray();
|
return finalOut.toByteArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
private byte[] applyVisualSealToPdf(byte[] pdfBytes, byte[] sealPng, PdfSealTheme theme,
|
private void applyVisualSealToDocument(PDDocument doc, byte[] sealPng, PdfSealTheme theme,
|
||||||
PdfSignatureOptions options) throws IOException {
|
PdfSignatureOptions options) throws IOException {
|
||||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
|
||||||
try (PDDocument doc = Loader.loadPDF(pdfBytes)) {
|
|
||||||
int totalPages = doc.getNumberOfPages();
|
int totalPages = doc.getNumberOfPages();
|
||||||
if (totalPages == 0) {
|
if (totalPages == 0) {
|
||||||
return pdfBytes;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
PDImageXObject pdImage = PDImageXObject.createFromByteArray(doc, sealPng, "cybercore_seal");
|
PDImageXObject pdImage = PDImageXObject.createFromByteArray(doc, sealPng, "cybercore_seal");
|
||||||
@@ -261,8 +257,8 @@ public class PdfSigningService {
|
|||||||
cs.drawImage(pdImage, x, y, sealWidth, sealHeight);
|
cs.drawImage(pdImage, x, y, sealWidth, sealHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
doc.save(baos);
|
markPageAsUpdated(page);
|
||||||
return baos.toByteArray();
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Caso contrário, usa posicionamento por enum (BOTTOM_RIGHT, etc.)
|
// Caso contrário, usa posicionamento por enum (BOTTOM_RIGHT, etc.)
|
||||||
@@ -306,11 +302,22 @@ public class PdfSigningService {
|
|||||||
try (PDPageContentStream cs = new PDPageContentStream(doc, page, PDPageContentStream.AppendMode.APPEND, true, true)) {
|
try (PDPageContentStream cs = new PDPageContentStream(doc, page, PDPageContentStream.AppendMode.APPEND, true, true)) {
|
||||||
cs.drawImage(pdImage, x, y, sealWidth, sealHeight);
|
cs.drawImage(pdImage, x, y, sealWidth, sealHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
markPageAsUpdated(page);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
doc.save(baos);
|
private void markPageAsUpdated(PDPage page) {
|
||||||
|
if (page != null && page.getCOSObject() != null) {
|
||||||
|
page.getCOSObject().setNeedToBeUpdated(true);
|
||||||
|
if (page.getResources() != null && page.getResources().getCOSObject() != null) {
|
||||||
|
page.getResources().getCOSObject().setNeedToBeUpdated(true);
|
||||||
|
}
|
||||||
|
org.apache.pdfbox.cos.COSBase contents = page.getCOSObject().getDictionaryObject(org.apache.pdfbox.cos.COSName.CONTENTS);
|
||||||
|
if (contents instanceof org.apache.pdfbox.cos.COSUpdateInfo updateInfo) {
|
||||||
|
updateInfo.setNeedToBeUpdated(true);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return baos.toByteArray();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String bytesToHex(byte[] bytes) {
|
private static String bytesToHex(byte[] bytes) {
|
||||||
|
|||||||
+76
-2
@@ -18,6 +18,8 @@ import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
|
|||||||
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
|
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
|
||||||
import org.bouncycastle.operator.ContentSigner;
|
import org.bouncycastle.operator.ContentSigner;
|
||||||
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
|
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
|
||||||
|
import br.com.cybercore.signer.core.validation.SignatureValidationService;
|
||||||
|
import br.com.cybercore.signer.core.validation.ValidationResult;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
import org.junit.jupiter.api.DisplayName;
|
import org.junit.jupiter.api.DisplayName;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
@@ -41,6 +43,7 @@ class PdfSigningServiceTest {
|
|||||||
private CertificateManager certificateManager;
|
private CertificateManager certificateManager;
|
||||||
private PdfSigningService pdfSigningService;
|
private PdfSigningService pdfSigningService;
|
||||||
private String testCertId;
|
private String testCertId;
|
||||||
|
private String testCert2Id;
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void setUp() throws Exception {
|
void setUp() throws Exception {
|
||||||
@@ -74,13 +77,29 @@ class PdfSigningServiceTest {
|
|||||||
char[] pwd = "password123".toCharArray();
|
char[] pwd = "password123".toCharArray();
|
||||||
ks.setKeyEntry("test-cert", kp.getPrivate(), pwd, new Certificate[]{cert});
|
ks.setKeyEntry("test-cert", kp.getPrivate(), pwd, new Certificate[]{cert});
|
||||||
|
|
||||||
|
// Gera segundo par de chaves e certificado para co-assinatura
|
||||||
|
KeyPair kp2 = kpg.generateKeyPair();
|
||||||
|
X500Name subject2 = new X500Name("CN=Maria Souza:98765432100, O=CyberCore Testes, C=BR");
|
||||||
|
X509v3CertificateBuilder certBuilder2 = new JcaX509v3CertificateBuilder(
|
||||||
|
subject2,
|
||||||
|
BigInteger.valueOf(System.currentTimeMillis() + 100),
|
||||||
|
notBefore,
|
||||||
|
notAfter,
|
||||||
|
subject2,
|
||||||
|
kp2.getPublic()
|
||||||
|
);
|
||||||
|
ContentSigner signer2 = new JcaContentSignerBuilder("SHA256withRSA").build(kp2.getPrivate());
|
||||||
|
X509Certificate cert2 = new JcaX509CertificateConverter().getCertificate(certBuilder2.build(signer2));
|
||||||
|
ks.setKeyEntry("test-cert-2", kp2.getPrivate(), pwd, new Certificate[]{cert2});
|
||||||
|
|
||||||
Pkcs12CertificateProvider provider = new Pkcs12CertificateProvider("test-provider", "Provedor de Teste");
|
Pkcs12CertificateProvider provider = new Pkcs12CertificateProvider("test-provider", "Provedor de Teste");
|
||||||
provider.setKeyStore(ks, pwd);
|
provider.setKeyStore(ks, pwd);
|
||||||
certificateManager.registerProvider(provider);
|
certificateManager.registerProvider(provider);
|
||||||
|
|
||||||
List<ManagedCertificate> list = certificateManager.discoverCertificates();
|
List<ManagedCertificate> list = certificateManager.discoverCertificates();
|
||||||
assertThat(list).isNotEmpty();
|
assertThat(list).hasSize(2);
|
||||||
this.testCertId = list.get(0).id();
|
this.testCertId = list.stream().filter(c -> c.alias().equals("test-cert")).findFirst().orElseThrow().id();
|
||||||
|
this.testCert2Id = list.stream().filter(c -> c.alias().equals("test-cert-2")).findFirst().orElseThrow().id();
|
||||||
|
|
||||||
this.pdfSigningService = new PdfSigningService(certificateManager);
|
this.pdfSigningService = new PdfSigningService(certificateManager);
|
||||||
}
|
}
|
||||||
@@ -215,4 +234,59 @@ class PdfSigningServiceTest {
|
|||||||
byte[] pngBytes = PdfSigningService.renderPagePngBytes(originalPdf, 0, 72f);
|
byte[] pngBytes = PdfSigningService.renderPagePngBytes(originalPdf, 0, 72f);
|
||||||
assertThat(pngBytes).isNotEmpty();
|
assertThat(pngBytes).isNotEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Deve manter ambas as assinaturas válidas ao assinar um PDF que já possui assinatura digital (Co-assinatura PAdES)")
|
||||||
|
void shouldKeepBothSignaturesValidWhenSigningAlreadySignedPdf() throws Exception {
|
||||||
|
byte[] originalPdf = createSamplePdf();
|
||||||
|
|
||||||
|
// 1ª Assinatura com o primeiro certificado
|
||||||
|
PdfSignatureOptions firstOptions = PdfSignatureOptions.builder(testCertId)
|
||||||
|
.visibleSeal(true)
|
||||||
|
.position(PdfSealPosition.BOTTOM_RIGHT)
|
||||||
|
.reason("Primeira Aprovação")
|
||||||
|
.location("Brasília, DF")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
byte[] firstSignedPdf = pdfSigningService.signPdf(originalPdf, firstOptions);
|
||||||
|
assertThat(firstSignedPdf).isNotNull();
|
||||||
|
|
||||||
|
// 2ª Assinatura sobre o documento já assinado com o segundo certificado
|
||||||
|
PdfSignatureOptions secondOptions = PdfSignatureOptions.builder(testCert2Id)
|
||||||
|
.visibleSeal(true)
|
||||||
|
.position(PdfSealPosition.TOP_LEFT)
|
||||||
|
.reason("Segunda Aprovação Conjunta")
|
||||||
|
.location("São Paulo, SP")
|
||||||
|
.build();
|
||||||
|
|
||||||
|
byte[] doubleSignedPdf = pdfSigningService.signPdf(firstSignedPdf, secondOptions);
|
||||||
|
assertThat(doubleSignedPdf).isNotNull();
|
||||||
|
assertThat(doubleSignedPdf.length).isGreaterThan(firstSignedPdf.length);
|
||||||
|
|
||||||
|
// Validação estrutural das assinaturas no PDDocument
|
||||||
|
try (PDDocument doc = Loader.loadPDF(doubleSignedPdf)) {
|
||||||
|
List<PDSignature> signatures = doc.getSignatureDictionaries();
|
||||||
|
assertThat(signatures).hasSize(2);
|
||||||
|
assertThat(signatures.get(0).getName()).contains("Joao da Silva");
|
||||||
|
assertThat(signatures.get(0).getReason()).isEqualTo("Primeira Aprovação");
|
||||||
|
assertThat(signatures.get(1).getName()).contains("Maria Souza");
|
||||||
|
assertThat(signatures.get(1).getReason()).isEqualTo("Segunda Aprovação Conjunta");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validação criptográfica rigorosa via SignatureValidationService (ambas as assinaturas devem ser íntegras)
|
||||||
|
SignatureValidationService validationService = new SignatureValidationService();
|
||||||
|
ValidationResult validationResult = validationService.validateAttached(doubleSignedPdf, "duplo_assinado.pdf");
|
||||||
|
|
||||||
|
assertThat(validationResult.isValid()).isTrue();
|
||||||
|
assertThat(validationResult.signers()).hasSize(2);
|
||||||
|
|
||||||
|
// Ambas as assinaturas e digests devem ser 100% válidos
|
||||||
|
assertThat(validationResult.signers().get(0).signatureValid()).isTrue();
|
||||||
|
assertThat(validationResult.signers().get(0).digestValid()).isTrue();
|
||||||
|
assertThat(validationResult.signers().get(0).signerName()).contains("Joao da Silva");
|
||||||
|
|
||||||
|
assertThat(validationResult.signers().get(1).signatureValid()).isTrue();
|
||||||
|
assertThat(validationResult.signers().get(1).digestValid()).isTrue();
|
||||||
|
assertThat(validationResult.signers().get(1).signerName()).contains("Maria Souza");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+90
-6
@@ -144,6 +144,8 @@ public final class PdfSignDialog {
|
|||||||
private Label lblSealScaleIndicator;
|
private Label lblSealScaleIndicator;
|
||||||
private double resizeStartSceneX;
|
private double resizeStartSceneX;
|
||||||
private double resizeStartBaseWidth;
|
private double resizeStartBaseWidth;
|
||||||
|
private boolean isResizingSealInProgress = false;
|
||||||
|
private long lastResizeActionTimestamp = 0;
|
||||||
|
|
||||||
// Visualizador Interativo da Folha
|
// Visualizador Interativo da Folha
|
||||||
private ScrollPane sheetScroll;
|
private ScrollPane sheetScroll;
|
||||||
@@ -547,12 +549,17 @@ public final class PdfSignDialog {
|
|||||||
interactiveOverlayPane = new Pane();
|
interactiveOverlayPane = new Pane();
|
||||||
interactiveOverlayPane.setCursor(Cursor.CROSSHAIR);
|
interactiveOverlayPane.setCursor(Cursor.CROSSHAIR);
|
||||||
interactiveOverlayPane.setOnMouseClicked(event -> {
|
interactiveOverlayPane.setOnMouseClicked(event -> {
|
||||||
if (sealDragOccurred) {
|
if (sealDragOccurred || isResizingSealInProgress || (System.currentTimeMillis() - lastResizeActionTimestamp < 400)) {
|
||||||
// Previne que a soltura do mouse no término do arraste cause clique residual
|
// Previne que a soltura do mouse no término do redimensionamento ou arraste cause clique residual
|
||||||
sealDragOccurred = false;
|
sealDragOccurred = false;
|
||||||
event.consume();
|
event.consume();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (isResizeModeActive) {
|
||||||
|
// No modo redimensionamento ativo, clicar fora não deve saltar o selo acidentalmente
|
||||||
|
event.consume();
|
||||||
|
return;
|
||||||
|
}
|
||||||
handleSheetClick(event);
|
handleSheetClick(event);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -572,6 +579,10 @@ public final class PdfSignDialog {
|
|||||||
sealSelectionBorder.setStrokeWidth(1.8);
|
sealSelectionBorder.setStrokeWidth(1.8);
|
||||||
sealSelectionBorder.getStrokeDashArray().addAll(5.0, 4.0);
|
sealSelectionBorder.getStrokeDashArray().addAll(5.0, 4.0);
|
||||||
sealSelectionBorder.setVisible(false);
|
sealSelectionBorder.setVisible(false);
|
||||||
|
sealSelectionBorder.setOnMousePressed(e -> e.consume());
|
||||||
|
sealSelectionBorder.setOnMouseDragged(e -> e.consume());
|
||||||
|
sealSelectionBorder.setOnMouseReleased(e -> e.consume());
|
||||||
|
sealSelectionBorder.setOnMouseClicked(e -> e.consume());
|
||||||
|
|
||||||
resizeHandle = new Circle(8);
|
resizeHandle = new Circle(8);
|
||||||
resizeHandle.setFill(Color.rgb(2, 132, 199));
|
resizeHandle.setFill(Color.rgb(2, 132, 199));
|
||||||
@@ -611,10 +622,15 @@ public final class PdfSignDialog {
|
|||||||
event.consume();
|
event.consume();
|
||||||
});
|
});
|
||||||
|
|
||||||
sealStampOverlayView.setOnMouseReleased(event -> event.consume());
|
sealStampOverlayView.setOnMouseReleased(event -> {
|
||||||
|
lastResizeActionTimestamp = System.currentTimeMillis();
|
||||||
|
event.consume();
|
||||||
|
});
|
||||||
|
|
||||||
// Eventos da Alça de Redimensionamento Interativa
|
// Eventos da Alça de Redimensionamento Interativa
|
||||||
resizeHandle.setOnMousePressed(event -> {
|
resizeHandle.setOnMousePressed(event -> {
|
||||||
|
isResizingSealInProgress = true;
|
||||||
|
sealDragOccurred = true;
|
||||||
resizeStartSceneX = event.getSceneX();
|
resizeStartSceneX = event.getSceneX();
|
||||||
resizeStartBaseWidth = currentBaseSealWidth * zoomFactor * sealScale;
|
resizeStartBaseWidth = currentBaseSealWidth * zoomFactor * sealScale;
|
||||||
event.consume();
|
event.consume();
|
||||||
@@ -622,17 +638,28 @@ public final class PdfSignDialog {
|
|||||||
|
|
||||||
resizeHandle.setOnMouseDragged(event -> {
|
resizeHandle.setOnMouseDragged(event -> {
|
||||||
if (!chkVisibleSeal.isSelected() || selectedPdfBytes == null) return;
|
if (!chkVisibleSeal.isSelected() || selectedPdfBytes == null) return;
|
||||||
|
isResizingSealInProgress = true;
|
||||||
|
sealDragOccurred = true;
|
||||||
double deltaX = event.getSceneX() - resizeStartSceneX;
|
double deltaX = event.getSceneX() - resizeStartSceneX;
|
||||||
double newWidth = resizeStartBaseWidth + deltaX;
|
double newWidth = resizeStartBaseWidth + deltaX;
|
||||||
double baseW = currentBaseSealWidth * zoomFactor;
|
double baseW = currentBaseSealWidth * zoomFactor;
|
||||||
if (baseW > 0) {
|
if (baseW > 0) {
|
||||||
applySealScale(newWidth / baseW);
|
applySealScalePreservingOrigin(newWidth / baseW);
|
||||||
}
|
}
|
||||||
event.consume();
|
event.consume();
|
||||||
});
|
});
|
||||||
|
|
||||||
resizeHandle.setOnMouseReleased(event -> event.consume());
|
resizeHandle.setOnMouseReleased(event -> {
|
||||||
resizeHandle.setOnMouseClicked(event -> event.consume());
|
isResizingSealInProgress = false;
|
||||||
|
lastResizeActionTimestamp = System.currentTimeMillis();
|
||||||
|
sealDragOccurred = true;
|
||||||
|
event.consume();
|
||||||
|
});
|
||||||
|
resizeHandle.setOnMouseClicked(event -> {
|
||||||
|
isResizingSealInProgress = false;
|
||||||
|
lastResizeActionTimestamp = System.currentTimeMillis();
|
||||||
|
event.consume();
|
||||||
|
});
|
||||||
|
|
||||||
sealGroupPane.getChildren().addAll(sealStampOverlayView, sealSelectionBorder, resizeHandle);
|
sealGroupPane.getChildren().addAll(sealStampOverlayView, sealSelectionBorder, resizeHandle);
|
||||||
interactiveOverlayPane.getChildren().add(sealGroupPane);
|
interactiveOverlayPane.getChildren().add(sealGroupPane);
|
||||||
@@ -647,6 +674,7 @@ public final class PdfSignDialog {
|
|||||||
|
|
||||||
private void toggleResizeMode() {
|
private void toggleResizeMode() {
|
||||||
this.isResizeModeActive = !this.isResizeModeActive;
|
this.isResizeModeActive = !this.isResizeModeActive;
|
||||||
|
this.lastResizeActionTimestamp = System.currentTimeMillis();
|
||||||
if (sealSelectionBorder != null) {
|
if (sealSelectionBorder != null) {
|
||||||
sealSelectionBorder.setVisible(isResizeModeActive);
|
sealSelectionBorder.setVisible(isResizeModeActive);
|
||||||
}
|
}
|
||||||
@@ -666,6 +694,62 @@ public final class PdfSignDialog {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void applySealScalePreservingOrigin(double newScale) {
|
||||||
|
if (!chkVisibleSeal.isSelected() || selectedPdfBytes == null) return;
|
||||||
|
|
||||||
|
double currentX = sealGroupPane != null ? sealGroupPane.getLayoutX() : 0;
|
||||||
|
double currentY = sealGroupPane != null ? sealGroupPane.getLayoutY() : 0;
|
||||||
|
|
||||||
|
this.sealScale = Math.max(MIN_SEAL_SCALE, Math.min(MAX_SEAL_SCALE, Math.round(newScale * 100.0) / 100.0));
|
||||||
|
if (lblSealScaleIndicator != null) {
|
||||||
|
lblSealScaleIndicator.setText(String.format("%.0f%%", sealScale * 100));
|
||||||
|
}
|
||||||
|
if (btnResetSealScale != null) {
|
||||||
|
btnResetSealScale.setVisible(Math.abs(sealScale - 1.0) > 0.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateContainerZoomDimensions();
|
||||||
|
|
||||||
|
double sheetW = currentBaseSheetWidth * zoomFactor;
|
||||||
|
double sheetH = currentBaseSheetHeight * zoomFactor;
|
||||||
|
double sealW = currentBaseSealWidth * zoomFactor * sealScale;
|
||||||
|
double sealH = currentBaseSealHeight * zoomFactor * sealScale;
|
||||||
|
|
||||||
|
// Mantém a quina superior esquerda ancorada no mesmo ponto exato
|
||||||
|
double clampedX = Math.max(0, Math.min(currentX, sheetW - sealW));
|
||||||
|
double clampedY = Math.max(0, Math.min(currentY, sheetH - sealH));
|
||||||
|
|
||||||
|
if (sealGroupPane != null) {
|
||||||
|
sealGroupPane.setLayoutX(clampedX);
|
||||||
|
sealGroupPane.setLayoutY(clampedY);
|
||||||
|
sealGroupPane.setVisible(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.sealRelativeX = (sheetW > 0) ? (clampedX / sheetW) : 0;
|
||||||
|
this.sealRelativeY = (sheetH > 0) ? (clampedY / sheetH) : 0;
|
||||||
|
|
||||||
|
PdfSealTheme theme = activeSealTheme != null ? activeSealTheme : PdfSealThemeRegistry.getDefaultTheme();
|
||||||
|
float basePdfW = (float) ((theme != null) ? theme.width() * 0.72 : 144.0);
|
||||||
|
float basePdfH = (float) ((theme != null) ? theme.height() * 0.72 : 44.0);
|
||||||
|
float scaledPdfW = basePdfW * (float) sealScale;
|
||||||
|
float scaledPdfH = basePdfH * (float) sealScale;
|
||||||
|
|
||||||
|
double scale = (currentPagePdfWidth > 0) ? (sheetW / currentPagePdfWidth) : 1.0;
|
||||||
|
float calculatedPdfX = (float) (clampedX / scale);
|
||||||
|
float calculatedPdfY = (float) (currentPagePdfHeight - (clampedY / scale) - scaledPdfH);
|
||||||
|
|
||||||
|
this.customPdfX = Math.max(0, Math.min(calculatedPdfX, currentPagePdfWidth - scaledPdfW));
|
||||||
|
this.customPdfY = Math.max(0, Math.min(calculatedPdfY, currentPagePdfHeight - scaledPdfH));
|
||||||
|
this.customPageNumber = currentPageIndex + 1;
|
||||||
|
this.isCustomPositionActive = true;
|
||||||
|
|
||||||
|
if (positionCombo.getValue() != PositionOption.CUSTOM) {
|
||||||
|
positionCombo.setValue(PositionOption.CUSTOM);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateFeedbackLabel();
|
||||||
|
}
|
||||||
|
|
||||||
private void applySealScale(double newScale) {
|
private void applySealScale(double newScale) {
|
||||||
this.sealScale = Math.max(MIN_SEAL_SCALE, Math.min(MAX_SEAL_SCALE, Math.round(newScale * 100.0) / 100.0));
|
this.sealScale = Math.max(MIN_SEAL_SCALE, Math.min(MAX_SEAL_SCALE, Math.round(newScale * 100.0) / 100.0));
|
||||||
if (lblSealScaleIndicator != null) {
|
if (lblSealScaleIndicator != null) {
|
||||||
|
|||||||
Reference in New Issue
Block a user