ruby_pptx : des fichiers PowerPoint depuis Ruby
En Python, lorsqu’il faut générer une présentation PowerPoint par code, on se tourne vers python-pptx de Steve Canny. C’est une bibliothèque bien testée, dotée d’un modèle objet complet pour le format OOXML PresentationML. Je voulais la même chose en Ruby, alors je l’ai portée.
ruby_pptx crée, lit et modifie des fichiers .pptx. En dessous se trouve le même modèle objet que python-pptx. Au-dessus, une API publique repensée pour Ruby : arguments nommés, longueurs explicites, collections qui se comportent comme des tableaux, et pattern matching.
Installation
Le gem nécessite Ruby 3.3 ou plus récent :
gem install ruby_pptx
Il dépend de rubyzip, Nokogiri et REXML. La version actuelle est la 0.2.0, et le code source se trouve sur GitHub.
Une première diapositive
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")
Les longueurs, et 2.cm grâce à un refinement
PowerPoint enregistre presque toutes les distances en EMU (English Metric Units), un entier où un pouce vaut 914 400. ruby_pptx n’accepte pas de nombres bruts pour les positions et les tailles. Il utilise des objets Pptx::Length, que l’on crée avec des helpers du module Pptx : Pptx.emu, Pptx.inches, Pptx.cm, Pptx.mm, Pptx.pt et Pptx.centipoints. Un Length stocke l’entier EMU, se reconvertit avec .emu, .inches, .cm, .mm et .pt, et prend en charge +, - et * nombre.
C’est explicite, mais Pptx.cm(2) partout devient vite verbeux. Le gem fournit donc aussi un refinement, Pptx::Lengths. Il n’est pas chargé par require "ruby_pptx" ; on le charge séparément et on l’active avec 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")
Le refinement ajoute emu, inches/inch, cm, mm, pt/point/points et centipoints à Numeric ; ces méthodes fonctionnent donc sur les entiers comme sur les flottants, et chacune renvoie un Pptx::Length. Sans le refinement, les deux mêmes formes s’écrivent ainsi :
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)])
J’en ai fait un refinement à dessein. using Pptx::Lengths n’agit que sur le fichier où il apparaît ; Numeric ne gagne donc pas de méthode cm dans le reste de votre application ni dans les autres gems qui tournent dans le même processus. Si vous voulez vraiment ces méthodes partout, require "ruby_pptx/core_ext" modifie Numeric pour tout le processus. Dans une bibliothèque, je m’en tiendrais au refinement.
Construire une présentation entière
Pour les rapports générés, je préfère en général décrire la présentation de haut en bas. Pptx.build est une fine couche déclarative au-dessus de la même 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")
Lire des présentations existantes
La lecture d’une présentation fonctionne avec case/in. Les valeurs d’énumération apparaissent comme des symboles dans un 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
Les collections s’indexent comme des tableaux (slides[-1], slides[1..3]), et lire une propriété ne modifie jamais le document. Dans python-pptx, certains getters, comme les titres d’axes et de graphiques, écrivent dans le fichier. Ici, non.
Ce qui est couvert
Chaque classe et chaque membre public de python-pptx 1.0.2 a son équivalent, et une spec le vérifie par rapport à python-pptx lui-même. Cela comprend le texte et les polices, les remplissages et les traits, les images, les vidéos, les tableaux avec cellules fusionnées, les connecteurs, les groupes, les espaces réservés, les objets OLE, les notes du présentateur et les graphiques. Les specs comparent aussi, partie par partie, les paquets que les deux bibliothèques écrivent pour une même opération. Là où ruby_pptx diffère volontairement, surtout parce qu’il ne reproduit pas les bugs de python-pptx, PORTING.md recense chaque cas.
Il y a aussi cinq choses que python-pptx ne fait pas :
- les images SVG (vous fournissez une image PNG de repli pour les visionneuses plus anciennes)
- les sections de diapositives
- la répartition d’un long tableau sur autant de diapositives que nécessaire
- les graphiques combinés avec axe secondaire
- la définition par code d’un masque des diapositives, de son thème et de ses dispositions
Exécution dans ruby.wasm
La version 0.2.0 a ajouté un backend XML en Ruby pur basé sur REXML. Si Nokogiri ne peut pas être chargé, par exemple sous ruby.wasm dans le navigateur, le gem se rabat sur REXML sans aucune configuration. Le résultat est identique, et la CI exécute toutes les specs avec les deux backends. REXML est toutefois plus lent : pour une présentation de 60 diapositives avec texte, tableaux et graphiques, la construction et l’enregistrement ont pris environ 3,3 s, contre 0,5 s avec Nokogiri. Pptx.xml_backend indique le backend actif.
État et remerciements
ruby_pptx est jeune : les versions 0.1.0 et 0.2.0 sont toutes deux sorties le 28 septembre 2026. Jusqu’à la 1.0, une version mineure peut modifier l’API publique ; fixez donc la version si vous en dépendez.
Le gem est sous licence MIT. C’est un portage de python-pptx de Steve Canny, lui aussi sous licence MIT, et il inclut certains fichiers modèles de python-pptx, comme la présentation par défaut et les modèles de notes. L’essentiel du mérite pour la conception du modèle objet lui revient.
Si vous l’essayez et que quelque chose casse ou vous semble peu idiomatique en Ruby, je serais content de le savoir. Le code et les tickets sont sur GitHub, et le gem sur RubyGems.