]> git.lizzy.rs Git - dragonfireclient.git/commitdiff
Add a simple PNG image encoder with Lua API (#11485)
authorhecks <42101236+hecktest@users.noreply.github.com>
Thu, 29 Jul 2021 03:10:10 +0000 (05:10 +0200)
committerGitHub <noreply@github.com>
Thu, 29 Jul 2021 03:10:10 +0000 (05:10 +0200)
* 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 <sfan5@live.de>
.gitignore
builtin/game/misc.lua
doc/lua_api.txt
games/devtest/mods/testnodes/textures.lua
src/script/lua_api/l_util.cpp
src/script/lua_api/l_util.h
src/util/CMakeLists.txt
src/util/png.cpp [new file with mode: 0755]
src/util/png.h [new file with mode: 0755]

index df1386bce4629c3041a9ad0d791f21fde3378e52..a83a3718f6d30909940dc0c3235c034a03df75c4 100644 (file)
@@ -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
index c13a583f0b7a9fcb40d45622a462e40361b8e789..cee95dd23c0390710079d7cb9cc37e67b74da001 100644 (file)
@@ -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
index 7ee9a3f2c1cddf2c8e1a91fd2380720763e75062..21e34b1ecc24f6046253e8971c25a8d977849b6f 100644 (file)
@@ -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
index f6e6a0c2a95b26266e1b82386f6220ddd89cb444..4652007d941a330122a4d48fd5913c27d54f4848 100644 (file)
@@ -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 },
+})
index 8de2d67c8042d1c8c6e7a31cb8e8b034c95f20f6..87436fce017957a215b761075907ba13aa473c29 100644 (file)
@@ -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 <algorithm>
 #include <cstdio>
 
@@ -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<int>(L, 1);
+       u32 height = readParam<int>(L, 2);
+       const char *data = luaL_checklstring(L, 3, NULL);
+       s32 compression = readParam<int>(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");
index 6943a6afbd9c83ca78450d9b5eaeefd802d15560..54d2be619d662de9341b12a157c6c84757d2fbfd 100644 (file)
@@ -104,6 +104,12 @@ class ModApiUtil : public ModApiBase
        // 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);
index cd2e468d197f73a7e2791d926a6ed24576bea605..6bc97915f3cb00e1c44e78a0a028f76d4052ede5 100644 (file)
@@ -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 (executable)
index 0000000..7ac2e94
--- /dev/null
@@ -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 <string>
+#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)
+{
+       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 (executable)
index 0000000..92387ae
--- /dev/null
@@ -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 <string>
+#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);