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 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 only reads explicit `<a:lnL>`, `<a:lnR>`, `<a:lnT>`, `<a:lnB>` on each cell. python-pptx and most generators rely on style references. |
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
- break;
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-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`) |
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-missing-borders" | "table-style-unresolved" | "group-as-pdf" | "chart-no-fallback" | "text-inscription-shift" | "rotation-forces-pdf" | "geometry-forces-pdf" | "vertical-text";
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 some cells lack explicit borders — OfficeImport may not resolve the style`,
292
- suggestion: `Inline borders explicitly on each cell`,
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
- // Check for cells with no borders at all
298
- const noBorderCells = table.rows.reduce((count, row) => count + row.cells.filter(cell => !cell.hMerge && !cell.vMerge && !hasAnyBorder(cell)).length, 0);
299
- const totalCells = table.rows.reduce((count, row) => count + row.cells.filter(cell => !cell.hMerge && !cell.vMerge).length, 0);
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})` : "";
@@ -8,7 +8,7 @@ const ARRAY_ELEMENTS = new Set([
8
8
  "AlternateContent",
9
9
  "p", "r", "br", "fld",
10
10
  "tr", "tc", "gridCol",
11
- "gs", "gd", "font",
11
+ "gs", "gd", "font", "embeddedFont",
12
12
  "effectStyle",
13
13
  "Relationship", "Default", "Override",
14
14
  ]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "quicklook-pptx-renderer",
3
- "version": "0.3.0",
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",