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

pptxrs

v0.1.11

Published

Create, read, modify, and export .pptx files — Rust/WASM npm library for Node.js

Readme

pptxrs

Created by Rafael Costa - @rcosta02.

Performant Rust/WASM library for creating, reading, and modifying PowerPoint (.pptx) files in Node.js — with text measurement and JSON interchange.

Why pptxrs over PptxGenJS?

  • Import existing files — open any .pptx, read its elements, modify them, re-export.
  • Text measurement — get the exact rendered height and width of a text string (using real font metrics) before writing it to a slide, so you can position other elements relative to it.
  • JSON interchange — serialize a full presentation to/from JSON for storage, diffing, or templating.
  • Performance — the core runs in Rust compiled to WASM; no native dependencies.

Installation

npm install pptxrs

Requires Node.js ≥ 16.


Quick start

const { Presentation } = require("pptxrs");

const pres = new Presentation({ layout: "LAYOUT_16x9", title: "My Deck" });

const slide = pres.addSlide();
const title = slide.addText("Hello, pptxrs!", {
  x: 96, // px (96 px = 1 in)
  y: 96,
  w: 768,
  h: 144,
  fontSize: 44,
  bold: true,
});

console.log(title.width, title.height); // pixels

await pres.writeFile("deck.pptx");

Table of contents

  1. Coordinates
  2. Creating a presentation
  3. Adding slides
  4. Text
  5. Images
  6. Shapes
  7. Tables
  8. Charts
  9. Speaker notes
  10. Slide masters
  11. Exporting
  12. Importing existing .pptx files
  13. Reading element dimensions
  14. Modifying imported slides
  15. Updating charts in imported slides
  16. Updating tables in imported slides
  17. Text measurement
  18. JSON interchange
  19. Full API reference

Coordinates

All position and size values (x, y, w, h) are in pixels (96 DPI). You can also pass a percentage string.

| Value | Meaning | | ------- | -------------------------- | | 96 | 96 px = 1 inch | | "50%" | 50% of the slide dimension |

Standard slide dimensions at 96 DPI:

| Layout | Width (px) | Height (px) | | ----------------------- | ---------- | ----------- | | LAYOUT_16x9 (default) | 960 | 540 | | LAYOUT_4x3 | 960 | 720 | | LAYOUT_WIDE | 1280 | 720 |

Colors are hex strings without #, e.g. "FF0000" for red.

Reading element dimensions

add* methods and slide.getElements() return SlideElement instances:

// from add* — immediate handle
const el = slide.addText("Hello", { x: 96, y: 96, w: 480, h: 72 });
el.width;        // pixels (96 DPI)
el.height;       // pixels (96 DPI)
el.x;            // x position in pixels
el.y;            // y position in pixels
el.widthInches;  // inches
el.heightInches; // inches
el.elementType;  // "text" | "image" | "shape" | "table" | "chart" | "notes"
el.toJson();     // full element data/options object

// from getElements() — same type
const elements = slide.getElements();
elements[0].width;

Creating a presentation

const { Presentation } = require("pptxrs");

const pres = new Presentation({
  layout: "LAYOUT_16x9", // 'LAYOUT_4x3' | 'LAYOUT_WIDE'
  title: "Q3 Results",
  author: "Jane Smith",
  company: "Acme Corp",
});

// Metadata can also be set as properties
pres.title = "Updated Title";
pres.author = "John Doe";
pres.company = "ACME";
pres.layout = "LAYOUT_4x3";

Adding slides

addSlide() returns a Slide that is auto-tracked — no syncSlide needed:

const slide = pres.addSlide();
slide.addText("Slide 1", { x: 96, y: 96, w: 768, h: 96 });
slide.setBackground("F0F4FF");
// done — write whenever you're ready

To use a named slide master:

const slide = pres.addSlide("MASTER_BRAND");
slide.addText("Content here", { x: 96, y: 192, w: 768, h: 288 });

A callback form is also accepted (useful when you want to define the slide inline):

pres.addSlide(null, (slide) => {
  slide.addText("Slide 1", { x: 96, y: 96, w: 768, h: 96 });
});

Text

Plain text

slide.addText("Hello world", {
  x: 96,
  y: 96,
  w: 768,
  h: 144,

  // Font
  fontSize: 24, // points
  fontFace: "Calibri",
  bold: true,
  italic: false,
  underline: true,
  strike: "sngStrike", // 'sngStrike' | 'dblStrike'
  color: "4472C4", // hex
  highlight: "FFFF00", // hex

  // Alignment
  align: "center", // 'left' | 'center' | 'right'
  valign: "middle", // 'top' | 'middle' | 'bottom'

  // Spacing
  lineSpacingMultiple: 1.5,
  charSpacing: 2,
  paraSpaceBefore: 6,
  paraSpaceAfter: 6,

  // Box behaviour
  wrap: true,
  autoFit: true, // shrink text to fit box
  fit: "shrink", // 'none' | 'shrink' | 'resize'
  margin: 0.1, // or [top, right, bottom, left] in inches

  // Effects
  shadow: { type: "outer", angle: 45, blur: 4, color: "000000", opacity: 0.5 },
  glow: { size: 8, opacity: 0.4, color: "4472C4" },

  // Hyperlink
  hyperlink: { url: "https://example.com", tooltip: "Visit site" },
  // or link to slide number:
  hyperlink: { slide: 3 },

  // RTL / language
  rtlMode: false,
  lang: "en-US",
});

Mixed formatting (TextRun array)

Pass an array of runs to mix styles within one text box:

slide.addText(
  [
    { text: "Normal text, " },
    { text: "bold red, ", options: { bold: true, color: "FF0000" } },
    { text: "italic blue.", options: { italic: true, color: "0070C0" } },
  ],
  { x: 96, y: 96, w: 768, h: 96, fontSize: 18 },
);

Bullet lists

slide.addText(
  [
    { text: "First item", options: { bullet: true } },
    { text: "Second item", options: { bullet: true } },
    {
      text: "Numbered",
      options: { bullet: { type: "number", style: "arabicPeriod" } },
    },
    {
      text: "Custom char",
      options: { bullet: { code: "2713", color: "70AD47" } },
    },
  ],
  { x: 96, y: 96, w: 768, h: 384, fontSize: 18 },
);

Bullet indent levels (1–32):

slide.addText(
  [
    { text: "Level 1", options: { bullet: true, indentLevel: 1 } },
    { text: "Level 2", options: { bullet: true, indentLevel: 2 } },
  ],
  { x: 96, y: 96, w: 768, h: 288 },
);

Superscript / subscript

slide.addText(
  [
    { text: "E = mc" },
    { text: "2", options: { superscript: true, fontSize: 12 } },
  ],
  { x: 96, y: 96, w: 384, h: 96, fontSize: 24 },
);

Images

Images can be loaded from the filesystem (auto-resolved) or passed as base64.

// From file path (Node.js — resolved automatically)
slide.addImage({
  path: "./assets/logo.png",
  x: 48,
  y: 48,
  w: 192,
  h: 96,
});

// From base64
const imgData = fs.readFileSync("./photo.jpg").toString("base64");
slide.addImage({
  data: imgData,
  x: 288,
  y: 96,
  w: 384,
  h: 288,
});

// Sizing modes
slide.addImage({
  path: "./bg.jpg",
  x: 0,
  y: 0,
  w: 960,
  h: 540,
  sizing: { type: "cover" }, // 'contain' | 'cover' | 'crop'
});

// Effects
slide.addImage({
  path: "./photo.png",
  x: 96,
  y: 96,
  w: 288,
  h: 288,
  rotate: 15,
  flipH: true,
  rounding: true, // circular crop
  transparency: 20, // 0–100
  shadow: { type: "outer", angle: 45, blur: 6, color: "000000", opacity: 0.4 },
  hyperlink: { url: "https://example.com" },
  altText: "Company logo",
});

Shapes

shapeType is a string matching PowerPoint preset geometry names.

slide.addShape("rect", {
  x: 96,
  y: 96,
  w: 288,
  h: 192,
  fill: { color: "4472C4", transparency: 20 },
  line: { color: "002060", width: 2, dashType: "dash" },
  shadow: { type: "outer", angle: 45, blur: 4, color: "000000", opacity: 0.3 },
  rotate: 10,
  rectRadius: 0.1, // corner rounding (for roundRect)
});

// Shape with text inside
slide.addShape("roundRect", {
  x: 384,
  y: 96,
  w: 384,
  h: 192,
  fill: { color: "ED7D31" },
  text: "Click me",
  fontSize: 20,
  bold: true,
  color: "FFFFFF",
  align: "center",
  valign: "middle",
  hyperlink: { url: "https://example.com" },
});

Common shape types:

| Category | Values | | -------- | ------------------------------------------------------------------------------------------------------------------------------------------- | | Basic | rect roundRect ellipse triangle rightTriangle diamond | | Polygons | pentagon hexagon heptagon octagon | | Stars | star4 star5 star6 star7 star8 star10 star12 star16 star24 star32 | | Arrows | rightArrow leftArrow upArrow downArrow leftRightArrow upDownArrow bentArrow uturnArrow curvedRightArrow curvedLeftArrow | | Callouts | callout1 callout2 callout3 | | Symbols | heart cloud sun moon lightningBolt smileyFace | | Lines | line arc | | Math | mathPlus mathMinus mathMultiply mathDivide mathEqual mathNotEqual | | Misc | donut pie blockArc ribbon ribbon2 |


Tables

// Simple string data
slide.addTable(
  [
    ["Name", "Department", "Score"],
    ["Alice", "Engineering", "95"],
    ["Bob", "Design", "87"],
    ["Carol", "PM", "91"],
  ],
  {
    x: 96,
    y: 192,
    w: 768,
    h: 288,
    colW: [288, 288, 192], // column widths in pixels
    fontSize: 14,
    border: { pt: 1, color: "CCCCCC" },
  },
);

// Per-cell formatting
slide.addTable(
  [
    [
      {
        text: "Header",
        options: {
          bold: true,
          fill: "4472C4",
          color: "FFFFFF",
          align: "center",
        },
      },
      {
        text: "Value",
        options: {
          bold: true,
          fill: "4472C4",
          color: "FFFFFF",
          align: "center",
        },
      },
    ],
    ["Row 1", "100"],
    ["Row 2", "200"],
  ],
  { x: 96, y: 192, w: 576, h: 288 },
);

// Cell spanning
slide.addTable(
  [
    [
      {
        text: "Merged header",
        options: { colspan: 3, align: "center", bold: true },
      },
    ],
    ["Col A", "Col B", "Col C"],
  ],
  { x: 96, y: 96, w: 768, h: 192 },
);

// Auto-paging (table continues onto new slides)
slide.addTable(data, {
  x: 48,
  y: 96,
  w: 864,
  autoPage: true,
  autoPageRepeatHeader: true,
  autoPageHeaderRows: 1,
  newSlideStartY: 96,
});

Charts

Bar / column chart

slide.addChart(
  "bar",
  [
    {
      name: "Revenue",
      labels: ["Q1", "Q2", "Q3", "Q4"],
      values: [120, 190, 160, 230],
    },
    {
      name: "Expenses",
      labels: ["Q1", "Q2", "Q3", "Q4"],
      values: [80, 110, 90, 130],
    },
  ],
  {
    x: 48,
    y: 48,
    w: 864,
    h: 480,

    barDir: "col", // 'col' (vertical) | 'bar' (horizontal)
    barGrouping: "clustered", // 'clustered' | 'stacked' | 'percentStacked'

    showTitle: true,
    title: "Quarterly Financials",
    titleFontSize: 14,

    showLegend: true,
    legendPos: "b", // 'b' | 't' | 'l' | 'r' | 'tr'

    showValue: true,
    dataLabelPosition: "outEnd",

    chartColors: ["4472C4", "ED7D31", "A9D18E"],
    catAxisTitle: "Quarter",
    valAxisTitle: "USD (thousands)",
    valAxisMaxVal: 300,
    valAxisMajorUnit: 50,
  },
);

Line chart

slide.addChart(
  "line",
  [
    {
      name: "Series A",
      labels: ["Jan", "Feb", "Mar", "Apr"],
      values: [10, 25, 18, 32],
    },
  ],
  {
    x: 48,
    y: 48,
    w: 864,
    h: 480,
    lineSmooth: true,
    lineSize: 2.5,
    lineDataSymbol: "circle",
    lineDataSymbolSize: 8,
    showTitle: true,
    title: "Monthly Trend",
  },
);

Pie / doughnut chart

slide.addChart(
  "pie",
  [
    {
      labels: ["North", "South", "East", "West"],
      values: [35, 25, 20, 20],
    },
  ],
  {
    x: 96,
    y: 48,
    w: 768,
    h: 480,
    showPercent: true,
    showLegend: true,
    legendPos: "r",
    chartColors: ["4472C4", "ED7D31", "A9D18E", "FFC000"],
  },
);

Scatter / bubble chart

slide.addChart(
  "scatter",
  [{ name: "Group A", labels: ["1", "2", "3"], values: [10, 20, 30] }],
  { x: 48, y: 48, w: 864, h: 480 },
);

slide.addChart(
  "bubble",
  [{ name: "Data", values: [10, 20, 30], sizes: [5, 10, 15] }],
  { x: 48, y: 48, w: 864, h: 480 },
);

Combo chart (multiple types)

slide.addComboChart(
  ["bar", "line"], // first type = primary
  [
    // Data for 'bar' series
    [
      {
        name: "Revenue",
        labels: ["Q1", "Q2", "Q3", "Q4"],
        values: [120, 190, 160, 230],
      },
    ],
    // Data for 'line' series
    [
      {
        name: "Margin %",
        labels: ["Q1", "Q2", "Q3", "Q4"],
        values: [30, 40, 35, 45],
      },
    ],
  ],
  {
    x: 48,
    y: 48,
    w: 864,
    h: 480,
    secondaryValAxis: true,
  },
);

Chart types

'area' 'bar' 'bar3d' 'bubble' 'bubble3d' 'doughnut' 'line' 'pie' 'radar' 'scatter'


Speaker notes

slide.addNotes(
  "Mention the 30% YoY growth. Pause for questions after this slide.",
);

Slide masters

Define a master before adding slides that reference it:

pres.defineSlideMaster({
  title: "MASTER_BRAND",
  background: { color: "002060" },
  objects: [
    // Logo in the top-right corner
    {
      type: "image",
      options: { path: "./logo.png", x: 816, y: 10, w: 115, h: 48 },
    },
    // Footer bar
    {
      type: "shape",
      shapeType: "rect",
      options: { x: 0, y: 509, w: 960, h: 31, fill: { color: "4472C4" } },
    },
    // Footer text
    {
      type: "text",
      text: "Confidential — Acme Corp",
      options: {
        x: 29,
        y: 514,
        w: 480,
        h: 24,
        fontSize: 10,
        color: "FFFFFF",
      },
    },
  ],
  slideNumber: { x: 864, y: 514, w: 58, color: "FFFFFF", align: "right" },
});

const slide = pres.addSlide("MASTER_BRAND");
slide.addText("Branded slide", {
  x: 96,
  y: 192,
  w: 768,
  h: 192,
  fontSize: 36,
  color: "FFFFFF",
});

Exporting

Write to file

await pres.writeFile("output.pptx");

Get a Buffer (Node.js)

const buf = pres.write("nodebuffer"); // Buffer
fs.writeFileSync("output.pptx", buf);

Get a Uint8Array

const bytes = pres.write("uint8array");

Get base64

const b64 = pres.write("base64");
// e.g. send as HTTP response:
res.json({ file: b64 });

Importing existing .pptx files

const { Presentation } = require("pptxrs");
const fs = require("fs");

// From file
const pres = Presentation.fromBuffer(fs.readFileSync("existing.pptx"));

// From a Uint8Array (e.g. received over HTTP)
const pres2 = Presentation.fromBuffer(new Uint8Array(arrayBuffer));

console.log(pres.layout); // 'LAYOUT_16x9'

const slides = pres.getSlides(); // Slide[]
console.log(slides.length);

for (const slide of slides) {
  const elements = slide.getElements();
  for (const el of elements) {
    console.log(el.elementType, el.width, el.height); // pixels

    const data = el.toJson(); // full element with all options
    if (data.type === "text") {
      console.log(data.text); // string or TextRun[]
      console.log(data.options.fontSize); // e.g. 24
      console.log(data.options.bold); // true | false
      console.log(data.options.color); // e.g. "FF0000"
      console.log(data.options.align); // "left" | "center" | "right"
      console.log(
        data.options.x,
        data.options.y,
        data.options.w,
        data.options.h,
      ); // pixels
    }
    if (data.type === "image") {
      console.log("image base64 length:", data.options.data?.length);
    }
    if (data.type === "shape") {
      console.log(data.shapeType); // e.g. "rect"
      console.log(data.options.fill?.color); // e.g. "4472C4"
    }
    if (data.type === "table") {
      console.log(data.data); // TableCell[][]
      console.log(data.options.colW); // column widths in pixels
    }
  }
}

What gets parsed from existing .pptx files

The parser extracts the following from every element type:

| Element | Properties extracted | | --------- | ------------------------------------------------------------------------------------------------------------- | | Text | position (x/y/w/h), fontSize, bold, italic, color, align, valign, wrap, multi-run paragraphs | | Shape | position, shape type (rect, ellipse, roundRect, …), fill color, line width + color, text content if any | | Image | position, image data as base64 | | Table | position, column widths (colW), all cell text | | Chart | position, chart type, all series names, category labels, and data values | | Slide | background fill color |

Tip: toJson() on both a Presentation and a SlideElement returns the complete object including all styling fields — use it to inspect any property.


Reading element dimensions

Every add* method returns a SlideElement handle. slide.getElements() returns the same type. Both work identically whether the slide came from a file, JSON, or was built from scratch.

const { Presentation } = require("pptxrs");
const fs = require("fs");

// ── Example 1: element added to a brand-new slide ─────────────────────────────

const pres = new Presentation();
const slide = pres.addSlide();

const textEl = slide.addText("Hello", { x: 96, y: 96, w: 480, h: 144, fontSize: 32 });
console.log(textEl.elementType);  // "text"
console.log(textEl.height);       // 144   (pixels)
console.log(textEl.width);        // 480   (pixels)
console.log(textEl.heightInches); // 1.5   (inches)

const imgEl = slide.addImage({ data: imgBase64, x: 96, y: 288, w: 192, h: 192 });
console.log(imgEl.elementType);   // "image"
console.log(imgEl.height);        // 192   (pixels)
console.log(imgEl.width);         // 192   (pixels)

// ── Example 2: elements read from an existing .pptx ───────────────────────────

const imported = Presentation.fromBuffer(fs.readFileSync("deck.pptx"));

for (const slide of imported.getSlides()) {
  for (const el of slide.getElements()) {
    if (el.elementType === "notes") continue; // notes have no dimensions

    console.log(
      el.elementType,
      `${el.width}×${el.height} px`,
      `(${el.widthInches.toFixed(2)}×${el.heightInches.toFixed(2)} in)`,
    );
  }
}

Modifying imported slides

After importing, get slides, mutate them, push them back with syncSlide:

const pres = Presentation.fromBuffer(fs.readFileSync("deck.pptx"));
const slides = pres.getSlides();

// Add a watermark to every slide
slides.forEach((slide, i) => {
  slide.addText("DRAFT", {
    x: 192,
    y: 192,
    w: 576,
    h: 192,
    fontSize: 72,
    color: "FF0000",
    transparency: 70,
    rotate: 45,
    bold: true,
    align: "center",
    valign: "middle",
  });
  pres.syncSlide(i, slide);
});

await pres.writeFile("deck-draft.pptx");

Remove a slide:

pres.removeSlide(0); // remove first slide

Reorder slides by rebuilding:

const slides = pres.getSlides();
// swap slide 0 and slide 1
pres.syncSlide(0, slides[1]);
pres.syncSlide(1, slides[0]);

Updating charts in imported slides

slide.updateChart(elementIndex, data) replaces the data for a chart that was parsed from an existing .pptx. All original chart formatting — colors, axis titles, legend position, data labels — is preserved unchanged. Only the series values and labels are updated.

const pres = Presentation.fromBuffer(fs.readFileSync("report.pptx"));
const slides = pres.getSlides();

// Inspect what charts are on slide 0
slides[0].getElements().forEach((el, i) => {
  if (el.elementType === "chart") {
    const d = el.toJson();
    console.log(
      i,
      d.chartType,
      d.data.map((s) => s.name),
    );
  }
});

// Update the first chart (index 0) with new data
slides[0].updateChart(0, [
  {
    name: "Revenue",
    labels: ["Q1", "Q2", "Q3", "Q4"],
    values: [142, 198, 175, 251],
  },
  {
    name: "Expenses",
    labels: ["Q1", "Q2", "Q3", "Q4"],
    values: [88, 115, 97, 140],
  },
]);

pres.syncSlide(0, slides[0]);
await pres.writeFile("report-updated.pptx");

updateChart also works on charts created from scratch with addChart — in that case there is no original formatting to preserve, so the full chart is regenerated with the new data.


Updating tables in imported slides

slide.updateTable(elementIndex, data) replaces the cell content of a table that was parsed from an existing .pptx. All original table formatting — border styles, cell shading, font colors, column widths — is preserved unchanged. Only the text inside each cell is updated.

const pres = Presentation.fromBuffer(fs.readFileSync("report.pptx"));
const slides = pres.getSlides();

// Find all tables on slide 2 and log their current content
slides[2].getElements().forEach((el, i) => {
  if (el.elementType === "table") {
    console.log(i, el.toJson().data);
  }
});

// Replace the content of the table at element index 1
slides[2].updateTable(1, [
  ["Department", "Headcount", "Avg Score"],
  ["Engineering", "142", "4.2"],
  ["Design", "38", "4.5"],
  ["Operations", "61", "3.9"],
]);

pres.syncSlide(2, slides[2]);
await pres.writeFile("report-updated.pptx");

Note: The new data must have the same number of rows and columns as the original table. Changing the grid dimensions is not supported via updateTable — use addTable on a new slide instead.


Text measurement

Measure the exact rendered height and width of a string before placing it, so you can stack or position elements dynamically.

You must provide the raw font file so pptxrs can use real font metrics (via HarfBuzz shaping).

const fs = require("fs");

// 1. Register the font
pres.registerFont("Calibri", fs.readFileSync("./fonts/Calibri.ttf"));

// 2. Measure
const metrics = pres.measureText("Hello, world!", {
  font: "Calibri",
  fontSize: 24, // points
  bold: false,
  italic: false,
});

// { height: 30.4, width: 148.2, lines: 1, lineHeight: 30.4 }
console.log(metrics.height); // total height in points
console.log(metrics.width); // longest line width in points
console.log(metrics.lines); // number of rendered lines
console.log(metrics.lineHeight); // per-line height in points

Word-wrap: measure in a constrained box

Pass width in pixels to enable automatic word-wrap:

const m = pres.measureText(longText, {
  font: "Calibri",
  fontSize: 18,
  width: 576, // text box width in pixels (= 6 inches × 96)
});

console.log(m.lines); // how many lines it wraps to
console.log(m.height); // total height; use to size the text box

Dynamic layout: stack text boxes

pres.registerFont("Calibri", fs.readFileSync("./fonts/Calibri.ttf"));

const title = "Section Title";
const body = "Here is a longer body paragraph that may wrap…";
const padding = 19; // pixels (~0.2 inch)

const titleMetrics = pres.measureText(title, {
  font: "Calibri",
  fontSize: 36,
  bold: true,
});
// measureText returns points; convert to pixels: points / 72 * 96
const titleH = (titleMetrics.height / 72) * 96;

const bodyMetrics = pres.measureText(body, {
  font: "Calibri",
  fontSize: 18,
  width: 768, // pixels
});
const bodyH = (bodyMetrics.height / 72) * 96;

const slide = pres.addSlide();
slide.addText(title, {
  x: 96,
  y: 96,
  w: 768,
  h: titleH + padding,
  fontSize: 36,
  bold: true,
});
slide.addText(body, {
  x: 96,
  y: 96 + titleH + padding * 2,
  w: 768,
  h: bodyH + padding,
  fontSize: 18,
  wrap: true,
});

JSON interchange

Convert a presentation to a plain JSON object and back. The JSON is fully self-contained — images are stored as base64, and when the presentation was imported from an existing .pptx the original ZIP bytes are embedded as sourceZipB64 so that the round-trip fromJson(toJson()) reproduces the file with complete fidelity: slide masters, theme, fonts, chart formatting, table styles, and every other detail from the source file are all preserved.

Serialize

// As a JS object
const json = pres.toJson();

// As a JSON string
const jsonStr = pres.toJsonString();
fs.writeFileSync("deck.json", jsonStr);

Deserialize

// From a JS object
const pres2 = Presentation.fromJson(
  JSON.parse(fs.readFileSync("deck.json", "utf8")),
);
await pres2.writeFile("rebuilt.pptx");

// fromJson also accepts the live JS object directly
const pres3 = Presentation.fromJson(pres.toJson());

Lossless round-trip for imported files

When you call toJson() on a presentation that was loaded via fromBuffer(), the JSON includes a sourceZipB64 field containing the original ZIP encoded as base64. fromJson() detects this field and restores the source ZIP internally, so write() uses the original file as a base and only replaces slides that were actually modified. This means:

// pres-new.pptx and pres2-new.pptx are byte-for-byte identical
const pres = Presentation.fromBuffer(fs.readFileSync("pres.pptx"));
await pres.writeFile("pres-new.pptx");

const pres2 = Presentation.fromJson(pres.toJson());
await pres2.writeFile("pres2-new.pptx"); // identical to pres-new.pptx ✓

Presentations built from scratch with new Presentation() do not include sourceZipB64 — the JSON stays lean and the fresh builder is used on the way back.

Schema

interface PresentationJson {
  meta: {
    title?: string;
    author?: string;
    company?: string;
    layout: string; // 'LAYOUT_16x9' | 'LAYOUT_4x3' | 'LAYOUT_WIDE'
  };
  slides: {
    background?: { color?: string };
    master?: string;
    elements: SlideElementJson[]; // discriminated union on element.type
  }[];
  /** Present only when the presentation was loaded from a .pptx file.
   *  Enables lossless round-trip through toJson() → fromJson(). */
  sourceZipB64?: string;
}

Each element JSON is a discriminated union:

type SlideElementJson =
  | { type: "text"; text: string | TextRun[]; options: TextOptions }
  | { type: "image"; options: ImageOptions }
  | { type: "shape"; shapeType: string; options: ShapeOptions }
  | { type: "table"; data: TableCell[][]; options: TableOptions }
  | {
      type: "chart";
      chartType: string;
      data: ChartData[];
      options: ChartOptions;
    }
  | { type: "notes"; text: string };

Use cases

Store in a database:

await db.query("INSERT INTO decks (id, data) VALUES ($1, $2)", [
  id,
  pres.toJsonString(),
]);

// Later:
const row = await db.query("SELECT data FROM decks WHERE id = $1", [id]);
const pres = Presentation.fromJson(JSON.parse(row.data));

Generate from a template object:

function buildReport(data) {
  const json = {
    meta: { title: data.title, layout: "LAYOUT_16x9" },
    slides: data.sections.map((section) => ({
      elements: [
        {
          type: "text",
          text: section.heading,
          options: { x: 96, y: 96, w: 768, h: 96, fontSize: 32, bold: true },
        },
        {
          type: "text",
          text: section.body,
          options: { x: 96, y: 240, w: 768, h: 288, fontSize: 16 },
        },
      ],
    })),
  };
  return Presentation.fromJson(json);
}

Full API reference

new Presentation(options?)

| Option | Type | Default | | --------- | ---------------------------------------------------- | --------------- | | layout | 'LAYOUT_16x9' | 'LAYOUT_4x3' | 'LAYOUT_WIDE' | 'LAYOUT_16x9' | | title | string | — | | author | string | — | | company | string | — |

Presentation static methods

| Method | Description | | ------------------------------ | ----------------------------------------------------- | | Presentation.fromBuffer(buf) | Import a .pptx file from a Buffer or Uint8Array | | Presentation.fromJson(json) | Reconstruct from a PresentationJson object |

Presentation instance methods

| Method | Returns | Description | | ---------------------------- | ------------------------------------ | ------------------------------------------------------- | | registerFont(name, buf) | this | Register a TTF/OTF font for measureText | | measureText(text, opts) | TextMetrics | Measure text dimensions in points | | defineSlideMaster(opts) | this | Define a named slide master | | addSlide(masterName?, fn?) | Slide | Add a slide; auto-tracked, no syncSlide needed | | getSlides() | Slide[] | Get all slides | | syncSlide(index, slide) | this | Push a modified slide back | | removeSlide(index) | this | Remove slide at index | | write(outputType?) | Buffer | Uint8Array | string | Export ('nodebuffer' | 'uint8array' | 'base64') | | writeFile(path) | Promise<void> | Write to disk (Node.js) | | toJson() | PresentationJson | Serialize to JS object | | toJsonString() | string | Serialize to JSON string |

Slide instance methods

| Method | Returns | Description | | ----------------------------------- | -------------- | ----------------------------------------------------------------- | | addText(text, opts) | SlideElement | Add text or TextRun[] | | addImage(opts) | SlideElement | Add image (path auto-resolved in Node.js) | | addShape(type, opts) | SlideElement | Add a preset shape | | addTable(data, opts?) | SlideElement | Add a table | | addChart(type, data, opts?) | SlideElement | Add a chart | | addComboChart(types, data, opts?) | SlideElement | Add a combo chart | | addNotes(text) | this | Set speaker notes | | setBackground(hexColor) | this | Set background color | | updateChart(elementIndex, data) | this | Replace data for an existing chart; preserves all formatting | | updateTable(elementIndex, data) | this | Replace cell text for an existing table; preserves all formatting | | getElements() | SlideElement[] | Get all elements with dimension accessors |

SlideElement properties and methods

| Property / Method | Returns | Description | | ------------------- | ------------------- | ------------------------------------------------------------------------- | | elementType | string | "text" | "image" | "shape" | "table" | "chart" | "notes" | | width | number | Width in pixels (96 DPI) | | height | number | Height in pixels (96 DPI) | | x | number | X position in pixels (96 DPI) | | y | number | Y position in pixels (96 DPI) | | widthInches | number | Width in inches | | heightInches | number | Height in inches | | xInches | number | X position in inches | | yInches | number | Y position in inches | | toJson() | SlideElementJson | Full element data/options as a plain object |

MeasureOptions

| Field | Type | Description | | --------------------- | --------- | ------------------------------------------ | | font | string | Font name (must be registered) | | fontSize | number | Points | | bold | boolean | | | italic | boolean | | | charSpacing | number | Extra char spacing in points | | lineSpacingMultiple | number | Multiplier, default 1.0 | | width | number | Box width in pixels; enables word-wrap |

TextMetrics

| Field | Type | Description | | ------------ | -------- | --------------------------------- | | height | number | Total text block height in points | | width | number | Longest line width in points | | lines | number | Number of rendered lines | | lineHeight | number | Per-line height in points |


License

MIT