From 80d12dbedb67191a5eb3e4f3c36b04baed1f8afb Mon Sep 17 00:00:00 2001 From: hecks <42101236+hecktest@users.noreply.github.com> Date: Thu, 29 Jul 2021 05:10:10 +0200 Subject: [PATCH] Add a simple PNG image encoder with Lua API (#11485) * Add a simple PNG image encoder with Lua API Add ColorSpec to RGBA converter Make a safety wrapper for the encoder Create devtest examples Co-authored-by: hecktest <> Co-authored-by: sfan5 --- .gitignore | 1 + builtin/game/misc.lua | 39 ++++++++++++ doc/lua_api.txt | 19 +++++- games/devtest/mods/testnodes/textures.lua | 75 +++++++++++++++++++++++ src/script/lua_api/l_util.cpp | 43 +++++++++++++ src/script/lua_api/l_util.h | 6 ++ src/util/CMakeLists.txt | 1 + src/util/png.cpp | 68 ++++++++++++++++++++ src/util/png.h | 27 ++++++++ 9 files changed, 278 insertions(+), 1 deletion(-) create mode 100755 src/util/png.cpp create mode 100755 src/util/png.h diff --git a/.gitignore b/.gitignore index df1386bce..a83a3718f 100644 --- a/.gitignore +++ b/.gitignore @@ -87,6 +87,7 @@ src/test_config.h src/cmake_config.h src/cmake_config_githash.h src/unittest/test_world/world.mt +games/devtest/mods/testnodes/textures/testnodes_generated_*.png /locale/ .directory *.cbp diff --git a/builtin/game/misc.lua b/builtin/game/misc.lua index c13a583f0..cee95dd23 100644 --- a/builtin/game/misc.lua +++ b/builtin/game/misc.lua @@ -290,3 +290,42 @@ function core.dynamic_add_media(filepath, callback) end return true end + + +-- PNG encoder safety wrapper + +local o_encode_png = core.encode_png +function core.encode_png(width, height, data, compression) + if type(width) ~= "number" then + error("Incorrect type for 'width', expected number, got " .. type(width)) + end + if type(height) ~= "number" then + error("Incorrect type for 'height', expected number, got " .. type(height)) + end + + local expected_byte_count = width * height * 4; + + if type(data) ~= "table" and type(data) ~= "string" then + error("Incorrect type for 'height', expected table or string, got " .. type(height)); + end + + local data_length = type(data) == "table" and #data * 4 or string.len(data); + + if data_length ~= expected_byte_count then + error(string.format( + "Incorrect length of 'data', width and height imply %d bytes but %d were provided", + expected_byte_count, + data_length + )) + end + + if type(data) == "table" then + local dataBuf = {} + for i = 1, #data do + dataBuf[i] = core.colorspec_to_bytes(data[i]) + end + data = table.concat(dataBuf) + end + + return o_encode_png(width, height, data, compression or 6) +end diff --git a/doc/lua_api.txt b/doc/lua_api.txt index 7ee9a3f2c..21e34b1ec 100644 --- a/doc/lua_api.txt +++ b/doc/lua_api.txt @@ -4611,6 +4611,23 @@ Utilities * `minetest.colorspec_to_colorstring(colorspec)`: Converts a ColorSpec to a ColorString. If the ColorSpec is invalid, returns `nil`. * `colorspec`: The ColorSpec to convert +* `minetest.colorspec_to_bytes(colorspec)`: Converts a ColorSpec to a raw + string of four bytes in an RGBA layout, returned as a string. + * `colorspec`: The ColorSpec to convert +* `minetest.encode_png(width, height, data, [compression])`: Encode a PNG + image and return it in string form. + * `width`: Width of the image + * `height`: Height of the image + * `data`: Image data, one of: + * array table of ColorSpec, length must be width*height + * string with raw RGBA pixels, length must be width*height*4 + * `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 may use this to procedurally generate textures during server init. Logging ------- @@ -7631,7 +7648,7 @@ Used by `minetest.register_node`. leveled_max = 127, -- Maximum value for `leveled` (0-127), enforced in -- `minetest.set_node_level` and `minetest.add_node_level`. - -- Values above 124 might causes collision detection issues. + -- Values above 124 might causes collision detection issues. liquid_range = 8, -- Maximum distance that flowing liquid nodes can spread around diff --git a/games/devtest/mods/testnodes/textures.lua b/games/devtest/mods/testnodes/textures.lua index f6e6a0c2a..4652007d9 100644 --- a/games/devtest/mods/testnodes/textures.lua +++ b/games/devtest/mods/testnodes/textures.lua @@ -65,3 +65,78 @@ for a=1,#alphas do }) end +-- Generate PNG textures + +local function mandelbrot(w, h, iterations) + local r = {} + for y=0, h-1 do + for x=0, w-1 do + local re = (x - w/2) * 4/w + local im = (y - h/2) * 4/h + -- zoom in on a nice view + re = re / 128 - 0.23 + im = im / 128 - 0.82 + + local px, py = 0, 0 + local i = 0 + while px*px + py*py <= 4 and i < iterations do + px, py = px*px - py*py + re, 2 * px * py + im + i = i + 1 + end + r[w*y+x+1] = i / iterations + end + end + return r +end + +local function gen_checkers(w, h, tile) + local r = {} + for y=0, h-1 do + for x=0, w-1 do + local hori = math.floor(x / tile) % 2 == 0 + local vert = math.floor(y / tile) % 2 == 0 + r[w*y+x+1] = hori ~= vert and 1 or 0 + end + end + return r +end + +local fractal = mandelbrot(512, 512, 128) +local checker = gen_checkers(512, 512, 32) + +local floor = math.floor +local abs = math.abs +local data_mb = {} +local data_ck = {} +for i=1, #fractal do + data_mb[i] = { + r = floor(fractal[i] * 255), + g = floor(abs(fractal[i] * 2 - 1) * 255), + b = floor(abs(1 - fractal[i]) * 255), + a = 255, + } + data_ck[i] = checker[i] > 0 and "#F80" or "#000" +end + +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) +) +minetest.safe_file_write( + textures_path .. "testnodes_generated_ck.png", + minetest.encode_png(512,512,data_ck) +) + +minetest.register_node("testnodes:generated_png_mb", { + description = S("Generated Mandelbrot PNG Test Node"), + tiles = { "testnodes_generated_mb.png" }, + + groups = { dig_immediate = 2 }, +}) +minetest.register_node("testnodes:generated_png_ck", { + description = S("Generated Checker PNG Test Node"), + tiles = { "testnodes_generated_ck.png" }, + + groups = { dig_immediate = 2 }, +}) diff --git a/src/script/lua_api/l_util.cpp b/src/script/lua_api/l_util.cpp index 8de2d67c8..87436fce0 100644 --- a/src/script/lua_api/l_util.cpp +++ b/src/script/lua_api/l_util.cpp @@ -40,6 +40,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "version.h" #include "util/hex.h" #include "util/sha1.h" +#include "util/png.h" #include #include @@ -497,6 +498,43 @@ int ModApiUtil::l_colorspec_to_colorstring(lua_State *L) return 0; } +// colorspec_to_bytes(colorspec) +int ModApiUtil::l_colorspec_to_bytes(lua_State *L) +{ + NO_MAP_LOCK_REQUIRED; + + video::SColor color(0); + if (read_color(L, 1, &color)) { + u8 colorbytes[4] = { + (u8) color.getRed(), + (u8) color.getGreen(), + (u8) color.getBlue(), + (u8) color.getAlpha(), + }; + lua_pushlstring(L, (const char*) colorbytes, 4); + return 1; + } + + return 0; +} + +// encode_png(w, h, data, level) +int ModApiUtil::l_encode_png(lua_State *L) +{ + NO_MAP_LOCK_REQUIRED; + + // The args are already pre-validated on the lua side. + u32 width = readParam(L, 1); + u32 height = readParam(L, 2); + const char *data = luaL_checklstring(L, 3, NULL); + s32 compression = readParam(L, 4); + + std::string out = encodePNG((const u8*)data, width, height, compression); + + lua_pushlstring(L, out.data(), out.size()); + return 1; +} + void ModApiUtil::Initialize(lua_State *L, int top) { API_FCT(log); @@ -532,6 +570,9 @@ void ModApiUtil::Initialize(lua_State *L, int top) API_FCT(get_version); API_FCT(sha1); API_FCT(colorspec_to_colorstring); + API_FCT(colorspec_to_bytes); + + API_FCT(encode_png); LuaSettings::create(L, g_settings, g_settings_path); lua_setfield(L, top, "settings"); @@ -557,6 +598,7 @@ void ModApiUtil::InitializeClient(lua_State *L, int top) API_FCT(get_version); API_FCT(sha1); API_FCT(colorspec_to_colorstring); + API_FCT(colorspec_to_bytes); } void ModApiUtil::InitializeAsync(lua_State *L, int top) @@ -585,6 +627,7 @@ void ModApiUtil::InitializeAsync(lua_State *L, int top) API_FCT(get_version); API_FCT(sha1); API_FCT(colorspec_to_colorstring); + API_FCT(colorspec_to_bytes); LuaSettings::create(L, g_settings, g_settings_path); lua_setfield(L, top, "settings"); diff --git a/src/script/lua_api/l_util.h b/src/script/lua_api/l_util.h index 6943a6afb..54d2be619 100644 --- a/src/script/lua_api/l_util.h +++ b/src/script/lua_api/l_util.h @@ -104,6 +104,12 @@ private: // colorspec_to_colorstring(colorspec) static int l_colorspec_to_colorstring(lua_State *L); + // colorspec_to_bytes(colorspec) + static int l_colorspec_to_bytes(lua_State *L); + + // encode_png(w, h, data, level) + static int l_encode_png(lua_State *L); + public: static void Initialize(lua_State *L, int top); static void InitializeAsync(lua_State *L, int top); diff --git a/src/util/CMakeLists.txt b/src/util/CMakeLists.txt index cd2e468d1..6bc97915f 100644 --- a/src/util/CMakeLists.txt +++ b/src/util/CMakeLists.txt @@ -15,4 +15,5 @@ set(UTIL_SRCS ${CMAKE_CURRENT_SOURCE_DIR}/string.cpp ${CMAKE_CURRENT_SOURCE_DIR}/srp.cpp ${CMAKE_CURRENT_SOURCE_DIR}/timetaker.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/png.cpp PARENT_SCOPE) diff --git a/src/util/png.cpp b/src/util/png.cpp new file mode 100755 index 000000000..7ac2e94a1 --- /dev/null +++ b/src/util/png.cpp @@ -0,0 +1,68 @@ +/* +Minetest +Copyright (C) 2021 hecks + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation; either version 2.1 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License along +with this program; if not, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +#include "png.h" +#include +#include +#include +#include +#include "util/serialize.h" +#include "serialization.h" +#include "irrlichttypes.h" + +static void writeChunk(std::ostringstream &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())); +} + +std::string encodePNG(const u8 *data, u32 width, u32 height, s32 compression) +{ + auto file = std::ostringstream(std::ios::binary); + file << "\x89PNG\r\n\x1a\n"; + + { + auto IHDR = std::ostringstream(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()); + } + + { + auto IDAT = std::ostringstream(std::ios::binary); + IDAT << "IDAT"; + auto scanlines = std::ostringstream(std::ios::binary); + for(u32 i = 0; i < height; i++) { + scanlines.write("\x00", 1); // Null predictor + scanlines.write((const char*) data + width * 4 * i, width * 4); + } + compressZlib(scanlines.str(), IDAT, compression); + writeChunk(file, IDAT.str()); + } + + file.write("\x00\x00\x00\x00IEND\xae\x42\x60\x82", 12); + + return file.str(); +} diff --git a/src/util/png.h b/src/util/png.h new file mode 100755 index 000000000..92387aef0 --- /dev/null +++ b/src/util/png.h @@ -0,0 +1,27 @@ +/* +Minetest +Copyright (C) 2021 hecks + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation; either version 2.1 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License along +with this program; if not, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +#pragma once + +#include +#include "irrlichttypes.h" + +/* Simple PNG encoder. Encodes an RGBA image with no predictors. + Returns a binary string. */ +std::string encodePNG(const u8 *data, u32 width, u32 height, s32 compression);