diff --git a/genicon.py b/genicon.py new file mode 100644 index 0000000..0e328c2 --- /dev/null +++ b/genicon.py @@ -0,0 +1,179 @@ +#!/usr/bin/env python3 + +## requires poppler and pillow: pip3 install pillow +## run as +## - python genicon.py '$\lambda$' +## will produce favicon.png and favicon.ico + +import argparse +import subprocess +import tempfile +from pathlib import Path + +from PIL import Image + + +LATEX_TEMPLATE = r""" +\documentclass[border=2pt]{standalone} +\usepackage{amsmath} +\usepackage{amssymb} + +\begin{document} +%s +\end{document} +""" + + +def run(command, cwd=None): + subprocess.run( + command, + cwd=cwd, + check=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + + +def render_favicon( + latex=r"$\mathcal{F}$", + png_path="favicon.png", + ico_path="favicon.ico", + png_size=128, + padding=12, +): + png_path = Path(png_path) + ico_path = Path(ico_path) + + with tempfile.TemporaryDirectory() as tmp: + tmp = Path(tmp) + + tex_file = tmp / "favicon.tex" + tex_file.write_text(LATEX_TEMPLATE % latex, encoding="utf-8") + + # 1. LaTeX -> tightly cropped PDF via standalone + run( + [ + "pdflatex", + "-interaction=nonstopmode", + "-halt-on-error", + "favicon.tex", + ], + cwd=tmp, + ) + + pdf_file = tmp / "favicon.pdf" + + # 2. PDF -> high-resolution PNG + raw_png_prefix = tmp / "render" + + run( + [ + "pdftocairo", + "-png", + "-singlefile", + "-r", + "600", + str(pdf_file), + str(raw_png_prefix), + ] + ) + + raw_png = tmp / "render.png" + + # 3. Composite onto a pure white background + image = Image.open(raw_png).convert("RGBA") + + white = Image.new("RGBA", image.size, (255, 255, 255, 255)) + white.alpha_composite(image) + image = white.convert("RGB") + + # 4. Scale while preserving aspect ratio + inner_size = png_size - 2 * padding + + image.thumbnail( + (inner_size, inner_size), + Image.Resampling.LANCZOS, + ) + + canvas = Image.new( + "RGB", + (png_size, png_size), + "white", + ) + + x = (png_size - image.width) // 2 + y = (png_size - image.height) // 2 + + canvas.paste(image, (x, y)) + + # 5. Save PNG + canvas.save( + png_path, + "PNG", + optimize=True, + ) + + # 6. Save ICO containing common favicon resolutions + canvas.save( + ico_path, + format="ICO", + sizes=[ + (16, 16), + (32, 32), + (48, 48), + (64, 64), + (128, 128), + ], + ) + + print(f"Created {png_path}") + print(f"Created {ico_path}") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Render LaTeX into favicon PNG/ICO files." + ) + + parser.add_argument( + "latex", + nargs="?", + default=r"$\mathcal{F}$", + help=r'LaTeX to render, e.g. "$\mathcal{F}$"', + ) + + parser.add_argument( + "--png", + default="favicon.png", + help="Output PNG filename", + ) + + parser.add_argument( + "--ico", + default="favicon.ico", + help="Output ICO filename", + ) + + parser.add_argument( + "--size", + type=int, + default=128, + help="PNG canvas size in pixels", + ) + + parser.add_argument( + "--padding", + type=int, + default=12, + help="Padding around the rendered symbol", + ) + + args = parser.parse_args() + + render_favicon( + latex=args.latex, + png_path=args.png, + ico_path=args.ico, + png_size=args.size, + padding=args.padding, + )