* 音视频入门文章目录 *

预热

上一篇 【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 file\n");
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 file\n");
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

gzip,deflate,zlib辨析

Zlib库的安装与使用

内容有误?联系作者:


本文由博客一文多发平台 OpenWrite 发布!

音视频入门-12-手动生成一张PNG图片的更多相关文章

  1. 音视频入门-18-手动生成一张GIF图片

    * 音视频入门文章目录 * GIF 编码知识 GIF 包含的数据块: 文件头(Header) 逻辑屏幕标识符(Logical Screen Descriptor) 全局颜色表(Global Color ...

  2. 音视频入门-13-使用开源库生成PNG图片

    * 音视频入门文章目录 * RGB-to-PNG 回顾 上一篇 [手动生成一张PNG图片] 根据 [PNG文件格式详解] 一步一步地手动实现了将 RGB 数据生成了一张 PNG 图片. 有许多开源的 ...

  3. 音视频入门-19-使用giflib处理GIF图片

    * 音视频入门文章目录 * GIFLIB The GIFLIB project 上一篇 [手动生成一张GIF图片], 自己生成了一张 GIF 动态图 rainbow.gif. 下面,使用 GIFLIB ...

  4. 音视频入门-20-BMP、PNG、JPG、GIF静态图生成GIF动态图

    * 音视频入门文章目录 * 静态图 -> 动态图 前面 [18-手动生成一张GIF图片] 和 [19-使用giflib处理GIF图片] 生成的 GIF 每一帧都是一个颜色,平时用到的 GIF 每 ...

  5. 音视频入门-11-PNG文件格式详解

    * 音视频入门文章目录 * PNG 文件格式解析 PNG 图像格式文件由一个 8 字节的 PNG 文件署名域和 3 个以上的后续数据块(IHDR.IDAT.IEND)组成. PNG 文件包括 8 字节 ...

  6. 音视频入门-14-JPEG文件格式详解

    * 音视频入门文章目录 * JPEG 文件格式解析 JPEG 文件使用的数据存储方式有多种.最常用的格式称为 JPEG 文件交换格式(JPEG File Interchange Format,JFIF ...

  7. 音视频入门-08-RGB&YUV

    * 音视频入门文章目录 * YUV & RGB 相互转换公式 YCbCr 的 Y 与 YUV 中的 Y 含义一致,Cb 和 Cr 与 UV 同样都指色彩,Cb 指蓝色色度,Cr 指红色色度,在 ...

  8. 音视频入门-04-BMP图像四字节对齐的问题

    * 音视频入门文章目录 * BMP 图像四字节对齐 表示 BMP 位图中像素的位元是以行为单位对齐存储的,每一行的大小都向上取整为4字节(32 位 DWORD)的倍数.如果图像的高度大于 1,多个经过 ...

  9. 音视频入门-03-RGB转成BMP图片

    * 音视频入门文章目录 * BMP 文件格式解析 BMP 文件由文件头.位图信息头.颜色信息和图形数据四部分组成. 位图文件头(14个字节) 位图信息头(40个字节) 颜色信息 图形数据 文件头与信息 ...

随机推荐

  1. 【linux】【gitlab】gitlab安装、备份、恢复、升级、内存消耗问题

    前言 GitLab:GitLab 是一个用于仓库管理系统的开源项目,使用Git作为代码管理工具,并在此基础上搭建起来的web服务.功能:Gitlab 是一个提供代码托管.提交审核和问题跟踪的代码管理平 ...

  2. Java常识及数据类型

    上次介绍完了JDK的下载,安装,以及配置了环境变量 .这次我们来讲讲Java的常识及Java的数据类型; 常见Java开发工具 编辑器: 1:UltraEdit; 2:EditPlus等; 集成开发环 ...

  3. [python]兔子问题,斐波那契数列 递归&非递归

    假设一对幼年兔子需要一个月长成成年兔子,一对成年兔子一个月后每个月都可以繁衍出一对新的幼年兔子(即兔子诞生两个月后开始繁殖).不考虑死亡的情况,问第 N 个月时共有多少对兔子? 结果前几个月的兔子数量 ...

  4. Android Studio [Activity的生命周期]

    package com.xdw.a122; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; imp ...

  5. Ubuntu server16.04安装配置驱动418.87、cuda10.1、cudnn7.6.4.38、anaconda、pytorch超详细解决

    目录 安装GCC 安装NVIDIA驱动 1. 卸载原有驱动(没装跳过) 2. 禁用nouveau 3. 安装NVIDIA显卡驱动 安装CUDA10.1 安装cudnn 安装anaconda 安装ten ...

  6. Django REST Framework之权限组件

    权限控制是如何实现的? 一般来说,先有认证才有权限,也就是用户登录后才能判断其权限,未登录用户给他一个默认权限. Django接收到一个请求,首先经过权限的检查,如果通过检查,拥有访问的权限,则予以放 ...

  7. Spring 梳理-处理Multipart 请求

    原理讲解 简单的HTTP POST 大家通过HTTP向服务器发送POST请求提交数据,都是通过form表单提交的,代码如下: <form method="post"actio ...

  8. 题解:2018级算法第二次上机 Zexal的竞赛

    题目描述: 样例: 实现解释: 一道需要一点思考的动态规划题目 知识点:动态规划,数据记录 首先将题目描述调整:分别输入不同分数的题目总分(便于后续计算),当获得了i分数的总分后无法获得i-1和i+1 ...

  9. Cobalt Strike之信息收集、木马钓鱼

    System Profiler使用 System Profiler 模块,搜集目标的各类机器信息(操作系统版本,浏览器版本等) Attacks->web drive-by->System ...

  10. 爬虫1:html页面+beautifulsoap模块+get方式+demo

    前言:最近公司要求编写一个爬虫,需要完善后续金融项目的数据,由于工作隐私,就不付被爬的网址url了,下面总结下spider的工作原理. 语言:python:工具:jupyter:    概要:说到爬虫 ...