180 lines
3.7 KiB
Python
180 lines
3.7 KiB
Python
#!/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,
|
|
)
|