apdf 1.3__tar.gz → 1.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. {apdf-1.3 → apdf-1.4}/PKG-INFO +1 -1
  2. apdf-1.4/apdf/cli.py +102 -0
  3. {apdf-1.3 → apdf-1.4}/apdf/codec.py +74 -74
  4. apdf-1.4/apdf/engine.py +210 -0
  5. {apdf-1.3 → apdf-1.4}/apdf/fallback.py +63 -62
  6. apdf-1.4/apdf/operations.py +584 -0
  7. {apdf-1.3/apdf.obfuscation-backup-20261011-141855 → apdf-1.4/apdf.obfuscation-backup-20261013-111301}/fallback.py +8 -1
  8. {apdf-1.3/apdf.obfuscation-backup-20261011-141855 → apdf-1.4/apdf.obfuscation-backup-20261013-111301}/operations.py +280 -20
  9. {apdf-1.3 → apdf-1.4}/pyproject.toml +1 -1
  10. {apdf-1.3 → apdf-1.4}/uv.lock +1 -1
  11. apdf-1.3/apdf/cli.py +0 -102
  12. apdf-1.3/apdf/engine.py +0 -210
  13. apdf-1.3/apdf/operations.py +0 -469
  14. {apdf-1.3 → apdf-1.4}/.gitignore +0 -0
  15. {apdf-1.3 → apdf-1.4}/README.md +0 -0
  16. {apdf-1.3 → apdf-1.4}/apdf/__init__.py +0 -0
  17. {apdf-1.3 → apdf-1.4}/apdf/__main__.py +0 -0
  18. {apdf-1.3 → apdf-1.4}/apdf/exceptions.py +0 -0
  19. {apdf-1.3 → apdf-1.4}/apdf/models.py +0 -0
  20. {apdf-1.3 → apdf-1.4}/apdf/utils.py +0 -0
  21. {apdf-1.3/apdf.obfuscation-backup-20261011-141855 → apdf-1.4/apdf.obfuscation-backup-20261013-111301}/__init__.py +0 -0
  22. {apdf-1.3/apdf.obfuscation-backup-20261011-141855 → apdf-1.4/apdf.obfuscation-backup-20261013-111301}/__main__.py +0 -0
  23. {apdf-1.3/apdf.obfuscation-backup-20261011-141855 → apdf-1.4/apdf.obfuscation-backup-20261013-111301}/cli.py +0 -0
  24. {apdf-1.3/apdf.obfuscation-backup-20261011-141855 → apdf-1.4/apdf.obfuscation-backup-20261013-111301}/codec.py +0 -0
  25. {apdf-1.3/apdf.obfuscation-backup-20261011-141855 → apdf-1.4/apdf.obfuscation-backup-20261013-111301}/engine.py +0 -0
  26. {apdf-1.3/apdf.obfuscation-backup-20261011-141855 → apdf-1.4/apdf.obfuscation-backup-20261013-111301}/exceptions.py +0 -0
  27. {apdf-1.3/apdf.obfuscation-backup-20261011-141855 → apdf-1.4/apdf.obfuscation-backup-20261013-111301}/models.py +0 -0
  28. {apdf-1.3/apdf.obfuscation-backup-20261011-141855 → apdf-1.4/apdf.obfuscation-backup-20261013-111301}/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: apdf
3
- Version: 1.3
3
+ Version: 1.4
4
4
  Summary: Meet another powerful PDF editing tool, designed to modify raw PDF data in hexadecimal format as well as Unicode text.
5
5
  Author: Abodx9
6
6
  License-Expression: MIT
apdf-1.4/apdf/cli.py ADDED
@@ -0,0 +1,102 @@
1
+ from __future__ import annotations
2
+ import argparse
3
+ import sys
4
+ from importlib.metadata import PackageNotFoundError, version
5
+ from typing import Sequence
6
+ from apdf.engine import replace_pdf_text_detailed
7
+ from apdf.exceptions import PdfReplacementError
8
+ from apdf.utils import _c, _supports_color, _BLUE, _BOLD, _CYAN, _DIM, _GRAY, _GREEN, _MAGENTA, _RED, _YELLOW
9
+
10
+ def _installed_version() -> str:
11
+ try:
12
+ return version('apdf')
13
+ except PackageNotFoundError:
14
+ return '1.0'
15
+ _BANNER = '\n_______ __________________________\n___ | ___ __ \\__ __ ' + '\\__ ____/\n__ /| |__' + '______ /_/ /_ / / /_ /_ \n_ ___ |/_____/ ____/_ /_/ /_ __/ \n/_/ |_| /_/ /_____/ /_/ \n \n'
16
+
17
+ def _print_banner() -> None:
18
+ _luspywqkvmvrcd = _installed_version()
19
+ if _supports_color():
20
+ _bnvotxiqadqonq = _BANNER.strip('\n').splitlines()
21
+ _vmkdofpuauzemf = [_RED, _RED, _BLUE, _BLUE, _CYAN]
22
+ for _dxghtiudepbhsk, _rfgkkppfsaibby in enumerate(_bnvotxiqadqonq):
23
+ _spscwgxozidwla = _vmkdofpuauzemf[_dxghtiudepbhsk] if _dxghtiudepbhsk < len(_vmkdofpuauzemf) else _CYAN
24
+ print(_c(_rfgkkppfsaibby, _spscwgxozidwla, _BOLD), file=sys.stderr)
25
+ else:
26
+ print(_BANNER.strip('\n'), file=sys.stderr)
27
+ _tdpoveqqavadxr = _c(' Low-level P' + 'DF Text Replace' + 'r', _DIM)
28
+ _rnhharzhosuggk = _c(f' v{_luspywqkvmvrcd} ', _BOLD, _MAGENTA)
29
+ _yrmhryoociptmz = _c(' by ' + 'Abodx9', _YELLOW, _BOLD)
30
+ print(f'{_tdpoveqqavadxr}{_rnhharzhosuggk}{_yrmhryoociptmz}\n', file=sys.stderr)
31
+ print(_c(' ' + '─' * 52, _GRAY), file=sys.stderr)
32
+ print(file=sys.stderr)
33
+
34
+ def _parse_replacements(values: Sequence[str]) -> dict[str, str]:
35
+ _vpoyhgaaogdcrf: dict[str, str] = {}
36
+ for _cjyjvzbuqivfrb in values:
37
+ if '=' not in _cjyjvzbuqivfrb:
38
+ raise ValueError('-' + 'r requires OLD=NEW')
39
+ old, new = _cjyjvzbuqivfrb.split('=', 1)
40
+ if not old:
41
+ raise ValueError('OL' + 'D cannot' + ' be empt' + 'y')
42
+ _vpoyhgaaogdcrf[old] = new
43
+ return _vpoyhgaaogdcrf
44
+
45
+ def _parse_pages(value: str | None) -> set[int] | None:
46
+ if value is None:
47
+ return None
48
+ pages: set[int] = set()
49
+ for _trsktuknqeatlq in value.split(','):
50
+ _tzxijeernzifec = _trsktuknqeatlq.strip()
51
+ if not _tzxijeernzifec:
52
+ raise ValueError('-' + 'p contains an ' + 'empty page' + ' number')
53
+ if '-' in _tzxijeernzifec:
54
+ _jcgcxqgvrkiuug = [piece.strip() for piece in _tzxijeernzifec.split('-')]
55
+ if len(_jcgcxqgvrkiuug) != 2 or not all((piece.isdigit() for piece in _jcgcxqgvrkiuug)):
56
+ raise ValueError(f'invalid page range: {_tzxijeernzifec!r}')
57
+ start, _bqgczixucnkfim = map(int, _jcgcxqgvrkiuug)
58
+ if start < 1 or _bqgczixucnkfim < 1:
59
+ raise ValueError('page' + ' ' + 'numbers must be 1 or greater')
60
+ if start > _bqgczixucnkfim:
61
+ raise ValueError(f'page range must be ascending: {_tzxijeernzifec!r}')
62
+ pages.update(range(start, _bqgczixucnkfim + 1))
63
+ else:
64
+ if not _tzxijeernzifec.isdigit():
65
+ raise ValueError(f'invalid page number: {_tzxijeernzifec!r}')
66
+ _mhcnzbjhwlcbnx = int(_tzxijeernzifec)
67
+ if _mhcnzbjhwlcbnx < 1:
68
+ raise ValueError('page number' + 's' + ' must be 1 or grea' + 'ter')
69
+ pages.add(_mhcnzbjhwlcbnx)
70
+ return pages
71
+
72
+ def main(argv: Sequence[str] | None=None) -> int:
73
+ _print_banner()
74
+ if argv is None:
75
+ argv = sys.argv[1:]
76
+ _azqupustegpfle = argparse.ArgumentParser(description='Replace Unicode and' + ' Hex text directly in PDF ' + 'content stream' + 's.')
77
+ _azqupustegpfle.add_argument('input', help='Inp' + 'ut PDF')
78
+ _azqupustegpfle.add_argument('output', nargs='?', default='output.pdf', help='Outpu' + 't PDF')
79
+ _azqupustegpfle.add_argument('-r', action='append', default=[], metavar='OLD=NEW', dest='replace', help='Replac' + 'e: Unicode replacement; may be sup' + 'plied more' + ' than once')
80
+ _azqupustegpfle.add_argument('-p', metavar='PAGES', dest='pages', help='Pages to ed' + 'it, e.g' + '. 2, 2,3,4, or 1-5; defaults to all pages')
81
+ _azqupustegpfle.add_argument('-nopa', action='store_true', dest='no_preserve_advance', help='No preserve advance: Do not comp' + 'e' + 'nsate TJ widths after replacements')
82
+ _azqupustegpfle.add_argument('-s', action='store_true', dest='strict', help='Strict: Fail if text is m' + 'issing or the selected font' + ' cannot encode it')
83
+ _azqupustegpfle.add_argument('-ff', metavar='FONT.ttf', dest='fallback_font', help="Fallback font: TrueType font to embed when the PDF's sub" + 'set font lacks a glyph; def' + 'aults to a commo' + 'n system sans-serif font')
84
+ _azqupustegpfle.add_argument('-no-ff', action='store_true', dest='no_font_fallback', help='No font ' + 'f' + 'allback: Reject missing glyphs inste' + 'ad of embedding a fallback font')
85
+ _wpxvtcvrscmlcr = _azqupustegpfle.parse_args(argv)
86
+ try:
87
+ replacements = _parse_replacements(_wpxvtcvrscmlcr.replace)
88
+ pages = _parse_pages(_wpxvtcvrscmlcr.pages)
89
+ if not replacements:
90
+ _azqupustegpfle.error('provide' + ' a' + 't least ' + 'one -r OLD=NEW')
91
+ _udobssarculrcm = replace_pdf_text_detailed(_wpxvtcvrscmlcr.input, _wpxvtcvrscmlcr.output, replacements, preserve_advance=not _wpxvtcvrscmlcr.no_preserve_advance, strict=_wpxvtcvrscmlcr.strict, fallback_font=_wpxvtcvrscmlcr.fallback_font, font_fallback=not _wpxvtcvrscmlcr.no_font_fallback, pages=pages)
92
+ except (OSError, PdfReplacementError, ValueError) as _gzcgcunigakueg:
93
+ _azqupustegpfle.exit(2, _c(f'error: {_gzcgcunigakueg}\n', _RED))
94
+ except Exception as _gzcgcunigakueg:
95
+ _azqupustegpfle.exit(2, _c(f'error: An unexpected error occurred: {type(_gzcgcunigakueg).__name__}: {_gzcgcunigakueg}\n', _RED))
96
+ print(_c(f'Replaced {_udobssarculrcm.replacements} occurrence(s) in {_udobssarculrcm.changed_streams}/{_udobssarculrcm.scanned_streams} stream(s) in {_udobssarculrcm.elapsed_seconds:.3f}s.', _GREEN))
97
+ if _udobssarculrcm.missing:
98
+ print(_c('Not f' + 'ound: ' + ', '.join((repr(value) for value in _udobssarculrcm.missing)), _RED))
99
+ if _udobssarculrcm.encoding_failures:
100
+ print(_c(f'Skipped {len(_udobssarculrcm.encoding_failures)} unencodable replacement(s).', _RED))
101
+ print(_c(f'Saved: {_wpxvtcvrscmlcr.output}', _GREEN, _BOLD))
102
+ return 0
@@ -32,19 +32,19 @@ class FontCodec:
32
32
 
33
33
  @property
34
34
  def description(self) -> str:
35
- _dtonbtsnepubbb = str(self.pdf_font.get('/Subtype', 'unknown'))
36
- _abqsmvngjipqzp = str(self.pdf_font.get('/BaseFont', 'unknown'))
37
- return f'{self.resource_name} {_dtonbtsnepubbb} {_abqsmvngjipqzp}'
35
+ _fwisjtxoozvyes = str(self.pdf_font.get('/Subtype', 'unknown'))
36
+ _sdsulzvqjnzjej = str(self.pdf_font.get('/BaseFont', 'unknown'))
37
+ return f'{self.resource_name} {_fwisjtxoozvyes} {_sdsulzvqjnzjej}'
38
38
 
39
39
  def _build_inverse_maps(self) -> None:
40
- for _cpszezpkxhlcgm, _rrrtuzutsrcpaf in self.font.character_map.items():
41
- if isinstance(_cpszezpkxhlcgm, str) and isinstance(_rrrtuzutsrcpaf, str):
42
- self._unicode_to_intermediate.setdefault(_rrrtuzutsrcpaf, _cpszezpkxhlcgm)
40
+ for _gtlnoasxovzwkd, _okilniypkixbng in self.font.character_map.items():
41
+ if isinstance(_gtlnoasxovzwkd, str) and isinstance(_okilniypkixbng, str):
42
+ self._unicode_to_intermediate.setdefault(_okilniypkixbng, _gtlnoasxovzwkd)
43
43
  encoding = self.font.encoding
44
44
  if isinstance(encoding, dict):
45
- for _mijbpyiwaochif, _nnbahipgbrogqf in encoding.items():
46
- if isinstance(_mijbpyiwaochif, int) and 0 <= _mijbpyiwaochif <= 255 and isinstance(_nnbahipgbrogqf, str):
47
- self._intermediate_to_bytes.setdefault(_nnbahipgbrogqf, bytes((_mijbpyiwaochif,)))
45
+ for _gjilmuozklwhre, _grstvsghjoxrnq in encoding.items():
46
+ if isinstance(_gjilmuozklwhre, int) and 0 <= _gjilmuozklwhre <= 255 and isinstance(_grstvsghjoxrnq, str):
47
+ self._intermediate_to_bytes.setdefault(_grstvsghjoxrnq, bytes((_gjilmuozklwhre,)))
48
48
 
49
49
  def _load_glyph_inventory(self) -> None:
50
50
  encoding = _deref(self.pdf_font.get('/Encoding'))
@@ -97,10 +97,10 @@ class FontCodec:
97
97
  def _gid_for_cid(self, cid: int) -> int:
98
98
  if self._cid_to_gid_bytes is None:
99
99
  return cid
100
- _dqxyefsqqlowdi = cid * 2
101
- if _dqxyefsqqlowdi + 2 > len(self._cid_to_gid_bytes):
100
+ _jatgoccybumvbw = cid * 2
101
+ if _jatgoccybumvbw + 2 > len(self._cid_to_gid_bytes):
102
102
  return 0
103
- return int.from_bytes(self._cid_to_gid_bytes[_dqxyefsqqlowdi:_dqxyefsqqlowdi + 2], 'big')
103
+ return int.from_bytes(self._cid_to_gid_bytes[_jatgoccybumvbw:_jatgoccybumvbw + 2], 'big')
104
104
 
105
105
  def _glyph_is_drawable(self, gid: int) -> bool:
106
106
  cached = self._drawable_glyphs.get(gid)
@@ -138,55 +138,55 @@ class FontCodec:
138
138
  if not isinstance(encoding, str):
139
139
  return
140
140
  try:
141
- _rzcdzoakbxyqft = raw.decode(encoding, errors='strict')
141
+ _ggkrhwxuukljqr = raw.decode(encoding, errors='strict')
142
142
  except (LookupError, UnicodeError):
143
143
  return
144
- for _pozbahvroafvai in _rzcdzoakbxyqft:
145
- _uxqxdjmxeovyve = self.font.character_map.get(_pozbahvroafvai, _pozbahvroafvai)
146
- if _uxqxdjmxeovyve and all((character.isspace() for character in _uxqxdjmxeovyve)):
144
+ for _qyrzffrmdzxhez in _ggkrhwxuukljqr:
145
+ _unjmlnhyycmucn = self.font.character_map.get(_qyrzffrmdzxhez, _qyrzffrmdzxhez)
146
+ if _unjmlnhyycmucn and all((character.isspace() for character in _unjmlnhyycmucn)):
147
147
  continue
148
- _fhykuxrvpsrlom = ord(_pozbahvroafvai)
149
- _txhbexxrxmbeky = self._gid_for_cid(_fhykuxrvpsrlom)
150
- if not self._glyph_is_drawable(_txhbexxrxmbeky):
151
- _hwpecwftjyvqmd = ' '.join((f'U+{ord(character):04X}' for character in _uxqxdjmxeovyve)) or f'CID {_fhykuxrvpsrlom}'
152
- raise FontEncodingError(f'{self.description} embedded subset has no drawable glyph for {_hwpecwftjyvqmd} (CID {_fhykuxrvpsrlom}, GID {_txhbexxrxmbeky})')
148
+ _yfptaenaqsasji = ord(_qyrzffrmdzxhez)
149
+ _ubvtskzofhgavi = self._gid_for_cid(_yfptaenaqsasji)
150
+ if not self._glyph_is_drawable(_ubvtskzofhgavi):
151
+ _egiaintnwxlucm = ' '.join((f'U+{ord(character):04X}' for character in _unjmlnhyycmucn)) or f'CID {_yfptaenaqsasji}'
152
+ raise FontEncodingError(f'{self.description} embedded subset has no drawable glyph for {_egiaintnwxlucm} (CID {_yfptaenaqsasji}, GID {_ubvtskzofhgavi})')
153
153
 
154
154
  def _glyph_name_for_code(self, code: int) -> str | None:
155
155
  if code in self._difference_glyphs:
156
156
  return self._difference_glyphs[code]
157
157
  encoding = self.font.encoding
158
158
  if isinstance(encoding, dict):
159
- _xdjugpuibuolde = encoding.get(code)
160
- if isinstance(_xdjugpuibuolde, str) and len(_xdjugpuibuolde) == 1:
161
- return UV2AGL.get(ord(_xdjugpuibuolde))
162
- _ipxmujfdbqmznv = str(self.pdf_font.get('/Encoding', ''))
163
- codec = {'/WinAnsiEncoding': 'cp1252', '/MacRomanEncoding': 'mac_roman'}.get(_ipxmujfdbqmznv, 'latin-1')
159
+ _wnpbluhafmlfxt = encoding.get(code)
160
+ if isinstance(_wnpbluhafmlfxt, str) and len(_wnpbluhafmlfxt) == 1:
161
+ return UV2AGL.get(ord(_wnpbluhafmlfxt))
162
+ _msnuxxjktrptvl = str(self.pdf_font.get('/Encoding', ''))
163
+ codec = {'/WinAnsiEncoding': 'cp1252', '/MacRomanEncoding': 'mac_roman'}.get(_msnuxxjktrptvl, 'latin-1')
164
164
  try:
165
- _yhbgyxeyqreqah = bytes((code,)).decode(codec)
165
+ _pattpbonwpdlia = bytes((code,)).decode(codec)
166
166
  except (UnicodeDecodeError, LookupError):
167
167
  return None
168
- return UV2AGL.get(ord(_yhbgyxeyqreqah))
168
+ return UV2AGL.get(ord(_pattpbonwpdlia))
169
169
 
170
170
  def _validate_embedded_glyphs(self, raw: bytes, visible: str) -> None:
171
- _aojpgqqhfkrpwi = str(self.pdf_font.get('/Subtype', ''))
172
- if _aojpgqqhfkrpwi == '/Type3':
171
+ _vqsenaebmchtxj = str(self.pdf_font.get('/Subtype', ''))
172
+ if _vqsenaebmchtxj == '/Type3':
173
173
  encoding = self.font.encoding
174
174
  if isinstance(encoding, dict):
175
- for _gcjnhinzjlcpmh in raw:
176
- _mnkuifxpsuezme = encoding.get(_gcjnhinzjlcpmh, chr(_gcjnhinzjlcpmh))
177
- if _mnkuifxpsuezme not in self.font.character_map:
178
- raise FontEncodingError(f'{self.description} has no explicit Type3 mapping for character code 0x{_gcjnhinzjlcpmh:02X}')
175
+ for _pflflsknediqaw in raw:
176
+ _laybgcolzwxzyj = encoding.get(_pflflsknediqaw, chr(_pflflsknediqaw))
177
+ if _laybgcolzwxzyj not in self.font.character_map:
178
+ raise FontEncodingError(f'{self.description} has no explicit Type3 mapping for character code 0x{_pflflsknediqaw:02X}')
179
179
  if self.decode(raw) != visible:
180
180
  raise FontEncodingError(f'{self.description} has no reversible glyph mapping for {visible!r}')
181
181
  return
182
- if _aojpgqqhfkrpwi == '/Type0':
182
+ if _vqsenaebmchtxj == '/Type0':
183
183
  self._validate_type0_glyph_outlines(raw)
184
184
  if self._embedded_glyphs is not None:
185
185
  missing: list[str] = []
186
- for _gcjnhinzjlcpmh in raw:
187
- _vsecktyjcjrjfy = self._glyph_name_for_code(_gcjnhinzjlcpmh)
188
- if _vsecktyjcjrjfy and _vsecktyjcjrjfy not in self._embedded_glyphs:
189
- missing.append(_vsecktyjcjrjfy)
186
+ for _pflflsknediqaw in raw:
187
+ _jctlgvfwnicjwx = self._glyph_name_for_code(_pflflsknediqaw)
188
+ if _jctlgvfwnicjwx and _jctlgvfwnicjwx not in self._embedded_glyphs:
189
+ missing.append(_jctlgvfwnicjwx)
190
190
  if missing:
191
191
  names = ', '.join(sorted(set(missing)))
192
192
  raise FontEncodingError(f'{self.description} embedded subset lacks glyph(s): {names}')
@@ -197,63 +197,63 @@ class FontCodec:
197
197
  encoding = self.font.encoding
198
198
  if isinstance(encoding, str):
199
199
  try:
200
- _prjxvllsrsceqe = raw.decode(encoding, errors='surrogatepass')
200
+ _tpaceezyldticp = raw.decode(encoding, errors='surrogatepass')
201
201
  except (LookupError, UnicodeError):
202
- _vvtbwoypfiegky = 'utf-16-be' if len(raw) % 2 == 0 else 'latin-1'
203
- _prjxvllsrsceqe = raw.decode(_vvtbwoypfiegky, errors='replace')
202
+ _jhcxvfjfjxrcfa = 'utf-16-be' if len(raw) % 2 == 0 else 'latin-1'
203
+ _tpaceezyldticp = raw.decode(_jhcxvfjfjxrcfa, errors='replace')
204
204
  else:
205
- _prjxvllsrsceqe = ''.join((encoding.get(byte, chr(byte)) for byte in raw))
206
- return ''.join((self.font.character_map.get(character, character) for character in _prjxvllsrsceqe))
205
+ _tpaceezyldticp = ''.join((encoding.get(byte, chr(byte)) for byte in raw))
206
+ return ''.join((self.font.character_map.get(character, character) for character in _tpaceezyldticp))
207
207
 
208
208
  @staticmethod
209
209
  def _greedy_map(text: str, mapping: Mapping[str, str]) -> str:
210
210
  if not mapping:
211
211
  return text
212
- _cfguxjxbjcwaau = sorted(mapping, key=len, reverse=True)
212
+ _cforlarevilftd = sorted(mapping, key=len, reverse=True)
213
213
  output: list[str] = []
214
- _qjbwwztaspnkuz = 0
215
- while _qjbwwztaspnkuz < len(text):
216
- for key in _cfguxjxbjcwaau:
217
- if key and text.startswith(key, _qjbwwztaspnkuz):
214
+ _zzusctkjdsatuj = 0
215
+ while _zzusctkjdsatuj < len(text):
216
+ for key in _cforlarevilftd:
217
+ if key and text.startswith(key, _zzusctkjdsatuj):
218
218
  output.append(mapping[key])
219
- _qjbwwztaspnkuz += len(key)
219
+ _zzusctkjdsatuj += len(key)
220
220
  break
221
221
  else:
222
- output.append(text[_qjbwwztaspnkuz])
223
- _qjbwwztaspnkuz += 1
222
+ output.append(text[_zzusctkjdsatuj])
223
+ _zzusctkjdsatuj += 1
224
224
  return ''.join(output)
225
225
 
226
226
  def encode(self, visible: str) -> bytes:
227
- _bwcwwxglrozrgj = self._greedy_map(visible, self._unicode_to_intermediate)
227
+ _smuyjzjannqguq = self._greedy_map(visible, self._unicode_to_intermediate)
228
228
  encoding = self.font.encoding
229
229
  if isinstance(encoding, str):
230
230
  try:
231
- _zwmhatqlzblobv = _bwcwwxglrozrgj.encode(encoding, errors='strict')
232
- except (LookupError, UnicodeError) as _dbmfrervtovraa:
233
- raise FontEncodingError(f'{self.description} cannot encode {visible!r}') from _dbmfrervtovraa
234
- self._validate_embedded_glyphs(_zwmhatqlzblobv, visible)
235
- return _zwmhatqlzblobv
236
- _habhwtgwcviujd = sorted(self._intermediate_to_bytes, key=len, reverse=True)
237
- _qeoxigncjzbjdy = bytearray()
238
- _plyeupfjypdihj = 0
239
- while _plyeupfjypdihj < len(_bwcwwxglrozrgj):
240
- for key in _habhwtgwcviujd:
241
- if key and _bwcwwxglrozrgj.startswith(key, _plyeupfjypdihj):
242
- _qeoxigncjzbjdy.extend(self._intermediate_to_bytes[key])
243
- _plyeupfjypdihj += len(key)
231
+ _vllohhdgfteusd = _smuyjzjannqguq.encode(encoding, errors='strict')
232
+ except (LookupError, UnicodeError) as _ewqpmknbgmswnz:
233
+ raise FontEncodingError(f'{self.description} cannot encode {visible!r}') from _ewqpmknbgmswnz
234
+ self._validate_embedded_glyphs(_vllohhdgfteusd, visible)
235
+ return _vllohhdgfteusd
236
+ _pdgwbgnnwdjilr = sorted(self._intermediate_to_bytes, key=len, reverse=True)
237
+ _vgbbptfknztcnt = bytearray()
238
+ _vdtdodqdcncmhs = 0
239
+ while _vdtdodqdcncmhs < len(_smuyjzjannqguq):
240
+ for key in _pdgwbgnnwdjilr:
241
+ if key and _smuyjzjannqguq.startswith(key, _vdtdodqdcncmhs):
242
+ _vgbbptfknztcnt.extend(self._intermediate_to_bytes[key])
243
+ _vdtdodqdcncmhs += len(key)
244
244
  break
245
245
  else:
246
- raise FontEncodingError(f'{self.description} cannot encode character {_bwcwwxglrozrgj[_plyeupfjypdihj]!r} in {visible!r}')
247
- _zwmhatqlzblobv = bytes(_qeoxigncjzbjdy)
248
- self._validate_embedded_glyphs(_zwmhatqlzblobv, visible)
249
- return _zwmhatqlzblobv
246
+ raise FontEncodingError(f'{self.description} cannot encode character {_smuyjzjannqguq[_vdtdodqdcncmhs]!r} in {visible!r}')
247
+ _vllohhdgfteusd = bytes(_vgbbptfknztcnt)
248
+ self._validate_embedded_glyphs(_vllohhdgfteusd, visible)
249
+ return _vllohhdgfteusd
250
250
 
251
251
  def width(self, visible: str) -> float:
252
252
  try:
253
- _bvfcdxkscamzsr = self._greedy_map(visible, self._unicode_to_intermediate)
254
- _ctwrdofcwpavtu = self.font.character_widths
255
- default = float(_ctwrdofcwpavtu.get('default', 0.0))
256
- return sum((float(_ctwrdofcwpavtu.get(character, default)) for character in _bvfcdxkscamzsr))
253
+ _iqaaghkxvvufsn = self._greedy_map(visible, self._unicode_to_intermediate)
254
+ _tbcdhpmrdrmcet = self.font.character_widths
255
+ default = float(_tbcdhpmrdrmcet.get('default', 0.0))
256
+ return sum((float(_tbcdhpmrdrmcet.get(character, default)) for character in _iqaaghkxvvufsn))
257
257
  except Exception:
258
258
  return 0.0
259
259
 
@@ -0,0 +1,210 @@
1
+ from __future__ import annotations
2
+ import os
3
+ import sys
4
+ import tempfile
5
+ import time
6
+ from pathlib import Path
7
+ from typing import Any, Iterable, Mapping, Sequence
8
+ from pypdf import PdfReader, PdfWriter
9
+ from pypdf.errors import DependencyError
10
+ from pypdf.generic import ArrayObject, ContentStream, DictionaryObject, NameObject, StreamObject
11
+ from apdf.codec import _font_codecs
12
+ from apdf.exceptions import PdfReplacementError, PdfDependencyError, PdfPermissionError, PdfFileNotFoundError
13
+ from apdf.fallback import FallbackFontRegistry
14
+ from apdf.models import Replacement, ReplacementReport
15
+ from apdf.operations import _rewrite_operations
16
+ from apdf.utils import _deref, _object_key, _c, _BLUE
17
+
18
+ def _set_decoded_stream_data(stream: StreamObject, data: bytes) -> None:
19
+ try:
20
+ stream.set_data(data)
21
+ except Exception as _uzqxnhhwivanmc:
22
+ raise PdfReplacementError('Cannot r' + 'ewrite a content stream w' + 'ith its current PD' + 'F filters') from _uzqxnhhwivanmc
23
+
24
+ def _rewrite_stream(stream: StreamObject, pdf: PdfWriter, resources: Any, replacements: Sequence[Replacement], preserve_advance: bool, report: ReplacementReport, matched: set[str], fallback_registry: FallbackFontRegistry, page_number: int) -> None:
25
+ report.scanned_streams += 1
26
+ content = ContentStream(stream, pdf)
27
+ operations = content.operations
28
+ codecs = _font_codecs(resources)
29
+ count = _rewrite_operations(operations, codecs, replacements, preserve_advance, report, matched, page_number, lambda original, size, visible: fallback_registry.resolve(original, size, visible, resources))
30
+ if count:
31
+ content.operations = operations
32
+ _set_decoded_stream_data(stream, content.get_data())
33
+ report.changed_streams += 1
34
+
35
+ def _content_streams(owner: Any) -> Iterable[StreamObject]:
36
+ _avprdqgqwmnmeg = _deref(owner.get('/Contents'))
37
+ if _avprdqgqwmnmeg is None:
38
+ return
39
+ if isinstance(_avprdqgqwmnmeg, (list, ArrayObject)):
40
+ for _txyretiadsatpw in _avprdqgqwmnmeg:
41
+ _pqbfxcufmqdpgp = _deref(_txyretiadsatpw)
42
+ if isinstance(_pqbfxcufmqdpgp, StreamObject):
43
+ yield _pqbfxcufmqdpgp
44
+ elif isinstance(_avprdqgqwmnmeg, StreamObject):
45
+ yield _avprdqgqwmnmeg
46
+
47
+ def _shallow_stream_clone(stream: StreamObject, writer: PdfWriter) -> Any:
48
+ _foypyflaryhsuf = type(stream)()
49
+ for key, _qidrprqgtxbosn in stream.items():
50
+ if str(key) != '/Length':
51
+ _foypyflaryhsuf[key] = _qidrprqgtxbosn
52
+ _foypyflaryhsuf._data = stream._data
53
+ return writer._add_object(_foypyflaryhsuf)
54
+
55
+ def _isolate_resources(resources: Any, writer: PdfWriter, forms: dict[tuple[Any, ...], Any]) -> DictionaryObject:
56
+ _kwpmlbkldqrhil = _deref(resources)
57
+ _rimlebyulqzort = DictionaryObject()
58
+ if not _kwpmlbkldqrhil:
59
+ return _rimlebyulqzort
60
+ for key, _zzmtqrfztivzcm in _kwpmlbkldqrhil.items():
61
+ _rimlebyulqzort[key] = _zzmtqrfztivzcm
62
+ _dyjjieirnhpvnd = _deref(_kwpmlbkldqrhil.get('/Font'))
63
+ if _dyjjieirnhpvnd is not None:
64
+ _rimlebyulqzort[NameObject('/Font')] = DictionaryObject(_dyjjieirnhpvnd.items())
65
+ _vypkqemsdptblt = _deref(_kwpmlbkldqrhil.get('/XObject'))
66
+ if _vypkqemsdptblt is not None:
67
+ _mjkzsoxjapafpa = DictionaryObject()
68
+ _rimlebyulqzort[NameObject('/XObject')] = _mjkzsoxjapafpa
69
+ for name, _cikijagztdgvdn in _vypkqemsdptblt.items():
70
+ _gjkfrcethbpnvf = _deref(_cikijagztdgvdn)
71
+ if not isinstance(_gjkfrcethbpnvf, StreamObject) or _gjkfrcethbpnvf.get('/Subtype') != '/Form':
72
+ _mjkzsoxjapafpa[name] = _cikijagztdgvdn
73
+ continue
74
+ key = _object_key(_gjkfrcethbpnvf)
75
+ _ajdwclzfwnexwg = forms.get(key)
76
+ if _ajdwclzfwnexwg is None:
77
+ _ajdwclzfwnexwg = _shallow_stream_clone(_gjkfrcethbpnvf, writer)
78
+ forms[key] = _ajdwclzfwnexwg
79
+ _cvfbdeeeptkbjw = _deref(_ajdwclzfwnexwg)
80
+ _tglkbhlmzsvdoe = _gjkfrcethbpnvf.get('/Resources')
81
+ if _tglkbhlmzsvdoe is not None:
82
+ _cvfbdeeeptkbjw[NameObject('/Resources')] = _isolate_resources(_tglkbhlmzsvdoe, writer, forms)
83
+ _mjkzsoxjapafpa[name] = _ajdwclzfwnexwg
84
+ return _rimlebyulqzort
85
+
86
+ def _isolate_contents(contents: Any, writer: PdfWriter) -> Any:
87
+ _jrimzwzmmvxizi = _deref(contents)
88
+ if isinstance(_jrimzwzmmvxizi, (list, ArrayObject)):
89
+ _lbcempgxkgbhvm = ArrayObject()
90
+ for _xzhtzaunhnlubj in _jrimzwzmmvxizi:
91
+ _efisgocfxjygnk = _deref(_xzhtzaunhnlubj)
92
+ _lbcempgxkgbhvm.append(_shallow_stream_clone(_efisgocfxjygnk, writer) if isinstance(_efisgocfxjygnk, StreamObject) else _xzhtzaunhnlubj)
93
+ return _lbcempgxkgbhvm
94
+ if isinstance(_jrimzwzmmvxizi, StreamObject):
95
+ return _shallow_stream_clone(_jrimzwzmmvxizi, writer)
96
+ return contents
97
+
98
+ def _isolate_selected_page(page: Any, writer: PdfWriter) -> None:
99
+ _hynfvpnwoqktzv = page.get('/Contents')
100
+ if _hynfvpnwoqktzv is not None:
101
+ page[NameObject('/Contents')] = _isolate_contents(_hynfvpnwoqktzv, writer)
102
+ _lpvstzdynixypx = page.get('/Resources')
103
+ if _lpvstzdynixypx is not None:
104
+ page[NameObject('/Resources')] = _isolate_resources(_lpvstzdynixypx, writer, {})
105
+
106
+ def _selected_page_numbers(pages: Iterable[int] | int | None, page_count: int) -> set[int]:
107
+ if pages is None:
108
+ return set(range(1, page_count + 1))
109
+ if isinstance(pages, bool):
110
+ raise ValueError('pages must contain page numbe' + 'rs, not a boolean')
111
+ if isinstance(pages, int):
112
+ _dlpxeyfjatniau = {pages}
113
+ elif isinstance(pages, (str, bytes)):
114
+ raise ValueError('pages must be page numb' + 'ers; use the CLI -p option for ' + 'ranges')
115
+ else:
116
+ _dlpxeyfjatniau = {int(page) for page in pages}
117
+ if not _dlpxeyfjatniau:
118
+ raise ValueError('at least' + ' on' + 'e page mu' + 'st be selected')
119
+ _tnnaqceezrapma = sorted((page for page in _dlpxeyfjatniau if page < 1 or page > page_count))
120
+ if _tnnaqceezrapma:
121
+ _pghyiyrrqhgdje = ', '.join((str(page) for page in _tnnaqceezrapma))
122
+ raise ValueError(f'page selection out of range: {_pghyiyrrqhgdje}; PDF has {page_count} page(s)')
123
+ return _dlpxeyfjatniau
124
+
125
+ def _rewrite_forms(resources: Any, pdf: PdfWriter, replacements: Sequence[Replacement], preserve_advance: bool, report: ReplacementReport, matched: set[str], visited: set[tuple[Any, ...]], fallback_registry: FallbackFontRegistry, page_number: int) -> None:
126
+ resources = _deref(resources)
127
+ xobjects = _deref(resources.get('/XObject', {})) if resources else {}
128
+ for _, reference in getattr(xobjects, 'items', lambda: [])():
129
+ form = _deref(reference)
130
+ if not isinstance(form, StreamObject) or form.get('/Subtype') != '/Form':
131
+ continue
132
+ key = _object_key(form)
133
+ if key in visited:
134
+ continue
135
+ visited.add(key)
136
+ form_resources = _deref(form.get('/Resources')) or resources
137
+ _rewrite_stream(form, pdf, form_resources, replacements, preserve_advance, report, matched, fallback_registry, page_number)
138
+ _rewrite_forms(form_resources, pdf, replacements, preserve_advance, report, matched, visited, fallback_registry, page_number)
139
+
140
+ def replace_pdf_text_detailed(input_pdf: str | os.PathLike[str], output_pdf: str | os.PathLike[str], replacements: Mapping[str, str], *, preserve_advance: bool=True, strict: bool=False, fallback_font: str | os.PathLike[str] | None=None, font_fallback: bool=True, pages: Iterable[int] | int | None=None) -> ReplacementReport:
141
+ _zsjjgpavxoyeiy = time.perf_counter()
142
+ _mousvcnaqcrmzc = [Replacement(str(old), str(new)) for old, new in replacements.items() if old]
143
+ if not _mousvcnaqcrmzc:
144
+ raise ValueError('At least one non-e' + 'mpty source string is ' + 'req' + 'uired')
145
+ _tfihdvgamexdzh = Path(input_pdf)
146
+ _bivdralwetvsjy = Path(output_pdf)
147
+ if not _tfihdvgamexdzh.is_file():
148
+ raise PdfFileNotFoundError(f'File not found: {_tfihdvgamexdzh}')
149
+ _bivdralwetvsjy.parent.mkdir(parents=True, exist_ok=True)
150
+ try:
151
+ _vztlargbsowaxh = PdfReader(_tfihdvgamexdzh, strict=False)
152
+ except DependencyError as _yehswjecufgmvw:
153
+ raise PdfDependencyError(f"Missing dependency for encrypted PDF: {_yehswjecufgmvw}\nPlease install it, e.g., 'uv pip install cryptography'") from _yehswjecufgmvw
154
+ except PermissionError as _yehswjecufgmvw:
155
+ raise PdfPermissionError(f'Permission denied when opening {_tfihdvgamexdzh}. The PDF might be opened by another application.') from _yehswjecufgmvw
156
+ if _vztlargbsowaxh.is_encrypted:
157
+ raise PdfReplacementError('Encrypted PDFs are n' + 'ot supported without a password')
158
+ writer = PdfWriter(clone_from=_vztlargbsowaxh)
159
+ _jtmgtsgyhkksup = _selected_page_numbers(pages, len(writer.pages))
160
+ _wgpmzsmmkjcaaw = len(_jtmgtsgyhkksup) != len(writer.pages)
161
+ _dgfdvonchmlydh = FallbackFontRegistry(writer, explicit_font=fallback_font, enabled=font_fallback)
162
+ _koppsudxhgbxvc = ReplacementReport(pages=len(writer.pages))
163
+ _xgcotpgrimnkmc: set[str] = set()
164
+ _dcnqkkuuzntnmg: set[tuple[Any, ...]] = set()
165
+ for _fmefrtitqvbple, _lymbmtoympvook in enumerate(writer.pages, start=1):
166
+ if _fmefrtitqvbple not in _jtmgtsgyhkksup:
167
+ continue
168
+ print(_c(f'Processing page {_fmefrtitqvbple}/{_koppsudxhgbxvc.pages}', _BLUE), file=sys.stderr)
169
+ if _wgpmzsmmkjcaaw:
170
+ _isolate_selected_page(_lymbmtoympvook, writer)
171
+ _vciywsdpxmsrtj = _deref(_lymbmtoympvook.get('/Resources', {}))
172
+ for _uolfadhbwtgzec in _content_streams(_lymbmtoympvook):
173
+ key = _object_key(_uolfadhbwtgzec)
174
+ if key in _dcnqkkuuzntnmg:
175
+ continue
176
+ _dcnqkkuuzntnmg.add(key)
177
+ _rewrite_stream(_uolfadhbwtgzec, writer, _vciywsdpxmsrtj, _mousvcnaqcrmzc, preserve_advance, _koppsudxhgbxvc, _xgcotpgrimnkmc, _dgfdvonchmlydh, _fmefrtitqvbple)
178
+ _rewrite_forms(_vciywsdpxmsrtj, writer, _mousvcnaqcrmzc, preserve_advance, _koppsudxhgbxvc, _xgcotpgrimnkmc, _dcnqkkuuzntnmg, _dgfdvonchmlydh, _fmefrtitqvbple)
179
+ _dgfdvonchmlydh.finalize()
180
+ _koppsudxhgbxvc.missing = [rule.old for rule in _mousvcnaqcrmzc if rule.old not in _xgcotpgrimnkmc]
181
+ _koppsudxhgbxvc.elapsed_seconds = time.perf_counter() - _zsjjgpavxoyeiy
182
+ if strict and (_koppsudxhgbxvc.missing or _koppsudxhgbxvc.encoding_failures):
183
+ _xvjnkuuysvwlfj = []
184
+ if _koppsudxhgbxvc.missing:
185
+ _xvjnkuuysvwlfj.append(f'not found: {_koppsudxhgbxvc.missing!r}')
186
+ if _koppsudxhgbxvc.encoding_failures:
187
+ _xvjnkuuysvwlfj.append(f'encoding failures: {_koppsudxhgbxvc.encoding_failures!r}')
188
+ raise PdfReplacementError('; '.join(_xvjnkuuysvwlfj))
189
+ writer.compress_identical_objects(remove_duplicates=True, remove_unreferenced=True)
190
+ _rpdadeikbebjkm: str | None = None
191
+ try:
192
+ try:
193
+ with tempfile.NamedTemporaryFile(mode='wb', prefix=f'.{_bivdralwetvsjy.name}.', suffix='.tmp', dir=_bivdralwetvsjy.parent, delete=False) as _kegxlnzjvrxfhw:
194
+ _rpdadeikbebjkm = _kegxlnzjvrxfhw.name
195
+ writer.write(_kegxlnzjvrxfhw)
196
+ _ydjqprcywncvtl = PdfReader(_rpdadeikbebjkm, strict=False)
197
+ if len(_ydjqprcywncvtl.pages) != _koppsudxhgbxvc.pages:
198
+ raise PdfReplacementError('Output verification found ' + 'a page-count mismatch')
199
+ os.replace(_rpdadeikbebjkm, _bivdralwetvsjy)
200
+ _rpdadeikbebjkm = None
201
+ except PermissionError as _yehswjecufgmvw:
202
+ raise PdfPermissionError(f'Permission denied when saving to {_bivdralwetvsjy}. The PDF might be opened by another application.') from _yehswjecufgmvw
203
+ finally:
204
+ if _rpdadeikbebjkm and os.path.exists(_rpdadeikbebjkm):
205
+ os.unlink(_rpdadeikbebjkm)
206
+ _koppsudxhgbxvc.elapsed_seconds = time.perf_counter() - _zsjjgpavxoyeiy
207
+ return _koppsudxhgbxvc
208
+
209
+ def replace_pdf_text(input_pdf: str | os.PathLike[str], output_pdf: str | os.PathLike[str], replacements: Mapping[str, str], **kwargs: Any) -> int:
210
+ return replace_pdf_text_detailed(input_pdf, output_pdf, replacements, **kwargs).replacements