音影片入門-12-手動生成一張PNG圖片

  • 2019 年 10 月 30 日
  • 筆記

* 音影片入門文章目錄 *

預熱

上一篇 【PNG文件格式詳解】詳細介紹了 PNG 文件的格式。

PNG 影像格式文件由一個 8 位元組的 PNG 文件署名域和 3 個以上的後續數據塊(IHDR、IDAT、IEND)組成。

PNG 文件包括 8 位元組文件署名(89 50 4E 47 0D 0A 1A 0A,十六進位),用來識別 PNG 格式。

數據塊中有 4 個關鍵數據塊:

  • 文件頭數據塊 IHDR(header chunk):包含有影像基本資訊,作為第一個數據塊出現並只出現一次。
  • 調色板數據塊 PLTE(palette chunk):必須放在影像數據塊之前。
  • 影像數據塊 IDAT(image data chunk):存儲實際影像數據。PNG 數據允許包含多個連續的影像數據塊。
  • 影像結束數據 IEND(image trailer chunk):放在文件尾部,表示 PNG 數據流結束。

數據塊連起來,大概這個樣子:

PNG 標識符 PNG 數據塊(IHDR) PNG 數據塊(其他類型數據塊) PNG 結尾數據塊(IEND)

目標圖:

彩虹條

生成真彩 PNG 圖片

真彩 PNG 圖片不需要 PLTE 調色板數據塊,IDAT 數據塊中存放的是影像的 RGB 數據。

分析 – 真彩 PNG IDAT 數據塊

以 7X7 解析度為例:

true-color-idat.png

程式碼 – 生成真彩 PNG IDAT 數據塊

// 彩虹的七種顏色  uint32_t rainbowColors[] = {          0XFF0000, // 紅          0XFFA500, // 橙          0XFFFF00, // 黃          0X00FF00, // 綠          0X007FFF, // 青          0X0000FF, // 藍          0X8B00FF  // 紫  };  // 生成真彩 PNG 圖片的影像數據塊 IDAT  void genRGB24Data(uint8_t *rgbData, int width, int height) {        for (int i = 0; i < height; ++i) {          // 當前顏色          uint32_t currentColor = rainbowColors[0];          if(i < 100) {              currentColor = rainbowColors[0];          } else if(i < 200) {              currentColor = rainbowColors[1];          } else if(i < 300) {              currentColor = rainbowColors[2];          } else if(i < 400) {              currentColor = rainbowColors[3];          } else if(i < 500) {              currentColor = rainbowColors[4];          } else if(i < 600) {              currentColor = rainbowColors[5];          } else if(i < 700) {              currentColor = rainbowColors[6];          }          // 當前顏色 R 分量          uint8_t R = (currentColor & 0xFF0000) >> 16;          // 當前顏色 G 分量          uint8_t G = (currentColor & 0x00FF00) >> 8;          // 當前顏色 B 分量          uint8_t B = currentColor & 0x0000FF;            // 每個掃描行前第一個位元組是過濾器類型          rgbData[3*(i*width)+i] = 0x00;            for (int j = 0; j < width; ++j) {              int currentIndex = 3*(i*width+j)+(i+1);              rgbData[currentIndex] = R;              rgbData[currentIndex+1] = G;              rgbData[currentIndex+2] = B;          }      }  }

生成真彩 PNG 完整程式碼

#include <stdio.h>  #include <stdint.h>  #include <stdlib.h>  #include <stdbool.h>  #include "zlib.h"    // ***** functions in util.c *****  bool isBigEndianOrder();  void genRGB24Data(uint8_t *rgbData, int width, int height);  uint32_t switchUint32(uint32_t i);  uint32_t calcCrc32(uint32_t dataASCII, uint8_t *data, uint32_t length);    typedef struct {      uint32_t width;      uint32_t height;      uint8_t bitDepth;      uint8_t colorType;      uint8_t compressionMethod;      uint8_t filterMethod;      uint8_t interlaceMethod;  } PNG_IHDR_DATA;    int main() {      // PNG 圖片尺寸      int width = 700, height = 700;      // IDAT 中數據部分長度      uint32_t IDAT_RGB_DATA_LENGTH = width*height*3+height;        // PNG 文件包括 8 位元組文件署名(89 50 4E 47 0D 0A 1A 0A,十六進位),用來識別 PNG 格式。      uint8_t PNG_FILE_SIGNATURE[] = {0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A};      // IHDR 每個字母對應的 ASCII      uint32_t IHDR_ASCII = switchUint32(0x49484452);      // IDAT 每個字母對應的ASCII      uint32_t IDAT_ASCII = switchUint32(0x49444154);      // PNG 文件的結尾 12 個位元組看起來總應該是這樣的:(00 00 00 00 49 45 4E 44 AE 42 60 82,十六進位)      uint8_t PNG_IEND_DATA[] = {0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82};        FILE *file = fopen("/Users/staff/Desktop/0-true-color.png", "wb");      // FILE *file = fopen("C:\Users\Administrator\Desktop\0-true-color.png", "wb+");      if (!file) {          printf("Could not write filen");          return -1;      }        // 真彩 PNG 圖片 存儲的是 RGB 數      uint8_t *rgb24Data = (uint8_t *)malloc(IDAT_RGB_DATA_LENGTH);      // 填充 IDAT 的 RGB 數據      genRGB24Data(rgb24Data, width, height);        // 寫 PNG 文件署名      fwrite(PNG_FILE_SIGNATURE, 1, sizeof(PNG_FILE_SIGNATURE), file);        // 準備 IHDR 數據      PNG_IHDR_DATA pngIhdrData;      pngIhdrData.width = switchUint32(width);      pngIhdrData.height = switchUint32(height);      pngIhdrData.bitDepth = 8;      pngIhdrData.colorType = 2;// 2:真彩色影像,8或16    6:帶α通道數據的真彩色影像,8或16      pngIhdrData.compressionMethod = 0;      pngIhdrData.filterMethod = 0;      pngIhdrData.interlaceMethod = 0;        // IHDR 數據長度      uint32_t IHDR_DATA_LENGTH = 13;      // IHDR 數據長度 轉換成大端位元組序      uint32_t pngIhdrDataSize = switchUint32(IHDR_DATA_LENGTH);      // 計算 IHDR CRC32      uint32_t ihdrDataCrc32 = calcCrc32(IHDR_ASCII, (uint8_t *) &pngIhdrData, IHDR_DATA_LENGTH);        // 寫 IHDR 數據長度      fwrite(&pngIhdrDataSize, 1, sizeof(pngIhdrDataSize), file);      // 寫 IHDR ASCII      fwrite(&IHDR_ASCII, 1, sizeof(IHDR_ASCII), file);      // 寫 IHDR 數據      fwrite(&pngIhdrData, 1, IHDR_DATA_LENGTH, file);      // 寫 IHDR CRC32      fwrite(&ihdrDataCrc32, 1, sizeof(ihdrDataCrc32), file);        // zlib 壓縮數據      uint8_t buf[IDAT_RGB_DATA_LENGTH];      // 壓縮後 buf 的數據長度 壓縮完成後就是實際大小了      uint32_t buflen = IDAT_RGB_DATA_LENGTH;        // 執行 zlib 的壓縮方法      compress(buf, (uLongf *) &buflen, rgb24Data, IDAT_RGB_DATA_LENGTH);      printf("n壓縮前數據長度:%d n壓縮後數據長度為:%d n", IDAT_RGB_DATA_LENGTH, buflen);        // 計算 IDAT CRC32      uint32_t idatDataCrc32 = calcCrc32(IDAT_ASCII, buf, buflen);      // IDAT 數據長度 轉換成大端位元組序      uint32_t tmpBuflen = switchUint32(buflen);        // 寫 IDAT 數據長度      fwrite(&tmpBuflen, 1, sizeof(tmpBuflen), file);      // 寫 IDAT ASCII      fwrite(&IDAT_ASCII, 1, sizeof(IDAT_ASCII), file);      // 寫 IDAT 數據      fwrite(buf, 1, buflen, file);      // 寫 IDAT CRC32      fwrite(&idatDataCrc32, 1, sizeof(idatDataCrc32), file);        // 寫 IEND 資訊      fwrite(PNG_IEND_DATA, 1, sizeof(PNG_IEND_DATA), file);        // 查看位元組序      if(isBigEndianOrder()) {          printf("大端位元組序");      } else {          printf("小端位元組序");      }        // 收尾工作      fflush(file);      free(rgb24Data);      fclose(file);      return 0;  }

生成索引 PNG 圖片

索引 PNG 圖片必須有 PLTE 調色板數據塊,IDAT 數據塊中存放的是影像的 PLTE 調色板顏色索引數據。

分析 – 索引 PNG IDAT 數據塊

以 7X7 解析度為例:

indexed-color-idat.png

程式碼 – 生成索引 PNG PLTE 調色板

// 彩虹的七種顏色  uint32_t rainbowColors[] = {          0XFF0000, // 紅          0XFFA500, // 橙          0XFFFF00, // 黃          0X00FF00, // 綠          0X007FFF, // 青          0X0000FF, // 藍          0X8B00FF  // 紫  };    /**   * 生成索引 PNG 圖片的調色板 PLTE   * @param rgbPLTEData   */  void genRGBPLTE(uint8_t *rgbPLTEData) {      for (int i = 0; i < 7; ++i) {          uint32_t currentColor = rainbowColors[i];          // 當前顏色 R 分量          uint8_t R = (currentColor & 0xFF0000) >> 16;          // 當前顏色 G 分量          uint8_t G = (currentColor & 0x00FF00) >> 8;          // 當前顏色 B 分量          uint8_t B = currentColor & 0x0000FF;            int currentIndex = 3*i;          rgbPLTEData[currentIndex] = R;          rgbPLTEData[currentIndex+1] = G;          rgbPLTEData[currentIndex+2] = B;      }  }

程式碼 – 生成索引 PNG IDAT 數據塊

/**   * 生成索引 PNG 圖片的影像數據塊 IDAT   * @param rgbIndexData   * @param width   * @param height   */  void genRGBIndexData(uint8_t *rgbIndexData, int width, int height) {      for (int i = 0; i < height; ++i) {          uint8_t currentColorIndex = 0;          if(i < 100) {              currentColorIndex = 0;          } else if(i < 200) {              currentColorIndex = 1;          } else if(i < 300) {              currentColorIndex = 2;          } else if(i < 400) {              currentColorIndex = 3;          } else if(i < 500) {              currentColorIndex = 4;          } else if(i < 600) {              currentColorIndex = 5;          } else if(i < 700) {              currentColorIndex = 6;          }          // 每個掃描行前第一個位元組是過濾器類型          rgbIndexData[(i*width)/2+i] = 0x00;          for (int j = 0; j < width; ++j) {              int currentIndex = (i*width+j)/2+(i+1);              int positionInByte = j%2;              if(positionInByte == 0) {                  rgbIndexData[currentIndex] = currentColorIndex << 4;              } else {                  rgbIndexData[currentIndex] += currentColorIndex;              }          }      }  }

生成索引 PNG 完整程式碼

#include <stdio.h>  #include <stdint.h>  #include <stdlib.h>  #include <stdbool.h>  #include "zlib.h"    // ***** functions in util.c *****  bool isBigEndianOrder();  void genRGBPLTE(uint8_t *rgbData);  void genRGBIndexData(uint8_t *rgbIndexData, int width, int height);  uint32_t switchUint32(uint32_t i);  uint32_t calcCrc32(uint32_t dataASCII, uint8_t *data, uint32_t length);    typedef struct {      uint32_t width;      uint32_t height;      uint8_t bitDepth;      uint8_t colorType;      uint8_t compressionMethod;      uint8_t filterMethod;      uint8_t interlaceMethod;  } PNG_IHDR_DATA;    int main() {      // PNG 圖片尺寸      int width = 700, height = 700;      // IDAT 中數據部分長度      uint32_t IDAT_INDEX_DATA_LENGTH = width*height/2+height;        // PNG 文件包括 8 位元組文件署名(89 50 4E 47 0D 0A 1A 0A,十六進位),用來識別 PNG 格式。      uint8_t PNG_FILE_SIGNATURE[] = {0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A};      // IHDR 每個字母對應的 ASCII      uint32_t IHDR_ASCII = switchUint32(0x49484452);      // PLTE 每個字母對應的ASCII      uint32_t PLTE_ASCII = switchUint32(0x504C5445);      // IDAT 每個字母對應的ASCII      uint32_t IDAT_ASCII = switchUint32(0x49444154);      // PNG 文件的結尾 12 個位元組看起來總應該是這樣的:(00 00 00 00 49 45 4E 44 AE 42 60 82,十六進位)      uint8_t PNG_IEND_DATA[] = {0x00, 0x00, 0x00, 0x00, 0x49, 0x45, 0x4E, 0x44, 0xAE, 0x42, 0x60, 0x82};        FILE *file = fopen("/Users/staff/Desktop/0-indexed-color.png", "wb");      // FILE *file = fopen("C:\Users\Administrator\Desktop\0-indexed-color.png", "wb+");      if (!file) {          printf("Could not write filen");          return -1;      }        // 紅橙黃綠青藍紫-七種顏色的調色板  7 種顏色 * 每種顏色占 3 位元組      uint8_t *rgbPLTEData = (uint8_t *)malloc(7*3);      // 索引 PNG 圖片,IDAT 存儲的是 PLTE 中的圖片索引      uint8_t *rgbIndexData = (uint8_t *)malloc(IDAT_INDEX_DATA_LENGTH);        // 填充 紅橙黃綠青藍紫-七種顏色的調色板      genRGBPLTE(rgbPLTEData);      // 填充 IDAT 的 PLTE 索引      genRGBIndexData(rgbIndexData, width, height);        // 寫 PNG 文件署名      fwrite(PNG_FILE_SIGNATURE, 1, sizeof(PNG_FILE_SIGNATURE), file);        // 準備 IHDR 數據      PNG_IHDR_DATA pngIhdrData;      pngIhdrData.width = switchUint32(width);      pngIhdrData.height = switchUint32(height);      pngIhdrData.bitDepth = 4;      pngIhdrData.colorType = 3; // 3:索引彩色影像,1,2,4或8      pngIhdrData.compressionMethod = 0;      pngIhdrData.filterMethod = 0;      pngIhdrData.interlaceMethod = 0;        // IHDR 數據長度      uint32_t IHDR_DATA_LENGTH = 13;      // IHDR 數據長度 轉換成大端位元組序      uint32_t pngIhdrDataSize = switchUint32(IHDR_DATA_LENGTH);      // 計算 IHDR CRC32      uint32_t ihdrDataCrc32 = calcCrc32(IHDR_ASCII, (uint8_t *) &pngIhdrData, IHDR_DATA_LENGTH);        // 寫 IHDR 數據長度      fwrite(&pngIhdrDataSize, 1, sizeof(pngIhdrDataSize), file);      // 寫 IHDR ASCII      fwrite(&IHDR_ASCII, 1, sizeof(IHDR_ASCII), file);      // 寫 IHDR 數據      fwrite(&pngIhdrData, 1, IHDR_DATA_LENGTH, file);      // 寫 IHDR CRC32      fwrite(&ihdrDataCrc32, 1, sizeof(ihdrDataCrc32), file);          // 準備 PLTE 調色板資訊      // PLTE 數據長度      uint32_t PLTE_DATA_LENGTH = 21;      // PLTE 數據長度 轉換成大端位元組序      uint32_t pngPlteDataLength = switchUint32(PLTE_DATA_LENGTH);      // 計算 PLTE CRC32      uint32_t plteDataCrc32 = calcCrc32(PLTE_ASCII, rgbPLTEData, PLTE_DATA_LENGTH);        // 寫 PLTE 數據長度      fwrite(&pngPlteDataLength, 1, sizeof(pngPlteDataLength), file);      // 寫 PLTE ASCII      fwrite(&PLTE_ASCII, 1, sizeof(PLTE_ASCII), file);      // 寫 PLTE 數據      fwrite(rgbPLTEData, 1, PLTE_DATA_LENGTH, file);      // 寫 PLTE CRC32      fwrite(&plteDataCrc32, 1, sizeof(plteDataCrc32), file);        // zlib 壓縮數據      // buf 用於存放壓縮後的數據      uint8_t buf[IDAT_INDEX_DATA_LENGTH];      // 壓縮後 buf 的數據長度 壓縮完成後就是實際大小了      uint32_t buflen = IDAT_INDEX_DATA_LENGTH;        // 執行 zlib 的壓縮方法      compress(buf, (uLongf *) &buflen, rgbIndexData, IDAT_INDEX_DATA_LENGTH);      printf("n壓縮前數據長度:%d n壓縮後數據長度為:%d n", IDAT_INDEX_DATA_LENGTH, buflen);        // 計算 IDAT CRC32      uint32_t idatDataCrc32 = calcCrc32(IDAT_ASCII, buf, buflen);      // IDAT 數據長度 轉換成大端位元組序      uint32_t tmpBuflen = switchUint32(buflen);        // 寫 IDAT 數據長度      fwrite(&tmpBuflen, 1, sizeof(tmpBuflen), file);      // 寫 IDAT ASCII      fwrite(&IDAT_ASCII, 1, sizeof(IDAT_ASCII), file);      // 寫 IDAT 數據      fwrite(buf, 1, buflen, file);      // 寫 IDAT CRC32      fwrite(&idatDataCrc32, 1, sizeof(idatDataCrc32), file);        // 寫 IEND 資訊      fwrite(PNG_IEND_DATA, 1, sizeof(PNG_IEND_DATA), file);        // 查看位元組序      if(isBigEndianOrder()) {          printf("大端位元組序");      } else {          printf("小端位元組序");      }        // 收尾工作      fflush(file);      free(rgbPLTEData);      free(rgbIndexData);      fclose(file);      return 0;  }

總結 & 查看

生成真彩 PNG、索引 PNG 圖片之間的區別:

  • IHDR 文件頭數據塊中的顏色類型,索引 PNG 顏色類型是 3:索引彩色影像,真彩 PNG 顏色類型是 2:真彩色影像
  • PLTE 調色板數據塊,索引 PNG 必須有調色板,真彩 PNG 不需要調色板。
  • IDAT 數據塊存儲的數據,索引 PNG 存儲的是調色板顏色的索引,真彩 PNG 存儲的是 RGB 數據。

來看一看純手工打造的 PNG 圖片:

generate-png-by-hand.jpg

Congratulations!


程式碼:
11-rgb-to-png

參考資料:

Portable Network Graphics (PNG) Specification and Extensions

gzip,deflate,zlib辨析

Zlib庫的安裝與使用

內容有誤?聯繫作者:

聯繫作者


本文由部落格一文多發平台 OpenWrite 發布!