画像ライブラリ
守矢諏訪子 <suwako@076.moe>、2026-11-11 09:24
コミット 459b5228cb860c8a79e91d053182209ee4192307
・親 da1e704
・ファイルを見る
16個のファイル、+6332 -225
| @@ -42,13 +42,13 @@ | ||
| 42 | 42 | /** |
| 43 | 43 | * アップロードされた画像と動画(投稿の添付とアイコン) |
| 44 | 44 | * |
| 45 | - * 種類はファイルの中身で判断し(PNG・JPEG・GIF、動画はMP4・MKV・WebM)、位置情報等が入る部分 | |
| 46 | - * (JPEGのAPPn、PNGのeXIf・テキスト、MP4のudta・meta・uuid)を取り除く。 | |
| 45 | + * 種類はファイルの中身で判断し(PNG・JPEG・GIF・WebP、動画はMP4・MKV・WebM)、位置情報等が入る部分 | |
| 46 | + * (JPEGのAPPn、PNGのeXIf・テキスト、WebPのEXIF・XMP、MP4のudta・meta・uuid)を取り除く。 | |
| 47 | 47 | */ |
| 48 | 48 | final class Media { |
| 49 | 49 | public const MAX_BYTES = 20971520; // 20 MiB |
| 50 | 50 | public const MAX_VIDEO_BYTES = 419430400; // 400 MiB |
| 51 | - private const TYPES = ['image/png' => 'png', 'image/jpeg' => 'jpg', 'image/gif' => 'gif', 'video/mp4' => 'mp4', 'video/matroska' => 'mkv', 'video/webm' => 'webm']; | |
| 51 | + private const TYPES = ['image/png' => 'png', 'image/jpeg' => 'jpg', 'image/gif' => 'gif', 'image/webp' => 'webp', 'video/mp4' => 'mp4', 'video/matroska' => 'mkv', 'video/webm' => 'webm']; | |
| 52 | 52 | |
| 53 | 53 | /** |
| 54 | 54 | * $_FILESの1件を保存する |
| @@ -77,7 +77,7 @@ | ||
| 77 | 77 | if ($fh) fclose($fh); |
| 78 | 78 | $isVideo = $type !== null && str_starts_with($type, 'video/'); |
| 79 | 79 | if ($type === null || ($isVideo && !$video)) { |
| 80 | - return \Result::Error($video ? 'PNG・JPEG・GIFの画像と、MP4・MKV・WebMの動画だけアップロード出来ます。' : 'PNG・JPEG・GIFの画像だけアップロード出来ます。'); | |
| 80 | + return \Result::Error($video ? 'PNG・JPEG・GIF・WebPの画像と、MP4・MKV・WebMの動画だけアップロード出来ます。' : 'PNG・JPEG・GIF・WebPの画像だけアップロード出来ます。'); | |
| 81 | 81 | } |
| 82 | 82 | if (!$isVideo && $size > self::MAX_BYTES) return \Result::Error(self::tooLarge(false)); |
| 83 | 83 | |
| @@ -126,6 +126,7 @@ | ||
| 126 | 126 | if (str_starts_with($data, "\x89PNG\r\n\x1a\n")) return 'image/png'; |
| 127 | 127 | if (str_starts_with($data, "\xff\xd8\xff")) return 'image/jpeg'; |
| 128 | 128 | if (str_starts_with($data, 'GIF87a') || str_starts_with($data, 'GIF89a')) return 'image/gif'; |
| 129 | + if (str_starts_with($data, 'RIFF') && substr($data, 8, 4) === 'WEBP') return 'image/webp'; | |
| 129 | 130 | // ISO BMFF:AVIF・HEIC(画像)とQuickTime(ブラウザで再生出来ない事が多い)は除く |
| 130 | 131 | if (substr($data, 4, 4) === 'ftyp') { |
| 131 | 132 | $brand = substr($data, 8, 4); |
| @@ -151,6 +152,7 @@ | ||
| 151 | 152 | * |
| 152 | 153 | * JPEG:APP1〜APP15(Exif、XMP等)とCOMを消す(APP0のJFIFは残す) |
| 153 | 154 | * PNG:eXIf、tEXt、zTXt、iTXt、tIMEを消す |
| 155 | + * WebP:EXIFとXMPのチャンクを消す(VP8Xの印も外す) | |
| 154 | 156 | * MP4:udta、meta、uuid(位置、機種、XMP等)を同じ大きさの「free」にし、mvhd・tkhd・mdhdの日時を0にする |
| 155 | 157 | */ |
| 156 | 158 | public static function strip(string $data, string $type): string { |
| @@ -192,6 +194,25 @@ | ||
| 192 | 194 | $pos += $total; |
| 193 | 195 | } |
| 194 | 196 | return $out; |
| 197 | + } | |
| 198 | + if ($type === 'image/webp') { | |
| 199 | + $out = ''; | |
| 200 | + $pos = 12; | |
| 201 | + $len = min(strlen($data), 8 + unpack('V', substr($data, 4, 4) ?: "\0\0\0\0")[1]); | |
| 202 | + while ($pos + 8 <= $len) { | |
| 203 | + $ctype = substr($data, $pos, 4); | |
| 204 | + $total = 8 + unpack('V', substr($data, $pos + 4, 4))[1]; | |
| 205 | + $total += $total & 1; | |
| 206 | + if ($ctype === 'VP8X') { | |
| 207 | + $chunk = substr($data, $pos, $total); | |
| 208 | + if (strlen($chunk) > 8) $chunk[8] = chr(ord($chunk[8]) & ~0x0c); | |
| 209 | + $out .= $chunk; | |
| 210 | + } else if ($ctype !== 'EXIF' && $ctype !== 'XMP ') { | |
| 211 | + $out .= substr($data, $pos, $total); | |
| 212 | + } | |
| 213 | + $pos += $total; | |
| 214 | + } | |
| 215 | + return 'RIFF'.pack('V', 4 + strlen($out)).'WEBP'.$out; | |
| 195 | 216 | } |
| 196 | 217 | return $data; |
| 197 | 218 | } |
| @@ -38,10 +38,13 @@ | ||
| 38 | 38 | namespace Std\Lib; |
| 39 | 39 | |
| 40 | 40 | use Std\Lib\Image\ImageInterface; |
| 41 | +use Std\Lib\Image\Bitmap; | |
| 42 | +use Std\Lib\Image\Bmp; | |
| 41 | 43 | use Std\Lib\Image\Gif; |
| 42 | 44 | use Std\Lib\Image\Jpeg; |
| 43 | 45 | use Std\Lib\Image\Png; |
| 44 | 46 | use Std\Lib\Image\Targa; |
| 47 | +use Std\Lib\Image\Webp; | |
| 45 | 48 | |
| 46 | 49 | class RGB { |
| 47 | 50 | public int $r; |
| @@ -65,11 +68,13 @@ | ||
| 65 | 68 | public int $bytes = 0; |
| 66 | 69 | public \stdClass $fullInfo; |
| 67 | 70 | |
| 68 | - private array $supported = [ | |
| 69 | - 'image/gif' => Gif::class, | |
| 70 | - 'image/jpeg' => Jpeg::class, | |
| 71 | - 'image/png' => Png::class, | |
| 72 | - 'image/x-tga' => Targa::class, | |
| 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, | |
| 73 | 78 | ]; |
| 74 | 79 | |
| 75 | 80 | public function __construct(string $file) { |
| @@ -80,7 +85,7 @@ | ||
| 80 | 85 | if (!file_exists($file)) return; |
| 81 | 86 | $fp = fopen($file, 'rb'); |
| 82 | 87 | if (!$fp) return; |
| 83 | - $extBytes = fread($fp, 15); | |
| 88 | + $extBytes = (string)fread($fp, 32); | |
| 84 | 89 | fclose($fp); |
| 85 | 90 | |
| 86 | 91 | $this->bytes = filesize($file); |
| @@ -88,14 +93,9 @@ | ||
| 88 | 93 | $this->extension = pathinfo($file, PATHINFO_EXTENSION); |
| 89 | 94 | $this->filename = str_replace('.'.$this->extension, '', array_last(explode('/', $file))); |
| 90 | 95 | |
| 91 | - if (substr($extBytes, 0, 3) === "\xff\xd8\xff") $this->type = 'image/jpeg'; | |
| 92 | - 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'; | |
| 93 | - else if (substr($extBytes, 0, 3) === "\x42\x4d\x36") $this->type = 'image/bmp'; | |
| 94 | - else if (substr($extBytes, 0, 6) === "\x89\x50\x4e\x47\x0d\x0a") $this->type = 'image/png'; | |
| 95 | - 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'; | |
| 96 | - else if (str_ends_with($file, '.tga')) $this->type = 'image/x-tga'; | |
| 97 | - | |
| 98 | - $this->fullInfo = $this->createHandler($this->type)->parse($file); | |
| 96 | + $this->type = self::detect($extBytes, $file) ?? 'UNSUPPORTED'; | |
| 97 | + | |
| 98 | + $this->fullInfo = self::handler($this->type)->parse($file); | |
| 99 | 99 | $this->fullInfo->type = $this->type; |
| 100 | 100 | $this->fullInfo->size = $this->size; |
| 101 | 101 | $this->fullInfo->bytes = $this->bytes; |
| @@ -105,11 +105,51 @@ | ||
| 105 | 105 | } |
| 106 | 106 | } |
| 107 | 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 | + | |
| 108 | 148 | /////////////// |
| 109 | 149 | |
| 110 | - private function createHandler(string $ext): ImageInterface { | |
| 111 | - $class = $this->supported[$ext] ?? null; | |
| 112 | - if (!$class) throw new \RuntimeException("形式のハンドルが存在しない: {$ext}"); | |
| 150 | + private static function handler(string $type): ImageInterface { | |
| 151 | + $class = self::SUPPORTED[$type] ?? null; | |
| 152 | + if (!$class) throw new \RuntimeException("形式のハンドルが存在しない: {$type}"); | |
| 113 | 153 | return new $class(); |
| 114 | 154 | } |
| 115 | 155 |
| @@ -0,0 +1,175 @@ | ||
| 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\Image; | |
| 39 | + | |
| 40 | +/** | |
| 41 | + * 画素データ(RGBA、各8ビット、左上から行の順) | |
| 42 | + * | |
| 43 | + * 全ての形式はこれに読み込み(decode)、これから書き出す(encode)。 | |
| 44 | + * アニメーション(APNG・GIF・WebP)はframesに、画面と同じ大きさに合成したフレームが入る。 | |
| 45 | + */ | |
| 46 | +final class Bitmap { | |
| 47 | + /** 読み込める最大の画素数(RGBAで約100MiB。それ以上はメモリが足りない事が多い) */ | |
| 48 | + public const MAX_PIXELS = 25000000; | |
| 49 | + | |
| 50 | + public int $width; | |
| 51 | + public int $height; | |
| 52 | + /** 画素(1画素4バイト:R, G, B, A) */ | |
| 53 | + public string $data; | |
| 54 | + /** アニメーションのフレーム([['bitmap' => Bitmap, 'delay' => ミリ秒], ...])。静止画は空 */ | |
| 55 | + public array $frames = []; | |
| 56 | + /** アニメーションの繰り返し回数(0 = 無限) */ | |
| 57 | + public int $loops = 0; | |
| 58 | + | |
| 59 | + public function __construct(int $width, int $height, ?string $data = null) { | |
| 60 | + self::check($width, $height); | |
| 61 | + $this->width = $width; | |
| 62 | + $this->height = $height; | |
| 63 | + $this->data = $data ?? str_repeat("\0\0\0\0", $width * $height); | |
| 64 | + if (strlen($this->data) !== $width * $height * 4) throw new \InvalidArgumentException('画素データの大きさが合いません。'); | |
| 65 | + } | |
| 66 | + | |
| 67 | + /** | |
| 68 | + * 画像の大きさを確認する(読み込む前に、壊れた・大き過ぎるファイルで落ちない様に) | |
| 69 | + */ | |
| 70 | + public static function check(int $width, int $height): void { | |
| 71 | + if ($width < 1 || $height < 1) throw new \RuntimeException("画像の大きさが不正です({$width}x{$height})。"); | |
| 72 | + if ($width * $height > self::MAX_PIXELS) throw new \RuntimeException("画像が大き過ぎます({$width}x{$height})。"); | |
| 73 | + } | |
| 74 | + | |
| 75 | + /** 1色で塗った画像 */ | |
| 76 | + public static function fill(int $width, int $height, int $r, int $g, int $b, int $a = 255): self { | |
| 77 | + self::check($width, $height); | |
| 78 | + return new self($width, $height, str_repeat(chr($r).chr($g).chr($b).chr($a), $width * $height)); | |
| 79 | + } | |
| 80 | + | |
| 81 | + /** | |
| 82 | + * 画素を読む | |
| 83 | + * | |
| 84 | + * @return array [r, g, b, a] | |
| 85 | + */ | |
| 86 | + public function get(int $x, int $y): array { | |
| 87 | + $o = ($y * $this->width + $x) * 4; | |
| 88 | + return [ord($this->data[$o]), ord($this->data[$o + 1]), ord($this->data[$o + 2]), ord($this->data[$o + 3])]; | |
| 89 | + } | |
| 90 | + | |
| 91 | + public function set(int $x, int $y, int $r, int $g, int $b, int $a = 255): void { | |
| 92 | + $o = ($y * $this->width + $x) * 4; | |
| 93 | + $this->data[$o] = chr($r); | |
| 94 | + $this->data[$o + 1] = chr($g); | |
| 95 | + $this->data[$o + 2] = chr($b); | |
| 96 | + $this->data[$o + 3] = chr($a); | |
| 97 | + } | |
| 98 | + | |
| 99 | + /** 1行分の画素(RGBA) */ | |
| 100 | + public function row(int $y): string { | |
| 101 | + return substr($this->data, $y * $this->width * 4, $this->width * 4); | |
| 102 | + } | |
| 103 | + | |
| 104 | + /** 全ての画素が不透明か */ | |
| 105 | + public function isOpaque(): bool { | |
| 106 | + $len = strlen($this->data); | |
| 107 | + for ($i = 3; $i < $len; $i += 4) if ($this->data[$i] !== "\xff") return false; | |
| 108 | + return true; | |
| 109 | + } | |
| 110 | + | |
| 111 | + /** 全ての画素が灰色(R = G = B)か */ | |
| 112 | + public function isGray(): bool { | |
| 113 | + $len = strlen($this->data); | |
| 114 | + for ($i = 0; $i < $len; $i += 4) { | |
| 115 | + if ($this->data[$i] !== $this->data[$i + 1] || $this->data[$i] !== $this->data[$i + 2]) return false; | |
| 116 | + } | |
| 117 | + return true; | |
| 118 | + } | |
| 119 | + | |
| 120 | + /** | |
| 121 | + * 別の画像を重ねる(はみ出した部分は切る) | |
| 122 | + * | |
| 123 | + * @param bool $blend true:アルファで合成(over)、false:そのまま置き換える(source) | |
| 124 | + */ | |
| 125 | + public function draw(Bitmap $src, int $dx, int $dy, bool $blend = true): void { | |
| 126 | + $x0 = max(0, $dx); | |
| 127 | + $x1 = min($this->width, $dx + $src->width); | |
| 128 | + if ($x0 >= $x1) return; | |
| 129 | + for ($sy = max(0, -$dy); $sy < $src->height && $sy + $dy < $this->height; $sy++) { | |
| 130 | + $line = substr($src->data, ($sy * $src->width + $x0 - $dx) * 4, ($x1 - $x0) * 4); | |
| 131 | + $at = (($sy + $dy) * $this->width + $x0) * 4; | |
| 132 | + if ($blend) $line = self::over(substr($this->data, $at, strlen($line)), $line); | |
| 133 | + $this->data = substr_replace($this->data, $line, $at, strlen($line)); | |
| 134 | + } | |
| 135 | + } | |
| 136 | + | |
| 137 | + /** 範囲を1色にする */ | |
| 138 | + public function clear(int $x, int $y, int $w, int $h, int $r = 0, int $g = 0, int $b = 0, int $a = 0): void { | |
| 139 | + $this->draw(self::fill(max(1, $w), max(1, $h), $r, $g, $b, $a), $x, $y, false); | |
| 140 | + } | |
| 141 | + | |
| 142 | + /** | |
| 143 | + * RGBAの列のアルファ合成($fgを$bgの上に) | |
| 144 | + */ | |
| 145 | + public static function over(string $bg, string $fg): string { | |
| 146 | + // 殆どの画素は完全に不透明か透明なので、その場合は計算しない | |
| 147 | + $len = strlen($fg); | |
| 148 | + $out = $bg; | |
| 149 | + for ($i = 0; $i < $len; $i += 4) { | |
| 150 | + $fa = ord($fg[$i + 3]); | |
| 151 | + if ($fa === 255) { | |
| 152 | + $out[$i] = $fg[$i]; | |
| 153 | + $out[$i + 1] = $fg[$i + 1]; | |
| 154 | + $out[$i + 2] = $fg[$i + 2]; | |
| 155 | + $out[$i + 3] = "\xff"; | |
| 156 | + } else if ($fa !== 0) { | |
| 157 | + $ba = ord($bg[$i + 3]); | |
| 158 | + $oa = $fa + intdiv($ba * (255 - $fa) + 127, 255); | |
| 159 | + for ($c = 0; $c < 3; $c++) { | |
| 160 | + $v = (ord($fg[$i + $c]) * $fa * 255 + ord($bg[$i + $c]) * $ba * (255 - $fa)) / (255 * $oa); | |
| 161 | + $out[$i + $c] = chr((int)round($v)); | |
| 162 | + } | |
| 163 | + $out[$i + 3] = chr($oa); | |
| 164 | + } | |
| 165 | + } | |
| 166 | + return $out; | |
| 167 | + } | |
| 168 | + | |
| 169 | + public function copy(): self { | |
| 170 | + $b = new self($this->width, $this->height, $this->data); | |
| 171 | + $b->frames = $this->frames; | |
| 172 | + $b->loops = $this->loops; | |
| 173 | + return $b; | |
| 174 | + } | |
| 175 | +} |
| @@ -0,0 +1,357 @@ | ||
| 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\Image; | |
| 39 | + | |
| 40 | +/** | |
| 41 | + * BMP(Windows・OS/2) | |
| 42 | + * | |
| 43 | + * ヘッダー:OS/2 1.x(12バイト)、OS/2 2.x(16~64バイト)、BITMAPINFOHEADER(40)、V2(52)、V3(56)、V4(108)、V5(124) | |
| 44 | + * 画素:1・2・4・8ビット(パレット)、16・24・32ビット(既定の並び又はビットフィールド) | |
| 45 | + * 圧縮:無し、RLE8、RLE4、BITFIELDS、ALPHABITFIELDS、OS/2のRLE24、埋め込んだPNG・JPEG | |
| 46 | + * 下から上(既定)と上から下(丈が負) | |
| 47 | + */ | |
| 48 | +class Bmp implements ImageInterface { | |
| 49 | + private const RGB = 0, RLE8 = 1, RLE4 = 2, BITFIELDS = 3, JPEG = 4, PNG = 5, ALPHABITFIELDS = 6; | |
| 50 | + | |
| 51 | + public function parse(string $file): \stdClass { | |
| 52 | + $h = $this->header((string)file_get_contents($file, false, null, 0, 1024)); | |
| 53 | + return (object)[ | |
| 54 | + 'width' => $h['width'], | |
| 55 | + 'height' => $h['height'], | |
| 56 | + 'bitDepth' => $h['bits'], | |
| 57 | + 'headerSize' => $h['dib'], | |
| 58 | + 'compression' => $h['comp'], | |
| 59 | + 'topDown' => $h['topDown'], | |
| 60 | + 'colors' => $h['colors'], | |
| 61 | + 'hasAlpha' => $h['masks'][3] !== 0, | |
| 62 | + ]; | |
| 63 | + } | |
| 64 | + | |
| 65 | + /** | |
| 66 | + * ヘッダーを読む | |
| 67 | + */ | |
| 68 | + private function header(string $d): array { | |
| 69 | + if (strlen($d) < 26 || substr($d, 0, 2) !== 'BM') throw new \RuntimeException('BMPではありません。'); | |
| 70 | + $offset = unpack('V', substr($d, 10, 4))[1]; | |
| 71 | + $dib = unpack('V', substr($d, 14, 4))[1]; | |
| 72 | + if (strlen($d) < 14 + min($dib, 64)) throw new \RuntimeException('BMPのヘッダーが途中で切れています。'); | |
| 73 | + $os2 = false; | |
| 74 | + if ($dib === 12) { | |
| 75 | + // OS/2 1.x:大きさは16ビット、パレットは3バイト | |
| 76 | + $h = unpack('vwidth/vheight/vplanes/vbits', substr($d, 18, 8)); | |
| 77 | + $h += ['comp' => self::RGB, 'colors' => 0]; | |
| 78 | + $os2 = true; | |
| 79 | + } else if ($dib >= 16) { | |
| 80 | + $h = unpack('lwidth/lheight/vplanes/vbits', substr($d, 18, 12)); | |
| 81 | + $more = $dib >= 40 ? unpack('Vcomp/Vsize/lxppm/lyppm/Vcolors', substr($d, 30, 20)) : ['comp' => 0, 'colors' => 0]; | |
| 82 | + $h += $more; | |
| 83 | + // OS/2 2.xは圧縮の番号が違う(3 = ハフマン、4 = RLE24) | |
| 84 | + $os2 = $dib === 16 || $dib === 64; | |
| 85 | + } else { | |
| 86 | + throw new \RuntimeException("未対応のBMPのヘッダーです({$dib}バイト)。"); | |
| 87 | + } | |
| 88 | + $topDown = $h['height'] < 0; | |
| 89 | + $h['height'] = abs($h['height']); | |
| 90 | + Bitmap::check($h['width'], $h['height']); | |
| 91 | + if (!in_array($h['bits'], [1, 2, 4, 8, 16, 24, 32], true) && !in_array($h['comp'], [self::JPEG, self::PNG], true)) { | |
| 92 | + throw new \RuntimeException("未対応のBMPのビット深さです({$h['bits']})。"); | |
| 93 | + } | |
| 94 | + | |
| 95 | + // ビットフィールド:V2以上はヘッダーの中、40バイトのヘッダーは後ろに続く | |
| 96 | + $masks = [0, 0, 0, 0]; | |
| 97 | + $palAt = 14 + $dib; | |
| 98 | + if ($h['comp'] === self::BITFIELDS && !$os2 || $h['comp'] === self::ALPHABITFIELDS) { | |
| 99 | + $n = $h['comp'] === self::ALPHABITFIELDS ? 4 : 3; | |
| 100 | + if ($dib >= 52) { | |
| 101 | + $masks = array_values(unpack('V4', substr($d, 54, 16) ?: str_repeat("\0", 16))); | |
| 102 | + if ($dib < 56) $masks[3] = 0; | |
| 103 | + } else { | |
| 104 | + $masks = array_values(unpack("V{$n}", substr($d, $palAt, $n * 4))); | |
| 105 | + if ($n === 3) $masks[] = 0; | |
| 106 | + $palAt += $n * 4; | |
| 107 | + } | |
| 108 | + } else if ($h['bits'] === 16) { | |
| 109 | + $masks = [0x7c00, 0x03e0, 0x001f, 0]; | |
| 110 | + } else if ($h['bits'] === 32) { | |
| 111 | + $masks = [0xff0000, 0xff00, 0xff, 0]; | |
| 112 | + // V3以上でアルファのマスクがあれば使う | |
| 113 | + if ($dib >= 56) $masks[3] = unpack('V', substr($d, 66, 4))[1]; | |
| 114 | + } | |
| 115 | + | |
| 116 | + $entry = $dib === 12 ? 3 : 4; | |
| 117 | + $colors = $h['bits'] <= 8 ? ($h['colors'] ?: 1 << $h['bits']) : 0; | |
| 118 | + return [ | |
| 119 | + 'offset' => $offset, 'dib' => $dib, 'width' => $h['width'], 'height' => $h['height'], 'bits' => $h['bits'], | |
| 120 | + 'comp' => $h['comp'], 'os2' => $os2, 'topDown' => $topDown, 'masks' => $masks, | |
| 121 | + 'colors' => min($colors, 256), 'palAt' => $palAt, 'entry' => $entry, | |
| 122 | + ]; | |
| 123 | + } | |
| 124 | + | |
| 125 | + public function decode(string $data): Bitmap { | |
| 126 | + $h = $this->header($data); | |
| 127 | + $w = $h['width']; | |
| 128 | + $ht = $h['height']; | |
| 129 | + $comp = $h['comp']; | |
| 130 | + $pixels = substr($data, $h['offset']); | |
| 131 | + | |
| 132 | + // 埋め込んだPNG・JPEG(OS/2の4はRLE24) | |
| 133 | + if ($comp === self::PNG && !$h['os2']) return (new Png())->decode($pixels); | |
| 134 | + if ($comp === self::JPEG && !$h['os2']) return (new Jpeg())->decode($pixels); | |
| 135 | + if ($h['os2'] && $comp === 3) throw new \RuntimeException('OS/2のハフマン圧縮のBMPは未対応です。'); | |
| 136 | + | |
| 137 | + // パレット => RGBA | |
| 138 | + $lut = null; | |
| 139 | + if ($h['bits'] <= 8) { | |
| 140 | + $lut = []; | |
| 141 | + for ($i = 0; $i < 256; $i++) { | |
| 142 | + $e = $i < $h['colors'] ? substr($data, $h['palAt'] + $i * $h['entry'], 3) : ''; | |
| 143 | + $lut[chr($i)] = strlen($e) === 3 ? $e[2].$e[1].$e[0]."\xff" : "\0\0\0\xff"; | |
| 144 | + } | |
| 145 | + } | |
| 146 | + | |
| 147 | + if ($comp === self::RLE8 || $comp === self::RLE4 || ($h['os2'] && $comp === 4)) { | |
| 148 | + $rows = $this->rle($pixels, $w, $ht, $comp === self::RLE4 ? 4 : ($comp === self::RLE8 ? 8 : 24), $lut); | |
| 149 | + } else { | |
| 150 | + $stride = (($h['bits'] * $w + 31) >> 5) * 4; | |
| 151 | + if (strlen($pixels) < $stride * $ht) throw new \RuntimeException('BMPの画素データが足りません。'); | |
| 152 | + $rows = []; | |
| 153 | + $alphaSeen = false; | |
| 154 | + for ($y = 0; $y < $ht; $y++) { | |
| 155 | + $row = substr($pixels, $y * $stride, $stride); | |
| 156 | + $rows[] = $this->row($row, $w, $h, $lut); | |
| 157 | + } | |
| 158 | + // 32ビットでアルファのマスクが無い場合:全部0なら不透明(Windowsはアルファを使わない)、それ以外は使う | |
| 159 | + if ($h['bits'] === 32 && $h['masks'][3] === 0 && $comp !== self::BITFIELDS) { | |
| 160 | + foreach ($rows as $y => $r) { | |
| 161 | + $raw = substr($pixels, $y * $stride, $w * 4); | |
| 162 | + for ($i = 3; $i < $w * 4; $i += 4) if ($raw[$i] !== "\0") { | |
| 163 | + $alphaSeen = true; | |
| 164 | + break 2; | |
| 165 | + } | |
| 166 | + } | |
| 167 | + if ($alphaSeen) { | |
| 168 | + foreach ($rows as $y => $r) { | |
| 169 | + $raw = substr($pixels, $y * $stride, $w * 4); | |
| 170 | + for ($i = 3; $i < $w * 4; $i += 4) $r[$i] = $raw[$i]; | |
| 171 | + $rows[$y] = $r; | |
| 172 | + } | |
| 173 | + } | |
| 174 | + } | |
| 175 | + } | |
| 176 | + if (!$h['topDown']) $rows = array_reverse($rows); | |
| 177 | + return new Bitmap($w, $ht, implode('', $rows)); | |
| 178 | + } | |
| 179 | + | |
| 180 | + /** | |
| 181 | + * 圧縮していない1行をRGBAにする | |
| 182 | + */ | |
| 183 | + private function row(string $row, int $w, array $h, ?array $lut): string { | |
| 184 | + $bits = $h['bits']; | |
| 185 | + if ($lut !== null) { | |
| 186 | + if ($bits < 8) { | |
| 187 | + static $unpack = []; | |
| 188 | + if (!isset($unpack[$bits])) { | |
| 189 | + $n = 8 / $bits; | |
| 190 | + $mask = (1 << $bits) - 1; | |
| 191 | + for ($b = 0; $b < 256; $b++) { | |
| 192 | + $s = ''; | |
| 193 | + for ($k = $n - 1; $k >= 0; $k--) $s .= chr(($b >> ($k * $bits)) & $mask); | |
| 194 | + $unpack[$bits][chr($b)] = $s; | |
| 195 | + } | |
| 196 | + } | |
| 197 | + $row = strtr($row, $unpack[$bits]); | |
| 198 | + } | |
| 199 | + return strtr(substr($row, 0, $w), $lut); | |
| 200 | + } | |
| 201 | + if ($bits === 24) return preg_replace('/(.)(.)(.)/s', '$3$2$1'."\xff", substr($row, 0, $w * 3)); | |
| 202 | + $masks = $h['masks']; | |
| 203 | + if ($bits === 32 && $masks === [0xff0000, 0xff00, 0xff, 0]) return preg_replace('/(.)(.)(.)./s', '$3$2$1'."\xff", substr($row, 0, $w * 4)); | |
| 204 | + if ($bits === 32 && $masks === [0xff0000, 0xff00, 0xff, 0xff000000]) return preg_replace('/(.)(.)(.)(.)/s', '$3$2$1$4', substr($row, 0, $w * 4)); | |
| 205 | + | |
| 206 | + // ビットフィールド(16・32ビット) | |
| 207 | + static $cache = []; | |
| 208 | + $key = implode(',', $masks); | |
| 209 | + if (!isset($cache[$key])) { | |
| 210 | + $cache[$key] = array_map(function($m) { | |
| 211 | + if ($m === 0) return null; | |
| 212 | + $shift = 0; | |
| 213 | + while ((($m >> $shift) & 1) === 0) $shift++; | |
| 214 | + $max = $m >> $shift; | |
| 215 | + return [$shift, $max]; | |
| 216 | + }, $masks); | |
| 217 | + } | |
| 218 | + $f = $cache[$key]; | |
| 219 | + $vals = array_values(unpack($bits === 16 ? 'v*' : 'V*', substr($row, 0, $w * ($bits >> 3)))); | |
| 220 | + $out = ''; | |
| 221 | + foreach ($vals as $v) { | |
| 222 | + for ($c = 0; $c < 4; $c++) { | |
| 223 | + if ($f[$c] === null) { | |
| 224 | + $out .= $c === 3 ? "\xff" : "\0"; | |
| 225 | + continue; | |
| 226 | + } | |
| 227 | + [$shift, $max] = $f[$c]; | |
| 228 | + $out .= chr(intdiv((($v >> $shift) & $max) * 255 + ($max >> 1), $max)); | |
| 229 | + } | |
| 230 | + } | |
| 231 | + return $out; | |
| 232 | + } | |
| 233 | + | |
| 234 | + /** | |
| 235 | + * RLE8・RLE4・RLE24(OS/2)を展開する。飛ばした画素(デルタ、行・画像の終わり)は透明 | |
| 236 | + * | |
| 237 | + * @return array 行(下から上) | |
| 238 | + */ | |
| 239 | + private function rle(string $d, int $w, int $h, int $bits, ?array $lut): array { | |
| 240 | + $rows = array_fill(0, $h, array_fill(0, $w, "\0\0\0\0")); | |
| 241 | + $len = strlen($d); | |
| 242 | + $p = 0; | |
| 243 | + $x = $y = 0; | |
| 244 | + $px = function(string $idx) use ($lut, $bits): string { | |
| 245 | + return $bits === 24 ? $idx[2].$idx[1].$idx[0]."\xff" : $lut[$idx]; | |
| 246 | + }; | |
| 247 | + while ($p + 1 < $len && $y < $h) { | |
| 248 | + $n = ord($d[$p]); | |
| 249 | + $c = $d[$p + 1]; | |
| 250 | + $p += 2; | |
| 251 | + if ($n > 0) { | |
| 252 | + // 繰り返し | |
| 253 | + if ($bits === 24) { | |
| 254 | + $c = substr($d, $p - 1, 3); | |
| 255 | + $p += 1; | |
| 256 | + $v = $px($c); | |
| 257 | + for ($i = 0; $i < $n && $x < $w; $i++) $rows[$y][$x++] = $v; | |
| 258 | + } else if ($bits === 8) { | |
| 259 | + $v = $px($c); | |
| 260 | + for ($i = 0; $i < $n && $x < $w; $i++) $rows[$y][$x++] = $v; | |
| 261 | + } else { | |
| 262 | + $hi = $px(chr(ord($c) >> 4)); | |
| 263 | + $lo = $px(chr(ord($c) & 15)); | |
| 264 | + for ($i = 0; $i < $n && $x < $w; $i++) $rows[$y][$x++] = $i % 2 ? $lo : $hi; | |
| 265 | + } | |
| 266 | + continue; | |
| 267 | + } | |
| 268 | + $cmd = ord($c); | |
| 269 | + if ($cmd === 0) { | |
| 270 | + $x = 0; | |
| 271 | + $y++; | |
| 272 | + } else if ($cmd === 1) { | |
| 273 | + break; | |
| 274 | + } else if ($cmd === 2) { | |
| 275 | + if ($p + 1 >= $len) break; | |
| 276 | + $x += ord($d[$p]); | |
| 277 | + $y += ord($d[$p + 1]); | |
| 278 | + $p += 2; | |
| 279 | + } else { | |
| 280 | + // そのままの画素(2バイト境界に揃える) | |
| 281 | + $bytes = $bits === 24 ? $cmd * 3 : ($bits === 8 ? $cmd : ($cmd + 1) >> 1); | |
| 282 | + $chunk = substr($d, $p, $bytes); | |
| 283 | + $p += $bytes + ($bytes & 1); | |
| 284 | + for ($i = 0; $i < $cmd && $x < $w; $i++) { | |
| 285 | + if ($bits === 24) $v = $px(substr($chunk, $i * 3, 3)); | |
| 286 | + else if ($bits === 8) $v = $px($chunk[$i] ?? "\0"); | |
| 287 | + else { | |
| 288 | + $b = ord($chunk[$i >> 1] ?? "\0"); | |
| 289 | + $v = $px(chr($i % 2 ? $b & 15 : $b >> 4)); | |
| 290 | + } | |
| 291 | + $rows[$y][$x++] = $v; | |
| 292 | + } | |
| 293 | + } | |
| 294 | + } | |
| 295 | + return array_map(fn($r) => implode('', $r), $rows); | |
| 296 | + } | |
| 297 | + | |
| 298 | + /** | |
| 299 | + * 画素をBMPにする | |
| 300 | + * | |
| 301 | + * 不透明で256色以下なら1・4・8ビットのパレット、不透明なら24ビット、 | |
| 302 | + * 透明があれば32ビット(BITMAPV4HEADERのアルファのマスク)。下から上の順。 | |
| 303 | + */ | |
| 304 | + public function encode(Bitmap $img, array $opt = []): string { | |
| 305 | + $w = $img->width; | |
| 306 | + $h = $img->height; | |
| 307 | + $opaque = $img->isOpaque(); | |
| 308 | + $colors = []; | |
| 309 | + if ($opaque) { | |
| 310 | + for ($y = 0; $y < $h && count($colors) <= 256; $y++) $colors += array_flip(str_split($img->row($y), 4)); | |
| 311 | + } | |
| 312 | + | |
| 313 | + $palette = ''; | |
| 314 | + if ($opaque && count($colors) <= 256) { | |
| 315 | + $n = count($colors); | |
| 316 | + $bits = $n <= 2 ? 1 : ($n <= 16 ? 4 : 8); | |
| 317 | + $map = []; | |
| 318 | + foreach (array_keys($colors) as $i => $c) { | |
| 319 | + $map[$c] = chr($i); | |
| 320 | + $palette .= $c[2].$c[1].$c[0]."\0"; | |
| 321 | + } | |
| 322 | + $dib = pack('VllvvVVllVV', 40, $w, $h, 1, $bits, self::RGB, 0, 2835, 2835, $n, 0); | |
| 323 | + } else if ($opaque) { | |
| 324 | + $bits = 24; | |
| 325 | + $dib = pack('VllvvVVllVV', 40, $w, $h, 1, 24, self::RGB, 0, 2835, 2835, 0, 0); | |
| 326 | + } else { | |
| 327 | + $bits = 32; | |
| 328 | + // V4:ビットフィールド(BGRA)+sRGB | |
| 329 | + $dib = pack('VllvvVVllVV', 108, $w, $h, 1, 32, self::BITFIELDS, 0, 2835, 2835, 0, 0) | |
| 330 | + .pack('VVVV', 0x00ff0000, 0x0000ff00, 0x000000ff, 0xff000000).'BGRs'.str_repeat("\0", 48); | |
| 331 | + } | |
| 332 | + | |
| 333 | + $stride = (($bits * $w + 31) >> 5) * 4; | |
| 334 | + $body = ''; | |
| 335 | + for ($y = $h - 1; $y >= 0; $y--) { | |
| 336 | + $row = $img->row($y); | |
| 337 | + $line = match ($bits) { | |
| 338 | + 24 => preg_replace('/(.)(.)(.)./s', '$3$2$1', $row), | |
| 339 | + 32 => preg_replace('/(.)(.)(.)(.)/s', '$3$2$1$4', $row), | |
| 340 | + default => strtr($row, $map), | |
| 341 | + }; | |
| 342 | + if ($bits < 8) { | |
| 343 | + $per = 8 / $bits; | |
| 344 | + $packed = ''; | |
| 345 | + foreach (str_split($line, $per) as $g) { | |
| 346 | + $b = 0; | |
| 347 | + for ($k = 0; $k < $per; $k++) $b = ($b << $bits) | ($k < strlen($g) ? ord($g[$k]) : 0); | |
| 348 | + $packed .= chr($b); | |
| 349 | + } | |
| 350 | + $line = $packed; | |
| 351 | + } | |
| 352 | + $body .= str_pad($line, $stride, "\0"); | |
| 353 | + } | |
| 354 | + $offset = 14 + strlen($dib) + strlen($palette); | |
| 355 | + return 'BM'.pack('VvvV', $offset + strlen($body), 0, 0, $offset).$dib.$palette.$body; | |
| 356 | + } | |
| 357 | +} |
| @@ -37,91 +37,468 @@ | ||
| 37 | 37 | */ |
| 38 | 38 | namespace Std\Lib\Image; |
| 39 | 39 | |
| 40 | -use Std\Lib\Image\ImageInterface; | |
| 41 | - | |
| 40 | +/** | |
| 41 | + * GIF(87a・89a) | |
| 42 | + * | |
| 43 | + * 読み込み:LZW(12ビット、クリア、遅延クリア)、インターレース、全体・ローカルのパレット、透明色、 | |
| 44 | + * アニメーション(グラフィック制御拡張の遅延・廃棄方法、NETSCAPE2.0の繰り返し回数)。 | |
| 45 | + * 書き出し:256色以下はそのまま、それ以上はメディアンカットで減色(任意で誤差拡散)。framesがあればアニメーション。 | |
| 46 | + */ | |
| 42 | 47 | class Gif implements ImageInterface { |
| 43 | - public int $width = 0; | |
| 44 | - public int $height = 0; | |
| 45 | - public int $bitDepth = 0; | |
| 46 | - public int $numColors = 0; | |
| 47 | - public string $compression = 'NONE'; | |
| 48 | - public int $interlace = 0; | |
| 49 | - public int $bgColor = 0; | |
| 50 | - public float $delayTime = 0; | |
| 51 | - public array $globalColorTable = []; | |
| 52 | - public int $numTransparentPixels = 0; | |
| 53 | - public int $lzwMinCodeSize = 0; | |
| 54 | - public string $imageData = ''; | |
| 48 | + public function parse(string $file): \stdClass { | |
| 49 | + $d = (string)file_get_contents($file); | |
| 50 | + $s = $this->scan($d, false); | |
| 51 | + $first = $s['images'][0] ?? null; | |
| 52 | + return (object)[ | |
| 53 | + 'version' => substr($d, 3, 3), | |
| 54 | + 'width' => $s['width'], | |
| 55 | + 'height' => $s['height'], | |
| 56 | + 'bitDepth' => $s['depth'], | |
| 57 | + 'numColors' => 1 << $s['depth'], | |
| 58 | + 'globalColors' => strlen($s['gct']) / 3, | |
| 59 | + 'bgColor' => $s['bg'], | |
| 60 | + 'frames' => count($s['images']), | |
| 61 | + 'loops' => $s['loops'], | |
| 62 | + 'delayTime' => $first ? $first['delay'] / 100.0 : 0, | |
| 63 | + 'hasAlpha' => $first !== null && $first['transparent'] !== null, | |
| 64 | + 'interlace' => $first !== null && $first['interlace'], | |
| 65 | + 'compression' => 'LZW', | |
| 66 | + ]; | |
| 67 | + } | |
| 55 | 68 | |
| 56 | 69 | /** |
| 57 | - * TODO: 現在、インタレースした画像が未対応です。 | |
| 70 | + * ブロックを読む | |
| 71 | + * | |
| 72 | + * @param bool $decode 画像のLZWを展開するか(falseなら大きさ等だけ) | |
| 58 | 73 | */ |
| 59 | - public function parse(string $file): \stdClass { | |
| 60 | - $gif = new \stdClass(); | |
| 61 | - $fp = fopen($file, 'rb'); | |
| 62 | - $magic = fread($fp, 6); // マジックのスキップ | |
| 74 | + private function scan(string $d, bool $decode): array { | |
| 75 | + $v = substr($d, 0, 6); | |
| 76 | + if ($v !== 'GIF87a' && $v !== 'GIF89a') throw new \RuntimeException('GIFではありません。'); | |
| 77 | + if (strlen($d) < 13) throw new \RuntimeException('GIFのヘッダーが途中で切れています。'); | |
| 78 | + $h = unpack('vw/vh/Cflags/Cbg/Caspect', substr($d, 6, 7)); | |
| 79 | + Bitmap::check($h['w'], $h['h']); | |
| 80 | + $pos = 13; | |
| 81 | + $gct = ''; | |
| 82 | + if ($h['flags'] & 0x80) { | |
| 83 | + $gct = substr($d, $pos, 3 << (($h['flags'] & 7) + 1)); | |
| 84 | + $pos += strlen($gct); | |
| 85 | + } | |
| 86 | + $len = strlen($d); | |
| 87 | + $images = []; | |
| 88 | + $gce = null; | |
| 89 | + $loops = 1; // NETSCAPE2.0が無ければ1回 | |
| 90 | + while ($pos < $len) { | |
| 91 | + $b = ord($d[$pos++]); | |
| 92 | + if ($b === 0x3b) break; | |
| 93 | + if ($b === 0x21) { | |
| 94 | + $label = ord($d[$pos++] ?? "\0"); | |
| 95 | + [$data, $pos] = $this->subBlocks($d, $pos); | |
| 96 | + if ($label === 0xf9 && strlen($data) >= 4) { | |
| 97 | + $g = unpack('Cflags/vdelay/Ctrans', $data); | |
| 98 | + $gce = ['dispose' => ($g['flags'] >> 2) & 7, 'delay' => $g['delay'], 'transparent' => $g['flags'] & 1 ? $g['trans'] : null]; | |
| 99 | + } else if ($label === 0xff && (str_starts_with($data, 'NETSCAPE2.0') || str_starts_with($data, 'ANIMEXTS1.0')) && strlen($data) >= 14 && ord($data[11]) === 1) { | |
| 100 | + // 繰り返し回数(0 = 無限)。ブラウザと同じく、最初の再生に加えてn回 | |
| 101 | + $n = unpack('v', substr($data, 12, 2))[1]; | |
| 102 | + $loops = $n === 0 ? 0 : $n + 1; | |
| 103 | + } | |
| 104 | + continue; | |
| 105 | + } | |
| 106 | + if ($b !== 0x2c) { | |
| 107 | + // 不明なブロック:ブラウザと同じく、そこで終わりにする | |
| 108 | + break; | |
| 109 | + } | |
| 110 | + if ($pos + 9 > $len) throw new \RuntimeException('GIFの画像記述子が途中で切れています。'); | |
| 111 | + $im = unpack('vx/vy/vw/vh/Cflags', substr($d, $pos, 9)); | |
| 112 | + $pos += 9; | |
| 113 | + $lct = ''; | |
| 114 | + if ($im['flags'] & 0x80) { | |
| 115 | + $lct = substr($d, $pos, 3 << (($im['flags'] & 7) + 1)); | |
| 116 | + $pos += strlen($lct); | |
| 117 | + } | |
| 118 | + $min = ord($d[$pos++] ?? "\0"); | |
| 119 | + [$data, $pos] = $this->subBlocks($d, $pos); | |
| 120 | + $images[] = [ | |
| 121 | + 'x' => $im['x'], 'y' => $im['y'], 'w' => $im['w'], 'h' => $im['h'], | |
| 122 | + 'interlace' => ($im['flags'] & 0x40) !== 0, | |
| 123 | + 'palette' => $lct !== '' ? $lct : $gct, | |
| 124 | + 'indices' => $decode ? $this->unlzw($data, $min, $im['w'] * $im['h']) : '', | |
| 125 | + 'dispose' => $gce['dispose'] ?? 0, | |
| 126 | + 'delay' => $gce['delay'] ?? 0, | |
| 127 | + 'transparent' => $gce['transparent'] ?? null, | |
| 128 | + ]; | |
| 129 | + $gce = null; | |
| 130 | + } | |
| 131 | + if (!$images) throw new \RuntimeException('GIFに画像がありません。'); | |
| 132 | + return ['width' => $h['w'], 'height' => $h['h'], 'gct' => $gct, 'bg' => $h['bg'], 'depth' => (($h['flags'] >> 4) & 7) + 1, 'images' => $images, 'loops' => $loops]; | |
| 133 | + } | |
| 63 | 134 | |
| 64 | - // v = リトルエンディアン(2ビット) | |
| 65 | - $this->width = unpack('v', fread($fp, 2))[1]; | |
| 66 | - $this->height = unpack('v', fread($fp, 2))[1]; | |
| 67 | - $packed = ord(fread($fp, 1)); | |
| 68 | - $this->bitDepth = ($packed & 0x07) + 1; | |
| 69 | - $this->numColors = 1 << $this->bitDepth; | |
| 70 | - $hasGct = $this->bitDepth === 8; | |
| 135 | + /** データのサブブロック(長さ+データ、長さ0で終わり)を繋げる */ | |
| 136 | + private function subBlocks(string $d, int $pos): array { | |
| 137 | + $out = ''; | |
| 138 | + $len = strlen($d); | |
| 139 | + while ($pos < $len) { | |
| 140 | + $n = ord($d[$pos++]); | |
| 141 | + if ($n === 0) break; | |
| 142 | + $out .= substr($d, $pos, $n); | |
| 143 | + $pos += $n; | |
| 144 | + } | |
| 145 | + return [$out, $pos]; | |
| 146 | + } | |
| 71 | 147 | |
| 72 | - $gif->width = $this->width; | |
| 73 | - $gif->height = $this->height; | |
| 74 | - $gif->bitDepth = $this->bitDepth; | |
| 75 | - $gif->numColors = $this->numColors; | |
| 76 | - if ($magic === 'GIF87a' || !$hasGct) return $gif; // それ以外情報がない | |
| 148 | + /** | |
| 149 | + * LZWを展開する | |
| 150 | + * | |
| 151 | + * @return string パレットの指標(1画素1バイト) | |
| 152 | + */ | |
| 153 | + private function unlzw(string $data, int $min, int $count): string { | |
| 154 | + if ($min < 2 || $min > 8) throw new \RuntimeException('GIFのLZWの最小符号長が不正です。'); | |
| 155 | + $clear = 1 << $min; | |
| 156 | + $eoi = $clear + 1; | |
| 157 | + $base = []; | |
| 158 | + for ($i = 0; $i < $clear; $i++) $base[] = chr($i); | |
| 159 | + $base[] = ''; | |
| 160 | + $base[] = ''; | |
| 161 | + $dict = $base; | |
| 162 | + $size = $min + 1; | |
| 163 | + $mask = (1 << $size) - 1; | |
| 164 | + $out = ''; | |
| 165 | + $outLen = 0; | |
| 166 | + $prev = null; | |
| 167 | + $buf = $bits = $pos = 0; | |
| 168 | + $len = strlen($data); | |
| 169 | + while ($outLen < $count) { | |
| 170 | + while ($bits < $size) { | |
| 171 | + if ($pos >= $len) break 2; | |
| 172 | + $buf |= ord($data[$pos++]) << $bits; | |
| 173 | + $bits += 8; | |
| 174 | + } | |
| 175 | + $code = $buf & $mask; | |
| 176 | + $buf >>= $size; | |
| 177 | + $bits -= $size; | |
| 77 | 178 | |
| 78 | - $this->bgColor = (int)bin2hex(fread($fp, 1)); | |
| 79 | - $gif->bgColor = $this->bgColor; | |
| 80 | - bin2hex(fread($fp, 1)); // いつでも 0:0? | |
| 81 | - | |
| 82 | - $gceLen = 0; | |
| 83 | - while (!feof($fp)) { | |
| 84 | - $skip = bin2hex(fread($fp, 3)); | |
| 85 | - $color = new RGB((int)($skip[0].$skip[1]), (int)($skip[2].$skip[3]), (int)($skip[4].$skip[5])); | |
| 86 | - $this->globalColorTable[] = $color->rgb; | |
| 87 | - if ($skip[0].$skip[1] === '21' && $skip[2].$skip[3] === 'f9') { | |
| 88 | - $gceLen = (int)($skip[4].$skip[5]); | |
| 179 | + if ($code === $clear) { | |
| 180 | + $dict = $base; | |
| 181 | + $size = $min + 1; | |
| 182 | + $mask = (1 << $size) - 1; | |
| 183 | + $prev = null; | |
| 184 | + continue; | |
| 185 | + } | |
| 186 | + if ($code === $eoi) break; | |
| 187 | + $n = count($dict); | |
| 188 | + if ($code < $n && $code !== $clear && $code !== $eoi) { | |
| 189 | + $entry = $dict[$code]; | |
| 190 | + } else if ($code === $n && $prev !== null) { | |
| 191 | + $entry = $prev.$prev[0]; | |
| 192 | + } else { | |
| 193 | + // 壊れたデータ:ブラウザと同じく、そこまでを表示する | |
| 89 | 194 | break; |
| 90 | 195 | } |
| 196 | + $out .= $entry; | |
| 197 | + $outLen += strlen($entry); | |
| 198 | + if ($prev !== null && $n < 4096) { | |
| 199 | + $dict[] = $prev.$entry[0]; | |
| 200 | + if ($n + 1 >= (1 << $size) && $size < 12) { | |
| 201 | + $size++; | |
| 202 | + $mask = (1 << $size) - 1; | |
| 203 | + } | |
| 204 | + } | |
| 205 | + $prev = $entry; | |
| 91 | 206 | } |
| 207 | + return $outLen >= $count ? substr($out, 0, $count) : $out; | |
| 208 | + } | |
| 92 | 209 | |
| 93 | - // GCE開始 | |
| 94 | - $this->hasAlpha = (ord(fread($fp, 1)) & 0x01) === 0x01; | |
| 95 | - $this->delayTime = (unpack('v', fread($fp, 2))[1]) / 100.0; | |
| 96 | - $this->numTransparentPixels = ord(fread($fp, 1)); | |
| 97 | - $gif->hasAlpha = $this->hasAlpha; | |
| 98 | - $gif->delayTime = $this->delayTime; | |
| 99 | - $gif->numTransparentPixels = $this->numTransparentPixels; | |
| 100 | - fread($fp, 1); // GCE終了 | |
| 101 | - fread($fp, 1); // 画像指定子開始 | |
| 102 | - fread($fp, 4); // 画面のXとY、いつでも(0, 0) | |
| 103 | - fread($fp, 4); // 画像のサイズ、関数の開始で既に保存した | |
| 104 | - $pack = ord(fread($fp, 1)); | |
| 105 | - $this->interlace = ($pack & 0x40); // 画像のサイズ、関数の開始で既に保存した | |
| 210 | + public function decode(string $data): Bitmap { | |
| 211 | + $s = $this->scan($data, true); | |
| 212 | + $w = $s['width']; | |
| 213 | + $h = $s['height']; | |
| 214 | + $canvas = new Bitmap($w, $h); | |
| 215 | + $img = null; | |
| 216 | + foreach ($s['images'] as $f) { | |
| 217 | + $frame = $this->frame($f); | |
| 218 | + $prevCanvas = $f['dispose'] === 3 ? $canvas->copy() : null; | |
| 219 | + $canvas->draw($frame, $f['x'], $f['y'], true); | |
| 220 | + if ($img === null) $img = $canvas->copy(); | |
| 221 | + $img->frames[] = ['bitmap' => $canvas->copy(), 'delay' => $f['delay'] * 10]; | |
| 222 | + // 廃棄方法:2 = 背景に戻す(ブラウザと同じく透明)、3 = 前に戻す | |
| 223 | + if ($f['dispose'] === 2) $canvas->clear($f['x'], $f['y'], $f['w'], $f['h']); | |
| 224 | + else if ($f['dispose'] === 3) $canvas = $prevCanvas; | |
| 225 | + } | |
| 226 | + if (count($img->frames) < 2) { | |
| 227 | + $img->frames = []; | |
| 228 | + } else { | |
| 229 | + $img->loops = $s['loops']; | |
| 230 | + } | |
| 231 | + return $img; | |
| 232 | + } | |
| 106 | 233 | |
| 107 | - $this->lzwMinCodeSize = ord(fread($fp, 1)); // 画像データ開始 | |
| 108 | - $gif->lzwMinCodeSize = $this->lzwMinCodeSize; | |
| 109 | - $imageData = ''; | |
| 110 | - while (true) { | |
| 111 | - $blockSize = ord(fread($fp, 1)); | |
| 112 | - if ($blockSize === 0) break; | |
| 113 | - $imageData .= fread($fp, 1); | |
| 234 | + /** | |
| 235 | + * 1枚の画像をRGBAにする(透明色は透明、足りない画素も透明) | |
| 236 | + */ | |
| 237 | + private function frame(array $f): Bitmap { | |
| 238 | + $lut = []; | |
| 239 | + $pal = $f['palette']; | |
| 240 | + $n = intdiv(strlen($pal), 3); | |
| 241 | + for ($i = 0; $i < 256; $i++) { | |
| 242 | + $lut[chr($i)] = $i === $f['transparent'] ? "\0\0\0\0" : ($i < $n ? substr($pal, $i * 3, 3)."\xff" : "\0\0\0\xff"); | |
| 114 | 243 | } |
| 244 | + $count = $f['w'] * $f['h']; | |
| 245 | + $px = strtr($f['indices'], $lut); | |
| 246 | + if (strlen($px) < $count * 4) $px .= str_repeat("\0\0\0\0", $count - intdiv(strlen($px), 4)); | |
| 247 | + $rows = str_split($px, $f['w'] * 4); | |
| 248 | + if ($f['interlace']) { | |
| 249 | + // 行の順:8行毎(0から)、8行毎(4から)、4行毎(2から)、2行毎(1から) | |
| 250 | + $order = []; | |
| 251 | + foreach ([[0, 8], [4, 8], [2, 4], [1, 2]] as [$start, $step]) for ($y = $start; $y < $f['h']; $y += $step) $order[] = $y; | |
| 252 | + $sorted = array_fill(0, $f['h'], ''); | |
| 253 | + foreach ($order as $i => $y) $sorted[$y] = $rows[$i]; | |
| 254 | + $rows = $sorted; | |
| 255 | + } | |
| 256 | + return new Bitmap($f['w'], $f['h'], implode('', $rows)); | |
| 257 | + } | |
| 115 | 258 | |
| 116 | - $this->imageData = base64_encode($imageData); | |
| 117 | - $this->colorType = 3; | |
| 118 | - $this->compression = 'LZW'; | |
| 119 | - $gif->colorType = $this->colorType; | |
| 120 | - $gif->compression = $this->compression; | |
| 121 | - $gif->globalColorTable = $this->globalColorTable; | |
| 122 | - $gif->imageData = $this->imageData; | |
| 259 | + /** | |
| 260 | + * 画素をGIFにする | |
| 261 | + * | |
| 262 | + * 透明(アルファ128未満)は1つの透明色にする。framesがあればアニメーション(全体のパレットを共有)。 | |
| 263 | + * | |
| 264 | + * @param array $opt dither:256色を超える場合に誤差拡散を使う(既定false) | |
| 265 | + */ | |
| 266 | + public function encode(Bitmap $img, array $opt = []): string { | |
| 267 | + $images = $img->frames ? array_column($img->frames, 'bitmap') : [$img]; | |
| 268 | + $w = $img->width; | |
| 269 | + $h = $img->height; | |
| 123 | 270 | |
| 124 | - fclose($fp); | |
| 125 | - return $gif; | |
| 271 | + // 色を数える(透明は1色) | |
| 272 | + $colors = []; | |
| 273 | + $hasAlpha = false; | |
| 274 | + foreach ($images as $f) { | |
| 275 | + if ($f->width !== $w || $f->height !== $h) throw new \InvalidArgumentException('フレームの大きさが画像と違います。'); | |
| 276 | + for ($i = 0, $n = strlen($f->data); $i < $n && count($colors) <= 256; $i += 4) { | |
| 277 | + if (ord($f->data[$i + 3]) < 128) { | |
| 278 | + $hasAlpha = true; | |
| 279 | + } else { | |
| 280 | + $colors[substr($f->data, $i, 3)] = true; | |
| 281 | + } | |
| 282 | + } | |
| 283 | + } | |
| 284 | + $max = $hasAlpha ? 255 : 256; | |
| 285 | + $exact = count($colors) <= $max; | |
| 286 | + $palette = $exact ? array_keys($colors) : $this->medianCut($images, $max); | |
| 287 | + if (!$palette) $palette = ["\0\0\0"]; | |
| 288 | + $trans = $hasAlpha ? count($palette) : null; | |
| 289 | + if ($hasAlpha) $palette[] = "\0\0\0"; | |
| 290 | + | |
| 291 | + $bits = 1; | |
| 292 | + while ((1 << $bits) < count($palette)) $bits++; | |
| 293 | + $gct = str_pad(implode('', $palette), 3 << $bits, "\0"); | |
| 294 | + $min = max(2, $bits); | |
| 295 | + | |
| 296 | + $out = 'GIF89a'.pack('vvCCC', $w, $h, 0x80 | 0x70 | ($bits - 1), 0, 0).$gct; | |
| 297 | + if ($img->frames && $img->loops !== 1) { | |
| 298 | + $out .= "\x21\xff\x0bNETSCAPE2.0\x03\x01".pack('v', $img->loops === 0 ? 0 : min(65535, $img->loops - 1))."\0"; | |
| 299 | + } | |
| 300 | + $index = $exact ? array_flip($palette) : null; | |
| 301 | + foreach ($images as $i => $f) { | |
| 302 | + $idx = $exact ? $this->mapExact($f, $index, $trans) : $this->mapNearest($f, $palette, $trans, (bool)($opt['dither'] ?? false)); | |
| 303 | + $delay = $img->frames ? (int)round($img->frames[$i]['delay'] / 10) : 0; | |
| 304 | + // 透明があるフレームは、次のフレームの前に消す(前のフレームが透けない様に) | |
| 305 | + $dispose = $img->frames ? ($hasAlpha ? 2 : 1) : 0; | |
| 306 | + if ($img->frames || $hasAlpha) { | |
| 307 | + $out .= "\x21\xf9\x04".chr(($dispose << 2) | ($hasAlpha ? 1 : 0)).pack('v', min(65535, $delay)).chr($trans ?? 0)."\0"; | |
| 308 | + } | |
| 309 | + $out .= "\x2c".pack('vvvvC', 0, 0, $w, $h, 0).chr($min); | |
| 310 | + foreach (str_split($this->lzw($idx, $min), 255) as $block) $out .= chr(strlen($block)).$block; | |
| 311 | + $out .= "\0"; | |
| 312 | + } | |
| 313 | + return $out."\x3b"; | |
| 314 | + } | |
| 315 | + | |
| 316 | + private function mapExact(Bitmap $f, array $index, ?int $trans): string { | |
| 317 | + $out = ''; | |
| 318 | + for ($i = 0, $n = strlen($f->data); $i < $n; $i += 4) { | |
| 319 | + $out .= ord($f->data[$i + 3]) < 128 ? chr($trans) : chr($index[substr($f->data, $i, 3)]); | |
| 320 | + } | |
| 321 | + return $out; | |
| 322 | + } | |
| 323 | + | |
| 324 | + /** | |
| 325 | + * 一番近い色にする(5ビット毎にまとめた色で探した結果を覚えておく) | |
| 326 | + */ | |
| 327 | + private function mapNearest(Bitmap $f, array $palette, ?int $trans, bool $dither): string { | |
| 328 | + $pal = array_map(fn($c) => [ord($c[0]), ord($c[1]), ord($c[2])], $palette); | |
| 329 | + if ($trans !== null) array_pop($pal); | |
| 330 | + $cache = []; | |
| 331 | + $nearest = function(int $r, int $g, int $b) use ($pal, &$cache): int { | |
| 332 | + $key = (($r >> 3) << 10) | (($g >> 3) << 5) | ($b >> 3); | |
| 333 | + if (isset($cache[$key])) return $cache[$key]; | |
| 334 | + $best = 0; | |
| 335 | + $bd = PHP_INT_MAX; | |
| 336 | + foreach ($pal as $i => [$pr, $pg, $pb]) { | |
| 337 | + $dr = $pr - $r; | |
| 338 | + $dg = $pg - $g; | |
| 339 | + $db = $pb - $b; | |
| 340 | + $d = 2 * $dr * $dr + 4 * $dg * $dg + 3 * $db * $db; | |
| 341 | + if ($d < $bd) { | |
| 342 | + $bd = $d; | |
| 343 | + $best = $i; | |
| 344 | + } | |
| 345 | + } | |
| 346 | + return $cache[$key] = $best; | |
| 347 | + }; | |
| 348 | + $w = $f->width; | |
| 349 | + $out = ''; | |
| 350 | + $err = $dither ? [array_fill(0, $w * 3 + 6, 0), array_fill(0, $w * 3 + 6, 0)] : null; | |
| 351 | + for ($y = 0; $y < $f->height; $y++) { | |
| 352 | + $row = $f->row($y); | |
| 353 | + if ($dither) { | |
| 354 | + $cur = $err[0]; | |
| 355 | + $next = array_fill(0, $w * 3 + 6, 0); | |
| 356 | + } | |
| 357 | + for ($x = 0; $x < $w; $x++) { | |
| 358 | + $o = $x * 4; | |
| 359 | + if (ord($row[$o + 3]) < 128) { | |
| 360 | + $out .= chr($trans); | |
| 361 | + continue; | |
| 362 | + } | |
| 363 | + $c = [ord($row[$o]), ord($row[$o + 1]), ord($row[$o + 2])]; | |
| 364 | + if ($dither) for ($k = 0; $k < 3; $k++) $c[$k] = max(0, min(255, $c[$k] + intdiv($cur[3 + $x * 3 + $k], 16))); | |
| 365 | + $i = $nearest($c[0], $c[1], $c[2]); | |
| 366 | + $out .= chr($i); | |
| 367 | + if ($dither) { | |
| 368 | + // Floyd–Steinberg:右7、左下3、下5、右下1 | |
| 369 | + for ($k = 0; $k < 3; $k++) { | |
| 370 | + $e = $c[$k] - $pal[$i][$k]; | |
| 371 | + $cur[3 + ($x + 1) * 3 + $k] += $e * 7; | |
| 372 | + $next[3 + ($x - 1) * 3 + $k] += $e * 3; | |
| 373 | + $next[3 + $x * 3 + $k] += $e * 5; | |
| 374 | + $next[3 + ($x + 1) * 3 + $k] += $e; | |
| 375 | + } | |
| 376 | + } | |
| 377 | + } | |
| 378 | + if ($dither) $err[0] = $next; | |
| 379 | + } | |
| 380 | + return $out; | |
| 381 | + } | |
| 382 | + | |
| 383 | + /** | |
| 384 | + * メディアンカットで減色する | |
| 385 | + * | |
| 386 | + * @return array 色(3バイトの文字列) | |
| 387 | + */ | |
| 388 | + private function medianCut(array $images, int $max): array { | |
| 389 | + // 多過ぎる場合は間引く | |
| 390 | + $total = 0; | |
| 391 | + foreach ($images as $f) $total += $f->width * $f->height; | |
| 392 | + $step = max(1, intdiv($total, 200000)); | |
| 393 | + $px = []; | |
| 394 | + $k = 0; | |
| 395 | + foreach ($images as $f) { | |
| 396 | + for ($i = 0, $n = strlen($f->data); $i < $n; $i += 4) { | |
| 397 | + if ($k++ % $step || ord($f->data[$i + 3]) < 128) continue; | |
| 398 | + $px[] = (ord($f->data[$i]) << 16) | (ord($f->data[$i + 1]) << 8) | ord($f->data[$i + 2]); | |
| 399 | + } | |
| 400 | + } | |
| 401 | + if (!$px) return []; | |
| 402 | + // 箱:[色, 一番広い色の軸, 分ける優先度](範囲は箱を作る時に1回だけ計算する) | |
| 403 | + $measure = function(array $box): array { | |
| 404 | + $best = -1; | |
| 405 | + $ch = 0; | |
| 406 | + foreach ([16, 8, 0] as $sh) { | |
| 407 | + $lo = 255; | |
| 408 | + $hi = 0; | |
| 409 | + foreach ($box as $c) { | |
| 410 | + $v = ($c >> $sh) & 255; | |
| 411 | + if ($v < $lo) $lo = $v; | |
| 412 | + if ($v > $hi) $hi = $v; | |
| 413 | + } | |
| 414 | + if ($hi - $lo > $best) { | |
| 415 | + $best = $hi - $lo; | |
| 416 | + $ch = $sh; | |
| 417 | + } | |
| 418 | + } | |
| 419 | + return [$box, $ch, count($box) > 1 ? $best * count($box) : 0]; | |
| 420 | + }; | |
| 421 | + $boxes = [$measure($px)]; | |
| 422 | + while (count($boxes) < $max) { | |
| 423 | + $bi = -1; | |
| 424 | + $top = 0; | |
| 425 | + foreach ($boxes as $i => [, , $score]) { | |
| 426 | + if ($score > $top) { | |
| 427 | + $top = $score; | |
| 428 | + $bi = $i; | |
| 429 | + } | |
| 430 | + } | |
| 431 | + if ($bi < 0) break; | |
| 432 | + [$box, $ch] = $boxes[$bi]; | |
| 433 | + // 1つの軸(8ビット)で並べる:数え上げソート | |
| 434 | + $buckets = array_fill(0, 256, []); | |
| 435 | + foreach ($box as $c) $buckets[($c >> $ch) & 255][] = $c; | |
| 436 | + $box = array_merge(...$buckets); | |
| 437 | + $mid = intdiv(count($box), 2); | |
| 438 | + array_splice($boxes, $bi, 1, [$measure(array_slice($box, 0, $mid)), $measure(array_slice($box, $mid))]); | |
| 439 | + } | |
| 440 | + $boxes = array_column($boxes, 0); | |
| 441 | + return array_map(function($box) { | |
| 442 | + $r = $g = $b = 0; | |
| 443 | + foreach ($box as $c) { | |
| 444 | + $r += ($c >> 16) & 255; | |
| 445 | + $g += ($c >> 8) & 255; | |
| 446 | + $b += $c & 255; | |
| 447 | + } | |
| 448 | + $n = count($box); | |
| 449 | + return chr(intdiv($r + ($n >> 1), $n)).chr(intdiv($g + ($n >> 1), $n)).chr(intdiv($b + ($n >> 1), $n)); | |
| 450 | + }, $boxes); | |
| 451 | + } | |
| 452 | + | |
| 453 | + /** | |
| 454 | + * LZWで圧縮する | |
| 455 | + */ | |
| 456 | + private function lzw(string $idx, int $min): string { | |
| 457 | + $clear = 1 << $min; | |
| 458 | + $eoi = $clear + 1; | |
| 459 | + $size = $min + 1; | |
| 460 | + $next = $clear + 2; | |
| 461 | + $dict = []; | |
| 462 | + $out = ''; | |
| 463 | + $buf = $bits = 0; | |
| 464 | + $emit = function(int $code) use (&$buf, &$bits, &$out, &$size): void { | |
| 465 | + $buf |= $code << $bits; | |
| 466 | + $bits += $size; | |
| 467 | + while ($bits >= 8) { | |
| 468 | + $out .= chr($buf & 255); | |
| 469 | + $buf >>= 8; | |
| 470 | + $bits -= 8; | |
| 471 | + } | |
| 472 | + }; | |
| 473 | + $emit($clear); | |
| 474 | + $n = strlen($idx); | |
| 475 | + if ($n === 0) { | |
| 476 | + $emit($eoi); | |
| 477 | + return $out.($bits > 0 ? chr($buf & 255) : ''); | |
| 478 | + } | |
| 479 | + $w = $idx[0]; | |
| 480 | + for ($i = 1; $i < $n; $i++) { | |
| 481 | + $c = $idx[$i]; | |
| 482 | + $wc = $w.$c; | |
| 483 | + if (isset($dict[$wc])) { | |
| 484 | + $w = $wc; | |
| 485 | + continue; | |
| 486 | + } | |
| 487 | + $emit(strlen($w) === 1 ? ord($w) : $dict[$w]); | |
| 488 | + if ($next < 4096) { | |
| 489 | + $dict[$wc] = $next++; | |
| 490 | + if ($next > (1 << $size) && $size < 12) $size++; | |
| 491 | + } else { | |
| 492 | + // 辞書が一杯:クリアして最初から | |
| 493 | + $emit($clear); | |
| 494 | + $dict = []; | |
| 495 | + $size = $min + 1; | |
| 496 | + $next = $clear + 2; | |
| 497 | + } | |
| 498 | + $w = $c; | |
| 499 | + } | |
| 500 | + $emit(strlen($w) === 1 ? ord($w) : $dict[$w]); | |
| 501 | + $emit($eoi); | |
| 502 | + return $out.($bits > 0 ? chr($buf & 255) : ''); | |
| 126 | 503 | } |
| 127 | 504 | } |
| @@ -38,5 +38,12 @@ | ||
| 38 | 38 | namespace Std\Lib\Image; |
| 39 | 39 | |
| 40 | 40 | interface ImageInterface { |
| 41 | + /** ヘッダー等の情報(width・heightを含む) */ | |
| 41 | 42 | public function parse(string $file): \stdClass; |
| 43 | + | |
| 44 | + /** ファイルの中身を画素にする(壊れたファイルは\RuntimeException) */ | |
| 45 | + public function decode(string $data): Bitmap; | |
| 46 | + | |
| 47 | + /** 画素をファイルの中身にする */ | |
| 48 | + public function encode(Bitmap $img, array $opt = []): string; | |
| 42 | 49 | } |
| @@ -37,54 +37,1001 @@ | ||
| 37 | 37 | */ |
| 38 | 38 | namespace Std\Lib\Image; |
| 39 | 39 | |
| 40 | -use Std\Lib\Image\ImageInterface; | |
| 41 | - | |
| 40 | +/** | |
| 41 | + * JPEG(JFIF・Exif・Adobe) | |
| 42 | + * | |
| 43 | + * 読み込み:ハフマン符号のベースライン(SOF0)・拡張(SOF1、8ビット)・プログレッシブ(SOF2)、リスタート、 | |
| 44 | + * 全てのサンプリング係数、グレースケール・YCbCr・CMYK・YCCK(Adobe)、Exifの向き(ブラウザと同じく適用する)。 | |
| 45 | + * 色差の拡大と色の変換はlibjpegと同じ計算。 | |
| 46 | + * 算術符号・ロスレス・階層・12ビットは未対応(ブラウザも読めない)。 | |
| 47 | + * 書き出し:ベースライン、品質1~100(IJGの表)、4:2:0(又は4:4:4)、灰色はグレースケール。 | |
| 48 | + */ | |
| 42 | 49 | class Jpeg implements ImageInterface { |
| 43 | - public int $width = 0; | |
| 44 | - public int $height = 0; | |
| 45 | - public int $bpp = 0; | |
| 46 | - public string $imageData = ''; | |
| 50 | + /** ジグザグの順 => 自然な順 */ | |
| 51 | + private const ZIGZAG = [ | |
| 52 | + 0, 1, 8, 16, 9, 2, 3, 10, 17, 24, 32, 25, 18, 11, 4, 5, 12, 19, 26, 33, 40, 48, 41, 34, 27, 20, 13, 6, 7, 14, 21, 28, | |
| 53 | + 35, 42, 49, 56, 57, 50, 43, 36, 29, 22, 15, 23, 30, 37, 44, 51, 58, 59, 52, 45, 38, 31, 39, 46, 53, 60, 61, 54, 47, 55, 62, 63, | |
| 54 | + ]; | |
| 47 | 55 | |
| 56 | + // ビットを読む状態 | |
| 57 | + private string $d = ''; | |
| 58 | + private int $p = 0; | |
| 59 | + private int $buf = 0; | |
| 60 | + private int $bits = 0; | |
| 61 | + private int $len = 0; | |
| 62 | + | |
| 63 | + private static ?array $cos = null; | |
| 64 | + | |
| 65 | + public function parse(string $file): \stdClass { | |
| 66 | + $d = (string)file_get_contents($file); | |
| 67 | + $info = $this->headers($d); | |
| 68 | + $f = $info['frame']; | |
| 69 | + if ($f === null) throw new \RuntimeException('JPEGのフレームがありません。'); | |
| 70 | + $rotated = in_array($info['orientation'], [5, 6, 7, 8], true); | |
| 71 | + return (object)[ | |
| 72 | + 'width' => $rotated ? $f['h'] : $f['w'], | |
| 73 | + 'height' => $rotated ? $f['w'] : $f['h'], | |
| 74 | + 'bpp' => $f['precision'], | |
| 75 | + 'components' => count($f['comps']), | |
| 76 | + 'progressive' => $f['type'] === 2, | |
| 77 | + 'sampling' => implode(',', array_map(fn($c) => "{$c['h']}x{$c['v']}", $f['comps'])), | |
| 78 | + 'orientation' => $info['orientation'], | |
| 79 | + 'adobe' => $info['adobe'], | |
| 80 | + ]; | |
| 81 | + } | |
| 82 | + | |
| 48 | 83 | /** |
| 49 | - * TODO: 現在、IHDRヘッダーのみを受け取る | |
| 84 | + * マーカーを順に読む(SOSの後の符号化データは飛ばす) | |
| 85 | + * | |
| 86 | + * @param callable|null $onScan SOS毎に呼ぶ(読み込み用):fn(array $scan, int $pos): int(次の位置) | |
| 50 | 87 | */ |
| 51 | - public function parse(string $file): \stdClass { | |
| 52 | - $jpg = new \stdClass; | |
| 53 | - $fp = fopen($file, 'rb'); | |
| 88 | + private function headers(string $d, ?callable $onScan = null, array &$tables = []): array { | |
| 89 | + if (!str_starts_with($d, "\xff\xd8")) throw new \RuntimeException('JPEGではありません。'); | |
| 90 | + $len = strlen($d); | |
| 91 | + $pos = 2; | |
| 92 | + $info = ['frame' => null, 'orientation' => 1, 'adobe' => null, 'jfif' => false, 'restart' => 0]; | |
| 93 | + $tables += ['q' => [], 'dc' => [], 'ac' => []]; | |
| 94 | + while ($pos < $len) { | |
| 95 | + // マーカーを探す(埋め草の0xFFは飛ばす) | |
| 96 | + if ($d[$pos] !== "\xff") { | |
| 97 | + $pos++; | |
| 98 | + continue; | |
| 99 | + } | |
| 100 | + while ($pos < $len && $d[$pos] === "\xff") $pos++; | |
| 101 | + if ($pos >= $len) break; | |
| 102 | + $m = ord($d[$pos++]); | |
| 103 | + // 0x00は符号化データの中の0xFF、RSTnと0x01は長さが無い | |
| 104 | + if ($m === 0x00 || $m === 0xd8 || ($m >= 0xd0 && $m <= 0xd7) || $m === 0x01) continue; | |
| 105 | + if ($m === 0xd9) break; | |
| 106 | + if ($pos + 2 > $len) throw new \RuntimeException('JPEGのマーカーが途中で切れています。'); | |
| 107 | + $segLen = unpack('n', substr($d, $pos, 2))[1]; | |
| 108 | + $seg = substr($d, $pos + 2, $segLen - 2); | |
| 109 | + $next = $pos + $segLen; | |
| 54 | 110 | |
| 55 | - $i = 0; | |
| 56 | - while (!feof($fp)) { | |
| 57 | - $byte = fread($fp, 1); | |
| 58 | - if ($byte !== "\xFF") continue; | |
| 111 | + if ($m === 0xc0 || $m === 0xc1 || $m === 0xc2) { | |
| 112 | + $f = unpack('Cprecision/nh/nw/Cn', $seg); | |
| 113 | + if ($f['precision'] !== 8) throw new \RuntimeException('12ビットのJPEGは未対応です。'); | |
| 114 | + if ($f['h'] === 0) throw new \RuntimeException('DNLで丈を決めるJPEGは未対応です。'); | |
| 115 | + Bitmap::check($f['w'], $f['h']); | |
| 116 | + $comps = []; | |
| 117 | + for ($i = 0; $i < $f['n']; $i++) { | |
| 118 | + $c = unpack('Cid/Csamp/Ctq', substr($seg, 6 + $i * 3, 3)); | |
| 119 | + $h = $c['samp'] >> 4; | |
| 120 | + $v = $c['samp'] & 15; | |
| 121 | + if ($h < 1 || $h > 4 || $v < 1 || $v > 4) throw new \RuntimeException('JPEGのサンプリング係数が不正です。'); | |
| 122 | + $comps[] = ['id' => $c['id'], 'h' => $h, 'v' => $v, 'tq' => $c['tq']]; | |
| 123 | + } | |
| 124 | + if (!in_array(count($comps), [1, 3, 4], true)) throw new \RuntimeException('未対応のJPEGの色の数です('.count($comps).')。'); | |
| 125 | + $info['frame'] = ['type' => $m - 0xc0, 'precision' => $f['precision'], 'w' => $f['w'], 'h' => $f['h'], 'comps' => $comps]; | |
| 126 | + } else if (($m >= 0xc3 && $m <= 0xcf) && $m !== 0xc4 && $m !== 0xc8 && $m !== 0xcc) { | |
| 127 | + throw new \RuntimeException('算術符号・ロスレス・階層のJPEGは未対応です。'); | |
| 128 | + } else if ($m === 0xc4) { | |
| 129 | + $tables = $this->dht($seg, $tables); | |
| 130 | + } else if ($m === 0xdb) { | |
| 131 | + $tables = $this->dqt($seg, $tables); | |
| 132 | + } else if ($m === 0xdd) { | |
| 133 | + $info['restart'] = unpack('n', $seg)[1]; | |
| 134 | + } else if ($m === 0xe0 && str_starts_with($seg, "JFIF\0")) { | |
| 135 | + $info['jfif'] = true; | |
| 136 | + } else if ($m === 0xe1 && str_starts_with($seg, "Exif\0\0")) { | |
| 137 | + $info['orientation'] = $this->orientation(substr($seg, 6)); | |
| 138 | + } else if ($m === 0xee && str_starts_with($seg, 'Adobe') && strlen($seg) >= 12) { | |
| 139 | + $info['adobe'] = ord($seg[11]); | |
| 140 | + } else if ($m === 0xda) { | |
| 141 | + if ($onScan === null) { | |
| 142 | + $pos = $next; | |
| 143 | + continue; | |
| 144 | + } | |
| 145 | + $n = ord($seg[0]); | |
| 146 | + $scan = ['comps' => []]; | |
| 147 | + for ($i = 0; $i < $n; $i++) $scan['comps'][] = ['id' => ord($seg[1 + $i * 2]), 'td' => ord($seg[2 + $i * 2]) >> 4, 'ta' => ord($seg[2 + $i * 2]) & 15]; | |
| 148 | + $scan['ss'] = ord($seg[1 + $n * 2]); | |
| 149 | + $scan['se'] = ord($seg[2 + $n * 2]); | |
| 150 | + $scan['ah'] = ord($seg[3 + $n * 2]) >> 4; | |
| 151 | + $scan['al'] = ord($seg[3 + $n * 2]) & 15; | |
| 152 | + $pos = $onScan($scan, $next, $info, $tables); | |
| 153 | + continue; | |
| 154 | + } | |
| 155 | + $pos = $next; | |
| 156 | + } | |
| 157 | + return $info; | |
| 158 | + } | |
| 59 | 159 | |
| 60 | - $marker = ord(fread($fp, 1)); | |
| 61 | - if ($marker >= 0xC0 && $marker <= 0xC3) { // 0xC0 = SOF0, 0xC1 = SOF1, 0xC2 = SOF2, 0xC3 = SOF3 | |
| 62 | - fread($fp, 2); | |
| 63 | - $this->bpp = ord(fread($fp, 1)); | |
| 64 | - // n = ビッグエンディアン(2ビット) | |
| 65 | - $this->height = unpack('n', fread($fp, 2))[1]; | |
| 66 | - $this->width = unpack('n', fread($fp, 2))[1]; | |
| 67 | - $jpg->width = $this->width; | |
| 68 | - $jpg->height = $this->height; | |
| 69 | - $jpg->bpp = $this->bpp; | |
| 70 | - } else if ($marker == 0xC4) { // DHT (Huffmanテーブル(複数可)) | |
| 71 | - // | |
| 72 | - } else if ($marker == 0xDB) { // DQT (量子化テーブル(複数可)) | |
| 73 | - // | |
| 74 | - } else if ($marker == 0xDD) { // DRI (リセット期間) | |
| 75 | - } else if ($marker == 0xDA) { // SOS(画像開始) | |
| 76 | - } else if ($marker >= 0xD0 && $marker <= 0xD7) { // RST | |
| 77 | - } else if ($marker >= 0xE1 && $marker <= 0xEF) { // APP | |
| 78 | - } else if ($marker == 0xFE) { // COM | |
| 79 | - } else if ($marker == 0xD9) { // EOI(画像終了) | |
| 160 | + private function dqt(string $s, array $t): array { | |
| 161 | + $p = 0; | |
| 162 | + while ($p < strlen($s)) { | |
| 163 | + $pq = ord($s[$p]) >> 4; | |
| 164 | + $id = ord($s[$p]) & 15; | |
| 165 | + $p++; | |
| 166 | + $q = $pq ? array_values(unpack('n64', substr($s, $p, 128))) : array_values(unpack('C64', substr($s, $p, 64))); | |
| 167 | + $p += $pq ? 128 : 64; | |
| 168 | + $t['q'][$id] = $q; // ジグザグの順 | |
| 169 | + } | |
| 170 | + return $t; | |
| 171 | + } | |
| 172 | + | |
| 173 | + private function dht(string $s, array $t): array { | |
| 174 | + $p = 0; | |
| 175 | + while ($p + 17 <= strlen($s)) { | |
| 176 | + $cls = ord($s[$p]) >> 4; | |
| 177 | + $id = ord($s[$p]) & 15; | |
| 178 | + $counts = array_values(unpack('C16', substr($s, $p + 1, 16))); | |
| 179 | + $p += 17; | |
| 180 | + $n = array_sum($counts); | |
| 181 | + $syms = array_values(unpack("C{$n}", substr($s, $p, $n))); | |
| 182 | + $p += $n; | |
| 183 | + $t[$cls ? 'ac' : 'dc'][$id] = self::huffTable($counts, $syms); | |
| 184 | + } | |
| 185 | + return $t; | |
| 186 | + } | |
| 187 | + | |
| 188 | + /** | |
| 189 | + * ハフマン表(9ビットの先読み表と、長い符号用のmaxcode等) | |
| 190 | + */ | |
| 191 | + private static function huffTable(array $counts, array $syms): array { | |
| 192 | + $look = array_fill(0, 512, -1); | |
| 193 | + $maxcode = array_fill(0, 18, -1); | |
| 194 | + $mincode = $valptr = array_fill(0, 18, 0); | |
| 195 | + $code = $k = 0; | |
| 196 | + for ($l = 1; $l <= 16; $l++) { | |
| 197 | + $mincode[$l] = $code; | |
| 198 | + $valptr[$l] = $k; | |
| 199 | + for ($i = 0; $i < $counts[$l - 1]; $i++) { | |
| 200 | + if ($l <= 9) { | |
| 201 | + $shift = 9 - $l; | |
| 202 | + $base = $code << $shift; | |
| 203 | + for ($j = 0; $j < (1 << $shift); $j++) $look[$base + $j] = ($l << 8) | $syms[$k]; | |
| 204 | + } | |
| 205 | + $code++; | |
| 206 | + $k++; | |
| 207 | + } | |
| 208 | + $maxcode[$l] = $counts[$l - 1] ? $code - 1 : -1; | |
| 209 | + $code <<= 1; | |
| 210 | + } | |
| 211 | + return ['look' => $look, 'max' => $maxcode, 'min' => $mincode, 'ptr' => $valptr, 'syms' => $syms]; | |
| 212 | + } | |
| 213 | + | |
| 214 | + /** | |
| 215 | + * Exif(TIFF)のIFD0から向き(0x0112)を読む | |
| 216 | + */ | |
| 217 | + private function orientation(string $t): int { | |
| 218 | + if (strlen($t) < 8) return 1; | |
| 219 | + $le = substr($t, 0, 2) === 'II'; | |
| 220 | + $u16 = fn($o) => unpack($le ? 'v' : 'n', substr($t, $o, 2))[1] ?? 0; | |
| 221 | + $u32 = fn($o) => unpack($le ? 'V' : 'N', substr($t, $o, 4))[1] ?? 0; | |
| 222 | + $ifd = $u32(4); | |
| 223 | + if ($ifd + 2 > strlen($t)) return 1; | |
| 224 | + $n = $u16($ifd); | |
| 225 | + for ($i = 0; $i < $n; $i++) { | |
| 226 | + $e = $ifd + 2 + $i * 12; | |
| 227 | + if ($e + 12 > strlen($t)) break; | |
| 228 | + if ($u16($e) === 0x0112) { | |
| 229 | + $v = $u16($e + 8); | |
| 230 | + return $v >= 1 && $v <= 8 ? $v : 1; | |
| 231 | + } | |
| 232 | + } | |
| 233 | + return 1; | |
| 234 | + } | |
| 235 | + | |
| 236 | + //////// ビットを読む | |
| 237 | + | |
| 238 | + private function fill(): void { | |
| 239 | + // 使い終わったビットを捨ててから足す(64ビットを超えない様に) | |
| 240 | + $this->buf &= (1 << $this->bits) - 1; | |
| 241 | + while ($this->bits <= 48) { | |
| 242 | + $byte = 0; | |
| 243 | + if ($this->p < $this->len) { | |
| 244 | + $byte = ord($this->d[$this->p]); | |
| 245 | + if ($byte === 0xff) { | |
| 246 | + $nx = ord($this->d[$this->p + 1] ?? "\0"); | |
| 247 | + if ($nx === 0) { | |
| 248 | + $this->p += 2; | |
| 249 | + } else { | |
| 250 | + // マーカー:進まずに0を足す | |
| 251 | + $byte = 0; | |
| 252 | + } | |
| 253 | + } else { | |
| 254 | + $this->p++; | |
| 255 | + } | |
| 256 | + } | |
| 257 | + $this->buf = ($this->buf << 8) | $byte; | |
| 258 | + $this->bits += 8; | |
| 259 | + } | |
| 260 | + } | |
| 261 | + | |
| 262 | + private function getBits(int $n): int { | |
| 263 | + if ($n === 0) return 0; | |
| 264 | + if ($this->bits < $n) $this->fill(); | |
| 265 | + $this->bits -= $n; | |
| 266 | + return ($this->buf >> $this->bits) & ((1 << $n) - 1); | |
| 267 | + } | |
| 268 | + | |
| 269 | + private function extend(int $v, int $s): int { | |
| 270 | + return $v < (1 << ($s - 1)) ? $v - (1 << $s) + 1 : $v; | |
| 271 | + } | |
| 272 | + | |
| 273 | + private function huff(array $t): int { | |
| 274 | + if ($this->bits < 16) $this->fill(); | |
| 275 | + $e = $t['look'][($this->buf >> ($this->bits - 9)) & 511]; | |
| 276 | + if ($e >= 0) { | |
| 277 | + $this->bits -= $e >> 8; | |
| 278 | + return $e & 255; | |
| 279 | + } | |
| 280 | + for ($l = 10; $l <= 16; $l++) { | |
| 281 | + $code = ($this->buf >> ($this->bits - $l)) & ((1 << $l) - 1); | |
| 282 | + if ($code <= $t['max'][$l]) { | |
| 283 | + $this->bits -= $l; | |
| 284 | + return $t['syms'][$t['ptr'][$l] + $code - $t['min'][$l]] ?? 0; | |
| 285 | + } | |
| 286 | + } | |
| 287 | + throw new \RuntimeException('JPEGのハフマン符号が不正です。'); | |
| 288 | + } | |
| 289 | + | |
| 290 | + /** リスタートマーカーを飛ばして、ビットを捨てる */ | |
| 291 | + private function restart(): void { | |
| 292 | + $this->buf = $this->bits = 0; | |
| 293 | + while ($this->p + 1 < $this->len && !($this->d[$this->p] === "\xff" && ($m = ord($this->d[$this->p + 1])) >= 0xd0 && $m <= 0xd7)) { | |
| 294 | + // 符号化データの残り(壊れたファイル)を飛ばす | |
| 295 | + if ($this->d[$this->p] === "\xff" && ord($this->d[$this->p + 1]) !== 0) break; | |
| 296 | + $this->p++; | |
| 297 | + } | |
| 298 | + if ($this->p + 1 < $this->len && $this->d[$this->p] === "\xff" && ($m = ord($this->d[$this->p + 1])) >= 0xd0 && $m <= 0xd7) $this->p += 2; | |
| 299 | + } | |
| 300 | + | |
| 301 | + //////// 読み込み | |
| 302 | + | |
| 303 | + public function decode(string $data): Bitmap { | |
| 304 | + $this->d = $data; | |
| 305 | + $this->len = strlen($data); | |
| 306 | + $frame = null; | |
| 307 | + $planes = []; | |
| 308 | + $coefs = []; | |
| 309 | + $layout = null; | |
| 310 | + $tables = []; | |
| 311 | + $scans = 0; | |
| 312 | + | |
| 313 | + $info = $this->headers($data, function(array $scan, int $pos, array $info, array $tables) use (&$frame, &$planes, &$coefs, &$layout, &$scans): int { | |
| 314 | + if ($info['frame'] === null) throw new \RuntimeException('SOFの前にSOSがあります。'); | |
| 315 | + if ($layout === null) { | |
| 316 | + $frame = $info['frame']; | |
| 317 | + $layout = $this->layout($frame); | |
| 318 | + foreach ($layout['comps'] as $i => $c) { | |
| 319 | + // プログレッシブは全てのスキャンが終わるまで係数を保つ(16ビットの文字列:配列の1/8のメモリ) | |
| 320 | + if ($frame['type'] === 2) $coefs[$i] = str_repeat("\0", $c['bw'] * $c['bh'] * 128); | |
| 321 | + else $planes[$i] = str_repeat("\x80", $c['bw'] * $c['bh'] * 64); | |
| 322 | + } | |
| 323 | + } | |
| 324 | + $scans++; | |
| 325 | + $this->p = $pos; | |
| 326 | + $this->buf = $this->bits = 0; | |
| 327 | + $this->scan($scan, $frame, $layout, $info, $tables, $planes, $coefs); | |
| 328 | + // 次のマーカー(RST以外)まで進む | |
| 329 | + $p = $this->p; | |
| 330 | + while ($p + 1 < $this->len) { | |
| 331 | + if ($this->d[$p] === "\xff") { | |
| 332 | + $m = ord($this->d[$p + 1]); | |
| 333 | + if ($m !== 0 && $m !== 0xff && ($m < 0xd0 || $m > 0xd7)) break; | |
| 334 | + } | |
| 335 | + $p++; | |
| 336 | + } | |
| 337 | + return $p; | |
| 338 | + }, $tables); | |
| 339 | + if ($layout === null || $scans === 0) throw new \RuntimeException('JPEGに画像データがありません。'); | |
| 340 | + | |
| 341 | + // プログレッシブ:最後に逆量子化と逆DCT | |
| 342 | + if ($frame['type'] === 2) { | |
| 343 | + foreach ($layout['comps'] as $i => $c) { | |
| 344 | + $q = $tables['q'][$c['tq']] ?? null; | |
| 345 | + if ($q === null) throw new \RuntimeException('量子化表がありません。'); | |
| 346 | + $qn = array_fill(0, 64, 0); | |
| 347 | + foreach (self::ZIGZAG as $k => $z) $qn[$z] = $q[$k]; | |
| 348 | + $planes[$i] = str_repeat("\x80", $c['bw'] * $c['bh'] * 64); | |
| 349 | + $stride = $c['bw'] * 8; | |
| 350 | + $co = $coefs[$i]; | |
| 351 | + for ($by = 0; $by < $c['bh']; $by++) { | |
| 352 | + for ($bx = 0; $bx < $c['bw']; $bx++) { | |
| 353 | + $raw = unpack('v64', $co, ($by * $c['bw'] + $bx) * 128); | |
| 354 | + $blk = []; | |
| 355 | + for ($k = 0; $k < 64; $k++) { | |
| 356 | + $v = $raw[$k + 1]; | |
| 357 | + $blk[$k] = ($v >= 32768 ? $v - 65536 : $v) * $qn[$k]; | |
| 358 | + } | |
| 359 | + $this->idct($blk, $planes[$i], $by * 8 * $stride + $bx * 8, $stride); | |
| 360 | + } | |
| 361 | + } | |
| 362 | + unset($coefs[$i]); | |
| 363 | + } | |
| 364 | + } | |
| 365 | + | |
| 366 | + $img = $this->toRgba($frame, $layout, $planes, $info); | |
| 367 | + $this->d = ''; | |
| 368 | + return $this->orient($img, $info['orientation']); | |
| 369 | + } | |
| 370 | + | |
| 371 | + /** | |
| 372 | + * 色毎のブロックの数等 | |
| 373 | + */ | |
| 374 | + private function layout(array $f): array { | |
| 375 | + $hmax = max(array_column($f['comps'], 'h')); | |
| 376 | + $vmax = max(array_column($f['comps'], 'v')); | |
| 377 | + $mcux = intdiv($f['w'] + 8 * $hmax - 1, 8 * $hmax); | |
| 378 | + $mcuy = intdiv($f['h'] + 8 * $vmax - 1, 8 * $vmax); | |
| 379 | + $comps = []; | |
| 380 | + foreach ($f['comps'] as $c) { | |
| 381 | + $comps[] = $c + [ | |
| 382 | + 'bw' => $mcux * $c['h'], 'bh' => $mcuy * $c['v'], | |
| 383 | + // 1色だけのスキャンで使うブロック(画像の大きさまで) | |
| 384 | + 'cw' => intdiv(intdiv($f['w'] * $c['h'] + $hmax - 1, $hmax) + 7, 8), | |
| 385 | + 'ch' => intdiv(intdiv($f['h'] * $c['v'] + $vmax - 1, $vmax) + 7, 8), | |
| 386 | + ]; | |
| 387 | + } | |
| 388 | + return ['hmax' => $hmax, 'vmax' => $vmax, 'mcux' => $mcux, 'mcuy' => $mcuy, 'comps' => $comps]; | |
| 389 | + } | |
| 390 | + | |
| 391 | + /** | |
| 392 | + * 1つのスキャンを読む | |
| 393 | + */ | |
| 394 | + private function scan(array $scan, array $frame, array $layout, array $info, array $tables, array &$planes, array &$coefs): void { | |
| 395 | + $progressive = $frame['type'] === 2; | |
| 396 | + $idx = []; | |
| 397 | + foreach ($scan['comps'] as $sc) { | |
| 398 | + $found = null; | |
| 399 | + foreach ($layout['comps'] as $i => $c) if ($c['id'] === $sc['id']) $found = $i; | |
| 400 | + if ($found === null) throw new \RuntimeException('スキャンの色が不明です。'); | |
| 401 | + $idx[] = [$found, $sc]; | |
| 402 | + } | |
| 403 | + $pred = array_fill(0, count($layout['comps']), 0); | |
| 404 | + $eobrun = 0; | |
| 405 | + $restart = $info['restart']; | |
| 406 | + | |
| 407 | + // 量子化表(ベースライン用、ジグザグの順) | |
| 408 | + $qt = []; | |
| 409 | + if (!$progressive) { | |
| 410 | + foreach ($idx as [$i]) { | |
| 411 | + $q = $tables['q'][$layout['comps'][$i]['tq']] ?? null; | |
| 412 | + if ($q === null) throw new \RuntimeException('量子化表がありません。'); | |
| 413 | + $qt[$i] = $q; | |
| 414 | + } | |
| 415 | + } | |
| 416 | + | |
| 417 | + $decodeBlock = function(int $ci, array $sc, int $bx, int $by) use (&$pred, &$eobrun, $progressive, $scan, $tables, $layout, &$planes, &$coefs, $qt): void { | |
| 418 | + $c = $layout['comps'][$ci]; | |
| 419 | + $dc = $tables['dc'][$sc['td']] ?? null; | |
| 420 | + $ac = $tables['ac'][$sc['ta']] ?? null; | |
| 421 | + if (!$progressive) { | |
| 422 | + if ($dc === null || $ac === null) throw new \RuntimeException('ハフマン表がありません。'); | |
| 423 | + $q = $qt[$ci]; | |
| 424 | + $blk = array_fill(0, 64, 0); | |
| 425 | + $t = $this->huff($dc); | |
| 426 | + $pred[$ci] += $t ? $this->extend($this->getBits($t), $t) : 0; | |
| 427 | + $blk[0] = $pred[$ci] * $q[0]; | |
| 428 | + for ($k = 1; $k < 64;) { | |
| 429 | + $rs = $this->huff($ac); | |
| 430 | + $r = $rs >> 4; | |
| 431 | + $s = $rs & 15; | |
| 432 | + if ($s === 0) { | |
| 433 | + if ($r !== 15) break; | |
| 434 | + $k += 16; | |
| 435 | + continue; | |
| 436 | + } | |
| 437 | + $k += $r; | |
| 438 | + if ($k > 63) break; | |
| 439 | + $blk[self::ZIGZAG[$k]] = $this->extend($this->getBits($s), $s) * $q[$k]; | |
| 440 | + $k++; | |
| 441 | + } | |
| 442 | + $stride = $c['bw'] * 8; | |
| 443 | + $this->idct($blk, $planes[$ci], $by * 8 * $stride + $bx * 8, $stride); | |
| 444 | + return; | |
| 445 | + } | |
| 446 | + | |
| 447 | + // 係数は16ビット(リトルエンディアン):$o+自然な順の位置×2 | |
| 448 | + // (クロージャで$coを値で取ると、書く度に文字列全体が複写されるので、ここで直接読み書きする) | |
| 449 | + $o = ($by * $c['bw'] + $bx) * 128; | |
| 450 | + $co = &$coefs[$ci]; | |
| 451 | + $ss = $scan['ss']; | |
| 452 | + $se = $scan['se']; | |
| 453 | + $al = $scan['al']; | |
| 454 | + if ($ss === 0) { | |
| 455 | + // DC | |
| 456 | + if ($scan['ah'] === 0) { | |
| 457 | + $t = $this->huff($dc); | |
| 458 | + $pred[$ci] += $t ? $this->extend($this->getBits($t), $t) : 0; | |
| 459 | + $v = $pred[$ci] * (1 << $al); | |
| 460 | + } else if ($this->getBits(1)) { | |
| 461 | + $v = ord($co[$o]) | (ord($co[$o + 1]) << 8) | (1 << $al); | |
| 462 | + } else { | |
| 463 | + return; | |
| 464 | + } | |
| 465 | + $co[$o] = chr($v & 255); | |
| 466 | + $co[$o + 1] = chr(($v >> 8) & 255); | |
| 467 | + return; | |
| 468 | + } | |
| 469 | + if ($scan['ah'] === 0) { | |
| 470 | + // AC:最初 | |
| 471 | + if ($eobrun > 0) { | |
| 472 | + $eobrun--; | |
| 473 | + return; | |
| 474 | + } | |
| 475 | + for ($k = $ss; $k <= $se; $k++) { | |
| 476 | + $rs = $this->huff($ac); | |
| 477 | + $r = $rs >> 4; | |
| 478 | + $s = $rs & 15; | |
| 479 | + if ($s === 0) { | |
| 480 | + if ($r < 15) { | |
| 481 | + $eobrun = (1 << $r) - 1; | |
| 482 | + if ($r) $eobrun += $this->getBits($r); | |
| 483 | + break; | |
| 484 | + } | |
| 485 | + $k += 15; | |
| 486 | + continue; | |
| 487 | + } | |
| 488 | + $k += $r; | |
| 489 | + if ($k > 63) break; | |
| 490 | + $v = $this->extend($this->getBits($s), $s) * (1 << $al); | |
| 491 | + $z = $o + self::ZIGZAG[$k] * 2; | |
| 492 | + $co[$z] = chr($v & 255); | |
| 493 | + $co[$z + 1] = chr(($v >> 8) & 255); | |
| 494 | + } | |
| 495 | + return; | |
| 496 | + } | |
| 497 | + // AC:精度を上げる(libjpegのdecode_mcu_AC_refineと同じ) | |
| 498 | + $p1 = 1 << $al; | |
| 499 | + $m1 = -1 << $al; | |
| 500 | + $k = $ss; | |
| 501 | + if ($eobrun <= 0) { | |
| 502 | + for (; $k <= $se; $k++) { | |
| 503 | + $rs = $this->huff($ac); | |
| 504 | + $r = $rs >> 4; | |
| 505 | + $s = $rs & 15; | |
| 506 | + if ($s) { | |
| 507 | + $s = $this->getBits(1) ? $p1 : $m1; | |
| 508 | + } else if ($r !== 15) { | |
| 509 | + $eobrun = 1 << $r; | |
| 510 | + if ($r) $eobrun += $this->getBits($r); | |
| 511 | + break; | |
| 512 | + } | |
| 513 | + do { | |
| 514 | + $z = $o + self::ZIGZAG[$k] * 2; | |
| 515 | + $v = ord($co[$z]) | (ord($co[$z + 1]) << 8); | |
| 516 | + if ($v !== 0) { | |
| 517 | + if ($this->getBits(1) && ($v & $p1) === 0) { | |
| 518 | + if ($v >= 32768) $v -= 65536; | |
| 519 | + $v += $v >= 0 ? $p1 : $m1; | |
| 520 | + $co[$z] = chr($v & 255); | |
| 521 | + $co[$z + 1] = chr(($v >> 8) & 255); | |
| 522 | + } | |
| 523 | + } else if (--$r < 0) { | |
| 524 | + break; | |
| 525 | + } | |
| 526 | + $k++; | |
| 527 | + } while ($k <= $se); | |
| 528 | + if ($s && $k <= 63) { | |
| 529 | + $z = $o + self::ZIGZAG[$k] * 2; | |
| 530 | + $co[$z] = chr($s & 255); | |
| 531 | + $co[$z + 1] = chr(($s >> 8) & 255); | |
| 532 | + } | |
| 533 | + } | |
| 534 | + } | |
| 535 | + if ($eobrun > 0) { | |
| 536 | + for (; $k <= $se; $k++) { | |
| 537 | + $z = $o + self::ZIGZAG[$k] * 2; | |
| 538 | + $v = ord($co[$z]) | (ord($co[$z + 1]) << 8); | |
| 539 | + if ($v !== 0 && $this->getBits(1) && ($v & $p1) === 0) { | |
| 540 | + if ($v >= 32768) $v -= 65536; | |
| 541 | + $v += $v >= 0 ? $p1 : $m1; | |
| 542 | + $co[$z] = chr($v & 255); | |
| 543 | + $co[$z + 1] = chr(($v >> 8) & 255); | |
| 544 | + } | |
| 545 | + } | |
| 546 | + $eobrun--; | |
| 547 | + } | |
| 548 | + }; | |
| 549 | + | |
| 550 | + $mcu = 0; | |
| 551 | + $next = function() use (&$mcu, $restart, &$pred, &$eobrun): void { | |
| 552 | + $mcu++; | |
| 553 | + if ($restart > 0 && $mcu % $restart === 0) { | |
| 554 | + $this->restart(); | |
| 555 | + $pred = array_fill(0, count($pred), 0); | |
| 556 | + $eobrun = 0; | |
| 557 | + } | |
| 558 | + }; | |
| 559 | + | |
| 560 | + if (count($idx) === 1) { | |
| 561 | + // 1色だけ:ブロック毎がMCU | |
| 562 | + [$ci, $sc] = $idx[0]; | |
| 563 | + $c = $layout['comps'][$ci]; | |
| 564 | + for ($by = 0; $by < $c['ch']; $by++) { | |
| 565 | + for ($bx = 0; $bx < $c['cw']; $bx++) { | |
| 566 | + $decodeBlock($ci, $sc, $bx, $by); | |
| 567 | + $next(); | |
| 568 | + } | |
| 569 | + } | |
| 570 | + return; | |
| 571 | + } | |
| 572 | + for ($my = 0; $my < $layout['mcuy']; $my++) { | |
| 573 | + for ($mx = 0; $mx < $layout['mcux']; $mx++) { | |
| 574 | + foreach ($idx as [$ci, $sc]) { | |
| 575 | + $c = $layout['comps'][$ci]; | |
| 576 | + for ($v = 0; $v < $c['v']; $v++) { | |
| 577 | + for ($h = 0; $h < $c['h']; $h++) $decodeBlock($ci, $sc, $mx * $c['h'] + $h, $my * $c['v'] + $v); | |
| 578 | + } | |
| 579 | + } | |
| 580 | + $next(); | |
| 581 | + } | |
| 582 | + } | |
| 583 | + } | |
| 584 | + | |
| 585 | + /** 逆DCT・DCTの係数:$cos[u][x] = C(u)/2 × cos((2x+1)uπ/16) */ | |
| 586 | + private static function cosTable(): array { | |
| 587 | + if (self::$cos === null) { | |
| 588 | + for ($u = 0; $u < 8; $u++) { | |
| 589 | + for ($x = 0; $x < 8; $x++) self::$cos[$u][$x] = ($u === 0 ? M_SQRT1_2 : 1.0) / 2 * cos((2 * $x + 1) * $u * M_PI / 16); | |
| 590 | + } | |
| 591 | + } | |
| 592 | + return self::$cos; | |
| 593 | + } | |
| 594 | + | |
| 595 | + /** | |
| 596 | + * 8x8の逆DCT(係数は自然な順、逆量子化済み)。結果に128を足して面に書く | |
| 597 | + */ | |
| 598 | + private function idct(array $f, string &$plane, int $off, int $stride): void { | |
| 599 | + // 交流成分が無いブロック(多い)は1色 | |
| 600 | + $ac = false; | |
| 601 | + for ($k = 1; $k < 64; $k++) if ($f[$k] !== 0) { | |
| 602 | + $ac = true; | |
| 603 | + break; | |
| 604 | + } | |
| 605 | + if (!$ac) { | |
| 606 | + $v = (int)round($f[0] / 8 + 128); | |
| 607 | + $ch = chr($v < 0 ? 0 : ($v > 255 ? 255 : $v)); | |
| 608 | + // その場で書く(substr_replaceは面の全部を複写するので遅い) | |
| 609 | + for ($y = 0; $y < 8; $y++) { | |
| 610 | + $o = $off + $y * $stride; | |
| 611 | + for ($x = 0; $x < 8; $x++) $plane[$o + $x] = $ch; | |
| 612 | + } | |
| 613 | + return; | |
| 614 | + } | |
| 615 | + $K = self::cosTable(); | |
| 616 | + // 行(v毎に、xについて) | |
| 617 | + $t = []; | |
| 618 | + for ($v = 0; $v < 8; $v++) { | |
| 619 | + $b = $v * 8; | |
| 620 | + $zero = true; | |
| 621 | + for ($u = 1; $u < 8; $u++) if ($f[$b + $u] !== 0) { | |
| 622 | + $zero = false; | |
| 80 | 623 | break; |
| 81 | 624 | } |
| 625 | + if ($zero) { | |
| 626 | + $d = $f[$b] * $K[0][0]; | |
| 627 | + for ($x = 0; $x < 8; $x++) $t[$b + $x] = $d; | |
| 628 | + continue; | |
| 629 | + } | |
| 630 | + for ($x = 0; $x < 8; $x++) { | |
| 631 | + $s = 0.0; | |
| 632 | + for ($u = 0; $u < 8; $u++) if ($f[$b + $u] !== 0) $s += $f[$b + $u] * $K[$u][$x]; | |
| 633 | + $t[$b + $x] = $s; | |
| 634 | + } | |
| 82 | 635 | } |
| 636 | + // 列 | |
| 637 | + for ($y = 0; $y < 8; $y++) { | |
| 638 | + $o = $off + $y * $stride; | |
| 639 | + for ($x = 0; $x < 8; $x++) { | |
| 640 | + $s = 0.0; | |
| 641 | + for ($v = 0; $v < 8; $v++) $s += $t[$v * 8 + $x] * $K[$v][$y]; | |
| 642 | + $p = (int)round($s + 128); | |
| 643 | + $plane[$o + $x] = chr($p < 0 ? 0 : ($p > 255 ? 255 : $p)); | |
| 644 | + } | |
| 645 | + } | |
| 646 | + } | |
| 83 | 647 | |
| 84 | - // kys($jpg); | |
| 648 | + /** | |
| 649 | + * 色の面をRGBAにする(色差の拡大はlibjpegの「fancy upsampling」、色の変換もlibjpegと同じ整数の計算) | |
| 650 | + * メモリを使わない様に、1行ずつ処理する | |
| 651 | + */ | |
| 652 | + private function toRgba(array $frame, array $layout, array $planes, array $info): Bitmap { | |
| 653 | + $w = $frame['w']; | |
| 654 | + $h = $frame['h']; | |
| 655 | + $n = count($layout['comps']); | |
| 656 | + $src = []; | |
| 657 | + foreach ($layout['comps'] as $i => $c) { | |
| 658 | + $sx = intdiv($layout['hmax'], $c['h']); | |
| 659 | + $sy = intdiv($layout['vmax'], $c['v']); | |
| 660 | + $src[$i] = ['plane' => $planes[$i], 'pw' => $c['bw'] * 8, 'cols' => intdiv($w + $sx - 1, $sx), 'rows' => intdiv($h + $sy - 1, $sy), 'sx' => $sx, 'sy' => $sy]; | |
| 661 | + } | |
| 85 | 662 | |
| 86 | - $i++; | |
| 87 | - fclose($fp); | |
| 88 | - return $jpg; | |
| 663 | + // 色の変換 | |
| 664 | + $transform = $info['adobe']; | |
| 665 | + $ids = array_column($layout['comps'], 'id'); | |
| 666 | + if ($n === 3) { | |
| 667 | + // Adobeの変換0、又はJFIF無しで成分IDが「R」「G」「B」ならRGB | |
| 668 | + $ycc = !($transform === 0 || (!$info['jfif'] && $transform === null && $ids === [0x52, 0x47, 0x42])); | |
| 669 | + } else { | |
| 670 | + $ycc = $n === 4 && $transform === 2; | |
| 671 | + } | |
| 672 | + static $tab = null; | |
| 673 | + if ($tab === null) { | |
| 674 | + // libjpegのjdcolor.c(SCALEBITS 16) | |
| 675 | + $tab = ['rcr' => [], 'bcb' => [], 'gcr' => [], 'gcb' => []]; | |
| 676 | + for ($i = 0; $i < 256; $i++) { | |
| 677 | + $x = $i - 128; | |
| 678 | + $tab['rcr'][$i] = (91881 * $x + 32768) >> 16; | |
| 679 | + $tab['bcb'][$i] = (116130 * $x + 32768) >> 16; | |
| 680 | + $tab['gcr'][$i] = -46802 * $x; | |
| 681 | + $tab['gcb'][$i] = -22554 * $x + 32768; | |
| 682 | + } | |
| 683 | + } | |
| 684 | + $rcr = $tab['rcr']; | |
| 685 | + $bcb = $tab['bcb']; | |
| 686 | + $gcr = $tab['gcr']; | |
| 687 | + $gcb = $tab['gcb']; | |
| 688 | + $out = ''; | |
| 689 | + for ($y = 0; $y < $h; $y++) { | |
| 690 | + if ($n === 1) { | |
| 691 | + $s = $src[0]; | |
| 692 | + $out .= preg_replace('/(.)/s', '$1$1$1'."\xff", substr($s['plane'], $y * $s['pw'], $w)); | |
| 693 | + continue; | |
| 694 | + } | |
| 695 | + $c0 = $this->upsampleRow($src[0], $y, $w); | |
| 696 | + $c1 = $this->upsampleRow($src[1], $y, $w); | |
| 697 | + $c2 = $this->upsampleRow($src[2], $y, $w); | |
| 698 | + $c3 = $n === 4 ? $this->upsampleRow($src[3], $y, $w) : null; | |
| 699 | + $line = []; | |
| 700 | + for ($x = 0; $x < $w; $x++) { | |
| 701 | + $a = $c0[$x]; | |
| 702 | + $b = $c1[$x]; | |
| 703 | + $c = $c2[$x]; | |
| 704 | + if ($ycc) { | |
| 705 | + $r = $a + $rcr[$c]; | |
| 706 | + $g = $a + (($gcb[$b] + $gcr[$c]) >> 16); | |
| 707 | + $bl = $a + $bcb[$b]; | |
| 708 | + $r = $r < 0 ? 0 : ($r > 255 ? 255 : $r); | |
| 709 | + $g = $g < 0 ? 0 : ($g > 255 ? 255 : $g); | |
| 710 | + $bl = $bl < 0 ? 0 : ($bl > 255 ? 255 : $bl); | |
| 711 | + } else { | |
| 712 | + $r = $a; | |
| 713 | + $g = $b; | |
| 714 | + $bl = $c; | |
| 715 | + } | |
| 716 | + if ($n === 4) { | |
| 717 | + // CMYK(Adobeは反転して保存する):R = C × K ÷ 255 | |
| 718 | + $k = $c3[$x]; | |
| 719 | + if ($transform === null) { | |
| 720 | + $r = 255 - $r; | |
| 721 | + $g = 255 - $g; | |
| 722 | + $bl = 255 - $bl; | |
| 723 | + $k = 255 - $k; | |
| 724 | + } | |
| 725 | + $r = intdiv($r * $k + 127, 255); | |
| 726 | + $g = intdiv($g * $k + 127, 255); | |
| 727 | + $bl = intdiv($bl * $k + 127, 255); | |
| 728 | + } | |
| 729 | + $line[] = $r; | |
| 730 | + $line[] = $g; | |
| 731 | + $line[] = $bl; | |
| 732 | + $line[] = 255; | |
| 733 | + } | |
| 734 | + $out .= pack('C*', ...$line); | |
| 735 | + } | |
| 736 | + return new Bitmap($w, $h, $out); | |
| 737 | + } | |
| 738 | + | |
| 739 | + /** | |
| 740 | + * 色の面の1行を画像の幅にする(2倍はlibjpegの三角フィルター、それ以外は繰り返し) | |
| 741 | + * | |
| 742 | + * @return array 整数の配列(幅$w) | |
| 743 | + */ | |
| 744 | + private function upsampleRow(array $s, int $y, int $w): array { | |
| 745 | + $sx = $s['sx']; | |
| 746 | + $sy = $s['sy']; | |
| 747 | + $row = fn(int $r) => array_values(unpack('C*', substr($s['plane'], max(0, min($s['rows'] - 1, $r)) * $s['pw'], $s['cols']))); | |
| 748 | + if ($sy === 2 && ($sx === 1 || $sx === 2)) { | |
| 749 | + // 縦:近い行×3+隣の行(上の出力行は上の行、下の出力行は下の行) | |
| 750 | + $cur = $row($y >> 1); | |
| 751 | + $near = $row(($y >> 1) + ($y & 1 ? 1 : -1)); | |
| 752 | + $cols = count($cur); | |
| 753 | + if ($sx === 1) { | |
| 754 | + $line = []; | |
| 755 | + for ($x = 0; $x < $w; $x++) $line[] = ($cur[$x] * 3 + $near[$x] + 1 + ($y & 1)) >> 2; | |
| 756 | + return $line; | |
| 757 | + } | |
| 758 | + $sum = []; | |
| 759 | + for ($x = 0; $x < $cols; $x++) $sum[$x] = $cur[$x] * 3 + $near[$x]; | |
| 760 | + $line = []; | |
| 761 | + for ($x = 0; $x < $cols; $x++) { | |
| 762 | + $prev = $x > 0 ? $sum[$x - 1] : $sum[$x]; | |
| 763 | + $nxt = $x < $cols - 1 ? $sum[$x + 1] : $sum[$x]; | |
| 764 | + $line[] = ($sum[$x] * 3 + $prev + 8) >> 4; | |
| 765 | + $line[] = ($sum[$x] * 3 + $nxt + 7) >> 4; | |
| 766 | + } | |
| 767 | + return array_slice($line, 0, $w); | |
| 768 | + } | |
| 769 | + $cur = $row(intdiv($y, $sy)); | |
| 770 | + if ($sx === 1) return array_slice($cur, 0, $w); | |
| 771 | + $cols = count($cur); | |
| 772 | + $line = []; | |
| 773 | + if ($sx === 2) { | |
| 774 | + for ($x = 0; $x < $cols; $x++) { | |
| 775 | + $prev = $x > 0 ? $cur[$x - 1] : $cur[$x]; | |
| 776 | + $nxt = $x < $cols - 1 ? $cur[$x + 1] : $cur[$x]; | |
| 777 | + $line[] = ($cur[$x] * 3 + $prev + 1) >> 2; | |
| 778 | + $line[] = ($cur[$x] * 3 + $nxt + 2) >> 2; | |
| 779 | + } | |
| 780 | + return array_slice($line, 0, $w); | |
| 781 | + } | |
| 782 | + for ($x = 0; $x < $w; $x++) $line[] = $cur[intdiv($x, $sx)]; | |
| 783 | + return $line; | |
| 784 | + } | |
| 785 | + | |
| 786 | + /** | |
| 787 | + * Exifの向きを適用する | |
| 788 | + */ | |
| 789 | + private function orient(Bitmap $img, int $o): Bitmap { | |
| 790 | + if ($o <= 1 || $o > 8) return $img; | |
| 791 | + $w = $img->width; | |
| 792 | + $h = $img->height; | |
| 793 | + $px = str_split($img->data, 4); | |
| 794 | + $swap = $o >= 5; | |
| 795 | + $nw = $swap ? $h : $w; | |
| 796 | + $nh = $swap ? $w : $h; | |
| 797 | + $out = array_fill(0, $nw * $nh, ''); | |
| 798 | + for ($y = 0; $y < $h; $y++) { | |
| 799 | + for ($x = 0; $x < $w; $x++) { | |
| 800 | + [$nx, $ny] = match ($o) { | |
| 801 | + 2 => [$w - 1 - $x, $y], | |
| 802 | + 3 => [$w - 1 - $x, $h - 1 - $y], | |
| 803 | + 4 => [$x, $h - 1 - $y], | |
| 804 | + 5 => [$y, $x], | |
| 805 | + 6 => [$h - 1 - $y, $x], | |
| 806 | + 7 => [$h - 1 - $y, $w - 1 - $x], | |
| 807 | + 8 => [$y, $w - 1 - $x], | |
| 808 | + }; | |
| 809 | + $out[$ny * $nw + $nx] = $px[$y * $w + $x]; | |
| 810 | + } | |
| 811 | + } | |
| 812 | + return new Bitmap($nw, $nh, implode('', $out)); | |
| 813 | + } | |
| 814 | + | |
| 815 | + //////// 書き出し | |
| 816 | + | |
| 817 | + /** 標準の量子化表(JPEGの附属書K、自然な順):輝度、色差 */ | |
| 818 | + private const QT = [ | |
| 819 | + [16, 11, 10, 16, 24, 40, 51, 61, 12, 12, 14, 19, 26, 58, 60, 55, 14, 13, 16, 24, 40, 57, 69, 56, 14, 17, 22, 29, 51, 87, 80, 62, | |
| 820 | + 18, 22, 37, 56, 68, 109, 103, 77, 24, 35, 55, 64, 81, 104, 113, 92, 49, 64, 78, 87, 103, 121, 120, 101, 72, 92, 95, 98, 112, 100, 103, 99], | |
| 821 | + [17, 18, 24, 47, 99, 99, 99, 99, 18, 21, 26, 66, 99, 99, 99, 99, 24, 26, 56, 99, 99, 99, 99, 99, 47, 66, 99, 99, 99, 99, 99, 99, | |
| 822 | + 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99, 99], | |
| 823 | + ]; | |
| 824 | + /** 標準のハフマン表(附属書K.3):[符号長毎の数, 記号] */ | |
| 825 | + private const HT = [ | |
| 826 | + 'dc0' => [[0, 1, 5, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0], [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]], | |
| 827 | + 'dc1' => [[0, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0], [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]], | |
| 828 | + 'ac0' => [[0, 2, 1, 3, 3, 2, 4, 3, 5, 5, 4, 4, 0, 0, 1, 0x7d], [ | |
| 829 | + 0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, 0x21, 0x31, 0x41, 0x06, 0x13, 0x51, 0x61, 0x07, 0x22, 0x71, 0x14, 0x32, 0x81, 0x91, 0xa1, 0x08, | |
| 830 | + 0x23, 0x42, 0xb1, 0xc1, 0x15, 0x52, 0xd1, 0xf0, 0x24, 0x33, 0x62, 0x72, 0x82, 0x09, 0x0a, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x25, 0x26, 0x27, 0x28, | |
| 831 | + 0x29, 0x2a, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, | |
| 832 | + 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, | |
| 833 | + 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, | |
| 834 | + 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xe1, 0xe2, | |
| 835 | + 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa]], | |
| 836 | + 'ac1' => [[0, 2, 1, 2, 4, 4, 3, 4, 7, 5, 4, 4, 0, 1, 2, 0x77], [ | |
| 837 | + 0x00, 0x01, 0x02, 0x03, 0x11, 0x04, 0x05, 0x21, 0x31, 0x06, 0x12, 0x41, 0x51, 0x07, 0x61, 0x71, 0x13, 0x22, 0x32, 0x81, 0x08, 0x14, 0x42, 0x91, | |
| 838 | + 0xa1, 0xb1, 0xc1, 0x09, 0x23, 0x33, 0x52, 0xf0, 0x15, 0x62, 0x72, 0xd1, 0x0a, 0x16, 0x24, 0x34, 0xe1, 0x25, 0xf1, 0x17, 0x18, 0x19, 0x1a, 0x26, | |
| 839 | + 0x27, 0x28, 0x29, 0x2a, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, | |
| 840 | + 0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, | |
| 841 | + 0x88, 0x89, 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xb2, 0xb3, 0xb4, | |
| 842 | + 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, | |
| 843 | + 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa]], | |
| 844 | + ]; | |
| 845 | + | |
| 846 | + private string $out = ''; | |
| 847 | + private int $obuf = 0; | |
| 848 | + private int $obits = 0; | |
| 849 | + | |
| 850 | + /** | |
| 851 | + * 画素をJPEG(ベースライン)にする | |
| 852 | + * | |
| 853 | + * 透明な部分は背景色(既定は白)と合成する。灰色だけの画像はグレースケールにする。 | |
| 854 | + * | |
| 855 | + * @param array $opt quality:1~100(既定90)、subsampling:'420'(既定)又は'444'、background:[r, g, b] | |
| 856 | + */ | |
| 857 | + public function encode(Bitmap $img, array $opt = []): string { | |
| 858 | + $quality = max(1, min(100, (int)($opt['quality'] ?? 90))); | |
| 859 | + $scale = $quality < 50 ? intdiv(5000, $quality) : 200 - $quality * 2; | |
| 860 | + $qt = []; | |
| 861 | + foreach (self::QT as $i => $base) $qt[$i] = array_map(fn($v) => max(1, min(255, intdiv($v * $scale + 50, 100))), $base); | |
| 862 | + | |
| 863 | + $w = $img->width; | |
| 864 | + $h = $img->height; | |
| 865 | + $gray = $img->isGray(); | |
| 866 | + $sub = !$gray && ($opt['subsampling'] ?? '420') !== '444'; | |
| 867 | + [$br, $bg, $bb] = $opt['background'] ?? [255, 255, 255]; | |
| 868 | + | |
| 869 | + // YCbCrの面(端は最後の画素を繰り返して、MCUの倍数にする) | |
| 870 | + $mcu = $sub ? 16 : 8; | |
| 871 | + $pw = intdiv($w + $mcu - 1, $mcu) * $mcu; | |
| 872 | + $ph = intdiv($h + $mcu - 1, $mcu) * $mcu; | |
| 873 | + $Y = $Cb = $Cr = []; | |
| 874 | + for ($y = 0; $y < $ph; $y++) { | |
| 875 | + $row = $img->row(min($y, $h - 1)); | |
| 876 | + for ($x = 0; $x < $pw; $x++) { | |
| 877 | + $o = min($x, $w - 1) * 4; | |
| 878 | + $a = ord($row[$o + 3]); | |
| 879 | + $r = ord($row[$o]); | |
| 880 | + $g = ord($row[$o + 1]); | |
| 881 | + $b = ord($row[$o + 2]); | |
| 882 | + if ($a !== 255) { | |
| 883 | + $r = intdiv($r * $a + $br * (255 - $a) + 127, 255); | |
| 884 | + $g = intdiv($g * $a + $bg * (255 - $a) + 127, 255); | |
| 885 | + $b = intdiv($b * $a + $bb * (255 - $a) + 127, 255); | |
| 886 | + } | |
| 887 | + $Y[] = 0.299 * $r + 0.587 * $g + 0.114 * $b - 128; | |
| 888 | + if (!$gray) { | |
| 889 | + $Cb[] = -0.168736 * $r - 0.331264 * $g + 0.5 * $b; | |
| 890 | + $Cr[] = 0.5 * $r - 0.418688 * $g - 0.081312 * $b; | |
| 891 | + } | |
| 892 | + } | |
| 893 | + } | |
| 894 | + if ($sub) { | |
| 895 | + // 2x2の平均 | |
| 896 | + $cw = $pw >> 1; | |
| 897 | + $sCb = $sCr = []; | |
| 898 | + for ($y = 0; $y < ($ph >> 1); $y++) { | |
| 899 | + for ($x = 0; $x < $cw; $x++) { | |
| 900 | + $i = ($y * 2) * $pw + $x * 2; | |
| 901 | + $sCb[] = ($Cb[$i] + $Cb[$i + 1] + $Cb[$i + $pw] + $Cb[$i + $pw + 1]) / 4; | |
| 902 | + $sCr[] = ($Cr[$i] + $Cr[$i + 1] + $Cr[$i + $pw] + $Cr[$i + $pw + 1]) / 4; | |
| 903 | + } | |
| 904 | + } | |
| 905 | + $Cb = $sCb; | |
| 906 | + $Cr = $sCr; | |
| 907 | + } | |
| 908 | + | |
| 909 | + $codes = []; | |
| 910 | + foreach (self::HT as $k => [$counts, $syms]) { | |
| 911 | + $codes[$k] = []; | |
| 912 | + $code = $n = 0; | |
| 913 | + for ($l = 1; $l <= 16; $l++) { | |
| 914 | + for ($i = 0; $i < $counts[$l - 1]; $i++) $codes[$k][$syms[$n++]] = [$code++, $l]; | |
| 915 | + $code <<= 1; | |
| 916 | + } | |
| 917 | + } | |
| 918 | + | |
| 919 | + $this->out = ''; | |
| 920 | + $this->obuf = $this->obits = 0; | |
| 921 | + $pred = [0, 0, 0]; | |
| 922 | + $block = function(array $plane, int $stride, int $bx, int $by, int $c) use ($qt, $codes, &$pred): void { | |
| 923 | + $f = []; | |
| 924 | + for ($y = 0; $y < 8; $y++) { | |
| 925 | + $o = ($by * 8 + $y) * $stride + $bx * 8; | |
| 926 | + for ($x = 0; $x < 8; $x++) $f[] = $plane[$o + $x]; | |
| 927 | + } | |
| 928 | + $this->fdct($f, $qt[$c === 0 ? 0 : 1], $c === 0 ? $codes['dc0'] : $codes['dc1'], $c === 0 ? $codes['ac0'] : $codes['ac1'], $pred[$c]); | |
| 929 | + }; | |
| 930 | + $mx = intdiv($pw, $mcu); | |
| 931 | + $my = intdiv($ph, $mcu); | |
| 932 | + for ($y = 0; $y < $my; $y++) { | |
| 933 | + for ($x = 0; $x < $mx; $x++) { | |
| 934 | + if ($sub) { | |
| 935 | + for ($v = 0; $v < 2; $v++) for ($u = 0; $u < 2; $u++) $block($Y, $pw, $x * 2 + $u, $y * 2 + $v, 0); | |
| 936 | + $block($Cb, $pw >> 1, $x, $y, 1); | |
| 937 | + $block($Cr, $pw >> 1, $x, $y, 2); | |
| 938 | + } else { | |
| 939 | + $block($Y, $pw, $x, $y, 0); | |
| 940 | + if (!$gray) { | |
| 941 | + $block($Cb, $pw, $x, $y, 1); | |
| 942 | + $block($Cr, $pw, $x, $y, 2); | |
| 943 | + } | |
| 944 | + } | |
| 945 | + } | |
| 946 | + } | |
| 947 | + // 残りのビットを1で埋める | |
| 948 | + if ($this->obits > 0) $this->put((1 << (8 - $this->obits)) - 1, 8 - $this->obits); | |
| 949 | + $data = $this->out; | |
| 950 | + $this->out = ''; | |
| 951 | + | |
| 952 | + $seg = fn(int $m, string $d) => "\xff".chr($m).pack('n', strlen($d) + 2).$d; | |
| 953 | + $zz = fn(array $q) => pack('C64', ...array_map(fn($z) => $q[$z], self::ZIGZAG)); | |
| 954 | + $jpg = "\xff\xd8".$seg(0xe0, "JFIF\0\x01\x01\0".pack('nn', 1, 1)."\0\0"); | |
| 955 | + $jpg .= $seg(0xdb, "\x00".$zz($qt[0]).($gray ? '' : "\x01".$zz($qt[1]))); | |
| 956 | + $comps = $gray ? "\x01\x11\x00" : "\x01".($sub ? "\x22" : "\x11")."\x00\x02\x11\x01\x03\x11\x01"; | |
| 957 | + $jpg .= $seg(0xc0, "\x08".pack('nn', $h, $w).chr($gray ? 1 : 3).$comps); | |
| 958 | + $dht = ''; | |
| 959 | + foreach (['dc0' => 0x00, 'ac0' => 0x10, 'dc1' => 0x01, 'ac1' => 0x11] as $k => $id) { | |
| 960 | + if ($gray && $k[2] === '1') continue; | |
| 961 | + $dht .= chr($id).pack('C16', ...self::HT[$k][0]).pack('C*', ...self::HT[$k][1]); | |
| 962 | + } | |
| 963 | + $jpg .= $seg(0xc4, $dht); | |
| 964 | + $sos = $gray ? "\x01\x01\x00" : "\x03\x01\x00\x02\x11\x03\x11"; | |
| 965 | + $jpg .= $seg(0xda, $sos."\x00\x3f\x00"); | |
| 966 | + return $jpg.$data."\xff\xd9"; | |
| 967 | + } | |
| 968 | + | |
| 969 | + /** | |
| 970 | + * 8x8のDCT、量子化、ハフマン符号化 | |
| 971 | + */ | |
| 972 | + private function fdct(array $f, array $q, array $dc, array $ac, int &$pred): void { | |
| 973 | + $K = self::cosTable(); | |
| 974 | + // 行 | |
| 975 | + $t = []; | |
| 976 | + for ($y = 0; $y < 8; $y++) { | |
| 977 | + $b = $y * 8; | |
| 978 | + for ($u = 0; $u < 8; $u++) { | |
| 979 | + $s = 0.0; | |
| 980 | + for ($x = 0; $x < 8; $x++) $s += $f[$b + $x] * $K[$u][$x]; | |
| 981 | + $t[$b + $u] = $s; | |
| 982 | + } | |
| 983 | + } | |
| 984 | + // 列、量子化(ジグザグの順) | |
| 985 | + $zz = []; | |
| 986 | + foreach (self::ZIGZAG as $k => $z) { | |
| 987 | + $u = $z & 7; | |
| 988 | + $v = $z >> 3; | |
| 989 | + $s = 0.0; | |
| 990 | + for ($y = 0; $y < 8; $y++) $s += $t[$y * 8 + $u] * $K[$v][$y]; | |
| 991 | + $zz[$k] = (int)round($s / $q[$z]); | |
| 992 | + } | |
| 993 | + | |
| 994 | + $diff = $zz[0] - $pred; | |
| 995 | + $pred = $zz[0]; | |
| 996 | + $this->value($diff, $dc); | |
| 997 | + $run = 0; | |
| 998 | + for ($k = 1; $k < 64; $k++) { | |
| 999 | + $v = $zz[$k]; | |
| 1000 | + if ($v === 0) { | |
| 1001 | + $run++; | |
| 1002 | + continue; | |
| 1003 | + } | |
| 1004 | + while ($run > 15) { | |
| 1005 | + $this->put(...$ac[0xf0]); | |
| 1006 | + $run -= 16; | |
| 1007 | + } | |
| 1008 | + $this->value($v, $ac, $run); | |
| 1009 | + $run = 0; | |
| 1010 | + } | |
| 1011 | + if ($run > 0) $this->put(...$ac[0x00]); | |
| 1012 | + } | |
| 1013 | + | |
| 1014 | + /** 値を「(run << 4 | 大きさ)の符号+ビット」で書く */ | |
| 1015 | + private function value(int $v, array $table, int $run = -1): void { | |
| 1016 | + $a = abs($v); | |
| 1017 | + $size = 0; | |
| 1018 | + while ($a) { | |
| 1019 | + $size++; | |
| 1020 | + $a >>= 1; | |
| 1021 | + } | |
| 1022 | + $sym = $run < 0 ? $size : ($run << 4) | $size; | |
| 1023 | + $this->put(...$table[$sym]); | |
| 1024 | + if ($size) $this->put($v < 0 ? $v + (1 << $size) - 1 : $v, $size); | |
| 1025 | + } | |
| 1026 | + | |
| 1027 | + private function put(int $code, int $len): void { | |
| 1028 | + $this->obuf = ($this->obuf << $len) | ($code & ((1 << $len) - 1)); | |
| 1029 | + $this->obits += $len; | |
| 1030 | + while ($this->obits >= 8) { | |
| 1031 | + $this->obits -= 8; | |
| 1032 | + $byte = ($this->obuf >> $this->obits) & 255; | |
| 1033 | + $this->out .= $byte === 0xff ? "\xff\x00" : chr($byte); | |
| 1034 | + } | |
| 1035 | + $this->obuf &= (1 << $this->obits) - 1; | |
| 89 | 1036 | } |
| 90 | 1037 | } |
| @@ -640,6 +640,443 @@ | ||
| 640 | 640 | |
| 641 | 641 | /////////////// |
| 642 | 642 | |
| 643 | + //////// 画素の読み書き | |
| 644 | + | |
| 645 | + private const SIGNATURE = "\x89PNG\r\n\x1a\n"; | |
| 646 | + /** Adam7の各段階:[x開始, y開始, x間隔, y間隔] */ | |
| 647 | + private const ADAM7 = [[0, 0, 8, 8], [4, 0, 8, 8], [0, 4, 4, 8], [2, 0, 4, 4], [0, 2, 2, 4], [1, 0, 2, 2], [0, 1, 1, 2]]; | |
| 648 | + | |
| 649 | + /** | |
| 650 | + * PNG(APNGを含む)を画素にする | |
| 651 | + * | |
| 652 | + * 全ての色類とビット深さ(16ビットは上位8ビット)、PLTE・tRNS、Adam7、APNGのフレームに対応。 | |
| 653 | + * 必須のチャンクのCRCが合わない場合は例外。 | |
| 654 | + */ | |
| 655 | + public function decode(string $data): Bitmap { | |
| 656 | + if (!str_starts_with($data, self::SIGNATURE)) throw new \RuntimeException('PNGではありません。'); | |
| 657 | + $len = strlen($data); | |
| 658 | + $pos = 8; | |
| 659 | + $hdr = null; | |
| 660 | + $palette = null; | |
| 661 | + $trns = null; | |
| 662 | + $idat = ''; | |
| 663 | + $actl = null; | |
| 664 | + $frames = []; | |
| 665 | + $seenIdat = false; | |
| 666 | + $firstIsDefault = false; | |
| 667 | + $ended = false; | |
| 668 | + | |
| 669 | + while ($pos + 12 <= $len) { | |
| 670 | + $clen = unpack('N', substr($data, $pos, 4))[1]; | |
| 671 | + $type = substr($data, $pos + 4, 4); | |
| 672 | + if ($clen > $len - $pos - 12) throw new \RuntimeException("{$type}チャンクが途中で切れています。"); | |
| 673 | + $body = substr($data, $pos + 8, $clen); | |
| 674 | + $crc = unpack('N', substr($data, $pos + 8 + $clen, 4))[1]; | |
| 675 | + $pos += 12 + $clen; | |
| 676 | + if (crc32($type.$body) !== $crc) { | |
| 677 | + // 補助チャンク(1文字目が小文字)は飛ばす | |
| 678 | + if (ctype_lower($type[0])) continue; | |
| 679 | + throw new \RuntimeException("{$type}チャンクのCRCが合いません。"); | |
| 680 | + } | |
| 681 | + | |
| 682 | + switch ($type) { | |
| 683 | + case 'IHDR': | |
| 684 | + if ($clen !== 13) throw new \RuntimeException('IHDRが不正です。'); | |
| 685 | + $h = unpack('Nw/Nh/Cdepth/Cctype/Ccomp/Cfilter/Cinterlace', $body); | |
| 686 | + $ok = match ($h['ctype']) { | |
| 687 | + 0 => in_array($h['depth'], [1, 2, 4, 8, 16], true), | |
| 688 | + 3 => in_array($h['depth'], [1, 2, 4, 8], true), | |
| 689 | + 2, 4, 6 => in_array($h['depth'], [8, 16], true), | |
| 690 | + default => false, | |
| 691 | + }; | |
| 692 | + if (!$ok || $h['comp'] !== 0 || $h['filter'] !== 0 || $h['interlace'] > 1) throw new \RuntimeException('未対応の色類・ビット深さです。'); | |
| 693 | + Bitmap::check($h['w'], $h['h']); | |
| 694 | + $hdr = $h; | |
| 695 | + break; | |
| 696 | + case 'PLTE': | |
| 697 | + if ($clen % 3 !== 0 || $clen > 768 || $clen === 0) throw new \RuntimeException('PLTEが不正です。'); | |
| 698 | + $palette = $body; | |
| 699 | + break; | |
| 700 | + case 'tRNS': | |
| 701 | + $trns = $body; | |
| 702 | + break; | |
| 703 | + case 'acTL': | |
| 704 | + $actl = unpack('Nframes/Nplays', $body); | |
| 705 | + break; | |
| 706 | + case 'fcTL': | |
| 707 | + if ($clen !== 26) throw new \RuntimeException('fcTLが不正です。'); | |
| 708 | + $f = unpack('Nseq/Nw/Nh/Nx/Ny/ndnum/nden/Cdispose/Cblend', $body); | |
| 709 | + if (!$seenIdat) $firstIsDefault = true; | |
| 710 | + $frames[] = ['ctl' => $f, 'data' => '']; | |
| 711 | + break; | |
| 712 | + case 'IDAT': | |
| 713 | + $seenIdat = true; | |
| 714 | + $idat .= $body; | |
| 715 | + if ($firstIsDefault && count($frames) === 1) $frames[0]['data'] .= $body; | |
| 716 | + break; | |
| 717 | + case 'fdAT': | |
| 718 | + if ($frames) $frames[count($frames) - 1]['data'] .= substr($body, 4); | |
| 719 | + break; | |
| 720 | + case 'IEND': | |
| 721 | + $ended = true; | |
| 722 | + break 2; | |
| 723 | + } | |
| 724 | + } | |
| 725 | + if ($hdr === null || $idat === '') throw new \RuntimeException('IHDRかIDATがありません。'); | |
| 726 | + if (!$ended) throw new \RuntimeException('IENDがありません。'); | |
| 727 | + if ($hdr['ctype'] === 3 && $palette === null) throw new \RuntimeException('PLTEがありません。'); | |
| 728 | + | |
| 729 | + $img = new Bitmap($hdr['w'], $hdr['h'], self::pixels($idat, $hdr['w'], $hdr['h'], $hdr, $palette, $trns)); | |
| 730 | + if ($actl === null || !$frames) return $img; | |
| 731 | + | |
| 732 | + // APNG:各フレームを画面に合成する | |
| 733 | + $canvas = new Bitmap($hdr['w'], $hdr['h']); | |
| 734 | + $img->loops = $actl['plays']; | |
| 735 | + foreach ($frames as $i => $fr) { | |
| 736 | + $c = $fr['ctl']; | |
| 737 | + if ($c['x'] + $c['w'] > $hdr['w'] || $c['y'] + $c['h'] > $hdr['h']) throw new \RuntimeException('APNGのフレームが画面からはみ出しています。'); | |
| 738 | + $sub = new Bitmap($c['w'], $c['h'], self::pixels($fr['data'], $c['w'], $c['h'], $hdr, $palette, $trns)); | |
| 739 | + // 最初のフレームの「前に戻す」は「消す」と同じ | |
| 740 | + $dispose = $i === 0 && $c['dispose'] === 2 ? 1 : $c['dispose']; | |
| 741 | + $before = $dispose === 2 ? $canvas->copy() : null; | |
| 742 | + $canvas->draw($sub, $c['x'], $c['y'], $c['blend'] === 1); | |
| 743 | + $img->frames[] = ['bitmap' => $canvas->copy(), 'delay' => (int)round($c['dnum'] * 1000 / ($c['den'] ?: 100))]; | |
| 744 | + if ($dispose === 1) $canvas->clear($c['x'], $c['y'], $c['w'], $c['h']); | |
| 745 | + else if ($dispose === 2) $canvas = $before; | |
| 746 | + } | |
| 747 | + return $img; | |
| 748 | + } | |
| 749 | + | |
| 750 | + /** | |
| 751 | + * 圧縮した画像データ(IDAT・fdAT)をRGBAにする | |
| 752 | + */ | |
| 753 | + private static function pixels(string $zdata, int $w, int $h, array $hdr, ?string $palette, ?string $trns): string { | |
| 754 | + $channels = [0 => 1, 2 => 3, 3 => 1, 4 => 2, 6 => 4][$hdr['ctype']]; | |
| 755 | + $bits = $channels * $hdr['depth']; | |
| 756 | + $bpp = max(1, $bits >> 3); | |
| 757 | + $rowBytes = fn(int $pw) => ($pw * $bits + 7) >> 3; | |
| 758 | + | |
| 759 | + $passes = $hdr['interlace'] ? self::ADAM7 : [[0, 0, 1, 1]]; | |
| 760 | + $expected = 0; | |
| 761 | + foreach ($passes as [$xs, $ys, $xd, $yd]) { | |
| 762 | + $pw = intdiv($w - $xs + $xd - 1, $xd); | |
| 763 | + $ph = intdiv($h - $ys + $yd - 1, $yd); | |
| 764 | + if ($pw > 0 && $ph > 0) $expected += $ph * (1 + $rowBytes($pw)); | |
| 765 | + } | |
| 766 | + // 展開する大きさを制限する(圧縮爆弾対策) | |
| 767 | + $raw = @gzuncompress($zdata, $expected + 1024); | |
| 768 | + if ($raw === false) throw new \RuntimeException('画像データを展開出来ません。'); | |
| 769 | + if (strlen($raw) < $expected) throw new \RuntimeException('画像データが足りません。'); | |
| 770 | + | |
| 771 | + $lut = self::lookup($hdr, $palette, $trns); | |
| 772 | + if (!$hdr['interlace']) { | |
| 773 | + $rows = self::unfilter($raw, 0, $rowBytes($w), $h, $bpp); | |
| 774 | + return implode('', array_map(fn($r) => self::toRgba($r, $w, $hdr, $lut, $trns), $rows)); | |
| 775 | + } | |
| 776 | + | |
| 777 | + $grid = array_fill(0, $h, array_fill(0, $w, "\0\0\0\0")); | |
| 778 | + $off = 0; | |
| 779 | + foreach (self::ADAM7 as [$xs, $ys, $xd, $yd]) { | |
| 780 | + $pw = intdiv($w - $xs + $xd - 1, $xd); | |
| 781 | + $ph = intdiv($h - $ys + $yd - 1, $yd); | |
| 782 | + if ($pw <= 0 || $ph <= 0) continue; | |
| 783 | + $rows = self::unfilter($raw, $off, $rowBytes($pw), $ph, $bpp); | |
| 784 | + $off += $ph * (1 + $rowBytes($pw)); | |
| 785 | + foreach ($rows as $j => $r) { | |
| 786 | + $px = str_split(self::toRgba($r, $pw, $hdr, $lut, $trns), 4); | |
| 787 | + $y = $ys + $j * $yd; | |
| 788 | + foreach ($px as $i => $p) $grid[$y][$xs + $i * $xd] = $p; | |
| 789 | + } | |
| 790 | + } | |
| 791 | + return implode('', array_map(fn($r) => implode('', $r), $grid)); | |
| 792 | + } | |
| 793 | + | |
| 794 | + /** | |
| 795 | + * フィルターを戻す | |
| 796 | + * | |
| 797 | + * @return array 行(フィルターの種類のバイトを除いた文字列) | |
| 798 | + */ | |
| 799 | + private static function unfilter(string $raw, int $off, int $rowBytes, int $rows, int $bpp): array { | |
| 800 | + $out = []; | |
| 801 | + $prev = array_fill(0, $rowBytes, 0); | |
| 802 | + for ($y = 0; $y < $rows; $y++) { | |
| 803 | + $ft = ord($raw[$off]); | |
| 804 | + $line = substr($raw, $off + 1, $rowBytes); | |
| 805 | + $off += $rowBytes + 1; | |
| 806 | + if ($ft === 0) { | |
| 807 | + $out[] = $line; | |
| 808 | + $prev = array_values(unpack('C*', $line)); | |
| 809 | + continue; | |
| 810 | + } | |
| 811 | + $cur = array_values(unpack('C*', $line)); | |
| 812 | + switch ($ft) { | |
| 813 | + case 1: | |
| 814 | + for ($i = $bpp; $i < $rowBytes; $i++) $cur[$i] = ($cur[$i] + $cur[$i - $bpp]) & 255; | |
| 815 | + break; | |
| 816 | + case 2: | |
| 817 | + for ($i = 0; $i < $rowBytes; $i++) $cur[$i] = ($cur[$i] + $prev[$i]) & 255; | |
| 818 | + break; | |
| 819 | + case 3: | |
| 820 | + for ($i = 0; $i < $bpp; $i++) $cur[$i] = ($cur[$i] + ($prev[$i] >> 1)) & 255; | |
| 821 | + for (; $i < $rowBytes; $i++) $cur[$i] = ($cur[$i] + (($cur[$i - $bpp] + $prev[$i]) >> 1)) & 255; | |
| 822 | + break; | |
| 823 | + case 4: | |
| 824 | + for ($i = 0; $i < $bpp; $i++) $cur[$i] = ($cur[$i] + $prev[$i]) & 255; | |
| 825 | + for (; $i < $rowBytes; $i++) { | |
| 826 | + $a = $cur[$i - $bpp]; | |
| 827 | + $b = $prev[$i]; | |
| 828 | + $c = $prev[$i - $bpp]; | |
| 829 | + $p = $a + $b - $c; | |
| 830 | + $pa = abs($p - $a); | |
| 831 | + $pb = abs($p - $b); | |
| 832 | + $pc = abs($p - $c); | |
| 833 | + $cur[$i] = ($cur[$i] + ($pa <= $pb && $pa <= $pc ? $a : ($pb <= $pc ? $b : $c))) & 255; | |
| 834 | + } | |
| 835 | + break; | |
| 836 | + default: | |
| 837 | + throw new \RuntimeException("不正なフィルターの種類です({$ft})。"); | |
| 838 | + } | |
| 839 | + $out[] = pack('C*', ...$cur); | |
| 840 | + $prev = $cur; | |
| 841 | + } | |
| 842 | + return $out; | |
| 843 | + } | |
| 844 | + | |
| 845 | + /** | |
| 846 | + * パレット又は灰色(8ビット以下)の値 => RGBA(4バイト)の表 | |
| 847 | + */ | |
| 848 | + private static function lookup(array $hdr, ?string $palette, ?string $trns): ?array { | |
| 849 | + if ($hdr['ctype'] === 3) { | |
| 850 | + $lut = []; | |
| 851 | + $n = intdiv(strlen($palette), 3); | |
| 852 | + for ($i = 0; $i < 256; $i++) { | |
| 853 | + // パレットに無い指標は黒(不正なファイルでも落ちない様に) | |
| 854 | + $rgb = $i < $n ? substr($palette, $i * 3, 3) : "\0\0\0"; | |
| 855 | + $a = $trns !== null && $i < strlen($trns) ? $trns[$i] : "\xff"; | |
| 856 | + $lut[chr($i)] = $rgb.$a; | |
| 857 | + } | |
| 858 | + return $lut; | |
| 859 | + } | |
| 860 | + if ($hdr['ctype'] === 0 && $hdr['depth'] <= 8) { | |
| 861 | + $max = (1 << $hdr['depth']) - 1; | |
| 862 | + $key = $trns !== null && strlen($trns) >= 2 ? unpack('n', $trns)[1] : -1; | |
| 863 | + $lut = []; | |
| 864 | + for ($i = 0; $i <= $max; $i++) { | |
| 865 | + $g = chr(intdiv($i * 255 + intdiv($max, 2), $max)); | |
| 866 | + $lut[chr($i)] = $g.$g.$g.($i === $key ? "\0" : "\xff"); | |
| 867 | + } | |
| 868 | + return $lut; | |
| 869 | + } | |
| 870 | + return null; | |
| 871 | + } | |
| 872 | + | |
| 873 | + /** | |
| 874 | + * フィルターを戻した1行をRGBAにする | |
| 875 | + */ | |
| 876 | + private static function toRgba(string $row, int $w, array $hdr, ?array $lut, ?string $trns): string { | |
| 877 | + $depth = $hdr['depth']; | |
| 878 | + $ctype = $hdr['ctype']; | |
| 879 | + | |
| 880 | + if ($lut !== null) { | |
| 881 | + if ($depth < 8) { | |
| 882 | + // 1バイトに入っている複数の値を1バイトずつにする | |
| 883 | + static $unpack = []; | |
| 884 | + if (!isset($unpack[$depth])) { | |
| 885 | + $n = 8 / $depth; | |
| 886 | + $mask = (1 << $depth) - 1; | |
| 887 | + for ($b = 0; $b < 256; $b++) { | |
| 888 | + $s = ''; | |
| 889 | + for ($k = $n - 1; $k >= 0; $k--) $s .= chr(($b >> ($k * $depth)) & $mask); | |
| 890 | + $unpack[$depth][chr($b)] = $s; | |
| 891 | + } | |
| 892 | + } | |
| 893 | + $row = substr(strtr($row, $unpack[$depth]), 0, $w); | |
| 894 | + } | |
| 895 | + return strtr($row, $lut); | |
| 896 | + } | |
| 897 | + | |
| 898 | + // 16ビット:透明色の比較は16ビットで、表示は上位8ビット | |
| 899 | + $key = null; | |
| 900 | + if ($trns !== null && ($ctype === 0 || $ctype === 2)) $key = $ctype === 0 ? substr($trns, 0, 2) : substr($trns, 0, 6); | |
| 901 | + if ($depth === 16) { | |
| 902 | + $samples = $row; | |
| 903 | + $row = preg_replace('/(.)./s', '$1', $row); | |
| 904 | + } | |
| 905 | + | |
| 906 | + switch ($ctype) { | |
| 907 | + case 0: // 16ビットの灰色(8ビット以下はlookup) | |
| 908 | + $out = preg_replace('/(.)/s', "\$1\$1\$1\xff", $row); | |
| 909 | + if ($key !== null) { | |
| 910 | + for ($i = 0; $i < $w; $i++) if (substr($samples, $i * 2, 2) === $key) $out[$i * 4 + 3] = "\0"; | |
| 911 | + } | |
| 912 | + return $out; | |
| 913 | + case 2: | |
| 914 | + $out = implode("\xff", str_split($row, 3))."\xff"; | |
| 915 | + if ($key !== null) { | |
| 916 | + $sz = $depth === 16 ? 6 : 3; | |
| 917 | + $src = $depth === 16 ? $samples : $row; | |
| 918 | + $k = $depth === 16 ? $key : substr($key, 1, 1).substr($key, 3, 1).substr($key, 5, 1); | |
| 919 | + for ($i = 0; $i < $w; $i++) if (substr($src, $i * $sz, $sz) === $k) $out[$i * 4 + 3] = "\0"; | |
| 920 | + } | |
| 921 | + return $out; | |
| 922 | + case 4: | |
| 923 | + return preg_replace('/(.)(.)/s', '$1$1$1$2', $row); | |
| 924 | + default: // 6 | |
| 925 | + return $row; | |
| 926 | + } | |
| 927 | + } | |
| 928 | + | |
| 929 | + /** | |
| 930 | + * 画素をPNGにする | |
| 931 | + * | |
| 932 | + * 色の数等を見て、一番小さくなる形式を選ぶ:256色以下ならパレット(2色なら1ビット)、 | |
| 933 | + * 不透明ならアルファ無し、灰色ならグレースケール。framesがあればAPNGにする。 | |
| 934 | + * | |
| 935 | + * @param array $opt level:圧縮レベル(0~9、既定9) | |
| 936 | + */ | |
| 937 | + public function encode(Bitmap $img, array $opt = []): string { | |
| 938 | + $level = max(0, min(9, (int)($opt['level'] ?? 9))); | |
| 939 | + $images = $img->frames ? array_column($img->frames, 'bitmap') : [$img]; | |
| 940 | + foreach ($images as $f) { | |
| 941 | + if ($f->width !== $img->width || $f->height !== $img->height) throw new \InvalidArgumentException('フレームの大きさが画像と違います。'); | |
| 942 | + } | |
| 943 | + | |
| 944 | + // 色を数える(257色を超えたら止める) | |
| 945 | + $colors = []; | |
| 946 | + foreach ($images as $f) { | |
| 947 | + for ($y = 0; $y < $f->height && count($colors) <= 256; $y++) $colors += array_flip(str_split($f->row($y), 4)); | |
| 948 | + } | |
| 949 | + $opaque = true; | |
| 950 | + $gray = true; | |
| 951 | + foreach ($images as $f) { | |
| 952 | + $opaque = $opaque && $f->isOpaque(); | |
| 953 | + $gray = $gray && $f->isGray(); | |
| 954 | + } | |
| 955 | + | |
| 956 | + $plte = $trnsChunk = ''; | |
| 957 | + $map = null; | |
| 958 | + if (count($colors) <= 256) { | |
| 959 | + // 透明な色を先にする(tRNSを短くする為) | |
| 960 | + $keys = array_keys($colors); | |
| 961 | + usort($keys, fn($a, $b) => ord($a[3]) <=> ord($b[3])); | |
| 962 | + $map = []; | |
| 963 | + $alpha = ''; | |
| 964 | + foreach ($keys as $i => $k) { | |
| 965 | + $map[$k] = chr($i); | |
| 966 | + $plte .= substr($k, 0, 3); | |
| 967 | + $alpha .= $k[3]; | |
| 968 | + } | |
| 969 | + $alpha = rtrim($alpha, "\xff"); | |
| 970 | + $n = count($keys); | |
| 971 | + $depth = $n <= 2 ? 1 : ($n <= 4 ? 2 : ($n <= 16 ? 4 : 8)); | |
| 972 | + $ctype = 3; | |
| 973 | + if ($alpha !== '') $trnsChunk = self::chunk('tRNS', $alpha); | |
| 974 | + $plte = self::chunk('PLTE', $plte); | |
| 975 | + } else { | |
| 976 | + $depth = 8; | |
| 977 | + $ctype = $gray ? ($opaque ? 0 : 4) : ($opaque ? 2 : 6); | |
| 978 | + } | |
| 979 | + $channels = [0 => 1, 2 => 3, 3 => 1, 4 => 2, 6 => 4][$ctype]; | |
| 980 | + $bpp = max(1, ($channels * $depth) >> 3); | |
| 981 | + | |
| 982 | + $raw = function(Bitmap $f) use ($ctype, $depth, $map, $bpp, $level): string { | |
| 983 | + $out = ''; | |
| 984 | + $prev = null; | |
| 985 | + for ($y = 0; $y < $f->height; $y++) { | |
| 986 | + $row = $f->row($y); | |
| 987 | + $line = match ($ctype) { | |
| 988 | + 0 => preg_replace('/(.).../s', '$1', $row), | |
| 989 | + 2 => preg_replace('/(...)./s', '$1', $row), | |
| 990 | + 4 => preg_replace('/(.)..(.)/s', '$1$2', $row), | |
| 991 | + 3 => strtr($row, $map), | |
| 992 | + default => $row, | |
| 993 | + }; | |
| 994 | + if ($ctype === 3 && $depth < 8) { | |
| 995 | + $per = 8 / $depth; | |
| 996 | + $packed = ''; | |
| 997 | + foreach (str_split($line, $per) as $group) { | |
| 998 | + $b = 0; | |
| 999 | + for ($k = 0; $k < $per; $k++) $b = ($b << $depth) | ($k < strlen($group) ? ord($group[$k]) : 0); | |
| 1000 | + $packed .= chr($b); | |
| 1001 | + } | |
| 1002 | + $line = $packed; | |
| 1003 | + } | |
| 1004 | + // パレットにはフィルターを使わない(仕様の推奨)。それ以外は行毎に一番良い物を選ぶ | |
| 1005 | + $out .= $ctype === 3 ? "\0".$line : self::filter($line, $prev, $bpp); | |
| 1006 | + $prev = $line; | |
| 1007 | + } | |
| 1008 | + return gzcompress($out, $level); | |
| 1009 | + }; | |
| 1010 | + | |
| 1011 | + $png = self::SIGNATURE.self::chunk('IHDR', pack('NNCCCCC', $img->width, $img->height, $depth, $ctype, 0, 0, 0)).$plte.$trnsChunk; | |
| 1012 | + if (!$img->frames) return $png.self::chunk('IDAT', $raw($img)).self::chunk('IEND', ''); | |
| 1013 | + | |
| 1014 | + $png .= self::chunk('acTL', pack('NN', count($images), $img->loops)); | |
| 1015 | + $seq = 0; | |
| 1016 | + foreach ($img->frames as $i => $fr) { | |
| 1017 | + // 遅延はミリ秒(分母1000) | |
| 1018 | + $png .= self::chunk('fcTL', pack('NNNNNnnCC', $seq++, $img->width, $img->height, 0, 0, min(65535, max(0, (int)$fr['delay'])), 1000, 0, 0)); | |
| 1019 | + $z = $raw($fr['bitmap']); | |
| 1020 | + if ($i === 0) { | |
| 1021 | + $png .= self::chunk('IDAT', $z); | |
| 1022 | + } else { | |
| 1023 | + $png .= self::chunk('fdAT', pack('N', $seq++).$z); | |
| 1024 | + } | |
| 1025 | + } | |
| 1026 | + return $png.self::chunk('IEND', ''); | |
| 1027 | + } | |
| 1028 | + | |
| 1029 | + private static function chunk(string $type, string $data): string { | |
| 1030 | + return pack('N', strlen($data)).$type.$data.pack('N', crc32($type.$data)); | |
| 1031 | + } | |
| 1032 | + | |
| 1033 | + /** | |
| 1034 | + * 1行にフィルターを掛ける(5種類の内、差の絶対値の合計が一番小さい物:仕様の推奨する方法) | |
| 1035 | + * | |
| 1036 | + * @return string フィルターの種類のバイト+データ | |
| 1037 | + */ | |
| 1038 | + private static function filter(string $line, ?string $prevLine, int $bpp): string { | |
| 1039 | + $cur = array_values(unpack('C*', $line)); | |
| 1040 | + $n = count($cur); | |
| 1041 | + $prev = $prevLine === null ? array_fill(0, $n, 0) : array_values(unpack('C*', $prevLine)); | |
| 1042 | + $cands = []; | |
| 1043 | + $cands[0] = $cur; | |
| 1044 | + $f = $cur; | |
| 1045 | + for ($i = $bpp; $i < $n; $i++) $f[$i] = ($cur[$i] - $cur[$i - $bpp]) & 255; | |
| 1046 | + $cands[1] = $f; | |
| 1047 | + if ($prevLine !== null) { | |
| 1048 | + $f = []; | |
| 1049 | + for ($i = 0; $i < $n; $i++) $f[] = ($cur[$i] - $prev[$i]) & 255; | |
| 1050 | + $cands[2] = $f; | |
| 1051 | + $f = []; | |
| 1052 | + for ($i = 0; $i < $n; $i++) $f[] = ($cur[$i] - ((($i >= $bpp ? $cur[$i - $bpp] : 0) + $prev[$i]) >> 1)) & 255; | |
| 1053 | + $cands[3] = $f; | |
| 1054 | + $f = []; | |
| 1055 | + for ($i = 0; $i < $n; $i++) { | |
| 1056 | + $a = $i >= $bpp ? $cur[$i - $bpp] : 0; | |
| 1057 | + $b = $prev[$i]; | |
| 1058 | + $c = $i >= $bpp ? $prev[$i - $bpp] : 0; | |
| 1059 | + $p = $a + $b - $c; | |
| 1060 | + $pa = abs($p - $a); | |
| 1061 | + $pb = abs($p - $b); | |
| 1062 | + $pc = abs($p - $c); | |
| 1063 | + $f[] = ($cur[$i] - ($pa <= $pb && $pa <= $pc ? $a : ($pb <= $pc ? $b : $c))) & 255; | |
| 1064 | + } | |
| 1065 | + $cands[4] = $f; | |
| 1066 | + } | |
| 1067 | + $best = 0; | |
| 1068 | + $bestSum = PHP_INT_MAX; | |
| 1069 | + foreach ($cands as $ft => $f) { | |
| 1070 | + $sum = 0; | |
| 1071 | + foreach ($f as $v) $sum += $v < 128 ? $v : 256 - $v; | |
| 1072 | + if ($sum < $bestSum) { | |
| 1073 | + $bestSum = $sum; | |
| 1074 | + $best = $ft; | |
| 1075 | + } | |
| 1076 | + } | |
| 1077 | + return chr($best).pack('C*', ...$cands[$best]); | |
| 1078 | + } | |
| 1079 | + | |
| 643 | 1080 | private function getSamplesPerPixel(int $colorType): int { |
| 644 | 1081 | return match($colorType) { |
| 645 | 1082 | 0 => 1, // グレースケール |
| @@ -37,115 +37,230 @@ | ||
| 37 | 37 | */ |
| 38 | 38 | namespace Std\Lib\Image; |
| 39 | 39 | |
| 40 | -use Std\Lib\Image\ImageInterface; | |
| 41 | - | |
| 40 | +/** | |
| 41 | + * TGA(Truevision TGA 1.0・2.0) | |
| 42 | + * | |
| 43 | + * 種類:1(パレット)、2(トゥルーカラー)、3(グレースケール)と、そのRLE(9・10・11) | |
| 44 | + * 画素:8・15・16・24・32ビット(グレースケールは8ビット又は16ビット=灰色+アルファ) | |
| 45 | + * パレット:15・16・24・32ビット。原点:左下(既定)・左上・右上・右下 | |
| 46 | + * アルファ:属性ビット(記述子の下4ビット)と、TGA 2.0の拡張領域の属性の種類(無し・事前乗算) | |
| 47 | + */ | |
| 42 | 48 | class Targa implements ImageInterface { |
| 43 | - public int $id = 0; | |
| 44 | - public int $colorMapType = 0; | |
| 45 | - public int $imageTypeByte = 0; | |
| 46 | - public string $imageType = ''; | |
| 47 | - public int $colorMapEntryIndex = 0; | |
| 48 | - public int $colorMapLength = 0; | |
| 49 | - public int $colorMapBits = 0; | |
| 50 | - public int $x = 0; | |
| 51 | - public int $y = 0; | |
| 52 | - public int $width = 0; | |
| 53 | - public int $height = 0; | |
| 54 | - public int $bitDepth = 0; | |
| 55 | - public int $bpp = 0; | |
| 56 | - public int $interlace = 0; | |
| 57 | - public int $isRightToLeft = 0; // 0 = 左→右, 1 = 右→左 | |
| 58 | - public int $isTopToBottom = 0; // 0 = 下→上, 1 = 上→下 | |
| 59 | - public int $alphaBits = 0; | |
| 60 | - public bool $hasAlpha = false; | |
| 61 | - //-------- | |
| 62 | - public string $imageId = ''; | |
| 63 | - public string $colorMapData = ''; | |
| 64 | - public string $imageData = ''; | |
| 65 | - | |
| 66 | - private array $imageTypes = [ | |
| 67 | - 0 => 'NO DATA', | |
| 49 | + private const TYPES = [ | |
| 68 | 50 | 1 => 'UNCOMPRESSED COLOR-MAPPED IMAGE', |
| 69 | 51 | 2 => 'UNCOMPRESSED TRUE COLOR IMAGE', |
| 70 | 52 | 3 => 'UNCOMPRESSED GRAYSCALE IMAGE', |
| 71 | 53 | 9 => 'RUN-LENGTH ENCODED COLOR-MAPPED IMAGE', |
| 72 | 54 | 10 => 'RUN-LENGTH ENCODED TRUE-COLOR IMAGE', |
| 73 | 55 | 11 => 'RUN-LENGTH ENCODED GRAYSCALE IMAGE', |
| 74 | - 32 => 'HUFFMAN-DELTA-RUN-LENGTH ENCODED COLOR-MAPPED IMAGE', | |
| 75 | - 33 => 'HUFFMAN-DELTA-RUN-LENGTH-4-PASS-QUADTREE-TYPE PROCESS ENCODED COLOR-MAPPED IMAGE', | |
| 76 | 56 | ]; |
| 57 | + private const FOOTER = "TRUEVISION-XFILE.\0"; | |
| 77 | 58 | |
| 78 | 59 | public function parse(string $file): \stdClass { |
| 79 | - $tga = new \stdClass(); | |
| 80 | - $fp = fopen($file, 'rb'); | |
| 60 | + $d = (string)file_get_contents($file); | |
| 61 | + $h = $this->header($d); | |
| 62 | + return (object)[ | |
| 63 | + 'width' => $h['width'], | |
| 64 | + 'height' => $h['height'], | |
| 65 | + 'imageTypeByte' => $h['type'], | |
| 66 | + 'imageType' => self::TYPES[$h['type']], | |
| 67 | + 'bitDepth' => $h['depth'], | |
| 68 | + 'colorMapType' => $h['cmType'], | |
| 69 | + 'colorMapLength' => $h['cmLength'], | |
| 70 | + 'colorMapBits' => $h['cmBits'], | |
| 71 | + 'x' => $h['x'], | |
| 72 | + 'y' => $h['y'], | |
| 73 | + 'isTopToBottom' => $h['top'], | |
| 74 | + 'isRightToLeft' => $h['right'], | |
| 75 | + 'alphaBits' => $h['alphaBits'], | |
| 76 | + 'hasAlpha' => $h['alpha'], | |
| 77 | + 'imageId' => $h['id'], | |
| 78 | + 'version' => $h['v2'] ? 2 : 1, | |
| 79 | + ]; | |
| 80 | + } | |
| 81 | 81 | |
| 82 | - $this->id = ord(fread($fp, 1)); | |
| 83 | - $this->colorMapType = ord(fread($fp, 1)); // 0 = なし, 1 = あり, 2~127 = Truevision向け, 128~255 = 開発者向け | |
| 84 | - $this->imageTypeByte = ord(fread($fp, 1)); | |
| 85 | - $this->imageType = $this->imageTypes[$this->imageTypeByte]; | |
| 86 | - $tga->id = $this->id; | |
| 87 | - $tga->colorMapType = $this->colorMapType; | |
| 88 | - $tga->imageTypeByte = $this->imageTypeByte; | |
| 89 | - $tga->imageType = $this->imageType; | |
| 82 | + /** | |
| 83 | + * ヘッダー(とTGA 2.0のフッター・拡張領域)を読む | |
| 84 | + */ | |
| 85 | + private function header(string $d): array { | |
| 86 | + if (strlen($d) < 18) throw new \RuntimeException('TGAではありません。'); | |
| 87 | + $h = unpack('CidLen/CcmType/Ctype/vcmFirst/vcmLength/CcmBits/vx/vy/vwidth/vheight/Cdepth/Cdesc', $d); | |
| 88 | + if (!isset(self::TYPES[$h['type']])) throw new \RuntimeException("未対応のTGAの種類です({$h['type']})。"); | |
| 89 | + $okDepth = match ($h['type'] & 7) { | |
| 90 | + 1 => in_array($h['depth'], [8, 16], true) && $h['cmType'] === 1, | |
| 91 | + 2 => in_array($h['depth'], [15, 16, 24, 32], true), | |
| 92 | + 3 => in_array($h['depth'], [8, 16], true), | |
| 93 | + }; | |
| 94 | + if (!$okDepth || ($h['cmType'] === 1 && !in_array($h['cmBits'], [15, 16, 24, 32], true))) { | |
| 95 | + throw new \RuntimeException('TGAのビット深さが不正です。'); | |
| 96 | + } | |
| 97 | + Bitmap::check($h['width'], $h['height']); | |
| 90 | 98 | |
| 91 | - $this->colorMapEntryIndex = unpack('v', fread($fp, 2))[1]; | |
| 92 | - $this->colorMapLength = unpack('v', fread($fp, 2))[1]; | |
| 93 | - $this->colorMapBits = ord(fread($fp, 1)); | |
| 94 | - $tga->colorMapEntryIndex = $this->colorMapEntryIndex; | |
| 95 | - $tga->colorMapLength = $this->colorMapLength; | |
| 96 | - $tga->colorMapBits = $this->colorMapBits; | |
| 99 | + $alphaBits = $h['desc'] & 0x0f; | |
| 100 | + // TGA 2.0:拡張領域の「属性の種類」(0 = アルファ無し、1・2 = 未定義、3 = アルファ、4 = 事前乗算したアルファ) | |
| 101 | + $v2 = strlen($d) >= 44 && substr($d, -18) === self::FOOTER; | |
| 102 | + $attr = null; | |
| 103 | + if ($v2) { | |
| 104 | + $ext = unpack('V', substr($d, -26, 4))[1]; | |
| 105 | + if ($ext > 0 && $ext + 495 <= strlen($d) - 26 && unpack('v', substr($d, $ext, 2))[1] >= 495) $attr = ord($d[$ext + 494]); | |
| 106 | + } | |
| 107 | + $alpha = $alphaBits > 0 && ($attr === null || $attr >= 3); | |
| 108 | + // パレットの画像は、パレットの項目のアルファを使う | |
| 109 | + if (($h['type'] & 7) === 1) $alpha = $h['cmBits'] === 32 || ($h['cmBits'] === 16 && $alphaBits > 0); | |
| 110 | + if (($h['type'] & 7) === 3 && $h['depth'] === 16) $alpha = true; | |
| 97 | 111 | |
| 98 | - $this->x = unpack('v', fread($fp, 2))[1]; | |
| 99 | - $this->y = unpack('v', fread($fp, 2))[1]; | |
| 100 | - $this->width = unpack('v', fread($fp, 2))[1]; | |
| 101 | - $this->height = unpack('v', fread($fp, 2))[1]; | |
| 102 | - $this->bitDepth = ord(fread($fp, 1)); | |
| 103 | - $tga->x = $this->x; | |
| 104 | - $tga->y = $this->y; | |
| 105 | - $tga->width = $this->width; | |
| 106 | - $tga->height = $this->height; | |
| 107 | - $tga->bitDepth = $this->bitDepth; | |
| 112 | + return $h + [ | |
| 113 | + 'id' => substr($d, 18, $h['idLen']), | |
| 114 | + 'top' => ($h['desc'] & 0x20) !== 0, | |
| 115 | + 'right' => ($h['desc'] & 0x10) !== 0, | |
| 116 | + 'alphaBits' => $alphaBits, | |
| 117 | + 'alpha' => $alpha, | |
| 118 | + 'premultiplied' => $attr === 4, | |
| 119 | + 'v2' => $v2, | |
| 120 | + ]; | |
| 121 | + } | |
| 108 | 122 | |
| 109 | - $desc = ord(fread($fp, 1)); | |
| 110 | - $this->interlace = ($desc & 0xC0) >> 6; | |
| 111 | - $this->isTopToBottom = ($desc & 0x20) !== 0; | |
| 112 | - $this->isRightToLeft = ($desc & 0x10) !== 0; | |
| 113 | - $this->alphaBits = $desc & 0x0F; | |
| 114 | - $this->hasAlpha = ($this->alphaBits == 8) && in_array($this->imageTypeByte == [2, 3, 10, 11]); | |
| 115 | - $tga->interlace = $this->interlace; | |
| 116 | - $tga->isRightToLeft = $this->isRightToLeft; | |
| 117 | - $tga->isTopToBottom = $this->isTopToBottom; | |
| 118 | - $tga->alphaBits = $this->alphaBits; | |
| 119 | - $tga->hasAlpha = $this->hasAlpha; | |
| 123 | + public function decode(string $data): Bitmap { | |
| 124 | + $h = $this->header($data); | |
| 125 | + $w = $h['width']; | |
| 126 | + $ht = $h['height']; | |
| 127 | + $kind = $h['type'] & 7; | |
| 128 | + $pos = 18 + $h['idLen']; | |
| 120 | 129 | |
| 121 | - $needsXFlip = $tga->isRightToLeft; | |
| 122 | - $needsYFlip = !$tga->isTopToBottom; | |
| 123 | - // 0010 0000 | |
| 130 | + // パレット | |
| 131 | + $lut = null; | |
| 132 | + if ($h['cmType'] === 1) { | |
| 133 | + $eb = ($h['cmBits'] + 7) >> 3; | |
| 134 | + $cm = substr($data, $pos, $h['cmLength'] * $eb); | |
| 135 | + $pos += $h['cmLength'] * $eb; | |
| 136 | + if ($kind === 1) { | |
| 137 | + $lut = []; | |
| 138 | + $n = $h['depth'] === 16 ? 65536 : 256; | |
| 139 | + $entries = str_split($cm, $eb); | |
| 140 | + foreach ($entries as $i => $e) $lut[$h['cmFirst'] + $i] = $this->pixel($e, $h['cmBits'], $h['alpha']); | |
| 141 | + } | |
| 142 | + } | |
| 124 | 143 | |
| 125 | - //--------- | |
| 126 | - $imageIdBytes = $this->id; | |
| 127 | - $colorMapBytes = 0; | |
| 128 | - if ($this->colorMapType === 1 && $this->colorMapLength > 0) { | |
| 129 | - $bytesPerEntry = (int)($this->colorMapBits / 8); | |
| 130 | - $colorMapBytes = $this->colorMapLength * $bytesPerEntry; | |
| 144 | + $bpp = ($h['depth'] + 7) >> 3; | |
| 145 | + $count = $w * $ht; | |
| 146 | + if ($h['type'] >= 9) { | |
| 147 | + $raw = $this->unrle($data, $pos, $count, $bpp); | |
| 148 | + } else { | |
| 149 | + $raw = substr($data, $pos, $count * $bpp); | |
| 150 | + if (strlen($raw) < $count * $bpp) throw new \RuntimeException('TGAの画素データが足りません。'); | |
| 131 | 151 | } |
| 132 | 152 | |
| 133 | - $this->imageId = $imageIdBytes > 0 ? fread($fp, $imageIdBytes) : ''; | |
| 134 | - $this->colorMapData = $colorMapBytes > 0 ? fread($fp, $colorMapBytes) : ''; | |
| 135 | - $tga->imageId = $this->imageId; | |
| 136 | - $tga->colorMapData = $this->colorMapData; | |
| 153 | + // RGBAにする | |
| 154 | + if ($kind === 1) { | |
| 155 | + $idx = $h['depth'] === 16 ? array_values(unpack('v*', $raw)) : array_values(unpack('C*', $raw)); | |
| 156 | + $black = "\0\0\0".($h['alpha'] ? "\0" : "\xff"); | |
| 157 | + $px = implode('', array_map(fn($i) => $lut[$i] ?? $black, $idx)); | |
| 158 | + } else if ($kind === 3) { | |
| 159 | + $px = $h['depth'] === 8 ? preg_replace('/(.)/s', '$1$1$1'."\xff", $raw) : preg_replace('/(.)(.)/s', '$1$1$1$2', $raw); | |
| 160 | + } else if ($h['depth'] === 24) { | |
| 161 | + $px = preg_replace('/(.)(.)(.)/s', '$3$2$1'."\xff", $raw); | |
| 162 | + } else if ($h['depth'] === 32) { | |
| 163 | + $px = $h['alpha'] ? preg_replace('/(.)(.)(.)(.)/s', '$3$2$1$4', $raw) : preg_replace('/(.)(.)(.)./s', '$3$2$1'."\xff", $raw); | |
| 164 | + } else { | |
| 165 | + $alpha = $h['alpha'] && $h['depth'] === 16; | |
| 166 | + $px = implode('', array_map(fn($p) => $this->pixel($p, 16, $alpha), str_split($raw, 2))); | |
| 167 | + } | |
| 137 | 168 | |
| 138 | - if (in_array($this->imageTypeByte, [9, 10, 11])) { // RLE | |
| 139 | - // TODO | |
| 140 | - } else if (in_array($this->imageTypeByte, [1, 2, 3])) { // 非圧縮化 | |
| 141 | - $this->bpp = (int)($this->bitDepth / 8); | |
| 142 | - $bytes = $this->width * $this->height * $this->bpp; | |
| 143 | - $this->imageData = fread($fp, $bytes); | |
| 144 | - $tga->bpp = $this->bpp; | |
| 145 | - $tga->imageData = $this->imageData; | |
| 169 | + if ($h['premultiplied']) $px = $this->unpremultiply($px); | |
| 170 | + | |
| 171 | + // 向き(既定は左下が原点) | |
| 172 | + $rows = str_split($px, $w * 4); | |
| 173 | + if (!$h['top']) $rows = array_reverse($rows); | |
| 174 | + if ($h['right']) $rows = array_map(fn($r) => implode('', array_reverse(str_split($r, 4))), $rows); | |
| 175 | + return new Bitmap($w, $ht, implode('', $rows)); | |
| 176 | + } | |
| 177 | + | |
| 178 | + /** | |
| 179 | + * パレットの項目又は15・16ビットの画素をRGBAにする | |
| 180 | + */ | |
| 181 | + private function pixel(string $p, int $bits, bool $alpha): string { | |
| 182 | + if ($bits === 24) return $p[2].$p[1].$p[0]."\xff"; | |
| 183 | + if ($bits === 32) return $p[2].$p[1].$p[0].($alpha ? $p[3] : "\xff"); | |
| 184 | + // A1R5G5B5(リトルエンディアン) | |
| 185 | + $v = ord($p[0]) | (ord($p[1] ?? "\0") << 8); | |
| 186 | + $c = fn($x) => chr(($x << 3) | ($x >> 2)); | |
| 187 | + $a = $bits === 16 && $alpha ? ($v & 0x8000 ? "\xff" : "\0") : "\xff"; | |
| 188 | + return $c(($v >> 10) & 31).$c(($v >> 5) & 31).$c($v & 31).$a; | |
| 189 | + } | |
| 190 | + | |
| 191 | + /** | |
| 192 | + * RLEを展開する(パケットが行を跨いでも良い) | |
| 193 | + */ | |
| 194 | + private function unrle(string $d, int $pos, int $count, int $bpp): string { | |
| 195 | + $out = ''; | |
| 196 | + $n = 0; | |
| 197 | + $len = strlen($d); | |
| 198 | + while ($n < $count) { | |
| 199 | + if ($pos >= $len) throw new \RuntimeException('TGAのRLEデータが足りません。'); | |
| 200 | + $hdr = ord($d[$pos++]); | |
| 201 | + $k = ($hdr & 0x7f) + 1; | |
| 202 | + if ($hdr & 0x80) { | |
| 203 | + $out .= str_repeat(substr($d, $pos, $bpp), $k); | |
| 204 | + $pos += $bpp; | |
| 205 | + } else { | |
| 206 | + $out .= substr($d, $pos, $k * $bpp); | |
| 207 | + $pos += $k * $bpp; | |
| 208 | + } | |
| 209 | + $n += $k; | |
| 146 | 210 | } |
| 211 | + return substr($out, 0, $count * $bpp); | |
| 212 | + } | |
| 147 | 213 | |
| 148 | - fclose($fp); | |
| 149 | - return $tga; | |
| 214 | + private function unpremultiply(string $px): string { | |
| 215 | + $len = strlen($px); | |
| 216 | + for ($i = 0; $i < $len; $i += 4) { | |
| 217 | + $a = ord($px[$i + 3]); | |
| 218 | + if ($a === 0 || $a === 255) continue; | |
| 219 | + for ($c = 0; $c < 3; $c++) $px[$i + $c] = chr(min(255, intdiv(ord($px[$i + $c]) * 255 + ($a >> 1), $a))); | |
| 220 | + } | |
| 221 | + return $px; | |
| 222 | + } | |
| 223 | + | |
| 224 | + /** | |
| 225 | + * 画素をTGAにする(RLEのトゥルーカラー、不透明なら24ビット、それ以外は32ビット、左上が原点、TGA 2.0のフッター付き) | |
| 226 | + * | |
| 227 | + * @param array $opt rle:false で圧縮しない | |
| 228 | + */ | |
| 229 | + public function encode(Bitmap $img, array $opt = []): string { | |
| 230 | + $alpha = !$img->isOpaque(); | |
| 231 | + $bpp = $alpha ? 4 : 3; | |
| 232 | + $rle = $opt['rle'] ?? true; | |
| 233 | + $raw = $alpha ? preg_replace('/(.)(.)(.)(.)/s', '$3$2$1$4', $img->data) : preg_replace('/(.)(.)(.)./s', '$3$2$1', $img->data); | |
| 234 | + $header = pack('CCCvvCvvvvCC', 0, 0, $rle ? 10 : 2, 0, 0, 0, 0, 0, $img->width, $img->height, $bpp * 8, 0x20 | ($alpha ? 8 : 0)); | |
| 235 | + $body = $rle ? $this->rle($raw, $img->width, $bpp) : $raw; | |
| 236 | + return $header.$body.pack('VV', 0, 0).self::FOOTER; | |
| 237 | + } | |
| 238 | + | |
| 239 | + /** | |
| 240 | + * RLEにする(TGA 2.0の推奨通り、パケットは行を跨がない) | |
| 241 | + */ | |
| 242 | + private function rle(string $raw, int $w, int $bpp): string { | |
| 243 | + $out = ''; | |
| 244 | + foreach (str_split($raw, $w * $bpp) as $row) { | |
| 245 | + $px = str_split($row, $bpp); | |
| 246 | + $n = count($px); | |
| 247 | + $i = 0; | |
| 248 | + while ($i < $n) { | |
| 249 | + // 同じ画素の繰り返し | |
| 250 | + $j = $i + 1; | |
| 251 | + while ($j < $n && $j - $i < 128 && $px[$j] === $px[$i]) $j++; | |
| 252 | + if ($j - $i >= 2) { | |
| 253 | + $out .= chr(0x80 | ($j - $i - 1)).$px[$i]; | |
| 254 | + $i = $j; | |
| 255 | + continue; | |
| 256 | + } | |
| 257 | + // 繰り返さない画素(次の繰り返しの前まで) | |
| 258 | + $j = $i + 1; | |
| 259 | + while ($j < $n && $j - $i < 128 && !($j + 1 < $n && $px[$j] === $px[$j + 1])) $j++; | |
| 260 | + $out .= chr($j - $i - 1).implode('', array_slice($px, $i, $j - $i)); | |
| 261 | + $i = $j; | |
| 262 | + } | |
| 263 | + } | |
| 264 | + return $out; | |
| 150 | 265 | } |
| 151 | 266 | } |
| @@ -0,0 +1,1989 @@ | ||
| 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\Image; | |
| 39 | + | |
| 40 | +/** | |
| 41 | + * WebPの非可逆圧縮(VP8のキーフレーム、RFC 6386) | |
| 42 | + * | |
| 43 | + * 読み込み:セグメント、全ての予測モード、複数のパーティション、ループフィルター(通常・簡易)。 | |
| 44 | + * 色はlibwebpと同じく、色差を滑らかに拡大(fancy upsampling)してRGBにする。 | |
| 45 | + * 書き出し:16x16と4x4の予測を選び、係数の確率を画像に合わせて更新し、通常のループフィルターを掛ける。 | |
| 46 | + */ | |
| 47 | +final class Vp8 { | |
| 48 | + private const DC_TABLE = [ | |
| 49 | + 4, 5, 6, 7, 8, 9, 10, 10, 11, 12, 13, 14, 15, 16, 17, 17, | |
| 50 | + 18, 19, 20, 20, 21, 21, 22, 22, 23, 23, 24, 25, 25, 26, 27, 28, | |
| 51 | + 29, 30, 31, 32, 33, 34, 35, 36, 37, 37, 38, 39, 40, 41, 42, 43, | |
| 52 | + 44, 45, 46, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, | |
| 53 | + 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, | |
| 54 | + 75, 76, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, | |
| 55 | + 91, 93, 95, 96, 98, 100, 101, 102, 104, 106, 108, 110, 112, 114, 116, 118, | |
| 56 | + 122, 124, 126, 128, 130, 132, 134, 136, 138, 140, 143, 145, 148, 151, 154, 157, | |
| 57 | + ]; | |
| 58 | + private const AC_TABLE = [ | |
| 59 | + 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, | |
| 60 | + 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, | |
| 61 | + 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, | |
| 62 | + 52, 53, 54, 55, 56, 57, 58, 60, 62, 64, 66, 68, 70, 72, 74, 76, | |
| 63 | + 78, 80, 82, 84, 86, 88, 90, 92, 94, 96, 98, 100, 102, 104, 106, 108, | |
| 64 | + 110, 112, 114, 116, 119, 122, 125, 128, 131, 134, 137, 140, 143, 146, 149, 152, | |
| 65 | + 155, 158, 161, 164, 167, 170, 173, 177, 181, 185, 189, 193, 197, 201, 205, 209, | |
| 66 | + 213, 217, 221, 225, 229, 234, 239, 245, 249, 254, 259, 264, 269, 274, 279, 284, | |
| 67 | + ]; | |
| 68 | + /** 係数の確率の既定値 [種類][帯][文脈][11] */ | |
| 69 | + private const COEFF_PROBS = [ | |
| 70 | + [ | |
| 71 | + [ | |
| 72 | + [128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128], | |
| 73 | + [128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128], | |
| 74 | + [128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128], | |
| 75 | + ], | |
| 76 | + [ | |
| 77 | + [253, 136, 254, 255, 228, 219, 128, 128, 128, 128, 128], | |
| 78 | + [189, 129, 242, 255, 227, 213, 255, 219, 128, 128, 128], | |
| 79 | + [106, 126, 227, 252, 214, 209, 255, 255, 128, 128, 128], | |
| 80 | + ], | |
| 81 | + [ | |
| 82 | + [1, 98, 248, 255, 236, 226, 255, 255, 128, 128, 128], | |
| 83 | + [181, 133, 238, 254, 221, 234, 255, 154, 128, 128, 128], | |
| 84 | + [78, 134, 202, 247, 198, 180, 255, 219, 128, 128, 128], | |
| 85 | + ], | |
| 86 | + [ | |
| 87 | + [1, 185, 249, 255, 243, 255, 128, 128, 128, 128, 128], | |
| 88 | + [184, 150, 247, 255, 236, 224, 128, 128, 128, 128, 128], | |
| 89 | + [77, 110, 216, 255, 236, 230, 128, 128, 128, 128, 128], | |
| 90 | + ], | |
| 91 | + [ | |
| 92 | + [1, 101, 251, 255, 241, 255, 128, 128, 128, 128, 128], | |
| 93 | + [170, 139, 241, 252, 236, 209, 255, 255, 128, 128, 128], | |
| 94 | + [37, 116, 196, 243, 228, 255, 255, 255, 128, 128, 128], | |
| 95 | + ], | |
| 96 | + [ | |
| 97 | + [1, 204, 254, 255, 245, 255, 128, 128, 128, 128, 128], | |
| 98 | + [207, 160, 250, 255, 238, 128, 128, 128, 128, 128, 128], | |
| 99 | + [102, 103, 231, 255, 211, 171, 128, 128, 128, 128, 128], | |
| 100 | + ], | |
| 101 | + [ | |
| 102 | + [1, 152, 252, 255, 240, 255, 128, 128, 128, 128, 128], | |
| 103 | + [177, 135, 243, 255, 234, 225, 128, 128, 128, 128, 128], | |
| 104 | + [80, 129, 211, 255, 194, 224, 128, 128, 128, 128, 128], | |
| 105 | + ], | |
| 106 | + [ | |
| 107 | + [1, 1, 255, 128, 128, 128, 128, 128, 128, 128, 128], | |
| 108 | + [246, 1, 255, 128, 128, 128, 128, 128, 128, 128, 128], | |
| 109 | + [255, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128], | |
| 110 | + ], | |
| 111 | + ], | |
| 112 | + [ | |
| 113 | + [ | |
| 114 | + [198, 35, 237, 223, 193, 187, 162, 160, 145, 155, 62], | |
| 115 | + [131, 45, 198, 221, 172, 176, 220, 157, 252, 221, 1], | |
| 116 | + [68, 47, 146, 208, 149, 167, 221, 162, 255, 223, 128], | |
| 117 | + ], | |
| 118 | + [ | |
| 119 | + [1, 149, 241, 255, 221, 224, 255, 255, 128, 128, 128], | |
| 120 | + [184, 141, 234, 253, 222, 220, 255, 199, 128, 128, 128], | |
| 121 | + [81, 99, 181, 242, 176, 190, 249, 202, 255, 255, 128], | |
| 122 | + ], | |
| 123 | + [ | |
| 124 | + [1, 129, 232, 253, 214, 197, 242, 196, 255, 255, 128], | |
| 125 | + [99, 121, 210, 250, 201, 198, 255, 202, 128, 128, 128], | |
| 126 | + [23, 91, 163, 242, 170, 187, 247, 210, 255, 255, 128], | |
| 127 | + ], | |
| 128 | + [ | |
| 129 | + [1, 200, 246, 255, 234, 255, 128, 128, 128, 128, 128], | |
| 130 | + [109, 178, 241, 255, 231, 245, 255, 255, 128, 128, 128], | |
| 131 | + [44, 130, 201, 253, 205, 192, 255, 255, 128, 128, 128], | |
| 132 | + ], | |
| 133 | + [ | |
| 134 | + [1, 132, 239, 251, 219, 209, 255, 165, 128, 128, 128], | |
| 135 | + [94, 136, 225, 251, 218, 190, 255, 255, 128, 128, 128], | |
| 136 | + [22, 100, 174, 245, 186, 161, 255, 199, 128, 128, 128], | |
| 137 | + ], | |
| 138 | + [ | |
| 139 | + [1, 182, 249, 255, 232, 235, 128, 128, 128, 128, 128], | |
| 140 | + [124, 143, 241, 255, 227, 234, 128, 128, 128, 128, 128], | |
| 141 | + [35, 77, 181, 251, 193, 211, 255, 205, 128, 128, 128], | |
| 142 | + ], | |
| 143 | + [ | |
| 144 | + [1, 157, 247, 255, 236, 231, 255, 255, 128, 128, 128], | |
| 145 | + [121, 141, 235, 255, 225, 227, 255, 255, 128, 128, 128], | |
| 146 | + [45, 99, 188, 251, 195, 217, 255, 224, 128, 128, 128], | |
| 147 | + ], | |
| 148 | + [ | |
| 149 | + [1, 1, 251, 255, 213, 255, 128, 128, 128, 128, 128], | |
| 150 | + [203, 1, 248, 255, 255, 128, 128, 128, 128, 128, 128], | |
| 151 | + [137, 1, 177, 255, 224, 255, 128, 128, 128, 128, 128], | |
| 152 | + ], | |
| 153 | + ], | |
| 154 | + [ | |
| 155 | + [ | |
| 156 | + [253, 9, 248, 251, 207, 208, 255, 192, 128, 128, 128], | |
| 157 | + [175, 13, 224, 243, 193, 185, 249, 198, 255, 255, 128], | |
| 158 | + [73, 17, 171, 221, 161, 179, 236, 167, 255, 234, 128], | |
| 159 | + ], | |
| 160 | + [ | |
| 161 | + [1, 95, 247, 253, 212, 183, 255, 255, 128, 128, 128], | |
| 162 | + [239, 90, 244, 250, 211, 209, 255, 255, 128, 128, 128], | |
| 163 | + [155, 77, 195, 248, 188, 195, 255, 255, 128, 128, 128], | |
| 164 | + ], | |
| 165 | + [ | |
| 166 | + [1, 24, 239, 251, 218, 219, 255, 205, 128, 128, 128], | |
| 167 | + [201, 51, 219, 255, 196, 186, 128, 128, 128, 128, 128], | |
| 168 | + [69, 46, 190, 239, 201, 218, 255, 228, 128, 128, 128], | |
| 169 | + ], | |
| 170 | + [ | |
| 171 | + [1, 191, 251, 255, 255, 128, 128, 128, 128, 128, 128], | |
| 172 | + [223, 165, 249, 255, 213, 255, 128, 128, 128, 128, 128], | |
| 173 | + [141, 124, 248, 255, 255, 128, 128, 128, 128, 128, 128], | |
| 174 | + ], | |
| 175 | + [ | |
| 176 | + [1, 16, 248, 255, 255, 128, 128, 128, 128, 128, 128], | |
| 177 | + [190, 36, 230, 255, 236, 255, 128, 128, 128, 128, 128], | |
| 178 | + [149, 1, 255, 128, 128, 128, 128, 128, 128, 128, 128], | |
| 179 | + ], | |
| 180 | + [ | |
| 181 | + [1, 226, 255, 128, 128, 128, 128, 128, 128, 128, 128], | |
| 182 | + [247, 192, 255, 128, 128, 128, 128, 128, 128, 128, 128], | |
| 183 | + [240, 128, 255, 128, 128, 128, 128, 128, 128, 128, 128], | |
| 184 | + ], | |
| 185 | + [ | |
| 186 | + [1, 134, 252, 255, 255, 128, 128, 128, 128, 128, 128], | |
| 187 | + [213, 62, 250, 255, 255, 128, 128, 128, 128, 128, 128], | |
| 188 | + [55, 93, 255, 128, 128, 128, 128, 128, 128, 128, 128], | |
| 189 | + ], | |
| 190 | + [ | |
| 191 | + [128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128], | |
| 192 | + [128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128], | |
| 193 | + [128, 128, 128, 128, 128, 128, 128, 128, 128, 128, 128], | |
| 194 | + ], | |
| 195 | + ], | |
| 196 | + [ | |
| 197 | + [ | |
| 198 | + [202, 24, 213, 235, 186, 191, 220, 160, 240, 175, 255], | |
| 199 | + [126, 38, 182, 232, 169, 184, 228, 174, 255, 187, 128], | |
| 200 | + [61, 46, 138, 219, 151, 178, 240, 170, 255, 216, 128], | |
| 201 | + ], | |
| 202 | + [ | |
| 203 | + [1, 112, 230, 250, 199, 191, 247, 159, 255, 255, 128], | |
| 204 | + [166, 109, 228, 252, 211, 215, 255, 174, 128, 128, 128], | |
| 205 | + [39, 77, 162, 232, 172, 180, 245, 178, 255, 255, 128], | |
| 206 | + ], | |
| 207 | + [ | |
| 208 | + [1, 52, 220, 246, 198, 199, 249, 220, 255, 255, 128], | |
| 209 | + [124, 74, 191, 243, 183, 193, 250, 221, 255, 255, 128], | |
| 210 | + [24, 71, 130, 219, 154, 170, 243, 182, 255, 255, 128], | |
| 211 | + ], | |
| 212 | + [ | |
| 213 | + [1, 182, 225, 249, 219, 240, 255, 224, 128, 128, 128], | |
| 214 | + [149, 150, 226, 252, 216, 205, 255, 171, 128, 128, 128], | |
| 215 | + [28, 108, 170, 242, 183, 194, 254, 223, 255, 255, 128], | |
| 216 | + ], | |
| 217 | + [ | |
| 218 | + [1, 81, 230, 252, 204, 203, 255, 192, 128, 128, 128], | |
| 219 | + [123, 102, 209, 247, 188, 196, 255, 233, 128, 128, 128], | |
| 220 | + [20, 95, 153, 243, 164, 173, 255, 203, 128, 128, 128], | |
| 221 | + ], | |
| 222 | + [ | |
| 223 | + [1, 222, 248, 255, 216, 213, 128, 128, 128, 128, 128], | |
| 224 | + [168, 175, 246, 252, 235, 205, 255, 255, 128, 128, 128], | |
| 225 | + [47, 116, 215, 255, 211, 212, 255, 255, 128, 128, 128], | |
| 226 | + ], | |
| 227 | + [ | |
| 228 | + [1, 121, 236, 253, 212, 214, 255, 255, 128, 128, 128], | |
| 229 | + [141, 84, 213, 252, 201, 202, 255, 219, 128, 128, 128], | |
| 230 | + [42, 80, 160, 240, 162, 185, 255, 205, 128, 128, 128], | |
| 231 | + ], | |
| 232 | + [ | |
| 233 | + [1, 1, 255, 128, 128, 128, 128, 128, 128, 128, 128], | |
| 234 | + [244, 1, 255, 128, 128, 128, 128, 128, 128, 128, 128], | |
| 235 | + [238, 1, 255, 128, 128, 128, 128, 128, 128, 128, 128], | |
| 236 | + ], | |
| 237 | + ], | |
| 238 | + ]; | |
| 239 | + /** 係数の確率を更新する確率 */ | |
| 240 | + private const COEFF_UPDATE_PROBS = [ | |
| 241 | + [ | |
| 242 | + [ | |
| 243 | + [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], | |
| 244 | + [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], | |
| 245 | + [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], | |
| 246 | + ], | |
| 247 | + [ | |
| 248 | + [176, 246, 255, 255, 255, 255, 255, 255, 255, 255, 255], | |
| 249 | + [223, 241, 252, 255, 255, 255, 255, 255, 255, 255, 255], | |
| 250 | + [249, 253, 253, 255, 255, 255, 255, 255, 255, 255, 255], | |
| 251 | + ], | |
| 252 | + [ | |
| 253 | + [255, 244, 252, 255, 255, 255, 255, 255, 255, 255, 255], | |
| 254 | + [234, 254, 254, 255, 255, 255, 255, 255, 255, 255, 255], | |
| 255 | + [253, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], | |
| 256 | + ], | |
| 257 | + [ | |
| 258 | + [255, 246, 254, 255, 255, 255, 255, 255, 255, 255, 255], | |
| 259 | + [239, 253, 254, 255, 255, 255, 255, 255, 255, 255, 255], | |
| 260 | + [254, 255, 254, 255, 255, 255, 255, 255, 255, 255, 255], | |
| 261 | + ], | |
| 262 | + [ | |
| 263 | + [255, 248, 254, 255, 255, 255, 255, 255, 255, 255, 255], | |
| 264 | + [251, 255, 254, 255, 255, 255, 255, 255, 255, 255, 255], | |
| 265 | + [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], | |
| 266 | + ], | |
| 267 | + [ | |
| 268 | + [255, 253, 254, 255, 255, 255, 255, 255, 255, 255, 255], | |
| 269 | + [251, 254, 254, 255, 255, 255, 255, 255, 255, 255, 255], | |
| 270 | + [254, 255, 254, 255, 255, 255, 255, 255, 255, 255, 255], | |
| 271 | + ], | |
| 272 | + [ | |
| 273 | + [255, 254, 253, 255, 254, 255, 255, 255, 255, 255, 255], | |
| 274 | + [250, 255, 254, 255, 254, 255, 255, 255, 255, 255, 255], | |
| 275 | + [254, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], | |
| 276 | + ], | |
| 277 | + [ | |
| 278 | + [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], | |
| 279 | + [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], | |
| 280 | + [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], | |
| 281 | + ], | |
| 282 | + ], | |
| 283 | + [ | |
| 284 | + [ | |
| 285 | + [217, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], | |
| 286 | + [225, 252, 241, 253, 255, 255, 254, 255, 255, 255, 255], | |
| 287 | + [234, 250, 241, 250, 253, 255, 253, 254, 255, 255, 255], | |
| 288 | + ], | |
| 289 | + [ | |
| 290 | + [255, 254, 255, 255, 255, 255, 255, 255, 255, 255, 255], | |
| 291 | + [223, 254, 254, 255, 255, 255, 255, 255, 255, 255, 255], | |
| 292 | + [238, 253, 254, 254, 255, 255, 255, 255, 255, 255, 255], | |
| 293 | + ], | |
| 294 | + [ | |
| 295 | + [255, 248, 254, 255, 255, 255, 255, 255, 255, 255, 255], | |
| 296 | + [249, 254, 255, 255, 255, 255, 255, 255, 255, 255, 255], | |
| 297 | + [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], | |
| 298 | + ], | |
| 299 | + [ | |
| 300 | + [255, 253, 255, 255, 255, 255, 255, 255, 255, 255, 255], | |
| 301 | + [247, 254, 255, 255, 255, 255, 255, 255, 255, 255, 255], | |
| 302 | + [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], | |
| 303 | + ], | |
| 304 | + [ | |
| 305 | + [255, 253, 254, 255, 255, 255, 255, 255, 255, 255, 255], | |
| 306 | + [252, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], | |
| 307 | + [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], | |
| 308 | + ], | |
| 309 | + [ | |
| 310 | + [255, 254, 254, 255, 255, 255, 255, 255, 255, 255, 255], | |
| 311 | + [253, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], | |
| 312 | + [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], | |
| 313 | + ], | |
| 314 | + [ | |
| 315 | + [255, 254, 253, 255, 255, 255, 255, 255, 255, 255, 255], | |
| 316 | + [250, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], | |
| 317 | + [254, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], | |
| 318 | + ], | |
| 319 | + [ | |
| 320 | + [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], | |
| 321 | + [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], | |
| 322 | + [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], | |
| 323 | + ], | |
| 324 | + ], | |
| 325 | + [ | |
| 326 | + [ | |
| 327 | + [186, 251, 250, 255, 255, 255, 255, 255, 255, 255, 255], | |
| 328 | + [234, 251, 244, 254, 255, 255, 255, 255, 255, 255, 255], | |
| 329 | + [251, 251, 243, 253, 254, 255, 254, 255, 255, 255, 255], | |
| 330 | + ], | |
| 331 | + [ | |
| 332 | + [255, 253, 254, 255, 255, 255, 255, 255, 255, 255, 255], | |
| 333 | + [236, 253, 254, 255, 255, 255, 255, 255, 255, 255, 255], | |
| 334 | + [251, 253, 253, 254, 254, 255, 255, 255, 255, 255, 255], | |
| 335 | + ], | |
| 336 | + [ | |
| 337 | + [255, 254, 254, 255, 255, 255, 255, 255, 255, 255, 255], | |
| 338 | + [254, 254, 254, 255, 255, 255, 255, 255, 255, 255, 255], | |
| 339 | + [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], | |
| 340 | + ], | |
| 341 | + [ | |
| 342 | + [255, 254, 255, 255, 255, 255, 255, 255, 255, 255, 255], | |
| 343 | + [254, 254, 255, 255, 255, 255, 255, 255, 255, 255, 255], | |
| 344 | + [254, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], | |
| 345 | + ], | |
| 346 | + [ | |
| 347 | + [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], | |
| 348 | + [254, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], | |
| 349 | + [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], | |
| 350 | + ], | |
| 351 | + [ | |
| 352 | + [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], | |
| 353 | + [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], | |
| 354 | + [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], | |
| 355 | + ], | |
| 356 | + [ | |
| 357 | + [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], | |
| 358 | + [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], | |
| 359 | + [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], | |
| 360 | + ], | |
| 361 | + [ | |
| 362 | + [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], | |
| 363 | + [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], | |
| 364 | + [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], | |
| 365 | + ], | |
| 366 | + ], | |
| 367 | + [ | |
| 368 | + [ | |
| 369 | + [248, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], | |
| 370 | + [250, 254, 252, 254, 255, 255, 255, 255, 255, 255, 255], | |
| 371 | + [248, 254, 249, 253, 255, 255, 255, 255, 255, 255, 255], | |
| 372 | + ], | |
| 373 | + [ | |
| 374 | + [255, 253, 253, 255, 255, 255, 255, 255, 255, 255, 255], | |
| 375 | + [246, 253, 253, 255, 255, 255, 255, 255, 255, 255, 255], | |
| 376 | + [252, 254, 251, 254, 254, 255, 255, 255, 255, 255, 255], | |
| 377 | + ], | |
| 378 | + [ | |
| 379 | + [255, 254, 252, 255, 255, 255, 255, 255, 255, 255, 255], | |
| 380 | + [248, 254, 253, 255, 255, 255, 255, 255, 255, 255, 255], | |
| 381 | + [253, 255, 254, 254, 255, 255, 255, 255, 255, 255, 255], | |
| 382 | + ], | |
| 383 | + [ | |
| 384 | + [255, 251, 254, 255, 255, 255, 255, 255, 255, 255, 255], | |
| 385 | + [245, 251, 254, 255, 255, 255, 255, 255, 255, 255, 255], | |
| 386 | + [253, 253, 254, 255, 255, 255, 255, 255, 255, 255, 255], | |
| 387 | + ], | |
| 388 | + [ | |
| 389 | + [255, 251, 253, 255, 255, 255, 255, 255, 255, 255, 255], | |
| 390 | + [252, 253, 254, 255, 255, 255, 255, 255, 255, 255, 255], | |
| 391 | + [255, 254, 255, 255, 255, 255, 255, 255, 255, 255, 255], | |
| 392 | + ], | |
| 393 | + [ | |
| 394 | + [255, 252, 255, 255, 255, 255, 255, 255, 255, 255, 255], | |
| 395 | + [249, 255, 254, 255, 255, 255, 255, 255, 255, 255, 255], | |
| 396 | + [255, 255, 254, 255, 255, 255, 255, 255, 255, 255, 255], | |
| 397 | + ], | |
| 398 | + [ | |
| 399 | + [255, 255, 253, 255, 255, 255, 255, 255, 255, 255, 255], | |
| 400 | + [250, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], | |
| 401 | + [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], | |
| 402 | + ], | |
| 403 | + [ | |
| 404 | + [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], | |
| 405 | + [254, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], | |
| 406 | + [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255], | |
| 407 | + ], | |
| 408 | + ], | |
| 409 | + ]; | |
| 410 | + /** 4x4の予測モードの確率 [上][左][9] */ | |
| 411 | + private const BMODE_PROBS = [ | |
| 412 | + [ | |
| 413 | + [231, 120, 48, 89, 115, 113, 120, 152, 112], | |
| 414 | + [152, 179, 64, 126, 170, 118, 46, 70, 95], | |
| 415 | + [175, 69, 143, 80, 85, 82, 72, 155, 103], | |
| 416 | + [56, 58, 10, 171, 218, 189, 17, 13, 152], | |
| 417 | + [114, 26, 17, 163, 44, 195, 21, 10, 173], | |
| 418 | + [121, 24, 80, 195, 26, 62, 44, 64, 85], | |
| 419 | + [144, 71, 10, 38, 171, 213, 144, 34, 26], | |
| 420 | + [170, 46, 55, 19, 136, 160, 33, 206, 71], | |
| 421 | + [63, 20, 8, 114, 114, 208, 12, 9, 226], | |
| 422 | + [81, 40, 11, 96, 182, 84, 29, 16, 36], | |
| 423 | + ], | |
| 424 | + [ | |
| 425 | + [134, 183, 89, 137, 98, 101, 106, 165, 148], | |
| 426 | + [72, 187, 100, 130, 157, 111, 32, 75, 80], | |
| 427 | + [66, 102, 167, 99, 74, 62, 40, 234, 128], | |
| 428 | + [41, 53, 9, 178, 241, 141, 26, 8, 107], | |
| 429 | + [74, 43, 26, 146, 73, 166, 49, 23, 157], | |
| 430 | + [65, 38, 105, 160, 51, 52, 31, 115, 128], | |
| 431 | + [104, 79, 12, 27, 217, 255, 87, 17, 7], | |
| 432 | + [87, 68, 71, 44, 114, 51, 15, 186, 23], | |
| 433 | + [47, 41, 14, 110, 182, 183, 21, 17, 194], | |
| 434 | + [66, 45, 25, 102, 197, 189, 23, 18, 22], | |
| 435 | + ], | |
| 436 | + [ | |
| 437 | + [88, 88, 147, 150, 42, 46, 45, 196, 205], | |
| 438 | + [43, 97, 183, 117, 85, 38, 35, 179, 61], | |
| 439 | + [39, 53, 200, 87, 26, 21, 43, 232, 171], | |
| 440 | + [56, 34, 51, 104, 114, 102, 29, 93, 77], | |
| 441 | + [39, 28, 85, 171, 58, 165, 90, 98, 64], | |
| 442 | + [34, 22, 116, 206, 23, 34, 43, 166, 73], | |
| 443 | + [107, 54, 32, 26, 51, 1, 81, 43, 31], | |
| 444 | + [68, 25, 106, 22, 64, 171, 36, 225, 114], | |
| 445 | + [34, 19, 21, 102, 132, 188, 16, 76, 124], | |
| 446 | + [62, 18, 78, 95, 85, 57, 50, 48, 51], | |
| 447 | + ], | |
| 448 | + [ | |
| 449 | + [193, 101, 35, 159, 215, 111, 89, 46, 111], | |
| 450 | + [60, 148, 31, 172, 219, 228, 21, 18, 111], | |
| 451 | + [112, 113, 77, 85, 179, 255, 38, 120, 114], | |
| 452 | + [40, 42, 1, 196, 245, 209, 10, 25, 109], | |
| 453 | + [88, 43, 29, 140, 166, 213, 37, 43, 154], | |
| 454 | + [61, 63, 30, 155, 67, 45, 68, 1, 209], | |
| 455 | + [100, 80, 8, 43, 154, 1, 51, 26, 71], | |
| 456 | + [142, 78, 78, 16, 255, 128, 34, 197, 171], | |
| 457 | + [41, 40, 5, 102, 211, 183, 4, 1, 221], | |
| 458 | + [51, 50, 17, 168, 209, 192, 23, 25, 82], | |
| 459 | + ], | |
| 460 | + [ | |
| 461 | + [138, 31, 36, 171, 27, 166, 38, 44, 229], | |
| 462 | + [67, 87, 58, 169, 82, 115, 26, 59, 179], | |
| 463 | + [63, 59, 90, 180, 59, 166, 93, 73, 154], | |
| 464 | + [40, 40, 21, 116, 143, 209, 34, 39, 175], | |
| 465 | + [47, 15, 16, 183, 34, 223, 49, 45, 183], | |
| 466 | + [46, 17, 33, 183, 6, 98, 15, 32, 183], | |
| 467 | + [57, 46, 22, 24, 128, 1, 54, 17, 37], | |
| 468 | + [65, 32, 73, 115, 28, 128, 23, 128, 205], | |
| 469 | + [40, 3, 9, 115, 51, 192, 18, 6, 223], | |
| 470 | + [87, 37, 9, 115, 59, 77, 64, 21, 47], | |
| 471 | + ], | |
| 472 | + [ | |
| 473 | + [104, 55, 44, 218, 9, 54, 53, 130, 226], | |
| 474 | + [64, 90, 70, 205, 40, 41, 23, 26, 57], | |
| 475 | + [54, 57, 112, 184, 5, 41, 38, 166, 213], | |
| 476 | + [30, 34, 26, 133, 152, 116, 10, 32, 134], | |
| 477 | + [39, 19, 53, 221, 26, 114, 32, 73, 255], | |
| 478 | + [31, 9, 65, 234, 2, 15, 1, 118, 73], | |
| 479 | + [75, 32, 12, 51, 192, 255, 160, 43, 51], | |
| 480 | + [88, 31, 35, 67, 102, 85, 55, 186, 85], | |
| 481 | + [56, 21, 23, 111, 59, 205, 45, 37, 192], | |
| 482 | + [55, 38, 70, 124, 73, 102, 1, 34, 98], | |
| 483 | + ], | |
| 484 | + [ | |
| 485 | + [125, 98, 42, 88, 104, 85, 117, 175, 82], | |
| 486 | + [95, 84, 53, 89, 128, 100, 113, 101, 45], | |
| 487 | + [75, 79, 123, 47, 51, 128, 81, 171, 1], | |
| 488 | + [57, 17, 5, 71, 102, 57, 53, 41, 49], | |
| 489 | + [38, 33, 13, 121, 57, 73, 26, 1, 85], | |
| 490 | + [41, 10, 67, 138, 77, 110, 90, 47, 114], | |
| 491 | + [115, 21, 2, 10, 102, 255, 166, 23, 6], | |
| 492 | + [101, 29, 16, 10, 85, 128, 101, 196, 26], | |
| 493 | + [57, 18, 10, 102, 102, 213, 34, 20, 43], | |
| 494 | + [117, 20, 15, 36, 163, 128, 68, 1, 26], | |
| 495 | + ], | |
| 496 | + [ | |
| 497 | + [102, 61, 71, 37, 34, 53, 31, 243, 192], | |
| 498 | + [69, 60, 71, 38, 73, 119, 28, 222, 37], | |
| 499 | + [68, 45, 128, 34, 1, 47, 11, 245, 171], | |
| 500 | + [62, 17, 19, 70, 146, 85, 55, 62, 70], | |
| 501 | + [37, 43, 37, 154, 100, 163, 85, 160, 1], | |
| 502 | + [63, 9, 92, 136, 28, 64, 32, 201, 85], | |
| 503 | + [75, 15, 9, 9, 64, 255, 184, 119, 16], | |
| 504 | + [86, 6, 28, 5, 64, 255, 25, 248, 1], | |
| 505 | + [56, 8, 17, 132, 137, 255, 55, 116, 128], | |
| 506 | + [58, 15, 20, 82, 135, 57, 26, 121, 40], | |
| 507 | + ], | |
| 508 | + [ | |
| 509 | + [164, 50, 31, 137, 154, 133, 25, 35, 218], | |
| 510 | + [51, 103, 44, 131, 131, 123, 31, 6, 158], | |
| 511 | + [86, 40, 64, 135, 148, 224, 45, 183, 128], | |
| 512 | + [22, 26, 17, 131, 240, 154, 14, 1, 209], | |
| 513 | + [45, 16, 21, 91, 64, 222, 7, 1, 197], | |
| 514 | + [56, 21, 39, 155, 60, 138, 23, 102, 213], | |
| 515 | + [83, 12, 13, 54, 192, 255, 68, 47, 28], | |
| 516 | + [85, 26, 85, 85, 128, 128, 32, 146, 171], | |
| 517 | + [18, 11, 7, 63, 144, 171, 4, 4, 246], | |
| 518 | + [35, 27, 10, 146, 174, 171, 12, 26, 128], | |
| 519 | + ], | |
| 520 | + [ | |
| 521 | + [190, 80, 35, 99, 180, 80, 126, 54, 45], | |
| 522 | + [85, 126, 47, 87, 176, 51, 41, 20, 32], | |
| 523 | + [101, 75, 128, 139, 118, 146, 116, 128, 85], | |
| 524 | + [56, 41, 15, 176, 236, 85, 37, 9, 62], | |
| 525 | + [71, 30, 17, 119, 118, 255, 17, 18, 138], | |
| 526 | + [101, 38, 60, 138, 55, 70, 43, 26, 142], | |
| 527 | + [146, 36, 19, 30, 171, 255, 97, 27, 20], | |
| 528 | + [138, 45, 61, 62, 219, 1, 81, 188, 64], | |
| 529 | + [32, 41, 20, 117, 151, 142, 20, 21, 163], | |
| 530 | + [112, 19, 12, 61, 195, 128, 48, 4, 24], | |
| 531 | + ], | |
| 532 | + ]; | |
| 533 | + private const ZIGZAG = [0, 1, 4, 8, 5, 2, 3, 6, 9, 12, 13, 10, 7, 11, 14, 15]; | |
| 534 | + private const BANDS = [0, 1, 2, 3, 6, 4, 5, 6, 6, 6, 6, 6, 6, 6, 6, 7, 0]; | |
| 535 | + /** 大きい係数の追加ビットの確率(DCT_CAT3~6) */ | |
| 536 | + private const CAT = [[173, 148, 140], [176, 155, 140, 135], [180, 157, 141, 134, 130], [254, 254, 243, 230, 196, 177, 153, 140, 133, 130, 129]]; | |
| 537 | + /** 4x4の予測モードの木(0:DC、1:TM、2:VE、3:HE、4:RD、5:VR、6:LD、7:VL、8:HD、9:HU) */ | |
| 538 | + private const BMODE_TREE = [0, 1, -1, 2, -2, 3, 4, 6, -3, 5, -4, -5, -6, 7, -7, 8, -8, -9]; | |
| 539 | + private const DC = 0; | |
| 540 | + private const TM = 1; | |
| 541 | + private const VE = 2; | |
| 542 | + private const HE = 3; | |
| 543 | + /** 作業用の配列の1行(左に1、右に4画素の余白) */ | |
| 544 | + private const BPS = 32; | |
| 545 | + | |
| 546 | + // 算術復号 | |
| 547 | + private string $d = ''; | |
| 548 | + private int $p = 0; | |
| 549 | + private int $value = 0; | |
| 550 | + private int $count = -8; | |
| 551 | + private int $range = 255; | |
| 552 | + /** 正規化のシフト量 */ | |
| 553 | + private array $norm = []; | |
| 554 | + | |
| 555 | + private array $probs = []; | |
| 556 | + | |
| 557 | + public function __construct() { | |
| 558 | + for ($r = 1; $r < 128; $r++) { | |
| 559 | + $s = 0; | |
| 560 | + while (($r << $s) < 128) $s++; | |
| 561 | + $this->norm[$r] = $s; | |
| 562 | + } | |
| 563 | + } | |
| 564 | + | |
| 565 | + //////// 読み込み | |
| 566 | + | |
| 567 | + /** | |
| 568 | + * VP8チャンクの中身を読む | |
| 569 | + * | |
| 570 | + * @return array [幅, 丈, RGBA(アルファは255)] | |
| 571 | + */ | |
| 572 | + public function decode(string $data): array { | |
| 573 | + $len = strlen($data); | |
| 574 | + if ($len < 10) throw new \RuntimeException('VP8のデータが短過ぎます。'); | |
| 575 | + $tag = ord($data[0]) | (ord($data[1]) << 8) | (ord($data[2]) << 16); | |
| 576 | + if ($tag & 1) throw new \RuntimeException('VP8のキーフレームではありません。'); | |
| 577 | + if ((($tag >> 1) & 7) > 3) throw new \RuntimeException('未対応のVP8の版です。'); | |
| 578 | + $part = $tag >> 5; | |
| 579 | + if (substr($data, 3, 3) !== "\x9d\x01\x2a") throw new \RuntimeException('VP8の開始コードがありません。'); | |
| 580 | + $w = unpack('v', substr($data, 6, 2))[1] & 0x3fff; | |
| 581 | + $h = unpack('v', substr($data, 8, 2))[1] & 0x3fff; | |
| 582 | + Bitmap::check($w, $h); | |
| 583 | + if (10 + $part > $len) throw new \RuntimeException('VP8のデータが途中で切れています。'); | |
| 584 | + $this->init(substr($data, 10, $part), 0); | |
| 585 | + | |
| 586 | + // フレームのヘッダー | |
| 587 | + $this->value(1); // 色空間 | |
| 588 | + $this->value(1); // クランプ | |
| 589 | + $seg = ['use' => $this->value(1), 'map' => 0, 'abs' => 0, 'q' => [0, 0, 0, 0], 'lf' => [0, 0, 0, 0], 'probs' => [255, 255, 255]]; | |
| 590 | + if ($seg['use']) { | |
| 591 | + $seg['map'] = $this->value(1); | |
| 592 | + if ($this->value(1)) { | |
| 593 | + $seg['abs'] = $this->value(1); | |
| 594 | + for ($s = 0; $s < 4; $s++) $seg['q'][$s] = $this->value(1) ? $this->signed(7) : 0; | |
| 595 | + for ($s = 0; $s < 4; $s++) $seg['lf'][$s] = $this->value(1) ? $this->signed(6) : 0; | |
| 596 | + } | |
| 597 | + if ($seg['map']) for ($s = 0; $s < 3; $s++) $seg['probs'][$s] = $this->value(1) ? $this->value(8) : 255; | |
| 598 | + } | |
| 599 | + $simple = $this->value(1); | |
| 600 | + $level = $this->value(6); | |
| 601 | + $sharp = $this->value(3); | |
| 602 | + $refDelta = $modeDelta = 0; | |
| 603 | + $useDelta = $this->value(1); | |
| 604 | + if ($useDelta && $this->value(1)) { | |
| 605 | + // 参照フレームとモード毎の差(キーフレームでは「現在のフレーム」と「B_PRED」だけ使う) | |
| 606 | + for ($i = 0; $i < 4; $i++) if ($this->value(1)) { $v = $this->signed(6); if ($i === 0) $refDelta = $v; } | |
| 607 | + for ($i = 0; $i < 4; $i++) if ($this->value(1)) { $v = $this->signed(6); if ($i === 0) $modeDelta = $v; } | |
| 608 | + } | |
| 609 | + $filterType = $level === 0 ? 0 : ($simple ? 1 : 2); | |
| 610 | + $numParts = 1 << $this->value(2); | |
| 611 | + | |
| 612 | + // 量子化 | |
| 613 | + $base = $this->value(7); | |
| 614 | + $dq = []; | |
| 615 | + for ($i = 0; $i < 5; $i++) $dq[$i] = $this->value(1) ? $this->signed(4) : 0; | |
| 616 | + $quant = []; | |
| 617 | + for ($s = 0; $s < 4; $s++) { | |
| 618 | + $q = $seg['use'] ? $seg['q'][$s] + ($seg['abs'] ? 0 : $base) : $base; | |
| 619 | + $c = fn(int $v, int $max = 127): int => $v < 0 ? 0 : ($v > $max ? $max : $v); | |
| 620 | + $quant[$s] = [ | |
| 621 | + self::DC_TABLE[$c($q + $dq[0])], self::AC_TABLE[$c($q)], | |
| 622 | + self::DC_TABLE[$c($q + $dq[1])] * 2, max(8, (self::AC_TABLE[$c($q + $dq[2])] * 101581) >> 16), | |
| 623 | + self::DC_TABLE[$c($q + $dq[3], 117)], self::AC_TABLE[$c($q + $dq[4])], | |
| 624 | + ]; | |
| 625 | + } | |
| 626 | + $this->value(1); // 確率を保存するか(静止画では関係無い) | |
| 627 | + $this->probs = self::COEFF_PROBS; | |
| 628 | + for ($t = 0; $t < 4; $t++) for ($b = 0; $b < 8; $b++) for ($c = 0; $c < 3; $c++) for ($i = 0; $i < 11; $i++) { | |
| 629 | + if ($this->bit(self::COEFF_UPDATE_PROBS[$t][$b][$c][$i])) $this->probs[$t][$b][$c][$i] = $this->value(8); | |
| 630 | + } | |
| 631 | + $useSkip = $this->value(1); | |
| 632 | + $skipProb = $useSkip ? $this->value(8) : 0; | |
| 633 | + | |
| 634 | + // フィルターの強さ(セグメントと4x4の予測かで変わる) | |
| 635 | + $fstr = []; | |
| 636 | + for ($s = 0; $s < 4; $s++) { | |
| 637 | + $lvl = $seg['use'] ? $seg['lf'][$s] + ($seg['abs'] ? 0 : $level) : $level; | |
| 638 | + for ($i4 = 0; $i4 < 2; $i4++) { | |
| 639 | + $l = $lvl; | |
| 640 | + if ($useDelta) $l += $refDelta + ($i4 ? $modeDelta : 0); | |
| 641 | + $l = $l < 0 ? 0 : ($l > 63 ? 63 : $l); | |
| 642 | + if ($l === 0) { | |
| 643 | + $fstr[$s][$i4] = null; | |
| 644 | + continue; | |
| 645 | + } | |
| 646 | + $il = $l; | |
| 647 | + if ($sharp > 0) { | |
| 648 | + $il >>= $sharp > 4 ? 2 : 1; | |
| 649 | + if ($il > 9 - $sharp) $il = 9 - $sharp; | |
| 650 | + } | |
| 651 | + if ($il < 1) $il = 1; | |
| 652 | + $fstr[$s][$i4] = [2 * $l + $il, $il, $l >= 40 ? 2 : ($l >= 15 ? 1 : 0)]; | |
| 653 | + } | |
| 654 | + } | |
| 655 | + | |
| 656 | + $mbw = ($w + 15) >> 4; | |
| 657 | + $mbh = ($h + 15) >> 4; | |
| 658 | + // 全てのマクロブロックの予測モード(最初のパーティション) | |
| 659 | + $mbs = []; | |
| 660 | + $intraT = array_fill(0, 4 * $mbw, self::DC); | |
| 661 | + for ($my = 0; $my < $mbh; $my++) { | |
| 662 | + $intraL = [self::DC, self::DC, self::DC, self::DC]; | |
| 663 | + for ($mx = 0; $mx < $mbw; $mx++) { | |
| 664 | + $mb = ['seg' => 0, 'skip' => 0]; | |
| 665 | + if ($seg['map']) $mb['seg'] = !$this->bit($seg['probs'][0]) ? $this->bit($seg['probs'][1]) : 2 + $this->bit($seg['probs'][2]); | |
| 666 | + if ($useSkip) $mb['skip'] = $this->bit($skipProb); | |
| 667 | + $mb['i4'] = !$this->bit(145); | |
| 668 | + if (!$mb['i4']) { | |
| 669 | + $ym = $this->bit(156) ? ($this->bit(128) ? self::TM : self::HE) : ($this->bit(163) ? self::VE : self::DC); | |
| 670 | + $mb['y'] = $ym; | |
| 671 | + for ($i = 0; $i < 4; $i++) $intraT[4 * $mx + $i] = $intraL[$i] = $ym; | |
| 672 | + } else { | |
| 673 | + $modes = []; | |
| 674 | + for ($y = 0; $y < 4; $y++) { | |
| 675 | + $ym = $intraL[$y]; | |
| 676 | + for ($x = 0; $x < 4; $x++) { | |
| 677 | + $prob = self::BMODE_PROBS[$intraT[4 * $mx + $x]][$ym]; | |
| 678 | + $i = self::BMODE_TREE[$this->bit($prob[0])]; | |
| 679 | + while ($i > 0) $i = self::BMODE_TREE[2 * $i + $this->bit($prob[$i])]; | |
| 680 | + $ym = -$i; | |
| 681 | + $intraT[4 * $mx + $x] = $ym; | |
| 682 | + $modes[] = $ym; | |
| 683 | + } | |
| 684 | + $intraL[$y] = $ym; | |
| 685 | + } | |
| 686 | + $mb['b'] = $modes; | |
| 687 | + } | |
| 688 | + $mb['uv'] = !$this->bit(142) ? self::DC : (!$this->bit(114) ? self::VE : ($this->bit(183) ? self::TM : self::HE)); | |
| 689 | + $mbs[] = $mb; | |
| 690 | + } | |
| 691 | + } | |
| 692 | + if ($this->eof()) throw new \RuntimeException('VP8の最初のパーティションが途中で切れています。'); | |
| 693 | + | |
| 694 | + // 係数のパーティション | |
| 695 | + $pos = 10 + $part; | |
| 696 | + $sizes = $pos; | |
| 697 | + $pos += 3 * ($numParts - 1); | |
| 698 | + if ($pos > $len) throw new \RuntimeException('VP8のデータが途中で切れています。'); | |
| 699 | + $parts = []; | |
| 700 | + for ($i = 0; $i < $numParts; $i++) { | |
| 701 | + $psize = $i < $numParts - 1 ? ord($data[$sizes + 3 * $i]) | (ord($data[$sizes + 3 * $i + 1]) << 8) | (ord($data[$sizes + 3 * $i + 2]) << 16) : $len - $pos; | |
| 702 | + $psize = min($psize, $len - $pos); | |
| 703 | + $parts[] = substr($data, $pos, $psize); | |
| 704 | + $pos += $psize; | |
| 705 | + } | |
| 706 | + | |
| 707 | + $bands = []; | |
| 708 | + for ($t = 0; $t < 4; $t++) for ($n = 0; $n < 17; $n++) $bands[$t][$n] = $this->probs[$t][self::BANDS[$n]]; | |
| 709 | + | |
| 710 | + $yw = $mbw * 16; | |
| 711 | + $uw = $mbw * 8; | |
| 712 | + $Y = str_repeat("\0", $yw * $mbh * 16); | |
| 713 | + $U = str_repeat("\0", $uw * $mbh * 8); | |
| 714 | + $V = $U; | |
| 715 | + $filters = []; | |
| 716 | + $tnz = array_fill(0, $mbw, array_fill(0, 9, 0)); | |
| 717 | + $states = []; | |
| 718 | + for ($my = 0; $my < $mbh; $my++) { | |
| 719 | + $pi = $my % $numParts; | |
| 720 | + if (isset($states[$pi])) $this->restore($states[$pi]); | |
| 721 | + else $this->init($parts[$pi], 0); | |
| 722 | + $lnz = array_fill(0, 9, 0); | |
| 723 | + for ($mx = 0; $mx < $mbw; $mx++) { | |
| 724 | + $mb = $mbs[$my * $mbw + $mx]; | |
| 725 | + $q = $quant[$mb['seg']]; | |
| 726 | + $coef = array_fill(0, 384, 0); | |
| 727 | + $nz = array_fill(0, 24, false); | |
| 728 | + $t = &$tnz[$mx]; | |
| 729 | + if ($mb['skip']) { | |
| 730 | + for ($i = 0; $i < 8; $i++) $t[$i] = $lnz[$i] = 0; | |
| 731 | + if (!$mb['i4']) $t[8] = $lnz[8] = 0; | |
| 732 | + $any = false; | |
| 733 | + } else { | |
| 734 | + $any = $this->residuals($mb['i4'], $q, $bands, $t, $lnz, $coef, $nz); | |
| 735 | + } | |
| 736 | + unset($t); | |
| 737 | + if ($filterType) { | |
| 738 | + $f = $fstr[$mb['seg']][$mb['i4'] ? 1 : 0]; | |
| 739 | + $filters[] = $f === null ? null : [...$f, $mb['i4'] || $any]; | |
| 740 | + } | |
| 741 | + $this->reconstruct($mb, $mx, $my, $mbw, $coef, $nz, $Y, $U, $V); | |
| 742 | + } | |
| 743 | + if ($this->eof()) throw new \RuntimeException('VP8の係数のデータが途中で切れています。'); | |
| 744 | + $states[$pi] = $this->save(); | |
| 745 | + } | |
| 746 | + | |
| 747 | + if ($filterType) { | |
| 748 | + for ($my = 0, $i = 0; $my < $mbh; $my++) { | |
| 749 | + for ($mx = 0; $mx < $mbw; $mx++, $i++) { | |
| 750 | + if ($filters[$i] === null) continue; | |
| 751 | + [$limit, $il, $hev, $inner] = $filters[$i]; | |
| 752 | + if ($filterType === 1) self::simpleFilter($Y, $yw, $mx, $my, $limit, $inner); | |
| 753 | + else self::normalFilter($Y, $U, $V, $yw, $uw, $mx, $my, $limit, $il, $hev, $inner); | |
| 754 | + } | |
| 755 | + } | |
| 756 | + } | |
| 757 | + return [$w, $h, self::toRgba($Y, $U, $V, $yw, $uw, $w, $h)]; | |
| 758 | + } | |
| 759 | + | |
| 760 | + private function init(string $data, int $pos): void { | |
| 761 | + $this->d = $data; | |
| 762 | + $this->p = $pos; | |
| 763 | + $this->value = 0; | |
| 764 | + $this->count = -8; | |
| 765 | + $this->range = 255; | |
| 766 | + } | |
| 767 | + | |
| 768 | + /** データの終わりを超えて読んだか(先読みした分は除く) */ | |
| 769 | + private function eof(): bool { | |
| 770 | + return $this->p - ($this->count >> 3) > strlen($this->d) + 1; | |
| 771 | + } | |
| 772 | + | |
| 773 | + private function save(): array { | |
| 774 | + return [$this->d, $this->p, $this->value, $this->count, $this->range]; | |
| 775 | + } | |
| 776 | + | |
| 777 | + private function restore(array $s): void { | |
| 778 | + [$this->d, $this->p, $this->value, $this->count, $this->range] = $s; | |
| 779 | + } | |
| 780 | + | |
| 781 | + private function bit(int $prob): int { | |
| 782 | + $split = 1 + ((($this->range - 1) * $prob) >> 8); | |
| 783 | + if ($this->count < 0) { | |
| 784 | + // 足りなければ数バイト読む(終わりの後は0) | |
| 785 | + do { | |
| 786 | + $this->value = ($this->value << 8) | ord($this->d[$this->p++] ?? "\0"); | |
| 787 | + $this->count += 8; | |
| 788 | + } while ($this->count < 40); | |
| 789 | + } | |
| 790 | + $big = $split << $this->count; | |
| 791 | + if ($this->value >= $big) { | |
| 792 | + $this->range -= $split; | |
| 793 | + $this->value -= $big; | |
| 794 | + $bit = 1; | |
| 795 | + } else { | |
| 796 | + $this->range = $split; | |
| 797 | + $bit = 0; | |
| 798 | + } | |
| 799 | + if ($this->range < 128) { | |
| 800 | + $s = $this->norm[$this->range]; | |
| 801 | + $this->range <<= $s; | |
| 802 | + $this->count -= $s; | |
| 803 | + } | |
| 804 | + return $bit; | |
| 805 | + } | |
| 806 | + | |
| 807 | + private function value(int $n): int { | |
| 808 | + $v = 0; | |
| 809 | + while ($n-- > 0) $v = ($v << 1) | $this->bit(128); | |
| 810 | + return $v; | |
| 811 | + } | |
| 812 | + | |
| 813 | + private function signed(int $n): int { | |
| 814 | + $v = $this->value($n); | |
| 815 | + return $this->bit(128) ? -$v : $v; | |
| 816 | + } | |
| 817 | + | |
| 818 | + /** | |
| 819 | + * マクロブロックの係数を読む | |
| 820 | + * | |
| 821 | + * @param array $t 上の非ゼロ(Yの4列、Uの2列、Vの2列、Y2) | |
| 822 | + * @param array $l 左の非ゼロ | |
| 823 | + * @return bool 0以外の係数があるか | |
| 824 | + */ | |
| 825 | + private function residuals(bool $i4, array $q, array $bands, array &$t, array &$l, array &$coef, array &$nz): bool { | |
| 826 | + $any = false; | |
| 827 | + if (!$i4) { | |
| 828 | + $dc = array_fill(0, 16, 0); | |
| 829 | + $n = $this->coeffs($bands[1], $t[8] + $l[8], $q[2], $q[3], 0, $dc, 0); | |
| 830 | + $t[8] = $l[8] = $n > 0 ? 1 : 0; | |
| 831 | + if ($n > 0) { | |
| 832 | + self::iwht($dc, $coef); | |
| 833 | + for ($b = 0; $b < 16; $b++) if ($coef[$b * 16] !== 0) $nz[$b] = $any = true; | |
| 834 | + } | |
| 835 | + $first = 1; | |
| 836 | + $ac = $bands[0]; | |
| 837 | + } else { | |
| 838 | + $first = 0; | |
| 839 | + $ac = $bands[3]; | |
| 840 | + } | |
| 841 | + for ($y = 0; $y < 4; $y++) { | |
| 842 | + $left = $l[$y]; | |
| 843 | + for ($x = 0; $x < 4; $x++) { | |
| 844 | + $b = $y * 4 + $x; | |
| 845 | + $n = $this->coeffs($ac, $left + $t[$x], $q[0], $q[1], $first, $coef, $b * 16); | |
| 846 | + $left = $t[$x] = $n > $first ? 1 : 0; | |
| 847 | + if ($left) $nz[$b] = $any = true; | |
| 848 | + } | |
| 849 | + $l[$y] = $left; | |
| 850 | + } | |
| 851 | + for ($ch = 0; $ch < 2; $ch++) { | |
| 852 | + for ($y = 0; $y < 2; $y++) { | |
| 853 | + $left = $l[4 + $ch * 2 + $y]; | |
| 854 | + for ($x = 0; $x < 2; $x++) { | |
| 855 | + $b = 16 + $ch * 4 + $y * 2 + $x; | |
| 856 | + $n = $this->coeffs($bands[2], $left + $t[4 + $ch * 2 + $x], $q[4], $q[5], 0, $coef, $b * 16); | |
| 857 | + $left = $t[4 + $ch * 2 + $x] = $n > 0 ? 1 : 0; | |
| 858 | + if ($left) $nz[$b] = $any = true; | |
| 859 | + } | |
| 860 | + $l[4 + $ch * 2 + $y] = $left; | |
| 861 | + } | |
| 862 | + } | |
| 863 | + return $any; | |
| 864 | + } | |
| 865 | + | |
| 866 | + /** | |
| 867 | + * 1ブロックの係数を読む | |
| 868 | + * | |
| 869 | + * @return int 最後の0以外の係数の次の位置 | |
| 870 | + */ | |
| 871 | + private function coeffs(array $bp, int $ctx, int $dq0, int $dq1, int $n, array &$out, int $o): int { | |
| 872 | + $p = $bp[$n][$ctx]; | |
| 873 | + for (; $n < 16; $n++) { | |
| 874 | + if (!$this->bit($p[0])) return $n; | |
| 875 | + while (!$this->bit($p[1])) { | |
| 876 | + $p = $bp[++$n][0]; | |
| 877 | + if ($n === 16) return 16; | |
| 878 | + } | |
| 879 | + if (!$this->bit($p[2])) { | |
| 880 | + $v = 1; | |
| 881 | + $next = 1; | |
| 882 | + } else { | |
| 883 | + $v = $this->large($p); | |
| 884 | + $next = 2; | |
| 885 | + } | |
| 886 | + $out[$o + self::ZIGZAG[$n]] = ($this->bit(128) ? -$v : $v) * ($n > 0 ? $dq1 : $dq0); | |
| 887 | + $p = $bp[$n + 1][$next]; | |
| 888 | + } | |
| 889 | + return 16; | |
| 890 | + } | |
| 891 | + | |
| 892 | + private function large(array $p): int { | |
| 893 | + if (!$this->bit($p[3])) { | |
| 894 | + if (!$this->bit($p[4])) return 2; | |
| 895 | + return 3 + $this->bit($p[5]); | |
| 896 | + } | |
| 897 | + if (!$this->bit($p[6])) { | |
| 898 | + if (!$this->bit($p[7])) return 5 + $this->bit(159); | |
| 899 | + $v = 7 + 2 * $this->bit(165); | |
| 900 | + return $v + $this->bit(145); | |
| 901 | + } | |
| 902 | + $b1 = $this->bit($p[8]); | |
| 903 | + $cat = 2 * $b1 + $this->bit($p[9 + $b1]); | |
| 904 | + $v = 0; | |
| 905 | + foreach (self::CAT[$cat] as $prob) $v = 2 * $v + $this->bit($prob); | |
| 906 | + return $v + 3 + (8 << $cat); | |
| 907 | + } | |
| 908 | + | |
| 909 | + /** Y2の逆ウォルシュ・アダマール変換(結果は各ブロックの直流) */ | |
| 910 | + private static function iwht(array $in, array &$coef): void { | |
| 911 | + $tmp = []; | |
| 912 | + for ($i = 0; $i < 4; $i++) { | |
| 913 | + $a0 = $in[$i] + $in[12 + $i]; | |
| 914 | + $a1 = $in[4 + $i] + $in[8 + $i]; | |
| 915 | + $a2 = $in[4 + $i] - $in[8 + $i]; | |
| 916 | + $a3 = $in[$i] - $in[12 + $i]; | |
| 917 | + $tmp[$i] = $a0 + $a1; | |
| 918 | + $tmp[8 + $i] = $a0 - $a1; | |
| 919 | + $tmp[4 + $i] = $a3 + $a2; | |
| 920 | + $tmp[12 + $i] = $a3 - $a2; | |
| 921 | + } | |
| 922 | + for ($i = 0; $i < 4; $i++) { | |
| 923 | + $dc = $tmp[$i * 4] + 3; | |
| 924 | + $a0 = $dc + $tmp[$i * 4 + 3]; | |
| 925 | + $a1 = $tmp[$i * 4 + 1] + $tmp[$i * 4 + 2]; | |
| 926 | + $a2 = $tmp[$i * 4 + 1] - $tmp[$i * 4 + 2]; | |
| 927 | + $a3 = $dc - $tmp[$i * 4 + 3]; | |
| 928 | + $coef[($i * 4) * 16] = ($a0 + $a1) >> 3; | |
| 929 | + $coef[($i * 4 + 1) * 16] = ($a3 + $a2) >> 3; | |
| 930 | + $coef[($i * 4 + 2) * 16] = ($a0 - $a1) >> 3; | |
| 931 | + $coef[($i * 4 + 3) * 16] = ($a3 - $a2) >> 3; | |
| 932 | + } | |
| 933 | + } | |
| 934 | + | |
| 935 | + /** | |
| 936 | + * 逆DCTして作業用の配列に足す | |
| 937 | + */ | |
| 938 | + private static function idctAdd(array $c, int $o, array &$ws, int $at): void { | |
| 939 | + $tmp = []; | |
| 940 | + for ($i = 0; $i < 4; $i++) { | |
| 941 | + $c0 = $c[$o + $i]; | |
| 942 | + $c4 = $c[$o + 4 + $i]; | |
| 943 | + $c8 = $c[$o + 8 + $i]; | |
| 944 | + $c12 = $c[$o + 12 + $i]; | |
| 945 | + $a = $c0 + $c8; | |
| 946 | + $b = $c0 - $c8; | |
| 947 | + $cc = (($c4 * 35468) >> 16) - ((($c12 * 20091) >> 16) + $c12); | |
| 948 | + $d = ((($c4 * 20091) >> 16) + $c4) + (($c12 * 35468) >> 16); | |
| 949 | + $tmp[$i * 4] = $a + $d; | |
| 950 | + $tmp[$i * 4 + 1] = $b + $cc; | |
| 951 | + $tmp[$i * 4 + 2] = $b - $cc; | |
| 952 | + $tmp[$i * 4 + 3] = $a - $d; | |
| 953 | + } | |
| 954 | + for ($i = 0; $i < 4; $i++) { | |
| 955 | + $dc = $tmp[$i] + 4; | |
| 956 | + $a = $dc + $tmp[8 + $i]; | |
| 957 | + $b = $dc - $tmp[8 + $i]; | |
| 958 | + $cc = (($tmp[4 + $i] * 35468) >> 16) - ((($tmp[12 + $i] * 20091) >> 16) + $tmp[12 + $i]); | |
| 959 | + $d = ((($tmp[4 + $i] * 20091) >> 16) + $tmp[4 + $i]) + (($tmp[12 + $i] * 35468) >> 16); | |
| 960 | + $r = $at + $i * self::BPS; | |
| 961 | + foreach ([$a + $d, $b + $cc, $b - $cc, $a - $d] as $k => $v) { | |
| 962 | + $v = $ws[$r + $k] + ($v >> 3); | |
| 963 | + $ws[$r + $k] = $v < 0 ? 0 : ($v > 255 ? 255 : $v); | |
| 964 | + } | |
| 965 | + } | |
| 966 | + } | |
| 967 | + | |
| 968 | + /** | |
| 969 | + * 予測して残差を足し、平面に書く | |
| 970 | + */ | |
| 971 | + private function reconstruct(array $mb, int $mx, int $my, int $mbw, array $coef, array $nz, string &$Y, string &$U, string &$V): void { | |
| 972 | + $yw = $mbw * 16; | |
| 973 | + $ws = self::border($Y, $yw, 16, $mx, $my); | |
| 974 | + if ($mb['i4']) { | |
| 975 | + self::topRight($ws, $Y, $yw, $mx, $my, $mbw); | |
| 976 | + for ($b = 0; $b < 16; $b++) { | |
| 977 | + $at = (($b >> 2) * 4 + 1) * self::BPS + ($b & 3) * 4 + 1; | |
| 978 | + self::predict4($mb['b'][$b], $ws, $at); | |
| 979 | + if ($nz[$b]) self::idctAdd($coef, $b * 16, $ws, $at); | |
| 980 | + } | |
| 981 | + } else { | |
| 982 | + self::predictBlock($mb['y'], $ws, 16, $mx, $my); | |
| 983 | + for ($b = 0; $b < 16; $b++) { | |
| 984 | + if ($nz[$b]) self::idctAdd($coef, $b * 16, $ws, (($b >> 2) * 4 + 1) * self::BPS + ($b & 3) * 4 + 1); | |
| 985 | + } | |
| 986 | + } | |
| 987 | + self::store($Y, $yw, 16, $mx, $my, $ws); | |
| 988 | + $uw = $mbw * 8; | |
| 989 | + foreach ([&$U, &$V] as $ch => &$plane) { | |
| 990 | + $ws = self::border($plane, $uw, 8, $mx, $my); | |
| 991 | + self::predictBlock($mb['uv'], $ws, 8, $mx, $my); | |
| 992 | + for ($b = 0; $b < 4; $b++) { | |
| 993 | + $bi = 16 + $ch * 4 + $b; | |
| 994 | + if ($nz[$bi]) self::idctAdd($coef, $bi * 16, $ws, (($b >> 1) * 4 + 1) * self::BPS + ($b & 1) * 4 + 1); | |
| 995 | + } | |
| 996 | + self::store($plane, $uw, 8, $mx, $my, $ws); | |
| 997 | + } | |
| 998 | + unset($plane); | |
| 999 | + } | |
| 1000 | + | |
| 1001 | + /** | |
| 1002 | + * 4x4の予測の右上:上のマクロブロックの行の続き(右端では最後の画素)。2行目以降の右端のブロックも同じ物を使う | |
| 1003 | + */ | |
| 1004 | + private static function topRight(array &$ws, string $Y, int $yw, int $mx, int $my, int $mbw): void { | |
| 1005 | + for ($i = 0; $i < 4; $i++) { | |
| 1006 | + $tr = $my === 0 ? 127 : ($mx < $mbw - 1 ? ord($Y[($my * 16 - 1) * $yw + $mx * 16 + 16 + $i]) : ord($Y[($my * 16 - 1) * $yw + $mx * 16 + 15])); | |
| 1007 | + foreach ([0, 4, 8, 12] as $r) $ws[$r * self::BPS + 17 + $i] = $tr; | |
| 1008 | + } | |
| 1009 | + } | |
| 1010 | + | |
| 1011 | + /** | |
| 1012 | + * 作業用の配列に上と左の画素を入れる(画像の外は上が127、左が129) | |
| 1013 | + */ | |
| 1014 | + private static function border(string $plane, int $stride, int $size, int $mx, int $my): array { | |
| 1015 | + $ws = array_fill(0, ($size + 1) * self::BPS, 0); | |
| 1016 | + $x0 = $mx * $size; | |
| 1017 | + $y0 = $my * $size; | |
| 1018 | + if ($my > 0) { | |
| 1019 | + $o = ($y0 - 1) * $stride + $x0; | |
| 1020 | + for ($x = 0; $x < $size; $x++) $ws[1 + $x] = ord($plane[$o + $x]); | |
| 1021 | + } else { | |
| 1022 | + for ($x = 0; $x < $size; $x++) $ws[1 + $x] = 127; | |
| 1023 | + } | |
| 1024 | + $ws[0] = $my === 0 ? 127 : ($mx === 0 ? 129 : ord($plane[($y0 - 1) * $stride + $x0 - 1])); | |
| 1025 | + for ($y = 0; $y < $size; $y++) $ws[($y + 1) * self::BPS] = $mx === 0 ? 129 : ord($plane[($y0 + $y) * $stride + $x0 - 1]); | |
| 1026 | + return $ws; | |
| 1027 | + } | |
| 1028 | + | |
| 1029 | + private static function store(string &$plane, int $stride, int $size, int $mx, int $my, array $ws): void { | |
| 1030 | + for ($y = 0; $y < $size; $y++) { | |
| 1031 | + $o = ($my * $size + $y) * $stride + $mx * $size; | |
| 1032 | + $r = ($y + 1) * self::BPS + 1; | |
| 1033 | + for ($x = 0; $x < $size; $x++) $plane[$o + $x] = chr($ws[$r + $x]); | |
| 1034 | + } | |
| 1035 | + } | |
| 1036 | + | |
| 1037 | + /** 16x16(輝度)又は8x8(色差)の予測 */ | |
| 1038 | + private static function predictBlock(int $mode, array &$ws, int $size, int $mx, int $my): void { | |
| 1039 | + $B = self::BPS; | |
| 1040 | + $shift = $size === 16 ? 4 : 3; | |
| 1041 | + if ($mode === self::DC) { | |
| 1042 | + if ($mx === 0 && $my === 0) { | |
| 1043 | + $dc = 128; | |
| 1044 | + } else { | |
| 1045 | + $sum = 0; | |
| 1046 | + if ($my > 0) for ($x = 0; $x < $size; $x++) $sum += $ws[1 + $x]; | |
| 1047 | + if ($mx > 0) for ($y = 0; $y < $size; $y++) $sum += $ws[($y + 1) * $B]; | |
| 1048 | + $s = ($mx > 0 && $my > 0) ? $shift + 1 : $shift; | |
| 1049 | + $dc = ($sum + (1 << ($s - 1))) >> $s; | |
| 1050 | + } | |
| 1051 | + for ($y = 0; $y < $size; $y++) for ($x = 0; $x < $size; $x++) $ws[($y + 1) * $B + 1 + $x] = $dc; | |
| 1052 | + return; | |
| 1053 | + } | |
| 1054 | + for ($y = 0; $y < $size; $y++) { | |
| 1055 | + $r = ($y + 1) * $B; | |
| 1056 | + $left = $ws[$r]; | |
| 1057 | + for ($x = 0; $x < $size; $x++) { | |
| 1058 | + if ($mode === self::VE) { | |
| 1059 | + $v = $ws[1 + $x]; | |
| 1060 | + } else if ($mode === self::HE) { | |
| 1061 | + $v = $left; | |
| 1062 | + } else { | |
| 1063 | + $v = $ws[1 + $x] + $left - $ws[0]; | |
| 1064 | + $v = $v < 0 ? 0 : ($v > 255 ? 255 : $v); | |
| 1065 | + } | |
| 1066 | + $ws[$r + 1 + $x] = $v; | |
| 1067 | + } | |
| 1068 | + } | |
| 1069 | + } | |
| 1070 | + | |
| 1071 | + /** 4x4の予測($atはブロックの左上) */ | |
| 1072 | + private static function predict4(int $mode, array &$ws, int $at): void { | |
| 1073 | + $B = self::BPS; | |
| 1074 | + $t = $at - $B; | |
| 1075 | + $X = $ws[$t - 1]; | |
| 1076 | + [$A, $Bb, $C, $D, $E, $F, $G, $H] = [$ws[$t], $ws[$t + 1], $ws[$t + 2], $ws[$t + 3], $ws[$t + 4], $ws[$t + 5], $ws[$t + 6], $ws[$t + 7]]; | |
| 1077 | + [$I, $J, $K, $L] = [$ws[$at - 1], $ws[$at + $B - 1], $ws[$at + 2 * $B - 1], $ws[$at + 3 * $B - 1]]; | |
| 1078 | + $avg3 = fn($a, $b, $c) => ($a + 2 * $b + $c + 2) >> 2; | |
| 1079 | + $avg2 = fn($a, $b) => ($a + $b + 1) >> 1; | |
| 1080 | + $o = []; | |
| 1081 | + switch ($mode) { | |
| 1082 | + case 0: // DC | |
| 1083 | + $dc = ($A + $Bb + $C + $D + $I + $J + $K + $L + 4) >> 3; | |
| 1084 | + $o = array_fill(0, 16, $dc); | |
| 1085 | + break; | |
| 1086 | + case 1: // TM | |
| 1087 | + foreach ([$I, $J, $K, $L] as $y => $l) { | |
| 1088 | + foreach ([$A, $Bb, $C, $D] as $x => $tp) { | |
| 1089 | + $v = $tp + $l - $X; | |
| 1090 | + $o[$y * 4 + $x] = $v < 0 ? 0 : ($v > 255 ? 255 : $v); | |
| 1091 | + } | |
| 1092 | + } | |
| 1093 | + break; | |
| 1094 | + case 2: // VE | |
| 1095 | + $row = [$avg3($X, $A, $Bb), $avg3($A, $Bb, $C), $avg3($Bb, $C, $D), $avg3($C, $D, $E)]; | |
| 1096 | + $o = [...$row, ...$row, ...$row, ...$row]; | |
| 1097 | + break; | |
| 1098 | + case 3: // HE | |
| 1099 | + foreach ([$avg3($X, $I, $J), $avg3($I, $J, $K), $avg3($J, $K, $L), $avg3($K, $L, $L)] as $y => $v) for ($x = 0; $x < 4; $x++) $o[$y * 4 + $x] = $v; | |
| 1100 | + break; | |
| 1101 | + case 4: // RD | |
| 1102 | + $o[12] = $avg3($J, $K, $L); | |
| 1103 | + $o[13] = $o[8] = $avg3($I, $J, $K); | |
| 1104 | + $o[14] = $o[9] = $o[4] = $avg3($X, $I, $J); | |
| 1105 | + $o[15] = $o[10] = $o[5] = $o[0] = $avg3($A, $X, $I); | |
| 1106 | + $o[11] = $o[6] = $o[1] = $avg3($Bb, $A, $X); | |
| 1107 | + $o[7] = $o[2] = $avg3($C, $Bb, $A); | |
| 1108 | + $o[3] = $avg3($D, $C, $Bb); | |
| 1109 | + break; | |
| 1110 | + case 5: // VR | |
| 1111 | + $o[0] = $o[9] = $avg2($X, $A); | |
| 1112 | + $o[1] = $o[10] = $avg2($A, $Bb); | |
| 1113 | + $o[2] = $o[11] = $avg2($Bb, $C); | |
| 1114 | + $o[3] = $avg2($C, $D); | |
| 1115 | + $o[12] = $avg3($K, $J, $I); | |
| 1116 | + $o[8] = $avg3($J, $I, $X); | |
| 1117 | + $o[4] = $o[13] = $avg3($I, $X, $A); | |
| 1118 | + $o[5] = $o[14] = $avg3($X, $A, $Bb); | |
| 1119 | + $o[6] = $o[15] = $avg3($A, $Bb, $C); | |
| 1120 | + $o[7] = $avg3($Bb, $C, $D); | |
| 1121 | + break; | |
| 1122 | + case 6: // LD | |
| 1123 | + $o[0] = $avg3($A, $Bb, $C); | |
| 1124 | + $o[1] = $o[4] = $avg3($Bb, $C, $D); | |
| 1125 | + $o[2] = $o[5] = $o[8] = $avg3($C, $D, $E); | |
| 1126 | + $o[3] = $o[6] = $o[9] = $o[12] = $avg3($D, $E, $F); | |
| 1127 | + $o[7] = $o[10] = $o[13] = $avg3($E, $F, $G); | |
| 1128 | + $o[11] = $o[14] = $avg3($F, $G, $H); | |
| 1129 | + $o[15] = $avg3($G, $H, $H); | |
| 1130 | + break; | |
| 1131 | + case 7: // VL | |
| 1132 | + $o[0] = $avg2($A, $Bb); | |
| 1133 | + $o[1] = $o[8] = $avg2($Bb, $C); | |
| 1134 | + $o[2] = $o[9] = $avg2($C, $D); | |
| 1135 | + $o[3] = $o[10] = $avg2($D, $E); | |
| 1136 | + $o[4] = $avg3($A, $Bb, $C); | |
| 1137 | + $o[5] = $o[12] = $avg3($Bb, $C, $D); | |
| 1138 | + $o[6] = $o[13] = $avg3($C, $D, $E); | |
| 1139 | + $o[7] = $o[14] = $avg3($D, $E, $F); | |
| 1140 | + $o[11] = $avg3($E, $F, $G); | |
| 1141 | + $o[15] = $avg3($F, $G, $H); | |
| 1142 | + break; | |
| 1143 | + case 8: // HD | |
| 1144 | + $o[0] = $o[6] = $avg2($I, $X); | |
| 1145 | + $o[4] = $o[10] = $avg2($J, $I); | |
| 1146 | + $o[8] = $o[14] = $avg2($K, $J); | |
| 1147 | + $o[12] = $avg2($L, $K); | |
| 1148 | + $o[3] = $avg3($A, $Bb, $C); | |
| 1149 | + $o[2] = $avg3($X, $A, $Bb); | |
| 1150 | + $o[1] = $o[7] = $avg3($I, $X, $A); | |
| 1151 | + $o[5] = $o[11] = $avg3($J, $I, $X); | |
| 1152 | + $o[9] = $o[15] = $avg3($K, $J, $I); | |
| 1153 | + $o[13] = $avg3($L, $K, $J); | |
| 1154 | + break; | |
| 1155 | + default: // HU | |
| 1156 | + $o[0] = $avg2($I, $J); | |
| 1157 | + $o[2] = $o[4] = $avg2($J, $K); | |
| 1158 | + $o[6] = $o[8] = $avg2($K, $L); | |
| 1159 | + $o[1] = $avg3($I, $J, $K); | |
| 1160 | + $o[3] = $o[5] = $avg3($J, $K, $L); | |
| 1161 | + $o[7] = $o[9] = $avg3($K, $L, $L); | |
| 1162 | + $o[11] = $o[10] = $o[12] = $o[13] = $o[14] = $o[15] = $L; | |
| 1163 | + } | |
| 1164 | + for ($i = 0; $i < 16; $i++) $ws[$at + ($i >> 2) * $B + ($i & 3)] = $o[$i]; | |
| 1165 | + } | |
| 1166 | + | |
| 1167 | + //////// 書き出し | |
| 1168 | + | |
| 1169 | + // 算術符号化 | |
| 1170 | + private string $out = ''; | |
| 1171 | + private int $bottom = 0; | |
| 1172 | + private int $erange = 255; | |
| 1173 | + private int $shiftCount = 24; | |
| 1174 | + /** 確率を決める為に数えるだけ([種類][帯][文脈][番号] => [0の数, 1の数]) */ | |
| 1175 | + private ?array $stats = null; | |
| 1176 | + | |
| 1177 | + /** | |
| 1178 | + * RGBAをVP8チャンクの中身にする(アルファは無視) | |
| 1179 | + * | |
| 1180 | + * @param array $opt quality(0~100、既定80)、filter(normal・simple・none)、sharpness(0~7)、partitions(1・2・4・8) | |
| 1181 | + */ | |
| 1182 | + public function encode(string $rgba, int $w, int $h, array $opt = []): string { | |
| 1183 | + $quality = (int)($opt['quality'] ?? 80); | |
| 1184 | + $filter = $opt['filter'] ?? 'normal'; | |
| 1185 | + $sharp = max(0, min(7, (int)($opt['sharpness'] ?? 0))); | |
| 1186 | + $parts = (int)($opt['partitions'] ?? 1); | |
| 1187 | + if (!in_array($parts, [1, 2, 4, 8], true)) throw new \InvalidArgumentException('VP8のパーティションの数は1・2・4・8です。'); | |
| 1188 | + $mbw = ($w + 15) >> 4; | |
| 1189 | + $mbh = ($h + 15) >> 4; | |
| 1190 | + [$SY, $SU, $SV] = self::toYuv($rgba, $w, $h, $mbw, $mbh); | |
| 1191 | + $yw = $mbw * 16; | |
| 1192 | + $uw = $mbw * 8; | |
| 1193 | + | |
| 1194 | + // 品質から量子化(libwebpと同じ曲線) | |
| 1195 | + $c = max(0, min(100, $quality)) / 100; | |
| 1196 | + $c = $c < 0.75 ? $c * 2 / 3 : 2 * $c - 1; | |
| 1197 | + $q = max(0, min(127, (int)(127 * (1 - $c ** (1 / 3))))); | |
| 1198 | + $Q = [ | |
| 1199 | + self::DC_TABLE[$q], self::AC_TABLE[$q], | |
| 1200 | + self::DC_TABLE[$q] * 2, max(8, (self::AC_TABLE[$q] * 101581) >> 16), | |
| 1201 | + self::DC_TABLE[min($q, 117)], self::AC_TABLE[$q], | |
| 1202 | + ]; | |
| 1203 | + // 歪み(二乗誤差)とビット数の重み | |
| 1204 | + $lambda = 0.03 * $Q[1] * $Q[1]; | |
| 1205 | + | |
| 1206 | + $RY = str_repeat("\0", $yw * $mbh * 16); | |
| 1207 | + $RU = str_repeat("\0", $uw * $mbh * 8); | |
| 1208 | + $RV = $RU; | |
| 1209 | + $mbs = []; | |
| 1210 | + for ($my = 0; $my < $mbh; $my++) { | |
| 1211 | + for ($mx = 0; $mx < $mbw; $mx++) { | |
| 1212 | + $mbs[] = $this->encodeMb($SY, $SU, $SV, $RY, $RU, $RV, $mx, $my, $mbw, $Q, $lambda); | |
| 1213 | + } | |
| 1214 | + } | |
| 1215 | + | |
| 1216 | + // 係数の確率を画像に合わせる | |
| 1217 | + $this->stats = []; | |
| 1218 | + $this->probs = self::COEFF_PROBS; | |
| 1219 | + $this->tokens($mbs, $mbw, 1); | |
| 1220 | + $updates = []; | |
| 1221 | + $cost = fn(int $p, int $bit): float => -log(($bit ? 256 - $p : $p) / 256, 2); | |
| 1222 | + for ($t = 0; $t < 4; $t++) for ($b = 0; $b < 8; $b++) for ($k = 0; $k < 3; $k++) for ($i = 0; $i < 11; $i++) { | |
| 1223 | + [$n0, $n1] = $this->stats[$t][$b][$k][$i] ?? [0, 0]; | |
| 1224 | + if ($n0 + $n1 === 0) continue; | |
| 1225 | + $old = self::COEFF_PROBS[$t][$b][$k][$i]; | |
| 1226 | + $new = max(1, min(255, (int)round(256 * $n0 / ($n0 + $n1)))); | |
| 1227 | + $up = self::COEFF_UPDATE_PROBS[$t][$b][$k][$i]; | |
| 1228 | + $saved = $n0 * ($cost($old, 0) - $cost($new, 0)) + $n1 * ($cost($old, 1) - $cost($new, 1)) - 8 - $cost($up, 1) + $cost($up, 0); | |
| 1229 | + if ($new !== $old && $saved > 0) { | |
| 1230 | + $updates[$t][$b][$k][$i] = $new; | |
| 1231 | + $this->probs[$t][$b][$k][$i] = $new; | |
| 1232 | + } | |
| 1233 | + } | |
| 1234 | + $this->stats = null; | |
| 1235 | + $skips = count(array_filter($mbs, fn($mb) => $mb['skip'])); | |
| 1236 | + $skipProb = max(1, min(255, (int)round(256 * (count($mbs) - $skips) / count($mbs)))); | |
| 1237 | + | |
| 1238 | + // 最初のパーティション:ヘッダーと予測モード | |
| 1239 | + $this->startWrite(); | |
| 1240 | + $this->putValue(0, 2); // 色空間、クランプ | |
| 1241 | + $this->putValue(0, 1); // セグメント無し | |
| 1242 | + $level = (self::AC_TABLE[$q] >> 2) * 300 / 356 * 1.2; | |
| 1243 | + $level = $level < 2 || $filter === 'none' ? 0 : min(63, (int)round($level)); | |
| 1244 | + $this->putValue($filter === 'simple' ? 1 : 0, 1); | |
| 1245 | + $this->putValue($level, 6); | |
| 1246 | + $this->putValue($sharp, 3); | |
| 1247 | + $this->putValue(0, 1); // モード毎のフィルターの差は無し | |
| 1248 | + $this->putValue((int)log($parts, 2), 2); | |
| 1249 | + $this->putValue($q, 7); | |
| 1250 | + $this->putValue(0, 5); // 量子化の差は無し | |
| 1251 | + $this->putValue(0, 1); | |
| 1252 | + for ($t = 0; $t < 4; $t++) for ($b = 0; $b < 8; $b++) for ($k = 0; $k < 3; $k++) for ($i = 0; $i < 11; $i++) { | |
| 1253 | + $new = $updates[$t][$b][$k][$i] ?? null; | |
| 1254 | + $this->put(self::COEFF_UPDATE_PROBS[$t][$b][$k][$i], $new !== null ? 1 : 0); | |
| 1255 | + if ($new !== null) $this->putValue($new, 8); | |
| 1256 | + } | |
| 1257 | + $this->putValue(1, 1); | |
| 1258 | + $this->putValue($skipProb, 8); | |
| 1259 | + $paths = self::bmodePaths(); | |
| 1260 | + $intraT = array_fill(0, 4 * $mbw, self::DC); | |
| 1261 | + foreach ($mbs as $n => $mb) { | |
| 1262 | + $mx = $n % $mbw; | |
| 1263 | + if ($mx === 0) $intraL = [self::DC, self::DC, self::DC, self::DC]; | |
| 1264 | + $this->put($skipProb, $mb['skip'] ? 1 : 0); | |
| 1265 | + $this->put(145, $mb['i4'] ? 0 : 1); | |
| 1266 | + if (!$mb['i4']) { | |
| 1267 | + $ym = $mb['y']; | |
| 1268 | + if ($ym === self::DC || $ym === self::VE) { | |
| 1269 | + $this->put(156, 0); | |
| 1270 | + $this->put(163, $ym === self::VE ? 1 : 0); | |
| 1271 | + } else { | |
| 1272 | + $this->put(156, 1); | |
| 1273 | + $this->put(128, $ym === self::TM ? 1 : 0); | |
| 1274 | + } | |
| 1275 | + for ($i = 0; $i < 4; $i++) $intraT[4 * $mx + $i] = $intraL[$i] = $ym; | |
| 1276 | + } else { | |
| 1277 | + for ($y = 0; $y < 4; $y++) { | |
| 1278 | + for ($x = 0; $x < 4; $x++) { | |
| 1279 | + $m = $mb['b'][$y * 4 + $x]; | |
| 1280 | + $prob = self::BMODE_PROBS[$intraT[4 * $mx + $x]][$intraL[$y]]; | |
| 1281 | + foreach ($paths[$m] as [$pi, $bit]) $this->put($prob[$pi], $bit); | |
| 1282 | + $intraT[4 * $mx + $x] = $intraL[$y] = $m; | |
| 1283 | + } | |
| 1284 | + } | |
| 1285 | + } | |
| 1286 | + $uv = $mb['uv']; | |
| 1287 | + $this->put(142, $uv === self::DC ? 0 : 1); | |
| 1288 | + if ($uv !== self::DC) { | |
| 1289 | + $this->put(114, $uv === self::VE ? 0 : 1); | |
| 1290 | + if ($uv !== self::VE) $this->put(183, $uv === self::TM ? 1 : 0); | |
| 1291 | + } | |
| 1292 | + } | |
| 1293 | + $first = $this->finishWrite(); | |
| 1294 | + | |
| 1295 | + // 係数のパーティション(マクロブロックの行毎に順に使う) | |
| 1296 | + $tokens = $this->tokens($mbs, $mbw, $parts); | |
| 1297 | + $sizes = ''; | |
| 1298 | + for ($i = 0; $i < $parts - 1; $i++) $sizes .= substr(pack('V', strlen($tokens[$i])), 0, 3); | |
| 1299 | + | |
| 1300 | + $tag = (strlen($first) << 5) | 0x10; | |
| 1301 | + return chr($tag & 255).chr(($tag >> 8) & 255).chr($tag >> 16)."\x9d\x01\x2a".pack('vv', $w, $h).$first.$sizes.implode('', $tokens); | |
| 1302 | + } | |
| 1303 | + | |
| 1304 | + /** 4x4の予測モードの木の道筋(モード => [[確率の番号, ビット], ...]) */ | |
| 1305 | + private static function bmodePaths(): array { | |
| 1306 | + $paths = []; | |
| 1307 | + $walk = function(int $node, array $path) use (&$walk, &$paths): void { | |
| 1308 | + for ($b = 0; $b < 2; $b++) { | |
| 1309 | + $child = self::BMODE_TREE[2 * $node + $b]; | |
| 1310 | + $p = [...$path, [$node, $b]]; | |
| 1311 | + if ($child <= 0) $paths[-$child] = $p; | |
| 1312 | + else $walk($child, $p); | |
| 1313 | + } | |
| 1314 | + }; | |
| 1315 | + $walk(0, []); | |
| 1316 | + return $paths; | |
| 1317 | + } | |
| 1318 | + | |
| 1319 | + /** | |
| 1320 | + * RGBAをYUV 4:2:0にする(マクロブロックの大きさまで端の画素を伸ばす) | |
| 1321 | + */ | |
| 1322 | + private static function toYuv(string $rgba, int $w, int $h, int $mbw, int $mbh): array { | |
| 1323 | + $yw = $mbw * 16; | |
| 1324 | + $uw = $mbw * 8; | |
| 1325 | + $Y = $U = $V = ''; | |
| 1326 | + $prev = null; | |
| 1327 | + for ($y = 0; $y < $mbh * 16; $y++) { | |
| 1328 | + $row = array_values(unpack('C*', substr($rgba, min($y, $h - 1) * $w * 4, $w * 4))); | |
| 1329 | + $line = []; | |
| 1330 | + for ($x = 0; $x < $yw; $x++) { | |
| 1331 | + $o = min($x, $w - 1) * 4; | |
| 1332 | + $line[] = (16839 * $row[$o] + 33059 * $row[$o + 1] + 6420 * $row[$o + 2] + 32768 + (16 << 16)) >> 16; | |
| 1333 | + } | |
| 1334 | + $Y .= pack('C*', ...$line); | |
| 1335 | + if (!($y & 1)) { | |
| 1336 | + $prev = $row; | |
| 1337 | + continue; | |
| 1338 | + } | |
| 1339 | + $ul = $vl = []; | |
| 1340 | + for ($x = 0; $x < $uw; $x++) { | |
| 1341 | + $o0 = min(2 * $x, $w - 1) * 4; | |
| 1342 | + $o1 = min(2 * $x + 1, $w - 1) * 4; | |
| 1343 | + $r = $prev[$o0] + $prev[$o1] + $row[$o0] + $row[$o1]; | |
| 1344 | + $g = $prev[$o0 + 1] + $prev[$o1 + 1] + $row[$o0 + 1] + $row[$o1 + 1]; | |
| 1345 | + $b = $prev[$o0 + 2] + $prev[$o1 + 2] + $row[$o0 + 2] + $row[$o1 + 2]; | |
| 1346 | + $u = (-9719 * $r - 19081 * $g + 28800 * $b + (1 << 17) + (128 << 18)) >> 18; | |
| 1347 | + $v = (28800 * $r - 24116 * $g - 4684 * $b + (1 << 17) + (128 << 18)) >> 18; | |
| 1348 | + $ul[] = $u < 0 ? 0 : ($u > 255 ? 255 : $u); | |
| 1349 | + $vl[] = $v < 0 ? 0 : ($v > 255 ? 255 : $v); | |
| 1350 | + } | |
| 1351 | + $U .= pack('C*', ...$ul); | |
| 1352 | + $V .= pack('C*', ...$vl); | |
| 1353 | + } | |
| 1354 | + return [$Y, $U, $V]; | |
| 1355 | + } | |
| 1356 | + | |
| 1357 | + /** 平面からブロックの画素を読む */ | |
| 1358 | + private static function block(string $plane, int $stride, int $size, int $mx, int $my): array { | |
| 1359 | + $px = []; | |
| 1360 | + for ($y = 0; $y < $size; $y++) { | |
| 1361 | + foreach (unpack('C*', substr($plane, ($my * $size + $y) * $stride + $mx * $size, $size)) as $v) $px[] = $v; | |
| 1362 | + } | |
| 1363 | + return $px; | |
| 1364 | + } | |
| 1365 | + | |
| 1366 | + /** | |
| 1367 | + * 1つのマクロブロックの予測モードを選んで量子化し、復元した画素を書く | |
| 1368 | + */ | |
| 1369 | + private function encodeMb(string $SY, string $SU, string $SV, string &$RY, string &$RU, string &$RV, int $mx, int $my, int $mbw, array $Q, float $lambda): array { | |
| 1370 | + $B = self::BPS; | |
| 1371 | + $yw = $mbw * 16; | |
| 1372 | + $src = self::block($SY, $yw, 16, $mx, $my); | |
| 1373 | + $ws0 = self::border($RY, $yw, 16, $mx, $my); | |
| 1374 | + | |
| 1375 | + // 16x16:予測の誤差が一番小さいモード | |
| 1376 | + $best = null; | |
| 1377 | + foreach ([self::DC, self::VE, self::HE, self::TM] as $m) { | |
| 1378 | + $ws = $ws0; | |
| 1379 | + self::predictBlock($m, $ws, 16, $mx, $my); | |
| 1380 | + $sad = 0; | |
| 1381 | + for ($i = 0; $i < 256; $i++) $sad += abs($src[$i] - $ws[(($i >> 4) + 1) * $B + ($i & 15) + 1]); | |
| 1382 | + if ($best === null || $sad < $best[0]) $best = [$sad, $m, $ws]; | |
| 1383 | + } | |
| 1384 | + [, $m16, $ws16] = $best; | |
| 1385 | + $res = []; | |
| 1386 | + $dcs = []; | |
| 1387 | + for ($b = 0; $b < 16; $b++) { | |
| 1388 | + $at = (($b >> 2) * 4 + 1) * $B + ($b & 3) * 4 + 1; | |
| 1389 | + $s = (($b >> 2) * 4) * 16 + ($b & 3) * 4; | |
| 1390 | + $res[$b] = self::fdct($src, $s, 16, $ws16, $at); | |
| 1391 | + $dcs[] = $res[$b][0]; | |
| 1392 | + } | |
| 1393 | + $lv2 = []; | |
| 1394 | + $dq2 = self::quant(self::fwht($dcs), $Q[2], $Q[3], 96, 108, 0, $lv2); | |
| 1395 | + $dc = array_fill(0, 384, 0); | |
| 1396 | + self::iwht($dq2, $dc); | |
| 1397 | + $lv16 = []; | |
| 1398 | + $bits16 = self::bits($lv2, 0) + 3; | |
| 1399 | + for ($b = 0; $b < 16; $b++) { | |
| 1400 | + $l = []; | |
| 1401 | + $dq = self::quant($res[$b], $Q[0], $Q[1], 96, 110, 1, $l); | |
| 1402 | + $lv16[$b] = $l; | |
| 1403 | + $bits16 += self::bits($l, 1); | |
| 1404 | + $dq[0] = $dc[$b * 16]; | |
| 1405 | + if (array_filter($dq)) self::idctAdd($dq, 0, $ws16, (($b >> 2) * 4 + 1) * $B + ($b & 3) * 4 + 1); | |
| 1406 | + } | |
| 1407 | + $score16 = self::sse($src, $ws16, 16) + $lambda * $bits16; | |
| 1408 | + | |
| 1409 | + // 4x4:ブロック毎に予測の誤差が一番小さいモード(先に復元したブロックから予測する) | |
| 1410 | + $ws4 = $ws0; | |
| 1411 | + self::topRight($ws4, $RY, $yw, $mx, $my, $mbw); | |
| 1412 | + $modes = []; | |
| 1413 | + $lv4 = []; | |
| 1414 | + $bits4 = 1; | |
| 1415 | + for ($b = 0; $b < 16; $b++) { | |
| 1416 | + $at = (($b >> 2) * 4 + 1) * $B + ($b & 3) * 4 + 1; | |
| 1417 | + $s = (($b >> 2) * 4) * 16 + ($b & 3) * 4; | |
| 1418 | + $bestM = 0; | |
| 1419 | + $bestSad = PHP_INT_MAX; | |
| 1420 | + for ($m = 0; $m < 10; $m++) { | |
| 1421 | + self::predict4($m, $ws4, $at); | |
| 1422 | + $sad = 0; | |
| 1423 | + for ($i = 0; $i < 16; $i++) $sad += abs($src[$s + ($i >> 2) * 16 + ($i & 3)] - $ws4[$at + ($i >> 2) * $B + ($i & 3)]); | |
| 1424 | + // DC以外は少し不利に(モードのビット) | |
| 1425 | + if ($m > 0) $sad += 4; | |
| 1426 | + if ($sad < $bestSad) { | |
| 1427 | + $bestSad = $sad; | |
| 1428 | + $bestM = $m; | |
| 1429 | + } | |
| 1430 | + } | |
| 1431 | + self::predict4($bestM, $ws4, $at); | |
| 1432 | + $modes[] = $bestM; | |
| 1433 | + $l = []; | |
| 1434 | + $dq = self::quant(self::fdct($src, $s, 16, $ws4, $at), $Q[0], $Q[1], 96, 110, 0, $l); | |
| 1435 | + $lv4[$b] = $l; | |
| 1436 | + $bits4 += self::bits($l, 0) + ($bestM === 0 ? 1 : 4); | |
| 1437 | + if (array_filter($dq)) self::idctAdd($dq, 0, $ws4, $at); | |
| 1438 | + } | |
| 1439 | + $score4 = self::sse($src, $ws4, 16) + $lambda * $bits4; | |
| 1440 | + | |
| 1441 | + $mb = ['skip' => false]; | |
| 1442 | + if ($score4 < $score16) { | |
| 1443 | + $mb['i4'] = true; | |
| 1444 | + $mb['b'] = $modes; | |
| 1445 | + $mb['lv'] = $lv4; | |
| 1446 | + $ws = $ws4; | |
| 1447 | + } else { | |
| 1448 | + $mb['i4'] = false; | |
| 1449 | + $mb['y'] = $m16; | |
| 1450 | + $mb['y2'] = $lv2; | |
| 1451 | + $mb['lv'] = $lv16; | |
| 1452 | + $ws = $ws16; | |
| 1453 | + } | |
| 1454 | + self::store($RY, $yw, 16, $mx, $my, $ws); | |
| 1455 | + | |
| 1456 | + // 色差:UとVの予測の誤差の合計が一番小さいモード | |
| 1457 | + $uw = $mbw * 8; | |
| 1458 | + $srcU = self::block($SU, $uw, 8, $mx, $my); | |
| 1459 | + $srcV = self::block($SV, $uw, 8, $mx, $my); | |
| 1460 | + $wu0 = self::border($RU, $uw, 8, $mx, $my); | |
| 1461 | + $wv0 = self::border($RV, $uw, 8, $mx, $my); | |
| 1462 | + $best = null; | |
| 1463 | + foreach ([self::DC, self::VE, self::HE, self::TM] as $m) { | |
| 1464 | + $wu = $wu0; | |
| 1465 | + $wv = $wv0; | |
| 1466 | + self::predictBlock($m, $wu, 8, $mx, $my); | |
| 1467 | + self::predictBlock($m, $wv, 8, $mx, $my); | |
| 1468 | + $sad = 0; | |
| 1469 | + for ($i = 0; $i < 64; $i++) { | |
| 1470 | + $k = (($i >> 3) + 1) * $B + ($i & 7) + 1; | |
| 1471 | + $sad += abs($srcU[$i] - $wu[$k]) + abs($srcV[$i] - $wv[$k]); | |
| 1472 | + } | |
| 1473 | + if ($best === null || $sad < $best[0]) $best = [$sad, $m, $wu, $wv]; | |
| 1474 | + } | |
| 1475 | + [, $mb['uv'], $wu, $wv] = $best; | |
| 1476 | + $mb['uvlv'] = []; | |
| 1477 | + foreach ([[$srcU, &$wu, &$RU], [$srcV, &$wv, &$RV]] as [$s8, &$w8, &$plane]) { | |
| 1478 | + for ($b = 0; $b < 4; $b++) { | |
| 1479 | + $at = (($b >> 1) * 4 + 1) * $B + ($b & 1) * 4 + 1; | |
| 1480 | + $l = []; | |
| 1481 | + $dq = self::quant(self::fdct($s8, (($b >> 1) * 4) * 8 + ($b & 1) * 4, 8, $w8, $at), $Q[4], $Q[5], 110, 115, 0, $l); | |
| 1482 | + $mb['uvlv'][] = $l; | |
| 1483 | + if (array_filter($dq)) self::idctAdd($dq, 0, $w8, $at); | |
| 1484 | + } | |
| 1485 | + self::store($plane, $uw, 8, $mx, $my, $w8); | |
| 1486 | + } | |
| 1487 | + unset($w8, $plane); | |
| 1488 | + | |
| 1489 | + $mb['skip'] = !array_filter($mb['y2'] ?? []) && !array_filter(array_merge(...$mb['lv'], ...$mb['uvlv'])); | |
| 1490 | + return $mb; | |
| 1491 | + } | |
| 1492 | + | |
| 1493 | + /** 二乗誤差 */ | |
| 1494 | + private static function sse(array $src, array $ws, int $size): int { | |
| 1495 | + $e = 0; | |
| 1496 | + for ($y = 0; $y < $size; $y++) { | |
| 1497 | + $r = ($y + 1) * self::BPS + 1; | |
| 1498 | + for ($x = 0; $x < $size; $x++) { | |
| 1499 | + $d = $src[$y * $size + $x] - $ws[$r + $x]; | |
| 1500 | + $e += $d * $d; | |
| 1501 | + } | |
| 1502 | + } | |
| 1503 | + return $e; | |
| 1504 | + } | |
| 1505 | + | |
| 1506 | + /** 係数のビット数の大体の見積もり */ | |
| 1507 | + private static function bits(array $lv, int $first): int { | |
| 1508 | + $bits = 1; | |
| 1509 | + for ($n = $first; $n < 16; $n++) { | |
| 1510 | + $a = abs($lv[$n]); | |
| 1511 | + if ($a) $bits += 3 + 2 * (int)log($a, 2); | |
| 1512 | + else $bits++; | |
| 1513 | + } | |
| 1514 | + return $bits; | |
| 1515 | + } | |
| 1516 | + | |
| 1517 | + /** | |
| 1518 | + * 4x4の残差の離散コサイン変換(libwebpと同じ整数の計算) | |
| 1519 | + * | |
| 1520 | + * @param int $s 元の画素の左上、$strideは1行の数 | |
| 1521 | + */ | |
| 1522 | + private static function fdct(array $src, int $s, int $stride, array $ws, int $at): array { | |
| 1523 | + $tmp = []; | |
| 1524 | + for ($i = 0; $i < 4; $i++) { | |
| 1525 | + $r = $s + $i * $stride; | |
| 1526 | + $p = $at + $i * self::BPS; | |
| 1527 | + $d0 = $src[$r] - $ws[$p]; | |
| 1528 | + $d1 = $src[$r + 1] - $ws[$p + 1]; | |
| 1529 | + $d2 = $src[$r + 2] - $ws[$p + 2]; | |
| 1530 | + $d3 = $src[$r + 3] - $ws[$p + 3]; | |
| 1531 | + $a0 = $d0 + $d3; | |
| 1532 | + $a1 = $d1 + $d2; | |
| 1533 | + $a2 = $d1 - $d2; | |
| 1534 | + $a3 = $d0 - $d3; | |
| 1535 | + $tmp[$i * 4] = ($a0 + $a1) * 8; | |
| 1536 | + $tmp[$i * 4 + 1] = ($a2 * 2217 + $a3 * 5352 + 1812) >> 9; | |
| 1537 | + $tmp[$i * 4 + 2] = ($a0 - $a1) * 8; | |
| 1538 | + $tmp[$i * 4 + 3] = ($a3 * 2217 - $a2 * 5352 + 937) >> 9; | |
| 1539 | + } | |
| 1540 | + $out = []; | |
| 1541 | + for ($i = 0; $i < 4; $i++) { | |
| 1542 | + $a0 = $tmp[$i] + $tmp[12 + $i]; | |
| 1543 | + $a1 = $tmp[4 + $i] + $tmp[8 + $i]; | |
| 1544 | + $a2 = $tmp[4 + $i] - $tmp[8 + $i]; | |
| 1545 | + $a3 = $tmp[$i] - $tmp[12 + $i]; | |
| 1546 | + $out[$i] = ($a0 + $a1 + 7) >> 4; | |
| 1547 | + $out[4 + $i] = (($a2 * 2217 + $a3 * 5352 + 12000) >> 16) + ($a3 !== 0 ? 1 : 0); | |
| 1548 | + $out[8 + $i] = ($a0 - $a1 + 7) >> 4; | |
| 1549 | + $out[12 + $i] = ($a3 * 2217 - $a2 * 5352 + 51000) >> 16; | |
| 1550 | + } | |
| 1551 | + ksort($out); | |
| 1552 | + return $out; | |
| 1553 | + } | |
| 1554 | + | |
| 1555 | + /** 16個の直流のウォルシュ・アダマール変換 */ | |
| 1556 | + private static function fwht(array $in): array { | |
| 1557 | + $tmp = []; | |
| 1558 | + for ($i = 0; $i < 4; $i++) { | |
| 1559 | + $a0 = $in[$i * 4] + $in[$i * 4 + 2]; | |
| 1560 | + $a1 = $in[$i * 4 + 1] + $in[$i * 4 + 3]; | |
| 1561 | + $a2 = $in[$i * 4 + 1] - $in[$i * 4 + 3]; | |
| 1562 | + $a3 = $in[$i * 4] - $in[$i * 4 + 2]; | |
| 1563 | + $tmp[$i * 4] = $a0 + $a1; | |
| 1564 | + $tmp[$i * 4 + 1] = $a3 + $a2; | |
| 1565 | + $tmp[$i * 4 + 2] = $a3 - $a2; | |
| 1566 | + $tmp[$i * 4 + 3] = $a0 - $a1; | |
| 1567 | + } | |
| 1568 | + $out = []; | |
| 1569 | + for ($i = 0; $i < 4; $i++) { | |
| 1570 | + $a0 = $tmp[$i] + $tmp[8 + $i]; | |
| 1571 | + $a1 = $tmp[4 + $i] + $tmp[12 + $i]; | |
| 1572 | + $a2 = $tmp[4 + $i] - $tmp[12 + $i]; | |
| 1573 | + $a3 = $tmp[$i] - $tmp[8 + $i]; | |
| 1574 | + $out[$i] = ($a0 + $a1) >> 1; | |
| 1575 | + $out[4 + $i] = ($a3 + $a2) >> 1; | |
| 1576 | + $out[8 + $i] = ($a3 - $a2) >> 1; | |
| 1577 | + $out[12 + $i] = ($a0 - $a1) >> 1; | |
| 1578 | + } | |
| 1579 | + ksort($out); | |
| 1580 | + return $out; | |
| 1581 | + } | |
| 1582 | + | |
| 1583 | + /** | |
| 1584 | + * 量子化 | |
| 1585 | + * | |
| 1586 | + * @param array $lv 量子化した値(ジグザグの順) | |
| 1587 | + * @return array 逆量子化した係数(復元用) | |
| 1588 | + */ | |
| 1589 | + private static function quant(array $c, int $q0, int $q1, int $bias0, int $bias1, int $first, array &$lv): array { | |
| 1590 | + $dq = array_fill(0, 16, 0); | |
| 1591 | + $lv = array_fill(0, 16, 0); | |
| 1592 | + for ($n = $first; $n < 16; $n++) { | |
| 1593 | + $j = self::ZIGZAG[$n]; | |
| 1594 | + [$q, $bias] = $n > 0 ? [$q1, $bias1] : [$q0, $bias0]; | |
| 1595 | + $level = intdiv(abs($c[$j]) * 256 + $bias * $q, $q * 256); | |
| 1596 | + if ($level === 0) continue; | |
| 1597 | + $level = min(2047, $level); | |
| 1598 | + if ($c[$j] < 0) $level = -$level; | |
| 1599 | + $lv[$n] = $level; | |
| 1600 | + $dq[$j] = $level * $q; | |
| 1601 | + } | |
| 1602 | + return $dq; | |
| 1603 | + } | |
| 1604 | + | |
| 1605 | + /** | |
| 1606 | + * 全ての係数を書く(statsが配列なら数えるだけ) | |
| 1607 | + * | |
| 1608 | + * @return array パーティション毎の中身 | |
| 1609 | + */ | |
| 1610 | + private function tokens(array $mbs, int $mbw, int $parts): array { | |
| 1611 | + $tnz = array_fill(0, $mbw, array_fill(0, 9, 0)); | |
| 1612 | + $states = []; | |
| 1613 | + $cur = -1; | |
| 1614 | + foreach ($mbs as $n => $mb) { | |
| 1615 | + $mx = $n % $mbw; | |
| 1616 | + if ($mx === 0) { | |
| 1617 | + $lnz = array_fill(0, 9, 0); | |
| 1618 | + if ($this->stats === null) { | |
| 1619 | + if ($cur >= 0) $states[$cur] = [$this->out, $this->bottom, $this->erange, $this->shiftCount]; | |
| 1620 | + $cur = intdiv($n, $mbw) % $parts; | |
| 1621 | + if (isset($states[$cur])) [$this->out, $this->bottom, $this->erange, $this->shiftCount] = $states[$cur]; | |
| 1622 | + else $this->startWrite(); | |
| 1623 | + } | |
| 1624 | + } | |
| 1625 | + $t = &$tnz[$mx]; | |
| 1626 | + if ($mb['skip']) { | |
| 1627 | + for ($i = 0; $i < 8; $i++) $t[$i] = $lnz[$i] = 0; | |
| 1628 | + if (!$mb['i4']) $t[8] = $lnz[8] = 0; | |
| 1629 | + unset($t); | |
| 1630 | + continue; | |
| 1631 | + } | |
| 1632 | + if (!$mb['i4']) { | |
| 1633 | + $t[8] = $lnz[8] = $this->block1(1, $t[8] + $lnz[8], $mb['y2'], 0); | |
| 1634 | + $type = 0; | |
| 1635 | + $first = 1; | |
| 1636 | + } else { | |
| 1637 | + $type = 3; | |
| 1638 | + $first = 0; | |
| 1639 | + } | |
| 1640 | + for ($y = 0; $y < 4; $y++) { | |
| 1641 | + for ($x = 0; $x < 4; $x++) { | |
| 1642 | + $t[$x] = $lnz[$y] = $this->block1($type, $lnz[$y] + $t[$x], $mb['lv'][$y * 4 + $x], $first); | |
| 1643 | + } | |
| 1644 | + } | |
| 1645 | + for ($ch = 0; $ch < 2; $ch++) { | |
| 1646 | + for ($y = 0; $y < 2; $y++) { | |
| 1647 | + for ($x = 0; $x < 2; $x++) { | |
| 1648 | + $ti = 4 + $ch * 2 + $x; | |
| 1649 | + $li = 4 + $ch * 2 + $y; | |
| 1650 | + $t[$ti] = $lnz[$li] = $this->block1(2, $lnz[$li] + $t[$ti], $mb['uvlv'][$ch * 4 + $y * 2 + $x], 0); | |
| 1651 | + } | |
| 1652 | + } | |
| 1653 | + } | |
| 1654 | + unset($t); | |
| 1655 | + } | |
| 1656 | + if ($this->stats !== null) return []; | |
| 1657 | + $states[$cur] = [$this->out, $this->bottom, $this->erange, $this->shiftCount]; | |
| 1658 | + $out = []; | |
| 1659 | + for ($i = 0; $i < $parts; $i++) { | |
| 1660 | + if (isset($states[$i])) [$this->out, $this->bottom, $this->erange, $this->shiftCount] = $states[$i]; | |
| 1661 | + else $this->startWrite(); | |
| 1662 | + $out[] = $this->finishWrite(); | |
| 1663 | + } | |
| 1664 | + return $out; | |
| 1665 | + } | |
| 1666 | + | |
| 1667 | + /** | |
| 1668 | + * 1ブロックの係数を書く | |
| 1669 | + * | |
| 1670 | + * @return int 0以外の係数があれば1 | |
| 1671 | + */ | |
| 1672 | + private function block1(int $type, int $ctx, array $lv, int $first): int { | |
| 1673 | + $last = -1; | |
| 1674 | + for ($n = 15; $n >= $first; $n--) { | |
| 1675 | + if ($lv[$n] !== 0) { | |
| 1676 | + $last = $n; | |
| 1677 | + break; | |
| 1678 | + } | |
| 1679 | + } | |
| 1680 | + $n = $first; | |
| 1681 | + $band = self::BANDS[$n]; | |
| 1682 | + if ($last < 0) { | |
| 1683 | + $this->tok($type, $band, $ctx, 0, 0); | |
| 1684 | + return 0; | |
| 1685 | + } | |
| 1686 | + while (true) { | |
| 1687 | + $this->tok($type, $band, $ctx, 0, 1); | |
| 1688 | + while ($lv[$n] === 0) { | |
| 1689 | + $this->tok($type, $band, $ctx, 1, 0); | |
| 1690 | + $n++; | |
| 1691 | + $band = self::BANDS[$n]; | |
| 1692 | + $ctx = 0; | |
| 1693 | + } | |
| 1694 | + $this->tok($type, $band, $ctx, 1, 1); | |
| 1695 | + $v = abs($lv[$n]); | |
| 1696 | + if ($v === 1) { | |
| 1697 | + $this->tok($type, $band, $ctx, 2, 0); | |
| 1698 | + $ctx = 1; | |
| 1699 | + } else { | |
| 1700 | + $this->tok($type, $band, $ctx, 2, 1); | |
| 1701 | + if ($v <= 4) { | |
| 1702 | + $this->tok($type, $band, $ctx, 3, 0); | |
| 1703 | + $this->tok($type, $band, $ctx, 4, $v === 2 ? 0 : 1); | |
| 1704 | + if ($v > 2) $this->tok($type, $band, $ctx, 5, $v - 3); | |
| 1705 | + } else if ($v <= 10) { | |
| 1706 | + $this->tok($type, $band, $ctx, 3, 1); | |
| 1707 | + $this->tok($type, $band, $ctx, 6, 0); | |
| 1708 | + if ($v <= 6) { | |
| 1709 | + $this->tok($type, $band, $ctx, 7, 0); | |
| 1710 | + $this->fixed(159, $v - 5); | |
| 1711 | + } else { | |
| 1712 | + $this->tok($type, $band, $ctx, 7, 1); | |
| 1713 | + $this->fixed(165, ($v - 7) >> 1); | |
| 1714 | + $this->fixed(145, ($v - 7) & 1); | |
| 1715 | + } | |
| 1716 | + } else { | |
| 1717 | + $this->tok($type, $band, $ctx, 3, 1); | |
| 1718 | + $this->tok($type, $band, $ctx, 6, 1); | |
| 1719 | + $cat = $v < 19 ? 0 : ($v < 35 ? 1 : ($v < 67 ? 2 : 3)); | |
| 1720 | + $this->tok($type, $band, $ctx, 8, $cat >> 1); | |
| 1721 | + $this->tok($type, $band, $ctx, 9 + ($cat >> 1), $cat & 1); | |
| 1722 | + $extra = $v - 3 - (8 << $cat); | |
| 1723 | + $nb = count(self::CAT[$cat]); | |
| 1724 | + foreach (self::CAT[$cat] as $k => $prob) $this->fixed($prob, ($extra >> ($nb - 1 - $k)) & 1); | |
| 1725 | + } | |
| 1726 | + $ctx = 2; | |
| 1727 | + } | |
| 1728 | + $this->fixed(128, $lv[$n] < 0 ? 1 : 0); | |
| 1729 | + $n++; | |
| 1730 | + if ($n === 16) return 1; | |
| 1731 | + $band = self::BANDS[$n]; | |
| 1732 | + if ($n > $last) { | |
| 1733 | + $this->tok($type, $band, $ctx, 0, 0); | |
| 1734 | + return 1; | |
| 1735 | + } | |
| 1736 | + } | |
| 1737 | + } | |
| 1738 | + | |
| 1739 | + private function tok(int $t, int $b, int $c, int $i, int $bit): void { | |
| 1740 | + if ($this->stats !== null) { | |
| 1741 | + $this->stats[$t][$b][$c][$i][$bit] = ($this->stats[$t][$b][$c][$i][$bit] ?? 0) + 1; | |
| 1742 | + $this->stats[$t][$b][$c][$i][1 - $bit] ??= 0; | |
| 1743 | + return; | |
| 1744 | + } | |
| 1745 | + $this->put($this->probs[$t][$b][$c][$i], $bit); | |
| 1746 | + } | |
| 1747 | + | |
| 1748 | + private function fixed(int $prob, int $bit): void { | |
| 1749 | + if ($this->stats === null) $this->put($prob, $bit); | |
| 1750 | + } | |
| 1751 | + | |
| 1752 | + private function startWrite(): void { | |
| 1753 | + $this->out = ''; | |
| 1754 | + $this->bottom = 0; | |
| 1755 | + $this->erange = 255; | |
| 1756 | + $this->shiftCount = 24; | |
| 1757 | + } | |
| 1758 | + | |
| 1759 | + private function put(int $prob, int $bit): void { | |
| 1760 | + $split = 1 + ((($this->erange - 1) * $prob) >> 8); | |
| 1761 | + if ($bit) { | |
| 1762 | + $this->bottom += $split; | |
| 1763 | + $this->erange -= $split; | |
| 1764 | + } else { | |
| 1765 | + $this->erange = $split; | |
| 1766 | + } | |
| 1767 | + while ($this->erange < 128) { | |
| 1768 | + $this->erange <<= 1; | |
| 1769 | + if ($this->bottom & 0x80000000) $this->carry(); | |
| 1770 | + $this->bottom = ($this->bottom << 1) & 0xffffffff; | |
| 1771 | + if (!--$this->shiftCount) { | |
| 1772 | + $this->out .= chr($this->bottom >> 24); | |
| 1773 | + $this->bottom &= 0xffffff; | |
| 1774 | + $this->shiftCount = 8; | |
| 1775 | + } | |
| 1776 | + } | |
| 1777 | + } | |
| 1778 | + | |
| 1779 | + /** 桁上がりを書いた分に足す */ | |
| 1780 | + private function carry(): void { | |
| 1781 | + for ($i = strlen($this->out) - 1; $i >= 0 && $this->out[$i] === "\xff"; $i--) $this->out[$i] = "\0"; | |
| 1782 | + if ($i >= 0) $this->out[$i] = chr(ord($this->out[$i]) + 1); | |
| 1783 | + } | |
| 1784 | + | |
| 1785 | + private function putValue(int $v, int $n): void { | |
| 1786 | + while ($n-- > 0) $this->put(128, ($v >> $n) & 1); | |
| 1787 | + } | |
| 1788 | + | |
| 1789 | + private function finishWrite(): string { | |
| 1790 | + $c = $this->shiftCount; | |
| 1791 | + $v = $this->bottom; | |
| 1792 | + if ($v & (1 << (32 - $c))) $this->carry(); | |
| 1793 | + $v = ($v << ($c & 7)) & 0xffffffff; | |
| 1794 | + for ($c >>= 3; --$c >= 0;) $v = ($v << 8) & 0xffffffff; | |
| 1795 | + for ($c = 0; $c < 4; $c++) { | |
| 1796 | + $this->out .= chr($v >> 24); | |
| 1797 | + $v = ($v << 8) & 0xffffffff; | |
| 1798 | + } | |
| 1799 | + return $this->out; | |
| 1800 | + } | |
| 1801 | + | |
| 1802 | + //////// ループフィルター | |
| 1803 | + | |
| 1804 | + private static function simpleFilter(string &$Y, int $stride, int $mx, int $my, int $limit, bool $inner): void { | |
| 1805 | + $o = $my * 16 * $stride + $mx * 16; | |
| 1806 | + if ($mx > 0) self::simpleEdge($Y, $o, 1, $stride, 2 * ($limit + 4) + 1); | |
| 1807 | + if ($inner) for ($k = 4; $k < 16; $k += 4) self::simpleEdge($Y, $o + $k, 1, $stride, 2 * $limit + 1); | |
| 1808 | + if ($my > 0) self::simpleEdge($Y, $o, $stride, 1, 2 * ($limit + 4) + 1); | |
| 1809 | + if ($inner) for ($k = 4; $k < 16; $k += 4) self::simpleEdge($Y, $o + $k * $stride, $stride, 1, 2 * $limit + 1); | |
| 1810 | + } | |
| 1811 | + | |
| 1812 | + /** | |
| 1813 | + * @param int $hs 端を跨ぐ方向の1画素 | |
| 1814 | + * @param int $vs 端に沿う方向の1画素 | |
| 1815 | + */ | |
| 1816 | + private static function simpleEdge(string &$p, int $o, int $hs, int $vs, int $t2): void { | |
| 1817 | + for ($i = 0; $i < 16; $i++, $o += $vs) { | |
| 1818 | + $p1 = ord($p[$o - 2 * $hs]); | |
| 1819 | + $p0 = ord($p[$o - $hs]); | |
| 1820 | + $q0 = ord($p[$o]); | |
| 1821 | + $q1 = ord($p[$o + $hs]); | |
| 1822 | + if (4 * abs($p0 - $q0) + abs($p1 - $q1) > $t2) continue; | |
| 1823 | + self::filter2($p, $o, $hs, $p1, $p0, $q0, $q1); | |
| 1824 | + } | |
| 1825 | + } | |
| 1826 | + | |
| 1827 | + private static function normalFilter(string &$Y, string &$U, string &$V, int $yw, int $uw, int $mx, int $my, int $limit, int $il, int $hev, bool $inner): void { | |
| 1828 | + $oy = $my * 16 * $yw + $mx * 16; | |
| 1829 | + $ou = $my * 8 * $uw + $mx * 8; | |
| 1830 | + if ($mx > 0) { | |
| 1831 | + self::edge($Y, $oy, 1, $yw, 16, $limit + 4, $il, $hev, true); | |
| 1832 | + self::edge($U, $ou, 1, $uw, 8, $limit + 4, $il, $hev, true); | |
| 1833 | + self::edge($V, $ou, 1, $uw, 8, $limit + 4, $il, $hev, true); | |
| 1834 | + } | |
| 1835 | + if ($inner) { | |
| 1836 | + for ($k = 4; $k < 16; $k += 4) self::edge($Y, $oy + $k, 1, $yw, 16, $limit, $il, $hev, false); | |
| 1837 | + self::edge($U, $ou + 4, 1, $uw, 8, $limit, $il, $hev, false); | |
| 1838 | + self::edge($V, $ou + 4, 1, $uw, 8, $limit, $il, $hev, false); | |
| 1839 | + } | |
| 1840 | + if ($my > 0) { | |
| 1841 | + self::edge($Y, $oy, $yw, 1, 16, $limit + 4, $il, $hev, true); | |
| 1842 | + self::edge($U, $ou, $uw, 1, 8, $limit + 4, $il, $hev, true); | |
| 1843 | + self::edge($V, $ou, $uw, 1, 8, $limit + 4, $il, $hev, true); | |
| 1844 | + } | |
| 1845 | + if ($inner) { | |
| 1846 | + for ($k = 4; $k < 16; $k += 4) self::edge($Y, $oy + $k * $yw, $yw, 1, 16, $limit, $il, $hev, false); | |
| 1847 | + self::edge($U, $ou + 4 * $uw, $uw, 1, 8, $limit, $il, $hev, false); | |
| 1848 | + self::edge($V, $ou + 4 * $uw, $uw, 1, 8, $limit, $il, $hev, false); | |
| 1849 | + } | |
| 1850 | + } | |
| 1851 | + | |
| 1852 | + /** | |
| 1853 | + * 通常のフィルター($mb:マクロブロックの端なら6画素、中なら4画素を変える) | |
| 1854 | + */ | |
| 1855 | + private static function edge(string &$p, int $o, int $hs, int $vs, int $n, int $thresh, int $it, int $hevT, bool $mb): void { | |
| 1856 | + $t2 = 2 * $thresh + 1; | |
| 1857 | + for ($i = 0; $i < $n; $i++, $o += $vs) { | |
| 1858 | + $p1 = ord($p[$o - 2 * $hs]); | |
| 1859 | + $p0 = ord($p[$o - $hs]); | |
| 1860 | + $q0 = ord($p[$o]); | |
| 1861 | + $q1 = ord($p[$o + $hs]); | |
| 1862 | + if (4 * abs($p0 - $q0) + abs($p1 - $q1) > $t2) continue; | |
| 1863 | + $p3 = ord($p[$o - 4 * $hs]); | |
| 1864 | + $p2 = ord($p[$o - 3 * $hs]); | |
| 1865 | + $q2 = ord($p[$o + 2 * $hs]); | |
| 1866 | + $q3 = ord($p[$o + 3 * $hs]); | |
| 1867 | + if (abs($p3 - $p2) > $it || abs($p2 - $p1) > $it || abs($p1 - $p0) > $it || abs($q3 - $q2) > $it || abs($q2 - $q1) > $it || abs($q1 - $q0) > $it) continue; | |
| 1868 | + if (abs($p1 - $p0) > $hevT || abs($q1 - $q0) > $hevT) { | |
| 1869 | + self::filter2($p, $o, $hs, $p1, $p0, $q0, $q1); | |
| 1870 | + } else if ($mb) { | |
| 1871 | + $a = 3 * ($q0 - $p0) + self::sclip1($p1 - $q1); | |
| 1872 | + $a = $a < -128 ? -128 : ($a > 127 ? 127 : $a); | |
| 1873 | + $a1 = (27 * $a + 63) >> 7; | |
| 1874 | + $a2 = (18 * $a + 63) >> 7; | |
| 1875 | + $a3 = (9 * $a + 63) >> 7; | |
| 1876 | + $p[$o - 3 * $hs] = chr(self::clip($p2 + $a3)); | |
| 1877 | + $p[$o - 2 * $hs] = chr(self::clip($p1 + $a2)); | |
| 1878 | + $p[$o - $hs] = chr(self::clip($p0 + $a1)); | |
| 1879 | + $p[$o] = chr(self::clip($q0 - $a1)); | |
| 1880 | + $p[$o + $hs] = chr(self::clip($q1 - $a2)); | |
| 1881 | + $p[$o + 2 * $hs] = chr(self::clip($q2 - $a3)); | |
| 1882 | + } else { | |
| 1883 | + $a = 3 * ($q0 - $p0); | |
| 1884 | + $a1 = self::sclip2(($a + 4) >> 3); | |
| 1885 | + $a2 = self::sclip2(($a + 3) >> 3); | |
| 1886 | + $a3 = ($a1 + 1) >> 1; | |
| 1887 | + $p[$o - 2 * $hs] = chr(self::clip($p1 + $a3)); | |
| 1888 | + $p[$o - $hs] = chr(self::clip($p0 + $a2)); | |
| 1889 | + $p[$o] = chr(self::clip($q0 - $a1)); | |
| 1890 | + $p[$o + $hs] = chr(self::clip($q1 - $a3)); | |
| 1891 | + } | |
| 1892 | + } | |
| 1893 | + } | |
| 1894 | + | |
| 1895 | + private static function filter2(string &$p, int $o, int $hs, int $p1, int $p0, int $q0, int $q1): void { | |
| 1896 | + $a = 3 * ($q0 - $p0) + self::sclip1($p1 - $q1); | |
| 1897 | + $a1 = self::sclip2(($a + 4) >> 3); | |
| 1898 | + $a2 = self::sclip2(($a + 3) >> 3); | |
| 1899 | + $p[$o - $hs] = chr(self::clip($p0 + $a2)); | |
| 1900 | + $p[$o] = chr(self::clip($q0 - $a1)); | |
| 1901 | + } | |
| 1902 | + | |
| 1903 | + private static function sclip1(int $v): int { | |
| 1904 | + return $v < -128 ? -128 : ($v > 127 ? 127 : $v); | |
| 1905 | + } | |
| 1906 | + | |
| 1907 | + private static function sclip2(int $v): int { | |
| 1908 | + return $v < -16 ? -16 : ($v > 15 ? 15 : $v); | |
| 1909 | + } | |
| 1910 | + | |
| 1911 | + private static function clip(int $v): int { | |
| 1912 | + return $v < 0 ? 0 : ($v > 255 ? 255 : $v); | |
| 1913 | + } | |
| 1914 | + | |
| 1915 | + //////// 色 | |
| 1916 | + | |
| 1917 | + /** | |
| 1918 | + * YUV 4:2:0をRGBAにする(色差は近い方の行と列に3:1の重みで補間) | |
| 1919 | + */ | |
| 1920 | + private static function toRgba(string $Y, string $U, string $V, int $yw, int $uw, int $w, int $h): string { | |
| 1921 | + static $tab = null; | |
| 1922 | + if ($tab === null) { | |
| 1923 | + for ($i = 0; $i < 256; $i++) { | |
| 1924 | + $tab['y'][$i] = ($i * 19077) >> 8; | |
| 1925 | + $tab['rv'][$i] = (($i * 26149) >> 8) - 14234; | |
| 1926 | + $tab['gu'][$i] = ($i * 6419) >> 8; | |
| 1927 | + $tab['gv'][$i] = (($i * 13320) >> 8) - 8708; | |
| 1928 | + $tab['bu'][$i] = (($i * 33050) >> 8) - 17685; | |
| 1929 | + } | |
| 1930 | + // 14ビットの固定小数点を0~255にする | |
| 1931 | + for ($i = -20000; $i < 40000; $i++) $tab['clip'][$i] = $i < 0 ? 0 : ($i > 16383 ? 255 : $i >> 6); | |
| 1932 | + } | |
| 1933 | + ['y' => $ty, 'rv' => $trv, 'gu' => $tgu, 'gv' => $tgv, 'bu' => $tbu, 'clip' => $clip] = $tab; | |
| 1934 | + $rows = ($h + 1) >> 1; | |
| 1935 | + $cw = ($w + 1) >> 1; | |
| 1936 | + $last = ($w - 1) >> 1; | |
| 1937 | + $out = ''; | |
| 1938 | + for ($y = 0; $y < $h; $y++) { | |
| 1939 | + if ($y === 0) { | |
| 1940 | + $n = $f = 0; | |
| 1941 | + } else if ($y & 1) { | |
| 1942 | + $n = ($y - 1) >> 1; | |
| 1943 | + $f = min($n + 1, $rows - 1); | |
| 1944 | + } else { | |
| 1945 | + $n = $y >> 1; | |
| 1946 | + $f = $n - 1; | |
| 1947 | + } | |
| 1948 | + $nu = array_values(unpack('C*', substr($U, $n * $uw, $cw))); | |
| 1949 | + $nv = array_values(unpack('C*', substr($V, $n * $uw, $cw))); | |
| 1950 | + $fu = array_values(unpack('C*', substr($U, $f * $uw, $cw))); | |
| 1951 | + $fv = array_values(unpack('C*', substr($V, $f * $uw, $cw))); | |
| 1952 | + $u = [(3 * $nu[0] + $fu[0] + 2) >> 2]; | |
| 1953 | + $v = [(3 * $nv[0] + $fv[0] + 2) >> 2]; | |
| 1954 | + for ($x = 1; $x <= $last; $x++) { | |
| 1955 | + $nl = $nu[$x - 1]; | |
| 1956 | + $nr = $nu[$x]; | |
| 1957 | + $fl = $fu[$x - 1]; | |
| 1958 | + $fr = $fu[$x]; | |
| 1959 | + $avg = $nl + $nr + $fl + $fr + 8; | |
| 1960 | + $u[2 * $x - 1] = ((($avg + 2 * ($nr + $fl)) >> 3) + $nl) >> 1; | |
| 1961 | + $u[2 * $x] = ((($avg + 2 * ($nl + $fr)) >> 3) + $nr) >> 1; | |
| 1962 | + $nl = $nv[$x - 1]; | |
| 1963 | + $nr = $nv[$x]; | |
| 1964 | + $fl = $fv[$x - 1]; | |
| 1965 | + $fr = $fv[$x]; | |
| 1966 | + $avg = $nl + $nr + $fl + $fr + 8; | |
| 1967 | + $v[2 * $x - 1] = ((($avg + 2 * ($nr + $fl)) >> 3) + $nl) >> 1; | |
| 1968 | + $v[2 * $x] = ((($avg + 2 * ($nl + $fr)) >> 3) + $nr) >> 1; | |
| 1969 | + } | |
| 1970 | + if (!($w & 1)) { | |
| 1971 | + $u[$w - 1] = (3 * $nu[$last] + $fu[$last] + 2) >> 2; | |
| 1972 | + $v[$w - 1] = (3 * $nv[$last] + $fv[$last] + 2) >> 2; | |
| 1973 | + } | |
| 1974 | + $row = []; | |
| 1975 | + $yrow = unpack('C*', substr($Y, $y * $yw, $w)); | |
| 1976 | + for ($x = 0; $x < $w; $x++) { | |
| 1977 | + $l = $ty[$yrow[$x + 1]]; | |
| 1978 | + $uu = $u[$x]; | |
| 1979 | + $vv = $v[$x]; | |
| 1980 | + $row[] = $clip[$l + $trv[$vv]]; | |
| 1981 | + $row[] = $clip[$l - $tgu[$uu] - $tgv[$vv]]; | |
| 1982 | + $row[] = $clip[$l + $tbu[$uu]]; | |
| 1983 | + $row[] = 255; | |
| 1984 | + } | |
| 1985 | + $out .= pack('C*', ...$row); | |
| 1986 | + } | |
| 1987 | + return $out; | |
| 1988 | + } | |
| 1989 | +} |
| @@ -0,0 +1,1035 @@ | ||
| 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\Image; | |
| 39 | + | |
| 40 | +/** | |
| 41 | + * WebPの可逆圧縮(VP8L) | |
| 42 | + * | |
| 43 | + * 読み込み:全ての変換(予測、色、緑を引く、パレット)、カラーキャッシュ、メタプレフィックス符号、後方参照。 | |
| 44 | + * 書き出し:256色以下はパレット(少なければ画素を詰める)、それ以外は緑を引く+予測、LZ77とカラーキャッシュ、ハフマン符号。 | |
| 45 | + * 画素は内部ではARGBの整数(又はその4バイトの列、ビッグエンディアン)。 | |
| 46 | + */ | |
| 47 | +final class Vp8l { | |
| 48 | + private const ORDER = [17, 18, 0, 1, 2, 3, 4, 5, 16, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]; | |
| 49 | + /** 距離の符号1~120の(x, y)のずれ */ | |
| 50 | + private const PLANE = [ | |
| 51 | + [0, 1], [1, 0], [1, 1], [-1, 1], [0, 2], [2, 0], [1, 2], [-1, 2], [2, 1], [-2, 1], [2, 2], [-2, 2], [0, 3], [3, 0], [1, 3], [-1, 3], | |
| 52 | + [3, 1], [-3, 1], [2, 3], [-2, 3], [3, 2], [-3, 2], [0, 4], [4, 0], [1, 4], [-1, 4], [4, 1], [-4, 1], [3, 3], [-3, 3], [2, 4], [-2, 4], | |
| 53 | + [4, 2], [-4, 2], [0, 5], [3, 4], [-3, 4], [4, 3], [-4, 3], [5, 0], [1, 5], [-1, 5], [5, 1], [-5, 1], [2, 5], [-2, 5], [5, 2], [-5, 2], | |
| 54 | + [4, 4], [-4, 4], [3, 5], [-3, 5], [5, 3], [-5, 3], [0, 6], [6, 0], [1, 6], [-1, 6], [6, 1], [-6, 1], [2, 6], [-2, 6], [6, 2], [-6, 2], | |
| 55 | + [4, 5], [-4, 5], [5, 4], [-5, 4], [3, 6], [-3, 6], [6, 3], [-6, 3], [0, 7], [7, 0], [1, 7], [-1, 7], [5, 5], [-5, 5], [7, 1], [-7, 1], | |
| 56 | + [4, 6], [-4, 6], [6, 4], [-6, 4], [2, 7], [-2, 7], [7, 2], [-7, 2], [3, 7], [-3, 7], [7, 3], [-7, 3], [5, 6], [-5, 6], [6, 5], [-6, 5], | |
| 57 | + [8, 0], [4, 7], [-4, 7], [7, 4], [-7, 4], [8, 1], [8, 2], [6, 6], [-6, 6], [8, 3], [5, 7], [-5, 7], [7, 5], [-7, 5], [8, 4], [6, 7], | |
| 58 | + [-6, 7], [7, 6], [-7, 6], [8, 5], [7, 7], [-7, 7], [8, 6], [8, 7], | |
| 59 | + ]; | |
| 60 | + private const HASH = 0x1e35a7bd; | |
| 61 | + private const MAX_LENGTH = 4096; | |
| 62 | + private const MAX_DISTANCE = 1048576; | |
| 63 | + | |
| 64 | + private string $d = ''; | |
| 65 | + private int $p = 0; | |
| 66 | + private int $buf = 0; | |
| 67 | + private int $bits = 0; | |
| 68 | + | |
| 69 | + private string $out = ''; | |
| 70 | + private int $acc = 0; | |
| 71 | + private int $nb = 0; | |
| 72 | + | |
| 73 | + //////// 読み込み | |
| 74 | + | |
| 75 | + /** | |
| 76 | + * VP8Lチャンクの中身を読む | |
| 77 | + * | |
| 78 | + * @return array [幅, 丈, ARGBの列] | |
| 79 | + */ | |
| 80 | + public function decode(string $data): array { | |
| 81 | + if (strlen($data) < 5 || $data[0] !== "\x2f") throw new \RuntimeException('VP8Lの印がありません。'); | |
| 82 | + $this->start($data, 1); | |
| 83 | + $w = $this->read(14) + 1; | |
| 84 | + $h = $this->read(14) + 1; | |
| 85 | + $this->read(1); // アルファを使うか(参考だけ) | |
| 86 | + if ($this->read(3) !== 0) throw new \RuntimeException('未対応のVP8Lの版です。'); | |
| 87 | + Bitmap::check($w, $h); | |
| 88 | + return [$w, $h, $this->imageStream($w, $h)]; | |
| 89 | + } | |
| 90 | + | |
| 91 | + /** | |
| 92 | + * ヘッダーの無い画像(ALPHチャンクの中身)を読む | |
| 93 | + */ | |
| 94 | + public function decodeImage(string $data, int $w, int $h): string { | |
| 95 | + $this->start($data, 0); | |
| 96 | + return $this->imageStream($w, $h); | |
| 97 | + } | |
| 98 | + | |
| 99 | + private function start(string $data, int $pos): void { | |
| 100 | + $this->d = $data; | |
| 101 | + $this->p = $pos; | |
| 102 | + $this->buf = $this->bits = 0; | |
| 103 | + } | |
| 104 | + | |
| 105 | + private function read(int $n): int { | |
| 106 | + while ($this->bits < $n) { | |
| 107 | + $this->buf |= ord($this->d[$this->p++] ?? "\0") << $this->bits; | |
| 108 | + $this->bits += 8; | |
| 109 | + } | |
| 110 | + $v = $this->buf & ((1 << $n) - 1); | |
| 111 | + $this->buf >>= $n; | |
| 112 | + $this->bits -= $n; | |
| 113 | + return $v; | |
| 114 | + } | |
| 115 | + | |
| 116 | + private function imageStream(int $w, int $h): string { | |
| 117 | + $transforms = []; | |
| 118 | + $xsize = $w; | |
| 119 | + while ($this->read(1)) { | |
| 120 | + $type = $this->read(2); | |
| 121 | + if (isset($transforms[$type])) throw new \RuntimeException('VP8Lの変換が重複しています。'); | |
| 122 | + $t = ['type' => $type, 'w' => $xsize]; | |
| 123 | + if ($type === 0 || $type === 1) { | |
| 124 | + $t['bits'] = $this->read(3) + 2; | |
| 125 | + $t['bw'] = self::subSize($xsize, $t['bits']); | |
| 126 | + $t['data'] = $this->subImage($t['bw'], self::subSize($h, $t['bits'])); | |
| 127 | + } else if ($type === 3) { | |
| 128 | + $n = $this->read(8) + 1; | |
| 129 | + $pal = $this->subImage($n, 1); | |
| 130 | + for ($i = 1; $i < $n; $i++) $pal[$i] = self::add($pal[$i], $pal[$i - 1]); | |
| 131 | + // 範囲外の番号は透明な黒 | |
| 132 | + $t['palette'] = array_pad(array_map(fn($c) => pack('N', $c), $pal), 256, "\0\0\0\0"); | |
| 133 | + $t['bits'] = $n <= 2 ? 3 : ($n <= 4 ? 2 : ($n <= 16 ? 1 : 0)); | |
| 134 | + $xsize = self::subSize($xsize, $t['bits']); | |
| 135 | + } | |
| 136 | + $transforms[$type] = $t; | |
| 137 | + } | |
| 138 | + $px = $this->entropyImage($xsize, $h, true); | |
| 139 | + foreach (array_reverse($transforms, true) as $type => $t) { | |
| 140 | + $px = match ($type) { | |
| 141 | + 0 => self::inversePredictor($px, $t, $h), | |
| 142 | + 1 => self::inverseColor($px, $t, $h), | |
| 143 | + 2 => self::addGreen($px, $t['w'], $h), | |
| 144 | + 3 => self::inversePalette($px, $t, $h, $xsize), | |
| 145 | + }; | |
| 146 | + if ($type === 3) $xsize = $t['w']; | |
| 147 | + } | |
| 148 | + return $px; | |
| 149 | + } | |
| 150 | + | |
| 151 | + private static function subSize(int $size, int $bits): int { | |
| 152 | + return ($size + (1 << $bits) - 1) >> $bits; | |
| 153 | + } | |
| 154 | + | |
| 155 | + /** 変換用の小さい画像(ARGBの整数の配列) */ | |
| 156 | + private function subImage(int $w, int $h): array { | |
| 157 | + return array_values(unpack('N*', $this->entropyImage($w, $h, false))); | |
| 158 | + } | |
| 159 | + | |
| 160 | + /** | |
| 161 | + * エントロピー符号化した画像 | |
| 162 | + * | |
| 163 | + * @param bool $main 主画像(メタプレフィックス符号を使える) | |
| 164 | + */ | |
| 165 | + private function entropyImage(int $w, int $h, bool $main): string { | |
| 166 | + $cacheBits = 0; | |
| 167 | + if ($this->read(1)) { | |
| 168 | + $cacheBits = $this->read(4); | |
| 169 | + if ($cacheBits < 1 || $cacheBits > 11) throw new \RuntimeException('VP8Lのカラーキャッシュが不正です。'); | |
| 170 | + } | |
| 171 | + $meta = null; | |
| 172 | + $metaBits = 0; | |
| 173 | + $metaW = 0; | |
| 174 | + $groups = 1; | |
| 175 | + if ($main && $this->read(1)) { | |
| 176 | + $metaBits = $this->read(3) + 2; | |
| 177 | + $metaW = self::subSize($w, $metaBits); | |
| 178 | + $meta = $this->subImage($metaW, self::subSize($h, $metaBits)); | |
| 179 | + foreach ($meta as $i => $v) $meta[$i] = ($v >> 8) & 0xffff; | |
| 180 | + $groups = max($meta) + 1; | |
| 181 | + } | |
| 182 | + $cacheSize = $cacheBits ? 1 << $cacheBits : 0; | |
| 183 | + $codes = []; | |
| 184 | + for ($g = 0; $g < $groups; $g++) { | |
| 185 | + $codes[] = [$this->readCode(280 + $cacheSize), $this->readCode(256), $this->readCode(256), $this->readCode(256), $this->readCode(40)]; | |
| 186 | + } | |
| 187 | + | |
| 188 | + $total = $w * $h; | |
| 189 | + $px = ''; | |
| 190 | + $cache = $cacheSize ? array_fill(0, $cacheSize, 0) : null; | |
| 191 | + $shift = 32 - $cacheBits; | |
| 192 | + $pos = $x = $y = 0; | |
| 193 | + $mask = (1 << $metaBits) - 1; | |
| 194 | + [$G, $R, $B, $A, $D] = $codes[0]; | |
| 195 | + while ($pos < $total) { | |
| 196 | + if ($meta !== null && ($x & $mask) === 0) [$G, $R, $B, $A, $D] = $codes[$meta[($y >> $metaBits) * $metaW + ($x >> $metaBits)]]; | |
| 197 | + $s = $this->sym($G); | |
| 198 | + if ($s < 256) { | |
| 199 | + $r = $this->sym($R); | |
| 200 | + $b = $this->sym($B); | |
| 201 | + $v = ($this->sym($A) << 24) | ($r << 16) | ($s << 8) | $b; | |
| 202 | + $px .= pack('N', $v); | |
| 203 | + if ($cache !== null) $cache[(($v * self::HASH) & 0xffffffff) >> $shift] = $v; | |
| 204 | + $pos++; | |
| 205 | + if (++$x === $w) { | |
| 206 | + $x = 0; | |
| 207 | + $y++; | |
| 208 | + // 壊れた(短い)ファイルで長く掛からない様に | |
| 209 | + if ($this->p > strlen($this->d) + 8) throw new \RuntimeException('VP8Lのデータが途中で切れています。'); | |
| 210 | + } | |
| 211 | + continue; | |
| 212 | + } | |
| 213 | + if ($s < 280) { | |
| 214 | + $len = $this->prefixValue($s - 256); | |
| 215 | + $dcode = $this->prefixValue($this->sym($D)); | |
| 216 | + if ($dcode > 120) { | |
| 217 | + $dist = $dcode - 120; | |
| 218 | + } else { | |
| 219 | + [$xi, $yi] = self::PLANE[$dcode - 1]; | |
| 220 | + $dist = max(1, $xi + $yi * $w); | |
| 221 | + } | |
| 222 | + if ($dist > $pos || $pos + $len > $total) throw new \RuntimeException('VP8Lの後方参照が不正です。'); | |
| 223 | + $chunk = substr($px, ($pos - $dist) * 4, min($dist, $len) * 4); | |
| 224 | + if ($dist < $len) $chunk = substr(str_repeat($chunk, intdiv($len, $dist) + 1), 0, $len * 4); | |
| 225 | + $px .= $chunk; | |
| 226 | + if ($cache !== null) foreach (unpack('N*', $chunk) as $v) $cache[(($v * self::HASH) & 0xffffffff) >> $shift] = $v; | |
| 227 | + $pos += $len; | |
| 228 | + $x += $len; | |
| 229 | + if ($x >= $w) { | |
| 230 | + $y += intdiv($x, $w); | |
| 231 | + $x %= $w; | |
| 232 | + if ($this->p > strlen($this->d) + 8) throw new \RuntimeException('VP8Lのデータが途中で切れています。'); | |
| 233 | + } | |
| 234 | + if ($meta !== null && $pos < $total) [$G, $R, $B, $A, $D] = $codes[$meta[($y >> $metaBits) * $metaW + ($x >> $metaBits)]]; | |
| 235 | + continue; | |
| 236 | + } | |
| 237 | + if ($cache === null) throw new \RuntimeException('VP8Lのカラーキャッシュがありません。'); | |
| 238 | + $v = $cache[$s - 280]; | |
| 239 | + $px .= pack('N', $v); | |
| 240 | + $cache[(($v * self::HASH) & 0xffffffff) >> $shift] = $v; | |
| 241 | + $pos++; | |
| 242 | + if (++$x === $w) { | |
| 243 | + $x = 0; | |
| 244 | + $y++; | |
| 245 | + } | |
| 246 | + } | |
| 247 | + if ($this->p - ($this->bits >> 3) > strlen($this->d)) throw new \RuntimeException('VP8Lのデータが途中で切れています。'); | |
| 248 | + return $px; | |
| 249 | + } | |
| 250 | + | |
| 251 | + private function prefixValue(int $prefix): int { | |
| 252 | + if ($prefix < 4) return $prefix + 1; | |
| 253 | + $extra = ($prefix - 2) >> 1; | |
| 254 | + return ((2 + ($prefix & 1)) << $extra) + $this->read($extra) + 1; | |
| 255 | + } | |
| 256 | + | |
| 257 | + /** プレフィックス符号(ハフマン符号)を読む */ | |
| 258 | + private function readCode(int $alphabet): array { | |
| 259 | + $lengths = array_fill(0, $alphabet, 0); | |
| 260 | + if ($this->read(1)) { | |
| 261 | + // 簡単な符号:1か2個の記号 | |
| 262 | + $two = $this->read(1); | |
| 263 | + $s = $this->read($this->read(1) ? 8 : 1); | |
| 264 | + if ($s >= $alphabet) throw new \RuntimeException('VP8Lの符号が不正です。'); | |
| 265 | + $lengths[$s] = 1; | |
| 266 | + if ($two) { | |
| 267 | + $s = $this->read(8); | |
| 268 | + if ($s >= $alphabet) throw new \RuntimeException('VP8Lの符号が不正です。'); | |
| 269 | + $lengths[$s] = 1; | |
| 270 | + } | |
| 271 | + return self::buildCode($lengths); | |
| 272 | + } | |
| 273 | + $clen = array_fill(0, 19, 0); | |
| 274 | + $n = 4 + $this->read(4); | |
| 275 | + for ($i = 0; $i < $n; $i++) $clen[self::ORDER[$i]] = $this->read(3); | |
| 276 | + $cl = self::buildCode($clen); | |
| 277 | + $max = $alphabet; | |
| 278 | + if ($this->read(1)) { | |
| 279 | + $max = 2 + $this->read(2 + 2 * $this->read(3)); | |
| 280 | + if ($max > $alphabet) throw new \RuntimeException('VP8Lの符号長が多過ぎます。'); | |
| 281 | + } | |
| 282 | + $prev = 8; | |
| 283 | + $sym = 0; | |
| 284 | + while ($sym < $alphabet && $max-- > 0) { | |
| 285 | + $c = $this->sym($cl); | |
| 286 | + if ($c < 16) { | |
| 287 | + $lengths[$sym++] = $c; | |
| 288 | + if ($c !== 0) $prev = $c; | |
| 289 | + continue; | |
| 290 | + } | |
| 291 | + [$rep, $val] = match ($c) {16 => [3 + $this->read(2), $prev], 17 => [3 + $this->read(3), 0], default => [11 + $this->read(7), 0]}; | |
| 292 | + if ($sym + $rep > $alphabet) throw new \RuntimeException('VP8Lの符号長が不正です。'); | |
| 293 | + for ($i = 0; $i < $rep; $i++) $lengths[$sym++] = $val; | |
| 294 | + } | |
| 295 | + return self::buildCode($lengths); | |
| 296 | + } | |
| 297 | + | |
| 298 | + /** | |
| 299 | + * 符号長から読む為の表を作る(下位ビットから読むので、符号を反転して引く) | |
| 300 | + * | |
| 301 | + * @return array ['single' => 記号](0ビット)、又は[根の表, 長い符号の表] | |
| 302 | + */ | |
| 303 | + private static function buildCode(array $lengths): array { | |
| 304 | + $count = array_fill(0, 16, 0); | |
| 305 | + $one = 0; | |
| 306 | + $used = 0; | |
| 307 | + foreach ($lengths as $s => $l) { | |
| 308 | + if ($l === 0) continue; | |
| 309 | + $count[$l]++; | |
| 310 | + $one = $s; | |
| 311 | + $used++; | |
| 312 | + } | |
| 313 | + if ($used <= 1) return ['single' => $one]; | |
| 314 | + $kraft = 0; | |
| 315 | + for ($l = 1; $l < 16; $l++) $kraft += $count[$l] << (15 - $l); | |
| 316 | + if ($kraft !== 1 << 15) throw new \RuntimeException('VP8Lのハフマン符号が不完全です。'); | |
| 317 | + $next = []; | |
| 318 | + $code = 0; | |
| 319 | + for ($l = 1; $l < 16; $l++) { | |
| 320 | + $code = ($code + $count[$l - 1]) << 1; | |
| 321 | + $next[$l] = $code; | |
| 322 | + } | |
| 323 | + $prim = 8; | |
| 324 | + $root = array_fill(0, 1 << $prim, 0); | |
| 325 | + $codes = []; | |
| 326 | + $subBits = []; | |
| 327 | + foreach ($lengths as $s => $l) { | |
| 328 | + if ($l === 0) continue; | |
| 329 | + $c = $next[$l]++; | |
| 330 | + $r = 0; | |
| 331 | + for ($i = 0; $i < $l; $i++) $r |= (($c >> $i) & 1) << ($l - 1 - $i); | |
| 332 | + $codes[] = [$s, $l, $r]; | |
| 333 | + if ($l > $prim) { | |
| 334 | + $k = $r & 0xff; | |
| 335 | + $subBits[$k] = max($subBits[$k] ?? 0, $l - $prim); | |
| 336 | + } | |
| 337 | + } | |
| 338 | + $long = []; | |
| 339 | + foreach ($subBits as $k => $sb) { | |
| 340 | + $long[$k] = [$sb, array_fill(0, 1 << $sb, 0)]; | |
| 341 | + $root[$k] = -1; | |
| 342 | + } | |
| 343 | + foreach ($codes as [$s, $l, $r]) { | |
| 344 | + if ($l <= $prim) { | |
| 345 | + for ($j = $r; $j < 256; $j += 1 << $l) $root[$j] = ($l << 16) | $s; | |
| 346 | + } else { | |
| 347 | + $k = $r & 0xff; | |
| 348 | + $sb = $long[$k][0]; | |
| 349 | + for ($j = $r >> $prim; $j < (1 << $sb); $j += 1 << ($l - $prim)) $long[$k][1][$j] = ($l << 16) | $s; | |
| 350 | + } | |
| 351 | + } | |
| 352 | + return [$root, $long]; | |
| 353 | + } | |
| 354 | + | |
| 355 | + private function sym(array $c): int { | |
| 356 | + if (isset($c['single'])) return $c['single']; | |
| 357 | + if ($this->bits < 15) { | |
| 358 | + do { | |
| 359 | + $this->buf |= ord($this->d[$this->p++] ?? "\0") << $this->bits; | |
| 360 | + $this->bits += 8; | |
| 361 | + } while ($this->bits <= 48); | |
| 362 | + } | |
| 363 | + $e = $c[0][$this->buf & 0xff]; | |
| 364 | + if ($e < 0) { | |
| 365 | + [$sb, $sub] = $c[1][$this->buf & 0xff]; | |
| 366 | + $e = $sub[($this->buf >> 8) & ((1 << $sb) - 1)]; | |
| 367 | + } | |
| 368 | + $l = $e >> 16; | |
| 369 | + $this->buf >>= $l; | |
| 370 | + $this->bits -= $l; | |
| 371 | + return $e & 0xffff; | |
| 372 | + } | |
| 373 | + | |
| 374 | + //////// 逆変換 | |
| 375 | + | |
| 376 | + /** 各成分を256で割った余りで足す */ | |
| 377 | + private static function add(int $a, int $b): int { | |
| 378 | + return ((($a & 0xff00ff00) + ($b & 0xff00ff00)) & 0xff00ff00) | ((($a & 0x00ff00ff) + ($b & 0x00ff00ff)) & 0x00ff00ff); | |
| 379 | + } | |
| 380 | + | |
| 381 | + private static function sub(int $a, int $b): int { | |
| 382 | + return ((($a | 0x00ff00ff) - ($b & 0xff00ff00)) & 0xff00ff00) | ((($a | 0xff00ff00) - ($b & 0x00ff00ff)) & 0x00ff00ff); | |
| 383 | + } | |
| 384 | + | |
| 385 | + private static function avg(int $a, int $b): int { | |
| 386 | + return ((($a ^ $b) & 0xfefefefe) >> 1) + ($a & $b); | |
| 387 | + } | |
| 388 | + | |
| 389 | + /** | |
| 390 | + * 予測値(モード0~13) | |
| 391 | + */ | |
| 392 | + private static function predict(int $mode, int $L, int $T, int $TR, int $TL): int { | |
| 393 | + return match ($mode) { | |
| 394 | + 1 => $L, | |
| 395 | + 2 => $T, | |
| 396 | + 3 => $TR, | |
| 397 | + 4 => $TL, | |
| 398 | + 5 => self::avg(self::avg($L, $TR), $T), | |
| 399 | + 6 => self::avg($L, $TL), | |
| 400 | + 7 => self::avg($L, $T), | |
| 401 | + 8 => self::avg($TL, $T), | |
| 402 | + 9 => self::avg($T, $TR), | |
| 403 | + 10 => self::avg(self::avg($L, $TL), self::avg($T, $TR)), | |
| 404 | + 11 => self::select($L, $T, $TL), | |
| 405 | + 12 => self::clampAdd($L, $T, $TL), | |
| 406 | + 13 => self::clampHalf(self::avg($L, $T), $TL), | |
| 407 | + default => 0xff000000, | |
| 408 | + }; | |
| 409 | + } | |
| 410 | + | |
| 411 | + private static function select(int $L, int $T, int $TL): int { | |
| 412 | + // 左なら|T - TL|、上なら|L - TL|が予測との距離 | |
| 413 | + $pl = $pt = 0; | |
| 414 | + for ($s = 0; $s < 32; $s += 8) { | |
| 415 | + $tl = ($TL >> $s) & 255; | |
| 416 | + $pl += abs((($T >> $s) & 255) - $tl); | |
| 417 | + $pt += abs((($L >> $s) & 255) - $tl); | |
| 418 | + } | |
| 419 | + return $pl < $pt ? $L : $T; | |
| 420 | + } | |
| 421 | + | |
| 422 | + private static function clampAdd(int $a, int $b, int $c): int { | |
| 423 | + $out = 0; | |
| 424 | + for ($s = 0; $s < 32; $s += 8) { | |
| 425 | + $v = (($a >> $s) & 255) + (($b >> $s) & 255) - (($c >> $s) & 255); | |
| 426 | + $out |= ($v < 0 ? 0 : ($v > 255 ? 255 : $v)) << $s; | |
| 427 | + } | |
| 428 | + return $out; | |
| 429 | + } | |
| 430 | + | |
| 431 | + private static function clampHalf(int $a, int $b): int { | |
| 432 | + $out = 0; | |
| 433 | + for ($s = 0; $s < 32; $s += 8) { | |
| 434 | + $av = ($a >> $s) & 255; | |
| 435 | + $v = $av + intdiv($av - (($b >> $s) & 255), 2); | |
| 436 | + $out |= ($v < 0 ? 0 : ($v > 255 ? 255 : $v)) << $s; | |
| 437 | + } | |
| 438 | + return $out; | |
| 439 | + } | |
| 440 | + | |
| 441 | + private static function inversePredictor(string $px, array $t, int $h): string { | |
| 442 | + $w = $t['w']; | |
| 443 | + $bits = $t['bits']; | |
| 444 | + $bw = $t['bw']; | |
| 445 | + $modes = $t['data']; | |
| 446 | + $out = ''; | |
| 447 | + $prev = []; | |
| 448 | + for ($y = 0; $y < $h; $y++) { | |
| 449 | + $res = unpack('N*', substr($px, $y * $w * 4, $w * 4)); | |
| 450 | + // 左上は黒、1行目は左、左の列は上 | |
| 451 | + $cur = [self::add($res[1], $y === 0 ? 0xff000000 : $prev[0])]; | |
| 452 | + if ($y === 0) { | |
| 453 | + for ($x = 1; $x < $w; $x++) $cur[$x] = self::add($res[$x + 1], $cur[$x - 1]); | |
| 454 | + } else { | |
| 455 | + $mrow = ($y >> $bits) * $bw; | |
| 456 | + for ($x = 1; $x < $w; $x++) { | |
| 457 | + // 右端の右上は、この行の左端 | |
| 458 | + $pred = self::predict(($modes[$mrow + ($x >> $bits)] >> 8) & 15, $cur[$x - 1], $prev[$x], $x + 1 < $w ? $prev[$x + 1] : $cur[0], $prev[$x - 1]); | |
| 459 | + $cur[$x] = self::add($res[$x + 1], $pred); | |
| 460 | + } | |
| 461 | + } | |
| 462 | + $out .= pack('N*', ...$cur); | |
| 463 | + $prev = $cur; | |
| 464 | + } | |
| 465 | + return $out; | |
| 466 | + } | |
| 467 | + | |
| 468 | + private static function inverseColor(string $px, array $t, int $h): string { | |
| 469 | + $w = $t['w']; | |
| 470 | + $bits = $t['bits']; | |
| 471 | + $bw = $t['bw']; | |
| 472 | + $out = ''; | |
| 473 | + $s8 = fn(int $v): int => $v >= 128 ? $v - 256 : $v; | |
| 474 | + for ($y = 0; $y < $h; $y++) { | |
| 475 | + $row = unpack('N*', substr($px, $y * $w * 4, $w * 4)); | |
| 476 | + $mrow = ($y >> $bits) * $bw; | |
| 477 | + for ($x = 0; $x < $w; $x++) { | |
| 478 | + $e = $t['data'][$mrow + ($x >> $bits)]; | |
| 479 | + $p = $row[$x + 1]; | |
| 480 | + $g = $s8(($p >> 8) & 255); | |
| 481 | + $r = ((($p >> 16) & 255) + (($s8($e & 255) * $g) >> 5)) & 255; | |
| 482 | + $b = (($p & 255) + (($s8(($e >> 8) & 255) * $g) >> 5) + (($s8(($e >> 16) & 255) * $s8($r)) >> 5)) & 255; | |
| 483 | + $row[$x + 1] = ($p & 0xff00ff00) | ($r << 16) | $b; | |
| 484 | + } | |
| 485 | + $out .= pack('N*', ...$row); | |
| 486 | + } | |
| 487 | + return $out; | |
| 488 | + } | |
| 489 | + | |
| 490 | + private static function addGreen(string $px, int $w, int $h): string { | |
| 491 | + $out = ''; | |
| 492 | + for ($y = 0; $y < $h; $y++) { | |
| 493 | + $row = unpack('N*', substr($px, $y * $w * 4, $w * 4)); | |
| 494 | + foreach ($row as $i => $p) { | |
| 495 | + $g = ($p >> 8) & 255; | |
| 496 | + $row[$i] = ($p & 0xff00ff00) | (((($p >> 16) + $g) & 255) << 16) | ((($p & 255) + $g) & 255); | |
| 497 | + } | |
| 498 | + $out .= pack('N*', ...$row); | |
| 499 | + } | |
| 500 | + return $out; | |
| 501 | + } | |
| 502 | + | |
| 503 | + private static function inversePalette(string $px, array $t, int $h, int $pw): string { | |
| 504 | + $pal = $t['palette']; | |
| 505 | + $bits = $t['bits']; | |
| 506 | + $w = $t['w']; | |
| 507 | + $out = ''; | |
| 508 | + if ($bits === 0) { | |
| 509 | + for ($i = 2, $n = strlen($px); $i < $n; $i += 4) $out .= $pal[ord($px[$i])]; | |
| 510 | + return $out; | |
| 511 | + } | |
| 512 | + $ib = 8 >> $bits; | |
| 513 | + $mask = (1 << $ib) - 1; | |
| 514 | + $per = (1 << $bits) - 1; | |
| 515 | + for ($y = 0; $y < $h; $y++) { | |
| 516 | + $o = $y * $pw * 4 + 2; | |
| 517 | + $g = 0; | |
| 518 | + for ($x = 0; $x < $w; $x++) { | |
| 519 | + if (($x & $per) === 0) $g = ord($px[$o + ($x >> $bits) * 4]); | |
| 520 | + $out .= $pal[$g & $mask]; | |
| 521 | + $g >>= $ib; | |
| 522 | + } | |
| 523 | + } | |
| 524 | + return $out; | |
| 525 | + } | |
| 526 | + | |
| 527 | + //////// 書き出し | |
| 528 | + | |
| 529 | + /** | |
| 530 | + * VP8Lチャンクの中身を作る | |
| 531 | + * | |
| 532 | + * @param array $px ARGBの整数の配列(左上から行の順) | |
| 533 | + */ | |
| 534 | + public function encode(array $px, int $w, int $h, bool $alpha): string { | |
| 535 | + $this->out = ''; | |
| 536 | + $this->acc = $this->nb = 0; | |
| 537 | + $this->put(0x2f, 8); | |
| 538 | + $this->put($w - 1, 14); | |
| 539 | + $this->put($h - 1, 14); | |
| 540 | + $this->put($alpha ? 1 : 0, 1); | |
| 541 | + $this->put(0, 3); | |
| 542 | + $this->stream($px, $w, $h); | |
| 543 | + return $this->flush(); | |
| 544 | + } | |
| 545 | + | |
| 546 | + /** | |
| 547 | + * ヘッダーの無い画像(ALPHチャンク用) | |
| 548 | + */ | |
| 549 | + public function encodeImage(array $px, int $w, int $h): string { | |
| 550 | + $this->out = ''; | |
| 551 | + $this->acc = $this->nb = 0; | |
| 552 | + $this->stream($px, $w, $h); | |
| 553 | + return $this->flush(); | |
| 554 | + } | |
| 555 | + | |
| 556 | + private function put(int $v, int $n): void { | |
| 557 | + $this->acc |= $v << $this->nb; | |
| 558 | + $this->nb += $n; | |
| 559 | + while ($this->nb >= 8) { | |
| 560 | + $this->out .= chr($this->acc & 255); | |
| 561 | + $this->acc >>= 8; | |
| 562 | + $this->nb -= 8; | |
| 563 | + } | |
| 564 | + } | |
| 565 | + | |
| 566 | + private function flush(): string { | |
| 567 | + if ($this->nb > 0) $this->out .= chr($this->acc & 255); | |
| 568 | + $this->acc = $this->nb = 0; | |
| 569 | + return $this->out; | |
| 570 | + } | |
| 571 | + | |
| 572 | + private function stream(array $px, int $w, int $h): void { | |
| 573 | + $colors = []; | |
| 574 | + foreach ($px as $c) { | |
| 575 | + $colors[$c] = true; | |
| 576 | + if (count($colors) > 256) break; | |
| 577 | + } | |
| 578 | + if (count($colors) <= 256) { | |
| 579 | + // パレット(色の番号を緑に入れ、少なければ1バイトに詰める) | |
| 580 | + $pal = array_keys($colors); | |
| 581 | + sort($pal); | |
| 582 | + $n = count($pal); | |
| 583 | + $index = array_flip($pal); | |
| 584 | + $bits = $n <= 2 ? 3 : ($n <= 4 ? 2 : ($n <= 16 ? 1 : 0)); | |
| 585 | + $this->put(1, 1); | |
| 586 | + $this->put(3, 2); | |
| 587 | + $this->put($n - 1, 8); | |
| 588 | + $delta = [$pal[0]]; | |
| 589 | + for ($i = 1; $i < $n; $i++) $delta[] = self::sub($pal[$i], $pal[$i - 1]); | |
| 590 | + $this->entropy($delta, $n, 1, false); | |
| 591 | + $pw = self::subSize($w, $bits); | |
| 592 | + $per = 1 << $bits; | |
| 593 | + $ib = 8 >> $bits; | |
| 594 | + $packed = []; | |
| 595 | + for ($y = 0; $y < $h; $y++) { | |
| 596 | + $row = $y * $w; | |
| 597 | + for ($xb = 0; $xb < $pw; $xb++) { | |
| 598 | + $g = 0; | |
| 599 | + for ($k = 0, $x = $xb * $per; $k < $per && $x < $w; $k++, $x++) $g |= $index[$px[$row + $x]] << ($k * $ib); | |
| 600 | + $packed[] = $g << 8; | |
| 601 | + } | |
| 602 | + } | |
| 603 | + $px = $packed; | |
| 604 | + $w = $pw; | |
| 605 | + } else { | |
| 606 | + // 緑を引いてから予測 | |
| 607 | + $this->put(1, 1); | |
| 608 | + $this->put(2, 2); | |
| 609 | + foreach ($px as $i => $c) { | |
| 610 | + $g = ($c >> 8) & 255; | |
| 611 | + $px[$i] = ($c & 0xff00ff00) | (((($c >> 16) - $g) & 255) << 16) | ((($c & 255) - $g) & 255); | |
| 612 | + } | |
| 613 | + $bits = $w * $h > 65536 ? 5 : 4; | |
| 614 | + [$modes, $px] = self::predictor($px, $w, $h, $bits); | |
| 615 | + $this->put(1, 1); | |
| 616 | + $this->put(0, 2); | |
| 617 | + $this->put($bits - 2, 3); | |
| 618 | + $this->entropy($modes, self::subSize($w, $bits), self::subSize($h, $bits), false); | |
| 619 | + } | |
| 620 | + $this->put(0, 1); | |
| 621 | + $this->entropy($px, $w, $h, true); | |
| 622 | + } | |
| 623 | + | |
| 624 | + /** | |
| 625 | + * 予測の変換(ブロック毎に残差の小さいモードを選ぶ) | |
| 626 | + * | |
| 627 | + * @return array [モードの画像, 残差] | |
| 628 | + */ | |
| 629 | + private static function predictor(array $px, int $w, int $h, int $bits): array { | |
| 630 | + $bw = self::subSize($w, $bits); | |
| 631 | + $bh = self::subSize($h, $bits); | |
| 632 | + $size = 1 << $bits; | |
| 633 | + $modes = []; | |
| 634 | + $res = $px; | |
| 635 | + $cost = static function(int $d): int { | |
| 636 | + $c = 0; | |
| 637 | + for ($s = 0; $s < 32; $s += 8) { | |
| 638 | + $v = ($d >> $s) & 255; | |
| 639 | + $c += $v < 128 ? $v : 256 - $v; | |
| 640 | + } | |
| 641 | + return $c; | |
| 642 | + }; | |
| 643 | + for ($by = 0; $by < $bh; $by++) { | |
| 644 | + for ($bx = 0; $bx < $bw; $bx++) { | |
| 645 | + $x0 = $bx * $size; | |
| 646 | + $y0 = $by * $size; | |
| 647 | + $x1 = min($w, $x0 + $size); | |
| 648 | + $y1 = min($h, $y0 + $size); | |
| 649 | + $best = 1; | |
| 650 | + $bestCost = PHP_INT_MAX; | |
| 651 | + foreach ([1, 2, 11, 12, 7, 13, 3, 4] as $m) { | |
| 652 | + $c = 0; | |
| 653 | + for ($y = max(1, $y0); $y < $y1 && $c < $bestCost; $y++) { | |
| 654 | + $row = $y * $w; | |
| 655 | + for ($x = max(1, $x0); $x < $x1; $x++) { | |
| 656 | + $i = $row + $x; | |
| 657 | + $c += $cost(self::sub($px[$i], self::predict($m, $px[$i - 1], $px[$i - $w], $x + 1 < $w ? $px[$i - $w + 1] : $px[$row], $px[$i - $w - 1]))); | |
| 658 | + } | |
| 659 | + } | |
| 660 | + if ($c < $bestCost) { | |
| 661 | + $bestCost = $c; | |
| 662 | + $best = $m; | |
| 663 | + } | |
| 664 | + } | |
| 665 | + $modes[$by * $bw + $bx] = 0xff000000 | ($best << 8); | |
| 666 | + } | |
| 667 | + } | |
| 668 | + $res[0] = self::sub($px[0], 0xff000000); | |
| 669 | + for ($x = 1; $x < $w; $x++) $res[$x] = self::sub($px[$x], $px[$x - 1]); | |
| 670 | + for ($y = 1; $y < $h; $y++) { | |
| 671 | + $row = $y * $w; | |
| 672 | + $res[$row] = self::sub($px[$row], $px[$row - $w]); | |
| 673 | + $mrow = ($y >> $bits) * $bw; | |
| 674 | + for ($x = 1; $x < $w; $x++) { | |
| 675 | + $i = $row + $x; | |
| 676 | + $m = ($modes[$mrow + ($x >> $bits)] >> 8) & 15; | |
| 677 | + $res[$i] = self::sub($px[$i], self::predict($m, $px[$i - 1], $px[$i - $w], $x + 1 < $w ? $px[$i - $w + 1] : $px[$row], $px[$i - $w - 1])); | |
| 678 | + } | |
| 679 | + } | |
| 680 | + return [$modes, $res]; | |
| 681 | + } | |
| 682 | + | |
| 683 | + /** | |
| 684 | + * エントロピー符号化した画像を書く | |
| 685 | + */ | |
| 686 | + private function entropy(array $px, int $w, int $h, bool $main): void { | |
| 687 | + $tokens = self::lz77($px, $w); | |
| 688 | + // カラーキャッシュの大きさは、見積もった大きさで選ぶ | |
| 689 | + $best = null; | |
| 690 | + foreach ($main ? [0, 6, 10] : [0] as $cacheBits) { | |
| 691 | + $hist = self::histogram($px, $tokens, $w, $cacheBits); | |
| 692 | + $size = 0; | |
| 693 | + foreach ($hist as $hh) $size += self::entropyBits($hh); | |
| 694 | + if ($best === null || $size < $best[0]) $best = [$size, $cacheBits, $hist]; | |
| 695 | + } | |
| 696 | + [, $cacheBits, $hist] = $best; | |
| 697 | + if ($cacheBits) { | |
| 698 | + $this->put(1, 1); | |
| 699 | + $this->put($cacheBits, 4); | |
| 700 | + } else { | |
| 701 | + $this->put(0, 1); | |
| 702 | + } | |
| 703 | + if ($main) $this->put(0, 1); // メタプレフィックス符号は使わない | |
| 704 | + $codes = []; | |
| 705 | + foreach ($hist as $hh) $codes[] = $this->writeCode($hh); | |
| 706 | + [[$gc, $gl], [$rc, $rl], [$bc, $bl], [$ac, $al], [$dc, $dl]] = $codes; | |
| 707 | + | |
| 708 | + $cache = $cacheBits ? array_fill(0, 1 << $cacheBits, -1) : null; | |
| 709 | + $shift = 32 - $cacheBits; | |
| 710 | + $pos = 0; | |
| 711 | + foreach ($tokens as $t) { | |
| 712 | + if ($t >= 0) { | |
| 713 | + if ($cache !== null) { | |
| 714 | + $k = (($t * self::HASH) & 0xffffffff) >> $shift; | |
| 715 | + if ($cache[$k] === $t) { | |
| 716 | + $this->put($gc[280 + $k], $gl[280 + $k]); | |
| 717 | + $pos++; | |
| 718 | + continue; | |
| 719 | + } | |
| 720 | + $cache[$k] = $t; | |
| 721 | + } | |
| 722 | + $g = ($t >> 8) & 255; | |
| 723 | + $r = ($t >> 16) & 255; | |
| 724 | + $b = $t & 255; | |
| 725 | + $a = ($t >> 24) & 255; | |
| 726 | + $this->put($gc[$g], $gl[$g]); | |
| 727 | + $this->put($rc[$r], $rl[$r]); | |
| 728 | + $this->put($bc[$b], $bl[$b]); | |
| 729 | + $this->put($ac[$a], $al[$a]); | |
| 730 | + $pos++; | |
| 731 | + continue; | |
| 732 | + } | |
| 733 | + $len = (-$t) >> 21; | |
| 734 | + $dcode = (-$t) & 0x1fffff; | |
| 735 | + [$p, $eb, $ev] = self::prefix($len); | |
| 736 | + $this->put($gc[256 + $p], $gl[256 + $p]); | |
| 737 | + if ($eb) $this->put($ev, $eb); | |
| 738 | + [$p, $eb, $ev] = self::prefix($dcode); | |
| 739 | + $this->put($dc[$p], $dl[$p]); | |
| 740 | + if ($eb) $this->put($ev, $eb); | |
| 741 | + if ($cache !== null) { | |
| 742 | + for ($i = $pos; $i < $pos + $len; $i++) $cache[(($px[$i] * self::HASH) & 0xffffffff) >> $shift] = $px[$i]; | |
| 743 | + } | |
| 744 | + $pos += $len; | |
| 745 | + } | |
| 746 | + } | |
| 747 | + | |
| 748 | + /** | |
| 749 | + * 値をプレフィックス符号と追加ビットにする | |
| 750 | + * | |
| 751 | + * @return array [符号, 追加ビット数, 追加ビット] | |
| 752 | + */ | |
| 753 | + private static function prefix(int $v): array { | |
| 754 | + $d = $v - 1; | |
| 755 | + if ($d < 4) return [$d, 0, 0]; | |
| 756 | + $hb = strlen(decbin($d)) - 1; | |
| 757 | + $s = ($d >> ($hb - 1)) & 1; | |
| 758 | + return [2 * $hb + $s, $hb - 1, $d & ((1 << ($hb - 1)) - 1)]; | |
| 759 | + } | |
| 760 | + | |
| 761 | + /** | |
| 762 | + * LZ77(同じ画素列の繰り返しを後方参照にする) | |
| 763 | + * | |
| 764 | + * @return array 画素(0以上)、又は-(長さ << 21 | 距離の符号) | |
| 765 | + */ | |
| 766 | + private static function lz77(array $px, int $w): array { | |
| 767 | + $n = count($px); | |
| 768 | + $head = []; | |
| 769 | + $chain = []; | |
| 770 | + $tokens = []; | |
| 771 | + $plane = []; | |
| 772 | + foreach (self::PLANE as $i => [$xi, $yi]) $plane[$yi * 16 + 8 - $xi] = $i + 1; | |
| 773 | + $insert = function(int $i) use (&$head, &$chain, $px, $n): void { | |
| 774 | + if ($i + 1 >= $n) return; | |
| 775 | + $k = ($px[$i] << 32) ^ $px[$i + 1]; | |
| 776 | + if (isset($head[$k])) $chain[$i] = $head[$k]; | |
| 777 | + $head[$k] = $i; | |
| 778 | + }; | |
| 779 | + $i = 0; | |
| 780 | + while ($i < $n) { | |
| 781 | + $max = min(self::MAX_LENGTH, $n - $i); | |
| 782 | + $best = 0; | |
| 783 | + $bestDist = 0; | |
| 784 | + if ($max >= 3) { | |
| 785 | + // すぐ左とすぐ上を先に試す | |
| 786 | + foreach ([1, $w] as $dist) { | |
| 787 | + if ($dist > $i || $px[$i] !== $px[$i - $dist]) continue; | |
| 788 | + $l = 1; | |
| 789 | + while ($l < $max && $px[$i + $l] === $px[$i - $dist + $l]) $l++; | |
| 790 | + if ($l > $best) { | |
| 791 | + $best = $l; | |
| 792 | + $bestDist = $dist; | |
| 793 | + } | |
| 794 | + } | |
| 795 | + $k = ($px[$i] << 32) ^ $px[$i + 1]; | |
| 796 | + $c = $head[$k] ?? -1; | |
| 797 | + for ($tries = 0; $c >= 0 && $tries < 16 && $best < $max && $i - $c <= self::MAX_DISTANCE; $tries++) { | |
| 798 | + $l = 0; | |
| 799 | + while ($l < $max && $px[$c + $l] === $px[$i + $l]) $l++; | |
| 800 | + if ($l > $best) { | |
| 801 | + $best = $l; | |
| 802 | + $bestDist = $i - $c; | |
| 803 | + } | |
| 804 | + $c = $chain[$c] ?? -1; | |
| 805 | + } | |
| 806 | + } | |
| 807 | + if ($best >= 3) { | |
| 808 | + $yi = intdiv($bestDist, $w); | |
| 809 | + $xi = $bestDist - $yi * $w; | |
| 810 | + $code = $bestDist + 120; | |
| 811 | + if ($xi <= 8 && $yi < 8 && isset($plane[$yi * 16 + 8 - $xi])) { | |
| 812 | + $code = $plane[$yi * 16 + 8 - $xi]; | |
| 813 | + } else if ($w - $xi <= 7 && $yi + 1 < 8 && isset($plane[($yi + 1) * 16 + 8 + $w - $xi])) { | |
| 814 | + $code = $plane[($yi + 1) * 16 + 8 + $w - $xi]; | |
| 815 | + } | |
| 816 | + $tokens[] = -(($best << 21) | $code); | |
| 817 | + for ($j = $i; $j < $i + $best; $j++) $insert($j); | |
| 818 | + $i += $best; | |
| 819 | + } else { | |
| 820 | + $tokens[] = $px[$i]; | |
| 821 | + $insert($i); | |
| 822 | + $i++; | |
| 823 | + } | |
| 824 | + } | |
| 825 | + return $tokens; | |
| 826 | + } | |
| 827 | + | |
| 828 | + /** | |
| 829 | + * 5つの符号の記号の数(カラーキャッシュの当たりも数える) | |
| 830 | + */ | |
| 831 | + private static function histogram(array $px, array $tokens, int $w, int $cacheBits): array { | |
| 832 | + $g = array_fill(0, 280 + ($cacheBits ? 1 << $cacheBits : 0), 0); | |
| 833 | + $r = $b = $a = array_fill(0, 256, 0); | |
| 834 | + $d = array_fill(0, 40, 0); | |
| 835 | + $cache = $cacheBits ? array_fill(0, 1 << $cacheBits, -1) : null; | |
| 836 | + $shift = 32 - $cacheBits; | |
| 837 | + $pos = 0; | |
| 838 | + foreach ($tokens as $t) { | |
| 839 | + if ($t >= 0) { | |
| 840 | + if ($cache !== null) { | |
| 841 | + $k = (($t * self::HASH) & 0xffffffff) >> $shift; | |
| 842 | + if ($cache[$k] === $t) { | |
| 843 | + $g[280 + $k]++; | |
| 844 | + $pos++; | |
| 845 | + continue; | |
| 846 | + } | |
| 847 | + $cache[$k] = $t; | |
| 848 | + } | |
| 849 | + $g[($t >> 8) & 255]++; | |
| 850 | + $r[($t >> 16) & 255]++; | |
| 851 | + $b[$t & 255]++; | |
| 852 | + $a[($t >> 24) & 255]++; | |
| 853 | + $pos++; | |
| 854 | + continue; | |
| 855 | + } | |
| 856 | + $len = (-$t) >> 21; | |
| 857 | + $g[256 + self::prefix($len)[0]]++; | |
| 858 | + $d[self::prefix((-$t) & 0x1fffff)[0]]++; | |
| 859 | + if ($cache !== null) { | |
| 860 | + for ($i = $pos; $i < $pos + $len; $i++) $cache[(($px[$i] * self::HASH) & 0xffffffff) >> $shift] = $px[$i]; | |
| 861 | + } | |
| 862 | + $pos += $len; | |
| 863 | + } | |
| 864 | + return [$g, $r, $b, $a, $d]; | |
| 865 | + } | |
| 866 | + | |
| 867 | + /** 記号の数から見積もった符号の大きさ(ビット) */ | |
| 868 | + private static function entropyBits(array $hist): float { | |
| 869 | + $total = array_sum($hist); | |
| 870 | + if ($total === 0) return 0.0; | |
| 871 | + $bits = 0.0; | |
| 872 | + $used = 0; | |
| 873 | + foreach ($hist as $c) { | |
| 874 | + if ($c === 0) continue; | |
| 875 | + $bits -= $c * log($c / $total, 2); | |
| 876 | + $used++; | |
| 877 | + } | |
| 878 | + // 符号長の分も大体足す | |
| 879 | + return $bits + $used * 4; | |
| 880 | + } | |
| 881 | + | |
| 882 | + /** | |
| 883 | + * プレフィックス符号を書く | |
| 884 | + * | |
| 885 | + * @return array [記号 => 反転した符号, 記号 => 長さ] | |
| 886 | + */ | |
| 887 | + private function writeCode(array $hist): array { | |
| 888 | + $used = []; | |
| 889 | + foreach ($hist as $s => $c) if ($c > 0) $used[] = $s; | |
| 890 | + $len = array_fill(0, count($hist), 0); | |
| 891 | + if (count($used) <= 2 && ($used[1] ?? $used[0] ?? 0) < 256) { | |
| 892 | + // 簡単な符号 | |
| 893 | + $syms = $used ?: [0]; | |
| 894 | + $this->put(1, 1); | |
| 895 | + $this->put(count($syms) - 1, 1); | |
| 896 | + if ($syms[0] < 2) { | |
| 897 | + $this->put(0, 1); | |
| 898 | + $this->put($syms[0], 1); | |
| 899 | + } else { | |
| 900 | + $this->put(1, 1); | |
| 901 | + $this->put($syms[0], 8); | |
| 902 | + } | |
| 903 | + if (count($syms) === 2) { | |
| 904 | + $this->put($syms[1], 8); | |
| 905 | + $len[$syms[0]] = $len[$syms[1]] = 1; | |
| 906 | + } | |
| 907 | + return self::canonical($len); | |
| 908 | + } | |
| 909 | + $this->put(0, 1); | |
| 910 | + if (count($used) === 1) { | |
| 911 | + // 1つだけの記号は0ビット | |
| 912 | + $len[$used[0]] = 1; | |
| 913 | + $this->writeLengths($len); | |
| 914 | + $len[$used[0]] = 0; | |
| 915 | + return self::canonical($len); | |
| 916 | + } | |
| 917 | + $len = self::lengths($hist, 15); | |
| 918 | + $this->writeLengths($len); | |
| 919 | + return self::canonical($len); | |
| 920 | + } | |
| 921 | + | |
| 922 | + private function writeLengths(array $len): void { | |
| 923 | + // 連長圧縮(16:前の長さを3~6回、17:0を3~10回、18:0を11~138回) | |
| 924 | + $tokens = []; | |
| 925 | + $n = count($len); | |
| 926 | + $prev = 8; | |
| 927 | + for ($i = 0; $i < $n;) { | |
| 928 | + $v = $len[$i]; | |
| 929 | + $run = 1; | |
| 930 | + while ($i + $run < $n && $len[$i + $run] === $v) $run++; | |
| 931 | + $i += $run; | |
| 932 | + if ($v === 0) { | |
| 933 | + while ($run >= 11) { | |
| 934 | + $r = min($run, 138); | |
| 935 | + $tokens[] = [18, 7, $r - 11]; | |
| 936 | + $run -= $r; | |
| 937 | + } | |
| 938 | + if ($run >= 3) { | |
| 939 | + $tokens[] = [17, 3, $run - 3]; | |
| 940 | + $run = 0; | |
| 941 | + } | |
| 942 | + for (; $run > 0; $run--) $tokens[] = [0, 0, 0]; | |
| 943 | + continue; | |
| 944 | + } | |
| 945 | + if ($v !== $prev) { | |
| 946 | + $tokens[] = [$v, 0, 0]; | |
| 947 | + $prev = $v; | |
| 948 | + $run--; | |
| 949 | + } | |
| 950 | + while ($run >= 3) { | |
| 951 | + $r = min($run, 6); | |
| 952 | + $tokens[] = [16, 2, $r - 3]; | |
| 953 | + $run -= $r; | |
| 954 | + } | |
| 955 | + for (; $run > 0; $run--) $tokens[] = [$v, 0, 0]; | |
| 956 | + } | |
| 957 | + $hist = array_fill(0, 19, 0); | |
| 958 | + foreach ($tokens as [$s]) $hist[$s]++; | |
| 959 | + $used = array_keys(array_filter($hist)); | |
| 960 | + if (count($used) === 1) { | |
| 961 | + $cl = array_fill(0, 19, 0); | |
| 962 | + $cl[$used[0]] = 1; | |
| 963 | + } else { | |
| 964 | + $cl = self::lengths($hist, 7); | |
| 965 | + } | |
| 966 | + $num = 19; | |
| 967 | + while ($num > 4 && $cl[self::ORDER[$num - 1]] === 0) $num--; | |
| 968 | + $this->put($num - 4, 4); | |
| 969 | + for ($i = 0; $i < $num; $i++) $this->put($cl[self::ORDER[$i]], 3); | |
| 970 | + $this->put(0, 1); // 全ての記号の長さを書く | |
| 971 | + if (count($used) === 1) $cl[$used[0]] = 0; | |
| 972 | + [$codes, $cl] = self::canonical($cl); | |
| 973 | + foreach ($tokens as [$s, $eb, $ev]) { | |
| 974 | + $this->put($codes[$s], $cl[$s]); | |
| 975 | + if ($eb) $this->put($ev, $eb); | |
| 976 | + } | |
| 977 | + } | |
| 978 | + | |
| 979 | + /** | |
| 980 | + * 長さの制限付きのハフマン符号長(制限を超えたら、少ない記号の数を底上げしてやり直す) | |
| 981 | + */ | |
| 982 | + private static function lengths(array $hist, int $limit): array { | |
| 983 | + $len = array_fill(0, count($hist), 0); | |
| 984 | + for ($min = 1; ; $min *= 2) { | |
| 985 | + $leaves = []; | |
| 986 | + foreach ($hist as $s => $c) if ($c > 0) $leaves[] = [max($c, $min), $s]; | |
| 987 | + usort($leaves, fn($a, $b) => $a[0] <=> $b[0] ?: $a[1] <=> $b[1]); | |
| 988 | + $m = count($leaves); | |
| 989 | + $freq = array_column($leaves, 0); | |
| 990 | + $parent = []; | |
| 991 | + $li = 0; | |
| 992 | + $ni = $m; | |
| 993 | + for ($next = $m; $next < 2 * $m - 1; $next++) { | |
| 994 | + $pick = []; | |
| 995 | + for ($k = 0; $k < 2; $k++) { | |
| 996 | + if ($li < $m && ($ni >= $next || $freq[$li] <= $freq[$ni])) $pick[] = $li++; | |
| 997 | + else $pick[] = $ni++; | |
| 998 | + } | |
| 999 | + $freq[$next] = $freq[$pick[0]] + $freq[$pick[1]]; | |
| 1000 | + $parent[$pick[0]] = $parent[$pick[1]] = $next; | |
| 1001 | + } | |
| 1002 | + $depth = [2 * $m - 2 => 0]; | |
| 1003 | + for ($i = 2 * $m - 3; $i >= 0; $i--) $depth[$i] = $depth[$parent[$i]] + 1; | |
| 1004 | + $max = 0; | |
| 1005 | + for ($i = 0; $i < $m; $i++) $max = max($max, $depth[$i]); | |
| 1006 | + if ($max <= $limit) { | |
| 1007 | + for ($i = 0; $i < $m; $i++) $len[$leaves[$i][1]] = $depth[$i]; | |
| 1008 | + return $len; | |
| 1009 | + } | |
| 1010 | + } | |
| 1011 | + } | |
| 1012 | + | |
| 1013 | + /** | |
| 1014 | + * 符号長から符号を作る(書く時は下位ビットからなので反転する) | |
| 1015 | + */ | |
| 1016 | + private static function canonical(array $len): array { | |
| 1017 | + $count = array_fill(0, 16, 0); | |
| 1018 | + foreach ($len as $l) if ($l) $count[$l]++; | |
| 1019 | + $next = []; | |
| 1020 | + $code = 0; | |
| 1021 | + for ($l = 1; $l < 16; $l++) { | |
| 1022 | + $code = ($code + $count[$l - 1]) << 1; | |
| 1023 | + $next[$l] = $code; | |
| 1024 | + } | |
| 1025 | + $codes = array_fill(0, count($len), 0); | |
| 1026 | + foreach ($len as $s => $l) { | |
| 1027 | + if (!$l) continue; | |
| 1028 | + $c = $next[$l]++; | |
| 1029 | + $r = 0; | |
| 1030 | + for ($i = 0; $i < $l; $i++) $r |= (($c >> $i) & 1) << ($l - 1 - $i); | |
| 1031 | + $codes[$s] = $r; | |
| 1032 | + } | |
| 1033 | + return [$codes, $len]; | |
| 1034 | + } | |
| 1035 | +} |
| @@ -0,0 +1,302 @@ | ||
| 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\Image; | |
| 39 | + | |
| 40 | +/** | |
| 41 | + * WebP(RIFFの入れ物) | |
| 42 | + * | |
| 43 | + * 読み込み:非可逆(VP8)、可逆(VP8L)、拡張形式(VP8X)のアルファ(ALPH、無圧縮・可逆、全てのフィルター)、 | |
| 44 | + * アニメーション(ANIM・ANMF、合成方法と廃棄方法、繰り返し回数)。 | |
| 45 | + * 書き出し:既定は非可逆(quality、アルファがあればALPHを可逆圧縮)、lossless => trueなら可逆。framesがあればアニメーション。 | |
| 46 | + */ | |
| 47 | +class Webp implements ImageInterface { | |
| 48 | + public function parse(string $file): \stdClass { | |
| 49 | + $d = (string)file_get_contents($file); | |
| 50 | + $chunks = self::chunks($d); | |
| 51 | + $info = (object)['width' => 0, 'height' => 0, 'compression' => null, 'hasAlpha' => false, 'animated' => false, 'frames' => 1, 'loops' => 0, 'icc' => false, 'exif' => false, 'xmp' => false]; | |
| 52 | + $image = null; | |
| 53 | + foreach ($chunks as [$id, $body]) { | |
| 54 | + if ($id === 'VP8X') { | |
| 55 | + if (strlen($body) < 10) throw new \RuntimeException('VP8Xチャンクが短過ぎます。'); | |
| 56 | + $f = ord($body[0]); | |
| 57 | + $info->width = 1 + self::u24($body, 4); | |
| 58 | + $info->height = 1 + self::u24($body, 7); | |
| 59 | + $info->hasAlpha = (bool)($f & 0x10); | |
| 60 | + $info->animated = (bool)($f & 0x02); | |
| 61 | + $info->icc = (bool)($f & 0x20); | |
| 62 | + $info->exif = (bool)($f & 0x08); | |
| 63 | + $info->xmp = (bool)($f & 0x04); | |
| 64 | + } else if ($id === 'ANIM' && strlen($body) >= 6) { | |
| 65 | + $info->loops = unpack('v', substr($body, 4, 2))[1]; | |
| 66 | + } else if ($id === 'ANMF') { | |
| 67 | + $image ??= self::chunks($body, 16)[0] ?? null; | |
| 68 | + } else if ($id === 'VP8 ' || $id === 'VP8L') { | |
| 69 | + $image ??= [$id, $body]; | |
| 70 | + } | |
| 71 | + } | |
| 72 | + if ($info->animated) $info->frames = count(array_filter($chunks, fn($c) => $c[0] === 'ANMF')); | |
| 73 | + if ($image !== null) { | |
| 74 | + [$id, $body] = $image; | |
| 75 | + $info->compression = $id === 'VP8L' ? 'VP8L' : 'VP8'; | |
| 76 | + if ($chunks[0][0] !== 'VP8X') { | |
| 77 | + if ($id === 'VP8L' && strlen($body) >= 5) { | |
| 78 | + $b = unpack('V', substr($body, 1, 4))[1]; | |
| 79 | + $info->width = ($b & 0x3fff) + 1; | |
| 80 | + $info->height = (($b >> 14) & 0x3fff) + 1; | |
| 81 | + $info->hasAlpha = (bool)(($b >> 28) & 1); | |
| 82 | + } else if ($id === 'VP8 ' && strlen($body) >= 10) { | |
| 83 | + $info->width = unpack('v', substr($body, 6, 2))[1] & 0x3fff; | |
| 84 | + $info->height = unpack('v', substr($body, 8, 2))[1] & 0x3fff; | |
| 85 | + } | |
| 86 | + } | |
| 87 | + } | |
| 88 | + return $info; | |
| 89 | + } | |
| 90 | + | |
| 91 | + /** | |
| 92 | + * RIFFのチャンク | |
| 93 | + * | |
| 94 | + * @param int $pos 最初のチャンクの位置(ファイルなら12、ANMFの中なら16) | |
| 95 | + * @return array [[ID, 中身], ...] | |
| 96 | + */ | |
| 97 | + private static function chunks(string $d, int $pos = 12): array { | |
| 98 | + $end = strlen($d); | |
| 99 | + if ($pos === 12) { | |
| 100 | + if (strlen($d) < 12 || !str_starts_with($d, 'RIFF') || substr($d, 8, 4) !== 'WEBP') throw new \RuntimeException('WebPではありません。'); | |
| 101 | + $end = min($end, 8 + unpack('V', substr($d, 4, 4))[1]); | |
| 102 | + } | |
| 103 | + $out = []; | |
| 104 | + while ($pos + 8 <= $end) { | |
| 105 | + $id = substr($d, $pos, 4); | |
| 106 | + $size = unpack('V', substr($d, $pos + 4, 4))[1]; | |
| 107 | + if ($pos + 8 + $size > $end) throw new \RuntimeException("WebPの{$id}チャンクが途中で切れています。"); | |
| 108 | + $out[] = [$id, substr($d, $pos + 8, $size)]; | |
| 109 | + $pos += 8 + $size + ($size & 1); | |
| 110 | + } | |
| 111 | + return $out; | |
| 112 | + } | |
| 113 | + | |
| 114 | + private static function u24(string $s, int $o): int { | |
| 115 | + return ord($s[$o]) | (ord($s[$o + 1]) << 8) | (ord($s[$o + 2]) << 16); | |
| 116 | + } | |
| 117 | + | |
| 118 | + public function decode(string $data): Bitmap { | |
| 119 | + $chunks = self::chunks($data); | |
| 120 | + $first = $chunks[0][0] ?? ''; | |
| 121 | + if ($first === 'VP8 ' || $first === 'VP8L') return self::still($chunks); | |
| 122 | + if ($first !== 'VP8X' || strlen($chunks[0][1]) < 10) throw new \RuntimeException('WebPの画像がありません。'); | |
| 123 | + $body = $chunks[0][1]; | |
| 124 | + $cw = 1 + self::u24($body, 4); | |
| 125 | + $ch = 1 + self::u24($body, 7); | |
| 126 | + Bitmap::check($cw, $ch); | |
| 127 | + if (!(ord($body[0]) & 0x02)) return self::still(array_slice($chunks, 1), $cw, $ch); | |
| 128 | + | |
| 129 | + // アニメーション:透明な画面にフレームを順に重ねる(背景色はブラウザと同じく使わない) | |
| 130 | + $loops = 0; | |
| 131 | + $canvas = new Bitmap($cw, $ch); | |
| 132 | + $img = null; | |
| 133 | + $dispose = null; | |
| 134 | + foreach ($chunks as [$id, $body]) { | |
| 135 | + if ($id === 'ANIM' && strlen($body) >= 6) $loops = unpack('v', substr($body, 4, 2))[1]; | |
| 136 | + if ($id !== 'ANMF') continue; | |
| 137 | + if (strlen($body) < 16) throw new \RuntimeException('ANMFチャンクが短過ぎます。'); | |
| 138 | + $x = 2 * self::u24($body, 0); | |
| 139 | + $y = 2 * self::u24($body, 3); | |
| 140 | + $fw = 1 + self::u24($body, 6); | |
| 141 | + $fh = 1 + self::u24($body, 9); | |
| 142 | + $flags = ord($body[15]); | |
| 143 | + if ($x + $fw > $cw || $y + $fh > $ch) throw new \RuntimeException('WebPのフレームが画面からはみ出しています。'); | |
| 144 | + if ($dispose !== null) $canvas->clear(...$dispose); | |
| 145 | + $canvas->draw(self::still(self::chunks($body, 16), $fw, $fh), $x, $y, !($flags & 0x02)); | |
| 146 | + $img ??= $canvas->copy(); | |
| 147 | + $img->frames[] = ['bitmap' => $canvas->copy(), 'delay' => self::u24($body, 12)]; | |
| 148 | + $dispose = $flags & 0x01 ? [$x, $y, $fw, $fh] : null; | |
| 149 | + } | |
| 150 | + if ($img === null) throw new \RuntimeException('WebPのアニメーションにフレームがありません。'); | |
| 151 | + if (count($img->frames) < 2) $img->frames = []; | |
| 152 | + else $img->loops = $loops; | |
| 153 | + return $img; | |
| 154 | + } | |
| 155 | + | |
| 156 | + /** | |
| 157 | + * 1枚の画像(VP8L、又はALPHとVP8) | |
| 158 | + * | |
| 159 | + * @param int|null $w 期待する幅と丈(VP8XやANMFの大きさ) | |
| 160 | + */ | |
| 161 | + private static function still(array $chunks, ?int $w = null, ?int $h = null): Bitmap { | |
| 162 | + $alpha = null; | |
| 163 | + foreach ($chunks as [$id, $body]) { | |
| 164 | + if ($id === 'ALPH') { | |
| 165 | + $alpha ??= $body; | |
| 166 | + } else if ($id === 'VP8L') { | |
| 167 | + [$iw, $ih, $argb] = (new Vp8l())->decode($body); | |
| 168 | + $rgba = self::rgba($argb); | |
| 169 | + break; | |
| 170 | + } else if ($id === 'VP8 ') { | |
| 171 | + [$iw, $ih, $rgba] = (new Vp8())->decode($body); | |
| 172 | + if ($alpha !== null) $rgba = self::applyAlpha($rgba, $alpha, $iw, $ih); | |
| 173 | + break; | |
| 174 | + } | |
| 175 | + } | |
| 176 | + if (!isset($rgba)) throw new \RuntimeException('WebPの画像データがありません。'); | |
| 177 | + if ($w !== null && ($iw !== $w || $ih !== $h)) throw new \RuntimeException('WebPの画像の大きさが合いません。'); | |
| 178 | + return new Bitmap($iw, $ih, $rgba); | |
| 179 | + } | |
| 180 | + | |
| 181 | + /** ARGB(ビッグエンディアン)の列をRGBAにする */ | |
| 182 | + private static function rgba(string $argb): string { | |
| 183 | + $out = ''; | |
| 184 | + for ($o = 0, $n = strlen($argb); $o < $n; $o += 16384) { | |
| 185 | + $px = unpack('N*', substr($argb, $o, 16384)); | |
| 186 | + foreach ($px as $i => $v) $px[$i] = (($v << 8) & 0xffffffff) | ($v >> 24); | |
| 187 | + $out .= pack('N*', ...$px); | |
| 188 | + } | |
| 189 | + return $out; | |
| 190 | + } | |
| 191 | + | |
| 192 | + /** | |
| 193 | + * ALPHチャンクのアルファを画素に入れる | |
| 194 | + */ | |
| 195 | + private static function applyAlpha(string $rgba, string $alph, int $w, int $h): string { | |
| 196 | + if ($alph === '') throw new \RuntimeException('ALPHチャンクが空です。'); | |
| 197 | + $hdr = ord($alph[0]); | |
| 198 | + $n = $w * $h; | |
| 199 | + if (($hdr & 3) === 0) { | |
| 200 | + $a = substr($alph, 1, $n); | |
| 201 | + if (strlen($a) < $n) throw new \RuntimeException('ALPHチャンクが途中で切れています。'); | |
| 202 | + } else if (($hdr & 3) === 1) { | |
| 203 | + // 可逆圧縮:緑がアルファ | |
| 204 | + $argb = (new Vp8l())->decodeImage(substr($alph, 1), $w, $h); | |
| 205 | + $a = ''; | |
| 206 | + for ($i = 2, $len = strlen($argb); $i < $len; $i += 4) $a .= $argb[$i]; | |
| 207 | + } else { | |
| 208 | + throw new \RuntimeException('未対応のALPHの圧縮方法です。'); | |
| 209 | + } | |
| 210 | + $filter = ($hdr >> 2) & 3; | |
| 211 | + if ($filter) $a = self::unfilter($a, $w, $h, $filter); | |
| 212 | + for ($i = 0; $i < $n; $i++) $rgba[$i * 4 + 3] = $a[$i]; | |
| 213 | + return $rgba; | |
| 214 | + } | |
| 215 | + | |
| 216 | + /** | |
| 217 | + * アルファのフィルターを戻す(1:左、2:上、3:勾配。1行目は左、左端は上から) | |
| 218 | + */ | |
| 219 | + private static function unfilter(string $a, int $w, int $h, int $filter): string { | |
| 220 | + $out = ''; | |
| 221 | + $prev = null; | |
| 222 | + for ($y = 0; $y < $h; $y++) { | |
| 223 | + $row = array_values(unpack('C*', substr($a, $y * $w, $w))); | |
| 224 | + if ($prev === null) { | |
| 225 | + $left = 0; | |
| 226 | + for ($x = 0; $x < $w; $x++) $row[$x] = $left = ($row[$x] + $left) & 255; | |
| 227 | + } else if ($filter === 1) { | |
| 228 | + $left = $prev[0]; | |
| 229 | + for ($x = 0; $x < $w; $x++) $row[$x] = $left = ($row[$x] + $left) & 255; | |
| 230 | + } else if ($filter === 2) { | |
| 231 | + for ($x = 0; $x < $w; $x++) $row[$x] = ($row[$x] + $prev[$x]) & 255; | |
| 232 | + } else { | |
| 233 | + $left = $topLeft = $prev[0]; | |
| 234 | + for ($x = 0; $x < $w; $x++) { | |
| 235 | + $g = $left + $prev[$x] - $topLeft; | |
| 236 | + $left = $row[$x] = ($row[$x] + ($g < 0 ? 0 : ($g > 255 ? 255 : $g))) & 255; | |
| 237 | + $topLeft = $prev[$x]; | |
| 238 | + } | |
| 239 | + } | |
| 240 | + $out .= pack('C*', ...$row); | |
| 241 | + $prev = $row; | |
| 242 | + } | |
| 243 | + return $out; | |
| 244 | + } | |
| 245 | + | |
| 246 | + /** | |
| 247 | + * @param array $opt lossless(既定false)、quality(非可逆、0~100、既定80)、filter・sharpness・partitions(非可逆の細かい設定) | |
| 248 | + */ | |
| 249 | + public function encode(Bitmap $img, array $opt = []): string { | |
| 250 | + $lossless = (bool)($opt['lossless'] ?? false); | |
| 251 | + if (!$lossless && ($img->width > 16383 || $img->height > 16383)) throw new \RuntimeException('非可逆のWebPは16383x16383までです。'); | |
| 252 | + if ($img->width > 16384 || $img->height > 16384) throw new \RuntimeException('WebPは16384x16384までです。'); | |
| 253 | + if (!$img->frames) { | |
| 254 | + $alpha = !$img->isOpaque(); | |
| 255 | + $body = self::image($img, $lossless, $alpha, $opt); | |
| 256 | + if ($lossless || !$alpha) return self::riff($body); | |
| 257 | + return self::riff(self::vp8x($img, 0x10).$body); | |
| 258 | + } | |
| 259 | + $alpha = false; | |
| 260 | + foreach ($img->frames as $f) $alpha = $alpha || !$f['bitmap']->isOpaque(); | |
| 261 | + $out = self::vp8x($img, 0x02 | ($alpha ? 0x10 : 0)); | |
| 262 | + // 背景色(白、BGRA)と繰り返し回数 | |
| 263 | + $out .= self::chunk('ANIM', "\xff\xff\xff\xff".pack('v', min(65535, $img->loops))); | |
| 264 | + foreach ($img->frames as $f) { | |
| 265 | + $b = $f['bitmap']; | |
| 266 | + if ($b->width !== $img->width || $b->height !== $img->height) throw new \InvalidArgumentException('フレームの大きさが画像と違います。'); | |
| 267 | + // 毎回画面全体を置き換える(合成しない、廃棄しない) | |
| 268 | + $hdr = "\0\0\0\0\0\0".substr(pack('V', $b->width - 1), 0, 3).substr(pack('V', $b->height - 1), 0, 3).substr(pack('V', max(0, min(0xffffff, (int)$f['delay']))), 0, 3)."\x02"; | |
| 269 | + $out .= self::chunk('ANMF', $hdr.self::image($b, $lossless, !$b->isOpaque(), $opt)); | |
| 270 | + } | |
| 271 | + return self::riff($out); | |
| 272 | + } | |
| 273 | + | |
| 274 | + /** 1枚の画像のチャンク(VP8L、VP8、又はALPHとVP8) */ | |
| 275 | + private static function image(Bitmap $b, bool $lossless, bool $alpha, array $opt): string { | |
| 276 | + if ($lossless) { | |
| 277 | + $px = []; | |
| 278 | + foreach (unpack('N*', $b->data) as $v) $px[] = (($v & 255) << 24) | ($v >> 8); | |
| 279 | + return self::chunk('VP8L', (new Vp8l())->encode($px, $b->width, $b->height, $alpha)); | |
| 280 | + } | |
| 281 | + $out = ''; | |
| 282 | + if ($alpha) { | |
| 283 | + $a = []; | |
| 284 | + foreach (unpack('N*', $b->data) as $v) $a[] = ($v & 255) << 8; | |
| 285 | + // 可逆圧縮、フィルター無し | |
| 286 | + $out .= self::chunk('ALPH', "\x01".(new Vp8l())->encodeImage($a, $b->width, $b->height)); | |
| 287 | + } | |
| 288 | + return $out.self::chunk('VP8 ', (new Vp8())->encode($b->data, $b->width, $b->height, $opt)); | |
| 289 | + } | |
| 290 | + | |
| 291 | + private static function vp8x(Bitmap $img, int $flags): string { | |
| 292 | + return self::chunk('VP8X', chr($flags)."\0\0\0".substr(pack('V', $img->width - 1), 0, 3).substr(pack('V', $img->height - 1), 0, 3)); | |
| 293 | + } | |
| 294 | + | |
| 295 | + private static function chunk(string $id, string $body): string { | |
| 296 | + return $id.pack('V', strlen($body)).$body.(strlen($body) & 1 ? "\0" : ''); | |
| 297 | + } | |
| 298 | + | |
| 299 | + private static function riff(string $body): string { | |
| 300 | + return 'RIFF'.pack('V', 4 + strlen($body)).'WEBP'.$body; | |
| 301 | + } | |
| 302 | +} |
| @@ -174,6 +174,7 @@ | ||
| 174 | 174 | $test->assertEquals('image/png', Media::detect("\x89PNG\r\n\x1a\nxxxx")); |
| 175 | 175 | $test->assertEquals('image/jpeg', Media::detect("\xff\xd8\xff\xe0xxxx")); |
| 176 | 176 | $test->assertEquals('image/gif', Media::detect('GIF89axxxx')); |
| 177 | + $test->assertEquals('image/webp', Media::detect('RIFF'.pack('V', 100).'WEBPVP8 ')); | |
| 177 | 178 | $test->assertNull(Media::detect('<?php echo 1;')); |
| 178 | 179 | $test->assertEquals('video/mp4', Media::detect("\0\0\0\x20ftypisom\0\0\x02\0")); |
| 179 | 180 | $test->assertEquals('video/mp4', Media::detect("\0\0\0\x18ftypmp42\0\0\0\0")); |
| @@ -211,7 +212,7 @@ | ||
| 211 | 212 | $test->assertEquals(strpos($mp4, $mdat), strpos($out, $mdat)); |
| 212 | 213 | }); |
| 213 | 214 | |
| 214 | - $test->it('JPEGとPNGのメタデータを取り除くはず', function($test): void { | |
| 215 | + $test->it('JPEG・PNG・WebPのメタデータを取り除くはず', function($test): void { | |
| 215 | 216 | $exif = "\xff\xe1".pack('n', 12).'Exif'."\0\0GPS!"; |
| 216 | 217 | $jfif = "\xff\xe0".pack('n', 6).'JFIF'; |
| 217 | 218 | $com = "\xff\xfe".pack('n', 6).'note'; |
| @@ -230,6 +231,15 @@ | ||
| 230 | 231 | $test->assertFalse(str_contains($out, 'eXIf')); |
| 231 | 232 | $test->assertStringContains('IDAT', $out); |
| 232 | 233 | $test->assertStringContains('IEND', $out); |
| 234 | + | |
| 235 | + $riff = fn($t, $d) => $t.pack('V', strlen($d)).$d.(strlen($d) & 1 ? "\0" : ''); | |
| 236 | + $body = $riff('VP8X', "\x0c\0\0\0\0\0\0\0\0\0").$riff('VP8L', 'IMG').$riff('EXIF', 'GPS!').$riff('XMP ', '<x:secret/>'); | |
| 237 | + $out = Media::strip('RIFF'.pack('V', 4 + strlen($body)).'WEBP'.$body, 'image/webp'); | |
| 238 | + $test->assertFalse(str_contains($out, 'GPS!')); | |
| 239 | + $test->assertFalse(str_contains($out, 'secret')); | |
| 240 | + $test->assertStringContains('IMG', $out); | |
| 241 | + $test->assertEquals(0, ord($out[20]) & 0x0c); | |
| 242 | + $test->assertEquals(strlen($out) - 8, unpack('V', substr($out, 4, 4))[1]); | |
| 233 | 243 | }); |
| 234 | 244 | }); |
| 235 | 245 |
| @@ -41,6 +41,7 @@ | ||
| 41 | 41 | |
| 42 | 42 | use Std\Lib\Tester; |
| 43 | 43 | use Std\Lib\Image; |
| 44 | +use Std\Lib\Image\{Bitmap, Png, Bmp, Targa, Gif, Jpeg, Webp}; | |
| 44 | 45 | |
| 45 | 46 | $test = new Tester([ |
| 46 | 47 | 'colorOutput' => true, |
| @@ -62,6 +63,7 @@ | ||
| 62 | 63 | file_put_contents("{$dir}/a.jpg", "\xff\xd8\xff\xe0".pack('n', 16)."JFIF\0\x01\x01\x00\x00\x01\x00\x01\x00\x00" |
| 63 | 64 | ."\xff\xc0".pack('n', 17)."\x08".pack('nn', 6, 7)."\x03\x01\x22\x00\x02\x11\x01\x03\x11\x01\xff\xd9"); |
| 64 | 65 | file_put_contents("{$dir}/a.tga", pack('CCCvvCvvvvCC', 0, 0, 2, 0, 0, 0, 0, 0, 2, 3, 24, 0).str_repeat("\x00\x00\xff", 6)); |
| 66 | +file_put_contents("{$dir}/a.webp", base64_decode('UklGRkwBAABXRUJQVlA4TEABAAAvE8ADAM2VIaL/MQAIwv+4hRjw/ocANSBo2zZmMP5wfxQpxEaSFEkFc0z+O3nPuDNVTiLJdq2pavRQ+Pyjh34QkBQk/z6Og1CbtgEDnpxyCOWacx4555JbuTfncuo9zT2Xcu86ykUAiVREGAJKIGa5DrM4IpgiCSIQQKkAOo4F+WEPUkgxGzs2CAjCLOA4szH4htnzjwBmMijB1IC6DjCYlLcQqIgS6APQYTKxNC+IM6HQDCkBUKcxkcWB/HAARI0phCdxHQJMdqxQxDcGiGAWXYg80NOICGjMwnwAzpDiVS8CqdMIsCCDaQFmcYKr7c4AejOmvo49kAX4XRwuTCALOY6qQQApoFD5MSYChxEMECvmnx1BhJzoYfEGKSDHZQwmJiAEFBGACEOeiOPB/+MIZmOABBFjtJpvNBac/X8cAA==')); | |
| 65 | 67 | file_put_contents("{$dir}/fake.png", 'これは画像ではない'); |
| 66 | 68 | file_put_contents("{$dir}/big.png", "\x89PNG\r\n\x1a\n".str_repeat("\0", 3000)); |
| 67 | 69 | |
| @@ -77,13 +79,14 @@ | ||
| 77 | 79 | $test->assertEquals('image/png', $img->fullInfo->type); |
| 78 | 80 | }); |
| 79 | 81 | |
| 80 | - $test->it('GIF・JPEG・TGAの大きさを読めるはず', function($test) use ($rel): void { | |
| 81 | - foreach (['a.gif' => ['image/gif', 4, 5], 'a.jpg' => ['image/jpeg', 7, 6], 'a.tga' => ['image/x-tga', 2, 3]] as $f => [$type, $w, $h]) { | |
| 82 | + $test->it('GIF・JPEG・TGA・WebPの大きさを読めるはず', function($test) use ($rel): void { | |
| 83 | + foreach (['a.gif' => ['image/gif', 4, 5], 'a.jpg' => ['image/jpeg', 7, 6], 'a.tga' => ['image/x-tga', 2, 3], 'a.webp' => ['image/webp', 20, 16]] as $f => [$type, $w, $h]) { | |
| 82 | 84 | $img = new Image("{$rel}/{$f}"); |
| 83 | 85 | $test->assertEquals($type, $img->type, "{$f}の種類"); |
| 84 | 86 | $test->assertEquals($w, $img->fullInfo->width, "{$f}の横"); |
| 85 | 87 | $test->assertEquals($h, $img->fullInfo->height, "{$f}の丈"); |
| 86 | 88 | } |
| 89 | + $test->assertEquals('VP8L', (new Image("{$rel}/a.webp"))->fullInfo->compression); | |
| 87 | 90 | }); |
| 88 | 91 | |
| 89 | 92 | $test->it('大きさを読み易い形式にするはず', function($test) use ($rel): void { |
| @@ -103,6 +106,298 @@ | ||
| 103 | 106 | $test->it('未対応の形式は例外のはず', function($test) use ($rel): void { |
| 104 | 107 | // 拡張子ではなく中身で判断する |
| 105 | 108 | $test->assertThrows(fn() => new Image("{$rel}/fake.png"), \RuntimeException::class); |
| 109 | + }); | |
| 110 | +}); | |
| 111 | + | |
| 112 | +$test->describe('画素の読み書き', function($test): void { | |
| 113 | + // テスト用の画素:塗り分け(2色、16色、灰色、半透明) | |
| 114 | + $make = function(string $kind, int $w = 13, int $h = 7): Bitmap { | |
| 115 | + $b = new Bitmap($w, $h); | |
| 116 | + for ($y = 0; $y < $h; $y++) for ($x = 0; $x < $w; $x++) { | |
| 117 | + match ($kind) { | |
| 118 | + 'two' => ($x + $y) % 2 ? $b->set($x, $y, 0, 0, 0) : $b->set($x, $y, 255, 255, 255), | |
| 119 | + 'sixteen' => $b->set($x, $y, ($x % 4) * 80, ($y % 4) * 80, 40), | |
| 120 | + 'gray' => $b->set($x, $y, ($x * 19 + $y * 7) & 255, ($x * 19 + $y * 7) & 255, ($x * 19 + $y * 7) & 255), | |
| 121 | + 'rgb' => $b->set($x, $y, ($x * 19) & 255, ($y * 37) & 255, ($x * $y * 11) & 255), | |
| 122 | + 'rgba' => $b->set($x, $y, ($x * 19) & 255, ($y * 37) & 255, ($x * $y * 11) & 255, ($x * 23 + $y) & 255), | |
| 123 | + }; | |
| 124 | + } | |
| 125 | + return $b; | |
| 126 | + }; | |
| 127 | + | |
| 128 | + $test->it('PNGは色に合わせて一番小さい形式で書き、そのまま読み戻せるはず', function($test) use ($make): void { | |
| 129 | + // 色類とビット深さ(IHDRの25・24バイト目) | |
| 130 | + foreach (['two' => [3, 1], 'sixteen' => [3, 4], 'rgb' => [2, 8], 'rgba' => [6, 8]] as $kind => [$ctype, $depth]) { | |
| 131 | + // 256色以下はパレットになるので、RGBは大きくする | |
| 132 | + $b = str_starts_with($kind, 'rgb') ? $make($kind, 40, 20) : $make($kind); | |
| 133 | + $png = (new Png())->encode($b); | |
| 134 | + $test->assertEquals($ctype, ord($png[25]), "{$kind}の色類"); | |
| 135 | + $test->assertEquals($depth, ord($png[24]), "{$kind}のビット深さ"); | |
| 136 | + $test->assertEquals($b->data, (new Png())->decode($png)->data, "{$kind}の画素"); | |
| 137 | + } | |
| 138 | + // 257色以上の灰色はグレースケール | |
| 139 | + $g = new Bitmap(300, 1); | |
| 140 | + for ($x = 0; $x < 300; $x++) $g->set($x, 0, $x % 256, $x % 256, $x % 256); | |
| 141 | + $g->set(299, 0, 1, 1, 1, 0); | |
| 142 | + $test->assertEquals(4, ord((new Png())->encode($g)[25])); | |
| 143 | + }); | |
| 144 | + | |
| 145 | + $test->it('他のソフトが書いたPNG(Adam7・16ビット・複数のIDAT、2ビットの灰色とtRNS)を読めるはず', function($test): void { | |
| 146 | + // Pythonで書いた画像と、仕様通りに計算した画素 | |
| 147 | + $a = (new Png())->decode(base64_decode('iVBORw0KGgoAAAANSUhEUgAAAAUAAAADEAYAAAF8oSktAAAARUlEQVR42gF/AID/ATkADM6M0H1iBNbLcN3l/o5vBNgGEEUPQS/4AZYbuShicyOc4RV6jmAsa7Y+tjiOxhKWPAJySEdxNHosg2/8MHdoAAAARUlEQVTxdx8NlmXOBKUuLT5gsPxX0TRDWr8dWK4DA1NRvthgrjWNHCc8Ag1VPO0yCC+LZ9tZnaKZ3CYt/v2Q0+fcfpgdOR3QNgufp1a3AAAAAElFTkSuQmCC')); | |
| 148 | + $test->assertEquals('390c8c7d7247342cd8100f2f6f770d65d670e58e0351d8ae8e4f6eac342fc231b7b08716eb3fc12896b96223177494287733c28ee8ba53bdb56b8824', bin2hex($a->data)); | |
| 149 | + $b = (new Png())->decode(base64_decode('iVBORw0KGgoAAAANSUhEUgAAAAUAAAADAgAAAAA07YKFAAAAAnRSTlMAApidrBQAAAAISURBVHjaYwnPZApswiXMeQAAAAlJREFUYGo7AAALNwLgcgWJwgAAAABJRU5ErkJggg==')); | |
| 150 | + $test->assertEquals('555555ff555555ff555555ffffffffffffffffffaaaaaa00aaaaaa00aaaaaa00000000ff555555ff000000ffaaaaaa00ffffffffaaaaaa00000000ff', bin2hex($b->data)); | |
| 151 | + }); | |
| 152 | + | |
| 153 | + $test->it('壊れたPNGは例外のはず', function($test) use ($make): void { | |
| 154 | + $png = (new Png())->encode($make('rgb')); | |
| 155 | + $bad = $png; | |
| 156 | + $bad[40] = chr(ord($bad[40]) ^ 1); // IDATの中(CRCが合わなくなる) | |
| 157 | + $test->assertThrows(fn() => (new Png())->decode($bad), \RuntimeException::class); | |
| 158 | + $test->assertThrows(fn() => (new Png())->decode(substr($png, 0, 60)), \RuntimeException::class); | |
| 159 | + $test->assertThrows(fn() => (new Png())->decode('GIF89a'), \RuntimeException::class); | |
| 160 | + }); | |
| 161 | + | |
| 162 | + $test->it('APNGのフレーム・遅延・繰り返しを読み書き出来るはず', function($test): void { | |
| 163 | + $a = Bitmap::fill(4, 3, 255, 0, 0); | |
| 164 | + $a->frames = [ | |
| 165 | + ['bitmap' => Bitmap::fill(4, 3, 255, 0, 0), 'delay' => 100], | |
| 166 | + ['bitmap' => Bitmap::fill(4, 3, 0, 0, 255, 128), 'delay' => 250], | |
| 167 | + ]; | |
| 168 | + $a->loops = 3; | |
| 169 | + $b = (new Png())->decode((new Png())->encode($a)); | |
| 170 | + $test->assertCount(2, $b->frames); | |
| 171 | + $test->assertEquals([100, 250], array_column($b->frames, 'delay')); | |
| 172 | + $test->assertEquals(3, $b->loops); | |
| 173 | + $test->assertEquals([0, 0, 255, 128], $b->frames[1]['bitmap']->get(2, 1)); | |
| 174 | + }); | |
| 175 | + | |
| 176 | + $test->it('アルファで重ねられるはず', function($test): void { | |
| 177 | + $bg = Bitmap::fill(2, 1, 255, 0, 0); | |
| 178 | + $bg->draw(Bitmap::fill(1, 1, 0, 0, 255, 128), 1, 0); | |
| 179 | + $test->assertEquals([255, 0, 0, 255], $bg->get(0, 0)); | |
| 180 | + $test->assertEquals([127, 0, 128, 255], $bg->get(1, 0)); | |
| 181 | + }); | |
| 182 | + | |
| 183 | + $test->it('BMPを読み書き出来るはず(パレット、24ビット、アルファ付き32ビット、RLE4)', function($test) use ($make): void { | |
| 184 | + foreach (['two' => 1, 'sixteen' => 4, 'rgb' => 24, 'rgba' => 32] as $kind => $bits) { | |
| 185 | + $b = str_starts_with($kind, 'rgb') ? $make($kind, 40, 20) : $make($kind); | |
| 186 | + $bmp = (new Bmp())->encode($b); | |
| 187 | + $test->assertEquals($bits, unpack('v', substr($bmp, 28, 2))[1], "{$kind}のビット深さ"); | |
| 188 | + $test->assertEquals($b->data, (new Bmp())->decode($bmp)->data, "{$kind}の画素"); | |
| 189 | + } | |
| 190 | + // RLE4(繰り返し、そのままの画素、デルタ、行と画像の終わり)。飛ばした画素は透明 | |
| 191 | + $r = (new Bmp())->decode(base64_decode('Qk2MAAAAAAAAAHYAAAAoAAAABwAAAAQAAAABAAQAAgAAABYAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAA/wAA/wAA/wAAAEBAQABQUFAAYGBgAHBwcACAgIAAkJCQAKCgoACwsLAAwMDAANDQ0ADg4OAA8PDwAAcSAAAABTEjQAACVQAAAAIDAQT/AAE=')); | |
| 192 | + $test->assertEquals([0, 0, 0, 0], $r->get(0, 0)); | |
| 193 | + $test->assertEquals([240, 240, 240, 255], $r->get(3, 0)); | |
| 194 | + $test->assertEquals([0, 0, 0, 0], $r->get(3, 1)); | |
| 195 | + $test->assertEquals([0, 0, 255, 255], $r->get(0, 2)); | |
| 196 | + $test->assertEquals([80, 80, 80, 255], $r->get(6, 2)); | |
| 197 | + $test->assertEquals([0, 255, 0, 255], $r->get(1, 3)); | |
| 198 | + }); | |
| 199 | + | |
| 200 | + $test->it('TGAを読み書き出来るはず(左下の原点、行を跨ぐRLE)', function($test) use ($make): void { | |
| 201 | + foreach (['rgb', 'rgba'] as $kind) { | |
| 202 | + $b = $make($kind); | |
| 203 | + $test->assertEquals($b->data, (new Targa())->decode((new Targa())->encode($b))->data, "{$kind}(RLE)"); | |
| 204 | + $test->assertEquals($b->data, (new Targa())->decode((new Targa())->encode($b, ['rle' => false]))->data, "{$kind}(無圧縮)"); | |
| 205 | + } | |
| 206 | + $t = (new Targa())->decode(base64_decode('AAACAAAAAAAAAAAAAwACABgA////AAAAAP//AAD/AP8A/wAA')); | |
| 207 | + $test->assertEquals('ff0000ff00ff00ff0000ffffffffffff000000ffffff00ff', bin2hex($t->data)); | |
| 208 | + $t = (new Targa())->decode(base64_decode('AAAKAAAAAAAAAAAAAwACACAoAQAA//8A/wCAgv8AAAAAAP///w==')); | |
| 209 | + $test->assertEquals('ff0000ff00ff00800000ff000000ff000000ff00ffff00ff', bin2hex($t->data)); | |
| 210 | + }); | |
| 211 | + | |
| 212 | + $test->it('GIFを読み書き出来るはず(パレット、透明、256色を超える減色)', function($test) use ($make): void { | |
| 213 | + foreach (['two', 'sixteen', 'gray'] as $kind) { | |
| 214 | + $b = $make($kind); | |
| 215 | + $test->assertEquals($b->data, (new Gif())->decode((new Gif())->encode($b))->data, $kind); | |
| 216 | + } | |
| 217 | + // 透明はアルファ128未満、GIFに半透明は無い | |
| 218 | + $t = $make('two'); | |
| 219 | + $t->set(1, 1, 9, 9, 9, 0); | |
| 220 | + $t->set(2, 1, 9, 9, 9, 200); | |
| 221 | + $d = (new Gif())->decode((new Gif())->encode($t)); | |
| 222 | + $test->assertEquals(0, $d->get(1, 1)[3]); | |
| 223 | + $test->assertEquals([9, 9, 9, 255], $d->get(2, 1)); | |
| 224 | + // 減色:大きさと大体の色は保つ | |
| 225 | + $rgb = $make('rgb', 40, 20); | |
| 226 | + $q = (new Gif())->decode((new Gif())->encode($rgb, ['dither' => true])); | |
| 227 | + $diff = 0; | |
| 228 | + for ($i = 0; $i < strlen($rgb->data); $i++) $diff += abs(ord($rgb->data[$i]) - ord($q->data[$i])); | |
| 229 | + $test->assertTrue($diff / strlen($rgb->data) < 16, '平均の差'); | |
| 230 | + }); | |
| 231 | + | |
| 232 | + $test->it('他のソフトが書いたGIF(インターレース、オフセットと廃棄方法のあるアニメーション)を読めるはず', function($test): void { | |
| 233 | + // Pillowが書いたインターレースの3x9:真ん中の列は3行ずらした色 | |
| 234 | + $cols = [[255, 0, 0], [0, 255, 0], [0, 0, 255], [255, 255, 0], [0, 255, 255], [255, 0, 255], [255, 255, 255], [0, 0, 0], [128, 128, 128]]; | |
| 235 | + $i = (new Gif())->decode(base64_decode('R0lGODdhAwAJAIMAAP//////AICAgAD//wD/AP8A//8AAAAA/wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACwAAAAAAwAJAAAIIQANBDBAYACBAwUOBAAQYACCAQUEFABgAAACAggEHBAQEAA7')); | |
| 236 | + for ($y = 0; $y < 9; $y++) { | |
| 237 | + $test->assertEquals([...$cols[$y], 255], $i->get(0, $y), "{$y}行目"); | |
| 238 | + $test->assertEquals([...$cols[($y + 3) % 9], 255], $i->get(1, $y), "{$y}行目の真ん中"); | |
| 239 | + } | |
| 240 | + // ImageMagickが書いた6x5:紺の背景、赤(前に戻す)、黄(背景に戻す)、緑。画素はChromiumと同じ | |
| 241 | + $a = (new Gif())->decode(base64_decode('R0lGODlhBgAFAPAAAAAAgAAAACH/C05FVFNDQVBFMi4wAwEAAAAh+QQECgAAACH/C0ltYWdlTWFnaWNrDmdhbW1hPTAuNDU0NTQ1ACwAAAAABgAFAAACBYSPqWsFACH5BAwUAAAAIf8LSW1hZ2VNYWdpY2sOZ2FtbWE9MC40NTQ1NDUALAEAAQACAAIAgP8AAAAAAAIChFEAIfkECB4AAAAh/wtJbWFnZU1hZ2ljaw5nYW1tYT0wLjQ1NDU0NQAsAwACAAIAAgCA//8AAAAAAgKEUQAh+QQEBQAAACH/C0ltYWdlTWFnaWNrDmdhbW1hPTAuNDU0NTQ1ACwAAAMAAgACAIAA/wAAAAACAoRRADs=')); | |
| 242 | + $test->assertCount(4, $a->frames); | |
| 243 | + $test->assertEquals([100, 200, 300, 50], array_column($a->frames, 'delay')); | |
| 244 | + $test->assertEquals(0, $a->loops); | |
| 245 | + $f = array_column($a->frames, 'bitmap'); | |
| 246 | + $test->assertEquals([255, 0, 0, 255], $f[1]->get(1, 1)); | |
| 247 | + $test->assertEquals([0, 0, 128, 255], $f[2]->get(1, 1), '赤は前に戻した'); | |
| 248 | + $test->assertEquals([255, 255, 0, 255], $f[2]->get(3, 2)); | |
| 249 | + $test->assertEquals([0, 0, 0, 0], $f[3]->get(3, 2), '黄は背景(透明)に戻した'); | |
| 250 | + $test->assertEquals([0, 255, 0, 255], $f[3]->get(0, 3)); | |
| 251 | + }); | |
| 252 | + | |
| 253 | + $test->it('GIFのアニメーションを書けるはず', function($test): void { | |
| 254 | + $a = Bitmap::fill(5, 4, 255, 0, 0); | |
| 255 | + $a->frames = [ | |
| 256 | + ['bitmap' => Bitmap::fill(5, 4, 255, 0, 0), 'delay' => 100], | |
| 257 | + ['bitmap' => Bitmap::fill(5, 4, 0, 0, 255), 'delay' => 250], | |
| 258 | + ]; | |
| 259 | + $a->loops = 0; | |
| 260 | + $b = (new Gif())->decode((new Gif())->encode($a)); | |
| 261 | + $test->assertEquals([100, 250], array_column($b->frames, 'delay')); | |
| 262 | + $test->assertEquals(0, $b->loops); | |
| 263 | + $test->assertEquals([0, 0, 255, 255], $b->frames[1]['bitmap']->get(4, 3)); | |
| 264 | + }); | |
| 265 | + | |
| 266 | + $test->it('他のソフトが書いたJPEG(プログレッシブ+リスタート、CMYK、グレースケール、Exifの向き)を読めるはず', function($test): void { | |
| 267 | + // Pillow(libjpeg-turbo)で書いて、libjpeg-turboで読んだ画素(逆DCTの計算の違いで±3まで) | |
| 268 | + $cases = [ | |
| 269 | + 'prog' => ['/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAIBAQEBAQIBAQECAgICAgQDAgICAgUEBAMEBgUGBgYFBgYGBwkIBgcJBwYGCAsICQoKCgoKBggLDAsKDAkKCgr/2wBDAQICAgICAgUDAwUKBwYHCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgoKCgr/wgARCAAHAAkDASIAAhEBAxEB/8QAFgABAQEAAAAAAAAAAAAAAAAAAAYH/8QAFQEBAQAAAAAAAAAAAAAAAAAAAwT/3QAEAAH/2gAMAwEAAhADEAAAAZ/WyUf/xAAYEAACAwAAAAAAAAAAAAAAAAAAAwYHF//aAAgBAQABBQKI0+g//9DH0H//xAAZEQABBQAAAAAAAAAAAAAAAAAGAAEHFSP/2gAIAQMBAT8Bj81IaNtV/8QAFxEBAAMAAAAAAAAAAAAAAAAABAATMf/aAAgBAgEBPwELk07P/8QAFxABAQEBAAAAAAAAAAAAAAAAAgAjM//aAAgBAQAGPwI5m//Q5m//xAAXEAADAQAAAAAAAAAAAAAAAAAAAdHx/9oACAEBAAE/Iaaj/9DZR//aAAwDAQACAAMAAAAQ+//EABgRAAIDAAAAAAAAAAAAAAAAAABRMUFx/9oACAEDAQE/EJPprT//xAAVEQEBAAAAAAAAAAAAAAAAAAAAEf/aAAgBAgEBPxCC/wD/xAAWEAEBAQAAAAAAAAAAAAAAAAAAAXH/2gAIAQEAAT8Qrxr/0C3/2Q==', '000100ff110708ff2f0618ff490627ff640736ff810645ff9e0758ffb60764ffcf0b71ff091e0fff1c231bff39242bff54243aff6e234aff8a235affa9236affc22478ffd92883ff0a4322ff1b472cff37493dff53494aff6e495bff89486affa8477affc14987ffd84c93ff096732ff1d6c3dff386e4cff556e5bff6e6d6bff8b6c7bffa96d89ffc36e97ffd971a4ff0a8a3fff1b8e49ff388f58ff558f67ff6f8f78ff8c8f88ffaa8f98ffc38fa5ffd893b0ff0ead50ff1fb25aff3db369ff59b278ff74b289ff90b299ffafb2a9ffc7b3b5ffddb7c2ff11cd5fff25d16bff40d37aff5dd389ff78d39aff93d1a8ffb1d1b9ffcbd3c6ffe1d6d4ff', 9, 7], | |
| 270 | + 'cmyk' => ['/9j/7gAOQWRvYmUAZAAAAAAA/9sAQwACAQEBAQECAQEBAgICAgIEAwICAgIFBAQDBAYFBgYGBQYGBgcJCAYHCQcGBggLCAkKCgoKCgYICwwLCgwJCgoK/8AAFAgABwAJBEMRAE0RAFkRAEsRAP/EAB8AAAEFAQEBAQEBAAAAAAAAAAABAgMEBQYHCAkKC//EALUQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+v/aAA4EQwBNAFkASwAAPwDwf/ggD/zBP+2f9Kxf+IaHwh/0BdN/7+R/4143/wAE7v2AtL/0H/RLf+H+Ja/fyv34o/4hofCH/QF03/v5H/jX6H/8MBaX/wA+lv8A99LRX//Z', '000000ff1c000eff38001eff54002cff70003cff8c004affa8005affc40068ffe00078ff002410ff1c241eff38242eff54243cff70244cff8c245affa8246affc42478ffe02488ff00481eff1c482cff38483cff54484aff70485aff8c4868ffa84878ffc44886ffe04896ff006c2eff1c6c3cff386c4cff546c5aff706c6aff8c6c78ffa86c88ffc46c96ffe06ca6ff00903cff1c904aff38905aff549068ff709078ff8c9086ffa89096ffc490a4ffe090b4ff00b44bff1cb45aff38b469ff54b478ff70b487ff8cb496ffa8b4a5ffc4b4b4ffe0b4c3ff00d85bff1cd869ff38d879ff54d887ff70d897ff8cd8a5ffa8d8b5ffc4d8c3ffe0d8d3ff', 9, 7], | |
| 271 | + 'gray' => ['/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAIBAQEBAQIBAQECAgICAgQDAgICAgUEBAMEBgUGBgYFBgYGBwkIBgcJBwYGCAsICQoKCgoKBggLDAsKDAkKCgr/wAALCAAHAAkBAREA/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/9oACAEBAAA/AOA/ZH/4I/aAfsv/ABLrTt/GtfW3/Dn3Qv8AoH2n/fa1/9k=', '000000ff0a0a0aff141414ff1e1e1eff282828ff323232ff3d3d3dff464646ff515151ff161616ff202020ff2b2b2bff353535ff3e3e3eff484848ff535353ff5d5d5dff676767ff2e2e2eff373737ff424242ff4c4c4cff565656ff5f5f5fff6a6a6aff747474ff7e7e7eff454545ff4f4f4fff5a5a5aff646464ff6d6d6dff777777ff828282ff8c8c8cff969696ff5b5b5bff646464ff6f6f6fff797979ff838383ff8d8d8dff989898ffa1a1a1ffabababff737373ff7c7c7cff878787ff919191ff9b9b9bffa5a5a5ffb0b0b0ffb9b9b9ffc4c4c4ff888888ff929292ff9d9d9dffa7a7a7ffb1b1b1ffbababaffc5c5c5ffcfcfcfffd9d9d9ff', 9, 7], | |
| 272 | + 'o6' => ['/9j/4AAQSkZJRgABAQAAAQABAAD/4QAiRXhpZgAATU0AKgAAAAgAAQESAAMAAAABAAYAAAAAAAD/2wBDAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/2wBDAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/wAARCAACAAMDAREAAhEBAxEB/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQAAAF9AQIDAAQRBRIhMUEGE1FhByJxFDKBkaEII0KxwRVS0fAkM2JyggkKFhcYGRolJicoKSo0NTY3ODk6Q0RFRkdISUpTVFVWV1hZWmNkZWZnaGlqc3R1dnd4eXqDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uHi4+Tl5ufo6erx8vP09fb3+Pn6/8QAHwEAAwEBAQEBAQEBAQAAAAAAAAECAwQFBgcICQoL/8QAtREAAgECBAQDBAcFBAQAAQJ3AAECAxEEBSExBhJBUQdhcRMiMoEIFEKRobHBCSMzUvAVYnLRChYkNOEl8RcYGRomJygpKjU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6goOEhYaHiImKkpOUlZaXmJmaoqOkpaanqKmqsrO0tba3uLm6wsPExcbHyMnK0tPU1dbX2Nna4uPk5ebn6Onq8vP09fb3+Pn6/9oADAMBAAIRAxEAPwDzv9vn4I/Be3/a9+N1nb/CL4YQWeneINI0rT7SHwD4UitrDS9K8J+H9N0vTbKBNJWK1sNN061trCws4Ejt7Oyt4LW3jjghjjX/AG+/ZL8TcR5N+zP+g7RyjiDO8qo5j9G7wz4lzCjlua4/A0sdxHxfkGH4r4sz/GU8LiKUMVnfFHFGc5xxJxFmtdTx2dZ/muZZxmVfE5jjsViav+4/7LLwQ8FvFf6IFDxD8UvCHwv8SuP+KfpQfT/x/E3HPH/AHCnGPGHEWOp/T8+k3g6eNz3ibiLKcxzrN8XDCYbDYWGIzDG4itHDYehQjNUqVOMf/9k=', 'ffff00fffe0000ff00ffffff01ff00ffff00feff0000feff', 2, 3], | |
| 273 | + ]; | |
| 274 | + foreach ($cases as $name => [$b64, $hex, $w, $h]) { | |
| 275 | + $b = (new Jpeg())->decode(base64_decode($b64)); | |
| 276 | + $test->assertEquals([$w, $h], [$b->width, $b->height], "{$name}の大きさ"); | |
| 277 | + $exp = hex2bin($hex); | |
| 278 | + $max = 0; | |
| 279 | + for ($i = 0; $i < strlen($exp); $i++) $max = max($max, abs(ord($exp[$i]) - ord($b->data[$i]))); | |
| 280 | + $test->assertTrue($max <= 3, "{$name}の画素(差{$max})"); | |
| 281 | + } | |
| 282 | + // 向き6(時計回りに90度):3x2が2x3になる | |
| 283 | + $info = (new Jpeg())->decode(base64_decode($cases['o6'][0])); | |
| 284 | + $test->assertEquals(2, $info->width); | |
| 285 | + }); | |
| 286 | + | |
| 287 | + $test->it('JPEGを書けるはず(品質、4:2:0・4:4:4、グレースケール、透明は白)', function($test) use ($make): void { | |
| 288 | + // 滑らかなグラデーション(画素毎に色が変わる画像は4:2:0で色差が失われるのが正しい) | |
| 289 | + $b = new Bitmap(40, 20); | |
| 290 | + for ($y = 0; $y < 20; $y++) for ($x = 0; $x < 40; $x++) $b->set($x, $y, $x * 6, $y * 12, ($x + $y) * 4); | |
| 291 | + $hi = (new Jpeg())->encode($b, ['quality' => 95]); | |
| 292 | + $lo = (new Jpeg())->encode($b, ['quality' => 30]); | |
| 293 | + $test->assertTrue(strlen($lo) < strlen($hi), '品質が低いと小さい'); | |
| 294 | + $err = function(Bitmap $a, Bitmap $c): float { | |
| 295 | + $s = 0; | |
| 296 | + for ($i = 0; $i < strlen($a->data); $i++) if ($i % 4 !== 3) $s += abs(ord($a->data[$i]) - ord($c->data[$i])); | |
| 297 | + return $s / (strlen($a->data) * 3 / 4); | |
| 298 | + }; | |
| 299 | + $test->assertTrue($err($b, (new Jpeg())->decode($hi)) < 2, '品質95の平均の差'); | |
| 300 | + $test->assertTrue($err($b, (new Jpeg())->decode((new Jpeg())->encode($b, ['quality' => 95, 'subsampling' => '444']))) < 1.5, '4:4:4の平均の差'); | |
| 301 | + // 灰色はグレースケール(SOF0の成分の数が1) | |
| 302 | + $g = (new Jpeg())->encode($make('gray')); | |
| 303 | + $test->assertEquals(1, ord($g[strpos($g, "\xff\xc0") + 9])); | |
| 304 | + // 透明は白と合成 | |
| 305 | + $t = Bitmap::fill(16, 16, 0, 0, 0, 0); | |
| 306 | + $test->assertTrue(min((new Jpeg())->decode((new Jpeg())->encode($t))->get(8, 8)) >= 250); | |
| 307 | + }); | |
| 308 | + | |
| 309 | + $test->it('libwebpが書いたWebP(非可逆、可逆、アルファ、アニメーション)を読めるはず', function($test): void { | |
| 310 | + // Pillow(libwebp 1.6)で書いて、libwebpで読んだ画素のMD5(計算はlibwebpと全く同じなので一致する) | |
| 311 | + $cases = [ | |
| 312 | + 'ly' => ['UklGRqYAAABXRUJQVlA4IJoAAADQBACdASoYABQAPrVOoEqnJCMhsBgIAOAWiUAWHYHlTnwr36n6ZUj8wDDu9+DCyAD+opZKvMHS233rVxNfgljnxvWSmFqCXbB3yF9HmOg8rtcoGdyLn5z6+4VxeQ1ImRbIwaoZ1v7erNwZKFXCzKxRsQdQwWWn+vJ3DfH+/zPcdEROIODEUzTDqVxI6LKvZMfVtHqlKUQ+AYQA', 24, 20, ['3d2d40581e10cc86445dd4d4f2c935c4']], | |
| 313 | + 'll' => ['UklGRkwBAABXRUJQVlA4TEABAAAvE8ADAM2VIaL/MQAIwv+4hRjw/ocANSBo2zZmMP5wfxQpxEaSFEkFc0z+O3nPuDNVTiLJdq2pavRQ+Pyjh34QkBQk/z6Og1CbtgEDnpxyCOWacx4555JbuTfncuo9zT2Xcu86ykUAiVREGAJKIGa5DrM4IpgiCSIQQKkAOo4F+WEPUkgxGzs2CAjCLOA4szH4htnzjwBmMijB1IC6DjCYlLcQqIgS6APQYTKxNC+IM6HQDCkBUKcxkcWB/HAARI0phCdxHQJMdqxQxDcGiGAWXYg80NOICGjMwnwAzpDiVS8CqdMIsCCDaQFmcYKr7c4AejOmvo49kAX4XRwuTCALOY6qQQApoFD5MSYChxEMECvmnx1BhJzoYfEGKSDHZQwmJiAEFBGACEOeiOPB/+MIZmOABBFjtJpvNBac/X8cAA==', 20, 16, ['d815286545f188d51ae2c856a6f5082c']], | |
| 314 | + 'lya' => ['UklGRmgAAABXRUJQVlA4WAoAAAAQAAAADwAACwAAQUxQSCQAAAABR6CQbQTI3/aO4IeIiBTxMMxGUhmWZVmW5fzfBxDR/4Bb8xdWUDggHgAAAHABAJ0BKhAADAACwEwllAJ0AUAAAP72JivN6Z4AAA==', 16, 12, ['34aea5dccb0104704648d56751219bbc']], | |
| 315 | + 'an' => ['UklGRtQBAABXRUJQVlA4WAoAAAASAAAADwAACwAAQU5JTQYAAAAAAAAAAgBBTk1GogAAAAAAAAAAAA8AAAsAACgAAAJWUDggigAAAFABAJ0BKhAADAACQDglsAAEdAAA/u1jqjpe1yd//xMetXZf+DXz/HzO0n/0HuFcl/EhnxTn0nP3xTxDvrfSTJaHOCrPsAzkZfKf/73SWFW+zS0Fi24G8cxE7+8Xr6jlWkP/8zQiYwzd5n+5wb3KX//1em9W3/J3X/v/+P+P+f+ftLXAlKa01l4AAEFOTUaSAAAAAAAAAQAACQAABQAAMgAAAkFMUEgTAAAAAQ9wrdKICCEQSDLYX3iGiP5HBgBWUDggXgAAAFABAJ0BKgoABgACQDglsAAEdAAA/vCixaMTQv+HtdHh7XR4e193///iY5/Aj+ii1+QeONk0LN/+qGF+r/Q18Gx+bdzX//6WH/fFx/Y3//8SF//pYf98XH9jf/w/gABBTk1GZAAAAAIAAAEAAAkAAAUAADwAAABWUDggTAAAAFQBAJ0BKgoABgAAADglsAAEdAAA/vCI1w8Uzl4f69KERUrb//bMkc3+Sn6WUnj11///Uaf9i2f+Jb5n//Ehf/6WH/cWz/wQ//D+AAA=', 16, 12, ['878e0516e278e5b3287d4fe34e020d74', 'cc6eb9ea67b8b238e6d1e7044cf59124', '832f71c95b62e156655925ddaaa6fb29']], | |
| 316 | + 'anl' => ['UklGRtIDAABXRUJQVlA4WAoAAAASAAAADwAACwAAQU5JTQYAAAAAAAAAAABBTk1GSAIAAAAAAAAAAA8AAAsAACgAAAJWUDhMLwIAAC8PwAIA5wTXtq3j1XfOd37EnW3bTqo8bkZqO9e+nd3aeg03jSRJcjhy3jX3vINwOjcCABDK2fZlY7L5017QI9pba7Jtu/mP7N+VAggPiRZCnD9nY0uZiENYUe56Az+mHi568cJxoJPx50YAgP9I523pQhmiiS7eEslAh83E8MDLXwAE+4mGHg7bQIg4zW+M4zpSVOBrr6MixkcscsFC/CYgB+hi0UWRE/hDD8AFtTGQ73CSKwSbdJKn454AAj39hwKghyxkRwizV1cdp99eRQWaAC8MBFCmtRBCQQTYQX+is0FrdS7ohnz02rIL89la7yAoaKeWGrdpPS1uiDWrqSxNsPQTgoGRJ8bvqyXZBnpieLOxlOWg9PEwFGpHop9+XRYKXxQR5yXYtAZLDqlEgLQ7thIoTAjv6rs1yrCq7mnctZUJQT77expvpuH21wiPwTG61STFmDFEfp1RlarMPRNIx+UE2Y5J8KObbp9zSP+W4f66fxos8vC74/rkOX8MQAwAsIx0tm3btm2+bdu2bWbcDBHR//j4/v/pItKVwc67HSsVifj6/Xsi4YSK8tbbgUmCQQEAAAAa+9zi5c1CXCuDswOvkyNH66OtGry5++wWOfLjSxNwOpETSEO6yHQHzCGwmBun/WvdNlhtUcnPr++vnGQ8BdvX01ute8ODs9tDDDS70hy+2C81/NF2xmBO8vUnt6vvD8szMd9Ygecqclcej+fPPl/2DqdSRrEnQAMAQU5NRqgAAAAAAAABAAAJAAAFAAAyAAACVlA4TJAAAAAvCUABEMegNpKtBHeNfkZKLfTfACVYam2ojSS1eQMRKXIdqAH1X5oYBQBYaHztZZ1ZRt3VwREkEEji3G/PQB9UXzEDhfNHQXwCSI/HhxHHRG2fsq3hTalWKAblr3vV7ogag6NBxwo6J26DE8kUFLSRpOwzM/rXuiIi+h/nzwQAwN1V8hte8SrGcdM2cxCxEwBBTk1GpgAAAAIAAAEAAAkAAAUAADwAAABWUDhMjgAAAC8JQAEAx8A2tq1EOAUwZHTg0j4dEK9Gm/k24G0oaNuG6dkFNXBjzji2rTS4QxWfAui/DYdIA1klW5n5D3uQQlMwpriI0A4+cGEaoOEmB6jsfCQjs4sfmcHJGyaDCvnjpSKafeKmPv3ocuU7KGgjSXlmxvWvdUVE9D+kDgkAAOFqZzvX5vFL5nON4td8JAA=', 16, 12, ['8dba6b986075f2fe3083ea5a7feba53a', 'b2a89c12a21cceb0aefe1858efcf0b98', '61d35682a51b88e281f8aa058aa7d441']], | |
| 317 | + ]; | |
| 318 | + foreach ($cases as $name => [$b64, $w, $h, $md5]) { | |
| 319 | + $b = (new Webp())->decode(base64_decode($b64)); | |
| 320 | + $test->assertEquals([$w, $h], [$b->width, $b->height], "{$name}の大きさ"); | |
| 321 | + $frames = $b->frames ? array_column($b->frames, 'bitmap') : [$b]; | |
| 322 | + $test->assertEquals($md5, array_map(fn($f) => md5($f->data), $frames), "{$name}の画素"); | |
| 323 | + } | |
| 324 | + $an = (new Webp())->decode(base64_decode($cases['an'][0])); | |
| 325 | + $test->assertEquals([40, 50, 60], array_column($an->frames, 'delay')); | |
| 326 | + $test->assertEquals(2, $an->loops); | |
| 327 | + }); | |
| 328 | + | |
| 329 | + $test->it('WebPを書けるはず(可逆はそのまま、非可逆は品質、アルファは正確、アニメーション)', function($test) use ($make): void { | |
| 330 | + foreach (['two', 'sixteen', 'gray', 'rgba'] as $kind) { | |
| 331 | + $b = $kind === 'rgba' ? $make($kind, 40, 20) : $make($kind); | |
| 332 | + $ll = (new Webp())->encode($b, ['lossless' => true]); | |
| 333 | + $test->assertEquals('VP8L', substr($ll, 12, 4), "{$kind}は可逆"); | |
| 334 | + $test->assertEquals($b->data, (new Webp())->decode($ll)->data, "{$kind}の画素"); | |
| 335 | + } | |
| 336 | + $g = new Bitmap(40, 20); | |
| 337 | + for ($y = 0; $y < 20; $y++) for ($x = 0; $x < 40; $x++) $g->set($x, $y, $x * 6, $y * 12, ($x + $y) * 4); | |
| 338 | + $hi = (new Webp())->encode($g, ['quality' => 95]); | |
| 339 | + $lo = (new Webp())->encode($g, ['quality' => 30]); | |
| 340 | + $test->assertTrue(strlen($lo) < strlen($hi), '品質が低いと小さい'); | |
| 341 | + $test->assertEquals('VP8 ', substr($hi, 12, 4), '不透明ならVP8だけ'); | |
| 342 | + $err = 0; | |
| 343 | + $back = (new Webp())->decode($hi); | |
| 344 | + for ($i = 0; $i < strlen($g->data); $i++) if ($i % 4 !== 3) $err += abs(ord($g->data[$i]) - ord($back->data[$i])); | |
| 345 | + $test->assertTrue($err / (40 * 20 * 3) < 2, '品質95の平均の差'); | |
| 346 | + // アルファがあればVP8XとALPH(アルファは可逆) | |
| 347 | + $t = $make('rgba', 40, 20); | |
| 348 | + $ly = (new Webp())->encode($t, ['quality' => 50]); | |
| 349 | + $test->assertEquals('VP8X', substr($ly, 12, 4)); | |
| 350 | + $back = (new Webp())->decode($ly); | |
| 351 | + $alpha = fn(string $d) => implode('', array_map(fn($i) => $d[$i], range(3, strlen($d) - 1, 4))); | |
| 352 | + $test->assertEquals($alpha($t->data), $alpha($back->data), 'アルファ'); | |
| 353 | + // アニメーション | |
| 354 | + $a = Bitmap::fill(5, 4, 255, 0, 0); | |
| 355 | + $a->frames = [ | |
| 356 | + ['bitmap' => Bitmap::fill(5, 4, 255, 0, 0), 'delay' => 100], | |
| 357 | + ['bitmap' => Bitmap::fill(5, 4, 0, 0, 255, 128), 'delay' => 250], | |
| 358 | + ]; | |
| 359 | + $a->loops = 3; | |
| 360 | + foreach ([true, false] as $lossless) { | |
| 361 | + $b = (new Webp())->decode((new Webp())->encode($a, ['lossless' => $lossless])); | |
| 362 | + $test->assertEquals([100, 250], array_column($b->frames, 'delay')); | |
| 363 | + $test->assertEquals(3, $b->loops); | |
| 364 | + $test->assertEquals(128, $b->frames[1]['bitmap']->get(2, 2)[3]); | |
| 365 | + if ($lossless) $test->assertEquals([0, 0, 255, 128], $b->frames[1]['bitmap']->get(4, 3)); | |
| 366 | + } | |
| 367 | + }); | |
| 368 | + | |
| 369 | + $test->it('壊れたWebPは例外のはず', function($test): void { | |
| 370 | + $ly = base64_decode('UklGRqYAAABXRUJQVlA4IJoAAADQBACdASoYABQAPrVOoEqnJCMhsBgIAOAWiUAWHYHlTnwr36n6ZUj8wDDu9+DCyAD+opZKvMHS233rVxNfgljnxvWSmFqCXbB3yF9HmOg8rtcoGdyLn5z6+4VxeQ1ImRbIwaoZ1v7erNwZKFXCzKxRsQdQwWWn+vJ3DfH+/zPcdEROIODEUzTDqVxI6LKvZMfVtHqlKUQ+AYQA'); | |
| 371 | + $short = substr($ly, 0, 120); | |
| 372 | + $short = substr_replace(substr_replace($short, pack('V', strlen($short) - 8), 4, 4), pack('V', strlen($short) - 20), 16, 4); | |
| 373 | + $test->assertThrows(fn() => (new Webp())->decode($short), \RuntimeException::class); | |
| 374 | + $test->assertThrows(fn() => (new Webp())->decode('RIFF'.pack('V', 17).'WEBPVP8L'.pack('V', 5)."\x2e\0\0\0\0"), \RuntimeException::class); | |
| 375 | + $test->assertThrows(fn() => (new Webp())->decode('RIFF'.pack('V', 4).'WEBP'), \RuntimeException::class); | |
| 376 | + }); | |
| 377 | + | |
| 378 | + $test->it('未対応のJPEG(算術符号)は例外のはず', function($test): void { | |
| 379 | + $sof9 = "\xff\xd8\xff\xc9".pack('n', 11)."\x08".pack('nn', 8, 8)."\x01\x01\x11\x00\xff\xd9"; | |
| 380 | + $test->assertThrows(fn() => (new Jpeg())->decode($sof9), \RuntimeException::class); | |
| 381 | + }); | |
| 382 | + | |
| 383 | + $test->it('中身で形式を判断して読めるはず', function($test) use ($make): void { | |
| 384 | + $b = $make('rgba'); | |
| 385 | + foreach (['image/png', 'image/bmp', 'image/gif'] as $type) { | |
| 386 | + $data = Image::encode($b, $type); | |
| 387 | + $test->assertEquals($type, Image::detect($data)); | |
| 388 | + if ($type !== 'image/gif') $test->assertEquals($b->data, Image::decode($data)->data); | |
| 389 | + } | |
| 390 | + $test->assertEquals('image/webp', Image::detect('RIFF'.pack('V', 100).'WEBPVP8L')); | |
| 391 | + $test->assertEquals($b->data, Image::decode(Image::encode($b, 'image/webp', ['lossless' => true]))->data); | |
| 392 | + $test->assertEquals('image/x-tga', Image::detect("\0\0\2", 'a.TGA')); | |
| 393 | + $test->assertNull(Image::detect('これは画像ではない')); | |
| 394 | + $test->assertThrows(fn() => Image::decode('これは画像ではない'), \RuntimeException::class); | |
| 395 | + }); | |
| 396 | + | |
| 397 | + $test->it('大き過ぎる画像は読まないはず', function($test): void { | |
| 398 | + $huge = "\x89PNG\r\n\x1a\n".pack('N', 13).'IHDR'.pack('NNCCCCC', 60000, 60000, 8, 6, 0, 0, 0); | |
| 399 | + $huge .= pack('N', crc32(substr($huge, 12, 17))); | |
| 400 | + $test->assertThrows(fn() => (new Png())->decode($huge), \RuntimeException::class); | |
| 106 | 401 | }); |
| 107 | 402 | }); |
| 108 | 403 |
| @@ -19,7 +19,7 @@ | ||
| 19 | 19 | <td>アイコン:</td> |
| 20 | 20 | <td> |
| 21 | 21 | <img src="{{ $e->avatarUrl }}" alt="" style="max-width: 100px; max-height: 100px;" /><br /> |
| 22 | - <input type="file" name="avatar" accept="image/png,image/jpeg,image/gif" /><br /> | |
| 22 | + <input type="file" name="avatar" accept="image/png,image/jpeg,image/gif,image/webp" /><br /> | |
| 23 | 23 | {@ if ($e->avatar !== '') @} |
| 24 | 24 | <label><input type="checkbox" name="removeAvatar" value="1" /> アイコンを削除する</label> |
| 25 | 25 | {@ endif @} |
| @@ -9,10 +9,10 @@ | ||
| 9 | 9 | <input type="text" name="cw" maxlength="200" placeholder="注意書き(任意。入力すると本文が隠れます)" value="{{ $draftCw }}" /> |
| 10 | 10 | <textarea name="content" rows="4" maxlength="{{ $maxLength }}" placeholder="いまどうしてる?">{{ $draft !== '' ? $draft : $replyText }}</textarea> |
| 11 | 11 | <details> |
| 12 | - <summary>画像・動画を添付({{ $maxMedia }}つまで。画像はPNG・JPEG・GIFで{{ \Std\Lib\Fedi\Media::MAX_BYTES >> 20 }}MiB、動画はMP4・MKV・WebMで{{ \Std\Lib\Fedi\Media::MAX_VIDEO_BYTES >> 20 }}MiBまで)</summary> | |
| 12 | + <summary>画像・動画を添付({{ $maxMedia }}つまで。画像はPNG・JPEG・GIF・WebPで{{ \Std\Lib\Fedi\Media::MAX_BYTES >> 20 }}MiB、動画はMP4・MKV・WebMで{{ \Std\Lib\Fedi\Media::MAX_VIDEO_BYTES >> 20 }}MiBまで)</summary> | |
| 13 | 13 | {@ for ($i = 0; $i < $maxMedia; $i++) @} |
| 14 | 14 | <div class="mb-file"> |
| 15 | - <input type="file" name="media[]" accept="image/png,image/jpeg,image/gif,video/mp4,video/matroska,video/webm" /> | |
| 15 | + <input type="file" name="media[]" accept="image/png,image/jpeg,image/gif,image/webp,video/mp4,video/matroska,video/webm" /> | |
| 16 | 16 | <input type="text" name="alt[]" maxlength="1500" placeholder="説明(代替テキスト)" /> |
| 17 | 17 | </div> |
| 18 | 18 | {@ endfor @} |