-前言-
上篇我們把文件上傳的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;
}
使用這個方法有兩個缺點:
- 運行時間很慢,每個像素點都會通過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 = $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沒有直接下載文件的接口,所以有很多變種的方法實現,這裏我們使用<a>標籤的下載實現方式。
因爲我源代碼中還利用了其它方式下載,所以我們這裏新建一個downloadMethodByCreateHerfA方法來標識通過<a>標籤下載文件。
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 = 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數據,接着我們通過新建的<a>標籤,然後自動觸發自動下載。
-總結-
至此圖集從上傳到下載的全過程就算寫完了,功能是基本能達到,不過也來說說如上實現的幾個缺點
- 圖集分解後多個圖片瀏覽器會報提示是否下載多文件,這個打算下一步將所有生成的Image對象打包成一個Zip文件然後再下載。
- 圖集下載地址只能是默認的下載地址,這個我還沒找到什麼解決方法。
- 以上沒有考慮性能問題,都是怎麼方便怎麼來,很多值得優化的地方,不過我覺得一個工具能實現功能是第一目標。
- 這種劃分碎圖的方式會將諸如美術字這種間隔很開的圖片強行分開,別的圖片效果還是不錯。
總結完了,不過這個軟件還沒完,接下來需要實現如下功能
- 多圖集分解功能
- 多圖集帶配置(.atlas)的分解功能(精準分解的功能)
- 優化頁面表現(這個小項目不光想實現圖集分解打包這個功能也想練習下我的Web知識,所以界面也會完善)
- 下載文件最終得下載zip文件,不然全是碎圖一次性下載下來,不夠完美