0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

PHPでImageMagickがPDFを扱えるか正しく判定する【queryFormats では分からない】

0
Posted at

先に答えを書きます。

// これでは分からない
Imagick::queryFormats('PDF');   // ポリシーで禁止されていても PDF を返す

// これで分かる
try {
    (new Imagick())->readImageBlob($small_pdf);   // 実際に読ませる
} catch (\ImagickException $e) { }

queryFormats() が答えているのは**「そのコーダーがビルドに含まれているか」**です。policy.xml による許可・禁止は、読み込み時の IsCoderAuthorized() で判定されるので、問い合わせでは分かりません。

この記事では、PDFを扱えるかを正しく判定して、使えない理由まで切り分けるコードを置いておきます。

判定する4つの状態

「使えない」だけでは、次に何をすればいいか分かりません。

サーバー会社に頼む内容が、状態によって違います。

状態 判定コード 依頼する内容
拡張が無い no_extension Imagick を入れてもらう
ポリシーで禁止 pdf_blocked_by_policy policy.xml で PDF に read 権限を
ビルドが非対応 pdf_unsupported PDF 対応のビルドか確認してもらう
判定できない error ImageMagick の状態を確認

この4つを分けます。

実際に読ませる

数百バイトのPDFを埋め込んでおいて、読ませます。

<?php

final class PdfCapability
{
    /**
     * 最小限のPDF(1ページ・空・約400バイト)
     * base64 にしてソースに埋め込む
     */
    private const PROBE_PDF = 'JVBERi0xLjQKMSAwIG9iago8PC9UeXBlL0NhdGFsb2cvUGFnZXMgMiAwIF'
        . 'I+PgplbmRvYmoKMiAwIG9iago8PC9UeXBlL1BhZ2VzL0tpZHNbMyAwIFJdL0NvdW50IDE+PgplbmRv'
        . 'YmoKMyAwIG9iago8PC9UeXBlL1BhZ2UvUGFyZW50IDIgMCBSL01lZGlhQm94WzAgMCA3MiA3Ml0+Pg'
        . 'plbmRvYmoKdHJhaWxlcgo8PC9Sb290IDEgMCBSPj4K';

    public function check(): array
    {
        // 1. 拡張があるか
        if (!extension_loaded('imagick') || !class_exists('Imagick')) {
            return [
                'code'    => 'no_extension',
                'message' => 'Imagick 拡張が読み込まれていません。',
                'action'  => 'Imagick(ImageMagick の PHP バインディング)を入れてもらう',
            ];
        }

        // 2. 実際に読ませる
        try {
            $im = new \Imagick();
            $im->readImageBlob(base64_decode(self::PROBE_PDF));
            $im->clear();
            $im->destroy();

            return ['code' => 'ok', 'message' => 'PDF を読み込めます。'];

        } catch (\ImagickException $e) {
            return $this->classify($e->getMessage());
        } catch (\Throwable $e) {
            return [
                'code'    => 'error',
                'message' => '判定できませんでした: ' . $e->getMessage(),
                'action'  => 'ImageMagick の状態を確認する',
            ];
        }
    }
}

要点は、readImageBlob() を通すことです。 ここで初めて IsCoderAuthorized() が走ります。

例外メッセージで理由を分ける

    private function classify(string $message): array
    {
        // ポリシーで禁止
        if (str_contains($message, 'not allowed by the security policy')
            || str_contains($message, 'IsCoderAuthorized')) {
            return [
                'code'    => 'pdf_blocked_by_policy',
                'message' => 'ImageMagick は入っていますが、セキュリティポリシーで '
                           . 'PDF の読み込みが禁止されています。',
                'action'  => 'policy.xml で PDF コーダーに read 権限を付けてもらう',
                'policy'  => $this->findPolicyFile(),
            ];
        }

        // Ghostscript が無い
        if (str_contains($message, "FailedToExecuteCommand")
            && str_contains($message, 'gs')) {
            return [
                'code'    => 'pdf_unsupported',
                'message' => 'PDF の描画に使う外部プログラムが見つかりません。',
                'action'  => 'Ghostscript を入れてもらう',
            ];
        }

        // コーダーが無い
        if (str_contains($message, 'NoDecodeDelegateForThisImageFormat')) {
            return [
                'code'    => 'pdf_unsupported',
                'message' => 'このビルドの ImageMagick は PDF に対応していません。',
                'action'  => 'PDF 対応のビルドか確認してもらう',
            ];
        }

        return [
            'code'    => 'error',
            'message' => '判定できませんでした: ' . $message,
            'action'  => 'ImageMagick の状態を確認する',
        ];
    }

メッセージの文字列に依存しているので、ここは環境で変わる可能性があります。 最後に error へ落とすようにしてあります。

policy.xml を見つける

禁止されている場合、どのファイルが効いているかを示せると、依頼が早くなります。

    private function findPolicyFile(): ?string
    {
        $candidates = [];

        // 1. 環境変数が最優先
        $env = getenv('MAGICK_CONFIGURE_PATH');
        if ($env) {
            foreach (explode(PATH_SEPARATOR, $env) as $dir) {
                $candidates[] = rtrim($dir, '/') . '/policy.xml';
            }
        }

        // 2. ビルド時の設定パス
        if (class_exists('Imagick')) {
            $opts = \Imagick::getConfigureOptions('CONFIGURE_PATH');
            if (!empty($opts['CONFIGURE_PATH'])) {
                $candidates[] = rtrim($opts['CONFIGURE_PATH'], '/') . '/policy.xml';
            }
        }

        // 3. よくある場所
        $candidates = array_merge($candidates, [
            '/etc/ImageMagick-7/policy.xml',
            '/etc/ImageMagick-6/policy.xml',
            '/etc/ImageMagick/policy.xml',
            '/usr/local/etc/ImageMagick-7/policy.xml',
            '/usr/local/etc/ImageMagick-6/policy.xml',
            '/opt/homebrew/etc/ImageMagick-7/policy.xml',
        ]);

        foreach (array_unique($candidates) as $path) {
            if (is_readable($path)) {
                return $path;
            }
        }
        return null;
    }

環境変数を先に見るのが要点です。

getConfigureOptions() が返すのはビルド時のパスで、実行時には存在しないことがあります。実測した環境では、こうなっていました。

MAGICK_CONFIGURE_PATH  (実際に効いているパス)
getConfigureOptions    /var/folders/.../imagemagick-source/ImageMagick-7.1.1-15/build/etc/ImageMagick-7/

後者はビルド作業ディレクトリの残骸です。 これだけを見ると、効いているファイルにたどり着けません。

コメントアウトを除く

ここが、いちばんはまりました。

policy.xml の中身を読むとき、素朴に <policy> を全部拾うと誤判定します。

実際のサーバーから、そのまま引用します。

<!-- 有効なもの -->
<policy domain="coder" rights="none" pattern="EPHEMERAL" />
<policy domain="coder" rights="none" pattern="HTTPS" />
<policy domain="coder" rights="none" pattern="MVG" />
<!-- コメントの中にあるもの=効いていない -->
<!--
  <policy domain="coder"  rights="none"       pattern="*" />
  <policy domain="coder"  rights="read|write" pattern="{GIF,JPEG,PNG,WEBP}" />
  <policy domain="module" rights="none"       pattern="{PS,PDF,XPS}" />
-->

「PS/PDF/XPS モジュール禁止」は、コメントの中にあります。

上流の policy.xml に設定例として書かれているものが、そのまま残っているだけです。

ホスティング事業者が PDF を解禁するとき、行を削除せずコメントアウトします。 だから、こういう状態になります。

素朴に拾うと、許可されているサーバーを「禁止」と判定します。

    /**
     * コメントを除いてから coder ルールを拾う
     */
    private function activeCoderRules(string $path): array
    {
        $xml = @file_get_contents($path);
        if ($xml === false) {
            return [];
        }

        // コメントを先に除去する
        $xml = preg_replace('/<!--.*?-->/s', '', $xml);

        preg_match_all(
            '/<policy\s+domain=["\']coder["\'][^>]*>/i',
            $xml,
            $m
        );
        return $m[0] ?? [];
    }

preg_replace でコメントを落としてから、preg_match_all で拾います。

順番を逆にすると、意味がありません。

exec() が使えない場合

identify -list policy を打てば、有効なポリシーだけが出ます。 これがいちばん確実です。

identify -list policy

ただ、WordPress.org で配布するプラグインでは exec() 系が使えません。 規約で禁止されています。

共有レンタルサーバーでも、無効にされていることがあります。

だから、自前でパースする必要がありました。

使い方

$result = (new PdfCapability())->check();

if ($result['code'] === 'ok') {
    // 生成できる
} else {
    printf(
        "%s\nやること: %s\n",
        $result['message'],
        $result['action']
    );
    if (!empty($result['policy'])) {
        printf("設定ファイル: %s\n", $result['policy']);
    }
}

出力の例です。

ImageMagick は入っていますが、セキュリティポリシーで PDF の読み込みが
禁止されています。
やること: policy.xml で PDF コーダーに read 権限を付けてもらう
設定ファイル: /etc/ImageMagick-6/policy.xml

サーバー会社への問い合わせに、そのまま貼れる形にしてあります。

バージョン番号で決めつけない

もう1つ、同じ種類の間違いをしていました。

「ImageMagick 6 は CMYK の PDF を真っ白にすることがある」という既知の問題があります。なので、バージョンを見て警告を出していました。

実測したら、壊れませんでした。

ImageMagick 6.9.13-25 Q16
  colorspace           CMYK (id 12)
  centre pixel (sRGB)  #00FFFF

CMYK の経路を通って、シアンが正しく描画されています。

デリゲートの設定次第なので、バージョン番号だけでは決まりません。

ここも、テストページを実際に描画してから報告するように変えました。

定数の値が環境で違う

ついでに見つけたものです。

// ImageMagick 7.1.1
Imagick::COLORSPACE_CMYK  // => 2

// ImageMagick 6.9.13
Imagick::COLORSPACE_CMYK  // => 12
if ($im->getImageColorspace() === 12) {          // 片方の環境で壊れる
if ($im->getImageColorspace() === Imagick::COLORSPACE_CMYK) {   // これ

リテラルで比較しているコードは、どちらかの環境で動きません。

まとめ

  • Imagick::queryFormats()ビルドに含まれるかを答える。 ポリシーは見ていない
  • 許可の判定は読み込み時の IsCoderAuthorized()実際に読むまで分からない
  • Ghostscript が無い場合も、queryFormats() は素通りする
  • 数百バイトのPDFを読ませて判定する
  • 例外メッセージで理由を分ける。依頼先が違うため
  • policy.xml環境変数を先に見る。 getConfigureOptions() はビルド時のパスを返すことがある
  • コメントを除去してから <policy> を拾う。 禁止行はコメントアウトされて残る
  • exec() が使えない環境では、自前でパースするしかない
  • バージョン番号で CMYK の可否を決めない。 描画して確かめる
  • Imagick::COLORSPACE_CMYK の値は環境で違う

能力を問い合わせるのではなく、能力を使ってみる。

数百バイトのコストで、誤診がなくなりました。


ふだんはraplsworks.comで、WordPressプラグイン開発やClaude Codeまわりのことを書いています。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?