master
ブランチ master
LittleBeast / src / Std / Lib / Image / Webp.php
13.3 KB、302行 生 Blame 履歴
1<?php
2/*************************************************************
3# 076 License
4
5Copyright (c) テクニカル諏訪子
6
7Permission is hereby granted to any person obtaining a copy of the software
8Little Beast (the "Software") to use, modify, merge, copy, publish, distribute,
9sublicense, 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
31THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
32INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
33PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
34HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
35CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
36OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
37 */
38namespace Std\Lib\Image;
39
40/**
41 * WebP(RIFFの入れ物)
42 *
43 * 読み込み:非可逆(VP8)、可逆(VP8L)、拡張形式(VP8X)のアルファ(ALPH、無圧縮・可逆、全てのフィルター)、
44 * アニメーション(ANIM・ANMF、合成方法と廃棄方法、繰り返し回数)。
45 * 書き出し:既定は非可逆(quality、アルファがあればALPHを可逆圧縮)、lossless => trueなら可逆。framesがあればアニメーション。
46 */
47class 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}