Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
68 / 68
100.00% covered (success)
100.00%
4 / 4
CRAP
100.00% covered (success)
100.00%
1 / 1
Flate
100.00% covered (success)
100.00%
68 / 68
100.00% covered (success)
100.00%
4 / 4
29
100.00% covered (success)
100.00%
1 / 1
 decode
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 applyPredictor
100.00% covered (success)
100.00%
42 / 42
100.00% covered (success)
100.00%
1 / 1
18
 paeth
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
4
 applyTiffPredictor
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
4
1<?php
2declare(strict_types=1);
3/**
4 * Pop PHP Framework (https://www.popphp.org/)
5 *
6 * @link       https://github.com/popphp/popphp-framework
7 * @author     Nick Sagona, III <nick@popphp.org>
8 * @copyright  Copyright (c) 2009-2026 Nick Sagona, III
9 * @license    https://www.popphp.org/license     New BSD License
10 */
11
12/**
13 * @namespace
14 */
15namespace Pop\Pdf\Extract\Filter;
16
17use Pop\Pdf\Extract\Exception;
18
19/**
20 * Pdf extract flate filter class
21 *
22 * @category   Pop
23 * @package    Pop\Pdf
24 * @author     Nick Sagona, III <nick@popphp.org>
25 * @copyright  Copyright (c) 2009-2026 Nick Sagona, III
26 * @license    https://www.popphp.org/license     New BSD License
27 * @version    6.0.0
28 */
29class Flate implements FilterInterface
30{
31
32    /**
33     * Maximum decoded stream size in bytes
34     */
35    protected const MAX_DECODED_LENGTH = 67108864;
36
37    /**
38     * Decode a FlateDecode stream, applying a PNG/TIFF predictor if specified
39     *
40     * @param  string $data
41     * @param  array  $params
42     * @throws Exception
43     * @return string
44     */
45    public function decode(string $data, array $params = []): string
46    {
47        $decoded = @gzuncompress($data, self::MAX_DECODED_LENGTH);
48
49        if ($decoded === false) {
50            $decoded = @gzinflate(substr($data, 2), self::MAX_DECODED_LENGTH);
51        }
52
53        if ($decoded === false) {
54            throw new Exception('Error: Unable to decode a FlateDecode stream.');
55        }
56
57        return $this->applyPredictor($decoded, $params);
58    }
59
60    /**
61     * Apply the PNG (per-row) or TIFF predictor to decoded stream data
62     *
63     * @param  string $data
64     * @param  array  $params
65     * @throws Exception
66     * @return string
67     */
68    protected function applyPredictor(string $data, array $params): string
69    {
70        $predictor = $params['Predictor'] ?? 1;
71
72        if ($predictor <= 1) {
73            return $data;
74        }
75
76        $columns = $params['Columns'] ?? 1;
77        $colors  = $params['Colors'] ?? 1;
78        $bpc     = $params['BitsPerComponent'] ?? 8;
79
80        // Columns/Colors/BitsPerComponent come straight out of a
81        // producer-controlled /DecodeParms dict - a non-numeric value would
82        // throw a TypeError at the applyTiffPredictor() call boundary below,
83        // and a zero/negative Columns turns $rowLen into a value that never
84        // advances the loop offset in applyTiffPredictor(), hanging forever
85        // on otherwise-legitimate data. Reject all of that up front instead.
86        if (!is_numeric($columns) || !is_numeric($colors) || !is_numeric($bpc) ||
87            ($columns <= 0) || ($colors <= 0) || ($bpc <= 0)) {
88            throw new Exception('Error: Invalid predictor parameters for a FlateDecode stream.');
89        }
90
91        $bpp     = (int) ceil(($colors * $bpc) / 8);
92        $rowLen  = (int) ceil(($colors * $bpc * $columns) / 8);
93
94        if ($predictor == 2) {
95            return $this->applyTiffPredictor($data, $columns, $colors, $bpc);
96        }
97
98        $out    = '';
99        $prev   = str_repeat("\0", $rowLen);
100        $offset = 0;
101        $length = strlen($data);
102
103        while (($offset + 1 + $rowLen) <= $length) {
104            $filterType = ord($data[$offset]);
105            $row        = substr($data, $offset + 1, $rowLen);
106            $offset    += 1 + $rowLen;
107            $decodedRow = '';
108
109            for ($i = 0; $i < $rowLen; $i++) {
110                $raw = ord($row[$i]);
111                $a   = ($i >= $bpp) ? ord($decodedRow[$i - $bpp]) : 0;
112                $b   = ord($prev[$i]);
113                $c   = ($i >= $bpp) ? ord($prev[$i - $bpp]) : 0;
114
115                if ($filterType === 0) {
116                    $value = $raw;
117                } elseif ($filterType === 1) {
118                    $value = $raw + $a;
119                } elseif ($filterType === 2) {
120                    $value = $raw + $b;
121                } elseif ($filterType === 3) {
122                    $value = $raw + (int) floor(($a + $b) / 2);
123                } elseif ($filterType === 4) {
124                    $value = $raw + $this->paeth($a, $b, $c);
125                } else {
126                    $value = $raw;
127                }
128
129                $decodedRow .= chr($value & 0xFF);
130            }
131
132            $out .= $decodedRow;
133            $prev = $decodedRow;
134        }
135
136        return $out;
137    }
138
139    /**
140     * Compute the PNG Paeth predictor value
141     *
142     * @param  int $a
143     * @param  int $b
144     * @param  int $c
145     * @return int
146     */
147    protected function paeth(int $a, int $b, int $c): int
148    {
149        $p  = $a + $b - $c;
150        $pa = abs($p - $a);
151        $pb = abs($p - $b);
152        $pc = abs($p - $c);
153
154        if (($pa <= $pb) && ($pa <= $pc)) {
155            return $a;
156        } elseif ($pb <= $pc) {
157            return $b;
158        }
159
160        return $c;
161    }
162
163    /**
164     * Apply the TIFF (horizontal differencing) predictor to decoded stream data
165     *
166     * @param  string $data
167     * @param  int    $columns
168     * @param  int    $colors
169     * @param  int    $bpc
170     * @return string
171     */
172    protected function applyTiffPredictor(string $data, int $columns, int $colors, int $bpc): string
173    {
174        if ($bpc != 8) {
175            return $data;
176        }
177
178        $rowLen = $columns * $colors;
179        $out    = '';
180        $length = strlen($data);
181
182        for ($offset = 0; $offset < $length; $offset += $rowLen) {
183            $row = substr($data, $offset, $rowLen);
184            for ($i = $colors; $i < strlen($row); $i++) {
185                $row[$i] = chr((ord($row[$i]) + ord($row[$i - $colors])) & 0xFF);
186            }
187            $out .= $row;
188        }
189
190        return $out;
191    }
192
193}