/ src / library / av_processing / Vp8Decoder.php
<?php
/**
 * SeekQuarry/Yioop --
 * Open Source Pure PHP Search Engine, Crawler, and Indexer
 *
 * Copyright (C) 2009 - 2026  Chris Pollett chris@pollett.org
 *
 * LICENSE:
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 *
 * END LICENSE
 *
 * @author Chris Pollett chris@pollett.org
 * @license https://www.gnu.org/licenses/ GPL3
 * @link https://www.seekquarry.com/
 * @copyright 2009 - 2026
 * @filesource
 *
 * This file turns a VP8 keyframe into a picture.
 */
namespace seekquarry\yioop\library\av_processing;
/**
 * Vp8Tables the constant tables the VP8 standard publishes, gathered in one
 * place so the decoding code reads as arithmetic rather than as data.
 */
final class Vp8Tables
{
    /**
     * DEFAULT_COEFF_PROBS is how likely each coded value is when a frame
     * starts. A frame may change these afterwards, but a keyframe begins from
     * them.
     * @var mixed
     */
    public const DEFAULT_COEFF_PROBS = [
        [
            [
                [128,128,128,128,128,128,128,128,128,128,128],
                [128,128,128,128,128,128,128,128,128,128,128],
                [128,128,128,128,128,128,128,128,128,128,128]
            ],
            [
                [253,136,254,255,228,219,128,128,128,128,128],
                [189,129,242,255,227,213,255,219,128,128,128],
                [106,126,227,252,214,209,255,255,128,128,128]
            ],
            [
                [1,98,248,255,236,226,255,255,128,128,128],
                [181,133,238,254,221,234,255,154,128,128,128],
                [78,134,202,247,198,180,255,219,128,128,128]
            ],
            [
                [1,185,249,255,243,255,128,128,128,128,128],
                [184,150,247,255,236,224,128,128,128,128,128],
                [77,110,216,255,236,230,128,128,128,128,128]
            ],
            [
                [1,101,251,255,241,255,128,128,128,128,128],
                [170,139,241,252,236,209,255,255,128,128,128],
                [37,116,196,243,228,255,255,255,128,128,128]
            ],
            [
                [1,204,254,255,245,255,128,128,128,128,128],
                [207,160,250,255,238,128,128,128,128,128,128],
                [102,103,231,255,211,171,128,128,128,128,128]
            ],
            [
                [1,152,252,255,240,255,128,128,128,128,128],
                [177,135,243,255,234,225,128,128,128,128,128],
                [80,129,211,255,194,224,128,128,128,128,128]
            ],
            [
                [1,1,255,128,128,128,128,128,128,128,128],
                [246,1,255,128,128,128,128,128,128,128,128],
                [255,128,128,128,128,128,128,128,128,128,128]
            ]
        ],
        [
            [
                [198,35,237,223,193,187,162,160,145,155,62],
                [131,45,198,221,172,176,220,157,252,221,1],
                [68,47,146,208,149,167,221,162,255,223,128]
            ],
            [
                [1,149,241,255,221,224,255,255,128,128,128],
                [184,141,234,253,222,220,255,199,128,128,128],
                [81,99,181,242,176,190,249,202,255,255,128]
            ],
            [
                [1,129,232,253,214,197,242,196,255,255,128],
                [99,121,210,250,201,198,255,202,128,128,128],
                [23,91,163,242,170,187,247,210,255,255,128]
            ],
            [
                [1,200,246,255,234,255,128,128,128,128,128],
                [109,178,241,255,231,245,255,255,128,128,128],
                [44,130,201,253,205,192,255,255,128,128,128]
            ],
            [
                [1,132,239,251,219,209,255,165,128,128,128],
                [94,136,225,251,218,190,255,255,128,128,128],
                [22,100,174,245,186,161,255,199,128,128,128]
            ],
            [
                [1,182,249,255,232,235,128,128,128,128,128],
                [124,143,241,255,227,234,128,128,128,128,128],
                [35,77,181,251,193,211,255,205,128,128,128]
            ],
            [
                [1,157,247,255,236,231,255,255,128,128,128],
                [121,141,235,255,225,227,255,255,128,128,128],
                [45,99,188,251,195,217,255,224,128,128,128]
            ],
            [
                [1,1,251,255,213,255,128,128,128,128,128],
                [203,1,248,255,255,128,128,128,128,128,128],
                [137,1,177,255,224,255,128,128,128,128,128]
            ]
        ],
        [
            [
                [253,9,248,251,207,208,255,192,128,128,128],
                [175,13,224,243,193,185,249,198,255,255,128],
                [73,17,171,221,161,179,236,167,255,234,128]
            ],
            [
                [1,95,247,253,212,183,255,255,128,128,128],
                [239,90,244,250,211,209,255,255,128,128,128],
                [155,77,195,248,188,195,255,255,128,128,128]
            ],
            [
                [1,24,239,251,218,219,255,205,128,128,128],
                [201,51,219,255,196,186,128,128,128,128,128],
                [69,46,190,239,201,218,255,228,128,128,128]
            ],
            [
                [1,191,251,255,255,128,128,128,128,128,128],
                [223,165,249,255,213,255,128,128,128,128,128],
                [141,124,248,255,255,128,128,128,128,128,128]
            ],
            [
                [1,16,248,255,255,128,128,128,128,128,128],
                [190,36,230,255,236,255,128,128,128,128,128],
                [149,1,255,128,128,128,128,128,128,128,128]
            ],
            [
                [1,226,255,128,128,128,128,128,128,128,128],
                [247,192,255,128,128,128,128,128,128,128,128],
                [240,128,255,128,128,128,128,128,128,128,128]
            ],
            [
                [1,134,252,255,255,128,128,128,128,128,128],
                [213,62,250,255,255,128,128,128,128,128,128],
                [55,93,255,128,128,128,128,128,128,128,128]
            ],
            [
                [128,128,128,128,128,128,128,128,128,128,128],
                [128,128,128,128,128,128,128,128,128,128,128],
                [128,128,128,128,128,128,128,128,128,128,128]
            ]
        ],
        [
            [
                [202,24,213,235,186,191,220,160,240,175,255],
                [126,38,182,232,169,184,228,174,255,187,128],
                [61,46,138,219,151,178,240,170,255,216,128]
            ],
            [
                [1,112,230,250,199,191,247,159,255,255,128],
                [166,109,228,252,211,215,255,174,128,128,128],
                [39,77,162,232,172,180,245,178,255,255,128]
            ],
            [
                [1,52,220,246,198,199,249,220,255,255,128],
                [124,74,191,243,183,193,250,221,255,255,128],
                [24,71,130,219,154,170,243,182,255,255,128]
            ],
            [
                [1,182,225,249,219,240,255,224,128,128,128],
                [149,150,226,252,216,205,255,171,128,128,128],
                [28,108,170,242,183,194,254,223,255,255,128]
            ],
            [
                [1,81,230,252,204,203,255,192,128,128,128],
                [123,102,209,247,188,196,255,233,128,128,128],
                [20,95,153,243,164,173,255,203,128,128,128]
            ],
            [
                [1,222,248,255,216,213,128,128,128,128,128],
                [168,175,246,252,235,205,255,255,128,128,128],
                [47,116,215,255,211,212,255,255,128,128,128]
            ],
            [
                [1,121,236,253,212,214,255,255,128,128,128],
                [141,84,213,252,201,202,255,219,128,128,128],
                [42,80,160,240,162,185,255,205,128,128,128]
            ],
            [
                [1,1,255,128,128,128,128,128,128,128,128],
                [244,1,255,128,128,128,128,128,128,128,128],
                [238,1,255,128,128,128,128,128,128,128,128]
            ]
        ]
    ];
    /**
     * COEFF_UPDATE_PROBS is how likely a frame is to change each of those, read
     * before any change is read.
     * @var mixed
     */
    public const COEFF_UPDATE_PROBS = [
        [
            [
                [255,255,255,255,255,255,255,255,255,255,255],
                [255,255,255,255,255,255,255,255,255,255,255],
                [255,255,255,255,255,255,255,255,255,255,255]
            ],
            [
                [176,246,255,255,255,255,255,255,255,255,255],
                [223,241,252,255,255,255,255,255,255,255,255],
                [249,253,253,255,255,255,255,255,255,255,255]
            ],
            [
                [255,244,252,255,255,255,255,255,255,255,255],
                [234,254,254,255,255,255,255,255,255,255,255],
                [253,255,255,255,255,255,255,255,255,255,255]
            ],
            [
                [255,246,254,255,255,255,255,255,255,255,255],
                [239,253,254,255,255,255,255,255,255,255,255],
                [254,255,254,255,255,255,255,255,255,255,255]
            ],
            [
                [255,248,254,255,255,255,255,255,255,255,255],
                [251,255,254,255,255,255,255,255,255,255,255],
                [255,255,255,255,255,255,255,255,255,255,255]
            ],
            [
                [255,253,254,255,255,255,255,255,255,255,255],
                [251,254,254,255,255,255,255,255,255,255,255],
                [254,255,254,255,255,255,255,255,255,255,255]
            ],
            [
                [255,254,253,255,254,255,255,255,255,255,255],
                [250,255,254,255,254,255,255,255,255,255,255],
                [254,255,255,255,255,255,255,255,255,255,255]
            ],
            [
                [255,255,255,255,255,255,255,255,255,255,255],
                [255,255,255,255,255,255,255,255,255,255,255],
                [255,255,255,255,255,255,255,255,255,255,255]
            ]
        ],
        [
            [
                [217,255,255,255,255,255,255,255,255,255,255],
                [225,252,241,253,255,255,254,255,255,255,255],
                [234,250,241,250,253,255,253,254,255,255,255]
            ],
            [
                [255,254,255,255,255,255,255,255,255,255,255],
                [223,254,254,255,255,255,255,255,255,255,255],
                [238,253,254,254,255,255,255,255,255,255,255]
            ],
            [
                [255,248,254,255,255,255,255,255,255,255,255],
                [249,254,255,255,255,255,255,255,255,255,255],
                [255,255,255,255,255,255,255,255,255,255,255]
            ],
            [
                [255,253,255,255,255,255,255,255,255,255,255],
                [247,254,255,255,255,255,255,255,255,255,255],
                [255,255,255,255,255,255,255,255,255,255,255]
            ],
            [
                [255,253,254,255,255,255,255,255,255,255,255],
                [252,255,255,255,255,255,255,255,255,255,255],
                [255,255,255,255,255,255,255,255,255,255,255]
            ],
            [
                [255,254,254,255,255,255,255,255,255,255,255],
                [253,255,255,255,255,255,255,255,255,255,255],
                [255,255,255,255,255,255,255,255,255,255,255]
            ],
            [
                [255,254,253,255,255,255,255,255,255,255,255],
                [250,255,255,255,255,255,255,255,255,255,255],
                [254,255,255,255,255,255,255,255,255,255,255]
            ],
            [
                [255,255,255,255,255,255,255,255,255,255,255],
                [255,255,255,255,255,255,255,255,255,255,255],
                [255,255,255,255,255,255,255,255,255,255,255]
            ]
        ],
        [
            [
                [186,251,250,255,255,255,255,255,255,255,255],
                [234,251,244,254,255,255,255,255,255,255,255],
                [251,251,243,253,254,255,254,255,255,255,255]
            ],
            [
                [255,253,254,255,255,255,255,255,255,255,255],
                [236,253,254,255,255,255,255,255,255,255,255],
                [251,253,253,254,254,255,255,255,255,255,255]
            ],
            [
                [255,254,254,255,255,255,255,255,255,255,255],
                [254,254,254,255,255,255,255,255,255,255,255],
                [255,255,255,255,255,255,255,255,255,255,255]
            ],
            [
                [255,254,255,255,255,255,255,255,255,255,255],
                [254,254,255,255,255,255,255,255,255,255,255],
                [254,255,255,255,255,255,255,255,255,255,255]
            ],
            [
                [255,255,255,255,255,255,255,255,255,255,255],
                [254,255,255,255,255,255,255,255,255,255,255],
                [255,255,255,255,255,255,255,255,255,255,255]
            ],
            [
                [255,255,255,255,255,255,255,255,255,255,255],
                [255,255,255,255,255,255,255,255,255,255,255],
                [255,255,255,255,255,255,255,255,255,255,255]
            ],
            [
                [255,255,255,255,255,255,255,255,255,255,255],
                [255,255,255,255,255,255,255,255,255,255,255],
                [255,255,255,255,255,255,255,255,255,255,255]
            ],
            [
                [255,255,255,255,255,255,255,255,255,255,255],
                [255,255,255,255,255,255,255,255,255,255,255],
                [255,255,255,255,255,255,255,255,255,255,255]
            ]
        ],
        [
            [
                [248,255,255,255,255,255,255,255,255,255,255],
                [250,254,252,254,255,255,255,255,255,255,255],
                [248,254,249,253,255,255,255,255,255,255,255]
            ],
            [
                [255,253,253,255,255,255,255,255,255,255,255],
                [246,253,253,255,255,255,255,255,255,255,255],
                [252,254,251,254,254,255,255,255,255,255,255]
            ],
            [
                [255,254,252,255,255,255,255,255,255,255,255],
                [248,254,253,255,255,255,255,255,255,255,255],
                [253,255,254,254,255,255,255,255,255,255,255]
            ],
            [
                [255,251,254,255,255,255,255,255,255,255,255],
                [245,251,254,255,255,255,255,255,255,255,255],
                [253,253,254,255,255,255,255,255,255,255,255]
            ],
            [
                [255,251,253,255,255,255,255,255,255,255,255],
                [252,253,254,255,255,255,255,255,255,255,255],
                [255,254,255,255,255,255,255,255,255,255,255]
            ],
            [
                [255,252,255,255,255,255,255,255,255,255,255],
                [249,255,254,255,255,255,255,255,255,255,255],
                [255,255,254,255,255,255,255,255,255,255,255]
            ],
            [
                [255,255,253,255,255,255,255,255,255,255,255],
                [250,255,255,255,255,255,255,255,255,255,255],
                [255,255,255,255,255,255,255,255,255,255,255]
            ],
            [
                [255,255,255,255,255,255,255,255,255,255,255],
                [254,255,255,255,255,255,255,255,255,255,255],
                [255,255,255,255,255,255,255,255,255,255,255]
            ]
        ]
    ];
    /**
     * BMODE_PROBS is how likely each way of guessing a four by four block is,
     * given how its neighbors above and to the left were guessed.
     * @var mixed
     */
    public const BMODE_PROBS = [
        [
            [231,120,48,89,115,113,120,152,112],
            [152,179,64,126,170,118,46,70,95], [175,69,143,80,85,82,72,155,103],
            [56,58,10,171,218,189,17,13,152], [144,71,10,38,171,213,144,34,26],
            [114,26,17,163,44,195,21,10,173], [121,24,80,195,26,62,44,64,85],
            [170,46,55,19,136,160,33,206,71], [63,20,8,114,114,208,12,9,226],
            [81,40,11,96,182,84,29,16,36]
        ],
        [
            [134,183,89,137,98,101,106,165,148],
            [72,187,100,130,157,111,32,75,80], [66,102,167,99,74,62,40,234,128],
            [41,53,9,178,241,141,26,8,107], [104,79,12,27,217,255,87,17,7],
            [74,43,26,146,73,166,49,23,157], [65,38,105,160,51,52,31,115,128],
            [87,68,71,44,114,51,15,186,23], [47,41,14,110,182,183,21,17,194],
            [66,45,25,102,197,189,23,18,22]
        ],
        [
            [88,88,147,150,42,46,45,196,205], [43,97,183,117,85,38,35,179,61],
            [39,53,200,87,26,21,43,232,171], [56,34,51,104,114,102,29,93,77],
            [107,54,32,26,51,1,81,43,31], [39,28,85,171,58,165,90,98,64],
            [34,22,116,206,23,34,43,166,73], [68,25,106,22,64,171,36,225,114],
            [34,19,21,102,132,188,16,76,124], [62,18,78,95,85,57,50,48,51]
        ],
        [
            [193,101,35,159,215,111,89,46,111],
            [60,148,31,172,219,228,21,18,111],
            [112,113,77,85,179,255,38,120,114], [40,42,1,196,245,209,10,25,109],
            [100,80,8,43,154,1,51,26,71], [88,43,29,140,166,213,37,43,154],
            [61,63,30,155,67,45,68,1,209], [142,78,78,16,255,128,34,197,171],
            [41,40,5,102,211,183,4,1,221], [51,50,17,168,209,192,23,25,82]
        ],
        [
            [125,98,42,88,104,85,117,175,82], [95,84,53,89,128,100,113,101,45],
            [75,79,123,47,51,128,81,171,1], [57,17,5,71,102,57,53,41,49],
            [115,21,2,10,102,255,166,23,6], [38,33,13,121,57,73,26,1,85],
            [41,10,67,138,77,110,90,47,114], [101,29,16,10,85,128,101,196,26],
            [57,18,10,102,102,213,34,20,43], [117,20,15,36,163,128,68,1,26]
        ],
        [
            [138,31,36,171,27,166,38,44,229], [67,87,58,169,82,115,26,59,179],
            [63,59,90,180,59,166,93,73,154], [40,40,21,116,143,209,34,39,175],
            [57,46,22,24,128,1,54,17,37], [47,15,16,183,34,223,49,45,183],
            [46,17,33,183,6,98,15,32,183], [65,32,73,115,28,128,23,128,205],
            [40,3,9,115,51,192,18,6,223], [87,37,9,115,59,77,64,21,47]
        ],
        [
            [104,55,44,218,9,54,53,130,226], [64,90,70,205,40,41,23,26,57],
            [54,57,112,184,5,41,38,166,213], [30,34,26,133,152,116,10,32,134],
            [75,32,12,51,192,255,160,43,51], [39,19,53,221,26,114,32,73,255],
            [31,9,65,234,2,15,1,118,73], [88,31,35,67,102,85,55,186,85],
            [56,21,23,111,59,205,45,37,192], [55,38,70,124,73,102,1,34,98]
        ],
        [
            [102,61,71,37,34,53,31,243,192], [69,60,71,38,73,119,28,222,37],
            [68,45,128,34,1,47,11,245,171], [62,17,19,70,146,85,55,62,70],
            [75,15,9,9,64,255,184,119,16], [37,43,37,154,100,163,85,160,1],
            [63,9,92,136,28,64,32,201,85], [86,6,28,5,64,255,25,248,1],
            [56,8,17,132,137,255,55,116,128], [58,15,20,82,135,57,26,121,40]
        ],
        [
            [164,50,31,137,154,133,25,35,218], [51,103,44,131,131,123,31,6,158],
            [86,40,64,135,148,224,45,183,128], [22,26,17,131,240,154,14,1,209],
            [83,12,13,54,192,255,68,47,28], [45,16,21,91,64,222,7,1,197],
            [56,21,39,155,60,138,23,102,213], [85,26,85,85,128,128,32,146,171],
            [18,11,7,63,144,171,4,4,246], [35,27,10,146,174,171,12,26,128]
        ],
        [
            [190,80,35,99,180,80,126,54,45], [85,126,47,87,176,51,41,20,32],
            [101,75,128,139,118,146,116,128,85], [56,41,15,176,236,85,37,9,62],
            [146,36,19,30,171,255,97,27,20], [71,30,17,119,118,255,17,18,138],
            [101,38,60,138,55,70,43,26,142], [138,45,61,62,219,1,81,188,64],
            [32,41,20,117,151,142,20,21,163], [112,19,12,61,195,128,48,4,24]
        ]
    ];
    /**
     * DC_QLOOKUP is the step the first value of a block is scaled by, one entry
     * for each quantizer the format allows.
     * @var mixed
     */
    public const DC_QLOOKUP
        = [
            4, 5, 6, 7, 8, 9, 10, 10, 11, 12, 13, 14, 15, 16, 17, 17, 18, 19,
            20, 20, 21, 21, 22, 22, 23, 23, 24, 25, 25, 26, 27, 28, 29, 30, 31,
            32, 33, 34, 35, 36, 37, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 46,
            47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63,
            64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 76, 77, 78, 79,
            80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 91, 93, 95, 96, 98, 100,
            101, 102, 104, 106, 108, 110, 112, 114, 116, 118, 122, 124, 126,
            128,
        130,132,134,136,138,140,143,145,148,151,154,157];
    /**
     * AC_QLOOKUP is the step the rest of a block's values are scaled by, one
     * entry per quantizer.
     * @var mixed
     */
    public const AC_QLOOKUP
        = [
            4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21,
            22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38,
            39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55,
            56, 57, 58, 60, 62, 64, 66, 68, 70, 72, 74, 76, 78, 80, 82, 84, 86,
            88, 90, 92, 94, 96, 98, 100, 102, 104, 106, 108, 110, 112, 114, 116,
            119, 122, 125, 128, 131, 134, 137, 140, 143, 146, 149, 152, 155,
            158, 161, 164, 167, 170, 173, 177, 181, 185, 189, 193, 197, 201,
            205, 209, 213, 217, 221, 225, 229, 234, 239, 245, 249, 254, 259,
            264, 269, 274, 279,
        284];
    /**
     * COEFF_BAND is which group of probabilities each place in a block reads
     * with. Places near the start of a block behave differently from those near
     * the end, so they are grouped.
     * @var mixed
     */
    public const COEFF_BAND = [0,1,2,3,6,4,5,6,6,6,6,6,6,6,6,7];
}

/**
 * Vp8BoolDecoder vP8 boolean entropy decoder (a numbered internet standard 6386
 * section 7). A binary arithmetic coder: each call splits the current range
 * according to a probability and returns which side of the split the value
 * falls on.
 */
final class Vp8BoolDecoder
{
    /**
     * $bytes stores the part of the frame being read. VP8 splits a frame's
     * values across parts that can be read side by side.
     * @var string
     */
    private string $bytes;
    /**
     * $position stores how far into those bytes the reading has gone.
     * @var int
     */
    private int $position;
    /**
     * $end stores where this part stops. Reading past it gives zeros.
     * @var int
     */
    private int $end;
    /**
     * $range stores how wide the range of values still in play is. Each bit
     * read narrows it by how likely that bit was.
     * @var int
     */
    private int $range = 255;
    /**
     * $range_position stores where in that range the number being read sits.
     * @var int
     */
    private int $range_position = 0;
    /**
     * $bit_count stores how many bits of the current byte are still unread, so
     * the reader knows when to take in another.
     * @var int
     */
    private int $bit_count = 0;
    /**
     * __construct sets up a reader over one arithmetic coded part of a frame.
     *
     * @param string $bytes the bytes to read
     * @param int $offset where in the file to start reading
     * @param int $length how many bytes to read
     */
    public function __construct(string $bytes, int $offset, int $length)
    {
        $this->bytes = $bytes;
        $this->position = $offset;
        $this->end = $offset + $length;
        if ($length < 2 || $this->end > strlen($bytes)) {
            throw new VideoException('VP8 partition is truncated');
        }
        $this->range_position = (ord($bytes[$this->position]) << 8) |
            ord($bytes[$this->position + 1]);
        $this->position += 2;
    }
    /**
     * readOneBit reads one decision, given how likely a zero is.
     *
     * @param int $prob the probability a value is read with
     * @return int what was read
     */
    public function readOneBit(int $prob): int
    {
        $split = 1 + ((($this->range - 1) * $prob) >> 8);
        $big_split = $split << 8;
        if ($this->range_position >= $big_split) {
            $result = 1;
            $this->range -= $split;
            $this->range_position -= $big_split;
        } else {
            $result = 0;
            $this->range = $split;
        }
        while ($this->range < 128) {
            $this->range_position <<= 1;
            $this->range <<= 1;
            if (++$this->bit_count === 8) {
                $this->bit_count = 0;
                $this->range_position |= ($this->position < $this->end)
                    ? ord($this->bytes[$this->position]) : 0;
                $this->position++;
            }
        }
        return $result;
    }
    /**
     * literal reads a plain number of the given width, its highest bit first.
     * VP8 writes most of its numbers through the probabilities, but a few
     * fields are written straight, and this reads those.
     *
     * @param int $bits the reader the stream's bits are taken from
     * @return int what was read
     */
    public function literal(int $bits): int
    {
        $variant = 0;
        for ($i = 0; $i < $bits; $i++) {
            $variant = ($variant << 1) | $this->readOneBit(128);
        }
        return $variant;
    }
    /**
     * readSignedValue reads such a plain number and then a bit saying whether
     * it is negative, which is how VP8 writes the nudges it applies to a
     * quantizer or to the smoothing.
     *
     * @param int $bits the reader the stream's bits are taken from
     * @return int what was read
     */
    public function readSignedValue(int $bits): int
    {
        $variant = $this->literal($bits);
        return $this->readOneBit(128) ? -$variant : $variant;
    }
    /**
     * readFlag reads one plain bit, where either value is equally likely.
     *
     * @return int what was read
     */
    public function readFlag(): int
    {
        return $this->readOneBit(128);
    }
    /**
     * walkTree walk a token tree. Nodes are pairs; a negative entry is the leaf
     * value encoded as -(value + 1). read from
     *
     * @param array $tree the tree of choices a value is
     * @param array $probs the probabilities values are read with
     * @param int $node where in the tree the reading is
     * @return int what was read
     */
    public function walkTree(array $tree, array $probs, int $node = 0): int
    {
        $guard = 0;
        while (true) {
            $next = $tree[$node][$this->readOneBit($probs[$node])];
            if ($next < 0) {
                return -$next - 1;
            }
            $node = $next;
            if (++$guard > 32) {
                throw new VideoException('malformed VP8 token tree');
            }
        }
    }
}

/**
 * Vp8Decoder vP8 keyframe decoder. Keyframes need no reference frames, so
 * motion vectors, inter modes and reference buffers are all absent: every
 * macroblock is predicted from its own already-reconstructed neighbors. What
 * remains is the frame header, the intra mode tree, the coefficient tokens
 * spread across one to eight partitions, two inverse transforms, ten sub-block
 * predictors and the deblocking filter.
 */
final class Vp8Decoder
{
    /**
     * smooth3 average of three neighboring samples, weighted toward the middle
     * one. This is how the format smooths an edge it predicts from.
     *
     * @param int $first sample before the middle one
     * @param int $middle sample the result sits on
     * @param int $last sample after the middle one
     * @return int the smoothed value
     */
    private static function smooth3($first, $middle, $last)
    {
        return ($first + 2 * $middle + $last + 2) >> 2;
    }
    /**
     * smooth2 average of two neighboring samples, rounded upward.
     *
     * @param int $first one sample
     * @param int $second the sample beside it
     * @return int the average
     */
    private static function smooth2($first, $second)
    {
        return ($first + $second + 1) >> 1;
    }
    /**
     * DC_PRED is guessing a whole block as one value, the average of its
     * neighbors.
     * @var mixed
     */
    private const DC_PRED = 0;
    /**
     * V_PRED is guessing a block from the row of samples above it.
     * @var mixed
     */
    private const V_PRED = 1;
    /**
     * H_PRED is guessing a block from the column of samples to its left.
     * @var mixed
     */
    private const H_PRED = 2;
    /**
     * TM_PRED is guessing a block from the row above and the column to the left
     * together, following the slope between them.
     * @var mixed
     */
    private const TM_PRED = 3;
    /**
     * B_PRED is guessing a macroblock in sixteen smaller squares, each with a
     * way of its own.
     * @var mixed
     */
    private const B_PRED = 4;
    /**
     * B_DC is sub-block modes, in specification order.
     * @var mixed
     */
    private const B_DC = 0;
    /**
     * B_TM is guessing a four by four block from the row above and the column
     * to the left together, following the slope between them.
     * @var mixed
     */
    private const B_TM = 1;
    /**
     * B_VE is guessing a block straight down from the row above it.
     * @var mixed
     */
    private const B_VE = 2;
    /**
     * B_HE is guessing a block straight across from the column to its left.
     * @var mixed
     */
    private const B_HE = 3;
    /**
     * B_LD is guessing a block along a slope running down and to the left.
     * @var mixed
     */
    private const B_LD = 4;
    /**
     * B_RD is guessing a block along a slope running down and to the right.
     * @var mixed
     */
    private const B_RD = 5;
    /**
     * B_VR marks guessing a block along a steep slope that leans to
     * the right.
     * @var mixed
     */
    private const B_VR = 6;
    /**
     * B_VL is guessing a block along a steep slope leaning left.
     * @var mixed
     */
    private const B_VL = 7;
    /**
     * B_HD is guessing a block along a shallow slope leaning down.
     * @var mixed
     */
    private const B_HD = 8;
    /**
     * B_HU is guessing a block along a shallow slope leaning up.
     * @var mixed
     */
    private const B_HU = 9;
    /**
     * YMODE_TREE is the tree of choices that says how a whole
     * macroblock of brightness was guessed, for a frame that stands on
     * its own. A leaf of the tree holds one less than the way it
     * stands for, made negative, which is how a leaf is told from a
     * branch.
     */
    private const YMODE_TREE = [
        [-(self::B_PRED + 1), 1],
        [2, 3],
        [-(self::DC_PRED + 1), -(self::V_PRED + 1)],
        [-(self::H_PRED + 1), -(self::TM_PRED + 1)],
    ];
    /**
     * YMODE_PROBS is how likely each way of guessing a whole macroblock of
     * brightness is.
     * @var mixed
     */
    private const YMODE_PROBS = [145, 156, 163, 128];
    /**
     * UVMODE_TREE is the tree of choices that says how the color planes of a
     * macroblock were guessed.
     * @var mixed
     */
    private const UVMODE_TREE = [
        [-(self::DC_PRED + 1), 1],
        [-(self::V_PRED + 1), 2],
        [-(self::H_PRED + 1), -(self::TM_PRED + 1)],
    ];
    /**
     * UVMODE_PROBS is how likely each of those choices is.
     * @var mixed
     */
    private const UVMODE_PROBS = [142, 114, 183];
    /**
     * BMODE_TREE is the tree of choices that says how one four by four block
     * was guessed.
     * @var mixed
     */
    private const BMODE_TREE = [
        [-(self::B_DC + 1), 1],
        [-(self::B_TM + 1), 2],
        [-(self::B_VE + 1), 3],
        [4, 6],
        [-(self::B_HE + 1), 5],
        [-(self::B_RD + 1), -(self::B_VR + 1)],
        [-(self::B_LD + 1), 7],
        [-(self::B_VL + 1), 8],
        [-(self::B_HD + 1), -(self::B_HU + 1)],
    ];
    /**
     * SEGMENT_TREE is the tree of choices that says which segment a macroblock
     * belongs to.
     * @var mixed
     */
    private const SEGMENT_TREE = [
        [1, 2],
        [-(0 + 1), -(1 + 1)],
        [-(2 + 1), -(3 + 1)],
    ];
    /**
     * YMODE_TO_BMODE is the sixteen by sixteen mode a whole macroblock
     * contributes to its neighbors' context.
     */
    private const YMODE_TO_BMODE = [
        self::DC_PRED => self::B_DC,
        self::V_PRED  => self::B_VE,
        self::H_PRED  => self::B_HE,
        self::TM_PRED => self::B_TM,
    ];
    /**
     * COEFF_TREE is the tree of choices that reads one coded value of a block,
     * from whether there is a value at all up to how large it is.
     * @var mixed
     */
    private const COEFF_TREE = [
        /* leaf -1 marks end of block */
        [-1, 1],
        /* literal 0 */
        [-(0 + 2), 2],
        /* literal 1 */
        [-(1 + 2), 3],
        [4, 6],
        [-(2 + 2), 5],
        [-(3 + 2), -(4 + 2)],
        [7, 8],
        /* categories 1 and 2 */
        [-(11 + 2), -(12 + 2)],
        [9, 10],
        /* categories 3 and 4 */
        [-(13 + 2), -(14 + 2)],
        /* categories 5 and 6 */
        [-(15 + 2), -(16 + 2)],
    ];
    /**
     * CAT_PROBS is extra-bit probabilities and base values for the six
     * magnitude categories.
     */
    private const CAT_PROBS = [
        [159], [165, 145], [173, 148, 140], [176, 155, 140, 135],
        [180, 157, 141, 134, 130],
        [254, 254, 243, 230, 196, 177, 153, 140, 133, 130, 129],
    ];
    /**
     * CAT_BASE is the smallest value each of the six large-value groups stands
     * for; the bits that follow say how much larger.
     * @var mixed
     */
    private const CAT_BASE = [5, 7, 11, 19, 35, 67];
    /**
     * $frame_width stores how wide the frame is, in pixels, as its header says.
     * @var int
     */
    private int $frame_width = 0;
    /**
     * $frame_height stores how tall the frame is, in pixels.
     * @var int
     */
    private int $frame_height = 0;
    /**
     * $macroblock_width stores how many macroblocks the frame is across. A
     * macroblock
     * is the sixteen by sixteen square VP8 works in.
     * @var int
     */
    private int $macroblock_width = 0;
    /**
     * $macroblock_height stores how many macroblocks the frame is down.
     * @var int
     */
    private int $macroblock_height = 0;
    /**
     * $segment_enabled stores whether the frame is split into segments that
     * carry their own quantizer and smoothing.
     * @var bool
     */
    private bool $segment_enabled = false;
    /**
     * $segment_absolute stores whether a segment's settings replace the frame's
     * or are added to them.
     * @var bool
     */
    private bool $segment_absolute = false;
    /**
     * $segment_quant stores the quantizer each segment uses.
     * @var array
     */
    private array $segment_quant = [0, 0, 0, 0];
    /**
     * $segment_filter stores how strongly each segment's edges are smoothed.
     * @var array
     */
    private array $segment_filter = [0, 0, 0, 0];
    /**
     * $segment_probs stores how likely each segment is, used while reading
     * which segment a macroblock belongs to.
     * @var array
     */
    private array $segment_probs = [255, 255, 255];
    /**
     * $update_segment_map stores whether this frame says afresh which
     * macroblock belongs to which segment.
     * @var bool
     */
    private bool $update_segment_map = false;
    /**
     * $filter_simple stores whether the simpler of VP8's two edge smoothings is
     * used.
     * @var bool
     */
    private bool $filter_simple = false;
    /**
     * $filter_level stores how strongly block edges are smoothed. Zero leaves
     * them alone.
     * @var int
     */
    private int $filter_level = 0;
    /**
     * $sharpness stores how much that smoothing is held back at sharp edges.
     * @var int
     */
    private int $sharpness = 0;
    /**
     * $smoothing_delta_enabled stores whether the smoothing is nudged by how a
     * macroblock was guessed.
     * @var bool
     */
    private bool $smoothing_delta_enabled = false;
    /**
     * $reference_smoothing_deltas stores those nudges, by which frame a
     * macroblock
     * leans on.
     * @var array
     */
    private array $reference_smoothing_deltas = [0, 0, 0, 0];
    /**
     * $mode_smoothing_deltas stores those nudges, by how a macroblock was
     * guessed.
     * @var array
     */
    private array $mode_smoothing_deltas = [0, 0, 0, 0];
    /**
     * $value_probs stores how likely each value is, read from the frame's
     * header and used for every block after it.
     * @var array
     */
    private array $value_probs = [];
    /**
     * $skip_enabled stores whether the frame says which macroblocks carry no
     * values at all.
     * @var bool
     */
    private bool $skip_enabled = false;
    /**
     * $skip_prob stores how likely a macroblock is to be skipped that way.
     * @var int
     */
    private int $skip_prob = 0;
    /**
     * $quantizer_steps stores the steps each segment's values are
     * scaled by: one for the first brightness value, one for the
     * rest, two more for the second brightness plane, and two for the
     * color planes.
     * @var array
     */
    private array $quant = [];
    /**
     * $planes stores the brightness and color values of the picture being
     * built.
     * @var array
     */
    private array $planes = [];
    /**
     * $strides stores how many values one row of each plane takes.
     * @var array
     */
    private array $strides = [];
    /**
     * $offsets stores where the picture proper starts inside each plane, since
     * the decoder works in whole macroblocks.
     * @var array
     */
    private array $offsets = [];
    /**
     * $macroblock_y_mode stores how the brightness of each macroblock was
     * guessed. A
     * neighbor's guess is needed while reading the next.
     * @var array
     */
    private array $macroblock_y_mode = [];
    /**
     * $macroblock_segment stores which segment each macroblock belongs to.
     * @var array
     */
    private array $macroblock_segment = [];
    /**
     * $macroblock_skip stores whether each macroblock carried any values.
     * @var array
     */
    private array $macroblock_skip = [];
    /**
     * $macroblock_has_values stores whether each macroblock's values were
     * read, which
     * the smoothing looks at to decide whether an edge needs work.
     * @var array
     */
    private array $macroblock_has_values = [];
    /**
     * $block_modes stores how each of the sixteen smaller squares of a
     * macroblock
     * was guessed, where the macroblock was split.
     * @var array
     */
    private array $block_modes = [];
    /**
     * decodeKeyframe decode a VP8 keyframe.
     *
     * @return VideoPicture what was read
     * @param string $frame the frame being built
     */
    public function decodeKeyframe(string $frame): VideoPicture
    {
        if (strlen($frame) < 10) {
            throw new VideoException('VP8 frame is too short');
        }
        $tag = ord($frame[0]) | (ord($frame[1]) << 8) | (ord($frame[2]) << 16);
        if (($tag & 1) !== 0) {
            throw new VideoException('only VP8 keyframes can be decoded');
        }
        $first_part_size = ($tag >> 5) & 0x7FFFF;
        if (substr($frame, 3, 3) !== "\x9d\x01\x2a") {
            throw new VideoException('missing VP8 keyframe start code');
        }
        $wide = (ord($frame[6]) | (ord($frame[7]) << 8)) & 0x3FFF;
        $header = (ord($frame[8]) | (ord($frame[9]) << 8)) & 0x3FFF;
        if ($wide < 1 || $header < 1 || $wide > 16383 || $header > 16383) {
            throw new VideoException('bad VP8 frame size');
        }
        $this->frame_width = $wide;
        $this->frame_height = $header;
        $this->macroblock_width = intdiv($wide + 15, 16);
        $this->macroblock_height = intdiv($header + 15, 16);
        $header_start = 10;
        if ($header_start + $first_part_size > strlen($frame)) {
            throw new VideoException('VP8 first partition runs past the frame');
        }
        $chunk = new Vp8BoolDecoder($frame, $header_start, $first_part_size);
        $this->parseHeader($chunk);
        $partitions = $this->splitPartitions($frame, $header_start
            + $first_part_size, $chunk);
        $this->allocatePlanes();
        $this->decodeMacroblocks($chunk, $partitions);
        $this->loopFilter();
        return $this->toPicture();
    }
    /* header */
    /**
     * parseHeader reads the frame header: how the picture is split into
     * segments, how strongly it is filtered, how it is quantized, and the
     * probabilities its coefficients are coded with. from
     *
     * @param Vp8BoolDecoder $chunk the reader this part of the frame is taken
     */
    private function parseHeader(Vp8BoolDecoder $chunk): void
    {
        /* color space */
        $chunk->readFlag();
        /* clamping type */
        $chunk->readFlag();
        $this->segment_enabled = $chunk->readFlag() === 1;
        $this->update_segment_map = false;
        $this->segment_probs = [255, 255, 255];
        if ($this->segment_enabled) {
            $this->parseSegmentation($chunk);
        } else {
            $this->segment_quant = [0, 0, 0, 0];
            $this->segment_filter = [0, 0, 0, 0];
        }
        $this->filter_simple = $chunk->readFlag() === 1;
        $this->filter_level = $chunk->literal(6);
        $this->sharpness = $chunk->literal(3);
        $this->smoothing_delta_enabled = $chunk->readFlag() === 1;
        if ($this->smoothing_delta_enabled && $chunk->readFlag() === 1) {
            for ($i = 0; $i < 4; $i++) {
                if ($chunk->readFlag() === 1) {
                    $this->reference_smoothing_deltas[$i] = $chunk
                        ->readSignedValue(6);
                }
            }
            for ($i = 0; $i < 4; $i++) {
                if ($chunk->readFlag() === 1) {
                    $this->mode_smoothing_deltas[$i] = $chunk
                        ->readSignedValue(6);
                }
            }
        }
        $this->partition_count = 1 << $chunk->literal(2);
        $this->parseQuantisers($chunk);
        /* refresh entropy probabilities */
        $chunk->readFlag();
        $this->value_probs = Vp8Tables::DEFAULT_COEFF_PROBS;
        for ($i = 0; $i < 4; $i++) {
            for ($j = 0; $j < 8; $j++) {
                for ($k = 0; $k < 3; $k++) {
                    for ($span
                        = 0; $span < 11; $span++) {
                        $chance
                            = Vp8Tables::COEFF_UPDATE_PROBS[$i][$j][$k][$span];
                        if ($chunk->readOneBit($chance) === 1) {
                            $this->value_probs[$i][$j][$k][$span]
                                = $chunk->literal(8);
                        }
                    }
                }
            }
        }
        $this->skip_enabled = $chunk->readFlag() === 1;
        $this->skip_prob = $this->skip_enabled ? $chunk->literal(8) : 0;
    }
    /**
     * $partition_count stores how many parts the frame's values are split
     * across, one, two, four or eight.
     * @var int
     */
    private int $partition_count = 1;
    /**
     * parseSegmentation reads how the picture is split into segments and what
     * quantizer and filter strength each one carries. from
     *
     * @param Vp8BoolDecoder $chunk the reader this part of the frame is taken
     */
    private function parseSegmentation(Vp8BoolDecoder $chunk): void
    {
        $this->update_segment_map = $chunk->readFlag() === 1;
        $update_data = $chunk->readFlag() === 1;
        if ($update_data) {
            $this->segment_absolute = $chunk->readFlag() === 1;
            for ($i = 0; $i < 4; $i++) {
                $this->segment_quant[$i] = $chunk->readFlag() === 1
                    ? $chunk->readSignedValue(7) : 0;
            }
            for ($i = 0; $i < 4; $i++) {
                $this->segment_filter[$i] = $chunk->readFlag() === 1
                    ? $chunk->readSignedValue(6) : 0;
            }
        }
        if ($this->update_segment_map) {
            for ($i = 0; $i < 3; $i++) {
                $this->segment_probs[$i] = $chunk->readFlag() === 1
                    ? $chunk->literal(8)
                    : 255;
            }
        }
    }
    /**
     * parseQuantisers works out the numbers each coefficient is multiplied by,
     * for every segment and for each kind of block. from
     *
     * @param Vp8BoolDecoder $chunk the reader this part of the frame is taken
     */
    private function parseQuantisers(Vp8BoolDecoder $chunk): void
    {
        $base = $chunk->literal(7);
        $deltas = [];
        foreach (['ydc', 'y2dc', 'y2ac', 'uvdc', 'uvac'] as $k) {
            $deltas[$k] = $chunk->readFlag() === 1 ? $chunk
                ->readSignedValue(4) : 0;
        }
        $this->quant = [];
        for ($seg = 0; $seg < 4; $seg++) {
            $quality = $base;
            if ($this->segment_enabled) {
                $quality = $this->segment_absolute
                    ? $this->segment_quant[$seg]
                    : $base + $this->segment_quant[$seg];
            }
            $quality = max(0, min(127, $quality));
            $clamp = static fn(int $variant): int => max(0, min(127, $variant));
            $brightness_first_value =
                Vp8Tables::DC_QLOOKUP[$clamp($quality + $deltas['ydc'])];
            $brightness_other_values = Vp8Tables::AC_QLOOKUP[$quality];
            $second_brightness_first_value
                = Vp8Tables::DC_QLOOKUP[$clamp($quality + $deltas['y2dc'])] * 2;
            $second_brightness_other_values =
                (int) (Vp8Tables::AC_QLOOKUP[$clamp($quality
                + $deltas['y2ac'])] * 155 / 100);
            if ($second_brightness_other_values < 8) {
                $second_brightness_other_values = 8;
            }
            $color_first_value = Vp8Tables::DC_QLOOKUP[$clamp($quality +
                $deltas['uvdc'])];
            if ($color_first_value > 132) {
                $color_first_value = 132;
            }
            $color_other_values = Vp8Tables::AC_QLOOKUP[$clamp($quality +
                $deltas['uvac'])];
            $this->quant[$seg] = [$brightness_first_value,
                $brightness_other_values, $second_brightness_first_value,
                    $second_brightness_other_values, $color_first_value,
                        $color_other_values];
        }
    }
    /**
     * splitPartitions from
     *
     * @return Vp8BoolDecoder[] what was read
     * @param string $frame the frame being built
     * @param int $offset how far into the file to read from
     * @param Vp8BoolDecoder $chunk the reader this part of the frame is taken
     */
    private function splitPartitions(string $frame, int $offset,
        Vp8BoolDecoder $chunk): array
    {
        $total = $this->partition_count;
        $sizes_length = 3 * ($total - 1);
        $data_start = $offset + $sizes_length;
        if ($data_start > strlen($frame)) {
            throw new VideoException('VP8 partition sizes run past the frame');
        }
        $parts = [];
        $position = $data_start;
        for ($i = 0; $i < $total; $i++) {
            if ($i < $total - 1) {
                $position = $offset + 3 * $i;
                $size
                    = ord($frame[$position]) | (ord($frame[$position
                        + 1]) << 8) | (ord($frame[$position + 2]) << 16);
            } else {
                $size = strlen($frame) - $position;
            }
            if ($size < 0 || $position + $size > strlen($frame)) {
                throw new VideoException('VP8 partition runs past the frame');
            }
            $parts[] = new Vp8BoolDecoder($frame, $position, $size);
            $position += $size;
        }
        return $parts;
    }
    /* planes */
    /**
     * allocatePlanes sets up the sample planes with a border, so prediction at
     * the picture's edge reads the values the format says it should.
     */
    private function allocatePlanes(): void
    {
        $line_width = $this->macroblock_width * 16;
        $left_half = $this->macroblock_height * 16;
        $code_word = $this->macroblock_width * 8;
        $channel = $this->macroblock_height * 8;
        /* one row of border above and eight columns either side, so the top */
        /* row reads 127, the left column reads 129, and the four above-right */
        /* samples past the right edge always exist */
        foreach ([[0, $line_width, $left_half], [1, $code_word, $channel],
            [2, $code_word,
            $channel]] as [$plane_at, $wide,
            $header]) {
            $stride = $wide + 16;
            $this->strides[$plane_at] = $stride;
            $this->offsets[$plane_at] = $stride + 8;
            $plane = array_fill(0, $stride * ($header + 1), 129);
            for ($across = 0; $across < $stride; $across++) {
                /* the row above the frame */
                $plane[$across] = 127;
            }
            $this->planes[$plane_at] = $plane;
        }
    }
    /**
     * sampleAt works out where one sample sits in a plane. for color
     *
     * @param int $plane_at which plane, zero for brightness and one or two
     * @param int $across how far across the block
     * @param int $down how far down the block
     * @return int what was read
     */
    private function sampleAt(int $plane_at, int $across, int $down): int
    {
        return $this->offsets[$plane_at] + $down *
            $this->strides[$plane_at] + $across;
    }
    /** replicate the right-hand edge of a luma row band so above-right always
      exists */
    /**
     * extendRight repeats the rightmost sample of a row of macroblocks past the
     * picture, so prediction that reaches up and to the right always has
     * something to read.
     *
     * @param int $macroblock_y which macroblock down
     */
    private function extendRight(int $macroblock_y): void
    {
        $stride = $this->strides[0];
        $wide = $this->macroblock_width * 16;
        for ($down =
            $macroblock_y * 16; $down < $macroblock_y * 16 + 16; $down++) {
            $last = $this->planes[0][$this->sampleAt(0, $wide - 1, $down)];
            $base = $this->sampleAt(0, $wide, $down);
            for ($k = 0; $k < 8; $k++) {
                $this->planes[0][$base + $k] = $last;
            }
        }
    }
    /* macroblock loop */
    /**
     * decodeMacroblocks decodes every macroblock: its modes, its coefficients,
     * and the samples that come out of them. from
     *
     * @param Vp8BoolDecoder $chunk the reader this part of the frame is taken
     * @param array $partitions the parts the frame's values are split across
     */
    private function decodeMacroblocks(
        Vp8BoolDecoder $chunk, array $partitions): void
    {
        $macroblocks_across = $this->macroblock_width;
        $macroblocks_down = $this->macroblock_height;
        $this->block_modes = array_fill(0, ($macroblocks_across * 4) *
            ($macroblocks_down * 4), self::B_DC);
        $this->macroblock_y_mode = array_fill(0, $macroblocks_across *
            $macroblocks_down, self::DC_PRED);
        $this->macroblock_segment = array_fill(0, $macroblocks_across *
            $macroblocks_down, 0);
        $this->macroblock_skip = array_fill(0, $macroblocks_across *
            $macroblocks_down, 0);
        $this->macroblock_has_values = array_fill(0, $macroblocks_across *
            $macroblocks_down, 0);
        /* non-zero-coefficient contexts: four luma columns, two each for the */
        /* chroma planes, one for the second-order block */
        $top_value_count = array_fill(0, $macroblocks_across, array_fill(0,
            9, 0));
        $left_value_count = array_fill(0, 9, 0);
        for ($macroblock_y = 0; $macroblock_y <
            $macroblocks_down; $macroblock_y++) {
            $left_value_count = array_fill(0, 9, 0);
            $part = $partitions[$macroblock_y % count($partitions)];
            for ($macroblock_x = 0; $macroblock_x <
                $macroblocks_across; $macroblock_x++) {
                $position = $macroblock_y * $macroblocks_across + $macroblock_x;
                $segment = 0;
                if ($this->segment_enabled && $this->update_segment_map) {
                    $segment
                        = $chunk->walkTree(self::SEGMENT_TREE,
                            $this->segment_probs);
                }
                $this->macroblock_segment[$position] = $segment;
                $skip = $this->skip_enabled ? $chunk->readOneBit($this
                    ->skip_prob) : 0;
                $this->macroblock_skip[$position] = $skip;
                $y_mode = $chunk->walkTree(self::YMODE_TREE, self::YMODE_PROBS);
                $this->macroblock_y_mode[$position] = $y_mode;
                $this->readSubModes($chunk, $macroblock_x, $macroblock_y,
                    $y_mode);
                $color_mode = $chunk->walkTree(self::UVMODE_TREE,
                    self::UVMODE_PROBS);
                $values = null;
                $has_values = false;
                if ($skip === 0) {
                    [$values, $has_values] = $this->decodeCoefficients(
                        $part, $macroblock_x, $y_mode, $segment,
                            $top_value_count,
                            $left_value_count
                    );
                } else {
                    for ($i = 0; $i < 8; $i++) {
                        $top_value_count[$macroblock_x][$i] = 0;
                        $left_value_count[$i] = 0;
                    }
                    if ($y_mode !== self::B_PRED) {
                        $top_value_count[$macroblock_x][8] = 0;
                        $left_value_count[8] = 0;
                    }
                }
                $this->macroblock_has_values[$position] = $has_values ? 1 : 0;
                $this->reconstruct($macroblock_x, $macroblock_y, $y_mode,
                    $color_mode, $values);
            }
            $this->extendRight($macroblock_y);
        }
    }
    /**
     * readSubModes reads the prediction modes of a macroblock, one for the
     * whole block or one for each four sample square. from
     *
     * @param Vp8BoolDecoder $chunk the reader this part of the frame is taken
     * @param int $macroblock_x which macroblock across
     * @param int $macroblock_y which macroblock down
     * @param int $y_mode which way the brightness block is guessed
     */
    private function readSubModes(Vp8BoolDecoder $chunk, int $macroblock_x,
        int $macroblock_y,
        int $y_mode): void
    {
        $stride = $this->macroblock_width * 4;
        $block_start_x = $macroblock_x * 4;
        $block_start_y = $macroblock_y * 4;
        if ($y_mode !== self::B_PRED) {
            $matches = self::YMODE_TO_BMODE[$y_mode];
            for ($down = 0; $down < 4; $down++) {
                for ($across = 0; $across < 4; $across++) {
                    $this->block_modes[($block_start_y + $down) * $stride +
                        $block_start_x + $across]
                        = $matches;
                }
            }
            return;
        }
        for ($down = 0; $down < 4; $down++) {
            for ($across = 0; $across < 4; $across++) {
                /* outside the frame the context is the flat predictor */
                $above = ($block_start_y + $down === 0)
                    ? self::B_DC
                    : $this->block_modes[($block_start_y + $down - 1) * $stride
                        + $block_start_x + $across];
                $left = ($block_start_x + $across === 0)
                    ? self::B_DC
                    : $this->block_modes[($block_start_y + $down) * $stride
                        + $block_start_x + $across - 1];
                $mode = $chunk->walkTree(
                    self::BMODE_TREE, Vp8Tables::BMODE_PROBS[$above][$left]);
                $this->block_modes[($block_start_y + $down) * $stride +
                    $block_start_x + $across]
                    = $mode;
            }
        }
    }
    /* coefficients */
    /**
     * decodeCoefficients coefficient] from carried
     *
     * @return array [dequantized blocks, any non-zero
     * @param Vp8BoolDecoder $chunk the reader this part of the frame is taken
     * @param int $macroblock_x which macroblock across
     * @param int $y_mode which way the brightness block is guessed
     * @param int $segment which segment of the frame the block belongs to
     * @param array $top_value_count how many values the blocks above carried
     * @param array $left_value_count how many values the blocks to the left
     */
    private function decodeCoefficients(
        Vp8BoolDecoder $chunk, int $macroblock_x, int $y_mode, int $segment,
        array &$top_value_count, array &$left_value_count
    ): array {
        [$brightness_first_value, $brightness_other_values,
            $second_brightness_first_value, $second_brightness_other_values,
                $color_first_value, $color_other_values] = $this
                    ->quant[$segment];
        $has_second_brightness = $y_mode !== self::B_PRED;
        $blocks = [];
        $any = false;
        if ($has_second_brightness) {
            $context =
                $top_value_count[$macroblock_x][8] + $left_value_count[8];
            $second_brightness = array_fill(0, 16, 0);
            $total = $this->decodeBlock($chunk, $second_brightness, 1,
                $context, 0, $second_brightness_first_value,
                $second_brightness_other_values);
            $top_value_count[$macroblock_x][8] = $left_value_count[8] =
                $total > 0 ? 1 : 0;
            if ($total > 0) {
                $any = true;
            }
            $blocks['y2'] = self::inverseWalsh($second_brightness);
        }
        $type = $has_second_brightness ? 0 : 3;
        $first = $has_second_brightness ? 1 : 0;
        for ($down = 0; $down < 4; $down++) {
            for ($across = 0; $across < 4; $across++) {
                $context = $top_value_count[$macroblock_x][$across] +
                    $left_value_count[$down];
                $block_at = array_fill(0, 16, 0);
                $total = $this->decodeBlock(
                    $chunk, $block_at, $type, $context, $first,
                        $brightness_first_value, $brightness_other_values);
                $top_value_count[$macroblock_x][$across] =
                    $left_value_count[$down] = $total > 0 ?
                    1 : 0;
                if ($total > 0) {
                    $any = true;
                }
                $blocks['y'][$down * 4 + $across] = $block_at;
            }
        }
        foreach ([1, 2] as $plane_at) {
            for ($down = 0; $down < 2; $down++) {
                for ($across = 0; $across < 2; $across++) {
                    $ti = 4 + ($plane_at - 1) * 2 + $across;
                    $li = 4 + ($plane_at - 1) * 2 + $down;
                    $context = $top_value_count[$macroblock_x][$ti] +
                        $left_value_count[$li];
                    $block_at = array_fill(0, 16, 0);
                    $total = $this->decodeBlock(
                        $chunk, $block_at, 2, $context, 0,
                            $color_first_value, $color_other_values);
                    $top_value_count[$macroblock_x][$ti] =
                        $left_value_count[$li] =
                        $total > 0 ? 1 : 0;
                    if ($total > 0) {
                        $any = true;
                    }
                    $blocks['uv'][$plane_at][$down * 2 + $across] = $block_at;
                }
            }
        }
        return [$blocks, $any];
    }
    /**
     * decodeBlock one block of coefficient tokens, dequantized into $written in
     * raster order. from
     *
     * @return int 0 when the block ends immediately, otherwise where it ended
     * @param Vp8BoolDecoder $chunk the reader this part of the frame is taken
     * @param array $written filled in with what was read
     * @param int $type which kind
     * @param int $context which set of probabilities to read with
     * @param int $first the first one
     * @param int $first_step the step the first value is scaled by
     * @param int $other_steps the step the rest are scaled by
     */
    private function decodeBlock(
        Vp8BoolDecoder $chunk, array
            &$written, int $type, int $context, int $first, int $first_step,
                int $other_steps
    ): int {
        $probs_for_type = $this->value_probs[$type];
        $block_order = H264Scan::ZZ4;
        $i = $first;
        $skip_end_of_block = false;
        $last_non_zero = 0;
        while ($i < 16) {
            $probs = $probs_for_type[Vp8Tables::COEFF_BAND[$i]][$context];
            if (!$skip_end_of_block && $chunk->readOneBit($probs[0]) === 0) {
                break;
            }
            $skip_end_of_block = false;
            if ($chunk->readOneBit($probs[1]) === 0) {
                /* a zero coefficient: the next token cannot be an end of block
                  */
                $context = 0;
                $skip_end_of_block = true;
                $i++;
                continue;
            }
            if ($chunk->readOneBit($probs[2]) === 0) {
                $value = 1;
                $context = 1;
            } else {
                $context = 2;
                if ($chunk->readOneBit($probs[3]) === 0) {
                    $value = $chunk->readOneBit($probs[4]) === 0 ? 2 : 3
                        + $chunk->readOneBit($probs[5]);
                } else {
                    if ($chunk->readOneBit($probs[6]) === 0) {
                        $block_kind = $chunk
                            ->readOneBit($probs[7]) === 0 ? 0 : 1;
                    } elseif ($chunk->readOneBit($probs[8]) === 0) {
                        $block_kind = $chunk
                            ->readOneBit($probs[9]) === 0 ? 2 : 3;
                    } else {
                        $block_kind = $chunk
                            ->readOneBit($probs[10]) === 0 ? 4 : 5;
                    }
                    $value = self::CAT_BASE[$block_kind];
                    $extra = 0;
                    foreach (self::CAT_PROBS[$block_kind] as $position) {
                        $extra = ($extra << 1) | $chunk->readOneBit($position);
                    }
                    $value += $extra;
                }
            }
            if ($chunk->readOneBit(128) === 1) {
                $value = -$value;
            }
            $written[$block_order[$i]] = $value * ($i === 0
                ? $first_step : $other_steps);
            $last_non_zero = $i + 1;
            $i++;
        }
        return $i > $first ? max($last_non_zero, $i) : 0;
    }
    /**
     * COS_MINUS1 is transforms.
     * @var mixed
     */
    private const COS_MINUS1 = 20091;
    /**
     * SIN is the sines the transform multiplies by, held as whole numbers so
     * the arithmetic stays exact.
     * @var mixed
     */
    private const SIN = 35468;
    /**
     * inverseDct the four by four inverse transform of a numbered internet
     * standard 6386
     *
     * @param array $self_contained whether the block stands on its own
     * @return array what was read
     */
    private static function inverseDct(array $self_contained): array
    {
        $held = array_fill(0, 16, 0);
        for ($i = 0; $i < 4; $i++) {
            $left_one = $self_contained[$i] + $self_contained[$i + 8];
            $bit_one = $self_contained[$i] - $self_contained[$i + 8];
            $held_one = ($self_contained[$i + 4] * self::SIN) >> 16;
            $held_two = $self_contained[$i + 12] + (($self_contained[$i + 12] *
                self::COS_MINUS1) >> 16);
            $corner_one = $held_one - $held_two;
            $held_one = $self_contained[$i + 4] + (($self_contained[$i + 4] *
                self::COS_MINUS1) >> 16);
            $held_two = ($self_contained[$i + 12] * self::SIN) >> 16;
            $difference_one = $held_one + $held_two;
            $held[$i] = $left_one + $difference_one;
            $held[$i + 12] = $left_one - $difference_one;
            $held[$i + 4] = $bit_one + $corner_one;
            $held[$i + 8] = $bit_one - $corner_one;
        }
        $written = array_fill(0, 16, 0);
        for ($i = 0; $i < 4; $i++) {
            $origin = $i * 4;
            $left_one = $held[$origin] + $held[$origin + 2];
            $bit_one = $held[$origin] - $held[$origin + 2];
            $held_one = ($held[$origin + 1] * self::SIN) >> 16;
            $held_two = $held[$origin + 3]
                + (($held[$origin + 3] * self::COS_MINUS1) >> 16);
            $corner_one = $held_one - $held_two;
            $held_one = $held[$origin + 1]
                + (($held[$origin + 1] * self::COS_MINUS1) >> 16);
            $held_two = ($held[$origin + 3] * self::SIN) >> 16;
            $difference_one = $held_one + $held_two;
            $written[$origin] = ($left_one + $difference_one + 4) >> 3;
            $written[$origin + 3] = ($left_one - $difference_one + 4) >> 3;
            $written[$origin + 1] = ($bit_one + $corner_one + 4) >> 3;
            $written[$origin + 2] = ($bit_one - $corner_one + 4) >> 3;
        }
        return $written;
    }
    /**
     * inverseWalsh the second-order Walsh-a transform of sums and differences
     * alone transform applied to the luma DC block @param array $self_contained
     * whether the block stands on its own @return array what was read.
     */
    private static function inverseWalsh(array $self_contained): array
    {
        $held = array_fill(0, 16, 0);
        for ($i = 0; $i < 4; $i++) {
            $left_one = $self_contained[$i] + $self_contained[$i + 12];
            $bit_one = $self_contained[$i + 4] + $self_contained[$i + 8];
            $corner_one = $self_contained[$i + 4] - $self_contained[$i + 8];
            $difference_one = $self_contained[$i] - $self_contained[$i + 12];
            $held[$i] = $left_one + $bit_one;
            $held[$i + 4] = $corner_one + $difference_one;
            $held[$i + 8] = $left_one - $bit_one;
            $held[$i + 12] = $difference_one - $corner_one;
        }
        $written = array_fill(0, 16, 0);
        for ($i = 0; $i < 4; $i++) {
            $origin = $i * 4;
            $left_one = $held[$origin] + $held[$origin + 3];
            $bit_one = $held[$origin + 1] + $held[$origin + 2];
            $corner_one = $held[$origin + 1] - $held[$origin + 2];
            $difference_one = $held[$origin] - $held[$origin + 3];
            $written[$origin] = ($left_one + $bit_one + 3) >> 3;
            $written[$origin + 1] = ($corner_one + $difference_one + 3) >> 3;
            $written[$origin + 2] = ($left_one - $bit_one + 3) >> 3;
            $written[$origin + 3] = ($difference_one - $corner_one + 3) >> 3;
        }
        return $written;
    }
    /**
     * holdInsideByte holds a sample inside the range a byte can carry.
     *
     * @param int $variant which form of the format is in use
     * @return int what was read
     */
    private static function holdInsideByte(int $variant): int
    {
        return $variant < 0 ? 0 : ($variant > 255 ? 255 : $variant);
    }
    /* reconstruction */
    /**
     * reconstruct turns the prediction and the coefficients of a macroblock
     * into samples in the planes.
     *
     * @param int $macroblock_x which macroblock across
     * @param int $macroblock_y which macroblock down
     * @param int $y_mode which way the brightness block is guessed
     * @param int $color_mode which way the color blocks are guessed
     * @param array $values the values the block was coded as
     */
    private function reconstruct(int $macroblock_x, int $macroblock_y,
        int $y_mode,
        int $color_mode,
        ?array $values): void
    {
        if ($y_mode === self::B_PRED) {
            $this->reconstructB($macroblock_x, $macroblock_y, $values);
        } else {
            $this->predictBlock(
                0, $macroblock_x * 16, $macroblock_y * 16, 16, $y_mode,
                    $macroblock_x > 0, $macroblock_y > 0);
            if ($values !== null) {
                $first_value = $values['y2'];
                for ($byte = 0; $byte < 16; $byte++) {
                    $block_at = $values['y'][$byte];
                    $block_at[0] = $first_value[$byte];
                    $this->addResidual(0, $macroblock_x * 16
                        + ($byte & 3) * 4, $macroblock_y * 16
                        + ($byte >> 2) * 4, $block_at);
                }
            }
        }
        foreach ([1, 2] as $plane_at) {
            $this->predictBlock(
                $plane_at, $macroblock_x * 8, $macroblock_y * 8, 8,
                    $color_mode, $macroblock_x > 0,
                    $macroblock_y > 0);
            if ($values !== null) {
                for ($byte = 0; $byte < 4; $byte++) {
                    $this->addResidual(
                        $plane_at,
                        $macroblock_x * 8 + ($byte & 1) * 4,
                        $macroblock_y * 8 + ($byte >> 1) * 4,
                        $values['uv'][$plane_at][$byte]
                    );
                }
            }
        }
    }
    /**
     * addResidual transforms one block of coefficients and adds the result to
     * the prediction already in the plane. for color
     *
     * @param int $plane_at which plane, zero for brightness and one or two
     * @param int $block_x how far across the frame the block starts
     * @param int $block_y how far down the frame the block starts
     * @param array $block the block's values
     */
    private function addResidual(int $plane_at, int $block_x, int $block_y,
        array $block): void
    {
        foreach ($block as $variant) {
            if ($variant !== 0) {
                $run = self::inverseDct($block);
                $stride = $this->strides[$plane_at];
                for ($down = 0; $down < 4; $down++) {
                    $row = $this->sampleAt($plane_at, $block_x,
                        $block_y + $down);
                    for ($across = 0; $across < 4; $across++) {
                        $this->planes[$plane_at][$row + $across] =
                            self::holdInsideByte($this
                                ->planes[$plane_at][$row + $across]
                                + $run[$down * 4 + $across]);
                    }
                }
                return;
            }
        }
    }
    /**
     * predictBlock whole-block prediction for sixteen by sixteen luma and eight
     * by eight chroma for color
     *
     * @param int $plane_at which plane, zero for brightness and one or two
     * @param int $block_x how far across the frame the block starts
     * @param int $block_y how far down the frame the block starts
     * @param int $size how many bytes
     * @param int $mode which way the block is guessed from its neighbors
     * @param bool $has_left whether the block to the left is there to read
     * @param bool $has_top whether the block above is there to read
     */
    private function predictBlock(int $plane_at, int $block_x, int $block_y,
        int $size,
        int $mode, bool $has_left, bool $has_top): void
    {
        $stride = $this->strides[$plane_at];
        $plane = &$this->planes[$plane_at];
        $above = [];
        $left = [];
        for ($i = 0; $i < $size; $i++) {
            $above[$i] = $plane[$this->sampleAt($plane_at, $block_x + $i,
                $block_y - 1)];
            $left[$i] = $plane[$this->sampleAt($plane_at, $block_x - 1,
                $block_y +
                $i)];
        }
        $corner = $plane[$this->sampleAt($plane_at, $block_x - 1,
            $block_y - 1)];
        switch ($mode) {
            case self::DC_PRED:
                if ($has_top && $has_left) {
                    $first_value = (array_sum($above) + array_sum($left) +
                        $size)
                        >> (int) (log($size, 2) + 1);
                } elseif ($has_top) {
                    $first_value = (array_sum($above) + $size / 2)
                        >> (int) log($size, 2);
                } elseif ($has_left) {
                    $first_value = (array_sum($left) + $size /
                        2) >> (int) log($size, 2);
                } else {
                    $first_value = 128;
                }
                $first_value = (int) $first_value;
                for ($down = 0; $down < $size; $down++) {
                    $row = $this->sampleAt($plane_at, $block_x,
                        $block_y + $down);
                    for ($across = 0; $across < $size; $across++) {
                        $plane[$row + $across] = $first_value;
                    }
                }
                break;
            case self::V_PRED:
                for ($down = 0; $down < $size; $down++) {
                    $row = $this->sampleAt($plane_at, $block_x,
                        $block_y + $down);
                    for ($across = 0; $across < $size; $across++) {
                        $plane[$row + $across] = $above[$across];
                    }
                }
                break;
            case self::H_PRED:
                for ($down = 0; $down < $size; $down++) {
                    $row = $this->sampleAt($plane_at, $block_x,
                        $block_y + $down);
                    for ($across = 0; $across < $size; $across++) {
                        $plane[$row + $across] = $left[$down];
                    }
                }
                break;
            case self::TM_PRED:
                for ($down = 0; $down < $size; $down++) {
                    $row = $this->sampleAt($plane_at, $block_x,
                        $block_y + $down);
                    for ($across = 0; $across < $size; $across++) {
                        $plane[$row + $across]
                            = self::holdInsideByte(
                                $left[$down] + $above[$across] - $corner);
                    }
                }
                break;
            default:
                throw new VideoException("bad VP8 prediction mode $mode");
        }
        unset($plane);
    }
    /**
     * reconstructB rebuilds a macroblock predicted in four sample squares, each
     * with its own mode.
     *
     * @param int $macroblock_x which macroblock across
     * @param int $macroblock_y which macroblock down
     * @param array $values the values the block was coded as
     */
    private function reconstructB(int $macroblock_x, int $macroblock_y,
        ?array $values): void
    {
        $stride = $this->strides[0];
        $block_stride = $this->macroblock_width * 4;
        $wide = $this->macroblock_width * 16;
        /* the four samples above and to the right of the macroblock, reused by
          */
        /* every sub-block in the right-hand column whatever its row */
        $top_right = [];
        for ($k = 0; $k < 4; $k++) {
            $top_right[$k] = ($macroblock_x === $this
                ->macroblock_width - 1 && $macroblock_y > 0)
                ? $this->planes[0][$this->sampleAt(0, $wide - 1,
                    $macroblock_y * 16 - 1)]
                : $this->planes[0][$this->sampleAt(0,
                    $macroblock_x * 16 + 16 + $k,
                    $macroblock_y * 16
                    - 1)];
        }
        for ($block_y = 0; $block_y < 4; $block_y++) {
            for ($block_x = 0; $block_x < 4; $block_x++) {
                $sample_x = $macroblock_x * 16 + $block_x * 4;
                $sample_y = $macroblock_y * 16 + $block_y * 4;
                $mode
                    = $this
                        ->block_modes[($macroblock_y * 4 + $block_y) *
                            $block_stride + $macroblock_x * 4
                        + $block_x];
                $above_run = [];
                for ($k = 0; $k < 4; $k++) {
                    $above_run[$k]
                        = $this->planes[0][$this->sampleAt(0, $sample_x + $k,
                            $sample_y - 1)];
                }
                if ($block_x === 3) {
                    for ($k = 0; $k < 4; $k++) {
                        $above_run[4 + $k] = $top_right[$k];
                    }
                } else {
                    for ($k = 0; $k < 4; $k++) {
                        $above_run[4 + $k]
                            = $this->planes[0][
                                $this->sampleAt(0, $sample_x + 4 + $k,
                                    $sample_y - 1)];
                    }
                }
                $left_run = [];
                for ($k = 0; $k < 4; $k++) {
                    $left_run[$k]
                        = $this->planes[0][$this->sampleAt(0, $sample_x - 1,
                            $sample_y + $k)];
                }
                $prior = $this->planes[0][$this->sampleAt(0, $sample_x - 1,
                    $sample_y - 1)];
                $guess = self::predictSubBlock(
                    $mode, $above_run, $left_run, $prior);
                for ($down = 0; $down < 4; $down++) {
                    $row = $this->sampleAt(0, $sample_x, $sample_y + $down);
                    for ($across = 0; $across < 4; $across++) {
                        $this->planes[0][$row + $across]
                            = $guess[$down * 4 + $across];
                    }
                }
                if ($values !== null) {
                    $this->addResidual(0, $sample_x, $sample_y,
                        $values['y'][$block_y * 4
                        + $block_x]);
                }
            }
        }
    }
    /**
     * predictSubBlock the ten sub-block predictors of a numbered internet
     * standard 6386. $above_run holds the four samples above plus four above-
     * right, $left_run the four to the left, $prior the corner.
     *
     * @param int $mode which way the block is guessed from its neighbors
     * @param array $above_run how many the row above skipped
     * @param array $left_run how many the column to the left skipped
     * @param int $prior what was read before
     * @return array what was read
     */
    private static function predictSubBlock(int $mode, array $above_run,
        array $left_run, int $prior): array
    {
        $below = array_fill(0, 16, 0);
        $set
            = static function (int $run, int $chunk,
                int $variant) use (&$below): void {
            $below[$run * 4 + $chunk] = $variant;
        };
        $average_of_two = static fn(int $amount, int $byte): int
            => ($amount + $byte + 1) >> 1;
        $average_of_three = static fn(int $amount, int $byte, int $chunk): int
            => ($amount + 2 * $byte + $chunk + 2) >> 2;
        /* E indexes the left column, corner and above row as one run */
        $east
            = [$left_run[3], $left_run[2], $left_run[1], $left_run[0], $prior,
                $above_run[0], $above_run[1], $above_run[2], $above_run[3]];
        switch ($mode) {
            case self::B_DC:
                $sum = $above_run[0] + $above_run[1] + $above_run[2]
                    + $above_run[3] + $left_run[0] + $left_run[1] + $left_run[2]
                    + $left_run[3] + 4;
                $below = array_fill(0, 16, $sum >> 3);
                break;
            case self::B_TM:
                for ($run = 0; $run < 4; $run++) {
                    for ($chunk = 0; $chunk < 4; $chunk++) {
                        $set($run, $chunk, self::holdInsideByte($left_run[$run]
                            + $above_run[$chunk] - $prior));
                    }
                }
                break;
            case self::B_VE:
                $variant = [
                    $average_of_three($prior, $above_run[0], $above_run[1]),
                    $average_of_three($above_run[0], $above_run[1],
                        $above_run[2]),
                    $average_of_three($above_run[1], $above_run[2],
                        $above_run[3]),
                    $average_of_three($above_run[2], $above_run[3],
                        $above_run[4]),
                ];
                for ($run = 0; $run < 4; $run++) {
                    for ($chunk = 0; $chunk < 4; $chunk++) {
                        $set($run, $chunk, $variant[$chunk]);
                    }
                }
                break;
            case self::B_HE:
                $variant = [
                    $average_of_three($prior, $left_run[0], $left_run[1]),
                    $average_of_three($left_run[0], $left_run[1], $left_run[2]),
                    $average_of_three($left_run[1], $left_run[2], $left_run[3]),
                    $average_of_three($left_run[2], $left_run[3], $left_run[3]),
                ];
                for ($run = 0; $run < 4; $run++) {
                    for ($chunk = 0; $chunk < 4; $chunk++) {
                        $set($run, $chunk, $variant[$run]);
                    }
                }
                break;
            case self::B_LD:
                for ($run = 0; $run < 4; $run++) {
                    for ($chunk = 0; $chunk < 4; $chunk++) {
                        $i = $run + $chunk;
                        $set($run, $chunk, $i < 6
                            ? $average_of_three(
                                $above_run[$i], $above_run[$i + 1],
                                $above_run[$i + 2])
                            : $average_of_three($above_run[6],  $above_run[7],
                                $above_run[7]));
                    }
                }
                break;
            case self::B_RD:
                for ($run = 0; $run < 4; $run++) {
                    for ($chunk = 0; $chunk < 4; $chunk++) {
                        $i = $chunk - $run + 3;
                        $set($run, $chunk, $average_of_three($east[$i], $east[$i
                            + 1], $east[$i + 2]));
                    }
                }
                break;
            case self::B_VR:
                $set(3, 0, $average_of_three($east[1], $east[2], $east[3]));
                $set(2, 0, $average_of_three($east[2], $east[3], $east[4]));
                $set(3, 1, $average_of_three($east[3], $east[4], $east[5]));
                $set(1, 0, $average_of_three($east[3], $east[4], $east[5]));
                $set(2, 1, $average_of_two($east[4], $east[5]));
                $set(0, 0, $average_of_two($east[4], $east[5]));
                $set(3, 2, $average_of_three($east[4], $east[5], $east[6]));
                $set(1, 1, $average_of_three($east[4], $east[5], $east[6]));
                $set(2, 2, $average_of_two($east[5], $east[6]));
                $set(0, 1, $average_of_two($east[5], $east[6]));
                $set(3, 3, $average_of_three($east[5], $east[6], $east[7]));
                $set(1, 2, $average_of_three($east[5], $east[6], $east[7]));
                $set(2, 3, $average_of_two($east[6], $east[7]));
                $set(0, 2, $average_of_two($east[6], $east[7]));
                $set(1, 3, $average_of_three($east[6], $east[7], $east[8]));
                $set(0, 3, $average_of_two($east[7], $east[8]));
                break;
            case self::B_VL:
                $set(0, 0, $average_of_two($above_run[0], $above_run[1]));
                $set(1, 0, $average_of_three($above_run[0], $above_run[1],
                    $above_run[2]));
                $set(2, 0, $average_of_two($above_run[1], $above_run[2]));
                $set(0, 1, $average_of_two($above_run[1], $above_run[2]));
                $set(1, 1, $average_of_three($above_run[1], $above_run[2],
                    $above_run[3]));
                $set(3, 0, $average_of_three($above_run[1], $above_run[2],
                    $above_run[3]));
                $set(2, 1, $average_of_two($above_run[2], $above_run[3]));
                $set(0, 2, $average_of_two($above_run[2], $above_run[3]));
                $set(3, 1, $average_of_three($above_run[2], $above_run[3],
                    $above_run[4]));
                $set(1, 2, $average_of_three($above_run[2], $above_run[3],
                    $above_run[4]));
                $set(2, 2, $average_of_two($above_run[3], $above_run[4]));
                $set(0, 3, $average_of_two($above_run[3], $above_run[4]));
                $set(3, 2, $average_of_three($above_run[3], $above_run[4],
                    $above_run[5]));
                $set(1, 3, $average_of_three($above_run[3], $above_run[4],
                    $above_run[5]));
                $set(2, 3, $average_of_three($above_run[4], $above_run[5],
                    $above_run[6]));
                $set(3, 3, $average_of_three($above_run[5], $above_run[6],
                    $above_run[7]));
                break;
            case self::B_HD:
                $set(3, 0, $average_of_two($east[0], $east[1]));
                $set(3, 1, $average_of_three($east[0], $east[1], $east[2]));
                $set(2, 0, $average_of_two($east[1], $east[2]));
                $set(3, 2, $average_of_two($east[1], $east[2]));
                $set(2, 1, $average_of_three($east[1], $east[2], $east[3]));
                $set(3, 3, $average_of_three($east[1], $east[2], $east[3]));
                $set(2, 2, $average_of_two($east[2], $east[3]));
                $set(1, 0, $average_of_two($east[2], $east[3]));
                $set(2, 3, $average_of_three($east[2], $east[3], $east[4]));
                $set(1, 1, $average_of_three($east[2], $east[3], $east[4]));
                $set(1, 2, $average_of_two($east[3], $east[4]));
                $set(0, 0, $average_of_two($east[3], $east[4]));
                $set(1, 3, $average_of_three($east[3], $east[4], $east[5]));
                $set(0, 1, $average_of_three($east[3], $east[4], $east[5]));
                $set(0, 2, $average_of_three($east[4], $east[5], $east[6]));
                $set(0, 3, $average_of_three($east[5], $east[6], $east[7]));
                break;
            case self::B_HU:
                $set(0, 0, $average_of_two($left_run[0], $left_run[1]));
                $set(0, 1, $average_of_three($left_run[0], $left_run[1],
                    $left_run[2]));
                $set(0, 2, $average_of_two($left_run[1], $left_run[2]));
                $set(1, 0, $average_of_two($left_run[1], $left_run[2]));
                $set(0, 3, $average_of_three($left_run[1], $left_run[2],
                    $left_run[3]));
                $set(1, 1, $average_of_three($left_run[1], $left_run[2],
                    $left_run[3]));
                $set(1, 2, $average_of_two($left_run[2], $left_run[3]));
                $set(2, 0, $average_of_two($left_run[2], $left_run[3]));
                $set(1, 3, $average_of_three($left_run[2], $left_run[3],
                    $left_run[3]));
                $set(2, 1, $average_of_three($left_run[2], $left_run[3],
                    $left_run[3]));
                foreach ([[2, 2], [2, 3], [3, 0], [3, 1], [3, 2],
                    [3, 3]] as [$run, $checksum]) {
                    $set($run, $checksum, $left_run[3]);
                }
                break;
            default:
                throw new VideoException("bad VP8 sub-block mode $mode");
        }
        return $below;
    }
    /* loop filter */
    /**
     * loopFilter smooths the sample values either side of every block edge, so
     * that quantization does not leave a visible seam.
     */
    private function loopFilter(): void
    {
        if ($this->filter_level === 0) {
            return;
        }
        for ($macroblock_y = 0; $macroblock_y < $this
            ->macroblock_height; $macroblock_y++) {
            for ($macroblock_x = 0; $macroblock_x < $this
                ->macroblock_width; $macroblock_x++) {
                $position = $macroblock_y * $this
                    ->macroblock_width + $macroblock_x;
                $level = $this->filterLevelFor($position);
                if ($level === 0) {
                    continue;
                }
                $interior = $level;
                if ($this->sharpness > 0) {
                    $interior >>= ($this->sharpness > 4) ? 2 : 1;
                    if ($interior > 9 - $this->sharpness) {
                        $interior = 9 - $this->sharpness;
                    }
                }
                if ($interior < 1) {
                    $interior = 1;
                }
                $macroblock_limit = 2 * ($level + 2) + $interior;
                $b_limit = 2 * $level + $interior;
                $sharp_edge = $level >= 40 ? 2 : ($level >= 15 ? 1 : 0);
                $inner = $this->macroblock_has_values[$position] === 1
                    || $this->macroblock_y_mode[$position] === self::B_PRED;
                $this->filterMacroblock(
                    $macroblock_x, $macroblock_y, $macroblock_limit,
                        $b_limit, $interior,
                        $sharp_edge, $inner);
            }
        }
    }
    /**
     * filterLevelFor works out how strongly one macroblock should be filtered,
     * given the frame's setting and any adjustment its segment or mode carries.
     *
     * @param int $position where the block sits in the frame
     * @return int what was read
     */
    private function filterLevelFor(int $position): int
    {
        $level = $this->filter_level;
        if ($this->segment_enabled) {
            $seg = $this->macroblock_segment[$position];
            $level = $this->segment_absolute
                ? $this->segment_filter[$seg]
                : $level + $this->segment_filter[$seg];
            $level = max(0, min(63, $level));
        }
        if ($this->smoothing_delta_enabled) {
            /* intra frame */
            $level += $this->reference_smoothing_deltas[0];
            if ($this->macroblock_y_mode[$position] === self::B_PRED) {
                $level += $this->mode_smoothing_deltas[0];
            }
            $level = max(0, min(63, $level));
        }
        return $level;
    }
    /**
     * filterMacroblock filters the edges of one macroblock, the outer ones more
     * widely than the ones inside it.
     *
     * @param int $macroblock_x which macroblock across
     * @param int $macroblock_y which macroblock down
     * @param int $macroblock_limit how far a value at a macroblock edge may be
     *     moved
     * @param int $b_limit how far a value inside a macroblock may be moved
     * @param int $interior how far an inside value may be moved
     * @param int $sharp_edge whether the edge is too sharp to smooth
     * @param bool $inner whether the inside edges are smoothed too
     */
    private function filterMacroblock(
        int $macroblock_x, int $macroblock_y, int $macroblock_limit,
            int $b_limit, int $interior,
            int $sharp_edge,
        bool $inner
    ): void {
        /* the simple filter touches luma only */
        $planes = $this->filter_simple
            ? [[0, 16, 16]]
            : [[0, 16, 16], [1, 8, 8], [2, 8, 8]];
        foreach ($planes as [$plane_at, $size, $length]) {
            $stride = $this->strides[$plane_at];
            $block_x = $macroblock_x * $size;
            $block_y = $macroblock_y * $size;
            $step = ($plane_at === 0) ? 4 : 4;
            $inners = ($plane_at === 0) ? [4, 8, 12] : [4];
            if ($macroblock_x > 0) {
                for ($k = 0; $k < $length; $k++) {
                    $this->filterEdge($plane_at, $this->sampleAt($plane_at,
                        $block_x, $block_y
                        + $k), 1, $macroblock_limit, $interior, $sharp_edge,
                            true);
                }
            }
            if ($inner) {
                foreach ($inners as $step_across) {
                    for ($k = 0; $k < $length; $k++) {
                        $this->filterEdge($plane_at, $this->sampleAt($plane_at,
                            $block_x + $step_across, $block_y
                            + $k), 1, $b_limit, $interior, $sharp_edge, false);
                    }
                }
            }
            if ($macroblock_y > 0) {
                for ($k = 0; $k < $length; $k++) {
                    $this->filterEdge($plane_at, $this->sampleAt($plane_at,
                        $block_x
                        + $k, $block_y), $stride, $macroblock_limit, $interior,
                            $sharp_edge, true);
                }
            }
            if ($inner) {
                foreach ($inners as $step_down) {
                    for ($k = 0; $k < $length; $k++) {
                        $this->filterEdge($plane_at, $this->sampleAt($plane_at,
                            $block_x + $k, $block_y
                            + $step_down), $stride, $b_limit, $interior,
                                $sharp_edge, false);
                    }
                }
            }
        }
    }
    /**
     * holdInsideSignedByte holds a value inside the range a signed byte can
     * carry.
     *
     * @param int $variant which form of the format is in use
     * @return int what was read
     */
    private static function holdInsideSignedByte(int $variant): int
    {
        return $variant < -128 ? -128 : ($variant > 127 ? 127 : $variant);
    }
    /**
     * filterEdge filters one line of samples crossing a block edge. for color
     * above
     *
     * @param int $plane_at which plane, zero for brightness and one or two
     * @param int $base the value the rest are measured from
     * @param int $step how far to move each time
     * @param int $limit the most that may be read
     * @param int $interior how far an inside value may be moved
     * @param int $sharp_edge_threshold the sharpness an edge is left alone
     * @param bool $macroblock_edge whether this is the edge between macroblocks
     */
    private function filterEdge(
        int $plane_at, int $base, int $step, int $limit, int $interior,
        int $sharp_edge_threshold, bool $macroblock_edge
    ): void {
        $plane = &$this->planes[$plane_at];
        $before_edge_three = $plane[$base - 4 * $step];
        $before_edge_two = $plane[$base - 3 * $step];
        $before_edge_one = $plane[$base - 2 * $step];
        $before_edge = $plane[$base - $step];
        $after_edge = $plane[$base];
        $after_edge_one = $plane[$base + $step];
        $after_edge_two = $plane[$base + 2 * $step];
        $after_edge_three = $plane[$base + 3 * $step];
        if ($this->filter_simple) {
            if ((abs($before_edge - $after_edge) * 2 +
                intdiv(abs($before_edge_one - $after_edge_one),
                2)) <= $limit) {
                self::commonAdjust($plane, $base, $step, true);
            }
            unset($plane);
            return;
        }
        $ok = (abs($before_edge - $after_edge) * 2 +
            intdiv(abs($before_edge_one - $after_edge_one),
            2)) <= $limit
            && abs($before_edge_three - $before_edge_two) <= $interior &&
                abs($before_edge_two - $before_edge_one) <= $interior
            && abs($before_edge_one - $before_edge) <= $interior &&
                abs($after_edge_three - $after_edge_two) <= $interior
            && abs($after_edge_two - $after_edge_one) <= $interior &&
                abs($after_edge_one - $after_edge) <=
                $interior;
        if (!$ok) {
            unset($plane);
            return;
        }
        $high_edge = abs($before_edge_one - $before_edge) >
            $sharp_edge_threshold || abs($after_edge_one -
            $after_edge) > $sharp_edge_threshold;
        if (!$macroblock_edge) {
            $amount = self::commonAdjust($plane, $base, $step, $high_edge);
            if (!$high_edge) {
                $amount = ($amount + 1) >> 1;
                $plane[$base + $step] = self::shiftBackFromSigned(
                    self::shiftToSigned($after_edge_one) - $amount);
                $plane[$base - 2 * $step] = self::shiftBackFromSigned(
                    self::shiftToSigned($before_edge_one) + $amount);
            }
            unset($plane);
            return;
        }
        if ($high_edge) {
            self::commonAdjust($plane, $base, $step, true);
            unset($plane);
            return;
        }
        $before_edge_signed_one = self::shiftToSigned($before_edge_one);
        $before_edge_signed = self::shiftToSigned($before_edge);
        $after_edge_signed = self::shiftToSigned($after_edge);
        $after_edge_signed_one = self::shiftToSigned($after_edge_one);
        $across_edge = self::holdInsideSignedByte(
            $before_edge_signed_one - $after_edge_signed_one);
        $wide = self::holdInsideSignedByte($across_edge
            + 3 * ($after_edge_signed - $before_edge_signed));
        $amount = (27 * $wide + 63) >> 7;
        $plane[$base] = self::shiftBackFromSigned($after_edge_signed - $amount);
        $plane[$base - $step] =
            self::shiftBackFromSigned($before_edge_signed + $amount);
        $amount = (18 * $wide + 63) >> 7;
        $plane[$base + $step] =
            self::shiftBackFromSigned($after_edge_signed_one - $amount);
        $plane[$base - 2 * $step] =
            self::shiftBackFromSigned($before_edge_signed_one + $amount);
        $amount = (9 * $wide + 63) >> 7;
        $plane[$base + 2 * $step] = self::shiftBackFromSigned(
            self::shiftToSigned($after_edge_two) - $amount);
        $plane[$base - 3 * $step] = self::shiftBackFromSigned(
            self::shiftToSigned($before_edge_two) + $amount);
        unset($plane);
    }
    /**
     * shiftToSigned shifts a sample so that its middle value sits at zero,
     * which is how the filter arithmetic is defined.
     *
     * @param int $variant which form of the format is in use
     * @return int what was read
     */
    private static function shiftToSigned(int $variant): int
    {
        return $variant - 128;
    }
    /**
     * shiftBackFromSigned shifts a filtered value back into the range a sample
     * uses.
     *
     * @param int $variant which form of the format is in use
     * @return int what was read
     */
    private static function shiftBackFromSigned(int $variant): int
    {
        return self::holdInsideByte(self::holdInsideSignedByte($variant) + 128);
    }
    /**
     * commonAdjust moves the two samples either side of an edge toward each
     * other by the amount the filter calls for.
     *
     * @param array & $plane zero for luma, one and two for the chroma planes
     * @param int $base the value the rest are measured from
     * @param int $step how far to move each time
     * @param bool $use_outer whether the outer setting is used
     * @return int what was read
     */
    private static function commonAdjust(array
        &$plane, int $base, int $step, bool $use_outer): int
    {
        $before_edge_one = self::shiftToSigned($plane[$base - 2 * $step]);
        $before_edge = self::shiftToSigned($plane[$base - $step]);
        $after_edge = self::shiftToSigned($plane[$base]);
        $after_edge_one = self::shiftToSigned($plane[$base + $step]);
        $amount = self::holdInsideSignedByte(($use_outer
            ? self::holdInsideSignedByte($before_edge_one -
                $after_edge_one) : 0)
            + 3 * ($after_edge - $before_edge));
        $field = self::holdInsideSignedByte($amount + 4) >> 3;
        $entry = self::holdInsideSignedByte($amount + 3) >> 3;
        $plane[$base] = self::shiftBackFromSigned($after_edge - $field);
        $plane[$base - $step] = self::shiftBackFromSigned($before_edge +
            $entry);
        return $field;
    }
    /* output */
    /**
     * toPicture hands back the decoded planes as a picture.
     *
     * @return VideoPicture what was read
     */
    private function toPicture(): VideoPicture
    {
        $line_width = $this->macroblock_width * 16;
        $left_half = $this->macroblock_height * 16;
        $code_word = $this->macroblock_width * 8;
        $channel = $this->macroblock_height * 8;
        $luma = array_fill(0, $line_width * $left_half, 0);
        for ($down = 0; $down < $left_half; $down++) {
            $source = $this->sampleAt(0, 0, $down);
            $target = $down * $line_width;
            for ($across = 0; $across < $line_width; $across++) {
                $luma[$target + $across] = $this->planes[0][$source + $across];
            }
        }
        $blue = array_fill(0, $code_word * $channel, 0);
        $red = array_fill(0, $code_word * $channel, 0);
        for ($down = 0; $down < $channel; $down++) {
            $sum_one = $this->sampleAt(1, 0, $down);
            $sum_two = $this->sampleAt(2, 0, $down);
            $target = $down * $code_word;
            for ($across = 0; $across < $code_word; $across++) {
                $blue[$target + $across] = $this->planes[1][$sum_one + $across];
                $red[$target + $across] = $this->planes[2][$sum_two + $across];
            }
        }
        return new VideoPicture(
            $luma, $blue, $red, $this->frame_width, $this->frame_height,
                $line_width,
                $code_word, 0, 0);
    }
}
X