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 directBaseExceptionsubclass, soexcept Exceptiondoes not catch it — a worker that guards its work withexcept Exceptionwill die anyway. - Releasing one on another thread leaks it. pyo3 skips the Rust destructor
and writes an unraisable
RuntimeErrorinstead (visible only throughsys.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
- BetterOffice — the project
- Documentation
- Source —
bindings/python-pptx - betteroffice-pptx on crates.io — the engine this wraps
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
Built Distributions
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2893eb8fe7fb24a65f8957073e72bf61ab17d5815fdbf52d958985444ec705f6
|
|
| MD5 |
ba65659217247fc42719ca0f01619e4f
|
|
| BLAKE2b-256 |
6ffca8ba48c64e1087c68e8b7d1c3b07120261a3933ae465a11bf599b42a7665
|
Provenance
The following attestation bundles were made for betteroffice_pptx-0.0.1.tar.gz:
Publisher:
publish-python-binding.yml on openooxml/betteroffice
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
betteroffice_pptx-0.0.1.tar.gz -
Subject digest:
2893eb8fe7fb24a65f8957073e72bf61ab17d5815fdbf52d958985444ec705f6 - Sigstore transparency entry: 2492583060
- Sigstore integration time:
-
Permalink:
openooxml/betteroffice@12d83a986cfba30a48e382930c2915217ea94d16 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/openooxml
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-python-binding.yml@12d83a986cfba30a48e382930c2915217ea94d16 -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file betteroffice_pptx-0.0.1-cp39-abi3-win_amd64.whl.
File metadata
- Download URL: betteroffice_pptx-0.0.1-cp39-abi3-win_amd64.whl
- Upload date:
- Size: 2.1 MB
- Tags: CPython 3.9+, Windows x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
81a6f5baa7d6387f7761af918cf475a98816130eca16711d2cf024b2a220f267
|
|
| MD5 |
cf957fb79b3bf690b15f9c3dd25dc316
|
|
| BLAKE2b-256 |
471c55dd2f678fbd867d2b6b9650624cd0a0596ff4182ace188eb1f84d43310d
|
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
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
betteroffice_pptx-0.0.1-cp39-abi3-win_amd64.whl -
Subject digest:
81a6f5baa7d6387f7761af918cf475a98816130eca16711d2cf024b2a220f267 - Sigstore transparency entry: 2492584488
- Sigstore integration time:
-
Permalink:
openooxml/betteroffice@12d83a986cfba30a48e382930c2915217ea94d16 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/openooxml
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-python-binding.yml@12d83a986cfba30a48e382930c2915217ea94d16 -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file betteroffice_pptx-0.0.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.
File metadata
- Download URL: betteroffice_pptx-0.0.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
- Upload date:
- Size: 2.2 MB
- Tags: CPython 3.9+, manylinux: glibc 2.17+ x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
fcba656e4e49b9e2f9929a6b5a9cd30c83f0b42c328fb4e7e9c1d7bcd6c4fa29
|
|
| MD5 |
6c72d9621f99365e8eb22d6f32d4f91f
|
|
| BLAKE2b-256 |
91ae9fb695ebd8894467c16e189a50f0c4c0af2c1796da08bb177941355f76f0
|
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
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
betteroffice_pptx-0.0.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl -
Subject digest:
fcba656e4e49b9e2f9929a6b5a9cd30c83f0b42c328fb4e7e9c1d7bcd6c4fa29 - Sigstore transparency entry: 2492583316
- Sigstore integration time:
-
Permalink:
openooxml/betteroffice@12d83a986cfba30a48e382930c2915217ea94d16 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/openooxml
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-python-binding.yml@12d83a986cfba30a48e382930c2915217ea94d16 -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file betteroffice_pptx-0.0.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl.
File metadata
- Download URL: betteroffice_pptx-0.0.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
- Upload date:
- Size: 2.0 MB
- Tags: CPython 3.9+, manylinux: glibc 2.17+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
638acf906e96cdd7a8b38055b90d5f69953c62d5ae25896eacc5f92c9c1e1783
|
|
| MD5 |
96917d3c0bd30cdcd2d4cecb4a1dbdbc
|
|
| BLAKE2b-256 |
b84e4acf96798df431a507d5d1d1e9cb2a31fefcffec8256d80a7ada9d63cc46
|
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
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
betteroffice_pptx-0.0.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl -
Subject digest:
638acf906e96cdd7a8b38055b90d5f69953c62d5ae25896eacc5f92c9c1e1783 - Sigstore transparency entry: 2492583755
- Sigstore integration time:
-
Permalink:
openooxml/betteroffice@12d83a986cfba30a48e382930c2915217ea94d16 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/openooxml
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-python-binding.yml@12d83a986cfba30a48e382930c2915217ea94d16 -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file betteroffice_pptx-0.0.1-cp39-abi3-macosx_11_0_arm64.whl.
File metadata
- Download URL: betteroffice_pptx-0.0.1-cp39-abi3-macosx_11_0_arm64.whl
- Upload date:
- Size: 2.0 MB
- Tags: CPython 3.9+, macOS 11.0+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
fafd7cf0ad239ee58685c7e68ceec6d7050312a82b7f946063c08c664dc41067
|
|
| MD5 |
7aab507198b4d24e879aeecfa0cbb689
|
|
| BLAKE2b-256 |
ed46c02f7cdcfb58d5b2a00c6636489192fbe066b5b6bd552ae1f245a6319bb9
|
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
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
betteroffice_pptx-0.0.1-cp39-abi3-macosx_11_0_arm64.whl -
Subject digest:
fafd7cf0ad239ee58685c7e68ceec6d7050312a82b7f946063c08c664dc41067 - Sigstore transparency entry: 2492583605
- Sigstore integration time:
-
Permalink:
openooxml/betteroffice@12d83a986cfba30a48e382930c2915217ea94d16 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/openooxml
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-python-binding.yml@12d83a986cfba30a48e382930c2915217ea94d16 -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file betteroffice_pptx-0.0.1-cp39-abi3-macosx_10_12_x86_64.whl.
File metadata
- Download URL: betteroffice_pptx-0.0.1-cp39-abi3-macosx_10_12_x86_64.whl
- Upload date:
- Size: 2.1 MB
- Tags: CPython 3.9+, macOS 10.12+ x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
718411018e15e8f3eaf184a33a603741c839c579f6150eda293fc57c5e04885c
|
|
| MD5 |
a656ed6c682441bddf79265f0ed3ebfd
|
|
| BLAKE2b-256 |
3f531c9b438972314b3d5c80a1580850f4d5baa8676c52f85824b2b896b16ab3
|
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
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
betteroffice_pptx-0.0.1-cp39-abi3-macosx_10_12_x86_64.whl -
Subject digest:
718411018e15e8f3eaf184a33a603741c839c579f6150eda293fc57c5e04885c - Sigstore transparency entry: 2492584037
- Sigstore integration time:
-
Permalink:
openooxml/betteroffice@12d83a986cfba30a48e382930c2915217ea94d16 -
Branch / Tag:
refs/heads/main - Owner: https://github.com/openooxml
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-python-binding.yml@12d83a986cfba30a48e382930c2915217ea94d16 -
Trigger Event:
workflow_dispatch
-
Statement type: