ゆう's Blog
WebPへ変換

$img = new Imagick($filepath); $frames = $img->getNumberImages(); if ($frames > 1) { if ($file_ext === 'gif') { // アニメ GIF s_webp -> ffmpeg webp -> gif2webp o_webp -> gif2webp } // アニメ WebP s_webp -> アニメ WebP → PNG → ffmpeg(静止画) webp = o_webp -> そのままコピー(PHP の copy) } else { if ($file_ext === 'gif') { // 静止 GIF s_webp -> ffmpeg webp -> 静止GIF → PNG → cwebp o_webp -> 静止GIF → PNG → cwebp } // JPEG/PNG/静止 WebP s_webp -> ffmpeg webp -> cwebp o_webp -> cwebp }

※ cwebp が GIF をサポートしていない場合
※ ffmpeg で Animated WebP をデコードできない場合

ffmpeg でアニメGIFを「中央クロップ150px角のアニメWebP」に変換する

ffmpeg の crop は 入力画像より大きいサイズを指定すると必ずエラーになるため scale で短い方を150pxに“拡大”して揃えてから中央150px角を cropする。

アニメGIF → アニメWebP

ffmpeg -i input.gif \ -vf "scale='if(gt(iw,ih),-1,150)':'if(gt(iw,ih),150,-1)',crop=150:150" \ -c:v libwebp -loop 0 output.webp

アニメGIF → 静止サムネイル(PNG)

ffmpeg -i input.gif \ -vf "scale='if(gt(iw,ih),-1,150)':'if(gt(iw,ih),150,-1)',crop=150:150" \ -vframes 1 temp.png

● 1. まず短い方を 必ず150pxに揃える
横長(iw > ih)
→ 高さを150に、幅は自動計算(-1)
縦長(ih > iw)
→ 幅を150に、高さは自動計算(-1)
つまり 短い方が必ず150pxになる
● 2. その後に中央150px角を crop

ffmpeg の “increase” が効かない特殊ケース

Invalid too big or non positive size for width '150' or height '150' Failed to configure input pad on Parsed_crop_1 Error reinitializing filters! Failed to inject frame into filter network: Invalid argument Error while filtering: Invalid argument

内部で「拡大後のサイズが整数にならない」などの理由で crop の初期化に失敗することがある。
220×123 の GIF は、縦が 123px(150px 未満)なので本来は increase で 150px 以上に拡大されるはずですが、 拡大後のサイズが 奇数/偶数の組み合わせで ffmpeg が scale → crop の再初期化に失敗することがあります。

PHP の外部コマンド実行に必要な 3 つの要素

外部コマンドを扱うときは、次の 3 つが基本になります。
sprintf:文字列をフォーマットしてコマンドを組み立てる
escapeshellarg:引数を安全にエスケープする
exec:コマンドを実行して結果を受け取る

🧱 1. sprintf:フォーマットしてコマンドを組み立てる
役割
「%s の部分に変数を埋め込んで、整った文字列を作る」

$cmd = sprintf('cwebp %s -o %s', $input, $output);

🛡 2. escapeshellarg:引数を安全にする
役割
「シェルで危険な文字を無害化して、1つの引数として扱わせる」

$input = escapeshellarg($inputPath); $output = escapeshellarg($outputPath);

🚀 3. exec:コマンドを実行して結果を受け取る

exec($cmd, $output, $ret);

引数の意味
$cmd:実行するコマンド
$output:コマンドの標準出力(配列で返る)
$ret:終了ステータス(0 が成功)

exec($cmd, $output, $ret); if ($ret !== 0) { echo "エラーが発生しました"; }

🧩 3 つを組み合わせた「正しい外部コマンド実行」

$input = escapeshellarg($inputPath); $output = escapeshellarg($outputPath); $cmd = sprintf( 'cwebp %s -o %s', $input, $output ); exec($cmd, $outputLines, $ret); if ($ret !== 0) { // エラー処理 }

画像変換

SVG → PNG/WebP の品質は、
rsvg-convert + cwebp の組み合わせが最強。

SVG → PNG(rsvg-convert)
PNG → WebP(cwebp)

GIF / JPEG / PNG / WebP → WebP の品質は、
cwebp + gif2webp の組み合わせが最強。

🌟 cwebp は何をするツールなのか
✔ 入力:PNG / JPEG / TIFF / BMP / PPM / WebP(再圧縮)
✔ 出力:WebP(lossy または lossless)

GIF と WebP 変換の正しい整理

1. アニメーション GIF
フレームを複数持つ
→ gif2webp が必須
cwebp はアニメーションを扱えない
フレーム保持・最適化・ループ情報などを正しく処理できるのは gif2webp

2. 静止画 GIF(1フレーム)
実体は「256色の PNG に近い静止画像」
→ cwebp で完全に変換可能
透明度もそのまま扱える
画質も PNG と同じ扱いで良い

判定

● GIF のフレーム数をチェック

$img = new Imagick($input);
if ($img->getNumberImages() > 1) {
  $isAnimatedGif = true;
}

または、

$cmd = sprintf('identify -format "%%n" %s', escapeshellarg($tmpImg));
$frames = (int) trim(shell_exec($cmd));

但し、cwebp が GIF をサポートしている必要がある。
GIF デコーダを含まないビルドであるならば、最適な変換フローは、

JPEG / PNG / WebP → cwebp
静止 GIF → PNG(Imagick)→ WebP(cwebp)
アニメ GIF → gif2webp

また、cwebpでリサイズはできるが、gif2webpではリサイズはできない。アニメgifのリサイズ+WebPへの変換は ffmpeg が使える。

curl_exec() の結果のバイト数を取得

$data = curl_exec($ch); $byteLength = strlen($data); echo "Data length in bytes: " . $byteLength;

strlen() はデータを文字列として扱うので、このようなバイナリデータやメモリ内データの場合に便利です。

filesize() は、ファイルのサイズを取得するために設計されているため、ファイル自体が物理的に存在している必要があります。対して、curl_exec() の結果はバッファされたデータであり、直接ファイルとして保存されているわけではないので、filesize() を使用して $data のサイズを取得しようとすると、正常に動作しません。

IP2Location.io

IP2Location.io

$ch = curl_init('https://api.ip2location.io/?ip=8.8.8.8'); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); $json = curl_exec($ch); curl_close($ch); $api_result = json_decode($json, true); echo '<pre>'; var_dump($api_result); echo '</pre>';

APIキーなしでも動作するが、1日あたり1,000件のクエリに制限される。
無料プランに登録するとAPIキーを取得でき、1か月あたり5万件のクエリを利用できる。

アクセス解析

// tracking.php // 訪問データの取得 $ip = $_SERVER['REMOTE_ADDR']; $page = $_SERVER['REQUEST_URI']; $time = date('Y-m-d H:i:s'); $referrer = isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : ''; $user_agent = $_SERVER['HTTP_USER_AGENT']; // ここでデータベースへの接続および記録処理を行います try { $pdo = new PDO('mysql:host=ホスト名;dbname=データベース名', 'ユーザー名', 'パスワード', [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION ]); $stmt = $pdo->prepare("INSERT INTO access_logs (ip, page, access_time, referrer, user_agent) VALUES (?, ?, ?, ?, ?)"); $stmt->execute([$ip, $page, $time, $referrer, $user_agent]); } catch (PDOException $e) { error_log("DBエラー: " . $e->getMessage()); }

echo substr('abcdef', 0, 8); // abcdef

文字数,バイト数を確認

// 絵文字制限(例: VARCHAR(30)) $maxLength = 30; $maxBytes = $maxLength * 4; // 文字数,バイト数を確認 if (mb_strlen($_POST['text'], 'UTF-8') > $maxLength || strlen($_POST['text']) > $maxBytes) { $error = "文字数またはバイト数がオーバーしています。"; }

配列から特定の要素を削除する

<?php $array = ['apple', 'banana', 'cherry']; // 'banana'を削除したい $key = array_search('banana', $array); // インデックスを取得 if ($key !== false) { unset($array[$key]); // 配列から削除 $array = array_values($array); // インデックスを再構築 } print_r($array); ?>

フォーム毎トークン

function setToken($formName) { // トークンを生成 $csrf_token = bin2hex(random_bytes(32)); // $_SESSION['csrf_token']が配列であることを確認 if (!isset($_SESSION['csrf_token']) || !is_array($_SESSION['csrf_token'])) { $_SESSION['csrf_token'] = array(); } $_SESSION['csrf_token'][$formName] = $csrf_token; return $csrf_token; }


<input type="hidden" name="form_name" value="entry-form"> <input type="hidden" name="csrf_token" value="<?php echo h(setToken('entry-form')); ?>">


session_start(); if ($_SERVER['REQUEST_METHOD'] == 'POST') { $formName = $_POST['form_name']; if (!isset($_POST['csrf_token']) || $_POST['csrf_token'] !== $_SESSION['csrf_token'][$formName]) { exit('不正なリクエスト'); } unset($_SESSION['csrf_token'][$formName]); unset($_POST['form_name']); }