23
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

RubyXLでExcelに画像を挿入する

Posted at

参考URL等

version

rubyXL (3.3.22)

想定

あるxlsxファイルを雛形として、そのファイルに文字なり画像を挿入した結果を出力したいとする。

前提

  • 文字の挿入や装飾はRubyXLでメソッドが用意されている
  • 画像の挿入はRubyXLでメソッドが用意されてない(つらかった)
  • xlsxファイルはxmlや画像などを固めたzipファイル。拡張子を .zip にリネームして解凍すれば中身を覗ける。

RubyXL概要

  1. book = RubyXL::Parser.parse(source_path) でxlsxファイルを読み込む(※内部ではzip解凍し、中身のxmlも画像もRubyXLのオブジェクトにマッピングされてある)
  2. 文字を挿入したりなど。そのオブジェクトに変更を加える
  3. book.write(output_path) でxlsxファイルを指定したパスに書き出す(※内部ではRubyXLのオブジェクトを基にzip圧縮してxlsxファイルを生成している)

要は、1の段階でxlsxファイルはRubyXLのオブジェクトになっているので、書き出す前にそのオブジェクトをごにょごにょすれば良いことに気づく

xlsxが画像描画するのに必要なファイル

ファイル 概要
xl/media/xxx.yyy 画像ファイル。ブック全体で共有
xl/drawings/drawingX.xml シートごとに画像やオートシェイプのみを管理するxml
xl/drawings/_rels/drawingX.xml.rels リレーション定義(画像ファイルとdrawingX.xmlを紐付ける)
xl/worksheets/sheetY.xml シートの情報
xl/worksheets/_rels/sheetY.xml.rels リレーション定義(シートと諸々を紐付ける)
  • drawingX.xmlsheetY.xmlX, Y には数字が入る。drawing1.xml, sheet2.xmlなど。

どのシートがどの drawingX.xml を参照するかは xl/worksheets/_rels/sheetY.xml.rels で定義されている(どんだけ参照すんねん)。

調査(xlsxファイル)

画像の入っていないbefore.xlsxと、
それをMS Officeで開いて画像を挿入してできたafter.xlsx
のdiffを取ってみた(chromeアドオンのOOXML toolで)ところ、以下が変わっていた

  • xl/media/xxx.yyy
  • xl/drawings/drawingX.xml
  • xl/drawings/_rels/drawingX.xml.rels

上3つをRubyXLでいじれればいいことがわかった。
drawingX.xml 内の画像やオートシェイプはそれ一つ一つが <xdr:twoCellAnchor editAs="oneCell"> タグで括られているのが判った。

調査(RubyXL)

ひたすらpryでオブジェクトのクラスやメソッド調べながら泥のようにgithubのソースを追いかけてた。
気が向いたら詳細を書く

できたもの

<xdr:twoCellAnchor editAs="oneCell"> はzip解凍したdrawingX.xmlの中にあったものコピペしてそれをベースに自前で用意。中のタグ一つ一つが何を意味してるかはよく知らないし調べる気力も残ってない。

module Excel
  class Image
    attr_accessor :df, :sheet, :rid, :object, :begin_cell, :end_cell

    BASE_XML =<<-HTML.squish!
      <xdr:twoCellAnchor editAs="oneCell">
        <xdr:from>
          <xdr:col>%{begin_col}</xdr:col>
          <xdr:colOff>8000</xdr:colOff>
          <xdr:row>%{begin_row}</xdr:row>
          <xdr:rowOff>17000</xdr:rowOff>
        </xdr:from>
        <xdr:to>
          <xdr:col>%{end_col}</xdr:col>
          <xdr:colOff>0</xdr:colOff>
          <xdr:row>%{end_row}</xdr:row>
          <xdr:rowOff>0</xdr:rowOff>
        </xdr:to>
        <xdr:pic>
          <xdr:nvPicPr>
            <xdr:cNvPr id="4" name="図 3"/>
            <xdr:cNvPicPr>
              <a:picLocks noChangeAspect="1"/>
            </xdr:cNvPicPr>
          </xdr:nvPicPr>
          <xdr:blipFill>
            <a:blip xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships" r:embed="%{rid}">
              <a:extLst>
                <a:ext uri="{28A0092B-C50C-407E-A947-70E740481C1C}">
                  <a14:useLocalDpi xmlns:a14="http://schemas.microsoft.com/office/drawing/2010/main" val="0"/>
                </a:ext>
              </a:extLst>
            </a:blip>
            <a:stretch>
              <a:fillRect/>
            </a:stretch>
          </xdr:blipFill>
          <xdr:spPr>
            <a:xfrm>
              <a:off x="4533900" y="1600200"/>
              <a:ext cx="888687" cy="660400"/>
            </a:xfrm>
            <a:prstGeom prst="rect">
              <a:avLst/>
            </a:prstGeom>
          </xdr:spPr>
        </xdr:pic>
        <xdr:clientData/>
      </xdr:twoCellAnchor>
    HTML


    def initialize(sheet, file_path, begin_cell, end_cell)
      self.df         = get_drawing(sheet)
      self.sheet      = sheet
      self.begin_cell = begin_cell
      self.end_cell   = end_cell
      self.object = RubyXL::BinaryImageFile.new(
          Pathname("/xl/media/#{SecureRandom.hex}_#{file_path.basename}"),
          File.read(file_path)
        )
    end


    def self.add_to!(sheet, file_path, begin_cell, end_cell)
      image = new(sheet, file_path, begin_cell, end_cell)

      image.store_media
      image.append_rel
      image.add_to_xml
    end


    # xl/media/XXXX.xxx に画像ファイルを置く
    def store_media
      self.sheet.collect_related_objects
      self.sheet.attach_relationship(:dummy, object)
    end


    # xl/drawings/_rels/drawingX.xml.rels に自身を定義
    def append_rel
      self.df.relationship_container ||= RubyXL::OOXMLRelationshipsFile.new
      self.df.collect_related_objects
      self.df.relationship_container.send(:add_relationship, object)
      self.rid = df.relationship_container.relationships.last.id
    end


    # xl/drawings/drawingX.xml に自身を定義
    def add_to_xml
      xml = Nokogiri::XML.parse(df.data)
      xml.root.add_child(to_xml)
      self.df.data = xml.to_s
    end


    def begin_rowcol
      @begin_rowcol ||= ref_to_idx(begin_cell)
    end

    def end_rowcol
      @end_rowcol ||= ref_to_idx(end_cell)
    end

    def ref_to_idx(cell_addr)
      RubyXL::Reference.ref2ind(cell_addr)
    end

	# RubyXL::DrawingFile
    def get_drawing(sheet)
      rid = sheet.drawing
      if rid
        sheet.relationship_container.related_files[rid.r_id]
      end
    end


    def to_xml
      xml = BASE_XML.dup
      xml.sub!(/%{begin_row}/, begin_rowcol[0].to_s)
      xml.sub!(/%{begin_col}/, begin_rowcol[1].to_s)
      xml.sub!(/%{end_row}/, end_rowcol[0].to_s)
      xml.sub!(/%{end_col}/, end_rowcol[1].to_s)
      xml.sub!(/%{rid}/, rid)

      xml
    end
  end
end

というクラスを定義。

book  = RubyXL::Parser.parse('before.xlsx')
sheet = book[1]

Excel::Image.add_to!(sheet, Pathname('test.png'), 'A1', 'C5')

book.write('after.xlsx')

で、始点A1、終点C5にtest.png画像をbefore.xlsxに挿入したafter.xlsxファイルを書き出す。

できてないこと

xl/drawings/drawingX.xml が無い状態でそれを生成して xl/worksheets/sheetY.xml に紐付けるのは時間がなくて諦めた。

画像貼り付けるシートには既にオートシェイプか画像が既に存在する前提で動作する

23
12
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
23
12

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?