mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-20 05:56:53 +00:00
87c73e50fb
A new book was a cover, a spread and a back cover, so it opened straight onto pages 2 and 3. A bound book does not: the inside of the cover is blank, and the first thing facing the reader is a single right-hand page, page 1. The same at the end, where a single left-hand page sits before the back cover. Both are laid down now, empty, in a new book and in a laid-out one, and they are fixed the way the covers are: an inner spread only ever goes between them. The page numbers follow. They were counted from a spread's position in the array, which assumed the spread after the cover was the first, and three places did that sum on their own. One function now counts along the book, one page for a single leaf and two for a spread, so the sheet on screen and the label in the print view and the folio on the page all say the same number. A book with a first page starts at 1; a book from before the first page existed has none and keeps the numbers it always had.
183 lines
6.3 KiB
TypeScript
183 lines
6.3 KiB
TypeScript
import type { BookDocument, BookSpread } from '@trek/shared'
|
||
|
||
/**
|
||
* Cutting a book into printable sheets.
|
||
*
|
||
* ── Why a sheet is not a spread ──────────────────────────────────────────
|
||
*
|
||
* The document is made of spreads, because that is the unit a person designs
|
||
* in: a picture crossing the gutter is one decision, not two. A printer takes
|
||
* pages — single leaves, each with its own bleed on all four sides, in reading
|
||
* order. So the export cuts every inner spread down the middle and hands over
|
||
* the halves.
|
||
*
|
||
* The cut is a window, not a re-layout. Each sheet shows the same spread
|
||
* through an opening the size of one page, shifted by a page width for the
|
||
* right-hand leaf, which is why a photograph that crosses the gutter comes out
|
||
* of the press aligned: both halves were positioned by the same numbers.
|
||
*
|
||
* ── Why the spread mode exists anyway ────────────────────────────────────
|
||
*
|
||
* For reading, not for printing. A book PDF that opens two pages at a time is
|
||
* what you send someone to look at, and cutting it into leaves for that would
|
||
* make every photograph across the gutter look broken.
|
||
*
|
||
* ── Millimetres, all of it ───────────────────────────────────────────────
|
||
*
|
||
* Every number here is millimetres, matching the document. The renderer maps mm
|
||
* onto the PDF at a fixed ratio, so these are the sizes the press measures.
|
||
*/
|
||
|
||
/** How far a crop mark reaches out past the bleed. */
|
||
export const MARK_LENGTH = 4
|
||
|
||
/** How thick it is drawn — hairline, the way a mark should be. */
|
||
export const MARK_WEIGHT = 0.2
|
||
|
||
export type SheetMode = 'pages' | 'spreads'
|
||
|
||
export interface Sheet {
|
||
/** The spread this leaf is cut from. */
|
||
spread: BookSpread
|
||
/** Its index in the document, which is what the folios count from. */
|
||
spreadIndex: number
|
||
/**
|
||
* How far into the spread the window sits, in millimetres.
|
||
*
|
||
* 0 for a left-hand leaf, one page width for a right-hand one. It is a shift
|
||
* of the *view*, never of the contents.
|
||
*/
|
||
offset: number
|
||
/** Trim width of this leaf. */
|
||
width: number
|
||
/** Trim height, which never varies. */
|
||
height: number
|
||
/** Width of the whole spread behind the window, for positioning it. */
|
||
spreadWidth: number
|
||
/**
|
||
* Whether this leaf is one page rather than two.
|
||
*
|
||
* Covers are, in both modes. It matters to the print CSS: a document that
|
||
* mixes widths needs a second `@page` rule for the narrow sheets, or they get
|
||
* laid onto the wide page box with white either side.
|
||
*/
|
||
single: boolean
|
||
/** A label for the print view — "3", or "2 – 3" for a spread. */
|
||
label: string
|
||
}
|
||
|
||
/** How big one printed sheet is, bleed and crop marks included. */
|
||
export interface SheetBox {
|
||
/** Sheet width including bleed and any room for marks. */
|
||
width: number
|
||
height: number
|
||
/** Distance from the sheet edge to the trim edge. */
|
||
margin: number
|
||
/** The bleed itself, which is part of that margin. */
|
||
bleed: number
|
||
}
|
||
|
||
export function sheetBox(
|
||
trimWidth: number,
|
||
trimHeight: number,
|
||
bleed: number,
|
||
marks: boolean,
|
||
): SheetBox {
|
||
/*
|
||
* Marks are drawn outside the bleed, not inside it. Putting them in the bleed
|
||
* would mean printing them onto the part of the sheet that gets cut off —
|
||
* they would be trimmed away along with the thing they were marking.
|
||
*/
|
||
const margin = marks ? bleed + MARK_LENGTH : bleed
|
||
return {
|
||
width: trimWidth + margin * 2,
|
||
height: trimHeight + margin * 2,
|
||
margin,
|
||
bleed,
|
||
}
|
||
}
|
||
|
||
/**
|
||
* A book, as leaves.
|
||
*
|
||
* Covers stay whole in both modes: a cover is one page, and half a cover is
|
||
* not a thing anybody prints.
|
||
*/
|
||
export function sheetsFor(doc: BookDocument, mode: SheetMode): Sheet[] {
|
||
const { pageWidth, pageHeight, pageNumbers } = doc.page
|
||
const out: Sheet[] = []
|
||
|
||
doc.spreads.forEach((spread, spreadIndex) => {
|
||
const single = spread.role !== 'inner'
|
||
const spreadWidth = single ? pageWidth : pageWidth * 2
|
||
const folios = foliosOf(doc.spreads, spreadIndex, pageNumbers.startAt)
|
||
|
||
if (single || mode === 'spreads') {
|
||
out.push({
|
||
spread,
|
||
spreadIndex,
|
||
offset: 0,
|
||
width: spreadWidth,
|
||
height: pageHeight,
|
||
spreadWidth,
|
||
single,
|
||
label: folioLabel(folios),
|
||
})
|
||
return;
|
||
}
|
||
|
||
for (const half of [0, 1]) {
|
||
out.push({
|
||
spread,
|
||
spreadIndex,
|
||
offset: half * pageWidth,
|
||
width: pageWidth,
|
||
height: pageHeight,
|
||
spreadWidth,
|
||
single: true,
|
||
label: String(folios[half]),
|
||
})
|
||
}
|
||
})
|
||
|
||
return out
|
||
}
|
||
|
||
/** Which of a book's leaves carry a page number: everything inside the covers. */
|
||
export function isNumbered(role: BookSpread['role']): boolean {
|
||
return role !== 'cover' && role !== 'back'
|
||
}
|
||
|
||
/**
|
||
* The page numbers a spread carries, in reading order.
|
||
*
|
||
* Counted along the book rather than from the index: the covers carry none,
|
||
* a first or last page carries one, an inner spread two, and the first
|
||
* numbered page is `startAt`. A book from before the single first page
|
||
* existed has `startAt` 2 and no such page, so its first spread still reads
|
||
* 2 – 3; a book laid out since has a first page and starts at 1, so that page
|
||
* is 1 and the same spread behind it is 2 – 3 (#2317). Either way the number
|
||
* follows what is in the book, not where the spread sits in the array.
|
||
*
|
||
* The renderer and the print labels both come through here, so a number seen
|
||
* on screen is the one that is printed.
|
||
*
|
||
* Empty for a cover, and for an index past the end.
|
||
*/
|
||
export function foliosOf(spreads: readonly BookSpread[], spreadIndex: number, startAt: number): number[] {
|
||
const spread = spreads[spreadIndex]
|
||
if (!spread || !isNumbered(spread.role)) return []
|
||
let next = startAt
|
||
for (let i = 0; i < spreadIndex; i++) {
|
||
const role = spreads[i].role
|
||
if (!isNumbered(role)) continue
|
||
next += role === 'inner' ? 2 : 1
|
||
}
|
||
return spread.role === 'inner' ? [next, next + 1] : [next]
|
||
}
|
||
|
||
/** "3", or "2 – 3" for a spread; nothing for a cover. */
|
||
export function folioLabel(folios: readonly number[]): string {
|
||
return folios.length === 2 ? `${folios[0]} – ${folios[1]}` : folios.length === 1 ? String(folios[0]) : ''
|
||
}
|