Optimize and improve built-in PNG writer (#14020)

This commit is contained in:
sfan5 2023-12-27 11:56:48 +01:00 committed by GitHub
parent 5054918efc
commit 93dfa8a6d8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 120 additions and 26 deletions

@ -64,6 +64,13 @@ function core.encode_png(width, height, data, compression)
error("Incorrect type for 'height', expected number, got " .. type(height)) error("Incorrect type for 'height', expected number, got " .. type(height))
end 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 local expected_byte_count = width * height * 4
if type(data) ~= "table" and type(data) ~= "string" then if type(data) ~= "table" and type(data) ~= "string" then

@ -5418,9 +5418,8 @@ Utilities
* `compression`: Optional zlib compression level, number in range 0 to 9. * `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 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. 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, You can use `colorspec_to_bytes` to generate raw RGBA values.
use `colorspec_to_bytes` to generate raw RGBA values in a predictable way. Palettes are not supported at the moment.
The resulting PNG image is always 32-bit. Palettes are not supported at the moment.
You may use this to procedurally generate textures during server init. You may use this to procedurally generate textures during server init.
* `minetest.urlencode(str)`: Encodes non-unreserved URI characters by a * `minetest.urlencode(str)`: Encodes non-unreserved URI characters by a
percent sign followed by two hex digits. See percent sign followed by two hex digits. See

@ -105,6 +105,19 @@ local function gen_checkers(w, h, tile)
return r return r
end 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 fractal = mandelbrot(512, 512, 128)
local frac_emb = mandelbrot(64, 64, 64) local frac_emb = mandelbrot(64, 64, 64)
local checker = gen_checkers(512, 512, 32) local checker = gen_checkers(512, 512, 32)
@ -129,17 +142,21 @@ for i=1, #fractal do
b = floor(abs(1 - fractal[i]) * 255), b = floor(abs(1 - fractal[i]) * 255),
a = 255, a = 255,
} }
data_ck[i] = checker[i] > 0 and "#F80" or "#000" data_ck[i] = checker[i] > 0 and "#888" or "#000"
end end
fractal = nil
frac_emb = nil
checker = nil
local textures_path = minetest.get_modpath( minetest.get_current_modname() ) .. "/textures/" local textures_path = minetest.get_modpath( minetest.get_current_modname() ) .. "/textures/"
minetest.safe_file_write( minetest.safe_file_write(
textures_path .. "testnodes_generated_mb.png", 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( minetest.safe_file_write(
textures_path .. "testnodes_generated_ck.png", 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", { minetest.register_node("testnodes:generated_png_mb", {
@ -155,7 +172,8 @@ minetest.register_node("testnodes:generated_png_ck", {
groups = { dig_immediate = 2 }, 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", { minetest.register_node("testnodes:generated_png_emb", {
description = S("Generated In-Band Mandelbrot PNG Test Node"), description = S("Generated In-Band Mandelbrot PNG Test Node"),
@ -182,6 +200,10 @@ minetest.register_node("testnodes:generated_png_dst_emb", {
groups = { dig_immediate = 2 }, 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. The following nodes can be used to demonstrate the TGA format support.

@ -19,6 +19,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include "png.h" #include "png.h"
#include <string> #include <string>
#include <optional>
#include <sstream> #include <sstream>
#include <zlib.h> #include <zlib.h>
#include <cassert> #include <cassert>
@ -26,43 +27,108 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include "serialization.h" #include "serialization.h"
#include "irrlichttypes.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);
assert(chunk_str.size() - 4 < U32_MAX); assert(chunk_str.size() - 4 < U32_MAX);
writeU32(target, chunk_str.size() - 4); // Write length minus the identifier u8 tmp[4];
target << chunk_str; target.reserve(target.size() + 4 + chunk_str.size() + 4);
writeU32(target, crc32(0,(const u8*)chunk_str.data(), chunk_str.size()));
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::string encodePNG(const u8 *data, u32 width, u32 height, s32 compression)
{ {
std::ostringstream file(std::ios::binary); u8 color_type = COLOR_RGBA;
file << "\x89PNG\r\n\x1a\n"; 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); std::ostringstream header(std::ios::binary);
IHDR << "IHDR"; header << "IHDR";
writeU32(IHDR, width); writeU32(header, width);
writeU32(IHDR, height); writeU32(header, height);
// 8 bpp, color type 6 (RGBA) writeU8(header, 8); // bpp
IHDR.write("\x08\x06\x00\x00\x00", 5); writeU8(header, color_type);
writeChunk(file, IHDR.str()); header.write("\x00\x00\x00", 3);
writeChunk(file, header.str());
} }
{ {
std::ostringstream IDAT(std::ios::binary); std::ostringstream IDAT(std::ios::binary);
IDAT << "IDAT"; 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++) { for(u32 i = 0; i < height; i++) {
scanlines.write("\x00", 1); // Null predictor scanlines.append(1, 0); // Null predictor
scanlines.write((const char*) data + width * 4 * i, width * 4); 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()); 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;
} }