はじめに

印刷系の業務システムで働くエンジニアの方や、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のまま読み込むと色反転した画像になります。そこで、読み込み時にTIFFImageReadParamPhotometricInterpretationを確認し、CMYKならRGB変換フラグを立てます。

Java
Iterator<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)を無視した簡易版です。

Java
public 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で補正されないため、色反転したように見えます。
これを回避するには、ImageReadParamdestinationBandsを明示的にRGB 3バンドにマッピングするか、上記のように自前で変換する必要があります。

解決策:高速化のためのルックアップテーブル(LUT)

ピクセルごとの行列計算は遅いため、256^3通りの変換結果を事前に計算して配列に保持しておくと、1000×1000ピクセルで約20ms→3msに高速化できました。

Java
private 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近似するためのニューラルネット軽量モデルへの置き換えを扱う予定です。

参考資料