PHP の Imagick で画像を加工して PNG で保存しようとしたとき、ImagickException が投げられ、メッセージに「No IDATs written into file」と出ることがあります。見慣れない言葉ですが、PNG のファイルの作りを知ると、何が起きたのか見当を付けやすくなります。原因は 1 つに決まらないので、ここではエラーの意味と調べ方を整理します。

IDAT は PNG の画像データ

PNG のファイルは、「チャンク」と呼ばれるかたまりが並んだ作りです。主なものは次の 3 つです。

  • IHDR: 幅や高さ、色の種類など、画像の基本の情報
  • IDAT: 画像のデータの本体 (ピクセルの並びを圧縮したもの)
  • IEND: ファイルの終わり

「No IDATs written into file」は、PNG を書き出すライブラリ libpng が出すエラーで、「ファイルに IDAT が 1 つも書かれなかった」という意味です。ImageMagick は PNG の書き出しに libpng を使っており、そのエラーが Imagick を通じて例外として届きます。書き出しを終えようとした時点で、画像のデータが 1 行も書かれていなかった、ということを示しています。

起こりうる原因

画像のデータが書かれないのは、書き出そうとした画像の中身が空だったか、書き出しが途中で進まなかったかのどちらかです。次のような場面で起こりうるとされています。ただし、ImageMagick の版や環境によって振る舞いが違うので、手元で確かめながら絞り込みます。

  • 幅や高さが 0 の画像を書き出そうとした (縮小の大きさを計算した結果が 0 になった、など)
  • 切り抜きの範囲が、画像の外にはみ出していた。範囲が画像と重ならないと、警告とともに思わぬ大きさの画像になることがある
  • 画像の読み込みに失敗していたのに、例外を握りつぶして加工を続けた
  • アニメーション GIF のように複数のフレームを持つ画像で、加工のあとに一部のフレームが空になっていた

書き出す前に大きさを確かめる

書き出す直前に、画像の大きさを確かめます。失敗したときに原因を追えるよう、例外を捕まえて、そのときの画像の状態を記録しておきます。

<?php
// PNG で保存する。大きさを確かめ、失敗したら画像の状態を記録する
function save_png(Imagick $image, string $path): void
{
    $width = $image->getImageWidth();
    $height = $image->getImageHeight();
    if ($width < 1 || $height < 1) {
        throw new RuntimeException("画像の大きさが正しくありません ({$width}x{$height})");
    }

    $image->setImageFormat('png');
    try {
        $image->writeImage($path);
    } catch (ImagickException $e) {
        $page = $image->getImagePage(); // 仮想キャンバスの大きさと位置
        error_log(sprintf(
            'PNG の書き出しに失敗しました: %s (大きさ %dx%d、仮想キャンバス %dx%d+%d+%d、フレーム数 %d)',
            $e->getMessage(), $width, $height,
            $page['width'], $page['height'], $page['x'], $page['y'],
            $image->getNumberImages()
        ));
        throw $e;
    }
}

まだ画像を読み込んでいない空の Imagick に対しては、getImageWidth() 自体が例外を投げます。読み込みの失敗に気づくきっかけにもなるので、この例外も握りつぶさないようにします。

切り抜きの範囲を外から受け取るときは、画像の内側に収めてから切り抜きます。

<?php
// 切り抜きの範囲を、画像の内側に収めてから切り抜く
function crop_within(Imagick $image, int $x, int $y, int $w, int $h): void
{
    $iw = $image->getImageWidth();
    $ih = $image->getImageHeight();
    $x = max(0, min($x, $iw - 1));
    $y = max(0, min($y, $ih - 1));
    $w = max(1, min($w, $iw - $x));
    $h = max(1, min($h, $ih - $y));

    $image->cropImage($w, $h, $x, $y);
    // 切り抜いたあとに残る仮想キャンバスの情報を、画像に合わせてリセットする
    $image->setImagePage(0, 0, 0, 0);
}

ImageMagick の画像は、実際のピクセルの大きさとは別に、「仮想キャンバス」(ページ) の大きさと位置を持っています。切り抜いたあとも元の位置の情報が残るため、setImagePage(0, 0, 0, 0) でリセットしておくと、そのあとの加工で思わぬ結果になるのを防げます。

複数のフレームを持つ画像は、フレームごとに大きさや位置が違うことがあります。coalesceImages() で、どのフレームも全体と同じ大きさの画像にそろえてから加工すると扱いやすくなります (戻り値は新しい Imagick のオブジェクトです)。

原因を絞り込む手順

再現する画像が見つかったら、次の順に試すと、どこで画像が空になったのかを絞り込めます。

  1. 例外のメッセージと、書き出す直前の大きさ、仮想キャンバス、フレーム数を記録する (上の save_png() のように)
  2. 同じ画像を、加工せずにそのまま PNG で書き出してみる。これで失敗するなら、読み込みや元の画像を疑う
  3. 加工の処理を 1 つずつ外して、どの処理のあとで大きさがおかしくなるかを見る
  4. JPEG など別の形式で書き出してみる。PNG のときだけ失敗するのかどうかで、見当が変わる
  5. ImageMagick と Imagick の版を記録する。Imagick::getVersion() や php --ri imagick で確かめられる。本番と手元で版が違うと、同じ処理でも結果が違うことがある

手元の環境に Imagick を入れるときの注意は、XAMPP に Imagick を入れたら Apache ごと落ちる で取り上げています。

まとめ

  • 「No IDATs written into file」は libpng のエラーで、PNG に画像のデータが書かれなかったことを示す
  • 幅や高さが 0 の画像、画像の外にはみ出した切り抜き、読み込みに失敗した画像などで起こりうる。原因は手元で確かめて絞り込む
  • 書き出す前に getImageWidth() などで大きさを確かめ、ImagickException を捕まえて画像の状態を記録する
  • 切り抜きの範囲は画像の内側に収め、切り抜いたあとは setImagePage(0, 0, 0, 0) でリセットする

参考