@openpresentation/opf-render 0.11.3 → 0.11.4

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/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  **Aptos previews with Intos.** The Office pack includes Intos, Intos Display, Intos Narrow and Intos Serif (16 faces, about 12 MB, vendored under `fonts/intos/`, SIL OFL 1.1, pinned to upstream commit fef9315c14da9e4b23b4c3cac8e718998d4e4736 and per-file SHA-256, unmodified from the upstream files). They are the metric-compatible previews for Aptos, Aptos Display, Aptos Narrow and Aptos Serif, the default `aptos` font scheme: 0.000% mean and maximum width difference against Aptos 2.01 in all four styles, with equal vertical metrics. The letter shapes are Intos's own, not Aptos's. The exported PPTX still names Aptos; no Aptos file is bundled or embedded. A registry built without these faces (the base pack) has no Aptos substitute: visual policy falls back to Roboto or Carlito. Like the open families, Intos is `embed: "used"`: it is not in `registry.embeddedFonts` (the 33 eager npm faces), `prepareNodeFonts().options.embeddedFonts` supplies it, and an SVG embeds only the Intos faces its text draws. `registry.lazyFonts` lists the 51 vendored faces (35 open, 16 Intos) with package-relative file and sha256, and `loadBrowserFontRegistry(faces, {lazyFontsBaseUrl})` loads them on demand: `await registry.ensureLazyFonts(presentation)` fetches and verifies only the files the document's font families resolve to (the default Aptos scheme: Intos and Intos Display, 8 files, 5.9 MB), then adds them to the document and the registry together so measurement and painting never disagree. Hosts copy the vendored directories next to the page.
4
4
 
5
- Version 0.11.3 previews the classic chart types natively, draws deprecated chart ids as their replacement and scales value axes like Office (no API change; the preview of chart slides changes). Version 0.11.2 makes `scripts: 'auto'` load the script faces a font scheme names and adds `details.loadedFaceHasGlyph` to `missing-glyph` (no export change). Version 0.11.1 adds the open replacement font families the policy routes to and complete Red Hat styles (no API change). Version 0.11.0 requires `@openpresentation/opf` ^0.11.2 (cover centering, Intos Aptos previews, lazy font loading, automatic script fonts and glyph fallback); use it with PPTX 0.11.0 and editor 0.10.0 so preview and export resolve the same core. Version 0.10.0 required `@openpresentation/opf` ^0.11.1 (font policy table, script fonts and right-to-left paragraphs, socials, slide-image treatments, formatted slide-number and date furniture with the host `date` option, per-item alignment). See the changelog for the intentional preview-output changes.
5
+ Version 0.11.4 previews the seven chartex chart types natively (the world map as a non-geographic tile grid), keeps the Latin Noto Sans replacement for script schemes under `scripts: 'auto'` in Node, shapes Noto Sans Mongolian again and bundles Raleway and Playfair Display (`registry.lazyFonts` lists 94 faces; no API change). Version 0.11.3 previews the classic chart types natively, draws deprecated chart ids as their replacement and scales value axes like Office (no API change; the preview of chart slides changes). Version 0.11.2 makes `scripts: 'auto'` load the script faces a font scheme names and adds `details.loadedFaceHasGlyph` to `missing-glyph` (no export change). Version 0.11.1 adds the open replacement font families the policy routes to and complete Red Hat styles (no API change). Version 0.11.0 requires `@openpresentation/opf` ^0.11.2 (cover centering, Intos Aptos previews, lazy font loading, automatic script fonts and glyph fallback); use it with PPTX 0.11.0 and editor 0.10.0 so preview and export resolve the same core. Version 0.10.0 required `@openpresentation/opf` ^0.11.1 (font policy table, script fonts and right-to-left paragraphs, socials, slide-image treatments, formatted slide-number and date furniture with the host `date` option, per-item alignment). See the changelog for the intentional preview-output changes.
6
6
 
7
7
  Version 0.9.0 required `@openpresentation/opf` ^0.11.0 and resolved content ColorRef / `variables` through core `resolveColorRef()`. Authored `#RRGGBB` paint stays authored. It retains the existing rendering APIs.
8
8
 
@@ -146,9 +146,9 @@ Font schemes select openly licensed Latin families that no Office substitute cov
146
146
  | Libre Caslon Text | `@expo-google-fonts/libre-caslon-text@0.4.0` (same) | 400, 400 italic, 700 (Baskerville Old Face, Bookman Old Style) |
147
147
  | Bitter | `@expo-google-fonts/bitter@0.4.2` (instanced statics named "Bitter"; the OFL reserves "Bitter Pro") | 400, 700, italics (Rockwell) |
148
148
 
149
- All are SIL OFL 1.1, and each manifest entry records the SPDX id read from the shipped notice, its Reserved Font Names, the upstream project and copyright line. An OFL Reserved Font Name stops a modified version from using the reserved name, so the families that have one (PT Serif, Lora, Merriweather Sans, Source Sans 3) are the copyright holder's byte-identical files: every face records `upstreamFile { url, sha256 }` with a commit-pinned URL and a sha256 equal to the face's own, and `test/font-licenses.mjs` enforces it. The other families have none, except Bitter (Reserved Font Name "Bitter Pro"); their files are either google/fonts statics (Barlow and Anton: byte-identical, also pinned by `upstreamFile`) or the instanced statics of the `@expo-google-fonts` packages, whose notice and version `PROVENANCE.json` records.
149
+ All are SIL OFL 1.1, and each manifest entry records the SPDX id read from the shipped notice, its Reserved Font Names, the upstream project and copyright line. An OFL Reserved Font Name stops a modified version from using the reserved name, so the families that have one (PT Serif, Lora, Merriweather Sans, Source Sans 3, Raleway, Playfair Display) are the copyright holder's byte-identical files: every face records `upstreamFile { url, sha256 }` with a commit-pinned URL and a sha256 equal to the face's own, and `test/font-licenses.mjs` enforces it. The other families have none, except Bitter (Reserved Font Name "Bitter Pro"); their files are either google/fonts statics (Barlow and Anton: byte-identical, also pinned by `upstreamFile`) or the instanced statics of the `@expo-google-fonts` packages, whose notice and version `PROVENANCE.json` records.
150
150
 
151
- Not bundled: **Raleway** and **Playfair Display**. Their upstream releases ship only variable fonts, and the Node raster engine (resvg 2.6.2) ignores the weight axis: weights 400 and 700 rendered byte-identically, and Raleway's default instance is Thin. Bundling the instanced statics would modify them under their Reserved Font Names. They keep failing with "no face" until the raster engine supports variable weights or upstream publishes statics. **Bitter** (the Rockwell replacement) reserves the name "Bitter Pro". OFL only stops a modified font from carrying its Reserved Font Name in its family or file name, so the instanced statics, named "Bitter", are allowed; `test/font-licenses.mjs` implements exactly that name-contains rule for instanced faces (an instanced face named Carlito, Raleway, Lora or Playfair Display still fails, and a family that must keep its reserved name ships the unmodified upstream file). **Libre Caslon Text** has no bold italic (its static releases and the Google Fonts instances stop at Regular, Italic and Bold): a bold italic request draws the real italic and reports `visual`; bold is never synthesized.
151
+ **Raleway** and **Playfair Display** reserve their own names, and Google Fonts serves them only as variable fonts, which the Node raster engine (resvg 2.6.2) draws at one weight (400 and 700 rendered byte-identically, and Raleway's default instance is Thin). Instancing them would be a modified version under the reserved name, so they are bundled as the copyright holders' unmodified static files instead: googlefonts/Raleway at commit `7e0be84` and clauseggers/Playfair at `6e115d7` (tag 1.202), `fonts/TTF`, four styles each, byte-identical with `upstreamFile` pins. **Bitter** (the Rockwell replacement) reserves the name "Bitter Pro". OFL only stops a modified font from carrying its Reserved Font Name in its family or file name, so the instanced statics, named "Bitter", are allowed; `test/font-licenses.mjs` implements exactly that name-contains rule for instanced faces (an instanced face named Carlito, Raleway, Lora or Playfair Display still fails, and a family that must keep its reserved name ships the unmodified upstream file). **Libre Caslon Text** has no bold italic (its static releases and the Google Fonts instances stop at Regular, Italic and Bold): a bold italic request draws the real italic and reports `visual`; bold is never synthesized.
152
152
 
153
153
  Source Sans Pro was renamed Source Sans 3. A request for the old name draws the Source Sans 3 faces through a built-in alias (`renamedFrom` in the manifest) and is reported `visual`, in any substitution policy, because the releases differ (bold advances by about 0.7%). Source Sans 3 itself resolves exact and is also the declared replacement for Candara, Corbel, Gill Sans MT and Seaford. Red Hat Display and Text are the declared replacements for Segoe UI and Tahoma. The RedHatFont repository's own static files cannot be used: the Regular, Light and SemiBold italics (and Red Hat Text Italic) do not set the OS/2 italic bit (only the Bold Italic files do), Red Hat Display SemiBold declares weight 707 and Bold 799 (Red Hat Text Bold declares 700), so in resvg a 600 request paints as Bold and the regular and bold italics paint as one face (probed). The vendored Red Hat faces are therefore the correctly labelled instanced statics of `@expo-google-fonts` (Red Hat has no Reserved Font Name): Segoe UI Semibold draws a real 600 (and 600 italic), and Segoe UI, Segoe UI Light and Tahoma draw real italics, none synthesized. Trade-off: the shipped Red Hat Display Bold (Google Fonts weight 700 instance) is about 2.5% narrower than the RedHatFont Bold it replaces, so the Segoe UI Bold preview moves from +0.6% to -1.8% mean width against Segoe UI Bold 5.71 (mean absolute 0.95% to 1.86%, max 6.0% to 5.5%); the regular widths are identical. A style a family lacks is not synthesized: Bebas Neue and Anton bold draw the regular face and report `visual`, and an italic request throws `font-style-unavailable`, as for Roboto Mono. A test renders every vendored face through resvg and checks that exactly that face paints.
154
154
 
@@ -244,6 +244,8 @@ Dependency policy:
244
244
 
245
245
  Browser support boundary: `renderSvg`, `renderSvgDeck`, and `resolvePresentation` are browser-importable pure JavaScript APIs. `svgToPng` and `svgToPdf` are Node APIs in this package version because they depend on the Node build of resvg.
246
246
 
247
+ Chartex chart previews (FF-22b) draw the constructs opf-pptx exports as Office 2016 chartex parts: `treemap` (squarified tiles of the first series, one colour per tile, category labels), `histogram` (a lone value column binned like PowerPoint with Scott's rule count and right-closed bins, or one column per category), `pareto` (columns sorted descending with the cumulative-percentage line on a 0-100% axis), `box-and-whisker` (rows grouped by category, one box per series, exclusive quartiles, whiskers within 1.5 IQR, mean markers, outlier points), `waterfall` (floating bars from the running total, increases and decreases in the first two palette colours, connector lines) and `funnel` (centred bars with value labels). `world` is an honest non-geographic preview: one tile per region shaded by value with its name and value. No geography data is shipped; PowerPoint draws the real map from Bing geodata it fetches itself, so the preview and the native map agree on labels, values and the series colour, not on shapes. Each chartex kind also accepts a lone value column (row numbers as categories; histogram and pareto bin the values). Every mark and label keeps a `data-opf-path` (bins and boxes trace to their value column).
248
+
247
249
  Classic chart previews keep finite axis coordinates for subnormal values and values up to `Number.MAX_VALUE`, including mixed positive/negative ranges. At these limits, available numeric precision can merge adjacent axis ticks or prevent extra headroom. Authored values are unchanged. Stacked, percentage or pie/doughnut totals that overflow the finite numeric range are still unsupported: rendering throws a `RangeError` naming the chart path and asking the caller to rescale the values. It does not substitute an empty chart or silently discard the overflowing values.
248
250
 
249
251
  ## Development
package/dist/charts.js CHANGED
@@ -105,9 +105,21 @@ const RENDERERS = {
105
105
  pie: renderCircularChart,
106
106
  doughnut: renderCircularChart,
107
107
  scatter: renderScatterChart,
108
- radar: renderRadarChart
108
+ radar: renderRadarChart,
109
+ // Chartex constructs (opf-pptx writes them as cx:chartSpace parts, FF-22b).
110
+ treemap: renderTreemapChart,
111
+ histogram: renderHistogramChart,
112
+ pareto: renderHistogramChart,
113
+ box: renderBoxWhiskerChart,
114
+ waterfall: renderWaterfallChart,
115
+ funnel: renderFunnelChart,
116
+ map: renderRegionMapChart
109
117
  };
110
118
 
119
+ // Chartex constructs also accept a lone value column (the exporter plots it
120
+ // against row numbers, or bins it for histogram and pareto).
121
+ const CHARTEX_KINDS = new Set(["treemap", "histogram", "pareto", "box", "waterfall", "funnel", "map"]);
122
+
111
123
  /**
112
124
  * Render a catalog chart. Returns null when the id is outside the catalog, has
113
125
  * no preview renderer, or carries no inline category-major data, so the caller
@@ -120,7 +132,7 @@ export function renderCatalogChart(item, box, bound, options, svg) {
120
132
  const data = item.value?.data;
121
133
  const rows = Array.isArray(data?.rows) ? data.rows.map((row) => Array.isArray(row) ? row : [row]) : [];
122
134
  const columns = Array.isArray(data?.columns) ? data.columns : [];
123
- if (!render || !rows.length || columns.length < 2) return null;
135
+ if (!render || !rows.length || columns.length < (CHARTEX_KINDS.has(spec.kind) ? 1 : 2)) return null;
124
136
  const c = chartContext(item, box, bound, options, svg, rows, columns);
125
137
  c.mark("rect", { x: box.x, y: box.y, width: box.width, height: box.height, fill: c.surface, stroke: bound.design.colors.border, "stroke-width": 1 }, item.path);
126
138
  render(c, spec);
@@ -642,3 +654,356 @@ function renderRadarChart(c, spec) {
642
654
  c.text(label, { x: cx - width - fontPx * 0.2, y: cy - rAt(tick) - c.lineHeight / 2, width, height: c.lineHeight }, c.path, "right");
643
655
  }
644
656
  }
657
+
658
+ // ---------------------------------------------------------------------------
659
+ // Chartex constructs (FF-22b). opf-pptx writes these as native Office 2016
660
+ // chartex parts (cx:series layoutId treemap, clusteredColumn with binning or
661
+ // aggregation, paretoLine, boxWhisker, waterfall, funnel, regionMap); the
662
+ // previews below draw the same data with the same colours and defaults.
663
+
664
+ /**
665
+ * The plotted column of a chartex chart: [Category, Value, ...] plots the first
666
+ * value column against its categories; a lone value column is plotted against
667
+ * the row numbers (histogram and pareto bin the values instead).
668
+ */
669
+ export function chartexValues(rows, columns) {
670
+ const hasCategories = columns.length > 1;
671
+ const column = hasCategories ? 1 : 0;
672
+ const values = rows.map((row) => chartNumber(row[column]));
673
+ const categories = rows.map((row, i) => hasCategories ? (row[0] === null || row[0] === undefined ? "" : String(row[0])) : String(i + 1));
674
+ return { hasCategories, column, values, categories };
675
+ }
676
+
677
+ // Office's automatic histogram bins: Scott's normal reference rule gives the bin
678
+ // width 3.49 * sd * n^(-1/3) (sample standard deviation) over [min, max]; the
679
+ // exporter writes the same count as cx:binCount.
680
+ export function scottBinCount(values) {
681
+ const n = values.length;
682
+ if (n === 0) return 1;
683
+ let min = Infinity, max = -Infinity, mean = 0;
684
+ for (const value of values) { if (value < min) min = value; if (value > max) max = value; mean += value / n; }
685
+ if (!(max > min) || n < 2) return 1;
686
+ let variance = 0;
687
+ for (const value of values) variance += (value - mean) * (value - mean) / (n - 1);
688
+ const width = 3.49 * Math.sqrt(variance) * Math.cbrt(n) ** -1;
689
+ if (!(width > 0) || !Number.isFinite(width)) return 1;
690
+ const count = Math.ceil((max - min) / width - 1e-9);
691
+ return Number.isFinite(count) && count >= 1 ? count : 1;
692
+ }
693
+
694
+ /**
695
+ * Equal-width bins from the minimum, closed on the right like PowerPoint's
696
+ * ("[min, e1]", then "(e1, e2]"): each value falls in the last bin whose upper
697
+ * edge it does not exceed. Labels carry six significant digits, and more when
698
+ * two bins would read alike.
699
+ */
700
+ export function histogramBins(values) {
701
+ const finite = values.filter((value) => value !== null && Number.isFinite(value));
702
+ if (!finite.length) return [];
703
+ const min = finite.reduce((a, b) => Math.min(a, b)), max = finite.reduce((a, b) => Math.max(a, b));
704
+ const count = scottBinCount(finite);
705
+ const edge = (index) => index === 0 ? min : index === count ? max : min / count * (count - index) + max / count * index;
706
+ const bins = Array.from({ length: count }, (_, index) => ({ low: edge(index), high: edge(index + 1), count: 0 }));
707
+ for (const value of finite) {
708
+ let index = 0;
709
+ while (index + 1 < count && value > bins[index].high) index++;
710
+ bins[index].count++;
711
+ }
712
+ for (let precision = 6; ; precision++) {
713
+ const format = (value) => String(Number(value.toPrecision(precision)));
714
+ const labels = bins.map((bin, index) => count === 1 && min === max ? format(min) : `${index === 0 ? "[" : "("}${format(bin.low)}, ${format(bin.high)}]`);
715
+ if (new Set(labels).size === labels.length || precision === 17) return bins.map((bin, index) => ({ ...bin, label: labels[index] }));
716
+ }
717
+ }
718
+
719
+ // Vertical layout shared by the axis-bearing constructs: category labels below,
720
+ // value ticks on the left (and a percentage axis on the right for pareto).
721
+ function chartexPlot(c, { categories, legendWidth = 0, tickLabels = [], percentAxis = false }) {
722
+ const { box, pad, fontPx } = c;
723
+ const top = box.y + pad + c.lineHeight / 2;
724
+ const bottom = box.y + box.height - pad - c.lineHeight;
725
+ const height = Math.max(1, bottom - top);
726
+ const tickWidth = Math.max(0, ...tickLabels.map((label) => c.width(label))) + fontPx * 0.5;
727
+ const x = box.x + pad + tickWidth;
728
+ const right = box.x + box.width - pad - legendWidth - (percentAxis ? c.width("100%") + fontPx : fontPx / 2);
729
+ const plot = { x, y: top, width: Math.max(1, right - x), height };
730
+ const band = plot.width / Math.max(1, categories.length);
731
+ categories.forEach((entry, i) => c.text(entry.name, { x: plot.x + i * band, y: plot.y + plot.height, width: band, height: c.lineHeight }, entry.path));
732
+ return { plot, band };
733
+ }
734
+
735
+ function valueAxis(c, plot, scale) {
736
+ const at = (value) => plot.y + axisFraction(value, scale, true) * plot.height;
737
+ for (const tick of scale.ticks) {
738
+ const y = at(tick);
739
+ gridline(c, plot.x, y, plot.x + plot.width, y);
740
+ c.text(formatTick(tick), { x: c.box.x + c.pad, y: y - c.lineHeight / 2, width: plot.x - c.box.x - c.pad - c.fontPx * 0.35, height: c.lineHeight }, c.path, "right");
741
+ }
742
+ const crossing = at(Math.min(scale.max, Math.max(scale.min, 0)));
743
+ axisLine(c, plot.x, plot.y, plot.x, plot.y + plot.height);
744
+ axisLine(c, plot.x, crossing, plot.x + plot.width, crossing);
745
+ return { at, crossing };
746
+ }
747
+
748
+ // Histogram (cx:binning or cx:aggregation) and Pareto (sorted columns with the cumulative-percentage line on a percentage axis).
749
+ function renderHistogramChart(c, spec) {
750
+ const pareto = spec.kind === "pareto";
751
+ const { hasCategories, column, values, categories } = chartexValues(c.rows, c.columns);
752
+ let bars;
753
+ if (hasCategories) {
754
+ bars = values.map((value, i) => ({ value, name: categories[i], path: `${c.path}.data.rows.${i}.${column}`, labelPath: `${c.path}.data.rows.${i}.0` })).filter((bar) => bar.value !== null);
755
+ } else {
756
+ bars = histogramBins(values).map((bin) => ({ value: bin.count, name: bin.label, path: `${c.path}.data.columns.${column}`, labelPath: c.path }));
757
+ }
758
+ if (pareto) bars = bars.slice().sort((a, b) => b.value - a.value);
759
+ const total = bars.reduce((sum, bar) => sum + Math.max(0, bar.value), 0);
760
+ if (!Number.isFinite(total)) throw chartAggregateError(c.path, "cumulative total");
761
+ const dataMax = bars.length ? Math.max(...bars.map((bar) => bar.value)) : 1, dataMin = bars.length ? Math.min(0, ...bars.map((bar) => bar.value)) : 0;
762
+ const probe = niceScale(dataMin, dataMax, 10);
763
+ const { plot, band } = chartexPlot(c, { categories: bars.map((bar) => ({ name: bar.name, path: bar.labelPath })), tickLabels: probe.ticks.map((tick) => formatTick(tick)), percentAxis: pareto });
764
+ const scale = niceScale(dataMin, dataMax, maxIntervalsFor(plot.height, c.lineHeight * 1.2));
765
+ const { at, crossing } = valueAxis(c, plot, scale);
766
+ const width = band / 1.06, offset = (band - width) / 2;
767
+ bars.forEach((bar, i) => {
768
+ const y = at(bar.value);
769
+ c.mark("rect", { x: plot.x + i * band + offset, y: Math.min(y, crossing), width, height: Math.abs(crossing - y), fill: c.colors[0] }, bar.path);
770
+ });
771
+ if (pareto && bars.length) {
772
+ // Percentage axis on the right, 0-100%, and the cumulative line through the bar centres.
773
+ const percent = niceScale(0, 1, maxIntervalsFor(plot.height, c.lineHeight * 1.2), { percent: true });
774
+ const right = plot.x + plot.width;
775
+ axisLine(c, right, plot.y, right, plot.y + plot.height);
776
+ for (const tick of percent.ticks) {
777
+ const y = plot.y + axisFraction(tick, percent, true) * plot.height;
778
+ c.text(formatTick(tick, true), { x: right + c.fontPx * 0.35, y: y - c.lineHeight / 2, width: c.box.x + c.box.width - c.pad - right - c.fontPx * 0.35, height: c.lineHeight }, c.path, "left");
779
+ }
780
+ let running = 0;
781
+ const points = bars.map((bar, i) => {
782
+ running += Math.max(0, bar.value);
783
+ const fraction = total ? running / total : 0;
784
+ return `${c.num(plot.x + (i + 0.5) * band)},${c.num(plot.y + axisFraction(fraction, percent, true) * plot.height)}`;
785
+ });
786
+ c.mark("polyline", { points: points.join(" "), fill: "none", stroke: c.colors[1 % c.colors.length], "stroke-width": 2 * c.pt, "stroke-linejoin": "round", "stroke-linecap": "round" }, `${c.path}.data.columns.${column}`);
787
+ }
788
+ }
789
+
790
+ // Waterfall: floating bars from the running total, increases and decreases in the first two palette colours, connector lines between bars.
791
+ function renderWaterfallChart(c) {
792
+ const { column, values, categories } = chartexValues(c.rows, c.columns);
793
+ let running = 0;
794
+ const bars = values.map((value, i) => {
795
+ if (value === null) return null;
796
+ const from = running;
797
+ running += value;
798
+ if (!Number.isFinite(running)) throw chartAggregateError(c.path, "running total");
799
+ return { i, value, from, to: running };
800
+ }).filter(Boolean);
801
+ const extents = bars.flatMap((bar) => [bar.from, bar.to]);
802
+ const dataMin = extents.length ? Math.min(0, ...extents) : 0, dataMax = extents.length ? Math.max(0, ...extents) : 1;
803
+ const probe = niceScale(dataMin, dataMax, 10);
804
+ const { plot, band } = chartexPlot(c, { categories: categories.map((name, i) => ({ name, path: `${c.path}.data.rows.${i}.0` })), tickLabels: probe.ticks.map((tick) => formatTick(tick)) });
805
+ const scale = niceScale(dataMin, dataMax, maxIntervalsFor(plot.height, c.lineHeight * 1.2));
806
+ const { at } = valueAxis(c, plot, scale);
807
+ const width = band / 1.5, offset = (band - width) / 2;
808
+ bars.forEach((bar, k) => {
809
+ const a = at(bar.from), b = at(bar.to);
810
+ const x = plot.x + bar.i * band + offset;
811
+ c.mark("rect", { x, y: Math.min(a, b), width, height: Math.abs(b - a), fill: c.colors[bar.value < 0 ? 1 : 0] }, `${c.path}.data.rows.${bar.i}.${column}`);
812
+ const next = bars[k + 1];
813
+ if (next) c.mark("line", { x1: x + width, y1: b, x2: plot.x + next.i * band + offset, y2: b, stroke: c.axisColor, "stroke-width": c.pt });
814
+ });
815
+ }
816
+
817
+ // Funnel: centred bars from the top, widths proportional to the value, value labels inside and category labels on the left.
818
+ function renderFunnelChart(c) {
819
+ const { box, pad, fontPx } = c;
820
+ const { column, values, categories } = chartexValues(c.rows, c.columns);
821
+ const count = categories.length;
822
+ const max = Math.max(0, ...values.filter((value) => value !== null));
823
+ const labelWidth = Math.min(box.width * 0.3, Math.max(0, ...categories.map(c.width)) + fontPx * 0.5);
824
+ const plot = { x: box.x + pad + labelWidth + pad / 2, y: box.y + pad, width: Math.max(1, box.width - 2 * pad - labelWidth - pad / 2), height: Math.max(1, box.height - 2 * pad) };
825
+ const band = plot.height / count, height = band / 1.06, offset = (band - height) / 2;
826
+ categories.forEach((name, i) => {
827
+ const y = plot.y + i * band;
828
+ c.text(name, { x: box.x + pad, y, width: labelWidth, height: band }, `${c.path}.data.rows.${i}.0`, "right");
829
+ const value = values[i];
830
+ if (value === null || !(max > 0) || value <= 0) return;
831
+ const width = plot.width * Math.min(1, value / max);
832
+ const x = plot.x + (plot.width - width) / 2;
833
+ c.mark("rect", { x, y: y + offset, width, height, fill: c.colors[0] }, `${c.path}.data.rows.${i}.${column}`);
834
+ c.text(formatTick(value), { x: plot.x, y: y + offset, width: plot.width, height }, `${c.path}.data.rows.${i}.${column}`);
835
+ });
836
+ }
837
+
838
+ /**
839
+ * Squarified treemap layout (Bruls, Huizing and van Wijk): items are laid out
840
+ * in rows along the shorter side, each row closed when adding the next item
841
+ * would worsen its worst aspect ratio. Returns one rectangle per item, in the
842
+ * item order given (null for items without area).
843
+ */
844
+ export function squarify(items, x, y, width, height) {
845
+ const total = items.reduce((sum, item) => sum + item.value, 0);
846
+ const rects = new Array(items.length).fill(null);
847
+ if (!(total > 0) || !(width > 0) || !(height > 0)) return rects;
848
+ const scale = width * height / total;
849
+ const ordered = items.map((item, index) => ({ area: item.value * scale, index })).filter((item) => item.area > 0).sort((a, b) => b.area - a.area || a.index - b.index);
850
+ let free = { x, y, width, height };
851
+ let row = [];
852
+ const worst = (candidates, side) => {
853
+ const sum = candidates.reduce((s, item) => s + item.area, 0);
854
+ const min = Math.min(...candidates.map((item) => item.area)), max = Math.max(...candidates.map((item) => item.area));
855
+ return Math.max(side * side * max / (sum * sum), sum * sum / (side * side * min));
856
+ };
857
+ const layoutRow = () => {
858
+ const sum = row.reduce((s, item) => s + item.area, 0);
859
+ const horizontal = free.width >= free.height;
860
+ const side = horizontal ? free.height : free.width;
861
+ const thickness = side > 0 ? sum / side : 0;
862
+ let offset = 0;
863
+ for (const item of row) {
864
+ const length = thickness > 0 ? item.area / thickness : 0;
865
+ rects[item.index] = horizontal
866
+ ? { x: free.x, y: free.y + offset, width: thickness, height: length }
867
+ : { x: free.x + offset, y: free.y, width: length, height: thickness };
868
+ offset += length;
869
+ }
870
+ free = horizontal
871
+ ? { x: free.x + thickness, y: free.y, width: Math.max(0, free.width - thickness), height: free.height }
872
+ : { x: free.x, y: free.y + thickness, width: free.width, height: Math.max(0, free.height - thickness) };
873
+ row = [];
874
+ };
875
+ for (const item of ordered) {
876
+ const side = Math.min(free.width, free.height);
877
+ if (row.length && side > 0 && worst([...row, item], side) > worst(row, side)) layoutRow();
878
+ row.push(item);
879
+ }
880
+ if (row.length) layoutRow();
881
+ return rects;
882
+ }
883
+
884
+ // Treemap: one tile per category from the first value column (positive values only), one palette colour per tile, category labels inside.
885
+ function renderTreemapChart(c) {
886
+ const { box, pad, fontPx } = c;
887
+ const { column, values, categories } = chartexValues(c.rows, c.columns);
888
+ const items = values.map((value) => ({ value: value !== null && value > 0 ? value : 0 }));
889
+ const total = items.reduce((sum, item) => sum + item.value, 0);
890
+ if (!Number.isFinite(total)) throw chartAggregateError(c.path, "tile total");
891
+ const area = { x: box.x + pad, y: box.y + pad, width: Math.max(1, box.width - 2 * pad), height: Math.max(1, box.height - 2 * pad) };
892
+ if (!total) {
893
+ c.text("No positive chart values", area, c.path);
894
+ return;
895
+ }
896
+ const rects = squarify(items, area.x, area.y, area.width, area.height);
897
+ rects.forEach((rect, i) => {
898
+ if (!rect || !(rect.width > 0) || !(rect.height > 0)) return;
899
+ c.mark("rect", { x: rect.x, y: rect.y, width: rect.width, height: rect.height, fill: c.colors[i % c.colors.length], stroke: "#FFFFFF", "stroke-width": c.pt }, `${c.path}.data.rows.${i}.${column}`);
900
+ if (rect.width >= fontPx * 2 && rect.height >= c.lineHeight) {
901
+ c.text(categories[i], { x: rect.x + fontPx * 0.25, y: rect.y, width: rect.width - fontPx * 0.5, height: rect.height }, `${c.path}.data.rows.${i}.0`);
902
+ }
903
+ });
904
+ }
905
+
906
+ /**
907
+ * Box statistics as PowerPoint computes them with the exclusive quartile
908
+ * method (Excel QUARTILE.EXC): the rank p(n + 1) interpolates between sorted
909
+ * values and clamps to the extremes, whiskers reach the furthest values within
910
+ * 1.5 IQR of the box, and values beyond are outliers.
911
+ */
912
+ export function boxStatistics(values) {
913
+ const sorted = values.filter((value) => value !== null && Number.isFinite(value)).sort((a, b) => a - b);
914
+ const n = sorted.length;
915
+ if (!n) return null;
916
+ const quantile = (p) => {
917
+ const rank = p * (n + 1);
918
+ if (rank <= 1) return sorted[0];
919
+ if (rank >= n) return sorted[n - 1];
920
+ const lower = Math.floor(rank), fraction = rank - lower;
921
+ return sorted[lower - 1] + fraction * (sorted[lower] - sorted[lower - 1]);
922
+ };
923
+ const q1 = quantile(0.25), median = quantile(0.5), q3 = quantile(0.75);
924
+ const iqr = q3 - q1;
925
+ const inside = sorted.filter((value) => value >= q1 - 1.5 * iqr && value <= q3 + 1.5 * iqr);
926
+ const mean = sorted.reduce((sum, value) => sum + value, 0) / n;
927
+ return { q1, median, q3, mean, low: inside[0] ?? q1, high: inside[inside.length - 1] ?? q3, outliers: sorted.filter((value) => value < q1 - 1.5 * iqr || value > q3 + 1.5 * iqr) };
928
+ }
929
+
930
+ // Box and whisker: rows with the same category form one box per series; median line, mean marker and outliers, gap width 100%.
931
+ function renderBoxWhiskerChart(c) {
932
+ // A lone value column has no category column: like the exporter, its values plot against their row numbers (one box per row).
933
+ const lone = c.columns.length === 1;
934
+ const series = lone ? [{ name: c.label(c.columns[0]), column: 0, index: 0, values: c.rows.map((row) => chartNumber(row[0])) }] : c.series();
935
+ const groups = [];
936
+ c.rows.forEach((row, i) => {
937
+ const name = lone ? String(i + 1) : c.label(row[0]);
938
+ let group = groups.find((entry) => entry.name === name);
939
+ if (!group) groups.push(group = { name, first: i, rows: [] });
940
+ group.rows.push(i);
941
+ });
942
+ const boxes = series.map((s) => groups.map((group) => ({ stats: boxStatistics(group.rows.map((i) => s.values[i])), rows: group.rows })));
943
+ const all = boxes.flat().flatMap((entry) => entry.stats ? [entry.stats.low, entry.stats.high, ...entry.stats.outliers] : []);
944
+ const dataMin = all.length ? Math.min(...all) : 0, dataMax = all.length ? Math.max(...all) : 1;
945
+ const legendWidth = seriesLegend(c, series);
946
+ const probe = niceScale(dataMin, dataMax, 10);
947
+ const { plot, band } = chartexPlot(c, { categories: groups.map((group) => ({ name: group.name, path: lone ? c.path : `${c.path}.data.rows.${group.first}.0` })), legendWidth, tickLabels: probe.ticks.map((tick) => formatTick(tick)) });
948
+ const scale = niceScale(dataMin, dataMax, maxIntervalsFor(plot.height, c.lineHeight * 1.2));
949
+ const { at } = valueAxis(c, plot, scale);
950
+ const group = band / 2, width = group / series.length, offset = (band - group) / 2;
951
+ series.forEach((s, j) => {
952
+ const color = c.colors[j % c.colors.length], path = `${c.path}.data.columns.${s.column}`;
953
+ boxes[j].forEach((entry, i) => {
954
+ const stats = entry.stats;
955
+ if (!stats) return;
956
+ const x = plot.x + i * band + offset + j * width, mid = x + width / 2;
957
+ const top = at(stats.q3), bottom = at(stats.q1);
958
+ c.mark("line", { x1: mid, y1: at(stats.high), x2: mid, y2: top, stroke: color, "stroke-width": c.pt }, path);
959
+ c.mark("line", { x1: mid, y1: bottom, x2: mid, y2: at(stats.low), stroke: color, "stroke-width": c.pt }, path);
960
+ c.mark("line", { x1: x + width * 0.25, y1: at(stats.high), x2: x + width * 0.75, y2: at(stats.high), stroke: color, "stroke-width": c.pt }, path);
961
+ c.mark("line", { x1: x + width * 0.25, y1: at(stats.low), x2: x + width * 0.75, y2: at(stats.low), stroke: color, "stroke-width": c.pt }, path);
962
+ c.mark("rect", { x, y: top, width, height: Math.max(c.pt, bottom - top), fill: color }, path);
963
+ c.mark("line", { x1: x, y1: at(stats.median), x2: x + width, y2: at(stats.median), stroke: c.labelColor, "stroke-width": c.pt }, path);
964
+ const r = 2.5 * c.pt, my = at(stats.mean);
965
+ c.mark("path", { d: `M ${c.num(mid - r)} ${c.num(my - r)} L ${c.num(mid + r)} ${c.num(my + r)} M ${c.num(mid - r)} ${c.num(my + r)} L ${c.num(mid + r)} ${c.num(my - r)}`, fill: "none", stroke: c.labelColor, "stroke-width": c.pt }, path);
966
+ for (const value of stats.outliers) {
967
+ const row = entry.rows.find((k) => s.values[k] === value);
968
+ c.mark("circle", { cx: mid, cy: at(value), r: 3 * c.pt, fill: color, stroke: color, "stroke-width": 0.75 * c.pt }, `${c.path}.data.rows.${row}.${s.column}`);
969
+ }
970
+ });
971
+ });
972
+ }
973
+
974
+ /** Mix two hex colours (#RRGGBB) by `t` in [0, 1], rounded per channel. */
975
+ export function mixHex(from, to, t) {
976
+ const channel = (hex, i) => parseInt(hex.slice(1 + 2 * i, 3 + 2 * i), 16);
977
+ const clamp = Math.min(1, Math.max(0, t));
978
+ return `#${[0, 1, 2].map((i) => Math.round(channel(from, i) + (channel(to, i) - channel(from, i)) * clamp).toString(16).toUpperCase().padStart(2, "0")).join("")}`;
979
+ }
980
+
981
+ // Region map: an honest non-geographic preview. PowerPoint draws the map from
982
+ // Bing geodata it fetches itself; the preview shows one tile per region, shaded
983
+ // from a light tint to the series colour by value, with the region name and
984
+ // its value, so labels and colours are traceable without shipping geography.
985
+ function renderRegionMapChart(c) {
986
+ const { box, pad, fontPx } = c;
987
+ const { column, values, categories } = chartexValues(c.rows, c.columns);
988
+ const count = categories.length;
989
+ const area = { x: box.x + pad, y: box.y + pad, width: Math.max(1, box.width - 2 * pad), height: Math.max(1, box.height - 2 * pad) };
990
+ const columns = Math.max(1, Math.min(count, Math.round(Math.sqrt(count * area.width / Math.max(1, area.height)))));
991
+ const rowsCount = Math.ceil(count / columns);
992
+ const tileWidth = area.width / columns, tileHeight = area.height / rowsCount;
993
+ const finite = values.filter((value) => value !== null);
994
+ const min = finite.length ? Math.min(...finite) : 0, max = finite.length ? Math.max(...finite) : 0;
995
+ const light = mixHex(/^#[0-9A-Fa-f]{6}$/.test(c.surface) ? c.surface : "#FFFFFF", c.colors[0], 0.15);
996
+ categories.forEach((name, i) => {
997
+ const value = values[i];
998
+ const t = value === null ? 0 : max > min ? (value - min) / (max - min) : 1;
999
+ const x = area.x + (i % columns) * tileWidth, y = area.y + Math.floor(i / columns) * tileHeight;
1000
+ c.mark("rect", { x, y, width: tileWidth, height: tileHeight, fill: value === null ? c.surface : mixHex(light, c.colors[0], t), stroke: "#FFFFFF", "stroke-width": c.pt }, `${c.path}.data.rows.${i}.${column}`);
1001
+ const inset = { x: x + fontPx * 0.25, width: Math.max(1, tileWidth - fontPx * 0.5) };
1002
+ if (tileHeight >= c.lineHeight * 2) {
1003
+ c.text(name, { ...inset, y: y + tileHeight / 2 - c.lineHeight, height: c.lineHeight }, `${c.path}.data.rows.${i}.0`);
1004
+ if (value !== null) c.text(formatTick(value), { ...inset, y: y + tileHeight / 2, height: c.lineHeight }, `${c.path}.data.rows.${i}.${column}`);
1005
+ } else if (tileHeight >= c.lineHeight) {
1006
+ c.text(name, { ...inset, y: y + (tileHeight - c.lineHeight) / 2, height: c.lineHeight }, `${c.path}.data.rows.${i}.0`);
1007
+ }
1008
+ });
1009
+ }
@@ -1121,6 +1121,130 @@ export const BUNDLED_FONT_MANIFEST=freeze({
1121
1121
  }
1122
1122
  ]
1123
1123
  },
1124
+ {
1125
+ "name": "raleway",
1126
+ "vendored": "fonts/raleway",
1127
+ "version": "7e0be846a3097743b9bf442eb8e65bc1fd96c934",
1128
+ "pack": "open",
1129
+ "source": "https://github.com/googlefonts/Raleway/tree/7e0be846a3097743b9bf442eb8e65bc1fd96c934/fonts/TTF",
1130
+ "license": "OFL-1.1",
1131
+ "licenseFile": "OFL.txt",
1132
+ "licenseSha256": "7e946cf1171784d1015279e7dc35f827957a6b5d1f1f659ae0c98e5f5e37ed9b",
1133
+ "upstreamLicenseUrl": "https://raw.githubusercontent.com/googlefonts/Raleway/7e0be846a3097743b9bf442eb8e65bc1fd96c934/OFL.txt",
1134
+ "reservedFontNames": [
1135
+ "Raleway"
1136
+ ],
1137
+ "upstream": "http://scripts.sil.org/OFL",
1138
+ "copyright": "Copyright 2010 The Raleway Project Authors (impallari@gmail.com), with Reserved Font Name \"Raleway\".",
1139
+ "faces": [
1140
+ {
1141
+ "file": "Raleway-Regular.ttf",
1142
+ "family": "Raleway",
1143
+ "weight": 400,
1144
+ "italic": false,
1145
+ "sha256": "cb70f8815eecd850263d54fa59286377b1d7362bec612e981155a00039d8b513",
1146
+ "upstreamFile": {
1147
+ "url": "https://raw.githubusercontent.com/googlefonts/Raleway/7e0be846a3097743b9bf442eb8e65bc1fd96c934/fonts/TTF/Raleway-Regular.ttf",
1148
+ "sha256": "cb70f8815eecd850263d54fa59286377b1d7362bec612e981155a00039d8b513"
1149
+ }
1150
+ },
1151
+ {
1152
+ "file": "Raleway-Italic.ttf",
1153
+ "family": "Raleway",
1154
+ "weight": 400,
1155
+ "italic": true,
1156
+ "sha256": "63f52b6d8e88afa43d94162147ff55846f28af630d834018de5cb7ccc75a8ee8",
1157
+ "upstreamFile": {
1158
+ "url": "https://raw.githubusercontent.com/googlefonts/Raleway/7e0be846a3097743b9bf442eb8e65bc1fd96c934/fonts/TTF/Raleway-Italic.ttf",
1159
+ "sha256": "63f52b6d8e88afa43d94162147ff55846f28af630d834018de5cb7ccc75a8ee8"
1160
+ }
1161
+ },
1162
+ {
1163
+ "file": "Raleway-Bold.ttf",
1164
+ "family": "Raleway",
1165
+ "weight": 700,
1166
+ "italic": false,
1167
+ "sha256": "6894481ab1c298e9c66f23455ecf05ccb25393d719a6b2d63428358c0409d2d0",
1168
+ "upstreamFile": {
1169
+ "url": "https://raw.githubusercontent.com/googlefonts/Raleway/7e0be846a3097743b9bf442eb8e65bc1fd96c934/fonts/TTF/Raleway-Bold.ttf",
1170
+ "sha256": "6894481ab1c298e9c66f23455ecf05ccb25393d719a6b2d63428358c0409d2d0"
1171
+ }
1172
+ },
1173
+ {
1174
+ "file": "Raleway-BoldItalic.ttf",
1175
+ "family": "Raleway",
1176
+ "weight": 700,
1177
+ "italic": true,
1178
+ "sha256": "57eb1a701136eca30b52a4847edcec93daa53f0b5597dd98dbc0b07f475ce44a",
1179
+ "upstreamFile": {
1180
+ "url": "https://raw.githubusercontent.com/googlefonts/Raleway/7e0be846a3097743b9bf442eb8e65bc1fd96c934/fonts/TTF/Raleway-BoldItalic.ttf",
1181
+ "sha256": "57eb1a701136eca30b52a4847edcec93daa53f0b5597dd98dbc0b07f475ce44a"
1182
+ }
1183
+ }
1184
+ ]
1185
+ },
1186
+ {
1187
+ "name": "playfair-display",
1188
+ "vendored": "fonts/playfair-display",
1189
+ "version": "6e115d70ee7ff6ed887babf3829ccd1de65fd2bd",
1190
+ "pack": "open",
1191
+ "source": "https://github.com/clauseggers/Playfair/tree/6e115d70ee7ff6ed887babf3829ccd1de65fd2bd/fonts/TTF",
1192
+ "license": "OFL-1.1",
1193
+ "licenseFile": "OFL.txt",
1194
+ "licenseSha256": "e414e07f7a4da16ed47ebec80d52dd79f21d3ca42bc92385a755d8078a038814",
1195
+ "upstreamLicenseUrl": "https://raw.githubusercontent.com/clauseggers/Playfair/6e115d70ee7ff6ed887babf3829ccd1de65fd2bd/OFL.txt",
1196
+ "reservedFontNames": [
1197
+ "Playfair Display"
1198
+ ],
1199
+ "upstream": "https://github.com/clauseggers/Playfair-Display",
1200
+ "copyright": "Copyright 2017 The Playfair Display Project Authors (https://github.com/clauseggers/Playfair-Display), with Reserved Font Name \"Playfair Display\".",
1201
+ "faces": [
1202
+ {
1203
+ "file": "PlayfairDisplay-Regular.ttf",
1204
+ "family": "Playfair Display",
1205
+ "weight": 400,
1206
+ "italic": false,
1207
+ "sha256": "70ef3ee8de32dc81c39f2733a4a2a050c7e6ba78a2f1632f9d5c6cfd482c3abb",
1208
+ "upstreamFile": {
1209
+ "url": "https://raw.githubusercontent.com/clauseggers/Playfair/6e115d70ee7ff6ed887babf3829ccd1de65fd2bd/fonts/TTF/PlayfairDisplay-Regular.ttf",
1210
+ "sha256": "70ef3ee8de32dc81c39f2733a4a2a050c7e6ba78a2f1632f9d5c6cfd482c3abb"
1211
+ }
1212
+ },
1213
+ {
1214
+ "file": "PlayfairDisplay-Italic.ttf",
1215
+ "family": "Playfair Display",
1216
+ "weight": 400,
1217
+ "italic": true,
1218
+ "sha256": "30a7eb5a9309754d4b3e205e6435ef07d80e1b86735774b2ace427f6ed61f991",
1219
+ "upstreamFile": {
1220
+ "url": "https://raw.githubusercontent.com/clauseggers/Playfair/6e115d70ee7ff6ed887babf3829ccd1de65fd2bd/fonts/TTF/PlayfairDisplay-Italic.ttf",
1221
+ "sha256": "30a7eb5a9309754d4b3e205e6435ef07d80e1b86735774b2ace427f6ed61f991"
1222
+ }
1223
+ },
1224
+ {
1225
+ "file": "PlayfairDisplay-Bold.ttf",
1226
+ "family": "Playfair Display",
1227
+ "weight": 700,
1228
+ "italic": false,
1229
+ "sha256": "898a01a59214578e72012131ef4c346844c1fd35613f4ee113a37d61a094a2d0",
1230
+ "upstreamFile": {
1231
+ "url": "https://raw.githubusercontent.com/clauseggers/Playfair/6e115d70ee7ff6ed887babf3829ccd1de65fd2bd/fonts/TTF/PlayfairDisplay-Bold.ttf",
1232
+ "sha256": "898a01a59214578e72012131ef4c346844c1fd35613f4ee113a37d61a094a2d0"
1233
+ }
1234
+ },
1235
+ {
1236
+ "file": "PlayfairDisplay-BoldItalic.ttf",
1237
+ "family": "Playfair Display",
1238
+ "weight": 700,
1239
+ "italic": true,
1240
+ "sha256": "bd3b48ca605438de4337d05ece1ecb9e4149a975d3207174f28e02098bbec3ff",
1241
+ "upstreamFile": {
1242
+ "url": "https://raw.githubusercontent.com/clauseggers/Playfair/6e115d70ee7ff6ed887babf3829ccd1de65fd2bd/fonts/TTF/PlayfairDisplay-BoldItalic.ttf",
1243
+ "sha256": "bd3b48ca605438de4337d05ece1ecb9e4149a975d3207174f28e02098bbec3ff"
1244
+ }
1245
+ }
1246
+ ]
1247
+ },
1124
1248
  {
1125
1249
  "name": "barlow",
1126
1250
  "vendored": "fonts/barlow",
@@ -7,7 +7,7 @@ import { createFontRegistry, OPFFontError } from "./fonts.js";
7
7
  import { BUNDLED_FONT_MANIFEST } from "./font-manifest.js";
8
8
  import { lazyFontList } from "./lazy-font-list.js";
9
9
  export { BUNDLED_FONT_MANIFEST } from "./font-manifest.js";
10
- import { analyzePresentationScripts, nextFallbackPackage, scriptFontPackages, scriptSelectionOf, uncoveredCjkCharacters } from "./script-font-pack.js";
10
+ import { analyzePresentationScripts, autoScriptSelection, nextFallbackPackage, scriptFontPackages, scriptSelectionOf, uncoveredCjkCharacters } from "./script-font-pack.js";
11
11
  export { autoScriptSelection, detectPresentationScripts, scriptFontPackages } from "./script-font-pack.js";
12
12
  // Script faces are embedded in a standalone SVG only when the slide's text uses their family.
13
13
  const embedUsed = entries => entries.map(entry => ({...entry, embed: "used"}));
@@ -174,8 +174,11 @@ export async function loadOfficeFontRegistry({scripts, faces, presentation, onDi
174
174
  // Noto Sans (regular, bold, italic, bold italic) is the default Latin, Cyrillic and Greek fallback face
175
175
  // (glyph fallback), so Georgia with Russian text previews without the scripts option. It is embedded in an
176
176
  // SVG only when the slide's text draws it (embed "used"); raster output reads it from fontFiles. Unless the
177
- // caller asked for it with scripts it is fallback-only, so no other family's preview changes.
178
- const requested = scripts === undefined || scripts === "auto" || (Array.isArray(scripts) && !scripts.length) ? [] : scriptFontPackages(scripts).map(pkg => pkg.name);
177
+ // caller asked for it with scripts it is fallback-only, so no other family's preview changes. "auto" counts as asking for it when the
178
+ // presentation itself selects Latn (a Sylfaen scheme: Noto Sans is its designated replacement, which a fallback-only face never is), as
179
+ // an explicit list does; a presentation that does not select Latn keeps it fallback-only.
180
+ const requested = scripts === "auto" && presentation !== null && typeof presentation === "object" ? scriptFontPackages(autoScriptSelection(presentation).scripts).map(pkg => pkg.name)
181
+ : scripts === undefined || scripts === "auto" || (Array.isArray(scripts) && !scripts.length) ? [] : scriptFontPackages(scripts).map(pkg => pkg.name);
179
182
  const fallback = await loadPackages(scriptFontPackages(["Latn"]).filter(pkg => !requested.includes(pkg.name)), undefined, {fallbackOnly:true});
180
183
  fontFiles.push(...fallback.fontFiles);
181
184
  entries.push(...fallback.entries);
package/dist/fonts.js CHANGED
@@ -45,6 +45,27 @@ function unavailableFontError(family, style, policy) {
45
45
  return new OPFFontError("font-unavailable", `No local font face for '${family}'. ${owner} None of its replacements (${candidates.join(", ")}) is loaded${packs.length ? `; load the ${packs.map(pack=>"'"+pack+"'").join(" or ")} font pack` : ""}, or ${hook}. The PPTX names '${family}' either way.`, details);
46
46
  }
47
47
  /** Local font files only. The caller explicitly chooses aliases and fallback. */
48
+ // fontkit 2.0.4 cannot decode every OpenType lookup: its GSUB type 8 (reverse chaining contextual single substitution) struct lacks the
49
+ // backtrackGlyphCount field, so the lookup fails with "Not a fixed size" when a feature that references it is applied, and it has no processor
50
+ // for the type anyway. Noto Sans Mongolian 3.x has such a lookup under calt and rclt in every script, so every shaping of the face threw (FF-44).
51
+ // A lookup that cannot be decoded is treated as one that applies nothing (no subtables), so the rest of the feature still runs: advances equal
52
+ // a browser's for the face's Latin and Mongolian samples (test/mongolian-shaping.mjs). Applied only after shaping failed, once per font.
53
+ const lookupsGuarded = new WeakSet();
54
+ function skipUndecodableLookups(font) {
55
+ if (lookupsGuarded.has(font)) return false;
56
+ lookupsGuarded.add(font);
57
+ let guarded = false;
58
+ for (const tag of ["GSUB", "GPOS"]) {
59
+ let list;
60
+ try { list = font[tag]?.lookupList; } catch { continue; }
61
+ if (typeof list?.get !== "function") continue;
62
+ const get = list.get.bind(list);
63
+ list.get = index => { try { return get(index); } catch { return { lookupType: 1, flags: {}, subTables: [] }; } };
64
+ guarded = true;
65
+ }
66
+ return guarded;
67
+ }
68
+
48
69
  export function createFontRegistry(entries, options = {}) {
49
70
  if (!Array.isArray(entries) || !entries.length) throw new OPFFontError("empty-font-registry", "Supply at least one font file.");
50
71
  const makeFace = entry => {
@@ -203,11 +224,15 @@ export function createFontRegistry(entries, options = {}) {
203
224
  let run;
204
225
  try { run=shape(); }
205
226
  catch (error) {
227
+ // A lookup fontkit cannot decode (GSUB type 8 in Noto Sans Mongolian) applies nothing; shape again with the rest (FF-44).
228
+ if (skipUndecodableLookups(face.font)) { try { run=shape(); } catch { /* the mark retry below */ } }
206
229
  // fontkit rejects some mark-attachment lookups (null anchors, for example
207
230
  // Gurmukhi tippi in Noto Sans Gurmukhi). Mark positioning moves marks
208
231
  // without changing advances, so measure again without it (FF-19).
209
- try { run=shape({abvm:false,blwm:false,mark:false,mkmk:false}); }
210
- catch { throw new OPFFontError("font-shaping-failed", `Font '${face.family}' cannot shape this text.`, {path:style.path,fontFamily:face.family,cause:error?.message}); }
232
+ if (!run) {
233
+ try { run=shape({abvm:false,blwm:false,mark:false,mkmk:false}); }
234
+ catch { throw new OPFFontError("font-shaping-failed", `Font '${face.family}' cannot shape this text.`, {path:style.path,fontFamily:face.family,cause:error?.message}); }
235
+ }
211
236
  }
212
237
  value={width:run.positions.reduce((total,position)=>total+position.xAdvance,0)/face.font.unitsPerEm,...value};
213
238
  if(includeOutline) {
@@ -0,0 +1,93 @@
1
+ Copyright 2017 The Playfair Display Project Authors (https://github.com/clauseggers/Playfair-Display), with Reserved Font Name "Playfair Display".
2
+
3
+ This Font Software is licensed under the SIL Open Font License, Version 1.1.
4
+ This license is copied below, and is also available with a FAQ at:
5
+ http://scripts.sil.org/OFL
6
+
7
+
8
+ -----------------------------------------------------------
9
+ SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
10
+ -----------------------------------------------------------
11
+
12
+ PREAMBLE
13
+ The goals of the Open Font License (OFL) are to stimulate worldwide
14
+ development of collaborative font projects, to support the font creation
15
+ efforts of academic and linguistic communities, and to provide a free and
16
+ open framework in which fonts may be shared and improved in partnership
17
+ with others.
18
+
19
+ The OFL allows the licensed fonts to be used, studied, modified and
20
+ redistributed freely as long as they are not sold by themselves. The
21
+ fonts, including any derivative works, can be bundled, embedded,
22
+ redistributed and/or sold with any software provided that any reserved
23
+ names are not used by derivative works. The fonts and derivatives,
24
+ however, cannot be released under any other type of license. The
25
+ requirement for fonts to remain under this license does not apply
26
+ to any document created using the fonts or their derivatives.
27
+
28
+ DEFINITIONS
29
+ "Font Software" refers to the set of files released by the Copyright
30
+ Holder(s) under this license and clearly marked as such. This may
31
+ include source files, build scripts and documentation.
32
+
33
+ "Reserved Font Name" refers to any names specified as such after the
34
+ copyright statement(s).
35
+
36
+ "Original Version" refers to the collection of Font Software components as
37
+ distributed by the Copyright Holder(s).
38
+
39
+ "Modified Version" refers to any derivative made by adding to, deleting,
40
+ or substituting -- in part or in whole -- any of the components of the
41
+ Original Version, by changing formats or by porting the Font Software to a
42
+ new environment.
43
+
44
+ "Author" refers to any designer, engineer, programmer, technical
45
+ writer or other person who contributed to the Font Software.
46
+
47
+ PERMISSION & CONDITIONS
48
+ Permission is hereby granted, free of charge, to any person obtaining
49
+ a copy of the Font Software, to use, study, copy, merge, embed, modify,
50
+ redistribute, and sell modified and unmodified copies of the Font
51
+ Software, subject to the following conditions:
52
+
53
+ 1) Neither the Font Software nor any of its individual components,
54
+ in Original or Modified Versions, may be sold by itself.
55
+
56
+ 2) Original or Modified Versions of the Font Software may be bundled,
57
+ redistributed and/or sold with any software, provided that each copy
58
+ contains the above copyright notice and this license. These can be
59
+ included either as stand-alone text files, human-readable headers or
60
+ in the appropriate machine-readable metadata fields within text or
61
+ binary files as long as those fields can be easily viewed by the user.
62
+
63
+ 3) No Modified Version of the Font Software may use the Reserved Font
64
+ Name(s) unless explicit written permission is granted by the corresponding
65
+ Copyright Holder. This restriction only applies to the primary font name as
66
+ presented to the users.
67
+
68
+ 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
69
+ Software shall not be used to promote, endorse or advertise any
70
+ Modified Version, except to acknowledge the contribution(s) of the
71
+ Copyright Holder(s) and the Author(s) or with their explicit written
72
+ permission.
73
+
74
+ 5) The Font Software, modified or unmodified, in part or in whole,
75
+ must be distributed entirely under this license, and must not be
76
+ distributed under any other license. The requirement for fonts to
77
+ remain under this license does not apply to any document created
78
+ using the Font Software.
79
+
80
+ TERMINATION
81
+ This license becomes null and void if any of the above conditions are
82
+ not met.
83
+
84
+ DISCLAIMER
85
+ THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
86
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
87
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
88
+ OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
89
+ COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
90
+ INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
91
+ DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
92
+ FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
93
+ OTHER DEALINGS IN THE FONT SOFTWARE.
@@ -0,0 +1,48 @@
1
+ {
2
+ "upstream": {
3
+ "source": "https://github.com/clauseggers/Playfair/tree/6e115d70ee7ff6ed887babf3829ccd1de65fd2bd/fonts/TTF",
4
+ "commit": "6e115d70ee7ff6ed887babf3829ccd1de65fd2bd",
5
+ "licenseUrl": "https://raw.githubusercontent.com/clauseggers/Playfair/6e115d70ee7ff6ed887babf3829ccd1de65fd2bd/OFL.txt"
6
+ },
7
+ "license": "OFL-1.1",
8
+ "licenseFile": "OFL.txt",
9
+ "licenseSha256": "e414e07f7a4da16ed47ebec80d52dd79f21d3ca42bc92385a755d8078a038814",
10
+ "reservedFontNames": [
11
+ "Playfair Display"
12
+ ],
13
+ "copyright": "Copyright 2017 The Playfair Display Project Authors (https://github.com/clauseggers/Playfair-Display), with Reserved Font Name \"Playfair Display\".",
14
+ "faces": [
15
+ {
16
+ "file": "PlayfairDisplay-Regular.ttf",
17
+ "family": "Playfair Display",
18
+ "weight": 400,
19
+ "italic": false,
20
+ "sha256": "70ef3ee8de32dc81c39f2733a4a2a050c7e6ba78a2f1632f9d5c6cfd482c3abb",
21
+ "upstreamUrl": "https://raw.githubusercontent.com/clauseggers/Playfair/6e115d70ee7ff6ed887babf3829ccd1de65fd2bd/fonts/TTF/PlayfairDisplay-Regular.ttf"
22
+ },
23
+ {
24
+ "file": "PlayfairDisplay-Italic.ttf",
25
+ "family": "Playfair Display",
26
+ "weight": 400,
27
+ "italic": true,
28
+ "sha256": "30a7eb5a9309754d4b3e205e6435ef07d80e1b86735774b2ace427f6ed61f991",
29
+ "upstreamUrl": "https://raw.githubusercontent.com/clauseggers/Playfair/6e115d70ee7ff6ed887babf3829ccd1de65fd2bd/fonts/TTF/PlayfairDisplay-Italic.ttf"
30
+ },
31
+ {
32
+ "file": "PlayfairDisplay-Bold.ttf",
33
+ "family": "Playfair Display",
34
+ "weight": 700,
35
+ "italic": false,
36
+ "sha256": "898a01a59214578e72012131ef4c346844c1fd35613f4ee113a37d61a094a2d0",
37
+ "upstreamUrl": "https://raw.githubusercontent.com/clauseggers/Playfair/6e115d70ee7ff6ed887babf3829ccd1de65fd2bd/fonts/TTF/PlayfairDisplay-Bold.ttf"
38
+ },
39
+ {
40
+ "file": "PlayfairDisplay-BoldItalic.ttf",
41
+ "family": "Playfair Display",
42
+ "weight": 700,
43
+ "italic": true,
44
+ "sha256": "bd3b48ca605438de4337d05ece1ecb9e4149a975d3207174f28e02098bbec3ff",
45
+ "upstreamUrl": "https://raw.githubusercontent.com/clauseggers/Playfair/6e115d70ee7ff6ed887babf3829ccd1de65fd2bd/fonts/TTF/PlayfairDisplay-BoldItalic.ttf"
46
+ }
47
+ ]
48
+ }
@@ -0,0 +1,93 @@
1
+ Copyright 2010 The Raleway Project Authors (impallari@gmail.com), with Reserved Font Name "Raleway".
2
+
3
+ This Font Software is licensed under the SIL Open Font License, Version 1.1.
4
+ This license is copied below, and is also available with a FAQ at:
5
+ http://scripts.sil.org/OFL
6
+
7
+
8
+ -----------------------------------------------------------
9
+ SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
10
+ -----------------------------------------------------------
11
+
12
+ PREAMBLE
13
+ The goals of the Open Font License (OFL) are to stimulate worldwide
14
+ development of collaborative font projects, to support the font creation
15
+ efforts of academic and linguistic communities, and to provide a free and
16
+ open framework in which fonts may be shared and improved in partnership
17
+ with others.
18
+
19
+ The OFL allows the licensed fonts to be used, studied, modified and
20
+ redistributed freely as long as they are not sold by themselves. The
21
+ fonts, including any derivative works, can be bundled, embedded,
22
+ redistributed and/or sold with any software provided that any reserved
23
+ names are not used by derivative works. The fonts and derivatives,
24
+ however, cannot be released under any other type of license. The
25
+ requirement for fonts to remain under this license does not apply
26
+ to any document created using the fonts or their derivatives.
27
+
28
+ DEFINITIONS
29
+ "Font Software" refers to the set of files released by the Copyright
30
+ Holder(s) under this license and clearly marked as such. This may
31
+ include source files, build scripts and documentation.
32
+
33
+ "Reserved Font Name" refers to any names specified as such after the
34
+ copyright statement(s).
35
+
36
+ "Original Version" refers to the collection of Font Software components as
37
+ distributed by the Copyright Holder(s).
38
+
39
+ "Modified Version" refers to any derivative made by adding to, deleting,
40
+ or substituting -- in part or in whole -- any of the components of the
41
+ Original Version, by changing formats or by porting the Font Software to a
42
+ new environment.
43
+
44
+ "Author" refers to any designer, engineer, programmer, technical
45
+ writer or other person who contributed to the Font Software.
46
+
47
+ PERMISSION & CONDITIONS
48
+ Permission is hereby granted, free of charge, to any person obtaining
49
+ a copy of the Font Software, to use, study, copy, merge, embed, modify,
50
+ redistribute, and sell modified and unmodified copies of the Font
51
+ Software, subject to the following conditions:
52
+
53
+ 1) Neither the Font Software nor any of its individual components,
54
+ in Original or Modified Versions, may be sold by itself.
55
+
56
+ 2) Original or Modified Versions of the Font Software may be bundled,
57
+ redistributed and/or sold with any software, provided that each copy
58
+ contains the above copyright notice and this license. These can be
59
+ included either as stand-alone text files, human-readable headers or
60
+ in the appropriate machine-readable metadata fields within text or
61
+ binary files as long as those fields can be easily viewed by the user.
62
+
63
+ 3) No Modified Version of the Font Software may use the Reserved Font
64
+ Name(s) unless explicit written permission is granted by the corresponding
65
+ Copyright Holder. This restriction only applies to the primary font name as
66
+ presented to the users.
67
+
68
+ 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
69
+ Software shall not be used to promote, endorse or advertise any
70
+ Modified Version, except to acknowledge the contribution(s) of the
71
+ Copyright Holder(s) and the Author(s) or with their explicit written
72
+ permission.
73
+
74
+ 5) The Font Software, modified or unmodified, in part or in whole,
75
+ must be distributed entirely under this license, and must not be
76
+ distributed under any other license. The requirement for fonts to
77
+ remain under this license does not apply to any document created
78
+ using the Font Software.
79
+
80
+ TERMINATION
81
+ This license becomes null and void if any of the above conditions are
82
+ not met.
83
+
84
+ DISCLAIMER
85
+ THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
86
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
87
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
88
+ OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
89
+ COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
90
+ INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
91
+ DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
92
+ FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
93
+ OTHER DEALINGS IN THE FONT SOFTWARE.
@@ -0,0 +1,48 @@
1
+ {
2
+ "upstream": {
3
+ "source": "https://github.com/googlefonts/Raleway/tree/7e0be846a3097743b9bf442eb8e65bc1fd96c934/fonts/TTF",
4
+ "commit": "7e0be846a3097743b9bf442eb8e65bc1fd96c934",
5
+ "licenseUrl": "https://raw.githubusercontent.com/googlefonts/Raleway/7e0be846a3097743b9bf442eb8e65bc1fd96c934/OFL.txt"
6
+ },
7
+ "license": "OFL-1.1",
8
+ "licenseFile": "OFL.txt",
9
+ "licenseSha256": "7e946cf1171784d1015279e7dc35f827957a6b5d1f1f659ae0c98e5f5e37ed9b",
10
+ "reservedFontNames": [
11
+ "Raleway"
12
+ ],
13
+ "copyright": "Copyright 2010 The Raleway Project Authors (impallari@gmail.com), with Reserved Font Name \"Raleway\".",
14
+ "faces": [
15
+ {
16
+ "file": "Raleway-Regular.ttf",
17
+ "family": "Raleway",
18
+ "weight": 400,
19
+ "italic": false,
20
+ "sha256": "cb70f8815eecd850263d54fa59286377b1d7362bec612e981155a00039d8b513",
21
+ "upstreamUrl": "https://raw.githubusercontent.com/googlefonts/Raleway/7e0be846a3097743b9bf442eb8e65bc1fd96c934/fonts/TTF/Raleway-Regular.ttf"
22
+ },
23
+ {
24
+ "file": "Raleway-Italic.ttf",
25
+ "family": "Raleway",
26
+ "weight": 400,
27
+ "italic": true,
28
+ "sha256": "63f52b6d8e88afa43d94162147ff55846f28af630d834018de5cb7ccc75a8ee8",
29
+ "upstreamUrl": "https://raw.githubusercontent.com/googlefonts/Raleway/7e0be846a3097743b9bf442eb8e65bc1fd96c934/fonts/TTF/Raleway-Italic.ttf"
30
+ },
31
+ {
32
+ "file": "Raleway-Bold.ttf",
33
+ "family": "Raleway",
34
+ "weight": 700,
35
+ "italic": false,
36
+ "sha256": "6894481ab1c298e9c66f23455ecf05ccb25393d719a6b2d63428358c0409d2d0",
37
+ "upstreamUrl": "https://raw.githubusercontent.com/googlefonts/Raleway/7e0be846a3097743b9bf442eb8e65bc1fd96c934/fonts/TTF/Raleway-Bold.ttf"
38
+ },
39
+ {
40
+ "file": "Raleway-BoldItalic.ttf",
41
+ "family": "Raleway",
42
+ "weight": 700,
43
+ "italic": true,
44
+ "sha256": "57eb1a701136eca30b52a4847edcec93daa53f0b5597dd98dbc0b07f475ce44a",
45
+ "upstreamUrl": "https://raw.githubusercontent.com/googlefonts/Raleway/7e0be846a3097743b9bf442eb8e65bc1fd96c934/fonts/TTF/Raleway-BoldItalic.ttf"
46
+ }
47
+ ]
48
+ }
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openpresentation/opf-render",
3
- "version": "0.11.3",
3
+ "version": "0.11.4",
4
4
  "description": "Deterministic local OPF renderer for SVG, PNG, and PDF output.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -56,9 +56,9 @@
56
56
  "sideEffects": false,
57
57
  "scripts": {
58
58
  "build": "node scripts/build.mjs",
59
- "typecheck": "node --check src/index.js && node --check scripts/build.mjs && node --check scripts/validate-package.mjs && node --check test/smoke.mjs && node --check test/golden.mjs && node --check src/fonts.js && node --check src/script-fonts.js && node --check src/script-font-pack.js && node --check test/script-fonts.mjs && node --check test/glyph-fallback.mjs && node --check test/script-fonts-browser.mjs && node --check test/script-fonts-auto.mjs && node --check test/script-fonts-auto-robust.mjs && node --check test/script-fonts-auto-browser.mjs && node --check src/fonts-node.js && node --check src/font-compatibility.js && node --check test/office-fonts.mjs && node --check test/font-policy.mjs && node --check test/open-fonts.mjs && node --check test/aptos-preview.mjs && node --check test/aptos-preview-browser.mjs && node --check test/lazy-fonts.mjs && node --check test/lazy-fonts-browser.mjs && node --check test/lazy-face-fallback.mjs && node --check test/lazy-face-fallback-browser.mjs && node --check src/font-policy.js && node --check scripts/update-font-policy.mjs && node --check src/fonts-browser.js && node --check test/fonts-browser.mjs && node --check src/svg.js && node --check src/raster.js && node --check src/raster-images.js && node --check test/webp.mjs && node --check test/jpeg-orientation.mjs && node --check scripts/build-browser-check.mjs && node --check test/jpeg-browser.js && node --check test/rich-table.mjs && node --check test/rich-tab.mjs && node --check test/rich-tab-browser.mjs && node --check test/rich-tab-estimated-browser.mjs && node --check test/styled-table.mjs && node --check test/color-references.mjs && node --check test/default-font-scheme.mjs && node --check test/chart-colors.mjs && node --check test/chart-axis.mjs && node --check src/charts.js && node --check test/chart-types.mjs && node --check test/chart-scale.mjs && node --check test/chart-catalog.mjs && node --check test/image-placeholders.mjs && node --check test/image-placeholders-browser.mjs && node --check test/accepted-text.mjs && node --check test/accepted-text-fixtures.mjs && node --check test/accepted-text-browser.mjs && node --check test/rich-spacing-browser.mjs && node --check test/rich-flow-browser.mjs && node --check test/rich-flow-review-sheets.mjs && node --check test/furniture.mjs && node --check test/socials.mjs && node --check test/slide-image.mjs && node --check test/slide-image-treatments.mjs && node --check test/item-alignment.mjs && node --check test/cover-centering.mjs && node --check scripts/font-license.mjs && node --check test/font-licenses.mjs && node --check scripts/check-font-hotlinks.mjs && node --check scripts/check-font-hotlinks.test.mjs",
59
+ "typecheck": "node --check src/index.js && node --check scripts/build.mjs && node --check scripts/validate-package.mjs && node --check test/smoke.mjs && node --check test/golden.mjs && node --check src/fonts.js && node --check src/script-fonts.js && node --check src/script-font-pack.js && node --check test/script-fonts.mjs && node --check test/glyph-fallback.mjs && node --check test/script-fonts-browser.mjs && node --check test/script-fonts-auto.mjs && node --check test/script-fonts-auto-robust.mjs && node --check test/mongolian-shaping.mjs && node --check test/script-fonts-auto-browser.mjs && node --check src/fonts-node.js && node --check src/font-compatibility.js && node --check test/office-fonts.mjs && node --check test/font-policy.mjs && node --check test/open-fonts.mjs && node --check test/aptos-preview.mjs && node --check test/aptos-preview-browser.mjs && node --check test/lazy-fonts.mjs && node --check test/lazy-fonts-browser.mjs && node --check test/lazy-face-fallback.mjs && node --check test/lazy-face-fallback-browser.mjs && node --check src/font-policy.js && node --check scripts/update-font-policy.mjs && node --check src/fonts-browser.js && node --check test/fonts-browser.mjs && node --check src/svg.js && node --check src/raster.js && node --check src/raster-images.js && node --check test/webp.mjs && node --check test/jpeg-orientation.mjs && node --check scripts/build-browser-check.mjs && node --check test/jpeg-browser.js && node --check test/rich-table.mjs && node --check test/rich-tab.mjs && node --check test/rich-tab-browser.mjs && node --check test/rich-tab-estimated-browser.mjs && node --check test/styled-table.mjs && node --check test/color-references.mjs && node --check test/default-font-scheme.mjs && node --check test/chart-colors.mjs && node --check test/chart-axis.mjs && node --check src/charts.js && node --check test/chart-types.mjs && node --check test/chart-scale.mjs && node --check test/chart-catalog.mjs && node --check test/image-placeholders.mjs && node --check test/image-placeholders-browser.mjs && node --check test/accepted-text.mjs && node --check test/accepted-text-fixtures.mjs && node --check test/accepted-text-browser.mjs && node --check test/rich-spacing-browser.mjs && node --check test/rich-flow-browser.mjs && node --check test/rich-flow-review-sheets.mjs && node --check test/furniture.mjs && node --check test/socials.mjs && node --check test/slide-image.mjs && node --check test/slide-image-treatments.mjs && node --check test/item-alignment.mjs && node --check test/cover-centering.mjs && node --check scripts/font-license.mjs && node --check test/font-licenses.mjs && node --check scripts/check-font-hotlinks.mjs && node --check scripts/check-font-hotlinks.test.mjs",
60
60
  "validate": "node scripts/validate-package.mjs",
61
- "test": "npm run build && npm run check:fonts && node test/furniture.mjs && node test/socials.mjs && node test/font-outlines.mjs && node test/accepted-text.mjs && node test/image-placeholders.mjs && node test/slide-image.mjs && node test/slide-image-treatments.mjs && node test/chart-colors.mjs && node test/chart-axis.mjs && node test/chart-types.mjs && node test/chart-scale.mjs && node test/chart-catalog.mjs && node test/smoke.mjs && node test/webp.mjs && node test/jpeg-orientation.mjs && node test/fonts.mjs && node test/office-fonts.mjs && node test/font-policy.mjs && node test/open-fonts.mjs && node test/aptos-preview.mjs && node test/lazy-fonts.mjs && node test/lazy-face-fallback.mjs && node test/timeline.mjs && node test/quote-footer.mjs && node test/shared-quote.mjs && node test/rich-trace.mjs && node test/rich-table.mjs && node test/rich-tab.mjs && node test/color-references.mjs && node test/default-font-scheme.mjs && node test/golden-gate.mjs && node test/golden.mjs && node test/fonts-browser.mjs && node test/script-fonts.mjs && node test/glyph-fallback.mjs && node test/script-fonts-auto.mjs && node test/script-fonts-auto-robust.mjs && node test/design-preview.mjs && node test/item-alignment.mjs && node test/cover-centering.mjs && npm run build:browser-check && npm run test:styled-table",
61
+ "test": "npm run build && npm run check:fonts && node test/furniture.mjs && node test/socials.mjs && node test/font-outlines.mjs && node test/accepted-text.mjs && node test/image-placeholders.mjs && node test/slide-image.mjs && node test/slide-image-treatments.mjs && node test/chart-colors.mjs && node test/chart-axis.mjs && node test/chart-types.mjs && node test/chart-scale.mjs && node test/chart-catalog.mjs && node test/smoke.mjs && node test/webp.mjs && node test/jpeg-orientation.mjs && node test/fonts.mjs && node test/office-fonts.mjs && node test/font-policy.mjs && node test/open-fonts.mjs && node test/aptos-preview.mjs && node test/lazy-fonts.mjs && node test/lazy-face-fallback.mjs && node test/timeline.mjs && node test/quote-footer.mjs && node test/shared-quote.mjs && node test/rich-trace.mjs && node test/rich-table.mjs && node test/rich-tab.mjs && node test/color-references.mjs && node test/default-font-scheme.mjs && node test/golden-gate.mjs && node test/golden.mjs && node test/fonts-browser.mjs && node test/script-fonts.mjs && node test/glyph-fallback.mjs && node test/script-fonts-auto.mjs && node test/script-fonts-auto-robust.mjs && node test/mongolian-shaping.mjs && node test/design-preview.mjs && node test/item-alignment.mjs && node test/cover-centering.mjs && npm run build:browser-check && npm run test:styled-table",
62
62
  "check:fonts": "node test/font-licenses.mjs && npm run check:font-hotlinks",
63
63
  "check:font-hotlinks": "node --test scripts/check-font-hotlinks.test.mjs && node scripts/check-font-hotlinks.mjs",
64
64
  "test:golden": "npm run build && node test/golden.mjs",