Files
2608_BuB/scripts/prepare-assets.py
André Fiedler 5647b4aba7
showcase/deploy Live at https://showcase.andrefiedler.de/2608_BuB/
Add mobile tape shadows and center bank heading
2026-09-07 19:03:47 +02:00

168 lines
7.7 KiB
Python

#!/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.')