file-compression
Compress files to reduce storage and transfer size. Use this skill when users ask to shrink PDFs or images, optimize upload/share size, or balance quality and size. Supports PDF compression and image compression with Python-first workflows plus Node.js fallback when Python dependencies are unavailable.
Packaged view
This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.
Install command
npx @skill-hub/cli install openclaw-skills-file-compression
Repository
Skill path: skills/hexavi8/file-compression
Compress files to reduce storage and transfer size. Use this skill when users ask to shrink PDFs or images, optimize upload/share size, or balance quality and size. Supports PDF compression and image compression with Python-first workflows plus Node.js fallback when Python dependencies are unavailable.
Open repositoryBest for
Primary workflow: Ship Full Stack.
Technical facets: Full Stack.
Target audience: everyone.
License: Unknown.
Original source
Catalog source: SkillHub Club.
Repository owner: openclaw.
This is still a mirrored public skill entry. Review the repository before installing into production workflows.
What it helps with
- Install file-compression into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
- Review https://github.com/openclaw/skills before adding file-compression to shared team environments
- Use file-compression for development workflows
Works across
Favorites: 0.
Sub-skills: 0.
Aggregator: No.
Original source / Raw SKILL.md
---
name: file-compression
description: Compress files to reduce storage and transfer size. Use this skill when users ask to shrink PDFs or images, optimize upload/share size, or balance quality and size. Supports PDF compression and image compression with Python-first workflows plus Node.js fallback when Python dependencies are unavailable.
metadata:
{
"clawdbot":
{
"requires":
{
"bins": ["python3", "node", "gs"],
"packages": ["pikepdf", "pillow", "sharp"],
},
"primaryEnv": "python",
},
}
---
# File Compression
Compress files with Python-first workflows and Node.js fallback workflows.
## Supported File Types
- PDF: `.pdf`
- Image: `.jpg`, `.jpeg`, `.png`, `.webp`
## What This Skill Can Do
- Compress PDF with preset quality levels.
- Compress image with quality/resize/format controls.
- Switch backend automatically when dependencies are missing.
- Detect bad compression results and retry with better strategy.
## Installation Spec (Before Running)
Required binaries:
- `python3` (recommended `>= 3.8`)
- `node`
- `gs` (Ghostscript, required for PDF Ghostscript paths)
Python install spec:
```bash
python3 -m pip install -r {baseDir}/requirements.txt
```
Node install spec:
```bash
cd {baseDir}
npm install
```
Ghostscript install examples:
- macOS: `brew install ghostscript`
- Ubuntu/Debian: `sudo apt-get update && sudo apt-get install -y ghostscript`
Safety note:
- Explain to the user before each install command that third-party packages are being installed.
- If installation fails, report the failing command and switch to available fallback backend.
## CLI Options Cheat Sheet
PDF (`scripts/compress_pdf.py`):
- `--preset screen|ebook|printer|prepress`
- `--strategy auto|ghostscript|pikepdf`
- `--remove-metadata`
- `--no-linearize`
- `--overwrite`
PDF Node (`scripts/compress_pdf_node.mjs`):
- `--preset screen|ebook|printer|prepress`
Image (`scripts/compress_image.py`):
- `--quality <1-100>`
- `--format keep|jpeg|png|webp`
- `--max-width <n>`
- `--max-height <n>`
- `--strategy auto|pillow|node`
- `--overwrite`
Image Node (`scripts/compress_image_node.mjs`):
- `--quality <1-100>`
- `--format keep|jpeg|png|webp`
- `--max-width <n>`
- `--max-height <n>`
## Example Set (Python + Node)
PDF default:
```bash
python {baseDir}/scripts/compress_pdf.py in.pdf out.pdf
```
PDF aggressive:
```bash
python {baseDir}/scripts/compress_pdf.py in.pdf out.pdf --preset screen --strategy ghostscript
```
PDF with pikepdf:
```bash
python {baseDir}/scripts/compress_pdf.py in.pdf out.pdf --strategy pikepdf --remove-metadata
```
PDF via Node:
```bash
node {baseDir}/scripts/compress_pdf_node.mjs in.pdf out.pdf --preset ebook
```
Image default:
```bash
python {baseDir}/scripts/compress_image.py in.jpg out.jpg --quality 75
```
Image convert + resize:
```bash
python {baseDir}/scripts/compress_image.py in.png out.webp --format webp --quality 72 --max-width 1920
```
Image force Node backend:
```bash
python {baseDir}/scripts/compress_image.py in.jpg out.jpg --strategy node --quality 70
```
Image direct Node:
```bash
node {baseDir}/scripts/compress_image_node.mjs in.jpg out.jpg --quality 70 --max-width 1600
```
## Environment and Fallback
Check and install in this order:
1. Python: `python3 --version` (fallback: `python --version`)
2. Node: `node --version`
3. Ghostscript: `gs --version` (required for PDF Ghostscript paths)
4. Python deps when needed:
- `pip install pikepdf`
- `pip install pillow`
5. Node deps when needed:
- `npm install`
Fallback policy:
- PDF: `ghostscript` -> `pikepdf` -> `node-ghostscript`
- Image: `pillow` -> `node-sharp`
If `python3.8+` is unavailable, try `python3.11/3.10/3.9/3.8`; if still blocked, use Node flow when possible.
## Execution Transparency
Always communicate each step:
1. Tell user what you are checking or running.
2. Show the exact command before execution.
3. For slow steps (`pip install`, `npm install`, large Ghostscript jobs), say you are waiting.
4. After each step, report result and next action.
## Bad Result Recovery
When `output_size >= input_size`, do not stop:
1. Report exact from/to numbers and compression ratio.
2. Explain likely cause:
- PDF: already optimized, scanned-image content, metadata overhead, unsuitable preset.
- Image: unsuitable format conversion, quality too high, small-file overhead.
3. Retry with alternate strategy:
- PDF: `ebook -> screen`, then switch backend.
- Image: lower quality, switch backend, convert to `webp`, optionally resize.
4. Return the best attempt and state which command produced it.
## Agent Response Contract
After every compression task, always return:
1. Output absolute path.
2. `from <before_size> to <after_size>`.
3. `saved <delta_size> (<ratio>%)`.
4. Backend used.
---
## Referenced Files
> The following files are referenced in this skill and included for context.
### scripts/compress_pdf.py
```python
#!/usr/bin/env python3
"""Compress PDF files using Ghostscript or pikepdf."""
from __future__ import annotations
import argparse
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path
GS_PRESETS = {
"screen": "/screen",
"ebook": "/ebook",
"printer": "/printer",
"prepress": "/prepress",
}
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Compress a PDF file and report size reduction."
)
parser.add_argument("input", type=Path, help="Path to source PDF")
parser.add_argument("output", type=Path, help="Path to output PDF")
parser.add_argument(
"--preset",
choices=sorted(GS_PRESETS.keys()),
default="ebook",
help="Compression preset (default: ebook)",
)
parser.add_argument(
"--strategy",
choices=["auto", "ghostscript", "pikepdf"],
default="auto",
help="Compression backend (default: auto)",
)
parser.add_argument(
"--remove-metadata",
action="store_true",
help="Remove document metadata when using pikepdf",
)
parser.add_argument(
"--no-linearize",
action="store_true",
help="Disable linearized output for web fast-start",
)
parser.add_argument(
"--overwrite",
action="store_true",
help="Allow overwriting output path if it already exists",
)
return parser.parse_args()
def bytes_to_human(value: int) -> str:
units = ["B", "KB", "MB", "GB"]
size = float(value)
for unit in units:
if size < 1024 or unit == units[-1]:
return f"{size:.2f} {unit}"
size /= 1024
return f"{value} B"
def run_ghostscript(input_pdf: Path, output_pdf: Path, preset: str) -> None:
gs_bin = shutil.which("gs")
if not gs_bin:
raise RuntimeError("Ghostscript not found in PATH")
cmd = [
gs_bin,
"-sDEVICE=pdfwrite",
"-dCompatibilityLevel=1.6",
f"-dPDFSETTINGS={GS_PRESETS[preset]}",
"-dNOPAUSE",
"-dBATCH",
"-dQUIET",
"-dDetectDuplicateImages=true",
"-dCompressFonts=true",
"-dSubsetFonts=true",
f"-sOutputFile={output_pdf}",
str(input_pdf),
]
proc = subprocess.run(cmd, capture_output=True, text=True)
if proc.returncode != 0:
stderr = proc.stderr.strip() or proc.stdout.strip() or "unknown error"
raise RuntimeError(f"Ghostscript failed: {stderr}")
def run_pikepdf(
input_pdf: Path,
output_pdf: Path,
remove_metadata: bool,
linearize: bool,
) -> None:
try:
import pikepdf
except ImportError as exc:
raise RuntimeError(
"pikepdf is not installed. Install with: pip install pikepdf"
) from exc
with pikepdf.open(input_pdf) as pdf:
if remove_metadata:
pdf.docinfo.clear()
pdf.remove_unreferenced_resources()
pdf.save(
output_pdf,
compress_streams=True,
object_stream_mode=pikepdf.ObjectStreamMode.generate,
recompress_flate=True,
linearize=linearize,
)
def validate_paths(input_pdf: Path, output_pdf: Path, overwrite: bool) -> None:
if not input_pdf.exists():
raise FileNotFoundError(f"Input file not found: {input_pdf}")
if input_pdf.suffix.lower() != ".pdf":
raise ValueError(f"Input must be a .pdf file: {input_pdf}")
if output_pdf.exists() and not overwrite:
raise FileExistsError(
f"Output file already exists: {output_pdf}. Use --overwrite to replace it."
)
if input_pdf.resolve() == output_pdf.resolve():
raise ValueError("Input and output paths must be different")
def compress(args: argparse.Namespace) -> tuple[str, int, int, Path]:
input_pdf = args.input.expanduser().resolve()
output_pdf = args.output.expanduser().resolve()
validate_paths(input_pdf, output_pdf, args.overwrite)
output_pdf.parent.mkdir(parents=True, exist_ok=True)
tmp_dir = Path(tempfile.mkdtemp(prefix="pdf-compress-"))
tmp_output = tmp_dir / output_pdf.name
linearize = not args.no_linearize
strategy = args.strategy
try:
if strategy == "ghostscript":
run_ghostscript(input_pdf, tmp_output, args.preset)
chosen = "ghostscript"
elif strategy == "pikepdf":
run_pikepdf(input_pdf, tmp_output, args.remove_metadata, linearize)
chosen = "pikepdf"
else:
try:
run_ghostscript(input_pdf, tmp_output, args.preset)
chosen = "ghostscript"
except Exception:
run_pikepdf(input_pdf, tmp_output, args.remove_metadata, linearize)
chosen = "pikepdf"
before = input_pdf.stat().st_size
after = tmp_output.stat().st_size
shutil.move(str(tmp_output), str(output_pdf))
return chosen, before, after, output_pdf
finally:
shutil.rmtree(tmp_dir, ignore_errors=True)
def main() -> int:
args = parse_args()
try:
backend, before, after, output_path = compress(args)
except Exception as exc:
print(f"Error: {exc}", file=sys.stderr)
return 1
saved = before - after
ratio = (saved / before * 100) if before else 0
print(f"Backend: {backend}")
print(f"Output path: {output_path}")
print(f"Input size: {bytes_to_human(before)}")
print(f"Output size: {bytes_to_human(after)}")
print(f"Saved: {bytes_to_human(saved)} ({ratio:.2f}%)")
print(f"From/To: from {bytes_to_human(before)} to {bytes_to_human(after)}")
if after >= before:
print(
"Warning: output is not smaller than input. Try another preset/backend.",
file=sys.stderr,
)
return 0
if __name__ == "__main__":
raise SystemExit(main())
```
### scripts/compress_image.py
```python
#!/usr/bin/env python3
"""Compress image files with Pillow and Node.js fallback."""
from __future__ import annotations
import argparse
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path
SUPPORTED = {"jpg", "jpeg", "png", "webp"}
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Compress an image and report size reduction."
)
parser.add_argument("input", type=Path, help="Path to source image")
parser.add_argument("output", type=Path, help="Path to output image")
parser.add_argument(
"--quality",
type=int,
default=75,
help="Output quality for lossy formats (default: 75)",
)
parser.add_argument("--max-width", type=int, help="Resize max width")
parser.add_argument("--max-height", type=int, help="Resize max height")
parser.add_argument(
"--format",
choices=["keep", "jpeg", "png", "webp"],
default="keep",
help="Output format (default: keep input format)",
)
parser.add_argument(
"--strategy",
choices=["auto", "pillow", "node"],
default="auto",
help="Compression backend (default: auto)",
)
parser.add_argument(
"--overwrite",
action="store_true",
help="Allow overwriting output path if it already exists",
)
return parser.parse_args()
def bytes_to_human(value: int) -> str:
units = ["B", "KB", "MB", "GB"]
size = float(value)
for unit in units:
if size < 1024 or unit == units[-1]:
return f"{size:.2f} {unit}"
size /= 1024
return f"{value} B"
def validate_paths(input_path: Path, output_path: Path, overwrite: bool) -> None:
if not input_path.exists():
raise FileNotFoundError(f"Input file not found: {input_path}")
if output_path.exists() and not overwrite:
raise FileExistsError(
f"Output file already exists: {output_path}. Use --overwrite to replace it."
)
if input_path.resolve() == output_path.resolve():
raise ValueError("Input and output paths must be different")
in_ext = input_path.suffix.lower().lstrip(".")
if in_ext not in SUPPORTED:
raise ValueError(f"Unsupported input format: .{in_ext}")
def output_format(input_path: Path, requested: str) -> str:
if requested != "keep":
return requested
ext = input_path.suffix.lower().lstrip(".")
return "jpeg" if ext == "jpg" else ext
def run_pillow(
input_path: Path,
output_path: Path,
quality: int,
fmt: str,
max_width: int | None,
max_height: int | None,
) -> None:
try:
from PIL import Image
except ImportError as exc:
raise RuntimeError("Pillow not installed. Install with: pip install pillow") from exc
save_format = "JPEG" if fmt == "jpeg" else fmt.upper()
output_path.parent.mkdir(parents=True, exist_ok=True)
with Image.open(input_path) as img:
if max_width or max_height:
target = (max_width or img.width, max_height or img.height)
img.thumbnail(target, Image.Resampling.LANCZOS)
params: dict[str, object] = {"optimize": True}
if save_format in {"JPEG", "WEBP"}:
params["quality"] = max(1, min(100, quality))
if save_format == "PNG":
params["compress_level"] = 9
if save_format == "JPEG" and img.mode not in ("RGB", "L"):
img = img.convert("RGB")
img.save(output_path, format=save_format, **params)
def run_node_backend(
input_path: Path,
output_path: Path,
quality: int,
fmt: str,
max_width: int | None,
max_height: int | None,
) -> None:
node_bin = shutil.which("node")
if not node_bin:
raise RuntimeError("Node.js not found in PATH")
script_path = Path(__file__).resolve().parent / "compress_image_node.mjs"
cmd = [
node_bin,
str(script_path),
str(input_path),
str(output_path),
"--quality",
str(quality),
"--format",
fmt,
]
if max_width:
cmd += ["--max-width", str(max_width)]
if max_height:
cmd += ["--max-height", str(max_height)]
proc = subprocess.run(cmd, capture_output=True, text=True)
if proc.returncode != 0:
stderr = proc.stderr.strip() or proc.stdout.strip() or "unknown error"
raise RuntimeError(f"Node backend failed: {stderr}")
def compress(args: argparse.Namespace) -> tuple[str, int, int, Path]:
input_path = args.input.expanduser().resolve()
output_path = args.output.expanduser().resolve()
validate_paths(input_path, output_path, args.overwrite)
fmt = output_format(input_path, args.format)
tmp_dir = Path(tempfile.mkdtemp(prefix="img-compress-"))
tmp_output = tmp_dir / output_path.name
try:
chosen = args.strategy
if chosen == "pillow":
run_pillow(
input_path, tmp_output, args.quality, fmt, args.max_width, args.max_height
)
backend = "pillow"
elif chosen == "node":
run_node_backend(
input_path, tmp_output, args.quality, fmt, args.max_width, args.max_height
)
backend = "node-sharp"
else:
try:
run_pillow(
input_path,
tmp_output,
args.quality,
fmt,
args.max_width,
args.max_height,
)
backend = "pillow"
except Exception:
run_node_backend(
input_path,
tmp_output,
args.quality,
fmt,
args.max_width,
args.max_height,
)
backend = "node-sharp"
output_path.parent.mkdir(parents=True, exist_ok=True)
before = input_path.stat().st_size
after = tmp_output.stat().st_size
shutil.move(str(tmp_output), str(output_path))
return backend, before, after, output_path
finally:
shutil.rmtree(tmp_dir, ignore_errors=True)
def main() -> int:
args = parse_args()
try:
backend, before, after, output_path = compress(args)
except Exception as exc:
print(f"Error: {exc}", file=sys.stderr)
return 1
saved = before - after
ratio = (saved / before * 100) if before else 0
print(f"Backend: {backend}")
print(f"Output path: {output_path}")
print(f"Input size: {bytes_to_human(before)}")
print(f"Output size: {bytes_to_human(after)}")
print(f"Saved: {bytes_to_human(saved)} ({ratio:.2f}%)")
print(f"From/To: from {bytes_to_human(before)} to {bytes_to_human(after)}")
if after >= before:
print(
"Warning: output is not smaller than input. Try another quality/format/backend.",
file=sys.stderr,
)
return 0
if __name__ == "__main__":
raise SystemExit(main())
```
---
## Skill Companion Files
> Additional files collected from the skill directory layout.
### _meta.json
```json
{
"owner": "hexavi8",
"slug": "file-compression",
"displayName": "File Compression",
"latest": {
"version": "1.0.1",
"publishedAt": 1772037787755,
"commit": "https://github.com/openclaw/skills/commit/9ee054608ffadaca8acf25903f9e1e9bf8c19895"
},
"history": []
}
```