Google Apps Script(GAS)でPNGファイル自動生成したいと思ったところ、当然ながら<canvas>が無いので、Javascriptのようには行かないことがわかりました。
しかし、ビットマップのBlobを作ればBlob.getAs(mineTipe)メソッドでPNGに変換することは出来ます。
というわけで、画像データからビットマップのBlobを生成する関数を作りました。
#ソースコード
24bitフルカラー固定です。
/*
dataは左上から右下に向かってのピクセルの色情報を持つUint8Array。色はRGBAの4バイト。
width, heightは横縦のピクセル数。
*/
function toBMP( data, width, height ) {
const RGBA_BYTES = 4;
const BMP_BYTES_PER_DOT = 4;
const BYTES_PER_DOT = 4;
if ( data == null || width == null || height == null ) {
throw new Error("引数が不足しています。");
}
data = Uint8ClampedArray.from( data.map( v => ( v >>> 0 ) & 0xff) );
if ( data.length != ( height * width * RGBA_BYTES ) ) {
throw new Error("データの長さが画像サイズと一致しません。");
}
const rowCount = height;
const rowByteCount = width * RGBA_BYTES;
const paddingCount = ( BMP_BYTES_PER_DOT - ( rowByteCount % BMP_BYTES_PER_DOT ) ) % BMP_BYTES_PER_DOT;
const imageDataSize = paddingCount == 0 ? data.length : ( rowByteCount + paddingCount ) * rowCount;
const imageData = new Uint8ClampedArray( imageDataSize );
const paletteData = new Uint8ClampedArray( 0 ); //フルカラーなら要らない
for ( let rowIndex = rowCount -1, j = 0; rowIndex >= 0; rowIndex-- ) {
const offset = rowIndex * rowByteCount;
for ( let i = offset, end = rowByteCount + offset; i < end; ) {
const [ red, green, blue, alpha ] = data.slice( i, i += BYTES_PER_DOT );
for (const ori of [ blue, green, red ]) {
const color = (ori * alpha / 0xff) | 0;
imageData[ j++ ] = color;
}
}
for ( let i = 0; i < paddingCount; i++ ) {
imageData[ j++ ] = 0;
}
}
const paletteDataSize = 0;
const informationHeaderSize = 40;
const fileHeaderSize = 14;
const imageDataOffset = fileHeaderSize + informationHeaderSize + paletteDataSize;
const fileSize = fileHeaderSize + informationHeaderSize + paletteDataSize + imageDataSize;
const bmpData = new Uint8Array( fileSize );
let cursor = 0;
// Bitmap File Header //
for ( const bfType of "BM".split("").map( ch => ch.charCodeAt( 0 ) ) ) {
bmpData[ cursor++ ] = bfType;
}
for ( const bfSize of UnsignedLong( fileSize ) ) {
bmpData[ cursor++ ] = bfSize;
}
for ( const bfReserved1 of UnsignedInt( 0 ) ) {
bmpData[ cursor++ ] = bfReserved1;
}
for ( const bfReserved2 of UnsignedInt( 0 ) ) {
bmpData[ cursor++ ] = bfReserved2;
}
for ( const bfOffBits of UnsignedLong( imageDataOffset ) ) {
bmpData[ cursor++ ] = bfOffBits;
}
// Bitmap Information Header //
for ( const biSize of UnsignedLong( informationHeaderSize ) ) {
bmpData[ cursor++ ] = biSize;
}
for ( const biWidth of Long( width ) ) {
bmpData[ cursor++ ] = biWidth;
}
for ( const biHeight of Long( height ) ) {
bmpData[ cursor++ ] = biHeight;
}
for ( const biPlanes of UnsignedInt( 1 ) ) {
bmpData[ cursor++ ] = biPlanes;
}
for ( const biBitCount of UnsignedInt( 24 ) ) {
bmpData[ cursor++ ] = biBitCount;
}
for ( const biCompression of UnsignedLong( 0 ) ) {
bmpData[ cursor++ ] = biCompression;
}
for ( const biSizeImage of UnsignedLong( imageDataSize ) ) {
bmpData[ cursor++ ] = biSizeImage;
}
for ( const biXPixPerMeter of Long( width ) ) {
bmpData[ cursor++ ] = biXPixPerMeter;
}
for ( const biYPixPerMeter of Long( height ) ) {
bmpData[ cursor++ ] = biYPixPerMeter;
}
for ( const biClrUsed of UnsignedLong( 0 ) ) {
bmpData[ cursor++ ] = biClrUsed;
}
for ( const biCirImportant of UnsignedLong( 0 ) ) {
bmpData[ cursor++ ] = biCirImportant;
}
// Palette Data //
for ( const palette of paletteData ) {
bmpData[ cursor++ ] = palette;
}
// Image Data //
for ( const image of imageData ) {
bmpData[ cursor++ ] = image;
}
const blob = Utilities.newBlob( bmpData, MimeType.BMP );
//Javascriptでは下記の方法でBlobが作れる。
// const blob = new Blob( [ bmpData.buffer ], { type: "image/bmp" } );
return blob;
}
最後はスプレッド構文にしたら関数呼び過ぎで落ちてしまったので、for文たくさんに置き換えましたけど、もうちょっと上手い書き方がありそうですね。
#参考
Bitmapファイルフォーマット
http://www.umekkii.jp/data/computer/file_format/bitmap.cgi
BMP ファイルフォーマット
https://www.setsuki.com/hsp/ext/bmp.htm