pptx-fix 0.2.0 → 0.3.1

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
@@ -17,6 +17,7 @@ Common artifacts:
17
17
  - **Gradients become flat colors** — 3+ color stops are averaged to a single solid color
18
18
  - **Drop shadows become opaque blocks** — shapes with effects render as opaque PDF images that cover content behind them
19
19
  - **Fonts shift and text reflows** — Calibri becomes Helvetica Neue, Arial becomes Helvetica, with different metrics
20
+ - **Embedded fonts ignored** — custom fonts embedded in the PPTX are completely ignored by QuickLook, falling back to system substitutes
20
21
 
21
22
  `pptx-fix` rewrites the OOXML XML inside the PPTX ZIP to work around these OfficeImport quirks. The output is a valid `.pptx` that looks correct in both PowerPoint and Apple's preview.
22
23
 
@@ -37,7 +38,7 @@ npm install pptx-fix
37
38
  npx pptx-fix input.pptx -o output.pptx
38
39
 
39
40
  # Apply only specific transforms
40
- npx pptx-fix input.pptx -o output.pptx --only table-styles,gradients
41
+ npx pptx-fix input.pptx -o output.pptx --only table-styles,gradients,fonts
41
42
 
42
43
  # See what was changed
43
44
  npx pptx-fix input.pptx -o output.pptx --report
@@ -77,9 +78,13 @@ for (const issue of issues) {
77
78
  | Transform | Status | What it fixes |
78
79
  |-----------|--------|--------------|
79
80
  | **table-styles** | Done | Resolves `tableStyleId` references and inlines explicit `<a:lnL/R/T/B>` borders on every cell. Handles `firstRow`, `lastRow`, `bandRow` conditional formatting. Only adds borders where none are explicitly defined. |
80
- | **geometries** | Planned | Replace unsupported `<a:prstGeom>` (heart, cloud, lightningBolt, sun, moon, frame, arc, chord, etc.) with `<a:custGeom>` path data. |
81
- | **gradients** | Planned | Collapse 3+ stop gradients to 2-stop (start + end color) so QuickLook renders a gradient instead of a flat color. |
82
- | **effects** | Planned | Strip or adjust shapes with `<effectLst>` (drop shadow, glow, reflection) that render as opaque PDF blocks covering content. |
81
+ | **geometries** | Done | Replaces unsupported `<a:prstGeom>` presets (~120 shapes not in OfficeImport's supported set) with the visually-closest supported alternative (e.g. heart→ellipse, cloud→cloudCallout) so shapes are visible instead of invisible. |
82
+ | **gradients** | Done | Collapses 3+ stop gradients to 2-stop (first + last color) so QuickLook renders a gradient instead of a flat color. |
83
+ | **effects** | Done | Strips `<effectLst>` and `<effectDag>` (drop shadow, glow, reflection) from shape properties to prevent opaque PDF block rendering. |
84
+ | **fonts** | Done | Replaces high-risk Windows fonts (Calibri +14.4%, Segoe UI +14%, Corbel +18.8%, etc.) with metrically-closest cross-platform alternatives to prevent text reflow on macOS. Also fixes fonts in theme XML. |
85
+ | **groups** | Done | Ungroups shape groups so children render individually instead of being merged into a single opaque PDF block. Transforms child coordinates from group-space to slide-space. Skips rotated groups. |
86
+ | **embedded-fonts** | Done | Strips embedded font data (ignored by QuickLook) and replaces font references with the metrically-closest cross-platform alternative. Reduces file size and ensures consistent rendering. |
87
+ | **chart-fallbacks** | Done | Renders charts to PNG and embeds as fallback images so QuickLook displays charts instead of blank rectangles. Requires Playwright (optional — skips silently if not installed). |
83
88
 
84
89
  Detection of all issues (including font substitution, chart fallbacks, text inscription shifts, and more) is handled by [quicklook-pptx-renderer](https://www.npmjs.com/package/quicklook-pptx-renderer)'s 12-rule linter. Run `pptx-fix analyze` to see all issues, or use the linter directly in CI.
85
90
 
@@ -123,7 +128,7 @@ The round-trip preserves all XML elements the tool doesn't explicitly modify —
123
128
  For comprehensive linting with 12 rules, CI integration, cross-platform rendering, and pixel-diff comparison against actual QuickLook output, see [**quicklook-pptx-renderer**](https://www.npmjs.com/package/quicklook-pptx-renderer) — a companion renderer + linter that replicates Apple's QuickLook output pixel for pixel, runs on Linux/Docker without a Mac.
124
129
 
125
130
  ```bash
126
- # Lint (12 rules, JSON output for CI)
131
+ # Lint (JSON output for CI)
127
132
  npx quicklook-pptx lint presentation.pptx --json
128
133
 
129
134
  # Render slides as PNG (see exactly what Mac users see)
package/dist/analyze.d.ts CHANGED
@@ -2,4 +2,4 @@
2
2
  * Analyze a PPTX for issues that will cause QuickLook rendering problems.
3
3
  * Delegates to quicklook-pptx-renderer's linter — single source of truth.
4
4
  */
5
- export { lint as analyze, formatIssues, type LintResult, type LintIssue } from "quicklook-pptx-renderer";
5
+ export { lint as analyze, formatIssues, type LintResult, type LintIssue, type LintFix } from "quicklook-pptx-renderer";
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Chart fallback generator — render charts to PNG and embed as fallback
3
+ * images so QuickLook displays charts instead of blank rectangles.
4
+ *
5
+ * QuickLook (OfficeImport) doesn't parse chart XML — it only uses
6
+ * pre-rendered fallback images stored in the PPTX. Tools like python-pptx
7
+ * and PptxGenJS don't generate these fallback images.
8
+ *
9
+ * Uses Playwright to screenshot chart HTML rendered by quicklook-pptx-renderer.
10
+ * Playwright is a dynamic import — if not installed, this step is skipped.
11
+ */
12
+ import type JSZip from "jszip";
13
+ /**
14
+ * Add fallback images to charts that don't have them.
15
+ * Requires Playwright — skips silently if not installed.
16
+ */
17
+ export declare function addChartFallbacks(zip: JSZip, pptxBuffer: Buffer, reportLines: string[]): Promise<void>;
@@ -0,0 +1,221 @@
1
+ /**
2
+ * Chart fallback generator — render charts to PNG and embed as fallback
3
+ * images so QuickLook displays charts instead of blank rectangles.
4
+ *
5
+ * QuickLook (OfficeImport) doesn't parse chart XML — it only uses
6
+ * pre-rendered fallback images stored in the PPTX. Tools like python-pptx
7
+ * and PptxGenJS don't generate these fallback images.
8
+ *
9
+ * Uses Playwright to screenshot chart HTML rendered by quicklook-pptx-renderer.
10
+ * Playwright is a dynamic import — if not installed, this step is skipped.
11
+ */
12
+ import { XMLParser, XMLBuilder } from "fast-xml-parser";
13
+ const CHART_REL_TYPE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/chart";
14
+ const IMAGE_REL_TYPE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image";
15
+ const relsParserOptions = {
16
+ ignoreAttributes: false,
17
+ attributeNamePrefix: "@_",
18
+ removeNSPrefix: true,
19
+ parseTagValue: false,
20
+ parseAttributeValue: false,
21
+ isArray: (name) => name === "Relationship",
22
+ };
23
+ const relsBuilderOptions = {
24
+ ignoreAttributes: false,
25
+ attributeNamePrefix: "@_",
26
+ format: true,
27
+ suppressEmptyNode: false,
28
+ };
29
+ /** Find charts without fallback images in the PPTX. */
30
+ async function findChartsWithoutFallback(zip) {
31
+ const parser = new XMLParser(relsParserOptions);
32
+ const results = [];
33
+ const slideFiles = Object.keys(zip.files)
34
+ .filter(f => /^ppt\/slides\/slide\d+\.xml$/.test(f))
35
+ .sort();
36
+ for (const slidePath of slideFiles) {
37
+ const slideNum = parseInt(slidePath.match(/\d+/)[0]);
38
+ // Read slide rels
39
+ const slideRelsPath = slidePath.replace("slides/", "slides/_rels/").replace(".xml", ".xml.rels");
40
+ const slideRelsFile = zip.file(slideRelsPath);
41
+ if (!slideRelsFile)
42
+ continue;
43
+ const slideRels = parser.parse(await slideRelsFile.async("string"));
44
+ const rels = slideRels.Relationships?.Relationship ?? [];
45
+ // Find chart relationships
46
+ const chartRels = rels.filter((r) => r["@_Type"]?.includes("/chart") || r["@_Type"] === CHART_REL_TYPE);
47
+ for (const chartRel of chartRels) {
48
+ const target = chartRel["@_Target"];
49
+ if (!target)
50
+ continue;
51
+ // Resolve chart path relative to slide
52
+ const chartPath = target.startsWith("../")
53
+ ? "ppt/" + target.slice(3)
54
+ : "ppt/slides/" + target;
55
+ // Check if chart already has a fallback image
56
+ const chartRelsPath = chartPath.replace(/([^/]+)$/, "_rels/$1.rels");
57
+ const chartRelsFile = zip.file(chartRelsPath);
58
+ if (chartRelsFile) {
59
+ const chartRelsXml = parser.parse(await chartRelsFile.async("string"));
60
+ const chartRelsList = chartRelsXml.Relationships?.Relationship ?? [];
61
+ const hasImage = chartRelsList.some((r) => r["@_Type"]?.includes("/image") || r["@_Type"] === IMAGE_REL_TYPE);
62
+ if (hasImage)
63
+ continue; // Already has fallback
64
+ }
65
+ // Get chart bounds from slide XML
66
+ const slideXmlStr = await zip.file(slidePath).async("string");
67
+ const slideXml = parser.parse(slideXmlStr);
68
+ const bounds = findChartBounds(slideXml, chartRel["@_Id"]);
69
+ if (!bounds)
70
+ continue;
71
+ results.push({ slideNum, chartPath, chartRelsPath, bounds });
72
+ }
73
+ }
74
+ return results;
75
+ }
76
+ /** Find the graphicFrame bounds for a chart by its relationship ID. */
77
+ function findChartBounds(slideXml, rId) {
78
+ const frames = findGraphicFrames(slideXml);
79
+ for (const frame of frames) {
80
+ const chartRef = frame.graphic?.graphicData?.chart;
81
+ if (!chartRef)
82
+ continue;
83
+ if (chartRef["@_id"] === rId || chartRef["@_r:id"] === rId) {
84
+ const xfrm = frame.xfrm;
85
+ if (!xfrm?.off || !xfrm?.ext)
86
+ continue;
87
+ return {
88
+ x: Number(xfrm.off["@_x"] ?? 0),
89
+ y: Number(xfrm.off["@_y"] ?? 0),
90
+ cx: Number(xfrm.ext["@_cx"] ?? 0),
91
+ cy: Number(xfrm.ext["@_cy"] ?? 0),
92
+ };
93
+ }
94
+ }
95
+ return null;
96
+ }
97
+ function findGraphicFrames(node) {
98
+ const results = [];
99
+ if (!node || typeof node !== "object")
100
+ return results;
101
+ if (node.graphicFrame) {
102
+ const frames = Array.isArray(node.graphicFrame) ? node.graphicFrame : [node.graphicFrame];
103
+ results.push(...frames);
104
+ }
105
+ for (const key of Object.keys(node)) {
106
+ if (key.startsWith("@_"))
107
+ continue;
108
+ const children = Array.isArray(node[key]) ? node[key] : [node[key]];
109
+ for (const child of children) {
110
+ results.push(...findGraphicFrames(child));
111
+ }
112
+ }
113
+ return results;
114
+ }
115
+ /** Render a chart to PNG using Playwright and the renderer. */
116
+ async function renderChartToPng(pptxBuffer, chart, chromium) {
117
+ const { PptxPackage, readPresentation, generateHtml } = await import("quicklook-pptx-renderer");
118
+ const pkg = await PptxPackage.open(pptxBuffer);
119
+ const pres = await readPresentation(pkg);
120
+ const { html } = await generateHtml(pres, { pkg });
121
+ const EMU_PER_PX = 12700;
122
+ const slideWidth = pres.slideSize.cx / EMU_PER_PX;
123
+ const slideHeight = pres.slideSize.cy / EMU_PER_PX;
124
+ const browser = await chromium.launch();
125
+ try {
126
+ const page = await browser.newPage({ viewport: { width: Math.ceil(slideWidth), height: Math.ceil(slideHeight) } });
127
+ await page.setContent(html, { waitUntil: "networkidle" });
128
+ // Navigate to the correct slide (slides are stacked vertically in the HTML)
129
+ const slideSelector = `.slide:nth-child(${chart.slideNum})`;
130
+ const slideEl = await page.$(slideSelector);
131
+ if (slideEl)
132
+ await slideEl.scrollIntoViewIfNeeded();
133
+ const clip = {
134
+ x: chart.bounds.x / EMU_PER_PX,
135
+ y: chart.bounds.y / EMU_PER_PX + (chart.slideNum - 1) * slideHeight,
136
+ width: chart.bounds.cx / EMU_PER_PX,
137
+ height: chart.bounds.cy / EMU_PER_PX,
138
+ };
139
+ const png = await page.screenshot({ clip, type: "png" });
140
+ return Buffer.from(png);
141
+ }
142
+ finally {
143
+ await browser.close();
144
+ }
145
+ }
146
+ /** Embed a fallback PNG for a chart into the PPTX zip. */
147
+ async function embedFallback(zip, chart, png, index) {
148
+ const parser = new XMLParser(relsParserOptions);
149
+ const builder = new XMLBuilder(relsBuilderOptions);
150
+ // Add PNG to media folder
151
+ const mediaPath = `ppt/media/chart${index}-fallback.png`;
152
+ zip.file(mediaPath, png);
153
+ // Get or create chart rels
154
+ const chartRelsFile = zip.file(chart.chartRelsPath);
155
+ let relsXml;
156
+ if (chartRelsFile) {
157
+ relsXml = parser.parse(await chartRelsFile.async("string"));
158
+ }
159
+ else {
160
+ relsXml = {
161
+ Relationships: {
162
+ "@_xmlns": "http://schemas.openxmlformats.org/package/2006/relationships",
163
+ Relationship: [],
164
+ },
165
+ };
166
+ }
167
+ if (!relsXml.Relationships)
168
+ relsXml.Relationships = { "@_xmlns": "http://schemas.openxmlformats.org/package/2006/relationships" };
169
+ if (!relsXml.Relationships.Relationship)
170
+ relsXml.Relationships.Relationship = [];
171
+ if (!Array.isArray(relsXml.Relationships.Relationship)) {
172
+ relsXml.Relationships.Relationship = [relsXml.Relationships.Relationship];
173
+ }
174
+ // Compute relative path from chart to media
175
+ const chartDir = chart.chartPath.replace(/[^/]+$/, "");
176
+ const relTarget = "../" + mediaPath.slice(4); // strip "ppt/"
177
+ const rId = `rId${relsXml.Relationships.Relationship.length + 1}`;
178
+ relsXml.Relationships.Relationship.push({
179
+ "@_Id": rId,
180
+ "@_Type": IMAGE_REL_TYPE,
181
+ "@_Target": relTarget,
182
+ });
183
+ zip.file(chart.chartRelsPath, '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n' + builder.build(relsXml));
184
+ // Ensure Content_Types.xml includes png
185
+ const ctFile = zip.file("[Content_Types].xml");
186
+ if (ctFile) {
187
+ let ct = await ctFile.async("string");
188
+ if (!ct.includes('Extension="png"')) {
189
+ ct = ct.replace("</Types>", ' <Default Extension="png" ContentType="image/png"/>\n</Types>');
190
+ zip.file("[Content_Types].xml", ct);
191
+ }
192
+ }
193
+ }
194
+ /**
195
+ * Add fallback images to charts that don't have them.
196
+ * Requires Playwright — skips silently if not installed.
197
+ */
198
+ export async function addChartFallbacks(zip, pptxBuffer, reportLines) {
199
+ let chromium;
200
+ try {
201
+ const pw = await import("playwright");
202
+ chromium = pw.chromium;
203
+ }
204
+ catch {
205
+ return; // Playwright not installed — skip
206
+ }
207
+ const charts = await findChartsWithoutFallback(zip);
208
+ if (charts.length === 0)
209
+ return;
210
+ for (let i = 0; i < charts.length; i++) {
211
+ const chart = charts[i];
212
+ try {
213
+ const png = await renderChartToPng(pptxBuffer, chart, chromium);
214
+ await embedFallback(zip, chart, png, i + 1);
215
+ reportLines.push(`Slide ${chart.slideNum}: added chart fallback image (${png.length} bytes)`);
216
+ }
217
+ catch (err) {
218
+ reportLines.push(`Slide ${chart.slideNum}: chart fallback failed — ${err.message}`);
219
+ }
220
+ }
221
+ }
package/dist/cli.js CHANGED
@@ -25,6 +25,10 @@ function positional() {
25
25
  return out;
26
26
  }
27
27
  async function main() {
28
+ if (hasFlag("--version") || hasFlag("-v")) {
29
+ console.log("0.3.1");
30
+ process.exit(0);
31
+ }
28
32
  const pos = positional();
29
33
  const command = pos[0];
30
34
  if (command === "analyze") {
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Strip embedded fonts from a PPTX and replace with cross-platform alternatives.
3
+ *
4
+ * QuickLook (OfficeImport) ignores embedded fonts entirely — they bloat the
5
+ * file and the text renders with system substitutes anyway. This:
6
+ * 1. Finds the best cross-platform replacement for each embedded font
7
+ * 2. Replaces all font references across slides and themes
8
+ * 3. Removes the font data files, embeddedFontLst, rels, and content types
9
+ */
10
+ import type JSZip from "jszip";
11
+ export declare function stripEmbeddedFonts(zip: JSZip, reportLines: string[]): Promise<void>;
@@ -0,0 +1,170 @@
1
+ /**
2
+ * Strip embedded fonts from a PPTX and replace with cross-platform alternatives.
3
+ *
4
+ * QuickLook (OfficeImport) ignores embedded fonts entirely — they bloat the
5
+ * file and the text renders with system substitutes anyway. This:
6
+ * 1. Finds the best cross-platform replacement for each embedded font
7
+ * 2. Replaces all font references across slides and themes
8
+ * 3. Removes the font data files, embeddedFontLst, rels, and content types
9
+ */
10
+ import { XMLParser, XMLBuilder } from "fast-xml-parser";
11
+ import { FONT_METRICS, FONT_SUBSTITUTIONS, findClosestFont } from "quicklook-pptx-renderer";
12
+ const SAFE_CANDIDATES = Object.keys(FONT_METRICS).filter(f => !FONT_SUBSTITUTIONS[f]);
13
+ const FONT_ELEMENTS = new Set(["latin", "ea", "cs", "sym", "buFont"]);
14
+ const relsParserOptions = {
15
+ ignoreAttributes: false,
16
+ attributeNamePrefix: "@_",
17
+ removeNSPrefix: true,
18
+ parseTagValue: false,
19
+ parseAttributeValue: false,
20
+ isArray: (name) => name === "Relationship",
21
+ };
22
+ const relsBuilderOptions = {
23
+ ignoreAttributes: false,
24
+ attributeNamePrefix: "@_",
25
+ format: true,
26
+ suppressEmptyNode: false,
27
+ };
28
+ const presParserOptions = {
29
+ ignoreAttributes: false,
30
+ attributeNamePrefix: "@_",
31
+ removeNSPrefix: true,
32
+ parseTagValue: false,
33
+ parseAttributeValue: false,
34
+ trimValues: false,
35
+ isArray: (name) => name === "embeddedFont",
36
+ };
37
+ const presBuilderOptions = {
38
+ ignoreAttributes: false,
39
+ attributeNamePrefix: "@_",
40
+ format: false,
41
+ suppressEmptyNode: false,
42
+ suppressBooleanAttributes: false,
43
+ };
44
+ /** Find the best cross-platform replacement for a font name. */
45
+ function findReplacement(fontName) {
46
+ if (!FONT_METRICS[fontName])
47
+ return null;
48
+ const matches = findClosestFont(fontName, {
49
+ candidates: SAFE_CANDIDATES,
50
+ sameCategory: true,
51
+ limit: 1,
52
+ });
53
+ return matches.length > 0 ? matches[0].font : null;
54
+ }
55
+ /** Walk an XML tree and replace typeface attributes matching the replacement map. */
56
+ function replaceTypefaces(node, replacements) {
57
+ if (!node || typeof node !== "object")
58
+ return;
59
+ for (const key of FONT_ELEMENTS) {
60
+ if (!node[key])
61
+ continue;
62
+ const elements = Array.isArray(node[key]) ? node[key] : [node[key]];
63
+ for (const el of elements) {
64
+ const typeface = el["@_typeface"];
65
+ if (typeface && replacements.has(typeface)) {
66
+ el["@_typeface"] = replacements.get(typeface);
67
+ }
68
+ }
69
+ }
70
+ for (const key of Object.keys(node)) {
71
+ if (key.startsWith("@_") || FONT_ELEMENTS.has(key))
72
+ continue;
73
+ const children = Array.isArray(node[key]) ? node[key] : [node[key]];
74
+ for (const child of children) {
75
+ replaceTypefaces(child, replacements);
76
+ }
77
+ }
78
+ }
79
+ export async function stripEmbeddedFonts(zip, reportLines) {
80
+ const presFile = zip.file("ppt/presentation.xml");
81
+ if (!presFile)
82
+ return;
83
+ const parser = new XMLParser(presParserOptions);
84
+ const presXml = parser.parse(await presFile.async("string"));
85
+ const presNode = presXml?.Presentation ?? presXml?.presentation ?? presXml;
86
+ if (!presNode)
87
+ return;
88
+ const embFontLst = presNode.embeddedFontLst?.embeddedFont;
89
+ if (!embFontLst)
90
+ return;
91
+ const entries = Array.isArray(embFontLst) ? embFontLst : [embFontLst];
92
+ if (entries.length === 0)
93
+ return;
94
+ // Build replacement map and collect rIds to remove
95
+ const replacements = new Map();
96
+ const fontNames = [];
97
+ const rIdsToRemove = new Set();
98
+ for (const entry of entries) {
99
+ const typeface = entry.font?.["@_typeface"];
100
+ if (typeface) {
101
+ fontNames.push(typeface);
102
+ const replacement = findReplacement(typeface);
103
+ if (replacement)
104
+ replacements.set(typeface, replacement);
105
+ }
106
+ for (const variant of ["regular", "bold", "italic", "boldItalic"]) {
107
+ const rId = entry[variant]?.["@_id"];
108
+ if (rId)
109
+ rIdsToRemove.add(rId);
110
+ }
111
+ }
112
+ if (rIdsToRemove.size === 0)
113
+ return;
114
+ // Replace font references across slides and themes
115
+ if (replacements.size > 0) {
116
+ const xmlFiles = Object.keys(zip.files).filter(f => /^ppt\/(slides\/slide|theme\/theme)\d+\.xml$/.test(f));
117
+ for (const path of xmlFiles) {
118
+ const xml = await zip.file(path).async("string");
119
+ const parsed = parser.parse(xml);
120
+ replaceTypefaces(parsed, replacements);
121
+ const builder = new XMLBuilder(presBuilderOptions);
122
+ zip.file(path, builder.build(parsed));
123
+ }
124
+ }
125
+ // Remove embeddedFontLst from presentation XML
126
+ delete presNode.embeddedFontLst;
127
+ const builder = new XMLBuilder(presBuilderOptions);
128
+ zip.file("ppt/presentation.xml", builder.build(presXml));
129
+ // Remove font files via presentation rels
130
+ const relsPath = "ppt/_rels/presentation.xml.rels";
131
+ const relsFile = zip.file(relsPath);
132
+ if (relsFile) {
133
+ const relsParser = new XMLParser(relsParserOptions);
134
+ const relsXml = relsParser.parse(await relsFile.async("string"));
135
+ const rels = relsXml?.Relationships?.Relationship ?? [];
136
+ const kept = [];
137
+ for (const rel of rels) {
138
+ if (rIdsToRemove.has(rel["@_Id"])) {
139
+ const target = rel["@_Target"];
140
+ const resolved = target.startsWith("/")
141
+ ? target.slice(1)
142
+ : "ppt/" + (target.startsWith("../") ? target.slice(3) : target);
143
+ zip.remove(resolved);
144
+ }
145
+ else {
146
+ kept.push(rel);
147
+ }
148
+ }
149
+ relsXml.Relationships.Relationship = kept;
150
+ const relsBuilder = new XMLBuilder(relsBuilderOptions);
151
+ zip.file(relsPath, '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n' + relsBuilder.build(relsXml));
152
+ }
153
+ // Clean up Content_Types.xml — remove fntdata entries if no font files remain
154
+ const hasFontFiles = Object.keys(zip.files).some(f => f.endsWith(".fntdata"));
155
+ if (!hasFontFiles) {
156
+ const ctFile = zip.file("[Content_Types].xml");
157
+ if (ctFile) {
158
+ let ct = await ctFile.async("string");
159
+ ct = ct.replace(/<Default[^>]*Extension="fntdata"[^>]*\/>\s*/g, "");
160
+ ct = ct.replace(/<Override[^>]*PartName="[^"]*\/fonts\/[^"]*"[^>]*\/>\s*/g, "");
161
+ zip.file("[Content_Types].xml", ct);
162
+ }
163
+ }
164
+ // Report
165
+ const details = fontNames.map(f => {
166
+ const r = replacements.get(f);
167
+ return r ? `"${f}" → "${r}"` : `"${f}" (stripped)`;
168
+ }).join(", ");
169
+ reportLines.push(`embedded fonts: ${details}`);
170
+ }
package/dist/index.d.ts CHANGED
@@ -6,6 +6,6 @@
6
6
  * output is a valid PPTX that looks correct in Apple's OfficeImport pipeline.
7
7
  */
8
8
  export { fix, type FixOptions, type FixResult } from "./writer.js";
9
- export { analyze, formatIssues, type LintResult, type LintIssue } from "./analyze.js";
9
+ export { analyze, formatIssues, type LintResult, type LintIssue, type LintFix } from "./analyze.js";
10
10
  export { type TransformName, ALL_TRANSFORMS } from "./transforms/index.js";
11
- export { FONT_METRICS, findClosestFont, widthDelta, type FontMetrics, type FontMatch, type FontCategory } from "quicklook-pptx-renderer";
11
+ export { FONT_METRICS, FONT_SUBSTITUTIONS, SUPPORTED_GEOMETRIES, GEOMETRY_FALLBACKS, findClosestFont, widthDelta, type FontMetrics, type FontMatch, type FontCategory, } from "quicklook-pptx-renderer";
package/dist/index.js CHANGED
@@ -8,4 +8,4 @@
8
8
  export { fix } from "./writer.js";
9
9
  export { analyze, formatIssues } from "./analyze.js";
10
10
  export { ALL_TRANSFORMS } from "./transforms/index.js";
11
- export { FONT_METRICS, findClosestFont, widthDelta } from "quicklook-pptx-renderer";
11
+ export { FONT_METRICS, FONT_SUBSTITUTIONS, SUPPORTED_GEOMETRIES, GEOMETRY_FALLBACKS, findClosestFont, widthDelta, } from "quicklook-pptx-renderer";
@@ -1,6 +1,6 @@
1
1
  /**
2
- * effects transform — Strip or adjust shapes with effectLst that render
3
- * as opaque PDF blocks in QuickLook.
2
+ * effects transform — Strip effectLst/effectDag from shape properties so
3
+ * shapes render as CSS instead of opaque PDF blocks in QuickLook.
4
4
  *
5
5
  * Detection is handled by quicklook-pptx-renderer's linter (opaque-pdf-block rule).
6
6
  */
@@ -1,13 +1,53 @@
1
1
  /**
2
- * effects transform — Strip or adjust shapes with effectLst that render
3
- * as opaque PDF blocks in QuickLook.
2
+ * effects transform — Strip effectLst/effectDag from shape properties so
3
+ * shapes render as CSS instead of opaque PDF blocks in QuickLook.
4
4
  *
5
5
  * Detection is handled by quicklook-pptx-renderer's linter (opaque-pdf-block rule).
6
6
  */
7
+ function stripEffects(node, changes) {
8
+ if (!node || typeof node !== "object")
9
+ return;
10
+ // Shape elements: sp, cxnSp, pic
11
+ const shapeKeys = ["sp", "cxnSp", "pic"];
12
+ for (const key of shapeKeys) {
13
+ if (!node[key])
14
+ continue;
15
+ const shapes = Array.isArray(node[key]) ? node[key] : [node[key]];
16
+ for (const shape of shapes) {
17
+ const spPr = shape.spPr;
18
+ if (!spPr)
19
+ continue;
20
+ const name = shape.nvSpPr?.cNvPr?.["@_name"]
21
+ ?? shape.nvCxnSpPr?.cNvPr?.["@_name"]
22
+ ?? shape.nvPicPr?.cNvPr?.["@_name"]
23
+ ?? "shape";
24
+ if (spPr.effectLst) {
25
+ delete spPr.effectLst;
26
+ changes.push(`stripped effects from "${name}"`);
27
+ }
28
+ if (spPr.effectDag) {
29
+ delete spPr.effectDag;
30
+ changes.push(`stripped effect DAG from "${name}"`);
31
+ }
32
+ }
33
+ }
34
+ // Groups: recurse into grpSp children
35
+ if (node.grpSp) {
36
+ const groups = Array.isArray(node.grpSp) ? node.grpSp : [node.grpSp];
37
+ for (const group of groups) {
38
+ stripEffects(group, changes);
39
+ }
40
+ }
41
+ // Recurse into spTree (the slide's shape tree)
42
+ if (node.spTree) {
43
+ stripEffects(node.spTree, changes);
44
+ }
45
+ }
7
46
  export const effects = {
8
47
  name: "effects",
9
- apply(_slideXml, _slideNum, _ctx) {
10
- // TODO: configurable — strip effects, reorder z-index, or no-op
11
- return { changed: false, changes: [] };
48
+ apply(slideXml, _slideNum, _ctx) {
49
+ const changes = [];
50
+ stripEffects(slideXml, changes);
51
+ return { changed: changes.length > 0, changes };
12
52
  },
13
53
  };
@@ -1,6 +1,13 @@
1
1
  /**
2
- * fonts transform — Add explicit <a:latin>, <a:ea>, <a:cs> fallback
3
- * typefaces matching what OfficeImport's TCFontUtils would pick.
2
+ * fonts transform — Replace high-risk Windows fonts with metrically-closest
3
+ * cross-platform alternatives to prevent text reflow on macOS.
4
+ *
5
+ * macOS substitutes Windows fonts (Calibri → Helvetica Neue +14.4%, etc.)
6
+ * with different-width fonts, causing text overflow and line breaks to shift.
7
+ * This replaces those fonts with cross-platform safe fonts that have minimal
8
+ * width delta on macOS, chosen by the findClosestFont similarity algorithm.
9
+ *
10
+ * Detection is handled by quicklook-pptx-renderer's linter (font-substitution rule).
4
11
  */
5
12
  import type { Transform } from "./index.js";
6
13
  export declare const fonts: Transform;
@@ -1,15 +1,75 @@
1
1
  /**
2
- * fonts transform — Add explicit <a:latin>, <a:ea>, <a:cs> fallback
3
- * typefaces matching what OfficeImport's TCFontUtils would pick.
2
+ * fonts transform — Replace high-risk Windows fonts with metrically-closest
3
+ * cross-platform alternatives to prevent text reflow on macOS.
4
+ *
5
+ * macOS substitutes Windows fonts (Calibri → Helvetica Neue +14.4%, etc.)
6
+ * with different-width fonts, causing text overflow and line breaks to shift.
7
+ * This replaces those fonts with cross-platform safe fonts that have minimal
8
+ * width delta on macOS, chosen by the findClosestFont similarity algorithm.
9
+ *
10
+ * Detection is handled by quicklook-pptx-renderer's linter (font-substitution rule).
4
11
  */
12
+ import { FONT_METRICS, FONT_SUBSTITUTIONS, findClosestFont, widthDelta } from "quicklook-pptx-renderer";
13
+ const DELTA_THRESHOLD = 10;
14
+ // Fonts available on macOS — everything in the metrics DB except Windows-only
15
+ const SAFE_CANDIDATES = Object.keys(FONT_METRICS).filter(f => !FONT_SUBSTITUTIONS[f]);
16
+ const replacementCache = new Map();
17
+ function getReplacement(fontName) {
18
+ if (replacementCache.has(fontName))
19
+ return replacementCache.get(fontName);
20
+ const macSub = FONT_SUBSTITUTIONS[fontName];
21
+ const srcMetrics = FONT_METRICS[fontName];
22
+ const subMetrics = macSub ? FONT_METRICS[macSub] : undefined;
23
+ if (!macSub || !srcMetrics || !subMetrics || Math.abs(widthDelta(srcMetrics, subMetrics)) < DELTA_THRESHOLD) {
24
+ replacementCache.set(fontName, null);
25
+ return null;
26
+ }
27
+ const matches = findClosestFont(fontName, {
28
+ candidates: SAFE_CANDIDATES,
29
+ sameCategory: true,
30
+ limit: 1,
31
+ });
32
+ const result = matches.length > 0 ? matches[0].font : null;
33
+ replacementCache.set(fontName, result);
34
+ return result;
35
+ }
36
+ const FONT_ELEMENTS = new Set(["latin", "ea", "cs", "sym", "buFont"]);
37
+ function replaceFonts(node, seen, changes) {
38
+ if (!node || typeof node !== "object")
39
+ return;
40
+ for (const key of FONT_ELEMENTS) {
41
+ if (!node[key])
42
+ continue;
43
+ const elements = Array.isArray(node[key]) ? node[key] : [node[key]];
44
+ for (const el of elements) {
45
+ const typeface = el["@_typeface"];
46
+ if (!typeface || typeface.startsWith("+"))
47
+ continue;
48
+ const replacement = getReplacement(typeface);
49
+ if (replacement) {
50
+ el["@_typeface"] = replacement;
51
+ if (!seen.has(typeface)) {
52
+ seen.add(typeface);
53
+ changes.push(`replaced font "${typeface}" ��� "${replacement}"`);
54
+ }
55
+ }
56
+ }
57
+ }
58
+ for (const key of Object.keys(node)) {
59
+ if (key.startsWith("@_") || FONT_ELEMENTS.has(key))
60
+ continue;
61
+ const children = Array.isArray(node[key]) ? node[key] : [node[key]];
62
+ for (const child of children) {
63
+ replaceFonts(child, seen, changes);
64
+ }
65
+ }
66
+ }
5
67
  export const fonts = {
6
68
  name: "fonts",
7
- detect(_slideXml, _slideNum) {
8
- // TODO: detect text runs missing explicit font declarations
9
- return [];
10
- },
11
- apply(_slideXml, _slideNum, _ctx) {
12
- // TODO: implement — add fallback typefaces
13
- return { changed: false, changes: [] };
69
+ apply(slideXml, _slideNum, _ctx) {
70
+ const changes = [];
71
+ const seen = new Set();
72
+ replaceFonts(slideXml, seen, changes);
73
+ return { changed: changes.length > 0, changes };
14
74
  },
15
75
  };
@@ -1,6 +1,6 @@
1
1
  /**
2
- * geometries transform — Replace unsupported preset geometries with
3
- * equivalent <a:custGeom> path data.
2
+ * geometries transform — Replace unsupported preset geometries with the
3
+ * visually-closest supported alternative so shapes are visible in QuickLook.
4
4
  *
5
5
  * Detection is handled by quicklook-pptx-renderer's linter (unsupported-geometry rule).
6
6
  */
@@ -1,13 +1,43 @@
1
1
  /**
2
- * geometries transform — Replace unsupported preset geometries with
3
- * equivalent <a:custGeom> path data.
2
+ * geometries transform — Replace unsupported preset geometries with the
3
+ * visually-closest supported alternative so shapes are visible in QuickLook.
4
4
  *
5
5
  * Detection is handled by quicklook-pptx-renderer's linter (unsupported-geometry rule).
6
6
  */
7
+ import { SUPPORTED_GEOMETRIES, GEOMETRY_FALLBACKS } from "quicklook-pptx-renderer";
8
+ function fixGeometries(node, changes) {
9
+ if (!node || typeof node !== "object")
10
+ return;
11
+ if (node.sp) {
12
+ const shapes = Array.isArray(node.sp) ? node.sp : [node.sp];
13
+ for (const shape of shapes) {
14
+ const spPr = shape.spPr;
15
+ if (!spPr?.prstGeom)
16
+ continue;
17
+ const prst = spPr.prstGeom["@_prst"];
18
+ if (!prst || SUPPORTED_GEOMETRIES.has(prst))
19
+ continue;
20
+ const fallback = GEOMETRY_FALLBACKS[prst] ?? "rect";
21
+ const name = shape.nvSpPr?.cNvPr?.["@_name"] ?? "shape";
22
+ spPr.prstGeom["@_prst"] = fallback;
23
+ changes.push(`replaced unsupported geometry "${prst}" with ${fallback} on "${name}"`);
24
+ }
25
+ }
26
+ if (node.grpSp) {
27
+ const groups = Array.isArray(node.grpSp) ? node.grpSp : [node.grpSp];
28
+ for (const group of groups) {
29
+ fixGeometries(group, changes);
30
+ }
31
+ }
32
+ if (node.spTree) {
33
+ fixGeometries(node.spTree, changes);
34
+ }
35
+ }
7
36
  export const geometries = {
8
37
  name: "geometries",
9
- apply(_slideXml, _slideNum, _ctx) {
10
- // TODO: replace prstGeom with custGeom containing equivalent path data
11
- return { changed: false, changes: [] };
38
+ apply(slideXml, _slideNum, _ctx) {
39
+ const changes = [];
40
+ fixGeometries(slideXml, changes);
41
+ return { changed: changes.length > 0, changes };
12
42
  },
13
43
  };
@@ -4,10 +4,36 @@
4
4
  *
5
5
  * Detection is handled by quicklook-pptx-renderer's linter (gradient-flattened rule).
6
6
  */
7
+ function collapseGradients(node, changes, slideNum) {
8
+ if (!node || typeof node !== "object")
9
+ return;
10
+ if (node.gradFill) {
11
+ const fills = Array.isArray(node.gradFill) ? node.gradFill : [node.gradFill];
12
+ for (const fill of fills) {
13
+ const gsLst = fill.gsLst;
14
+ if (!gsLst)
15
+ continue;
16
+ const stops = gsLst.gs;
17
+ if (!Array.isArray(stops) || stops.length < 3)
18
+ continue;
19
+ gsLst.gs = [stops[0], stops[stops.length - 1]];
20
+ changes.push(`collapsed ${stops.length}-stop gradient to 2 stops`);
21
+ }
22
+ }
23
+ for (const key of Object.keys(node)) {
24
+ if (key.startsWith("@_"))
25
+ continue;
26
+ const children = Array.isArray(node[key]) ? node[key] : [node[key]];
27
+ for (const child of children) {
28
+ collapseGradients(child, changes, slideNum);
29
+ }
30
+ }
31
+ }
7
32
  export const gradients = {
8
33
  name: "gradients",
9
- apply(_slideXml, _slideNum, _ctx) {
10
- // TODO: collapse 3+ stop gradients to 2-stop (endpoints only)
11
- return { changed: false, changes: [] };
34
+ apply(slideXml, slideNum, _ctx) {
35
+ const changes = [];
36
+ collapseGradients(slideXml, changes, slideNum);
37
+ return { changed: changes.length > 0, changes };
12
38
  },
13
39
  };
@@ -0,0 +1,12 @@
1
+ /**
2
+ * groups transform — Ungroup shape groups so children render individually
3
+ * instead of being merged into a single opaque PDF block by QuickLook.
4
+ *
5
+ * Coordinate math: each child's position is relative to the group's
6
+ * chOff/chExt (child coordinate space). To ungroup, we transform each
7
+ * child's coordinates from group-space to slide-space.
8
+ *
9
+ * Detection is handled by quicklook-pptx-renderer's linter (group-as-pdf rule).
10
+ */
11
+ import type { Transform } from "./index.js";
12
+ export declare const groups: Transform;
@@ -0,0 +1,103 @@
1
+ /**
2
+ * groups transform — Ungroup shape groups so children render individually
3
+ * instead of being merged into a single opaque PDF block by QuickLook.
4
+ *
5
+ * Coordinate math: each child's position is relative to the group's
6
+ * chOff/chExt (child coordinate space). To ungroup, we transform each
7
+ * child's coordinates from group-space to slide-space.
8
+ *
9
+ * Detection is handled by quicklook-pptx-renderer's linter (group-as-pdf rule).
10
+ */
11
+ const CHILD_KEYS = ["sp", "pic", "cxnSp", "graphicFrame"];
12
+ function getXfrm(child, key) {
13
+ if (key === "graphicFrame")
14
+ return child.xfrm;
15
+ return child.spPr?.xfrm;
16
+ }
17
+ function transformCoords(xfrm, groupOff, groupExt, chOff, chExt) {
18
+ const off = xfrm.off;
19
+ const ext = xfrm.ext;
20
+ if (!off || !ext)
21
+ return;
22
+ const scaleX = chExt.cx > 0 ? groupExt.cx / chExt.cx : 1;
23
+ const scaleY = chExt.cy > 0 ? groupExt.cy / chExt.cy : 1;
24
+ off["@_x"] = String(Math.round(groupOff.x + (Number(off["@_x"] ?? 0) - chOff.x) * scaleX));
25
+ off["@_y"] = String(Math.round(groupOff.y + (Number(off["@_y"] ?? 0) - chOff.y) * scaleY));
26
+ ext["@_cx"] = String(Math.round(Number(ext["@_cx"] ?? 0) * scaleX));
27
+ ext["@_cy"] = String(Math.round(Number(ext["@_cy"] ?? 0) * scaleY));
28
+ }
29
+ function ensureArray(parent, key) {
30
+ if (!parent[key])
31
+ parent[key] = [];
32
+ if (!Array.isArray(parent[key]))
33
+ parent[key] = [parent[key]];
34
+ return parent[key];
35
+ }
36
+ function ungroupInto(parent, changes) {
37
+ if (!parent.grpSp)
38
+ return;
39
+ const groups = Array.isArray(parent.grpSp) ? parent.grpSp : [parent.grpSp];
40
+ const kept = [];
41
+ for (const group of groups) {
42
+ // Recurse into nested groups first (ungroup from inside out)
43
+ ungroupInto(group, changes);
44
+ const xfrm = group.grpSpPr?.xfrm;
45
+ if (!xfrm?.off || !xfrm?.ext || !xfrm?.chOff || !xfrm?.chExt) {
46
+ kept.push(group);
47
+ continue;
48
+ }
49
+ // Skip groups with rotation — ungrouping would lose the visual effect
50
+ if (xfrm["@_rot"] && Number(xfrm["@_rot"]) !== 0) {
51
+ kept.push(group);
52
+ continue;
53
+ }
54
+ const groupOff = { x: Number(xfrm.off["@_x"] ?? 0), y: Number(xfrm.off["@_y"] ?? 0) };
55
+ const groupExt = { cx: Number(xfrm.ext["@_cx"] ?? 1), cy: Number(xfrm.ext["@_cy"] ?? 1) };
56
+ const chOff = { x: Number(xfrm.chOff["@_x"] ?? 0), y: Number(xfrm.chOff["@_y"] ?? 0) };
57
+ const chExt = { cx: Number(xfrm.chExt["@_cx"] ?? 1), cy: Number(xfrm.chExt["@_cy"] ?? 1) };
58
+ const name = group.nvGrpSpPr?.cNvPr?.["@_name"] ?? "group";
59
+ let childCount = 0;
60
+ for (const key of CHILD_KEYS) {
61
+ if (!group[key])
62
+ continue;
63
+ const children = Array.isArray(group[key]) ? group[key] : [group[key]];
64
+ const target = ensureArray(parent, key);
65
+ for (const child of children) {
66
+ const childXfrm = getXfrm(child, key);
67
+ if (childXfrm)
68
+ transformCoords(childXfrm, groupOff, groupExt, chOff, chExt);
69
+ target.push(child);
70
+ childCount++;
71
+ }
72
+ }
73
+ // Any nested groups that couldn't be ungrouped stay as top-level groups
74
+ if (group.grpSp) {
75
+ const nested = Array.isArray(group.grpSp) ? group.grpSp : [group.grpSp];
76
+ for (const ng of nested) {
77
+ const ngXfrm = ng.grpSpPr?.xfrm;
78
+ if (ngXfrm)
79
+ transformCoords(ngXfrm, groupOff, groupExt, chOff, chExt);
80
+ kept.push(ng);
81
+ }
82
+ }
83
+ if (childCount > 0) {
84
+ changes.push(`ungrouped "${name}" (${childCount} children)`);
85
+ }
86
+ }
87
+ if (kept.length > 0) {
88
+ parent.grpSp = kept;
89
+ }
90
+ else {
91
+ delete parent.grpSp;
92
+ }
93
+ }
94
+ export const groups = {
95
+ name: "groups",
96
+ apply(slideXml, _slideNum, _ctx) {
97
+ const changes = [];
98
+ const spTree = slideXml?.sld?.cSld?.spTree ?? slideXml?.cSld?.spTree;
99
+ if (spTree)
100
+ ungroupInto(spTree, changes);
101
+ return { changed: changes.length > 0, changes };
102
+ },
103
+ };
@@ -5,7 +5,7 @@
5
5
  * Detection is handled by quicklook-pptx-renderer's linter.
6
6
  * These transforms only apply fixes.
7
7
  */
8
- export type TransformName = "table-styles" | "gradients" | "geometries" | "effects";
8
+ export type TransformName = "table-styles" | "gradients" | "geometries" | "effects" | "fonts" | "groups" | "embedded-fonts";
9
9
  export interface TransformContext {
10
10
  tableStyleXml?: any;
11
11
  }
@@ -9,9 +9,13 @@ import { tableStyles } from "./table-styles.js";
9
9
  import { gradients } from "./gradients.js";
10
10
  import { geometries } from "./geometries.js";
11
11
  import { effects } from "./effects.js";
12
+ import { fonts } from "./fonts.js";
13
+ import { groups } from "./groups.js";
12
14
  export const ALL_TRANSFORMS = [
13
15
  tableStyles,
14
16
  gradients,
15
17
  geometries,
16
18
  effects,
19
+ fonts,
20
+ groups,
17
21
  ];
package/dist/writer.js CHANGED
@@ -8,12 +8,15 @@ import JSZip from "jszip";
8
8
  import { XMLParser, XMLBuilder } from "fast-xml-parser";
9
9
  import { xmlParserOptions, xmlBuilderOptions } from "./xml.js";
10
10
  import { ALL_TRANSFORMS } from "./transforms/index.js";
11
+ import { addChartFallbacks } from "./chart-fallbacks.js";
12
+ import { stripEmbeddedFonts } from "./embedded-fonts.js";
11
13
  export async function fix(pptxBuffer, options) {
12
14
  const zip = await JSZip.loadAsync(pptxBuffer);
13
15
  const parser = new XMLParser(xmlParserOptions);
14
16
  const builder = new XMLBuilder(xmlBuilderOptions);
15
17
  const reportLines = [];
16
- const enabledNames = new Set(options?.transforms ?? ALL_TRANSFORMS.map(t => t.name));
18
+ const allNames = [...ALL_TRANSFORMS.map(t => t.name), "embedded-fonts"];
19
+ const enabledNames = new Set(options?.transforms ?? allNames);
17
20
  const enabled = ALL_TRANSFORMS.filter(t => enabledNames.has(t.name));
18
21
  // Find all slide XML files
19
22
  const slideFiles = Object.keys(zip.files)
@@ -51,6 +54,28 @@ export async function fix(pptxBuffer, options) {
51
54
  zip.file(slidePath, builder.build(parsed));
52
55
  }
53
56
  }
57
+ // Process theme XML for font replacement
58
+ if (enabledNames.has("fonts")) {
59
+ const fontsTransform = enabled.find(t => t.name === "fonts");
60
+ const themeFiles = Object.keys(zip.files).filter(f => /^ppt\/theme\/theme\d+\.xml$/.test(f));
61
+ for (const themePath of themeFiles) {
62
+ const xml = await zip.file(themePath).async("string");
63
+ const parsed = parser.parse(xml);
64
+ const result = fontsTransform.apply(parsed, 0, { tableStyleXml });
65
+ if (result.changed) {
66
+ zip.file(themePath, builder.build(parsed));
67
+ for (const line of result.changes) {
68
+ reportLines.push(`Theme: ${line}`);
69
+ }
70
+ }
71
+ }
72
+ }
73
+ // Strip embedded fonts (QuickLook ignores them, they just bloat the file)
74
+ if (enabledNames.has("embedded-fonts")) {
75
+ await stripEmbeddedFonts(zip, reportLines);
76
+ }
77
+ // Generate chart fallback images (requires Playwright — skips if not installed)
78
+ await addChartFallbacks(zip, pptxBuffer, reportLines);
54
79
  const outBuffer = Buffer.from(await zip.generateAsync({ type: "nodebuffer" }));
55
80
  const result = { buffer: outBuffer };
56
81
  if (options?.report) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pptx-fix",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "Fix PowerPoint files that render incorrectly on macOS QuickLook and iOS — resolves missing table borders, invisible shapes, flattened gradients, and opaque blocks caused by OfficeImport rendering gaps.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -61,7 +61,7 @@
61
61
  "dependencies": {
62
62
  "fast-xml-parser": "^5.2.0",
63
63
  "jszip": "^3.10.1",
64
- "quicklook-pptx-renderer": "^0.2.1"
64
+ "quicklook-pptx-renderer": "^0.3.2"
65
65
  },
66
66
  "devDependencies": {
67
67
  "@types/node": "^22.0.0",