Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
98.04% covered (success)
98.04%
50 / 51
66.67% covered (warning)
66.67%
2 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
ListRenderer
98.04% covered (success)
98.04%
50 / 51
66.67% covered (warning)
66.67%
2 / 3
10
0.00% covered (danger)
0.00%
0 / 1
 measureItem
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 drawItem
96.30% covered (success)
96.30%
26 / 27
0.00% covered (danger)
0.00%
0 / 1
6
 drawBlock
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2
3declare(strict_types=1);
4
5namespace Phpdftk\Pdf\Writer;
6
7use Phpdftk\FontMetrics\AfmData;
8use Phpdftk\Pdf\Core\Content\ContentStream;
9
10/**
11 * Renders {@see ListBlock} content into a {@see ContentStream}.
12 *
13 * The renderer is stateless; pagination logic lives at the call site
14 * (`Pdf::addList` measures each item and starts a new page when one
15 * would overflow). Items are independent — measure and draw one at a
16 * time, threading a running item number for numbered lists.
17 *
18 * Coordinates: `$y` is the top of the area available to the item; the
19 * renderer draws downward and returns the height consumed.
20 */
21final class ListRenderer
22{
23    /**
24     * Height needed to render a single item, in points.
25     */
26    public function measureItem(
27        string $item,
28        float $maxWidth,
29        Font $font,
30        AfmData $metrics,
31        float $fontSize,
32        float $lineHeight,
33        ListStyle $style,
34    ): float {
35        $textWidth = max(0.0, $maxWidth - $style->indent);
36        $encoded = $font->getTextEncoder()?->encode($item) ?? $item;
37        $lines = TextLayout::wrap($encoded, $metrics, $fontSize, $textWidth);
38        $lineCount = max(1, count($lines));
39        return $lineCount * $fontSize * $lineHeight + $style->itemSpacing;
40    }
41
42    /**
43     * Draw a single item at `(x, y)`. The marker (bullet glyph or
44     * numbered prefix) sits at `$x`; the wrapped text starts at
45     * `$x + $style->indent`.
46     */
47    public function drawItem(
48        ContentStream $cs,
49        float $x,
50        float $y,
51        string $item,
52        float $maxWidth,
53        Font $font,
54        AfmData $metrics,
55        float $fontSize,
56        float $lineHeight,
57        ListStyle $style,
58        ?int $itemNumber,
59    ): float {
60        $textX = $x + $style->indent;
61        $textWidth = max(0.0, $maxWidth - $style->indent);
62
63        $marker = $itemNumber !== null
64            ? $itemNumber . $style->numberSuffix
65            : $style->bulletAt(0);
66
67        $encoder = $font->getTextEncoder();
68        $encodedMarker = $encoder?->encode($marker) ?? $marker;
69        $encodedItem = $encoder?->encode($item) ?? $item;
70
71        $lines = TextLayout::wrap($encodedItem, $metrics, $fontSize, $textWidth);
72        if ($lines === []) {
73            $lines = [''];
74        }
75
76        $lineH = $fontSize * $lineHeight;
77        foreach ($lines as $lineIdx => $line) {
78            $baselineY = $y - ($lineIdx + 1) * $lineH + ($lineH - $fontSize) / 2.0;
79
80            if ($lineIdx === 0) {
81                $cs->beginText()
82                   ->setFont($font->getResourceName(), $fontSize)
83                   ->moveTextPosition($x, $baselineY)
84                   ->showText($encodedMarker)
85                   ->endText();
86            }
87
88            if ($line !== '') {
89                $cs->beginText()
90                   ->setFont($font->getResourceName(), $fontSize)
91                   ->moveTextPosition($textX, $baselineY)
92                   ->showText($line)
93                   ->endText();
94            }
95        }
96
97        return count($lines) * $lineH + $style->itemSpacing;
98    }
99
100    /**
101     * Convenience: draw an entire {@see ListBlock} starting at `(x, y)`
102     * without pagination. Returns the height consumed. Useful for
103     * `Writer\Page::drawList()` — for flow-paginated rendering, drive
104     * the per-item loop yourself via {@see measureItem}/{@see drawItem}.
105     */
106    public function drawBlock(
107        ContentStream $cs,
108        float $x,
109        float $y,
110        ListBlock $block,
111        float $maxWidth,
112        Font $font,
113        AfmData $metrics,
114        float $fontSize,
115        float $lineHeight,
116        ListStyle $style,
117    ): float {
118        $consumed = 0.0;
119        $itemNumber = 1;
120        foreach ($block->items as $item) {
121            $h = $this->drawItem(
122                $cs,
123                $x,
124                $y - $consumed,
125                $item,
126                $maxWidth,
127                $font,
128                $metrics,
129                $fontSize,
130                $lineHeight,
131                $style,
132                $block->numbered ? $itemNumber : null,
133            );
134            $consumed += $h;
135            $itemNumber++;
136        }
137        return $consumed;
138    }
139}