genicon.py
This commit is contained in:
+179
@@ -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,
|
||||
)
|
||||
Reference in New Issue
Block a user