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>
nephele-craftmethods
hecks 3 months ago
committed by GitHub
parent
commit
80d12dbedb
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      .gitignore
  2. 39
      builtin/game/misc.lua
  3. 19
      doc/lua_api.txt
  4. 75
      games/devtest/mods/testnodes/textures.lua
  5. 43
      src/script/lua_api/l_util.cpp
  6. 6
      src/script/lua_api/l_util.h
  7. 1
      src/util/CMakeLists.txt
  8. 68
      src/util/png.cpp
  9. 27
      src/util/png.h

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

19
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
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
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
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
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
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
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);
Loading…
Cancel
Save