norm[$r] = $s; } } //////// 読み込み /** * VP8チャンクの中身を読む * * @return array [幅, 丈, RGBA(アルファは255)] */ public function decode(string $data): array { $len = strlen($data); if ($len < 10) throw new \RuntimeException('VP8のデータが短過ぎます。'); $tag = ord($data[0]) | (ord($data[1]) << 8) | (ord($data[2]) << 16); if ($tag & 1) throw new \RuntimeException('VP8のキーフレームではありません。'); if ((($tag >> 1) & 7) > 3) throw new \RuntimeException('未対応のVP8の版です。'); $part = $tag >> 5; if (substr($data, 3, 3) !== "\x9d\x01\x2a") throw new \RuntimeException('VP8の開始コードがありません。'); $w = unpack('v', substr($data, 6, 2))[1] & 0x3fff; $h = unpack('v', substr($data, 8, 2))[1] & 0x3fff; Bitmap::check($w, $h); if (10 + $part > $len) throw new \RuntimeException('VP8のデータが途中で切れています。'); $this->init(substr($data, 10, $part), 0); // フレームのヘッダー $this->value(1); // 色空間 $this->value(1); // クランプ $seg = ['use' => $this->value(1), 'map' => 0, 'abs' => 0, 'q' => [0, 0, 0, 0], 'lf' => [0, 0, 0, 0], 'probs' => [255, 255, 255]]; if ($seg['use']) { $seg['map'] = $this->value(1); if ($this->value(1)) { $seg['abs'] = $this->value(1); for ($s = 0; $s < 4; $s++) $seg['q'][$s] = $this->value(1) ? $this->signed(7) : 0; for ($s = 0; $s < 4; $s++) $seg['lf'][$s] = $this->value(1) ? $this->signed(6) : 0; } if ($seg['map']) for ($s = 0; $s < 3; $s++) $seg['probs'][$s] = $this->value(1) ? $this->value(8) : 255; } $simple = $this->value(1); $level = $this->value(6); $sharp = $this->value(3); $refDelta = $modeDelta = 0; $useDelta = $this->value(1); if ($useDelta && $this->value(1)) { // 参照フレームとモード毎の差(キーフレームでは「現在のフレーム」と「B_PRED」だけ使う) for ($i = 0; $i < 4; $i++) if ($this->value(1)) { $v = $this->signed(6); if ($i === 0) $refDelta = $v; } for ($i = 0; $i < 4; $i++) if ($this->value(1)) { $v = $this->signed(6); if ($i === 0) $modeDelta = $v; } } $filterType = $level === 0 ? 0 : ($simple ? 1 : 2); $numParts = 1 << $this->value(2); // 量子化 $base = $this->value(7); $dq = []; for ($i = 0; $i < 5; $i++) $dq[$i] = $this->value(1) ? $this->signed(4) : 0; $quant = []; for ($s = 0; $s < 4; $s++) { $q = $seg['use'] ? $seg['q'][$s] + ($seg['abs'] ? 0 : $base) : $base; $c = fn(int $v, int $max = 127): int => $v < 0 ? 0 : ($v > $max ? $max : $v); $quant[$s] = [ self::DC_TABLE[$c($q + $dq[0])], self::AC_TABLE[$c($q)], self::DC_TABLE[$c($q + $dq[1])] * 2, max(8, (self::AC_TABLE[$c($q + $dq[2])] * 101581) >> 16), self::DC_TABLE[$c($q + $dq[3], 117)], self::AC_TABLE[$c($q + $dq[4])], ]; } $this->value(1); // 確率を保存するか(静止画では関係無い) $this->probs = self::COEFF_PROBS; for ($t = 0; $t < 4; $t++) for ($b = 0; $b < 8; $b++) for ($c = 0; $c < 3; $c++) for ($i = 0; $i < 11; $i++) { if ($this->bit(self::COEFF_UPDATE_PROBS[$t][$b][$c][$i])) $this->probs[$t][$b][$c][$i] = $this->value(8); } $useSkip = $this->value(1); $skipProb = $useSkip ? $this->value(8) : 0; // フィルターの強さ(セグメントと4x4の予測かで変わる) $fstr = []; for ($s = 0; $s < 4; $s++) { $lvl = $seg['use'] ? $seg['lf'][$s] + ($seg['abs'] ? 0 : $level) : $level; for ($i4 = 0; $i4 < 2; $i4++) { $l = $lvl; if ($useDelta) $l += $refDelta + ($i4 ? $modeDelta : 0); $l = $l < 0 ? 0 : ($l > 63 ? 63 : $l); if ($l === 0) { $fstr[$s][$i4] = null; continue; } $il = $l; if ($sharp > 0) { $il >>= $sharp > 4 ? 2 : 1; if ($il > 9 - $sharp) $il = 9 - $sharp; } if ($il < 1) $il = 1; $fstr[$s][$i4] = [2 * $l + $il, $il, $l >= 40 ? 2 : ($l >= 15 ? 1 : 0)]; } } $mbw = ($w + 15) >> 4; $mbh = ($h + 15) >> 4; // 全てのマクロブロックの予測モード(最初のパーティション) $mbs = []; $intraT = array_fill(0, 4 * $mbw, self::DC); for ($my = 0; $my < $mbh; $my++) { $intraL = [self::DC, self::DC, self::DC, self::DC]; for ($mx = 0; $mx < $mbw; $mx++) { $mb = ['seg' => 0, 'skip' => 0]; if ($seg['map']) $mb['seg'] = !$this->bit($seg['probs'][0]) ? $this->bit($seg['probs'][1]) : 2 + $this->bit($seg['probs'][2]); if ($useSkip) $mb['skip'] = $this->bit($skipProb); $mb['i4'] = !$this->bit(145); if (!$mb['i4']) { $ym = $this->bit(156) ? ($this->bit(128) ? self::TM : self::HE) : ($this->bit(163) ? self::VE : self::DC); $mb['y'] = $ym; for ($i = 0; $i < 4; $i++) $intraT[4 * $mx + $i] = $intraL[$i] = $ym; } else { $modes = []; for ($y = 0; $y < 4; $y++) { $ym = $intraL[$y]; for ($x = 0; $x < 4; $x++) { $prob = self::BMODE_PROBS[$intraT[4 * $mx + $x]][$ym]; $i = self::BMODE_TREE[$this->bit($prob[0])]; while ($i > 0) $i = self::BMODE_TREE[2 * $i + $this->bit($prob[$i])]; $ym = -$i; $intraT[4 * $mx + $x] = $ym; $modes[] = $ym; } $intraL[$y] = $ym; } $mb['b'] = $modes; } $mb['uv'] = !$this->bit(142) ? self::DC : (!$this->bit(114) ? self::VE : ($this->bit(183) ? self::TM : self::HE)); $mbs[] = $mb; } } if ($this->eof()) throw new \RuntimeException('VP8の最初のパーティションが途中で切れています。'); // 係数のパーティション $pos = 10 + $part; $sizes = $pos; $pos += 3 * ($numParts - 1); if ($pos > $len) throw new \RuntimeException('VP8のデータが途中で切れています。'); $parts = []; for ($i = 0; $i < $numParts; $i++) { $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; $psize = min($psize, $len - $pos); $parts[] = substr($data, $pos, $psize); $pos += $psize; } $bands = []; for ($t = 0; $t < 4; $t++) for ($n = 0; $n < 17; $n++) $bands[$t][$n] = $this->probs[$t][self::BANDS[$n]]; $yw = $mbw * 16; $uw = $mbw * 8; $Y = str_repeat("\0", $yw * $mbh * 16); $U = str_repeat("\0", $uw * $mbh * 8); $V = $U; $filters = []; $tnz = array_fill(0, $mbw, array_fill(0, 9, 0)); $states = []; for ($my = 0; $my < $mbh; $my++) { $pi = $my % $numParts; if (isset($states[$pi])) $this->restore($states[$pi]); else $this->init($parts[$pi], 0); $lnz = array_fill(0, 9, 0); for ($mx = 0; $mx < $mbw; $mx++) { $mb = $mbs[$my * $mbw + $mx]; $q = $quant[$mb['seg']]; $coef = array_fill(0, 384, 0); $nz = array_fill(0, 24, false); $t = &$tnz[$mx]; if ($mb['skip']) { for ($i = 0; $i < 8; $i++) $t[$i] = $lnz[$i] = 0; if (!$mb['i4']) $t[8] = $lnz[8] = 0; $any = false; } else { $any = $this->residuals($mb['i4'], $q, $bands, $t, $lnz, $coef, $nz); } unset($t); if ($filterType) { $f = $fstr[$mb['seg']][$mb['i4'] ? 1 : 0]; $filters[] = $f === null ? null : [...$f, $mb['i4'] || $any]; } $this->reconstruct($mb, $mx, $my, $mbw, $coef, $nz, $Y, $U, $V); } if ($this->eof()) throw new \RuntimeException('VP8の係数のデータが途中で切れています。'); $states[$pi] = $this->save(); } if ($filterType) { for ($my = 0, $i = 0; $my < $mbh; $my++) { for ($mx = 0; $mx < $mbw; $mx++, $i++) { if ($filters[$i] === null) continue; [$limit, $il, $hev, $inner] = $filters[$i]; if ($filterType === 1) self::simpleFilter($Y, $yw, $mx, $my, $limit, $inner); else self::normalFilter($Y, $U, $V, $yw, $uw, $mx, $my, $limit, $il, $hev, $inner); } } } return [$w, $h, self::toRgba($Y, $U, $V, $yw, $uw, $w, $h)]; } private function init(string $data, int $pos): void { $this->d = $data; $this->p = $pos; $this->value = 0; $this->count = -8; $this->range = 255; } /** データの終わりを超えて読んだか(先読みした分は除く) */ private function eof(): bool { return $this->p - ($this->count >> 3) > strlen($this->d) + 1; } private function save(): array { return [$this->d, $this->p, $this->value, $this->count, $this->range]; } private function restore(array $s): void { [$this->d, $this->p, $this->value, $this->count, $this->range] = $s; } private function bit(int $prob): int { $split = 1 + ((($this->range - 1) * $prob) >> 8); if ($this->count < 0) { // 足りなければ数バイト読む(終わりの後は0) do { $this->value = ($this->value << 8) | ord($this->d[$this->p++] ?? "\0"); $this->count += 8; } while ($this->count < 40); } $big = $split << $this->count; if ($this->value >= $big) { $this->range -= $split; $this->value -= $big; $bit = 1; } else { $this->range = $split; $bit = 0; } if ($this->range < 128) { $s = $this->norm[$this->range]; $this->range <<= $s; $this->count -= $s; } return $bit; } private function value(int $n): int { $v = 0; while ($n-- > 0) $v = ($v << 1) | $this->bit(128); return $v; } private function signed(int $n): int { $v = $this->value($n); return $this->bit(128) ? -$v : $v; } /** * マクロブロックの係数を読む * * @param array $t 上の非ゼロ(Yの4列、Uの2列、Vの2列、Y2) * @param array $l 左の非ゼロ * @return bool 0以外の係数があるか */ private function residuals(bool $i4, array $q, array $bands, array &$t, array &$l, array &$coef, array &$nz): bool { $any = false; if (!$i4) { $dc = array_fill(0, 16, 0); $n = $this->coeffs($bands[1], $t[8] + $l[8], $q[2], $q[3], 0, $dc, 0); $t[8] = $l[8] = $n > 0 ? 1 : 0; if ($n > 0) { self::iwht($dc, $coef); for ($b = 0; $b < 16; $b++) if ($coef[$b * 16] !== 0) $nz[$b] = $any = true; } $first = 1; $ac = $bands[0]; } else { $first = 0; $ac = $bands[3]; } for ($y = 0; $y < 4; $y++) { $left = $l[$y]; for ($x = 0; $x < 4; $x++) { $b = $y * 4 + $x; $n = $this->coeffs($ac, $left + $t[$x], $q[0], $q[1], $first, $coef, $b * 16); $left = $t[$x] = $n > $first ? 1 : 0; if ($left) $nz[$b] = $any = true; } $l[$y] = $left; } for ($ch = 0; $ch < 2; $ch++) { for ($y = 0; $y < 2; $y++) { $left = $l[4 + $ch * 2 + $y]; for ($x = 0; $x < 2; $x++) { $b = 16 + $ch * 4 + $y * 2 + $x; $n = $this->coeffs($bands[2], $left + $t[4 + $ch * 2 + $x], $q[4], $q[5], 0, $coef, $b * 16); $left = $t[4 + $ch * 2 + $x] = $n > 0 ? 1 : 0; if ($left) $nz[$b] = $any = true; } $l[4 + $ch * 2 + $y] = $left; } } return $any; } /** * 1ブロックの係数を読む * * @return int 最後の0以外の係数の次の位置 */ private function coeffs(array $bp, int $ctx, int $dq0, int $dq1, int $n, array &$out, int $o): int { $p = $bp[$n][$ctx]; for (; $n < 16; $n++) { if (!$this->bit($p[0])) return $n; while (!$this->bit($p[1])) { $p = $bp[++$n][0]; if ($n === 16) return 16; } if (!$this->bit($p[2])) { $v = 1; $next = 1; } else { $v = $this->large($p); $next = 2; } $out[$o + self::ZIGZAG[$n]] = ($this->bit(128) ? -$v : $v) * ($n > 0 ? $dq1 : $dq0); $p = $bp[$n + 1][$next]; } return 16; } private function large(array $p): int { if (!$this->bit($p[3])) { if (!$this->bit($p[4])) return 2; return 3 + $this->bit($p[5]); } if (!$this->bit($p[6])) { if (!$this->bit($p[7])) return 5 + $this->bit(159); $v = 7 + 2 * $this->bit(165); return $v + $this->bit(145); } $b1 = $this->bit($p[8]); $cat = 2 * $b1 + $this->bit($p[9 + $b1]); $v = 0; foreach (self::CAT[$cat] as $prob) $v = 2 * $v + $this->bit($prob); return $v + 3 + (8 << $cat); } /** Y2の逆ウォルシュ・アダマール変換(結果は各ブロックの直流) */ private static function iwht(array $in, array &$coef): void { $tmp = []; for ($i = 0; $i < 4; $i++) { $a0 = $in[$i] + $in[12 + $i]; $a1 = $in[4 + $i] + $in[8 + $i]; $a2 = $in[4 + $i] - $in[8 + $i]; $a3 = $in[$i] - $in[12 + $i]; $tmp[$i] = $a0 + $a1; $tmp[8 + $i] = $a0 - $a1; $tmp[4 + $i] = $a3 + $a2; $tmp[12 + $i] = $a3 - $a2; } for ($i = 0; $i < 4; $i++) { $dc = $tmp[$i * 4] + 3; $a0 = $dc + $tmp[$i * 4 + 3]; $a1 = $tmp[$i * 4 + 1] + $tmp[$i * 4 + 2]; $a2 = $tmp[$i * 4 + 1] - $tmp[$i * 4 + 2]; $a3 = $dc - $tmp[$i * 4 + 3]; $coef[($i * 4) * 16] = ($a0 + $a1) >> 3; $coef[($i * 4 + 1) * 16] = ($a3 + $a2) >> 3; $coef[($i * 4 + 2) * 16] = ($a0 - $a1) >> 3; $coef[($i * 4 + 3) * 16] = ($a3 - $a2) >> 3; } } /** * 逆DCTして作業用の配列に足す */ private static function idctAdd(array $c, int $o, array &$ws, int $at): void { $tmp = []; for ($i = 0; $i < 4; $i++) { $c0 = $c[$o + $i]; $c4 = $c[$o + 4 + $i]; $c8 = $c[$o + 8 + $i]; $c12 = $c[$o + 12 + $i]; $a = $c0 + $c8; $b = $c0 - $c8; $cc = (($c4 * 35468) >> 16) - ((($c12 * 20091) >> 16) + $c12); $d = ((($c4 * 20091) >> 16) + $c4) + (($c12 * 35468) >> 16); $tmp[$i * 4] = $a + $d; $tmp[$i * 4 + 1] = $b + $cc; $tmp[$i * 4 + 2] = $b - $cc; $tmp[$i * 4 + 3] = $a - $d; } for ($i = 0; $i < 4; $i++) { $dc = $tmp[$i] + 4; $a = $dc + $tmp[8 + $i]; $b = $dc - $tmp[8 + $i]; $cc = (($tmp[4 + $i] * 35468) >> 16) - ((($tmp[12 + $i] * 20091) >> 16) + $tmp[12 + $i]); $d = ((($tmp[4 + $i] * 20091) >> 16) + $tmp[4 + $i]) + (($tmp[12 + $i] * 35468) >> 16); $r = $at + $i * self::BPS; foreach ([$a + $d, $b + $cc, $b - $cc, $a - $d] as $k => $v) { $v = $ws[$r + $k] + ($v >> 3); $ws[$r + $k] = $v < 0 ? 0 : ($v > 255 ? 255 : $v); } } } /** * 予測して残差を足し、平面に書く */ private function reconstruct(array $mb, int $mx, int $my, int $mbw, array $coef, array $nz, string &$Y, string &$U, string &$V): void { $yw = $mbw * 16; $ws = self::border($Y, $yw, 16, $mx, $my); if ($mb['i4']) { self::topRight($ws, $Y, $yw, $mx, $my, $mbw); for ($b = 0; $b < 16; $b++) { $at = (($b >> 2) * 4 + 1) * self::BPS + ($b & 3) * 4 + 1; self::predict4($mb['b'][$b], $ws, $at); if ($nz[$b]) self::idctAdd($coef, $b * 16, $ws, $at); } } else { self::predictBlock($mb['y'], $ws, 16, $mx, $my); for ($b = 0; $b < 16; $b++) { if ($nz[$b]) self::idctAdd($coef, $b * 16, $ws, (($b >> 2) * 4 + 1) * self::BPS + ($b & 3) * 4 + 1); } } self::store($Y, $yw, 16, $mx, $my, $ws); $uw = $mbw * 8; foreach ([&$U, &$V] as $ch => &$plane) { $ws = self::border($plane, $uw, 8, $mx, $my); self::predictBlock($mb['uv'], $ws, 8, $mx, $my); for ($b = 0; $b < 4; $b++) { $bi = 16 + $ch * 4 + $b; if ($nz[$bi]) self::idctAdd($coef, $bi * 16, $ws, (($b >> 1) * 4 + 1) * self::BPS + ($b & 1) * 4 + 1); } self::store($plane, $uw, 8, $mx, $my, $ws); } unset($plane); } /** * 4x4の予測の右上:上のマクロブロックの行の続き(右端では最後の画素)。2行目以降の右端のブロックも同じ物を使う */ private static function topRight(array &$ws, string $Y, int $yw, int $mx, int $my, int $mbw): void { for ($i = 0; $i < 4; $i++) { $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])); foreach ([0, 4, 8, 12] as $r) $ws[$r * self::BPS + 17 + $i] = $tr; } } /** * 作業用の配列に上と左の画素を入れる(画像の外は上が127、左が129) */ private static function border(string $plane, int $stride, int $size, int $mx, int $my): array { $ws = array_fill(0, ($size + 1) * self::BPS, 0); $x0 = $mx * $size; $y0 = $my * $size; if ($my > 0) { $o = ($y0 - 1) * $stride + $x0; for ($x = 0; $x < $size; $x++) $ws[1 + $x] = ord($plane[$o + $x]); } else { for ($x = 0; $x < $size; $x++) $ws[1 + $x] = 127; } $ws[0] = $my === 0 ? 127 : ($mx === 0 ? 129 : ord($plane[($y0 - 1) * $stride + $x0 - 1])); for ($y = 0; $y < $size; $y++) $ws[($y + 1) * self::BPS] = $mx === 0 ? 129 : ord($plane[($y0 + $y) * $stride + $x0 - 1]); return $ws; } private static function store(string &$plane, int $stride, int $size, int $mx, int $my, array $ws): void { for ($y = 0; $y < $size; $y++) { $o = ($my * $size + $y) * $stride + $mx * $size; $r = ($y + 1) * self::BPS + 1; for ($x = 0; $x < $size; $x++) $plane[$o + $x] = chr($ws[$r + $x]); } } /** 16x16(輝度)又は8x8(色差)の予測 */ private static function predictBlock(int $mode, array &$ws, int $size, int $mx, int $my): void { $B = self::BPS; $shift = $size === 16 ? 4 : 3; if ($mode === self::DC) { if ($mx === 0 && $my === 0) { $dc = 128; } else { $sum = 0; if ($my > 0) for ($x = 0; $x < $size; $x++) $sum += $ws[1 + $x]; if ($mx > 0) for ($y = 0; $y < $size; $y++) $sum += $ws[($y + 1) * $B]; $s = ($mx > 0 && $my > 0) ? $shift + 1 : $shift; $dc = ($sum + (1 << ($s - 1))) >> $s; } for ($y = 0; $y < $size; $y++) for ($x = 0; $x < $size; $x++) $ws[($y + 1) * $B + 1 + $x] = $dc; return; } for ($y = 0; $y < $size; $y++) { $r = ($y + 1) * $B; $left = $ws[$r]; for ($x = 0; $x < $size; $x++) { if ($mode === self::VE) { $v = $ws[1 + $x]; } else if ($mode === self::HE) { $v = $left; } else { $v = $ws[1 + $x] + $left - $ws[0]; $v = $v < 0 ? 0 : ($v > 255 ? 255 : $v); } $ws[$r + 1 + $x] = $v; } } } /** 4x4の予測($atはブロックの左上) */ private static function predict4(int $mode, array &$ws, int $at): void { $B = self::BPS; $t = $at - $B; $X = $ws[$t - 1]; [$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]]; [$I, $J, $K, $L] = [$ws[$at - 1], $ws[$at + $B - 1], $ws[$at + 2 * $B - 1], $ws[$at + 3 * $B - 1]]; $avg3 = fn($a, $b, $c) => ($a + 2 * $b + $c + 2) >> 2; $avg2 = fn($a, $b) => ($a + $b + 1) >> 1; $o = []; switch ($mode) { case 0: // DC $dc = ($A + $Bb + $C + $D + $I + $J + $K + $L + 4) >> 3; $o = array_fill(0, 16, $dc); break; case 1: // TM foreach ([$I, $J, $K, $L] as $y => $l) { foreach ([$A, $Bb, $C, $D] as $x => $tp) { $v = $tp + $l - $X; $o[$y * 4 + $x] = $v < 0 ? 0 : ($v > 255 ? 255 : $v); } } break; case 2: // VE $row = [$avg3($X, $A, $Bb), $avg3($A, $Bb, $C), $avg3($Bb, $C, $D), $avg3($C, $D, $E)]; $o = [...$row, ...$row, ...$row, ...$row]; break; case 3: // HE 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; break; case 4: // RD $o[12] = $avg3($J, $K, $L); $o[13] = $o[8] = $avg3($I, $J, $K); $o[14] = $o[9] = $o[4] = $avg3($X, $I, $J); $o[15] = $o[10] = $o[5] = $o[0] = $avg3($A, $X, $I); $o[11] = $o[6] = $o[1] = $avg3($Bb, $A, $X); $o[7] = $o[2] = $avg3($C, $Bb, $A); $o[3] = $avg3($D, $C, $Bb); break; case 5: // VR $o[0] = $o[9] = $avg2($X, $A); $o[1] = $o[10] = $avg2($A, $Bb); $o[2] = $o[11] = $avg2($Bb, $C); $o[3] = $avg2($C, $D); $o[12] = $avg3($K, $J, $I); $o[8] = $avg3($J, $I, $X); $o[4] = $o[13] = $avg3($I, $X, $A); $o[5] = $o[14] = $avg3($X, $A, $Bb); $o[6] = $o[15] = $avg3($A, $Bb, $C); $o[7] = $avg3($Bb, $C, $D); break; case 6: // LD $o[0] = $avg3($A, $Bb, $C); $o[1] = $o[4] = $avg3($Bb, $C, $D); $o[2] = $o[5] = $o[8] = $avg3($C, $D, $E); $o[3] = $o[6] = $o[9] = $o[12] = $avg3($D, $E, $F); $o[7] = $o[10] = $o[13] = $avg3($E, $F, $G); $o[11] = $o[14] = $avg3($F, $G, $H); $o[15] = $avg3($G, $H, $H); break; case 7: // VL $o[0] = $avg2($A, $Bb); $o[1] = $o[8] = $avg2($Bb, $C); $o[2] = $o[9] = $avg2($C, $D); $o[3] = $o[10] = $avg2($D, $E); $o[4] = $avg3($A, $Bb, $C); $o[5] = $o[12] = $avg3($Bb, $C, $D); $o[6] = $o[13] = $avg3($C, $D, $E); $o[7] = $o[14] = $avg3($D, $E, $F); $o[11] = $avg3($E, $F, $G); $o[15] = $avg3($F, $G, $H); break; case 8: // HD $o[0] = $o[6] = $avg2($I, $X); $o[4] = $o[10] = $avg2($J, $I); $o[8] = $o[14] = $avg2($K, $J); $o[12] = $avg2($L, $K); $o[3] = $avg3($A, $Bb, $C); $o[2] = $avg3($X, $A, $Bb); $o[1] = $o[7] = $avg3($I, $X, $A); $o[5] = $o[11] = $avg3($J, $I, $X); $o[9] = $o[15] = $avg3($K, $J, $I); $o[13] = $avg3($L, $K, $J); break; default: // HU $o[0] = $avg2($I, $J); $o[2] = $o[4] = $avg2($J, $K); $o[6] = $o[8] = $avg2($K, $L); $o[1] = $avg3($I, $J, $K); $o[3] = $o[5] = $avg3($J, $K, $L); $o[7] = $o[9] = $avg3($K, $L, $L); $o[11] = $o[10] = $o[12] = $o[13] = $o[14] = $o[15] = $L; } for ($i = 0; $i < 16; $i++) $ws[$at + ($i >> 2) * $B + ($i & 3)] = $o[$i]; } //////// 書き出し // 算術符号化 private string $out = ''; private int $bottom = 0; private int $erange = 255; private int $shiftCount = 24; /** 確率を決める為に数えるだけ([種類][帯][文脈][番号] => [0の数, 1の数]) */ private ?array $stats = null; /** * RGBAをVP8チャンクの中身にする(アルファは無視) * * @param array $opt quality(0~100、既定80)、filter(normal・simple・none)、sharpness(0~7)、partitions(1・2・4・8) */ public function encode(string $rgba, int $w, int $h, array $opt = []): string { $quality = (int)($opt['quality'] ?? 80); $filter = $opt['filter'] ?? 'normal'; $sharp = max(0, min(7, (int)($opt['sharpness'] ?? 0))); $parts = (int)($opt['partitions'] ?? 1); if (!in_array($parts, [1, 2, 4, 8], true)) throw new \InvalidArgumentException('VP8のパーティションの数は1・2・4・8です。'); $mbw = ($w + 15) >> 4; $mbh = ($h + 15) >> 4; [$SY, $SU, $SV] = self::toYuv($rgba, $w, $h, $mbw, $mbh); $yw = $mbw * 16; $uw = $mbw * 8; // 品質から量子化(libwebpと同じ曲線) $c = max(0, min(100, $quality)) / 100; $c = $c < 0.75 ? $c * 2 / 3 : 2 * $c - 1; $q = max(0, min(127, (int)(127 * (1 - $c ** (1 / 3))))); $Q = [ self::DC_TABLE[$q], self::AC_TABLE[$q], self::DC_TABLE[$q] * 2, max(8, (self::AC_TABLE[$q] * 101581) >> 16), self::DC_TABLE[min($q, 117)], self::AC_TABLE[$q], ]; // 歪み(二乗誤差)とビット数の重み $lambda = 0.03 * $Q[1] * $Q[1]; $RY = str_repeat("\0", $yw * $mbh * 16); $RU = str_repeat("\0", $uw * $mbh * 8); $RV = $RU; $mbs = []; for ($my = 0; $my < $mbh; $my++) { for ($mx = 0; $mx < $mbw; $mx++) { $mbs[] = $this->encodeMb($SY, $SU, $SV, $RY, $RU, $RV, $mx, $my, $mbw, $Q, $lambda); } } // 係数の確率を画像に合わせる $this->stats = []; $this->probs = self::COEFF_PROBS; $this->tokens($mbs, $mbw, 1); $updates = []; $cost = fn(int $p, int $bit): float => -log(($bit ? 256 - $p : $p) / 256, 2); for ($t = 0; $t < 4; $t++) for ($b = 0; $b < 8; $b++) for ($k = 0; $k < 3; $k++) for ($i = 0; $i < 11; $i++) { [$n0, $n1] = $this->stats[$t][$b][$k][$i] ?? [0, 0]; if ($n0 + $n1 === 0) continue; $old = self::COEFF_PROBS[$t][$b][$k][$i]; $new = max(1, min(255, (int)round(256 * $n0 / ($n0 + $n1)))); $up = self::COEFF_UPDATE_PROBS[$t][$b][$k][$i]; $saved = $n0 * ($cost($old, 0) - $cost($new, 0)) + $n1 * ($cost($old, 1) - $cost($new, 1)) - 8 - $cost($up, 1) + $cost($up, 0); if ($new !== $old && $saved > 0) { $updates[$t][$b][$k][$i] = $new; $this->probs[$t][$b][$k][$i] = $new; } } $this->stats = null; $skips = count(array_filter($mbs, fn($mb) => $mb['skip'])); $skipProb = max(1, min(255, (int)round(256 * (count($mbs) - $skips) / count($mbs)))); // 最初のパーティション:ヘッダーと予測モード $this->startWrite(); $this->putValue(0, 2); // 色空間、クランプ $this->putValue(0, 1); // セグメント無し $level = (self::AC_TABLE[$q] >> 2) * 300 / 356 * 1.2; $level = $level < 2 || $filter === 'none' ? 0 : min(63, (int)round($level)); $this->putValue($filter === 'simple' ? 1 : 0, 1); $this->putValue($level, 6); $this->putValue($sharp, 3); $this->putValue(0, 1); // モード毎のフィルターの差は無し $this->putValue((int)log($parts, 2), 2); $this->putValue($q, 7); $this->putValue(0, 5); // 量子化の差は無し $this->putValue(0, 1); for ($t = 0; $t < 4; $t++) for ($b = 0; $b < 8; $b++) for ($k = 0; $k < 3; $k++) for ($i = 0; $i < 11; $i++) { $new = $updates[$t][$b][$k][$i] ?? null; $this->put(self::COEFF_UPDATE_PROBS[$t][$b][$k][$i], $new !== null ? 1 : 0); if ($new !== null) $this->putValue($new, 8); } $this->putValue(1, 1); $this->putValue($skipProb, 8); $paths = self::bmodePaths(); $intraT = array_fill(0, 4 * $mbw, self::DC); foreach ($mbs as $n => $mb) { $mx = $n % $mbw; if ($mx === 0) $intraL = [self::DC, self::DC, self::DC, self::DC]; $this->put($skipProb, $mb['skip'] ? 1 : 0); $this->put(145, $mb['i4'] ? 0 : 1); if (!$mb['i4']) { $ym = $mb['y']; if ($ym === self::DC || $ym === self::VE) { $this->put(156, 0); $this->put(163, $ym === self::VE ? 1 : 0); } else { $this->put(156, 1); $this->put(128, $ym === self::TM ? 1 : 0); } for ($i = 0; $i < 4; $i++) $intraT[4 * $mx + $i] = $intraL[$i] = $ym; } else { for ($y = 0; $y < 4; $y++) { for ($x = 0; $x < 4; $x++) { $m = $mb['b'][$y * 4 + $x]; $prob = self::BMODE_PROBS[$intraT[4 * $mx + $x]][$intraL[$y]]; foreach ($paths[$m] as [$pi, $bit]) $this->put($prob[$pi], $bit); $intraT[4 * $mx + $x] = $intraL[$y] = $m; } } } $uv = $mb['uv']; $this->put(142, $uv === self::DC ? 0 : 1); if ($uv !== self::DC) { $this->put(114, $uv === self::VE ? 0 : 1); if ($uv !== self::VE) $this->put(183, $uv === self::TM ? 1 : 0); } } $first = $this->finishWrite(); // 係数のパーティション(マクロブロックの行毎に順に使う) $tokens = $this->tokens($mbs, $mbw, $parts); $sizes = ''; for ($i = 0; $i < $parts - 1; $i++) $sizes .= substr(pack('V', strlen($tokens[$i])), 0, 3); $tag = (strlen($first) << 5) | 0x10; return chr($tag & 255).chr(($tag >> 8) & 255).chr($tag >> 16)."\x9d\x01\x2a".pack('vv', $w, $h).$first.$sizes.implode('', $tokens); } /** 4x4の予測モードの木の道筋(モード => [[確率の番号, ビット], ...]) */ private static function bmodePaths(): array { $paths = []; $walk = function(int $node, array $path) use (&$walk, &$paths): void { for ($b = 0; $b < 2; $b++) { $child = self::BMODE_TREE[2 * $node + $b]; $p = [...$path, [$node, $b]]; if ($child <= 0) $paths[-$child] = $p; else $walk($child, $p); } }; $walk(0, []); return $paths; } /** * RGBAをYUV 4:2:0にする(マクロブロックの大きさまで端の画素を伸ばす) */ private static function toYuv(string $rgba, int $w, int $h, int $mbw, int $mbh): array { $yw = $mbw * 16; $uw = $mbw * 8; $Y = $U = $V = ''; $prev = null; for ($y = 0; $y < $mbh * 16; $y++) { $row = array_values(unpack('C*', substr($rgba, min($y, $h - 1) * $w * 4, $w * 4))); $line = []; for ($x = 0; $x < $yw; $x++) { $o = min($x, $w - 1) * 4; $line[] = (16839 * $row[$o] + 33059 * $row[$o + 1] + 6420 * $row[$o + 2] + 32768 + (16 << 16)) >> 16; } $Y .= pack('C*', ...$line); if (!($y & 1)) { $prev = $row; continue; } $ul = $vl = []; for ($x = 0; $x < $uw; $x++) { $o0 = min(2 * $x, $w - 1) * 4; $o1 = min(2 * $x + 1, $w - 1) * 4; $r = $prev[$o0] + $prev[$o1] + $row[$o0] + $row[$o1]; $g = $prev[$o0 + 1] + $prev[$o1 + 1] + $row[$o0 + 1] + $row[$o1 + 1]; $b = $prev[$o0 + 2] + $prev[$o1 + 2] + $row[$o0 + 2] + $row[$o1 + 2]; $u = (-9719 * $r - 19081 * $g + 28800 * $b + (1 << 17) + (128 << 18)) >> 18; $v = (28800 * $r - 24116 * $g - 4684 * $b + (1 << 17) + (128 << 18)) >> 18; $ul[] = $u < 0 ? 0 : ($u > 255 ? 255 : $u); $vl[] = $v < 0 ? 0 : ($v > 255 ? 255 : $v); } $U .= pack('C*', ...$ul); $V .= pack('C*', ...$vl); } return [$Y, $U, $V]; } /** 平面からブロックの画素を読む */ private static function block(string $plane, int $stride, int $size, int $mx, int $my): array { $px = []; for ($y = 0; $y < $size; $y++) { foreach (unpack('C*', substr($plane, ($my * $size + $y) * $stride + $mx * $size, $size)) as $v) $px[] = $v; } return $px; } /** * 1つのマクロブロックの予測モードを選んで量子化し、復元した画素を書く */ 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 { $B = self::BPS; $yw = $mbw * 16; $src = self::block($SY, $yw, 16, $mx, $my); $ws0 = self::border($RY, $yw, 16, $mx, $my); // 16x16:予測の誤差が一番小さいモード $best = null; foreach ([self::DC, self::VE, self::HE, self::TM] as $m) { $ws = $ws0; self::predictBlock($m, $ws, 16, $mx, $my); $sad = 0; for ($i = 0; $i < 256; $i++) $sad += abs($src[$i] - $ws[(($i >> 4) + 1) * $B + ($i & 15) + 1]); if ($best === null || $sad < $best[0]) $best = [$sad, $m, $ws]; } [, $m16, $ws16] = $best; $res = []; $dcs = []; for ($b = 0; $b < 16; $b++) { $at = (($b >> 2) * 4 + 1) * $B + ($b & 3) * 4 + 1; $s = (($b >> 2) * 4) * 16 + ($b & 3) * 4; $res[$b] = self::fdct($src, $s, 16, $ws16, $at); $dcs[] = $res[$b][0]; } $lv2 = []; $dq2 = self::quant(self::fwht($dcs), $Q[2], $Q[3], 96, 108, 0, $lv2); $dc = array_fill(0, 384, 0); self::iwht($dq2, $dc); $lv16 = []; $bits16 = self::bits($lv2, 0) + 3; for ($b = 0; $b < 16; $b++) { $l = []; $dq = self::quant($res[$b], $Q[0], $Q[1], 96, 110, 1, $l); $lv16[$b] = $l; $bits16 += self::bits($l, 1); $dq[0] = $dc[$b * 16]; if (array_filter($dq)) self::idctAdd($dq, 0, $ws16, (($b >> 2) * 4 + 1) * $B + ($b & 3) * 4 + 1); } $score16 = self::sse($src, $ws16, 16) + $lambda * $bits16; // 4x4:ブロック毎に予測の誤差が一番小さいモード(先に復元したブロックから予測する) $ws4 = $ws0; self::topRight($ws4, $RY, $yw, $mx, $my, $mbw); $modes = []; $lv4 = []; $bits4 = 1; for ($b = 0; $b < 16; $b++) { $at = (($b >> 2) * 4 + 1) * $B + ($b & 3) * 4 + 1; $s = (($b >> 2) * 4) * 16 + ($b & 3) * 4; $bestM = 0; $bestSad = PHP_INT_MAX; for ($m = 0; $m < 10; $m++) { self::predict4($m, $ws4, $at); $sad = 0; for ($i = 0; $i < 16; $i++) $sad += abs($src[$s + ($i >> 2) * 16 + ($i & 3)] - $ws4[$at + ($i >> 2) * $B + ($i & 3)]); // DC以外は少し不利に(モードのビット) if ($m > 0) $sad += 4; if ($sad < $bestSad) { $bestSad = $sad; $bestM = $m; } } self::predict4($bestM, $ws4, $at); $modes[] = $bestM; $l = []; $dq = self::quant(self::fdct($src, $s, 16, $ws4, $at), $Q[0], $Q[1], 96, 110, 0, $l); $lv4[$b] = $l; $bits4 += self::bits($l, 0) + ($bestM === 0 ? 1 : 4); if (array_filter($dq)) self::idctAdd($dq, 0, $ws4, $at); } $score4 = self::sse($src, $ws4, 16) + $lambda * $bits4; $mb = ['skip' => false]; if ($score4 < $score16) { $mb['i4'] = true; $mb['b'] = $modes; $mb['lv'] = $lv4; $ws = $ws4; } else { $mb['i4'] = false; $mb['y'] = $m16; $mb['y2'] = $lv2; $mb['lv'] = $lv16; $ws = $ws16; } self::store($RY, $yw, 16, $mx, $my, $ws); // 色差:UとVの予測の誤差の合計が一番小さいモード $uw = $mbw * 8; $srcU = self::block($SU, $uw, 8, $mx, $my); $srcV = self::block($SV, $uw, 8, $mx, $my); $wu0 = self::border($RU, $uw, 8, $mx, $my); $wv0 = self::border($RV, $uw, 8, $mx, $my); $best = null; foreach ([self::DC, self::VE, self::HE, self::TM] as $m) { $wu = $wu0; $wv = $wv0; self::predictBlock($m, $wu, 8, $mx, $my); self::predictBlock($m, $wv, 8, $mx, $my); $sad = 0; for ($i = 0; $i < 64; $i++) { $k = (($i >> 3) + 1) * $B + ($i & 7) + 1; $sad += abs($srcU[$i] - $wu[$k]) + abs($srcV[$i] - $wv[$k]); } if ($best === null || $sad < $best[0]) $best = [$sad, $m, $wu, $wv]; } [, $mb['uv'], $wu, $wv] = $best; $mb['uvlv'] = []; foreach ([[$srcU, &$wu, &$RU], [$srcV, &$wv, &$RV]] as [$s8, &$w8, &$plane]) { for ($b = 0; $b < 4; $b++) { $at = (($b >> 1) * 4 + 1) * $B + ($b & 1) * 4 + 1; $l = []; $dq = self::quant(self::fdct($s8, (($b >> 1) * 4) * 8 + ($b & 1) * 4, 8, $w8, $at), $Q[4], $Q[5], 110, 115, 0, $l); $mb['uvlv'][] = $l; if (array_filter($dq)) self::idctAdd($dq, 0, $w8, $at); } self::store($plane, $uw, 8, $mx, $my, $w8); } unset($w8, $plane); $mb['skip'] = !array_filter($mb['y2'] ?? []) && !array_filter(array_merge(...$mb['lv'], ...$mb['uvlv'])); return $mb; } /** 二乗誤差 */ private static function sse(array $src, array $ws, int $size): int { $e = 0; for ($y = 0; $y < $size; $y++) { $r = ($y + 1) * self::BPS + 1; for ($x = 0; $x < $size; $x++) { $d = $src[$y * $size + $x] - $ws[$r + $x]; $e += $d * $d; } } return $e; } /** 係数のビット数の大体の見積もり */ private static function bits(array $lv, int $first): int { $bits = 1; for ($n = $first; $n < 16; $n++) { $a = abs($lv[$n]); if ($a) $bits += 3 + 2 * (int)log($a, 2); else $bits++; } return $bits; } /** * 4x4の残差の離散コサイン変換(libwebpと同じ整数の計算) * * @param int $s 元の画素の左上、$strideは1行の数 */ private static function fdct(array $src, int $s, int $stride, array $ws, int $at): array { $tmp = []; for ($i = 0; $i < 4; $i++) { $r = $s + $i * $stride; $p = $at + $i * self::BPS; $d0 = $src[$r] - $ws[$p]; $d1 = $src[$r + 1] - $ws[$p + 1]; $d2 = $src[$r + 2] - $ws[$p + 2]; $d3 = $src[$r + 3] - $ws[$p + 3]; $a0 = $d0 + $d3; $a1 = $d1 + $d2; $a2 = $d1 - $d2; $a3 = $d0 - $d3; $tmp[$i * 4] = ($a0 + $a1) * 8; $tmp[$i * 4 + 1] = ($a2 * 2217 + $a3 * 5352 + 1812) >> 9; $tmp[$i * 4 + 2] = ($a0 - $a1) * 8; $tmp[$i * 4 + 3] = ($a3 * 2217 - $a2 * 5352 + 937) >> 9; } $out = []; for ($i = 0; $i < 4; $i++) { $a0 = $tmp[$i] + $tmp[12 + $i]; $a1 = $tmp[4 + $i] + $tmp[8 + $i]; $a2 = $tmp[4 + $i] - $tmp[8 + $i]; $a3 = $tmp[$i] - $tmp[12 + $i]; $out[$i] = ($a0 + $a1 + 7) >> 4; $out[4 + $i] = (($a2 * 2217 + $a3 * 5352 + 12000) >> 16) + ($a3 !== 0 ? 1 : 0); $out[8 + $i] = ($a0 - $a1 + 7) >> 4; $out[12 + $i] = ($a3 * 2217 - $a2 * 5352 + 51000) >> 16; } ksort($out); return $out; } /** 16個の直流のウォルシュ・アダマール変換 */ private static function fwht(array $in): array { $tmp = []; for ($i = 0; $i < 4; $i++) { $a0 = $in[$i * 4] + $in[$i * 4 + 2]; $a1 = $in[$i * 4 + 1] + $in[$i * 4 + 3]; $a2 = $in[$i * 4 + 1] - $in[$i * 4 + 3]; $a3 = $in[$i * 4] - $in[$i * 4 + 2]; $tmp[$i * 4] = $a0 + $a1; $tmp[$i * 4 + 1] = $a3 + $a2; $tmp[$i * 4 + 2] = $a3 - $a2; $tmp[$i * 4 + 3] = $a0 - $a1; } $out = []; for ($i = 0; $i < 4; $i++) { $a0 = $tmp[$i] + $tmp[8 + $i]; $a1 = $tmp[4 + $i] + $tmp[12 + $i]; $a2 = $tmp[4 + $i] - $tmp[12 + $i]; $a3 = $tmp[$i] - $tmp[8 + $i]; $out[$i] = ($a0 + $a1) >> 1; $out[4 + $i] = ($a3 + $a2) >> 1; $out[8 + $i] = ($a3 - $a2) >> 1; $out[12 + $i] = ($a0 - $a1) >> 1; } ksort($out); return $out; } /** * 量子化 * * @param array $lv 量子化した値(ジグザグの順) * @return array 逆量子化した係数(復元用) */ private static function quant(array $c, int $q0, int $q1, int $bias0, int $bias1, int $first, array &$lv): array { $dq = array_fill(0, 16, 0); $lv = array_fill(0, 16, 0); for ($n = $first; $n < 16; $n++) { $j = self::ZIGZAG[$n]; [$q, $bias] = $n > 0 ? [$q1, $bias1] : [$q0, $bias0]; $level = intdiv(abs($c[$j]) * 256 + $bias * $q, $q * 256); if ($level === 0) continue; $level = min(2047, $level); if ($c[$j] < 0) $level = -$level; $lv[$n] = $level; $dq[$j] = $level * $q; } return $dq; } /** * 全ての係数を書く(statsが配列なら数えるだけ) * * @return array パーティション毎の中身 */ private function tokens(array $mbs, int $mbw, int $parts): array { $tnz = array_fill(0, $mbw, array_fill(0, 9, 0)); $states = []; $cur = -1; foreach ($mbs as $n => $mb) { $mx = $n % $mbw; if ($mx === 0) { $lnz = array_fill(0, 9, 0); if ($this->stats === null) { if ($cur >= 0) $states[$cur] = [$this->out, $this->bottom, $this->erange, $this->shiftCount]; $cur = intdiv($n, $mbw) % $parts; if (isset($states[$cur])) [$this->out, $this->bottom, $this->erange, $this->shiftCount] = $states[$cur]; else $this->startWrite(); } } $t = &$tnz[$mx]; if ($mb['skip']) { for ($i = 0; $i < 8; $i++) $t[$i] = $lnz[$i] = 0; if (!$mb['i4']) $t[8] = $lnz[8] = 0; unset($t); continue; } if (!$mb['i4']) { $t[8] = $lnz[8] = $this->block1(1, $t[8] + $lnz[8], $mb['y2'], 0); $type = 0; $first = 1; } else { $type = 3; $first = 0; } for ($y = 0; $y < 4; $y++) { for ($x = 0; $x < 4; $x++) { $t[$x] = $lnz[$y] = $this->block1($type, $lnz[$y] + $t[$x], $mb['lv'][$y * 4 + $x], $first); } } for ($ch = 0; $ch < 2; $ch++) { for ($y = 0; $y < 2; $y++) { for ($x = 0; $x < 2; $x++) { $ti = 4 + $ch * 2 + $x; $li = 4 + $ch * 2 + $y; $t[$ti] = $lnz[$li] = $this->block1(2, $lnz[$li] + $t[$ti], $mb['uvlv'][$ch * 4 + $y * 2 + $x], 0); } } } unset($t); } if ($this->stats !== null) return []; $states[$cur] = [$this->out, $this->bottom, $this->erange, $this->shiftCount]; $out = []; for ($i = 0; $i < $parts; $i++) { if (isset($states[$i])) [$this->out, $this->bottom, $this->erange, $this->shiftCount] = $states[$i]; else $this->startWrite(); $out[] = $this->finishWrite(); } return $out; } /** * 1ブロックの係数を書く * * @return int 0以外の係数があれば1 */ private function block1(int $type, int $ctx, array $lv, int $first): int { $last = -1; for ($n = 15; $n >= $first; $n--) { if ($lv[$n] !== 0) { $last = $n; break; } } $n = $first; $band = self::BANDS[$n]; if ($last < 0) { $this->tok($type, $band, $ctx, 0, 0); return 0; } while (true) { $this->tok($type, $band, $ctx, 0, 1); while ($lv[$n] === 0) { $this->tok($type, $band, $ctx, 1, 0); $n++; $band = self::BANDS[$n]; $ctx = 0; } $this->tok($type, $band, $ctx, 1, 1); $v = abs($lv[$n]); if ($v === 1) { $this->tok($type, $band, $ctx, 2, 0); $ctx = 1; } else { $this->tok($type, $band, $ctx, 2, 1); if ($v <= 4) { $this->tok($type, $band, $ctx, 3, 0); $this->tok($type, $band, $ctx, 4, $v === 2 ? 0 : 1); if ($v > 2) $this->tok($type, $band, $ctx, 5, $v - 3); } else if ($v <= 10) { $this->tok($type, $band, $ctx, 3, 1); $this->tok($type, $band, $ctx, 6, 0); if ($v <= 6) { $this->tok($type, $band, $ctx, 7, 0); $this->fixed(159, $v - 5); } else { $this->tok($type, $band, $ctx, 7, 1); $this->fixed(165, ($v - 7) >> 1); $this->fixed(145, ($v - 7) & 1); } } else { $this->tok($type, $band, $ctx, 3, 1); $this->tok($type, $band, $ctx, 6, 1); $cat = $v < 19 ? 0 : ($v < 35 ? 1 : ($v < 67 ? 2 : 3)); $this->tok($type, $band, $ctx, 8, $cat >> 1); $this->tok($type, $band, $ctx, 9 + ($cat >> 1), $cat & 1); $extra = $v - 3 - (8 << $cat); $nb = count(self::CAT[$cat]); foreach (self::CAT[$cat] as $k => $prob) $this->fixed($prob, ($extra >> ($nb - 1 - $k)) & 1); } $ctx = 2; } $this->fixed(128, $lv[$n] < 0 ? 1 : 0); $n++; if ($n === 16) return 1; $band = self::BANDS[$n]; if ($n > $last) { $this->tok($type, $band, $ctx, 0, 0); return 1; } } } private function tok(int $t, int $b, int $c, int $i, int $bit): void { if ($this->stats !== null) { $this->stats[$t][$b][$c][$i][$bit] = ($this->stats[$t][$b][$c][$i][$bit] ?? 0) + 1; $this->stats[$t][$b][$c][$i][1 - $bit] ??= 0; return; } $this->put($this->probs[$t][$b][$c][$i], $bit); } private function fixed(int $prob, int $bit): void { if ($this->stats === null) $this->put($prob, $bit); } private function startWrite(): void { $this->out = ''; $this->bottom = 0; $this->erange = 255; $this->shiftCount = 24; } private function put(int $prob, int $bit): void { $split = 1 + ((($this->erange - 1) * $prob) >> 8); if ($bit) { $this->bottom += $split; $this->erange -= $split; } else { $this->erange = $split; } while ($this->erange < 128) { $this->erange <<= 1; if ($this->bottom & 0x80000000) $this->carry(); $this->bottom = ($this->bottom << 1) & 0xffffffff; if (!--$this->shiftCount) { $this->out .= chr($this->bottom >> 24); $this->bottom &= 0xffffff; $this->shiftCount = 8; } } } /** 桁上がりを書いた分に足す */ private function carry(): void { for ($i = strlen($this->out) - 1; $i >= 0 && $this->out[$i] === "\xff"; $i--) $this->out[$i] = "\0"; if ($i >= 0) $this->out[$i] = chr(ord($this->out[$i]) + 1); } private function putValue(int $v, int $n): void { while ($n-- > 0) $this->put(128, ($v >> $n) & 1); } private function finishWrite(): string { $c = $this->shiftCount; $v = $this->bottom; if ($v & (1 << (32 - $c))) $this->carry(); $v = ($v << ($c & 7)) & 0xffffffff; for ($c >>= 3; --$c >= 0;) $v = ($v << 8) & 0xffffffff; for ($c = 0; $c < 4; $c++) { $this->out .= chr($v >> 24); $v = ($v << 8) & 0xffffffff; } return $this->out; } //////// ループフィルター private static function simpleFilter(string &$Y, int $stride, int $mx, int $my, int $limit, bool $inner): void { $o = $my * 16 * $stride + $mx * 16; if ($mx > 0) self::simpleEdge($Y, $o, 1, $stride, 2 * ($limit + 4) + 1); if ($inner) for ($k = 4; $k < 16; $k += 4) self::simpleEdge($Y, $o + $k, 1, $stride, 2 * $limit + 1); if ($my > 0) self::simpleEdge($Y, $o, $stride, 1, 2 * ($limit + 4) + 1); if ($inner) for ($k = 4; $k < 16; $k += 4) self::simpleEdge($Y, $o + $k * $stride, $stride, 1, 2 * $limit + 1); } /** * @param int $hs 端を跨ぐ方向の1画素 * @param int $vs 端に沿う方向の1画素 */ private static function simpleEdge(string &$p, int $o, int $hs, int $vs, int $t2): void { for ($i = 0; $i < 16; $i++, $o += $vs) { $p1 = ord($p[$o - 2 * $hs]); $p0 = ord($p[$o - $hs]); $q0 = ord($p[$o]); $q1 = ord($p[$o + $hs]); if (4 * abs($p0 - $q0) + abs($p1 - $q1) > $t2) continue; self::filter2($p, $o, $hs, $p1, $p0, $q0, $q1); } } 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 { $oy = $my * 16 * $yw + $mx * 16; $ou = $my * 8 * $uw + $mx * 8; if ($mx > 0) { self::edge($Y, $oy, 1, $yw, 16, $limit + 4, $il, $hev, true); self::edge($U, $ou, 1, $uw, 8, $limit + 4, $il, $hev, true); self::edge($V, $ou, 1, $uw, 8, $limit + 4, $il, $hev, true); } if ($inner) { for ($k = 4; $k < 16; $k += 4) self::edge($Y, $oy + $k, 1, $yw, 16, $limit, $il, $hev, false); self::edge($U, $ou + 4, 1, $uw, 8, $limit, $il, $hev, false); self::edge($V, $ou + 4, 1, $uw, 8, $limit, $il, $hev, false); } if ($my > 0) { self::edge($Y, $oy, $yw, 1, 16, $limit + 4, $il, $hev, true); self::edge($U, $ou, $uw, 1, 8, $limit + 4, $il, $hev, true); self::edge($V, $ou, $uw, 1, 8, $limit + 4, $il, $hev, true); } if ($inner) { for ($k = 4; $k < 16; $k += 4) self::edge($Y, $oy + $k * $yw, $yw, 1, 16, $limit, $il, $hev, false); self::edge($U, $ou + 4 * $uw, $uw, 1, 8, $limit, $il, $hev, false); self::edge($V, $ou + 4 * $uw, $uw, 1, 8, $limit, $il, $hev, false); } } /** * 通常のフィルター($mb:マクロブロックの端なら6画素、中なら4画素を変える) */ private static function edge(string &$p, int $o, int $hs, int $vs, int $n, int $thresh, int $it, int $hevT, bool $mb): void { $t2 = 2 * $thresh + 1; for ($i = 0; $i < $n; $i++, $o += $vs) { $p1 = ord($p[$o - 2 * $hs]); $p0 = ord($p[$o - $hs]); $q0 = ord($p[$o]); $q1 = ord($p[$o + $hs]); if (4 * abs($p0 - $q0) + abs($p1 - $q1) > $t2) continue; $p3 = ord($p[$o - 4 * $hs]); $p2 = ord($p[$o - 3 * $hs]); $q2 = ord($p[$o + 2 * $hs]); $q3 = ord($p[$o + 3 * $hs]); 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; if (abs($p1 - $p0) > $hevT || abs($q1 - $q0) > $hevT) { self::filter2($p, $o, $hs, $p1, $p0, $q0, $q1); } else if ($mb) { $a = 3 * ($q0 - $p0) + self::sclip1($p1 - $q1); $a = $a < -128 ? -128 : ($a > 127 ? 127 : $a); $a1 = (27 * $a + 63) >> 7; $a2 = (18 * $a + 63) >> 7; $a3 = (9 * $a + 63) >> 7; $p[$o - 3 * $hs] = chr(self::clip($p2 + $a3)); $p[$o - 2 * $hs] = chr(self::clip($p1 + $a2)); $p[$o - $hs] = chr(self::clip($p0 + $a1)); $p[$o] = chr(self::clip($q0 - $a1)); $p[$o + $hs] = chr(self::clip($q1 - $a2)); $p[$o + 2 * $hs] = chr(self::clip($q2 - $a3)); } else { $a = 3 * ($q0 - $p0); $a1 = self::sclip2(($a + 4) >> 3); $a2 = self::sclip2(($a + 3) >> 3); $a3 = ($a1 + 1) >> 1; $p[$o - 2 * $hs] = chr(self::clip($p1 + $a3)); $p[$o - $hs] = chr(self::clip($p0 + $a2)); $p[$o] = chr(self::clip($q0 - $a1)); $p[$o + $hs] = chr(self::clip($q1 - $a3)); } } } private static function filter2(string &$p, int $o, int $hs, int $p1, int $p0, int $q0, int $q1): void { $a = 3 * ($q0 - $p0) + self::sclip1($p1 - $q1); $a1 = self::sclip2(($a + 4) >> 3); $a2 = self::sclip2(($a + 3) >> 3); $p[$o - $hs] = chr(self::clip($p0 + $a2)); $p[$o] = chr(self::clip($q0 - $a1)); } private static function sclip1(int $v): int { return $v < -128 ? -128 : ($v > 127 ? 127 : $v); } private static function sclip2(int $v): int { return $v < -16 ? -16 : ($v > 15 ? 15 : $v); } private static function clip(int $v): int { return $v < 0 ? 0 : ($v > 255 ? 255 : $v); } //////// 色 /** * YUV 4:2:0をRGBAにする(色差は近い方の行と列に3:1の重みで補間) */ private static function toRgba(string $Y, string $U, string $V, int $yw, int $uw, int $w, int $h): string { static $tab = null; if ($tab === null) { for ($i = 0; $i < 256; $i++) { $tab['y'][$i] = ($i * 19077) >> 8; $tab['rv'][$i] = (($i * 26149) >> 8) - 14234; $tab['gu'][$i] = ($i * 6419) >> 8; $tab['gv'][$i] = (($i * 13320) >> 8) - 8708; $tab['bu'][$i] = (($i * 33050) >> 8) - 17685; } // 14ビットの固定小数点を0~255にする for ($i = -20000; $i < 40000; $i++) $tab['clip'][$i] = $i < 0 ? 0 : ($i > 16383 ? 255 : $i >> 6); } ['y' => $ty, 'rv' => $trv, 'gu' => $tgu, 'gv' => $tgv, 'bu' => $tbu, 'clip' => $clip] = $tab; $rows = ($h + 1) >> 1; $cw = ($w + 1) >> 1; $last = ($w - 1) >> 1; $out = ''; for ($y = 0; $y < $h; $y++) { if ($y === 0) { $n = $f = 0; } else if ($y & 1) { $n = ($y - 1) >> 1; $f = min($n + 1, $rows - 1); } else { $n = $y >> 1; $f = $n - 1; } $nu = array_values(unpack('C*', substr($U, $n * $uw, $cw))); $nv = array_values(unpack('C*', substr($V, $n * $uw, $cw))); $fu = array_values(unpack('C*', substr($U, $f * $uw, $cw))); $fv = array_values(unpack('C*', substr($V, $f * $uw, $cw))); $u = [(3 * $nu[0] + $fu[0] + 2) >> 2]; $v = [(3 * $nv[0] + $fv[0] + 2) >> 2]; for ($x = 1; $x <= $last; $x++) { $nl = $nu[$x - 1]; $nr = $nu[$x]; $fl = $fu[$x - 1]; $fr = $fu[$x]; $avg = $nl + $nr + $fl + $fr + 8; $u[2 * $x - 1] = ((($avg + 2 * ($nr + $fl)) >> 3) + $nl) >> 1; $u[2 * $x] = ((($avg + 2 * ($nl + $fr)) >> 3) + $nr) >> 1; $nl = $nv[$x - 1]; $nr = $nv[$x]; $fl = $fv[$x - 1]; $fr = $fv[$x]; $avg = $nl + $nr + $fl + $fr + 8; $v[2 * $x - 1] = ((($avg + 2 * ($nr + $fl)) >> 3) + $nl) >> 1; $v[2 * $x] = ((($avg + 2 * ($nl + $fr)) >> 3) + $nr) >> 1; } if (!($w & 1)) { $u[$w - 1] = (3 * $nu[$last] + $fu[$last] + 2) >> 2; $v[$w - 1] = (3 * $nv[$last] + $fv[$last] + 2) >> 2; } $row = []; $yrow = unpack('C*', substr($Y, $y * $yw, $w)); for ($x = 0; $x < $w; $x++) { $l = $ty[$yrow[$x + 1]]; $uu = $u[$x]; $vv = $v[$x]; $row[] = $clip[$l + $trv[$vv]]; $row[] = $clip[$l - $tgu[$uu] - $tgv[$vv]]; $row[] = $clip[$l + $tbu[$uu]]; $row[] = 255; } $out .= pack('C*', ...$row); } return $out; } }