"""Image utilities.""" import subprocess from pathlib import Path import click from bin.api.image import _RATIO_MAP, generate_image from bin.config import config 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)") @image.command("generate") @click.argument("prompt", required=False) @click.option("-f", "--file", type=click.Path(exists=True), help="从文件读取 prompt") @click.option("-s", "--size", help='图片尺寸 (如 2K, 3K, 4K 或 2304x1728)') @click.option("-r", "--ratio", type=click.Choice(list(_RATIO_MAP.keys())), help="快捷比例 (1:1, 4:3, 3:4, 16:9, 9:16)") @click.option("-o", "--output", help="保存路径(默认只输出 URL)") @click.option("--no-watermark", is_flag=True, help="不添加豆包水印") @click.option("--b64", is_flag=True, help="使用 b64_json 格式获取图片数据") @handle_errors def generate_image_cmd(prompt, file, size, ratio, output, no_watermark, b64): """使用豆包/Seedream 模型生成图片。""" if file: prompt = Path(file).read_text(encoding="utf-8").strip() elif not prompt: raise click.UsageError("必须提供 prompt 或使用 -f/--file 从文件读取") api_key = config.get("apikey_ark") if not api_key: click.echo("Error: apikey_ark not set. Run: mytoolkit env set apikey_ark ", err=True) raise click.Abort() if size and ratio: click.echo("Warning: --size 和 --ratio 同时指定,--size 优先", err=True) result = generate_image( api_key=api_key, prompt=prompt, size=size, ratio=ratio, output=output, watermark=not no_watermark, b64=b64, ) click.echo(result)