画像ライブラリ

守矢諏訪子 <suwako@076.moe>、2026-11-11 09:24
コミット 459b5228cb860c8a79e91d053182209ee4192307 ・親 da1e704 ・ファイルを見る

16個のファイル、+6332 -225

@@ -42,13 +42,13 @@
4242 /**
4343 * アップロードされた画像と動画(投稿の添付とアイコン)
4444 *
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)を取り除く。
4747 */
4848 final class Media {
4949 public const MAX_BYTES = 20971520; // 20 MiB
5050 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'];
5252
5353 /**
5454 * $_FILESの1件を保存する
@@ -77,7 +77,7 @@
7777 if ($fh) fclose($fh);
7878 $isVideo = $type !== null && str_starts_with($type, 'video/');
7979 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の画像だけアップロード出来ます。');
8181 }
8282 if (!$isVideo && $size > self::MAX_BYTES) return \Result::Error(self::tooLarge(false));
8383
@@ -126,6 +126,7 @@
126126 if (str_starts_with($data, "\x89PNG\r\n\x1a\n")) return 'image/png';
127127 if (str_starts_with($data, "\xff\xd8\xff")) return 'image/jpeg';
128128 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';
129130 // ISO BMFF:AVIF・HEIC(画像)とQuickTime(ブラウザで再生出来ない事が多い)は除く
130131 if (substr($data, 4, 4) === 'ftyp') {
131132 $brand = substr($data, 8, 4);
@@ -151,6 +152,7 @@
151152 *
152153 * JPEG:APP1〜APP15(Exif、XMP等)とCOMを消す(APP0のJFIFは残す)
153154 * PNG:eXIf、tEXt、zTXt、iTXt、tIMEを消す
155+ * WebP:EXIFとXMPのチャンクを消す(VP8Xの印も外す)
154156 * MP4:udta、meta、uuid(位置、機種、XMP等)を同じ大きさの「free」にし、mvhd・tkhd・mdhdの日時を0にする
155157 */
156158 public static function strip(string $data, string $type): string {
@@ -192,6 +194,25 @@
192194 $pos += $total;
193195 }
194196 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;
195216 }
196217 return $data;
197218 }
@@ -38,10 +38,13 @@
3838 namespace Std\Lib;
3939
4040 use Std\Lib\Image\ImageInterface;
41+use Std\Lib\Image\Bitmap;
42+use Std\Lib\Image\Bmp;
4143 use Std\Lib\Image\Gif;
4244 use Std\Lib\Image\Jpeg;
4345 use Std\Lib\Image\Png;
4446 use Std\Lib\Image\Targa;
47+use Std\Lib\Image\Webp;
4548
4649 class RGB {
4750 public int $r;
@@ -65,11 +68,13 @@
6568 public int $bytes = 0;
6669 public \stdClass $fullInfo;
6770
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,
7378 ];
7479
7580 public function __construct(string $file) {
@@ -80,7 +85,7 @@
8085 if (!file_exists($file)) return;
8186 $fp = fopen($file, 'rb');
8287 if (!$fp) return;
83- $extBytes = fread($fp, 15);
88+ $extBytes = (string)fread($fp, 32);
8489 fclose($fp);
8590
8691 $this->bytes = filesize($file);
@@ -88,14 +93,9 @@
8893 $this->extension = pathinfo($file, PATHINFO_EXTENSION);
8994 $this->filename = str_replace('.'.$this->extension, '', array_last(explode('/', $file)));
9095
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);
9999 $this->fullInfo->type = $this->type;
100100 $this->fullInfo->size = $this->size;
101101 $this->fullInfo->bytes = $this->bytes;
@@ -105,11 +105,51 @@
105105 }
106106 }
107107
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+
108148 ///////////////
109149
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}");
113153 return new $class();
114154 }
115155
@@ -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 @@
3737 */
3838 namespace Std\Lib\Image;
3939
40-use Std\Lib\Image\ImageInterface;
41-
40+/**
41+ * GIF(87a・89a)
42+ *
43+ * 読み込み:LZW(12ビット、クリア、遅延クリア)、インターレース、全体・ローカルのパレット、透明色、
44+ * アニメーション(グラフィック制御拡張の遅延・廃棄方法、NETSCAPE2.0の繰り返し回数)。
45+ * 書き出し:256色以下はそのまま、それ以上はメディアンカットで減色(任意で誤差拡散)。framesがあればアニメーション。
46+ */
4247 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+ }
5568
5669 /**
57- * TODO: 現在、インタレースした画像が未対応です。
70+ * ブロックを読む
71+ *
72+ * @param bool $decode 画像のLZWを展開するか(falseなら大きさ等だけ)
5873 */
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+ }
63134
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+ }
71147
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;
77178
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+ // 壊れたデータ:ブラウザと同じく、そこまでを表示する
89194 break;
90195 }
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;
91206 }
207+ return $outLen >= $count ? substr($out, 0, $count) : $out;
208+ }
92209
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+ }
106233
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");
114243 }
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+ }
115258
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;
123270
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) : '');
126503 }
127504 }
@@ -38,5 +38,12 @@
3838 namespace Std\Lib\Image;
3939
4040 interface ImageInterface {
41+ /** ヘッダー等の情報(width・heightを含む) */
4142 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;
4249 }
@@ -37,54 +37,1001 @@
3737 */
3838 namespace Std\Lib\Image;
3939
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+ */
4249 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+ ];
4755
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+
4883 /**
49- * TODO: 現在、IHDRヘッダーのみを受け取る
84+ * マーカーを順に読む(SOSの後の符号化データは飛ばす)
85+ *
86+ * @param callable|null $onScan SOS毎に呼ぶ(読み込み用):fn(array $scan, int $pos): int(次の位置)
5087 */
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;
54110
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+ }
59159
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;
80623 break;
81624 }
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+ }
82635 }
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+ }
83647
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+ }
85662
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;
891036 }
901037 }
@@ -640,6 +640,443 @@
640640
641641 ///////////////
642642
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+
6431080 private function getSamplesPerPixel(int $colorType): int {
6441081 return match($colorType) {
6451082 0 => 1, // グレースケール
@@ -37,115 +37,230 @@
3737 */
3838 namespace Std\Lib\Image;
3939
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+ */
4248 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 = [
6850 1 => 'UNCOMPRESSED COLOR-MAPPED IMAGE',
6951 2 => 'UNCOMPRESSED TRUE COLOR IMAGE',
7052 3 => 'UNCOMPRESSED GRAYSCALE IMAGE',
7153 9 => 'RUN-LENGTH ENCODED COLOR-MAPPED IMAGE',
7254 10 => 'RUN-LENGTH ENCODED TRUE-COLOR IMAGE',
7355 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',
7656 ];
57+ private const FOOTER = "TRUEVISION-XFILE.\0";
7758
7859 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+ }
8181
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']);
9098
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;
97111
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+ }
108122
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'];
120129
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+ }
124143
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の画素データが足りません。');
131151 }
132152
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+ }
137168
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;
146210 }
211+ return substr($out, 0, $count * $bpp);
212+ }
147213
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;
150265 }
151266 }
@@ -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 @@
174174 $test->assertEquals('image/png', Media::detect("\x89PNG\r\n\x1a\nxxxx"));
175175 $test->assertEquals('image/jpeg', Media::detect("\xff\xd8\xff\xe0xxxx"));
176176 $test->assertEquals('image/gif', Media::detect('GIF89axxxx'));
177+ $test->assertEquals('image/webp', Media::detect('RIFF'.pack('V', 100).'WEBPVP8 '));
177178 $test->assertNull(Media::detect('<?php echo 1;'));
178179 $test->assertEquals('video/mp4', Media::detect("\0\0\0\x20ftypisom\0\0\x02\0"));
179180 $test->assertEquals('video/mp4', Media::detect("\0\0\0\x18ftypmp42\0\0\0\0"));
@@ -211,7 +212,7 @@
211212 $test->assertEquals(strpos($mp4, $mdat), strpos($out, $mdat));
212213 });
213214
214- $test->it('JPEGとPNGのメタデータを取り除くはず', function($test): void {
215+ $test->it('JPEG・PNG・WebPのメタデータを取り除くはず', function($test): void {
215216 $exif = "\xff\xe1".pack('n', 12).'Exif'."\0\0GPS!";
216217 $jfif = "\xff\xe0".pack('n', 6).'JFIF';
217218 $com = "\xff\xfe".pack('n', 6).'note';
@@ -230,6 +231,15 @@
230231 $test->assertFalse(str_contains($out, 'eXIf'));
231232 $test->assertStringContains('IDAT', $out);
232233 $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]);
233243 });
234244 });
235245
@@ -41,6 +41,7 @@
4141
4242 use Std\Lib\Tester;
4343 use Std\Lib\Image;
44+use Std\Lib\Image\{Bitmap, Png, Bmp, Targa, Gif, Jpeg, Webp};
4445
4546 $test = new Tester([
4647 'colorOutput' => true,
@@ -62,6 +63,7 @@
6263 file_put_contents("{$dir}/a.jpg", "\xff\xd8\xff\xe0".pack('n', 16)."JFIF\0\x01\x01\x00\x00\x01\x00\x01\x00\x00"
6364 ."\xff\xc0".pack('n', 17)."\x08".pack('nn', 6, 7)."\x03\x01\x22\x00\x02\x11\x01\x03\x11\x01\xff\xd9");
6465 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=='));
6567 file_put_contents("{$dir}/fake.png", 'これは画像ではない');
6668 file_put_contents("{$dir}/big.png", "\x89PNG\r\n\x1a\n".str_repeat("\0", 3000));
6769
@@ -77,13 +79,14 @@
7779 $test->assertEquals('image/png', $img->fullInfo->type);
7880 });
7981
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]) {
8284 $img = new Image("{$rel}/{$f}");
8385 $test->assertEquals($type, $img->type, "{$f}の種類");
8486 $test->assertEquals($w, $img->fullInfo->width, "{$f}の横");
8587 $test->assertEquals($h, $img->fullInfo->height, "{$f}の丈");
8688 }
89+ $test->assertEquals('VP8L', (new Image("{$rel}/a.webp"))->fullInfo->compression);
8790 });
8891
8992 $test->it('大きさを読み易い形式にするはず', function($test) use ($rel): void {
@@ -103,6 +106,298 @@
103106 $test->it('未対応の形式は例外のはず', function($test) use ($rel): void {
104107 // 拡張子ではなく中身で判断する
105108 $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);
106401 });
107402 });
108403
@@ -19,7 +19,7 @@
1919 <td>アイコン:</td>
2020 <td>
2121 <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 />
2323 {@ if ($e->avatar !== '') @}
2424 <label><input type="checkbox" name="removeAvatar" value="1" /> アイコンを削除する</label>
2525 {@ endif @}
@@ -9,10 +9,10 @@
99 <input type="text" name="cw" maxlength="200" placeholder="注意書き(任意。入力すると本文が隠れます)" value="{{ $draftCw }}" />
1010 <textarea name="content" rows="4" maxlength="{{ $maxLength }}" placeholder="いまどうしてる?">{{ $draft !== '' ? $draft : $replyText }}</textarea>
1111 <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>
1313 {@ for ($i = 0; $i < $maxMedia; $i++) @}
1414 <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" />
1616 <input type="text" name="alt[]" maxlength="1500" placeholder="説明(代替テキスト)" />
1717 </div>
1818 {@ endfor @}