Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Optimize and improve built-in PNG writer #14020

Merged
merged 6 commits into from Dec 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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 @@ -5412,9 +5412,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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You could put a do + end block around the relevant code to automatically let the variables run out of scope. Diff viewers can ignore whitespace changes.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's a bunch of lines so meh


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;
}