画像の減色・誤差拡散・タイリング処理

昔の貧弱な表示装置を思い出しつつ、24ビットカラーを様々な形式に変換するものを作ってみました。（画像ビュワー作成も目的の１つです）

スクリーン ショットは元画像を基本8色(黒、青、赤、緑、紫、水色、黄色、白)の誤差拡散法で処理したものです。

![Sample.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/226677/8087fd36-ec84-e3ed-02dd-0700048ac146.jpeg)

<p class="codepen" data-height="600" data-default-tab="result" data-slug-hash="KKEREBG" data-user="ikiuo" style="height: 600px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;">
  <span>See the Pen <a href="https://codepen.io/ikiuo/pen/KKEREBG">
  低質画像生成</a> by Ikiuo (<a href="https://codepen.io/ikiuo">@ikiuo</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://cpwebassets.codepen.io/assets/embed/ei.js"></script>

# ソースコード

<details><summary>HTML+JavaScript (長いので折りたたみ)</summary><div>

```html:
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
    <title>低質画像生成</title>
    <style>
     html {
         background-color: #3f5f7f;
     }

     .bggray {
         background-color: lightgray;
     }
     .bgwhite {
         background-color: white;
     }

     .absolute {
         position: absolute;
     }
     .relative {
         position: relative;
         left: 0; top: 0;
     }
     .center {
         text-align: center;
     }
     .right {
         text-align: right;
     }
     .vtop {
         vertical-align: top;
     }
     .small {
         font-size: small;
     }
     .xsmall {
         font-size: x-small;
     }
     .xxsmall {
         font-size: xx-small;
     }

     .noframe {
         padding: 0;
         border-width: 0;
         border-collaspe: collaspe;
         border-spacing: 0;
         margin: 0;
     }
     .menuframe {
         background-color: white;
         left: 0; top: 0;
         z-index: 999;
     }
     .menuheader {
         cursor: default;
         font-wight: bold;
         font-size: small;
     }
     .menuitem {
         border-width: 0;
         cursor: default;
         font-size: small;
     }
     .menuitem:hover {
         background-color: #dfefff;
     }

     .altcolor {
         display: default;
     }

     .screen {
         padding: 0px;
         border: solid 1px black;
         /* border-width: 0px; */
         margin: 0px;
     }
    </style>
  </head>
  <body class="noframe">

    <table class="absolute noframe menuframe">
      <tr>
        <td class="noframe">
          <table id="tagMenu" class="noframe menuframe" width="100%" hidden>
            <tr><td id="menuRendering" class="menuitem">描画形式</td></tr>
            <tr><td id="menuRenderingAuto" class="menuitem">&nbsp;&nbsp;auto</td></tr>
            <tr><td id="menuRenderingSmooth" class="menuitem" hidden>&nbsp;&nbsp;smooth</td></tr>
            <tr><td id="menuRenderingHighQuality" class="menuitem" hidden>&nbsp;&nbsp;high-quality</td></tr>
            <tr><td id="menuRenderingCrispEdges" class="menuitem" hidden>&nbsp;&nbsp;crisp-edges</td></tr>
            <tr><td id="menuRenderingPixelated" class="menuitem">&nbsp;&nbsp;pixelated</td></tr>

            <tr><td><hr></td></tr>

            <tr><td id="menuOriginal" class="menuitem">元画像</td></tr>

            <tr><td><table>
              <tr>
                <td class="menuheader right">色成分</td>
                <td id="menuColorRed" class="menuitem">赤</td>
                <td id="menuColorGreen" class="menuitem">緑</td>
                <td id="menuColorBlue" class="menuitem">青</td>
              </tr>
              <tr>
                <td class="xsmall center" colspan="5">- 固定色変換 -</td>
              </tr>
              <tr>
                <td class="menuheader right">65536色</td>
                <td id="menuReduceColor65536" class="menuitem">単</td>
                <td id="menuErrDiffColor65536F" class="menuitem">散</td>
                <td id="menuTileColor65536F" class="menuitem">柄</td>
              </tr>
              <tr>
                <td class="menuheader right">32768色</td>
                <td id="menuReduceColor32768" class="menuitem">単</td>
                <td id="menuErrDiffColor32768F" class="menuitem">散</td>
                <td id="menuTileColor32768F" class="menuitem">柄</td>
              </tr>
              <tr>
                <td class="menuheader right">4096色</td>
                <td id="menuReduceColor4096" class="menuitem">単</td>
                <td id="menuErrDiffColor4096F" class="menuitem">散</td>
                <td id="menuTileColor4096F" class="menuitem">柄</td>
              </tr>
              <tr>
                <td class="menuheader right">512色</td>
                <td id="menuReduceColor512" class="menuitem">単</td>
                <td id="menuErrDiffColor512F" class="menuitem">散</td>
                <td id="menuTileColor512F" class="menuitem">柄</td>
              </tr>
              <tr>
                <td class="menuheader right">256色</td>
                <td id="menuReduceColor256" class="menuitem">単</td>
                <td id="menuErrDiffColor256F" class="menuitem">散</td>
                <td id="menuTileColor256F" class="menuitem">柄</td>
              </tr>
              <tr class="altcolor">
                <td class="menuheader right">216色</td>
                <td id="menuReduceColor216" class="menuitem">単</td>
                <td id="menuErrDiffColor216F" class="menuitem">散</td>
                <td id="menuTileColor216F" class="menuitem">柄</td>
              </tr>
              <tr class="altcolor">
                <td class="menuheader right">125色</td>
                <td id="menuReduceColor125" class="menuitem">単</td>
                <td id="menuErrDiffColor125F" class="menuitem">散</td>
                <td id="menuTileColor125F" class="menuitem">柄</td>
              </tr>
              <tr>
                <td class="menuheader right">64色</td>
                <td id="menuReduceColor64" class="menuitem">単</td>
                <td id="menuErrDiffColor64F" class="menuitem">散</td>
                <td id="menuTileColor64F" class="menuitem">柄</td>
              </tr>
              <tr class="altcolor">
                <td class="menuheader right">27色</td>
                <td id="menuReduceColor27" class="menuitem">単</td>
                <td id="menuErrDiffColor27F" class="menuitem">散</td>
                <td id="menuTileColor27F" class="menuitem">柄</td>
              </tr>
              <tr>
                <td class="menuheader right">16色</td>
                <td id="menuReduceColor16" class="menuitem">単</td>
                <td id="menuErrDiffColor16F" class="menuitem">散</td>
                <td id="menuTileColor16F" class="menuitem">柄</td>
              </tr>
              <tr>
                <td class="menuheader right">8色</td>
                <td id="menuReduceColor8" class="menuitem">単</td>
                <td id="menuErrDiffColor8" class="menuitem">散</td>
                <td id="menuTileColor8F" class="menuitem">柄</td>
              </tr>

              <tr>
                <td class="menuheader right">256階調</td>
                <td id="menuGrayScale256" class="menuitem">単</td>
                <!-- <td id="menuErrDiffGrayScale256" class="menuitem">散</td> -->
              </tr>
              <tr>
                <td class="menuheader right">64階調</td>
                <td id="menuGrayScale64" class="menuitem">単</td>
                <td id="menuErrDiffGrayScale64" class="menuitem">散</td>
                <td id="menuTileGrayScale64" class="menuitem">柄</td>
              </tr>
              <tr>
                <td class="menuheader right">16階調</td>
                <td id="menuGrayScale16" class="menuitem">単</td>
                <td id="menuErrDiffGrayScale16" class="menuitem">散</td>
                <td id="menuTileGrayScale16" class="menuitem">柄</td>
              </tr>
              <tr>
                <td class="menuheader right">8階調</td>
                <td id="menuGrayScale8" class="menuitem">単</td>
                <td id="menuErrDiffGrayScale8" class="menuitem">散</td>
                <td id="menuTileGrayScale8" class="menuitem">柄</td>
              </tr>
              <tr>
                <td class="menuheader right">4階調</td>
                <td id="menuGrayScale4" class="menuitem">単</td>
                <td id="menuErrDiffGrayScale4" class="menuitem">散</td>
                <td id="menuTileGrayScale4" class="menuitem">柄</td>
              </tr>
              <tr>
                <td class="menuheader right">白黒2色</td>
                <td id="menuColorWB" class="menuitem">単</td>
                <td id="menuErrDiffColorWB" class="menuitem">散</td>
                <td id="menuTileColorWB" class="menuitem">柄</td>
              </tr>

            </table></td></tr>

            <tr id="setGIF1" hidden><td><hr></td></tr>

            <tr id="setGIF2" hidden><td>
              <input id="menuCreateGIF" name="menuCreateGIF" type="checkbox">
              <label for="menuCreateGIF" class="small">GIF生成を抑制</label>
            </td></tr>

            <tr><td><hr></td></tr>

            <tr>
              <td class="small">
                <details><summary>キー操作</summary>
                  <div>
                    &nbsp;&nbsp;「9」: ウィンドウ合わせ<br>
                    &nbsp;&nbsp;「0」: 等倍<br>
                    &nbsp;&nbsp;「-」: 縮小(半分)<br>
                    &nbsp;&nbsp;「+」: 拡大(２倍)<br>
                    スクロール<br>
                    &nbsp;&nbsp;CTRL+マウス<br>
                    ズーム<br>
                    &nbsp;&nbsp;CTRL+ホイール<br>
                  </div>
                </details>
                <details>
                  <summary>説明</summary>
                  <div>
                    変換種類<br>
                    &nbsp;&nbsp;単：単純減色<br>
                    &nbsp;&nbsp;散：誤差拡散<br>
                    &nbsp;&nbsp;柄：市松模様<br>
                  </div>
                </details>
              </td>
            </tr>
          </table>
        </td>
      </tr>
    </table>

    <table class="absolute bgwhite noframe">
      <tr><td id="tagScreen" class="noframe bggray"></td></tr>
    </table>

    <script>

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

     /*
      * 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());
         }

         // データ URL を返す.
         toDataURL() {
             const b64s = this.S.encodeBase64(this.get_binary())
             return 'data:image/gif;charset=utf-8;base64,' + b64s;
         }

         // 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 ?? 49,

                 global_color_table: null,

                 set_color: (function(table) {
                     const flag = (table != null);
                     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 == null) ? 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: null,
                 table_based_image_data: S.createTableBasedImageData(data),

                 set_color_table: (function(table) {
                     const flag = (table != null);
                     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_flag ? 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]) == null)
                         break;
                     while (data != next.data)
                         if ((next = tree[next.next]) == null)
                             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 != null);
                     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 符号化.
         static encodeBase64(b) {
             const table = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
             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;
         }
     }

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

     class Vector {
         static add(a, b) { return a.map((v, i) => v + b[i]); }
         static sub(a, b) { return a.map((v, i) => v - b[i]); }
         static dot(a, b) { return a.reduce((a, v, i) => a + v * b[i], 0); }
         static mul(v, m) { return v.map((v) => v * m); }
         static div(v, m) { return v.map((v) => v / m); }

         static length(v) { return Math.sqrt(Vector.dot(v, v)); }
         static length2(s, e) { return Vector.length(Vector.sub(e, s)); }
         static unit(v) { return Vector.div(v, Vector.length(v)); }
         static unit2(s, e) { return Vector.unit(Vector.sub(e, s)); }
         static zoom(p, c, s) { return Vector.add(Vector.mul(Vector.sub(p, c), s), c); }

         static op_as = [Vector.add, Vector.sub];
         static op_asm = [Vector.add, Vector.sub, Vector.mul];
         static op_asd = [Vector.add, Vector.sub, Vector.div];
         static op_amd = [Vector.add, Vector.mul, Vector.div];
         static op_smd = [Vector.sub, Vector.mul, Vector.div];
         static op_asmd = [Vector.add, Vector.sub, Vector.mul, Vector.div];
     }

     function clamp(min, val, max) {
         return Math.max(min, Math.min(val, max));
     }

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

     /*
      * 成分色化
      */

     function createColorPlane(rgba, index, width, height) {
         const len = rgba.length;
         const out = new Uint8Array(len >> 2);

         for (let ip = index, op = 0; ip < len; ip +=4, op++)
             out[op] = rgba[ip];

         const clut = [...Array(256)].map((_, i) => [
             index == 0 ? i : 0,
             index == 1 ? i : 0,
             index == 2 ? i : 0,
         ]);

         return [clut, out, width, height];
     }
     function createColorRed(rgba, width, heigh) {
         return createColorPlane(rgba, 0, width, heigh);
     }
     function createColorGreen(rgba, width, heigh) {
         return createColorPlane(rgba, 1, width, heigh);
     }
     function createColorBlue(rgba, width, heigh) {
         return createColorPlane(rgba, 2, width, heigh);
     }

     /*
      * インデックス・カラー値変換表生成
      */

     function makeColorToIndex(level) {
         const l = level - 1;
         return new Uint8Array([...Array(256)]
             .map((_, v) => Math.trunc(v * l / 255)));
     }

     function makeColorToIndexR(level) {
         const l = level - 1;
         return new Uint8Array([...Array(256)]
             .map((_, v) => clamp(0, Math.round(v * l / 255), 255)));
     }

     function makeIndexToColor(level) {
         const l = level - 1;
         return new Uint8Array([...Array(level)]
             .map((_, v) => Math.trunc(v * 255 / l)));
     }

     function makeColorIndex(level) {
         return [
             makeIndexToColor(level),
             makeColorToIndex(level),
         ];
     }

     function makeColorIndexR(level) {
         return [
             makeIndexToColor(level),
             makeColorToIndexR(level),
         ];
     }

     function makeColorToColor(level) {
         const [ctab, btab] = makeColorIndex(level);
         return btab.map((v) => ctab[v]);
     }

     function makeColorToColorR(level) {
         const [ctab, btab] = makeColorIndexR(level);
         return btab.map((v) => ctab[v]);
     }

     function makeGrayScaleClut(ctab) {
         return [...ctab].map((v) => [v, v, v]);
     }

     function makeClut(table) {
         const [rt, gt, bt] = table;
         const [rn, gn, bn] = table.map((v) => v.length);
         const [cm, rm, gm, bm] = [rn * gn * bn, gn * bn, bn, 1];
         return [...Array(cm)].map((_, i) => [
             rt[Math.trunc(i / rm) % rn],
             gt[Math.trunc(i / gm) % gn],
             bt[Math.trunc(i / bm) % bn],
         ]);
     }

     /*
      * 色選択
      */

     const SQRT3 = Math.sqrt(3);
     const ISQRT3 = 1 / SQRT3;

     function rgbToKey(r, g, b) {
         return (r << 16) | (g << 8) | b;
     }

     function colorInfo(r, g, b) {
         const rf = r / 255;
         const gf = g / 255;
         const bf = b / 255;
         const lf = Math.sqrt(rf * rf + gf * gf + bf * bf);
         return {
             key: rgbToKey(r, g, b),
             rgb: [r, g, b],
             vec: [rf, gf, bf],
             unit: (lf == 0
                  ? [ISQRT3, ISQRT3, ISQRT3]
                  : [rf / lf, gf / lf, bf / lf]),
             count: 0,
             index: null,
         }
     }

     function colorDistance(p, q) {
         const [pr, pg, pb] = p.vec;
         const [qr, qg, qb] = q.vec;
         const dr = pr - qr;
         const dg = pg - qg;
         const db = pb - qb;
         const cl = dr * dr + dg * dg + db * db;
         const [px, py, pz] = p.unit;
         const [qx, qy, qz] = q.unit;
         const cc = px * qx + py * qy + pz * qz;
         return cl * (2.0 - Math.pow(cc, 0.25));
     }

     function allColorTable(rgba) {
         const len = rgba.length;
         const color = new Map();
         const ckey = new Array(len >> 2);

         for (let p = 0, k = 0; p < len; p += 4, k++) {
             const r = rgba[p + 0];
             const g = rgba[p + 1];
             const b = rgba[p + 2];
             const d = rgbToKey(r, g, b);
             const i = color.get(d);
             if (i == null) {
                 const n = colorInfo(r, g, b);
                 color.set(d, n);
                 ckey[k] = n;
             } else {
                 i.count += 1;
                 ckey[k] = i;
             }
         }
         return [color, ckey];
     }

     function filterColor(color, reserved) {
         for (const rgb of reserved)
             color.delete(rgbToKey(...rgb));
         return color;
     }

     function sortColorTable(color) {
         const table = Array();
         for (const item of color.entries())
             table.push(item);
         table.sort((a, b) => clamp(-1, (b.count - a.count), +1));
         return table;
     }

     function selectColor(rgba, width, height, color) {
         const tcol = color.map((c) => colorInfo(...c));
         const [pcol, ckey] = allColorTable(rgba);

         const tcnt = tcol.length;
         for (const [k, p] of pcol) {
             let dp = 0;
             let dmin = colorDistance(p, tcol[0]);
             for (let tp = 1; tp < tcnt; tp++) {
                 const dlen = colorDistance(p, tcol[tp]);
                 if (dmin > dlen) {
                     dp = tp;
                     dmin = dlen;
                 }
             }
             p.index = dp;
         }

         const clen = ckey.length;
         const cindex = Array(clen);
         for (let cp = 0; cp < clen; cp++)
             cindex[cp] = ckey[cp].index;
         return cindex;
     }

     /*
      * 減色
      */

     function reduceColorTableI(rgba, width, height, table) {
         const len = rgba.length;
         const out = new Uint8ClampedArray(len >> 2);

         const [[rct, rbt], [gct, gbt], [bct, bbt]] = table;
         const [rn, gn, bn] = [rct.length, gct.length, bct.length];
         const [cm, rm, gm, bm] = [rn * gn * bn, gn * bn, bn, 1];

         let op = 0;
         for (let cp = 0; cp < len; cp += 4) {
             const r = rgba[cp + 0];
             const g = rgba[cp + 1];
             const b = rgba[cp + 2];
             const a = rgba[cp + 3];

             const ri = rbt[r];
             const gi = gbt[g];
             const bi = bbt[b];

             out[op++] = ri * rm + gm * gi + bi;
         }

         const clut = [...Array(cm)].map((_, i) => [
             rct[Math.trunc(i / rm) % rn],
             gct[Math.trunc(i / gm) % gn],
             bct[Math.trunc(i / bm) % bn],
         ]);
         return [clut, out, width, height];
     }

     function reduceColorLevelRI(rgba, width, height, level) {
         const tab = makeColorIndexR(level);
         return reduceColorTableI(rgba, width, height, [tab, tab, tab]);
     }

     function reduceColorLevelR(rgba, width, height, level) {
         const len = rgba.length;
         const out = new Uint8ClampedArray(len);
         const rtab = makeColorToColorR(level);
         for (let cp = 0; cp < len; cp += 4) {
             const r = rgba[cp + 0];
             const g = rgba[cp + 1];
             const b = rgba[cp + 2];
             const a = rgba[cp + 3];

             out[cp + 0] = rtab[r];
             out[cp + 1] = rtab[g];
             out[cp + 2] = rtab[b];
             out[cp + 3] = a;
         }
         return [null, out, width, height];
     }

     function reduceColorClutN(rgba, width, height, clut) {
         const out = selectColor(rgba, width, height, clut);
         return [clut, out, width, height];
     }

     function reduceColorTableNI(rgba, width, height, table) {
         return reduceColorClutN(rgba, width, height, makeClut(table));
     }

     function reduceColorLevelNI(rgba, width, height, level) {
         const ctab = makeIndexToColor(level);
         const table = [ctab, ctab, ctab];
         return reduceColorTableNI(rgba, width, height, table);
     }

     /*
      * 簡易減色
      */

     const Color16Clut = [
         [0x00, 0x00, 0x00],
         [0x7f, 0x00, 0x00],
         [0x00, 0x7f, 0x00],
         [0x7f, 0x7f, 0x00],
         [0x00, 0x00, 0x7f],
         [0x7f, 0x00, 0x7f],
         [0x00, 0x7f, 0x7f],
         [0x7f, 0x7f, 0x7f],

         [0x00, 0x00, 0x00],
         [0xff, 0x00, 0x00],
         [0x00, 0xff, 0x00],
         [0xff, 0xff, 0x00],
         [0x00, 0x00, 0xff],
         [0xff, 0x00, 0xff],
         [0x00, 0xff, 0xff],
         [0xff, 0xff, 0xff],
     ];

     const Color16ClutIndex = [
         0+0, // 0:0:0
         0+1, // 1:0:0
         8+1, // 2:0:0
         0,

         0+2, // 0:1:0
         0+3, // 1:1:0
         8+1, // 2:1:0
         0,

         8+2, // 0:2:0
         8+2, // 1:2:0
         8+3, // 2:2:0
         0,

         0, 0, 0, 0,

         0+4, // 0:0:1
         0+5, // 1:0:1
         8+1, // 2:0:1
         0,

         0+6, // 0:1:1
         0+7, // 1:1:1
         8+1, // 2:1:1
         0,

         8+2, // 0:2:1
         8+2, // 1:2:1
         8+3, // 2:2:1
         0,

         0, 0, 0, 0,

         8+4, // 0:0:2
         8+4, // 1:0:2
         8+5, // 2:0:2
         0,

         8+4, // 0:1:2
         8+4, // 1:1:2
         8+5, // 2:1:2
         0,

         8+6, // 0:2:2
         8+6, // 1:2:2
         8+7, // 2:2:2
         0,

         0, 0, 0, 0,
     ];

     function reduceColor8(rgba, width, height) {
         return reduceColorLevelRI(rgba, width, height, 2);
     }

     function reduceColor16(rgba, width, height) {
         const len = rgba.length;
         const out = new Uint8Array(len >> 2);
         const index = Color16ClutIndex;

         for (let ip = 0, op = 0; ip < len; ip +=4, op++) {
             const r = rgba[ip + 0];
             const g = rgba[ip + 1];
             const b = rgba[ip + 2];

             const ri = (r < 64) ? 0 : (r < 192) ? 0x01 : 0x02;
             const gi = (g < 64) ? 0 : (g < 192) ? 0x04 : 0x08;
             const bi = (b < 64) ? 0 : (b < 192) ? 0x10 : 0x20;

             out[op] = index[ri | gi | bi];
         }
         return [Color16Clut, out, width, height];
     }

     function reduceColor27(rgba, width, height) {
         return reduceColorLevelRI(rgba, width, height, 3);
     }

     function reduceColor64(rgba, width, height) {
         return reduceColorLevelRI(rgba, width, height, 4);
     }

     function reduceColor125(rgba, width, height) {
         return reduceColorLevelRI(rgba, width, height, 5);
     }

     function reduceColor216(rgba, width, height) {
         return reduceColorLevelRI(rgba, width, height, 6);
     }

     function reduceColor256(rgba, width, height) {
         const ctab8 = makeColorIndexR(8);
         const ctab4 = makeColorIndexR(4);
         const table = [ctab8, ctab8, ctab4];
         return reduceColorTableI(rgba, width, height, table);
     }

     function reduceColor512(rgba, width, height) {
         return reduceColorLevelR(rgba, width, height, 8);
     }

     function reduceColor4096(rgba, width, height) {
         return reduceColorLevelR(rgba, width, height, 16);
     }

     function reduceColor32768(rgba, width, height) {
         return reduceColorLevelR(rgba, width, height, 32);
     }

     function reduceColor65536(rgba, width, height, bits) {
         const len = rgba.length;
         const out = new Uint8ClampedArray(len);

         const [ctab, btab] = makeColorIndex(1 << 6);

         let op = 0;
         for (let cp = 0; cp < len; cp += 4) {
             const r = rgba[cp + 0];
             const g = rgba[cp + 1];
             const b = rgba[cp + 2];
             const a = rgba[cp + 3];

             const ri = btab[r];
             const gi = btab[g];
             const bi = btab[b];
             const rm = Math.max(ri, gi, bi) < 32 ? 0x1f : 0x3e;

             const rc = ctab[ri & rm];
             const gc = ctab[gi & rm];
             const bc = ctab[bi & rm];

             out[cp + 0] = rc;
             out[cp + 1] = gc;
             out[cp + 2] = bc;
             out[cp + 3] = a;
         }

         return [null, out, width, height];
     }

     /*
      * 近傍色
      */

     function nearestColorLevel(rgba, width, height, level) {
         const tab = makeIndexToColor(level);
         const color = makeClut([tab, tab, tab]);
         const index = selectColor(rgba, width, height, color);
         return [color, index, width, height];
     }

     function toNearestColor8Fixed(rgba, width, height) {
         return nearestColorLevel(rgba, width, height, 2);
     }

     /*
      * 白黒(グレースケール)
      */

     function toGrayScale(r, g, b) {
         return (g >>> 1) + (r >>> 2) + (r >>> 3) + (b >>> 3);
     }

     /*
      * 白黒
      */

     function toGrayScaleLevel(rgba, width, height, level) {
         const len = rgba.length;
         const out = new Uint8Array(len >> 2);
         const [ctab, btab] = makeColorIndexR(level);

         for (let ip = 0, op = 0; ip < len; ip +=4, op++) {
             const r = rgba[ip + 0];
             const g = rgba[ip + 1];
             const b = rgba[ip + 2];
             out[op] = btab[toGrayScale(r, g, b)];
         }

         const clut = [...Array(level)].map(
             (_, i) => [ctab[i], ctab[i], ctab[i]]);
         return [clut, out, width, height];
     }

     function toColorWB(rgba, width, height) {
         return toGrayScaleLevel(rgba, width, height, 2);
     }

     function toGrayScale4(rgba, width, height) {
         return toGrayScaleLevel(rgba, width, height, 4);
     }

     function toGrayScale8(rgba, width, height) {
         return toGrayScaleLevel(rgba, width, height, 8);
     }

     function toGrayScale16(rgba, width, height) {
         return toGrayScaleLevel(rgba, width, height, 16);
     }

     function toGrayScale64(rgba, width, height) {
         return toGrayScaleLevel(rgba, width, height, 64);
     }

     function toGrayScale256(rgba, width, height) {
         return toGrayScaleLevel(rgba, width, height, 256);
     }

     /*
      *
      */

     function setGrayScaleError(l, lc, d1, d2, d3, xp) {
         const ld = l - lc;
         const l2 = ld >>> 2;
         const l3 = ld >>> 3;
         const l4 = ld >>> 4;
         const lx = ld - (l2 << 1)  - (l3 << 1) - (l4 << 2);

         //

         d1[xp + 4 * 1 + 0] += l2;
         d1[xp + 4 * 2 + 0] += l4;

         //

         d2[xp - 4 * 2 + 0] += l4;
         d2[xp - 4 * 1 + 0] += l3;
         d2[xp + 4 * 0 + 0] += l2;
         d2[xp + 4 * 1 + 0] += l3;
         d2[xp + 4 * 2 + 0] += l4;

         //

         d3[xp - 4 * 1 + 0] += lx;
         d3[xp + 4 * 0 + 0] += l4;
     }

     /*
      * 誤差拡散白黒(N階調)
      */

     function toErrDiffGrayScaleLevel(rgba, width, height, level) {
         const len = rgba.length;
         const out = new Uint8Array(len >> 2);
         const lbsz = (width + 16) << 2;

         const [ctab, btab] = makeColorIndex(level);

         let cp = 0, op = 0;
         let dl1 = new Uint16Array(lbsz);
         let dl2 = new Uint16Array(lbsz);
         let dl3 = new Uint16Array(lbsz);

         for (let y = 0; y < 3; y++) {
             const d1 = dl1;
             const d2 = dl2;
             const d3 = dl3;
             d3.fill(0);

             cp = 0;
             for (let x = 0, xp = 4 * 4; x < width; x++, cp += 4, xp += 4) {
                 const r = rgba[cp + 0];
                 const g = rgba[cp + 1];
                 const b = rgba[cp + 2];

                 const w = toGrayScale(r, g, b);
                 const l = w + d1[xp];
                 const li = btab[Math.min(255, l)];

                 const lc = ctab[li];
                 setGrayScaleError(l, lc, d1, d2, d3, xp);
             }

             dl1 = d2;
             dl2 = d3;
             dl3 = d1;
         }

         cp = 0;
         for (let y = 0; y < height; y++) {
             const d1 = dl1;
             const d2 = dl2;
             const d3 = dl3;
             d3.fill(0);

             for (let x = 0, xp = 1 * 4; x < 3; x++, xp += 4) {
                 const r = rgba[cp + 0];
                 const g = rgba[cp + 1];
                 const b = rgba[cp + 2];

                 const w = toGrayScale(r, g, b);
                 const l = w + d1[xp];
                 const li = btab[Math.min(255, l)];

                 const lc = ctab[li];
                 setGrayScaleError(l, lc, d1, d2, d3, xp);
             }

             for (let x = 0, xp = 4 * 4; x < width; x++, cp += 4, xp += 4) {
                 const r = rgba[cp + 0];
                 const g = rgba[cp + 1];
                 const b = rgba[cp + 2];

                 const w = toGrayScale(r, g, b);
                 const l = w + d1[xp];
                 const li = btab[Math.min(255, l)];

                 out[op++] = li;

                 const lc = ctab[li];
                 setGrayScaleError(l, lc, d1, d2, d3, xp);
             }

             for (let x = 0, xp = (width + 4) * 4; x < 3; x++, xp += 4) {
                 const r = rgba[cp + 0 - 4];
                 const g = rgba[cp + 1 - 4];
                 const b = rgba[cp + 2 - 4];

                 const w = toGrayScale(r, g, b);
                 const l = w + d1[xp];
                 const li = btab[Math.min(255, l)];

                 const lc = ctab[li];
                 setGrayScaleError(l, lc, d1, d2, d3, xp);
             }

             dl1 = d2;
             dl2 = d3;
             dl3 = d1;
         }

         const clut = makeGrayScaleClut(ctab);
         return [clut, out, width, height];
     }

     function toErrDiffColorWB(rgba, width, height) {
         return toErrDiffGrayScaleLevel(rgba, width, height, 2);
     }

     function toErrDiffGrayScale4(rgba, width, height) {
         return toErrDiffGrayScaleLevel(rgba, width, height, 4);
     }

     function toErrDiffGrayScale8(rgba, width, height) {
         return toErrDiffGrayScaleLevel(rgba, width, height, 8);
     }

     function toErrDiffGrayScale16(rgba, width, height) {
         return toErrDiffGrayScaleLevel(rgba, width, height, 16);
     }

     function toErrDiffGrayScale64(rgba, width, height) {
         return toErrDiffGrayScaleLevel(rgba, width, height, 64);
     }

     function toErrDiffGrayScale256(rgba, width, height) {
         return toErrDiffGrayScaleLevel(rgba, width, height, 256);
     }

     /*
      * 誤差拡散
      */

     function setColorError(r, g, b, rc, gc, bc, d1, d2, d3, xp) {
         const rd = r - rc;
         const gd = g - gc;
         const bd = b - bc;

         const r2 = rd >>> 2;
         const g2 = gd >>> 2;
         const b2 = bd >>> 2;

         const r3 = rd >>> 3;
         const g3 = gd >>> 3;
         const b3 = bd >>> 3;

         const r4 = rd >>> 4;
         const g4 = gd >>> 4;
         const b4 = bd >>> 4;

         const rx = rd - (r2 << 1)  - (r3 << 1) - (r4 << 2);
         const gx = gd - (g2 << 1)  - (g3 << 1) - (g4 << 2);
         const bx = bd - (b2 << 1)  - (b3 << 1) - (b4 << 2);

         //

         d1[xp + 4 * 1 + 0] += r2;
         d1[xp + 4 * 1 + 1] += g2;
         d1[xp + 4 * 1 + 2] += b2;

         d1[xp + 4 * 2 + 0] += r4;
         d1[xp + 4 * 2 + 1] += g4;
         d1[xp + 4 * 2 + 2] += b4;

         //

         d2[xp - 4 * 2 + 0] += r4;
         d2[xp - 4 * 2 + 1] += g4;
         d2[xp - 4 * 2 + 2] += b4;

         d2[xp - 4 * 1 + 0] += r3;
         d2[xp - 4 * 1 + 1] += g3;
         d2[xp - 4 * 1 + 2] += b3;

         d2[xp + 4 * 0 + 0] += r2;
         d2[xp + 4 * 0 + 1] += g2;
         d2[xp + 4 * 0 + 2] += b2;

         d2[xp + 4 * 1 + 0] += r3;
         d2[xp + 4 * 1 + 1] += g3;
         d2[xp + 4 * 1 + 2] += b3;

         d2[xp + 4 * 2 + 0] += r4;
         d2[xp + 4 * 2 + 1] += g4;
         d2[xp + 4 * 2 + 2] += b4;

         //

         d3[xp - 4 * 1 + 0] += rx;
         d3[xp - 4 * 1 + 1] += gx;
         d3[xp - 4 * 1 + 2] += bx;

         d3[xp + 4 * 0 + 0] += r4;
         d3[xp + 4 * 0 + 1] += g4;
         d3[xp + 4 * 0 + 2] += b4;
     }

     function toErrDiffColorTable(rgba, width, height, table) {
         const len = rgba.length;
         const out = new Uint8Array(len >> 2);
         const lbsz = (width + 16) << 2;

         const [[rct, rbt], [gct, gbt], [bct, bbt]] = table;
         const [rn, gn, bn] = [rct.length, gct.length, bct.length];
         const [cm, rm, gm, bm] = [rn * gn * bn, gn * bn, bn, 1];

         let cp = 0, op = 0;
         let dl1 = new Uint16Array(lbsz);
         let dl2 = new Uint16Array(lbsz);
         let dl3 = new Uint16Array(lbsz);

         for (let y = 0; y < 3; y++) {
             const d1 = dl1;
             const d2 = dl2;
             const d3 = dl3;
             d3.fill(0);

             cp = 0;
             for (let x = 0, xp = 4 * 4; x < width; x++, cp += 4, xp += 4) {
                 const r = rgba[cp + 0] + d1[xp + 0];
                 const g = rgba[cp + 1] + d1[xp + 1];
                 const b = rgba[cp + 2] + d1[xp + 2];

                 const ri = rbt[Math.min(255, r)];
                 const gi = rbt[Math.min(255, g)];
                 const bi = rbt[Math.min(255, b)];

                 const rc = rct[ri];
                 const gc = gct[gi];
                 const bc = bct[bi];

                 setColorError(r, g, b, rc, gc, bc, d1, d2, d3, xp);
             }

             dl1 = d2;
             dl2 = d3;
             dl3 = d1;
         }

         cp = 0;
         for (let y = 0; y < height; y++) {
             const d1 = dl1;
             const d2 = dl2;
             const d3 = dl3;
             d3.fill(0);

             for (let x = 0, xp = 1 * 4; x < 3; x++, xp += 4) {
                 const r = rgba[cp + 0] + d1[xp + 0];
                 const g = rgba[cp + 1] + d1[xp + 1];
                 const b = rgba[cp + 2] + d1[xp + 2];

                 const ri = rbt[Math.min(255, r)];
                 const gi = rbt[Math.min(255, g)];
                 const bi = rbt[Math.min(255, b)];

                 const rc = rct[ri];
                 const gc = gct[gi];
                 const bc = bct[bi];

                 setColorError(r, g, b, rc, gc, bc, d1, d2, d3, xp);
             }

             for (let x = 0, xp = 4 * 4; x < width; x++, cp += 4, xp += 4) {
                 const r = rgba[cp + 0] + d1[xp + 0];
                 const g = rgba[cp + 1] + d1[xp + 1];
                 const b = rgba[cp + 2] + d1[xp + 2];

                 const ri = rbt[Math.min(255, r)];
                 const gi = rbt[Math.min(255, g)];
                 const bi = rbt[Math.min(255, b)];

                 out[op++] = ri * rm + gm * gi + bi;

                 const rc = rct[ri];
                 const gc = gct[gi];
                 const bc = bct[bi];

                 setColorError(r, g, b, rc, gc, bc, d1, d2, d3, xp);
             }

             for (let x = 0, xp = (width + 4) * 4; x < 3; x++, xp += 4) {
                 const r = rgba[cp + 0 - 4] + d1[xp + 0];
                 const g = rgba[cp + 1 - 4] + d1[xp + 1];
                 const b = rgba[cp + 2 - 4] + d1[xp + 2];

                 const ri = rbt[Math.min(255, r)];
                 const gi = rbt[Math.min(255, g)];
                 const bi = rbt[Math.min(255, b)];

                 const rc = rct[ri];
                 const gc = gct[gi];
                 const bc = bct[bi];

                 setColorError(r, g, b, rc, gc, bc, d1, d2, d3, xp);
             }

             dl1 = d2;
             dl2 = d3;
             dl3 = d1;
         }

         const clut = [...Array(cm)].map((_, i) => [
             rct[Math.trunc(i / rm) % rn],
             gct[Math.trunc(i / gm) % gn],
             bct[Math.trunc(i / bm) % bn],
         ]);
         return [clut, out, width, height];
     }

     function toErrDiffColor8(rgba, width, height) {
         const ctab = [0x00, 0xff];
         const btab = [...Array(256)].map((_, i) => (i >= 255) ? 1 : 0);
         const tab = [ctab, btab];
         return toErrDiffColorTable(rgba, width, height, [tab, tab, tab]);
     }

     function toErrDiffColor16Fixed(rgba, width, height) {
         const len = rgba.length;
         const out = new Uint8Array(len >> 2);
         const lbsz = (width + 16) << 2;

         const clut = Color16Clut;
         const index = Color16ClutIndex;

         let cp = 0, op = 0;
         let dl1 = new Uint16Array(lbsz);
         let dl2 = new Uint16Array(lbsz);
         let dl3 = new Uint16Array(lbsz);

         for (let y = 0; y < 3; y++) {
             const d1 = dl1;
             const d2 = dl2;
             const d3 = dl3;
             d3.fill(0);

             cp = 0;
             for (let x = 0, xp = 4 * 4; x < width; x++, cp += 4, xp += 4) {
                 const r = rgba[cp + 0] + d1[xp + 0];
                 const g = rgba[cp + 1] + d1[xp + 1];
                 const b = rgba[cp + 2] + d1[xp + 2];

                 const ri = (r < 223) ? 0 : (r < 255) ? 0x01 : 0x02;
                 const gi = (g < 223) ? 0 : (g < 255) ? 0x04 : 0x08;
                 const bi = (b < 223) ? 0 : (b < 255) ? 0x10 : 0x20;
                 const ci = index[ri | gi | bi];

                 const color = clut[ci];
                 const rc = color[0];
                 const gc = color[1];
                 const bc = color[2];

                 setColorError(r, g, b, rc, gc, bc, d1, d2, d3, xp);
             }

             dl1 = d2;
             dl2 = d3;
             dl3 = d1;
         }

         cp = 0;
         for (let y = 0; y < height; y++) {
             const d1 = dl1;
             const d2 = dl2;
             const d3 = dl3;
             d3.fill(0);

             for (let x = 0, xp = 1 * 4; x < 3; x++, xp += 4) {
                 const r = rgba[cp + 0] + d1[xp + 0];
                 const g = rgba[cp + 1] + d1[xp + 1];
                 const b = rgba[cp + 2] + d1[xp + 2];

                 const ri = (r < 223) ? 0 : (r < 255) ? 0x01 : 0x02;
                 const gi = (g < 223) ? 0 : (g < 255) ? 0x04 : 0x08;
                 const bi = (b < 223) ? 0 : (b < 255) ? 0x10 : 0x20;
                 const ci = index[ri | gi | bi];

                 const color = clut[ci];
                 const rc = color[0];
                 const gc = color[1];
                 const bc = color[2];

                 setColorError(r, g, b, rc, gc, bc, d1, d2, d3, xp);
             }

             for (let x = 0, xp = 4 * 4; x < width; x++, cp += 4, xp += 4) {
                 const r = rgba[cp + 0] + d1[xp + 0];
                 const g = rgba[cp + 1] + d1[xp + 1];
                 const b = rgba[cp + 2] + d1[xp + 2];

                 const ri = (r < 223) ? 0 : (r < 255) ? 0x01 : 0x02;
                 const gi = (g < 223) ? 0 : (g < 255) ? 0x04 : 0x08;
                 const bi = (b < 223) ? 0 : (b < 255) ? 0x10 : 0x20;
                 const ci = index[ri | gi | bi];

                 out[op++] = ci;

                 const color = clut[ci];
                 const rc = color[0];
                 const gc = color[1];
                 const bc = color[2];

                 setColorError(r, g, b, rc, gc, bc, d1, d2, d3, xp);
             }

             for (let x = 0, xp = (width + 4) * 4; x < 3; x++, xp += 4) {
                 const r = rgba[cp + 0 - 4] + d1[xp + 0];
                 const g = rgba[cp + 1 - 4] + d1[xp + 1];
                 const b = rgba[cp + 2 - 4] + d1[xp + 2];

                 const ri = (r < 223) ? 0 : (r < 255) ? 0x01 : 0x02;
                 const gi = (g < 223) ? 0 : (g < 255) ? 0x04 : 0x08;
                 const bi = (b < 223) ? 0 : (b < 255) ? 0x10 : 0x20;
                 const ci = index[ri | gi | bi];

                 const color = clut[ci];
                 const rc = color[0];
                 const gc = color[1];
                 const bc = color[2];

                 setColorError(r, g, b, rc, gc, bc, d1, d2, d3, xp);
             }

             dl1 = d2;
             dl2 = d3;
             dl3 = d1;
         }

         return [clut, out, width, height];
     }

     function toErrDiffColor27Fixed(rgba, width, height) {
         const tab = makeColorIndex(3);
         return toErrDiffColorTable(rgba, width, height, [tab, tab, tab]);
     }

     function toErrDiffColor64Fixed(rgba, width, height) {
         const tab = makeColorIndex(4);
         return toErrDiffColorTable(rgba, width, height, [tab, tab, tab]);
     }

     function toErrDiffColor125Fixed(rgba, width, height) {
         const tab = makeColorIndex(5);
         return toErrDiffColorTable(rgba, width, height, [tab, tab, tab]);
     }

     function toErrDiffColor216Fixed(rgba, width, height) {
         const tab = makeColorIndex(6);
         return toErrDiffColorTable(rgba, width, height, [tab, tab, tab]);
     }

     function toErrDiffColor256Fixed(rgba, width, height) {
         const len = rgba.length;
         const out = new Uint8Array(len >> 2);
         const lbsz = (width + 16) << 2;

         const [ctab1, btab1] = makeColorIndex(8);
         const [ctab2, btab2] = makeColorIndex(4);

         let cp = 0, op = 0;
         let dl1 = new Uint16Array(lbsz);
         let dl2 = new Uint16Array(lbsz);
         let dl3 = new Uint16Array(lbsz);

         for (let y = 0; y < 3; y++) {
             const d1 = dl1;
             const d2 = dl2;
             const d3 = dl3;
             d3.fill(0);

             cp = 0;
             for (let x = 0, xp = 4 * 4; x < width; x++, cp += 4, xp += 4) {
                 const r = rgba[cp + 0] + d1[xp + 0];
                 const g = rgba[cp + 1] + d1[xp + 1];
                 const b = rgba[cp + 2] + d1[xp + 2];

                 const ri = btab1[Math.min(255, r)];
                 const gi = btab1[Math.min(255, g)];
                 const bi = btab2[Math.min(255, b)];

                 const rc = ctab1[ri];
                 const gc = ctab1[gi];
                 const bc = ctab2[bi];

                 setColorError(r, g, b, rc, gc, bc, d1, d2, d3, xp);
             }

             dl1 = d2;
             dl2 = d3;
             dl3 = d1;
         }

         cp = 0;
         for (let y = 0; y < height; y++) {
             const d1 = dl1;
             const d2 = dl2;
             const d3 = dl3;
             d3.fill(0);

             for (let x = 0, xp = 1 * 4; x < 3; x++, xp += 4) {
                 const r = rgba[cp + 0] + d1[xp + 0];
                 const g = rgba[cp + 1] + d1[xp + 1];
                 const b = rgba[cp + 2] + d1[xp + 2];

                 const ri = btab1[Math.min(255, r)];
                 const gi = btab1[Math.min(255, g)];
                 const bi = btab2[Math.min(255, b)];

                 const rc = ctab1[ri];
                 const gc = ctab1[gi];
                 const bc = ctab2[bi];

                 setColorError(r, g, b, rc, gc, bc, d1, d2, d3, xp);
             }

             for (let x = 0, xp = 4 * 4; x < width; x++, cp += 4, xp += 4) {
                 const r = rgba[cp + 0] + d1[xp + 0];
                 const g = rgba[cp + 1] + d1[xp + 1];
                 const b = rgba[cp + 2] + d1[xp + 2];

                 const ri = btab1[Math.min(255, r)];
                 const gi = btab1[Math.min(255, g)];
                 const bi = btab2[Math.min(255, b)];

                 out[op++] = (ri << 5) | (gi << 2) | bi;

                 const rc = ctab1[ri];
                 const gc = ctab1[gi];
                 const bc = ctab2[bi];

                 setColorError(r, g, b, rc, gc, bc, d1, d2, d3, xp);
             }

             for (let x = 0, xp = (width + 4) * 4; x < 3; x++, xp += 4) {
                 const r = rgba[cp + 0 - 4] + d1[xp + 0];
                 const g = rgba[cp + 1 - 4] + d1[xp + 1];
                 const b = rgba[cp + 2 - 4] + d1[xp + 2];

                 const ri = btab1[Math.min(255, r)];
                 const gi = btab1[Math.min(255, g)];
                 const bi = btab2[Math.min(255, b)];

                 const rc = ctab1[ri];
                 const gc = ctab1[gi];
                 const bc = ctab2[bi];

                 setColorError(r, g, b, rc, gc, bc, d1, d2, d3, xp);
             }

             dl1 = d2;
             dl2 = d3;
             dl3 = d1;
         }

         const clut = [...Array(256)].map((_, i) => [
             ctab1[(i >> 5) & 7],
             ctab1[(i >> 2) & 7],
             ctab2[(i >> 0) & 3],
         ]);
         return [clut, out, width, height];
     }

     function toErrDiffColorFixed(rgba, width, height, level) {
         const len = rgba.length;
         const out = new Uint8ClampedArray(len);
         const lbsz = (width + 16) << 2;

         const ctab = makeColorToColor(level);

         let cp = 0;
         let dl1 = new Uint16Array(lbsz);
         let dl2 = new Uint16Array(lbsz);
         let dl3 = new Uint16Array(lbsz);

         for (let y = 0; y < 3; y++) {
             const d1 = dl1;
             const d2 = dl2;
             const d3 = dl3;
             d3.fill(0);

             cp = 0;
             for (let x = 0, xp = 4 * 4; x < width; x++, cp += 4, xp += 4) {
                 const r = rgba[cp + 0] + d1[xp + 0];
                 const g = rgba[cp + 1] + d1[xp + 1];
                 const b = rgba[cp + 2] + d1[xp + 2];

                 const rc = ctab[Math.min(255, r)];
                 const gc = ctab[Math.min(255, g)];
                 const bc = ctab[Math.min(255, b)];

                 setColorError(r, g, b, rc, gc, bc, d1, d2, d3, xp);
             }

             dl1 = d2;
             dl2 = d3;
             dl3 = d1;
         }

         cp = 0;
         for (let y = 0; y < height; y++) {
             const d1 = dl1;
             const d2 = dl2;
             const d3 = dl3;
             d3.fill(0);

             for (let x = 0, xp = 1 * 4; x < 3; x++, xp += 4) {
                 const r = rgba[cp + 0] + d1[xp + 0];
                 const g = rgba[cp + 1] + d1[xp + 1];
                 const b = rgba[cp + 2] + d1[xp + 2];

                 const rc = ctab[Math.min(255, r)];
                 const gc = ctab[Math.min(255, g)];
                 const bc = ctab[Math.min(255, b)];

                 setColorError(r, g, b, rc, gc, bc, d1, d2, d3, xp);
             }

             for (let x = 0, xp = 4 * 4; x < width; x++, cp += 4, xp += 4) {
                 const r = rgba[cp + 0] + d1[xp + 0];
                 const g = rgba[cp + 1] + d1[xp + 1];
                 const b = rgba[cp + 2] + d1[xp + 2];
                 const a = rgba[cp + 3];

                 const rc = ctab[Math.min(255, r)];
                 const gc = ctab[Math.min(255, g)];
                 const bc = ctab[Math.min(255, b)];

                 out[cp + 0] = rc;
                 out[cp + 1] = gc;
                 out[cp + 2] = bc;
                 out[cp + 3] = a;

                 setColorError(r, g, b, rc, gc, bc, d1, d2, d3, xp);
             }

             for (let x = 0, xp = (width + 4) * 4; x < 3; x++, xp += 4) {
                 const r = rgba[cp + 0 - 4] + d1[xp + 0];
                 const g = rgba[cp + 1 - 4] + d1[xp + 1];
                 const b = rgba[cp + 2 - 4] + d1[xp + 2];

                 const rc = ctab[Math.min(255, r)];
                 const gc = ctab[Math.min(255, g)];
                 const bc = ctab[Math.min(255, b)];

                 setColorError(r, g, b, rc, gc, bc, d1, d2, d3, xp);
             }

             dl1 = d2;
             dl2 = d3;
             dl3 = d1;
         }

         return [null, out, width, height];
     }

     function toErrDiffColor512Fixed(rgba, width, height) {
         return toErrDiffColorFixed(rgba, width, height, 8);
     }
     function toErrDiffColor4096Fixed(rgba, width, height) {
         return toErrDiffColorFixed(rgba, width, height, 16);
     }
     function toErrDiffColor32768Fixed(rgba, width, height) {
         return toErrDiffColorFixed(rgba, width, height, 32);
     }

     function toErrDiffColor65536Fixed(rgba, width, height, bits) {
         const len = rgba.length;
         const out = new Uint8ClampedArray(len);
         const lbsz = (width + 16) << 2;

         const [ctab, btab] = makeColorIndex(1 << 6);

         let cp = 0;
         let dl1 = new Uint16Array(lbsz);
         let dl2 = new Uint16Array(lbsz);
         let dl3 = new Uint16Array(lbsz);

         for (let y = 0; y < 3; y++) {
             const d1 = dl1;
             const d2 = dl2;
             const d3 = dl3;
             d3.fill(0);

             cp = 0;
             for (let x = 0, xp = 4 * 4; x < width; x++, cp += 4, xp += 4) {
                 const r = rgba[cp + 0] + d1[xp + 0];
                 const g = rgba[cp + 1] + d1[xp + 1];
                 const b = rgba[cp + 2] + d1[xp + 2];

                 const ri = btab[Math.min(255, r)];
                 const gi = btab[Math.min(255, g)];
                 const bi = btab[Math.min(255, b)];
                 const rm = Math.max(ri, gi, bi) < 32 ? 0x1f : 0x3e;

                 const rc = ctab[ri & rm];
                 const gc = ctab[gi & rm];
                 const bc = ctab[bi & rm];

                 setColorError(r, g, b, rc, gc, bc, d1, d2, d3, xp);
             }

             dl1 = d2;
             dl2 = d3;
             dl3 = d1;
         }

         cp = 0;
         for (let y = 0; y < height; y++) {
             const d1 = dl1;
             const d2 = dl2;
             const d3 = dl3;
             d3.fill(0);

             for (let x = 0, xp = 1 * 4; x < 3; x++, xp += 4) {
                 const r = rgba[cp + 0] + d1[xp + 0];
                 const g = rgba[cp + 1] + d1[xp + 1];
                 const b = rgba[cp + 2] + d1[xp + 2];

                 const ri = btab[Math.min(255, r)];
                 const gi = btab[Math.min(255, g)];
                 const bi = btab[Math.min(255, b)];
                 const rm = Math.max(ri, gi, bi) < 32 ? 0x1f : 0x3e;

                 const rc = ctab[ri & rm];
                 const gc = ctab[gi & rm];
                 const bc = ctab[bi & rm];

                 setColorError(r, g, b, rc, gc, bc, d1, d2, d3, xp);
             }

             for (let x = 0, xp = 4 * 4; x < width; x++, cp += 4, xp += 4) {
                 const r = rgba[cp + 0] + d1[xp + 0];
                 const g = rgba[cp + 1] + d1[xp + 1];
                 const b = rgba[cp + 2] + d1[xp + 2];
                 const a = rgba[cp + 3];

                 const ri = btab[Math.min(255, r)];
                 const gi = btab[Math.min(255, g)];
                 const bi = btab[Math.min(255, b)];
                 const rm = Math.max(ri, gi, bi) < 32 ? 0x1f : 0x3e;

                 const rc = ctab[ri & rm];
                 const gc = ctab[gi & rm];
                 const bc = ctab[bi & rm];

                 out[cp + 0] = rc;
                 out[cp + 1] = gc;
                 out[cp + 2] = bc;
                 out[cp + 3] = a;

                 setColorError(r, g, b, rc, gc, bc, d1, d2, d3, xp);
             }

             for (let x = 0, xp = (width + 4) * 4; x < 3; x++, xp += 4) {
                 const r = rgba[cp + 0 - 4] + d1[xp + 0];
                 const g = rgba[cp + 1 - 4] + d1[xp + 1];
                 const b = rgba[cp + 2 - 4] + d1[xp + 2];

                 const ri = btab[Math.min(255, r)];
                 const gi = btab[Math.min(255, g)];
                 const bi = btab[Math.min(255, b)];
                 const rm = Math.max(ri, gi, bi) < 32 ? 0x1f : 0x3e;

                 const rc = ctab[ri & rm];
                 const gc = ctab[gi & rm];
                 const bc = ctab[bi & rm];

                 setColorError(r, g, b, rc, gc, bc, d1, d2, d3, xp);
             }

             dl1 = d2;
             dl2 = d3;
             dl3 = d1;
         }

         return [null, out, width, height];
     }

     /*
      * タイリング
      */

     class FillPattern4x4 {
         static table = [
             [4, 4],
             [0, 0], [2, 2], [2, 0], [0, 2],
             [1, 1], [3, 3], [3, 1], [1, 3],
             [1, 0], [3, 2], [3, 0], [1, 2],
             [0, 1], [2, 3], [2, 1], [0, 3],
         ].map((v) => [(v[1] << 2) | v[0]]);

         static flag = [...Array(16)].map(
             (_, i) => [...Array(16)].map(
                 (_, j) => FillPattern4x4.table.slice(0, j + 1).findIndex(
                     (v) => (v == i)) < 0 ? 0 : 1));

         static matrix = [0, 4, 8, 12].map(
             (v) => FillPattern4x4.flag.slice(v, v + 4));

         static colorToIndex(level) {
             const l = ((level - 1) << 4);
             return new Uint16Array([...Array(256)]
                 .map((_, v) => Math.trunc(v * l / 255)));
         }
     }

     function tileColorTableI(rgba, width, height, table) {
         const P = FillPattern4x4;
         const matrix = P.matrix;
         const len = rgba.length;
         const out = new Uint8Array(len >> 2);

         const [rct, gct, bct] = table;
         const [rn, gn, bn] = [rct.length, gct.length, bct.length];
         const [cm, rm, gm, bm] = [rn * gn * bn, gn * bn, bn, 1];

         const rpt = P.colorToIndex(rn);
         const gpt = P.colorToIndex(gn);
         const bpt = P.colorToIndex(bn);

         let cp = 0, op = 0;
         for (let y = 0; y < height; y++) {
             const pat = matrix[y & 3];

             for (let x = 0; x < width; x++, cp += 4) {
                 const q = pat[x & 3];

                 const r = rgba[cp + 0];
                 const g = rgba[cp + 1];
                 const b = rgba[cp + 2];

                 const rp = rpt[r];
                 const gp = gpt[g];
                 const bp = bpt[b];

                 const ro = q[rp & 15];
                 const go = q[gp & 15];
                 const bo = q[bp & 15];

                 const ri = (rp >> 4) + ro;
                 const gi = (gp >> 4) + go;
                 const bi = (bp >> 4) + bo;

                 out[op++] = ri * rm + gm * gi + bi;
             }
         }

         const clut = [...Array(cm)].map((_, i) => [
             rct[Math.trunc(i / rm) % rn],
             gct[Math.trunc(i / gm) % gn],
             bct[Math.trunc(i / bm) % bn],
         ]);
         return [clut, out, width, height];
     }

     function tileColorLevelI(rgba, width, height, level) {
         const tab = makeIndexToColor(level);
         return tileColorTableI(rgba, width, height, [tab, tab, tab]);
     }

     function tileColor8Fixed(rgba, width, height) {
         return tileColorLevelI(rgba, width, height, 2);
     }

     function tileColor16Fixed(rgba, width, height) {
         const P = FillPattern4x4;
         const matrix = P.matrix;
         const len = rgba.length;
         const out = new Uint8Array(len >> 2);

         const clut = Color16Clut;
         const index = Color16ClutIndex;

         const ptab = P.colorToIndex(3);

         let cp = 0, op = 0;
         for (let y = 0; y < height; y++) {
             const pat = matrix[y & 3];

             for (let x = 0; x < width; x++, cp += 4) {
                 const q = pat[x & 3];

                 const r = rgba[cp + 0];
                 const g = rgba[cp + 1];
                 const b = rgba[cp + 2];

                 const rp = ptab[r];
                 const gp = ptab[g];
                 const bp = ptab[b];

                 const ro = q[rp & 15];
                 const go = q[gp & 15];
                 const bo = q[bp & 15];

                 const ri = (rp >> 4) + ro;
                 const gi = (gp >> 4) + go;
                 const bi = (bp >> 4) + bo;

                 const ci = index[(ri << 0) | (gi << 2) | (bi << 4)];

                 out[op++] = ci;
             }
         }

         return [clut, out, width, height];
     }

     function tileColor27Fixed(rgba, width, height) {
         return tileColorLevelI(rgba, width, height, 3);
     }

     function tileColor64Fixed(rgba, width, height) {
         return tileColorLevelI(rgba, width, height, 4);
     }

     function tileColor125Fixed(rgba, width, height) {
         return tileColorLevelI(rgba, width, height, 5);
     }

     function tileColor216Fixed(rgba, width, height) {
         return tileColorLevelI(rgba, width, height, 6);
     }

     function tileColor256Fixed(rgba, width, height) {
         const tab1 = makeIndexToColor(8);
         const tab2 = makeIndexToColor(4);
         return tileColorTableI(rgba, width, height, [tab1, tab1, tab2]);
     }

     function tileColorTable(rgba, width, height, table) {
         const P = FillPattern4x4;
         const matrix = P.matrix;
         const len = rgba.length;
         const out = new Uint8ClampedArray(len);

         const [rct, gct, bct] = table;
         const rpt = P.colorToIndex(rct.length);
         const gpt = P.colorToIndex(gct.length);
         const bpt = P.colorToIndex(bct.length);

         let cp = 0, op = 0;
         for (let y = 0; y < height; y++) {
             const pat = matrix[y & 3];

             for (let x = 0; x < width; x++, cp += 4) {
                 const q = pat[x & 3];

                 const r = rgba[cp + 0];
                 const g = rgba[cp + 1];
                 const b = rgba[cp + 2];
                 const a = rgba[cp + 3];

                 const rp = rpt[r];
                 const gp = gpt[g];
                 const bp = bpt[b];

                 const ro = q[rp & 15];
                 const go = q[gp & 15];
                 const bo = q[bp & 15];

                 const ri = (rp >> 4) + ro;
                 const gi = (gp >> 4) + go;
                 const bi = (bp >> 4) + bo;

                 out[cp + 0] = rct[ri];
                 out[cp + 1] = gct[gi];
                 out[cp + 2] = bct[bi];
                 out[cp + 3] = a;
             }
         }

         return [null, out, width, height];
     }

     function tileColorLevel(rgba, width, height, level) {
         const tab = makeIndexToColor(level);
         return tileColorTable(rgba, width, height, [tab, tab, tab]);
     }

     function tileColor512Fixed(rgba, width, height) {
         return tileColorLevel(rgba, width, height, 8);
     }

     function tileColor4096Fixed(rgba, width, height) {
         return tileColorLevel(rgba, width, height, 16);
     }

     function tileColor32768Fixed(rgba, width, height) {
         return tileColorLevel(rgba, width, height, 32);
     }

     function tileColor65536Fixed(rgba, width, height) {
         const P = FillPattern4x4;
         const matrix = P.matrix;
         const len = rgba.length;
         const out = new Uint8ClampedArray(len);

         const ctab = makeIndexToColor(64);
         const ptab = P.colorToIndex(64);

         let cp = 0, op = 0;
         for (let y = 0; y < height; y++) {
             const pat = matrix[y & 3];

             for (let x = 0; x < width; x++, cp += 4) {
                 const q = pat[x & 3];

                 const r = rgba[cp + 0];
                 const g = rgba[cp + 1];
                 const b = rgba[cp + 2];
                 const a = rgba[cp + 3];

                 const rp = ptab[r];
                 const gp = ptab[g];
                 const bp = ptab[b];

                 const ro = q[rp & 15];
                 const go = q[gp & 15];
                 const bo = q[bp & 15];

                 const ri = (rp >> 4) + ro;
                 const gi = (gp >> 4) + go;
                 const bi = (bp >> 4) + bo;
                 const rm = Math.max(ri, gi, bi) < 32 ? 0x1f : 0x3e;

                 out[cp + 0] = ctab[ri & rm];
                 out[cp + 1] = ctab[gi & rm];
                 out[cp + 2] = ctab[bi & rm];
                 out[cp + 3] = a;
             }
         }

         return [null, out, width, height];
     }

     function tileGrayScaleLevel(rgba, width, height, level) {
         const P = FillPattern4x4;
         const matrix = P.matrix;
         const inp = toGrayScale256(rgba, width, height)[1];
         const out = new Uint8Array(inp.length);

         const ctab = makeIndexToColor(level);
         const ptab = P.colorToIndex(level);

         let cp = 0;
         for (let y = 0; y < height; y++) {
             const pat = matrix[y & 3];

             for (let x = 0; x < width; x++) {
                 const q = pat[x & 3];
                 const w = inp[cp];
                 const pi = ptab[w];
                 const po = q[pi & 15];
                 out[cp++] = (pi >> 4) + po;
             }
         }

         const clut = makeGrayScaleClut(ctab);
         return [clut, out, width, height];
     }

     function tileColorWB(rgba, width, height) {
         return tileGrayScaleLevel(rgba, width, height, 2);
     }

     function tileGrayScale4(rgba, width, height) {
         return tileGrayScaleLevel(rgba, width, height, 4);
     }

     function tileGrayScale8(rgba, width, height) {
         return tileGrayScaleLevel(rgba, width, height, 8);
     }

     function tileGrayScale16(rgba, width, height) {
         return tileGrayScaleLevel(rgba, width, height, 16);
     }

     function tileGrayScale64(rgba, width, height) {
         return tileGrayScaleLevel(rgba, width, height, 64);
     }

     /*
      *
      */

     const createTag = ((name) => document.createElement(name));

     function getImageData(image) {
         const [iw, ih] = [image.naturalWidth, image.naturalHeight];
         const osc = new OffscreenCanvas(iw, ih);
         const ctx = osc.getContext('2d');
         ctx.drawImage(image, 0, 0);
         return [[iw, ih], ctx.getImageData(0, 0, iw, ih)];
     }

     function blobToURL(blob) {
         return blob.arrayBuffer().then((buffer) => (
             `data:${blob.type};charset=utf-8;base64,` +
             MyGIF.encodeBase64(new Uint8Array(buffer))));
     }

     function createBitmap(parameter) {
         const [clut, pixel, width, height] = parameter;
         let binary = pixel;
         if (clut) {
             const color = clut.map((d) => [d[0], d[1], d[2], 255]);
             const data = [...pixel].map((p) => color[p]);
             binary = new Uint8ClampedArray(data.flat());
         }
         const image = new ImageData(binary, width, height);
         const osc = new OffscreenCanvas(width, height);
         const ctx = osc.getContext('2d');
         ctx.putImageData(image, 0, 0);
         return osc.convertToBlob().then(blobToURL);
     }

     function createGIF(parameter) {
         const [clut, pixel, width, height] = parameter;
         let bits = Math.max(1, 31 - Math.clz32(clut.length));
         const gif = new MyGIF(width, height, bits, 0, clut);
         gif.appendImageDescriptor(0, 0, width, height, pixel);
         gif.appendTrailer();
         return gif;
     }

     function createGIFURL(parameter) {
         return new Promise((resolve) =>
             resolve(createGIF(parameter).toDataURL()));
     }

     function createImageObject(parameter, nogif) {
         const bmp = nogif || !parameter[0] || (parameter[0].length > 256);
         return (bmp ? createBitmap : createGIFURL)(parameter);
     }

     /*
      *
      */

     class DropImage {
         static fromEvent(event) {
             return [...event.dataTransfer.files]
                 .filter((f) => f.type.match('^image/'));
         }

         static load(blobs) {
             const files = [...blobs].map((b) => ({blob: b}));
             return new Promise((resolve, reject) => {
                 function next(files, index) {
                     const drop = files[index];
                     drop.image = new Image();
                     drop.image.onload = (() => (++index < files.length)
                                              ? next(files, index) : resolve(files));
                     drop.image.onerror = reject;
                     drop.image.src = drop.src = URL.createObjectURL(drop.blob);
                 }
                 (files.length > 0) ? next(files, 0) : resolve(files);
             });
         }
     }

     class PageScroll {
         #stopFlag = false;

         constructor() {
             const keyboard = ((ev) => this.keyboardEvent(ev));
             const pointer = ((ev) => this.pointerEvent(ev));

             window.addEventListener('DOMMouseScroll', pointer);
             window.addEventListener('keydown', keyboard);
             window.addEventListener('touchmove', pointer, { passive: false });
             window.addEventListener('wheel', pointer, { passive: false });
         }

         pointerEvent(event) {
             if (this.#stopFlag)
                 event.preventDefault();
         }

         keyboardEvent(event) {
             if (this.#stopFlag) {
                 if ([37, 38, 39, 40].includes(event.code))
                     event.preventDefault();
             }
         }

         disable() { this.#stopFlag = true; }
         enable() { this.#stopFlag = false; }
     }

     class ImageViewer {
         constructor() {
             const S = ImageViewer;

             const html = document.documentElement;
             const screen = tagScreen;

             this.S = S;
             this.window = window;
             this.html = html;
             this.scroll = new PageScroll();
             this.screen = screen;
             this.screen_wmin = 448;
             this.screen_hmin = 336;

             this.original = null;
             this.image = null;
             this.image_scale = 1.0;
             this.image_offset = [0.0, 0.0];

             this.mouse_last = null;

             this.ondrop = null;

             const keyboard = ((ev) => this.keyboardEvent(ev));
             const resize = ((ev) => this.resizeEvent(ev));
             window.addEventListener('keydown', keyboard);
             window.addEventListener('keyup', keyboard);
             window.addEventListener('resize', resize);

             const pointer = ((ev) => this.pointerEvent(ev));
             screen.addEventListener('mouseenter', pointer);
             screen.addEventListener('mousemove', pointer);
             screen.addEventListener('mouseleave', pointer);
             screen.addEventListener('wheel', pointer, { passive: false });

             const dragover = ((ev) => this.dragOverEvent(ev));
             const drop = ((ev) => this.dropEvent(ev));
             screen.addEventListener('dragover', dragover);
             screen.addEventListener('drop', drop);

             this.setOriginal();
             this.updateScreenSize();
         }

         /*
          *
          */

         getScreenSize() {
             const screen = this.screen;
             return [screen.width, screen.height];
         }

         getScreenCenter() {
             const [w, h] = this.getScreenSize()
             return [w / 2.0, h / 2.0];
         }

         getImageSize() {
             const image = this.image;
             return [image.naturalWidth, image.naturalHeight];
         }

         getImageCenter() {
             const [w, h] = this.getImageSize()
             return [w / 2.0, h / 2.0];
         }

         imageToScreen(pos) {
             const [vadd, vsub, vmul] = Vector.op_asm;
             const spos = vmul(vsub(pos, this.getImageCenter()), this.image_scale);
             return vadd(vadd(spos, this.getScreenCenter()), this.image_offset);
         }

         screenToImage(pos) {
             const [vadd, vsub, vdiv] = Vector.op_asd;
             const spos = vsub(vsub(pos, this.getScreenCenter()), this.image_offset);
             return vadd(vdiv(spos, this.image_scale), this.getImageCenter());
         }

         setImagePosition(pos) {
             const image = this.image;
             if (!image)
                 return;
             if (!pos)
                 pos = this.imageToScreen([0.0, 0.0]);

             const screen = this.screen;
             screen.style.textAlign = 'left';
             screen.style.verticalAlign = 'top';

             const [ix, iy] = pos;
             const [dw, dh] = Vector.mul(this.getImageSize(), this.image_scale);
             image.style.left = `${Math.trunc(ix)}px`;
             image.style.top = `${Math.trunc(iy)}px`;
             image.width = dw;
             image.height = dh;
         }

         adjustScale(scale) {
             const image = this.image;
             if (!image)
                 return scale;

             const [sw, sh] = this.getScreenSize();
             const [iw, ih] = this.getImageSize();
             const fscale = Math.min(sw / iw, sh / ih);
             const smin = Math.min(fscale, 1.0) / 8.0;
             const smax = Math.max(fscale, 2.0) * 16.0;
             return clamp(smin, scale, smax);
         }

         setScroll(offset) {
             const image = this.image;
             if (!image)
                 return;
             this.image_offset = Vector.add(this.image_offset, offset);
             this.updateImagePosition();
         }

         setZoom(scale, pos, direct) {
             const image = this.image;
             if (!image)
                 return;
             if (!pos)
                 pos = this.getScreenCenter();

             const pscale = this.image_scale;
             const nscale = this.adjustScale(direct ? scale : pscale * scale);
             this.image_scale = nscale;
             scale = nscale / pscale;

             pos = Vector.sub(pos, this.getScreenCenter());
             this.image_offset = Vector.zoom(this.image_offset, pos, scale);

             this.updateImagePosition();
         }

         setFitScreen() {
             const image = this.image;
             if (!image) return;

             const [sw, sh] = this.getScreenSize();
             const [iw, ih] = this.getImageSize();
             const scale = Math.min(sw / iw, sh / ih);
             this.image_scale = scale;
             this.image_offset = [0.0, 0.0];

             const [vadd, vsub, vmul, vdiv] = Vector.op_asmd;
             this.setImagePosition(vdiv(vsub([sw, sh], vmul([iw, ih], scale)), 2.0));
         }

         updateImagePosition() {
             const image = this.image;
             if (!image)
                 return;

             const ip = this.image_offset;
             const [lx, ly] = Vector.mul(this.getImageCenter(), this.image_scale);
             this.image_offset = [clamp(-lx, ip[0], lx), clamp(-ly, ip[1], ly)];
             this.setImagePosition();
         }

         updateScreenSize() {
             const html = this.html;
             const screen = this.screen;

             const width = Math.max(this.screen_wmin, html.clientWidth);
             const height = Math.max(this.screen_hmin, html.clientHeight);

             screen.width = width;
             screen.height = height;

             this.image_offset = [0.0, 0.0];
             this.setZoom(1.0);
         }

         resizeEvent(event) {
             this.updateScreenSize();
         }

         /*
          *
          */

         clearScreen() {
             const screen = this.screen;
             while (screen.firstChild)
                 screen.removeChild(screen.firstChild);
         }

         setMessage(message) {
             this.clearScreen();

             const screen = this.screen;
             screen.style.textAlign = 'center';
             screen.style.verticalAlign = 'middle';

             const tag = createTag('div');
             tag.style.fontSize = 'xx-large';
             tag.style.color = 'white';
             tag.innerHTML = message;

             screen.appendChild(tag);
         }

         setImage(image) {
             this.clearScreen();
             image.className = 'relative';
             this.screen.appendChild(image);
             this.image = image;
         }

         setOriginal(image) {
             if (this.original) {
                 const url = this.original.src;
                 this.original = null;
                 URL.revokeObjectURL(url);
             }
             this.image = image;
             if (image) {
                 this.original = image;
                 this.setImage(image);
                 this.setFitScreen();

                 if (this.ondrop)
                     this.ondrop(this);
             } else {
                 this.setMessage('ここに画像ファイルをドロップしてください');
             }
         }

         dragOverEvent(event) {
             event.preventDefault();
             event.stopPropagation();
         }

         dropEvent(event) {
             event.preventDefault();
             event.stopPropagation();
             const images = DropImage.fromEvent(event);
             if (images.length == 1) {
                 this.setMessage('ただいま読み込み中...');
                 DropImage.load(images).then(
                     (files) => this.setOriginal(files[0].image),
                     console.log
                 );
             }
         }

         /*
          *
          */

         #focus = false;
         #scrollLock = false;

         setScrollLock(mode) {
             this.#scrollLock = mode;
             if (mode)
                 this.disableScroll();
         }

         disableScroll() {
             if (this.#focus || this.#scrollLock)
                 this.scroll.disable();
         }

         enableScroll() {
             if (!this.#scrollLock)
                 this.scroll.enable();
         }

         checkScrollKey(event) {
             return false;
         }

         keyboardEvent(event) {
             switch (event.type) {
                 case 'keydown':
                     if (this.checkScrollKey(event)) {
                         this.disableScroll();
                         break;
                     }
                     let scroll = null;
                     switch (event.code) {
                         case 'ArrowUp':
                             scroll = [0.0, -16.0];
                             break;
                         case 'ArrowDown':
                             scroll = [0.0, +16.0];
                             break;
                         case 'ArrowLeft':
                             scroll = [-16.0, 0.0];
                             break;
                         case 'ArrowRight':
                             scroll = [+16.0, 0.0];
                             break;
                     }
                     if (scroll) {
                         if (event.shiftKey)
                             scroll = Vector.div(scroll, 8.0);
                         this.setScroll(scroll);
                         break;
                     }

                     let scale = null;
                     let image_scale = null;
                     switch (event.key) {
                         case '0':
                             this.image_offset = [0.0, 0.0];
                         case '1':
                             image_scale = 1.0;
                             break;
                         case '2':
                             image_scale = 2.0;
                             break;
                         case '3':
                             image_scale = 3.0;
                             break;
                         case '4':
                             image_scale = 4.0;
                             break;

                         case '9':
                             this.setFitScreen();
                             break;
                         case '+':
                         case ';':
                             scale = 2.0;
                             break;
                         case '=':
                         case '-':
                             scale = 0.5;
                             break;
                     }
                     if (image_scale) {
                         this.setZoom(image_scale, null, true);
                     } else if (scale) {
                         if (event.ctrlKey)
                             scale = Math.pow(scale, 0.125);
                         this.setZoom(scale);
                         break;
                     }

                     break;

                 case 'keyup':
                     if (!this.checkScrollKey(event))
                         this.enableScroll();
                     break;
             }
         }

         getEventOffset(event) {
             if (!event.offsetX || !event.offsetY)
                 return null;
             const offs = [event.offsetX, event.offsetY];
             const target = event.target;
             if (target.id == this.screen.id)
                 return offs;
             return Vector.add(offs, [target.offsetLeft, target.offsetTop]);
         }

         pointerEvent(event) {
             const compose = ((ev) => (ev.altKey ||  ev.ctrlKey ||
                                       ev.metaKey || ev.shiftKey));

             const ppos = this.mouse_last;
             const cpos = this.getEventOffset(event);
             switch (event.type) {
                 case 'mouseenter':
                     this.#focus = true;
                     if (this.checkScrollKey(event))
                         this.disableScroll();
                     break;

                 case 'mouseleave':
                     this.#focus = false;
                     this.enableScroll();
                     break;

                 case 'mousemove':
                     if (compose(event) && ppos)
                         this.setScroll(Vector.sub(cpos, ppos));
                     break;

                 case 'wheel':
                     if (!compose(event))
                         this.setScroll([-event.deltaX, -event.deltaY]);
                     else
                         this.setZoom(Math.pow(2, - event.deltaY / 128), cpos);
                     break;
             }
             this.mouse_last = cpos;
         }
     }

     /*
      *
      */

     window.onload = function() {
         document.body.className = 'noframe';

         const viewer = new ImageViewer();
         viewer.setScrollLock(true);

         const image_rendering = [
             ['auto', menuRenderingAuto],
             // ['smooth', menuRenderingSmooth],
             // ['high-quality', menuRenderingHighQuality],
             // ['crisp-edges', menuRenderingCrispEdges],
             ['pixelated', menuRenderingPixelated],
         ];

         const generated = {
             pixel: null,
             rendering: 'pixelated',
             image: {
                 "menuColorRed": { func: createColorRed, image: null },
                 "menuColorGreen": { func: createColorGreen, image: null },
                 "menuColorBlue": { func: createColorBlue, image: null },

                 "menuReduceColor65536": { func: reduceColor65536, image: null, png: true },
                 "menuReduceColor32768": { func: reduceColor32768, image: null, png: true },
                 "menuReduceColor4096": { func: reduceColor4096, image: null, png: true },
                 "menuReduceColor512": { func: reduceColor512, image: null, png: true },
                 "menuReduceColor256": { func: reduceColor256, image: null },
                 "menuReduceColor216": { func: reduceColor216, image: null },
                 "menuReduceColor125": { func: reduceColor125, image: null },
                 "menuReduceColor64": { func: reduceColor64, image: null },
                 "menuReduceColor27": { func: reduceColor27, image: null },
                 "menuReduceColor16": { func: reduceColor16, image: null },
                 "menuReduceColor8": { func: reduceColor8, image: null },

                 "menuErrDiffColor65536F": { func: toErrDiffColor65536Fixed, image: null, png: true },
                 "menuErrDiffColor32768F": { func: toErrDiffColor32768Fixed, image: null, png: true },
                 "menuErrDiffColor4096F": { func: toErrDiffColor4096Fixed, image: null, png: true },
                 "menuErrDiffColor512F": { func: toErrDiffColor512Fixed, image: null, png: true },
                 "menuErrDiffColor256F": { func: toErrDiffColor256Fixed, image: null },
                 "menuErrDiffColor216F": { func: toErrDiffColor216Fixed, image: null },
                 "menuErrDiffColor125F": { func: toErrDiffColor125Fixed, image: null },
                 "menuErrDiffColor64F": { func: toErrDiffColor64Fixed, image: null },
                 "menuErrDiffColor27F": { func: toErrDiffColor27Fixed, image: null },
                 "menuErrDiffColor16F": { func: toErrDiffColor16Fixed, image: null },
                 "menuErrDiffColor8": { func: toErrDiffColor8, image: null },

                 "menuErrDiffGrayScale256": { func: toErrDiffGrayScale256, image: null },
                 "menuErrDiffGrayScale64": { func: toErrDiffGrayScale64, image: null },
                 "menuErrDiffGrayScale16": { func: toErrDiffGrayScale16, image: null },
                 "menuErrDiffGrayScale8": { func: toErrDiffGrayScale8, image: null },
                 "menuErrDiffGrayScale4": { func: toErrDiffGrayScale4, image: null },
                 "menuErrDiffColorWB": { func: toErrDiffColorWB, image: null },

                 "menuTileColor65536F": { func: tileColor65536Fixed, image: null },
                 "menuTileColor32768F": { func: tileColor32768Fixed, image: null },
                 "menuTileColor4096F": { func: tileColor4096Fixed, image: null },
                 "menuTileColor512F": { func: tileColor512Fixed, image: null },
                 "menuTileColor256F": { func: tileColor256Fixed, image: null },
                 "menuTileColor216F": { func: tileColor216Fixed, image: null },
                 "menuTileColor125F": { func: tileColor125Fixed, image: null },
                 "menuTileColor64F": { func: tileColor64Fixed, image: null },
                 "menuTileColor27F": { func: tileColor27Fixed, image: null },
                 "menuTileColor16F": { func: tileColor16Fixed, image: null },
                 "menuTileColor8F": { func: tileColor8Fixed, image: null },

                 "menuGrayScale256": { func: toGrayScale256, image: null },
                 "menuGrayScale64": { func: toGrayScale64, image: null },
                 "menuGrayScale16": { func: toGrayScale16, image: null },
                 "menuGrayScale8": { func: toGrayScale8, image: null },
                 "menuGrayScale4": { func: toGrayScale4, image: null },
                 "menuColorWB": { func: toColorWB, image: null },

                 "menuTileGrayScale64": { func: tileGrayScale64, image: null },
                 "menuTileGrayScale16": { func: tileGrayScale16, image: null },
                 "menuTileGrayScale8": { func: tileGrayScale8, image: null },
                 "menuTileGrayScale4": { func: tileGrayScale4, image: null },
                 "menuTileColorWB": { func: tileColorWB, image: null },
             },
         }

         const image_table = Object.keys(generated.image).map(
             (id) => document.getElementById(id)).filter(
                 (item) => item != null);

         function getPixelData() {
             if (!generated.pixel)
                 generated.pixel = getImageData(viewer.original);
             return generated.pixel;
         }
         function clearImage(image) {
             URL.revokeObjectURL(image.src);
             return null;
         }

         function getImageRendering(mode) {
             return image_rendering.findIndex((v) => v[0] == mode);
         }
         function setImageRendering() {
             const rendering = generated.rendering;
             viewer.image.style.setProperty('image-rendering', rendering, 'important');
             image_rendering.forEach((d, i) => {
                 const tag = d[1];
                 tag.style.color = (d[0] == rendering) ? 'green': 'black';
             });
         }
         function setImage(image) {
             viewer.setImage(image);
             viewer.setImagePosition();
             setImageRendering(image);
         }
         function setSelectImage(id) {
             image_table.forEach((tag) => {
                 tag.style.color = (id == tag.id) ? 'darkred' : 'black';
             });
         }

         function setFilteredImage(mid, mgen) {
             const pixel = getPixelData();
             const [iw, ih] = pixel[0];
             const idata = mgen.func(pixel[1].data, iw, ih);
             const nogif = menuCreateGIF.checked;
             createImageObject(idata, nogif).then((url) => {
                 const tag = createTag('img');
                 tag.width = iw;
                 tag.height = ih;
                 tag.onload = ((e) => setImage(e.target));
                 tag.src = url;
                 mgen.image = tag;
             });
             setSelectImage(mid);
         }

         function onclick(event) {
             const mid = event.target.id;
             if (mid == 'menuOriginal') {
                 setImage(viewer.original);
                 setSelectImage(mid);
                 return;
             }
             const mgen = generated.image[mid];
             if (mgen) {
                 if (mgen.image != null) {
                     setImage(mgen.image);
                     setSelectImage(mid);
                 } else {
                     viewer.setMessage(
                         'ただいま生成中、しばらくお待ち下さい。<br>' +
                         '<small>数分かかることがあります。<br>' +
                         'その間はブラウザが応答しません。</small>');
                     setTimeout(() => setFilteredImage(mid, mgen), 1);
                 }
                 return;
             }

             let rendering = null;
             if (mid == 'menuRendering') {
                 const ridx = getImageRendering(generated.rendering);
                 const nidx = (ridx + 1) % image_rendering.length;
                 rendering = image_rendering[nidx][0];
             } else {
                 const ridx = image_rendering.findIndex((v) => v[1].id == mid);
                 const nidx = (ridx >= 0) ? ridx : image_rendering.length - 1;
                 rendering = image_rendering[nidx][0];
             }
             if (rendering) {
                 generated.rendering = rendering;
                 setImageRendering();
             }
         }

         viewer.ondrop = ((viewer) => {
             generated.pixel = null;
             Object.values(generated.image).forEach((v) => v.image = null);

             const [w, h] = viewer.getImageSize();
             menuOriginal.innerText = `元画像(${w}x${h})`;
             setImageRendering();
             setSelectImage('menuOriginal');
             tagMenu.hidden = false;
         });

         document.querySelectorAll('.menuitem').forEach(
             (item) => item.addEventListener('click', onclick));

         menuCreateGIF.onchange = ((event) => {
             Object.values(generated.image).forEach(
                 (v) => { if (!v.png) v.image = null; });
         });
     }

    </script>
  </body>
</html>
```

</div></details>