@segnavia/format 0.1.0 → 0.2.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.
@@ -0,0 +1,62 @@
1
+ import { PaperBox } from "./paper.js";
2
+ import { Imposition, Page, Print } from "./index.js";
3
+ //#region src/imposition.d.ts
4
+ export interface MillimeterEdges {
5
+ top: number;
6
+ right: number;
7
+ bottom: number;
8
+ left: number;
9
+ }
10
+ export interface Placement {
11
+ page: number;
12
+ paper: number;
13
+ side: 'front' | 'back';
14
+ column: number;
15
+ row: number;
16
+ trim: PaperBox;
17
+ bleed: MillimeterEdges;
18
+ bleedBox: PaperBox;
19
+ safeBox: PaperBox;
20
+ clipped: Array<'top' | 'right' | 'bottom' | 'left'>;
21
+ }
22
+ export interface Imposed {
23
+ paper: {
24
+ width: number;
25
+ height: number;
26
+ };
27
+ papers: number;
28
+ sides: 1 | 2;
29
+ placements: Placement[];
30
+ }
31
+ export interface PageFit {
32
+ flow: boolean;
33
+ width: number;
34
+ height: number | null;
35
+ scale: number;
36
+ x: number;
37
+ y: number;
38
+ bands: {
39
+ inline: number;
40
+ block: number;
41
+ };
42
+ crop: MillimeterEdges;
43
+ differs: boolean;
44
+ }
45
+ export type PagePlacementKind = 'fluid' | 'growing' | 'fixed';
46
+ export interface PagePlacementLayout {
47
+ kind: PagePlacementKind;
48
+ piece: {
49
+ width: number;
50
+ height: number;
51
+ };
52
+ inset: boolean;
53
+ paper: PaperBox;
54
+ }
55
+ export declare function mirrorPlacement(front: Placement, paper: Imposed['paper'], flip: Imposition['flip'], page?: number): Placement;
56
+ export declare function impose(imposition: Imposition, pageCount: number): Imposed;
57
+ export declare function fitPage(page: {
58
+ size: Page['size'];
59
+ print?: Print;
60
+ }, placement: Placement, fit: 'contain' | 'cover'): PageFit;
61
+ export declare function pagePlacementLayout(size: Page['size'], placement: Placement, fit: PageFit): PagePlacementLayout;
62
+ //#endregion
@@ -0,0 +1,294 @@
1
+ import { PX_PER_MM, roundMillimeters, sheetMm, toMillimeters } from "./paper.js";
2
+ //#region src/imposition.ts
3
+ const zeroCrop = () => ({
4
+ top: 0,
5
+ right: 0,
6
+ bottom: 0,
7
+ left: 0
8
+ });
9
+ function placementAt(imposition, paper, cell, page, paperSize) {
10
+ const mm = (value) => toMillimeters(value, imposition.unit);
11
+ const column = cell % imposition.columns;
12
+ const row = Math.floor(cell / imposition.columns);
13
+ const physicalColumn = imposition.dir === "rtl" ? imposition.columns - column - 1 : column;
14
+ const cellWidth = mm(imposition.cell.width);
15
+ const cellHeight = mm(imposition.cell.height);
16
+ const columnGap = mm(imposition.gap.column);
17
+ 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);
19
+ const y = mm(imposition.margin.top) + row * (cellHeight + rowGap);
20
+ const requested = mm(imposition.bleed);
21
+ const bleed = {
22
+ top: Math.min(requested, row > 0 ? rowGap / 2 : y),
23
+ right: Math.min(requested, physicalColumn < imposition.columns - 1 ? columnGap / 2 : Math.max(0, paperSize.width - x - cellWidth)),
24
+ bottom: Math.min(requested, row < imposition.rows - 1 ? rowGap / 2 : Math.max(0, paperSize.height - y - cellHeight)),
25
+ left: Math.min(requested, physicalColumn > 0 ? columnGap / 2 : Math.max(0, x))
26
+ };
27
+ const clipped = [
28
+ "top",
29
+ "right",
30
+ "bottom",
31
+ "left"
32
+ ].filter((side) => bleed[side] < requested);
33
+ const trim = {
34
+ x: roundMillimeters(x),
35
+ y: roundMillimeters(y),
36
+ width: roundMillimeters(cellWidth),
37
+ height: roundMillimeters(cellHeight)
38
+ };
39
+ const roundedBleed = {
40
+ top: roundMillimeters(bleed.top),
41
+ right: roundMillimeters(bleed.right),
42
+ bottom: roundMillimeters(bleed.bottom),
43
+ left: roundMillimeters(bleed.left)
44
+ };
45
+ const safe = mm(imposition.safe);
46
+ return {
47
+ page,
48
+ paper,
49
+ side: "front",
50
+ column,
51
+ row,
52
+ trim,
53
+ bleed: roundedBleed,
54
+ bleedBox: {
55
+ x: roundMillimeters(x - bleed.left),
56
+ y: roundMillimeters(y - bleed.top),
57
+ width: roundMillimeters(cellWidth + bleed.left + bleed.right),
58
+ height: roundMillimeters(cellHeight + bleed.top + bleed.bottom)
59
+ },
60
+ safeBox: {
61
+ x: roundMillimeters(x + safe),
62
+ y: roundMillimeters(y + safe),
63
+ width: roundMillimeters(cellWidth - safe * 2),
64
+ height: roundMillimeters(cellHeight - safe * 2)
65
+ },
66
+ clipped
67
+ };
68
+ }
69
+ const mirrorBox = (box, paper, axis) => axis === "x" ? {
70
+ ...box,
71
+ x: roundMillimeters(paper.width - box.x - box.width)
72
+ } : {
73
+ ...box,
74
+ y: roundMillimeters(paper.height - box.y - box.height)
75
+ };
76
+ const EDGE_ORDER = [
77
+ "top",
78
+ "right",
79
+ "bottom",
80
+ "left"
81
+ ];
82
+ function mirrorEdge(edge, axis) {
83
+ if (axis === "x") return edge === "left" ? "right" : edge === "right" ? "left" : edge;
84
+ return edge === "top" ? "bottom" : edge === "bottom" ? "top" : edge;
85
+ }
86
+ function mirrorEdges(edges, axis) {
87
+ const mirrored = { ...edges };
88
+ for (const edge of EDGE_ORDER) mirrored[mirrorEdge(edge, axis)] = edges[edge];
89
+ return mirrored;
90
+ }
91
+ function mirrorPlacement(front, paper, flip, page = front.page) {
92
+ const portrait = paper.width <= paper.height;
93
+ const axis = flip === "long" === portrait ? "x" : "y";
94
+ const mirroredClipped = new Set(front.clipped.map((edge) => mirrorEdge(edge, axis)));
95
+ return {
96
+ ...front,
97
+ page,
98
+ side: "back",
99
+ trim: mirrorBox(front.trim, paper, axis),
100
+ bleed: mirrorEdges(front.bleed, axis),
101
+ bleedBox: mirrorBox(front.bleedBox, paper, axis),
102
+ safeBox: mirrorBox(front.safeBox, paper, axis),
103
+ clipped: EDGE_ORDER.filter((edge) => mirroredClipped.has(edge))
104
+ };
105
+ }
106
+ function assignPieces(imposition, pieces, papers) {
107
+ const cells = imposition.columns * imposition.rows;
108
+ const assignments = [];
109
+ let piece = 0;
110
+ for (let paperIndex = 0; paperIndex < papers; paperIndex += 1) {
111
+ const firstCell = paperIndex === 0 ? imposition.skip : 0;
112
+ for (let cell = firstCell; cell < cells; cell += 1) {
113
+ const assignedPiece = imposition.fill === "repeat" ? paperIndex : piece;
114
+ if (assignedPiece >= pieces) break;
115
+ assignments.push({
116
+ paper: paperIndex,
117
+ cell,
118
+ piece: assignedPiece
119
+ });
120
+ if (imposition.fill === "sequence") piece += 1;
121
+ }
122
+ }
123
+ return assignments;
124
+ }
125
+ function expandAssignments(imposition, assignments, pageCount, paper) {
126
+ const placements = [];
127
+ let currentPaper = -1;
128
+ let fronts = [];
129
+ let backs = [];
130
+ const flush = () => {
131
+ placements.push(...fronts, ...backs);
132
+ fronts = [];
133
+ backs = [];
134
+ };
135
+ for (const assignment of assignments) {
136
+ if (assignment.paper !== currentPaper) {
137
+ flush();
138
+ currentPaper = assignment.paper;
139
+ }
140
+ const frontPage = imposition.sides === "two" ? assignment.piece * 2 : assignment.piece;
141
+ const front = placementAt(imposition, assignment.paper, assignment.cell, frontPage, paper);
142
+ fronts.push(front);
143
+ if (imposition.sides === "two" && frontPage + 1 < pageCount) backs.push(mirrorPlacement(front, paper, imposition.flip, frontPage + 1));
144
+ }
145
+ flush();
146
+ return placements;
147
+ }
148
+ function impose(imposition, pageCount) {
149
+ if (!Number.isInteger(pageCount) || pageCount < 0) throw new RangeError(`pageCount must be a nonnegative integer; received ${String(pageCount)}`);
150
+ const paper = sheetMm({
151
+ paper: imposition.paper,
152
+ orientation: imposition.orientation
153
+ });
154
+ const sides = imposition.sides === "two" ? 2 : 1;
155
+ if (pageCount === 0) return {
156
+ paper,
157
+ papers: 0,
158
+ sides,
159
+ placements: []
160
+ };
161
+ const cells = imposition.columns * imposition.rows;
162
+ const pieces = imposition.sides === "two" ? Math.ceil(pageCount / 2) : pageCount;
163
+ const papers = imposition.fill === "repeat" ? pieces : Math.ceil((pieces + imposition.skip) / cells);
164
+ return {
165
+ paper,
166
+ papers,
167
+ sides,
168
+ placements: expandAssignments(imposition, assignPieces(imposition, pieces, papers), pageCount, paper)
169
+ };
170
+ }
171
+ function trimDiffers(page, placement) {
172
+ if (!page.print) return false;
173
+ const trim = sheetMm(page.print);
174
+ return Math.abs(trim.width - placement.trim.width) > .01 || Math.abs(trim.height - placement.trim.height) > .01;
175
+ }
176
+ function fitPage(page, placement, fit) {
177
+ const differs = trimDiffers(page, placement);
178
+ if (page.size === "fluid") return {
179
+ flow: true,
180
+ width: placement.bleedBox.width * PX_PER_MM,
181
+ height: null,
182
+ scale: 1,
183
+ x: placement.bleedBox.x,
184
+ y: placement.bleedBox.y,
185
+ bands: {
186
+ inline: 0,
187
+ block: 0
188
+ },
189
+ crop: zeroCrop(),
190
+ differs
191
+ };
192
+ if (page.size.height === "auto") return {
193
+ flow: true,
194
+ width: page.size.width,
195
+ height: null,
196
+ scale: placement.trim.width * PX_PER_MM / page.size.width,
197
+ x: placement.trim.x,
198
+ y: placement.trim.y,
199
+ bands: {
200
+ inline: 0,
201
+ block: 0
202
+ },
203
+ crop: zeroCrop(),
204
+ differs
205
+ };
206
+ if (fit === "contain") {
207
+ const scale = Math.min(placement.trim.width * PX_PER_MM / page.size.width, placement.trim.height * PX_PER_MM / page.size.height);
208
+ const width = page.size.width * scale / PX_PER_MM;
209
+ const height = page.size.height * scale / PX_PER_MM;
210
+ const inline = (placement.trim.width - width) / 2;
211
+ const block = (placement.trim.height - height) / 2;
212
+ return {
213
+ flow: false,
214
+ width: page.size.width,
215
+ height: page.size.height,
216
+ scale,
217
+ x: roundMillimeters(placement.trim.x + inline),
218
+ y: roundMillimeters(placement.trim.y + block),
219
+ bands: {
220
+ inline: roundMillimeters(inline),
221
+ block: roundMillimeters(block)
222
+ },
223
+ crop: zeroCrop(),
224
+ differs
225
+ };
226
+ }
227
+ const horizontalBleed = Math.max(placement.bleed.left, placement.bleed.right);
228
+ const verticalBleed = Math.max(placement.bleed.top, placement.bleed.bottom);
229
+ const scale = Math.max((placement.trim.width + horizontalBleed * 2) * PX_PER_MM / page.size.width, (placement.trim.height + verticalBleed * 2) * PX_PER_MM / page.size.height);
230
+ const width = page.size.width * scale / PX_PER_MM;
231
+ const height = page.size.height * scale / PX_PER_MM;
232
+ const x = placement.trim.x + (placement.trim.width - width) / 2;
233
+ const y = placement.trim.y + (placement.trim.height - height) / 2;
234
+ return {
235
+ flow: false,
236
+ width: page.size.width,
237
+ height: page.size.height,
238
+ scale,
239
+ x: roundMillimeters(x),
240
+ y: roundMillimeters(y),
241
+ bands: {
242
+ inline: 0,
243
+ block: 0
244
+ },
245
+ crop: {
246
+ top: roundMillimeters(Math.max(0, placement.bleedBox.y - y)),
247
+ right: roundMillimeters(Math.max(0, x + width - placement.bleedBox.x - placement.bleedBox.width)),
248
+ bottom: roundMillimeters(Math.max(0, y + height - placement.bleedBox.y - placement.bleedBox.height)),
249
+ left: roundMillimeters(Math.max(0, placement.bleedBox.x - x))
250
+ },
251
+ differs
252
+ };
253
+ }
254
+ function pagePlacementLayout(size, placement, fit) {
255
+ if (size === "fluid") return {
256
+ kind: "fluid",
257
+ piece: {
258
+ width: placement.bleedBox.width * PX_PER_MM,
259
+ height: placement.bleedBox.height * PX_PER_MM
260
+ },
261
+ inset: true,
262
+ paper: placement.bleedBox
263
+ };
264
+ if (size.height === "auto") return {
265
+ kind: "growing",
266
+ piece: {
267
+ width: size.width,
268
+ height: placement.trim.height * PX_PER_MM / fit.scale
269
+ },
270
+ inset: false,
271
+ paper: {
272
+ x: fit.x,
273
+ y: fit.y,
274
+ width: placement.trim.width,
275
+ height: placement.trim.height
276
+ }
277
+ };
278
+ return {
279
+ kind: "fixed",
280
+ piece: {
281
+ width: size.width,
282
+ height: size.height
283
+ },
284
+ inset: false,
285
+ paper: {
286
+ x: fit.x,
287
+ y: fit.y,
288
+ width: size.width * fit.scale / PX_PER_MM,
289
+ height: size.height * fit.scale / PX_PER_MM
290
+ }
291
+ };
292
+ }
293
+ //#endregion
294
+ export { fitPage, impose, mirrorPlacement, pagePlacementLayout };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  import { TextDirection, firstStrongDirection, languageDirection, resolveDirection } from "./direction.js";
2
+ import { MARK_GAP_MM, MARK_LENGTH_MM, MM_PER_PT, PAPER_MM, PX_PER_MM, PaperBox, printBoxes, printPadding, printProperties, roundMillimeters, sheetMm, toMillimeters } from "./paper.js";
3
+ import { Imposed, MillimeterEdges, PageFit, PagePlacementKind, PagePlacementLayout, Placement, fitPage, impose, mirrorPlacement, pagePlacementLayout } from "./imposition.js";
2
4
  import { z } from "zod";
3
5
  //#region src/index.d.ts
4
6
  export declare const Size: z.ZodObject<{
@@ -30,6 +32,7 @@ export declare const Paper: z.ZodUnion<readonly [z.ZodEnum<{
30
32
  unit: z.ZodEnum<{
31
33
  mm: "mm";
32
34
  in: "in";
35
+ pt: "pt";
33
36
  }>;
34
37
  }, z.core.$strip>]>;
35
38
  export type Paper = z.infer<typeof Paper>;
@@ -53,6 +56,7 @@ export declare const Print: z.ZodObject<{
53
56
  unit: z.ZodEnum<{
54
57
  mm: "mm";
55
58
  in: "in";
59
+ pt: "pt";
56
60
  }>;
57
61
  }, z.core.$strip>]>;
58
62
  orientation: z.ZodDefault<z.ZodEnum<{
@@ -65,8 +69,81 @@ export declare const Print: z.ZodObject<{
65
69
  bottom: z.ZodDefault<z.ZodNumber>;
66
70
  left: z.ZodDefault<z.ZodNumber>;
67
71
  }, z.core.$strip>>;
72
+ bleed: z.ZodDefault<z.ZodNumber>;
73
+ safe: z.ZodDefault<z.ZodNumber>;
74
+ marks: z.ZodDefault<z.ZodBoolean>;
68
75
  }, z.core.$strip>;
69
76
  export type Print = z.infer<typeof Print>;
77
+ export type PrintInput = z.input<typeof Print>;
78
+ export declare const MAX_IMPOSITION_CELLS = 10000;
79
+ export declare const Imposition: z.ZodObject<{
80
+ paper: z.ZodUnion<readonly [z.ZodEnum<{
81
+ letter: "letter";
82
+ legal: "legal";
83
+ tabloid: "tabloid";
84
+ a3: "a3";
85
+ a4: "a4";
86
+ a5: "a5";
87
+ }>, z.ZodObject<{
88
+ width: z.ZodNumber;
89
+ height: z.ZodNumber;
90
+ unit: z.ZodEnum<{
91
+ mm: "mm";
92
+ in: "in";
93
+ pt: "pt";
94
+ }>;
95
+ }, z.core.$strip>]>;
96
+ orientation: z.ZodDefault<z.ZodEnum<{
97
+ portrait: "portrait";
98
+ landscape: "landscape";
99
+ }>>;
100
+ unit: z.ZodDefault<z.ZodEnum<{
101
+ mm: "mm";
102
+ in: "in";
103
+ pt: "pt";
104
+ }>>;
105
+ margin: z.ZodDefault<z.ZodObject<{
106
+ top: z.ZodDefault<z.ZodNumber>;
107
+ right: z.ZodDefault<z.ZodNumber>;
108
+ bottom: z.ZodDefault<z.ZodNumber>;
109
+ left: z.ZodDefault<z.ZodNumber>;
110
+ }, z.core.$strip>>;
111
+ columns: z.ZodNumber;
112
+ rows: z.ZodNumber;
113
+ cell: z.ZodObject<{
114
+ width: z.ZodNumber;
115
+ height: z.ZodNumber;
116
+ }, z.core.$strip>;
117
+ gap: z.ZodDefault<z.ZodObject<{
118
+ column: z.ZodDefault<z.ZodNumber>;
119
+ row: z.ZodDefault<z.ZodNumber>;
120
+ }, z.core.$strip>>;
121
+ bleed: z.ZodDefault<z.ZodNumber>;
122
+ safe: z.ZodDefault<z.ZodNumber>;
123
+ fit: z.ZodDefault<z.ZodEnum<{
124
+ contain: "contain";
125
+ cover: "cover";
126
+ }>>;
127
+ fill: z.ZodDefault<z.ZodEnum<{
128
+ sequence: "sequence";
129
+ repeat: "repeat";
130
+ }>>;
131
+ sides: z.ZodDefault<z.ZodEnum<{
132
+ one: "one";
133
+ two: "two";
134
+ }>>;
135
+ flip: z.ZodDefault<z.ZodEnum<{
136
+ long: "long";
137
+ short: "short";
138
+ }>>;
139
+ skip: z.ZodDefault<z.ZodNumber>;
140
+ dir: z.ZodDefault<z.ZodEnum<{
141
+ ltr: "ltr";
142
+ rtl: "rtl";
143
+ }>>;
144
+ }, z.core.$strip>;
145
+ export type Imposition = z.infer<typeof Imposition>;
146
+ export type ImpositionInput = z.input<typeof Imposition>;
70
147
  export declare const PageSize: z.ZodUnion<readonly [z.ZodLiteral<"fluid">, z.ZodObject<{
71
148
  width: z.ZodNumber;
72
149
  height: z.ZodNumber;
@@ -108,6 +185,7 @@ export declare const Page: z.ZodObject<{
108
185
  unit: z.ZodEnum<{
109
186
  mm: "mm";
110
187
  in: "in";
188
+ pt: "pt";
111
189
  }>;
112
190
  }, z.core.$strip>]>;
113
191
  orientation: z.ZodDefault<z.ZodEnum<{
@@ -120,6 +198,9 @@ export declare const Page: z.ZodObject<{
120
198
  bottom: z.ZodDefault<z.ZodNumber>;
121
199
  left: z.ZodDefault<z.ZodNumber>;
122
200
  }, z.core.$strip>>;
201
+ bleed: z.ZodDefault<z.ZodNumber>;
202
+ safe: z.ZodDefault<z.ZodNumber>;
203
+ marks: z.ZodDefault<z.ZodBoolean>;
123
204
  }, z.core.$strip>>;
124
205
  }, z.core.$strip>;
125
206
  export type Page = z.infer<typeof Page>;
@@ -655,6 +736,7 @@ export declare function jsonSchemas(): {
655
736
  unit: z.ZodEnum<{
656
737
  mm: "mm";
657
738
  in: "in";
739
+ pt: "pt";
658
740
  }>;
659
741
  }, z.core.$strip>]>;
660
742
  orientation: z.ZodDefault<z.ZodEnum<{
@@ -667,7 +749,76 @@ export declare function jsonSchemas(): {
667
749
  bottom: z.ZodDefault<z.ZodNumber>;
668
750
  left: z.ZodDefault<z.ZodNumber>;
669
751
  }, z.core.$strip>>;
752
+ bleed: z.ZodDefault<z.ZodNumber>;
753
+ safe: z.ZodDefault<z.ZodNumber>;
754
+ marks: z.ZodDefault<z.ZodBoolean>;
755
+ }, z.core.$strip>>;
756
+ }, z.core.$strip>>;
757
+ imposition: z.core.ZodStandardJSONSchemaPayload<z.ZodObject<{
758
+ paper: z.ZodUnion<readonly [z.ZodEnum<{
759
+ letter: "letter";
760
+ legal: "legal";
761
+ tabloid: "tabloid";
762
+ a3: "a3";
763
+ a4: "a4";
764
+ a5: "a5";
765
+ }>, z.ZodObject<{
766
+ width: z.ZodNumber;
767
+ height: z.ZodNumber;
768
+ unit: z.ZodEnum<{
769
+ mm: "mm";
770
+ in: "in";
771
+ pt: "pt";
772
+ }>;
773
+ }, z.core.$strip>]>;
774
+ orientation: z.ZodDefault<z.ZodEnum<{
775
+ portrait: "portrait";
776
+ landscape: "landscape";
777
+ }>>;
778
+ unit: z.ZodDefault<z.ZodEnum<{
779
+ mm: "mm";
780
+ in: "in";
781
+ pt: "pt";
782
+ }>>;
783
+ margin: z.ZodDefault<z.ZodObject<{
784
+ top: z.ZodDefault<z.ZodNumber>;
785
+ right: z.ZodDefault<z.ZodNumber>;
786
+ bottom: z.ZodDefault<z.ZodNumber>;
787
+ left: z.ZodDefault<z.ZodNumber>;
788
+ }, z.core.$strip>>;
789
+ columns: z.ZodNumber;
790
+ rows: z.ZodNumber;
791
+ cell: z.ZodObject<{
792
+ width: z.ZodNumber;
793
+ height: z.ZodNumber;
794
+ }, z.core.$strip>;
795
+ gap: z.ZodDefault<z.ZodObject<{
796
+ column: z.ZodDefault<z.ZodNumber>;
797
+ row: z.ZodDefault<z.ZodNumber>;
670
798
  }, z.core.$strip>>;
799
+ bleed: z.ZodDefault<z.ZodNumber>;
800
+ safe: z.ZodDefault<z.ZodNumber>;
801
+ fit: z.ZodDefault<z.ZodEnum<{
802
+ contain: "contain";
803
+ cover: "cover";
804
+ }>>;
805
+ fill: z.ZodDefault<z.ZodEnum<{
806
+ sequence: "sequence";
807
+ repeat: "repeat";
808
+ }>>;
809
+ sides: z.ZodDefault<z.ZodEnum<{
810
+ one: "one";
811
+ two: "two";
812
+ }>>;
813
+ flip: z.ZodDefault<z.ZodEnum<{
814
+ long: "long";
815
+ short: "short";
816
+ }>>;
817
+ skip: z.ZodDefault<z.ZodNumber>;
818
+ dir: z.ZodDefault<z.ZodEnum<{
819
+ ltr: "ltr";
820
+ rtl: "rtl";
821
+ }>>;
671
822
  }, z.core.$strip>>;
672
823
  deck: z.core.ZodStandardJSONSchemaPayload<z.ZodObject<{
673
824
  title: z.ZodString;
@@ -872,14 +1023,5 @@ export declare function jsonSchemas(): {
872
1023
  }, z.core.$strip>;
873
1024
  }, z.core.$strip>>;
874
1025
  };
875
- export declare const PAPER_MM: Record<(typeof NAMED_PAPERS)[number], {
876
- width: number;
877
- height: number;
878
- }>;
879
- export declare function sheetMm(print: Print): {
880
- width: number;
881
- height: number;
882
- };
883
- export declare const PX_PER_MM: number;
884
1026
  //#endregion
885
- export { type TextDirection, firstStrongDirection, languageDirection, resolveDirection };
1027
+ export { type Imposed, MARK_GAP_MM, MARK_LENGTH_MM, MM_PER_PT, type MillimeterEdges, PAPER_MM, PX_PER_MM, type PageFit, type PagePlacementKind, type PagePlacementLayout, type PaperBox, type Placement, type TextDirection, firstStrongDirection, fitPage, impose, languageDirection, mirrorPlacement, pagePlacementLayout, printBoxes, printPadding, printProperties, resolveDirection, roundMillimeters, sheetMm, toMillimeters };
package/dist/index.js CHANGED
@@ -1,4 +1,6 @@
1
+ import { MARK_GAP_MM, MARK_LENGTH_MM, MM_PER_PT, PAPER_MM, PX_PER_MM, printBoxes, printPadding, printProperties, roundMillimeters, sheetMm, toMillimeters } from "./paper.js";
1
2
  import { firstStrongDirection, languageDirection, resolveDirection } from "./direction.js";
3
+ import { fitPage, impose, mirrorPlacement, pagePlacementLayout } from "./imposition.js";
2
4
  import { z } from "zod";
3
5
  //#region src/index.ts
4
6
  const Size = z.object({
@@ -22,7 +24,11 @@ const NAMED_PAPERS = [
22
24
  const Paper = z.union([z.enum(NAMED_PAPERS), z.object({
23
25
  width: z.number().positive(),
24
26
  height: z.number().positive(),
25
- unit: z.enum(["mm", "in"])
27
+ unit: z.enum([
28
+ "mm",
29
+ "in",
30
+ "pt"
31
+ ])
26
32
  })]);
27
33
  const Margin = z.object({
28
34
  top: z.number().nonnegative().default(0),
@@ -33,7 +39,106 @@ const Margin = z.object({
33
39
  const Print = z.object({
34
40
  paper: Paper,
35
41
  orientation: z.enum(["portrait", "landscape"]).default("portrait"),
36
- margin: Margin.optional()
42
+ margin: Margin.optional(),
43
+ bleed: z.number().nonnegative().default(0),
44
+ safe: z.number().nonnegative().default(0),
45
+ marks: z.boolean().default(false)
46
+ }).superRefine((print, ctx) => {
47
+ const trim = sheetMm(print);
48
+ if (print.safe * 2 >= trim.width) ctx.addIssue({
49
+ code: "custom",
50
+ path: ["safe"],
51
+ message: `safe is ${print.safe}mm on the left and right, but the trim is ${trim.width}mm wide; safe must leave area inside the trim`
52
+ });
53
+ if (print.safe * 2 >= trim.height) ctx.addIssue({
54
+ code: "custom",
55
+ path: ["safe"],
56
+ message: `safe is ${print.safe}mm on the top and bottom, but the trim is ${trim.height}mm high; safe must leave area inside the trim`
57
+ });
58
+ });
59
+ const ImpositionGap = z.object({
60
+ column: z.number().nonnegative().default(0),
61
+ row: z.number().nonnegative().default(0)
62
+ });
63
+ const MAX_IMPOSITION_CELLS = 1e4;
64
+ const Imposition = z.object({
65
+ paper: Paper,
66
+ orientation: z.enum(["portrait", "landscape"]).default("portrait"),
67
+ unit: z.enum([
68
+ "mm",
69
+ "in",
70
+ "pt"
71
+ ]).default("mm"),
72
+ margin: Margin.default({
73
+ top: 0,
74
+ right: 0,
75
+ bottom: 0,
76
+ left: 0
77
+ }),
78
+ columns: z.number().int().positive(),
79
+ rows: z.number().int().positive(),
80
+ cell: z.object({
81
+ width: z.number().positive(),
82
+ height: z.number().positive()
83
+ }),
84
+ gap: ImpositionGap.default({
85
+ column: 0,
86
+ row: 0
87
+ }),
88
+ bleed: z.number().nonnegative().default(0),
89
+ safe: z.number().nonnegative().default(0),
90
+ fit: z.enum(["contain", "cover"]).default("contain"),
91
+ fill: z.enum(["sequence", "repeat"]).default("sequence"),
92
+ sides: z.enum(["one", "two"]).default("one"),
93
+ flip: z.enum(["long", "short"]).default("long"),
94
+ skip: z.number().int().nonnegative().default(0),
95
+ dir: z.enum(["ltr", "rtl"]).default("ltr")
96
+ }).superRefine((imposition, ctx) => {
97
+ const cellCount = BigInt(imposition.columns) * BigInt(imposition.rows);
98
+ if (imposition.columns > 1e4 || imposition.rows > 1e4 || cellCount > BigInt(1e4)) {
99
+ ctx.addIssue({
100
+ code: "custom",
101
+ path: ["columns"],
102
+ message: `the grid has ${cellCount} cells and the ceiling is ${MAX_IMPOSITION_CELLS}; fewer columns or rows clears this refusal`
103
+ });
104
+ return;
105
+ }
106
+ const mm = (value) => toMillimeters(value, imposition.unit);
107
+ const paper = sheetMm({
108
+ paper: imposition.paper,
109
+ orientation: imposition.orientation
110
+ });
111
+ const gridWidth = mm(imposition.margin.left + imposition.columns * imposition.cell.width + (imposition.columns - 1) * imposition.gap.column + imposition.margin.right);
112
+ const gridHeight = mm(imposition.margin.top + imposition.rows * imposition.cell.height + (imposition.rows - 1) * imposition.gap.row + imposition.margin.bottom);
113
+ if (gridWidth - paper.width > .01) ctx.addIssue({
114
+ code: "custom",
115
+ path: ["columns"],
116
+ message: `the grid is ${roundMillimeters(gridWidth)}mm wide and the paper is ${paper.width}mm wide, an overflow of ${roundMillimeters(gridWidth - paper.width)}mm; reduce the horizontal margins, columns, cell width or column gap`
117
+ });
118
+ if (gridHeight - paper.height > .01) ctx.addIssue({
119
+ code: "custom",
120
+ path: ["rows"],
121
+ message: `the grid is ${roundMillimeters(gridHeight)}mm high and the paper is ${paper.height}mm high, an overflow of ${roundMillimeters(gridHeight - paper.height)}mm; reduce the vertical margins, rows, cell height or row gap`
122
+ });
123
+ const safe = mm(imposition.safe);
124
+ const cellWidth = mm(imposition.cell.width);
125
+ const cellHeight = mm(imposition.cell.height);
126
+ if (safe * 2 >= cellWidth) ctx.addIssue({
127
+ code: "custom",
128
+ path: ["safe"],
129
+ message: `safe is ${roundMillimeters(safe)}mm on the left and right, but the cell is ${roundMillimeters(cellWidth)}mm wide; safe must leave area inside the cell`
130
+ });
131
+ if (safe * 2 >= cellHeight) ctx.addIssue({
132
+ code: "custom",
133
+ path: ["safe"],
134
+ message: `safe is ${roundMillimeters(safe)}mm on the top and bottom, but the cell is ${roundMillimeters(cellHeight)}mm high; safe must leave area inside the cell`
135
+ });
136
+ const cells = imposition.columns * imposition.rows;
137
+ if (imposition.skip >= cells) ctx.addIssue({
138
+ code: "custom",
139
+ path: ["skip"],
140
+ message: `skip is ${imposition.skip}, but the grid has ${cells} cells; skip must be less than ${cells}`
141
+ });
37
142
  });
38
143
  const PageSize = z.union([
39
144
  z.literal("fluid"),
@@ -229,55 +334,11 @@ function shotsUsed(deck) {
229
334
  function jsonSchemas() {
230
335
  return {
231
336
  page: z.toJSONSchema(Page, { io: "input" }),
337
+ imposition: z.toJSONSchema(Imposition, { io: "input" }),
232
338
  deck: z.toJSONSchema(DeckShape, { io: "input" }),
233
339
  manifest: z.toJSONSchema(Manifest, { io: "input" }),
234
340
  publishedShot: z.toJSONSchema(PublishedShot, { io: "input" })
235
341
  };
236
342
  }
237
- const PAPER_MM = {
238
- letter: {
239
- width: 215.9,
240
- height: 279.4
241
- },
242
- legal: {
243
- width: 215.9,
244
- height: 355.6
245
- },
246
- tabloid: {
247
- width: 279.4,
248
- height: 431.8
249
- },
250
- a3: {
251
- width: 297,
252
- height: 420
253
- },
254
- a4: {
255
- width: 210,
256
- height: 297
257
- },
258
- a5: {
259
- width: 148,
260
- height: 210
261
- }
262
- };
263
- function sheetMm(print) {
264
- const portrait = typeof print.paper === "string" ? PAPER_MM[print.paper] : print.paper.unit === "in" ? {
265
- width: print.paper.width * 25.4,
266
- height: print.paper.height * 25.4
267
- } : {
268
- width: print.paper.width,
269
- height: print.paper.height
270
- };
271
- const [short, long] = [Math.min(portrait.width, portrait.height), Math.max(portrait.width, portrait.height)];
272
- const round = (mm) => Math.round(mm * 100) / 100;
273
- return print.orientation === "landscape" ? {
274
- width: round(long),
275
- height: round(short)
276
- } : {
277
- width: round(short),
278
- height: round(long)
279
- };
280
- }
281
- const PX_PER_MM = 96 / 25.4;
282
343
  //#endregion
283
- export { Annotation, Box, Cue, DECK_SIZE, Deck, Direction, Manifest, Margin, NAMED_PAPERS, Narration, PAPER_MM, PX_PER_MM, Page, PageSize, Paper, Point, Print, PublishedShot, SHOT_KEY, Scripts, Shot, Size, Slide, SlideContent, Word, annotationCueIssues, exceedsLimits, firstStrongDirection, jsonSchemas, languageDirection, resolveDirection, sheetMm, shotsUsed };
344
+ export { Annotation, Box, Cue, DECK_SIZE, Deck, Direction, Imposition, MARK_GAP_MM, MARK_LENGTH_MM, MAX_IMPOSITION_CELLS, MM_PER_PT, Manifest, Margin, NAMED_PAPERS, Narration, PAPER_MM, PX_PER_MM, Page, PageSize, Paper, Point, Print, PublishedShot, SHOT_KEY, Scripts, Shot, Size, Slide, SlideContent, Word, annotationCueIssues, exceedsLimits, firstStrongDirection, fitPage, impose, jsonSchemas, languageDirection, mirrorPlacement, pagePlacementLayout, printBoxes, printPadding, printProperties, resolveDirection, roundMillimeters, sheetMm, shotsUsed, toMillimeters };
@@ -0,0 +1,47 @@
1
+ import { Paper, Print } from "./index.js";
2
+ //#region src/paper.d.ts
3
+ export declare const MM_PER_PT: number;
4
+ export declare const MARK_LENGTH_MM = 5;
5
+ export declare const MARK_GAP_MM = 1;
6
+ export declare const PAPER_MM: Record<Extract<Paper, string>, {
7
+ width: number;
8
+ height: number;
9
+ }>;
10
+ export declare const roundMillimeters: (mm: number) => number;
11
+ export declare function toMillimeters(value: number, unit: 'mm' | 'in' | 'pt'): number;
12
+ export declare function sheetMm(print: Pick<Print, 'paper' | 'orientation'>): {
13
+ width: number;
14
+ height: number;
15
+ };
16
+ export declare const PX_PER_MM: number;
17
+ export declare function printProperties(print: Pick<Print, 'bleed' | 'safe'>, unit?: 'mm' | 'in' | 'pt'): string;
18
+ export declare function printPadding(edges: {
19
+ top: number;
20
+ right: number;
21
+ bottom: number;
22
+ left: number;
23
+ }): string;
24
+ export interface PaperBox {
25
+ x: number;
26
+ y: number;
27
+ width: number;
28
+ height: number;
29
+ }
30
+ export interface MarkSegment {
31
+ x1: number;
32
+ y1: number;
33
+ x2: number;
34
+ y2: number;
35
+ }
36
+ export interface PrintBoxes {
37
+ media: {
38
+ width: number;
39
+ height: number;
40
+ };
41
+ bleedBox: PaperBox;
42
+ trim: PaperBox;
43
+ safeBox: PaperBox;
44
+ marks: MarkSegment[];
45
+ }
46
+ export declare function printBoxes(print: Print): PrintBoxes;
47
+ //#endregion
package/dist/paper.js ADDED
@@ -0,0 +1,159 @@
1
+ //#region src/paper.ts
2
+ const MM_PER_PT = 25.4 / 72;
3
+ const MARK_LENGTH_MM = 5;
4
+ const MARK_GAP_MM = 1;
5
+ const PAPER_MM = {
6
+ letter: {
7
+ width: 215.9,
8
+ height: 279.4
9
+ },
10
+ legal: {
11
+ width: 215.9,
12
+ height: 355.6
13
+ },
14
+ tabloid: {
15
+ width: 279.4,
16
+ height: 431.8
17
+ },
18
+ a3: {
19
+ width: 297,
20
+ height: 420
21
+ },
22
+ a4: {
23
+ width: 210,
24
+ height: 297
25
+ },
26
+ a5: {
27
+ width: 148,
28
+ height: 210
29
+ }
30
+ };
31
+ const roundMillimeters = (mm) => Math.round(mm * 100) / 100;
32
+ function toMillimeters(value, unit) {
33
+ return value * (unit === "in" ? 25.4 : unit === "pt" ? MM_PER_PT : 1);
34
+ }
35
+ function sheetMm(print) {
36
+ const portrait = typeof print.paper === "string" ? PAPER_MM[print.paper] : {
37
+ width: toMillimeters(print.paper.width, print.paper.unit),
38
+ height: toMillimeters(print.paper.height, print.paper.unit)
39
+ };
40
+ const [short, long] = [Math.min(portrait.width, portrait.height), Math.max(portrait.width, portrait.height)];
41
+ return print.orientation === "landscape" ? {
42
+ width: roundMillimeters(long),
43
+ height: roundMillimeters(short)
44
+ } : {
45
+ width: roundMillimeters(short),
46
+ height: roundMillimeters(long)
47
+ };
48
+ }
49
+ const PX_PER_MM = 96 / 25.4;
50
+ function printProperties(print, unit = "mm") {
51
+ return `:root{--segnavia-bleed:${roundMillimeters(toMillimeters(print.bleed, unit))}mm;--segnavia-safe:${roundMillimeters(toMillimeters(print.safe, unit))}mm}`;
52
+ }
53
+ function printPadding(edges) {
54
+ return `:where(html){padding:${[
55
+ edges.top,
56
+ edges.right,
57
+ edges.bottom,
58
+ edges.left
59
+ ].map((value) => `${value}mm`).join(" ")}}`;
60
+ }
61
+ function printBoxes(print) {
62
+ const sheet = sheetMm(print);
63
+ const outside = print.marks ? 6 : 0;
64
+ const bleedBox = {
65
+ x: outside,
66
+ y: outside,
67
+ width: roundMillimeters(sheet.width + print.bleed * 2),
68
+ height: roundMillimeters(sheet.height + print.bleed * 2)
69
+ };
70
+ const trim = {
71
+ x: roundMillimeters(outside + print.bleed),
72
+ y: roundMillimeters(outside + print.bleed),
73
+ width: sheet.width,
74
+ height: sheet.height
75
+ };
76
+ const safeBox = {
77
+ x: roundMillimeters(trim.x + print.safe),
78
+ y: roundMillimeters(trim.y + print.safe),
79
+ width: roundMillimeters(trim.width - print.safe * 2),
80
+ height: roundMillimeters(trim.height - print.safe * 2)
81
+ };
82
+ const media = {
83
+ width: roundMillimeters(bleedBox.width + outside * 2),
84
+ height: roundMillimeters(bleedBox.height + outside * 2)
85
+ };
86
+ if (!print.marks) return {
87
+ media,
88
+ bleedBox,
89
+ trim,
90
+ safeBox,
91
+ marks: []
92
+ };
93
+ const leftStart = roundMillimeters(bleedBox.x - 1);
94
+ const rightStart = roundMillimeters(bleedBox.x + bleedBox.width + 1);
95
+ const topStart = roundMillimeters(bleedBox.y - 1);
96
+ const bottomStart = roundMillimeters(bleedBox.y + bleedBox.height + 1);
97
+ const right = roundMillimeters(rightStart + 5);
98
+ const bottom = roundMillimeters(bottomStart + 5);
99
+ const trimRight = roundMillimeters(trim.x + trim.width);
100
+ const trimBottom = roundMillimeters(trim.y + trim.height);
101
+ return {
102
+ media,
103
+ bleedBox,
104
+ trim,
105
+ safeBox,
106
+ marks: [
107
+ {
108
+ x1: roundMillimeters(leftStart - 5),
109
+ y1: trim.y,
110
+ x2: leftStart,
111
+ y2: trim.y
112
+ },
113
+ {
114
+ x1: trim.x,
115
+ y1: roundMillimeters(topStart - 5),
116
+ x2: trim.x,
117
+ y2: topStart
118
+ },
119
+ {
120
+ x1: rightStart,
121
+ y1: trim.y,
122
+ x2: right,
123
+ y2: trim.y
124
+ },
125
+ {
126
+ x1: trimRight,
127
+ y1: roundMillimeters(topStart - 5),
128
+ x2: trimRight,
129
+ y2: topStart
130
+ },
131
+ {
132
+ x1: roundMillimeters(leftStart - 5),
133
+ y1: trimBottom,
134
+ x2: leftStart,
135
+ y2: trimBottom
136
+ },
137
+ {
138
+ x1: trim.x,
139
+ y1: bottomStart,
140
+ x2: trim.x,
141
+ y2: bottom
142
+ },
143
+ {
144
+ x1: rightStart,
145
+ y1: trimBottom,
146
+ x2: right,
147
+ y2: trimBottom
148
+ },
149
+ {
150
+ x1: trimRight,
151
+ y1: bottomStart,
152
+ x2: trimRight,
153
+ y2: bottom
154
+ }
155
+ ]
156
+ };
157
+ }
158
+ //#endregion
159
+ export { MARK_GAP_MM, MARK_LENGTH_MM, MM_PER_PT, PAPER_MM, PX_PER_MM, printBoxes, printPadding, printProperties, roundMillimeters, sheetMm, toMillimeters };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@segnavia/format",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "repository": {