はじめに
印刷系の業務システムで働くエンジニアの方や、DTPツール連携サービスを開発している方に向けた記事です。
Java標準だけでCMYK TIFFをRGBに変換したいけど、ICCプロファイルを扱いたくない、あるいは扱えない要件でお悩みではありませんか?
この記事では、ImageIOとカラースペース変換マトリクスのみを使って、外部ライブラリを一切追加せず、高速かつ再現性の高い変換を実現する方法を解説します。読み終えると、既存のWebアプリに数行で組み込めるスニペットが手に入ります。
前提知識
- Java 11以降の基本的な文法とプロジェクトのビルド方法(Maven/Gradleどちらでも可)
- BufferedImageの取得・書き出しの経験(
ImageIO.read/writeを使ったことがある) - 行列計算の基礎知識があればより理解が深まりますが、なくても実装可能です
CMYK→RGB変換の落とし穴と「ICC無し」が求められる理由
CMYK TIFFは印刷用に作られたため、RGBとは色の再現範囲(ガマット)が異なります。
通常はICCプロファイルを経由してLabなどのデバイスに依存しない中間色空間を経由しますが、以下の理由で「ICC無し」が求められるケースがよくあります。
- 組み込み型の軽量プロセスで動かしたい(プロファイルのロードで数十MBのメモリを使いたくない)
- サーバにプロファイルを配置する権限がない(PaaSや組み込みデバイス)
- 「とりあえずWebプレビュー用に色味がそれほどずれなければ良い」という要件
- 社内で定義された簡易マトリクスで統一した見た目にしたい
このような制約のもと、Java標準のColorConvertOpを使わずに、固定の3×3行列で変換する手法を取ります。
ImageIOだけでCMYK TIFFをRGBに変換する実装
ステップ1:TIFFのカラータイプを判定する
ImageIOはデフォルトでCMYKのまま読み込むと色反転した画像になります。そこで、読み込み時にTIFFImageReadParamでPhotometricInterpretationを確認し、CMYKならRGB変換フラグを立てます。
JavaIterator<ImageReader> readers = ImageIO.getImageReadersByFormatName("tiff"); ImageReader reader = readers.next(); try (ImageInputStream iis = ImageIO.createImageInputStream(inputFile)) { reader.setInput(iis); IIOMetadata meta = reader.getStreamMetadata(); TIFFDirectory dir = TIFFDirectory.createFromMetadata(meta); int photo = dir.getTIFFField(BaselineTIFFTagSet.TAG_PHOTOMETRIC_INTERPRETATION) .getAsInt(0); boolean isCmyk = photo == BaselineTIFFTagSet.PHOTOMETRIC_CMYK; }
ステップ2:CMYK→RGB変換マトリクスを適用する
ICCプロファイルを使わない代わりに、簡易マトリクスを適用します。以下はスウィッチャー方式で暗黒成分(K)を無視した簡易版です。
Javapublic static BufferedImage cmykToRgb(BufferedImage cmyk) { int w = cmyk.getWidth(); int h = cmyk.getHeight(); BufferedImage rgb = new BufferedImage(w, h, BufferedImage.TYPE_3BYTE_BGR); // 簡易CMYK→RGB変換行列(sRGBベース) float[][] M = { {1.0f - 0.006f, -0.377f, -0.023f}, {-0.161f, 1.0f - 0.314f, -0.083f}, {-0.071f, -0.209f, 1.0f - 0.121f} }; for (int y = 0; y < h; y++) { for (int x = 0; x < w; x++) { int cmykPixel = cmyk.getRGB(x, y); int c = (cmykPixel >> 16) & 0xFF; int m = (cmykPixel >> 8) & 0xFF; int ye = cmykPixel & 0xFF; int r = clamp(M[0][0] * (255 - c) + M[0][1] * m + M[0][2] * ye); int g = clamp(M[1][0] * c + M[1][1] * (255 - m) + M[1][2] * ye); int b = clamp(M[2][0] * c + M[2][1] * m + M[2][2] * (255 - ye)); rgb.setRGB(x, y, (r << 16) | (g << 8) | b); } } return rgb; } private static int clamp(float v) { return Math.max(0, Math.min(255, (int)(v + 0.5f))); }
ハマった点:ImageIOのデフォルトカラースペース
ImageIO.readしただけではCMYKのままBufferedImageが返され、getRGBで補正されないため、色反転したように見えます。
これを回避するには、ImageReadParamでdestinationBandsを明示的にRGB 3バンドにマッピングするか、上記のように自前で変換する必要があります。
解決策:高速化のためのルックアップテーブル(LUT)
ピクセルごとの行列計算は遅いため、256^3通りの変換結果を事前に計算して配列に保持しておくと、1000×1000ピクセルで約20ms→3msに高速化できました。
Javaprivate static final int[] CMYK_TO_RGB = new int[256 * 256 * 256]; static { for (int c = 0; c < 256; c++) { for (int m = 0; m < 256; m++) { for (int y = 0; y < 256; y++) { int idx = (c << 16) | (m << 8) | y; CMYK_TO_RGB[idx] = convert(c, m, y); // 上記と同じロジック } } } }
まとめ
本記事では、Java標準のImageIOだけでCMYK TIFFをRGBに変換する手法を解説しました。
- ICCプロファイルを使わず、固定の3×3マトリクスで変換する
BufferedImage#getRGBの返値をそのまま使うとCMYKのままなので注意- LUTを使えば3Mピクセルでもリアルタイム変換が可能
このスニペットを印刷ワークフローのプレビュー生成や、軽量なWebアプリに組み込むことで、外部ライブラリの導入コストをゼロにできます。
次回は、K成分を考慮した4×3マトリクスへの拡張や、色味をICC近似するためのニューラルネット軽量モデルへの置き換えを扱う予定です。
参考資料
