A4サイズのtiff原稿を2つ結合してA3サイズのtiff原稿を作成する。このとき各tiff原稿には3mmの塗り足し部分が含まれており、これを考慮しないといけない。この操作をPythonを用いて自動化した。そのついでにPython/PILによるTIFFファイルの読出しについてまとめておく。マルチチャンネルの信号データやGeoTiffは考慮していない。あくまで1枚の画像ファイルのTIFFだ。

Pillow

Pythonの標準的な画像処理ライブラリであるPillowはtiffフォーマットの読み書きに対応している。PIL(Python Imaging Library)という言葉もあるようだがこちらの方が古くPillowのほうが新しいようだ。インポート時の名前だけPILを引き継いでいる。投稿時点ではPillowのバージョン8.0.1を使用している。Pythonにはtiffファイルに関するライブラリがいくつかあるようだが今回のよyな用途では結論としてPillowで十分かつ最善。

tiffファイルの読出し

tiffファイルを読みだす基本的なコードを示す。読みだす方法自体は普通の画像ファイルと変わらない。拡張子などから勝手にtiffファイルだと判断される。tiffファイルには様々なメタデータを格納することができこれもPillowで読みだすことが出来る。ファイル名の指定には引数を用いる。そのほうが後々自動化する際に便利。

import sys
from PIL import Image
from PIL import TiffTags

filename = sys.argv[1]

with Image.open(filename) as img:
  print(img.width, img.height)
  print(img.info)

  for key, value in img.tag.items():
    taginfo = TiffTags.lookup(key)

    #switching key and value
    tagdict = {(y,) : x for x, y in taginfo.enum.items()}

    print(taginfo.name, ':', tagdict.get(value, value))

このときの実行結果は例として次のようになる。

5368 7548
{'compression': 'tiff_lzw', 'dpi': (450, 450)}
ImageWidth : (5368,)
ImageLength : (7548,)
BitsPerSample : (8, 8, 8, 8)
Compression : LZW
PhotometricInterpretation : CMYK
ResolutionUnit : inch
StripOffsets : (8,)
SamplesPerPixel : (4,)
RowsPerStrip : (7548,)
StripByteCounts : (9867165,)
XResolution : ((450, 1),)
YResolution : ((450, 1),)
PlanarConfiguration : Contiguous

あたりまえのように、ファイル名を引数に与えなかったり存在しないファイルの名前を与えてコードを実行するとIndexErrorやFileNotFoundErrorが発生する。礼儀正しいツールにするならtryとexceptでこれらの例外を拾って適切なエラーメッセージを表示すべきだが、単に自分用として使う分には例外のメッセージそのものが適切なエラーメッセージだ。そのほうがコードは簡潔になる。また拡張子がtif/tiffであることの確認もしていないのでTiffTagsモジュールがうまく機能しないこともある。

tiffメタデータ

Pillowでtiffを読みだすとImage.openが返すイメージオブジェクトにはcompressionとdpiの情報を含むinfoプロパティがセットされる。これによってtiffファイルの圧縮形式と寸法がわかる。tiffファイルを入稿に用いる利点はjpegなどと違い可逆圧縮が使える点とCMYKの色指定ができる点がある。これらの情報はtagsプロパティを介して読み取ることができてTiffTagsモジュールを使うことによって見やすい形式で表示できる。

メタデータに関する情報についてはtiffフォーマットの仕様を調べても良いが、どのようなタグがあってその値の列挙型は他に何があるか等はTiffTagsモジュールのソースコードを参照するのが簡潔で早いだろう。

tiffファイルの書き込み

ここでは2つのtiff原稿を結合することを目的としているので、あくまでもとの原稿とサイズ以外の仕様がほとんど等しいようなtiffファイルの作成および書き込みを目指している。このような操作が望ましい場合はそこそこあるだろう。単純に同じフォーマットのtiffを吐き出すコードを示す。

import sys
from PIL import Image

filename1 = sys.argv[1]
filename2 = sys.argv[2]

with Image.open(filename1) as img:
  newimg = Image.new(mode=img.mode, size=(img.width, img.height))

  newimg.paste(img, (0, 0))

    newimg.save(filename2, **img.info)

pasteやcropなどは普通のPillowの使い方と何ら変わらない。ポイントはImage.newのキーワード引数の値をもとにする画像から取ってきているところと、saveの際に圧縮形式とdpi情報の入ったinfoプロパティをアンパックして渡しているところだ。他にもコピーしたいメタデータがあれば適宜saveの引数に追加すればよい。

tiffファイルの結合

あとはwidhやheightなどをごにょごにょしてやれば3mmの塗り足しを考慮してtiffファイルを結合できることは想像つくだろう。結果のみ示す。実際にツールとして即興で書いて使用したものママなので気が向いたら更新することがありえる。

import sys
from PIL import Image
from PIL import TiffTags

try:
    f1 = sys.argv[1]
    f2 = sys.argv[2]
    f3 = sys.argv[3]
except IndexError:
    print('filename required.')
    quit()

#f1 + f2 -> f3

with Image.open(f1) as img:
    metadata1 = {TiffTags.TAGS[key] : img.tag[key] for key in img.tag.keys()}
    width1, height1 = img.width, img.height
    info1 = img.info

print(width1, height1, info1)
print(metadata1)

with Image.open(f2) as img:
    metadata2 = {TiffTags.TAGS[key] : img.tag[key] for key in img.tag.keys()}
    width2, height2 = img.width, img.height
    info2 = img.info

print(width2, height2, info2)
print(metadata2)

if info1['dpi'] != info2['dpi']:
    print('dpi mismatch.')
    quit()

if width1 != width2 or height1 != height2:
    print('size mismatch.')
    quit()

info = {**info1, **info2}

if info['dpi'][0] != info['dpi'][1]:
    print('noneven dpi.')
    quit()

dpi = info['dpi'][0]

if width1 > height1:

    width = max(width1, width2)
    height = round(height1 + height2 - 6/25.4*dpi)
    print(width, height, info)

    newimg = Image.new(mode='CMYK', size=(width, height))

    with Image.open(f1) as img:
        newimg.paste(img, (0, 0))

    with Image.open(f2) as img:
        box = (0, round(3/25.4*dpi), width2, height2)
        newimg.paste(img.crop(box), (0, height1 - round(3/25.4*dpi)))

    newimg.save(f3, **info)

else:

    width = round(width1 + width2 - 6/25.4*dpi)
    height = max(height1, height2)
    print(width, height, info)

    newimg = Image.new(mode='CMYK', size=(width, height))

    with Image.open(f1) as img:
        newimg.paste(img, (0, 0))

    with Image.open(f2) as img:
        box = (round(3/25.4*dpi), 0, width2, height2)
        newimg.paste(img.crop(box), (width1 - round(3/25.4*dpi), 0))

    newimg.save(f3, **info)

with Image.open(f3) as img:
    metadata = {TiffTags.TAGS[key] : img.tag[key] for key in img.tag.keys()}

print(metadata)

おわりに

ラスタイメージの編集ソフトで手動でやろうとおもったらゾッとする作業だ。本当に便利。