apdf 1.2__tar.gz → 1.3__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.2 → apdf-1.3}/PKG-INFO +3 -3
- {apdf-1.2 → apdf-1.3}/README.md +2 -2
- {apdf-1.2 → apdf-1.3}/apdf/__init__.py +0 -0
- {apdf-1.2 → apdf-1.3}/apdf/__main__.py +0 -0
- apdf-1.3/apdf/cli.py +102 -0
- apdf-1.3/apdf/codec.py +270 -0
- apdf-1.3/apdf/engine.py +210 -0
- {apdf-1.2 → apdf-1.3}/apdf/exceptions.py +0 -0
- {apdf-1.2 → apdf-1.3}/apdf/fallback.py +62 -62
- {apdf-1.2 → apdf-1.3}/apdf/models.py +0 -0
- apdf-1.3/apdf/operations.py +469 -0
- {apdf-1.2 → apdf-1.3}/apdf/utils.py +0 -0
- {apdf-1.2/apdf.obfuscation-backup-20260824-112646 → apdf-1.3/apdf.obfuscation-backup-20261011-141855}/__init__.py +0 -0
- {apdf-1.2/apdf.obfuscation-backup-20260824-112646 → apdf-1.3/apdf.obfuscation-backup-20261011-141855}/__main__.py +0 -0
- {apdf-1.2/apdf.obfuscation-backup-20260824-112646 → apdf-1.3/apdf.obfuscation-backup-20261011-141855}/codec.py +131 -5
- {apdf-1.2/apdf.obfuscation-backup-20260824-112646 → apdf-1.3/apdf.obfuscation-backup-20261011-141855}/engine.py +89 -17
- {apdf-1.2/apdf.obfuscation-backup-20260824-112646 → apdf-1.3/apdf.obfuscation-backup-20261011-141855}/exceptions.py +0 -0
- {apdf-1.2/apdf.obfuscation-backup-20260824-112646 → apdf-1.3/apdf.obfuscation-backup-20261011-141855}/fallback.py +0 -0
- {apdf-1.2/apdf.obfuscation-backup-20260824-112646 → apdf-1.3/apdf.obfuscation-backup-20261011-141855}/models.py +0 -0
- {apdf-1.2/apdf.obfuscation-backup-20260824-112646 → apdf-1.3/apdf.obfuscation-backup-20261011-141855}/operations.py +120 -3
- {apdf-1.2/apdf.obfuscation-backup-20260824-112646 → apdf-1.3/apdf.obfuscation-backup-20261011-141855}/utils.py +0 -0
- {apdf-1.2 → apdf-1.3}/pyproject.toml +1 -1
- {apdf-1.2 → apdf-1.3}/uv.lock +2 -2
- apdf-1.2/apdf/cli.py +0 -102
- apdf-1.2/apdf/codec.py +0 -169
- apdf-1.2/apdf/engine.py +0 -163
- apdf-1.2/apdf/operations.py +0 -408
- {apdf-1.2 → apdf-1.3}/.gitignore +0 -0
- {apdf-1.2/apdf.obfuscation-backup-20260824-112646 → apdf-1.3/apdf.obfuscation-backup-20261011-141855}/cli.py +0 -0
{apdf-1.2 → apdf-1.3}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: apdf
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3
|
|
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
|
|
@@ -61,7 +61,7 @@ rep = replace_pdf_text_detailed(
|
|
|
61
61
|
},
|
|
62
62
|
|
|
63
63
|
# Optional parameters:
|
|
64
|
-
pages={2, 3, 4}, # Only these pages
|
|
64
|
+
#pages={2, 3, 4}, # Only these pages
|
|
65
65
|
# pages=2, # Only page 2
|
|
66
66
|
# pages=range(1, 6), # Pages 1 through 5
|
|
67
67
|
# pages=None, # All pages (default)
|
|
@@ -81,7 +81,7 @@ print("Encoding failures:", rep.encoding_failures)
|
|
|
81
81
|
## Usage As CLI Tool
|
|
82
82
|
|
|
83
83
|
```bash
|
|
84
|
-
apdf [-h] [-r OLD=NEW] [-nopa] [-s] [-ff FONT.ttf] [-no-ff] input [output]
|
|
84
|
+
apdf [-h] [-r OLD=NEW] [-nopa] [-s] [-ff FONT.ttf] [-no-ff] [-p PAGELIST] input [output]
|
|
85
85
|
```
|
|
86
86
|
|
|
87
87
|
### Examples
|
{apdf-1.2 → apdf-1.3}/README.md
RENAMED
|
@@ -41,7 +41,7 @@ rep = replace_pdf_text_detailed(
|
|
|
41
41
|
},
|
|
42
42
|
|
|
43
43
|
# Optional parameters:
|
|
44
|
-
pages={2, 3, 4}, # Only these pages
|
|
44
|
+
#pages={2, 3, 4}, # Only these pages
|
|
45
45
|
# pages=2, # Only page 2
|
|
46
46
|
# pages=range(1, 6), # Pages 1 through 5
|
|
47
47
|
# pages=None, # All pages (default)
|
|
@@ -61,7 +61,7 @@ print("Encoding failures:", rep.encoding_failures)
|
|
|
61
61
|
## Usage As CLI Tool
|
|
62
62
|
|
|
63
63
|
```bash
|
|
64
|
-
apdf [-h] [-r OLD=NEW] [-nopa] [-s] [-ff FONT.ttf] [-no-ff] input [output]
|
|
64
|
+
apdf [-h] [-r OLD=NEW] [-nopa] [-s] [-ff FONT.ttf] [-no-ff] [-p PAGELIST] input [output]
|
|
65
65
|
```
|
|
66
66
|
|
|
67
67
|
### Examples
|
|
File without changes
|
|
File without changes
|
apdf-1.3/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
|
+
_yexglwvsffnawf = _installed_version()
|
|
19
|
+
if _supports_color():
|
|
20
|
+
_elnqbhxbtmzjqr = _BANNER.strip('\n').splitlines()
|
|
21
|
+
_ytnyzrzieholjd = [_RED, _RED, _BLUE, _BLUE, _CYAN]
|
|
22
|
+
for _yaekunimrrgxrg, _srftlwvcvaeoen in enumerate(_elnqbhxbtmzjqr):
|
|
23
|
+
_piqkxxfcklbsrf = _ytnyzrzieholjd[_yaekunimrrgxrg] if _yaekunimrrgxrg < len(_ytnyzrzieholjd) else _CYAN
|
|
24
|
+
print(_c(_srftlwvcvaeoen, _piqkxxfcklbsrf, _BOLD), file=sys.stderr)
|
|
25
|
+
else:
|
|
26
|
+
print(_BANNER.strip('\n'), file=sys.stderr)
|
|
27
|
+
_uylanzrxtqrxso = _c(' ' + ' Low-level PDF T' + 'ext Repl' + 'acer', _DIM)
|
|
28
|
+
_ebgbjsqtetgxjd = _c(f' v{_yexglwvsffnawf} ', _BOLD, _MAGENTA)
|
|
29
|
+
_odmebanoixtncs = _c(' b' + 'y' + ' ' + 'Abodx9', _YELLOW, _BOLD)
|
|
30
|
+
print(f'{_uylanzrxtqrxso}{_ebgbjsqtetgxjd}{_odmebanoixtncs}\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
|
+
_zszcsjpxxcncoj: dict[str, str] = {}
|
|
36
|
+
for _vphsugihrzinwv in values:
|
|
37
|
+
if '=' not in _vphsugihrzinwv:
|
|
38
|
+
raise ValueError('-r requi' + 'res OLD=N' + 'EW')
|
|
39
|
+
old, new = _vphsugihrzinwv.split('=', 1)
|
|
40
|
+
if not old:
|
|
41
|
+
raise ValueError('O' + 'LD' + ' cannot be empt' + 'y')
|
|
42
|
+
_zszcsjpxxcncoj[old] = new
|
|
43
|
+
return _zszcsjpxxcncoj
|
|
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 _bdlvvsabzejmct in value.split(','):
|
|
50
|
+
_jwnjjyfnbpszvo = _bdlvvsabzejmct.strip()
|
|
51
|
+
if not _jwnjjyfnbpszvo:
|
|
52
|
+
raise ValueError('-p contains' + ' an ' + 'empty page' + ' number')
|
|
53
|
+
if '-' in _jwnjjyfnbpszvo:
|
|
54
|
+
_olqxnewpzkktlx = [piece.strip() for piece in _jwnjjyfnbpszvo.split('-')]
|
|
55
|
+
if len(_olqxnewpzkktlx) != 2 or not all((piece.isdigit() for piece in _olqxnewpzkktlx)):
|
|
56
|
+
raise ValueError(f'invalid page range: {_jwnjjyfnbpszvo!r}')
|
|
57
|
+
start, _tkoxvyynftxxla = map(int, _olqxnewpzkktlx)
|
|
58
|
+
if start < 1 or _tkoxvyynftxxla < 1:
|
|
59
|
+
raise ValueError('page num' + 'bers mus' + 't be 1 or' + ' greater')
|
|
60
|
+
if start > _tkoxvyynftxxla:
|
|
61
|
+
raise ValueError(f'page range must be ascending: {_jwnjjyfnbpszvo!r}')
|
|
62
|
+
pages.update(range(start, _tkoxvyynftxxla + 1))
|
|
63
|
+
else:
|
|
64
|
+
if not _jwnjjyfnbpszvo.isdigit():
|
|
65
|
+
raise ValueError(f'invalid page number: {_jwnjjyfnbpszvo!r}')
|
|
66
|
+
_jeymhbseuolonm = int(_jwnjjyfnbpszvo)
|
|
67
|
+
if _jeymhbseuolonm < 1:
|
|
68
|
+
raise ValueError('page nu' + 'mbe' + 'rs must be 1 or greate' + 'r')
|
|
69
|
+
pages.add(_jeymhbseuolonm)
|
|
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
|
+
_beqkwgnnaawbyl = argparse.ArgumentParser(description='Replace Unicode and Hex text dire' + 'ctly in PDF cont' + 'ent' + ' streams.')
|
|
77
|
+
_beqkwgnnaawbyl.add_argument('input', help='Inp' + 'ut' + ' PDF')
|
|
78
|
+
_beqkwgnnaawbyl.add_argument('output', nargs='?', default='output.pdf', help='Output ' + 'PDF')
|
|
79
|
+
_beqkwgnnaawbyl.add_argument('-r', action='append', default=[], metavar='OLD=NEW', dest='replace', help='Replace: Unicode replacement; may be supplied more than on' + 'ce')
|
|
80
|
+
_beqkwgnnaawbyl.add_argument('-p', metavar='PAGES', dest='pages', help='Page' + 's to edit, e.g. 2,' + ' 2,3,4, or 1-5; defaults to all pages')
|
|
81
|
+
_beqkwgnnaawbyl.add_argument('-nopa', action='store_true', dest='no_preserve_advance', help='No pres' + 'erve advance: Do not compensate TJ widt' + 'hs after replacements')
|
|
82
|
+
_beqkwgnnaawbyl.add_argument('-s', action='store_true', dest='strict', help='Strict: Fail if text is ' + 'missing or' + ' the selected font cannot encode' + ' it')
|
|
83
|
+
_beqkwgnnaawbyl.add_argument('-ff', metavar='FONT.ttf', dest='fallback_font', help='Fallback font: TrueType font to embed when the ' + "PDF's subset font lacks a glyph; defaults to a com" + 'mon system sans-serif font')
|
|
84
|
+
_beqkwgnnaawbyl.add_argument('-no-ff', action='store_true', dest='no_font_fallback', help='No font fallback: Reject mis' + 'sing glyphs instead of embe' + 'ddi' + 'ng a fallback font')
|
|
85
|
+
_wxgrxwtxatoxen = _beqkwgnnaawbyl.parse_args(argv)
|
|
86
|
+
try:
|
|
87
|
+
replacements = _parse_replacements(_wxgrxwtxatoxen.replace)
|
|
88
|
+
pages = _parse_pages(_wxgrxwtxatoxen.pages)
|
|
89
|
+
if not replacements:
|
|
90
|
+
_beqkwgnnaawbyl.error('pro' + 'vide at le' + 'ast' + ' one -r OLD=NEW')
|
|
91
|
+
_xlyrhnjfdmkefx = replace_pdf_text_detailed(_wxgrxwtxatoxen.input, _wxgrxwtxatoxen.output, replacements, preserve_advance=not _wxgrxwtxatoxen.no_preserve_advance, strict=_wxgrxwtxatoxen.strict, fallback_font=_wxgrxwtxatoxen.fallback_font, font_fallback=not _wxgrxwtxatoxen.no_font_fallback, pages=pages)
|
|
92
|
+
except (OSError, PdfReplacementError, ValueError) as _uzfibstyfyttzp:
|
|
93
|
+
_beqkwgnnaawbyl.exit(2, _c(f'error: {_uzfibstyfyttzp}\n', _RED))
|
|
94
|
+
except Exception as _uzfibstyfyttzp:
|
|
95
|
+
_beqkwgnnaawbyl.exit(2, _c(f'error: An unexpected error occurred: {type(_uzfibstyfyttzp).__name__}: {_uzfibstyfyttzp}\n', _RED))
|
|
96
|
+
print(_c(f'Replaced {_xlyrhnjfdmkefx.replacements} occurrence(s) in {_xlyrhnjfdmkefx.changed_streams}/{_xlyrhnjfdmkefx.scanned_streams} stream(s) in {_xlyrhnjfdmkefx.elapsed_seconds:.3f}s.', _GREEN))
|
|
97
|
+
if _xlyrhnjfdmkefx.missing:
|
|
98
|
+
print(_c('Not ' + 'f' + 'o' + 'und: ' + ', '.join((repr(value) for value in _xlyrhnjfdmkefx.missing)), _RED))
|
|
99
|
+
if _xlyrhnjfdmkefx.encoding_failures:
|
|
100
|
+
print(_c(f'Skipped {len(_xlyrhnjfdmkefx.encoding_failures)} unencodable replacement(s).', _RED))
|
|
101
|
+
print(_c(f'Saved: {_wxgrxwtxatoxen.output}', _GREEN, _BOLD))
|
|
102
|
+
return 0
|
apdf-1.3/apdf/codec.py
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import sys
|
|
3
|
+
from io import BytesIO
|
|
4
|
+
from typing import Any, Mapping
|
|
5
|
+
from fontTools.agl import UV2AGL
|
|
6
|
+
from fontTools.pens.boundsPen import BoundsPen
|
|
7
|
+
from fontTools.ttLib import TTFont
|
|
8
|
+
from pypdf._font import Font
|
|
9
|
+
from pypdf.generic import DictionaryObject
|
|
10
|
+
from apdf.exceptions import FontEncodingError
|
|
11
|
+
from apdf.utils import _deref
|
|
12
|
+
|
|
13
|
+
class FontCodec:
|
|
14
|
+
|
|
15
|
+
def __init__(self, resource_name: str, font_object: DictionaryObject):
|
|
16
|
+
self.resource_name = resource_name
|
|
17
|
+
self.pdf_font = _deref(font_object)
|
|
18
|
+
self.font = Font.from_font_resource(self.pdf_font)
|
|
19
|
+
self._unicode_to_intermediate: dict[str, str] = {}
|
|
20
|
+
self._intermediate_to_bytes: dict[str, bytes] = {}
|
|
21
|
+
self._difference_glyphs: dict[int, str] = {}
|
|
22
|
+
self._embedded_glyphs: set[str] | None = None
|
|
23
|
+
self._embedded_ttfont: TTFont | None = None
|
|
24
|
+
self._embedded_font_bytes: bytes | None = None
|
|
25
|
+
self._embedded_font_stream: Any | None = None
|
|
26
|
+
self._embedded_ttfont_loaded = False
|
|
27
|
+
self._embedded_glyph_order: list[str] = []
|
|
28
|
+
self._drawable_glyphs: dict[int, bool] = {}
|
|
29
|
+
self._cid_to_gid_bytes: bytes | None = None
|
|
30
|
+
self._build_inverse_maps()
|
|
31
|
+
self._load_glyph_inventory()
|
|
32
|
+
|
|
33
|
+
@property
|
|
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}'
|
|
38
|
+
|
|
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)
|
|
43
|
+
encoding = self.font.encoding
|
|
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,)))
|
|
48
|
+
|
|
49
|
+
def _load_glyph_inventory(self) -> None:
|
|
50
|
+
encoding = _deref(self.pdf_font.get('/Encoding'))
|
|
51
|
+
if isinstance(encoding, DictionaryObject):
|
|
52
|
+
code: int | None = None
|
|
53
|
+
for item in encoding.get('/Differences', []):
|
|
54
|
+
item = _deref(item)
|
|
55
|
+
if isinstance(item, int):
|
|
56
|
+
code = int(item)
|
|
57
|
+
elif code is not None:
|
|
58
|
+
self._difference_glyphs[code] = str(item).lstrip('/')
|
|
59
|
+
code += 1
|
|
60
|
+
descriptor_owner = self.pdf_font
|
|
61
|
+
if str(self.pdf_font.get('/Subtype', '')) == '/Type0':
|
|
62
|
+
descendants = _deref(self.pdf_font.get('/DescendantFonts', []))
|
|
63
|
+
if descendants:
|
|
64
|
+
descriptor_owner = _deref(descendants[0])
|
|
65
|
+
cid_to_gid = _deref(descriptor_owner.get('/CIDToGIDMap'))
|
|
66
|
+
if hasattr(cid_to_gid, 'get_data'):
|
|
67
|
+
try:
|
|
68
|
+
self._cid_to_gid_bytes = cid_to_gid.get_data()
|
|
69
|
+
except Exception:
|
|
70
|
+
self._cid_to_gid_bytes = b''
|
|
71
|
+
descriptor = _deref(descriptor_owner.get('/FontDescriptor'))
|
|
72
|
+
charset = descriptor.get('/CharSet') if descriptor else None
|
|
73
|
+
if charset:
|
|
74
|
+
self._embedded_glyphs = {name for name in str(charset).split('/') if name}
|
|
75
|
+
font_file = _deref(descriptor.get('/FontFile2')) if descriptor else None
|
|
76
|
+
if font_file is not None and hasattr(font_file, 'get_data'):
|
|
77
|
+
self._embedded_font_stream = font_file
|
|
78
|
+
|
|
79
|
+
def _ensure_embedded_ttfont(self) -> None:
|
|
80
|
+
if self._embedded_ttfont_loaded:
|
|
81
|
+
return
|
|
82
|
+
self._embedded_ttfont_loaded = True
|
|
83
|
+
if self._embedded_font_bytes is None and self._embedded_font_stream is not None:
|
|
84
|
+
try:
|
|
85
|
+
self._embedded_font_bytes = self._embedded_font_stream.get_data()
|
|
86
|
+
except Exception:
|
|
87
|
+
self._embedded_font_bytes = None
|
|
88
|
+
if self._embedded_font_bytes is None:
|
|
89
|
+
return
|
|
90
|
+
try:
|
|
91
|
+
self._embedded_ttfont = TTFont(BytesIO(self._embedded_font_bytes), lazy=False, recalcBBoxes=False, recalcTimestamp=False)
|
|
92
|
+
self._embedded_glyph_order = self._embedded_ttfont.getGlyphOrder()
|
|
93
|
+
except Exception:
|
|
94
|
+
self._embedded_ttfont = None
|
|
95
|
+
self._embedded_glyph_order = []
|
|
96
|
+
|
|
97
|
+
def _gid_for_cid(self, cid: int) -> int:
|
|
98
|
+
if self._cid_to_gid_bytes is None:
|
|
99
|
+
return cid
|
|
100
|
+
_dqxyefsqqlowdi = cid * 2
|
|
101
|
+
if _dqxyefsqqlowdi + 2 > len(self._cid_to_gid_bytes):
|
|
102
|
+
return 0
|
|
103
|
+
return int.from_bytes(self._cid_to_gid_bytes[_dqxyefsqqlowdi:_dqxyefsqqlowdi + 2], 'big')
|
|
104
|
+
|
|
105
|
+
def _glyph_is_drawable(self, gid: int) -> bool:
|
|
106
|
+
cached = self._drawable_glyphs.get(gid)
|
|
107
|
+
if cached is not None:
|
|
108
|
+
return cached
|
|
109
|
+
font = self._embedded_ttfont
|
|
110
|
+
if font is None or gid <= 0 or gid >= len(self._embedded_glyph_order):
|
|
111
|
+
self._drawable_glyphs[gid] = False
|
|
112
|
+
return False
|
|
113
|
+
name = self._embedded_glyph_order[gid]
|
|
114
|
+
if name == '.notdef':
|
|
115
|
+
self._drawable_glyphs[gid] = False
|
|
116
|
+
return False
|
|
117
|
+
drawable = False
|
|
118
|
+
try:
|
|
119
|
+
glyf = font.get('glyf')
|
|
120
|
+
if glyf is not None:
|
|
121
|
+
glyph = glyf[name]
|
|
122
|
+
drawable = bool(glyph.isComposite() or int(getattr(glyph, 'numberOfContours', 0)) > 0)
|
|
123
|
+
else:
|
|
124
|
+
glyph_set = font.getGlyphSet()
|
|
125
|
+
pen = BoundsPen(glyph_set)
|
|
126
|
+
glyph_set[name].draw(pen)
|
|
127
|
+
drawable = pen.bounds is not None
|
|
128
|
+
except Exception:
|
|
129
|
+
drawable = False
|
|
130
|
+
self._drawable_glyphs[gid] = drawable
|
|
131
|
+
return drawable
|
|
132
|
+
|
|
133
|
+
def _validate_type0_glyph_outlines(self, raw: bytes) -> None:
|
|
134
|
+
self._ensure_embedded_ttfont()
|
|
135
|
+
if self._embedded_ttfont is None:
|
|
136
|
+
return
|
|
137
|
+
encoding = self.font.encoding
|
|
138
|
+
if not isinstance(encoding, str):
|
|
139
|
+
return
|
|
140
|
+
try:
|
|
141
|
+
_rzcdzoakbxyqft = raw.decode(encoding, errors='strict')
|
|
142
|
+
except (LookupError, UnicodeError):
|
|
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)):
|
|
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})')
|
|
153
|
+
|
|
154
|
+
def _glyph_name_for_code(self, code: int) -> str | None:
|
|
155
|
+
if code in self._difference_glyphs:
|
|
156
|
+
return self._difference_glyphs[code]
|
|
157
|
+
encoding = self.font.encoding
|
|
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')
|
|
164
|
+
try:
|
|
165
|
+
_yhbgyxeyqreqah = bytes((code,)).decode(codec)
|
|
166
|
+
except (UnicodeDecodeError, LookupError):
|
|
167
|
+
return None
|
|
168
|
+
return UV2AGL.get(ord(_yhbgyxeyqreqah))
|
|
169
|
+
|
|
170
|
+
def _validate_embedded_glyphs(self, raw: bytes, visible: str) -> None:
|
|
171
|
+
_aojpgqqhfkrpwi = str(self.pdf_font.get('/Subtype', ''))
|
|
172
|
+
if _aojpgqqhfkrpwi == '/Type3':
|
|
173
|
+
encoding = self.font.encoding
|
|
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}')
|
|
179
|
+
if self.decode(raw) != visible:
|
|
180
|
+
raise FontEncodingError(f'{self.description} has no reversible glyph mapping for {visible!r}')
|
|
181
|
+
return
|
|
182
|
+
if _aojpgqqhfkrpwi == '/Type0':
|
|
183
|
+
self._validate_type0_glyph_outlines(raw)
|
|
184
|
+
if self._embedded_glyphs is not None:
|
|
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)
|
|
190
|
+
if missing:
|
|
191
|
+
names = ', '.join(sorted(set(missing)))
|
|
192
|
+
raise FontEncodingError(f'{self.description} embedded subset lacks glyph(s): {names}')
|
|
193
|
+
if self.decode(raw) != visible:
|
|
194
|
+
raise FontEncodingError(f'{self.description} does not round-trip replacement {visible!r}')
|
|
195
|
+
|
|
196
|
+
def decode(self, raw: bytes) -> str:
|
|
197
|
+
encoding = self.font.encoding
|
|
198
|
+
if isinstance(encoding, str):
|
|
199
|
+
try:
|
|
200
|
+
_prjxvllsrsceqe = raw.decode(encoding, errors='surrogatepass')
|
|
201
|
+
except (LookupError, UnicodeError):
|
|
202
|
+
_vvtbwoypfiegky = 'utf-16-be' if len(raw) % 2 == 0 else 'latin-1'
|
|
203
|
+
_prjxvllsrsceqe = raw.decode(_vvtbwoypfiegky, errors='replace')
|
|
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))
|
|
207
|
+
|
|
208
|
+
@staticmethod
|
|
209
|
+
def _greedy_map(text: str, mapping: Mapping[str, str]) -> str:
|
|
210
|
+
if not mapping:
|
|
211
|
+
return text
|
|
212
|
+
_cfguxjxbjcwaau = sorted(mapping, key=len, reverse=True)
|
|
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):
|
|
218
|
+
output.append(mapping[key])
|
|
219
|
+
_qjbwwztaspnkuz += len(key)
|
|
220
|
+
break
|
|
221
|
+
else:
|
|
222
|
+
output.append(text[_qjbwwztaspnkuz])
|
|
223
|
+
_qjbwwztaspnkuz += 1
|
|
224
|
+
return ''.join(output)
|
|
225
|
+
|
|
226
|
+
def encode(self, visible: str) -> bytes:
|
|
227
|
+
_bwcwwxglrozrgj = self._greedy_map(visible, self._unicode_to_intermediate)
|
|
228
|
+
encoding = self.font.encoding
|
|
229
|
+
if isinstance(encoding, str):
|
|
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)
|
|
244
|
+
break
|
|
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
|
|
250
|
+
|
|
251
|
+
def width(self, visible: str) -> float:
|
|
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))
|
|
257
|
+
except Exception:
|
|
258
|
+
return 0.0
|
|
259
|
+
|
|
260
|
+
def _font_codecs(resources: Any) -> dict[str, FontCodec]:
|
|
261
|
+
resources = _deref(resources)
|
|
262
|
+
fonts = _deref(resources.get('/Font', {})) if resources else {}
|
|
263
|
+
result: dict[str, FontCodec] = {}
|
|
264
|
+
for name, font_ref in getattr(fonts, 'items', lambda: [])():
|
|
265
|
+
resource_name = str(name)
|
|
266
|
+
try:
|
|
267
|
+
result[resource_name] = FontCodec(resource_name, _deref(font_ref))
|
|
268
|
+
except Exception as exc:
|
|
269
|
+
print(f'Warning: Skipping unreadable font {resource_name}: {exc}', file=sys.stderr)
|
|
270
|
+
return result
|
apdf-1.3/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 _ugpcfjadzgdirg:
|
|
22
|
+
raise PdfReplacementError('Cannot r' + 'ewrite a content stream with its current' + ' PDF filters') from _ugpcfjadzgdirg
|
|
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
|
+
_neazdpgwrnkyhw = _deref(owner.get('/Contents'))
|
|
37
|
+
if _neazdpgwrnkyhw is None:
|
|
38
|
+
return
|
|
39
|
+
if isinstance(_neazdpgwrnkyhw, (list, ArrayObject)):
|
|
40
|
+
for _bufadhhaukiyve in _neazdpgwrnkyhw:
|
|
41
|
+
_dayimapdbmhlhp = _deref(_bufadhhaukiyve)
|
|
42
|
+
if isinstance(_dayimapdbmhlhp, StreamObject):
|
|
43
|
+
yield _dayimapdbmhlhp
|
|
44
|
+
elif isinstance(_neazdpgwrnkyhw, StreamObject):
|
|
45
|
+
yield _neazdpgwrnkyhw
|
|
46
|
+
|
|
47
|
+
def _shallow_stream_clone(stream: StreamObject, writer: PdfWriter) -> Any:
|
|
48
|
+
_wfeyjveilhmvmf = type(stream)()
|
|
49
|
+
for key, _hwggqkeyajawhf in stream.items():
|
|
50
|
+
if str(key) != '/Length':
|
|
51
|
+
_wfeyjveilhmvmf[key] = _hwggqkeyajawhf
|
|
52
|
+
_wfeyjveilhmvmf._data = stream._data
|
|
53
|
+
return writer._add_object(_wfeyjveilhmvmf)
|
|
54
|
+
|
|
55
|
+
def _isolate_resources(resources: Any, writer: PdfWriter, forms: dict[tuple[Any, ...], Any]) -> DictionaryObject:
|
|
56
|
+
_qvjhxwvnscjxwg = _deref(resources)
|
|
57
|
+
_goaibhlcgkcnri = DictionaryObject()
|
|
58
|
+
if not _qvjhxwvnscjxwg:
|
|
59
|
+
return _goaibhlcgkcnri
|
|
60
|
+
for key, _zfeehupelhlfhe in _qvjhxwvnscjxwg.items():
|
|
61
|
+
_goaibhlcgkcnri[key] = _zfeehupelhlfhe
|
|
62
|
+
_yrlfrkxsowknpg = _deref(_qvjhxwvnscjxwg.get('/Font'))
|
|
63
|
+
if _yrlfrkxsowknpg is not None:
|
|
64
|
+
_goaibhlcgkcnri[NameObject('/Font')] = DictionaryObject(_yrlfrkxsowknpg.items())
|
|
65
|
+
_qnfihroivpagha = _deref(_qvjhxwvnscjxwg.get('/XObject'))
|
|
66
|
+
if _qnfihroivpagha is not None:
|
|
67
|
+
_exberyaazfdcst = DictionaryObject()
|
|
68
|
+
_goaibhlcgkcnri[NameObject('/XObject')] = _exberyaazfdcst
|
|
69
|
+
for name, _txolusbadwgrya in _qnfihroivpagha.items():
|
|
70
|
+
_ppdzawokdondzc = _deref(_txolusbadwgrya)
|
|
71
|
+
if not isinstance(_ppdzawokdondzc, StreamObject) or _ppdzawokdondzc.get('/Subtype') != '/Form':
|
|
72
|
+
_exberyaazfdcst[name] = _txolusbadwgrya
|
|
73
|
+
continue
|
|
74
|
+
key = _object_key(_ppdzawokdondzc)
|
|
75
|
+
_satjsphwxjxnya = forms.get(key)
|
|
76
|
+
if _satjsphwxjxnya is None:
|
|
77
|
+
_satjsphwxjxnya = _shallow_stream_clone(_ppdzawokdondzc, writer)
|
|
78
|
+
forms[key] = _satjsphwxjxnya
|
|
79
|
+
_ualqasmuyrlrqd = _deref(_satjsphwxjxnya)
|
|
80
|
+
_vhiyefugqdsdwl = _ppdzawokdondzc.get('/Resources')
|
|
81
|
+
if _vhiyefugqdsdwl is not None:
|
|
82
|
+
_ualqasmuyrlrqd[NameObject('/Resources')] = _isolate_resources(_vhiyefugqdsdwl, writer, forms)
|
|
83
|
+
_exberyaazfdcst[name] = _satjsphwxjxnya
|
|
84
|
+
return _goaibhlcgkcnri
|
|
85
|
+
|
|
86
|
+
def _isolate_contents(contents: Any, writer: PdfWriter) -> Any:
|
|
87
|
+
_fkbsewzneyskya = _deref(contents)
|
|
88
|
+
if isinstance(_fkbsewzneyskya, (list, ArrayObject)):
|
|
89
|
+
_rujmummusiizgx = ArrayObject()
|
|
90
|
+
for _fknsoginotgtdd in _fkbsewzneyskya:
|
|
91
|
+
_ryfrzizgkphqcw = _deref(_fknsoginotgtdd)
|
|
92
|
+
_rujmummusiizgx.append(_shallow_stream_clone(_ryfrzizgkphqcw, writer) if isinstance(_ryfrzizgkphqcw, StreamObject) else _fknsoginotgtdd)
|
|
93
|
+
return _rujmummusiizgx
|
|
94
|
+
if isinstance(_fkbsewzneyskya, StreamObject):
|
|
95
|
+
return _shallow_stream_clone(_fkbsewzneyskya, writer)
|
|
96
|
+
return contents
|
|
97
|
+
|
|
98
|
+
def _isolate_selected_page(page: Any, writer: PdfWriter) -> None:
|
|
99
|
+
_xfosugyumsurgg = page.get('/Contents')
|
|
100
|
+
if _xfosugyumsurgg is not None:
|
|
101
|
+
page[NameObject('/Contents')] = _isolate_contents(_xfosugyumsurgg, writer)
|
|
102
|
+
_vkdvwpzmynlyno = page.get('/Resources')
|
|
103
|
+
if _vkdvwpzmynlyno is not None:
|
|
104
|
+
page[NameObject('/Resources')] = _isolate_resources(_vkdvwpzmynlyno, 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 cont' + 'ain page numbers, not a boolean')
|
|
111
|
+
if isinstance(pages, int):
|
|
112
|
+
_obwbprdoklcsrk = {pages}
|
|
113
|
+
elif isinstance(pages, (str, bytes)):
|
|
114
|
+
raise ValueError('pages must b' + 'e page' + ' number' + 's; use the CLI -p option for ranges')
|
|
115
|
+
else:
|
|
116
|
+
_obwbprdoklcsrk = {int(page) for page in pages}
|
|
117
|
+
if not _obwbprdoklcsrk:
|
|
118
|
+
raise ValueError('at least one page must be selecte' + 'd')
|
|
119
|
+
_eaambvomkguowr = sorted((page for page in _obwbprdoklcsrk if page < 1 or page > page_count))
|
|
120
|
+
if _eaambvomkguowr:
|
|
121
|
+
_pvlavdaqfuzuzq = ', '.join((str(page) for page in _eaambvomkguowr))
|
|
122
|
+
raise ValueError(f'page selection out of range: {_pvlavdaqfuzuzq}; PDF has {page_count} page(s)')
|
|
123
|
+
return _obwbprdoklcsrk
|
|
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
|
+
_jyhvxxfycftmnc = time.perf_counter()
|
|
142
|
+
_bvspezhylfkdnj = [Replacement(str(old), str(new)) for old, new in replacements.items() if old]
|
|
143
|
+
if not _bvspezhylfkdnj:
|
|
144
|
+
raise ValueError('At least on' + 'e non-empty sourc' + 'e string' + ' is required')
|
|
145
|
+
_ymqeotapitgtmu = Path(input_pdf)
|
|
146
|
+
_vrppczsatjqpcp = Path(output_pdf)
|
|
147
|
+
if not _ymqeotapitgtmu.is_file():
|
|
148
|
+
raise PdfFileNotFoundError(f'File not found: {_ymqeotapitgtmu}')
|
|
149
|
+
_vrppczsatjqpcp.parent.mkdir(parents=True, exist_ok=True)
|
|
150
|
+
try:
|
|
151
|
+
_gjzrhihamjwcvu = PdfReader(_ymqeotapitgtmu, strict=False)
|
|
152
|
+
except DependencyError as _jaychfagiqcghf:
|
|
153
|
+
raise PdfDependencyError(f"Missing dependency for encrypted PDF: {_jaychfagiqcghf}\nPlease install it, e.g., 'uv pip install cryptography'") from _jaychfagiqcghf
|
|
154
|
+
except PermissionError as _jaychfagiqcghf:
|
|
155
|
+
raise PdfPermissionError(f'Permission denied when opening {_ymqeotapitgtmu}. The PDF might be opened by another application.') from _jaychfagiqcghf
|
|
156
|
+
if _gjzrhihamjwcvu.is_encrypted:
|
|
157
|
+
raise PdfReplacementError('E' + 'ncrypted P' + 'DFs are no' + 't supported without a password')
|
|
158
|
+
writer = PdfWriter(clone_from=_gjzrhihamjwcvu)
|
|
159
|
+
_vahcrawjnhoeeu = _selected_page_numbers(pages, len(writer.pages))
|
|
160
|
+
_jpdxghcahiblhs = len(_vahcrawjnhoeeu) != len(writer.pages)
|
|
161
|
+
_kpoajrmsgwgebq = FallbackFontRegistry(writer, explicit_font=fallback_font, enabled=font_fallback)
|
|
162
|
+
_kwvbmdsitcleny = ReplacementReport(pages=len(writer.pages))
|
|
163
|
+
_pgfbmwtzgqhwpu: set[str] = set()
|
|
164
|
+
_jchjazcahpawrz: set[tuple[Any, ...]] = set()
|
|
165
|
+
for _hasupdutpcshsp, _ccjftxomkyeqpp in enumerate(writer.pages, start=1):
|
|
166
|
+
if _hasupdutpcshsp not in _vahcrawjnhoeeu:
|
|
167
|
+
continue
|
|
168
|
+
print(_c(f'Processing page {_hasupdutpcshsp}/{_kwvbmdsitcleny.pages}', _BLUE), file=sys.stderr)
|
|
169
|
+
if _jpdxghcahiblhs:
|
|
170
|
+
_isolate_selected_page(_ccjftxomkyeqpp, writer)
|
|
171
|
+
_xjgejknjifwjnp = _deref(_ccjftxomkyeqpp.get('/Resources', {}))
|
|
172
|
+
for _hfzdavhzclbqdu in _content_streams(_ccjftxomkyeqpp):
|
|
173
|
+
key = _object_key(_hfzdavhzclbqdu)
|
|
174
|
+
if key in _jchjazcahpawrz:
|
|
175
|
+
continue
|
|
176
|
+
_jchjazcahpawrz.add(key)
|
|
177
|
+
_rewrite_stream(_hfzdavhzclbqdu, writer, _xjgejknjifwjnp, _bvspezhylfkdnj, preserve_advance, _kwvbmdsitcleny, _pgfbmwtzgqhwpu, _kpoajrmsgwgebq, _hasupdutpcshsp)
|
|
178
|
+
_rewrite_forms(_xjgejknjifwjnp, writer, _bvspezhylfkdnj, preserve_advance, _kwvbmdsitcleny, _pgfbmwtzgqhwpu, _jchjazcahpawrz, _kpoajrmsgwgebq, _hasupdutpcshsp)
|
|
179
|
+
_kpoajrmsgwgebq.finalize()
|
|
180
|
+
_kwvbmdsitcleny.missing = [rule.old for rule in _bvspezhylfkdnj if rule.old not in _pgfbmwtzgqhwpu]
|
|
181
|
+
_kwvbmdsitcleny.elapsed_seconds = time.perf_counter() - _jyhvxxfycftmnc
|
|
182
|
+
if strict and (_kwvbmdsitcleny.missing or _kwvbmdsitcleny.encoding_failures):
|
|
183
|
+
_nezldqejbyizsc = []
|
|
184
|
+
if _kwvbmdsitcleny.missing:
|
|
185
|
+
_nezldqejbyizsc.append(f'not found: {_kwvbmdsitcleny.missing!r}')
|
|
186
|
+
if _kwvbmdsitcleny.encoding_failures:
|
|
187
|
+
_nezldqejbyizsc.append(f'encoding failures: {_kwvbmdsitcleny.encoding_failures!r}')
|
|
188
|
+
raise PdfReplacementError('; '.join(_nezldqejbyizsc))
|
|
189
|
+
writer.compress_identical_objects(remove_duplicates=True, remove_unreferenced=True)
|
|
190
|
+
_uskarlkklezhao: str | None = None
|
|
191
|
+
try:
|
|
192
|
+
try:
|
|
193
|
+
with tempfile.NamedTemporaryFile(mode='wb', prefix=f'.{_vrppczsatjqpcp.name}.', suffix='.tmp', dir=_vrppczsatjqpcp.parent, delete=False) as _mvbqwmzhbcjcxs:
|
|
194
|
+
_uskarlkklezhao = _mvbqwmzhbcjcxs.name
|
|
195
|
+
writer.write(_mvbqwmzhbcjcxs)
|
|
196
|
+
_haaipmhglhleal = PdfReader(_uskarlkklezhao, strict=False)
|
|
197
|
+
if len(_haaipmhglhleal.pages) != _kwvbmdsitcleny.pages:
|
|
198
|
+
raise PdfReplacementError('O' + 'utput ' + 'verification found a page-c' + 'ount mismatch')
|
|
199
|
+
os.replace(_uskarlkklezhao, _vrppczsatjqpcp)
|
|
200
|
+
_uskarlkklezhao = None
|
|
201
|
+
except PermissionError as _jaychfagiqcghf:
|
|
202
|
+
raise PdfPermissionError(f'Permission denied when saving to {_vrppczsatjqpcp}. The PDF might be opened by another application.') from _jaychfagiqcghf
|
|
203
|
+
finally:
|
|
204
|
+
if _uskarlkklezhao and os.path.exists(_uskarlkklezhao):
|
|
205
|
+
os.unlink(_uskarlkklezhao)
|
|
206
|
+
_kwvbmdsitcleny.elapsed_seconds = time.perf_counter() - _jyhvxxfycftmnc
|
|
207
|
+
return _kwvbmdsitcleny
|
|
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
|
|
File without changes
|