Files
DarkRoom/tools/manual/pdf.py
T
dtourolle b53d4dc391
Benchmarks / CPU and I/O (per commit) (push) Successful in 3m5s
Benchmarks / Frame budget (on demand) (push) Skipped
Build and test / Desktop (Linux) (push) Successful in 1h5m29s
Build and test / Layer separation (push) Successful in 29s
🐳 Android image / Build and push (push) Successful in 5s
Build and test / android-image (push) Successful in 8s
🐳 Windows image / Build and push (push) Successful in 5s
Build and test / windows-image (push) Successful in 7s
Manual pages / Publish the manual (push) Successful in 1m2s
Traceability / Requirement traces (push) Successful in 1m1s
Build and test / Android (aarch64) (push) Successful in 23m57s
Build and test / Windows (x86_64, cross) (push) Successful in 55m49s
Build and test / Publish the release (push) Skipped
Offer the manual as PDF and EPUB, laid out for print
The page gains a print stylesheet: A4 with a cover, a contents page whose
entries carry their page numbers, a chapter to a page with its name at the
head of each page and the page count at the foot, figures and table rows
never split, and the light palette. A line above the title links the PDF
and the EPUB on the published site, absolutely, since an installed copy
has none beside it.

The Manual pages workflow makes both: tools/manual/pdf.py renders the page
with WeasyPrint from JPEG copies of its pictures, which takes the PDF from
28 MB to 8, and pandoc makes the EPUB from the Markdown.
2026-10-08 22:41:07 -04:00

58 lines
2.1 KiB
Python

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