Skip to content

Commit 78e231f

Browse files
authored
Merge pull request #19 from LibreSign/copilot/fix-composer-mutation-test-issues
Fix remaining CI failures in PDF exporter test coverage branch
2 parents 179b9f7 + 96a5a38 commit 78e231f

39 files changed

Lines changed: 2617 additions & 453 deletions

src/Layout/TextLineBreaker.php

Lines changed: 40 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ public function wrap(
6565
$lines[] = $currentLine;
6666
}
6767

68-
return $lines === [] ? [$text] : $lines;
68+
return $lines;
6969
}
7070

7171
private function fitsOnCurrentLine(
@@ -98,14 +98,9 @@ private function appendBrokenWord(
9898
string &$currentLine,
9999
): void {
100100
$segments = $this->breakWord($word, $maxWidth, $fontAlias, $fontSize, $hyphens);
101-
$lastIndex = count($segments) - 1;
102-
103-
foreach ($segments as $index => $segment) {
104-
if ($index === $lastIndex) {
105-
$currentLine = $segment;
106-
continue;
107-
}
101+
$currentLine = array_pop($segments) ?? '';
108102

103+
foreach ($segments as $segment) {
109104
$lines[] = $segment;
110105
}
111106
}
@@ -120,7 +115,7 @@ private function breakWord(
120115
float $fontSize,
121116
string $hyphens,
122117
): array {
123-
if ($hyphens === 'none') {
118+
if ($hyphens === 'none' || ($hyphens !== 'manual' && $hyphens !== 'auto')) {
124119
return [$word];
125120
}
126121

@@ -129,10 +124,6 @@ private function breakWord(
129124
return $manualSegments;
130125
}
131126

132-
if ($hyphens !== 'auto') {
133-
return [$word];
134-
}
135-
136127
return $this->breakWordAutomatically($word, $maxWidth, $fontAlias, $fontSize);
137128
}
138129

@@ -146,15 +137,17 @@ private function resolveManualBreaks(
146137
float $fontSize,
147138
string $hyphens,
148139
): ?array {
149-
if ($hyphens !== 'manual' || !str_contains($word, "\u{00AD}")) {
140+
if ($hyphens !== 'manual') {
141+
return null;
142+
}
143+
144+
if (!str_contains($word, "\u{00AD}")) {
150145
return null;
151146
}
152147

153148
$manualBreaks = explode("\u{00AD}", $word);
154149

155-
return count($manualBreaks) > 1
156-
? $this->packManualSegments($manualBreaks, $maxWidth, $fontAlias, $fontSize)
157-
: null;
150+
return $this->packManualSegments($manualBreaks, $maxWidth, $fontAlias, $fontSize);
158151
}
159152

160153
/**
@@ -166,64 +159,67 @@ private function breakWordAutomatically(
166159
string $fontAlias,
167160
float $fontSize,
168161
): array {
162+
$characters = $this->splitCharacters($word);
163+
if ($characters === []) {
164+
return [$word];
165+
}
166+
169167
$segments = [];
170-
$remaining = $this->splitCharacters($word);
168+
$hyphenWidth = $this->fontMetrics->measureString($fontAlias, $fontSize, '-');
169+
$offset = 0;
170+
$characterCount = count($characters);
171171

172-
while ($remaining !== []) {
173-
['segment' => $segment, 'consumed' => $consumed] = $this->resolveAutoSegment(
174-
$remaining,
172+
while (isset($characters[$offset])) {
173+
$segment = $this->resolveAutoSegment(
174+
array_slice($characters, $offset),
175175
$maxWidth,
176176
$fontAlias,
177177
$fontSize,
178+
$hyphenWidth,
178179
);
179180

180-
if ($consumed <= 0) {
181-
break;
182-
}
183-
184-
$remaining = array_slice($remaining, $consumed);
185-
$segments[] = $remaining === [] ? $segment : ($segment . '-');
181+
$segmentCharacters = $this->splitCharacters($segment);
182+
$fallbackCount = count($this->splitCharacters($characters[$offset]));
183+
$consumedCount = max(count($segmentCharacters) + $fallbackCount - 1, $fallbackCount);
184+
$offset = min($offset + $consumedCount, $characterCount);
185+
$segments[] = !isset($characters[$offset]) ? $segment : ($segment . '-');
186186
}
187187

188-
return $segments === [] ? [$word] : $segments;
188+
return $segments;
189189
}
190190

191191
/**
192192
* @param list<string> $remaining
193-
* @return array{segment: string, consumed: int}
193+
* @return string
194194
*/
195195
private function resolveAutoSegment(
196196
array $remaining,
197197
float $maxWidth,
198198
string $fontAlias,
199199
float $fontSize,
200-
): array {
200+
float $hyphenWidth,
201+
): string {
201202
$segment = '';
202-
$consumed = 0;
203203
$remainingCount = count($remaining);
204204

205205
foreach ($remaining as $index => $character) {
206206
$candidate = $segment . $character;
207-
$isLastCharacter = $index === ($remainingCount - 1);
208-
$candidateWidth = $this->fontMetrics->measureString(
209-
$fontAlias,
210-
$fontSize,
211-
$candidate . ($isLastCharacter ? '' : '-'),
212-
);
207+
$hasMoreCharacters = ($index + 1) < $remainingCount;
208+
$candidateWidth = $this->fontMetrics->measureString($fontAlias, $fontSize, $candidate)
209+
+ ($hasMoreCharacters ? $hyphenWidth : 0.0);
213210

214211
if ($candidateWidth > $maxWidth && $segment !== '') {
215212
break;
216213
}
217214

218215
if ($candidateWidth > $maxWidth) {
219-
return ['segment' => $character, 'consumed' => 1];
216+
return $character;
220217
}
221218

222219
$segment = $candidate;
223-
$consumed = $index + 1;
224220
}
225221

226-
return ['segment' => $segment, 'consumed' => $consumed];
222+
return $segment;
227223
}
228224

229225
/**
@@ -239,13 +235,15 @@ private function packManualSegments(
239235
$packed = [];
240236
$current = '';
241237
$lastIndex = count($segments) - 1;
238+
$hyphenWidth = $this->fontMetrics->measureString($fontAlias, $fontSize, '-');
242239

243240
foreach ($segments as $index => $segment) {
244241
$candidate = $current . $segment;
245-
$candidateWithHyphen = $candidate . ($index === $lastIndex ? '' : '-');
242+
$candidateWidth = $this->fontMetrics->measureString($fontAlias, $fontSize, $candidate)
243+
+ ($index === $lastIndex ? 0.0 : $hyphenWidth);
246244
if (
247245
$current !== ''
248-
&& $this->fontMetrics->measureString($fontAlias, $fontSize, $candidateWithHyphen) > $maxWidth
246+
&& $candidateWidth > $maxWidth
249247
) {
250248
$packed[] = $current . '-';
251249
$current = $segment;

src/Layout/TextOverflowTruncator.php

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,13 @@ public function forceEllipsis(
3636
float $fontSize,
3737
): string {
3838
$ellipsis = '...';
39+
$ellipsisWidth = $this->fontMetrics->measureString($fontAlias, $fontSize, $ellipsis);
3940
$characters = $this->splitCharacters($text);
4041

41-
while ($characters !== []) {
42-
$candidate = implode('', $characters) . $ellipsis;
43-
if ($this->fontMetrics->measureString($fontAlias, $fontSize, $candidate) <= $maxWidth) {
44-
return rtrim(implode('', $characters)) . $ellipsis;
42+
foreach ($this->buildCandidates($characters) as $candidate) {
43+
if (($this->fontMetrics->measureString($fontAlias, $fontSize, $candidate) + $ellipsisWidth) <= $maxWidth) {
44+
return rtrim($candidate) . $ellipsis;
4545
}
46-
47-
array_pop($characters);
4846
}
4947

5048
return $ellipsis;
@@ -59,4 +57,21 @@ private function splitCharacters(string $text): array
5957

6058
return $characters === false ? [] : $characters;
6159
}
60+
61+
/**
62+
* @param list<string> $characters
63+
* @return list<string>
64+
*/
65+
private function buildCandidates(array $characters): array
66+
{
67+
$candidates = [];
68+
$candidate = '';
69+
70+
foreach ($characters as $character) {
71+
$candidate .= $character;
72+
$candidates[] = $candidate;
73+
}
74+
75+
return array_reverse($candidates);
76+
}
6277
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php
2+
3+
// SPDX-FileCopyrightText: 2026 LibreSign
4+
// SPDX-License-Identifier: AGPL-3.0-or-later
5+
6+
declare(strict_types=1);
7+
8+
namespace LibreSign\XObjectTemplate\Pdf;
9+
10+
use InvalidArgumentException;
11+
12+
/** @internal */
13+
final readonly class FilesystemImageSourceReader implements FilesystemImageSourceReaderInterface
14+
{
15+
private WarningToExceptionConverterInterface $warningConverter;
16+
17+
public function __construct(?WarningToExceptionConverterInterface $warningConverter = null)
18+
{
19+
$this->warningConverter = $warningConverter ?? new PhpWarningToExceptionConverter();
20+
}
21+
22+
public function read(string $source): string
23+
{
24+
$this->assertReadableSource($source);
25+
26+
$contents = $this->warningConverter->run(
27+
static fn (): string|false => file_get_contents($source),
28+
sprintf('Failed to read image source "%s".', $source),
29+
);
30+
if (!is_string($contents)) {
31+
throw new InvalidArgumentException(sprintf('Failed to read image source "%s".', $source));
32+
}
33+
34+
return $contents;
35+
}
36+
37+
private function assertReadableSource(string $source): void
38+
{
39+
if (!is_file($source)) {
40+
throw new InvalidArgumentException(sprintf('Image source "%s" must be an existing file.', $source));
41+
}
42+
43+
if (!is_readable($source)) {
44+
throw new InvalidArgumentException(sprintf('Image source "%s" must be readable.', $source));
45+
}
46+
}
47+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
3+
// SPDX-FileCopyrightText: 2026 LibreSign
4+
// SPDX-License-Identifier: AGPL-3.0-or-later
5+
6+
declare(strict_types=1);
7+
8+
namespace LibreSign\XObjectTemplate\Pdf;
9+
10+
/** @internal */
11+
interface FilesystemImageSourceReaderInterface
12+
{
13+
public function read(string $source): string;
14+
}

0 commit comments

Comments
 (0)