画像パーシングライブラリーの追加

諏訪子 <suwako@076.moe>、2025-12-31 06:03
コミット f0b5c7be2c3f600078e391df10c8a40a8aaf781c ・親 cb03343 ・ファイルを見る

5個のファイル、+274 -6

変更 README-JP.md
@@ -7,7 +7,7 @@
77 サンプル:[https://lbdemo.technicalsuwako.moe/](https://lbdemo.technicalsuwako.moe/)
88
99 ## Little Beast とは?
10-Little Beast は PHP 8.3 以上向けのフレームワークで、076.moe(ゲーム開発会社)と technicalsuwako.moe(社長のブログ)向けに作られました。\
10+Little Beast は PHP 8.5 以上向けのフレームワークで、076.moe(ゲーム開発会社)と technicalsuwako.moe(社長のブログ)向けに作られました。\
1111 メイン考え方は「必要な物だけ拡張し、不要な物は削除する」です。\
1212 各コア機能はライブラリに分割されている為、必要な物だけを選び易い設計になっています。\
1313 全てのモジュールはゼロから書かれており、データベースは一切必要ありません。
変更 README.md
@@ -7,7 +7,7 @@
77 Demo installation: [https://lbdemo.technicalsuwako.moe/](https://lbdemo.technicalsuwako.moe/)
88
99 ## 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).\
1111 The core mentality is to extend what you need, and remove what you don't need.\
1212 Each core feature is split into libraries, so it is easy to choose what you need.\
1313 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 @@
381381
382382 return $len == $sum;
383383 }
384+
385+function getImageInfo(string $url): \Site\Lib\Image {
386+ $img = new \Site\Lib\Image($url);
387+ return $img;
388+}
@@ -15,7 +15,7 @@
1515 {@ if (isset($meta->author)) @}
1616 <meta name="author" content="{{ $meta->author }}" />
1717 {@ endif @}
18-{@ if (isset($meta->thumbnail)) @}
18+{@ if (sset($meta->thumbnail) && !empty($meta->thumbnail)) @}
1919 <meta name="thumbnail" content="/static/{{ $meta->thumbnail }}" />
2020 {@ endif @}
2121 {@ endif @}
@@ -24,19 +24,28 @@
2424
2525 <meta property="og:title" content="{{ SITEINFO['title'] }}: {{ $pagetit }}" />
2626 <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'] }}" />
2729 <meta property="og:type" content="{{ isset($meta) && isset($meta->thumbnail) ? 'article' : 'website' }}" />
2830 <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)) @}
3032 <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 }}" />
3139 {@ endif @}
3240
3341 {@ if (TWITTER_HANDLE != '') @}
3442 <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 }}" />
3645 <meta name="twitter:title" content="{{ SITEINFO['title'] }}: {{ $pagetit }}" />
3746 <meta name="twitter:url" content="{{ isset($_SERVER['REQUEST_URI']) ? 'https://'.$_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI'] : 'https://'.$_SERVER['HTTP_HOST'] }}" />
3847 <meta name="twitter:description" content="{{ $description }}" />
39-{@ if (isset($meta) && isset($meta->thumbnail)) @}
48+{@ if (isset($meta) && isset($meta->thumbnail) && !empty($meta->thumbnail)) @}
4049 <meta name="twitter:image:src" content="https://{{ $_SERVER['HTTP_HOST'] }}/static/article/{{ $meta->thumbnail }}" />
4150 {@ endif @}
4251 {@ endif @}