JavaScript でデータを Deflate (Zlib, Gzip) 圧縮および解凍する

本記事では以下の 2 種類の方法を書きます:

- Compression Streams API を用いる方法
- ライブラリ pako を用いる方法

# 1. 予備知識

## 1.1. Deflate 圧縮のデータ形式について

Deflate 圧縮のデータ形式は複数あり、主に以下のものが使用されています:

- 生の deflate 形式 (RFC 1951)
- zlib 形式 (RFC 1950)
    - 生の deflate 形式のデータにヘッダとフッタを付け加えた形式
    - **※単に「deflate 形式」と言う場合は「生の deflate 形式」でなく「zlib 形式」を指す場合があるため注意**
- gzip 形式 (RFC 1952)
    - 生の deflate 形式のデータにヘッダとフッタを付け加えた形式

参考「[RFC 1950: ZLIB Compressed Data Format Specification version 3.3](https://www.rfc-editor.org/rfc/rfc1950)」
参考「[RFC 1951: DEFLATE Compressed Data Format Specification version 1.3](https://www.rfc-editor.org/rfc/rfc1951)」
参考「[RFC 1952: GZIP file format specification version 4.3](https://www.rfc-editor.org/rfc/rfc1952)」

## 1.2. Compression Streams API について

Compression Streams API は deflate アルゴリズムでデータの圧縮や解凍を行える API で、この API が使用できる環境であれば別途ライブラリ等を利用しなくても圧縮および解凍できます。

ただし、2023-03-15 現在は以下のような一部の WEB ブラウザや環境によっては利用できないため、注意が必要です:

- Safari はベータ版 16.4 でサポート
- Firefox は非サポート
- Node.js は「生の deflate 形式」(`deflate-raw`) は非サポート

参考「[Compression Streams API - Web API | MDN](https://developer.mozilla.org/ja/docs/Web/API/Compression_Streams_API)」

## 1.3. ライブラリ pako について

ライブラリ pako は deflate アルゴリズムでデータの圧縮や解凍を行う機能を提供します。

ライブラリ pako 自体は MIT License で利用できます。

(※ pako のビルド前のソースコードの一部は zlib License なため、ビルド前のソースコードを利用する場合は注意。)

参考「[GitHub - nodeca/pako: high speed zlib port to javascript, works in browser & node.js](https://github.com/nodeca/pako)」
参考「[pako 2.1.0 API documentation](https://nodeca.github.io/pako/)」

# 2. Deflate 圧縮および解凍

## 2.1. 生の Deflate 形式

Compression Streams API を用いる場合はデータをストリームでやり取りするため、データとストリームの相互変換を行う必要があります。

`ReadableStream` とデータの相互変換については別記事にしました。

参考「[[JavaScript] ReadableStream とその他の型の相互変換 - Qiita](https://qiita.com/kerupani129/items/9a7ea8b2e1ca82301c87)」

`ReadableStream.prototype.pipeThrough` メソッドを使用することでデータを変換するストリームを得られます。

`CompressionStream` や `DecompressionStream` を `transformStream` として指定することでデータを圧縮および解凍するストリームを取得できます (`TransformStream` を継承していなくても指定可能です) 。

参考「[ReadableStream.pipeThrough() - Web API | MDN](https://developer.mozilla.org/ja/docs/Web/API/ReadableStream/pipeThrough)」
参考「[CompressionStream - Web API | MDN](https://developer.mozilla.org/ja/docs/Web/API/CompressionStream)」
参考「[DecompressionStream - Web API | MDN](https://developer.mozilla.org/ja/docs/Web/API/DecompressionStream)」

Compression Streams API で生の Deflate 形式を扱うには `format` に `deflate-raw` を指定します。

参考「[CompressionStream() - Web API | MDN](https://developer.mozilla.org/ja/docs/Web/API/CompressionStream/CompressionStream)」
参考「[DecompressionStream() - Web API | MDN](https://developer.mozilla.org/ja/docs/Web/API/DecompressionStream/DecompressionStream)」

```js:Compression Streams API を用いる場合
const deflateRaw = async data => {
	const readableStream = new Blob([data]).stream();
	const compressedStream = readableStream.pipeThrough(
        // メモ: 毎回インスタンス化する必要がある
        new CompressionStream('deflate-raw'),
    );
	const arrayBuffer = await new Response(compressedStream).arrayBuffer();
	return arrayBuffer;
};

const inflateRaw = async data => {
	const readableStream = new Blob([data]).stream();
	const decompressedStream = readableStream.pipeThrough(
        // メモ: 毎回インスタンス化する必要がある
        new DecompressionStream('deflate-raw'),
    );
	const arrayBuffer = await new Response(decompressedStream).arrayBuffer();
	return arrayBuffer;
};
```

ライブラリ pako を用いる場合、`pako.deflateRaw` 関数及び `pako.inflateRaw` 関数を使用することで生の Deflate 形式で圧縮および解凍できます。

参考「[deflateRaw - pako 2.1.0 API documentation](https://nodeca.github.io/pako/#deflateRaw)」
参考「[inflateRaw - pako 2.1.0 API documentation](https://nodeca.github.io/pako/#inflateRaw)」

```js:ライブラリ pako を用いる場合
const deflateRaw = data => pako.deflateRaw(data).buffer;

const inflateRaw = data => pako.inflateRaw(data).buffer;
```

上記の方法で定義した `deflateRaw` 関数および `inflateRaw` 関数で以下のようにして圧縮および解凍できます。

```js
const textA = '犬や猫は肉球を持つが、兎は肉球を持たない。犬と猫は食肉目だが、兎は重歯目である。';
console.log(textA);
const textUint8ArrayA = new TextEncoder().encode(textA);
console.log(textUint8ArrayA.length);

// 圧縮
// メモ: deflateRaw 関数に直接 textA を渡すことも可
const arrayBufferA = await deflateRaw(textUint8ArrayA);
console.log(arrayBufferA.byteLength);

// 解凍
// TODO: 実際は例外処理が必要
const arrayBufferB = await inflateRaw(arrayBufferA);
console.log(arrayBufferB.byteLength);
const textB = new TextDecoder().decode(arrayBufferB);
console.log(textB);
```

```:実行結果
犬や猫は肉球を持つが、兎は肉球を持たない。犬と猫は食肉目だが、兎は重歯目である。
120
84
120
犬や猫は肉球を持つが、兎は肉球を持たない。犬と猫は食肉目だが、兎は重歯目である。
```

## 2.2. zlib 形式

同様にして zlib 形式で圧縮および解凍することができます。

Compression Streams API で zlib 形式を扱うには `format` に `deflate` を指定します。

参考「[CompressionStream() - Web API | MDN](https://developer.mozilla.org/ja/docs/Web/API/CompressionStream/CompressionStream)」
参考「[DecompressionStream() - Web API | MDN](https://developer.mozilla.org/ja/docs/Web/API/DecompressionStream/DecompressionStream)」

```js:Compression Streams API を用いる場合
const deflate = async data => {
	const readableStream = new Blob([data]).stream();
	const compressedStream = readableStream.pipeThrough(
        // メモ: 毎回インスタンス化する必要がある
        new CompressionStream('deflate'),
    );
	const arrayBuffer = await new Response(compressedStream).arrayBuffer();
	return arrayBuffer;
};

const inflate = async data => {
	const readableStream = new Blob([data]).stream();
	const decompressedStream = readableStream.pipeThrough(
        // メモ: 毎回インスタンス化する必要がある
        new DecompressionStream('deflate'),
    );
	const arrayBuffer = await new Response(decompressedStream).arrayBuffer();
	return arrayBuffer;
};
```

ライブラリ pako を用いる場合、`pako.deflate` 関数及び `pako.inflate` 関数を使用することで zlib 形式で圧縮および解凍できます。

参考「[deflate - pako 2.1.0 API documentation](https://nodeca.github.io/pako/#deflate)」
参考「[inflate - pako 2.1.0 API documentation](https://nodeca.github.io/pako/#inflate)」

```js:ライブラリ pako を用いる場合
const deflate = data => pako.deflate(data).buffer;

const inflate = data => pako.inflate(data).buffer;
```

上記の方法で定義した `deflate` 関数および `inflate` 関数で以下のようにして圧縮および解凍できます。

```js
const textA = '犬や猫は肉球を持つが、兎は肉球を持たない。犬と猫は食肉目だが、兎は重歯目である。';
console.log(textA);
const textUint8ArrayA = new TextEncoder().encode(textA);
console.log(textUint8ArrayA.length);

// 圧縮
// メモ: deflate 関数に直接 textA を渡すことも可
const arrayBufferA = await deflate(textUint8ArrayA);
console.log(arrayBufferA.byteLength);

// 解凍
// TODO: 実際は例外処理が必要
const arrayBufferB = await inflate(arrayBufferA);
console.log(arrayBufferB.byteLength);
const textB = new TextDecoder().decode(arrayBufferB);
console.log(textB);
```

```:実行結果
犬や猫は肉球を持つが、兎は肉球を持たない。犬と猫は食肉目だが、兎は重歯目である。
120
90
120
犬や猫は肉球を持つが、兎は肉球を持たない。犬と猫は食肉目だが、兎は重歯目である。
```

zlib 形式は生の deflate 形式のデータにヘッダとフッタを付け加えるため、生の deflate 形式より圧縮後のサイズが大きくなります (6 バイトまたは 10 バイト) 。

参考「[Page 4 - RFC 1950: ZLIB Compressed Data Format Specification version 3.3](https://www.rfc-editor.org/rfc/rfc1950#page-4)」(「2.2. Data format」を参照)

## 2.3. gzip 形式

同様にして gzip 形式で圧縮および解凍することができます。

Compression Streams API で gzip 形式を扱うには `format` に `gzip` を指定します。

参考「[CompressionStream() - Web API | MDN](https://developer.mozilla.org/ja/docs/Web/API/CompressionStream/CompressionStream)」
参考「[DecompressionStream() - Web API | MDN](https://developer.mozilla.org/ja/docs/Web/API/DecompressionStream/DecompressionStream)」

```js:Compression Streams API を用いる場合
const gzip = async data => {
	const readableStream = new Blob([data]).stream();
	const compressedStream = readableStream.pipeThrough(
        // メモ: 毎回インスタンス化する必要がある
        new CompressionStream('gzip'),
    );
	const arrayBuffer = await new Response(compressedStream).arrayBuffer();
	return arrayBuffer;
};

const ungzip = async data => {
	const readableStream = new Blob([data]).stream();
	const decompressedStream = readableStream.pipeThrough(
        // メモ: 毎回インスタンス化する必要がある
        new DecompressionStream('gzip'),
    );
	const arrayBuffer = await new Response(decompressedStream).arrayBuffer();
	return arrayBuffer;
};
```

ライブラリ pako を用いる場合、`pako.gzip` 関数及び `pako.ungzip` 関数を使用することで gzip 形式で圧縮および解凍できます。

参考「[gzip - pako 2.1.0 API documentation](https://nodeca.github.io/pako/#gzip)」
参考「[ungzip - pako 2.1.0 API documentation](https://nodeca.github.io/pako/#ungzip)」

```js:ライブラリ pako を用いる場合
const gzip = data => pako.gzip(data).buffer;

const ungzip = data => pako.ungzip(data).buffer;
```

上記の方法で定義した `gzip` 関数および `ungzip` 関数で以下のようにして圧縮および解凍できます。

```js
const textA = '犬や猫は肉球を持つが、兎は肉球を持たない。犬と猫は食肉目だが、兎は重歯目である。';
console.log(textA);
const textUint8ArrayA = new TextEncoder().encode(textA);
console.log(textUint8ArrayA.length);

// 圧縮
// メモ: gzip 関数に直接 textA を渡すことも可
const arrayBufferA = await gzip(textUint8ArrayA);
console.log(arrayBufferA.byteLength);

// 解凍
// TODO: 実際は例外処理が必要
const arrayBufferB = await ungzip(arrayBufferA);
console.log(arrayBufferB.byteLength);
const textB = new TextDecoder().decode(arrayBufferB);
console.log(textB);
```

```:実行結果
犬や猫は肉球を持つが、兎は肉球を持たない。犬と猫は食肉目だが、兎は重歯目である。
120
102
120
犬や猫は肉球を持つが、兎は肉球を持たない。犬と猫は食肉目だが、兎は重歯目である。
```

gzip 形式は生の deflate 形式のデータにヘッダとフッタを付け加えるため、生の deflate 形式より圧縮後のサイズが大きくなります (18 バイト以上) 。

参考「[Page 5 - RFC 1952: GZIP file format specification version 4.3](https://www.rfc-editor.org/rfc/rfc1952#page-5)」(「2.3. Member format」を参照)