Skip to content

feat: Sign usign only PHP#2595

Merged
vitormattos merged 45 commits intomainfrom
feature/sign-usign-only-php
Mar 4, 2026
Merged

feat: Sign usign only PHP#2595
vitormattos merged 45 commits intomainfrom
feature/sign-usign-only-php

Conversation

@vitormattos
Copy link
Member

@vitormattos vitormattos commented Mar 26, 2024

Target: Make possible sign using only PHP

Start to implement jeidison/pdf-signer

ref: jeidison/signer-php#3

Thanks to @jeidison, the author of package pdf-signer-php

Administration setting: change the signature engine
image

Example with different sign modes:

Type of signature Presentation
Description only image
Signature and description image
Signer name and description image
Signature only image

Performance test: Results for 10 documents with clickToSign:

Engine Start End Duration
PhpNative 18:32:49 18:33:08 19s
JSignPdf 18:33:55 18:34:32 37s

PhpNative is ~2× faster than JSignPdf (1.9s vs 3.7s per document). The native PHP implementation cuts signing time roughly in half by eliminating the Java/JVM overhead.

Was used the follow scenario to make the performance test:

Feature: performance benchmark - sign N documents to compare signing speed
  # PURPOSE: Compare signing performance between signature engines.
  #
  # - JSignPdf engine: signing via external Java process (slow)
  # - PhpNative engine: signing via native PHP library (fast)
  #
  # HOW TO RUN (inside the container, from tests/integration directory):
  #   Run each scenario individually so that "time" measures only that engine:
  #
  #   vendor/bin/behat features/sign/performance.feature --name "JSignPdf"
  #   vendor/bin/behat features/sign/performance.feature --name "PhpNative"
  #
  # The date markers (>&2) print directly to the terminal regardless of verbosity.

  Background:
    Given as user "admin"
    And run the command "config:app:set libresign signing_mode --value=sync --type=string" with result code 0
    And run the command "libresign:install --use-local-cert --java" with result code 0
    And run the command "libresign:install --use-local-cert --jsignpdf" with result code 0
    And run the command "libresign:install --use-local-cert --pdftk" with result code 0
    And run the command "libresign:configure:openssl --cn=Common\ Name --c=BR --o=Organization --st=State\ of\ Company --l=City\ Name --ou=Organization\ Unit" with result code 0
    And sending "post" to ocs "/apps/provisioning_api/api/v1/config/apps/libresign/identify_methods"
      | value | (string)[{"name":"account","enabled":true,"mandatory":true,"signatureMethods":{"clickToSign":{"enabled":true}}}] |
    And the response should have a status code 200
    When sending "post" to ocs "/apps/libresign/api/v1/request-signature"
      | file | {"url":"<BASE_URL>/apps/libresign/develop/pdf"} |
      | signers | [{"identify":{"account":"admin"}}] |
      | name | D1 |
    And fetch field "(SIGN_UUID_1)ocs.data.signers.0.sign_uuid" from previous JSON response
    When sending "post" to ocs "/apps/libresign/api/v1/request-signature"
      | file | {"url":"<BASE_URL>/apps/libresign/develop/pdf"} |
      | signers | [{"identify":{"account":"admin"}}] |
      | name | D2 |
    And fetch field "(SIGN_UUID_2)ocs.data.signers.0.sign_uuid" from previous JSON response
    When sending "post" to ocs "/apps/libresign/api/v1/request-signature"
      | file | {"url":"<BASE_URL>/apps/libresign/develop/pdf"} |
      | signers | [{"identify":{"account":"admin"}}] |
      | name | D3 |
    And fetch field "(SIGN_UUID_3)ocs.data.signers.0.sign_uuid" from previous JSON response
    When sending "post" to ocs "/apps/libresign/api/v1/request-signature"
      | file | {"url":"<BASE_URL>/apps/libresign/develop/pdf"} |
      | signers | [{"identify":{"account":"admin"}}] |
      | name | D4 |
    And fetch field "(SIGN_UUID_4)ocs.data.signers.0.sign_uuid" from previous JSON response
    When sending "post" to ocs "/apps/libresign/api/v1/request-signature"
      | file | {"url":"<BASE_URL>/apps/libresign/develop/pdf"} |
      | signers | [{"identify":{"account":"admin"}}] |
      | name | D5 |
    And fetch field "(SIGN_UUID_5)ocs.data.signers.0.sign_uuid" from previous JSON response
    When sending "post" to ocs "/apps/libresign/api/v1/request-signature"
      | file | {"url":"<BASE_URL>/apps/libresign/develop/pdf"} |
      | signers | [{"identify":{"account":"admin"}}] |
      | name | D6 |
    And fetch field "(SIGN_UUID_6)ocs.data.signers.0.sign_uuid" from previous JSON response
    When sending "post" to ocs "/apps/libresign/api/v1/request-signature"
      | file | {"url":"<BASE_URL>/apps/libresign/develop/pdf"} |
      | signers | [{"identify":{"account":"admin"}}] |
      | name | D7 |
    And fetch field "(SIGN_UUID_7)ocs.data.signers.0.sign_uuid" from previous JSON response
    When sending "post" to ocs "/apps/libresign/api/v1/request-signature"
      | file | {"url":"<BASE_URL>/apps/libresign/develop/pdf"} |
      | signers | [{"identify":{"account":"admin"}}] |
      | name | D8 |
    And fetch field "(SIGN_UUID_8)ocs.data.signers.0.sign_uuid" from previous JSON response
    When sending "post" to ocs "/apps/libresign/api/v1/request-signature"
      | file | {"url":"<BASE_URL>/apps/libresign/develop/pdf"} |
      | signers | [{"identify":{"account":"admin"}}] |
      | name | D9 |
    And fetch field "(SIGN_UUID_9)ocs.data.signers.0.sign_uuid" from previous JSON response
    When sending "post" to ocs "/apps/libresign/api/v1/request-signature"
      | file | {"url":"<BASE_URL>/apps/libresign/develop/pdf"} |
      | signers | [{"identify":{"account":"admin"}}] |
      | name | D10 |
    And fetch field "(SIGN_UUID_10)ocs.data.signers.0.sign_uuid" from previous JSON response

  Scenario: Sign 10 documents using click-to-sign with JSignPdf engine
    Given as user "admin"
    And run the command "config:app:set libresign signature_engine --value=JSignPdf --type=string" with result code 0
    And run the bash command "date '+[PERF] JSignPdf sign start: %T' >&2" with result code 0
    When sending "post" to ocs "/apps/libresign/api/v1/sign/uuid/<SIGN_UUID_1>"
      | method | clickToSign |
    When sending "post" to ocs "/apps/libresign/api/v1/sign/uuid/<SIGN_UUID_2>"
      | method | clickToSign |
    When sending "post" to ocs "/apps/libresign/api/v1/sign/uuid/<SIGN_UUID_3>"
      | method | clickToSign |
    When sending "post" to ocs "/apps/libresign/api/v1/sign/uuid/<SIGN_UUID_4>"
      | method | clickToSign |
    When sending "post" to ocs "/apps/libresign/api/v1/sign/uuid/<SIGN_UUID_5>"
      | method | clickToSign |
    When sending "post" to ocs "/apps/libresign/api/v1/sign/uuid/<SIGN_UUID_6>"
      | method | clickToSign |
    When sending "post" to ocs "/apps/libresign/api/v1/sign/uuid/<SIGN_UUID_7>"
      | method | clickToSign |
    When sending "post" to ocs "/apps/libresign/api/v1/sign/uuid/<SIGN_UUID_8>"
      | method | clickToSign |
    When sending "post" to ocs "/apps/libresign/api/v1/sign/uuid/<SIGN_UUID_9>"
      | method | clickToSign |
    When sending "post" to ocs "/apps/libresign/api/v1/sign/uuid/<SIGN_UUID_10>"
      | method | clickToSign |
    Then the response should have a status code 200
    And run the bash command "date '+[PERF] JSignPdf sign end: %T' >&2" with result code 0

  Scenario: Sign 10 documents using click-to-sign with PhpNative engine
    Given as user "admin"
    And run the command "config:app:set libresign signature_engine --value=PhpNative --type=string" with result code 0
    And run the bash command "date '+[PERF] PhpNative sign start: %T' >&2" with result code 0
    When sending "post" to ocs "/apps/libresign/api/v1/sign/uuid/<SIGN_UUID_1>"
      | method | clickToSign |
    When sending "post" to ocs "/apps/libresign/api/v1/sign/uuid/<SIGN_UUID_2>"
      | method | clickToSign |
    When sending "post" to ocs "/apps/libresign/api/v1/sign/uuid/<SIGN_UUID_3>"
      | method | clickToSign |
    When sending "post" to ocs "/apps/libresign/api/v1/sign/uuid/<SIGN_UUID_4>"
      | method | clickToSign |
    When sending "post" to ocs "/apps/libresign/api/v1/sign/uuid/<SIGN_UUID_5>"
      | method | clickToSign |
    When sending "post" to ocs "/apps/libresign/api/v1/sign/uuid/<SIGN_UUID_6>"
      | method | clickToSign |
    When sending "post" to ocs "/apps/libresign/api/v1/sign/uuid/<SIGN_UUID_7>"
      | method | clickToSign |
    When sending "post" to ocs "/apps/libresign/api/v1/sign/uuid/<SIGN_UUID_8>"
      | method | clickToSign |
    When sending "post" to ocs "/apps/libresign/api/v1/sign/uuid/<SIGN_UUID_9>"
      | method | clickToSign |
    When sending "post" to ocs "/apps/libresign/api/v1/sign/uuid/<SIGN_UUID_10>"
      | method | clickToSign |
    Then the response should have a status code 200
    And run the bash command "date '+[PERF] PhpNative sign end: %T' >&2" with result code 0

@vitormattos vitormattos added this to the Next Major (29) milestone Mar 26, 2024
@vitormattos vitormattos self-assigned this Mar 26, 2024
@vitormattos vitormattos force-pushed the feature/sign-usign-only-php branch 2 times, most recently from 4b1b252 to 424f9f4 Compare March 26, 2024 13:29
@vitormattos vitormattos changed the title Start to implement jeidison/pdf-signe [WIP] Start to implement jeidison/pdf-signe Mar 26, 2024
@vitormattos vitormattos changed the title [WIP] Start to implement jeidison/pdf-signe [WIP] Sign usign only PHP Mar 26, 2024
@vitormattos vitormattos force-pushed the feature/sign-usign-only-php branch from 424f9f4 to 581e196 Compare April 11, 2024 04:45
@vitormattos vitormattos force-pushed the feature/sign-usign-only-php branch 3 times, most recently from d7414c1 to c78d966 Compare April 23, 2024 23:44
@vitormattos vitormattos force-pushed the feature/sign-usign-only-php branch from c78d966 to e71f53a Compare May 17, 2024 17:30
@vitormattos vitormattos force-pushed the main branch 9 times, most recently from 7c7ad4e to cae8ce7 Compare June 25, 2024 02:20
@vitormattos vitormattos force-pushed the feature/sign-usign-only-php branch 2 times, most recently from df6ade7 to 24f7ca9 Compare March 4, 2026 17:20
vitormattos and others added 5 commits March 4, 2026 14:25
Signed-off-by: Vitor Mattos <vitor@php.rio>
Signed-off-by: Vitor Mattos <vitor@php.rio>
Signed-off-by: Vitor Mattos <vitor@php.rio>
Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com>
Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com>
…ement

needCreateSignature computed was duplicating the validator logic and also
required a placed visual element box, so it returned false for clickToSign
documents in GRAPHIC_ONLY mode.

Added signerHasSignRequest computed (true when the current user has a
signRequestId) and pass it to requirementValidator.getFirstUnmetRequirement()
in both confirmSignDocument and executeSigningAction so the validator can
prompt for the signature drawing even when no element box was placed.

Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com>
Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com>
…image

When canCreateSignature is true but the signer performs a clickToSign
(no drawn image submitted), the DB file element was silently skipped,
producing an empty visibleElements list and no stamp on the document.
Include the element with an empty tempFile so the admin background
image (n0 layer) is still rendered in the signature stamp.

Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com>
…ment

Add case: canCreateSignature=true, signer submits no element but an
admin-placed file element exists. The visibleElements list must still
contain that element (with empty tempFile) so the stamp is rendered.

Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com>
…y image path

When background is present but the signer submitted no image
(clickToSign), passing an empty string to mergeBackgroundWithSignature
caused new Imagick('') to throw. Guard the call: only merge when both
paths are non-empty; fall back to background-only or signature-only
as appropriate. Also skip --img-path entirely when signatureImagePath
is empty in GRAPHIC_AND_DESCRIPTION mode.

Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com>
…background

Add providerSignAffectedParams case: GRAPHIC_AND_DESCRIPTION render
mode with a background image but no user signature (empty imagePath).
Expects --bg-path with background only and no --img-path, verifying
the Imagick crash guard introduced in JSignPdfHandler.

Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com>
…ME alignment

Three fixes:
- useDefaultAppearance: false for invisible signatures (no visibleElements)
  to prevent the pdf-signer-php default stamp from appearing.
- GRAPHIC_ONLY: buildXObject now returns an empty n2 stream immediately,
  preventing description text from leaking into graphic-only stamps.
- GRAPHIC_ONLY: buildAppearanceForElement now assigns the user's drawn
  image to the full bbox (signatureImageFrame=null), fixing blank stamps.
- SIGNAME_AND_DESCRIPTION: signer name is now horizontally centred within
  the left half of the stamp instead of being pinned to leftPadding.

Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com>
…centering

Add three regression tests:
- testBuildXObjectGraphicOnlyReturnsEmptyStream: n2 stream must be empty
  for GRAPHIC_ONLY so no text leaks into graphic-only stamps.
- testBuildAppearanceForElementSetsSignatureImageInGraphicOnlyMode: user
  image must be assigned to the full bbox (signatureImageFrame=null).
- testBuildXObjectSignameAndDescriptionCentersNameInLeftHalf: signer name
  X position must be centred (39.60) not left-aligned (2.00).

Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com>
…acing

JSignPdf renders description text with baseline-to-baseline equal to the
font size (leading factor 1.0).  PhpNative was using 1.2, producing 20%
extra space between lines and a visually inconsistent stamp.  Set the
factor to 1.0 for both the description block and the signer-name block.

Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com>
…ine height

nameStartY = (80 + 20) / 2 - 20 = 30.0  (was 32.0 with old 1.2 factor)
Update expected Td coordinate from 32.00 to 30.00 and fix the doc comment.

Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com>
@vitormattos vitormattos marked this pull request as ready for review March 4, 2026 21:27
@vitormattos
Copy link
Member Author

/backport to stable33

…s (clickToSign fix)

Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com>
…oid blocking clickToSign

The commit e9ea794 changed needCreateSignature to return true for any
signer with a signRequestId, without checking whether visual elements
were placed. This caused the 'Sign the document.' button to be hidden
in clickToSign scenarios, because the 'Define your signature' prompt
was shown instead.

The signerHasSignRequest flag is already passed to the validator in
confirmSignDocument and executeSigningAction, so the drawing prompt
is triggered at action time when appropriate (e.g. GRAPHIC_ONLY mode).
The upfront needCreateSignature computed should only block the sign
button when there are placed visible elements for this signer.

Fixes E2E tests: multi-signer-sequential, sign-email-token-unauthenticated,
sign-herself-with-click-to-sign

Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com>
@vitormattos vitormattos force-pushed the feature/sign-usign-only-php branch from 672e471 to 04d8711 Compare March 4, 2026 22:00
…t that bypassed visibleElements check

The shortcut added in e9ea794 caused needsCreateSignature to return true
whenever the signer had a signRequestId, regardless of whether any visual
element box was placed. This silently opened the draw modal for clickToSign
documents with no placed elements.

Remove the shortcut and keep only the visibleElements.some() check,
also normalising signRequestId comparison to String() to be type-safe.

Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com>
…o validator

signerHasSignRequest was added to bypass the visibleElements check in
SigningRequirementValidator, causing the draw modal to open for clickToSign
documents with no placed elements. With the validator fix in place, this
computed and its two call-sites are no longer needed.

Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com>
…se when no visibleElements placed

Corrects the test added in bc17180 that expected true when signerHasSignRequest
was passed; that was testing the (now-removed) broken shortcut. The correct
expectation is false: no placed element means the draw modal must not appear.

Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com>
Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com>
@vitormattos vitormattos merged commit ceb3666 into main Mar 4, 2026
62 checks passed
@vitormattos vitormattos deleted the feature/sign-usign-only-php branch March 4, 2026 23:00
@github-project-automation github-project-automation bot moved this from 0. Needs triage to 4. to release in Roadmap Mar 4, 2026
@vitormattos vitormattos changed the title [WIP] Sign usign only PHP feat: Sign usign only PHP Mar 4, 2026
@vitormattos
Copy link
Member Author

/backport to stable32

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: 4. to release

Development

Successfully merging this pull request may close these issues.

2 participants