浏览代码

Reworked image container classes for easier support of new features

Ivan Savenko 9 月之前
父节点
当前提交
4a600a9d4c

+ 2 - 2
client/CMakeLists.txt

@@ -99,7 +99,7 @@ set(vcmiclientcommon_SRCS
 	renderSDL/CursorHardware.cpp
 	renderSDL/CursorSoftware.cpp
 	renderSDL/FontChain.cpp
-	renderSDL/ImageScaled.cpp
+	renderSDL/ScalableImage.cpp
 	renderSDL/RenderHandler.cpp
 	renderSDL/SDLImage.cpp
 	renderSDL/SDLImageLoader.cpp
@@ -307,7 +307,7 @@ set(vcmiclientcommon_HEADERS
 	renderSDL/CursorHardware.h
 	renderSDL/CursorSoftware.h
 	renderSDL/FontChain.h
-	renderSDL/ImageScaled.h
+	renderSDL/ScalableImage.h
 	renderSDL/RenderHandler.h
 	renderSDL/SDLImage.h
 	renderSDL/SDLImageLoader.h

+ 18 - 26
client/render/AssetGenerator.cpp

@@ -58,10 +58,9 @@ void AssetGenerator::createAdventureOptionsCleanBackground()
 		return;
 	ResourcePath savePath(filename, EResType::IMAGE);
 
-	auto locator = ImageLocator(ImagePath::builtin("ADVOPTBK"));
-	locator.scalingFactor = 1;
+	auto locator = ImageLocator(ImagePath::builtin("ADVOPTBK"), EImageBlitMode::OPAQUE);
 
-	std::shared_ptr<IImage> img = GH.renderHandler().loadImage(locator, EImageBlitMode::OPAQUE);
+	std::shared_ptr<IImage> img = GH.renderHandler().loadImage(locator);
 
 	Canvas canvas = Canvas(Point(575, 585), CanvasScalingPolicy::IGNORE);
 	canvas.draw(img, Point(0, 0), Rect(0, 0, 575, 585));
@@ -88,10 +87,9 @@ void AssetGenerator::createBigSpellBook()
 		return;
 	ResourcePath savePath(filename, EResType::IMAGE);
 
-	auto locator = ImageLocator(ImagePath::builtin("SpelBack"));
-	locator.scalingFactor = 1;
+	auto locator = ImageLocator(ImagePath::builtin("SpelBack"), EImageBlitMode::OPAQUE);
 
-	std::shared_ptr<IImage> img = GH.renderHandler().loadImage(locator, EImageBlitMode::OPAQUE);
+	std::shared_ptr<IImage> img = GH.renderHandler().loadImage(locator);
 	Canvas canvas = Canvas(Point(800, 600), CanvasScalingPolicy::IGNORE);
 	// edges
 	canvas.draw(img, Point(0, 0), Rect(15, 38, 90, 45));
@@ -152,10 +150,9 @@ void AssetGenerator::createPlayerColoredBackground(const PlayerColor & player)
 
 	ResourcePath savePath(filename, EResType::IMAGE);
 
-	auto locator = ImageLocator(ImagePath::builtin("DiBoxBck"));
-	locator.scalingFactor = 1;
+	auto locator = ImageLocator(ImagePath::builtin("DiBoxBck"), EImageBlitMode::OPAQUE);
 
-	std::shared_ptr<IImage> texture = GH.renderHandler().loadImage(locator, EImageBlitMode::OPAQUE);
+	std::shared_ptr<IImage> texture = GH.renderHandler().loadImage(locator);
 
 	// transform to make color of brown DIBOX.PCX texture match color of specified player
 	auto filterSettings = VLC->settingsHandler->getFullConfig()["interface"]["playerColoredBackground"];
@@ -199,10 +196,10 @@ void AssetGenerator::createCombatUnitNumberWindow()
 	   !CResourceHandler::get("local")->createResource(savePathNegative.getOriginalName() + ".png"))
 		return;
 
-	auto locator = ImageLocator(ImagePath::builtin("CMNUMWIN"));
-	locator.scalingFactor = 1;
+	auto locator = ImageLocator(ImagePath::builtin("CMNUMWIN"), EImageBlitMode::OPAQUE);
+	locator.layer = EImageBlitMode::OPAQUE;
 
-	std::shared_ptr<IImage> texture = GH.renderHandler().loadImage(locator, EImageBlitMode::OPAQUE);
+	std::shared_ptr<IImage> texture = GH.renderHandler().loadImage(locator);
 
 	static const auto shifterNormal   = ColorFilter::genRangeShifter( 0.f, 0.f, 0.f, 0.6f, 0.2f, 1.0f );
 	static const auto shifterPositive = ColorFilter::genRangeShifter( 0.f, 0.f, 0.f, 0.2f, 1.0f, 0.2f );
@@ -233,10 +230,9 @@ void AssetGenerator::createCampaignBackground()
 		return;
 	ResourcePath savePath(filename, EResType::IMAGE);
 
-	auto locator = ImageLocator(ImagePath::builtin("CAMPBACK"));
-	locator.scalingFactor = 1;
+	auto locator = ImageLocator(ImagePath::builtin("CAMPBACK"), EImageBlitMode::OPAQUE);
 
-	std::shared_ptr<IImage> img = GH.renderHandler().loadImage(locator, EImageBlitMode::OPAQUE);
+	std::shared_ptr<IImage> img = GH.renderHandler().loadImage(locator);
 	Canvas canvas = Canvas(Point(800, 600), CanvasScalingPolicy::IGNORE);
 	
 	canvas.draw(img, Point(0, 0), Rect(0, 0, 800, 600));
@@ -263,9 +259,8 @@ void AssetGenerator::createCampaignBackground()
 	canvas.draw(img, Point(404, 414), Rect(313, 74, 197, 114));
 
 	// skull
-	auto locatorSkull = ImageLocator(ImagePath::builtin("CAMPNOSC"));
-	locatorSkull.scalingFactor = 1;
-	std::shared_ptr<IImage> imgSkull = GH.renderHandler().loadImage(locatorSkull, EImageBlitMode::OPAQUE);
+	auto locatorSkull = ImageLocator(ImagePath::builtin("CAMPNOSC"), EImageBlitMode::OPAQUE);
+	std::shared_ptr<IImage> imgSkull = GH.renderHandler().loadImage(locatorSkull);
 	canvas.draw(imgSkull, Point(562, 509), Rect(178, 108, 43, 19));
 
 	std::shared_ptr<IImage> image = GH.renderHandler().createImage(canvas.getInternalSurface());
@@ -290,10 +285,9 @@ void AssetGenerator::createChroniclesCampaignImages()
 			continue;
 		ResourcePath savePath(filename, EResType::IMAGE);
 
-		auto locator = ImageLocator(imgPathBg);
-		locator.scalingFactor = 1;
+		auto locator = ImageLocator(imgPathBg, EImageBlitMode::OPAQUE);
 
-		std::shared_ptr<IImage> img = GH.renderHandler().loadImage(locator, EImageBlitMode::OPAQUE);
+		std::shared_ptr<IImage> img = GH.renderHandler().loadImage(locator);
 		Canvas canvas = Canvas(Point(200, 116), CanvasScalingPolicy::IGNORE);
 		
 		switch (i)
@@ -323,9 +317,8 @@ void AssetGenerator::createChroniclesCampaignImages()
 			canvas.draw(img, Point(0, 0), Rect(268, 210, 200, 116));
 
 			//skull
-			auto locatorSkull = ImageLocator(ImagePath::builtin("CampSP1"));
-			locatorSkull.scalingFactor = 1;
-			std::shared_ptr<IImage> imgSkull = GH.renderHandler().loadImage(locatorSkull, EImageBlitMode::OPAQUE);
+			auto locatorSkull = ImageLocator(ImagePath::builtin("CampSP1"), EImageBlitMode::OPAQUE);
+			std::shared_ptr<IImage> imgSkull = GH.renderHandler().loadImage(locatorSkull);
 			canvas.draw(imgSkull, Point(162, 94), Rect(162, 94, 41, 22));
 			canvas.draw(img, Point(162, 94), Rect(424, 304, 14, 4));
 			canvas.draw(img, Point(162, 98), Rect(424, 308, 10, 4));
@@ -403,8 +396,7 @@ void AssetGenerator::createPaletteShiftedSprites()
 					return;
 
 				auto imgLoc = anim->getImageLocator(j, 0);
-				imgLoc.scalingFactor = 1;
-				auto img = GH.renderHandler().loadImage(imgLoc, EImageBlitMode::COLORKEY);
+				auto img = GH.renderHandler().loadImage(imgLoc);
 				for(int k = 0; k < paletteAnimations[i].size(); k++)
 				{
 					auto element = paletteAnimations[i][k];

+ 2 - 2
client/render/CAnimation.cpp

@@ -30,7 +30,7 @@ bool CAnimation::loadFrame(size_t frame, size_t group, bool verbose)
 	if(auto image = getImageImpl(frame, group, false))
 		return true;
 
-	std::shared_ptr<IImage> image = GH.renderHandler().loadImage(getImageLocator(frame, group), mode);
+	std::shared_ptr<IImage> image = GH.renderHandler().loadImage(getImageLocator(frame, group));
 
 	if(image)
 	{
@@ -224,5 +224,5 @@ ImageLocator CAnimation::getImageLocator(size_t frame, size_t group) const
 		throw std::runtime_error("Frame " + std::to_string(frame) + " of group " + std::to_string(group) + " is missing from animation " + name.getOriginalName() );
 	}
 
-	return ImageLocator(name, frame, group);
+	return ImageLocator(name, frame, group, mode);
 }

+ 4 - 4
client/render/Canvas.cpp

@@ -106,7 +106,7 @@ void Canvas::draw(const std::shared_ptr<IImage>& image, const Point & pos)
 {
 	assert(image);
 	if (image)
-		image->draw(surface, transformPos(pos));
+		image->draw(surface, transformPos(pos), nullptr, getScalingFactor());
 }
 
 void Canvas::draw(const std::shared_ptr<IImage>& image, const Point & pos, const Rect & sourceRect)
@@ -114,7 +114,7 @@ void Canvas::draw(const std::shared_ptr<IImage>& image, const Point & pos, const
 	Rect realSourceRect = sourceRect * getScalingFactor();
 	assert(image);
 	if (image)
-		image->draw(surface, transformPos(pos), &realSourceRect);
+		image->draw(surface, transformPos(pos), &realSourceRect, getScalingFactor());
 }
 
 void Canvas::draw(const Canvas & image, const Point & pos)
@@ -218,11 +218,11 @@ void Canvas::fillTexture(const std::shared_ptr<IImage>& image)
 	for (int y=0; y < surface->h; y+= imageArea.h)
 	{
 		for (int x=0; x < surface->w; x+= imageArea.w)
-			image->draw(surface, Point(renderArea.x + x * getScalingFactor(), renderArea.y + y * getScalingFactor()));
+			image->draw(surface, Point(renderArea.x + x * getScalingFactor(), renderArea.y + y * getScalingFactor()), nullptr, getScalingFactor());
 	}
 }
 
-SDL_Surface * Canvas::getInternalSurface()
+SDL_Surface * Canvas::getInternalSurface() const
 {
 	return surface;
 }

+ 1 - 1
client/render/Canvas.h

@@ -115,7 +115,7 @@ public:
 	int getScalingFactor() const;
 
 	/// Compatibility method. AVOID USAGE. To be removed once SDL abstraction layer is finished.
-	SDL_Surface * getInternalSurface();
+	SDL_Surface * getInternalSurface() const;
 
 	/// get the render area
 	Rect getRenderArea() const;

+ 4 - 8
client/render/IImage.h

@@ -68,10 +68,9 @@ class IImage
 {
 public:
 	//draws image on surface "where" at position
-	virtual void draw(SDL_Surface * where, const Point & pos, const Rect * src = nullptr) const = 0;
+	virtual void draw(SDL_Surface * where, const Point & pos, const Rect * src, int scalingFactor) const = 0;
 
 	virtual void scaleTo(const Point & size) = 0;
-	virtual void scaleInteger(int factor) = 0;
 
 	virtual void exportBitmap(const boost::filesystem::path & path) const = 0;
 
@@ -92,13 +91,10 @@ public:
 	virtual void adjustPalette(const ColorFilter & shifter, uint32_t colorsToSkipMask) = 0;
 
 	virtual void setAlpha(uint8_t value) = 0;
-	virtual void setBlitMode(EImageBlitMode mode) = 0;
 
 	//only indexed bitmaps with 7 special colors
 	virtual void setOverlayColor(const ColorRGBA & color) = 0;
 
-	virtual std::shared_ptr<const ISharedImage> getSharedImage() const = 0;
-
 	virtual ~IImage() = default;
 };
 
@@ -114,13 +110,13 @@ public:
 	virtual Rect contentRect() const = 0;
 	virtual void draw(SDL_Surface * where, SDL_Palette * palette, const Point & dest, const Rect * src, const ColorRGBA & colorMultiplier, uint8_t alpha, EImageBlitMode mode) const = 0;
 
-	[[nodiscard]] virtual std::shared_ptr<IImage> createImageReference(EImageBlitMode mode) const = 0;
+	virtual ~ISharedImage() = default;
+
+	virtual const SDL_Palette * getPalette() const = 0;
 
 	[[nodiscard]] virtual std::shared_ptr<const ISharedImage> horizontalFlip() const = 0;
 	[[nodiscard]] virtual std::shared_ptr<const ISharedImage> verticalFlip() const = 0;
 	[[nodiscard]] virtual std::shared_ptr<const ISharedImage> scaleInteger(int factor, SDL_Palette * palette, EImageBlitMode blitMode) const = 0;
 	[[nodiscard]] virtual std::shared_ptr<const ISharedImage> scaleTo(const Point & size, SDL_Palette * palette) const = 0;
 
-
-	virtual ~ISharedImage() = default;
 };

+ 5 - 1
client/render/IRenderHandler.h

@@ -20,6 +20,7 @@ struct SDL_Surface;
 class IFont;
 class IImage;
 class CAnimation;
+class SDLImageShared;
 enum class EImageBlitMode : uint8_t;
 enum EFonts : int8_t;
 
@@ -32,10 +33,13 @@ public:
 	virtual void onLibraryLoadingFinished(const Services * services) = 0;
 
 	/// Loads image using given path
-	virtual std::shared_ptr<IImage> loadImage(const ImageLocator & locator, EImageBlitMode mode) = 0;
+	virtual std::shared_ptr<IImage> loadImage(const ImageLocator & locator) = 0;
 	virtual std::shared_ptr<IImage> loadImage(const ImagePath & path, EImageBlitMode mode) = 0;
 	virtual std::shared_ptr<IImage> loadImage(const AnimationPath & path, int frame, int group, EImageBlitMode mode) = 0;
 
+	/// Loads single image without scaling support
+	virtual std::shared_ptr<SDLImageShared> loadSingleImage(const ImageLocator & locator) = 0;
+
 	/// temporary compatibility method. Creates IImage from existing SDL_Surface
 	/// Surface will be shared, caller must still free it with SDL_FreeSurface
 	virtual std::shared_ptr<IImage> createImage(SDL_Surface * source) = 0;

+ 14 - 81
client/render/ImageLocator.cpp

@@ -15,11 +15,10 @@
 
 #include "../../lib/json/JsonNode.h"
 
-ImageLocator::ImageLocator(const JsonNode & config)
+SharedImageLocator::SharedImageLocator(const JsonNode & config, EImageBlitMode mode)
 	: defFrame(config["defFrame"].Integer())
 	, defGroup(config["defGroup"].Integer())
-	, verticalFlip(config["verticalFlip"].Bool())
-	, horizontalFlip(config["horizontalFlip"].Bool())
+	, layer(mode)
 {
 	if(!config["file"].isNull())
 		image = ImagePath::fromJson(config["file"]);
@@ -28,19 +27,28 @@ ImageLocator::ImageLocator(const JsonNode & config)
 		defFile = AnimationPath::fromJson(config["defFile"]);
 }
 
-ImageLocator::ImageLocator(const ImagePath & path)
+SharedImageLocator::SharedImageLocator(const ImagePath & path, EImageBlitMode mode)
 	: image(path)
+	, layer(mode)
 {
 }
 
-ImageLocator::ImageLocator(const AnimationPath & path, int frame, int group)
+SharedImageLocator::SharedImageLocator(const AnimationPath & path, int frame, int group, EImageBlitMode mode)
 	: defFile(path)
 	, defFrame(frame)
 	, defGroup(group)
+	, layer(mode)
+{
+}
+
+ImageLocator::ImageLocator(const JsonNode & config, EImageBlitMode mode)
+	: SharedImageLocator(config, mode)
+	, verticalFlip(config["verticalFlip"].Bool())
+	, horizontalFlip(config["horizontalFlip"].Bool())
 {
 }
 
-bool ImageLocator::operator<(const ImageLocator & other) const
+bool SharedImageLocator::operator < (const SharedImageLocator & other) const
 {
 	if(image != other.image)
 		return image < other.image;
@@ -50,14 +58,6 @@ bool ImageLocator::operator<(const ImageLocator & other) const
 		return defGroup < other.defGroup;
 	if(defFrame != other.defFrame)
 		return defFrame < other.defFrame;
-	if(verticalFlip != other.verticalFlip)
-		return verticalFlip < other.verticalFlip;
-	if(horizontalFlip != other.horizontalFlip)
-		return horizontalFlip < other.horizontalFlip;
-	if(scalingFactor != other.scalingFactor)
-		return scalingFactor < other.scalingFactor;
-	if(playerColored != other.playerColored)
-		return playerColored < other.playerColored;
 	if(layer != other.layer)
 		return layer < other.layer;
 
@@ -68,70 +68,3 @@ bool ImageLocator::empty() const
 {
 	return !image.has_value() && !defFile.has_value();
 }
-
-ImageLocator ImageLocator::copyFile() const
-{
-	ImageLocator result;
-	result.scalingFactor = 1;
-	result.preScaledFactor = preScaledFactor;
-	result.image = image;
-	result.defFile = defFile;
-	result.defFrame = defFrame;
-	result.defGroup = defGroup;
-	return result;
-}
-
-ImageLocator ImageLocator::copyFileTransform() const
-{
-	ImageLocator result = copyFile();
-	result.horizontalFlip = horizontalFlip;
-	result.verticalFlip = verticalFlip;
-	return result;
-}
-
-ImageLocator ImageLocator::copyFileTransformScale() const
-{
-	return *this; // full copy
-}
-
-std::string ImageLocator::toString() const
-{
-	std::string result;
-	if (empty())
-		return "invalid";
-
-	if (image)
-	{
-		result += image->getOriginalName();
-		assert(!result.empty());
-	}
-
-	if (defFile)
-	{
-		result += defFile->getOriginalName();
-		assert(!result.empty());
-		result += "-" + std::to_string(defGroup);
-		result += "-" + std::to_string(defFrame);
-	}
-
-	if (verticalFlip)
-		result += "-vflip";
-
-	if (horizontalFlip)
-		result += "-hflip";
-
-	if (scalingFactor > 1)
-		result += "-scale" + std::to_string(scalingFactor);
-
-	if (playerColored.isValidPlayer())
-		result += "-player" + playerColored.toString();
-
-	if (layer == EImageBlitMode::ONLY_OVERLAY)
-		result += "-overlay";
-
-	if (layer == EImageBlitMode::ONLY_SHADOW)
-		result += "-shadow";
-
-
-	return result;
-}

+ 15 - 18
client/render/ImageLocator.h

@@ -14,35 +14,32 @@
 #include "../../lib/filesystem/ResourcePath.h"
 #include "../../lib/constants/EntityIdentifiers.h"
 
-struct ImageLocator
+struct SharedImageLocator
 {
 	std::optional<ImagePath> image;
 	std::optional<AnimationPath> defFile;
 	int defFrame = -1;
 	int defGroup = -1;
+	EImageBlitMode layer = EImageBlitMode::OPAQUE;
+
+	SharedImageLocator() = default;
+	SharedImageLocator(const AnimationPath & path, int frame, int group, EImageBlitMode layer);
+	SharedImageLocator(const JsonNode & config, EImageBlitMode layer);
+	SharedImageLocator(const ImagePath & path, EImageBlitMode layer);
+
+	bool operator < (const SharedImageLocator & other) const;
+};
 
-	PlayerColor playerColored = PlayerColor::CANNOT_DETERMINE; // FIXME: treat as identical to blue to avoid double-loading?
+struct ImageLocator : SharedImageLocator
+{
+	PlayerColor playerColored = PlayerColor::CANNOT_DETERMINE;
 
 	bool verticalFlip = false;
 	bool horizontalFlip = false;
 	int8_t scalingFactor = 0; // 0 = auto / use default scaling
-	int8_t preScaledFactor = 1;
-	EImageBlitMode layer = EImageBlitMode::OPAQUE;
 
-	ImageLocator() = default;
-	ImageLocator(const AnimationPath & path, int frame, int group);
-	explicit ImageLocator(const JsonNode & config);
-	explicit ImageLocator(const ImagePath & path);
+	using SharedImageLocator::SharedImageLocator;
+	 ImageLocator(const JsonNode & config, EImageBlitMode layer);
 
-	bool operator < (const ImageLocator & other) const;
 	bool empty() const;
-
-	ImageLocator copyFile() const;
-	ImageLocator copyFileTransform() const;
-	ImageLocator copyFileTransformScale() const;
-
-	// generates string representation of this image locator
-	// guaranteed to be a valid file path with no extension
-	// but may contain '/' if source file is in directory
-	std::string toString() const;
 };

+ 1 - 1
client/renderSDL/CursorHardware.cpp

@@ -59,7 +59,7 @@ void CursorHardware::setImage(std::shared_ptr<IImage> image, const Point & pivot
 
 	CSDL_Ext::fillSurface(cursorSurface, CSDL_Ext::toSDL(Colors::TRANSPARENCY));
 
-	image->draw(cursorSurface, Point(0,0));
+	image->draw(cursorSurface, Point(0,0), nullptr, GH.screenHandler().getScalingFactor());
 	auto cursorSurfaceScaled = CSDL_Ext::scaleSurface(cursorSurface, cursorDimensionsScaled.x, cursorDimensionsScaled.y );
 
 	auto oldCursor = cursor;

+ 1 - 1
client/renderSDL/CursorSoftware.cpp

@@ -65,7 +65,7 @@ void CursorSoftware::updateTexture()
 
 	CSDL_Ext::fillSurface(cursorSurface, CSDL_Ext::toSDL(Colors::TRANSPARENCY));
 
-	cursorImage->draw(cursorSurface, Point(0,0));
+	cursorImage->draw(cursorSurface, Point(0,0), nullptr, GH.screenHandler().getScalingFactor());
 	SDL_UpdateTexture(cursorTexture, nullptr, cursorSurface->pixels, cursorSurface->pitch);
 	needUpdate = false;
 }

+ 0 - 172
client/renderSDL/ImageScaled.cpp

@@ -1,172 +0,0 @@
-/*
- * ImageScaled.cpp, part of VCMI engine
- *
- * Authors: listed in file AUTHORS in main folder
- *
- * License: GNU General Public License v2.0 or later
- * Full text of license available in license.txt file, in main folder
- *
- */
-#include "StdInc.h"
-#include "ImageScaled.h"
-
-#include "SDLImage.h"
-#include "SDL_Extensions.h"
-
-#include "../gui/CGuiHandler.h"
-#include "../render/IScreenHandler.h"
-#include "../render/Colors.h"
-
-#include "../../lib/constants/EntityIdentifiers.h"
-
-#include <SDL_surface.h>
-
-ImageScaled::ImageScaled(const ImageLocator & inputLocator, const std::shared_ptr<const ISharedImage> & source, EImageBlitMode mode)
-	: source(source)
-	, locator(inputLocator)
-	, colorMultiplier(Colors::WHITE_TRUE)
-	, alphaValue(SDL_ALPHA_OPAQUE)
-	, blitMode(mode)
-{
-	prepareImages();
-}
-
-std::shared_ptr<const ISharedImage> ImageScaled::getSharedImage() const
-{
-	return body;
-}
-
-void ImageScaled::scaleInteger(int factor)
-{
-	assert(0);
-}
-
-void ImageScaled::scaleTo(const Point & size)
-{
-	if (source)
-		source = source->scaleTo(size, nullptr);
-
-	if (body)
-		body = body->scaleTo(size * GH.screenHandler().getScalingFactor(), nullptr);
-}
-
-void ImageScaled::exportBitmap(const boost::filesystem::path &path) const
-{
-	source->exportBitmap(path, nullptr);
-}
-
-bool ImageScaled::isTransparent(const Point &coords) const
-{
-	return source->isTransparent(coords);
-}
-
-Rect ImageScaled::contentRect() const
-{
-	return source->contentRect();
-}
-
-Point ImageScaled::dimensions() const
-{
-	return source->dimensions();
-}
-
-void ImageScaled::setAlpha(uint8_t value)
-{
-	alphaValue = value;
-}
-
-void ImageScaled::setBlitMode(EImageBlitMode mode)
-{
-	blitMode = mode;
-}
-
-void ImageScaled::draw(SDL_Surface *where, const Point &pos, const Rect *src) const
-{
-	if (shadow)
-		shadow->draw(where, nullptr, pos, src, Colors::WHITE_TRUE, alphaValue, blitMode);
-	if (body)
-		body->draw(where, nullptr, pos, src, Colors::WHITE_TRUE, alphaValue, blitMode);
-	if (overlay)
-		overlay->draw(where, nullptr, pos, src, colorMultiplier, colorMultiplier.a * alphaValue / 255, blitMode);
-}
-
-void ImageScaled::setOverlayColor(const ColorRGBA & color)
-{
-	colorMultiplier = color;
-}
-
-void ImageScaled::playerColored(PlayerColor player)
-{
-	playerColor = player;
-	prepareImages();
-}
-
-void ImageScaled::shiftPalette(uint32_t firstColorID, uint32_t colorsToMove, uint32_t distanceToMove)
-{
-	// TODO: implement
-}
-
-void ImageScaled::adjustPalette(const ColorFilter &shifter, uint32_t colorsToSkipMask)
-{
-	// TODO: implement
-}
-
-void ImageScaled::prepareImages()
-{
-	switch(blitMode)
-	{
-		case EImageBlitMode::OPAQUE:
-		case EImageBlitMode::COLORKEY:
-		case EImageBlitMode::SIMPLE:
-			locator.layer = blitMode;
-			locator.playerColored = playerColor;
-			body = GH.renderHandler().loadImage(locator, blitMode)->getSharedImage();
-			break;
-
-		case EImageBlitMode::WITH_SHADOW_AND_OVERLAY:
-		case EImageBlitMode::ONLY_BODY:
-			locator.layer = EImageBlitMode::ONLY_BODY;
-			locator.playerColored = playerColor;
-			body = GH.renderHandler().loadImage(locator, blitMode)->getSharedImage();
-			break;
-
-		case EImageBlitMode::WITH_SHADOW:
-		case EImageBlitMode::ONLY_BODY_IGNORE_OVERLAY:
-			locator.layer = EImageBlitMode::ONLY_BODY_IGNORE_OVERLAY;
-			locator.playerColored = playerColor;
-			body = GH.renderHandler().loadImage(locator, blitMode)->getSharedImage();
-			break;
-
-		case EImageBlitMode::ONLY_SHADOW:
-		case EImageBlitMode::ONLY_OVERLAY:
-			body = nullptr;
-			break;
-	}
-
-	switch(blitMode)
-	{
-		case EImageBlitMode::WITH_SHADOW:
-		case EImageBlitMode::ONLY_SHADOW:
-		case EImageBlitMode::WITH_SHADOW_AND_OVERLAY:
-			locator.layer = EImageBlitMode::ONLY_SHADOW;
-			locator.playerColored = PlayerColor::CANNOT_DETERMINE;
-			shadow = GH.renderHandler().loadImage(locator, blitMode)->getSharedImage();
-			break;
-		default:
-			shadow = nullptr;
-			break;
-	}
-
-	switch(blitMode)
-	{
-		case EImageBlitMode::ONLY_OVERLAY:
-		case EImageBlitMode::WITH_SHADOW_AND_OVERLAY:
-			locator.layer = EImageBlitMode::ONLY_OVERLAY;
-			locator.playerColored = PlayerColor::CANNOT_DETERMINE;
-			overlay = GH.renderHandler().loadImage(locator, blitMode)->getSharedImage();
-			break;
-		default:
-			overlay = nullptr;
-			break;
-	}
-}

+ 0 - 66
client/renderSDL/ImageScaled.h

@@ -1,66 +0,0 @@
-/*
- * ImageScaled.h, part of VCMI engine
- *
- * Authors: listed in file AUTHORS in main folder
- *
- * License: GNU General Public License v2.0 or later
- * Full text of license available in license.txt file, in main folder
- *
- */
-#pragma once
-
-#include "../render/IImage.h"
-#include "../render/IRenderHandler.h"
-
-#include "../../lib/Color.h"
-#include "../../lib/constants/EntityIdentifiers.h"
-
-struct SDL_Palette;
-
-class SDLImageShared;
-
-// Upscaled image with several mechanisms to emulate H3 palette effects
-class ImageScaled final : public IImage
-{
-private:
-
-	/// Original unscaled image
-	std::shared_ptr<const ISharedImage> source;
-
-	/// Upscaled shadow of our image, may be null
-	std::shared_ptr<const ISharedImage> shadow;
-
-	/// Upscaled main part of our image, may be null
-	std::shared_ptr<const ISharedImage> body;
-
-	/// Upscaled overlay (player color, selection highlight) of our image, may be null
-	std::shared_ptr<const ISharedImage> overlay;
-
-	ImageLocator locator;
-
-	ColorRGBA colorMultiplier;
-	PlayerColor playerColor = PlayerColor::CANNOT_DETERMINE;
-
-	uint8_t alphaValue;
-	EImageBlitMode blitMode;
-
-	void prepareImages();
-public:
-	ImageScaled(const ImageLocator & locator, const std::shared_ptr<const ISharedImage> & source, EImageBlitMode mode);
-
-	void scaleInteger(int factor) override;
-	void scaleTo(const Point & size) override;
-	void exportBitmap(const boost::filesystem::path & path) const override;
-	bool isTransparent(const Point & coords) const override;
-	Rect contentRect() const override;
-	Point dimensions() const override;
-	void setAlpha(uint8_t value) override;
-	void setBlitMode(EImageBlitMode mode) override;
-	void draw(SDL_Surface * where, const Point & pos, const Rect * src) const override;
-	void setOverlayColor(const ColorRGBA & color) override;
-	void playerColored(PlayerColor player) override;
-	void shiftPalette(uint32_t firstColorID, uint32_t colorsToMove, uint32_t distanceToMove) override;
-	void adjustPalette(const ColorFilter & shifter, uint32_t colorsToSkipMask) override;
-
-	std::shared_ptr<const ISharedImage> getSharedImage() const override;
-};

+ 105 - 189
client/renderSDL/RenderHandler.cpp

@@ -11,7 +11,7 @@
 #include "RenderHandler.h"
 
 #include "SDLImage.h"
-#include "ImageScaled.h"
+#include "ScalableImage.h"
 #include "FontChain.h"
 
 #include "../gui/CGuiHandler.h"
@@ -22,6 +22,7 @@
 #include "../render/ColorFilter.h"
 #include "../render/IScreenHandler.h"
 #include "../../lib/json/JsonUtils.h"
+#include "../../lib/CThreadHelper.h"
 #include "../../lib/filesystem/Filesystem.h"
 #include "../../lib/VCMIDirs.h"
 
@@ -55,60 +56,7 @@ std::shared_ptr<CDefFile> RenderHandler::getAnimationFile(const AnimationPath &
 	return result;
 }
 
-std::optional<ResourcePath> RenderHandler::getPathForScaleFactor(const ResourcePath & path, const std::string & factor)
-{
-	if(path.getType() == EResType::IMAGE)
-	{
-		auto p = ImagePath::builtin(path.getName());
-		if(CResourceHandler::get()->existsResource(p.addPrefix("SPRITES" + factor + "X/")))
-			return std::optional<ResourcePath>(p.addPrefix("SPRITES" + factor + "X/"));
-		if(CResourceHandler::get()->existsResource(p.addPrefix("DATA" + factor + "X/")))
-			return std::optional<ResourcePath>(p.addPrefix("DATA" + factor + "X/"));
-	}
-	else
-	{
-		auto p = AnimationPath::builtin(path.getName());
-		auto pJson = p.toType<EResType::JSON>();
-		if(CResourceHandler::get()->existsResource(p.addPrefix("SPRITES" + factor + "X/")))
-			return std::optional<ResourcePath>(p.addPrefix("SPRITES" + factor + "X/"));
-		if(CResourceHandler::get()->existsResource(pJson))
-			return std::optional<ResourcePath>(p);
-		if(CResourceHandler::get()->existsResource(pJson.addPrefix("SPRITES" + factor + "X/")))
-			return std::optional<ResourcePath>(p.addPrefix("SPRITES" + factor + "X/"));
-	}
-
-	return std::nullopt;
-}
-
-std::pair<ResourcePath, int> RenderHandler::getScalePath(const ResourcePath & p)
-{
-	auto path = p;
-	int scaleFactor = 1;
-	if(getScalingFactor() > 1)
-	{
-		std::vector<int> factorsToCheck = {getScalingFactor(), 4, 3, 2};
-		for(auto factorToCheck : factorsToCheck)
-		{
-			std::string name = boost::algorithm::to_upper_copy(p.getName());
-			boost::replace_all(name, "SPRITES/", std::string("SPRITES") + std::to_string(factorToCheck) + std::string("X/"));
-			boost::replace_all(name, "DATA/", std::string("DATA") + std::to_string(factorToCheck) + std::string("X/"));
-			ResourcePath scaledPath = ImagePath::builtin(name);
-			if(p.getType() != EResType::IMAGE)
-				scaledPath = AnimationPath::builtin(name);
-			auto tmpPath = getPathForScaleFactor(scaledPath, std::to_string(factorToCheck));
-			if(tmpPath)
-			{
-				path = *tmpPath;
-				scaleFactor = factorToCheck;
-				break;
-			}
-		}
-	}
-
-	return std::pair<ResourcePath, int>(path, scaleFactor);
-};
-
-void RenderHandler::initFromJson(AnimationLayoutMap & source, const JsonNode & config)
+void RenderHandler::initFromJson(AnimationLayoutMap & source, const JsonNode & config, EImageBlitMode mode)
 {
 	std::string basepath;
 	basepath = config["basepath"].String();
@@ -128,7 +76,7 @@ void RenderHandler::initFromJson(AnimationLayoutMap & source, const JsonNode & c
 			JsonNode toAdd = frame;
 			JsonUtils::inherit(toAdd, base);
 			toAdd["file"].String() = basepath + frame.String();
-			source[groupID].emplace_back(toAdd);
+			source[groupID].emplace_back(toAdd, mode);
 		}
 	}
 
@@ -149,15 +97,26 @@ void RenderHandler::initFromJson(AnimationLayoutMap & source, const JsonNode & c
 		if (toAdd.Struct().count("defFile"))
 			toAdd["defFile"].String() = basepath + node["defFile"].String();
 
-		source[group][frame] = ImageLocator(toAdd);
+		source[group][frame] = ImageLocator(toAdd, mode);
 	}
 }
 
-RenderHandler::AnimationLayoutMap & RenderHandler::getAnimationLayout(const AnimationPath & path)
+RenderHandler::AnimationLayoutMap & RenderHandler::getAnimationLayout(const AnimationPath & path, int scalingFactor, EImageBlitMode mode)
 {
-	auto tmp = getScalePath(path);
-	auto animPath = AnimationPath::builtin(tmp.first.getName());
-	AnimationPath actualPath = boost::starts_with(animPath.getName(), "SPRITES") ? animPath : animPath.addPrefix("SPRITES/");
+	static constexpr std::array scaledSpritesPath = {
+		"", // 0x
+		"SPRITES/",
+		"SPRITES2X/",
+		"SPRITES3X/",
+		"SPRITES4X/",
+	};
+
+	std::string pathString = path.getName();
+
+	if (boost::starts_with(pathString, "SPRITES/"))
+		pathString = pathString.substr(std::string("SPRITES/").length());
+
+	AnimationPath actualPath = AnimationPath::builtin(scaledSpritesPath.at(scalingFactor) + pathString);
 
 	auto it = animationLayouts.find(actualPath);
 
@@ -184,15 +143,11 @@ RenderHandler::AnimationLayoutMap & RenderHandler::getAnimationLayout(const Anim
 		std::unique_ptr<ui8[]> textData(new ui8[stream->getSize()]);
 		stream->read(textData.get(), stream->getSize());
 
-		const JsonNode config(reinterpret_cast<const std::byte*>(textData.get()), stream->getSize(), animPath.getOriginalName());
+		const JsonNode config(reinterpret_cast<const std::byte*>(textData.get()), stream->getSize(), path.getOriginalName());
 
-		initFromJson(result, config);
+		initFromJson(result, config, mode);
 	}
 
-	for(auto & g : result)
-		for(auto & i : g.second)
-			i.preScaledFactor = tmp.second;
-
 	animationLayouts[actualPath] = result;
 	return animationLayouts[actualPath];
 }
@@ -202,209 +157,170 @@ int RenderHandler::getScalingFactor() const
 	return GH.screenHandler().getScalingFactor();
 }
 
-ImageLocator RenderHandler::getLocatorForAnimationFrame(const AnimationPath & path, int frame, int group)
+ImageLocator RenderHandler::getLocatorForAnimationFrame(const AnimationPath & path, int frame, int group, EImageBlitMode mode)
 {
-	const auto & layout = getAnimationLayout(path);
+	const auto & layout = getAnimationLayout(path, 1, mode);
 	if (!layout.count(group))
-		return ImageLocator(ImagePath::builtin("DEFAULT"));
+		return ImageLocator(ImagePath::builtin("DEFAULT"), mode);
 
 	if (frame >= layout.at(group).size())
-		return ImageLocator(ImagePath::builtin("DEFAULT"));
+		return ImageLocator(ImagePath::builtin("DEFAULT"), mode);
 
 	const auto & locator = layout.at(group).at(frame);
 	if (locator.image || locator.defFile)
 		return locator;
 
-	return ImageLocator(path, frame, group);
+	return ImageLocator(path, frame, group, mode);
 }
 
-std::shared_ptr<const ISharedImage> RenderHandler::loadImageImpl(const ImageLocator & locator)
+std::shared_ptr<ScalableImageShared> RenderHandler::loadImageImpl(const ImageLocator & locator)
 {
 	auto it = imageFiles.find(locator);
 	if (it != imageFiles.end())
 		return it->second;
 
-	// TODO: order should be different:
-	// 1) try to find correctly scaled image
-	// 2) if fails -> try to find correctly transformed
-	// 3) if also fails -> try to find image from correct file
-	// 4) load missing part of the sequence
-	// TODO: check whether (load -> transform -> scale) or (load -> scale -> transform) order should be used for proper loading of pre-scaled data
-	auto imageFromFile = loadImageFromFile(locator.copyFile());
-	auto transformedImage = transformImage(locator.copyFileTransform(), imageFromFile);
-	auto scaledImage = scaleImage(locator.copyFileTransformScale(), transformedImage);
+	auto sdlImage = loadImageFromFileUncached(locator);
+	auto scaledImage = std::make_shared<ScalableImageShared>(locator, sdlImage);
 
+	storeCachedImage(locator, scaledImage);
 	return scaledImage;
 }
 
-std::shared_ptr<const ISharedImage> RenderHandler::loadImageFromFileUncached(const ImageLocator & locator)
+std::shared_ptr<SDLImageShared> RenderHandler::loadImageFromFileUncached(const ImageLocator & locator)
 {
 	if(locator.image)
 	{
 		// TODO: create EmptySharedImage class that will be instantiated if image does not exists or fails to load
-		return std::make_shared<SDLImageShared>(*locator.image, locator.preScaledFactor);
+		return std::make_shared<SDLImageShared>(*locator.image);
 	}
 
 	if(locator.defFile)
 	{
 		auto defFile = getAnimationFile(*locator.defFile);
-		int preScaledFactor = locator.preScaledFactor;
-		if(!defFile) // no prescale for this frame
-		{
-			auto tmpPath = (*locator.defFile).getName();
-			boost::algorithm::replace_all(tmpPath, "SPRITES2X/", "SPRITES/");
-			boost::algorithm::replace_all(tmpPath, "SPRITES3X/", "SPRITES/");
-			boost::algorithm::replace_all(tmpPath, "SPRITES4X/", "SPRITES/");
-			preScaledFactor = 1;
-			defFile = getAnimationFile(AnimationPath::builtin(tmpPath));
-		}
 		if(defFile->hasFrame(locator.defFrame, locator.defGroup))
-			return std::make_shared<SDLImageShared>(defFile.get(), locator.defFrame, locator.defGroup, preScaledFactor);
+			return std::make_shared<SDLImageShared>(defFile.get(), locator.defFrame, locator.defGroup);
 		else
 		{
 			logGlobal->error("Frame %d in group %d not found in file: %s", 
 				locator.defFrame, locator.defGroup, locator.defFile->getName().c_str());
-			return std::make_shared<SDLImageShared>(ImagePath::builtin("DEFAULT"), locator.preScaledFactor);
+			return std::make_shared<SDLImageShared>(ImagePath::builtin("DEFAULT"));
 		}
 	}
 
 	throw std::runtime_error("Invalid image locator received!");
 }
 
-void RenderHandler::storeCachedImage(const ImageLocator & locator, std::shared_ptr<const ISharedImage> image)
+void RenderHandler::storeCachedImage(const ImageLocator & locator, std::shared_ptr<ScalableImageShared> image)
 {
 	imageFiles[locator] = image;
-
-#if 0
-	const boost::filesystem::path outPath = VCMIDirs::get().userExtractedPath() / "imageCache" / (locator.toString() + ".png");
-	boost::filesystem::path outDir = outPath;
-	outDir.remove_filename();
-	boost::filesystem::create_directories(outDir);
-	image->exportBitmap(outPath , nullptr);
-#endif
 }
 
-std::shared_ptr<const ISharedImage> RenderHandler::loadImageFromFile(const ImageLocator & locator)
+std::shared_ptr<SDLImageShared> RenderHandler::loadSingleImage(const ImageLocator & locator)
 {
-	if (imageFiles.count(locator))
-		return imageFiles.at(locator);
+	assert(locator.scalingFactor != 0);
+
+	static constexpr std::array scaledDataPath = {
+		"", // 0x
+		"DATA/",
+		"DATA2X/",
+		"DATA3X/",
+		"DATA4X/",
+	};
+
+	static constexpr std::array scaledSpritesPath = {
+		"", // 0x
+		"SPRITES/",
+		"SPRITES2X/",
+		"SPRITES3X/",
+		"SPRITES4X/",
+	};
 
-	auto result = loadImageFromFileUncached(locator);
-	storeCachedImage(locator, result);
-	return result;
-}
-
-std::shared_ptr<const ISharedImage> RenderHandler::transformImage(const ImageLocator & locator, std::shared_ptr<const ISharedImage> image)
-{
-	if (imageFiles.count(locator))
-		return imageFiles.at(locator);
-
-	auto result = image;
+	if(locator.image)
+	{
+		std::string imagePathString = locator.image->getName();
 
-	if (locator.verticalFlip)
-		result = result->verticalFlip();
+		if(locator.layer == EImageBlitMode::ONLY_OVERLAY)
+			imagePathString += "-OVERLAY";
+		if(locator.layer == EImageBlitMode::ONLY_SHADOW)
+			imagePathString += "-SHADOW";
 
-	if (locator.horizontalFlip)
-		result = result->horizontalFlip();
+		auto imagePath = ImagePath::builtin(imagePathString);
+		auto imagePathSprites = ImagePath::builtin(imagePathString).addPrefix(scaledSpritesPath.at(locator.scalingFactor));
+		auto imagePathData = ImagePath::builtin(imagePathString).addPrefix(scaledDataPath.at(locator.scalingFactor));
 
-	storeCachedImage(locator, result);
-	return result;
-}
+		if(CResourceHandler::get()->existsResource(imagePathSprites))
+			return std::make_shared<SDLImageShared>(imagePathSprites);
 
-std::shared_ptr<const ISharedImage> RenderHandler::scaleImage(const ImageLocator & locator, std::shared_ptr<const ISharedImage> image)
-{
-	if (imageFiles.count(locator))
-		return imageFiles.at(locator);
+		if(CResourceHandler::get()->existsResource(imagePathData))
+			return std::make_shared<SDLImageShared>(imagePathData);
 
-	auto handle = image->createImageReference(locator.layer);
+		if(CResourceHandler::get()->existsResource(imagePath))
+			return std::make_shared<SDLImageShared>(imagePath);
+	}
 
-	assert(locator.scalingFactor != 1); // should be filtered-out before
-	if (locator.playerColored != PlayerColor::CANNOT_DETERMINE)
-		handle->playerColored(locator.playerColored);
+	if(locator.defFile)
+	{
+		AnimationPath defFilePath = locator.defFile->addPrefix(scaledSpritesPath.at(locator.scalingFactor));
+		auto defFile = getAnimationFile(defFilePath);
 
-	handle->scaleInteger(locator.scalingFactor);
+		if(defFile && defFile->hasFrame(locator.defFrame, locator.defGroup))
+		{
+			return std::make_shared<SDLImageShared>(defFile.get(), locator.defFrame, locator.defGroup);
+		}
+	}
 
-	auto result = handle->getSharedImage();
-	storeCachedImage(locator, result);
-	return result;
+	return nullptr;
 }
 
-std::shared_ptr<IImage> RenderHandler::loadImage(const ImageLocator & locator, EImageBlitMode mode)
+std::shared_ptr<IImage> RenderHandler::loadImage(const ImageLocator & locator)
 {
 	ImageLocator adjustedLocator = locator;
 
-	if(adjustedLocator.image)
-	{
-		std::string imgPath = (*adjustedLocator.image).getName();
-		if(adjustedLocator.layer == EImageBlitMode::ONLY_OVERLAY)
-			imgPath += "-OVERLAY";
-		if(adjustedLocator.layer == EImageBlitMode::ONLY_SHADOW)
-			imgPath += "-SHADOW";
-
-		if(CResourceHandler::get()->existsResource(ImagePath::builtin(imgPath)) ||
-		   CResourceHandler::get()->existsResource(ImagePath::builtin(imgPath).addPrefix("DATA/")) ||
-		   CResourceHandler::get()->existsResource(ImagePath::builtin(imgPath).addPrefix("SPRITES/")))
-			adjustedLocator.image = ImagePath::builtin(imgPath);
-	}
-
-	if(adjustedLocator.defFile && adjustedLocator.scalingFactor == 0)
-	{
-		auto tmp = getScalePath(*adjustedLocator.defFile);
-		adjustedLocator.defFile = AnimationPath::builtin(tmp.first.getName());
-		adjustedLocator.preScaledFactor = tmp.second;
-	}
-	if(adjustedLocator.image && adjustedLocator.scalingFactor == 0)
-	{
-		auto tmp = getScalePath(*adjustedLocator.image);
-		adjustedLocator.image = ImagePath::builtin(tmp.first.getName());
-		adjustedLocator.preScaledFactor = tmp.second;
-	}
-
-	if (adjustedLocator.scalingFactor == 0 && getScalingFactor() != 1 )
-	{
-		auto unscaledLocator = adjustedLocator;
-		auto scaledLocator = adjustedLocator;
-
-		unscaledLocator.scalingFactor = 1;
-		scaledLocator.scalingFactor = getScalingFactor();
-		auto unscaledImage = loadImageImpl(unscaledLocator);
-
-		return std::make_shared<ImageScaled>(scaledLocator, unscaledImage, mode);
-	}
+	std::shared_ptr<ScalableImageInstance> result;
 
 	if (adjustedLocator.scalingFactor == 0)
 	{
 		auto scaledLocator = adjustedLocator;
 		scaledLocator.scalingFactor = getScalingFactor();
 
-		return loadImageImpl(scaledLocator)->createImageReference(mode);
+		result = loadImageImpl(scaledLocator)->createImageReference();
 	}
 	else
-		return loadImageImpl(adjustedLocator)->createImageReference(mode);
+		result = loadImageImpl(adjustedLocator)->createImageReference();
+
+	if (locator.horizontalFlip)
+		result->horizontalFlip();
+	if (locator.verticalFlip)
+		result->verticalFlip();
+
+	return result;
 }
 
 std::shared_ptr<IImage> RenderHandler::loadImage(const AnimationPath & path, int frame, int group, EImageBlitMode mode)
 {
-	auto tmp = getScalePath(path);
-	ImageLocator locator = getLocatorForAnimationFrame(AnimationPath::builtin(tmp.first.getName()), frame, group);
-	locator.preScaledFactor = tmp.second;
-	return loadImage(locator, mode);
+	ImageLocator locator = getLocatorForAnimationFrame(path, frame, group, mode);
+	return loadImage(locator);
 }
 
 std::shared_ptr<IImage> RenderHandler::loadImage(const ImagePath & path, EImageBlitMode mode)
 {
-	ImageLocator locator(path);
-	return loadImage(locator, mode);
+	ImageLocator locator(path, mode);
+	return loadImage(locator);
 }
 
 std::shared_ptr<IImage> RenderHandler::createImage(SDL_Surface * source)
 {
-	return std::make_shared<SDLImageShared>(source)->createImageReference(EImageBlitMode::SIMPLE);
+	auto baseImage = std::make_shared<SDLImageShared>(source);
+	SharedImageLocator locator;
+	locator.layer = EImageBlitMode::SIMPLE;
+	auto scalableImage = std::make_shared<ScalableImageShared>(locator, baseImage);
+
+	return scalableImage->createImageReference();
 }
 
 std::shared_ptr<CAnimation> RenderHandler::loadAnimation(const AnimationPath & path, EImageBlitMode mode)
 {
-	return std::make_shared<CAnimation>(path, getAnimationLayout(path), mode);
+	return std::make_shared<CAnimation>(path, getAnimationLayout(path, 1, mode), mode);
 }
 
 void RenderHandler::addImageListEntries(const EntityService * service)
@@ -416,7 +332,7 @@ void RenderHandler::addImageListEntries(const EntityService * service)
 			if (imageName.empty())
 				return;
 
-			auto & layout = getAnimationLayout(AnimationPath::builtin("SPRITES/" + listName));
+			auto & layout = getAnimationLayout(AnimationPath::builtin("SPRITES/" + listName), 1, EImageBlitMode::SIMPLE);
 
 			JsonNode entry;
 			entry["file"].String() = imageName;
@@ -424,7 +340,7 @@ void RenderHandler::addImageListEntries(const EntityService * service)
 			if (index >= layout[group].size())
 				layout[group].resize(index + 1);
 
-			layout[group][index] = ImageLocator(entry);
+			layout[group][index] = ImageLocator(entry, EImageBlitMode::SIMPLE);
 		});
 	});
 }

+ 11 - 15
client/renderSDL/RenderHandler.h

@@ -17,7 +17,7 @@ VCMI_LIB_NAMESPACE_END
 
 class CDefFile;
 class SDLImageShared;
-class ISharedImage;
+class ScalableImageShared;
 
 class RenderHandler : public IRenderHandler
 {
@@ -25,28 +25,22 @@ class RenderHandler : public IRenderHandler
 
 	std::map<AnimationPath, std::shared_ptr<CDefFile>> animationFiles;
 	std::map<AnimationPath, AnimationLayoutMap> animationLayouts;
-	std::map<ImageLocator, std::shared_ptr<const ISharedImage>> imageFiles;
+	std::map<SharedImageLocator, std::shared_ptr<ScalableImageShared>> imageFiles;
 	std::map<EFonts, std::shared_ptr<const IFont>> fonts;
 
 	std::shared_ptr<CDefFile> getAnimationFile(const AnimationPath & path);
-	std::optional<ResourcePath> getPathForScaleFactor(const ResourcePath & path, const std::string & factor);
-	std::pair<ResourcePath, int> getScalePath(const ResourcePath & p);
-	AnimationLayoutMap & getAnimationLayout(const AnimationPath & path);
-	void initFromJson(AnimationLayoutMap & layout, const JsonNode & config);
+	AnimationLayoutMap & getAnimationLayout(const AnimationPath & path, int scalingFactor, EImageBlitMode mode);
+	void initFromJson(AnimationLayoutMap & layout, const JsonNode & config, EImageBlitMode mode);
 
 	void addImageListEntry(size_t index, size_t group, const std::string & listName, const std::string & imageName);
 	void addImageListEntries(const EntityService * service);
-	void storeCachedImage(const ImageLocator & locator, std::shared_ptr<const ISharedImage> image);
+	void storeCachedImage(const ImageLocator & locator, std::shared_ptr<ScalableImageShared> image);
 
-	std::shared_ptr<const ISharedImage> loadImageImpl(const ImageLocator & config);
+	std::shared_ptr<ScalableImageShared> loadImageImpl(const ImageLocator & config);
 
-	std::shared_ptr<const ISharedImage> loadImageFromFileUncached(const ImageLocator & locator);
-	std::shared_ptr<const ISharedImage> loadImageFromFile(const ImageLocator & locator);
+	std::shared_ptr<SDLImageShared> loadImageFromFileUncached(const ImageLocator & locator);
 
-	std::shared_ptr<const ISharedImage> transformImage(const ImageLocator & locator, std::shared_ptr<const ISharedImage> image);
-	std::shared_ptr<const ISharedImage> scaleImage(const ImageLocator & locator, std::shared_ptr<const ISharedImage> image);
-
-	ImageLocator getLocatorForAnimationFrame(const AnimationPath & path, int frame, int group);
+	ImageLocator getLocatorForAnimationFrame(const AnimationPath & path, int frame, int group, EImageBlitMode mode);
 
 	int getScalingFactor() const;
 
@@ -55,10 +49,12 @@ public:
 	// IRenderHandler implementation
 	void onLibraryLoadingFinished(const Services * services) override;
 
-	std::shared_ptr<IImage> loadImage(const ImageLocator & locator, EImageBlitMode mode) override;
+	std::shared_ptr<IImage> loadImage(const ImageLocator & locator) override;
 	std::shared_ptr<IImage> loadImage(const ImagePath & path, EImageBlitMode mode) override;
 	std::shared_ptr<IImage> loadImage(const AnimationPath & path, int frame, int group, EImageBlitMode mode) override;
 
+	std::shared_ptr<SDLImageShared> loadSingleImage(const ImageLocator & locator) override;
+
 	std::shared_ptr<CAnimation> loadAnimation(const AnimationPath & path, EImageBlitMode mode) override;
 
 	std::shared_ptr<IImage> createImage(SDL_Surface * source) override;

+ 25 - 305
client/renderSDL/SDLImage.cpp

@@ -28,59 +28,6 @@
 
 class SDLImageLoader;
 
-//First 8 colors in def palette used for transparency
-static constexpr std::array<SDL_Color, 8> sourcePalette = {{
-	{0,   255, 255, SDL_ALPHA_OPAQUE},
-	{255, 150, 255, SDL_ALPHA_OPAQUE},
-	{255, 100, 255, SDL_ALPHA_OPAQUE},
-	{255, 50,  255, SDL_ALPHA_OPAQUE},
-	{255, 0,   255, SDL_ALPHA_OPAQUE},
-	{255, 255, 0,   SDL_ALPHA_OPAQUE},
-	{180, 0,   255, SDL_ALPHA_OPAQUE},
-	{0,   255, 0,   SDL_ALPHA_OPAQUE}
-}};
-
-static constexpr std::array<ColorRGBA, 8> targetPalette = {{
-	{0, 0, 0, 0  }, // 0 - transparency                  ( used in most images )
-	{0, 0, 0, 64 }, // 1 - shadow border                 ( used in battle, adventure map def's )
-	{0, 0, 0, 64 }, // 2 - shadow border                 ( used in fog-of-war def's )
-	{0, 0, 0, 128}, // 3 - shadow body                   ( used in fog-of-war def's )
-	{0, 0, 0, 128}, // 4 - shadow body                   ( used in battle, adventure map def's )
-	{0, 0, 0, 0  }, // 5 - selection / owner flag        ( used in battle, adventure map def's )
-	{0, 0, 0, 128}, // 6 - shadow body   below selection ( used in battle def's )
-	{0, 0, 0, 64 }  // 7 - shadow border below selection ( used in battle def's )
-}};
-
-static ui8 mixChannels(ui8 c1, ui8 c2, ui8 a1, ui8 a2)
-{
-	return c1*a1 / 256 + c2*a2*(255 - a1) / 256 / 256;
-}
-
-static ColorRGBA addColors(const ColorRGBA & base, const ColorRGBA & over)
-{
-	return ColorRGBA(
-		mixChannels(over.r, base.r, over.a, base.a),
-		mixChannels(over.g, base.g, over.a, base.a),
-		mixChannels(over.b, base.b, over.a, base.a),
-		static_cast<ui8>(over.a + base.a * (255 - over.a) / 256)
-		);
-}
-
-static bool colorsSimilar (const SDL_Color & lhs, const SDL_Color & rhs)
-{
-	// it seems that H3 does not requires exact match to replace colors -> (255, 103, 255) gets interpreted as shadow
-	// exact logic is not clear and requires extensive testing with image editing
-	// potential reason is that H3 uses 16-bit color format (565 RGB bits), meaning that 3 least significant bits are lost in red and blue component
-	static const int threshold = 8;
-
-	int diffR = static_cast<int>(lhs.r) - rhs.r;
-	int diffG = static_cast<int>(lhs.g) - rhs.g;
-	int diffB = static_cast<int>(lhs.b) - rhs.b;
-	int diffA = static_cast<int>(lhs.a) - rhs.a;
-
-	return std::abs(diffR) < threshold && std::abs(diffG) < threshold && std::abs(diffB) < threshold && std::abs(diffA) < threshold;
-}
-
 int IImage::width() const
 {
 	return dimensions().x;
@@ -91,12 +38,11 @@ int IImage::height() const
 	return dimensions().y;
 }
 
-SDLImageShared::SDLImageShared(const CDefFile * data, size_t frame, size_t group, int preScaleFactor)
+SDLImageShared::SDLImageShared(const CDefFile * data, size_t frame, size_t group)
 	: surf(nullptr),
 	margins(0, 0),
 	fullSize(0, 0),
-	originalPalette(nullptr),
-	preScaleFactor(preScaleFactor)
+	originalPalette(nullptr)
 {
 	SDLImageLoader loader(this);
 	data->loadFrame(frame, group, loader);
@@ -104,12 +50,11 @@ SDLImageShared::SDLImageShared(const CDefFile * data, size_t frame, size_t group
 	savePalette();
 }
 
-SDLImageShared::SDLImageShared(SDL_Surface * from, int preScaleFactor)
+SDLImageShared::SDLImageShared(SDL_Surface * from)
 	: surf(nullptr),
 	margins(0, 0),
 	fullSize(0, 0),
-	originalPalette(nullptr),
-	preScaleFactor(preScaleFactor)
+	originalPalette(nullptr)
 {
 	surf = from;
 	if (surf == nullptr)
@@ -122,12 +67,11 @@ SDLImageShared::SDLImageShared(SDL_Surface * from, int preScaleFactor)
 	fullSize.y = surf->h;
 }
 
-SDLImageShared::SDLImageShared(const ImagePath & filename, int preScaleFactor)
+SDLImageShared::SDLImageShared(const ImagePath & filename)
 	: surf(nullptr),
 	margins(0, 0),
 	fullSize(0, 0),
-	originalPalette(nullptr),
-	preScaleFactor(preScaleFactor)
+	originalPalette(nullptr)
 {
 	surf = BitmapHandler::loadBitmap(filename);
 
@@ -146,7 +90,6 @@ SDLImageShared::SDLImageShared(const ImagePath & filename, int preScaleFactor)
 	}
 }
 
-
 void SDLImageShared::draw(SDL_Surface * where, SDL_Palette * palette, const Point & dest, const Rect * src, const ColorRGBA & colorMultiplier, uint8_t alpha, EImageBlitMode mode) const
 {
 	if (!surf)
@@ -292,26 +235,20 @@ std::shared_ptr<const ISharedImage> SDLImageShared::scaleInteger(int factor, SDL
 		SDL_SetSurfacePalette(surf, palette);
 
 	SDL_Surface * scaled = nullptr;
-	if(preScaleFactor == factor)
-		return shared_from_this();
-	else if(preScaleFactor == 1)
-	{
-		// dump heuristics to differentiate tileable UI elements from map object / combat assets
-		if (mode == EImageBlitMode::OPAQUE || mode == EImageBlitMode::COLORKEY || mode == EImageBlitMode::SIMPLE)
-			scaled = CSDL_Ext::scaleSurfaceIntegerFactor(surf, factor, EScalingAlgorithm::XBRZ_OPAQUE);
-		else
-			scaled = CSDL_Ext::scaleSurfaceIntegerFactor(surf, factor, EScalingAlgorithm::XBRZ_ALPHA);
-	}
+
+	// dump heuristics to differentiate tileable UI elements from map object / combat assets
+	if (mode == EImageBlitMode::OPAQUE || mode == EImageBlitMode::COLORKEY || mode == EImageBlitMode::SIMPLE)
+		scaled = CSDL_Ext::scaleSurfaceIntegerFactor(surf, factor, EScalingAlgorithm::XBRZ_OPAQUE);
 	else
-		scaled = CSDL_Ext::scaleSurface(surf, (int) round((float)surf->w * factor / preScaleFactor), (int) round((float)surf->h * factor / preScaleFactor));
+		scaled = CSDL_Ext::scaleSurfaceIntegerFactor(surf, factor, EScalingAlgorithm::XBRZ_ALPHA);
 
-	auto ret = std::make_shared<SDLImageShared>(scaled, preScaleFactor);
+	auto ret = std::make_shared<SDLImageShared>(scaled);
 
 	ret->fullSize.x = fullSize.x * factor;
 	ret->fullSize.y = fullSize.y * factor;
 
-	ret->margins.x = (int) round((float)margins.x * factor / preScaleFactor);
-	ret->margins.y = (int) round((float)margins.y * factor / preScaleFactor);
+	ret->margins.x = (int) round((float)margins.x * factor);
+	ret->margins.y = (int) round((float)margins.y * factor);
 	ret->optimizeSurface();
 
 	// erase our own reference
@@ -340,7 +277,7 @@ std::shared_ptr<const ISharedImage> SDLImageShared::scaleTo(const Point & size,
 	else
 		CSDL_Ext::setDefaultColorKey(scaled);//just in case
 
-	auto ret = std::make_shared<SDLImageShared>(scaled, preScaleFactor);
+	auto ret = std::make_shared<SDLImageShared>(scaled);
 
 	ret->fullSize.x = (int) round((float)fullSize.x * scaleX);
 	ret->fullSize.y = (int) round((float)fullSize.y * scaleY);
@@ -369,11 +306,6 @@ void SDLImageShared::exportBitmap(const boost::filesystem::path& path, SDL_Palet
 		SDL_SetSurfacePalette(surf, originalPalette);
 }
 
-void SDLImageIndexed::playerColored(PlayerColor player)
-{
-	graphics->setPlayerPalette(currentPalette, player);
-}
-
 bool SDLImageShared::isTransparent(const Point & coords) const
 {
 	if (surf)
@@ -384,22 +316,21 @@ bool SDLImageShared::isTransparent(const Point & coords) const
 
 Rect SDLImageShared::contentRect() const
 {
-	auto tmpMargins = margins / preScaleFactor;
-	auto tmpSize = Point(surf->w, surf->h) / preScaleFactor;
+	auto tmpMargins = margins;
+	auto tmpSize = Point(surf->w, surf->h);
 	return Rect(tmpMargins, tmpSize);
 }
 
-Point SDLImageShared::dimensions() const
+const SDL_Palette * SDLImageShared::getPalette() const
 {
-	return fullSize / preScaleFactor;
+	if (!surf)
+		return nullptr;
+	return surf->format->palette;
 }
 
-std::shared_ptr<IImage> SDLImageShared::createImageReference(EImageBlitMode mode) const
+Point SDLImageShared::dimensions() const
 {
-	if (surf && surf->format->palette)
-		return std::make_shared<SDLImageIndexed>(shared_from_this(), originalPalette, mode);
-	else
-		return std::make_shared<SDLImageRGB>(shared_from_this(), mode);
+	return fullSize;
 }
 
 std::shared_ptr<const ISharedImage> SDLImageShared::horizontalFlip() const
@@ -408,7 +339,7 @@ std::shared_ptr<const ISharedImage> SDLImageShared::horizontalFlip() const
 		return shared_from_this();
 
 	SDL_Surface * flipped = CSDL_Ext::horizontalFlip(surf);
-	auto ret = std::make_shared<SDLImageShared>(flipped, preScaleFactor);
+	auto ret = std::make_shared<SDLImageShared>(flipped);
 	ret->fullSize = fullSize;
 	ret->margins.x = margins.x;
 	ret->margins.y = fullSize.y - surf->h - margins.y;
@@ -423,7 +354,7 @@ std::shared_ptr<const ISharedImage> SDLImageShared::verticalFlip() const
 		return shared_from_this();
 
 	SDL_Surface * flipped = CSDL_Ext::verticalFlip(surf);
-	auto ret = std::make_shared<SDLImageShared>(flipped, preScaleFactor);
+	auto ret = std::make_shared<SDLImageShared>(flipped);
 	ret->fullSize = fullSize;
 	ret->margins.x = fullSize.x - surf->w - margins.x;
 	ret->margins.y = margins.y;
@@ -445,219 +376,8 @@ void SDLImageShared::savePalette()
 	SDL_SetPaletteColors(originalPalette, surf->format->palette->colors, 0, surf->format->palette->ncolors);
 }
 
-void SDLImageIndexed::shiftPalette(uint32_t firstColorID, uint32_t colorsToMove, uint32_t distanceToMove)
-{
-	std::vector<SDL_Color> shifterColors(colorsToMove);
-
-	for(uint32_t i=0; i<colorsToMove; ++i)
-		shifterColors[(i+distanceToMove)%colorsToMove] = originalPalette->colors[firstColorID + i];
-
-	SDL_SetPaletteColors(currentPalette, shifterColors.data(), firstColorID, colorsToMove);
-}
-
-void SDLImageIndexed::adjustPalette(const ColorFilter & shifter, uint32_t colorsToSkipMask)
-{
-	// If shadow is enabled, following colors must be skipped unconditionally
-	if (blitMode == EImageBlitMode::WITH_SHADOW || blitMode == EImageBlitMode::WITH_SHADOW_AND_OVERLAY)
-		colorsToSkipMask |= (1 << 0) + (1 << 1) + (1 << 4);
-
-	// Note: here we skip first colors in the palette that are predefined in H3 images
-	for(int i = 0; i < currentPalette->ncolors; i++)
-	{
-		if (i < std::size(sourcePalette) && colorsSimilar(sourcePalette[i], originalPalette->colors[i]))
-			continue;
-
-		if(i < std::numeric_limits<uint32_t>::digits && ((colorsToSkipMask >> i) & 1) == 1)
-			continue;
-
-		currentPalette->colors[i] = CSDL_Ext::toSDL(shifter.shiftColor(CSDL_Ext::fromSDL(originalPalette->colors[i])));
-	}
-}
-
-SDLImageIndexed::SDLImageIndexed(const std::shared_ptr<const ISharedImage> & image, SDL_Palette * originalPalette, EImageBlitMode mode)
-	:SDLImageBase::SDLImageBase(image, mode)
-	,originalPalette(originalPalette)
-{
-	currentPalette = SDL_AllocPalette(originalPalette->ncolors);
-	SDL_SetPaletteColors(currentPalette, originalPalette->colors, 0, originalPalette->ncolors);
-
-	preparePalette();
-}
-
-SDLImageIndexed::~SDLImageIndexed()
-{
-	SDL_FreePalette(currentPalette);
-}
-
-void SDLImageIndexed::setShadowTransparency(float factor)
-{
-	ColorRGBA shadow50(0, 0, 0, 128 * factor);
-	ColorRGBA shadow25(0, 0, 0,  64 * factor);
-
-	std::array<SDL_Color, 5> colorsSDL = {
-		originalPalette->colors[0],
-		originalPalette->colors[1],
-		originalPalette->colors[2],
-		originalPalette->colors[3],
-		originalPalette->colors[4]
-	};
-
-	// seems to be used unconditionally
-	colorsSDL[0] = CSDL_Ext::toSDL(Colors::TRANSPARENCY);
-	colorsSDL[1] = CSDL_Ext::toSDL(shadow25);
-	colorsSDL[4] = CSDL_Ext::toSDL(shadow50);
-
-	// seems to be used only if color matches
-	if (colorsSimilar(originalPalette->colors[2], sourcePalette[2]))
-		colorsSDL[2] = CSDL_Ext::toSDL(shadow25);
-
-	if (colorsSimilar(originalPalette->colors[3], sourcePalette[3]))
-		colorsSDL[3] = CSDL_Ext::toSDL(shadow50);
-
-	SDL_SetPaletteColors(currentPalette, colorsSDL.data(), 0, colorsSDL.size());
-}
-
-void SDLImageIndexed::setOverlayColor(const ColorRGBA & color)
-{
-	currentPalette->colors[5] = CSDL_Ext::toSDL(addColors(targetPalette[5], color));
-
-	for (int i : {6,7})
-	{
-		if (colorsSimilar(originalPalette->colors[i], sourcePalette[i]))
-			currentPalette->colors[i] = CSDL_Ext::toSDL(addColors(targetPalette[i], color));
-	}
-}
-
-void SDLImageIndexed::preparePalette()
-{
-	switch(blitMode)
-	{
-		case EImageBlitMode::ONLY_SHADOW:
-		case EImageBlitMode::ONLY_OVERLAY:
-			adjustPalette(ColorFilter::genAlphaShifter(0), 0);
-			break;
-	}
-
-	switch(blitMode)
-	{
-		case EImageBlitMode::SIMPLE:
-		case EImageBlitMode::WITH_SHADOW:
-		case EImageBlitMode::ONLY_SHADOW:
-		case EImageBlitMode::WITH_SHADOW_AND_OVERLAY:
-			setShadowTransparency(1.0);
-			break;
-		case EImageBlitMode::ONLY_BODY:
-		case EImageBlitMode::ONLY_BODY_IGNORE_OVERLAY:
-		case EImageBlitMode::ONLY_OVERLAY:
-			setShadowTransparency(0.0);
-			break;
-	}
-
-	switch(blitMode)
-	{
-		case EImageBlitMode::ONLY_OVERLAY:
-		case EImageBlitMode::WITH_SHADOW_AND_OVERLAY:
-			setOverlayColor(Colors::WHITE_TRUE);
-			break;
-		case EImageBlitMode::ONLY_SHADOW:
-		case EImageBlitMode::ONLY_BODY:
-			setOverlayColor(Colors::TRANSPARENCY);
-			break;
-	}
-}
-
 SDLImageShared::~SDLImageShared()
 {
 	SDL_FreeSurface(surf);
 	SDL_FreePalette(originalPalette);
 }
-
-SDLImageBase::SDLImageBase(const std::shared_ptr<const ISharedImage> & image, EImageBlitMode mode)
-	:image(image)
-	, alphaValue(SDL_ALPHA_OPAQUE)
-	, blitMode(mode)
-{}
-
-std::shared_ptr<const ISharedImage> SDLImageBase::getSharedImage() const
-{
-	return image;
-}
-
-void SDLImageRGB::draw(SDL_Surface * where, const Point & pos, const Rect * src) const
-{
-	image->draw(where, nullptr, pos, src, Colors::WHITE_TRUE, alphaValue, blitMode);
-}
-
-void SDLImageIndexed::draw(SDL_Surface * where, const Point & pos, const Rect * src) const
-{
-	image->draw(where, currentPalette, pos, src, Colors::WHITE_TRUE, alphaValue, blitMode);
-}
-
-void SDLImageIndexed::exportBitmap(const boost::filesystem::path & path) const
-{
-	image->exportBitmap(path, currentPalette);
-}
-
-void SDLImageIndexed::scaleTo(const Point & size)
-{
-	image = image->scaleTo(size, currentPalette);
-}
-
-void SDLImageRGB::scaleTo(const Point & size)
-{
-	image = image->scaleTo(size, nullptr);
-}
-
-void SDLImageIndexed::scaleInteger(int factor)
-{
-	image = image->scaleInteger(factor, currentPalette, blitMode);
-}
-
-void SDLImageRGB::scaleInteger(int factor)
-{
-	image = image->scaleInteger(factor, nullptr, blitMode);
-}
-
-void SDLImageRGB::exportBitmap(const boost::filesystem::path & path) const
-{
-	image->exportBitmap(path, nullptr);
-}
-
-bool SDLImageBase::isTransparent(const Point & coords) const
-{
-	return image->isTransparent(coords);
-}
-
-Rect SDLImageBase::contentRect() const
-{
-	return image->contentRect();
-}
-
-Point SDLImageBase::dimensions() const
-{
-	return image->dimensions();
-}
-
-void SDLImageBase::setAlpha(uint8_t value)
-{
-	alphaValue = value;
-}
-
-void SDLImageBase::setBlitMode(EImageBlitMode mode)
-{
-	blitMode = mode;
-}
-
-void SDLImageRGB::setOverlayColor(const ColorRGBA & color)
-{}
-
-void SDLImageRGB::playerColored(PlayerColor player)
-{}
-
-void SDLImageRGB::shiftPalette(uint32_t firstColorID, uint32_t colorsToMove, uint32_t distanceToMove)
-{}
-
-void SDLImageRGB::adjustPalette(const ColorFilter & shifter, uint32_t colorsToSkipMask)
-{}
-
-

+ 6 - 62
client/renderSDL/SDLImage.h

@@ -35,9 +35,6 @@ class SDLImageShared final : public ISharedImage, public std::enable_shared_from
 	//total size including borders
 	Point fullSize;
 
-	//pre scaled image
-	int preScaleFactor;
-
 	// Keep the original palette, in order to do color switching operation
 	void savePalette();
 
@@ -45,11 +42,11 @@ class SDLImageShared final : public ISharedImage, public std::enable_shared_from
 
 public:
 	//Load image from def file
-	SDLImageShared(const CDefFile *data, size_t frame, size_t group=0, int preScaleFactor=1);
+	SDLImageShared(const CDefFile *data, size_t frame, size_t group=0);
 	//Load from bitmap file
-	SDLImageShared(const ImagePath & filename, int preScaleFactor=1);
+	SDLImageShared(const ImagePath & filename);
 	//Create using existing surface, extraRef will increase refcount on SDL_Surface
-	SDLImageShared(SDL_Surface * from, int preScaleFactor=1);
+	SDLImageShared(SDL_Surface * from);
 	~SDLImageShared();
 
 	void draw(SDL_Surface * where, SDL_Palette * palette, const Point & dest, const Rect * src, const ColorRGBA & colorMultiplier, uint8_t alpha, EImageBlitMode mode) const override;
@@ -58,7 +55,9 @@ public:
 	Point dimensions() const override;
 	bool isTransparent(const Point & coords) const override;
 	Rect contentRect() const override;
-	[[nodiscard]] std::shared_ptr<IImage> createImageReference(EImageBlitMode mode) const override;
+
+	const SDL_Palette * getPalette() const override;
+
 	[[nodiscard]] std::shared_ptr<const ISharedImage> horizontalFlip() const override;
 	[[nodiscard]] std::shared_ptr<const ISharedImage> verticalFlip() const override;
 	[[nodiscard]] std::shared_ptr<const ISharedImage> scaleInteger(int factor, SDL_Palette * palette, EImageBlitMode blitMode) const override;
@@ -66,58 +65,3 @@ public:
 
 	friend class SDLImageLoader;
 };
-
-class SDLImageBase : public IImage, boost::noncopyable
-{
-protected:
-	std::shared_ptr<const ISharedImage> image;
-
-	uint8_t alphaValue;
-	EImageBlitMode blitMode;
-
-public:
-	SDLImageBase(const std::shared_ptr<const ISharedImage> & image, EImageBlitMode mode);
-
-	bool isTransparent(const Point & coords) const override;
-	Rect contentRect() const override;
-	Point dimensions() const override;
-	void setAlpha(uint8_t value) override;
-	void setBlitMode(EImageBlitMode mode) override;
-	std::shared_ptr<const ISharedImage> getSharedImage() const override;
-};
-
-class SDLImageIndexed final : public SDLImageBase
-{
-	SDL_Palette * currentPalette = nullptr;
-	SDL_Palette * originalPalette = nullptr;
-
-	void setShadowTransparency(float factor);
-	void preparePalette();
-public:
-	SDLImageIndexed(const std::shared_ptr<const ISharedImage> & image, SDL_Palette * palette, EImageBlitMode mode);
-	~SDLImageIndexed();
-
-	void draw(SDL_Surface * where, const Point & pos, const Rect * src) const override;
-	void setOverlayColor(const ColorRGBA & color) override;
-	void playerColored(PlayerColor player) override;
-	void shiftPalette(uint32_t firstColorID, uint32_t colorsToMove, uint32_t distanceToMove) override;
-	void adjustPalette(const ColorFilter & shifter, uint32_t colorsToSkipMask) override;
-	void scaleInteger(int factor) override;
-	void scaleTo(const Point & size) override;
-	void exportBitmap(const boost::filesystem::path & path) const override;
-};
-
-class SDLImageRGB final : public SDLImageBase
-{
-public:
-	using SDLImageBase::SDLImageBase;
-
-	void draw(SDL_Surface * where, const Point & pos, const Rect * src) const override;
-	void setOverlayColor(const ColorRGBA & color) override;
-	void playerColored(PlayerColor player) override;
-	void shiftPalette(uint32_t firstColorID, uint32_t colorsToMove, uint32_t distanceToMove) override;
-	void adjustPalette(const ColorFilter & shifter, uint32_t colorsToSkipMask) override;
-	void scaleInteger(int factor) override;
-	void scaleTo(const Point & size) override;
-	void exportBitmap(const boost::filesystem::path & path) const override;
-};

+ 489 - 0
client/renderSDL/ScalableImage.cpp

@@ -0,0 +1,489 @@
+/*
+ * ScalableImage.cpp, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+#include "StdInc.h"
+#include "ScalableImage.h"
+
+#include "SDLImage.h"
+#include "SDL_Extensions.h"
+
+#include "../gui/CGuiHandler.h"
+
+#include "../render/ColorFilter.h"
+#include "../render/Colors.h"
+#include "../render/Graphics.h"
+#include "../render/IRenderHandler.h"
+#include "../render/IScreenHandler.h"
+
+#include "../../lib/constants/EntityIdentifiers.h"
+
+#include <SDL_surface.h>
+
+//First 8 colors in def palette used for transparency
+static constexpr std::array<SDL_Color, 8> sourcePalette = {{
+	{0,   255, 255, SDL_ALPHA_OPAQUE},
+	{255, 150, 255, SDL_ALPHA_OPAQUE},
+	{255, 100, 255, SDL_ALPHA_OPAQUE},
+	{255, 50,  255, SDL_ALPHA_OPAQUE},
+	{255, 0,   255, SDL_ALPHA_OPAQUE},
+	{255, 255, 0,   SDL_ALPHA_OPAQUE},
+	{180, 0,   255, SDL_ALPHA_OPAQUE},
+	{0,   255, 0,   SDL_ALPHA_OPAQUE}
+}};
+
+static constexpr std::array<ColorRGBA, 8> targetPalette = {{
+	{0, 0, 0, 0  }, // 0 - transparency                  ( used in most images )
+	{0, 0, 0, 64 }, // 1 - shadow border                 ( used in battle, adventure map def's )
+	{0, 0, 0, 64 }, // 2 - shadow border                 ( used in fog-of-war def's )
+	{0, 0, 0, 128}, // 3 - shadow body                   ( used in fog-of-war def's )
+	{0, 0, 0, 128}, // 4 - shadow body                   ( used in battle, adventure map def's )
+	{0, 0, 0, 0  }, // 5 - selection / owner flag        ( used in battle, adventure map def's )
+	{0, 0, 0, 128}, // 6 - shadow body   below selection ( used in battle def's )
+	{0, 0, 0, 64 }  // 7 - shadow border below selection ( used in battle def's )
+}};
+
+static ui8 mixChannels(ui8 c1, ui8 c2, ui8 a1, ui8 a2)
+{
+	return c1*a1 / 256 + c2*a2*(255 - a1) / 256 / 256;
+}
+
+static ColorRGBA addColors(const ColorRGBA & base, const ColorRGBA & over)
+{
+	return ColorRGBA(
+		mixChannels(over.r, base.r, over.a, base.a),
+		mixChannels(over.g, base.g, over.a, base.a),
+		mixChannels(over.b, base.b, over.a, base.a),
+		static_cast<ui8>(over.a + base.a * (255 - over.a) / 256)
+		);
+}
+static bool colorsSimilar (const SDL_Color & lhs, const SDL_Color & rhs)
+{
+	// it seems that H3 does not requires exact match to replace colors -> (255, 103, 255) gets interpreted as shadow
+	// exact logic is not clear and requires extensive testing with image editing
+	// potential reason is that H3 uses 16-bit color format (565 RGB bits), meaning that 3 least significant bits are lost in red and blue component
+	static const int threshold = 8;
+
+	int diffR = static_cast<int>(lhs.r) - rhs.r;
+	int diffG = static_cast<int>(lhs.g) - rhs.g;
+	int diffB = static_cast<int>(lhs.b) - rhs.b;
+	int diffA = static_cast<int>(lhs.a) - rhs.a;
+
+	return std::abs(diffR) < threshold && std::abs(diffG) < threshold && std::abs(diffB) < threshold && std::abs(diffA) < threshold;
+}
+
+ScalableImageParameters::ScalableImageParameters(const SDL_Palette * originalPalette, EImageBlitMode blitMode)
+{
+	if (originalPalette)
+	{
+		palette = SDL_AllocPalette(originalPalette->ncolors);
+		SDL_SetPaletteColors(palette, originalPalette->colors, 0, originalPalette->ncolors);
+		preparePalette(originalPalette, blitMode);
+	}
+}
+
+ScalableImageParameters::~ScalableImageParameters()
+{
+	SDL_FreePalette(palette);
+}
+
+void ScalableImageParameters::preparePalette(const SDL_Palette * originalPalette, EImageBlitMode blitMode)
+{
+	switch(blitMode)
+	{
+		case EImageBlitMode::ONLY_SHADOW:
+		case EImageBlitMode::ONLY_OVERLAY:
+			adjustPalette(originalPalette, blitMode, ColorFilter::genAlphaShifter(0), 0);
+			break;
+	}
+
+	switch(blitMode)
+	{
+		case EImageBlitMode::SIMPLE:
+		case EImageBlitMode::WITH_SHADOW:
+		case EImageBlitMode::ONLY_SHADOW:
+		case EImageBlitMode::WITH_SHADOW_AND_OVERLAY:
+			setShadowTransparency(originalPalette, 1.0);
+			break;
+		case EImageBlitMode::ONLY_BODY:
+		case EImageBlitMode::ONLY_BODY_IGNORE_OVERLAY:
+		case EImageBlitMode::ONLY_OVERLAY:
+			setShadowTransparency(originalPalette, 0.0);
+			break;
+	}
+
+	switch(blitMode)
+	{
+		case EImageBlitMode::ONLY_OVERLAY:
+		case EImageBlitMode::WITH_SHADOW_AND_OVERLAY:
+			setOverlayColor(originalPalette, Colors::WHITE_TRUE);
+			break;
+		case EImageBlitMode::ONLY_SHADOW:
+		case EImageBlitMode::ONLY_BODY:
+			setOverlayColor(originalPalette, Colors::TRANSPARENCY);
+			break;
+	}
+}
+
+void ScalableImageParameters::setOverlayColor(const SDL_Palette * originalPalette, const ColorRGBA & color)
+{
+	palette->colors[5] = CSDL_Ext::toSDL(addColors(targetPalette[5], color));
+
+	for (int i : {6,7})
+	{
+		if (colorsSimilar(originalPalette->colors[i], sourcePalette[i]))
+			palette->colors[i] = CSDL_Ext::toSDL(addColors(targetPalette[i], color));
+	}
+}
+
+void ScalableImageParameters::shiftPalette(const SDL_Palette * originalPalette, uint32_t firstColorID, uint32_t colorsToMove, uint32_t distanceToMove)
+{
+	std::vector<SDL_Color> shifterColors(colorsToMove);
+
+	for(uint32_t i=0; i<colorsToMove; ++i)
+		shifterColors[(i+distanceToMove)%colorsToMove] = originalPalette->colors[firstColorID + i];
+
+	SDL_SetPaletteColors(palette, shifterColors.data(), firstColorID, colorsToMove);
+}
+
+void ScalableImageParameters::setShadowTransparency(const SDL_Palette * originalPalette, float factor)
+{
+	ColorRGBA shadow50(0, 0, 0, 128 * factor);
+	ColorRGBA shadow25(0, 0, 0,  64 * factor);
+
+	std::array<SDL_Color, 5> colorsSDL = {
+		originalPalette->colors[0],
+		originalPalette->colors[1],
+		originalPalette->colors[2],
+		originalPalette->colors[3],
+		originalPalette->colors[4]
+	};
+
+	// seems to be used unconditionally
+	colorsSDL[0] = CSDL_Ext::toSDL(Colors::TRANSPARENCY);
+	colorsSDL[1] = CSDL_Ext::toSDL(shadow25);
+	colorsSDL[4] = CSDL_Ext::toSDL(shadow50);
+
+	// seems to be used only if color matches
+	if (colorsSimilar(originalPalette->colors[2], sourcePalette[2]))
+		colorsSDL[2] = CSDL_Ext::toSDL(shadow25);
+
+	if (colorsSimilar(originalPalette->colors[3], sourcePalette[3]))
+		colorsSDL[3] = CSDL_Ext::toSDL(shadow50);
+
+	SDL_SetPaletteColors(palette, colorsSDL.data(), 0, colorsSDL.size());
+}
+
+void ScalableImageParameters::adjustPalette(const SDL_Palette * originalPalette, EImageBlitMode blitMode, const ColorFilter & shifter, uint32_t colorsToSkipMask)
+{
+	// If shadow is enabled, following colors must be skipped unconditionally
+	if (blitMode == EImageBlitMode::WITH_SHADOW || blitMode == EImageBlitMode::WITH_SHADOW_AND_OVERLAY)
+		colorsToSkipMask |= (1 << 0) + (1 << 1) + (1 << 4);
+
+	// Note: here we skip first colors in the palette that are predefined in H3 images
+	for(int i = 0; i < palette->ncolors; i++)
+	{
+		if (i < std::size(sourcePalette) && colorsSimilar(sourcePalette[i], originalPalette->colors[i]))
+			continue;
+
+		if(i < std::numeric_limits<uint32_t>::digits && ((colorsToSkipMask >> i) & 1) == 1)
+			continue;
+
+		palette->colors[i] = CSDL_Ext::toSDL(shifter.shiftColor(CSDL_Ext::fromSDL(originalPalette->colors[i])));
+	}
+}
+
+ScalableImageShared::ScalableImageShared(const SharedImageLocator & locator, const std::shared_ptr<const ISharedImage> & baseImage)
+	:locator(locator)
+{
+	base[0] = baseImage;
+	assert(base[0] != nullptr);
+
+	loadScaledImages(GH.screenHandler().getScalingFactor(), PlayerColor::CANNOT_DETERMINE);
+}
+
+Point ScalableImageShared::dimensions() const
+{
+	return base[0]->dimensions();
+}
+
+void ScalableImageShared::exportBitmap(const boost::filesystem::path & path, const ScalableImageParameters & parameters) const
+{
+	base[0]->exportBitmap(path, parameters.palette);
+}
+
+bool ScalableImageShared::isTransparent(const Point & coords) const
+{
+	return base[0]->isTransparent(coords);
+}
+
+Rect ScalableImageShared::contentRect() const
+{
+	return base[0]->contentRect();
+}
+
+void ScalableImageShared::draw(SDL_Surface * where, const Point & dest, const Rect * src, const ScalableImageParameters & parameters, int scalingFactor)
+{
+	const auto & flipAndDraw = [&](FlippedImages & images, const ColorRGBA & colorMultiplier, uint8_t alphaValue){
+		int index = 0;
+		if (parameters.flipVertical)
+		{
+			if (!images[index|1])
+				images[index|1] = images[index]->verticalFlip();
+
+			index |= 1;
+		}
+
+		if (parameters.flipHorizontal)
+		{
+			if (!images[index|2])
+				images[index|2] = images[index]->horizontalFlip();
+
+			index |= 2;
+		}
+
+		images[index]->draw(where, parameters.palette, dest, src, colorMultiplier, alphaValue, locator.layer);
+	};
+
+	if (scalingFactor == 1)
+	{
+		flipAndDraw(base, parameters.colorMultiplier, parameters.alphaValue);
+	}
+	else
+	{
+		if (scaled.at(scalingFactor).shadow.at(0))
+			flipAndDraw(scaled.at(scalingFactor).shadow, Colors::WHITE_TRUE, parameters.alphaValue);
+
+		if (parameters.player != PlayerColor::CANNOT_DETERMINE)
+		{
+			scaled.at(scalingFactor).playerColored[parameters.player.getNum()]->draw(where, parameters.palette, dest, src, Colors::WHITE_TRUE, parameters.alphaValue, locator.layer);
+		}
+		else
+		{
+			if (scaled.at(scalingFactor).body.at(0))
+				flipAndDraw(scaled.at(scalingFactor).body, parameters.colorMultiplier, parameters.alphaValue);
+		}
+
+		if (scaled.at(scalingFactor).overlay.at(0))
+			flipAndDraw(scaled.at(scalingFactor).overlay, parameters.ovelayColorMultiplier, static_cast<int>(parameters.alphaValue) * parameters.ovelayColorMultiplier.a / 255);
+	}
+}
+
+const SDL_Palette * ScalableImageShared::getPalette() const
+{
+	return base[0]->getPalette();
+}
+
+std::shared_ptr<ScalableImageInstance> ScalableImageShared::createImageReference()
+{
+	return std::make_shared<ScalableImageInstance>(shared_from_this(), locator.layer);
+}
+
+ScalableImageInstance::ScalableImageInstance(const std::shared_ptr<ScalableImageShared> & image, EImageBlitMode blitMode)
+	:image(image)
+	,parameters(image->getPalette(), blitMode)
+	,blitMode(blitMode)
+{
+	assert(image);
+}
+
+void ScalableImageInstance::scaleTo(const Point & size)
+{
+	assert(0);
+}
+
+void ScalableImageInstance::exportBitmap(const boost::filesystem::path & path) const
+{
+	image->exportBitmap(path, parameters);
+}
+
+bool ScalableImageInstance::isTransparent(const Point & coords) const
+{
+	return image->isTransparent(coords);
+}
+
+Rect ScalableImageInstance::contentRect() const
+{
+	return image->contentRect();
+}
+
+Point ScalableImageInstance::dimensions() const
+{
+	return image->dimensions();
+}
+
+void ScalableImageInstance::setAlpha(uint8_t value)
+{
+	parameters.alphaValue = value;
+}
+
+void ScalableImageInstance::draw(SDL_Surface * where, const Point & pos, const Rect * src, int scalingFactor) const
+{
+	image->draw(where, pos, src, parameters, scalingFactor);
+}
+
+void ScalableImageInstance::setOverlayColor(const ColorRGBA & color)
+{
+	parameters.ovelayColorMultiplier = color;
+
+	if (parameters.palette)
+		parameters.setOverlayColor(image->getPalette(), color);
+}
+
+void ScalableImageInstance::playerColored(PlayerColor player)
+{
+	parameters.player = player;
+
+	if (!parameters.palette)
+		parameters.playerColored(player);
+
+	image->preparePlayerColoredImage(player);
+}
+
+void ScalableImageParameters::playerColored(PlayerColor player)
+{
+	graphics->setPlayerPalette(palette, player);
+}
+
+void ScalableImageInstance::shiftPalette(uint32_t firstColorID, uint32_t colorsToMove, uint32_t distanceToMove)
+{
+	if (parameters.palette)
+		parameters.shiftPalette(image->getPalette(),firstColorID, colorsToMove, distanceToMove);
+}
+
+void ScalableImageInstance::adjustPalette(const ColorFilter & shifter, uint32_t colorsToSkipMask)
+{
+	if (parameters.palette)
+		parameters.adjustPalette(image->getPalette(), blitMode, shifter, colorsToSkipMask);
+}
+
+void ScalableImageInstance::horizontalFlip()
+{
+	parameters.flipHorizontal = !parameters.flipHorizontal;
+}
+
+void ScalableImageInstance::verticalFlip()
+{
+	parameters.flipVertical = !parameters.flipVertical;
+}
+
+std::shared_ptr<const ISharedImage> ScalableImageShared::loadOrGenerateImage(EImageBlitMode mode, int8_t scalingFactor, PlayerColor color) const
+{
+	ImageLocator loadingLocator;
+
+	loadingLocator.image = locator.image;
+	loadingLocator.defFile = locator.defFile;
+	loadingLocator.defFrame = locator.defFrame;
+	loadingLocator.defGroup = locator.defGroup;
+	loadingLocator.layer = mode;
+	loadingLocator.scalingFactor = scalingFactor;
+	loadingLocator.playerColored = color;
+
+	// best case - requested image is already available in filesystem
+	auto loadedImage = GH.renderHandler().loadSingleImage(loadingLocator);
+	if (loadedImage)
+		return loadedImage;
+
+	// alternatively, find largest pre-scaled image, load it and rescale to desired scaling
+	Point targetSize = base[0]->dimensions() * scalingFactor;
+	for (int8_t scaling = 4; scaling > 1; --scaling)
+	{
+		loadingLocator.scalingFactor = scaling;
+		auto loadedImage = GH.renderHandler().loadSingleImage(loadingLocator);
+		if (loadedImage)
+			return loadedImage->scaleTo(targetSize, nullptr);
+	}
+
+	// if all else fails - use base (presumably, indexed) image and convert it to desired form
+	ScalableImageParameters parameters(getPalette(), mode);
+	if (color != PlayerColor::CANNOT_DETERMINE)
+		parameters.playerColored(color);
+	return base[0]->scaleInteger(scalingFactor, parameters.palette, mode);
+}
+
+void ScalableImageShared::loadScaledImages(int8_t scalingFactor, PlayerColor color)
+{
+	if (scalingFactor == 1)
+		return; // no op. TODO: consider loading 1x images for mods, as alternative to palette-based animations
+
+	if (scaled[scalingFactor].body[0] == nullptr)
+	{
+		switch(locator.layer)
+		{
+			case EImageBlitMode::OPAQUE:
+			case EImageBlitMode::COLORKEY:
+			case EImageBlitMode::SIMPLE:
+				scaled[scalingFactor].body[0] = loadOrGenerateImage(locator.layer, scalingFactor, PlayerColor::CANNOT_DETERMINE);
+				break;
+
+			case EImageBlitMode::WITH_SHADOW_AND_OVERLAY:
+			case EImageBlitMode::ONLY_BODY:
+				scaled[scalingFactor].body[0] = loadOrGenerateImage(EImageBlitMode::ONLY_BODY, scalingFactor, PlayerColor::CANNOT_DETERMINE);
+				break;
+
+			case EImageBlitMode::WITH_SHADOW:
+			case EImageBlitMode::ONLY_BODY_IGNORE_OVERLAY:
+				scaled[scalingFactor].body[0] = loadOrGenerateImage(EImageBlitMode::ONLY_BODY_IGNORE_OVERLAY, scalingFactor, PlayerColor::CANNOT_DETERMINE);
+				break;
+		}
+	}
+
+	if (color != PlayerColor::CANNOT_DETERMINE && scaled[scalingFactor].playerColored[color.getNum()] == nullptr)
+	{
+		switch(locator.layer)
+		{
+			case EImageBlitMode::OPAQUE:
+			case EImageBlitMode::COLORKEY:
+			case EImageBlitMode::SIMPLE:
+				scaled[scalingFactor].playerColored[color.getNum()] = loadOrGenerateImage(locator.layer, scalingFactor, color);
+				break;
+
+			case EImageBlitMode::WITH_SHADOW_AND_OVERLAY:
+			case EImageBlitMode::ONLY_BODY:
+				scaled[scalingFactor].playerColored[color.getNum()] = loadOrGenerateImage(EImageBlitMode::ONLY_BODY, scalingFactor, color);
+				break;
+
+			case EImageBlitMode::WITH_SHADOW:
+			case EImageBlitMode::ONLY_BODY_IGNORE_OVERLAY:
+				scaled[scalingFactor].playerColored[color.getNum()] = loadOrGenerateImage(EImageBlitMode::ONLY_BODY_IGNORE_OVERLAY, scalingFactor, color);
+				break;
+		}
+	}
+
+	if (scaled[scalingFactor].shadow[0] == nullptr)
+	{
+		switch(locator.layer)
+		{
+			case EImageBlitMode::WITH_SHADOW:
+			case EImageBlitMode::ONLY_SHADOW:
+			case EImageBlitMode::WITH_SHADOW_AND_OVERLAY:
+				scaled[scalingFactor].shadow[0] = loadOrGenerateImage(EImageBlitMode::ONLY_SHADOW, scalingFactor, PlayerColor::CANNOT_DETERMINE);
+				break;
+			default:
+				break;
+		}
+	}
+
+	if (scaled[scalingFactor].overlay[0] == nullptr)
+	{
+		switch(locator.layer)
+		{
+			case EImageBlitMode::ONLY_OVERLAY:
+			case EImageBlitMode::WITH_SHADOW_AND_OVERLAY:
+				scaled[scalingFactor].overlay[0] = loadOrGenerateImage(EImageBlitMode::ONLY_OVERLAY, scalingFactor, PlayerColor::CANNOT_DETERMINE);
+				break;
+			default:
+				break;
+		}
+	}
+}
+
+void ScalableImageShared::preparePlayerColoredImage(PlayerColor color)
+{
+	loadScaledImages(GH.screenHandler().getScalingFactor(), color);
+}

+ 124 - 0
client/renderSDL/ScalableImage.h

@@ -0,0 +1,124 @@
+/*
+ * ScalableImage.h, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+#pragma once
+
+#include "../render/IImage.h"
+#include "../render/ImageLocator.h"
+#include "../render/Colors.h"
+
+#include "../../lib/Color.h"
+
+struct SDL_Palette;
+
+class ScalableImageInstance;
+
+struct ScalableImageParameters : boost::noncopyable
+{
+	SDL_Palette * palette = nullptr;
+
+	ColorRGBA colorMultiplier = Colors::WHITE_TRUE;
+	ColorRGBA ovelayColorMultiplier = Colors::WHITE_TRUE;
+
+	PlayerColor player = PlayerColor::CANNOT_DETERMINE;
+	uint8_t alphaValue = 255;
+
+	bool flipVertical = false;
+	bool flipHorizontal = false;
+
+	ScalableImageParameters(const SDL_Palette * originalPalette, EImageBlitMode blitMode);
+	~ScalableImageParameters();
+
+	void setShadowTransparency(const SDL_Palette * originalPalette, float factor);
+	void shiftPalette(const SDL_Palette * originalPalette, uint32_t firstColorID, uint32_t colorsToMove, uint32_t distanceToMove);
+	void playerColored(PlayerColor player);
+	void setOverlayColor(const SDL_Palette * originalPalette, const ColorRGBA & color);
+	void preparePalette(const SDL_Palette * originalPalette, EImageBlitMode blitMode);
+	void adjustPalette(const SDL_Palette * originalPalette, EImageBlitMode blitMode, const ColorFilter & shifter, uint32_t colorsToSkipMask);
+};
+
+class ScalableImageShared final : public std::enable_shared_from_this<ScalableImageShared>, boost::noncopyable
+{
+	static constexpr int maxScaling = 4;
+	static constexpr int maxFlips = 4;
+
+	using FlippedImages = std::array<std::shared_ptr<const ISharedImage>, maxFlips>;
+	using PlayerColoredImages = std::array<std::shared_ptr<const ISharedImage>, PlayerColor::PLAYER_LIMIT_I>;
+
+	struct ScaledImage
+	{
+		/// Upscaled shadow of our image, may be null
+		FlippedImages shadow;
+
+		/// Upscaled main part of our image, may be null
+		FlippedImages body;
+
+		/// Upscaled overlay (player color, selection highlight) of our image, may be null
+		FlippedImages overlay;
+
+		// player-colored images of this particular scale. These are never flipped in h3
+		PlayerColoredImages playerColored;
+	};
+
+	// 1x image, usually indexed. Array of 4 images for every potential flip
+	FlippedImages base;
+
+	// 1x-4x images. 1x versions are only used if dedicated image is present, othervice palette transform is applied to base image
+	std::array<ScaledImage, maxScaling> scaled;
+
+	const SharedImageLocator locator;
+
+	std::shared_ptr<const ISharedImage> loadOrGenerateImage(EImageBlitMode mode, int8_t scalingFactor, PlayerColor color) const;
+
+	void loadScaledImages(int8_t scalingFactor, PlayerColor color);
+
+public:
+	ScalableImageShared(const SharedImageLocator & locator, const std::shared_ptr<const ISharedImage> & baseImage);
+
+	Point dimensions() const;
+	void exportBitmap(const boost::filesystem::path & path, const ScalableImageParameters & parameters) const;
+	bool isTransparent(const Point & coords) const;
+	Rect contentRect() const;
+	void draw(SDL_Surface * where, const Point & dest, const Rect * src, const ScalableImageParameters & parameters, int scalingFactor);
+
+	const SDL_Palette * getPalette() const;
+
+	std::shared_ptr<ScalableImageInstance> createImageReference();
+
+	void preparePlayerColoredImage(PlayerColor color);
+};
+
+class ScalableImageInstance final : public IImage
+{
+	friend class ScalableImageShared;
+
+	std::shared_ptr<ScalableImageShared> image;
+
+	ScalableImageParameters parameters;
+	EImageBlitMode blitMode;
+
+public:
+	ScalableImageInstance(const std::shared_ptr<ScalableImageShared> & image, EImageBlitMode blitMode);
+
+	void scaleTo(const Point & size) override;
+	void exportBitmap(const boost::filesystem::path & path) const override;
+	bool isTransparent(const Point & coords) const override;
+	Rect contentRect() const override;
+	Point dimensions() const override;
+	void setAlpha(uint8_t value) override;
+	void draw(SDL_Surface * where, const Point & pos, const Rect * src, int scalingFactor) const override;
+	void setOverlayColor(const ColorRGBA & color) override;
+	void playerColored(PlayerColor player) override;
+	void shiftPalette(uint32_t firstColorID, uint32_t colorsToMove, uint32_t distanceToMove) override;
+	void adjustPalette(const ColorFilter & shifter, uint32_t colorsToSkipMask) override;
+
+	void horizontalFlip();
+	void verticalFlip();
+};
+

+ 6 - 2
client/widgets/Images.cpp

@@ -53,8 +53,8 @@ CPicture::CPicture( const ImagePath & bmpname )
 	: CPicture(bmpname, Point(0,0))
 {}
 
-CPicture::CPicture( const ImagePath & bmpname, const Point & position )
-	: bg(GH.renderHandler().loadImage(bmpname, EImageBlitMode::COLORKEY))
+CPicture::CPicture( const ImagePath & bmpname, const Point & position, EImageBlitMode mode )
+	: bg(GH.renderHandler().loadImage(bmpname, mode))
 	, needRefresh(false)
 {
 	pos.x += position.x;
@@ -74,6 +74,10 @@ CPicture::CPicture( const ImagePath & bmpname, const Point & position )
 	addUsedEvents(SHOW_POPUP);
 }
 
+CPicture::CPicture( const ImagePath & bmpname, const Point & position )
+	:CPicture(bmpname, position, EImageBlitMode::COLORKEY)
+{}
+
 CPicture::CPicture(const ImagePath & bmpname, const Rect &SrcRect, int x, int y)
 	: CPicture(bmpname, Point(x,y))
 {

+ 2 - 0
client/widgets/Images.h

@@ -21,6 +21,7 @@ class CAnimImage;
 class CLabel;
 class CAnimation;
 class IImage;
+enum class EImageBlitMode : uint8_t;
 
 // Image class
 class CPicture : public CIntObject
@@ -49,6 +50,7 @@ public:
 
 	/// Loads image from specified file name
 	CPicture(const ImagePath & bmpname);
+	CPicture(const ImagePath & bmpname, const Point & position, EImageBlitMode mode);
 	CPicture(const ImagePath & bmpname, const Point & position);
 	CPicture(const ImagePath & bmpname, int x, int y);
 

+ 1 - 2
client/windows/CCastleInterface.cpp

@@ -570,9 +570,8 @@ CCastleBuildings::CCastleBuildings(const CGTownInstance* Town):
 {
 	OBJECT_CONSTRUCTION;
 
-	background = std::make_shared<CPicture>(town->getTown()->clientInfo.townBackground);
+	background = std::make_shared<CPicture>(town->getTown()->clientInfo.townBackground, Point(0,0), EImageBlitMode::OPAQUE);
 	background->needRefresh = true;
-	background->getSurface()->setBlitMode(EImageBlitMode::OPAQUE);
 	pos.w = background->pos.w;
 	pos.h = background->pos.h;
 

+ 2 - 3
client/windows/CMapOverview.cpp

@@ -139,9 +139,8 @@ std::shared_ptr<CPicture> CMapOverviewWidget::buildDrawMinimap(const JsonNode &
 	double resize = maxSideLengthSrc / maxSideLengthDst;
 	Point newMinimapSize = Point(minimapRect.w / resize, minimapRect.h / resize);
 
-	Canvas canvasScaled = Canvas(Point(rect.w, rect.h), CanvasScalingPolicy::AUTO);
-	canvasScaled.drawScaled(minimaps[id], Point((rect.w - newMinimapSize.x) / 2, (rect.h - newMinimapSize.y) / 2), newMinimapSize);
-	std::shared_ptr<IImage> img = GH.renderHandler().createImage(canvasScaled.getInternalSurface());
+	std::shared_ptr<IImage> img = GH.renderHandler().createImage(minimaps[id].getInternalSurface());
+	img->scaleTo(newMinimapSize);
 
 	return std::make_shared<CPicture>(img, Point(rect.x, rect.y));
 }

+ 6 - 8
client/windows/CWindowObject.cpp

@@ -87,8 +87,7 @@ std::shared_ptr<CPicture> CWindowObject::createBg(const ImagePath & imageName, b
 	if(imageName.empty())
 		return nullptr;
 
-	auto image = std::make_shared<CPicture>(imageName);
-	image->getSurface()->setBlitMode(EImageBlitMode::OPAQUE);
+	auto image = std::make_shared<CPicture>(imageName, Point(0,0), EImageBlitMode::OPAQUE);
 	if(playerColored)
 		image->setPlayerColor(LOCPLINT->playerID);
 	return image;
@@ -116,8 +115,7 @@ void CWindowObject::updateShadow()
 void CWindowObject::setShadow(bool on)
 {
 	//size of shadow
-	int sizeOriginal = 8;
-	int size = sizeOriginal * GH.screenHandler().getScalingFactor();
+	int size = 8;
 
 	if(on == !shadowParts.empty())
 		return;
@@ -182,9 +180,9 @@ void CWindowObject::setShadow(bool on)
 		//FIXME: do something with this points
 		Point shadowStart;
 		if (options & BORDERED)
-			shadowStart = Point(sizeOriginal - 14, sizeOriginal - 14);
+			shadowStart = Point(size - 14, size - 14);
 		else
-			shadowStart = Point(sizeOriginal, sizeOriginal);
+			shadowStart = Point(size, size);
 
 		Point shadowPos;
 		if (options & BORDERED)
@@ -200,8 +198,8 @@ void CWindowObject::setShadow(bool on)
 
 		//create base 8x8 piece of shadow
 		SDL_Surface * shadowCorner = CSDL_Ext::copySurface(shadowCornerTempl);
-		SDL_Surface * shadowBottom = CSDL_Ext::scaleSurface(shadowBottomTempl, (fullsize.x - sizeOriginal) * GH.screenHandler().getScalingFactor(), size);
-		SDL_Surface * shadowRight  = CSDL_Ext::scaleSurface(shadowRightTempl,  size, (fullsize.y - sizeOriginal) * GH.screenHandler().getScalingFactor());
+		SDL_Surface * shadowBottom = CSDL_Ext::scaleSurface(shadowBottomTempl, (fullsize.x - size), size);
+		SDL_Surface * shadowRight  = CSDL_Ext::scaleSurface(shadowRightTempl,  size, (fullsize.y - size));
 
 		blitAlphaCol(shadowBottom, 0);
 		blitAlphaRow(shadowRight, 0);