参考URL等
- RubyXL公式
- Excel ブック (_.xlsx) 形式概要 _ OpenBook
- Office Open XML - DrawingML - Object in a Spreadsheet Document - Two Cell Anchoring
- OOXML tool(chromeアドオン。xlsxをzip解凍しなくても中身が覗けたり、diffが取れる←すげえ!)
version
rubyXL (3.3.22)
想定
あるxlsxファイルを雛形として、そのファイルに文字なり画像を挿入した結果を出力したいとする。
前提
- 文字の挿入や装飾はRubyXLでメソッドが用意されている
- 画像の挿入はRubyXLでメソッドが用意されてない(つらかった)
- xlsxファイルはxmlや画像などを固めたzipファイル。拡張子を
.zip
にリネームして解凍すれば中身を覗ける。
RubyXL概要
-
book = RubyXL::Parser.parse(source_path)
でxlsxファイルを読み込む(※内部ではzip解凍し、中身のxmlも画像もRubyXLのオブジェクトにマッピングされてある) - 文字を挿入したりなど。そのオブジェクトに変更を加える
-
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.xml
やsheetY.xml
のX
,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 に紐付けるのは時間がなくて諦めた。
画像貼り付けるシートには既にオートシェイプか画像が既に存在する前提で動作する