前提
- PowerPointやGoogle Slideは自動生成が可能
- 特定のデザインにデータを流し込みたい人向け
Presentationの生成自動化についての前提
- PowerPoint
- OpenXML(Office Open XML, OOXML)形式
-
.pptx
をzipにリネームして解答すると中身が見られる - つまり操作としてはzip解凍して中のxmlを修正したりすればなんとかなる
- パワポの中のグラフを弄るのは実はパワポの中にExcelが内包されているので二重に面倒
- ただ、パワポのほうでキャッシュされている数値を弄れば見た目上は変わる
- できること
- 画像操作
- グラフ操作
- テキスト操作
- Google Slide
- GASなどでいじれる
- できること
- 画像操作
- テキスト操作
今回何をするか
- 例として、車の販売店がパワポで販売している車の一覧を作るという状況を考える
- 毎回スライドを更新するのはつらいので、データベースやSpreadsheetからデータを取ってきて自動更新したい
- PowerPointの自動生成例では説明の簡易化のためにデータベースから取得する部分を割愛
- Google Slideの自動生成例ではSpreadsheetからデータを取得する
PowerPointの自動生成を行う
準備
- データベースを容易(もしくはcsvなど)
- テンプレートを容易
操作方法
基本的な操作としては以下の流れで実装する.
- テンプレートpptxを解凍
- データリソース分だけ以下の処理をする
- slide1.xmlをコピーして連番付けして中のテキスト部分置換して保存
- 画像を連番付けして保存
- ファイル群を保存
パーツ群
覚える必要がないが、この記事を見て拡張性のあるスクリプト書きたいと思った人向けに各ファイルの解説をしておく.
- _rels
- [Content_Types].xml
- ファイルごとの属性定義
- スライドについてだけ追記する
<Override PartName="/ppt/slides/slide1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slide+xml" />
- [Content_Types].xml
- docProps
- app.xml
- 基本情報
- いじらない
- core.xml
- タイトル, 作成者名, 編集者名, 作成日時, 最新更新日時などを編集したいときは弄る
- thumbnail.jpeg
- サムネイル変えたいときは弄る
- app.xml
- ppt
- _rels
- presentation.xml.rels
- 各ファイルへのrelationshipが貼られている
- slideN
- presProps
- viewProps
- themeN
- tableStyles
- slideNを増やしたいので、pressProps以降はrID (relationship ID)をずらす
- 各ファイルへのrelationshipが貼られている
- presentation.xml.rels
- media
- 画像が入っている
- presentation.xml
- slideMasterとslideが構造化されている
- slideを追加する
- presProps.xml
- プレゼンテーションプロパティーパーツ
- 弄らない
- slideLayouts
- スライドのレイアウト
- 弄らない
- slideMaster
- スライドマスター
- 弄らない
- slides
- _rels
- slide1.xml.rels
- 画像やslideLayoutへの参照
- チャートへの参照などもここに
- スライド毎に作る
- 画像やslideLayoutへの参照
- slide1.xml.rels
- slide1.xml
- 画像やテキスト実体が内包されている
- ここをメインで弄る
- _rels
- tableStyles.xml
- テーブルスタイル
- 弄らない
- theme
- theme1.xml
- テーマ
- 弄らない
- theme1.xml
- viewProps.xml
- ビュープロパティーパーツ
- 弄らない
- _rels
実装例
# 簡単のためにrubyzip, nokogiriを使う
require 'rubygems'
require 'zip'
require 'nokogiri'
require 'open-uri'
# データソースの準備, 基本はcsvやデータベースから引っ張ってくる
class Car
attr_accessor :name, :description, :price, :image_url
def initialize(name, desc, price, url)
@name = name
@description = desc
@price = price
@image_url = url
end
end
carlist = [
Car.new('hoge', 'hoge_description', '300円', '{画像URL(1)}'),
Car.new('piyo', 'piyo_description', '400円', '{画像URL(2)}'),
Car.new('fuga', 'fuga_description', '500円', '{画像URL(3)}')
]
# テンプレpptxの取得
template_pptx = Zip::File.open("/tmp/template_carlist.pptx", Zip::File::CREATE)
template_filenames = ["ppt/slides/slide1.xml", "ppt/slides/_rels/slide1.xml.rels", "ppt/media/image1.png"]
# 処理
Zip::OutputStream.open('/tmp/output.pptx') do |out|
template_pptx.entries.each do |entry|
next if template_filenames.include?(entry.name)
out.put_next_entry(entry.name)
# [Content_Types].xmlの処理
# <Override PartName="/ppt/slides/slide1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slide+xml" /> を追加している
if entry.name == "[Content_Types].xml"
string_buffer = template_pptx.read("[Content_Types].xml")
string_buffer.force_encoding("utf-8")
doc = Nokogiri::XML::Document.parse string_buffer
slide1 = doc.xpath('//*[@PartName="/ppt/slides/slide1.xml"]').first
(2..carlist.count).each do |count|
new_slide = slide1.dup
new_slide[:PartName] = "/ppt/slides/slide#{count}.xml"
slide1.add_next_sibling(new_slide)
end
out.write doc.to_xml
# ppt/_rels/presentation.xml.relsの処理
elsif entry.name == "ppt/_rels/presentation.xml.rels"
string_buffer = template_pptx.read("ppt/_rels/presentation.xml.rels")
string_buffer.force_encoding("utf-8")
doc = Nokogiri::XML::Document.parse string_buffer
slide1 = doc.xpath('//*[@Target="slides/slide1.xml"]').first
(2..carlist.count).each do |count|
new_slide = slide1.dup
new_slide[:Id] = "rId#{1 + count}"
new_slide[:Target] = "slides/slide#{count}.xml"
slide1.add_next_sibling(new_slide)
end
doc.xpath('//*[@Target="presProps.xml"]').first[:Id] = "rId#{carlist.count + 2}"
doc.xpath('//*[@Target="viewProps.xml"]').first[:Id] = "rId#{carlist.count + 3}"
doc.xpath('//*[@Target="theme/theme1.xml"]').first[:Id] = "rId#{carlist.count + 4}"
doc.xpath('//*[@Target="tableStyles.xml"]').first[:Id] = "rId#{carlist.count + 5}"
out.write doc.to_xml
# ppt/presentation.xmlの処理
elsif entry.name == "ppt/presentation.xml"
string_buffer = template_pptx.read("ppt/presentation.xml")
string_buffer.force_encoding("utf-8")
doc = Nokogiri::XML::Document.parse string_buffer
slide1 = doc.xpath('//p:sldId').first
(2..carlist.count).each do |count|
new_slide = slide1.dup
new_slide[:id] = "#{slide1[:id].to_i + count}"
new_slide["r:id"] = "rId#{1 + count}"
slide1.add_next_sibling(new_slide)
end
out.write doc.to_xml
else
out.write entry.get_input_stream.read
end
end
# 本編
# slideN.xml, rels/slideN.xml.rels, ppt/media/imageN.pngを追加
carlist.each_with_index do |car, i|
# slideファイル作成 with 脳筋置換処理
string_buffer = template_pptx.read("ppt/slides/slide1.xml")
string_buffer.force_encoding("utf-8")
string_buffer.gsub!("くるまのなまえ", car.name)
string_buffer.gsub!("くるまのせつめい", car.description)
string_buffer.gsub!("くるまのきんがく", car.price)
out.put_next_entry("ppt/slides/slide#{i + 1}.xml")
out.write string_buffer
# 画像ファイル作成
out.put_next_entry("ppt/media/image#{i + 1}.png")
image_buffer = open(car.image_url).read
out.write image_buffer
# relファイル作成
string_buffer = template_pptx.read("ppt/slides/_rels/slide1.xml.rels")
string_buffer.force_encoding("utf-8")
doc = Nokogiri::XML::Document.parse string_buffer
doc.xpath('//*[@Id="rId2"]').first[:Target] = "../media/image#{i + 1}.png"
out.put_next_entry("ppt/slides/_rels/slide#{i + 1}.xml.rels")
out.write doc.to_xml
end
end
Google Slideの自動生成を行う
準備
- テンプレートとなるスライドを用意
- データソースとするSpreadsheetを準備
操作
- PowerPointとあまり変わらないので割愛
実装例
# デフォルトスライド
var default_slide = SlidesApp.openById('{スライドID}').getSlides()[0];
// 資料作成
// 新しいプレゼンテーションを作りデフォルトスライドのデザインを引っ張ってくる
// データはspreadsheetを参照する
function updateSlide() {
var new_presentation = SlidesApp.create('販売リスト_ver' + Utilities.formatDate( new Date(), 'Asia/Tokyo', 'yyMMdd'));
// データ元のspreadsheetの参照
var data_source = SpreadsheetApp.openById('{スプレッドシートID}');
// データ元のシート名の参照
var data_sheet = data_source.getSheetByName('{シート名}');
var values = data_sheet.getDataRange().getValues();
for (let i = 1; i < 4; i++) {
// データの準備
var name = values[i][0];
var description = values[i][1];
var price = values[i][2];
var image_url = values[i][3]
// slideの追加
var slide = new_presentation.appendSlide(default_slide);
// slideの更新
var shapes = slide.getShapes();
shapes[1].getText().replaceAllText('くるまのなまえ', name);
shapes[2].getText().replaceAllText('くるまのせつめい', description)
shapes[3].getText().replaceAllText('くるまのきんがく', price);
// 画像更新
var images = slide.getImages();
images[0].replace(image_url);
}
console.log(new_presentation.getUrl());
}