@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.
- package/CHANGELOG.md +118 -0
- package/README.md +65 -11
- package/dist/cli.js +1 -1
- package/dist/index.d.ts +51 -18
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/{main-DLx2onii.js → main-CQsGTsfq.js} +226 -94
- package/dist/main-CQsGTsfq.js.map +1 -0
- package/package.json +4 -4
- package/dist/main-DLx2onii.js.map +0 -1
|
@@ -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 =
|
|
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 (
|
|
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
|
-
|
|
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
|
|
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
|
-
*
|
|
626
|
-
*
|
|
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
|
-
|
|
654
|
-
|
|
655
|
-
|
|
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
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
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
|
|
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
|
|
826
|
-
if (
|
|
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
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
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
|
-
*
|
|
871
|
-
*
|
|
872
|
-
*
|
|
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
|
-
*
|
|
880
|
-
*
|
|
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}
|
|
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)
|
|
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:
|
|
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
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
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
|
-
*
|
|
1035
|
-
*
|
|
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(
|
|
1042
|
-
if (
|
|
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(
|
|
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
|
|
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: (
|
|
1182
|
+
slides: (slide === null ? document.slides.map((sheet, at) => ({
|
|
1054
1183
|
sheet,
|
|
1055
1184
|
number: at + 1
|
|
1056
1185
|
})) : [{
|
|
1057
|
-
sheet: document.slides[
|
|
1058
|
-
number:
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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 {
|
|
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-
|
|
1702
|
+
//# sourceMappingURL=main-CQsGTsfq.js.map
|