quicklook-pptx-renderer 0.3.2 → 0.3.3

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
@@ -167,14 +167,14 @@ for (const issue of result.issues) {
167
167
  break;
168
168
 
169
169
  case "strip-embedded-fonts":
170
- // issue.fix.fonts = [{ name: "Montserrat", replacement: "Nunito Sans" }]
170
+ // issue.fix.fonts = [{ name: "Montserrat", replacement: "DIN Alternate" }]
171
171
  // Strip embedded font data and replace with cross-platform alternatives
172
172
  break;
173
173
  }
174
174
  }
175
175
  ```
176
176
 
177
- Also available: `SUPPORTED_GEOMETRIES` (Set of 60 presets OfficeImport renders), `FONT_SUBSTITUTIONS` (Windows → macOS font map), `GEOMETRY_FALLBACKS` (unsupported → closest supported preset), `findClosestFont()` (metric-based font matching), `widthDelta()` (percentage width difference between fonts).
177
+ Also available: `SUPPORTED_GEOMETRIES` (Set of 60 presets OfficeImport renders), `FONT_SUBSTITUTIONS` (Windows → macOS font map), `GEOMETRY_FALLBACKS` (unsupported → closest supported preset), `findClosestFont()` (metric-based font matching with narrower-preference), `widthDelta()` (percentage width difference between fonts), `APPLE_SYSTEM_FONTS` (29 fonts preinstalled on both macOS and iOS), `MACOS_SYSTEM_FONTS` (38 fonts on macOS).
178
178
 
179
179
  ---
180
180
 
@@ -304,7 +304,7 @@ Plus CJK mappings: MS Gothic → Hiragino Sans, SimSun → STSong, Microsoft YaH
304
304
 
305
305
  ### Metric-Compatible Google Fonts (Croscore)
306
306
 
307
- These open-source fonts (SIL OFL) were designed as drop-in replacements for Microsoft core fonts. Width deltas measured from OS/2 tables and included in the font metrics database (135 fonts total):
307
+ These open-source fonts (SIL OFL) were designed as drop-in replacements for Microsoft core fonts. Width deltas measured from OS/2 tables and included in the font metrics database (106 fonts total):
308
308
 
309
309
  | Microsoft Font | Google Font Replacement | Width Δ vs Original | License |
310
310
  |---|---|---|---|
@@ -325,7 +325,14 @@ Verified by testing against actual OfficeImport output:
325
325
  - **Font stacks / fallback chains**: OOXML supports only one `typeface` per text run. No CSS-like `font-family: "Carlito", "Calibri", sans-serif` — it's a single value.
326
326
  - **Parent font inheritance as fallback**: OOXML property inheritance only applies when the property is **missing**, not when the font isn't installed. If a run specifies `typeface="Carlito"`, the parent's `typeface="Arial"` is not used as a fallback.
327
327
 
328
- **The only way to control fonts in QuickLook is to use a font that macOS has installed.** The `FONT_SUBSTITUTIONS` map and `findClosestFont()` API provide the data to pick the best replacement.
328
+ **The only way to control fonts in QuickLook is to use a font that macOS has installed.** The `FONT_SUBSTITUTIONS` map and `findClosestFont()` API provide the data to pick the best replacement. Use `APPLE_SYSTEM_FONT_LIST` as candidates to ensure results work on both macOS and iOS:
329
+
330
+ ```typescript
331
+ import { findClosestFont, APPLE_SYSTEM_FONT_LIST } from "quicklook-pptx-renderer";
332
+
333
+ // Montserrat → DIN Alternate (-4.3% width) — narrower is preferred over wider
334
+ const [best] = findClosestFont("Montserrat", { candidates: APPLE_SYSTEM_FONT_LIST });
335
+ ```
329
336
 
330
337
  ---
331
338
 
package/dist/index.d.ts CHANGED
@@ -26,6 +26,6 @@ export { readPresentation } from "./reader/presentation.js";
26
26
  export { generateHtml, type MapperOptions, type HtmlOutput } from "./mapper/html-generator.js";
27
27
  export { resolveColor } from "./resolve/color-resolver.js";
28
28
  export { resolveFontFamily, FONT_SUBSTITUTIONS } from "./resolve/font-map.js";
29
- export { FONT_METRICS, findClosestFont, widthDelta, type FontMetrics, type FontMatch, type FontCategory } from "./resolve/font-metrics.js";
29
+ export { FONT_METRICS, findClosestFont, widthDelta, APPLE_SYSTEM_FONTS, APPLE_SYSTEM_FONT_LIST, MACOS_SYSTEM_FONTS, MACOS_SYSTEM_FONT_LIST, type FontMetrics, type FontMatch, type FontCategory, } from "./resolve/font-metrics.js";
30
30
  export { SUPPORTED_GEOMETRIES } from "./mapper/shape-mapper.js";
31
31
  export type { Presentation, Slide, SlideLayout, SlideMaster, Shape, Picture, Group, Connector, GraphicFrame, TextBody, Paragraph, TextRun, CharacterProperties, ParagraphProperties, Color, Fill, Stroke, Effect, Theme, Drawable, DrawableBase, OrientedBounds, } from "./model/types.js";
package/dist/index.js CHANGED
@@ -70,5 +70,5 @@ export { readPresentation } from "./reader/presentation.js";
70
70
  export { generateHtml } from "./mapper/html-generator.js";
71
71
  export { resolveColor } from "./resolve/color-resolver.js";
72
72
  export { resolveFontFamily, FONT_SUBSTITUTIONS } from "./resolve/font-map.js";
73
- export { FONT_METRICS, findClosestFont, widthDelta } from "./resolve/font-metrics.js";
73
+ export { FONT_METRICS, findClosestFont, widthDelta, APPLE_SYSTEM_FONTS, APPLE_SYSTEM_FONT_LIST, MACOS_SYSTEM_FONTS, MACOS_SYSTEM_FONT_LIST, } from "./resolve/font-metrics.js";
74
74
  export { SUPPORTED_GEOMETRIES } from "./mapper/shape-mapper.js";
package/dist/lint.js CHANGED
@@ -12,7 +12,7 @@ import { PptxPackage } from "./package/package.js";
12
12
  import { readPresentation } from "./reader/presentation.js";
13
13
  import { SUPPORTED_GEOMETRIES } from "./mapper/shape-mapper.js";
14
14
  import { FONT_SUBSTITUTIONS } from "./resolve/font-map.js";
15
- import { FONT_METRICS, widthDelta, findClosestFont } from "./resolve/font-metrics.js";
15
+ import { FONT_METRICS, widthDelta, findClosestFont, APPLE_SYSTEM_FONT_LIST } from "./resolve/font-metrics.js";
16
16
  import { resolveColor } from "./resolve/color-resolver.js";
17
17
  // ── Geometry fallback map (unsupported → closest supported) ────────
18
18
  /** Maps commonly-used unsupported OOXML presets to visually-closest supported alternative. */
@@ -111,7 +111,7 @@ export async function lint(pptxBuffer) {
111
111
  const embFontLst = presNode.embeddedFontLst?.embeddedFont;
112
112
  if (embFontLst) {
113
113
  const entries = Array.isArray(embFontLst) ? embFontLst : [embFontLst];
114
- const safeCandidates = Object.keys(FONT_METRICS).filter(f => !FONT_SUBSTITUTIONS[f]);
114
+ const safeCandidates = APPLE_SYSTEM_FONT_LIST;
115
115
  const fonts = [];
116
116
  for (const entry of entries) {
117
117
  const name = entry.font?.["@_typeface"];
@@ -404,7 +404,7 @@ function lintTextBody(body, slide, element, issues) {
404
404
  // Find metrically-closest alternatives (consumer decides which to use)
405
405
  const alternatives = [];
406
406
  if (FONT_METRICS[font]) {
407
- const matches = findClosestFont(font, { limit: 5 });
407
+ const matches = findClosestFont(font, { candidates: APPLE_SYSTEM_FONT_LIST, limit: 5 });
408
408
  for (const m of matches) {
409
409
  alternatives.push({ font: m.font, widthDelta: m.widthDelta });
410
410
  }
@@ -35,6 +35,22 @@ export interface FontMetrics {
35
35
  * @capsizecss/metrics (capsize.dev), which is approximate.
36
36
  */
37
37
  export declare const FONT_METRICS: Record<string, FontMetrics>;
38
+ /**
39
+ * Fonts preinstalled on BOTH macOS and iOS.
40
+ * Only includes fonts that have entries in FONT_METRICS above.
41
+ * Use as `candidates` in findClosestFont() to ensure the result is a real system font.
42
+ */
43
+ export declare const APPLE_SYSTEM_FONTS: ReadonlySet<string>;
44
+ /** Array form for passing to findClosestFont({ candidates }) */
45
+ export declare const APPLE_SYSTEM_FONT_LIST: readonly string[];
46
+ /**
47
+ * macOS-only system fonts (not on iOS).
48
+ * Combined with APPLE_SYSTEM_FONTS for macOS-specific matching.
49
+ */
50
+ export declare const MACOS_ONLY_FONTS: ReadonlySet<string>;
51
+ /** All macOS system fonts (APPLE_SYSTEM_FONTS + MACOS_ONLY_FONTS) */
52
+ export declare const MACOS_SYSTEM_FONTS: ReadonlySet<string>;
53
+ export declare const MACOS_SYSTEM_FONT_LIST: readonly string[];
38
54
  /** Width delta (%) between two fonts. Positive = target is wider. */
39
55
  export declare function widthDelta(source: FontMetrics, target: FontMetrics): number;
40
56
  export interface FontMatch {
@@ -54,7 +70,7 @@ export interface FontMatch {
54
70
  * @param options.limit - Max results to return (default: 3)
55
71
  */
56
72
  export declare function findClosestFont(source: string | FontMetrics, options?: {
57
- candidates?: string[];
73
+ candidates?: readonly string[];
58
74
  sameCategory?: boolean;
59
75
  limit?: number;
60
76
  }): FontMatch[];
@@ -58,7 +58,26 @@ export const FONT_METRICS = {
58
58
  "Avenir Next": { cat: "sans-serif", upm: 1000, xWidthAvg: 624, capH: 708, xH: 498, asc: 1000, desc: -366, lineGap: 0 },
59
59
  "Avenir Next Medium": { cat: "sans-serif", upm: 1000, xWidthAvg: 630, capH: 708, xH: 498, asc: 1000, desc: -366, lineGap: 0 },
60
60
  "Geneva": { cat: "sans-serif", upm: 2048, xWidthAvg: 1225, capH: 1552, xH: 1117, asc: 2048, desc: -512, lineGap: 171 },
61
- "Impact": { cat: "sans-serif", upm: 2048, xWidthAvg: 989, capH: 1620, xH: 1327, asc: 2066, desc: -432, lineGap: 0 },
61
+ "Impact": { cat: "display", upm: 2048, xWidthAvg: 989, capH: 1620, xH: 1327, asc: 2066, desc: -432, lineGap: 0 },
62
+ "Gill Sans": { cat: "sans-serif", upm: 2048, xWidthAvg: 1082, capH: 1407, xH: 921, asc: 1415, desc: -471, lineGap: 305 },
63
+ "Optima": { cat: "sans-serif", upm: 1000, xWidthAvg: 432, capH: 700, xH: 500, asc: 919, desc: -271, lineGap: 25 },
64
+ "Galvji": { cat: "sans-serif", upm: 2048, xWidthAvg: 1279, capH: 1443, xH: 1078, asc: 1948, desc: -426, lineGap: 0 },
65
+ "DIN Alternate": { cat: "sans-serif", upm: 2048, xWidthAvg: 986, capH: 1470, xH: 1470, asc: 1585, desc: -421, lineGap: 336 },
66
+ "DIN Condensed": { cat: "sans-serif", upm: 1000, xWidthAvg: 374, capH: 712, xH: 507, asc: 712, desc: -288, lineGap: 200 },
67
+ "Copperplate": { cat: "display", upm: 1000, xWidthAvg: 661, capH: 550, xH: 440, asc: 763, desc: -250, lineGap: 20 },
68
+ "Baskerville": { cat: "serif", upm: 2048, xWidthAvg: 1185, capH: 1370, xH: 819, asc: 1839, desc: -504, lineGap: 0 },
69
+ "Didot": { cat: "serif", upm: 1000, xWidthAvg: 549, capH: 721, xH: 436, asc: 941, desc: -299, lineGap: 26 },
70
+ "Cochin": { cat: "serif", upm: 1000, xWidthAvg: 583, capH: 658, xH: 372, asc: 712, desc: -250, lineGap: 72 },
71
+ "Big Caslon": { cat: "serif", upm: 1000, xWidthAvg: 540, capH: 725, xH: 478, asc: 726, desc: -252, lineGap: 18 },
72
+ "Hoefler Text": { cat: "serif", upm: 2000, xWidthAvg: 831, capH: 1379, xH: 850, asc: 1864, desc: -636, lineGap: 0 },
73
+ "Bodoni 72": { cat: "serif", upm: 1000, xWidthAvg: 367, capH: 702, xH: 439, asc: 936, desc: -266, lineGap: 0 },
74
+ "American Typewriter": { cat: "serif", upm: 1000, xWidthAvg: 479, capH: 671, xH: 501, asc: 691, desc: -180, lineGap: 199 },
75
+ "Rockwell": { cat: "serif", upm: 2048, xWidthAvg: 1285, capH: 1507, xH: 1051, asc: 1548, desc: -498, lineGap: 145 },
76
+ "Charter": { cat: "serif", upm: 2048, xWidthAvg: 898, capH: 1400, xH: 950, asc: 2007, desc: -492, lineGap: 0 },
77
+ "Monaco": { cat: "monospace", upm: 2048, xWidthAvg: 1228, capH: 1552, xH: 1117, asc: 2048, desc: -512, lineGap: 171 },
78
+ "PT Sans": { cat: "sans-serif", upm: 1000, xWidthAvg: 431, capH: 700, xH: 500, asc: 1018, desc: -276, lineGap: 0 },
79
+ "PT Serif": { cat: "serif", upm: 1000, xWidthAvg: 448, capH: 700, xH: 500, asc: 1039, desc: -286, lineGap: 0 },
80
+ "PT Mono": { cat: "monospace", upm: 1000, xWidthAvg: 500, capH: 700, xH: 500, asc: 1039, desc: -286, lineGap: 0 },
62
81
  // ── Google Fonts (from @capsizecss/metrics — raw glyph widths) ────
63
82
  "Roboto": { cat: "sans-serif", upm: 2048, xWidthAvg: 911, capH: 1456, xH: 1082, asc: 1900, desc: -500, lineGap: 0 },
64
83
  "Open Sans": { cat: "sans-serif", upm: 2048, xWidthAvg: 960, capH: 1462, xH: 1096, asc: 2189, desc: -600, lineGap: 0 },
@@ -80,8 +99,6 @@ export const FONT_METRICS = {
80
99
  "Noto Sans": { cat: "sans-serif", upm: 1000, xWidthAvg: 474, capH: 714, xH: 536, asc: 1069, desc: -293, lineGap: 0 },
81
100
  "Noto Serif": { cat: "serif", upm: 1000, xWidthAvg: 481, capH: 714, xH: 536, asc: 1069, desc: -293, lineGap: 0 },
82
101
  "Noto Sans Mono": { cat: "monospace", upm: 1000, xWidthAvg: 600, capH: 714, xH: 536, asc: 1069, desc: -293, lineGap: 0 },
83
- "PT Sans": { cat: "sans-serif", upm: 1000, xWidthAvg: 431, capH: 700, xH: 500, asc: 1018, desc: -276, lineGap: 0 },
84
- "PT Serif": { cat: "serif", upm: 1000, xWidthAvg: 448, capH: 700, xH: 500, asc: 1039, desc: -286, lineGap: 0 },
85
102
  "Ubuntu": { cat: "sans-serif", upm: 1000, xWidthAvg: 455, capH: 693, xH: 520, asc: 932, desc: -189, lineGap: 28 },
86
103
  "Ubuntu Mono": { cat: "monospace", upm: 1000, xWidthAvg: 500, capH: 693, xH: 520, asc: 830, desc: -170, lineGap: 0 },
87
104
  // ── Croscore — metric-compatible Google Fonts (OS/2 table) ────────
@@ -111,6 +128,41 @@ export const FONT_METRICS = {
111
128
  "EB Garamond": { cat: "serif", upm: 1000, xWidthAvg: 528, capH: 650, xH: 400, asc: 1007, desc: -298, lineGap: 0 },
112
129
  "Zilla Slab": { cat: "serif", upm: 1000, xWidthAvg: 538, capH: 650, xH: 445, asc: 787, desc: -213, lineGap: 200 },
113
130
  };
131
+ // ── Platform font sets ──────────────────────────────────────────────
132
+ /**
133
+ * Fonts preinstalled on BOTH macOS and iOS.
134
+ * Only includes fonts that have entries in FONT_METRICS above.
135
+ * Use as `candidates` in findClosestFont() to ensure the result is a real system font.
136
+ */
137
+ export const APPLE_SYSTEM_FONTS = new Set([
138
+ // Sans-serif
139
+ "Arial", "Helvetica", "Helvetica Neue", "Helvetica Neue Light", "Helvetica Neue Medium",
140
+ "Avenir", "Avenir Next", "Avenir Next Medium",
141
+ "Futura", "Gill Sans", "Optima", "Galvji",
142
+ "DIN Alternate", "DIN Condensed",
143
+ "Verdana", "Trebuchet MS",
144
+ // Serif
145
+ "Georgia", "Times New Roman", "Palatino",
146
+ "Baskerville", "Didot", "Cochin", "Hoefler Text",
147
+ "Bodoni 72", "American Typewriter", "Rockwell", "Charter",
148
+ // Monospace
149
+ "Menlo", "Courier New",
150
+ ]);
151
+ /** Array form for passing to findClosestFont({ candidates }) */
152
+ export const APPLE_SYSTEM_FONT_LIST = [...APPLE_SYSTEM_FONTS];
153
+ /**
154
+ * macOS-only system fonts (not on iOS).
155
+ * Combined with APPLE_SYSTEM_FONTS for macOS-specific matching.
156
+ */
157
+ export const MACOS_ONLY_FONTS = new Set([
158
+ "PT Sans", "PT Serif", "PT Mono",
159
+ "Tahoma", "Geneva", "Monaco",
160
+ "Times", "Courier",
161
+ "Big Caslon",
162
+ ]);
163
+ /** All macOS system fonts (APPLE_SYSTEM_FONTS + MACOS_ONLY_FONTS) */
164
+ export const MACOS_SYSTEM_FONTS = new Set([...APPLE_SYSTEM_FONTS, ...MACOS_ONLY_FONTS]);
165
+ export const MACOS_SYSTEM_FONT_LIST = [...MACOS_SYSTEM_FONTS];
114
166
  // ── Similarity matching ─────────────────────────────────────────────
115
167
  /** Normalize a metric by unitsPerEm so fonts at different scales are comparable. */
116
168
  function norm(m) {
@@ -124,12 +176,14 @@ function norm(m) {
124
176
  }
125
177
  /**
126
178
  * Compute similarity score between two fonts. Lower = more similar.
127
- * Weights character width (60%) above vertical metrics (40%) because
128
- * width determines text reflow / overflow in bounded text boxes.
179
+ * Width cost is asymmetric: wider substitutes are penalised 3× more than
180
+ * narrower ones, because wider causes text overflow while narrower just
181
+ * leaves a small gap.
129
182
  */
130
183
  function similarity(a, b) {
131
184
  const na = norm(a), nb = norm(b);
132
- const wd = Math.abs(na.w - nb.w) / (na.w || 1);
185
+ const rawWd = (nb.w - na.w) / (na.w || 1); // positive = wider
186
+ const wd = rawWd > 0 ? rawWd * 3 : Math.abs(rawWd); // 3× penalty for wider
133
187
  const lh = Math.abs(na.lh - nb.lh) / (na.lh || 1);
134
188
  // capH/xH might be 0 for some fonts — only include if both have data
135
189
  let vertScore = 0, vertWeight = 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "quicklook-pptx-renderer",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
4
4
  "description": "Open-source PPTX rendering engine that replicates Apple's macOS QuickLook and iOS preview — pixel for pixel. Test how PowerPoint files look on Mac/iPhone without a Mac.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",