Web|JavaScript原生实现图集分解并下载功能教程(二、实现图集切割及下载)

-前言-
上篇我们把文件上传的Html文件写好了,也把基本的读取图片数据写完了,本篇就具体如何实现分解来详解。
完整项目地址:https://github.com/dengxuhui/ImagePackerWeb
如果想直接使用该功能的同学:http://dengxuhui.cn/
-正文-
当我们点击”点击分解图片“按钮时会触发onClick方法

this.btnUpload.onclick = function (e) { if (e.currentTarget != $this.btnUpload) return; var dropzone = window.DropZoneLogic.dropzone; var len = dropzone.files.length; $this.ctx.clearRect(0,0,$this.ctx.width,$this.ctx.height); $this.isSpliting = true; for (var i = 0; i < len; ++i) { $this.saveFileToCanvas(dropzone.files[i]); } }/** * 保存数据 * @param {File} file */ saveFileToCanvas(file) { var $this = this; this.preFix = file.name.split(".")[0]; createImageBitmap(file).then((data) => { $this.startSplit(data, $this); }); }

触发onclick后我们通过canvas为载体,将图片文件的数据绘制到canvas上,然后通过读取像素数据来划分图像数据。
这里需要注意的是createImageBitmap返回一个ImageBitmap数据对象。这是一个异步方法,返回的是Promise状态。
接下来一步就开始真正分解图片了。
绘制图集到Canvas
首先我们将整张图集先绘制到Canvas上,这一步是为了通过canvas绘制的图形方便获取每个像素数据。
//将image对象绘制到cavans上 $this.ctx.drawImage(data, 0, 0); var w = data.width > ViewLogic.WIDTH ? ViewLogic.WIDTH : data.width; var h = data.height > ViewLogic.HEIGHT ? ViewLogic.HEIGHT : data.height; $this.atlasH = h; $this.atlasW = w;

这里需要注意一点,默认我们设置的Canvas最大尺寸为2048x2048,当图片大于2048我们还是去2048,这时候图片会被截取,另外2048大小的图集恐怕目前这个算法跑也会跑1很多钟。
使用一个二维数组来标识每个像素点是否有颜色
接下来我们通过获取ImageData中每个像素的数据来判定每个像素点是否有颜色,这也是我们用来区分碎图的边缘的评判标准。
getColors($this) { var has = []; var count; for (var i = 0; i < $this.atlasW; ++i) { has[i] = []; for (var j = 0; j < $this.atlasH; ++j) { var piexel = $this.ctx.getImageData(i, j, 1, 1); count = 0; if (piexel.data[0] < 4) count++; if (piexel.data[1] < 4) count++; if (piexel.data[2] < 4) count++; if (piexel.data[3] < 3 || (count > 2 && piexel.data[3] < 30)) has[i][j] = false; else has[i][j] = true; } } console.log("GET Colors Complete"); return has; }

【Web|JavaScript原生实现图集分解并下载功能教程(二、实现图集切割及下载)】使用这个方法有两个缺点:
  • 运行时间很慢,每个像素点都会通过2DCanvas的getImageData去获取,比较慢
  • 通过这种标识方法来标识碎图边缘会让一些本来是整图只是每个图间隔有一定距离的图也被切割,典型的有美术字图片。
下面贴出相对于之上的优化代码,通过优化速度从秒级分解直接降到毫秒级,可见我们直接调用Canvas的API是多么耗时
//我们一次性就可以获取所有像素区域数据,然后通过计算获取数组下标索引即可,不必每次使用Canvas方法 getColorsNew($this){ $this.btnUpload.innerText = "正在解析像素...."; var has = []; var count; var sT = new Date().getTime(); var allPixel = $this.ctx.getImageData(0,0,$this.atlasW,$this.atlasH); for (var i = 0; i < $this.atlasW; ++i) { has[i] = []; for (var j = 0; j < $this.atlasH; ++j) { // var pixel = $this.ctx.getImageData(i, j, 1, 1); //计算公式:(y * width + x) * 4 4数组中每4个数据表示一个像素的数据 var startIndex = (j * $this.atlasW + i) * 4; count = 0; if (allPixel.data[startIndex] < 4) count++; if (allPixel.data[startIndex + 1] < 4) count++; if (allPixel.data[startIndex + 2] < 4) count++; if (allPixel.data[startIndex + 3] < 3 || (count > 2 && allPixel.data[startIndex + 3] < 30)) has[i][j] = false; else has[i][j] = true; } } var eT = new Date().getTime(); //结果表明运行时间成指数及降低,这也让我们记住直接调用Canvas的API是很慢的方法,最好都讲数据同步到本地再使用 console.log("用时:" + (eT - sT) + "毫秒"); console.log("GET Colors Complete"); return has; }

划分每个碎图的矩形包围盒区域
在这一步之前我们先新建一个简单的Rectangle对象,来保存像素点区域的x,y,width,height属性
class Rectangle { constructor(x = 0, y = 0, width = 0, height = 0) { var $this = this; $this.x = x; $this.y = y; $this.width = width; $this.height = height; } }

接下来我们就可以从左上角0,0点开始利用一个嵌套循环来遍历每个矩形点来划分碎图区域
var rects = []; //Rectangle Array var rect = null; //Rectangle Pointer for (var i = 0; i < $this.atlasW; ++i) { for (var j = 0; j < $this.atlasH; ++j) { if ($this.isExist(colors, i, j)) { rect = $this.getRect(colors, i, j); if (rect.width > 5 && rect.height > 5) { rects.push(rect); } } } }

首先我们每取得一个坐标都去判定这个坐标是否是有颜色的像素,如果在图集范围内就去colors二维数组中取有没有这个颜色
isExist(colors, x, y) { if (x < 0 || y < 0 || x >= colors.length || y >= colors[0].length) return false; return colors[x][y]; }

当取得有这个坐标像素点是有颜色或者还没有被使用,就进行下一步,确定这个碎图的矩形范围。
getRect(colors, x, y) { var rect = new Rectangle(x, y, 1, 1); var flag; do { flag = false; while (this.R_Exist(colors, rect)) { rect.width++; flag = true } while (this.D_Exist(colors, rect)) { rect.height++; flag = true } while (this.L_Exist(colors, rect)) { rect.width++; rect.x--; flag = true } while (this.U_Exist(colors, rect)) { rect.height++; rect.y--; flag = true } } while (flag); this.clearRect(colors, rect); rect.width++; rect.height++; return rect; }

这一步我们通过从目标点x,y开始,首先向右检测扩张,一旦遇到又侧像素点无颜色停止,同理接下来这样去检测它的下方、左方、上方。需要注意的是检测左方或者上方的时候,如果有像素,rectangle的x,y坐标也要相应的挪一个像素。
当循环完成后,我们把rect中的像素从colors二维数组中清除,以确保rectangle的唯一性。
下面只贴出右侧像素检测的方法,其余方法基本相同
R_Exist(colors, rect) { var right = rect.x + rect.width; if (right >= colors.length || rect.x < 0) return false; for (var i = 0; i < rect.height; i++) { if (this.isExist(colors, right + 1, rect.y + i)) return true; } return false; }

最后我们得到一个rect对象,来标识碎图在图集中的像素区域,我们这里将那种小于5像素的碎图直接抛弃不要。这样我们就得到一个在图集中的碎图矩形区域数组。接下来就需要将每张图单独从之前获得的信息中切出来单独保存起来。
每个碎图数据获取
上一步拿到了每个碎图的矩形区域之后,这一步就比较简单勒
var imageDataAry = []; for (var i = 0; i < rects.length; ++i) { var data = https://www.it610.com/article/$this.ctx.getImageData(rects[i].x, rects[i].y, rects[i].width, rects[i].height); imageDataAry.push(data); } $this.ctx.clearRect(0, 0, ViewLogic.WIDTH, ViewLogic.HEIGHT);

我们分别从当前canvas中获取每个区域的像素数据,然后保存到imageDataAry中。
绘制每个碎图并保存及下载
图片下载的方法有很多,Js没有直接下载文件的接口,所以有很多变种的方法实现,这里我们使用标签的下载实现方式。
因为我源代码中还利用了其它方式下载,所以我们这里新建一个downloadMethodByCreateHerfA方法来标识通过标签下载文件。
downloadMethodByCreateHerfA(imageDataAry) { var $this = this; var dLink = document.createElement("a"); for (var i = 0; i < imageDataAry.length; ++i) { $this.canvas.width = imageDataAry[i].width; $this.canvas.height = imageDataAry[i].height; $this.ctx.putImageData(imageDataAry[i], 0, 0); var imgUrl = $this.canvas.toDataURL("image/png", 1); dLink.download = $this.preFix + "_" + i; dLink.href = https://www.it610.com/article/imgUrl; dLink.dataset.downloadurl = ["image/png", dLink.download, dLink.href].join(":"); document.body.appendChild(dLink); dLink.click(); } document.body.removeChild(dLink); }

这里我们通过每个ImageData将其绘制到Canvas中,然后通过Canvas中的toDataURL方法把ImageData转换为base64数据,接着我们通过新建的标签,然后自动触发自动下载。
-总结-
至此图集从上传到下载的全过程就算写完了,功能是基本能达到,不过也来说说如上实现的几个缺点
  • 图集分解后多个图片浏览器会报提示是否下载多文件,这个打算下一步将所有生成的Image对象打包成一个Zip文件然后再下载。
  • 图集下载地址只能是默认的下载地址,这个我还没找到什么解决方法。
  • 以上没有考虑性能问题,都是怎么方便怎么来,很多值得优化的地方,不过我觉得一个工具能实现功能是第一目标。
  • 这种划分碎图的方式会将诸如美术字这种间隔很开的图片强行分开,别的图片效果还是不错。
总结完了,不过这个软件还没完,接下来需要实现如下功能
  • 多图集分解功能
  • 多图集带配置(.atlas)的分解功能(精准分解的功能)
  • 优化页面表现(这个小项目不光想实现图集分解打包这个功能也想练习下我的Web知识,所以界面也会完善)
  • 下载文件最终得下载zip文件,不然全是碎图一次性下载下来,不够完美

    推荐阅读