r = $r; $this->g = $g; $this->b = $b; $this->rgb = ['r' => $r, 'g' => $g, 'b' => $b]; } } class Image { public string $filename = 'UNKNOWN'; public string $extension = 'UNKNOWN'; public string $type = 'UNSUPPORTED'; public string $size = '0 B'; public int $bytes = 0; public \stdClass $fullInfo; private const SUPPORTED = [ 'image/bmp' => Bmp::class, 'image/gif' => Gif::class, 'image/jpeg' => Jpeg::class, 'image/png' => Png::class, 'image/webp' => Webp::class, 'image/x-tga' => Targa::class, ]; public function __construct(string $file) { // ファイルが無くてもfullInfoを読める様に(以前は未初期化のままで、記事のサムネイルが無いとページが落ちた) $this->fullInfo = (object)['type' => $this->type, 'size' => $this->size, 'bytes' => 0, 'width' => 0, 'height' => 0]; $file = ROOT.$file; // $file = ROOT.'/public/static/article/mock-screenshot.tga'; if (!file_exists($file)) return; $fp = fopen($file, 'rb'); if (!$fp) return; $extBytes = (string)fread($fp, 32); fclose($fp); $this->bytes = filesize($file); $this->size = $this->formatBytes($this->bytes); $this->extension = pathinfo($file, PATHINFO_EXTENSION); $this->filename = str_replace('.'.$this->extension, '', array_last(explode('/', $file))); $this->type = self::detect($extBytes, $file) ?? 'UNSUPPORTED'; $this->fullInfo = self::handler($this->type)->parse($file); $this->fullInfo->type = $this->type; $this->fullInfo->size = $this->size; $this->fullInfo->bytes = $this->bytes; if (!isset($this->fullInfo->width) && isset($this->fullInfo->header->width)) { $this->fullInfo->width = $this->fullInfo->header->width; $this->fullInfo->height = $this->fullInfo->header->height; } } /** * ファイルの先頭から形式を判断する(TGAは印が無いので、ファイル名が「.tga」の場合だけ) * * @param string $head ファイルの先頭(32バイト以上) * @param string $name ファイル名(TGA用、任意) * @return string|null MIME、分からなければnull */ public static function detect(string $head, string $name = ''): ?string { if (str_starts_with($head, "\xff\xd8\xff")) return 'image/jpeg'; if (str_starts_with($head, "\x89PNG\r\n\x1a\n")) return 'image/png'; if (str_starts_with($head, 'GIF87a') || str_starts_with($head, 'GIF89a')) return 'image/gif'; if (str_starts_with($head, 'RIFF') && substr($head, 8, 4) === 'WEBP') return 'image/webp'; // BMP:「BM」と、知っているヘッダーの大きさ if (str_starts_with($head, 'BM') && strlen($head) >= 18 && in_array(unpack('V', substr($head, 14, 4))[1], [12, 16, 40, 52, 56, 64, 108, 124], true)) return 'image/bmp'; if (str_ends_with(strtolower($name), '.tga')) return 'image/x-tga'; return null; } /** * ファイルの中身を画素にする * * @param string $data ファイルの中身 * @param string $name ファイル名(TGA用、任意) * @return Bitmap * @throws \RuntimeException 未対応の形式又は壊れたファイル */ public static function decode(string $data, string $name = ''): Bitmap { return self::handler(self::detect($data, $name) ?? '')->decode($data); } /** * 画素をファイルの中身にする * * @param string $type MIME(image/png等) * @param array $opt 形式毎の設定(PNG:level、JPEG:quality等) */ public static function encode(Bitmap $img, string $type, array $opt = []): string { return self::handler($type)->encode($img, $opt); } /////////////// private static function handler(string $type): ImageInterface { $class = self::SUPPORTED[$type] ?? null; if (!$class) throw new \RuntimeException("形式のハンドルが存在しない: {$type}"); return new $class(); } private function formatBytes(int $size, int $precision = 2): string { $units = ['B', 'KiB', 'MiB', 'GiB', 'TiB']; for ($i = 0; $size > 1024 && $i < count($units) - 1; ++$i) $size /= 1024; return round($size, $precision).' '.$units[$i]; } }