diff --git a/src/Site/Lib/Image.php b/src/Site/Lib/Image.php index 8961640..27961c6 100644 --- a/src/Site/Lib/Image.php +++ b/src/Site/Lib/Image.php @@ -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]; } } diff --git a/src/Site/Lib/Image/Png.php b/src/Site/Lib/Image/Png.php index c5e023d..a8e16da 100644 --- a/src/Site/Lib/Image/Png.php +++ b/src/Site/Lib/Image/Png.php @@ -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; // 主な色度とホワイトポイント + // 白X(4バイト) + // 白Y(4バイト) + // 赤X(4バイト) + // 赤Y(4バイト) + // 緑X(4バイト) + // 緑Y(4バイト) + // 青X(4バイト) + // 青Y(4バイト) + public \stdClass $gAMA; // ガンマ(4バイト) + public \stdClass $iCCP; // 組み込みしたICCプロファイル + // プロファイル名(1~79バイト) + // 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; // 提案されている配色パターン + // パレット名(1~79バイト) + // 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; } diff --git a/util.php b/util.php index 547fcaf..70b0a94 100644 --- a/util.php +++ b/util.php @@ -5,6 +5,7 @@ enum LogType { case ActivityPub; case Auth; case Mailer; + case Image; } class Result {