nestedpdfmerger 1.0.0__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.
- nestedpdfmerger-1.0.0/LICENSE +21 -0
- nestedpdfmerger-1.0.0/PKG-INFO +163 -0
- nestedpdfmerger-1.0.0/README.md +114 -0
- nestedpdfmerger-1.0.0/pyproject.toml +55 -0
- nestedpdfmerger-1.0.0/setup.cfg +4 -0
- nestedpdfmerger-1.0.0/src/nestedpdfmerger/__init__.py +6 -0
- nestedpdfmerger-1.0.0/src/nestedpdfmerger/__main__.py +5 -0
- nestedpdfmerger-1.0.0/src/nestedpdfmerger/bookmarks.py +37 -0
- nestedpdfmerger-1.0.0/src/nestedpdfmerger/cli.py +132 -0
- nestedpdfmerger-1.0.0/src/nestedpdfmerger/config.py +8 -0
- nestedpdfmerger-1.0.0/src/nestedpdfmerger/errors.py +13 -0
- nestedpdfmerger-1.0.0/src/nestedpdfmerger/merger.py +289 -0
- nestedpdfmerger-1.0.0/src/nestedpdfmerger/sorting.py +58 -0
- nestedpdfmerger-1.0.0/src/nestedpdfmerger.egg-info/PKG-INFO +163 -0
- nestedpdfmerger-1.0.0/src/nestedpdfmerger.egg-info/SOURCES.txt +18 -0
- nestedpdfmerger-1.0.0/src/nestedpdfmerger.egg-info/dependency_links.txt +1 -0
- nestedpdfmerger-1.0.0/src/nestedpdfmerger.egg-info/entry_points.txt +2 -0
- nestedpdfmerger-1.0.0/src/nestedpdfmerger.egg-info/requires.txt +7 -0
- nestedpdfmerger-1.0.0/src/nestedpdfmerger.egg-info/top_level.txt +1 -0
- nestedpdfmerger-1.0.0/tests/test_merger.py +377 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 nested-pdf-merger contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nestedpdfmerger
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Merge PDFs recursively from a folder tree into a single PDF with hierarchical bookmarks.
|
|
5
|
+
License: MIT License
|
|
6
|
+
|
|
7
|
+
Copyright (c) 2026 nested-pdf-merger contributors
|
|
8
|
+
|
|
9
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
10
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
11
|
+
in the Software without restriction, including without limitation the rights
|
|
12
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
13
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
14
|
+
furnished to do so, subject to the following conditions:
|
|
15
|
+
|
|
16
|
+
The above copyright notice and this permission notice shall be included in all
|
|
17
|
+
copies or substantial portions of the Software.
|
|
18
|
+
|
|
19
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
20
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
21
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
22
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
23
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
24
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
25
|
+
SOFTWARE.
|
|
26
|
+
|
|
27
|
+
Project-URL: Repository, https://github.com/Lyutenant/nested-pdf-merger
|
|
28
|
+
Keywords: pdf,merge,bookmarks,cli
|
|
29
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
30
|
+
Classifier: Environment :: Console
|
|
31
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
32
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
33
|
+
Classifier: Operating System :: OS Independent
|
|
34
|
+
Classifier: Programming Language :: Python :: 3
|
|
35
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
36
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
37
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
38
|
+
Classifier: Topic :: Utilities
|
|
39
|
+
Requires-Python: >=3.10
|
|
40
|
+
Description-Content-Type: text/markdown
|
|
41
|
+
License-File: LICENSE
|
|
42
|
+
Requires-Dist: pypdf>=4.0
|
|
43
|
+
Requires-Dist: natsort>=8.0
|
|
44
|
+
Provides-Extra: dev
|
|
45
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
46
|
+
Requires-Dist: pytest-cov>=4.0; extra == "dev"
|
|
47
|
+
Requires-Dist: ruff>=0.1; extra == "dev"
|
|
48
|
+
Dynamic: license-file
|
|
49
|
+
|
|
50
|
+
# nestedpdfmerger
|
|
51
|
+
|
|
52
|
+
Merge PDFs recursively from a folder tree into a single PDF with automatic hierarchical bookmarks based on the directory structure.
|
|
53
|
+
|
|
54
|
+
## Installation
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
pip install nestedpdfmerger
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Quick start
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
nestedpdfmerger ./reports -o merged.pdf
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Example input tree
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
reports/
|
|
70
|
+
├── intro.pdf
|
|
71
|
+
├── chapter1/
|
|
72
|
+
│ ├── part1.pdf
|
|
73
|
+
│ └── part2.pdf
|
|
74
|
+
└── appendix/
|
|
75
|
+
└── appendixA.pdf
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Resulting bookmarks in `merged.pdf`:
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
intro
|
|
82
|
+
chapter1
|
|
83
|
+
├─ part1
|
|
84
|
+
└─ part2
|
|
85
|
+
appendix
|
|
86
|
+
└─ appendixA
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Usage
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
nestedpdfmerger INPUT_DIR -o OUTPUT.pdf [options]
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Options
|
|
96
|
+
|
|
97
|
+
| Flag | Description |
|
|
98
|
+
|---|---|
|
|
99
|
+
| `-o, --output PATH` | Output PDF path (default: `<INPUT_DIR>.pdf`) |
|
|
100
|
+
| `--sort {natural,alpha,mtime}` | Sort mode (default: `natural`) |
|
|
101
|
+
| `--reverse` | Reverse sort order |
|
|
102
|
+
| `--exclude NAME [NAME ...]` | Directory names to skip |
|
|
103
|
+
| `--exclude-hidden` | Skip hidden directories (starting with `.`) |
|
|
104
|
+
| `--no-bookmarks` | Disable hierarchical bookmarks |
|
|
105
|
+
| `--dry-run` | Preview merge order without writing output |
|
|
106
|
+
| `--strict` | Stop on first PDF error instead of skipping |
|
|
107
|
+
| `--verbose` | Show detailed progress |
|
|
108
|
+
| `--quiet` | Suppress non-error output |
|
|
109
|
+
| `--version` | Show version and exit |
|
|
110
|
+
|
|
111
|
+
### Examples
|
|
112
|
+
|
|
113
|
+
Preview what would be merged:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
nestedpdfmerger ./reports --dry-run
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Natural sort, exclude backup folders, verbose output:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
nestedpdfmerger ./reports \
|
|
123
|
+
--output merged.pdf \
|
|
124
|
+
--sort natural \
|
|
125
|
+
--exclude Backup Data \
|
|
126
|
+
--verbose
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Sort by modification time, newest last:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
nestedpdfmerger ./reports -o merged.pdf --sort mtime --reverse
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Python API
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
from nestedpdfmerger import merge_pdf_tree
|
|
139
|
+
|
|
140
|
+
merge_pdf_tree(
|
|
141
|
+
input_dir="reports",
|
|
142
|
+
output_file="merged.pdf",
|
|
143
|
+
sort_mode="natural",
|
|
144
|
+
exclude=["Backup", "Data"],
|
|
145
|
+
bookmarks=True,
|
|
146
|
+
)
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Sort modes
|
|
150
|
+
|
|
151
|
+
| Mode | Description |
|
|
152
|
+
|---|---|
|
|
153
|
+
| `natural` | Human-friendly natural sort (1, 2, 10 not 1, 10, 2) |
|
|
154
|
+
| `alpha` | Case-insensitive alphabetical |
|
|
155
|
+
| `mtime` | File/directory modification time (oldest first) |
|
|
156
|
+
|
|
157
|
+
## Error handling
|
|
158
|
+
|
|
159
|
+
By default, corrupted, encrypted, or unreadable PDFs are **warned and skipped**. Use `--strict` to stop on the first error.
|
|
160
|
+
|
|
161
|
+
## License
|
|
162
|
+
|
|
163
|
+
MIT
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# nestedpdfmerger
|
|
2
|
+
|
|
3
|
+
Merge PDFs recursively from a folder tree into a single PDF with automatic hierarchical bookmarks based on the directory structure.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install nestedpdfmerger
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick start
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
nestedpdfmerger ./reports -o merged.pdf
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Example input tree
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
reports/
|
|
21
|
+
├── intro.pdf
|
|
22
|
+
├── chapter1/
|
|
23
|
+
│ ├── part1.pdf
|
|
24
|
+
│ └── part2.pdf
|
|
25
|
+
└── appendix/
|
|
26
|
+
└── appendixA.pdf
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Resulting bookmarks in `merged.pdf`:
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
intro
|
|
33
|
+
chapter1
|
|
34
|
+
├─ part1
|
|
35
|
+
└─ part2
|
|
36
|
+
appendix
|
|
37
|
+
└─ appendixA
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Usage
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
nestedpdfmerger INPUT_DIR -o OUTPUT.pdf [options]
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
### Options
|
|
47
|
+
|
|
48
|
+
| Flag | Description |
|
|
49
|
+
|---|---|
|
|
50
|
+
| `-o, --output PATH` | Output PDF path (default: `<INPUT_DIR>.pdf`) |
|
|
51
|
+
| `--sort {natural,alpha,mtime}` | Sort mode (default: `natural`) |
|
|
52
|
+
| `--reverse` | Reverse sort order |
|
|
53
|
+
| `--exclude NAME [NAME ...]` | Directory names to skip |
|
|
54
|
+
| `--exclude-hidden` | Skip hidden directories (starting with `.`) |
|
|
55
|
+
| `--no-bookmarks` | Disable hierarchical bookmarks |
|
|
56
|
+
| `--dry-run` | Preview merge order without writing output |
|
|
57
|
+
| `--strict` | Stop on first PDF error instead of skipping |
|
|
58
|
+
| `--verbose` | Show detailed progress |
|
|
59
|
+
| `--quiet` | Suppress non-error output |
|
|
60
|
+
| `--version` | Show version and exit |
|
|
61
|
+
|
|
62
|
+
### Examples
|
|
63
|
+
|
|
64
|
+
Preview what would be merged:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
nestedpdfmerger ./reports --dry-run
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Natural sort, exclude backup folders, verbose output:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
nestedpdfmerger ./reports \
|
|
74
|
+
--output merged.pdf \
|
|
75
|
+
--sort natural \
|
|
76
|
+
--exclude Backup Data \
|
|
77
|
+
--verbose
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Sort by modification time, newest last:
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
nestedpdfmerger ./reports -o merged.pdf --sort mtime --reverse
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Python API
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
from nestedpdfmerger import merge_pdf_tree
|
|
90
|
+
|
|
91
|
+
merge_pdf_tree(
|
|
92
|
+
input_dir="reports",
|
|
93
|
+
output_file="merged.pdf",
|
|
94
|
+
sort_mode="natural",
|
|
95
|
+
exclude=["Backup", "Data"],
|
|
96
|
+
bookmarks=True,
|
|
97
|
+
)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Sort modes
|
|
101
|
+
|
|
102
|
+
| Mode | Description |
|
|
103
|
+
|---|---|
|
|
104
|
+
| `natural` | Human-friendly natural sort (1, 2, 10 not 1, 10, 2) |
|
|
105
|
+
| `alpha` | Case-insensitive alphabetical |
|
|
106
|
+
| `mtime` | File/directory modification time (oldest first) |
|
|
107
|
+
|
|
108
|
+
## Error handling
|
|
109
|
+
|
|
110
|
+
By default, corrupted, encrypted, or unreadable PDFs are **warned and skipped**. Use `--strict` to stop on the first error.
|
|
111
|
+
|
|
112
|
+
## License
|
|
113
|
+
|
|
114
|
+
MIT
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "nestedpdfmerger"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Merge PDFs recursively from a folder tree into a single PDF with hierarchical bookmarks."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { file = "LICENSE" }
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
keywords = ["pdf", "merge", "bookmarks", "cli"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 5 - Production/Stable",
|
|
15
|
+
"Environment :: Console",
|
|
16
|
+
"Intended Audience :: End Users/Desktop",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Topic :: Utilities",
|
|
24
|
+
]
|
|
25
|
+
dependencies = [
|
|
26
|
+
"pypdf>=4.0",
|
|
27
|
+
"natsort>=8.0",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.optional-dependencies]
|
|
31
|
+
dev = [
|
|
32
|
+
"pytest>=7.0",
|
|
33
|
+
"pytest-cov>=4.0",
|
|
34
|
+
"ruff>=0.1",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
[project.scripts]
|
|
38
|
+
nestedpdfmerger = "nestedpdfmerger.cli:main"
|
|
39
|
+
|
|
40
|
+
[project.urls]
|
|
41
|
+
Repository = "https://github.com/Lyutenant/nested-pdf-merger"
|
|
42
|
+
|
|
43
|
+
[tool.setuptools.packages.find]
|
|
44
|
+
where = ["src"]
|
|
45
|
+
|
|
46
|
+
[tool.pytest.ini_options]
|
|
47
|
+
testpaths = ["tests"]
|
|
48
|
+
|
|
49
|
+
[tool.ruff]
|
|
50
|
+
line-length = 88
|
|
51
|
+
target-version = "py310"
|
|
52
|
+
exclude = ["script.py", "examples/"]
|
|
53
|
+
|
|
54
|
+
[tool.ruff.lint]
|
|
55
|
+
select = ["E", "F", "W", "I"]
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""Bookmark data structure for hierarchical PDF outline entries."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Bookmark:
|
|
7
|
+
"""Represents a single PDF outline (bookmark) entry with optional children."""
|
|
8
|
+
|
|
9
|
+
__slots__ = ("_page", "_title", "_children")
|
|
10
|
+
|
|
11
|
+
def __init__(self, page: int, title: str) -> None:
|
|
12
|
+
self._page = page
|
|
13
|
+
self._title = title
|
|
14
|
+
self._children: list[Bookmark] = []
|
|
15
|
+
|
|
16
|
+
@property
|
|
17
|
+
def page(self) -> int:
|
|
18
|
+
return self._page
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def title(self) -> str:
|
|
22
|
+
return self._title
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def children(self) -> list[Bookmark]:
|
|
26
|
+
return self._children
|
|
27
|
+
|
|
28
|
+
def add_child(self, child: Bookmark) -> None:
|
|
29
|
+
if not isinstance(child, Bookmark):
|
|
30
|
+
raise TypeError("child must be a Bookmark instance")
|
|
31
|
+
self._children.append(child)
|
|
32
|
+
|
|
33
|
+
def __repr__(self) -> str:
|
|
34
|
+
return (
|
|
35
|
+
f"Bookmark(page={self._page!r}, title={self._title!r},"
|
|
36
|
+
f" children={len(self._children)})"
|
|
37
|
+
)
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""Command-line interface for nestedpdfmerger."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import logging
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from . import __version__
|
|
11
|
+
from .config import DEFAULT_SORT_MODE, VALID_SORT_MODES
|
|
12
|
+
from .errors import MergeError
|
|
13
|
+
from .merger import merge_pdf_tree
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _setup_logging(verbose: bool, quiet: bool) -> None:
|
|
17
|
+
if quiet:
|
|
18
|
+
level = logging.ERROR
|
|
19
|
+
elif verbose:
|
|
20
|
+
level = logging.DEBUG
|
|
21
|
+
else:
|
|
22
|
+
level = logging.INFO
|
|
23
|
+
logging.basicConfig(level=level, format="%(message)s")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
27
|
+
parser = argparse.ArgumentParser(
|
|
28
|
+
prog="nestedpdfmerger",
|
|
29
|
+
description=(
|
|
30
|
+
"Merge PDFs recursively from a folder tree into a single PDF "
|
|
31
|
+
"with automatic hierarchical bookmarks."
|
|
32
|
+
),
|
|
33
|
+
)
|
|
34
|
+
parser.add_argument(
|
|
35
|
+
"input_dir",
|
|
36
|
+
metavar="INPUT_DIR",
|
|
37
|
+
help="Root directory to scan for PDFs.",
|
|
38
|
+
)
|
|
39
|
+
parser.add_argument(
|
|
40
|
+
"-o",
|
|
41
|
+
"--output",
|
|
42
|
+
metavar="PATH",
|
|
43
|
+
default=None,
|
|
44
|
+
help=(
|
|
45
|
+
"Output PDF file path. "
|
|
46
|
+
"Defaults to <INPUT_DIR>.pdf in the same parent directory."
|
|
47
|
+
),
|
|
48
|
+
)
|
|
49
|
+
parser.add_argument(
|
|
50
|
+
"--sort",
|
|
51
|
+
choices=list(VALID_SORT_MODES),
|
|
52
|
+
default=DEFAULT_SORT_MODE,
|
|
53
|
+
metavar="{" + ",".join(VALID_SORT_MODES) + "}",
|
|
54
|
+
help="Sort mode for files and directories (default: %(default)s).",
|
|
55
|
+
)
|
|
56
|
+
parser.add_argument(
|
|
57
|
+
"--reverse",
|
|
58
|
+
action="store_true",
|
|
59
|
+
help="Reverse the sort order.",
|
|
60
|
+
)
|
|
61
|
+
parser.add_argument(
|
|
62
|
+
"--exclude",
|
|
63
|
+
nargs="+",
|
|
64
|
+
metavar="NAME",
|
|
65
|
+
default=[],
|
|
66
|
+
help="Directory names to exclude (space-separated).",
|
|
67
|
+
)
|
|
68
|
+
parser.add_argument(
|
|
69
|
+
"--exclude-hidden",
|
|
70
|
+
action="store_true",
|
|
71
|
+
help="Exclude hidden directories (names starting with '.').",
|
|
72
|
+
)
|
|
73
|
+
parser.add_argument(
|
|
74
|
+
"--no-bookmarks",
|
|
75
|
+
action="store_true",
|
|
76
|
+
help="Disable hierarchical bookmarks in the output PDF.",
|
|
77
|
+
)
|
|
78
|
+
parser.add_argument(
|
|
79
|
+
"--dry-run",
|
|
80
|
+
action="store_true",
|
|
81
|
+
help="Preview merge order and bookmark structure without writing output.",
|
|
82
|
+
)
|
|
83
|
+
parser.add_argument(
|
|
84
|
+
"--strict",
|
|
85
|
+
action="store_true",
|
|
86
|
+
help="Stop on the first PDF error instead of skipping the file.",
|
|
87
|
+
)
|
|
88
|
+
parser.add_argument(
|
|
89
|
+
"--verbose",
|
|
90
|
+
action="store_true",
|
|
91
|
+
help="Show detailed progress (scanned folders, detected PDFs, etc.).",
|
|
92
|
+
)
|
|
93
|
+
parser.add_argument(
|
|
94
|
+
"--quiet",
|
|
95
|
+
action="store_true",
|
|
96
|
+
help="Suppress all non-error output.",
|
|
97
|
+
)
|
|
98
|
+
parser.add_argument(
|
|
99
|
+
"--version",
|
|
100
|
+
action="version",
|
|
101
|
+
version=f"%(prog)s {__version__}",
|
|
102
|
+
)
|
|
103
|
+
return parser
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def main(argv: list[str] | None = None) -> None:
|
|
107
|
+
parser = build_parser()
|
|
108
|
+
args = parser.parse_args(argv)
|
|
109
|
+
|
|
110
|
+
_setup_logging(args.verbose, args.quiet)
|
|
111
|
+
|
|
112
|
+
output = args.output
|
|
113
|
+
if output is None and not args.dry_run:
|
|
114
|
+
output = str(Path(args.input_dir).resolve().with_suffix(".pdf"))
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
merge_pdf_tree(
|
|
118
|
+
input_dir=args.input_dir,
|
|
119
|
+
output_file=output,
|
|
120
|
+
sort_mode=args.sort,
|
|
121
|
+
reverse=args.reverse,
|
|
122
|
+
exclude=args.exclude,
|
|
123
|
+
bookmarks=not args.no_bookmarks,
|
|
124
|
+
exclude_hidden=args.exclude_hidden,
|
|
125
|
+
dry_run=args.dry_run,
|
|
126
|
+
strict=args.strict,
|
|
127
|
+
verbose=args.verbose,
|
|
128
|
+
quiet=args.quiet,
|
|
129
|
+
)
|
|
130
|
+
except (MergeError, ValueError) as exc:
|
|
131
|
+
print(f"Error: {exc}", file=sys.stderr)
|
|
132
|
+
sys.exit(1)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Custom exceptions for nestedpdfmerger."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class MergeError(Exception):
|
|
5
|
+
"""Raised in strict mode when a PDF cannot be processed."""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class EncryptedPDFError(MergeError):
|
|
9
|
+
"""Raised when a PDF file is encrypted and cannot be merged."""
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class CorruptedPDFError(MergeError):
|
|
13
|
+
"""Raised when a PDF file is corrupted and cannot be read."""
|