0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2022-01-08

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 詰合情報 <Packed Fields>
11 背景色番号 (Background Color Index)
12 ピクセル縦横比 (Pixel Aspect Ratio)
(13) (共通色表:存在する場合)
 (Global Color Table)

詰合情報 <Packed Fields>

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

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

計算式は次のとおり。

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)$ 個の色情報が必要です。

色番号 バイト位置
(共通色表)
+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)

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

先頭バイト値
(Descriptor)
後続バイト値
(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)

バイト位置
(画像記述子)
内容
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 詰合情報 <Packed Fields>
(10) (固有色表:存在する場合)
 (Local Color Table)
(?) 色番式画像 (Table Based Image Data)

詰合情報 <Packed Fields>

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

色番号 バイト位置
(固有色表)
+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)

バイト位置
(色番式画像)
内容 備考
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)

バイト位置
(副塊情報形式)
内容
$0$ 後続のバイト数 $N$
$1$ データ列($N$ バイト)
$N+2$ 後続のバイト数
$N+3$ データ列(続き)
... ...
... ...
... ...
- 後続のバイト数
- データ列(続き)
- 後続のバイト数 = 0

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

「89a」版による拡張

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

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

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

図形制御拡張 (Graphic Control Extension)

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

詰合情報 <Packed Fields>

ビット位置 ビット数 内容
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)

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

文字画面拡張 (Plain Text Extension)

バイト位置
(文字画面拡張)
内容
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)

バイト位置
(応用拡張)
内容
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)

バイト位置
(オフセット)
内容
0 終端子 (Trailer):0x3B

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

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

内容 値        
各コードのビット幅
(可変長データ)
3 〜 12
LMCS の最小値 2
初期化コード
<Clear Code>
$2^{LMCS}$
最初のコード $2^{LMCS} + 1$
コードの最大値 $2^{12} - 1$

LZW の圧縮と展開

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

圧縮と展開の動作を示すサンプル プログラム
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) + '\'');
実行結果
$ 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'

サンプル プログラム

HTML+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>
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?