91超碰碰碰碰久久久久久综合_超碰av人澡人澡人澡人澡人掠_国产黄大片在线观看画质优化_txt小说免费全本

溫馨提示×

溫馨提示×

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

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

如何使用Node進行圖片壓縮

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

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

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

接口和參數打印如下:

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

如何使用Node進行圖片壓縮

要進行壓縮,我們就需要拿到圖像數據。可以看到,唯一能藏匿圖像數據的就是這串buffer。那這串buffer描述了什么,就需要先弄清什么是PNG。【相關教程推薦:nodejs視頻教程、編程教學】

PNG

這里是PNG的WIKI地址。

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

如何使用Node進行圖片壓縮

其中:

文件頭是由一個被稱為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。

IHDR

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官方支持的組合如下表:

如何使用Node進行圖片壓縮

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

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

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

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

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

如何使用Node進行圖片壓縮

PLTE

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

IDAT

IDAT的塊內容是圖片原始數據經過PNG壓縮轉換后的數據,它可能有多個重復的塊,但必須是連續的,并且只有當上一個塊填充滿時,才會有下一個塊。

IEND

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;
}

如何使用Node進行圖片壓縮

這里我測試用的圖沒有PLTE,剛好我去TinyPNG壓縮我那張測試圖之后進行上傳,發現有PLTE塊,可以看一下,結果如下圖。

如何使用Node進行圖片壓縮

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

PNG的壓縮

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

Huffman Coding

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

舉個例子,有字符串"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)。

完整的哈夫曼樹如下(忽略箭頭,沒找到連線- -!):

如何使用Node進行圖片壓縮

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

LZ77

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

我們還是以上面這個字符串"ABCBCABABADA"為例,現假設有一個4 byte的動態窗口和一個2byte的預讀緩沖區,然后對它進行LZ77算法壓縮,過程順序從上往下,示意圖如下:

如何使用Node進行圖片壓縮

總結下來,就是預讀緩沖區在動態窗口中找到最長相同項,然后用長度較短的標記來替代這個相同項,從而實現壓縮。從上圖也可以看出,壓縮比跟動態窗口的大小,預讀緩沖區的大小和被壓縮數據的重復度有關。

DEFLATE

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,
  };
}

如何使用Node進行圖片壓縮

最終打印的結果,我們需要注意紅框的那幾個部分。可以看到上圖,位深和顏色類型決定了每個像素由4 byte組成,然后由于過濾方式的存在,會在每行的第一個字節進行標記。因此該圖的原始數據所占大小為:707 * 475 * 4 byte + 475 * 1 byte = 1343775 byte。正好是我們打印的結果。

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

如何使用Node進行圖片壓縮

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

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

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

根河市| 奉新县| 井陉县| 大田县| 麦盖提县| 孝感市| 赤城县| 建阳市| 凌海市| 中阳县| 汪清县| 安国市| 恭城| 鲜城| 会东县| 茌平县| 九台市| 个旧市| 东平县| 兴城市| 连南| 衢州市| 镇远县| 响水县| 澳门| 雷山县| 永清县| 洞口县| 全州县| 合阳县| 安多县| 达尔| 搜索| 独山县| 亚东县| 庄浪县| 昌都县| 济阳县| 大渡口区| 南阳市| 缙云县|