音视频入门-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 分辨率为例:
代码 – 生成真彩 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 分辨率为例:
代码 – 生成索引 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 图片:
Congratulations!
代码:
11-rgb-to-png
参考资料:
Portable Network Graphics (PNG) Specification and Extensions
内容有误?联系作者:
本文由博客一文多发平台 OpenWrite 发布!