quicklook-pptx-renderer 0.2.3 → 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/README.md +103 -15
- package/dist/index.d.ts +3 -2
- package/dist/index.js +3 -2
- package/dist/lint.d.ts +42 -1
- package/dist/lint.js +167 -58
- package/dist/resolve/font-map.d.ts +1 -0
- package/dist/resolve/font-map.js +1 -1
- package/dist/resolve/font-metrics.js +26 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -114,24 +114,85 @@ for (const issue of result.issues) {
|
|
|
114
114
|
}
|
|
115
115
|
```
|
|
116
116
|
|
|
117
|
+
### Building Fix Tools
|
|
118
|
+
|
|
119
|
+
The lint API is designed to power automated PPTX fixers. Each issue's `fix` object tells you exactly what to change — no re-detection needed.
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
import { lint, SUPPORTED_GEOMETRIES, FONT_SUBSTITUTIONS, GEOMETRY_FALLBACKS } from "quicklook-pptx-renderer";
|
|
123
|
+
|
|
124
|
+
const result = await lint(pptxBuffer);
|
|
125
|
+
|
|
126
|
+
for (const issue of result.issues) {
|
|
127
|
+
if (!issue.fix) continue; // informational only
|
|
128
|
+
|
|
129
|
+
switch (issue.fix.action) {
|
|
130
|
+
case "replace-geometry":
|
|
131
|
+
// issue.fix.current = "heart", issue.fix.suggested = "ellipse"
|
|
132
|
+
// Replace <a:prstGeom prst="heart"> with <a:prstGeom prst="ellipse">
|
|
133
|
+
break;
|
|
134
|
+
|
|
135
|
+
case "strip-effects":
|
|
136
|
+
// Remove <a:effectLst> and <a:effectDag> from the shape
|
|
137
|
+
break;
|
|
138
|
+
|
|
139
|
+
case "reduce-stops":
|
|
140
|
+
// issue.fix.firstColor = "#FF0000", issue.fix.lastColor = "#0000FF"
|
|
141
|
+
// issue.fix.averageColor = "#7F007F", issue.fix.angle = 5400000
|
|
142
|
+
// Replace 3+ gradient stops with 2-stop using first/last colors
|
|
143
|
+
break;
|
|
144
|
+
|
|
145
|
+
case "replace-font":
|
|
146
|
+
// issue.fix.from = "Calibri", issue.fix.macTarget = "Helvetica Neue"
|
|
147
|
+
// issue.fix.widthDelta = 14.4, issue.fix.alternatives = [
|
|
148
|
+
// { font: "Carlito", widthDelta: 2.1 },
|
|
149
|
+
// { font: "Montserrat", widthDelta: 0.4 }
|
|
150
|
+
// ]
|
|
151
|
+
break;
|
|
152
|
+
|
|
153
|
+
case "inline-borders":
|
|
154
|
+
// issue.fix.tableStyleId = "{5C22544A-...}"
|
|
155
|
+
// Resolve table style and write explicit borders on each cell
|
|
156
|
+
break;
|
|
157
|
+
|
|
158
|
+
case "add-borders":
|
|
159
|
+
// Add default 1px solid black borders to all cells
|
|
160
|
+
break;
|
|
161
|
+
|
|
162
|
+
case "add-fallback-image":
|
|
163
|
+
// Generate or embed a chart fallback image via mc:AlternateContent
|
|
164
|
+
break;
|
|
165
|
+
|
|
166
|
+
case "ungroup":
|
|
167
|
+
// issue.fix.childCount = 5, issue.fix.containsPictures = false
|
|
168
|
+
// Promote group children to slide-level drawables
|
|
169
|
+
break;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
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).
|
|
175
|
+
|
|
117
176
|
---
|
|
118
177
|
|
|
119
178
|
## Lint Rules
|
|
120
179
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
|
124
|
-
|
|
125
|
-
| `
|
|
126
|
-
| `
|
|
127
|
-
| `
|
|
128
|
-
| `
|
|
129
|
-
| `
|
|
130
|
-
| `
|
|
131
|
-
| `
|
|
132
|
-
| `
|
|
133
|
-
| `
|
|
134
|
-
| `
|
|
180
|
+
Every issue carries a typed `fix` object with machine-readable remediation data — downstream tools can apply transforms without re-detecting the problem.
|
|
181
|
+
|
|
182
|
+
| Rule | Severity | What It Catches | `fix.action` |
|
|
183
|
+
|------|----------|----------------|-------------|
|
|
184
|
+
| `unsupported-geometry` | error | Shape preset not in OfficeImport's ~60 supported geometries — will be invisible | `replace-geometry` (includes closest supported preset) |
|
|
185
|
+
| `chart-no-fallback` | error | Chart without fallback image — renders as blank rectangle | `add-fallback-image` |
|
|
186
|
+
| `opaque-pdf-block` | warn | Shape with effects rendered as opaque PDF covering content behind it | `strip-effects` |
|
|
187
|
+
| `gradient-flattened` | warn | 3+ gradient stops collapsed to single average color | `reduce-stops` (includes computed 2-stop colors + average) |
|
|
188
|
+
| `table-missing-borders` | warn | Table cells lack explicit borders — will appear borderless | `add-borders` |
|
|
189
|
+
| `table-style-unresolved` | warn | Table style reference that OfficeImport won't resolve | `inline-borders` (includes `tableStyleId`) |
|
|
190
|
+
| `font-substitution` | warn/info | Font will be substituted on macOS — includes width delta (warn if ≥10% reflow risk) | `replace-font` (includes macOS target, delta, metrically-closest alternatives) |
|
|
191
|
+
| `group-as-pdf` | warn | Group rendered as single opaque PDF image | `ungroup` (includes child count, whether group contains pictures) |
|
|
192
|
+
| `geometry-forces-pdf` | info | Non-rect geometry renders as PDF — opaque background may cover adjacent content | — |
|
|
193
|
+
| `rotation-forces-pdf` | info | Rotated rect renders as PDF — enlarged bounding box may cover adjacent content | — |
|
|
194
|
+
| `text-inscription-shift` | info | Text in non-rect shape uses geometry-inscribed bounds (text may shift) | `replace-geometry` (suggests `rect`) |
|
|
195
|
+
| `vertical-text` | info | Vertical text uses CSS writing-mode | — |
|
|
135
196
|
|
|
136
197
|
---
|
|
137
198
|
|
|
@@ -234,10 +295,35 @@ macOS replaces Windows fonts at render time (from `TCFontUtils` in OfficeImport)
|
|
|
234
295
|
|
|
235
296
|
**Highest reflow risk**: Calibri Light (+20.9%), Arial Narrow (+20.0%), Corbel (+18.8%), Franklin Gothic Medium (+17.8%), Arial Black (-15.9%), Calibri (+14.4%), Segoe UI (+14.0%), Tahoma (+10.0%).
|
|
236
297
|
|
|
237
|
-
The linter
|
|
298
|
+
The linter reports width deltas, upgrades font substitution to `warn` severity when the delta exceeds ±10%, and includes metrically-closest cross-platform alternatives in the `fix` data.
|
|
238
299
|
|
|
239
300
|
Plus CJK mappings: MS Gothic → Hiragino Sans, SimSun → STSong, Microsoft YaHei → PingFang SC, Malgun Gothic → Apple SD Gothic Neo, and more.
|
|
240
301
|
|
|
302
|
+
### Metric-Compatible Google Fonts (Croscore)
|
|
303
|
+
|
|
304
|
+
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):
|
|
305
|
+
|
|
306
|
+
| Microsoft Font | Google Font Replacement | Width Δ vs Original | License |
|
|
307
|
+
|---|---|---|---|
|
|
308
|
+
| Calibri | **Carlito** | -2.1% | OFL |
|
|
309
|
+
| Cambria | **Caladea** | -5.6% | OFL |
|
|
310
|
+
| Arial | **Arimo** | -1.2% | Apache 2.0 |
|
|
311
|
+
| Times New Roman | **Tinos** | +0.5% | Apache 2.0 |
|
|
312
|
+
| Courier New | **Cousine** | 0.0% | Apache 2.0 |
|
|
313
|
+
|
|
314
|
+
Compare: Calibri → Helvetica Neue (the QL substitution) is **+14.4%**. Carlito at -2.1% is 7x better.
|
|
315
|
+
|
|
316
|
+
### What Doesn't Work for Font Fixes
|
|
317
|
+
|
|
318
|
+
Verified by testing against actual OfficeImport output:
|
|
319
|
+
|
|
320
|
+
- **Embedded fonts (`<p:embeddedFont>`)**: OfficeImport ignores them completely. No `OADEmbeddedFont` class exists in the framework. Embedding helps PowerPoint and LibreOffice, but not QuickLook.
|
|
321
|
+
- **`pitchFamily` attribute**: OfficeImport ignores it. All unknown fonts produce `font-family:"FontName"` in CSS with no generic family fallback. WebKit then falls back to **serif (Times)** regardless of the pitchFamily hint.
|
|
322
|
+
- **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.
|
|
323
|
+
- **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.
|
|
324
|
+
|
|
325
|
+
**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.
|
|
326
|
+
|
|
241
327
|
---
|
|
242
328
|
|
|
243
329
|
## Key Discoveries
|
|
@@ -252,6 +338,8 @@ These findings come from reverse engineering OfficeImport via Objective-C runtim
|
|
|
252
338
|
6. **3+ gradient stops → average color** (not rendered as gradient)
|
|
253
339
|
7. **Font substitution**: Calibri → Helvetica Neue, Arial → Helvetica, etc. (full map above)
|
|
254
340
|
8. **Text inscription for non-rect shapes** uses `float` radius and `trunc()` in pixel space
|
|
341
|
+
9. **Embedded fonts are ignored** — no `OADEmbeddedFont` class; `TCImportFontCache` only does CoreText system font lookup
|
|
342
|
+
10. **`pitchFamily` is ignored** — all unknown fonts get bare `font-family:"Name"` in CSS with no generic family fallback, regardless of pitchFamily value
|
|
255
343
|
|
|
256
344
|
---
|
|
257
345
|
|
package/dist/index.d.ts
CHANGED
|
@@ -20,11 +20,12 @@ export interface RenderResult {
|
|
|
20
20
|
* embedded CSS — ready to open in WebKit for pixel-perfect rendering.
|
|
21
21
|
*/
|
|
22
22
|
export declare function render(pptxBuffer: Buffer): Promise<RenderResult>;
|
|
23
|
-
export { lint, formatIssues, type LintResult, type LintIssue, type Severity, type RuleId } from "./lint.js";
|
|
23
|
+
export { lint, formatIssues, GEOMETRY_FALLBACKS, type LintResult, type LintIssue, type LintFix, type Severity, type RuleId } from "./lint.js";
|
|
24
24
|
export { PptxPackage } from "./package/package.js";
|
|
25
25
|
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
|
-
export { resolveFontFamily } from "./resolve/font-map.js";
|
|
28
|
+
export { resolveFontFamily, FONT_SUBSTITUTIONS } from "./resolve/font-map.js";
|
|
29
29
|
export { FONT_METRICS, findClosestFont, widthDelta, type FontMetrics, type FontMatch, type FontCategory } from "./resolve/font-metrics.js";
|
|
30
|
+
export { SUPPORTED_GEOMETRIES } from "./mapper/shape-mapper.js";
|
|
30
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
|
@@ -63,11 +63,12 @@ export async function render(pptxBuffer) {
|
|
|
63
63
|
return { slides, fullHtml: html, attachments };
|
|
64
64
|
}
|
|
65
65
|
// ── Linter (QuickLook compatibility checks) ─────────────────────
|
|
66
|
-
export { lint, formatIssues } from "./lint.js";
|
|
66
|
+
export { lint, formatIssues, GEOMETRY_FALLBACKS } from "./lint.js";
|
|
67
67
|
// ── Building blocks ─────────────────────────────────────────────
|
|
68
68
|
export { PptxPackage } from "./package/package.js";
|
|
69
69
|
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
|
-
export { resolveFontFamily } from "./resolve/font-map.js";
|
|
72
|
+
export { resolveFontFamily, FONT_SUBSTITUTIONS } from "./resolve/font-map.js";
|
|
73
73
|
export { FONT_METRICS, findClosestFont, widthDelta } from "./resolve/font-metrics.js";
|
|
74
|
+
export { SUPPORTED_GEOMETRIES } from "./mapper/shape-mapper.js";
|
package/dist/lint.d.ts
CHANGED
|
@@ -3,9 +3,47 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Parses a PPTX and checks every shape, text run, table, and fill against
|
|
5
5
|
* known OfficeImport quirks. No rendering required — just parse and check.
|
|
6
|
+
*
|
|
7
|
+
* Each issue carries a typed `fix` object with machine-readable remediation
|
|
8
|
+
* data so downstream tools (like pptx-fix) can apply transforms without
|
|
9
|
+
* re-detecting the problem.
|
|
6
10
|
*/
|
|
7
11
|
export type Severity = "error" | "warn" | "info";
|
|
8
|
-
export type RuleId = "unsupported-geometry" | "opaque-pdf-block" | "gradient-flattened" | "font-substitution" | "table-missing-borders" | "table-style-unresolved" | "group-as-pdf" | "chart-no-fallback" | "text-inscription-shift" | "rotation-forces-pdf" | "
|
|
12
|
+
export type RuleId = "unsupported-geometry" | "opaque-pdf-block" | "gradient-flattened" | "font-substitution" | "table-missing-borders" | "table-style-unresolved" | "group-as-pdf" | "chart-no-fallback" | "text-inscription-shift" | "rotation-forces-pdf" | "geometry-forces-pdf" | "vertical-text";
|
|
13
|
+
/** Machine-readable remediation data — one variant per fix strategy. */
|
|
14
|
+
export type LintFix = {
|
|
15
|
+
action: "replace-geometry";
|
|
16
|
+
current: string;
|
|
17
|
+
suggested: string;
|
|
18
|
+
} | {
|
|
19
|
+
action: "strip-effects";
|
|
20
|
+
} | {
|
|
21
|
+
action: "reduce-stops";
|
|
22
|
+
firstColor: string;
|
|
23
|
+
lastColor: string;
|
|
24
|
+
averageColor: string;
|
|
25
|
+
angle?: number;
|
|
26
|
+
} | {
|
|
27
|
+
action: "replace-font";
|
|
28
|
+
from: string;
|
|
29
|
+
macTarget: string;
|
|
30
|
+
widthDelta: number;
|
|
31
|
+
alternatives: Array<{
|
|
32
|
+
font: string;
|
|
33
|
+
widthDelta: number;
|
|
34
|
+
}>;
|
|
35
|
+
} | {
|
|
36
|
+
action: "inline-borders";
|
|
37
|
+
tableStyleId: string;
|
|
38
|
+
} | {
|
|
39
|
+
action: "add-borders";
|
|
40
|
+
} | {
|
|
41
|
+
action: "add-fallback-image";
|
|
42
|
+
} | {
|
|
43
|
+
action: "ungroup";
|
|
44
|
+
childCount: number;
|
|
45
|
+
containsPictures: boolean;
|
|
46
|
+
};
|
|
9
47
|
export interface LintIssue {
|
|
10
48
|
rule: RuleId;
|
|
11
49
|
severity: Severity;
|
|
@@ -13,6 +51,7 @@ export interface LintIssue {
|
|
|
13
51
|
element?: string;
|
|
14
52
|
message: string;
|
|
15
53
|
suggestion?: string;
|
|
54
|
+
fix?: LintFix;
|
|
16
55
|
}
|
|
17
56
|
export interface LintResult {
|
|
18
57
|
issues: LintIssue[];
|
|
@@ -23,5 +62,7 @@ export interface LintResult {
|
|
|
23
62
|
slides: number;
|
|
24
63
|
};
|
|
25
64
|
}
|
|
65
|
+
/** Maps commonly-used unsupported OOXML presets to visually-closest supported alternative. */
|
|
66
|
+
export declare const GEOMETRY_FALLBACKS: Record<string, string>;
|
|
26
67
|
export declare function lint(pptxBuffer: Buffer): Promise<LintResult>;
|
|
27
68
|
export declare function formatIssues(result: LintResult): string;
|
package/dist/lint.js
CHANGED
|
@@ -3,35 +3,95 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Parses a PPTX and checks every shape, text run, table, and fill against
|
|
5
5
|
* known OfficeImport quirks. No rendering required — just parse and check.
|
|
6
|
+
*
|
|
7
|
+
* Each issue carries a typed `fix` object with machine-readable remediation
|
|
8
|
+
* data so downstream tools (like pptx-fix) can apply transforms without
|
|
9
|
+
* re-detecting the problem.
|
|
6
10
|
*/
|
|
7
11
|
import { PptxPackage } from "./package/package.js";
|
|
8
12
|
import { readPresentation } from "./reader/presentation.js";
|
|
9
13
|
import { SUPPORTED_GEOMETRIES } from "./mapper/shape-mapper.js";
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
|
|
20
|
-
"
|
|
21
|
-
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
|
|
25
|
-
"
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"
|
|
14
|
+
import { FONT_SUBSTITUTIONS } from "./resolve/font-map.js";
|
|
15
|
+
import { FONT_METRICS, widthDelta, findClosestFont } from "./resolve/font-metrics.js";
|
|
16
|
+
import { resolveColor } from "./resolve/color-resolver.js";
|
|
17
|
+
// ── Geometry fallback map (unsupported → closest supported) ────────
|
|
18
|
+
/** Maps commonly-used unsupported OOXML presets to visually-closest supported alternative. */
|
|
19
|
+
export const GEOMETRY_FALLBACKS = {
|
|
20
|
+
// Rounded rect variants → roundRect
|
|
21
|
+
round1Rect: "roundRect", round2SameRect: "roundRect", round2DiagRect: "roundRect",
|
|
22
|
+
snipRoundRect: "roundRect", plaque: "roundRect",
|
|
23
|
+
// Snip rect variants → rect
|
|
24
|
+
snip1Rect: "rect", snip2SameRect: "rect", snip2DiagRect: "rect",
|
|
25
|
+
// Organic shapes → ellipse
|
|
26
|
+
heart: "ellipse", donut: "ellipse", noSmoking: "ellipse",
|
|
27
|
+
smileyFace: "ellipse", sun: "ellipse", moon: "ellipse", blockArc: "ellipse",
|
|
28
|
+
// Angular shapes → closest polygon
|
|
29
|
+
decagon: "octagon", heptagon: "hexagon", dodecagon: "octagon",
|
|
30
|
+
// Arrows (unsupported variants → closest supported arrow)
|
|
31
|
+
bentArrow: "rightArrow", curvedLeftArrow: "leftArrow",
|
|
32
|
+
curvedRightArrow: "rightArrow", curvedUpArrow: "upArrow",
|
|
33
|
+
curvedDownArrow: "downArrow", notchedRightArrow: "rightArrow",
|
|
34
|
+
stripedRightArrow: "rightArrow", uturnArrow: "downArrow",
|
|
35
|
+
// Flowcharts → closest match
|
|
36
|
+
flowChartProcess: "rect", flowChartTerminator: "roundRect",
|
|
37
|
+
flowChartDocument: "rect", flowChartInputOutput: "parallelogram",
|
|
38
|
+
flowChartPreparation: "hexagon", flowChartManualInput: "trapezoid",
|
|
39
|
+
flowChartManualOperation: "trapezoid", flowChartConnector: "ellipse",
|
|
40
|
+
flowChartPredefinedProcess: "rect", flowChartInternalStorage: "rect",
|
|
41
|
+
flowChartMultidocument: "rect", flowChartOffpageConnector: "pentagon",
|
|
42
|
+
flowChartPunchedCard: "rect", flowChartPunchedTape: "rect",
|
|
43
|
+
flowChartSummingJunction: "ellipse", flowChartOr: "ellipse",
|
|
44
|
+
flowChartCollate: "diamond", flowChartSort: "diamond",
|
|
45
|
+
flowChartExtract: "triangle", flowChartMerge: "triangle",
|
|
46
|
+
flowChartOnlineStorage: "can", flowChartDelay: "roundRect",
|
|
47
|
+
flowChartMagneticTape: "ellipse", flowChartMagneticDisk: "can",
|
|
48
|
+
flowChartMagneticDrum: "can", flowChartDisplay: "roundRect",
|
|
49
|
+
// Misc
|
|
50
|
+
foldedCorner: "rect", frame: "rect", bevel: "rect",
|
|
51
|
+
lightningBolt: "rightArrow", cloud: "cloudCallout",
|
|
52
|
+
ribbon: "rect", ribbon2: "rect",
|
|
53
|
+
horizontalScroll: "verticalScroll",
|
|
54
|
+
wave: "rect", doubleWave: "rect",
|
|
55
|
+
// Callout variants → closest supported callout
|
|
56
|
+
callout1: "borderCallout1", callout2: "borderCallout1", callout3: "borderCallout1",
|
|
57
|
+
accentCallout1: "borderCallout1", accentCallout2: "borderCallout1", accentCallout3: "borderCallout1",
|
|
58
|
+
accentBorderCallout1: "borderCallout1", accentBorderCallout2: "borderCallout1", accentBorderCallout3: "borderCallout1",
|
|
59
|
+
borderCallout2: "borderCallout1", borderCallout3: "borderCallout1",
|
|
60
|
+
// Math → plus (only supported math-like shape)
|
|
61
|
+
mathPlus: "plus", mathMinus: "rect", mathMultiply: "plus",
|
|
62
|
+
mathDivide: "rect", mathEqual: "rect", mathNotEqual: "rect",
|
|
63
|
+
// Brackets/braces → rect fallback
|
|
64
|
+
leftBrace: "rect", rightBrace: "rect", leftBracket: "rect", rightBracket: "rect",
|
|
65
|
+
bracePair: "rect", bracketPair: "rect",
|
|
66
|
+
// Stars (unsupported sizes) → closest supported star
|
|
67
|
+
star7: "star8",
|
|
68
|
+
// Action buttons → rect
|
|
69
|
+
actionButtonBlank: "rect", actionButtonHome: "rect", actionButtonHelp: "rect",
|
|
70
|
+
actionButtonInformation: "rect", actionButtonBackPrevious: "rect",
|
|
71
|
+
actionButtonForwardNext: "rect", actionButtonBeginning: "rect",
|
|
72
|
+
actionButtonEnd: "rect", actionButtonReturn: "rect",
|
|
73
|
+
actionButtonDocument: "rect", actionButtonSound: "rect",
|
|
74
|
+
actionButtonMovie: "rect",
|
|
75
|
+
// Misc exotic
|
|
76
|
+
gear6: "hexagon", gear9: "octagon",
|
|
77
|
+
funnel: "triangle", pie: "ellipse", pieWedge: "triangle",
|
|
78
|
+
irregularSeal1: "diamond", irregularSeal2: "diamond",
|
|
31
79
|
};
|
|
80
|
+
// Geometries where text inscription differs from a simple rect inset
|
|
81
|
+
const NON_RECT_TEXT_GEOMETRIES = new Set([
|
|
82
|
+
"roundRect", "ellipse", "diamond", "triangle", "rtTriangle",
|
|
83
|
+
"parallelogram", "trapezoid", "hexagon", "octagon", "pentagon",
|
|
84
|
+
"star4", "star5", "star6", "star8",
|
|
85
|
+
]);
|
|
86
|
+
function rgbaToHex(c) {
|
|
87
|
+
const r = c.r.toString(16).padStart(2, "0");
|
|
88
|
+
const g = c.g.toString(16).padStart(2, "0");
|
|
89
|
+
const b = c.b.toString(16).padStart(2, "0");
|
|
90
|
+
return `#${r}${g}${b}`;
|
|
91
|
+
}
|
|
32
92
|
/** Compute width delta between a source font and its OfficeImport substitute. */
|
|
33
93
|
function fontWidthDelta(sourceName) {
|
|
34
|
-
const targetName =
|
|
94
|
+
const targetName = FONT_SUBSTITUTIONS[sourceName];
|
|
35
95
|
if (!targetName)
|
|
36
96
|
return null;
|
|
37
97
|
const src = FONT_METRICS[sourceName];
|
|
@@ -40,12 +100,6 @@ function fontWidthDelta(sourceName) {
|
|
|
40
100
|
return null;
|
|
41
101
|
return widthDelta(src, tgt);
|
|
42
102
|
}
|
|
43
|
-
// Geometries where text inscription differs from a simple rect inset
|
|
44
|
-
const NON_RECT_TEXT_GEOMETRIES = new Set([
|
|
45
|
-
"roundRect", "ellipse", "diamond", "triangle", "rtTriangle",
|
|
46
|
-
"parallelogram", "trapezoid", "hexagon", "octagon", "pentagon",
|
|
47
|
-
"star4", "star5", "star6", "star8",
|
|
48
|
-
]);
|
|
49
103
|
// ── Main API ────────────────────────────────────────────────────────
|
|
50
104
|
export async function lint(pptxBuffer) {
|
|
51
105
|
const pkg = await PptxPackage.open(pptxBuffer);
|
|
@@ -54,7 +108,14 @@ export async function lint(pptxBuffer) {
|
|
|
54
108
|
for (let i = 0; i < pres.slides.length; i++) {
|
|
55
109
|
const slide = pres.slides[i];
|
|
56
110
|
const slideNum = i + 1;
|
|
57
|
-
|
|
111
|
+
const master = slide.slideLayout.slideMaster;
|
|
112
|
+
const ctx = {
|
|
113
|
+
colorMap: slide.colorMapOverride
|
|
114
|
+
?? slide.slideLayout.colorMapOverride
|
|
115
|
+
?? master.colorMap,
|
|
116
|
+
colorScheme: master.theme.colorScheme,
|
|
117
|
+
};
|
|
118
|
+
lintDrawables(slide.drawables, slideNum, issues, ctx);
|
|
58
119
|
}
|
|
59
120
|
// Deduplicate font substitution warnings (one per font, not per run)
|
|
60
121
|
deduplicateFonts(issues);
|
|
@@ -69,39 +130,41 @@ export async function lint(pptxBuffer) {
|
|
|
69
130
|
};
|
|
70
131
|
}
|
|
71
132
|
// ── Drawable walker ─────────────────────────────────────────────────
|
|
72
|
-
function lintDrawables(drawables, slide, issues) {
|
|
133
|
+
function lintDrawables(drawables, slide, issues, ctx) {
|
|
73
134
|
for (const d of drawables) {
|
|
74
135
|
switch (d.drawableType) {
|
|
75
136
|
case "sp":
|
|
76
|
-
lintShape(d, slide, issues);
|
|
137
|
+
lintShape(d, slide, issues, ctx);
|
|
77
138
|
break;
|
|
78
139
|
case "pic":
|
|
79
140
|
lintPicture(d, slide, issues);
|
|
80
141
|
break;
|
|
81
142
|
case "grpSp":
|
|
82
|
-
lintGroup(d, slide, issues);
|
|
143
|
+
lintGroup(d, slide, issues, ctx);
|
|
83
144
|
break;
|
|
84
145
|
case "cxnSp":
|
|
85
146
|
lintConnector(d, slide, issues);
|
|
86
147
|
break;
|
|
87
148
|
case "graphicFrame":
|
|
88
|
-
lintGraphicFrame(d, slide, issues);
|
|
149
|
+
lintGraphicFrame(d, slide, issues, ctx);
|
|
89
150
|
break;
|
|
90
151
|
}
|
|
91
152
|
}
|
|
92
153
|
}
|
|
93
154
|
// ── Shape checks ────────────────────────────────────────────────────
|
|
94
|
-
function lintShape(shape, slide, issues) {
|
|
155
|
+
function lintShape(shape, slide, issues, ctx) {
|
|
95
156
|
const geom = shape.geometry?.preset ?? "rect";
|
|
96
157
|
const name = shape.name || `shape #${shape.id}`;
|
|
97
158
|
// 1. Unsupported geometry → invisible
|
|
98
159
|
if (geom !== "rect" && !SUPPORTED_GEOMETRIES.has(geom)) {
|
|
160
|
+
const suggested = GEOMETRY_FALLBACKS[geom] ?? "rect";
|
|
99
161
|
issues.push({
|
|
100
162
|
rule: "unsupported-geometry",
|
|
101
163
|
severity: "error",
|
|
102
164
|
slide, element: name,
|
|
103
165
|
message: `"${geom}" is not supported by OfficeImport — this shape will be invisible in QuickLook`,
|
|
104
|
-
suggestion: `
|
|
166
|
+
suggestion: `Replace with "${suggested}" (closest supported preset) or embed as an image`,
|
|
167
|
+
fix: { action: "replace-geometry", current: geom, suggested },
|
|
105
168
|
});
|
|
106
169
|
return; // no point checking further
|
|
107
170
|
}
|
|
@@ -116,24 +179,24 @@ function lintShape(shape, slide, issues) {
|
|
|
116
179
|
slide, element: name,
|
|
117
180
|
message: `"${geom}" with effects renders as an opaque PDF image — it will cover content behind it with a white background`,
|
|
118
181
|
suggestion: `Remove drop shadow/glow effects, or use a plain rect to keep CSS rendering`,
|
|
182
|
+
fix: { action: "strip-effects" },
|
|
119
183
|
});
|
|
120
184
|
}
|
|
121
185
|
else if (geom !== "rect") {
|
|
122
186
|
issues.push({
|
|
123
|
-
rule: "
|
|
124
|
-
severity: "
|
|
187
|
+
rule: "geometry-forces-pdf",
|
|
188
|
+
severity: "info",
|
|
125
189
|
slide, element: name,
|
|
126
|
-
message: `"${geom}" renders as PDF in QuickLook (only plain rect uses CSS)`,
|
|
127
|
-
suggestion: `Use rect geometry to keep CSS rendering, or accept PDF output`,
|
|
190
|
+
message: `"${geom}" renders as PDF in QuickLook (only plain rect uses CSS) — opaque background may cover adjacent content`,
|
|
128
191
|
});
|
|
129
192
|
}
|
|
130
193
|
if (hasRotation && geom === "rect") {
|
|
194
|
+
const rotDeg = Math.round((shape.bounds.rot / 60000) % 360);
|
|
131
195
|
issues.push({
|
|
132
196
|
rule: "rotation-forces-pdf",
|
|
133
|
-
severity: "
|
|
197
|
+
severity: "info",
|
|
134
198
|
slide, element: name,
|
|
135
|
-
message: `Rotated rect renders as PDF
|
|
136
|
-
suggestion: `Remove rotation or accept PDF rendering`,
|
|
199
|
+
message: `Rotated rect (${rotDeg}°) renders as PDF — enlarged bounding box may cover adjacent content`,
|
|
137
200
|
});
|
|
138
201
|
}
|
|
139
202
|
}
|
|
@@ -145,20 +208,22 @@ function lintShape(shape, slide, issues) {
|
|
|
145
208
|
slide, element: name,
|
|
146
209
|
message: `rect with effects (shadow/glow) renders as opaque PDF — covers content behind it`,
|
|
147
210
|
suggestion: `Remove effects to keep lightweight CSS rendering`,
|
|
211
|
+
fix: { action: "strip-effects" },
|
|
148
212
|
});
|
|
149
213
|
}
|
|
150
214
|
// 4. Non-rect text inscription shift
|
|
151
215
|
if (NON_RECT_TEXT_GEOMETRIES.has(geom) && hasNonEmptyText(shape.textBody)) {
|
|
152
216
|
issues.push({
|
|
153
217
|
rule: "text-inscription-shift",
|
|
154
|
-
severity: "
|
|
218
|
+
severity: "info",
|
|
155
219
|
slide, element: name,
|
|
156
220
|
message: `Text in "${geom}" uses geometry-inscribed bounds — text position will differ from PowerPoint`,
|
|
157
221
|
suggestion: `Use rect for predictable text positioning, or test the specific geometry`,
|
|
222
|
+
fix: { action: "replace-geometry", current: geom, suggested: "rect" },
|
|
158
223
|
});
|
|
159
224
|
}
|
|
160
225
|
// 5. Gradient flattening
|
|
161
|
-
lintFill(shape.fill, slide, name, issues);
|
|
226
|
+
lintFill(shape.fill, slide, name, issues, ctx);
|
|
162
227
|
// 6. Font substitution in text
|
|
163
228
|
if (shape.textBody)
|
|
164
229
|
lintTextBody(shape.textBody, slide, name, issues);
|
|
@@ -177,24 +242,26 @@ function lintPicture(_pic, _slide, _issues) {
|
|
|
177
242
|
// Pictures are straightforward — OfficeImport handles them well
|
|
178
243
|
}
|
|
179
244
|
// ── Group checks ────────────────────────────────────────────────────
|
|
180
|
-
function lintGroup(group, slide, issues) {
|
|
245
|
+
function lintGroup(group, slide, issues, ctx) {
|
|
181
246
|
const name = group.name || `group #${group.id}`;
|
|
247
|
+
const containsPictures = group.children.some(c => c.drawableType === "pic");
|
|
182
248
|
issues.push({
|
|
183
249
|
rule: "group-as-pdf",
|
|
184
250
|
severity: "warn",
|
|
185
251
|
slide, element: name,
|
|
186
|
-
message: `Group renders as single PDF image in QuickLook — all children merged into one opaque block`,
|
|
252
|
+
message: `Group (${group.children.length} children) renders as single PDF image in QuickLook — all children merged into one opaque block`,
|
|
187
253
|
suggestion: `Ungroup shapes for individual rendering, or accept merged PDF`,
|
|
254
|
+
fix: { action: "ungroup", childCount: group.children.length, containsPictures },
|
|
188
255
|
});
|
|
189
256
|
// Still lint children for their own issues
|
|
190
|
-
lintDrawables(group.children, slide, issues);
|
|
257
|
+
lintDrawables(group.children, slide, issues, ctx);
|
|
191
258
|
}
|
|
192
259
|
// ── Connector checks ────────────────────────────────────────────────
|
|
193
260
|
function lintConnector(_conn, _slide, _issues) {
|
|
194
261
|
// Connectors are always PDF, no special warnings needed
|
|
195
262
|
}
|
|
196
263
|
// ── GraphicFrame (tables/charts) ────────────────────────────────────
|
|
197
|
-
function lintGraphicFrame(frame, slide, issues) {
|
|
264
|
+
function lintGraphicFrame(frame, slide, issues, ctx) {
|
|
198
265
|
const name = frame.name || `frame #${frame.id}`;
|
|
199
266
|
// Chart without fallback image
|
|
200
267
|
if (frame.chartRId && !frame.fallbackImageData) {
|
|
@@ -204,14 +271,15 @@ function lintGraphicFrame(frame, slide, issues) {
|
|
|
204
271
|
slide, element: name,
|
|
205
272
|
message: `Chart has no fallback image — will render as blank rectangle in QuickLook`,
|
|
206
273
|
suggestion: `Save from PowerPoint (not python-pptx) to generate fallback images, or use mc:AlternateContent`,
|
|
274
|
+
fix: { action: "add-fallback-image" },
|
|
207
275
|
});
|
|
208
276
|
}
|
|
209
277
|
// Table checks
|
|
210
278
|
if (frame.tableData) {
|
|
211
|
-
lintTable(frame.tableData, slide, name, issues);
|
|
279
|
+
lintTable(frame.tableData, slide, name, issues, ctx);
|
|
212
280
|
}
|
|
213
281
|
}
|
|
214
|
-
function lintTable(table, slide, element, issues) {
|
|
282
|
+
function lintTable(table, slide, element, issues, ctx) {
|
|
215
283
|
// Check for table style reference without explicit borders
|
|
216
284
|
if (table.tableStyleId) {
|
|
217
285
|
const hasMissingBorders = table.rows.some(row => row.cells.some(cell => !hasExplicitBorders(cell)));
|
|
@@ -221,7 +289,8 @@ function lintTable(table, slide, element, issues) {
|
|
|
221
289
|
severity: "warn",
|
|
222
290
|
slide, element,
|
|
223
291
|
message: `Table uses style "${table.tableStyleId}" but some cells lack explicit borders — OfficeImport may not resolve the style`,
|
|
224
|
-
suggestion: `
|
|
292
|
+
suggestion: `Inline borders explicitly on each cell`,
|
|
293
|
+
fix: { action: "inline-borders", tableStyleId: table.tableStyleId },
|
|
225
294
|
});
|
|
226
295
|
}
|
|
227
296
|
}
|
|
@@ -235,6 +304,7 @@ function lintTable(table, slide, element, issues) {
|
|
|
235
304
|
slide, element,
|
|
236
305
|
message: `All ${totalCells} table cells have no explicit borders — table will appear borderless in QuickLook`,
|
|
237
306
|
suggestion: `Add explicit borders to cells if borders are expected`,
|
|
307
|
+
fix: { action: "add-borders" },
|
|
238
308
|
});
|
|
239
309
|
}
|
|
240
310
|
// Lint text in table cells
|
|
@@ -242,7 +312,7 @@ function lintTable(table, slide, element, issues) {
|
|
|
242
312
|
for (const cell of row.cells) {
|
|
243
313
|
if (cell.textBody)
|
|
244
314
|
lintTextBody(cell.textBody, slide, element, issues);
|
|
245
|
-
lintFill(cell.fill, slide, element, issues);
|
|
315
|
+
lintFill(cell.fill, slide, element, issues, ctx);
|
|
246
316
|
}
|
|
247
317
|
}
|
|
248
318
|
}
|
|
@@ -259,17 +329,37 @@ function hasAnyBorder(cell) {
|
|
|
259
329
|
return !!(b.top?.width || b.bottom?.width || b.left?.width || b.right?.width);
|
|
260
330
|
}
|
|
261
331
|
// ── Fill checks ─────────────────────────────────────────────────────
|
|
262
|
-
function lintFill(fill, slide, element, issues) {
|
|
332
|
+
function lintFill(fill, slide, element, issues, ctx) {
|
|
263
333
|
if (!fill || fill.type !== "gradient")
|
|
264
334
|
return;
|
|
265
335
|
const grad = fill;
|
|
266
336
|
if (grad.stops.length >= 3) {
|
|
337
|
+
// Compute resolved colors for fix data
|
|
338
|
+
let firstColor = "#888888", lastColor = "#888888", averageColor = "#888888";
|
|
339
|
+
try {
|
|
340
|
+
const first = resolveColor(grad.stops[0].color, ctx.colorMap, ctx.colorScheme);
|
|
341
|
+
const last = resolveColor(grad.stops[grad.stops.length - 1].color, ctx.colorMap, ctx.colorScheme);
|
|
342
|
+
firstColor = rgbaToHex(first);
|
|
343
|
+
lastColor = rgbaToHex(last);
|
|
344
|
+
let r = 0, g = 0, b = 0;
|
|
345
|
+
for (const stop of grad.stops) {
|
|
346
|
+
const c = resolveColor(stop.color, ctx.colorMap, ctx.colorScheme);
|
|
347
|
+
r += c.r;
|
|
348
|
+
g += c.g;
|
|
349
|
+
b += c.b;
|
|
350
|
+
}
|
|
351
|
+
const n = grad.stops.length;
|
|
352
|
+
averageColor = rgbaToHex({ r: Math.round(r / n), g: Math.round(g / n), b: Math.round(b / n), a: 1 });
|
|
353
|
+
}
|
|
354
|
+
catch { /* color resolution may fail for exotic color types */ }
|
|
355
|
+
const angle = grad.linear?.angle;
|
|
267
356
|
issues.push({
|
|
268
357
|
rule: "gradient-flattened",
|
|
269
358
|
severity: "warn",
|
|
270
359
|
slide, element,
|
|
271
|
-
message: `Gradient with ${grad.stops.length} stops will be flattened to a single average color in QuickLook`,
|
|
272
|
-
suggestion: `Use exactly 2 gradient stops for proper rendering
|
|
360
|
+
message: `Gradient with ${grad.stops.length} stops will be flattened to a single average color (${averageColor}) in QuickLook`,
|
|
361
|
+
suggestion: `Use exactly 2 gradient stops for proper rendering — first: ${firstColor}, last: ${lastColor}`,
|
|
362
|
+
fix: { action: "reduce-stops", firstColor, lastColor, averageColor, angle },
|
|
273
363
|
});
|
|
274
364
|
}
|
|
275
365
|
}
|
|
@@ -283,18 +373,37 @@ function lintTextBody(body, slide, element, issues) {
|
|
|
283
373
|
if (!props)
|
|
284
374
|
continue;
|
|
285
375
|
const font = props.latinFont;
|
|
286
|
-
if (font &&
|
|
376
|
+
if (font && FONT_SUBSTITUTIONS[font]) {
|
|
377
|
+
const macTarget = FONT_SUBSTITUTIONS[font];
|
|
287
378
|
const delta = fontWidthDelta(font);
|
|
288
379
|
const deltaStr = delta != null && Math.abs(delta) >= 0.5
|
|
289
380
|
? ` (${delta > 0 ? "+" : ""}${delta.toFixed(1)}% width)`
|
|
290
381
|
: "";
|
|
291
382
|
const highRisk = delta != null && Math.abs(delta) >= 10;
|
|
383
|
+
// Find metrically-closest alternatives (consumer decides which to use)
|
|
384
|
+
const alternatives = [];
|
|
385
|
+
if (FONT_METRICS[font]) {
|
|
386
|
+
const matches = findClosestFont(font, { limit: 5 });
|
|
387
|
+
for (const m of matches) {
|
|
388
|
+
alternatives.push({ font: m.font, widthDelta: m.widthDelta });
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
const bestAlt = alternatives[0];
|
|
292
392
|
issues.push({
|
|
293
393
|
rule: "font-substitution",
|
|
294
394
|
severity: highRisk ? "warn" : "info",
|
|
295
395
|
slide, element,
|
|
296
|
-
message: `"${font}" → "${
|
|
297
|
-
suggestion: highRisk
|
|
396
|
+
message: `"${font}" → "${macTarget}" on macOS${deltaStr}${highRisk ? " — high risk of text reflow/overflow" : ""}`,
|
|
397
|
+
suggestion: highRisk
|
|
398
|
+
? `Closest alternative: ${bestAlt ? `${bestAlt.font} (${bestAlt.widthDelta > 0 ? "+" : ""}${bestAlt.widthDelta.toFixed(1)}% width)` : "test on macOS"}`
|
|
399
|
+
: undefined,
|
|
400
|
+
fix: {
|
|
401
|
+
action: "replace-font",
|
|
402
|
+
from: font,
|
|
403
|
+
macTarget,
|
|
404
|
+
widthDelta: delta ?? 0,
|
|
405
|
+
alternatives,
|
|
406
|
+
},
|
|
298
407
|
});
|
|
299
408
|
}
|
|
300
409
|
}
|
package/dist/resolve/font-map.js
CHANGED
|
@@ -84,6 +84,32 @@ export const FONT_METRICS = {
|
|
|
84
84
|
"PT Serif": { cat: "serif", upm: 1000, xWidthAvg: 448, capH: 700, xH: 500, asc: 1039, desc: -286, lineGap: 0 },
|
|
85
85
|
"Ubuntu": { cat: "sans-serif", upm: 1000, xWidthAvg: 455, capH: 693, xH: 520, asc: 932, desc: -189, lineGap: 28 },
|
|
86
86
|
"Ubuntu Mono": { cat: "monospace", upm: 1000, xWidthAvg: 500, capH: 693, xH: 520, asc: 830, desc: -170, lineGap: 0 },
|
|
87
|
+
// ── Croscore — metric-compatible Google Fonts (OS/2 table) ────────
|
|
88
|
+
"Carlito": { cat: "sans-serif", upm: 2048, xWidthAvg: 1048, capH: 1314, xH: 978, asc: 1536, desc: -512, lineGap: 452 },
|
|
89
|
+
"Caladea": { cat: "serif", upm: 1000, xWidthAvg: 510, capH: 667, xH: 467, asc: 900, desc: -250, lineGap: 0 },
|
|
90
|
+
"Arimo": { cat: "sans-serif", upm: 2048, xWidthAvg: 1190, capH: 1409, xH: 1082, asc: 1854, desc: -434, lineGap: 67 },
|
|
91
|
+
"Tinos": { cat: "serif", upm: 2048, xWidthAvg: 1116, capH: 1341, xH: 940, asc: 1420, desc: -442, lineGap: 307 },
|
|
92
|
+
"Cousine": { cat: "monospace", upm: 2048, xWidthAvg: 1229, capH: 1349, xH: 1082, asc: 1255, desc: -386, lineGap: 0 },
|
|
93
|
+
// ── More Google Fonts (OS/2 table) ────────────────────────────────
|
|
94
|
+
"DM Sans": { cat: "sans-serif", upm: 1000, xWidthAvg: 553, capH: 700, xH: 526, asc: 992, desc: -310, lineGap: 0 },
|
|
95
|
+
"Work Sans": { cat: "sans-serif", upm: 1000, xWidthAvg: 597, capH: 660, xH: 500, asc: 930, desc: -243, lineGap: 0 },
|
|
96
|
+
"Rubik": { cat: "sans-serif", upm: 1000, xWidthAvg: 622, capH: 700, xH: 520, asc: 935, desc: -250, lineGap: 0 },
|
|
97
|
+
"Libre Baskerville": { cat: "serif", upm: 1000, xWidthAvg: 654, capH: 770, xH: 530, asc: 970, desc: -270, lineGap: 0 },
|
|
98
|
+
"Libre Franklin": { cat: "sans-serif", upm: 1000, xWidthAvg: 589, capH: 742, xH: 530, asc: 966, desc: -246, lineGap: 0 },
|
|
99
|
+
"Barlow": { cat: "sans-serif", upm: 1000, xWidthAvg: 513, capH: 700, xH: 506, asc: 1000, desc: -200, lineGap: 0 },
|
|
100
|
+
"Josefin Sans": { cat: "sans-serif", upm: 1000, xWidthAvg: 547, capH: 702, xH: 378, asc: 750, desc: -250, lineGap: 0 },
|
|
101
|
+
"Quicksand": { cat: "sans-serif", upm: 1000, xWidthAvg: 541, capH: 700, xH: 503, asc: 1000, desc: -250, lineGap: 0 },
|
|
102
|
+
"Cabin": { cat: "sans-serif", upm: 2000, xWidthAvg: 1054, capH: 1400, xH: 980, asc: 1930, desc: -500, lineGap: 0 },
|
|
103
|
+
"Karla": { cat: "sans-serif", upm: 2000, xWidthAvg: 1055, capH: 1256, xH: 956, asc: 1834, desc: -504, lineGap: 0 },
|
|
104
|
+
"Manrope": { cat: "sans-serif", upm: 2000, xWidthAvg: 1131, capH: 1440, xH: 1080, asc: 2132, desc: -600, lineGap: 0 },
|
|
105
|
+
"Space Grotesk": { cat: "sans-serif", upm: 1000, xWidthAvg: 559, capH: 700, xH: 486, asc: 984, desc: -292, lineGap: 0 },
|
|
106
|
+
"IBM Plex Sans": { cat: "sans-serif", upm: 1000, xWidthAvg: 592, capH: 698, xH: 516, asc: 1025, desc: -275, lineGap: 0 },
|
|
107
|
+
"IBM Plex Mono": { cat: "monospace", upm: 1000, xWidthAvg: 600, capH: 698, xH: 516, asc: 1025, desc: -275, lineGap: 0 },
|
|
108
|
+
"JetBrains Mono": { cat: "monospace", upm: 1000, xWidthAvg: 602, capH: 730, xH: 550, asc: 1020, desc: -300, lineGap: 0 },
|
|
109
|
+
"Bitter": { cat: "serif", upm: 1000, xWidthAvg: 590, capH: 692, xH: 522, asc: 935, desc: -265, lineGap: 0 },
|
|
110
|
+
"Crimson Text": { cat: "serif", upm: 1024, xWidthAvg: 547, capH: 656, xH: 430, asc: 972, desc: -359, lineGap: 0 },
|
|
111
|
+
"EB Garamond": { cat: "serif", upm: 1000, xWidthAvg: 528, capH: 650, xH: 400, asc: 1007, desc: -298, lineGap: 0 },
|
|
112
|
+
"Zilla Slab": { cat: "serif", upm: 1000, xWidthAvg: 538, capH: 650, xH: 445, asc: 787, desc: -213, lineGap: 200 },
|
|
87
113
|
};
|
|
88
114
|
// ── Similarity matching ─────────────────────────────────────────────
|
|
89
115
|
/** Normalize a metric by unitsPerEm so fonts at different scales are comparable. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "quicklook-pptx-renderer",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
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",
|