Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
100.00% |
42 / 42 |
|
100.00% |
6 / 6 |
CRAP | |
100.00% |
1 / 1 |
| ObjectSerializer | |
100.00% |
42 / 42 |
|
100.00% |
6 / 6 |
27 | |
100.00% |
1 / 1 |
| serializeValue | |
100.00% |
19 / 19 |
|
100.00% |
1 / 1 |
12 | |||
| serializeDict | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
| serializeArray | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
| formatFloat | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
5 | |||
| escapeName | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
5 | |||
| escapeLiteralString | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| 1 | <?php |
| 2 | declare(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 | */ |
| 15 | namespace Pop\Pdf\Build\Import; |
| 16 | |
| 17 | use Pop\Pdf\Build\Exception; |
| 18 | use Pop\Pdf\Extract\Value; |
| 19 | |
| 20 | /** |
| 21 | * Pdf build import object serializer class |
| 22 | * |
| 23 | * Turns a decoded Extract\Value tree (dict/array/Name/Reference/Keyword/scalar) |
| 24 | * back into raw PDF object syntax - the write-side counterpart Extract\* never |
| 25 | * needed for read-only text extraction. |
| 26 | * |
| 27 | * @category Pop |
| 28 | * @package Pop\Pdf |
| 29 | * @author Nick Sagona, III <nick@popphp.org> |
| 30 | * @copyright Copyright (c) 2009-2026 Nick Sagona, III |
| 31 | * @license https://www.popphp.org/license New BSD License |
| 32 | * @version 6.0.0 |
| 33 | */ |
| 34 | class ObjectSerializer |
| 35 | { |
| 36 | |
| 37 | /** |
| 38 | * Serialize a single decoded value into PDF syntax |
| 39 | * |
| 40 | * @param mixed $value |
| 41 | * @throws Exception |
| 42 | * @return string |
| 43 | */ |
| 44 | public static function serializeValue(mixed $value): string |
| 45 | { |
| 46 | if ($value instanceof Value\Reference) { |
| 47 | return $value->objNum . ' 0 R'; |
| 48 | } elseif ($value instanceof Value\Name) { |
| 49 | return '/' . self::escapeName($value->name); |
| 50 | } elseif ($value instanceof Value\Keyword) { |
| 51 | return $value->keyword; |
| 52 | } elseif (is_bool($value)) { |
| 53 | return $value ? 'true' : 'false'; |
| 54 | } elseif ($value === null) { |
| 55 | return 'null'; |
| 56 | } elseif (is_int($value)) { |
| 57 | return (string) $value; |
| 58 | } elseif (is_float($value)) { |
| 59 | return self::formatFloat($value); |
| 60 | } elseif (is_string($value)) { |
| 61 | return '(' . self::escapeLiteralString($value) . ')'; |
| 62 | } elseif (is_array($value)) { |
| 63 | return array_is_list($value) ? self::serializeArray($value) : self::serializeDict($value); |
| 64 | } |
| 65 | |
| 66 | throw new Exception('Error: Cannot serialize a PDF value of type ' . get_debug_type($value) . '.'); |
| 67 | } |
| 68 | |
| 69 | /** |
| 70 | * Serialize a dictionary (associative array) |
| 71 | * |
| 72 | * @param array $dict |
| 73 | * @return string |
| 74 | */ |
| 75 | public static function serializeDict(array $dict): string |
| 76 | { |
| 77 | $parts = []; |
| 78 | |
| 79 | foreach ($dict as $key => $value) { |
| 80 | $parts[] = '/' . self::escapeName((string) $key); |
| 81 | $parts[] = self::serializeValue($value); |
| 82 | } |
| 83 | |
| 84 | return '<< ' . implode(' ', $parts) . ' >>'; |
| 85 | } |
| 86 | |
| 87 | /** |
| 88 | * Serialize an array (list) |
| 89 | * |
| 90 | * @param array $items |
| 91 | * @return string |
| 92 | */ |
| 93 | public static function serializeArray(array $items): string |
| 94 | { |
| 95 | $parts = []; |
| 96 | |
| 97 | foreach ($items as $item) { |
| 98 | $parts[] = self::serializeValue($item); |
| 99 | } |
| 100 | |
| 101 | return '[ ' . implode(' ', $parts) . ' ]'; |
| 102 | } |
| 103 | |
| 104 | /** |
| 105 | * Format a float without scientific notation or a trailing decimal point |
| 106 | * |
| 107 | * @param float $value |
| 108 | * @return string |
| 109 | */ |
| 110 | protected static function formatFloat(float $value): string |
| 111 | { |
| 112 | if ($value == (int) $value) { |
| 113 | return (string) (int) $value; |
| 114 | } |
| 115 | |
| 116 | $formatted = rtrim(rtrim(sprintf('%.6F', $value), '0'), '.'); |
| 117 | |
| 118 | return ($formatted === '' || $formatted === '-' || $formatted === '-0') ? '0' : $formatted; |
| 119 | } |
| 120 | |
| 121 | /** |
| 122 | * Escape a name's bytes per PDF name syntax (mirrors Tokenizer::readName()'s decoding, in reverse) |
| 123 | * |
| 124 | * @param string $name |
| 125 | * @return string |
| 126 | */ |
| 127 | protected static function escapeName(string $name): string |
| 128 | { |
| 129 | $escaped = ''; |
| 130 | $length = strlen($name); |
| 131 | |
| 132 | for ($i = 0; $i < $length; $i++) { |
| 133 | $c = $name[$i]; |
| 134 | $ord = ord($c); |
| 135 | |
| 136 | if (($ord <= 0x20) || ($ord >= 0x7F) || (strpos('()<>[]{}/%#', $c) !== false)) { |
| 137 | $escaped .= '#' . strtoupper(sprintf('%02x', $ord)); |
| 138 | } else { |
| 139 | $escaped .= $c; |
| 140 | } |
| 141 | } |
| 142 | |
| 143 | return $escaped; |
| 144 | } |
| 145 | |
| 146 | /** |
| 147 | * Escape a literal string's backslashes and parentheses |
| 148 | * |
| 149 | * @param string $value |
| 150 | * @return string |
| 151 | */ |
| 152 | protected static function escapeLiteralString(string $value): string |
| 153 | { |
| 154 | return str_replace(['\\', '(', ')'], ['\\\\', '\\(', '\\)'], $value); |
| 155 | } |
| 156 | |
| 157 | } |