Skip to main content

betteroffice-pptx

Read, edit, and lay out PPTX presentations from Python. python-pptx reads a deck and writes one back; this also lays slides out — line breaking, text metrics, a display list — and merges edits across replicas, because the Rust BetterOffice PPTX core is compiled into the wheel: no PowerPoint, no LibreOffice subprocess, no COM.

pip install betteroffice-pptx

The distribution is hyphenated, the module is not: import betteroffice_pptx.

Read a deck

from betteroffice_pptx import Presentation

deck = Presentation.open_path("quarterly.pptx")

for slide in deck:
    print(slide.index, slide.name, repr(slide.text))

shape = next((s for slide in deck for s in slide.shapes), None)
if shape is not None:
    print(shape.kind, shape.geometry, shape.x, shape.y, shape.width, shape.height)

Geometry is in English Metric Units — 914400 to the inch. The module exports EMU_PER_INCH, EMU_PER_CENTIMETER, and EMU_PER_POINT so you rarely have to type the constant.

snapshot() returns the whole deck as plain data in one call; slide(key) returns one slide by index or by ID. Both are values, not live views — read them again after an edit.

Edit shapes and slides

from betteroffice_pptx import EMU_PER_INCH as INCH

deck = Presentation.open(open("deck.pptx", "rb").read())
slide_id = deck.slide_ids[0]

edit = deck.add_text_box(
    slide_id, x=INCH, y=INCH, width=4 * INCH, height=INCH,
    text="Revenue", bold=True, font_size=32.0,
)
deck.move_shape(slide_id, edit.shape_id, 2 * INCH, INCH)
deck.resize_shape(slide_id, edit.shape_id, 5 * INCH, 2 * INCH)

box = deck.add_shape(slide_id, "roundRect", x=INCH, y=3 * INCH,
                     width=2 * INCH, height=INCH, fill="#2563eb")
deck.set_shape_stroke(slide_id, box.shape_id, color="#111827", width_pt=2.0)
deck.set_shape_adjust(slide_id, box.shape_id, {"adj": 0.25})

Every mutating call returns a receipt naming what it touched: ShapeEdit has the new shape's ID and z-order index, TransformEdit carries the rect before and after, and AdjustEdit shows the values after the engine clamped them into their guide's legal range.

An unsupported preset geometry, a non-positive size, or an unknown adjustment guide raises ValueError instead of writing a shape PowerPoint would reject.

Edit text

Text lives in stories — one editable flow per text-bearing shape. Offsets are UTF-16 code units, and every paragraph ends with a pilcrow occupying one of them.

story = next(
    (s for slide in deck for shape in slide.shapes for s in shape.stories), None
)
if story is not None:
    print(story.text, story.length)

    deck.insert_text(story.id, 0, "Q3 ", bold=True)
    deck.format_text(story.id, 0, 3, color="#dc2626")
    deck.insert_paragraph_break(story.id, 3)
    removed = deck.delete_text(story.id, 0, 3)
    print(removed.text)      # 'Q3 '

A shape with no text has no story, so a deck of pictures alone yields none. deck.story(id) looks one up directly and raises KeyError if it is gone.

format_text patches only the arguments you pass, and a range spanning several paragraphs styles each of them as a single undoable edit. delete_text is the strict one: a range crossing a paragraph boundary raises RangeError rather than silently swallowing the break.

Lay a slide out

No font is compiled into the wheel, so laying out a slide that has text needs at least one registered face. Before that, render_slide raises:

deck.render_slide(0)
# RenderError: no font has been registered for slide text

Register the faces the deck uses — one call per family, weight, and slant — and it lays out:

from pathlib import Path

deck.register_font("Inter", Path("Inter-Regular.ttf").read_bytes())
deck.register_font("Inter", Path("Inter-Bold.ttf").read_bytes(), bold=True)

layout = deck.render_slide(0)
print(layout.width, layout.height, len(layout))   # 1280.0 720.0 42
layout.write("slide-0.json")
scene = layout.to_dict()

Once at least one face exists nothing raises again: a family the deck names but you never registered resolves to the same family at regular weight, and failing that to the first face you registered at all. One registration therefore renders every slide — in that one typeface, at its metrics. Register the real faces when line breaking has to match what PowerPoint would do.

render_slide returns the display list — the same drawing contract the browser editor paints, as JSON. There is no PPTX rasterizer yet, so this is a scene description rather than pixels; feed it to your own canvas or renderer.

Collaboration

open_collaborative gives this replica a unique client ID, which peers need in order to converge:

left = Presentation.open_collaborative(data)
right = Presentation.open_collaborative(data)

left.add_text_box(0, x=INCH, y=INCH, width=4 * INCH, height=INCH, text="Q3")
right.apply_update(left.diff(right.state_vector()))   # right now agrees

joiner = Presentation.open_collaborative(data)
joiner.apply_update(left.state_as_update())           # catch up from nothing

A deck from open or open_path is not a replica: it has no client ID of its own, so two of them would author under the same identity and never converge. state_vector, state_as_update, diff, and apply_update raise NotCollaborativeError on such a deck rather than diverging silently, and is_collaborative says which kind you are holding:

deck = Presentation.open(data)
deck.is_collaborative                  # False
deck.state_vector()                    # NotCollaborativeError

The binding generates a client ID when it is omitted and exposes it through the read-only client_id property. Explicit IDs must be unique among connected peers, because Yrs cannot detect duplicates once two replicas have started authoring. Byte inputs accept bytes, bytearray, and memoryview, and an oversized payload is refused before it is copied.

Undo, redo, and attribution

deck.author = "ana"
edit = deck.add_text_box(0, x=INCH, y=INCH, width=INCH, height=INCH, text="Q3")
deck.move_shape(0, edit.shape_id, 0, 0)
deck.add_undo_barrier()      # the next edit starts a new undo step
deck.undo()
deck.redo()

Undo covers this replica's own local edits. Updates applied from a peer are not in local history, so undo will not revert someone else's work. Consecutive edits inside half a second coalesce into one step; add_undo_barrier() splits them.

Setting origin to "agent", "remote", or "system" tags edits for attribution — and takes them out of the local undo stack, which is the point: an agent's write is not something the user undoes by accident.

Writing

save() and save_path() serialize the deck with every accepted edit applied. Slides you did not touch keep their exact source part bytes; edited slides are patched at the XML level, so unmodeled markup survives:

deck = Presentation.open(data)
deck.insert_slide(1)
deck.is_edited                     # True
deck.save_path("copy.pptx")        # edits included

reopened = Presentation.open_path("copy.pptx")
reopened.slide_count               # one more than the source

is_edited reports whether the engine has accepted an edit since the deck was opened. Only an edit the engine accepted sets it: an edit that raised leaves the flag untouched.

Compared with python-pptx

python-pptx betteroffice-pptx
Read shapes and text yes yes
Write shapes and text back to a file yes yes — see Writing
Lay slides out (line breaking, text metrics) no yes, display list
Collaborative editing (CRDT) no yes, Yrs
Undo/redo no yes
Engine pure Python Rust, compiled

python-pptx is a far broader library and covers plenty this does not — charts, tables, and templating in particular. If you need slides laid out, or edits that merge across replicas, that is the gap this fills.

API

Presentation.open(data) / open_path(path) open from bytes or a path
Presentation.open_collaborative(data) open a Yrs replica
deck.snapshot() the whole deck as plain data
deck[key] / deck.slide(key) a Slide by index or ID
deck.slide_ids / slide_count / layouts deck metadata
deck.width_emu / height_emu slide size in EMU
deck.author / deck.origin who an edit is attributed to, and how
deck.story(id) one text flow
deck.media() embedded images and other binary parts
insert_slide / delete_slide / move_slide slide order
add_text_box / add_shape / remove_shape shape lifecycle
move_shape / resize_shape shape geometry
set_shape_fill / set_shape_stroke / set_shape_adjust shape styling
insert_text / delete_text / format_text text editing
insert_paragraph_break split a paragraph
register_font / render_slide layout
diff / apply_update / state_vector / state_as_update Yrs replicas
deck.is_collaborative / deck.client_id whether this deck may exchange updates, and as whom
deck.is_edited whether the engine has accepted an edit since open
undo / redo / add_undo_barrier / can_undo / can_redo history
deck.save() / save_path(path) serialize to PPTX — see Writing

Errors raise PptxError or a more specific subclass: ParseError, RangeError, RenderError, InvalidUpdateError, CollaborativeStateError, NotCollaborativeError. An unknown slide, shape, or story ID raises KeyError; a bad argument — an unsupported geometry, an out-of-range client ID, an unknown parse limit — raises ValueError.

Parser bounds can be tightened for untrusted input:

Presentation.open(untrusted, limits={"max_shapes": 5_000, "max_runs": 50_000})

An unknown limit name raises ValueError rather than being ignored.

Threads

A Presentation is pinned to the thread that opened it, and must also be released there. The engine's undo manager is not Send, so the class is declared unsendable, and pyo3 enforces that in two ways worth knowing about:

  • Touching one from another thread raises pyo3_runtime.PanicException. That is a direct BaseException subclass, so except Exception does not catch it — a worker that guards its work with except Exception will die anyway.
  • Releasing one on another thread leaks it. pyo3 skips the Rust destructor and writes an unraisable RuntimeError instead (visible only through sys.unraisablehook), stranding roughly 1.5 MB per deck. Nothing is raised into your code.

The leak is easy to hit by accident, because the release does not have to be an explicit del:

with ThreadPoolExecutor() as pool:
    decks = [f.result() for f in [pool.submit(load, p) for p in paths]]
# every deck was opened on a worker and is now dropped on the main thread

The cyclic garbage collector counts too. If a Presentation is caught in a reference cycle — a traceback that reaches it, an object graph that points back at itself — the collector frees it wherever it happens to run, which may be any thread. Giving each worker its own Presentation therefore is not enough on its own; the deck must also become garbage on its owning thread. Open, use, and drop each deck inside one thread, and break any cycle holding it before that thread finishes.

Engine calls hold the GIL for their duration, unlike betteroffice-xlsx. Only the file I/O releases it: open_path's read, and the writes in save_path, Media.write and DisplayList.write.

Status

0.0.x, and the API may change before 0.1.0. save writes edits back at the XML level and copies untouched parts through byte for byte; the container is rebuilt, so output is not byte-identical to the source — see Writing.

Wheels are built for Linux (x86_64, aarch64), macOS (arm64, x86_64), and Windows (x86_64) against the stable ABI for CPython 3.9 and up.

Links

Apache-2.0.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

betteroffice_pptx-0.0.1.tar.gz (544.9 kB view details)

Uploaded Source

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

betteroffice_pptx-0.0.1-cp39-abi3-win_amd64.whl (2.1 MB view details)

Uploaded CPython 3.9+Windows x86-64

betteroffice_pptx-0.0.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (2.2 MB view details)

Uploaded CPython 3.9+manylinux: glibc 2.17+ x86-64

betteroffice_pptx-0.0.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl (2.0 MB view details)

Uploaded CPython 3.9+manylinux: glibc 2.17+ ARM64

betteroffice_pptx-0.0.1-cp39-abi3-macosx_11_0_arm64.whl (2.0 MB view details)

Uploaded CPython 3.9+macOS 11.0+ ARM64

betteroffice_pptx-0.0.1-cp39-abi3-macosx_10_12_x86_64.whl (2.1 MB view details)

Uploaded CPython 3.9+macOS 10.12+ x86-64

File details

Details for the file betteroffice_pptx-0.0.1.tar.gz.

File metadata

  • Download URL: betteroffice_pptx-0.0.1.tar.gz
  • Upload date:
  • Size: 544.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for betteroffice_pptx-0.0.1.tar.gz
Algorithm Hash digest
SHA256 2893eb8fe7fb24a65f8957073e72bf61ab17d5815fdbf52d958985444ec705f6
MD5 ba65659217247fc42719ca0f01619e4f
BLAKE2b-256 6ffca8ba48c64e1087c68e8b7d1c3b07120261a3933ae465a11bf599b42a7665

See more details on using hashes here.

Provenance

The following attestation bundles were made for betteroffice_pptx-0.0.1.tar.gz:

Publisher: publish-python-binding.yml on openooxml/betteroffice

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file betteroffice_pptx-0.0.1-cp39-abi3-win_amd64.whl.

File metadata

File hashes

Hashes for betteroffice_pptx-0.0.1-cp39-abi3-win_amd64.whl
Algorithm Hash digest
SHA256 81a6f5baa7d6387f7761af918cf475a98816130eca16711d2cf024b2a220f267
MD5 cf957fb79b3bf690b15f9c3dd25dc316
BLAKE2b-256 471c55dd2f678fbd867d2b6b9650624cd0a0596ff4182ace188eb1f84d43310d

See more details on using hashes here.

Provenance

The following attestation bundles were made for betteroffice_pptx-0.0.1-cp39-abi3-win_amd64.whl:

Publisher: publish-python-binding.yml on openooxml/betteroffice

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file betteroffice_pptx-0.0.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.

File metadata

File hashes

Hashes for betteroffice_pptx-0.0.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
Algorithm Hash digest
SHA256 fcba656e4e49b9e2f9929a6b5a9cd30c83f0b42c328fb4e7e9c1d7bcd6c4fa29
MD5 6c72d9621f99365e8eb22d6f32d4f91f
BLAKE2b-256 91ae9fb695ebd8894467c16e189a50f0c4c0af2c1796da08bb177941355f76f0

See more details on using hashes here.

Provenance

The following attestation bundles were made for betteroffice_pptx-0.0.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl:

Publisher: publish-python-binding.yml on openooxml/betteroffice

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file betteroffice_pptx-0.0.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.

File metadata

File hashes

Hashes for betteroffice_pptx-0.0.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
Algorithm Hash digest
SHA256 638acf906e96cdd7a8b38055b90d5f69953c62d5ae25896eacc5f92c9c1e1783
MD5 96917d3c0bd30cdcd2d4cecb4a1dbdbc
BLAKE2b-256 b84e4acf96798df431a507d5d1d1e9cb2a31fefcffec8256d80a7ada9d63cc46

See more details on using hashes here.

Provenance

The following attestation bundles were made for betteroffice_pptx-0.0.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl:

Publisher: publish-python-binding.yml on openooxml/betteroffice

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file betteroffice_pptx-0.0.1-cp39-abi3-macosx_11_0_arm64.whl.

File metadata

File hashes

Hashes for betteroffice_pptx-0.0.1-cp39-abi3-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 fafd7cf0ad239ee58685c7e68ceec6d7050312a82b7f946063c08c664dc41067
MD5 7aab507198b4d24e879aeecfa0cbb689
BLAKE2b-256 ed46c02f7cdcfb58d5b2a00c6636489192fbe066b5b6bd552ae1f245a6319bb9

See more details on using hashes here.

Provenance

The following attestation bundles were made for betteroffice_pptx-0.0.1-cp39-abi3-macosx_11_0_arm64.whl:

Publisher: publish-python-binding.yml on openooxml/betteroffice

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file betteroffice_pptx-0.0.1-cp39-abi3-macosx_10_12_x86_64.whl.

File metadata

File hashes

Hashes for betteroffice_pptx-0.0.1-cp39-abi3-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 718411018e15e8f3eaf184a33a603741c839c579f6150eda293fc57c5e04885c
MD5 a656ed6c682441bddf79265f0ed3ebfd
BLAKE2b-256 3f531c9b438972314b3d5c80a1580850f4d5baa8676c52f85824b2b896b16ab3

See more details on using hashes here.

Provenance

The following attestation bundles were made for betteroffice_pptx-0.0.1-cp39-abi3-macosx_10_12_x86_64.whl:

Publisher: publish-python-binding.yml on openooxml/betteroffice

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page