今回の目的
指定したビットマップ画像(PNGなど)をページ内の指定した場所と大きさに貼ったPDFファイルを作成する。
サンプルファイルの作成
まず、画像を貼ったPDFファイルの構造を調査するため、適当な画像を貼ったPDFファイルを作成した。
最初に、PDFファイルに貼る画像をペイントで作成した。
今回は、のちの解析に便利そうなように、色が
#524742 #726762 #313233
#343536 #373839 #78797A
となっている3×2の画像を用意した。これらの色は、ASCIIとして解釈すると
RGB rgb 123
456 789 xyz
となる。
次に、この画像を貼ったPDFファイルを LibreOffice Draw を用いて作成した。
ページサイズは、名刺によくある91×55 [mm] とした。
画像を貼り、位置は上から1cm、左から2cm、サイズは幅3cm、高さ2cmとした。
「ファイル → 次の形式でエクスポート → PDFとしてエクスポート」で、PDFファイルを書き出した。
画像は「可逆圧縮」とし、「アーカイブ」を有効にした。
ここでは PDF/A を指定してエクスポートしたが、今回自分で作る際は PDF/A への準拠は目指さない。
サンプルファイルの解析
PDF by Hand - Kobu.Com
を参考に、作成したPDFファイルの構造を確認する。
最後の部分は以下のようになっており、18個のオブジェクトからなることがわかる。
xref
0 19
0000000000 65535 f
0000000588 00000 n
0000000019 00000 n
0000000247 00000 n
0000014543 00000 n
0000000267 00000 n
0000000450 00000 n
0000014743 00000 n
0000000469 00000 n
0000000491 00000 n
0000000728 00000 n
0000009290 00000 n
0000009312 00000 n
0000009434 00000 n
0000014628 00000 n
0000014458 00000 n
0000014701 00000 n
0000014867 00000 n
0000015073 00000 n
trailer
<</Size 19/Root 17 0 R
/Info 18 0 R
/ID [ <923023A20E1D9323F6EB9F94CEEDFA92>
<923023A20E1D9323F6EB9F94CEEDFA92> ]
/DocChecksum /06BCED98FA13BEA89D67E7AB46DF8870
>>
startxref
15240
%%EOF
各オブジェクトの参照関係は、以下のようになっていた。
書類情報 (18)
18 0 obj
<</Creator<FEFF0044007200610077>
/Producer<FEFF004C0069006200720065004F0066006600690063006500200037002E0035>
/CreationDate(D:20251017015302+09'00')>>
endobj
PDFファイルについての情報が記録されている。
Creatorをデコードすると「Draw」
Producerをデコードすると「LibreOffice 7.5」
となる。
カタログオブジェクト (17)
17 0 obj
<</Type/Catalog/Pages 7 0 R
/PageMode/UseOutlines
/OpenAction[1 0 R /XYZ null null 0]
/StructTreeRoot 14 0 R
/Lang(ja-JP)
/MarkInfo<</Marked true>>
/OutputIntents[12 0 R]/Metadata 13 0 R>>
endobj
/Pages でページ一覧オブジェクトを指定している。
ここでは他にも様々な情報が出力されているが、重要ではないだろう。
メタデータ (13)
ファイルのメタデータが、XML形式で格納されている。
自分が出力する際には省略していいだろう。
13 0 obj
<</Type/Metadata/Subtype/XML/Length 4946>>
stream
<?xpacket begin="" id="W5M0MpCehiHzreSzNTczkc9d"?>
<x:xmpmeta xmlns:x="adobe:ns:meta/">
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
<rdf:Description rdf:about="" xmlns:pdfaid="http://www.aiim.org/pdfa/ns/id/">
<pdfaid:part>3</pdfaid:part>
<pdfaid:conformance>B</pdfaid:conformance>
</rdf:Description>
<rdf:Description rdf:about="" xmlns:pdf="http://ns.adobe.com/pdf/1.3/">
<pdf:Producer>LibreOffice 7.5</pdf:Producer>
</rdf:Description>
<rdf:Description rdf:about="" xmlns:xmp="http://ns.adobe.com/xap/1.0/">
<xmp:CreatorTool>Draw</xmp:CreatorTool>
<xmp:CreateDate>2025-10-17T01:53:02+09:00</xmp:CreateDate>
</rdf:Description>
</rdf:RDF>
</x:xmpmeta>
(省略:空白だけの行が並んでいる)
<?xpacket end="w"?>
endstream
endobj
OutputIntent (12, 10, 11)
出力の目的の指定?
オブジェクト10番はバイナリデータで、11番はそのサイズである。
自分が出力する際には省略していいだろう。
12 0 obj
<</Type/OutputIntent/S/GTS_PDFA1/OutputConditionIdentifier(sRGB IEC61966-2.1)/DestOutputProfile 10 0 R>>
endobj
StructTreeRoot (14, 16, 4, 15)
木構造?
自分が出力する際には省略していいだろう。
15 0 obj
<</O/Layout
/Placement/Block
/BBox[85.039 42.491 170.107 99.212]
>>
endobj
4 0 obj
<</Type/StructElem
/S/Figure
/P 14 0 R
/Pg 1 0 R
/A 15 0 R
/K[0 ]
>>
endobj
14 0 obj
<</Type/StructTreeRoot
/ParentTree 16 0 R
/K[4 0 R ]
>>
endobj
16 0 obj
<</Nums[
0 [ 4 0 R ]
]>>
endobj
ページ一覧オブジェクト (7)
ページの列の情報が格納されている。
7 0 obj
<</Type/Pages
/Resources 9 0 R
/MediaBox[ 0 0 257.952755905512 155.905511811024 ]
/Kids[ 1 0 R ]
/Count 1>>
endobj
ページサイズ (MediaBox)、ページ数 (Count)、ページリスト (Kids) の指定がある。
また、PDF by Hand ではみられなかったリソースの指定 (Resources) もある。
リソースオブジェクト (9, 8)
描画に使う命令リスト (ProcSet) と、画像データ (XObject) が指定されている。
今回は、フォント (Font) は空である。
8 0 obj
<<
>>
endobj
9 0 obj
<</Font 8 0 R
/XObject<</Im5 5 0 R>>
/ProcSet[/PDF/Text/ImageC/ImageI/ImageB]
>>
endobj
画像データ (5, 6)
Zlib Deflate 圧縮された画像データが格納されているようである。
(FlateDecode が圧縮形式の指定だろう)
圧縮しながらファイルを書き出すためか、(圧縮後の) 長さがデータの後に置かれている。
5 0 obj
<</Type/XObject/Subtype/Image/Width 3/Height 2/BitsPerComponent 8/Length 6 0 R
/Filter/FlateDecode/ColorSpace/DeviceRGB
>>
stream
(バイナリデータ)
endstream
endobj
6 0 obj
26
endobj
圧縮されたバイナリデータを展開すると、以下のようになった。
RGBrgb123456789xyz
画像データはRGBの順で、上から下、左から右の順で並んでいるようである。
ページオブジェクト (1)
ページの情報が格納されている。
リソース (Resources) とページサイズ (MediaBox) が再設定されている。
StructParents は構造 (アウトライン) に関するものだろうか?
1 0 obj
<</Type/Page/Parent 7 0 R/Resources 9 0 R/MediaBox[0 0 257.952755905512 155.905511811024]/StructParents 0
/Contents 2 0 R>>
endobj
コンテンツストリーム (2, 3)
ページの描画内容が格納されている。
画像データと同様に、(圧縮後の) サイズが後付けになっている。
2 0 obj
<</Length 3 0 R/Filter/FlateDecode>>
stream
(バイナリデータ)
endstream
endobj
3 0 obj
157
endobj
圧縮データを展開すると、以下のようになっていた。
0.1 w
/Artifact BMC
q 0 0.028 257.924 155.877 re
W* n
1 1 1 rg
0 155.905 m
257.924 155.905 l 257.924 0.028 l 0 0.028 l 0 155.905 l
h
f*
EMC
/Figure<</MCID 0>>BDC
q 85.039 0 0 56.693 85.039 42.548 cm
/Im5 Do Q
EMC
Q
PDF by Hand の内容と見比べると、以下の意味だと解釈できる。(一部改行を追加した)
0.1 w % 線の太さを 0.1 に設定する
/Artifact BMC % わからない
q % 描画設定をpushする
0 0.028 257.924 155.877 re % わからない
W* n % わからない
1 1 1 rg % 描画色を白に設定する
0 155.905 m % ページ左上の端に移動する
257.924 155.905 l % ページ(左上から)右上の端まで線を引く
257.924 0.028 l % ページ(右上から)右下の端まで線を引く
0 0.028 l % ページ(右下から)左下の端まで線を引く
0 155.905 l % ページ(左下から)左上の端まで線を引く
h % わからない
f* % わからない
EMC % わからない
/Figure<</MCID 0>>BDC % わからない
q % 描画設定をpushする
85.039 0 0 56.693 85.039 42.548 cm % 描画を行う座標系を設定する
/Im5 Do % わからない
Q % 描画設定をpopする
EMC % わからない
Q % 描画設定をpopする
わからない所も多いが、大きく分けて
- 四角形を描画する
- 画像を描画する
という2ステップの描画をしていそうである。
/Im5 は、リソースオブジェクトの /XObject 内で定義されていた画像データのIDである。
よって、画像を描画する位置を座標系として設定し、(画像ID) Do というおまじないを唱えることで、画像が描画できそうだ、と推測できる。
手動でPDFファイルを作ってみる
これまでの解析結果をもとに、手動で画像を貼ったPDFファイルを作ってみた。
画像データ
簡単に記述するため、ASCIIで表現できる範囲の色のみを用いる。
また、圧縮も行わない。
1 0 obj
<</Type/XObject/Subtype/Image/Width 3/Height 3/BitsPerComponent 8/Length 27
/ColorSpace/DeviceRGB
>>
stream
~!!~!!~!!!!~~~~!~!!!~!~!!~!
endstream
endobj
リソースデータ
フォントは空なので、参照にせず直接記述する。
今回は、画像データに /Image という名前をつける。
/ProcSet は、
PDFってどういう構造よ?その(3) | テン*シー*シー
を参照すると、/ImageC はカラー画像を扱うことを意味するようなので、これを指定する。
2 0 obj
<<
/Font <<>>
/XObject<</Image 1 0 R>>
/ProcSet[/PDF/ImageC]
>>
endobj
コンテンツストリーム
状態保存・画像描画 (描画位置の設定を含む)・状態復元の命令のみを用いる。
描画位置の指定は、PDF by Hand より、
幅 0 0 高さ 左端からの左下の距離 下端からの左下の距離 cm
である。これらの長さには、指定したい長さ [mm] に 72 を掛けて 25.4 で割った値を指定する。
3 0 obj
<</Length 92>>
stream
q
85.03937007874016 0 0 85.03937007874016 86.45669291338582 35.43307086614173 cm
/Image Do
Q
endstream
endobj
ページオブジェクト
ページとコンテンツストリームを紐づける。
今回はページ一覧オブジェクトをページオブジェクトの後に配置したが、/Parent を指定することを考えると前に配置したほうがページの数を気にせずにすみ、よさそうだ。
4 0 obj
<</Type/Page/Parent 5 0 R/Contents 3 0 R>>
endobj
ページ一覧オブジェクト
ページの列を定義する。
また、各ページで共通に用いるリソースやページサイズも、ここで指定できるようだ。
ページサイズの指定は [0 0 幅 高さ] であり、数値の求め方は画像の位置と同様だろう。
5 0 obj
<<
/Type/Pages
/Resources 2 0 R
/MediaBox[0 0 257.952755905512 155.905511811024]
/Count 1
/Kids[4 0 R]
>>
endobj
カタログオブジェクト
今回は、ページ一覧オブジェクトのみを指定する。
6 0 obj
<</Type/Catalog/Pages 5 0 R>>
endobj
書類情報
今回は、タイトルのみを指定する。
7 0 obj
<<
/Title (image test)
>>
endobj
まとめ
これらのオブジェクト群を並べ、ヘッダや相互参照などを追加することで、PDFデータが完成する。
改行コードは LF である。
%PDF-1.2
1 0 obj
<</Type/XObject/Subtype/Image/Width 3/Height 3/BitsPerComponent 8/Length 27
/ColorSpace/DeviceRGB
>>
stream
~!!~!!~!!!!~~~~!~!!!~!~!!~!
endstream
endobj
2 0 obj
<<
/Font <<>>
/XObject<</Image 1 0 R>>
/ProcSet[/PDF/ImageC]
>>
endobj
3 0 obj
<</Length 92>>
stream
q
85.03937007874016 0 0 85.03937007874016 86.45669291338582 35.43307086614173 cm
/Image Do
Q
endstream
endobj
4 0 obj
<</Type/Page/Parent 5 0 R/Contents 3 0 R>>
endobj
5 0 obj
<<
/Type/Pages
/Resources 2 0 R
/MediaBox[0 0 257.952755905512 155.905511811024]
/Count 1
/Kids[4 0 R]
>>
endobj
6 0 obj
<</Type/Catalog/Pages 5 0 R>>
endobj
7 0 obj
<<
/Title (image test)
>>
endobj
xref
0 8
0000000000 65535 f
0000000010 00000 n
0000000172 00000 n
0000000252 00000 n
0000000393 00000 n
0000000452 00000 n
0000000574 00000 n
0000000620 00000 n
trailer
<<
/Root 6 0 R
/Info 7 0 R
/Size 8
>>
startxref
662
%%EOF
このPDFファイルを Firefox で開くと、見事期待通りの図形がページ中央に表示された。
他の画像形式を見てみる
これまでの実験では、アルファチャンネルなしのビットマップ画像を扱ってきた。
ここでは、他の形式の画像を LibreOffice Draw で貼ったPDFを作成し、どのような形で格納されるかを見てみる。
JPEG画像
適当な写真をページに貼り、画像を「JPEG圧縮」に設定し、さらに「画像の解像度を下げる」にチェックを入れてPDFへのエクスポートを行った。
その結果、以下の形式で画像データが格納された。
-
/Filterの後に/DCTDecodeを指定する - ストリームの内容は、JPEGファイルのデータをそのまま格納する
5 0 obj
<</Type/XObject/Subtype/Image/Width 275 /Height 413 /BitsPerComponent 8 /ColorSpace/DeviceRGB/Filter/DCTDecode/Length 25421>>
stream
(JPEGデータ)
endstream
endobj
透明度付きの画像
色が
#52474241 #72676261
(RGBA rgba) となっているPNG画像をページに貼り、PDFにエクスポートしてみた。
その結果、以下の形式で画像データが格納された。
- メインの画像データはこれまで見たのと同じRGBの並びで、
/SMaskでアルファチャンネルを表す画像データのオブジェクトを指定する - アルファチャンネルを表す画像データは
-
/ColorSpaceに/DeviceGrayを指定する - おまじない
/Decode [ 1 0 ]を指定する - 格納するデータは、255からアルファチャンネルの値 (不透明度) を引いた値とする (すなわち、透明度を格納する)
-
5 0 obj
<</Type/XObject/Subtype/Image/Width 2/Height 1/BitsPerComponent 8/Length 6 0 R
/Filter/FlateDecode/ColorSpace/DeviceRGB
/SMask 7 0 R
>>
stream
(バイナリデータ)
endstream
endobj
6 0 obj
14
endobj
7 0 obj
<</Type/XObject/Subtype/Image/Width 2/Height 1/BitsPerComponent 8/Length 8 0 R
/Filter/FlateDecode/ColorSpace/DeviceGray
/Decode [ 1 0 ]
>>
stream
(バイナリデータ)
endstream
endobj
8 0 obj
10
endobj
RGBrgb
00000000 be 9e |..|
さらに、この形式を用いる場合、リソースデータの /ProcSet に、グレースケール画像を扱うことを表す /ImageB を追加しておくべきである。
(実際には扱わない場合でも、追加しておいてよい)
PNGファイルでは、乗算済みアルファ値は用いず、アルファ値を乗算する前のRGB値を格納する。
(Portable Network Graphics (PNG) Specification (Second Edition) 6.2 Alpha representation)
PDFに埋め込んだ際もPNGファイルに記録された値がそのまま用いられたので、PDFでも乗算済みアルファ値は用いないと考えられる。
ここで用いる /SMask は、PDF 1.4 以降の機能である。
/Decode [ a b ] は、「サンプルの最小値を a に、最大値を b にマッピングして解釈する」という意味である。
このため、/Decode [ 1 0 ] を指定すると、透明度情報の解釈が反転することになる。
プログラムでPDFファイルを作ってみる
これまでの調査結果を踏まえ、指定した画像を貼ったPDFファイルを作るプログラムを作ってみた。
今回は、指定した画像をA4縦の用紙の中央に、10ピクセル=2.54mmとなるような大きさで配置する。
このスケールで印刷すると、PasS で作成した設計データを原寸大で印刷でき、組立時に便利である。
アルファチャンネル付きの画像にも対応する。
/Decode は用いず、画像のアルファチャンネルをそのまま格納する。
JPEGの埋め込みについては、データをそのまま埋め込むだけであり、面白さ・難しさが少ないため、省略する。
画像の読み込みには、Pillow を用いた。
import sys
import itertools
import zlib
from PIL import Image
page_width_mm = 210
page_height_mm = 297
img_mm_per_pixel = 0.254
def mm_to_pt(length_mm):
return length_mm / 25.4 * 72
if len(sys.argv) < 3:
sys.stderr.write("Usage: img2pdf.py input-file output-file")
sys.exit(1)
pdf_objects = [None]
img = Image.open(sys.argv[1])
if img.mode == "RGBA":
img_data = img.getdata()
img_data_rgb = bytes(itertools.chain.from_iterable([x[0:3] for x in img_data]))
img_data_mask = bytes([x[3] for x in img_data])
img_data_rgb_deflated = zlib.compress(img_data_rgb)
img_data_mask_deflated = zlib.compress(img_data_mask)
mask_obj_no = len(pdf_objects)
mask_obj = (
("<</Type/XObject/Subtype/Image/Width %d/Height %d" % img.size) +
("/BitsPerComponent 8/Length %d" % len(img_data_mask_deflated)) +
"/Filter/FlateDecode/ColorSpace/DeviceGray>>\nstream\n"
).encode() + img_data_mask_deflated + b"\nendstream"
pdf_objects.append(mask_obj)
img_obj_no = len(pdf_objects)
img_obj = (
("<</Type/XObject/Subtype/Image/Width %d/Height %d" % img.size) +
("/BitsPerComponent 8/Length %d" % len(img_data_rgb_deflated)) +
("/Filter/FlateDecode/ColorSpace/DeviceRGB/SMask %d 0 R>>\nstream\n" % mask_obj_no)
).encode() + img_data_rgb_deflated + b"\nendstream"
pdf_objects.append(img_obj)
else:
img = img.convert("RGB")
img_data = img.getdata()
img_data_rgb = bytes(itertools.chain.from_iterable(img_data))
img_data_rgb_deflated = zlib.compress(img_data_rgb)
img_obj_no = len(pdf_objects)
img_obj = (
("<</Type/XObject/Subtype/Image/Width %d/Height %d" % img.size) +
("/BitsPerComponent 8/Length %d" % len(img_data_rgb_deflated)) +
"/Filter/FlateDecode/ColorSpace/DeviceRGB>>\nstream\n"
).encode() + img_data_rgb_deflated + b"\nendstream"
pdf_objects.append(img_obj)
res_obj_no = len(pdf_objects)
res_obj = (
"<</Font<<>>/XObject<</Image %d 0 R>>/ProcSet[/PDF/ImageC/ImageB]>>" % img_obj_no
).encode()
pdf_objects.append(res_obj)
contents_obj_no = len(pdf_objects)
img_width_mm = img.size[0] * img_mm_per_pixel
img_height_mm = img.size[1] * img_mm_per_pixel
contents_data = (
"q %.15f 0 0 %.15f %.15f %.15f cm /Image Do Q" % (
mm_to_pt(img_width_mm),
mm_to_pt(img_height_mm),
mm_to_pt((page_width_mm - img_width_mm) / 2),
mm_to_pt((page_height_mm - img_height_mm) / 2)
)
).encode()
contents_obj = (
"<</Length %d>>\nstream\n" % len(contents_data)
).encode() + contents_data + b"\nendstream"
pdf_objects.append(contents_obj)
pagelist_obj_no = len(pdf_objects)
pdf_objects.append(None)
page_obj_no = len(pdf_objects)
page_obj = (
"<</Type/Page/Parent %d 0 R/Contents %d 0 R>>" % (
pagelist_obj_no,
contents_obj_no
)
).encode()
pdf_objects.append(page_obj)
pagelist_obj = (
("<</Type/Pages/Resources %d 0 R" % res_obj_no) +
("/MediaBox[0 0 %.15f %.15f]" % (mm_to_pt(page_width_mm), mm_to_pt(page_height_mm))) +
("/Count 1/Kids[%d 0 R]>>" % page_obj_no)
).encode()
pdf_objects[pagelist_obj_no] = pagelist_obj
catalog_obj_no = len(pdf_objects)
catalog_obj = (
"<</Type/Catalog/Pages %d 0 R>>" % pagelist_obj_no
).encode()
pdf_objects.append(catalog_obj)
info_obj_no = len(pdf_objects)
info_obj = (
"<</Title (image test)>>"
).encode()
pdf_objects.append(info_obj)
pdf_data = b"%PDF-1.4\n"
xref = ("xref\n0 %d\n" % len(pdf_objects)).encode()
for idx, obj in enumerate(pdf_objects):
if obj is None:
xref += b"0000000000 65535 f\n"
else:
xref += ("%010d 00000 n\n" % len(pdf_data)).encode()
pdf_data += ("%d 0 obj\n" % idx).encode() + obj + b"\nendobj\n"
xref_pos = len(pdf_data)
pdf_data += xref
pdf_data += (
"trailer\n<</Root %d 0 R/Info %d 0 R/Size %d>>\nstartxref\n%d\n%%%%EOF\n" % (
catalog_obj_no,
info_obj_no,
len(pdf_objects),
xref_pos
)
).encode()
with open(sys.argv[2], "wb") as f:
f.write(pdf_data)
まとめ
画像データをPDFに埋め込み、それを描画するページを作成する方法を学んだ。
画像データは、JPEGはそのまま埋め込むことができ、その他のビットマップデータはRGBの値の列として埋め込むことがわかった。
透明度を用いる際は、画像データ本体とは別のオブジェクトとして不透明度の列を埋め込み、参照することがわかった。
調査結果に基づいて実際にPDFを出力するプログラムを書き、PDFが出力できることを確かめた。




