Skip to content

Commit

Permalink
Optimize and improve built-in PNG writer (#14020)
Browse files Browse the repository at this point in the history
  • Loading branch information
sfan5 committed Dec 27, 2023
1 parent 5054918 commit 93dfa8a
Show file tree
Hide file tree
Showing 4 changed files with 120 additions and 26 deletions.
7 changes: 7 additions & 0 deletions builtin/game/misc_s.lua
Expand Up @@ -64,6 +64,13 @@ function core.encode_png(width, height, data, compression)
error("Incorrect type for 'height', expected number, got " .. type(height))
end

if width < 1 then
error("Incorrect value for 'width', must be at least 1")
end
if height < 1 then
error("Incorrect value for 'height', must be at least 1")
end

local expected_byte_count = width * height * 4

if type(data) ~= "table" and type(data) ~= "string" then
Expand Down
5 changes: 2 additions & 3 deletions doc/lua_api.md
Expand Up @@ -5418,9 +5418,8 @@ Utilities
* `compression`: Optional zlib compression level, number in range 0 to 9.
The data is one-dimensional, starting in the upper left corner of the image
and laid out in scanlines going from left to right, then top to bottom.
Please note that it's not safe to use string.char to generate raw data,
use `colorspec_to_bytes` to generate raw RGBA values in a predictable way.
The resulting PNG image is always 32-bit. Palettes are not supported at the moment.
You can use `colorspec_to_bytes` to generate raw RGBA values.
Palettes are not supported at the moment.
You may use this to procedurally generate textures during server init.
* `minetest.urlencode(str)`: Encodes non-unreserved URI characters by a
percent sign followed by two hex digits. See
Expand Down
30 changes: 26 additions & 4 deletions games/devtest/mods/testnodes/textures.lua
Expand Up @@ -105,6 +105,19 @@ local function gen_checkers(w, h, tile)
return r
end

-- The engine should perform color reduction of the generated PNG in certain
-- cases, so we have this helper to check the result
local function encode_and_check(w, h, ctype, data)
local ret = core.encode_png(w, h, data)
assert(ret:sub(1, 8) == "\137PNG\r\n\026\n", "missing png signature")
assert(ret:sub(9, 16) == "\000\000\000\rIHDR", "didn't find ihdr chunk")
local ctype_actual = ret:byte(26) -- Color Type (1 byte)
ctype = ({rgba=6, rgb=2, gray=0})[ctype]
assert(ctype_actual == ctype, "png should have color type " .. ctype ..
" but actually has " .. ctype_actual)
return ret
end

local fractal = mandelbrot(512, 512, 128)
local frac_emb = mandelbrot(64, 64, 64)
local checker = gen_checkers(512, 512, 32)
Expand All @@ -129,17 +142,21 @@ for i=1, #fractal do
b = floor(abs(1 - fractal[i]) * 255),
a = 255,
}
data_ck[i] = checker[i] > 0 and "#F80" or "#000"
data_ck[i] = checker[i] > 0 and "#888" or "#000"
end

fractal = nil
frac_emb = nil
checker = nil

local textures_path = minetest.get_modpath( minetest.get_current_modname() ) .. "/textures/"
minetest.safe_file_write(
textures_path .. "testnodes_generated_mb.png",
minetest.encode_png(512,512,data_mb)
encode_and_check(512, 512, "rgb", data_mb)
)
minetest.safe_file_write(
textures_path .. "testnodes_generated_ck.png",
minetest.encode_png(512,512,data_ck)
encode_and_check(512, 512, "gray", data_ck)
)

minetest.register_node("testnodes:generated_png_mb", {
Expand All @@ -155,7 +172,8 @@ minetest.register_node("testnodes:generated_png_ck", {
groups = { dig_immediate = 2 },
})

local png_emb = "[png:" .. minetest.encode_base64(minetest.encode_png(64,64,data_emb))
local png_emb = "[png:" .. minetest.encode_base64(
encode_and_check(64, 64, "rgba", data_emb))

minetest.register_node("testnodes:generated_png_emb", {
description = S("Generated In-Band Mandelbrot PNG Test Node"),
Expand All @@ -182,6 +200,10 @@ minetest.register_node("testnodes:generated_png_dst_emb", {
groups = { dig_immediate = 2 },
})

data_emb = nil
data_mb = nil
data_ck = nil

--[[
The following nodes can be used to demonstrate the TGA format support.
Expand Down
104 changes: 85 additions & 19 deletions src/util/png.cpp
Expand Up @@ -19,50 +19,116 @@ with this program; if not, write to the Free Software Foundation, Inc.,

#include "png.h"
#include <string>
#include <optional>
#include <sstream>
#include <zlib.h>
#include <cassert>
#include "util/serialize.h"
#include "serialization.h"
#include "irrlichttypes.h"

static void writeChunk(std::ostringstream &target, const std::string &chunk_str)
enum {
COLOR_GRAY = 0,
COLOR_RGB = 2,
COLOR_RGBA = 6,
};

static void writeChunk(std::string &target, const std::string &chunk_str)
{
assert(chunk_str.size() >= 4);
assert(chunk_str.size() - 4 < U32_MAX);
writeU32(target, chunk_str.size() - 4); // Write length minus the identifier
target << chunk_str;
writeU32(target, crc32(0,(const u8*)chunk_str.data(), chunk_str.size()));
u8 tmp[4];
target.reserve(target.size() + 4 + chunk_str.size() + 4);

writeU32(tmp, chunk_str.size() - 4); // Length minus the identifier
target.append(reinterpret_cast<char*>(tmp), 4);
target.append(chunk_str); // Data
const u32 csum = crc32(0, reinterpret_cast<const u8*>(chunk_str.data()),
chunk_str.size());
writeU32(tmp, csum); // CRC32 checksum
target.append(reinterpret_cast<char*>(tmp), 4);
}

static std::optional<u8> reduceColor(const u8 *data, u32 width, u32 height, std::string &new_data)
{
const u32 npixels = width * height;
// check if the alpha channel is all opaque
for (u32 i = 0; i < npixels; i++) {
if (data[4*i + 3] != 255)
return std::nullopt;
}

// check if RGB components are identical
bool gray = true;
for (u32 i = 0; i < npixels; i++) {
const u8 *pixel = &data[4*i];
if (pixel[0] != pixel[1] || pixel[1] != pixel[2]) {
gray = false;
break;
}
}

if (gray) {
// convert to grayscale
new_data.resize(width * height);
u8 *dst = reinterpret_cast<u8*>(new_data.data());
for (u32 i = 0; i < npixels; i++)
dst[i] = data[4*i];
return COLOR_GRAY;
} else {
// convert to RGB
new_data.resize(width * 3 * height);
u8 *dst = reinterpret_cast<u8*>(new_data.data());
for (u32 i = 0; i < npixels; i++)
memcpy(&dst[3*i], &data[4*i], 3);
return COLOR_RGB;
}
}

std::string encodePNG(const u8 *data, u32 width, u32 height, s32 compression)
{
std::ostringstream file(std::ios::binary);
file << "\x89PNG\r\n\x1a\n";
u8 color_type = COLOR_RGBA;
std::string new_data;
if (compression == Z_DEFAULT_COMPRESSION || compression >= 2) {
// try to reduce the image data to grayscale or RGB
if (auto ret = reduceColor(data, width, height, new_data); ret.has_value()) {
color_type = ret.value();
assert(!new_data.empty());
data = reinterpret_cast<u8*>(new_data.data());
}
}

std::string file;
file.append("\x89PNG\r\n\x1a\n");

{
std::ostringstream IHDR(std::ios::binary);
IHDR << "IHDR";
writeU32(IHDR, width);
writeU32(IHDR, height);
// 8 bpp, color type 6 (RGBA)
IHDR.write("\x08\x06\x00\x00\x00", 5);
writeChunk(file, IHDR.str());
std::ostringstream header(std::ios::binary);
header << "IHDR";
writeU32(header, width);
writeU32(header, height);
writeU8(header, 8); // bpp
writeU8(header, color_type);
header.write("\x00\x00\x00", 3);
writeChunk(file, header.str());
}

{
std::ostringstream IDAT(std::ios::binary);
IDAT << "IDAT";
std::ostringstream scanlines(std::ios::binary);
const u32 ps = color_type == COLOR_GRAY ? 1 :
(color_type == COLOR_RGB ? 3 : 4);
std::string scanlines;
scanlines.reserve(width * ps * height + height);
for(u32 i = 0; i < height; i++) {
scanlines.write("\x00", 1); // Null predictor
scanlines.write((const char*) data + width * 4 * i, width * 4);
scanlines.append(1, 0); // Null predictor
scanlines.append(reinterpret_cast<const char*>(data + width * ps * i),
width * ps);
}
compressZlib(scanlines.str(), IDAT, compression);
compressZlib(scanlines, IDAT, compression);
writeChunk(file, IDAT.str());
}

file.write("\x00\x00\x00\x00IEND\xae\x42\x60\x82", 12);
file.append("\x00\x00\x00\x00IEND\xae\x42\x60\x82", 12);

return file.str();
return file;
}

0 comments on commit 93dfa8a

Please sign in to comment.