Build responsive Buch & Bohne website from supplied design
showcase/deploy Live at https://showcase.andrefiedler.de/2608_BuB/
showcase/deploy Live at https://showcase.andrefiedler.de/2608_BuB/
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
import { cp, mkdir, rm } from 'node:fs/promises';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const root = new URL('../', import.meta.url);
|
||||
const output = new URL('dist/', root);
|
||||
const publicFiles = ['index.html', 'assets', 'favicon.ico', 'favicon.svg', 'sitemap.xml', 'robots.txt', 'site.webmanifest'];
|
||||
|
||||
await rm(output, { recursive: true, force: true });
|
||||
await mkdir(output, { recursive: true });
|
||||
for (const filename of publicFiles) {
|
||||
await cp(new URL(filename, root), new URL(filename, output), { recursive: true });
|
||||
}
|
||||
console.log(`Static site built at ${fileURLToPath(output)}`);
|
||||
@@ -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.')
|
||||
Reference in New Issue
Block a user