DarkRoom, shown
A tour of what the application does, one picture per thing. Every image on this page was captured from the desktop build driving itself — nothing is a diff --git a/tools/manual/pdf.py b/tools/manual/pdf.py new file mode 100644 index 0000000..9a27b62 --- /dev/null +++ b/tools/manual/pdf.py @@ -0,0 +1,57 @@ +#!/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]) diff --git a/tools/traceability/src/manual.rs b/tools/traceability/src/manual.rs index 5ec330c..034ee78 100644 --- a/tools/traceability/src/manual.rs +++ b/tools/traceability/src/manual.rs @@ -39,6 +39,16 @@ use pulldown_cmark::{CowStr, Event, HeadingLevel, Options, Parser, Tag, TagEnd}; /// is where the reader of a design document is anyway. pub const FORGE_TREE: &str = "https://gitea.tourolle.paris/dtourolle/DarkRoom/src/branch/master/"; +/// Where the manual is published, with its PDF and EPUB beside the page +/// (`.gitea/workflows/manual-pages.yml`). The page links them absolutely: an +/// installed copy has no PDF next to it, and a download is a thing done +/// online anyway. +pub const PAGES_SITE: &str = "https://pages.tourolle.paris/dtourolle/darkroom/"; + +/// The downloads' file names on [`PAGES_SITE`], which the workflow writes. +pub const PDF_NAME: &str = "darkroom-manual.pdf"; +pub const EPUB_NAME: &str = "darkroom-manual.epub"; + /// The manual's directory inside the repository, for resolving its links. const MANUAL_DIR: &str = "docs/manual"; @@ -274,11 +284,22 @@ pub fn render_html(markdown: &str) -> String { page.push_str("\n\n
\n{}
\ +The DarkRoom manual · {PAGES_SITE}