diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index acf64a7..c10b571 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -6,6 +6,9 @@ name: coverage on: pull_request: +env: + COVERAGE_THRESHOLDS: '95 95' + jobs: coverage: runs-on: ubuntu-latest @@ -23,4 +26,27 @@ jobs: coverage: xdebug - run: composer install --no-interaction --prefer-dist - run: composer bin phpunit install --no-interaction --prefer-dist - - run: composer run test:coverage + - name: Generate coverage reports + run: XDEBUG_MODE=coverage vendor-bin/phpunit/vendor/phpunit/phpunit/phpunit --coverage-text --coverage-clover=build/coverage/clover.xml --coverage-cobertura=build/coverage/cobertura.xml --coverage-html=build/coverage/html + - name: Store temporary coverage cache + uses: actions/cache/save@v4 + with: + path: build/coverage + key: coverage-${{ github.event.pull_request.number }}-${{ github.sha }} + - name: Upload temporary coverage artifact + uses: actions/upload-artifact@v4 + with: + name: coverage-report-${{ github.event.pull_request.number }}-${{ github.sha }} + path: build/coverage + retention-days: 7 + - name: Validate minimum line coverage + uses: irongut/CodeCoverageSummary@v1.3.0 + with: + filename: build/coverage/cobertura.xml + fail_below_min: true + format: text + hide_branch_rate: false + hide_complexity: true + indicators: true + output: console + thresholds: ${{ env.COVERAGE_THRESHOLDS }} diff --git a/.github/workflows/licenses-compliance.yml b/.github/workflows/licenses-compliance.yml deleted file mode 100644 index f793b6f..0000000 --- a/.github/workflows/licenses-compliance.yml +++ /dev/null @@ -1,25 +0,0 @@ -# SPDX-FileCopyrightText: 2026 LibreSign -# SPDX-License-Identifier: AGPL-3.0-or-later - -name: licenses-compliance - -on: - pull_request: - -jobs: - licenses-compliance: - runs-on: ubuntu-latest - steps: - - 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" - - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ steps.php_min.outputs.version }} - - run: composer install --no-interaction --prefer-dist - - run: composer bin qa install --no-interaction --prefer-dist - - run: composer run licenses:check diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 27c4bc1..0057cfb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -22,3 +22,9 @@ Run key checks before opening/updating PR: - `composer run test:unit` - `composer run test:coverage` - `composer run deps:audit` + +## Coverage policy + +- CI generates Clover XML and HTML reports under `build/coverage/` +- Pull requests must keep **minimum line coverage at 95%** +- Coverage reports are stored temporarily in GitHub Actions ephemeral storage for post-PR analysis diff --git a/README.md b/README.md index d1ec745..cd9cafd 100644 --- a/README.md +++ b/README.md @@ -3,16 +3,71 @@ # xobject-template -> Compile a minimal HTML+CSS subset into reusable PDF Form XObject templates for visible signatures. +> Compile a minimal HTML+CSS subset into reusable PDF Form XObject payloads. -`xobject-template` is a focused rendering engine for digital-signature ecosystems that need **beautiful, vector-first, stable** appearance templates with predictable performance. +`xobject-template` is a focused rendering engine for projects that need **beautiful, vector-first, stable** reusable overlays inside PDF workflows. ## Why this package -- Product-ready visible signature templates (text + image) as real XObject payloads -- Clean integration path for `pdf-signer-php` and LibreSign-like platforms +- 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 ## Quick integration -Use the compiler to generate `content stream`, `resources`, and `bbox`, then map the output to your signature appearance DTO adapter. +Use the compiler to generate a reusable XObject result. Consumers that prefer arrays over DTOs can adapt the result into a generic payload shape. + +```php +use LibreSign\XObjectTemplate\Dto\CompileRequest; +use LibreSign\XObjectTemplate\Integration\XObjectPayloadAdapter; +use LibreSign\XObjectTemplate\XObjectTemplateCompiler; + +$compiler = new XObjectTemplateCompiler(); + +$result = $compiler->compile(new CompileRequest( + html: '
',
+ width: 240.0,
+ height: 84.0,
+));
+
+$payload = (new XObjectPayloadAdapter())->toXObjectPayload($result);
+```
+
+### 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`
+
+## Supported HTML/CSS subset
+
+### HTML
+
+- Supported elements: ``, ``, ` Reference: {{reference}} Reason: {{reason}} Document approved Document approved
`, ``
+- 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
+
+### CSS used by the renderer
+
+- Typography: `font-size`, `font-family`, `font-weight`, `line-height`, `color`
+- Layout: `margin`, `padding`, `text-align`, `width`, `height`
+- Numeric values can be provided as unitless numbers or `px`
+- `px` values are converted to PDF points using the package conversion rules
+- Unknown or incomplete CSS declarations are ignored instead of aborting the render
+
+### Rendering notes
+
+- Font family mapping currently targets the built-in Helvetica, Times, and Courier aliases used by the generated PDF resources
+- `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
+
+## Failure modes
+
+- 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 2f381d5..c985bcb 100644
--- a/composer.json
+++ b/composer.json
@@ -1,12 +1,14 @@
{
"name": "libresign/xobject-template",
- "description": "Minimal HTML+CSS to PDF Form XObject compiler for visible signature appearance.",
+ "description": "Minimal HTML+CSS to reusable PDF Form XObject compiler.",
"type": "library",
"license": "AGPL-3.0-or-later",
"keywords": [
"pdf",
"xobject",
- "signature",
+ "template",
+ "rendering",
+ "layout",
"html",
"css"
],
@@ -59,7 +61,6 @@
"composer:validate": "composer validate --strict",
"composer:normalize:check": "vendor-bin/qa/vendor/ergebnis/composer-normalize/bin/composer-normalize --dry-run",
"bc:check": "vendor-bin/qa/vendor/roave/backward-compatibility-check/bin/roave-backward-compatibility-check --from=origin/main --to=HEAD",
- "licenses:check": "php scripts/licenses-check.php",
"changelog:check": "echo 'Handled by GitHub Action: .github/workflows/changelog.yml'",
"release:draft": "echo 'Handled by GitHub Action: .github/workflows/release-draft.yml'",
"deps:audit": "composer audit",
diff --git a/scripts/licenses-check.php b/scripts/licenses-check.php
deleted file mode 100644
index 1c00e8d..0000000
--- a/scripts/licenses-check.php
+++ /dev/null
@@ -1,37 +0,0 @@
-
+ *
+ * @throws UnsupportedSubsetException If the HTML fragment contains an element outside the supported subset.
*/
public function parse(string $html): array
{
diff --git a/src/Integration/SignatureAppearanceXObjectAdapter.php b/src/Integration/XObjectPayloadAdapter.php
similarity index 75%
rename from src/Integration/SignatureAppearanceXObjectAdapter.php
rename to src/Integration/XObjectPayloadAdapter.php
index 38463e4..cf5f796 100644
--- a/src/Integration/SignatureAppearanceXObjectAdapter.php
+++ b/src/Integration/XObjectPayloadAdapter.php
@@ -9,10 +9,10 @@
use LibreSign\XObjectTemplate\Dto\CompileResult;
-final class SignatureAppearanceXObjectAdapter
+final class XObjectPayloadAdapter
{
/**
- * Output compatible with consumers expecting stream/resources pair.
+ * Output compatible with consumers expecting a generic Form XObject payload.
*
* @return array{
* stream: string,
@@ -20,7 +20,7 @@ final class SignatureAppearanceXObjectAdapter
* bbox: array{0: float, 1: float, 2: float, 3: float}
* }
*/
- public function toPdfSignerPayload(CompileResult $result): array
+ public function toXObjectPayload(CompileResult $result): array
{
return [
'stream' => $result->contentStream,
diff --git a/src/XObjectTemplateCompiler.php b/src/XObjectTemplateCompiler.php
index f2b6545..2154712 100644
--- a/src/XObjectTemplateCompiler.php
+++ b/src/XObjectTemplateCompiler.php
@@ -10,6 +10,7 @@
use LibreSign\XObjectTemplate\Contract\XObjectTemplateCompilerInterface;
use LibreSign\XObjectTemplate\Dto\CompileRequest;
use LibreSign\XObjectTemplate\Dto\CompileResult;
+use LibreSign\XObjectTemplate\Exception\UnsupportedSubsetException;
use LibreSign\XObjectTemplate\Html\SubsetHtmlParser;
use LibreSign\XObjectTemplate\Layout\LinearLayoutEngine;
use LibreSign\XObjectTemplate\Pdf\ColorParser;
@@ -37,6 +38,11 @@ public function __construct(
);
}
+ /**
+ * Compile the supported HTML+CSS subset into a reusable PDF Form XObject payload.
+ *
+ * @throws UnsupportedSubsetException If the HTML fragment contains an unsupported element.
+ */
public function compile(CompileRequest $request): CompileResult
{
$start = hrtime(true);
diff --git a/tests/Fixtures/overlay-basic.html b/tests/Fixtures/overlay-basic.html
new file mode 100644
index 0000000..83f5d44
--- /dev/null
+++ b/tests/Fixtures/overlay-basic.html
@@ -0,0 +1,2 @@
+
'
+ yield 'image with reference text' => [
+ 'html' => '
'
. 'ID 42',
'maxStreamLength' => 1400,
];
- yield 'styled signer with alignment and spacing' => [
+ yield 'styled block with alignment and spacing' => [
'html' => '
Signed by Alice
', - 'expectedSnippet' => '(Signed by Alice) Tj', + 'html' => 'Rendered for Alice
', + 'expectedSnippet' => '(Rendered for Alice) Tj', ]; yield 'line break text' => [ @@ -49,7 +49,7 @@ public static function htmlProvider(): iterable ]; yield 'image command' => [ - 'html' => '
',
+ 'html' => '
',
'expectedSnippet' => '/Im0 Do',
];
@@ -86,7 +86,7 @@ public function testCompileUsesProvidedTemplateDocumentBuilderInstance(): void
self::assertArrayNotHasKey('F1', $result->resources['Font']);
}
- public function testCompilerConstructorStillAcceptsLegacyPdfDependencies(): void
+ public function testCompilerConstructorAcceptsExplicitPdfDependencies(): void
{
$pdfEscaper = new PdfEscaper();
$colorParser = new ColorParser();