Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
62 / 62
100.00% covered (success)
100.00%
3 / 3
CRAP
100.00% covered (success)
100.00%
1 / 1
Lzw
100.00% covered (success)
100.00%
62 / 62
100.00% covered (success)
100.00%
3 / 3
19
100.00% covered (success)
100.00%
1 / 1
 decode
100.00% covered (success)
100.00%
39 / 39
100.00% covered (success)
100.00%
1 / 1
14
 initialTable
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 readCode
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
3
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 lzw 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 Lzw implements FilterInterface
30{
31
32    /**
33     * LZW clear-table code
34     */
35    protected const CLEAR = 256;
36
37    /**
38     * LZW end-of-data code
39     */
40    protected const EOD = 257;
41
42    /**
43     * Maximum decoded stream size in bytes
44     */
45    protected const MAX_DECODED_LENGTH = 67108864;
46
47    /**
48     * Decode an LZWDecode stream
49     *
50     * @param  string $data
51     * @param  array  $params
52     * @throws Exception
53     * @return string
54     */
55    public function decode(string $data, array $params = []): string
56    {
57        $earlyChange = $params['EarlyChange'] ?? 1;
58        $bytes       = strlen($data);
59        $bitPos      = 0;
60        $codeWidth   = 9;
61        $table       = $this->initialTable();
62        $prev        = null;
63        $out         = '';
64
65        while (true) {
66            $code = $this->readCode($data, $bytes, $bitPos, $codeWidth);
67
68            if (($code === null) || ($code === self::EOD)) {
69                break;
70            }
71
72            if ($code === self::CLEAR) {
73                $table     = $this->initialTable();
74                $codeWidth = 9;
75                $prev      = null;
76                continue;
77            }
78
79            if (isset($table[$code])) {
80                $entry = $table[$code];
81            } elseif (($code === count($table)) && ($prev !== null)) {
82                $entry = $prev . $prev[0];
83            } else {
84                throw new Exception('Error: Invalid LZW code encountered.');
85            }
86
87            $out .= $entry;
88
89            if (strlen($out) > self::MAX_DECODED_LENGTH) {
90                throw new Exception('Error: Decoded LZWDecode stream exceeds the maximum allowed size.');
91            }
92
93            if ($prev !== null) {
94                // Codes above 4095 are unreachable at the max 12-bit code
95                // width, so a stream that never sends an explicit Clear code
96                // must not be allowed to grow the table without bound -
97                // treat hitting the cap as an implicit clear/reset instead.
98                if (count($table) >= 4096) {
99                    $table     = $this->initialTable();
100                    $codeWidth = 9;
101                } else {
102                    $table[] = $prev . $entry[0];
103                }
104            }
105
106            $prev = $entry;
107
108            $nextSize = count($table) + $earlyChange;
109            if ($nextSize > 2047) {
110                $codeWidth = 12;
111            } elseif ($nextSize > 1023) {
112                $codeWidth = 11;
113            } elseif ($nextSize > 511) {
114                $codeWidth = 10;
115            } else {
116                $codeWidth = 9;
117            }
118        }
119
120        return $out;
121    }
122
123    /**
124     * Build the initial 256-entry single-byte LZW table
125     *
126     * @return array
127     */
128    protected function initialTable(): array
129    {
130        $table = [];
131        for ($i = 0; $i < 256; $i++) {
132            $table[$i] = chr($i);
133        }
134        $table[256] = '';
135        $table[257] = '';
136
137        return $table;
138    }
139
140    /**
141     * Read the next fixed-width code from the bitstream
142     *
143     * @param  string $data
144     * @param  int    $bytes
145     * @param  int    $bitPos
146     * @param  int    $codeWidth
147     * @return ?int
148     */
149    protected function readCode(string $data, int $bytes, int &$bitPos, int $codeWidth): ?int
150    {
151        $value    = 0;
152        $bitsRead = 0;
153
154        while ($bitsRead < $codeWidth) {
155            $bytePos = intdiv($bitPos, 8);
156            if ($bytePos >= $bytes) {
157                return null;
158            }
159
160            $bitOffset  = $bitPos % 8;
161            $bitsLeft   = 8 - $bitOffset;
162            $bitsToTake = min($bitsLeft, $codeWidth - $bitsRead);
163            $byte       = ord($data[$bytePos]);
164            $shift      = $bitsLeft - $bitsToTake;
165            $mask       = (1 << $bitsToTake) - 1;
166            $chunk      = ($byte >> $shift) & $mask;
167
168            $value     = ($value << $bitsToTake) | $chunk;
169            $bitsRead += $bitsToTake;
170            $bitPos   += $bitsToTake;
171        }
172
173        return $value;
174    }
175
176}