画像ライブラリーはクラス化、TGA対応の追加
諏訪子 <suwako@076.moe>、2025-12-31 10:37
コミット 105cbdec7de32b56df6b33e8eec017779ba4ce60
・親 9a5d5db
・ファイルを見る
11個のファイル、+382 -211
| @@ -30,3 +30,4 @@ | ||
| 30 | 30 | define('AUTH_ENABLED', false); |
| 31 | 31 | define('AUTH_REGISTER_ENABLED', false); |
| 32 | 32 | define('COPYRIGHT_YEAR', '2018-'.date('Y')); |
| 33 | +define('DEBUG_MODE', false); |
変更
newpost.php
| @@ -2,7 +2,8 @@ | ||
| 2 | 2 | if (!isset($argv[1])) die('usage: php newpost.php [slug]'); |
| 3 | 3 | if (file_exists("blog/{$argv[1]}.md")) die("エラー: ファイル「blog/{$argv[1]}.md」は既に存在します。\n"); |
| 4 | 4 | |
| 5 | -define('CURL_ENABLED', false); // 黙れ・・・ | |
| 5 | +define('ROOT', realpath(__DIR__)); | |
| 6 | +require_once ROOT.'/config/config.php'; | |
| 6 | 7 | include('util.php'); |
| 7 | 8 | $post = fopen("blog/{$argv[1]}.md", "w"); |
| 8 | 9 | fwrite($post, "title: 【】\n"); |
| @@ -14,4 +15,3 @@ | ||
| 14 | 15 | fwrite($post, "category: \n"); |
| 15 | 16 | fwrite($post, "----\n"); |
| 16 | 17 | fclose($post); |
| 17 | -?> |
変更
route.php
| @@ -1,6 +1,4 @@ | ||
| 1 | 1 | <?php |
| 2 | -error_reporting(E_ALL); | |
| 3 | -ini_set('display_errors', 1); | |
| 4 | 2 | |
| 5 | 3 | require_once __DIR__.DIRECTORY_SEPARATOR.'/autoload.php'; |
| 6 | 4 | |
| @@ -16,7 +14,12 @@ | ||
| 16 | 14 | define('ROOT', realpath(__DIR__)); |
| 17 | 15 | |
| 18 | 16 | include(ROOT.'/config/config.php'); |
| 17 | +if (DEBUG_MODE) { | |
| 18 | + error_reporting(E_ALL); | |
| 19 | + ini_set('display_errors', 1); | |
| 20 | +} | |
| 19 | 21 | include(ROOT.'/util.php'); |
| 22 | + | |
| 20 | 23 | |
| 21 | 24 | $routes = [ |
| 22 | 25 | Route::add('GET', 'blog/{page}', Home::class.'@article'), |
| @@ -1,6 +1,12 @@ | ||
| 1 | 1 | <?php |
| 2 | 2 | namespace Site\Lib; |
| 3 | 3 | |
| 4 | +use Site\Lib\Image\ImageInterface; | |
| 5 | +use Site\Lib\Image\Gif; | |
| 6 | +use Site\Lib\Image\Jpeg; | |
| 7 | +use Site\Lib\Image\Png; | |
| 8 | +use Site\Lib\Image\Targa; | |
| 9 | + | |
| 4 | 10 | class RGB { |
| 5 | 11 | public int $r; |
| 6 | 12 | public int $g; |
| @@ -19,236 +25,55 @@ | ||
| 19 | 25 | public string $filename = 'UNKNOWN'; |
| 20 | 26 | public string $extension = 'UNKNOWN'; |
| 21 | 27 | public string $type = 'UNSUPPORTED'; |
| 22 | - public int $width = 0; | |
| 23 | - public int $height = 0; | |
| 24 | - public int $bpp = 0; | |
| 25 | - public int $bitDepth = 0; | |
| 26 | - public int $numColors = 0; // GIF, PNG | |
| 27 | - public int $colorType = 0; // PNG | |
| 28 | - public string $compression = 'NONE'; // GIF, PNG | |
| 29 | - public int $filter = 0; // PNG | |
| 30 | - public int $interlace = 0; // GIF, PNG | |
| 31 | - public bool $hasAlpha = false; | |
| 32 | - public int $bgColor = 0; // GIF | |
| 33 | - public float $delayTime = 0; // GIF | |
| 34 | - public array $globalColorTable = []; // GIF | |
| 35 | - public int $numTransparentPixels = 0; // GIF | |
| 36 | - public int $lzwMinCodeSize = 0; // GIF | |
| 37 | - public string $imageData = ''; | |
| 38 | 28 | public string $size = '0 B'; |
| 39 | 29 | public int $bytes = 0; |
| 40 | 30 | public \stdClass $fullInfo; |
| 41 | 31 | |
| 32 | + private array $supported = [ | |
| 33 | + 'image/gif' => Gif::class, | |
| 34 | + 'image/jpeg' => Jpeg::class, | |
| 35 | + 'image/png' => Png::class, | |
| 36 | + 'image/x-tga' => Targa::class, | |
| 37 | + ]; | |
| 38 | + | |
| 42 | 39 | public function __construct(string $file) { |
| 43 | 40 | $file = ROOT.$file; |
| 44 | - // $file = ROOT.'/public/static/article/mock-screenshot.jpg'; | |
| 41 | + // $file = ROOT.'/public/static/article/mock-screenshot.tga'; | |
| 45 | 42 | if (!file_exists($file)) return; |
| 46 | 43 | $fp = fopen($file, 'rb'); |
| 47 | 44 | if (!$fp) return; |
| 48 | - | |
| 49 | 45 | $extBytes = fread($fp, 15); |
| 50 | 46 | fclose($fp); |
| 47 | + | |
| 51 | 48 | $this->bytes = filesize($file); |
| 52 | 49 | $this->size = $this->formatBytes($this->bytes); |
| 53 | 50 | $this->extension = pathinfo($file, PATHINFO_EXTENSION); |
| 54 | 51 | $this->filename = str_replace('.'.$this->extension, '', array_last(explode('/', $file))); |
| 55 | 52 | |
| 56 | - if ($extBytes === false) return; | |
| 57 | - else if (substr($extBytes, 0, 3) === "\xff\xd8\xff") { | |
| 58 | - $this->type = 'image/jpeg'; | |
| 59 | - $this->parseJPEG($file); | |
| 60 | - } else if (substr($extBytes, 0, 15) === "\x52\x49\x46\x46\x1a\x28\x00\x00\x57\x45\x42\x50\x56\x50\x38") { | |
| 61 | - $this->type = 'image/webp'; | |
| 62 | - // $this->parseWEBP($file); | |
| 63 | - } else if (substr($extBytes, 0, 3) === "\x42\x4d\x36") { | |
| 64 | - $this->type = 'image/bmp'; | |
| 65 | - // $this->parseBMP($file); | |
| 66 | - } else if (substr($extBytes, 0, 6) === "\x89\x50\x4e\x47\x0d\x0a") { | |
| 67 | - $this->type = 'image/png'; | |
| 68 | - $this->fullInfo = $this->parsePNG($file); | |
| 69 | - } else if (substr($extBytes, 0, 6) === "\x47\x49\x46\x38\x37\x61" || substr($extBytes, 0, 6) === "\x47\x49\x46\x38\x39\x61") { | |
| 70 | - $this->type = 'image/gif'; | |
| 71 | - $this->fullInfo = $this->parseGIF($file); | |
| 72 | - } else if (str_ends_with($file, '.tga')) { | |
| 73 | - $this->type = 'image/tga'; | |
| 74 | - // $this->parseTGA($file); | |
| 75 | - } | |
| 53 | + if (substr($extBytes, 0, 3) === "\xff\xd8\xff") $this->type = 'image/jpeg'; | |
| 54 | + else if (substr($extBytes, 0, 15) === "\x52\x49\x46\x46\x1a\x28\x00\x00\x57\x45\x42\x50\x56\x50\x38") $this->type = 'image/webp'; | |
| 55 | + else if (substr($extBytes, 0, 3) === "\x42\x4d\x36") $this->type = 'image/bmp'; | |
| 56 | + else if (substr($extBytes, 0, 6) === "\x89\x50\x4e\x47\x0d\x0a") $this->type = 'image/png'; | |
| 57 | + else if (substr($extBytes, 0, 6) === "\x47\x49\x46\x38\x37\x61" || substr($extBytes, 0, 6) === "\x47\x49\x46\x38\x39\x61") $this->type = 'image/gif'; | |
| 58 | + else if (str_ends_with($file, '.tga')) $this->type = 'image/x-tga'; | |
| 59 | + | |
| 60 | + $this->fullInfo = $this->createHandler($this->type)->parse($file); | |
| 61 | + $this->fullInfo->type = $this->type; | |
| 62 | + $this->fullInfo->size = $this->size; | |
| 63 | + $this->fullInfo->bytes = $this->bytes; | |
| 76 | 64 | } |
| 77 | 65 | |
| 78 | - private function parseJPEG(string $file): \stdClass { | |
| 79 | - $jpg = new \stdClass; | |
| 80 | - $fp = fopen($file, 'rb'); | |
| 66 | + /////////////// | |
| 81 | 67 | |
| 82 | - $i = 0; | |
| 83 | - while (!feof($fp)) { | |
| 84 | - $byte = fread($fp, 1); | |
| 85 | - if ($byte !== "\xFF") continue; | |
| 86 | - | |
| 87 | - $marker = ord(fread($fp, 1)); | |
| 88 | - if ($marker >= 0xC0 && $marker <= 0xC3) { // 0xC0 = SOF0, 0xC1 = SOF1, 0xC2 = SOF2, 0xC3 = SOF3 | |
| 89 | - fread($fp, 2); | |
| 90 | - $this->bpp = ord(fread($fp, 1)); | |
| 91 | - // n = ビッグエンディアン(2ビット) | |
| 92 | - $this->height = unpack('n', fread($fp, 2))[1]; | |
| 93 | - $this->width = unpack('n', fread($fp, 2))[1]; | |
| 94 | - $jpg->width = $this->width; | |
| 95 | - $jpg->height = $this->height; | |
| 96 | - $jpg->bpp = $this->bpp; | |
| 97 | - } else if ($marker == 0xC4) { // DHT (Huffmanテーブル(複数可)) | |
| 98 | - // | |
| 99 | - } else if ($marker == 0xDB) { // DQT (量子化テーブル(複数可)) | |
| 100 | - // | |
| 101 | - } else if ($marker == 0xDD) { // DRI (リセット期間) | |
| 102 | - } else if ($marker == 0xDA) { // SOS(画像開始) | |
| 103 | - } else if ($marker >= 0xD0 && $marker <= 0xD7) { // RST | |
| 104 | - } else if ($marker >= 0xE1 && $marker <= 0xEF) { // APP | |
| 105 | - } else if ($marker == 0xFE) { // COM | |
| 106 | - } else if ($marker == 0xD9) { // EOI(画像終了) | |
| 107 | - break; | |
| 108 | - } | |
| 109 | - } | |
| 110 | - | |
| 111 | - // kys($jpg); | |
| 112 | - | |
| 113 | - $i++; | |
| 114 | - fclose($fp); | |
| 115 | - return $jpg; | |
| 68 | + private function createHandler(string $ext): ImageInterface { | |
| 69 | + $class = $this->supported[$ext] ?? null; | |
| 70 | + if (!$class) throw new \RuntimeException("形式のハンドルが存在しない: {$ext}"); | |
| 71 | + return new $class(); | |
| 116 | 72 | } |
| 117 | 73 | |
| 118 | - /** | |
| 119 | - * TODO: 現在、IHDRヘッダーのみを受け取る | |
| 120 | - */ | |
| 121 | - private function parsePNG(string $file): \stdClass { | |
| 122 | - $png = new \stdClass(); | |
| 123 | - $fp = fopen($file, 'rb'); | |
| 124 | - | |
| 125 | - fread($fp, 8); // マジックのスキップ | |
| 126 | - unpack('N', fread($fp, 4))[1]; | |
| 127 | - $type = fread($fp, 4); | |
| 128 | - if ($type !== 'IHDR') { | |
| 129 | - fclose($fp); | |
| 130 | - return $png; | |
| 131 | - } | |
| 132 | - | |
| 133 | - // IHDRデータ | |
| 134 | - $data = fread($fp, 13); | |
| 135 | - | |
| 136 | - // N = ビッグエンディアン(4ビット) | |
| 137 | - $this->width = unpack('N', substr($data, 0, 4))[1]; | |
| 138 | - $this->height = unpack('N', substr($data, 4, 4))[1]; | |
| 139 | - $this->bitDepth = ord($data[8]); | |
| 140 | - $this->numColors = 1 << $this->bitDepth; | |
| 141 | - $this->colorType = ord($data[9]); | |
| 142 | - $this->compression = 'DEFLATE'; | |
| 143 | - $this->filter = ord($data[11]); | |
| 144 | - $this->interlace = ord($data[12]); // 0 = なし, 1 = Adam7 | |
| 145 | - $this->bpp = $this->bitDepth * $this->getSamplesPerPixel($this->colorType); | |
| 146 | - $this->delayTime = 0; | |
| 147 | - $this->hasAlpha = ($this->colorType === 4 || $this->colorType === 6); | |
| 148 | - | |
| 149 | - $png->width = $this->width; | |
| 150 | - $png->height = $this->height; | |
| 151 | - $png->bitDepth = $this->bitDepth; | |
| 152 | - $png->numColors = $this->numColors; | |
| 153 | - $png->colorType = $this->colorType; | |
| 154 | - $png->compression = $this->compression; | |
| 155 | - $png->filter = $this->filter; | |
| 156 | - $png->interlace = $this->interlace; | |
| 157 | - $png->bpp = $this->bpp; | |
| 158 | - $png->delayTime = $this->delayTime; | |
| 159 | - $png->hasAlpha = $this->hasAlpha; | |
| 160 | - | |
| 161 | - fclose($fp); | |
| 162 | - | |
| 163 | - return $png; | |
| 164 | - } | |
| 165 | - | |
| 166 | - /** | |
| 167 | - * TODO: 現在、インタレースした画像が未対応です。 | |
| 168 | - */ | |
| 169 | - private function parseGIF(string $file): \stdClass { | |
| 170 | - $gif = new \stdClass(); | |
| 171 | - $fp = fopen($file, 'rb'); | |
| 172 | - $magic = fread($fp, 6); // マジックのスキップ | |
| 173 | - | |
| 174 | - // v = リトルエンディアン(2ビット) | |
| 175 | - $this->width = unpack('v', fread($fp, 2))[1]; | |
| 176 | - $this->height = unpack('v', fread($fp, 2))[1]; | |
| 177 | - $packed = ord(fread($fp, 1)); | |
| 178 | - $this->bitDepth = ($packed & 0x07) + 1; | |
| 179 | - $this->numColors = 1 << $this->bitDepth; | |
| 180 | - $hasGct = $this->bitDepth === 8; | |
| 181 | - | |
| 182 | - $gif->width = $this->width; | |
| 183 | - $gif->height = $this->height; | |
| 184 | - $gif->bitDepth = $this->bitDepth; | |
| 185 | - $gif->numColors = $this->numColors; | |
| 186 | - if ($magic === 'GIF87a' || !$hasGct) return $gif; // それ以外情報がない | |
| 187 | - | |
| 188 | - $this->bgColor = (int)bin2hex(fread($fp, 1)); | |
| 189 | - $gif->bgColor = $this->bgColor; | |
| 190 | - bin2hex(fread($fp, 1)); // いつでも 0:0? | |
| 191 | - | |
| 192 | - $gceLen = 0; | |
| 193 | - while (!feof($fp)) { | |
| 194 | - $skip = bin2hex(fread($fp, 3)); | |
| 195 | - $color = new RGB((int)($skip[0].$skip[1]), (int)($skip[2].$skip[3]), (int)($skip[4].$skip[5])); | |
| 196 | - $this->globalColorTable[] = $color->rgb; | |
| 197 | - if ($skip[0].$skip[1] === '21' && $skip[2].$skip[3] === 'f9') { | |
| 198 | - $gceLen = (int)($skip[4].$skip[5]); | |
| 199 | - break; | |
| 200 | - } | |
| 201 | - } | |
| 202 | - | |
| 203 | - // GCE開始 | |
| 204 | - $this->hasAlpha = (ord(fread($fp, 1)) & 0x01) === 0x01; | |
| 205 | - $this->delayTime = (unpack('v', fread($fp, 2))[1]) / 100.0; | |
| 206 | - $this->numTransparentPixels = ord(fread($fp, 1)); | |
| 207 | - $gif->hasAlpha = $this->hasAlpha; | |
| 208 | - $gif->delayTime = $this->delayTime; | |
| 209 | - $gif->numTransparentPixels = $this->numTransparentPixels; | |
| 210 | - fread($fp, 1); // GCE終了 | |
| 211 | - fread($fp, 1); // 画像指定子開始 | |
| 212 | - fread($fp, 4); // 画面のXとY、いつでも(0, 0) | |
| 213 | - fread($fp, 4); // 画像のサイズ、関数の開始で既に保存した | |
| 214 | - $pack = ord(fread($fp, 1)); | |
| 215 | - $this->interlace = ($pack & 0x40); // 画像のサイズ、関数の開始で既に保存した | |
| 216 | - | |
| 217 | - $this->lzwMinCodeSize = ord(fread($fp, 1)); // 画像データ開始 | |
| 218 | - $gif->lzwMinCodeSize = $this->lzwMinCodeSize; | |
| 219 | - $imageData = ''; | |
| 220 | - while (true) { | |
| 221 | - $blockSize = ord(fread($fp, 1)); | |
| 222 | - if ($blockSize === 0) break; | |
| 223 | - $imageData .= fread($fp, 1); | |
| 224 | - } | |
| 225 | - | |
| 226 | - $this->imageData = base64_encode($imageData); | |
| 227 | - $this->colorType = 3; | |
| 228 | - $this->compression = 'LZW'; | |
| 229 | - $gif->colorType = $this->colorType; | |
| 230 | - $gif->compression = $this->compression; | |
| 231 | - $gif->globalColorTable = $this->globalColorTable; | |
| 232 | - $gif->imageData = $this->imageData; | |
| 233 | - | |
| 234 | - fclose($fp); | |
| 235 | - return $gif; | |
| 236 | - } | |
| 237 | - | |
| 238 | 74 | private function formatBytes(int $size, int $precision = 2): string { |
| 239 | 75 | $units = ['B', 'KiB', 'MiB', 'GiB', 'TiB']; |
| 240 | 76 | for ($i = 0; $size > 1024 && $i < count($units) - 1; ++$i) $size /= 1024; |
| 241 | 77 | return round($size, $precision).' '.$units[$i]; |
| 242 | - } | |
| 243 | - | |
| 244 | - private function getSamplesPerPixel(int $colorType): int { | |
| 245 | - return match($colorType) { | |
| 246 | - 0 => 1, // グレースケール | |
| 247 | - 2 => 3, // RGB | |
| 248 | - 3 => 1, // 指標した色(ペレット) | |
| 249 | - 4 => 2, // グレースケール+アルファ | |
| 250 | - 6 => 4, // RGBA | |
| 251 | - default => 1, | |
| 252 | - }; | |
| 253 | 78 | } |
| 254 | 79 | } |
| @@ -0,0 +1,91 @@ | ||
| 1 | +<?php | |
| 2 | +namespace Site\Lib\Image; | |
| 3 | + | |
| 4 | +use Site\Lib\Image\ImageInterface; | |
| 5 | + | |
| 6 | +class Gif implements ImageInterface { | |
| 7 | + public int $width = 0; | |
| 8 | + public int $height = 0; | |
| 9 | + public int $bitDepth = 0; | |
| 10 | + public int $numColors = 0; | |
| 11 | + public string $compression = 'NONE'; | |
| 12 | + public int $interlace = 0; | |
| 13 | + public int $bgColor = 0; | |
| 14 | + public float $delayTime = 0; | |
| 15 | + public array $globalColorTable = []; | |
| 16 | + public int $numTransparentPixels = 0; | |
| 17 | + public int $lzwMinCodeSize = 0; | |
| 18 | + public string $imageData = ''; | |
| 19 | + | |
| 20 | + /** | |
| 21 | + * TODO: 現在、インタレースした画像が未対応です。 | |
| 22 | + */ | |
| 23 | + public function parse(string $file): \stdClass { | |
| 24 | + $gif = new \stdClass(); | |
| 25 | + $fp = fopen($file, 'rb'); | |
| 26 | + $magic = fread($fp, 6); // マジックのスキップ | |
| 27 | + | |
| 28 | + // v = リトルエンディアン(2ビット) | |
| 29 | + $this->width = unpack('v', fread($fp, 2))[1]; | |
| 30 | + $this->height = unpack('v', fread($fp, 2))[1]; | |
| 31 | + $packed = ord(fread($fp, 1)); | |
| 32 | + $this->bitDepth = ($packed & 0x07) + 1; | |
| 33 | + $this->numColors = 1 << $this->bitDepth; | |
| 34 | + $hasGct = $this->bitDepth === 8; | |
| 35 | + | |
| 36 | + $gif->width = $this->width; | |
| 37 | + $gif->height = $this->height; | |
| 38 | + $gif->bitDepth = $this->bitDepth; | |
| 39 | + $gif->numColors = $this->numColors; | |
| 40 | + if ($magic === 'GIF87a' || !$hasGct) return $gif; // それ以外情報がない | |
| 41 | + | |
| 42 | + $this->bgColor = (int)bin2hex(fread($fp, 1)); | |
| 43 | + $gif->bgColor = $this->bgColor; | |
| 44 | + bin2hex(fread($fp, 1)); // いつでも 0:0? | |
| 45 | + | |
| 46 | + $gceLen = 0; | |
| 47 | + while (!feof($fp)) { | |
| 48 | + $skip = bin2hex(fread($fp, 3)); | |
| 49 | + $color = new RGB((int)($skip[0].$skip[1]), (int)($skip[2].$skip[3]), (int)($skip[4].$skip[5])); | |
| 50 | + $this->globalColorTable[] = $color->rgb; | |
| 51 | + if ($skip[0].$skip[1] === '21' && $skip[2].$skip[3] === 'f9') { | |
| 52 | + $gceLen = (int)($skip[4].$skip[5]); | |
| 53 | + break; | |
| 54 | + } | |
| 55 | + } | |
| 56 | + | |
| 57 | + // GCE開始 | |
| 58 | + $this->hasAlpha = (ord(fread($fp, 1)) & 0x01) === 0x01; | |
| 59 | + $this->delayTime = (unpack('v', fread($fp, 2))[1]) / 100.0; | |
| 60 | + $this->numTransparentPixels = ord(fread($fp, 1)); | |
| 61 | + $gif->hasAlpha = $this->hasAlpha; | |
| 62 | + $gif->delayTime = $this->delayTime; | |
| 63 | + $gif->numTransparentPixels = $this->numTransparentPixels; | |
| 64 | + fread($fp, 1); // GCE終了 | |
| 65 | + fread($fp, 1); // 画像指定子開始 | |
| 66 | + fread($fp, 4); // 画面のXとY、いつでも(0, 0) | |
| 67 | + fread($fp, 4); // 画像のサイズ、関数の開始で既に保存した | |
| 68 | + $pack = ord(fread($fp, 1)); | |
| 69 | + $this->interlace = ($pack & 0x40); // 画像のサイズ、関数の開始で既に保存した | |
| 70 | + | |
| 71 | + $this->lzwMinCodeSize = ord(fread($fp, 1)); // 画像データ開始 | |
| 72 | + $gif->lzwMinCodeSize = $this->lzwMinCodeSize; | |
| 73 | + $imageData = ''; | |
| 74 | + while (true) { | |
| 75 | + $blockSize = ord(fread($fp, 1)); | |
| 76 | + if ($blockSize === 0) break; | |
| 77 | + $imageData .= fread($fp, 1); | |
| 78 | + } | |
| 79 | + | |
| 80 | + $this->imageData = base64_encode($imageData); | |
| 81 | + $this->colorType = 3; | |
| 82 | + $this->compression = 'LZW'; | |
| 83 | + $gif->colorType = $this->colorType; | |
| 84 | + $gif->compression = $this->compression; | |
| 85 | + $gif->globalColorTable = $this->globalColorTable; | |
| 86 | + $gif->imageData = $this->imageData; | |
| 87 | + | |
| 88 | + fclose($fp); | |
| 89 | + return $gif; | |
| 90 | + } | |
| 91 | +} |
| @@ -0,0 +1,6 @@ | ||
| 1 | +<?php | |
| 2 | +namespace Site\Lib\Image; | |
| 3 | + | |
| 4 | +interface ImageInterface { | |
| 5 | + public function parse(string $file): \stdClass; | |
| 6 | +} |
| @@ -0,0 +1,54 @@ | ||
| 1 | +<?php | |
| 2 | +namespace Site\Lib\Image; | |
| 3 | + | |
| 4 | +use Site\Lib\Image\ImageInterface; | |
| 5 | + | |
| 6 | +class Jpeg implements ImageInterface { | |
| 7 | + public int $width = 0; | |
| 8 | + public int $height = 0; | |
| 9 | + public int $bpp = 0; | |
| 10 | + public string $imageData = ''; | |
| 11 | + | |
| 12 | + /** | |
| 13 | + * TODO: 現在、IHDRヘッダーのみを受け取る | |
| 14 | + */ | |
| 15 | + public function parse(string $file): \stdClass { | |
| 16 | + $jpg = new \stdClass; | |
| 17 | + $fp = fopen($file, 'rb'); | |
| 18 | + | |
| 19 | + $i = 0; | |
| 20 | + while (!feof($fp)) { | |
| 21 | + $byte = fread($fp, 1); | |
| 22 | + if ($byte !== "\xFF") continue; | |
| 23 | + | |
| 24 | + $marker = ord(fread($fp, 1)); | |
| 25 | + if ($marker >= 0xC0 && $marker <= 0xC3) { // 0xC0 = SOF0, 0xC1 = SOF1, 0xC2 = SOF2, 0xC3 = SOF3 | |
| 26 | + fread($fp, 2); | |
| 27 | + $this->bpp = ord(fread($fp, 1)); | |
| 28 | + // n = ビッグエンディアン(2ビット) | |
| 29 | + $this->height = unpack('n', fread($fp, 2))[1]; | |
| 30 | + $this->width = unpack('n', fread($fp, 2))[1]; | |
| 31 | + $jpg->width = $this->width; | |
| 32 | + $jpg->height = $this->height; | |
| 33 | + $jpg->bpp = $this->bpp; | |
| 34 | + } else if ($marker == 0xC4) { // DHT (Huffmanテーブル(複数可)) | |
| 35 | + // | |
| 36 | + } else if ($marker == 0xDB) { // DQT (量子化テーブル(複数可)) | |
| 37 | + // | |
| 38 | + } else if ($marker == 0xDD) { // DRI (リセット期間) | |
| 39 | + } else if ($marker == 0xDA) { // SOS(画像開始) | |
| 40 | + } else if ($marker >= 0xD0 && $marker <= 0xD7) { // RST | |
| 41 | + } else if ($marker >= 0xE1 && $marker <= 0xEF) { // APP | |
| 42 | + } else if ($marker == 0xFE) { // COM | |
| 43 | + } else if ($marker == 0xD9) { // EOI(画像終了) | |
| 44 | + break; | |
| 45 | + } | |
| 46 | + } | |
| 47 | + | |
| 48 | + // kys($jpg); | |
| 49 | + | |
| 50 | + $i++; | |
| 51 | + fclose($fp); | |
| 52 | + return $jpg; | |
| 53 | + } | |
| 54 | +} |
| @@ -0,0 +1,74 @@ | ||
| 1 | +<?php | |
| 2 | +namespace Site\Lib\Image; | |
| 3 | + | |
| 4 | +use Site\Lib\Image\ImageInterface; | |
| 5 | + | |
| 6 | +class Png implements ImageInterface { | |
| 7 | + public int $width = 0; | |
| 8 | + public int $height = 0; | |
| 9 | + public int $bpp = 0; | |
| 10 | + public int $bitDepth = 0; | |
| 11 | + public int $numColors = 0; | |
| 12 | + public int $colorType = 0; | |
| 13 | + public string $compression = 'NONE'; | |
| 14 | + public int $filter = 0; | |
| 15 | + public int $interlace = 0; | |
| 16 | + public bool $hasAlpha = false; | |
| 17 | + public string $imageData = ''; | |
| 18 | + | |
| 19 | + public function parse(string $file): \stdClass { | |
| 20 | + $png = new \stdClass(); | |
| 21 | + $fp = fopen($file, 'rb'); | |
| 22 | + | |
| 23 | + fread($fp, 8); // マジックのスキップ | |
| 24 | + unpack('N', fread($fp, 4))[1]; | |
| 25 | + $type = fread($fp, 4); | |
| 26 | + if ($type !== 'IHDR') { | |
| 27 | + fclose($fp); | |
| 28 | + return $png; | |
| 29 | + } | |
| 30 | + | |
| 31 | + // IHDRデータ | |
| 32 | + $data = fread($fp, 13); | |
| 33 | + | |
| 34 | + // N = ビッグエンディアン(4ビット) | |
| 35 | + $this->width = unpack('N', substr($data, 0, 4))[1]; | |
| 36 | + $this->height = unpack('N', substr($data, 4, 4))[1]; | |
| 37 | + $this->bitDepth = ord($data[8]); | |
| 38 | + $this->numColors = 1 << $this->bitDepth; | |
| 39 | + $this->colorType = ord($data[9]); | |
| 40 | + $this->compression = 'DEFLATE'; | |
| 41 | + $this->filter = ord($data[11]); | |
| 42 | + $this->interlace = ord($data[12]); // 0 = なし, 1 = Adam7 | |
| 43 | + $this->bpp = $this->bitDepth * $this->getSamplesPerPixel($this->colorType); | |
| 44 | + $this->hasAlpha = ($this->colorType === 4 || $this->colorType === 6); | |
| 45 | + | |
| 46 | + $png->width = $this->width; | |
| 47 | + $png->height = $this->height; | |
| 48 | + $png->bitDepth = $this->bitDepth; | |
| 49 | + $png->numColors = $this->numColors; | |
| 50 | + $png->colorType = $this->colorType; | |
| 51 | + $png->compression = $this->compression; | |
| 52 | + $png->filter = $this->filter; | |
| 53 | + $png->interlace = $this->interlace; | |
| 54 | + $png->bpp = $this->bpp; | |
| 55 | + $png->hasAlpha = $this->hasAlpha; | |
| 56 | + | |
| 57 | + fclose($fp); | |
| 58 | + | |
| 59 | + return $png; | |
| 60 | + } | |
| 61 | + | |
| 62 | + /////////////// | |
| 63 | + | |
| 64 | + private function getSamplesPerPixel(int $colorType): int { | |
| 65 | + return match($colorType) { | |
| 66 | + 0 => 1, // グレースケール | |
| 67 | + 2 => 3, // RGB | |
| 68 | + 3 => 1, // 指標した色(ペレット) | |
| 69 | + 4 => 2, // グレースケール+アルファ | |
| 70 | + 6 => 4, // RGBA | |
| 71 | + default => 1, | |
| 72 | + }; | |
| 73 | + } | |
| 74 | +} |
| @@ -0,0 +1,115 @@ | ||
| 1 | +<?php | |
| 2 | +namespace Site\Lib\Image; | |
| 3 | + | |
| 4 | +use Site\Lib\Image\ImageInterface; | |
| 5 | + | |
| 6 | +class Targa implements ImageInterface { | |
| 7 | + public int $id = 0; | |
| 8 | + public int $colorMapType = 0; | |
| 9 | + public int $imageTypeByte = 0; | |
| 10 | + public string $imageType = ''; | |
| 11 | + public int $colorMapEntryIndex = 0; | |
| 12 | + public int $colorMapLength = 0; | |
| 13 | + public int $colorMapBits = 0; | |
| 14 | + public int $x = 0; | |
| 15 | + public int $y = 0; | |
| 16 | + public int $width = 0; | |
| 17 | + public int $height = 0; | |
| 18 | + public int $bitDepth = 0; | |
| 19 | + public int $bpp = 0; | |
| 20 | + public int $interlace = 0; | |
| 21 | + public int $isRightToLeft = 0; // 0 = 左→右, 1 = 右→左 | |
| 22 | + public int $isTopToBottom = 0; // 0 = 下→上, 1 = 上→下 | |
| 23 | + public int $alphaBits = 0; | |
| 24 | + public bool $hasAlpha = false; | |
| 25 | + //-------- | |
| 26 | + public string $imageId = ''; | |
| 27 | + public string $colorMapData = ''; | |
| 28 | + public string $imageData = ''; | |
| 29 | + | |
| 30 | + private array $imageTypes = [ | |
| 31 | + 0 => 'NO DATA', | |
| 32 | + 1 => 'UNCOMPRESSED COLOR-MAPPED IMAGE', | |
| 33 | + 2 => 'UNCOMPRESSED TRUE COLOR IMAGE', | |
| 34 | + 3 => 'UNCOMPRESSED GRAYSCALE IMAGE', | |
| 35 | + 9 => 'RUN-LENGTH ENCODED COLOR-MAPPED IMAGE', | |
| 36 | + 10 => 'RUN-LENGTH ENCODED TRUE-COLOR IMAGE', | |
| 37 | + 11 => 'RUN-LENGTH ENCODED GRAYSCALE IMAGE', | |
| 38 | + 32 => 'HUFFMAN-DELTA-RUN-LENGTH ENCODED COLOR-MAPPED IMAGE', | |
| 39 | + 33 => 'HUFFMAN-DELTA-RUN-LENGTH-4-PASS-QUADTREE-TYPE PROCESS ENCODED COLOR-MAPPED IMAGE', | |
| 40 | + ]; | |
| 41 | + | |
| 42 | + public function parse(string $file): \stdClass { | |
| 43 | + $tga = new \stdClass(); | |
| 44 | + $fp = fopen($file, 'rb'); | |
| 45 | + | |
| 46 | + $this->id = ord(fread($fp, 1)); | |
| 47 | + $this->colorMapType = ord(fread($fp, 1)); // 0 = なし, 1 = あり, 2~127 = Truevision向け, 128~255 = 開発者向け | |
| 48 | + $this->imageTypeByte = ord(fread($fp, 1)); | |
| 49 | + $this->imageType = $this->imageTypes[$this->imageTypeByte]; | |
| 50 | + $tga->id = $this->id; | |
| 51 | + $tga->colorMapType = $this->colorMapType; | |
| 52 | + $tga->imageTypeByte = $this->imageTypeByte; | |
| 53 | + $tga->imageType = $this->imageType; | |
| 54 | + | |
| 55 | + $this->colorMapEntryIndex = unpack('v', fread($fp, 2))[1]; | |
| 56 | + $this->colorMapLength = unpack('v', fread($fp, 2))[1]; | |
| 57 | + $this->colorMapBits = ord(fread($fp, 1)); | |
| 58 | + $tga->colorMapEntryIndex = $this->colorMapEntryIndex; | |
| 59 | + $tga->colorMapLength = $this->colorMapLength; | |
| 60 | + $tga->colorMapBits = $this->colorMapBits; | |
| 61 | + | |
| 62 | + $this->x = unpack('v', fread($fp, 2))[1]; | |
| 63 | + $this->y = unpack('v', fread($fp, 2))[1]; | |
| 64 | + $this->width = unpack('v', fread($fp, 2))[1]; | |
| 65 | + $this->height = unpack('v', fread($fp, 2))[1]; | |
| 66 | + $this->bitDepth = ord(fread($fp, 1)); | |
| 67 | + $tga->x = $this->x; | |
| 68 | + $tga->y = $this->y; | |
| 69 | + $tga->width = $this->width; | |
| 70 | + $tga->height = $this->height; | |
| 71 | + $tga->bitDepth = $this->bitDepth; | |
| 72 | + | |
| 73 | + $desc = ord(fread($fp, 1)); | |
| 74 | + $this->interlace = ($desc & 0xC0) >> 6; | |
| 75 | + $this->isTopToBottom = ($desc & 0x20) !== 0; | |
| 76 | + $this->isRightToLeft = ($desc & 0x10) !== 0; | |
| 77 | + $this->alphaBits = $desc & 0x0F; | |
| 78 | + $this->hasAlpha = ($this->alphaBits == 8) && in_array($this->imageTypeByte == [2, 3, 10, 11]); | |
| 79 | + $tga->interlace = $this->interlace; | |
| 80 | + $tga->isRightToLeft = $this->isRightToLeft; | |
| 81 | + $tga->isTopToBottom = $this->isTopToBottom; | |
| 82 | + $tga->alphaBits = $this->alphaBits; | |
| 83 | + $tga->hasAlpha = $this->hasAlpha; | |
| 84 | + | |
| 85 | + $needsXFlip = $tga->isRightToLeft; | |
| 86 | + $needsYFlip = !$tga->isTopToBottom; | |
| 87 | + // 0010 0000 | |
| 88 | + | |
| 89 | + //--------- | |
| 90 | + $imageIdBytes = $this->id; | |
| 91 | + $colorMapBytes = 0; | |
| 92 | + if ($this->colorMapType === 1 && $this->colorMapLength > 0) { | |
| 93 | + $bytesPerEntry = (int)($this->colorMapBits / 8); | |
| 94 | + $colorMapBytes = $this->colorMapLength * $bytesPerEntry; | |
| 95 | + } | |
| 96 | + | |
| 97 | + $this->imageId = $imageIdBytes > 0 ? fread($fp, $imageIdBytes) : ''; | |
| 98 | + $this->colorMapData = $colorMapBytes > 0 ? fread($fp, $colorMapBytes) : ''; | |
| 99 | + $tga->imageId = $this->imageId; | |
| 100 | + $tga->colorMapData = $this->colorMapData; | |
| 101 | + | |
| 102 | + if (in_array($this->imageTypeByte, [9, 10, 11])) { // RLE | |
| 103 | + // TODO | |
| 104 | + } else if (in_array($this->imageTypeByte, [1, 2, 3])) { // 非圧縮化 | |
| 105 | + $this->bpp = (int)($this->bitDepth / 8); | |
| 106 | + $bytes = $this->width * $this->height * $this->bpp; | |
| 107 | + $this->imageData = fread($fp, $bytes); | |
| 108 | + $tga->bpp = $this->bpp; | |
| 109 | + $tga->imageData = $this->imageData; | |
| 110 | + } | |
| 111 | + | |
| 112 | + fclose($fp); | |
| 113 | + return $tga; | |
| 114 | + } | |
| 115 | +} |
変更
util.php
| @@ -127,6 +127,7 @@ | ||
| 127 | 127 | } |
| 128 | 128 | |
| 129 | 129 | function kys(mixed $arg): void { |
| 130 | + if (!DEBUG_MODE) return; | |
| 130 | 131 | if (gettype($arg) == 'boolean') $arg = $arg === true ? 'true' : 'false'; |
| 131 | 132 | if (gettype($arg) == 'array' || gettype($arg) == 'object') { |
| 132 | 133 | foreach ($arg as $a) { if (gettype($a) == 'boolean') $a = $a === true ? 'true' : 'false'; } |
| @@ -140,6 +141,7 @@ | ||
| 140 | 141 | } |
| 141 | 142 | |
| 142 | 143 | function ffs(): void { |
| 144 | + if (!DEBUG_MODE) return; | |
| 143 | 145 | echo '<style>html { color: #fcfcfc; background-color: #232023; } body { margin: 0; }</style>'; |
| 144 | 146 | echo '<header style="padding: 10px 0; display: flex; justify-content: space-evenly; background-color: #b61729; margin-bottom: 20px; position: sticky; top: 0;"><div><b>F</b>OR</div> <div><b>F</b>UCKS</div> <div><b>S</b>AKE</div></header>'; |
| 145 | 147 |
| @@ -31,7 +31,7 @@ | ||
| 31 | 31 | {@ if (isset($meta) && isset($meta->thumbnail) && !empty($meta->thumbnail)) @} |
| 32 | 32 | <meta property="og:image" content="https://{{ $_SERVER['HTTP_HOST'] }}/static/article/{{ $meta->thumbnail }}" /> |
| 33 | 33 | <meta property="og:image:secure_url" content="https://{{ $_SERVER['HTTP_HOST'] }}/static/article/{{ $meta->thumbnail }}" /> |
| 34 | - {$ $imgspec = getImageInfo('/public/static/article/'.$meta->thumbnail); $} | |
| 34 | + {$ $imgspec = getImageInfo('/public/static/article/'.$meta->thumbnail)->fullInfo; $} | |
| 35 | 35 | <meta property="og:image:type" content="{{ $imgspec->type }}" /> |
| 36 | 36 | <meta property="og:image:width" content="{{ $imgspec->width }}" /> |
| 37 | 37 | <meta property="og:image:height" content="{{ $imgspec->height }}" /> |