Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/pypdf/generic/_utils.py: 45%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1import codecs
2from dataclasses import dataclass
3from typing import Union
5from .._codecs import _pdfdoc_encoding
6from .._utils import StreamType, logger_warning, read_non_whitespace
7from ..errors import STREAM_TRUNCATED_PREMATURELY, PdfStreamError
8from ._base import ByteStringObject, TextStringObject
11def hex_to_rgb(value: str) -> tuple[float, float, float]:
12 return tuple(int(value.lstrip("#")[i : i + 2], 16) / 255.0 for i in (0, 2, 4)) # type: ignore[return-value]
15def read_hex_string_from_stream(
16 stream: StreamType,
17 forced_encoding: Union[str, list[str], dict[int, str], None] = None,
18) -> Union["TextStringObject", "ByteStringObject"]:
19 stream.read(1)
20 arr = bytearray()
21 x = b""
22 while True:
23 tok = read_non_whitespace(stream)
24 if not tok:
25 raise PdfStreamError(STREAM_TRUNCATED_PREMATURELY)
26 if tok == b">":
27 break
28 if tok not in b"0123456789abcdefABCDEF":
29 raise PdfStreamError(
30 f"Invalid hexadecimal character {tok!r} in hex string"
31 )
32 x += tok
33 if len(x) == 2:
34 arr.append(int(x, base=16))
35 x = b""
36 if len(x) == 1:
37 x += b"0"
38 if x != b"":
39 arr.append(int(x, base=16))
40 return create_string_object(bytes(arr), forced_encoding)
43__ESCAPE_DICT__ = {
44 b"n": ord(b"\n"),
45 b"r": ord(b"\r"),
46 b"t": ord(b"\t"),
47 b"b": ord(b"\b"),
48 b"f": ord(b"\f"),
49 b"(": ord(b"("),
50 b")": ord(b")"),
51 b"/": ord(b"/"),
52 b"\\": ord(b"\\"),
53 b" ": ord(b" "),
54 b"%": ord(b"%"),
55 b"<": ord(b"<"),
56 b">": ord(b">"),
57 b"[": ord(b"["),
58 b"]": ord(b"]"),
59 b"#": ord(b"#"),
60 b"_": ord(b"_"),
61 b"&": ord(b"&"),
62 b"$": ord(b"$"),
63}
64__BACKSLASH_CODE__ = 92
67def read_string_from_stream(
68 stream: StreamType,
69 forced_encoding: Union[str, list[str], dict[int, str], None] = None,
70) -> Union["TextStringObject", "ByteStringObject"]:
71 tok = stream.read(1)
72 parens = 1
73 txt = []
74 while True:
75 tok = stream.read(1)
76 if not tok:
77 raise PdfStreamError(STREAM_TRUNCATED_PREMATURELY)
78 if tok == b"(":
79 parens += 1
80 elif tok == b")":
81 parens -= 1
82 if parens == 0:
83 break
84 elif tok == b"\\":
85 tok = stream.read(1)
86 try:
87 txt.append(__ESCAPE_DICT__[tok])
88 continue
89 except KeyError:
90 if b"0" <= tok <= b"7":
91 # "The number ddd may consist of one, two, or three
92 # octal digits; high-order overflow shall be ignored.
93 # Three octal digits shall be used, with leading zeros
94 # as needed, if the next character of the string is also
95 # a digit." (PDF reference 7.3.4.2, p 16)
96 sav = stream.tell() - 1
97 for _ in range(2):
98 ntok = stream.read(1)
99 if b"0" <= ntok <= b"7":
100 tok += ntok
101 else:
102 stream.seek(-1, 1) # ntok has to be analyzed
103 break
104 i = int(tok, base=8)
105 if i > 255:
106 txt.append(__BACKSLASH_CODE__)
107 stream.seek(sav)
108 else:
109 txt.append(i)
110 continue
111 if tok in b"\n\r":
112 # This case is hit when a backslash followed by a line
113 # break occurs. If it's a multi-char EOL, consume the
114 # second character:
115 tok = stream.read(1)
116 if tok not in b"\n\r":
117 stream.seek(-1, 1)
118 # Then don't add anything to the actual string, since this
119 # line break was escaped:
120 continue
121 logger_warning(
122 "Unexpected escaped string: %(token)s",
123 source=__name__,
124 token=tok.decode("utf-8", "ignore"),
125 )
126 txt.append(__BACKSLASH_CODE__)
127 txt.append(ord(tok))
128 return create_string_object(bytes(txt), forced_encoding)
131def create_string_object(
132 string: Union[str, bytes],
133 forced_encoding: Union[str, list[str], dict[int, str], None] = None,
134) -> Union[TextStringObject, ByteStringObject]:
135 """
136 Create a ByteStringObject or a TextStringObject from a string to represent the string.
138 Args:
139 string: The data being used
140 forced_encoding: Typically None, or an encoding string
142 Returns:
143 A ByteStringObject
145 Raises:
146 TypeError: If string is not of type str or bytes.
148 """
149 if isinstance(string, str):
150 return TextStringObject(string)
151 if isinstance(string, bytes):
152 if isinstance(forced_encoding, (list, dict)):
153 out = []
155 if isinstance(forced_encoding, list):
156 list_length = len(forced_encoding)
157 else:
158 list_length = None
160 for x in string:
161 if (
162 (list_length is not None and x < list_length) or
163 (list_length is None and x in forced_encoding)
164 ):
165 value = forced_encoding[x]
166 else:
167 value = bytes((x,)).decode("charmap")
168 out.append(value)
169 obj = TextStringObject("".join(out))
170 obj._original_bytes = string
171 return obj
172 if isinstance(forced_encoding, str):
173 if forced_encoding == "bytes":
174 return ByteStringObject(string)
175 obj = TextStringObject(string.decode(forced_encoding))
176 obj._original_bytes = string
177 return obj
178 try:
179 if string.startswith((codecs.BOM_UTF16_BE, codecs.BOM_UTF16_LE)):
180 retval = TextStringObject(string.decode("utf-16"))
181 retval._original_bytes = string
182 retval.autodetect_utf16 = True
183 retval.utf16_bom = string[:2]
184 return retval
185 if string.startswith(b"\x00"):
186 retval = TextStringObject(string.decode("utf-16be"))
187 retval._original_bytes = string
188 retval.autodetect_utf16 = True
189 retval.utf16_bom = codecs.BOM_UTF16_BE
190 return retval
191 if string[1:2] == b"\x00":
192 retval = TextStringObject(string.decode("utf-16le"))
193 retval._original_bytes = string
194 retval.autodetect_utf16 = True
195 retval.utf16_bom = codecs.BOM_UTF16_LE
196 return retval
198 # This is probably a big performance hit here, but we need
199 # to convert string objects into the text/unicode-aware
200 # version if possible... and the only way to check if that's
201 # possible is to try.
202 # Some strings are strings, some are just byte arrays.
203 retval = TextStringObject(decode_pdfdocencoding(string))
204 retval._original_bytes = string
205 retval.autodetect_pdfdocencoding = True
206 return retval
207 except UnicodeDecodeError:
208 return ByteStringObject(string)
209 else:
210 raise TypeError("create_string_object should have str or unicode arg")
213def decode_pdfdocencoding(byte_array: bytes) -> str:
214 retval = ""
215 for b in byte_array:
216 c = _pdfdoc_encoding[b]
217 if c == "\u0000":
218 raise UnicodeDecodeError(
219 "pdfdocencoding",
220 bytearray(b),
221 -1,
222 -1,
223 "does not exist in translation table",
224 )
225 retval += c
226 return retval
229@dataclass
230class StreamParsingBudget:
231 """
232 Shared, mutable budget for limiting parsing work.
234 Pass the same instance to recursive parsing calls to enforce a
235 cumulative budget across nested arrays and dictionaries.
236 """
237 remaining_budget: int