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

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

449 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 PdfObjectProtocol, PdfWriterProtocol 

49from .._utils import ( 

50 StreamType, 

51 classproperty, 

52 deprecation_no_replacement, 

53 deprecation_with_replacement, 

54 logger_warning, 

55 read_non_whitespace, 

56 read_until_regex, 

57) 

58from ..errors import STREAM_TRUNCATED_PREMATURELY, PdfReadError, PdfStreamError 

59 

60__author__ = "Mathieu Fenniak" 

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

62 

63 

64class PdfObject(PdfObjectProtocol): 

65 # function for calculating a hash value 

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

67 indirect_reference: Optional["IndirectObject"] 

68 

69 def hash_bin(self) -> int: 

70 """ 

71 Used to detect modified object. 

72 

73 Returns: 

74 Hash considering type and value. 

75 

76 """ 

77 raise NotImplementedError( 

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

79 ) 

80 

81 def hash_value_data(self) -> bytes: 

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

83 

84 def hash_value(self) -> bytes: 

85 return ( 

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

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

88 ).encode() 

89 

90 def replicate( 

91 self, 

92 pdf_dest: PdfWriterProtocol, 

93 ) -> "PdfObject": 

94 """ 

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

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

97 

98 Args: 

99 pdf_dest: Target to clone to. 

100 

101 Returns: 

102 The cloned PdfObject 

103 

104 """ 

105 return self.clone(pdf_dest) 

106 

107 def clone( 

108 self, 

109 pdf_dest: PdfWriterProtocol, 

110 force_duplicate: bool = False, 

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

112 ) -> "PdfObject": 

113 """ 

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

115 

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

117 

118 

119 Args: 

120 pdf_dest: Target to clone to. 

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

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

123 (Default value = ``False``) 

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

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

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

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

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

129 

130 Returns: 

131 The cloned PdfObject 

132 

133 """ 

134 raise NotImplementedError( 

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

136 ) 

137 

138 def _reference_clone( 

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

140 ) -> "PdfObject": 

141 """ 

142 Reference the object within the _objects of pdf_dest only if 

143 indirect_reference attribute exists (which means the objects was 

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

145 referenced do nothing. 

146 

147 Args: 

148 clone: 

149 pdf_dest: 

150 

151 Returns: 

152 The clone 

153 

154 """ 

155 try: 

156 if ( 

157 not force_duplicate 

158 and clone.indirect_reference is not None 

159 and clone.indirect_reference.pdf == pdf_dest 

160 ): 

161 return clone 

162 except Exception: 

163 pass 

164 # if hasattr(clone, "indirect_reference"): 

165 try: 

166 ind = self.indirect_reference 

167 except AttributeError: 

168 return clone 

169 if ( 

170 pdf_dest.incremental 

171 and ind is not None 

172 and ind.pdf == pdf_dest._reader 

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

174 ): 

175 i = ind.idnum 

176 else: 

177 i = len(pdf_dest._objects) + 1 

178 if ind is not None: 

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

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

181 pdf_dest._id_translated[id(ind.pdf)]["PreventGC"] = ind.pdf # type: ignore[index] 

182 if ( 

183 not force_duplicate 

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

185 ): 

186 obj = pdf_dest.get_object( 

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

188 ) 

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

190 return obj 

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

192 try: 

193 pdf_dest._objects[i - 1] = clone 

194 except IndexError: 

195 pdf_dest._objects.append(clone) 

196 i = len(pdf_dest._objects) 

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

198 return clone 

199 

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

201 """Resolve indirect references.""" 

202 return self 

203 

204 def write_to_stream( 

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

206 ) -> None: 

207 raise NotImplementedError 

208 

209 

210class NullObject(PdfObject): 

211 def clone( 

212 self, 

213 pdf_dest: PdfWriterProtocol, 

214 force_duplicate: bool = False, 

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

216 ) -> "NullObject": 

217 """Clone object into pdf_dest.""" 

218 return cast( 

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

220 ) 

221 

222 def hash_bin(self) -> int: 

223 """ 

224 Used to detect modified object. 

225 

226 Returns: 

227 Hash considering type and value. 

228 

229 """ 

230 return hash((self.__class__,)) 

231 

232 def write_to_stream( 

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

234 ) -> None: 

235 if encryption_key is not None: # deprecated 

236 deprecation_no_replacement( 

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

238 ) 

239 stream.write(b"null") 

240 

241 @staticmethod 

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

243 nulltxt = stream.read(4) 

244 if nulltxt != b"null": 

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

246 return NullObject() 

247 

248 def __repr__(self) -> str: 

249 return "NullObject" 

250 

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

252 return isinstance(other, NullObject) 

253 

254 def __hash__(self) -> int: 

255 return self.hash_bin() 

256 

257 

258class BooleanObject(PdfObject): 

259 value: bool 

260 

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

262 self.value = value 

263 

264 def clone( 

265 self, 

266 pdf_dest: PdfWriterProtocol, 

267 force_duplicate: bool = False, 

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

269 ) -> "BooleanObject": 

270 """Clone object into pdf_dest.""" 

271 return cast( 

272 "BooleanObject", 

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

274 ) 

275 

276 def hash_bin(self) -> int: 

277 """ 

278 Used to detect modified object. 

279 

280 Returns: 

281 Hash considering type and value. 

282 

283 """ 

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

285 

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

287 if isinstance(o, BooleanObject): 

288 return self.value == o.value 

289 if isinstance(o, bool): 

290 return self.value == o 

291 return False 

292 

293 def __hash__(self) -> int: 

294 return self.hash_bin() 

295 

296 def __repr__(self) -> str: 

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

298 

299 def write_to_stream( 

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

301 ) -> None: 

302 if encryption_key is not None: # deprecated 

303 deprecation_no_replacement( 

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

305 ) 

306 if self.value: 

307 stream.write(b"true") 

308 else: 

309 stream.write(b"false") 

310 

311 @staticmethod 

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

313 word = stream.read(4) 

314 if word == b"true": 

315 return BooleanObject(True) 

316 if word == b"fals": 

317 stream.read(1) 

318 return BooleanObject(False) 

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

320 

321 

322class IndirectObject(PdfObject): 

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

324 self.idnum = idnum 

325 self.generation = generation 

326 self.pdf = pdf 

327 

328 def __hash__(self) -> int: 

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

330 

331 def hash_bin(self) -> int: 

332 """ 

333 Used to detect modified object. 

334 

335 Returns: 

336 Hash considering type and value. 

337 

338 """ 

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

340 

341 def replicate( 

342 self, 

343 pdf_dest: PdfWriterProtocol, 

344 ) -> "PdfObject": 

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

346 

347 def clone( 

348 self, 

349 pdf_dest: PdfWriterProtocol, 

350 force_duplicate: bool = False, 

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

352 ) -> "IndirectObject": 

353 """Clone object into pdf_dest.""" 

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

355 # Already duplicated and no extra duplication required 

356 return self 

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

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

359 pdf_dest._id_translated[id(self.pdf)]["PreventGC"] = self.pdf # type: ignore[index] 

360 

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

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

363 if force_duplicate: 

364 assert dup is not None 

365 assert dup.indirect_reference is not None 

366 idref = dup.indirect_reference 

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

368 else: 

369 obj = self.get_object() 

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

371 if obj is None: 

372 # this normally 

373 obj = NullObject() 

374 assert isinstance(self, (IndirectObject,)) 

375 obj.indirect_reference = self 

376 dup = pdf_dest._add_object( 

377 obj.clone(pdf_dest, force_duplicate, ignore_fields) 

378 ) 

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

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

381 return dup.indirect_reference 

382 

383 @property 

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

385 return self 

386 

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

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

389 return obj 

390 

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

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

393 

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

395 o = self.get_object() 

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

397 if isinstance(o, IndirectObject): 

398 raise PdfStreamError( 

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

400 ) 

401 return o 

402 

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

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

405 try: 

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

407 except AttributeError: 

408 raise AttributeError( 

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

410 ) 

411 

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

413 # items should be extracted from pointed Object 

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

415 

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

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

418 

419 def __iter__(self) -> Any: 

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

421 

422 def __float__(self) -> str: 

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

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

425 

426 def __int__(self) -> int: 

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

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

429 

430 def __str__(self) -> str: 

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

432 return self.get_object().__str__() 

433 

434 def __repr__(self) -> str: 

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

436 

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

438 return ( 

439 other is not None 

440 and isinstance(other, IndirectObject) 

441 and self.idnum == other.idnum 

442 and self.generation == other.generation 

443 and self.pdf is other.pdf 

444 ) 

445 

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

447 return not self.__eq__(other) 

448 

449 def write_to_stream( 

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

451 ) -> None: 

452 if encryption_key is not None: # deprecated 

453 deprecation_no_replacement( 

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

455 ) 

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

457 

458 @staticmethod 

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

460 idnum = b"" 

461 while True: 

462 tok = stream.read(1) 

463 if not tok: 

464 raise PdfStreamError(STREAM_TRUNCATED_PREMATURELY) 

465 if tok.isspace(): 

466 break 

467 idnum += tok 

468 generation = b"" 

469 while True: 

470 tok = stream.read(1) 

471 if not tok: 

472 raise PdfStreamError(STREAM_TRUNCATED_PREMATURELY) 

473 if tok.isspace(): 

474 if not generation: 

475 continue 

476 break 

477 generation += tok 

478 r = read_non_whitespace(stream) 

479 if r != b"R": 

480 raise PdfReadError( 

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

482 ) 

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

484 

485 

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

487 

488 

489class FloatObject(float, PdfObject): 

490 def __new__( 

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

492 ) -> Self: 

493 try: 

494 value = float(value) 

495 return float.__new__(cls, value) 

496 except Exception as e: 

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

498 # fallback to 0 

499 logger_warning( 

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

501 source=__name__, 

502 error=e, 

503 value=value, 

504 ) 

505 return float.__new__(cls, 0.0) 

506 

507 def clone( 

508 self, 

509 pdf_dest: Any, 

510 force_duplicate: bool = False, 

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

512 ) -> "FloatObject": 

513 """Clone object into pdf_dest.""" 

514 return cast( 

515 "FloatObject", 

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

517 ) 

518 

519 def hash_bin(self) -> int: 

520 """ 

521 Used to detect modified object. 

522 

523 Returns: 

524 Hash considering type and value. 

525 

526 """ 

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

528 

529 def myrepr(self) -> str: 

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

531 return "0.0" 

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

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

534 

535 def __repr__(self) -> str: 

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

537 

538 def as_numeric(self) -> float: 

539 return float(self) 

540 

541 def write_to_stream( 

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

543 ) -> None: 

544 if encryption_key is not None: # deprecated 

545 deprecation_no_replacement( 

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

547 ) 

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

549 

550 

551class NumberObject(int, PdfObject): 

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

553 _LENGTH_LIMIT = 64 

554 

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

556 try: 

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

558 except ValueError: 

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

560 return int.__new__(cls, 0) 

561 

562 def clone( 

563 self, 

564 pdf_dest: Any, 

565 force_duplicate: bool = False, 

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

567 ) -> "NumberObject": 

568 """Clone object into pdf_dest.""" 

569 return cast( 

570 "NumberObject", 

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

572 ) 

573 

574 def hash_bin(self) -> int: 

575 """ 

576 Used to detect modified object. 

577 

578 Returns: 

579 Hash considering type and value. 

580 

581 """ 

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

583 

584 def as_numeric(self) -> int: 

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

586 

587 def write_to_stream( 

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

589 ) -> None: 

590 if encryption_key is not None: # deprecated 

591 deprecation_no_replacement( 

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

593 ) 

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

595 

596 @staticmethod 

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

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

599 if b"." in num: 

600 return FloatObject(num) 

601 return NumberObject(num) 

602 

603 

604class ByteStringObject(bytes, PdfObject): 

605 """ 

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

607 

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

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

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

611 """ 

612 

613 def clone( 

614 self, 

615 pdf_dest: Any, 

616 force_duplicate: bool = False, 

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

618 ) -> "ByteStringObject": 

619 """Clone object into pdf_dest.""" 

620 return cast( 

621 "ByteStringObject", 

622 self._reference_clone( 

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

624 ), 

625 ) 

626 

627 def hash_bin(self) -> int: 

628 """ 

629 Used to detect modified object. 

630 

631 Returns: 

632 Hash considering type and value. 

633 

634 """ 

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

636 

637 @property 

638 def original_bytes(self) -> bytes: 

639 """For compatibility with TextStringObject.original_bytes.""" 

640 return self 

641 

642 def write_to_stream( 

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

644 ) -> None: 

645 if encryption_key is not None: # deprecated 

646 deprecation_no_replacement( 

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

648 ) 

649 stream.write(b"<") 

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

651 stream.write(b">") 

652 

653 def __str__(self) -> str: 

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

655 for enc in charset_to_try: 

656 try: 

657 return self.decode(enc) 

658 except UnicodeDecodeError: 

659 pass 

660 raise PdfReadError("Cannot decode ByteStringObject.") 

661 

662 

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

664 """ 

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

666 

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

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

669 to occur. 

670 """ 

671 

672 autodetect_pdfdocencoding: bool 

673 autodetect_utf16: bool 

674 utf16_bom: bytes 

675 _original_bytes: Optional[bytes] = None 

676 

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

678 original_bytes = None 

679 if isinstance(value, bytes): 

680 original_bytes = value 

681 value = value.decode("charmap") 

682 text_string_object = str.__new__(cls, value) 

683 text_string_object._original_bytes = original_bytes 

684 text_string_object.autodetect_utf16 = False 

685 text_string_object.autodetect_pdfdocencoding = False 

686 text_string_object.utf16_bom = b"" 

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

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

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

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

691 # due to already being a string. 

692 try: 

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

694 except UnicodeDecodeError as exception: 

695 logger_warning( 

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

697 source=__name__, 

698 exception=exception, 

699 initial_string=exception.object, 

700 ) 

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

702 text_string_object._original_bytes = original_bytes 

703 text_string_object.autodetect_utf16 = True 

704 text_string_object.utf16_bom = original_bytes[:2] 

705 else: 

706 try: 

707 encode_pdfdocencoding(text_string_object) 

708 text_string_object.autodetect_pdfdocencoding = True 

709 except UnicodeEncodeError: 

710 text_string_object.autodetect_utf16 = True 

711 text_string_object.utf16_bom = codecs.BOM_UTF16_BE 

712 return text_string_object 

713 

714 def clone( 

715 self, 

716 pdf_dest: Any, 

717 force_duplicate: bool = False, 

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

719 ) -> "TextStringObject": 

720 """Clone object into pdf_dest.""" 

721 obj = TextStringObject(self) 

722 obj._original_bytes = self._original_bytes 

723 obj.autodetect_pdfdocencoding = self.autodetect_pdfdocencoding 

724 obj.autodetect_utf16 = self.autodetect_utf16 

725 obj.utf16_bom = self.utf16_bom 

726 return cast( 

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

728 ) 

729 

730 def hash_bin(self) -> int: 

731 """ 

732 Used to detect modified object. 

733 

734 Returns: 

735 Hash considering type and value. 

736 

737 """ 

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

739 

740 @property 

741 def original_bytes(self) -> bytes: 

742 """ 

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

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

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

746 back-calculate what the original encoded bytes were. 

747 """ 

748 if self._original_bytes is not None: 

749 return self._original_bytes 

750 return self.get_original_bytes() 

751 

752 def get_original_bytes(self) -> bytes: 

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

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

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

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

757 # method. 

758 if self.autodetect_utf16: 

759 if self.utf16_bom == codecs.BOM_UTF16_LE: 

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

761 if self.utf16_bom == codecs.BOM_UTF16_BE: 

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

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

764 if self.autodetect_pdfdocencoding: 

765 return encode_pdfdocencoding(self) 

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

767 

768 def get_encoded_bytes(self) -> bytes: 

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

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

771 # here for trying... 

772 try: 

773 if self._original_bytes is not None: 

774 return self._original_bytes 

775 if self.autodetect_utf16: 

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

777 bytearr = encode_pdfdocencoding(self) 

778 except UnicodeEncodeError: 

779 if self.utf16_bom == codecs.BOM_UTF16_LE: 

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

781 elif self.utf16_bom == codecs.BOM_UTF16_BE: 

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

783 else: 

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

785 return bytearr 

786 

787 def write_to_stream( 

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

789 ) -> None: 

790 if encryption_key is not None: # deprecated 

791 deprecation_no_replacement( 

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

793 ) 

794 bytearr = self.get_encoded_bytes() 

795 stream.write(b"(") 

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

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

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

799 # This: 

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

801 # gives 

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

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

804 else: 

805 stream.write(c) 

806 stream.write(b")") 

807 

808 

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

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

811 prefix = b"/" 

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

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

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

815 } 

816 _LENGTH_LIMIT = 4096 

817 

818 def clone( 

819 self, 

820 pdf_dest: Any, 

821 force_duplicate: bool = False, 

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

823 ) -> "NameObject": 

824 """Clone object into pdf_dest.""" 

825 return cast( 

826 "NameObject", 

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

828 ) 

829 

830 def hash_bin(self) -> int: 

831 """ 

832 Used to detect modified object. 

833 

834 Returns: 

835 Hash considering type and value. 

836 

837 """ 

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

839 

840 def write_to_stream( 

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

842 ) -> None: 

843 if encryption_key is not None: # deprecated 

844 deprecation_no_replacement( 

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

846 ) 

847 stream.write(self.renumber()) 

848 

849 def renumber(self) -> bytes: 

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

851 if out != b"/": 

852 deprecation_no_replacement( 

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

854 "5.0.0", 

855 ) 

856 parts = [out] 

857 for c in self[1:]: 

858 if c > "~": 

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

860 else: 

861 try: 

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

863 except KeyError: 

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

865 return b"".join(parts) 

866 

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

868 """ 

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

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

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

872 underscores. 

873 

874 Returns: 

875 NameObject with sanitized name. 

876 """ 

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

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

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

880 return NameObject("/" + name) 

881 

882 @classproperty 

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

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

885 return b"/" 

886 

887 @staticmethod 

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

889 result = bytearray() 

890 i = 0 

891 while i < len(sin): 

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

893 try: 

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

895 i += 3 

896 continue 

897 except (ValueError, IndexError): 

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

899 # we change nothing and carry on 

900 pass 

901 result.append(sin[i]) 

902 i += 1 

903 return bytes(result) 

904 

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

906 

907 @staticmethod 

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

909 name = stream.read(1) 

910 if name != NameObject.prefix: 

911 raise PdfReadError("Name read error") 

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

913 try: 

914 # Name objects should represent irregular characters 

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

916 name = NameObject.unnumber(name) 

917 for enc in NameObject.CHARSETS: 

918 try: 

919 ret = name.decode(enc) 

920 return NameObject(ret) 

921 except Exception: 

922 pass 

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

924 except (UnicodeEncodeError, UnicodeDecodeError) as e: 

925 if not pdf.strict: 

926 logger_warning( 

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

928 source=__name__, 

929 name=name, 

930 ) 

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

932 raise PdfReadError( 

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

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

935 ) from e 

936 

937 

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

939 try: 

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

941 except KeyError: 

942 raise UnicodeEncodeError( 

943 "pdfdocencoding", 

944 unicode_string, 

945 -1, 

946 -1, 

947 "does not exist in translation table", 

948 ) 

949 

950 

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

952 """ 

953 Returns: 

954 True if x is None or NullObject. 

955 

956 """ 

957 return x is None or ( 

958 isinstance(x, PdfObject) 

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

960 )