diff --git a/distribution/openrct2.d.ts b/distribution/openrct2.d.ts index 49e0699a97..312beba13b 100644 --- a/distribution/openrct2.d.ts +++ b/distribution/openrct2.d.ts @@ -2894,10 +2894,14 @@ declare global { type: 'png'; /** + * How the colours of the .png file are converted to the OpenRCT2 palette. * If keep is specified for palette, the raw 8bpp .png bytes will be loaded. The palette * in the .png will not be read. This will improve load performance. + * Closest will find the closest matching colour from the OpenRCT2 palette. + * Dither will add noise to reduce colour banding for images rich in colour. + * If undefined, only colours that are in OpenRCT2 palette will be imported. */ - palette?: 'keep'; + palette?: 'keep' | 'closest' | 'dither'; /** * Data can either by a: diff --git a/src/openrct2-ui/scripting/CustomImages.cpp b/src/openrct2-ui/scripting/CustomImages.cpp index a283c7e14a..902f828128 100644 --- a/src/openrct2-ui/scripting/CustomImages.cpp +++ b/src/openrct2-ui/scripting/CustomImages.cpp @@ -11,6 +11,9 @@ # include "CustomImages.h" +# include +using namespace OpenRCT2::Drawing; + namespace OpenRCT2::Scripting { enum class PixelDataKind @@ -22,12 +25,21 @@ namespace OpenRCT2::Scripting Png }; + enum class PixelDataPaletteKind + { + None, + Keep, + Closest, + Dither + }; + struct PixelData { PixelDataKind Type; int32_t Width; int32_t Height; int32_t Stride; + PixelDataPaletteKind Palette; DukValue Data; }; @@ -100,35 +112,17 @@ namespace OpenRCT2::Scripting return obj.Take(); } - static void SetPixelDataFromBuffer( - uint8_t* dst, const uint8_t* src, size_t srcLen, int32_t width, int32_t height, int32_t stride) + static std::vector GetBufferFromDukStack(duk_context* ctx) { - auto srcEnd = src + srcLen; - auto dstLen = static_cast(width) * height; - if (stride == width) + std::vector result; + duk_size_t bufferLen{}; + const auto* buffer = reinterpret_cast(duk_get_buffer_data(ctx, -1, &bufferLen)); + if (buffer != nullptr) { - std::memcpy(dst, src, std::min(srcLen, dstLen)); - if (dstLen > srcLen) - { - std::memset(dst + srcLen, 0, dstLen - srcLen); - } - } - else - { - std::memset(dst, 0, dstLen); - auto srcLine = src; - for (int32_t y = 0; y < height; y++) - { - auto dstI = y * width; - auto lineWidth = std::min(srcEnd - srcLine, width); - std::memcpy(dst + dstI, srcLine, lineWidth); - if (lineWidth < width) - { - break; - } - srcLine += stride; - } + result.resize(bufferLen); + std::memcpy(result.data(), buffer, bufferLen); } + return result; } static std::vector DukGetDataFromBufferLikeObject(const DukValue& data) @@ -156,111 +150,84 @@ namespace OpenRCT2::Scripting // From base64 string data.push(); duk_base64_decode(ctx, -1); - duk_size_t bufferLen{}; - const auto* buffer = reinterpret_cast(duk_get_buffer_data(ctx, -1, &bufferLen)); - if (buffer != nullptr) - { - result = std::vector(buffer, buffer + bufferLen); - } + result = GetBufferFromDukStack(ctx); duk_pop(ctx); } else if (data.type() == DukValue::Type::OBJECT) { // From Uint8Array data.push(); - duk_size_t bufferLen{}; - const auto* buffer = reinterpret_cast(duk_get_buffer_data(ctx, -1, &bufferLen)); - if (buffer != nullptr) - { - result = std::vector(buffer, buffer + bufferLen); - } + result = GetBufferFromDukStack(ctx); duk_pop(ctx); } return result; } - static uint8_t* GetBufferFromPixelData(duk_context* ctx, const PixelData& pixelData) + static std::vector RemovePadding(const std::vector& srcData, const PixelData& pixelData) { - auto padding = pixelData.Stride - pixelData.Width; - auto imageData = new (std::nothrow) uint8_t[pixelData.Width * pixelData.Height]; - if (imageData == nullptr) + std::vector unpadded(pixelData.Width * pixelData.Height, 0); + auto* src = srcData.data(); + auto* dst = unpadded.data(); + for (int32_t y = 0; y < pixelData.Height; y++) { - throw std::runtime_error("Unable to allocate memory for pixel data."); + std::memcpy(dst, src, pixelData.Width); + src += pixelData.Stride; + dst += pixelData.Width; } + return unpadded; + } - // Ensure image data is auto freed if exception occurs - std::unique_ptr uniqueImageData(imageData); + static std::vector GetBufferFromPixelData(duk_context* ctx, PixelData& pixelData) + { + std::vector imageData; switch (pixelData.Type) { case PixelDataKind::Raw: { - // Set the pixel data - auto& data = pixelData.Data; - if (data.is_array()) + auto data = DukGetDataFromBufferLikeObject(pixelData.Data); + if (pixelData.Stride != pixelData.Width) { - // From array of numbers - data.push(); - duk_uarridx_t i = 0; - for (int32_t y = 0; y < pixelData.Height; y++) - { - for (int32_t x = 0; x < pixelData.Width; x++) - { - auto dstI = y * pixelData.Width + x; - if (duk_get_prop_index(ctx, -1, i)) - { - imageData[dstI] = duk_get_int(ctx, -1) & 0xFF; - duk_pop(ctx); - } - else - { - imageData[dstI] = 0; - } - i++; - } - i += padding; - } - duk_pop(ctx); - } - else if (data.type() == DukValue::Type::STRING) - { - // From base64 string - data.push(); - duk_base64_decode(ctx, -1); - duk_size_t bufferLen{}; - const auto* buffer = reinterpret_cast(duk_get_buffer_data(ctx, -1, &bufferLen)); - if (buffer != nullptr) - { - SetPixelDataFromBuffer( - imageData, buffer, bufferLen, pixelData.Width, pixelData.Height, pixelData.Stride); - } - duk_pop(ctx); - } - else if (data.type() == DukValue::Type::OBJECT) - { - // From Uint8Array - data.push(); - duk_size_t bufferLen{}; - const auto* buffer = reinterpret_cast(duk_get_buffer_data(ctx, -1, &bufferLen)); - if (buffer != nullptr) - { - SetPixelDataFromBuffer( - imageData, buffer, bufferLen, pixelData.Width, pixelData.Height, pixelData.Stride); - } - duk_pop(ctx); + // Make sure data is expected size for RemovePadding + data.resize(pixelData.Stride * pixelData.Height); + data = RemovePadding(data, pixelData); } + + // Make sure data is expected size + data.resize(pixelData.Width * pixelData.Height); break; } case PixelDataKind::Rle: { - auto data = DukGetDataFromBufferLikeObject(pixelData.Data); - std::memcpy(imageData, data.data(), data.size()); + imageData = DukGetDataFromBufferLikeObject(pixelData.Data); break; } case PixelDataKind::Png: + { + auto imageFormat = pixelData.Palette == PixelDataPaletteKind::Keep ? IMAGE_FORMAT::PNG : IMAGE_FORMAT::PNG_32; + auto palette = pixelData.Palette == PixelDataPaletteKind::Keep ? ImageImporter::Palette::KeepIndices + : ImageImporter::Palette::OpenRCT2; + auto importMode = ImageImporter::ImportMode::Default; + if (pixelData.Palette == PixelDataPaletteKind::Closest) + importMode = ImageImporter::ImportMode::Closest; + else if (pixelData.Palette == PixelDataPaletteKind::Dither) + importMode = ImageImporter::ImportMode::Dithering; + auto pngData = DukGetDataFromBufferLikeObject(pixelData.Data); + auto image = Imaging::ReadFromBuffer(pngData, imageFormat); + + ImageImporter importer; + auto importResult = importer.Import(image, 0, 0, palette, ImageImporter::ImportFlags::RLE, importMode); + + pixelData.Type = PixelDataKind::Rle; + pixelData.Width = importResult.Element.width; + pixelData.Height = importResult.Element.height; + + imageData = std::move(importResult.Buffer); + break; + } default: throw std::runtime_error("Unsupported pixel data type."); } - return uniqueImageData.release(); + return imageData; } template<> PixelDataKind FromDuk(const DukValue& d) @@ -280,18 +247,34 @@ namespace OpenRCT2::Scripting return PixelDataKind::Unknown; } + template<> PixelDataPaletteKind FromDuk(const DukValue& d) + { + if (d.type() == DukValue::Type::STRING) + { + auto& s = d.as_string(); + if (s == "keep") + return PixelDataPaletteKind::Keep; + if (s == "closest") + return PixelDataPaletteKind::Closest; + if (s == "dither") + return PixelDataPaletteKind::Dither; + } + return PixelDataPaletteKind::None; + } + static PixelData GetPixelDataFromDuk(const DukValue& dukPixelData) { PixelData pixelData; pixelData.Type = FromDuk(dukPixelData["type"]); - pixelData.Width = dukPixelData["width"].as_int(); - pixelData.Height = dukPixelData["height"].as_int(); + pixelData.Palette = FromDuk(dukPixelData["palette"]); + pixelData.Width = AsOrDefault(dukPixelData["width"], 0); + pixelData.Height = AsOrDefault(dukPixelData["height"], 0); pixelData.Stride = AsOrDefault(dukPixelData["stride"], pixelData.Width); pixelData.Data = dukPixelData["data"]; return pixelData; } - static void ReplacePixelDataForImage(ImageIndex id, const PixelData& pixelData, void* data) + static void ReplacePixelDataForImage(ImageIndex id, const PixelData& pixelData, std::vector&& data) { // Setup the g1 element rct_g1_element el{}; @@ -301,7 +284,12 @@ namespace OpenRCT2::Scripting el = *lastel; delete[] el.offset; } - el.offset = reinterpret_cast(data); + + // Copy data into new unmanaged uint8_t[] + auto newData = new uint8_t[data.size()]; + std::memcpy(newData, data.data(), data.size()); + + el.offset = newData; el.width = pixelData.Width; el.height = pixelData.Height; el.flags = 0; @@ -318,7 +306,7 @@ namespace OpenRCT2::Scripting try { auto newData = GetBufferFromPixelData(ctx, pixelData); - ReplacePixelDataForImage(id, pixelData, newData); + ReplacePixelDataForImage(id, pixelData, std::move(newData)); } catch (const std::runtime_error& e) {