apdf 1.0__tar.gz → 1.2__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.0 → apdf-1.2}/PKG-INFO +38 -3
  2. {apdf-1.0 → apdf-1.2}/README.md +37 -2
  3. {apdf-1.0 → apdf-1.2}/apdf/__init__.py +0 -0
  4. {apdf-1.0 → apdf-1.2}/apdf/__main__.py +0 -0
  5. apdf-1.2/apdf/cli.py +102 -0
  6. {apdf-1.0 → apdf-1.2}/apdf/codec.py +69 -69
  7. apdf-1.2/apdf/engine.py +163 -0
  8. {apdf-1.0 → apdf-1.2}/apdf/exceptions.py +0 -0
  9. {apdf-1.0 → apdf-1.2}/apdf/fallback.py +62 -62
  10. {apdf-1.0 → apdf-1.2}/apdf/models.py +0 -0
  11. apdf-1.2/apdf/operations.py +408 -0
  12. {apdf-1.0 → apdf-1.2}/apdf/utils.py +0 -0
  13. {apdf-1.0/apdf.obfuscation-backup-20260820-110606 → apdf-1.2/apdf.obfuscation-backup-20260824-112646}/__init__.py +0 -0
  14. {apdf-1.0/apdf.obfuscation-backup-20260820-110606 → apdf-1.2/apdf.obfuscation-backup-20260824-112646}/__main__.py +0 -0
  15. {apdf-1.0/apdf.obfuscation-backup-20260820-110606 → apdf-1.2/apdf.obfuscation-backup-20260824-112646}/cli.py +34 -0
  16. {apdf-1.0/apdf.obfuscation-backup-20260820-110606 → apdf-1.2/apdf.obfuscation-backup-20260824-112646}/codec.py +0 -0
  17. {apdf-1.0/apdf.obfuscation-backup-20260820-110606 → apdf-1.2/apdf.obfuscation-backup-20260824-112646}/engine.py +66 -4
  18. {apdf-1.0/apdf.obfuscation-backup-20260820-110606 → apdf-1.2/apdf.obfuscation-backup-20260824-112646}/exceptions.py +0 -0
  19. {apdf-1.0/apdf.obfuscation-backup-20260820-110606 → apdf-1.2/apdf.obfuscation-backup-20260824-112646}/fallback.py +0 -0
  20. {apdf-1.0/apdf.obfuscation-backup-20260820-110606 → apdf-1.2/apdf.obfuscation-backup-20260824-112646}/models.py +0 -0
  21. {apdf-1.0/apdf.obfuscation-backup-20260820-110606 → apdf-1.2/apdf.obfuscation-backup-20260824-112646}/operations.py +77 -10
  22. {apdf-1.0/apdf.obfuscation-backup-20260820-110606 → apdf-1.2/apdf.obfuscation-backup-20260824-112646}/utils.py +0 -0
  23. {apdf-1.0 → apdf-1.2}/pyproject.toml +1 -1
  24. {apdf-1.0 → apdf-1.2}/uv.lock +1 -1
  25. apdf-1.0/apdf/cli.py +0 -73
  26. apdf-1.0/apdf/engine.py +0 -125
  27. apdf-1.0/apdf/operations.py +0 -384
  28. {apdf-1.0 → apdf-1.2}/.gitignore +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: apdf
3
- Version: 1.0
3
+ Version: 1.2
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
@@ -18,7 +18,7 @@ Requires-Dist: fonttools==4.63.0
18
18
  Requires-Dist: pypdf==6.16.1
19
19
  Description-Content-Type: text/markdown
20
20
 
21
- # Another PDF editor
21
+ # A-PDF
22
22
 
23
23
  **A-PDF** is a fast, command-line tool designed to replace Unicode and Hex text directly within PDF content streams.
24
24
 
@@ -28,6 +28,7 @@ Description-Content-Type: text/markdown
28
28
  - **Spacing Compensation**: Automatically compensates for `TJ` (Text Justification) width adjustments after text replacements to preserve document layout (can be disabled).
29
29
  - **Strict Mode**: Optionally fail replacements if text is missing or the font cannot encode the requested characters.
30
30
  - **Fallback Font Support**: Embed a custom TrueType font when the PDF's subset font lacks a glyph, or disable fallback entirely.
31
+ - **Page Selection**: Select specific pages to process.
31
32
 
32
33
  ## Installation
33
34
 
@@ -45,7 +46,39 @@ pip install apdf
45
46
  apdf --help
46
47
  ```
47
48
 
48
- ## Usage
49
+ ## Usage As Library
50
+
51
+ ```python
52
+ from apdf import replace_pdf_text_detailed
53
+
54
+ rep = replace_pdf_text_detailed(
55
+ input_pdf="Test1.pdf",
56
+ output_pdf="Test1_out.pdf",
57
+ replacements={
58
+ "Old Text": "New Text",
59
+ # "Hello": "World",
60
+ # "2026": "2027",
61
+ },
62
+
63
+ # Optional parameters:
64
+ pages={2, 3, 4}, # Only these pages
65
+ # pages=2, # Only page 2
66
+ # pages=range(1, 6), # Pages 1 through 5
67
+ # pages=None, # All pages (default)
68
+
69
+ # fallback_font="FONT.ttf",
70
+ # font_fallback=True,
71
+ # strict=True,
72
+ )
73
+
74
+ print("Replacements:", rep.replacements)
75
+ print("Changed streams:", rep.changed_streams)
76
+ print("Scanned streams:", rep.scanned_streams)
77
+ print("Missing:", rep.missing)
78
+ print("Encoding failures:", rep.encoding_failures)
79
+ ```
80
+
81
+ ## Usage As CLI Tool
49
82
 
50
83
  ```bash
51
84
  apdf [-h] [-r OLD=NEW] [-nopa] [-s] [-ff FONT.ttf] [-no-ff] input [output]
@@ -80,6 +113,8 @@ apdf input.pdf output.pdf -r "foo=bar" -s
80
113
  * `input`: The source PDF file.
81
114
  * `output`: (Optional) The destination PDF file. Defaults to `output.pdf`.
82
115
  * `-r OLD=NEW`: Replace. The text replacement pair. You can supply this argument multiple times.
116
+ * `-p 2`, `-p 2,3,4`, or `-p 1-5` to edit only selected pages. Without
117
+ `-p`, all pages are processed.
83
118
  * `-nopa`: No preserve advance. Do not compensate `TJ` text widths after making replacements.
84
119
  * `-s`: Strict. Fail if the original text is missing or the selected font cannot encode the new text.
85
120
  * `-ff FONT.ttf`: Fallback font. TrueType font to embed when the PDF's subset font lacks a glyph.
@@ -1,4 +1,4 @@
1
- # Another PDF editor
1
+ # A-PDF
2
2
 
3
3
  **A-PDF** is a fast, command-line tool designed to replace Unicode and Hex text directly within PDF content streams.
4
4
 
@@ -8,6 +8,7 @@
8
8
  - **Spacing Compensation**: Automatically compensates for `TJ` (Text Justification) width adjustments after text replacements to preserve document layout (can be disabled).
9
9
  - **Strict Mode**: Optionally fail replacements if text is missing or the font cannot encode the requested characters.
10
10
  - **Fallback Font Support**: Embed a custom TrueType font when the PDF's subset font lacks a glyph, or disable fallback entirely.
11
+ - **Page Selection**: Select specific pages to process.
11
12
 
12
13
  ## Installation
13
14
 
@@ -25,7 +26,39 @@ pip install apdf
25
26
  apdf --help
26
27
  ```
27
28
 
28
- ## Usage
29
+ ## Usage As Library
30
+
31
+ ```python
32
+ from apdf import replace_pdf_text_detailed
33
+
34
+ rep = replace_pdf_text_detailed(
35
+ input_pdf="Test1.pdf",
36
+ output_pdf="Test1_out.pdf",
37
+ replacements={
38
+ "Old Text": "New Text",
39
+ # "Hello": "World",
40
+ # "2026": "2027",
41
+ },
42
+
43
+ # Optional parameters:
44
+ pages={2, 3, 4}, # Only these pages
45
+ # pages=2, # Only page 2
46
+ # pages=range(1, 6), # Pages 1 through 5
47
+ # pages=None, # All pages (default)
48
+
49
+ # fallback_font="FONT.ttf",
50
+ # font_fallback=True,
51
+ # strict=True,
52
+ )
53
+
54
+ print("Replacements:", rep.replacements)
55
+ print("Changed streams:", rep.changed_streams)
56
+ print("Scanned streams:", rep.scanned_streams)
57
+ print("Missing:", rep.missing)
58
+ print("Encoding failures:", rep.encoding_failures)
59
+ ```
60
+
61
+ ## Usage As CLI Tool
29
62
 
30
63
  ```bash
31
64
  apdf [-h] [-r OLD=NEW] [-nopa] [-s] [-ff FONT.ttf] [-no-ff] input [output]
@@ -60,6 +93,8 @@ apdf input.pdf output.pdf -r "foo=bar" -s
60
93
  * `input`: The source PDF file.
61
94
  * `output`: (Optional) The destination PDF file. Defaults to `output.pdf`.
62
95
  * `-r OLD=NEW`: Replace. The text replacement pair. You can supply this argument multiple times.
96
+ * `-p 2`, `-p 2,3,4`, or `-p 1-5` to edit only selected pages. Without
97
+ `-p`, all pages are processed.
63
98
  * `-nopa`: No preserve advance. Do not compensate `TJ` text widths after making replacements.
64
99
  * `-s`: Strict. Fail if the original text is missing or the selected font cannot encode the new text.
65
100
  * `-ff FONT.ttf`: Fallback font. TrueType font to embed when the PDF's subset font lacks a glyph.
File without changes
File without changes
apdf-1.2/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
+ _amswkxtziscddt = _installed_version()
19
+ if _supports_color():
20
+ _dbeoudxilennje = _BANNER.strip('\n').splitlines()
21
+ _dtfuvktjmwgvjl = [_RED, _RED, _BLUE, _BLUE, _CYAN]
22
+ for _wqkqcfhqfcawmm, _svuhqohydsiiyg in enumerate(_dbeoudxilennje):
23
+ _gbgexydwonsspj = _dtfuvktjmwgvjl[_wqkqcfhqfcawmm] if _wqkqcfhqfcawmm < len(_dtfuvktjmwgvjl) else _CYAN
24
+ print(_c(_svuhqohydsiiyg, _gbgexydwonsspj, _BOLD), file=sys.stderr)
25
+ else:
26
+ print(_BANNER.strip('\n'), file=sys.stderr)
27
+ _jtwtfqmxwneoft = _c(' Low-level PDF Te' + 'xt Replacer', _DIM)
28
+ _sgfacrlkmprjnb = _c(f' v{_amswkxtziscddt} ', _BOLD, _MAGENTA)
29
+ _cdaqjslqrxukic = _c(' by Abodx' + '9', _YELLOW, _BOLD)
30
+ print(f'{_jtwtfqmxwneoft}{_sgfacrlkmprjnb}{_cdaqjslqrxukic}\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
+ _qfrpzdwqfdzlmi: dict[str, str] = {}
36
+ for _dxupsndrhayzgq in values:
37
+ if '=' not in _dxupsndrhayzgq:
38
+ raise ValueError('-r requires OLD=NE' + 'W')
39
+ old, new = _dxupsndrhayzgq.split('=', 1)
40
+ if not old:
41
+ raise ValueError('OLD cannot b' + 'e empty')
42
+ _qfrpzdwqfdzlmi[old] = new
43
+ return _qfrpzdwqfdzlmi
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 _olmmerqreqttbs in value.split(','):
50
+ _hpcjkwacufzsbb = _olmmerqreqttbs.strip()
51
+ if not _hpcjkwacufzsbb:
52
+ raise ValueError('-p contains an empty p' + 'age number')
53
+ if '-' in _hpcjkwacufzsbb:
54
+ _nyefzumqaqiojc = [piece.strip() for piece in _hpcjkwacufzsbb.split('-')]
55
+ if len(_nyefzumqaqiojc) != 2 or not all((piece.isdigit() for piece in _nyefzumqaqiojc)):
56
+ raise ValueError(f'invalid page range: {_hpcjkwacufzsbb!r}')
57
+ start, _wnpdxkishjpszz = map(int, _nyefzumqaqiojc)
58
+ if start < 1 or _wnpdxkishjpszz < 1:
59
+ raise ValueError('page numbers ' + 'must be 1 or greater')
60
+ if start > _wnpdxkishjpszz:
61
+ raise ValueError(f'page range must be ascending: {_hpcjkwacufzsbb!r}')
62
+ pages.update(range(start, _wnpdxkishjpszz + 1))
63
+ else:
64
+ if not _hpcjkwacufzsbb.isdigit():
65
+ raise ValueError(f'invalid page number: {_hpcjkwacufzsbb!r}')
66
+ _pblbvcmmbkfown = int(_hpcjkwacufzsbb)
67
+ if _pblbvcmmbkfown < 1:
68
+ raise ValueError('page numbers must be 1 or greate' + 'r')
69
+ pages.add(_pblbvcmmbkfown)
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
+ _xfsdndyzqtewaa = argparse.ArgumentParser(description='Replace Unico' + 'de and ' + 'Hex text directly in PDF content streams.')
77
+ _xfsdndyzqtewaa.add_argument('input', help='In' + 'pu' + 't' + ' PDF')
78
+ _xfsdndyzqtewaa.add_argument('output', nargs='?', default='output.pdf', help='Outp' + 'ut PDF')
79
+ _xfsdndyzqtewaa.add_argument('-r', action='append', default=[], metavar='OLD=NEW', dest='replace', help='Replace: Unicode' + ' replacement; may be supplied' + ' ' + 'more than once')
80
+ _xfsdndyzqtewaa.add_argument('-p', metavar='PAGES', dest='pages', help='Pages to edit, e.g. 2, 2,3,4, or 1-5; defaults to' + ' all pages')
81
+ _xfsdndyzqtewaa.add_argument('-nopa', action='store_true', dest='no_preserve_advance', help='No preserve a' + 'dvanc' + 'e: Do not compensate TJ widths after replacements')
82
+ _xfsdndyzqtewaa.add_argument('-s', action='store_true', dest='strict', help='Strict: Fail if text is missing' + ' or the selected font cannot encode ' + 'it')
83
+ _xfsdndyzqtewaa.add_argument('-ff', metavar='FONT.ttf', dest='fallback_font', help='Fallback font: TrueType ' + 'font to emb' + "ed when the PDF's subset font lacks a glyph; defaults to a common system sans" + '-serif font')
84
+ _xfsdndyzqtewaa.add_argument('-no-ff', action='store_true', dest='no_font_fallback', help='No font fallback: Reject missing glyphs instead of embedding a fallback f' + 'ont')
85
+ _mwbmhuvhqcexzn = _xfsdndyzqtewaa.parse_args(argv)
86
+ try:
87
+ replacements = _parse_replacements(_mwbmhuvhqcexzn.replace)
88
+ pages = _parse_pages(_mwbmhuvhqcexzn.pages)
89
+ if not replacements:
90
+ _xfsdndyzqtewaa.error('provide at ' + 'l' + 'east one -r OLD=NEW')
91
+ _gslytzbbikuhza = replace_pdf_text_detailed(_mwbmhuvhqcexzn.input, _mwbmhuvhqcexzn.output, replacements, preserve_advance=not _mwbmhuvhqcexzn.no_preserve_advance, strict=_mwbmhuvhqcexzn.strict, fallback_font=_mwbmhuvhqcexzn.fallback_font, font_fallback=not _mwbmhuvhqcexzn.no_font_fallback, pages=pages)
92
+ except (OSError, PdfReplacementError, ValueError) as _hnjryszzanbgws:
93
+ _xfsdndyzqtewaa.exit(2, _c(f'error: {_hnjryszzanbgws}\n', _RED))
94
+ except Exception as _hnjryszzanbgws:
95
+ _xfsdndyzqtewaa.exit(2, _c(f'error: An unexpected error occurred: {type(_hnjryszzanbgws).__name__}: {_hnjryszzanbgws}\n', _RED))
96
+ print(_c(f'Replaced {_gslytzbbikuhza.replacements} occurrence(s) in {_gslytzbbikuhza.changed_streams}/{_gslytzbbikuhza.scanned_streams} stream(s) in {_gslytzbbikuhza.elapsed_seconds:.3f}s.', _GREEN))
97
+ if _gslytzbbikuhza.missing:
98
+ print(_c('Not fou' + 'nd: ' + ', '.join((repr(value) for value in _gslytzbbikuhza.missing)), _RED))
99
+ if _gslytzbbikuhza.encoding_failures:
100
+ print(_c(f'Skipped {len(_gslytzbbikuhza.encoding_failures)} unencodable replacement(s).', _RED))
101
+ print(_c(f'Saved: {_mwbmhuvhqcexzn.output}', _GREEN, _BOLD))
102
+ return 0
@@ -22,70 +22,70 @@ class FontCodec:
22
22
 
23
23
  @property
24
24
  def description(self) -> str:
25
- _wozpkadibwzfdo = str(self.pdf_font.get('/Subtype', 'unknown'))
26
- _jrcevktdeljeee = str(self.pdf_font.get('/BaseFont', 'unknown'))
27
- return f'{self.resource_name} {_wozpkadibwzfdo} {_jrcevktdeljeee}'
25
+ _uyxuhhghnfdole = str(self.pdf_font.get('/Subtype', 'unknown'))
26
+ _hfgtpkklfwfyhu = str(self.pdf_font.get('/BaseFont', 'unknown'))
27
+ return f'{self.resource_name} {_uyxuhhghnfdole} {_hfgtpkklfwfyhu}'
28
28
 
29
29
  def _build_inverse_maps(self) -> None:
30
- for _sqmvsfamvtglze, _uxcpimgqqzwkij in self.font.character_map.items():
31
- if isinstance(_sqmvsfamvtglze, str) and isinstance(_uxcpimgqqzwkij, str):
32
- self._unicode_to_intermediate.setdefault(_uxcpimgqqzwkij, _sqmvsfamvtglze)
30
+ for _lcvsmynogancdz, _eosacjoncaonky in self.font.character_map.items():
31
+ if isinstance(_lcvsmynogancdz, str) and isinstance(_eosacjoncaonky, str):
32
+ self._unicode_to_intermediate.setdefault(_eosacjoncaonky, _lcvsmynogancdz)
33
33
  encoding = self.font.encoding
34
34
  if isinstance(encoding, dict):
35
- for _vxlaoowljmxybg, _gsujpyyvkhsxxv in encoding.items():
36
- if isinstance(_vxlaoowljmxybg, int) and 0 <= _vxlaoowljmxybg <= 255 and isinstance(_gsujpyyvkhsxxv, str):
37
- self._intermediate_to_bytes.setdefault(_gsujpyyvkhsxxv, bytes((_vxlaoowljmxybg,)))
35
+ for _qpgpfkcfrltxfd, _bdjwjukswkkzno in encoding.items():
36
+ if isinstance(_qpgpfkcfrltxfd, int) and 0 <= _qpgpfkcfrltxfd <= 255 and isinstance(_bdjwjukswkkzno, str):
37
+ self._intermediate_to_bytes.setdefault(_bdjwjukswkkzno, bytes((_qpgpfkcfrltxfd,)))
38
38
 
39
39
  def _load_glyph_inventory(self) -> None:
40
40
  encoding = _deref(self.pdf_font.get('/Encoding'))
41
41
  if isinstance(encoding, DictionaryObject):
42
- _mrxxjcwxuerymf: int | None = None
43
- for _xcgndakfwknleb in encoding.get('/Differences', []):
44
- _xcgndakfwknleb = _deref(_xcgndakfwknleb)
45
- if isinstance(_xcgndakfwknleb, int):
46
- _mrxxjcwxuerymf = int(_xcgndakfwknleb)
47
- elif _mrxxjcwxuerymf is not None:
48
- self._difference_glyphs[_mrxxjcwxuerymf] = str(_xcgndakfwknleb).lstrip('/')
49
- _mrxxjcwxuerymf += 1
50
- _erhylqimslkpma = _deref(self.pdf_font.get('/FontDescriptor'))
51
- _bdhfnisphgpnnu = _erhylqimslkpma.get('/CharSet') if _erhylqimslkpma else None
52
- if _bdhfnisphgpnnu:
53
- self._embedded_glyphs = {name for name in str(_bdhfnisphgpnnu).split('/') if name}
42
+ _idyuovizmaukyk: int | None = None
43
+ for _rpkqhzmqmopedm in encoding.get('/Differences', []):
44
+ _rpkqhzmqmopedm = _deref(_rpkqhzmqmopedm)
45
+ if isinstance(_rpkqhzmqmopedm, int):
46
+ _idyuovizmaukyk = int(_rpkqhzmqmopedm)
47
+ elif _idyuovizmaukyk is not None:
48
+ self._difference_glyphs[_idyuovizmaukyk] = str(_rpkqhzmqmopedm).lstrip('/')
49
+ _idyuovizmaukyk += 1
50
+ _wwcgqjoucuolee = _deref(self.pdf_font.get('/FontDescriptor'))
51
+ _fahxkzceocodwc = _wwcgqjoucuolee.get('/CharSet') if _wwcgqjoucuolee else None
52
+ if _fahxkzceocodwc:
53
+ self._embedded_glyphs = {name for name in str(_fahxkzceocodwc).split('/') if name}
54
54
 
55
55
  def _glyph_name_for_code(self, code: int) -> str | None:
56
56
  if code in self._difference_glyphs:
57
57
  return self._difference_glyphs[code]
58
58
  encoding = self.font.encoding
59
59
  if isinstance(encoding, dict):
60
- _ffpiyggwzspapy = encoding.get(code)
61
- if isinstance(_ffpiyggwzspapy, str) and len(_ffpiyggwzspapy) == 1:
62
- return UV2AGL.get(ord(_ffpiyggwzspapy))
63
- _pbpwakbjyfyhga = str(self.pdf_font.get('/Encoding', ''))
64
- codec = {'/WinAnsiEncoding': 'cp1252', '/MacRomanEncoding': 'mac_roman'}.get(_pbpwakbjyfyhga, 'latin-1')
60
+ _dywvfsvhnaqyii = encoding.get(code)
61
+ if isinstance(_dywvfsvhnaqyii, str) and len(_dywvfsvhnaqyii) == 1:
62
+ return UV2AGL.get(ord(_dywvfsvhnaqyii))
63
+ _tchrccifvowoxq = str(self.pdf_font.get('/Encoding', ''))
64
+ codec = {'/WinAnsiEncoding': 'cp1252', '/MacRomanEncoding': 'mac_roman'}.get(_tchrccifvowoxq, 'latin-1')
65
65
  try:
66
- _ltexlwkjjboggt = bytes((code,)).decode(codec)
66
+ _qaxtbdsbsyhctz = bytes((code,)).decode(codec)
67
67
  except (UnicodeDecodeError, LookupError):
68
68
  return None
69
- return UV2AGL.get(ord(_ltexlwkjjboggt))
69
+ return UV2AGL.get(ord(_qaxtbdsbsyhctz))
70
70
 
71
71
  def _validate_embedded_glyphs(self, raw: bytes, visible: str) -> None:
72
- _cmjxrgamnknxhq = str(self.pdf_font.get('/Subtype', ''))
73
- if _cmjxrgamnknxhq == '/Type3':
72
+ _eoqbsnlnttdoyj = str(self.pdf_font.get('/Subtype', ''))
73
+ if _eoqbsnlnttdoyj == '/Type3':
74
74
  encoding = self.font.encoding
75
75
  if isinstance(encoding, dict):
76
- for _ynczgtjbhzcjld in raw:
77
- _mbeamyadqmkcmb = encoding.get(_ynczgtjbhzcjld, chr(_ynczgtjbhzcjld))
78
- if _mbeamyadqmkcmb not in self.font.character_map:
79
- raise FontEncodingError(f'{self.description} has no explicit Type3 mapping for character code 0x{_ynczgtjbhzcjld:02X}')
76
+ for _otreezbwbhttam in raw:
77
+ _jwdtjvgtwfyfil = encoding.get(_otreezbwbhttam, chr(_otreezbwbhttam))
78
+ if _jwdtjvgtwfyfil not in self.font.character_map:
79
+ raise FontEncodingError(f'{self.description} has no explicit Type3 mapping for character code 0x{_otreezbwbhttam:02X}')
80
80
  if self.decode(raw) != visible:
81
81
  raise FontEncodingError(f'{self.description} has no reversible glyph mapping for {visible!r}')
82
82
  return
83
83
  if self._embedded_glyphs is not None:
84
84
  missing: list[str] = []
85
- for _ynczgtjbhzcjld in raw:
86
- _wglsywbrgcuwqn = self._glyph_name_for_code(_ynczgtjbhzcjld)
87
- if _wglsywbrgcuwqn and _wglsywbrgcuwqn not in self._embedded_glyphs:
88
- missing.append(_wglsywbrgcuwqn)
85
+ for _otreezbwbhttam in raw:
86
+ _thkcwazszuuere = self._glyph_name_for_code(_otreezbwbhttam)
87
+ if _thkcwazszuuere and _thkcwazszuuere not in self._embedded_glyphs:
88
+ missing.append(_thkcwazszuuere)
89
89
  if missing:
90
90
  names = ', '.join(sorted(set(missing)))
91
91
  raise FontEncodingError(f'{self.description} embedded subset lacks glyph(s): {names}')
@@ -96,56 +96,56 @@ class FontCodec:
96
96
  encoding = self.font.encoding
97
97
  if isinstance(encoding, str):
98
98
  try:
99
- _dwjqepqdmzpydx = raw.decode(encoding, errors='surrogatepass')
99
+ _xplgkxapbgdsjn = raw.decode(encoding, errors='surrogatepass')
100
100
  except (LookupError, UnicodeError):
101
- _aiyhqgkozvuxvl = 'utf-16-be' if len(raw) % 2 == 0 else 'latin-1'
102
- _dwjqepqdmzpydx = raw.decode(_aiyhqgkozvuxvl, errors='replace')
101
+ _yszhhuogijrnto = 'utf-16-be' if len(raw) % 2 == 0 else 'latin-1'
102
+ _xplgkxapbgdsjn = raw.decode(_yszhhuogijrnto, errors='replace')
103
103
  else:
104
- _dwjqepqdmzpydx = ''.join((encoding.get(byte, chr(byte)) for byte in raw))
105
- return ''.join((self.font.character_map.get(character, character) for character in _dwjqepqdmzpydx))
104
+ _xplgkxapbgdsjn = ''.join((encoding.get(byte, chr(byte)) for byte in raw))
105
+ return ''.join((self.font.character_map.get(character, character) for character in _xplgkxapbgdsjn))
106
106
 
107
107
  @staticmethod
108
108
  def _greedy_map(text: str, mapping: Mapping[str, str]) -> str:
109
109
  if not mapping:
110
110
  return text
111
- _taysbvduzlwvpf = sorted(mapping, key=len, reverse=True)
111
+ _dmfudfobrlgihq = sorted(mapping, key=len, reverse=True)
112
112
  output: list[str] = []
113
- _rjciwatenvnubt = 0
114
- while _rjciwatenvnubt < len(text):
115
- for key in _taysbvduzlwvpf:
116
- if key and text.startswith(key, _rjciwatenvnubt):
113
+ _cckiwwydtundcz = 0
114
+ while _cckiwwydtundcz < len(text):
115
+ for key in _dmfudfobrlgihq:
116
+ if key and text.startswith(key, _cckiwwydtundcz):
117
117
  output.append(mapping[key])
118
- _rjciwatenvnubt += len(key)
118
+ _cckiwwydtundcz += len(key)
119
119
  break
120
120
  else:
121
- output.append(text[_rjciwatenvnubt])
122
- _rjciwatenvnubt += 1
121
+ output.append(text[_cckiwwydtundcz])
122
+ _cckiwwydtundcz += 1
123
123
  return ''.join(output)
124
124
 
125
125
  def encode(self, visible: str) -> bytes:
126
- _mqcsiaerslqqcw = self._greedy_map(visible, self._unicode_to_intermediate)
126
+ _exvunfwfpazrqi = self._greedy_map(visible, self._unicode_to_intermediate)
127
127
  encoding = self.font.encoding
128
128
  if isinstance(encoding, str):
129
129
  try:
130
- _zqiehzxforrqfg = _mqcsiaerslqqcw.encode(encoding, errors='strict')
131
- except (LookupError, UnicodeError) as _ethzaketqjwtcf:
132
- raise FontEncodingError(f'{self.description} cannot encode {visible!r}') from _ethzaketqjwtcf
133
- self._validate_embedded_glyphs(_zqiehzxforrqfg, visible)
134
- return _zqiehzxforrqfg
135
- _golytujyyqqjgy = sorted(self._intermediate_to_bytes, key=len, reverse=True)
136
- _npwxcsvwxxfsgx = bytearray()
137
- _pnqijbrslweiwd = 0
138
- while _pnqijbrslweiwd < len(_mqcsiaerslqqcw):
139
- for key in _golytujyyqqjgy:
140
- if key and _mqcsiaerslqqcw.startswith(key, _pnqijbrslweiwd):
141
- _npwxcsvwxxfsgx.extend(self._intermediate_to_bytes[key])
142
- _pnqijbrslweiwd += len(key)
130
+ _okuzpqmtslbyny = _exvunfwfpazrqi.encode(encoding, errors='strict')
131
+ except (LookupError, UnicodeError) as _ummwdfctjvzuyw:
132
+ raise FontEncodingError(f'{self.description} cannot encode {visible!r}') from _ummwdfctjvzuyw
133
+ self._validate_embedded_glyphs(_okuzpqmtslbyny, visible)
134
+ return _okuzpqmtslbyny
135
+ _qcjmtvbxylehwm = sorted(self._intermediate_to_bytes, key=len, reverse=True)
136
+ _dsncebznuwabyp = bytearray()
137
+ _rjokoymizxbigc = 0
138
+ while _rjokoymizxbigc < len(_exvunfwfpazrqi):
139
+ for key in _qcjmtvbxylehwm:
140
+ if key and _exvunfwfpazrqi.startswith(key, _rjokoymizxbigc):
141
+ _dsncebznuwabyp.extend(self._intermediate_to_bytes[key])
142
+ _rjokoymizxbigc += len(key)
143
143
  break
144
144
  else:
145
- raise FontEncodingError(f'{self.description} cannot encode character {_mqcsiaerslqqcw[_pnqijbrslweiwd]!r} in {visible!r}')
146
- _zqiehzxforrqfg = bytes(_npwxcsvwxxfsgx)
147
- self._validate_embedded_glyphs(_zqiehzxforrqfg, visible)
148
- return _zqiehzxforrqfg
145
+ raise FontEncodingError(f'{self.description} cannot encode character {_exvunfwfpazrqi[_rjokoymizxbigc]!r} in {visible!r}')
146
+ _okuzpqmtslbyny = bytes(_dsncebznuwabyp)
147
+ self._validate_embedded_glyphs(_okuzpqmtslbyny, visible)
148
+ return _okuzpqmtslbyny
149
149
 
150
150
  def width(self, visible: str) -> float:
151
151
  try:
@@ -0,0 +1,163 @@
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, 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, _RED
17
+
18
+ def _set_decoded_stream_data(stream: StreamObject, data: bytes) -> None:
19
+ try:
20
+ stream.set_data(data)
21
+ except Exception as _qimfvkudpmbtfm:
22
+ raise PdfReplacementError(_c('Ca' + 'nnot rewrite a content stream with its current PDF filters', _RED)) from _qimfvkudpmbtfm
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
+ _dolsbllmxcvcmg = _deref(owner.get('/Contents'))
37
+ if _dolsbllmxcvcmg is None:
38
+ return
39
+ if isinstance(_dolsbllmxcvcmg, (list, ArrayObject)):
40
+ for _bqakijrtfdaakn in _dolsbllmxcvcmg:
41
+ _kttfvedgmsdpoh = _deref(_bqakijrtfdaakn)
42
+ if isinstance(_kttfvedgmsdpoh, StreamObject):
43
+ yield _kttfvedgmsdpoh
44
+ elif isinstance(_dolsbllmxcvcmg, StreamObject):
45
+ yield _dolsbllmxcvcmg
46
+
47
+ def _isolated_reference(value: Any, writer: PdfWriter) -> Any:
48
+ value = _deref(value)
49
+ clone = value.clone(writer, force_duplicate=True)
50
+ return getattr(clone, 'indirect_reference', None) or clone
51
+
52
+ def _isolate_selected_page(page: Any, writer: PdfWriter) -> None:
53
+ _hpzzzcblqqbfum = page.get('/Contents')
54
+ if _hpzzzcblqqbfum is not None:
55
+ page[NameObject('/Contents')] = _isolated_reference(_hpzzzcblqqbfum, writer)
56
+ _yaplwytvllhltw = page.get('/Resources')
57
+ if _yaplwytvllhltw is not None:
58
+ page[NameObject('/Resources')] = _isolated_reference(_yaplwytvllhltw, writer)
59
+
60
+ def _selected_page_numbers(pages: Iterable[int] | int | None, page_count: int) -> set[int]:
61
+ if pages is None:
62
+ return set(range(1, page_count + 1))
63
+ if isinstance(pages, bool):
64
+ raise ValueError(_c('pages must cont' + 'ain ' + 'page numbers, not a bool' + 'ean', _RED))
65
+ if isinstance(pages, int):
66
+ _vloosseizdeydt = {pages}
67
+ elif isinstance(pages, (str, bytes)):
68
+ raise ValueError(_c('pages must be page numb' + 'ers; use the CLI -p option for ranges', _RED))
69
+ else:
70
+ _vloosseizdeydt = {int(page) for page in pages}
71
+ if not _vloosseizdeydt:
72
+ raise ValueError(_c('a' + 't least' + ' o' + 'ne page must be selected', _RED))
73
+ _itizoyugiqqlgt = sorted((page for page in _vloosseizdeydt if page < 1 or page > page_count))
74
+ if _itizoyugiqqlgt:
75
+ _rersoiglxwwowp = ', '.join((str(page) for page in _itizoyugiqqlgt))
76
+ raise ValueError(_c(f'page selection out of range: {_rersoiglxwwowp}; PDF has {page_count} page(s)', _RED))
77
+ return _vloosseizdeydt
78
+
79
+ 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:
80
+ resources = _deref(resources)
81
+ xobjects = _deref(resources.get('/XObject', {})) if resources else {}
82
+ for _, reference in getattr(xobjects, 'items', lambda: [])():
83
+ form = _deref(reference)
84
+ if not isinstance(form, StreamObject) or form.get('/Subtype') != '/Form':
85
+ continue
86
+ key = _object_key(form)
87
+ if key in visited:
88
+ continue
89
+ visited.add(key)
90
+ form_resources = _deref(form.get('/Resources')) or resources
91
+ _rewrite_stream(form, pdf, form_resources, replacements, preserve_advance, report, matched, fallback_registry, page_number)
92
+ _rewrite_forms(form_resources, pdf, replacements, preserve_advance, report, matched, visited, fallback_registry, page_number)
93
+
94
+ 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:
95
+ _eiplhxbzivdjid = time.perf_counter()
96
+ _asqgrajlilnymb = [Replacement(str(old), str(new)) for old, new in replacements.items() if old]
97
+ if not _asqgrajlilnymb:
98
+ raise ValueError('At le' + 'ast one non-empty source string i' + 's ' + 'required')
99
+ _avkchisdcnfnfu = Path(input_pdf)
100
+ _kmbfuvsptjbvvm = Path(output_pdf)
101
+ if not _avkchisdcnfnfu.is_file():
102
+ raise PdfFileNotFoundError(f'File not found: {_avkchisdcnfnfu}')
103
+ _kmbfuvsptjbvvm.parent.mkdir(parents=True, exist_ok=True)
104
+ try:
105
+ _znbdymmzxdbjxz = PdfReader(_avkchisdcnfnfu, strict=False)
106
+ except DependencyError as _vxuekhsvigxblx:
107
+ raise PdfDependencyError(f"Missing dependency for encrypted PDF: {_vxuekhsvigxblx}\nPlease install it, e.g., 'uv pip install cryptography'") from _vxuekhsvigxblx
108
+ except PermissionError as _vxuekhsvigxblx:
109
+ raise PdfPermissionError(f"Permission denied when opening {_avkchisdcnfnfu}.{_c('The PDF might be opened by another application.', _RED)}") from _vxuekhsvigxblx
110
+ if _znbdymmzxdbjxz.is_encrypted:
111
+ raise PdfReplacementError(_c('Encry' + 'pted PDFs are not supported ' + 'without' + ' a password', _RED))
112
+ writer = PdfWriter(clone_from=_znbdymmzxdbjxz)
113
+ _pgqxvhnhiiuknc = _selected_page_numbers(pages, len(writer.pages))
114
+ _gpjfndmlmfddsg = len(_pgqxvhnhiiuknc) != len(writer.pages)
115
+ _xxopteijgitciy = FallbackFontRegistry(writer, explicit_font=fallback_font, enabled=font_fallback)
116
+ _fhjnljuqkjsjef = ReplacementReport(pages=len(writer.pages))
117
+ _kgjsxoleqweexl: set[str] = set()
118
+ _jzjtbirwyfcdkt: set[tuple[Any, ...]] = set()
119
+ for _jrstnpxllcbbay, _gijoksblqlrcsn in enumerate(writer.pages, start=1):
120
+ if _jrstnpxllcbbay not in _pgqxvhnhiiuknc:
121
+ continue
122
+ print(_c(f'Processing page {_jrstnpxllcbbay}/{_fhjnljuqkjsjef.pages}', _BLUE), file=sys.stderr)
123
+ if _gpjfndmlmfddsg:
124
+ _isolate_selected_page(_gijoksblqlrcsn, writer)
125
+ _rorabtaxuurlig = _deref(_gijoksblqlrcsn.get('/Resources', {}))
126
+ for _mbvmfdocfgxdom in _content_streams(_gijoksblqlrcsn):
127
+ key = _object_key(_mbvmfdocfgxdom)
128
+ if key in _jzjtbirwyfcdkt:
129
+ continue
130
+ _jzjtbirwyfcdkt.add(key)
131
+ _rewrite_stream(_mbvmfdocfgxdom, writer, _rorabtaxuurlig, _asqgrajlilnymb, preserve_advance, _fhjnljuqkjsjef, _kgjsxoleqweexl, _xxopteijgitciy, _jrstnpxllcbbay)
132
+ _rewrite_forms(_rorabtaxuurlig, writer, _asqgrajlilnymb, preserve_advance, _fhjnljuqkjsjef, _kgjsxoleqweexl, _jzjtbirwyfcdkt, _xxopteijgitciy, _jrstnpxllcbbay)
133
+ _xxopteijgitciy.finalize()
134
+ _fhjnljuqkjsjef.missing = [rule.old for rule in _asqgrajlilnymb if rule.old not in _kgjsxoleqweexl]
135
+ _fhjnljuqkjsjef.elapsed_seconds = time.perf_counter() - _eiplhxbzivdjid
136
+ if strict and (_fhjnljuqkjsjef.missing or _fhjnljuqkjsjef.encoding_failures):
137
+ _emuwckmalnjhtg = []
138
+ if _fhjnljuqkjsjef.missing:
139
+ _emuwckmalnjhtg.append(f'not found: {_fhjnljuqkjsjef.missing!r}')
140
+ if _fhjnljuqkjsjef.encoding_failures:
141
+ _emuwckmalnjhtg.append(f'encoding failures: {_fhjnljuqkjsjef.encoding_failures!r}')
142
+ raise PdfReplacementError('; '.join(_emuwckmalnjhtg))
143
+ _bmpmljfrdmkhvo: str | None = None
144
+ try:
145
+ try:
146
+ with tempfile.NamedTemporaryFile(mode='wb', prefix=f'.{_kmbfuvsptjbvvm.name}.', suffix='.tmp', dir=_kmbfuvsptjbvvm.parent, delete=False) as _dtbxckupjrbqnd:
147
+ _bmpmljfrdmkhvo = _dtbxckupjrbqnd.name
148
+ writer.write(_dtbxckupjrbqnd)
149
+ _miwiypflfwltgy = PdfReader(_bmpmljfrdmkhvo, strict=False)
150
+ if len(_miwiypflfwltgy.pages) != _fhjnljuqkjsjef.pages:
151
+ raise PdfReplacementError('Out' + 'put veri' + 'fication found a page-count mismatch')
152
+ os.replace(_bmpmljfrdmkhvo, _kmbfuvsptjbvvm)
153
+ _bmpmljfrdmkhvo = None
154
+ except PermissionError as _vxuekhsvigxblx:
155
+ raise PdfPermissionError(f'Permission denied when saving to {_kmbfuvsptjbvvm}. The PDF might be opened by another application.') from _vxuekhsvigxblx
156
+ finally:
157
+ if _bmpmljfrdmkhvo and os.path.exists(_bmpmljfrdmkhvo):
158
+ os.unlink(_bmpmljfrdmkhvo)
159
+ _fhjnljuqkjsjef.elapsed_seconds = time.perf_counter() - _eiplhxbzivdjid
160
+ return _fhjnljuqkjsjef
161
+
162
+ def replace_pdf_text(input_pdf: str | os.PathLike[str], output_pdf: str | os.PathLike[str], replacements: Mapping[str, str], **kwargs: Any) -> int:
163
+ return replace_pdf_text_detailed(input_pdf, output_pdf, replacements, **kwargs).replacements
File without changes