#!/usr/bin/env python3 """Derive web artwork from the supplied PDF; never redraw the brand assets. Development only: uv run --with pymupdf --with pillow --with 'fonttools[woff]' scripts/prepare-assets.py. Generated assets are committed; deployment needs only Node. """ from copy import deepcopy from io import BytesIO from pathlib import Path import re import subprocess import urllib.request import xml.etree.ElementTree as ET from zipfile import ZipFile import pymupdf from PIL import Image from fontTools import subset from fontTools.ttLib import TTFont from fontTools.varLib.instancer import instantiateVariableFont ROOT = Path(__file__).resolve().parents[1] ASSETS = ROOT / 'assets' FONTS = ASSETS / 'fonts' TMP = ROOT / 'tmp' / 'assets' for directory in [ASSETS, FONTS, TMP]: directory.mkdir(parents=True, exist_ok=True) SVG = 'http://www.w3.org/2000/svg' XLINK = 'http://www.w3.org/1999/xlink' ET.register_namespace('', SVG) ET.register_namespace('xlink', XLINK) pdf = pymupdf.open(ROOT / 'design/2608_BuB_website_baustelle.pdf') page = pdf[0] source = ET.fromstring(page.get_svg_image(text_as_path=True)) definitions = {element.get('id'): element for element in source.iter() if element.get('id')} def artwork(elements, viewbox): root = ET.Element(f'{{{SVG}}}svg', {'viewBox': ' '.join(map(str, viewbox))}) defs = ET.SubElement(root, f'{{{SVG}}}defs') required = set() def dependencies(element): text = ET.tostring(element, encoding='unicode') refs = re.findall(r'url\(#([^)]*)\)', text) refs += re.findall(r'(?:xlink:)?href="#([^"]*)"', text) for ref in refs: if ref not in required: required.add(ref) dependencies(definitions[ref]) for element in elements: dependencies(element) for key, value in definitions.items(): if key in required: defs.append(deepcopy(value)) root.extend(deepcopy(element) for element in elements) return ET.tostring(root, encoding='unicode') def raster(svg, width): document = pymupdf.open(stream=svg.encode(), filetype='svg') vector = pymupdf.open('pdf', document.convert_to_pdf()) pix = vector[0].get_pixmap(matrix=pymupdf.Matrix(width / vector[0].rect.width, width / vector[0].rect.width), alpha=True) return Image.open(BytesIO(pix.tobytes('png'))) # These drawing groups are verified against the supplied one-page PDF. # The print export flattens photos, tint and tape shadows into clipped tiles. # Remove only text, wholly enclosed logo/brush vectors, and the QR resource. # Poppler preserves the original print color conversion and clipping exactly. assert len(source[50]) == 6 and source[504].get('data-text') == 'W' background_pdf = pymupdf.open(ROOT / 'design/2608_BuB_website_baustelle.pdf') background_page = background_pdf[0] background_page.add_redact_annot(background_page.rect, fill=False, cross_out=False) background_page.apply_redactions(images=0, graphics=0, text=0) for bounds in [(506, 141, 694, 290), (363, 435, 819, 485)]: background_page.add_redact_annot(pymupdf.Rect(*bounds), fill=False, cross_out=False) background_page.apply_redactions(images=0, graphics=1, text=1) background_page.delete_image(48) background_pdf.save(TMP / 'background.pdf') subprocess.run(['pdftoppm', '-scale-to', '2400', '-singlefile', '-png', str(TMP / 'background.pdf'), str(TMP / 'background')], check=True) background = Image.open(TMP / 'background.png').convert('RGB') for width in [1200, 2400]: background.resize((width, width * 3 // 4), Image.Resampling.LANCZOS).save( ASSETS / f'background-{width}.webp', quality=91, method=6) # On tall screens the photo fills the page and the tape stays above the logo. background.crop((0, 340, 2400, 1800)).resize((1200, 730), Image.Resampling.LANCZOS).save( ASSETS / 'background-mobile.webp', quality=89, method=6) tape = artwork([*list(source)[12:24], *list(source)[41:50]], (0, 0, 1200, 270)) tape = tape.replace('#fdc200', '#fbbb0c').replace('#020102', '#0c0c0c') tape_root = ET.fromstring(tape) # Original IDML shadow, applied separately so the diagonal tape shadows the lower tape. shadow = ET.SubElement(tape_root[0], f'{{{SVG}}}filter', { 'id': 'tape-shadow', 'filterUnits': 'userSpaceOnUse', 'x': '-20', 'y': '-20', 'width': '1240', 'height': '310', 'color-interpolation-filters': 'sRGB', }) ET.SubElement(shadow, f'{{{SVG}}}feDropShadow', { 'dx': '2.789536', 'dy': '2.789536', 'stdDeviation': '1.9725', 'flood-color': '#000', 'flood-opacity': '0.35', }) tape_paths = list(tape_root)[1:] for paths in [tape_paths[:12], tape_paths[12:]]: group = ET.SubElement(tape_root, f'{{{SVG}}}g', {'filter': 'url(#tape-shadow)'}) for path in paths: tape_root.remove(path) group.append(path) (ASSETS / 'construction-tape.svg').write_text(ET.tostring(tape_root, encoding='unicode')) logo_paths = list(source[50]) logo = artwork(logo_paths, (507.62, 142, 184.76, 146.67)) (ASSETS / 'logo.svg').write_text(logo) heading_glyphs = [element for element in source if element.get(f'{{{XLINK}}}href', '').startswith('#font_63_')] # Match the PDF's sRGB print rendering, as used for the photographic backdrop. for element in source[503].iter(): if element.get('fill'): element.set('fill', '#ef3a57') heading = artwork([source[503], *heading_glyphs], (363.77, 435.92, 454.02, 47.90)) (ASSETS / 'support-heading.svg').write_text(heading) qr = pdf.extract_image(48) Image.open(BytesIO(qr['image'])).convert('RGB').quantize(colors=64).save(ASSETS / 'paypal-qr.png', optimize=True) mark = artwork(logo_paths[:4], (600, 136, 99, 94)) mark_root = ET.fromstring(mark) mark_root.set('x', '12') mark_root.set('y', '12') mark_root.set('width', '104') mark_root.set('height', '104') icon = ET.Element(f'{{{SVG}}}svg', {'viewBox': '0 0 128 128'}) ET.SubElement(icon, f'{{{SVG}}}rect', {'width': '128', 'height': '128', 'rx': '20', 'fill': '#982249'}) icon.append(mark_root) icon_svg = ET.tostring(icon, encoding='unicode') (ROOT / 'favicon.svg').write_text(icon_svg) icon_image = raster(icon_svg, 512) for filename, size in [('apple-touch-icon.png', 180), ('icon-192.png', 192), ('icon-512.png', 512)]: icon_image.resize((size, size), Image.Resampling.LANCZOS).save(ASSETS / filename, optimize=True) icon_image.save(ROOT / 'favicon.ico', sizes=[(16, 16), (32, 32), (48, 48)]) preview = page.get_pixmap(clip=pymupdf.Rect(0, 100, 1200, 730), alpha=False) Image.open(BytesIO(preview.tobytes('png'))).save(ASSETS / 'social-preview.jpg', quality=90, optimize=True) archive = ROOT / 'tmp/fonts/Roboto_v3.015.zip' archive.parent.mkdir(parents=True, exist_ok=True) if not archive.exists(): urllib.request.urlretrieve('https://github.com/googlefonts/roboto-3-classic/releases/download/v3.015/Roboto_v3.015.zip', archive) with ZipFile(archive) as zipped: for name, filename in [('Roboto', 'roboto-latin.woff2'), ('Roboto-Italic', 'roboto-italic-latin.woff2')]: font = TTFont(BytesIO(zipped.read(f'web/split/{name}[wdth,wght].ttf'))) options = subset.Options() options.flavor = 'woff2' subsetter = subset.Subsetter(options=options) subsetter.populate(unicodes=[*range(0x20, 0x100), *range(0x2000, 0x2070), 0x20AC, 0xFFFD]) subsetter.subset(font) font = instantiateVariableFont(font, {'wdth': 100, 'wght': (400, 700)}, inplace=True) font.flavor = 'woff2' font.save(FONTS / filename) urllib.request.urlretrieve('https://raw.githubusercontent.com/googlefonts/roboto-3-classic/v3.015/OFL.txt', FONTS / 'OFL.txt') license_file = FONTS / 'OFL.txt' license_file.write_text('\n'.join(line.rstrip() for line in license_file.read_text().splitlines()) + '\n') print('Prepared original artwork, QR code, logo icons and local Roboto 3.015 fonts.')