| 1 | <?php |
| 2 | /************************************************************* |
| 3 | # 076 License |
| 4 | |
| 5 | Copyright (c) テクニカル諏訪子 |
| 6 | |
| 7 | Permission is hereby granted to any person obtaining a copy of the software |
| 8 | Little Beast (the "Software") to use, modify, merge, copy, publish, distribute, |
| 9 | sublicense, and/or sell copies of the Software, subject to the following conditions: |
| 10 | |
| 11 | 1. **Origin Attribution**: |
| 12 | - You must not misrepresent the origin of the Software; you must not claim |
| 13 | you created the original Software. |
| 14 | - If the Software is used in a product, you must either: |
| 15 | a. Provide clear attribution in the product's documentation, user interface, |
| 16 | or other visible areas, **OR** |
| 17 | b. Pay the original developers a fee they specify in writing. |
| 18 | 2. **Usage Restriction**: |
| 19 | - The Software, or any derivative works, dependencies, or libraries |
| 20 | incorporating it, must not be used for censorship or to suppress freedom of |
| 21 | speech, expression, or creativity. Prohibited uses include, but are not |
| 22 | limited to: |
| 23 | - Censorship of so-called "hate speech", visuals, non-mainstream opinions, |
| 24 | ideas, or objective reality. |
| 25 | - Tools or systems designed to restrict access to information or |
| 26 | artistic works. |
| 27 | 3. **Notice Preservation**: |
| 28 | - This license and the above copyright notice must remain intact in all copies |
| 29 | of the source code. |
| 30 | |
| 31 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, |
| 32 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A |
| 33 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT |
| 34 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF |
| 35 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE |
| 36 | OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
| 37 | */ |
| 38 | namespace Std\Lib; |
| 39 | |
| 40 | use Std\Lib\Image\ImageInterface; |
| 41 | use Std\Lib\Image\Bitmap; |
| 42 | use Std\Lib\Image\Bmp; |
| 43 | use Std\Lib\Image\Gif; |
| 44 | use Std\Lib\Image\Jpeg; |
| 45 | use Std\Lib\Image\Png; |
| 46 | use Std\Lib\Image\Targa; |
| 47 | use Std\Lib\Image\Webp; |
| 48 | |
| 49 | class RGB { |
| 50 | public int $r; |
| 51 | public int $g; |
| 52 | public int $b; |
| 53 | public array $rgb; |
| 54 | |
| 55 | public function __construct(int $r, int $g, int $b) { |
| 56 | $this->r = $r; |
| 57 | $this->g = $g; |
| 58 | $this->b = $b; |
| 59 | $this->rgb = ['r' => $r, 'g' => $g, 'b' => $b]; |
| 60 | } |
| 61 | } |
| 62 | |
| 63 | class Image { |
| 64 | public string $filename = 'UNKNOWN'; |
| 65 | public string $extension = 'UNKNOWN'; |
| 66 | public string $type = 'UNSUPPORTED'; |
| 67 | public string $size = '0 B'; |
| 68 | public int $bytes = 0; |
| 69 | public \stdClass $fullInfo; |
| 70 | |
| 71 | private const SUPPORTED = [ |
| 72 | 'image/bmp' => Bmp::class, |
| 73 | 'image/gif' => Gif::class, |
| 74 | 'image/jpeg' => Jpeg::class, |
| 75 | 'image/png' => Png::class, |
| 76 | 'image/webp' => Webp::class, |
| 77 | 'image/x-tga' => Targa::class, |
| 78 | ]; |
| 79 | |
| 80 | public function __construct(string $file) { |
| 81 | // ファイルが無くてもfullInfoを読める様に(以前は未初期化のままで、記事のサムネイルが無いとページが落ちた) |
| 82 | $this->fullInfo = (object)['type' => $this->type, 'size' => $this->size, 'bytes' => 0, 'width' => 0, 'height' => 0]; |
| 83 | $file = ROOT.$file; |
| 84 | // $file = ROOT.'/public/static/article/mock-screenshot.tga'; |
| 85 | if (!file_exists($file)) return; |
| 86 | $fp = fopen($file, 'rb'); |
| 87 | if (!$fp) return; |
| 88 | $extBytes = (string)fread($fp, 32); |
| 89 | fclose($fp); |
| 90 | |
| 91 | $this->bytes = filesize($file); |
| 92 | $this->size = $this->formatBytes($this->bytes); |
| 93 | $this->extension = pathinfo($file, PATHINFO_EXTENSION); |
| 94 | $this->filename = str_replace('.'.$this->extension, '', array_last(explode('/', $file))); |
| 95 | |
| 96 | $this->type = self::detect($extBytes, $file) ?? 'UNSUPPORTED'; |
| 97 | |
| 98 | $this->fullInfo = self::handler($this->type)->parse($file); |
| 99 | $this->fullInfo->type = $this->type; |
| 100 | $this->fullInfo->size = $this->size; |
| 101 | $this->fullInfo->bytes = $this->bytes; |
| 102 | if (!isset($this->fullInfo->width) && isset($this->fullInfo->header->width)) { |
| 103 | $this->fullInfo->width = $this->fullInfo->header->width; |
| 104 | $this->fullInfo->height = $this->fullInfo->header->height; |
| 105 | } |
| 106 | } |
| 107 | |
| 108 | /** |
| 109 | * ファイルの先頭から形式を判断する(TGAは印が無いので、ファイル名が「.tga」の場合だけ) |
| 110 | * |
| 111 | * @param string $head ファイルの先頭(32バイト以上) |
| 112 | * @param string $name ファイル名(TGA用、任意) |
| 113 | * @return string|null MIME、分からなければnull |
| 114 | */ |
| 115 | public static function detect(string $head, string $name = ''): ?string { |
| 116 | if (str_starts_with($head, "\xff\xd8\xff")) return 'image/jpeg'; |
| 117 | if (str_starts_with($head, "\x89PNG\r\n\x1a\n")) return 'image/png'; |
| 118 | if (str_starts_with($head, 'GIF87a') || str_starts_with($head, 'GIF89a')) return 'image/gif'; |
| 119 | if (str_starts_with($head, 'RIFF') && substr($head, 8, 4) === 'WEBP') return 'image/webp'; |
| 120 | // BMP:「BM」と、知っているヘッダーの大きさ |
| 121 | if (str_starts_with($head, 'BM') && strlen($head) >= 18 && in_array(unpack('V', substr($head, 14, 4))[1], [12, 16, 40, 52, 56, 64, 108, 124], true)) return 'image/bmp'; |
| 122 | if (str_ends_with(strtolower($name), '.tga')) return 'image/x-tga'; |
| 123 | return null; |
| 124 | } |
| 125 | |
| 126 | /** |
| 127 | * ファイルの中身を画素にする |
| 128 | * |
| 129 | * @param string $data ファイルの中身 |
| 130 | * @param string $name ファイル名(TGA用、任意) |
| 131 | * @return Bitmap |
| 132 | * @throws \RuntimeException 未対応の形式又は壊れたファイル |
| 133 | */ |
| 134 | public static function decode(string $data, string $name = ''): Bitmap { |
| 135 | return self::handler(self::detect($data, $name) ?? '')->decode($data); |
| 136 | } |
| 137 | |
| 138 | /** |
| 139 | * 画素をファイルの中身にする |
| 140 | * |
| 141 | * @param string $type MIME(image/png等) |
| 142 | * @param array $opt 形式毎の設定(PNG:level、JPEG:quality等) |
| 143 | */ |
| 144 | public static function encode(Bitmap $img, string $type, array $opt = []): string { |
| 145 | return self::handler($type)->encode($img, $opt); |
| 146 | } |
| 147 | |
| 148 | /////////////// |
| 149 | |
| 150 | private static function handler(string $type): ImageInterface { |
| 151 | $class = self::SUPPORTED[$type] ?? null; |
| 152 | if (!$class) throw new \RuntimeException("形式のハンドルが存在しない: {$type}"); |
| 153 | return new $class(); |
| 154 | } |
| 155 | |
| 156 | private function formatBytes(int $size, int $precision = 2): string { |
| 157 | $units = ['B', 'KiB', 'MiB', 'GiB', 'TiB']; |
| 158 | for ($i = 0; $size > 1024 && $i < count($units) - 1; ++$i) $size /= 1024; |
| 159 | return round($size, $precision).' '.$units[$i]; |
| 160 | } |
| 161 | } |