← Blog

ruby_pptx: RubyでPowerPointファイルを扱う

PythonでPowerPointのスライドをコードから生成したいときは、Steve Cannyさんのpython-pptxを使うのが定番です。よくテストされたライブラリで、OOXMLのPresentationML形式をオブジェクトモデルとして一通り表現しています。同じものをRubyでも使いたかったので、移植することにしました。

ruby_pptxは.pptxファイルの作成・読み込み・更新ができるgemです。内部のオブジェクトモデルはpython-pptxと同じものですが、公開APIはRuby向けに設計し直しています。キーワード引数、明示的な長さの指定、配列のように扱えるコレクション、そしてパターンマッチに対応しています。

インストール

Ruby 3.3以降が必要です。

gem install ruby_pptx

依存しているのはrubyzip、Nokogiri、REXMLです。現在のバージョンは0.2.0で、ソースコードはGitHubで公開しています。

最初のスライド

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

長さの指定と、refinementによる2.cm

PowerPointはほとんどの距離をEMU(English Metric Units)という整数で保存しています。1インチは914,400 EMUです。ruby_pptxでは位置やサイズにただの数値を渡すのではなく、Pptx::Lengthオブジェクトを使います。作成にはPptxモジュールのヘルパー、Pptx.emu、Pptx.inches、Pptx.cm、Pptx.mm、Pptx.pt、Pptx.centipointsを使います。LengthはEMUの整数値を保持していて、.emu、.inches、.cm、.mm、.ptで各単位に変換でき、+、-、* 数値で計算もできます。

明示的でわかりやすい反面、あちこちにPptx.cm(2)と書くと読みにくくなります。そこでこのgemにはPptx::Lengthsというrefinementも用意しています。require "ruby_pptx"では読み込まれないので、別途requireしてから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")

このrefinementはNumericにemu、inches/inch、cm、mm、pt/point/points、centipointsを追加します。そのため整数でも浮動小数点数でも使え、どのメソッドもPptx::Lengthを返します。refinementを使わない場合、同じ2つの図形は次のように書きます。

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

refinementにしたのは意図的です。using Pptx::Lengthsの効果は、それを書いたファイルの中だけに限られます。アプリケーションのほかの部分や、同じプロセスで動くほかのgemのNumericにcmメソッドが増えることはありません。どうしてもすべての場所で使いたい場合は、require "ruby_pptx/core_ext"でプロセス全体のNumericを拡張することもできます。ただ、ライブラリの中ではrefinementを使うことをおすすめします。

プレゼンテーション全体を組み立てる

レポートを自動生成するときは、スライドを上から順に記述したいことが多いです。Pptx.buildは同じ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")

既存のファイルを読む

読み込んだスライドはcase/inで扱えます。enumの値はパターンの中ではシンボルとして現れます。

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

コレクションは配列と同じようにインデックスで参照できます(slides[-1]、slides[1..3]など)。また、プロパティを読むだけでドキュメントが変わることはありません。python-pptxでは、軸やグラフのタイトルなど一部のgetterがファイルに書き込みますが、ruby_pptxではそうなりません。

対応範囲

python-pptx 1.0.2のすべての公開クラスとメンバーに対応するものがあり、specがpython-pptx本体と照らし合わせて確認しています。テキストとフォント、塗りつぶしと線、画像、動画、セル結合ができる表、コネクタ、グループ、プレースホルダー、OLEオブジェクト、発表者ノート、グラフなどが含まれます。さらにspecでは、同じ操作に対して両方のライブラリが書き出すパッケージをパーツごとに比較しています。ruby_pptxが意図的に異なる動作をする箇所は、ほとんどがpython-pptxのバグを再現しないためのもので、PORTING.mdに一つずつ記載しています。

python-pptxにはない機能も5つあります。

ruby.wasmでの動作

バージョン0.2.0では、REXMLをベースにしたピュアRubyのXMLバックエンドを追加しました。ブラウザ上のruby.wasmなど、Nokogiriを読み込めない環境では、設定なしで自動的にREXMLに切り替わります。出力は同じで、CIではすべてのspecを両方のバックエンドで実行しています。ただしREXMLは遅く、テキスト・表・グラフを含む60枚のスライドの作成と保存に、Nokogiriの約0.5秒に対して約3.3秒かかりました。どちらが使われているかはPptx.xml_backendで確認できます。

現状とクレジット

ruby_pptxはまだ新しいgemで、0.1.0と0.2.0はどちらも2026年9月28日にリリースしました。1.0になるまではマイナーバージョンで公開APIが変わる可能性があるので、使う場合はバージョンを固定しておくことをおすすめします。

ライセンスはMITです。Steve Cannyさんのpython-pptx(こちらもMITライセンス)の移植で、デフォルトのプレゼンテーションやノートのテンプレートなど、python-pptxのテンプレートファイルをいくつか同梱しています。オブジェクトモデルの設計の功績の大部分はpython-pptxにあります。

試してみて動かないところやRubyらしくないと感じるところがあれば、ぜひ教えてください。コードとissueはGitHubに、gemはRubyGemsにあります。