◆ Blob からは FileReader をつかって非同期で変換処理
  ◆ ArrayBuffer, DataURI, text, BinaryString
◆ ArrayBuffer や BinaryString, Uint 系配列 から Blob は Blob のコンストラクタでできる
◆ Uint 系配列の buffer プロパティが ArrayBuffer
◆ ArrayBuffer を Uint 系のコンストラクタに入れるとその TypedArray に変換できる 

Blob とか ArrayBuffer とか JavaScript でバイナリデータを扱うものがあります
色々あって変換するときにどうすればいいんだっけと思うのでまとめ

今回の対象はこれ

  • Blob
  • ArrayBuffer
  • UintXXArray
  • File
  • BinaryString
  • DataURI

UintXXArray は Uint8Array とかのシリーズです

おおまかに分けると

  • Blob
  • File



  • ArrayBuffer
  • UintXXArray



  • BinaryString
  • DataURI

になります
ArrayBuffer のグループと BinaryString は近めの関係です


簡単な変換する方法の図
js-bin-translate


いろいろ見づらいので詳しく書いていきます

Blob と File

まずは Blob と File について

Blob 型はバイナリデータを保持していますが 中身の場所を指定して値を取り出すことができないものです
Blob.prototype.slice や Blob.prototype.size というプロパティがあるのでサイズの確認や切り出しはできます

実際に中身を見るには Uint8Array などの型にする必要があります

File は Blob を継承しています
Blob にファイル名や最終更新日の情報が追加されてます
基本は Blob と同じように扱って他の型に変換します

<追記>
あまり利用するシーンはありませんが Blob から File への変換は File のコンストラクタに Blob 型を指定することで変換できます

この記事も参照

ArrayBuffer と UintXXArray

ArrayBuffer はバイナリデータでデータがずらーっと並んでるようなものです
UintXXArray は ArrayBuffer を決まったバイト数で配列に格納してプログラムで扱えるようにしたものです

Uint8Array だと 8bit なので 1バイト分ずつ ArrayBuffer から取り出して数値型として配列に入っています
それぞれの要素は 0 から 255 の範囲です

Uint16Array だと 16bit なので 2 バイトずつになります
0 ~ 65535 の範囲です

Uint32Array は 32bit で 4 バイト 0 ~ 4294967295 までです

ab


UintXXArray から ArrayBuffer を取得するときは buffer プロパティにアクセスするだけです
逆に ArrayBuffer を UintXXArray にしたいときは 変換したい種類のコンストラクタに ArrayBuffer を入れるだけです
var ua = Uint8Array.of(1,2,3,4,5,6)
var buf = ua.buffer

ua
// [1, 2, 3, 4, 5, 6]

buf
// ArrayBuffer {}

new Uint8Array(buf)
// [1, 2, 3, 4, 5, 6]


16 と 32 もやってみます
new Uint16Array(buf)
// [513, 1027, 1541]

new Uint32Array(buf)
Uncaught RangeError: byte length of Uint32Array should be a multiple of 4(…)

32 の方ではエラーになりました
32 は 8 の 4 倍で 32bit (4byte) でまとまりになります
元が Uint8Array で長さ 6 の配列なので Uint32Array に変換するにはサイズが合わないのでエラーです

Uint8Array の長さが 4 の倍数になってる必要があります

16 の方も 8 の 2 倍なので Uint8Array のサイズが奇数だと
var ua = Uint8Array.of(1,2,3,4,5,6,7)
var buf = ua.buffer
new Uint16Array(buf)
Uncaught RangeError: byte length of Uint16Array should be a multiple of 2(…)

同じくエラーになります

Uint8Array で長さ 4 のデータにすると
var u8 = Uint8Array.of(1,2,3,4)
var buf = u8.buffer
var u16 = new Uint16Array(buf)
var u32 = new Uint32Array(buf)

u8
// [1, 2, 3, 4]
u16
// [513, 1027]
u32
// [67305985]

とちゃんと変換できます

このときの 2 バイト 4 バイトのデータですが
("0000000000000000" + 513..toString(2)).substr(-16)
// "0000001000000001"

という 2 進数のデータです
8 文字ずつに分けると

00000010 00000001

2 と 1 になっています
元が [1, 2] なので逆です

もうひとつも
("0000000000000000" + 1027..toString(2)).substr(-16)
// "0000010000000011"

00000100 00000011
4 と 3 です

リトルエンディアンなので 2 バイトで取り出すと 逆向きの 00000100 と 00000011 の順番になってるんだと思います


32 バイトの場合も
("00000000000000000000000000000000" + 67305985..toString(2)).substr(-32)
// "00000100000000110000001000000001"

8 つずつにすると

00000100 00000011 00000010 00000001

4 3 2 1 となっていて全体が逆になってます

BinaryString と DataURI

BinaryString はバイナリデータを文字列にしたもの
単純に String.fromCharCode で 0 ~ 255 の値を文字にしてつなげたもの
制御文字になったりもするので表示するには向かないです
内部的な処理で配列より文字列でもっておいた方が効率がいいときに使ったり base64 変換するときの途中で作られたりします

DataURI は URL にデータを書けるようにする DataURI Scheme の文字列です
data:text/html, <html contenteditable>

こういうの

正式な書き方は
dataurl    := "data:" [ mediatype ] [ ";base64" ] "," data
mediatype  := [ type "/" subtype ] *( ";" parameter )
data       := *urlchar
parameter  := attribute "=" value

data: って書いてファイルの種類を mimetype で書きます
charset="utf-8" みたいなパラメータもかけるみたい

その後に base64 なら ;base64 って書いて , で区切ってその後に好きなデータを書けるという構文


最小サイズの gif
data:image/gif;base64,R0lGODlhAQABAPAAAP///wAAACwAAAAAAQABAAACAkQBADs=

この DataURI で base64 を使う場合に BinaryString が関係してきます


base64 はバイナリデータを 64 文字の英数字とちょっと記号で表したものです
制御文字とか入らないようにしてるので同じデータを表すにも必要なデータ量がちょっと増えます

この base64 は JavaScript だと BinaryString から作ります

普通の文字列 abc を使ってますが方法はこうなります
var b_str = "abc"
btoa(b_str)
// YWJj

btoa 関数に BinaryString を渡すと base64 文字列で返って来ます
btoa は binary to ascii の略らしいです

逆の atob (ascii to binary) もあって base64 文字列を BinaryString に変換できます
上で出力された UWJj を BinaryString に変換してみます
var a_str = "YWJj"
atob(a_str)
// abc

ちゃんと戻ってます

ArrayBuffer → Blob

ArrayBuffer を Blob にするときは Blob のコンストラクタを使います
var u8 = new Uint8Array(16).map((e,i) => i*16)
var blob = new Blob([u8.buffer], {type: "application/octet-binary"})
console.log(blob.size)
// 16

第一引数には 配列で ArrayBuffer を渡します
第二引数では type に mimetype を指定します

blob のサイズで Uint8Array のサイズ 16 が表示されています

UintXXArray → Blob

UintXXArray からでも Blob コンストラクタを使います
var u8 = Uint8Array.of(1,2,3,4,5)
var blob = new Blob([u8], {type: "application/octet-binary"})
console.log(blob.size)
// 5

注意は Uint8Array って配列だからといってそのまま第一引数に渡してしまうとダメということ
var u8 = Uint8Array.of(1,2,3,4,5)
var blob = new Blob(u8, {type: "application/octet-binary"})
console.log(blob.size)
// 5

read(blob).arrayBuffer().then(e => {console.log(new Uint8Array(e))})
// [49, 50, 51, 52, 53]

サイズは一緒ですが 1,2,3,4,5 が文字列として扱われてしまって この Blob から取り出した Uint8Array は [49, 50, 51, 52, 53] になっています
49 は "1" の char code です

↑の最後の行の read 関数などは自作関数で Uint8Array にしてるだけです

BinaryString → Blob

文字列の場合も同じで Blob コンストラクタにいれるだけ
var blob = new Blob(["abcd"], {type: "application/octet-binary"})
console.log(blob.size)
// 4

Blob →

Blob から変換するには FileReader を使う必要があります
var fr = new FileReader()
fr.onload = eve => {
console.log(fr.result)
}
fr.onerror = eve => {
console.error(fr.error)
}
fr.readAsArrayBuffer()

のようにします

Blob からの変換は絶対に非同期になるので注意が必要です

readAsArrayBuffer のところに readAsText や readAsDataURL もありどれを実行するかで結果 (result プロパティ) が変わります
readAsBinaryString を使って BinaryString にも変換できますが 一応このメソッドは 4 年前の時点で廃止になってるようです
あまり積極的に使わない方が良さそうです
そもそも BinaryString を作るときはたいてい base64 が欲しいときなので readAsDataURL を使えば必要になることはそうないはずです

BinaryString 自体が欲しい時に自分で変換するなら
var pstr1 = read(blob).binaryString()
var pstr2 = read(blob).arrayBuffer().then(e => Array.from(new Uint8Array(e), e => String.fromCharCode(e)).join(""))
Promise.all([pstr1, pstr2]).then(e => {
    console.log(e[0] === e[1])
})
// true

ArrayBuffer と取り出して pstr2 の then の中にあるような処理をすればいいです

<追記>
readAsBinaryString は一度仕様から削除されましたが後方互換性のために再導入されています
readAsArrayBuffer が推奨されていますが 経緯的に削除される予定はなさそうなので 頑張って避けるほどでもなさそうです

Blob に text や arrayBuffer メソッドができたので FileReader を使わないで済むことが多くなりました

この記事も参照

Uint8Array → BinaryString

それぞれの要素の数値を char code として String.fromCharCode で文字列化してつなげるだけです
var u8 = Uint8Array.of(97,98)
Array.from(u8, e => String.fromCharCode(e)).join("")
// "ab"

BinaryString → Uint8Array

一文字一文字に charCodeAt して取り出した数値を配列にします
var str = "ab"
Uint8Array.from(str.split(""), e => e.charCodeAt(0))
// [97, 98]


まとめた関数


関数にするまでもないのもありますが 方法をまとめて書くためということで

Blob のタイプは application/octet-stream にしてます
// BinaryString -> Uint8Array
Uint8Array.fromBinaryString = function(str){
    return Uint8Array.from(str.split(""), e => e.charCodeAt(0))
}

// Uint8Array -> BinaryString
Uint8Array.prototype.binaryString = function(){
    return Array.from(this, e => String.fromCharCode(e)).join("")
}

// BinaryString -> DataURI
function bStr2dataURI(b_str){
return "data:application/octet-stream;base64," + btoa(b_str)
}

// DataURI -> BinaryString
function dataURI2bStr(data){
return atob(data.split(",")[1])
}

// UintXXArray -> ArrayBuffer
function toArrayBuffer(ua){
return ua.buffer
}

// ArrayBuffer -> Uint8Array
ArrayBuffer.prototype.asUint8Array = function(){
return new Uint8Array(this)
}
// ArrayBuffer -> Uint16Array
ArrayBuffer.prototype.asUint8Array = function(){
return new Uint16Array(this)
}
// ArrayBuffer -> Uint32Array
ArrayBuffer.prototype.asUint8Array = function(){
return new Uint32Array(this)
}

// BinaryString, UintXXArray, ArrayBuffer -> Blob

function toBlob(val){
return new Blob([val], {type: "application/octet-stream"})
}

// Blob -> ArrayBuffer, BinaryString, DataURL, text

function read(blob){
var fr = new FileReader()
var pr = new Promise((resolve, reject) => {
fr.onload = eve => {
resolve(fr.result)
}
fr.onerror = eve => {
reject(fr.error)
}
})

return {
arrayBuffer(){
fr.readAsArrayBuffer(blob)
return pr
},
binaryString(){
fr.readAsBinaryString(blob)
return pr
},
dataURL(){
fr.readAsDataURL(blob)
return pr
},
text(){
fr.readAsText(blob)
return pr
},
}
}