refactor: restructure mytoolkit with new subcommands and md-to-pdf improvements
- Add new subcommands: convert, preflight, server, templates, webpage - Migrate config from bin/config.json to ~/.config/mytoolkit - Fix expand_bookmarks to modify writer objects instead of reader - Improve md_to_pdf with CJK bookmark support - Update README and project metadata
This commit is contained in:
@@ -0,0 +1,206 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user