用MiniZ生成PNG文件

编程语言 2026-07-11

受到维基百科关于PNG的页面的启发,我在尝试写一个用来写PNG的小函数。

我不想重写libPNG或 spng,我只想要一个简单的函数来写PNG文件,输入一个文件名和一些像素数据。我不需要PNG的所有选项:我只想要真彩色,过滤方法为0 的type 0(等同于没有过滤),只要最基本的。

维基百科上的示例相当直观:一个1x1的红色像素PNG,被编码为:

{ 00 FF 00 00 }

第一个问题:如果我理解正确,第一個0x00代表过滤方法0 的类型0,那么就能得到我们的小红像素(FF = 红色255,绿色和蓝色为0)。每一行可以定义自己的类型,生成PNG的软件会选择最合适的类型(但我并不追求那种聪明的做法)。

我已经写好了用于生成它的静态代码(#FIXED开关):它能够工作,我得到了美丽的红色像素。

为了让它从任意像素数组生成PNG图像,我尝试降低动态性。首先我重新实现了一个CRC32的计算函数,成功了;随后我尝试用 MiniZ 库去压缩同样的像素数据,但得到的只是一个悲伤的黑色像素。

在PNG格式中,原始像素数据是用DEFLATE进行编码的,这个过程是非确定性的,在维基百科页面的示例中可能会产生以下结果:

{ 63 F8 CF C0 00 00 }

使用miniz,我尝试了两种方法:

int status = mz_compress(buffer_out, &buffer_out_length, buffer_in, buffer_in_length);
or 
int status = mz_compress2(buffer_out, &buffer_out_length, buffer_in, buffer_in_length, MZ_FIXED | MZ_DEFAULT_COMPRESSION);

得到的压缩值不同,但解压后会还原为初始数据。因此我可以做一次往返,但我真的在使用DEFLATE吗?文档相当有限。使用这个函数应该会产生所需的压缩数据和所需的Zlib头。

但当我尝试打开生成的图片时,只有一个黑色像素……我在编码数据时一定做错了什么,但到底是什么……我把压缩数据放在了末尾。

我是在Windows上编写这段代码。

任何帮助我理解错误之处的建议,将不胜感激。

#include <stdint.h> // int32_t
#include ".\libraries\miniz.h"

// compile with : cl .\q1.c .\libraries\miniz.c /Fe:q1

//#define FIXED

// from https://www.w3.org/TR/png/#D-CRCAppendix
uint32_t calc_crc32(unsigned char * buffer, uint32_t length)
{
    // Init table
    uint32_t c, crc_table[256];
    uint16_t n, k;

    for (n = 0; n < 256; n++)
    {
        c = (uint32_t)n;
        for (k = 0; k < 8; k++)
        {
            if (c & 1)
                c = 0xedb88320L ^ (c >> 1);
            else
                c = c >> 1;
        }
        crc_table[n] = c;
    }
    // Compute
    c = 0xffffffffu;
    for (n = 0; n < length; n++) {
        c = crc_table[(c ^ buffer[n]) & 0xff] ^ (c >> 8);
    }
    printf("crc32 = %u\n", c ^ 0xffffffffu);
    return c ^ 0xffffffffu;
}

int main(int argc, const char *argv[])
{
    const char * file_path = "q1.png";

    printf("Version of miniz.c : %s\n", MZ_VERSION);

    // PNG signature
    uint8_t signature[8] = {0};
    signature[0] = 0x89;
    signature[1] = 0x50; // P in ASCII
    signature[2] = 0x4E; // N in ASCII
    signature[3] = 0x47; // G in ASCII
    signature[4] = 0x0D; // CR
    signature[5] = 0x0A; // LF (CRLF = DOS line ending)
    signature[6] = 0x1A; // DOS end of file
    signature[7] = 0x0A; // LF (UNIX line ending)
    printf("Signature OK\n");

    // IHDR = image header
    // IHDR - LENGTH (4)
    uint32_t length = 13;
    uint32_t swapped_length = _byteswap_ulong(length); // 00 00 00 0D;
    // IHDR - CHUNK TYPE (4)
    uint8_t type[4] = {'I', 'H', 'D', 'R'}; // 49 48 44 52
    uint32_t width = _byteswap_ulong(1);
    uint32_t height = _byteswap_ulong(1);
    uint8_t bits_per_channel = 8;
    uint8_t color_type = 2;
    uint8_t compression_method = 0;
    uint8_t filter_method = 0;
    uint8_t interlaced = 0;
    // Writing to buffer
    uint32_t idhr_length = 4 + 4 + length + 4;
    uint8_t idhr[4 + 4 + 13 + 4] = {0};
    memcpy(idhr, &swapped_length, 4);
    memcpy(idhr + 4, &type, 4);
    memcpy(idhr + 8, &width, 4);
    memcpy(idhr + 12, &height, 4);
    memcpy(idhr + 16, &bits_per_channel, 1);
    memcpy(idhr + 17, &color_type, 1);
    memcpy(idhr + 18, &compression_method, 1);
    memcpy(idhr + 19, &filter_method, 1);
    memcpy(idhr + 20, &interlaced, 1);
    // IHDR - CRC (4) on type & content but not length
    //uint32_t crc = _byteswap_ulong(0x907753DE); // 2423739358
    uint32_t crc = _byteswap_ulong(calc_crc32(idhr + 4, length + 4)); // ignore length, add chunck type length
    memcpy(idhr + 21, &crc, 4);
    printf("IHDR OK\n");

    // IDAT
    // IDAT - LENGTH (4)
    length = 12;
    swapped_length = _byteswap_ulong(length); // 00 00 00 0C
    // IDAT - CHUNK TYPE (4)
    type[0] = 'I'; // 49
    type[1] = 'D'; // 44
    type[2] = 'A'; // 41
    type[3] = 'T'; // 54
    // IDAT - DATA (12)
#ifdef FIXED
    compression_method = 0x08; // Obligatoire et fixe pour PNG
    uint8_t fcheck = 0xD7;
    uint8_t *data = malloc(sizeof(uint8_t) * 6);
    data[0] = 0x63;
    data[1] = 0xF8;
    data[2] = 0xCF;
    data[3] = 0xC0;
    data[4] = 0x00;
    data[5] = 0x00;
    uint32_t adler32 = _byteswap_ulong(0x03010100);

    // Writing to buffer
    uint32_t idat_length = 4 + 4 + length + 4;
    uint8_t idat[4 + 4 + 12 + 4] = {0};
    memcpy(idat, &length, 4);
    memcpy(idat + 4, &type, 4);
    memcpy(idat + 8, &compression_method, 1);
    memcpy(idat + 9, &fcheck, 1);
    memcpy(idat + 10, data, 6);
    memcpy(idat + 16, &adler32, 4);
    // IDAT - CRC (4) on type & content but not length
    //crc = _byteswap_ulong(0x18DD8DB0); // 417172912
    crc = _byteswap_ulong(calc_crc32(idat + 4, length + 4)); // ignore length idat + 4, add chunck type length + 4 (avant)
    memcpy(idat + 20, &crc, 4);
    printf("IDAT OK\n");
#else
    // Compression test
    uint8_t buffer_in[4] = {0x00, 0xFF, 0x00, 0x00}; // type 0 de method filtre 0 puis un pixel rouge
    size_t buffer_in_length = sizeof(buffer_in);
    // Le résultat doit être : 63 F8 CF C0 00 00
    uint8_t buffer_out[256] = {0};
    size_t buffer_out_length = sizeof(buffer_out);
    // Test1 int status = mz_compress(buffer_out, &buffer_out_length, buffer_in, buffer_in_length);
    // Test2
    int status = mz_compress2(buffer_out, &buffer_out_length, buffer_in, buffer_in_length, MZ_FIXED | MZ_DEFAULT_COMPRESSION);
    if (status != Z_OK)
    {
        printf("mz_compress() failed!\n");
        return;
    }
    printf("Compressed output %zu bytes:\n", buffer_out_length);
    for (uint16_t i = 0; i < buffer_out_length; i++) // stream.total_out; i++)
    {
        printf("%02X\n", buffer_out[i], i);
    }
    uint8_t buffer_uncompressed[256] = {0};
    size_t buffer_uncompressed_length = sizeof(buffer_uncompressed);
    status = mz_uncompress2(buffer_uncompressed, &buffer_uncompressed_length, buffer_out, &buffer_out_length);
    printf("Uncompressed %zu bytes: \n", buffer_out_length);
    for (uint16_t i = 0; i < buffer_uncompressed_length; i++)
    {
        printf("0x%02x\n", buffer_uncompressed[i]);
    }
    length = buffer_out_length;
    swapped_length = _byteswap_ulong(length);
    uint32_t idat_length = 4 + 4 + length + 4;
    uint8_t * idat = calloc(idat_length, sizeof(uint8_t));
    crc = _byteswap_ulong(calc_crc32(idat + 4, length + 4)); // ignore length idat + 4, add chunck type length + 4 (avant)
    memcpy(idat, &length, 4);
    memcpy(idat + 4, &type, 4);
    memcpy(idat + 8, buffer_out, buffer_out_length);
    memcpy(idat + 20, &crc, 4);
    printf("IDAT OK\n");
#endif

    // IEND
    // IEND - LENGTH (4)
    length = 0;
    swapped_length = _byteswap_ulong(length);
    // IEND - CHUNK TYPE (4)
    type[0] = 'I'; // 49
    type[1] = 'E'; // 45
    type[2] = 'N'; // 4E
    type[3] = 'D'; // 44
    // Writing to buffer
    uint32_t iend_length = 4 + 4 + length + 4;
    uint8_t iend[4 + 4 + 0 + 4] = {0};
    memcpy(iend, &swapped_length, 4);
    memcpy(iend + 4, &type, 4);
    // IEND - CRC (4) on type & content but not length
    // crc = _byteswap_ulong(0xAE426082); // 2923585666
    crc = _byteswap_ulong(calc_crc32(iend + 4, length + 4)); // ignore length, add chunck type length
    memcpy(iend + 8, &crc, 4);
    printf("IDEND OK\n");

    // Writing file
    FILE *f = fopen(file_path, "wb");
    if (f == NULL)
    {
        printf("ERROR: Unable to open file %s for writing.\n", file_path);
        return;
    }
    fwrite(signature, 1, 8, f);
    fwrite(idhr, 1, idhr_length, f);
    fwrite(idat, 1, idat_length, f);
    fwrite(iend, 1, iend_length, f);
    fclose(f);
    printf("Saved at : %s\n", file_path);
}

/*

output with FIXED q1.png
89 50 4E 47 0D 0A 1A 0A 00 00 00 0D 49 48 44 52 00 00 00 01 00 00 00 01 08 02 00 00 00 90 77 53 DE 0C 00 00 00 49 44 41 54 08 D7 63 F8 CF C0 00 00 03 01 01 00 18 DD 8D B0 00 00 00 00 49 45 4E 44 AE 42 60 82

output without FIXED T1 (mz_compress) q1.png
89 50 4E 47 0D 0A 1A 0A 00 00 00 0D 49 48 44 52 00 00 00 01 00 00 00 01 08 02 00 00 00 90 77 53 DE 0F 00 00 00 49 44 41 54 78 9C 01 04 00 FB FF 00 FF 00 00 03 DA 08 C9 6F 00 00 00 00 00 00 00 49 45 4E 44 AE 42 60 82

output without FIXED T2 (mz_compress2 with MZ_FIXED | MZ_DEFAULT_COMPRESSION) q1.png
89 50 4E 47 0D 0A 1A 0A 00 00 00 0D 49 48 44 52 00 00 00 01 00 00 00 01 08 02 00 00 00 90 77 53 DE 0F 00 00 00 49 44 41 54 78 9C 01 04 00 FB FF 00 FF 00 00 03 DA 08 C9 6F 00 00 00 00 00 00 00 49 45 4E 44 AE 42 60 82

*/

解决方案

看了一下你的PNG输出,首先让我注意到的是你把IDAT块的长度写成了错误的字节序。PNG里的整数是高字节在前。你写成了0c 00 00 00,而应该写成00 00 00 0c(前提是12是正确的长度)。

更新:

我查看了代码,发现还有两个错误。CRC是在要放入IDAT的类型和数据的内存被写入之前就计算了!因此CRC是在垃圾数据上计算的。然后CRC被写到了IDAT块中的一个固定且错误的位置。它本应写入到 8 + length 的偏移处。

一旦改正了这一切,就会生成一个只有一个红色像素的有效PNG文件。上述说明适用于未定义 FIXED 的代码。当定义了 FIXED 时,IDAT块也会出现相同的字节序错误。

顺便说一句,题中的代码并非可移植的,因为它假设了一个小端机器。下面给出一个简单、可移植的C 语言PNG文件生成器的示例:

// easypng.c -- an example of a simple PNG file generator.
// Placed in the public domain by Mark Adler, 5 Apr 2026.

// The downsides of this simple example are that this is limited to PNG images
// with eight bits per sample, that interlaced and palette-based images are not
// supported, and that the image and its compression are stored in memory in
// their entirety.

#include <stdio.h>      // FILE, fwrite, perror, fread, ferror, fputs, stderr,
                        // getc, EOF, stdin, stdout
#include <stdlib.h>     // size_t, exit, calloc, malloc, free, strtol
#include <stdint.h>     // uint8_t, uint32_t, int32_t, INT32_MIN
#include <errno.h>      // errno
#include <assert.h>     // assert

// define MINIZ to use miniz instead of zlib.
#ifdef MINIZ
#  include "miniz/miniz.h"
#else
#  include "zlib.h"     // crc32, compressBound_z, compress_z, Z_OK
#endif

// When using MSVC or any other compiler with 32-bit longs, and when using
// miniz or a version of zlib before 1.3.2, the raw and compressed image sizes
// will be limited to 4 GB.
#if defined(MINIZ) || ZLIB_VERNUM < 0x1320
size_t compressBound_z(size_t len) {
    unsigned long n = (unsigned long)len;
    assert(n == len && "image too big for unsigned long");
    return compressBound(n);
}
int compress_z(void *dst, size_t *dlen, void const *src, size_t slen) {
    unsigned long d = (unsigned long)*dlen, s = (unsigned long)slen;
    assert(d == *dlen && s == slen && "image too big for unsigned long");
    int ret = compress(dst, &d, src, s);
    *dlen = d;
    return ret;
}
#endif

// Write val as a four-byte big-endian integer to big. Return big.
static uint8_t *big4(uint8_t *big, uint32_t val) {
    big[0] = val >> 24;
    big[1] = (uint8_t)(val >> 16);
    big[2] = (uint8_t)(val >> 8);
    big[3] = (uint8_t)val;
    return big;
}

// Write a PNG chunk to out of type type[0..3] with the len bytes at dat.
static void chunk(FILE *out, char const *type, void const *dat, uint32_t len) {
    // Compute CRC.
    uint32_t crc = crc32(0, (uint8_t const *)type, 4);
    if (len)
        crc = crc32(crc, dat, len);

    // Write chunk.
    uint8_t big[4];
    if (fwrite(big4(big, len), 1, 4, out) < 4 ||    // length
        fwrite(type, 1, 4, out) < 4 ||              // type
        fwrite(dat, 1, len, out) < len ||           // data
        fwrite(big4(big, crc), 1, 4, out) < 4) {    // crc
        perror("** write error");
        exit(1);
    }
}

// Write a PNG signature to out.
static void signature(FILE *out) {
    if (fwrite("\x89PNG\r\n\x1a\n", 1, 8, out) < 8) {
        perror("** write error");
        exit(1);
    }
}

// Write a PNG header chunk to out for a width by height image with bpp bytes
// per pixel. width and height must be in the range 1..2^31-1. bpp must be in
// the range 1..4, where 1 is gray-scale, 2 is gray-scale with alpha, 3 is RGB
// color, 4 is RGB color with alpha.
static void header(FILE *out, int32_t width, int32_t height, int32_t bpp) {
    uint8_t head[13];
    big4(head, (uint32_t)width);
    big4(head + 4, (uint32_t)height);
    head[8] = 8;                // bits per sample
    head[9] = ((bpp - 1) & 2) | (((bpp - 1) & 1) << 2);     // 0, 4, 2, 6
    head[10] = 0;               // compression (only 0 allowed)
    head[11] = 0;               // filter (only 0 allowed)
    head[12] = 0;               // no interlacing
    chunk(out, "IHDR", head, 13);
}

// These four functions: sub, up, average, and paeth, apply that PNG filter to
// the image in place. The parameters are the same for all of them, where row
// is the number of bytes in a row, excluding the filter byte, size is the size
// of the image data in bytes, including the filter bytes, and bpp is the
// number of bytes per pixel. row must be a multiple of bpp. size must be a
// multiple of row + 1. row and size must be non-zero.
//
// Since the up filter has no effect on the first row, up() applies the sub
// filter to the first row. Similarly, since the average filter uses half the
// preceding pixel as the predictor for the first row, which would be a good
// predictor only for exponentially increasing pixel values, average() also
// applies the sub filter to the first row instead of the average filter. Since
// the paeth filter devolves into the sub filter for the first row, paeth()
// marks it as a sub filter to simplify reconstruction. The bottom line (so to
// speak) is that the sub filter is always used for the first row, unless there
// is no filtering.
static void sub(size_t row, size_t size, int32_t bpp, uint8_t *image) {
    for (size_t i = 0; i < size; i += row + 1) {
        image[i] = 1;
        for (size_t j = i + row; j > i + bpp; j--)
            image[j] -= image[j - bpp];
    }
}
static void up(size_t row, size_t size, int32_t bpp, uint8_t *image) {
    for (size_t i = size - row - 1; i; i -= row + 1) {
        image[i] = 2;
        for (size_t j = i + 1; j < i + row + 1; j++)
            image[j] -= image[j - row - 1];
    }
    image[0] = 1;
    for (size_t j = row; j > (size_t)bpp; j--)
        image[j] -= image[j - bpp];
}
static void average(size_t row, size_t size, int32_t bpp, uint8_t *image) {
    for (size_t i = size - row - 1; i; i -= row + 1) {
        image[i] = 3;
        size_t j = i + row;
        for (; j > i + bpp; j--)
            image[j] -= (image[j - bpp] + image[j - row - 1]) >> 1;
        for (; j > i; j--)
            image[j] -= image[j - row - 1] >> 1;
    }
    image[0] = 1;
    for (size_t j = row; j > (size_t)bpp; j--)
        image[j] -= image[j - bpp];
}
static int pred(int a, int b, int c) {
    // a = left, b = above, c = above and left
    int p = a + b - c;
    int pa = p < a ? a - p : p - a;
    int pb = p < b ? b - p : p - b;
    int pc = p < c ? c - p : p - c;
    return pa <= pb && pa <= pc ? a : pb <= pc ? b : c;
}
static void paeth(size_t row, size_t size, int32_t bpp, uint8_t *image) {
    for (size_t i = size - row - 1; i; i -= row + 1) {
        image[i] = 4;
        size_t j = i + row;
        for (; j > i + bpp; j--)
            image[j] -= (uint8_t)pred(image[j - bpp], image[j - row - 1],
                                      image[j - row - 1 - bpp]);
        for (; j > i; j--)
            image[j] -= image[j - row - 1];
    }
    image[0] = 1;
    for (size_t j = row; j > (size_t)bpp; j--)
        image[j] -= image[j - bpp];
}

// Write a PNG data chunk to out using raw pixel data from in. See the header()
// comments above for the parameter constraints. filt is the filter to apply in
// 0..4. Return 0 if in provided the correct number of input bytes, 1 if it was
// over, or -1 if it was under.
static int data(FILE *out, int32_t width, int32_t height, int32_t bpp,
                int32_t filt, FILE *in) {
    // Load the raw image data from in. If there's not enough, the remainder is
    // filled with zeros. Each row initially has no filtering.
    int awry = 0;
    size_t row = (size_t)width * bpp;
    size_t size = (size_t)height * (row + 1);
    assert(bpp && row / bpp == (size_t)width &&
           size / (row + 1) == (size_t)height &&
           "size_t not large enough for image dimensions");
            // that can't happen for a 64-bit size_t
    uint8_t *image = calloc(size, 1);       // fill with zeros
    assert(image != NULL && "out of memory");
    for (size_t i = 0; i < size; i += row + 1)
        if (fread(image + i + 1, 1, row, in) < row) {
            if (ferror(in)) {
                perror("** read error");
                exit(1);
            }
            awry = -1;      // not enough input
            break;
        }
    if (awry == 0 && getc(in) != EOF)
        awry = 1;           // too much input

    // Apply the requested filter, if any.
    if (filt == 1)
        sub(row, size, bpp, image);
    else if (filt == 2)
        up(row, size, bpp, image);
    else if (filt == 3)
        average(row, size, bpp, image);
    else if (filt == 4)
        paeth(row, size, bpp, image);

    // Compress the image.
    size_t len = compressBound_z(size);
    uint8_t *comp = malloc(len);
    assert(comp != NULL && "out of memory");
    int ret = compress_z(comp, &len, image, size);
    assert(ret == Z_OK && "out of memory");

    // Write the compressed image data. Limit the IDAT chunk sizes.
    uint8_t *next = comp;
    uint32_t const max = 1UL << 20;     // limit data per chunk to 1MB
    assert(max > 0 && "max can't be zero");
    while (len > max) {
        chunk(out, "IDAT", next, max);
        next += max;
        len -= max;
    }
    chunk(out, "IDAT", next, (uint32_t)len);

    // Clean up.
    free(comp);
    free(image);
    return awry;
}

// Write a PNG end chunk to out.
static void end(FILE *out) {
    chunk(out, "IEND", NULL, 0);
}

// Return str taken as a signed decimal 32-bit integer. If it's not valid or
// out of range, then the minimum negative 32-bit value is returned.
static int32_t toi32(char const *str) {
    errno = 0;
    char *end;
    long got = strtol(str, &end, 10);
    int32_t ret = got;
    return *str == 0 || *end || errno || ret != got ? INT32_MIN : ret;
}

// Circumvent Windows insanity.
#ifdef _WIN32
#  include <fcntl.h>
#  include <io.h>
#  define BINARY(file) _setmode(_fileno(file), _O_BINARY)
#else
#  define BINARY(file)
#endif

// Read raw image data from stdin and write a PNG file to stdout. The image
// characteristics are given on the command line per the help below. The width
// and height must both be in the range 1..2^31-1, bpp must be in 1..4, and
// filter must be in 0..4. If the data on stdin comes up short, then the
// remainder of the raw image is filled with zeros.
int main(int argc, char **argv) {
    // Show help.
    if (argc == 1) {
        fputs("usage: easypng width height [bpp] [filter] < rawpixels\n"
              "       bpp is bytes per pixel as 1 (G, default), 2 (GA), "
                     "3 (RGB), or 4 (RGBA)\n"
              "       filter is 0 (none, default), 1 (sub), 2 (up), "
                     "3 (average), or 4 (paeth)\n",
              stderr);
        return 0;
    }

    // Get the image parameters from the command line.
    int32_t width, height, bpp = 1, filt = 0;
    if (argc < 3 || argc > 5 ||
        (width = toi32(argv[1])) < 1 ||
        (height = toi32(argv[2])) < 1 ||
        (argc >= 4 && ((bpp = toi32(argv[3])) < 1 || bpp > 4)) ||
        (argc == 5 && ((filt = toi32(argv[4])) < 0 || filt > 4))) {
        fputs("** invalid arguments\n", stderr);
        return 1;
    }

    // Write PNG signature, header, image data, and end chunks to stdout,
    // reading the raw image data from stdin.
    BINARY(stdin);
    BINARY(stdout);
    signature(stdout);
    header(stdout, width, height, bpp);
    int ret = data(stdout, width, height, bpp, filt, stdin);
    if (ret)
        fputs(ret < 0 ? "!! input short\n" : "!! input long\n", stderr);
    end(stdout);
    return 0;
}
站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章