@segnavia/format 0.25.0 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/book.d.ts CHANGED
@@ -93,12 +93,17 @@ export declare const BookView: z.ZodObject<{
93
93
  sandboxed: "sandboxed";
94
94
  }>>;
95
95
  lang: z.ZodOptional<z.ZodString>;
96
+ fonts: z.ZodOptional<z.ZodArray<z.ZodUnion<readonly [z.ZodObject<{
97
+ app: z.ZodString;
98
+ }, z.core.$strip>, z.ZodObject<{
99
+ local: z.ZodString;
100
+ fallback: z.ZodString;
101
+ }, z.core.$strip>]>>>;
96
102
  title: z.ZodOptional<z.ZodString>;
97
103
  html: z.ZodString;
98
104
  }, z.core.$strip>;
99
105
  export type BookView = z.infer<typeof BookView>;
100
106
  export type BookViewInput = z.input<typeof BookView>;
101
- export declare function isHebrew(lang: string | undefined): boolean;
102
107
  export declare const Book: z.ZodPipe<z.ZodObject<{
103
108
  title: z.ZodString;
104
109
  mode: z.ZodEnum<{
@@ -177,6 +182,12 @@ export declare const Book: z.ZodPipe<z.ZodObject<{
177
182
  html: z.ZodString;
178
183
  runningHead: z.ZodOptional<z.ZodString>;
179
184
  }, z.core.$strip>>;
185
+ fonts: z.ZodOptional<z.ZodArray<z.ZodUnion<readonly [z.ZodObject<{
186
+ app: z.ZodString;
187
+ }, z.core.$strip>, z.ZodObject<{
188
+ local: z.ZodString;
189
+ fallback: z.ZodString;
190
+ }, z.core.$strip>]>>>;
180
191
  numbering: z.ZodDefault<z.ZodObject<{
181
192
  frontMatter: z.ZodOptional<z.ZodEnum<{
182
193
  arabic: "arabic";
@@ -272,6 +283,12 @@ export declare const Book: z.ZodPipe<z.ZodObject<{
272
283
  } | undefined;
273
284
  dir?: "ltr" | "rtl" | "auto" | undefined;
274
285
  lang?: string | undefined;
286
+ fonts?: ({
287
+ app: string;
288
+ } | {
289
+ local: string;
290
+ fallback: string;
291
+ })[] | undefined;
275
292
  }, {
276
293
  title: string;
277
294
  mode: "continuous" | "paginated";
@@ -323,6 +340,12 @@ export declare const Book: z.ZodPipe<z.ZodObject<{
323
340
  } | undefined;
324
341
  dir?: "ltr" | "rtl" | "auto" | undefined;
325
342
  lang?: string | undefined;
343
+ fonts?: ({
344
+ app: string;
345
+ } | {
346
+ local: string;
347
+ fallback: string;
348
+ })[] | undefined;
326
349
  }>>;
327
350
  export type Book = z.infer<typeof Book>;
328
351
  export type BookInput = z.input<typeof Book>;
package/dist/book.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { Direction } from "./direction.js";
2
2
  import { Scripts } from "./scripts.js";
3
- import { NoteMarker, NumberSystem } from "./numbering.js";
3
+ import { FontReference } from "./fonts.js";
4
+ import { NoteMarker, NumberSystem, isHebrew } from "./numbering.js";
4
5
  import { Orientation, Paper, sheetMm } from "./paper.js";
5
6
  import { z } from "zod";
6
7
  //#region src/book.ts
@@ -42,6 +43,7 @@ const BookShape = z.object({
42
43
  lang: z.string().optional(),
43
44
  frontMatter: z.array(BookFrontMatterEntry).default([]),
44
45
  chapters: z.array(Chapter).min(1),
46
+ fonts: z.array(FontReference).optional(),
45
47
  numbering: z.object({
46
48
  frontMatter: NumberSystem.optional(),
47
49
  body: NumberSystem.optional()
@@ -68,15 +70,12 @@ const BookView = BookShape.pick({
68
70
  title: true,
69
71
  dir: true,
70
72
  lang: true,
71
- scripts: true
73
+ scripts: true,
74
+ fonts: true
72
75
  }).extend({
73
76
  title: BookShape.shape.title.optional(),
74
77
  html: z.string()
75
78
  });
76
- function isHebrew(lang) {
77
- const primary = lang?.split("-")[0]?.toLowerCase();
78
- return primary === "he" || primary === "iw";
79
- }
80
79
  const Book = BookShape.transform((book) => {
81
80
  const hebrew = isHebrew(book.lang);
82
81
  return {
@@ -137,4 +136,4 @@ function noteMarkerIssues(book, used) {
137
136
  return [`footnote and endnote markers are both ${book.footnotes.marker}; the two must differ when both kinds are used`];
138
137
  }
139
138
  //#endregion
140
- export { Book, BookContents, BookFrontMatterEntry, BookMode, BookPage, BookSection, BookShape, BookView, Chapter, isBookContents, isHebrew, noteMarkerIssues };
139
+ export { Book, BookContents, BookFrontMatterEntry, BookMode, BookPage, BookSection, BookShape, BookView, Chapter, isBookContents, noteMarkerIssues };
package/dist/fold.d.ts ADDED
@@ -0,0 +1,16 @@
1
+ import "./paper.js";
2
+ //#region src/fold.d.ts
3
+ export declare const FOLD_KINDS: readonly ["half", "letter", "z", "gate", "custom"];
4
+ export type FoldKind = (typeof FOLD_KINDS)[number];
5
+ export declare const FOLD_TUCK_IN_MM = 1.5;
6
+ export declare function foldColumns(fold: FoldKind, panelCount?: number): number;
7
+ export declare function foldPanelMap(fold: FoldKind, columns: number): {
8
+ front: number[];
9
+ back: number[];
10
+ };
11
+ export declare function foldCellPages(fold: FoldKind, columns: number): Array<{
12
+ front: number;
13
+ back: number;
14
+ }>;
15
+ export declare function foldPageCountMessage(fold: FoldKind, pageCount: number, columns: number): string;
16
+ //#endregion
package/dist/fold.js ADDED
@@ -0,0 +1,175 @@
1
+ import { fromMillimeters, roundMillimeters, sheetMm, toMillimeters } from "./paper.js";
2
+ import { refuseOverflow, refuseSafe } from "./imposition-fit.js";
3
+ //#region src/fold.ts
4
+ const FOLD_KINDS = [
5
+ "half",
6
+ "letter",
7
+ "z",
8
+ "gate",
9
+ "custom"
10
+ ];
11
+ const FOLD_TUCK_IN_MM = 1.5;
12
+ const FOLD_HEIGHT_REMEDY = "reduce the vertical margins or cell height";
13
+ function foldColumns(fold, panelCount = 0) {
14
+ if (fold === "custom") return panelCount;
15
+ return fold === "half" ? 2 : 3;
16
+ }
17
+ function foldPanelMap(fold, columns) {
18
+ if (fold === "gate") return {
19
+ front: [
20
+ 2,
21
+ 6,
22
+ 1
23
+ ],
24
+ back: [
25
+ 3,
26
+ 4,
27
+ 5
28
+ ]
29
+ };
30
+ return {
31
+ front: [...Array.from({ length: columns - 1 }, (_, index) => columns + 2 + index), 1],
32
+ back: Array.from({ length: columns }, (_, index) => index + 2)
33
+ };
34
+ }
35
+ function foldCellPages(fold, columns) {
36
+ const map = foldPanelMap(fold, columns);
37
+ return Array.from({ length: columns }, (_, cell) => ({
38
+ front: (map.front[cell] ?? 1) - 1,
39
+ back: (map.back[columns - 1 - cell] ?? 1) - 1
40
+ }));
41
+ }
42
+ function foldPageCountMessage(fold, pageCount, columns) {
43
+ const need = columns * 2;
44
+ return `this ${fold} fold has ${pageCount} pages and needs ${need} (2 × ${columns} panels); pass ${need} pages to clear this refusal`;
45
+ }
46
+ function splitSum(available, count) {
47
+ const share = available / count;
48
+ const widths = Array.from({ length: count - 1 }, () => roundMillimeters(share));
49
+ const used = widths.reduce((sum, width) => sum + width, 0);
50
+ widths.push(roundMillimeters(available - used));
51
+ return widths;
52
+ }
53
+ function presetPanelWidthsMm(fold, availableMm) {
54
+ if (fold === "letter") {
55
+ const tuck = roundMillimeters(availableMm / 3 - FOLD_TUCK_IN_MM);
56
+ const rest = availableMm - tuck;
57
+ const second = roundMillimeters(rest / 2);
58
+ return [
59
+ tuck,
60
+ second,
61
+ roundMillimeters(rest - second)
62
+ ];
63
+ }
64
+ if (fold === "gate") {
65
+ const flap = roundMillimeters(availableMm / 4 - FOLD_TUCK_IN_MM);
66
+ return [
67
+ flap,
68
+ roundMillimeters(availableMm - flap * 2),
69
+ flap
70
+ ];
71
+ }
72
+ return splitSum(availableMm, foldColumns(fold));
73
+ }
74
+ function availableWidthMm(draft) {
75
+ const paper = sheetMm({
76
+ paper: draft.paper,
77
+ orientation: draft.orientation
78
+ });
79
+ return roundMillimeters(paper.width - toMillimeters(draft.margin.left, draft.unit) - toMillimeters(draft.margin.right, draft.unit));
80
+ }
81
+ function panelWidthsMm(draft) {
82
+ if (draft.panels) return draft.panels.map((width) => toMillimeters(width, draft.unit));
83
+ if (!draft.fold || draft.fold === "custom") return [];
84
+ return presetPanelWidthsMm(draft.fold, availableWidthMm(draft));
85
+ }
86
+ function panelsInUnit(draft) {
87
+ if (draft.panels) return draft.panels;
88
+ return panelWidthsMm(draft).map((width) => draft.unit === "mm" ? width : fromMillimeters(width, draft.unit));
89
+ }
90
+ function issue(ctx, path, message) {
91
+ ctx.addIssue({
92
+ code: "custom",
93
+ path,
94
+ message
95
+ });
96
+ }
97
+ function refuseShape(draft, ctx, columns) {
98
+ if (draft.rows !== void 0 && draft.rows !== 1) issue(ctx, ["rows"], `rows is ${draft.rows}, but a fold is one row; set rows to 1 or omit it to clear this refusal`);
99
+ if (draft.sides === "one") issue(ctx, ["sides"], "sides is one, but a fold is two-sided; set sides to two or omit it to clear this refusal");
100
+ if (draft.fill === "repeat") issue(ctx, ["fill"], "fill is repeat, but a fold fills in sequence; set fill to sequence or omit it to clear this refusal");
101
+ if (draft.skip !== 0) issue(ctx, ["skip"], `skip is ${draft.skip}, but a fold has no skipped cells; set skip to 0 or omit it to clear this refusal`);
102
+ if (draft.gap.column !== 0) issue(ctx, ["gap", "column"], `column gap is ${draft.gap.column}, but a fold's panels touch; set gap.column to 0 or omit it to clear this refusal`);
103
+ if (draft.columns !== void 0 && draft.columns !== columns) issue(ctx, ["columns"], `columns is ${draft.columns}, but a ${draft.fold} fold has ${columns} panels; set columns to ${columns} or omit it to clear this refusal`);
104
+ }
105
+ function refineFold(draft, ctx) {
106
+ const fold = draft.fold;
107
+ if (!fold) return;
108
+ if (fold === "custom" && (draft.panels === void 0 || draft.panels.length < 2)) {
109
+ issue(ctx, ["panels"], "a custom fold needs panels with at least two widths; give panel widths that sum to the paper width to clear this refusal");
110
+ return;
111
+ }
112
+ const columns = foldColumns(fold, draft.panels?.length);
113
+ if (fold !== "custom" && draft.panels !== void 0 && draft.panels.length !== columns) {
114
+ issue(ctx, ["panels"], `panels has ${draft.panels.length} widths and a ${fold} fold has ${columns} columns; give ${columns} widths or omit panels to use the preset`);
115
+ return;
116
+ }
117
+ refuseShape(draft, ctx, columns);
118
+ const paper = sheetMm({
119
+ paper: draft.paper,
120
+ orientation: draft.orientation
121
+ });
122
+ const available = availableWidthMm(draft);
123
+ if (available <= 0) {
124
+ issue(ctx, ["margin"], `the horizontal margins leave ${available}mm of the ${paper.width}mm paper; reduce them`);
125
+ return;
126
+ }
127
+ const widths = panelWidthsMm(draft);
128
+ if (widths.some((width) => width <= 0)) {
129
+ issue(ctx, ["panels"], `a ${fold} fold has a non-positive panel after the ${FOLD_TUCK_IN_MM}mm tuck-in allowance; use a wider paper or override panels to clear this refusal`);
130
+ return;
131
+ }
132
+ const marginLeft = toMillimeters(draft.margin.left, draft.unit);
133
+ const marginRight = toMillimeters(draft.margin.right, draft.unit);
134
+ const sum = roundMillimeters(marginLeft + widths.reduce((total, width) => total + width, 0) + marginRight);
135
+ if (Math.abs(sum - paper.width) > .01) issue(ctx, ["panels"], `the panels and horizontal margins sum to ${sum}mm and the paper is ${paper.width}mm wide; change panels or margins so they sum to ${paper.width}mm to clear this refusal`);
136
+ const height = toMillimeters(draft.margin.top, draft.unit) + toMillimeters(draft.cell.height, draft.unit) + toMillimeters(draft.margin.bottom, draft.unit);
137
+ refuseOverflow(ctx, paper, { height }, { height: {
138
+ path: ["cell", "height"],
139
+ subject: "the folded piece",
140
+ remedy: FOLD_HEIGHT_REMEDY
141
+ } });
142
+ refuseSafe(ctx, toMillimeters(draft.safe, draft.unit), {
143
+ width: Math.min(...widths),
144
+ height: toMillimeters(draft.cell.height, draft.unit)
145
+ });
146
+ }
147
+ function resolveImposition(value) {
148
+ if (!value.fold) return {
149
+ ...value,
150
+ columns: value.columns ?? 1,
151
+ rows: value.rows ?? 1,
152
+ sides: value.sides ?? "one",
153
+ cell: {
154
+ width: value.cell.width ?? 1,
155
+ height: value.cell.height
156
+ }
157
+ };
158
+ const columns = foldColumns(value.fold, value.panels?.length);
159
+ const panels = panelsInUnit(value);
160
+ return {
161
+ ...value,
162
+ columns,
163
+ rows: 1,
164
+ sides: "two",
165
+ fill: "sequence",
166
+ skip: 0,
167
+ gap: {
168
+ column: 0,
169
+ row: value.gap.row
170
+ },
171
+ panels
172
+ };
173
+ }
174
+ //#endregion
175
+ export { FOLD_KINDS, FOLD_TUCK_IN_MM, foldCellPages, foldColumns, foldPageCountMessage, foldPanelMap, panelWidthsMm, presetPanelWidthsMm, refineFold, resolveImposition };
@@ -0,0 +1,77 @@
1
+ import { z } from "zod";
2
+ //#region src/fonts.d.ts
3
+ export declare const APP_FONT_NAME: RegExp;
4
+ export declare const APP_FONT_NAME_MESSAGE = "app font names are lowercase letters, digits and hyphens";
5
+ export declare const FontReference: z.ZodUnion<readonly [z.ZodObject<{
6
+ app: z.ZodString;
7
+ }, z.core.$strip>, z.ZodObject<{
8
+ local: z.ZodString;
9
+ fallback: z.ZodString;
10
+ }, z.core.$strip>]>;
11
+ export type FontReference = z.infer<typeof FontReference>;
12
+ export declare const FontReferenceInput: z.ZodUnion<readonly [z.ZodObject<{
13
+ app: z.ZodString;
14
+ }, z.core.$strict>, z.ZodObject<{
15
+ local: z.ZodString;
16
+ fallback: z.ZodString;
17
+ }, z.core.$strict>]>;
18
+ export type FontReferenceInput = z.input<typeof FontReferenceInput>;
19
+ export declare function localFontReferences(references: readonly FontReference[]): Array<{
20
+ local: string;
21
+ fallback: string;
22
+ }>;
23
+ export declare const FONT_FORMATS: readonly ["woff2", "woff", "truetype", "opentype"];
24
+ export type FontFormat = (typeof FONT_FORMATS)[number];
25
+ export declare const LocalFaceTransport: z.ZodObject<{
26
+ family: z.ZodString;
27
+ format: z.ZodEnum<{
28
+ woff2: "woff2";
29
+ woff: "woff";
30
+ truetype: "truetype";
31
+ opentype: "opentype";
32
+ }>;
33
+ weight: z.ZodOptional<z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>>;
34
+ style: z.ZodOptional<z.ZodEnum<{
35
+ normal: "normal";
36
+ italic: "italic";
37
+ }>>;
38
+ data: z.ZodString;
39
+ }, z.core.$strict>;
40
+ export type LocalFaceTransport = z.infer<typeof LocalFaceTransport>;
41
+ export declare const LocalFacesTransport: z.ZodArray<z.ZodObject<{
42
+ family: z.ZodString;
43
+ format: z.ZodEnum<{
44
+ woff2: "woff2";
45
+ woff: "woff";
46
+ truetype: "truetype";
47
+ opentype: "opentype";
48
+ }>;
49
+ weight: z.ZodOptional<z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>>;
50
+ style: z.ZodOptional<z.ZodEnum<{
51
+ normal: "normal";
52
+ italic: "italic";
53
+ }>>;
54
+ data: z.ZodString;
55
+ }, z.core.$strict>>;
56
+ export type LocalFacesInput = LocalFaceTransport[];
57
+ export declare const FONT_GENERICS: readonly ["serif", "sans-serif", "monospace", "cursive", "fantasy", "system-ui"];
58
+ export type FontGeneric = (typeof FONT_GENERICS)[number];
59
+ export interface FontFace {
60
+ family: string;
61
+ data: Uint8Array;
62
+ format: FontFormat;
63
+ weight?: number | string;
64
+ style?: 'normal' | 'italic';
65
+ unicodeRange?: string;
66
+ }
67
+ export interface AppFont {
68
+ family: string;
69
+ faces: FontFace[];
70
+ generic?: FontGeneric;
71
+ }
72
+ export interface FontSource {
73
+ font(name: string, options: {
74
+ signal: AbortSignal;
75
+ }): Promise<AppFont | undefined>;
76
+ }
77
+ //#endregion
package/dist/fonts.js ADDED
@@ -0,0 +1,49 @@
1
+ import { z } from "zod";
2
+ //#region src/fonts.ts
3
+ const APP_FONT_NAME = /^[a-z0-9-]+$/;
4
+ const APP_FONT_NAME_MESSAGE = "app font names are lowercase letters, digits and hyphens";
5
+ const AppFontName = z.string().min(1).regex(APP_FONT_NAME, APP_FONT_NAME_MESSAGE);
6
+ const AppFontShape = { app: AppFontName };
7
+ const LocalFontShape = {
8
+ local: z.string().min(1),
9
+ fallback: AppFontName
10
+ };
11
+ const FontReference = z.union([z.object(AppFontShape), z.object(LocalFontShape)]);
12
+ const FontReferenceInput = z.union([z.strictObject(AppFontShape), z.strictObject(LocalFontShape)]);
13
+ function localFontReferences(references) {
14
+ const seen = /* @__PURE__ */ new Set();
15
+ const locals = [];
16
+ for (const reference of references) {
17
+ if (!("local" in reference)) continue;
18
+ const key = JSON.stringify(reference);
19
+ if (seen.has(key)) continue;
20
+ seen.add(key);
21
+ locals.push(reference);
22
+ }
23
+ return locals;
24
+ }
25
+ const FONT_FORMATS = [
26
+ "woff2",
27
+ "woff",
28
+ "truetype",
29
+ "opentype"
30
+ ];
31
+ const LocalFaceTransportShape = {
32
+ family: z.string().min(1),
33
+ format: z.enum(FONT_FORMATS),
34
+ weight: z.union([z.number(), z.string()]).optional(),
35
+ style: z.enum(["normal", "italic"]).optional(),
36
+ data: z.string()
37
+ };
38
+ const LocalFaceTransport = z.strictObject(LocalFaceTransportShape);
39
+ const LocalFacesTransport = z.array(LocalFaceTransport);
40
+ const FONT_GENERICS = [
41
+ "serif",
42
+ "sans-serif",
43
+ "monospace",
44
+ "cursive",
45
+ "fantasy",
46
+ "system-ui"
47
+ ];
48
+ //#endregion
49
+ export { APP_FONT_NAME, APP_FONT_NAME_MESSAGE, FONT_FORMATS, FONT_GENERICS, FontReference, FontReferenceInput, LocalFaceTransport, LocalFacesTransport, localFontReferences };
@@ -0,0 +1,3 @@
1
+ //#region src/imposition-fit.d.ts
2
+ export declare const PAPER_FIT_TOLERANCE_MM = 0.01;
3
+ //#endregion
@@ -0,0 +1,33 @@
1
+ import { roundMillimeters } from "./paper.js";
2
+ //#region src/imposition-fit.ts
3
+ const PAPER_FIT_TOLERANCE_MM = .01;
4
+ const GRID_WIDTH_REMEDY = "reduce the horizontal margins, columns, cell width or column gap";
5
+ const GRID_HEIGHT_REMEDY = "reduce the vertical margins, rows, cell height or row gap";
6
+ function refuseOverflow(ctx, paper, used, axis = {}) {
7
+ const width = used.width;
8
+ if (width !== void 0 && width - paper.width > .01) ctx.addIssue({
9
+ code: "custom",
10
+ path: axis.width?.path ?? ["columns"],
11
+ message: `${axis.width?.subject ?? "the grid"} is ${roundMillimeters(width)}mm wide and the paper is ${paper.width}mm wide, an overflow of ${roundMillimeters(width - paper.width)}mm; ${axis.width?.remedy ?? GRID_WIDTH_REMEDY}`
12
+ });
13
+ const height = used.height;
14
+ if (height !== void 0 && height - paper.height > .01) ctx.addIssue({
15
+ code: "custom",
16
+ path: axis.height?.path ?? ["rows"],
17
+ message: `${axis.height?.subject ?? "the grid"} is ${roundMillimeters(height)}mm high and the paper is ${paper.height}mm high, an overflow of ${roundMillimeters(height - paper.height)}mm; ${axis.height?.remedy ?? GRID_HEIGHT_REMEDY}`
18
+ });
19
+ }
20
+ function refuseSafe(ctx, safe, cell) {
21
+ if (safe * 2 >= cell.width) ctx.addIssue({
22
+ code: "custom",
23
+ path: ["safe"],
24
+ message: `safe is ${roundMillimeters(safe)}mm on the left and right, but the cell is ${roundMillimeters(cell.width)}mm wide; safe must leave area inside the cell`
25
+ });
26
+ if (safe * 2 >= cell.height) ctx.addIssue({
27
+ code: "custom",
28
+ path: ["safe"],
29
+ message: `safe is ${roundMillimeters(safe)}mm on the top and bottom, but the cell is ${roundMillimeters(cell.height)}mm high; safe must leave area inside the cell`
30
+ });
31
+ }
32
+ //#endregion
33
+ export { PAPER_FIT_TOLERANCE_MM, refuseOverflow, refuseSafe };
@@ -1,9 +1,9 @@
1
1
  import { roundMillimeters } from "./paper.js";
2
- import { impose } from "./imposition.js";
2
+ import { gridPlacements } from "./imposition.js";
3
3
  //#region src/imposition-marks.ts
4
4
  const MIN_MARK_LENGTH_MM = 2;
5
5
  const unique = (values) => [...new Set(values.map(roundMillimeters))].sort((a, b) => a - b);
6
- function addMark(result, edge, cutPosition, start, available, horizontal, direction) {
6
+ function addMark(result, edge, cutPosition, start, available, horizontal, direction, dashed = false) {
7
7
  const length = Math.min(5, roundMillimeters(available));
8
8
  if (length < 2) {
9
9
  result.omittedMarks.push({
@@ -14,19 +14,22 @@ function addMark(result, edge, cutPosition, start, available, horizontal, direct
14
14
  return;
15
15
  }
16
16
  const end = roundMillimeters(start + direction * length);
17
+ const dash = dashed ? { dashed: true } : {};
17
18
  result.marks.push(horizontal ? {
18
19
  x1: Math.min(start, end),
19
20
  y1: cutPosition,
20
21
  x2: Math.max(start, end),
21
- y2: cutPosition
22
+ y2: cutPosition,
23
+ ...dash
22
24
  } : {
23
25
  x1: cutPosition,
24
26
  y1: Math.min(start, end),
25
27
  x2: cutPosition,
26
- y2: Math.max(start, end)
28
+ y2: Math.max(start, end),
29
+ ...dash
27
30
  });
28
31
  }
29
- function outerMarks(side, placements, paper) {
32
+ function outerMarks(side, placements, paper, foldCuts) {
30
33
  const verticalCuts = unique(placements.flatMap(({ trim }) => [trim.x, trim.x + trim.width]));
31
34
  const horizontalCuts = unique(placements.flatMap(({ trim }) => [trim.y, trim.y + trim.height]));
32
35
  const top = Math.min(...placements.map(({ bleedBox }) => bleedBox.y));
@@ -34,6 +37,7 @@ function outerMarks(side, placements, paper) {
34
37
  const bottom = Math.max(...placements.map(({ bleedBox }) => bleedBox.y + bleedBox.height));
35
38
  const left = Math.min(...placements.map(({ bleedBox }) => bleedBox.x));
36
39
  for (const cut of verticalCuts) {
40
+ if (foldCuts.has(cut)) continue;
37
41
  addMark(side, "top", cut, roundMillimeters(top - 1), top - 1, false, -1);
38
42
  addMark(side, "bottom", cut, roundMillimeters(bottom + 1), paper.height - bottom - 1, false, 1);
39
43
  }
@@ -41,6 +45,10 @@ function outerMarks(side, placements, paper) {
41
45
  addMark(side, "left", cut, roundMillimeters(left - 1), left - 1, true, -1);
42
46
  addMark(side, "right", cut, roundMillimeters(right + 1), paper.width - right - 1, true, 1);
43
47
  }
48
+ for (const cut of foldCuts) {
49
+ addMark(side, "top", cut, roundMillimeters(top - 1), top - 1, false, -1, true);
50
+ addMark(side, "bottom", cut, roundMillimeters(bottom + 1), paper.height - bottom - 1, false, 1, true);
51
+ }
44
52
  }
45
53
  function intersectsBleed(mark, boxes) {
46
54
  return boxes.some((box) => Math.max(mark.x1, box.x) <= Math.min(mark.x2, box.x + box.width) && Math.max(mark.y1, box.y) <= Math.min(mark.y2, box.y + box.height));
@@ -146,39 +154,38 @@ function cornerMarks(side, placements, rows) {
146
154
  }
147
155
  }
148
156
  }
157
+ function foldCutPositions(placements) {
158
+ const left = Math.min(...placements.map(({ trim }) => trim.x));
159
+ const right = Math.max(...placements.map(({ trim }) => trim.x + trim.width));
160
+ return new Set(unique(placements.flatMap(({ trim }) => [trim.x, trim.x + trim.width])).filter((cut) => cut !== left && cut !== right));
161
+ }
149
162
  /** Computes physical crop-mark geometry without changing the fixed paper size. */
150
163
  function impositionMarks(imposition) {
151
164
  if (!imposition.marks) return [];
152
- const cellCount = imposition.columns * imposition.rows;
153
- const frontImposed = impose({
154
- ...imposition,
155
- sides: "one",
156
- fill: "sequence",
157
- skip: 0
158
- }, cellCount);
159
- const frontPlacements = frontImposed.placements;
165
+ const { paper, placements: frontPlacements } = gridPlacements(imposition);
166
+ const foldCuts = imposition.fold ? foldCutPositions(frontPlacements) : /* @__PURE__ */ new Set();
160
167
  const build = (sideName, placements) => {
161
168
  const side = {
162
169
  side: sideName,
163
170
  marks: [],
164
171
  omittedMarks: []
165
172
  };
166
- outerMarks(side, placements, frontImposed.paper);
173
+ outerMarks(side, placements, paper, foldCuts);
167
174
  if (imposition.cornerMarks) cornerMarks(side, placements, imposition.rows);
168
175
  return side;
169
176
  };
170
177
  const front = build("front", frontPlacements);
171
178
  if (imposition.sides === "one") return [front];
172
- const portrait = frontImposed.paper.width <= frontImposed.paper.height;
179
+ const portrait = paper.width <= paper.height;
173
180
  const axis = imposition.flip === "long" === portrait ? "x" : "y";
174
181
  const mirrorMark = (mark) => axis === "x" ? {
175
182
  ...mark,
176
- x1: roundMillimeters(frontImposed.paper.width - mark.x1),
177
- x2: roundMillimeters(frontImposed.paper.width - mark.x2)
183
+ x1: roundMillimeters(paper.width - mark.x1),
184
+ x2: roundMillimeters(paper.width - mark.x2)
178
185
  } : {
179
186
  ...mark,
180
- y1: roundMillimeters(frontImposed.paper.height - mark.y1),
181
- y2: roundMillimeters(frontImposed.paper.height - mark.y2)
187
+ y1: roundMillimeters(paper.height - mark.y1),
188
+ y2: roundMillimeters(paper.height - mark.y2)
182
189
  };
183
190
  const mirrorEdge = (edge) => {
184
191
  if (axis === "x") return edge === "left" ? "right" : edge === "right" ? "left" : edge;
@@ -190,7 +197,7 @@ function impositionMarks(imposition) {
190
197
  omittedMarks: front.omittedMarks.map(({ edge, cutPosition }) => ({
191
198
  side: "back",
192
199
  edge: mirrorEdge(edge),
193
- cutPosition: roundMillimeters(axis === "x" && (edge === "top" || edge === "bottom") ? frontImposed.paper.width - cutPosition : axis === "y" && (edge === "left" || edge === "right") ? frontImposed.paper.height - cutPosition : cutPosition)
200
+ cutPosition: roundMillimeters(axis === "x" && (edge === "top" || edge === "bottom") ? paper.width - cutPosition : axis === "y" && (edge === "left" || edge === "right") ? paper.height - cutPosition : cutPosition)
194
201
  }))
195
202
  }];
196
203
  }
@@ -1,4 +1,5 @@
1
1
  import { PX_PER_MM, roundMillimeters, sheetMm, toMillimeters } from "./paper.js";
2
+ import { foldCellPages, foldPageCountMessage } from "./fold.js";
2
3
  //#region src/imposition.ts
3
4
  const zeroCrop = () => ({
4
5
  top: 0,
@@ -6,16 +7,34 @@ const zeroCrop = () => ({
6
7
  bottom: 0,
7
8
  left: 0
8
9
  });
10
+ function columnWidthsMm(imposition) {
11
+ if (imposition.fold) return (imposition.panels ?? []).map((width) => toMillimeters(width, imposition.unit));
12
+ return Array.from({ length: imposition.columns }, () => toMillimeters(imposition.cell.width ?? 0, imposition.unit));
13
+ }
14
+ function columnXMm(imposition, column, widths, paperWidth) {
15
+ const mm = (value) => toMillimeters(value, imposition.unit);
16
+ const gap = mm(imposition.gap.column);
17
+ if (imposition.dir === "rtl") {
18
+ let x = paperWidth - mm(imposition.margin.right);
19
+ for (let index = 0; index <= column; index += 1) x -= widths[index] ?? 0;
20
+ for (let index = 0; index < column; index += 1) x -= gap;
21
+ return x;
22
+ }
23
+ let x = mm(imposition.margin.left);
24
+ for (let index = 0; index < column; index += 1) x += (widths[index] ?? 0) + gap;
25
+ return x;
26
+ }
9
27
  function placementAt(imposition, paper, cell, page, paperSize) {
10
28
  const mm = (value) => toMillimeters(value, imposition.unit);
11
29
  const column = cell % imposition.columns;
12
30
  const row = Math.floor(cell / imposition.columns);
13
31
  const physicalColumn = imposition.dir === "rtl" ? imposition.columns - column - 1 : column;
14
- const cellWidth = mm(imposition.cell.width);
32
+ const widths = columnWidthsMm(imposition);
33
+ const cellWidth = widths[column] ?? mm(imposition.cell.width ?? 0);
15
34
  const cellHeight = mm(imposition.cell.height);
16
35
  const columnGap = mm(imposition.gap.column);
17
36
  const rowGap = mm(imposition.gap.row);
18
- const x = imposition.dir === "rtl" ? paperSize.width - mm(imposition.margin.right) - cellWidth - column * (cellWidth + columnGap) : mm(imposition.margin.left) + column * (cellWidth + columnGap);
37
+ const x = columnXMm(imposition, column, widths, paperSize.width);
19
38
  const y = mm(imposition.margin.top) + row * (cellHeight + rowGap);
20
39
  const requested = mm(imposition.bleed);
21
40
  const bleed = {
@@ -145,6 +164,38 @@ function expandAssignments(imposition, assignments, pageCount, paper) {
145
164
  flush();
146
165
  return placements;
147
166
  }
167
+ function imposeFold(imposition, pageCount, paper) {
168
+ const fold = imposition.fold;
169
+ if (!fold) throw new Error("imposeFold requires a fold");
170
+ const columns = imposition.columns;
171
+ if (pageCount !== columns * 2) throw new Error(foldPageCountMessage(fold, pageCount, columns));
172
+ const cells = foldCellPages(fold, columns);
173
+ const fronts = [];
174
+ const backs = [];
175
+ for (const [cell, pages] of cells.entries()) {
176
+ if (!pages) continue;
177
+ const front = placementAt(imposition, 0, cell, pages.front, paper);
178
+ fronts.push(front);
179
+ backs.push(mirrorPlacement(front, paper, imposition.flip, pages.back));
180
+ }
181
+ return {
182
+ paper,
183
+ papers: 1,
184
+ sides: 2,
185
+ placements: [...fronts, ...backs]
186
+ };
187
+ }
188
+ function gridPlacements(imposition) {
189
+ const paper = sheetMm({
190
+ paper: imposition.paper,
191
+ orientation: imposition.orientation
192
+ });
193
+ const cells = imposition.columns * imposition.rows;
194
+ return {
195
+ paper,
196
+ placements: Array.from({ length: cells }, (_, cell) => placementAt(imposition, 0, cell, 0, paper))
197
+ };
198
+ }
148
199
  function impose(imposition, pageCount) {
149
200
  if (!Number.isInteger(pageCount) || pageCount < 0) throw new RangeError(`pageCount must be a nonnegative integer; received ${String(pageCount)}`);
150
201
  const paper = sheetMm({
@@ -152,6 +203,7 @@ function impose(imposition, pageCount) {
152
203
  orientation: imposition.orientation
153
204
  });
154
205
  const sides = imposition.sides === "two" ? 2 : 1;
206
+ if (imposition.fold) return imposeFold(imposition, pageCount, paper);
155
207
  if (pageCount === 0) return {
156
208
  paper,
157
209
  papers: 0,
@@ -291,4 +343,4 @@ function pagePlacementLayout(size, placement, fit) {
291
343
  };
292
344
  }
293
345
  //#endregion
294
- export { fitPage, impose, mirrorPlacement, pagePlacementLayout };
346
+ export { fitPage, gridPlacements, impose, mirrorPlacement, pagePlacementLayout };