现在很多公司都开始用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);
}
}