← Articoli

ruby_pptx: file PowerPoint con Ruby

In Python, quando bisogna generare una presentazione PowerPoint da codice, si ricorre a python-pptx di Steve Canny. È una libreria ben testata, con un modello a oggetti completo per il formato OOXML PresentationML. Volevo la stessa cosa in Ruby, quindi l’ho portata.

ruby_pptx crea, legge e modifica file .pptx. Sotto c’è lo stesso modello a oggetti di python-pptx; sopra, un’API pubblica riprogettata per Ruby: keyword argument, lunghezze esplicite, collezioni che si comportano come array e pattern matching.

Installazione

La gem richiede Ruby 3.3 o successivo:

gem install ruby_pptx

Dipende da rubyzip, Nokogiri e REXML. La versione attuale è la 0.2.0 e il sorgente si trova su GitHub.

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

Lunghezze, e 2.cm con una refinement

PowerPoint memorizza quasi ogni distanza in EMU (English Metric Units), un intero in cui un pollice vale 914’400. ruby_pptx non accetta numeri nudi per posizioni e dimensioni: usa oggetti Pptx::Length, che si creano con gli helper del modulo Pptx: Pptx.emu, Pptx.inches, Pptx.cm, Pptx.mm, Pptx.pt e Pptx.centipoints. Una Length contiene l’intero EMU, si riconverte con .emu, .inches, .cm, .mm e .pt e supporta +, - e * numero.

È esplicito, ma Pptx.cm(2) ovunque diventa pesante. Per questo la gem include anche una refinement, Pptx::Lengths. Non viene caricata da require "ruby_pptx": va richiesta a parte e attivata con 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")

La refinement aggiunge emu, inches/inch, cm, mm, pt/point/points e centipoints a Numeric, quindi funzionano sia con interi sia con float, e ognuno restituisce una Pptx::Length. Senza la refinement, le stesse due forme si scrivono così:

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

Ho scelto apposta una refinement. using Pptx::Lengths agisce solo sul file in cui compare, quindi Numeric non acquisisce un metodo cm nel resto della vostra applicazione o in altre gem che girano nello stesso processo. Se volete davvero i metodi ovunque, require "ruby_pptx/core_ext" modifica Numeric per l’intero processo. All’interno di una libreria resterei con la refinement.

Costruire un’intera presentazione

Per i report generati di solito voglio descrivere la presentazione dall’inizio alla fine. Pptx.build è un sottile strato dichiarativo sopra la stessa 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")

Leggere presentazioni esistenti

Per rileggere una presentazione si usa case/in. Nei pattern i valori enum compaiono come simboli:

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

Le collezioni si indicizzano come array (slides[-1], slides[1..3]) e leggere una proprietà non modifica mai il documento. In python-pptx alcuni getter, come i titoli degli assi e dei grafici, scrivono nel file. Qui no.

Cosa copre

Ogni classe e membro pubblico di python-pptx 1.0.2 ha un corrispettivo, e una spec lo verifica direttamente contro python-pptx. Questo include testo e font, riempimenti e linee, immagini, video, tabelle con celle unite, connettori, gruppi, segnaposto, oggetti OLE, note del relatore e grafici. Le spec confrontano anche, parte per parte, i pacchetti che le due librerie scrivono per la stessa operazione. Dove ruby_pptx si discosta di proposito, per lo più perché non riproduce i bug di python-pptx, PORTING.md elenca ogni caso.

Ci sono inoltre cinque cose che python-pptx non fa:

Esecuzione in ruby.wasm

La versione 0.2.0 ha aggiunto un backend XML in puro Ruby basato su REXML. Se Nokogiri non si carica, per esempio con ruby.wasm nel browser, la gem passa a REXML senza alcuna configurazione. Il risultato è identico e la CI esegue ogni spec con entrambi i backend. REXML però è più lento: per una presentazione di 60 slide con testo, tabelle e grafici, costruzione e salvataggio hanno richiesto circa 3,3 s contro 0,5 s con Nokogiri. Quale backend sia attivo lo indica Pptx.xml_backend.

Stato e riconoscimenti

ruby_pptx è giovane: la 0.1.0 e la 0.2.0 sono uscite entrambe il 28 settembre 2026. Fino alla 1.0 una versione minore può cambiare l’API pubblica, quindi fissate la versione se ne dipendete.

La gem è rilasciata con licenza MIT. È un port di python-pptx di Steve Canny, anch’esso con licenza MIT, e include alcuni file modello di python-pptx, come la presentazione predefinita e i modelli delle note. Gran parte del merito per il design del modello a oggetti va a quel progetto.

Se la provate e qualcosa non funziona o vi sembra poco «Ruby», mi farebbe piacere saperlo. Codice e issue sono su GitHub, la gem è su RubyGems.