This commit is contained in:
Jude Southworth committed 2026-10-27 09:02:46 +00:00
commit 6db0286fc1
24 files changed
+668

No files matched your search

+158
View File
@@ -0,0 +1,158 @@
"""Render a model with render_model.py and compose it into a bottle label:
the render on the left, title and subtitle on the right.
Takes the same arguments as render_model.py (the model path first, then
--texture, --project, --azimuth, ...), plus the label options below.
--output is an A4 sheet with the label repeated --labels-per-sheet times
down the page (PDF or PNG by extension, at --dpi). The render itself is made
with a transparent background, trimmed to its visible pixels and scaled to fit
the left panel of each label.
Usage:
python make_label.py MODEL.blend --texture TEX --output labels.pdf \
[render_model.py options] [--title jider] \
[--subtitle "alc 5.5% · mostly maggot-free"] [--labels-per-sheet 4]
"""
import argparse
import os
import subprocess
import sys
import tempfile
from PIL import Image, ImageDraw, ImageFilter, ImageFont
HERE = os.path.dirname(os.path.abspath(__file__))
RENDER_MODEL = os.path.join(HERE, "render_model.py")
A4_MM = (210, 297)
def parse_args():
"""Split argv into label options and the arguments passed on to render_model.py."""
p = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="All other options are passed to render_model.py "
f"(see: python {os.path.basename(RENDER_MODEL)} "
"MODEL.blend --help).")
p.add_argument("model", help="the .blend file to render")
p.add_argument("--output", required=True, help="output A4 sheet (.pdf or .png)")
p.add_argument("--save-render", help="also keep the apple render at this path")
p.add_argument("--title", default="jider")
p.add_argument("--subtitle", default="alc 5.5% · mostly maggot-free")
p.add_argument("--labels-per-sheet", type=int, default=4,
help="labels stacked vertically on the A4 page")
p.add_argument("--page-margin", type=float, default=10.0,
help="blank margin around the page in mm")
p.add_argument("--label-gap", type=float, default=4.0, help="space between labels in mm")
p.add_argument("--dpi", type=int, default=300)
p.add_argument("--label-split", type=float, default=0.42,
help="fraction of the width given to the apple panel")
p.add_argument("--label-background", default="#ffffff", help="label colour")
p.add_argument("--label-ink", default="#7a1f12", help="text and border colour")
p.add_argument("--label-font", default=os.path.join(HERE, "assets/fonts/comic.ttf"),
help="subtitle font")
p.add_argument("--label-title-font", default=os.path.join(HERE, "assets/fonts/comicbd.ttf"),
help="title font")
return p.parse_known_args()
def render_apple(model, render_args, out_path):
# --resolution 1500 is a default the caller's own --resolution overrides (last one wins).
cmd = [sys.executable, RENDER_MODEL, model, "--resolution", "1500", *render_args,
"--transparent", "--output", out_path]
print("Running:", " ".join(cmd))
result = subprocess.run(cmd)
if result.returncode != 0 or not os.path.exists(out_path):
sys.exit(f"render_model.py failed (exit code {result.returncode})")
def fit_font(path, text, max_w, max_h):
"""Largest font size at which `text` fits inside max_w x max_h."""
lo, hi = 4, 2000
while lo < hi:
size = (lo + hi + 1) // 2
left, top, right, bottom = ImageFont.truetype(path, size).getbbox(text)
if right - left <= max_w and bottom - top <= max_h:
lo = size
else:
hi = size - 1
return ImageFont.truetype(path, lo)
def draw_centered(draw, font, text, cx, top, fill):
"""Draw text horizontally centred on cx with its ink starting at y = top."""
left, t, right, _ = font.getbbox(text)
draw.text((cx - (left + right) / 2, top - t), text, font=font, fill=fill)
def main():
args, render_args = parse_args()
with tempfile.TemporaryDirectory() as tmp:
apple_path = args.save_render or os.path.join(tmp, "apple.png")
render_apple(args.model, render_args, apple_path)
apple = Image.open(apple_path).convert("RGBA")
# A4 page with the labels stacked vertically.
def px(mm):
return round(mm / 25.4 * args.dpi)
page_w, page_h = px(A4_MM[0]), px(A4_MM[1])
n = args.labels_per_sheet
margin, gap = px(args.page_margin), px(args.label_gap)
label_w = page_w - 2 * margin
label_h = (page_h - 2 * margin - (n - 1) * gap) // n
label = compose_label(args, apple, label_w, label_h)
page = Image.new("RGB", (page_w, page_h), "white")
for i in range(n):
page.paste(label, (margin, margin + i * (label_h + gap)))
page.save(args.output, dpi=(args.dpi, args.dpi), resolution=args.dpi)
print(f"Wrote {args.output}: {n} labels of "
f"{label_w / args.dpi * 25.4:.0f} x {label_h / args.dpi * 25.4:.1f} mm on A4")
def compose_label(args, apple, W, H):
pad = round(H * 0.07)
label = Image.new("RGBA", (W, H), args.label_background)
draw = ImageDraw.Draw(label)
ink = args.label_ink
# Double border.
for inset, width in [(pad * 0.35, max(2, H // 120)), (pad * 0.55, max(1, H // 300))]:
draw.rounded_rectangle((inset, inset, W - inset, H - inset), radius=pad * 0.6,
outline=ink, width=round(width))
# Left panel: apple trimmed to its pixels, scaled to fit, with a soft contact shadow.
apple = apple.crop(apple.getchannel("A").getbbox())
panel_w = round(W * args.label_split)
box_w, box_h = panel_w - 2 * pad, H - 2.6 * pad
scale = min(box_w / apple.width, box_h / apple.height)
apple = apple.resize((round(apple.width * scale), round(apple.height * scale)), Image.LANCZOS)
ax = pad + (box_w - apple.width) // 2
ay = round(1.1 * pad + (box_h - apple.height) / 2)
shadow = Image.new("RGBA", (W, H), (0, 0, 0, 0))
sw, sh = apple.width * 0.75, apple.height * 0.08
scx, scy = ax + apple.width / 2, ay + apple.height - sh * 0.3
ImageDraw.Draw(shadow).ellipse((scx - sw / 2, scy - sh / 2, scx + sw / 2, scy + sh / 2),
fill=(60, 30, 10, 110))
label.alpha_composite(shadow.filter(ImageFilter.GaussianBlur(H / 60)))
label.alpha_composite(apple, (ax, ay))
# Right panel: big title over the subtitle, the pair centred vertically.
text_l, text_r = panel_w, W - 1.5 * pad
text_w, cx = text_r - text_l, (text_l + text_r) / 2
title_font = fit_font(args.label_title_font, args.title, text_w, H * 0.5)
sub_font = fit_font(args.label_font, args.subtitle, text_w, H * 0.09)
_, tt, _, tb = title_font.getbbox(args.title)
_, st, _, sb = sub_font.getbbox(args.subtitle)
gap = H * 0.06
top = (H - ((tb - tt) + gap + (sb - st))) / 2
draw_centered(draw, title_font, args.title, cx, top, ink)
draw_centered(draw, sub_font, args.subtitle, cx, top + (tb - tt) + gap, ink)
return label.convert("RGB")
if __name__ == "__main__":
main()