Metadata-Version: 2.5
Name: pdfsqueeze
Version: 1.0.3
Summary: Reduce PDF file size by targeting raster images, without any external engine.
Project-URL: Homepage, https://github.com/nyxko-dev/pdfsqueeze
Project-URL: Author, https://github.com/nyxko-dev
Author: Nyxko
License: MIT License
        
        Copyright (c) 2026 Nyxko
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
License-File: LICENSE
Keywords: compression,images,pdf,pikepdf,pillow
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Console
Classifier: Intended Audience :: End Users/Desktop
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Utilities
Requires-Python: >=3.11
Requires-Dist: pikepdf<11,>=9.0
Requires-Dist: pillow<13,>=10.3
Provides-Extra: dev
Requires-Dist: mypy>=1.11; extra == 'dev'
Requires-Dist: numpy>=1.26; extra == 'dev'
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: ruff>=0.6; extra == 'dev'
Requires-Dist: scipy>=1.11; extra == 'dev'
Description-Content-Type: text/markdown

# pdfsqueeze

Shrink PDF files by targeting raster images — **no external engine**.

`pdfsqueeze` relies solely on [`pikepdf`](https://pikepdf.readthedocs.io) to read and rewrite the
document, and on [`Pillow`](https://pillow.readthedocs.io) to decode, transform and encode images.
No other runtime dependency.

The guiding rule is **document safety over size**: anything uncertain means the original is kept.
Nothing is published that has not been validated, and the source is never overwritten.

> The reduction is an **estimate, not a guarantee**. It depends on how much of the file is images,
> on the content itself, on filters already applied, and on the document structure.

## Installation

```bash
pip install pdfsqueeze
```

Python 3.11 or later. No system package required.

## Usage

```bash
# Inspect without touching anything
pdfsqueeze --analyze report.pdf
pdfsqueeze --analyze --json report.pdf        # pure JSON on stdout

# Find out what each mode would produce, before committing to one
pdfsqueeze --analyze --sizes report.pdf

# Dry run: what would happen, without writing
pdfsqueeze --dry-run --mode medium report.pdf

# Compress, output next to the source
pdfsqueeze -m medium -y report.pdf             # -> report_compressed.pdf

# Into a directory, with a custom suffix
pdfsqueeze -m light --suffix _web -o ./published report.pdf invoice.pdf

# A whole folder, in parallel
pdfsqueeze -r -m strong -y --jobs 4 ./scans

# Aim for a size cap
pdfsqueeze --target-size 5MB -y report.pdf
```

### Profiles

| Mode | Target DPI | Threshold | JPEG quality | Chroma | Metadata | Measured SSIM |
|---|---:|---:|---:|---|---|---:|
| `light` | 225 | 330 | 85 | `4:4:4` | kept | ≥ 0.99 |
| `medium` | 150 | 180 | 70 | `4:2:0` | kept | ≥ 0.86 |
| `strong` | 96 | 110 | 45 | `4:2:0` | stripped | ≥ 0.81 |

The **threshold** is the effective resolution above which an image becomes a candidate for
downsampling. `light` deliberately sits above the 300 DPI band, so it leaves ordinary office scans
untouched. These values were measured, not assumed.

`medium` and `strong` ask for confirmation before running; `-y` answers once for the whole run.

### Choosing a mode

`--analyze --sizes` reports what every mode would produce for a document:

```text
  Projected output sizes:
  ┌────────┬─────┬─────────┬───────────┬────────┐
  │ Mode   │ DPI │ Quality │ Size      │ Gain   │
  ├────────┼─────┼─────────┼───────────┼────────┤
  │ light  │ 225 │ 85      │ 4.8 MiB   │ +0.1 % │
  │ medium │ 150 │ 70      │ 1.2 MiB   │ 75.5 % │
  │ strong │ 96  │ 45      │ 746.6 KiB │ 84.8 % │
  └────────┴─────┴─────────┴───────────┴────────┘
```

These figures are **not estimates**: each mode is processed and serialised in
memory, through the same steps a real run uses, so the size shown is the size
the command would write. The cost is that of a full compression per mode minus
the disk write — roughly 2 to 4 seconds for a heavy document — which is why it
sits behind an explicit option and `--analyze` alone stays immediate.

`--dpi` and `--quality` apply to every projected mode, which is why the table
spells out the DPI and quality actually used.

### Target size

`--target-size 5MB` is a **cap**, never a size to hit exactly. Accepted forms are `5MB`, `800KB`,
`1.5M` and `500k` (decimal units, case-insensitive).

The selected mode runs first; as soon as an output fits under the cap it is published and the
search stops. Every iteration re-reads the **original source** — a profile is never applied to the
result of the previous one. The search is capped at 6 iterations and never goes below 72 DPI or
quality 30. If the cap stays out of reach, the smallest valid result is published and the command
exits with code 6.

## What it does not do

- It **does not recompress** `JPXDecode`, `CCITTFaxDecode` or `JBIG2Decode` streams — they are
  passed through untouched.
- It **does not convert** CMYK images, and says so.
- It **does not re-encode** 1-bit images, where JPEG would wreck a bitonal scan.
- It **never asks for, stores or guesses a password**. A PDF that requires one is refused (exit
  code 4); a PDF that merely restricts permissions is processed, with its restrictions reproduced
  identically.
- It **never claims to preserve a digital signature**: any rewrite invalidates it, and this is
  reported.
- **DPI estimation (v1)**: the transformation matrix is tracked through `q`/`Q`, `cm` and `Do`,
  nested form XObjects included. Skewed matrices and exotic graphics states yield an approximation.

## Exit codes

| Code | Meaning |
|---:|---|
| 0 | Success, analysis or dry run completed |
| 1 | Unexpected internal error |
| 2 | Invalid arguments, missing confirmation, or missing source |
| 3 | I/O error, path collision |
| 4 | PDF requires a password, or its encryption cannot be reproduced |
| 5 | Corrupt PDF, or the produced document failed validation |
| 6 | `--target-size` cap not reached |
| 130 | Interrupted with `Ctrl-C` |

With several sources, processing continues and the most severe code wins, following the
documented priority 5, 4, 3, 6, 2, 1.

## License

MIT.
