From bd4b5f6e7c9bd72c12b73cb541200d12f6927b73 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Mon, 1 Jun 2026 01:49:17 -0300 Subject: [PATCH 01/31] docs: add tested user docs site and github pages deploy Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .github/ISSUE_TEMPLATE/bug_report.yml | 13 ++ .github/ISSUE_TEMPLATE/commercial_support.yml | 36 ++++ .github/ISSUE_TEMPLATE/config.yml | 6 + .github/ISSUE_TEMPLATE/documentation.yml | 23 +++ .github/ISSUE_TEMPLATE/feature_request.yml | 7 + .github/ISSUE_TEMPLATE/rendering_bug.yml | 72 ++++++++ .github/PULL_REQUEST_TEMPLATE.md | 13 ++ .github/pull_request_template.md | 34 ++++ .github/workflows/docs.yml | 81 +++++++++ README.md | 155 ++++-------------- composer.json | 9 + docs/community/contributing.md | 51 ++++++ docs/community/maintainer-notes.md | 27 +++ docs/community/roadmap.md | 17 ++ docs/community/sustainability.md | 32 ++++ docs/getting-started.md | 43 +++++ docs/guides/basic-template.md | 24 +++ docs/guides/images.md | 37 +++++ docs/guides/interpolation.md | 21 +++ docs/guides/placement.md | 23 +++ docs/guides/preview-pdf.md | 22 +++ docs/guides/signer-integration.md | 25 +++ docs/guides/svg.md | 22 +++ docs/index.md | 39 +++++ docs/installation.md | 31 ++++ docs/reference/errors.md | 38 +++++ docs/reference/limitations.md | 23 +++ docs/reference/output-contract.md | 33 ++++ docs/reference/public-api.md | 52 ++++++ docs/reference/supported-html-css.md | 43 +++++ docs/reference/supported-svg.md | 91 ++++++++++ docs/use-cases/libresign.md | 23 +++ examples/assets/sample.svg | 5 + examples/assets/tiny-jpeg.base64 | 1 + examples/assets/tiny-png.base64 | 1 + examples/basic-template.php | 44 +++++ examples/images.php | 66 ++++++++ examples/interpolation.php | 45 +++++ examples/placement.php | 44 +++++ examples/preview-pdf.php | 38 +++++ examples/svg.php | 51 ++++++ mkdocs.yml | 44 +++++ phpunit.xml.dist | 3 + requirements-docs.txt | 5 + scripts/check-docs-drift.php | 113 +++++++++++++ tests/Documentation/ExamplesTest.php | 101 ++++++++++++ 46 files changed, 1607 insertions(+), 120 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/commercial_support.yml create mode 100644 .github/ISSUE_TEMPLATE/documentation.yml create mode 100644 .github/ISSUE_TEMPLATE/rendering_bug.yml create mode 100644 .github/pull_request_template.md create mode 100644 .github/workflows/docs.yml create mode 100644 docs/community/contributing.md create mode 100644 docs/community/maintainer-notes.md create mode 100644 docs/community/roadmap.md create mode 100644 docs/community/sustainability.md create mode 100644 docs/getting-started.md create mode 100644 docs/guides/basic-template.md create mode 100644 docs/guides/images.md create mode 100644 docs/guides/interpolation.md create mode 100644 docs/guides/placement.md create mode 100644 docs/guides/preview-pdf.md create mode 100644 docs/guides/signer-integration.md create mode 100644 docs/guides/svg.md create mode 100644 docs/index.md create mode 100644 docs/installation.md create mode 100644 docs/reference/errors.md create mode 100644 docs/reference/limitations.md create mode 100644 docs/reference/output-contract.md create mode 100644 docs/reference/public-api.md create mode 100644 docs/reference/supported-html-css.md create mode 100644 docs/reference/supported-svg.md create mode 100644 docs/use-cases/libresign.md create mode 100644 examples/assets/sample.svg create mode 100644 examples/assets/tiny-jpeg.base64 create mode 100644 examples/assets/tiny-png.base64 create mode 100644 examples/basic-template.php create mode 100644 examples/images.php create mode 100644 examples/interpolation.php create mode 100644 examples/placement.php create mode 100644 examples/preview-pdf.php create mode 100644 examples/svg.php create mode 100644 mkdocs.yml create mode 100644 requirements-docs.txt create mode 100644 scripts/check-docs-drift.php create mode 100644 tests/Documentation/ExamplesTest.php diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 33ec828..e5fdf6c 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -9,12 +9,14 @@ body: id: summary attributes: label: Summary + description: Provide a short, objective problem statement. validations: required: true - type: textarea id: steps attributes: label: Steps to reproduce + description: Include exact input and commands. validations: required: true - type: textarea @@ -23,3 +25,14 @@ body: label: Expected behavior validations: required: true + - type: textarea + id: actual + attributes: + label: Actual behavior + validations: + required: true + - type: input + id: runtime + attributes: + label: Runtime details + placeholder: PHP version, package version/commit, OS diff --git a/.github/ISSUE_TEMPLATE/commercial_support.yml b/.github/ISSUE_TEMPLATE/commercial_support.yml new file mode 100644 index 0000000..32ea6b9 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/commercial_support.yml @@ -0,0 +1,36 @@ +# SPDX-FileCopyrightText: 2026 LibreSign +# SPDX-License-Identifier: AGPL-3.0-or-later + +name: Commercial support +description: Ask about paid support or sponsored prioritization +labels: [support] +body: + - type: markdown + attributes: + value: | + Thanks for your interest in commercial support. + + This template does not guarantee free consulting. Sponsored or paid work can help prioritize compatibility, features, and long-term maintenance. + + Current public funding channel: https://github.com/sponsors/LibreSign + - type: textarea + id: context + attributes: + label: Project context + description: Describe your use case, expected timeline, and technical scope. + validations: + required: true + - type: textarea + id: requested_scope + attributes: + label: Requested support scope + description: Include expected deliverables (for example SVG compatibility work, integration support, QA hardening). + validations: + required: true + - type: input + id: contact + attributes: + label: Preferred contact method + placeholder: email or issue-based follow-up + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 2e55af7..edce05d 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -6,3 +6,9 @@ contact_links: - name: Security disclosure url: https://github.com/LibreSign/xobject-template/security about: Please report vulnerabilities privately. + - name: Documentation site + url: https://github.com/LibreSign/xobject-template/tree/main/docs + about: Browse the user documentation source. + - name: Sponsor maintenance + url: https://github.com/sponsors/LibreSign + about: Funding helps prioritize fixes and compatibility work. diff --git a/.github/ISSUE_TEMPLATE/documentation.yml b/.github/ISSUE_TEMPLATE/documentation.yml new file mode 100644 index 0000000..656ae97 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/documentation.yml @@ -0,0 +1,23 @@ +# SPDX-FileCopyrightText: 2026 LibreSign +# SPDX-License-Identifier: AGPL-3.0-or-later + +name: Documentation +description: Report documentation errors or suggest improvements +labels: [documentation] +body: + - type: textarea + id: problem + attributes: + label: What is unclear or incorrect? + validations: + required: true + - type: input + id: page + attributes: + label: Affected page/path + placeholder: docs/reference/supported-svg.md + - type: textarea + id: suggestion + attributes: + label: Suggested improvement + description: Keep suggestions objective and user-focused. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml index 0350a47..d87bb0e 100644 --- a/.github/ISSUE_TEMPLATE/feature_request.yml +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -9,11 +9,18 @@ body: id: problem attributes: label: Problem statement + description: Describe the user or integration problem first. validations: required: true - type: textarea id: proposal attributes: label: Proposed solution + description: Keep scope explicit and include expected API/behavior impact. validations: required: true + - type: textarea + id: non_goals + attributes: + label: Non-goals / out of scope + description: Clarify what should not be included. diff --git a/.github/ISSUE_TEMPLATE/rendering_bug.yml b/.github/ISSUE_TEMPLATE/rendering_bug.yml new file mode 100644 index 0000000..29a60a0 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/rendering_bug.yml @@ -0,0 +1,72 @@ +# SPDX-FileCopyrightText: 2026 LibreSign +# SPDX-License-Identifier: AGPL-3.0-or-later + +name: Rendering bug +description: Report a minimal reproducible rendering issue for HTML/CSS/SVG to XObject output +labels: [bug, rendering] +body: + - type: textarea + id: summary + attributes: + label: Summary + description: Describe the rendering issue in one or two sentences. + validations: + required: true + - type: textarea + id: minimal_input + attributes: + label: Minimal HTML/CSS/SVG input + description: Paste the smallest possible input that reproduces the issue. + render: markdown + validations: + required: true + - type: textarea + id: expected_output + attributes: + label: Expected output + validations: + required: true + - type: textarea + id: actual_output + attributes: + label: Actual output + validations: + required: true + - type: textarea + id: payload_details + attributes: + label: Generated content stream/resources (if possible) + description: Include compile result content stream/resources snippet when available. + - type: dropdown + id: affected_area + attributes: + label: Affected area + options: + - text + - PNG/JPEG + - SVG + - layout + - CSS + - interpolation + - placement + validations: + required: true + - type: input + id: php_version + attributes: + label: PHP version + placeholder: '8.2.x' + validations: + required: true + - type: input + id: package_version + attributes: + label: Package version or commit + placeholder: 'composer version, tag, or commit hash' + validations: + required: true + - type: textarea + id: fixture + attributes: + label: Minimal fixture attachment details + description: If possible, attach or describe a minimal fixture file set. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 0dfe019..b282c69 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -10,11 +10,24 @@ - [ ] Tests added/updated - [ ] Relevant checks executed +## Documentation + +- [ ] Documentation updated +- [ ] Examples updated if public behavior changed +- [ ] Supported HTML/CSS/SVG matrix updated if relevant +- [ ] Documentation drift checks pass + ## Performance - [ ] Performance impact assessed - [ ] Benchmark threshold still green +## Output integrity + +- [ ] No unsupported feature documented as supported +- [ ] No generated build artifacts committed to the source branch +- [ ] LibreSign use case impact considered if relevant + ## Compliance - [ ] DCO sign-off in all commits diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..4445706 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,34 @@ + + + +## Scope + +- [ ] Change scope and impact are clearly described + +## Evidence + +- [ ] Tests added/updated +- [ ] Relevant checks executed + +## Documentation + +- [ ] Documentation updated +- [ ] Examples updated if public behavior changed +- [ ] Supported HTML/CSS/SVG matrix updated if relevant +- [ ] Documentation drift checks pass + +## Performance + +- [ ] Performance impact considered + +## Output integrity + +- [ ] No unsupported feature documented as supported +- [ ] No generated build artifacts committed to the source branch +- [ ] LibreSign use case impact considered if relevant + +## Compliance + +- [ ] DCO sign-off in all commits +- [ ] REUSE/SPDX metadata preserved +- [ ] License compatibility reviewed diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..81ad86a --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,81 @@ +# SPDX-FileCopyrightText: 2026 LibreSign +# SPDX-License-Identifier: AGPL-3.0-or-later + +name: docs + +on: + pull_request: + push: + branches: [main] + +permissions: + contents: read + +jobs: + validate-docs: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Detect minimum PHP from composer.json + id: php_min + run: | + php_version=$(grep -Po '"php"\s*:\s*"\K[^"]+' composer.json | grep -Eo '[0-9]+\.[0-9]+' | head -n1) + [[ -n "$php_version" ]] || { echo "Could not determine minimum PHP version"; exit 1; } + echo "version=$php_version" >> "$GITHUB_OUTPUT" + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ steps.php_min.outputs.version }} + + - name: Setup Python + uses: actions/setup-python@v6 + with: + python-version: '3.12' + + - name: Install Composer dependencies + run: composer install --no-interaction --prefer-dist + + - name: Install PHPUnit bin dependencies + run: composer bin phpunit install --no-interaction --prefer-dist + + - name: Install docs dependencies + run: python -m pip install --upgrade pip && pip install -r requirements-docs.txt + + # Pull requests only validate docs. No deployment from PRs. + - name: Run docs tests + run: composer run docs:test + + deploy-docs: + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + needs: validate-docs + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Setup Python + uses: actions/setup-python@v6 + with: + python-version: '3.12' + + - name: Install docs dependencies + run: python -m pip install --upgrade pip && pip install -r requirements-docs.txt + + - name: Build docs site + run: python -m mkdocs build --strict + + # Pushes to main publish docs to gh-pages branch. + # Maintainers should configure GitHub Pages source as: + # Deploy from branch -> gh-pages -> /root + - name: Deploy site to gh-pages branch + uses: peaceiris/actions-gh-pages@v4 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./site + publish_branch: gh-pages + force_orphan: true diff --git a/README.md b/README.md index 190be8a..d2ec7b9 100644 --- a/README.md +++ b/README.md @@ -3,150 +3,65 @@ # xobject-template -> Compile a minimal HTML+CSS subset into reusable PDF Form XObject payloads. +Minimal HTML+CSS to reusable PDF Form XObject compiler for visible signature appearances and document overlays. -`xobject-template` is a focused rendering engine for projects that need **beautiful, vector-first, stable** reusable overlays inside PDF workflows. +## What is xobject-template? -## Why this package +`xobject-template` is a focused PHP rendering engine that compiles a deterministic HTML/CSS subset into Form XObject-oriented output (`contentStream`, `resources`, `bbox`) for downstream PDF workflows. -- Reusable XObject payloads for labels, stamps, approvals, and other document overlays -- Clean integration path for any PDF pipeline that can embed Form XObjects -- Lean API designed for long-term compatibility and monetizable maintainability +## Why it exists -## Quick integration +Many systems need stable visual signature/stamp/label appearances, but do not want browser-grade rendering complexity inside PDF pipelines. -Use the compiler to generate a reusable XObject result. Consumers that prefer arrays over DTOs can adapt the result into a generic payload shape. +This package provides a scoped, integration-friendly rendering layer for that gap. -```php -use LibreSign\XObjectTemplate\Dto\CompileRequest; -use LibreSign\XObjectTemplate\Integration\XObjectPayloadAdapter; -use LibreSign\XObjectTemplate\Pdf\SinglePagePdfExporter; -use LibreSign\XObjectTemplate\XObjectTemplateCompiler; +## Install -$compiler = new XObjectTemplateCompiler(); - -$result = $compiler->compile(new CompileRequest( - html: '
Rendered for Alice
' - . '', - width: 240.0, - height: 84.0, -)); - -$payload = (new XObjectPayloadAdapter())->toXObjectPayload($result); -$pdf = (new SinglePagePdfExporter())->export($result); - -file_put_contents(__DIR__ . '/build/preview.pdf', $pdf); +```bash +composer require libresign/xobject-template ``` -### Standalone PDF export - -`SinglePagePdfExporter` wraps a compiled XObject result into a one-page PDF whose `MediaBox` matches the compiled `bbox` size exactly. - -- The page size is derived from `$result->bbox` -- Non-zero bounding boxes are translated back to the page origin automatically -- Local PNG and JPEG image sources are embedded into the standalone PDF during export - -### Output contract - -- `$result->contentStream`: PDF operators ready for a Form XObject stream -- `$result->resources`: font/image resource dictionary keyed for PDF serialization -- `$result->bbox`: bounding box as `[x1, y1, x2, y2]` -- `$result->metadata`: render diagnostics such as `line_count`, `image_count`, `node_count`, and `render_ms` -- `$payload`: transport-agnostic array with `stream`, `resources`, and `bbox` -- `$pdf`: standalone PDF bytes ready to save, stream, or attach to preview workflows - -### Scaling a compiled XObject - -`CompileRequest::width` and `CompileRequest::height` define the base design size of the template. -If a downstream consumer needs to place the compiled stamp at a different size while preserving the -original aspect ratio, it should keep the compiled XObject unchanged and apply a uniform scale during -PDF placement instead of recompiling the HTML with new dimensions. - -- Read the base size from `$result->bbox` -- Compute a single scale factor from the target width or target height -- Apply the same scale to both axes in the placement matrix +## Quick example ```php -[$minX, $minY, $maxX, $maxY] = $result->bbox; - -$baseWidth = $maxX - $minX; -$baseHeight = $maxY - $minY; - -$targetWidth = 175.0; -$scale = $targetWidth / $baseWidth; -$targetHeight = $baseHeight * $scale; - -// Consumer-side PDF placement concept: -$placement = sprintf( - 'q %F 0 0 %F %F %F cm /Fm0 Do Q', - $scale, - $scale, - $x, - $y, -); -``` - -Using a uniform placement scale keeps text, images, spacing, and line breaks visually aligned. -Recompiling only to emulate a proportional resize is usually the wrong integration point for this -package. +fromWidth($result, 175.0, 36.0, 72.0); +$result = $compiler->compile(new CompileRequest( + html: '
Signed by {{ name }}
', + width: 240.0, + height: 84.0, + context: ['name' => 'Alice'], +)); -$pdfCommand = $placement->toPdfCommand('Fm0'); -// q 0.729167 0 0 0.729167 36.000000 72.000000 cm /Fm0 Do Q +// $result->contentStream +// $result->resources +// $result->bbox ``` -### Optional context interpolation - -If the caller passes `CompileRequest::context`, the compiler can interpolate simple `{{ key }}` -placeholders before parsing the HTML subset. +## Documentation -- Values are HTML-escaped before insertion -- Unknown placeholders are left untouched -- Twig users can keep rendering HTML upstream and skip this feature entirely +Full documentation site source is in `docs/` and published via GitHub Pages after merge: -## Supported HTML/CSS subset +- Installation, guides, examples, and support matrices: `docs/` -### HTML +## LibreSign use case -- Supported elements: `
`, `

`, ``, `
`, `` -- Text fragments are normalized into inline text nodes internally -- Inline styles are read from the `style` attribute -- Images use the `src` attribute as the source reference included in the resource dictionary +- Docs page: `docs/use-cases/libresign.md` +- LibreSign project: -### CSS used by the renderer +## Contributing -- Typography: `font-size`, `font-family`, `font-weight`, `line-height`, `color`, `text-align`, `hyphens`, `white-space` -- Layout: `margin`, `padding`, `width`, `height`, `overflow`, `text-overflow` -- Vector box styling: `background-color`, `border-color`, `border-width`, `border-radius` -- Structured layout: `display:flex`, `flex-direction`, `justify-content`, `align-items`, `gap` -- Absolute placement: `position:absolute`, `top`, `right`, `bottom`, `left` -- Numeric values can be provided as unitless numbers or `px`; `width`, `height`, and positional offsets also accept `%` -- `px` values are converted to PDF points using the package conversion rules -- Unknown or incomplete CSS declarations are ignored instead of aborting the render +- Main guide: `docs/community/contributing.md` +- Repository policy: `CONTRIBUTING.md` -### Rendering notes +If this package helps your project generate reliable PDF signature appearances, please star the repository. It helps other developers discover the project and signals that this work is worth maintaining. -- Font family mapping currently targets the built-in Helvetica, Times, and Courier aliases used by the generated PDF resources -- Text alignment uses measured widths for left, center, right, and basic justified output (`Tw` word spacing) -- Hyphenation supports a small deterministic subset: `hyphens:auto`, `hyphens:manual` with soft hyphens, and `hyphens:none` -- Overflow clipping uses PDF clipping paths; `text-overflow:ellipsis` applies when hidden overflow truncates visible text -- Backgrounds and borders are emitted as vector rectangles, including rounded corners -- Percentage-based sizing and offsets resolve relative to the current layout container -- Flex layouts are intentionally small-scope and predictable: the engine supports deterministic row/column compositions for stamps, labels, and approval blocks rather than full browser-grade CSS -- `img` width/height fall back to `32x32` when omitted or invalid -- Image and text placement are clamped to the requested output box -- The compiler output is not tied to any single downstream package; any consumer that understands Form XObject stream/resources/bbox data can use it +## Support the project -## Failure modes +- Sponsor maintenance: -- Unsupported HTML elements raise `UnsupportedSubsetException` -- Malformed HTML fragments are normalized by `DOMDocument` before traversal -- Empty text nodes and invalid inline-style fragments are ignored during parsing/rendering diff --git a/composer.json b/composer.json index e619327..df68c23 100644 --- a/composer.json +++ b/composer.json @@ -73,6 +73,15 @@ "sh:security": "shellcheck scripts/*.sh", "test:unit": "vendor-bin/phpunit/vendor/phpunit/phpunit/phpunit --testsuite unit", "test:integration": "vendor-bin/phpunit/vendor/phpunit/phpunit/phpunit --testsuite integration", + "docs:examples": "vendor-bin/phpunit/vendor/phpunit/phpunit/phpunit --testsuite documentation --filter ExamplesTest", + "docs:check-drift": "php scripts/check-docs-drift.php", + "docs:build": "python3 -m mkdocs build --strict", + "docs:serve": "python3 -m mkdocs serve", + "docs:test": [ + "@docs:examples", + "@docs:check-drift", + "@docs:build" + ], "test:coverage": "XDEBUG_MODE=coverage vendor-bin/phpunit/vendor/phpunit/phpunit/phpunit --coverage-text --coverage-clover=build/coverage/clover.xml", "mutation:test": "vendor-bin/mutation/vendor/infection/infection/bin/infection --threads=max", "benchmark:run": "vendor-bin/phpbench/vendor/phpbench/phpbench/bin/phpbench run --config=phpbench.json --report=aggregate", diff --git a/docs/community/contributing.md b/docs/community/contributing.md new file mode 100644 index 0000000..8177495 --- /dev/null +++ b/docs/community/contributing.md @@ -0,0 +1,51 @@ + + + +# Contributing + +Contributions are welcome. Keep quality high and changes scoped. + +## Setup + +Install dependencies and run checks from repository root. + +## Core quality commands + +- `composer lint` +- `composer test:unit` +- `composer test:integration` +- `composer docs:test` + +## Documentation and examples + +- Add/update runnable examples in `examples/`. +- Keep examples small and deterministic. +- Ensure docs snippets come from real examples. +- Run docs drift checks before opening a PR. + +## Adding supported behavior + +When adding HTML/CSS/SVG support: + +1. Add/adjust implementation. +2. Add or update fixture-based tests. +3. Update support reference pages. +4. Keep limitations explicit (do not overstate support). + +## Reporting rendering bugs + +A good minimal reproducible report includes: + +- minimal HTML/CSS/SVG input, +- expected vs actual result, +- generated output details when possible, +- package version/commit and PHP version. + +## High-value contributions + +- SVG edge-case fixtures, +- PDF compatibility fixtures, +- layout bug fixes, +- documentation improvements, +- tests for real-world signature templates, +- performance improvements backed by benchmark evidence. diff --git a/docs/community/maintainer-notes.md b/docs/community/maintainer-notes.md new file mode 100644 index 0000000..a03fb19 --- /dev/null +++ b/docs/community/maintainer-notes.md @@ -0,0 +1,27 @@ + + + +# Maintainer notes + +## Documentation deployment + +- Pull requests validate docs only. +- Pushes to `main` validate and publish the generated site to `gh-pages`. +- GitHub Pages should be configured as: deploy from branch `gh-pages`, folder `/root`. + +## Validation flow + +- Execute `composer docs:test`. +- Ensure examples in `examples/` remain runnable. +- Keep support references aligned with parser/layout/SVG behavior and tests. + +## Scope control + +- Keep user docs focused on public contracts. +- Avoid scope creep into browser-engine semantics or full PDF-signing behavior. +- Keep LibreSign use case visible without making this package LibreSign-only. + +## Drift policy + +- Support matrix docs are checked by `scripts/check-docs-drift.php` against known code-level indicators. +- TODO: add finer-grained automated extraction for CSS property lists if parser/layout structure changes substantially. diff --git a/docs/community/roadmap.md b/docs/community/roadmap.md new file mode 100644 index 0000000..9b2fbaf --- /dev/null +++ b/docs/community/roadmap.md @@ -0,0 +1,17 @@ + + + +# Roadmap + +> This page describes priorities, not implemented behavior. + +Possible priority areas: + +- SVG compatibility improvements. +- Additional layout fixtures. +- More PDF compatibility tests. +- More documentation examples. +- Performance benchmark expansions. +- Additional downstream signer integration examples. + +Roadmap priorities may be influenced by real-world LibreSign needs, community contributions, and funded maintenance work. diff --git a/docs/community/sustainability.md b/docs/community/sustainability.md new file mode 100644 index 0000000..8ff697b --- /dev/null +++ b/docs/community/sustainability.md @@ -0,0 +1,32 @@ + + + +# Sustainability + +Reliable PDF appearance generation needs sustained maintenance. + +This package sits at the intersection of PDF constraints, deterministic layout, images, scoped SVG handling, and downstream signer integration. Edge cases accumulate quickly, and maintaining high-confidence fixtures and tests takes ongoing effort. + +## Why funding matters + +Sponsorship and paid support help sustain: + +- SVG support hardening, +- documentation quality, +- QA and drift checks, +- benchmark and performance maintenance, +- integration examples for downstream signer workflows. + +## Practical ways to support + +- Star the repository. +- Sponsor the project. +- Contribute fixtures/tests. +- Report minimal reproducible issues. +- Evaluate LibreSign if you need a full digital-signature workflow. + +Support channel: + +- GitHub Sponsors: + +If this package helps your project generate reliable PDF signature appearances, consider starring the repository, contributing a fixture, or sponsoring maintenance. diff --git a/docs/getting-started.md b/docs/getting-started.md new file mode 100644 index 0000000..217241c --- /dev/null +++ b/docs/getting-started.md @@ -0,0 +1,43 @@ + + + +# Getting started + +This page uses the real example file `examples/basic-template.php`. + +## Minimal compile flow + +The example creates a `CompileRequest`, calls `XObjectTemplateCompiler::compile()`, and writes a JSON output summary under `build/examples/`. + +```php +compile(new CompileRequest( + html: '

Signed by {{ name }}
', + width: 240.0, + height: 84.0, + context: ['name' => 'Alice'], +)); +``` + +## Result at user level + +The compile result provides: + +- `contentStream`: PDF operators for a Form XObject stream. +- `resources`: font/image resource dictionary data for downstream serialization. +- `bbox`: `[x1, y1, x2, y2]` bounding box. +- `metadata`: rendering diagnostics (`render_ms`, `line_count`, `image_count`, `node_count`). + +## Next steps + +- Basic usage details: [Guides / Basic template](guides/basic-template.md) +- Placement/scaling in downstream PDFs: [Guides / Placement and scaling](guides/placement.md) +- Full output contract: [Reference / Output contract](reference/output-contract.md) + +If this package helps your project generate reliable PDF signature appearances, consider starring the repository, contributing a fixture, or sponsoring maintenance. diff --git a/docs/guides/basic-template.md b/docs/guides/basic-template.md new file mode 100644 index 0000000..72edba6 --- /dev/null +++ b/docs/guides/basic-template.md @@ -0,0 +1,24 @@ + + + +# Basic template + +Source example: `examples/basic-template.php` + +## What the example demonstrates + +- HTML input with inline styles. +- `CompileRequest` with `width`, `height`, and optional `context`. +- Compilation through `XObjectTemplateCompiler`. +- Inspection of `contentStream`, `resources`, `bbox`, and `metadata`. + +## Expected output + +The example writes `build/examples/basic-template-result.json` and includes: + +- non-empty content stream, +- font resources, +- a 4-value bounding box, +- render metadata. + +Use this as your baseline before moving to images, SVG, and placement guides. diff --git a/docs/guides/images.md b/docs/guides/images.md new file mode 100644 index 0000000..f9585a7 --- /dev/null +++ b/docs/guides/images.md @@ -0,0 +1,37 @@ + + + +# Images + +Source example: `examples/images.php` + +## Supported raster formats + +Based on implementation and tests: + +- PNG: supported. +- JPEG: supported. +- Other raster formats (for example GIF): not supported. + +## Source resolution + +Image sources are file paths from ``. + +- During compile, the source path is carried into XObject resources. +- During export/embedding, the file is read from disk. + +## Dimensions and resources + +- `` dimensions come from style width/height when provided. +- Missing/invalid image dimensions default to `32x32` in layout. +- Generated resources include XObject aliases (for example `Im0`, `Im1`) with `Source`, `Width`, and `Height` metadata. + +## Error behavior and limitations + +Typical failures include: + +- source file missing/unreadable, +- unsupported mime type, +- invalid PNG/JPEG payload. + +See [Errors and exceptions](../reference/errors.md) and [Supported SVG](../reference/supported-svg.md) for vector behavior. diff --git a/docs/guides/interpolation.md b/docs/guides/interpolation.md new file mode 100644 index 0000000..1d45f6e --- /dev/null +++ b/docs/guides/interpolation.md @@ -0,0 +1,21 @@ + + + +# Context interpolation + +Source example: `examples/interpolation.php` + +## Placeholder syntax + +The current interpolation feature supports placeholders like: + +- `{{ name }}` +- `{{ role }}` + +## Behavior + +- Interpolation happens before HTML subset parsing. +- Values are HTML-escaped before insertion. +- Unknown placeholders remain unchanged. + +This is useful for signature labels and document workflow overlays where a template shape is fixed but content changes per signer/document. diff --git a/docs/guides/placement.md b/docs/guides/placement.md new file mode 100644 index 0000000..351bef4 --- /dev/null +++ b/docs/guides/placement.md @@ -0,0 +1,23 @@ + + + +# Placement and scaling + +Source example: `examples/placement.php` + +## Downstream placement model + +Compile once at a design size, then place the generated Form XObject in downstream PDFs. + +In most workflows, proportional resizing should happen through placement scaling, not by recompiling HTML for each target size. + +## Public helper classes + +- `LibreSign\XObjectTemplate\Integration\XObjectPlacementCalculator` +- `LibreSign\XObjectTemplate\Integration\XObjectPlacement` + +The example shows `fromWidth()`, `fromHeight()`, and `fromScale()` and emits a placement command string with `toPdfCommand()`. + +## Why this is recommended + +Uniform placement scaling keeps text, spacing, and images visually consistent and avoids introducing avoidable template variants. diff --git a/docs/guides/preview-pdf.md b/docs/guides/preview-pdf.md new file mode 100644 index 0000000..a7fec22 --- /dev/null +++ b/docs/guides/preview-pdf.md @@ -0,0 +1,22 @@ + + + +# Preview PDF + +Source example: `examples/preview-pdf.php` + +## Why preview export is useful + +`xobject-template` compiles Form XObject payloads. For visual debugging, `SinglePagePdfExporter` can wrap a compile result into a one-page PDF using the result `bbox`. + +This makes template iteration easier because you can quickly inspect appearance output without wiring a full downstream signer flow. + +## What the example does + +- Compiles a simple template. +- Exports a preview PDF. +- Writes output to `build/examples/preview.pdf`. + +## Scope reminder + +Preview export is a helper for inspection and integration tests. Production signing/placement still happens in your downstream PDF workflow. diff --git a/docs/guides/signer-integration.md b/docs/guides/signer-integration.md new file mode 100644 index 0000000..171d1fe --- /dev/null +++ b/docs/guides/signer-integration.md @@ -0,0 +1,25 @@ + + + +# PDF signer integration + +`xobject-template` does **not** sign PDFs. + +It generates appearance payloads that downstream signers (or any PDF pipeline that can place Form XObjects) can consume. + +## What downstream consumers typically use + +- `contentStream` +- `resources` +- `bbox` +- optional placement data via integration helpers + +## Typical flow + +1. Compile template to Form XObject payload. +2. Place XObject in target page coordinates. +3. Let downstream signer handle signature cryptography and document-level signing workflow. + +## LibreSign context + +LibreSign is a flagship use case where this package focuses on deterministic appearance generation while LibreSign handles broader digital-signature workflow responsibilities. diff --git a/docs/guides/svg.md b/docs/guides/svg.md new file mode 100644 index 0000000..a3dcdbd --- /dev/null +++ b/docs/guides/svg.md @@ -0,0 +1,22 @@ + + + +# SVG + +Source example: `examples/svg.php` + +## Current SVG usage path + +SVG is handled through image source input (``) and then converted to a PDF Form XObject by the current SVG implementation. + +## Typical use cases + +- deterministic logos, +- deterministic icons, +- deterministic vector shape overlays in signature appearances. + +## Important scope limits + +SVG support is intentionally scoped and deterministic. It is not a full browser-grade SVG engine. + +See [Reference / Supported SVG](../reference/supported-svg.md) for exact supported primitives, path commands, transforms, paint behavior, and known unsupported features. diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..1a90464 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,39 @@ + + + +# xobject-template + +`xobject-template` compiles a focused HTML+CSS subset into reusable PDF Form XObject payloads. + +It is designed for deterministic visual appearance generation in digital-signature and document-workflow systems: signature appearances, stamps, labels, and overlays. + +It solves a practical integration problem: many PDF signers can place Form XObjects but do not want to implement HTML/CSS rendering. + +## What this package does + +- Renders text from a minimal HTML/CSS subset. +- Supports context interpolation for template personalization. +- Supports PNG/JPEG image embedding through file sources. +- Supports scoped SVG rendering through current implementation support. +- Produces reusable output contracts (`contentStream`, `resources`, `bbox`, metadata). + +## What this package is not + +- A full PDF signer. +- A browser engine. +- A generic PDF editor. +- A complete HTML/CSS/SVG renderer. + +## Start quickly + +Go to [Getting started](getting-started.md) for a runnable example based on `examples/basic-template.php`. + +## Key references + +- [Supported HTML and CSS](reference/supported-html-css.md) +- [Supported SVG](reference/supported-svg.md) +- [LibreSign use case](use-cases/libresign.md) +- [Contributing](community/contributing.md) +- [Sustainability](community/sustainability.md) + +If this package helps your project generate reliable PDF signature appearances, consider starring the repository, contributing a fixture, or sponsoring maintenance. diff --git a/docs/installation.md b/docs/installation.md new file mode 100644 index 0000000..bb329b5 --- /dev/null +++ b/docs/installation.md @@ -0,0 +1,31 @@ + + + +# Installation + +## Requirements + +From `composer.json`: + +- PHP: `^8.2` +- Required PHP extensions: no explicit extra extensions declared by this package. + +Runtime notes: + +- The HTML parser uses `DOMDocument`. +- Image metadata detection uses `getimagesizefromstring` for raster images. +- The package reads local image files for image embedding/export workflows. + +## Install with Composer + +```bash +composer require libresign/xobject-template +``` + +## Verify installation + +A quick verification is to run one example from this repository after dependencies are installed: + +- `examples/basic-template.php` + +See [Getting started](getting-started.md) for the exact flow and output expectations. diff --git a/docs/reference/errors.md b/docs/reference/errors.md new file mode 100644 index 0000000..b8e78fe --- /dev/null +++ b/docs/reference/errors.md @@ -0,0 +1,38 @@ + + + +# Errors and exceptions + +## Public exception to catch + +- `LibreSign\XObjectTemplate\Exception\UnsupportedSubsetException` + - Raised when HTML includes unsupported tags in the subset parser. + +## Common invalid argument failures + +`\InvalidArgumentException` can be raised by integration/export/image/SVG paths, for example: + +- invalid `bbox` for export/placement, +- empty placement alias, +- image source missing or unreadable, +- unsupported image format, +- invalid SVG/viewBox/path payload. + +## What users should do + +- Validate input templates against [Supported HTML and CSS](supported-html-css.md). +- Validate SVG assets against [Supported SVG](supported-svg.md). +- Keep reproducible template and asset fixtures for failing cases. + +## How to report a minimal reproducible issue + +Please include: + +- minimal HTML/CSS/SVG input, +- compile request dimensions, +- expected behavior, +- actual behavior, +- package version or commit hash, +- PHP version and runtime context. + +See [Community / Contributing](../community/contributing.md) for reporting guidance. diff --git a/docs/reference/limitations.md b/docs/reference/limitations.md new file mode 100644 index 0000000..1d2ec6a --- /dev/null +++ b/docs/reference/limitations.md @@ -0,0 +1,23 @@ + + + +# Limitations + +`xobject-template` is intentionally scoped. + +## Explicit non-goals + +- Not a browser engine. +- Not a full PDF signer. +- Not a generic PDF editor. +- Not a complete HTML/CSS/SVG renderer. + +## Rendering scope + +- HTML/CSS/SVG support is intentionally limited to deterministic overlay/stamp workflows. +- Behavior is deterministic, but not browser-compatible. +- Unsupported subset inputs should be treated as out of scope, not as promised behavior. + +## Integration expectation + +Consumers should validate templates that matter to their workflow and keep fixture-based regression tests for critical appearance requirements. diff --git a/docs/reference/output-contract.md b/docs/reference/output-contract.md new file mode 100644 index 0000000..67efc7f --- /dev/null +++ b/docs/reference/output-contract.md @@ -0,0 +1,33 @@ + + + +# Output contract + +`CompileResult` is the stable output boundary for integration. + +## Fields + +| Field | Type | Meaning | +|---|---|---| +| `contentStream` | `string` | PDF operators intended for a Form XObject stream body. | +| `resources` | `array` | Resource dictionary data (fonts + referenced image XObjects). | +| `bbox` | `[float,float,float,float]` | Bounding box `[x1,y1,x2,y2]` used for placement/export. | +| `metadata` | `array` | Diagnostics such as `render_ms`, `line_count`, `image_count`, `node_count`. | + +## Resource shape used by helpers + +- Fonts are keyed aliases (for example `F1`..`F6`) with PDF font dictionaries. +- Image resources are keyed aliases (for example `Im0`) with at least source/size metadata. + +## What downstream consumers should rely on + +- `contentStream` + `resources` + `bbox` as the core Form XObject payload contract. +- `bbox` defines base geometry for placement scaling. + +## What is intentionally not guaranteed + +- Browser-equivalent rendering behavior. +- Full HTML/CSS/SVG compatibility. +- Stable internals of parser/layout implementation classes. + +For transport-neutral payload arrays, see `XObjectPayloadAdapter` in [Public API](public-api.md). diff --git a/docs/reference/public-api.md b/docs/reference/public-api.md new file mode 100644 index 0000000..3b9b5c8 --- /dev/null +++ b/docs/reference/public-api.md @@ -0,0 +1,52 @@ + + + +# Public API + +This page documents user-facing API intended for integration. + +## Compiler entry point + +- `LibreSign\XObjectTemplate\XObjectTemplateCompiler` +- `LibreSign\XObjectTemplate\Contract\XObjectTemplateCompilerInterface` + +Primary method: + +- `compile(CompileRequest $request): CompileResult` + +## Request DTO + +- `LibreSign\XObjectTemplate\Dto\CompileRequest` + - `html: string` + - `width: float = 240.0` + - `height: float = 84.0` + - `context: array = []` + +## Result DTO + +- `LibreSign\XObjectTemplate\Dto\CompileResult` + - `contentStream: string` + - `resources: array` + - `bbox: array{0: float, 1: float, 2: float, 3: float}` + - `metadata: array` + +## Integration helpers + +- `LibreSign\XObjectTemplate\Integration\XObjectPayloadAdapter` + - `toXObjectPayload(CompileResult $result): array{stream,resources,bbox}` +- `LibreSign\XObjectTemplate\Integration\XObjectPlacementCalculator` + - `fromWidth(...)`, `fromHeight(...)`, `fromScale(...)` +- `LibreSign\XObjectTemplate\Integration\XObjectPlacement` + - `toPdfCommand(string $alias): string` + +## Preview/export helper + +- `LibreSign\XObjectTemplate\Pdf\SinglePagePdfExporter` + - `export(CompileResult $result): string` + +## Exceptions users should handle + +- `LibreSign\XObjectTemplate\Exception\UnsupportedSubsetException` +- `\InvalidArgumentException` from invalid placement/bounding box/image source/format scenarios. + +For exact output semantics, see [Output contract](output-contract.md). diff --git a/docs/reference/supported-html-css.md b/docs/reference/supported-html-css.md new file mode 100644 index 0000000..16db04f --- /dev/null +++ b/docs/reference/supported-html-css.md @@ -0,0 +1,43 @@ + + + +# Supported HTML and CSS + +This page reflects current implementation and tests. + +## HTML elements + +Supported: + +- `
` +- `

` +- `` +- `
` +- `` + +Not supported (examples): tables, lists, semantic formatting tags like ``. + +## CSS properties used by renderer + +Common supported properties include: + +- Typography: `font-size`, `font-family`, `font-weight`, `line-height`, `color`, `text-align`, `hyphens`, `white-space` +- Layout/box: `margin`, `padding`, `width`, `height`, `overflow`, `text-overflow` +- Decoration: `background-color`, `border-color`, `border-width`, `border-radius` +- Flex subset: `display:flex`, `flex-direction`, `justify-content`, `align-items`, `gap` +- Absolute positioning: `position:absolute`, `top`, `right`, `bottom`, `left` + +## Units and values + +- Unitless numeric values are accepted for many properties. +- `px` is accepted and converted to PDF points. +- `%` is supported for dimensions/offsets in current layout implementation. +- Unknown or invalid declarations are ignored, not fatal. + +## Practical notes + +- Font alias mapping targets built-in Helvetica/Times/Courier variants. +- Overflow clipping uses PDF clipping paths (`overflow:hidden`). +- The behavior is deterministic and intentionally scoped for overlays/stamps, not browser parity. + +See [Limitations](limitations.md) before designing production templates. diff --git a/docs/reference/supported-svg.md b/docs/reference/supported-svg.md new file mode 100644 index 0000000..95e339c --- /dev/null +++ b/docs/reference/supported-svg.md @@ -0,0 +1,91 @@ + + + +# Supported SVG + +SVG support is intentionally scoped and deterministic. + +## Supported usage pattern + +- SVG is consumed through image source input (for example ``). +- `.svg` and `.svgz` extension detection is supported. +- Content-based SVG detection is also supported (` `f` +- stroke only -> `S` +- fill + stroke -> `B` + +`stroke-width` is supported (style attribute takes precedence over presentation attribute). + +## ViewBox and dimensions + +- `viewBox` is supported and takes precedence over `width`/`height`. +- If no valid `viewBox` is present, positive `width` and `height` are required. +- Non-positive viewport dimensions are rejected. + +## Unsupported or intentionally out-of-scope + +- Full SVG text layout support. +- Filters, masks, gradients, patterns, and full SVG compositing model. +- Browser-equivalent SVG rendering semantics. + +## Error behavior + +Common error cases include: + +- invalid SVG root/empty payload, +- malformed path data, +- unsupported path command, +- invalid or non-positive viewport definitions. + +If this package helps your project generate reliable PDF signature appearances, consider starring the repository, contributing a fixture, or sponsoring maintenance. diff --git a/docs/use-cases/libresign.md b/docs/use-cases/libresign.md new file mode 100644 index 0000000..bba7d58 --- /dev/null +++ b/docs/use-cases/libresign.md @@ -0,0 +1,23 @@ + + + +# LibreSign use case + +LibreSign is the flagship real-world use case for this package. + +`xobject-template` helps generate deterministic visual signature appearances, stamps, and overlays. LibreSign uses this kind of appearance generation inside a broader digital-signature workflow. + +## Scope split + +- `xobject-template`: appearance generation (Form XObject-oriented rendering). +- LibreSign: end-to-end digital-signature workflow and platform integration. + +## Official links + +- LibreSign website: +- LibreSign GitHub repository: +- LibreSign documentation: + +If you need a complete digital-signature solution, evaluate LibreSign. + +If this package helps your project generate reliable PDF signature appearances, consider starring the repository, contributing a fixture, or sponsoring maintenance. diff --git a/examples/assets/sample.svg b/examples/assets/sample.svg new file mode 100644 index 0000000..523d596 --- /dev/null +++ b/examples/assets/sample.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/examples/assets/tiny-jpeg.base64 b/examples/assets/tiny-jpeg.base64 new file mode 100644 index 0000000..12a3eeb --- /dev/null +++ b/examples/assets/tiny-jpeg.base64 @@ -0,0 +1 @@ +/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkSEw8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJCQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjL/wAARCAABAAEDASIAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwDi6KKK+ZP3E//Z diff --git a/examples/assets/tiny-png.base64 b/examples/assets/tiny-png.base64 new file mode 100644 index 0000000..eb83931 --- /dev/null +++ b/examples/assets/tiny-png.base64 @@ -0,0 +1 @@ +iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO5X2s8AAAAASUVORK5CYII= diff --git a/examples/basic-template.php b/examples/basic-template.php new file mode 100644 index 0000000..057f581 --- /dev/null +++ b/examples/basic-template.php @@ -0,0 +1,44 @@ +compile(new CompileRequest( + html: '

Signed by {{ name }}
' + . '
Role: {{ role }}
', + width: 240.0, + height: 84.0, + context: [ + 'name' => 'Alice', + 'role' => 'Approver', + ], +)); + +$outputFile = $outputDir . '/basic-template-result.json'; +file_put_contents($outputFile, json_encode([ + 'content_stream_length' => strlen($result->contentStream), + 'resources' => $result->resources, + 'bbox' => $result->bbox, + 'metadata' => $result->metadata, +], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); + +return [ + 'example' => 'basic-template', + 'generated_files' => [$outputFile], + 'content_stream' => $result->contentStream, + 'resources' => $result->resources, + 'bbox' => $result->bbox, +]; diff --git a/examples/images.php b/examples/images.php new file mode 100644 index 0000000..3933beb --- /dev/null +++ b/examples/images.php @@ -0,0 +1,66 @@ +' + . '' + . '' + . 'PNG + JPEG' + . '
'; + +$result = (new XObjectTemplateCompiler())->compile(new CompileRequest( + html: $html, + width: 260.0, + height: 90.0, +)); + +$pdfFile = $outputDir . '/images-preview.pdf'; +$pdfBytes = (new SinglePagePdfExporter())->export($result); +file_put_contents($pdfFile, $pdfBytes); + +$jsonFile = $outputDir . '/images-result.json'; +file_put_contents($jsonFile, json_encode([ + 'bbox' => $result->bbox, + 'resources' => $result->resources, + 'metadata' => $result->metadata, +], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); + +return [ + 'example' => 'images', + 'generated_files' => [$pngPath, $jpegPath, $pdfFile, $jsonFile], + 'content_stream' => $result->contentStream, + 'resources' => $result->resources, + 'bbox' => $result->bbox, + 'pdf_file' => $pdfFile, +]; diff --git a/examples/interpolation.php b/examples/interpolation.php new file mode 100644 index 0000000..09380b7 --- /dev/null +++ b/examples/interpolation.php @@ -0,0 +1,45 @@ +compile(new CompileRequest( + html: '
Signed by {{ name }}
' + . '
Role: {{ role }}
' + . '
Missing: {{ missing }}
', + width: 260.0, + height: 90.0, + context: [ + 'name' => 'Alice ', + 'role' => 'Approver', + ], +)); + +$outputFile = $outputDir . '/interpolation-result.json'; +file_put_contents($outputFile, json_encode([ + 'bbox' => $result->bbox, + 'content_stream' => $result->contentStream, + 'resources' => $result->resources, + 'metadata' => $result->metadata, +], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); + +return [ + 'example' => 'interpolation', + 'generated_files' => [$outputFile], + 'content_stream' => $result->contentStream, + 'resources' => $result->resources, + 'bbox' => $result->bbox, +]; diff --git a/examples/placement.php b/examples/placement.php new file mode 100644 index 0000000..3c9578e --- /dev/null +++ b/examples/placement.php @@ -0,0 +1,44 @@ +compile(new CompileRequest( + html: '
Placement baseline
', + width: 240.0, + height: 84.0, +)); + +$calculator = new XObjectPlacementCalculator(); +$fromWidth = $calculator->fromWidth($result, 175.0, 36.0, 72.0); +$fromHeight = $calculator->fromHeight($result, 61.25, 36.0, 72.0); +$fromScale = $calculator->fromScale($result, 0.729167, 36.0, 72.0); + +$outputFile = $outputDir . '/placement-result.json'; +file_put_contents($outputFile, json_encode([ + 'from_width_command' => $fromWidth->toPdfCommand('Fm0'), + 'from_height_command' => $fromHeight->toPdfCommand('Fm0'), + 'from_scale_command' => $fromScale->toPdfCommand('Fm0'), +], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); + +return [ + 'example' => 'placement', + 'generated_files' => [$outputFile], + 'content_stream' => $result->contentStream, + 'resources' => $result->resources, + 'bbox' => $result->bbox, +]; diff --git a/examples/preview-pdf.php b/examples/preview-pdf.php new file mode 100644 index 0000000..95c3ab1 --- /dev/null +++ b/examples/preview-pdf.php @@ -0,0 +1,38 @@ +compile(new CompileRequest( + html: '
Preview me
' + . '
Visible appearance test
', + width: 260.0, + height: 90.0, +)); + +$pdfBytes = (new SinglePagePdfExporter())->export($result); +$pdfFile = $outputDir . '/preview.pdf'; +file_put_contents($pdfFile, $pdfBytes); + +return [ + 'example' => 'preview-pdf', + 'generated_files' => [$pdfFile], + 'content_stream' => $result->contentStream, + 'resources' => $result->resources, + 'bbox' => $result->bbox, + 'pdf_file' => $pdfFile, +]; diff --git a/examples/svg.php b/examples/svg.php new file mode 100644 index 0000000..233d4b5 --- /dev/null +++ b/examples/svg.php @@ -0,0 +1,51 @@ +' + . '' + . 'SVG sample' + . '
'; + +$result = (new XObjectTemplateCompiler())->compile(new CompileRequest( + html: $html, + width: 280.0, + height: 100.0, +)); + +$pdfFile = $outputDir . '/svg-preview.pdf'; +$pdfBytes = (new SinglePagePdfExporter())->export($result); +file_put_contents($pdfFile, $pdfBytes); + +$jsonFile = $outputDir . '/svg-result.json'; +file_put_contents($jsonFile, json_encode([ + 'bbox' => $result->bbox, + 'resources' => $result->resources, + 'metadata' => $result->metadata, +], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); + +return [ + 'example' => 'svg', + 'generated_files' => [$pdfFile, $jsonFile], + 'content_stream' => $result->contentStream, + 'resources' => $result->resources, + 'bbox' => $result->bbox, + 'pdf_file' => $pdfFile, +]; diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..8bb7b09 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,44 @@ +# SPDX-FileCopyrightText: 2026 LibreSign +# SPDX-License-Identifier: AGPL-3.0-or-later + +site_name: xobject-template +site_description: Minimal HTML+CSS to PDF Form XObject compiler for visible signature appearances +repo_url: https://github.com/LibreSign/xobject-template +repo_name: LibreSign/xobject-template +strict: true +theme: + name: material +plugins: + - search +markdown_extensions: + - admonition + - tables + - toc: + permalink: true +nav: + - Home: + - Overview: index.md + - Installation: installation.md + - Getting started: getting-started.md + - Guides: + - Basic template: guides/basic-template.md + - Preview PDF: guides/preview-pdf.md + - Images: guides/images.md + - SVG: guides/svg.md + - Placement and scaling: guides/placement.md + - Context interpolation: guides/interpolation.md + - PDF signer integration: guides/signer-integration.md + - Reference: + - Public API: reference/public-api.md + - Output contract: reference/output-contract.md + - Supported HTML and CSS: reference/supported-html-css.md + - Supported SVG: reference/supported-svg.md + - Errors and exceptions: reference/errors.md + - Limitations: reference/limitations.md + - Use cases: + - LibreSign: use-cases/libresign.md + - Community: + - Contributing: community/contributing.md + - Sustainability: community/sustainability.md + - Roadmap: community/roadmap.md + - Maintainer notes: community/maintainer-notes.md diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 2927b38..63edd57 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -16,6 +16,9 @@ tests/Integration + + tests/Documentation + diff --git a/requirements-docs.txt b/requirements-docs.txt new file mode 100644 index 0000000..f4a88ee --- /dev/null +++ b/requirements-docs.txt @@ -0,0 +1,5 @@ +# SPDX-FileCopyrightText: 2026 LibreSign +# SPDX-License-Identifier: AGPL-3.0-or-later + +mkdocs>=1.6,<2 +mkdocs-material>=9.6,<10 diff --git a/scripts/check-docs-drift.php b/scripts/check-docs-drift.php new file mode 100644 index 0000000..39d61eb --- /dev/null +++ b/scripts/check-docs-drift.php @@ -0,0 +1,113 @@ +isFile()) { + continue; + } + + if (strtolower($docEntry->getExtension()) !== 'md') { + continue; + } + + $docsFiles[] = $docEntry->getPathname(); +} + +$allDocContent = ''; +foreach ($docsFiles as $docFile) { + $content = file_get_contents($docFile); + if (is_string($content)) { + $allDocContent .= "\n" . $content; + } +} + +preg_match_all('/examples\/[a-z0-9\-]+\.php/', $allDocContent, $exampleMatches); +$referencedExamples = array_values(array_unique($exampleMatches[0] ?? [])); +foreach ($referencedExamples as $exampleReference) { + $examplePath = $projectRoot . '/' . $exampleReference; + if (!is_file($examplePath)) { + $errors[] = sprintf('Documented example does not exist: %s', $exampleReference); + } +} + +$examplesTestPath = $projectRoot . '/tests/Documentation/ExamplesTest.php'; +$examplesTest = file_get_contents($examplesTestPath); +if (!is_string($examplesTest)) { + $errors[] = 'Could not read tests/Documentation/ExamplesTest.php'; +} else { + $allExamples = glob($projectRoot . '/examples/*.php'); + if (!is_array($allExamples)) { + $allExamples = []; + } + + foreach ($allExamples as $exampleFile) { + $base = basename($exampleFile); + if (!str_contains($examplesTest, "'" . $base . "'")) { + $errors[] = sprintf('Example not declared in ExamplesTest::documentedExamples(): %s', $base); + } + } +} + +$subsetParserPath = $projectRoot . '/src/Html/SubsetHtmlParser.php'; +$subsetParser = file_get_contents($subsetParserPath); +$supportedHtmlDoc = file_get_contents($projectRoot . '/docs/reference/supported-html-css.md'); +if (is_string($subsetParser) && is_string($supportedHtmlDoc)) { + if (preg_match('/private array \$allowedTags\s*=\s*\[(.*?)\];/s', $subsetParser, $allowedTagsMatch) === 1) { + preg_match_all("/'([a-z]+)'\\s*=>\\s*true/", $allowedTagsMatch[1], $tagMatches); + $tags = array_values(array_unique($tagMatches[1] ?? [])); + foreach ($tags as $tag) { + if (!str_contains($supportedHtmlDoc, '<' . $tag . '>')) { + $errors[] = sprintf('Supported HTML tag missing from docs/reference/supported-html-css.md: <%s>', $tag); + } + } + } +} + +$svgParserPath = $projectRoot . '/src/Pdf/Svg/SvgPathCommandParser.php'; +$svgParser = file_get_contents($svgParserPath); +$supportedSvgDoc = file_get_contents($projectRoot . '/docs/reference/supported-svg.md'); +if (is_string($svgParser) && is_string($supportedSvgDoc)) { + preg_match_all("/'([A-Z])'\s*=>/", $svgParser, $cmdMatches); + $commands = array_values(array_unique($cmdMatches[1] ?? [])); + foreach ($commands as $command) { + if (!str_contains($supportedSvgDoc, '`' . $command . '`,') && !str_contains($supportedSvgDoc, '`' . $command . '`')) { + $errors[] = sprintf('SVG path command missing from docs/reference/supported-svg.md: %s', $command); + } + } +} + +if ($errors !== []) { + foreach ($errors as $error) { + fwrite(STDERR, '- ' . $error . PHP_EOL); + } + + exit(1); +} + +fwrite(STDOUT, "Documentation drift checks passed.\n"); diff --git a/tests/Documentation/ExamplesTest.php b/tests/Documentation/ExamplesTest.php new file mode 100644 index 0000000..8a7ad7b --- /dev/null +++ b/tests/Documentation/ExamplesTest.php @@ -0,0 +1,101 @@ + + */ + public static function documentedExamples(): array + { + return [ + 'basic-template.php', + 'preview-pdf.php', + 'images.php', + 'svg.php', + 'placement.php', + 'interpolation.php', + ]; + } + + /** + * @return iterable + */ + public static function exampleProvider(): iterable + { + foreach (self::documentedExamples() as $exampleFile) { + yield $exampleFile => ['exampleFile' => $exampleFile]; + } + } + + #[DataProvider('exampleProvider')] + public function testExampleScriptsExecuteAndGenerateExpectedArtifacts(string $exampleFile): void + { + $projectRoot = dirname(__DIR__, 2); + $examplePath = $projectRoot . '/examples/' . $exampleFile; + self::assertFileExists($examplePath); + + $result = require $examplePath; + + self::assertIsArray($result); + self::assertArrayHasKey('generated_files', $result); + self::assertArrayHasKey('content_stream', $result); + self::assertArrayHasKey('resources', $result); + self::assertArrayHasKey('bbox', $result); + + self::assertIsString($result['content_stream']); + self::assertNotSame('', trim((string) $result['content_stream'])); + + self::assertIsArray($result['resources']); + self::assertArrayHasKey('Font', $result['resources']); + + self::assertIsArray($result['bbox']); + self::assertCount(4, $result['bbox']); + + /** @var list $generatedFiles */ + $generatedFiles = $result['generated_files']; + self::assertNotSame([], $generatedFiles); + + foreach ($generatedFiles as $generatedFile) { + self::assertFileExists($generatedFile); + $contents = file_get_contents($generatedFile); + self::assertIsString($contents); + self::assertNotSame('', $contents); + + if (str_ends_with($generatedFile, '.pdf')) { + self::assertStringStartsWith('%PDF-', $contents); + self::assertStringContainsString('%%EOF', $contents); + } + } + + if (isset($result['pdf_file']) && is_string($result['pdf_file'])) { + $pdfContents = file_get_contents($result['pdf_file']); + self::assertIsString($pdfContents); + self::assertStringStartsWith('%PDF-', $pdfContents); + } + } + + public function testEveryPhpExampleFileIsCoveredByTheProvider(): void + { + $projectRoot = dirname(__DIR__, 2); + $allPhpExamples = glob($projectRoot . '/examples/*.php'); + self::assertIsArray($allPhpExamples); + + $actual = array_map(static fn (string $path): string => basename($path), $allPhpExamples); + sort($actual); + + $expected = self::documentedExamples(); + sort($expected); + + self::assertSame($expected, $actual); + } +} From 271fecd3f8845ecf7278d57d5beaa112ea492d1b Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Mon, 1 Jun 2026 01:50:33 -0300 Subject: [PATCH 02/31] docs: remove duplicate PR template and reduce contributing overlap Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .github/pull_request_template.md | 34 -------------------------------- docs/community/contributing.md | 14 ++++++++----- 2 files changed, 9 insertions(+), 39 deletions(-) delete mode 100644 .github/pull_request_template.md diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md deleted file mode 100644 index 4445706..0000000 --- a/.github/pull_request_template.md +++ /dev/null @@ -1,34 +0,0 @@ - - - -## Scope - -- [ ] Change scope and impact are clearly described - -## Evidence - -- [ ] Tests added/updated -- [ ] Relevant checks executed - -## Documentation - -- [ ] Documentation updated -- [ ] Examples updated if public behavior changed -- [ ] Supported HTML/CSS/SVG matrix updated if relevant -- [ ] Documentation drift checks pass - -## Performance - -- [ ] Performance impact considered - -## Output integrity - -- [ ] No unsupported feature documented as supported -- [ ] No generated build artifacts committed to the source branch -- [ ] LibreSign use case impact considered if relevant - -## Compliance - -- [ ] DCO sign-off in all commits -- [ ] REUSE/SPDX metadata preserved -- [ ] License compatibility reviewed diff --git a/docs/community/contributing.md b/docs/community/contributing.md index 8177495..d7ad91c 100644 --- a/docs/community/contributing.md +++ b/docs/community/contributing.md @@ -5,25 +5,29 @@ Contributions are welcome. Keep quality high and changes scoped. -## Setup +The repository-level governance lives in the root file: -Install dependencies and run checks from repository root. +- `CONTRIBUTING.md` -## Core quality commands +Use this page as a **docs/examples complement**, not as a replacement for root contribution policy. + +## Docs and examples workflow + +From repository root, run: - `composer lint` - `composer test:unit` - `composer test:integration` - `composer docs:test` -## Documentation and examples +## Documentation and examples rules - Add/update runnable examples in `examples/`. - Keep examples small and deterministic. - Ensure docs snippets come from real examples. - Run docs drift checks before opening a PR. -## Adding supported behavior +## When adding supported behavior When adding HTML/CSS/SVG support: From 315c860449b00dfe57a213a99085db753d516af4 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Mon, 1 Jun 2026 01:57:39 -0300 Subject: [PATCH 03/31] docs: switch to minimal maintenance mode Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .github/ISSUE_TEMPLATE/config.yml | 3 - .github/workflows/docs.yml | 81 ------------------- README.md | 10 +-- composer.json | 10 +-- docs/community/contributing.md | 55 ------------- docs/community/maintainer-notes.md | 27 ------- docs/community/roadmap.md | 17 ---- docs/community/sustainability.md | 32 -------- docs/getting-started.md | 43 ---------- docs/guides/basic-template.md | 24 ------ docs/guides/images.md | 37 --------- docs/guides/interpolation.md | 21 ----- docs/guides/placement.md | 23 ------ docs/guides/preview-pdf.md | 22 ------ docs/guides/signer-integration.md | 25 ------ docs/guides/svg.md | 22 ------ docs/index.md | 39 --------- docs/installation.md | 31 -------- docs/reference/errors.md | 38 --------- docs/reference/limitations.md | 23 ------ docs/reference/output-contract.md | 33 -------- docs/reference/public-api.md | 52 ------------ docs/reference/supported-html-css.md | 43 ---------- docs/reference/supported-svg.md | 91 --------------------- docs/use-cases/libresign.md | 23 ------ mkdocs.yml | 44 ----------- requirements-docs.txt | 5 -- scripts/check-docs-drift.php | 113 --------------------------- 28 files changed, 2 insertions(+), 985 deletions(-) delete mode 100644 .github/workflows/docs.yml delete mode 100644 docs/community/contributing.md delete mode 100644 docs/community/maintainer-notes.md delete mode 100644 docs/community/roadmap.md delete mode 100644 docs/community/sustainability.md delete mode 100644 docs/getting-started.md delete mode 100644 docs/guides/basic-template.md delete mode 100644 docs/guides/images.md delete mode 100644 docs/guides/interpolation.md delete mode 100644 docs/guides/placement.md delete mode 100644 docs/guides/preview-pdf.md delete mode 100644 docs/guides/signer-integration.md delete mode 100644 docs/guides/svg.md delete mode 100644 docs/index.md delete mode 100644 docs/installation.md delete mode 100644 docs/reference/errors.md delete mode 100644 docs/reference/limitations.md delete mode 100644 docs/reference/output-contract.md delete mode 100644 docs/reference/public-api.md delete mode 100644 docs/reference/supported-html-css.md delete mode 100644 docs/reference/supported-svg.md delete mode 100644 docs/use-cases/libresign.md delete mode 100644 mkdocs.yml delete mode 100644 requirements-docs.txt delete mode 100644 scripts/check-docs-drift.php diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index edce05d..2be618c 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -6,9 +6,6 @@ contact_links: - name: Security disclosure url: https://github.com/LibreSign/xobject-template/security about: Please report vulnerabilities privately. - - name: Documentation site - url: https://github.com/LibreSign/xobject-template/tree/main/docs - about: Browse the user documentation source. - name: Sponsor maintenance url: https://github.com/sponsors/LibreSign about: Funding helps prioritize fixes and compatibility work. diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml deleted file mode 100644 index 81ad86a..0000000 --- a/.github/workflows/docs.yml +++ /dev/null @@ -1,81 +0,0 @@ -# SPDX-FileCopyrightText: 2026 LibreSign -# SPDX-License-Identifier: AGPL-3.0-or-later - -name: docs - -on: - pull_request: - push: - branches: [main] - -permissions: - contents: read - -jobs: - validate-docs: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v6 - - - name: Detect minimum PHP from composer.json - id: php_min - run: | - php_version=$(grep -Po '"php"\s*:\s*"\K[^"]+' composer.json | grep -Eo '[0-9]+\.[0-9]+' | head -n1) - [[ -n "$php_version" ]] || { echo "Could not determine minimum PHP version"; exit 1; } - echo "version=$php_version" >> "$GITHUB_OUTPUT" - - - name: Setup PHP - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ steps.php_min.outputs.version }} - - - name: Setup Python - uses: actions/setup-python@v6 - with: - python-version: '3.12' - - - name: Install Composer dependencies - run: composer install --no-interaction --prefer-dist - - - name: Install PHPUnit bin dependencies - run: composer bin phpunit install --no-interaction --prefer-dist - - - name: Install docs dependencies - run: python -m pip install --upgrade pip && pip install -r requirements-docs.txt - - # Pull requests only validate docs. No deployment from PRs. - - name: Run docs tests - run: composer run docs:test - - deploy-docs: - if: github.event_name == 'push' && github.ref == 'refs/heads/main' - needs: validate-docs - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - name: Checkout - uses: actions/checkout@v6 - - - name: Setup Python - uses: actions/setup-python@v6 - with: - python-version: '3.12' - - - name: Install docs dependencies - run: python -m pip install --upgrade pip && pip install -r requirements-docs.txt - - - name: Build docs site - run: python -m mkdocs build --strict - - # Pushes to main publish docs to gh-pages branch. - # Maintainers should configure GitHub Pages source as: - # Deploy from branch -> gh-pages -> /root - - name: Deploy site to gh-pages branch - uses: peaceiris/actions-gh-pages@v4 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: ./site - publish_branch: gh-pages - force_orphan: true diff --git a/README.md b/README.md index d2ec7b9..30cd00a 100644 --- a/README.md +++ b/README.md @@ -43,21 +43,13 @@ $result = $compiler->compile(new CompileRequest( // $result->bbox ``` -## Documentation - -Full documentation site source is in `docs/` and published via GitHub Pages after merge: - -- Installation, guides, examples, and support matrices: `docs/` - ## LibreSign use case -- Docs page: `docs/use-cases/libresign.md` - LibreSign project: ## Contributing -- Main guide: `docs/community/contributing.md` -- Repository policy: `CONTRIBUTING.md` +- Repository guide: `CONTRIBUTING.md` If this package helps your project generate reliable PDF signature appearances, please star the repository. It helps other developers discover the project and signals that this work is worth maintaining. diff --git a/composer.json b/composer.json index df68c23..90ae953 100644 --- a/composer.json +++ b/composer.json @@ -73,15 +73,7 @@ "sh:security": "shellcheck scripts/*.sh", "test:unit": "vendor-bin/phpunit/vendor/phpunit/phpunit/phpunit --testsuite unit", "test:integration": "vendor-bin/phpunit/vendor/phpunit/phpunit/phpunit --testsuite integration", - "docs:examples": "vendor-bin/phpunit/vendor/phpunit/phpunit/phpunit --testsuite documentation --filter ExamplesTest", - "docs:check-drift": "php scripts/check-docs-drift.php", - "docs:build": "python3 -m mkdocs build --strict", - "docs:serve": "python3 -m mkdocs serve", - "docs:test": [ - "@docs:examples", - "@docs:check-drift", - "@docs:build" - ], + "examples:test": "vendor-bin/phpunit/vendor/phpunit/phpunit/phpunit --testsuite documentation --filter ExamplesTest", "test:coverage": "XDEBUG_MODE=coverage vendor-bin/phpunit/vendor/phpunit/phpunit/phpunit --coverage-text --coverage-clover=build/coverage/clover.xml", "mutation:test": "vendor-bin/mutation/vendor/infection/infection/bin/infection --threads=max", "benchmark:run": "vendor-bin/phpbench/vendor/phpbench/phpbench/bin/phpbench run --config=phpbench.json --report=aggregate", diff --git a/docs/community/contributing.md b/docs/community/contributing.md deleted file mode 100644 index d7ad91c..0000000 --- a/docs/community/contributing.md +++ /dev/null @@ -1,55 +0,0 @@ - - - -# Contributing - -Contributions are welcome. Keep quality high and changes scoped. - -The repository-level governance lives in the root file: - -- `CONTRIBUTING.md` - -Use this page as a **docs/examples complement**, not as a replacement for root contribution policy. - -## Docs and examples workflow - -From repository root, run: - -- `composer lint` -- `composer test:unit` -- `composer test:integration` -- `composer docs:test` - -## Documentation and examples rules - -- Add/update runnable examples in `examples/`. -- Keep examples small and deterministic. -- Ensure docs snippets come from real examples. -- Run docs drift checks before opening a PR. - -## When adding supported behavior - -When adding HTML/CSS/SVG support: - -1. Add/adjust implementation. -2. Add or update fixture-based tests. -3. Update support reference pages. -4. Keep limitations explicit (do not overstate support). - -## Reporting rendering bugs - -A good minimal reproducible report includes: - -- minimal HTML/CSS/SVG input, -- expected vs actual result, -- generated output details when possible, -- package version/commit and PHP version. - -## High-value contributions - -- SVG edge-case fixtures, -- PDF compatibility fixtures, -- layout bug fixes, -- documentation improvements, -- tests for real-world signature templates, -- performance improvements backed by benchmark evidence. diff --git a/docs/community/maintainer-notes.md b/docs/community/maintainer-notes.md deleted file mode 100644 index a03fb19..0000000 --- a/docs/community/maintainer-notes.md +++ /dev/null @@ -1,27 +0,0 @@ - - - -# Maintainer notes - -## Documentation deployment - -- Pull requests validate docs only. -- Pushes to `main` validate and publish the generated site to `gh-pages`. -- GitHub Pages should be configured as: deploy from branch `gh-pages`, folder `/root`. - -## Validation flow - -- Execute `composer docs:test`. -- Ensure examples in `examples/` remain runnable. -- Keep support references aligned with parser/layout/SVG behavior and tests. - -## Scope control - -- Keep user docs focused on public contracts. -- Avoid scope creep into browser-engine semantics or full PDF-signing behavior. -- Keep LibreSign use case visible without making this package LibreSign-only. - -## Drift policy - -- Support matrix docs are checked by `scripts/check-docs-drift.php` against known code-level indicators. -- TODO: add finer-grained automated extraction for CSS property lists if parser/layout structure changes substantially. diff --git a/docs/community/roadmap.md b/docs/community/roadmap.md deleted file mode 100644 index 9b2fbaf..0000000 --- a/docs/community/roadmap.md +++ /dev/null @@ -1,17 +0,0 @@ - - - -# Roadmap - -> This page describes priorities, not implemented behavior. - -Possible priority areas: - -- SVG compatibility improvements. -- Additional layout fixtures. -- More PDF compatibility tests. -- More documentation examples. -- Performance benchmark expansions. -- Additional downstream signer integration examples. - -Roadmap priorities may be influenced by real-world LibreSign needs, community contributions, and funded maintenance work. diff --git a/docs/community/sustainability.md b/docs/community/sustainability.md deleted file mode 100644 index 8ff697b..0000000 --- a/docs/community/sustainability.md +++ /dev/null @@ -1,32 +0,0 @@ - - - -# Sustainability - -Reliable PDF appearance generation needs sustained maintenance. - -This package sits at the intersection of PDF constraints, deterministic layout, images, scoped SVG handling, and downstream signer integration. Edge cases accumulate quickly, and maintaining high-confidence fixtures and tests takes ongoing effort. - -## Why funding matters - -Sponsorship and paid support help sustain: - -- SVG support hardening, -- documentation quality, -- QA and drift checks, -- benchmark and performance maintenance, -- integration examples for downstream signer workflows. - -## Practical ways to support - -- Star the repository. -- Sponsor the project. -- Contribute fixtures/tests. -- Report minimal reproducible issues. -- Evaluate LibreSign if you need a full digital-signature workflow. - -Support channel: - -- GitHub Sponsors: - -If this package helps your project generate reliable PDF signature appearances, consider starring the repository, contributing a fixture, or sponsoring maintenance. diff --git a/docs/getting-started.md b/docs/getting-started.md deleted file mode 100644 index 217241c..0000000 --- a/docs/getting-started.md +++ /dev/null @@ -1,43 +0,0 @@ - - - -# Getting started - -This page uses the real example file `examples/basic-template.php`. - -## Minimal compile flow - -The example creates a `CompileRequest`, calls `XObjectTemplateCompiler::compile()`, and writes a JSON output summary under `build/examples/`. - -```php -compile(new CompileRequest( - html: '
Signed by {{ name }}
', - width: 240.0, - height: 84.0, - context: ['name' => 'Alice'], -)); -``` - -## Result at user level - -The compile result provides: - -- `contentStream`: PDF operators for a Form XObject stream. -- `resources`: font/image resource dictionary data for downstream serialization. -- `bbox`: `[x1, y1, x2, y2]` bounding box. -- `metadata`: rendering diagnostics (`render_ms`, `line_count`, `image_count`, `node_count`). - -## Next steps - -- Basic usage details: [Guides / Basic template](guides/basic-template.md) -- Placement/scaling in downstream PDFs: [Guides / Placement and scaling](guides/placement.md) -- Full output contract: [Reference / Output contract](reference/output-contract.md) - -If this package helps your project generate reliable PDF signature appearances, consider starring the repository, contributing a fixture, or sponsoring maintenance. diff --git a/docs/guides/basic-template.md b/docs/guides/basic-template.md deleted file mode 100644 index 72edba6..0000000 --- a/docs/guides/basic-template.md +++ /dev/null @@ -1,24 +0,0 @@ - - - -# Basic template - -Source example: `examples/basic-template.php` - -## What the example demonstrates - -- HTML input with inline styles. -- `CompileRequest` with `width`, `height`, and optional `context`. -- Compilation through `XObjectTemplateCompiler`. -- Inspection of `contentStream`, `resources`, `bbox`, and `metadata`. - -## Expected output - -The example writes `build/examples/basic-template-result.json` and includes: - -- non-empty content stream, -- font resources, -- a 4-value bounding box, -- render metadata. - -Use this as your baseline before moving to images, SVG, and placement guides. diff --git a/docs/guides/images.md b/docs/guides/images.md deleted file mode 100644 index f9585a7..0000000 --- a/docs/guides/images.md +++ /dev/null @@ -1,37 +0,0 @@ - - - -# Images - -Source example: `examples/images.php` - -## Supported raster formats - -Based on implementation and tests: - -- PNG: supported. -- JPEG: supported. -- Other raster formats (for example GIF): not supported. - -## Source resolution - -Image sources are file paths from ``. - -- During compile, the source path is carried into XObject resources. -- During export/embedding, the file is read from disk. - -## Dimensions and resources - -- `` dimensions come from style width/height when provided. -- Missing/invalid image dimensions default to `32x32` in layout. -- Generated resources include XObject aliases (for example `Im0`, `Im1`) with `Source`, `Width`, and `Height` metadata. - -## Error behavior and limitations - -Typical failures include: - -- source file missing/unreadable, -- unsupported mime type, -- invalid PNG/JPEG payload. - -See [Errors and exceptions](../reference/errors.md) and [Supported SVG](../reference/supported-svg.md) for vector behavior. diff --git a/docs/guides/interpolation.md b/docs/guides/interpolation.md deleted file mode 100644 index 1d45f6e..0000000 --- a/docs/guides/interpolation.md +++ /dev/null @@ -1,21 +0,0 @@ - - - -# Context interpolation - -Source example: `examples/interpolation.php` - -## Placeholder syntax - -The current interpolation feature supports placeholders like: - -- `{{ name }}` -- `{{ role }}` - -## Behavior - -- Interpolation happens before HTML subset parsing. -- Values are HTML-escaped before insertion. -- Unknown placeholders remain unchanged. - -This is useful for signature labels and document workflow overlays where a template shape is fixed but content changes per signer/document. diff --git a/docs/guides/placement.md b/docs/guides/placement.md deleted file mode 100644 index 351bef4..0000000 --- a/docs/guides/placement.md +++ /dev/null @@ -1,23 +0,0 @@ - - - -# Placement and scaling - -Source example: `examples/placement.php` - -## Downstream placement model - -Compile once at a design size, then place the generated Form XObject in downstream PDFs. - -In most workflows, proportional resizing should happen through placement scaling, not by recompiling HTML for each target size. - -## Public helper classes - -- `LibreSign\XObjectTemplate\Integration\XObjectPlacementCalculator` -- `LibreSign\XObjectTemplate\Integration\XObjectPlacement` - -The example shows `fromWidth()`, `fromHeight()`, and `fromScale()` and emits a placement command string with `toPdfCommand()`. - -## Why this is recommended - -Uniform placement scaling keeps text, spacing, and images visually consistent and avoids introducing avoidable template variants. diff --git a/docs/guides/preview-pdf.md b/docs/guides/preview-pdf.md deleted file mode 100644 index a7fec22..0000000 --- a/docs/guides/preview-pdf.md +++ /dev/null @@ -1,22 +0,0 @@ - - - -# Preview PDF - -Source example: `examples/preview-pdf.php` - -## Why preview export is useful - -`xobject-template` compiles Form XObject payloads. For visual debugging, `SinglePagePdfExporter` can wrap a compile result into a one-page PDF using the result `bbox`. - -This makes template iteration easier because you can quickly inspect appearance output without wiring a full downstream signer flow. - -## What the example does - -- Compiles a simple template. -- Exports a preview PDF. -- Writes output to `build/examples/preview.pdf`. - -## Scope reminder - -Preview export is a helper for inspection and integration tests. Production signing/placement still happens in your downstream PDF workflow. diff --git a/docs/guides/signer-integration.md b/docs/guides/signer-integration.md deleted file mode 100644 index 171d1fe..0000000 --- a/docs/guides/signer-integration.md +++ /dev/null @@ -1,25 +0,0 @@ - - - -# PDF signer integration - -`xobject-template` does **not** sign PDFs. - -It generates appearance payloads that downstream signers (or any PDF pipeline that can place Form XObjects) can consume. - -## What downstream consumers typically use - -- `contentStream` -- `resources` -- `bbox` -- optional placement data via integration helpers - -## Typical flow - -1. Compile template to Form XObject payload. -2. Place XObject in target page coordinates. -3. Let downstream signer handle signature cryptography and document-level signing workflow. - -## LibreSign context - -LibreSign is a flagship use case where this package focuses on deterministic appearance generation while LibreSign handles broader digital-signature workflow responsibilities. diff --git a/docs/guides/svg.md b/docs/guides/svg.md deleted file mode 100644 index a3dcdbd..0000000 --- a/docs/guides/svg.md +++ /dev/null @@ -1,22 +0,0 @@ - - - -# SVG - -Source example: `examples/svg.php` - -## Current SVG usage path - -SVG is handled through image source input (``) and then converted to a PDF Form XObject by the current SVG implementation. - -## Typical use cases - -- deterministic logos, -- deterministic icons, -- deterministic vector shape overlays in signature appearances. - -## Important scope limits - -SVG support is intentionally scoped and deterministic. It is not a full browser-grade SVG engine. - -See [Reference / Supported SVG](../reference/supported-svg.md) for exact supported primitives, path commands, transforms, paint behavior, and known unsupported features. diff --git a/docs/index.md b/docs/index.md deleted file mode 100644 index 1a90464..0000000 --- a/docs/index.md +++ /dev/null @@ -1,39 +0,0 @@ - - - -# xobject-template - -`xobject-template` compiles a focused HTML+CSS subset into reusable PDF Form XObject payloads. - -It is designed for deterministic visual appearance generation in digital-signature and document-workflow systems: signature appearances, stamps, labels, and overlays. - -It solves a practical integration problem: many PDF signers can place Form XObjects but do not want to implement HTML/CSS rendering. - -## What this package does - -- Renders text from a minimal HTML/CSS subset. -- Supports context interpolation for template personalization. -- Supports PNG/JPEG image embedding through file sources. -- Supports scoped SVG rendering through current implementation support. -- Produces reusable output contracts (`contentStream`, `resources`, `bbox`, metadata). - -## What this package is not - -- A full PDF signer. -- A browser engine. -- A generic PDF editor. -- A complete HTML/CSS/SVG renderer. - -## Start quickly - -Go to [Getting started](getting-started.md) for a runnable example based on `examples/basic-template.php`. - -## Key references - -- [Supported HTML and CSS](reference/supported-html-css.md) -- [Supported SVG](reference/supported-svg.md) -- [LibreSign use case](use-cases/libresign.md) -- [Contributing](community/contributing.md) -- [Sustainability](community/sustainability.md) - -If this package helps your project generate reliable PDF signature appearances, consider starring the repository, contributing a fixture, or sponsoring maintenance. diff --git a/docs/installation.md b/docs/installation.md deleted file mode 100644 index bb329b5..0000000 --- a/docs/installation.md +++ /dev/null @@ -1,31 +0,0 @@ - - - -# Installation - -## Requirements - -From `composer.json`: - -- PHP: `^8.2` -- Required PHP extensions: no explicit extra extensions declared by this package. - -Runtime notes: - -- The HTML parser uses `DOMDocument`. -- Image metadata detection uses `getimagesizefromstring` for raster images. -- The package reads local image files for image embedding/export workflows. - -## Install with Composer - -```bash -composer require libresign/xobject-template -``` - -## Verify installation - -A quick verification is to run one example from this repository after dependencies are installed: - -- `examples/basic-template.php` - -See [Getting started](getting-started.md) for the exact flow and output expectations. diff --git a/docs/reference/errors.md b/docs/reference/errors.md deleted file mode 100644 index b8e78fe..0000000 --- a/docs/reference/errors.md +++ /dev/null @@ -1,38 +0,0 @@ - - - -# Errors and exceptions - -## Public exception to catch - -- `LibreSign\XObjectTemplate\Exception\UnsupportedSubsetException` - - Raised when HTML includes unsupported tags in the subset parser. - -## Common invalid argument failures - -`\InvalidArgumentException` can be raised by integration/export/image/SVG paths, for example: - -- invalid `bbox` for export/placement, -- empty placement alias, -- image source missing or unreadable, -- unsupported image format, -- invalid SVG/viewBox/path payload. - -## What users should do - -- Validate input templates against [Supported HTML and CSS](supported-html-css.md). -- Validate SVG assets against [Supported SVG](supported-svg.md). -- Keep reproducible template and asset fixtures for failing cases. - -## How to report a minimal reproducible issue - -Please include: - -- minimal HTML/CSS/SVG input, -- compile request dimensions, -- expected behavior, -- actual behavior, -- package version or commit hash, -- PHP version and runtime context. - -See [Community / Contributing](../community/contributing.md) for reporting guidance. diff --git a/docs/reference/limitations.md b/docs/reference/limitations.md deleted file mode 100644 index 1d2ec6a..0000000 --- a/docs/reference/limitations.md +++ /dev/null @@ -1,23 +0,0 @@ - - - -# Limitations - -`xobject-template` is intentionally scoped. - -## Explicit non-goals - -- Not a browser engine. -- Not a full PDF signer. -- Not a generic PDF editor. -- Not a complete HTML/CSS/SVG renderer. - -## Rendering scope - -- HTML/CSS/SVG support is intentionally limited to deterministic overlay/stamp workflows. -- Behavior is deterministic, but not browser-compatible. -- Unsupported subset inputs should be treated as out of scope, not as promised behavior. - -## Integration expectation - -Consumers should validate templates that matter to their workflow and keep fixture-based regression tests for critical appearance requirements. diff --git a/docs/reference/output-contract.md b/docs/reference/output-contract.md deleted file mode 100644 index 67efc7f..0000000 --- a/docs/reference/output-contract.md +++ /dev/null @@ -1,33 +0,0 @@ - - - -# Output contract - -`CompileResult` is the stable output boundary for integration. - -## Fields - -| Field | Type | Meaning | -|---|---|---| -| `contentStream` | `string` | PDF operators intended for a Form XObject stream body. | -| `resources` | `array` | Resource dictionary data (fonts + referenced image XObjects). | -| `bbox` | `[float,float,float,float]` | Bounding box `[x1,y1,x2,y2]` used for placement/export. | -| `metadata` | `array` | Diagnostics such as `render_ms`, `line_count`, `image_count`, `node_count`. | - -## Resource shape used by helpers - -- Fonts are keyed aliases (for example `F1`..`F6`) with PDF font dictionaries. -- Image resources are keyed aliases (for example `Im0`) with at least source/size metadata. - -## What downstream consumers should rely on - -- `contentStream` + `resources` + `bbox` as the core Form XObject payload contract. -- `bbox` defines base geometry for placement scaling. - -## What is intentionally not guaranteed - -- Browser-equivalent rendering behavior. -- Full HTML/CSS/SVG compatibility. -- Stable internals of parser/layout implementation classes. - -For transport-neutral payload arrays, see `XObjectPayloadAdapter` in [Public API](public-api.md). diff --git a/docs/reference/public-api.md b/docs/reference/public-api.md deleted file mode 100644 index 3b9b5c8..0000000 --- a/docs/reference/public-api.md +++ /dev/null @@ -1,52 +0,0 @@ - - - -# Public API - -This page documents user-facing API intended for integration. - -## Compiler entry point - -- `LibreSign\XObjectTemplate\XObjectTemplateCompiler` -- `LibreSign\XObjectTemplate\Contract\XObjectTemplateCompilerInterface` - -Primary method: - -- `compile(CompileRequest $request): CompileResult` - -## Request DTO - -- `LibreSign\XObjectTemplate\Dto\CompileRequest` - - `html: string` - - `width: float = 240.0` - - `height: float = 84.0` - - `context: array = []` - -## Result DTO - -- `LibreSign\XObjectTemplate\Dto\CompileResult` - - `contentStream: string` - - `resources: array` - - `bbox: array{0: float, 1: float, 2: float, 3: float}` - - `metadata: array` - -## Integration helpers - -- `LibreSign\XObjectTemplate\Integration\XObjectPayloadAdapter` - - `toXObjectPayload(CompileResult $result): array{stream,resources,bbox}` -- `LibreSign\XObjectTemplate\Integration\XObjectPlacementCalculator` - - `fromWidth(...)`, `fromHeight(...)`, `fromScale(...)` -- `LibreSign\XObjectTemplate\Integration\XObjectPlacement` - - `toPdfCommand(string $alias): string` - -## Preview/export helper - -- `LibreSign\XObjectTemplate\Pdf\SinglePagePdfExporter` - - `export(CompileResult $result): string` - -## Exceptions users should handle - -- `LibreSign\XObjectTemplate\Exception\UnsupportedSubsetException` -- `\InvalidArgumentException` from invalid placement/bounding box/image source/format scenarios. - -For exact output semantics, see [Output contract](output-contract.md). diff --git a/docs/reference/supported-html-css.md b/docs/reference/supported-html-css.md deleted file mode 100644 index 16db04f..0000000 --- a/docs/reference/supported-html-css.md +++ /dev/null @@ -1,43 +0,0 @@ - - - -# Supported HTML and CSS - -This page reflects current implementation and tests. - -## HTML elements - -Supported: - -- `
` -- `

` -- `` -- `
` -- `` - -Not supported (examples): tables, lists, semantic formatting tags like ``. - -## CSS properties used by renderer - -Common supported properties include: - -- Typography: `font-size`, `font-family`, `font-weight`, `line-height`, `color`, `text-align`, `hyphens`, `white-space` -- Layout/box: `margin`, `padding`, `width`, `height`, `overflow`, `text-overflow` -- Decoration: `background-color`, `border-color`, `border-width`, `border-radius` -- Flex subset: `display:flex`, `flex-direction`, `justify-content`, `align-items`, `gap` -- Absolute positioning: `position:absolute`, `top`, `right`, `bottom`, `left` - -## Units and values - -- Unitless numeric values are accepted for many properties. -- `px` is accepted and converted to PDF points. -- `%` is supported for dimensions/offsets in current layout implementation. -- Unknown or invalid declarations are ignored, not fatal. - -## Practical notes - -- Font alias mapping targets built-in Helvetica/Times/Courier variants. -- Overflow clipping uses PDF clipping paths (`overflow:hidden`). -- The behavior is deterministic and intentionally scoped for overlays/stamps, not browser parity. - -See [Limitations](limitations.md) before designing production templates. diff --git a/docs/reference/supported-svg.md b/docs/reference/supported-svg.md deleted file mode 100644 index 95e339c..0000000 --- a/docs/reference/supported-svg.md +++ /dev/null @@ -1,91 +0,0 @@ - - - -# Supported SVG - -SVG support is intentionally scoped and deterministic. - -## Supported usage pattern - -- SVG is consumed through image source input (for example ``). -- `.svg` and `.svgz` extension detection is supported. -- Content-based SVG detection is also supported (` `f` -- stroke only -> `S` -- fill + stroke -> `B` - -`stroke-width` is supported (style attribute takes precedence over presentation attribute). - -## ViewBox and dimensions - -- `viewBox` is supported and takes precedence over `width`/`height`. -- If no valid `viewBox` is present, positive `width` and `height` are required. -- Non-positive viewport dimensions are rejected. - -## Unsupported or intentionally out-of-scope - -- Full SVG text layout support. -- Filters, masks, gradients, patterns, and full SVG compositing model. -- Browser-equivalent SVG rendering semantics. - -## Error behavior - -Common error cases include: - -- invalid SVG root/empty payload, -- malformed path data, -- unsupported path command, -- invalid or non-positive viewport definitions. - -If this package helps your project generate reliable PDF signature appearances, consider starring the repository, contributing a fixture, or sponsoring maintenance. diff --git a/docs/use-cases/libresign.md b/docs/use-cases/libresign.md deleted file mode 100644 index bba7d58..0000000 --- a/docs/use-cases/libresign.md +++ /dev/null @@ -1,23 +0,0 @@ - - - -# LibreSign use case - -LibreSign is the flagship real-world use case for this package. - -`xobject-template` helps generate deterministic visual signature appearances, stamps, and overlays. LibreSign uses this kind of appearance generation inside a broader digital-signature workflow. - -## Scope split - -- `xobject-template`: appearance generation (Form XObject-oriented rendering). -- LibreSign: end-to-end digital-signature workflow and platform integration. - -## Official links - -- LibreSign website: -- LibreSign GitHub repository: -- LibreSign documentation: - -If you need a complete digital-signature solution, evaluate LibreSign. - -If this package helps your project generate reliable PDF signature appearances, consider starring the repository, contributing a fixture, or sponsoring maintenance. diff --git a/mkdocs.yml b/mkdocs.yml deleted file mode 100644 index 8bb7b09..0000000 --- a/mkdocs.yml +++ /dev/null @@ -1,44 +0,0 @@ -# SPDX-FileCopyrightText: 2026 LibreSign -# SPDX-License-Identifier: AGPL-3.0-or-later - -site_name: xobject-template -site_description: Minimal HTML+CSS to PDF Form XObject compiler for visible signature appearances -repo_url: https://github.com/LibreSign/xobject-template -repo_name: LibreSign/xobject-template -strict: true -theme: - name: material -plugins: - - search -markdown_extensions: - - admonition - - tables - - toc: - permalink: true -nav: - - Home: - - Overview: index.md - - Installation: installation.md - - Getting started: getting-started.md - - Guides: - - Basic template: guides/basic-template.md - - Preview PDF: guides/preview-pdf.md - - Images: guides/images.md - - SVG: guides/svg.md - - Placement and scaling: guides/placement.md - - Context interpolation: guides/interpolation.md - - PDF signer integration: guides/signer-integration.md - - Reference: - - Public API: reference/public-api.md - - Output contract: reference/output-contract.md - - Supported HTML and CSS: reference/supported-html-css.md - - Supported SVG: reference/supported-svg.md - - Errors and exceptions: reference/errors.md - - Limitations: reference/limitations.md - - Use cases: - - LibreSign: use-cases/libresign.md - - Community: - - Contributing: community/contributing.md - - Sustainability: community/sustainability.md - - Roadmap: community/roadmap.md - - Maintainer notes: community/maintainer-notes.md diff --git a/requirements-docs.txt b/requirements-docs.txt deleted file mode 100644 index f4a88ee..0000000 --- a/requirements-docs.txt +++ /dev/null @@ -1,5 +0,0 @@ -# SPDX-FileCopyrightText: 2026 LibreSign -# SPDX-License-Identifier: AGPL-3.0-or-later - -mkdocs>=1.6,<2 -mkdocs-material>=9.6,<10 diff --git a/scripts/check-docs-drift.php b/scripts/check-docs-drift.php deleted file mode 100644 index 39d61eb..0000000 --- a/scripts/check-docs-drift.php +++ /dev/null @@ -1,113 +0,0 @@ -isFile()) { - continue; - } - - if (strtolower($docEntry->getExtension()) !== 'md') { - continue; - } - - $docsFiles[] = $docEntry->getPathname(); -} - -$allDocContent = ''; -foreach ($docsFiles as $docFile) { - $content = file_get_contents($docFile); - if (is_string($content)) { - $allDocContent .= "\n" . $content; - } -} - -preg_match_all('/examples\/[a-z0-9\-]+\.php/', $allDocContent, $exampleMatches); -$referencedExamples = array_values(array_unique($exampleMatches[0] ?? [])); -foreach ($referencedExamples as $exampleReference) { - $examplePath = $projectRoot . '/' . $exampleReference; - if (!is_file($examplePath)) { - $errors[] = sprintf('Documented example does not exist: %s', $exampleReference); - } -} - -$examplesTestPath = $projectRoot . '/tests/Documentation/ExamplesTest.php'; -$examplesTest = file_get_contents($examplesTestPath); -if (!is_string($examplesTest)) { - $errors[] = 'Could not read tests/Documentation/ExamplesTest.php'; -} else { - $allExamples = glob($projectRoot . '/examples/*.php'); - if (!is_array($allExamples)) { - $allExamples = []; - } - - foreach ($allExamples as $exampleFile) { - $base = basename($exampleFile); - if (!str_contains($examplesTest, "'" . $base . "'")) { - $errors[] = sprintf('Example not declared in ExamplesTest::documentedExamples(): %s', $base); - } - } -} - -$subsetParserPath = $projectRoot . '/src/Html/SubsetHtmlParser.php'; -$subsetParser = file_get_contents($subsetParserPath); -$supportedHtmlDoc = file_get_contents($projectRoot . '/docs/reference/supported-html-css.md'); -if (is_string($subsetParser) && is_string($supportedHtmlDoc)) { - if (preg_match('/private array \$allowedTags\s*=\s*\[(.*?)\];/s', $subsetParser, $allowedTagsMatch) === 1) { - preg_match_all("/'([a-z]+)'\\s*=>\\s*true/", $allowedTagsMatch[1], $tagMatches); - $tags = array_values(array_unique($tagMatches[1] ?? [])); - foreach ($tags as $tag) { - if (!str_contains($supportedHtmlDoc, '<' . $tag . '>')) { - $errors[] = sprintf('Supported HTML tag missing from docs/reference/supported-html-css.md: <%s>', $tag); - } - } - } -} - -$svgParserPath = $projectRoot . '/src/Pdf/Svg/SvgPathCommandParser.php'; -$svgParser = file_get_contents($svgParserPath); -$supportedSvgDoc = file_get_contents($projectRoot . '/docs/reference/supported-svg.md'); -if (is_string($svgParser) && is_string($supportedSvgDoc)) { - preg_match_all("/'([A-Z])'\s*=>/", $svgParser, $cmdMatches); - $commands = array_values(array_unique($cmdMatches[1] ?? [])); - foreach ($commands as $command) { - if (!str_contains($supportedSvgDoc, '`' . $command . '`,') && !str_contains($supportedSvgDoc, '`' . $command . '`')) { - $errors[] = sprintf('SVG path command missing from docs/reference/supported-svg.md: %s', $command); - } - } -} - -if ($errors !== []) { - foreach ($errors as $error) { - fwrite(STDERR, '- ' . $error . PHP_EOL); - } - - exit(1); -} - -fwrite(STDOUT, "Documentation drift checks passed.\n"); From 1e061a929c2b8290a8d7df80b059df52a38c4f75 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Mon, 1 Jun 2026 01:59:19 -0300 Subject: [PATCH 04/31] docs: isolate sources under docs/source and ignore docs/site outputs Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .gitignore | 2 ++ docs/README.md | 19 +++++++++++++++++++ docs/source/README.md | 12 ++++++++++++ 3 files changed, 33 insertions(+) create mode 100644 docs/README.md create mode 100644 docs/source/README.md diff --git a/.gitignore b/.gitignore index 29c30da..0d25515 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,8 @@ /vendor-bin/**/composer.lock /composer.lock /build/ +/site/ +/docs/site/ /.phpunit.result.cache /.infection.cache /.idea/ diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..91e4377 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,19 @@ + + + +# Documentation folder layout + +This folder is intentionally split to isolate source content from generated output. + +## Structure + +- `docs/source/`: documentation source files maintained by contributors. +- `docs/site/`: generated static site output (build artifact, not versioned). + +## Rules + +- Keep hand-written documentation in `docs/source/`. +- Never commit generated files under `docs/site/`. +- Build outputs in root `site/` are also ignored to reduce accidental commits. + +If a docs tool is reintroduced in the future, configure it to read from `docs/source/` and write to `docs/site/`. diff --git a/docs/source/README.md b/docs/source/README.md new file mode 100644 index 0000000..d014dfd --- /dev/null +++ b/docs/source/README.md @@ -0,0 +1,12 @@ + + + +# Documentation sources + +Place documentation source files in this directory. + +Suggested future convention: + +- one page per topic, +- short user-focused sections, +- examples that map to runnable files under `examples/`. From 34bc1b096644e7773c4ee0b120c8412c37a50a51 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Mon, 1 Jun 2026 02:03:36 -0300 Subject: [PATCH 05/31] docs: move examples to docs/source/examples with updated path references Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .github/.performance/baseline.json | 16 ++++++++++++++++ .../source/examples}/assets/sample.svg | 0 .../source/examples}/assets/tiny-jpeg.base64 | 0 .../source/examples}/assets/tiny-png.base64 | 0 .../source/examples}/basic-template.php | 4 ++-- {examples => docs/source/examples}/images.php | 6 +++--- .../source/examples}/interpolation.php | 4 ++-- {examples => docs/source/examples}/placement.php | 4 ++-- .../source/examples}/preview-pdf.php | 4 ++-- {examples => docs/source/examples}/svg.php | 6 +++--- tests/Documentation/ExamplesTest.php | 4 ++-- 11 files changed, 32 insertions(+), 16 deletions(-) create mode 100644 .github/.performance/baseline.json rename {examples => docs/source/examples}/assets/sample.svg (100%) rename {examples => docs/source/examples}/assets/tiny-jpeg.base64 (100%) rename {examples => docs/source/examples}/assets/tiny-png.base64 (100%) rename {examples => docs/source/examples}/basic-template.php (93%) rename {examples => docs/source/examples}/images.php (93%) rename {examples => docs/source/examples}/interpolation.php (93%) rename {examples => docs/source/examples}/placement.php (93%) rename {examples => docs/source/examples}/preview-pdf.php (92%) rename {examples => docs/source/examples}/svg.php (90%) diff --git a/.github/.performance/baseline.json b/.github/.performance/baseline.json new file mode 100644 index 0000000..68d5a90 --- /dev/null +++ b/.github/.performance/baseline.json @@ -0,0 +1,16 @@ +{ + "version": "1.0.0", + "created_at": "2026-06-01", + "allowed_regression_pct": 25.0, + "stale_threshold_pct": 35.0, + "benchmarks": { + "LibreSign\\XObjectTemplate\\Benchmarks\\CompilerBench::benchSimpleHtml": { + "mean": 0.356085, + "memory_real": 768 + }, + "LibreSign\\XObjectTemplate\\Benchmarks\\CompilerBench::benchComplexHtml": { + "mean": 1.366, + "memory_real": 1024 + } + } +} diff --git a/examples/assets/sample.svg b/docs/source/examples/assets/sample.svg similarity index 100% rename from examples/assets/sample.svg rename to docs/source/examples/assets/sample.svg diff --git a/examples/assets/tiny-jpeg.base64 b/docs/source/examples/assets/tiny-jpeg.base64 similarity index 100% rename from examples/assets/tiny-jpeg.base64 rename to docs/source/examples/assets/tiny-jpeg.base64 diff --git a/examples/assets/tiny-png.base64 b/docs/source/examples/assets/tiny-png.base64 similarity index 100% rename from examples/assets/tiny-png.base64 rename to docs/source/examples/assets/tiny-png.base64 diff --git a/examples/basic-template.php b/docs/source/examples/basic-template.php similarity index 93% rename from examples/basic-template.php rename to docs/source/examples/basic-template.php index 057f581..6f19f74 100644 --- a/examples/basic-template.php +++ b/docs/source/examples/basic-template.php @@ -8,9 +8,9 @@ use LibreSign\XObjectTemplate\Dto\CompileRequest; use LibreSign\XObjectTemplate\XObjectTemplateCompiler; -require dirname(__DIR__) . '/vendor/autoload.php'; +require dirname(__DIR__, 3) . '/vendor/autoload.php'; -$projectRoot = dirname(__DIR__); +$projectRoot = dirname(__DIR__, 3); $outputDir = $projectRoot . '/build/examples'; if (!is_dir($outputDir)) { mkdir($outputDir, 0777, true); diff --git a/examples/images.php b/docs/source/examples/images.php similarity index 93% rename from examples/images.php rename to docs/source/examples/images.php index 3933beb..dd7cf12 100644 --- a/examples/images.php +++ b/docs/source/examples/images.php @@ -9,10 +9,10 @@ use LibreSign\XObjectTemplate\Pdf\SinglePagePdfExporter; use LibreSign\XObjectTemplate\XObjectTemplateCompiler; -require dirname(__DIR__) . '/vendor/autoload.php'; +require dirname(__DIR__, 3) . '/vendor/autoload.php'; -$projectRoot = dirname(__DIR__); -$assetDir = $projectRoot . '/examples/assets'; +$projectRoot = dirname(__DIR__, 3); +$assetDir = $projectRoot . '/docs/source/examples/assets'; $outputDir = $projectRoot . '/build/examples'; $outputAssetDir = $outputDir . '/assets'; diff --git a/examples/interpolation.php b/docs/source/examples/interpolation.php similarity index 93% rename from examples/interpolation.php rename to docs/source/examples/interpolation.php index 09380b7..eef92eb 100644 --- a/examples/interpolation.php +++ b/docs/source/examples/interpolation.php @@ -8,9 +8,9 @@ use LibreSign\XObjectTemplate\Dto\CompileRequest; use LibreSign\XObjectTemplate\XObjectTemplateCompiler; -require dirname(__DIR__) . '/vendor/autoload.php'; +require dirname(__DIR__, 3) . '/vendor/autoload.php'; -$projectRoot = dirname(__DIR__); +$projectRoot = dirname(__DIR__, 3); $outputDir = $projectRoot . '/build/examples'; if (!is_dir($outputDir)) { mkdir($outputDir, 0777, true); diff --git a/examples/placement.php b/docs/source/examples/placement.php similarity index 93% rename from examples/placement.php rename to docs/source/examples/placement.php index 3c9578e..8613911 100644 --- a/examples/placement.php +++ b/docs/source/examples/placement.php @@ -9,9 +9,9 @@ use LibreSign\XObjectTemplate\Integration\XObjectPlacementCalculator; use LibreSign\XObjectTemplate\XObjectTemplateCompiler; -require dirname(__DIR__) . '/vendor/autoload.php'; +require dirname(__DIR__, 3) . '/vendor/autoload.php'; -$projectRoot = dirname(__DIR__); +$projectRoot = dirname(__DIR__, 3); $outputDir = $projectRoot . '/build/examples'; if (!is_dir($outputDir)) { mkdir($outputDir, 0777, true); diff --git a/examples/preview-pdf.php b/docs/source/examples/preview-pdf.php similarity index 92% rename from examples/preview-pdf.php rename to docs/source/examples/preview-pdf.php index 95c3ab1..25f7f17 100644 --- a/examples/preview-pdf.php +++ b/docs/source/examples/preview-pdf.php @@ -9,9 +9,9 @@ use LibreSign\XObjectTemplate\Pdf\SinglePagePdfExporter; use LibreSign\XObjectTemplate\XObjectTemplateCompiler; -require dirname(__DIR__) . '/vendor/autoload.php'; +require dirname(__DIR__, 3) . '/vendor/autoload.php'; -$projectRoot = dirname(__DIR__); +$projectRoot = dirname(__DIR__, 3); $outputDir = $projectRoot . '/build/examples'; if (!is_dir($outputDir)) { mkdir($outputDir, 0777, true); diff --git a/examples/svg.php b/docs/source/examples/svg.php similarity index 90% rename from examples/svg.php rename to docs/source/examples/svg.php index 233d4b5..1801994 100644 --- a/examples/svg.php +++ b/docs/source/examples/svg.php @@ -9,11 +9,11 @@ use LibreSign\XObjectTemplate\Pdf\SinglePagePdfExporter; use LibreSign\XObjectTemplate\XObjectTemplateCompiler; -require dirname(__DIR__) . '/vendor/autoload.php'; +require dirname(__DIR__, 3) . '/vendor/autoload.php'; -$projectRoot = dirname(__DIR__); +$projectRoot = dirname(__DIR__, 3); $outputDir = $projectRoot . '/build/examples'; -$svgPath = $projectRoot . '/examples/assets/sample.svg'; +$svgPath = $projectRoot . '/docs/source/examples/assets/sample.svg'; if (!is_dir($outputDir)) { mkdir($outputDir, 0777, true); diff --git a/tests/Documentation/ExamplesTest.php b/tests/Documentation/ExamplesTest.php index 8a7ad7b..d8b630d 100644 --- a/tests/Documentation/ExamplesTest.php +++ b/tests/Documentation/ExamplesTest.php @@ -41,7 +41,7 @@ public static function exampleProvider(): iterable public function testExampleScriptsExecuteAndGenerateExpectedArtifacts(string $exampleFile): void { $projectRoot = dirname(__DIR__, 2); - $examplePath = $projectRoot . '/examples/' . $exampleFile; + $examplePath = $projectRoot . '/docs/source/examples/' . $exampleFile; self::assertFileExists($examplePath); $result = require $examplePath; @@ -87,7 +87,7 @@ public function testExampleScriptsExecuteAndGenerateExpectedArtifacts(string $ex public function testEveryPhpExampleFileIsCoveredByTheProvider(): void { $projectRoot = dirname(__DIR__, 2); - $allPhpExamples = glob($projectRoot . '/examples/*.php'); + $allPhpExamples = glob($projectRoot . '/docs/source/examples/*.php'); self::assertIsArray($allPhpExamples); $actual = array_map(static fn (string $path): string => basename($path), $allPhpExamples); From 8bfdcbe7f7bed6e316b569d135e6cba577dbe359 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Mon, 1 Jun 2026 10:32:37 -0300 Subject: [PATCH 06/31] chore: remove unecessary template Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .github/ISSUE_TEMPLATE/commercial_support.yml | 36 ------------------- 1 file changed, 36 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/commercial_support.yml diff --git a/.github/ISSUE_TEMPLATE/commercial_support.yml b/.github/ISSUE_TEMPLATE/commercial_support.yml deleted file mode 100644 index 32ea6b9..0000000 --- a/.github/ISSUE_TEMPLATE/commercial_support.yml +++ /dev/null @@ -1,36 +0,0 @@ -# SPDX-FileCopyrightText: 2026 LibreSign -# SPDX-License-Identifier: AGPL-3.0-or-later - -name: Commercial support -description: Ask about paid support or sponsored prioritization -labels: [support] -body: - - type: markdown - attributes: - value: | - Thanks for your interest in commercial support. - - This template does not guarantee free consulting. Sponsored or paid work can help prioritize compatibility, features, and long-term maintenance. - - Current public funding channel: https://github.com/sponsors/LibreSign - - type: textarea - id: context - attributes: - label: Project context - description: Describe your use case, expected timeline, and technical scope. - validations: - required: true - - type: textarea - id: requested_scope - attributes: - label: Requested support scope - description: Include expected deliverables (for example SVG compatibility work, integration support, QA hardening). - validations: - required: true - - type: input - id: contact - attributes: - label: Preferred contact method - placeholder: email or issue-based follow-up - validations: - required: true From f06ecb02c3370f6635f132c9b91ff3e73b053ad1 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Mon, 1 Jun 2026 10:40:45 -0300 Subject: [PATCH 07/31] docs: restore minimal public documentation pages Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- docs/README.md | 4 +- docs/index.md | 49 +++++++++++++++++++++ docs/source/README.md | 15 ++++++- docs/source/guides/getting-started.md | 45 +++++++++++++++++++ docs/source/guides/index.md | 6 +++ docs/source/index.md | 43 ++++++++++++++++++ docs/source/reference/examples.md | 30 +++++++++++++ docs/source/reference/index.md | 6 +++ docs/source/use-cases/index.md | 6 +++ docs/source/use-cases/visible-signatures.md | 27 ++++++++++++ 10 files changed, 228 insertions(+), 3 deletions(-) create mode 100644 docs/index.md create mode 100644 docs/source/guides/getting-started.md create mode 100644 docs/source/guides/index.md create mode 100644 docs/source/index.md create mode 100644 docs/source/reference/examples.md create mode 100644 docs/source/reference/index.md create mode 100644 docs/source/use-cases/index.md create mode 100644 docs/source/use-cases/visible-signatures.md diff --git a/docs/README.md b/docs/README.md index 91e4377..6083d98 100644 --- a/docs/README.md +++ b/docs/README.md @@ -7,9 +7,11 @@ This folder is intentionally split to isolate source content from generated outp ## Structure -- `docs/source/`: documentation source files maintained by contributors. +- `docs/source/`: documentation source files maintained by contributors and intended for publication. - `docs/site/`: generated static site output (build artifact, not versioned). +The public entry page is `docs/index.md`. + ## Rules - Keep hand-written documentation in `docs/source/`. diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..6021511 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,49 @@ + + + +# xobject-template documentation + +This is the public documentation landing page for the repository. +The full hand-written source lives under `docs/source/`. + +## What this library does + +`xobject-template` compiles a constrained HTML/CSS subset into reusable PDF Form XObject output. +It is designed for predictable signature appearances, labels, stamps, overlays, and other compact PDF decorations. + +## Start here + +- [Getting started](source/guides/) +- [Examples](source/reference/) +- [Visible signature use case](source/use-cases/) + +## Highlights + +- deterministic rendering for PDF workflows, +- a focused PHP API, +- runnable examples covered by tests, +- a small documentation surface that is easier to maintain. + +## Repository layout + +- `src/` — library code +- `tests/` — unit, integration, and documentation tests +- `docs/` — public documentation pages +- `docs/source/` — source pages and example files + +## Quick example + +```php +use LibreSign\XObjectTemplate\Dto\CompileRequest; +use LibreSign\XObjectTemplate\XObjectTemplateCompiler; + +$compiler = new XObjectTemplateCompiler(); +$result = $compiler->compile(new CompileRequest( + html: '

Signed by {{ name }}
', + width: 240.0, + height: 84.0, + context: ['name' => 'Alice'], +)); +``` + +For executable samples, see the [examples page](source/reference/). diff --git a/docs/source/README.md b/docs/source/README.md index d014dfd..d20190b 100644 --- a/docs/source/README.md +++ b/docs/source/README.md @@ -3,10 +3,21 @@ # Documentation sources -Place documentation source files in this directory. +This directory contains the hand-written documentation pages that are meant to be published. -Suggested future convention: +## Entry point + +- `index.md` — documentation home + +## Suggested structure - one page per topic, - short user-focused sections, - examples that map to runnable files under `examples/`. + +## Published pages + +- `guides/getting-started.md` +- `reference/examples.md` +- `use-cases/visible-signatures.md` +- `examples/` — runnable example sources and fixtures diff --git a/docs/source/guides/getting-started.md b/docs/source/guides/getting-started.md new file mode 100644 index 0000000..fe47d2f --- /dev/null +++ b/docs/source/guides/getting-started.md @@ -0,0 +1,45 @@ + + + +# Getting started + +## Install + +```bash +composer require libresign/xobject-template +``` + +## Render a template + +```php +compile(new CompileRequest( + html: '
Signed by {{ name }}
', + width: 240.0, + height: 84.0, + context: ['name' => 'Alice'], +)); + +// $result->contentStream +// $result->resources +// $result->bbox +``` + +## Validate the examples + +Run the documentation test suite to execute every example file and verify the generated artifacts: + +```bash +composer run examples:test +``` + +## Where to look next + +- [Examples](../reference/examples.md) +- [Visible signature use case](../use-cases/visible-signatures.md) diff --git a/docs/source/guides/index.md b/docs/source/guides/index.md new file mode 100644 index 0000000..06410a6 --- /dev/null +++ b/docs/source/guides/index.md @@ -0,0 +1,6 @@ + + + +# Guides + +- [Getting started](getting-started.md) diff --git a/docs/source/index.md b/docs/source/index.md new file mode 100644 index 0000000..db167e5 --- /dev/null +++ b/docs/source/index.md @@ -0,0 +1,43 @@ + + + +# xobject-template documentation + +`xobject-template` turns a constrained HTML/CSS subset into reusable PDF Form XObject output for signatures, stamps, labels, and overlays. + +## Start here + +- [Getting started](guides/) +- [Examples](reference/) +- [Visible signature use case](use-cases/) + +## What you get + +- deterministic rendering for PDF workflows, +- a focused PHP API, +- runnable examples that are covered by tests, +- a small documentation footprint that is easier to maintain. + +## Repository layout + +- `src/` — library code +- `tests/` — unit, integration, and documentation tests +- `docs/source/` — documentation pages intended for publication +- `docs/source/examples/` — executable examples with fixtures + +## Quick preview + +```php +use LibreSign\XObjectTemplate\Dto\CompileRequest; +use LibreSign\XObjectTemplate\XObjectTemplateCompiler; + +$compiler = new XObjectTemplateCompiler(); +$result = $compiler->compile(new CompileRequest( + html: '
Signed by {{ name }}
', + width: 240.0, + height: 84.0, + context: ['name' => 'Alice'], +)); +``` + +For executable samples, see the [examples page](reference/). diff --git a/docs/source/reference/examples.md b/docs/source/reference/examples.md new file mode 100644 index 0000000..9bc4af1 --- /dev/null +++ b/docs/source/reference/examples.md @@ -0,0 +1,30 @@ + + + +# Examples + +The examples are executable PHP files under `docs/source/examples/`. +Each one is covered by `tests/Documentation/ExamplesTest.php`. + +## Included examples + +- `basic-template.php` — compile a simple template with a context variable +- `preview-pdf.php` — export a single-page PDF preview +- `images.php` — embed PNG and JPEG assets from base64 fixtures +- `svg.php` — render SVG content as an image source +- `placement.php` — demonstrate placement calculations +- `interpolation.php` — interpolate context variables in text + +## How to run them + +```bash +composer run examples:test +``` + +## Output files + +The examples write their generated artifacts to `build/examples/` so the repository stays clean and the outputs remain disposable. + +## Assets + +Supporting fixtures live in `docs/source/examples/assets/`. diff --git a/docs/source/reference/index.md b/docs/source/reference/index.md new file mode 100644 index 0000000..b5deb52 --- /dev/null +++ b/docs/source/reference/index.md @@ -0,0 +1,6 @@ + + + +# Reference + +- [Examples](examples.md) diff --git a/docs/source/use-cases/index.md b/docs/source/use-cases/index.md new file mode 100644 index 0000000..7b52953 --- /dev/null +++ b/docs/source/use-cases/index.md @@ -0,0 +1,6 @@ + + + +# Use cases + +- [Visible signature appearances](visible-signatures.md) diff --git a/docs/source/use-cases/visible-signatures.md b/docs/source/use-cases/visible-signatures.md new file mode 100644 index 0000000..d23b425 --- /dev/null +++ b/docs/source/use-cases/visible-signatures.md @@ -0,0 +1,27 @@ + + + +# Visible signature appearances + +This library exists to support cases where a PDF needs a predictable visible appearance: + +- signature labels, +- approval stamps, +- overlay annotations, +- status blocks, +- other compact PDF decorations. + +## Why this matters + +PDF pipelines usually need stable layout more than browser-grade rendering. `xobject-template` keeps the surface area small so the output is easier to test and reason about. + +## Typical flow + +1. Build a `CompileRequest`. +2. Pass HTML, dimensions, and context to `XObjectTemplateCompiler`. +3. Consume the generated `contentStream`, `resources`, and `bbox` in downstream PDF tooling. + +## Related pages + +- [Getting started](../guides/getting-started.md) +- [Examples](../reference/examples.md) From 39f9b0807796eebf494705a7ccfbe1d60850f906 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Mon, 1 Jun 2026 10:46:13 -0300 Subject: [PATCH 08/31] docs: simplify documentation landing pages Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- docs/index.md | 17 ++++------------- docs/source/index.md | 13 ++++--------- 2 files changed, 8 insertions(+), 22 deletions(-) diff --git a/docs/index.md b/docs/index.md index 6021511..d094503 100644 --- a/docs/index.md +++ b/docs/index.md @@ -3,13 +3,11 @@ # xobject-template documentation -This is the public documentation landing page for the repository. -The full hand-written source lives under `docs/source/`. +Public docs for `xobject-template`. -## What this library does +Source pages live in `docs/source/`. -`xobject-template` compiles a constrained HTML/CSS subset into reusable PDF Form XObject output. -It is designed for predictable signature appearances, labels, stamps, overlays, and other compact PDF decorations. +`xobject-template` compiles a constrained HTML/CSS subset into PDF Form XObject output. ## Start here @@ -17,19 +15,12 @@ It is designed for predictable signature appearances, labels, stamps, overlays, - [Examples](source/reference/) - [Visible signature use case](source/use-cases/) -## Highlights - -- deterministic rendering for PDF workflows, -- a focused PHP API, -- runnable examples covered by tests, -- a small documentation surface that is easier to maintain. - ## Repository layout - `src/` — library code - `tests/` — unit, integration, and documentation tests - `docs/` — public documentation pages -- `docs/source/` — source pages and example files +- `docs/source/` — editable source pages and examples ## Quick example diff --git a/docs/source/index.md b/docs/source/index.md index db167e5..4982af8 100644 --- a/docs/source/index.md +++ b/docs/source/index.md @@ -3,7 +3,9 @@ # xobject-template documentation -`xobject-template` turns a constrained HTML/CSS subset into reusable PDF Form XObject output for signatures, stamps, labels, and overlays. +Editable source for the public docs. + +`xobject-template` compiles a constrained HTML/CSS subset into PDF Form XObject output. ## Start here @@ -11,13 +13,6 @@ - [Examples](reference/) - [Visible signature use case](use-cases/) -## What you get - -- deterministic rendering for PDF workflows, -- a focused PHP API, -- runnable examples that are covered by tests, -- a small documentation footprint that is easier to maintain. - ## Repository layout - `src/` — library code @@ -25,7 +20,7 @@ - `docs/source/` — documentation pages intended for publication - `docs/source/examples/` — executable examples with fixtures -## Quick preview +## Quick example ```php use LibreSign\XObjectTemplate\Dto\CompileRequest; From c3f1666f273d5c579ea4e66094f488badd6fe73f Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Mon, 1 Jun 2026 10:47:30 -0300 Subject: [PATCH 09/31] docs: clarify documentation landing pages Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- docs/index.md | 4 ++-- docs/source/index.md | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/index.md b/docs/index.md index d094503..88aa00e 100644 --- a/docs/index.md +++ b/docs/index.md @@ -3,9 +3,9 @@ # xobject-template documentation -Public docs for `xobject-template`. +This is the front door for the public docs site. -Source pages live in `docs/source/`. +If you're a developer trying to use `xobject-template`, start with the install guide, then open an example, then check a real use case. `xobject-template` compiles a constrained HTML/CSS subset into PDF Form XObject output. diff --git a/docs/source/index.md b/docs/source/index.md index 4982af8..8d1344f 100644 --- a/docs/source/index.md +++ b/docs/source/index.md @@ -3,7 +3,9 @@ # xobject-template documentation -Editable source for the public docs. +Editable source for the public docs site. + +Use this folder when you want to update the pages that get published. `xobject-template` compiles a constrained HTML/CSS subset into PDF Form XObject output. From 14367f41cc8d9717cc98e6f2aa3fef44cdb177d8 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Mon, 1 Jun 2026 10:49:09 -0300 Subject: [PATCH 10/31] docs: remove architecture-heavy landing page text Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- docs/README.md | 15 --------------- docs/index.md | 11 +---------- docs/source/index.md | 11 +---------- 3 files changed, 2 insertions(+), 35 deletions(-) diff --git a/docs/README.md b/docs/README.md index 6083d98..9179016 100644 --- a/docs/README.md +++ b/docs/README.md @@ -4,18 +4,3 @@ # Documentation folder layout This folder is intentionally split to isolate source content from generated output. - -## Structure - -- `docs/source/`: documentation source files maintained by contributors and intended for publication. -- `docs/site/`: generated static site output (build artifact, not versioned). - -The public entry page is `docs/index.md`. - -## Rules - -- Keep hand-written documentation in `docs/source/`. -- Never commit generated files under `docs/site/`. -- Build outputs in root `site/` are also ignored to reduce accidental commits. - -If a docs tool is reintroduced in the future, configure it to read from `docs/source/` and write to `docs/site/`. diff --git a/docs/index.md b/docs/index.md index 88aa00e..7b73376 100644 --- a/docs/index.md +++ b/docs/index.md @@ -3,9 +3,7 @@ # xobject-template documentation -This is the front door for the public docs site. - -If you're a developer trying to use `xobject-template`, start with the install guide, then open an example, then check a real use case. +Read the docs. `xobject-template` compiles a constrained HTML/CSS subset into PDF Form XObject output. @@ -15,13 +13,6 @@ If you're a developer trying to use `xobject-template`, start with the install g - [Examples](source/reference/) - [Visible signature use case](source/use-cases/) -## Repository layout - -- `src/` — library code -- `tests/` — unit, integration, and documentation tests -- `docs/` — public documentation pages -- `docs/source/` — editable source pages and examples - ## Quick example ```php diff --git a/docs/source/index.md b/docs/source/index.md index 8d1344f..61eebf8 100644 --- a/docs/source/index.md +++ b/docs/source/index.md @@ -3,9 +3,7 @@ # xobject-template documentation -Editable source for the public docs site. - -Use this folder when you want to update the pages that get published. +Docs content. `xobject-template` compiles a constrained HTML/CSS subset into PDF Form XObject output. @@ -15,13 +13,6 @@ Use this folder when you want to update the pages that get published. - [Examples](reference/) - [Visible signature use case](use-cases/) -## Repository layout - -- `src/` — library code -- `tests/` — unit, integration, and documentation tests -- `docs/source/` — documentation pages intended for publication -- `docs/source/examples/` — executable examples with fixtures - ## Quick example ```php From c09655fda5054372d8c3ae176b14a54c136e7122 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Mon, 1 Jun 2026 10:49:47 -0300 Subject: [PATCH 11/31] docs: trim examples reference page Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- docs/source/reference/examples.md | 4 ---- 1 file changed, 4 deletions(-) diff --git a/docs/source/reference/examples.md b/docs/source/reference/examples.md index 9bc4af1..4740a7b 100644 --- a/docs/source/reference/examples.md +++ b/docs/source/reference/examples.md @@ -24,7 +24,3 @@ composer run examples:test ## Output files The examples write their generated artifacts to `build/examples/` so the repository stays clean and the outputs remain disposable. - -## Assets - -Supporting fixtures live in `docs/source/examples/assets/`. From 5e95e93089ac23eb73587ae037a5b7688a828f63 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Mon, 1 Jun 2026 10:50:19 -0300 Subject: [PATCH 12/31] docs: simplify public docs summary Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- docs/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.md b/docs/index.md index 7b73376..50df52d 100644 --- a/docs/index.md +++ b/docs/index.md @@ -5,7 +5,7 @@ Read the docs. -`xobject-template` compiles a constrained HTML/CSS subset into PDF Form XObject output. +`xobject-template` compiles a HTML/CSS template into PDF Form XObject output. ## Start here From fbbedae9c88400167f4d00ba3e2c1f144f815949 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Mon, 1 Jun 2026 10:52:39 -0300 Subject: [PATCH 13/31] docs: remove redundant reference index page Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- docs/index.md | 6 ++---- docs/source/README.md | 15 +-------------- docs/source/index.md | 4 ++-- docs/source/reference/index.md | 6 ------ 4 files changed, 5 insertions(+), 26 deletions(-) delete mode 100644 docs/source/reference/index.md diff --git a/docs/index.md b/docs/index.md index 50df52d..9bd2354 100644 --- a/docs/index.md +++ b/docs/index.md @@ -3,14 +3,12 @@ # xobject-template documentation -Read the docs. - `xobject-template` compiles a HTML/CSS template into PDF Form XObject output. ## Start here - [Getting started](source/guides/) -- [Examples](source/reference/) +- [Examples](source/reference/examples.md) - [Visible signature use case](source/use-cases/) ## Quick example @@ -28,4 +26,4 @@ $result = $compiler->compile(new CompileRequest( )); ``` -For executable samples, see the [examples page](source/reference/). +For executable samples, see the [examples page](source/reference/examples.md). diff --git a/docs/source/README.md b/docs/source/README.md index d20190b..ae3be7b 100644 --- a/docs/source/README.md +++ b/docs/source/README.md @@ -3,21 +3,8 @@ # Documentation sources -This directory contains the hand-written documentation pages that are meant to be published. +Hand-written docs live here. ## Entry point - `index.md` — documentation home - -## Suggested structure - -- one page per topic, -- short user-focused sections, -- examples that map to runnable files under `examples/`. - -## Published pages - -- `guides/getting-started.md` -- `reference/examples.md` -- `use-cases/visible-signatures.md` -- `examples/` — runnable example sources and fixtures diff --git a/docs/source/index.md b/docs/source/index.md index 61eebf8..c7da7b4 100644 --- a/docs/source/index.md +++ b/docs/source/index.md @@ -10,7 +10,7 @@ Docs content. ## Start here - [Getting started](guides/) -- [Examples](reference/) +- [Examples](reference/examples.md) - [Visible signature use case](use-cases/) ## Quick example @@ -28,4 +28,4 @@ $result = $compiler->compile(new CompileRequest( )); ``` -For executable samples, see the [examples page](reference/). +For executable samples, see the [examples page](reference/examples.md). diff --git a/docs/source/reference/index.md b/docs/source/reference/index.md deleted file mode 100644 index b5deb52..0000000 --- a/docs/source/reference/index.md +++ /dev/null @@ -1,6 +0,0 @@ - - - -# Reference - -- [Examples](examples.md) From 2649c765bb7f62acc63a1e1f9b88c51bc5543379 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Mon, 1 Jun 2026 10:53:24 -0300 Subject: [PATCH 14/31] docs: remove redundant use-cases index page Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- docs/index.md | 2 +- docs/source/index.md | 2 +- docs/source/use-cases/index.md | 6 ------ 3 files changed, 2 insertions(+), 8 deletions(-) delete mode 100644 docs/source/use-cases/index.md diff --git a/docs/index.md b/docs/index.md index 9bd2354..0a98899 100644 --- a/docs/index.md +++ b/docs/index.md @@ -9,7 +9,7 @@ - [Getting started](source/guides/) - [Examples](source/reference/examples.md) -- [Visible signature use case](source/use-cases/) +- [Visible signature use case](source/use-cases/visible-signatures.md) ## Quick example diff --git a/docs/source/index.md b/docs/source/index.md index c7da7b4..508e456 100644 --- a/docs/source/index.md +++ b/docs/source/index.md @@ -11,7 +11,7 @@ Docs content. - [Getting started](guides/) - [Examples](reference/examples.md) -- [Visible signature use case](use-cases/) +- [Visible signature use case](use-cases/visible-signatures.md) ## Quick example diff --git a/docs/source/use-cases/index.md b/docs/source/use-cases/index.md deleted file mode 100644 index 7b52953..0000000 --- a/docs/source/use-cases/index.md +++ /dev/null @@ -1,6 +0,0 @@ - - - -# Use cases - -- [Visible signature appearances](visible-signatures.md) From 8650dae4d06155eaaf68ba898be3351df8f2f2f8 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Mon, 1 Jun 2026 12:07:03 -0300 Subject: [PATCH 15/31] docs: remove redundant guides index page Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- docs/index.md | 2 +- docs/source/guides/index.md | 6 ------ docs/source/index.md | 2 +- 3 files changed, 2 insertions(+), 8 deletions(-) delete mode 100644 docs/source/guides/index.md diff --git a/docs/index.md b/docs/index.md index 0a98899..1195ead 100644 --- a/docs/index.md +++ b/docs/index.md @@ -7,7 +7,7 @@ ## Start here -- [Getting started](source/guides/) +- [Getting started](source/guides/getting-started.md) - [Examples](source/reference/examples.md) - [Visible signature use case](source/use-cases/visible-signatures.md) diff --git a/docs/source/guides/index.md b/docs/source/guides/index.md deleted file mode 100644 index 06410a6..0000000 --- a/docs/source/guides/index.md +++ /dev/null @@ -1,6 +0,0 @@ - - - -# Guides - -- [Getting started](getting-started.md) diff --git a/docs/source/index.md b/docs/source/index.md index 508e456..e456b25 100644 --- a/docs/source/index.md +++ b/docs/source/index.md @@ -9,7 +9,7 @@ Docs content. ## Start here -- [Getting started](guides/) +- [Getting started](guides/getting-started.md) - [Examples](reference/examples.md) - [Visible signature use case](use-cases/visible-signatures.md) From 4c0ae06188421b261412e63e8e12e9f57a9c53df Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Mon, 1 Jun 2026 12:08:49 -0300 Subject: [PATCH 16/31] docs: remove duplicate source/index.md Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- docs/source/index.md | 31 ------------------------------- 1 file changed, 31 deletions(-) delete mode 100644 docs/source/index.md diff --git a/docs/source/index.md b/docs/source/index.md deleted file mode 100644 index e456b25..0000000 --- a/docs/source/index.md +++ /dev/null @@ -1,31 +0,0 @@ - - - -# xobject-template documentation - -Docs content. - -`xobject-template` compiles a constrained HTML/CSS subset into PDF Form XObject output. - -## Start here - -- [Getting started](guides/getting-started.md) -- [Examples](reference/examples.md) -- [Visible signature use case](use-cases/visible-signatures.md) - -## Quick example - -```php -use LibreSign\XObjectTemplate\Dto\CompileRequest; -use LibreSign\XObjectTemplate\XObjectTemplateCompiler; - -$compiler = new XObjectTemplateCompiler(); -$result = $compiler->compile(new CompileRequest( - html: '
Signed by {{ name }}
', - width: 240.0, - height: 84.0, - context: ['name' => 'Alice'], -)); -``` - -For executable samples, see the [examples page](reference/examples.md). From 9b48b27c2475de8a895a40d7cad95b7e0642ffdb Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Mon, 1 Jun 2026 12:13:00 -0300 Subject: [PATCH 17/31] docs: add MkDocs build and GitHub Pages deployment Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .github/workflows/docs.yml | 52 ++++++++++++++++++++++++++++++++++++++ mkdocs.yml | 34 +++++++++++++++++++++++++ requirements-docs.txt | 2 ++ 3 files changed, 88 insertions(+) create mode 100644 .github/workflows/docs.yml create mode 100644 mkdocs.yml create mode 100644 requirements-docs.txt diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..6d8fdff --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,52 @@ +# SPDX-FileCopyrightText: 2026 LibreSign +# SPDX-License-Identifier: AGPL-3.0-or-later + +name: docs + +on: + push: + branches: + - main + paths: + - 'docs/**' + - 'mkdocs.yml' + - 'requirements-docs.txt' + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: true + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Install docs dependencies + run: pip install -r requirements-docs.txt + + - name: Build docs + run: mkdocs build --strict + + - uses: actions/upload-pages-artifact@v3 + with: + path: docs/site + + deploy: + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - id: deployment + uses: actions/deploy-pages@v4 diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..64c9e83 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,34 @@ +# SPDX-FileCopyrightText: 2026 LibreSign +# SPDX-License-Identifier: AGPL-3.0-or-later + +site_name: xobject-template +site_description: Minimal HTML+CSS to reusable PDF Form XObject compiler. +site_url: https://libresign.github.io/xobject-template/ +repo_url: https://github.com/LibreSign/xobject-template +repo_name: LibreSign/xobject-template +edit_uri: edit/main/docs/ + +docs_dir: docs +site_dir: docs/site + +theme: + name: material + palette: + scheme: default + primary: indigo + features: + - navigation.instant + - navigation.top + - content.code.copy + +nav: + - Home: index.md + - Getting started: source/guides/getting-started.md + - Examples: source/reference/examples.md + - Visible signature use case: source/use-cases/visible-signatures.md + +markdown_extensions: + - pymdownx.highlight: + anchor_linenums: true + - pymdownx.superfences + - pymdownx.inlinehilite diff --git a/requirements-docs.txt b/requirements-docs.txt new file mode 100644 index 0000000..372ffe2 --- /dev/null +++ b/requirements-docs.txt @@ -0,0 +1,2 @@ +mkdocs>=1.6 +mkdocs-material>=9.5 From 6226768cf89a9c1003eb16b2db7d05c134de83a6 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Mon, 1 Jun 2026 12:28:49 -0300 Subject: [PATCH 18/31] docs: flatten docs/source/ into docs/ root Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- docs/{source => }/examples/assets/sample.svg | 0 docs/{source => }/examples/assets/tiny-jpeg.base64 | 0 docs/{source => }/examples/assets/tiny-png.base64 | 0 docs/{source => }/examples/basic-template.php | 4 ++-- docs/{source => }/examples/images.php | 6 +++--- docs/{source => }/examples/interpolation.php | 4 ++-- docs/{source => }/examples/placement.php | 4 ++-- docs/{source => }/examples/preview-pdf.php | 4 ++-- docs/{source => }/examples/svg.php | 6 +++--- docs/{source => }/guides/getting-started.md | 0 docs/index.md | 8 ++++---- docs/{source => }/reference/examples.md | 2 +- docs/source/README.md | 10 ---------- docs/{source => }/use-cases/visible-signatures.md | 0 mkdocs.yml | 6 +++--- tests/Documentation/ExamplesTest.php | 4 ++-- 16 files changed, 24 insertions(+), 34 deletions(-) rename docs/{source => }/examples/assets/sample.svg (100%) rename docs/{source => }/examples/assets/tiny-jpeg.base64 (100%) rename docs/{source => }/examples/assets/tiny-png.base64 (100%) rename docs/{source => }/examples/basic-template.php (93%) rename docs/{source => }/examples/images.php (93%) rename docs/{source => }/examples/interpolation.php (93%) rename docs/{source => }/examples/placement.php (93%) rename docs/{source => }/examples/preview-pdf.php (92%) rename docs/{source => }/examples/svg.php (90%) rename docs/{source => }/guides/getting-started.md (100%) rename docs/{source => }/reference/examples.md (91%) delete mode 100644 docs/source/README.md rename docs/{source => }/use-cases/visible-signatures.md (100%) diff --git a/docs/source/examples/assets/sample.svg b/docs/examples/assets/sample.svg similarity index 100% rename from docs/source/examples/assets/sample.svg rename to docs/examples/assets/sample.svg diff --git a/docs/source/examples/assets/tiny-jpeg.base64 b/docs/examples/assets/tiny-jpeg.base64 similarity index 100% rename from docs/source/examples/assets/tiny-jpeg.base64 rename to docs/examples/assets/tiny-jpeg.base64 diff --git a/docs/source/examples/assets/tiny-png.base64 b/docs/examples/assets/tiny-png.base64 similarity index 100% rename from docs/source/examples/assets/tiny-png.base64 rename to docs/examples/assets/tiny-png.base64 diff --git a/docs/source/examples/basic-template.php b/docs/examples/basic-template.php similarity index 93% rename from docs/source/examples/basic-template.php rename to docs/examples/basic-template.php index 6f19f74..e317945 100644 --- a/docs/source/examples/basic-template.php +++ b/docs/examples/basic-template.php @@ -8,9 +8,9 @@ use LibreSign\XObjectTemplate\Dto\CompileRequest; use LibreSign\XObjectTemplate\XObjectTemplateCompiler; -require dirname(__DIR__, 3) . '/vendor/autoload.php'; +require dirname(__DIR__, 2) . '/vendor/autoload.php'; -$projectRoot = dirname(__DIR__, 3); +$projectRoot = dirname(__DIR__, 2); $outputDir = $projectRoot . '/build/examples'; if (!is_dir($outputDir)) { mkdir($outputDir, 0777, true); diff --git a/docs/source/examples/images.php b/docs/examples/images.php similarity index 93% rename from docs/source/examples/images.php rename to docs/examples/images.php index dd7cf12..4f21abc 100644 --- a/docs/source/examples/images.php +++ b/docs/examples/images.php @@ -9,10 +9,10 @@ use LibreSign\XObjectTemplate\Pdf\SinglePagePdfExporter; use LibreSign\XObjectTemplate\XObjectTemplateCompiler; -require dirname(__DIR__, 3) . '/vendor/autoload.php'; +require dirname(__DIR__, 2) . '/vendor/autoload.php'; -$projectRoot = dirname(__DIR__, 3); -$assetDir = $projectRoot . '/docs/source/examples/assets'; +$projectRoot = dirname(__DIR__, 2); +$assetDir = $projectRoot . '/docs/examples/assets'; $outputDir = $projectRoot . '/build/examples'; $outputAssetDir = $outputDir . '/assets'; diff --git a/docs/source/examples/interpolation.php b/docs/examples/interpolation.php similarity index 93% rename from docs/source/examples/interpolation.php rename to docs/examples/interpolation.php index eef92eb..77b23d3 100644 --- a/docs/source/examples/interpolation.php +++ b/docs/examples/interpolation.php @@ -8,9 +8,9 @@ use LibreSign\XObjectTemplate\Dto\CompileRequest; use LibreSign\XObjectTemplate\XObjectTemplateCompiler; -require dirname(__DIR__, 3) . '/vendor/autoload.php'; +require dirname(__DIR__, 2) . '/vendor/autoload.php'; -$projectRoot = dirname(__DIR__, 3); +$projectRoot = dirname(__DIR__, 2); $outputDir = $projectRoot . '/build/examples'; if (!is_dir($outputDir)) { mkdir($outputDir, 0777, true); diff --git a/docs/source/examples/placement.php b/docs/examples/placement.php similarity index 93% rename from docs/source/examples/placement.php rename to docs/examples/placement.php index 8613911..fbb9cd4 100644 --- a/docs/source/examples/placement.php +++ b/docs/examples/placement.php @@ -9,9 +9,9 @@ use LibreSign\XObjectTemplate\Integration\XObjectPlacementCalculator; use LibreSign\XObjectTemplate\XObjectTemplateCompiler; -require dirname(__DIR__, 3) . '/vendor/autoload.php'; +require dirname(__DIR__, 2) . '/vendor/autoload.php'; -$projectRoot = dirname(__DIR__, 3); +$projectRoot = dirname(__DIR__, 2); $outputDir = $projectRoot . '/build/examples'; if (!is_dir($outputDir)) { mkdir($outputDir, 0777, true); diff --git a/docs/source/examples/preview-pdf.php b/docs/examples/preview-pdf.php similarity index 92% rename from docs/source/examples/preview-pdf.php rename to docs/examples/preview-pdf.php index 25f7f17..d14ad03 100644 --- a/docs/source/examples/preview-pdf.php +++ b/docs/examples/preview-pdf.php @@ -9,9 +9,9 @@ use LibreSign\XObjectTemplate\Pdf\SinglePagePdfExporter; use LibreSign\XObjectTemplate\XObjectTemplateCompiler; -require dirname(__DIR__, 3) . '/vendor/autoload.php'; +require dirname(__DIR__, 2) . '/vendor/autoload.php'; -$projectRoot = dirname(__DIR__, 3); +$projectRoot = dirname(__DIR__, 2); $outputDir = $projectRoot . '/build/examples'; if (!is_dir($outputDir)) { mkdir($outputDir, 0777, true); diff --git a/docs/source/examples/svg.php b/docs/examples/svg.php similarity index 90% rename from docs/source/examples/svg.php rename to docs/examples/svg.php index 1801994..36d5561 100644 --- a/docs/source/examples/svg.php +++ b/docs/examples/svg.php @@ -9,11 +9,11 @@ use LibreSign\XObjectTemplate\Pdf\SinglePagePdfExporter; use LibreSign\XObjectTemplate\XObjectTemplateCompiler; -require dirname(__DIR__, 3) . '/vendor/autoload.php'; +require dirname(__DIR__, 2) . '/vendor/autoload.php'; -$projectRoot = dirname(__DIR__, 3); +$projectRoot = dirname(__DIR__, 2); $outputDir = $projectRoot . '/build/examples'; -$svgPath = $projectRoot . '/docs/source/examples/assets/sample.svg'; +$svgPath = $projectRoot . '/docs/examples/assets/sample.svg'; if (!is_dir($outputDir)) { mkdir($outputDir, 0777, true); diff --git a/docs/source/guides/getting-started.md b/docs/guides/getting-started.md similarity index 100% rename from docs/source/guides/getting-started.md rename to docs/guides/getting-started.md diff --git a/docs/index.md b/docs/index.md index 1195ead..a30198b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -7,9 +7,9 @@ ## Start here -- [Getting started](source/guides/getting-started.md) -- [Examples](source/reference/examples.md) -- [Visible signature use case](source/use-cases/visible-signatures.md) +- [Getting started](guides/getting-started.md) +- [Examples](reference/examples.md) +- [Visible signature use case](use-cases/visible-signatures.md) ## Quick example @@ -26,4 +26,4 @@ $result = $compiler->compile(new CompileRequest( )); ``` -For executable samples, see the [examples page](source/reference/examples.md). +For executable samples, see the [examples page](reference/examples.md). diff --git a/docs/source/reference/examples.md b/docs/reference/examples.md similarity index 91% rename from docs/source/reference/examples.md rename to docs/reference/examples.md index 4740a7b..2321388 100644 --- a/docs/source/reference/examples.md +++ b/docs/reference/examples.md @@ -3,7 +3,7 @@ # Examples -The examples are executable PHP files under `docs/source/examples/`. +The examples are executable PHP files under `docs/examples/`. Each one is covered by `tests/Documentation/ExamplesTest.php`. ## Included examples diff --git a/docs/source/README.md b/docs/source/README.md deleted file mode 100644 index ae3be7b..0000000 --- a/docs/source/README.md +++ /dev/null @@ -1,10 +0,0 @@ - - - -# Documentation sources - -Hand-written docs live here. - -## Entry point - -- `index.md` — documentation home diff --git a/docs/source/use-cases/visible-signatures.md b/docs/use-cases/visible-signatures.md similarity index 100% rename from docs/source/use-cases/visible-signatures.md rename to docs/use-cases/visible-signatures.md diff --git a/mkdocs.yml b/mkdocs.yml index 64c9e83..dde31c4 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -23,9 +23,9 @@ theme: nav: - Home: index.md - - Getting started: source/guides/getting-started.md - - Examples: source/reference/examples.md - - Visible signature use case: source/use-cases/visible-signatures.md + - Getting started: guides/getting-started.md + - Examples: reference/examples.md + - Visible signature use case: use-cases/visible-signatures.md markdown_extensions: - pymdownx.highlight: diff --git a/tests/Documentation/ExamplesTest.php b/tests/Documentation/ExamplesTest.php index d8b630d..2f8fae0 100644 --- a/tests/Documentation/ExamplesTest.php +++ b/tests/Documentation/ExamplesTest.php @@ -41,7 +41,7 @@ public static function exampleProvider(): iterable public function testExampleScriptsExecuteAndGenerateExpectedArtifacts(string $exampleFile): void { $projectRoot = dirname(__DIR__, 2); - $examplePath = $projectRoot . '/docs/source/examples/' . $exampleFile; + $examplePath = $projectRoot . '/docs/examples/' . $exampleFile; self::assertFileExists($examplePath); $result = require $examplePath; @@ -87,7 +87,7 @@ public function testExampleScriptsExecuteAndGenerateExpectedArtifacts(string $ex public function testEveryPhpExampleFileIsCoveredByTheProvider(): void { $projectRoot = dirname(__DIR__, 2); - $allPhpExamples = glob($projectRoot . '/docs/source/examples/*.php'); + $allPhpExamples = glob($projectRoot . '/docs/examples/*.php'); self::assertIsArray($allPhpExamples); $actual = array_map(static fn (string $path): string => basename($path), $allPhpExamples); From 305aa5d8cb53878cb0d4f27b2b14e73a39dd28fc Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Mon, 1 Jun 2026 12:37:41 -0300 Subject: [PATCH 19/31] docs: move mkdocs config to docs/ and deploy to gh-pages branch Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .github/workflows/docs.yml | 30 +++++-------------- mkdocs.yml => docs/mkdocs.yml | 4 +-- .../requirements-docs.txt | 0 3 files changed, 9 insertions(+), 25 deletions(-) rename mkdocs.yml => docs/mkdocs.yml (95%) rename requirements-docs.txt => docs/requirements-docs.txt (100%) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 6d8fdff..37706a6 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -9,20 +9,18 @@ on: - main paths: - 'docs/**' - - 'mkdocs.yml' - - 'requirements-docs.txt' + - 'docs/mkdocs.yml' + - 'docs/requirements-docs.txt' permissions: - contents: read - pages: write - id-token: write + contents: write concurrency: group: pages cancel-in-progress: true jobs: - build: + deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -32,21 +30,7 @@ jobs: python-version: '3.12' - name: Install docs dependencies - run: pip install -r requirements-docs.txt - - - name: Build docs - run: mkdocs build --strict - - - uses: actions/upload-pages-artifact@v3 - with: - path: docs/site + run: pip install -r docs/requirements-docs.txt - deploy: - needs: build - runs-on: ubuntu-latest - environment: - name: github-pages - url: ${{ steps.deployment.outputs.page_url }} - steps: - - id: deployment - uses: actions/deploy-pages@v4 + - name: Deploy to gh-pages + run: mkdocs gh-deploy --force --config-file docs/mkdocs.yml diff --git a/mkdocs.yml b/docs/mkdocs.yml similarity index 95% rename from mkdocs.yml rename to docs/mkdocs.yml index dde31c4..0bf2a0f 100644 --- a/mkdocs.yml +++ b/docs/mkdocs.yml @@ -8,8 +8,8 @@ repo_url: https://github.com/LibreSign/xobject-template repo_name: LibreSign/xobject-template edit_uri: edit/main/docs/ -docs_dir: docs -site_dir: docs/site +docs_dir: . +site_dir: site theme: name: material diff --git a/requirements-docs.txt b/docs/requirements-docs.txt similarity index 100% rename from requirements-docs.txt rename to docs/requirements-docs.txt From 5e43208e3b2841fccb1c658e2b7949ea6b8c3be2 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Mon, 1 Jun 2026 12:41:15 -0300 Subject: [PATCH 20/31] chore: remove stale /site/ entry from .gitignore Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .gitignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitignore b/.gitignore index 0d25515..c489f4d 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,6 @@ /vendor-bin/**/composer.lock /composer.lock /build/ -/site/ /docs/site/ /.phpunit.result.cache /.infection.cache From 3be90d06efaa2a5e1efcb349bcf468b998d8af85 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Mon, 1 Jun 2026 12:43:00 -0300 Subject: [PATCH 21/31] docs: fix site_dir outside docs_dir and remove conflicting README Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .gitignore | 1 - docs/README.md | 6 ------ docs/mkdocs.yml | 2 +- 3 files changed, 1 insertion(+), 8 deletions(-) delete mode 100644 docs/README.md diff --git a/.gitignore b/.gitignore index c489f4d..29c30da 100644 --- a/.gitignore +++ b/.gitignore @@ -6,7 +6,6 @@ /vendor-bin/**/composer.lock /composer.lock /build/ -/docs/site/ /.phpunit.result.cache /.infection.cache /.idea/ diff --git a/docs/README.md b/docs/README.md deleted file mode 100644 index 9179016..0000000 --- a/docs/README.md +++ /dev/null @@ -1,6 +0,0 @@ - - - -# Documentation folder layout - -This folder is intentionally split to isolate source content from generated output. diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 0bf2a0f..96090eb 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -9,7 +9,7 @@ repo_name: LibreSign/xobject-template edit_uri: edit/main/docs/ docs_dir: . -site_dir: site +site_dir: ../build/docs-site theme: name: material From 60aafd21f61168666907fb721fbc460fae1fbdc3 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Mon, 1 Jun 2026 12:50:25 -0300 Subject: [PATCH 22/31] docs: enable pygments syntax highlighting Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- docs/mkdocs.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 96090eb..4931ff6 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -30,5 +30,7 @@ nav: markdown_extensions: - pymdownx.highlight: anchor_linenums: true + use_pygments: true + pygments_lang_class: true - pymdownx.superfences - pymdownx.inlinehilite From 87805e33f639e7cd6b3c6ac864de6d9d1587acff Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Mon, 1 Jun 2026 12:52:03 -0300 Subject: [PATCH 23/31] docs: move mkdocs config back to project root Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .github/workflows/docs.yml | 8 ++++---- docs/mkdocs.yml => mkdocs.yml | 4 ++-- docs/requirements-docs.txt => requirements-docs.txt | 0 3 files changed, 6 insertions(+), 6 deletions(-) rename docs/mkdocs.yml => mkdocs.yml (95%) rename docs/requirements-docs.txt => requirements-docs.txt (100%) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 37706a6..d92eddf 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -9,8 +9,8 @@ on: - main paths: - 'docs/**' - - 'docs/mkdocs.yml' - - 'docs/requirements-docs.txt' + - 'mkdocs.yml' + - 'requirements-docs.txt' permissions: contents: write @@ -30,7 +30,7 @@ jobs: python-version: '3.12' - name: Install docs dependencies - run: pip install -r docs/requirements-docs.txt + run: pip install -r requirements-docs.txt - name: Deploy to gh-pages - run: mkdocs gh-deploy --force --config-file docs/mkdocs.yml + run: mkdocs gh-deploy --force diff --git a/docs/mkdocs.yml b/mkdocs.yml similarity index 95% rename from docs/mkdocs.yml rename to mkdocs.yml index 4931ff6..66139c5 100644 --- a/docs/mkdocs.yml +++ b/mkdocs.yml @@ -8,8 +8,8 @@ repo_url: https://github.com/LibreSign/xobject-template repo_name: LibreSign/xobject-template edit_uri: edit/main/docs/ -docs_dir: . -site_dir: ../build/docs-site +docs_dir: docs +site_dir: build/docs-site theme: name: material diff --git a/docs/requirements-docs.txt b/requirements-docs.txt similarity index 100% rename from docs/requirements-docs.txt rename to requirements-docs.txt From 51a0439f6d0c7bcbbec3616312837d181e478d92 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Mon, 1 Jun 2026 12:56:33 -0300 Subject: [PATCH 24/31] docs: reorganize to docs/source/ structure for cleaner separation Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .github/workflows/docs.yml | 7 ++-- docs/mkdocs.yml | 36 +++++++++++++++++++ .../requirements-docs.txt | 0 docs/{ => source}/examples/assets/sample.svg | 0 .../examples/assets/tiny-jpeg.base64 | 0 .../examples/assets/tiny-png.base64 | 0 docs/{ => source}/examples/basic-template.php | 4 +-- docs/{ => source}/examples/images.php | 6 ++-- docs/{ => source}/examples/interpolation.php | 4 +-- docs/{ => source}/examples/placement.php | 4 +-- docs/{ => source}/examples/preview-pdf.php | 4 +-- docs/{ => source}/examples/svg.php | 6 ++-- docs/{ => source}/guides/getting-started.md | 0 docs/{ => source}/index.md | 0 docs/{ => source}/reference/examples.md | 2 +- .../use-cases/visible-signatures.md | 0 tests/Documentation/ExamplesTest.php | 4 +-- 17 files changed, 56 insertions(+), 21 deletions(-) create mode 100644 docs/mkdocs.yml rename requirements-docs.txt => docs/requirements-docs.txt (100%) rename docs/{ => source}/examples/assets/sample.svg (100%) rename docs/{ => source}/examples/assets/tiny-jpeg.base64 (100%) rename docs/{ => source}/examples/assets/tiny-png.base64 (100%) rename docs/{ => source}/examples/basic-template.php (93%) rename docs/{ => source}/examples/images.php (93%) rename docs/{ => source}/examples/interpolation.php (93%) rename docs/{ => source}/examples/placement.php (93%) rename docs/{ => source}/examples/preview-pdf.php (92%) rename docs/{ => source}/examples/svg.php (90%) rename docs/{ => source}/guides/getting-started.md (100%) rename docs/{ => source}/index.md (100%) rename docs/{ => source}/reference/examples.md (91%) rename docs/{ => source}/use-cases/visible-signatures.md (100%) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index d92eddf..f8a5bf1 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -9,8 +9,7 @@ on: - main paths: - 'docs/**' - - 'mkdocs.yml' - - 'requirements-docs.txt' + - '.github/workflows/docs.yml' permissions: contents: write @@ -30,7 +29,7 @@ jobs: python-version: '3.12' - name: Install docs dependencies - run: pip install -r requirements-docs.txt + run: pip install -r docs/requirements-docs.txt - name: Deploy to gh-pages - run: mkdocs gh-deploy --force + run: mkdocs gh-deploy --force --config-file docs/mkdocs.yml diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml new file mode 100644 index 0000000..b39cc79 --- /dev/null +++ b/docs/mkdocs.yml @@ -0,0 +1,36 @@ +# SPDX-FileCopyrightText: 2026 LibreSign +# SPDX-License-Identifier: AGPL-3.0-or-later + +site_name: xobject-template +site_description: Minimal HTML+CSS to reusable PDF Form XObject compiler. +site_url: https://libresign.github.io/xobject-template/ +repo_url: https://github.com/LibreSign/xobject-template +repo_name: LibreSign/xobject-template +edit_uri: edit/main/docs/source/ + +docs_dir: source +site_dir: ../../build/docs-site + +theme: + name: material + palette: + scheme: default + primary: indigo + features: + - navigation.instant + - navigation.top + - content.code.copy + +nav: + - Home: index.md + - Getting started: guides/getting-started.md + - Examples: reference/examples.md + - Visible signature use case: use-cases/visible-signatures.md + +markdown_extensions: + - pymdownx.highlight: + anchor_linenums: true + use_pygments: true + pygments_lang_class: true + - pymdownx.superfences + - pymdownx.inlinehilite diff --git a/requirements-docs.txt b/docs/requirements-docs.txt similarity index 100% rename from requirements-docs.txt rename to docs/requirements-docs.txt diff --git a/docs/examples/assets/sample.svg b/docs/source/examples/assets/sample.svg similarity index 100% rename from docs/examples/assets/sample.svg rename to docs/source/examples/assets/sample.svg diff --git a/docs/examples/assets/tiny-jpeg.base64 b/docs/source/examples/assets/tiny-jpeg.base64 similarity index 100% rename from docs/examples/assets/tiny-jpeg.base64 rename to docs/source/examples/assets/tiny-jpeg.base64 diff --git a/docs/examples/assets/tiny-png.base64 b/docs/source/examples/assets/tiny-png.base64 similarity index 100% rename from docs/examples/assets/tiny-png.base64 rename to docs/source/examples/assets/tiny-png.base64 diff --git a/docs/examples/basic-template.php b/docs/source/examples/basic-template.php similarity index 93% rename from docs/examples/basic-template.php rename to docs/source/examples/basic-template.php index e317945..6f19f74 100644 --- a/docs/examples/basic-template.php +++ b/docs/source/examples/basic-template.php @@ -8,9 +8,9 @@ use LibreSign\XObjectTemplate\Dto\CompileRequest; use LibreSign\XObjectTemplate\XObjectTemplateCompiler; -require dirname(__DIR__, 2) . '/vendor/autoload.php'; +require dirname(__DIR__, 3) . '/vendor/autoload.php'; -$projectRoot = dirname(__DIR__, 2); +$projectRoot = dirname(__DIR__, 3); $outputDir = $projectRoot . '/build/examples'; if (!is_dir($outputDir)) { mkdir($outputDir, 0777, true); diff --git a/docs/examples/images.php b/docs/source/examples/images.php similarity index 93% rename from docs/examples/images.php rename to docs/source/examples/images.php index 4f21abc..dd7cf12 100644 --- a/docs/examples/images.php +++ b/docs/source/examples/images.php @@ -9,10 +9,10 @@ use LibreSign\XObjectTemplate\Pdf\SinglePagePdfExporter; use LibreSign\XObjectTemplate\XObjectTemplateCompiler; -require dirname(__DIR__, 2) . '/vendor/autoload.php'; +require dirname(__DIR__, 3) . '/vendor/autoload.php'; -$projectRoot = dirname(__DIR__, 2); -$assetDir = $projectRoot . '/docs/examples/assets'; +$projectRoot = dirname(__DIR__, 3); +$assetDir = $projectRoot . '/docs/source/examples/assets'; $outputDir = $projectRoot . '/build/examples'; $outputAssetDir = $outputDir . '/assets'; diff --git a/docs/examples/interpolation.php b/docs/source/examples/interpolation.php similarity index 93% rename from docs/examples/interpolation.php rename to docs/source/examples/interpolation.php index 77b23d3..eef92eb 100644 --- a/docs/examples/interpolation.php +++ b/docs/source/examples/interpolation.php @@ -8,9 +8,9 @@ use LibreSign\XObjectTemplate\Dto\CompileRequest; use LibreSign\XObjectTemplate\XObjectTemplateCompiler; -require dirname(__DIR__, 2) . '/vendor/autoload.php'; +require dirname(__DIR__, 3) . '/vendor/autoload.php'; -$projectRoot = dirname(__DIR__, 2); +$projectRoot = dirname(__DIR__, 3); $outputDir = $projectRoot . '/build/examples'; if (!is_dir($outputDir)) { mkdir($outputDir, 0777, true); diff --git a/docs/examples/placement.php b/docs/source/examples/placement.php similarity index 93% rename from docs/examples/placement.php rename to docs/source/examples/placement.php index fbb9cd4..8613911 100644 --- a/docs/examples/placement.php +++ b/docs/source/examples/placement.php @@ -9,9 +9,9 @@ use LibreSign\XObjectTemplate\Integration\XObjectPlacementCalculator; use LibreSign\XObjectTemplate\XObjectTemplateCompiler; -require dirname(__DIR__, 2) . '/vendor/autoload.php'; +require dirname(__DIR__, 3) . '/vendor/autoload.php'; -$projectRoot = dirname(__DIR__, 2); +$projectRoot = dirname(__DIR__, 3); $outputDir = $projectRoot . '/build/examples'; if (!is_dir($outputDir)) { mkdir($outputDir, 0777, true); diff --git a/docs/examples/preview-pdf.php b/docs/source/examples/preview-pdf.php similarity index 92% rename from docs/examples/preview-pdf.php rename to docs/source/examples/preview-pdf.php index d14ad03..25f7f17 100644 --- a/docs/examples/preview-pdf.php +++ b/docs/source/examples/preview-pdf.php @@ -9,9 +9,9 @@ use LibreSign\XObjectTemplate\Pdf\SinglePagePdfExporter; use LibreSign\XObjectTemplate\XObjectTemplateCompiler; -require dirname(__DIR__, 2) . '/vendor/autoload.php'; +require dirname(__DIR__, 3) . '/vendor/autoload.php'; -$projectRoot = dirname(__DIR__, 2); +$projectRoot = dirname(__DIR__, 3); $outputDir = $projectRoot . '/build/examples'; if (!is_dir($outputDir)) { mkdir($outputDir, 0777, true); diff --git a/docs/examples/svg.php b/docs/source/examples/svg.php similarity index 90% rename from docs/examples/svg.php rename to docs/source/examples/svg.php index 36d5561..1801994 100644 --- a/docs/examples/svg.php +++ b/docs/source/examples/svg.php @@ -9,11 +9,11 @@ use LibreSign\XObjectTemplate\Pdf\SinglePagePdfExporter; use LibreSign\XObjectTemplate\XObjectTemplateCompiler; -require dirname(__DIR__, 2) . '/vendor/autoload.php'; +require dirname(__DIR__, 3) . '/vendor/autoload.php'; -$projectRoot = dirname(__DIR__, 2); +$projectRoot = dirname(__DIR__, 3); $outputDir = $projectRoot . '/build/examples'; -$svgPath = $projectRoot . '/docs/examples/assets/sample.svg'; +$svgPath = $projectRoot . '/docs/source/examples/assets/sample.svg'; if (!is_dir($outputDir)) { mkdir($outputDir, 0777, true); diff --git a/docs/guides/getting-started.md b/docs/source/guides/getting-started.md similarity index 100% rename from docs/guides/getting-started.md rename to docs/source/guides/getting-started.md diff --git a/docs/index.md b/docs/source/index.md similarity index 100% rename from docs/index.md rename to docs/source/index.md diff --git a/docs/reference/examples.md b/docs/source/reference/examples.md similarity index 91% rename from docs/reference/examples.md rename to docs/source/reference/examples.md index 2321388..4740a7b 100644 --- a/docs/reference/examples.md +++ b/docs/source/reference/examples.md @@ -3,7 +3,7 @@ # Examples -The examples are executable PHP files under `docs/examples/`. +The examples are executable PHP files under `docs/source/examples/`. Each one is covered by `tests/Documentation/ExamplesTest.php`. ## Included examples diff --git a/docs/use-cases/visible-signatures.md b/docs/source/use-cases/visible-signatures.md similarity index 100% rename from docs/use-cases/visible-signatures.md rename to docs/source/use-cases/visible-signatures.md diff --git a/tests/Documentation/ExamplesTest.php b/tests/Documentation/ExamplesTest.php index 2f8fae0..d8b630d 100644 --- a/tests/Documentation/ExamplesTest.php +++ b/tests/Documentation/ExamplesTest.php @@ -41,7 +41,7 @@ public static function exampleProvider(): iterable public function testExampleScriptsExecuteAndGenerateExpectedArtifacts(string $exampleFile): void { $projectRoot = dirname(__DIR__, 2); - $examplePath = $projectRoot . '/docs/examples/' . $exampleFile; + $examplePath = $projectRoot . '/docs/source/examples/' . $exampleFile; self::assertFileExists($examplePath); $result = require $examplePath; @@ -87,7 +87,7 @@ public function testExampleScriptsExecuteAndGenerateExpectedArtifacts(string $ex public function testEveryPhpExampleFileIsCoveredByTheProvider(): void { $projectRoot = dirname(__DIR__, 2); - $allPhpExamples = glob($projectRoot . '/docs/examples/*.php'); + $allPhpExamples = glob($projectRoot . '/docs/source/examples/*.php'); self::assertIsArray($allPhpExamples); $actual = array_map(static fn (string $path): string => basename($path), $allPhpExamples); From 69716760b01fdb155741a3ed59ea50f3baa4f1ab Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Mon, 1 Jun 2026 12:57:33 -0300 Subject: [PATCH 25/31] docs: fix site_dir path for docs/ config location Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- docs/mkdocs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index b39cc79..374347b 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -9,7 +9,7 @@ repo_name: LibreSign/xobject-template edit_uri: edit/main/docs/source/ docs_dir: source -site_dir: ../../build/docs-site +site_dir: ../build/docs-site theme: name: material From d7f26b0d5bd0bcb7ea611866b9a336f17dcc141c Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Mon, 1 Jun 2026 14:28:31 -0300 Subject: [PATCH 26/31] docs: refine govbr visible stamp example to match official appearance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Adjust GovBR layout dimensions from 190px to 210px height for better spacing - Fine-tune typography: font sizes, colors (#666666 for labels, #000000 for name) - Improve padding/margins distribution (18px vertical, 20px horizontal) - Increase logo section width proportion (30% → 28%) and metadata width (70% → 72%) - Adjust line-height and margins between text elements for official alignment - Regenerate example artifacts with new dimensions and styling Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .docker/Dockerfile | 8 +- composer.json | 3 +- docker-compose.yml | 2 + docs/mkdocs.yml | 22 +- docs/source/guides/getting-started.md | 8 +- docs/source/index.md | 2 + docs/source/reference/examples.md | 62 ++- mkdocs.yml | 36 -- tests/Documentation/ExamplesTest.php | 101 ----- .../ExamplesOutputScenarioTest.php | 406 ++++++++++++++++++ .../VisibleStampPreviewFactory.php | 318 ++++++++++++++ .../VisibleStampTemplateScenarioTest.php | 377 +++------------- .../XObjectRenderingScenarioTest.php | 12 +- 13 files changed, 893 insertions(+), 464 deletions(-) delete mode 100644 mkdocs.yml delete mode 100644 tests/Documentation/ExamplesTest.php create mode 100644 tests/Integration/ExamplesOutputScenarioTest.php create mode 100644 tests/Integration/VisibleStampPreviewFactory.php diff --git a/.docker/Dockerfile b/.docker/Dockerfile index 3adb176..d80bc9f 100644 --- a/.docker/Dockerfile +++ b/.docker/Dockerfile @@ -7,9 +7,15 @@ ARG UID=1000 ARG GID=1000 RUN apt-get update \ - && apt-get install -y --no-install-recommends git unzip zip bash curl ca-certificates \ + && apt-get install -y --no-install-recommends git unzip zip bash curl ca-certificates python3 python3-pip python3-venv \ && rm -rf /var/lib/apt/lists/* +COPY docs/requirements-docs.txt /tmp/requirements-docs.txt + +RUN python3 -m venv /opt/docs-venv \ + && /opt/docs-venv/bin/pip install --no-cache-dir -r /tmp/requirements-docs.txt \ + && ln -sf /opt/docs-venv/bin/mkdocs /usr/local/bin/mkdocs + RUN pecl install xdebug \ && docker-php-ext-enable xdebug diff --git a/composer.json b/composer.json index 90ae953..f78466f 100644 --- a/composer.json +++ b/composer.json @@ -73,7 +73,8 @@ "sh:security": "shellcheck scripts/*.sh", "test:unit": "vendor-bin/phpunit/vendor/phpunit/phpunit/phpunit --testsuite unit", "test:integration": "vendor-bin/phpunit/vendor/phpunit/phpunit/phpunit --testsuite integration", - "examples:test": "vendor-bin/phpunit/vendor/phpunit/phpunit/phpunit --testsuite documentation --filter ExamplesTest", + "examples:test": "vendor-bin/phpunit/vendor/phpunit/phpunit/phpunit --testsuite integration --filter ExamplesOutputScenarioTest", + "docs:watch": "mkdocs serve -f docs/mkdocs.yml -a 0.0.0.0:8000", "test:coverage": "XDEBUG_MODE=coverage vendor-bin/phpunit/vendor/phpunit/phpunit/phpunit --coverage-text --coverage-clover=build/coverage/clover.xml", "mutation:test": "vendor-bin/mutation/vendor/infection/infection/bin/infection --threads=max", "benchmark:run": "vendor-bin/phpbench/vendor/phpbench/phpbench/bin/phpbench run --config=phpbench.json --report=aggregate", diff --git a/docker-compose.yml b/docker-compose.yml index be31fab..12feccc 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -24,4 +24,6 @@ services: # Optional override (default shown): COMPOSER_CACHE_DIR=${HOME}/.composer - ${COMPOSER_CACHE_DIR:-${HOME}/.composer}:/var/www/.composer/ - ./.profile:/var/www/html/.profile + ports: + - "${DOCS_PORT:-8000}:8000" command: ["bash", "-lc", "composer --version && bash"] diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 374347b..6fe1e00 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -13,13 +13,24 @@ site_dir: ../build/docs-site theme: name: material + logo: https://avatars.githubusercontent.com/u/79158919?v=4 + favicon: https://avatars.githubusercontent.com/u/79158919?v=4 palette: - scheme: default - primary: indigo + - scheme: default + primary: indigo + toggle: + icon: material/weather-night + name: Switch to dark mode + - scheme: slate + primary: indigo + toggle: + icon: material/weather-sunny + name: Switch to light mode features: - navigation.instant - navigation.top - content.code.copy + - content.action.edit nav: - Home: index.md @@ -31,6 +42,11 @@ markdown_extensions: - pymdownx.highlight: anchor_linenums: true use_pygments: true - pygments_lang_class: true + - admonition + - pymdownx.details - pymdownx.superfences - pymdownx.inlinehilite + - pymdownx.snippets: + base_path: + - docs/source + check_paths: true diff --git a/docs/source/guides/getting-started.md b/docs/source/guides/getting-started.md index fe47d2f..ad356e5 100644 --- a/docs/source/guides/getting-started.md +++ b/docs/source/guides/getting-started.md @@ -33,12 +33,18 @@ $result = $compiler->compile(new CompileRequest( ## Validate the examples -Run the documentation test suite to execute every example file and verify the generated artifacts: +Run the integration scenario for examples to execute every example file and verify the generated artifacts: ```bash composer run examples:test ``` +This command also materializes additional reusable integration scenarios under +`build/examples/integration-scenarios/`. + +Visible stamp scenarios (including GovBR-like) are also materialized under +`build/examples/visible-stamp/`. + ## Where to look next - [Examples](../reference/examples.md) diff --git a/docs/source/index.md b/docs/source/index.md index a30198b..9eb989a 100644 --- a/docs/source/index.md +++ b/docs/source/index.md @@ -14,6 +14,8 @@ ## Quick example ```php + - */ - public static function documentedExamples(): array - { - return [ - 'basic-template.php', - 'preview-pdf.php', - 'images.php', - 'svg.php', - 'placement.php', - 'interpolation.php', - ]; - } - - /** - * @return iterable - */ - public static function exampleProvider(): iterable - { - foreach (self::documentedExamples() as $exampleFile) { - yield $exampleFile => ['exampleFile' => $exampleFile]; - } - } - - #[DataProvider('exampleProvider')] - public function testExampleScriptsExecuteAndGenerateExpectedArtifacts(string $exampleFile): void - { - $projectRoot = dirname(__DIR__, 2); - $examplePath = $projectRoot . '/docs/source/examples/' . $exampleFile; - self::assertFileExists($examplePath); - - $result = require $examplePath; - - self::assertIsArray($result); - self::assertArrayHasKey('generated_files', $result); - self::assertArrayHasKey('content_stream', $result); - self::assertArrayHasKey('resources', $result); - self::assertArrayHasKey('bbox', $result); - - self::assertIsString($result['content_stream']); - self::assertNotSame('', trim((string) $result['content_stream'])); - - self::assertIsArray($result['resources']); - self::assertArrayHasKey('Font', $result['resources']); - - self::assertIsArray($result['bbox']); - self::assertCount(4, $result['bbox']); - - /** @var list $generatedFiles */ - $generatedFiles = $result['generated_files']; - self::assertNotSame([], $generatedFiles); - - foreach ($generatedFiles as $generatedFile) { - self::assertFileExists($generatedFile); - $contents = file_get_contents($generatedFile); - self::assertIsString($contents); - self::assertNotSame('', $contents); - - if (str_ends_with($generatedFile, '.pdf')) { - self::assertStringStartsWith('%PDF-', $contents); - self::assertStringContainsString('%%EOF', $contents); - } - } - - if (isset($result['pdf_file']) && is_string($result['pdf_file'])) { - $pdfContents = file_get_contents($result['pdf_file']); - self::assertIsString($pdfContents); - self::assertStringStartsWith('%PDF-', $pdfContents); - } - } - - public function testEveryPhpExampleFileIsCoveredByTheProvider(): void - { - $projectRoot = dirname(__DIR__, 2); - $allPhpExamples = glob($projectRoot . '/docs/source/examples/*.php'); - self::assertIsArray($allPhpExamples); - - $actual = array_map(static fn (string $path): string => basename($path), $allPhpExamples); - sort($actual); - - $expected = self::documentedExamples(); - sort($expected); - - self::assertSame($expected, $actual); - } -} diff --git a/tests/Integration/ExamplesOutputScenarioTest.php b/tests/Integration/ExamplesOutputScenarioTest.php new file mode 100644 index 0000000..f8ba52b --- /dev/null +++ b/tests/Integration/ExamplesOutputScenarioTest.php @@ -0,0 +1,406 @@ + $generatedFiles */ + $generatedFiles = $result['generated_files']; + self::assertNotSame([], $generatedFiles); + + foreach ($generatedFiles as $generatedFile) { + self::assertStringStartsWith($examplesOutputRoot . '/', $generatedFile); + self::assertFileExists($generatedFile); + $contents = file_get_contents($generatedFile); + self::assertIsString($contents); + self::assertNotSame('', $contents); + + if (str_ends_with($generatedFile, '.pdf')) { + self::assertStringStartsWith('%PDF-', $contents); + self::assertStringContainsString('%%EOF', $contents); + } + } + + if (isset($result['pdf_file']) && is_string($result['pdf_file'])) { + self::assertStringStartsWith($examplesOutputRoot . '/', $result['pdf_file']); + $pdfContents = file_get_contents($result['pdf_file']); + self::assertIsString($pdfContents); + self::assertStringStartsWith('%PDF-', $pdfContents); + } + } + + public function testEveryPhpExampleFileIsCoveredByTheProvider(): void + { + $projectRoot = dirname(__DIR__, 2); + $allPhpExamples = glob($projectRoot . '/docs/source/examples/*.php'); + self::assertIsArray($allPhpExamples); + + $actual = array_map(static fn (string $path): string => basename($path), $allPhpExamples); + sort($actual); + + $expected = self::documentedExamples(); + sort($expected); + + self::assertSame($expected, $actual); + } + + /** + * @return list + */ + private static function documentedExamples(): array + { + return [ + 'basic-template.php', + 'preview-pdf.php', + 'images.php', + 'svg.php', + 'placement.php', + 'interpolation.php', + ]; + } + + /** + * @return iterable + */ + public static function exampleProvider(): iterable + { + foreach (self::documentedExamples() as $exampleFile) { + yield $exampleFile => ['exampleFile' => $exampleFile]; + } + } + + #[DataProvider('integrationRenderingScenarioProvider')] + public function testIntegrationRenderingScenariosAlsoGenerateExampleArtifacts( + string $slug, + string $html, + float $width, + float $height, + int $maxStreamLength, + ): void { + $projectRoot = dirname(__DIR__, 2); + $scenarioOutputRoot = $projectRoot . '/build/examples/integration-scenarios'; + + if (!is_dir($scenarioOutputRoot)) { + mkdir($scenarioOutputRoot, 0777, true); + } + + $resolvedHtml = $this->resolveIntegrationScenarioHtmlAssets($html, $scenarioOutputRoot); + + $compiler = new XObjectTemplateCompiler(); + $result = $compiler->compile(new CompileRequest( + html: $resolvedHtml, + width: $width, + height: $height, + )); + + self::assertStringContainsString('BT', $result->contentStream); + self::assertStringContainsString('ET', $result->contentStream); + self::assertLessThan($maxStreamLength, strlen($result->contentStream)); + + $pdf = (new SinglePagePdfExporter())->export($result); + $pdfFile = $scenarioOutputRoot . '/' . $slug . '.pdf'; + file_put_contents($pdfFile, $pdf); + + $jsonFile = $scenarioOutputRoot . '/' . $slug . '.json'; + file_put_contents($jsonFile, json_encode([ + 'slug' => $slug, + 'bbox' => $result->bbox, + 'resources' => $result->resources, + 'content_stream_length' => strlen($result->contentStream), + 'metadata' => $result->metadata, + ], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); + + self::assertFileExists($pdfFile); + self::assertFileExists($jsonFile); + + $pdfContents = file_get_contents($pdfFile); + self::assertIsString($pdfContents); + self::assertStringStartsWith('%PDF-', $pdfContents); + self::assertStringContainsString('%%EOF', $pdfContents); + + $jsonContents = file_get_contents($jsonFile); + self::assertIsString($jsonContents); + self::assertStringContainsString('"content_stream_length"', $jsonContents); + self::assertStringContainsString('"resources"', $jsonContents); + } + + private function resolveIntegrationScenarioHtmlAssets(string $html, string $scenarioOutputRoot): string + { + if (!str_contains($html, '/fixture/example-image.png')) { + return $html; + } + + $assetRoot = $scenarioOutputRoot . '/assets'; + if (!is_dir($assetRoot)) { + mkdir($assetRoot, 0777, true); + } + + $imagePath = $assetRoot . '/example-image.png'; + if (!is_file($imagePath)) { + $contents = PngFixtureFactory::createRgbaPngFromPixelRenderer( + 20, + 20, + static fn (): array => [48, 98, 188, 255], + ); + file_put_contents($imagePath, $contents); + } + + return str_replace('/fixture/example-image.png', $imagePath, $html); + } + + /** + * @return iterable + */ + public static function integrationRenderingScenarioProvider(): iterable + { + yield 'title and status block' => [ + 'slug' => 'title-status-block', + 'html' => '
Prepared for Demo User
' + . '

Document approved

', + 'width' => 260.0, + 'height' => 90.0, + 'maxStreamLength' => 1200, + ]; + + yield 'image with reference text' => [ + 'slug' => 'image-reference-text', + 'html' => '' + . 'ID 42', + 'width' => 260.0, + 'height' => 90.0, + 'maxStreamLength' => 1400, + ]; + + yield 'styled block with alignment and spacing' => [ + 'slug' => 'styled-aligned-spacing', + 'html' => '
Prepared by Styled User
', + 'width' => 260.0, + 'height' => 90.0, + 'maxStreamLength' => 1800, + ]; + } + + #[DataProvider('visibleStampExampleScenarioProvider')] + public function testVisibleStampScenariosAlsoGenerateExampleArtifacts( + string $kind, + string $slug, + ?string $layout, + float $width, + float $height, + int $expectedImageCount, + array $expectedTexts, + ): void { + $projectRoot = dirname(__DIR__, 2); + $outputRoot = $projectRoot . '/build/examples/visible-stamp'; + $factory = new VisibleStampPreviewFactory($projectRoot); + + if ($kind === 'phase_one') { + if (!is_string($layout)) { + throw new \InvalidArgumentException('Phase-one visible stamp scenario requires a layout identifier.'); + } + + ['result' => $result, 'pdf' => $pdf, 'previewPath' => $previewPath] = $factory->compilePhaseOneLayoutPreview( + $slug, + $layout, + $outputRoot, + ); + + self::assertSame($width, $result->resources['XObject']['Im0']['Width']); + self::assertSame($height, $result->resources['XObject']['Im0']['Height']); + } else { + [ + 'result' => $result, + 'pdf' => $pdf, + 'previewPath' => $previewPath, + 'logoPath' => $logoPath, + ] = $factory->compileGovBrLikePreview( + $slug, + $width, + $height, + $outputRoot, + ); + + self::assertSame($logoPath, $result->resources['XObject']['Im0']['Source']); + } + + self::assertSame($expectedImageCount, count($result->resources['XObject'] ?? [])); + self::assertStringStartsWith('%PDF-', $pdf); + self::assertStringContainsString('/Subtype /Form', $pdf); + self::assertStringContainsString('/Im0 Do', $result->contentStream); + self::assertFileExists($previewPath); + self::assertSame($pdf, file_get_contents($previewPath)); + self::assertStringStartsWith($outputRoot . '/', $previewPath); + + foreach ($expectedTexts as $expectedText) { + self::assertStringContainsString($expectedText, $result->contentStream); + self::assertStringContainsString($expectedText, $pdf); + } + } + + /** + * @return iterable + * }> + */ + public static function visibleStampExampleScenarioProvider(): iterable + { + foreach (self::visibleStampPhaseOneLayouts() as $name => $scenario) { + yield 'phase one: ' . $name => [ + 'kind' => 'phase_one', + 'slug' => $scenario['slug'], + 'layout' => $scenario['layout'], + 'width' => VisibleStampPreviewFactory::DEFAULT_PREVIEW_WIDTH, + 'height' => VisibleStampPreviewFactory::DEFAULT_PREVIEW_HEIGHT, + 'expectedImageCount' => $scenario['expectedImageCount'], + 'expectedTexts' => $scenario['expectedTexts'], + ]; + } + + $govBr = self::govBrLikeScenario(); + yield 'govbr-like' => [ + 'kind' => 'govbr', + 'slug' => $govBr['slug'], + 'layout' => null, + 'width' => $govBr['width'], + 'height' => $govBr['height'], + 'expectedImageCount' => $govBr['expectedImageCount'], + 'expectedTexts' => $govBr['expectedTexts'], + ]; + } + + /** + * @return iterable + * }> + */ + private static function visibleStampPhaseOneLayouts(): iterable + { + yield 'signature and metadata at right' => [ + 'slug' => 'signature-and-metadata-right', + 'layout' => 'signature_and_metadata_right', + 'expectedImageCount' => 2, + 'expectedTexts' => [ + 'Signed with LibreSign', + 'admin', + 'Issuer: Preview Issuer', + 'Date: 2026-05-28T16:40:21+00:00', + ], + ]; + + yield 'label and metadata at right' => [ + 'slug' => 'label-and-metadata-right', + 'layout' => 'label_and_metadata_right', + 'expectedImageCount' => 1, + 'expectedTexts' => [ + 'admin', + 'Signed with LibreSign', + 'Issuer: Preview Issuer', + 'Date: 2026-05-28T16:40:21+00:00', + ], + ]; + + yield 'signature centered' => [ + 'slug' => 'signature-centered', + 'layout' => 'signature_centered', + 'expectedImageCount' => 2, + 'expectedTexts' => [], + ]; + + yield 'metadata only at top left' => [ + 'slug' => 'metadata-only-top-left', + 'layout' => 'metadata_only_top_left', + 'expectedImageCount' => 1, + 'expectedTexts' => [ + 'Signed with LibreSign', + 'admin', + 'Issuer: Preview Issuer', + 'Date: 2026-05-28T16:40:21+00:00', + ], + ]; + + yield 'two columns with centered cells' => [ + 'slug' => 'two-columns-centered-cells', + 'layout' => 'two_columns_centered_cells', + 'expectedImageCount' => 2, + 'expectedTexts' => [ + 'Signed with LibreSign', + 'Preview Issuer', + 'Date:', + '2026-05-28T16:40:21+00:00', + ], + ]; + } + + /** + * @return array{ + * slug: string, + * width: float, + * height: float, + * expectedImageCount: int, + * expectedTexts: list + * } + */ + private static function govBrLikeScenario(): array + { + return [ + 'slug' => 'govbr-like-visible-stamp', + 'width' => 760.0, + 'height' => 210.0, + 'expectedImageCount' => 1, + 'expectedTexts' => [ + 'Documento assinado digitalmente', + 'ASSINANTE DE EXEMPLO', + 'Data: 01/01/2026 12:00:00-0300', + 'Verifique em https://verificador.iti.br', + ], + ]; + } +} \ No newline at end of file diff --git a/tests/Integration/VisibleStampPreviewFactory.php b/tests/Integration/VisibleStampPreviewFactory.php new file mode 100644 index 0000000..cc217f7 --- /dev/null +++ b/tests/Integration/VisibleStampPreviewFactory.php @@ -0,0 +1,318 @@ +ensureDirectoryExists($previewRoot); + $this->ensureDirectoryExists($assetRoot); + + $backgroundPath = $this->createBackgroundPreview($assetRoot . '/background-' . $slug . '.png'); + $signaturePath = $this->layoutUsesSignatureImage($layout) + ? $this->createSignaturePreview($assetRoot . '/signature-' . $slug . '.png') + : null; + + return $this->compilePreviewWithSize( + $slug, + $this->buildLayoutHtml($layout, $backgroundPath, $signaturePath), + self::DEFAULT_PREVIEW_WIDTH, + self::DEFAULT_PREVIEW_HEIGHT, + $previewRoot, + ); + } + + /** + * @return array{result: \LibreSign\XObjectTemplate\Dto\CompileResult, pdf: string, previewPath: string, logoPath: string} + */ + public function compileGovBrLikePreview( + string $slug, + float $width, + float $height, + string $previewRoot, + ): array { + $logoPath = $this->projectRoot . '/tests/Fixtures/Pdf/Svg/govbr-logo.svg'; + if (!is_file($logoPath)) { + throw new \InvalidArgumentException(sprintf('GovBR logo fixture was not found at "%s".', $logoPath)); + } + + $preview = $this->compilePreviewWithSize( + $slug, + $this->buildGovBrLikeLayoutHtml($logoPath), + $width, + $height, + $previewRoot, + ); + + return $preview + ['logoPath' => $logoPath]; + } + + /** + * @return array{result: \LibreSign\XObjectTemplate\Dto\CompileResult, pdf: string, previewPath: string} + */ + private function compilePreviewWithSize( + string $slug, + string $html, + float $width, + float $height, + string $previewRoot, + ): array { + $this->ensureDirectoryExists($previewRoot); + $this->removeLegacyPreviewPngs($previewRoot, $slug); + + $compiler = new XObjectTemplateCompiler(); + $result = $compiler->compile(new CompileRequest( + html: $html, + width: $width, + height: $height, + )); + + $pdf = (new SinglePagePdfExporter())->export($result); + $previewPath = $previewRoot . '/' . $slug . '.pdf'; + file_put_contents($previewPath, $pdf); + + return [ + 'result' => $result, + 'pdf' => $pdf, + 'previewPath' => $previewPath, + ]; + } + + private function buildLayoutHtml(string $layout, string $backgroundPath, ?string $signaturePath): string + { + $background = sprintf( + '', + $this->escapeAttribute($backgroundPath), + ); + + return match ($layout) { + 'signature_and_metadata_right' => sprintf( + '
%s' + . '
' + . '' + . '
' + . '
' + . '
Signed with LibreSign
' + . '
admin
' + . '
Issuer: Preview Issuer
' + . '
Date: 2026-05-28T16:40:21+00:00
' + . '
' + . '
', + $background, + $this->requireSignaturePath($signaturePath), + ), + 'label_and_metadata_right' => sprintf( + '
%s' + . '
' + . '
admin
' + . '
' + . '
' + . '
Signed with LibreSign
' + . '
Issuer: Preview Issuer
' + . '
Date: 2026-05-28T16:40:21+00:00
' + . '
' + . '
', + $background, + ), + 'signature_centered' => sprintf( + '
%s' + . '' + . '
', + $background, + $this->requireSignaturePath($signaturePath), + ), + 'metadata_only_top_left' => sprintf( + '
%s' + . '
' + . '
Signed with LibreSign
' + . '
admin
' + . '
Issuer: Preview Issuer
' + . '
Date: 2026-05-28T16:40:21+00:00
' + . '
' + . '
', + $background, + ), + 'two_columns_centered_cells' => sprintf( + '
%s' + . '
' + . '' + . '
' + . '
' + . '
' + . '
Signed with LibreSign
' + . '
Preview Issuer
' + . '
Date: 2026-05-28T16:40:21+00:00
' + . '
' + . '
' + . '
', + $background, + $this->requireSignaturePath($signaturePath), + ), + default => throw new \InvalidArgumentException(sprintf('Unknown visible stamp layout "%s".', $layout)), + }; + } + + private function buildGovBrLikeLayoutHtml(string $logoPath): string + { + return sprintf( + '
' + . '
' + . '' + . '
' + . '
' + . '
Documento assinado digitalmente
' + . '
ASSINANTE DE EXEMPLO
' + . '
Data: 01/01/2026 12:00:00-0300
' + . '
Verifique em https://verificador.iti.br
' + . '
' + . '
', + $this->escapeAttribute($logoPath), + ); + } + + private function createBackgroundPreview(string $path): string + { + if (is_file($path)) { + return $path; + } + + $contents = PngFixtureFactory::createRgbaPngFromPixelRenderer( + (int) self::DEFAULT_PREVIEW_WIDTH, + (int) self::DEFAULT_PREVIEW_HEIGHT, + function (int $x, int $y, int $width, int $height): array { + $background = [245, 247, 250, 255]; + $diagonal = abs(($height * $x) - ($width * $y)); + $inverseDiagonal = abs(($height * ($width - $x)) - ($width * $y)); + $bandStrength = ($diagonal < $width * 10 || $inverseDiagonal < $width * 10) ? 55 : 0; + $ringCenterX = $width * 0.74; + $ringCenterY = $height * 0.5; + $distance = sqrt((($x - $ringCenterX) ** 2) + (($y - $ringCenterY) ** 2)); + $ringStrength = ($distance > $height * 0.18 && $distance < $height * 0.24) ? 35 : 0; + $strength = max($bandStrength, $ringStrength); + + return [ + max(0, $background[0] - $strength), + max(0, $background[1] - $strength), + max(0, $background[2] - $strength), + 255, + ]; + }, + ); + + file_put_contents($path, $contents); + + return $path; + } + + private function createSignaturePreview(string $path): string + { + if (is_file($path)) { + return $path; + } + + $contents = PngFixtureFactory::createRgbaPngFromPixelRenderer( + 640, + 180, + function (int $x, int $y, int $width, int $height): array { + $normalizedX = $width === 1 ? 0.0 : $x / ($width - 1); + $primaryWave = ($height * 0.56) + + sin($normalizedX * 7.1) * ($height * 0.13) + + sin($normalizedX * 14.2) * ($height * 0.035); + $secondaryWave = ($height * 0.4) + + cos($normalizedX * 16.0) * ($height * 0.05); + $underline = ($height * 0.78) + sin($normalizedX * 5.6) * ($height * 0.015); + + $alpha = 0; + if (abs($y - $primaryWave) <= 2.2 && $normalizedX >= 0.08 && $normalizedX <= 0.92) { + $alpha = 255; + } + + if (abs($y - $secondaryWave) <= 1.6 && $normalizedX >= 0.0 && $normalizedX <= 0.18) { + $alpha = max($alpha, 220); + } + + if (abs($y - $underline) <= 1.0 && $normalizedX >= 0.16 && $normalizedX <= 0.9) { + $alpha = max($alpha, 185); + } + + return [20, 28, 40, $alpha]; + }, + ); + + file_put_contents($path, $contents); + + return $path; + } + + private function removeLegacyPreviewPngs(string $previewRoot, string $slug): void + { + $legacyCandidates = [ + $previewRoot . '/' . $slug . '.png', + $previewRoot . '/' . $slug . '-1.png', + ]; + + foreach ($legacyCandidates as $legacyCandidate) { + if (is_file($legacyCandidate)) { + unlink($legacyCandidate); + } + } + } + + private function requireSignaturePath(?string $signaturePath): string + { + if ($signaturePath === null) { + throw new \InvalidArgumentException('This visible stamp layout requires a signature image.'); + } + + return $this->escapeAttribute($signaturePath); + } + + private function ensureDirectoryExists(string $directory): void + { + if (is_dir($directory)) { + return; + } + + mkdir($directory, 0777, true); + } + + private function layoutUsesSignatureImage(string $layout): bool + { + return in_array( + $layout, + ['signature_and_metadata_right', 'signature_centered', 'two_columns_centered_cells'], + true, + ); + } + + private function escapeAttribute(string $value): string + { + return htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); + } +} diff --git a/tests/Integration/VisibleStampTemplateScenarioTest.php b/tests/Integration/VisibleStampTemplateScenarioTest.php index f6f0d1c..12e8bf2 100644 --- a/tests/Integration/VisibleStampTemplateScenarioTest.php +++ b/tests/Integration/VisibleStampTemplateScenarioTest.php @@ -7,19 +7,12 @@ namespace LibreSign\XObjectTemplate\Tests\Integration; -use LibreSign\XObjectTemplate\Dto\CompileRequest; use LibreSign\XObjectTemplate\Dto\CompileResult; -use LibreSign\XObjectTemplate\Pdf\SinglePagePdfExporter; -use LibreSign\XObjectTemplate\Tests\Support\PngFixtureFactory; -use LibreSign\XObjectTemplate\XObjectTemplateCompiler; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; final class VisibleStampTemplateScenarioTest extends TestCase { - private const PREVIEW_WIDTH = 804; - private const PREVIEW_HEIGHT = 230; - #[DataProvider('visibleStampLayoutProvider')] public function testPhaseOneVisibleStampLayoutsCanBeCompiledAndExported( string $slug, @@ -27,19 +20,21 @@ public function testPhaseOneVisibleStampLayoutsCanBeCompiledAndExported( int $expectedImageCount, array $expectedTexts, ): void { - ['assetRoot' => $assetRoot] = $this->ensurePreviewDirectories(); - - $backgroundPath = $this->createBackgroundPreview($assetRoot . '/background-' . $slug . '.png'); - $signaturePath = $this->layoutUsesSignatureImage($layout) - ? $this->createSignaturePreview($assetRoot . '/signature-' . $slug . '.png') - : null; - - ['result' => $result, 'pdf' => $pdf, 'previewPath' => $previewPath] = $this->compilePreview( + $factory = new VisibleStampPreviewFactory(dirname(__DIR__, 2)); + ['result' => $result, 'pdf' => $pdf, 'previewPath' => $previewPath] = $factory->compilePhaseOneLayoutPreview( $slug, - $this->buildLayoutHtml($layout, $backgroundPath, $signaturePath), + $layout, + dirname(__DIR__, 2) . '/build/visible-stamp-previews', ); - $this->assertBasePreviewExport($result, $pdf, $previewPath, $expectedImageCount); + $this->assertBasePreviewExport( + $result, + $pdf, + $previewPath, + $expectedImageCount, + VisibleStampPreviewFactory::DEFAULT_PREVIEW_WIDTH, + VisibleStampPreviewFactory::DEFAULT_PREVIEW_HEIGHT, + ); foreach ($expectedTexts as $expectedText) { self::assertStringContainsString($expectedText, $result->contentStream); @@ -49,32 +44,30 @@ public function testPhaseOneVisibleStampLayoutsCanBeCompiledAndExported( public function testGovBrLikeVisibleStampCanBeCompiledAndExportedUsingSupportedHtmlAndCssOnly(): void { - $logoPath = dirname(__DIR__) . '/Fixtures/Pdf/Svg/govbr-logo.svg'; - self::assertFileExists($logoPath); + $scenario = self::govBrLikeScenario(); + $factory = new VisibleStampPreviewFactory(dirname(__DIR__, 2)); - ['result' => $result, 'pdf' => $pdf, 'previewPath' => $previewPath] = $this->compilePreviewWithSize( - 'govbr-like-visible-stamp', - $this->buildGovBrLikeLayoutHtml($logoPath), - 760.0, - 190.0, + [ + 'result' => $result, + 'pdf' => $pdf, + 'previewPath' => $previewPath, + 'logoPath' => $logoPath, + ] = $factory->compileGovBrLikePreview( + $scenario['slug'], + $scenario['width'], + $scenario['height'], + dirname(__DIR__, 2) . '/build/visible-stamp-previews', ); - self::assertSame(1, count($result->resources['XObject'] ?? [])); - self::assertStringStartsWith("%PDF-1.4\n", $pdf); - self::assertStringContainsString('/Subtype /Form', $pdf); - self::assertStringContainsString('/Im0 Do', $result->contentStream); - self::assertFileExists($previewPath); - self::assertSame($pdf, file_get_contents($previewPath)); + $this->assertBasePreviewExport( + $result, + $pdf, + $previewPath, + $scenario['expectedImageCount'], + ); self::assertSame($logoPath, $result->resources['XObject']['Im0']['Source']); - $expectedTexts = [ - 'Documento assinado digitalmente', - 'ASSINANTE DE EXEMPLO', - 'Data: 01/01/2026 12:00:00-0300', - 'Verifique em https://verificador.iti.br', - ]; - - foreach ($expectedTexts as $expectedText) { + foreach ($scenario['expectedTexts'] as $expectedText) { self::assertStringContainsString($expectedText, $result->contentStream); self::assertStringContainsString($expectedText, $pdf); } @@ -149,164 +142,29 @@ public static function visibleStampLayoutProvider(): iterable ]; } - private function buildLayoutHtml(string $layout, string $backgroundPath, ?string $signaturePath): string - { - $background = sprintf( - '', - $this->escapeAttribute($backgroundPath), - ); - - return match ($layout) { - 'signature_and_metadata_right' => sprintf( - '
%s' - . '
' - . '' - . '
' - . '
' - . '
Signed with LibreSign
' - . '
admin
' - . '
Issuer: Preview Issuer
' - . '
Date: 2026-05-28T16:40:21+00:00
' - . '
' - . '
', - $background, - $this->requireSignaturePath($signaturePath), - ), - 'label_and_metadata_right' => sprintf( - '
%s' - . '
' - . '
admin
' - . '
' - . '
' - . '
Signed with LibreSign
' - . '
Issuer: Preview Issuer
' - . '
Date: 2026-05-28T16:40:21+00:00
' - . '
' - . '
', - $background, - ), - 'signature_centered' => sprintf( - '
%s' - . '' - . '
', - $background, - $this->requireSignaturePath($signaturePath), - ), - 'metadata_only_top_left' => sprintf( - '
%s' - . '
' - . '
Signed with LibreSign
' - . '
admin
' - . '
Issuer: Preview Issuer
' - . '
Date: 2026-05-28T16:40:21+00:00
' - . '
' - . '
', - $background, - ), - 'two_columns_centered_cells' => sprintf( - '
%s' - . '
' - . '' - . '
' - . '
' - . '
' - . '
Signed with LibreSign
' - . '
Preview Issuer
' - . '
Date: 2026-05-28T16:40:21+00:00
' - . '
' - . '
' - . '
', - $background, - $this->requireSignaturePath($signaturePath), - ), - default => throw new \InvalidArgumentException(sprintf('Unknown visible stamp layout "%s".', $layout)), - }; - } - - private function buildGovBrLikeLayoutHtml(string $logoPath): string - { - return sprintf( - '
' - . '
' - . '' - . '
' - . '
' - . '
Documento assinado digitalmente
' - . '
ASSINANTE DE EXEMPLO
' - . '
Data: 01/01/2026 12:00:00-0300
' - . '
Verifique em https://verificador.iti.br
' - . '
' - . '
', - $this->escapeAttribute($logoPath), - ); - } - /** - * @return array{previewRoot: string, assetRoot: string} - */ - private function ensurePreviewDirectories(): array - { - $previewRoot = dirname(__DIR__, 2) . '/build/visible-stamp-previews'; - $assetRoot = $previewRoot . '/assets'; - $this->ensureDirectoryExists($previewRoot); - $this->ensureDirectoryExists($assetRoot); - - return [ - 'previewRoot' => $previewRoot, - 'assetRoot' => $assetRoot, - ]; - } - - /** - * @return array{result: CompileResult, pdf: string, previewPath: string} - */ - private function compilePreview(string $slug, string $html): array - { - return $this->compilePreviewWithSize($slug, $html, (float) self::PREVIEW_WIDTH, (float) self::PREVIEW_HEIGHT); - } - - /** - * @return array{result: CompileResult, pdf: string, previewPath: string} + * @return array{ + * slug: string, + * width: float, + * height: float, + * expectedImageCount: int, + * expectedTexts: list + * } */ - private function compilePreviewWithSize(string $slug, string $html, float $width, float $height): array + private static function govBrLikeScenario(): array { - ['previewRoot' => $previewRoot] = $this->ensurePreviewDirectories(); - $this->removeLegacyPreviewPngs($previewRoot, $slug); - - $compiler = new XObjectTemplateCompiler(); - $result = $compiler->compile(new CompileRequest( - html: $html, - width: $width, - height: $height, - )); - - $pdf = (new SinglePagePdfExporter())->export($result); - $previewPath = $previewRoot . '/' . $slug . '.pdf'; - file_put_contents($previewPath, $pdf); - return [ - 'result' => $result, - 'pdf' => $pdf, - 'previewPath' => $previewPath, - ]; - } - - private function removeLegacyPreviewPngs(string $previewRoot, string $slug): void - { - $legacyCandidates = [ - $previewRoot . '/' . $slug . '.png', - $previewRoot . '/' . $slug . '-1.png', + 'slug' => 'govbr-like-visible-stamp', + 'width' => 760.0, + 'height' => 210.0, + 'expectedImageCount' => 1, + 'expectedTexts' => [ + 'Documento assinado digitalmente', + 'ASSINANTE DE EXEMPLO', + 'Data: 01/01/2026 12:00:00-0300', + 'Verifique em https://verificador.iti.br', + ], ]; - - foreach ($legacyCandidates as $legacyCandidate) { - if (is_file($legacyCandidate)) { - unlink($legacyCandidate); - } - } } private function assertBasePreviewExport( @@ -314,22 +172,28 @@ private function assertBasePreviewExport( string $pdf, string $previewPath, int $expectedImageCount, + ?float $expectedWidth = null, + ?float $expectedHeight = null, ): void { self::assertSame($expectedImageCount, count($result->resources['XObject'] ?? [])); - self::assertSame((float) self::PREVIEW_WIDTH, $result->resources['XObject']['Im0']['Width']); - self::assertSame((float) self::PREVIEW_HEIGHT, $result->resources['XObject']['Im0']['Height']); + + if ($expectedWidth !== null && $expectedHeight !== null) { + self::assertSame($expectedWidth, $result->resources['XObject']['Im0']['Width']); + self::assertSame($expectedHeight, $result->resources['XObject']['Im0']['Height']); + self::assertStringContainsString( + sprintf( + 'q %F 0 0 %F %F %F cm /Im0 Do Q', + $expectedWidth, + $expectedHeight, + 0.0, + 0.0, + ), + $result->contentStream, + ); + } + self::assertStringStartsWith("%PDF-1.4\n", $pdf); self::assertStringContainsString('/Subtype /Form', $pdf); - self::assertStringContainsString( - sprintf( - 'q %F 0 0 %F %F %F cm /Im0 Do Q', - (float) self::PREVIEW_WIDTH, - (float) self::PREVIEW_HEIGHT, - 0.0, - 0.0, - ), - $result->contentStream, - ); self::assertStringContainsString('/Im0 Do', $result->contentStream); self::assertFileExists($previewPath); self::assertSame($pdf, file_get_contents($previewPath)); @@ -338,111 +202,4 @@ private function assertBasePreviewExport( self::assertStringContainsString('/Im1 Do', $result->contentStream); } } - - private function createBackgroundPreview(string $path): string - { - if (is_file($path)) { - return $path; - } - - $contents = PngFixtureFactory::createRgbaPngFromPixelRenderer( - self::PREVIEW_WIDTH, - self::PREVIEW_HEIGHT, - function (int $x, int $y, int $width, int $height): array { - $background = [245, 247, 250, 255]; - $diagonal = abs(($height * $x) - ($width * $y)); - $inverseDiagonal = abs(($height * ($width - $x)) - ($width * $y)); - $bandStrength = ($diagonal < $width * 10 || $inverseDiagonal < $width * 10) ? 55 : 0; - $ringCenterX = $width * 0.74; - $ringCenterY = $height * 0.5; - $distance = sqrt((($x - $ringCenterX) ** 2) + (($y - $ringCenterY) ** 2)); - $ringStrength = ($distance > $height * 0.18 && $distance < $height * 0.24) ? 35 : 0; - $strength = max($bandStrength, $ringStrength); - - return [ - max(0, $background[0] - $strength), - max(0, $background[1] - $strength), - max(0, $background[2] - $strength), - 255, - ]; - }, - ); - - file_put_contents($path, $contents); - - return $path; - } - - private function createSignaturePreview(string $path): string - { - if (is_file($path)) { - return $path; - } - - $contents = PngFixtureFactory::createRgbaPngFromPixelRenderer( - 640, - 180, - function (int $x, int $y, int $width, int $height): array { - $normalizedX = $width === 1 ? 0.0 : $x / ($width - 1); - $primaryWave = ($height * 0.56) - + sin($normalizedX * 7.1) * ($height * 0.13) - + sin($normalizedX * 14.2) * ($height * 0.035); - $secondaryWave = ($height * 0.4) - + cos($normalizedX * 16.0) * ($height * 0.05); - $underline = ($height * 0.78) + sin($normalizedX * 5.6) * ($height * 0.015); - - $alpha = 0; - if (abs($y - $primaryWave) <= 2.2 && $normalizedX >= 0.08 && $normalizedX <= 0.92) { - $alpha = 255; - } - - if (abs($y - $secondaryWave) <= 1.6 && $normalizedX >= 0.0 && $normalizedX <= 0.18) { - $alpha = max($alpha, 220); - } - - if (abs($y - $underline) <= 1.0 && $normalizedX >= 0.16 && $normalizedX <= 0.9) { - $alpha = max($alpha, 185); - } - - return [20, 28, 40, $alpha]; - }, - ); - - file_put_contents($path, $contents); - - return $path; - } - - - private function requireSignaturePath(?string $signaturePath): string - { - if ($signaturePath === null) { - throw new \InvalidArgumentException('This visible stamp layout requires a signature image.'); - } - - return $this->escapeAttribute($signaturePath); - } - - private function ensureDirectoryExists(string $directory): void - { - if (is_dir($directory)) { - return; - } - - mkdir($directory, 0777, true); - } - - private function layoutUsesSignatureImage(string $layout): bool - { - return in_array( - $layout, - ['signature_and_metadata_right', 'signature_centered', 'two_columns_centered_cells'], - true, - ); - } - - private function escapeAttribute(string $value): string - { - return htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); - } } diff --git a/tests/Integration/XObjectRenderingScenarioTest.php b/tests/Integration/XObjectRenderingScenarioTest.php index e2ddd7c..885d49f 100644 --- a/tests/Integration/XObjectRenderingScenarioTest.php +++ b/tests/Integration/XObjectRenderingScenarioTest.php @@ -15,10 +15,10 @@ final class XObjectRenderingScenarioTest extends TestCase { #[DataProvider('renderingScenarioProvider')] - public function testReusableXObjectScenarios(string $html, int $maxStreamLength): void + public function testReusableXObjectScenarios(string $html, float $width, float $height, int $maxStreamLength): void { $compiler = new XObjectTemplateCompiler(); - $result = $compiler->compile(new CompileRequest(html: $html, width: 260.0, height: 90.0)); + $result = $compiler->compile(new CompileRequest(html: $html, width: $width, height: $height)); self::assertStringContainsString('BT', $result->contentStream); self::assertStringContainsString('ET', $result->contentStream); @@ -28,25 +28,31 @@ public function testReusableXObjectScenarios(string $html, int $maxStreamLength) } /** - * @return iterable + * @return iterable */ public static function renderingScenarioProvider(): iterable { yield 'title and status block' => [ 'html' => '
Prepared for Demo User
' . '

Document approved

', + 'width' => 260.0, + 'height' => 90.0, 'maxStreamLength' => 1200, ]; yield 'image with reference text' => [ 'html' => '' . 'ID 42', + 'width' => 260.0, + 'height' => 90.0, 'maxStreamLength' => 1400, ]; yield 'styled block with alignment and spacing' => [ 'html' => '
Prepared by Styled User
', + 'width' => 260.0, + 'height' => 90.0, 'maxStreamLength' => 1800, ]; } From a9923d66a8bc9d947b1921c241e11e81cd036dfb Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Mon, 1 Jun 2026 17:01:14 -0300 Subject: [PATCH 27/31] test: simplify visible stamp integration scenarios Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .../ExamplesOutputScenarioTest.php | 187 +------- .../VisibleStampPreviewFactory.php | 318 -------------- .../VisibleStampTemplateScenarioTest.php | 398 +++++++++++++++--- 3 files changed, 331 insertions(+), 572 deletions(-) delete mode 100644 tests/Integration/VisibleStampPreviewFactory.php diff --git a/tests/Integration/ExamplesOutputScenarioTest.php b/tests/Integration/ExamplesOutputScenarioTest.php index f8ba52b..9334686 100644 --- a/tests/Integration/ExamplesOutputScenarioTest.php +++ b/tests/Integration/ExamplesOutputScenarioTest.php @@ -218,189 +218,4 @@ public static function integrationRenderingScenarioProvider(): iterable 'maxStreamLength' => 1800, ]; } - - #[DataProvider('visibleStampExampleScenarioProvider')] - public function testVisibleStampScenariosAlsoGenerateExampleArtifacts( - string $kind, - string $slug, - ?string $layout, - float $width, - float $height, - int $expectedImageCount, - array $expectedTexts, - ): void { - $projectRoot = dirname(__DIR__, 2); - $outputRoot = $projectRoot . '/build/examples/visible-stamp'; - $factory = new VisibleStampPreviewFactory($projectRoot); - - if ($kind === 'phase_one') { - if (!is_string($layout)) { - throw new \InvalidArgumentException('Phase-one visible stamp scenario requires a layout identifier.'); - } - - ['result' => $result, 'pdf' => $pdf, 'previewPath' => $previewPath] = $factory->compilePhaseOneLayoutPreview( - $slug, - $layout, - $outputRoot, - ); - - self::assertSame($width, $result->resources['XObject']['Im0']['Width']); - self::assertSame($height, $result->resources['XObject']['Im0']['Height']); - } else { - [ - 'result' => $result, - 'pdf' => $pdf, - 'previewPath' => $previewPath, - 'logoPath' => $logoPath, - ] = $factory->compileGovBrLikePreview( - $slug, - $width, - $height, - $outputRoot, - ); - - self::assertSame($logoPath, $result->resources['XObject']['Im0']['Source']); - } - - self::assertSame($expectedImageCount, count($result->resources['XObject'] ?? [])); - self::assertStringStartsWith('%PDF-', $pdf); - self::assertStringContainsString('/Subtype /Form', $pdf); - self::assertStringContainsString('/Im0 Do', $result->contentStream); - self::assertFileExists($previewPath); - self::assertSame($pdf, file_get_contents($previewPath)); - self::assertStringStartsWith($outputRoot . '/', $previewPath); - - foreach ($expectedTexts as $expectedText) { - self::assertStringContainsString($expectedText, $result->contentStream); - self::assertStringContainsString($expectedText, $pdf); - } - } - - /** - * @return iterable - * }> - */ - public static function visibleStampExampleScenarioProvider(): iterable - { - foreach (self::visibleStampPhaseOneLayouts() as $name => $scenario) { - yield 'phase one: ' . $name => [ - 'kind' => 'phase_one', - 'slug' => $scenario['slug'], - 'layout' => $scenario['layout'], - 'width' => VisibleStampPreviewFactory::DEFAULT_PREVIEW_WIDTH, - 'height' => VisibleStampPreviewFactory::DEFAULT_PREVIEW_HEIGHT, - 'expectedImageCount' => $scenario['expectedImageCount'], - 'expectedTexts' => $scenario['expectedTexts'], - ]; - } - - $govBr = self::govBrLikeScenario(); - yield 'govbr-like' => [ - 'kind' => 'govbr', - 'slug' => $govBr['slug'], - 'layout' => null, - 'width' => $govBr['width'], - 'height' => $govBr['height'], - 'expectedImageCount' => $govBr['expectedImageCount'], - 'expectedTexts' => $govBr['expectedTexts'], - ]; - } - - /** - * @return iterable - * }> - */ - private static function visibleStampPhaseOneLayouts(): iterable - { - yield 'signature and metadata at right' => [ - 'slug' => 'signature-and-metadata-right', - 'layout' => 'signature_and_metadata_right', - 'expectedImageCount' => 2, - 'expectedTexts' => [ - 'Signed with LibreSign', - 'admin', - 'Issuer: Preview Issuer', - 'Date: 2026-05-28T16:40:21+00:00', - ], - ]; - - yield 'label and metadata at right' => [ - 'slug' => 'label-and-metadata-right', - 'layout' => 'label_and_metadata_right', - 'expectedImageCount' => 1, - 'expectedTexts' => [ - 'admin', - 'Signed with LibreSign', - 'Issuer: Preview Issuer', - 'Date: 2026-05-28T16:40:21+00:00', - ], - ]; - - yield 'signature centered' => [ - 'slug' => 'signature-centered', - 'layout' => 'signature_centered', - 'expectedImageCount' => 2, - 'expectedTexts' => [], - ]; - - yield 'metadata only at top left' => [ - 'slug' => 'metadata-only-top-left', - 'layout' => 'metadata_only_top_left', - 'expectedImageCount' => 1, - 'expectedTexts' => [ - 'Signed with LibreSign', - 'admin', - 'Issuer: Preview Issuer', - 'Date: 2026-05-28T16:40:21+00:00', - ], - ]; - - yield 'two columns with centered cells' => [ - 'slug' => 'two-columns-centered-cells', - 'layout' => 'two_columns_centered_cells', - 'expectedImageCount' => 2, - 'expectedTexts' => [ - 'Signed with LibreSign', - 'Preview Issuer', - 'Date:', - '2026-05-28T16:40:21+00:00', - ], - ]; - } - - /** - * @return array{ - * slug: string, - * width: float, - * height: float, - * expectedImageCount: int, - * expectedTexts: list - * } - */ - private static function govBrLikeScenario(): array - { - return [ - 'slug' => 'govbr-like-visible-stamp', - 'width' => 760.0, - 'height' => 210.0, - 'expectedImageCount' => 1, - 'expectedTexts' => [ - 'Documento assinado digitalmente', - 'ASSINANTE DE EXEMPLO', - 'Data: 01/01/2026 12:00:00-0300', - 'Verifique em https://verificador.iti.br', - ], - ]; - } -} \ No newline at end of file +} diff --git a/tests/Integration/VisibleStampPreviewFactory.php b/tests/Integration/VisibleStampPreviewFactory.php deleted file mode 100644 index cc217f7..0000000 --- a/tests/Integration/VisibleStampPreviewFactory.php +++ /dev/null @@ -1,318 +0,0 @@ -ensureDirectoryExists($previewRoot); - $this->ensureDirectoryExists($assetRoot); - - $backgroundPath = $this->createBackgroundPreview($assetRoot . '/background-' . $slug . '.png'); - $signaturePath = $this->layoutUsesSignatureImage($layout) - ? $this->createSignaturePreview($assetRoot . '/signature-' . $slug . '.png') - : null; - - return $this->compilePreviewWithSize( - $slug, - $this->buildLayoutHtml($layout, $backgroundPath, $signaturePath), - self::DEFAULT_PREVIEW_WIDTH, - self::DEFAULT_PREVIEW_HEIGHT, - $previewRoot, - ); - } - - /** - * @return array{result: \LibreSign\XObjectTemplate\Dto\CompileResult, pdf: string, previewPath: string, logoPath: string} - */ - public function compileGovBrLikePreview( - string $slug, - float $width, - float $height, - string $previewRoot, - ): array { - $logoPath = $this->projectRoot . '/tests/Fixtures/Pdf/Svg/govbr-logo.svg'; - if (!is_file($logoPath)) { - throw new \InvalidArgumentException(sprintf('GovBR logo fixture was not found at "%s".', $logoPath)); - } - - $preview = $this->compilePreviewWithSize( - $slug, - $this->buildGovBrLikeLayoutHtml($logoPath), - $width, - $height, - $previewRoot, - ); - - return $preview + ['logoPath' => $logoPath]; - } - - /** - * @return array{result: \LibreSign\XObjectTemplate\Dto\CompileResult, pdf: string, previewPath: string} - */ - private function compilePreviewWithSize( - string $slug, - string $html, - float $width, - float $height, - string $previewRoot, - ): array { - $this->ensureDirectoryExists($previewRoot); - $this->removeLegacyPreviewPngs($previewRoot, $slug); - - $compiler = new XObjectTemplateCompiler(); - $result = $compiler->compile(new CompileRequest( - html: $html, - width: $width, - height: $height, - )); - - $pdf = (new SinglePagePdfExporter())->export($result); - $previewPath = $previewRoot . '/' . $slug . '.pdf'; - file_put_contents($previewPath, $pdf); - - return [ - 'result' => $result, - 'pdf' => $pdf, - 'previewPath' => $previewPath, - ]; - } - - private function buildLayoutHtml(string $layout, string $backgroundPath, ?string $signaturePath): string - { - $background = sprintf( - '', - $this->escapeAttribute($backgroundPath), - ); - - return match ($layout) { - 'signature_and_metadata_right' => sprintf( - '
%s' - . '
' - . '' - . '
' - . '
' - . '
Signed with LibreSign
' - . '
admin
' - . '
Issuer: Preview Issuer
' - . '
Date: 2026-05-28T16:40:21+00:00
' - . '
' - . '
', - $background, - $this->requireSignaturePath($signaturePath), - ), - 'label_and_metadata_right' => sprintf( - '
%s' - . '
' - . '
admin
' - . '
' - . '
' - . '
Signed with LibreSign
' - . '
Issuer: Preview Issuer
' - . '
Date: 2026-05-28T16:40:21+00:00
' - . '
' - . '
', - $background, - ), - 'signature_centered' => sprintf( - '
%s' - . '' - . '
', - $background, - $this->requireSignaturePath($signaturePath), - ), - 'metadata_only_top_left' => sprintf( - '
%s' - . '
' - . '
Signed with LibreSign
' - . '
admin
' - . '
Issuer: Preview Issuer
' - . '
Date: 2026-05-28T16:40:21+00:00
' - . '
' - . '
', - $background, - ), - 'two_columns_centered_cells' => sprintf( - '
%s' - . '
' - . '' - . '
' - . '
' - . '
' - . '
Signed with LibreSign
' - . '
Preview Issuer
' - . '
Date: 2026-05-28T16:40:21+00:00
' - . '
' - . '
' - . '
', - $background, - $this->requireSignaturePath($signaturePath), - ), - default => throw new \InvalidArgumentException(sprintf('Unknown visible stamp layout "%s".', $layout)), - }; - } - - private function buildGovBrLikeLayoutHtml(string $logoPath): string - { - return sprintf( - '
' - . '
' - . '' - . '
' - . '
' - . '
Documento assinado digitalmente
' - . '
ASSINANTE DE EXEMPLO
' - . '
Data: 01/01/2026 12:00:00-0300
' - . '
Verifique em https://verificador.iti.br
' - . '
' - . '
', - $this->escapeAttribute($logoPath), - ); - } - - private function createBackgroundPreview(string $path): string - { - if (is_file($path)) { - return $path; - } - - $contents = PngFixtureFactory::createRgbaPngFromPixelRenderer( - (int) self::DEFAULT_PREVIEW_WIDTH, - (int) self::DEFAULT_PREVIEW_HEIGHT, - function (int $x, int $y, int $width, int $height): array { - $background = [245, 247, 250, 255]; - $diagonal = abs(($height * $x) - ($width * $y)); - $inverseDiagonal = abs(($height * ($width - $x)) - ($width * $y)); - $bandStrength = ($diagonal < $width * 10 || $inverseDiagonal < $width * 10) ? 55 : 0; - $ringCenterX = $width * 0.74; - $ringCenterY = $height * 0.5; - $distance = sqrt((($x - $ringCenterX) ** 2) + (($y - $ringCenterY) ** 2)); - $ringStrength = ($distance > $height * 0.18 && $distance < $height * 0.24) ? 35 : 0; - $strength = max($bandStrength, $ringStrength); - - return [ - max(0, $background[0] - $strength), - max(0, $background[1] - $strength), - max(0, $background[2] - $strength), - 255, - ]; - }, - ); - - file_put_contents($path, $contents); - - return $path; - } - - private function createSignaturePreview(string $path): string - { - if (is_file($path)) { - return $path; - } - - $contents = PngFixtureFactory::createRgbaPngFromPixelRenderer( - 640, - 180, - function (int $x, int $y, int $width, int $height): array { - $normalizedX = $width === 1 ? 0.0 : $x / ($width - 1); - $primaryWave = ($height * 0.56) - + sin($normalizedX * 7.1) * ($height * 0.13) - + sin($normalizedX * 14.2) * ($height * 0.035); - $secondaryWave = ($height * 0.4) - + cos($normalizedX * 16.0) * ($height * 0.05); - $underline = ($height * 0.78) + sin($normalizedX * 5.6) * ($height * 0.015); - - $alpha = 0; - if (abs($y - $primaryWave) <= 2.2 && $normalizedX >= 0.08 && $normalizedX <= 0.92) { - $alpha = 255; - } - - if (abs($y - $secondaryWave) <= 1.6 && $normalizedX >= 0.0 && $normalizedX <= 0.18) { - $alpha = max($alpha, 220); - } - - if (abs($y - $underline) <= 1.0 && $normalizedX >= 0.16 && $normalizedX <= 0.9) { - $alpha = max($alpha, 185); - } - - return [20, 28, 40, $alpha]; - }, - ); - - file_put_contents($path, $contents); - - return $path; - } - - private function removeLegacyPreviewPngs(string $previewRoot, string $slug): void - { - $legacyCandidates = [ - $previewRoot . '/' . $slug . '.png', - $previewRoot . '/' . $slug . '-1.png', - ]; - - foreach ($legacyCandidates as $legacyCandidate) { - if (is_file($legacyCandidate)) { - unlink($legacyCandidate); - } - } - } - - private function requireSignaturePath(?string $signaturePath): string - { - if ($signaturePath === null) { - throw new \InvalidArgumentException('This visible stamp layout requires a signature image.'); - } - - return $this->escapeAttribute($signaturePath); - } - - private function ensureDirectoryExists(string $directory): void - { - if (is_dir($directory)) { - return; - } - - mkdir($directory, 0777, true); - } - - private function layoutUsesSignatureImage(string $layout): bool - { - return in_array( - $layout, - ['signature_and_metadata_right', 'signature_centered', 'two_columns_centered_cells'], - true, - ); - } - - private function escapeAttribute(string $value): string - { - return htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); - } -} diff --git a/tests/Integration/VisibleStampTemplateScenarioTest.php b/tests/Integration/VisibleStampTemplateScenarioTest.php index 12e8bf2..9f14d3b 100644 --- a/tests/Integration/VisibleStampTemplateScenarioTest.php +++ b/tests/Integration/VisibleStampTemplateScenarioTest.php @@ -7,24 +7,42 @@ namespace LibreSign\XObjectTemplate\Tests\Integration; +use LibreSign\XObjectTemplate\Dto\CompileRequest; use LibreSign\XObjectTemplate\Dto\CompileResult; +use LibreSign\XObjectTemplate\Pdf\SinglePagePdfExporter; +use LibreSign\XObjectTemplate\Tests\Support\PngFixtureFactory; +use LibreSign\XObjectTemplate\XObjectTemplateCompiler; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; final class VisibleStampTemplateScenarioTest extends TestCase { - #[DataProvider('visibleStampLayoutProvider')] - public function testPhaseOneVisibleStampLayoutsCanBeCompiledAndExported( + #[DataProvider('visibleStampLayoutScenarios')] + public function testVisibleStampLayoutsCanBeCompiledAndExported( string $slug, string $layout, + float $width, + float $height, int $expectedImageCount, array $expectedTexts, ): void { - $factory = new VisibleStampPreviewFactory(dirname(__DIR__, 2)); - ['result' => $result, 'pdf' => $pdf, 'previewPath' => $previewPath] = $factory->compilePhaseOneLayoutPreview( + $projectRoot = dirname(__DIR__, 2); + $previewRoot = $projectRoot . '/build/examples/visible-stamp'; + $assetRoot = $previewRoot . '/assets'; + self::ensureDirectoryExists($previewRoot); + self::ensureDirectoryExists($assetRoot); + + $backgroundPath = self::createBackgroundPreview($assetRoot . '/background-' . $slug . '.png', (int) $width, (int) $height); + $signaturePath = self::layoutUsesSignatureImage($layout) + ? self::createSignaturePreview($assetRoot . '/signature-' . $slug . '.png') + : null; + + ['result' => $result, 'pdf' => $pdf, 'previewPath' => $previewPath] = self::compilePreviewWithSize( $slug, - $layout, - dirname(__DIR__, 2) . '/build/visible-stamp-previews', + self::buildLayoutHtml($layout, $backgroundPath, $signaturePath), + $width, + $height, + $previewRoot, ); $this->assertBasePreviewExport( @@ -32,8 +50,8 @@ public function testPhaseOneVisibleStampLayoutsCanBeCompiledAndExported( $pdf, $previewPath, $expectedImageCount, - VisibleStampPreviewFactory::DEFAULT_PREVIEW_WIDTH, - VisibleStampPreviewFactory::DEFAULT_PREVIEW_HEIGHT, + $width, + $height, ); foreach ($expectedTexts as $expectedText) { @@ -42,53 +60,16 @@ public function testPhaseOneVisibleStampLayoutsCanBeCompiledAndExported( } } - public function testGovBrLikeVisibleStampCanBeCompiledAndExportedUsingSupportedHtmlAndCssOnly(): void - { - $scenario = self::govBrLikeScenario(); - $factory = new VisibleStampPreviewFactory(dirname(__DIR__, 2)); - - [ - 'result' => $result, - 'pdf' => $pdf, - 'previewPath' => $previewPath, - 'logoPath' => $logoPath, - ] = $factory->compileGovBrLikePreview( - $scenario['slug'], - $scenario['width'], - $scenario['height'], - dirname(__DIR__, 2) . '/build/visible-stamp-previews', - ); - - $this->assertBasePreviewExport( - $result, - $pdf, - $previewPath, - $scenario['expectedImageCount'], - ); - self::assertSame($logoPath, $result->resources['XObject']['Im0']['Source']); - - foreach ($scenario['expectedTexts'] as $expectedText) { - self::assertStringContainsString($expectedText, $result->contentStream); - self::assertStringContainsString($expectedText, $pdf); - } - - self::assertStringContainsString('1 1 1 rg', $result->contentStream); - self::assertStringContainsString('RG', $result->contentStream); - } - /** - * @return iterable - * }> + * @return iterable}> */ - public static function visibleStampLayoutProvider(): iterable + public static function visibleStampLayoutScenarios(): iterable { yield 'signature and metadata at right' => [ 'slug' => 'signature-and-metadata-right', 'layout' => 'signature_and_metadata_right', + 'width' => 804.0, + 'height' => 230.0, 'expectedImageCount' => 2, 'expectedTexts' => [ 'Signed with LibreSign', @@ -101,6 +82,8 @@ public static function visibleStampLayoutProvider(): iterable yield 'label and metadata at right' => [ 'slug' => 'label-and-metadata-right', 'layout' => 'label_and_metadata_right', + 'width' => 804.0, + 'height' => 230.0, 'expectedImageCount' => 1, 'expectedTexts' => [ 'admin', @@ -113,6 +96,8 @@ public static function visibleStampLayoutProvider(): iterable yield 'signature centered' => [ 'slug' => 'signature-centered', 'layout' => 'signature_centered', + 'width' => 804.0, + 'height' => 230.0, 'expectedImageCount' => 2, 'expectedTexts' => [], ]; @@ -120,6 +105,8 @@ public static function visibleStampLayoutProvider(): iterable yield 'metadata only at top left' => [ 'slug' => 'metadata-only-top-left', 'layout' => 'metadata_only_top_left', + 'width' => 804.0, + 'height' => 230.0, 'expectedImageCount' => 1, 'expectedTexts' => [ 'Signed with LibreSign', @@ -132,6 +119,8 @@ public static function visibleStampLayoutProvider(): iterable yield 'two columns with centered cells' => [ 'slug' => 'two-columns-centered-cells', 'layout' => 'two_columns_centered_cells', + 'width' => 804.0, + 'height' => 230.0, 'expectedImageCount' => 2, 'expectedTexts' => [ 'Signed with LibreSign', @@ -142,31 +131,304 @@ public static function visibleStampLayoutProvider(): iterable ]; } + public function testGovBrLikeVisibleStampCanBeCompiledAndExportedUsingSupportedHtmlAndCssOnly(): void + { + $projectRoot = dirname(__DIR__, 2); + $slug = 'govbr-like-visible-stamp'; + $expectedImageCount = 1; + $expectedTexts = [ + 'Documento assinado digitalmente', + 'ASSINANTE DE EXEMPLO', + 'Data: 01/01/2026 12:00:00-0300', + 'Verifique em https://verificador.iti.br', + ]; + + $width = 400.0; + $height = 100.0; + $logoPath = $projectRoot . '/tests/Fixtures/Pdf/Svg/govbr-logo.svg'; + if (!is_file($logoPath)) { + throw new \InvalidArgumentException(sprintf('GovBR logo fixture was not found at "%s".', $logoPath)); + } + + $html = sprintf( + << + +
+
+ Documento assinado digitalmente +
+
+ ASSINANTE DE EXEMPLO +
+
+ Data: 01/01/2026 12:00:00-0300 +
+
+ Verifique em https://verificador.iti.br +
+
+
+ HTML, + htmlspecialchars($logoPath, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'), + ); + + [ + 'result' => $result, + 'pdf' => $pdf, + 'previewPath' => $previewPath, + ] = self::compilePreviewWithSize( + $slug, + $html, + $width, + $height, + $projectRoot . '/build/examples/visible-stamp', + ); + + $this->assertBasePreviewExport( + $result, + $pdf, + $previewPath, + $expectedImageCount, + ); + self::assertSame($width, $result->bbox[2] - $result->bbox[0]); + self::assertSame($height, $result->bbox[3] - $result->bbox[1]); + self::assertSame($logoPath, $result->resources['XObject']['Im0']['Source']); + + foreach ($expectedTexts as $expectedText) { + self::assertStringContainsString($expectedText, $result->contentStream); + self::assertStringContainsString($expectedText, $pdf); + } + + self::assertStringContainsString('1 1 1 rg', $result->contentStream); + self::assertStringNotContainsString('RG', $result->contentStream); + } + /** - * @return array{ - * slug: string, - * width: float, - * height: float, - * expectedImageCount: int, - * expectedTexts: list - * } + * @return array{result: CompileResult, pdf: string, previewPath: string} */ - private static function govBrLikeScenario(): array - { + private static function compilePreviewWithSize( + string $slug, + string $html, + float $width, + float $height, + string $previewRoot, + ): array { + self::ensureDirectoryExists($previewRoot); + self::removeLegacyPreviewPngs($previewRoot, $slug); + + $compiler = new XObjectTemplateCompiler(); + $result = $compiler->compile(new CompileRequest( + html: $html, + width: $width, + height: $height, + )); + + $pdf = (new SinglePagePdfExporter())->export($result); + $previewPath = $previewRoot . '/' . $slug . '.pdf'; + file_put_contents($previewPath, $pdf); + return [ - 'slug' => 'govbr-like-visible-stamp', - 'width' => 760.0, - 'height' => 210.0, - 'expectedImageCount' => 1, - 'expectedTexts' => [ - 'Documento assinado digitalmente', - 'ASSINANTE DE EXEMPLO', - 'Data: 01/01/2026 12:00:00-0300', - 'Verifique em https://verificador.iti.br', - ], + 'result' => $result, + 'pdf' => $pdf, + 'previewPath' => $previewPath, ]; } + private static function buildLayoutHtml(string $layout, string $backgroundPath, ?string $signaturePath): string + { + $background = sprintf( + '', + self::escapeAttribute($backgroundPath), + ); + + return match ($layout) { + 'signature_and_metadata_right' => sprintf( + '
%s' + . '
' + . '' + . '
' + . '
' + . '
Signed with LibreSign
' + . '
admin
' + . '
Issuer: Preview Issuer
' + . '
Date: 2026-05-28T16:40:21+00:00
' + . '
' + . '
', + $background, + self::requireSignaturePath($signaturePath), + ), + 'label_and_metadata_right' => sprintf( + '
%s' + . '
' + . '
admin
' + . '
' + . '
' + . '
Signed with LibreSign
' + . '
Issuer: Preview Issuer
' + . '
Date: 2026-05-28T16:40:21+00:00
' + . '
' + . '
', + $background, + ), + 'signature_centered' => sprintf( + '
%s' + . '' + . '
', + $background, + self::requireSignaturePath($signaturePath), + ), + 'metadata_only_top_left' => sprintf( + '
%s' + . '
' + . '
Signed with LibreSign
' + . '
admin
' + . '
Issuer: Preview Issuer
' + . '
Date: 2026-05-28T16:40:21+00:00
' + . '
' + . '
', + $background, + ), + 'two_columns_centered_cells' => sprintf( + '
%s' + . '
' + . '' + . '
' + . '
' + . '
' + . '
Signed with LibreSign
' + . '
Preview Issuer
' + . '
Date: 2026-05-28T16:40:21+00:00
' + . '
' + . '
' + . '
', + $background, + self::requireSignaturePath($signaturePath), + ), + default => throw new \InvalidArgumentException(sprintf('Unknown visible stamp layout "%s".', $layout)), + }; + } + + private static function createBackgroundPreview(string $path, int $width, int $height): string + { + if (is_file($path)) { + return $path; + } + + $contents = PngFixtureFactory::createRgbaPngFromPixelRenderer( + $width, + $height, + function (int $x, int $y, int $width, int $height): array { + $background = [245, 247, 250, 255]; + $diagonal = abs(($height * $x) - ($width * $y)); + $inverseDiagonal = abs(($height * ($width - $x)) - ($width * $y)); + $bandStrength = ($diagonal < $width * 10 || $inverseDiagonal < $width * 10) ? 55 : 0; + $ringCenterX = $width * 0.74; + $ringCenterY = $height * 0.5; + $distance = sqrt((($x - $ringCenterX) ** 2) + (($y - $ringCenterY) ** 2)); + $ringStrength = ($distance > $height * 0.18 && $distance < $height * 0.24) ? 35 : 0; + $strength = max($bandStrength, $ringStrength); + + return [ + max(0, $background[0] - $strength), + max(0, $background[1] - $strength), + max(0, $background[2] - $strength), + 255, + ]; + }, + ); + + file_put_contents($path, $contents); + + return $path; + } + + private static function createSignaturePreview(string $path): string + { + if (is_file($path)) { + return $path; + } + + $contents = PngFixtureFactory::createRgbaPngFromPixelRenderer( + 640, + 180, + function (int $x, int $y, int $width, int $height): array { + $normalizedX = $width === 1 ? 0.0 : $x / ($width - 1); + $primaryWave = ($height * 0.56) + + sin($normalizedX * 7.1) * ($height * 0.13) + + sin($normalizedX * 14.2) * ($height * 0.035); + $secondaryWave = ($height * 0.4) + + cos($normalizedX * 16.0) * ($height * 0.05); + $underline = ($height * 0.78) + sin($normalizedX * 5.6) * ($height * 0.015); + + $alpha = 0; + if (abs($y - $primaryWave) <= 2.2 && $normalizedX >= 0.08 && $normalizedX <= 0.92) { + $alpha = 255; + } + + if (abs($y - $secondaryWave) <= 1.6 && $normalizedX >= 0.0 && $normalizedX <= 0.18) { + $alpha = max($alpha, 220); + } + + if (abs($y - $underline) <= 1.0 && $normalizedX >= 0.16 && $normalizedX <= 0.9) { + $alpha = max($alpha, 185); + } + + return [20, 28, 40, $alpha]; + }, + ); + + file_put_contents($path, $contents); + + return $path; + } + + private static function removeLegacyPreviewPngs(string $previewRoot, string $slug): void + { + $legacyCandidates = [ + $previewRoot . '/' . $slug . '.png', + $previewRoot . '/' . $slug . '-1.png', + ]; + + foreach ($legacyCandidates as $legacyCandidate) { + if (is_file($legacyCandidate)) { + unlink($legacyCandidate); + } + } + } + + private static function requireSignaturePath(?string $signaturePath): string + { + if ($signaturePath === null) { + throw new \InvalidArgumentException('This visible stamp layout requires a signature image.'); + } + + return self::escapeAttribute($signaturePath); + } + + private static function ensureDirectoryExists(string $directory): void + { + if (is_dir($directory)) { + return; + } + + mkdir($directory, 0777, true); + } + + private static function layoutUsesSignatureImage(string $layout): bool + { + return in_array( + $layout, + ['signature_and_metadata_right', 'signature_centered', 'two_columns_centered_cells'], + true, + ); + } + + private static function escapeAttribute(string $value): string + { + return htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); + } + private function assertBasePreviewExport( CompileResult $result, string $pdf, From 5bba710560c9b3dbfe53823603c4fb5993ad879e Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Mon, 1 Jun 2026 17:27:09 -0300 Subject: [PATCH 28/31] docs: simplify navigation and docs watch defaults Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- README.md | 2 +- docker-compose.yml | 2 +- docs/mkdocs.yml | 5 +++-- docs/source/index.md | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 30cd00a..e6a1814 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ -# xobject-template +# XObject Template Minimal HTML+CSS to reusable PDF Form XObject compiler for visible signature appearances and document overlays. diff --git a/docker-compose.yml b/docker-compose.yml index 12feccc..291c889 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -26,4 +26,4 @@ services: - ./.profile:/var/www/html/.profile ports: - "${DOCS_PORT:-8000}:8000" - command: ["bash", "-lc", "composer --version && bash"] + command: ["bash", "-lc", "composer run docs:watch"] diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 6fe1e00..1a77e3a 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: 2026 LibreSign # SPDX-License-Identifier: AGPL-3.0-or-later -site_name: xobject-template +site_name: XObject Template site_description: Minimal HTML+CSS to reusable PDF Form XObject compiler. site_url: https://libresign.github.io/xobject-template/ repo_url: https://github.com/LibreSign/xobject-template @@ -13,7 +13,7 @@ site_dir: ../build/docs-site theme: name: material - logo: https://avatars.githubusercontent.com/u/79158919?v=4 + logo: https://raw.githubusercontent.com/LibreSign/documentation/main/main/images/logo.png favicon: https://avatars.githubusercontent.com/u/79158919?v=4 palette: - scheme: default @@ -29,6 +29,7 @@ theme: features: - navigation.instant - navigation.top + - toc.integrate - content.code.copy - content.action.edit diff --git a/docs/source/index.md b/docs/source/index.md index 9eb989a..250a6b7 100644 --- a/docs/source/index.md +++ b/docs/source/index.md @@ -1,7 +1,7 @@ -# xobject-template documentation +# XObject Template documentation `xobject-template` compiles a HTML/CSS template into PDF Form XObject output. From 830b6a27a0f74f72f85a857c0d78c7f7ff58a654 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Mon, 1 Jun 2026 17:39:14 -0300 Subject: [PATCH 29/31] fix: REUSE Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- REUSE.toml | 10 ++++++++++ docs/requirements-docs.txt | 3 +++ 2 files changed, 13 insertions(+) diff --git a/REUSE.toml b/REUSE.toml index 0e962fb..f134394 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -17,3 +17,13 @@ path = ["phpunit.xml.dist", "vendor-bin/**/composer.json"] precedence = "override" SPDX-FileCopyrightText = "2026 LibreSign" SPDX-License-Identifier = "AGPL-3.0-or-later" + +[[annotations]] +path = [ + "docs/source/examples/assets/sample.svg", + "docs/source/examples/assets/tiny-jpeg.base64", + "docs/source/examples/assets/tiny-png.base64", +] +precedence = "override" +SPDX-FileCopyrightText = "2026 LibreSign" +SPDX-License-Identifier = "AGPL-3.0-or-later" diff --git a/docs/requirements-docs.txt b/docs/requirements-docs.txt index 372ffe2..6ab9a75 100644 --- a/docs/requirements-docs.txt +++ b/docs/requirements-docs.txt @@ -1,2 +1,5 @@ +# SPDX-FileCopyrightText: 2026 LibreSign +# SPDX-License-Identifier: AGPL-3.0-or-later + mkdocs>=1.6 mkdocs-material>=9.5 From 3ff433ee30d3860acf6a9189e49fcd46f2097bc0 Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Mon, 1 Jun 2026 17:42:20 -0300 Subject: [PATCH 30/31] fix: cs Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- .../VisibleStampTemplateScenarioTest.php | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/tests/Integration/VisibleStampTemplateScenarioTest.php b/tests/Integration/VisibleStampTemplateScenarioTest.php index 9f14d3b..b47688d 100644 --- a/tests/Integration/VisibleStampTemplateScenarioTest.php +++ b/tests/Integration/VisibleStampTemplateScenarioTest.php @@ -32,7 +32,11 @@ public function testVisibleStampLayoutsCanBeCompiledAndExported( self::ensureDirectoryExists($previewRoot); self::ensureDirectoryExists($assetRoot); - $backgroundPath = self::createBackgroundPreview($assetRoot . '/background-' . $slug . '.png', (int) $width, (int) $height); + $backgroundPath = self::createBackgroundPreview( + $assetRoot . '/background-' . $slug . '.png', + (int) $width, + (int) $height, + ); $signaturePath = self::layoutUsesSignatureImage($layout) ? self::createSignaturePreview($assetRoot . '/signature-' . $slug . '.png') : null; @@ -61,7 +65,14 @@ public function testVisibleStampLayoutsCanBeCompiledAndExported( } /** - * @return iterable}> + * @return iterable + * }> */ public static function visibleStampLayoutScenarios(): iterable { From 27de4c7a8dd3cc77bf61828352fa3501b969316e Mon Sep 17 00:00:00 2001 From: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> Date: Mon, 1 Jun 2026 17:44:28 -0300 Subject: [PATCH 31/31] fix: remove empty suite Signed-off-by: Vitor Mattos <1079143+vitormattos@users.noreply.github.com> --- phpunit.xml.dist | 3 --- 1 file changed, 3 deletions(-) diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 63edd57..2927b38 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -16,9 +16,6 @@ tests/Integration - - tests/Documentation -