このページは、【JavaScript】 自力でCanvas画像をBMPに変換する
『パターン2:パレットを生成』のデモページです
ソースコード
HTML
<html lang="ja">
<head>
<meta charset="UTF-8">
<script src="./bitmap-ui.js" type="module"></script>
<style>
#inch{
width: 1in;
height: 1in;
padding: 0;
border: 0;
box-sizing: border-box;
}
</style>
</head>
<body>
<div id="inch"></div>
<p><input type="file" id="file_select"></p>
<p><button id="button">bmp生成</button></p>
<p><canvas id="canvas"></canvas></p>
</body>
</html>
bitmap-ui.js
import {imageDataToBmp} from "./imagedatatobmp.js";
(()=>{
window.addEventListener( "DOMContentLoaded" , ()=>{
// dpi取得
const dpi = document.getElementById("inch").offsetHeight;
start("file_select","canvas","button",dpi)
});
const start = (fileSelectId,canvasId,buttonId,dpm) =>{
const context = document.getElementById(canvasId).getContext("2d");
const image = new Image();
image.onload = ()=>{
context.canvas.width = image.width;
context.canvas.height = image.height;
context.drawImage(image, 0, 0, context.canvas.width, context.canvas.height);
};
image.onerror = ()=>alert("読み込めませんでした");
const reader = new FileReader();
reader.onload = () => image.src = reader.result
document.getElementById(fileSelectId)
.addEventListener("change",function(){
reader.readAsDataURL(this.files[0]);
});
document.getElementById(buttonId)
.addEventListener("click",()=>bitmapDownload(context,dpm));
};
// ビットマップファイルダウンロード
const bitmapDownload = (context,dpi) =>{
const {width,height} = context.canvas;
const imageData = context.getImageData(0,0,width,height).data;
imageDataToBmp( imageData , width , height ,dpi )
.then( buffers => {
const blob = new Blob( buffers, { type: "octet/stream" });
const link = document.createElement("a");
link.href = URL.createObjectURL(blob);
link.download = "image.bmp";
link.click();
URL.revokeObjectURL(link.href);
});
};
})();
imagedatatobmp.js
export {imageDataToBmp};
const imageDataToBmp = (()=>{
"use strict";
// RGBAをRGBに変換(透明度考慮)
const RGBAtoRGB = ( r , g , b , a ) =>{
if( a === 0 ) return [255,255,255];
const opacity = a / 255;
const cb = Math.round(b * opacity + 255 * ( 1 - opacity));
const cg = Math.round(g * opacity + 255 * ( 1 - opacity));
const cr = Math.round(r * opacity + 255 * ( 1 - opacity));
return [cr,cg,cb];
};
// 4バイト合わせ
const widthAdjustBytes = widthBits =>Math.ceil(Math.ceil(widthBits/8)/4) * 4;
// バッファーセットクラス
const SetBuffer = class{
#buffer; #pos=0;
constructor(size) {this.#buffer = new Uint8Array( size );}
setData(value){this.#buffer[this.#pos++] = value;}
get result(){ return this.#buffer;}
}
const Set8 = class extends SetBuffer{ // 1バイト毎
#outCount=0;
constructor(size) {super(size);}
setData(value) {
this.#outCount++;
return super.setData(value);
}
lineEnd(){
const mod = this.#outCount % 4;
if( mod > 0 ) for( let i = 4 - mod; i > 0 ; i -- )
super.setData(0);
this.#outCount = 0;
}
end(){this.lineEnd();}
};
const Set4 = class extends Set8 { // 4bit毎
#highBit4 = -1;
constructor(size) {super(size);}
setData(value){
if( this.#highBit4 === -1 ) { this.#highBit4 = value << 4;}
else{super.setData(this.#highBit4 | value); this.#highBit4=-1;}
}
lineEnd(){
if( this.#highBit4 !== -1 ) this.setData(0);
super.lineEnd();
}
};
const Set1 =class extends Set8{ // 1bit毎
#byte =0; #count=0;
constructor(size) {super(size);}
setData(value){
const byte = this.#byte | value;
this.#count ++;
if( this.#count === 8 ) {
super.setData(byte);
this.#byte = this.#count = 0;return;
}
this.#byte = byte << 1;
}
lineEnd(){
if( this.#count !== 0 ) {
super.setData( this.#byte << (7 - this.#count) );
}
super.lineEnd();
this.#byte = this.#count = 0;
}
};
const Set24 = class extends Set8{ // 3バイト毎
constructor(size) {super(size);}
setData(rgb){
super.setData(rgb[2]);super.setData(rgb[1]);super.setData(rgb[0]);
}
};
// ビットマップデータ部生成
const getBitmapData = imgMgr =>{
const size = widthAdjustBytes(imgMgr.width * imgMgr.bitPerPixel)
* imgMgr.height;
// データセットクラスをセレクト
const setter = (()=>{
switch (imgMgr.bitPerPixel ){
case 24:return new Set24(size);
case 8:return new Set8(size);
case 4:return new Set4(size);
case 1:return new Set1(size);
}
})();
// データ出力
return imgMgr.imageConvert( setter );
};
// imageData.data(DataView) からパレットを抽出
const extractPalette = imageData =>{
const dataLength = imageData.byteLength;
const paletteBuffer = new Uint32Array(256); // パレット用バッファー
let indexStart = 256;
for( let i = 0 ; i < dataLength ; i += 4 ){
const color = imageData.getUint32( i ); // ピクセルRGBA(32bit)をBEで取得
if( paletteBuffer.indexOf(color,indexStart) === -1){ // indexStartから最後まで検索
if( indexStart <= 0) { // 色が256以上ある パレット無効
return null;
}
indexStart --;
paletteBuffer[indexStart]=color;
}
}
if( indexStart === 256 ) return null;
// paletteBufferから使用している部分を切り出し
return indexStart === 0 ? paletteBuffer : paletteBuffer.slice(indexStart);
};
// パレットを有効にするかの判定
const judgementPaletteSize = (palette,imgManager)=>{
if( palette === null ) return null;
const {width,height} = imgManager;
const length = palette.length;
const bitPerPixel = imgManager.lengthToBitPerPixel(palette);
// パレット未使用時サイズ
const rgbFileSize = widthAdjustBytes(width * 24) * height;
// パレット使用時のパレット+ビットマップデータバイト数
const paletteFileSize = widthAdjustBytes(width * bitPerPixel)
* height + length * 4;
// パレット出力とRGB出力のサイズを比較
return ( paletteFileSize > rgbFileSize ) ? null : palette;
};
// パレットをビットマップファイルでの形式に変換(RGBA=>BGR)
const convertPalette = (palette)=>{ // paletteはUint32Array
// Uint32ArrayはLEなので、メモリ上にRGBAはABGRでセットされている
// DataViewで1バイトずつ処理する
const dView = new DataView( palette.buffer );
for( let i = 0 ; i < dView.byteLength ; i+=4 ){
const rgb = RGBAtoRGB( dView.getUint8(i+3) , dView.getUint8(i+2)
, dView.getUint8(i+1) , dView.getUint8(i) );
dView.setUint8(i,rgb[2]);dView.setUint8(i+1,rgb[1]);
dView.setUint8(i+2,rgb[0]);dView.setUint8(i+3,0);
}
return palette;
};
// imageData の管理
const imgManager = class {
#srcData; #width; #height; #palette; #isConverted = false;
constructor( imageData,width,height ) {
this.#width=width;this.#height=height;
this.#srcData = new DataView(imageData.buffer);
// パレット抽出後、パレット有効判定
this.#palette = judgementPaletteSize(extractPalette(this.#srcData),this);
}
get compression(){ return 0;}
get width(){ return this.#width;}
get height(){ return this.#height;}
get bitPerPixel(){return this.lengthToBitPerPixel(this.#palette);}
lengthToBitPerPixel(palette){
if( palette === null ) return 24;
const len = palette.length;
if( len <= 2 ) return 1;
return len < 17 ? 4 : 8;
}
imageConvert( setter ){ // データの列挙
const width = this.#width,height = this.#height,src = this.#srcData;
const palette = this.#palette;
const lineBytes = width * 4;
const func = palette === null
? (pos)=>RGBAtoRGB(src.getUint8(pos),src.getUint8(pos+1)
,src.getUint8(pos+2),src.getUint8(pos+3))
: (pos)=>palette.indexOf(src.getUint32( pos ));
// ボトムアップでピクセルを取得
for( let col = height -1 ; col >= 0 ; col -- ){
for( let row = 0 , pos = col * lineBytes ; row < width ; row ++, pos += 4 )
if( setter.setData( func(pos ) ) === false) break;
if( col !== 0 ) setter.lineEnd();
}
setter.end( );
return setter.result;
}
get palette(){ // ビットマップファイル上での形式に変換したパレット情報を返す
if( this.#palette === null ) {
const buffer = [];buffer.byteLength = 0;
return {buffer:buffer,length:0}; // DataViewを偽装
}
if( this.#isConverted ) return this.#palette;
this.#isConverted = true;
return convertPalette(this.#palette);
}
};
// ヘッダー生成用フォーマット定数
const [U8,U16,U32,I32] = (()=>{
const dp = DataView.prototype;
return [[dp.setUint8,1],[dp.setUint16,2],[dp.setUint32,4],[dp.setInt32,4]];
})();
// ヘッダー生成
const getHeader = (format,values)=> {
const size = format.reduce((a,b)=>a+b[1],0);
const buffer = new DataView(new ArrayBuffer( size ));
values.reduce((a,b,index)=>{
format[index][0].call(buffer,a,b,true);
return a + format[index][1];
},0);
return buffer;
};
// BMPバイナリイメージの生成
return (imageData,width,height,dpi=96) =>{
const dpm = Math.ceil((dpi === 0 ? 96 : dpi) * 39.3701 );
const imgMgr = new imgManager( imageData,width,height );
// データ部生成
const bitmapData = getBitmapData(imgMgr);
// パレットデータ取得
const paletteData = imgMgr.palette;
// BITMAPFILEHEADERのセット
const dataOffset = paletteData.buffer.byteLength + 14 + 40;
const fileSize = bitmapData.buffer.byteLength + dataOffset;
const fileHeader = getHeader( [U8,U8,U32,U16,U16,U32]
,["B".charCodeAt(0) , "M".charCodeAt(0) , fileSize , 0 , 0 ,dataOffset]);
// BITMAPINFOHEADERのセット
const infoHeader = getHeader( [U32,U32,I32,U16,U16,U32,U32,U32,U32,U32,U32]
,[40 , imgMgr.width , imgMgr.height , 1 , imgMgr.bitPerPixel
,imgMgr.compression , 0 , dpm , dpm , paletteData.length , paletteData.length]);
const buffers = [fileHeader.buffer,infoHeader.buffer,paletteData.buffer,bitmapData.buffer];
return new Promise( resolve => resolve( buffers) );
};
})();