Browse Source

Optimize and improve built-in PNG writer (#14020)

sfan5 4 months ago
parent
commit
93dfa8a6d8
4 changed files with 120 additions and 26 deletions
  1. 7 0
      builtin/game/misc_s.lua
  2. 2 3
      doc/lua_api.md
  3. 26 4
      games/devtest/mods/testnodes/textures.lua
  4. 85 19
      src/util/png.cpp

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

+ 2 - 3
doc/lua_api.md

@@ -5418,9 +5418,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

+ 26 - 4
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.

+ 85 - 19
src/util/png.cpp

@@ -19,6 +19,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
 
 #include "png.h"
 #include <string>
+#include <optional>
 #include <sstream>
 #include <zlib.h>
 #include <cassert>
@@ -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<char*>(tmp), 4);
+	target.append(chunk_str); // Data
+	const u32 csum = crc32(0, reinterpret_cast<const u8*>(chunk_str.data()),
+			chunk_str.size());
+	writeU32(tmp, csum); // CRC32 checksum
+	target.append(reinterpret_cast<char*>(tmp), 4);
+}
+
+static std::optional<u8> 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<u8*>(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<u8*>(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<u8*>(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<const char*>(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;
 }