← Blog

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:

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.