Skip to content

PowerPoint

Installed as @ranger/pptx.

Open a .pptx, read it, change it, write it back — draw it to PNG or PDF, and put a Vega chart on it as shapes.

Generated from the Ranger sources that declare the surface — gallery/pptx/api/PptxApi.rgr and gallery/pptx/api/PptxRenderApi.rgr and gallery/pptx/api/PptxChartApi.rgr — so this page cannot describe a method that is not there.

import { … } from "@ranger/pptx";

Headless: a ZIP reader, an XML parser, the model and the writer. No canvas and no fonts.

A run of text inside a shape, addressed by paragraph and position.

A deck’s text is not a string: it is paragraphs of runs, and a run is the unit that carries a font, a size, a weight and a colour. setText on a shape replaces the lot and keeps the FIRST run’s formatting, because that is what someone filling in a template means. Reach for this class when that is not what you mean — when the second half of a sentence has to be red and the first half must not change.

Call Gives What it is for
run.exists boolean Is this run still there? A handle taken before an edit that removed paragraphs is a handle to nothing, and every writer below checks this first rather than reaching past the end of an array.
run.text string
run.setText(s) Run
run.font(family, sizePt) Run The font this run NAMES.
run.bold(on) Run
run.italic(on) Run
run.color(hex) Run “4472C4” — six hexadecimal digits, with or without a leading ‘#’.

Is this run still there? A handle taken before an edit that removed paragraphs is a handle to nothing, and every writer below checks this first rather than reaching past the end of an array.

The font this run NAMES. It is not necessarily the font it is drawn in — a renderer substitutes what it has — but it is what the file says and what PowerPoint will use.

One shape on one slide: a box, a picture, a group, a text placeholder.

index addresses the slide’s TOP-LEVEL shapes. A shape inside a group is not reachable from here, and that is a stated limit rather than an oversight: a group’s children live in the group’s own coordinate space, and handing them out through the same handle as a slide’s shapes would make at(x y) mean two different things depending on where the shape came from.

Call Gives What it is for
shape.exists boolean
shape.name string
shape.setName(s) Shape
shape.preset string The preset geometry: “rect”, “ellipse”, “roundRect”, “star5” — one of the 187 names ECMA-376 defines.
shape.setPreset(name) Shape
shape.x number
shape.y number
shape.width number
shape.height number
shape.rotation number
shape.at(nx, ny) Shape Points, from the top left of the slide.
shape.size(w, h) Shape
shape.rotate(deg) Shape Degrees clockwise.
shape.fill(hex) Shape
shape.noFill() Shape
shape.line(hex, widthPt) Shape
shape.noLine() Shape
shape.text string Every character in the shape, paragraphs joined by a newline.
shape.addClass(name) Shape Give this shape a class a stylesheet can address.
shape.removeClass(name) Shape
shape.hasClass(name) boolean
shape.setStyleId(id) Shape The id a stylesheet addresses with #name.
shape.styleId() string
shape.style(name, value) Shape One property, stated here rather than in a sheet.
shape.setText(s) Shape Replace all of the text, keeping the first run’s formatting.
shape.paragraphCount number
shape.runCount(para) number
shape.run(para, index) Run
shape.addRun(para, text) Run Add a run to the end of a paragraph, formatted like the one before it.
shape.align(how) Shape Horizontal alignment of every paragraph: “l”, “ctr”, “r”, “just”.

The preset geometry: “rect”, “ellipse”, “roundRect”, “star5” — one of the 187 names ECMA-376 defines. gallery/office/geom/assets/presets.txt is the catalogue, and gallery/pptx/docs/PRESET_SHAPES.md lists them.

Points, from the top left of the slide. A deck stores English Metric Units — 12700 to the point — and nothing above this line ever sees one.

hasXfrm is set here because a shape that states no transform inherits its box from the layout: moving such a shape without saying it now has its own geometry writes a position the file does not carry, and the shape reopens where the layout put it.

Degrees clockwise. The file stores sixtieths of a thousandth of a degree; the writer converts, and a negative angle wraps rather than being written as a negative number, which PowerPoint does not read.

Every character in the shape, paragraphs joined by a newline. This is what a search reads; it is not what a writer should round-trip, because the runs and their formatting are not in it.

A GROUP ANSWERS FOR ITS CHILDREN. This used to stop at the group and return nothing, because a group carries no text body of its own — so a deck whose text boxes had been grouped, which is most decks anybody has arranged, read back as blank. Nothing failed; the search index was simply empty and there was no way to tell that from a deck with no words in it.

Give this shape a class a stylesheet can address.

Chainable and repeatable: addClass("card").addClass("wide") carries both, and a class added twice is carried once.

The id a stylesheet addresses with #name.

NOT setName. That is the PowerPoint object name — what an author sees in the selection pane and renames without thinking about styling — and conflating the two would mean a rename silently restyles the box. Set both when you want both.

One property, stated here rather than in a sheet.

This is CSS’s inline style and it behaves like it: nothing in any sheet can override it, !important included. The fluent setters beside it — fill, noLine, run(0,0).bold() — write the model directly and are not affected by the cascade either, so the two spellings agree.

Replace all of the text, keeping the first run’s formatting.

A newline starts a new paragraph, and each paragraph keeps the FIRST paragraph’s properties — alignment, bullet, indent. Filling a template is what this is for: the box was styled by whoever made the template, and a caller who replaces the words means to keep that styling. Building text whose parts differ from one another is runAt and addRun.

Add a run to the end of a paragraph, formatted like the one before it. A shape with no text at all gets its first paragraph here.

One slide.

Call Gives What it is for
slide.exists boolean
slide.width number
slide.height number
slide.shapeCount number
slide.shape(i) Shape
slide.shapeNamed(name) Shape The first shape whose name matches, or a handle that says it does not exist.
slide.text string Every character on the slide, shape by shape in the order they are drawn.
slide.addClass(name) Slide Give this slide a class a stylesheet can address: slide.review \{ … \}.
slide.setStyleId(id) Slide The id a stylesheet addresses with #name.
slide.style(name, value) Slide One property, stated here rather than in a sheet.
slide.addTextBox(x, y, w, h, text) Shape
slide.addShape(preset, x, y, w, h) Shape
slide.removeShape(i) boolean
slide.background(hex) Slide

The first shape whose name matches, or a handle that says it does not exist. A template names its boxes — “Title”, “Subtitle”, “Footer” — and filling one in by name is what most callers want; by index is what breaks the moment somebody reorders the slide in PowerPoint.

The id a stylesheet addresses with #name. See PptxShapeRef.setStyleId for why this is separate from anything the file calls the slide.

The deck.

Two ways out, and the difference matters more than it looks:

save() writes over the package the deck was OPENED from. Every part nobody here models — animations, embedded workbooks, custom XML, the parts a future version of PowerPoint adds — is copied through byte for byte, and only the slides marked dirty are rewritten. This is what “edit a document” should mean. saveNew() writes a package built entirely from the model. Everything not modelled is gone. It is the right answer for a deck this API CREATED, and the wrong one for a deck it opened.

save() on a created deck falls back to saveNew(), because there is no package to write over — so the common case needs no thought.

Call Gives What it is for
deck.addStyleSheet(text) Deck Add CSS over the deck.
deck.applyStyles() Deck Resolve the sheet over every slide, shape, paragraph and run.
deck.styleWarnings() string[] Property names the sheet used that a slide cannot honour, and the selectors this layer could not read.
deck.slideCount number
deck.slide(i) Slide
deck.width number Points.
deck.height number
deck.setSize(w, h) Deck
deck.addSlide() Slide
deck.removeSlide(i) boolean
deck.text string Every character in the deck, slide by slide.
deck.save() Uint8Array
deck.saveNew() Uint8Array

Add CSS over the deck.

The rules are resolved when the deck is SAVED — or when applyStyles is called — rather than here, because a sheet is declarative and the boxes it describes are usually created after it. Called more than once, the later rules simply come later in source order, which is what a second sheet should mean.

The selector subset is small on purpose: .class, #id, an element name (slide, shape, textBox, picture, group, table, chart, paragraph, run), the two combined, and the descendant combinator. font-family, font-size, font-weight, font-style and color inherit down to the runs; fill, stroke and stroke-width do not. Anything a slide cannot honour — display: flex and the rest of the layout properties — is reported through styleWarnings rather than ignored in silence.

Resolve the sheet over every slide, shape, paragraph and run.

Called for you by save and saveNew, and by the renderer before it draws — so the only reason to call it yourself is to READ a styled value back off the model before doing either.

Idempotent: the cascade computes the same answer from the same sheet and the same classes, so calling it twice sets the same values twice.

Property names the sheet used that a slide cannot honour, and the selectors this layer could not read. Empty when everything landed.

The front door.

Call Gives What it is for
Pptx.version string The version of this SURFACE, not of the repository.
Pptx.open(bytes) Deck Open a .pptx from its bytes.
Pptx.create() Deck A deck with nothing in it: one 16:9 page size, no slides.

The version of this SURFACE, not of the repository. It changes when a method changes meaning, so a caller can tell what it is talking to.

A function rather than an sdef: the compiler inlines a static constant at its use sites and emits nothing for it, so a constant no Ranger code reads does not exist in the compiled module at all — Pptx.version came out undefined in JavaScript and nothing had failed to build.

Open a .pptx from its bytes.

Bytes rather than a path on purpose: this has to work in a browser, where there is no file system, and in a build step, where the deck arrived over HTTP. A caller with a path reads it and hands the bytes over.

A deck with nothing in it: one 16:9 page size, no slides. addSlide is the next call.

The two programs below call this API in the page. Edit the JavaScript and press Run. The viewer opens the .pptx the code produced — not a preview of the model in memory.

Pptx.create — a title slideCtrl+Enter runs. The viewer opens the .pptx the code produced.Open in the playground
deck.addSlide — a three-slide stackCtrl+Enter runs. The viewer opens the .pptx the code produced.Open in the playground
import { … } from "@ranger/pptx/render";

PNG and PDF. A separate entry point because its bundle is five times the size.

Draws decks. Give it fonts, then ask for a PNG or a PDF.

It is built on PptxView — the editor’s own painter — so a PNG from here and a screenshot of the editor are the same picture rather than two renderings that agree until they do not.

Call Gives What it is for
renderer.useFontDir(dir) Renderer Every face in a directory laid out the way this repository’s font assets are: the four Open Sans styles as the text family, then the emoji, bullet and Arabic faces joined to the fallback pool.
renderer.addFont(family, data) Renderer One face, named, from its bytes.
renderer.addFace(data) Renderer A face that joins the per-codepoint fallback pool WITHOUT becoming a family anything is drawn in.
Renderer.standardFaces() string[] The faces this repository ships and every editor here loads, in the order they are loaded — which is the order the fallback walk takes them in.
renderer.usePresets(text) Renderer The 187 preset geometries, as the catalogue text.
renderer.toPng(deck, index, scale) Uint8Array — PNG ––––––––––––––––––––––––––––––––––
renderer.pngWidth() number
renderer.pngHeight() number
renderer.toPdf(deck, index) Uint8Array — PDF ––––––––––––––––––––––––––––––––––
renderer.toPdfDeck(deck) Uint8Array — the whole deck, one file ———————————————–

Every face in a directory laid out the way this repository’s font assets are: the four Open Sans styles as the text family, then the emoji, bullet and Arabic faces joined to the fallback pool.

gallery/pdf_writer/assets/fonts is that directory.

One face, named, from its bytes. The first call for a family makes that family; later calls with the same name add its bold, italic and bold italic — FontManager keys a face by family AND style.

A face that joins the per-codepoint fallback pool WITHOUT becoming a family anything is drawn in. This is how an emoji face is added: text stays in the text font and a 😀 is answered from here.

The faces this repository ships and every editor here loads, in the order they are loaded — which is the order the fallback walk takes them in. Handed out so a browser can fetch exactly this list and be sure it measures what the desktop measures.

The 187 preset geometries, as the catalogue text. Without it the painter falls back to the hand-written table and every shape the specification defines and nobody typed in comes out as a rectangle. gallery/office/geom/assets/presets.txt is the file.

— PNG ––––––––––––––––––––––––––––––––––

scale is a multiplier on 96 dots per inch, which is what a slide point means on a screen: 1.0 gives a 960pt-wide deck at 1280 pixels, 2.0 at 2560. It is not a pixel width on purpose — a deck’s pages can differ in size, and a caller asking for “twice as sharp” means that whatever the page is.

— PDF ––––––––––––––––––––––––––––––––––

One slide, as a vector page at the slide’s own size — a 960x540pt deck becomes a 960x540pt page, because a PDF unit IS a typographic point and a deck already states its size in them. Nothing is rasterized: the text is text a reader can select and search, and the shapes are path operators.

The route is DISPLAY LIST, not the element tree slideToEvg builds. That distinction is the whole reason this works: the display list is what the software canvas, the WebGL backend and the browser all paint, so it is where every fidelity fix in this gallery has gone — the 187 preset geometries, the bidirectional reordering, the resolved fonts, the group rotation. The element tree is a coarse approximation that predates all of it. Printing the approximation would give a PDF that disagreed with the screen in exactly the places the screen was carefully made right.

EVGListToElements converts one to the other so the PDF writer needs no second input format, and it is deliberately not pptx-specific: the .docx reader and the spreadsheet emit the same display lists.

— the whole deck, one file ———————————————–

Every slide becomes a page of one PDF, in order. This is what a deck IS to anyone who wants to print it or send it, and doing it slide by slide and stitching afterwards would embed the fonts once per page — the same face, subsetted separately, as many times as there are slides.

A deck whose slides differ in size gets the FIRST slide’s size for every page, because a section states one page size for all the pages under it. That is a real limit and it is stated rather than hidden: PowerPoint has one size per presentation, so a deck that hits it was assembled from two.

import { … } from "@ranger/pptx/chart";

A Vega or Vega-Lite specification onto a slide, as DrawingML shapes rather than as a picture.

Puts a Vega or Vega-Lite chart on a slide.

A failure is a value here, exactly as in PptxApi: a specification with a typo in it leaves ok false and says why in error, rather than throwing through a compiled boundary that has no exceptions to throw.

Call Gives What it is for
chart.font(family) Chart What “sans-serif” is called on this deck.
chart.config(json) Chart Vega-Lite config for every chart this object draws.
chart.curveSteps(n) Chart How finely a curve is subdivided — an area’s top edge, a pie wedge, a smoothed line.
chart.addTo(slide, spec, x, y, w, h) PptxShapeRef Compile spec and put it on slide, inside the box, in points.

What “sans-serif” is called on this deck.

Vega names a CSS family. A slide names a font. Writing “sans-serif” into a run leaves every reader to pick, which is the one outcome that makes two people looking at the same deck see different charts. The default is Calibri; name the deck’s own font here.

Vega-Lite config for every chart this object draws.

A deck of six charts otherwise states the same thing six times — the same axis colours, the same absent domain line, the same view with no border round it — and the interesting half of each specification, which is its DATA, ends up buried in the half that never changes. Say it once here and the specifications carry only what differs.

A chart’s own config still wins, key by key and one level down: setting view on one chart replaces the view block and leaves the axis block from here standing, rather than dropping every default it also wanted.

How finely a curve is subdivided — an area’s top edge, a pie wedge, a smoothed line. Sixteen segments is under a tenth of a point of error at chart size; raise it for a chart that fills the slide, lower it for a sparkline in a table cell.

Compile spec and put it on slide, inside the box, in points.

The chart keeps the aspect ratio the specification asked for and is centred in the box: a 200×200 specification in a 400×200 box is a square chart in the middle of it rather than a stretched one. State the size in the specification and the room in the box.

The handle that comes back is the GROUP. Moving it moves the chart; ungrouping it in PowerPoint leaves the bars and the labels behind as shapes, which is the point of not shipping a picture.

Licence: AGPL-3.0-or-later. This page quotes the comments in those files.

Licence: AGPL-3.0-or-later · commit f323fd4 · Ranger language documentation (MIT)