核心代码
获取图片的像素信息
我们创建一个canvas,渲染图片,然后通过 getImageData 方法拿到图片信息。
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
const image = new Image();
image.onload = function () {
canvas.width = image.width;
canvas.height = image.height;
ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
console.log(imageData);
}
image.src = 'https://cdn.shopifycdn.net/s/files/1/0504/5931/2316/files/Archer_Idle_1.png?v=1640657443';
image.crossOrigin = "Anonymous";
打印出来的图片信息为:
width: 图片宽度height: 图片高度colorSpace: 色彩模式,这里是srgbdata: 色彩信息
我们重点来看看data:
data是个一维数组- 每4个元素构成一个
RGBA像素块 - 每个元素的值范围都是
0-255 RGB代表红绿蓝三原色- A代表透明度(
0是完全透明,255是不透明)
获取像素块索引
我们已知width和height,那么嵌套循环就能获取到每一个像素块索引:
for (let col = 0; col < width; col++) {
for (let row = 0; row < height; row++) {
// 当前像素块相对于图片的索引位置
const pxIndex = (row * width + col);
}
}
获取每个像素块的RGBA信息
我们有像素块的索引,又知道在色彩信息中,每4个元素表示一个像素颜色,那么我们就能获取到每个像素块的颜色信息了:
const pxStartIndex = pxIndex * 4;
const pxData = {
r: data[pxStartIndex],
g: data[pxStartIndex + 1],
b: data[pxStartIndex + 2],
a: data[pxStartIndex + 3]
};
计算裁剪的起点和终点坐标
我们需要将png图片周围的透明区域去掉。所以先判断是否存在色彩:
// 不透明
const colorExist = pxData.a !== 0;
起点和终点初始化值一定要先设置成极限值,也就是两点互换位置:
let startX = width,
startY = height,
endX = 0,
endY = 0;
startX坐标取当前col和startX的最小值endX坐标取当前col和endX的最大值startY坐标取当前row和startY的最小值endY坐标取当前row和endY的最大值 如果之前初始化的时候没有取极限,使用Math.min判断的时候一直会是0
if (colorExist) {
startX = Math.min(col, startX);
endX = Math.max(col, startX);
startY = Math.min(row, startY);
endY = Math.max(row, endY);
}
终点需要再向外扩大1px,否则裁剪的时候会少1px:
endX += 1;
endY += 1;
我们可能并不想完全贴着图片裁剪,所以可以设置一个内边距padding:
startX -= padding;
startY -= padding;
endX += padding;
endY += padding;
优化!更快的计算裁剪点位
上面计算点位需要遍历整张图片的像素,这样在图片偏大的时候,效率方面会有问题。 那我们有什么更好的办法呢?
我们可以单独计算每个方向的偏移,同时执行,然后通过Promise.all统一返回结果。
- 左偏移:
startX左开始,一列一列去找
const getOffsetLeft = () => {
for (let i = 0; i < width; i++) {
for (let j = 0; j < height; j++) {
const pxStartIndex = (j * width + i) * 4;
if (data[pxStartIndex + 3] > 0) {
return Promise.resolve(i);
}
}
}
return Promise.resolve(0);
};
- 上偏移:
startY从左开始,一行一行的查找
const getOffsetTop = () => {
for (let i = 0; i < height; i++) {
for (let j = 0; j < width; j++) {
const pxStartIndex = (i * width + j) * 4;
if (data[pxStartIndex + 3] > 0) {
return Promise.resolve(i);
}
}
}
return Promise.resolve(0);
};
- 右偏移:
endX从右开始,一列一列去找
const getOffsetRight = () => {
for (let i = width - 1; i >= 0; i--) {
for (let j = 0; j < height; j++) {
const pxStartIndex = (j * width + i) * 4;
if (data[pxStartIndex + 3] > 0) {
return Promise.resolve(i);
}
}
}
return Promise.resolve(0);
};
- 下偏移:
endY从右开始,一行一行的查找
const getOffsetBottom = () => {
for (let i = height - 1; i >= 0; i--) {
for (let j = 0; j < width; j++) {
const pxStartIndex = (i * width + j) * 4;
if (data[pxStartIndex + 3] > 0) {
return Promise.resolve(i);
}
}
}
return Promise.resolve(0);
};
- 然后一起执行
const queue = [
getOffsetLeft(),
getOffsetTop(),
getOffsetRight(),
getOffsetBottom()
];
Promise.all(queue).then(res => {
let [startX, startY, endX, endY] = res;
...
})
经测试,一张3000x3000的图片,使用之前方式大概需要200ms,而优化后的方法基本稳定在10ms左右,感觉提升巨大是不是,其实实际使用的时候感觉并不明显,200ms和10ms,相对于加载图片的耗时可以完全忽略不计,如果追求极致的性能可以使用优化后的方法,只是代码量会多一点。
优化后的代码在线地址
裁剪新图
const cropCanvas = document.createElement("canvas");
const cropCtx = cropCanvas.getContext("2d");
cropCanvas.width = endX - startX;
cropCanvas.height = endY - startY;
cropCtx.drawImage(
image,
startX,
startY,
cropCanvas.width,
cropCanvas.height,
0,
0,
cropCanvas.width,
cropCanvas.height
);
cropCanvas.toDataURL()
在线地址
完整代码
async function handleClear() {
const url =
"https://cdn.shopifycdn.net/s/files/1/0343/0275/4948/files/png_7261d2f1-9f99-4972-8e2f-7a00535a9f34.png?v=1634027745";
// const url =
// "https://cdn.shopifycdn.net/s/files/1/0504/5931/2316/files/Archer_Idle_1.png?v=1640657443";
const base64 = await clearImageEdgeBlank(url, 4);
document.getElementById("img").setAttribute("src", base64);
}
/**
* 清楚图片周围空白区域
* @param {string} url - 图片地址或base64
* @param {number} [padding=0] - 内边距
* @return {string} base64 - 裁剪后的图片字符串
*/
function clearImageEdgeBlank(url, padding = 0) {
return new Promise((resolve, reject) => {
// create canvas
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
// create image
const image = new Image();
image.onload = draw;
image.src = url;
image.crossOrigin = "Anonymous";
function draw() {
canvas.width = image.width;
canvas.height = image.height;
ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
console.log(imageData);
const { data, width, height } = imageData;
// 裁剪需要的起点和终点,初始值为画布左上和右下点互换设置成极限值。
let startX = width,
startY = height,
endX = 0,
endY = 0;
/*
col为列,row为行,两层循环构造每一个网格,
便利所有网格的像素,如果有色彩则设置裁剪的起点和终点
*/
for (let col = 0; col < width; col++) {
for (let row = 0; row < height; row++) {
// 网格索引
const pxStartIndex = (row * width + col) * 4;
// 网格的实际像素RGBA
const pxData = {
r: data[pxStartIndex],
g: data[pxStartIndex + 1],
b: data[pxStartIndex + 2],
a: data[pxStartIndex + 3]
};
// 存在色彩:不透明
const colorExist = pxData.a !== 0;
/*
如果当前像素点有色彩
startX坐标取当前col和startX的最小值
endX坐标取当前col和endX的最大值
startY坐标取当前row和startY的最小值
endY坐标取当前row和endY的最大值
*/
if (colorExist) {
startX = Math.min(col, startX);
endX = Math.max(col, startX);
startY = Math.min(row, startY);
endY = Math.max(row, endY);
}
}
}
// 右下坐标需要扩展1px,才能完整的截取到图像
endX += 1;
endY += 1;
// 加上padding
startX -= padding;
startY -= padding;
endX += padding;
endY += padding;
// 根据计算的起点终点进行裁剪
const cropCanvas = document.createElement("canvas");
const cropCtx = cropCanvas.getContext("2d");
cropCanvas.width = endX - startX;
cropCanvas.height = endY - startY;
cropCtx.drawImage(
image,
startX,
startY,
cropCanvas.width,
cropCanvas.height,
0,
0,
cropCanvas.width,
cropCanvas.height
);
// rosolve裁剪后的图像字符串
resolve(cropCanvas.toDataURL());
}
});
}