Browse Source

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 <sfan5@live.de>
hecks 2 years ago
parent
commit
80d12dbedb

+ 1 - 0
.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

+ 39 - 0
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

+ 18 - 1
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

+ 75 - 0
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 },
+})

+ 43 - 0
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 <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");

+ 6 - 0
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);

+ 1 - 0
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)

+ 68 - 0
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 <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();
+}

+ 27 - 0
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 <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);