Files
free-file-sync-mirror/wx+/image_resources.cpp
2025-12-10 14:38:26 -08:00

341 lines
12 KiB
C++

// *****************************************************************************
// * This file is part of the FreeFileSync project. It is distributed under *
// * GNU General Public License: https://www.gnu.org/licenses/gpl-3.0 *
// * Copyright (C) Zenju (zenju AT freefilesync DOT org) - All Rights Reserved *
// *****************************************************************************
#include "image_resources.h"
#include <zen/utf.h>
#include <zen/thread.h>
#include <zen/file_io.h>
#include <zen/file_traverser.h>
#include <wx/zipstrm.h>
#include <wx/mstream.h>
#include <xBRZ/src/xbrz.h>
#include <xBRZ/src/xbrz_tools.h>
#include "image_tools.h"
#include "image_holder.h"
#include "dc.h"
using namespace zen;
namespace
{
ImageHolder xbrzScale(int width, int height, const unsigned char* imageRgb, const unsigned char* imageAlpha, int hqScale)
{
assert(imageRgb && imageAlpha && width > 0 && height > 0); //see convertToVanillaImage()
if (width <= 0 || height <= 0)
return ImageHolder(0, 0, true /*withAlpha*/);
const int hqWidth = width * hqScale;
const int hqHeight = height * hqScale;
//get rid of allocation and buffer std::vector<> at thread-level? => no discernable perf improvement
std::vector<uint32_t> buf(hqWidth * hqHeight + width * height);
uint32_t* const argbSrc = buf.data() + hqWidth * hqHeight;
uint32_t* const xbrTrg = buf.data();
//convert RGB (RGB byte order) to ARGB (BGRA byte order)
{
const unsigned char* rgb = imageRgb;
const unsigned char* rgbEnd = rgb + 3 * width * height;
const unsigned char* alpha = imageAlpha;
uint32_t* out = argbSrc;
for (; rgb < rgbEnd; rgb += 3)
*out++ = xbrz::makePixel(*alpha++, rgb[0], rgb[1], rgb[2]);
}
//-----------------------------------------------------
xbrz::scale(hqScale, //size_t factor - valid range: 2 - SCALE_FACTOR_MAX
argbSrc, //const uint32_t* src
xbrTrg, //uint32_t* trg
width, height, //int srcWidth, int srcHeight
xbrz::ColorFormat::argbUnbuffered); //ColorFormat colFmt
//test: total xBRZ scaling time with ARGB: 300ms, ARGB unbuffered: 50ms
//-----------------------------------------------------
//convert BGRA to RGB + alpha
ImageHolder trgImg(hqWidth, hqHeight, true /*withAlpha*/);
std::for_each(xbrTrg, xbrTrg + hqWidth * hqHeight, [rgb = trgImg.getRgb(), alpha = trgImg.getAlpha()](uint32_t col) mutable
{
*alpha++ = xbrz::getAlpha(col);
*rgb++ = xbrz::getRed (col);
*rgb++ = xbrz::getGreen(col);
*rgb++ = xbrz::getBlue (col);
});
return trgImg;
}
auto createScalerTask(const std::string& imageName, const wxImage& img, int hqScale, Protected<std::vector<std::pair<std::string, ImageHolder>>>& protResult)
{
assert(runningOnMainThread());
return [imageName,
width = img.GetWidth(), //
height = img.GetHeight(), //don't call these wxWidgets functions from worker thread
rgb = img.GetData(), //
alpha = img.GetAlpha(), //
hqScale, &protResult]
{
ImageHolder ih = xbrzScale(width, height, rgb, alpha, hqScale);
protResult.access([&](std::vector<std::pair<std::string, ImageHolder>>& result) { result.emplace_back(imageName, std::move(ih)); });
};
}
class HqParallelScaler
{
public:
explicit HqParallelScaler(int hqScale) : hqScale_(hqScale) { assert(hqScale > 1); }
~HqParallelScaler() { threadGroup_ = {}; } //imgKeeper_ must out-live threadGroup!!!
void add(const std::string& imageName, const wxImage& img)
{
assert(runningOnMainThread());
imgKeeper_.push_back(img); //retain (ref-counted) wxImage so that the rgb/alpha pointers remain valid after passed to threads
threadGroup_->run(createScalerTask(imageName, img, hqScale_, protResult_));
}
std::unordered_map<std::string, wxImage> waitAndGetResult()
{
assert(runningOnMainThread());
threadGroup_->wait();
std::unordered_map<std::string, wxImage> output;
protResult_.access([&](std::vector<std::pair<std::string, ImageHolder>>& result)
{
for (auto& [imageName, ih] : result)
{
wxImage img(ih.getWidth(), ih.getHeight(), ih.releaseRgb(), false /*static_data*/); //pass ownership
img.SetAlpha(ih.releaseAlpha(), false /*static_data*/);
output.emplace(imageName, std::move(img));
}
});
return output;
}
private:
const int hqScale_;
std::vector<wxImage> imgKeeper_;
Protected<std::vector<std::pair<std::string, ImageHolder>>> protResult_;
using TaskType = FunctionReturnTypeT<decltype(&createScalerTask)>;
std::optional<ThreadGroup<TaskType>> threadGroup_{ThreadGroup<TaskType>(std::max<int>(std::thread::hardware_concurrency(), 1), Zstr("xBRZ Scaler"))};
//hardware_concurrency() == 0 if "not computable or well defined"
};
//================================================================================================
//================================================================================================
class ImageBuffer
{
public:
explicit ImageBuffer(const Zstring& filePath); //throw FileError
const wxImage& getImage(const std::string& name, int maxWidth /*optional*/, int maxHeight /*optional*/);
private:
ImageBuffer (const ImageBuffer&) = delete;
ImageBuffer& operator=(const ImageBuffer&) = delete;
const wxImage& getRawImage (const std::string& name);
const wxImage& getHqScaledImage(const std::string& name);
std::unordered_map<std::string, wxImage> imagesRaw_;
std::unordered_map<std::string, wxImage> imagesScaled_;
std::optional<HqParallelScaler> hqScaler_;
using OutImageKey = std::tuple<std::string /*name*/, int /*height*/>;
struct OutImageKeyHash
{
size_t operator()(const OutImageKey& imKey) const
{
const auto& [name, height] = imKey;
FNV1aHash<size_t> hash;
for (const char c : name)
hash.add(c);
hash.add(height);
return hash.get();
}
};
std::unordered_map<OutImageKey, wxImage, OutImageKeyHash> imagesOut_;
};
ImageBuffer::ImageBuffer(const Zstring& zipPath) //throw FileError
{
std::vector<std::pair<Zstring /*file name*/, std::string /*byte stream*/>> streams;
try //to load from ZIP first:
{
//wxFFileInputStream/wxZipInputStream loads in junks of 512 bytes => WTF!!! => implement sane file loading:
const std::string rawStream = getFileContent(zipPath, nullptr /*notifyUnbufferedIO*/); //throw FileError
wxMemoryInputStream memStream(rawStream.c_str(), rawStream.size()); //does not take ownership
wxZipInputStream zipStream(memStream, wxConvUTF8);
//do NOT rely on wxConvLocal! On failure shows unhelpful popup "Cannot convert from the charset 'Unknown encoding (-1)'!"
while (const auto& entry = std::unique_ptr<wxZipEntry>(zipStream.GetNextEntry())) //take ownership!
if (std::string stream(entry->GetSize(), '\0');
zipStream.ReadAll(stream.data(), stream.size()))
streams.emplace_back(utfTo<Zstring>(entry->GetName()), std::move(stream));
else
assert(false);
}
catch (FileError&) //fall back to folder: dev build (only!?)
{
const Zstring fallbackFolder = beforeLast(zipPath, Zstr(".zip"), IfNotFoundReturn::none);
if (!itemExists(fallbackFolder)) //throw FileError
throw;
traverseFolder(fallbackFolder, [&](const FileInfo& fi)
{
if (endsWith(fi.fullPath, Zstr(".png")))
{
std::string stream = getFileContent(fi.fullPath, nullptr /*notifyUnbufferedIO*/); //throw FileError
streams.emplace_back(fi.itemName, std::move(stream));
}
}, nullptr, nullptr); //throw FileError
}
//--------------------------------------------------------------------
wxImage::AddHandler(new wxPNGHandler/*ownership passed*/); //activate support for .png files
//do we need xBRZ scaling for high quality DPI images?
const int hqScale = std::clamp(static_cast<int>(std::ceil(getScreenDpiScale())), 1, xbrz::SCALE_FACTOR_MAX);
//even for 125% DPI scaling, "2xBRZ + bilinear downscale" gives a better result than mere "125% bilinear upscale"!
if (hqScale > 1)
hqScaler_.emplace(hqScale);
for (const auto& [fileName, stream] : streams)
if (endsWith(fileName, Zstr(".png")))
{
wxMemoryInputStream wxstream(stream.c_str(), stream.size()); //stream does not take ownership of data
wxImage img(wxstream, wxBITMAP_TYPE_PNG);
assert(img.IsOk());
//end this alpha/no-alpha/mask/wxDC::DrawBitmap/RTL/high-contrast-scheme interoperability nightmare here and now!!!!
//=> there's only one type of wxImage: with alpha channel, no mask!!!
convertToVanillaImage(img);
const std::string imageName = utfTo<std::string>(beforeLast(fileName, Zstr("."), IfNotFoundReturn::none));
imagesRaw_.emplace(imageName, img);
if (hqScaler_)
hqScaler_->add(imageName, img); //scale in parallel!
else
imagesScaled_.emplace(imageName, img);
//wxBitmap::NewFromPNGData(stream.c_str(), stream.size())?
// => Windows: just a (slow!) wrapper for wxBitmap(wxImage())!
}
else
assert(false);
}
const wxImage& ImageBuffer::getRawImage(const std::string& name)
{
if (auto it = imagesRaw_.find(name);
it != imagesRaw_.end())
return it->second;
assert(false);
return wxNullImage;
}
const wxImage& ImageBuffer::getHqScaledImage(const std::string& name)
{
//test: this function is first called about 220ms after ImageBuffer::ImageBuffer() has ended
// => should be enough time to finish xBRZ scaling in parallel (which takes 50ms)
//debug perf: extra 800-1000ms during startup
if (hqScaler_)
{
imagesScaled_ = hqScaler_->waitAndGetResult();
hqScaler_.reset();
}
if (auto it = imagesScaled_.find(name);
it != imagesScaled_.end())
return it->second;
assert(false);
return wxNullImage;
}
const wxImage& ImageBuffer::getImage(const std::string& name, int maxWidth /*optional*/, int maxHeight /*optional*/)
{
const wxImage& rawImg = getRawImage(name);
const wxSize dpiSize(dipToScreen(rawImg.GetWidth ()),
dipToScreen(rawImg.GetHeight()));
int outHeight = dpiSize.y;
if (maxWidth >= 0 && maxWidth < dpiSize.x)
outHeight = numeric::intDivRound(maxWidth * rawImg.GetHeight(), rawImg.GetWidth());
if (maxHeight >= 0 && maxHeight < outHeight)
outHeight = maxHeight;
const OutImageKey imgKey{name, outHeight};
auto it = imagesOut_.find(imgKey);
if (it == imagesOut_.end())
{
if (rawImg.GetHeight() >= outHeight) //=> skip needless xBRZ upscaling
it = imagesOut_.emplace(imgKey, shrinkImage(rawImg, -1 /*maxWidth*/, outHeight)).first;
else if (rawImg.GetHeight() >= 0.9 * outHeight) //almost there: also no need for xBRZ-scale
it = imagesOut_.emplace(imgKey, bilinearScale(rawImg, numeric::intDivRound(outHeight * rawImg.GetWidth(), rawImg.GetHeight()), outHeight)).first;
else //however: for 125% DPI scaling, "2xBRZ + bilinear downscale" gives a better result than mere "125% bilinear upscale"
it = imagesOut_.emplace(imgKey, shrinkImage(getHqScaledImage(name), -1 /*maxWidth*/, outHeight)).first;
}
return it->second;
}
std::optional<ImageBuffer> globalImageBuffer;
}
void zen::imageResourcesInit(const Zstring& zipPath) //throw FileError
{
assert(runningOnMainThread()); //wxWidgets is not thread-safe!
assert(!globalImageBuffer);
globalImageBuffer.emplace(zipPath); //throw FileError
}
void zen::imageResourcesCleanup()
{
assert(runningOnMainThread()); //wxWidgets is not thread-safe!
assert(globalImageBuffer);
globalImageBuffer.reset();
}
const wxImage& zen::loadImage(const std::string& name, int maxWidth /*optional*/, int maxHeight /*optional*/)
{
assert(runningOnMainThread()); //wxWidgets is not thread-safe!
assert(globalImageBuffer);
if (globalImageBuffer)
return globalImageBuffer->getImage(name, maxWidth, maxHeight);
return wxNullImage;
}
const wxImage& zen::loadImage(const std::string& name, int maxSize)
{
return loadImage(name, maxSize, maxSize);
}