"""Universal format conversion.""" from pathlib import Path import click from pypdf import PdfReader, PdfWriter from PIL import Image from bin.md_to_pdf import build_pdf from bin import templates_registry from bin.utils import handle_errors, run_command @click.command("convert") @click.argument("inputs", nargs=-1, required=True) @click.option("-o", "--output", required=True, help="Output file path") @click.option( "-t", "--template", type=click.Choice(["default", "cv", "textbook", "manual", "review"]), default="default", help="Template for md→pdf / md→docx", ) @click.option( "-q", "--quality", type=click.Choice(["screen", "ebook", "printer", "prepress"]), default="screen", help="PDF compression quality", ) @click.option("-d", "--density", default="300", help="DPI for PDF→TIFF") @click.option("--reference-doc", help="Reference docx template for md→docx") @click.option( "--bookmark-depth", type=int, default=1, help="PDF bookmark expansion depth (md→pdf only). 1=top-level only, 2=expand to second level, etc.", ) @click.option("--dry-run", "-n", is_flag=True, help="Show what would be done") @handle_errors def convert_cmd(inputs, output, template, quality, density, reference_doc, bookmark_depth, dry_run): """Universal format conversion. Auto-detects from file extensions. \b Examples: mytoolkit convert doc.md -o doc.pdf mytoolkit convert doc.md -o doc.pdf -t cv mytoolkit convert doc.md -o doc.docx mytoolkit convert doc.md -o doc.docx -t review mytoolkit convert *.jpg -o album.pdf mytoolkit convert fig.eps -o fig.pdf mytoolkit convert scan.tiff -o scan.pdf mytoolkit convert scan.pdf -o scan.tiff mytoolkit convert video.avi -o video.mp4 """ out_path = Path(output) out_ext = out_path.suffix.lower() # Resolve input globs input_paths = [] for pattern in inputs: p = Path(pattern) if p.exists(): input_paths.append(p) else: # Try glob matches = list(Path(".").glob(pattern)) input_paths.extend(sorted(matches)) if not input_paths: click.secho("No input files found", fg="red", err=True) raise SystemExit(1) # --- pdf → pdf (compress) --- if len(input_paths) == 1 and input_paths[0].suffix.lower() == ".pdf" and out_ext == ".pdf": if dry_run: click.echo(f"Would compress {input_paths[0]} to {output} (quality={quality})") return run_command( [ "gs", "-sDEVICE=pdfwrite", "-dNOPAUSE", "-dQUIET", "-dBATCH", f"-dPDFSETTINGS=/{quality}", "-dCompatibilityLevel=1.4", f"-sOutputFile={out_path}", str(input_paths[0]), ], check=True, ) click.secho(f"Compressed PDF: {output}", fg="green") return # --- md → docx --- if all(p.suffix.lower() == ".md" for p in input_paths) and out_ext == ".docx": ref_doc = reference_doc if not ref_doc and template != "default": ref_path = templates_registry.get_md_to_docx_root() / template / "template.docx" if ref_path.exists(): ref_doc = str(ref_path) else: click.secho(f"Docx template not found for '{template}': {ref_path}", fg="red", err=True) raise SystemExit(1) if dry_run: msg = f"Would convert {len(input_paths)} markdown file(s) to {output}" if ref_doc: msg += f" (template={ref_doc})" click.echo(msg) return cmd = ["pandoc"] + [str(p) for p in input_paths] + ["-o", str(out_path)] if ref_doc: cmd.extend(["--reference-doc", ref_doc]) run_command(cmd, check=True) click.secho(f"Word document generated: {output}", fg="green") return # --- md → pdf --- if all(p.suffix.lower() == ".md" for p in input_paths) and out_ext == ".pdf": if template == "review": click.secho("Template 'review' is only available for md→docx", fg="red", err=True) raise SystemExit(1) if dry_run: click.echo(f"Would convert {len(input_paths)} markdown file(s) to {output}") return build_pdf(template, input_paths, out_path, bookmark_depth=bookmark_depth) click.secho(f"PDF generated: {output}", fg="green") return # --- images → pdf --- img_exts = (".jpg", ".jpeg", ".png", ".tif", ".tiff") if all(p.suffix.lower() in img_exts for p in input_paths) and out_ext == ".pdf": if dry_run: click.echo(f"Would convert {len(input_paths)} image(s) to {output}") return _images_to_pdf(input_paths, out_path) click.secho(f"PDF generated: {output}", fg="green") return # --- eps → pdf --- if len(input_paths) == 1 and input_paths[0].suffix.lower() in (".eps", ".ps") and out_ext == ".pdf": if dry_run: click.echo(f"Would convert {input_paths[0]} to {output}") return run_command(["epstopdf", str(input_paths[0]), "--outfile", str(out_path)], check=True) click.secho(f"PDF generated: {output}", fg="green") return # --- pdf → tiff --- if len(input_paths) == 1 and input_paths[0].suffix.lower() == ".pdf" and out_ext in (".tiff", ".tif"): if dry_run: click.echo(f"Would convert {input_paths[0]} to {output}") return run_command( ["convert", "-density", density, str(input_paths[0]), "-compress", "zip", str(out_path)], check=False, ) click.secho(f"TIFF generated: {output}", fg="green") return # --- avi → mp4 --- if len(input_paths) == 1 and input_paths[0].suffix.lower() == ".avi" and out_ext == ".mp4": if dry_run: click.echo(f"Would convert {input_paths[0]} to {output}") return run_command( ["ffmpeg", "-i", str(input_paths[0]), "-c:v", "libx264", "-c:a", "aac", str(out_path)], check=False, ) click.secho(f"MP4 generated: {output}", fg="green") return # --- bib → md --- if len(input_paths) == 1 and input_paths[0].suffix.lower() == ".bib" and out_ext == ".md": if dry_run: click.echo(f"Would convert {input_paths[0]} to {output}") return content = input_paths[0].read_text() md_content = f"# Bibliography\n\n```bibtex\n{content}\n```\n" out_path.write_text(md_content) click.secho(f"Markdown generated: {output}", fg="green") return click.secho( f"Unsupported conversion: {[p.suffix for p in input_paths]} → {out_ext}", fg="red", err=True, ) raise SystemExit(1) def _images_to_pdf(images: list[Path], output: Path) -> None: """Merge images into a single PDF.""" writer = PdfWriter() for img_path in images: img = Image.open(img_path) if img.mode in ("RGBA", "P"): img = img.convert("RGB") temp_pdf = img_path.with_suffix(".temp.pdf") img.save(temp_pdf, "PDF", resolution=150.0) reader = PdfReader(str(temp_pdf)) writer.add_page(reader.pages[0]) temp_pdf.unlink() with open(output, "wb") as f: writer.write(f)