超大图

425 阅读2分钟

现在很多公司都开始用electron做桌面端,其中不乏一些对超大图片查看的需求。

展示

一般展示图片可以用image 或者canvas,然后当显示超大图的时image等待时间真的打扰了。那么能不能将一张大图分开来加载显示呢,同时在加载时不阻塞主进程。那么就用canvas

注意:canvas有像素限制。如下:

  • 在chrome 和 Edge中,
    • canvas矩形的单边最大长度不能超过 65535,并且总像素面积不能超过 268421360 平方像素,也就是说如果矩形的长边是 65535 ,那么短边不能超过4096,如果超出这些限制,则不能正常显示。
  • 在FireFox中
    • 一般情况下矩形的长边不超过 32767 ,总像素面积不超过125w,但是也有例外:比如宽高设置为 width="3890" height="32133" 虽然满足条件但是不能显示,具体原因不明。

于是搜索一波后,写了demo。(react、vue版本自行改造):raw.bmp 为超大图片

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, init ial-scale=1.0">
    <title>bigImg</title>
</head>
<style>
    body{
        overflow: scroll;
    }
    .grid {
        overflow: scroll;
        display: grid;
        grid-template-columns: 1fr 1fr 1fr;
    }

    canvas {
        width: 4288px;
        height: 3225px;
        border: 1pc solid gold;
    }

    button {
        font-size: 50px;
        width: 200px;
        height: 100px;
    }

    p{
        font-size: 50px;
    }
</style>

<body>
    <button onclick="cut()">切片</button>
    <p>完成切片后点击黄格子就可以显示出响应的部分了,可按需做懒加载等等</p>
    <div class="grid"></div>
</body>

<script>
    const imageWidth = 21440, imageHeight = 16128, chunkWidth = 4288, chunkHeight = 3225;
    
    const worker = new Worker("offscreencanvas.js");
    worker.onmessage = (e) => {
        switch (e.data.type) {
            case 'runChunk':
                console.log('完成切片===', e.data.positionArr, new Date().valueOf());
                generateCanvas(e.data.positionArr, e.data.totalChunks);
                // 可根据结果生成canvas数量
                break;
            case 'draw':
                console.log('生成imageBitmap===', e.data, new Date().valueOf());
                drawInCanvase(e.data)
                break;
        }
    }

    // 切片
    function cut() {
        worker.postMessage({
            msg: 'init',
            cutParam: {
                imageWidth,
                imageHeight,
                chunkWidth,
                chunkHeight,
            }
        })
    }

    // 生成绘制数据
    function draw(e) {
        console.log('开始生成imageBitmap===', e, e.dataset, new Date().valueOf());
        worker.postMessage({ msg: 'draw', canvasi: e.dataset.canvasi, canvasj: e.dataset.canvasj })
    }

    // 绘制
    function drawInCanvase(drawData) {
        const ctxx = document.getElementById(`canvas${drawData.canvasi}${drawData.canvasj}`).getContext('bitmaprenderer');
        ctxx.transferFromImageBitmap(drawData.imageBitmap)
    }

    // 生成canvas
    function generateCanvas(positionArr, totalChunks) {
        if (positionArr.length > 0) {
            let gridTemplateColumns = '', canvasHtml = '';
            // 设置容器样式
            positionArr[0].forEach(col => {
                gridTemplateColumns += `1fr `
            })
            // 生成canvas
            positionArr.forEach((position, i) => {
                position.forEach((val, j) => {
                    if (val === 1) {
                        canvasHtml += `<canvas id="canvas${i}${j}" onclick="draw(this)" data-canvasi="${i}" data-canvasj="${j}" ></canvas>`
                    }
                });
            })
            const gridEl = document.querySelector('.grid');
            gridEl.style.gridTemplateColumns = gridTemplateColumns;

            gridEl.insertAdjacentHTML('beforeend', canvasHtml)
        }
    }
</script>

</html>

子线程

将计算的耗时任务给worker,防止主线程阻塞。

注意:

  • worker有兼容性问题,使用前注意一下。
  • 如果不支持worker,就在主线程中使用img.src。
  • 在worker中使用fetch拉取图片会比 img.src快,因为 img.src,会包含完整的pixels data,这里仅仅是fetch,所以时间上有较大的差距。
var chunksData = null, chunksWithoutCtx = []
// type Chunk = {
//     x: number;
//     y: number;
//     width: number;
//     height: number;
//     ctx: OffscreenCanvasRenderingContext2D;
// };

// type TwoDimChunks = Chunk[][]
// type ChunkWithoutCtx = Omit<Chunk, 'ctx'>;


// 算商和余
function calcRemainder(dividend, divisor) {
    const quotient = Math.floor(dividend / divisor);
    const remainder = dividend % divisor;

    return {
        quotient: remainder > 0 ? quotient + 1 : quotient,
        remainder: remainder === 0 ? divisor : remainder,
    };
}

/**
 * 分片算法
 * prop
 * {
 *     imageWidth: number,
 *     imageHeight: number,
 *     chunkWidth: number,
 *     chunkHeight: number,
 * }
 * return ChunkWithoutCtx[][]
 */

function make2DimChunks(props) {
    const chunks = [], positionArr = [];
    let totalChunks = 0;
    const {
        imageHeight, imageWidth, chunkWidth, chunkHeight,
    } = props;
    const { quotient: rowNum, remainder: rowRemainder } = calcRemainder(imageHeight, chunkHeight);
    const { quotient: colNum, remainder: colRemainder } = calcRemainder(imageWidth, chunkWidth);

    for (let i = 0; i < rowNum; i += 1) {
        chunks.push([]);
        positionArr.push([]);
        for (let j = 0; j < colNum; j += 1) {
            const ctxObj = {
                x: j * chunkWidth,
                y: i * chunkHeight,
                width: j === colNum - 1 ? colRemainder : chunkWidth,
                height: i === rowNum - 1 ? rowRemainder : chunkHeight,
            };
            chunks[i].push(ctxObj);
            positionArr[i].push(1);
            totalChunks++
        }
    }

    return { chunks, positionArr, totalChunks };
}



// 获取pixel数据
// function getPixel(x: number, y: number, options: { chunkWidth, chunkHeight: number }) {
function getPixel(x, y, options) {
    const rowIndex = Math.floor(y / options.chunkHeight);
    const colIndex = Math.floor(x / options.chunkWidth);

    // 找到对应的chunk
    const ctxObj = twoDimChunks?.[rowIndex]?.[colIndex];

    if (ctxObj) {
        // 通过getImageData取色
        return Array.from(ctxObj.ctx.getImageData(x - ctxObj.x, y - ctxObj.y, 1, 1).data).slice(0, 3);
    }

    return [0, 0, 0];
}


// 运行切片
function runChunk(cutParam) {
    const cutData = make2DimChunks(cutParam);
    chunksWithoutCtx = cutData.chunks
    fetch("raw.bmp").then((resp) => {
        console.log('开始切片===', new Date().valueOf());
        return resp.blob()
    }).then((blob) => createImageBitmap(blob)).then((imageBitmap) => {
        // 拼上 canvas 2d context,便于之后取色
        chunksData = chunksWithoutCtx.map((row) => row.map((chunk) => {
            // 若 存在兼容性问题,可以使用普通的dom canvas代替
            const offscreen = new OffscreenCanvas(chunk.width, chunk.height);
            const tempCtx = offscreen.getContext('2d');
            tempCtx.drawImage(
                imageBitmap,
                chunk.x, chunk.y, chunk.width, chunk.height,
                0, 0, chunk.width, chunk.height,
            );
            const offscreenImageBitmap = offscreen.transferToImageBitmap();
            return { ...chunk, ctx: tempCtx, offscreen, offscreenImageBitmap }
        }));
        postMessage({ type: 'runChunk', positionArr: cutData.positionArr, totalChunks: cutData.totalChunks })
    });
}

// 绘制
function draw(params) {
    const curImageBitmap = chunksData[params.data.canvasi][params.data.canvasj].offscreenImageBitmap;
    postMessage({ type: 'draw', imageBitmap: curImageBitmap, canvasi: params.data.canvasi, canvasj: params.data.canvasj });
}

onmessage = function (e) {
    if (e.data.msg == 'init') {
        runChunk(e.data.cutParam);
    } else if (e.data.msg == 'draw') {
        draw(e);
    }
}