From 03817b7c9802697d9e362b5705208828ed9ece1c Mon Sep 17 00:00:00 2001 From: "Josef (kwart) Cacek" Date: Fri, 7 Aug 2026 22:34:26 +0200 Subject: [PATCH 1/2] Lock the visible-signature toggle to the selected field Selecting an existing signature field forced the visible flag on and disabled the side-panel checkbox, but the menu item and the toolbar toggle stayed switchable. Turning them off changed nothing about the output - SignerLogic sets visible=true for a field-placed signature regardless - so the status badge just started disagreeing with the signature that came out. Both toggles now lock together with the checkbox; "(create new field)" is the way back to an invisible signature. Turning the flag back on then placed a 100x100 box in the lower-left corner of the page: selecting a field resets the position options to their defaults, and the auto-placement accepted that untouched default rectangle as a last-known position. The same box appeared on a fresh profile, which has never held any other coordinates. The default rectangle is now recognised as "nothing chosen yet" by both the auto-placement and the preset-load path, which carried a copy of the check, so the intended bottom-right fallback applies. The auto-placement also stays out of the way entirely while a field is selected - the field's own /Rect decides where the signature goes. Deselecting a field left the forced-on visible flag behind with no rectangle on screen, which would have signed at the default coordinates without showing anything; the rectangle now comes back with it. --- distribution/doc/release-notes/3.2.0.md | 1 + .../fx/view/MainWindowController.java | 38 +++++++++++--- .../VisibleSignatureCoordinator.java | 41 ++++++++++++--- .../VisibleSignatureCoordinatorTest.java | 50 +++++++++++++++++++ website/docs/JSignPdf.adoc | 7 ++- 5 files changed, 120 insertions(+), 17 deletions(-) diff --git a/distribution/doc/release-notes/3.2.0.md b/distribution/doc/release-notes/3.2.0.md index 1d90de3b..20695aa9 100644 --- a/distribution/doc/release-notes/3.2.0.md +++ b/distribution/doc/release-notes/3.2.0.md @@ -6,6 +6,7 @@ A release about getting out of your way. The improvements below smooth over the - **New `debug` output for signing diagnostics** — enable it on the new _General_ tab of Preferences, or with `debug=true` in `advanced.properties` (or `-o debug=true` for a single CLI run), to log the signing certificate chain (subject, issuer, serial, validity, key usage, QC statements, and the AIA and CRL distribution-point URLs of each certificate) plus, for the DSS engine, the trust anchors it loaded and every AIA, CRL, and OCSP request with the target URL, the certificate it is for, the response size, the outcome, and the elapsed time. It is off by default so normal runs stay quiet; `-q` silences everything regardless. See issue 452. - **Preferences dialog gains a _General_ tab** gathering the signing-engine selection and the new `debug` toggle; both apply immediately. - **Visible signature images keep their aspect ratio with the DSS engine** — background and graphic images were previously stretched to fill the signature box and came out distorted. A `--bg-scale` of zero still stretches to fill; any other value fits the image and centers it, matching the OpenPDF engine. See issue 460. +- **The visible signature no longer starts in the lower-left corner** — enabling _Visible signature_ without dragging a rectangle first dropped a 100×100 point box at the page origin whenever no position had been chosen yet, instead of the intended bottom-right default. The stored defaults are now recognised as "nothing chosen yet", so the fallback placement applies on a fresh installation and after selecting a signature field. - **Clear list in the Recent files menu** — the File menu's recent-files trail can now be emptied on its own, which previously required a factory reset that discarded every other setting as well. The item appears only when there is something to clear. See issue 453. - **Pick the interface language** — a new _Language_ selector on the _General_ tab of Preferences lets you choose the UI language explicitly instead of always following the operating-system locale; _System default_ stays the default. It is stored as `ui.language` in `advanced.properties` and works on the command line too (`-o ui.language=de`, e.g. to read `--help` in German). The setting is read at startup, so restart to apply it, and it affects interface text only — number/date formatting and the signed output are unchanged. See issue 444. - **Sign very large PDFs without a bigger heap** — set `buffering.mode=temp` in `advanced.properties` (or `-o buffering.mode=temp` for a single run) to stage the document in temporary files instead of on the Java heap, so its size no longer has to fit in `-Xmx`. A 400 MB document that fails with an out-of-memory error under a 512 MB heap signs fine on both engines with this on. Optionally point `buffering.tempDir` at a fast disk; with the DSS engine, add `-Djava.io.tmpdir` too if your system temporary directory is small or RAM-backed. The signed output is identical either way; the cost is disk space and a little speed. See issue 178. diff --git a/jsignpdf/src/main/java/net/sf/jsignpdf/fx/view/MainWindowController.java b/jsignpdf/src/main/java/net/sf/jsignpdf/fx/view/MainWindowController.java index 0e243bb4..17415c6a 100644 --- a/jsignpdf/src/main/java/net/sf/jsignpdf/fx/view/MainWindowController.java +++ b/jsignpdf/src/main/java/net/sf/jsignpdf/fx/view/MainWindowController.java @@ -8,6 +8,8 @@ import javafx.animation.PauseTransition; import javafx.application.Platform; +import javafx.beans.property.BooleanProperty; +import javafx.beans.property.SimpleBooleanProperty; import javafx.fxml.FXML; import javafx.geometry.Insets; import javafx.scene.Node; @@ -116,6 +118,10 @@ public class MainWindowController { private SignatureFieldInfo selectedSigField; /** Marker rectangle of {@link #selectedSigField}, relative to the displayed page (see PdfExtraInfo). */ private float[] selectedSigFieldMarker; + /** Mirrors {@link #selectedSigField} for the bindings that lock the visible-signature toggles. */ + private final BooleanProperty sigFieldSelected = new SimpleBooleanProperty(false); + /** True while no document is loaded, i.e. there is nothing to place a visible signature on. */ + private final BooleanProperty noDocument = new SimpleBooleanProperty(true); private PdfPageView pdfPageView; private SignatureOverlay signatureOverlay; /** Holds the side panel node while it's detached from the SplitPane (hidden). */ @@ -305,6 +311,13 @@ private void initialize() { btnVisibleSig.selectedProperty().bindBidirectional(signingVM.visibleProperty()); btnTsa.selectedProperty().bindBidirectional(signingVM.tsaEnabledProperty()); + // Signing into an existing field always draws the appearance into that field's rectangle - SignerLogic + // forces visible=true for it regardless of this flag - so the menu item and the toolbar toggle lock + // together with the side-panel checkbox. Left switchable they would only make the UI disagree with the + // signature that comes out. "(create new field)" is the way back to an invisible signature. + menuVisibleSig.disableProperty().bind(noDocument.or(sigFieldSelected)); + btnVisibleSig.disableProperty().bind(noDocument.or(sigFieldSelected)); + // When TSA is turned on but no URL is configured yet, jump the side-panel // accordion to the TSA section so the user can fill the required field. signingVM.tsaEnabledProperty().addListener((obs, was, on) -> { @@ -742,14 +755,15 @@ private void setDocumentControlsDisabled(boolean disabled) { txtPageNumber.setDisable(disabled); btnNextPage.setDisable(disabled); btnSign.setDisable(disabled); - btnVisibleSig.setDisable(disabled); menuSign.setDisable(disabled); menuClose.setDisable(disabled); menuSaveAs.setDisable(disabled); - menuVisibleSig.setDisable(disabled); menuZoomIn.setDisable(disabled); menuZoomOut.setDisable(disabled); menuZoomFit.setDisable(disabled); + // The visible-signature toggles (menu item + toolbar button) are bound to this property instead of being + // disabled here, so that a selected signature field can lock them as well. + noDocument.set(disabled); if (signatureSettingsController != null) { signatureSettingsController.setVisibleSigCheckBoxDisabled(disabled); } @@ -792,9 +806,13 @@ private void updateSigCoordsBadge() { * coordinates persisted in the ViewModel — if they form a valid rectangle * that fits the current page — and falls back to a safe bottom-right * default otherwise. Always re-targets the current page. + *

+ * Does nothing while an existing signature field is selected: that field's own {@code /Rect} decides where the + * signature goes, so a placement rectangle would only promise a position the signing path ignores. */ private void autoPlaceVisibleSignature() { - if (!documentVM.isDocumentLoaded() || placementVM.isPlaced() || options == null) { + if (!documentVM.isDocumentLoaded() || placementVM.isPlaced() || options == null + || selectedSigField != null) { return; } PageInfo pageInfo = new PdfExtraInfo(options).getPageInfo(documentVM.getCurrentPage()); @@ -809,11 +827,7 @@ private void autoPlaceVisibleSignature() { float urx = signingVM.positionURXProperty().get(); float ury = signingVM.positionURYProperty().get(); - boolean fits = urx - llx > 1f && ury - lly > 1f - && llx >= 0f && lly >= 0f - && urx <= pw && ury <= ph; - - if (fits) { + if (VisibleSignatureCoordinator.hasUsablePosition(llx, lly, urx, ury, pw, ph)) { placementVM.fromPdfCoordinates(llx, lly, urx, ury, pw, ph); } else { // Safe default: bottom-right, 15% × 8% of the page with ~5% margins. @@ -1495,11 +1509,19 @@ private void refreshSignatureFields() { * @param field the selected field, or {@code null} for "create a new field" */ private void onSigFieldSelected(SignatureFieldInfo field) { + final boolean hadField = selectedSigField != null; selectedSigField = field; selectedSigFieldMarker = null; + sigFieldSelected.set(field != null); if (field == null) { signatureOverlay.clearFieldHighlight(); signatureOverlay.setMouseTransparent(false); + if (hadField && signingVM.visibleProperty().get()) { + // The visible flag the field forced on survives its deselection, so the rectangle has to come + // back with it - otherwise signing would place the appearance at coordinates nothing on screen + // shows. The position options were reset with the selection, so this lands on the default spot. + autoPlaceVisibleSignature(); + } updateSigStateBadge(); return; } diff --git a/jsignpdf/src/main/java/net/sf/jsignpdf/fx/viewmodel/VisibleSignatureCoordinator.java b/jsignpdf/src/main/java/net/sf/jsignpdf/fx/viewmodel/VisibleSignatureCoordinator.java index b47b99bb..c677a14e 100644 --- a/jsignpdf/src/main/java/net/sf/jsignpdf/fx/viewmodel/VisibleSignatureCoordinator.java +++ b/jsignpdf/src/main/java/net/sf/jsignpdf/fx/viewmodel/VisibleSignatureCoordinator.java @@ -1,5 +1,7 @@ package net.sf.jsignpdf.fx.viewmodel; +import net.sf.jsignpdf.Constants; + /** * Static helpers for moving the visible-signature rectangle between the placement overlay (relative page coords in * {@link SignaturePlacementViewModel}) and the signing configuration (PDF coords in {@link SigningOptionsViewModel}). @@ -44,9 +46,9 @@ public static void pushPlacementToSigning(SignaturePlacementViewModel placementV * {@code MainWindowController#autoPlaceVisibleSignature()}, this always replaces the existing placement rather than * bailing out when one is already present. *

- * No-op if the signing VM has {@code visible=false} or the coordinates do not describe a meaningful rectangle - * (non-positive dimensions or out-of-page bounds). The latter guard means callers can pass stale default coordinates - * without corrupting the placement — the user just has to place the rectangle themselves. + * No-op if the signing VM has {@code visible=false} or {@link #hasUsablePosition} rejects the coordinates. The + * latter guard means callers can pass stale default coordinates without corrupting the placement — the user just + * has to place the rectangle themselves. */ public static void pushSigningToPlacement(SigningOptionsViewModel signingVM, SignaturePlacementViewModel placementVM, @@ -58,12 +60,37 @@ public static void pushSigningToPlacement(SigningOptionsViewModel signingVM, float lly = signingVM.positionLLYProperty().get(); float urx = signingVM.positionURXProperty().get(); float ury = signingVM.positionURYProperty().get(); - boolean fits = urx - llx > 1f && ury - lly > 1f - && llx >= 0f && lly >= 0f - && urx <= pageWidth && ury <= pageHeight; - if (!fits) { + if (!hasUsablePosition(llx, lly, urx, ury, pageWidth, pageHeight)) { return; } placementVM.fromPdfCoordinates(llx, lly, urx, ury, pageWidth, pageHeight); } + + /** + * Decides whether the given PDF coordinates are a position worth restoring on screen, i.e. a rectangle with + * positive dimensions that sits inside the page. + *

+ * The untouched default rectangle ({@code DEFVAL_LLX..DEFVAL_URY}) is rejected even though it technically fits: + * it is what a fresh profile starts with, and what selecting an existing signature field writes back, so it means + * "no position chosen yet" rather than "put a 100×100 box in the lower-left corner". Callers fall back to their + * own default placement instead. + * + * @param llx lower-left X in PDF points + * @param lly lower-left Y in PDF points + * @param urx upper-right X in PDF points + * @param ury upper-right Y in PDF points + * @param pageWidth width of the target page in PDF units + * @param pageHeight height of the target page in PDF units + * @return true when the rectangle can be shown as-is + */ + public static boolean hasUsablePosition(float llx, float lly, float urx, float ury, + float pageWidth, float pageHeight) { + if (llx == Constants.DEFVAL_LLX && lly == Constants.DEFVAL_LLY + && urx == Constants.DEFVAL_URX && ury == Constants.DEFVAL_URY) { + return false; + } + return urx - llx > 1f && ury - lly > 1f + && llx >= 0f && lly >= 0f + && urx <= pageWidth && ury <= pageHeight; + } } diff --git a/jsignpdf/src/test/java/net/sf/jsignpdf/fx/viewmodel/VisibleSignatureCoordinatorTest.java b/jsignpdf/src/test/java/net/sf/jsignpdf/fx/viewmodel/VisibleSignatureCoordinatorTest.java index e03d9ddf..ee4bd175 100644 --- a/jsignpdf/src/test/java/net/sf/jsignpdf/fx/viewmodel/VisibleSignatureCoordinatorTest.java +++ b/jsignpdf/src/test/java/net/sf/jsignpdf/fx/viewmodel/VisibleSignatureCoordinatorTest.java @@ -6,6 +6,8 @@ import org.junit.Test; +import net.sf.jsignpdf.Constants; + /** * Tests for {@link VisibleSignatureCoordinator} — the placement↔signing view-model sync used by the preset save/load flows * and the sign/close flows. @@ -128,6 +130,54 @@ public void pushSigning_noOp_whenCoordsAreDegenerate() { assertFalse(pvm.isPlaced()); } + /** + * The default rectangle fits any page, but it is what a fresh profile carries and what selecting an existing + * signature field writes back - restoring it would drop a 100×100 box in the lower-left corner of the page. + */ + @Test + public void pushSigning_noOp_whenCoordsAreTheUntouchedDefaults() { + SigningOptionsViewModel svm = new SigningOptionsViewModel(); + svm.visibleProperty().set(true); + svm.positionLLXProperty().set(Constants.DEFVAL_LLX); + svm.positionLLYProperty().set(Constants.DEFVAL_LLY); + svm.positionURXProperty().set(Constants.DEFVAL_URX); + svm.positionURYProperty().set(Constants.DEFVAL_URY); + SignaturePlacementViewModel pvm = new SignaturePlacementViewModel(); + + VisibleSignatureCoordinator.pushSigningToPlacement(svm, pvm, PAGE_W, PAGE_H); + + assertFalse("the default rectangle means 'no position chosen yet'", pvm.isPlaced()); + } + + // ---- hasUsablePosition ---- + + @Test + public void hasUsablePosition_acceptsARectangleInsideThePage() { + assertTrue(VisibleSignatureCoordinator.hasUsablePosition(120f, 560f, 180f, 640f, PAGE_W, PAGE_H)); + } + + @Test + public void hasUsablePosition_rejectsTheDefaultRectangle() { + assertFalse(VisibleSignatureCoordinator.hasUsablePosition(Constants.DEFVAL_LLX, Constants.DEFVAL_LLY, + Constants.DEFVAL_URX, Constants.DEFVAL_URY, PAGE_W, PAGE_H)); + } + + /** Only the untouched defaults are special - the same box moved anywhere else is a real user choice. */ + @Test + public void hasUsablePosition_acceptsTheDefaultSizedRectangleElsewhere() { + assertTrue(VisibleSignatureCoordinator.hasUsablePosition(Constants.DEFVAL_LLX, Constants.DEFVAL_LLY + 10f, + Constants.DEFVAL_URX, Constants.DEFVAL_URY + 10f, PAGE_W, PAGE_H)); + } + + @Test + public void hasUsablePosition_rejectsDegenerateAndOutOfPageRectangles() { + assertFalse("zero width", VisibleSignatureCoordinator.hasUsablePosition(10f, 10f, 10f, 80f, PAGE_W, PAGE_H)); + assertFalse("beyond the page", VisibleSignatureCoordinator.hasUsablePosition(10f, 10f, PAGE_W + 1f, 80f, + PAGE_W, PAGE_H)); + assertFalse("negative origin", VisibleSignatureCoordinator.hasUsablePosition(-5f, 10f, 80f, 80f, + PAGE_W, PAGE_H)); + } + @Test public void pushSigning_replacesExistingPlacement() { // Load-preset semantics: even if a rectangle is already placed, it gets replaced with the preset's coords. diff --git a/website/docs/JSignPdf.adoc b/website/docs/JSignPdf.adoc index 89cb8d80..3a08869a 100644 --- a/website/docs/JSignPdf.adoc +++ b/website/docs/JSignPdf.adoc @@ -882,8 +882,11 @@ fills their own box. JSignPdf can sign into such a field instead of creating a n In the JavaFX UI, the _Signature field_ combo box at the top of the _Signature Appearance_ panel lists the empty signature fields of the opened document. Pick one and the signature goes there; the field is marked on the page preview and the position controls are switched off, because the field's own rectangle decides where -the signature lands. The combo stays on _(create new field)_ for documents without empty fields, and the -selection is reset whenever you open another document. +the signature lands. _Visible signature_ is turned on and locked (in the panel, the toolbar and the menu +alike) for as long as a field is selected, because the appearance is always drawn into the field -- switch +back to _(create new field)_ if you want an invisible signature, or pick a field whose rectangle has zero +size, which gives one anyway. The combo stays on _(create new field)_ for documents without empty fields, and +the selection is reset whenever you open another document. On the command line, `-lsf` shows what a document offers: From 14642c6e8b5380b55abc9b3c222a6737d67e72be Mon Sep 17 00:00:00 2001 From: Josef Cacek Date: Sat, 8 Aug 2026 10:41:28 +0200 Subject: [PATCH 2/2] Apply suggestion from @kwart --- distribution/doc/release-notes/3.2.0.md | 1 - 1 file changed, 1 deletion(-) diff --git a/distribution/doc/release-notes/3.2.0.md b/distribution/doc/release-notes/3.2.0.md index 20695aa9..1d90de3b 100644 --- a/distribution/doc/release-notes/3.2.0.md +++ b/distribution/doc/release-notes/3.2.0.md @@ -6,7 +6,6 @@ A release about getting out of your way. The improvements below smooth over the - **New `debug` output for signing diagnostics** — enable it on the new _General_ tab of Preferences, or with `debug=true` in `advanced.properties` (or `-o debug=true` for a single CLI run), to log the signing certificate chain (subject, issuer, serial, validity, key usage, QC statements, and the AIA and CRL distribution-point URLs of each certificate) plus, for the DSS engine, the trust anchors it loaded and every AIA, CRL, and OCSP request with the target URL, the certificate it is for, the response size, the outcome, and the elapsed time. It is off by default so normal runs stay quiet; `-q` silences everything regardless. See issue 452. - **Preferences dialog gains a _General_ tab** gathering the signing-engine selection and the new `debug` toggle; both apply immediately. - **Visible signature images keep their aspect ratio with the DSS engine** — background and graphic images were previously stretched to fill the signature box and came out distorted. A `--bg-scale` of zero still stretches to fill; any other value fits the image and centers it, matching the OpenPDF engine. See issue 460. -- **The visible signature no longer starts in the lower-left corner** — enabling _Visible signature_ without dragging a rectangle first dropped a 100×100 point box at the page origin whenever no position had been chosen yet, instead of the intended bottom-right default. The stored defaults are now recognised as "nothing chosen yet", so the fallback placement applies on a fresh installation and after selecting a signature field. - **Clear list in the Recent files menu** — the File menu's recent-files trail can now be emptied on its own, which previously required a factory reset that discarded every other setting as well. The item appears only when there is something to clear. See issue 453. - **Pick the interface language** — a new _Language_ selector on the _General_ tab of Preferences lets you choose the UI language explicitly instead of always following the operating-system locale; _System default_ stays the default. It is stored as `ui.language` in `advanced.properties` and works on the command line too (`-o ui.language=de`, e.g. to read `--help` in German). The setting is read at startup, so restart to apply it, and it affects interface text only — number/date formatting and the signed output are unchanged. See issue 444. - **Sign very large PDFs without a bigger heap** — set `buffering.mode=temp` in `advanced.properties` (or `-o buffering.mode=temp` for a single run) to stage the document in temporary files instead of on the Java heap, so its size no longer has to fit in `-Xmx`. A 400 MB document that fails with an out-of-memory error under a 512 MB heap signs fine on both engines with this on. Optionally point `buffering.tempDir` at a fast disk; with the DSS engine, add `-Djava.io.tmpdir` too if your system temporary directory is small or RAM-backed. The signed output is identical either way; the cost is disk space and a little speed. See issue 178.