← Blog

ruby_pptx: PowerPoint-Dateien mit Ruby

Wer in Python eine PowerPoint-Präsentation per Code erzeugen will, greift zu python-pptx von Steve Canny. Die Bibliothek ist gut getestet und bildet das OOXML-Format PresentationML vollständig als Objektmodell ab. Ich wollte dasselbe in Ruby haben, also habe ich sie portiert.

ruby_pptx erstellt, liest und bearbeitet .pptx-Dateien. Darunter liegt dasselbe Objektmodell wie bei python-pptx. Darüber liegt eine öffentliche API, die für Ruby neu gestaltet ist: Keyword-Argumente, explizite Längenangaben, Collections, die sich wie Arrays verhalten, und Pattern Matching.

Installation

Das Gem benötigt Ruby 3.3 oder neuer:

gem install ruby_pptx

Es hängt von rubyzip, Nokogiri und REXML ab. Die aktuelle Version ist 0.2.0, der Quellcode liegt auf GitHub.

Eine erste Folie

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")

Längen und 2.cm dank Refinement

PowerPoint speichert fast alle Abstände in EMU (English Metric Units), einer Ganzzahl, bei der ein Zoll 914’400 entspricht. ruby_pptx nimmt für Positionen und Grössen keine nackten Zahlen, sondern Pptx::Length-Objekte. Diese erzeugen Sie mit Hilfsmethoden auf dem Modul Pptx: Pptx.emu, Pptx.inches, Pptx.cm, Pptx.mm, Pptx.pt und Pptx.centipoints. Eine Length speichert den EMU-Wert als Ganzzahl, rechnet mit .emu, .inches, .cm, .mm und .pt zurück und unterstützt +, - und * Zahl.

Das ist eindeutig, aber überall Pptx.cm(2) zu schreiben, wird schnell unübersichtlich. Deshalb bringt das Gem auch ein Refinement mit: Pptx::Lengths. Es wird von require "ruby_pptx" nicht geladen. Sie laden es separat und schalten es mit using ein:

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")

Das Refinement ergänzt Numeric um emu, inches/inch, cm, mm, pt/point/points und centipoints. Es funktioniert also mit Ganzzahlen und Gleitkommazahlen, und jede dieser Methoden gibt eine Pptx::Length zurück. Ohne Refinement sehen dieselben zwei Formen so aus:

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)])

Dass es ein Refinement ist, ist Absicht. using Pptx::Lengths wirkt nur in der Datei, in der es steht. Numeric bekommt also im Rest Ihrer Anwendung und in anderen Gems im selben Prozess keine Methode cm. Wer die Methoden wirklich überall haben will, kann mit require "ruby_pptx/core_ext" Numeric für den ganzen Prozess erweitern. In einer Bibliothek würde ich beim Refinement bleiben.

Eine ganze Präsentation aufbauen

Bei generierten Berichten möchte ich die Präsentation meistens von oben nach unten beschreiben. Pptx.build ist eine dünne deklarative Schicht über derselben 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")

Bestehende Präsentationen lesen

Beim Einlesen funktioniert case/in. Enum-Werte erscheinen im Pattern als Symbole:

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 lassen sich wie Arrays indexieren (slides[-1], slides[1..3]), und das Lesen einer Eigenschaft verändert das Dokument nie. Bei python-pptx schreiben einige Getter, etwa für Achsen- und Diagrammtitel, in die Datei. Hier nicht.

Was abgedeckt ist

Jede öffentliche Klasse und jedes öffentliche Member von python-pptx 1.0.2 hat ein Gegenstück, und ein Spec prüft das direkt gegen python-pptx. Dazu gehören Text und Schriften, Füllungen und Linien, Bilder, Videos, Tabellen mit verbundenen Zellen, Verbinder, Gruppen, Platzhalter, OLE-Objekte, Sprechernotizen und Diagramme. Die Specs vergleichen ausserdem die Pakete, die beide Bibliotheken für dieselbe Operation schreiben, Teil für Teil. Wo ruby_pptx bewusst abweicht, meist weil es Fehler von python-pptx nicht nachbaut, ist jeder Fall in PORTING.md aufgeführt.

Dazu kommen fünf Dinge, die python-pptx nicht kann:

Betrieb in ruby.wasm

Version 0.2.0 hat ein XML-Backend in reinem Ruby auf Basis von REXML hinzugefügt. Wenn Nokogiri nicht geladen werden kann, zum Beispiel unter ruby.wasm im Browser, weicht das Gem ohne jede Konfiguration auf REXML aus. Die Ausgabe ist dieselbe, und die CI führt jeden Spec mit beiden Backends aus. REXML ist allerdings langsamer: Bei einer Präsentation mit 60 Folien mit Text, Tabellen und Diagrammen dauerten Aufbauen und Speichern etwa 3,3 s statt 0,5 s mit Nokogiri. Welches Backend aktiv ist, verrät Pptx.xml_backend.

Stand und Dank

ruby_pptx ist noch jung: 0.1.0 und 0.2.0 sind beide am 28. September 2026 erschienen. Bis 1.0 kann eine Minor-Version die öffentliche API ändern. Wenn Sie darauf aufbauen, sollten Sie die Version also festlegen.

Das Gem steht unter der MIT-Lizenz. Es ist eine Portierung von python-pptx von Steve Canny, ebenfalls MIT-lizenziert, und enthält einige Vorlagendateien von python-pptx, etwa die Standardpräsentation und die Notizvorlagen. Der grösste Teil der Anerkennung für das Design des Objektmodells gebührt diesem Projekt.

Wenn Sie es ausprobieren und etwas nicht funktioniert oder sich nicht nach Ruby anfühlt, freue ich mich über eine Rückmeldung. Code und Issues finden Sie auf GitHub, das Gem auf RubyGems.