Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/pypdf/generic/_base.py: 47%

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

454 statements  

1# Copyright (c) 2006, Mathieu Fenniak 

2# All rights reserved. 

3# 

4# Redistribution and use in source and binary forms, with or without 

5# modification, are permitted provided that the following conditions are 

6# met: 

7# 

8# * Redistributions of source code must retain the above copyright notice, 

9# this list of conditions and the following disclaimer. 

10# * Redistributions in binary form must reproduce the above copyright notice, 

11# this list of conditions and the following disclaimer in the documentation 

12# and/or other materials provided with the distribution. 

13# * The name of the author may not be used to endorse or promote products 

14# derived from this software without specific prior written permission. 

15# 

16# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 

17# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 

18# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 

19# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 

20# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 

21# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 

22# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 

23# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 

24# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 

25# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 

26# POSSIBILITY OF SUCH DAMAGE. 

27import binascii 

28import codecs 

29import hashlib 

30import re 

31import sys 

32from collections.abc import Sequence 

33from math import log10 

34from struct import iter_unpack 

35from typing import Any, Callable, ClassVar, Optional, Union, cast 

36 

37if sys.version_info[:2] >= (3, 10): 

38 from typing import TypeGuard 

39else: 

40 from typing_extensions import TypeGuard # PEP 647 

41 

42if sys.version_info >= (3, 11): 

43 from typing import Self 

44else: 

45 from typing_extensions import Self 

46 

47from .._codecs import _pdfdoc_encoding_rev 

48from .._protocols import PdfWriterProtocol 

49from .._utils import ( 

50 WHITESPACES, 

51 StreamType, 

52 classproperty, 

53 deprecation_no_replacement, 

54 deprecation_with_replacement, 

55 logger_warning, 

56 read_non_whitespace, 

57 read_until_regex, 

58) 

59from ..errors import STREAM_TRUNCATED_PREMATURELY, PdfReadError, PdfStreamError 

60 

61__author__ = "Mathieu Fenniak" 

62__author_email__ = "biziqe@mathieu.fenniak.net" 

63 

64 

65class PdfObject: 

66 # function for calculating a hash value 

67 hash_func: Callable[..., "hashlib._Hash"] = hashlib.sha1 

68 indirect_reference: Optional["IndirectObject"] 

69 

70 def hash_bin(self) -> int: 

71 """ 

72 Used to detect modified object. 

73 

74 Returns: 

75 Hash considering type and value. 

76 

77 """ 

78 raise NotImplementedError( 

79 f"{self.__class__.__name__} does not implement .hash_bin() so far" 

80 ) 

81 

82 def hash_value_data(self) -> bytes: 

83 return f"{self}".encode() 

84 

85 def hash_value(self) -> bytes: 

86 return ( 

87 f"{self.__class__.__name__}:" 

88 f"{self.hash_func(self.hash_value_data()).hexdigest()}" 

89 ).encode() 

90 

91 def replicate( 

92 self, 

93 pdf_dest: PdfWriterProtocol, 

94 ) -> "PdfObject": 

95 """ 

96 Clone object into pdf_dest (PdfWriterProtocol which is an interface for PdfWriter) 

97 without ensuring links. This is used in clone_document_from_root with incremental = True. 

98 

99 Args: 

100 pdf_dest: Target to clone to. 

101 

102 Returns: 

103 The cloned PdfObject 

104 

105 """ 

106 return self.clone(pdf_dest) 

107 

108 def clone( 

109 self, 

110 pdf_dest: PdfWriterProtocol, 

111 force_duplicate: bool = False, 

112 ignore_fields: Optional[Sequence[Union[str, int]]] = (), 

113 ) -> "PdfObject": 

114 """ 

115 Clone object into pdf_dest (PdfWriterProtocol which is an interface for PdfWriter). 

116 

117 By default, this method will call ``_reference_clone`` (see ``_reference``). 

118 

119 

120 Args: 

121 pdf_dest: Target to clone to. 

122 force_duplicate: By default, if the object has already been cloned and referenced, 

123 the copy will be returned; when ``True``, a new copy will be created. 

124 (Default value = ``False``) 

125 ignore_fields: List/tuple of field names (for dictionaries) that will be ignored 

126 during cloning (applies to children duplication as well). If fields are to be 

127 considered for a limited number of levels, you have to add it as integer, for 

128 example ``[1,"/B","/TOTO"]`` means that ``"/B"`` will be ignored at the first 

129 level only but ``"/TOTO"`` on all levels. 

130 

131 Returns: 

132 The cloned PdfObject 

133 

134 """ 

135 raise NotImplementedError( 

136 f"{self.__class__.__name__} does not implement .clone so far" 

137 ) 

138 

139 def _reference_clone( 

140 self, clone: "PdfObject", pdf_dest: PdfWriterProtocol, force_duplicate: bool = False 

141 ) -> "PdfObject": 

142 """ 

143 Reference the object within the _objects of pdf_dest only if 

144 indirect_reference attribute exists (which means the objects was 

145 already identified in xref/xobjstm) if object has been already 

146 referenced do nothing. 

147 

148 Args: 

149 clone: 

150 pdf_dest: 

151 

152 Returns: 

153 The clone 

154 

155 """ 

156 if not hasattr(self, "indirect_reference"): 

157 # Direct object: nothing to reference. 

158 # Returning early is much faster than handling the exceptions below. 

159 return clone 

160 try: 

161 if ( 

162 not force_duplicate 

163 and clone.indirect_reference is not None 

164 and clone.indirect_reference.pdf == pdf_dest 

165 ): 

166 return clone 

167 except Exception: 

168 pass 

169 ind = self.indirect_reference 

170 if ( 

171 pdf_dest.incremental 

172 and ind is not None 

173 and ind.pdf == pdf_dest._reader 

174 and ind.idnum <= len(pdf_dest._objects) 

175 ): 

176 i = ind.idnum 

177 else: 

178 i = len(pdf_dest._objects) + 1 

179 if ind is not None: 

180 if id(ind.pdf) not in pdf_dest._id_translated: 

181 pdf_dest._id_translated[id(ind.pdf)] = {} 

182 pdf_dest._id_translated[id(ind.pdf)]["PreventGC"] = ind.pdf 

183 if ( 

184 not force_duplicate 

185 and ind.idnum in pdf_dest._id_translated[id(ind.pdf)] 

186 ): 

187 obj = pdf_dest.get_object( 

188 pdf_dest._id_translated[id(ind.pdf)][ind.idnum] 

189 ) 

190 assert isinstance(obj, PdfObject), "mypy" 

191 return obj 

192 pdf_dest._id_translated[id(ind.pdf)][ind.idnum] = i 

193 try: 

194 pdf_dest._objects[i - 1] = clone 

195 except IndexError: 

196 pdf_dest._objects.append(clone) 

197 i = len(pdf_dest._objects) 

198 clone.indirect_reference = IndirectObject(i, 0, pdf_dest) 

199 return clone 

200 

201 def get_object(self) -> Optional["PdfObject"]: 

202 """Resolve indirect references.""" 

203 return self 

204 

205 def write_to_stream( 

206 self, stream: StreamType, encryption_key: Union[str, bytes, None] = None 

207 ) -> None: 

208 raise NotImplementedError 

209 

210 

211class NullObject(PdfObject): 

212 def clone( 

213 self, 

214 pdf_dest: PdfWriterProtocol, 

215 force_duplicate: bool = False, 

216 ignore_fields: Optional[Sequence[Union[str, int]]] = (), 

217 ) -> "NullObject": 

218 """Clone object into pdf_dest.""" 

219 if not hasattr(self, "indirect_reference"): 

220 # Direct object: shortcut, as some arrays consist mostly of nulls 

221 return NullObject() 

222 return cast( 

223 "NullObject", self._reference_clone(NullObject(), pdf_dest, force_duplicate) 

224 ) 

225 

226 def hash_bin(self) -> int: 

227 """ 

228 Used to detect modified object. 

229 

230 Returns: 

231 Hash considering type and value. 

232 

233 """ 

234 return hash((self.__class__,)) 

235 

236 def write_to_stream( 

237 self, stream: StreamType, encryption_key: Union[str, bytes, None] = None 

238 ) -> None: 

239 if encryption_key is not None: # deprecated 

240 deprecation_no_replacement( 

241 "the encryption_key parameter of write_to_stream", "5.0.0" 

242 ) 

243 stream.write(b"null") 

244 

245 @staticmethod 

246 def read_from_stream(stream: StreamType) -> "NullObject": 

247 nulltxt = stream.read(4) 

248 if nulltxt != b"null": 

249 raise PdfReadError("Could not read Null object") 

250 return NullObject() 

251 

252 def __repr__(self) -> str: 

253 return "NullObject" 

254 

255 def __eq__(self, other: object) -> bool: 

256 return isinstance(other, NullObject) 

257 

258 def __hash__(self) -> int: 

259 return self.hash_bin() 

260 

261 

262class BooleanObject(PdfObject): 

263 value: bool 

264 

265 def __init__(self, value: Any) -> None: 

266 self.value = value 

267 

268 def clone( 

269 self, 

270 pdf_dest: PdfWriterProtocol, 

271 force_duplicate: bool = False, 

272 ignore_fields: Optional[Sequence[Union[str, int]]] = (), 

273 ) -> "BooleanObject": 

274 """Clone object into pdf_dest.""" 

275 return cast( 

276 "BooleanObject", 

277 self._reference_clone(BooleanObject(self.value), pdf_dest, force_duplicate), 

278 ) 

279 

280 def hash_bin(self) -> int: 

281 """ 

282 Used to detect modified object. 

283 

284 Returns: 

285 Hash considering type and value. 

286 

287 """ 

288 return hash((self.__class__, self.value)) 

289 

290 def __eq__(self, o: object, /) -> bool: 

291 if isinstance(o, BooleanObject): 

292 return self.value == o.value 

293 if isinstance(o, bool): 

294 return self.value == o 

295 return False 

296 

297 def __hash__(self) -> int: 

298 return self.hash_bin() 

299 

300 def __repr__(self) -> str: 

301 return "True" if self.value else "False" 

302 

303 def write_to_stream( 

304 self, stream: StreamType, encryption_key: Union[str, bytes, None] = None 

305 ) -> None: 

306 if encryption_key is not None: # deprecated 

307 deprecation_no_replacement( 

308 "the encryption_key parameter of write_to_stream", "5.0.0" 

309 ) 

310 if self.value: 

311 stream.write(b"true") 

312 else: 

313 stream.write(b"false") 

314 

315 @staticmethod 

316 def read_from_stream(stream: StreamType) -> "BooleanObject": 

317 word = stream.read(4) 

318 if word == b"true": 

319 return BooleanObject(True) 

320 if word == b"fals": 

321 stream.read(1) 

322 return BooleanObject(False) 

323 raise PdfReadError("Could not read Boolean object") 

324 

325 

326class IndirectObject(PdfObject): 

327 _MAXIMUM_PART_LENGTH = 64 

328 

329 def __init__(self, idnum: int, generation: int, pdf: Any) -> None: # PdfReader 

330 self.idnum = idnum 

331 self.generation = generation 

332 self.pdf = pdf 

333 

334 def __hash__(self) -> int: 

335 return hash((self.idnum, self.generation, id(self.pdf))) 

336 

337 def hash_bin(self) -> int: 

338 """ 

339 Used to detect modified object. 

340 

341 Returns: 

342 Hash considering type and value. 

343 

344 """ 

345 return hash((self.__class__, self.idnum, self.generation, id(self.pdf))) 

346 

347 def replicate( 

348 self, 

349 pdf_dest: PdfWriterProtocol, 

350 ) -> "PdfObject": 

351 return IndirectObject(self.idnum, self.generation, pdf_dest) 

352 

353 def clone( 

354 self, 

355 pdf_dest: PdfWriterProtocol, 

356 force_duplicate: bool = False, 

357 ignore_fields: Optional[Sequence[Union[str, int]]] = (), 

358 ) -> "IndirectObject": 

359 """Clone object into pdf_dest.""" 

360 if self.pdf == pdf_dest and not force_duplicate: 

361 # Already duplicated and no extra duplication required 

362 return self 

363 if id(self.pdf) not in pdf_dest._id_translated: 

364 pdf_dest._id_translated[id(self.pdf)] = {} 

365 pdf_dest._id_translated[id(self.pdf)]["PreventGC"] = self.pdf 

366 

367 if self.idnum in pdf_dest._id_translated[id(self.pdf)]: 

368 dup = pdf_dest.get_object(pdf_dest._id_translated[id(self.pdf)][self.idnum]) 

369 if force_duplicate: 

370 assert dup is not None 

371 assert dup.indirect_reference is not None 

372 idref = dup.indirect_reference 

373 return IndirectObject(idref.idnum, idref.generation, idref.pdf) 

374 else: 

375 obj = self.get_object() 

376 # case observed : a pointed object can not be found 

377 if obj is None: 

378 # this normally 

379 obj = NullObject() 

380 assert isinstance(self, (IndirectObject,)) 

381 obj.indirect_reference = self 

382 dup = pdf_dest._add_object( 

383 obj.clone(pdf_dest, force_duplicate, ignore_fields) 

384 ) 

385 assert isinstance(dup, PdfObject), "mypy" 

386 assert dup.indirect_reference is not None, "mypy" 

387 return dup.indirect_reference 

388 

389 @property 

390 def indirect_reference(self) -> "IndirectObject": # type: ignore[override] 

391 return self 

392 

393 def get_object(self) -> Optional["PdfObject"]: 

394 obj: Optional[PdfObject] = self.pdf.get_object(self) 

395 return obj 

396 

397 def __deepcopy__(self, memo: Any) -> "IndirectObject": 

398 return IndirectObject(self.idnum, self.generation, self.pdf) 

399 

400 def _get_object_with_check(self) -> Optional["PdfObject"]: 

401 o = self.get_object() 

402 # the check is done here to not slow down get_object() 

403 if isinstance(o, IndirectObject): 

404 raise PdfStreamError( 

405 f"{self.__repr__()} references an IndirectObject {o.__repr__()}" 

406 ) 

407 return o 

408 

409 def __getattr__(self, name: str) -> Any: 

410 # Attribute not found in object: look in pointed object 

411 try: 

412 return getattr(self._get_object_with_check(), name) 

413 except AttributeError as exception: 

414 raise AttributeError( 

415 f"No attribute {name} found in IndirectObject or pointed object" 

416 ) from exception 

417 

418 def __getitem__(self, key: Any) -> Any: 

419 # items should be extracted from pointed Object 

420 return self._get_object_with_check()[key] # type: ignore[index] 

421 

422 def __contains__(self, key: Any) -> bool: 

423 return key in self._get_object_with_check() # type: ignore[operator] 

424 

425 def __iter__(self) -> Any: 

426 return self._get_object_with_check().__iter__() # type: ignore[union-attr] 

427 

428 def __float__(self) -> str: 

429 # in this case we are looking for the pointed data 

430 return self.get_object().__float__() # type: ignore[union-attr, no-any-return] 

431 

432 def __int__(self) -> int: 

433 # in this case we are looking for the pointed data 

434 return self.get_object().__int__() # type: ignore[union-attr, no-any-return] 

435 

436 def __str__(self) -> str: 

437 # in this case we are looking for the pointed data 

438 return self.get_object().__str__() 

439 

440 def __repr__(self) -> str: 

441 return f"IndirectObject({self.idnum!r}, {self.generation!r}, {id(self.pdf)})" 

442 

443 def __eq__(self, other: object) -> bool: 

444 return ( 

445 other is not None 

446 and isinstance(other, IndirectObject) 

447 and self.idnum == other.idnum 

448 and self.generation == other.generation 

449 and self.pdf is other.pdf 

450 ) 

451 

452 def __ne__(self, other: object) -> bool: 

453 return not self.__eq__(other) 

454 

455 def write_to_stream( 

456 self, stream: StreamType, encryption_key: Union[str, bytes, None] = None 

457 ) -> None: 

458 if encryption_key is not None: # deprecated 

459 deprecation_no_replacement( 

460 "the encryption_key parameter of write_to_stream", "5.0.0" 

461 ) 

462 stream.write(f"{self.idnum} {self.generation} R".encode()) 

463 

464 @staticmethod 

465 def read_from_stream(stream: StreamType, pdf: Any) -> "IndirectObject": # PdfReader 

466 def read_part(name: str, skip_leading_whitespace: bool) -> bytes: 

467 result = bytearray() 

468 while True: 

469 tok = stream.read(1) 

470 if not tok: 

471 raise PdfStreamError(STREAM_TRUNCATED_PREMATURELY) 

472 if tok.isspace() or tok in WHITESPACES: 

473 if skip_leading_whitespace and not result: 

474 continue 

475 break 

476 if len(result) >= IndirectObject._MAXIMUM_PART_LENGTH: 

477 raise PdfReadError(f"{name} exceeds maximum length limit of {IndirectObject._MAXIMUM_PART_LENGTH}.") 

478 result += tok 

479 return bytes(result) 

480 

481 idnum = read_part("Object ID", skip_leading_whitespace=False) 

482 generation = read_part("Generation number", skip_leading_whitespace=True) 

483 

484 r = read_non_whitespace(stream) 

485 if r != b"R": 

486 raise PdfReadError( 

487 f"Error reading indirect object reference at byte {hex(stream.tell())}" 

488 ) 

489 try: 

490 return IndirectObject(int(idnum), int(generation), pdf) 

491 except (ValueError, OverflowError) as exception: 

492 raise PdfReadError( 

493 f"Invalid indirect object reference ({idnum!r} {generation!r} R): {exception}" 

494 ) from exception 

495 

496 

497FLOAT_WRITE_PRECISION = 8 # shall be min 5 digits max, allow user adj 

498 

499 

500class FloatObject(float, PdfObject): 

501 def __new__( 

502 cls, value: Any = "0.0", context: Optional[Any] = None 

503 ) -> Self: 

504 try: 

505 value = float(value) 

506 return float.__new__(cls, value) 

507 except Exception as exception: 

508 # If this isn't a valid decimal (happens in malformed PDFs) 

509 # fallback to 0 

510 logger_warning( 

511 "%(error)s : FloatObject (%(value)s) invalid; use 0.0 instead", 

512 source=__name__, 

513 error=exception, 

514 value=value, 

515 ) 

516 return float.__new__(cls, 0.0) 

517 

518 def clone( 

519 self, 

520 pdf_dest: Any, 

521 force_duplicate: bool = False, 

522 ignore_fields: Optional[Sequence[Union[str, int]]] = (), 

523 ) -> "FloatObject": 

524 """Clone object into pdf_dest.""" 

525 return cast( 

526 "FloatObject", 

527 self._reference_clone(FloatObject(self), pdf_dest, force_duplicate), 

528 ) 

529 

530 def hash_bin(self) -> int: 

531 """ 

532 Used to detect modified object. 

533 

534 Returns: 

535 Hash considering type and value. 

536 

537 """ 

538 return hash((self.__class__, self.as_numeric())) 

539 

540 def myrepr(self) -> str: 

541 if self == 0: # type: ignore[comparison-overlap] 

542 return "0.0" 

543 nb = FLOAT_WRITE_PRECISION - int(log10(abs(self))) 

544 return f"{self:.{max(1, nb)}f}".rstrip("0").rstrip(".") 

545 

546 def __repr__(self) -> str: 

547 return self.myrepr() # repr(float(self)) 

548 

549 def as_numeric(self) -> float: 

550 return float(self) 

551 

552 def write_to_stream( 

553 self, stream: StreamType, encryption_key: Union[str, bytes, None] = None 

554 ) -> None: 

555 if encryption_key is not None: # deprecated 

556 deprecation_no_replacement( 

557 "the encryption_key parameter of write_to_stream", "5.0.0" 

558 ) 

559 stream.write(self.myrepr().encode("utf8")) 

560 

561 

562class NumberObject(int, PdfObject): 

563 NumberPattern = re.compile(b"[^+-.0-9]") 

564 _LENGTH_LIMIT = 64 

565 

566 def __new__(cls, value: Any) -> Self: 

567 try: 

568 return int.__new__(cls, int(value)) 

569 except ValueError: 

570 logger_warning("NumberObject(%(value)s) invalid; use 0 instead", source=__name__, value=value) 

571 return int.__new__(cls, 0) 

572 

573 def clone( 

574 self, 

575 pdf_dest: Any, 

576 force_duplicate: bool = False, 

577 ignore_fields: Optional[Sequence[Union[str, int]]] = (), 

578 ) -> "NumberObject": 

579 """Clone object into pdf_dest.""" 

580 return cast( 

581 "NumberObject", 

582 self._reference_clone(NumberObject(self), pdf_dest, force_duplicate), 

583 ) 

584 

585 def hash_bin(self) -> int: 

586 """ 

587 Used to detect modified object. 

588 

589 Returns: 

590 Hash considering type and value. 

591 

592 """ 

593 return hash((self.__class__, self.as_numeric())) 

594 

595 def as_numeric(self) -> int: 

596 return int(repr(self).encode("utf8")) 

597 

598 def write_to_stream( 

599 self, stream: StreamType, encryption_key: Union[str, bytes, None] = None 

600 ) -> None: 

601 if encryption_key is not None: # deprecated 

602 deprecation_no_replacement( 

603 "the encryption_key parameter of write_to_stream", "5.0.0" 

604 ) 

605 stream.write(repr(self).encode("utf8")) 

606 

607 @staticmethod 

608 def read_from_stream(stream: StreamType) -> Union["NumberObject", "FloatObject"]: 

609 num = read_until_regex(stream=stream, regex=NumberObject.NumberPattern, length=NumberObject._LENGTH_LIMIT) 

610 if b"." in num: 

611 return FloatObject(num) 

612 return NumberObject(num) 

613 

614 

615class ByteStringObject(bytes, PdfObject): 

616 """ 

617 Represents a string object where the text encoding could not be determined. 

618 

619 This occurs quite often, as the PDF spec doesn't provide an alternate way to 

620 represent strings -- for example, the encryption data stored in files (like 

621 /O) is clearly not text, but is still stored in a "String" object. 

622 """ 

623 

624 def clone( 

625 self, 

626 pdf_dest: Any, 

627 force_duplicate: bool = False, 

628 ignore_fields: Optional[Sequence[Union[str, int]]] = (), 

629 ) -> "ByteStringObject": 

630 """Clone object into pdf_dest.""" 

631 return cast( 

632 "ByteStringObject", 

633 self._reference_clone( 

634 ByteStringObject(bytes(self)), pdf_dest, force_duplicate 

635 ), 

636 ) 

637 

638 def hash_bin(self) -> int: 

639 """ 

640 Used to detect modified object. 

641 

642 Returns: 

643 Hash considering type and value. 

644 

645 """ 

646 return hash((self.__class__, bytes(self))) 

647 

648 @property 

649 def original_bytes(self) -> bytes: 

650 """For compatibility with TextStringObject.original_bytes.""" 

651 return self 

652 

653 def write_to_stream( 

654 self, stream: StreamType, encryption_key: Union[str, bytes, None] = None 

655 ) -> None: 

656 if encryption_key is not None: # deprecated 

657 deprecation_no_replacement( 

658 "the encryption_key parameter of write_to_stream", "5.0.0" 

659 ) 

660 stream.write(b"<") 

661 stream.write(binascii.hexlify(self)) 

662 stream.write(b">") 

663 

664 def __str__(self) -> str: 

665 charset_to_try = ["utf-16", *list(NameObject.CHARSETS)] 

666 for enc in charset_to_try: 

667 try: 

668 return self.decode(enc) 

669 except UnicodeDecodeError: 

670 pass 

671 raise PdfReadError("Cannot decode ByteStringObject.") 

672 

673 

674class TextStringObject(str, PdfObject): # noqa: SLOT000 

675 """ 

676 A string object that has been decoded into a real unicode string. 

677 

678 If read from a PDF document, this string appeared to match the 

679 PDFDocEncoding, or contained a UTF-16BE BOM mark to cause UTF-16 decoding 

680 to occur. 

681 """ 

682 

683 autodetect_pdfdocencoding: bool 

684 autodetect_utf16: bool 

685 utf16_bom: bytes 

686 _original_bytes: Optional[bytes] = None 

687 

688 def __new__(cls, value: Any) -> Self: 

689 original_bytes = None 

690 if isinstance(value, bytes): 

691 original_bytes = value 

692 value = value.decode("charmap") 

693 text_string_object = str.__new__(cls, value) 

694 text_string_object._original_bytes = original_bytes 

695 text_string_object.autodetect_utf16 = False 

696 text_string_object.autodetect_pdfdocencoding = False 

697 text_string_object.utf16_bom = b"" 

698 if original_bytes is not None and original_bytes[:2] in {codecs.BOM_UTF16_LE, codecs.BOM_UTF16_BE}: 

699 # The value of `original_bytes` is only set for inputs being `bytes`. 

700 # If this is UTF-16 data according to the BOM (first two characters), 

701 # perform special handling. All other cases should not need any special conversion 

702 # due to already being a string. 

703 try: 

704 text_string_object = str.__new__(cls, original_bytes.decode("utf-16")) 

705 except UnicodeDecodeError as exception: 

706 logger_warning( 

707 "%(exception)s; initial string: %(initial_string)r", 

708 source=__name__, 

709 exception=exception, 

710 initial_string=exception.object, 

711 ) 

712 text_string_object = str.__new__(cls, exception.object[: exception.start].decode("utf-16")) 

713 text_string_object._original_bytes = original_bytes 

714 text_string_object.autodetect_utf16 = True 

715 text_string_object.utf16_bom = original_bytes[:2] 

716 else: 

717 try: 

718 encode_pdfdocencoding(text_string_object) 

719 text_string_object.autodetect_pdfdocencoding = True 

720 except UnicodeEncodeError: 

721 text_string_object.autodetect_utf16 = True 

722 text_string_object.utf16_bom = codecs.BOM_UTF16_BE 

723 return text_string_object 

724 

725 def clone( 

726 self, 

727 pdf_dest: Any, 

728 force_duplicate: bool = False, 

729 ignore_fields: Optional[Sequence[Union[str, int]]] = (), 

730 ) -> "TextStringObject": 

731 """Clone object into pdf_dest.""" 

732 obj = TextStringObject(self) 

733 obj._original_bytes = self._original_bytes 

734 obj.autodetect_pdfdocencoding = self.autodetect_pdfdocencoding 

735 obj.autodetect_utf16 = self.autodetect_utf16 

736 obj.utf16_bom = self.utf16_bom 

737 return cast( 

738 "TextStringObject", self._reference_clone(obj, pdf_dest, force_duplicate) 

739 ) 

740 

741 def hash_bin(self) -> int: 

742 """ 

743 Used to detect modified object. 

744 

745 Returns: 

746 Hash considering type and value. 

747 

748 """ 

749 return hash((self.__class__, self.original_bytes)) 

750 

751 @property 

752 def original_bytes(self) -> bytes: 

753 """ 

754 It is occasionally possible that a text string object gets created where 

755 a byte string object was expected due to the autodetection mechanism -- 

756 if that occurs, this "original_bytes" property can be used to 

757 back-calculate what the original encoded bytes were. 

758 """ 

759 if self._original_bytes is not None: 

760 return self._original_bytes 

761 return self.get_original_bytes() 

762 

763 def get_original_bytes(self) -> bytes: 

764 # We're a text string object, but the library is trying to get our raw 

765 # bytes. This can happen if we auto-detected this string as text, but 

766 # we were wrong. It's pretty common. Return the original bytes that 

767 # would have been used to create this object, based upon the autodetect 

768 # method. 

769 if self.autodetect_utf16: 

770 if self.utf16_bom == codecs.BOM_UTF16_LE: 

771 return codecs.BOM_UTF16_LE + self.encode("utf-16le") 

772 if self.utf16_bom == codecs.BOM_UTF16_BE: 

773 return codecs.BOM_UTF16_BE + self.encode("utf-16be") 

774 return self.encode("utf-16be") 

775 if self.autodetect_pdfdocencoding: 

776 return encode_pdfdocencoding(self) 

777 raise Exception("no information about original bytes") # pragma: no cover 

778 

779 def get_encoded_bytes(self) -> bytes: 

780 # Try to write the string out as a PDFDocEncoding encoded string. It's 

781 # nicer to look at in the PDF file. Sadly, we take a performance hit 

782 # here for trying... 

783 try: 

784 if self._original_bytes is not None: 

785 return self._original_bytes 

786 if self.autodetect_utf16: 

787 raise UnicodeEncodeError("", "forced", -1, -1, "") 

788 bytearr = encode_pdfdocencoding(self) 

789 except UnicodeEncodeError: 

790 if self.utf16_bom == codecs.BOM_UTF16_LE: 

791 bytearr = codecs.BOM_UTF16_LE + self.encode("utf-16le") 

792 elif self.utf16_bom == codecs.BOM_UTF16_BE: 

793 bytearr = codecs.BOM_UTF16_BE + self.encode("utf-16be") 

794 else: 

795 bytearr = self.encode("utf-16be") 

796 return bytearr 

797 

798 def write_to_stream( 

799 self, stream: StreamType, encryption_key: Union[str, bytes, None] = None 

800 ) -> None: 

801 if encryption_key is not None: # deprecated 

802 deprecation_no_replacement( 

803 "the encryption_key parameter of write_to_stream", "5.0.0" 

804 ) 

805 bytearr = self.get_encoded_bytes() 

806 stream.write(b"(") 

807 for c_ in iter_unpack("c", bytearr): 

808 c = cast(bytes, c_[0]) 

809 if not c.isalnum() and c != b" ": 

810 # This: 

811 # stream.write(rf"\{c:0>3o}".encode()) 

812 # gives 

813 # https://github.com/davidhalter/parso/issues/207 

814 stream.write(b"\\%03o" % ord(c)) 

815 else: 

816 stream.write(c) 

817 stream.write(b")") 

818 

819 

820class NameObject(str, PdfObject): # noqa: SLOT000 

821 delimiter_pattern = re.compile(rb"\s+|[\(\)<>\[\]{}/%]") 

822 prefix = b"/" 

823 renumber_table: ClassVar[dict[str, bytes]] = { 

824 **{chr(i): f"#{i:02X}".encode() for i in b"#()<>[]{}/%"}, 

825 **{chr(i): f"#{i:02X}".encode() for i in range(33)}, 

826 } 

827 _LENGTH_LIMIT = 4096 

828 

829 def clone( 

830 self, 

831 pdf_dest: Any, 

832 force_duplicate: bool = False, 

833 ignore_fields: Optional[Sequence[Union[str, int]]] = (), 

834 ) -> "NameObject": 

835 """Clone object into pdf_dest.""" 

836 return cast( 

837 "NameObject", 

838 self._reference_clone(NameObject(self), pdf_dest, force_duplicate), 

839 ) 

840 

841 def hash_bin(self) -> int: 

842 """ 

843 Used to detect modified object. 

844 

845 Returns: 

846 Hash considering type and value. 

847 

848 """ 

849 return hash((self.__class__, self)) 

850 

851 def write_to_stream( 

852 self, stream: StreamType, encryption_key: Union[str, bytes, None] = None 

853 ) -> None: 

854 if encryption_key is not None: # deprecated 

855 deprecation_no_replacement( 

856 "the encryption_key parameter of write_to_stream", "5.0.0" 

857 ) 

858 stream.write(self.renumber()) 

859 

860 def renumber(self) -> bytes: 

861 out = self[0].encode("utf-8") 

862 if out != b"/": 

863 deprecation_no_replacement( 

864 f"Incorrect first char in NameObject, should start with '/': ({self})", 

865 "5.0.0", 

866 ) 

867 parts = [out] 

868 for c in self[1:]: 

869 if c > "~": 

870 parts.extend(f"#{x:02X}".encode() for x in c.encode("utf-8")) 

871 else: 

872 try: 

873 parts.append(self.renumber_table[c]) 

874 except KeyError: 

875 parts.append(c.encode("utf-8")) 

876 return b"".join(parts) 

877 

878 def _sanitize(self) -> "NameObject": 

879 """ 

880 Sanitize the NameObject's name to be a valid PDF name part 

881 (alphanumeric, underscore, hyphen). The _sanitize method replaces 

882 spaces and any non-alphanumeric/non-underscore/non-hyphen with 

883 underscores. 

884 

885 Returns: 

886 NameObject with sanitized name. 

887 """ 

888 name = str(self).removeprefix("/") 

889 name = re.sub(r" ", "_", name) 

890 name = re.sub(r"[^a-zA-Z0-9_-]", "_", name) 

891 return NameObject("/" + name) 

892 

893 @classproperty 

894 def surfix(cls) -> bytes: # noqa: N805 

895 deprecation_with_replacement("surfix", "prefix", "5.0.0") 

896 return b"/" 

897 

898 @staticmethod 

899 def unnumber(sin: bytes) -> bytes: 

900 if b"#" not in sin: 

901 return bytes(sin) 

902 result = bytearray() 

903 i = 0 

904 while i < len(sin): 

905 if sin[i:i + 1] == b"#": 

906 try: 

907 result.append(int(sin[i + 1 : i + 3], 16)) 

908 i += 3 

909 continue 

910 except (ValueError, IndexError): 

911 # if the 2 characters after # can not be converted to hex 

912 # we change nothing and carry on 

913 pass 

914 result.append(sin[i]) 

915 i += 1 

916 return bytes(result) 

917 

918 CHARSETS = ("utf-8", "gbk", "latin1") 

919 

920 @staticmethod 

921 def read_from_stream(stream: StreamType, pdf: Any) -> "NameObject": # PdfReader 

922 name = stream.read(1) 

923 if name != NameObject.prefix: 

924 raise PdfReadError("Name read error") 

925 name += read_until_regex(stream=stream, regex=NameObject.delimiter_pattern, length=NameObject._LENGTH_LIMIT) 

926 try: 

927 # Name objects should represent irregular characters 

928 # with a '#' followed by the symbol's hex number 

929 name = NameObject.unnumber(name) 

930 for enc in NameObject.CHARSETS: 

931 try: 

932 ret = name.decode(enc) 

933 return NameObject(ret) 

934 except Exception: 

935 pass 

936 raise UnicodeDecodeError("", name, 0, 0, "Code Not Found") 

937 except (UnicodeEncodeError, UnicodeDecodeError) as exception: 

938 if not pdf.strict: 

939 logger_warning( 

940 "Illegal character in NameObject (%(name)r), you may need to adjust NameObject.CHARSETS", 

941 source=__name__, 

942 name=name, 

943 ) 

944 return NameObject(name.decode("charmap")) 

945 raise PdfReadError( 

946 f"Illegal character in NameObject ({name!r}). " 

947 "You may need to adjust NameObject.CHARSETS.", 

948 ) from exception 

949 

950 

951def encode_pdfdocencoding(unicode_string: str) -> bytes: 

952 try: 

953 return bytes([_pdfdoc_encoding_rev[k] for k in unicode_string]) 

954 except KeyError as exception: 

955 raise UnicodeEncodeError( 

956 "pdfdocencoding", 

957 unicode_string, 

958 -1, 

959 -1, 

960 "does not exist in translation table", 

961 ) from exception 

962 

963 

964def is_null_or_none(x: Any) -> TypeGuard[Union[NullObject, IndirectObject, None]]: 

965 """ 

966 Returns: 

967 True if x is None or NullObject. 

968 

969 """ 

970 return x is None or ( 

971 isinstance(x, PdfObject) 

972 and (x.get_object() is None or isinstance(x.get_object(), NullObject)) 

973 )