0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

画像を貼ったPDFファイルを作る

Last updated at Posted at 2025-10-17

今回の目的

指定したビットマップ画像(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としてエクスポートする際の設定

ここでは 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

各オブジェクトの参照関係は、以下のようになっていた。

PDF内のオブジェクトの参照関係

書類情報 (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する

わからない所も多いが、大きく分けて

  1. 四角形を描画する
  2. 画像を描画する

という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 で開くと、見事期待通りの図形がページ中央に表示された。

作成したPDFファイルを開いた結果

他の画像形式を見てみる

これまでの実験では、アルファチャンネルなしのビットマップ画像を扱ってきた。
ここでは、他の形式の画像を 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
オブジェクト5番のストリームの展開結果
RGBrgb
オブジェクト7番のストリームの展開結果 (hexdump)
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 を用いた。

img2pdf.py
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が出力できることを確かめた。

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?