@pptx-studio/cli 0.1.0 → 0.3.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 +149 -0
- package/README.md +95 -11
- package/dist/cli.js +1 -1
- package/dist/index.d.ts +56 -19
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/{main-DLx2onii.js → main-3sNR_Xlp.js} +262 -99
- package/dist/main-3sNR_Xlp.js.map +1 -0
- package/package.json +9 -9
- package/dist/main-DLx2onii.js.map +0 -1
|
@@ -11,7 +11,7 @@ import { bisectPackages, describeChange, flattenChanges, roundTripPackage, summa
|
|
|
11
11
|
import { censusPackage, formatCensus } from "@pptx-studio/census";
|
|
12
12
|
import { loadDocument } from "@pptx-studio/model";
|
|
13
13
|
import { renderSlide } from "@pptx-studio/render-svg";
|
|
14
|
-
import { LAST_RESORT_FAMILIES, kerningEnabled, substituteFor } from "@pptx-studio/text";
|
|
14
|
+
import { FACE_BOX_PX, LAST_RESORT_FAMILIES, TextError, cssFamily, fontStack, kerningEnabled, substituteFor } from "@pptx-studio/text";
|
|
15
15
|
//#region src/bisect.ts
|
|
16
16
|
const BISECT_DEFAULTS = {
|
|
17
17
|
oracle: "validate",
|
|
@@ -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,12 @@ 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, CoreText on macOS, DirectWrite elsewhere. ADR 0045, ADR 0053. */
|
|
310
|
+
function backendFor(platform) {
|
|
311
|
+
if (platform === "linux") return "freetype";
|
|
312
|
+
if (platform === "darwin") return "coretext";
|
|
313
|
+
return "directwrite";
|
|
314
|
+
}
|
|
308
315
|
function readerOf(bytes) {
|
|
309
316
|
return {
|
|
310
317
|
bytes,
|
|
@@ -323,6 +330,9 @@ function i16(r, at) {
|
|
|
323
330
|
function u32(r, at) {
|
|
324
331
|
return r.view.getUint32(at);
|
|
325
332
|
}
|
|
333
|
+
function tagAt(r, at) {
|
|
334
|
+
return String.fromCharCode(u8(r, at), u8(r, at + 1), u8(r, at + 2), u8(r, at + 3));
|
|
335
|
+
}
|
|
326
336
|
function unreadable(what, subject) {
|
|
327
337
|
throw new RenderError("CLI_FONT_UNREADABLE", what, subject);
|
|
328
338
|
}
|
|
@@ -334,7 +344,7 @@ function directoryAt(bytes, start, subject) {
|
|
|
334
344
|
for (let i = 0; i < count; i++) {
|
|
335
345
|
const at = start + 12 + i * 16;
|
|
336
346
|
if (at + 16 > bytes.length) unreadable("table directory runs past the file", subject);
|
|
337
|
-
const tag =
|
|
347
|
+
const tag = tagAt(r, at);
|
|
338
348
|
const offset = u32(r, at + 8);
|
|
339
349
|
const length = u32(r, at + 12);
|
|
340
350
|
if (offset < bytes.length) out.set(tag, bytes.subarray(offset, Math.min(offset + length, bytes.length)));
|
|
@@ -514,6 +524,10 @@ function valueSize(format) {
|
|
|
514
524
|
}
|
|
515
525
|
/** `XAdvance` is bit 2, and it is the only field a horizontal advance reads. */
|
|
516
526
|
const X_ADVANCE = 4;
|
|
527
|
+
/** `LookupType` 2, pair adjustment: the only positioning that moves an advance. */
|
|
528
|
+
const PAIR_POS = 2;
|
|
529
|
+
/** `LookupType` 9, Extension Positioning, whose subtable names the real type. */
|
|
530
|
+
const EXTENSION_POS = 9;
|
|
517
531
|
function pairPosLookup(r, at) {
|
|
518
532
|
const format = u16(r, at);
|
|
519
533
|
const valueFormat1 = u16(r, at + 4);
|
|
@@ -550,10 +564,23 @@ function pairPosLookup(r, at) {
|
|
|
550
564
|
};
|
|
551
565
|
}
|
|
552
566
|
/**
|
|
567
|
+
* The pair adjustments behind an `ExtensionPosFormat1` subtable.
|
|
568
|
+
*
|
|
569
|
+
* The extension names the real lookup type and a 32-bit offset from its own
|
|
570
|
+
* start, and may not itself target another, so exactly one level is followed.
|
|
571
|
+
*/
|
|
572
|
+
function extensionPairPos(r, at) {
|
|
573
|
+
const end = r.bytes.length;
|
|
574
|
+
if (at + 8 > end || u16(r, at) !== 1 || u16(r, at + 2) !== PAIR_POS) return void 0;
|
|
575
|
+
const target = at + u32(r, at + 4);
|
|
576
|
+
return target + 16 > end ? void 0 : pairPosLookup(r, target);
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
553
579
|
* The `kern` feature's pair adjustments, if the font has GPOS.
|
|
554
580
|
*
|
|
555
581
|
* Only the `kern` feature: a font's GPOS also carries mark attachment and
|
|
556
|
-
* cursive positioning, and neither changes an advance.
|
|
582
|
+
* cursive positioning, and neither changes an advance. Its lookups are type 2,
|
|
583
|
+
* or the type 9 Extension a font compiler wraps them in.
|
|
557
584
|
*/
|
|
558
585
|
function gposKerning(tables) {
|
|
559
586
|
const table = tables.get("GPOS");
|
|
@@ -566,7 +593,7 @@ function gposKerning(tables) {
|
|
|
566
593
|
const featureCount = u16(r, featureList);
|
|
567
594
|
for (let i = 0; i < featureCount; i++) {
|
|
568
595
|
const record = featureList + 2 + i * 6;
|
|
569
|
-
if (
|
|
596
|
+
if (tagAt(r, record) !== "kern") continue;
|
|
570
597
|
const feature = featureList + u16(r, record + 4);
|
|
571
598
|
const lookups = u16(r, feature + 2);
|
|
572
599
|
for (let j = 0; j < lookups; j++) wanted.add(u16(r, feature + 4 + j * 2));
|
|
@@ -577,10 +604,12 @@ function gposKerning(tables) {
|
|
|
577
604
|
for (const index of wanted) {
|
|
578
605
|
if (index >= lookupCount) continue;
|
|
579
606
|
const lookup = lookupList + u16(r, lookupList + 2 + index * 2);
|
|
580
|
-
|
|
607
|
+
const type = u16(r, lookup);
|
|
608
|
+
if (type !== PAIR_POS && type !== EXTENSION_POS) continue;
|
|
581
609
|
const subtables = u16(r, lookup + 4);
|
|
582
610
|
for (let i = 0; i < subtables; i++) {
|
|
583
|
-
const
|
|
611
|
+
const at = lookup + u16(r, lookup + 6 + i * 2);
|
|
612
|
+
const pairs = type === PAIR_POS ? pairPosLookup(r, at) : extensionPairPos(r, at);
|
|
584
613
|
if (pairs !== void 0) found.push(pairs);
|
|
585
614
|
}
|
|
586
615
|
}
|
|
@@ -620,39 +649,98 @@ function legacyKerning(tables) {
|
|
|
620
649
|
return (left, right) => pairs.get(left << 16 | right) ?? 0;
|
|
621
650
|
}
|
|
622
651
|
/**
|
|
652
|
+
* The `BASE` horizontal axis' `ideo` coordinate under `DFLT`, in font units.
|
|
653
|
+
*
|
|
654
|
+
* One script and one tag: T13 built a font whose `DFLT`, `latn` and `hani`
|
|
655
|
+
* coordinates all differ and Chromium took `DFLT`, and a second whose `BASE`
|
|
656
|
+
* names every script but `DFLT` and Chromium read none of them. A table this
|
|
657
|
+
* cannot follow is answered with `undefined`, which is what HarfBuzz's
|
|
658
|
+
* sanitiser leaves the browser holding as well.
|
|
659
|
+
*/
|
|
660
|
+
function ideographicOf(tables) {
|
|
661
|
+
const table = tables.get("BASE");
|
|
662
|
+
if (table === void 0 || table.length < 8) return void 0;
|
|
663
|
+
const r = readerOf(table);
|
|
664
|
+
/** An offset that is neither NULL nor short of the bytes it points at. */
|
|
665
|
+
const within = (offset, need) => offset > 0 && offset + need <= table.length ? offset : void 0;
|
|
666
|
+
const axis = within(u16(r, 4), 4);
|
|
667
|
+
if (axis === void 0) return void 0;
|
|
668
|
+
const tagList = within(axis + u16(r, axis), 2);
|
|
669
|
+
const scriptList = within(axis + u16(r, axis + 2), 2);
|
|
670
|
+
if (tagList === void 0 || scriptList === void 0) return void 0;
|
|
671
|
+
const tagCount = u16(r, tagList);
|
|
672
|
+
if (tagList + 2 + tagCount * 4 > table.length) return void 0;
|
|
673
|
+
let tag = -1;
|
|
674
|
+
for (let i = 0; i < tagCount && tag < 0; i++) if (tagAt(r, tagList + 2 + i * 4) === "ideo") tag = i;
|
|
675
|
+
if (tag < 0) return void 0;
|
|
676
|
+
const scriptCount = u16(r, scriptList);
|
|
677
|
+
if (scriptList + 2 + scriptCount * 6 > table.length) return void 0;
|
|
678
|
+
let script;
|
|
679
|
+
for (let i = 0; i < scriptCount && script === void 0; i++) {
|
|
680
|
+
const record = scriptList + 2 + i * 6;
|
|
681
|
+
if (tagAt(r, record) === "DFLT") script = within(scriptList + u16(r, record + 4), 6);
|
|
682
|
+
}
|
|
683
|
+
if (script === void 0) return void 0;
|
|
684
|
+
const values = within(script + u16(r, script), 4);
|
|
685
|
+
if (values === void 0) return void 0;
|
|
686
|
+
if (tag >= u16(r, values + 2) || values + 4 + tag * 2 + 2 > table.length) return void 0;
|
|
687
|
+
const coord = within(values + u16(r, values + 4 + tag * 2), 4);
|
|
688
|
+
if (coord === void 0) return void 0;
|
|
689
|
+
const format = u16(r, coord);
|
|
690
|
+
return format >= 1 && format <= 3 ? -i16(r, coord + 2) : void 0;
|
|
691
|
+
}
|
|
692
|
+
/**
|
|
623
693
|
* The vertical metrics a browser reports for the face.
|
|
624
694
|
*
|
|
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.
|
|
695
|
+
* Bit 7 wins on DirectWrite and FreeType, which otherwise answer `usWin` and
|
|
696
|
+
* `hhea`; CoreText answers `hhea` either way, 248/248. ADR 0045, ADR 0053.
|
|
630
697
|
*/
|
|
631
|
-
function metricsOf(tables, subject) {
|
|
698
|
+
function metricsOf(tables, subject, backend) {
|
|
632
699
|
const unitsPerEm = u16(readerOf(required(tables, "head", subject)), 18);
|
|
633
700
|
if (unitsPerEm <= 0) unreadable("head.unitsPerEm is zero", subject);
|
|
701
|
+
const ideographic = ideographicOf(tables);
|
|
702
|
+
const hhea = readerOf(required(tables, "hhea", subject));
|
|
703
|
+
const hheaPair = {
|
|
704
|
+
ascent: i16(hhea, 4),
|
|
705
|
+
descent: -i16(hhea, 6)
|
|
706
|
+
};
|
|
634
707
|
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 {
|
|
708
|
+
if (os2 === void 0 || os2.length < 78) return {
|
|
646
709
|
unitsPerEm,
|
|
710
|
+
...hheaPair,
|
|
711
|
+
source: "hhea",
|
|
712
|
+
backend,
|
|
713
|
+
ideographic,
|
|
714
|
+
candidates: {
|
|
715
|
+
hhea: hheaPair,
|
|
716
|
+
usWin: void 0,
|
|
717
|
+
sTypo: void 0,
|
|
718
|
+
useTypoMetrics: false
|
|
719
|
+
}
|
|
720
|
+
};
|
|
721
|
+
const r = readerOf(os2);
|
|
722
|
+
const usWin = {
|
|
723
|
+
ascent: u16(r, 74),
|
|
724
|
+
descent: u16(r, 76)
|
|
725
|
+
};
|
|
726
|
+
const sTypo = {
|
|
647
727
|
ascent: i16(r, 68),
|
|
648
|
-
descent: -i16(r, 70)
|
|
649
|
-
source: "sTypo"
|
|
728
|
+
descent: -i16(r, 70)
|
|
650
729
|
};
|
|
730
|
+
const useTypoMetrics = (u16(r, 62) & USE_TYPO_METRICS) !== 0;
|
|
731
|
+
const source = backend === "coretext" ? "hhea" : useTypoMetrics ? "sTypo" : backend === "freetype" ? "hhea" : "usWin";
|
|
651
732
|
return {
|
|
652
733
|
unitsPerEm,
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
734
|
+
...source === "sTypo" ? sTypo : source === "hhea" ? hheaPair : usWin,
|
|
735
|
+
source,
|
|
736
|
+
backend,
|
|
737
|
+
ideographic,
|
|
738
|
+
candidates: {
|
|
739
|
+
hhea: hheaPair,
|
|
740
|
+
usWin,
|
|
741
|
+
sTypo,
|
|
742
|
+
useTypoMetrics
|
|
743
|
+
}
|
|
656
744
|
};
|
|
657
745
|
}
|
|
658
746
|
function advancesOf(tables, subject) {
|
|
@@ -668,7 +756,7 @@ function advancesOf(tables, subject) {
|
|
|
668
756
|
const BOLD_STYLE = /bold|black|heavy|semibold|extrabold|demibold/i;
|
|
669
757
|
const ITALIC_STYLE = /italic|oblique/i;
|
|
670
758
|
/** Read one font out of an already-located table directory. */
|
|
671
|
-
function faceOf(tables, subject) {
|
|
759
|
+
function faceOf(tables, subject, backend) {
|
|
672
760
|
const names = namesOf(tables, subject);
|
|
673
761
|
const family = names.get(1);
|
|
674
762
|
if (family === void 0 || family === "") unreadable("no family name", subject);
|
|
@@ -681,7 +769,7 @@ function faceOf(tables, subject) {
|
|
|
681
769
|
family,
|
|
682
770
|
subfamily,
|
|
683
771
|
typographicFamily: names.get(16),
|
|
684
|
-
metrics: metricsOf(tables, subject),
|
|
772
|
+
metrics: metricsOf(tables, subject, backend),
|
|
685
773
|
bold: BOLD_STYLE.test(subfamily) || (macStyle & 1) !== 0,
|
|
686
774
|
italic: ITALIC_STYLE.test(subfamily) || (macStyle & 2) !== 0,
|
|
687
775
|
advanceOf(codePoint) {
|
|
@@ -696,8 +784,8 @@ function faceOf(tables, subject) {
|
|
|
696
784
|
};
|
|
697
785
|
}
|
|
698
786
|
/** 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));
|
|
787
|
+
function facesIn(bytes, subject, backend) {
|
|
788
|
+
return fontsIn(bytes, subject).map((tables) => faceOf(tables, subject, backend));
|
|
701
789
|
}
|
|
702
790
|
//#endregion
|
|
703
791
|
//#region src/render/faces.ts
|
|
@@ -717,6 +805,21 @@ const MAX_DEPTH = 4;
|
|
|
717
805
|
function key(family) {
|
|
718
806
|
return family.trim().replace(/\s+/g, " ").toLowerCase();
|
|
719
807
|
}
|
|
808
|
+
/** Whether a name can be written into the markup that would draw it. */
|
|
809
|
+
function emittable(family) {
|
|
810
|
+
try {
|
|
811
|
+
cssFamily(family);
|
|
812
|
+
return true;
|
|
813
|
+
} catch (error) {
|
|
814
|
+
if (error instanceof TextError && error.code === "TEXT_FONT_FAMILY") return false;
|
|
815
|
+
throw error;
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
/** Code unit order, which is the same on every machine and in every locale. */
|
|
819
|
+
function compare(a, b) {
|
|
820
|
+
if (a === b) return 0;
|
|
821
|
+
return a < b ? -1 : 1;
|
|
822
|
+
}
|
|
720
823
|
/** Where fonts live on this platform, whether or not the directories exist. */
|
|
721
824
|
function systemFontDirectories(platform = process.platform) {
|
|
722
825
|
const home = homedir();
|
|
@@ -745,7 +848,7 @@ function filesUnder(directory, depth, out) {
|
|
|
745
848
|
} catch {
|
|
746
849
|
return;
|
|
747
850
|
}
|
|
748
|
-
for (const entry of entries) {
|
|
851
|
+
for (const entry of [...entries].sort((a, b) => compare(a.name, b.name))) {
|
|
749
852
|
const path = join(directory, entry.name);
|
|
750
853
|
if (entry.isDirectory()) filesUnder(path, depth + 1, out);
|
|
751
854
|
else if (FONT_FILE.test(entry.name)) out.push(path);
|
|
@@ -761,6 +864,32 @@ function slot(bold, italic) {
|
|
|
761
864
|
return (bold ? 1 : 0) | (italic ? 2 : 0);
|
|
762
865
|
}
|
|
763
866
|
/**
|
|
867
|
+
* The names a typeface is a variation of, longest first.
|
|
868
|
+
*
|
|
869
|
+
* DrawingML has no weight axis, so `Calibri Light` is a typeface name rather
|
|
870
|
+
* than Calibri at a weight, and Calibri is the nearer face to draw it in.
|
|
871
|
+
*/
|
|
872
|
+
function shorterNames(family) {
|
|
873
|
+
const words = family.trim().split(/\s+/);
|
|
874
|
+
const names = [];
|
|
875
|
+
for (let at = words.length - 1; at > 0; at--) names.push(words.slice(0, at).join(" "));
|
|
876
|
+
return names;
|
|
877
|
+
}
|
|
878
|
+
/**
|
|
879
|
+
* The families to try for one this machine lacks, in order.
|
|
880
|
+
*
|
|
881
|
+
* The measured chain first - `@pptx-studio/text`'s table and then what
|
|
882
|
+
* PowerPoint itself falls back to, ADR 0033 - and the shorter forms of the
|
|
883
|
+
* asked-for name only after it, where no measurement reaches.
|
|
884
|
+
*/
|
|
885
|
+
function standInsFor(family) {
|
|
886
|
+
return [
|
|
887
|
+
substituteFor(family)?.use,
|
|
888
|
+
...LAST_RESORT_FAMILIES,
|
|
889
|
+
...shorterNames(family).flatMap((name) => [name, substituteFor(name)?.use])
|
|
890
|
+
].filter((name) => name !== void 0);
|
|
891
|
+
}
|
|
892
|
+
/**
|
|
764
893
|
* Read every font file under the given directories, once.
|
|
765
894
|
*
|
|
766
895
|
* A file that will not parse is skipped rather than fatal: a font directory on
|
|
@@ -777,23 +906,28 @@ function indexFonts(options = {}) {
|
|
|
777
906
|
const directories = [...options.extra ?? [], ...options.system === false ? [] : systemFontDirectories(options.platform)];
|
|
778
907
|
const files = [];
|
|
779
908
|
for (const directory of directories) filesUnder(directory, 0, files);
|
|
909
|
+
const backend = backendFor(options.platform ?? process.platform);
|
|
780
910
|
const indexed = [];
|
|
781
911
|
const byFamily = /* @__PURE__ */ new Map();
|
|
782
912
|
const claim = (family, entry) => {
|
|
783
|
-
const
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
913
|
+
const at = key(family);
|
|
914
|
+
if (at === "" || !emittable(family)) return;
|
|
915
|
+
const found = byFamily.get(at) ?? {
|
|
916
|
+
name: family,
|
|
917
|
+
slots: [
|
|
918
|
+
void 0,
|
|
919
|
+
void 0,
|
|
920
|
+
void 0,
|
|
921
|
+
void 0
|
|
922
|
+
]
|
|
923
|
+
};
|
|
924
|
+
found.slots[slot(entry.face.bold, entry.face.italic)] ??= entry;
|
|
925
|
+
byFamily.set(at, found);
|
|
792
926
|
};
|
|
793
927
|
for (const file of files) {
|
|
794
928
|
let faces;
|
|
795
929
|
try {
|
|
796
|
-
faces = facesIn(new Uint8Array(readFileSync(file)), file);
|
|
930
|
+
faces = facesIn(new Uint8Array(readFileSync(file)), file, backend);
|
|
797
931
|
} catch {
|
|
798
932
|
continue;
|
|
799
933
|
}
|
|
@@ -808,9 +942,7 @@ function indexFonts(options = {}) {
|
|
|
808
942
|
}
|
|
809
943
|
}
|
|
810
944
|
/** 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;
|
|
945
|
+
const pickIn = (found, bold, italic) => {
|
|
814
946
|
const wanted = [
|
|
815
947
|
slot(bold, italic),
|
|
816
948
|
slot(bold, false),
|
|
@@ -822,29 +954,37 @@ function indexFonts(options = {}) {
|
|
|
822
954
|
3
|
|
823
955
|
];
|
|
824
956
|
for (const at of wanted) {
|
|
825
|
-
const
|
|
826
|
-
if (
|
|
957
|
+
const entry = found.slots[at];
|
|
958
|
+
if (entry !== void 0) return entry;
|
|
827
959
|
}
|
|
828
960
|
};
|
|
961
|
+
const drawnIn = (name, bold, italic, asked) => {
|
|
962
|
+
const family = byFamily.get(key(name));
|
|
963
|
+
if (family === void 0) return void 0;
|
|
964
|
+
const found = pickIn(family, bold, italic);
|
|
965
|
+
if (found === void 0) return void 0;
|
|
966
|
+
return {
|
|
967
|
+
face: found.face,
|
|
968
|
+
asked,
|
|
969
|
+
drawn: name,
|
|
970
|
+
file: found.file,
|
|
971
|
+
substituted: key(name) !== key(asked)
|
|
972
|
+
};
|
|
973
|
+
};
|
|
829
974
|
return {
|
|
830
975
|
indexed,
|
|
831
976
|
directories,
|
|
832
977
|
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);
|
|
978
|
+
for (const name of [family, ...standInsFor(family)]) {
|
|
979
|
+
const found = drawnIn(name, bold, italic, family);
|
|
980
|
+
if (found !== void 0) return found;
|
|
981
|
+
}
|
|
982
|
+
for (const [, entry] of [...byFamily].sort((a, b) => compare(a[0], b[0]))) {
|
|
983
|
+
const found = pickIn(entry, bold, italic);
|
|
844
984
|
if (found !== void 0) return {
|
|
845
985
|
face: found.face,
|
|
846
986
|
asked: family,
|
|
847
|
-
drawn: name,
|
|
987
|
+
drawn: entry.name,
|
|
848
988
|
file: found.file,
|
|
849
989
|
substituted: true
|
|
850
990
|
};
|
|
@@ -867,17 +1007,16 @@ function indexFonts(options = {}) {
|
|
|
867
1007
|
/**
|
|
868
1008
|
* The fixed-point step Chromium reports an advance in.
|
|
869
1009
|
*
|
|
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.
|
|
1010
|
+
* Quantising each glyph advance toward zero and each kern adjustment to nearest
|
|
1011
|
+
* fits all 540 of T13's widths, where summing exact floats fits 234 and is out
|
|
1012
|
+
* by up to 1.04e-4 px. ADR 0042.
|
|
876
1013
|
*/
|
|
877
1014
|
const FIXED = 65536;
|
|
1015
|
+
/** CoreText's `Fixed`: the em ratio is held as 16.16 before the box rounds. ADR 0053. */
|
|
1016
|
+
const EM_FIXED = 65536;
|
|
878
1017
|
/**
|
|
879
|
-
*
|
|
880
|
-
*
|
|
1018
|
+
* `hmtx.advanceWidth` is a `UFWORD`, so this is never negative and truncation
|
|
1019
|
+
* and flooring are one rule. ADR 0042.
|
|
881
1020
|
*/
|
|
882
1021
|
function quantiseAdvance(px) {
|
|
883
1022
|
return Math.trunc(px * FIXED) / FIXED;
|
|
@@ -886,6 +1025,15 @@ function quantiseKern(px) {
|
|
|
886
1025
|
return Math.round(px * FIXED) / FIXED;
|
|
887
1026
|
}
|
|
888
1027
|
/**
|
|
1028
|
+
* A box metric as the browser probe reports it: read at `FACE_BOX_PX`, rounded
|
|
1029
|
+
* half up to the pixel, and handed back as a fraction of the em. CoreText holds
|
|
1030
|
+
* the em ratio as 16.16 fixed point first. T13 30/30, T14 248/248. ADR 0053.
|
|
1031
|
+
*/
|
|
1032
|
+
function boxPixels(units, unitsPerEm, backend) {
|
|
1033
|
+
const px = backend === "coretext" ? Math.round(units / unitsPerEm * EM_FIXED) / EM_FIXED * FACE_BOX_PX : units * FACE_BOX_PX / unitsPerEm;
|
|
1034
|
+
return Math.floor(px + .5) / FACE_BOX_PX;
|
|
1035
|
+
}
|
|
1036
|
+
/**
|
|
889
1037
|
* A measurer bound to one library.
|
|
890
1038
|
*
|
|
891
1039
|
* Both probes cache by family, because a slide asks for the same handful of
|
|
@@ -897,11 +1045,14 @@ function createFontMeasurer(library) {
|
|
|
897
1045
|
/** Which face draws a code point the asked-for face has no glyph for. */
|
|
898
1046
|
const fallbacks = /* @__PURE__ */ new Map();
|
|
899
1047
|
const faceFor = (family, bold, italic) => {
|
|
900
|
-
const cacheKey = `${family}
|
|
1048
|
+
const cacheKey = `${family}\u0000${bold ? "b" : ""}${italic ? "i" : ""}`;
|
|
901
1049
|
const cached = resolved.get(cacheKey);
|
|
902
1050
|
if (cached !== void 0) return cached;
|
|
903
1051
|
const found = library.resolve(family, bold, italic);
|
|
904
|
-
if (found === void 0)
|
|
1052
|
+
if (found === void 0) {
|
|
1053
|
+
const where = library.directories.length === 0 ? "no directory was searched" : `nothing was found in ${library.directories.join(", ")}`;
|
|
1054
|
+
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);
|
|
1055
|
+
}
|
|
905
1056
|
resolved.set(cacheKey, found);
|
|
906
1057
|
return found;
|
|
907
1058
|
};
|
|
@@ -974,14 +1125,20 @@ function createFontMeasurer(library) {
|
|
|
974
1125
|
const cached = boxes.get(family);
|
|
975
1126
|
if (cached !== void 0) return cached;
|
|
976
1127
|
const { metrics } = faceFor(family, false, false).face;
|
|
1128
|
+
const descent = boxPixels(metrics.descent, metrics.unitsPerEm, metrics.backend);
|
|
1129
|
+
const ideographic = metrics.ideographic === void 0 ? descent : metrics.ideographic / metrics.unitsPerEm;
|
|
977
1130
|
const box = {
|
|
978
|
-
ascent: metrics.ascent
|
|
979
|
-
descent
|
|
980
|
-
ideographic:
|
|
1131
|
+
ascent: boxPixels(metrics.ascent, metrics.unitsPerEm, metrics.backend),
|
|
1132
|
+
descent,
|
|
1133
|
+
ideographic: ideographic > 0 && ideographic < 1 ? ideographic : 0
|
|
981
1134
|
};
|
|
982
1135
|
boxes.set(family, box);
|
|
983
1136
|
return box;
|
|
984
1137
|
} },
|
|
1138
|
+
cssFamilyFor(font) {
|
|
1139
|
+
const face = faceFor(font.family, font.bold === true, font.italic === true);
|
|
1140
|
+
return fontStack(face.asked, face.drawn);
|
|
1141
|
+
},
|
|
985
1142
|
used() {
|
|
986
1143
|
const seen = /* @__PURE__ */ new Map();
|
|
987
1144
|
for (const face of resolved.values()) seen.set(face.asked, {
|
|
@@ -1010,11 +1167,10 @@ function createFontMeasurer(library) {
|
|
|
1010
1167
|
*/
|
|
1011
1168
|
/** What a slide is drawn at when the caller says nothing. PowerPoint's own. */
|
|
1012
1169
|
const DEFAULT_WIDTH = 1920;
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
};
|
|
1170
|
+
/** Text is drawn unless the caller says otherwise. */
|
|
1171
|
+
function drawsText(options) {
|
|
1172
|
+
return options.text ?? true;
|
|
1173
|
+
}
|
|
1018
1174
|
/** The media resolver: an rId means the rels of the part the fill was written in. */
|
|
1019
1175
|
function mediaFrom(store) {
|
|
1020
1176
|
return (embed, part) => {
|
|
@@ -1031,46 +1187,50 @@ function mediaFrom(store) {
|
|
|
1031
1187
|
/**
|
|
1032
1188
|
* Render a deck that is already in memory.
|
|
1033
1189
|
*
|
|
1034
|
-
*
|
|
1035
|
-
*
|
|
1190
|
+
* The library entry point: no file system, no process, no stdout. `runRender` is
|
|
1191
|
+
* the verb wrapped around it, and the three fields it adds are its own.
|
|
1036
1192
|
*/
|
|
1037
|
-
function renderDeck(bytes, options) {
|
|
1193
|
+
function renderDeck(bytes, options = {}) {
|
|
1194
|
+
const width = options.width ?? 1920;
|
|
1195
|
+
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));
|
|
1196
|
+
const slide = options.slide ?? null;
|
|
1038
1197
|
const store = PartStore.open(bytes);
|
|
1039
1198
|
const document = loadDocument(store);
|
|
1040
1199
|
const size = document.slideSize;
|
|
1041
|
-
const height = Math.round(
|
|
1042
|
-
if (
|
|
1200
|
+
const height = Math.round(width * size.cy / size.cx);
|
|
1201
|
+
if (slide !== null) {
|
|
1043
1202
|
const count = document.slides.length;
|
|
1044
|
-
if (!Number.isInteger(
|
|
1203
|
+
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
1204
|
}
|
|
1046
1205
|
let library = null;
|
|
1047
|
-
const fonts = options
|
|
1048
|
-
extra: options.fontDirs,
|
|
1049
|
-
system: options.systemFonts
|
|
1206
|
+
const fonts = drawsText(options) ? createFontMeasurer(library = indexFonts({
|
|
1207
|
+
extra: options.fontDirs ?? [],
|
|
1208
|
+
system: options.systemFonts !== false
|
|
1050
1209
|
})) : null;
|
|
1051
1210
|
const media = mediaFrom(store);
|
|
1052
1211
|
return {
|
|
1053
|
-
slides: (
|
|
1212
|
+
slides: (slide === null ? document.slides.map((sheet, at) => ({
|
|
1054
1213
|
sheet,
|
|
1055
1214
|
number: at + 1
|
|
1056
1215
|
})) : [{
|
|
1057
|
-
sheet: document.slides[
|
|
1058
|
-
number:
|
|
1216
|
+
sheet: document.slides[slide - 1],
|
|
1217
|
+
number: slide
|
|
1059
1218
|
}]).map(({ sheet, number }) => ({
|
|
1060
1219
|
number,
|
|
1061
1220
|
svg: renderSlide(sheet, size, {
|
|
1062
|
-
width
|
|
1221
|
+
width,
|
|
1063
1222
|
height,
|
|
1064
1223
|
idPrefix: `s${String(number)}`,
|
|
1065
1224
|
media,
|
|
1066
1225
|
text: fonts === null ? false : {
|
|
1067
1226
|
defaultTextStyle: document.defaultTextStyle,
|
|
1068
1227
|
measurer: fonts.measurer,
|
|
1069
|
-
faceBox: fonts.faceBox
|
|
1228
|
+
faceBox: fonts.faceBox,
|
|
1229
|
+
cssFamilyFor: fonts.cssFamilyFor
|
|
1070
1230
|
}
|
|
1071
1231
|
})
|
|
1072
1232
|
})),
|
|
1073
|
-
width
|
|
1233
|
+
width,
|
|
1074
1234
|
height,
|
|
1075
1235
|
fonts: fonts?.used() ?? [],
|
|
1076
1236
|
missing: fonts?.missing() ?? [],
|
|
@@ -1117,7 +1277,7 @@ function report(result, written, options) {
|
|
|
1117
1277
|
const lines = [];
|
|
1118
1278
|
const slides = `${String(result.slides.length)} slide(s) at ${String(result.width)}x${String(result.height)}`;
|
|
1119
1279
|
lines.push(written.length === 0 ? slides : `${slides} -> ${written.length === 1 ? written[0] : `${String(written.length)} files`}`);
|
|
1120
|
-
if (options
|
|
1280
|
+
if (drawsText(options)) {
|
|
1121
1281
|
const substituted = result.fonts.filter((font) => font.substituted);
|
|
1122
1282
|
lines.push(`${String(result.fonts.length)} typeface(s) from ${String(result.facesIndexed)} indexed face(s)` + (substituted.length === 0 ? "" : `, ${String(substituted.length)} substituted`));
|
|
1123
1283
|
for (const font of substituted) lines.push(` ${font.asked} -> ${font.drawn}`);
|
|
@@ -1278,15 +1438,18 @@ function usage() {
|
|
|
1278
1438
|
"",
|
|
1279
1439
|
"render options:",
|
|
1280
1440
|
" --slide <n> one slide, 1-based; every slide by default",
|
|
1281
|
-
" --width <px> the SVG width attribute; the height follows the aspect",
|
|
1441
|
+
" --width <px> the SVG width attribute; the height follows the aspect (default " + String(DEFAULT_WIDTH) + ")",
|
|
1282
1442
|
" --out <path> a directory, or a file when rendering one slide",
|
|
1283
1443
|
" --font-dir <d> look for fonts here first; repeatable",
|
|
1284
|
-
" --no-system-fonts do not look in this platform own font directories",
|
|
1444
|
+
" --no-system-fonts do not look in this platform's own font directories",
|
|
1285
1445
|
" --no-text draw geometry only, and ask no font questions",
|
|
1286
1446
|
" --json what was drawn, and which face drew each typeface",
|
|
1287
1447
|
" --quiet no summary after writing",
|
|
1288
1448
|
"",
|
|
1289
1449
|
" With no --out the markup goes to stdout, which is one slide worth doing.",
|
|
1450
|
+
" In Node, `import { renderDeck } from \"@pptx-studio/cli\"` takes the five",
|
|
1451
|
+
" drawing options above and defaults every one; --out, --json and --quiet are",
|
|
1452
|
+
" this command's own.",
|
|
1290
1453
|
"",
|
|
1291
1454
|
"inspect options:",
|
|
1292
1455
|
" --json the census as JSON, for a script or for committing as a fixture",
|
|
@@ -1474,7 +1637,7 @@ function main(argv, streams = CONSOLE_STREAMS) {
|
|
|
1474
1637
|
streams.err("--slide wants a positive integer, got " + String(values.slide) + "\n");
|
|
1475
1638
|
return 2;
|
|
1476
1639
|
}
|
|
1477
|
-
const width = positiveInteger(values.width,
|
|
1640
|
+
const width = positiveInteger(values.width, DEFAULT_WIDTH);
|
|
1478
1641
|
if (width === null) {
|
|
1479
1642
|
streams.err("--width wants a positive integer, got " + String(values.width) + "\n");
|
|
1480
1643
|
return 2;
|
|
@@ -1565,6 +1728,6 @@ function positiveInteger(value, fallback) {
|
|
|
1565
1728
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
|
|
1566
1729
|
}
|
|
1567
1730
|
//#endregion
|
|
1568
|
-
export {
|
|
1731
|
+
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
1732
|
|
|
1570
|
-
//# sourceMappingURL=main-
|
|
1733
|
+
//# sourceMappingURL=main-3sNR_Xlp.js.map
|