Skip to content

feat(sign): make the visible signature configurable, and fix device certificates in self-hosted mode - #7262

Open
samuelsl27 wants to merge 34 commits into
Stirling-Tools:mainfrom
samuelsl27:feature/firma-posicionada
Open

feat(sign): make the visible signature configurable, and fix device certificates in self-hosted mode#7262
samuelsl27 wants to merge 34 commits into
Stirling-Tools:mainfrom
samuelsl27:feature/firma-posicionada

Conversation

@samuelsl27

@samuelsl27 samuelsl27 commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Description of Changes

Lets the user drag the signature box onto the document and pick which certificate fields appear inside it. Optionally repeats the appearance on every page.

The visible signature was drawn at a hardcoded PDRectangle(0, 0, 200, 50), so a caller could pick the page but not where on it, and the contents were fixed to signer name, date and reason. Signing a form meant the signature landed in the bottom-left corner regardless of where the document's signature area actually was.

What changed

  • SignatureBox — where the box goes, in PDF user space. Clamps inside the page and honours an offset media box, as cropped or imposed pages have.
  • CertificateAttributeService — reads 14 fields off the X.509 certificate; absent ones are omitted rather than returned blank.
  • SignatureAppearanceLayout — scales the type to whatever box was drawn, shrinking and then ellipsising rather than letting text escape the box and overlap the document.
  • SignatureMarkStamper — the every-page marks. Deliberately a separate class that must never touch the signature dictionary; keeping them in one file would make that line easy to blur later.
  • SignatureBoxDragOverlay — the drag interaction, mounted per page in the viewer. Supports click-drag and click-click, and uses pointer capture so the viewer does not pan mid-gesture.

The logo

showLogo drew a bundled Stirling mark scaled to 0.08 and pinned at a fixed offset that ignored the signature box entirely. Requested in discussion #7277, where the reporter wanted their company logo the way Foxit does it.

  • logoImage - an optional PNG or JPEG upload. Absent, the bundled mark is used exactly as before.
  • logoPosition - LEFT, RIGHT, TOP, BOTTOM or BEHIND. SignatureLogoPlacement holds the geometry as a separate class, so the five positions are unit-testable without rendering anything.

Two rules it enforces, both covered by tests:

  • The image keeps its aspect ratio. A stretched company logo reads as a rendering fault rather than a design choice.
  • The text keeps at least 65% of the box in both directions, so a banner-shaped logo cannot squeeze the signer out of their own signature.

BEHIND is the exception to the split: the logo fills the box as a watermark and the text keeps the whole box, drawn on top. It reuses the MULTIPLY blend at half opacity the bundled mark already used, which is what keeps text readable over it. The marks on the other pages receive the same logo, since a mark meant to match the signature cannot be missing the logo it has.

Why the coordinates use a bottom-left origin

/crop already works that way and the frontend's cropCoordinates helpers emit exactly those numbers, so the overlay feeds the endpoint directly. A second convention would have meant converting twice — the arithmetic most likely to end up wrong by one page height, and the hardest to notice, because the signature still appears, just in the wrong place.

On "show it on every page"

A PDF signature has one widget on one page, so only the drawn-on page carries the signature; the others get page content that looks the same and carries no cryptographic meaning. Each mark is a link annotation jumping to the signed page, where the reader can open the signature's properties — a link rather than anything signature-shaped, precisely because it is inert. The request field and the UI both say what is happening.

Device certificates against a self-hosted server (#7316)

Reported separately: the desktop offers the Windows certificate store and plugged-in PKCS#11 tokens as signing sources, but connecting it to a self-hosted server made them disappear. Three causes, all routing, none of them a backend restriction:

  1. The app-config comes from whichever backend the app is talking to, and it carries hardwareSigningAvailable. A self-hosted server answers that for itself - false, since it is not a desktop - so CertificateTypeSettings hid "This device" and reset signMode to MANUAL. The desktop now re-answers that one flag from its own machine and leaves the rest of the config alone: the others describe the deployment, and there the server is the authority.
  2. /api/v1/security/cert-sign/hardware/** matched the tool-endpoint rules and went to the server, which reported on its own hardware or refused outright.
  3. Signing itself posted to the server, which cannot reach a key held in the user's store or token.

The list of device-local endpoints lives in src/core and the router consults it, rather than the routing layer keeping a catalogue of which tools need local hardware - the same shape it already uses for CONVERSION_ENDPOINTS, and what #7510 suggested. Signing cannot be decided by path, since the endpoint is the same whichever certificate was picked, so the caller marks that one request instead.

Auth now follows the destination rather than the connection mode: before, a request routed to the local backend in self-hosted mode still carried the server's JWT to loopback.

Nothing is loosened. HardwareKeyStoreService.assertLocalDesktop already rejects these calls unless they come from the desktop bundle over loopback, and a regression test pins that an uploaded keystore still signs on the server rather than quietly moving everyone's signing onto the desktop.

Thanks to @Frooodle for confirming in #7510 that PRs are welcome in any directory - this one needed src/desktop/.
Backwards compatible

Every new field is optional. With no box and no field selection, the request produces the same output as before.

Closes #7261
Closes #7316


Checklist

General

Documentation

New tags were added to en-US/translation.toml first as the reference locale, then translated into en-GB and es-ES.

Translations (if applicable)

UI Changes (if applicable)

  • Screenshots or videos demonstrating the UI changes are attached
image image image image image

Testing (if applicable)

  • I have run task check to verify linters, typechecks, and tests pass
  • I have tested my changes locally

Signature placement, fields and logo. Backend tests cover the box maths, the certificate reading, the text fitting, the marks and the five logo placements. Verified end to end by building the Windows installer and signing real documents: placement at several zoom levels, both gestures, and the every-page marks with their links.

Device certificates against a self-hosted server. Covered by tests on the routing: the hardware endpoints resolve to the bundled backend in all three connection modes, a request marked device-local does the same on a shared path, the local answer for hardwareSigningAvailable wins over the server's while every other field is left alone, and no server token is attached to either. Each was watched failing before the fix - and only in self-hosted mode, which is where the bug lives.

Two regressions are pinned deliberately, since both would be silent: that an uploaded keystore still signs on the server rather than being moved onto the desktop, and that the app-config itself still comes from the server.

What I could not test. This half needs a real self-hosted server with a desktop build pointed at it, which I do not have here, so the end-to-end path - picking a certificate from the Windows store while connected to a server, and signing with it - is unverified. The routing logic is covered; the round trip is not. If a maintainer or @GUILHERME-GARCIATECH can try it, I would rather hear about it now than after merge.

Notes for reviewers

LocalEmbedPDF.tsx gains two lines (an import and the overlay mount). The overlay is inert until the cert-sign tool announces placement mode, so it costs nothing when the tool is not in use — but I would welcome a steer on whether that is the right home for it, which is question 1 on the issue.

samuelsl27 and others added 9 commits August 3, 2026 01:28
…te fields it shows

The visible signature was drawn at a hardcoded PDRectangle(0, 0, 200, 50), so a
caller could pick the page but not where on it, and the contents were fixed to
signer name, date and reason. Signing a form meant the signature landed in the
bottom-left corner regardless of where the document's signature area was.

Add four optional coordinates to the request plus a list of certificate fields
to draw. All are optional and, when none are supplied, the appearance is drawn
exactly as before - existing callers see no change.

Three pieces, each testable on its own:

SignatureBox owns the coordinate conversion. Callers place the box the way
someone dragging it on screen thinks - y measured downwards from the top edge -
while PDF's origin is bottom-left with y growing upwards. Keeping the flip in
one place is deliberate: it is the kind of arithmetic that looks right and is
wrong by exactly one page height. It also clamps, because a box dragged off the
page would otherwise produce an invisible signature that reads as a failure, and
an offset media box (cropped or imposed pages) would place it wrongly.

CertificateAttributeService reads the fields off the X.509 certificate and omits
the ones it does not carry rather than returning blanks, so a caller can offer
exactly the fields the user's own certificate can fill. A personal certificate
typically has a name and little else; an empty "Email:" line in a signature
looks like a defect.

SignatureAppearanceLayout scales the type to whatever box was drawn, shrinking
until the lines fit and ellipsising or dropping trailing lines when they cannot.
Text escaping the box would overlap the document itself.

29 tests, asserting the resulting geometry - measured line widths and total
height against the box, exact PDF rectangles for the axis flip - rather than
that a result came back.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…rop endpoint does

The box originally took y measured downwards from the top of the page, on the
grounds that it reads more naturally to someone dragging a rectangle. That put
it at odds with the rest of the API: CropController feeds its request values
straight into PDRectangle, so /crop is bottom-left origin with y growing
upwards, and the frontend's cropCoordinates helpers already produce exactly
that.

Keeping the other convention would have meant a second flip in the client, on
top of the one those helpers already do - the arithmetic most likely to end up
wrong by one page height, and the hardest to notice because the signature still
appears, just in the wrong place.

Two conventions across two endpoints is also the kind of thing a reviewer would
rightly ask about.

SignatureBox gets simpler as a result: the flip belongs to the client, and what
remains is the part worth keeping - clamping the box inside the page so it
cannot be dragged out of sight, and honouring a media box whose origin is not
(0,0), as cropped or imposed pages have.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…s it shows

Exposes the placement and field-selection the backend gained, so the feature is
reachable without hand-writing an API call.

SignaturePlacementPicker reuses the crop tool's CropAreaSelector rather than
growing a second area-selection widget. Users get the interaction they already
know - drag to draw, drag inside to move, corner handles to resize - and the
DOM-to-PDF coordinate maths stays in one implementation. That reuse is the
reason the backend was switched to PDF user space: cropCoordinates already
emits exactly the numbers the endpoint now takes, so the picker feeds it
straight through with no second conversion to get wrong.

It reads page dimensions from the page being signed rather than the first one,
since those differ in mixed-size PDFs, and falls back to A4 if the page cannot
be read - the backend clamps anything that overflows, so a wrong guess is
recoverable rather than fatal.

SignatureAttributePicker offers every field the backend understands rather than
only those the chosen certificate carries. The keystore is not opened until
signing, so the tool genuinely cannot know what is in it beforehand; ticking a
field that turns out to be absent costs nothing, because the backend skips it.

Both are additive: with no box drawn and no field ticked, the request is
byte-identical to what the tool sent before.

English and Spanish strings included.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two rules the earlier commits missed:

HowToAddNewLanguage requires new tags to land in en-US/translation.toml first -
it is the reference other locales are generated from - and only then in the
individual language files. The strings went into en-GB and es-ES but not the
reference, so any locale added later would have been missing these keys.

The frontend lint rule rejects Mantine's Button in favour of the shared
@app/ui/Button, which the attribute picker was importing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…tooltip

ADDING_TOOLS recommends tooltips for new controls, and the appearance step now
carries two the guide's existing tips say nothing about.

Extends the step's existing tooltip rather than adding a second one, since both
controls live in that step, and follows the guide's wording rules: what the user
gains rather than how it works, concrete rather than abstract - "drag its
corners to resize", "the text shrinks to fit" - and short enough to read while
deciding.

Also corrects a bullet that had gone stale: the visible-signature tip still said
the user could choose which page, which was the whole of the old placement
story and is now only half of it.

Strings land in en-US first as the reference locale requires, then en-GB and
es-ES.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Long documents are often initialled on every page so a reader can see at a
glance that the whole thing was signed, and the tool had no way to do that.

A PDF signature has one widget on one page, so this cannot be what it sounds
like: only the page named by pageNumber carries the signature. The other pages
get page content drawn to look the same, which carries no cryptographic meaning
and no validator will report. The request field says so, and the UI has to as
well - a document that looks signed in twelve places but is signed in one is
worse than one that is honest about it.

SignatureMarkStamper is deliberately a separate class from the signing code,
with a comment saying it must never touch the signature dictionary. Putting the
two in one file would make it easy to blur that line later by accident.

Order matters: marks are stamped before addSignature, so the signature covers
them. Stamping afterwards would alter the bytes the signature attests to and
every validator would flag the document as modified.

Marks are drawn in grey with a hairline border rather than matching the
signature exactly, so the two are distinguishable on the page.

Ten tests read the text back off each page - the only way to tell "marked" from
"skipped" - and cover the two that would break the document: that the signed
page is left alone, and that existing page content survives underneath.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… does

The box could only be positioned from a thumbnail in the side panel, which is
not how anyone expects to place a signature. Acrobat has you drag a rectangle
straight onto the page, and that is what this does.

SignatureBoxDragOverlay mounts on every page of the viewer but stays inert
until the tool announces placement mode. Whichever page the user drags on
becomes the signed page: having dragged a box onto page 5 and then signed page
1 would be a surprise, and the drag is the more deliberate of the two choices.

Tool and viewer talk over window events rather than a shared context. The
viewer is rendered far from the tool panel, so a context would have to be
threaded through the whole viewer tree for one optional feature; the project
already hands data to a tool this way for the guided tour's crop step.

The overlay reports PDF points with a bottom-left origin, which is exactly what
the endpoint takes, so the box travels from the user's mouse to the request
with a single conversion.

Two ways out of a modal gesture, because being stuck in one is worse than the
feature is worth: Escape, and the button that started it. The panel also cancels
on unmount - navigating away mid-drag would otherwise strand the viewer in
placement mode with no visible way back.

The side-panel thumbnail is now shown only for "repeat on every page", where
the same position applies to all of them and there is no single page to drag on.
That mode carries a visible warning that only one page is really signed; the
checkbox is disabled until a box exists, since there is otherwise no shape to
repeat.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…d link back

Three problems found by using it.

Drawing the box scrolled the document instead. The overlay swallowed the
mousedown but then listened for mousemove on the window, so the viewer went on
receiving the gesture and panned. Pointer capture fixes it properly: the events
are routed to the overlay until release, and touch-action/user-select stop the
browser interpreting the drag as a pan or a text selection.

Click-click now works as well as click-drag. A press released without real
movement sets the first corner and the next click closes the box, with the shape
previewing under the cursor in between. Both gestures are supported rather than
one replacing the other, since a drag that stalls would otherwise be a dead end,
and a 4px threshold keeps a shaky hand from losing the corner.

Marks on the other pages did not look like the signature: they were drawn in
Helvetica grey while the signature uses Times Bold black, which reads as a
rendering fault rather than the same signature repeated. They now match.

That removes the visual difference that made a mark identifiable, so honesty
now comes from behaviour instead: each mark carries a link annotation that jumps
to the page holding the real signature, where the reader can open its properties
and see who signed and whether it validates. A link is used rather than anything
signature-shaped precisely because it is inert - it navigates and does nothing
else.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…y works

The placement tip was written when the box was drawn on a thumbnail in the side
panel, and still said so: "drag a box on the page preview", "drag its corners to
resize it". Neither is true now - the box is drawn on the document and the
overlay has no corner handles - and a tooltip that describes an older version of
the UI is worse than none, because the user trusts it.

It now describes both gestures, and says which page ends up signed, since that
is decided by where the user draws rather than by the page field above it.

Adds the tip the all-pages option never had, leading with the part that matters:
only the page drawn on is really signed, and clicking a mark takes you there to
check it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@dosubot dosubot Bot added size:XXL This PR changes 1000+ lines ignoring generated files. enhancement New feature or request labels Aug 3, 2026
@stirlingbot stirlingbot Bot added Java Pull requests that update Java code Front End Issues or pull requests related to front-end development Back End Issues related to back-end development Translation Issues or pull requests related to translation API API-related issues or pull requests Test Testing-related issues or pull requests labels Aug 3, 2026
samuelsl27 and others added 2 commits August 3, 2026 12:42
Editing the file through PowerShell's Set-Content -Encoding utf8 re-read the
UTF-8 bytes as ANSI and wrote them back out, mangling every line with a
non-ASCII character and prefixing the file with a BOM:

    zeroPad = "Zero‑pad Width"   ->  "Zero‑pad Width"
    pageFormatA4 = "A4 (210×297mm)" -> "A4 (210×297mm)"

144 lines were affected, none of them ones this branch meant to touch, and the
diff carried 198 deletions that were pure collateral - enough noise to bury the
change under it.

Restored from main and reapplied the additions through a UTF-8-safe path. The
file's diff drops from +256/-198 to +52/-1, which is what it should have been.
en-US and es-ES were written by other means and were unaffected; both verified
clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@samuelsl27

samuelsl27 commented Aug 3, 2026

Copy link
Copy Markdown
Contributor Author

Two checklist items sorted:

Translation counter — ran it, no changes needed; the README badges were already accurate. Worth flagging that the script is scripts/counter_translation_v3.py now, while the PR template still points at scripts/counter_translation.py, which no longer exists.

Docs — opened Stirling-Tools/Stirling-Tools.github.io#123. Certificate-Signing.md said the visible signature "appears as a box on a chosen page showing the signer name, signing date, and reason", which this PR makes incomplete on all three counts.

The every-page option is documented behind a caution rather than a plain description: only the drawn-on page is really signed, and I would rather a reader learned that from the docs than discovered it after signing. If that option gets dropped from this PR, that section comes out with it.


On why the every-page option exists at all, since it is the part I would understand you hesitating over:

It is aimed at organisations and technical practices rather than one-off signing. In Spain, professional bodies (colegios profesionales — architects, engineers and so on) run a mandatory endorsement process called visado for technical documents, and the endorsement stamp is applied to every page of the project, not just the cover. A drawing set that carried the stamp only on page 1 would not be accepted, because there would be nothing preventing pages being swapped afterwards.

The same reasoning drives the practice of initialling every page of a paper contract. Anyone reproducing that workflow digitally today has to stamp the pages by other means and then sign, which is both awkward and easy to get wrong.

That is exactly why the marks link back to the signed page rather than merely looking like signatures: the reader gets the visual continuity the process expects, and one click to the signature that actually carries the cryptographic weight.

@stirlingbot stirlingbot Bot added the has conflicts Pull request has merge conflicts with the base branch label Aug 10, 2026
Resuelve el conflicto de CertSignController.java, que venia de tres
cambios del original fusionados despues de que saliera esta rama:

- Stirling-Tools#7400 movio CreateSignatureBase a stirling.software.SPDF.pdf.signature
- Stirling-Tools#7204 anadio @toolio(produces = ToolFormat.PDF)
- Stirling-Tools#6334 cambio getWidgets().get(0) por getWidgets().getFirst()

En los tres gana la version del original; la logica propia (el rect
condicionado al box, drawAttributeText y las marcas) se conserva intacta.
The visible signature could only draw the bundled Stirling mark, scaled to
0.08 and pinned at a fixed offset that ignored the signature box entirely.
Requested in discussion Stirling-Tools#7277, where the reporter wanted their company logo
the way Foxit does it.

Adds an optional logoImage upload (PNG or JPEG) and a logoPosition with five
values: LEFT, RIGHT, TOP, BOTTOM and BEHIND.

SignatureLogoPlacement is a separate, PDFBox-light class holding the geometry,
so the five positions are unit-testable without rendering anything. Two rules
it enforces:

- The image keeps its aspect ratio. A stretched company logo reads as a
  rendering fault rather than a design choice.
- The text keeps at least 65% of the box in both directions, so a
  banner-shaped logo cannot squeeze the signer out of their own signature.

BEHIND is the exception to the split: the logo fills the box as a watermark
and the text keeps the whole box, drawn on top. It reuses the MULTIPLY blend
at half opacity the bundled mark already used, which is what keeps text
readable over it.

Nothing changes for existing callers: with no box, no field selection and no
logo parameters, the appearance is byte-for-byte what it was. The marks
stamped on the other pages now receive the same effective logo as the
signature, since a mark meant to match it cannot be missing the logo it has.

Also adds the certSign.appearance.tooltip.attributes section that en-GB was
missing while en-US and es-ES had it.
Closes Stirling-Tools#7316. The desktop offers the Windows certificate store and plugged-in
PKCS#11 tokens as signing sources, but connecting it to a self-hosted server
made them disappear. Three separate causes, all of them routing:

1. The app-config comes from whichever backend the app is talking to, and it
   carries hardwareSigningAvailable. A self-hosted server answers that for
   itself - false, since it is not a desktop - so CertificateTypeSettings hid
   "This device" and reset signMode to MANUAL. The desktop now re-answers that
   one flag from its own machine and leaves the rest of the config alone: the
   others describe the deployment, where the server is the authority.

2. /api/v1/security/cert-sign/hardware/** matched the tool-endpoint rules and
   went to the server, which reported on its own hardware or refused outright.

3. Signing itself posted to the server, which cannot reach a key held in the
   user's store or token.

The list of device-local endpoints lives in src/core, MIT, so the routing layer
consults it rather than keeping a catalogue of which tools need local hardware -
the same shape it already uses for CONVERSION_ENDPOINTS, and what Stirling-Tools#7510
suggested. Signing cannot be decided by path, since the endpoint is the same
whichever certificate was picked, so the caller marks that request instead.

Auth now follows the destination rather than the connection mode. Before, a
request routed to the local backend in self-hosted mode still carried the
server's JWT to loopback.

Nothing is loosened: HardwareKeyStoreService.assertLocalDesktop already rejects
these calls unless they come from the desktop bundle over loopback, and a
regression test pins that an uploaded keystore still signs on the server.
@samuelsl27 samuelsl27 changed the title feat(sign): place the visible signature, choose its fields, and use your own logo feat(sign): place the visible signature, choose its fields, use your own logo, and keep device certificates working against a server Aug 16, 2026
@samuelsl27 samuelsl27 changed the title feat(sign): place the visible signature, choose its fields, use your own logo, and keep device certificates working against a server feat(sign): make the visible signature configurable, and fix device certificates in self-hosted mode Aug 16, 2026
The desktop override shadowed the whole of core/api/config.ts: five functions
re-exported unchanged so that one could be wrapped. The frontend guide asks for
the opposite - "instead of duplicating the entire file, create a new extension
module for the core app and override that" - and gives the reason, which is a
real bug rather than a style point: a sixth export added to core tomorrow would
simply vanish from the desktop build.

Core now owns applyDeviceCapabilities(), a no-op that returns the config as it
came, and fetchAppConfig calls it. The desktop overrides that one function.

Also drops the mentions of specific builds from comments in core. The guide is
explicit that core must not reference build targets by name, and the wording
was not carrying its weight anyway: what matters is that some builds have a
second backend to choose from, not which ones they are.
samuelsl27 and others added 2 commits August 16, 2026 22:38
createToolFlow renders the execute button after the steps, inside the panel's
scroll area, so a tall tool pushes it below the fold. Signing a PDF with a
certificate builds a 1703px panel against an 811px viewport at a 900px window:
the button only appears after scrolling almost twice the visible height, and a
user who does not think to scroll cannot run the tool at all.

Pin it to the bottom instead. Sticky costs nothing when the panel fits - the
button stays exactly where it was, as Convert and Compress still show - and
only holds its place once there is more content than room.

The wrapper is a flex column on purpose: the button was a direct child of the
Stack and stretched to its width, and a plain block would shrink it to its
label.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
It is the tallest step in the app, and the run button sits after it, so its
height is what decides whether the user can find the button at all.

The reset for the drawn box was a full-width button that only existed once a
box had been placed, so completing the placement made the panel a row taller
at exactly the moment the user was looking for the button. It is now the same
icon the thumbnail picker already uses, always present and disabled until
there is something to reset, so the height cannot change underfoot.

The fourteen certificate fields fold away behind a toggle that states what is
currently chosen. They are an advanced choice - leave them alone and the
backend draws the fields it always has - and they were some 300px of a panel
that already ran past the bottom of the window. Measured against the same
document and certificate: 1703px of panel before, 1384px after.

They are unmounted rather than collapsed. Mantine's Collapse keeps its
children in the page, so fourteen invisible tick boxes would still take
keyboard focus on the way to the button.

The new test was seen to fail against the old code, which is what makes it
worth having: seven controls without a box, eight with one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Aug 16, 2026

Copy link
Copy Markdown
Contributor

🌐 TOML Translation Verification Summary

🔄 Reference Branch: pr-branch

📃 File Check: en-US/translation.toml

  1. Test Status:Passed
  2. Test Status:Passed
  3. Test Status:Passed

✅ Overall Check Status: Success

Thanks @samuelsl27 for your help in keeping the translations up to date.

AGENTS.md is explicit that en-US is the only locale to edit by hand, and the
PR check makes the cost of ignoring it concrete: check_toml.yml verifies every
locale file a PR touches against that PR's en-US, so touching en-GB and es-ES
pulled them into a comparison they cannot pass. They are 167 keys ahead of
en-US and 150 behind it - and so is main, which is the point: the drift is the
project's own, carried by Crowdin, and nothing this branch can fix.

en-US keeps both new keys. The other locales fall back to it until Crowdin
catches up, which is the arrangement the project already relies on.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
samuelsl27 added a commit to samuelsl27/Stirling-PDF that referenced this pull request Aug 16, 2026
El PR Stirling-Tools#7262 se queda solo con en-US, porque check_toml.yml compara cada
fichero de idioma que toca un PR contra su propio en-US, y en-GB y es-ES
arrastran el desfase del proyecto: 167 claves de mas y 150 de menos, las
mismas que tiene main. No es algo que la rama pueda arreglar.

Pero mi-version no se propone a nadie, asi que aqui si se pueden tener: son
63 cadenas del panel de firma que en el original todavia no existen, y sin
ellas la herramienta sale en ingles dentro de una interfaz en espanol. En
cuanto el PR se fusione y Crowdin las traduzca, este commit se podra tirar.

Se restauran los dos ficheros exactamente como estaban en 1f05319, de modo
que el arbol vuelve a ser el mismo del que salio el MSI ya construido.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
samuelsl27 and others added 2 commits August 17, 2026 03:13
AppLayout puts banners above the app and rewrites .h-screen to 100% so that
everything below shrinks to what is left. The floating tool panel measured
itself in viewport units instead, so with a banner up it hung off the bottom of
the window by the banner's height - and since it clips its own overflow, that
strip was unreachable. No amount of scrolling brought it back.

What lived in the strip was the run button, pinned to the bottom of the panel.
Signing a PDF was therefore impossible while the desktop's banner offering to
make Stirling PDF the default PDF application was showing, and possible again
the moment it was dismissed, which is what made the fault look intermittent.

Measured at 1280x800 with a 56px banner: the panel ended 48px past the bottom of
the window before, and inside it after. The file sidebar has always used 100%;
this brings the rail into line with it.

The regression test inserts a banner rather than waiting for a real one: all
four are desktop-only and which of them shows depends on the machine, while what
the panel cares about is only that something above it took height away.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…d third

A third of the box was arbitrary in both directions. In a roomy box it left room
the text had no use for; above a short signature it gave the logo a band about a
millimetre tall, which users read - reasonably - as the logo not working.

The strip is now searched downwards from half the box and stops at the largest
share where the text still keeps every line, and type no more than a quarter
smaller than it would have had with the whole box to itself. The question is put
to SignatureAppearanceLayout, which is the class that already decides how text
fits, so nothing new decides it.

The old third stays as an unconditional floor, and that is the part worth
remembering. Sizing purely by what the text can spare makes a band above a short
signature thinner than it used to be, because three fields underneath demand
every point of the height - the very case that prompted the change. The floor is
what keeps the fix from regressing it, and the test checks it against the old
formula rather than against a number, so lowering it later fails there instead
of quietly shrinking logos.

Measured with a 4:1 image; logo width beside the text, band height above it:

  300x120   105 -> 145.2 pt    42 -> 55.2 pt
  200x60     67.6 -> 97.6 pt   18.6 -> 18.6 (floor)
  60x15      20.4 -> 29.4 pt    4.7 ->  4.7 (floor)

The chosen position is never second-guessed: what gives is the size, which is
what the user can see and adjust. BEHIND is unchanged - it already scaled to the
box - and now has a test pinning that for cramped boxes too.

The marks stamped on the other pages inherit all of it, because they go through
the same class. That is what the separation is for.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@GUILHERME-GARCIATECH

Copy link
Copy Markdown

Thanks for the work on the visible signature appearance. I tested the latest version of the branch in a real Windows Desktop + self-hosted setup.

The signing itself is working, but I found a few UX/layout issues that I think are worth improving.

1. Certificate selection UX

The current certificate selector works, but it becomes difficult to use when several certificates are available. Long certificate names overflow or become hard to distinguish inside the narrow side panel.

A dedicated modal/dialog for certificate selection would probably work much better. It could show, for example:

  • Friendly name / subject
  • Issuer
  • Expiration date
  • Certificate type
  • A clear “Select” action

The side panel could then only display the certificate that is currently selected.

Image

For comparison, this is how Foxit handles certificate selection. The list is still simple, but because it has more horizontal space it is much easier to identify which certificate is being selected.

Image

2. Signature layout does not adapt well to the selected fields

The visible signature currently seems to be designed around having most or all fields enabled.

When only a few fields are selected, the layout does not really adapt to the available space. The result can be:

  • Very small text inside a relatively large signature rectangle
  • Large amounts of unused whitespace
  • Poor balance between the logo and text
  • Very different visual results depending on how many fields are enabled

Ideally, the layout should adapt dynamically based on the content that is actually being rendered.

For example, if only Signed by, Date, and Reason are enabled, those fields could use more of the available area and a larger font instead of keeping the same proportions intended for a much larger amount of metadata.

Image

3. Logo sizing and positioning

The recent changes improved the logo behavior, but there are still some issues.

Depending on the signature rectangle and the number of enabled fields, the logo can become:

  • Too large
  • Too small
  • Poorly positioned relative to the text
  • Partially cropped

My custom logo, for example, gets clipped in some signature sizes.

It would be useful if the logo preserved its aspect ratio and was constrained to the actual remaining area after the text layout is calculated.

Image Image

4. Comparison with a more compact signature layout

As a reference, this is approximately the kind of adaptive layout I would expect.

Foxit's default visible signature uses the available rectangle much more efficiently: the signer identity gets appropriate emphasis, the detailed certificate information uses the remaining space, and the layout stays balanced.

I am not suggesting copying Foxit's design exactly, but I think it is a useful reference for how the signature content can adapt to the available area.

Image

One additional note: initially I thought the generated signature quality itself was low, but after opening the resulting PDF outside Stirling I confirmed that the exported PDF quality is good. The lower quality I observed appears to be related to the preview/rendering inside the Stirling viewer, so I do not consider that a signature-generation problem.

Overall, the functionality is working well now. My feedback here is mainly about making the certificate selection and visible signature layout more polished, adaptive, and intuitive.

@github-actions github-actions Bot added the has conflicts Pull request has merge conflicts with the base branch label Aug 27, 2026
97 commits del original, hasta d3708c1 (version 2.14.3). Dos conflictos,
los dos resueltos a favor del original, porque el original ha arreglado
por su cuenta el boton de ejecutar que no se veia:

- createToolFlow.tsx  -> Stirling-Tools#7688, de Frooodle. Hace lo mismo que nuestro
  50dca21 casi propiedad por propiedad (position: sticky, bottom: 0,
  z-index: 2, fondo var(--c-surface), margin-inline negativo), pero lo
  saca a un createToolFlow.module.css en vez de dejarlo en estilos en
  linea, lo aplica solo cuando el paso de revision no esta visible -- si
  no, la barra flotaria sobre los resultados -- y arregla ademas dos
  mecanismos de scroll muertos desde mayo: el closest() de ReviewToolStep,
  que nunca casaba porque Mantine escribe overflow: scroll, y un ref de
  Convert.tsx que no estaba enganchado a ningun elemento.

- ToolPanel.css  -> la reforma de barras laterales (Stirling-Tools#7518, Stirling-Tools#7660, Stirling-Tools#7695)
  ya deja .tool-panel--floating en height: 100%, que es lo que buscaba
  nuestro 81998b1 al sustituir el 100vh.

Su version es la mas completa, asi que se queda esa. El diff neto de esta
rama en los dos ficheros era exactamente esos dos arreglos y nada mas, de
modo que la resolucion no pierde ni una linea del trabajo de la firma: sus
ficheros propios pasan de 45 a 43.

El resto auto-fusiona, incluido en-US/translation.toml.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot removed the has conflicts Pull request has merge conflicts with the base branch label Aug 28, 2026
samuelsl27 and others added 7 commits August 28, 2026 17:18
addBanner busca el contenedor de AppLayout por sus estilos en linea para
colgarle una barra encima. Comparaba la altura contra "100vh", y el Stirling-Tools#7518
la cambio a "100dvh" para que el cromo del navegador movil no recorte la
aplicacion, asi que la busqueda no encontraba nada: addBanner devolvia
false y el test caia en su primera asercion, antes de llegar a medir el
boton.

Acepta las dos unidades. El guard que ya habia -- devolver false si no
hay shell -- se conserva a proposito: sin el, un cambio futuro de layout
dejaria el test midiendo una ventana sin barra, que pasa siempre.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…o it

The visible signature was designed around having most fields enabled. With
three, the type stopped at 12pt whatever the box, so a large rectangle came
out mostly empty, and a long subject name shrank the whole block to 4pt and
was then cut short with an ellipsis while space went unused below it.

The type now grows as well as shrinks, and long values are broken across
lines. Filling each line before starting the next is what makes the line
count rise with the size and never fall, so the largest size that fits can
be bisected for: nine passes instead of the previous thirty-three, and one
call to the layout per signature instead of ten.

Three things had to change for the type to be able to grow at all. The
margin is now a share of the box rather than a multiple of the font size,
which was circular. The block is measured against the font's bounding box
rather than its ascent and descent, so an accented capital - routine in the
names this feature exists to stamp - is not shaved by the top of the box.
And the height left over once the width is exhausted goes to the leading.

The logo split follows: with the text sizing itself to whatever it is
handed, there is no longer any text quality to trade a larger logo against,
so the search over shares is gone and the logo simply takes the largest
strip its shape can use. That also ends a real instability - enabling one
more field could shrink the logo by 17% - and makes left and right mirror
each other, which they did not. The logo is held one margin off the edge so
neither the appearance clip nor the mark's border can shave it.

The signer's name is drawn larger than the rest, marked on the attribute
rather than on its label so it survives translation.

Both drawers now share one routine that consumes already-positioned lines,
so the signature and the marks on the other pages cannot drift apart.

The legacy appearance is untouched: it reaches neither class.
The certificate picker was a dropdown in the tool panel, which is a few
hundred pixels wide. Everything a person needs to tell two certificates
apart - the subject, the issuer and the expiry date - was crammed into one
line, so with several certificates from the same authority the names ran
into each other and choosing between them was guesswork.

The list moves to a dialog, where there is room to give the subject, the
issuer, the expiry, the validity and the source a column each, and to search
by name, issuer or serial number. The panel keeps only the certificate that
is chosen. Expired and not-yet-valid certificates are still listed, because
knowing one is there and unusable beats wondering where it went, but they
cannot be picked - as before.

The loading moves to a hook shared by the panel and the dialog, so the two
cannot end up reading the store separately and disagreeing about it. The
naming and ordering rules move to a module of their own: they were closures
inside the component and could not be tested, and they carry the judgement
that matters here - a certificate whose subject is a bare GUID is named by
its Windows friendly name, a self-signed one does not repeat its own name as
its issuer, and what you can sign with sorts above what you cannot.

Only the alias is carried in the tool's parameters, so the panel re-derives
the rest from the loaded list. A token needs its PIN to enumerate, so after
the panel is reopened the alias can be all there is to show; that was true
before and is unchanged.
The legacy appearance draws the logo at a size taken from the image's own
pixels rather than from the box, which is the one place in this feature where
an image can be drawn larger than the area it is clipped to.

It does not misbehave today: the bundled mark is 512x512, which at the
long-standing 0.08 scale is 41pt inside a 200x50 box, and a custom logo
cannot reach this path at all. What is wrong is that nothing says so. An
image larger than the box is now letterboxed into it instead, and the two
numbers that decide this have names.

The path is otherwise left exactly as it was, down to the operators: the
guard only diverts an image that would not have fitted, so every existing
caller gets the same content stream it did before. The new test pins that
by reading the operators back out of the signed document - it fails on a
one-hundredth change to the scale.
The field labels drawn inside the signature came from a switch in Java that
only speaks English, while the tick boxes that choose those same fields are
translated. Someone using the application in Portuguese picked "Assinado
por" and got a document stamped "Signed by".

There is nowhere in the backend to fix this: the project keeps one
messages.properties with no per-language variants, and every translation it
has lives in the frontend's TOML files. So the caller sends the labels it
already has, alongside the fields they belong to, and Java falls back to
English for anything it is not given. That also means the signature is
stamped in the language of the person signing rather than of the server,
which for an appearance baked into the document is the right end to decide
it from.

The labels ride on the CreateSignature instance rather than widening
sign(...) for the fifteenth time, which is where the custom logo already
lives for the same reason.
The box arrives measured against what a viewer shows: the crop box, turned by
the page's rotation. It was being read against the media box, unturned. On an
ordinary page those are the same rectangle and nothing was wrong; on a trimmed,
imposed or rotated page they are not, and the signature landed somewhere the
user had not drawn it - which is what being told your logo is "cropped in some
signature sizes" looks like from the other side.

The new test says it plainly: against the old code, a page with a crop box
inset from the sheet renders with nothing drawn on it at all. The mark was
placed off the visible area entirely.

Rotation needs the appearance turned as well as the rectangle moved, or the
signature reads down the side of the page. The layout is done the way up the
reader sees things and turned into the page's own coordinates afterwards, once
for the signature through the appearance matrix and once for the marks through
the content stream. The render test measures the result: against the old code a
200x60 mark came out 44x179.

SignatureBox now takes the page rather than a rectangle, since the crop box and
the rotation both have to come from it, and the rectangle overload had no
callers left.
… module

Importing the application's own i18n module to read the field labels pulled
its whole bootstrap into every file that touches the cert-sign operation, and
two suites that mock react-i18next stopped loading because of it.

The bare i18next singleton is the same instance without the bootstrap. When it
has no translations loaded there is nothing useful to send, so nothing is sent
and the backend falls back to English, which is what it does for a caller that
never had them.
A box too small for every field drops the ones that do not fit and marks the
last line it kept, so the reader can tell the signature is short of something.
The mark was appended to that line whatever it was, so a box that dropped
"Location" left "Serial: 3245c6f5c366ecf5..." — a complete serial number wearing
the mark that means "this value continues". A verifier comparing it against the
certificate reads a mismatch, which is a worse failure than a missing field.

The mark now only joins the last character of a value the box cut in half. After
a whole value it follows a space, and when even that does not fit the line is
left alone: a signature that misstates what it certifies is worse than one that
quietly shows a field fewer.

Found by rendering the placement grid and looking at it: at 200x60 with the logo
in a TOP or BOTTOM band, six fields lose "Location" and the serial grew an
ellipsis it had not earned.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@samuelsl27

Copy link
Copy Markdown
Contributor Author

@GUILHERME-GARCIATECH thank you for testing this on a real Windows desktop and writing it up so
precisely. This is the first hands-on review the PR has had, and all four of your points turned out
to be actionable — the branch now carries seven commits that answer them. Point by point, including
what I did not do.


1. Certificate selection

Done. The dropdown is gone, and the list moved into a dedicated dialog with the columns you asked
for: Certificate (friendly name, with the full subject beneath it), Issued by, Valid
until
, Status and Type — plus a search box over name, issuer and serial number for when the
store has many. Expired and not-yet-valid certificates stay visible but are not selectable, so you
can see why one is unavailable instead of wondering where it went. Clicking a row selects it and
closes the dialog.

The side panel is then just the summary — the chosen certificate's name, its issuer, its expiry
date and a Change certificate button — which is what you asked for.

One limitation I would rather state than hide: for the Windows store the list reloads by itself, so
the summary survives a remount of the panel. PKCS#11 needs the PIN to enumerate, so after a remount
the panel falls back to showing the alias alone until you open the dialog again. That was already
the behaviour before this change — I have not made it worse, but I have not fixed it either.


2. Signature layout does not adapt to the selected fields

This was the real bug, and your diagnosis was exact. The layout only ever searched downwards from
a 12 pt ceiling: it could shrink to fit but never grow. Three fields in a large box got 12 pt type
and left the rest blank — precisely your screenshot.

It now finds the size by bisection over the range the box allows, with the ceiling raised from 12 pt
to 72 pt so the type can actually grow. It wraps long values across lines (continuations indented
under their label, so Signed by still reads as one field), measures with the font's real ascent and
descent instead of approximating from the point size, and centres the block vertically. When the
width binds before the height does — two short fields in a tall box — the leftover goes into the
leading rather than leaving a gap at the bottom. Signed by is set at 1.6× the other fields, which
is the piece of your Foxit reference I did take.

I render a grid for this: 4 box sizes × {3 fields, 6 fields} × {no logo, 1:1 logo, 4:1 logo} × 5
logo positions = 88 signatures, before and after. Measuring how much of the box height everything
drawn actually covers:

before after
average across all 88 67 % 90 %
worst case 11 % 82 %
spread between best and worst 86 points 12 points
no logo, 3 fields vs 6 fields 24 % vs 50 % 87 % vs 88 %

The last row is your complaint restated as a number. "Very different visual results depending on how
many fields are enabled" was a 26-point gap; it is now 1 point. And the worst case in the whole grid
went from a box that was 89 % empty to one that is 18 % empty.


3. Logo sizing and positioning

Done. SignatureLogoPlacement used to search for a strip between 50 % and 36 % of the box and
settle for whatever the text would tolerate — and when the text tolerated nothing it fell back to
35 % anyway, overriding its own veto. That is the "sometimes too large, sometimes too small" you saw.
It no longer searches; it computes.

For LEFT and RIGHT the logo takes the full box height, converted to width by its own aspect ratio,
capped at half the box width. TOP and BOTTOM are the transpose. BEHIND is unchanged: the image
covers the box and the text is drawn on top of it. The aspect ratio is always preserved, and the
text gets exactly what is left over.

About the clipping specifically. There is now a 4 % inset between the logo and the edge of the
box. The appearance stream is clipped to exactly the box, and the mark stamped on the other pages
draws its border down the same line, so an image drawn hard against that edge loses a row of pixels
to rounding — which is exactly what a cropped logo looks like to the person who uploaded it. That is
my best explanation of what you saw. If your logo still clips after this, please tell me the box
size and the image dimensions and I will chase it.

And the whole thing now scales with the rectangle you draw. Coverage of the box height by logo
position, before → after: LEFT 70 % → 91 %, RIGHT 70 % → 91 %, TOP 80 % → 93 %, BOTTOM 94 % → 91 %,
BEHIND 37 % → 87 %. The five positions used to range from 37 % to 94 %; they now sit between 87 %
and 93 %.

One case the grid found that I have not changed. With the logo in a TOP or BOTTOM band, the band
is as tall as the logo needs and no taller — but a square logo in a wide, short box (200 × 60) is
then 25 pt tall, centred in a 200 pt band, so most of that band is empty while the text is left with
half the box. The same box with the logo on the LEFT gives the logo more than twice the size and
the text all six fields. I have left it, because the band already is the logo's own height and any
rule that shrinks it further is a guess about what someone meant by choosing "above the text" for a
box that shape. If you run into it in practice, say so and I will revisit it.

And one it found that I did fix. In that same cramped case the layout drops the fields it cannot
hold and marks the last line it kept — and the mark was landing on Serial: 3245c6f5c366ecf5...: a
complete serial number wearing the sign that means "this value continues". Anyone checking it against
the certificate would read a mismatch. The mark now only joins a value the box actually cut, and
after a whole value it follows a space. That is the last commit on the branch.


4. Comparison with Foxit

Partially, and deliberately so. I took two things from it: the signer identity gets its own emphasis,
and the box is divided by what the content needs rather than by fixed proportions.

I did not reproduce Foxit's design — no two-column metadata block, no separator rules, no fixed
identity band. The reason is that Stirling's visible signature has to survive a rectangle the user
drags to any size and aspect, including strips like 200 × 60 that Foxit's layout never has to face,
and every borrowed structure is one more thing that can break at the extremes. If there is a specific
piece of it you think is worth having, name it and I will look at it properly.


The point you withdrew

Noted, and thank you for checking before reporting it. The viewer's rendering is a separate concern
from what gets written into the PDF.


Two more things your report turned up

  • Cropped and rotated pages. The rectangle you drag is measured in pdf.js's viewport, which is
    the CropBox with /Rotate applied. The backend was reading it against the MediaBox and ignoring
    rotation entirely, so on a trimmed or rotated page the signature landed offset and at the wrong
    scale. Fixed, with the rotation composed into the appearance stream's matrix so the text still
    reads upright on a rotated page.
  • The stamp was always in English. You run the app in Portuguese and the signature still came out
    with Signed by, Date, Reason. There is a single messages.properties on the backend with no
    per-language variants — all of the project's translation lives in the frontend TOMLs — so the
    labels are now sent from the frontend, which already has them translated, with the English strings
    kept as the fallback.

How this was checked

I render a grid of 88 signatures — every combination above — against both the code you tested and
this branch, and read all 176 renders rather than trusting the tests alone. That is where the two
items at the end of point 3 came from.

What has not been checked is the certificate dialog against a real Windows certificate store. I
have driven it in the test harness against mocked certificates — with long ICP-Brasil style names,
several at once, one of them expired — so the columns, the search and the flow are exercised, but the
enumeration itself is not. If it behaves differently against your own certificates, that is worth a
shout.

All of it is on the branch. I would be grateful if you could run your test again — and if
before-and-after screenshots of any of this would help, say which case and I will post them.

samuelsl27 and others added 2 commits August 30, 2026 18:12
…oaded

The pipeline builder renders every tool's settings so a run can be configured
before any document exists, and it mounts no file context on purpose. The
appearance panel subscribed to the workbench files at its top level, and the
file hooks throw outside their provider, so opening a certificate-signing step
there crashed the panel with "File hooks must be used within a
FileContextProvider".

The subscription moves into a child that is mounted only when the provider is
there. In the workbench nothing changes: the picker still receives the loaded
document and its thumbnail. In the pipeline builder the picker draws over an
empty page, which is what configuring a run without a document can show.

Measured both ways: PipelineStepSettings goes from failing on certSign to its
six tests passing, and it passes on main without this branch, so the crash came
from here and not from upstream.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
samuelsl27 added a commit to samuelsl27/Stirling-PDF that referenced this pull request Aug 30, 2026
El PR Stirling-Tools#7262 se queda solo con en-US, porque check_toml.yml compara cada
fichero de idioma que toca un PR contra su propio en-US, y en-GB y es-ES
arrastran el desfase del proyecto: 167 claves de mas y 150 de menos, las
mismas que tiene main. No es algo que la rama pueda arreglar.

Pero mi-version no se propone a nadie, asi que aqui si se pueden tener: son
63 cadenas del panel de firma que en el original todavia no existen, y sin
ellas la herramienta sale en ingles dentro de una interfaz en espanol. En
cuanto el PR se fusione y Crowdin las traduzca, este commit se podra tirar.

Se restauran los dos ficheros exactamente como estaban en 1f05319, de modo
que el arbol vuelve a ser el mismo del que salio el MSI ya construido.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 79c64bd)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

API API-related issues or pull requests Back End Issues related to back-end development enhancement New feature or request Front End Issues or pull requests related to front-end development Java Pull requests that update Java code size:XXL This PR changes 1000+ lines ignoring generated files. Test Testing-related issues or pull requests Translation Issues or pull requests related to translation

Projects

None yet

2 participants