音视频入门-12-手动生成一张PNG图片

* 音视频入门文章目录 *

预热

上一篇 【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 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 分辨率为例:

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 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 图片:

generate-png-by-hand.jpg

Congratulations!


代码:
11-rgb-to-png

参考资料:

Portable Network Graphics (PNG) Specification and Extensions

gzip,deflate,zlib辨析

Zlib库的安装与使用

内容有误?联系作者:

联系作者

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,923评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,154评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,775评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,960评论 1 290
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,976评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,972评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,893评论 3 416
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,709评论 0 271
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,159评论 1 308
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,400评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,552评论 1 346
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,265评论 5 341
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,876评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,528评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,701评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,552评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,451评论 2 352

推荐阅读更多精彩内容