_utils.py 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  1. import codecs
  2. from typing import Union
  3. from .._codecs import _pdfdoc_encoding
  4. from .._utils import StreamType, logger_warning, read_non_whitespace
  5. from ..errors import STREAM_TRUNCATED_PREMATURELY, PdfStreamError
  6. from ._base import ByteStringObject, TextStringObject
  7. def hex_to_rgb(value: str) -> tuple[float, float, float]:
  8. return tuple(int(value.lstrip("#")[i : i + 2], 16) / 255.0 for i in (0, 2, 4)) # type: ignore
  9. def read_hex_string_from_stream(
  10. stream: StreamType,
  11. forced_encoding: Union[None, str, list[str], dict[int, str]] = None,
  12. ) -> Union["TextStringObject", "ByteStringObject"]:
  13. stream.read(1)
  14. arr = []
  15. x = b""
  16. while True:
  17. tok = read_non_whitespace(stream)
  18. if not tok:
  19. raise PdfStreamError(STREAM_TRUNCATED_PREMATURELY)
  20. if tok == b">":
  21. break
  22. x += tok
  23. if len(x) == 2:
  24. arr.append(int(x, base=16))
  25. x = b""
  26. if len(x) == 1:
  27. x += b"0"
  28. if x != b"":
  29. arr.append(int(x, base=16))
  30. return create_string_object(bytes(arr), forced_encoding)
  31. __ESCAPE_DICT__ = {
  32. b"n": ord(b"\n"),
  33. b"r": ord(b"\r"),
  34. b"t": ord(b"\t"),
  35. b"b": ord(b"\b"),
  36. b"f": ord(b"\f"),
  37. b"(": ord(b"("),
  38. b")": ord(b")"),
  39. b"/": ord(b"/"),
  40. b"\\": ord(b"\\"),
  41. b" ": ord(b" "),
  42. b"%": ord(b"%"),
  43. b"<": ord(b"<"),
  44. b">": ord(b">"),
  45. b"[": ord(b"["),
  46. b"]": ord(b"]"),
  47. b"#": ord(b"#"),
  48. b"_": ord(b"_"),
  49. b"&": ord(b"&"),
  50. b"$": ord(b"$"),
  51. }
  52. __BACKSLASH_CODE__ = 92
  53. def read_string_from_stream(
  54. stream: StreamType,
  55. forced_encoding: Union[None, str, list[str], dict[int, str]] = None,
  56. ) -> Union["TextStringObject", "ByteStringObject"]:
  57. tok = stream.read(1)
  58. parens = 1
  59. txt = []
  60. while True:
  61. tok = stream.read(1)
  62. if not tok:
  63. raise PdfStreamError(STREAM_TRUNCATED_PREMATURELY)
  64. if tok == b"(":
  65. parens += 1
  66. elif tok == b")":
  67. parens -= 1
  68. if parens == 0:
  69. break
  70. elif tok == b"\\":
  71. tok = stream.read(1)
  72. try:
  73. txt.append(__ESCAPE_DICT__[tok])
  74. continue
  75. except KeyError:
  76. if b"0" <= tok <= b"7":
  77. # "The number ddd may consist of one, two, or three
  78. # octal digits; high-order overflow shall be ignored.
  79. # Three octal digits shall be used, with leading zeros
  80. # as needed, if the next character of the string is also
  81. # a digit." (PDF reference 7.3.4.2, p 16)
  82. sav = stream.tell() - 1
  83. for _ in range(2):
  84. ntok = stream.read(1)
  85. if b"0" <= ntok <= b"7":
  86. tok += ntok
  87. else:
  88. stream.seek(-1, 1) # ntok has to be analyzed
  89. break
  90. i = int(tok, base=8)
  91. if i > 255:
  92. txt.append(__BACKSLASH_CODE__)
  93. stream.seek(sav)
  94. else:
  95. txt.append(i)
  96. continue
  97. if tok in b"\n\r":
  98. # This case is hit when a backslash followed by a line
  99. # break occurs. If it's a multi-char EOL, consume the
  100. # second character:
  101. tok = stream.read(1)
  102. if tok not in b"\n\r":
  103. stream.seek(-1, 1)
  104. # Then don't add anything to the actual string, since this
  105. # line break was escaped:
  106. continue
  107. msg = f"Unexpected escaped string: {tok.decode('utf-8', 'ignore')}"
  108. logger_warning(msg, __name__)
  109. txt.append(__BACKSLASH_CODE__)
  110. txt.append(ord(tok))
  111. return create_string_object(bytes(txt), forced_encoding)
  112. def create_string_object(
  113. string: Union[str, bytes],
  114. forced_encoding: Union[None, str, list[str], dict[int, str]] = None,
  115. ) -> Union[TextStringObject, ByteStringObject]:
  116. """
  117. Create a ByteStringObject or a TextStringObject from a string to represent the string.
  118. Args:
  119. string: The data being used
  120. forced_encoding: Typically None, or an encoding string
  121. Returns:
  122. A ByteStringObject
  123. Raises:
  124. TypeError: If string is not of type str or bytes.
  125. """
  126. if isinstance(string, str):
  127. return TextStringObject(string)
  128. if isinstance(string, bytes):
  129. if isinstance(forced_encoding, (list, dict)):
  130. out = ""
  131. for x in string:
  132. try:
  133. out += forced_encoding[x]
  134. except Exception:
  135. out += bytes((x,)).decode("charmap")
  136. obj = TextStringObject(out)
  137. obj._original_bytes = string
  138. return obj
  139. if isinstance(forced_encoding, str):
  140. if forced_encoding == "bytes":
  141. return ByteStringObject(string)
  142. obj = TextStringObject(string.decode(forced_encoding))
  143. obj._original_bytes = string
  144. return obj
  145. try:
  146. if string.startswith((codecs.BOM_UTF16_BE, codecs.BOM_UTF16_LE)):
  147. retval = TextStringObject(string.decode("utf-16"))
  148. retval._original_bytes = string
  149. retval.autodetect_utf16 = True
  150. retval.utf16_bom = string[:2]
  151. return retval
  152. if string.startswith(b"\x00"):
  153. retval = TextStringObject(string.decode("utf-16be"))
  154. retval._original_bytes = string
  155. retval.autodetect_utf16 = True
  156. retval.utf16_bom = codecs.BOM_UTF16_BE
  157. return retval
  158. if string[1:2] == b"\x00":
  159. retval = TextStringObject(string.decode("utf-16le"))
  160. retval._original_bytes = string
  161. retval.autodetect_utf16 = True
  162. retval.utf16_bom = codecs.BOM_UTF16_LE
  163. return retval
  164. # This is probably a big performance hit here, but we need
  165. # to convert string objects into the text/unicode-aware
  166. # version if possible... and the only way to check if that's
  167. # possible is to try.
  168. # Some strings are strings, some are just byte arrays.
  169. retval = TextStringObject(decode_pdfdocencoding(string))
  170. retval._original_bytes = string
  171. retval.autodetect_pdfdocencoding = True
  172. return retval
  173. except UnicodeDecodeError:
  174. return ByteStringObject(string)
  175. else:
  176. raise TypeError("create_string_object should have str or unicode arg")
  177. def decode_pdfdocencoding(byte_array: bytes) -> str:
  178. retval = ""
  179. for b in byte_array:
  180. c = _pdfdoc_encoding[b]
  181. if c == "\u0000":
  182. raise UnicodeDecodeError(
  183. "pdfdocencoding",
  184. bytearray(b),
  185. -1,
  186. -1,
  187. "does not exist in translation table",
  188. )
  189. retval += c
  190. return retval