Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
eda6b26
test: cover remaining uncovered mutants
vitormattos May 29, 2026
4c786dc
refactor: inject PNG header unpacker
vitormattos May 29, 2026
b5adc39
refactor: inject PNG scanline compressor
vitormattos May 29, 2026
d3af18c
test: cover PNG runtime wrappers
vitormattos May 29, 2026
8979adf
chore: remove PNG namespace override helper
vitormattos May 29, 2026
036fb08
ci: enforce duplication workflow
vitormattos May 30, 2026
19960a1
ci: enforce mutation workflow
vitormattos May 30, 2026
f8a23bf
ci: add integration workflow
vitormattos May 30, 2026
8e09a80
chore: harden quality scripts
vitormattos May 30, 2026
7c6a8ee
test: raise infection thresholds
vitormattos May 30, 2026
7a9f0fe
test: fail on skipped and incomplete tests
vitormattos May 30, 2026
0e2009f
chore: raise psalm strictness
vitormattos May 30, 2026
aabf255
refactor: harden subset html parser
vitormattos May 30, 2026
30b0557
refactor: reduce renderer duplication
vitormattos May 30, 2026
7d32565
refactor: harden png color type description
vitormattos May 30, 2026
319eed0
refactor: harden png header unpacker
vitormattos May 30, 2026
0860e71
refactor: add compile result resource extractor
vitormattos May 30, 2026
fb69680
refactor: split pdf resource extraction
vitormattos May 30, 2026
a1c7565
refactor: tighten template document builder types
vitormattos May 30, 2026
b2d1edd
test: add reusable pdf image embedder double
vitormattos May 30, 2026
ef2ea6a
test: use providers in flex planner coverage
vitormattos May 30, 2026
e0648df
test: use providers in text box layout coverage
vitormattos May 30, 2026
264251c
test: add compile result resource extractor coverage
vitormattos May 30, 2026
a78f8ce
test: cover png header unpacker guards
vitormattos May 30, 2026
d389cca
test: harden png parser header failures
vitormattos May 30, 2026
82d5a76
test: harden single page exporter coverage
vitormattos May 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .github/workflows/duplication.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ on:
jobs:
duplication:
runs-on: ubuntu-latest
continue-on-error: true
steps:
- uses: actions/checkout@v6
- name: Detect minimum PHP from composer.json
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/mutation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ name: mutation

on:
pull_request:
push:
branches: [main]

jobs:
mutation:
Expand Down
27 changes: 27 additions & 0 deletions .github/workflows/test-integration.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# SPDX-FileCopyrightText: 2026 LibreSign
# SPDX-License-Identifier: AGPL-3.0-or-later

name: test-integration

on:
pull_request:
push:
branches: [main]

jobs:
test-integration:
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 phpunit install --no-interaction --prefer-dist
- run: composer run test:integration
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
"psalm": "vendor-bin/psalm/vendor/vimeo/psalm/psalm --no-progress",
"phpmd": "vendor-bin/phpmd/vendor/phpmd/phpmd/src/bin/phpmd src,tests text phpmd.xml",
"psalm:update-baseline": "vendor-bin/psalm/vendor/vimeo/psalm/psalm --set-baseline=psalm-baseline.xml",
"duplication:check": "vendor-bin/phpcpd/vendor/sebastian/phpcpd/phpcpd --min-lines=8 --min-tokens=70 --exclude vendor --exclude tests/Fixtures --exclude tests/Snapshots src tests",
"duplication:check": "vendor-bin/phpcpd/vendor/sebastian/phpcpd/phpcpd --min-lines=6 --min-tokens=50 --exclude vendor --exclude tests/Fixtures --exclude tests/Snapshots src tests",
"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",
Expand All @@ -69,6 +69,7 @@
"sh:security": "shellcheck scripts/*.sh",
"profile:xdebug": "XDEBUG_MODE=profile php benchmarks/profile.php",
"test:unit": "vendor-bin/phpunit/vendor/phpunit/phpunit/phpunit --testsuite unit",
"test:integration": "vendor-bin/phpunit/vendor/phpunit/phpunit/phpunit --testsuite integration",
"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",
"performance:check": "php benchmarks/compiler-benchmark.php",
Expand Down
4 changes: 2 additions & 2 deletions infection.json5
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
"phpUnit": {
"customPath": "vendor-bin/phpunit/vendor/bin/phpunit"
},
"minMsi": 75,
"minCoveredMsi": 82,
"minMsi": 100,
"minCoveredMsi": 100,
"testFramework": "phpunit",
"timeout": 120
}
8 changes: 7 additions & 1 deletion phpunit.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@
<phpunit bootstrap="vendor/autoload.php"
colors="true"
cacheDirectory="build/.phpunit-cache"
beStrictAboutTestsThatDoNotTestAnything="true">
beStrictAboutTestsThatDoNotTestAnything="true"
failOnDeprecation="true"
failOnIncomplete="true"
failOnNotice="true"
failOnRisky="true"
failOnSkipped="true"
failOnWarning="true">
<testsuites>
<testsuite name="unit">
<directory>tests/Unit</directory>
Expand Down
2 changes: 1 addition & 1 deletion psalm.xml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?xml version="1.0"?>
<!-- SPDX-FileCopyrightText: 2026 LibreSign -->
<!-- SPDX-License-Identifier: AGPL-3.0-or-later -->
<psalm errorLevel="3"
<psalm errorLevel="1"
resolveFromConfigFile="true"
findUnusedBaselineEntry="true"
findUnusedCode="false">
Expand Down
39 changes: 32 additions & 7 deletions src/Html/SubsetHtmlParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

namespace LibreSign\XObjectTemplate\Html;

use DOMAttr;
use DOMDocument;
use DOMElement;
use DOMNode;
Expand Down Expand Up @@ -58,7 +59,7 @@ public function parse(string $html): array
}

$nodes = [];
foreach ($body->childNodes as $child) {
foreach ($this->iterateChildNodes($body) as $child) {
$parsed = $this->parseDomNode($child, '');
if ($parsed !== null) {
$nodes[] = $parsed;
Expand Down Expand Up @@ -92,7 +93,7 @@ private function parseElementNode(DOMElement $node, string $inheritedStyle): Nod
}

$children = [];
foreach ($node->childNodes as $childNode) {
foreach ($this->iterateChildNodes($node) as $childNode) {
$child = $this->parseDomNode($childNode, $effectiveStyle);
if ($child !== null) {
$children[] = $child;
Expand Down Expand Up @@ -128,16 +129,40 @@ private function parseTextNode(DOMNode $node, string $inheritedStyle): ?Node
private function collectAttributes(DOMElement $node): array
{
$attributes = [];
$nodeAttrs = $node->attributes;
if ($nodeAttrs !== null) {
foreach ($nodeAttrs as $attribute) {
$attributes[$attribute->name] = trim($attribute->value);
}
foreach ($this->iterateAttributes($node) as $attribute) {
$attributes[$attribute->name] = trim($attribute->value);
}

return $attributes;
}

/**
* @return \Generator<int, DOMNode>
*/
private function iterateChildNodes(DOMNode $node): \Generator
{
/** @var DOMNode $child */
foreach ($node->childNodes as $child) {
yield $child;
}
}

/**
* @return \Generator<int, DOMAttr>
*/
private function iterateAttributes(DOMElement $node): \Generator
{
$attributes = $node->attributes;
if ($attributes === null) {
return;
}

/** @var DOMAttr $attribute */
foreach ($attributes as $attribute) {
yield $attribute;
}
}

private function mergeStyle(string $inheritedStyle, string $ownStyle): string
{
$inheritedStyle = $this->filterInheritableStyle($inheritedStyle);
Expand Down
59 changes: 46 additions & 13 deletions src/Layout/StructuredLayoutRenderer.php
Original file line number Diff line number Diff line change
Expand Up @@ -274,13 +274,15 @@ private function renderBlockContainer(
);
}

$renderedHeight = $localClipBox === null
? $this->boxResolver->resolveAutoContainerHeight($box['height'], $padding, $contentHeight)
: $this->boxResolver->resolveFixedContainerHeight($box['height'], $padding, $contentHeight);

$this->appendDecoration($style, $box, $renderedHeight, $canvasHeight, $decorations);

return $renderedHeight;
return $this->finalizeContainerRendering(
$style,
$box,
$padding,
$contentHeight,
$localClipBox,
$canvasHeight,
$decorations,
);
}

/**
Expand Down Expand Up @@ -324,12 +326,15 @@ private function renderFlexContainer(
);

if ($items === []) {
$renderedHeight = $localClipBox === null
? $this->boxResolver->resolveAutoContainerHeight($box['height'], $padding, 0.0)
: $this->boxResolver->resolveFixedContainerHeight($box['height'], $padding, 0.0);
$this->appendDecoration($style, $box, $renderedHeight, $canvasHeight, $decorations);

return $renderedHeight;
return $this->finalizeContainerRendering(
$style,
$box,
$padding,
0.0,
$localClipBox,
$canvasHeight,
$decorations,
);
}

$metrics = $this->flexPlanner->calculateMetrics($items, $direction, $justifyContent, $gap, $contentBox);
Expand Down Expand Up @@ -359,9 +364,37 @@ private function renderFlexContainer(
}

$contentHeight = $direction === 'row' ? $metrics['crossAxisSize'] : $metrics['totalMainAxisSize'];

return $this->finalizeContainerRendering(
$style,
$box,
$padding,
$contentHeight,
$localClipBox,
$canvasHeight,
$decorations,
);
}

/**
* @param array{x: float, y: float, width: float, height: float} $box
* @param array{top: float, right: float, bottom: float, left: float} $padding
* @param list<LayoutDecoration> $decorations
* @param array{x: float, y: float, width: float, height: float}|null $localClipBox
*/
private function finalizeContainerRendering(
StyleMap $style,
array $box,
array $padding,
float $contentHeight,
?array $localClipBox,
float $canvasHeight,
array &$decorations,
): float {
$renderedHeight = $localClipBox === null
? $this->boxResolver->resolveAutoContainerHeight($box['height'], $padding, $contentHeight)
: $this->boxResolver->resolveFixedContainerHeight($box['height'], $padding, $contentHeight);

$this->appendDecoration($style, $box, $renderedHeight, $canvasHeight, $decorations);

return $renderedHeight;
Expand Down
61 changes: 61 additions & 0 deletions src/Pdf/CompileResultResourceExtractor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?php

// SPDX-FileCopyrightText: 2026 LibreSign
// SPDX-License-Identifier: AGPL-3.0-or-later

declare(strict_types=1);

namespace LibreSign\XObjectTemplate\Pdf;

use InvalidArgumentException;
use LibreSign\XObjectTemplate\Dto\CompileResult;

/** @internal */
class CompileResultResourceExtractor
{
/**
* @return array<string, array<string, mixed>>
*/
public function extract(CompileResult $result, string $resourceType, string $itemMessageTemplate): array
{
if (!array_key_exists($resourceType, $result->resources)) {
return [];
}

/** @var mixed $rawResources */
$rawResources = $result->resources[$resourceType];
$resources = $this->requireStringKeyedArray(
$rawResources,
sprintf('%s resources must be an array.', $resourceType),
);

$normalizedResources = [];
foreach (array_keys($resources) as $alias) {
$normalizedResources[$alias] = $this->requireStringKeyedArray(
$resources[$alias],
sprintf($itemMessageTemplate, $alias),
);
}

return $normalizedResources;
}

/**
* @return array<string, mixed>
*/
private function requireStringKeyedArray(mixed $value, string $message): array
{
if (!is_array($value)) {
throw new InvalidArgumentException($message);
}

foreach (array_keys($value) as $key) {
if (!is_string($key)) {
throw new InvalidArgumentException($message);
}
}

/** @var array<string, mixed> $value */
return $value;
}
}
29 changes: 29 additions & 0 deletions src/Pdf/Png/PhpPngHeaderUnpacker.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

// SPDX-FileCopyrightText: 2026 LibreSign
// SPDX-License-Identifier: AGPL-3.0-or-later

declare(strict_types=1);

namespace LibreSign\XObjectTemplate\Pdf\Png;

/** @internal */
final class PhpPngHeaderUnpacker implements PngHeaderUnpackerInterface
{
private const HEADER_BYTES = 13;

public function unpack(string $data): array|false
{
if (strlen($data) < self::HEADER_BYTES) {
return false;
}

/** @var array{width: int, height: int, bitDepth: int, colorType: int, compression: int, filter: int, interlace: int} $header */
$header = unpack(
'Nwidth/Nheight/CbitDepth/CcolorType/Ccompression/Cfilter/Cinterlace',
$data,
);

return $header;
}
}
17 changes: 17 additions & 0 deletions src/Pdf/Png/PhpPngScanlineCompressor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

// SPDX-FileCopyrightText: 2026 LibreSign
// SPDX-License-Identifier: AGPL-3.0-or-later

declare(strict_types=1);

namespace LibreSign\XObjectTemplate\Pdf\Png;

/** @internal */
final class PhpPngScanlineCompressor implements PngScanlineCompressorInterface
{
public function compress(string $scanlines): string|false
{
return gzcompress($scanlines);
}
}
4 changes: 0 additions & 4 deletions src/Pdf/Png/PngColorTypeDescription.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,6 @@
/** @var positive-int */
public int $bytesPerPixel;

/**
* @param 1|3 $colorCount
* @param positive-int $bytesPerPixel
*/
public function __construct(
public string $colorSpace,
int $colorCount,
Expand Down
25 changes: 25 additions & 0 deletions src/Pdf/Png/PngHeaderUnpackerInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

// SPDX-FileCopyrightText: 2026 LibreSign
// SPDX-License-Identifier: AGPL-3.0-or-later

declare(strict_types=1);

namespace LibreSign\XObjectTemplate\Pdf\Png;

/** @internal */
interface PngHeaderUnpackerInterface
{
/**
* @return array{
* width: int,
* height: int,
* bitDepth: int,
* colorType: int,
* compression: int,
* filter: int,
* interlace: int
* }|false
*/
public function unpack(string $data): array|false;
}
Loading
Loading