您好,登錄后才能下訂單哦!
這篇文章主要介紹“如何使用Node進行圖片壓縮”的相關知識,小編通過實際案例向大家展示操作過程,操作方法簡單快捷,實用性強,希望這篇“如何使用Node進行圖片壓縮”文章能幫助大家解決問題。
我們先把圖片上傳到后端,看看后端接收了什么樣的參數。這里后端我用的是Node.js(Nest),圖片我以PNG圖片為例。
接口和參數打印如下:
@Post('/compression')
@UseInterceptors(FileInterceptor('file'))
async imageCompression(@UploadedFile() file: Express.Multer.File) {
return {
file
}
}
要進行壓縮,我們就需要拿到圖像數據。可以看到,唯一能藏匿圖像數據的就是這串buffer。那這串buffer描述了什么,就需要先弄清什么是PNG。【相關教程推薦:nodejs視頻教程、編程教學】
這里是PNG的WIKI地址。
閱讀之后,我了解到PNG是由一個8 byte的文件頭加上多個的塊(chunk)組成。示意圖如下:
其中:
文件頭是由一個被稱為magic number的組成。值為 89 50 4e 47 0d 0a 1a 0a(16進制)。它標記了這串數據是PNG格式。
塊分為兩種,一種叫關鍵塊(Critical chunks),一種叫輔助塊(Ancillary chunks)。關鍵塊是必不可少的,沒有關鍵塊,解碼器將不能正確識別并展示圖片。輔助塊是可選的,部分軟件在處理圖片之后就有可能攜帶輔助塊。每個塊都是四部分組成:4 byte 描述這個塊的內容有多長,4 byte 描述這個塊的類型是什么,n byte 描述塊的內容(n 就是前面4 byte 值的大小,也就是說,一個塊最大長度為28*4),4 byte CRC校驗檢查塊的數據,標記著一個塊的結束。其中,塊類型的4 byte 的值為4個acsii碼,第一個字母大寫表示是關鍵塊,小寫表示是輔助塊;第二個字母大寫表示是公有,小寫表示是私有;第三個字母必須是大寫,用于PNG后續的擴展;第四個字母表示該塊不識別時,能否安全復制,大寫表示未修改關鍵塊時才能安全復制,小寫表示都能安全復制。PNG官方提供很多定義的塊類型,這里只需要知道關鍵塊的類型即可,分別是IHDR,PLTE,IDAT,IEND。
PNG要求第一個塊必須是IHDR。IHDR的塊內容是固定的13 byte,包含了圖片的以下信息:
寬度 width (4 byte) & 高度 height (4 byte)
位深 bit depth (1 byte,值為1,2,4,8或者16) & 顏色類型 color type (1 byte,值為0,2,3,4或者6)
壓縮方法 compression method (1 byte,值為0) & 過濾方式 filter method (1 byte,值為0)
交錯方式 interlace method (1 byte,值為0或者1)
寬度和高度很容易理解,剩下的幾個好像都很陌生,接下來我將進行說明。
在說明位深之前,我們先來看顏色類型,顏色類型有5種值:
0 表示灰度(grayscale)它只有一個通道(channel),看成rgb的話,可以理解它的三色通道值是相等的,所以不需要多余兩個通道表示。
2 表示真實色彩(rgb)它有三個通道,分別是R(紅色),G(綠色),B(藍色)。
3 表示顏色索引(indexed)它也只有一個通道,表示顏色的索引值。該類型往往配備一組顏色列表,具體的顏色是根據索引值和顏色列表查詢得到的。
4 表示灰度和alpha 它有兩個通道,除了灰度的通道外,多了一個alpha通道,可以控制透明度。
6 表示真實色彩和alpha 它有四個通道。
之所以要說到通道,是因為它和這里的位深有關。位深的值就定義了每個通道所占的位數(bit)。位深跟顏色類型組合,就能知道圖片的顏色格式類型和每個像素所占的內存大小。PNG官方支持的組合如下表:
過濾和壓縮是因為PNG中存儲的不是圖像的原始數據,而是處理后的數據,這也是為什么PNG圖片所占內存較小的原因。PNG使用了兩步進行了圖片數據的壓縮轉換。
第一步,過濾。過濾的目的是為了讓原始圖片數據經過該規則后,能進行更大的壓縮比。舉個例子,如果有一張漸變圖片,從左往右,顏色依次為[#000000, #000001, #000002, ..., #ffffff],那么我們就可以約定一條規則,右邊的像素總是和它前一個左邊的像素進行比較,那么處理完的數據就變成了[1, 1, 1, ..., 1],這樣是不是就能進行更好的壓縮。PNG目前只有一種過濾方式,就是基于相鄰像素作為預測值,用當前像素減去預測值。過濾的類型一共有五種,(目前我還不知道這個類型值在哪里存儲,有可能在IDAT里,找到了再來刪除這條括號里的已確定該類型值儲存在IDAT數據中)如下表所示:
Type byte | Filter name | Predicted value |
---|---|---|
0 | None | 不做任何處理 |
1 | Sub | 左側相鄰像素 |
2 | Up | 上方相鄰像素 |
3 | Average | Math.floor((左側相鄰像素 + 上方相鄰像素) / 2) |
4 | Paeth | 取(左側相鄰像素 + 上方相鄰像素 - 左上方像素)最接近的值 |
第二步,壓縮。PNG也只有一種壓縮算法,使用的是DEFLATE算法。這里不細說,具體看下面的章節。
交錯方式,有兩種值。0表示不處理,1表示使用Adam7 算法進行處理。我沒有去詳細了解該算法,簡單來說,當值為0時,圖片需要所有數據都加載完畢時,圖片才會顯示。而值為1時,Adam7會把圖片劃分多個區域,每個區域逐級加載,顯示效果會有所優化,但通常會降低壓縮效率。加載過程可以看下面這張gif圖。
PLTE的塊內容為一組顏色列表,當顏色類型為顏色索引時需要配置。值得注意的是,顏色列表中的顏色一定是每個通道8bit,每個像素24bit的真實色彩列表。列表的長度,可以比位深約定的少,但不能多。比如位深是2,那么22,最多4種顏色,列表長度可以為3,但不能為5。
IDAT的塊內容是圖片原始數據經過PNG壓縮轉換后的數據,它可能有多個重復的塊,但必須是連續的,并且只有當上一個塊填充滿時,才會有下一個塊。
IEND的塊內容為0 byte,它表示圖片的結束。
閱讀到這里,我們把上面的接口改造一下,解析這串buffer。
@Post('/compression')
@UseInterceptors(FileInterceptor('file'))
async imageCompression(@UploadedFile() file: Express.Multer.File) {
const buffer = file.buffer;
const result = {
header: buffer.subarray(0, 8).toString('hex'),
chunks: [],
size: file.size,
};
let pointer = 8;
while (pointer < buffer.length) {
let chunk = {};
const length = parseInt(buffer.subarray(pointer, pointer + 4).toString('hex'), 16);
const chunkType = buffer.subarray(pointer + 4, pointer + 8).toString('ascii');
const crc = buffer.subarray(pointer + length, pointer + length + 4).toString('hex');
chunk = {
...chunk,
length,
chunkType,
crc,
};
switch (chunkType) {
case 'IHDR':
const width = parseInt(buffer.subarray(pointer + 8, pointer + 12).toString('hex'), 16);
const height = parseInt(buffer.subarray(pointer + 12, pointer + 16).toString('hex'), 16);
const bitDepth = parseInt(
buffer.subarray(pointer + 16, pointer + 17).toString('hex'),
16,
);
const colorType = parseInt(
buffer.subarray(pointer + 17, pointer + 18).toString('hex'),
16,
);
const compressionMethod = parseInt(
buffer.subarray(pointer + 18, pointer + 19).toString('hex'),
16,
);
const filterMethod = parseInt(
buffer.subarray(pointer + 19, pointer + 20).toString('hex'),
16,
);
const interlaceMethod = parseInt(
buffer.subarray(pointer + 20, pointer + 21).toString('hex'),
16,
);
chunk = {
...chunk,
width,
height,
bitDepth,
colorType,
compressionMethod,
filterMethod,
interlaceMethod,
};
break;
case 'PLTE':
const colorList = [];
const colorListStr = buffer.subarray(pointer + 8, pointer + 8 + length).toString('hex');
for (let i = 0; i < colorListStr.length; i += 6) {
colorList.push(colorListStr.slice(i, i + 6));
}
chunk = {
...chunk,
colorList,
};
break;
default:
break;
}
result.chunks.push(chunk);
pointer = pointer + 4 + 4 + length + 4;
}
return result;
}
這里我測試用的圖沒有PLTE,剛好我去TinyPNG壓縮我那張測試圖之后進行上傳,發現有PLTE塊,可以看一下,結果如下圖。
通過比對這兩張圖,壓縮圖片的方式我們也能窺探一二。
前面說過,PNG使用的是一種叫DEFLATE的無損壓縮算法,它是Huffman Coding跟LZ77的結合。除了PNG,我們經常使用的壓縮文件,.zip,.gzip也是使用的這種算法(7zip算法有更高的壓縮比,也可以了解下)。要了解DEFLATE,我們首先要了解Huffman Coding和LZ77。
哈夫曼編碼忘記在大學的哪門課接觸過了,它是一種根據字符出現頻率,用最少的字符替換出現頻率最高的字符,最終降低平均字符長度的算法。
舉個例子,有字符串"ABCBCABABADA",如果按照正常空間存儲,所占內存大小為12 * 8bit = 96bit,現對它進行哈夫曼編碼。
1.統計每個字符出現的頻率,得到A 5次 B 4次 C 2次 D 1次
2.對字符按照頻率從小到大排序,將得到一個隊列D1,C2,B4,A5
3.按順序構造哈夫曼樹,先構造一個空節點,最小頻率的字符分給該節點的左側,倒數第二頻率的字符分給右側,然后將頻率相加的值賦值給該節點。接著用賦值后節點的值和倒數第三頻率的字符進行比較,較小的值總是分配在左側,較大的值總是分配在右側,依次類推,直到隊列結束,最后把最大頻率和前面的所有值相加賦值給根節點,得到一棵完整的哈夫曼樹。
4.對每條路徑進行賦值,左側路徑賦值為0,右側路徑賦值為1。從根節點到葉子節點,進行遍歷,遍歷的結果就是該字符編碼后的二進制表示,得到:A(0)B(11)C(101)D(100)。
完整的哈夫曼樹如下(忽略箭頭,沒找到連線- -!):
壓縮后的字符串,所占內存大小為5 * 1bit + 4 * 2bit + 2 * 3bit + 1 * 3bit = 22bit。當然在實際傳輸過程中,還需要把編碼表的信息(原始字符和出現頻率)帶上。因此最終占比大小為 4 * 8bit + 4 * 3bit(頻率最大值為5,3bit可以表示)+ 22bit = 66bit(理想狀態),小于原有的96bit。
LZ77算法還是第一次知道,查了一下是一種基于字典和滑動窗的無所壓縮算法。(題外話:因為Lempel和Ziv在1977年提出的算法,所以叫LZ77,哈哈哈?)
我們還是以上面這個字符串"ABCBCABABADA"為例,現假設有一個4 byte的動態窗口和一個2byte的預讀緩沖區,然后對它進行LZ77算法壓縮,過程順序從上往下,示意圖如下:
總結下來,就是預讀緩沖區在動態窗口中找到最長相同項,然后用長度較短的標記來替代這個相同項,從而實現壓縮。從上圖也可以看出,壓縮比跟動態窗口的大小,預讀緩沖區的大小和被壓縮數據的重復度有關。
DEFLATE【RFC 1951】是先使用LZ77編碼,對編碼后的結果在進行哈夫曼編碼。我們這里不去討論具體的實現方法,直接使用其推薦庫Zlib,剛好Node.js內置了對Zlib的支持。接下來我們繼續改造上面那個接口,如下:
import * as zlib from 'zlib';
@Post('/compression')
@UseInterceptors(FileInterceptor('file'))
async imageCompression(@UploadedFile() file: Express.Multer.File) {
const buffer = file.buffer;
const result = {
header: buffer.subarray(0, 8).toString('hex'),
chunks: [],
size: file.size,
};
// 因為可能有多個IDAT的塊 需要個數組緩存最后拼接起來
const fileChunkDatas = [];
let pointer = 8;
while (pointer < buffer.length) {
let chunk = {};
const length = parseInt(buffer.subarray(pointer, pointer + 4).toString('hex'), 16);
const chunkType = buffer.subarray(pointer + 4, pointer + 8).toString('ascii');
const crc = buffer.subarray(pointer + length, pointer + length + 4).toString('hex');
chunk = {
...chunk,
length,
chunkType,
crc,
};
switch (chunkType) {
case 'IHDR':
const width = parseInt(buffer.subarray(pointer + 8, pointer + 12).toString('hex'), 16);
const height = parseInt(buffer.subarray(pointer + 12, pointer + 16).toString('hex'), 16);
const bitDepth = parseInt(
buffer.subarray(pointer + 16, pointer + 17).toString('hex'),
16,
);
const colorType = parseInt(
buffer.subarray(pointer + 17, pointer + 18).toString('hex'),
16,
);
const compressionMethod = parseInt(
buffer.subarray(pointer + 18, pointer + 19).toString('hex'),
16,
);
const filterMethod = parseInt(
buffer.subarray(pointer + 19, pointer + 20).toString('hex'),
16,
);
const interlaceMethod = parseInt(
buffer.subarray(pointer + 20, pointer + 21).toString('hex'),
16,
);
chunk = {
...chunk,
width,
height,
bitDepth,
colorType,
compressionMethod,
filterMethod,
interlaceMethod,
};
break;
case 'PLTE':
const colorList = [];
const colorListStr = buffer.subarray(pointer + 8, pointer + 8 + length).toString('hex');
for (let i = 0; i < colorListStr.length; i += 6) {
colorList.push(colorListStr.slice(i, i + 6));
}
chunk = {
...chunk,
colorList,
};
break;
case 'IDAT':
fileChunkDatas.push(buffer.subarray(pointer + 8, pointer + 8 + length));
break;
default:
break;
}
result.chunks.push(chunk);
pointer = pointer + 4 + 4 + length + 4;
}
const originFileData = zlib.unzipSync(Buffer.concat(fileChunkDatas));
// 這里原圖片數據太長了 我就只打印了長度
return {
...result,
originFileData: originFileData.length,
};
}
最終打印的結果,我們需要注意紅框的那幾個部分。可以看到上圖,位深和顏色類型決定了每個像素由4 byte組成,然后由于過濾方式的存在,會在每行的第一個字節進行標記。因此該圖的原始數據所占大小為:707 * 475 * 4 byte + 475 * 1 byte = 1343775 byte。正好是我們打印的結果。
我們也可以試試之前TinyPNG壓縮后的圖,如下:
可以看到位深為8,索引顏色類型的圖每像素占1 byte。計算得到:707 * 475 * 1 byte + 475 * 1 byte = 336300 byte。結果也正確。
關于“如何使用Node進行圖片壓縮”的內容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業相關的知識,可以關注億速云行業資訊頻道,小編每天都會為大家更新不同的知識點。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。