/***************************************************************************** * Copyright (c) 2014-2019 OpenRCT2 developers * * For a complete list of all authors, please refer to contributors.md * Interested in contributing? Visit https://github.com/OpenRCT2/OpenRCT2 * * OpenRCT2 is licensed under the GNU General Public License version 3. *****************************************************************************/ #include "ImageImporter.h" #include "../core/Imaging.h" #include #include #include using namespace OpenRCT2::Drawing; using ImportResult = ImageImporter::ImportResult; constexpr int32_t PALETTE_TRANSPARENT = -1; ImportResult ImageImporter::Import( const Image& image, int32_t offsetX, int32_t offsetY, IMPORT_FLAGS flags, IMPORT_MODE mode) const { if (image.Width > 256 || image.Height > 256) { throw std::invalid_argument("Only images 256x256 or less are supported."); } if ((flags & IMPORT_FLAGS::KEEP_PALETTE) && image.Depth != 8) { throw std::invalid_argument("Image is not palletted, it has bit depth of " + std::to_string(image.Depth)); } const auto width = image.Width; const auto height = image.Height; auto pixels = GetPixels(image.Pixels.data(), width, height, flags, mode); auto [buffer, bufferLength] = flags & IMPORT_FLAGS::RLE ? EncodeRLE(pixels.data(), width, height) : EncodeRaw(pixels.data(), width, height); rct_g1_element outElement; outElement.offset = static_cast(buffer); outElement.width = width; outElement.height = height; outElement.flags = (flags & IMPORT_FLAGS::RLE ? G1_FLAG_RLE_COMPRESSION : G1_FLAG_BMP); outElement.x_offset = offsetX; outElement.y_offset = offsetY; outElement.zoomed_offset = 0; ImportResult result; result.Element = outElement; result.Buffer = buffer; result.BufferLength = bufferLength; return result; } std::vector ImageImporter::GetPixels( const uint8_t* pixels, uint32_t width, uint32_t height, IMPORT_FLAGS flags, IMPORT_MODE mode) { std::vector buffer; buffer.reserve(width * height); // A larger range is needed for proper dithering auto palettedSrc = pixels; std::unique_ptr rgbaSrcBuffer; if (!(flags & IMPORT_FLAGS::KEEP_PALETTE)) { rgbaSrcBuffer = std::make_unique(height * width * 4); } auto rgbaSrc = rgbaSrcBuffer.get(); if (!(flags & IMPORT_FLAGS::KEEP_PALETTE)) { for (uint32_t x = 0; x < height * width * 4; x++) { rgbaSrc[x] = static_cast(pixels[x]); } } for (uint32_t y = 0; y < height; y++) { for (uint32_t x = 0; x < width; x++) { int32_t paletteIndex; if (flags & IMPORT_FLAGS::KEEP_PALETTE) { paletteIndex = *palettedSrc; // The 1st index is always transparent if (paletteIndex == 0) { paletteIndex = PALETTE_TRANSPARENT; } } else { paletteIndex = CalculatePaletteIndex(mode, rgbaSrc, x, y, width, height); } rgbaSrc += 4; palettedSrc += 1; buffer.push_back(paletteIndex); } } return buffer; } std::tuple ImageImporter::EncodeRaw(const int32_t* pixels, uint32_t width, uint32_t height) { auto bufferLength = width * height; auto buffer = static_cast(std::malloc(bufferLength)); for (size_t i = 0; i < bufferLength; i++) { auto p = pixels[i]; buffer[i] = (p == PALETTE_TRANSPARENT ? 0 : static_cast(p)); } return std::make_tuple(buffer, bufferLength); } std::tuple ImageImporter::EncodeRLE(const int32_t* pixels, uint32_t width, uint32_t height) { struct RLECode { uint8_t NumPixels{}; uint8_t OffsetX{}; }; auto src = pixels; auto buffer = static_cast(std::malloc((height * 2) + (width * height * 16))); if (buffer == nullptr) { throw std::bad_alloc(); } std::fill_n(buffer, (height * 2) + (width * height * 16), 0x00); auto yOffsets = reinterpret_cast(buffer); auto dst = buffer + (height * 2); for (uint32_t y = 0; y < height; y++) { yOffsets[y] = static_cast(dst - buffer); auto previousCode = (RLECode*)nullptr; auto currentCode = reinterpret_cast(dst); dst += 2; auto startX = 0; auto npixels = 0; bool pushRun = false; for (uint32_t x = 0; x < width; x++) { int32_t paletteIndex = *src++; if (paletteIndex == PALETTE_TRANSPARENT) { if (npixels != 0) { x--; src--; pushRun = true; } } else { if (npixels == 0) { startX = x; } npixels++; *dst++ = static_cast(paletteIndex); } if (npixels == 127 || x == width - 1) { pushRun = true; } if (pushRun) { if (npixels > 0) { previousCode = currentCode; currentCode->NumPixels = npixels; currentCode->OffsetX = startX; if (x == width - 1) { currentCode->NumPixels |= 0x80; } currentCode = reinterpret_cast(dst); dst += 2; } else { if (previousCode == nullptr) { currentCode->NumPixels = 0x80; currentCode->OffsetX = 0; } else { previousCode->NumPixels |= 0x80; dst -= 2; } } startX = 0; npixels = 0; pushRun = false; } } } auto bufferLength = static_cast(dst - buffer); buffer = static_cast(realloc(buffer, bufferLength)); if (buffer == nullptr) { throw std::bad_alloc(); } return std::make_tuple(buffer, bufferLength); } int32_t ImageImporter::CalculatePaletteIndex( IMPORT_MODE mode, int16_t* rgbaSrc, int32_t x, int32_t y, int32_t width, int32_t height) { auto palette = StandardPalette; auto paletteIndex = GetPaletteIndex(palette, rgbaSrc); if (mode == IMPORT_MODE::CLOSEST || mode == IMPORT_MODE::DITHERING) { if (paletteIndex == PALETTE_TRANSPARENT && !IsTransparentPixel(rgbaSrc)) { paletteIndex = GetClosestPaletteIndex(palette, rgbaSrc); } } if (mode == IMPORT_MODE::DITHERING) { if (!IsTransparentPixel(rgbaSrc) && IsChangablePixel(GetPaletteIndex(palette, rgbaSrc))) { auto dr = rgbaSrc[0] - static_cast(palette[paletteIndex].Red); auto dg = rgbaSrc[1] - static_cast(palette[paletteIndex].Green); auto db = rgbaSrc[2] - static_cast(palette[paletteIndex].Blue); if (x + 1 < width) { if (!IsTransparentPixel(rgbaSrc + 4) && IsChangablePixel(GetPaletteIndex(palette, rgbaSrc + 4))) { // Right rgbaSrc[4] += dr * 7 / 16; rgbaSrc[5] += dg * 7 / 16; rgbaSrc[6] += db * 7 / 16; } } if (y + 1 < height) { if (x > 0) { if (!IsTransparentPixel(rgbaSrc + 4 * (width - 1)) && IsChangablePixel(GetPaletteIndex(palette, rgbaSrc + 4 * (width - 1)))) { // Bottom left rgbaSrc[4 * (width - 1)] += dr * 3 / 16; rgbaSrc[4 * (width - 1) + 1] += dg * 3 / 16; rgbaSrc[4 * (width - 1) + 2] += db * 3 / 16; } } // Bottom if (!IsTransparentPixel(rgbaSrc + 4 * width) && IsChangablePixel(GetPaletteIndex(palette, rgbaSrc + 4 * width))) { rgbaSrc[4 * width] += dr * 5 / 16; rgbaSrc[4 * width + 1] += dg * 5 / 16; rgbaSrc[4 * width + 2] += db * 5 / 16; } if (x + 1 < width) { if (!IsTransparentPixel(rgbaSrc + 4 * (width + 1)) && IsChangablePixel(GetPaletteIndex(palette, rgbaSrc + 4 * (width + 1)))) { // Bottom right rgbaSrc[4 * (width + 1)] += dr * 1 / 16; rgbaSrc[4 * (width + 1) + 1] += dg * 1 / 16; rgbaSrc[4 * (width + 1) + 2] += db * 1 / 16; } } } } } return paletteIndex; } int32_t ImageImporter::GetPaletteIndex(const GamePalette& palette, int16_t* colour) { if (!IsTransparentPixel(colour)) { for (int32_t i = 0; i < PALETTE_SIZE; i++) { if (static_cast(palette[i].Red) == colour[0] && static_cast(palette[i].Green) == colour[1] && static_cast(palette[i].Blue) == colour[2]) { return i; } } } return PALETTE_TRANSPARENT; } bool ImageImporter::IsTransparentPixel(const int16_t* colour) { return colour[3] < 128; } /** * @returns true if pixel index is an index not used for remapping. */ bool ImageImporter::IsChangablePixel(int32_t paletteIndex) { if (paletteIndex == PALETTE_TRANSPARENT) return true; if (paletteIndex == 0) return false; if (paletteIndex >= 203 && paletteIndex < 214) return false; if (paletteIndex == 226) return false; if (paletteIndex >= 227 && paletteIndex < 229) return false; if (paletteIndex >= 243) return false; return true; } int32_t ImageImporter::GetClosestPaletteIndex(const GamePalette& palette, const int16_t* colour) { auto smallestError = static_cast(-1); auto bestMatch = PALETTE_TRANSPARENT; for (int32_t x = 0; x < PALETTE_SIZE; x++) { if (IsChangablePixel(x)) { uint32_t error = (static_cast(palette[x].Red) - colour[0]) * (static_cast(palette[x].Red) - colour[0]) + (static_cast(palette[x].Green) - colour[1]) * (static_cast(palette[x].Green) - colour[1]) + (static_cast(palette[x].Blue) - colour[2]) * (static_cast(palette[x].Blue) - colour[2]); if (smallestError == static_cast(-1) || smallestError > error) { bestMatch = x; smallestError = error; } } } return bestMatch; }