mirror of
https://github.com/minetest/minetest.git
synced 2024-11-23 08:03:45 +01:00
Optimize and improve built-in PNG writer (#14020)
This commit is contained in:
parent
5054918efc
commit
93dfa8a6d8
@ -64,6 +64,13 @@ function core.encode_png(width, height, data, compression)
|
|||||||
error("Incorrect type for 'height', expected number, got " .. type(height))
|
error("Incorrect type for 'height', expected number, got " .. type(height))
|
||||||
end
|
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
|
local expected_byte_count = width * height * 4
|
||||||
|
|
||||||
if type(data) ~= "table" and type(data) ~= "string" then
|
if type(data) ~= "table" and type(data) ~= "string" then
|
||||||
|
@ -5418,9 +5418,8 @@ Utilities
|
|||||||
* `compression`: Optional zlib compression level, number in range 0 to 9.
|
* `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
|
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.
|
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,
|
You can use `colorspec_to_bytes` to generate raw RGBA values.
|
||||||
use `colorspec_to_bytes` to generate raw RGBA values in a predictable way.
|
Palettes are not supported at the moment.
|
||||||
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.
|
You may use this to procedurally generate textures during server init.
|
||||||
* `minetest.urlencode(str)`: Encodes non-unreserved URI characters by a
|
* `minetest.urlencode(str)`: Encodes non-unreserved URI characters by a
|
||||||
percent sign followed by two hex digits. See
|
percent sign followed by two hex digits. See
|
||||||
|
@ -105,6 +105,19 @@ local function gen_checkers(w, h, tile)
|
|||||||
return r
|
return r
|
||||||
end
|
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 fractal = mandelbrot(512, 512, 128)
|
||||||
local frac_emb = mandelbrot(64, 64, 64)
|
local frac_emb = mandelbrot(64, 64, 64)
|
||||||
local checker = gen_checkers(512, 512, 32)
|
local checker = gen_checkers(512, 512, 32)
|
||||||
@ -129,17 +142,21 @@ for i=1, #fractal do
|
|||||||
b = floor(abs(1 - fractal[i]) * 255),
|
b = floor(abs(1 - fractal[i]) * 255),
|
||||||
a = 255,
|
a = 255,
|
||||||
}
|
}
|
||||||
data_ck[i] = checker[i] > 0 and "#F80" or "#000"
|
data_ck[i] = checker[i] > 0 and "#888" or "#000"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
fractal = nil
|
||||||
|
frac_emb = nil
|
||||||
|
checker = nil
|
||||||
|
|
||||||
local textures_path = minetest.get_modpath( minetest.get_current_modname() ) .. "/textures/"
|
local textures_path = minetest.get_modpath( minetest.get_current_modname() ) .. "/textures/"
|
||||||
minetest.safe_file_write(
|
minetest.safe_file_write(
|
||||||
textures_path .. "testnodes_generated_mb.png",
|
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(
|
minetest.safe_file_write(
|
||||||
textures_path .. "testnodes_generated_ck.png",
|
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", {
|
minetest.register_node("testnodes:generated_png_mb", {
|
||||||
@ -155,7 +172,8 @@ minetest.register_node("testnodes:generated_png_ck", {
|
|||||||
groups = { dig_immediate = 2 },
|
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", {
|
minetest.register_node("testnodes:generated_png_emb", {
|
||||||
description = S("Generated In-Band Mandelbrot PNG Test Node"),
|
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 },
|
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.
|
The following nodes can be used to demonstrate the TGA format support.
|
||||||
|
104
src/util/png.cpp
104
src/util/png.cpp
@ -19,6 +19,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||||||
|
|
||||||
#include "png.h"
|
#include "png.h"
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <optional>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
#include <zlib.h>
|
#include <zlib.h>
|
||||||
#include <cassert>
|
#include <cassert>
|
||||||
@ -26,43 +27,108 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||||||
#include "serialization.h"
|
#include "serialization.h"
|
||||||
#include "irrlichttypes.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);
|
||||||
assert(chunk_str.size() - 4 < U32_MAX);
|
assert(chunk_str.size() - 4 < U32_MAX);
|
||||||
writeU32(target, chunk_str.size() - 4); // Write length minus the identifier
|
u8 tmp[4];
|
||||||
target << chunk_str;
|
target.reserve(target.size() + 4 + chunk_str.size() + 4);
|
||||||
writeU32(target, crc32(0,(const u8*)chunk_str.data(), chunk_str.size()));
|
|
||||||
|
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::string encodePNG(const u8 *data, u32 width, u32 height, s32 compression)
|
||||||
{
|
{
|
||||||
std::ostringstream file(std::ios::binary);
|
u8 color_type = COLOR_RGBA;
|
||||||
file << "\x89PNG\r\n\x1a\n";
|
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);
|
std::ostringstream header(std::ios::binary);
|
||||||
IHDR << "IHDR";
|
header << "IHDR";
|
||||||
writeU32(IHDR, width);
|
writeU32(header, width);
|
||||||
writeU32(IHDR, height);
|
writeU32(header, height);
|
||||||
// 8 bpp, color type 6 (RGBA)
|
writeU8(header, 8); // bpp
|
||||||
IHDR.write("\x08\x06\x00\x00\x00", 5);
|
writeU8(header, color_type);
|
||||||
writeChunk(file, IHDR.str());
|
header.write("\x00\x00\x00", 3);
|
||||||
|
writeChunk(file, header.str());
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
std::ostringstream IDAT(std::ios::binary);
|
std::ostringstream IDAT(std::ios::binary);
|
||||||
IDAT << "IDAT";
|
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++) {
|
for(u32 i = 0; i < height; i++) {
|
||||||
scanlines.write("\x00", 1); // Null predictor
|
scanlines.append(1, 0); // Null predictor
|
||||||
scanlines.write((const char*) data + width * 4 * i, width * 4);
|
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());
|
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;
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user