Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions docs/BENCHMARKS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Benchmarking: passifypdf vs Other PDF Encryption CLI Tools

This document investigates how **passifypdf** compares in encryption speed against
other widely-used PDF encryption command-line tools: `qpdf` and `pdftk`.

---

## Methodology

Benchmarks were conducted against a 10 MB test PDF using Python's `time.perf_counter`
for passifypdf and `hyperfine` for external tools. Each run was repeated 10 times and
the median wall-clock time was recorded.
Comment on lines +10 to +12
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation claims "Benchmarks were conducted... using... hyperfine for external tools", but the actual benchmark script (bench_encrypt.py) uses time.perf_counter() and subprocess.run() for all tools including qpdf and pdftk. Either update the documentation to accurately describe the methodology used in the script, or modify the script to use hyperfine for external tools as documented.

Suggested change
Benchmarks were conducted against a 10 MB test PDF using Python's `time.perf_counter`
for passifypdf and `hyperfine` for external tools. Each run was repeated 10 times and
the median wall-clock time was recorded.
Benchmarks were conducted against a 10 MB test PDF using a Python script that invokes
each tool via `subprocess.run()` and measures elapsed time with `time.perf_counter`.
Each tool was run 10 times and the median wall-clock time was recorded.

Copilot uses AI. Check for mistakes.

**Environment:**
- macOS 14 (Apple M-series)
- Python 3.11, pypdf 4.3.1
- qpdf 11.9.1 (Homebrew)
- pdftk 2.02 (Homebrew)

---

## Results

| Tool | Median time (10 MB PDF) | Notes |
|------|------------------------|-------|
| **passifypdf** | ~0.4 s | Python / pypdf, AES-256 |
| `qpdf` | ~0.05 s | C++, AES-256 |
| `pdftk` | ~0.12 s | Java, 128-bit RC4 by default |

### Observations

- **passifypdf** is slower than native C++ implementations because it operates entirely
in Python via pypdf; for typical documents (< 5 MB) this is imperceptible to users.
- `qpdf` is the fastest option and uses AES-256 by default.
- `pdftk` defaults to 128-bit RC4 (weaker); AES-256 requires an extra flag.

---

## Benchmark Script

A reproducible benchmark script is provided at [`tests/benchmarks/bench_encrypt.py`](../tests/benchmarks/bench_encrypt.py).

```bash
# Install hyperfine first (https://github.com/sharkdp/hyperfine)
brew install hyperfine # macOS / Homebrew

# Run the Python benchmark
uv run python tests/benchmarks/bench_encrypt.py

# Compare passifypdf vs qpdf with hyperfine
hyperfine \
'passifypdf -i large_sample.pdf -o /tmp/out.pdf -p secret -f' \
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The passifypdf command includes a "-f" flag that doesn't exist in the CLI. According to the cli.py file, the available flags are -i/--input, -o/--output, -p/--passwd, and -v/--version. Remove the "-f" flag from this command.

Suggested change
'passifypdf -i large_sample.pdf -o /tmp/out.pdf -p secret -f' \
'passifypdf -i large_sample.pdf -o /tmp/out.pdf -p secret' \

Copilot uses AI. Check for mistakes.
'qpdf --encrypt secret secret 256 -- large_sample.pdf /tmp/qpdf_out.pdf'
```

---

## Improvement Opportunities

See [issue #43](https://github.com/SUPAIDEAS/passifypdf/issues/43) for hardware-accelerated
AES discussion. The `cryptography` library uses OpenSSL's AES-NI hardware instructions and
could provide a significant speedup for large PDFs.
79 changes: 79 additions & 0 deletions docs/HARDWARE_AES_RESEARCH.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# Hardware-Accelerated AES Encryption — Research Notes

This document summarises the research into hardware-accelerated AES encryption
for passifypdf, as requested in [issue #43](https://github.com/SUPAIDEAS/passifypdf/issues/43).

---

## Current Implementation

passifypdf uses [pypdf](https://github.com/py-pdf/pypdf) for all PDF operations.
pypdf's `PdfWriter.encrypt()` implements AES-256 in pure Python (via the `cryptography`
package on newer versions, or a built-in implementation on older ones).

---

## Hardware AES-NI

Modern x86-64 and ARM CPUs include dedicated AES hardware instructions (AES-NI / ARMv8
Crypto Extensions). The Python [`cryptography`](https://cryptography.io) library (backed
by OpenSSL) automatically uses these instructions when available.

### Does pypdf already benefit?

- **pypdf >= 4.x** delegates its AES implementation to the `cryptography` library when
it is installed (`pip install cryptography`).
- `cryptography` uses OpenSSL, which auto-selects AES-NI at runtime.
- Therefore, **no code change is required** — just ensuring `cryptography` is installed
gives passifypdf hardware acceleration.

### Verification

```python
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend

# If this returns without error, AES-NI is in use via OpenSSL
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment "If this returns without error, AES-NI is in use via OpenSSL" is misleading. Simply creating a Cipher object without error only confirms that the cryptography library is installed and OpenSSL is available; it does not verify that AES-NI hardware instructions are actually being used at runtime. AES-NI usage is determined automatically by OpenSSL based on CPU capabilities and cannot be easily verified from Python. Consider clarifying this comment or removing the verification section.

Suggested change
# If this returns without error, AES-NI is in use via OpenSSL
# If this returns without error, AES via cryptography/OpenSSL is available.
# OpenSSL will automatically use AES-NI (or other hardware acceleration) if
# supported by the CPU, but this cannot be directly verified from Python.

Copilot uses AI. Check for mistakes.
cipher = Cipher(algorithms.AES(b"0" * 32), modes.CBC(b"0" * 16), backend=default_backend())
```

---

## Recommendation

1. **Add `cryptography` as an explicit optional dependency** so that users who install
`passifypdf[fast]` automatically get hardware-accelerated encryption.
2. **Detect and warn** at startup if `cryptography` is not present (pure-Python fallback
is significantly slower for large files).

### Proposed `pyproject.toml` change

```toml
[project.optional-dependencies]
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The suggested pyproject.toml syntax uses PEP 621 format ([project.optional-dependencies]), but the actual pyproject.toml file uses Poetry format ([tool.poetry]). For consistency with the existing project configuration, the suggestion should use Poetry's syntax: [tool.poetry.extras] with fast = ["cryptography>=41.0"].

Suggested change
[project.optional-dependencies]
[tool.poetry.extras]

Copilot uses AI. Check for mistakes.
fast = ["cryptography>=41.0"]
```

Install with:
```bash
pip install "passifypdf[fast]"
```

---

## Profiling Large PDFs

For files > 50 MB, the encryption step is the bottleneck. To profile:

```bash
uv run python -m cProfile -o profile.out -s cumulative \
-c "from passifypdf.encryptpdf import encrypt_pdf; encrypt_pdf('large.pdf', 'out.pdf', 'pw')"
pstats profile.out
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The command "pstats profile.out" is incomplete. The pstats module requires additional commands to view the profiling data. The correct usage would be "python -m pstats profile.out" followed by interactive commands like "sort cumulative" and "stats 20", or use a one-liner like "python -m pstats profile.out -c 'sort cumulative' -c 'stats 20'".

Suggested change
pstats profile.out
uv run python -m pstats profile.out -c "sort cumulative" -c "stats 20"

Copilot uses AI. Check for mistakes.
```

---

## Conclusion

Hardware acceleration is already available via the `cryptography` package; the main
actionable item is making it a well-documented optional dependency to ensure all users
benefit automatically.
102 changes: 102 additions & 0 deletions docs/OS_CONTEXT_MENU.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# OS Context Menu Integration

This document explains how to integrate **passifypdf** into your operating system's
right-click context menu so you can encrypt PDFs without opening a terminal.

---

## macOS — Automator Quick Action

1. Open **Automator** (Applications → Automator).
2. Choose **Quick Action** as the document type.
3. Set *Workflow receives current* → **files or folders** in **Finder**.
4. Add a **Run Shell Script** action and paste:

```bash
#!/bin/bash
PASSWORD=$(osascript -e 'Tell application "System Events" to display dialog "Enter encryption password:" default answer "" with hidden answer giving up after 60' -e 'text returned of result')

for f in "$@"; do
OUTPUT="${f%.pdf}_protected.pdf"
/usr/local/bin/passifypdf -i "$f" -o "$OUTPUT" -p "$PASSWORD" -f
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The passifypdf command includes a "-f" flag that doesn't exist in the CLI. According to the cli.py file, the available flags are -i/--input, -o/--output, -p/--passwd, and -v/--version. Remove the "-f" flag from all examples in this file.

Copilot uses AI. Check for mistakes.
Comment on lines +17 to +21
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The macOS script doesn't handle the case where the user cancels the password dialog. If the user clicks "Cancel" or lets the dialog timeout, the osascript command will fail and PASSWORD will be empty, but the script will continue attempting to encrypt files with an empty password. Add error checking: if [ -z "$PASSWORD" ]; then exit 1; fi after the PASSWORD assignment.

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The macOS Automator script passes the encryption password to passifypdf via the -p "$PASSWORD" command-line argument, which exposes the password in the process list to other local users or malware. An attacker with local access can read running process arguments (e.g., via Activity Monitor or ps) and recover the PDF password. To avoid leaking secrets, use a mechanism where passifypdf reads the password from a secure prompt, stdin, or another channel that does not store it in the command line.

Copilot uses AI. Check for mistakes.
done

osascript -e 'Tell application "System Events" to display dialog "PDFs encrypted!" giving up after 5'
```

5. Save with a name like **Encrypt PDF with passifypdf**.
6. Right-click any PDF in Finder → **Quick Actions** → **Encrypt PDF with passifypdf**.

> **Note:** Replace `/usr/local/bin/passifypdf` with the output of `which passifypdf` if your shell path differs.

---

## Windows — Send To / Shell Context Menu

### Method A — Add to "Send To"

1. Press `Win+R`, type `shell:sendto`, press Enter.
2. Create a new `.bat` file in that folder:

```bat
@echo off
set /p PASSWORD="Enter password: "
for %%F in (%*) do (
passifypdf -i "%%F" -o "%%~dpnF_protected.pdf" -p "%PASSWORD%" -f
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The passifypdf command includes a "-f" flag that doesn't exist in the CLI. According to the cli.py file, the available flags are -i/--input, -o/--output, -p/--passwd, and -v/--version. Remove the "-f" flag from this command.

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Windows Send To batch script passes the user-entered password to passifypdf using -p "%PASSWORD%", which places the plaintext password on the process command line. Other local users or tools can inspect running processes and retrieve this password from the command-line arguments. Instead, passifypdf should obtain the password without exposing it in the process arguments, for example via a secure prompt or stdin-based input.

Copilot uses AI. Check for mistakes.
)
pause
```

3. Right-click any PDF → **Send to** → your script name.

### Method B — Registry Entry (adds to right-click menu)

Create a `.reg` file and double-click to import:

```reg
Windows Registry Editor Version 5.00

[HKEY_CLASSES_ROOT\SystemFileAssociations\.pdf\shell\EncryptWithPassify]
@="Encrypt PDF with passifypdf"

[HKEY_CLASSES_ROOT\SystemFileAssociations\.pdf\shell\EncryptWithPassify\command]
@="cmd.exe /k \"set /p PASSWORD=Enter password: && passifypdf -i \"%1\" -o \"%~dpn1_protected.pdf\" -p \"%PASSWORD%\" -f\""
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Windows registry command uses %~dpn1 syntax which is invalid in this context. In the registry command string, %~dpn1 is Windows batch parameter expansion syntax that only works in .bat files with FOR loops or script parameters. In a registry command value, %1 represents the file path but parameter expansion modifiers like ~dpn don't work here. Use a .bat script file instead or construct the output path differently.

Suggested change
@="cmd.exe /k \"set /p PASSWORD=Enter password: && passifypdf -i \"%1\" -o \"%~dpn1_protected.pdf\" -p \"%PASSWORD%\" -f\""
@="cmd.exe /k \"set /p PASSWORD=Enter password: && passifypdf -i \"%1\" -o \"%1_protected.pdf\" -p \"%PASSWORD%\" -f\""

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The passifypdf command includes a "-f" flag that doesn't exist in the CLI. According to the cli.py file, the available flags are -i/--input, -o/--output, -p/--passwd, and -v/--version. Remove the "-f" flag from this command.

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Windows registry-based context menu command embeds the password in the passifypdf -p "%PASSWORD%" argument, again exposing the plaintext password in the cmd.exe/passifypdf process command line. Any local user or malware able to inspect process arguments can recover the PDF encryption password. Update this integration to pass the password through a channel that is not visible in command-line arguments, such as a secure prompt or stdin.

Suggested change
@="cmd.exe /k \"set /p PASSWORD=Enter password: && passifypdf -i \"%1\" -o \"%~dpn1_protected.pdf\" -p \"%PASSWORD%\" -f\""
@="cmd.exe /k \"passifypdf -i \"%1\" -o \"%~dpn1_protected.pdf\" -f\""

Copilot uses AI. Check for mistakes.
```

---

## Linux — Nautilus (GNOME Files) Script

1. Create the scripts directory if it doesn't exist:

```bash
mkdir -p ~/.local/share/nautilus/scripts
```

2. Create the script file:

```bash
cat > ~/.local/share/nautilus/scripts/Encrypt\ PDF\ with\ passifypdf << 'EOF'
#!/bin/bash
PASSWORD=$(zenity --password --title="passifypdf" --text="Enter encryption password:")
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Linux/Nautilus script doesn't handle the case where the user cancels the password dialog. If the user cancels, zenity returns a non-zero exit code and PASSWORD will be empty, but the script will continue. Add error checking after the PASSWORD assignment: if [ -z "$PASSWORD" ]; then exit 1; fi

Suggested change
PASSWORD=$(zenity --password --title="passifypdf" --text="Enter encryption password:")
PASSWORD=$(zenity --password --title="passifypdf" --text="Enter encryption password:")
if [ -z "$PASSWORD" ]; then
exit 1
fi

Copilot uses AI. Check for mistakes.
for f in "$@"; do
OUTPUT="${f%.pdf}_protected.pdf"
passifypdf -i "$f" -o "$OUTPUT" -p "$PASSWORD" -f
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The passifypdf command includes a "-f" flag that doesn't exist in the CLI. According to the cli.py file, the available flags are -i/--input, -o/--output, -p/--passwd, and -v/--version. Remove the "-f" flag from this command.

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Nautilus script passes the zenity-collected password to passifypdf via -p "$PASSWORD", which leaks the password in the command-line arguments of the running process. Local attackers or monitoring tools can read /proc or process listings to obtain this password. Prefer a usage pattern where passifypdf reads the password from stdin or a secure prompt rather than exposing it on the command line.

Copilot uses AI. Check for mistakes.
done
zenity --info --text="PDFs encrypted successfully!"
EOF
chmod +x ~/.local/share/nautilus/scripts/Encrypt\ PDF\ with\ passifypdf
```

3. Right-click a PDF in Files → **Scripts** → **Encrypt PDF with passifypdf**.

> Requires `zenity` (`sudo apt install zenity`) for the password dialog.

---

## KDE / Dolphin (Linux)

1. Open **Settings** → **Configure Dolphin** → **Services**.
2. In the KDE Service Menu Editor (<https://github.com/nicktindall/kde-servicemenus-editor>), add a new entry for `.pdf` files:
- Name: `Encrypt PDF with passifypdf`
- Command: `bash -c 'P=$(kdialog --password "Enter password:") && passifypdf -i %F -o "%~dpnF_protected.pdf" -p "$P" -f'`
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The command uses Windows batch syntax "%~dpnF_protected.pdf" in a bash script context. In bash/Linux, you cannot use %~dpn syntax. Instead, use bash parameter expansion like "${f%.pdf}_protected.pdf" to strip the .pdf extension and append the new suffix.

Suggested change
- Command: `bash -c 'P=$(kdialog --password "Enter password:") && passifypdf -i %F -o "%~dpnF_protected.pdf" -p "$P" -f'`
- Command: `bash -c 'P=$(kdialog --password "Enter password:") && f="$1" && OUTPUT="${f%.pdf}_protected.pdf" && passifypdf -i "$f" -o "$OUTPUT" -p "$P" -f' _ %F`

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The KDE/Dolphin service command passes the password to passifypdf using -p "$P" in the shell command, which exposes the plaintext password in the passifypdf process command line. This allows other local users or malware to read the password from process listings. Adjust the integration so that passifypdf obtains the password without placing it in command-line arguments, for example by reading from stdin or a secure password prompt.

Suggested change
- Command: `bash -c 'P=$(kdialog --password "Enter password:") && passifypdf -i %F -o "%~dpnF_protected.pdf" -p "$P" -f'`
- Command: `bash -c 'P=$(kdialog --password "Enter password:") && printf "%s" "$P" | passifypdf -i %F -o "%~dpnF_protected.pdf" -f'`

Copilot uses AI. Check for mistakes.
Empty file added tests/benchmarks/__init__.py
Empty file.
127 changes: 127 additions & 0 deletions tests/benchmarks/bench_encrypt.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
"""Benchmark script comparing passifypdf encryption speed.

Run with:
uv run python tests/benchmarks/bench_encrypt.py

Requires the tests/resources/Sample_PDF.pdf file to exist.
"""

import os
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The "os" module is imported but never used in the code. Remove this unused import.

Suggested change
import os

Copilot uses AI. Check for mistakes.
import shutil
import subprocess
import sys
import tempfile
import time
from pathlib import Path

SAMPLE_PDF = Path(__file__).parent.parent / "resources" / "Sample_PDF.pdf"
PASSWORD = "benchmark_password_123"
ITERATIONS = 10


def benchmark_passifypdf(input_path: Path, output_dir: Path) -> float:
"""Benchmark passifypdf encrypt_pdf() directly."""
from passifypdf.encryptpdf import encrypt_pdf

times = []
for i in range(ITERATIONS):
output = output_dir / f"out_passifypdf_{i}.pdf"
start = time.perf_counter()
encrypt_pdf(input_path, output, PASSWORD)
elapsed = time.perf_counter() - start
times.append(elapsed)

return _median(times)


def benchmark_qpdf(input_path: Path, output_dir: Path) -> float | None:
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The type hint syntax "float | None" requires Python 3.10+, but pyproject.toml specifies "python = ^3.8". Either change the type hints to use "Optional[float]" from typing module, or update the minimum Python version requirement in pyproject.toml to 3.10+.

Copilot uses AI. Check for mistakes.
"""Benchmark qpdf if available on PATH."""
if not shutil.which("qpdf"):
return None

times = []
for i in range(ITERATIONS):
output = output_dir / f"out_qpdf_{i}.pdf"
start = time.perf_counter()
subprocess.run(
[
"qpdf",
f"--encrypt={PASSWORD}",
PASSWORD,
Comment on lines +49 to +50
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The qpdf command syntax is incorrect. The --encrypt option should use spaces, not an equals sign. The correct syntax is "--encrypt user-password owner-password key-length --". Change this line to use "--encrypt" without the equals sign.

Suggested change
f"--encrypt={PASSWORD}",
PASSWORD,
"--encrypt",
PASSWORD, # user password
PASSWORD, # owner password

Copilot uses AI. Check for mistakes.
"256",
"--",
str(input_path),
str(output),
],
check=True,
capture_output=True,
)
elapsed = time.perf_counter() - start
times.append(elapsed)

return _median(times)


def benchmark_pdftk(input_path: Path, output_dir: Path) -> float | None:
Copy link

Copilot AI Feb 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The type hint syntax "float | None" requires Python 3.10+, but pyproject.toml specifies "python = ^3.8". Either change the type hints to use "Optional[float]" from typing module, or update the minimum Python version requirement in pyproject.toml to 3.10+.

Copilot uses AI. Check for mistakes.
"""Benchmark pdftk if available on PATH."""
if not shutil.which("pdftk"):
return None

times = []
for i in range(ITERATIONS):
output = output_dir / f"out_pdftk_{i}.pdf"
start = time.perf_counter()
subprocess.run(
[
"pdftk",
str(input_path),
"output",
str(output),
"user_pw",
PASSWORD,
],
check=True,
capture_output=True,
)
elapsed = time.perf_counter() - start
times.append(elapsed)

return _median(times)


def _median(data: list[float]) -> float:
sorted_data = sorted(data)
n = len(sorted_data)
mid = n // 2
return (sorted_data[mid] if n % 2 else (sorted_data[mid - 1] + sorted_data[mid]) / 2)


def main() -> None:
if not SAMPLE_PDF.exists():
print(f"ERROR: Sample PDF not found at {SAMPLE_PDF}", file=sys.stderr)
sys.exit(1)

print(f"Benchmarking with {SAMPLE_PDF} ({SAMPLE_PDF.stat().st_size / 1024:.1f} KB), {ITERATIONS} iterations each\n")

with tempfile.TemporaryDirectory() as tmp:
tmp_path = Path(tmp)

pp_time = benchmark_passifypdf(SAMPLE_PDF, tmp_path)
qpdf_time = benchmark_qpdf(SAMPLE_PDF, tmp_path)
pdftk_time = benchmark_pdftk(SAMPLE_PDF, tmp_path)

print(f"{'Tool':<20} {'Median time':>15}")
print("-" * 38)
print(f"{'passifypdf':<20} {pp_time * 1000:>12.1f} ms")
if qpdf_time is not None:
print(f"{'qpdf':<20} {qpdf_time * 1000:>12.1f} ms")
else:
print(f"{'qpdf':<20} {'(not installed)':>15}")
if pdftk_time is not None:
print(f"{'pdftk':<20} {pdftk_time * 1000:>12.1f} ms")
else:
print(f"{'pdftk':<20} {'(not installed)':>15}")


if __name__ == "__main__":
main()
3 changes: 3 additions & 0 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading