quicklook-pptx-renderer 0.3.0 → 0.3.2
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 +10 -7
- package/dist/lint.d.ts +7 -3
- package/dist/lint.js +44 -23
- package/dist/package/package.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -22,7 +22,7 @@ It happens because Apple uses a private framework called `OfficeImport.framework
|
|
|
22
22
|
|---|---|
|
|
23
23
|
| **Shapes disappear** — heart, cloud, lightningBolt, sun, moon, and ~120 more | OfficeImport's `CMCanonicalShapeBuilder` only supports ~60 of 187 preset geometries. The rest are silently dropped — no error, no fallback. |
|
|
24
24
|
| **Opaque white blocks cover content** — rounded rectangles with shadows become solid rectangles | Any non-`rect` shape or any shape with effects (drop shadow, glow, reflection) is rendered as an opaque PDF image via `CMDrawingContext.copyPDF`. The PDF has a non-transparent background. |
|
|
25
|
-
| **Table borders missing** — tables appear as plain text without any grid | OfficeImport doesn't resolve `tableStyleId` references. It
|
|
25
|
+
| **Table borders missing** — tables appear as plain text without any grid | OfficeImport doesn't resolve `tableStyleId` references. It emits `border-style:none` per-cell unless borders are explicit in `<a:lnL>`, `<a:lnR>`, `<a:lnT>`, `<a:lnB>`. PowerPoint resolves the style and shows borders; QuickLook doesn't. python-pptx and most generators rely on style references. |
|
|
26
26
|
| **Gradients become flat colors** — gradient fills show as a single solid color | Gradients with 3+ color stops are averaged to one color instead of being rendered as a gradient. |
|
|
27
27
|
| **Fonts shift and text reflows** — text overflows boxes, lines break differently | Calibri → Helvetica Neue, Arial → Helvetica, Segoe UI → Helvetica Neue. Different metrics cause text reflow. |
|
|
28
28
|
| **Charts are blank rectangles** — chart content simply vanishes | Charts without an embedded fallback image render as empty rectangles. |
|
|
@@ -153,10 +153,8 @@ for (const issue of result.issues) {
|
|
|
153
153
|
case "inline-borders":
|
|
154
154
|
// issue.fix.tableStyleId = "{5C22544A-...}"
|
|
155
155
|
// Resolve table style and write explicit borders on each cell
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
case "add-borders":
|
|
159
|
-
// Add default 1px solid black borders to all cells
|
|
156
|
+
// Note: tables without a style AND without borders are intentionally
|
|
157
|
+
// borderless — both PowerPoint and QuickLook render them the same way.
|
|
160
158
|
break;
|
|
161
159
|
|
|
162
160
|
case "add-fallback-image":
|
|
@@ -167,6 +165,11 @@ for (const issue of result.issues) {
|
|
|
167
165
|
// issue.fix.childCount = 5, issue.fix.containsPictures = false
|
|
168
166
|
// Promote group children to slide-level drawables
|
|
169
167
|
break;
|
|
168
|
+
|
|
169
|
+
case "strip-embedded-fonts":
|
|
170
|
+
// issue.fix.fonts = [{ name: "Montserrat", replacement: "Nunito Sans" }]
|
|
171
|
+
// Strip embedded font data and replace with cross-platform alternatives
|
|
172
|
+
break;
|
|
170
173
|
}
|
|
171
174
|
}
|
|
172
175
|
```
|
|
@@ -185,13 +188,13 @@ Every issue carries a typed `fix` object with machine-readable remediation data
|
|
|
185
188
|
| `chart-no-fallback` | error | Chart without fallback image — renders as blank rectangle | `add-fallback-image` |
|
|
186
189
|
| `opaque-pdf-block` | warn | Shape with effects rendered as opaque PDF covering content behind it | `strip-effects` |
|
|
187
190
|
| `gradient-flattened` | warn | 3+ gradient stops collapsed to single average color | `reduce-stops` (includes computed 2-stop colors + average) |
|
|
188
|
-
| `table-
|
|
189
|
-
| `table-style-unresolved` | warn | Table style reference that OfficeImport won't resolve | `inline-borders` (includes `tableStyleId`) |
|
|
191
|
+
| `table-style-unresolved` | warn | Table has style but cells lack explicit borders — PowerPoint shows borders, QL won't | `inline-borders` (includes `tableStyleId`) |
|
|
190
192
|
| `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
193
|
| `group-as-pdf` | warn | Group rendered as single opaque PDF image | `ungroup` (includes child count, whether group contains pictures) |
|
|
192
194
|
| `geometry-forces-pdf` | info | Non-rect geometry renders as PDF — opaque background may cover adjacent content | — |
|
|
193
195
|
| `rotation-forces-pdf` | info | Rotated rect renders as PDF — enlarged bounding box may cover adjacent content | — |
|
|
194
196
|
| `text-inscription-shift` | info | Text in non-rect shape uses geometry-inscribed bounds (text may shift) | `replace-geometry` (suggests `rect`) |
|
|
197
|
+
| `embedded-font` | warn | Embedded fonts ignored by QuickLook — text renders with system substitutes | `strip-embedded-fonts` (includes per-font replacement when metrics available) |
|
|
195
198
|
| `vertical-text` | info | Vertical text uses CSS writing-mode | — |
|
|
196
199
|
|
|
197
200
|
---
|
package/dist/lint.d.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* re-detecting the problem.
|
|
10
10
|
*/
|
|
11
11
|
export type Severity = "error" | "warn" | "info";
|
|
12
|
-
export type RuleId = "unsupported-geometry" | "opaque-pdf-block" | "gradient-flattened" | "font-substitution" | "table-
|
|
12
|
+
export type RuleId = "unsupported-geometry" | "opaque-pdf-block" | "gradient-flattened" | "font-substitution" | "table-style-unresolved" | "group-as-pdf" | "chart-no-fallback" | "text-inscription-shift" | "rotation-forces-pdf" | "geometry-forces-pdf" | "vertical-text" | "embedded-font";
|
|
13
13
|
/** Machine-readable remediation data — one variant per fix strategy. */
|
|
14
14
|
export type LintFix = {
|
|
15
15
|
action: "replace-geometry";
|
|
@@ -35,14 +35,18 @@ export type LintFix = {
|
|
|
35
35
|
} | {
|
|
36
36
|
action: "inline-borders";
|
|
37
37
|
tableStyleId: string;
|
|
38
|
-
} | {
|
|
39
|
-
action: "add-borders";
|
|
40
38
|
} | {
|
|
41
39
|
action: "add-fallback-image";
|
|
42
40
|
} | {
|
|
43
41
|
action: "ungroup";
|
|
44
42
|
childCount: number;
|
|
45
43
|
containsPictures: boolean;
|
|
44
|
+
} | {
|
|
45
|
+
action: "strip-embedded-fonts";
|
|
46
|
+
fonts: Array<{
|
|
47
|
+
name: string;
|
|
48
|
+
replacement?: string;
|
|
49
|
+
}>;
|
|
46
50
|
};
|
|
47
51
|
export interface LintIssue {
|
|
48
52
|
rule: RuleId;
|
package/dist/lint.js
CHANGED
|
@@ -105,6 +105,40 @@ export async function lint(pptxBuffer) {
|
|
|
105
105
|
const pkg = await PptxPackage.open(pptxBuffer);
|
|
106
106
|
const pres = await readPresentation(pkg);
|
|
107
107
|
const issues = [];
|
|
108
|
+
// Presentation-level: detect embedded fonts (QuickLook ignores them)
|
|
109
|
+
const presXml = await pkg.getPartXml("ppt/presentation.xml");
|
|
110
|
+
const presNode = presXml?.Presentation ?? presXml?.presentation ?? presXml ?? {};
|
|
111
|
+
const embFontLst = presNode.embeddedFontLst?.embeddedFont;
|
|
112
|
+
if (embFontLst) {
|
|
113
|
+
const entries = Array.isArray(embFontLst) ? embFontLst : [embFontLst];
|
|
114
|
+
const safeCandidates = Object.keys(FONT_METRICS).filter(f => !FONT_SUBSTITUTIONS[f]);
|
|
115
|
+
const fonts = [];
|
|
116
|
+
for (const entry of entries) {
|
|
117
|
+
const name = entry.font?.["@_typeface"];
|
|
118
|
+
if (!name)
|
|
119
|
+
continue;
|
|
120
|
+
let replacement;
|
|
121
|
+
if (FONT_METRICS[name]) {
|
|
122
|
+
const matches = findClosestFont(name, { candidates: safeCandidates, sameCategory: true, limit: 1 });
|
|
123
|
+
if (matches.length > 0)
|
|
124
|
+
replacement = matches[0].font;
|
|
125
|
+
}
|
|
126
|
+
fonts.push({ name, replacement });
|
|
127
|
+
}
|
|
128
|
+
if (fonts.length > 0) {
|
|
129
|
+
const details = fonts.map(f => f.replacement ? `${f.name} → ${f.replacement}` : f.name).join(", ");
|
|
130
|
+
issues.push({
|
|
131
|
+
rule: "embedded-font",
|
|
132
|
+
severity: "warn",
|
|
133
|
+
slide: 0,
|
|
134
|
+
message: `Embedded font${fonts.length > 1 ? "s" : ""}: ${details} — QuickLook ignores embedded fonts, text will render with system substitutes`,
|
|
135
|
+
suggestion: fonts.some(f => f.replacement)
|
|
136
|
+
? `Strip embedded data and replace with cross-platform alternatives`
|
|
137
|
+
: `Remove embedded fonts to reduce file size; use cross-platform fonts instead`,
|
|
138
|
+
fix: { action: "strip-embedded-fonts", fonts },
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
108
142
|
for (let i = 0; i < pres.slides.length; i++) {
|
|
109
143
|
const slide = pres.slides[i];
|
|
110
144
|
const slideNum = i + 1;
|
|
@@ -280,7 +314,10 @@ function lintGraphicFrame(frame, slide, issues, ctx) {
|
|
|
280
314
|
}
|
|
281
315
|
}
|
|
282
316
|
function lintTable(table, slide, element, issues, ctx) {
|
|
283
|
-
// Check for table style reference without explicit borders
|
|
317
|
+
// Check for table style reference without explicit borders.
|
|
318
|
+
// OfficeImport emits `border-style:none` per-cell unless borders are explicit in XML.
|
|
319
|
+
// It ignores tableStyleId entirely — PowerPoint resolves the style and shows borders,
|
|
320
|
+
// but QuickLook shows a borderless grid. This is the #1 table issue for python-pptx users.
|
|
284
321
|
if (table.tableStyleId) {
|
|
285
322
|
const hasMissingBorders = table.rows.some(row => row.cells.some(cell => !hasExplicitBorders(cell)));
|
|
286
323
|
if (hasMissingBorders) {
|
|
@@ -288,25 +325,15 @@ function lintTable(table, slide, element, issues, ctx) {
|
|
|
288
325
|
rule: "table-style-unresolved",
|
|
289
326
|
severity: "warn",
|
|
290
327
|
slide, element,
|
|
291
|
-
message: `Table uses style "${table.tableStyleId}" but
|
|
292
|
-
suggestion: `Inline borders
|
|
328
|
+
message: `Table uses style "${table.tableStyleId}" but cells lack explicit borders — PowerPoint shows borders, QuickLook won't`,
|
|
329
|
+
suggestion: `Inline the style's borders as explicit <a:lnT>/<a:lnB>/<a:lnL>/<a:lnR> on each cell`,
|
|
293
330
|
fix: { action: "inline-borders", tableStyleId: table.tableStyleId },
|
|
294
331
|
});
|
|
295
332
|
}
|
|
296
333
|
}
|
|
297
|
-
//
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
if (noBorderCells > 0 && noBorderCells === totalCells) {
|
|
301
|
-
issues.push({
|
|
302
|
-
rule: "table-missing-borders",
|
|
303
|
-
severity: "warn",
|
|
304
|
-
slide, element,
|
|
305
|
-
message: `All ${totalCells} table cells have no explicit borders — table will appear borderless in QuickLook`,
|
|
306
|
-
suggestion: `Add explicit borders to cells if borders are expected`,
|
|
307
|
-
fix: { action: "add-borders" },
|
|
308
|
-
});
|
|
309
|
-
}
|
|
334
|
+
// Note: tables without a style AND without explicit borders are intentionally borderless.
|
|
335
|
+
// OfficeImport correctly renders them with border-style:none. No lint issue needed.
|
|
336
|
+
// (PowerPoint also shows these as borderless — no divergence.)
|
|
310
337
|
// Lint text in table cells
|
|
311
338
|
for (const row of table.rows) {
|
|
312
339
|
for (const cell of row.cells) {
|
|
@@ -322,12 +349,6 @@ function hasExplicitBorders(cell) {
|
|
|
322
349
|
const b = cell.borders;
|
|
323
350
|
return !!(b.top?.fill || b.bottom?.fill || b.left?.fill || b.right?.fill);
|
|
324
351
|
}
|
|
325
|
-
function hasAnyBorder(cell) {
|
|
326
|
-
if (!cell.borders)
|
|
327
|
-
return false;
|
|
328
|
-
const b = cell.borders;
|
|
329
|
-
return !!(b.top?.width || b.bottom?.width || b.left?.width || b.right?.width);
|
|
330
|
-
}
|
|
331
352
|
// ── Fill checks ─────────────────────────────────────────────────────
|
|
332
353
|
function lintFill(fill, slide, element, issues, ctx) {
|
|
333
354
|
if (!fill || fill.type !== "gradient")
|
|
@@ -444,7 +465,7 @@ export function formatIssues(result) {
|
|
|
444
465
|
for (const issue of result.issues) {
|
|
445
466
|
if (issue.slide !== currentSlide) {
|
|
446
467
|
currentSlide = issue.slide;
|
|
447
|
-
lines.push(`\nSlide ${currentSlide}:`);
|
|
468
|
+
lines.push(currentSlide === 0 ? `\nPresentation:` : `\nSlide ${currentSlide}:`);
|
|
448
469
|
}
|
|
449
470
|
const icon = SEVERITY_ICON[issue.severity];
|
|
450
471
|
const elem = issue.element ? ` (${issue.element})` : "";
|
package/dist/package/package.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "quicklook-pptx-renderer",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
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",
|