#include "Document/ImageAdjustments.h" #include "Document/BrushStroke.h" #include "IO/ProjectStore.h" #include #include #include #include extern "C" { #include "AdjustPixels.h" #include "LevelsPixels.h" } QImage ImageAdjustmentPixels::run(const QImage &image, const std::function &body) { QImage context = BrushRaster::context(image.width(), image.height(), false); { QPainter painter(&context); BrushRaster::draw(image, QRectF(0, 0, image.width(), image.height()), painter); } body(context.bits(), context.width(), context.height(), context.bytesPerLine()); return context; } double ImageAdjustmentPixels::clamp(double value, double low, double high, double fallback) { return std::isfinite(value) ? std::min(high, std::max(low, value)) : fallback; } bool AdjustmentColor::isValid() const { return red >= 0 && red <= 1 && green >= 0 && green <= 1 && blue >= 0 && blue <= 1; } AdjustmentColor AdjustmentColor::clamped() const { using ImageAdjustmentPixels::clamp; return {clamp(red, 0, 1, 0), clamp(green, 0, 1, 0), clamp(blue, 0, 1, 0)}; } bool ExposureSettings::isValid() const { return exposure >= exposureLow && exposure <= exposureHigh && offset >= offsetLow && offset <= offsetHigh && gamma >= gammaLow && gamma <= gammaHigh; } ExposureSettings ExposureSettings::normalized() const { using ImageAdjustmentPixels::clamp; return {clamp(exposure, exposureLow, exposureHigh, 0), clamp(offset, offsetLow, offsetHigh, 0), clamp(gamma, gammaLow, gammaHigh, 1)}; } std::array ExposureSettings::table() const { const double scale = std::pow(2, exposure); std::array result; for (size_t index = 0; index < 256; ++index) { const double encoded = double(index) / 255; double linear = encoded <= 0.04045 ? encoded / 12.92 : std::pow((encoded + 0.055) / 1.055, 2.4); linear = std::pow(std::max(0.0, linear * scale + offset), 1 / gamma); const double output = linear <= 0.0031308 ? linear * 12.92 : 1.055 * std::pow(linear, 1 / 2.4) - 0.055; result[index] = float(std::min(1.0, std::max(0.0, output))); } return result; } QImage ExposureSettings::apply(const QImage &image) const { if (!isValid()) throw ProjectError(ProjectError::Kind::invalid); const std::array curve = table(); std::array tables; for (size_t channel = 0; channel < 3; ++channel) std::copy(curve.begin(), curve.end(), tables.begin() + qsizetype(channel * 256)); return ImageAdjustmentPixels::run(image, [&](uchar *pixels, int width, int height, qsizetype) { levels_apply(pixels, size_t(width) * size_t(height), tables.data()); }); } bool GradientMapSettings::isValid() const { return shadows.isValid() && highlights.isValid(); } GradientMapSettings GradientMapSettings::normalized() const { return {shadows.clamped(), highlights.clamped(), reversed}; } GradientMapSettings::Ends GradientMapSettings::ends() const { return reversed ? Ends{highlights, shadows} : Ends{shadows, highlights}; } QImage GradientMapSettings::apply(const QImage &image) const { if (!isValid()) throw ProjectError(ProjectError::Kind::invalid); const auto [dark, light] = ends(); std::array table; const auto byte = [](double value) { return uint8_t(std::min(255.0, std::max(0.0, std::round(value * 255)))); }; for (size_t index = 0; index < 256; ++index) { const double t = double(index) / 255; table[index * 3] = byte(dark.red + (light.red - dark.red) * t); table[index * 3 + 1] = byte(dark.green + (light.green - dark.green) * t); table[index * 3 + 2] = byte(dark.blue + (light.blue - dark.blue) * t); } return ImageAdjustmentPixels::run(image, [&](uchar *pixels, int width, int height, qsizetype stride) { adjust_gradient_map(pixels, size_t(width), size_t(height), size_t(stride), table.data()); }); } bool GrainSettings::isValid() const { return amount >= amountLow && amount <= amountHigh && size >= sizeLow && size <= sizeHigh && roughness >= roughnessLow && roughness <= roughnessHigh; } GrainSettings GrainSettings::normalized() const { using ImageAdjustmentPixels::clamp; return {clamp(amount, amountLow, amountHigh, 25), clamp(size, sizeLow, sizeHigh, 1.5), clamp(roughness, roughnessLow, roughnessHigh, 50), seed}; } QImage GrainSettings::apply(const QImage &image, QPointF origin, double unitsPerPixel, std::optional seed) const { if (!isValid() || !std::isfinite(unitsPerPixel) || !(unitsPerPixel > 0)) throw ProjectError(ProjectError::Kind::invalid); if (!(amount > 0)) return image; const quint32 pattern = seed.value_or(this->seed); return ImageAdjustmentPixels::run(image, [&](uchar *pixels, int width, int height, qsizetype stride) { adjust_grain(pixels, size_t(width), size_t(height), size_t(stride), amount, size, roughness, pattern, origin.x(), origin.y(), unitsPerPixel); }); } bool BlackWhiteSettings::isValid() const { const std::array weights{reds, yellows, greens, cyans, blues, magentas}; return std::all_of(weights.begin(), weights.end(), [](double weight) { return weight >= low && weight <= high; }) && tintHue >= 0 && tintHue <= 360 && tintSaturation >= 0 && tintSaturation <= 100; } QImage BlackWhiteSettings::apply(const QImage &image) const { if (!isValid()) throw ProjectError(ProjectError::Kind::invalid); // The kernel's order: red, yellow, green, cyan, blue, magenta. const std::array weights{float(reds / 100), float(yellows / 100), float(greens / 100), float(cyans / 100), float(blues / 100), float(magentas / 100)}; return ImageAdjustmentPixels::run(image, [&](uchar *pixels, int width, int height, qsizetype stride) { adjust_black_white(pixels, size_t(width), size_t(height), size_t(stride), weights.data(), tint ? 1 : 0, tintHue, tintSaturation / 100); }); } namespace { std::array amounts(const ColorBalanceSettings &settings) { return {settings.shadowCyanRed, settings.shadowMagentaGreen, settings.shadowYellowBlue, settings.midCyanRed, settings.midMagentaGreen, settings.midYellowBlue, settings.highlightCyanRed, settings.highlightMagentaGreen, settings.highlightYellowBlue}; } } bool ColorBalanceSettings::isValid() const { const std::array all = amounts(*this); return std::all_of(all.begin(), all.end(), [](double amount) { return amount >= low && amount <= high; }); } bool ColorBalanceSettings::isIdentity() const { const std::array all = amounts(*this); return std::all_of(all.begin(), all.end(), [](double amount) { return amount == 0; }); } QImage ColorBalanceSettings::apply(const QImage &image) const { if (!isValid()) throw ProjectError(ProjectError::Kind::invalid); if (isIdentity()) return image; std::array ranges; const std::array all = amounts(*this); std::transform(all.begin(), all.end(), ranges.begin(), [](double amount) { return float(amount / 100); }); return ImageAdjustmentPixels::run(image, [&](uchar *pixels, int width, int height, qsizetype stride) { adjust_color_balance(pixels, size_t(width), size_t(height), size_t(stride), ranges.data(), ranges.data() + 3, ranges.data() + 6, preserveLuminosity ? 1 : 0); }); }