npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

quicklook-pptx-renderer

v0.3.5

Published

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.

Readme

quicklook-pptx-renderer

Open-source PPTX rendering engine that replicates Apple's macOS QuickLook and iOS preview — pixel for pixel. Lint, render, and diff PowerPoint files without a Mac.

Runs on Linux, Docker, AWS Lambda, and anywhere Node.js runs. No Apple hardware required.

Includes a linter that catches QuickLook compatibility issues before your users see them.


The Problem

PowerPoint files look different on Mac and iPhone than on Windows. Shapes disappear. Table borders vanish. Gradients become flat colors. Shadows turn into opaque white blocks that cover content.

This affects every .pptx created by python-pptx, PptxGenJS, Google Slides export, Canva export, LibreOffice Impress, Pandoc, Quarto, Apache POI, Aspose.Slides, and the Open XML SDK — any tool that generates OOXML presentations.

It happens because Apple uses a private framework called OfficeImport.framework — a completely independent OOXML parser with its own bugs and rendering quirks. It converts slides to HTML + CSS, renders complex shapes as opaque PDF images, and silently drops unsupported geometries. This framework powers QuickLook in macOS Finder (spacebar preview), the iOS Files app, Mail.app attachments, and iPadOS Quick Look.

What Goes Wrong

| What you see on Mac/iPhone | Why it happens | |---|---| | 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. | | 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. | | 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. | | 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. | | 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. | | Charts are blank rectangles — chart content simply vanishes | Charts without an embedded fallback image render as empty rectangles. | | Phantom shapes from other slides — shapes you didn't put there appear | A CMArchiveManager use-after-free bug: the PDF cache is keyed by pointer identity, causing cross-slide bleed. |

These issues affect presentations viewed in macOS Finder (spacebar / QuickLook), iOS Files app, iPadOS Quick Look, Mail.app attachment previews, and Messages.app link previews — anywhere Apple renders PPTX without Microsoft PowerPoint.


Quick Start

npm install quicklook-pptx-renderer

Lint a PPTX for QuickLook Compatibility Issues

npx quicklook-pptx lint presentation.pptx
Slide 4:
  [WARN] "roundRect" with effects renders as an opaque PDF image (Shape 9)
         -> Remove drop shadow/glow effects, or use a plain rect
  [ERR ] "cloud" is not supported by OfficeImport — shape will be invisible (Shape 12)
         -> Use a supported preset or embed as an image

0 errors, 5 warnings, 0 info across 18 slides
# JSON output for CI pipelines
npx quicklook-pptx lint presentation.pptx --json

Render Slides (see what Mac users see)

# Render all slides to PNG
npx quicklook-pptx render presentation.pptx --out ./slides/

# Single slide, high-DPI
npx quicklook-pptx render presentation.pptx --slide 3 --width 1920 --scale 2

# Get raw HTML (same format as QuickLook's Preview.html)
npx quicklook-pptx html presentation.pptx

Pixel-Diff Against Real QuickLook (macOS only)

npx quicklook-pptx diff presentation.pptx --quicklook-dir /tmp/ql-out/

Render to Pixel-Perfect PNGs

The renderer outputs the exact HTML+CSS that QuickLook generates internally. Rendering that HTML in Playwright WebKit produces PNGs identical to what macOS QuickLook shows — on any platform, no Mac required.

import { render } from "quicklook-pptx-renderer";
import { readFileSync, writeFileSync, mkdirSync } from "fs";
import { webkit } from "playwright";

const pptx = readFileSync("presentation.pptx");
const result = await render(pptx);

// Write HTML + attachments (PDF/PNG) to a servable directory
const dir = "/tmp/pptx-preview";
mkdirSync(dir, { recursive: true });
writeFileSync(`${dir}/Preview.html`, result.fullHtml);
for (const [name, buf] of result.attachments) {
  writeFileSync(`${dir}/${name}`, buf);
}

// Render in Playwright WebKit → pixel-perfect QuickLook PNGs
const browser = await webkit.launch();
const page = await browser.newPage({ viewport: { width: 736, height: 800 } });
await page.goto(`file://${dir}/Preview.html`);

const slides = await page.locator("div.slide").all();
for (let i = 0; i < slides.length; i++) {
  await slides[i].screenshot({ path: `slide-${i + 1}.png` });
}
await browser.close();

Why WebKit? QuickLook uses WebKit to render its HTML output. Using Playwright's WebKit engine reproduces the exact same rendering — fonts, CSS quirks, PDF image display — on Linux, Docker, or CI. Chromium or Firefox will produce slightly different results.

As a Library

import { render } from "quicklook-pptx-renderer";
import { readFileSync, writeFileSync } from "fs";

const pptx = readFileSync("presentation.pptx");
const result = await render(pptx);

// Each slide has the exact HTML that QuickLook would generate
for (const slide of result.slides) {
  writeFileSync(`slide-${slide.index + 1}.html`, slide.html);
}

// Full HTML document with CSS (open in WebKit for pixel-perfect result)
writeFileSync("preview.html", result.fullHtml);

Lint Programmatically

import { lint, formatIssues } from "quicklook-pptx-renderer";

const result = await lint(pptxBuffer);

// Human-readable output
console.log(formatIssues(result));

// Structured issues for CI gates
for (const issue of result.issues) {
  if (issue.severity === "error") {
    console.error(`Slide ${issue.slide}: ${issue.message}`);
  }
}

Building Fix Tools

The lint API is designed to power automated PPTX fixers. Each issue's fix object tells you exactly what to change — no re-detection needed.

import { lint, SUPPORTED_GEOMETRIES, FONT_SUBSTITUTIONS, GEOMETRY_FALLBACKS } from "quicklook-pptx-renderer";

const result = await lint(pptxBuffer);

for (const issue of result.issues) {
  if (!issue.fix) continue; // informational only

  switch (issue.fix.action) {
    case "replace-geometry":
      // issue.fix.current = "heart", issue.fix.suggested = "ellipse"
      // Replace <a:prstGeom prst="heart"> with <a:prstGeom prst="ellipse">
      break;

    case "strip-effects":
      // Remove <a:effectLst> and <a:effectDag> from the shape
      break;

    case "reduce-stops":
      // issue.fix.firstColor = "#FF0000", issue.fix.lastColor = "#0000FF"
      // issue.fix.averageColor = "#7F007F", issue.fix.angle = 5400000
      // Replace 3+ gradient stops with 2-stop using first/last colors
      break;

    case "replace-font":
      // issue.fix.from = "Calibri", issue.fix.macTarget = "Helvetica Neue"
      // issue.fix.widthDelta = 14.4, issue.fix.alternatives = [
      //   { font: "Carlito", widthDelta: 2.1 },
      //   { font: "Montserrat", widthDelta: 0.4 }
      // ]
      break;

    case "inline-borders":
      // issue.fix.tableStyleId = "{5C22544A-...}"
      // Resolve table style and write explicit borders on each cell
      // Note: tables without a style AND without borders are intentionally
      // borderless — both PowerPoint and QuickLook render them the same way.
      break;

    case "add-fallback-image":
      // Generate or embed a chart fallback image via mc:AlternateContent
      break;

    case "ungroup":
      // issue.fix.childCount = 5, issue.fix.containsPictures = false
      // Promote group children to slide-level drawables
      break;

    case "strip-embedded-fonts":
      // issue.fix.fonts = [{ name: "Montserrat", replacement: "DIN Alternate" }]
      // Strip embedded font data and replace with cross-platform alternatives
      break;
  }
}

Also available: SUPPORTED_GEOMETRIES (Set of 60 presets OfficeImport renders), FONT_SUBSTITUTIONS (Windows → macOS font map), GEOMETRY_FALLBACKS (unsupported → closest supported preset), findClosestFont() (metric-based font matching with narrower-preference), widthDelta() (percentage width difference between fonts), APPLE_SYSTEM_FONTS (29 fonts preinstalled on both macOS and iOS), MACOS_SYSTEM_FONTS (38 fonts on macOS).


Lint Rules

Every issue carries a typed fix object with machine-readable remediation data — downstream tools can apply transforms without re-detecting the problem.

| Rule | Severity | What It Catches | fix.action | |------|----------|----------------|-------------| | unsupported-geometry | error | Shape preset not in OfficeImport's ~60 supported geometries — will be invisible | replace-geometry (includes closest supported preset) | | chart-no-fallback | error | Chart without fallback image — renders as blank rectangle | add-fallback-image | | opaque-pdf-block | warn | Shape with effects rendered as opaque PDF covering content behind it | strip-effects | | gradient-flattened | warn | 3+ gradient stops collapsed to single average color | reduce-stops (includes computed 2-stop colors + average) | | table-style-unresolved | warn | Table has style but cells lack explicit borders — PowerPoint shows borders, QL won't | inline-borders (includes tableStyleId) | | 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) | | group-as-pdf | warn | Group rendered as single opaque PDF image | ungroup (includes child count, whether group contains pictures) | | geometry-forces-pdf | info | Non-rect geometry renders as PDF — opaque background may cover adjacent content | — | | rotation-forces-pdf | info | Rotated rect renders as PDF — enlarged bounding box may cover adjacent content | — | | text-inscription-shift | info | Text in non-rect shape uses geometry-inscribed bounds (text may shift) | replace-geometry (suggests rect) | | embedded-font | warn | Embedded fonts ignored by QuickLook — text renders with system substitutes | strip-embedded-fonts (includes per-font replacement when metrics available) | | vertical-text | info | Vertical text uses CSS writing-mode | — |


Rendering Accuracy

Pixel-level comparison against actual QuickLook output (qlmanage -p):

| Source | Slides | Avg Pixel Diff | Perfect (< 0.05%) | |--------|--------|----------------|---------------------| | Real-world presentation | 18 | 0.15% | 16/18 | | python-pptx generated | 10 | 0.17% | 6/10 | | python-pptx stress test | 10 | 0.42% | 4/10 | | PptxGenJS generated | 10 | 0.11% | 6/10 | | Chart test suite | 7 | 0.08% | 7/7 | | Overall | 55 | 0.19% | 39/55 |

236/236 synthetic HTML structure tests pass.


Which Tools Produce Affected Files

Any tool that generates .pptx files can produce presentations that render incorrectly on Apple devices:

| Tool | Typical issues on Mac/iPhone | |------|-----| | python-pptx | Table borders missing, shapes invisible, "PowerPoint found a problem with content" errors | | PptxGenJS | Missing thumbnails, shape rendering differences | | Google Slides (File → Download as .pptx) | Font substitution, formatting shifts, missing embedded fonts | | Canva (Download as .pptx) | Animations stripped, font substitution, layout differences | | LibreOffice Impress (Save As .pptx) | Table styles unresolved, gradient rendering differences | | Pandoc / Quarto (markdown → pptx) | Content type corruption, shapes missing, repair errors | | Apache POI (Java) | Content type errors, missing fallback images | | Aspose.Slides | Thumbnail missing if not explicitly generated | | Open XML SDK (.NET) | Repair errors, missing relationships |

If you generate presentations programmatically and your users view them on Mac or iPhone, you should lint with this tool.


How It Works

PPTX (ZIP) → Package → Reader → Resolve → Mapper → HTML + PDF attachments

| Module | OfficeImport Equivalent | Purpose | |--------|------------------------|---------| | package/ | OCP/OCX | ZIP extraction, .rels relationships | | reader/ | PX/OAX | OOXML XML → typed domain objects | | model/ | PD/OAD | TypeScript interfaces for slides, shapes, text, tables | | resolve/ | — | Color themes, font substitution, property inheritance | | mapper/ | PM/CM | HTML + CSS + PDF generation | | lint | — | Static analysis against known OfficeImport quirks |

Two Rendering Paths

OfficeImport routes every shape through one of two paths:

CSS — Only plain rect with no rotation and no effects → <div> with CSS properties.

PDF — Everything else (roundRect, ellipse, stars, arrows, any rotation/effects) → inline PDF via CMDrawingContext.copyPDF, embedded as <img src="AttachmentN.pdf">.

This is why shapes with drop shadows or rounded corners render as opaque blocks — the PDF image has a non-transparent background that covers content behind it.

Property Inheritance

shape local → slide placeholder → layout placeholder → master placeholder → master text style → theme

Font Substitution Map

macOS replaces Windows fonts at render time (from TCFontUtils in OfficeImport). Width deltas computed from xWidthAvg in the font metrics database:

| Windows Font | macOS Replacement | Width Δ | Text Reflow Risk | |---|---|---|---| | Calibri | Helvetica Neue | +14.4% | High — text overflows boxes | | Calibri Light | Helvetica Neue Light | +20.9% | High — significant reflow | | Arial | Helvetica | +0.0% | None — metrically identical | | Arial Black | Helvetica Neue | -15.9% | High — text shrinks | | Arial Narrow | Helvetica Neue | +20.0% | High — significant reflow | | Cambria | Georgia | +7.0% | Medium | | Consolas | Menlo | +9.5% | Medium — wider monospace | | Courier New | Courier | +0.0% | None | | Times New Roman | Times | +0.0% | None | | Tahoma | Geneva | +10.0% | High | | Segoe UI | Helvetica Neue | +14.0% | High | | Century Gothic | Futura | +1.0% | Low | | Franklin Gothic Medium | Avenir Next Medium | +17.8% | High | | Corbel | Avenir Next | +18.8% | High | | Candara | Avenir | +8.0% | Medium | | Constantia | Georgia | +4.2% | Low | | Palatino Linotype | Palatino | +0.0% | None | | Book Antiqua | Palatino | +0.0% | None | | Garamond | Georgia | +8.7% | Medium | | Impact | Impact | +0.0% | None |

Safe cross-platform fonts (identical on Windows and macOS): Arial, Verdana, Georgia, Trebuchet MS, Courier New, Times New Roman, Impact, Palatino Linotype, Book Antiqua.

Highest reflow risk: Calibri Light (+20.9%), Arial Narrow (+20.0%), Corbel (+18.8%), Franklin Gothic Medium (+17.8%), Arial Black (-15.9%), Calibri (+14.4%), Segoe UI (+14.0%), Tahoma (+10.0%).

The linter reports width deltas, upgrades font substitution to warn severity when the delta exceeds ±10%, and includes metrically-closest cross-platform alternatives in the fix data.

Plus CJK mappings: MS Gothic → Hiragino Sans, SimSun → STSong, Microsoft YaHei → PingFang SC, Malgun Gothic → Apple SD Gothic Neo, and more.

Metric-Compatible Google Fonts (Croscore)

These open-source fonts (SIL OFL) were designed as drop-in replacements for Microsoft core fonts. Width deltas measured from OS/2 tables and included in the font metrics database (106 fonts total):

| Microsoft Font | Google Font Replacement | Width Δ vs Original | License | |---|---|---|---| | Calibri | Carlito | -2.1% | OFL | | Cambria | Caladea | -5.6% | OFL | | Arial | Arimo | -1.2% | Apache 2.0 | | Times New Roman | Tinos | +0.5% | Apache 2.0 | | Courier New | Cousine | 0.0% | Apache 2.0 |

Compare: Calibri → Helvetica Neue (the QL substitution) is +14.4%. Carlito at -2.1% is 7x better.

What Doesn't Work for Font Fixes

Verified by testing against actual OfficeImport output:

  • Embedded fonts (<p:embeddedFont>): OfficeImport ignores them completely. No OADEmbeddedFont class exists in the framework. Embedding helps PowerPoint and LibreOffice, but not QuickLook.
  • pitchFamily attribute: OfficeImport ignores it. All unknown fonts produce font-family:"FontName" in CSS with no generic family fallback. WebKit then falls back to serif (Times) regardless of the pitchFamily hint.
  • Font stacks / fallback chains: OOXML supports only one typeface per text run. No CSS-like font-family: "Carlito", "Calibri", sans-serif — it's a single value.
  • Parent font inheritance as fallback: OOXML property inheritance only applies when the property is missing, not when the font isn't installed. If a run specifies typeface="Carlito", the parent's typeface="Arial" is not used as a fallback.

The only way to control fonts in QuickLook is to use a font that macOS has installed. The FONT_SUBSTITUTIONS map and findClosestFont() API provide the data to pick the best replacement. Use APPLE_SYSTEM_FONT_LIST as candidates to ensure results work on both macOS and iOS:

import { findClosestFont, APPLE_SYSTEM_FONT_LIST } from "quicklook-pptx-renderer";

// Montserrat → DIN Alternate (-4.3% width) — narrower is preferred over wider
const [best] = findClosestFont("Montserrat", { candidates: APPLE_SYSTEM_FONT_LIST });

Key Discoveries

These findings come from reverse engineering OfficeImport via Objective-C runtime introspection and ARM64 disassembly. They are not documented anywhere else:

  1. Only rect takes the CSS path — even roundRect and ellipse always go to PDF
  2. ~60 of 187 shape presets are supported — the rest are silently dropped (no error, no fallback)
  3. PDF shapes use the B operator (simultaneous fill+stroke) with invisible default stroke
  4. CMArchiveManager has a use-after-free — PDF cache keyed by pointer identity causes cross-slide bleed
  5. Table row heights use EMU / 101600, not EMU / 12700
  6. 3+ gradient stops → average color (not rendered as gradient)
  7. Font substitution: Calibri → Helvetica Neue, Arial → Helvetica, etc. (full map above)
  8. Text inscription for non-rect shapes uses float radius and trunc() in pixel space
  9. Embedded fonts are ignored — no OADEmbeddedFont class; TCImportFontCache only does CoreText system font lookup
  10. pitchFamily is ignored — all unknown fonts get bare font-family:"Name" in CSS with no generic family fallback, regardless of pitchFamily value

Coordinate System

| Measurement | Unit | Conversion | |------------|------|-----------| | Position / Size | EMU | 12,700 EMU = 1 CSS pixel | | Font size | Hundredths of a point | 1800 = 18pt | | Angles | 60,000ths of a degree | 5,400,000 = 90deg | | Color transforms | 1,000ths of percent | 100,000 = 100% | | Table row height | EMU / 101,600 | (OfficeImport quirk) |


Compatibility

| Platform | Status | |----------|--------| | Node.js 20+ (Linux, macOS, Windows) | Full support | | Docker / containers | Full support | | AWS Lambda / Cloud Functions | Full support (HTML output; PNG needs Playwright) | | Bun | Untested | | Deno | Untested |


Fixing Issues (not just detecting them)

This package detects QuickLook rendering issues. To automatically fix PPTX files so they render correctly on Apple devices, see pptx-fix — a companion tool that rewrites the OOXML to inline explicit borders, collapse gradients, replace unsupported geometries, and more.

# Detect issues
npx quicklook-pptx lint presentation.pptx

# Fix issues (separate package)
npx pptx-fix presentation.pptx -o fixed.pptx

Manual Fixes

If you prefer fixing at the source:

python-pptx — table borders missing:

from pptx.util import Pt
from pptx.dml.color import RGBColor

for cell in table.iter_cells():
    for border in [cell.border_top, cell.border_bottom,
                   cell.border_left, cell.border_right]:
        border.width = Pt(1)
        border.color.rgb = RGBColor(0, 0, 0)

Shapes disappearing: Your shape preset isn't in OfficeImport's supported list. Run the linter to find which shapes will be invisible. Use supported presets or embed complex shapes as images.

Opaque white blocks: Shapes with drop shadows render as opaque PDF images. The PDF has a non-transparent background. Remove effects or restructure z-ordering.


Related Projects

| Project | Difference | |---------|-----------| | pptx-fix | Companion tool — fixes PPTX files for QuickLook; this package detects issues and renders | | LibreOffice headless | Renders like LibreOffice, not like QuickLook | | python-pptx | Creates/reads PPTX; doesn't render | | PptxGenJS | Creates PPTX; doesn't render | | Apache POI | Java; renders like Java, not like QuickLook |

This is the only open-source project that replicates Apple's exact QuickLook rendering.


Dependencies

| Package | Purpose | Required | |---------|---------|----------| | jszip | ZIP extraction | Yes | | fast-xml-parser | OOXML XML parsing | Yes | | @napi-rs/canvas | PDF image generation | Optional (for render) | | playwright | HTML-to-PNG screenshots | Optional (for render/diff) |

The linter (npx quicklook-pptx lint) needs only jszip + fast-xml-parser — zero native dependencies.

License

MIT