151 lines
6.9 KiB
Python
151 lines
6.9 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, 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.')
|