ruby_pptx: PowerPoint files from Ruby
In Python, if you need to generate a PowerPoint deck from code, you reach for python-pptx by Steve Canny. It is a well-tested library with a complete object model for the OOXML PresentationML format. I wanted the same thing in Ruby, so I ported it.
ruby_pptx creates, reads and updates .pptx files. Underneath is the same object model as python-pptx. On top is a public API redesigned for Ruby: keyword arguments, explicit lengths, collections that behave like arrays, and pattern matching.
Installation
The gem needs Ruby 3.3 or newer:
gem install ruby_pptx
It depends on rubyzip, Nokogiri and REXML. The current version is 0.2.0, and the source is on GitHub.
A first slide
require "ruby_pptx"
prs = Pptx::Presentation.new_default # or .open("deck.pptx")
slide = prs.slides.add(prs.slide_layouts["Title and Content"])
slide.shapes.title.text = "Quarterly Review"
body = slide.placeholders[1].text_frame
body.text = "Revenue up 12%\nCosts flat"
body.paragraphs.first.runs.first.font.tap do |font|
font.bold = true
font.size = Pptx.pt(24)
font.color.rgb = Pptx::RGBColor["C0504D"]
end
box = slide.shapes.add_shape(:rounded_rectangle,
at: [Pptx.inches(1), Pptx.inches(5)],
size: [Pptx.inches(3), Pptx.inches(1)])
box.fill.solid
box.fill.fore_color.rgb = Pptx::RGBColor["1F497D"]
box.text_frame.text = "Next steps"
slide.notes = "Mention the Q3 dip."
prs.save("out.pptx")
Lengths, and 2.cm with a refinement
PowerPoint stores almost every distance in EMU (English Metric Units), an integer where one inch is 914,400. ruby_pptx doesn’t take bare numbers for positions and sizes. It uses Pptx::Length objects, and you create them with helpers on the Pptx module: Pptx.emu, Pptx.inches, Pptx.cm, Pptx.mm, Pptx.pt and Pptx.centipoints. A Length stores the EMU integer, converts back with .emu, .inches, .cm, .mm and .pt, and supports +, - and * number.
That’s explicit, but Pptx.cm(2) everywhere gets noisy. So the gem also ships a refinement, Pptx::Lengths. It isn’t loaded by require "ruby_pptx"; you require it separately and switch it on with using:
require "ruby_pptx"
require "ruby_pptx/refinements"
using Pptx::Lengths
prs = Pptx::Presentation.new_default
slide = prs.slides.add(prs.slide_layouts["Blank"])
box = slide.shapes.add_textbox(at: [2.cm, 2.cm], size: [10.cm, 1.5.cm])
box.text_frame.text = "Placed in centimetres"
slide.shapes.add_shape(:rectangle, at: [2.cm, 4.cm], size: [3.inch, 1.inch])
2.cm.emu #=> 720000
1.inch == 72.points #=> true
prs.save("out.pptx")
The refinement adds emu, inches/inch, cm, mm, pt/point/points and centipoints to Numeric, so they work on both integers and floats, and each one returns a Pptx::Length. Without the refinement, the same two shapes look like this:
slide.shapes.add_textbox(at: [Pptx.cm(2), Pptx.cm(2)], size: [Pptx.cm(10), Pptx.cm(1.5)])
slide.shapes.add_shape(:rectangle, at: [Pptx.cm(2), Pptx.cm(4)],
size: [Pptx.inches(3), Pptx.inches(1)])
I made it a refinement on purpose. using Pptx::Lengths only affects the file it appears in, so Numeric doesn’t gain a cm method in the rest of your app or in other gems that run in the same process. If you really want the methods everywhere, require "ruby_pptx/core_ext" patches Numeric for the whole process. Inside a library, I would stick with the refinement.
Building a whole deck
For generated reports I usually want to describe the deck from top to bottom. Pptx.build is a thin declarative layer over the same API:
deck = Pptx.build do |d|
d.slide_size = :widescreen
d.slide("Title Slide") do |s|
s.title = "Annual Report"
s.subtitle = "Prepared in Ruby"
end
d.section("Detail") do
d.slide("Blank") do |s|
s.shape :rounded_rectangle, at: [Pptx.inches(1), Pptx.inches(1)],
size: [Pptx.inches(3), Pptx.inches(1)],
fill: "1F497D", text: "Next steps"
s.chart :column_clustered, categories: %w[East West],
series: { "Q1" => [1, 2] },
at: [Pptx.inches(1), Pptx.inches(3)],
size: [Pptx.inches(6), Pptx.inches(4)]
end
end
end
deck.save("out.pptx")
Reading existing decks
Reading a deck back works with case/in. Enum values show up as symbols in a pattern:
slide.shapes.each do |shape|
case shape
in {shape_type: :PICTURE, name:} then puts "picture #{name}"
in {shape_type: :PLACEHOLDER, placeholder_format: {type: :TITLE}}
then puts shape.text_frame.text
in {width:} if width > Pptx.inches(5) then puts "#{shape.name} is wide"
else next
end
end
Collections index like arrays (slides[-1], slides[1..3]), and reading a property never changes the document. In python-pptx some getters, such as axis and chart titles, write to the file. Here they do not.
What it covers
Every public class and member of python-pptx 1.0.2 has a counterpart, and a spec checks this against python-pptx itself. That includes text and fonts, fills and lines, pictures, movies, tables with merged cells, connectors, groups, placeholders, OLE objects, speaker notes and charts. The specs also compare the packages both libraries write for the same operation, part by part. Where ruby_pptx deliberately differs, mostly because it does not reproduce python-pptx bugs, PORTING.md lists each case.
There are also five things python-pptx does not do:
- SVG pictures (you supply a PNG fallback for older viewers)
- slide sections
- paging a long table across as many slides as it needs
- combo charts with a secondary axis
- defining a slide master, its theme and its layouts in code
Running in ruby.wasm
Version 0.2.0 added a pure Ruby XML backend built on REXML. If Nokogiri cannot load, for example under ruby.wasm in the browser, the gem falls back to REXML without any configuration. The output is the same, and the CI runs every spec under both backends. REXML is slower, though. In a 60-slide deck with text, tables and charts, building and saving took about 3.3 s compared to 0.5 s with Nokogiri. You can check which one is active with Pptx.xml_backend.
Status and credits
ruby_pptx is young: 0.1.0 and 0.2.0 were both released on 2026-09-28. Until 1.0, a minor version may change the public API, so pin the version if you depend on it.
The gem is MIT licensed. It is a port of python-pptx by Steve Canny, also MIT licensed, and it includes some of python-pptx’s template files, such as the default deck and the notes templates. Most of the credit for the design of the object model goes there.
If you try it and something breaks or feels un-Ruby-like, I would like to hear about it. The code and issues are on GitHub, and the gem is on RubyGems.