"""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()