脳筋実装でPowerPointやGoogle Slideで資料を自動生成する

![Untitled.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/29618/c806afcd-549d-6bc9-2a11-06f9e7827135.png)

## 前提

- PowerPointやGoogle Slideは自動生成が可能
- 特定のデザインにデータを流し込みたい人向け

## Presentationの生成自動化についての前提

- PowerPoint
    - OpenXML(Office Open XML, OOXML)形式
        - [https://docs.microsoft.com/ja-jp/office/open-xml/structure-of-a-presentationml-document](https://docs.microsoft.com/ja-jp/office/open-xml/structure-of-a-presentationml-document)
    - `.pptx` をzipにリネームして解答すると中身が見られる
    - つまり操作としてはzip解凍して中のxmlを修正したりすればなんとかなる
    - パワポの中のグラフを弄るのは実はパワポの中にExcelが内包されているので二重に面倒
        - ただ、パワポのほうでキャッシュされている数値を弄れば見た目上は変わる
    - できること
        - 画像操作
        - グラフ操作
        - テキスト操作
- Google Slide
    - GASなどでいじれる
        - [https://developers.google.com/apps-script/reference/slides](https://developers.google.com/apps-script/reference/slides)
    - できること
        - 画像操作
        - テキスト操作

## 今回何をするか

- 例として、車の販売店がパワポで販売している車の一覧を作るという状況を考える
- 毎回スライドを更新するのはつらいので、データベースやSpreadsheetからデータを取ってきて自動更新したい
    - PowerPointの自動生成例では説明の簡易化のためにデータベースから取得する部分を割愛
    - Google Slideの自動生成例ではSpreadsheetからデータを取得する

## PowerPointの自動生成を行う

### 準備

- データベースを容易(もしくはcsvなど)
- テンプレートを容易

### 操作方法

基本的な操作としては以下の流れで実装する.

1. テンプレートpptxを解凍
2. データリソース分だけ以下の処理をする
    1. slide1.xmlをコピーして連番付けして中のテキスト部分置換して保存
    2. 画像を連番付けして保存
3. ファイル群を保存

### パーツ群

覚える必要がないが、この記事を見て拡張性のあるスクリプト書きたいと思った人向けに各ファイルの解説をしておく.

- _rels
    - [Content_Types].xml
        - ファイルごとの属性定義
        - スライドについてだけ追記する
            - `<Override PartName="/ppt/slides/slide1.xml" ContentType="application/vnd.openxmlformats-officedocument.presentationml.slide+xml" />`
- docProps
    - app.xml
        - 基本情報
        - いじらない
    - core.xml
        - タイトル, 作成者名, 編集者名, 作成日時, 最新更新日時などを編集したいときは弄る
    - thumbnail.jpeg
        - サムネイル変えたいときは弄る
- ppt
    - _rels
        - presentation.xml.rels
            - 各ファイルへのrelationshipが貼られている
                - slideN
                - presProps
                - viewProps
                - themeN
                - tableStyles
            - slideNを増やしたいので、pressProps以降はrID (relationship ID)をずらす
    - media
        - 画像が入っている
    - presentation.xml
        - slideMasterとslideが構造化されている
        - slideを追加する
    - presProps.xml
        - プレゼンテーションプロパティーパーツ
        - 弄らない
    - slideLayouts
        - スライドのレイアウト
        - 弄らない
    - slideMaster
        - スライドマスター
        - 弄らない
    - slides
        - _rels
            - slide1.xml.rels
                - 画像やslideLayoutへの参照
                    - チャートへの参照などもここに
                - スライド毎に作る
        - slide1.xml
            - 画像やテキスト実体が内包されている
            - ここをメインで弄る
    - tableStyles.xml
        - テーブルスタイル
        - 弄らない
    - theme
        - theme1.xml
            - テーマ
            - 弄らない
    - viewProps.xml
        - ビュープロパティーパーツ
        - 弄らない

### 実装例

```ruby
# 簡単のために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とあまり変わらないので割愛

### 実装例

```jsx
# デフォルトスライド
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());
}
```