GIF画像のデータ形式とHTML+JavaScript (ローカル生成)のサンプルプログラム

# GIF のデータ形式

大雑把に以下の並びになっています。

| バイト位置 | バイト数 | 内容 |
|:-:|:-:|:--|
| 0 | 6 | ヘッダ (Header) |
| 6 | 7 | 論理画面記述子 (Logical Screen Descriptor) |
| (13) | - | （共通色表：存在する場合） |
| - | - | 記述子 (Descriptor) |
| ... | ... | ... |
| - | 1 | 終端子 (Trailer) |

## ヘッダ (Header)

| バイト位置 | バイト数 | 内容 |
|:-:|:-:|:--|
| 0 | 3 | 花押 (Signature) |
| 3 | 3 | 版 (Version) : 「87a」または「89a」 |

### 花押 (Signature)

| バイト位置 | 文字 | 10進 | 16進 |
|:-:|:-:|:-:|:-:|
| 0 | 'G' | 71 | 0x47 |
| 1 | 'I' | 73 | 0x49 |
| 2 | 'F' | 70 | 0x46 |

### 版 (Version) 「87a」

| バイト位置 | 文字 | 10進 | 16進 |
|:-:|:-:|:-:|:-:|
| 3 | '8' | 56 | 0x38 |
| 4 | '7' | 55 | 0x37 |
| 5 | 'a' | 79 | 0x61 |

### 版 (Version) 「89a」

| バイト位置 | 文字 | 10進 | 16進 |
|:-:|:-:|:-:|:-:|
| 3 | '8' | 56 | 0x38 |
| 4 | '9' | 57 | 0x39 |
| 5 | 'a' | 79 | 0x61 |

## 論理画面記述子 (Logical Screen Descriptor)

| バイト位置 | 内容 |
|:-:|:--|
| 6 | 画面の幅の下位8ビット (Logical Screen Width) |
| 7 | 画面の幅の上位8ビット (Logical Screen Width) |
| 8 | 画面の高さの下位8ビット (Logical Screen Height) |
| 9 | 画面の高さの上位8ビット (Logical Screen Height) |
| 10 | 詰合情報 &lt;Packed Fields&gt; |
| 11 | 背景色番号 (Background Color Index) |
| 12 | ピクセル縦横比 (Pixel Aspect Ratio) |
| (13) | （共通色表：存在する場合）<br/>　(Global Color Table) |

### 詰合情報 &lt;Packed Fields&gt;

| ビット位置 | ビット数 | 内容 | 備考 |
|:-:|:-:|:--|:--|
| 0 | 3 | 共通色数のビット数<br/>(Size of Global Color Table) | SGCT と略す |
| 3 | 1 | 共通色表は重要度順になっている<br/>(Sort Flag) | |
| 4 | 3 | 論理画面の色解像度<br/>(Color Resolution) | ビット数（1〜8）<br/>を（0〜7）とする |
| 7 | 1 | 共通色表あり<br/>(Global Color Table Flag) | |

### ピクセル縦横比 (Pixel Aspect Ratio)

計算式は次のとおり。

```math:
Aspect\ Ratio = \frac{ Pixel\ Aspect\ Ratio + 15 }{ 64 } = \frac{ 幅 } { 高 }
```

正方形 $\left[1:1\right]$ は $\left(Pixel\ Aspect\ Ratio = 49 \right)$ とする。

### 共通色表 (Global Color Table)

共有色表が存在する場合は $\left( n=2^{SGCT+1} \right)$ 個の色情報が必要です。

| 色番号 | バイト位置<br/>（共通色表） | +0 | +1 | +2 |
|:-:|:-:|:-:|:-:|:-:|
| $0$ | $0$ | R[0] | G[0] | B[0] |
| $1$ | $3$ | R[1] | G[1] | B[1] |
| ... | ... | ... | ... | ... |
| $n-1$ | $3(n-1)$ | R[n-1] | G[n-1] | B[n-1] |

$n$ は最大で $256$ です。

# 記述子 (Descriptor)

記述子には以下の種類があります。

| 先頭バイト値<br/>(Descriptor) | 後続バイト値<br/>(Label) | 内容 |
|:-:|:-:|:--|
| 0x2C | - | 画像記述子 (Image Descriptor) |
| 0x3B | - | 終端子 (Trailer) |
| 0x21 | 必要 | 「89a」版による拡張 |
| 0x21 | 0xF9 | 図形制御拡張 (Graphic Control Extension) |
| 0x21 | 0xFE | 注釈拡張 (Comment Extension) |
| 0x21 | 0x01 | 文字画面拡張 (Plain Text Extension) |
| 0x21 | 0xFF | 応用拡張 (Application Extension) |

## 画像記述子 (Image Descriptor)

| バイト位置<br/>（画像記述子） | 内容 |
|:-:|:--|
| 0 | 画像記述子 (Image Separator) : 0x2C (Image Descriptor) |
| 1 | 画像の位置（左）の下位8ビット (Image Left Position) |
| 2 | 画像の位置（左）の上位8ビット (Image Left Position) |
| 3 | 画像の位置（上）の下位8ビット (Image Top Position) |
| 4 | 画像の位置（上）の上位8ビット (Image Top Position) |
| 5 | 画像の幅の下位8ビット (Image Width) |
| 6 | 画像の幅の上位8ビット (Image Width) |
| 7 | 画像の高さの下位8ビット (Image Height) |
| 8 | 画像の高さの上位8ビット (Image Height) |
| 9 | 詰合情報 &lt;Packed Fields&gt; |
| (10) | （固有色表：存在する場合）<br/>　(Local Color Table) |
| (?) | 色番式画像 (Table Based Image Data) |

### 詰合情報 &lt;Packed Fields&gt;

| ビット位置 | ビット数 | 内容 | 備考 |
|:-:|:-:|:--|:--|
| 0 | 3 | 固有色数のビット数<br/>(Size of Local Color Table) | SLCT と略す |
| 3 | 2 | 予約 (Reserved) | |
| 5 | 1 | 固有色表は重要度順になっている<br/>(Sort Flag) | |
| 6 | 1 | 縞状形式 (Interlace Flag) | |
| 7 | 1 | 固有色表あり<br/>(Local Color Table Flag) | |

**縞状形式 (Interlace Flag)**

縦方向を、次の順に並び替える。

1. 先頭行(Y=0)から8行間隔
2. 5行目(Y=4)から8行間隔
3. 3行目(Y=2)から4行間隔
4. 2行目(Y=1)から2行間隔

### 固有色表 (Local Color Table)

固有色表が存在する場合は $\left( n=2^{SLCT+1} \right)$ 個の色情報が必要です。

| 色番号 | バイト位置<br/>（固有色表） | +0 | +1 | +2 |
|:-:|:-:|:-:|:-:|:-:|
| $0$ | $0$ | R[0] | G[0] | B[0] |
| $1$ | $3$ | R[1] | G[1] | B[1] |
| ... | ... | ... | ... | ... |
| $n-1$ | $3(n-1)$ | R[n-1] | G[n-1] | B[n-1] |

$n$ は最大で $256$ です。

### 色番式画像 (Table Based Image Data)

| バイト位置<br/>（色番式画像） | 内容 |備考  |
|:-:|:--|:--|
| 0 | LZW 最小コード幅 (LZW Minimum Code Size) | LMCS と略す |
| 1 | 圧縮画像（副塊情報形式 : Data Sub-blocks） | LZW 圧縮 |

**圧縮画像**

画像の幅と高さを $w,h$ とすると $(w \times h)$ 個の色番号表が画像データとなります。

| ＼ | 0 | 1 | ... | w-1 |
|:-:|:-:|:-:|:-:|:-:|
| **0** | $c_0$ | $c_1$ | ... | $c_{w-1}$ |
| **1** | $c_w$ | $c_{w+1}$ | ... | $c_{2w-1}$ |
| ... | ... | ... | ... | ... |
| **h-1** | $c_{w(h-1)}$ | $c_{w(h-1)+1}$ | ... | $c_{w h - 1}$ |

圧縮画像は、画像データを LZW アルゴリズムで圧縮して副塊情報形式 (Data Sub-blocks) で格納します。

## 副塊情報形式 (Data Sub-blocks)

| バイト位置<br/>（副塊情報形式） | 内容 |
|:-:|:--|
| $0$ | 後続のバイト数 $N$ |
| $1$ | データ列（$N$ バイト） |
| $N+2$ | 後続のバイト数 |
| $N+3$ | データ列（続き） |
| ... | ... |
| ... | ... |
| ... | ... |
| - | 後続のバイト数 |
| - | データ列（続き） |
| - | 後続のバイト数 = 0 |

後続のバイト数は最大で 255 です。

## 「89a」版による拡張

「89a」版による拡張は、以下の形式となっています。

| バイト位置<br/>（識別子） | 内容 |
|:-:|:--|
| 0 | 「89a」版拡張 (Extension Introducer)：0x21 (Descriptor) |
| 1 | 表題 (Label) |
| 2 | 副塊情報形式 (Data Sub-blocks) |
| - | 〃 |

この形式により、未知の表題 (Label) を未知のデータとして処理可能になっています。

## 図形制御拡張 (Graphic Control Extension)

| バイト位置<br/>（図形制御拡張） | 内容 |
|:-:|:--|
| 0 | 「89a」版拡張 (Extension Introducer)：0x21 (Descriptor) |
| 1 | 図形制御表題 (Graphic Control Label)：0xF9 |
| 2 | 情報の大きさ：0x04 (副塊の後続バイト数) |
| 3 | 詰合情報 &lt;Packed Fields&gt; |
| 4 | 遅延時間(1/100秒単位)の下位8ビット (Delay Time) |
| 5 | 遅延時間(1/100秒単位)の上位8ビット (Delay Time) |
| 6 | 透明色とする色番号 (Transparet Color Index) |
| 7 | 終端：0x00 (副塊の後続バイト数) |

### 詰合情報 &lt;Packed Fields&gt;

| ビット位置 | ビット数 | 内容 |
|:-:|:-:|:--|
| 0 | 3 | 予約 (Reserved) |
| 3 | 3 | 消去方法 (Disposal Method) |
| 6 | 1 | ユーザー操作による遅延時間の短縮 (User Input Flag) |
| 7 | 1 | 透明色が有効 (Transparent Color Flag) |

**消去方法 (Disposal Method)**

| 値 | 内容 |
|:-:|:--|
| 0 | 未定義（デコーダは何もしない） |
| 1 | 消去しない |
| 2 | 背景色 (論理画面記述子で指定) を使う |
| 3 | 一つ前の画像に戻す |
| 4-7 | （予約） |

## 注釈拡張 (Comment Extension)

| バイト位置<br/>（注釈拡張） | 内容 |
|:-:|:--|
| 0 | 「89a」版拡張 (Extension Introducer)：0x21 (Descriptor) |
| 1 | 注釈表題 (Comment Label)：0xFE |
| 2 | 注釈情報 (副塊情報形式：Data Sub-blocks) |
| - | 〃 |

## 文字画面拡張 (Plain Text Extension)

| バイト位置<br/>（文字画面拡張） | 内容 |
|:-:|:--|
| 0 | 「89a」版拡張 (Extension Introducer)：0x21 (Descriptor) |
| 1 | 文字画面表題 (Plain Text Label)：0x01 |
| 2 | 文字画面設定のサイズ (副塊情報形式：Data Sub-blocks)：0x0C |
| 3 | 文字画面の位置（左）の下位8ビット (Text Grid Left Position) |
| 4 | 文字画面の位置（左）の上位8ビット (Text Grid Left Position) |
| 5 | 文字画面の位置（上）の下位8ビット (Text Grid Top Position) |
| 6 | 文字画面の位置（上）の上位8ビット (Text Grid Top Position) |
| 7 | 文字画面の幅の下位8ビット (Text Grid Width) |
| 8 | 文字画面の幅の上位8ビット (Text Grid Width) |
| 9 | 文字画面の高さの下位8ビット (Text Grid Height) |
| 10 | 文字画面の高さの上位8ビット (Text Grid Height) |
| 11 | 文字の幅 (Character Cell Width) |
| 12 | 文字の高さ (Character Cell Height) |
| 13 | 文字の共通色番号 (Text Foreground Color Index) |
| 14 | 文字背景の共通色番号 (Text Background Color Index) |
| 15 | 任意の文字列 (副塊情報形式：Data Sub-blocks) (Plain Text Data) |
| - | 〃 |

## 応用拡張 (Application Extension)

| バイト位置<br/>（応用拡張） | 内容 |
|:-:|:--|
| 0 | 「89a」版拡張 (Extension Introducer)：0x21 (Descriptor) |
| 1 | 応用拡張表題 (Application Extension Label)：0xFF |
| 2 | 識別子と認識コードのサイズ (副塊情報形式: Data Sub-blocks)：0x0B |
| 3 | 8バイトの識別子 (Application Identifier) |
| 11 | 3バイトの認識コード (Application Authentication Code) |
| 14 | 任意の応用拡張情報 (副塊情報形式：Data Sub-blocks) (Application Data) |
| - | 〃 |

## 終端子 (Trailer)

| バイト位置<br/>（オフセット） | 内容 |
|:-:|:--|
| 0 | 終端子 (Trailer)：0x3B |

# GIF における LZW のパラメータ

LZW 最小コード幅 (LZW Minimum Code Size) を LMCS とします。

| 内容 | 値　　　　　　　　 |
|--:|:--|
| 各コードのビット幅<br/>（可変長データ） | 3 〜 12 |
| LMCS の最小値 | 2 |
| 初期化コード<br/>&lt;Clear&nbsp;Code&gt; | $2^{LMCS}$ |
| 最初のコード | $2^{LMCS} + 1$ |
| コードの最大値 | $2^{12} - 1$ |

# LZW の圧縮と展開

クリアコードを省いているので完全ではありませんが、圧縮と展開のイメージが掴めればと思います。

<details><summary>圧縮と展開の動作を示すサンプル プログラム</summary><div>

```JavaScript:lzw_sample.js
// サンプル: (〒899-7103 鹿児島県)志布志市志布志町志布志 //
const SAMPLE_TEXT = 'SHIBUSHISHI SHIBUSHICHO SHIBUSHI';

// コードと文字の変換.
const CODE_TO_TEXT = ' BCHIOSU';
const codeToChar = ((c) => CODE_TO_TEXT[c]);

// 文字列の並びを反転.
const stringReverse = ((s) => s.split('').reverse().join(''));

/*
 * LZW 圧縮
 */


function encodeLZW(data, minimum_bits=3) {
    let encode_data = '';  // 圧縮データ列(10進).
    let encode_binary = '';  // 圧縮データ列(2進).

    // 空データ対策.
    if (data.length == 0)
        return [encode_data, encode_binary];

    const code_end = (1 << minimum_bits);      // 終端コード.
    const code_clear = code_end + 1;           // 辞書クリアコード.
    let code_dict;  // 文字列の辞書.
    let code_curr;  // 辞書に追加する文字列のコード.

    // 辞書の初期化処理.
    //     code_dict = {
    //         ' ': 0, 'B': 1, 'C': 2, 'H': 3,
    //         'I': 4, 'O': 5, 'S': 6, 'U': 7,
    //     }
    const initDict = function() {
        code_dict = new Map();
        for (let i = 0; i < CODE_TO_TEXT.length; i++)
            code_dict.set(codeToChar(i), i);
        code_curr = code_clear;
    };

    // 圧縮コードのビット列を生成.
    const make_binary = function(code) {
        bits = 32 - Math.clz32(code_curr);  // 出力するデータのビット数を求める.
        binary = '';
        for (let i = 0; i < bits; i++)
            binary = String((code >> i) & 1) + binary;
        return binary;
    }

    initDict();  // 辞書の初期化.

    console.log('圧縮: \'' + data + '\'');
    console.log('最小ビット = ' + minimum_bits);
    console.log('終端コード = ' + code_end);
    console.log('クリアコード = ' + code_clear);
    console.log('圧縮コードの先頭 = ' + (code_clear + 1));

    let diter = data[Symbol.iterator]();  // 参照するデータのイテレータ.
    let str = '';   // 検索文字列.
    let bin_curr;   // 最初の圧縮コード
    for (;;) {
        let dinf = diter.next();  // 配列から文字を取り出す.
        if (dinf.done) break;     // データがなければ終了.

        let ch = dinf.value;      // 文字データ.
        str += ch;                // 検索文字列の更新.
        let bin_try = code_dict.get(str);
        if (bin_try != undefined) {
            console.log('文字[\'' + ch + '\'] : 辞書あり: \'' + str + '\'');
            bin_curr = bin_try;   // 検索結果を更新.
            continue;  // 既に辞書にあれば検索文字列を長くする(次のデータへ).
        }
        console.log('文字[\'' + ch + '\'] : 辞書なし: \'' + str + '\'');

        // 圧縮コードは前回の検索結果を出力する.
        let binary = make_binary(bin_curr);
        encode_data += ' ' + bin_curr;
        encode_binary += ' ' + binary;

        // 辞書に文字列を登録する.
        code_dict.set(str, ++code_curr);

        console.log('  圧縮コード(前回分): \'' + str.slice(0, -1) + '\':' + bin_curr + '=' + binary);
        console.log('  新規辞書登録文字列: \'' + str + '\':' + code_curr);

        /* 本当は code_curr が最大コードに達したら、辞書クリアコードの発行と辞書の初期化が必要 */

        // 文字列の最終文字は圧縮保留状態なので、その文字から再開する.
        str = ch;
        bin_curr = code_dict.get(str);
    }

    {// 最後の圧縮コードを出力する.
        let binary = make_binary(bin_curr);
        encode_data += ' ' + bin_curr;
        encode_binary += ' ' + binary;

        console.log('  最後の圧縮コード  : \'' + str + '\':' + bin_curr + '=' + binary);
    }
    {// 終端コードを出力する.
        let binary = make_binary(code_end);
        encode_data += ' ' + code_end;
        encode_binary += ' ' + binary;

        console.log('  終端コード        : ' + code_end + '=' + binary);
    }

    return [encode_data, encode_binary];
}

// 圧縮データを得る.
const encode_data = function() {
    const [encode_list, encode_binary] = encodeLZW(SAMPLE_TEXT);
    const binaries = encode_binary.slice(1).split(' ');
    const encbin = binaries.reduce(function(p, v) { return p + stringReverse(v); }, '');

    let encstr = encbin;
    let encdat = [];
    while (encstr.length > 8) {
        encdat.push(stringReverse(encstr.slice(0, 8)));
        encstr = encstr.slice(8);
    }
    if (encstr.length > 0)
        encdat.push(stringReverse((encstr + '00000000').slice(0, 8)));
    const data = encdat.map((d) => Number('0b' + d));

    console.log('圧縮コード(10進):' + encode_list);
    console.log('圧縮コード( 2進):' + encode_binary);
    console.log(binaries);
    // console.log(encbin);
    console.log('リトル・エンディアンで詰めた圧縮データ（バイト列）');
    console.log(encdat);
    console.log(data);

    return data;
}();


/*
 * LZW 解凍
 */

function decodeLZW(data, minimum_bits=3) {
    const code_end = (1 << minimum_bits);      // 終端コード.
    const code_clear = code_end + 1;           // 辞書クリアコード.
    let code_dict;  // 文字列の辞書.
    let code_curr;  // 辞書に追加する文字列のコード.

    // 辞書の初期化処理.
    const initDict = function() {
        code_dict = [...Array(code_end)].map((_, i) => codeToChar(i));
        code_dict.push(undefined, undefined);
        code_curr = code_clear + 1;
    };

    // データからビット列を取り出す関数.
    let data_bit_pos = 0;
    const read = function() {
        bits = 32 - Math.clz32(code_curr);  // 入力するデータのビット数を求める.
        let bps = (data_bit_pos >> 3);
        let bpe = ((data_bit_pos + bits - 1) >> 3);
        let v = data[bps];
        if (bps != bpe) {
            v |= data[++bps] << 8;
            if (bps != bpe)
                v |= data[++bps] << 16;
        }
        let s = (data_bit_pos & 7);
        let m = ((1 << bits) - 1);
        data_bit_pos += bits;
        return ((v >> s) & m);
    };

    initDict();  // 辞書の初期化.

    console.log('解凍: 入力は ' + data.length + ' バイト');
    console.log('最小ビット = ' + minimum_bits);
    console.log('終端コード = ' + code_end);
    console.log('クリアコード = ' + code_clear);
    console.log('圧縮コードの先頭 = ' + (code_clear + 1));

    let decode_data = '';
    let code_last = undefined;
    let char_last = undefined;
    for (;;) {
        const code = read();  // コード・データの取得.

        // 終端コードならば終了.
        if (code == code_end) {
            console.log('コード(終端): ' + code);
            break;
        }

        // クリアコードならば辞書の初期化.
        // if (code == code_clear) {
        //     console.log('クリアコード: ' + code);
        //     initDict();
        //     code_last = undefined;
        //     char_last = undefined;
        //     continue;
        // }

        if (code_last == undefined) {
            // 辞書を初期化した直後のコード処理.
            code_last = code;
            char_last = codeToChar(code);
            decode_data += char_last;    // 1文字出力する
            console.log('コード(先頭): \'' + char_last + '\':' + code);
            continue;
        }

        let data_last = code_dict[code_last];
        let code_data;
        if (code < code_curr) {
            // コードは辞書内にあるので、データを辞書から取り出す.
            code_data = code_dict[code];
            console.log('コード(辞書): \'' + code_data + '\':' + code);
        } else if (code == code_curr) {
            // 圧縮時に辞書に追加されたパターンに追随する.
            code_data = data_last + char_last;
            console.log('コード(追随): \'' + code_data + '\':' + code);
        } else {
            console.log('コード(失敗): ' + code + ' > ' + code_curr);
            throw 'デコード失敗：データの異常を検出しました';
        }
        decode_data += code_data;   // データを出力する.

        // 辞書を更新
        char_last = code_data[0];
        code_dict.push(data_last + char_last);
        console.log('    辞書追加: \'' + data_last + char_last + '\':' + code_curr);
        code_curr++;

        // 次へ
        code_last = code;
    }
    return decode_data;
}

// 圧縮データの解凍をする.
console.log('解凍結果: \'' + decodeLZW(encode_data) + '\'');
```

```console:実行結果
$ node
Welcome to Node.js v16.13.1.
Type ".help" for more information.
>
$ node --trace-uncaught lzw_sample.js
圧縮: 'SHIBUSHISHI SHIBUSHICHO SHIBUSHI'
最小ビット = 3
終端コード = 8
クリアコード = 9
圧縮コードの先頭 = 10
文字['S'] : 辞書あり: 'S'
文字['H'] : 辞書なし: 'SH'
  圧縮コード(前回分): 'S':6=0110
  新規辞書登録文字列: 'SH':10
文字['I'] : 辞書なし: 'HI'
  圧縮コード(前回分): 'H':3=0011
  新規辞書登録文字列: 'HI':11
文字['B'] : 辞書なし: 'IB'
  圧縮コード(前回分): 'I':4=0100
  新規辞書登録文字列: 'IB':12
文字['U'] : 辞書なし: 'BU'
  圧縮コード(前回分): 'B':1=0001
  新規辞書登録文字列: 'BU':13
文字['S'] : 辞書なし: 'US'
  圧縮コード(前回分): 'U':7=0111
  新規辞書登録文字列: 'US':14
文字['H'] : 辞書あり: 'SH'
文字['I'] : 辞書なし: 'SHI'
  圧縮コード(前回分): 'SH':10=1010
  新規辞書登録文字列: 'SHI':15
文字['S'] : 辞書なし: 'IS'
  圧縮コード(前回分): 'I':4=0100
  新規辞書登録文字列: 'IS':16
文字['H'] : 辞書あり: 'SH'
文字['I'] : 辞書あり: 'SHI'
文字[' '] : 辞書なし: 'SHI '
  圧縮コード(前回分): 'SHI':15=01111
  新規辞書登録文字列: 'SHI ':17
文字['S'] : 辞書なし: ' S'
  圧縮コード(前回分): ' ':0=00000
  新規辞書登録文字列: ' S':18
文字['H'] : 辞書あり: 'SH'
文字['I'] : 辞書あり: 'SHI'
文字['B'] : 辞書なし: 'SHIB'
  圧縮コード(前回分): 'SHI':15=01111
  新規辞書登録文字列: 'SHIB':19
文字['U'] : 辞書あり: 'BU'
文字['S'] : 辞書なし: 'BUS'
  圧縮コード(前回分): 'BU':13=01101
  新規辞書登録文字列: 'BUS':20
文字['H'] : 辞書あり: 'SH'
文字['I'] : 辞書あり: 'SHI'
文字['C'] : 辞書なし: 'SHIC'
  圧縮コード(前回分): 'SHI':15=01111
  新規辞書登録文字列: 'SHIC':21
文字['H'] : 辞書なし: 'CH'
  圧縮コード(前回分): 'C':2=00010
  新規辞書登録文字列: 'CH':22
文字['O'] : 辞書なし: 'HO'
  圧縮コード(前回分): 'H':3=00011
  新規辞書登録文字列: 'HO':23
文字[' '] : 辞書なし: 'O '
  圧縮コード(前回分): 'O':5=00101
  新規辞書登録文字列: 'O ':24
文字['S'] : 辞書あり: ' S'
文字['H'] : 辞書なし: ' SH'
  圧縮コード(前回分): ' S':18=10010
  新規辞書登録文字列: ' SH':25
文字['I'] : 辞書あり: 'HI'
文字['B'] : 辞書なし: 'HIB'
  圧縮コード(前回分): 'HI':11=01011
  新規辞書登録文字列: 'HIB':26
文字['U'] : 辞書あり: 'BU'
文字['S'] : 辞書あり: 'BUS'
文字['H'] : 辞書なし: 'BUSH'
  圧縮コード(前回分): 'BUS':20=10100
  新規辞書登録文字列: 'BUSH':27
文字['I'] : 辞書あり: 'HI'
  最後の圧縮コード  : 'HI':11=01011
  終端コード        : 8=01000
圧縮コード(10進): 6 3 4 1 7 10 4 15 0 15 13 15 2 3 5 18 11 20 11 8
圧縮コード( 2進): 0110 0011 0100 0001 0111 1010 0100 01111 00000 01111 01101 01111 00010 00011 00101 10010 01011 10100 01011 01000
[
  '0110',  '0011',  '0100',
  '0001',  '0111',  '1010',
  '0100',  '01111', '00000',
  '01111', '01101', '01111',
  '00010', '00011', '00101',
  '10010', '01011', '10100',
  '01011', '01000'
]
リトル・エンディアンで詰めた圧縮データ（バイト列）
[
  '00110110', '00010100',
  '10100111', '11110100',
  '11000000', '01101011',
  '01001111', '10001100',
  '00100010', '00010111',
  '01011101', '00001000'
]
[
   54, 20, 167, 244, 192,
  107, 79, 140,  34,  23,
   93,  8
]
解凍: 入力は 12 バイト
最小ビット = 3
終端コード = 8
クリアコード = 9
圧縮コードの先頭 = 10
コード(先頭): 'S':6
コード(辞書): 'H':3
    辞書追加: 'SH':10
コード(辞書): 'I':4
    辞書追加: 'HI':11
コード(辞書): 'B':1
    辞書追加: 'IB':12
コード(辞書): 'U':7
    辞書追加: 'BU':13
コード(辞書): 'SH':10
    辞書追加: 'US':14
コード(辞書): 'I':4
    辞書追加: 'SHI':15
コード(辞書): 'SHI':15
    辞書追加: 'IS':16
コード(辞書): ' ':0
    辞書追加: 'SHI ':17
コード(辞書): 'SHI':15
    辞書追加: ' S':18
コード(辞書): 'BU':13
    辞書追加: 'SHIB':19
コード(辞書): 'SHI':15
    辞書追加: 'BUS':20
コード(辞書): 'C':2
    辞書追加: 'SHIC':21
コード(辞書): 'H':3
    辞書追加: 'CH':22
コード(辞書): 'O':5
    辞書追加: 'HO':23
コード(辞書): ' S':18
    辞書追加: 'O ':24
コード(辞書): 'HI':11
    辞書追加: ' SH':25
コード(辞書): 'BUS':20
    辞書追加: 'HIB':26
コード(辞書): 'HI':11
    辞書追加: 'BUSH':27
コード(終端): 8
解凍結果: 'SHIBUSHISHI SHIBUSHICHO SHIBUSHI'
```

</div></details>

# サンプル プログラム

<details><summary>HTML+JavaScriptでローカル生成[少々長いので折りたたみ]</summary><div>

```JavaScript:gif_test.html
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" >
    <title>GIF プログラムのテスト</title>
    <style>
      body {
          margin-left: auto;
          margin-right: auto;
          width: 750px;
      }
      table {
          border: solid 1px gray;
          border-collaspe: collaspe;
          border-spacing: 0;
      }
      th, td {
          border: solid 1px gray;
          padding: 4px;
      }
    </style>
  </head>
  <body>
    <h2>GIF プログラムのテスト</h2>
    <table>
      <tr>
        <td style="text-align: center;">
          Sample 1<br/>
          <span id="sample1"></span>
        </td>
      </tr>
      <tr>
        <td style="text-align: center;">
          Sample 2<br/>
          <span id="sample2"></span><br/>
          乱数を使用しているのでページを開く度に異なる
        </td>
      </tr>
      <tr>
        <td style="text-align: center;">
          Sample 3<br/>
          <span id="sample3"></span><br/>
          乱数を使用しているのでページを開く度に異なる
        </td>
      </tr>
      <tr>
        <td style="text-align: center;">
          Sample 4<br/>
          <span id="sample4"></span><br/>
          乱数を使用しているのでページを開く度に異なる
        </td>
      </tr>
      <tr>
        <td style="text-align: center;">
          Sample 5<br/>
          <span id="sample5"></span><br/>
          乱数を使用しているのでページを開く度に異なる
        </td>
      </tr>
<!--
-->
      <tr>
        <td style="text-align: center;">
          Sample A1<br/>
          <span id="sampleA1"></span><br/>
          乱数を使用しているのでページを開く度に異なる
        </td>
      </tr>
    </table>
    <script type="text/javascript">
    <!--

      /* ********************************** */

      /*
       * GIF 形式データを生成するクラス
       */

      class MyGIF {
          static signature = [0x47, 0x49, 0x46];
          static version87a = [0x38, 0x37, 0x61];
          static version89a = [0x38, 0x39, 0x61];

          // コンストラクタ.
          constructor(width, height, resolution, background, global_color, aspect) {
              const S = MyGIF;
              this.S = S;
              this.signature = S.signature;
              this.version = S.version89a;
              this.logicalScreen = S.createLogicalScreenDescriptor(
                  width, height, resolution, background, global_color, aspect);
              this.descriptor = [];
          };

          // GIF形式のバイナリを取得する.
          get_binary() {
              return [
                  this.signature,
                  this.version,
                  this.logicalScreen.get_binary(),
                  this.descriptor.map((d, _) => d.get_binary()).flat(),
              ].flat();
          };

          // 任意の Descriptor を追加する.
          appendDescriptor(descriptor) {
              this.descriptor.push(descriptor);
          };

          // Image Descriptor を追加する.
          appendImageDescriptor(x, y, w, h, data, color, interlace) {
              this.descriptor.push(this.S.createImageDescriptor(x, y, w, h, data, color, interlace));
          };

          // Graphic Control Extension を追加する.
          appendGraphicControlExtension(delay, method, transparent, input) {
              this.descriptor.push(this.S.createGraphicControlExtension(delay, method, transparent, input));
          };

          // Comment Extension を追加する.
          appendCommentExtension(text) {
              this.descriptor.push(this.S.createCommentExtension(text));
          };

          // Plain Text Extension を追加する.
          appendPlainTextExtension(x, y, w, h, cw, ch, fc, bc, text) {
              this.descriptor.push(this.S.createPlainTextExtension(x, y, w, h, cw, ch, fc, bc, text));
          };

          // Application Extension を追加する.
          appendApplicationExtension(id, code, data) {
              this.descriptor.push(this.S.createApplicationExtension(id, code, data));
          };

          // アニメーションのループ回数を設定する.
          appendApplicationExtensionForLoop(count) {
              this.appendApplicationExtension('NETSCAPE', '2.0', [1, (count & 0xff), ((count >> 8) & 0xff)]);
          };

          // Trailer を追加する.
          appendTrailer() {
              this.descriptor.push(this.S.createTrailer());
          };

          // 8 ビット形式の取得.
          static uint8el(x) {
              return ((!x ? 0 : x) & 0xff);
          };

          // 16 ビット・リトル・エンディアン形式の取得.
          static uint16el(x) {
              const y = (!x ? 0 : x);
              return [(y & 0xff), ((y >> 8) & 0xff)];
          };

          // 色情報の過不足を調整.
          static fixColorTable(depth, color) {
              const colen = (1 << (depth + 1)) * 3;
              let table = color.flat();
              return ((table.length > colen) ? table.slice(0, colen) :
                      table.concat(Array(colen - table.length).fill(0)));
          };

          // 文字の UTF-8 バイナリ化.
          static charToUTF8(c) {
              if (c < 0x0080) return c;
              if (c < 0x0800) return [
                  (0xc0 | (c >> 6)),
                  (0x80 | (c & 0x3f)),
              ];
              return [
                  (0xe0 | (c >> 12)),
                  (0x80 | ((c >> 6) & 0x3f)),
                  (0x80 | (c & 0x3f)),
              ];
          };
          static stringToUTF8(s) {
              return [...Array(s.length)].map((_, i) => MyGIF.charToUTF8(s.charCodeAt(i))).flat();
          };

          // Data Sub-blocks の生成.
          static createDataSubBlocks(data) {
              let block = [];
              let pos = 0;
              let length = data.length;
              while (length) {
                  let slen = (length < 256) ? length : 255;
                  block.push(slen);
                  block.push(data.slice(pos, pos + slen));
                  pos += slen;
                  length -= slen;
              };
              block.push(0);
              return block.flat();
          };

          // Logical Screen Descriptor の生成.
          static createLogicalScreenDescriptor(width, height, resolution, background, color, sort, aspect) {
              const S = MyGIF;
              const D = {
                  logical_screen_width: width,
                  logical_screen_height: height,

                  size_of_global_color_table: 0,
                  sort_flag: sort,
                  color_resolution: resolution,
                  global_color_table_flag: false,

                  background_color_index: background,
                  pixel_aspect_ratio: ((aspect == undefined) ? 49 : aspect),

                  global_color_table: undefined,

                  set_color: (function(table) {
                      const flag = (table != undefined);
                      D.size_of_global_color_table =
                          (!flag ? 0 : (31 - Math.clz32(table.length-1)));
                      D.global_color_table_flag = flag;
                      D.global_color_table = table;
                  }),

                  get_binary: (function() {
                      const sgct = (!D.size_of_global_color_table ? 0 : (7 & D.size_of_global_color_table));
                      const sort = (!D.sort_flag ? 0 : 1);
                      const res = (!D.color_resolution ? 0 : (7 & D.color_resolution));
                      const gcf = (!D.global_color_table_flag ? 0 : 1);
                      const bgc = (!D.background_color_index ? 0 : D.background_color_index);
                      const aspect = ((D.pixel_aspect_ratio == undefined) ? 49 : D.pixel_aspect_ratio);
                      const color = (!gcf ? [] : S.fixColorTable(sgct, D.global_color_table));
                      return [
                          S.uint16el(D.logical_screen_width),
                          S.uint16el(D.logical_screen_height),
                          (sgct | (sort << 3) | (res << 4) | (gcf << 7)),
                          S.uint8el(bgc),
                          S.uint8el(aspect),
                          color.map((v, _) => S.uint8el(v)),
                      ].flat();
                  }),
              };
              D.set_color(color);
              return D;
          };

          // Image Descriptor の生成.
          static createImageDescriptor(x, y, w, h, data, color, interlace, sort) {
              const S = MyGIF;
              const D = {
                  descriptor: 0x2c,

                  image_left_position: x,
                  image_top_position: y,
                  image_width: w,
                  image_height: h,

                  size_of_local_color_table: 0,
                  sort_flag: sort,
                  interlace_flag: interlace,
                  local_color_table_flag: false,

                  local_color_table: undefined,
                  table_based_image_data: S.createTableBasedImageData(data),

                  set_color_table: (function(table) {
                      const flag = (table != undefined);
                      D.size_of_local_color_table =
                          (!flag ? 0 : (31 - Math.clz32(table.length-1)));
                      D.local_color_table_flag = flag;
                      D.local_color_table = table;
                  }),

                  get_binary: (function() {
                      const slct = (!D.size_of_local_color_table ? 0 : (7 & D.size_of_local_color_table));
                      const sort = (!D.sort_flag ? 0 : 1);
                      const interlace = (!D.interlace ? 0 : 1);
                      const lcf = (!D.local_color_table_flag ? 0 : 1);
                      const color = (!lcf ? [] : S.fixColorTable(slct, D.local_color_table));
                      return [
                          D.descriptor,
                          S.uint16el(D.image_left_position),
                          S.uint16el(D.image_top_position),
                          S.uint16el(D.image_width),
                          S.uint16el(D.image_height),
                          (slct | (sort << 5) | (interlace << 6) | (lcf << 7)),
                          color.map((v, _) => S.uint8el(v)),
                          D.table_based_image_data.lzw_minimum_code_size,
                          D.table_based_image_data.data,
                      ].flat();
                  }),
              };
              D.set_color_table(color);
              return D;
          };

          // Table Based Image の生成 (LZW圧縮)
          static createTableBasedImageData(source) {
              const S = MyGIF;
              const source_size = source.length;
              // const src_max = Math.max.apply(null, source);  // 'Maximum call stack size exceeded' がでる.
              const src_max = function() { let m = 0; for (const d of source) if (m < d) m = d; return m; }();
              const src_max_clz = (32 - Math.clz32(src_max));

              const bits_init = ((src_max_clz < 2) ? 2 : src_max_clz);
              let bits_curr = (bits_init + 1);

              const code_base = (1 << bits_init);
              const code_clear = code_base;
              const code_end = code_base + 1;
              const code_max = ((1 << 12) - 1);

              let code_curr = code_end;
              let code_step = (1 << bits_curr);

              const tree = [...Array(code_max + 2)].map((_, i) => ({ code: i, next: -1, down: -1, data: 0 }));

              const buffer = [];
              let bs_pos = 0;

              const write = function(data) {
                  const bits = bits_curr;
                  let idxs = (bs_pos >> 3);
                  const idxe = ((bs_pos + bits - 1) >> 3);
                  data <<= (bs_pos & 7);
                  if (idxs < buffer.length) {
                      buffer[idxs] |= (data & 0xff);
                      data >>= 8;
                      idxs++;
                  };
                  while (idxs <= idxe) {
                      buffer.push(data & 0xff);
                      data >>= 8;
                      idxs++;
                  };
                  bs_pos += bits;
              };

              write(code_clear);
              if (!source_size) {
                  write(code_end);
                  return {
                      lzw_minimum_code_size: bits_init,
                      data: S.createDataSubBlocks(buffer),
                  };
              };

              let siter = source.values();
              let sdat = siter.next();
              lzw: for (;;) {
                  let data, next;
                  let node = tree[(data = sdat.value)];
                  scan: for (;;) {
                      if ((sdat = siter.next()).done) {
                          write(node.code);
                          break lzw;
                      };
                      data = sdat.value;
                      if ((next = tree[node.down]) == undefined)
                          break;
                      while (data != next.data)
                          if ((next = tree[next.next]) == undefined)
                              break scan;
                      node = next;
                  };

                  write(node.code);
                  {
                      const next = tree[++code_curr];
                      // next.code = code_curr;
                      next.next = node.down;
                      next.down = -1;
                      next.data = data;
                      node.down = code_curr;
                  }
                  if (code_curr < code_step)
                      continue;
                  if (code_curr < code_max) {
                      bits_curr++;
                      if ((code_step <<= 1) < code_max)
                          continue;
                      code_step = code_max;
                      continue;
                  };
                  write(code_clear);

                  bits_curr = bits_init + 1;
                  code_step = (1 << bits_curr);
                  code_curr = code_end;
                  for (let i = 0; i < code_base; i++) {
                      const node = tree[i];
                      // node.code = i;
                      node.next = -1;
                      node.down = -1;
                      // node.data = 0;
                  };
              };
              write(code_end);

              return {
                  lzw_minimum_code_size: bits_init,
                  data: S.createDataSubBlocks(buffer),
              };
          };

          // Graphic Control Extension の生成.
          static createGraphicControlExtension(delay, method, transparent, input) {
              const S = MyGIF;
              const D = {
                  descriptor: 0x21,
                  label: 0xf9,

                  disposal_method: method,
                  user_input_flag: input,
                  transparent_color_flag: false,

                  delay_time: delay,
                  transparent_color_index: 0,

                  set_transparent: (function(index) {
                      D.transparent_color_flag = (index != undefined);
                      D.transparent_color_index = (!index ? 0 : index);
                  }),

                  get_binary: (function() {
                      const method = (!D.disposal_method ? 0 : (7 & D.disposal_method));
                      const input = (!D.user_input_flag ? 0 : 1);
                      const ftrans = (!D.transparent_color_flag ? 0 : 1);
                      return [
                          D.descriptor,
                          D.label,
                          4, /* Data Sub-blocks */
                          ((method << 3) | (input << 6) | (ftrans << 7)),
                          S.uint16el(D.delay_time),
                          S.uint8el(D.transparent_color_index),
                          0, /* Data Sub-blocks */
                      ].flat();
                  }),
              };
              D.set_transparent(transparent);
              return D;
          };

          // Comment Extension の生成.
          static createCommentExtension(text) {
              const S = MyGIF;
              const D = {
                  descriptor: 0x21,
                  label: 0xfe,

                  text: text,

                  get_binary: (() => [
                      D.descriptor,
                      D.label,
                      S.createDataSubBlocks(S.stringToUTF8(D.tex)),
                  ].flat()),
              };
              return D;
          };

          // Plain Text Extension の生成.
          static createPlainTextExtension(x, y, w, h, cw, ch, fc, bc, text) {
              const S = MyGIF;
              const D = {
                  descriptor: 0x21,
                  label: 0x01,

                  text_grid_left_position: x,
                  text_grid_top_position: y,
                  text_grid_width: w,
                  text_grid_height: h,
                  character_cell_width: cw,
                  character_cell_height: ch,
                  text_foreground_color_index: fc,
                  text_background_color_index: bc,
                  plain_text_data: text,

                  get_binary: (() => [
                      D.descriptor,
                      D.label,
                      12, /* Data Sub-blocks */
                      S.uint16el(D.text_grid_left_position),
                      S.uint16el(D.text_grid_top_position),
                      S.uint16el(D.text_grid_width),
                      S.uint16el(D.text_grid_height),
                      S.uint8el(D.character_cell_width),
                      S.uint8el(D.character_cell_height),
                      S.uint8el(D.text_foreground_color_index),
                      S.uint8el(D.text_background_color_index),
                      S.createDataSubBlocks(S.stringToUTF8(D.plain_text_data)),
                  ].flat()),
              };
              return D;
          };

          // Application Extension の生成.
          static createApplicationExtension(id, code, data) {
              const S = MyGIF;
              const D = {
                  descriptor: 0x21,
                  label: 0xff,

                  application_identifier: id,
                  application_authentication_code: code,
                  application_data: data,

                  get_binary: (() => [
                      D.descriptor,
                      D.label,
                      11, /* Data Sub-blocks */
                      S.stringToUTF8(D.application_identifier + '        ').slice(0, 8),
                      S.stringToUTF8(D.application_authentication_code + '   ').slice(0, 3),
                      S.createDataSubBlocks(D.application_data.map((v, _) => S.uint8el(v))),
                  ].flat()),
              };
              return D;
          };

          // Trailer の生成.
          static createTrailer() {
              return {
                  descriptor: 0x3b,
                  get_binary: (() => 0x3b),
              };
          };
      };

      /* **************************************** */
      /*
       * Base 64 符号化
       */

      class MyBase64 {
          static EncodeTable = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';

          static encode(b) {
              const table = MyBase64.EncodeTable;
              const blen = b.length;
              const brem = blen % 3;
              const bcnt = blen - brem;
              let s = '';
              let i = 0;
              while (i < bcnt) {
                  const d0 = b[i++];
                  const d1 = b[i++];
                  const d2 = b[i++];
                  const d = (d0 << 16) | (d1 << 8) | d2;
                  s += table[(d >> 18) & 0x3f];
                  s += table[(d >> 12) & 0x3f];
                  s += table[(d >>  6) & 0x3f];
                  s += table[d & 0x3f];
              };
              if (brem) {
                  const b2 = (brem == 2);
                  const d0 = b[i++];
                  const d1 = (b2 ? b[i++] : 0);
                  const d = (d0 << 16) | (d1 << 8);
                  s += table[(d >> 18) & 0x3f];
                  s += table[(d >> 12) & 0x3f];
                  s += (b2 ? table[(d >> 6) & 0x3f] : '=');
                  s += '=';
              };
              return s;
          };
      };

      /* ********************************** */

      /*
       * GIF 画像生成用
       */

      class MyPixelBuffer {
          static grayScaleColor = function(count) {
              const m = 255;
              const n = count - 1;
              return [...Array(count)].map(function(_, k) {
                  const l = Math.trunc((k * m) / n);
                  return [l, l, l];
              });
          };
          static grayScaleColor4 = MyPixelBuffer.grayScaleColor(4);
          static grayScaleColor256 = MyPixelBuffer.grayScaleColor(256);

          static simpleColorBase = [...Array(8)].map((_, i) => Math.trunc(i * 255 / 7));
          static simpleColor = [...Array(256)].map((_, i) => [
              MyPixelBuffer.simpleColorBase[(i >> 5) & 7],
              MyPixelBuffer.simpleColorBase[(i >> 2) & 7],
              [0, 85, 170, 255][i & 3],
          ]);

          static interlaceMap = new Map();
          static createInterlaceTable(h) {
              const m = MyPixelBuffer.interlaceMap;
              if (m.has(h))
                  return m.get(h);
              const ph = [
                  [0, 8, ((h + 7) >> 3)],
                  [4, 8, ((h + 3) >> 3)],
                  [2, 4, ((h + 1) >> 2)],
                  [1, 2, (h >> 1)],
              ];
              const tab = [...Array(4)].map(
                  (_, i) => [...Array(ph[i][2])].map(
                      (_, j) => ph[i][0] + ph[i][1] * j)).flat();
              m.set(h, tab);
              return tab;
          };

          // コンストラクタ.
          constructor(color, width, height, defcol=0) {
              this.S = MyPixelBuffer;
              this.GIF = MyGIF;
              this.color = color;
              this.width = width;
              this.height = height;
              this.pixel = Array(width * height).fill(defcol);
          };

          // 点を打つ.
          setPoint(x, y, c) {
              const w = this.width;
              if ((0 <= x) && (x < w) &&
                  (0 <= y) && (y < this.height)) {
                  const p = y * w + x;
                  if ((p < 0) || (this.pixel.length <= p)) {
                      console.log(p);
                  };
                  this.pixel[p] = c;
              }
          };

          // 線を引く.
          setLine(x1, y1, x2, y2, c, sep) {
              const dx = Math.trunc(Math.abs(x1 - x2));
              const dy = Math.trunc(Math.abs(y1 - y2));
              if (!dx)
                  this.setVirticalLine(x1, y1, y2, c);
              else if (!dy)
                  this.setHorizontalLine(x1, x2, y1, c);
              else if (dx >= dy)
                  this.setLineH(x1, y1, x2, y2, dx, dy, c, sep);
              else
                  this.setLineV(x1, y1, x2, y2, dx, dy, c, sep);
          };
          setLineH(x1, y1, x2, y2, dx, dy, c, sep) {
              const S = this.S;
              const w = this.width;
              const h = this.height;
              let xs, ys, xe, ye;

              if (x1 < x2) {
                  xs = x1; ys = y1;
                  xe = x2; ye = y2;
              } else {
                  xs = x2; ys = y2;
                  xe = x1; ye = y1;
              };
              if ((xe < 0) || (w <= xs))
                  return;

              const ay = ((ys < ye) ? +1 : -1);
              const ap = (ay < 0) ? -w: +w;

              let k = (dy >> 1);
              dx++;
              if (sep) {
                  dy++;
                  k = 0;
              };
              if (xs < 0) {
                  const kt = k - (dy * xs);
                  const yh = Math.trunc(kt / dx);
                  xs = 0;
                  ys += ((ay < 0) ? -yh : +yh);
                  k = (kt % dx);
              };
              if (w <= xe)
                  xe = w - 1;

              const yt = Math.min(ys, ye);
              const yb = Math.max(ys, ye);
              if ((yb < 0) || (h <= yt))
                  return;
              if (ay < 0) {
                  if (ye < 0)
                      ye = 0;
                  if (h <= ys) {
                      const yh = ys - h + 1;
                      const kt = k + (dx * yh);
                      const xw = Math.trunc(kt / dy);
                      xs += xw;
                      ys = h - 1;
                      k = kt % dy;
                  };
              } else {
                  if (h <= ye)
                      ye = h - 1;
                  if (ys < 0) {
                      const kt = k - (dx * ys);
                      const xw = Math.trunc(kt / dy);
                      xs += xw;
                      ys = 0;
                      k = kt % dy;
                  };
              };
              ye += ay;

              const pixel = this.pixel;
              let p = ys * w + xs;
              while (xs++ <= xe) {
                  pixel[p++] = c;
                  if ((k += dy) >= dx) {
                      k -= dx;
                      p += ap;
                      if ((ys += ay) == ye)
                          break;
                  };
              };
          };
          setLineV(x1, y1, x2, y2, dx, dy, c, sep) {
              const S = this.S;
              const w = this.width;
              const h = this.height;
              let xs, ys, xe, ye;

              if (y1 < y2) {
                  xs = x1; ys = y1;
                  xe = x2; ye = y2;
              } else {
                  xs = x2; ys = y2;
                  xe = x1; ye = y1;
              };
              if ((ye < 0) || (h <= ys))
                  return;

              const ax = ((xs < xe) ? +1 : -1);

              let k = (dy >> 1);
              dy++;
              if (sep) {
                  dx++;
                  k = 0;
              };
              if (ys < 0) {
                  const kt = k - (dx * ys);
                  const xw = Math.trunc(kt / dy);
                  xs += ((ax < 0) ? -xw : +xw);
                  ys = 0;
                  k = (kt % dy);
              };
              if (h <= ye)
                  ye = h - 1;

              const xl = Math.min(xs, xe);
              const xr = Math.max(xs, xe);
              if ((xr < 0) || (w <= xl))
                  return;
              if (ax < 0) {
                  if (xe < 0)
                      xe = 0;
                  if (w <= xs) {
                      const xw = xs - w + 1;
                      const kt = k + (dy * xw);
                      const yh = Math.trunc(kt / dx);
                      xs = w - 1;
                      ys += yh;
                      k = kt % dx;
                  };
              } else {
                  if (w <= xe)
                      xe = w - 1;
                  if (xs < 0) {
                      const kt = k - (dy * xs);
                      const yh = Math.trunc(kt / dx);
                      xs = 0;
                      ys += yh;
                      k = kt % dx;
                  };
              };
              xe += ax;

              const pixel = this.pixel;
              let p = ys * w + xs;
              while (ys++ <= ye) {
                  pixel[p] = c; p += w;
                  if ((k += dx) >= dy) {
                      k -= dy;
                      p += ax;
                      if ((xs += ax) == xe)
                          break;
                  };
              };
          };

          // 水平線を引く.
          setHorizontalLine(x1, x2, y, c) {
              const w = this.width;
              const h = this.height;
              const pixel = this.pixel;
              const xs = Math.trunc(Math.min(x1, x2));
              const xe = Math.trunc(Math.max(x1, x2));
              if ((xe < 0) || (w <= xs) || (y < 0) || (h <= y))
                  return;
              const us = Math.trunc(Math.max(xs, 0));
              const ue = Math.trunc(Math.min(xe, w-1));
              const yp = y * w;
              pixel.fill(c, yp + us, yp + ue + 1);
          }

          // 垂直線を引く.
          setVirticalLine(x, y1, y2, c) {
              const w = this.width;
              const h = this.height;
              const pixel = this.pixel;
              const ys = Math.trunc(Math.min(y1, y2));
              const ye = Math.trunc(Math.max(y1, y2));
              if ((x < 0) || (w <= x) || (ye < 0) || (h <= ys))
                  return;
              const vs = Math.trunc(Math.max(ys, 0));
              const ve = Math.trunc(Math.min(ye, h-1));
              const q = ve * w + x;
              for (let p = vs * w + x; p <= q; p += w)
                  pixel[p] = c;
          }

          // 四角形を描く.
          setBox(x1, y1, x2, y2, c) {
              const xs = Math.trunc(Math.min(x1, x2));
              const xe = Math.trunc(Math.max(x1, x2));
              const ys = Math.trunc(Math.min(y1, y2));
              const ye = Math.trunc(Math.max(y1, y2));
              this.setHorizontalLine(xs, xe, ys, c);
              if (ys != ye) {
                  this.setVirticalLine(xs, ys+1, ye-1, c);
                  if (xs != xe)
                      this.setVirticalLine(xe, ys+1, ye-1, c);
                  this.setHorizontalLine(xs, xe, ye, c);
              };
          };

          // 四角形で塗りつぶす.
          setBoxFill(x1, y1, x2, y2, c) {
              const w = this.width;
              const h = this.height;
              const pixel = this.pixel;
              const xs = Math.min(x1, x2);
              const xe = Math.max(x1, x2);
              const ys = Math.min(y1, y2);
              const ye = Math.max(y1, y2);
              if ((xe < 0) || (w <= xs) ||
                  (ye < 0) || (h <= ys))
                  return;
              const us = Math.trunc(Math.max(xs, 0));
              const ue = Math.trunc(Math.min(xe, w-1));
              const vs = Math.trunc(Math.max(ys, 0));
              const ve = Math.trunc(Math.min(ye, h-1));
              const nu = (ue - us) + 1;
              const nv = (ve - vs) + 1;
              let vp = vs * w + us;
              const vq = nv * w + vp;
              for (; vp < vq; vp += w)
                  pixel.fill(c, vp, vp + nu);
          };

          // 円描画用の座標情報を生成する.
          static getCirclePos(radius) {
              if (radius <= 0)
                  return [[0, 0]];

              let x = radius;
              let y = 0;
              let s = 0;
              let t = 1;
              let u = (radius << 1) - 1;
              let v = u;

              /* 大きな円は（上下左右）端の直線を少し短くする */
              u -= ((u >> 2) + (u >> 3) + (u >> 4));

              let otmp = new Array();
              const out1 = new Array();
              const out2 = new Array();
              while (x > y) {
                  otmp.push(y);
                  out2.push([x]);
                  y = y + 1;
                  s = s + t;
                  t = t + 2;
                  if (s < u)
                      continue;
                  out1.push(otmp);
                  x = x - 1;
                  v = v - 2;
                  u = u + v;
                  otmp = new Array();
              };
              if (x == y)
                  out2.push([x]);
              if (otmp.length)
                  out1.push(otmp);
              out1.reverse();
              return out2.concat(out1);
          };

          // 円を描く.
          setCircle(x, y, r, c) {
              const w = this.width;
              const h = this.height;
              if (((x + r + 1) < 0) || (w < (x - r)) ||
                  ((y + r + 1) < 0) || (h < (y - r)))
                  return;

              const pixel = this.pixel;
              const ypos = this.S.getCirclePos(r);
              const ylen = ypos.length;
              {
                  let yi = 0;
                  let ys = y;
                  if (ys < 0) {
                      yi = -ys;
                      ys = 0;
                  };
                  let yp = ys * w;
                  while (yi < ylen) {
                      if (ys >= h)
                          break;
                      for (const xp of ypos[yi]) {
                          const xl = x - xp;
                          const xr = x + xp;

                          if (w <= xl) continue;
                          if (xr < 0) continue;
                          if (xr < w) pixel[yp + xr] = c;
                          if (0 <= xl) pixel[yp + xl] = c;
                      };
                      yi++;
                      ys++;
                      yp += w;
                  };
              };
              {
                  let yi = 0;
                  let ys = y;
                  if (ys >= h) {
                      yi += (ys - h + 1);
                      ys = h - 1;
                  };
                  let yp = ys * w;
                  while (yi < ylen) {
                      if (ys < 0)
                          break;
                      for (const xp of ypos[yi]) {
                          const xl = x - xp;
                          const xr = x + xp;

                          if (w <= xl) continue;
                          if (xr < 0) continue;
                          if (xr < w) pixel[yp + xr] = c;
                          if (0 <= xl) pixel[yp + xl] = c;
                      };
                      ys--;
                      yi++;
                      yp -= w;
                  };
              };
          };

          // 円で塗りつぶす.
          setCircleFill(x, y, r, c) {
              const w = this.width;
              const h = this.height;
              if (((x + r + 1) < 0) || (w < (x - r)) ||
                  ((y + r + 1) < 0) || (h < (y - r)))
                  return;

              const pixel = this.pixel;
              const ypos = this.S.getCirclePos(r);
              const ylen = ypos.length;
              {
                  let yi = 0;
                  let ys = y;
                  if (ys < 0) {
                      yi = -ys;
                      ys = 0;
                  };
                  let yp = ys * w;
                  while (yi < ylen) {
                      if (ys >= h)
                          break;
                      const yl = ypos[yi];
                      const xp = yl[yl.length - 1];
                      let xl = x - xp;
                      let xr = x + xp;
                      if ((0 <= xr) && (xl < w)) {
                          if (xl < 0) xl = 0;
                          if (xr >= w) xr = w - 1;
                          pixel.fill(c, yp + xl, yp + xr + 1);
                      }
                      yi++;
                      ys++;
                      yp += w;
                  };
              };
              {
                  let yi = 0;
                  let ys = y;
                  if (ys >= h) {
                      yi += (ys - h + 1);
                      ys = h - 1;
                  };
                  let yp = ys * w;
                  while (yi < ylen) {
                      if (ys < 0)
                          break;
                      const yl = ypos[yi];
                      const xp = yl[yl.length - 1];
                      let xl = x - xp;
                      let xr = x + xp;
                      if ((0 <= xr) && (xl < w)) {
                          if (xl < 0) xl = 0;
                          if (xr >= w) xr = w - 1;
                          pixel.fill(c, yp + xl, yp + xr + 1);
                      }
                      ys--;
                      yi++;
                      yp -= w;
                  };
              };
          };

          // 凸多角形を描画する.
          setPolygon(vertices, c) {
              const w = this.width;
              const h = this.height;
              const pixel = this.pixel;
              const vlen = vertices.length;
              if (!vlen) return;

              let xmin = w, xmax = -1;
              let ymin = h, ymax = -1;
              for (const [x, y] of vertices) {
                  xmin = Math.min(xmin, x);
                  xmax = Math.max(xmax, x);
                  ymin = Math.min(ymin, y);
                  ymax = Math.max(ymax, y);
              };
              xmin = Math.trunc(xmin);
              xmax = Math.trunc(xmax);
              ymin = Math.trunc(ymin);
              ymax = Math.trunc(ymax);
              if ((xmax < 0) || (w <= xmin) ||
                  (ymax < 0) || (h <= ymin))
                  return;

              const raster = [...Array(h)].map(() => []);
              let [nx, ny] = vertices[vlen - 1];
              for (const np of vertices) {
                  const lx = nx;
                  const ly = ny;
                  [nx, ny] = np;

                  const ay = (ny < ly) ? -1 : +1;
                  const dx = nx - lx;
                  const dy = (ay < 0) ? (ly - ny) : (ny - ly);

                  if (dy == 0) {
                      // if ((0 <= ly) && (ly < h))
                      //     raster[ly].push(lx, nx);
                      continue;
                  };

                  let ys = ly;
                  let ye = ny;
                  let yp = 0;
                  if (ay >= 0) {
                      if ((ye < 0) || (h <= ys))
                          continue;
                      if (ys < 0) {
                          yp -= ys;
                          ys = 0;
                      };
                      if (ye >= h)
                          ye = h - 1;
                  } else {
                      if ((ys < 0) || (h <= ye))
                          continue;
                      if (ys >= h) {
                          yp += ys - h + 1;
                          ys = h - 1;
                      };
                      if (ye < 0)
                          ye = -1;
                  };
                  for (; ys != ye; ys += ay, yp++)
                      raster[ys].push(lx + Math.trunc(dx * yp / dy));
              };

              const rcmp = ((a, b) => (a - b));
              let rs = Math.trunc(Math.max(ymin, 0));
              let re = Math.trunc(Math.min(ymax, h - 1));
              for (let yp = rs * w; rs <= re; yp += w) {
                  const rp = raster[rs++];
                  const rplen = rp.length;
                  if (rplen == 1) continue;
                  rp.sort(rcmp);
                  for (let np = 0; np < rplen; np += 2) {
                      let xl = rp[np];
                      let xr = rp[np + 1];
                      if (xl > xr)
                          [xl, xr] = [xr, xl];
                      if ((xr < 0) || (w <= xl))
                          continue;
                      if (xl < 0) xl = 0;
                      if (xr >= w) xr = w - 1;
                      pixel.fill(c, yp + xl, yp + xr + 1);
                  }
              };
          };

          // インターレース画像を作成する.
          createInterlaced() {
              const tab = this.S.createInterlaceTable(h);
              const w = this.width;
              const h = this.height;
              const pixel = this.pixel;
              const line = [...Array(h)].map(
                  function(_, y) { const p = y * w; return pixel.slice(p, p + w); });
              return [...Array(h)].map((_, y) => line[tab[y]]).flat();
          };

          // GIF 処理用オブジェクトを作成する.
          createGIF(global_color) {
              const color = (global_color ? this.color : undefined);
              return new this.GIF(this.width, this.height, (8 - 1), 0, color);
          };

          // Image Descriptor を作成する.
          createImageDescriptor(local_color, x, y, interlace) {
              const color = (local_color ? this.color : undefined);
              const w = this.width;
              const h = this.height;
              const pixel = this.pixel;
              const data = (!interlace ? pixel : this.createInterlaced());
              return this.GIF.createImageDescriptor(x, y, w, h, data, color, interlace);
          };
      };

      /* ********************************** */

      /*
       * テスト
       */

      function createImgWithBase64(b64s, id) {
          const priority = 'important';
          const img = document.createElement('img');
          if (id != undefined)
              img.setAttribute('id', id);
          img.setAttribute('class', 'b64img');
          img.setAttribute('src', 'data:image/gif;charset=utf-8;base64,' + b64s);
          img.style.setProperty('margin', '0', priority);
          img.style.setProperty('padding', '8px', priority);
          img.style.setProperty('image-rendering', 'pixelated', priority);
          return img;
      }

      function intRand(w) { return Math.trunc(Math.random() * w); }

      function makeSampleGIF(fb) {
          const gif = fb.createGIF(true);
          gif.appendDescriptor(fb.createImageDescriptor());
          gif.appendTrailer();
          return gif;
      }

      function makeSample1FB() {
          const fb = new MyPixelBuffer(MyPixelBuffer.grayScaleColor256, 640, 480);
          for (let p = 0; p < 256; p++) fb.setBox(p, p, 639-p, 479-p, p);
          return fb;
      }
      function makeSample1() { return makeSampleGIF(makeSample1FB()); }

      function makeSample2FB() {
          const fb = new MyPixelBuffer(MyPixelBuffer.simpleColor, 640, 480);
          for (let n = 0; n < 20; n++) {
              const x1 = intRand(640 + 320) - 160;
              const y1 = intRand(480 + 240) - 120;
              const w = intRand(320) + 16;
              const h = intRand(240) + 16;
              const c = intRand(256);
              const x2 = x1 + w;
              const y2 = y1 + h;
              fb.setLine(x1, y1, x2, y2, c);
              fb.setLine(x2, y1, x1, y2, c);
              fb.setBox(x1, y1, x2, y2, c);
              fb.setCircle(y1, x1, Math.min(x2, y2), c);
          }
          return fb;
      }
      function makeSample2() { return makeSampleGIF(makeSample2FB()); }

      function makeSample3FB() {
          const fb = new MyPixelBuffer(MyPixelBuffer.simpleColor, 640, 480);
          for (let n = 0; n < 100; n++) {
              const x = intRand(640+32) - 16;
              const y = intRand(480+32) - 16;
              const w = intRand(160);
              const h = intRand(120);
              const c = intRand(256);
              fb.setBoxFill(x, y, x+w, y+h, c);
          }
          return fb;
      }
      function makeSample3() { return makeSampleGIF(makeSample3FB()); }

      function makeSample4FB() {
          const fb = new MyPixelBuffer(MyPixelBuffer.simpleColor, 640, 480);
          for (let n = 0; n < 100; n++) {
              const x = intRand(640 + 320) - 160;
              const y = intRand(480 + 240) - 120;
              const r = intRand(80);
              const c = intRand(256);
              fb.setCircleFill(x, y, r, c);
          }
          return fb;
      }
      function makeSample4() { return makeSampleGIF(makeSample4FB()); }

      function makeSample5FB() {
          const fb = new MyPixelBuffer(MyPixelBuffer.simpleColor, 640, 480);
          for (let n1 = 0; n1 < 100; n1++) {
              const np = intRand(5) + 3;
              let x = intRand(640 + 320) - 160;
              let y = intRand(480 + 240) - 120;
              const c = intRand(256);
              const pl = new Array([x, y]);
              let da = intRand(360);
              let ra = 300;
              for (let n2 = 1; n2 < np; n2++) {
                  const dr = da * Math.PI / 180;
                  const dl = intRand(120) + 10;
                  const dx = Math.trunc(dl * Math.cos(dr));
                  const dy = Math.trunc(dl * Math.sin(dr));
                  pl.push([(x += dx), (y += dy)]);
                  const ma = Math.trunc(ra / (np - n2)) >> 1;
                  const na = intRand(ma) + ma;
                  da += na;
                  ra -= na;
              }
              fb.setPolygon(pl, c);
          }
          fb.setPolygon([[10, 10], [100, 10], [10, 100]], 255);
          // fb.setPolygon([[640-10, 480-10], [640-100, 480-10], [640-100, 480-100], [640-10, 480-100]], 0x1c);
          fb.setPolygon([[640-10, 480-10], [640-100, 480-10], [640-10, 480-100]], 255);
          // fb.setPolygon([[640-10, 10], [10, 150], [640-150, 480-10]], 0x03);
          return fb;
      }
      function makeSample5() { return makeSampleGIF(makeSample5FB()); }

      function makeSampleA() {
          const fb1 = makeSample1FB();
          const fb2 = makeSample2FB();
          const fb3 = makeSample3FB();
          const fb4 = makeSample4FB();
          const fb5 = makeSample5FB();
          const gif = fb2.createGIF();
          gif.appendApplicationExtensionForLoop(0);
          gif.appendGraphicControlExtension(30);
          gif.appendDescriptor(fb1.createImageDescriptor(true));
          gif.appendGraphicControlExtension(30);
          gif.appendDescriptor(fb2.createImageDescriptor(true));
          gif.appendGraphicControlExtension(30);
          gif.appendDescriptor(fb3.createImageDescriptor(true));
          gif.appendGraphicControlExtension(30);
          gif.appendDescriptor(fb4.createImageDescriptor(true));
          gif.appendGraphicControlExtension(30);
          gif.appendDescriptor(fb5.createImageDescriptor(true));
          gif.appendTrailer();
          return gif;
      }

      function setSample(id, gif) {
          const tag = document.getElementById(id);
          if (tag == undefined) return;

          const img = createImgWithBase64(MyBase64.encode(gif().get_binary()));
          while (tag.firstChild)
              tag.removeChild(giftag.firstChild);
          tag.append(img);
      }

      function onLoad() {
          setSample('sample1', makeSample1);
          setSample('sample2', makeSample2);
          setSample('sample3', makeSample3);
          setSample('sample4', makeSample4);
          setSample('sample5', makeSample5);
          setSample('sampleA1', makeSampleA);
      }
      onLoad();

      /* ********************************** */

      // !-->
    </script>
  </body>
</html>
```

</div></details>