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.
- {apdf-1.3 → apdf-1.4}/PKG-INFO +1 -1
- apdf-1.4/apdf/cli.py +102 -0
- {apdf-1.3 → apdf-1.4}/apdf/codec.py +74 -74
- apdf-1.4/apdf/engine.py +210 -0
- {apdf-1.3 → apdf-1.4}/apdf/fallback.py +63 -62
- apdf-1.4/apdf/operations.py +584 -0
- {apdf-1.3/apdf.obfuscation-backup-20261011-141855 → apdf-1.4/apdf.obfuscation-backup-20261013-111301}/fallback.py +8 -1
- {apdf-1.3/apdf.obfuscation-backup-20261011-141855 → apdf-1.4/apdf.obfuscation-backup-20261013-111301}/operations.py +280 -20
- {apdf-1.3 → apdf-1.4}/pyproject.toml +1 -1
- {apdf-1.3 → apdf-1.4}/uv.lock +1 -1
- apdf-1.3/apdf/cli.py +0 -102
- apdf-1.3/apdf/engine.py +0 -210
- apdf-1.3/apdf/operations.py +0 -469
- {apdf-1.3 → apdf-1.4}/.gitignore +0 -0
- {apdf-1.3 → apdf-1.4}/README.md +0 -0
- {apdf-1.3 → apdf-1.4}/apdf/__init__.py +0 -0
- {apdf-1.3 → apdf-1.4}/apdf/__main__.py +0 -0
- {apdf-1.3 → apdf-1.4}/apdf/exceptions.py +0 -0
- {apdf-1.3 → apdf-1.4}/apdf/models.py +0 -0
- {apdf-1.3 → apdf-1.4}/apdf/utils.py +0 -0
- {apdf-1.3/apdf.obfuscation-backup-20261011-141855 → apdf-1.4/apdf.obfuscation-backup-20261013-111301}/__init__.py +0 -0
- {apdf-1.3/apdf.obfuscation-backup-20261011-141855 → apdf-1.4/apdf.obfuscation-backup-20261013-111301}/__main__.py +0 -0
- {apdf-1.3/apdf.obfuscation-backup-20261011-141855 → apdf-1.4/apdf.obfuscation-backup-20261013-111301}/cli.py +0 -0
- {apdf-1.3/apdf.obfuscation-backup-20261011-141855 → apdf-1.4/apdf.obfuscation-backup-20261013-111301}/codec.py +0 -0
- {apdf-1.3/apdf.obfuscation-backup-20261011-141855 → apdf-1.4/apdf.obfuscation-backup-20261013-111301}/engine.py +0 -0
- {apdf-1.3/apdf.obfuscation-backup-20261011-141855 → apdf-1.4/apdf.obfuscation-backup-20261013-111301}/exceptions.py +0 -0
- {apdf-1.3/apdf.obfuscation-backup-20261011-141855 → apdf-1.4/apdf.obfuscation-backup-20261013-111301}/models.py +0 -0
- {apdf-1.3/apdf.obfuscation-backup-20261011-141855 → apdf-1.4/apdf.obfuscation-backup-20261013-111301}/utils.py +0 -0
{apdf-1.3 → apdf-1.4}/PKG-INFO
RENAMED
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
|
-
|
|
36
|
-
|
|
37
|
-
return f'{self.resource_name} {
|
|
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
|
|
41
|
-
if isinstance(
|
|
42
|
-
self._unicode_to_intermediate.setdefault(
|
|
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
|
|
46
|
-
if isinstance(
|
|
47
|
-
self._intermediate_to_bytes.setdefault(
|
|
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
|
-
|
|
101
|
-
if
|
|
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[
|
|
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
|
-
|
|
141
|
+
_ggkrhwxuukljqr = raw.decode(encoding, errors='strict')
|
|
142
142
|
except (LookupError, UnicodeError):
|
|
143
143
|
return
|
|
144
|
-
for
|
|
145
|
-
|
|
146
|
-
if
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
if not self._glyph_is_drawable(
|
|
151
|
-
|
|
152
|
-
raise FontEncodingError(f'{self.description} embedded subset has no drawable glyph for {
|
|
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
|
-
|
|
160
|
-
if isinstance(
|
|
161
|
-
return UV2AGL.get(ord(
|
|
162
|
-
|
|
163
|
-
codec = {'/WinAnsiEncoding': 'cp1252', '/MacRomanEncoding': 'mac_roman'}.get(
|
|
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
|
-
|
|
165
|
+
_pattpbonwpdlia = bytes((code,)).decode(codec)
|
|
166
166
|
except (UnicodeDecodeError, LookupError):
|
|
167
167
|
return None
|
|
168
|
-
return UV2AGL.get(ord(
|
|
168
|
+
return UV2AGL.get(ord(_pattpbonwpdlia))
|
|
169
169
|
|
|
170
170
|
def _validate_embedded_glyphs(self, raw: bytes, visible: str) -> None:
|
|
171
|
-
|
|
172
|
-
if
|
|
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
|
|
176
|
-
|
|
177
|
-
if
|
|
178
|
-
raise FontEncodingError(f'{self.description} has no explicit Type3 mapping for character code 0x{
|
|
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
|
|
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
|
|
187
|
-
|
|
188
|
-
if
|
|
189
|
-
missing.append(
|
|
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
|
-
|
|
200
|
+
_tpaceezyldticp = raw.decode(encoding, errors='surrogatepass')
|
|
201
201
|
except (LookupError, UnicodeError):
|
|
202
|
-
|
|
203
|
-
|
|
202
|
+
_jhcxvfjfjxrcfa = 'utf-16-be' if len(raw) % 2 == 0 else 'latin-1'
|
|
203
|
+
_tpaceezyldticp = raw.decode(_jhcxvfjfjxrcfa, errors='replace')
|
|
204
204
|
else:
|
|
205
|
-
|
|
206
|
-
return ''.join((self.font.character_map.get(character, character) for character in
|
|
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
|
-
|
|
212
|
+
_cforlarevilftd = sorted(mapping, key=len, reverse=True)
|
|
213
213
|
output: list[str] = []
|
|
214
|
-
|
|
215
|
-
while
|
|
216
|
-
for key in
|
|
217
|
-
if key and text.startswith(key,
|
|
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
|
-
|
|
219
|
+
_zzusctkjdsatuj += len(key)
|
|
220
220
|
break
|
|
221
221
|
else:
|
|
222
|
-
output.append(text[
|
|
223
|
-
|
|
222
|
+
output.append(text[_zzusctkjdsatuj])
|
|
223
|
+
_zzusctkjdsatuj += 1
|
|
224
224
|
return ''.join(output)
|
|
225
225
|
|
|
226
226
|
def encode(self, visible: str) -> bytes:
|
|
227
|
-
|
|
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
|
-
|
|
232
|
-
except (LookupError, UnicodeError) as
|
|
233
|
-
raise FontEncodingError(f'{self.description} cannot encode {visible!r}') from
|
|
234
|
-
self._validate_embedded_glyphs(
|
|
235
|
-
return
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
while
|
|
240
|
-
for key in
|
|
241
|
-
if key and
|
|
242
|
-
|
|
243
|
-
|
|
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 {
|
|
247
|
-
|
|
248
|
-
self._validate_embedded_glyphs(
|
|
249
|
-
return
|
|
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
|
-
|
|
254
|
-
|
|
255
|
-
default = float(
|
|
256
|
-
return sum((float(
|
|
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
|
|
apdf-1.4/apdf/engine.py
ADDED
|
@@ -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
|