diff --git a/src/openrct2/drawing/ImageImporter.cpp b/src/openrct2/drawing/ImageImporter.cpp index e28a882c93..c5fc730faa 100644 --- a/src/openrct2/drawing/ImageImporter.cpp +++ b/src/openrct2/drawing/ImageImporter.cpp @@ -16,374 +16,372 @@ #include #include -using namespace OpenRCT2::Drawing; -using ImportResult = ImageImporter::ImportResult; - -constexpr int32_t PALETTE_TRANSPARENT = -1; - -ImportResult ImageImporter::Import(const Image& image, ImageImportMeta& meta) const +namespace OpenRCT2::Drawing { - if (meta.srcSize.width == 0) - meta.srcSize.width = image.Width; + constexpr int32_t PALETTE_TRANSPARENT = -1; - if (meta.srcSize.height == 0) - meta.srcSize.height = image.Height; - - if (meta.srcSize.width > 256 || meta.srcSize.height > 256) + ImageImporter::ImportResult ImageImporter::Import(const Image& image, ImageImportMeta& meta) const { - throw std::invalid_argument("Only images 256x256 or less are supported."); - } + if (meta.srcSize.width == 0) + meta.srcSize.width = image.Width; - if (meta.palette == Palette::KeepIndices && image.Depth != 8) - { - throw std::invalid_argument("Image is not paletted, it has bit depth of " + std::to_string(image.Depth)); - } - const bool isRLE = meta.importFlags & ImportFlags::RLE; + if (meta.srcSize.height == 0) + meta.srcSize.height = image.Height; - auto pixels = GetPixels(image, meta); - auto buffer = isRLE ? EncodeRLE(pixels.data(), meta.srcSize) : EncodeRaw(pixels.data(), meta.srcSize); - - G1Element outElement; - outElement.width = meta.srcSize.width; - outElement.height = meta.srcSize.height; - outElement.flags = isRLE ? G1_FLAG_RLE_COMPRESSION : G1_FLAG_HAS_TRANSPARENCY; - outElement.x_offset = meta.offset.x; - outElement.y_offset = meta.offset.y; - outElement.zoomed_offset = meta.zoomedOffset; - - ImportResult result; - result.Element = outElement; - result.Buffer = std::move(buffer); - result.Element.offset = result.Buffer.data(); - return result; -} - -std::vector ImageImporter::GetPixels(const Image& image, const ImageImportMeta& meta) -{ - const uint8_t* pixels = image.Pixels.data(); - std::vector buffer; - buffer.reserve(meta.srcSize.width * meta.srcSize.height); - - // A larger range is needed for proper dithering - auto palettedSrc = pixels; - std::unique_ptr rgbaSrcBuffer; - if (meta.palette != Palette::KeepIndices) - { - rgbaSrcBuffer = std::make_unique(meta.srcSize.height * meta.srcSize.width * 4); - } - - auto rgbaSrc = rgbaSrcBuffer.get(); - if (meta.palette != Palette::KeepIndices) - { - auto src = pixels + (meta.srcOffset.y * image.Stride) + (meta.srcOffset.x * 4); - auto dst = rgbaSrc; - for (auto y = 0; y < meta.srcSize.height; y++) + if (meta.srcSize.width > 256 || meta.srcSize.height > 256) { - for (auto x = 0; x < meta.srcSize.width * 4; x++) - { - *dst = static_cast(*src); - src++; - dst++; - } - src += (image.Stride - (meta.srcSize.width * 4)); + throw std::invalid_argument("Only images 256x256 or less are supported."); } + + if (meta.palette == Palette::KeepIndices && image.Depth != 8) + { + throw std::invalid_argument("Image is not paletted, it has bit depth of " + std::to_string(image.Depth)); + } + const bool isRLE = meta.importFlags & ImportFlags::RLE; + + auto pixels = GetPixels(image, meta); + auto buffer = isRLE ? EncodeRLE(pixels.data(), meta.srcSize) : EncodeRaw(pixels.data(), meta.srcSize); + + G1Element outElement; + outElement.width = meta.srcSize.width; + outElement.height = meta.srcSize.height; + outElement.flags = isRLE ? G1_FLAG_RLE_COMPRESSION : G1_FLAG_HAS_TRANSPARENCY; + outElement.x_offset = meta.offset.x; + outElement.y_offset = meta.offset.y; + outElement.zoomed_offset = meta.zoomedOffset; + + ImageImporter::ImportResult result; + result.Element = outElement; + result.Buffer = std::move(buffer); + result.Element.offset = result.Buffer.data(); + return result; } - if (meta.palette == Palette::KeepIndices) + std::vector ImageImporter::GetPixels(const Image& image, const ImageImportMeta& meta) { - palettedSrc += meta.srcOffset.x + meta.srcOffset.y * image.Stride; - for (auto y = 0; y < meta.srcSize.height; y++) + const uint8_t* pixels = image.Pixels.data(); + std::vector buffer; + buffer.reserve(meta.srcSize.width * meta.srcSize.height); + + // A larger range is needed for proper dithering + auto palettedSrc = pixels; + std::unique_ptr rgbaSrcBuffer; + if (meta.palette != Palette::KeepIndices) { - for (auto x = 0; x < meta.srcSize.width; x++) + rgbaSrcBuffer = std::make_unique(meta.srcSize.height * meta.srcSize.width * 4); + } + + auto rgbaSrc = rgbaSrcBuffer.get(); + if (meta.palette != Palette::KeepIndices) + { + auto src = pixels + (meta.srcOffset.y * image.Stride) + (meta.srcOffset.x * 4); + auto dst = rgbaSrc; + for (auto y = 0; y < meta.srcSize.height; y++) { - int32_t paletteIndex = *palettedSrc; - // The 1st index is always transparent - if (paletteIndex == 0) + for (auto x = 0; x < meta.srcSize.width * 4; x++) { - paletteIndex = PALETTE_TRANSPARENT; + *dst = static_cast(*src); + src++; + dst++; } - palettedSrc += 1; - buffer.push_back(paletteIndex); - } - palettedSrc += (image.Stride - meta.srcSize.width); - } - } - else - { - for (auto y = 0; y < meta.srcSize.height; y++) - { - for (auto x = 0; x < meta.srcSize.width; x++) - { - auto paletteIndex = CalculatePaletteIndex( - meta.importMode, rgbaSrc, x, y, meta.srcSize.width, meta.srcSize.height); - rgbaSrc += 4; - buffer.push_back(paletteIndex); + src += (image.Stride - (meta.srcSize.width * 4)); } } - } - return buffer; -} - -std::vector ImageImporter::EncodeRaw(const int32_t* pixels, ScreenSize size) -{ - auto bufferLength = size.width * size.height; - std::vector buffer(bufferLength); - for (auto i = 0; i < bufferLength; i++) - { - auto p = pixels[i]; - buffer[i] = (p == PALETTE_TRANSPARENT ? 0 : static_cast(p)); - } - return buffer; -} - -std::vector ImageImporter::EncodeRLE(const int32_t* pixels, ScreenSize size) -{ - struct RLECode - { - uint8_t NumPixels{}; - uint8_t OffsetX{}; - }; - - auto src = pixels; - std::vector buffer((size.height * 2) + (size.width * size.height * 16)); - - std::fill_n(buffer.data(), (size.height * 2) + (size.width * size.height * 16), 0x00); - auto yOffsets = reinterpret_cast(buffer.data()); - auto dst = buffer.data() + (size.height * 2); - for (auto y = 0; y < size.height; y++) - { - yOffsets[y] = static_cast(dst - buffer.data()); - - auto previousCode = static_cast(nullptr); - auto currentCode = reinterpret_cast(dst); - dst += 2; - - auto startX = 0; - auto npixels = 0; - bool pushRun = false; - for (auto x = 0; x < size.width; x++) + if (meta.palette == Palette::KeepIndices) { - int32_t paletteIndex = *src++; - if (paletteIndex == PALETTE_TRANSPARENT) + palettedSrc += meta.srcOffset.x + meta.srcOffset.y * image.Stride; + for (auto y = 0; y < meta.srcSize.height; y++) { - if (npixels != 0) + for (auto x = 0; x < meta.srcSize.width; x++) { - x--; - src--; - pushRun = true; - } - } - else - { - if (npixels == 0) - { - startX = x; - } - - npixels++; - *dst++ = static_cast(paletteIndex); - } - if (npixels == 127 || x == size.width - 1) - { - pushRun = true; - } - - if (pushRun) - { - if (npixels > 0) - { - previousCode = currentCode; - currentCode->NumPixels = npixels; - currentCode->OffsetX = startX; - - if (x == size.width - 1) + int32_t paletteIndex = *palettedSrc; + // The 1st index is always transparent + if (paletteIndex == 0) { - currentCode->NumPixels |= 0x80; + paletteIndex = PALETTE_TRANSPARENT; } + palettedSrc += 1; + buffer.push_back(paletteIndex); + } + palettedSrc += (image.Stride - meta.srcSize.width); + } + } + else + { + for (auto y = 0; y < meta.srcSize.height; y++) + { + for (auto x = 0; x < meta.srcSize.width; x++) + { + auto paletteIndex = CalculatePaletteIndex( + meta.importMode, rgbaSrc, x, y, meta.srcSize.width, meta.srcSize.height); + rgbaSrc += 4; + buffer.push_back(paletteIndex); + } + } + } - currentCode = reinterpret_cast(dst); - dst += 2; + return buffer; + } + + std::vector ImageImporter::EncodeRaw(const int32_t* pixels, ScreenSize size) + { + auto bufferLength = size.width * size.height; + std::vector buffer(bufferLength); + for (auto i = 0; i < bufferLength; i++) + { + auto p = pixels[i]; + buffer[i] = (p == PALETTE_TRANSPARENT ? 0 : static_cast(p)); + } + return buffer; + } + + std::vector ImageImporter::EncodeRLE(const int32_t* pixels, ScreenSize size) + { + struct RLECode + { + uint8_t NumPixels{}; + uint8_t OffsetX{}; + }; + + auto src = pixels; + std::vector buffer((size.height * 2) + (size.width * size.height * 16)); + + std::fill_n(buffer.data(), (size.height * 2) + (size.width * size.height * 16), 0x00); + auto yOffsets = reinterpret_cast(buffer.data()); + auto dst = buffer.data() + (size.height * 2); + for (auto y = 0; y < size.height; y++) + { + yOffsets[y] = static_cast(dst - buffer.data()); + + auto previousCode = static_cast(nullptr); + auto currentCode = reinterpret_cast(dst); + dst += 2; + + auto startX = 0; + auto npixels = 0; + bool pushRun = false; + for (auto x = 0; x < size.width; x++) + { + int32_t paletteIndex = *src++; + if (paletteIndex == PALETTE_TRANSPARENT) + { + if (npixels != 0) + { + x--; + src--; + pushRun = true; + } } else { - if (previousCode == nullptr) + if (npixels == 0) { - currentCode->NumPixels = 0x80; - currentCode->OffsetX = 0; + startX = x; + } + + npixels++; + *dst++ = static_cast(paletteIndex); + } + if (npixels == 127 || x == size.width - 1) + { + pushRun = true; + } + + if (pushRun) + { + if (npixels > 0) + { + previousCode = currentCode; + currentCode->NumPixels = npixels; + currentCode->OffsetX = startX; + + if (x == size.width - 1) + { + currentCode->NumPixels |= 0x80; + } + + currentCode = reinterpret_cast(dst); + dst += 2; } else { - previousCode->NumPixels |= 0x80; - dst -= 2; + if (previousCode == nullptr) + { + currentCode->NumPixels = 0x80; + currentCode->OffsetX = 0; + } + else + { + previousCode->NumPixels |= 0x80; + dst -= 2; + } } - } - startX = 0; - npixels = 0; - pushRun = false; + startX = 0; + npixels = 0; + pushRun = false; + } } } + + auto bufferLength = static_cast(dst - buffer.data()); + buffer.resize(bufferLength); + return buffer; } - auto bufferLength = static_cast(dst - buffer.data()); - buffer.resize(bufferLength); - return buffer; -} - -int32_t ImageImporter::CalculatePaletteIndex( - ImportMode 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 == ImportMode::Closest || mode == ImportMode::Dithering) && !IsInPalette(palette, rgbaSrc)) + int32_t ImageImporter::CalculatePaletteIndex( + ImportMode mode, int16_t* rgbaSrc, int32_t x, int32_t y, int32_t width, int32_t height) { - paletteIndex = GetClosestPaletteIndex(palette, rgbaSrc); - if (mode == ImportMode::Dithering) + auto& palette = StandardPalette; + auto paletteIndex = GetPaletteIndex(palette, rgbaSrc); + if ((mode == ImportMode::Closest || mode == ImportMode::Dithering) && !IsInPalette(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); - - // We don't want to dither remappable colours with nonremappable colours, etc - PaletteIndexType thisIndexType = GetPaletteIndexType(paletteIndex); - - if (x + 1 < width) + paletteIndex = GetClosestPaletteIndex(palette, rgbaSrc); + if (mode == ImportMode::Dithering) { - if (!IsInPalette(palette, rgbaSrc + 4) - && thisIndexType == GetPaletteIndexType(GetClosestPaletteIndex(palette, rgbaSrc + 4))) - { - // Right - rgbaSrc[4] += dr * 7 / 16; - rgbaSrc[5] += dg * 7 / 16; - rgbaSrc[6] += db * 7 / 16; - } - } + 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 (y + 1 < height) - { - if (x > 0) - { - if (!IsInPalette(palette, rgbaSrc + 4 * (width - 1)) - && thisIndexType == GetPaletteIndexType(GetClosestPaletteIndex(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 (!IsInPalette(palette, rgbaSrc + 4 * width) - && thisIndexType == GetPaletteIndexType(GetClosestPaletteIndex(palette, rgbaSrc + 4 * width))) - { - rgbaSrc[4 * width] += dr * 5 / 16; - rgbaSrc[4 * width + 1] += dg * 5 / 16; - rgbaSrc[4 * width + 2] += db * 5 / 16; - } + // We don't want to dither remappable colours with nonremappable colours, etc + PaletteIndexType thisIndexType = GetPaletteIndexType(paletteIndex); if (x + 1 < width) { - if (!IsInPalette(palette, rgbaSrc + 4 * (width + 1)) - && thisIndexType == GetPaletteIndexType(GetClosestPaletteIndex(palette, rgbaSrc + 4 * (width + 1)))) + if (!IsInPalette(palette, rgbaSrc + 4) + && thisIndexType == GetPaletteIndexType(GetClosestPaletteIndex(palette, rgbaSrc + 4))) { - // 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; + // Right + rgbaSrc[4] += dr * 7 / 16; + rgbaSrc[5] += dg * 7 / 16; + rgbaSrc[6] += db * 7 / 16; + } + } + + if (y + 1 < height) + { + if (x > 0) + { + if (!IsInPalette(palette, rgbaSrc + 4 * (width - 1)) + && thisIndexType == GetPaletteIndexType(GetClosestPaletteIndex(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 (!IsInPalette(palette, rgbaSrc + 4 * width) + && thisIndexType == GetPaletteIndexType(GetClosestPaletteIndex(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 (!IsInPalette(palette, rgbaSrc + 4 * (width + 1)) + && thisIndexType == GetPaletteIndexType(GetClosestPaletteIndex(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; } - return paletteIndex; -} - -int32_t ImageImporter::GetPaletteIndex(const GamePalette& palette, int16_t* colour) -{ - if (!IsTransparentPixel(colour)) + int32_t ImageImporter::GetPaletteIndex(const GamePalette& palette, int16_t* colour) { - for (uint32_t i = 0; i < PALETTE_SIZE; i++) + if (!IsTransparentPixel(colour)) { - if (static_cast(palette[i].Red) == colour[0] && static_cast(palette[i].Green) == colour[1] - && static_cast(palette[i].Blue) == colour[2]) + for (uint32_t i = 0; i < PALETTE_SIZE; i++) { - return 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; } - return PALETTE_TRANSPARENT; -} -bool ImageImporter::IsTransparentPixel(const int16_t* colour) -{ - return colour[3] < 128; -} - -/** - * @returns true if this colour is in the standard palette. - */ -bool ImageImporter::IsInPalette(const GamePalette& palette, int16_t* colour) -{ - return !(GetPaletteIndex(palette, colour) == PALETTE_TRANSPARENT && !IsTransparentPixel(colour)); -} - -/** - * @returns true if palette index is an index not used for a special purpose. - */ -bool ImageImporter::IsChangablePixel(int32_t paletteIndex) -{ - PaletteIndexType entryType = GetPaletteIndexType(paletteIndex); - return entryType != PaletteIndexType::Special && entryType != PaletteIndexType::PrimaryRemap; -} - -/** - * @returns the type of palette entry this is. - */ -ImageImporter::PaletteIndexType ImageImporter::GetPaletteIndexType(int32_t paletteIndex) -{ - if (paletteIndex <= 9) - return PaletteIndexType::Special; - if (paletteIndex >= 230 && paletteIndex <= 239) - return PaletteIndexType::Special; - if (paletteIndex == 255) - return PaletteIndexType::Special; - if (paletteIndex >= 243 && paletteIndex <= 254) - return PaletteIndexType::PrimaryRemap; - if (paletteIndex >= 202 && paletteIndex <= 213) - return PaletteIndexType::SecondaryRemap; - if (paletteIndex >= 46 && paletteIndex <= 57) - return PaletteIndexType::TertiaryRemap; - return PaletteIndexType::Normal; -} - -int32_t ImageImporter::GetClosestPaletteIndex(const GamePalette& palette, const int16_t* colour) -{ - auto smallestError = static_cast(-1); - auto bestMatch = PALETTE_TRANSPARENT; - for (uint32_t x = 0; x < PALETTE_SIZE; x++) + bool ImageImporter::IsTransparentPixel(const int16_t* colour) { - 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]); + return colour[3] < 128; + } - if (smallestError == static_cast(-1) || smallestError > error) + /** + * @returns true if this colour is in the standard palette. + */ + bool ImageImporter::IsInPalette(const GamePalette& palette, int16_t* colour) + { + return !(GetPaletteIndex(palette, colour) == PALETTE_TRANSPARENT && !IsTransparentPixel(colour)); + } + + /** + * @returns true if palette index is an index not used for a special purpose. + */ + bool ImageImporter::IsChangablePixel(int32_t paletteIndex) + { + PaletteIndexType entryType = GetPaletteIndexType(paletteIndex); + return entryType != PaletteIndexType::Special && entryType != PaletteIndexType::PrimaryRemap; + } + + /** + * @returns the type of palette entry this is. + */ + ImageImporter::PaletteIndexType ImageImporter::GetPaletteIndexType(int32_t paletteIndex) + { + if (paletteIndex <= 9) + return PaletteIndexType::Special; + if (paletteIndex >= 230 && paletteIndex <= 239) + return PaletteIndexType::Special; + if (paletteIndex == 255) + return PaletteIndexType::Special; + if (paletteIndex >= 243 && paletteIndex <= 254) + return PaletteIndexType::PrimaryRemap; + if (paletteIndex >= 202 && paletteIndex <= 213) + return PaletteIndexType::SecondaryRemap; + if (paletteIndex >= 46 && paletteIndex <= 57) + return PaletteIndexType::TertiaryRemap; + return PaletteIndexType::Normal; + } + + int32_t ImageImporter::GetClosestPaletteIndex(const GamePalette& palette, const int16_t* colour) + { + auto smallestError = static_cast(-1); + auto bestMatch = PALETTE_TRANSPARENT; + for (uint32_t x = 0; x < PALETTE_SIZE; x++) + { + if (IsChangablePixel(x)) { - bestMatch = x; - smallestError = error; + 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; } - return bestMatch; -} -namespace OpenRCT2::Drawing -{ ImageImportMeta createImageImportMetaFromJson(json_t& input) { auto xOffset = Json::GetNumber(input["x"]);