@pptx-studio/cli 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.
@@ -272,6 +272,7 @@ const RENDER_ERROR_CODES = [
272
272
  "CLI_NO_FACE",
273
273
  "CLI_FONT_SIZE",
274
274
  "CLI_NO_SLIDE",
275
+ "CLI_WIDTH",
275
276
  "CLI_OUTPUT_PATH"
276
277
  ];
277
278
  var RenderError = class extends Error {
@@ -305,6 +306,10 @@ const TRUE_TYPE = 65536;
305
306
  const TTCF = 1953784678;
306
307
  /** `OS/2.fsSelection` bit 7: the typographic metrics are the ones to use. */
307
308
  const USE_TYPO_METRICS = 128;
309
+ /** FreeType on Linux, DirectWrite elsewhere. macOS is unmeasured. ADR 0045. */
310
+ function backendFor(platform) {
311
+ return platform === "linux" ? "freetype" : "directwrite";
312
+ }
308
313
  function readerOf(bytes) {
309
314
  return {
310
315
  bytes,
@@ -323,6 +328,9 @@ function i16(r, at) {
323
328
  function u32(r, at) {
324
329
  return r.view.getUint32(at);
325
330
  }
331
+ function tagAt(r, at) {
332
+ return String.fromCharCode(u8(r, at), u8(r, at + 1), u8(r, at + 2), u8(r, at + 3));
333
+ }
326
334
  function unreadable(what, subject) {
327
335
  throw new RenderError("CLI_FONT_UNREADABLE", what, subject);
328
336
  }
@@ -334,7 +342,7 @@ function directoryAt(bytes, start, subject) {
334
342
  for (let i = 0; i < count; i++) {
335
343
  const at = start + 12 + i * 16;
336
344
  if (at + 16 > bytes.length) unreadable("table directory runs past the file", subject);
337
- const tag = String.fromCharCode(u8(r, at), u8(r, at + 1), u8(r, at + 2), u8(r, at + 3));
345
+ const tag = tagAt(r, at);
338
346
  const offset = u32(r, at + 8);
339
347
  const length = u32(r, at + 12);
340
348
  if (offset < bytes.length) out.set(tag, bytes.subarray(offset, Math.min(offset + length, bytes.length)));
@@ -514,6 +522,10 @@ function valueSize(format) {
514
522
  }
515
523
  /** `XAdvance` is bit 2, and it is the only field a horizontal advance reads. */
516
524
  const X_ADVANCE = 4;
525
+ /** `LookupType` 2, pair adjustment: the only positioning that moves an advance. */
526
+ const PAIR_POS = 2;
527
+ /** `LookupType` 9, Extension Positioning, whose subtable names the real type. */
528
+ const EXTENSION_POS = 9;
517
529
  function pairPosLookup(r, at) {
518
530
  const format = u16(r, at);
519
531
  const valueFormat1 = u16(r, at + 4);
@@ -550,10 +562,23 @@ function pairPosLookup(r, at) {
550
562
  };
551
563
  }
552
564
  /**
565
+ * The pair adjustments behind an `ExtensionPosFormat1` subtable.
566
+ *
567
+ * The extension names the real lookup type and a 32-bit offset from its own
568
+ * start, and may not itself target another, so exactly one level is followed.
569
+ */
570
+ function extensionPairPos(r, at) {
571
+ const end = r.bytes.length;
572
+ if (at + 8 > end || u16(r, at) !== 1 || u16(r, at + 2) !== PAIR_POS) return void 0;
573
+ const target = at + u32(r, at + 4);
574
+ return target + 16 > end ? void 0 : pairPosLookup(r, target);
575
+ }
576
+ /**
553
577
  * The `kern` feature's pair adjustments, if the font has GPOS.
554
578
  *
555
579
  * Only the `kern` feature: a font's GPOS also carries mark attachment and
556
- * cursive positioning, and neither changes an advance.
580
+ * cursive positioning, and neither changes an advance. Its lookups are type 2,
581
+ * or the type 9 Extension a font compiler wraps them in.
557
582
  */
558
583
  function gposKerning(tables) {
559
584
  const table = tables.get("GPOS");
@@ -566,7 +591,7 @@ function gposKerning(tables) {
566
591
  const featureCount = u16(r, featureList);
567
592
  for (let i = 0; i < featureCount; i++) {
568
593
  const record = featureList + 2 + i * 6;
569
- if (String.fromCharCode(u8(r, record), u8(r, record + 1), u8(r, record + 2), u8(r, record + 3)) !== "kern") continue;
594
+ if (tagAt(r, record) !== "kern") continue;
570
595
  const feature = featureList + u16(r, record + 4);
571
596
  const lookups = u16(r, feature + 2);
572
597
  for (let j = 0; j < lookups; j++) wanted.add(u16(r, feature + 4 + j * 2));
@@ -577,10 +602,12 @@ function gposKerning(tables) {
577
602
  for (const index of wanted) {
578
603
  if (index >= lookupCount) continue;
579
604
  const lookup = lookupList + u16(r, lookupList + 2 + index * 2);
580
- if (u16(r, lookup) !== 2) continue;
605
+ const type = u16(r, lookup);
606
+ if (type !== PAIR_POS && type !== EXTENSION_POS) continue;
581
607
  const subtables = u16(r, lookup + 4);
582
608
  for (let i = 0; i < subtables; i++) {
583
- const pairs = pairPosLookup(r, lookup + u16(r, lookup + 6 + i * 2));
609
+ const at = lookup + u16(r, lookup + 6 + i * 2);
610
+ const pairs = type === PAIR_POS ? pairPosLookup(r, at) : extensionPairPos(r, at);
584
611
  if (pairs !== void 0) found.push(pairs);
585
612
  }
586
613
  }
@@ -620,39 +647,96 @@ function legacyKerning(tables) {
620
647
  return (left, right) => pairs.get(left << 16 | right) ?? 0;
621
648
  }
622
649
  /**
650
+ * The `BASE` horizontal axis' `ideo` coordinate under `DFLT`, in font units.
651
+ *
652
+ * One script and one tag: T13 built a font whose `DFLT`, `latn` and `hani`
653
+ * coordinates all differ and Chromium took `DFLT`, and a second whose `BASE`
654
+ * names every script but `DFLT` and Chromium read none of them. A table this
655
+ * cannot follow is answered with `undefined`, which is what HarfBuzz's
656
+ * sanitiser leaves the browser holding as well.
657
+ */
658
+ function ideographicOf(tables) {
659
+ const table = tables.get("BASE");
660
+ if (table === void 0 || table.length < 8) return void 0;
661
+ const r = readerOf(table);
662
+ /** An offset that is neither NULL nor short of the bytes it points at. */
663
+ const within = (offset, need) => offset > 0 && offset + need <= table.length ? offset : void 0;
664
+ const axis = within(u16(r, 4), 4);
665
+ if (axis === void 0) return void 0;
666
+ const tagList = within(axis + u16(r, axis), 2);
667
+ const scriptList = within(axis + u16(r, axis + 2), 2);
668
+ if (tagList === void 0 || scriptList === void 0) return void 0;
669
+ const tagCount = u16(r, tagList);
670
+ if (tagList + 2 + tagCount * 4 > table.length) return void 0;
671
+ let tag = -1;
672
+ for (let i = 0; i < tagCount && tag < 0; i++) if (tagAt(r, tagList + 2 + i * 4) === "ideo") tag = i;
673
+ if (tag < 0) return void 0;
674
+ const scriptCount = u16(r, scriptList);
675
+ if (scriptList + 2 + scriptCount * 6 > table.length) return void 0;
676
+ let script;
677
+ for (let i = 0; i < scriptCount && script === void 0; i++) {
678
+ const record = scriptList + 2 + i * 6;
679
+ if (tagAt(r, record) === "DFLT") script = within(scriptList + u16(r, record + 4), 6);
680
+ }
681
+ if (script === void 0) return void 0;
682
+ const values = within(script + u16(r, script), 4);
683
+ if (values === void 0) return void 0;
684
+ if (tag >= u16(r, values + 2) || values + 4 + tag * 2 + 2 > table.length) return void 0;
685
+ const coord = within(values + u16(r, values + 4 + tag * 2), 4);
686
+ if (coord === void 0) return void 0;
687
+ const format = u16(r, coord);
688
+ return format >= 1 && format <= 3 ? -i16(r, coord + 2) : void 0;
689
+ }
690
+ /**
623
691
  * The vertical metrics a browser reports for the face.
624
692
  *
625
- * T13 built one font whose `hhea`, `usWin` and `sTypo` pairs are all different
626
- * and asked Chromium for `fontBoundingBoxAscent`: it answered `usWin`, and
627
- * answered `sTypo` from the same font with `fsSelection` bit 7 set. So `hhea`
628
- * is never the answer, and a reader that ignores bit 7 is wrong on every font
629
- * that sets it, which is most of the ones shipped since 2015.
693
+ * Bit 7 wins on both backends; without it DirectWrite answers `usWin` and
694
+ * FreeType `hhea`, measured 24/24 and 79/79. ADR 0045.
630
695
  */
631
- function metricsOf(tables, subject) {
696
+ function metricsOf(tables, subject, backend) {
632
697
  const unitsPerEm = u16(readerOf(required(tables, "head", subject)), 18);
633
698
  if (unitsPerEm <= 0) unreadable("head.unitsPerEm is zero", subject);
699
+ const ideographic = ideographicOf(tables);
700
+ const hhea = readerOf(required(tables, "hhea", subject));
701
+ const hheaPair = {
702
+ ascent: i16(hhea, 4),
703
+ descent: -i16(hhea, 6)
704
+ };
634
705
  const os2 = tables.get("OS/2");
635
- if (os2 === void 0 || os2.length < 78) {
636
- const hhea = readerOf(required(tables, "hhea", subject));
637
- return {
638
- unitsPerEm,
639
- ascent: i16(hhea, 4),
640
- descent: -i16(hhea, 6),
641
- source: "usWin"
642
- };
643
- }
644
- const r = readerOf(os2);
645
- if ((u16(r, 62) & USE_TYPO_METRICS) !== 0) return {
706
+ if (os2 === void 0 || os2.length < 78) return {
646
707
  unitsPerEm,
708
+ ...hheaPair,
709
+ source: "hhea",
710
+ ideographic,
711
+ candidates: {
712
+ hhea: hheaPair,
713
+ usWin: void 0,
714
+ sTypo: void 0,
715
+ useTypoMetrics: false
716
+ }
717
+ };
718
+ const r = readerOf(os2);
719
+ const usWin = {
720
+ ascent: u16(r, 74),
721
+ descent: u16(r, 76)
722
+ };
723
+ const sTypo = {
647
724
  ascent: i16(r, 68),
648
- descent: -i16(r, 70),
649
- source: "sTypo"
725
+ descent: -i16(r, 70)
650
726
  };
727
+ const useTypoMetrics = (u16(r, 62) & USE_TYPO_METRICS) !== 0;
728
+ const source = useTypoMetrics ? "sTypo" : backend === "freetype" ? "hhea" : "usWin";
651
729
  return {
652
730
  unitsPerEm,
653
- ascent: u16(r, 74),
654
- descent: u16(r, 76),
655
- source: "usWin"
731
+ ...source === "sTypo" ? sTypo : source === "hhea" ? hheaPair : usWin,
732
+ source,
733
+ ideographic,
734
+ candidates: {
735
+ hhea: hheaPair,
736
+ usWin,
737
+ sTypo,
738
+ useTypoMetrics
739
+ }
656
740
  };
657
741
  }
658
742
  function advancesOf(tables, subject) {
@@ -668,7 +752,7 @@ function advancesOf(tables, subject) {
668
752
  const BOLD_STYLE = /bold|black|heavy|semibold|extrabold|demibold/i;
669
753
  const ITALIC_STYLE = /italic|oblique/i;
670
754
  /** Read one font out of an already-located table directory. */
671
- function faceOf(tables, subject) {
755
+ function faceOf(tables, subject, backend) {
672
756
  const names = namesOf(tables, subject);
673
757
  const family = names.get(1);
674
758
  if (family === void 0 || family === "") unreadable("no family name", subject);
@@ -681,7 +765,7 @@ function faceOf(tables, subject) {
681
765
  family,
682
766
  subfamily,
683
767
  typographicFamily: names.get(16),
684
- metrics: metricsOf(tables, subject),
768
+ metrics: metricsOf(tables, subject, backend),
685
769
  bold: BOLD_STYLE.test(subfamily) || (macStyle & 1) !== 0,
686
770
  italic: ITALIC_STYLE.test(subfamily) || (macStyle & 2) !== 0,
687
771
  advanceOf(codePoint) {
@@ -696,8 +780,8 @@ function faceOf(tables, subject) {
696
780
  };
697
781
  }
698
782
  /** Every face in a file, which is one unless the file is a collection. */
699
- function facesIn(bytes, subject) {
700
- return fontsIn(bytes, subject).map((tables) => faceOf(tables, subject));
783
+ function facesIn(bytes, subject, backend) {
784
+ return fontsIn(bytes, subject).map((tables) => faceOf(tables, subject, backend));
701
785
  }
702
786
  //#endregion
703
787
  //#region src/render/faces.ts
@@ -717,6 +801,11 @@ const MAX_DEPTH = 4;
717
801
  function key(family) {
718
802
  return family.trim().replace(/\s+/g, " ").toLowerCase();
719
803
  }
804
+ /** Code unit order, which is the same on every machine and in every locale. */
805
+ function compare(a, b) {
806
+ if (a === b) return 0;
807
+ return a < b ? -1 : 1;
808
+ }
720
809
  /** Where fonts live on this platform, whether or not the directories exist. */
721
810
  function systemFontDirectories(platform = process.platform) {
722
811
  const home = homedir();
@@ -745,7 +834,7 @@ function filesUnder(directory, depth, out) {
745
834
  } catch {
746
835
  return;
747
836
  }
748
- for (const entry of entries) {
837
+ for (const entry of [...entries].sort((a, b) => compare(a.name, b.name))) {
749
838
  const path = join(directory, entry.name);
750
839
  if (entry.isDirectory()) filesUnder(path, depth + 1, out);
751
840
  else if (FONT_FILE.test(entry.name)) out.push(path);
@@ -761,6 +850,32 @@ function slot(bold, italic) {
761
850
  return (bold ? 1 : 0) | (italic ? 2 : 0);
762
851
  }
763
852
  /**
853
+ * The names a typeface is a variation of, longest first.
854
+ *
855
+ * DrawingML has no weight axis, so `Calibri Light` is a typeface name rather
856
+ * than Calibri at a weight, and Calibri is the nearer face to draw it in.
857
+ */
858
+ function shorterNames(family) {
859
+ const words = family.trim().split(/\s+/);
860
+ const names = [];
861
+ for (let at = words.length - 1; at > 0; at--) names.push(words.slice(0, at).join(" "));
862
+ return names;
863
+ }
864
+ /**
865
+ * The families to try for one this machine lacks, in order.
866
+ *
867
+ * The measured chain first - `@pptx-studio/text`'s table and then what
868
+ * PowerPoint itself falls back to, ADR 0033 - and the shorter forms of the
869
+ * asked-for name only after it, where no measurement reaches.
870
+ */
871
+ function standInsFor(family) {
872
+ return [
873
+ substituteFor(family)?.use,
874
+ ...LAST_RESORT_FAMILIES,
875
+ ...shorterNames(family).flatMap((name) => [name, substituteFor(name)?.use])
876
+ ].filter((name) => name !== void 0);
877
+ }
878
+ /**
764
879
  * Read every font file under the given directories, once.
765
880
  *
766
881
  * A file that will not parse is skipped rather than fatal: a font directory on
@@ -777,23 +892,28 @@ function indexFonts(options = {}) {
777
892
  const directories = [...options.extra ?? [], ...options.system === false ? [] : systemFontDirectories(options.platform)];
778
893
  const files = [];
779
894
  for (const directory of directories) filesUnder(directory, 0, files);
895
+ const backend = backendFor(options.platform ?? process.platform);
780
896
  const indexed = [];
781
897
  const byFamily = /* @__PURE__ */ new Map();
782
898
  const claim = (family, entry) => {
783
- const slots = byFamily.get(key(family)) ?? [
784
- void 0,
785
- void 0,
786
- void 0,
787
- void 0
788
- ];
789
- const at = slot(entry.face.bold, entry.face.italic);
790
- slots[at] ??= entry;
791
- byFamily.set(key(family), slots);
899
+ const at = key(family);
900
+ if (at === "") return;
901
+ const found = byFamily.get(at) ?? {
902
+ name: family,
903
+ slots: [
904
+ void 0,
905
+ void 0,
906
+ void 0,
907
+ void 0
908
+ ]
909
+ };
910
+ found.slots[slot(entry.face.bold, entry.face.italic)] ??= entry;
911
+ byFamily.set(at, found);
792
912
  };
793
913
  for (const file of files) {
794
914
  let faces;
795
915
  try {
796
- faces = facesIn(new Uint8Array(readFileSync(file)), file);
916
+ faces = facesIn(new Uint8Array(readFileSync(file)), file, backend);
797
917
  } catch {
798
918
  continue;
799
919
  }
@@ -808,9 +928,7 @@ function indexFonts(options = {}) {
808
928
  }
809
929
  }
810
930
  /** The nearest slot to the one asked for: exact, then drop italic, then bold. */
811
- const pick = (family, bold, italic) => {
812
- const slots = byFamily.get(key(family));
813
- if (slots === void 0) return void 0;
931
+ const pickIn = (found, bold, italic) => {
814
932
  const wanted = [
815
933
  slot(bold, italic),
816
934
  slot(bold, false),
@@ -822,29 +940,37 @@ function indexFonts(options = {}) {
822
940
  3
823
941
  ];
824
942
  for (const at of wanted) {
825
- const found = slots[at];
826
- if (found !== void 0) return found;
943
+ const entry = found.slots[at];
944
+ if (entry !== void 0) return entry;
827
945
  }
828
946
  };
947
+ const drawnIn = (name, bold, italic, asked) => {
948
+ const family = byFamily.get(key(name));
949
+ if (family === void 0) return void 0;
950
+ const found = pickIn(family, bold, italic);
951
+ if (found === void 0) return void 0;
952
+ return {
953
+ face: found.face,
954
+ asked,
955
+ drawn: name,
956
+ file: found.file,
957
+ substituted: key(name) !== key(asked)
958
+ };
959
+ };
829
960
  return {
830
961
  indexed,
831
962
  directories,
832
963
  resolve(family, bold, italic) {
833
- const direct = pick(family, bold, italic);
834
- if (direct !== void 0) return {
835
- face: direct.face,
836
- asked: family,
837
- drawn: family,
838
- file: direct.file,
839
- substituted: false
840
- };
841
- const chain = [substituteFor(family)?.use, ...LAST_RESORT_FAMILIES].filter((name) => name !== void 0);
842
- for (const name of chain) {
843
- const found = pick(name, bold, italic);
964
+ for (const name of [family, ...standInsFor(family)]) {
965
+ const found = drawnIn(name, bold, italic, family);
966
+ if (found !== void 0) return found;
967
+ }
968
+ for (const [, entry] of [...byFamily].sort((a, b) => compare(a[0], b[0]))) {
969
+ const found = pickIn(entry, bold, italic);
844
970
  if (found !== void 0) return {
845
971
  face: found.face,
846
972
  asked: family,
847
- drawn: name,
973
+ drawn: entry.name,
848
974
  file: found.file,
849
975
  substituted: true
850
976
  };
@@ -867,17 +993,14 @@ function indexFonts(options = {}) {
867
993
  /**
868
994
  * The fixed-point step Chromium reports an advance in.
869
995
  *
870
- * T13 measured 252 widths across seven fonts, six strings and six sizes. Summing
871
- * exact floats fits 88 of them; quantising each glyph advance toward zero and
872
- * each kern adjustment to nearest, both at 1/65536 px, fits all 216. The
873
- * difference is never more than 1.6e-5 px and matters to nothing on a slide -
874
- * it is here because a rule that reproduces the browser exactly can be tested
875
- * exactly, and one that is merely close cannot.
996
+ * Quantising each glyph advance toward zero and each kern adjustment to nearest
997
+ * fits all 432 of T13's widths, where summing exact floats fits 150 and is out
998
+ * by up to 1.04e-4 px. ADR 0042.
876
999
  */
877
1000
  const FIXED = 65536;
878
1001
  /**
879
- * Every advance in this font is positive, so truncation and flooring are the
880
- * same rule; T13 had no probe that could separate them and does not claim one.
1002
+ * `hmtx.advanceWidth` is a `UFWORD`, so this is never negative and truncation
1003
+ * and flooring are one rule. ADR 0042.
881
1004
  */
882
1005
  function quantiseAdvance(px) {
883
1006
  return Math.trunc(px * FIXED) / FIXED;
@@ -897,11 +1020,14 @@ function createFontMeasurer(library) {
897
1020
  /** Which face draws a code point the asked-for face has no glyph for. */
898
1021
  const fallbacks = /* @__PURE__ */ new Map();
899
1022
  const faceFor = (family, bold, italic) => {
900
- const cacheKey = `${family}${bold ? "b" : ""}${italic ? "i" : ""}`;
1023
+ const cacheKey = `${family}\u0000${bold ? "b" : ""}${italic ? "i" : ""}`;
901
1024
  const cached = resolved.get(cacheKey);
902
1025
  if (cached !== void 0) return cached;
903
1026
  const found = library.resolve(family, bold, italic);
904
- if (found === void 0) throw new RenderError("CLI_NO_FACE", `no face on this machine can stand in for ${JSON.stringify(family)}; ${String(library.indexed.length)} face(s) were indexed from ${library.directories.join(", ")}`, family);
1027
+ if (found === void 0) {
1028
+ const where = library.directories.length === 0 ? "no directory was searched" : `nothing was found in ${library.directories.join(", ")}`;
1029
+ throw new RenderError("CLI_NO_FACE", `no face on this machine can stand in for ${JSON.stringify(family)}: the font library is empty, ${where}`, family);
1030
+ }
905
1031
  resolved.set(cacheKey, found);
906
1032
  return found;
907
1033
  };
@@ -974,10 +1100,11 @@ function createFontMeasurer(library) {
974
1100
  const cached = boxes.get(family);
975
1101
  if (cached !== void 0) return cached;
976
1102
  const { metrics } = faceFor(family, false, false).face;
1103
+ const ideographic = (metrics.ideographic ?? metrics.descent) / metrics.unitsPerEm;
977
1104
  const box = {
978
1105
  ascent: metrics.ascent / metrics.unitsPerEm,
979
1106
  descent: metrics.descent / metrics.unitsPerEm,
980
- ideographic: metrics.descent / metrics.unitsPerEm
1107
+ ideographic: ideographic > 0 && ideographic < 1 ? ideographic : 0
981
1108
  };
982
1109
  boxes.set(family, box);
983
1110
  return box;
@@ -1010,11 +1137,10 @@ function createFontMeasurer(library) {
1010
1137
  */
1011
1138
  /** What a slide is drawn at when the caller says nothing. PowerPoint's own. */
1012
1139
  const DEFAULT_WIDTH = 1920;
1013
- const RENDER_DEFAULTS = {
1014
- width: DEFAULT_WIDTH,
1015
- systemFonts: true,
1016
- text: true
1017
- };
1140
+ /** Text is drawn unless the caller says otherwise. */
1141
+ function drawsText(options) {
1142
+ return options.text ?? true;
1143
+ }
1018
1144
  /** The media resolver: an rId means the rels of the part the fill was written in. */
1019
1145
  function mediaFrom(store) {
1020
1146
  return (embed, part) => {
@@ -1031,35 +1157,38 @@ function mediaFrom(store) {
1031
1157
  /**
1032
1158
  * Render a deck that is already in memory.
1033
1159
  *
1034
- * Separate from `runRender` so that the whole pipeline is exercised by the test
1035
- * suite without a file system, a process or a captured stdout.
1160
+ * The library entry point: no file system, no process, no stdout. `runRender` is
1161
+ * the verb wrapped around it, and the three fields it adds are its own.
1036
1162
  */
1037
- function renderDeck(bytes, options) {
1163
+ function renderDeck(bytes, options = {}) {
1164
+ const width = options.width ?? 1920;
1165
+ if (!Number.isInteger(width) || width < 1) throw new RenderError("CLI_WIDTH", `a width of ${String(width)} is not a positive whole number of pixels`, String(width));
1166
+ const slide = options.slide ?? null;
1038
1167
  const store = PartStore.open(bytes);
1039
1168
  const document = loadDocument(store);
1040
1169
  const size = document.slideSize;
1041
- const height = Math.round(options.width * size.cy / size.cx);
1042
- if (options.slide !== null) {
1170
+ const height = Math.round(width * size.cy / size.cx);
1171
+ if (slide !== null) {
1043
1172
  const count = document.slides.length;
1044
- if (!Number.isInteger(options.slide) || options.slide < 1 || options.slide > count) throw new RenderError("CLI_NO_SLIDE", `--slide ${String(options.slide)}: the deck has ${String(count)} slide(s)`, String(options.slide));
1173
+ if (!Number.isInteger(slide) || slide < 1 || slide > count) throw new RenderError("CLI_NO_SLIDE", `slide ${String(slide)}: the deck has ${String(count)} slide(s)`, String(slide));
1045
1174
  }
1046
1175
  let library = null;
1047
- const fonts = options.text ? createFontMeasurer(library = indexFonts({
1048
- extra: options.fontDirs,
1049
- system: options.systemFonts
1176
+ const fonts = drawsText(options) ? createFontMeasurer(library = indexFonts({
1177
+ extra: options.fontDirs ?? [],
1178
+ system: options.systemFonts !== false
1050
1179
  })) : null;
1051
1180
  const media = mediaFrom(store);
1052
1181
  return {
1053
- slides: (options.slide === null ? document.slides.map((sheet, at) => ({
1182
+ slides: (slide === null ? document.slides.map((sheet, at) => ({
1054
1183
  sheet,
1055
1184
  number: at + 1
1056
1185
  })) : [{
1057
- sheet: document.slides[options.slide - 1],
1058
- number: options.slide
1186
+ sheet: document.slides[slide - 1],
1187
+ number: slide
1059
1188
  }]).map(({ sheet, number }) => ({
1060
1189
  number,
1061
1190
  svg: renderSlide(sheet, size, {
1062
- width: options.width,
1191
+ width,
1063
1192
  height,
1064
1193
  idPrefix: `s${String(number)}`,
1065
1194
  media,
@@ -1070,7 +1199,7 @@ function renderDeck(bytes, options) {
1070
1199
  }
1071
1200
  })
1072
1201
  })),
1073
- width: options.width,
1202
+ width,
1074
1203
  height,
1075
1204
  fonts: fonts?.used() ?? [],
1076
1205
  missing: fonts?.missing() ?? [],
@@ -1117,7 +1246,7 @@ function report(result, written, options) {
1117
1246
  const lines = [];
1118
1247
  const slides = `${String(result.slides.length)} slide(s) at ${String(result.width)}x${String(result.height)}`;
1119
1248
  lines.push(written.length === 0 ? slides : `${slides} -> ${written.length === 1 ? written[0] : `${String(written.length)} files`}`);
1120
- if (options.text) {
1249
+ if (drawsText(options)) {
1121
1250
  const substituted = result.fonts.filter((font) => font.substituted);
1122
1251
  lines.push(`${String(result.fonts.length)} typeface(s) from ${String(result.facesIndexed)} indexed face(s)` + (substituted.length === 0 ? "" : `, ${String(substituted.length)} substituted`));
1123
1252
  for (const font of substituted) lines.push(` ${font.asked} -> ${font.drawn}`);
@@ -1278,7 +1407,7 @@ function usage() {
1278
1407
  "",
1279
1408
  "render options:",
1280
1409
  " --slide <n> one slide, 1-based; every slide by default",
1281
- " --width <px> the SVG width attribute; the height follows the aspect",
1410
+ " --width <px> the SVG width attribute; the height follows the aspect (default " + String(DEFAULT_WIDTH) + ")",
1282
1411
  " --out <path> a directory, or a file when rendering one slide",
1283
1412
  " --font-dir <d> look for fonts here first; repeatable",
1284
1413
  " --no-system-fonts do not look in this platform own font directories",
@@ -1287,6 +1416,9 @@ function usage() {
1287
1416
  " --quiet no summary after writing",
1288
1417
  "",
1289
1418
  " With no --out the markup goes to stdout, which is one slide worth doing.",
1419
+ " In Node, `import { renderDeck } from \"@pptx-studio/cli\"` takes the five",
1420
+ " drawing options above and defaults every one; --out, --json and --quiet are",
1421
+ " this command's own.",
1290
1422
  "",
1291
1423
  "inspect options:",
1292
1424
  " --json the census as JSON, for a script or for committing as a fixture",
@@ -1474,7 +1606,7 @@ function main(argv, streams = CONSOLE_STREAMS) {
1474
1606
  streams.err("--slide wants a positive integer, got " + String(values.slide) + "\n");
1475
1607
  return 2;
1476
1608
  }
1477
- const width = positiveInteger(values.width, RENDER_DEFAULTS.width);
1609
+ const width = positiveInteger(values.width, DEFAULT_WIDTH);
1478
1610
  if (width === null) {
1479
1611
  streams.err("--width wants a positive integer, got " + String(values.width) + "\n");
1480
1612
  return 2;
@@ -1565,6 +1697,6 @@ function positiveInteger(value, fallback) {
1565
1697
  return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
1566
1698
  }
1567
1699
  //#endregion
1568
- export { runRender as a, systemFontDirectories as c, RenderError as d, isRenderError as f, runInspect as h, renderDeck as i, facesIn as l, inspectFile as m, DEFAULT_WIDTH as n, createFontMeasurer as o, INSPECT_DEFAULTS as p, RENDER_DEFAULTS as r, indexFonts as s, main as t, RENDER_ERROR_CODES as u };
1700
+ export { createFontMeasurer as a, backendFor as c, RenderError as d, isRenderError as f, runInspect as h, runRender as i, facesIn as l, inspectFile as m, DEFAULT_WIDTH as n, indexFonts as o, INSPECT_DEFAULTS as p, renderDeck as r, systemFontDirectories as s, main as t, RENDER_ERROR_CODES as u };
1569
1701
 
1570
- //# sourceMappingURL=main-DLx2onii.js.map
1702
+ //# sourceMappingURL=main-CQsGTsfq.js.map