2023-06-20 04:33:09 +00:00
|
|
|
#include "StarImageProcessing.hpp"
|
|
|
|
#include "StarMatrix3.hpp"
|
|
|
|
#include "StarInterpolation.hpp"
|
|
|
|
#include "StarLexicalCast.hpp"
|
|
|
|
#include "StarColor.hpp"
|
|
|
|
#include "StarImage.hpp"
|
2023-06-25 15:42:18 +00:00
|
|
|
#include "StarStringView.hpp"
|
2023-06-26 07:09:19 +00:00
|
|
|
#include "StarEncode.hpp"
|
2024-05-02 22:53:44 +00:00
|
|
|
#include "StarLogging.hpp"
|
2023-06-20 04:33:09 +00:00
|
|
|
|
|
|
|
namespace Star {
|
|
|
|
|
2024-05-02 22:53:44 +00:00
|
|
|
Image scaleNearest(Image const& srcImage, Vec2F scale) {
|
|
|
|
if (scale[0] < 0.0f || scale[1] < 0.0f) {
|
|
|
|
Logger::warn("Negative scale in scaleNearest({})", scale);
|
|
|
|
scale = scale.piecewiseMax(Vec2F::filled(0.f));
|
|
|
|
}
|
2023-06-20 04:33:09 +00:00
|
|
|
Vec2U srcSize = srcImage.size();
|
|
|
|
Vec2U destSize = Vec2U::round(vmult(Vec2F(srcSize), scale));
|
|
|
|
destSize[0] = max(destSize[0], 1u);
|
|
|
|
destSize[1] = max(destSize[1], 1u);
|
|
|
|
|
|
|
|
Image destImage(destSize, srcImage.pixelFormat());
|
|
|
|
|
|
|
|
for (unsigned y = 0; y < destSize[1]; ++y) {
|
|
|
|
for (unsigned x = 0; x < destSize[0]; ++x)
|
|
|
|
destImage.set({x, y}, srcImage.clamp(Vec2I::round(vdiv(Vec2F(x, y), scale))));
|
|
|
|
}
|
|
|
|
return destImage;
|
|
|
|
}
|
|
|
|
|
2024-05-02 22:53:44 +00:00
|
|
|
Image scaleBilinear(Image const& srcImage, Vec2F scale) {
|
|
|
|
if (scale[0] < 0.0f || scale[1] < 0.0f) {
|
|
|
|
Logger::warn("Negative scale in scaleBilinear({})", scale);
|
|
|
|
scale = scale.piecewiseMax(Vec2F::filled(0.f));
|
|
|
|
}
|
2023-06-20 04:33:09 +00:00
|
|
|
Vec2U srcSize = srcImage.size();
|
|
|
|
Vec2U destSize = Vec2U::round(vmult(Vec2F(srcSize), scale));
|
|
|
|
destSize[0] = max(destSize[0], 1u);
|
|
|
|
destSize[1] = max(destSize[1], 1u);
|
|
|
|
|
|
|
|
Image destImage(destSize, srcImage.pixelFormat());
|
|
|
|
|
|
|
|
for (unsigned y = 0; y < destSize[1]; ++y) {
|
|
|
|
for (unsigned x = 0; x < destSize[0]; ++x) {
|
|
|
|
auto pos = vdiv(Vec2F(x, y), scale);
|
|
|
|
auto ipart = Vec2I::floor(pos);
|
|
|
|
auto fpart = pos - Vec2F(ipart);
|
|
|
|
|
|
|
|
auto result = lerp(fpart[1], lerp(fpart[0], Vec4F(srcImage.clamp(ipart[0], ipart[1])), Vec4F(srcImage.clamp(ipart[0] + 1, ipart[1]))), lerp(fpart[0],
|
|
|
|
Vec4F(srcImage.clamp(ipart[0], ipart[1] + 1)), Vec4F(srcImage.clamp(ipart[0] + 1, ipart[1] + 1))));
|
|
|
|
|
|
|
|
destImage.set({x, y}, Vec4B(result));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return destImage;
|
|
|
|
}
|
|
|
|
|
2024-05-02 22:53:44 +00:00
|
|
|
Image scaleBicubic(Image const& srcImage, Vec2F scale) {
|
|
|
|
if (scale[0] < 0.0f || scale[1] < 0.0f) {
|
|
|
|
Logger::warn("Negative scale in scaleBicubic({})", scale);
|
|
|
|
scale = scale.piecewiseMax(Vec2F::filled(0.f));
|
|
|
|
}
|
2023-06-20 04:33:09 +00:00
|
|
|
Vec2U srcSize = srcImage.size();
|
|
|
|
Vec2U destSize = Vec2U::round(vmult(Vec2F(srcSize), scale));
|
|
|
|
destSize[0] = max(destSize[0], 1u);
|
|
|
|
destSize[1] = max(destSize[1], 1u);
|
|
|
|
|
|
|
|
Image destImage(destSize, srcImage.pixelFormat());
|
|
|
|
|
|
|
|
for (unsigned y = 0; y < destSize[1]; ++y) {
|
|
|
|
for (unsigned x = 0; x < destSize[0]; ++x) {
|
|
|
|
auto pos = vdiv(Vec2F(x, y), scale);
|
|
|
|
auto ipart = Vec2I::floor(pos);
|
|
|
|
auto fpart = pos - Vec2F(ipart);
|
|
|
|
|
|
|
|
Vec4F a = cubic4(fpart[0],
|
|
|
|
Vec4F(srcImage.clamp(ipart[0], ipart[1])),
|
|
|
|
Vec4F(srcImage.clamp(ipart[0] + 1, ipart[1])),
|
|
|
|
Vec4F(srcImage.clamp(ipart[0] + 2, ipart[1])),
|
|
|
|
Vec4F(srcImage.clamp(ipart[0] + 3, ipart[1])));
|
|
|
|
|
|
|
|
Vec4F b = cubic4(fpart[0],
|
|
|
|
Vec4F(srcImage.clamp(ipart[0], ipart[1] + 1)),
|
|
|
|
Vec4F(srcImage.clamp(ipart[0] + 1, ipart[1] + 1)),
|
|
|
|
Vec4F(srcImage.clamp(ipart[0] + 2, ipart[1] + 1)),
|
|
|
|
Vec4F(srcImage.clamp(ipart[0] + 3, ipart[1] + 1)));
|
|
|
|
|
|
|
|
Vec4F c = cubic4(fpart[0],
|
|
|
|
Vec4F(srcImage.clamp(ipart[0], ipart[1] + 2)),
|
|
|
|
Vec4F(srcImage.clamp(ipart[0] + 1, ipart[1] + 2)),
|
|
|
|
Vec4F(srcImage.clamp(ipart[0] + 2, ipart[1] + 2)),
|
|
|
|
Vec4F(srcImage.clamp(ipart[0] + 3, ipart[1] + 2)));
|
|
|
|
|
|
|
|
Vec4F d = cubic4(fpart[0],
|
|
|
|
Vec4F(srcImage.clamp(ipart[0], ipart[1] + 3)),
|
|
|
|
Vec4F(srcImage.clamp(ipart[0] + 1, ipart[1] + 3)),
|
|
|
|
Vec4F(srcImage.clamp(ipart[0] + 2, ipart[1] + 3)),
|
|
|
|
Vec4F(srcImage.clamp(ipart[0] + 3, ipart[1] + 3)));
|
|
|
|
|
|
|
|
auto result = cubic4(fpart[1], a, b, c, d);
|
|
|
|
|
|
|
|
destImage.set({x, y}, Vec4B(
|
|
|
|
clamp(result[0], 0.0f, 255.0f),
|
|
|
|
clamp(result[1], 0.0f, 255.0f),
|
|
|
|
clamp(result[2], 0.0f, 255.0f),
|
|
|
|
clamp(result[3], 0.0f, 255.0f)
|
|
|
|
));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return destImage;
|
|
|
|
}
|
|
|
|
|
|
|
|
StringList colorDirectivesFromConfig(JsonArray const& directives) {
|
|
|
|
List<String> result;
|
|
|
|
|
|
|
|
for (auto entry : directives) {
|
|
|
|
if (entry.type() == Json::Type::String) {
|
|
|
|
result.append(entry.toString());
|
|
|
|
} else if (entry.type() == Json::Type::Object) {
|
|
|
|
result.append(paletteSwapDirectivesFromConfig(entry));
|
|
|
|
} else {
|
|
|
|
throw StarException("Malformed color directives list.");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
String paletteSwapDirectivesFromConfig(Json const& swaps) {
|
|
|
|
ColorReplaceImageOperation paletteSwaps;
|
|
|
|
for (auto const& swap : swaps.iterateObject())
|
|
|
|
paletteSwaps.colorReplaceMap[Color::fromHex(swap.first).toRgba()] = Color::fromHex(swap.second.toString()).toRgba();
|
|
|
|
return "?" + imageOperationToString(paletteSwaps);
|
|
|
|
}
|
|
|
|
|
|
|
|
HueShiftImageOperation HueShiftImageOperation::hueShiftDegrees(float degrees) {
|
|
|
|
return HueShiftImageOperation{degrees / 360.0f};
|
|
|
|
}
|
|
|
|
|
|
|
|
SaturationShiftImageOperation SaturationShiftImageOperation::saturationShift100(float amount) {
|
|
|
|
return SaturationShiftImageOperation{amount / 100.0f};
|
|
|
|
}
|
|
|
|
|
|
|
|
BrightnessMultiplyImageOperation BrightnessMultiplyImageOperation::brightnessMultiply100(float amount) {
|
|
|
|
return BrightnessMultiplyImageOperation{amount / 100.0f + 1.0f};
|
|
|
|
}
|
|
|
|
|
|
|
|
FadeToColorImageOperation::FadeToColorImageOperation(Vec3B color, float amount) {
|
|
|
|
this->color = color;
|
|
|
|
this->amount = amount;
|
|
|
|
|
|
|
|
auto fcl = Color::rgb(color).toLinear();
|
|
|
|
for (int i = 0; i <= 255; ++i) {
|
|
|
|
auto r = Color::rgb(Vec3B(i, i, i)).toLinear().mix(fcl, amount).toSRGB().toRgb();
|
|
|
|
rTable[i] = r[0];
|
|
|
|
gTable[i] = r[1];
|
|
|
|
bTable[i] = r[2];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-06-25 15:42:18 +00:00
|
|
|
ImageOperation imageOperationFromString(StringView string) {
|
2023-06-20 04:33:09 +00:00
|
|
|
try {
|
2023-06-26 07:09:19 +00:00
|
|
|
std::string_view view = string.utf8();
|
2023-06-26 08:05:00 +00:00
|
|
|
//double time = view.size() > 10000 ? Time::monotonicTime() : 0.0;
|
2023-06-26 07:09:19 +00:00
|
|
|
auto firstBitEnd = view.find_first_of("=;");
|
|
|
|
if (view.substr(0, firstBitEnd).compare("replace") == 0 && (firstBitEnd + 1) != view.size()) {
|
|
|
|
//Perform optimized replace parse
|
|
|
|
ColorReplaceImageOperation operation;
|
|
|
|
|
|
|
|
std::string_view bits = view.substr(firstBitEnd + 1);
|
2023-06-26 08:05:00 +00:00
|
|
|
operation.colorReplaceMap.reserve(bits.size() / 8);
|
|
|
|
|
2023-06-26 07:09:19 +00:00
|
|
|
char const* hexPtr = nullptr;
|
|
|
|
unsigned int hexLen = 0;
|
|
|
|
|
|
|
|
char const* ptr = bits.data();
|
|
|
|
char const* end = ptr + bits.size();
|
|
|
|
|
|
|
|
char a[4]{}, b[4]{};
|
|
|
|
bool which = true;
|
|
|
|
|
|
|
|
while (true) {
|
|
|
|
char ch = *ptr;
|
|
|
|
|
|
|
|
if (ch == '=' || ch == ';' || ptr == end) {
|
2023-06-26 11:39:22 +00:00
|
|
|
if (hexLen != 0) {
|
|
|
|
char* c = which ? a : b;
|
|
|
|
|
|
|
|
if (hexLen == 3) {
|
|
|
|
nibbleDecode(hexPtr, 3, c, 4);
|
|
|
|
c[0] |= (c[0] << 4);
|
|
|
|
c[1] |= (c[1] << 4);
|
|
|
|
c[2] |= (c[2] << 4);
|
2024-02-28 17:11:55 +00:00
|
|
|
c[3] = static_cast<char>(255);
|
2023-06-26 11:39:22 +00:00
|
|
|
}
|
|
|
|
else if (hexLen == 4) {
|
|
|
|
nibbleDecode(hexPtr, 4, c, 4);
|
|
|
|
c[0] |= (c[0] << 4);
|
|
|
|
c[1] |= (c[1] << 4);
|
|
|
|
c[2] |= (c[2] << 4);
|
|
|
|
c[3] |= (c[3] << 4);
|
|
|
|
}
|
|
|
|
else if (hexLen == 6) {
|
|
|
|
hexDecode(hexPtr, 6, c, 4);
|
2024-02-28 17:11:55 +00:00
|
|
|
c[3] = static_cast<char>(255);
|
2023-06-26 11:39:22 +00:00
|
|
|
}
|
|
|
|
else if (hexLen == 8) {
|
|
|
|
hexDecode(hexPtr, 8, c, 4);
|
|
|
|
}
|
2023-06-26 17:38:57 +00:00
|
|
|
else if (!which || (ptr != end && ++ptr != end))
|
2024-04-24 23:39:23 +00:00
|
|
|
return ErrorImageOperation{strf("Improper size for hex string '{}'", StringView(hexPtr, hexLen))};
|
|
|
|
else // we're in A of A=B. In vanilla only A=B pairs are evaluated, so only throw an error if B is also there.
|
2024-02-19 17:39:01 +00:00
|
|
|
return operation;
|
2023-06-26 17:38:57 +00:00
|
|
|
|
2024-04-24 23:39:23 +00:00
|
|
|
if (which = !which)
|
2023-06-26 11:39:22 +00:00
|
|
|
operation.colorReplaceMap[*(Vec4B*)&a] = *(Vec4B*)&b;
|
|
|
|
|
|
|
|
hexLen = 0;
|
2023-06-26 07:09:19 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
else if (!hexLen++)
|
|
|
|
hexPtr = ptr;
|
|
|
|
|
|
|
|
if (ptr++ == end)
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
2023-06-26 08:05:00 +00:00
|
|
|
//if (time != 0.0)
|
|
|
|
// Logger::logf(LogLevel::Debug, "Parsed %u long directives to %u replace operations in %fs", view.size(), operation.colorReplaceMap.size(), Time::monotonicTime() - time);
|
2023-06-26 18:48:27 +00:00
|
|
|
return operation;
|
2023-06-26 07:09:19 +00:00
|
|
|
}
|
|
|
|
|
2023-06-25 15:42:18 +00:00
|
|
|
List<StringView> bits;
|
2023-06-26 07:09:19 +00:00
|
|
|
|
2023-06-25 15:42:18 +00:00
|
|
|
string.forEachSplitAnyView("=;", [&](StringView split, size_t, size_t) {
|
2023-06-25 15:51:57 +00:00
|
|
|
if (!split.empty())
|
|
|
|
bits.emplace_back(split);
|
2023-06-25 15:42:18 +00:00
|
|
|
});
|
|
|
|
|
2024-02-29 08:09:10 +00:00
|
|
|
StringView const& type = bits.at(0);
|
2023-06-20 04:33:09 +00:00
|
|
|
|
|
|
|
if (type == "hueshift") {
|
|
|
|
return HueShiftImageOperation::hueShiftDegrees(lexicalCast<float>(bits.at(1)));
|
|
|
|
|
|
|
|
} else if (type == "saturation") {
|
|
|
|
return SaturationShiftImageOperation::saturationShift100(lexicalCast<float>(bits.at(1)));
|
|
|
|
|
|
|
|
} else if (type == "brightness") {
|
|
|
|
return BrightnessMultiplyImageOperation::brightnessMultiply100(lexicalCast<float>(bits.at(1)));
|
|
|
|
|
|
|
|
} else if (type == "fade") {
|
|
|
|
return FadeToColorImageOperation(Color::fromHex(bits.at(1)).toRgb(), lexicalCast<float>(bits.at(2)));
|
|
|
|
|
|
|
|
} else if (type == "scanlines") {
|
|
|
|
return ScanLinesImageOperation{
|
|
|
|
FadeToColorImageOperation(Color::fromHex(bits.at(1)).toRgb(), lexicalCast<float>(bits.at(2))),
|
|
|
|
FadeToColorImageOperation(Color::fromHex(bits.at(3)).toRgb(), lexicalCast<float>(bits.at(4)))};
|
|
|
|
|
|
|
|
} else if (type == "setcolor") {
|
|
|
|
return SetColorImageOperation{Color::fromHex(bits.at(1)).toRgb()};
|
|
|
|
|
|
|
|
} else if (type == "replace") {
|
|
|
|
ColorReplaceImageOperation operation;
|
|
|
|
for (size_t i = 0; i < (bits.size() - 1) / 2; ++i)
|
2023-06-25 08:12:54 +00:00
|
|
|
operation.colorReplaceMap[Color::hexToVec4B(bits[i * 2 + 1])] = Color::hexToVec4B(bits[i * 2 + 2]);
|
2023-06-20 04:33:09 +00:00
|
|
|
|
|
|
|
return operation;
|
|
|
|
|
|
|
|
} else if (type == "addmask" || type == "submask") {
|
|
|
|
AlphaMaskImageOperation operation;
|
|
|
|
if (type == "addmask")
|
|
|
|
operation.mode = AlphaMaskImageOperation::Additive;
|
|
|
|
else
|
|
|
|
operation.mode = AlphaMaskImageOperation::Subtractive;
|
|
|
|
|
2023-06-25 15:42:18 +00:00
|
|
|
operation.maskImages = String(bits.at(1)).split('+');
|
2023-06-20 04:33:09 +00:00
|
|
|
|
|
|
|
if (bits.size() > 2)
|
|
|
|
operation.offset[0] = lexicalCast<int>(bits.at(2));
|
|
|
|
|
|
|
|
if (bits.size() > 3)
|
|
|
|
operation.offset[1] = lexicalCast<int>(bits.at(3));
|
|
|
|
|
|
|
|
return operation;
|
|
|
|
|
|
|
|
} else if (type == "blendmult" || type == "blendscreen") {
|
|
|
|
BlendImageOperation operation;
|
|
|
|
|
|
|
|
if (type == "blendmult")
|
|
|
|
operation.mode = BlendImageOperation::Multiply;
|
|
|
|
else
|
|
|
|
operation.mode = BlendImageOperation::Screen;
|
|
|
|
|
2023-06-25 15:42:18 +00:00
|
|
|
operation.blendImages = String(bits.at(1)).split('+');
|
2023-06-20 04:33:09 +00:00
|
|
|
|
|
|
|
if (bits.size() > 2)
|
|
|
|
operation.offset[0] = lexicalCast<int>(bits.at(2));
|
|
|
|
|
|
|
|
if (bits.size() > 3)
|
|
|
|
operation.offset[1] = lexicalCast<int>(bits.at(3));
|
|
|
|
|
|
|
|
return operation;
|
|
|
|
|
|
|
|
} else if (type == "multiply") {
|
|
|
|
return MultiplyImageOperation{Color::fromHex(bits.at(1)).toRgba()};
|
|
|
|
|
|
|
|
} else if (type == "border" || type == "outline") {
|
|
|
|
BorderImageOperation operation;
|
|
|
|
operation.pixels = lexicalCast<unsigned>(bits.at(1));
|
|
|
|
operation.startColor = Color::fromHex(bits.at(2)).toRgba();
|
|
|
|
if (bits.size() > 3)
|
|
|
|
operation.endColor = Color::fromHex(bits.at(3)).toRgba();
|
|
|
|
else
|
|
|
|
operation.endColor = operation.startColor;
|
|
|
|
operation.outlineOnly = type == "outline";
|
2023-06-21 08:59:15 +00:00
|
|
|
operation.includeTransparent = false; // Currently just here for anti-aliased fonts
|
2023-06-20 04:33:09 +00:00
|
|
|
|
|
|
|
return operation;
|
|
|
|
|
|
|
|
} else if (type == "scalenearest" || type == "scalebilinear" || type == "scalebicubic" || type == "scale") {
|
|
|
|
Vec2F scale;
|
|
|
|
if (bits.size() == 2)
|
|
|
|
scale = Vec2F::filled(lexicalCast<float>(bits.at(1)));
|
|
|
|
else
|
|
|
|
scale = Vec2F(lexicalCast<float>(bits.at(1)), lexicalCast<float>(bits.at(2)));
|
|
|
|
|
|
|
|
ScaleImageOperation::Mode mode;
|
|
|
|
if (type == "scalenearest")
|
|
|
|
mode = ScaleImageOperation::Nearest;
|
|
|
|
else if (type == "scalebicubic")
|
|
|
|
mode = ScaleImageOperation::Bicubic;
|
|
|
|
else
|
|
|
|
mode = ScaleImageOperation::Bilinear;
|
|
|
|
|
|
|
|
return ScaleImageOperation{mode, scale};
|
|
|
|
|
|
|
|
} else if (type == "crop") {
|
|
|
|
return CropImageOperation{RectI(lexicalCast<float>(bits.at(1)), lexicalCast<float>(bits.at(2)),
|
|
|
|
lexicalCast<float>(bits.at(3)), lexicalCast<float>(bits.at(4)))};
|
|
|
|
|
|
|
|
} else if (type == "flipx") {
|
|
|
|
return FlipImageOperation{FlipImageOperation::FlipX};
|
|
|
|
|
|
|
|
} else if (type == "flipy") {
|
|
|
|
return FlipImageOperation{FlipImageOperation::FlipY};
|
|
|
|
|
|
|
|
} else if (type == "flipxy") {
|
|
|
|
return FlipImageOperation{FlipImageOperation::FlipXY};
|
|
|
|
|
|
|
|
} else {
|
2024-04-24 23:39:23 +00:00
|
|
|
return NullImageOperation();
|
2023-06-20 04:33:09 +00:00
|
|
|
}
|
|
|
|
} catch (OutOfRangeException const& e) {
|
2024-04-30 17:35:22 +00:00
|
|
|
return ErrorImageOperation{std::string(e.what())};
|
2023-06-20 04:33:09 +00:00
|
|
|
} catch (BadLexicalCast const& e) {
|
2024-04-30 17:35:22 +00:00
|
|
|
return ErrorImageOperation{std::string(e.what())};
|
2024-04-30 17:29:05 +00:00
|
|
|
} catch (StarException const& e) {
|
2024-04-30 17:35:22 +00:00
|
|
|
return ErrorImageOperation{std::string(e.what())};
|
2023-06-20 04:33:09 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
String imageOperationToString(ImageOperation const& operation) {
|
|
|
|
if (auto op = operation.ptr<HueShiftImageOperation>()) {
|
2023-06-27 10:23:44 +00:00
|
|
|
return strf("hueshift={}", op->hueShiftAmount * 360.0f);
|
2023-06-20 04:33:09 +00:00
|
|
|
} else if (auto op = operation.ptr<SaturationShiftImageOperation>()) {
|
2023-06-27 10:23:44 +00:00
|
|
|
return strf("saturation={}", op->saturationShiftAmount * 100.0f);
|
2023-06-20 04:33:09 +00:00
|
|
|
} else if (auto op = operation.ptr<BrightnessMultiplyImageOperation>()) {
|
2023-06-27 10:23:44 +00:00
|
|
|
return strf("brightness={}", (op->brightnessMultiply - 1.0f) * 100.0f);
|
2023-06-20 04:33:09 +00:00
|
|
|
} else if (auto op = operation.ptr<FadeToColorImageOperation>()) {
|
2023-06-27 10:23:44 +00:00
|
|
|
return strf("fade={}={}", Color::rgb(op->color).toHex(), op->amount);
|
2023-06-20 04:33:09 +00:00
|
|
|
} else if (auto op = operation.ptr<ScanLinesImageOperation>()) {
|
2023-06-27 10:23:44 +00:00
|
|
|
return strf("scanlines={}={}={}={}",
|
2023-06-20 04:33:09 +00:00
|
|
|
Color::rgb(op->fade1.color).toHex(),
|
|
|
|
op->fade1.amount,
|
|
|
|
Color::rgb(op->fade2.color).toHex(),
|
|
|
|
op->fade2.amount);
|
|
|
|
} else if (auto op = operation.ptr<SetColorImageOperation>()) {
|
2023-06-27 10:23:44 +00:00
|
|
|
return strf("setcolor={}", Color::rgb(op->color).toHex());
|
2023-06-20 04:33:09 +00:00
|
|
|
} else if (auto op = operation.ptr<ColorReplaceImageOperation>()) {
|
|
|
|
String str = "replace";
|
|
|
|
for (auto const& pair : op->colorReplaceMap)
|
2023-06-27 10:23:44 +00:00
|
|
|
str += strf(";{}={}", Color::rgba(pair.first).toHex(), Color::rgba(pair.second).toHex());
|
2023-06-20 04:33:09 +00:00
|
|
|
return str;
|
|
|
|
} else if (auto op = operation.ptr<AlphaMaskImageOperation>()) {
|
|
|
|
if (op->mode == AlphaMaskImageOperation::Additive)
|
2023-06-27 10:23:44 +00:00
|
|
|
return strf("addmask={};{};{}", op->maskImages.join("+"), op->offset[0], op->offset[1]);
|
2023-06-20 04:33:09 +00:00
|
|
|
else if (op->mode == AlphaMaskImageOperation::Subtractive)
|
2023-06-27 10:23:44 +00:00
|
|
|
return strf("submask={};{};{}", op->maskImages.join("+"), op->offset[0], op->offset[1]);
|
2023-06-20 04:33:09 +00:00
|
|
|
} else if (auto op = operation.ptr<BlendImageOperation>()) {
|
|
|
|
if (op->mode == BlendImageOperation::Multiply)
|
2023-06-27 10:23:44 +00:00
|
|
|
return strf("blendmult={};{};{}", op->blendImages.join("+"), op->offset[0], op->offset[1]);
|
2023-06-20 04:33:09 +00:00
|
|
|
else if (op->mode == BlendImageOperation::Screen)
|
2023-06-27 10:23:44 +00:00
|
|
|
return strf("blendscreen={};{};{}", op->blendImages.join("+"), op->offset[0], op->offset[1]);
|
2023-06-20 04:33:09 +00:00
|
|
|
} else if (auto op = operation.ptr<MultiplyImageOperation>()) {
|
2023-06-27 10:23:44 +00:00
|
|
|
return strf("multiply={}", Color::rgba(op->color).toHex());
|
2023-06-20 04:33:09 +00:00
|
|
|
} else if (auto op = operation.ptr<BorderImageOperation>()) {
|
|
|
|
if (op->outlineOnly)
|
2023-06-27 10:23:44 +00:00
|
|
|
return strf("outline={};{};{}", op->pixels, Color::rgba(op->startColor).toHex(), Color::rgba(op->endColor).toHex());
|
2023-06-20 04:33:09 +00:00
|
|
|
else
|
2023-06-27 10:23:44 +00:00
|
|
|
return strf("border={};{};{}", op->pixels, Color::rgba(op->startColor).toHex(), Color::rgba(op->endColor).toHex());
|
2023-06-20 04:33:09 +00:00
|
|
|
} else if (auto op = operation.ptr<ScaleImageOperation>()) {
|
|
|
|
if (op->mode == ScaleImageOperation::Nearest)
|
2023-06-27 10:23:44 +00:00
|
|
|
return strf("scalenearest={}", op->scale);
|
2023-06-20 04:33:09 +00:00
|
|
|
else if (op->mode == ScaleImageOperation::Bilinear)
|
2023-06-27 10:23:44 +00:00
|
|
|
return strf("scalebilinear={}", op->scale);
|
2023-06-20 04:33:09 +00:00
|
|
|
else if (op->mode == ScaleImageOperation::Bicubic)
|
2023-06-27 10:23:44 +00:00
|
|
|
return strf("scalebicubic={}", op->scale);
|
2023-06-20 04:33:09 +00:00
|
|
|
} else if (auto op = operation.ptr<CropImageOperation>()) {
|
2023-06-27 10:23:44 +00:00
|
|
|
return strf("crop={};{};{};{}", op->subset.xMin(), op->subset.xMax(), op->subset.yMin(), op->subset.yMax());
|
2023-06-20 04:33:09 +00:00
|
|
|
} else if (auto op = operation.ptr<FlipImageOperation>()) {
|
|
|
|
if (op->mode == FlipImageOperation::FlipX)
|
|
|
|
return "flipx";
|
|
|
|
else if (op->mode == FlipImageOperation::FlipY)
|
|
|
|
return "flipy";
|
|
|
|
else if (op->mode == FlipImageOperation::FlipXY)
|
|
|
|
return "flipxy";
|
|
|
|
}
|
|
|
|
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
|
2023-06-25 15:42:18 +00:00
|
|
|
void parseImageOperations(StringView params, function<void(ImageOperation&&)> outputter) {
|
|
|
|
params.forEachSplitView("?", [&](StringView op, size_t, size_t) {
|
2023-06-24 03:06:13 +00:00
|
|
|
if (!op.empty())
|
|
|
|
outputter(imageOperationFromString(op));
|
2023-06-25 15:42:18 +00:00
|
|
|
});
|
2023-06-24 03:06:13 +00:00
|
|
|
}
|
|
|
|
|
2023-06-25 15:42:18 +00:00
|
|
|
List<ImageOperation> parseImageOperations(StringView params) {
|
2023-06-20 04:33:09 +00:00
|
|
|
List<ImageOperation> operations;
|
2023-06-25 15:42:18 +00:00
|
|
|
params.forEachSplitView("?", [&](StringView op, size_t, size_t) {
|
2023-06-20 04:33:09 +00:00
|
|
|
if (!op.empty())
|
|
|
|
operations.append(imageOperationFromString(op));
|
2023-06-25 15:42:18 +00:00
|
|
|
});
|
2023-06-24 03:06:13 +00:00
|
|
|
|
2023-06-20 04:33:09 +00:00
|
|
|
return operations;
|
|
|
|
}
|
|
|
|
|
|
|
|
String printImageOperations(List<ImageOperation> const& list) {
|
|
|
|
return StringList(list.transformed(imageOperationToString)).join("?");
|
|
|
|
}
|
|
|
|
|
2023-06-24 09:41:52 +00:00
|
|
|
void addImageOperationReferences(ImageOperation const& operation, StringList& out) {
|
|
|
|
if (auto op = operation.ptr<AlphaMaskImageOperation>())
|
|
|
|
out.appendAll(op->maskImages);
|
|
|
|
else if (auto op = operation.ptr<BlendImageOperation>())
|
|
|
|
out.appendAll(op->blendImages);
|
|
|
|
}
|
|
|
|
|
2023-06-20 04:33:09 +00:00
|
|
|
StringList imageOperationReferences(List<ImageOperation> const& operations) {
|
|
|
|
StringList references;
|
2023-06-24 09:41:52 +00:00
|
|
|
for (auto const& operation : operations)
|
|
|
|
addImageOperationReferences(operation, references);
|
2023-06-20 04:33:09 +00:00
|
|
|
return references;
|
|
|
|
}
|
|
|
|
|
2023-06-24 03:06:13 +00:00
|
|
|
void processImageOperation(ImageOperation const& operation, Image& image, ImageReferenceCallback refCallback) {
|
2024-03-24 21:40:02 +00:00
|
|
|
if (image.bytesPerPixel() == 3) {
|
|
|
|
// Convert to an image format that has alpha so certain operations function properly
|
|
|
|
image = image.convert(image.pixelFormat() == PixelFormat::BGR24 ? PixelFormat::BGRA32 : PixelFormat::RGBA32);
|
|
|
|
}
|
2023-06-24 03:06:13 +00:00
|
|
|
if (auto op = operation.ptr<HueShiftImageOperation>()) {
|
|
|
|
image.forEachPixel([&op](unsigned, unsigned, Vec4B& pixel) {
|
|
|
|
if (pixel[3] != 0)
|
|
|
|
pixel = Color::hueShiftVec4B(pixel, op->hueShiftAmount);
|
|
|
|
});
|
|
|
|
} else if (auto op = operation.ptr<SaturationShiftImageOperation>()) {
|
|
|
|
image.forEachPixel([&op](unsigned, unsigned, Vec4B& pixel) {
|
|
|
|
if (pixel[3] != 0) {
|
|
|
|
Color color = Color::rgba(pixel);
|
|
|
|
color.setSaturation(clamp(color.saturation() + op->saturationShiftAmount, 0.0f, 1.0f));
|
|
|
|
pixel = color.toRgba();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
} else if (auto op = operation.ptr<BrightnessMultiplyImageOperation>()) {
|
|
|
|
image.forEachPixel([&op](unsigned, unsigned, Vec4B& pixel) {
|
|
|
|
if (pixel[3] != 0) {
|
|
|
|
Color color = Color::rgba(pixel);
|
|
|
|
color.setValue(clamp(color.value() * op->brightnessMultiply, 0.0f, 1.0f));
|
|
|
|
pixel = color.toRgba();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
} else if (auto op = operation.ptr<FadeToColorImageOperation>()) {
|
|
|
|
image.forEachPixel([&op](unsigned, unsigned, Vec4B& pixel) {
|
|
|
|
pixel[0] = op->rTable[pixel[0]];
|
|
|
|
pixel[1] = op->gTable[pixel[1]];
|
|
|
|
pixel[2] = op->bTable[pixel[2]];
|
|
|
|
});
|
|
|
|
} else if (auto op = operation.ptr<ScanLinesImageOperation>()) {
|
|
|
|
image.forEachPixel([&op](unsigned, unsigned y, Vec4B& pixel) {
|
|
|
|
if (y % 2 == 0) {
|
|
|
|
pixel[0] = op->fade1.rTable[pixel[0]];
|
|
|
|
pixel[1] = op->fade1.gTable[pixel[1]];
|
|
|
|
pixel[2] = op->fade1.bTable[pixel[2]];
|
|
|
|
} else {
|
|
|
|
pixel[0] = op->fade2.rTable[pixel[0]];
|
|
|
|
pixel[1] = op->fade2.gTable[pixel[1]];
|
|
|
|
pixel[2] = op->fade2.bTable[pixel[2]];
|
|
|
|
}
|
|
|
|
});
|
|
|
|
} else if (auto op = operation.ptr<SetColorImageOperation>()) {
|
|
|
|
image.forEachPixel([&op](unsigned, unsigned, Vec4B& pixel) {
|
|
|
|
pixel[0] = op->color[0];
|
|
|
|
pixel[1] = op->color[1];
|
|
|
|
pixel[2] = op->color[2];
|
|
|
|
});
|
|
|
|
} else if (auto op = operation.ptr<ColorReplaceImageOperation>()) {
|
|
|
|
image.forEachPixel([&op](unsigned, unsigned, Vec4B& pixel) {
|
|
|
|
if (auto m = op->colorReplaceMap.maybe(pixel))
|
|
|
|
pixel = *m;
|
|
|
|
});
|
|
|
|
|
|
|
|
} else if (auto op = operation.ptr<AlphaMaskImageOperation>()) {
|
|
|
|
if (op->maskImages.empty())
|
2023-06-24 09:41:52 +00:00
|
|
|
return;
|
2023-06-24 03:06:13 +00:00
|
|
|
|
|
|
|
if (!refCallback)
|
|
|
|
throw StarException("Missing image ref callback during AlphaMaskImageOperation in ImageProcessor::process");
|
|
|
|
|
|
|
|
List<Image const*> maskImages;
|
|
|
|
for (auto const& reference : op->maskImages)
|
|
|
|
maskImages.append(refCallback(reference));
|
|
|
|
|
|
|
|
image.forEachPixel([&op, &maskImages](unsigned x, unsigned y, Vec4B& pixel) {
|
|
|
|
uint8_t maskAlpha = 0;
|
|
|
|
Vec2U pos = Vec2U(Vec2I(x, y) + op->offset);
|
|
|
|
for (auto mask : maskImages) {
|
|
|
|
if (pos[0] < mask->width() && pos[1] < mask->height()) {
|
|
|
|
if (op->mode == AlphaMaskImageOperation::Additive) {
|
|
|
|
// We produce our mask alpha from the maximum alpha of any of
|
|
|
|
// the
|
|
|
|
// mask images.
|
|
|
|
maskAlpha = std::max(maskAlpha, mask->get(pos)[3]);
|
|
|
|
} else if (op->mode == AlphaMaskImageOperation::Subtractive) {
|
|
|
|
// We produce our mask alpha from the minimum alpha of any of
|
|
|
|
// the
|
|
|
|
// mask images.
|
|
|
|
maskAlpha = std::min(maskAlpha, mask->get(pos)[3]);
|
2023-06-20 04:33:09 +00:00
|
|
|
}
|
|
|
|
}
|
2023-06-24 03:06:13 +00:00
|
|
|
}
|
|
|
|
pixel[3] = std::min(pixel[3], maskAlpha);
|
|
|
|
});
|
|
|
|
|
|
|
|
} else if (auto op = operation.ptr<BlendImageOperation>()) {
|
|
|
|
if (op->blendImages.empty())
|
2023-06-24 09:41:52 +00:00
|
|
|
return;
|
2023-06-24 03:06:13 +00:00
|
|
|
|
|
|
|
if (!refCallback)
|
|
|
|
throw StarException("Missing image ref callback during BlendImageOperation in ImageProcessor::process");
|
|
|
|
|
|
|
|
List<Image const*> blendImages;
|
|
|
|
for (auto const& reference : op->blendImages)
|
|
|
|
blendImages.append(refCallback(reference));
|
|
|
|
|
|
|
|
image.forEachPixel([&op, &blendImages](unsigned x, unsigned y, Vec4B& pixel) {
|
|
|
|
Vec2U pos = Vec2U(Vec2I(x, y) + op->offset);
|
|
|
|
Vec4F fpixel = Color::v4bToFloat(pixel);
|
|
|
|
for (auto blend : blendImages) {
|
|
|
|
if (pos[0] < blend->width() && pos[1] < blend->height()) {
|
|
|
|
Vec4F blendPixel = Color::v4bToFloat(blend->get(pos));
|
|
|
|
if (op->mode == BlendImageOperation::Multiply)
|
|
|
|
fpixel = fpixel.piecewiseMultiply(blendPixel);
|
|
|
|
else if (op->mode == BlendImageOperation::Screen)
|
|
|
|
fpixel = Vec4F::filled(1.0f) - (Vec4F::filled(1.0f) - fpixel).piecewiseMultiply(Vec4F::filled(1.0f) - blendPixel);
|
2023-06-20 04:33:09 +00:00
|
|
|
}
|
2023-06-24 03:06:13 +00:00
|
|
|
}
|
|
|
|
pixel = Color::v4fToByte(fpixel);
|
|
|
|
});
|
|
|
|
|
|
|
|
} else if (auto op = operation.ptr<MultiplyImageOperation>()) {
|
|
|
|
image.forEachPixel([&op](unsigned, unsigned, Vec4B& pixel) {
|
|
|
|
pixel = pixel.combine(op->color, [](uint8_t a, uint8_t b) -> uint8_t {
|
|
|
|
return (uint8_t)(((int)a * (int)b) / 255);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
} else if (auto op = operation.ptr<BorderImageOperation>()) {
|
|
|
|
Image borderImage(image.size() + Vec2U::filled(op->pixels * 2), PixelFormat::RGBA32);
|
|
|
|
borderImage.copyInto(Vec2U::filled(op->pixels), image);
|
|
|
|
Vec2I borderImageSize = Vec2I(borderImage.size());
|
|
|
|
|
|
|
|
borderImage.forEachPixel([&op, &image, &borderImageSize](int x, int y, Vec4B& pixel) {
|
|
|
|
int pixels = op->pixels;
|
|
|
|
bool includeTransparent = op->includeTransparent;
|
|
|
|
if (pixel[3] == 0 || (includeTransparent && pixel[3] != 255)) {
|
|
|
|
int dist = std::numeric_limits<int>::max();
|
|
|
|
for (int j = -pixels; j < pixels + 1; j++) {
|
|
|
|
for (int i = -pixels; i < pixels + 1; i++) {
|
|
|
|
if (i + x >= pixels && j + y >= pixels && i + x < borderImageSize[0] - pixels && j + y < borderImageSize[1] - pixels) {
|
|
|
|
Vec4B remotePixel = image.get(i + x - pixels, j + y - pixels);
|
|
|
|
if (remotePixel[3] != 0) {
|
|
|
|
dist = std::min(dist, abs(i) + abs(j));
|
|
|
|
if (dist == 1) // Early out, if dist is 1 it ain't getting shorter
|
|
|
|
break;
|
2023-06-20 04:33:09 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2023-06-24 03:06:13 +00:00
|
|
|
}
|
2023-06-20 04:33:09 +00:00
|
|
|
|
2023-06-24 03:06:13 +00:00
|
|
|
if (dist < std::numeric_limits<int>::max()) {
|
|
|
|
float percent = (dist - 1) / (2.0f * pixels - 1);
|
|
|
|
if (pixel[3] != 0) {
|
2024-04-18 01:54:31 +00:00
|
|
|
Color color = Color::rgba(op->startColor).mix(Color::rgba(op->endColor), percent);
|
2023-06-24 03:06:13 +00:00
|
|
|
if (op->outlineOnly) {
|
|
|
|
float pixelA = byteToFloat(pixel[3]);
|
|
|
|
color.setAlphaF((1.0f - pixelA) * fminf(pixelA, 0.5f) * 2.0f);
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
Color pixelF = Color::rgba(pixel);
|
|
|
|
float pixelA = pixelF.alphaF(), colorA = color.alphaF();
|
|
|
|
colorA += pixelA * (1.0f - colorA);
|
|
|
|
pixelF.convertToLinear(); //Mix in linear color space as it is more perceptually accurate
|
|
|
|
color.convertToLinear();
|
|
|
|
color = color.mix(pixelF, pixelA);
|
|
|
|
color.convertToSRGB();
|
|
|
|
color.setAlphaF(colorA);
|
2023-06-21 08:59:15 +00:00
|
|
|
}
|
2024-04-18 01:54:31 +00:00
|
|
|
pixel = color.toRgba();
|
|
|
|
} else {
|
|
|
|
pixel = Vec4B(Vec4F(op->startColor) * (1 - percent) + Vec4F(op->endColor) * percent);
|
2023-06-20 04:33:09 +00:00
|
|
|
}
|
|
|
|
}
|
2023-06-24 03:06:13 +00:00
|
|
|
} else if (op->outlineOnly) {
|
|
|
|
pixel = Vec4B(0, 0, 0, 0);
|
|
|
|
}
|
|
|
|
});
|
2023-06-20 04:33:09 +00:00
|
|
|
|
2023-06-24 03:06:13 +00:00
|
|
|
image = borderImage;
|
2023-06-20 04:33:09 +00:00
|
|
|
|
2023-06-24 03:06:13 +00:00
|
|
|
} else if (auto op = operation.ptr<ScaleImageOperation>()) {
|
|
|
|
if (op->mode == ScaleImageOperation::Nearest)
|
|
|
|
image = scaleNearest(image, op->scale);
|
|
|
|
else if (op->mode == ScaleImageOperation::Bilinear)
|
|
|
|
image = scaleBilinear(image, op->scale);
|
|
|
|
else if (op->mode == ScaleImageOperation::Bicubic)
|
|
|
|
image = scaleBicubic(image, op->scale);
|
2023-06-20 04:33:09 +00:00
|
|
|
|
2023-06-24 03:06:13 +00:00
|
|
|
} else if (auto op = operation.ptr<CropImageOperation>()) {
|
|
|
|
image = image.subImage(Vec2U(op->subset.min()), Vec2U(op->subset.size()));
|
2023-06-20 04:33:09 +00:00
|
|
|
|
2023-06-24 03:06:13 +00:00
|
|
|
} else if (auto op = operation.ptr<FlipImageOperation>()) {
|
|
|
|
if (op->mode == FlipImageOperation::FlipX || op->mode == FlipImageOperation::FlipXY) {
|
|
|
|
for (size_t y = 0; y < image.height(); ++y) {
|
|
|
|
for (size_t xLeft = 0; xLeft < image.width() / 2; ++xLeft) {
|
|
|
|
size_t xRight = image.width() - 1 - xLeft;
|
2023-06-20 04:33:09 +00:00
|
|
|
|
2023-06-24 03:06:13 +00:00
|
|
|
auto left = image.get(xLeft, y);
|
|
|
|
auto right = image.get(xRight, y);
|
2023-06-20 04:33:09 +00:00
|
|
|
|
2023-06-24 03:06:13 +00:00
|
|
|
image.set(xLeft, y, right);
|
|
|
|
image.set(xRight, y, left);
|
2023-06-20 04:33:09 +00:00
|
|
|
}
|
|
|
|
}
|
2023-06-24 03:06:13 +00:00
|
|
|
}
|
2023-06-20 04:33:09 +00:00
|
|
|
|
2023-06-24 03:06:13 +00:00
|
|
|
if (op->mode == FlipImageOperation::FlipY || op->mode == FlipImageOperation::FlipXY) {
|
|
|
|
for (size_t x = 0; x < image.width(); ++x) {
|
|
|
|
for (size_t yTop = 0; yTop < image.height() / 2; ++yTop) {
|
|
|
|
size_t yBottom = image.height() - 1 - yTop;
|
2023-06-20 04:33:09 +00:00
|
|
|
|
2023-06-24 03:06:13 +00:00
|
|
|
auto top = image.get(x, yTop);
|
|
|
|
auto bottom = image.get(x, yBottom);
|
2023-06-20 04:33:09 +00:00
|
|
|
|
2023-06-24 03:06:13 +00:00
|
|
|
image.set(x, yTop, bottom);
|
|
|
|
image.set(x, yBottom, top);
|
2023-06-20 04:33:09 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2023-06-24 03:06:13 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
Image processImageOperations(List<ImageOperation> const& operations, Image image, ImageReferenceCallback refCallback) {
|
|
|
|
for (auto const& operation : operations)
|
|
|
|
processImageOperation(operation, image, refCallback);
|
2023-06-20 04:33:09 +00:00
|
|
|
|
|
|
|
return image;
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|