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

135 statements  

1import codecs 

2from dataclasses import dataclass 

3from typing import Union 

4 

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 

9 

10 

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] 

13 

14 

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) 

41 

42 

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 

65 

66 

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) 

129 

130 

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. 

137 

138 Args: 

139 string: The data being used 

140 forced_encoding: Typically None, or an encoding string 

141 

142 Returns: 

143 A ByteStringObject 

144 

145 Raises: 

146 TypeError: If string is not of type str or bytes. 

147 

148 """ 

149 if isinstance(string, str): 

150 return TextStringObject(string) 

151 if isinstance(string, bytes): 

152 if isinstance(forced_encoding, (list, dict)): 

153 out = [] 

154 

155 if isinstance(forced_encoding, list): 

156 list_length = len(forced_encoding) 

157 else: 

158 list_length = None 

159 

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 

197 

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") 

211 

212 

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 

227 

228 

229@dataclass 

230class StreamParsingBudget: 

231 """ 

232 Shared, mutable budget for limiting parsing work. 

233 

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