#!/usr/bin/env python3 """The manual as a PDF: docs/manual/index.html through its print stylesheet. python3 tools/manual/pdf.py docs/manual out.pdf Needs WeasyPrint (and so Pillow): `pip install weasyprint`. The Manual pages workflow runs this and publishes the result beside the page. The page's pictures are PNG screenshots and GIF recordings, 16 MB and 46 MB of them, and WeasyPrint embeds a picture as it finds it: losslessly, a GIF whole. So the PDF is made from a copy of the page whose pictures are JPEGs (a recording's first frame, which is all a page can show) at the size they were recorded. Full chroma, because a 4:2:0 JPEG smears the coloured noise the AI denoise close-ups exist to show. """ import os import re import shutil import sys import tempfile from PIL import Image from weasyprint import HTML QUALITY = 88 def as_jpeg(src, dst): with Image.open(src) as im: im.seek(0) im.convert('RGB').save(dst, 'JPEG', quality=QUALITY, subsampling=0, optimize=True) def main(manual_dir, out): with tempfile.TemporaryDirectory() as tmp: os.mkdir(os.path.join(tmp, 'media')) renamed = {} for name in sorted(os.listdir(os.path.join(manual_dir, 'media'))): stem, ext = os.path.splitext(name) if ext.lower() not in ('.png', '.gif'): continue jpg = f'{stem}.{ext[1:].lower()}.jpg' # two pictures may share a stem as_jpeg(os.path.join(manual_dir, 'media', name), os.path.join(tmp, 'media', jpg)) renamed[name] = jpg page = open(os.path.join(manual_dir, 'index.html'), encoding='utf-8').read() page = re.sub(r'src="media/([^"]+)"', lambda m: f'src="media/{renamed.get(m.group(1), m.group(1))}"', page) with open(os.path.join(tmp, 'index.html'), 'w', encoding='utf-8') as f: f.write(page) HTML(os.path.join(tmp, 'index.html')).write_pdf(os.path.join(tmp, 'manual.pdf')) shutil.move(os.path.join(tmp, 'manual.pdf'), out) if __name__ == '__main__': if len(sys.argv) != 3: sys.exit(__doc__) main(sys.argv[1], sys.argv[2])