先に答えを書きます。
// これでは分からない
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まわりのことを書いています。