Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
28 / 28
100.00% covered (success)
100.00%
3 / 3
CRAP
100.00% covered (success)
100.00%
1 / 1
TextLayout
100.00% covered (success)
100.00%
28 / 28
100.00% covered (success)
100.00%
3 / 3
13
100.00% covered (success)
100.00%
1 / 1
 measure
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
2
 wrap
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
10
 winAnsi
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3declare(strict_types=1);
4
5namespace Phpdftk\Pdf\Writer;
6
7use Phpdftk\Encoding\WinAnsiTable;
8use Phpdftk\FontMetrics\AfmData;
9
10/**
11 * Shared text-layout primitives — greedy word wrapping and width
12 * measurement against AFM metrics. Used by `Pdf` and by the shared
13 * primitive renderers (`TableRenderer`, `ListRenderer`, etc.) so flow
14 * layout and explicit placement agree on geometry.
15 *
16 * Width measurement assumes WinAnsi-encoded byte input: the caller is
17 * responsible for encoding UTF-8 to the font's byte encoding before
18 * calling `measure()` / `wrap()`. This matches what `ContentStream::showText()`
19 * expects when the caller passes a resource-name string (no auto-encoding).
20 */
21final class TextLayout
22{
23    /** @var array<int, string> WinAnsi byte → glyph name, cached after first call. */
24    private static ?array $winAnsi = null;
25
26    /**
27     * Measure a single line of byte-encoded text in points.
28     */
29    public static function measure(string $text, AfmData $metrics, float $size): float
30    {
31        $winAnsi = self::winAnsi();
32        $units = 0;
33        $len = strlen($text);
34        for ($i = 0; $i < $len; $i++) {
35            $byte = ord($text[$i]);
36            $glyph = $winAnsi[$byte] ?? '.notdef';
37            $units += $metrics->getWidth($glyph);
38        }
39        return ($units / 1000.0) * $size;
40    }
41
42    /**
43     * Greedy word-wrap: split on whitespace, pack words onto lines
44     * until the next word would overflow the column width. A single
45     * word wider than the column is emitted on its own line without
46     * mid-word breaking. Explicit newlines produce paragraph breaks
47     * within the returned line list.
48     *
49     * @return list<string>
50     */
51    public static function wrap(string $text, AfmData $metrics, float $size, float $columnWidth): array
52    {
53        $out = [];
54        $text = str_replace(["\r\n", "\r"], "\n", $text);
55        $paragraphs = explode("\n", $text);
56
57        foreach ($paragraphs as $paragraph) {
58            $words = preg_split('/\s+/', trim($paragraph)) ?: [];
59            if ($words === [''] || $words === []) {
60                $out[] = '';
61                continue;
62            }
63            $line = '';
64            foreach ($words as $word) {
65                $candidate = $line === '' ? $word : ($line . ' ' . $word);
66                if (self::measure($candidate, $metrics, $size) <= $columnWidth) {
67                    $line = $candidate;
68                } else {
69                    if ($line !== '') {
70                        $out[] = $line;
71                    }
72                    $line = $word;
73                }
74            }
75            if ($line !== '') {
76                $out[] = $line;
77            }
78        }
79        return $out;
80    }
81
82    /** @return array<int, string> */
83    private static function winAnsi(): array
84    {
85        return self::$winAnsi ??= WinAnsiTable::getTable();
86    }
87}