From 8123863897f4aa26cc1d87508da95fa0ac18ca4b Mon Sep 17 00:00:00 2001 From: Mike MacCana Date: Tue, 6 Oct 2026 20:22:12 +0000 Subject: [PATCH 1/2] Add build-pdf-preview: a free sample up to Part I Builds the digital edition, keeps every page before the Part I title page, and appends a closing page inviting the reader to ask for the whole book by email. Links to pages the sample leaves out are removed. Claude-Session: https://claude.ai/code/session_0135186xHwGnGQgC4WZb62C4 --- PRINT-SPECIFICATION.md | 3 +- README.md | 2 +- package.json | 1 + scripts/build-preview.mjs | 218 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 222 insertions(+), 2 deletions(-) create mode 100644 scripts/build-preview.mjs diff --git a/PRINT-SPECIFICATION.md b/PRINT-SPECIFICATION.md index 986b1b74..fb4a24e4 100644 --- a/PRINT-SPECIFICATION.md +++ b/PRINT-SPECIFICATION.md @@ -106,6 +106,7 @@ It is not a book size and is not what gets printed. Letter's margins leave a 6.3 npm install npm run build-pdf-digital # the finished book for screens: cover and links, no draft markings npm run build-pdf-paperback # the KDP upload: the interior (no cover, no links, no draft markings), then the cover sized to it +npm run build-pdf-preview # the free sample: every page before Part I, then a closing page asking for an email address npm run build-pdf-draft -- --reviewer "..." # advance copy: name and commit hash on the title page and every folio npm run build-pdf-draft -- --reviewer "..." --size letter # the same at US Letter npm run build-pdf-digital -- out.pdf # explicit output path @@ -117,7 +118,7 @@ npm run preview -- --chapter escrow --pdf "build/Building Financial Softw An unrecognized switch, a `--size` or `--reviewer` with no value or an unknown size, and more than one output path all print the usage and exit nonzero, rather than taking the stray argument for a filename. So does a draft with no `--reviewer`, since a review copy nobody can trace is the one the stamp exists to prevent, and a final build with one, since a review copy is not final. -Each build writes its own file in `build/`: the digital edition is `Building Financial Software on Solana.pdf`, and every other build adds a label, `(paperback interior)`, `(draft, )` or `(letter)`, so one build never overwrites another. Beside each PDF goes a page map of the same name ending `.pages.json`, listing each chapter's file, title, and first and last page; `npm run preview -- --chapter` reads it, so a chapter preview breaks pages exactly where that build does. Previews land in `build/` as `-p.png`. The three scripts set `FINAL=1`, `NO_COVER=1` and `NO_LINKS=1` for `scripts/build-pdf.mjs`, and those still compose by hand for an unusual combination: `FINAL=1 NO_COVER=1 node scripts/build-pdf.mjs --size letter` is a cover-less final Letter build. +Each build writes its own file in `build/`: the digital edition is `Building Financial Software on Solana.pdf`, and every other build adds a label, `(paperback interior)`, `(draft, )` or `(letter)`, so one build never overwrites another. `build-pdf-preview` builds the digital edition, then writes `(preview)`: that edition's own pages up to the Part I title page (the cover, the front matter, the contents and the Introduction), followed by a page inviting the reader to ask for the whole book. The cut is found by the part title's text, so it moves with the front matter. Contents and cross-reference links to pages the preview leaves out are removed rather than left pointing nowhere. Beside each PDF goes a page map of the same name ending `.pages.json`, listing each chapter's file, title, and first and last page; `npm run preview -- --chapter` reads it, so a chapter preview breaks pages exactly where that build does. Previews land in `build/` as `-p.png`. The three scripts set `FINAL=1`, `NO_COVER=1` and `NO_LINKS=1` for `scripts/build-pdf.mjs`, and those still compose by hand for an unusual combination: `FINAL=1 NO_COVER=1 node scripts/build-pdf.mjs --size letter` is a cover-less final Letter build. **The interior uploaded to KDP carries no links.** A link in a PDF is an annotation laid over the page rather than ink on it, and KDP removes every one it finds, then reports each page it removed one from as "non-printable markup" to be checked: every table of contents page, and every page with a cross-reference or an address. Nothing printed changes when they go, because every address is printed in full after its link text, but the report cannot say so. `NO_LINKS=1` builds the file without them, and without the named destinations the internal links pointed at, so KDP has nothing to remove. Every other build keeps them, for reading on screen. diff --git a/README.md b/README.md index 7b07e990..3dddcce7 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ The book's home is [solanabook.org](https://solanabook.org), which hosts the pub ## Building the PDF -`npm install` then `npm run build-pdf-digital` renders the whole manuscript, in table-of-contents order, as a single-column textbook PDF (serif type, running headers, page numbers, a contents page with page numbers) into `build/`. The page is 7.44 x 9.69in, the KDP standard trim closest in proportion to the 7 x 9.19in size technical books are usually printed at. Three builds share it: `build-pdf-digital` is the finished book for reading on screen, `build-pdf-paperback` the interior uploaded to KDP, and `build-pdf-draft -- --reviewer ""` an advance copy stamped for one reviewer, which takes `--size letter` for a reviewer printing on an office printer. `npm run resize-cover` sizes the full-wrap cover export (`assets/cover-wrap.pdf`) to KDP's exact requirement, spine width included. [PRINT-SPECIFICATION.md](PRINT-SPECIFICATION.md) records the trim, margins, type, cover math, and the rest of the physical book. Requires a Chromium binary: `CHROMIUM_PATH` if set, otherwise `/opt/pw-browsers/chromium`, otherwise `/opt/homebrew/bin/chromium` (`brew install chromium` on a Mac). `npm run preview` renders pages of a built PDF to PNG, either the page carrying a phrase or a whole chapter; [PRINT-SPECIFICATION.md](PRINT-SPECIFICATION.md) lists its options. +`npm install` then `npm run build-pdf-digital` renders the whole manuscript, in table-of-contents order, as a single-column textbook PDF (serif type, running headers, page numbers, a contents page with page numbers) into `build/`. The page is 7.44 x 9.69in, the KDP standard trim closest in proportion to the 7 x 9.19in size technical books are usually printed at. Three builds share it: `build-pdf-digital` is the finished book for reading on screen, `build-pdf-paperback` the interior uploaded to KDP, and `build-pdf-draft -- --reviewer ""` an advance copy stamped for one reviewer, which takes `--size letter` for a reviewer printing on an office printer. `build-pdf-preview` cuts a free sample from the digital edition: everything before Part I, then a closing page asking for the reader's email address. `npm run resize-cover` sizes the full-wrap cover export (`assets/cover-wrap.pdf`) to KDP's exact requirement, spine width included. [PRINT-SPECIFICATION.md](PRINT-SPECIFICATION.md) records the trim, margins, type, cover math, and the rest of the physical book. Requires a Chromium binary: `CHROMIUM_PATH` if set, otherwise `/opt/pw-browsers/chromium`, otherwise `/opt/homebrew/bin/chromium` (`brew install chromium` on a Mac). `npm run preview` renders pages of a built PDF to PNG, either the page carrying a phrase or a whole chapter; [PRINT-SPECIFICATION.md](PRINT-SPECIFICATION.md) lists its options. ## Preparing for press diff --git a/package.json b/package.json index 2856cf05..ac9d1ac7 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "scripts": { "build-pdf-digital": "FINAL=1 node scripts/build-pdf.mjs", "build-pdf-draft": "node scripts/build-pdf.mjs", + "build-pdf-preview": "npm run build-pdf-digital && node scripts/build-preview.mjs", "build-pdf-paperback": "FINAL=1 NO_COVER=1 NO_LINKS=1 node scripts/build-pdf.mjs && node scripts/resize-cover.mjs", "lint": "node scripts/lint-manuscript.mjs", "measure-slot-drift": "node scripts/measure-slot-drift.mjs", diff --git a/scripts/build-preview.mjs b/scripts/build-preview.mjs new file mode 100644 index 00000000..29863095 --- /dev/null +++ b/scripts/build-preview.mjs @@ -0,0 +1,218 @@ +#!/usr/bin/env node +// Cut a free preview from the digital edition: every page before the Part I +// title page (the cover, the front matter, the contents and the Introduction), +// followed by one closing page inviting the reader to ask for the whole book. +// +// The pages are the digital edition's own, copied rather than rebuilt, so the +// preview breaks exactly where the book does. The closing page is printed by +// Chromium in the book's faces at the book's trim and appended. +// +// Usage: npm run build-pdf-preview (builds the digital edition first) +// node scripts/build-preview.mjs [--pdf ] [output.pdf] + +import { readFileSync, writeFileSync, mkdtempSync, mkdirSync, existsSync } from "node:fs"; +import { join, dirname, resolve } from "node:path"; +import { tmpdir } from "node:os"; +import { fileURLToPath, pathToFileURL } from "node:url"; +import { chromium } from "playwright-core"; +import { PDFArray, PDFDict, PDFDocument, PDFName, PDFRef } from "pdf-lib"; + +const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +const TITLE = "Building Financial Software on Solana"; + +const USAGE = `Usage: npm run build-pdf-preview + node scripts/build-preview.mjs [--pdf ] [output.pdf] + + Reads the digital edition (npm run build-pdf-digital), keeps every page + before the Part I title page, and appends a closing page. Writes + "build/${TITLE} (preview).pdf" unless an output path is given. + + --pdf Cut the preview from this build instead of the digital edition. + --help Print this and exit.`; + +const die = (message) => { + console.error(`${message}\n\n${USAGE}`); + process.exit(1); +}; + +const argv = process.argv.slice(2).filter((arg) => arg !== "--"); +let sourcePath = join(repoRoot, "build", `${TITLE}.pdf`); +const positional = []; +for (let i = 0; i < argv.length; i++) { + const arg = argv[i]; + if (arg === "--help" || arg === "-h") { + console.log(USAGE); + process.exit(0); + } else if (arg === "--pdf") { + if (i + 1 >= argv.length) die("--pdf needs a value."); + sourcePath = resolve(argv[++i]); + } else if (arg.startsWith("-")) { + die(`Unknown option ${JSON.stringify(arg)}.`); + } else { + positional.push(arg); + } +} +if (positional.length > 1) die(`Expected one output path, got ${positional.length}: ${positional.join(", ")}`); +if (!existsSync(sourcePath)) die(`No PDF at ${sourcePath}; run npm run build-pdf-digital first.`); +const outputPath = resolve(positional[0] ?? join(repoRoot, "build", `${TITLE} (preview).pdf`)); +mkdirSync(dirname(outputPath), { recursive: true }); + +// The cut falls at the Part I title page. Its page comes from the page map the +// build writes beside the PDF: the title page is the first page after the last +// chapter outside the parts, found by its text, so a page-count change in the +// front matter or the Introduction moves the cut with it. The search starts at +// the Introduction's last page because the contents also print the part's +// title. The label is matched with its spaces taken out: it is set in spaced +// capitals, which pdf.js reads back as "P A R T I". +const sourceBytes = readFileSync(sourcePath); +const mapPath = sourcePath.replace(/\.pdf$/i, "") + ".pages.json"; +if (!existsSync(mapPath)) die(`No page map at ${mapPath}; rebuild the PDF to write one.`); +const chapters = JSON.parse(readFileSync(mapPath, "utf8")); +const introduction = chapters.find(({ path }) => path === "prelude/introduction.md"); +if (!introduction) die(`The page map has no prelude/introduction.md, so the end of the preview cannot be found.`); + +const pdfjs = await import(pathToFileURL(join(repoRoot, "node_modules", "pdfjs-dist", "legacy", "build", "pdf.mjs")).href); +const loading = pdfjs.getDocument({ data: new Uint8Array(sourceBytes) }); +const reader = await loading.promise; +let partPage = null; +for (let number = introduction.last + 1; number <= reader.numPages && partPage === null; number++) { + const text = (await (await reader.getPage(number)).getTextContent()).items.map((item) => item.str ?? "").join(""); + if (/PARTI(?!I)/i.test(text.replace(/\s+/g, "")) && /The\s*Fundamentals/.test(text)) partPage = number; +} +await loading.destroy(); +if (partPage === null) die(`No page after the Introduction (page ${introduction.last}) carries the Part I title.`); +const keptPages = partPage - 1; + +// The closing page, at the trim the digital edition was printed at, centered +// both ways in the space the body text occupies, in the book's own faces: the +// heading in Denim INK like every heading in the book, the text in Gelasio. +const source = await PDFDocument.load(sourceBytes); +const { width: trimWidth, height: trimHeight } = source.getPage(0).getSize(); +const fontUrl = (file) => pathToFileURL(join(repoRoot, "assets", "fonts", file)).href; +const closingHtml = ` + + + +Want to keep reading? + + + +
+

End of preview

+

Want to keep reading?

+
+

You’ve reached the end of the preview!

+

For a full copy of ${TITLE}, please provide your email address and we will email it to you.

+
+ +`; + +const workDir = mkdtempSync(join(tmpdir(), "solana-book-preview-")); +const closingPath = join(workDir, "closing.html"); +writeFileSync(closingPath, closingHtml); +const chromiumPath = + process.env.CHROMIUM_PATH ?? (existsSync("/opt/pw-browsers/chromium") ? "/opt/pw-browsers/chromium" : "/opt/homebrew/bin/chromium"); +const browser = await chromium.launch({ executablePath: chromiumPath, args: ["--allow-file-access-from-files"] }); +const page = await browser.newPage(); +await page.goto(pathToFileURL(closingPath).href); +await page.evaluate(() => document.fonts.ready); +const closingBytes = await page.pdf({ preferCSSPageSize: true }); +await browser.close(); +const closing = await PDFDocument.load(closingBytes); +if (closing.getPageCount() !== 1) die(`The closing page printed as ${closing.getPageCount()} pages.`); + +// The kept pages are copied into a new document rather than the rest being +// removed from the edition: pdf-lib saves every object a document holds, +// referenced or not, so a cut edition still carried the whole book's pages and +// was as large as it. Copying leaves the catalog's named destinations behind, +// so those pointing at a kept page are carried over to its copy, and the +// contents' links into the Introduction still work. A link to a page the +// preview leaves out would go nowhere, so it is dropped. +const preview = await PDFDocument.create(); +const keptIndices = Array.from({ length: keptPages }, (_, index) => index); +const copies = await preview.copyPages(source, keptIndices); +const copyOf = new Map(keptIndices.map((index) => [source.getPage(index).ref.toString(), copies[index].ref])); +for (const copy of copies) preview.addPage(copy); + +const nameOf = (name) => name.decodeText?.() ?? name.toString(); +const liveDests = new Map(); +const sourceDests = source.catalog.lookupMaybe(PDFName.of("Dests"), PDFDict); +for (const [name, value] of sourceDests?.entries() ?? []) { + let target = source.context.lookup(value); + if (target instanceof PDFDict) target = source.context.lookup(target.get(PDFName.of("D"))); + if (!(target instanceof PDFArray)) continue; + const pageRef = target.get(0); + if (!(pageRef instanceof PDFRef) || !copyOf.has(pageRef.toString())) continue; + // The rest of a destination is a fit type and coordinates: names and + // numbers, which belong to no document and copy as they are. + liveDests.set(nameOf(name), [copyOf.get(pageRef.toString()), ...target.asArray().slice(1)]); +} +preview.catalog.set( + PDFName.of("Dests"), + preview.context.obj(Object.fromEntries([...liveDests].map(([name, dest]) => [name, preview.context.obj(dest)]))), +); + +let droppedLinks = 0; +for (const sheet of preview.getPages()) { + const annotations = sheet.node.lookupMaybe(PDFName.of("Annots"), PDFArray); + if (!annotations) continue; + const kept = annotations.asArray().filter((entry) => { + const dest = preview.context.lookup(entry).get(PDFName.of("Dest")); + if (dest === undefined) return true; // an address, which works anywhere + if (liveDests.has(nameOf(preview.context.lookup(dest)))) return true; + droppedLinks++; + return false; + }); + sheet.node.set(PDFName.of("Annots"), preview.context.obj(kept)); +} + +const [closingPage] = await preview.copyPages(closing, [0]); +closingPage.setMediaBox(0, 0, trimWidth, trimHeight); +closingPage.setCropBox(0, 0, trimWidth, trimHeight); +preview.addPage(closingPage); +preview.setTitle(`${TITLE} (preview)`); +preview.setAuthor("Mike MacCana"); +// A viewer's title bar shows the document title rather than the file name, as +// the edition's does. +preview.catalog.set( + PDFName.of("ViewerPreferences"), + preview.context.obj({ Type: "ViewerPreferences", DisplayDocTitle: true }), +); + +writeFileSync(outputPath, await preview.save()); +console.log(`Wrote "${outputPath}"`); +console.log(`${keptPages} page(s) of the book, up to the Part I title page on page ${partPage}, then the closing page.`); +if (droppedLinks > 0) console.log(`Unlinked ${droppedLinks} contents or cross-reference link(s) to pages the preview leaves out.`); From 8cb1f2950b21a4b6e5d0ac0a72cf617ce806ce4f Mon Sep 17 00:00:00 2001 From: Mike MacCana Date: Tue, 6 Oct 2026 20:33:52 +0000 Subject: [PATCH 2/2] Rename the preview build to build-pdf-website-preview Says where the file is for. The output is now build/Building Financial Software on Solana (website preview).pdf; the title a reader's viewer shows stays "(preview)". Claude-Session: https://claude.ai/code/session_0135186xHwGnGQgC4WZb62C4 --- PRINT-SPECIFICATION.md | 4 ++-- README.md | 2 +- package.json | 2 +- ...-preview.mjs => build-website-preview.mjs} | 21 +++++++++++-------- 4 files changed, 16 insertions(+), 13 deletions(-) rename scripts/{build-preview.mjs => build-website-preview.mjs} (92%) diff --git a/PRINT-SPECIFICATION.md b/PRINT-SPECIFICATION.md index fb4a24e4..7d4e354c 100644 --- a/PRINT-SPECIFICATION.md +++ b/PRINT-SPECIFICATION.md @@ -106,7 +106,7 @@ It is not a book size and is not what gets printed. Letter's margins leave a 6.3 npm install npm run build-pdf-digital # the finished book for screens: cover and links, no draft markings npm run build-pdf-paperback # the KDP upload: the interior (no cover, no links, no draft markings), then the cover sized to it -npm run build-pdf-preview # the free sample: every page before Part I, then a closing page asking for an email address +npm run build-pdf-website-preview # the free sample for the website: every page before Part I, then a closing page asking for an email address npm run build-pdf-draft -- --reviewer "..." # advance copy: name and commit hash on the title page and every folio npm run build-pdf-draft -- --reviewer "..." --size letter # the same at US Letter npm run build-pdf-digital -- out.pdf # explicit output path @@ -118,7 +118,7 @@ npm run preview -- --chapter escrow --pdf "build/Building Financial Softw An unrecognized switch, a `--size` or `--reviewer` with no value or an unknown size, and more than one output path all print the usage and exit nonzero, rather than taking the stray argument for a filename. So does a draft with no `--reviewer`, since a review copy nobody can trace is the one the stamp exists to prevent, and a final build with one, since a review copy is not final. -Each build writes its own file in `build/`: the digital edition is `Building Financial Software on Solana.pdf`, and every other build adds a label, `(paperback interior)`, `(draft, )` or `(letter)`, so one build never overwrites another. `build-pdf-preview` builds the digital edition, then writes `(preview)`: that edition's own pages up to the Part I title page (the cover, the front matter, the contents and the Introduction), followed by a page inviting the reader to ask for the whole book. The cut is found by the part title's text, so it moves with the front matter. Contents and cross-reference links to pages the preview leaves out are removed rather than left pointing nowhere. Beside each PDF goes a page map of the same name ending `.pages.json`, listing each chapter's file, title, and first and last page; `npm run preview -- --chapter` reads it, so a chapter preview breaks pages exactly where that build does. Previews land in `build/` as `-p.png`. The three scripts set `FINAL=1`, `NO_COVER=1` and `NO_LINKS=1` for `scripts/build-pdf.mjs`, and those still compose by hand for an unusual combination: `FINAL=1 NO_COVER=1 node scripts/build-pdf.mjs --size letter` is a cover-less final Letter build. +Each build writes its own file in `build/`: the digital edition is `Building Financial Software on Solana.pdf`, and every other build adds a label, `(paperback interior)`, `(draft, )` or `(letter)`, so one build never overwrites another. `build-pdf-website-preview` builds the digital edition, then writes `(website preview)`: that edition's own pages up to the Part I title page (the cover, the front matter, the contents and the Introduction), followed by a page inviting the reader to ask for the whole book. The cut is found by the part title's text, so it moves with the front matter. Contents and cross-reference links to pages the preview leaves out are removed rather than left pointing nowhere. Beside each PDF goes a page map of the same name ending `.pages.json`, listing each chapter's file, title, and first and last page; `npm run preview -- --chapter` reads it, so a chapter preview breaks pages exactly where that build does. Previews land in `build/` as `-p.png`. The three scripts set `FINAL=1`, `NO_COVER=1` and `NO_LINKS=1` for `scripts/build-pdf.mjs`, and those still compose by hand for an unusual combination: `FINAL=1 NO_COVER=1 node scripts/build-pdf.mjs --size letter` is a cover-less final Letter build. **The interior uploaded to KDP carries no links.** A link in a PDF is an annotation laid over the page rather than ink on it, and KDP removes every one it finds, then reports each page it removed one from as "non-printable markup" to be checked: every table of contents page, and every page with a cross-reference or an address. Nothing printed changes when they go, because every address is printed in full after its link text, but the report cannot say so. `NO_LINKS=1` builds the file without them, and without the named destinations the internal links pointed at, so KDP has nothing to remove. Every other build keeps them, for reading on screen. diff --git a/README.md b/README.md index 3dddcce7..271d3cc9 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ The book's home is [solanabook.org](https://solanabook.org), which hosts the pub ## Building the PDF -`npm install` then `npm run build-pdf-digital` renders the whole manuscript, in table-of-contents order, as a single-column textbook PDF (serif type, running headers, page numbers, a contents page with page numbers) into `build/`. The page is 7.44 x 9.69in, the KDP standard trim closest in proportion to the 7 x 9.19in size technical books are usually printed at. Three builds share it: `build-pdf-digital` is the finished book for reading on screen, `build-pdf-paperback` the interior uploaded to KDP, and `build-pdf-draft -- --reviewer ""` an advance copy stamped for one reviewer, which takes `--size letter` for a reviewer printing on an office printer. `build-pdf-preview` cuts a free sample from the digital edition: everything before Part I, then a closing page asking for the reader's email address. `npm run resize-cover` sizes the full-wrap cover export (`assets/cover-wrap.pdf`) to KDP's exact requirement, spine width included. [PRINT-SPECIFICATION.md](PRINT-SPECIFICATION.md) records the trim, margins, type, cover math, and the rest of the physical book. Requires a Chromium binary: `CHROMIUM_PATH` if set, otherwise `/opt/pw-browsers/chromium`, otherwise `/opt/homebrew/bin/chromium` (`brew install chromium` on a Mac). `npm run preview` renders pages of a built PDF to PNG, either the page carrying a phrase or a whole chapter; [PRINT-SPECIFICATION.md](PRINT-SPECIFICATION.md) lists its options. +`npm install` then `npm run build-pdf-digital` renders the whole manuscript, in table-of-contents order, as a single-column textbook PDF (serif type, running headers, page numbers, a contents page with page numbers) into `build/`. The page is 7.44 x 9.69in, the KDP standard trim closest in proportion to the 7 x 9.19in size technical books are usually printed at. Three builds share it: `build-pdf-digital` is the finished book for reading on screen, `build-pdf-paperback` the interior uploaded to KDP, and `build-pdf-draft -- --reviewer ""` an advance copy stamped for one reviewer, which takes `--size letter` for a reviewer printing on an office printer. `build-pdf-website-preview` cuts the free sample offered on the website from the digital edition: everything before Part I, then a closing page asking for the reader's email address. `npm run resize-cover` sizes the full-wrap cover export (`assets/cover-wrap.pdf`) to KDP's exact requirement, spine width included. [PRINT-SPECIFICATION.md](PRINT-SPECIFICATION.md) records the trim, margins, type, cover math, and the rest of the physical book. Requires a Chromium binary: `CHROMIUM_PATH` if set, otherwise `/opt/pw-browsers/chromium`, otherwise `/opt/homebrew/bin/chromium` (`brew install chromium` on a Mac). `npm run preview` renders pages of a built PDF to PNG, either the page carrying a phrase or a whole chapter; [PRINT-SPECIFICATION.md](PRINT-SPECIFICATION.md) lists its options. ## Preparing for press diff --git a/package.json b/package.json index ac9d1ac7..0c2f6bcd 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "scripts": { "build-pdf-digital": "FINAL=1 node scripts/build-pdf.mjs", "build-pdf-draft": "node scripts/build-pdf.mjs", - "build-pdf-preview": "npm run build-pdf-digital && node scripts/build-preview.mjs", + "build-pdf-website-preview": "npm run build-pdf-digital && node scripts/build-website-preview.mjs", "build-pdf-paperback": "FINAL=1 NO_COVER=1 NO_LINKS=1 node scripts/build-pdf.mjs && node scripts/resize-cover.mjs", "lint": "node scripts/lint-manuscript.mjs", "measure-slot-drift": "node scripts/measure-slot-drift.mjs", diff --git a/scripts/build-preview.mjs b/scripts/build-website-preview.mjs similarity index 92% rename from scripts/build-preview.mjs rename to scripts/build-website-preview.mjs index 29863095..4c127464 100644 --- a/scripts/build-preview.mjs +++ b/scripts/build-website-preview.mjs @@ -1,14 +1,15 @@ #!/usr/bin/env node -// Cut a free preview from the digital edition: every page before the Part I -// title page (the cover, the front matter, the contents and the Introduction), -// followed by one closing page inviting the reader to ask for the whole book. +// Cut the free preview offered on the book's website from the digital edition: +// every page before the Part I title page (the cover, the front matter, the +// contents and the Introduction), followed by one closing page inviting the +// reader to ask for the whole book. // // The pages are the digital edition's own, copied rather than rebuilt, so the // preview breaks exactly where the book does. The closing page is printed by // Chromium in the book's faces at the book's trim and appended. // -// Usage: npm run build-pdf-preview (builds the digital edition first) -// node scripts/build-preview.mjs [--pdf ] [output.pdf] +// Usage: npm run build-pdf-website-preview (builds the digital edition first) +// node scripts/build-website-preview.mjs [--pdf ] [output.pdf] import { readFileSync, writeFileSync, mkdtempSync, mkdirSync, existsSync } from "node:fs"; import { join, dirname, resolve } from "node:path"; @@ -20,12 +21,12 @@ import { PDFArray, PDFDict, PDFDocument, PDFName, PDFRef } from "pdf-lib"; const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), ".."); const TITLE = "Building Financial Software on Solana"; -const USAGE = `Usage: npm run build-pdf-preview - node scripts/build-preview.mjs [--pdf ] [output.pdf] +const USAGE = `Usage: npm run build-pdf-website-preview + node scripts/build-website-preview.mjs [--pdf ] [output.pdf] Reads the digital edition (npm run build-pdf-digital), keeps every page before the Part I title page, and appends a closing page. Writes - "build/${TITLE} (preview).pdf" unless an output path is given. + "build/${TITLE} (website preview).pdf" unless an output path is given. --pdf Cut the preview from this build instead of the digital edition. --help Print this and exit.`; @@ -54,7 +55,7 @@ for (let i = 0; i < argv.length; i++) { } if (positional.length > 1) die(`Expected one output path, got ${positional.length}: ${positional.join(", ")}`); if (!existsSync(sourcePath)) die(`No PDF at ${sourcePath}; run npm run build-pdf-digital first.`); -const outputPath = resolve(positional[0] ?? join(repoRoot, "build", `${TITLE} (preview).pdf`)); +const outputPath = resolve(positional[0] ?? join(repoRoot, "build", `${TITLE} (website preview).pdf`)); mkdirSync(dirname(outputPath), { recursive: true }); // The cut falls at the Part I title page. Its page comes from the page map the @@ -203,6 +204,8 @@ const [closingPage] = await preview.copyPages(closing, [0]); closingPage.setMediaBox(0, 0, trimWidth, trimHeight); closingPage.setCropBox(0, 0, trimWidth, trimHeight); preview.addPage(closingPage); +// The title a reader's viewer shows: "website" says where the file is offered, +// which is ours to know rather than theirs. preview.setTitle(`${TITLE} (preview)`); preview.setAuthor("Mike MacCana"); // A viewer's title bar shows the document title rather than the file name, as