自然な順 */ private const ZIGZAG = [ 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, 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, ]; // ビットを読む状態 private string $d = ''; private int $p = 0; private int $buf = 0; private int $bits = 0; private int $len = 0; private static ?array $cos = null; public function parse(string $file): \stdClass { $d = (string)file_get_contents($file); $info = $this->headers($d); $f = $info['frame']; if ($f === null) throw new \RuntimeException('JPEGのフレームがありません。'); $rotated = in_array($info['orientation'], [5, 6, 7, 8], true); return (object)[ 'width' => $rotated ? $f['h'] : $f['w'], 'height' => $rotated ? $f['w'] : $f['h'], 'bpp' => $f['precision'], 'components' => count($f['comps']), 'progressive' => $f['type'] === 2, 'sampling' => implode(',', array_map(fn($c) => "{$c['h']}x{$c['v']}", $f['comps'])), 'orientation' => $info['orientation'], 'adobe' => $info['adobe'], ]; } /** * マーカーを順に読む(SOSの後の符号化データは飛ばす) * * @param callable|null $onScan SOS毎に呼ぶ(読み込み用):fn(array $scan, int $pos): int(次の位置) */ private function headers(string $d, ?callable $onScan = null, array &$tables = []): array { if (!str_starts_with($d, "\xff\xd8")) throw new \RuntimeException('JPEGではありません。'); $len = strlen($d); $pos = 2; $info = ['frame' => null, 'orientation' => 1, 'adobe' => null, 'jfif' => false, 'restart' => 0]; $tables += ['q' => [], 'dc' => [], 'ac' => []]; while ($pos < $len) { // マーカーを探す(埋め草の0xFFは飛ばす) if ($d[$pos] !== "\xff") { $pos++; continue; } while ($pos < $len && $d[$pos] === "\xff") $pos++; if ($pos >= $len) break; $m = ord($d[$pos++]); // 0x00は符号化データの中の0xFF、RSTnと0x01は長さが無い if ($m === 0x00 || $m === 0xd8 || ($m >= 0xd0 && $m <= 0xd7) || $m === 0x01) continue; if ($m === 0xd9) break; if ($pos + 2 > $len) throw new \RuntimeException('JPEGのマーカーが途中で切れています。'); $segLen = unpack('n', substr($d, $pos, 2))[1]; $seg = substr($d, $pos + 2, $segLen - 2); $next = $pos + $segLen; if ($m === 0xc0 || $m === 0xc1 || $m === 0xc2) { $f = unpack('Cprecision/nh/nw/Cn', $seg); if ($f['precision'] !== 8) throw new \RuntimeException('12ビットのJPEGは未対応です。'); if ($f['h'] === 0) throw new \RuntimeException('DNLで丈を決めるJPEGは未対応です。'); Bitmap::check($f['w'], $f['h']); $comps = []; for ($i = 0; $i < $f['n']; $i++) { $c = unpack('Cid/Csamp/Ctq', substr($seg, 6 + $i * 3, 3)); $h = $c['samp'] >> 4; $v = $c['samp'] & 15; if ($h < 1 || $h > 4 || $v < 1 || $v > 4) throw new \RuntimeException('JPEGのサンプリング係数が不正です。'); $comps[] = ['id' => $c['id'], 'h' => $h, 'v' => $v, 'tq' => $c['tq']]; } if (!in_array(count($comps), [1, 3, 4], true)) throw new \RuntimeException('未対応のJPEGの色の数です('.count($comps).')。'); $info['frame'] = ['type' => $m - 0xc0, 'precision' => $f['precision'], 'w' => $f['w'], 'h' => $f['h'], 'comps' => $comps]; } else if (($m >= 0xc3 && $m <= 0xcf) && $m !== 0xc4 && $m !== 0xc8 && $m !== 0xcc) { throw new \RuntimeException('算術符号・ロスレス・階層のJPEGは未対応です。'); } else if ($m === 0xc4) { $tables = $this->dht($seg, $tables); } else if ($m === 0xdb) { $tables = $this->dqt($seg, $tables); } else if ($m === 0xdd) { $info['restart'] = unpack('n', $seg)[1]; } else if ($m === 0xe0 && str_starts_with($seg, "JFIF\0")) { $info['jfif'] = true; } else if ($m === 0xe1 && str_starts_with($seg, "Exif\0\0")) { $info['orientation'] = $this->orientation(substr($seg, 6)); } else if ($m === 0xee && str_starts_with($seg, 'Adobe') && strlen($seg) >= 12) { $info['adobe'] = ord($seg[11]); } else if ($m === 0xda) { if ($onScan === null) { $pos = $next; continue; } $n = ord($seg[0]); $scan = ['comps' => []]; 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]; $scan['ss'] = ord($seg[1 + $n * 2]); $scan['se'] = ord($seg[2 + $n * 2]); $scan['ah'] = ord($seg[3 + $n * 2]) >> 4; $scan['al'] = ord($seg[3 + $n * 2]) & 15; $pos = $onScan($scan, $next, $info, $tables); continue; } $pos = $next; } return $info; } private function dqt(string $s, array $t): array { $p = 0; while ($p < strlen($s)) { $pq = ord($s[$p]) >> 4; $id = ord($s[$p]) & 15; $p++; $q = $pq ? array_values(unpack('n64', substr($s, $p, 128))) : array_values(unpack('C64', substr($s, $p, 64))); $p += $pq ? 128 : 64; $t['q'][$id] = $q; // ジグザグの順 } return $t; } private function dht(string $s, array $t): array { $p = 0; while ($p + 17 <= strlen($s)) { $cls = ord($s[$p]) >> 4; $id = ord($s[$p]) & 15; $counts = array_values(unpack('C16', substr($s, $p + 1, 16))); $p += 17; $n = array_sum($counts); $syms = array_values(unpack("C{$n}", substr($s, $p, $n))); $p += $n; $t[$cls ? 'ac' : 'dc'][$id] = self::huffTable($counts, $syms); } return $t; } /** * ハフマン表(9ビットの先読み表と、長い符号用のmaxcode等) */ private static function huffTable(array $counts, array $syms): array { $look = array_fill(0, 512, -1); $maxcode = array_fill(0, 18, -1); $mincode = $valptr = array_fill(0, 18, 0); $code = $k = 0; for ($l = 1; $l <= 16; $l++) { $mincode[$l] = $code; $valptr[$l] = $k; for ($i = 0; $i < $counts[$l - 1]; $i++) { if ($l <= 9) { $shift = 9 - $l; $base = $code << $shift; for ($j = 0; $j < (1 << $shift); $j++) $look[$base + $j] = ($l << 8) | $syms[$k]; } $code++; $k++; } $maxcode[$l] = $counts[$l - 1] ? $code - 1 : -1; $code <<= 1; } return ['look' => $look, 'max' => $maxcode, 'min' => $mincode, 'ptr' => $valptr, 'syms' => $syms]; } /** * Exif(TIFF)のIFD0から向き(0x0112)を読む */ private function orientation(string $t): int { if (strlen($t) < 8) return 1; $le = substr($t, 0, 2) === 'II'; $u16 = fn($o) => unpack($le ? 'v' : 'n', substr($t, $o, 2))[1] ?? 0; $u32 = fn($o) => unpack($le ? 'V' : 'N', substr($t, $o, 4))[1] ?? 0; $ifd = $u32(4); if ($ifd + 2 > strlen($t)) return 1; $n = $u16($ifd); for ($i = 0; $i < $n; $i++) { $e = $ifd + 2 + $i * 12; if ($e + 12 > strlen($t)) break; if ($u16($e) === 0x0112) { $v = $u16($e + 8); return $v >= 1 && $v <= 8 ? $v : 1; } } return 1; } //////// ビットを読む private function fill(): void { // 使い終わったビットを捨ててから足す(64ビットを超えない様に) $this->buf &= (1 << $this->bits) - 1; while ($this->bits <= 48) { $byte = 0; if ($this->p < $this->len) { $byte = ord($this->d[$this->p]); if ($byte === 0xff) { $nx = ord($this->d[$this->p + 1] ?? "\0"); if ($nx === 0) { $this->p += 2; } else { // マーカー:進まずに0を足す $byte = 0; } } else { $this->p++; } } $this->buf = ($this->buf << 8) | $byte; $this->bits += 8; } } private function getBits(int $n): int { if ($n === 0) return 0; if ($this->bits < $n) $this->fill(); $this->bits -= $n; return ($this->buf >> $this->bits) & ((1 << $n) - 1); } private function extend(int $v, int $s): int { return $v < (1 << ($s - 1)) ? $v - (1 << $s) + 1 : $v; } private function huff(array $t): int { if ($this->bits < 16) $this->fill(); $e = $t['look'][($this->buf >> ($this->bits - 9)) & 511]; if ($e >= 0) { $this->bits -= $e >> 8; return $e & 255; } for ($l = 10; $l <= 16; $l++) { $code = ($this->buf >> ($this->bits - $l)) & ((1 << $l) - 1); if ($code <= $t['max'][$l]) { $this->bits -= $l; return $t['syms'][$t['ptr'][$l] + $code - $t['min'][$l]] ?? 0; } } throw new \RuntimeException('JPEGのハフマン符号が不正です。'); } /** リスタートマーカーを飛ばして、ビットを捨てる */ private function restart(): void { $this->buf = $this->bits = 0; while ($this->p + 1 < $this->len && !($this->d[$this->p] === "\xff" && ($m = ord($this->d[$this->p + 1])) >= 0xd0 && $m <= 0xd7)) { // 符号化データの残り(壊れたファイル)を飛ばす if ($this->d[$this->p] === "\xff" && ord($this->d[$this->p + 1]) !== 0) break; $this->p++; } if ($this->p + 1 < $this->len && $this->d[$this->p] === "\xff" && ($m = ord($this->d[$this->p + 1])) >= 0xd0 && $m <= 0xd7) $this->p += 2; } //////// 読み込み public function decode(string $data): Bitmap { $this->d = $data; $this->len = strlen($data); $frame = null; $planes = []; $coefs = []; $layout = null; $tables = []; $scans = 0; $info = $this->headers($data, function(array $scan, int $pos, array $info, array $tables) use (&$frame, &$planes, &$coefs, &$layout, &$scans): int { if ($info['frame'] === null) throw new \RuntimeException('SOFの前にSOSがあります。'); if ($layout === null) { $frame = $info['frame']; $layout = $this->layout($frame); foreach ($layout['comps'] as $i => $c) { // プログレッシブは全てのスキャンが終わるまで係数を保つ(16ビットの文字列:配列の1/8のメモリ) if ($frame['type'] === 2) $coefs[$i] = str_repeat("\0", $c['bw'] * $c['bh'] * 128); else $planes[$i] = str_repeat("\x80", $c['bw'] * $c['bh'] * 64); } } $scans++; $this->p = $pos; $this->buf = $this->bits = 0; $this->scan($scan, $frame, $layout, $info, $tables, $planes, $coefs); // 次のマーカー(RST以外)まで進む $p = $this->p; while ($p + 1 < $this->len) { if ($this->d[$p] === "\xff") { $m = ord($this->d[$p + 1]); if ($m !== 0 && $m !== 0xff && ($m < 0xd0 || $m > 0xd7)) break; } $p++; } return $p; }, $tables); if ($layout === null || $scans === 0) throw new \RuntimeException('JPEGに画像データがありません。'); // プログレッシブ:最後に逆量子化と逆DCT if ($frame['type'] === 2) { foreach ($layout['comps'] as $i => $c) { $q = $tables['q'][$c['tq']] ?? null; if ($q === null) throw new \RuntimeException('量子化表がありません。'); $qn = array_fill(0, 64, 0); foreach (self::ZIGZAG as $k => $z) $qn[$z] = $q[$k]; $planes[$i] = str_repeat("\x80", $c['bw'] * $c['bh'] * 64); $stride = $c['bw'] * 8; $co = $coefs[$i]; for ($by = 0; $by < $c['bh']; $by++) { for ($bx = 0; $bx < $c['bw']; $bx++) { $raw = unpack('v64', $co, ($by * $c['bw'] + $bx) * 128); $blk = []; for ($k = 0; $k < 64; $k++) { $v = $raw[$k + 1]; $blk[$k] = ($v >= 32768 ? $v - 65536 : $v) * $qn[$k]; } $this->idct($blk, $planes[$i], $by * 8 * $stride + $bx * 8, $stride); } } unset($coefs[$i]); } } $img = $this->toRgba($frame, $layout, $planes, $info); $this->d = ''; return $this->orient($img, $info['orientation']); } /** * 色毎のブロックの数等 */ private function layout(array $f): array { $hmax = max(array_column($f['comps'], 'h')); $vmax = max(array_column($f['comps'], 'v')); $mcux = intdiv($f['w'] + 8 * $hmax - 1, 8 * $hmax); $mcuy = intdiv($f['h'] + 8 * $vmax - 1, 8 * $vmax); $comps = []; foreach ($f['comps'] as $c) { $comps[] = $c + [ 'bw' => $mcux * $c['h'], 'bh' => $mcuy * $c['v'], // 1色だけのスキャンで使うブロック(画像の大きさまで) 'cw' => intdiv(intdiv($f['w'] * $c['h'] + $hmax - 1, $hmax) + 7, 8), 'ch' => intdiv(intdiv($f['h'] * $c['v'] + $vmax - 1, $vmax) + 7, 8), ]; } return ['hmax' => $hmax, 'vmax' => $vmax, 'mcux' => $mcux, 'mcuy' => $mcuy, 'comps' => $comps]; } /** * 1つのスキャンを読む */ private function scan(array $scan, array $frame, array $layout, array $info, array $tables, array &$planes, array &$coefs): void { $progressive = $frame['type'] === 2; $idx = []; foreach ($scan['comps'] as $sc) { $found = null; foreach ($layout['comps'] as $i => $c) if ($c['id'] === $sc['id']) $found = $i; if ($found === null) throw new \RuntimeException('スキャンの色が不明です。'); $idx[] = [$found, $sc]; } $pred = array_fill(0, count($layout['comps']), 0); $eobrun = 0; $restart = $info['restart']; // 量子化表(ベースライン用、ジグザグの順) $qt = []; if (!$progressive) { foreach ($idx as [$i]) { $q = $tables['q'][$layout['comps'][$i]['tq']] ?? null; if ($q === null) throw new \RuntimeException('量子化表がありません。'); $qt[$i] = $q; } } $decodeBlock = function(int $ci, array $sc, int $bx, int $by) use (&$pred, &$eobrun, $progressive, $scan, $tables, $layout, &$planes, &$coefs, $qt): void { $c = $layout['comps'][$ci]; $dc = $tables['dc'][$sc['td']] ?? null; $ac = $tables['ac'][$sc['ta']] ?? null; if (!$progressive) { if ($dc === null || $ac === null) throw new \RuntimeException('ハフマン表がありません。'); $q = $qt[$ci]; $blk = array_fill(0, 64, 0); $t = $this->huff($dc); $pred[$ci] += $t ? $this->extend($this->getBits($t), $t) : 0; $blk[0] = $pred[$ci] * $q[0]; for ($k = 1; $k < 64;) { $rs = $this->huff($ac); $r = $rs >> 4; $s = $rs & 15; if ($s === 0) { if ($r !== 15) break; $k += 16; continue; } $k += $r; if ($k > 63) break; $blk[self::ZIGZAG[$k]] = $this->extend($this->getBits($s), $s) * $q[$k]; $k++; } $stride = $c['bw'] * 8; $this->idct($blk, $planes[$ci], $by * 8 * $stride + $bx * 8, $stride); return; } // 係数は16ビット(リトルエンディアン):$o+自然な順の位置×2 // (クロージャで$coを値で取ると、書く度に文字列全体が複写されるので、ここで直接読み書きする) $o = ($by * $c['bw'] + $bx) * 128; $co = &$coefs[$ci]; $ss = $scan['ss']; $se = $scan['se']; $al = $scan['al']; if ($ss === 0) { // DC if ($scan['ah'] === 0) { $t = $this->huff($dc); $pred[$ci] += $t ? $this->extend($this->getBits($t), $t) : 0; $v = $pred[$ci] * (1 << $al); } else if ($this->getBits(1)) { $v = ord($co[$o]) | (ord($co[$o + 1]) << 8) | (1 << $al); } else { return; } $co[$o] = chr($v & 255); $co[$o + 1] = chr(($v >> 8) & 255); return; } if ($scan['ah'] === 0) { // AC:最初 if ($eobrun > 0) { $eobrun--; return; } for ($k = $ss; $k <= $se; $k++) { $rs = $this->huff($ac); $r = $rs >> 4; $s = $rs & 15; if ($s === 0) { if ($r < 15) { $eobrun = (1 << $r) - 1; if ($r) $eobrun += $this->getBits($r); break; } $k += 15; continue; } $k += $r; if ($k > 63) break; $v = $this->extend($this->getBits($s), $s) * (1 << $al); $z = $o + self::ZIGZAG[$k] * 2; $co[$z] = chr($v & 255); $co[$z + 1] = chr(($v >> 8) & 255); } return; } // AC:精度を上げる(libjpegのdecode_mcu_AC_refineと同じ) $p1 = 1 << $al; $m1 = -1 << $al; $k = $ss; if ($eobrun <= 0) { for (; $k <= $se; $k++) { $rs = $this->huff($ac); $r = $rs >> 4; $s = $rs & 15; if ($s) { $s = $this->getBits(1) ? $p1 : $m1; } else if ($r !== 15) { $eobrun = 1 << $r; if ($r) $eobrun += $this->getBits($r); break; } do { $z = $o + self::ZIGZAG[$k] * 2; $v = ord($co[$z]) | (ord($co[$z + 1]) << 8); if ($v !== 0) { if ($this->getBits(1) && ($v & $p1) === 0) { if ($v >= 32768) $v -= 65536; $v += $v >= 0 ? $p1 : $m1; $co[$z] = chr($v & 255); $co[$z + 1] = chr(($v >> 8) & 255); } } else if (--$r < 0) { break; } $k++; } while ($k <= $se); if ($s && $k <= 63) { $z = $o + self::ZIGZAG[$k] * 2; $co[$z] = chr($s & 255); $co[$z + 1] = chr(($s >> 8) & 255); } } } if ($eobrun > 0) { for (; $k <= $se; $k++) { $z = $o + self::ZIGZAG[$k] * 2; $v = ord($co[$z]) | (ord($co[$z + 1]) << 8); if ($v !== 0 && $this->getBits(1) && ($v & $p1) === 0) { if ($v >= 32768) $v -= 65536; $v += $v >= 0 ? $p1 : $m1; $co[$z] = chr($v & 255); $co[$z + 1] = chr(($v >> 8) & 255); } } $eobrun--; } }; $mcu = 0; $next = function() use (&$mcu, $restart, &$pred, &$eobrun): void { $mcu++; if ($restart > 0 && $mcu % $restart === 0) { $this->restart(); $pred = array_fill(0, count($pred), 0); $eobrun = 0; } }; if (count($idx) === 1) { // 1色だけ:ブロック毎がMCU [$ci, $sc] = $idx[0]; $c = $layout['comps'][$ci]; for ($by = 0; $by < $c['ch']; $by++) { for ($bx = 0; $bx < $c['cw']; $bx++) { $decodeBlock($ci, $sc, $bx, $by); $next(); } } return; } for ($my = 0; $my < $layout['mcuy']; $my++) { for ($mx = 0; $mx < $layout['mcux']; $mx++) { foreach ($idx as [$ci, $sc]) { $c = $layout['comps'][$ci]; for ($v = 0; $v < $c['v']; $v++) { for ($h = 0; $h < $c['h']; $h++) $decodeBlock($ci, $sc, $mx * $c['h'] + $h, $my * $c['v'] + $v); } } $next(); } } } /** 逆DCT・DCTの係数:$cos[u][x] = C(u)/2 × cos((2x+1)uπ/16) */ private static function cosTable(): array { if (self::$cos === null) { for ($u = 0; $u < 8; $u++) { 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); } } return self::$cos; } /** * 8x8の逆DCT(係数は自然な順、逆量子化済み)。結果に128を足して面に書く */ private function idct(array $f, string &$plane, int $off, int $stride): void { // 交流成分が無いブロック(多い)は1色 $ac = false; for ($k = 1; $k < 64; $k++) if ($f[$k] !== 0) { $ac = true; break; } if (!$ac) { $v = (int)round($f[0] / 8 + 128); $ch = chr($v < 0 ? 0 : ($v > 255 ? 255 : $v)); // その場で書く(substr_replaceは面の全部を複写するので遅い) for ($y = 0; $y < 8; $y++) { $o = $off + $y * $stride; for ($x = 0; $x < 8; $x++) $plane[$o + $x] = $ch; } return; } $K = self::cosTable(); // 行(v毎に、xについて) $t = []; for ($v = 0; $v < 8; $v++) { $b = $v * 8; $zero = true; for ($u = 1; $u < 8; $u++) if ($f[$b + $u] !== 0) { $zero = false; break; } if ($zero) { $d = $f[$b] * $K[0][0]; for ($x = 0; $x < 8; $x++) $t[$b + $x] = $d; continue; } for ($x = 0; $x < 8; $x++) { $s = 0.0; for ($u = 0; $u < 8; $u++) if ($f[$b + $u] !== 0) $s += $f[$b + $u] * $K[$u][$x]; $t[$b + $x] = $s; } } // 列 for ($y = 0; $y < 8; $y++) { $o = $off + $y * $stride; for ($x = 0; $x < 8; $x++) { $s = 0.0; for ($v = 0; $v < 8; $v++) $s += $t[$v * 8 + $x] * $K[$v][$y]; $p = (int)round($s + 128); $plane[$o + $x] = chr($p < 0 ? 0 : ($p > 255 ? 255 : $p)); } } } /** * 色の面をRGBAにする(色差の拡大はlibjpegの「fancy upsampling」、色の変換もlibjpegと同じ整数の計算) * メモリを使わない様に、1行ずつ処理する */ private function toRgba(array $frame, array $layout, array $planes, array $info): Bitmap { $w = $frame['w']; $h = $frame['h']; $n = count($layout['comps']); $src = []; foreach ($layout['comps'] as $i => $c) { $sx = intdiv($layout['hmax'], $c['h']); $sy = intdiv($layout['vmax'], $c['v']); $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]; } // 色の変換 $transform = $info['adobe']; $ids = array_column($layout['comps'], 'id'); if ($n === 3) { // Adobeの変換0、又はJFIF無しで成分IDが「R」「G」「B」ならRGB $ycc = !($transform === 0 || (!$info['jfif'] && $transform === null && $ids === [0x52, 0x47, 0x42])); } else { $ycc = $n === 4 && $transform === 2; } static $tab = null; if ($tab === null) { // libjpegのjdcolor.c(SCALEBITS 16) $tab = ['rcr' => [], 'bcb' => [], 'gcr' => [], 'gcb' => []]; for ($i = 0; $i < 256; $i++) { $x = $i - 128; $tab['rcr'][$i] = (91881 * $x + 32768) >> 16; $tab['bcb'][$i] = (116130 * $x + 32768) >> 16; $tab['gcr'][$i] = -46802 * $x; $tab['gcb'][$i] = -22554 * $x + 32768; } } $rcr = $tab['rcr']; $bcb = $tab['bcb']; $gcr = $tab['gcr']; $gcb = $tab['gcb']; $out = ''; for ($y = 0; $y < $h; $y++) { if ($n === 1) { $s = $src[0]; $out .= preg_replace('/(.)/s', '$1$1$1'."\xff", substr($s['plane'], $y * $s['pw'], $w)); continue; } $c0 = $this->upsampleRow($src[0], $y, $w); $c1 = $this->upsampleRow($src[1], $y, $w); $c2 = $this->upsampleRow($src[2], $y, $w); $c3 = $n === 4 ? $this->upsampleRow($src[3], $y, $w) : null; $line = []; for ($x = 0; $x < $w; $x++) { $a = $c0[$x]; $b = $c1[$x]; $c = $c2[$x]; if ($ycc) { $r = $a + $rcr[$c]; $g = $a + (($gcb[$b] + $gcr[$c]) >> 16); $bl = $a + $bcb[$b]; $r = $r < 0 ? 0 : ($r > 255 ? 255 : $r); $g = $g < 0 ? 0 : ($g > 255 ? 255 : $g); $bl = $bl < 0 ? 0 : ($bl > 255 ? 255 : $bl); } else { $r = $a; $g = $b; $bl = $c; } if ($n === 4) { // CMYK(Adobeは反転して保存する):R = C × K ÷ 255 $k = $c3[$x]; if ($transform === null) { $r = 255 - $r; $g = 255 - $g; $bl = 255 - $bl; $k = 255 - $k; } $r = intdiv($r * $k + 127, 255); $g = intdiv($g * $k + 127, 255); $bl = intdiv($bl * $k + 127, 255); } $line[] = $r; $line[] = $g; $line[] = $bl; $line[] = 255; } $out .= pack('C*', ...$line); } return new Bitmap($w, $h, $out); } /** * 色の面の1行を画像の幅にする(2倍はlibjpegの三角フィルター、それ以外は繰り返し) * * @return array 整数の配列(幅$w) */ private function upsampleRow(array $s, int $y, int $w): array { $sx = $s['sx']; $sy = $s['sy']; $row = fn(int $r) => array_values(unpack('C*', substr($s['plane'], max(0, min($s['rows'] - 1, $r)) * $s['pw'], $s['cols']))); if ($sy === 2 && ($sx === 1 || $sx === 2)) { // 縦:近い行×3+隣の行(上の出力行は上の行、下の出力行は下の行) $cur = $row($y >> 1); $near = $row(($y >> 1) + ($y & 1 ? 1 : -1)); $cols = count($cur); if ($sx === 1) { $line = []; for ($x = 0; $x < $w; $x++) $line[] = ($cur[$x] * 3 + $near[$x] + 1 + ($y & 1)) >> 2; return $line; } $sum = []; for ($x = 0; $x < $cols; $x++) $sum[$x] = $cur[$x] * 3 + $near[$x]; $line = []; for ($x = 0; $x < $cols; $x++) { $prev = $x > 0 ? $sum[$x - 1] : $sum[$x]; $nxt = $x < $cols - 1 ? $sum[$x + 1] : $sum[$x]; $line[] = ($sum[$x] * 3 + $prev + 8) >> 4; $line[] = ($sum[$x] * 3 + $nxt + 7) >> 4; } return array_slice($line, 0, $w); } $cur = $row(intdiv($y, $sy)); if ($sx === 1) return array_slice($cur, 0, $w); $cols = count($cur); $line = []; if ($sx === 2) { for ($x = 0; $x < $cols; $x++) { $prev = $x > 0 ? $cur[$x - 1] : $cur[$x]; $nxt = $x < $cols - 1 ? $cur[$x + 1] : $cur[$x]; $line[] = ($cur[$x] * 3 + $prev + 1) >> 2; $line[] = ($cur[$x] * 3 + $nxt + 2) >> 2; } return array_slice($line, 0, $w); } for ($x = 0; $x < $w; $x++) $line[] = $cur[intdiv($x, $sx)]; return $line; } /** * Exifの向きを適用する */ private function orient(Bitmap $img, int $o): Bitmap { if ($o <= 1 || $o > 8) return $img; $w = $img->width; $h = $img->height; $px = str_split($img->data, 4); $swap = $o >= 5; $nw = $swap ? $h : $w; $nh = $swap ? $w : $h; $out = array_fill(0, $nw * $nh, ''); for ($y = 0; $y < $h; $y++) { for ($x = 0; $x < $w; $x++) { [$nx, $ny] = match ($o) { 2 => [$w - 1 - $x, $y], 3 => [$w - 1 - $x, $h - 1 - $y], 4 => [$x, $h - 1 - $y], 5 => [$y, $x], 6 => [$h - 1 - $y, $x], 7 => [$h - 1 - $y, $w - 1 - $x], 8 => [$y, $w - 1 - $x], }; $out[$ny * $nw + $nx] = $px[$y * $w + $x]; } } return new Bitmap($nw, $nh, implode('', $out)); } //////// 書き出し /** 標準の量子化表(JPEGの附属書K、自然な順):輝度、色差 */ private const QT = [ [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, 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], [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, 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], ]; /** 標準のハフマン表(附属書K.3):[符号長毎の数, 記号] */ private const HT = [ '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]], '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]], 'ac0' => [[0, 2, 1, 3, 3, 2, 4, 3, 5, 5, 4, 4, 0, 0, 1, 0x7d], [ 0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, 0x21, 0x31, 0x41, 0x06, 0x13, 0x51, 0x61, 0x07, 0x22, 0x71, 0x14, 0x32, 0x81, 0x91, 0xa1, 0x08, 0x23, 0x42, 0xb1, 0xc1, 0x15, 0x52, 0xd1, 0xf0, 0x24, 0x33, 0x62, 0x72, 0x82, 0x09, 0x0a, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xe1, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa]], 'ac1' => [[0, 2, 1, 2, 4, 4, 3, 4, 7, 5, 4, 4, 0, 1, 2, 0x77], [ 0x00, 0x01, 0x02, 0x03, 0x11, 0x04, 0x05, 0x21, 0x31, 0x06, 0x12, 0x41, 0x51, 0x07, 0x61, 0x71, 0x13, 0x22, 0x32, 0x81, 0x08, 0x14, 0x42, 0x91, 0xa1, 0xb1, 0xc1, 0x09, 0x23, 0x33, 0x52, 0xf0, 0x15, 0x62, 0x72, 0xd1, 0x0a, 0x16, 0x24, 0x34, 0xe1, 0x25, 0xf1, 0x17, 0x18, 0x19, 0x1a, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa]], ]; private string $out = ''; private int $obuf = 0; private int $obits = 0; /** * 画素をJPEG(ベースライン)にする * * 透明な部分は背景色(既定は白)と合成する。灰色だけの画像はグレースケールにする。 * * @param array $opt quality:1~100(既定90)、subsampling:'420'(既定)又は'444'、background:[r, g, b] */ public function encode(Bitmap $img, array $opt = []): string { $quality = max(1, min(100, (int)($opt['quality'] ?? 90))); $scale = $quality < 50 ? intdiv(5000, $quality) : 200 - $quality * 2; $qt = []; foreach (self::QT as $i => $base) $qt[$i] = array_map(fn($v) => max(1, min(255, intdiv($v * $scale + 50, 100))), $base); $w = $img->width; $h = $img->height; $gray = $img->isGray(); $sub = !$gray && ($opt['subsampling'] ?? '420') !== '444'; [$br, $bg, $bb] = $opt['background'] ?? [255, 255, 255]; // YCbCrの面(端は最後の画素を繰り返して、MCUの倍数にする) $mcu = $sub ? 16 : 8; $pw = intdiv($w + $mcu - 1, $mcu) * $mcu; $ph = intdiv($h + $mcu - 1, $mcu) * $mcu; $Y = $Cb = $Cr = []; for ($y = 0; $y < $ph; $y++) { $row = $img->row(min($y, $h - 1)); for ($x = 0; $x < $pw; $x++) { $o = min($x, $w - 1) * 4; $a = ord($row[$o + 3]); $r = ord($row[$o]); $g = ord($row[$o + 1]); $b = ord($row[$o + 2]); if ($a !== 255) { $r = intdiv($r * $a + $br * (255 - $a) + 127, 255); $g = intdiv($g * $a + $bg * (255 - $a) + 127, 255); $b = intdiv($b * $a + $bb * (255 - $a) + 127, 255); } $Y[] = 0.299 * $r + 0.587 * $g + 0.114 * $b - 128; if (!$gray) { $Cb[] = -0.168736 * $r - 0.331264 * $g + 0.5 * $b; $Cr[] = 0.5 * $r - 0.418688 * $g - 0.081312 * $b; } } } if ($sub) { // 2x2の平均 $cw = $pw >> 1; $sCb = $sCr = []; for ($y = 0; $y < ($ph >> 1); $y++) { for ($x = 0; $x < $cw; $x++) { $i = ($y * 2) * $pw + $x * 2; $sCb[] = ($Cb[$i] + $Cb[$i + 1] + $Cb[$i + $pw] + $Cb[$i + $pw + 1]) / 4; $sCr[] = ($Cr[$i] + $Cr[$i + 1] + $Cr[$i + $pw] + $Cr[$i + $pw + 1]) / 4; } } $Cb = $sCb; $Cr = $sCr; } $codes = []; foreach (self::HT as $k => [$counts, $syms]) { $codes[$k] = []; $code = $n = 0; for ($l = 1; $l <= 16; $l++) { for ($i = 0; $i < $counts[$l - 1]; $i++) $codes[$k][$syms[$n++]] = [$code++, $l]; $code <<= 1; } } $this->out = ''; $this->obuf = $this->obits = 0; $pred = [0, 0, 0]; $block = function(array $plane, int $stride, int $bx, int $by, int $c) use ($qt, $codes, &$pred): void { $f = []; for ($y = 0; $y < 8; $y++) { $o = ($by * 8 + $y) * $stride + $bx * 8; for ($x = 0; $x < 8; $x++) $f[] = $plane[$o + $x]; } $this->fdct($f, $qt[$c === 0 ? 0 : 1], $c === 0 ? $codes['dc0'] : $codes['dc1'], $c === 0 ? $codes['ac0'] : $codes['ac1'], $pred[$c]); }; $mx = intdiv($pw, $mcu); $my = intdiv($ph, $mcu); for ($y = 0; $y < $my; $y++) { for ($x = 0; $x < $mx; $x++) { if ($sub) { for ($v = 0; $v < 2; $v++) for ($u = 0; $u < 2; $u++) $block($Y, $pw, $x * 2 + $u, $y * 2 + $v, 0); $block($Cb, $pw >> 1, $x, $y, 1); $block($Cr, $pw >> 1, $x, $y, 2); } else { $block($Y, $pw, $x, $y, 0); if (!$gray) { $block($Cb, $pw, $x, $y, 1); $block($Cr, $pw, $x, $y, 2); } } } } // 残りのビットを1で埋める if ($this->obits > 0) $this->put((1 << (8 - $this->obits)) - 1, 8 - $this->obits); $data = $this->out; $this->out = ''; $seg = fn(int $m, string $d) => "\xff".chr($m).pack('n', strlen($d) + 2).$d; $zz = fn(array $q) => pack('C64', ...array_map(fn($z) => $q[$z], self::ZIGZAG)); $jpg = "\xff\xd8".$seg(0xe0, "JFIF\0\x01\x01\0".pack('nn', 1, 1)."\0\0"); $jpg .= $seg(0xdb, "\x00".$zz($qt[0]).($gray ? '' : "\x01".$zz($qt[1]))); $comps = $gray ? "\x01\x11\x00" : "\x01".($sub ? "\x22" : "\x11")."\x00\x02\x11\x01\x03\x11\x01"; $jpg .= $seg(0xc0, "\x08".pack('nn', $h, $w).chr($gray ? 1 : 3).$comps); $dht = ''; foreach (['dc0' => 0x00, 'ac0' => 0x10, 'dc1' => 0x01, 'ac1' => 0x11] as $k => $id) { if ($gray && $k[2] === '1') continue; $dht .= chr($id).pack('C16', ...self::HT[$k][0]).pack('C*', ...self::HT[$k][1]); } $jpg .= $seg(0xc4, $dht); $sos = $gray ? "\x01\x01\x00" : "\x03\x01\x00\x02\x11\x03\x11"; $jpg .= $seg(0xda, $sos."\x00\x3f\x00"); return $jpg.$data."\xff\xd9"; } /** * 8x8のDCT、量子化、ハフマン符号化 */ private function fdct(array $f, array $q, array $dc, array $ac, int &$pred): void { $K = self::cosTable(); // 行 $t = []; for ($y = 0; $y < 8; $y++) { $b = $y * 8; for ($u = 0; $u < 8; $u++) { $s = 0.0; for ($x = 0; $x < 8; $x++) $s += $f[$b + $x] * $K[$u][$x]; $t[$b + $u] = $s; } } // 列、量子化(ジグザグの順) $zz = []; foreach (self::ZIGZAG as $k => $z) { $u = $z & 7; $v = $z >> 3; $s = 0.0; for ($y = 0; $y < 8; $y++) $s += $t[$y * 8 + $u] * $K[$v][$y]; $zz[$k] = (int)round($s / $q[$z]); } $diff = $zz[0] - $pred; $pred = $zz[0]; $this->value($diff, $dc); $run = 0; for ($k = 1; $k < 64; $k++) { $v = $zz[$k]; if ($v === 0) { $run++; continue; } while ($run > 15) { $this->put(...$ac[0xf0]); $run -= 16; } $this->value($v, $ac, $run); $run = 0; } if ($run > 0) $this->put(...$ac[0x00]); } /** 値を「(run << 4 | 大きさ)の符号+ビット」で書く */ private function value(int $v, array $table, int $run = -1): void { $a = abs($v); $size = 0; while ($a) { $size++; $a >>= 1; } $sym = $run < 0 ? $size : ($run << 4) | $size; $this->put(...$table[$sym]); if ($size) $this->put($v < 0 ? $v + (1 << $size) - 1 : $v, $size); } private function put(int $code, int $len): void { $this->obuf = ($this->obuf << $len) | ($code & ((1 << $len) - 1)); $this->obits += $len; while ($this->obits >= 8) { $this->obits -= 8; $byte = ($this->obuf >> $this->obits) & 255; $this->out .= $byte === 0xff ? "\xff\x00" : chr($byte); } $this->obuf &= (1 << $this->obits) - 1; } }