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.
- {apdf-1.0 → apdf-1.2}/PKG-INFO +38 -3
- {apdf-1.0 → apdf-1.2}/README.md +37 -2
- {apdf-1.0 → apdf-1.2}/apdf/__init__.py +0 -0
- {apdf-1.0 → apdf-1.2}/apdf/__main__.py +0 -0
- apdf-1.2/apdf/cli.py +102 -0
- {apdf-1.0 → apdf-1.2}/apdf/codec.py +69 -69
- apdf-1.2/apdf/engine.py +163 -0
- {apdf-1.0 → apdf-1.2}/apdf/exceptions.py +0 -0
- {apdf-1.0 → apdf-1.2}/apdf/fallback.py +62 -62
- {apdf-1.0 → apdf-1.2}/apdf/models.py +0 -0
- apdf-1.2/apdf/operations.py +408 -0
- {apdf-1.0 → apdf-1.2}/apdf/utils.py +0 -0
- {apdf-1.0/apdf.obfuscation-backup-20260820-110606 → apdf-1.2/apdf.obfuscation-backup-20260824-112646}/__init__.py +0 -0
- {apdf-1.0/apdf.obfuscation-backup-20260820-110606 → apdf-1.2/apdf.obfuscation-backup-20260824-112646}/__main__.py +0 -0
- {apdf-1.0/apdf.obfuscation-backup-20260820-110606 → apdf-1.2/apdf.obfuscation-backup-20260824-112646}/cli.py +34 -0
- {apdf-1.0/apdf.obfuscation-backup-20260820-110606 → apdf-1.2/apdf.obfuscation-backup-20260824-112646}/codec.py +0 -0
- {apdf-1.0/apdf.obfuscation-backup-20260820-110606 → apdf-1.2/apdf.obfuscation-backup-20260824-112646}/engine.py +66 -4
- {apdf-1.0/apdf.obfuscation-backup-20260820-110606 → apdf-1.2/apdf.obfuscation-backup-20260824-112646}/exceptions.py +0 -0
- {apdf-1.0/apdf.obfuscation-backup-20260820-110606 → apdf-1.2/apdf.obfuscation-backup-20260824-112646}/fallback.py +0 -0
- {apdf-1.0/apdf.obfuscation-backup-20260820-110606 → apdf-1.2/apdf.obfuscation-backup-20260824-112646}/models.py +0 -0
- {apdf-1.0/apdf.obfuscation-backup-20260820-110606 → apdf-1.2/apdf.obfuscation-backup-20260824-112646}/operations.py +77 -10
- {apdf-1.0/apdf.obfuscation-backup-20260820-110606 → apdf-1.2/apdf.obfuscation-backup-20260824-112646}/utils.py +0 -0
- {apdf-1.0 → apdf-1.2}/pyproject.toml +1 -1
- {apdf-1.0 → apdf-1.2}/uv.lock +1 -1
- apdf-1.0/apdf/cli.py +0 -73
- apdf-1.0/apdf/engine.py +0 -125
- apdf-1.0/apdf/operations.py +0 -384
- {apdf-1.0 → apdf-1.2}/.gitignore +0 -0
{apdf-1.0 → apdf-1.2}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: apdf
|
|
3
|
-
Version: 1.
|
|
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
|
-
#
|
|
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.
|
{apdf-1.0 → apdf-1.2}/README.md
RENAMED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
return f'{self.resource_name} {
|
|
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
|
|
31
|
-
if isinstance(
|
|
32
|
-
self._unicode_to_intermediate.setdefault(
|
|
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
|
|
36
|
-
if isinstance(
|
|
37
|
-
self._intermediate_to_bytes.setdefault(
|
|
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
|
-
|
|
43
|
-
for
|
|
44
|
-
|
|
45
|
-
if isinstance(
|
|
46
|
-
|
|
47
|
-
elif
|
|
48
|
-
self._difference_glyphs[
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
if
|
|
53
|
-
self._embedded_glyphs = {name for name in str(
|
|
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
|
-
|
|
61
|
-
if isinstance(
|
|
62
|
-
return UV2AGL.get(ord(
|
|
63
|
-
|
|
64
|
-
codec = {'/WinAnsiEncoding': 'cp1252', '/MacRomanEncoding': 'mac_roman'}.get(
|
|
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
|
-
|
|
66
|
+
_qaxtbdsbsyhctz = bytes((code,)).decode(codec)
|
|
67
67
|
except (UnicodeDecodeError, LookupError):
|
|
68
68
|
return None
|
|
69
|
-
return UV2AGL.get(ord(
|
|
69
|
+
return UV2AGL.get(ord(_qaxtbdsbsyhctz))
|
|
70
70
|
|
|
71
71
|
def _validate_embedded_glyphs(self, raw: bytes, visible: str) -> None:
|
|
72
|
-
|
|
73
|
-
if
|
|
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
|
|
77
|
-
|
|
78
|
-
if
|
|
79
|
-
raise FontEncodingError(f'{self.description} has no explicit Type3 mapping for character code 0x{
|
|
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
|
|
86
|
-
|
|
87
|
-
if
|
|
88
|
-
missing.append(
|
|
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
|
-
|
|
99
|
+
_xplgkxapbgdsjn = raw.decode(encoding, errors='surrogatepass')
|
|
100
100
|
except (LookupError, UnicodeError):
|
|
101
|
-
|
|
102
|
-
|
|
101
|
+
_yszhhuogijrnto = 'utf-16-be' if len(raw) % 2 == 0 else 'latin-1'
|
|
102
|
+
_xplgkxapbgdsjn = raw.decode(_yszhhuogijrnto, errors='replace')
|
|
103
103
|
else:
|
|
104
|
-
|
|
105
|
-
return ''.join((self.font.character_map.get(character, character) for character in
|
|
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
|
-
|
|
111
|
+
_dmfudfobrlgihq = sorted(mapping, key=len, reverse=True)
|
|
112
112
|
output: list[str] = []
|
|
113
|
-
|
|
114
|
-
while
|
|
115
|
-
for key in
|
|
116
|
-
if key and text.startswith(key,
|
|
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
|
-
|
|
118
|
+
_cckiwwydtundcz += len(key)
|
|
119
119
|
break
|
|
120
120
|
else:
|
|
121
|
-
output.append(text[
|
|
122
|
-
|
|
121
|
+
output.append(text[_cckiwwydtundcz])
|
|
122
|
+
_cckiwwydtundcz += 1
|
|
123
123
|
return ''.join(output)
|
|
124
124
|
|
|
125
125
|
def encode(self, visible: str) -> bytes:
|
|
126
|
-
|
|
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
|
-
|
|
131
|
-
except (LookupError, UnicodeError) as
|
|
132
|
-
raise FontEncodingError(f'{self.description} cannot encode {visible!r}') from
|
|
133
|
-
self._validate_embedded_glyphs(
|
|
134
|
-
return
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
while
|
|
139
|
-
for key in
|
|
140
|
-
if key and
|
|
141
|
-
|
|
142
|
-
|
|
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 {
|
|
146
|
-
|
|
147
|
-
self._validate_embedded_glyphs(
|
|
148
|
-
return
|
|
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:
|
apdf-1.2/apdf/engine.py
ADDED
|
@@ -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
|