master
ブランチ master
LittleBeast / src / Std / Lib / Image / Jpeg.php
39.7 KB、1037行 生 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 * 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 */
49class Jpeg implements ImageInterface {
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 ];
55
56 // ビットを読む状態
57 private string $d = '';
58 private int $p = 0;
59 private int $buf = 0;
60 private int $bits = 0;
61 private int $len = 0;
62
63 private static ?array $cos = null;
64
65 public function parse(string $file): \stdClass {
66 $d = (string)file_get_contents($file);
67 $info = $this->headers($d);
68 $f = $info['frame'];
69 if ($f === null) throw new \RuntimeException('JPEGのフレームがありません。');
70 $rotated = in_array($info['orientation'], [5, 6, 7, 8], true);
71 return (object)[
72 'width' => $rotated ? $f['h'] : $f['w'],
73 'height' => $rotated ? $f['w'] : $f['h'],
74 'bpp' => $f['precision'],
75 'components' => count($f['comps']),
76 'progressive' => $f['type'] === 2,
77 'sampling' => implode(',', array_map(fn($c) => "{$c['h']}x{$c['v']}", $f['comps'])),
78 'orientation' => $info['orientation'],
79 'adobe' => $info['adobe'],
80 ];
81 }
82
83 /**
84 * マーカーを順に読む(SOSの後の符号化データは飛ばす)
85 *
86 * @param callable|null $onScan SOS毎に呼ぶ(読み込み用):fn(array $scan, int $pos): int(次の位置)
87 */
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;
110
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 }
159
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;
623 break;
624 }
625 if ($zero) {
626 $d = $f[$b] * $K[0][0];
627 for ($x = 0; $x < 8; $x++) $t[$b + $x] = $d;
628 continue;
629 }
630 for ($x = 0; $x < 8; $x++) {
631 $s = 0.0;
632 for ($u = 0; $u < 8; $u++) if ($f[$b + $u] !== 0) $s += $f[$b + $u] * $K[$u][$x];
633 $t[$b + $x] = $s;
634 }
635 }
636 // 列
637 for ($y = 0; $y < 8; $y++) {
638 $o = $off + $y * $stride;
639 for ($x = 0; $x < 8; $x++) {
640 $s = 0.0;
641 for ($v = 0; $v < 8; $v++) $s += $t[$v * 8 + $x] * $K[$v][$y];
642 $p = (int)round($s + 128);
643 $plane[$o + $x] = chr($p < 0 ? 0 : ($p > 255 ? 255 : $p));
644 }
645 }
646 }
647
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 }
662
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;
1036 }
1037}