Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
42 / 42
100.00% covered (success)
100.00%
6 / 6
CRAP
100.00% covered (success)
100.00%
1 / 1
ObjectSerializer
100.00% covered (success)
100.00%
42 / 42
100.00% covered (success)
100.00%
6 / 6
27
100.00% covered (success)
100.00%
1 / 1
 serializeValue
100.00% covered (success)
100.00%
19 / 19
100.00% covered (success)
100.00%
1 / 1
12
 serializeDict
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 serializeArray
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 formatFloat
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
5
 escapeName
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
5
 escapeLiteralString
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
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\Build\Import;
16
17use Pop\Pdf\Build\Exception;
18use 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 */
34class 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}