@segnavia/format 0.38.0 → 0.39.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.
Files changed (59) hide show
  1. package/dist/board-edit.d.ts +2 -1
  2. package/dist/board-geometry.d.ts +2 -1
  3. package/dist/board-section-grid.d.ts +2 -1
  4. package/dist/board-sections.d.ts +2 -1
  5. package/dist/board.d.ts +8 -8
  6. package/dist/board.js +15 -11
  7. package/dist/book-exports.d.ts +2 -2
  8. package/dist/book-section.d.ts +171 -0
  9. package/dist/book-section.js +13 -2
  10. package/dist/book.d.ts +185 -43
  11. package/dist/book.js +18 -2
  12. package/dist/cover.d.ts +337 -0
  13. package/dist/cover.js +100 -0
  14. package/dist/deck-from-slides.d.ts +23 -4
  15. package/dist/deck.d.ts +144 -42
  16. package/dist/deck.js +22 -24
  17. package/dist/direction.d.ts +1 -1
  18. package/dist/font-collection.d.ts +8 -0
  19. package/dist/font-collection.js +191 -0
  20. package/dist/font-face.d.ts +8 -0
  21. package/dist/font-face.js +24 -0
  22. package/dist/font-refusal.d.ts +5 -0
  23. package/dist/font-refusal.js +9 -0
  24. package/dist/font-signature.d.ts +5 -0
  25. package/dist/font-signature.js +13 -0
  26. package/dist/fonts.d.ts +6 -1
  27. package/dist/fonts.js +3 -1
  28. package/dist/geometry.d.ts +20 -0
  29. package/dist/geometry.js +18 -0
  30. package/dist/image-fit.d.ts +10 -0
  31. package/dist/image-fit.js +10 -0
  32. package/dist/imposition-marks.d.ts +1 -1
  33. package/dist/imposition-schema.d.ts +185 -0
  34. package/dist/imposition-schema.js +122 -0
  35. package/dist/imposition.d.ts +8 -6
  36. package/dist/index.d.ts +23 -1756
  37. package/dist/index.js +29 -212
  38. package/dist/json-schemas.d.ts +1860 -0
  39. package/dist/json-schemas.js +43 -0
  40. package/dist/media.d.ts +152 -0
  41. package/dist/media.js +119 -0
  42. package/dist/note-series.d.ts +4 -4
  43. package/dist/note-series.js +0 -5
  44. package/dist/page.d.ts +174 -0
  45. package/dist/page.js +37 -0
  46. package/dist/paper.d.ts +1 -1
  47. package/dist/placement-cycle.d.ts +5 -0
  48. package/dist/placement-cycle.js +30 -0
  49. package/dist/placement-refusal.d.ts +5 -0
  50. package/dist/placement-refusal.js +6 -0
  51. package/dist/placement.d.ts +76 -0
  52. package/dist/placement.js +100 -0
  53. package/dist/print.d.ts +72 -0
  54. package/dist/print.js +38 -0
  55. package/dist/printed-page-imposition.d.ts +2 -1
  56. package/dist/shots-used.d.ts +0 -1
  57. package/dist/shots-used.js +2 -2
  58. package/dist/slots.d.ts +13 -13
  59. package/package.json +1 -1
package/dist/deck.d.ts CHANGED
@@ -1,29 +1,7 @@
1
+ import { Box, Point, Size } from "./geometry.js";
2
+ import { ImageFit } from "./image-fit.js";
1
3
  import { z } from "zod";
2
4
  //#region src/deck.d.ts
3
- export declare const Size: z.ZodObject<{
4
- width: z.ZodNumber;
5
- height: z.ZodNumber;
6
- }, z.core.$strip>;
7
- export type Size = z.infer<typeof Size>;
8
- export declare const Box: z.ZodObject<{
9
- x: z.ZodNumber;
10
- y: z.ZodNumber;
11
- width: z.ZodNumber;
12
- height: z.ZodNumber;
13
- }, z.core.$strip>;
14
- export type Box = z.infer<typeof Box>;
15
- export declare const Point: z.ZodObject<{
16
- x: z.ZodNumber;
17
- y: z.ZodNumber;
18
- }, z.core.$strip>;
19
- export type Point = z.infer<typeof Point>;
20
- export declare const ImageFit: z.ZodEnum<{
21
- width: "width";
22
- contain: "contain";
23
- cover: "cover";
24
- focus: "focus";
25
- }>;
26
- export type ImageFit = z.infer<typeof ImageFit>;
27
5
  export declare const Annotation: z.ZodObject<{
28
6
  ring: z.ZodNullable<z.ZodObject<{
29
7
  x: z.ZodNumber;
@@ -163,9 +141,9 @@ export declare const Narration: z.ZodObject<{
163
141
  text: z.ZodString;
164
142
  lang: z.ZodOptional<z.ZodString>;
165
143
  dir: z.ZodOptional<z.ZodEnum<{
144
+ auto: "auto";
166
145
  ltr: "ltr";
167
146
  rtl: "rtl";
168
- auto: "auto";
169
147
  }>>;
170
148
  voice: z.ZodOptional<z.ZodString>;
171
149
  words: z.ZodArray<z.ZodObject<{
@@ -183,7 +161,7 @@ export declare const Narration: z.ZodObject<{
183
161
  }, z.core.$strip>>>;
184
162
  }, z.core.$strip>;
185
163
  export type Narration = z.infer<typeof Narration>;
186
- export declare const SlideContent: z.ZodDiscriminatedUnion<[z.ZodObject<{
164
+ export declare const SlideContent: z.ZodUnion<readonly [z.ZodDiscriminatedUnion<[z.ZodObject<{
187
165
  kind: z.ZodLiteral<"html">;
188
166
  html: z.ZodString;
189
167
  }, z.core.$strip>, z.ZodObject<{
@@ -265,11 +243,52 @@ export declare const SlideContent: z.ZodDiscriminatedUnion<[z.ZodObject<{
265
243
  focus: "focus";
266
244
  }>>;
267
245
  frame: z.ZodOptional<z.ZodBoolean>;
268
- }, z.core.$strip>], "kind">;
246
+ }, z.core.$strip>], "kind">, z.ZodObject<{
247
+ media: z.ZodObject<{
248
+ kind: z.ZodEnum<{
249
+ deck: "deck";
250
+ page: "page";
251
+ paper: "paper";
252
+ book: "book";
253
+ media: "media";
254
+ shot: "shot";
255
+ }>;
256
+ address: z.ZodString;
257
+ page: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>;
258
+ box: z.ZodOptional<z.ZodEnum<{
259
+ media: "media";
260
+ bleed: "bleed";
261
+ crop: "crop";
262
+ trim: "trim";
263
+ art: "art";
264
+ }>>;
265
+ crop: z.ZodOptional<z.ZodObject<{
266
+ top: z.ZodOptional<z.ZodString>;
267
+ right: z.ZodOptional<z.ZodString>;
268
+ bottom: z.ZodOptional<z.ZodString>;
269
+ left: z.ZodOptional<z.ZodString>;
270
+ }, z.core.$strip>>;
271
+ fit: z.ZodOptional<z.ZodEnum<{
272
+ width: "width";
273
+ contain: "contain";
274
+ cover: "cover";
275
+ focus: "focus";
276
+ }>>;
277
+ alt: z.ZodOptional<z.ZodObject<{
278
+ text: z.ZodString;
279
+ lang: z.ZodOptional<z.ZodString>;
280
+ dir: z.ZodOptional<z.ZodEnum<{
281
+ auto: "auto";
282
+ ltr: "ltr";
283
+ rtl: "rtl";
284
+ }>>;
285
+ }, z.core.$strip>>;
286
+ }, z.core.$strip>;
287
+ }, z.core.$strip>]>;
269
288
  export type SlideContent = z.infer<typeof SlideContent>;
270
289
  export declare const Slide: z.ZodObject<{
271
290
  id: z.ZodString;
272
- content: z.ZodDiscriminatedUnion<[z.ZodObject<{
291
+ content: z.ZodUnion<readonly [z.ZodDiscriminatedUnion<[z.ZodObject<{
273
292
  kind: z.ZodLiteral<"html">;
274
293
  html: z.ZodString;
275
294
  }, z.core.$strip>, z.ZodObject<{
@@ -351,15 +370,56 @@ export declare const Slide: z.ZodObject<{
351
370
  focus: "focus";
352
371
  }>>;
353
372
  frame: z.ZodOptional<z.ZodBoolean>;
354
- }, z.core.$strip>], "kind">;
373
+ }, z.core.$strip>], "kind">, z.ZodObject<{
374
+ media: z.ZodObject<{
375
+ kind: z.ZodEnum<{
376
+ deck: "deck";
377
+ page: "page";
378
+ paper: "paper";
379
+ book: "book";
380
+ media: "media";
381
+ shot: "shot";
382
+ }>;
383
+ address: z.ZodString;
384
+ page: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>;
385
+ box: z.ZodOptional<z.ZodEnum<{
386
+ media: "media";
387
+ bleed: "bleed";
388
+ crop: "crop";
389
+ trim: "trim";
390
+ art: "art";
391
+ }>>;
392
+ crop: z.ZodOptional<z.ZodObject<{
393
+ top: z.ZodOptional<z.ZodString>;
394
+ right: z.ZodOptional<z.ZodString>;
395
+ bottom: z.ZodOptional<z.ZodString>;
396
+ left: z.ZodOptional<z.ZodString>;
397
+ }, z.core.$strip>>;
398
+ fit: z.ZodOptional<z.ZodEnum<{
399
+ width: "width";
400
+ contain: "contain";
401
+ cover: "cover";
402
+ focus: "focus";
403
+ }>>;
404
+ alt: z.ZodOptional<z.ZodObject<{
405
+ text: z.ZodString;
406
+ lang: z.ZodOptional<z.ZodString>;
407
+ dir: z.ZodOptional<z.ZodEnum<{
408
+ auto: "auto";
409
+ ltr: "ltr";
410
+ rtl: "rtl";
411
+ }>>;
412
+ }, z.core.$strip>>;
413
+ }, z.core.$strip>;
414
+ }, z.core.$strip>]>;
355
415
  notes: z.ZodOptional<z.ZodString>;
356
416
  narration: z.ZodOptional<z.ZodObject<{
357
417
  text: z.ZodString;
358
418
  lang: z.ZodOptional<z.ZodString>;
359
419
  dir: z.ZodOptional<z.ZodEnum<{
420
+ auto: "auto";
360
421
  ltr: "ltr";
361
422
  rtl: "rtl";
362
- auto: "auto";
363
423
  }>>;
364
424
  voice: z.ZodOptional<z.ZodString>;
365
425
  words: z.ZodArray<z.ZodObject<{
@@ -389,9 +449,9 @@ export declare const DECK_SIZE: {
389
449
  export declare const PLAYBACK_MODES: readonly ["segmented", "continuous"];
390
450
  export type PlaybackMode = (typeof PLAYBACK_MODES)[number];
391
451
  export declare const Playback: z.ZodUnion<readonly [z.ZodEnum<{
392
- continuous: "continuous";
393
452
  segmented: "segmented";
394
- }>, z.ZodPipe<z.ZodLiteral<"step">, z.ZodTransform<"continuous" | "segmented", "step">>]>;
453
+ continuous: "continuous";
454
+ }>, z.ZodPipe<z.ZodLiteral<"step">, z.ZodTransform<"segmented" | "continuous", "step">>]>;
395
455
  export declare function parsePlayback(value: unknown): PlaybackMode | undefined;
396
456
  export declare function annotationCueIssues(narration: Slide['narration'], annotations: number): string[];
397
457
  export declare const Deck: z.ZodObject<{
@@ -416,9 +476,9 @@ export declare const Deck: z.ZodObject<{
416
476
  sandboxed: "sandboxed";
417
477
  }>>;
418
478
  dir: z.ZodOptional<z.ZodEnum<{
479
+ auto: "auto";
419
480
  ltr: "ltr";
420
481
  rtl: "rtl";
421
- auto: "auto";
422
482
  }>>;
423
483
  lang: z.ZodOptional<z.ZodString>;
424
484
  transition: z.ZodOptional<z.ZodObject<{
@@ -430,9 +490,9 @@ export declare const Deck: z.ZodObject<{
430
490
  duration: z.ZodDefault<z.ZodNumber>;
431
491
  }, z.core.$strip>>;
432
492
  playback: z.ZodOptional<z.ZodUnion<readonly [z.ZodEnum<{
433
- continuous: "continuous";
434
493
  segmented: "segmented";
435
- }>, z.ZodPipe<z.ZodLiteral<"step">, z.ZodTransform<"continuous" | "segmented", "step">>]>>;
494
+ continuous: "continuous";
495
+ }>, z.ZodPipe<z.ZodLiteral<"step">, z.ZodTransform<"segmented" | "continuous", "step">>]>>;
436
496
  captions: z.ZodOptional<z.ZodEnum<{
437
497
  on: "on";
438
498
  off: "off";
@@ -443,11 +503,11 @@ export declare const Deck: z.ZodObject<{
443
503
  label: z.ZodString;
444
504
  type: z.ZodEnum<{
445
505
  text: "text";
506
+ image: "image";
446
507
  phone: "phone";
447
508
  email: "email";
448
509
  link: "link";
449
510
  qr: "qr";
450
- image: "image";
451
511
  }>;
452
512
  required: z.ZodDefault<z.ZodBoolean>;
453
513
  placeholder: z.ZodOptional<z.ZodString>;
@@ -461,7 +521,7 @@ export declare const Deck: z.ZodObject<{
461
521
  }, z.core.$strip>, z.ZodTransform<{
462
522
  key: string;
463
523
  label: string;
464
- type: "text" | "phone" | "email" | "link" | "qr" | "image";
524
+ type: "text" | "image" | "phone" | "email" | "link" | "qr";
465
525
  required: boolean;
466
526
  overflow: "refuse" | "shrink" | "truncate" | "wrap";
467
527
  placeholder?: string | undefined;
@@ -469,7 +529,7 @@ export declare const Deck: z.ZodObject<{
469
529
  }, {
470
530
  key: string;
471
531
  label: string;
472
- type: "text" | "phone" | "email" | "link" | "qr" | "image";
532
+ type: "text" | "image" | "phone" | "email" | "link" | "qr";
473
533
  required: boolean;
474
534
  overflow: "refuse" | "shrink" | "truncate" | "wrap";
475
535
  placeholder?: string | undefined;
@@ -477,7 +537,7 @@ export declare const Deck: z.ZodObject<{
477
537
  }>>>>;
478
538
  slides: z.ZodArray<z.ZodObject<{
479
539
  id: z.ZodString;
480
- content: z.ZodDiscriminatedUnion<[z.ZodObject<{
540
+ content: z.ZodUnion<readonly [z.ZodDiscriminatedUnion<[z.ZodObject<{
481
541
  kind: z.ZodLiteral<"html">;
482
542
  html: z.ZodString;
483
543
  }, z.core.$strip>, z.ZodObject<{
@@ -559,15 +619,56 @@ export declare const Deck: z.ZodObject<{
559
619
  focus: "focus";
560
620
  }>>;
561
621
  frame: z.ZodOptional<z.ZodBoolean>;
562
- }, z.core.$strip>], "kind">;
622
+ }, z.core.$strip>], "kind">, z.ZodObject<{
623
+ media: z.ZodObject<{
624
+ kind: z.ZodEnum<{
625
+ deck: "deck";
626
+ page: "page";
627
+ paper: "paper";
628
+ book: "book";
629
+ media: "media";
630
+ shot: "shot";
631
+ }>;
632
+ address: z.ZodString;
633
+ page: z.ZodOptional<z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>>;
634
+ box: z.ZodOptional<z.ZodEnum<{
635
+ media: "media";
636
+ bleed: "bleed";
637
+ crop: "crop";
638
+ trim: "trim";
639
+ art: "art";
640
+ }>>;
641
+ crop: z.ZodOptional<z.ZodObject<{
642
+ top: z.ZodOptional<z.ZodString>;
643
+ right: z.ZodOptional<z.ZodString>;
644
+ bottom: z.ZodOptional<z.ZodString>;
645
+ left: z.ZodOptional<z.ZodString>;
646
+ }, z.core.$strip>>;
647
+ fit: z.ZodOptional<z.ZodEnum<{
648
+ width: "width";
649
+ contain: "contain";
650
+ cover: "cover";
651
+ focus: "focus";
652
+ }>>;
653
+ alt: z.ZodOptional<z.ZodObject<{
654
+ text: z.ZodString;
655
+ lang: z.ZodOptional<z.ZodString>;
656
+ dir: z.ZodOptional<z.ZodEnum<{
657
+ auto: "auto";
658
+ ltr: "ltr";
659
+ rtl: "rtl";
660
+ }>>;
661
+ }, z.core.$strip>>;
662
+ }, z.core.$strip>;
663
+ }, z.core.$strip>]>;
563
664
  notes: z.ZodOptional<z.ZodString>;
564
665
  narration: z.ZodOptional<z.ZodObject<{
565
666
  text: z.ZodString;
566
667
  lang: z.ZodOptional<z.ZodString>;
567
668
  dir: z.ZodOptional<z.ZodEnum<{
669
+ auto: "auto";
568
670
  ltr: "ltr";
569
671
  rtl: "rtl";
570
- auto: "auto";
571
672
  }>>;
572
673
  voice: z.ZodOptional<z.ZodString>;
573
674
  words: z.ZodArray<z.ZodObject<{
@@ -647,4 +748,5 @@ export interface DeckLimits {
647
748
  maxNotesBytes?: number;
648
749
  }
649
750
  export declare function exceedsLimits(deck: Deck, limits: DeckLimits): string[];
650
- //#endregion
751
+ //#endregion
752
+ export { Box, ImageFit, Point, Size };
package/dist/deck.js CHANGED
@@ -1,30 +1,13 @@
1
1
  import { Direction } from "./direction.js";
2
+ import { Box, Point, Size } from "./geometry.js";
3
+ import { ImageFit } from "./image-fit.js";
4
+ import { Placement, PlacementShape } from "./placement.js";
2
5
  import { Scripts } from "./scripts.js";
3
6
  import { Slots } from "./slots.js";
4
7
  import { SlideTiming, slideTimingIssues } from "./timing.js";
5
8
  import { Transition } from "./transition.js";
6
9
  import { z } from "zod";
7
10
  //#region src/deck.ts
8
- const Size = z.object({
9
- width: z.number().int().positive(),
10
- height: z.number().int().positive()
11
- });
12
- const Box = z.object({
13
- x: z.number(),
14
- y: z.number(),
15
- width: z.number(),
16
- height: z.number()
17
- });
18
- const Point = z.object({
19
- x: z.number(),
20
- y: z.number()
21
- });
22
- const ImageFit = z.enum([
23
- "contain",
24
- "cover",
25
- "width",
26
- "focus"
27
- ]);
28
11
  const Annotation = z.object({
29
12
  ring: Box.nullable(),
30
13
  badge: z.object({
@@ -77,7 +60,7 @@ const Narration = z.object({
77
60
  live: z.boolean().default(false),
78
61
  cues: z.array(Cue).default([])
79
62
  });
80
- const SlideContent = z.discriminatedUnion("kind", [
63
+ const DirectSlideContent = z.discriminatedUnion("kind", [
81
64
  z.object({
82
65
  kind: z.literal("html"),
83
66
  html: z.string()
@@ -107,6 +90,7 @@ const SlideContent = z.discriminatedUnion("kind", [
107
90
  frame: z.boolean().optional()
108
91
  })
109
92
  ]);
93
+ const SlideContent = z.union([DirectSlideContent, z.object({ media: PlacementShape })]);
110
94
  const Slide = z.object({
111
95
  id: z.string(),
112
96
  content: SlideContent,
@@ -153,18 +137,32 @@ function annotationCueIssues(narration, annotations) {
153
137
  function cueIssues(slide) {
154
138
  const narration = slide.narration;
155
139
  if (!narration) return [];
156
- const annotations = slide.content.kind === "shot" ? slide.content.shot.annotations.length : null;
140
+ const annotations = "kind" in slide.content && slide.content.kind === "shot" ? slide.content.shot.annotations.length : null;
157
141
  const words = narration.live ? null : narration.words.length;
158
142
  const issues = [];
159
143
  for (const cue of narration.cues) {
160
144
  for (const word of [cue.word, cue.untilWord]) if (word !== void 0 && words !== null && word >= words) issues.push(`a cue names word ${word}, but the narration has ${words} words`);
161
- if (slide.content.kind !== "shot" && slide.content.kind !== "shot-ref") issues.push("a cue names an annotation, but only a shot carries annotations");
145
+ if (!("kind" in slide.content) || slide.content.kind !== "shot" && slide.content.kind !== "shot-ref") issues.push("a cue names an annotation, but only a shot carries annotations");
162
146
  }
163
147
  if (annotations !== null) issues.push(...annotationCueIssues(narration, annotations));
164
148
  return issues;
165
149
  }
166
150
  const Deck = DeckShape.superRefine((deck, ctx) => {
167
151
  deck.slides.forEach((slide, index) => {
152
+ if ("media" in slide.content) try {
153
+ Placement.parse(slide.content.media, "deck");
154
+ } catch (error) {
155
+ ctx.addIssue({
156
+ code: "custom",
157
+ path: [
158
+ "slides",
159
+ index,
160
+ "content",
161
+ "media"
162
+ ],
163
+ message: error instanceof Error ? error.message : String(error)
164
+ });
165
+ }
168
166
  for (const message of cueIssues(slide)) ctx.addIssue({
169
167
  code: "custom",
170
168
  path: [
@@ -211,7 +209,7 @@ function exceedsLimits(deck, limits) {
211
209
  const bytes = (text) => new TextEncoder().encode(text).length;
212
210
  if (limits.maxSlides !== void 0 && deck.slides.length > limits.maxSlides) reasons.push(`the deck has ${deck.slides.length} slides; the limit is ${limits.maxSlides}`);
213
211
  deck.slides.forEach((slide, index) => {
214
- if (limits.maxSlideBytes !== void 0 && slide.content.kind === "html" && bytes(slide.content.html) > limits.maxSlideBytes) reasons.push(`slide ${index + 1} is ${bytes(slide.content.html)} bytes; the limit is ${limits.maxSlideBytes}`);
212
+ if (limits.maxSlideBytes !== void 0 && "kind" in slide.content && slide.content.kind === "html" && bytes(slide.content.html) > limits.maxSlideBytes) reasons.push(`slide ${index + 1} is ${bytes(slide.content.html)} bytes; the limit is ${limits.maxSlideBytes}`);
215
213
  if (limits.maxNotesBytes !== void 0 && slide.notes && bytes(slide.notes) > limits.maxNotesBytes) reasons.push(`slide ${index + 1} has ${bytes(slide.notes)} bytes of notes; the limit is ${limits.maxNotesBytes}`);
216
214
  });
217
215
  return reasons;
@@ -1,9 +1,9 @@
1
1
  import { z } from "zod";
2
2
  //#region src/direction.d.ts
3
3
  export declare const Direction: z.ZodEnum<{
4
+ auto: "auto";
4
5
  ltr: "ltr";
5
6
  rtl: "rtl";
6
- auto: "auto";
7
7
  }>;
8
8
  export declare const TextDirection: z.ZodEnum<{
9
9
  ltr: "ltr";
@@ -0,0 +1,8 @@
1
+ //#region src/font-collection.d.ts
2
+ export type ExtractedFontFace = {
3
+ data: Uint8Array;
4
+ format: 'truetype' | 'opentype';
5
+ };
6
+ export declare function collectionFaces(bytes: Uint8Array): string[];
7
+ export declare function extractFace(bytes: Uint8Array, postscriptName: string): ExtractedFontFace;
8
+ //#endregion
@@ -0,0 +1,191 @@
1
+ import { FontFaceRefusedError } from "./font-refusal.js";
2
+ import { classifyFont } from "./font-signature.js";
3
+ //#region src/font-collection.ts
4
+ const MAX_COLLECTION_FACES = 64;
5
+ const MAX_FACE_TABLES = 64;
6
+ const MAX_FACE_NAME_RECORDS = 1024;
7
+ const MAX_FACE_NAME_BYTES = 1048576;
8
+ const CHECKSUM_MAGIC = 2981146554;
9
+ function tagAt(bytes, offset) {
10
+ return String.fromCharCode(bytes[offset] ?? 0, bytes[offset + 1] ?? 0, bytes[offset + 2] ?? 0, bytes[offset + 3] ?? 0);
11
+ }
12
+ function refuse(message, remedy) {
13
+ throw new FontFaceRefusedError(`${message}; ${remedy} to clear this refusal.`);
14
+ }
15
+ function malformedCollection(offset) {
16
+ refuse(`font collection header is malformed at byte ${offset}`, "supply a complete font collection");
17
+ }
18
+ function malformedFace(offset) {
19
+ refuse(`font face table directory is malformed at byte ${offset}`, "supply a complete supported font face");
20
+ }
21
+ function collectionView(bytes) {
22
+ return new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
23
+ }
24
+ function requireCollectionRange(bytes, offset, length) {
25
+ const end = offset + length;
26
+ if (!Number.isSafeInteger(offset) || !Number.isSafeInteger(length) || !Number.isSafeInteger(end) || offset < 0 || length < 0 || end > bytes.length) malformedCollection(Math.max(0, offset));
27
+ }
28
+ function requireFaceRange(bytes, offset, length) {
29
+ const end = offset + length;
30
+ if (!Number.isSafeInteger(offset) || !Number.isSafeInteger(length) || !Number.isSafeInteger(end) || offset < 0 || length < 0 || end > bytes.length) malformedFace(Math.max(0, offset));
31
+ }
32
+ function collectionOffsets(bytes) {
33
+ requireCollectionRange(bytes, 0, 12);
34
+ if (tagAt(bytes, 0) !== "ttcf") malformedCollection(0);
35
+ const view = collectionView(bytes);
36
+ const version = view.getUint32(4);
37
+ if (version !== 65536 && version !== 131072) malformedCollection(4);
38
+ const count = view.getUint32(8);
39
+ if (count > MAX_COLLECTION_FACES) refuse(`font collection declares ${count} faces, above the limit ${MAX_COLLECTION_FACES}`, `supply a collection with at most ${MAX_COLLECTION_FACES} faces`);
40
+ requireCollectionRange(bytes, 12, count * 4);
41
+ const offsets = [];
42
+ for (let index = 0; index < count; index += 1) offsets.push(view.getUint32(12 + index * 4));
43
+ if (version === 131072) requireCollectionRange(bytes, 12 + count * 4, 12);
44
+ return offsets;
45
+ }
46
+ function sfntFormat(bytes, offset) {
47
+ requireFaceRange(bytes, offset, 4);
48
+ const format = classifyFont(bytes, offset);
49
+ if (format === "opentype" || format === "truetype") return format;
50
+ malformedFace(offset);
51
+ }
52
+ function faceTables(bytes, offset) {
53
+ requireFaceRange(bytes, offset, 12);
54
+ const view = collectionView(bytes);
55
+ const count = view.getUint16(offset + 4);
56
+ if (count > MAX_FACE_TABLES) refuse(`font face declares ${count} tables, above the limit ${MAX_FACE_TABLES}`, `supply a face with at most ${MAX_FACE_TABLES} tables`);
57
+ requireFaceRange(bytes, offset + 12, count * 16);
58
+ const tables = [];
59
+ for (let index = 0; index < count; index += 1) {
60
+ const record = offset + 12 + index * 16;
61
+ const table = {
62
+ tag: tagAt(bytes, record),
63
+ offset: view.getUint32(record + 8),
64
+ length: view.getUint32(record + 12)
65
+ };
66
+ const end = table.offset + table.length;
67
+ if (!Number.isSafeInteger(end) || end > bytes.length) refuse(`font table ${table.tag} lies outside the font file`, "supply table offsets and lengths within the font data");
68
+ tables.push(table);
69
+ }
70
+ const ordered = tables.filter((table) => table.length > 0).sort((left, right) => left.offset - right.offset);
71
+ for (let index = 1; index < ordered.length; index += 1) {
72
+ const previous = ordered[index - 1];
73
+ const table = ordered[index];
74
+ if (previous && table && table.offset < previous.offset + previous.length) refuse(`font table ${table.tag} overlaps font table ${previous.tag}`, "supply a face whose table byte ranges do not overlap");
75
+ }
76
+ return tables;
77
+ }
78
+ function decodeUtf16Be(bytes) {
79
+ let value = "";
80
+ for (let index = 0; index + 1 < bytes.length; index += 2) value += String.fromCharCode((bytes[index] ?? 0) * 256 + (bytes[index + 1] ?? 0));
81
+ return value;
82
+ }
83
+ function decodeMacRoman(bytes) {
84
+ return new TextDecoder("macintosh").decode(bytes);
85
+ }
86
+ function sfntPostScriptNames(bytes, faceOffset = 0) {
87
+ sfntFormat(bytes, faceOffset);
88
+ const name = faceTables(bytes, faceOffset).find((table) => table.tag === "name");
89
+ if (!name) return [];
90
+ if (name.length < 6) refuse("font table name lies outside the font file", "supply a complete name table");
91
+ requireFaceRange(bytes, name.offset, name.length);
92
+ const view = collectionView(bytes);
93
+ const count = view.getUint16(name.offset + 2);
94
+ if (count > MAX_FACE_NAME_RECORDS) refuse(`font name table declares ${count} records, above the limit ${MAX_FACE_NAME_RECORDS}`, `supply a face with at most ${MAX_FACE_NAME_RECORDS} name records`);
95
+ const strings = view.getUint16(name.offset + 4);
96
+ if (6 + count * 12 > name.length) refuse("font table name lies outside the font file", "supply a complete name table");
97
+ const records = /* @__PURE__ */ new Map();
98
+ let decodedBytes = 0;
99
+ for (let index = 0; index < count; index += 1) {
100
+ const record = name.offset + 6 + index * 12;
101
+ const platform = view.getUint16(record);
102
+ if (view.getUint16(record + 6) !== 6 || platform !== 1 && platform !== 3) continue;
103
+ const length = view.getUint16(record + 8);
104
+ const relative = view.getUint16(record + 10);
105
+ if (strings + relative + length > name.length) refuse("font table name lies outside the font file", "supply a complete name table");
106
+ const start = name.offset + strings + relative;
107
+ const key = `${platform}:${start}:${length}`;
108
+ if (records.has(key)) continue;
109
+ decodedBytes += length;
110
+ if (decodedBytes > MAX_FACE_NAME_BYTES) refuse(`font name table requires ${decodedBytes} decoded bytes, above the limit ${MAX_FACE_NAME_BYTES}`, `supply a face whose PostScript names occupy at most ${MAX_FACE_NAME_BYTES} bytes`);
111
+ records.set(key, {
112
+ platform,
113
+ start,
114
+ length
115
+ });
116
+ }
117
+ const names = [...records.values()].map(({ platform, start, length }) => {
118
+ const encoded = bytes.subarray(start, start + length);
119
+ return {
120
+ platform,
121
+ value: platform === 3 ? decodeUtf16Be(encoded) : decodeMacRoman(encoded)
122
+ };
123
+ });
124
+ const preferred = names.filter((name) => name.platform === 3);
125
+ return [...new Set((preferred.length > 0 ? preferred : names).map((name) => name.value))];
126
+ }
127
+ function collectionFaces(bytes) {
128
+ return collectionOffsets(bytes).flatMap((offset) => sfntPostScriptNames(bytes, offset));
129
+ }
130
+ function checksum(bytes, offset = 0, length = bytes.length) {
131
+ let sum = 0;
132
+ for (let index = 0; index < length; index += 4) {
133
+ const word = ((bytes[offset + index] ?? 0) << 24 | (bytes[offset + index + 1] ?? 0) << 16 | (bytes[offset + index + 2] ?? 0) << 8 | (bytes[offset + index + 3] ?? 0)) >>> 0;
134
+ sum = sum + word >>> 0;
135
+ }
136
+ return sum;
137
+ }
138
+ function directoryFields(count) {
139
+ const power = 2 ** Math.floor(Math.log2(Math.max(1, count)));
140
+ return {
141
+ searchRange: power * 16,
142
+ entrySelector: Math.log2(power),
143
+ rangeShift: count * 16 - power * 16
144
+ };
145
+ }
146
+ function extractAt(bytes, faceOffset) {
147
+ const format = sfntFormat(bytes, faceOffset);
148
+ const tables = faceTables(bytes, faceOffset).filter((table) => table.tag !== "DSIG");
149
+ const directoryLength = 12 + tables.length * 16;
150
+ let total = directoryLength;
151
+ for (const table of tables) total += Math.ceil(table.length / 4) * 4;
152
+ if (!Number.isSafeInteger(total) || total > bytes.length) refuse(`font face extraction requires ${total} bytes, above the font file size ${bytes.length}`, "supply a face whose table data fits within the font file");
153
+ const data = new Uint8Array(total);
154
+ const view = new DataView(data.buffer);
155
+ data.set(bytes.subarray(faceOffset, faceOffset + 4), 0);
156
+ view.setUint16(4, tables.length);
157
+ const fields = directoryFields(tables.length);
158
+ view.setUint16(6, fields.searchRange);
159
+ view.setUint16(8, fields.entrySelector);
160
+ view.setUint16(10, fields.rangeShift);
161
+ let outputOffset = directoryLength;
162
+ let headOffset;
163
+ for (let index = 0; index < tables.length; index += 1) {
164
+ const table = tables[index];
165
+ if (!table) continue;
166
+ const record = 12 + index * 16;
167
+ for (let byte = 0; byte < 4; byte += 1) data[record + byte] = table.tag.charCodeAt(byte);
168
+ data.set(bytes.subarray(table.offset, table.offset + table.length), outputOffset);
169
+ if (table.tag === "head") {
170
+ if (table.length < 12) refuse("font table head lies outside the font file", "supply a complete head table");
171
+ new DataView(data.buffer).setUint32(outputOffset + 8, 0);
172
+ headOffset = outputOffset;
173
+ }
174
+ view.setUint32(record + 4, checksum(data, outputOffset, table.length));
175
+ view.setUint32(record + 8, outputOffset);
176
+ view.setUint32(record + 12, table.length);
177
+ outputOffset += Math.ceil(table.length / 4) * 4;
178
+ }
179
+ if (headOffset !== void 0) view.setUint32(headOffset + 8, CHECKSUM_MAGIC - checksum(data) >>> 0);
180
+ return {
181
+ data,
182
+ format
183
+ };
184
+ }
185
+ function extractFace(bytes, postscriptName) {
186
+ const offsets = collectionOffsets(bytes);
187
+ for (const offset of offsets) if (sfntPostScriptNames(bytes, offset).includes(postscriptName)) return extractAt(bytes, offset);
188
+ refuse(`font collection has no face named ${postscriptName}; it holds ${collectionFaces(bytes).join(", ").slice(0, 1024) || "(none)"}`, "send the PostScript name of a face the collection holds");
189
+ }
190
+ //#endregion
191
+ export { collectionFaces, extractFace, sfntPostScriptNames };
@@ -0,0 +1,8 @@
1
+ import { FontFormat } from "./fonts.js";
2
+ //#region src/font-face.d.ts
3
+ export interface ResolvedFontFace {
4
+ data: Uint8Array;
5
+ format: Exclude<FontFormat, 'collection'>;
6
+ }
7
+ export declare function resolveFontFace(data: Uint8Array, format: FontFormat, postscriptName?: string): ResolvedFontFace;
8
+ //#endregion
@@ -0,0 +1,24 @@
1
+ import { FontFaceRefusedError } from "./font-refusal.js";
2
+ import { classifyFont } from "./font-signature.js";
3
+ import { collectionFaces, extractFace, sfntPostScriptNames } from "./font-collection.js";
4
+ //#region src/font-face.ts
5
+ function resolveFontFace(data, format, postscriptName) {
6
+ const detected = classifyFont(data);
7
+ if (detected === "collection") {
8
+ if (!postscriptName) throw new FontFaceRefusedError(`font collection requires a PostScript name; it holds ${collectionFaces(data).join(", ").slice(0, 1024) || "(none)"}; send one of those PostScript names to clear this refusal.`);
9
+ return extractFace(data, postscriptName);
10
+ }
11
+ if (format === "collection") throw new FontFaceRefusedError("font declared as a collection does not contain a font collection; send bytes with a ttcf signature to clear this refusal.");
12
+ if (!postscriptName || detected === "woff" || detected === "woff2") return {
13
+ data,
14
+ format
15
+ };
16
+ const names = sfntPostScriptNames(data);
17
+ if (names.includes(postscriptName) && (detected === "opentype" || detected === "truetype")) return {
18
+ data,
19
+ format: detected
20
+ };
21
+ throw new FontFaceRefusedError(`font face has PostScript name ${names.join(", ").slice(0, 1024) || "(none)"}, not ${postscriptName}; send its matching PostScript name to clear this refusal.`);
22
+ }
23
+ //#endregion
24
+ export { resolveFontFace };
@@ -0,0 +1,5 @@
1
+ //#region src/font-refusal.d.ts
2
+ export declare class FontFaceRefusedError extends Error {
3
+ constructor(message: string);
4
+ }
5
+ //#endregion
@@ -0,0 +1,9 @@
1
+ //#region src/font-refusal.ts
2
+ var FontFaceRefusedError = class extends Error {
3
+ constructor(message) {
4
+ super(message);
5
+ this.name = "FontFaceRefusedError";
6
+ }
7
+ };
8
+ //#endregion
9
+ export { FontFaceRefusedError };
@@ -0,0 +1,5 @@
1
+ import { FontFormat } from "./fonts.js";
2
+ //#region src/font-signature.d.ts
3
+ export declare const COLLECTION_TAG = "ttcf";
4
+ export declare function classifyFont(bytes: Uint8Array, offset?: number): FontFormat | undefined;
5
+ //#endregion