600×400 ピクセルのロゴを生成した請求書のヘッダーに入れると、96 DPI の開発用モニターではちょうどよく見えます。ところが 1 週間後、高 DPI のノート PC を使う顧客から、切手ほどの大きさで印刷されると連絡が来ます。変わったのはピクセル数ではありません。変わったのは、ピクセル数が物理サイズを意味するという前提です。OOXML ではそうではありません。スプレッドシートの画像は寸法を EMU で保持し、EMU で考えるか、そこにきれいに対応する実世界の単位で考えない限り、レイアウトはレンダリング側がたまたま前提にする DPI に左右されます
HotXLS は、Excel や COM に依存せずに XLS と XLSX を読み書きできる、Delphi と C++Builder 向けのネイティブ VCL スプレッドシートコンポーネントです。v2.91.0 では、XLSX の画像オブジェクトが単位換算を手作業で行わせなくなりました。生の EMU に加えて、幅と高さをセンチメートル、インチ、ポイントで取得でき、さらに Scale メソッドは、割合でサイズ変更でき、必要に応じてアスペクト比を固定できます。この記事では、EMU が実際には何なのか、なぜ DrawingML がそれを採用したのか、そして信頼できないピクセル数ではなく物理サイズで画像を配置するために新しいジオメトリ面をどう使うかを解説します
EMU とは何か、そして DrawingML がそれを使う理由
EMU は English Metric Unit の略で、Office Open XML ファミリー全体で共有される描画レイヤー DrawingML の基準長単位です(ECMA-376, Part 1, §20)。EMU は、ちょうど 1 インチあたり 914400 EMU と 1 センチメートルあたり 360000 EMUになるよう定義されています。EMU という単位が存在する理由は、この 2 つの定数だけです。914400 は 2, 3, 4, 5, 6, 8, 9, 10, 12 などで割り切れます。素因数分解すると 26 × 32 × 52 × 127 です。1 インチ = 2.54 cm ちょうどなので、360000 と 914400 のきれいな約数の両方で割り切れる単位を選べば、フォーマットはインチ、センチメートル、ポイントを 整数として、単位境界で丸めることなく表現できます。浮動小数点の「1.27 cm」では誤差が生じますが、EMU なら 457200 を保持し、完全に正確です
ここで重要なもう 1 つの単位はポイントです。タイポグラフィのポイントは 1/72 インチなので、1 ポイントあたり 12700 EMU(914400 / 72)です。Excel 自身は内部で行の高さ、フォントサイズ、余白をポイント基準で扱うため、画像のジオメトリをポイントで公開しておくと、印刷用の定規ではなくテキストメトリクスに合わせたいときに役立ちます。HotXLS はこの 4 つの関係をすべてライブラリ内の単位定数として定義しています:
const
XlsxEmuPerInch = 914400; // 1 inch
XlsxEmuPerCm = 360000; // 1 centimetre
XlsxEmuPerPoint = 12700; // 1 point (1/72 inch)
XlsxEmuPerPixel = 9525; // 1 pixel at 96 DPI (914400 / 96)
最後の行が切手サイズ化バグの核心です。ピクセルに物理サイズが生まれるのは DPI を固定した後だけで、9525 EMU はピクセル 1 個の大きさに相当します(96 DPI の場合)。Excel の既定レンダリング DPI は 96 なので、100 ピクセルの画像は既定環境では 100 × 9525 = 952500 EMU ≈ 2.54 cm になります。しかし、ファイルのどこにも受け手が 96 を使う保証はありません。実寸単位で作成すればその曖昧さは消えます。画面が 96 DPI でも 220 DPI でも、4 cm は常に 4 cm です
TXLSXImage のジオメトリ面
HotXLS の埋め込み画像は TXLSXImageです。正規の保存先は 2 つの整数フィールド、WidthEMU と HeightEMUに配置され、1 始まりの Row と Col(画像がぶら下がる左上セル)を基準にします。実単位のプロパティはこれらの EMU フィールドをもとにした計算結果であり、別個の状態ではありません。読み出しは WidthCMで EMU を 360000 で割り、書き込みでは乗算して四捨五入して戻します。したがって、設定する各寸法は同じ基礎 EMU 値の別表記にすぎません:
WidthInch/HeightInch- EMU ÷ 914400WidthCM/HeightCM- EMU ÷ 360000WidthPt/HeightPt- EMU ÷ 12700WidthEMU/HeightEMU- 真の値の基準となる整数
画像は AddImage(ARow, ACol, AData, AFormat)で追加します。生のエンコード済みバイト列と TXLSXImageFormat (xlsxImagePng, xlsxImageJpeg, xlsxImageGif、または xlsxImageBmp); 戻り値は 0始まりのワークシートの Imagesコレクション内のインデックスです。さらに AddImageFromFile(ARow, ACol, AFileName)があります。こちらはファイル拡張子から形式を推論します。インデックスの基準に注意してください。AddImageは 0 始まりで返し、Images[]も 0 始まりです。これは 1 始まりの Cells[Row, Col]グリッドとの意図的な対比なので、両者が一致すると決めつけないでください
var
Sheet: TXLSXWorksheet;
Img: TXLSXImage;
Idx: Integer;
begin
Sheet := Workbook.Sheets.Add('Images');
// Anchor a PNG at row 3, column 2; AddImage returns a 0-based index.
Idx := Sheet.AddImage(3, 2, LogoBytes, xlsxImagePng);
Img := Sheet.Images[Idx];
Img.WidthCM := 4.0; // 4 cm wide -> 1440000 EMU
Img.HeightCM := 3.0; // 3 cm tall -> 1080000 EMU
// Same geometry, read back in other units.
// Img.WidthPt is now 113.39 pt, Img.WidthInch is 1.5748 in.
end;
新規作成した画像の既定値は 100×100 ピクセル、つまり 952500 EMU の正方形で、96 DPI ではおよそ 2.54 cm の枠になります。この既定値は、サイズ指定を忘れても画像が見えるようにするためのものですが、実際のレイアウトではピクセル換算の既定値に頼らず、物理サイズを明示してください
スケーリングとアスペクト比フラグ
現在の寸法に対する相対サイズ変更をしたいとき、たとえば取り込み時のサイズの 60% までグラフ画像を縮小したいときは、Scale:
procedure Scale(APercent: Double; AKeepAspect: Boolean = True);
APercentは割合で、100 なら変更なし、150 なら 1.5 倍、50 なら半分です。AKeepAspect の既定値が Trueなら、幅と高さの両方が同じ倍率で掛けられるため比率は保たれ、4×3 cm の画像は Scale(150)の後に 6×4.5 cm になります。Falseを渡すと、幅だけが拡大され、高さはそのままです。この非対称は意図的なものです。1 方向だけを独立に伸ばしたいなら、使うべきなのは明示的な WidthCM/HeightCM セッターです。そして Scale のアスペクト比を無視する分岐は、幅だけを調整する狭い用途のためにあります。Scale(150, False) を「両方を自由に伸ばす」と受け取って驚かないよう、本当に 2 つの独立した寸法を指定したいときはセッターを使ってください
Img.WidthCM := 4.0;
Img.HeightCM := 3.0;
Img.Scale(150); // aspect locked: now 6.0 x 4.5 cm
Img.Scale(100); // no-op, returns immediately
Img.Scale(50, False); // width only: 3.0 cm wide, height unchanged at 4.5 cm
覚えておきたい小さな挙動があります: Scale(100) は早期リターンしてどちらのフィールドにも触れません。そのため、割合が 100 の可能性があるループでも無条件に呼び出せます。さらにジオメトリは 整数 EMU で保存されるため、各セッターは四捨五入します。そのため、少数のセンチメートルを往復すると、EMU のごく一部だけずれることがあります。見た目に影響するほどではありませんが、テストで完全一致を検証するなら知っておく価値があります。ピクセル単位で厳密に制御したいなら、WidthEMU と HeightEMUを直接設定し、単位変換は完全に省いてください
ジオメトリを読み戻す
画像コレクションは問い合わせ可能で、既存のワークブックを読み込んだときに、今追加したものではなく、すでにそこにある内容を確認したり調整したりする際に重要です。Images.Count はシート上のすべての画像を列挙し、Images[i]は 0 始まりでインデックスを付け、FindAt(ARow, ACol)は特定のセルに固定された画像を返し、nil、なければ IndexOfCellでオブジェクトではなくインデックスを取得でき、DeleteAt / DeleteInRangeで削除できます
var
i: Integer;
Img: TXLSXImage;
begin
for i := 0 to Sheet.Images.Count - 1 do
begin
Img := Sheet.Images[i];
Writeln(Format('[%d] R%dC%d %.2f x %.2f cm (%d x %d EMU)',
[i, Img.Row, Img.Col, Img.WidthCM, Img.HeightCM,
Img.WidthEMU, Img.HeightEMU]));
end;
Img := Sheet.Images.FindAt(3, 2); // nil-check before use
if Img <> nil then
Img.Scale(80);
end;
実単位のプロパティはライブビューなので、ほかのツールから EMU サイズで取り込んだ画像は、変換作業を挟まずにすぐセンチメートルで寸法を返します。これは描画モデル全体とも自然にかみ合います。ラスター画像だけでなく、グラフや図形も配置するなら、Delphi における HotXLS のグラフ、画像、Excel 図形の解説で、それらのオブジェクトが共有するアンカーモデルを扱っています
メートル法のページ設定余白
同じ EMU と実単位の緊張関係は、1 つ外側のページ設定にも現れます。OOXML と Excel は印刷余白を インチで保存するため、レポートテンプレートが米国以外の多くの地域で一般的なミリメートル指定だと扱いにくくなります。v2.91.0 では、インチ余白の上にセンチメートルのラッパーが追加されました: MarginLeftCM, MarginRightCM, MarginTopCM, MarginBottomCM, MarginHeaderCM、および MarginFooterCMです。いずれも対応するインチプロパティの薄いラッパーにすぎず、正確な 1 インチ = 2.54 cm の比率で変換します
Sheet.MarginLeftCM := 2.0; // 2 cm == 0.7874 inch
Sheet.MarginRightCM := 2.0;
Sheet.MarginTopCM := 2.5;
Sheet.MarginBottomCM := 2.5;
Sheet.MarginHeaderCM := 1.0;
Sheet.MarginFooterCM := 1.0;
インチのプロパティ(MarginLeftなど)は引き続き正規の保存先です。したがって両者を混ぜて使えます。たとえば上余白をセンチメートルで設定してインチで読み戻したり、その逆をしたりしても、ディスクに書き込まれるファイルはどちらでも同一です。変換は単純に 2.54 を掛けるだけで、大まかなグリッドに丸めることはありません。そのため 2 cm は完全な倍精度のまま 2 cm です。これは画像ジオメトリと同じく、フォーマットの内部はインチ基準でも、ライブラリが仕様書に書かれた単位で記述することを可能にするという考え方です。周囲のレポートの配置、タイトル、メタデータブロック、合計については HotXLS における結合セルとレポートテンプレートのレイアウトを参照してください。そこでは、これらの余白を結合範囲や印刷範囲と組み合わせて使います
ジオメトリで保証されることと保証されないこと
ジオメトリプロパティが制御するのは 宣言された画像ファイル内でのサイズです。これは、準拠した利用側がそのサイズで描画するという宣言値に当たります。画像バイト列の再サンプリングは行いません。50×50 ピクセルの PNG を 8 cm にすると拡大され、Excel と同じように粗く見えます。サイズ指定は画像処理ではなくレイアウト操作なので、意図する物理サイズに対して十分な元解像度を用意してください。ライブラリは形式の再エンコードも行いません。AddImageに渡したバイト列は、そのまま保存され、TXLSXImageFormatとして宣言した形式で書き出されます。JPEG バイト列を渡して xlsxImagePngとしてタグ付けすると、Excel で開けないファイルになります。したがって、可能なときは AddImageFromFileに拡張子から形式を推論させてください
この下にある 1 つの考え方を理解してしまえば、どれも特別ではありません。OOXML では、物理サイズこそが本質であり、ピクセルは DPI に依存する派生的な影にすぎません。画像や余白はセンチメートル、インチ、ポイントで記述し、HotXLS に正確な EMU へ変換させれば、請求書やレポートは、開くマシンが違っても同じ大きさで印刷されます
ここで説明した画像ジオメトリ、スケーリング、メートル法余白の API は、HotXLS Delphi スプレッドシートコンポーネントに含まれており、Delphi と C++Builder から Excel のインストールなしで XLS と XLSX を読み書きできます