Skip to content
PaperJSX

[FIELD NOTE]

Migrate from python-pptx to PaperJSX

Side-by-side migration from python-pptx to PaperJSX (JSON/Node.js): slides, text, charts, images, tables. What you gain, what you lose, working code.

2026-08-048 min read

This is a technical decision aid. Check current package and hosted-access details before implementation.

PaperJSX is not a drop-in replacement for python-pptx — it is a different programming model in a different language. python-pptx uses imperative Python method calls; PaperJSX uses declarative JSON schemas in Node.js. Migration requires rewriting document definitions, not just swapping imports. This guide provides side-by-side code for every common task so you can evaluate the trade-offs before committing.

Why migrate?

Three factors drive python-pptx migration:

  • Inactive maintenance. python-pptx's last release was v1.0.2 in August 2024. According to Snyk, it is classified as inactive with

    439 open issues

    and 77 open pull requests. The Swiss Federal Railways formally flagged it as a dependency risk in January 2026.

  • Missing features. python-pptx cannot create combo charts (8-year-old gap), slide animations, or Office 2016+ chart types. It has no multi-format output and no AI agent integration.

  • Single-format limitation. python-pptx generates PPTX only. If the same report needs to ship as PDF, DOCX, or XLSX, you need additional libraries with separate APIs. PaperJSX handles all four from one JSON schema.

What do you gain and lose?

Capabilitypython-pptxPaperJSX
Read/modify existing PPTXYesNo (generate only)
LanguagePythonJavaScript / TypeScript
Combo chartsNoYes (Pro)
Slide animationsNoYes (Pro)
Multi-format outputPPTX onlyPPTX, DOCX, PDF, XLSX
SVG imagesNoYes
MCP server (AI agents)NoYes
PDF/UA accessibilityNoYes (Pro)
Maintenance statusInactive (since Aug 2024)Active
Native dependencieslxml, Pillow, XlsxWriterZero
Free tierMIT (full library)Apache-2.0 lite engines for all four formats
Slide Masters from templatesYes (read from file)JSON-defined only

The critical trade-off: python-pptx can open and modify existing presentations. PaperJSX cannot. If your workflow involves loading a branded template .pptx file and modifying specific placeholders, PaperJSX requires a different approach — you define the slide master styling in JSON instead of loading it from a file.

Task: add text to a slide

from pptx import Presentation
from pptx.util import Inches, Pt
from pptx.dml.color import RGBColor

prs = Presentation()
slide = prs.slides.add_slide(prs.slide_layouts[6])

txBox = slide.shapes.add_textbox(
    Inches(1), Inches(1), Inches(8), Inches(1)
)
tf = txBox.text_frame
p = tf.paragraphs[0]
p.text = "Q3 2026 results"
p.font.size = Pt(36)
p.font.bold = True
p.font.color.rgb = RGBColor(0x1A, 0x1A, 0x18)

prs.save("output.pptx")
import { generate } from "@paperjsx/json-to-pptx";
import { writeFileSync } from "node:fs";

const doc = {
  slides: [{
    elements: [{
      type: "text",
      value: "Q3 2026 results",
      style: { fontSize: 36, bold: true, color: "#1A1A18" }
    }]
  }]
};

const buffer = await generate(doc);
writeFileSync("output.pptx", buffer);

python-pptx: 12 lines, imperative. You create a presentation, add a slide, add a text box with pixel coordinates, access the text frame, access the paragraph, set text, set font size, set bold, set color, save. PaperJSX: 8 lines, declarative. You describe what the slide contains and call generate.

How do you add a chart?

from pptx.chart.data import ChartData
from pptx.enum.chart import XL_CHART_TYPE

chart_data = ChartData()
chart_data.categories = ["NA", "EMEA", "APAC"]
chart_data.add_series("Revenue", (4200, 3100, 2800))
chart_data.add_series("Expenses", (2800, 2100, 1900))

slide.shapes.add_chart(
    XL_CHART_TYPE.COLUMN_CLUSTERED,
    Inches(1), Inches(2), Inches(8), Inches(4),
    chart_data
)
{
  type: "chart",
  chartType: "bar",
  data: {
    categories: ["NA", "EMEA", "APAC"],
    series: [
      { name: "Revenue", values: [4200, 3100, 2800] },
      { name: "Expenses", values: [2800, 2100, 1900] }
    ]
  }
}

The data structure is nearly identical — categories and series with names and values. The difference: python-pptx requires ChartData objects and explicit positioning in inches. PaperJSX takes a JSON object with auto-layout. Both produce native, editable OOXML charts. PaperJSX additionally supports chartType: "combo" — a feature python-pptx has lacked since 2017.

Task: add an image

slide.shapes.add_picture(
    "logo.png",
    Inches(0.5), Inches(0.5),
    Inches(2), Inches(0.6)
)
{
  type: "image",
  src: "./logo.png",
  style: { width: 2, height: 0.6 }
}

Both accept file paths. PaperJSX additionally accepts HTTP URLs and base64 strings. python-pptx requires Inches() wrappers for positioning; PaperJSX uses plain numbers. PaperJSX supports SVG images; python-pptx does not.

Task: add a table

rows, cols = 4, 3
table_shape = slide.shapes.add_table(
    rows, cols,
    Inches(1), Inches(2), Inches(8), Inches(3)
)
table = table_shape.table

# set headers
table.cell(0, 0).text = "Region"
table.cell(0, 1).text = "Revenue"
table.cell(0, 2).text = "Growth"

# set data rows
data = [
    ("NA", "$4.2M", "+10.5%"),
    ("EMEA", "$3.1M", "+6.9%"),
    ("APAC", "$2.8M", "+27.3%"),
]
for i, row_data in enumerate(data, start=1):
    for j, val in enumerate(row_data):
        table.cell(i, j).text = val
{
  type: "table",
  headers: ["Region", "Revenue", "Growth"],
  rows: [
    ["NA", "$4.2M", "+10.5%"],
    ["EMEA", "$3.1M", "+6.9%"],
    ["APAC", "$2.8M", "+27.3%"]
  ],
  style: {
    headerBackground: "#1A1A18",
    headerColor: "#FFFFFF"
  }
}

python-pptx tables require pre-declaring row and column counts, then iterating through cells to set values individually. PaperJSX takes a headers array and a rows array — the structure maps directly to how developers think about tabular data. python-pptx: 16 lines. PaperJSX: 10 lines.

Task: multi-slide deck from data

This is where the architectural difference matters most. Building a 10-slide deck from database records in python-pptx means 10 iterations of imperative calls. In PaperJSX, it means mapping data to JSON objects.

prs = Presentation()

for region in regions:
    slide = prs.slides.add_slide(prs.slide_layouts[6])

    # title
    txBox = slide.shapes.add_textbox(
        Inches(1), Inches(0.5), Inches(8), Inches(1)
    )
    txBox.text_frame.paragraphs[0].text = region["name"]

    # chart
    chart_data = ChartData()
    chart_data.categories = region["quarters"]
    chart_data.add_series("Revenue", region["values"])
    slide.shapes.add_chart(
        XL_CHART_TYPE.COLUMN_CLUSTERED,
        Inches(1), Inches(2), Inches(8), Inches(4),
        chart_data
    )

prs.save("report.pptx")
const doc = {
  slides: regions.map(region => ({
    elements: [
      { type: "text", value: region.name,
        style: { fontSize: 28, bold: true } },
      {
        type: "chart",
        chartType: "bar",
        data: {
          categories: region.quarters,
          series: [{ name: "Revenue", values: region.values }]
        }
      }
    ]
  }))
};

const buffer = await generate(doc);

The PaperJSX version is a single .map() call. The entire deck is a JavaScript expression — it can be snapshot-tested, JSON-diffed, and schema-validated before generation. The python-pptx version is a loop of imperative side effects that can only be tested by generating the file and opening it.

How do you call PaperJSX from Python?

If your backend is Python and you cannot rewrite it in Node.js, you can still use PaperJSX by deploying it as an HTTP service.

# Python client
import requests
import json

schema = {
    "slides": [{
        "elements": [{
            "type": "text",
            "value": "Generated from Python",
            "style": { "fontSize": 36 }
        }]
    }]
}

response = requests.post(
    "https://your-api.vercel.app/api/generate?format=pptx",
    json=schema
)

with open("output.pptx", "wb") as f:
    f.write(response.content)

Deploy PaperJSX as a Next.js API route, a Vercel Function, or an Express microservice. Call it from Python via HTTP. The JSON schema is language-agnostic — Python's dict serializes to the same JSON that PaperJSX expects.

# Python calling Node.js via subprocess
import subprocess
import json

schema = { /* ... */ }

result = subprocess.run(
    ["node", "generate.mjs"],
    input=json.dumps(schema),
    capture_output=True,
    text=True
)

# generate.mjs reads stdin, calls generate(), writes to stdout

The subprocess approach avoids network latency but requires Node.js installed alongside Python. The HTTP approach is cleaner for production — it decouples the document generation service from the Python backend.

Keep the workflow in your product.

Use local engines or local MCP to prove the artifact. When release controls matter, start Platform for authenticated hosted delivery or discuss Enterprise terms.