Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
93.24% covered (success)
93.24%
69 / 74
50.00% covered (danger)
50.00%
1 / 2
CRAP
0.00% covered (danger)
0.00%
0 / 1
Ascii85Filter
93.24% covered (success)
93.24%
69 / 74
50.00% covered (danger)
50.00%
1 / 2
28.24
0.00% covered (danger)
0.00%
0 / 1
 encode
100.00% covered (success)
100.00%
33 / 33
100.00% covered (success)
100.00%
1 / 1
7
 decode
87.80% covered (warning)
87.80%
36 / 41
0.00% covered (danger)
0.00%
0 / 1
21.80
1<?php
2
3declare(strict_types=1);
4
5namespace Phpdftk\Filters;
6
7/**
8 * ASCII85Decode — base-85 encoding for binary data (ISO 32000-2 §7.4.3).
9 *
10 * Encodes 4 bytes into 5 ASCII characters (plus 'z' shorthand for
11 * all-zero groups), giving ~25% expansion vs ~100% for hex encoding.
12 * Commonly used for human-inspectable PDF streams where FlateDecode
13 * would be opaque.
14 */
15final class Ascii85Filter implements FilterInterface
16{
17    public function encode(string $data): string
18    {
19        $output = '';
20        $len = strlen($data);
21        $i = 0;
22        while ($i < $len) {
23            // Get up to 4 bytes
24            $remaining = $len - $i;
25            if ($remaining >= 4) {
26                $b0 = ord($data[$i]);
27                $b1 = ord($data[$i + 1]);
28                $b2 = ord($data[$i + 2]);
29                $b3 = ord($data[$i + 3]);
30                $value = ($b0 << 24) | ($b1 << 16) | ($b2 << 8) | $b3;
31                // Use unsigned 32-bit: PHP integers are 64-bit so handle sign
32                $value = $value & 0xFFFFFFFF;
33                if ($value === 0) {
34                    $output .= 'z';
35                } else {
36                    $chars = '';
37                    for ($j = 0; $j < 5; $j++) {
38                        $chars = chr(($value % 85) + 33) . $chars;
39                        $value = intdiv($value, 85);
40                    }
41                    $output .= $chars;
42                }
43                $i += 4;
44            } else {
45                // Partial last group: pad with zeros
46                $bytes = [0, 0, 0, 0];
47                for ($j = 0; $j < $remaining; $j++) {
48                    $bytes[$j] = ord($data[$i + $j]);
49                }
50                $value = ($bytes[0] << 24) | ($bytes[1] << 16) | ($bytes[2] << 8) | $bytes[3];
51                $value = $value & 0xFFFFFFFF;
52                $chars = '';
53                for ($j = 0; $j < 5; $j++) {
54                    $chars = chr(($value % 85) + 33) . $chars;
55                    $value = intdiv($value, 85);
56                }
57                // Output only $remaining + 1 chars
58                $output .= substr($chars, 0, $remaining + 1);
59                $i += $remaining;
60            }
61        }
62        $output .= '~>';
63        return $output;
64    }
65
66    public function decode(string $data): string
67    {
68        $output = '';
69        $len = strlen($data);
70        $i = 0;
71        $group = [];
72
73        while ($i < $len) {
74            $ch = $data[$i];
75
76            // Skip whitespace
77            if ($ch === ' ' || $ch === "\t" || $ch === "\n" || $ch === "\r" || $ch === "\f") {
78                $i++;
79                continue;
80            }
81
82            // End of data marker
83            if ($ch === '~') {
84                if ($i + 1 < $len && $data[$i + 1] === '>') {
85                    // Process remaining partial group
86                    if (count($group) > 0) {
87                        $n = count($group);
88                        // Pad with 'u' (84) to make 5 chars
89                        while (count($group) < 5) {
90                            $group[] = 84; // 'u' - 33
91                        }
92                        $value = 0;
93                        foreach ($group as $digit) {
94                            $value = $value * 85 + $digit;
95                        }
96                        // Output only n-1 bytes
97                        for ($j = 3; $j >= (4 - ($n - 1)); $j--) {
98                            $output .= chr(($value >> ($j * 8)) & 0xFF);
99                        }
100                    }
101                    break;
102                }
103                throw new \RuntimeException('Ascii85Filter: invalid ~ character');
104            }
105
106            // 'z' special case: all zeros
107            if ($ch === 'z') {
108                if (count($group) !== 0) {
109                    throw new \RuntimeException('Ascii85Filter: z not at start of group');
110                }
111                $output .= "\x00\x00\x00\x00";
112                $i++;
113                continue;
114            }
115
116            // Regular character: must be in range '!' to 'u'
117            $ord = ord($ch);
118            if ($ord < 33 || $ord > 117) {
119                throw new \RuntimeException("Ascii85Filter: invalid character '$ch' (ord $ord)");
120            }
121            $group[] = $ord - 33;
122            if (count($group) === 5) {
123                $value = 0;
124                foreach ($group as $digit) {
125                    $value = $value * 85 + $digit;
126                }
127                for ($j = 3; $j >= 0; $j--) {
128                    $output .= chr(($value >> ($j * 8)) & 0xFF);
129                }
130                $group = [];
131            }
132            $i++;
133        }
134
135        return $output;
136    }
137}