Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
46 / 46
100.00% covered (success)
100.00%
8 / 8
CRAP
100.00% covered (success)
100.00%
1 / 1
RasterSurface
100.00% covered (success)
100.00%
46 / 46
100.00% covered (success)
100.00%
8 / 8
15
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 getPixel
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
1
 setPixel
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 clear
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 buffer
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setBuffer
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
2
 byteSize
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 assertInBounds
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
5
1<?php
2
3declare(strict_types=1);
4
5namespace Phpdftk\Raster;
6
7use Phpdftk\Raster\Exception\RasterException;
8
9/**
10 * Mutable RGBA pixel buffer — the substrate everything else in this
11 * package paints into.
12 *
13 * Storage. Pixels live in a flat byte string with 4 bytes per pixel
14 * (R, G, B, A in that order), row-major (left-to-right, top-to-
15 * bottom). A 1024×1024 surface is ~4 MB. For hot-loop filter
16 * primitives, {@see buffer()} returns the raw string so callers can
17 * iterate without per-pixel call overhead.
18 *
19 * Coordinate convention. Y points down (0 at the top), matching SVG
20 * / CSS / WHATWG canvas. The exporter (4C.5) handles the SVG↔PDF
21 * flip when emitting the PDF Image XObject.
22 */
23final class RasterSurface
24{
25    private string $buffer;
26
27    public function __construct(
28        public readonly int $width,
29        public readonly int $height,
30    ) {
31        if ($width <= 0 || $height <= 0) {
32            throw new \InvalidArgumentException(sprintf(
33                'RasterSurface dimensions must be positive; got %d × %d',
34                $width,
35                $height,
36            ));
37        }
38        // Initialise to fully-transparent black. `str_repeat` is
39        // ~5x faster than a per-pixel zero loop on PHP 8.4.
40        $this->buffer = str_repeat("\0\0\0\0", $width * $height);
41    }
42
43    /**
44     * Read the pixel at `(x, y)`. Throws if out of bounds — callers
45     * are expected to clip before reading; cheap silent fallback
46     * would mask real bugs.
47     */
48    public function getPixel(int $x, int $y): RgbaPixel
49    {
50        $this->assertInBounds($x, $y);
51        $offset = ($y * $this->width + $x) * 4;
52        return new RgbaPixel(
53            r: ord($this->buffer[$offset]),
54            g: ord($this->buffer[$offset + 1]),
55            b: ord($this->buffer[$offset + 2]),
56            a: ord($this->buffer[$offset + 3]),
57        );
58    }
59
60    /**
61     * Write a pixel. Throws if out of bounds — same posture as
62     * {@see getPixel}.
63     */
64    public function setPixel(int $x, int $y, RgbaPixel $pixel): void
65    {
66        $this->assertInBounds($x, $y);
67        $offset = ($y * $this->width + $x) * 4;
68        $this->buffer[$offset] = chr($pixel->r);
69        $this->buffer[$offset + 1] = chr($pixel->g);
70        $this->buffer[$offset + 2] = chr($pixel->b);
71        $this->buffer[$offset + 3] = chr($pixel->a);
72    }
73
74    /**
75     * Fill the entire surface with one colour. ~10× faster than a
76     * per-pixel loop for large surfaces; useful for flood-fill
77     * filter primitives + initial background.
78     */
79    public function clear(RgbaPixel $pixel): void
80    {
81        $row = str_repeat(
82            chr($pixel->r) . chr($pixel->g) . chr($pixel->b) . chr($pixel->a),
83            $this->width,
84        );
85        $this->buffer = str_repeat($row, $this->height);
86    }
87
88    /**
89     * Raw byte access. The returned string is `width * height * 4`
90     * bytes, row-major RGBA. Hot-loop filter implementations operate
91     * on this directly to avoid per-pixel call overhead.
92     *
93     * The returned string is a copy (PHP string semantics) — modify
94     * locally and call {@see setBuffer} to commit back.
95     */
96    public function buffer(): string
97    {
98        return $this->buffer;
99    }
100
101    /**
102     * Replace the entire pixel buffer. The replacement must have
103     * exactly `width * height * 4` bytes — anything else is a
104     * programming error and throws.
105     */
106    public function setBuffer(string $bytes): void
107    {
108        $expected = $this->width * $this->height * 4;
109        if (strlen($bytes) !== $expected) {
110            throw new RasterException(sprintf(
111                'Buffer size mismatch: expected %d bytes (%d × %d × 4), got %d',
112                $expected,
113                $this->width,
114                $this->height,
115                strlen($bytes),
116            ));
117        }
118        $this->buffer = $bytes;
119    }
120
121    /**
122     * Total byte size of the pixel buffer. Useful when budgeting
123     * for cache eviction policy in the surface dedupe cache (4C.6).
124     */
125    public function byteSize(): int
126    {
127        return $this->width * $this->height * 4;
128    }
129
130    private function assertInBounds(int $x, int $y): void
131    {
132        if ($x < 0 || $x >= $this->width || $y < 0 || $y >= $this->height) {
133            throw new RasterException(sprintf(
134                'Pixel (%d, %d) out of bounds for %d × %d surface',
135                $x,
136                $y,
137                $this->width,
138                $this->height,
139            ));
140        }
141    }
142}