溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

如何使用Node進(jìn)行圖片壓縮

發(fā)布時間:2023-03-21 09:45:11 來源:億速云 閱讀:132 作者:iii 欄目:web開發(fā)

這篇文章主要介紹“如何使用Node進(jìn)行圖片壓縮”的相關(guān)知識,小編通過實際案例向大家展示操作過程,操作方法簡單快捷,實用性強(qiáng),希望這篇“如何使用Node進(jìn)行圖片壓縮”文章能幫助大家解決問題。

我們先把圖片上傳到后端,看看后端接收了什么樣的參數(shù)。這里后端我用的是Node.js(Nest),圖片我以PNG圖片為例。

接口和參數(shù)打印如下:

@Post('/compression')
@UseInterceptors(FileInterceptor('file'))
async imageCompression(@UploadedFile() file: Express.Multer.File) {
  
  return {
    file
  }
}

如何使用Node進(jìn)行圖片壓縮

要進(jìn)行壓縮,我們就需要拿到圖像數(shù)據(jù)??梢钥吹剑ㄒ荒懿啬鋱D像數(shù)據(jù)的就是這串buffer。那這串buffer描述了什么,就需要先弄清什么是PNG?!鞠嚓P(guān)教程推薦:nodejs視頻教程、編程教學(xué)】

PNG

這里是PNG的WIKI地址。

閱讀之后,我了解到PNG是由一個8 byte的文件頭加上多個的塊(chunk)組成。示意圖如下:

如何使用Node進(jìn)行圖片壓縮

其中:

文件頭是由一個被稱為magic number的組成。值為 89 50 4e 47 0d 0a 1a 0a(16進(jìn)制)。它標(biāo)記了這串?dāng)?shù)據(jù)是PNG格式。

塊分為兩種,一種叫關(guān)鍵塊(Critical chunks),一種叫輔助塊(Ancillary chunks)。關(guān)鍵塊是必不可少的,沒有關(guān)鍵塊,解碼器將不能正確識別并展示圖片。輔助塊是可選的,部分軟件在處理圖片之后就有可能攜帶輔助塊。每個塊都是四部分組成:4 byte 描述這個塊的內(nèi)容有多長,4 byte 描述這個塊的類型是什么,n byte 描述塊的內(nèi)容(n 就是前面4 byte 值的大小,也就是說,一個塊最大長度為28*4),4 byte CRC校驗檢查塊的數(shù)據(jù),標(biāo)記著一個塊的結(jié)束。其中,塊類型的4 byte 的值為4個acsii碼,第一個字母大寫表示是關(guān)鍵塊小寫表示是輔助塊;第二個字母大寫表示是公有小寫表示是私有;第三個字母必須是大寫,用于PNG后續(xù)的擴(kuò)展;第四個字母表示該塊不識別時,能否安全復(fù)制,大寫表示未修改關(guān)鍵塊時才能安全復(fù)制,小寫表示都能安全復(fù)制。PNG官方提供很多定義的塊類型,這里只需要知道關(guān)鍵塊的類型即可,分別是IHDR,PLTE,IDAT,IEND。

IHDR

PNG要求第一個塊必須是IHDR。IHDR的塊內(nèi)容是固定的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)

寬度和高度很容易理解,剩下的幾個好像都很陌生,接下來我將進(jìn)行說明。

在說明位深之前,我們先來看顏色類型,顏色類型有5種值:

  • 0 表示灰度(grayscale)它只有一個通道(channel),看成rgb的話,可以理解它的三色通道值是相等的,所以不需要多余兩個通道表示。

  • 2 表示真實色彩(rgb)它有三個通道,分別是R(紅色),G(綠色),B(藍(lán)色)。

  • 3 表示顏色索引(indexed)它也只有一個通道,表示顏色的索引值。該類型往往配備一組顏色列表,具體的顏色是根據(jù)索引值和顏色列表查詢得到的。

  • 4 表示灰度和alpha 它有兩個通道,除了灰度的通道外,多了一個alpha通道,可以控制透明度。

  • 6 表示真實色彩和alpha 它有四個通道。

之所以要說到通道,是因為它和這里的位深有關(guān)。位深的值就定義了每個通道所占的位數(shù)(bit)。位深跟顏色類型組合,就能知道圖片的顏色格式類型和每個像素所占的內(nèi)存大小。PNG官方支持的組合如下表:

如何使用Node進(jìn)行圖片壓縮

過濾和壓縮是因為PNG中存儲的不是圖像的原始數(shù)據(jù),而是處理后的數(shù)據(jù),這也是為什么PNG圖片所占內(nèi)存較小的原因。PNG使用了兩步進(jìn)行了圖片數(shù)據(jù)的壓縮轉(zhuǎn)換。

第一步,過濾。過濾的目的是為了讓原始圖片數(shù)據(jù)經(jīng)過該規(guī)則后,能進(jìn)行更大的壓縮比。舉個例子,如果有一張漸變圖片,從左往右,顏色依次為[#000000, #000001, #000002, ..., #ffffff],那么我們就可以約定一條規(guī)則,右邊的像素總是和它前一個左邊的像素進(jìn)行比較,那么處理完的數(shù)據(jù)就變成了[1, 1, 1, ..., 1],這樣是不是就能進(jìn)行更好的壓縮。PNG目前只有一種過濾方式,就是基于相鄰像素作為預(yù)測值,用當(dāng)前像素減去預(yù)測值。過濾的類型一共有五種,(目前我還不知道這個類型值在哪里存儲,有可能在IDAT里,找到了再來刪除這條括號里的已確定該類型值儲存在IDAT數(shù)據(jù)中)如下表所示:

Type byteFilter namePredicted value
0None不做任何處理
1Sub左側(cè)相鄰像素
2Up上方相鄰像素
3AverageMath.floor((左側(cè)相鄰像素 + 上方相鄰像素) / 2)
4Paeth取(左側(cè)相鄰像素 + 上方相鄰像素 - 左上方像素)最接近的值

第二步,壓縮。PNG也只有一種壓縮算法,使用的是DEFLATE算法。這里不細(xì)說,具體看下面的章節(jié)。

交錯方式,有兩種值。0表示不處理,1表示使用Adam7 算法進(jìn)行處理。我沒有去詳細(xì)了解該算法,簡單來說,當(dāng)值為0時,圖片需要所有數(shù)據(jù)都加載完畢時,圖片才會顯示。而值為1時,Adam7會把圖片劃分多個區(qū)域,每個區(qū)域逐級加載,顯示效果會有所優(yōu)化,但通常會降低壓縮效率。加載過程可以看下面這張gif圖。

如何使用Node進(jìn)行圖片壓縮

PLTE

PLTE的塊內(nèi)容為一組顏色列表,當(dāng)顏色類型為顏色索引時需要配置。值得注意的是,顏色列表中的顏色一定是每個通道8bit,每個像素24bit的真實色彩列表。列表的長度,可以比位深約定的少,但不能多。比如位深是2,那么22,最多4種顏色,列表長度可以為3,但不能為5。

IDAT

IDAT的塊內(nèi)容是圖片原始數(shù)據(jù)經(jīng)過PNG壓縮轉(zhuǎn)換后的數(shù)據(jù),它可能有多個重復(fù)的塊,但必須是連續(xù)的,并且只有當(dāng)上一個塊填充滿時,才會有下一個塊。

IEND

IEND的塊內(nèi)容為0 byte,它表示圖片的結(jié)束。

閱讀到這里,我們把上面的接口改造一下,解析這串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;
}

如何使用Node進(jìn)行圖片壓縮

這里我測試用的圖沒有PLTE,剛好我去TinyPNG壓縮我那張測試圖之后進(jìn)行上傳,發(fā)現(xiàn)有PLTE塊,可以看一下,結(jié)果如下圖。

如何使用Node進(jìn)行圖片壓縮

通過比對這兩張圖,壓縮圖片的方式我們也能窺探一二。

PNG的壓縮

前面說過,PNG使用的是一種叫DEFLATE的無損壓縮算法,它是Huffman Coding跟LZ77的結(jié)合。除了PNG,我們經(jīng)常使用的壓縮文件,.zip,.gzip也是使用的這種算法(7zip算法有更高的壓縮比,也可以了解下)。要了解DEFLATE,我們首先要了解Huffman Coding和LZ77。

Huffman Coding

哈夫曼編碼忘記在大學(xué)的哪門課接觸過了,它是一種根據(jù)字符出現(xiàn)頻率,用最少的字符替換出現(xiàn)頻率最高的字符,最終降低平均字符長度的算法。

舉個例子,有字符串"ABCBCABABADA",如果按照正??臻g存儲,所占內(nèi)存大小為12 * 8bit = 96bit,現(xiàn)對它進(jìn)行哈夫曼編碼。

1.統(tǒng)計每個字符出現(xiàn)的頻率,得到A 5次 B 4次 C 2次 D 1次

2.對字符按照頻率從小到大排序,將得到一個隊列D1,C2,B4,A5

3.按順序構(gòu)造哈夫曼樹,先構(gòu)造一個空節(jié)點,最小頻率的字符分給該節(jié)點的左側(cè),倒數(shù)第二頻率的字符分給右側(cè),然后將頻率相加的值賦值給該節(jié)點。接著用賦值后節(jié)點的值和倒數(shù)第三頻率的字符進(jìn)行比較,較小的值總是分配在左側(cè),較大的值總是分配在右側(cè),依次類推,直到隊列結(jié)束,最后把最大頻率和前面的所有值相加賦值給根節(jié)點,得到一棵完整的哈夫曼樹。

4.對每條路徑進(jìn)行賦值,左側(cè)路徑賦值為0,右側(cè)路徑賦值為1。從根節(jié)點到葉子節(jié)點,進(jìn)行遍歷,遍歷的結(jié)果就是該字符編碼后的二進(jìn)制表示,得到:A(0)B(11)C(101)D(100)。

完整的哈夫曼樹如下(忽略箭頭,沒找到連線- -?。?/p>

如何使用Node進(jìn)行圖片壓縮

壓縮后的字符串,所占內(nèi)存大小為5 * 1bit + 4 * 2bit + 2 * 3bit + 1 * 3bit = 22bit。當(dāng)然在實際傳輸過程中,還需要把編碼表的信息(原始字符和出現(xiàn)頻率)帶上。因此最終占比大小為 4 * 8bit + 4 * 3bit(頻率最大值為5,3bit可以表示)+ 22bit = 66bit(理想狀態(tài)),小于原有的96bit。

LZ77

LZ77算法還是第一次知道,查了一下是一種基于字典和滑動窗的無所壓縮算法。(題外話:因為Lempel和Ziv在1977年提出的算法,所以叫LZ77,哈哈哈?)

我們還是以上面這個字符串"ABCBCABABADA"為例,現(xiàn)假設(shè)有一個4 byte的動態(tài)窗口和一個2byte的預(yù)讀緩沖區(qū),然后對它進(jìn)行LZ77算法壓縮,過程順序從上往下,示意圖如下:

如何使用Node進(jìn)行圖片壓縮

總結(jié)下來,就是預(yù)讀緩沖區(qū)在動態(tài)窗口中找到最長相同項,然后用長度較短的標(biāo)記來替代這個相同項,從而實現(xiàn)壓縮。從上圖也可以看出,壓縮比跟動態(tài)窗口的大小,預(yù)讀緩沖區(qū)的大小和被壓縮數(shù)據(jù)的重復(fù)度有關(guān)。

DEFLATE

DEFLATE【RFC 1951】是先使用LZ77編碼,對編碼后的結(jié)果在進(jìn)行哈夫曼編碼。我們這里不去討論具體的實現(xiàn)方法,直接使用其推薦庫Zlib,剛好Node.js內(nèi)置了對Zlib的支持。接下來我們繼續(xù)改造上面那個接口,如下:

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的塊 需要個數(shù)組緩存最后拼接起來
  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));

  // 這里原圖片數(shù)據(jù)太長了 我就只打印了長度
  return {
    ...result,
    originFileData: originFileData.length,
  };
}

如何使用Node進(jìn)行圖片壓縮

最終打印的結(jié)果,我們需要注意紅框的那幾個部分??梢钥吹缴蠄D,位深和顏色類型決定了每個像素由4 byte組成,然后由于過濾方式的存在,會在每行的第一個字節(jié)進(jìn)行標(biāo)記。因此該圖的原始數(shù)據(jù)所占大小為:707 * 475 * 4 byte + 475 * 1 byte = 1343775 byte。正好是我們打印的結(jié)果。

我們也可以試試之前TinyPNG壓縮后的圖,如下:

如何使用Node進(jìn)行圖片壓縮

可以看到位深為8,索引顏色類型的圖每像素占1 byte。計算得到:707 * 475 * 1 byte + 475 * 1 byte = 336300 byte。結(jié)果也正確。

關(guān)于“如何使用Node進(jìn)行圖片壓縮”的內(nèi)容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業(yè)相關(guān)的知識,可以關(guān)注億速云行業(yè)資訊頻道,小編每天都會為大家更新不同的知識點。

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI