画像パーシングライブラリーの追加
諏訪子 <suwako@076.moe>、2025-12-31 06:03
コミット f0b5c7be2c3f600078e391df10c8a40a8aaf781c
・親 cb03343
・ファイルを見る
5個のファイル、+274 -6
変更
README-JP.md
| @@ -7,7 +7,7 @@ | ||
| 7 | 7 | サンプル:[https://lbdemo.technicalsuwako.moe/](https://lbdemo.technicalsuwako.moe/) |
| 8 | 8 | |
| 9 | 9 | ## Little Beast とは? |
| 10 | -Little Beast は PHP 8.3 以上向けのフレームワークで、076.moe(ゲーム開発会社)と technicalsuwako.moe(社長のブログ)向けに作られました。\ | |
| 10 | +Little Beast は PHP 8.5 以上向けのフレームワークで、076.moe(ゲーム開発会社)と technicalsuwako.moe(社長のブログ)向けに作られました。\ | |
| 11 | 11 | メイン考え方は「必要な物だけ拡張し、不要な物は削除する」です。\ |
| 12 | 12 | 各コア機能はライブラリに分割されている為、必要な物だけを選び易い設計になっています。\ |
| 13 | 13 | 全てのモジュールはゼロから書かれており、データベースは一切必要ありません。 |
変更
README.md
| @@ -7,7 +7,7 @@ | ||
| 7 | 7 | Demo installation: [https://lbdemo.technicalsuwako.moe/](https://lbdemo.technicalsuwako.moe/) |
| 8 | 8 | |
| 9 | 9 | ## What is Little Beast? |
| 10 | -Little Beast is a PHP 8.3 or above framework made for 076.moe (game developer company) and technicalsuwako.moe (CEO's blog).\ | |
| 10 | +Little Beast is a PHP 8.5 or above framework made for 076.moe (game developer company) and technicalsuwako.moe (CEO's blog).\ | |
| 11 | 11 | The core mentality is to extend what you need, and remove what you don't need.\ |
| 12 | 12 | Each core feature is split into libraries, so it is easy to choose what you need.\ |
| 13 | 13 | All modules are written from scratch, nothing is to be require a database. |
| @@ -0,0 +1,254 @@ | ||
| 1 | +<?php | |
| 2 | +namespace Site\Lib; | |
| 3 | + | |
| 4 | +class RGB { | |
| 5 | + public int $r; | |
| 6 | + public int $g; | |
| 7 | + public int $b; | |
| 8 | + public array $rgb; | |
| 9 | + | |
| 10 | + public function __construct(int $r, int $g, int $b) { | |
| 11 | + $this->r = $r; | |
| 12 | + $this->g = $g; | |
| 13 | + $this->b = $b; | |
| 14 | + $this->rgb = [$r, $g, $b]; | |
| 15 | + } | |
| 16 | +} | |
| 17 | + | |
| 18 | +class Image { | |
| 19 | + public string $filename = 'UNKNOWN'; | |
| 20 | + public string $extension = 'UNKNOWN'; | |
| 21 | + 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 | + public string $size = '0 B'; | |
| 39 | + public int $bytes = 0; | |
| 40 | + public \stdClass $fullInfo; | |
| 41 | + | |
| 42 | + public function __construct(string $file) { | |
| 43 | + $file = ROOT.$file; | |
| 44 | + // $file = ROOT.'/public/static/article/mock-screenshot.jpg'; | |
| 45 | + if (!file_exists($file)) return; | |
| 46 | + $fp = fopen($file, 'rb'); | |
| 47 | + if (!$fp) return; | |
| 48 | + | |
| 49 | + $extBytes = fread($fp, 15); | |
| 50 | + fclose($fp); | |
| 51 | + $this->bytes = filesize($file); | |
| 52 | + $this->size = $this->formatBytes($this->bytes); | |
| 53 | + $this->extension = pathinfo($file, PATHINFO_EXTENSION); | |
| 54 | + $this->filename = str_replace('.'.$this->extension, '', array_last(explode('/', $file))); | |
| 55 | + | |
| 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 | + } | |
| 76 | + } | |
| 77 | + | |
| 78 | + private function parseJPEG(string $file): \stdClass { | |
| 79 | + $jpg = new \stdClass; | |
| 80 | + $fp = fopen($file, 'rb'); | |
| 81 | + | |
| 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; | |
| 116 | + } | |
| 117 | + | |
| 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 | + private function formatBytes(int $size, int $precision = 2): string { | |
| 239 | + $units = ['B', 'KiB', 'MiB', 'GiB', 'TiB']; | |
| 240 | + for ($i = 0; $size > 1024 && $i < count($units) - 1; ++$i) $size /= 1024; | |
| 241 | + 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 | + } | |
| 254 | +} |
変更
util.php
| @@ -381,3 +381,8 @@ | ||
| 381 | 381 | |
| 382 | 382 | return $len == $sum; |
| 383 | 383 | } |
| 384 | + | |
| 385 | +function getImageInfo(string $url): \Site\Lib\Image { | |
| 386 | + $img = new \Site\Lib\Image($url); | |
| 387 | + return $img; | |
| 388 | +} |
| @@ -15,7 +15,7 @@ | ||
| 15 | 15 | {@ if (isset($meta->author)) @} |
| 16 | 16 | <meta name="author" content="{{ $meta->author }}" /> |
| 17 | 17 | {@ endif @} |
| 18 | -{@ if (isset($meta->thumbnail)) @} | |
| 18 | +{@ if (sset($meta->thumbnail) && !empty($meta->thumbnail)) @} | |
| 19 | 19 | <meta name="thumbnail" content="/static/{{ $meta->thumbnail }}" /> |
| 20 | 20 | {@ endif @} |
| 21 | 21 | {@ endif @} |
| @@ -24,19 +24,28 @@ | ||
| 24 | 24 | |
| 25 | 25 | <meta property="og:title" content="{{ SITEINFO['title'] }}: {{ $pagetit }}" /> |
| 26 | 26 | <meta property="og:description" content="{{ $description }}" /> |
| 27 | + <meta property="og:locale" content="{{ str_contains($_SERVER['REQUEST_URI'], '/enblog') ? 'en_US' : 'ja_JP' }}" /> | |
| 28 | + <meta property="og:site_name" content="{{ SITEINFO['title'] }}" /> | |
| 27 | 29 | <meta property="og:type" content="{{ isset($meta) && isset($meta->thumbnail) ? 'article' : 'website' }}" /> |
| 28 | 30 | <meta property="og:url" content="{{ isset($_SERVER['REQUEST_URI']) ? 'https://'.$_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI'] : 'https://'.$_SERVER['HTTP_HOST'] }}" /> |
| 29 | -{@ if (isset($meta) && isset($meta->thumbnail)) @} | |
| 31 | +{@ if (isset($meta) && isset($meta->thumbnail) && !empty($meta->thumbnail)) @} | |
| 30 | 32 | <meta property="og:image" content="https://{{ $_SERVER['HTTP_HOST'] }}/static/article/{{ $meta->thumbnail }}" /> |
| 33 | + <meta property="og:image:secure_url" content="https://{{ $_SERVER['HTTP_HOST'] }}/static/article/{{ $meta->thumbnail }}" /> | |
| 34 | + {$ $imgspec = getImageInfo('/public/static/article/'.$meta->thumbnail); $} | |
| 35 | + <meta property="og:image:type" content="{{ $imgspec->type }}" /> | |
| 36 | + <meta property="og:image:width" content="{{ $imgspec->width }}" /> | |
| 37 | + <meta property="og:image:height" content="{{ $imgspec->height }}" /> | |
| 38 | + <meta property="og:image:alt" content="{{ $meta->title }}" /> | |
| 31 | 39 | {@ endif @} |
| 32 | 40 | |
| 33 | 41 | {@ if (TWITTER_HANDLE != '') @} |
| 34 | 42 | <meta name="twitter:card" content="summary_large_image" /> |
| 35 | - <meta name="twitter:card" content="{{ TWITTER_HANDLE }}" /> | |
| 43 | + <meta name="twitter:site" content="{{ TWITTER_HANDLE }}" /> | |
| 44 | + <meta name="twitter:creator" content="{{ TWITTER_HANDLE }}" /> | |
| 36 | 45 | <meta name="twitter:title" content="{{ SITEINFO['title'] }}: {{ $pagetit }}" /> |
| 37 | 46 | <meta name="twitter:url" content="{{ isset($_SERVER['REQUEST_URI']) ? 'https://'.$_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI'] : 'https://'.$_SERVER['HTTP_HOST'] }}" /> |
| 38 | 47 | <meta name="twitter:description" content="{{ $description }}" /> |
| 39 | -{@ if (isset($meta) && isset($meta->thumbnail)) @} | |
| 48 | +{@ if (isset($meta) && isset($meta->thumbnail) && !empty($meta->thumbnail)) @} | |
| 40 | 49 | <meta name="twitter:image:src" content="https://{{ $_SERVER['HTTP_HOST'] }}/static/article/{{ $meta->thumbnail }}" /> |
| 41 | 50 | {@ endif @} |
| 42 | 51 | {@ endif @} |