diff --git a/builtin/game/misc_s.lua b/builtin/game/misc_s.lua index 93d2bafa8d79..af4002bf2793 100644 --- a/builtin/game/misc_s.lua +++ b/builtin/game/misc_s.lua @@ -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 diff --git a/doc/lua_api.md b/doc/lua_api.md index a152252469bb..6b3fcaf23a7b 100644 --- a/doc/lua_api.md +++ b/doc/lua_api.md @@ -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 diff --git a/games/devtest/mods/testnodes/textures.lua b/games/devtest/mods/testnodes/textures.lua index 8e9fec5155dd..9ecf0cc12523 100644 --- a/games/devtest/mods/testnodes/textures.lua +++ b/games/devtest/mods/testnodes/textures.lua @@ -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) @@ -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", { @@ -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"), @@ -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. diff --git a/src/util/png.cpp b/src/util/png.cpp index 698cbc9a56d0..b4bc6b72f552 100755 --- a/src/util/png.cpp +++ b/src/util/png.cpp @@ -19,6 +19,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "png.h" #include +#include #include #include #include @@ -26,43 +27,108 @@ with this program; if not, write to the Free Software Foundation, Inc., #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(tmp), 4); + target.append(chunk_str); // Data + const u32 csum = crc32(0, reinterpret_cast(chunk_str.data()), + chunk_str.size()); + writeU32(tmp, csum); // CRC32 checksum + target.append(reinterpret_cast(tmp), 4); +} + +static std::optional 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(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(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(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(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; }