Reference Source

src/viewer/scene/libs/canvas2image.js

/*
 * Canvas2Image v0.1
 * Copyright (c) 2008 Jacob Seidelin, cupboy@gmail.com
 * MIT License [http://www.opensource.org/licenses/mit-license.php]
 *
 * Modified by @xeolabs to permit vertical flipping, so that snapshot can be taken from WebGL frame buffers,
 * which vertically flip image data as part of the way that WebGL renders textures.
 */

/**
 * @private
 */
const Canvas2Image = (function () {
    // check if we have canvas support
    const oCanvas = document.createElement("canvas"), sc = String.fromCharCode, strDownloadMime = "image/octet-stream",
        bReplaceDownloadMime = false;

    // no canvas, bail out.
    if (!oCanvas.getContext) {
        return {
            saveAsBMP: function () {
            },
            saveAsPNG: function () {
            },
            saveAsJPEG: function () {
            }
        }
    }

    const bHasImageData = !!(oCanvas.getContext("2d").getImageData), bHasDataURL = !!(oCanvas.toDataURL),
        bHasBase64 = !!(window.btoa);

    // ok, we're good
    const readCanvasData = function (oCanvas) {
        const iWidth = parseInt(oCanvas.width), iHeight = parseInt(oCanvas.height);
        return oCanvas.getContext("2d").getImageData(0, 0, iWidth, iHeight);
    };

    // base64 encodes either a string or an array of charcodes
    const encodeData = function (data) {
        let i, aData, strData = "";

        if (typeof data == "string") {
            strData = data;
        } else {
            aData = data;
            for (i = 0; i < aData.length; i++) {
                strData += sc(aData[i]);
            }
        }
        return btoa(strData);
    };

    // creates a base64 encoded string containing BMP data takes an imagedata object as argument
    const createBMP = function (oData) {
        let strHeader = '';
        const iWidth = oData.width;
        const iHeight = oData.height;

        strHeader += 'BM';

        let iFileSize = iWidth * iHeight * 4 + 54; // total header size = 54 bytes
        strHeader += sc(iFileSize % 256);
        iFileSize = Math.floor(iFileSize / 256);
        strHeader += sc(iFileSize % 256);
        iFileSize = Math.floor(iFileSize / 256);
        strHeader += sc(iFileSize % 256);
        iFileSize = Math.floor(iFileSize / 256);
        strHeader += sc(iFileSize % 256);

        strHeader += sc(0, 0, 0, 0, 54, 0, 0, 0); // data offset
        strHeader += sc(40, 0, 0, 0); // info header size

        let iImageWidth = iWidth;
        strHeader += sc(iImageWidth % 256);
        iImageWidth = Math.floor(iImageWidth / 256);
        strHeader += sc(iImageWidth % 256);
        iImageWidth = Math.floor(iImageWidth / 256);
        strHeader += sc(iImageWidth % 256);
        iImageWidth = Math.floor(iImageWidth / 256);
        strHeader += sc(iImageWidth % 256);

        let iImageHeight = iHeight;
        strHeader += sc(iImageHeight % 256);
        iImageHeight = Math.floor(iImageHeight / 256);
        strHeader += sc(iImageHeight % 256);
        iImageHeight = Math.floor(iImageHeight / 256);
        strHeader += sc(iImageHeight % 256);
        iImageHeight = Math.floor(iImageHeight / 256);
        strHeader += sc(iImageHeight % 256);

        strHeader += sc(1, 0, 32, 0); // num of planes & num of bits per pixel
        strHeader += sc(0, 0, 0, 0); // compression = none

        let iDataSize = iWidth * iHeight * 4;
        strHeader += sc(iDataSize % 256);
        iDataSize = Math.floor(iDataSize / 256);
        strHeader += sc(iDataSize % 256);
        iDataSize = Math.floor(iDataSize / 256);
        strHeader += sc(iDataSize % 256);
        iDataSize = Math.floor(iDataSize / 256);
        strHeader += sc(iDataSize % 256);

        strHeader += sc(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); // these bytes are not used

        const aImgData = oData.data;
        let strPixelData = "";
        let c;
        let x;
        let y = iHeight;
        let iOffsetX;
        let iOffsetY;
        let strPixelRow;

        do {
            iOffsetY = iWidth * (y - 1) * 4;
            strPixelRow = "";
            for (x = 0; x < iWidth; x++) {
                iOffsetX = 4 * x;
                strPixelRow += sc(
                    aImgData[iOffsetY + iOffsetX + 2], // B
                    aImgData[iOffsetY + iOffsetX + 1], // G
                    aImgData[iOffsetY + iOffsetX],     // R
                    aImgData[iOffsetY + iOffsetX + 3]  // A
                );
            }
            strPixelData += strPixelRow;
        } while (--y);

        return encodeData(strHeader + strPixelData);
    };

    // sends the generated file to the client
    const saveFile = function (strData) {
        if (!window.open(strData)) {
            document.location.href = strData;
        }
    };

    const makeDataURI = function (strData, strMime) {
        return "data:" + strMime + ";base64," + strData;
    };

    // generates a <img> object containing the imagedata
    const makeImageObject = function (strSource) {
        const oImgElement = document.createElement("img");
        oImgElement.src = strSource;
        return oImgElement;
    };

    const scaleCanvas = function (oCanvas, iWidth, iHeight, flipy) {
        if (iWidth && iHeight) {
            const oSaveCanvas = document.createElement("canvas");
            oSaveCanvas.width = iWidth;
            oSaveCanvas.height = iHeight;
            oSaveCanvas.style.width = iWidth + "px";
            oSaveCanvas.style.height = iHeight + "px";
            const oSaveCtx = oSaveCanvas.getContext("2d");
            if (flipy) {
                oSaveCtx.save();
                oSaveCtx.scale(1.0, -1.0);
                oSaveCtx.imageSmoothingEnabled = true;
                oSaveCtx.drawImage(oCanvas, 0, 0, oCanvas.width, oCanvas.height, 0, 0, iWidth, -iHeight);
                oSaveCtx.restore();
            } else {
                oSaveCtx.imageSmoothingEnabled = true;
                oSaveCtx.drawImage(oCanvas, 0, 0, oCanvas.width, oCanvas.height, 0, 0, iWidth, iHeight);
            }
            return oSaveCanvas;
        }
        return oCanvas;
    };

    return {
        saveAsPNG: function (oCanvas, bReturnImg, iWidth, iHeight, flipy) {
            if (!bHasDataURL) return false;
            const oScaledCanvas = scaleCanvas(oCanvas, iWidth, iHeight, flipy);
            const strMime = "image/png";
            const strData = oScaledCanvas.toDataURL(strMime);
            if (bReturnImg) {
                return makeImageObject(strData);
            } else {
                saveFile(bReplaceDownloadMime ? strData.replace(strMime, strDownloadMime) : strData);
            }
            return true;
        },

        saveAsJPEG: function (oCanvas, bReturnImg, iWidth, iHeight, flipy) {
            if (!bHasDataURL) return false;
            const oScaledCanvas = scaleCanvas(oCanvas, iWidth, iHeight, flipy);
            const strMime = "image/jpeg";
            const strData = oScaledCanvas.toDataURL(strMime);
            // check if browser actually supports jpeg by looking for the mime type in the data uri. if not, return false
            if (strData.indexOf(strMime) != 5) return false;
            if (bReturnImg) {
                return makeImageObject(strData);
            } else {
                saveFile(bReplaceDownloadMime ? strData.replace(strMime, strDownloadMime) : strData);
            }
            return true;
        },

        saveAsBMP: function (oCanvas, bReturnImg, iWidth, iHeight, flipy) {
            if (!(bHasDataURL && bHasImageData && bHasBase64)) return false;
            const oScaledCanvas = scaleCanvas(oCanvas, iWidth, iHeight, flipy);
            const strMime = "image/bmp";
            const oData = readCanvasData(oScaledCanvas), strImgData = createBMP(oData);
            if (bReturnImg) {
                return makeImageObject(makeDataURI(strImgData, strMime));
            } else {
                saveFile(makeDataURI(strImgData, strMime));
            }
            return true;
        }
    };
})();

export {Canvas2Image};