LoginSignup
34
20

More than 1 year has passed since last update.

JavaScript/Node.jsでのバイナリデータ処理

Last updated at Posted at 2022-08-31

概要

JavaScriptでバイナリデータを扱おうとするとBufferArraryBufferという似た名前のオブジェクトが登場して混乱したり、Uint8Arrayという耳慣れないオブジェクトが登場して途方に暮れたりすることがあると思います。本記事ではこれらのオブジェクトの概念を整理することでJavaScriptでのバイナリデータの扱いの見通しを良くしていきます。

概念の整理

まずバイナリデータを処理しようとしている環境がNode.jsなのかブラウザ環境なのかが重要です。Node.js環境の場合はBufferクラスを利用することになります。BufferクラスはNode.jsの標準ライブラリとして実装されています。

ブラウザ環境ではJavaScript自体に実装されているArraryBufferオブジェクトを利用することになります。Bufferは実装されていません。

これらは名前は似ていますが別物です。

またUint8ArrayはJavaScript標準で実装されているArraryBufferのビューです。生のバイナリデータに近いArraryBufferをより使いやすい形にしたものという位置付けです。

以上のように環境によって利用できるオブジェクトが異なるため、この記事では環境ごとにバイナリデータの扱い方を解説していきます。

(補足として最後にDenoでのバイナリデータ処理について触れます。基本的にブラウザ環境と同じAPIを利用します。)

Node.jsでのバイナリデータ処理

Node.jsでバイナリデータを扱うならBufferクラスを利用します。後述するJavaScript標準のArraryBufferより便利なので利用できる場合は基本的にこちらを使うのが良いと思います。グローバルスコープで利用できるのでインポートしなくても利用できますが、以下のように明示的にインポートすることが推奨されています。

// cjs
const { Buffer } = require('node:buffer');

// esm
import { Buffer } from 'node:buffer';

コンパイルしてブラウザ環境でコードを実行する場合はポリフィルライブラリを利用するとNode.js環境と同じように実装することができます。

npm install buffer
// cjs
const Buffer = require('buffer/').Buffer;

// esm
import { Buffer } from 'buffer';

Buffer

Bufferクラスは固定長のメモリ領域を表現したオブジェクトです。Buffer.alloc()で長さを指定して生成したり、Buffer.from()で文字列などのデータから生成することができます。

// 10バイトのメモリ領域を確保する(各バイトは0で初期化される)
Buffer.alloc(10);
// <Buffer 00 00 00 00 00 00 00 00 00 00>

// バイトの配列から生成
// 値の範囲は0~255
Buffer.from([1, 255, 256]);
// <Buffer 01 ff 00>

// 文字列から生成
Buffer.from('テスト');
// <Buffer e3 83 86 e3 82 b9 e3 83 88>

各バイトにはインデックスによってアクセスすることができます。それぞれの値は10進数の数値型で表現されます。

const buf = Buffer.from('hoge');
> buf[0];
104
> buf[1];
111
> buf[2];
103
> buf[3];
101

取得するだけではなく上書きすることもできます。

buf[0] = 102;
buf[1] = 117;
buf[2] = 103;
buf[3] = 97;
buf;
// <Buffer 66 75 67 61>

// toString()について詳しくは後述
buf.toString();
// 'fuga'

文字列から生成する場合はエンコーディングを指定することができます。デフォルトではUTF-8でエンコードされたものとして解釈されます。

Buffer.from('テスト', 'utf8');
// <Buffer e3 83 86 e3 82 b9 e3 83 88>

16進数文字列から生成することもできます。

Buffer.from('e38386e382b9e38388', 'hex');
// <Buffer e3 83 86 e3 82 b9 e3 83 88>

サポートされている形式は以下に挙げられています。

Buffer.toString()を利用することでバイナリデータを文字列として出力することができます。その際もデフォルトの形式はUTF-8です。引数によって出力形式を指定することができます。UTF-8文字列とバイナリデータの16進数文字列を相互変換したり、文字列をbase46エンコードして出力することができます。

const buf1 = Buffer.from('e38386e382b9e38388', 'hex');
buf1.toString();
// > 'テスト'

const buf2 = Buffer.from('テスト', 'utf8');
buf2.toString('hex');
// > 'e38386e382b9e38388'
buf2.toString('base64');
// > '44OG44K544OI'

Node.jsでファイルを読み込む場合、エンコーディングを指定しない場合はこのBuffer型のデータとして扱われます。

import { readFile } from 'node:fs';

readFile('/path/to/file', (err, data) => {
    console.log(data);
    // <Buffer ...>
    data.toString('hex');
    // '...'
})

サーバサイドのヘッドレスブラウザなどでローカルファイルの画像のバイナリデータをimgタグで表示するとします。base64エンコーディング文字列への変換を活用して以下のように実装することができます。

<img
    src={`data:image/png;base64,${data.toString('base64')}`}
/>

Bufferクラスはnewによって生成することもできますが、非推奨とされています。引数に渡す値の型によって挙動が変わるため扱いが難しく、またNode.jsのバージョン8以前では割り当てられたメモリが初期化されない場合があるためセキュリティ上の危険性があります。

常にnew Buffer()ではなくBuffer.alloc()Buffer.from()を使うようにしましょう。

JavaScriptでのバイナリデータ処理

JavaScriptにはArrayBufferオブジェクトが実装されてます。こちらも固定長のメモリ領域を表現しています。ブラウザ環境で直接実行するコード上でバイナリデータを扱う場合はこのオブジェクトを利用します。ただしこのArrayBufferではデータの内容にアクセスすることはできません。

ArrayBufferの内容にアクセスするにはTypedArrayというオブジェクトを利用します。TypedArrayにはUint8ArrayFloat32Arrayといったいくつかの種類が存在します。これらはArrayBufferのビューとして機能します。

ArrayBuffer

ArrayBufferは確保するメモリのバイト長を指定して生成します。メソッドはほとんどないため、このArrayBufferを直接操作することはありません。

new ArrayBuffer(10);
// ArrayBuffer {
//   [Uint8Contents]: <00 00 00 00 00 00 00 00 00 00>,
//   byteLength: 10
// }

TypedArray

バイナリデータを操作するためにはArrayBufferからTypedArrayビューを生成する必要があります。その際に最もよく利用されるのがUint8Arrayです。Uint8Arrayは符号なしの8ビット=1バイト単位でメモリ領域を扱います。メモリは1バイト単位で扱われるのが一般的なのでこのUint8Arrayがメモリ領域を表現するのに一番自然です。

TypedArrayはコンストラクタにArrayBufferを入力することで生成できます。

const buffer = new ArrayBuffer(8);
new Uint8Array(buffer);
// Uint8Array(8) [
//   0, 0, 0, 0,
//   0, 0, 0, 0
// ]

またArrayBufferを経由せず直接TypedArrayを生成することもできます。この場合TypedArrayの種類に応じた単位の領域が指定した長さ分確保されます。また数値の配列から生成することもできます。その場合の数値の範囲はTypedArrayの種類によって変わります。Uint8Arrayなら値は8ビットなので0~255になります。

内部的にはArrayBufferが生成された上でそのビューが返されていますが、ArrayBuffer自体を意識する必要はあまりありません。

new Uint8Array(8);
// Uint8Array(8) [
//   0, 0, 0, 0,
//   0, 0, 0, 0
// ]

// こちらは32ビット=4バイト単位
// 8*4=32バイトのメモリ領域が確保される
new Int32Array(8);
// Int32Array(8) [
//   0, 0, 0, 0,
//   0, 0, 0, 0
// ]

// 符号なし8ビットなので値の範囲は0~255
new Uint8Array([0, 255, 256]);
// Uint8Array(3) [ 0, 255, 0 ]

TypedArrayはインデックスによってデータの内容にアクセスすることができます。その際の単位はTypedArrayの種類によって異なります。

const u8 = new Uint8Array(8);
u8[0];
// 0
u8[1];
// 0

u8[0] = 255;
u8;
// Uint8Array(8) [
//   255, 0, 0, 0,
//     0, 0, 0, 0
// ]

u8[1] = 256;
u8;
// Uint8Array(8) [
//   255, 0, 0, 0,
//     0, 0, 0, 0
// ]

その他、Arrayに似たメソッドが実装されています。

TypedArray単体ではNode.jsのBufferのように文字列からバイナリデータを作成したり、逆にバイナリデータを文字列に変換ことはできません。TextEncoder/TextDecoderを利用します。

文字列からバイナリデータを得るにはTextEncoder.encode()を使います。Uint8Arrayが返ってきます。エンコーディングはUTF-8のみ対応しています。

const encoder = new TextEncoder();
encoder.encode('テスト');
// Uint8Array(9) [
//   227, 131, 134,
//   227, 130, 185,
//   227, 131, 136
// ]

バイナリデータを文字列に変換するにはTextDecoder.decode()を使います。

const encoded = encoder.encode('テスト');
const decoder = new TextDecoder();
decoder.decode(encoded);
// 'テスト'

こちらはTextDecoderのコンストラクタの引数で指定することでUTF-8以外のエンコーディングを利用することができます。対応しているものの一覧は以下にあります。

バイナリデータと16進数文字列の変換手段は実装がありません。少しく工夫すれば自分で実装することもできます。

まずはバイナリデータから16進数文字列への変換です。

Array.from(encoded).map(
    (byte) => byte.toString(16).padStart(2, '0')
).join('');
// 'e38386e382b9e38388'

Uint8Arrayを配列に変換して各値を取り出すと10進数の数値になります。それらをNumber.prototype.toString()を利用して16進数文字列にします。さらに二桁で固定するためString.prototype.padStart()を使います。あとはArray.prototype.join()で繋げれば16進数文字列表記が得られます。

逆に16進数文字列からバイナリデータを生成する処理です。まずなんとかして16進数文字列を2文字ずつの配列に変換します。それらを10進数の数値に変換します。JavaScriptでは16進数は0x83といった形式で扱われるため、16進数文字列の頭に0xをつけてNumber()に渡せばOKです。最後にその数値の配列をTypedArrayのコンストラクタに渡せば完成です。

const hexStrings = ['e3', '83', '86', 'e3', '82', 'b9', 'e3', '83', '88'];
const hexNumbers = hexStrings.map((hex) => Number('0x' + hex));
const u8 = new Uint8Array(hexNumbers);

decoder.decode(u8);
// 'テスト'

base64エンコードについてはかなり複雑な事態が発生します。

base64文字列を生成する手段としてbtoa()atob()が実装されていますが、ASCII文字列との変換にしか対応していません。

上記のリンクの例を参考に実装するのであれば、Uint8Arrayのそれぞれのバイトを無理やりASCII文字列に変換してこのbtoa()に入力します。

const encoder = new TextEncoder();
const encoded = encoder.encode('テスト');

let result = '';
for (let i = 0; i < encoded.byteLength; i++) {
    result += String.fromCharCode(encoded[i]);
}
btoa(result);
// '44OG44K544OI'

逆にUint8Arrayにデコードするならatob()でデコードした文字列をString.prototype.charCodeAt()を使って一文字ずつバイトに変換していきます。String.prototype.charCodeAt()はUTF-16でエンコードしますがatob()の戻り値はASCII文字なので0~255の範囲で表現できます。なのでそのままUint8Arrayに入れて問題ありません。文字列に変換したければTextDecoderを使います。

const ascii = atob('44OG44K544OI');

const bytes = new Uint8Array(ascii.length);
for (let i = 0; i < bytes.length; i++) {
    bytes[i] = ascii.charCodeAt(i);
}

const decoder = new TextDecoder();
decoder.decode(bytes);
// 'テスト'

以上のように16進数文字列やbase64を利用する場合は複雑になるので可能な場合はNode.jsのBufferを利用した方がいいでしょう。

またはサードパーティライブラリのjs-base64を利用する手段もあります。

(補足)Denoでのバイナリデータ処理

DenoにはNode.jsのBufferクラスはありません。基本的にJavaScript標準のUint8Arrayを利用します。ファイルリーダの戻り値もPromise<Uint8Array>となっています。

バイナリデータと文字列との変換はTextEncoder/TextDecoderを利用します。

上の節で悩むことになったUint8Arrayに不足する機能は標準ライブラリによって補われています。例えばバイナリデータと16進数文字列との変換にはstd/encoding/hexライブラリが利用できます。

base64との変換にはstd/encoding/base64ライブラリが利用できます。

参考文献

34
20
0

Register as a new user and use Qiita more conveniently

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