Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
85 commits
Select commit Hold shift + click to select a range
c09c4c4
feat(svg): add xobject factory interface
vitormattos May 30, 2026
82a8ce3
feat(svg): implement native form xobject factory
vitormattos May 30, 2026
54b37a1
feat(pdf): route svg sources to native xobject factory
vitormattos May 30, 2026
eedbd50
feat(pdf): support generic xobject subtypes
vitormattos May 30, 2026
7e209fa
test(svg): cover native svg xobject scenarios
vitormattos May 30, 2026
6e24eca
test(pdf): validate svg embedder dispatch
vitormattos May 30, 2026
761b038
test(pdf): adjust exporter expectations for xobjects
vitormattos May 30, 2026
f7b467f
test(fixtures): add govbr svg fixture in structured path
vitormattos May 30, 2026
aa7ae16
test(integration): validate govbr flow with native svg support
vitormattos May 30, 2026
c6cced2
test(svg): raise coverage for native svg xobject factory
vitormattos May 30, 2026
e268bb9
test(integration): remove legacy png previews for visible stamp outputs
vitormattos May 30, 2026
8ebd28a
chore: convert to newest syntax
vitormattos May 30, 2026
f777639
test: expand SVG boundary detection to kill PregMatchRemoveDollar mutant
vitormattos May 30, 2026
98c5ccf
fix: correct SVG boundary detection test implementation
vitormattos May 30, 2026
09b5349
chore: increase composer process timeout to 600s for mutation tests
vitormattos May 30, 2026
eac76a3
refactor: extract path parsing state and context classes
vitormattos May 30, 2026
487ba66
refactor: extract arc-to-bezier conversion helper methods
vitormattos May 30, 2026
5176340
refactor: consolidate fill/stroke color resolution
vitormattos May 30, 2026
012adb9
refactor: extract arc conversion to SvgArcConverter
vitormattos May 30, 2026
70ba2e1
refactor: extract color resolution to SvgColorResolver
vitormattos May 30, 2026
eb78872
refactor: extract SvgElementPathBuilder, SvgTransformResolver, SvgPat…
vitormattos May 30, 2026
cc4f59c
refactor: extract ArcParams to its own file for PSR-4 autoloading com…
vitormattos May 30, 2026
5d0c351
refactor: extract PathCommandContext to its own file for PSR-4 compli…
vitormattos May 30, 2026
9e0fcbe
fix: line length
vitormattos May 30, 2026
5a8cfed
fix: line length
vitormattos May 30, 2026
710a563
refactor: resolve PHPMD quality gate issues - rename short variables
vitormattos May 30, 2026
e580563
fix: rector issues and indent code
vitormattos May 30, 2026
50df875
fix: php lint
vitormattos May 30, 2026
a653b83
fix: cs
vitormattos May 30, 2026
9b23646
fix: mutation tests issues
vitormattos May 30, 2026
fad1dcd
feat: increase coverage
vitormattos May 30, 2026
4563d12
chore: use dataprovider
vitormattos May 30, 2026
5df6298
chore: increase coverage
vitormattos May 30, 2026
fce4bcc
fix: cs
vitormattos May 30, 2026
12a76f4
feat: increase test coverage
vitormattos May 30, 2026
c7a35a0
feat: increase coverage
vitormattos May 30, 2026
f04c9f3
feat: increase coverage
vitormattos May 30, 2026
612eb3b
feat: increase coverage
vitormattos May 30, 2026
792cb9b
feat: increase coverage
vitormattos May 30, 2026
229195f
test: add viewport and dimension edge case tests for SvgPdfXObjectFac…
vitormattos May 31, 2026
baad47c
test: add comprehensive error and edge case tests for SvgPdfXObjectFa…
vitormattos May 31, 2026
17724bd
test: add multi-element and shape-specific tests for SvgPdfXObjectFac…
vitormattos May 31, 2026
692ba28
test: add case-insensitive element detection and specific edge case t…
vitormattos May 31, 2026
73e2669
chore: use dataprovider
vitormattos May 31, 2026
066eee8
test: add content-based SVG detection boundary tests for FilesystemPd…
vitormattos May 31, 2026
924a673
test: add boundary tests and mark equivalent mutants for SvgArcConverter
vitormattos May 31, 2026
d6fdd02
fix: cs
vitormattos May 31, 2026
e27b078
fix: duplicated rows
vitormattos May 31, 2026
7727a67
test: mark equivalent mutants for SvgColorResolver
vitormattos May 31, 2026
9d5b6fd
chore: use dataparoviders
vitormattos May 31, 2026
92ababa
fix: ensure arc converter endpoint matches target point exactly
vitormattos May 31, 2026
b229179
test: refactor SvgPathCommandParserTest with DataProviders and additi…
vitormattos May 31, 2026
bd9c150
fix: revert wrong infection config
vitormattos May 31, 2026
46aa7fb
fix: ci issues
vitormattos May 31, 2026
ea7719a
fix: ci issues
vitormattos May 31, 2026
362d9ae
fix(svg): harden transform/path mutation scenarios
vitormattos May 31, 2026
81dccee
test(svg): lock transform ancestor depth behavior
vitormattos May 31, 2026
5e647ef
fix(svg): guard path token loop progress
vitormattos May 31, 2026
1dc929e
fix(svg): harden arc converter edge handling
vitormattos May 31, 2026
25dc724
fix(svg): stabilize path builder parsing loops
vitormattos May 31, 2026
11facb0
test(svg): add element path builder coverage
vitormattos May 31, 2026
56aa989
test(svg): expand arc converter provider scenarios
vitormattos May 31, 2026
33a55db
test(svg): extend path parser final and invalid cases
vitormattos May 31, 2026
1103b19
test(svg): harden color resolver style parsing coverage
vitormattos May 31, 2026
19d4b42
refactor(svg): simplify class style rule guard
vitormattos May 31, 2026
69f1a80
test(svg): expand arc converter provider scenarios
vitormattos May 31, 2026
f535099
test(svg): cover empty and malformed style declarations
vitormattos May 31, 2026
28a4412
test(svg): polish element path builder assertions
vitormattos May 31, 2026
0365ff0
test(svg): extend path parser mutation scenarios
vitormattos May 31, 2026
8e35cde
test(svg): cover style-block skip branches in factory
vitormattos May 31, 2026
0940691
test(svg): refactor factory tests with data providers
vitormattos May 31, 2026
8c8c0d8
feat: harden svg parsing and test providers
vitormattos May 31, 2026
97e150a
test: consolidate svg parser scenarios
vitormattos May 31, 2026
88f981b
test: consolidate svg factory scenarios
vitormattos May 31, 2026
95a0bfb
style: fix svg factory test indentation
vitormattos May 31, 2026
5250a4d
fix(svg): remove redundant DOMElement guards
vitormattos May 31, 2026
df8fe5d
test(svg): harden parser mutation safety and cover transform edge cases
vitormattos Jun 1, 2026
1191d0e
test(svg): reduce resolver duplication and fix factory docblocks
vitormattos Jun 1, 2026
94b7b42
style(svg): fix misplaced inline @var annotation
vitormattos Jun 1, 2026
33a855a
style(test): fix multiline declaration brace placement
vitormattos Jun 1, 2026
cfa8790
fix(svg): normalize form xobject matrix for correct logo rendering
vitormattos Jun 1, 2026
37cdea7
feat: harden SVG color handling and test coverage
vitormattos Jun 1, 2026
8fc4e25
test: harden svg arc/color mutation and rector compliance
vitormattos Jun 1, 2026
6c55388
refactor: extract svg arc math collaborator and public tests
vitormattos Jun 1, 2026
ca010b2
style: fix phpcs issues in arc math refactor
vitormattos Jun 1, 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
29 changes: 19 additions & 10 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
@@ -1,18 +1,27 @@
# SPDX-FileCopyrightText: 2026 LibreSign
# SPDX-License-Identifier: AGPL-3.0-or-later

name: lint
name: Lint php

on:
pull_request:
push:
branches: [main]
on: pull_request

permissions:
contents: read

concurrency:
group: lint-php-${{ github.head_ref || github.run_id }}
cancel-in-progress: true

jobs:
lint:
php-lint:
runs-on: ubuntu-latest
name: php-lint

steps:
- uses: actions/checkout@v6
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Detect minimum PHP from composer.json
id: php_min
run: |
Expand All @@ -22,6 +31,6 @@ jobs:
- uses: shivammathur/setup-php@v2
with:
php-version: ${{ steps.php_min.outputs.version }}
- run: composer install --no-interaction --prefer-dist
- run: composer bin all install --no-interaction --prefer-dist
- run: composer lint

- name: Lint
run: composer run php:lint
3 changes: 3 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
},
"config": {
"sort-packages": true,
"process-timeout": 600,
"allow-plugins": {
"bamarni/composer-bin-plugin": true,
"infection/extension-installer": true
Expand All @@ -44,12 +45,14 @@
},
"scripts": {
"lint": [
"@php:lint",
"@cs:check",
"@rector:check",
"@psalm",
"@duplication:check",
"@composer:validate"
],
"php:lint": "find . -type f -name '*.php' -not -path './vendor/*' -not -path './vendor-bin/*' -not -path './build/*' -print0 | xargs -0 -n1 php -l",
"cs:check": "vendor-bin/phpcs/vendor/squizlabs/php_codesniffer/bin/phpcs -q",
"cs:fix": "vendor-bin/phpcs/vendor/squizlabs/php_codesniffer/bin/phpcbf -q",
"rector:check": "vendor-bin/rector/vendor/rector/rector/bin/rector process --dry-run",
Expand Down
22 changes: 22 additions & 0 deletions src/Pdf/FilesystemPdfImageEmbedder.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,29 +12,39 @@
use LibreSign\XObjectTemplate\Pdf\Jpeg\JpegPdfImageFactoryInterface;
use LibreSign\XObjectTemplate\Pdf\Png\PngPdfImageFactory;
use LibreSign\XObjectTemplate\Pdf\Png\PngPdfImageFactoryInterface;
use LibreSign\XObjectTemplate\Pdf\Svg\SvgPdfXObjectFactory;
use LibreSign\XObjectTemplate\Pdf\Svg\SvgPdfXObjectFactoryInterface;

final readonly class FilesystemPdfImageEmbedder implements PdfImageEmbedderInterface
{
private FilesystemImageSourceReaderInterface $sourceReader;
private ImageMetadataInspectorInterface $metadataInspector;
private JpegPdfImageFactoryInterface $jpegImageFactory;
private PngPdfImageFactoryInterface $pngImageFactory;
private SvgPdfXObjectFactoryInterface $svgXObjectFactory;

public function __construct(
?FilesystemImageSourceReaderInterface $sourceReader = null,
?ImageMetadataInspectorInterface $metadataInspector = null,
?JpegPdfImageFactoryInterface $jpegImageFactory = null,
?PngPdfImageFactoryInterface $pngImageFactory = null,
?SvgPdfXObjectFactoryInterface $svgXObjectFactory = null,
) {
$this->sourceReader = $sourceReader ?? new FilesystemImageSourceReader();
$this->metadataInspector = $metadataInspector ?? new ImageMetadataInspector();
$this->jpegImageFactory = $jpegImageFactory ?? new JpegPdfImageFactory();
$this->pngImageFactory = $pngImageFactory ?? new PngPdfImageFactory();
$this->svgXObjectFactory = $svgXObjectFactory ?? new SvgPdfXObjectFactory();
}

public function embed(string $source): EmbeddedPdfImage
{
$contents = $this->sourceReader->read($source);

if ($this->isSvgSource($source, $contents)) {
return $this->svgXObjectFactory->create($contents, $source);
}

$imageInfo = $this->metadataInspector->detect($contents, $source);
$mime = $this->metadataInspector->resolveMimeType($imageInfo, $source);

Expand All @@ -46,4 +56,16 @@ public function embed(string $source): EmbeddedPdfImage
),
};
}

private function isSvgSource(string $source, string $contents): bool
{
if (preg_match('/\.svgz?$/i', $source) === 1) {
return true;
}

$trimmed = ltrim($contents);

return str_starts_with($trimmed, '<svg')
|| (str_starts_with($trimmed, '<?xml') && str_contains($trimmed, '<svg'));
}
}
6 changes: 1 addition & 5 deletions src/Pdf/SinglePagePdfExporter.php
Original file line number Diff line number Diff line change
Expand Up @@ -127,14 +127,10 @@ private function createImageObjects(array &$objects, array $xObjects): array
$imageReferences = [];

foreach ($xObjects as $alias => $resource) {
if (($resource['Subtype'] ?? null) !== '/Image') {
throw new InvalidArgumentException(sprintf('Unsupported XObject subtype for "%s".', $alias));
}

$source = $resource['Source'] ?? null;
if (!is_string($source) || $source === '') {
throw new InvalidArgumentException(
sprintf('Image resource "%s" must expose a non-empty Source.', $alias),
sprintf('XObject resource "%s" must expose a non-empty Source.', $alias),
);
}

Expand Down
49 changes: 49 additions & 0 deletions src/Pdf/Svg/ArcParams.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

declare(strict_types=1);

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

namespace LibreSign\XObjectTemplate\Pdf\Svg;

/**
* Internal value object grouping the common arc parameters.
*
* @internal
*/
final readonly class ArcParams
{
/**
* @SuppressWarnings(PHPMD.ExcessiveParameterList)
*/
public function __construct(
public float $fromX,
public float $fromY,
public float $toX,
public float $toY,
public float $radiusX,
public float $radiusY,
public float $cosTh,
public float $sinTh,
public int $largeArc,
public int $sweep,
) {
}

public function withRadii(float $radiusX, float $radiusY): self
{
return new self(
$this->fromX,
$this->fromY,
$this->toX,
$this->toY,
$radiusX,
$radiusY,
$this->cosTh,
$this->sinTh,
$this->largeArc,
$this->sweep,
);
}
}
28 changes: 28 additions & 0 deletions src/Pdf/Svg/PathCommandContext.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

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

declare(strict_types=1);

namespace LibreSign\XObjectTemplate\Pdf\Svg;

/**
* Context for path command processing.
* Encapsulates transform and coordinate parameters.
*
* @internal
*/
final readonly class PathCommandContext
{
/**
* @param array{0:float,1:float,2:float,3:float,4:float,5:float} $transformMatrix
*/
public function __construct(
public array $transformMatrix,
public float $minX,
public float $maxY,
public string $source,
) {
}
}
31 changes: 31 additions & 0 deletions src/Pdf/Svg/PathParsingState.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

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

declare(strict_types=1);

namespace LibreSign\XObjectTemplate\Pdf\Svg;

/**
* Mutable state container for SVG path parsing.
* Tracks current position, control points, and accumulated commands.
*
* @internal
*/
final class PathParsingState
{
public function __construct(
public float $currentX = 0.0,
public float $currentY = 0.0,
public float $subpathStartX = 0.0,
public float $subpathStartY = 0.0,
public ?float $lastCubicControlX = null,
public ?float $lastCubicControlY = null,
public ?float $prevQuadCpX = null,
public ?float $prevQuadCpY = null,
/** @var list<string> */
public array $commands = [],
) {
}
}
94 changes: 94 additions & 0 deletions src/Pdf/Svg/SvgArcConverter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
<?php

declare(strict_types=1);

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

namespace LibreSign\XObjectTemplate\Pdf\Svg;

use LibreSign\XObjectTemplate\Pdf\Svg\SvgArcMath;

/**
* Converts SVG arc commands to cubic Bézier curve approximations.
*
* This class encapsulates the mathematical transformation of SVG arc path
* commands (A/a) into a series of cubic Bézier curves, which are directly
* supported by the PDF specification.
*
* The algorithm implements the SVG 2 specification's arc-to-Bézier conversion,
* decomposing the arc into multiple segments for accurate curve approximation.
*/
final readonly class SvgArcConverter
{
public function __construct(
private SvgArcMath $math = new SvgArcMath(),
) {
}

/**
* Convert SVG arc command parameters to cubic Bézier curves.
*
* This method takes the arc parameters as specified in SVG and converts
* them to an array of cubic Bézier curve control points that approximate
* the arc within the PDF coordinate space.
*
* @param float $fromX Starting X coordinate
* @param float $fromY Starting Y coordinate
* @param float $rx X-axis radius
* @param float $ry Y-axis radius
* @param float $rotation Rotation angle in degrees
* @param int $largeArc Large arc flag (0 or 1)
* @param int $sweep Sweep flag (0 or 1)
* @param float $toX Ending X coordinate
* @param float $toY Ending Y coordinate
* @return array<int, array<int, float>> Array of cubic Bézier control points
*/
public function arcToBezierCurves(
float $fromX,
float $fromY,
float $radiusX,
float $radiusY,
float $rotation,
int $largeArc,
int $sweep,
float $toX,
float $toY,
): array {
if (abs($toX - $fromX) < 1e-10 && abs($toY - $fromY) < 1e-10) {
return [];
}

if ($radiusX < 1e-10 || $radiusY < 1e-10) {
return [[$toX, $toY, $toX, $toY, $toX, $toY]];
}

$theta = deg2rad($rotation);
$params = new ArcParams(
$fromX,
$fromY,
$toX,
$toY,
$radiusX,
$radiusY,
cos($theta),
sin($theta),
$largeArc,
$sweep,
);

$params = $this->math->normalizeArcRadii($params);

[$centerX, $centerY] = $this->math->calculateArcCenter($params);

[$startAngle, $deltaAngle] = $this->math->calculateArcAngles($params);

return $this->math->generateArcCurves(
$params,
$centerX,
$centerY,
$startAngle,
$deltaAngle,
);
}
}
Loading
Loading