/** * Handing the book to the printer. * * ── Why the browser prints it ──────────────────────────────────────────── * * The alternative was a renderer on the server — headless Chromium, or a PDF * library drawing the pages a second time. Both were rejected for the same * reason: TREK is self-hosted, and neither Chromium in the image nor a parallel * renderer to proofread against is a cost worth carrying when the browser * already produces exactly what the editor shows. It is also how TREK's other * exports work, so this is the house style rather than a new one. * * What comes out is a real PDF: vector text, embedded fonts, images at full * resolution. What does not come out is PDF metadata and the TrimBox/BleedBox * entries — a browser sets neither. Which is why the sheets carry crop marks: * a press reads where to cut from the marks, and has done for rather longer * than the PDF format has existed. * * ── Why an iframe rather than a window ─────────────────────────────────── * * Same reason as the Journey export: `window.open` is blocked by Safari on iOS * inside an async callback, and `window.close` does not work reliably in a * standalone PWA. An overlay with a `srcdoc` iframe works everywhere. */ import { whenReady } from './printReady' /* * The only literal colours in Studio, and deliberately so. * * The print view is a document of its own — a `srcdoc` iframe that inherits * none of the app's tokens — and paper is white whatever theme the editor is * in. Naming them here keeps the stylesheets below readable and puts the * exception in one place instead of on every rule. */ const PAPER = '#ffffff' // theme-lint-disable — paper, not app chrome const WORKTOP = '#52525b' // theme-lint-disable — the grey the sheets sit on const BAR = '#0f172a' // theme-lint-disable — the print view's own header const BAR_LINE = '#e4e4e7' // theme-lint-disable — and the line under it /** Everything the print document needs, gathered before the iframe exists. */ export interface PrintSheetsInput { /** The rendered sheets, as HTML. */ html: string /** Sheet size in millimetres, bleed and crop-mark room included. */ sheetWidth: number sheetHeight: number /** * The size of a one-page sheet, when the document also holds two-page ones. * * Spread mode mixes them: the covers are single leaves and everything between * is a spread. Omitted when every sheet is the same size, which is the whole * of page mode. */ singleWidth?: number singleHeight?: number title: string /** Chrome for the overlay itself. */ labels: { save: string; close: string; count: string; preparing: string } } /** * Every stylesheet the app is currently using, as markup for the iframe head. * * Copied rather than re-listed. The sheets are the editor's own components, so * they need the editor's own CSS — including the `@font-face` rules for the * seven bundled families, which is the difference between a book that prints in * the typeface it was proofread in and one that prints in a substitute. * * In a build that is one ``; under Vite it is a pile of inline ``) } } return out.join('\n') } function escapeAttr(value: string): string { return value.replace(/&/g, '&').replace(/"/g, '"').replace(//g, '>') } /** * Open the print view. * * Resolves once the document is on screen and ready to print — which is *after* * its images and fonts have loaded, not merely after the markup is in place. * Printing early is how a book comes out with blank rectangles where the * photographs were. */ export function printSheets(input: PrintSheetsInput): () => void { /* * The document's language, carried over from the editor. * * Not decoration: `hyphens: auto` does nothing at all until the browser knows * which language's rules to break the words by, and it reads that from the * nearest `lang`. Without it a heading the editor had set as "Lorem ip-sum" * came out unhyphenated in the export, ran past its own frame and was clipped * — the one place where what you proofread and what you print disagreed. * TranslationContext sets this on the app, so taking it from there is what * makes the two agree by construction rather than by a second setting. */ const lang = document.documentElement.lang || 'en' const doc = `