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

This commit is contained in:
2025-12-31 06:03:47 +09:00
parent cb03343677
commit f0b5c7be2c
5 changed files with 274 additions and 6 deletions

View File

@@ -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社長のブログ向けに作られました。\
メイン考え方は「必要な物だけ拡張し、不要な物は削除する」です。\
各コア機能はライブラリに分割されている為、必要な物だけを選び易い設計になっています。\
全てのモジュールはゼロから書かれており、データベースは一切必要ありません。

View File

@@ -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.

254
src/Site/Lib/Image.php Normal file
View File

@@ -0,0 +1,254 @@
<?php
namespace Site\Lib;
class RGB {
public int $r;
public int $g;
public int $b;
public array $rgb;
public function __construct(int $r, int $g, int $b) {
$this->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,
};
}
}

View File

@@ -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;
}

View File

@@ -15,7 +15,7 @@
{@ if (isset($meta->author)) @}
<meta name="author" content="{{ $meta->author }}" />
{@ endif @}
{@ if (isset($meta->thumbnail)) @}
{@ if (sset($meta->thumbnail) && !empty($meta->thumbnail)) @}
<meta name="thumbnail" content="/static/{{ $meta->thumbnail }}" />
{@ endif @}
{@ endif @}
@@ -24,19 +24,28 @@
<meta property="og:title" content="{{ SITEINFO['title'] }}: {{ $pagetit }}" />
<meta property="og:description" content="{{ $description }}" />
<meta property="og:locale" content="{{ str_contains($_SERVER['REQUEST_URI'], '/enblog') ? 'en_US' : 'ja_JP' }}" />
<meta property="og:site_name" content="{{ SITEINFO['title'] }}" />
<meta property="og:type" content="{{ isset($meta) && isset($meta->thumbnail) ? 'article' : 'website' }}" />
<meta property="og:url" content="{{ isset($_SERVER['REQUEST_URI']) ? 'https://'.$_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI'] : 'https://'.$_SERVER['HTTP_HOST'] }}" />
{@ if (isset($meta) && isset($meta->thumbnail)) @}
{@ if (isset($meta) && isset($meta->thumbnail) && !empty($meta->thumbnail)) @}
<meta property="og:image" content="https://{{ $_SERVER['HTTP_HOST'] }}/static/article/{{ $meta->thumbnail }}" />
<meta property="og:image:secure_url" content="https://{{ $_SERVER['HTTP_HOST'] }}/static/article/{{ $meta->thumbnail }}" />
{$ $imgspec = getImageInfo('/public/static/article/'.$meta->thumbnail); $}
<meta property="og:image:type" content="{{ $imgspec->type }}" />
<meta property="og:image:width" content="{{ $imgspec->width }}" />
<meta property="og:image:height" content="{{ $imgspec->height }}" />
<meta property="og:image:alt" content="{{ $meta->title }}" />
{@ endif @}
{@ if (TWITTER_HANDLE != '') @}
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:card" content="{{ TWITTER_HANDLE }}" />
<meta name="twitter:site" content="{{ TWITTER_HANDLE }}" />
<meta name="twitter:creator" content="{{ TWITTER_HANDLE }}" />
<meta name="twitter:title" content="{{ SITEINFO['title'] }}: {{ $pagetit }}" />
<meta name="twitter:url" content="{{ isset($_SERVER['REQUEST_URI']) ? 'https://'.$_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI'] : 'https://'.$_SERVER['HTTP_HOST'] }}" />
<meta name="twitter:description" content="{{ $description }}" />
{@ if (isset($meta) && isset($meta->thumbnail)) @}
{@ if (isset($meta) && isset($meta->thumbnail) && !empty($meta->thumbnail)) @}
<meta name="twitter:image:src" content="https://{{ $_SERVER['HTTP_HOST'] }}/static/article/{{ $meta->thumbnail }}" />
{@ endif @}
{@ endif @}