term タグ別の記事一覧

PDFがCMYKで出力されているのを無課金で調べる

 私事で印刷業者にちょっとしたものを印刷してもらう用事ができたのですが、その入稿データとしてCMYKで出力されたPDFを要求されました。CMYKはシアン、マゼンタ、イエロー、黒の4色を混ぜ合わせてカラー原稿を表現する方式で、印刷業界で標準的に使われている方式です。いっぽう、PCでは一般的にRGB(レッド、グリーン、ブルー)の3色を混ぜ合わせて色を表現するのが一般的で、多くの画像編集ツールやデジタルカメラなどではこちらのRGBを使った形式でファイルを出力することが多いです。そのため、CMYKを取り扱うにはCMYK出力をサポートしたソフトウェアが必要になります。

 といっても、CMYKでPDFを出力すること自体はInkscape等のフリーソフトウェアでも可能です。とはいえ、特にCMYKでの出力は初めてということもあって、そこで出力したものが本当に正しくCMYKで出力されているのか、入稿前に確認しようと考えました。

 軽く調べたところ、Acrobatの有償版(Acrobat Pro)で利用できる「印刷プレビュー」機能を利用すれば、使用されている色やRGBデータの有無を確認できるようです。ただ、単にデータを確認するだけに安くない金額を払うのにはとても抵抗感があります。また、他にも印刷向けにPDFの情報をチェックできるソフトウェアが存在するようですが、見つけたものはいずれも有償のものでした。

 ということで、連休で時間があるというのもあってPDFフォーマットを分析して自力でPDFでCMYKが使用されているかを調べる方法を調べることにしました。

参考資料

 PDFのフォーマット仕様は標準規格として公開されており、その仕様書はAdobeのオープンソースサイト(https://opensource.adobe.com/dc-acrobat-sdk-docs/acrobatsdk/)で公開されています。ただし英語で分量も多いため、これをいきなり読むのはハードでしょう。

 日本語の情報としては、下記が有用でした。

PDFがCMYKで出力されているかを調べるために必要な情報の要約

 以上の資料を読んだうえで、後述するPDFを取り扱うライブラリを使っていくつかのPDFを確認したところ、以下のような理解になりました。

  • PDFはオブジェクトと呼ばれるデータ要素の組み合わせで構成されている
  • PDFには必ずルートオブジェクトがあり、ページ内のコンテンツはルートオブジェクトからたどれる/Pagesオブジェクトに入っている
    • ルートオブジェクトにはそのほか画面表示のためのプロパティやメタデータなどが含まれていることがあるが、これらはPDFが含む色には関係ない
      • PDF作成ツールによってはメタデータに使用している色空間の情報が入っていることもあるようだ
  • PDFには「Indirect Object」という別のオブジェクトを参照する仕組みがあり、各オブジェクトの子オブジェクトとして実際のオブジェクトを指定する代わりにIndirect Objectを使って別のオブジェクトを参照させることもできる
  • /Pagesオブジェクトはページを表現する/Pageオブジェクトを子オブジェクトとして保持する/Kidsオブジェクトを含んでいる
  • /Pagesオブジェクトや/Pageオブジェクトには各ページで使われているコンテンツや各種設定を含むリソースやページサイズ、印刷サイズといった情報が含まれている
  • PDFは1ファイル中にさまざまな色空間を混在させることができる
    • 色空間としてはグレイスケール、RGB、CMYKを取り扱える。さらにそれぞれカラーマネージメント(デバイスや出力装置に応じて適切に補正を行うことで色の再現性を高める仕組み)あり/なしの両方をサポートする
  • ページ内に描画するコンテンツは各/Pageオブジェクト内の/Contentsオブジェクトに格納されている
  • PDFはテキストベースで記述されているが、/Contentsオブジェクトにはテキストを圧縮したバイナリデータが含まれていることがある。その場合、/Contentsオブジェクト内のデータにアクセスするには指定された方式でバイナリを展開する必要がある
  • /Contentsオブジェクトには「<オペランド(パラメータ)> <オペレータ(命令)>」という形式で描画命令が格納されている。用意されている命令には直線や図形、文字などを描画するものや領域を塗るといったものに加えて、色や描画パラメータを変更するものや、描画するオブジェクトをグルーピング(レイヤー分け)するものなどが含まれている
    • 色や塗り潰しパラメータはグレイスケール/RGB/CMYKで指定できるほか、あらかじめ定義しておいた色(インデックスカラーもしくは特色)やパターンを選択することもできる
    • 色や塗り潰し、描画のパラメータは直接記述することもできるし、/Pagesや/Pageオブジェクト内の/Resourcesオブジェクトに格納しておいてそれを参照することもできる
      • あらかじめ定義しておいた色やパターンは/ColorSpaceというキー、また描画パラメータについては/ExtGStateというキーにひも付けられて格納されている

 まとめと言いながら長々と書いてしまいましたが、まず重要なポイントとしてはPDF内では複数の色空間を同時に扱えるという点です。そのため、「CMYKで出力されている」かどうかは、「グレイスケールもしくはRGBが使われていない」で判断することになりそうです。

 また、インデックスカラーや特色、塗りつぶしパターンは/Resourceオブジェクトに格納されていることもありますが、グレイスケール/RGB/CMYKの色指定は/Contentsオブジェクト内に記述された命令で行われています。そのため、グレイスケール/RGB/CMYKのどれが使われているかは/Contentsオブジェクト内で使われている色指定命令をすべて調べる必要があるようです。

pypdfでPDFの内部データにアクセスする

 PDFではIndirect Objectを使った参照が頻繁に使われるほか、バイナリデータを保持する/Contentsオブジェクトなども存在するため、内部構造を解析するだけでも大変です。そのため、今回はPython向けのPDFライブラリであるpypdfを利用して内部データにアクセスすることにしました。

 pypdfでは、次のようにpypdf.PdfReaderクラスを使用することでファイルオブジェクトなどからPDFデータを読みだすことができます。

with open(fn, "rb") as fp:
    r = pypdf.PdfReader(fp)

 ルートオブジェクトは、pypdf.PdfReaderオブジェクトのroot_objectプロパティに格納されています。たとえば次のようなコードでルートオブジェクトに格納されているオブジェクトとそのクラスを確認できます。

print("ROOT OBJECTS:")
for k in r.root_object:
    print(k, r.root_object[k].__class__)

 たとえば、とあるPDFのルートオブジェクトを確認した結果、次のような出力になりました。

ROOT OBJECTS:
/Type <class 'pypdf.generic._base.NameObject'>
/Pages <class 'pypdf.generic._data_structures.DictionaryObject'>
/ViewerPreferences <class 'pypdf.generic._data_structures.DictionaryObject'>
/Lang <class 'pypdf.generic._base.TextStringObject'>
/OCProperties <class 'pypdf.generic._data_structures.DictionaryObject'>
/Metadata <class 'pypdf.generic._data_structures.DecodedStreamObject'>

 pypdfではPDFで使われる各オブジェクトの形式に応じたクラスが定義されており、PDF内のオブジェクトは自動的に対応するpypdfのオブジェクトに変換されて木構造が構築されます。

ページ内のコンテンツを確認する

 前述のように、PDFの各ページはルートオブジェクトから参照される/Pagesオブジェクトに格納されています。このオブジェクトの情報は、次のようなコードで確認できます。

print("PAGES:")
print(p.root_object["/Pages"])

 実行結果は下記のようになりました。これは、PDFのページ数が1で、また/Pagesオブジェクトにはその実態となるオブジェクトの代わりにそのオブジェクトへの参照(IndirectObject)が含まれていることを意味します。

{'/Type': '/Pages', '/Count': 1, '/Kids': [IndirectObject(7, 0, 1219641424528)]}

 pypdfでは、get_object()メソッドでそのIndirect Objectが参照するオブジェクトの実態を取得できます。つまり、この例の場合、PDF内に含まれる唯一のページに対応するオブジェクトは次のようにして取得できます。

page = pages["/Kids"][0].get_object()

 /Pageオブジェクトには、/Contentsオブジェクトとしてそのページ内に描画されるデータが含まれています。また、get_data()メソッドでオブジェクトからその生バイナリデータを取得できます。

raw_content = page["/Contents"].get_data()

 PDFの描画データ(描画命令)はテキスト形式で表現できますが、/Contentsオブジェクトに含まれているデータはその生データ(バイナリデータ)なので、テキストとして扱いたい場合はデコードする必要があります。たとえばデコードしたデータを出力するには次のようにします。

print(raw_content.decode("ascii"))

 次の例は、このコードで実際に抜き出した描画データの一部です。

/OC/oc2 BDC
0.721569 0.678431 0.670588 0.882353 k
5.58236 48.749 m
8.19161 48.749 l
8.57899 48.749 8.86249 48.6587 9.04211 48.4782 c
9.22174 48.2979 9.31155 48.0342 9.31155 47.6872 c
9.31155 45.7844 l
9.31155 45.5798 9.27192 45.4135 9.19267 45.2857 c
9.11355 45.1579 8.99586 45.0742 8.83961 45.0346 c
8.99586 44.9937 9.11355 44.9182 9.19267 44.8081 c
9.27192 44.698 9.31155 44.5414 9.31155 44.3384 c
9.31155 42.2962 l
9.31155 41.9492 9.22174 41.6854 9.04211 41.5049 c
8.86249 41.3245 8.57899 41.2344 8.19161 41.2344 c
5.58236 41.2344 l
h

 最初の「/OC/oc2 BDC」はこれから描画するコンテンツがどのレイヤーに所属しているかを示すものです。「/oc2」は/Pageオブジェクトの/Resources内で定義されているプロパティで、このプロパティにはレイヤー名やそのレイヤーに関する情報が含まれています。

 次に「0.721569 0.678431 0.670588 0.882353 k」という命令が続きます。この「k」は次のような形でCMYK形式で色を指定する命令です。

<c> <m> <y> <k> k

つまり、これに続く命令はCMYKで指定された色で描画を行うことになります。

なお、CMYK形式の場合は「k」もしくは「K」ですが、グレイスケールの場合は「g」もしくは「G」、RGBの場合は「rg」もしくは「RG」で色を指定します。大文字の場合ストロークの色を、小文字の場合はストローク以外の塗りつぶし等の色を指定することになります。

 色指定にはそのほか「sc」「SC」や「scn」「SCN」という命令もありますが、これらは色空間を指定する「cs」「CS」命令と組み合わせて使うもので、csもしくはCS命令で指定した色空間に応じてパラメータを指定することになります。

描画に使用されている色空間を判断する

 さて、本記事のテーマである色空間の判別ですが、まとめると以下のようになります。

  • 描画データ中に「g」や「G」命令がある場合:グレイスケールが使われている
  • 描画データ中に「rg」や「RG」命令がある場合:RGBが使われている
  • 描画データ中に「k」や「K」命令がある場合:CMYKが使われている
  • 描画データ中に「sc」や「SC」、「scn」、「SCN」命令がある場合:その時点で最後に実行された「cs」「CS」で指定されている色空間が使われている

 この中で厄介なのが「sc」や「SC」、「scn」、「SCN」命令のパターンで、この場合まずその前に実行されている「cs」や「CS」命令を探し、そこで指定されている色空間がグレイスケール/RGB/CMYKのどれなのかを調べる必要があります。「cs」や「CS」命令ではパターンや特色を指定することもでき、その場合そのパターンや特色がどの色空間で指定されているかも調べる必要があります。

 これを踏まえて問題のPDFのデータを確認したところ、描画中には「k」と「K」命令しか含まれていませんでした。つまり、このPDFは無事CMYKで出力されていた、と言うことになります。

 ちなみに確認は上記で挙げたコード例のようなコードでページのコンテンツをテキストファイルに出力し、それをテキストエディタで開いて「g」や「rg」、「k」、「sc」という文字列を検索して目視で確認するという原始的な方法で行いました。今回は1ページだけのPDFファイルで、かつラスター画像を含まないものだったのでこれで十分でしたが、ページ数が増えた場合はプログラムを書いてちゃんと分析しないと大変そうです。