Files
mytoolkit/bin/commands/image.py
T
Zhengshou Lai c1df4098b3 refactor: rename package from mytoolkit to bin
- Move all source files from mytoolkit/ to bin/
- Update entry point and build config in pyproject.toml
- Add pillow and pypdf dependencies
- Update README and .gitignore paths
- Remove unused subprocess import in pdf.py
- Clean up duplicate imports in pdf merge command
2026-04-26 09:56:29 +08:00

184 lines
6.1 KiB
Python

"""Image utilities."""
import subprocess
from pathlib import Path
import click
from bin.utils import handle_errors, run_command
@click.group()
def image():
"""Image manipulation commands."""
pass
@image.command("eps-to-pdf")
@click.argument("files", nargs=-1, required=True)
@click.option("--dry-run", "-n", is_flag=True, help="Show what would be done")
@handle_errors
def eps_to_pdf(files, dry_run):
"""Convert EPS files to PDF."""
for pattern in files:
for eps_file in Path(".").glob(pattern):
if eps_file.suffix.lower() not in (".eps", ".ps"):
continue
output = eps_file.with_suffix(".pdf")
if dry_run:
click.echo(f"Would convert: {eps_file} -> {output}")
continue
click.echo(f"Converting: {eps_file} -> {output}")
run_command(["epstopdf", str(eps_file)], check=False)
@image.command("eps-fix")
@click.argument("file", type=click.Path(exists=True))
@click.option("--dry-run", "-n", is_flag=True, help="Show what would be done")
@handle_errors
def eps_fix(file, dry_run):
"""Fix EPS files created by print command."""
path = Path(file)
content = path.read_text()
if "/f/fill" not in content:
click.echo("File already fixed or not created by print command")
return
if dry_run:
click.echo(f"Would fix: {file}")
return
lines = content.split("\n")
new_lines = []
for line in lines:
if "/f/fill" in line:
line = line.replace("/f/fill", "")
new_lines.append(line)
path.write_text("\n".join(new_lines))
click.echo(f"Fixed: {file}")
@image.command("jpg-to-pdf")
@click.argument("files", nargs=-1, required=True)
@click.option("--output", "-o", help="Output PDF file")
@click.option("--dry-run", "-n", is_flag=True, help="Show what would be done")
@handle_errors
def jpg_to_pdf(files, output, dry_run):
"""Convert JPG files to PDF."""
images = []
for pattern in files:
for jpg_file in Path(".").glob(pattern):
if jpg_file.suffix.lower() in (".jpg", ".jpeg"):
images.append(str(jpg_file))
if not images:
click.echo("No JPG files found")
return
output = output or "output.pdf"
if dry_run:
click.echo(f"Would convert {len(images)} images to {output}")
return
click.echo(f"Converting {len(images)} images to {output}")
run_command(["convert"] + images + [output], check=False)
@image.command("tiff-to-pdf")
@click.argument("files", nargs=-1, required=True)
@click.option("--dry-run", "-n", is_flag=True, help="Show what would be done")
@handle_errors
def tiff_to_pdf(files, dry_run):
"""Convert TIFF files to PDF."""
for pattern in files:
for tiff_file in Path(".").glob(pattern):
if tiff_file.suffix.lower() not in (".tif", ".tiff"):
continue
output = tiff_file.with_suffix(".pdf")
if dry_run:
click.echo(f"Would convert: {tiff_file} -> {output}")
continue
click.echo(f"Converting: {tiff_file} -> {output}")
run_command(["convert", str(tiff_file), str(output)], check=False)
@image.command("tiff-compress")
@click.argument("files", nargs=-1, required=True)
@click.option("--dry-run", "-n", is_flag=True, help="Show what would be done")
@handle_errors
def tiff_compress(files, dry_run):
"""Compress TIFF files."""
for pattern in files:
for tiff_file in Path(".").glob(pattern):
if tiff_file.suffix.lower() not in (".tif", ".tiff"):
continue
if dry_run:
click.echo(f"Would compress: {tiff_file}")
continue
click.echo(f"Compressing: {tiff_file}")
run_command(
["convert", str(tiff_file), "-compress", "zip", str(tiff_file)],
check=False,
)
@image.command("compress")
@click.argument("files", nargs=-1, required=True)
@click.option("--target-mb", "-t", default=1.0, help="Target max file size in MB")
@click.option("--max-width", "-w", default=1600, help="Max width in pixels if resize needed")
@click.option("--quality", "-q", default=75, help="JPEG quality for resized images")
@click.option("--suffix", "-s", default="_compressed", help="Output filename suffix")
@click.option("--dry-run", "-n", is_flag=True, help="Show what would be done")
@handle_errors
def compress_images(files, target_mb, max_width, quality, suffix, dry_run):
"""Compress PNG/JPG images to fit under target size."""
from PIL import Image
for pattern in files:
for img_file in Path(".").glob(pattern):
if img_file.suffix.lower() not in (".png", ".jpg", ".jpeg"):
continue
output = img_file.with_stem(f"{img_file.stem}{suffix}")
if img_file.suffix.lower() == ".png":
output = output.with_suffix(".jpg")
if dry_run:
click.echo(f"Would compress: {img_file} -> {output}")
continue
img = Image.open(img_file)
if img.mode in ("RGBA", "P"):
img = img.convert("RGB")
# Try full-res first with q80
img.save(output, "JPEG", quality=80, optimize=True)
size_mb = output.stat().st_size / (1024 * 1024)
if size_mb > target_mb:
w, h = img.size
if w > max_width:
h = int(h * max_width / w)
w = max_width
try:
resample = Image.Resampling.LANCZOS
except AttributeError:
resample = Image.LANCZOS # type: ignore[attr-defined]
img = img.resize((w, h), resample)
img.save(output, "JPEG", quality=quality, optimize=True)
size_mb = output.stat().st_size / (1024 * 1024)
if size_mb > target_mb:
img.save(output, "JPEG", quality=quality - 5, optimize=True)
size_mb = output.stat().st_size / (1024 * 1024)
click.echo(f"Compressed: {img_file} -> {output} ({size_mb:.2f} MB)")