Metadata-Version: 2.4
Name: pdf-toolchest
Version: 0.2.1
Summary: A Swiss-army knife for everyday PDF workflows.
License-File: LICENSE
Author: Alex
Requires-Python: <4,>=3.11
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Programming Language :: Python :: 3.15
Requires-Dist: img2pdf (>=0.5.1,<0.6.0)
Requires-Dist: pillow (>=12.0.0,<13.0.0)
Requires-Dist: pydantic (>=2.10.6,<3.0.0)
Requires-Dist: pymupdf (>=1.26.3,<2.0.0)
Requires-Dist: pypdf (>=5.2.0,<6.0.0)
Requires-Dist: typer (>=0.15.1,<0.16.0)
Description-Content-Type: text/markdown

# pdf-tools

Tools for converting documents to PDF, merging PDFs, and adding text watermarks.
The Python package is `pdf-toolchest`; the installed command is `pdf-tools`.

## Supported operations

| Operation | Inputs | Behavior |
| --- | --- | --- |
| Convert | Images with `.jpg`, `.jpeg`, `.png`, `.tiff`, or `.bmp` extensions; Word `.doc` and `.docx` files | Pillow and img2pdf handle images; LibreOffice with `unoserver` and `unoconvert` handles Word files. Other Pillow-readable formats are not accepted by the dispatcher. |
| Merge | PDFs | Non-PDF inputs are skipped with a warning. Optional bookmarks use `File.bookmark_name`, falling back to the filename. |
| Process | Supported conversion inputs and existing PDFs | Converts what it can, then merges the successful conversions and existing PDFs. |
| Watermark | PDF | Adds text with PyMuPDF. Rotation must be a multiple of 90 degrees. |

## Installation

```bash
pip install pdf-toolchest
pdf-tools --help
```

Word conversion requires LibreOffice and the `unoserver`/`unoconvert` integration. The `unoserver` installation must be able to import LibreOffice's `uno` module. For many Linux setups:

```bash
pipx install unoserver --system-site-packages
```

CLI commands that encounter Word inputs start and stop the required listener automatically. You do not need to start one manually for normal CLI use.

## CLI quick start

Output parent directories must already exist. Commands do not create them. In this example, `out/` is created before conversion; the other outputs are written to the current directory.

```bash
# Convert one Word file beside its source
pdf-tools convert file-to-pdf draft.docx

# Convert immediate children of a folder into an existing output directory
mkdir -p out
pdf-tools convert folder-to-pdfs assets/ --output-dir out/

# Merge selected PDFs in the given order
pdf-tools merge pdf-files a.pdf b.pdf c.pdf -o merged.pdf

# Merge PDFs found in a folder
pdf-tools merge pdfs-in-folder scans/ -o merged.pdf

# Convert images and Word files, then merge the successful inputs
pdf-tools process convert-and-merge-pdfs image1.jpg doc1.docx doc2.docx -o final.pdf

# Add a red DRAFT watermark on every page
pdf-tools watermark add-text src.pdf stamped.pdf \
    --text "DRAFT" --color "#FF0000" --font-size 72 --opacity 0.2 --rotation 90
```

Watermark rotation must be a multiple of 90 degrees (for example, `0`, `90`, `180`, or `270`). `--rotation 45` fails model validation.

`merge pdfs-in-folder` uses the filesystem's directory enumeration order. Alphabetical order is not guaranteed; pass files explicitly to `merge pdf-files` in the desired sequence when order matters.

Batch conversion continues after an individual input fails. `convert files-to-pdfs` and `convert folder-to-pdfs` print a reason for each skipped file and a converted/skipped count. They exit successfully if at least one file converted, even when others were skipped; they exit with code 1 when none converted. A missing output directory makes each attempted conversion fail and appear in the skipped results.

`process convert-and-merge-pdfs` is **best-effort**: an input that cannot be converted is omitted, while successful conversions and existing PDFs may still be merged. The output can contain fewer documents than requested. The process CLI does not list omitted inputs. If every input fails, no merge is written. For all-or-nothing work, run conversion separately, inspect every result, and merge only after all required inputs succeeded.

## Python API

Common APIs are exported from `pdf_tools`. Their accepted input types vary by operation:

| Operation | Accepted input types |
| --- | --- |
| Single-file conversion (`convert_file_to_pdf`, `convert_image_to_pdf`, `convert_word_to_pdf`) | `File`, `str`, or `Path` |
| Batch conversion (`convert_files_to_pdfs`) | `Files` or a sequence of `File`, `str`, or `Path` |
| Folder conversion (`convert_folder_to_pdfs`) | Directory as `str` or `Path` |
| Merge (`merge_pdfs`) and process (`convert_and_merge_pdfs`) | `Files` or a sequence of `File`, `str`, or `Path` |
| Watermark (`add_text_watermark`) | Source and destination as `str` or `Path`; options as `WatermarkOptions` |

The destination's parent directory must exist for single-file conversion, merging, and processing. Single-file conversion and merging raise `FileNotFoundError` for a missing output parent. Batch conversion records that error in `ConversionBatchResult.skipped` for each affected input. Processing raises `FileNotFoundError` at merge time if at least one input was retained; if none were retained, it raises `ValueError` instead.

```python
from pathlib import Path
from pdf_tools import WatermarkOptions, add_text_watermark, convert_file_to_pdf, merge_pdfs

Path("out").mkdir(exist_ok=True)
img_pdf = convert_file_to_pdf("diagram.png", output_path=Path("out"))
merged = merge_pdfs(["intro.pdf", img_pdf.path], output_path="bundle.pdf", set_bookmarks=True)
add_text_watermark(
    src=merged.path,
    dst="bundle_wm.pdf",
    opts=WatermarkOptions(text="CONFIDENTIAL", font_size=36, all_pages=False),
)
```

Batch conversion returns `ConversionBatchResult`: `converted` contains `File` models for successful PDFs; `skipped` contains each failed input path and its reason. It continues after a failed input and can return both lists populated.

```python
from pathlib import Path
from pdf_tools import convert_files_to_pdfs

Path("out").mkdir(exist_ok=True)
result = convert_files_to_pdfs(["photo.jpg", "notes.txt"], output_dir="out")
print(result.converted)
print(result.skipped)
```

Direct Python service calls do not start a Word-conversion listener. Manage that resource around calls involving `.doc` or `.docx`, for example:

```python
from pdf_tools import convert_and_merge_pdfs, unoserver_listener

with unoserver_listener():
    convert_and_merge_pdfs(
        files=["doc1.docx", "pic.jpg", "appendix.pdf"],
        output_path="package.pdf",
    )
```

## Architecture

- **Models:** Pydantic v2 models describe inputs, options, and results.
- **Services:** Synchronous Python functions perform conversion, merging, processing, and watermarking.
- **CLI:** Typer commands parse arguments, print results, and manage CLI-specific resources.
- **External tools:** LibreOffice and `unoserver`/`unoconvert` perform Word conversion.

CLI setup and resource behavior can differ from direct service-layer calls.

## Development

```bash
git clone https://github.com/aswann45/pdf-tools.git
cd pdf-tools
poetry install --with dev
```

On Unix-like systems, `scripts/lint` is a Bash convenience wrapper. Run `poetry run bash scripts/lint` and `poetry run pytest`. The wrapper runs Mypy on `pdf_tools` and `tests`, Ruff lint on both directories with `ERA001` and `FIX002` ignored, and Ruff format checks on both directories.

On Windows, run the underlying commands in PowerShell. The Mypy configuration names `.venv/bin/python`, so override that Unix path with Poetry's environment interpreter:

```powershell
$venvPython = poetry env info --executable
poetry run mypy --python-executable $venvPython pdf_tools
poetry run mypy --python-executable $venvPython tests
poetry run ruff check pdf_tools --ignore ERA001 --ignore FIX002
poetry run ruff check tests --ignore ERA001 --ignore FIX002
poetry run ruff format pdf_tools --check
poetry run ruff format tests --check
poetry run pytest
```

The formatting check is `ruff format --check`; `scripts/format` applies formatting on Unix-like systems. Word-conversion tests marked `slow` require LibreOffice and working `uno` bindings.

