pptx-fix 0.2.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 ADDED
@@ -0,0 +1,149 @@
1
+ # pptx-fix
2
+
3
+ **Fix PowerPoint files that render incorrectly on macOS QuickLook and iOS.** Rewrites PPTX internals so tables, shapes, gradients, and effects display correctly on Apple devices — no manual editing needed.
4
+
5
+ If your `.pptx` looks perfect in PowerPoint but looks wrong when someone opens it on a Mac or iPhone, this tool fixes it.
6
+
7
+ ---
8
+
9
+ ## The Problem
10
+
11
+ Apple's QuickLook (macOS Finder spacebar preview), iOS Files app, and iPadOS use a private rendering engine called `OfficeImport.framework` — a completely independent OOXML parser that renders slides differently than Microsoft PowerPoint. Presentations created by python-pptx, PptxGenJS, Google Slides, Canva, LibreOffice, Pandoc, and other tools contain patterns that PowerPoint handles fine but OfficeImport renders incorrectly.
12
+
13
+ Common artifacts:
14
+
15
+ - **Table borders vanish** — generators set a table style reference, but OfficeImport only reads explicit border properties
16
+ - **Shapes disappear** — ~120 preset geometries (heart, cloud, lightningBolt, sun, moon...) are silently dropped
17
+ - **Gradients become flat colors** — 3+ color stops are averaged to a single solid color
18
+ - **Drop shadows become opaque blocks** — shapes with effects render as opaque PDF images that cover content behind them
19
+ - **Fonts shift and text reflows** — Calibri becomes Helvetica Neue, Arial becomes Helvetica, with different metrics
20
+
21
+ `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
+ ---
24
+
25
+ ## Install
26
+
27
+ ```bash
28
+ npm install pptx-fix
29
+ ```
30
+
31
+ ## Usage
32
+
33
+ ### CLI
34
+
35
+ ```bash
36
+ # Fix a PPTX file
37
+ npx pptx-fix input.pptx -o output.pptx
38
+
39
+ # Apply only specific transforms
40
+ npx pptx-fix input.pptx -o output.pptx --only table-styles,gradients
41
+
42
+ # See what was changed
43
+ npx pptx-fix input.pptx -o output.pptx --report
44
+
45
+ # Analyze without fixing (dry run)
46
+ npx pptx-fix analyze input.pptx
47
+ ```
48
+
49
+ ### As a Library
50
+
51
+ ```typescript
52
+ import { fix } from "pptx-fix";
53
+ import { readFileSync, writeFileSync } from "fs";
54
+
55
+ const input = readFileSync("presentation.pptx");
56
+ const result = await fix(input, { report: true });
57
+
58
+ writeFileSync("fixed.pptx", result.buffer);
59
+ console.log(result.report);
60
+ ```
61
+
62
+ ### Analyze Only
63
+
64
+ ```typescript
65
+ import { analyze } from "pptx-fix";
66
+
67
+ const issues = await analyze(pptxBuffer);
68
+ for (const issue of issues) {
69
+ console.log(`[${issue.severity}] Slide ${issue.slide}: ${issue.description}`);
70
+ }
71
+ ```
72
+
73
+ ---
74
+
75
+ ## Transforms
76
+
77
+ | Transform | Status | What it fixes |
78
+ |-----------|--------|--------------|
79
+ | **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. |
83
+
84
+ 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
+
86
+ ---
87
+
88
+ ## Which Tools Produce Affected Files
89
+
90
+ | Tool | Primary issue on Mac/iPhone |
91
+ |------|---|
92
+ | **python-pptx** | Tables render without borders — the #1 reported issue. Style references not resolved by OfficeImport. |
93
+ | **PptxGenJS** | Missing thumbnails, shape rendering differences, effect artifacts |
94
+ | **Google Slides** export | Font substitution, formatting shifts, missing content types |
95
+ | **Canva** export | Fonts substituted, layout differences, animation artifacts |
96
+ | **LibreOffice Impress** | Table styles unresolved, gradient rendering differences |
97
+ | **Pandoc** / **Quarto** | Content type corruption, missing shapes, "PowerPoint found a problem with content" errors |
98
+ | **Apache POI** | Content type errors (`InvalidFormatException: Package should contain a content type part [M1.13]`) |
99
+ | **Aspose.Slides** | Missing thumbnail if `refresh_thumbnail` not called |
100
+ | **Open XML SDK** | Repair errors, missing relationships |
101
+
102
+ ---
103
+
104
+ ## How It Works
105
+
106
+ ```
107
+ PPTX (ZIP) → extract XML → parse → detect issues → apply transforms → serialize → repack ZIP
108
+ ```
109
+
110
+ 1. **Extract** — JSZip opens the PPTX (which is a ZIP archive)
111
+ 2. **Parse** — fast-xml-parser converts slide XML to objects, preserving all unknown elements
112
+ 3. **Detect** — each transform scans for its class of issues
113
+ 4. **Apply** — transforms mutate the XML objects (e.g., inline borders from table style definitions)
114
+ 5. **Serialize** — XMLBuilder converts back to XML
115
+ 6. **Repack** — JSZip produces a new valid PPTX
116
+
117
+ The round-trip preserves all XML elements the tool doesn't explicitly modify — the output is always a valid PPTX.
118
+
119
+ ---
120
+
121
+ ## Detecting Issues Without Fixing
122
+
123
+ 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
+
125
+ ```bash
126
+ # Lint (12 rules, JSON output for CI)
127
+ npx quicklook-pptx lint presentation.pptx --json
128
+
129
+ # Render slides as PNG (see exactly what Mac users see)
130
+ npx quicklook-pptx render presentation.pptx --out ./slides/
131
+
132
+ # Fix (this package)
133
+ npx pptx-fix presentation.pptx -o fixed.pptx
134
+ ```
135
+
136
+ ---
137
+
138
+ ## Dependencies
139
+
140
+ | Package | Purpose |
141
+ |---------|---------|
142
+ | [jszip](https://stuk.github.io/jszip/) | ZIP extraction/repacking |
143
+ | [fast-xml-parser](https://github.com/NaturalIntelligence/fast-xml-parser) | OOXML XML parsing and serialization |
144
+
145
+ Zero native dependencies. Works everywhere Node.js 20+ runs.
146
+
147
+ ## License
148
+
149
+ MIT
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Analyze a PPTX for issues that will cause QuickLook rendering problems.
3
+ * Delegates to quicklook-pptx-renderer's linter — single source of truth.
4
+ */
5
+ export { lint as analyze, formatIssues, type LintResult, type LintIssue } from "quicklook-pptx-renderer";
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Analyze a PPTX for issues that will cause QuickLook rendering problems.
3
+ * Delegates to quicklook-pptx-renderer's linter — single source of truth.
4
+ */
5
+ export { lint as analyze, formatIssues } from "quicklook-pptx-renderer";
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env node
2
+ import { readFile, writeFile } from "node:fs/promises";
3
+ import { resolve } from "node:path";
4
+ import { fix } from "./writer.js";
5
+ import { analyze, formatIssues } from "./analyze.js";
6
+ const args = process.argv.slice(2);
7
+ function getFlag(name) {
8
+ const i = args.indexOf(name);
9
+ if (i === -1 || i + 1 >= args.length)
10
+ return undefined;
11
+ return args[i + 1];
12
+ }
13
+ function hasFlag(name) {
14
+ return args.includes(name);
15
+ }
16
+ function positional() {
17
+ const out = [];
18
+ for (let i = 0; i < args.length; i++) {
19
+ if (args[i].startsWith("--") || args[i] === "-o") {
20
+ i++;
21
+ continue;
22
+ }
23
+ out.push(args[i]);
24
+ }
25
+ return out;
26
+ }
27
+ async function main() {
28
+ const pos = positional();
29
+ const command = pos[0];
30
+ if (command === "analyze") {
31
+ const input = pos[1];
32
+ if (!input) {
33
+ console.error("Usage: pptx-fix analyze <input.pptx>");
34
+ process.exit(1);
35
+ }
36
+ const buf = await readFile(resolve(input));
37
+ const result = await analyze(buf);
38
+ console.log(formatIssues(result));
39
+ process.exit(result.summary.errors > 0 ? 1 : 0);
40
+ }
41
+ // Default: fix
42
+ const input = command;
43
+ if (!input || input.startsWith("-")) {
44
+ console.error("Usage: pptx-fix <input.pptx> -o <output.pptx> [--only table-styles,gradients] [--report]");
45
+ console.error(" pptx-fix analyze <input.pptx>");
46
+ process.exit(1);
47
+ }
48
+ const outPath = getFlag("-o") ?? getFlag("--out");
49
+ if (!outPath) {
50
+ console.error("Error: output path required (-o <output.pptx>)");
51
+ process.exit(1);
52
+ }
53
+ const onlyStr = getFlag("--only");
54
+ const transforms = onlyStr
55
+ ? onlyStr.split(",").map(s => s.trim())
56
+ : undefined;
57
+ const wantReport = hasFlag("--report");
58
+ const buf = await readFile(resolve(input));
59
+ const result = await fix(buf, { transforms, report: wantReport });
60
+ await writeFile(resolve(outPath), result.buffer);
61
+ if (wantReport && result.report) {
62
+ console.log(result.report);
63
+ }
64
+ console.log(`Fixed → ${outPath}`);
65
+ }
66
+ main().catch(err => {
67
+ console.error(err);
68
+ process.exit(1);
69
+ });
@@ -0,0 +1,11 @@
1
+ /**
2
+ * pptx-fix — Fix PPTX files for macOS QuickLook / iOS preview rendering.
3
+ *
4
+ * Detection delegates to quicklook-pptx-renderer's linter.
5
+ * Transforms operate on raw XML (preserving all unknown elements) so the
6
+ * output is a valid PPTX that looks correct in Apple's OfficeImport pipeline.
7
+ */
8
+ export { fix, type FixOptions, type FixResult } from "./writer.js";
9
+ export { analyze, formatIssues, type LintResult, type LintIssue } from "./analyze.js";
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";
package/dist/index.js ADDED
@@ -0,0 +1,11 @@
1
+ /**
2
+ * pptx-fix — Fix PPTX files for macOS QuickLook / iOS preview rendering.
3
+ *
4
+ * Detection delegates to quicklook-pptx-renderer's linter.
5
+ * Transforms operate on raw XML (preserving all unknown elements) so the
6
+ * output is a valid PPTX that looks correct in Apple's OfficeImport pipeline.
7
+ */
8
+ export { fix } from "./writer.js";
9
+ export { analyze, formatIssues } from "./analyze.js";
10
+ export { ALL_TRANSFORMS } from "./transforms/index.js";
11
+ export { FONT_METRICS, findClosestFont, widthDelta } from "quicklook-pptx-renderer";
@@ -0,0 +1,8 @@
1
+ /**
2
+ * effects transform — Strip or adjust shapes with effectLst that render
3
+ * as opaque PDF blocks in QuickLook.
4
+ *
5
+ * Detection is handled by quicklook-pptx-renderer's linter (opaque-pdf-block rule).
6
+ */
7
+ import type { Transform } from "./index.js";
8
+ export declare const effects: Transform;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * effects transform — Strip or adjust shapes with effectLst that render
3
+ * as opaque PDF blocks in QuickLook.
4
+ *
5
+ * Detection is handled by quicklook-pptx-renderer's linter (opaque-pdf-block rule).
6
+ */
7
+ export const effects = {
8
+ name: "effects",
9
+ apply(_slideXml, _slideNum, _ctx) {
10
+ // TODO: configurable — strip effects, reorder z-index, or no-op
11
+ return { changed: false, changes: [] };
12
+ },
13
+ };
@@ -0,0 +1,6 @@
1
+ /**
2
+ * fonts transform — Add explicit <a:latin>, <a:ea>, <a:cs> fallback
3
+ * typefaces matching what OfficeImport's TCFontUtils would pick.
4
+ */
5
+ import type { Transform } from "./index.js";
6
+ export declare const fonts: Transform;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * fonts transform — Add explicit <a:latin>, <a:ea>, <a:cs> fallback
3
+ * typefaces matching what OfficeImport's TCFontUtils would pick.
4
+ */
5
+ export const fonts = {
6
+ 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: [] };
14
+ },
15
+ };
@@ -0,0 +1,8 @@
1
+ /**
2
+ * geometries transform — Replace unsupported preset geometries with
3
+ * equivalent <a:custGeom> path data.
4
+ *
5
+ * Detection is handled by quicklook-pptx-renderer's linter (unsupported-geometry rule).
6
+ */
7
+ import type { Transform } from "./index.js";
8
+ export declare const geometries: Transform;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * geometries transform — Replace unsupported preset geometries with
3
+ * equivalent <a:custGeom> path data.
4
+ *
5
+ * Detection is handled by quicklook-pptx-renderer's linter (unsupported-geometry rule).
6
+ */
7
+ export const geometries = {
8
+ name: "geometries",
9
+ apply(_slideXml, _slideNum, _ctx) {
10
+ // TODO: replace prstGeom with custGeom containing equivalent path data
11
+ return { changed: false, changes: [] };
12
+ },
13
+ };
@@ -0,0 +1,8 @@
1
+ /**
2
+ * gradients transform — Collapse 3+ stop gradients to 2-stop so QuickLook
3
+ * renders a gradient instead of averaging to a flat color.
4
+ *
5
+ * Detection is handled by quicklook-pptx-renderer's linter (gradient-flattened rule).
6
+ */
7
+ import type { Transform } from "./index.js";
8
+ export declare const gradients: Transform;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * gradients transform — Collapse 3+ stop gradients to 2-stop so QuickLook
3
+ * renders a gradient instead of averaging to a flat color.
4
+ *
5
+ * Detection is handled by quicklook-pptx-renderer's linter (gradient-flattened rule).
6
+ */
7
+ export const gradients = {
8
+ name: "gradients",
9
+ apply(_slideXml, _slideNum, _ctx) {
10
+ // TODO: collapse 3+ stop gradients to 2-stop (endpoints only)
11
+ return { changed: false, changes: [] };
12
+ },
13
+ };
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Transform registry — each transform fixes one class of
3
+ * OfficeImport rendering issues by mutating raw OOXML.
4
+ *
5
+ * Detection is handled by quicklook-pptx-renderer's linter.
6
+ * These transforms only apply fixes.
7
+ */
8
+ export type TransformName = "table-styles" | "gradients" | "geometries" | "effects";
9
+ export interface TransformContext {
10
+ tableStyleXml?: any;
11
+ }
12
+ export interface TransformResult {
13
+ changed: boolean;
14
+ changes: string[];
15
+ }
16
+ export interface Transform {
17
+ name: TransformName;
18
+ apply: (slideXml: any, slideNum: number, ctx: TransformContext) => TransformResult;
19
+ }
20
+ export declare const ALL_TRANSFORMS: Transform[];
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Transform registry — each transform fixes one class of
3
+ * OfficeImport rendering issues by mutating raw OOXML.
4
+ *
5
+ * Detection is handled by quicklook-pptx-renderer's linter.
6
+ * These transforms only apply fixes.
7
+ */
8
+ import { tableStyles } from "./table-styles.js";
9
+ import { gradients } from "./gradients.js";
10
+ import { geometries } from "./geometries.js";
11
+ import { effects } from "./effects.js";
12
+ export const ALL_TRANSFORMS = [
13
+ tableStyles,
14
+ gradients,
15
+ geometries,
16
+ effects,
17
+ ];
@@ -0,0 +1,7 @@
1
+ /**
2
+ * properties transform — Resolve full inheritance chain
3
+ * (theme → master → layout → slide) and write explicit fill, font, color
4
+ * on each element.
5
+ */
6
+ import type { Transform } from "./index.js";
7
+ export declare const properties: Transform;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * properties transform — Resolve full inheritance chain
3
+ * (theme → master → layout → slide) and write explicit fill, font, color
4
+ * on each element.
5
+ */
6
+ export const properties = {
7
+ name: "properties",
8
+ detect(_slideXml, _slideNum) {
9
+ // TODO: detect elements relying on inherited properties
10
+ return [];
11
+ },
12
+ apply(_slideXml, _slideNum, _ctx) {
13
+ // TODO: implement — resolve inheritance and inline explicit properties
14
+ return { changed: false, changes: [] };
15
+ },
16
+ };
@@ -0,0 +1,9 @@
1
+ /**
2
+ * table-styles transform — Resolve table style conditional formatting into
3
+ * explicit borders on every <a:tcPr>.
4
+ *
5
+ * OfficeImport doesn't resolve tableStyleId references — it only sees
6
+ * explicit border properties. This inlines them from tableStyles.xml.
7
+ */
8
+ import type { Transform } from "./index.js";
9
+ export declare const tableStyles: Transform;
@@ -0,0 +1,148 @@
1
+ /**
2
+ * table-styles transform — Resolve table style conditional formatting into
3
+ * explicit borders on every <a:tcPr>.
4
+ *
5
+ * OfficeImport doesn't resolve tableStyleId references — it only sees
6
+ * explicit border properties. This inlines them from tableStyles.xml.
7
+ */
8
+ function findTables(node, path = "") {
9
+ const results = [];
10
+ if (!node || typeof node !== "object")
11
+ return results;
12
+ if (node.tbl) {
13
+ const tbls = Array.isArray(node.tbl) ? node.tbl : [node.tbl];
14
+ for (const t of tbls)
15
+ results.push({ table: t, path: path + "/tbl" });
16
+ }
17
+ for (const key of Object.keys(node)) {
18
+ if (key.startsWith("@_"))
19
+ continue;
20
+ const children = Array.isArray(node[key]) ? node[key] : [node[key]];
21
+ for (const child of children) {
22
+ results.push(...findTables(child, path + "/" + key));
23
+ }
24
+ }
25
+ return results;
26
+ }
27
+ function tableNeedsFix(table) {
28
+ const tblPr = table.tblPr;
29
+ if (!tblPr)
30
+ return false;
31
+ const styleId = tblPr["@_tblStyle"] ?? tblPr.tblStyle;
32
+ if (!styleId)
33
+ return false;
34
+ const rows = table.tr;
35
+ if (!rows)
36
+ return false;
37
+ const rowArr = Array.isArray(rows) ? rows : [rows];
38
+ for (const row of rowArr) {
39
+ const cells = row.tc;
40
+ if (!cells)
41
+ continue;
42
+ const cellArr = Array.isArray(cells) ? cells : [cells];
43
+ for (const cell of cellArr) {
44
+ const tcPr = cell.tcPr;
45
+ if (!tcPr)
46
+ return true;
47
+ if (!(tcPr.lnL || tcPr.lnR || tcPr.lnT || tcPr.lnB))
48
+ return true;
49
+ }
50
+ }
51
+ return false;
52
+ }
53
+ function findStyleDef(tableStyleXml, styleId) {
54
+ if (!tableStyleXml)
55
+ return undefined;
56
+ const styleLst = tableStyleXml.tblStyleLst;
57
+ if (!styleLst)
58
+ return undefined;
59
+ const styles = styleLst.tblStyle;
60
+ if (!styles)
61
+ return undefined;
62
+ const arr = Array.isArray(styles) ? styles : [styles];
63
+ return arr.find((s) => s["@_styleId"] === styleId);
64
+ }
65
+ function buildBorder(tcStyle, side) {
66
+ if (!tcStyle)
67
+ return undefined;
68
+ const borders = tcStyle.tcBdr;
69
+ if (!borders)
70
+ return undefined;
71
+ const border = borders[side];
72
+ if (!border)
73
+ return undefined;
74
+ return border.ln ?? border;
75
+ }
76
+ function applyTableStyle(table, styleDef) {
77
+ const changes = [];
78
+ const rows = table.tr;
79
+ if (!rows)
80
+ return changes;
81
+ const rowArr = Array.isArray(rows) ? rows : [rows];
82
+ const tblPr = table.tblPr ?? {};
83
+ const hasFirstRow = tblPr["@_firstRow"] === "1" || tblPr["@_firstRow"] === "true";
84
+ const hasLastRow = tblPr["@_lastRow"] === "1" || tblPr["@_lastRow"] === "true";
85
+ const hasBandRow = tblPr["@_bandRow"] === "1" || tblPr["@_bandRow"] === "true";
86
+ const wholeTbl = styleDef?.wholeTbl?.tcStyle;
87
+ const firstRowStyle = hasFirstRow ? styleDef?.firstRow?.tcStyle : undefined;
88
+ const lastRowStyle = hasLastRow ? styleDef?.lastRow?.tcStyle : undefined;
89
+ const band1H = hasBandRow ? styleDef?.band1H?.tcStyle : undefined;
90
+ const band2H = hasBandRow ? styleDef?.band2H?.tcStyle : undefined;
91
+ let cellCount = 0;
92
+ for (let ri = 0; ri < rowArr.length; ri++) {
93
+ const row = rowArr[ri];
94
+ const cells = row.tc;
95
+ if (!cells)
96
+ continue;
97
+ const cellArr = Array.isArray(cells) ? cells : [cells];
98
+ let activeStyle = wholeTbl;
99
+ if (ri === 0 && firstRowStyle)
100
+ activeStyle = firstRowStyle;
101
+ else if (ri === rowArr.length - 1 && lastRowStyle)
102
+ activeStyle = lastRowStyle;
103
+ else if (hasBandRow)
104
+ activeStyle = (ri % 2 === (hasFirstRow ? 1 : 0)) ? band1H ?? wholeTbl : band2H ?? wholeTbl;
105
+ for (const cell of cellArr) {
106
+ if (!cell.tcPr)
107
+ cell.tcPr = {};
108
+ const tcPr = cell.tcPr;
109
+ let modified = false;
110
+ for (const [xmlSide, styleSide] of [["lnL", "left"], ["lnR", "right"], ["lnT", "top"], ["lnB", "bottom"]]) {
111
+ if (tcPr[xmlSide])
112
+ continue;
113
+ const border = buildBorder(activeStyle, styleSide) ?? buildBorder(wholeTbl, styleSide);
114
+ if (border) {
115
+ tcPr[xmlSide] = border;
116
+ modified = true;
117
+ }
118
+ }
119
+ if (modified)
120
+ cellCount++;
121
+ }
122
+ }
123
+ if (cellCount > 0) {
124
+ changes.push(`inlined borders on ${cellCount} table cells`);
125
+ }
126
+ return changes;
127
+ }
128
+ export const tableStyles = {
129
+ name: "table-styles",
130
+ apply(slideXml, _slideNum, ctx) {
131
+ const tables = findTables(slideXml);
132
+ const changes = [];
133
+ for (const { table } of tables) {
134
+ if (!tableNeedsFix(table))
135
+ continue;
136
+ const styleId = table.tblPr?.["@_tblStyle"] ?? table.tblPr?.tblStyle;
137
+ if (!styleId)
138
+ continue;
139
+ const styleDef = findStyleDef(ctx.tableStyleXml, styleId);
140
+ if (!styleDef) {
141
+ changes.push(`table style ${styleId} not found in tableStyles.xml — skipped`);
142
+ continue;
143
+ }
144
+ changes.push(...applyTableStyle(table, styleDef));
145
+ }
146
+ return { changed: changes.length > 0, changes };
147
+ },
148
+ };
@@ -0,0 +1,18 @@
1
+ /**
2
+ * XML round-trip writer: parse → mutate → serialize → repack ZIP.
3
+ *
4
+ * Transforms operate on fast-xml-parser parsed objects (preserving unknown elements),
5
+ * then XMLBuilder serializes them back to XML. JSZip repacks the modified ZIP.
6
+ */
7
+ import { type TransformName } from "./transforms/index.js";
8
+ export interface FixOptions {
9
+ /** Apply only these transforms (default: all) */
10
+ transforms?: TransformName[];
11
+ /** Return a human-readable report of changes */
12
+ report?: boolean;
13
+ }
14
+ export interface FixResult {
15
+ buffer: Buffer;
16
+ report?: string;
17
+ }
18
+ export declare function fix(pptxBuffer: Buffer, options?: FixOptions): Promise<FixResult>;
package/dist/writer.js ADDED
@@ -0,0 +1,62 @@
1
+ /**
2
+ * XML round-trip writer: parse → mutate → serialize → repack ZIP.
3
+ *
4
+ * Transforms operate on fast-xml-parser parsed objects (preserving unknown elements),
5
+ * then XMLBuilder serializes them back to XML. JSZip repacks the modified ZIP.
6
+ */
7
+ import JSZip from "jszip";
8
+ import { XMLParser, XMLBuilder } from "fast-xml-parser";
9
+ import { xmlParserOptions, xmlBuilderOptions } from "./xml.js";
10
+ import { ALL_TRANSFORMS } from "./transforms/index.js";
11
+ export async function fix(pptxBuffer, options) {
12
+ const zip = await JSZip.loadAsync(pptxBuffer);
13
+ const parser = new XMLParser(xmlParserOptions);
14
+ const builder = new XMLBuilder(xmlBuilderOptions);
15
+ const reportLines = [];
16
+ const enabledNames = new Set(options?.transforms ?? ALL_TRANSFORMS.map(t => t.name));
17
+ const enabled = ALL_TRANSFORMS.filter(t => enabledNames.has(t.name));
18
+ // Find all slide XML files
19
+ const slideFiles = Object.keys(zip.files)
20
+ .filter(f => /^ppt\/slides\/slide\d+\.xml$/.test(f))
21
+ .sort((a, b) => {
22
+ const na = parseInt(a.match(/\d+/)[0]);
23
+ const nb = parseInt(b.match(/\d+/)[0]);
24
+ return na - nb;
25
+ });
26
+ // Load table style XML if needed (for table-styles transform)
27
+ let tableStyleXml = undefined;
28
+ if (enabledNames.has("table-styles")) {
29
+ const tsPath = "ppt/tableStyles.xml";
30
+ const tsFile = zip.file(tsPath);
31
+ if (tsFile) {
32
+ tableStyleXml = parser.parse(await tsFile.async("string"));
33
+ }
34
+ }
35
+ // Process each slide
36
+ for (const slidePath of slideFiles) {
37
+ const slideNum = parseInt(slidePath.match(/\d+/)[0]);
38
+ const xml = await zip.file(slidePath).async("string");
39
+ const parsed = parser.parse(xml);
40
+ let changed = false;
41
+ for (const transform of enabled) {
42
+ const result = transform.apply(parsed, slideNum, { tableStyleXml });
43
+ if (result.changed) {
44
+ changed = true;
45
+ for (const line of result.changes) {
46
+ reportLines.push(`Slide ${slideNum}: ${line}`);
47
+ }
48
+ }
49
+ }
50
+ if (changed) {
51
+ zip.file(slidePath, builder.build(parsed));
52
+ }
53
+ }
54
+ const outBuffer = Buffer.from(await zip.generateAsync({ type: "nodebuffer" }));
55
+ const result = { buffer: outBuffer };
56
+ if (options?.report) {
57
+ result.report = reportLines.length > 0
58
+ ? reportLines.join("\n")
59
+ : "No issues found — file unchanged.";
60
+ }
61
+ return result;
62
+ }
package/dist/xml.d.ts ADDED
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Shared XML parser/builder options matching OOXML conventions.
3
+ */
4
+ export declare const xmlParserOptions: {
5
+ ignoreAttributes: boolean;
6
+ attributeNamePrefix: string;
7
+ removeNSPrefix: boolean;
8
+ parseTagValue: boolean;
9
+ parseAttributeValue: boolean;
10
+ trimValues: boolean;
11
+ isArray: (name: string) => boolean;
12
+ };
13
+ export declare const xmlBuilderOptions: {
14
+ ignoreAttributes: boolean;
15
+ attributeNamePrefix: string;
16
+ format: boolean;
17
+ suppressEmptyNode: boolean;
18
+ suppressBooleanAttributes: boolean;
19
+ };
package/dist/xml.js ADDED
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Shared XML parser/builder options matching OOXML conventions.
3
+ */
4
+ export const xmlParserOptions = {
5
+ ignoreAttributes: false,
6
+ attributeNamePrefix: "@_",
7
+ removeNSPrefix: true,
8
+ parseTagValue: false,
9
+ parseAttributeValue: false,
10
+ trimValues: false,
11
+ isArray: (name) => ARRAY_ELEMENTS.has(name),
12
+ };
13
+ export const xmlBuilderOptions = {
14
+ ignoreAttributes: false,
15
+ attributeNamePrefix: "@_",
16
+ format: false,
17
+ suppressEmptyNode: false,
18
+ suppressBooleanAttributes: false,
19
+ };
20
+ /** Elements that must always be parsed as arrays (even with a single child). */
21
+ const ARRAY_ELEMENTS = new Set([
22
+ "sp", "pic", "cxnSp", "grpSp", "graphicFrame",
23
+ "p", "r", "br", "fld",
24
+ "gs", "ln", "solidFill", "gradFill",
25
+ "tr", "tc",
26
+ "tblStyleLst",
27
+ "ext",
28
+ ]);
package/package.json ADDED
@@ -0,0 +1,70 @@
1
+ {
2
+ "name": "pptx-fix",
3
+ "version": "0.2.0",
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
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "bin": {
9
+ "pptx-fix": "dist/cli.js"
10
+ },
11
+ "keywords": [
12
+ "pptx",
13
+ "powerpoint",
14
+ "quicklook",
15
+ "macos",
16
+ "ios",
17
+ "fix",
18
+ "repair",
19
+ "officeimport",
20
+ "ooxml",
21
+ "office-open-xml",
22
+ "table-styles",
23
+ "python-pptx",
24
+ "pptxgenjs",
25
+ "presentation",
26
+ "apple",
27
+ "preview",
28
+ "slides",
29
+ "iphone",
30
+ "ipad",
31
+ "finder",
32
+ "thumbnail",
33
+ "table-borders",
34
+ "shapes",
35
+ "gradients",
36
+ "rendering",
37
+ "google-slides",
38
+ "canva",
39
+ "libreoffice",
40
+ "pandoc",
41
+ "cross-platform",
42
+ "compatibility"
43
+ ],
44
+ "exports": {
45
+ ".": {
46
+ "types": "./dist/index.d.ts",
47
+ "import": "./dist/index.js"
48
+ }
49
+ },
50
+ "files": [
51
+ "dist"
52
+ ],
53
+ "scripts": {
54
+ "build": "tsc",
55
+ "dev": "tsc --watch"
56
+ },
57
+ "engines": {
58
+ "node": ">=20"
59
+ },
60
+ "license": "MIT",
61
+ "dependencies": {
62
+ "fast-xml-parser": "^5.2.0",
63
+ "jszip": "^3.10.1",
64
+ "quicklook-pptx-renderer": "^0.2.1"
65
+ },
66
+ "devDependencies": {
67
+ "@types/node": "^22.0.0",
68
+ "typescript": "^5.7.0"
69
+ }
70
+ }