pdfxz 0.2.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.
- pdfxz-0.2.0/CHANGELOG.md +62 -0
- pdfxz-0.2.0/LICENSE +21 -0
- pdfxz-0.2.0/PKG-INFO +237 -0
- pdfxz-0.2.0/README.md +209 -0
- pdfxz-0.2.0/pyproject.toml +55 -0
- pdfxz-0.2.0/src/pdfxz/__init__.py +7 -0
- pdfxz-0.2.0/src/pdfxz/__main__.py +6 -0
- pdfxz-0.2.0/src/pdfxz/app.py +629 -0
- pdfxz-0.2.0/src/pdfxz/app.tcss +177 -0
- pdfxz-0.2.0/src/pdfxz/cli.py +284 -0
- pdfxz-0.2.0/src/pdfxz/compressor.py +299 -0
- pdfxz-0.2.0/src/pdfxz/formatting.py +95 -0
- pdfxz-0.2.0/src/pdfxz/models.py +156 -0
- pdfxz-0.2.0/src/pdfxz/profiles.py +138 -0
- pdfxz-0.2.0/src/pdfxz/scanner.py +82 -0
- pdfxz-0.2.0/src/pdfxz/utils.py +200 -0
- pdfxz-0.2.0/src/pdfxz/workers.py +108 -0
- pdfxz-0.2.0/tests/conftest.py +58 -0
- pdfxz-0.2.0/tests/fixtures/fake_gs.py +58 -0
- pdfxz-0.2.0/tests/test_app.py +136 -0
- pdfxz-0.2.0/tests/test_cli.py +164 -0
- pdfxz-0.2.0/tests/test_compressor.py +185 -0
- pdfxz-0.2.0/tests/test_formatting.py +135 -0
- pdfxz-0.2.0/tests/test_profiles.py +93 -0
- pdfxz-0.2.0/tests/test_scanner.py +110 -0
- pdfxz-0.2.0/tests/test_utils.py +66 -0
pdfxz-0.2.0/CHANGELOG.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented in this file.
|
|
4
|
+
|
|
5
|
+
## [0.2.0]
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
- TUI redesigned to be more compact and minimal: smaller fields/buttons, a
|
|
9
|
+
larger/bolder title, and a volume-control-style slider.
|
|
10
|
+
- TUI's Input/Output fields now have a **Browse…** button that opens a
|
|
11
|
+
built-in folder/file picker (`textual.widgets.DirectoryTree`-based), so
|
|
12
|
+
typing a full path is no longer required.
|
|
13
|
+
- TUI dropped the Recursive / Create-missing-directories / Overwrite
|
|
14
|
+
checkboxes in favor of automatic, safe defaults: directories are always
|
|
15
|
+
scanned recursively, missing output directories are always created, and
|
|
16
|
+
an existing output file is never overwritten - a uniquely-named sibling
|
|
17
|
+
file is written instead (`utils.unique_output_path`). The CLI's
|
|
18
|
+
`-r/--recursive`, `-p/--parents`, and `--overwrite` flags are unchanged.
|
|
19
|
+
- TUI file sizes are now always shown in MB (`formatting.format_mb`)
|
|
20
|
+
instead of switching units; the CLI's multi-unit report is unchanged.
|
|
21
|
+
- TUI filenames are now truncated to a fixed width with an ellipsis
|
|
22
|
+
(`formatting.truncate_filename`); the full path is shown when a report
|
|
23
|
+
table row is highlighted, since `DataTable` has no per-cell tooltip.
|
|
24
|
+
- Removed the built-in "Screenshot" command from the TUI's command
|
|
25
|
+
palette (`PDFXZApp.get_system_commands`); all other default commands
|
|
26
|
+
(Quit, Theme, etc.) are unchanged.
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
- Test collection failure (`ModuleNotFoundError: No module named 'tests'`)
|
|
30
|
+
when running the suite via the bare `pytest` command instead of
|
|
31
|
+
`python -m pytest`. Root cause: `tests/` has no `__init__.py`, so
|
|
32
|
+
`tests.conftest` was only importable when the project root happened to
|
|
33
|
+
already be on `sys.path`. Fixed by importing `conftest` directly, which
|
|
34
|
+
pytest's own rootless-test-directory handling already supports
|
|
35
|
+
regardless of invocation method or working directory.
|
|
36
|
+
- The processing screen's `Ctrl+C` cancel shortcut never actually fired:
|
|
37
|
+
Textual's `App` binds `ctrl+c` to its own `help_quit` action with
|
|
38
|
+
`priority=True`, which silently pre-empts any Screen-level binding for
|
|
39
|
+
the same key. `Escape` was the only shortcut that ever worked; `Ctrl+C`
|
|
40
|
+
is no longer advertised as a cancel shortcut since it wasn't one.
|
|
41
|
+
|
|
42
|
+
## [0.1.0] - Initial release
|
|
43
|
+
|
|
44
|
+
### Added
|
|
45
|
+
- Interactive Textual TUI: configuration screen, live processing screen with
|
|
46
|
+
cancellable progress, and a scrollable final report screen.
|
|
47
|
+
- Non-interactive CLI supporting single-file and directory (batch) modes,
|
|
48
|
+
with `-o/--output`, `-p/--parents`, `-q/--quality`, `-r/--recursive`,
|
|
49
|
+
`--overwrite`, `--no-tui`, `--debug`, `--version`.
|
|
50
|
+
- Five explicit compression profiles (`maximum`, `high`, `balanced`,
|
|
51
|
+
`strong`, `maximum-compression`) built on documented Ghostscript
|
|
52
|
+
image-downsampling settings rather than the `/screen`-style presets.
|
|
53
|
+
- Ghostscript discovery across Linux, macOS, and Windows, including
|
|
54
|
+
`PDFXZ_GS_PATH` override support.
|
|
55
|
+
- PDF signature (`%PDF-`) validation independent of file extension.
|
|
56
|
+
- Collision-safe batch output planning that preserves relative directory
|
|
57
|
+
structure for recursive scans.
|
|
58
|
+
- Automatic partial-output cleanup on failure or cancellation; the
|
|
59
|
+
original input file is never modified or deleted.
|
|
60
|
+
- Full pytest suite (scanner, compressor, profiles, formatting, CLI, and a
|
|
61
|
+
TUI smoke test) that mocks Ghostscript and never requires it to be
|
|
62
|
+
installed.
|
pdfxz-0.2.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 pdfxz 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.
|
pdfxz-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: pdfxz
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: A terminal UI and CLI for compressing PDF files with Ghostscript.
|
|
5
|
+
Project-URL: Homepage, https://github.com/pythyn/pdfxz
|
|
6
|
+
Project-URL: Issues, https://github.com/pythyn/pdfxz/issues
|
|
7
|
+
Author: pdfxz contributors
|
|
8
|
+
License: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: cli,compression,ghostscript,pdf,textual,tui
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Environment :: Console :: Curses
|
|
13
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Programming Language :: Python :: 3
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Topic :: Multimedia :: Graphics :: Graphics Conversion
|
|
21
|
+
Classifier: Topic :: Utilities
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Requires-Dist: textual>=0.58.0
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
26
|
+
Requires-Dist: pytest>=7.4; extra == 'dev'
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# pdfxz
|
|
30
|
+
|
|
31
|
+
A terminal UI and CLI for compressing PDF files with [Ghostscript](https://www.ghostscript.com/).
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
┌───────────────────────────────────────────────────────────────┐
|
|
35
|
+
│ PDFXZ │
|
|
36
|
+
│ PDF Compression Utility │
|
|
37
|
+
├───────────────────────────────────────────────────────────────┤
|
|
38
|
+
│ INPUT (a PDF file or a directory) │
|
|
39
|
+
│ [ ~/Documents/papers ] │
|
|
40
|
+
│ │
|
|
41
|
+
│ OUTPUT (optional - a sensible default is used if left blank) │
|
|
42
|
+
│ [ ~/Documents/compressed ] │
|
|
43
|
+
│ │
|
|
44
|
+
│ QUALITY │
|
|
45
|
+
│ [ Balanced ▼ ] │
|
|
46
|
+
│ │
|
|
47
|
+
│ │
|
|
48
|
+
│ │
|
|
49
|
+
│ [ Compress PDFs ] │
|
|
50
|
+
├───────────────────────────────────────────────────────────────┤
|
|
51
|
+
│ Status: Ready │
|
|
52
|
+
└───────────────────────────────────────────────────────────────┘
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Features
|
|
56
|
+
|
|
57
|
+
- **Interactive TUI** for point-and-shoot use - no CLI syntax to memorize.
|
|
58
|
+
- **Non-interactive CLI** for scripts, cron jobs, and CI.
|
|
59
|
+
- **Five explicit compression profiles**, from archival quality to maximum size reduction.
|
|
60
|
+
- **Batch directory processing** with optional recursion, one bad file never aborts the batch.
|
|
61
|
+
- **Live, honest progress** - per-file counts and results, no fabricated percentages.
|
|
62
|
+
- **Cancellable** mid-batch, with automatic cleanup of partial output.
|
|
63
|
+
- **Data-safe by default**: never overwrites the input, never silently clobbers an existing output.
|
|
64
|
+
- **Cross-platform**: Linux, macOS, and Windows, wherever Python and Ghostscript are available.
|
|
65
|
+
|
|
66
|
+
## Installation
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
pip install pdfxz
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
or, from a local checkout:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
pip install .
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Ghostscript requirement
|
|
79
|
+
|
|
80
|
+
pdfxz compresses PDFs *using* Ghostscript, but does not bundle it. Install Ghostscript separately and make sure it's on your `PATH`:
|
|
81
|
+
|
|
82
|
+
| Platform | Command |
|
|
83
|
+
| --- | --- |
|
|
84
|
+
| Ubuntu / Debian | `sudo apt install ghostscript` |
|
|
85
|
+
| Fedora | `sudo dnf install ghostscript` |
|
|
86
|
+
| Arch Linux | `sudo pacman -S ghostscript` |
|
|
87
|
+
| macOS (Homebrew) | `brew install ghostscript` |
|
|
88
|
+
| Windows | Download the installer from [ghostscript.com](https://www.ghostscript.com/releases/gsdnld.html) |
|
|
89
|
+
|
|
90
|
+
On Windows, pdfxz looks for `gswin64c`, `gswin32c`, or `gs` on `PATH` (the console builds, not the GUI `gswin64.exe`). You can also point pdfxz at a specific binary with the `PDFXZ_GS_PATH` environment variable.
|
|
91
|
+
|
|
92
|
+
If Ghostscript isn't found, pdfxz tells you clearly instead of failing with a cryptic error.
|
|
93
|
+
|
|
94
|
+
## Usage
|
|
95
|
+
|
|
96
|
+
### TUI
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
pdfxz
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Pick an input PDF or folder and, optionally, a destination folder - either by typing a path or using the **Browse…** buttons, which open a built-in folder/file browser. Drag the **Quality** slider (arrow keys or click) to choose a compression profile. Directories are always scanned recursively, missing output folders are created automatically, and an existing output is never overwritten - a uniquely-named file (`report_compressed (2).pdf`, etc.) is written alongside it instead, so there's nothing to configure and nothing you can accidentally clobber.
|
|
103
|
+
|
|
104
|
+
Keyboard shortcuts (also shown at the bottom of the screen): `Esc` cancels a running batch; `q` quits (when a text field isn't focused); on the report screen, `n` starts a new batch. Long filenames are truncated in the UI - highlight a row in the report table to see the full path underneath it.
|
|
105
|
+
|
|
106
|
+
### CLI
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
pdfxz paper.pdf # -> paper_compressed.pdf
|
|
110
|
+
pdfxz input.pdf output.pdf # explicit output (positional)
|
|
111
|
+
pdfxz input.pdf -o output.pdf # explicit output (flag)
|
|
112
|
+
pdfxz ~/papers -o ~/compressed # compress a whole directory
|
|
113
|
+
pdfxz ~/papers -o ~/compressed --quality balanced
|
|
114
|
+
pdfxz ~/papers -o ~/compressed -q strong -r # recurse into subdirectories
|
|
115
|
+
pdfxz input.pdf -o ~/new/path/out.pdf -p # create missing output dirs
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
```
|
|
119
|
+
usage: pdfxz [-h] [-o PATH] [-p] [-q LEVEL] [-r] [--overwrite] [--no-tui]
|
|
120
|
+
[--debug] [--version]
|
|
121
|
+
[INPUT] [OUTPUT]
|
|
122
|
+
|
|
123
|
+
positional arguments:
|
|
124
|
+
INPUT A PDF file or a directory containing PDFs.
|
|
125
|
+
OUTPUT Output PDF file or directory (positional form).
|
|
126
|
+
|
|
127
|
+
options:
|
|
128
|
+
-h, --help show this help message and exit
|
|
129
|
+
-o PATH, --output PATH
|
|
130
|
+
Output PDF file or directory.
|
|
131
|
+
-p, --parents Create missing output directories (like 'mkdir -p').
|
|
132
|
+
-q LEVEL, --quality LEVEL
|
|
133
|
+
Compression profile: balanced, high, maximum,
|
|
134
|
+
maximum-compression, strong (default: balanced).
|
|
135
|
+
-r, --recursive Recurse into subdirectories when INPUT is a directory
|
|
136
|
+
(default: off, top level only).
|
|
137
|
+
--overwrite Allow overwriting an existing output file.
|
|
138
|
+
--no-tui Never launch the interactive TUI; exit with an error
|
|
139
|
+
if INPUT is missing instead of prompting.
|
|
140
|
+
--debug Enable verbose debug logging.
|
|
141
|
+
--version show program's version number and exit
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Running `pdfxz` with no arguments launches the TUI. Passing `INPUT` runs non-interactively - the TUI never blocks a script. `--no-tui` is a safety net for automation: if a script accidentally omits `INPUT`, pdfxz fails fast with a clear error instead of trying to open an interactive terminal.
|
|
145
|
+
|
|
146
|
+
Exit codes: `0` on full success, `1` if any file failed, was skipped as invalid, or was cancelled, `2` on a usage error, `130` on Ctrl+C.
|
|
147
|
+
|
|
148
|
+
## Compression profiles
|
|
149
|
+
|
|
150
|
+
Ghostscript's built-in `/screen`, `/ebook`, `/printer`, `/prepress` presets aren't used directly - their exact behavior has varied across Ghostscript versions. Instead, pdfxz defines five explicit, documented profiles built around image downsampling:
|
|
151
|
+
|
|
152
|
+
| Level | Color/Gray DPI | Mono DPI | Goal |
|
|
153
|
+
| --- | --- | --- | --- |
|
|
154
|
+
| Maximum Quality | 300 | 1200 | Minimal quality loss; archival copies |
|
|
155
|
+
| High Quality | 200 | 600 | Good visual quality, moderate compression |
|
|
156
|
+
| Balanced *(default)* | 150 | 300 | General-purpose default |
|
|
157
|
+
| Strong Compression | 120 | 200 | Smaller files, visible trade-off |
|
|
158
|
+
| Maximum Compression | 72 | 150 | Prioritizes file size |
|
|
159
|
+
|
|
160
|
+
No profile guarantees a specific percentage reduction - actual results depend heavily on the PDF's contents (image-heavy scans compress far more than text-only documents).
|
|
161
|
+
|
|
162
|
+
## Batch processing & output naming
|
|
163
|
+
|
|
164
|
+
- **Single file**: `paper.pdf` -> `paper_compressed.pdf` by default, or your chosen output path.
|
|
165
|
+
- **Directory**: each discovered PDF gets a `..._compressed.pdf` sibling under the output directory.
|
|
166
|
+
- **Recursive scans**: pdfxz preserves the input's relative directory structure under the output directory, so `papers/a/report.pdf` and `papers/b/report.pdf` never collide or overwrite each other.
|
|
167
|
+
- One failed or invalid file never aborts the batch - it's recorded and processing continues.
|
|
168
|
+
|
|
169
|
+
## Output & overwrite behavior
|
|
170
|
+
|
|
171
|
+
- pdfxz never overwrites the input file, and rejects any operation where input and output resolve to the same file.
|
|
172
|
+
- An existing output file is left untouched unless you pass `--overwrite` (CLI) or check "Overwrite existing output" (TUI).
|
|
173
|
+
- Missing output directories are only created if you pass `-p/--parents` (CLI) or leave "Create missing directories" checked (TUI, on by default).
|
|
174
|
+
- If Ghostscript fails or is cancelled partway through, any partial output file is deleted; the input is never touched.
|
|
175
|
+
|
|
176
|
+
## Defaults
|
|
177
|
+
|
|
178
|
+
| Setting | Default |
|
|
179
|
+
| --- | --- |
|
|
180
|
+
| Quality | `balanced` |
|
|
181
|
+
| Recursive | off (top-level only) |
|
|
182
|
+
| Create missing directories | on (TUI) / off (CLI, use `-p`) |
|
|
183
|
+
| Overwrite | off |
|
|
184
|
+
|
|
185
|
+
## Troubleshooting
|
|
186
|
+
|
|
187
|
+
**"Ghostscript is not installed"** - install it and confirm `gs --version` (or `gswin64c --version` on Windows) works in your terminal.
|
|
188
|
+
|
|
189
|
+
**"Output file already exists"** - pass `--overwrite`, or choose a different output path.
|
|
190
|
+
|
|
191
|
+
**"Output path is the same as the input path"** - choose a distinct output location; pdfxz refuses to overwrite the source PDF.
|
|
192
|
+
|
|
193
|
+
**"No PDF files found"** - check that files actually end in `.pdf` (case-insensitive) and, for nested folders, that you passed `-r/--recursive`.
|
|
194
|
+
|
|
195
|
+
**A file is reported invalid** - pdfxz checks the `%PDF-` signature, not just the extension; the file may be corrupt or not actually a PDF.
|
|
196
|
+
|
|
197
|
+
## Development
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
git clone https://github.com/example/pdfxz
|
|
201
|
+
cd pdfxz
|
|
202
|
+
python -m venv .venv
|
|
203
|
+
source .venv/bin/activate # .venv\Scripts\activate on Windows
|
|
204
|
+
pip install -e ".[dev]"
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### Project layout
|
|
208
|
+
|
|
209
|
+
```
|
|
210
|
+
src/pdfxz/
|
|
211
|
+
├── __init__.py # package version
|
|
212
|
+
├── __main__.py # `python -m pdfxz` entry point
|
|
213
|
+
├── cli.py # argument parsing + non-interactive execution
|
|
214
|
+
├── app.py # Textual TUI (config / processing / report screens)
|
|
215
|
+
├── app.tcss # TUI stylesheet
|
|
216
|
+
├── compressor.py # Ghostscript subprocess integration
|
|
217
|
+
├── workers.py # framework-agnostic batch orchestration (shared by CLI + TUI)
|
|
218
|
+
├── scanner.py # PDF discovery + %PDF- signature validation
|
|
219
|
+
├── models.py # typed result/state dataclasses
|
|
220
|
+
├── profiles.py # the five compression profiles
|
|
221
|
+
├── formatting.py # size/percentage/ratio/duration helpers
|
|
222
|
+
└── utils.py # Ghostscript discovery, path safety, output planning
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
The compressor has no knowledge of the TUI or CLI; the scanner has no knowledge of Ghostscript; the TUI and CLI both drive the same `BatchRunner`. This keeps each layer independently testable.
|
|
226
|
+
|
|
227
|
+
### Testing
|
|
228
|
+
|
|
229
|
+
```bash
|
|
230
|
+
pytest
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
The test suite mocks Ghostscript with a small fake executable (`tests/fixtures/fake_gs.py`), so it never requires a real Ghostscript installation. It covers the scanner, compressor (including cancellation and cleanup), profiles, formatting helpers, the CLI, and a TUI smoke test.
|
|
234
|
+
|
|
235
|
+
## License
|
|
236
|
+
|
|
237
|
+
MIT - see [LICENSE](LICENSE).
|
pdfxz-0.2.0/README.md
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
# pdfxz
|
|
2
|
+
|
|
3
|
+
A terminal UI and CLI for compressing PDF files with [Ghostscript](https://www.ghostscript.com/).
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
┌───────────────────────────────────────────────────────────────┐
|
|
7
|
+
│ PDFXZ │
|
|
8
|
+
│ PDF Compression Utility │
|
|
9
|
+
├───────────────────────────────────────────────────────────────┤
|
|
10
|
+
│ INPUT (a PDF file or a directory) │
|
|
11
|
+
│ [ ~/Documents/papers ] │
|
|
12
|
+
│ │
|
|
13
|
+
│ OUTPUT (optional - a sensible default is used if left blank) │
|
|
14
|
+
│ [ ~/Documents/compressed ] │
|
|
15
|
+
│ │
|
|
16
|
+
│ QUALITY │
|
|
17
|
+
│ [ Balanced ▼ ] │
|
|
18
|
+
│ │
|
|
19
|
+
│ │
|
|
20
|
+
│ │
|
|
21
|
+
│ [ Compress PDFs ] │
|
|
22
|
+
├───────────────────────────────────────────────────────────────┤
|
|
23
|
+
│ Status: Ready │
|
|
24
|
+
└───────────────────────────────────────────────────────────────┘
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Features
|
|
28
|
+
|
|
29
|
+
- **Interactive TUI** for point-and-shoot use - no CLI syntax to memorize.
|
|
30
|
+
- **Non-interactive CLI** for scripts, cron jobs, and CI.
|
|
31
|
+
- **Five explicit compression profiles**, from archival quality to maximum size reduction.
|
|
32
|
+
- **Batch directory processing** with optional recursion, one bad file never aborts the batch.
|
|
33
|
+
- **Live, honest progress** - per-file counts and results, no fabricated percentages.
|
|
34
|
+
- **Cancellable** mid-batch, with automatic cleanup of partial output.
|
|
35
|
+
- **Data-safe by default**: never overwrites the input, never silently clobbers an existing output.
|
|
36
|
+
- **Cross-platform**: Linux, macOS, and Windows, wherever Python and Ghostscript are available.
|
|
37
|
+
|
|
38
|
+
## Installation
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install pdfxz
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
or, from a local checkout:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pip install .
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Ghostscript requirement
|
|
51
|
+
|
|
52
|
+
pdfxz compresses PDFs *using* Ghostscript, but does not bundle it. Install Ghostscript separately and make sure it's on your `PATH`:
|
|
53
|
+
|
|
54
|
+
| Platform | Command |
|
|
55
|
+
| --- | --- |
|
|
56
|
+
| Ubuntu / Debian | `sudo apt install ghostscript` |
|
|
57
|
+
| Fedora | `sudo dnf install ghostscript` |
|
|
58
|
+
| Arch Linux | `sudo pacman -S ghostscript` |
|
|
59
|
+
| macOS (Homebrew) | `brew install ghostscript` |
|
|
60
|
+
| Windows | Download the installer from [ghostscript.com](https://www.ghostscript.com/releases/gsdnld.html) |
|
|
61
|
+
|
|
62
|
+
On Windows, pdfxz looks for `gswin64c`, `gswin32c`, or `gs` on `PATH` (the console builds, not the GUI `gswin64.exe`). You can also point pdfxz at a specific binary with the `PDFXZ_GS_PATH` environment variable.
|
|
63
|
+
|
|
64
|
+
If Ghostscript isn't found, pdfxz tells you clearly instead of failing with a cryptic error.
|
|
65
|
+
|
|
66
|
+
## Usage
|
|
67
|
+
|
|
68
|
+
### TUI
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
pdfxz
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Pick an input PDF or folder and, optionally, a destination folder - either by typing a path or using the **Browse…** buttons, which open a built-in folder/file browser. Drag the **Quality** slider (arrow keys or click) to choose a compression profile. Directories are always scanned recursively, missing output folders are created automatically, and an existing output is never overwritten - a uniquely-named file (`report_compressed (2).pdf`, etc.) is written alongside it instead, so there's nothing to configure and nothing you can accidentally clobber.
|
|
75
|
+
|
|
76
|
+
Keyboard shortcuts (also shown at the bottom of the screen): `Esc` cancels a running batch; `q` quits (when a text field isn't focused); on the report screen, `n` starts a new batch. Long filenames are truncated in the UI - highlight a row in the report table to see the full path underneath it.
|
|
77
|
+
|
|
78
|
+
### CLI
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
pdfxz paper.pdf # -> paper_compressed.pdf
|
|
82
|
+
pdfxz input.pdf output.pdf # explicit output (positional)
|
|
83
|
+
pdfxz input.pdf -o output.pdf # explicit output (flag)
|
|
84
|
+
pdfxz ~/papers -o ~/compressed # compress a whole directory
|
|
85
|
+
pdfxz ~/papers -o ~/compressed --quality balanced
|
|
86
|
+
pdfxz ~/papers -o ~/compressed -q strong -r # recurse into subdirectories
|
|
87
|
+
pdfxz input.pdf -o ~/new/path/out.pdf -p # create missing output dirs
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
usage: pdfxz [-h] [-o PATH] [-p] [-q LEVEL] [-r] [--overwrite] [--no-tui]
|
|
92
|
+
[--debug] [--version]
|
|
93
|
+
[INPUT] [OUTPUT]
|
|
94
|
+
|
|
95
|
+
positional arguments:
|
|
96
|
+
INPUT A PDF file or a directory containing PDFs.
|
|
97
|
+
OUTPUT Output PDF file or directory (positional form).
|
|
98
|
+
|
|
99
|
+
options:
|
|
100
|
+
-h, --help show this help message and exit
|
|
101
|
+
-o PATH, --output PATH
|
|
102
|
+
Output PDF file or directory.
|
|
103
|
+
-p, --parents Create missing output directories (like 'mkdir -p').
|
|
104
|
+
-q LEVEL, --quality LEVEL
|
|
105
|
+
Compression profile: balanced, high, maximum,
|
|
106
|
+
maximum-compression, strong (default: balanced).
|
|
107
|
+
-r, --recursive Recurse into subdirectories when INPUT is a directory
|
|
108
|
+
(default: off, top level only).
|
|
109
|
+
--overwrite Allow overwriting an existing output file.
|
|
110
|
+
--no-tui Never launch the interactive TUI; exit with an error
|
|
111
|
+
if INPUT is missing instead of prompting.
|
|
112
|
+
--debug Enable verbose debug logging.
|
|
113
|
+
--version show program's version number and exit
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Running `pdfxz` with no arguments launches the TUI. Passing `INPUT` runs non-interactively - the TUI never blocks a script. `--no-tui` is a safety net for automation: if a script accidentally omits `INPUT`, pdfxz fails fast with a clear error instead of trying to open an interactive terminal.
|
|
117
|
+
|
|
118
|
+
Exit codes: `0` on full success, `1` if any file failed, was skipped as invalid, or was cancelled, `2` on a usage error, `130` on Ctrl+C.
|
|
119
|
+
|
|
120
|
+
## Compression profiles
|
|
121
|
+
|
|
122
|
+
Ghostscript's built-in `/screen`, `/ebook`, `/printer`, `/prepress` presets aren't used directly - their exact behavior has varied across Ghostscript versions. Instead, pdfxz defines five explicit, documented profiles built around image downsampling:
|
|
123
|
+
|
|
124
|
+
| Level | Color/Gray DPI | Mono DPI | Goal |
|
|
125
|
+
| --- | --- | --- | --- |
|
|
126
|
+
| Maximum Quality | 300 | 1200 | Minimal quality loss; archival copies |
|
|
127
|
+
| High Quality | 200 | 600 | Good visual quality, moderate compression |
|
|
128
|
+
| Balanced *(default)* | 150 | 300 | General-purpose default |
|
|
129
|
+
| Strong Compression | 120 | 200 | Smaller files, visible trade-off |
|
|
130
|
+
| Maximum Compression | 72 | 150 | Prioritizes file size |
|
|
131
|
+
|
|
132
|
+
No profile guarantees a specific percentage reduction - actual results depend heavily on the PDF's contents (image-heavy scans compress far more than text-only documents).
|
|
133
|
+
|
|
134
|
+
## Batch processing & output naming
|
|
135
|
+
|
|
136
|
+
- **Single file**: `paper.pdf` -> `paper_compressed.pdf` by default, or your chosen output path.
|
|
137
|
+
- **Directory**: each discovered PDF gets a `..._compressed.pdf` sibling under the output directory.
|
|
138
|
+
- **Recursive scans**: pdfxz preserves the input's relative directory structure under the output directory, so `papers/a/report.pdf` and `papers/b/report.pdf` never collide or overwrite each other.
|
|
139
|
+
- One failed or invalid file never aborts the batch - it's recorded and processing continues.
|
|
140
|
+
|
|
141
|
+
## Output & overwrite behavior
|
|
142
|
+
|
|
143
|
+
- pdfxz never overwrites the input file, and rejects any operation where input and output resolve to the same file.
|
|
144
|
+
- An existing output file is left untouched unless you pass `--overwrite` (CLI) or check "Overwrite existing output" (TUI).
|
|
145
|
+
- Missing output directories are only created if you pass `-p/--parents` (CLI) or leave "Create missing directories" checked (TUI, on by default).
|
|
146
|
+
- If Ghostscript fails or is cancelled partway through, any partial output file is deleted; the input is never touched.
|
|
147
|
+
|
|
148
|
+
## Defaults
|
|
149
|
+
|
|
150
|
+
| Setting | Default |
|
|
151
|
+
| --- | --- |
|
|
152
|
+
| Quality | `balanced` |
|
|
153
|
+
| Recursive | off (top-level only) |
|
|
154
|
+
| Create missing directories | on (TUI) / off (CLI, use `-p`) |
|
|
155
|
+
| Overwrite | off |
|
|
156
|
+
|
|
157
|
+
## Troubleshooting
|
|
158
|
+
|
|
159
|
+
**"Ghostscript is not installed"** - install it and confirm `gs --version` (or `gswin64c --version` on Windows) works in your terminal.
|
|
160
|
+
|
|
161
|
+
**"Output file already exists"** - pass `--overwrite`, or choose a different output path.
|
|
162
|
+
|
|
163
|
+
**"Output path is the same as the input path"** - choose a distinct output location; pdfxz refuses to overwrite the source PDF.
|
|
164
|
+
|
|
165
|
+
**"No PDF files found"** - check that files actually end in `.pdf` (case-insensitive) and, for nested folders, that you passed `-r/--recursive`.
|
|
166
|
+
|
|
167
|
+
**A file is reported invalid** - pdfxz checks the `%PDF-` signature, not just the extension; the file may be corrupt or not actually a PDF.
|
|
168
|
+
|
|
169
|
+
## Development
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
git clone https://github.com/example/pdfxz
|
|
173
|
+
cd pdfxz
|
|
174
|
+
python -m venv .venv
|
|
175
|
+
source .venv/bin/activate # .venv\Scripts\activate on Windows
|
|
176
|
+
pip install -e ".[dev]"
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Project layout
|
|
180
|
+
|
|
181
|
+
```
|
|
182
|
+
src/pdfxz/
|
|
183
|
+
├── __init__.py # package version
|
|
184
|
+
├── __main__.py # `python -m pdfxz` entry point
|
|
185
|
+
├── cli.py # argument parsing + non-interactive execution
|
|
186
|
+
├── app.py # Textual TUI (config / processing / report screens)
|
|
187
|
+
├── app.tcss # TUI stylesheet
|
|
188
|
+
├── compressor.py # Ghostscript subprocess integration
|
|
189
|
+
├── workers.py # framework-agnostic batch orchestration (shared by CLI + TUI)
|
|
190
|
+
├── scanner.py # PDF discovery + %PDF- signature validation
|
|
191
|
+
├── models.py # typed result/state dataclasses
|
|
192
|
+
├── profiles.py # the five compression profiles
|
|
193
|
+
├── formatting.py # size/percentage/ratio/duration helpers
|
|
194
|
+
└── utils.py # Ghostscript discovery, path safety, output planning
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
The compressor has no knowledge of the TUI or CLI; the scanner has no knowledge of Ghostscript; the TUI and CLI both drive the same `BatchRunner`. This keeps each layer independently testable.
|
|
198
|
+
|
|
199
|
+
### Testing
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
pytest
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
The test suite mocks Ghostscript with a small fake executable (`tests/fixtures/fake_gs.py`), so it never requires a real Ghostscript installation. It covers the scanner, compressor (including cancellation and cleanup), profiles, formatting helpers, the CLI, and a TUI smoke test.
|
|
206
|
+
|
|
207
|
+
## License
|
|
208
|
+
|
|
209
|
+
MIT - see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pdfxz"
|
|
7
|
+
version = "0.2.0"
|
|
8
|
+
description = "A terminal UI and CLI for compressing PDF files with Ghostscript."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "pdfxz contributors" }]
|
|
13
|
+
keywords = ["pdf", "compression", "ghostscript", "tui", "cli", "textual"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"Programming Language :: Python :: 3.10",
|
|
17
|
+
"Programming Language :: Python :: 3.11",
|
|
18
|
+
"Programming Language :: Python :: 3.12",
|
|
19
|
+
"Environment :: Console",
|
|
20
|
+
"Environment :: Console :: Curses",
|
|
21
|
+
"Intended Audience :: End Users/Desktop",
|
|
22
|
+
"Topic :: Utilities",
|
|
23
|
+
"Topic :: Multimedia :: Graphics :: Graphics Conversion",
|
|
24
|
+
"License :: OSI Approved :: MIT License",
|
|
25
|
+
"Operating System :: OS Independent",
|
|
26
|
+
]
|
|
27
|
+
dependencies = [
|
|
28
|
+
"textual>=0.58.0",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[project.optional-dependencies]
|
|
32
|
+
dev = [
|
|
33
|
+
"pytest>=7.4",
|
|
34
|
+
"pytest-asyncio>=0.23",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
[project.scripts]
|
|
38
|
+
pdfxz = "pdfxz.cli:main"
|
|
39
|
+
|
|
40
|
+
[project.urls]
|
|
41
|
+
Homepage = "https://github.com/pythyn/pdfxz"
|
|
42
|
+
Issues = "https://github.com/pythyn/pdfxz/issues"
|
|
43
|
+
|
|
44
|
+
[tool.hatch.build.targets.wheel]
|
|
45
|
+
packages = ["src/pdfxz"]
|
|
46
|
+
|
|
47
|
+
[tool.hatch.build.targets.wheel.force-include]
|
|
48
|
+
"src/pdfxz/app.tcss" = "pdfxz/app.tcss"
|
|
49
|
+
|
|
50
|
+
[tool.hatch.build.targets.sdist]
|
|
51
|
+
include = ["src/pdfxz", "tests", "README.md", "LICENSE", "CHANGELOG.md"]
|
|
52
|
+
|
|
53
|
+
[tool.pytest.ini_options]
|
|
54
|
+
testpaths = ["tests"]
|
|
55
|
+
asyncio_mode = "auto"
|