Build responsive Buch & Bohne website from supplied design
showcase/deploy Live at https://showcase.andrefiedler.de/2608_BuB/

This commit is contained in:
André Fiedler
2026-09-07 17:20:09 +02:00
commit 0712615dfa
37 changed files with 1077 additions and 0 deletions
+150
View File
@@ -0,0 +1,150 @@
#!/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, 254))
tape = tape.replace('#fdc200', '#fbbb0c').replace('#020102', '#0c0c0c')
(ASSETS / 'construction-tape.svg').write_text(tape)
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.')