From f0b5c7be2c3f600078e391df10c8a40a8aaf781c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=AB=8F=E8=A8=AA=E5=AD=90?= Date: Wed, 31 Dec 2025 06:03:47 +0900 Subject: [PATCH] =?UTF-8?q?=E7=94=BB=E5=83=8F=E3=83=91=E3=83=BC=E3=82=B7?= =?UTF-8?q?=E3=83=B3=E3=82=B0=E3=83=A9=E3=82=A4=E3=83=96=E3=83=A9=E3=83=AA?= =?UTF-8?q?=E3=83=BC=E3=81=AE=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README-JP.md | 2 +- README.md | 2 +- src/Site/Lib/Image.php | 254 +++++++++++++++++++++++++++++++++++++++ util.php | 5 + view/common/header.maron | 17 ++- 5 files changed, 274 insertions(+), 6 deletions(-) create mode 100644 src/Site/Lib/Image.php diff --git a/README-JP.md b/README-JP.md index 17b3cd0..fa4c117 100644 --- a/README-JP.md +++ b/README-JP.md @@ -7,7 +7,7 @@ サンプル:[https://lbdemo.technicalsuwako.moe/](https://lbdemo.technicalsuwako.moe/) ## Little Beast とは? -Little Beast は PHP 8.3 以上向けのフレームワークで、076.moe(ゲーム開発会社)と technicalsuwako.moe(社長のブログ)向けに作られました。\ +Little Beast は PHP 8.5 以上向けのフレームワークで、076.moe(ゲーム開発会社)と technicalsuwako.moe(社長のブログ)向けに作られました。\ メイン考え方は「必要な物だけ拡張し、不要な物は削除する」です。\ 各コア機能はライブラリに分割されている為、必要な物だけを選び易い設計になっています。\ 全てのモジュールはゼロから書かれており、データベースは一切必要ありません。 diff --git a/README.md b/README.md index 4ff927b..78722e0 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ Simple, Pragmatic, Anti-bloat Demo installation: [https://lbdemo.technicalsuwako.moe/](https://lbdemo.technicalsuwako.moe/) ## What is Little Beast? -Little Beast is a PHP 8.3 or above framework made for 076.moe (game developer company) and technicalsuwako.moe (CEO's blog).\ +Little Beast is a PHP 8.5 or above framework made for 076.moe (game developer company) and technicalsuwako.moe (CEO's blog).\ The core mentality is to extend what you need, and remove what you don't need.\ Each core feature is split into libraries, so it is easy to choose what you need.\ All modules are written from scratch, nothing is to be require a database. diff --git a/src/Site/Lib/Image.php b/src/Site/Lib/Image.php new file mode 100644 index 0000000..4232857 --- /dev/null +++ b/src/Site/Lib/Image.php @@ -0,0 +1,254 @@ +r = $r; + $this->g = $g; + $this->b = $b; + $this->rgb = [$r, $g, $b]; + } +} + +class Image { + public string $filename = 'UNKNOWN'; + public string $extension = 'UNKNOWN'; + public string $type = 'UNSUPPORTED'; + public int $width = 0; + public int $height = 0; + public int $bpp = 0; + public int $bitDepth = 0; + public int $numColors = 0; // GIF, PNG + public int $colorType = 0; // PNG + public string $compression = 'NONE'; // GIF, PNG + public int $filter = 0; // PNG + public int $interlace = 0; // GIF, PNG + public bool $hasAlpha = false; + public int $bgColor = 0; // GIF + public float $delayTime = 0; // GIF + public array $globalColorTable = []; // GIF + public int $numTransparentPixels = 0; // GIF + public int $lzwMinCodeSize = 0; // GIF + public string $imageData = ''; + public string $size = '0 B'; + public int $bytes = 0; + public \stdClass $fullInfo; + + public function __construct(string $file) { + $file = ROOT.$file; + // $file = ROOT.'/public/static/article/mock-screenshot.jpg'; + if (!file_exists($file)) return; + $fp = fopen($file, 'rb'); + if (!$fp) return; + + $extBytes = fread($fp, 15); + 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))); + + if ($extBytes === false) return; + else if (substr($extBytes, 0, 3) === "\xff\xd8\xff") { + $this->type = 'image/jpeg'; + $this->parseJPEG($file); + } 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'; + // $this->parseWEBP($file); + } else if (substr($extBytes, 0, 3) === "\x42\x4d\x36") { + $this->type = 'image/bmp'; + // $this->parseBMP($file); + } else if (substr($extBytes, 0, 6) === "\x89\x50\x4e\x47\x0d\x0a") { + $this->type = 'image/png'; + $this->fullInfo = $this->parsePNG($file); + } 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'; + $this->fullInfo = $this->parseGIF($file); + } else if (str_ends_with($file, '.tga')) { + $this->type = 'image/tga'; + // $this->parseTGA($file); + } + } + + private function parseJPEG(string $file): \stdClass { + $jpg = new \stdClass; + $fp = fopen($file, 'rb'); + + $i = 0; + while (!feof($fp)) { + $byte = fread($fp, 1); + if ($byte !== "\xFF") continue; + + $marker = ord(fread($fp, 1)); + if ($marker >= 0xC0 && $marker <= 0xC3) { // 0xC0 = SOF0, 0xC1 = SOF1, 0xC2 = SOF2, 0xC3 = SOF3 + fread($fp, 2); + $this->bpp = ord(fread($fp, 1)); + // n = ビッグエンディアン(2ビット) + $this->height = unpack('n', fread($fp, 2))[1]; + $this->width = unpack('n', fread($fp, 2))[1]; + $jpg->width = $this->width; + $jpg->height = $this->height; + $jpg->bpp = $this->bpp; + } else if ($marker == 0xC4) { // DHT (Huffmanテーブル(複数可)) + // + } else if ($marker == 0xDB) { // DQT (量子化テーブル(複数可)) + // + } else if ($marker == 0xDD) { // DRI (リセット期間) + } else if ($marker == 0xDA) { // SOS(画像開始) + } else if ($marker >= 0xD0 && $marker <= 0xD7) { // RST + } else if ($marker >= 0xE1 && $marker <= 0xEF) { // APP + } else if ($marker == 0xFE) { // COM + } else if ($marker == 0xD9) { // EOI(画像終了) + break; + } + } + + // kys($jpg); + + $i++; + fclose($fp); + return $jpg; + } + + /** + * TODO: 現在、IHDRヘッダーのみを受け取る + */ + private function parsePNG(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') { + fclose($fp); + return $png; + } + + // IHDRデータ + $data = fread($fp, 13); + + // 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->delayTime = 0; + $this->hasAlpha = ($this->colorType === 4 || $this->colorType === 6); + + $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->delayTime = $this->delayTime; + $png->hasAlpha = $this->hasAlpha; + + fclose($fp); + + return $png; + } + + /** + * TODO: 現在、インタレースした画像が未対応です。 + */ + private function parseGIF(string $file): \stdClass { + $gif = new \stdClass(); + $fp = fopen($file, 'rb'); + $magic = fread($fp, 6); // マジックのスキップ + + // v = リトルエンディアン(2ビット) + $this->width = unpack('v', fread($fp, 2))[1]; + $this->height = unpack('v', fread($fp, 2))[1]; + $packed = ord(fread($fp, 1)); + $this->bitDepth = ($packed & 0x07) + 1; + $this->numColors = 1 << $this->bitDepth; + $hasGct = $this->bitDepth === 8; + + $gif->width = $this->width; + $gif->height = $this->height; + $gif->bitDepth = $this->bitDepth; + $gif->numColors = $this->numColors; + if ($magic === 'GIF87a' || !$hasGct) return $gif; // それ以外情報がない + + $this->bgColor = (int)bin2hex(fread($fp, 1)); + $gif->bgColor = $this->bgColor; + bin2hex(fread($fp, 1)); // いつでも 0:0? + + $gceLen = 0; + while (!feof($fp)) { + $skip = bin2hex(fread($fp, 3)); + $color = new RGB((int)($skip[0].$skip[1]), (int)($skip[2].$skip[3]), (int)($skip[4].$skip[5])); + $this->globalColorTable[] = $color->rgb; + if ($skip[0].$skip[1] === '21' && $skip[2].$skip[3] === 'f9') { + $gceLen = (int)($skip[4].$skip[5]); + break; + } + } + + // GCE開始 + $this->hasAlpha = (ord(fread($fp, 1)) & 0x01) === 0x01; + $this->delayTime = (unpack('v', fread($fp, 2))[1]) / 100.0; + $this->numTransparentPixels = ord(fread($fp, 1)); + $gif->hasAlpha = $this->hasAlpha; + $gif->delayTime = $this->delayTime; + $gif->numTransparentPixels = $this->numTransparentPixels; + fread($fp, 1); // GCE終了 + fread($fp, 1); // 画像指定子開始 + fread($fp, 4); // 画面のXとY、いつでも(0, 0) + fread($fp, 4); // 画像のサイズ、関数の開始で既に保存した + $pack = ord(fread($fp, 1)); + $this->interlace = ($pack & 0x40); // 画像のサイズ、関数の開始で既に保存した + + $this->lzwMinCodeSize = ord(fread($fp, 1)); // 画像データ開始 + $gif->lzwMinCodeSize = $this->lzwMinCodeSize; + $imageData = ''; + while (true) { + $blockSize = ord(fread($fp, 1)); + if ($blockSize === 0) break; + $imageData .= fread($fp, 1); + } + + $this->imageData = base64_encode($imageData); + $this->colorType = 3; + $this->compression = 'LZW'; + $gif->colorType = $this->colorType; + $gif->compression = $this->compression; + $gif->globalColorTable = $this->globalColorTable; + $gif->imageData = $this->imageData; + + fclose($fp); + return $gif; + } + + 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]; + } + + private function getSamplesPerPixel(int $colorType): int { + return match($colorType) { + 0 => 1, // グレースケール + 2 => 3, // RGB + 3 => 1, // 指標した色(ペレット) + 4 => 2, // グレースケール+アルファ + 6 => 4, // RGBA + default => 1, + }; + } +} \ No newline at end of file diff --git a/util.php b/util.php index e88df0a..f0cc78a 100644 --- a/util.php +++ b/util.php @@ -380,4 +380,9 @@ function countmatch(string $str): bool { $sum = $numUpper + $numLower + $numDigit + $numSymbol; return $len == $sum; +} + +function getImageInfo(string $url): \Site\Lib\Image { + $img = new \Site\Lib\Image($url); + return $img; } \ No newline at end of file diff --git a/view/common/header.maron b/view/common/header.maron index 09e6b5f..5927e80 100644 --- a/view/common/header.maron +++ b/view/common/header.maron @@ -15,7 +15,7 @@ {@ if (isset($meta->author)) @} {@ endif @} -{@ if (isset($meta->thumbnail)) @} +{@ if (sset($meta->thumbnail) && !empty($meta->thumbnail)) @} {@ endif @} {@ endif @} @@ -24,19 +24,28 @@ + + -{@ if (isset($meta) && isset($meta->thumbnail)) @} +{@ if (isset($meta) && isset($meta->thumbnail) && !empty($meta->thumbnail)) @} + + {$ $imgspec = getImageInfo('/public/static/article/'.$meta->thumbnail); $} + + + + {@ endif @} {@ if (TWITTER_HANDLE != '') @} - + + -{@ if (isset($meta) && isset($meta->thumbnail)) @} +{@ if (isset($meta) && isset($meta->thumbnail) && !empty($meta->thumbnail)) @} {@ endif @} {@ endif @}