Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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). */
Expand Down Expand Up @@ -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) -> {
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -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.
* <p>
* 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());
Expand All @@ -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.
Expand Down Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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}).
Expand Down Expand Up @@ -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.
* <p>
* 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,
Expand All @@ -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.
* <p>
* 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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down
7 changes: 5 additions & 2 deletions website/docs/JSignPdf.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
Loading