1<?php
2/**
3 * This file is part of FPDI
4 *
5 * @package   setasign\Fpdi
6 * @copyright Copyright (c) 2020 Setasign GmbH & Co. KG (https://www.setasign.com)
7 * @license   http://opensource.org/licenses/mit-license The MIT License
8 */
9
10namespace setasign\Fpdi\PdfParser\Type;
11
12use setasign\Fpdi\PdfParser\StreamReader;
13
14/**
15 * Class representing a PDF string object
16 *
17 * @package setasign\Fpdi\PdfParser\Type
18 */
19class PdfString extends PdfType
20{
21    /**
22     * Parses a string object from the stream reader.
23     *
24     * @param StreamReader $streamReader
25     * @return self
26     */
27    public static function parse(StreamReader $streamReader)
28    {
29        $pos = $startPos = $streamReader->getOffset();
30        $openBrackets = 1;
31        do {
32            $buffer = $streamReader->getBuffer(false);
33            for ($length = \strlen($buffer); $openBrackets !== 0 && $pos < $length; $pos++) {
34                switch ($buffer[$pos]) {
35                    case '(':
36                        $openBrackets++;
37                        break;
38                    case ')':
39                        $openBrackets--;
40                        break;
41                    case '\\':
42                        $pos++;
43                }
44            }
45        } while ($openBrackets !== 0 && $streamReader->increaseLength());
46
47        $result = \substr($buffer, $startPos, $openBrackets + $pos - $startPos - 1);
48        $streamReader->setOffset($pos);
49
50        $v = new self;
51        $v->value = $result;
52
53        return $v;
54    }
55
56    /**
57     * Helper method to create an instance.
58     *
59     * @param string $value The string needs to be escaped accordingly.
60     * @return self
61     */
62    public static function create($value)
63    {
64        $v = new self;
65        $v->value = $value;
66
67        return $v;
68    }
69
70    /**
71     * Ensures that the passed value is a PdfString instance.
72     *
73     * @param mixed $string
74     * @return self
75     * @throws PdfTypeException
76     */
77    public static function ensure($string)
78    {
79        return PdfType::ensureType(self::class, $string, 'String value expected.');
80    }
81
82    /**
83     * Unescapes escaped sequences in a PDF string according to the PDF specification.
84     *
85     * @param string $s
86     * @return string
87     */
88    public static function unescape($s)
89    {
90        $out = '';
91        /** @noinspection ForeachInvariantsInspection */
92        for ($count = 0, $n = \strlen($s); $count < $n; $count++) {
93            if ($s[$count] !== '\\') {
94                $out .= $s[$count];
95            } else {
96                // A backslash at the end of the string - ignore it
97                if ($count === ($n - 1)) {
98                    break;
99                }
100
101                switch ($s[++$count]) {
102                    case ')':
103                    case '(':
104                    case '\\':
105                        $out .= $s[$count];
106                        break;
107
108                    case 'f':
109                        $out .= "\x0C";
110                        break;
111
112                    case 'b':
113                        $out .= "\x08";
114                        break;
115
116                    case 't':
117                        $out .= "\x09";
118                        break;
119
120                    case 'r':
121                        $out .= "\x0D";
122                        break;
123
124                    case 'n':
125                        $out .= "\x0A";
126                        break;
127
128                    case "\r":
129                        if ($count !== $n - 1 && $s[$count + 1] === "\n") {
130                            $count++;
131                        }
132                        break;
133
134                    case "\n":
135                        break;
136
137                    default:
138                        $actualChar = \ord($s[$count]);
139                        // ascii 48 = number 0
140                        // ascii 57 = number 9
141                        if ($actualChar >= 48 &&
142                            $actualChar <= 57) {
143                            $oct = '' . $s[$count];
144
145                            /** @noinspection NotOptimalIfConditionsInspection */
146                            if ($count + 1 < $n &&
147                                \ord($s[$count + 1]) >= 48 &&
148                                \ord($s[$count + 1]) <= 57
149                            ) {
150                                $count++;
151                                $oct .= $s[$count];
152
153                                /** @noinspection NotOptimalIfConditionsInspection */
154                                if ($count + 1 < $n &&
155                                    \ord($s[$count + 1]) >= 48 &&
156                                    \ord($s[$count + 1]) <= 57
157                                ) {
158                                    $oct .= $s[++$count];
159                                }
160                            }
161
162                            $out .= \chr(\octdec($oct));
163                        } else {
164                            // If the character is not one of those defined, the backslash is ignored
165                            $out .= $s[$count];
166                        }
167                }
168            }
169        }
170        return $out;
171    }
172}
173