Last active
October 9, 2023 14:53
-
-
Save t-mat/fed60b83735a80896fa182a77d5259d6 to your computer and use it in GitHub Desktop.
[C++]Generate uncompressed PNG file
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Uncompressed PNG generator | |
// | |
// This is a header-only C++17 library which generates an uncompressed PNG. | |
// It may be useful for a program which requires low complexity but also | |
// needs to use PNG as a container. | |
// | |
// Usage: | |
// #include "UncompressedPngGenerator.hpp" | |
// int main() { | |
// int width = 256, height = 256; | |
// std::vector<uint8_t> buf(width * height * 4); // {R,G,B,A} pixels | |
// | |
// // ... write your content to buf[] here ... | |
// | |
// FILE* fp = fopen("out-rgba-uncompressed.png", "wb"); | |
// UncompressedPngGenerator::makeRgba32UncompressedPngFile( | |
// width, height, | |
// [&](int x, int y) -> std::tuple<uint8_t, uint8_t, uint8_t, uint8_t> { | |
// const auto o = (y * width + x) * 4; | |
// const uint8_t r = buf[o + 0]; | |
// const uint8_t g = buf[o + 1]; | |
// const uint8_t b = buf[o + 2]; | |
// const uint8_t a = buf[o + 3]; | |
// return { r, g, b, a }; | |
// }, | |
// [&](uint8_t c) { | |
// fputc(c, fp); | |
// } | |
// ); | |
// fclose(fp); | |
// } | |
// | |
// Functions: | |
// void makeRgba32UncompressedPngFile(int w, int h, const GetRgbaPixelFunc& get, const PutOneByteFunc& put); | |
// Generates uncompressed PNG file which contains RGBA (32bit) pixels. | |
// | |
// void makeRgb24UncompressedPngFile(int w, int h, const GetRgbaPixelFunc& get, const PutOneByteFunc& put); | |
// Generates uncompressed PNG file which contains RGB (24bit) pixels. | |
// | |
// License: | |
// Copyright (C) 2021, Takayuki Matsuoka. | |
// SPDX-License-Identifier: CC0-1.0 | |
#pragma once | |
#include <stdint.h> | |
#include <assert.h> | |
#include <functional> | |
#include <array> | |
namespace UncompressedPngGenerator | |
{ | |
using GetRgbaPixelFunc = std::function<std::tuple<uint8_t, uint8_t, uint8_t, uint8_t>(int, int)>; | |
using GetRgbPixelFunc = std::function<std::tuple<uint8_t, uint8_t, uint8_t>(int, int)>; | |
using PutOneByteFunc = std::function<void(uint8_t)>; | |
inline void makeRgbaUncompressedPngFile( | |
int bitsPerPixel, int width, int height, GetRgbaPixelFunc const &getRgbaPixel, PutOneByteFunc const &putOneByte) | |
{ | |
class Crc32 | |
{ | |
public: | |
// Using CRC32 algorithm to hash string at compile-time | |
// https://stackoverflow.com/a/36522355 by Deus Sum | |
constexpr std::array<uint32_t, 256> gen_crc32_table() | |
{ | |
// 0xEDB88320 = CRC-32 Reversed Polynomial. | |
// See | |
// https://en.wikipedia.org/wiki/Cyclic_redundancy_check#Polynomial_representations_of_cyclic_redundancy_checks | |
constexpr uint32_t polynomial = 0xEDB88320; | |
auto crc32_table = std::array<uint32_t, 256>{}; | |
for (uint32_t byte = 0; byte < crc32_table.size(); ++byte) { | |
uint32_t crc = byte; | |
for (int i = 0; i < 8; ++i) { | |
auto const m = crc & 1; | |
crc >>= 1; | |
if (m != 0) { | |
crc ^= polynomial; | |
} | |
} | |
crc32_table[byte] = crc; | |
} | |
return crc32_table; | |
} | |
void reset() | |
{ | |
crc = 0xffffffff; | |
} | |
uint32_t get() const | |
{ | |
return ~crc; | |
} | |
void update(uint8_t v) | |
{ | |
static auto const crc32Table = gen_crc32_table(); | |
crc = crc32Table[(crc ^ v) & 0xff] ^ (crc >> 8); | |
}; | |
private: | |
uint32_t crc = 0xffffffff; | |
}; | |
Crc32 crc; | |
// Output functions also calculate CRC-32 automatically. | |
auto const put8 = [&](uint8_t v) { | |
putOneByte(v); | |
crc.update(v); | |
}; | |
auto const put32be = [&](uint32_t v) { | |
put8(static_cast<uint8_t>(v >> 24)); | |
put8(static_cast<uint8_t>(v >> 16)); | |
put8(static_cast<uint8_t>(v >> 8)); | |
put8(static_cast<uint8_t>(v)); | |
}; | |
auto const putFourcc = [&](const char *v) { | |
put8(v[0]); | |
put8(v[1]); | |
put8(v[2]); | |
put8(v[3]); | |
}; | |
auto const put16le = [&](uint16_t v) { | |
put8(static_cast<uint8_t>(v)); | |
put8(static_cast<uint8_t>(v >> 8)); | |
}; | |
{ // File header | |
static uint8_t constexpr pngFileHeader[8] = {0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a}; | |
for (uint8_t v : pngFileHeader) { | |
put8(v); | |
} | |
// IHDR : http://www.libpng.org/pub/png/spec/1.2/PNG-Chunks.html | |
put32be(13); // Chunk length (big endian) | |
crc.reset(); // CRC calculation must ignore length but include chunk name. | |
putFourcc("IHDR"); // Chunk name | |
put32be(static_cast<uint32_t>(width)); // Width (big endian) | |
put32be(static_cast<uint32_t>(height)); // Height (big endian) | |
put8(8); // Bit depth (8: 8-bits per component) | |
put8(bitsPerPixel == 32 ? 6 : 2); // Color type (2:{R,G,B}, 6:{R,G,B,A}) | |
put8(0); // Compresison method (0:Deflate) | |
put8(0); // Filter method (0:adaptive filtering with five basic filter types) | |
put8(0); // Interlace method (0:no interlace) | |
put32be(crc.get()); // CRC-32 (big endian) | |
} | |
{ // IDAT | |
const uint32_t bytesPerPixel = bitsPerPixel / 8; | |
const uint32_t sizeofZlibHeader = 2; // 1(CMF) + 1(FLG) | |
const uint32_t sizeofZlibOverhead = height * (1 + 2 + 2); // 1(BFINAL, BTYPE) + 2(LEN) + 2(NLEN) | |
const uint32_t sizeofZlibFooter = 5 + 4; // 5(Final Empty Block) + 4(Adler32) | |
const uint32_t sizeofRow = 1 + width * bytesPerPixel; // 1(Filter Type) + width * sizeof({R,G,B,A}) | |
const uint32_t length = sizeofZlibHeader + sizeofZlibOverhead + sizeofZlibFooter + sizeofRow * height; | |
assert(sizeofRow < 65536); // This restriction is due to single deflate block size for simplicity. | |
uint32_t adler32_lo = 1, adler32_hi = 0; | |
const auto updateAdler32 = [&](uint8_t x) { | |
const uint32_t MOD_ADLER = 65521; | |
adler32_lo = (adler32_lo + x) % MOD_ADLER; | |
adler32_hi = (adler32_lo + adler32_hi) % MOD_ADLER; | |
}; | |
const auto getAdler32 = [&]() { return (adler32_hi << 16) | adler32_lo; }; | |
// For Adler-32 calculation, we use zput8() for deflate content. | |
const auto zput8 = [&](uint8_t x) { | |
updateAdler32(x); | |
put8(x); | |
}; | |
put32be(length); // Chunk length (big endian) | |
crc.reset(); // CRC calculation must ignore length but include chunk name. | |
putFourcc("IDAT"); // Chunk name | |
// Beginning of the deflate stream | |
put8(0x78); // deflate: CM=8, CINFO=7 | |
put8(0x01); // deflate: FLG=1 | |
for (int y = 0; y < height; ++y) { | |
put8(0); // deflate: 0:not a last block, 00:uncompressed block | |
put16le(sizeofRow); // deflate: length of the block (little endian) | |
put16le(sizeofRow ^ 0xffff); // deflate: length of the block (little endian, inverted) | |
zput8(0); // filter type: 0=none | |
for (int x = 0; x < width; ++x) { | |
const auto [r, g, b, a] = getRgbaPixel(x, y); | |
zput8(r); | |
zput8(g); | |
zput8(b); | |
if (bitsPerPixel == 32) { | |
zput8(a); | |
} | |
} | |
} | |
// Empty terminator block | |
put8(1); // deflate: 1:last block, 00:uncompressed block | |
put16le(0); // deflate: length of the block (little endian) | |
put16le(0 ^ 0xffff); // deflate: length of the block (little endian, inverted) | |
// End of the deflate stream | |
put32be(getAdler32()); // deflate: Adler-32 (big endian) | |
// End of the IDAT chunk | |
put32be(crc.get()); // CRC-32 (big endian) | |
} | |
// IEND | |
put32be(0); // Chunk length (big endian) | |
putFourcc("IEND"); | |
put32be(0xae426082); // CRC-32 (big endian). Since IEND doesn't have content, its CRC-32 is constant. | |
} | |
inline void makeRgba32UncompressedPngFile(int width, | |
int height, | |
const GetRgbaPixelFunc &getRgbaPixel, | |
const PutOneByteFunc &putOneByte) | |
{ | |
return makeRgbaUncompressedPngFile(32, width, height, getRgbaPixel, putOneByte); | |
} | |
inline void makeRgb24UncompressedPngFile(int width, | |
int height, | |
const GetRgbPixelFunc &getRgbPixel, | |
const PutOneByteFunc &putOneByte) | |
{ | |
return makeRgbaUncompressedPngFile( | |
24, | |
width, | |
height, | |
[&](int x, int y) -> std::tuple<uint8_t, uint8_t, uint8_t, uint8_t> { | |
const auto [r, g, b] = getRgbPixel(x, y); | |
return {r, g, b, 0xff}; | |
}, | |
putOneByte); | |
} | |
} // namespace UncompressedPngGenerator | |
#if defined(MY_UNCOMPRESSED_PNG_GENERATOR_TEST) | |
# include <stdio.h> | |
# include <stdlib.h> | |
# include <math.h> | |
# if defined(MY_UNCOMPRESSED_PNG_GENERATOR_TEST_USE_LODEPNG) | |
# include "lodepng/lodepng.cpp" // https://lodev.org/lodepng/ | |
# endif | |
int main() | |
{ | |
int const width = 256, height = 256; | |
std::vector<uint8_t> buf(width * height * 4); | |
for (int y = 0; y < height; ++y) { | |
for (int x = 0; x < width; ++x) { | |
auto const o = (y * width + x) * 4; | |
uint8_t const r = static_cast<uint8_t>(x); | |
uint8_t const g = static_cast<uint8_t>(y); | |
uint8_t const b = static_cast<uint8_t>(sqrt(x * x + y * y)); | |
uint8_t const a = (((x / 64) ^ (y / 64)) & 1) == 0 ? 0x80 : 0xff; | |
buf[o + 0] = r; | |
buf[o + 1] = g; | |
buf[o + 2] = b; | |
buf[o + 3] = a; | |
} | |
} | |
FILE *fp = fopen("out.png", "wb"); | |
UncompressedPngGenerator::makeRgba32UncompressedPngFile( | |
width, | |
height, | |
[&](int x, int y) -> std::tuple<uint8_t, uint8_t, uint8_t, uint8_t> { | |
auto const o = (y * width + x) * 4; | |
uint8_t const r = buf[o + 0]; | |
uint8_t const g = buf[o + 1]; | |
uint8_t const b = buf[o + 2]; | |
uint8_t const a = buf[o + 3]; | |
return {r, g, b, a}; | |
}, | |
[&](uint8_t c) { fputc(c, fp); }); | |
fclose(fp); | |
# if defined(MY_UNCOMPRESSED_PNG_GENERATOR_TEST_USE_LODEPNG) | |
{ | |
const char *filename = "out.png"; | |
unsigned char *out = nullptr; | |
unsigned w, h; | |
auto const lpr = lodepng_decode32_file(&out, &w, &h, filename); | |
printf("lodepng result = 0x%08x\n", lpr); | |
if (lpr != 0) { | |
printf("lodepng error : %s\n", lodepng_error_text(lpr)); | |
} | |
} | |
# endif | |
} | |
#endif // defined(MY_UNCOMPRESSED_PNG_GENERATOR_TEST) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment