PNGライブラリの延長

This commit is contained in:
2026-01-04 03:14:16 +09:00
parent 105cbdec7d
commit 6582737554
3 changed files with 480 additions and 38 deletions

View File

@@ -17,7 +17,7 @@ class RGB {
$this->r = $r;
$this->g = $g;
$this->b = $b;
$this->rgb = [$r, $g, $b];
$this->rgb = ['r' => $r, 'g' => $g, 'b' => $b];
}
}

View File

@@ -4,58 +4,499 @@ namespace Site\Lib\Image;
use Site\Lib\Image\ImageInterface;
class Png implements ImageInterface {
public int $width = 0;
public int $height = 0;
public int $bpp = 0;
public int $bitDepth = 0;
public int $numColors = 0;
public int $colorType = 0;
public string $compression = 'NONE';
public int $filter = 0;
public int $interlace = 0;
public bool $hasAlpha = false;
public string $imageData = '';
public \stdClass $IHDR; // 画像ヘッダー
// 横4バイト
// 丈4バイト
// ビット深さ1バイト
// 色類1バイト
//// | PNG画像タイプ | 色類 | 許可したビット深さ | 解釈 |
//// |--------------------------|------|--------------------|----------------------------------------------- |
//// | グレースケール | 0 | 1,2,4,8,16 | 各ピクセルはグレースケールのサンプル |
//// | トゥルーカラー | 2 | 8,16 | 各ピクセルはR,G,Bトリプル |
//// | 指標した色 | 3 | 1,2,4,8 | 各ピクセルはパレット指標PLTEチャンクあり |
//// | グレースケール+アルファ | 4 | 8,16 | 各ピクセルはグレースケール+アルファのサンプル |
//// | トゥルーカラー+アルファ | 6 | 8,16 | 各ピクセルはR,G,B,Aトリプル |
// 圧縮関数1バイト
//// 0 (DEFLATE)しかない
// フィルター関数1バイト
//// 0アダプティブ・フィルタリングしかない
// インタレース関数1バイト
//// 0 = なし
//// 1 = Adam7
public \stdClass $PLTE; // パレット(赤、緑、青)
// 色類3 = 必須
// 色類2と6 = 任意
public \stdClass $IDAT; // 画像データ
public \stdClass $IEND; // 画像終了
public \stdClass $tRNS; // 透明情報
// 色類0 = グレーサンプル値2バイト
// 色類2 = RGBサンプル値2x2x2バイト
// 色類3 = パレット指標のアルファ各1バイト
public \stdClass $cHRM; // 主な色度とホワイトポイント
// 白X4バイト
// 白Y4バイト
// 赤X4バイト
// 赤Y4バイト
// 緑X4バイト
// 緑Y4バイト
// 青X4バイト
// 青Y4バイト
public \stdClass $gAMA; // ガンマ4バイト
public \stdClass $iCCP; // 組み込みしたICCプロファイル
// プロファイル名179バイト
// NULL分離子1バイト
// 圧縮関数1バイト
// 圧縮プロファイルnバイト
public \stdClass $sBIT; // 上位ビット4バイト
// 色類0 = グレーサンプルビット1バイト
// 色類2, 3 = RGBサンプルビット1x1x1バイト
// 色類4 = グレーサンプルビット1x1バイト
// 色類6 = RGBサンプルビット1x1x1x1バイト
public \stdClass $sRGB; // レンダリングインテント1バイト
// 0 = 知覚
// 1 = 相対カラー・メトリック
// 2 = 彩度
// 3 = 絶対カラー・メトリック
public \stdClass $cICP; // 1バイト
public \stdClass $mDCV; // 1バイト
public \stdClass $cLLI; // 1バイト
public \stdClass $tEXt; // 文字データ
public \stdClass $iTXt; // 圧縮した文字データ
public \stdClass $zTXt; // 国際文字データ
public \stdClass $bKGD; // 拝啓色
public \stdClass $hIST; // 画像ヒストグラム
public \stdClass $pHYs; // 物理的なぷくセルのサイズ
// 単位当たりのピクセル数PPUのX軸4バイト
// 単位当たりのピクセル数PPUのY軸4バイト
// ユニット指定子1バイト
//// 0 = 不明
//// 1 = メートル
public \stdClass $sPLT; // 提案されている配色パターン
// パレット名179バイト
// NULL分離子1バイト
// 赤1か2バイト
// 緑1か2バイト
// 青1か2バイト
// アルファ1か2バイト
// 周波数2バイト
// 等
public \stdClass $eXIf; // Exifプロファイル
public \stdClass $tIME; // 画像の最後変更日
// 年2バイト
// 月1バイト1-12
// 日1バイト1-31
// 時1バイト0-23
// 分1バイト0-59
// 秒1バイト0-60
public \stdClass $acTL; // アニメーション・コントロール
// フレーム数4バイト
// 再生数4バイト、0 = 無限)
public array $frames = [];
private ?array $currentFrame = null;
// TODO: 残り: sBIT, cICP, mDCV, cLLI, iTXt, hIST, sPLT, tIME
public function parse(string $file): \stdClass {
$png = new \stdClass();
$fp = fopen($file, 'rb');
fread($fp, 8); // マジックのスキップ
unpack('N', fread($fp, 4))[1];
$type = fread($fp, 4);
if ($type !== 'IHDR') {
$magic = bin2hex(fread($fp, 8)); // マジックのスキップ
$length = unpack('N', fread($fp, 4))[1];
$header = fread($fp, 4);
if ($header !== 'IHDR') {
fclose($fp);
return $png;
}
// IHDRデータ
$data = fread($fp, 13);
$data = fread($fp, $length);
// N = ビッグエンディアン4ビット
$this->width = unpack('N', substr($data, 0, 4))[1];
$this->height = unpack('N', substr($data, 4, 4))[1];
$this->bitDepth = ord($data[8]);
$this->numColors = 1 << $this->bitDepth;
$this->colorType = ord($data[9]);
$this->compression = 'DEFLATE';
$this->filter = ord($data[11]);
$this->interlace = ord($data[12]); // 0 = なし, 1 = Adam7
$this->bpp = $this->bitDepth * $this->getSamplesPerPixel($this->colorType);
$this->hasAlpha = ($this->colorType === 4 || $this->colorType === 6);
$this->IHDR = new \stdClass;
$this->IHDR->width = unpack('N', substr($data, 0, 4))[1];
$this->IHDR->height = unpack('N', substr($data, 4, 4))[1];
$this->IHDR->bitDepth = ord($data[8]); // 1, 2, 4, 8, 16
$this->IHDR->numColors = 1 << $this->IHDR->bitDepth;
$this->IHDR->colorType = ord($data[9]); // 0, 2, 3, 4, 6
$this->IHDR->compressionType = ord($data[10]);
$this->IHDR->compression = 'DEFLATE';
$this->IHDR->filter = ord($data[11]);
$this->IHDR->interlace = ord($data[12]); // 0 = なし, 1 = Adam7
$this->IHDR->bpp = $this->IHDR->bitDepth * $this->getSamplesPerPixel($this->IHDR->colorType);
$this->IHDR->hasAlpha = ($this->IHDR->colorType === 4 || $this->IHDR->colorType === 6);
$crc = fread($fp, 4);
$this->IHDR->crc = $crc;
$this->IHDR->crc32 = unpack('N', $crc)[1];
$this->IHDR->crcHex = bin2hex($crc);
$png->width = $this->width;
$png->height = $this->height;
$png->bitDepth = $this->bitDepth;
$png->numColors = $this->numColors;
$png->colorType = $this->colorType;
$png->compression = $this->compression;
$png->filter = $this->filter;
$png->interlace = $this->interlace;
$png->bpp = $this->bpp;
$png->hasAlpha = $this->hasAlpha;
$png->header = $this->IHDR;
next_chunk:
$lengthBytes = fread($fp, 4);
if (strlen($lengthBytes) < 4) {
logger(LogType::Image, 'バイト長さが悪い。ファイル:'.$file.'、バイト長さ:'.'$lengthBytes');
fclose($fp);
return $png;
}
$length = unpack('N', $lengthBytes)[1];
$nextChunk = fread($fp, 4);
$data = $length > 0 ? fread($fp, $length) : '';
$crc = fread($fp, 4);
// PLTEデータ
if ($nextChunk === "\x50\x4c\x54\x45") { // PLTE
$palette = [];
for ($i = 0; $i < $length; $i += 3) {
$colors = new \Site\Lib\RGB(ord($data[$i]), ord($data[$i+1]), ord($data[$i+2]));
$palette[] = $colors->rgb;
}
$this->PLTE = new \stdClass;
$this->PLTE->palette = $palette;
$this->PLTE->crc = $crc;
$this->PLTE->crc32 = unpack('N', $crc)[1];
$this->PLTE->crcHex = bin2hex($crc);
$png->palette = $this->PLTE;
goto next_chunk;
} else if ($nextChunk === "\x74\x52\x4e\x53") { // tRNS
$this->tRNS = new \stdClass;
$vals = [];
$bytes = $this->IHDR->colorType === 2 ? 3 : 1;
for ($i = 0; $i < $length; $i += $bytes) {
if ($this->IHDR->colorType === 2) {
$colors = new \Site\Lib\RGB(ord($data[$i]), ord($data[$i+1]), ord($data[$i+2]));
$vals[] = $colors->rgb;
} else {
$vals[] = ord($data[$i]);
}
}
$this->tRNS->values = $vals;
$this->tRNS->crc = $crc;
$this->tRNS->crc32 = unpack('N', $crc)[1];
$this->tRNS->crcHex = bin2hex($crc);
$png->transparency = $this->tRNS;
goto next_chunk;
} else if ($nextChunk === "\x63\x48\x52\x4d") { // cHRM
$this->cHRM = new \stdClass;
$this->cHRM->whitePointX = unpack('N', substr($data, 0, 4))[1];
$this->cHRM->whitePointY = unpack('N', substr($data, 4, 4))[1];
$this->cHRM->redX = unpack('N', substr($data, 8, 4))[1];
$this->cHRM->redY = unpack('N', substr($data, 12, 4))[1];
$this->cHRM->greenX = unpack('N', substr($data, 16, 4))[1];
$this->cHRM->greenY = unpack('N', substr($data, 20, 4))[1];
$this->cHRM->blueX = unpack('N', substr($data, 24, 4))[1];
$this->cHRM->blueY = unpack('N', substr($data, 28, 4))[1];
$this->cHRM->crc = $crc;
$this->cHRM->crc32 = unpack('N', $crc)[1];
$this->cHRM->crcHex = bin2hex($crc);
$png->chromaticities = $this->cHRM;
goto next_chunk;
} else if ($nextChunk === "\x67\x41\x4d\x41") { // gAMA
$this->gAMA = new \stdClass;
$this->gAMA->value = unpack('N', $data)[1];
$this->gAMA->crc = $crc;
$this->gAMA->crc32 = unpack('N', $crc)[1];
$this->gAMA->crcHex = bin2hex($crc);
$png->gamma = $this->gAMA;
goto next_chunk;
} else if ($nextChunk === "\x69\x43\x43\x50") { // iCCP
$this->iCCP = new \stdClass;
$nullPos = strpos($data, "\0");
if ($nullPos === false || $nullPos < 1 || $nullPos > 79) {
$this->iCCP->valid = false;
$this->iCCP->error = "Missing or invalid profile name terminator";
goto iccp_crc;
}
$this->iCCP->profileName = substr($data, 0, $nullPos);
$compressionMethod = ord($data[$nullPos + 1]);
$this->iCCP->compressionMethod = $compressionMethod;
if ($compressionMethod !== 0) {
$this->iCCP->valid = false;
$this->iCCP->error = "Unsupported compression method: {$compressionMethod}";
goto iccp_crc;
}
$compressedData = substr($data, $nullPos + 2);
$this->iCCP->compressedData = $compressedData;
$this->iCCP->compressedSize = strlen($compressedData);
$this->iCCP->valid = true;
iccp_crc:
$this->iCCP->crc = $crc;
$this->iCCP->crc32 = unpack('N', $crc)[1];
$this->iCCP->crcHex = bin2hex($crc);
$png->iccProfile = $this->iCCP;
goto next_chunk;
} else if ($nextChunk === "\x73\x42\x49\x54") { // sBIT
$this->sBIT = new \stdClass;
kys('sBIT');
$png->significantBits = $this->sBIT;
goto next_chunk;
} else if ($nextChunk === "\x73\x52\x47\x42") { // sRGB
$this->sRGB = new \stdClass;
$this->sRGB->intent = ord($data);
$name = [
0 => 'Perceptual',
1 => 'Relative colorimetric',
2 => 'Saturation',
3 => 'Absolute colorimetric',
];
$this->sRGB->intentName = $name[$this->sRGB->intent];
$this->sRGB->crc = $crc;
$this->sRGB->crc32 = unpack('N', $crc)[1];
$this->sRGB->crcHex = bin2hex($crc);
$png->rgbColorSpace = $this->sRGB;
goto next_chunk;
} else if ($nextChunk === "\x63\x49\x43\x50") { // cICP
$this->cICP = new \stdClass;
kys('cICP');
$png->videoSignalTypeId = $this->cICP;
goto next_chunk;
} else if ($nextChunk === "\x6d\x44\x43\x56") { // mDCV
$this->mDCV = new \stdClass;
kys('mDCV');
$png->masterDisplayColorVolume = $this->mDCV;
goto next_chunk;
} else if ($nextChunk === "\x63\x4c\x4c\x49") { // cLLI
$this->cLLI = new \stdClass;
kys('cLLI');
$png->contentLightLevelInfo = $this->cLLI;
goto next_chunk;
} else if ($nextChunk === "\x74\x45\x58\x74") { // tEXt
$this->tEXt = new \stdClass;
$this->tEXt->text = $data;
$this->tEXt->crc = $crc;
$this->tEXt->crc32 = unpack('N', $crc)[1];
$this->tEXt->crcHex = bin2hex($crc);
$png->textualData = $this->tEXt;
goto next_chunk;
} else if ($nextChunk === "\x69\x54\x58\x74") { // iTXt
$this->iTXt = new \stdClass;
kys('iTXt');
$png->internationalTextualData = $this->iTXt;
goto next_chunk;
} else if ($nextChunk === "\x7a\x54\x58\x74") { // zTXt
$this->zTXt = new \stdClass;
$nullPos = strpos($data, "\0");
if ($nullPos === false || $nullPos < 1 || $nullPos > 79) {
$this->zTXt->valid = false;
$this->zTXt->error = "Missing or invalid profile name terminator";
goto zTXt_crc;
}
$this->zTXt->keyword = rtrim(substr($data, 0, $nullPos));
$compressionMethod = ord($data[$nullPos + 1] ?? "\xFF");
$this->zTXt->compressionMethod = $compressionMethod;
if ($compressionMethod !== 0) {
$this->zTXt->valid = false;
$this->zTXt->error = "Unsupported compression method";
goto zTXt_crc;
}
$compressedText = substr($data, $nullPos + 2);
fread($fp, 13); // ????????????????????????
$this->zTXt->compressedText = $compressedText;
$this->zTXt->compressedLength = strlen($compressedText);
$text = zlib_decode($compressedText);
if ($text === false) {
$this->zTXt->valid = false;
$this->zTXt->decompressionError = true;
$this->zTXt->text = null;
goto zTXt_crc;
} else {
$this->zTXt->text = $text;
$this->zTXt->textUtf8 = mb_convert_encoding($text, 'UTF-8', 'ISO-8859-1');
}
$this->zTXt->valid = true;
zTXt_crc:
$this->zTXt->crc = $crc;
$this->zTXt->crc32 = unpack('N', $crc)[1];
$this->zTXt->crcHex = bin2hex($crc);
$png->compressedTextualData = $this->zTXt;
goto next_chunk;
} else if ($nextChunk === "\x62\x4b\x47\x44") { // bKGD
$this->bKGD = new \stdClass;
if ($this->IHDR->colorType === 2 || $this->IHDR->colorType === 6) {
$r = (ord($data[0]) << 8) | ord($data[1]);
$g = (ord($data[2]) << 8) | ord($data[3]);
$b = (ord($data[4]) << 8) | ord($data[5]);
$this->bKGD->type = 'rgb16';
$this->bKGD->r = $r;
$this->bKGD->g = $g;
$this->bKGD->b = $b;
$this->bKGD->r8 = $r >> 8;
$this->bKGD->g8 = $g >> 8;
$this->bKGD->b8 = $b >> 8;
$this->bKGD->webColor = sprintf("#%02x%02x%02x", $this->bKGD->r8, $this->bKGD->g8, $this->bKGD->b8);
} else if ($this->IHDR->colorType === 2 || $this->IHDR->colorType === 6) {
$gray = (ord($data[0]) << 8) | ord($data[1]);
$this->bKGD->type = 'gray16';
$this->bKGD->gray = $gray;
$this->bKGD->gray8 = $gray >> 8;
} else if ($this->IHDR->colorType === 3) {
$this->bKGD->type = 'palette_index';
$this->bKGD->value = ord($data);
} else {
$this->bKGD->type = 'unknown';
$this->bKGD->error = 'Invalid color type for bKGD';
}
$this->bKGD->crc = $crc;
$this->bKGD->crc32 = unpack('N', $crc)[1];
$this->bKGD->crcHex = bin2hex($crc);
$png->backgroundColor = $this->bKGD;
goto next_chunk;
} else if ($nextChunk === "\x68\x49\x53\x54") { // hIST
$this->hIST = new \stdClass;
kys('hIST');
$png->imageHistogram = $this->hIST;
goto next_chunk;
} else if ($nextChunk === "\x70\x48\x59\x73") { // pHYs
$this->pHYs = new \stdClass;
$this->pHYs->x = unpack('N', substr($data, 0, 4))[1];
$this->pHYs->y = unpack('N', substr($data, 4, 4))[1];
$unit = [
0 => 'unit is unknown',
1 => 'unit is the metre',
];
$this->pHYs->unitId = ord($data[8]);
$this->pHYs->unit = $unit[$this->pHYs->unitId];
$this->pHYs->crc = $crc;
$this->pHYs->crc32 = unpack('N', $crc)[1];
$this->pHYs->crcHex = bin2hex($crc);
$png->physicalPixelDimensions = $this->pHYs;
goto next_chunk;
} else if ($nextChunk === "\x73\x50\x4c\x54") { // sPLT
$this->sPLT = new \stdClass;
kys('sPLT');
$png->suggestedPalette = $this->sPLT;
goto next_chunk;
} else if ($nextChunk === "\x65\x58\x49\x66") { // eXIf
$this->eXIf = new \stdClass;
if (strlen($data) < 8) {
$this->eXIf->valid = false;
$this->eXIf->error = "eXIf chunk too short";
goto exif_crc;
}
$byteOrder = substr($data, 0, 2);
$magic = substr($data, 2, 2);
$this->eXIf->rawData = $data;
$this->eXIf->rawLength = $length;
$this->eXIf->byteOrder = $byteOrder; // II = リトルエンディアン、MM = ビッグエンディアン
$this->eXIf->isBigEndian = ($byteOrder === 'MM');
$this->eXIf->magic = unpack('n', $magic)[1]; // いつでも42ではず
if (strlen($data) < 8) {
$this->eXIf->valid = false;
$this->eXIf->error = "Invalid TIFF magic number";
goto exif_crc;
}
$this->eXIf->valid = true;
$ifdOffset = unpack($this->eXIf->isBigEndian ? 'N' : 'V', substr($data, 4, 4))[1];
$this->eXIf->firstIfdOffset = $ifdOffset;
exif_crc:
$this->eXIf->crc = $crc;
$this->eXIf->crc32 = unpack('N', $crc)[1];
$this->eXIf->crcHex = bin2hex($crc);
$png->exifProfile = $this->eXIf;
goto next_chunk;
} else if ($nextChunk === "\x74\x49\x4d\x45") { // tIME
$this->tIME = new \stdClass;
kys('tIME');
$png->lastModificationTime = $this->tIME;
goto next_chunk;
} else if ($nextChunk === "\x61\x63\x54\x4c") { // acTL
$this->acTL = new \stdClass;
$this->acTL->numFrames = unpack('N', substr($data, 0, 4))[1];
$this->acTL->numPlays = unpack('N', substr($data, 4, 4))[1];
$this->acTL->crc = $crc;
$this->acTL->crc32 = unpack('N', $crc)[1];
$this->acTL->crcHex = bin2hex($crc);
$png->aniationControl = $this->acTL;
goto next_chunk;
} else if ($nextChunk === "\x66\x63\x54\x4c") { // fcTL
if ($this->currentFrame !== null) $this->frames[] = (object)$this->currentFrame;
$this->currentFrame = [
'sequenceNumber' => unpack('N', substr($data, 0, 4))[1],
'width' => unpack('N', substr($data, 4, 4))[1],
'height' => unpack('N', substr($data, 8, 4))[1],
'xOffset' => unpack('N', substr($data, 12, 4))[1],
'yOffset' => unpack('N', substr($data, 16, 4))[1],
'delayNum' => unpack('n', substr($data, 20, 2))[1],
'delayDen' => unpack('n', substr($data, 22, 2))[1],
'disposeOp' => ord($data[24]),
'blendOp' => ord($data[25]),
'data' => null,
'crc' => $crc,
'crc32' => unpack('N', $crc)[1],
'crcHex' => bin2hex($crc),
];
goto next_chunk;
} else if ($nextChunk === "\x49\x44\x41\x54" || $nextChunk === "\x66\x64\x41\x54") { // IDAT || fdAT
if ($this->currentFrame === null) {
$this->currentFrame = [
'sequenceNumber' => 0,
'isFirstFrame' => true,
'width' => $this->IHDR->width,
'height' => $this->IHDR->height,
'data' => null,
];
}
$compressedData = ($nextChunk === "\x66\x64\x41\x54") ? substr($data, 4) : $data;
$this->currentFrame['data'] = $compressedData;
$this->currentFrame['dataBase64'] = base64_encode($compressedData);
$this->currentFrame['dataHex'] = bin2hex($compressedData);
$this->currentFrame['dataLength'] = strlen($compressedData);
$this->currentFrame['dataChunkType'] = $nextChunk;
$this->currentFrame['crc'] = $crc;
$this->currentFrame['crcBase64'] = unpack('N', $crc)[1];
$this->currentFrame['crcHex'] = bin2hex($crc);
if ($nextChunk === "\x49\x44\x41\x54") {
$this->frames[] = (object)$this->currentFrame;
$this->currentFrame = null;
}
$png->data = $this->frames;
goto next_chunk;
} else if ($nextChunk === "\x49\x45\x4e\x44") { // IEND
if ($this->currentFrame !== null) {
$this->frames[] = (object)$this->currentFrame;
}
$png->end = (object)['crc' => $crc, 'crc32' => unpack('N', $crc)[1], 'crcHex' => bin2hex($crc)];
} else {
fclose($fp);
$err = '不明なチャンク:'.$nextChunk.'、HEX'.bin2hex($nextChunk);
kys($err);
logger(\LogType::Image, '【PNG】'.$err);
return $png;
}
fclose($fp);
return $png;
}

View File

@@ -5,6 +5,7 @@ enum LogType {
case ActivityPub;
case Auth;
case Mailer;
case Image;
}
class Result {