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.
@@ -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"
@@ -0,0 +1,7 @@
1
+ """pdfxz - a terminal UI and CLI for compressing PDF files with Ghostscript."""
2
+
3
+ from __future__ import annotations
4
+
5
+ __version__ = "0.2.0"
6
+
7
+ __all__ = ["__version__"]
@@ -0,0 +1,6 @@
1
+ from __future__ import annotations
2
+
3
+ from .cli import main
4
+
5
+ if __name__ == "__main__":
6
+ raise SystemExit(main())