このページは、【JavaScript】 自力でCanvas画像をBMPに変換する
『パターン1:画像データをRGBの3バイトで生成』のデモページです


画像ファイル(jpg,png)を読み込み、BMPで出力します。


ソースコード

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>
    <p>画像ファイル(jpg,png)を読み込み、BMPで出力します。</p>
    <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 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 = new Set24( size );
        return imgMgr.imageConvert( setter );
    };

        // imageData の管理
    const imgManager = class {
        #srcData; #width; #height;

        constructor( imageData,width,height ) {
            this.#width=width;this.#height=height;
            this.#srcData = imageData;
        }
        get compression(){ return 0;}
        get width(){ return this.#width;}
        get height(){ return this.#height;}
        get bitPerPixel(){ return 24;}
        imageConvert( setter ){ // データの列挙
            const width = this.#width,height = this.#height,src = this.#srcData;
            const lineBytes = width * 4;
                // ボトムアップでピクセルを取得
            for( let col = height -1 ; col >= 0 ; col -- ){
                for( let row = 0 , pos = col * lineBytes ; row < width ; row ++, pos += 4 )
                    if( setter.setData( RGBAtoRGB(src[pos] , src[pos+1],src[pos+2],src[pos+3] ) ) === false)
                        break;
                if( col !== 0 ) setter.lineEnd();
            }
            setter.end( );
            return setter.result;
        }
        get palette(){ // パレット情報を返す
            const buffer = [];buffer.byteLength = 0;
            return {buffer:buffer,length:0}; // TypedArrayを偽装
        }
    };

    // ヘッダー生成用フォーマット定数
    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) );

    };

})();