diff --git a/rtdata/languages/default b/rtdata/languages/default index fdcab9568..d47179bd9 100644 --- a/rtdata/languages/default +++ b/rtdata/languages/default @@ -1433,6 +1433,24 @@ HISTORY_MSG_FILMNEGATIVE_COLORSPACE;Film negative color space HISTORY_MSG_FILMNEGATIVE_ENABLED;Film Negative HISTORY_MSG_FILMNEGATIVE_REF_SPOT;FN - Reference input HISTORY_MSG_FILMNEGATIVE_VALUES;Film negative values +HISTORY_MSG_FRAMING_ABSOLUTE_HEIGHT;Framing - Border Height +HISTORY_MSG_FRAMING_ABSOLUTE_WIDTH;Framing - Border Width +HISTORY_MSG_FRAMING_ALLOW_UPSCALING;Framing - Upscaling +HISTORY_MSG_FRAMING_ASPECT_RATIO;Framing - Aspect Ratio +HISTORY_MSG_FRAMING_BASIS;Framing - Basis +HISTORY_MSG_FRAMING_BORDER_BLUE;Framing - Border (B) +HISTORY_MSG_FRAMING_BORDER_GREEN;Framing - Border (G) +HISTORY_MSG_FRAMING_BORDER_RED;Framing - Border (R) +HISTORY_MSG_FRAMING_BORDER_SIZE;Framing - Relative Size +HISTORY_MSG_FRAMING_BORDER_SIZE_METHOD;Framing - Border Sizing Method +HISTORY_MSG_FRAMING_ENABLED;Framing +HISTORY_MSG_FRAMING_FRAMED_HEIGHT;Framing - Framed Height +HISTORY_MSG_FRAMING_FRAMED_WIDTH;Framing - Framed Width +HISTORY_MSG_FRAMING_METHOD;Framing - Method +HISTORY_MSG_FRAMING_MIN_HEIGHT;Framing - Min Height +HISTORY_MSG_FRAMING_MIN_SIZE_ENABLED;Framing - Min Size +HISTORY_MSG_FRAMING_MIN_WIDTH;Framing - Min Width +HISTORY_MSG_FRAMING_ORIENTATION;Framing - Orientation HISTORY_MSG_GAMUTMUNSEL;Gamut-Munsell HISTORY_MSG_HISTMATCHING;Auto-matched tone curve HISTORY_MSG_HLBL;Color propagation - blur @@ -1587,8 +1605,8 @@ HISTORY_MSG_TONE_EQUALIZER_ENABLED;Tone equalizer HISTORY_MSG_TONE_EQUALIZER_PIVOT;Tone equalizer - Pivot HISTORY_MSG_TONE_EQUALIZER_REGULARIZATION;Tone equalizer - Regularization HISTORY_MSG_TONE_EQUALIZER_SHOW_COLOR_MAP;Tone equalizer - Tonal map -HISTORY_MSG_TRANS_SCALE;Geometry - Scale HISTORY_MSG_TRANS_METHOD;Geometry - Method +HISTORY_MSG_TRANS_SCALE;Geometry - Scale HISTORY_MSG_WAVBALCHROM;Equalizer chrominance HISTORY_MSG_WAVBALLUM;Equalizer luminance HISTORY_MSG_WAVBL;Blur levels @@ -1864,6 +1882,7 @@ PARTIALPASTE_FLATFIELDBLURTYPE;Flat-field blur type PARTIALPASTE_FLATFIELDCLIPCONTROL;Flat-field clip control PARTIALPASTE_FLATFIELDFILE;Flat-field file PARTIALPASTE_FLATFIELDFROMMETADATA;Flat-field from Metadata +PARTIALPASTE_FRAMING;Framing PARTIALPASTE_GRADIENT;Graduated filter PARTIALPASTE_HSVEQUALIZER;HSV equalizer PARTIALPASTE_ICMSETTINGS;Color management settings @@ -2709,6 +2728,36 @@ TP_FLATFIELD_CLIPCONTROL;Clip control TP_FLATFIELD_CLIPCONTROL_TOOLTIP;Clip control avoids clipped highlights caused by applying the flat field. If there are already clipped highlights before applying the flat field, value 0 is used. TP_FLATFIELD_FROMMETADATA;From Metadata TP_FLATFIELD_LABEL;Flat-Field +TP_FRAMING_ABSOLUTE_HEIGHT;Border Height +TP_FRAMING_ABSOLUTE_WIDTH;Border Width +TP_FRAMING_ALLOW_UPSCALING;Allow upscaling to frame +TP_FRAMING_ASPECT_RATIO;Aspect Ratio: +TP_FRAMING_BASIS;Basis +TP_FRAMING_BASIS_AUTO;Auto +TP_FRAMING_BASIS_HEIGHT;Height +TP_FRAMING_BASIS_LONG_SIDE;Long Edge +TP_FRAMING_BASIS_SHORT_SIDE;Short Edge +TP_FRAMING_BASIS_WIDTH;Width +TP_FRAMING_BLUE;Blue +TP_FRAMING_BORDER_COLOR;Border Color +TP_FRAMING_BORDER_SIZE;Size +TP_FRAMING_BORDER_SIZE_ABSOLUTE;Absolute +TP_FRAMING_BORDER_SIZE_METHOD;Sizing: +TP_FRAMING_BORDER_SIZE_RELATIVE;Relative +TP_FRAMING_BORDER_SIZE_UNIFORM_RELATIVE;Uniform Relative +TP_FRAMING_FRAMED_HEIGHT;Framed Height +TP_FRAMING_FRAMED_WIDTH;Framed Width +TP_FRAMING_GREEN;Green +TP_FRAMING_LABEL;Framing +TP_FRAMING_LIMIT_MINIMUM;Limit minimum size +TP_FRAMING_METHOD;Method: +TP_FRAMING_METHOD_BBOX;Bounding Box +TP_FRAMING_METHOD_FIXED;Fixed Frame +TP_FRAMING_METHOD_STANDARD;Standard +TP_FRAMING_MIN_HEIGHT;Minimum Height +TP_FRAMING_MIN_WIDTH;Minimum Width +TP_FRAMING_ORIENTATION;Orientation: +TP_FRAMING_RED;Red TP_GENERAL_11SCALE_TOOLTIP;The effects of this tool are only visible or only accurate at a preview scale of 1:1. TP_GRADIENT_CENTER;Center TP_GRADIENT_CENTER_X;Center X @@ -2876,11 +2925,11 @@ TP_LABCURVE_LCREDSK_TOOLTIP;If enabled, the LC Curve affects only red and skin-t TP_LABCURVE_RSTPROTECTION;Red and skin-tones protection TP_LABCURVE_RSTPRO_TOOLTIP;Works on the Chromaticity slider and the CC curve. TP_LENSGEOM_AUTOCROP;Auto-Crop -TP_LENSGEOM_SCALE;Scale TP_LENSGEOM_FILL;Auto-fill TP_LENSGEOM_LABEL;Lens / Geometry TP_LENSGEOM_LIN;Linear TP_LENSGEOM_LOG;Logarithmic +TP_LENSGEOM_SCALE;Scale TP_LENSPROFILE_CORRECTION_AUTOMATCH;Automatically selected TP_LENSPROFILE_CORRECTION_LCPFILE;LCP file TP_LENSPROFILE_CORRECTION_MANUAL;Manually selected @@ -3908,7 +3957,7 @@ TP_RESIZE_FITBOX;Bounding Box TP_RESIZE_FULLIMAGE;Full Image TP_RESIZE_H;Height: TP_RESIZE_HEIGHT;Height -TP_RESIZE_LABEL;Resize +TP_RESIZE_LABEL;Resize & Framing TP_RESIZE_LANCZOS;Lanczos TP_RESIZE_LE;Long Edge: TP_RESIZE_LONG;Long Edge diff --git a/rtengine/improcfun.h b/rtengine/improcfun.h index 6b15ac4d7..6200f177d 100644 --- a/rtengine/improcfun.h +++ b/rtengine/improcfun.h @@ -213,12 +213,33 @@ enum class BlurType { void sharpening(LabImage* lab, const procparams::SharpeningParams &sharpenParam, bool showMask = false); void sharpeningcam(CieImage* ncie, float** buffer, bool showMask = false); void transform(Imagefloat* original, Imagefloat* transformed, int cx, int cy, int sx, int sy, int oW, int oH, int fW, int fH, const FramesMetaData *metadata, int rawRotationDeg, bool fullImage, bool useOriginalBuffer = false); - float resizeScale(const procparams::ProcParams* params, int fw, int fh, int &imw, int &imh); void lab2monitorRgb(LabImage* lab, Image8* image); + + double resizeScale(const procparams::ProcParams* params, int fw, int fh, int &imw, int &imh); void resize(Imagefloat* src, Imagefloat* dst, float dScale); void Lanczos(const LabImage* src, LabImage* dst, float scale); void Lanczos(const Imagefloat* src, Imagefloat* dst, float scale); + struct FramingArgs { + const procparams::ProcParams* params = nullptr; + int cropWidth = 0; + int cropHeight = 0; + int resizeWidth = 0; + int resizeHeight = 0; + double resizeScale = 1.0f; + }; + struct FramingData { + bool enabled = false; + int imgWidth = 0; + int imgHeight = 0; + double scale = 1.0; + int framedWidth = 0; + int framedHeight = 0; + }; + FramingData framing(const FramingArgs& args) const; + Imagefloat* drawFrame(Imagefloat* rgb, const procparams::FramingParams& params, + const FramingData& dims) const; + void deconvsharpening(float** luminance, float** buffer, const float* const * blend, int W, int H, const procparams::SharpeningParams &sharpenParam, double Scale); void deconvsharpeningloc(float** luminance, float** buffer, int W, int H, float** loctemp, int damp, double radi, int ite, int amo, int contrast, double blurrad, int sk); diff --git a/rtengine/ipresize.cc b/rtengine/ipresize.cc index b6bdc5ede..f5d310b6a 100644 --- a/rtengine/ipresize.cc +++ b/rtengine/ipresize.cc @@ -20,6 +20,7 @@ #include "improcfun.h" #include "alignedbuffer.h" +#include "color.h" #include "imagefloat.h" #include "labimage.h" #include "opthelper.h" @@ -33,6 +34,678 @@ # include #endif +namespace +{ + +using ProcParams = rtengine::procparams::ProcParams; +using FramingParams = rtengine::procparams::FramingParams; + +using Basis = FramingParams::Basis; +using BorderSizing = FramingParams::BorderSizing; +using FramingMethod = FramingParams::FramingMethod; + +enum class Orientation { LANDSCAPE, PORTRAIT }; +enum class Side { WIDTH, HEIGHT }; + +struct Dimensions +{ + double width; + double height; + + Dimensions() : width(0), height(0) {} + Dimensions(double w, double h) : width(w), height(h) {} + + bool isDegenerate() const { return width <= 0.0 || height <= 0.0; } + + double aspectRatio() const { + if (isDegenerate()) return 1.0; + else return static_cast(width) / static_cast(height); + } + + Orientation orient() const { + return width >= height ? Orientation::LANDSCAPE : Orientation::PORTRAIT; + } + + bool inside(const Dimensions& other) const { + return width <= other.width && height <= other.height; + } + + void rotate(Orientation newOrient) { + if (newOrient != orient()) { + std::swap(width, height); + } + } + + bool operator==(const Dimensions& other) const { + return width == other.width && height == other.height; + } + bool operator!=(const Dimensions& other) const { return !(*this == other); } + + Dimensions intersect(const Dimensions& other) const { + return Dimensions(std::min(width, other.width), std::min(height, other.height)); + } + + void debug(const char* prefix) const { + printf("%s w=%f h=%f ar=%f\n", prefix, width, height, aspectRatio()); + } +}; + +struct ResizeArgs +{ + Dimensions size; + double scale = 1.0; + + ResizeArgs(const Dimensions& aSize, double aScale) : size(aSize), scale(aScale) {} +}; + +class Framing +{ +public: + Framing(const ProcParams& params, int fullWidth, int fullHeight); + + ResizeArgs adjustResizeForFraming(const ResizeArgs& resize) const; + Dimensions computeFramedSize(const Dimensions& imgSize) const; + +private: + Dimensions clampResize(const Dimensions& imgSize, const Dimensions& bounds) const; + ResizeArgs adjustResize(const ResizeArgs& resize, const Dimensions& newSize) const; + Dimensions computeRelativeImageBBoxInFrame(const Dimensions& imgSize, + const Dimensions& framedSize) const; + Dimensions computeUniformRelativeImageBBox(const Dimensions& imgSize, + const Dimensions& framedSize) const; + ResizeArgs resizeForFixedFrame(const ResizeArgs& resize) const; + ResizeArgs resizeForBBox(const ResizeArgs& resize) const; + Dimensions computeSizeWithBorders(const Dimensions& imgSize) const; + + const ProcParams& allParams; + const FramingParams framing; // Make a copy to sanitize inputs + const Dimensions postCropImageSize; + const Dimensions maxUpscalingBBox; +}; + +// Downscaling limit is 32x32px +constexpr double MIN_DOWNSCALE_PX = 32.0; +// Upscaling limit is 16x image size +constexpr double MAX_UPSCALE_FACTOR = 16.0; + +int computeSize(int dim, double scale) +{ + return static_cast(static_cast(dim) * scale + 0.5); +} + +std::pair computeImgAndBorderSize(double frameSize, double scale) +{ + // frame_len = img_len + 2 * scale * img_len = (1 + 2 * scale) * img_len + double imgFrameScale = (1.0 + 2.0 * scale); + double imgSize = frameSize / imgFrameScale; + double borderSize = scale * imgSize; + + return {imgSize, borderSize}; +} + +Orientation orient(const FramingParams& params, const Dimensions& imgSize) +{ + switch (params.orientation) { + case FramingParams::Orientation::LANDSCAPE: + return Orientation::LANDSCAPE; + case FramingParams::Orientation::PORTRAIT: + return Orientation::PORTRAIT; + case FramingParams::Orientation::AS_IMAGE: + default: + return imgSize.orient(); + } +} + +double flipAspectRatioByOrientation(double aspectRatio, Orientation orient) +{ + switch (orient) { + case Orientation::LANDSCAPE: + return aspectRatio >= 1.0 ? aspectRatio : 1.0 / aspectRatio; + case Orientation::PORTRAIT: + return aspectRatio <= 1.0 ? aspectRatio : 1.0 / aspectRatio; + default: + return aspectRatio; + } +} + +Side autoPickBasis(const FramingParams& params, const Dimensions& imgSize) +{ + if (imgSize.isDegenerate()) { + if (imgSize.width <= 0) return Side::HEIGHT; + else return Side::WIDTH; + } + + Orientation imgOrient = imgSize.orient(); + double imgAspectRatio = imgSize.aspectRatio(); + Orientation frameOrient = orient(params, imgSize); + double frameAspectRatio = flipAspectRatioByOrientation(params.aspectRatio, frameOrient); + + if (frameOrient == imgOrient) { + // Pick the more constrained side (i.e. hits 0 border width first) + return imgAspectRatio >= frameAspectRatio ? Side::WIDTH : Side::HEIGHT; + } else if (imgOrient == Orientation::LANDSCAPE) { + // Image in landscape, frame in portrait + return Side::WIDTH; + } else { + // Image in portrait, frame in landscape + return Side::HEIGHT; + } +} + +Side pickReferenceSide(const FramingParams& params, const Dimensions& imgSize) +{ + switch (params.basis) { + case Basis::WIDTH: + return Side::WIDTH; + case Basis::HEIGHT: + return Side::HEIGHT; + case Basis::LONG: + return imgSize.width >= imgSize.height ? Side::WIDTH : Side::HEIGHT; + case Basis::SHORT: + return imgSize.width <= imgSize.height ? Side::WIDTH : Side::HEIGHT; + case Basis::AUTO: + default: + return autoPickBasis(params, imgSize); + } +} + +constexpr bool INSIDE_BBOX = true; +constexpr bool OUTSIDE_BBOX = false; + +Dimensions clampToBBox(const Dimensions& img, const Dimensions& bbox, bool clampInside) +{ + double widthScale = 1.0; + double heightScale = 1.0; + if (bbox.width > 0) { + widthScale = img.width / bbox.width; + } + if (bbox.height > 0) { + heightScale = img.height / bbox.height; + } + + Dimensions newSize = img; + + if (clampInside) { + // If a side exceeds the bbox, scale down to bbox + double scale = std::max(widthScale, heightScale); + if (scale > 1.0) { + if (widthScale >= heightScale) { + newSize.width = bbox.width; + newSize.height = img.height / widthScale; + } else { + newSize.width = img.width / heightScale; + newSize.height = bbox.height; + } + } + } else { + // If a side is within the bbox, scale up to bbox + double scale = std::min(widthScale, heightScale); + if (scale < 1.0) { + if (widthScale <= heightScale) { + newSize.width = bbox.width; + newSize.height = img.height / widthScale; + } else { + newSize.width = img.width / heightScale; + newSize.height = bbox.height; + } + } + } + + return newSize; +} + +Dimensions downscaleToTouchBBox(const Dimensions& img, const Dimensions& bbox) +{ + if (bbox.isDegenerate()) return Dimensions(0, 0); + if (!bbox.inside(img)) return img; + + double widthScale = img.width / bbox.width; + double heightScale = img.height / bbox.height; + + Dimensions downscaled; + if (widthScale <= heightScale) { + downscaled.width = bbox.width; + downscaled.height = img.height / widthScale; + } else { + downscaled.height = bbox.height; + downscaled.width = img.width / heightScale; + } + return downscaled; +} + +Dimensions upscaleToBBox(const Dimensions& img, const Dimensions& bbox) +{ + if (bbox.isDegenerate()) return Dimensions(0, 0); + if (!img.inside(bbox)) return img; + + double widthScale = img.width / bbox.width; + double heightScale = img.height / bbox.height; + + Dimensions upscaled; + if (widthScale >= heightScale) { + upscaled.width = bbox.width; + upscaled.height = img.height / widthScale; + } else { + upscaled.height = bbox.height; + upscaled.width = img.width / heightScale; + } + + return upscaled; +} + +double orientAspectRatio(const FramingParams& framing, const Dimensions& imgSize) +{ + double aspectRatio = framing.aspectRatio; + if (aspectRatio == FramingParams::AS_IMAGE_ASPECT_RATIO) { + aspectRatio = imgSize.aspectRatio(); + } + + Orientation borderOrient = orient(framing, imgSize); + if ((borderOrient == Orientation::PORTRAIT && aspectRatio > 1.0) || + (borderOrient == Orientation::LANDSCAPE && aspectRatio < 1.0)) { + aspectRatio = 1.0 / aspectRatio; + } + return aspectRatio; +} + +Dimensions fromAspectRatio(const Dimensions& size, double aspectRatio) +{ + Dimensions result; + if (aspectRatio >= 1.0) { + result.height = size.height; + result.width = result.height * aspectRatio; + } else { + result.width = size.width; + result.height = result.width / aspectRatio; + } + return result; +} + +FramingParams sanitize(const FramingParams& dirty) +{ + FramingParams framing = dirty; + framing.framedWidth = std::max(static_cast(MIN_DOWNSCALE_PX), framing.framedWidth); + framing.framedHeight = std::max(static_cast(MIN_DOWNSCALE_PX), framing.framedHeight); + framing.relativeBorderSize = std::max(0.0, std::min(1.0, framing.relativeBorderSize)); + framing.minWidth = std::max(0, framing.minWidth); + framing.minHeight = std::max(0, framing.minHeight); + framing.absWidth = std::max(0, framing.absWidth); + framing.absHeight = std::max(0, framing.absHeight); + return framing; +} + +Framing::Framing(const ProcParams& params, int fullWidth, int fullHeight) : + allParams(params), + framing(sanitize(params.framing)), + postCropImageSize(params.crop.enabled ? + Dimensions(params.crop.w, params.crop.h) : + Dimensions(fullWidth, fullHeight)), + maxUpscalingBBox(Dimensions( + computeSize(postCropImageSize.width, MAX_UPSCALE_FACTOR), + computeSize(postCropImageSize.height, MAX_UPSCALE_FACTOR))) +{ +} + +Dimensions Framing::clampResize(const Dimensions& imgSize, const Dimensions& bounds) const +{ + // Don't adjust above upscaling limit. + // + // If the upscaling limit is contained inside the target bounds, scale + // down the bounds to outside the upscaling limit. This is needed since + // scaling the bounds to fit inside the upscaling bbox may artificially + // reduce the upscaling limit due to aspect ratio differences. + Dimensions clampedBounds = maxUpscalingBBox.inside(bounds) ? + downscaleToTouchBBox(bounds, maxUpscalingBBox) : + clampToBBox(bounds, maxUpscalingBBox, INSIDE_BBOX); + + if (!imgSize.inside(clampedBounds)) { + // Downscale large images to fit inside bounds (only if above limit) + + Dimensions minSize(MIN_DOWNSCALE_PX, MIN_DOWNSCALE_PX); + if (!minSize.inside(imgSize)) { + // Skip images below downscaling limit + return imgSize; + } else if (!minSize.inside(clampedBounds)) { + // Go as small as possible without exceeding downscaling limit + return downscaleToTouchBBox(imgSize, minSize); + } else { + // Downscale large images to fit inside bounds + return clampToBBox(imgSize, clampedBounds, INSIDE_BBOX); + } + } else { + // Consider upscaling... + if (!framing.allowUpscaling || + imgSize.width == clampedBounds.width || + imgSize.height == clampedBounds.height) { + return imgSize; + } else { + return upscaleToBBox(imgSize, clampedBounds); + } + } +} + +ResizeArgs Framing::adjustResize(const ResizeArgs& resize, const Dimensions& bbox) const +{ + Dimensions newSize = clampResize(resize.size, bbox); + double newScale = newSize.width / postCropImageSize.width; + return ResizeArgs(newSize, newScale); +} + +Dimensions Framing::computeRelativeImageBBoxInFrame(const Dimensions& imgSize, + const Dimensions& framedSize) const +{ + if (imgSize.isDegenerate() || framedSize.isDegenerate()) { + return Dimensions(0, 0); + } + + double imgAspectRatio = imgSize.aspectRatio(); + // Compute the width:height ratio of the border size for the requested + // image size and framed size. + // + // We do this by creating a dummy image. Then, scale the framed size to be + // larger than the dummy image such that there is a non-zero difference for + // widths and heights. + double borderAspectRatio = [&]() + { + Dimensions fakeImage = fromAspectRatio(framedSize, imgAspectRatio); + Dimensions bigFrame = clampToBBox(framedSize, fakeImage, OUTSIDE_BBOX); + bigFrame.width *= 2.0; + bigFrame.height *= 2.0; + + Dimensions diff(bigFrame.width - fakeImage.width, bigFrame.height - fakeImage.height); + return diff.aspectRatio(); + }(); + + Side side = pickReferenceSide(framing, imgSize); + double scale = framing.relativeBorderSize; + + // Compute image and border lengths on basis side + double frameBasis = side == Side::WIDTH ? framedSize.width : framedSize.height; + double frameOther = side == Side::WIDTH ? framedSize.height : framedSize.width; + + auto computedSizes = computeImgAndBorderSize(frameBasis, scale); + double imgBasis = computedSizes.first; + double borderBasis = computedSizes.second; + + // Compute image and border lengths for the non-basis side + double imgBasisToOther = side == Side::WIDTH ? 1.0 / imgAspectRatio : imgAspectRatio; + double borderBasisToOther = side == Side::WIDTH ? 1.0 / borderAspectRatio : borderAspectRatio; + double imgOther = imgBasis * imgBasisToOther; + double borderOther = borderBasis * borderBasisToOther; + + // Find the maximum allowed image size considering min size limits + double maxImageBasis = frameBasis; + double maxImageOther = frameOther; + if (framing.minSizeEnabled) { + double minBorderBasis = static_cast( + side == Side::WIDTH ? framing.minWidth : framing.minHeight); + double minBorderOther = static_cast( + side == Side::WIDTH ? framing.minHeight : framing.minWidth); + + maxImageOther = std::floor(frameOther - 2.0 * minBorderOther); + maxImageBasis = std::floor(frameBasis - 2.0 * minBorderBasis); + } + + // Image is too large to satisfy requirements: + // a. Min border size limit not satisfied + // b. Basis size is too small for the requested aspect ratio + // (i.e. original image clipped) + // + // Resize the image so that it fits in bounds + if (imgOther > maxImageOther) { + imgOther = maxImageOther; + imgBasis = imgOther / imgBasisToOther; + } + if (imgBasis > maxImageBasis) { + imgBasis = maxImageBasis; + imgOther = imgBasis * imgBasisToOther; + } + + if (side == Side::WIDTH) { + return Dimensions(imgBasis, imgOther); + } else { + return Dimensions(imgOther, imgBasis); + } +} + +Dimensions Framing::computeUniformRelativeImageBBox(const Dimensions& imgSize, + const Dimensions& framedSize) const +{ + if (imgSize.isDegenerate() || framedSize.isDegenerate()) { + return Dimensions(0, 0); + } + + Side side = pickReferenceSide(framing, imgSize); + double scale = framing.relativeBorderSize; + + // Compute image and border lengths on basis side + double frameBasis = side == Side::WIDTH ? framedSize.width : framedSize.height; + double frameOther = side == Side::WIDTH ? framedSize.height : framedSize.width; + + auto computedSizes = computeImgAndBorderSize(frameBasis, scale); + double imgBasis = computedSizes.first; + double border = computedSizes.second; + + // Compute image and border lengths for the non-basis side + double imgAspectRatio = imgSize.aspectRatio(); + double imgBasisToOther = side == Side::WIDTH ? 1.0 / imgAspectRatio : imgAspectRatio; + double imgOther = imgBasis * imgBasisToOther; + + // If the frame doesn't constrain the non-basis side length, we just need + // to check the border minimum size. However, if the non-basis side is + // constrained, we need to adjust the image size to fit while still + // maintaining the border scale w.r.t. the basis side. + double totalOther = imgOther + 2.0 * border; + if (totalOther > frameOther) { + // Let: + // imgOther = imgBasis * imgBasisToOther + // border = imgBasis * scale + // + // Want: + // frameOther = imgOther + 2 * border + // = imgBasis * imgBasisToOther + 2 * scale * imgBasis + // = imgBasis * (imgBasisToOther + 2 * scale) + // + // Rearrange: + // imgBasis = frameOther / (imgBasisToOther + 2 * scale) + imgBasis = frameOther / (imgBasisToOther + 2.0 * scale); + imgOther = imgBasis * imgBasisToOther; + border = imgBasis * scale; + } + + // Find the maximum allowed image size considering min size limits + double maxImageBasis = frameBasis; + double maxImageOther = frameOther; + if (framing.minSizeEnabled) { + double minBorder = static_cast( + side == Side::WIDTH ? framing.minWidth : framing.minHeight); + + maxImageBasis = std::floor(frameBasis - 2.0 * minBorder); + maxImageOther = std::floor(frameOther - 2.0 * minBorder); + } + + if (imgOther > maxImageOther) { + imgOther = maxImageOther; + imgBasis = imgOther / imgBasisToOther; + } + if (imgBasis > maxImageBasis) { + imgBasis = maxImageBasis; + imgOther = imgBasis * imgBasisToOther; + } + + if (side == Side::WIDTH) { + return Dimensions(imgBasis, imgOther); + } else { + return Dimensions(imgOther, imgBasis); + } +} + +ResizeArgs Framing::adjustResizeForFraming(const ResizeArgs& resize) const +{ + if (!framing.enabled) return resize; + + switch (framing.framingMethod) { + case FramingMethod::BBOX: + return resizeForBBox(resize); + case FramingMethod::FIXED_SIZE: + return resizeForFixedFrame(resize); + case FramingMethod::STANDARD: + default: + // No limits on framed size so do nothing + return resize; + } +} + +ResizeArgs Framing::resizeForFixedFrame(const ResizeArgs& args) const +{ + double framedWidth = framing.framedWidth; + double framedHeight = framing.framedHeight; + Dimensions frameSize(framedWidth, framedHeight); + + Dimensions bbox; + if (framing.borderSizingMethod == BorderSizing::FIXED_SIZE) { + auto length = [](double frame, double border) { + return std::max(0.0, frame - 2.0 * border); + }; + + bbox = { + length(framedWidth, framing.absWidth), + length(framedHeight, framing.absHeight) + }; + } else if (framing.borderSizingMethod == BorderSizing::UNIFORM_PERCENTAGE) { + bbox = computeUniformRelativeImageBBox(args.size, frameSize); + } else { + bbox = computeRelativeImageBBoxInFrame(args.size, frameSize); + } + + return adjustResize(args, bbox); +} + +ResizeArgs Framing::resizeForBBox(const ResizeArgs& args) const +{ + Dimensions boundary(framing.framedWidth, framing.framedHeight); + + Dimensions bbox; + if (framing.borderSizingMethod == BorderSizing::FIXED_SIZE) { + auto length = [](double frame, double border) { + return std::max(0.0, frame - 2.0 * border); + }; + + bbox = { + length(boundary.width, framing.absWidth), + length(boundary.height, framing.absHeight) + }; + } else if (framing.borderSizingMethod == BorderSizing::UNIFORM_PERCENTAGE) { + bbox = computeUniformRelativeImageBBox(args.size, boundary); + } else { + // For the requested aspect ratio, it must fit inside the requested + // bounding box + double aspectRatio = orientAspectRatio(framing, args.size); + Dimensions ratioBBox = fromAspectRatio(boundary, aspectRatio); + ratioBBox = clampToBBox(ratioBBox, boundary, INSIDE_BBOX); + + // Now we have the true max bounds for the framed image. Determine how the + // original image fits inside these bounds. This process is the same as + // in the fixed frame mode. + bbox = computeRelativeImageBBoxInFrame(args.size, ratioBBox); + } + + return adjustResize(args, bbox); +} + +Dimensions Framing::computeFramedSize(const Dimensions& imgSize) const +{ + if (!framing.enabled) return imgSize; + + // For constrained frame sizes, check if the image size (without frame) + // exceeds the constrained size. This indicates that a combination of + // parameters caused the downscaling limit to be hit. In which case, + // just return the original image size (i.e. don't insert border). + // + // If the image fits the constrained size, assume that previous resize + // calculations were correct and trim off any excess borders. The excess + // may be from rounding errors or hitting some downscaling limit. + switch (framing.framingMethod) { + case FramingMethod::BBOX: + { + Dimensions fixed(framing.framedWidth, framing.framedHeight); + if (imgSize.inside(fixed)) { + Dimensions framedSize = computeSizeWithBorders(imgSize); + return clampToBBox(framedSize, fixed, INSIDE_BBOX); + } else { + return imgSize; + } + } + case FramingMethod::FIXED_SIZE: + { + Dimensions fixed(framing.framedWidth, framing.framedHeight); + return imgSize.inside(fixed) ? fixed : imgSize; + } + case FramingMethod::STANDARD: + default: + return computeSizeWithBorders(imgSize); + } +} + +Dimensions Framing::computeSizeWithBorders(const Dimensions& imgSize) const +{ + if (framing.borderSizingMethod == BorderSizing::FIXED_SIZE) { + return Dimensions(imgSize.width + 2.0 * framing.absWidth, + imgSize.height + 2.0 * framing.absHeight); + } + + Side side = pickReferenceSide(framing, imgSize); + double scale = framing.relativeBorderSize; + + if (framing.borderSizingMethod == BorderSizing::UNIFORM_PERCENTAGE) { + double borderSize = 0; + if (side == Side::WIDTH) { + borderSize = scale * imgSize.width; + if (framing.minSizeEnabled && borderSize < framing.minWidth) { + borderSize = framing.minWidth; + } + } else { + borderSize = scale * imgSize.height; + if (framing.minSizeEnabled && borderSize < framing.minHeight) { + borderSize = framing.minHeight; + } + } + + return Dimensions(imgSize.width + 2.0 * borderSize, + imgSize.height + 2.0 * borderSize); + } + + double aspectRatio = orientAspectRatio(framing, imgSize); + + Dimensions framedSize; + if (side == Side::WIDTH) { + framedSize.width = (1.0 + 2.0 * scale) * imgSize.width; + framedSize.height = framedSize.width / aspectRatio; + } else { + framedSize.height = (1.0 + 2.0 * scale) * imgSize.height; + framedSize.width = framedSize.height * aspectRatio; + } + + // Check if the computed frame size satsifies the requested aspect ratio + // without cutting off the original image. If the image is cut off, use + // the smallest frame that preserves the original image and still + // satisfies the requested aspect ratio. + Dimensions minFramedSize = fromAspectRatio(imgSize, aspectRatio); + Dimensions limit = imgSize; + if (framing.minSizeEnabled) { + limit.width += 2.0 * framing.minWidth; + limit.height += 2.0 * framing.minHeight; + } + minFramedSize = clampToBBox(minFramedSize, limit, OUTSIDE_BBOX); + + if (minFramedSize.inside(framedSize)) { + return framedSize; + } else { + return minFramedSize; + } +} + +} // namespace namespace rtengine { @@ -333,7 +1006,7 @@ void ImProcFunctions::Lanczos (const LabImage* src, LabImage* dst, float scale) delete[] wwh; } -float ImProcFunctions::resizeScale (const ProcParams* params, int fw, int fh, int &imw, int &imh) +double ImProcFunctions::resizeScale (const ProcParams* params, int fw, int fh, int &imw, int &imh) { imw = fw; imh = fh; @@ -406,10 +1079,6 @@ float ImProcFunctions::resizeScale (const ProcParams* params, int fw, int fh, in break; } - if (fabs (dScale - 1.0) <= 1e-5) { - return 1.0; - } - if (params->crop.enabled && params->resize.appliesTo == "Full image") { imw = params->crop.w; imh = params->crop.h; @@ -418,9 +1087,13 @@ float ImProcFunctions::resizeScale (const ProcParams* params, int fw, int fh, in imh = refh; } - imw = (int) ( (double)imw * dScale + 0.5 ); - imh = (int) ( (double)imh * dScale + 0.5 ); - return (float)dScale; + if (fabs (dScale - 1.0) <= 1e-5) { + return 1.0; + } else { + imw = computeSize(imw, dScale); + imh = computeSize(imh, dScale); + return dScale; + } } void ImProcFunctions::resize (Imagefloat* src, Imagefloat* dst, float dScale) @@ -458,4 +1131,99 @@ void ImProcFunctions::resize (Imagefloat* src, Imagefloat* dst, float dScale) #endif } +ImProcFunctions::FramingData ImProcFunctions::framing(const FramingArgs& args) const +{ + FramingData result; + result.enabled = false; + result.imgWidth = args.resizeWidth; + result.imgHeight = args.resizeHeight; + result.scale = args.resizeScale; + result.framedWidth = args.resizeWidth; + result.framedHeight = args.resizeHeight; + + if (!args.params || !args.params->resize.enabled) return result; + if (!args.params->framing.enabled) return result; + + // For these calculations, try to keep everything as doubles to minimize + // rounding errors from intermediate steps! + + Framing util(*params, args.cropWidth, args.cropHeight); + ResizeArgs resize(Dimensions(args.resizeWidth, args.resizeHeight), args.resizeScale); + ResizeArgs adjusted = util.adjustResizeForFraming(resize); + Dimensions framedSize = util.computeFramedSize(adjusted.size); + + result.enabled = true; + result.imgWidth = std::round(adjusted.size.width); + result.imgHeight = std::round(adjusted.size.height); + result.scale = adjusted.scale; + result.framedWidth = std::round(framedSize.width); + result.framedHeight = std::round(framedSize.height); + + return result; } + +// Draws the border around the input image. +// It should be called after gamma correction. +Imagefloat* ImProcFunctions::drawFrame(Imagefloat* rgb, const FramingParams& params, + const FramingData& dims) const +{ + if (rgb->getWidth() > dims.framedWidth || rgb->getHeight() > dims.framedHeight) { + return rgb; + } + if (rgb->getWidth() == dims.framedWidth && rgb->getHeight() == dims.framedHeight) { + return rgb; + } + + Imagefloat* framed = new Imagefloat(dims.framedWidth, dims.framedHeight); + + // Color::gamma2curve expects a 16-bit value, but the GUI sliders are + // using 8-bit values. Step up the user value to 16-bits. + auto clip = [](int v) -> int { + int sanitized = std::max(0, std::min(v, 255)); + + double normalized = static_cast(sanitized) / 255.0; + return normalized * 65535.0; + }; + + float r = Color::gamma2curve[clip(params.borderRed)]; + float g = Color::gamma2curve[clip(params.borderGreen)]; + float b = Color::gamma2curve[clip(params.borderBlue)]; + +#ifdef _OPENMP + #pragma omp parallel for if (multiThread) +#endif + for (int i = 0; i < framed->getHeight(); i++) { + for (int j = 0; j < framed->getWidth(); j++) { + framed->r(i, j) = r; + framed->g(i, j) = g; + framed->b(i, j) = b; + } + } + + auto offset = [](int inner, int outer) { + double u = inner; + double v = outer; + return static_cast(std::round((v - u) / 2.0)); + }; + int rowOffset = offset(rgb->getHeight(), framed->getHeight()); + int colOffset = offset(rgb->getWidth(), framed->getWidth()); + +#ifdef _OPENMP + #pragma omp parallel for if (multiThread) +#endif + for (int i = 0; i < rgb->getHeight(); i++) { + for (int j = 0; j < rgb->getWidth(); j++) { + int row = i + rowOffset; + int col = j + colOffset; + + framed->r(row, col) = rgb->r(i, j); + framed->g(row, col) = rgb->g(i, j); + framed->b(row, col) = rgb->b(i, j); + } + } + + delete rgb; + return framed; +} + +} // namespace rtengine diff --git a/rtengine/procevents.h b/rtengine/procevents.h index a1408e405..44b3bf6c1 100644 --- a/rtengine/procevents.h +++ b/rtengine/procevents.h @@ -1177,7 +1177,6 @@ enum ProcEventCode { Evlocallabsigjz = 1147, Evlocallabsigq = 1148, Evlocallablogcie = 1149, - NUMOFEVENTS }; diff --git a/rtengine/procparams.cc b/rtengine/procparams.cc index 90d0bb5d5..d8a84329e 100644 --- a/rtengine/procparams.cc +++ b/rtengine/procparams.cc @@ -427,6 +427,181 @@ bool saveToKeyfile( return false; } +namespace ProfileKeys { + +#define DEFINE_KEY(VAR, NAME) \ + constexpr const char* VAR = NAME; + +DEFINE_KEY(TOOL_ENABLED, "Enabled"); + +namespace Framing +{ + DEFINE_KEY(TOOL_NAME, "Framing"); + + // Fields + DEFINE_KEY(FRAMING_METHOD, "FramingMethod"); + DEFINE_KEY(ASPECT_RATIO, "AspectRatio"); + DEFINE_KEY(ORIENTATION, "Orientation"); + DEFINE_KEY(FRAMED_WIDTH, "FramedWidth"); + DEFINE_KEY(FRAMED_HEIGHT, "FramedHeight"); + DEFINE_KEY(ALLOW_UPSCALING, "AllowUpscaling"); + + DEFINE_KEY(BORDER_SIZING_METHOD, "BorderSizingMethod"); + DEFINE_KEY(BASIS, "Basis"); + DEFINE_KEY(RELATIVE_BORDER_SIZE, "RelativeBorderSize"); + DEFINE_KEY(MIN_SIZE_ENABLED, "MinSizeEnabled"); + DEFINE_KEY(MIN_WIDTH, "MinWidth"); + DEFINE_KEY(MIN_HEIGHT, "MinHeight"); + DEFINE_KEY(ABS_WIDTH, "AbsWidth"); + DEFINE_KEY(ABS_HEIGHT, "AbsHeight"); + + DEFINE_KEY(BORDER_RED, "BorderRed"); + DEFINE_KEY(BORDER_GREEN, "BorderGreen"); + DEFINE_KEY(BORDER_BLUE, "BorderBlue"); + + // Enum mappings + DEFINE_KEY(FRAMING_METHOD_STANDARD, "Standard"); + DEFINE_KEY(FRAMING_METHOD_BBOX, "BoundingBox"); + DEFINE_KEY(FRAMING_METHOD_FIXED_SIZE, "FixedSize"); + DEFINE_KEY(ORIENT_AS_IMAGE, "AsImage"); + DEFINE_KEY(ORIENT_LANDSCAPE, "Landscape"); + DEFINE_KEY(ORIENT_PORTRAIT, "Portrait"); + DEFINE_KEY(BORDER_SIZING_PERCENTAGE, "Percentage"); + DEFINE_KEY(BORDER_SIZING_UNIFORM_PERCENTAGE, "UniformPercentage"); + DEFINE_KEY(BORDER_SIZING_FIXED_SIZE, "FixedSize"); + DEFINE_KEY(BASIS_AUTO, "Auto"); + DEFINE_KEY(BASIS_WIDTH, "Width"); + DEFINE_KEY(BASIS_HEIGHT, "Height"); + DEFINE_KEY(BASIS_LONG, "Long"); + DEFINE_KEY(BASIS_SHORT, "Short"); +} // namespace Framing + +} // namespace ProfileKeys + +void loadFramingParams( + const Glib::KeyFile& keyFile, + rtengine::procparams::FramingParams& params, + FramingParamsEdited& edited +) +{ + using namespace ProfileKeys; + using namespace ProfileKeys::Framing; + using FramingParams = rtengine::procparams::FramingParams; + + const Glib::ustring group{TOOL_NAME}; + if (!keyFile.has_group(group)) return; + + assignFromKeyfile(keyFile, group, TOOL_ENABLED, params.enabled, edited.enabled); + + using FramingMethod = FramingParams::FramingMethod; + const std::map framingMethodMapping = { + {FRAMING_METHOD_STANDARD, FramingMethod::STANDARD}, + {FRAMING_METHOD_BBOX, FramingMethod::BBOX}, + {FRAMING_METHOD_FIXED_SIZE, FramingMethod::FIXED_SIZE} + }; + assignFromKeyfile(keyFile, group, FRAMING_METHOD, framingMethodMapping, params.framingMethod, edited.framingMethod); + assignFromKeyfile(keyFile, group, ASPECT_RATIO, params.aspectRatio, edited.aspectRatio); + using Orientation = FramingParams::Orientation; + const std::map orientationMapping = { + {ORIENT_AS_IMAGE, Orientation::AS_IMAGE}, + {ORIENT_LANDSCAPE, Orientation::LANDSCAPE}, + {ORIENT_PORTRAIT, Orientation::PORTRAIT}, + }; + assignFromKeyfile(keyFile, group, ORIENTATION, orientationMapping, params.orientation, edited.orientation); + assignFromKeyfile(keyFile, group, FRAMED_WIDTH, params.framedWidth, edited.framedWidth); + assignFromKeyfile(keyFile, group, FRAMED_HEIGHT, params.framedHeight, edited.framedHeight); + assignFromKeyfile(keyFile, group, ALLOW_UPSCALING, params.allowUpscaling, edited.allowUpscaling); + + using BorderSizing = FramingParams::BorderSizing; + const std::map borderSizingMapping = { + {BORDER_SIZING_PERCENTAGE, BorderSizing::PERCENTAGE}, + {BORDER_SIZING_UNIFORM_PERCENTAGE, BorderSizing::UNIFORM_PERCENTAGE}, + {BORDER_SIZING_FIXED_SIZE, BorderSizing::FIXED_SIZE} + }; + assignFromKeyfile(keyFile, group, BORDER_SIZING_METHOD, borderSizingMapping, params.borderSizingMethod, edited.borderSizingMethod); + using Basis = FramingParams::Basis; + const std::map basisMapping = { + {BASIS_AUTO, Basis::AUTO}, + {BASIS_WIDTH, Basis::WIDTH}, + {BASIS_HEIGHT, Basis::HEIGHT}, + {BASIS_LONG, Basis::LONG}, + {BASIS_SHORT, Basis::SHORT} + }; + assignFromKeyfile(keyFile, group, BASIS, basisMapping, params.basis, edited.basis); + assignFromKeyfile(keyFile, group, RELATIVE_BORDER_SIZE, params.relativeBorderSize, edited.relativeBorderSize); + assignFromKeyfile(keyFile, group, MIN_SIZE_ENABLED, params.minSizeEnabled, edited.minSizeEnabled); + assignFromKeyfile(keyFile, group, MIN_WIDTH, params.minWidth, edited.minWidth); + assignFromKeyfile(keyFile, group, MIN_HEIGHT, params.minHeight, edited.minHeight); + assignFromKeyfile(keyFile, group, ABS_WIDTH, params.absWidth, edited.absWidth); + assignFromKeyfile(keyFile, group, ABS_HEIGHT, params.absHeight, edited.absHeight); + + assignFromKeyfile(keyFile, group, BORDER_RED, params.borderRed, edited.borderRed); + assignFromKeyfile(keyFile, group, BORDER_GREEN, params.borderGreen, edited.borderGreen); + assignFromKeyfile(keyFile, group, BORDER_BLUE, params.borderBlue, edited.borderBlue); +} + +void saveFramingParams( + Glib::KeyFile& keyFile, + const rtengine::procparams::FramingParams& params, + const ParamsEdited* pedited +) +{ + using namespace ProfileKeys; + using namespace ProfileKeys::Framing; + using FramingParams = rtengine::procparams::FramingParams; + + const Glib::ustring group{TOOL_NAME}; + + const FramingParamsEdited& edited = pedited->framing; + + saveToKeyfile(!pedited || edited.enabled, group, TOOL_ENABLED, params.enabled, keyFile); + + using FramingMethod = FramingParams::FramingMethod; + const std::map framingMethodMapping = { + {FramingMethod::STANDARD, FRAMING_METHOD_STANDARD}, + {FramingMethod::BBOX, FRAMING_METHOD_BBOX}, + {FramingMethod::FIXED_SIZE, FRAMING_METHOD_FIXED_SIZE} + }; + saveToKeyfile(!pedited || edited.framingMethod, group, FRAMING_METHOD, framingMethodMapping, params.framingMethod, keyFile); + saveToKeyfile(!pedited || edited.aspectRatio, group, ASPECT_RATIO, params.aspectRatio, keyFile); + using Orientation = FramingParams::Orientation; + const std::map orientationMapping = { + {Orientation::AS_IMAGE, ORIENT_AS_IMAGE}, + {Orientation::LANDSCAPE, ORIENT_LANDSCAPE}, + {Orientation::PORTRAIT, ORIENT_PORTRAIT}, + }; + saveToKeyfile(!pedited || edited.orientation, group, ORIENTATION, orientationMapping, params.orientation, keyFile); + saveToKeyfile(!pedited || edited.framedWidth, group, FRAMED_WIDTH, params.framedWidth, keyFile); + saveToKeyfile(!pedited || edited.framedHeight, group, FRAMED_HEIGHT, params.framedHeight, keyFile); + saveToKeyfile(!pedited || edited.allowUpscaling, group, ALLOW_UPSCALING, params.allowUpscaling, keyFile); + + using BorderSizing = FramingParams::BorderSizing; + const std::map borderSizingMapping = { + {BorderSizing::PERCENTAGE, BORDER_SIZING_PERCENTAGE}, + {BorderSizing::UNIFORM_PERCENTAGE, BORDER_SIZING_UNIFORM_PERCENTAGE}, + {BorderSizing::FIXED_SIZE, BORDER_SIZING_FIXED_SIZE} + }; + saveToKeyfile(!pedited || edited.borderSizingMethod, group, BORDER_SIZING_METHOD, borderSizingMapping, params.borderSizingMethod, keyFile); + using Basis = FramingParams::Basis; + const std::map basisMapping = { + {Basis::AUTO, BASIS_AUTO}, + {Basis::WIDTH, BASIS_WIDTH}, + {Basis::HEIGHT, BASIS_HEIGHT}, + {Basis::LONG, BASIS_LONG}, + {Basis::SHORT, BASIS_SHORT} + }; + saveToKeyfile(!pedited || edited.basis, group, BASIS, basisMapping, params.basis, keyFile); + saveToKeyfile(!pedited || edited.relativeBorderSize, group, RELATIVE_BORDER_SIZE, params.relativeBorderSize, keyFile); + saveToKeyfile(!pedited || edited.minSizeEnabled, group, MIN_SIZE_ENABLED, params.minSizeEnabled, keyFile); + saveToKeyfile(!pedited || edited.minWidth, group, MIN_WIDTH, params.minWidth, keyFile); + saveToKeyfile(!pedited || edited.minHeight, group, MIN_HEIGHT, params.minHeight, keyFile); + saveToKeyfile(!pedited || edited.absWidth, group, ABS_WIDTH, params.absWidth, keyFile); + saveToKeyfile(!pedited || edited.absHeight, group, ABS_HEIGHT, params.absHeight, keyFile); + + saveToKeyfile(!pedited || edited.borderRed, group, BORDER_RED, params.borderRed, keyFile); + saveToKeyfile(!pedited || edited.borderGreen, group, BORDER_GREEN, params.borderGreen, keyFile); + saveToKeyfile(!pedited || edited.borderBlue, group, BORDER_BLUE, params.borderBlue, keyFile); +} } // namespace @@ -2425,6 +2600,56 @@ bool ResizeParams::operator !=(const ResizeParams& other) const return !(*this == other); } +FramingParams::FramingParams() : + enabled(false), + framingMethod(FramingMethod::STANDARD), + aspectRatio(0), + orientation(Orientation::AS_IMAGE), + framedWidth(800), + framedHeight(600), + allowUpscaling(false), + borderSizingMethod(BorderSizing::PERCENTAGE), + basis(Basis::AUTO), + relativeBorderSize(0.1), + minSizeEnabled(false), + minWidth(0), + minHeight(0), + absWidth(0), + absHeight(0), + borderRed(255), + borderGreen(255), + borderBlue(255) +{ +} + +bool FramingParams::operator ==(const FramingParams& other) const +{ + return + enabled == other.enabled + && framingMethod == other.framingMethod + && aspectRatio == other.aspectRatio + && orientation == other.orientation + && framedWidth == other.framedWidth + && framedHeight == other.framedHeight + && allowUpscaling == other.allowUpscaling + && borderSizingMethod == other.borderSizingMethod + && basis == other.basis + && relativeBorderSize == other.relativeBorderSize + && minSizeEnabled == other.minSizeEnabled + && minWidth == other.minWidth + && minHeight == other.minHeight + && absWidth == other.absWidth + && absHeight == other.absHeight + && borderRed == other.borderRed + && borderGreen == other.borderGreen + && borderBlue == other.borderBlue; +} + +bool FramingParams::operator !=(const FramingParams& other) const +{ + return !(*this == other); +} + const Glib::ustring ColorManagementParams::NoICMString = Glib::ustring("No ICM: sRGB output"); const Glib::ustring ColorManagementParams::NoProfileString = Glib::ustring("(none)"); @@ -6358,6 +6583,8 @@ void ProcParams::setDefaults() resize = {}; + framing = {}; + icm = {}; wavelet = {}; @@ -7697,6 +7924,8 @@ int ProcParams::save(const Glib::ustring& fname, const Glib::ustring& fname2, bo saveToKeyfile(!pedited || pedited->resize.shortedge, "Resize", "ShortEdge", resize.shortedge, keyFile); saveToKeyfile(!pedited || pedited->resize.allowUpscaling, "Resize", "AllowUpscaling", resize.allowUpscaling, keyFile); + saveFramingParams(keyFile, framing, pedited); + // Post demosaic sharpening saveToKeyfile(!pedited || pedited->pdsharpening.enabled, "PostDemosaicSharpening", "Enabled", pdsharpening.enabled, keyFile); saveToKeyfile(!pedited || pedited->pdsharpening.contrast, "PostDemosaicSharpening", "Contrast", pdsharpening.contrast, keyFile); @@ -10252,6 +10481,8 @@ int ProcParams::load(const Glib::ustring& fname, ParamsEdited* pedited) } } + loadFramingParams(keyFile, framing, pedited->framing); + if (keyFile.has_group ("Spot removal")) { assignFromKeyfile(keyFile, "Spot removal", "Enabled", spot.enabled, pedited->spot.enabled); int i = 0; @@ -11498,6 +11729,7 @@ bool ProcParams::operator ==(const ProcParams& other) const && chmixer == other.chmixer && blackwhite == other.blackwhite && resize == other.resize + && framing == other.framing && spot == other.spot && raw == other.raw && icm == other.icm diff --git a/rtengine/procparams.h b/rtengine/procparams.h index 80a9100d1..94108e5fd 100644 --- a/rtengine/procparams.h +++ b/rtengine/procparams.h @@ -1984,6 +1984,68 @@ struct ResizeParams { bool operator !=(const ResizeParams& other) const; }; +struct FramingParams { + // How is framed size determined? + enum class FramingMethod { + STANDARD, // Unconstrained framed size + BBOX, // Framed size within bounding box + FIXED_SIZE // Fixed framed size + }; + + // Orientation of framed image + enum class Orientation { + AS_IMAGE, + LANDSCAPE, + PORTRAIT + }; + + // How to size border? + enum class BorderSizing { + PERCENTAGE, // Percentage of image size + UNIFORM_PERCENTAGE, // Percentage of image size (ignore aspect ratio) + FIXED_SIZE // Fixed pixel dimensions + }; + + // Which dimension to use for percentage based border sizing? + enum class Basis { + AUTO, // Determine by aspect ratio of image and frame + WIDTH, + HEIGHT, + LONG, // Use long side of image + SHORT // Use short side of image + }; + + // Indicates to use the image aspect ratio for border + static constexpr double AS_IMAGE_ASPECT_RATIO = 0.0; + + FramingParams(); + + bool enabled; + + FramingMethod framingMethod; + double aspectRatio; + Orientation orientation; + int framedWidth; + int framedHeight; + bool allowUpscaling; + + BorderSizing borderSizingMethod; + Basis basis; + double relativeBorderSize; + bool minSizeEnabled; + int minWidth; + int minHeight; + int absWidth; + int absHeight; + + int borderRed; + int borderGreen; + int borderBlue; + + bool operator ==(const FramingParams& other) const; + bool operator !=(const FramingParams& other) const; +}; + /** * Parameters entry */ @@ -2764,6 +2826,7 @@ public: ChannelMixerParams chmixer; ///< Channel mixer parameters BlackWhiteParams blackwhite; ///< Black& White parameters ResizeParams resize; ///< Resize parameters + FramingParams framing; ///< Framing parameters SpotParams spot; ///< Spot removal tool ColorManagementParams icm; ///< profiles/color spaces used during the image processing RAWParams raw; ///< RAW parameters before demosaicing diff --git a/rtengine/refreshmap.cc b/rtengine/refreshmap.cc index 5147e09ce..935001c93 100644 --- a/rtengine/refreshmap.cc +++ b/rtengine/refreshmap.cc @@ -1185,7 +1185,7 @@ int refreshmap[rtengine::NUMOFEVENTS] = { AUTOEXP, //Evlocallabforcebw AUTOEXP, //Evlocallabsigjz AUTOEXP, //Evlocallabsigq - AUTOEXP //Evlocallablogcie + AUTOEXP //Evlocallablogcie }; diff --git a/rtengine/simpleprocess.cc b/rtengine/simpleprocess.cc index fa15285a7..8493dd5c8 100644 --- a/rtengine/simpleprocess.cc +++ b/rtengine/simpleprocess.cc @@ -1893,20 +1893,52 @@ private: pl->setProgress(0.60); } - int imw, imh; - double tmpScale = ipf.resizeScale(¶ms, fw, fh, imw, imh); - bool labResize = params.resize.enabled && params.resize.method != "Nearest" && (tmpScale != 1.0 || params.prsharpening.enabled); - LabImage *tmplab; - // crop and convert to rgb16 int cx = 0, cy = 0, cw = labView->W, ch = labView->H; - if (params.crop.enabled) { cx = params.crop.x; cy = params.crop.y; cw = params.crop.w; ch = params.crop.h; + } + ImProcFunctions::FramingArgs framingArgs; + framingArgs.params = ¶ms; + framingArgs.cropWidth = cw; + framingArgs.cropHeight = ch; + { + int imw, imh; + double tmpScale = ipf.resizeScale(¶ms, fw, fh, imw, imh); + framingArgs.resizeWidth = imw; + framingArgs.resizeHeight = imh; + framingArgs.resizeScale = tmpScale; + + // If upscaling is not permitted, keep original sizing + if ((cw < imw || ch < imh) && !params.resize.allowUpscaling) { + framingArgs.resizeWidth = cw; + framingArgs.resizeHeight = ch; + framingArgs.resizeScale = 1.0; + } + } + + // If framing is not enabled, resize values simply pass through to output + ImProcFunctions::FramingData framingData = ipf.framing(framingArgs); + if (settings->verbose) { + printf("Framing Parameters (enabled=%s)\n", framingData.enabled ? "yes" : "no"); + printf(" Crop: w=%d h=%d\n", cw, ch); + printf(" Original resize: w=%d h=%d s=%f\n", + framingArgs.resizeWidth, framingArgs.resizeHeight, framingArgs.resizeScale); + printf(" Framed image size: w=%d h=%d s=%f\n", + framingData.imgWidth, framingData.imgHeight, framingData.scale); + printf(" Total size: w=%d h=%d\n", + framingData.framedWidth, framingData.framedHeight); + } + + bool labResize = params.resize.enabled && params.resize.method != "Nearest" && + (framingData.scale != 1.0 || params.prsharpening.enabled || framingData.enabled); + + LabImage *tmplab = nullptr; + if (params.crop.enabled) { if (labResize) { // crop lab data tmplab = new LabImage(cw, ch); @@ -1926,11 +1958,12 @@ private: } if (labResize) { // resize lab data - if ((labView->W != imw || labView->H != imh) && - (params.resize.allowUpscaling || (labView->W >= imw && labView->H >= imh))) { + int imw = framingData.imgWidth; + int imh = framingData.imgHeight; + if (labView->W != imw || labView->H != imh) { // resize image tmplab = new LabImage(imw, imh); - ipf.Lanczos(labView, tmplab, tmpScale); + ipf.Lanczos(labView, tmplab, framingData.scale); delete labView; labView = tmplab; } @@ -1984,12 +2017,20 @@ private: pl->setProgress(0.70); } - if (tmpScale != 1.0 && params.resize.method == "Nearest" && - (params.resize.allowUpscaling || (readyImg->getWidth() >= imw && readyImg->getHeight() >= imh))) { // resize rgb data (gamma applied) - Imagefloat* tempImage = new Imagefloat(imw, imh); - ipf.resize(readyImg, tempImage, tmpScale); - delete readyImg; - readyImg = tempImage; + if (framingData.scale != 1.0 && params.resize.method == "Nearest") { + int imw = framingData.imgWidth; + int imh = framingData.imgHeight; + if (readyImg->getWidth() != imw || readyImg->getHeight() != imh) { + // resize rgb data (gamma applied) + Imagefloat* tempImage = new Imagefloat(imw, imh); + ipf.resize(readyImg, tempImage, framingData.scale); + delete readyImg; + readyImg = tempImage; + } + } + + if (framingData.enabled) { + readyImg = ipf.drawFrame(readyImg, params.framing, framingData); } Exiv2Metadata info(imgsrc->getFileName()); diff --git a/rtgui/CMakeLists.txt b/rtgui/CMakeLists.txt index cd2b01895..5d3276ccd 100644 --- a/rtgui/CMakeLists.txt +++ b/rtgui/CMakeLists.txt @@ -13,6 +13,7 @@ set(CLISOURCEFILES set(NONCLISOURCEFILES adjuster.cc alignedmalloc.cc + aspectratios.cc batchqueue.cc batchqueuebuttonset.cc batchqueueentry.cc @@ -34,6 +35,7 @@ set(NONCLISOURCEFILES colorappearance.cc coloredbar.cc colortoning.cc + colorpreview.cc controllines.cc controlspotpanel.cc coordinateadjuster.cc @@ -76,6 +78,7 @@ set(NONCLISOURCEFILES filterpanel.cc flatcurveeditorsubgroup.cc flatfield.cc + framing.cc gradient.cc guiutils.cc histogrampanel.cc diff --git a/rtgui/addsetids.h b/rtgui/addsetids.h index 2a123a9f7..0753e8473 100644 --- a/rtgui/addsetids.h +++ b/rtgui/addsetids.h @@ -134,6 +134,10 @@ enum { ADDSET_SHARP_EDGETOL, ADDSET_SHARP_HALOCTRL, ADDSET_RESIZE_SCALE, + ADDSET_FRAMING_RELATIVE_SCALE, + ADDSET_FRAMING_BORDER_RED, + ADDSET_FRAMING_BORDER_GREEN, + ADDSET_FRAMING_BORDER_BLUE, ADDSET_EPD_STRENGTH, ADDSET_EPD_GAMMA, ADDSET_EPD_EDGESTOPPING, diff --git a/rtgui/aspectratios.cc b/rtgui/aspectratios.cc new file mode 100644 index 000000000..e9e525dbb --- /dev/null +++ b/rtgui/aspectratios.cc @@ -0,0 +1,65 @@ +/* + * This file is part of RawTherapee. + * + * Copyright (c) 2004-2010 Gabor Horvath + * + * RawTherapee is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * RawTherapee is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with RawTherapee. If not, see . + * + * 2024-2024 Daniel Gao + */ + +#include "aspectratios.h" + +namespace { + +static const std::vector ASPECT_RATIOS { + {"3:2", 3.0 / 2.0}, // L1.5, P0.666... + {"4:3", 4.0 / 3.0}, // L1.333..., P0.75 + {"16:9", 16.0 / 9.0}, // L1.777..., P0.5625 + {"16:10", 16.0 / 10.0}, // L1.6, P0.625 + {"1:1", 1.0 / 1.0}, // L1, P1 + {"2:1", 2.0 / 1.0}, // L2, P0.5 + {"3:1", 3.0 / 1.0}, // L3, P0.333... + {"4:1", 4.0 / 1.0}, // L4, P0.25 + {"5:1", 5.0 / 1.0}, // L5, P0.2 + {"6:1", 6.0 / 1.0}, // L6, P0.1666... + {"7:1", 7.0 / 1.0}, // L7, P0.142... + {"4:5", 4.0 / 5.0}, // L1.25, P0.8 + {"5:7", 5.0 / 7.0}, // L1.4, P0.714... + {"6:7", 6.0 / 7.0}, // L1.166..., P0.857... + {"6:17", 6.0 / 17.0}, // L2.833..., P0.352... + {"24:65 - XPAN", 24.0 / 65.0}, // L2.708..., P0.369... + {"1.414 - DIN EN ISO 216", 1.414}, // L1.414, P0.707... + {"3.5:5", 3.5 / 5.0}, // L1.428..., P0.7 + {"8.5:11 - US Letter", 8.5 / 11.0}, // L1.294..., P0.772... + {"9.5:12", 9.5 / 12.0}, // L1.263..., P0.791... + {"10:12", 10.0 / 12.0}, // L1.2, P0.833... + {"11:14", 11.0 / 14.0}, // L1.272..., P0.785... + {"11:17 - Tabloid", 11.0 / 17.0}, // L1.545..., P0.647... + {"13:19", 13.0 / 19.0}, // L1.461..., P0.684... + {"17:22", 17.0 / 22.0}, // L1.294..., P0.772... + {"45:35 - ePassport", 45.0 / 35.0}, // L1.285,... P0.777... + {"64:27", 64.0 / 27.0}, // L2.370..., P0.421... + {"13:18", 13.0 / 18.0}, // L1.384..., P0.722... +}; + +} // namespace + +void fillAspectRatios(std::vector& ratios) { + ratios.reserve(ratios.size() + ASPECT_RATIOS.size()); + + for (const auto& ratio : ASPECT_RATIOS) { + ratios.push_back(ratio); + } +} diff --git a/rtgui/aspectratios.h b/rtgui/aspectratios.h new file mode 100644 index 000000000..3f190522a --- /dev/null +++ b/rtgui/aspectratios.h @@ -0,0 +1,37 @@ +/* + * This file is part of RawTherapee. + * + * Copyright (c) 2004-2010 Gabor Horvath + * + * RawTherapee is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * RawTherapee is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with RawTherapee. If not, see . + * + * 2024-2024 Daniel Gao + */ + +#pragma once + +#include + +#include + +struct AspectRatio { + Glib::ustring label; + double value; +}; + +// Pushes known aspect ratios to the given vector. +// +// Callers should populate ratios for entries like "As Imange" and "Current" +// before calling this function. +void fillAspectRatios(std::vector& ratios); diff --git a/rtgui/batchtoolpanelcoord.cc b/rtgui/batchtoolpanelcoord.cc index 219054ca4..4b6e16dca 100644 --- a/rtgui/batchtoolpanelcoord.cc +++ b/rtgui/batchtoolpanelcoord.cc @@ -151,6 +151,7 @@ void BatchToolPanelCoordinator::initSession () colorappearance->setAdjusterBehavior (false, false, false, false, false, false, false, false, false, false, false, false, false, false, false); rotate->setAdjusterBehavior (false); resize->setAdjusterBehavior (false); + framing->setAdjusterBehavior (false, false, false, false); distortion->setAdjusterBehavior (false, false); perspective->setAdjusterBehavior (false, false, false, false, false, false, false); gradient->setAdjusterBehavior (false, false, false, false); @@ -196,6 +197,11 @@ void BatchToolPanelCoordinator::initSession () colorappearance->setAdjusterBehavior (options.baBehav[ADDSET_CAT_DEGREE], options.baBehav[ADDSET_CAT_ADAPTSCENE], options.baBehav[ADDSET_CAT_ADAPTVIEWING], options.baBehav[ADDSET_CAT_BADPIX], options.baBehav[ADDSET_CAT_LIGHT], options.baBehav[ADDSET_CAT_CHROMA], options.baBehav[ADDSET_CAT_CONTRAST], options.baBehav[ADDSET_CAT_RSTPRO], options.baBehav[ADDSET_CAT_BRIGHT], options.baBehav[ADDSET_CAT_CONTRAST_Q], options.baBehav[ADDSET_CAT_CHROMA_S], options.baBehav[ADDSET_CAT_CHROMA_M], options.baBehav[ADDSET_CAT_HUE],options.baBehav[ADDSET_CAT_DEGREEOUT], options.baBehav[ADDSET_CAT_TEMPOUT] ); rotate->setAdjusterBehavior (options.baBehav[ADDSET_ROTATE_DEGREE]); resize->setAdjusterBehavior (options.baBehav[ADDSET_RESIZE_SCALE]); + framing->setAdjusterBehavior ( + options.baBehav[ADDSET_FRAMING_RELATIVE_SCALE], + options.baBehav[ADDSET_FRAMING_BORDER_RED], + options.baBehav[ADDSET_FRAMING_BORDER_GREEN], + options.baBehav[ADDSET_FRAMING_BORDER_BLUE]); distortion->setAdjusterBehavior ( options.baBehav[ADDSET_DIST_AMOUNT], options.baBehav[ADDSET_DIST_FOCAL_LENGTH] @@ -324,6 +330,10 @@ void BatchToolPanelCoordinator::initSession () if (options.baBehav[ADDSET_DEHAZE_STRENGTH]) { pparams.dehaze.strength = 0; } if (options.baBehav[ADDSET_ROTATE_DEGREE]) { pparams.rotate.degree = 0; } if (options.baBehav[ADDSET_RESIZE_SCALE]) { pparams.resize.scale = 0; } + if (options.baBehav[ADDSET_FRAMING_RELATIVE_SCALE]) { pparams.framing.relativeBorderSize = 0; } + if (options.baBehav[ADDSET_FRAMING_BORDER_RED]) { pparams.framing.borderRed = 0; } + if (options.baBehav[ADDSET_FRAMING_BORDER_GREEN]) { pparams.framing.borderGreen = 0; } + if (options.baBehav[ADDSET_FRAMING_BORDER_BLUE]) { pparams.framing.borderBlue = 0; } if (options.baBehav[ADDSET_DIST_AMOUNT]) { pparams.distortion.amount = 0; } if (options.baBehav[ADDSET_PERSPECTIVE]) { pparams.perspective.horizontal = pparams.perspective.vertical = 0; } if (options.baBehav[ADDSET_PERSP_CAM_FOCAL_LENGTH]) { pparams.perspective.camera_focal_length = pparams.perspective.camera_crop_factor = 0; } @@ -467,9 +477,13 @@ void BatchToolPanelCoordinator::panelChanged(const rtengine::ProcEvent& event, c crop->write (&pparams, &pparamsEdited); resize->update (pparams.crop.enabled, pparams.crop.w, pparams.crop.h, w, h); resize->write (&pparams, &pparamsEdited); + framing->update (w, h); + framing->write (&pparams, &pparamsEdited); } else if (event == rtengine::EvCrop) { resize->update (pparams.crop.enabled, pparams.crop.w, pparams.crop.h); resize->write (&pparams, &pparamsEdited); + framing->update (w, h); + framing->write (&pparams, &pparamsEdited); } } else { // Compensate rotation on flip diff --git a/rtgui/colorpreview.cc b/rtgui/colorpreview.cc new file mode 100644 index 000000000..97fd53252 --- /dev/null +++ b/rtgui/colorpreview.cc @@ -0,0 +1,69 @@ +/* + * This file is part of RawTherapee. + * + * Copyright (c) 2004-2010 Gabor Horvath + * + * RawTherapee is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * RawTherapee is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with RawTherapee. If not, see . + * + * 2024-2024 Daniel Gao + */ + +#include "colorpreview.h" + +#include "rtscalable.h" + +ColorPreview::ColorPreview() : color_red(1.0), color_green(1.0), color_blue(1.0) +{ +} + +void ColorPreview::setRgb(double r, double g, double b) +{ + color_red = r; + color_green = g; + color_blue = b; + + queue_draw(); +} + +bool ColorPreview::on_draw(const Cairo::RefPtr& cr) +{ + cr->set_source_rgb(color_red, color_green, color_blue); + cr->paint(); + + return true; +} + +void ColorPreview::get_preferred_height_vfunc(int& minimum_height, int& natural_height) const +{ + minimum_height = RTScalable::scalePixelSize(10); + natural_height = RTScalable::scalePixelSize(100); +} + +void ColorPreview::get_preferred_width_vfunc(int& minimum_width, int& natural_width) const +{ + minimum_width = RTScalable::scalePixelSize(10); + natural_width = RTScalable::scalePixelSize(100); +} + +void ColorPreview::get_preferred_height_for_width_vfunc(int width, int& minimum_height, + int& natural_height) const +{ + get_preferred_height_vfunc(minimum_height, natural_height); +} + +void ColorPreview::get_preferred_width_for_height_vfunc(int height, int& minimum_width, + int& natural_width) const +{ + get_preferred_width_vfunc(minimum_width, natural_width); +} diff --git a/rtgui/colorpreview.h b/rtgui/colorpreview.h new file mode 100644 index 000000000..755d5fae1 --- /dev/null +++ b/rtgui/colorpreview.h @@ -0,0 +1,53 @@ +/* + * This file is part of RawTherapee. + * + * Copyright (c) 2004-2010 Gabor Horvath + * + * RawTherapee is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * RawTherapee is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with RawTherapee. If not, see . + * + * 2024-2024 Daniel Gao + */ + +#pragma once + +#include + +/** + * This widget displays a singular color as its contents. + */ +class ColorPreview : public Gtk::DrawingArea +{ +public: + ColorPreview(); + + // Values between 0.0 and 1.0 as in + // Cairo::Context::set_source_rgb() + void setRgb(double r, double g, double b); + + // Gtk::DrawingArea + bool on_draw(const Cairo::RefPtr& cr) override; + + // Gtk::Widget + void get_preferred_height_vfunc(int& minimum_height, int& natural_height) const override; + void get_preferred_width_vfunc(int& minimum_width, int& natural_width) const override; + void get_preferred_height_for_width_vfunc(int width, int& minimum_height, + int& natural_height) const override; + void get_preferred_width_for_height_vfunc(int height, int & minimum_width, + int& natural_width) const override; + +private: + double color_red; + double color_green; + double color_blue; +}; diff --git a/rtgui/crop.cc b/rtgui/crop.cc index 0bbbdbf1f..46aef2219 100644 --- a/rtgui/crop.cc +++ b/rtgui/crop.cc @@ -20,6 +20,7 @@ #include "crop.h" +#include "aspectratios.h" #include "options.h" #include "rtimage.h" @@ -57,37 +58,10 @@ public: CropRatios() : ratios{ {M("GENERAL_ASIMAGE"), 0.0}, - {M("GENERAL_CURRENT"), -1.0}, - {"3:2", 3.0 / 2.0}, // L1.5, P0.666... - {"4:3", 4.0 / 3.0}, // L1.333..., P0.75 - {"16:9", 16.0 / 9.0}, // L1.777..., P0.5625 - {"16:10", 16.0 / 10.0}, // L1.6, P0.625 - {"1:1", 1.0 / 1.0}, // L1, P1 - {"2:1", 2.0 / 1.0}, // L2, P0.5 - {"3:1", 3.0 / 1.0}, // L3, P0.333... - {"4:1", 4.0 / 1.0}, // L4, P0.25 - {"5:1", 5.0 / 1.0}, // L5, P0.2 - {"6:1", 6.0 / 1.0}, // L6, P0.1666... - {"7:1", 7.0 / 1.0}, // L7, P0.142... - {"4:5", 4.0 / 5.0}, // L1.25, P0.8 - {"5:7", 5.0 / 7.0}, // L1.4, P0.714... - {"6:7", 6.0 / 7.0}, // L1.166..., P0.857... - {"6:17", 6.0 / 17.0}, // L2.833..., P0.352... - {"24:65 - XPAN", 24.0 / 65.0}, // L2.708..., P0.369... - {"1.414 - DIN EN ISO 216", 1.414}, // L1.414, P0.707... - {"3.5:5", 3.5 / 5.0}, // L1.428..., P0.7 - {"8.5:11 - US Letter", 8.5 / 11.0}, // L1.294..., P0.772... - {"9.5:12", 9.5 / 12.0}, // L1.263..., P0.791... - {"10:12", 10.0 / 12.0}, // L1.2, P0.833... - {"11:14", 11.0 / 14.0}, // L1.272..., P0.785... - {"11:17 - Tabloid", 11.0 / 17.0}, // L1.545..., P0.647... - {"13:19", 13.0 / 19.0}, // L1.461..., P0.684... - {"17:22", 17.0 / 22.0}, // L1.294..., P0.772... - {"45:35 - ePassport", 45.0 / 35.0}, // L1.285,... P0.777... - {"64:27", 64.0 / 27.0}, // L2.370..., P0.421... - {"13:18", 13.0 / 18.0}, // L1.384..., P0.722... + {M("GENERAL_CURRENT"), -1.0} } { + fillAspectRatios(ratios); } std::vector getLabels() const @@ -117,12 +91,7 @@ public: } private: - struct CropRatio { - Glib::ustring label; - double value; - }; - - std::vector ratios; + std::vector ratios; }; Crop::Crop(): diff --git a/rtgui/framing.cc b/rtgui/framing.cc new file mode 100644 index 000000000..20cf194bc --- /dev/null +++ b/rtgui/framing.cc @@ -0,0 +1,1093 @@ +/* + * This file is part of RawTherapee. + * + * Copyright (c) 2004-2010 Gabor Horvath + * + * RawTherapee is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * RawTherapee is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with RawTherapee. If not, see . + * + * 2024-2024 Daniel Gao + */ + +#include "framing.h" + +#include "aspectratios.h" +#include "colorpreview.h" +#include "eventmapper.h" +#include "paramsedited.h" +#include "resize.h" + +#include "../rtengine/color.h" +#include "../rtengine/procparams.h" + +#include +#include +#include + +namespace +{ + +using namespace rtengine; +using rtengine::procparams::FramingParams; + +constexpr int EMPTY_COMBO_INDEX = -1; + +// Framing method combo box data +constexpr int INDEX_STANDARD = 0; +constexpr int INDEX_BBOX = 1; +constexpr int INDEX_FIXED = 2; +constexpr int INDEX_FRAMING_METHOD_UNCHANGED = 3; +constexpr std::array FRAMING_METHODS = { + "TP_FRAMING_METHOD_STANDARD", + "TP_FRAMING_METHOD_BBOX", + "TP_FRAMING_METHOD_FIXED" +}; + +int mapFramingMethod(FramingParams::FramingMethod framingMethod) +{ + using FramingMethod = FramingParams::FramingMethod; + switch (framingMethod) { + case FramingMethod::STANDARD: + return INDEX_STANDARD; + case FramingMethod::BBOX: + return INDEX_BBOX; + case FramingMethod::FIXED_SIZE: + return INDEX_FIXED; + default: + return INDEX_STANDARD; + } +} + +FramingParams::FramingMethod mapFramingMethod(int comboIndex) +{ + using FramingMethod = FramingParams::FramingMethod; + switch (comboIndex) { + case INDEX_STANDARD: + return FramingMethod::STANDARD; + case INDEX_BBOX: + return FramingMethod::BBOX; + case INDEX_FIXED: + return FramingMethod::FIXED_SIZE; + default: + return FramingMethod::STANDARD; + } +} + +// Orientation combo box data +constexpr int INDEX_AS_IMAGE = 0; +constexpr int INDEX_LANDSCAPE = 1; +constexpr int INDEX_PORTRAIT = 2; +constexpr int INDEX_ORIENTATION_UNCHANGED = 3; +constexpr std::array ORIENTATION = { + "GENERAL_ASIMAGE", + "GENERAL_LANDSCAPE", + "GENERAL_PORTRAIT" +}; + +int mapOrientation(FramingParams::Orientation orientation) +{ + using Orientation = FramingParams::Orientation; + switch (orientation) { + case Orientation::AS_IMAGE: + return INDEX_AS_IMAGE; + case Orientation::LANDSCAPE: + return INDEX_LANDSCAPE; + case Orientation::PORTRAIT: + return INDEX_PORTRAIT; + default: + return INDEX_AS_IMAGE; + } +} + +FramingParams::Orientation mapOrientation(int comboIndex) +{ + using Orientation = FramingParams::Orientation; + switch (comboIndex) { + case INDEX_AS_IMAGE: + return Orientation::AS_IMAGE; + case INDEX_LANDSCAPE: + return Orientation::LANDSCAPE; + case INDEX_PORTRAIT: + return Orientation::PORTRAIT; + default: + return Orientation::AS_IMAGE; + } +} + +// Border sizing method combo box data +constexpr int INDEX_SIZE_RELATIVE = 0; +constexpr int INDEX_SIZE_UNIFORM_RELATIVE = 1; +constexpr int INDEX_SIZE_ABSOLUTE = 2; +constexpr int INDEX_SIZE_UNCHANGED = 3; +constexpr std::array BORDER_SIZE_METHODS = { + "TP_FRAMING_BORDER_SIZE_RELATIVE", + "TP_FRAMING_BORDER_SIZE_UNIFORM_RELATIVE", + "TP_FRAMING_BORDER_SIZE_ABSOLUTE" +}; + +int mapBorderSizeMethod(FramingParams::BorderSizing sizing) +{ + using BorderSizing = FramingParams::BorderSizing; + switch (sizing) { + case BorderSizing::PERCENTAGE: + return INDEX_SIZE_RELATIVE; + case BorderSizing::UNIFORM_PERCENTAGE: + return INDEX_SIZE_UNIFORM_RELATIVE; + case BorderSizing::FIXED_SIZE: + return INDEX_SIZE_ABSOLUTE; + default: + return INDEX_SIZE_RELATIVE; + } +} + +FramingParams::BorderSizing mapBorderSizeMethod(int comboIndex) +{ + using BorderSizing = FramingParams::BorderSizing; + switch (comboIndex) { + case INDEX_SIZE_RELATIVE: + return BorderSizing::PERCENTAGE; + case INDEX_SIZE_UNIFORM_RELATIVE: + return BorderSizing::UNIFORM_PERCENTAGE; + case INDEX_SIZE_ABSOLUTE: + return BorderSizing::FIXED_SIZE; + default: + return BorderSizing::PERCENTAGE; + } +} + +// Relative sizing basis combo box data +constexpr int INDEX_BASIS_AUTO = 0; +constexpr int INDEX_BASIS_WIDTH = 1; +constexpr int INDEX_BASIS_HEIGHT = 2; +constexpr int INDEX_BASIS_LONG = 3; +constexpr int INDEX_BASIS_SHORT = 4; +constexpr int INDEX_BASIS_UNCHANGED = 5; +constexpr std::array BORDER_SIZE_BASIS = { + "TP_FRAMING_BASIS_AUTO", + "TP_FRAMING_BASIS_WIDTH", + "TP_FRAMING_BASIS_HEIGHT", + "TP_FRAMING_BASIS_LONG_SIDE", + "TP_FRAMING_BASIS_SHORT_SIDE" +}; + +int mapBasis(FramingParams::Basis basis) +{ + using Basis = FramingParams::Basis; + switch(basis) { + case Basis::AUTO: + return INDEX_BASIS_AUTO; + case Basis::WIDTH: + return INDEX_BASIS_WIDTH; + case Basis::HEIGHT: + return INDEX_BASIS_HEIGHT; + case Basis::LONG: + return INDEX_BASIS_LONG; + case Basis::SHORT: + return INDEX_BASIS_SHORT; + default: + return INDEX_BASIS_AUTO; + } +} + +FramingParams::Basis mapBasis(int comboIndex) +{ + using Basis = FramingParams::Basis; + switch(comboIndex) { + case INDEX_BASIS_AUTO: + return Basis::AUTO; + case INDEX_BASIS_WIDTH: + return Basis::WIDTH; + case INDEX_BASIS_HEIGHT: + return Basis::HEIGHT; + case INDEX_BASIS_LONG: + return Basis::LONG; + case INDEX_BASIS_SHORT: + return Basis::SHORT; + default: + return Basis::AUTO; + } +} + +constexpr int INITIAL_IMG_WIDTH = 100000; +constexpr int INITIAL_IMG_HEIGHT = 100000; +constexpr int MAX_COLOR_VAL = 255; + +constexpr int ROW_SPACING = 4; +constexpr float FRAME_LABEL_ALIGN_X = 0.025; +constexpr float FRAME_LABEL_ALIGN_Y = 0.5; + +Gtk::Label* createGridLabel(const char* text) +{ + Gtk::Label* label = Gtk::manage(new Gtk::Label(M(text))); + label->set_halign(Gtk::ALIGN_START); + return label; +} + +MySpinButton* createSpinButton() +{ + MySpinButton* button = Gtk::manage(new MySpinButton()); + button->set_width_chars(5); + button->set_digits(0); + button->set_increments(1, 100); + setExpandAlignProperties(button, false, false, Gtk::ALIGN_END, Gtk::ALIGN_CENTER); + return button; +} + +} // namespace + +const Glib::ustring Framing::TOOL_NAME = "framing"; + +class Framing::AspectRatios +{ +public: + static constexpr int INDEX_CURRENT = 0; + + AspectRatios() : + ratios{{M("GENERAL_ASIMAGE")}} + { + fillAspectRatios(ratios); + } + + void fillCombo(MyComboBoxText* combo) const + { + for (const auto& aspectRatio : ratios) { + combo->append(aspectRatio.label); + } + combo->set_active(INDEX_CURRENT); + } + + int unchangedIndex() const { return ratios.size(); } + + double value(int index) const + { + return ratios.at(index).value; + } + + int findIndex(double aspectRatio) const + { + if (aspectRatio == FramingParams::AS_IMAGE_ASPECT_RATIO) return INDEX_CURRENT; + + for (size_t i = 1; i < ratios.size(); i++) { + if (ratios[i].value == aspectRatio) return i; + } + + // Couldn't find a matching value + return INDEX_CURRENT; + } + +private: + std::vector ratios; +}; + +Framing::DimensionGui::DimensionGui(Gtk::Box* parent, const char* text) +{ + box = Gtk::manage(new Gtk::Box()); + Gtk::Label* label = Gtk::manage(new Gtk::Label(M(text))); + setExpandAlignProperties(label, false, false, Gtk::ALIGN_START, Gtk::ALIGN_CENTER); + value = createSpinButton(); + box->pack_start(*label); + box->pack_start(*value); + parent->pack_start(*box); +} + +void Framing::DimensionGui::connect(Framing& framing, CallbackFunc callback) +{ + connection = value->signal_value_changed().connect(sigc::mem_fun(framing, callback), true); +} + +Framing::Framing() : + FoldableToolPanel(this, TOOL_NAME, M("TP_FRAMING_LABEL"), false, true), + aspectRatioData(new AspectRatios), + imgWidth(INITIAL_IMG_WIDTH), + imgHeight(INITIAL_IMG_HEIGHT), + lastAllowUpscaling(false), + lastMinSizeEnabled(false) +{ + setupEvents(); + setupFramingMethodGui(); + pack_start(*Gtk::manage(new Gtk::Separator(Gtk::ORIENTATION_HORIZONTAL))); + setupBorderSizeGui(); + pack_start(*Gtk::manage(new Gtk::Separator(Gtk::ORIENTATION_HORIZONTAL))); + setupBorderColorsGui(); +} + +Framing::~Framing() { + idleRegister.destroy(); +} + +void Framing::setupEvents() +{ + auto m = ProcEventMapper::getInstance(); + + // clang-format off + EvFramingEnabled = m->newEvent(RESIZE, "HISTORY_MSG_FRAMING_ENABLED"); + EvFramingMethod = m->newEvent(RESIZE, "HISTORY_MSG_FRAMING_METHOD"); + EvFramingAspectRatio = m->newEvent(RESIZE, "HISTORY_MSG_FRAMING_ASPECT_RATIO"); + EvFramingOrientation = m->newEvent(RESIZE, "HISTORY_MSG_FRAMING_ORIENTATION"); + EvFramingFramedWidth = m->newEvent(RESIZE, "HISTORY_MSG_FRAMING_FRAMED_WIDTH"); + EvFramingFramedHeight = m->newEvent(RESIZE, "HISTORY_MSG_FRAMING_FRAMED_HEIGHT"); + EvFramingAllowUpscaling = m->newEvent(RESIZE, "HISTORY_MSG_FRAMING_ALLOW_UPSCALING"); + EvFramingBorderSizingMethod = m->newEvent(RESIZE, "HISTORY_MSG_FRAMING_BORDER_SIZE_METHOD"); + EvFramingBasis = m->newEvent(RESIZE, "HISTORY_MSG_FRAMING_BASIS"); + EvFramingRelativeBorderSize = m->newEvent(RESIZE, "HISTORY_MSG_FRAMING_BORDER_SIZE"); + EvFramingMinSizeEnabled = m->newEvent(RESIZE, "HISTORY_MSG_FRAMING_MIN_SIZE_ENABLED"); + EvFramingMinWidth = m->newEvent(RESIZE, "HISTORY_MSG_FRAMING_MIN_WIDTH"); + EvFramingMinHeight = m->newEvent(RESIZE, "HISTORY_MSG_FRAMING_MIN_HEIGHT"); + EvFramingAbsWidth = m->newEvent(RESIZE, "HISTORY_MSG_FRAMING_ABSOLUTE_WIDTH"); + EvFramingAbsHeight = m->newEvent(RESIZE, "HISTORY_MSG_FRAMING_ABSOLUTE_HEIGHT"); + EvFramingBorderRed = m->newEvent(RESIZE, "HISTORY_MSG_FRAMING_BORDER_RED"); + EvFramingBorderGreen = m->newEvent(RESIZE, "HISTORY_MSG_FRAMING_BORDER_GREEN"); + EvFramingBorderBlue = m->newEvent(RESIZE, "HISTORY_MSG_FRAMING_BORDER_BLUE"); + // clang-format on +} + +void Framing::setupFramingMethodGui() +{ + Gtk::Grid* combos = Gtk::manage(new Gtk::Grid()); + combos->set_row_spacing(ROW_SPACING); + + framingMethod = Gtk::manage(new MyComboBoxText()); + for (auto label : FRAMING_METHODS) { + framingMethod->append(M(label)); + } + framingMethod->set_active(INDEX_STANDARD); + framingMethod->set_hexpand(); + framingMethod->set_halign(Gtk::ALIGN_FILL); + + combos->attach(*createGridLabel("TP_FRAMING_METHOD"), 0, 0); + combos->attach(*framingMethod, 1, 0); + + aspectRatioLabel = createGridLabel("TP_FRAMING_ASPECT_RATIO"); + aspectRatio = Gtk::manage(new MyComboBoxText()); + aspectRatioData->fillCombo(aspectRatio); + aspectRatio->set_hexpand(); + aspectRatio->set_halign(Gtk::ALIGN_FILL); + + combos->attach(*aspectRatioLabel, 0, 1); + combos->attach(*aspectRatio, 1, 1); + + orientationLabel = createGridLabel("TP_FRAMING_ORIENTATION"); + orientation = Gtk::manage(new MyComboBoxText()); + for (auto label : ORIENTATION) { + orientation->append(M(label)); + } + orientation->set_active(INDEX_AS_IMAGE); + orientation->set_hexpand(); + orientation->set_halign(Gtk::ALIGN_FILL); + + combos->attach(*orientationLabel, 0, 2); + combos->attach(*orientation, 1, 2); + pack_start(*combos); + + width = DimensionGui(this, "TP_FRAMING_FRAMED_WIDTH"); + width.setRange(Resize::MIN_SIZE, Resize::MAX_SCALE * imgWidth); + width.setValue(imgWidth); + height = DimensionGui(this, "TP_FRAMING_FRAMED_HEIGHT"); + height.setRange(Resize::MIN_SIZE, Resize::MAX_SCALE * imgHeight); + height.setValue(imgHeight); + + allowUpscaling = Gtk::manage(new Gtk::CheckButton(M("TP_FRAMING_ALLOW_UPSCALING"))); + pack_start(*allowUpscaling); + + updateFramingMethodGui(); + + framingMethodChanged = framingMethod->signal_changed().connect( + sigc::mem_fun(*this, &Framing::onFramingMethodChanged)); + aspectRatioChanged = aspectRatio->signal_changed().connect( + sigc::mem_fun(*this, &Framing::onAspectRatioChanged)); + orientationChanged = orientation->signal_changed().connect( + sigc::mem_fun(*this, &Framing::onOrientationChanged)); + width.connect(*this, &Framing::onWidthChanged); + height.connect(*this, &Framing::onHeightChanged); + allowUpscalingConnection = allowUpscaling->signal_toggled().connect( + sigc::mem_fun(*this, &Framing::onAllowUpscalingToggled)); +} + +void Framing::setupBorderSizeGui() +{ + Gtk::Grid* combos = Gtk::manage(new Gtk::Grid()); + combos->set_row_spacing(ROW_SPACING); + + borderSizeMethod = Gtk::manage(new MyComboBoxText()); + for (auto label : BORDER_SIZE_METHODS) { + borderSizeMethod->append(M(label)); + } + borderSizeMethod->set_active(INDEX_SIZE_RELATIVE); + borderSizeMethod->set_hexpand(); + borderSizeMethod->set_halign(Gtk::ALIGN_FILL); + + combos->attach(*createGridLabel("TP_FRAMING_BORDER_SIZE_METHOD"), 0, 0); + combos->attach(*borderSizeMethod, 1, 0); + + basisLabel = createGridLabel("TP_FRAMING_BASIS"); + basis = Gtk::manage(new MyComboBoxText()); + for (auto label : BORDER_SIZE_BASIS) { + basis->append(M(label)); + } + basis->set_active(INDEX_BASIS_AUTO); + basis->set_hexpand(); + basis->set_halign(Gtk::ALIGN_FILL); + + combos->attach(*basisLabel, 0, 1); + combos->attach(*basis, 1, 1); + + pack_start(*combos); + + relativeBorderSize = Gtk::manage(new Adjuster(M("TP_FRAMING_BORDER_SIZE"), 0, 1, 0.01, 0.1)); + pack_start(*relativeBorderSize); + + minSizeFrame = Gtk::manage(new Gtk::Frame()); + minSizeFrame->set_label_align(FRAME_LABEL_ALIGN_X, FRAME_LABEL_ALIGN_Y); + minSizeEnabled = Gtk::manage(new Gtk::CheckButton(M("TP_FRAMING_LIMIT_MINIMUM"))); + minSizeFrame->set_label_widget(*minSizeEnabled); + + minSizeFrameContent = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL)); + + minWidth = DimensionGui(minSizeFrameContent, "TP_FRAMING_MIN_WIDTH"); + minWidth.setRange(0, imgWidth); + minWidth.setValue(0); + minHeight = DimensionGui(minSizeFrameContent, "TP_FRAMING_MIN_HEIGHT"); + minHeight.setRange(0, imgHeight); + minHeight.setValue(0); + + minSizeFrame->add(*minSizeFrameContent); + pack_start(*minSizeFrame); + + absWidth = DimensionGui(this, "TP_FRAMING_ABSOLUTE_WIDTH"); + absWidth.setRange(0, imgWidth); + absWidth.setValue(0); + absHeight = DimensionGui(this, "TP_FRAMING_ABSOLUTE_HEIGHT"); + absHeight.setRange(0, imgHeight); + absHeight.setValue(0); + + updateBorderSizeGui(); + + borderSizeMethodChanged = borderSizeMethod->signal_changed().connect( + sigc::mem_fun(*this, &Framing::onBorderSizeMethodChanged)); + basisChanged = basis->signal_changed().connect( + sigc::mem_fun(*this, &Framing::onBasisChanged)); + relativeBorderSize->setAdjusterListener(this); + minSizeEnabledConnection = minSizeEnabled->signal_toggled().connect( + sigc::mem_fun(*this, &Framing::onMinSizeToggled)); + minWidth.connect(*this, &Framing::onMinWidthChanged); + minHeight.connect(*this, &Framing::onMinHeightChanged); + absWidth.connect(*this, &Framing::onAbsWidthChanged); + absHeight.connect(*this, &Framing::onAbsHeightChanged); +} + +void Framing::setupBorderColorsGui() +{ + Gtk::Frame* const frame = Gtk::manage(new Gtk::Frame()); + + Gtk::Label* const label = Gtk::manage(new Gtk::Label(M("TP_FRAMING_BORDER_COLOR"))); + frame->set_label_align(FRAME_LABEL_ALIGN_X, FRAME_LABEL_ALIGN_Y); + frame->set_label_widget(*label); + + Gtk::Box* const box = Gtk::manage(new Gtk::Box(Gtk::ORIENTATION_VERTICAL)); + redAdj = Gtk::manage(new Adjuster(M("TP_FRAMING_RED"), 0, MAX_COLOR_VAL, 1, MAX_COLOR_VAL)); + box->add(*redAdj); + greenAdj = Gtk::manage(new Adjuster(M("TP_FRAMING_GREEN"), 0, MAX_COLOR_VAL, 1, MAX_COLOR_VAL)); + box->add(*greenAdj); + blueAdj = Gtk::manage(new Adjuster(M("TP_FRAMING_BLUE"), 0, MAX_COLOR_VAL, 1, MAX_COLOR_VAL)); + box->add(*blueAdj); + + Gtk::Frame* const colorFrame = Gtk::manage(new Gtk::Frame()); + colorPreview = Gtk::manage(new ColorPreview()); + colorFrame->add(*colorPreview); + box->add(*colorFrame); + + frame->add(*box); + pack_start(*frame); + + updateBorderColorGui(); + + redAdj->setAdjusterListener(this); + greenAdj->setAdjusterListener(this); + blueAdj->setAdjusterListener(this); +} + +void Framing::read(const rtengine::procparams::ProcParams* pp, const ParamsEdited* pedited) +{ + DisableListener disableListener(this); + + std::array blockers = { + ConnectionBlocker(framingMethodChanged), + ConnectionBlocker(aspectRatioChanged), + ConnectionBlocker(orientationChanged), + ConnectionBlocker(width.connection), + ConnectionBlocker(height.connection), + ConnectionBlocker(allowUpscalingConnection), + ConnectionBlocker(borderSizeMethodChanged), + ConnectionBlocker(basisChanged), + ConnectionBlocker(minSizeEnabledConnection), + ConnectionBlocker(minWidth.connection), + ConnectionBlocker(minHeight.connection), + ConnectionBlocker(absWidth.connection), + ConnectionBlocker(absHeight.connection) + }; + + BlockAdjusterEvents blockRelative(relativeBorderSize); + BlockAdjusterEvents blockRed(redAdj); + BlockAdjusterEvents blockGreen(greenAdj); + BlockAdjusterEvents blockBlue(blueAdj); + + readParams(pp); + readEdited(pedited); + + updateFramingMethodGui(); + updateBorderSizeGui(); + updateBorderColorGui(); + setDimensions(); +} + +void Framing::readParams(const rtengine::procparams::ProcParams* pp) +{ + const rtengine::procparams::FramingParams& params = pp->framing; + + setEnabled(params.enabled); + + framingMethod->set_active(mapFramingMethod(params.framingMethod)); + aspectRatio->set_active(aspectRatioData->findIndex(params.aspectRatio)); + orientation->set_active(mapOrientation(params.orientation)); + width.setValue(params.framedWidth); + width.isDirty = false; + height.setValue(params.framedHeight); + height.isDirty = false; + allowUpscaling->set_active(params.allowUpscaling); + lastAllowUpscaling = params.allowUpscaling; + + borderSizeMethod->set_active(mapBorderSizeMethod(params.borderSizingMethod)); + basis->set_active(mapBasis(params.basis)); + relativeBorderSize->setValue(params.relativeBorderSize); + minSizeEnabled->set_active(params.minSizeEnabled); + lastMinSizeEnabled = params.minSizeEnabled; + minWidth.setValue(params.minWidth); + minWidth.isDirty = false; + minHeight.setValue(params.minHeight); + minHeight.isDirty = false; + absWidth.setValue(params.absWidth); + absWidth.isDirty = false; + absHeight.setValue(params.absHeight); + absHeight.isDirty = false; + + redAdj->setValue(params.borderRed); + greenAdj->setValue(params.borderGreen); + blueAdj->setValue(params.borderBlue); +} + +void Framing::readEdited(const ParamsEdited* pedited) +{ + if (!pedited) return; + + const FramingParamsEdited& edits = pedited->framing; + + set_inconsistent(multiImage && !edits.enabled); + + if (!edits.framingMethod) { + framingMethod->set_active(EMPTY_COMBO_INDEX); + } + if (!edits.aspectRatio) { + aspectRatio->set_active(EMPTY_COMBO_INDEX); + } + if (!edits.orientation) { + orientation->set_active(EMPTY_COMBO_INDEX); + } + width.isDirty = edits.framedWidth; + height.isDirty = edits.framedHeight; + allowUpscaling->set_inconsistent(edits.allowUpscaling); + + if (!edits.borderSizingMethod) { + borderSizeMethod->set_active(EMPTY_COMBO_INDEX); + } + if (!edits.basis) { + basis->set_active(EMPTY_COMBO_INDEX); + } + relativeBorderSize->setEditedState(edits.relativeBorderSize ? Edited : UnEdited); + minSizeEnabled->set_inconsistent(edits.minSizeEnabled); + minWidth.isDirty = edits.minWidth; + minHeight.isDirty = edits.minHeight; + absWidth.isDirty = edits.absWidth; + absHeight.isDirty = edits.absHeight; + + redAdj->setEditedState(edits.borderRed ? Edited : UnEdited); + greenAdj->setEditedState(edits.borderGreen ? Edited : UnEdited); + blueAdj->setEditedState(edits.borderBlue ? Edited : UnEdited); +} + +void Framing::write(rtengine::procparams::ProcParams* pp, ParamsEdited* pedited) +{ + writeParams(pp); + writeEdited(pedited); +} + +void Framing::writeParams(rtengine::procparams::ProcParams* pp) +{ + rtengine::procparams::FramingParams& params = pp->framing; + + params.enabled = getEnabled(); + + params.framingMethod = mapFramingMethod(framingMethod->get_active_row_number()); + params.aspectRatio = aspectRatioData->value(aspectRatio->get_active_row_number()); + params.orientation = mapOrientation(orientation->get_active_row_number()); + params.framedWidth = width.value->get_value_as_int(); + params.framedHeight = height.value->get_value_as_int(); + params.allowUpscaling = allowUpscaling->get_active(); + + params.borderSizingMethod = mapBorderSizeMethod(borderSizeMethod->get_active_row_number()); + params.basis = mapBasis(basis->get_active_row_number()); + params.relativeBorderSize = relativeBorderSize->getValue(); + params.minSizeEnabled = minSizeEnabled->get_active(); + params.minWidth = minWidth.value->get_value_as_int(); + params.minHeight = minHeight.value->get_value_as_int(); + params.absWidth = absWidth.value->get_value_as_int(); + params.absHeight = absHeight.value->get_value_as_int(); + + params.borderRed = redAdj->getValue(); + params.borderGreen = greenAdj->getValue(); + params.borderBlue = blueAdj->getValue(); +} + +void Framing::writeEdited(ParamsEdited* pedited) +{ + if (!pedited) return; + + FramingParamsEdited& edits = pedited->framing; + + edits.enabled = !get_inconsistent(); + + edits.framingMethod = framingMethod->get_active_row_number() != INDEX_FRAMING_METHOD_UNCHANGED; + edits.aspectRatio = aspectRatio->get_active_row_number() != aspectRatioData->unchangedIndex(); + edits.orientation = orientation->get_active_row_number() != INDEX_ORIENTATION_UNCHANGED; + edits.framedWidth = width.isDirty; + edits.framedHeight = height.isDirty; + edits.allowUpscaling = !allowUpscaling->get_inconsistent(); + + edits.borderSizingMethod = borderSizeMethod->get_active_row_number() != INDEX_SIZE_UNCHANGED; + edits.basis = basis->get_active_row_number() != INDEX_BASIS_UNCHANGED; + edits.relativeBorderSize = relativeBorderSize->getEditedState(); + edits.minSizeEnabled = !minSizeEnabled->get_inconsistent(); + edits.minWidth = minWidth.isDirty; + edits.minHeight = minHeight.isDirty; + edits.absWidth = absWidth.isDirty; + edits.absHeight = absHeight.isDirty; + + edits.borderRed = redAdj->getEditedState(); + edits.borderGreen = greenAdj->getEditedState(); + edits.borderBlue = blueAdj->getEditedState(); +} + +void Framing::setDefaults(const rtengine::procparams::ProcParams* defParams, const ParamsEdited* pedited) +{ + const FramingParams& params = defParams->framing; + + relativeBorderSize->setDefault(params.relativeBorderSize); + redAdj->setDefault(params.borderRed); + greenAdj->setDefault(params.borderGreen); + blueAdj->setDefault(params.borderBlue); + + if (pedited) { + const FramingParamsEdited& edits = pedited->framing; + + relativeBorderSize->setDefaultEditedState(edits.relativeBorderSize ? Edited : UnEdited); + redAdj->setDefaultEditedState(edits.borderRed ? Edited : UnEdited); + greenAdj->setDefaultEditedState(edits.borderGreen ? Edited : UnEdited); + blueAdj->setDefaultEditedState(edits.borderBlue ? Edited : UnEdited); + } else { + relativeBorderSize->setDefaultEditedState(Irrelevant); + redAdj->setDefaultEditedState(Irrelevant); + greenAdj->setDefaultEditedState(Irrelevant); + blueAdj->setDefaultEditedState(Irrelevant); + } +} + +void Framing::trimValues(rtengine::procparams::ProcParams* pp) +{ + relativeBorderSize->trimValue(pp->framing.relativeBorderSize); + redAdj->trimValue(pp->framing.borderRed); + greenAdj->trimValue(pp->framing.borderGreen); + blueAdj->trimValue(pp->framing.borderBlue); +} + +void Framing::setBatchMode(bool batchMode) +{ + framingMethod->append(M("GENERAL_UNCHANGED")); + aspectRatio->append(M("GENERAL_UNCHANGED")); + orientation->append(M("GENERAL_UNCHANGED")); + borderSizeMethod->append(M("GENERAL_UNCHANGED")); + basis->append(M("GENERAL_UNCHANGED")); + + ToolPanel::setBatchMode(batchMode); + relativeBorderSize->showEditedCB(); + redAdj->showEditedCB(); + greenAdj->showEditedCB(); + blueAdj->showEditedCB(); +} + +void Framing::enabledChanged() +{ + if (listener) { + if (get_inconsistent()) { + listener->panelChanged(EvFramingEnabled, M("GENERAL_UNCHANGED")); + } else if (getEnabled()) { + listener->panelChanged(EvFramingEnabled, M("GENERAL_ENABLED")); + } else { + listener->panelChanged(EvFramingEnabled, M("GENERAL_DISABLED")); + } + } +} + +void Framing::update(int originalWidth, int originalHeight) +{ + // This is how it is checked in resize.cc + if (originalWidth && originalHeight) { + imgWidth = originalWidth; + imgHeight = originalHeight; + } +} + +void Framing::setAdjusterBehavior(bool addRelativeBorderSize, bool addRed, bool addGreen, + bool addBlue) +{ + relativeBorderSize->setAddMode(addRelativeBorderSize); + redAdj->setAddMode(addRed); + greenAdj->setAddMode(addGreen); + blueAdj->setAddMode(addBlue); +} + +void Framing::setDimensions() +{ + idleRegister.add([this]() -> bool { + std::array blockers = { + ConnectionBlocker(width.connection), + ConnectionBlocker(height.connection), + ConnectionBlocker(minWidth.connection), + ConnectionBlocker(minHeight.connection), + ConnectionBlocker(absWidth.connection), + ConnectionBlocker(absHeight.connection) + }; + + // 16x the full image size is probably a reasonable max + width.value->set_range(Resize::MIN_SIZE, Resize::MAX_SCALE * imgWidth); + height.value->set_range(Resize::MIN_SIZE, Resize::MAX_SCALE * imgHeight); + minWidth.value->set_range(0, Resize::MAX_SCALE * imgWidth); + minHeight.value->set_range(0, Resize::MAX_SCALE * imgHeight); + absWidth.value->set_range(0, Resize::MAX_SCALE * imgWidth); + absHeight.value->set_range(0, Resize::MAX_SCALE * imgHeight); + + return false; + }); +} + +void Framing::updateFramingMethodGui() +{ + if (batchMode) { + aspectRatioLabel->show(); + aspectRatio->show(); + orientationLabel->show(); + orientation->show(); + width.show(); + height.show(); + allowUpscaling->show(); + return; + } + + int activeRow = framingMethod->get_active_row_number(); + if (activeRow == INDEX_STANDARD) { + aspectRatioLabel->show(); + aspectRatio->show(); + orientationLabel->show(); + orientation->show(); + width.hide(); + height.hide(); + allowUpscaling->hide(); + } else if (activeRow == INDEX_BBOX) { + aspectRatioLabel->show(); + aspectRatio->show(); + orientationLabel->show(); + orientation->show(); + width.show(); + height.show(); + allowUpscaling->show(); + } else if (activeRow == INDEX_FIXED) { + aspectRatioLabel->hide(); + aspectRatio->hide(); + orientationLabel->hide(); + orientation->hide(); + width.show(); + height.show(); + allowUpscaling->show(); + } +} + +void Framing::updateBorderSizeGui() +{ + if (batchMode) { + basisLabel->show(); + basis->show(); + relativeBorderSize->show(); + minSizeFrame->show(); + absWidth.show(); + absHeight.show(); + + aspectRatio->set_sensitive(true); + orientation->set_sensitive(true); + + minSizeFrameContent->set_sensitive(true); + return; + } + + int activeRow = borderSizeMethod->get_active_row_number(); + if (activeRow == INDEX_SIZE_RELATIVE) { + basisLabel->show(); + basis->show(); + relativeBorderSize->show(); + minSizeFrame->show(); + absWidth.hide(); + absHeight.hide(); + + aspectRatio->set_sensitive(true); + orientation->set_sensitive(true); + } else if (activeRow == INDEX_SIZE_UNIFORM_RELATIVE) { + basisLabel->show(); + basis->show(); + relativeBorderSize->show(); + minSizeFrame->show(); + absWidth.hide(); + absHeight.hide(); + + aspectRatio->set_sensitive(false); + orientation->set_sensitive(false); + } else if (activeRow == INDEX_SIZE_ABSOLUTE) { + basisLabel->hide(); + basis->hide(); + relativeBorderSize->hide(); + minSizeFrame->hide(); + absWidth.show(); + absHeight.show(); + + aspectRatio->set_sensitive(false); + orientation->set_sensitive(false); + } + + minSizeFrameContent->set_sensitive(minSizeEnabled->get_active()); +} + +void Framing::updateBorderColorGui() +{ + auto gamma = [](double val) { + // adjuster is [0.0, 255.0] + // gamma2curve expects [0, 65535] + // setRgb expects [0.0, 1.0] + return Color::gamma2curve[val * (MAX_COLOR_VAL + 1)] / 65535.0; + }; + double r = gamma(redAdj->getValue()); + double g = gamma(greenAdj->getValue()); + double b = gamma(blueAdj->getValue()); + colorPreview->setRgb(r, g, b); +} + +void Framing::adjusterChanged(Adjuster* adj, double newVal) +{ + if (adj == redAdj || adj == greenAdj || adj == blueAdj) { + updateBorderColorGui(); + } + + if (listener && (getEnabled() || batchMode)) { + Glib::ustring costr; + if (adj == relativeBorderSize) { + costr = Glib::ustring::format(std::setw(3), std::fixed, std::setprecision(2), + adj->getValue()); + } else { + costr = Glib::ustring::format(static_cast(adj->getValue())); + } + + if (adj == relativeBorderSize) { + listener->panelChanged(EvFramingRelativeBorderSize, costr); + } else if (adj == redAdj) { + listener->panelChanged(EvFramingBorderRed, costr); + } else if (adj == greenAdj) { + listener->panelChanged(EvFramingBorderGreen, costr); + } else if (adj == blueAdj) { + listener->panelChanged(EvFramingBorderBlue, costr); + } + } +} + +void Framing::onFramingMethodChanged() +{ + updateFramingMethodGui(); + + if (listener && (getEnabled() || batchMode)) { + listener->panelChanged(EvFramingMethod, framingMethod->get_active_text()); + } +} + +void Framing::onAspectRatioChanged() +{ + if (listener && (getEnabled() || batchMode)) { + listener->panelChanged(EvFramingAspectRatio, aspectRatio->get_active_text()); + } +} + +void Framing::onOrientationChanged() +{ + if (listener && (getEnabled() || batchMode)) { + listener->panelChanged(EvFramingOrientation, orientation->get_active_text()); + } +} + +void Framing::onWidthChanged() +{ + width.isDirty = true; + + if (listener && (getEnabled() || batchMode)) { + listener->panelChanged(EvFramingFramedWidth, + Glib::ustring::format(width.value->get_value_as_int())); + } +} + +void Framing::onHeightChanged() +{ + height.isDirty = true; + + if (listener && (getEnabled() || batchMode)) { + listener->panelChanged(EvFramingFramedHeight, + Glib::ustring::format(height.value->get_value_as_int())); + } +} + +void Framing::onAllowUpscalingToggled() +{ + if (batchMode) { + if (allowUpscaling->get_inconsistent()) { + allowUpscaling->set_inconsistent(false); + ConnectionBlocker block(allowUpscalingConnection); + allowUpscaling->set_active(false); + } else if (lastAllowUpscaling) { + allowUpscaling->set_inconsistent(true); + } + + lastAllowUpscaling = allowUpscaling->get_active(); + } + + if (listener && (getEnabled() || batchMode)) { + if (allowUpscaling->get_inconsistent()) { + listener->panelChanged(EvFramingAllowUpscaling, M("GENERAL_UNCHANGED")); + } else if (allowUpscaling->get_active()) { + listener->panelChanged(EvFramingAllowUpscaling, M("GENERAL_ENABLED")); + } else { + listener->panelChanged(EvFramingAllowUpscaling, M("GENERAL_DISABLED")); + } + } +} + +void Framing::onBorderSizeMethodChanged() +{ + if (borderSizeMethod->get_active_row_number() == INDEX_SIZE_UNIFORM_RELATIVE) { + ConnectionBlocker block(minHeight.connection); + minHeight.isDirty = true; + minHeight.value->set_value(minWidth.value->get_value_as_int()); + } + + updateBorderSizeGui(); + + if (listener && (getEnabled() || batchMode)) { + listener->panelChanged(EvFramingBorderSizingMethod, borderSizeMethod->get_active_text()); + } +} + +void Framing::onBasisChanged() +{ + if (listener && (getEnabled() || batchMode)) { + listener->panelChanged(EvFramingBasis, basis->get_active_text()); + } +} + +void Framing::onMinSizeToggled() +{ + if (batchMode) { + if (minSizeEnabled->get_inconsistent()) { + minSizeEnabled->set_inconsistent(false); + ConnectionBlocker block(minSizeEnabledConnection); + minSizeEnabled->set_active(false); + } else if (lastMinSizeEnabled) { + minSizeEnabled->set_inconsistent(true); + } + + lastMinSizeEnabled = minSizeEnabled->get_active(); + } + + updateBorderSizeGui(); + + if (listener && (getEnabled() || batchMode)) { + if (minSizeEnabled->get_inconsistent()) { + listener->panelChanged(EvFramingMinSizeEnabled, M("GENERAL_UNCHANGED")); + } else if (minSizeEnabled->get_active()) { + listener->panelChanged(EvFramingMinSizeEnabled, M("GENERAL_ENABLED")); + } else { + listener->panelChanged(EvFramingMinSizeEnabled, M("GENERAL_DISABLED")); + } + } +} + +void Framing::onMinWidthChanged() +{ + minWidth.isDirty = true; + int value = minWidth.value->get_value_as_int(); + + if (borderSizeMethod->get_active_row_number() == INDEX_SIZE_UNIFORM_RELATIVE) { + ConnectionBlocker block(minHeight.connection); + minHeight.isDirty = true; + minHeight.value->set_value(value); + } + + if (listener && (getEnabled() || batchMode)) { + listener->panelChanged(EvFramingMinWidth, Glib::ustring::format(value)); + } +} + +void Framing::onMinHeightChanged() +{ + minHeight.isDirty = true; + int value = minHeight.value->get_value_as_int(); + + if (borderSizeMethod->get_active_row_number() == INDEX_SIZE_UNIFORM_RELATIVE) { + ConnectionBlocker block(minWidth.connection); + minWidth.isDirty = true; + minWidth.value->set_value(value); + } + + if (listener && (getEnabled() || batchMode)) { + listener->panelChanged(EvFramingMinHeight, Glib::ustring::format(value)); + } +} + +void Framing::onAbsWidthChanged() +{ + absWidth.isDirty = true; + + if (listener && (getEnabled() || batchMode)) { + listener->panelChanged(EvFramingAbsWidth, + Glib::ustring::format(absWidth.value->get_value_as_int())); + } +} + +void Framing::onAbsHeightChanged() +{ + absHeight.isDirty = true; + + if (listener && (getEnabled() || batchMode)) { + listener->panelChanged(EvFramingAbsHeight, + Glib::ustring::format(absHeight.value->get_value_as_int())); + } +} diff --git a/rtgui/framing.h b/rtgui/framing.h new file mode 100644 index 000000000..5b186a076 --- /dev/null +++ b/rtgui/framing.h @@ -0,0 +1,179 @@ +/* + * This file is part of RawTherapee. + * + * Copyright (c) 2004-2010 Gabor Horvath + * + * RawTherapee is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * RawTherapee is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with RawTherapee. If not, see . + * + * 2024-2024 Daniel Gao + */ + +#pragma once + +#include "adjuster.h" +#include "guiutils.h" +#include "toolpanel.h" + +#include "../rtengine/procevents.h" + +#include + +class ColorPreview; + +class Framing final : + public ToolParamBlock, + public AdjusterListener, + public FoldableToolPanel +{ +public: + static const Glib::ustring TOOL_NAME; + + Framing(); + ~Framing(); + + // FoldableToolPanel + void read(const rtengine::procparams::ProcParams* pp, + const ParamsEdited* pedited = nullptr) override; + void write(rtengine::procparams::ProcParams* pp, + ParamsEdited* pedited = nullptr) override; + void setDefaults(const rtengine::procparams::ProcParams* defParams, + const ParamsEdited* pedited = nullptr) override; + void trimValues(rtengine::procparams::ProcParams* pp) override; + void setBatchMode(bool batchMode) override; + void enabledChanged() override; + + void update(int originalWidth = 0, int originalHeight = 0); + void setAdjusterBehavior(bool addRelativeBorderSize, bool addRed, bool addGreen, bool addBlue); + + // AdjusterListener + void adjusterChanged(Adjuster* adj, double newVal) override; + + // Signal connections + void onFramingMethodChanged(); + void onAspectRatioChanged(); + void onOrientationChanged(); + void onWidthChanged(); + void onHeightChanged(); + void onAllowUpscalingToggled(); + void onBorderSizeMethodChanged(); + void onBasisChanged(); + void onMinSizeToggled(); + void onMinWidthChanged(); + void onMinHeightChanged(); + void onAbsWidthChanged(); + void onAbsHeightChanged(); + +private: + class AspectRatios; + + // Helper struct for repeated patterns + struct DimensionGui + { + using CallbackFunc = void(Framing::*)(); + + DimensionGui() = default; + DimensionGui(Gtk::Box* parent, const char* text); + + void setValue(int newValue) { value->set_value(newValue); } + void setRange(int min, int max) { value->set_range(min, max); } + void connect(Framing& framing, CallbackFunc callback); + + void show() { box->show(); } + void hide() { box->hide(); } + + Gtk::Box* box; + MySpinButton* value; + sigc::connection connection; + bool isDirty; + }; + + void setupEvents(); + void setupFramingMethodGui(); + void setupBorderSizeGui(); + void setupBorderColorsGui(); + + void readParams(const rtengine::procparams::ProcParams* pp); + void readEdited(const ParamsEdited* pedited); + void writeParams(rtengine::procparams::ProcParams* pp); + void writeEdited(ParamsEdited* pedited); + + void setDimensions(); + void updateFramingMethodGui(); + void updateBorderSizeGui(); + void updateBorderColorGui(); + + // Framing method + MyComboBoxText* framingMethod; + sigc::connection framingMethodChanged; + Gtk::Label* aspectRatioLabel; + MyComboBoxText* aspectRatio; + sigc::connection aspectRatioChanged; + Gtk::Label* orientationLabel; + MyComboBoxText* orientation; + sigc::connection orientationChanged; + DimensionGui width; + DimensionGui height; + Gtk::CheckButton* allowUpscaling; + sigc::connection allowUpscalingConnection; + + // Border sizing + MyComboBoxText* borderSizeMethod; + sigc::connection borderSizeMethodChanged; + Gtk::Label* basisLabel; + MyComboBoxText* basis; + sigc::connection basisChanged; + Adjuster* relativeBorderSize; + Gtk::Frame* minSizeFrame; + Gtk::Box* minSizeFrameContent; + Gtk::CheckButton* minSizeEnabled; + sigc::connection minSizeEnabledConnection; + DimensionGui minWidth; + DimensionGui minHeight; + DimensionGui absWidth; + DimensionGui absHeight; + + // Border colors + Adjuster* redAdj; + Adjuster* greenAdj; + Adjuster* blueAdj; + ColorPreview* colorPreview; + + // Events + rtengine::ProcEvent EvFramingEnabled; + rtengine::ProcEvent EvFramingMethod; + rtengine::ProcEvent EvFramingAspectRatio; + rtengine::ProcEvent EvFramingOrientation; + rtengine::ProcEvent EvFramingFramedWidth; + rtengine::ProcEvent EvFramingFramedHeight; + rtengine::ProcEvent EvFramingAllowUpscaling; + rtengine::ProcEvent EvFramingBorderSizingMethod; + rtengine::ProcEvent EvFramingBasis; + rtengine::ProcEvent EvFramingRelativeBorderSize; + rtengine::ProcEvent EvFramingMinSizeEnabled; + rtengine::ProcEvent EvFramingMinWidth; + rtengine::ProcEvent EvFramingMinHeight; + rtengine::ProcEvent EvFramingAbsWidth; + rtengine::ProcEvent EvFramingAbsHeight; + rtengine::ProcEvent EvFramingBorderRed; + rtengine::ProcEvent EvFramingBorderGreen; + rtengine::ProcEvent EvFramingBorderBlue; + + IdleRegister idleRegister; + std::unique_ptr aspectRatioData; + + int imgWidth; + int imgHeight; + bool lastAllowUpscaling; + bool lastMinSizeEnabled; +}; diff --git a/rtgui/guiutils.cc b/rtgui/guiutils.cc index 2bbdcd8cd..1060c4ac4 100644 --- a/rtgui/guiutils.cc +++ b/rtgui/guiutils.cc @@ -25,6 +25,8 @@ #include "rtimage.h" #include "rtscalable.h" #include "multilangmgr.h" +#include "adjuster.h" +#include "toolpanel.h" #include @@ -77,6 +79,34 @@ void IdleRegister::destroy() mutex.unlock(); } +BlockAdjusterEvents::BlockAdjusterEvents(Adjuster* adjuster) : adj(adjuster) +{ + if (adj) { + adj->block(true); + } +} + +BlockAdjusterEvents::~BlockAdjusterEvents() +{ + if (adj) { + adj->block(false); + } +} + +DisableListener::DisableListener(ToolPanel* panelToDisable) : panel(panelToDisable) +{ + if (panel) { + panel->disableListener(); + } +} + +DisableListener::~DisableListener() +{ + if (panel) { + panel->enableListener(); + } +} + Glib::ustring escapeHtmlChars(const Glib::ustring &src) { diff --git a/rtgui/guiutils.h b/rtgui/guiutils.h index 169d0ae11..aae2989a0 100644 --- a/rtgui/guiutils.h +++ b/rtgui/guiutils.h @@ -45,7 +45,9 @@ struct CropParams; } +class Adjuster; class RTImage; +class ToolPanel; Glib::ustring escapeHtmlChars(const Glib::ustring &src); bool removeIfThere (Gtk::Container* cont, Gtk::Widget* w, bool increference = true); @@ -161,6 +163,26 @@ private: bool wasBlocked; }; +class BlockAdjusterEvents +{ +public: + explicit BlockAdjusterEvents(Adjuster* adjuster); + ~BlockAdjusterEvents(); + +private: + Adjuster* adj; +}; + +class DisableListener +{ +public: + explicit DisableListener(ToolPanel* panelToDisable); + ~DisableListener(); + +private: + ToolPanel* panel; +}; + /** * @brief Glue box to control visibility of the MyExpender's content ; also handle the frame around it */ diff --git a/rtgui/paramsedited.cc b/rtgui/paramsedited.cc index 7a92194ba..ca6af8e08 100644 --- a/rtgui/paramsedited.cc +++ b/rtgui/paramsedited.cc @@ -25,6 +25,134 @@ #include "../rtengine/procparams.h" +namespace +{ + +using namespace rtengine::procparams; + +void setAll(FramingParamsEdited& framing, bool v) +{ + framing.enabled = v; + framing.framingMethod = v; + framing.aspectRatio = v; + framing.orientation = v; + framing.framedWidth = v; + framing.framedHeight = v; + framing.allowUpscaling = v; + + framing.borderSizingMethod = v; + framing.basis = v; + framing.relativeBorderSize = v; + framing.minSizeEnabled = v; + framing.minWidth = v; + framing.minHeight = v; + framing.absWidth = v; + framing.absHeight = v; + + framing.borderRed = v; + framing.borderGreen = v; + framing.borderBlue = v; +} + +void initFrom(FramingParamsEdited& edits, const ProcParams& params, const ProcParams& otherParams) +{ + const FramingParams& curr = params.framing; + const FramingParams& other = otherParams.framing; + + edits.enabled &= curr.enabled == other.enabled; + edits.framingMethod &= curr.framingMethod == other.framingMethod; + edits.aspectRatio &= curr.aspectRatio == other.aspectRatio; + edits.orientation &= curr.orientation == other.orientation; + edits.framedWidth &= curr.framedWidth == other.framedWidth; + edits.framedHeight &= curr.framedHeight == other.framedHeight; + edits.allowUpscaling &= curr.allowUpscaling == other.allowUpscaling; + + edits.borderSizingMethod &= curr.borderSizingMethod == other.borderSizingMethod; + edits.basis &= curr.basis == other.basis; + edits.relativeBorderSize &= curr.relativeBorderSize == other.relativeBorderSize; + edits.minSizeEnabled &= curr.minSizeEnabled == other.minSizeEnabled; + edits.minWidth &= curr.minWidth == other.minWidth; + edits.minHeight &= curr.minHeight == other.minHeight; + edits.absWidth &= curr.absWidth == other.absWidth; + edits.absHeight &= curr.absHeight == other.absHeight; + + edits.borderRed &= curr.borderRed == other.borderRed; + edits.borderGreen &= curr.borderGreen == other.borderGreen; + edits.borderBlue &= curr.borderBlue == other.borderBlue; +} + +void combine(FramingParams& toEdit, const FramingParams& mod, const FramingParamsEdited& edits, + bool dontForceSet) +{ + if (edits.enabled) { + toEdit.enabled = mod.enabled; + } + if (edits.framingMethod) { + toEdit.framingMethod = mod.framingMethod; + } + if (edits.aspectRatio) { + toEdit.aspectRatio = mod.aspectRatio; + } + if (edits.orientation) { + toEdit.orientation = mod.orientation; + } + if (edits.framedWidth) { + toEdit.framedWidth = mod.framedWidth; + } + if (edits.framedHeight) { + toEdit.framedHeight = mod.framedHeight; + } + if (edits.allowUpscaling) { + toEdit.allowUpscaling = mod.allowUpscaling; + } + + if (edits.borderSizingMethod) { + toEdit.borderSizingMethod = mod.borderSizingMethod; + } + if (edits.basis) { + toEdit.basis = mod.basis; + } + if (edits.relativeBorderSize) { + toEdit.relativeBorderSize = + dontForceSet && options.baBehav[ADDSET_FRAMING_RELATIVE_SCALE] ? + toEdit.relativeBorderSize + mod.relativeBorderSize : + mod.relativeBorderSize; + } + if (edits.minSizeEnabled) { + toEdit.minSizeEnabled = mod.minSizeEnabled; + } + if (edits.minWidth) { + toEdit.minWidth = mod.minWidth; + } + if (edits.minHeight) { + toEdit.minHeight = mod.minHeight; + } + if (edits.absWidth) { + toEdit.absWidth = mod.absWidth; + } + if (edits.absHeight) { + toEdit.absHeight = mod.absHeight; + } + + if (edits.borderRed) { + toEdit.borderRed = dontForceSet && options.baBehav[ADDSET_FRAMING_BORDER_RED] ? + toEdit.borderRed + mod.borderRed : + mod.borderRed; + } + if (edits.borderGreen) { + toEdit.borderGreen = dontForceSet && options.baBehav[ADDSET_FRAMING_BORDER_GREEN] ? + toEdit.borderGreen + mod.borderGreen : + mod.borderGreen; + } + if (edits.borderBlue) { + toEdit.borderBlue = dontForceSet && options.baBehav[ADDSET_FRAMING_BORDER_BLUE] ? + toEdit.borderBlue + mod.borderBlue : + mod.borderBlue; + } +} + +} // namespace + ParamsEdited::ParamsEdited(bool value) { @@ -446,7 +574,6 @@ void ParamsEdited::set(bool v) blackwhite.autoc = v; blackwhite.algo = v; - resize.scale = v; resize.appliesTo = v; resize.method = v; @@ -456,11 +583,13 @@ void ParamsEdited::set(bool v) resize.longedge = v; resize.shortedge = v; resize.enabled = v; + resize.allowUpscaling = v; + + ::setAll(framing, v); spot.enabled = v; spot.entries = v; - resize.allowUpscaling = v; icm.inputProfile = v; icm.toneCurve = v; icm.applyLookTable = v; @@ -1984,9 +2113,12 @@ void ParamsEdited::initFrom(const std::vector& resize.longedge = resize.longedge && p.resize.longedge == other.resize.longedge; resize.shortedge = resize.shortedge && p.resize.shortedge == other.resize.shortedge; resize.enabled = resize.enabled && p.resize.enabled == other.resize.enabled; + resize.allowUpscaling = resize.allowUpscaling && p.resize.allowUpscaling == other.resize.allowUpscaling; + + ::initFrom(framing, p, other); + spot.enabled = spot.enabled && p.spot.enabled == other.spot.enabled; spot.entries = spot.entries && p.spot.entries == other.spot.entries; - resize.allowUpscaling = resize.allowUpscaling && p.resize.allowUpscaling == other.resize.allowUpscaling; icm.inputProfile = icm.inputProfile && p.icm.inputProfile == other.icm.inputProfile; icm.toneCurve = icm.toneCurve && p.icm.toneCurve == other.icm.toneCurve; icm.applyLookTable = icm.applyLookTable && p.icm.applyLookTable == other.icm.applyLookTable; @@ -6856,6 +6988,8 @@ void ParamsEdited::combine(rtengine::procparams::ProcParams& toEdit, const rteng toEdit.resize.allowUpscaling = mods.resize.allowUpscaling; } + ::combine(toEdit.framing, mods.framing, framing, dontforceSet); + if (icm.inputProfile) { toEdit.icm.inputProfile = mods.icm.inputProfile; } diff --git a/rtgui/paramsedited.h b/rtgui/paramsedited.h index f3cefe653..68b362bf0 100644 --- a/rtgui/paramsedited.h +++ b/rtgui/paramsedited.h @@ -1307,6 +1307,29 @@ struct ResizeParamsEdited { bool allowUpscaling; }; +struct FramingParamsEdited { + bool enabled; + bool framingMethod; + bool aspectRatio; + bool orientation; + bool framedWidth; + bool framedHeight; + bool allowUpscaling; + + bool borderSizingMethod; + bool basis; + bool relativeBorderSize; + bool minSizeEnabled; + bool minWidth; + bool minHeight; + bool absWidth; + bool absHeight; + + bool borderRed; + bool borderGreen; + bool borderBlue; +}; + class SpotParamsEdited { public: @@ -1682,6 +1705,7 @@ struct ParamsEdited { ChannelMixerParamsEdited chmixer; BlackWhiteParamsEdited blackwhite; ResizeParamsEdited resize; + FramingParamsEdited framing; SpotParamsEdited spot; ColorManagementParamsEdited icm; RAWParamsEdited raw; diff --git a/rtgui/partialpastedlg.cc b/rtgui/partialpastedlg.cc index 3f03d81f6..d98c46489 100644 --- a/rtgui/partialpastedlg.cc +++ b/rtgui/partialpastedlg.cc @@ -266,6 +266,7 @@ PartialPasteDlg::PartialPasteDlg (const Glib::ustring &title, Gtk::Window* paren crop = Gtk::manage (new Gtk::CheckButton (M("PARTIALPASTE_CROP"))); resize = Gtk::manage (new Gtk::CheckButton (M("PARTIALPASTE_RESIZE"))); prsharpening = Gtk::manage (new Gtk::CheckButton (M("PARTIALPASTE_PRSHARPENING"))); + framing = Gtk::manage (new Gtk::CheckButton (M("PARTIALPASTE_FRAMING"))); perspective = Gtk::manage (new Gtk::CheckButton (M("PARTIALPASTE_PERSPECTIVE"))); commonTrans = Gtk::manage (new Gtk::CheckButton (M("PARTIALPASTE_COMMONTRANSFORMPARAMS"))); @@ -384,6 +385,7 @@ PartialPasteDlg::PartialPasteDlg (const Glib::ustring &title, Gtk::Window* paren vboxes[4]->pack_start (*crop, Gtk::PACK_SHRINK, 2); vboxes[4]->pack_start (*resize, Gtk::PACK_SHRINK, 2); vboxes[4]->pack_start (*prsharpening, Gtk::PACK_SHRINK, 2); + vboxes[4]->pack_start (*framing, Gtk::PACK_SHRINK, 2); vboxes[4]->pack_start (*perspective, Gtk::PACK_SHRINK, 2); vboxes[4]->pack_start (*commonTrans, Gtk::PACK_SHRINK, 2); @@ -547,6 +549,7 @@ PartialPasteDlg::PartialPasteDlg (const Glib::ustring &title, Gtk::Window* paren cropConn = crop->signal_toggled().connect (sigc::bind (sigc::mem_fun(*composition, &Gtk::CheckButton::set_inconsistent), true)); resizeConn = resize->signal_toggled().connect (sigc::bind (sigc::mem_fun(*composition, &Gtk::CheckButton::set_inconsistent), true)); prsharpeningConn = prsharpening->signal_toggled().connect (sigc::bind (sigc::mem_fun(*composition, &Gtk::CheckButton::set_inconsistent), true)); + framingConn = framing->signal_toggled().connect (sigc::bind (sigc::mem_fun(*composition, &Gtk::CheckButton::set_inconsistent), true)); perspectiveConn = perspective->signal_toggled().connect (sigc::bind (sigc::mem_fun(*composition, &Gtk::CheckButton::set_inconsistent), true)); commonTransConn = commonTrans->signal_toggled().connect (sigc::bind (sigc::mem_fun(*composition, &Gtk::CheckButton::set_inconsistent), true)); @@ -823,6 +826,7 @@ void PartialPasteDlg::compositionToggled () ConnectionBlocker cropBlocker(cropConn); ConnectionBlocker resizeBlocker(resizeConn); ConnectionBlocker prsharpeningBlocker(prsharpeningConn); + ConnectionBlocker framingBlocker(framingConn); ConnectionBlocker perspectiveBlocker(perspectiveConn); ConnectionBlocker commonTransBlocker(commonTransConn); @@ -833,6 +837,7 @@ void PartialPasteDlg::compositionToggled () crop->set_active (composition->get_active ()); resize->set_active (composition->get_active ()); prsharpening->set_active (composition->get_active ()); + framing->set_active (composition->get_active ()); perspective->set_active (composition->get_active ()); commonTrans->set_active (composition->get_active ()); } @@ -1043,6 +1048,10 @@ void PartialPasteDlg::applyPaste (rtengine::procparams::ProcParams* dstPP, Param filterPE.prsharpening = falsePE.prsharpening; } + if (!framing->get_active ()) { + filterPE.framing = falsePE.framing; + } + if (!perspective->get_active ()) { filterPE.perspective = falsePE.perspective; } diff --git a/rtgui/partialpastedlg.h b/rtgui/partialpastedlg.h index 1dee55a6e..10df7ed57 100644 --- a/rtgui/partialpastedlg.h +++ b/rtgui/partialpastedlg.h @@ -180,6 +180,7 @@ public: Gtk::CheckButton* crop; Gtk::CheckButton* resize; Gtk::CheckButton* prsharpening; + Gtk::CheckButton* framing; Gtk::CheckButton* perspective; Gtk::CheckButton* commonTrans; @@ -231,7 +232,7 @@ public: sigc::connection spotConn, sharpenConn, gradsharpenConn, microcontrastConn, impdenConn, dirpyrdenConn, defringeConn, epdConn, fattalConn, dirpyreqConn, waveletConn, retinexConn, dehazeConn; sigc::connection vibranceConn, chmixerConn, hsveqConn, rgbcurvesConn, chmixerbwConn, colortoningConn, filmSimulationConn, softlightConn; sigc::connection distortionConn, cacorrConn, vignettingConn, lcpConn; - sigc::connection coarserotConn, finerotConn, cropConn, resizeConn, prsharpeningConn, perspectiveConn, commonTransConn; + sigc::connection coarserotConn, finerotConn, cropConn, resizeConn, prsharpeningConn, framingConn, perspectiveConn, commonTransConn; sigc::connection metadataConn, exifchConn, iptcConn, icmConn; sigc::connection df_fileConn, df_AutoSelectConn, ff_fileConn, ff_AutoSelectConn, ff_FromMetaDataConn, ff_BlurRadiusConn, ff_BlurTypeConn, ff_ClipControlConn; sigc::connection raw_caredblueConn, raw_ca_autocorrectConn, raw_ca_avoid_colourshiftconn, raw_hotpix_filtConn, raw_deadpix_filtConn, raw_pdaf_lines_filterConn, raw_linenoiseConn, raw_greenthreshConn, raw_ccStepsConn, raw_methodConn, raw_borderConn, raw_imagenumConn, raw_dcb_iterationsConn, raw_lmmse_iterationsConn, raw_pixelshiftConn, raw_dcb_enhanceConn, raw_exposConn, raw_blackConn; diff --git a/rtgui/preferences.cc b/rtgui/preferences.cc index 6c9756051..d54d9fa27 100644 --- a/rtgui/preferences.cc +++ b/rtgui/preferences.cc @@ -364,6 +364,13 @@ Gtk::Widget* Preferences::getBatchProcPanel() mi->set_value(behavColumns.label, M("TP_RESIZE_LABEL")); appendBehavList(mi, M("TP_RESIZE_SCALE"), ADDSET_RESIZE_SCALE, true); + mi = behModel->append(); + mi->set_value(behavColumns.label, M("TP_FRAMING_LABEL")); + appendBehavList(mi, M("TP_FRAMING_BORDER_SIZE"), ADDSET_FRAMING_RELATIVE_SCALE, false); + appendBehavList(mi, M("TP_FRAMING_RED"), ADDSET_FRAMING_BORDER_RED, false); + appendBehavList(mi, M("TP_FRAMING_GREEN"), ADDSET_FRAMING_BORDER_GREEN, false); + appendBehavList(mi, M("TP_FRAMING_BLUE"), ADDSET_FRAMING_BORDER_BLUE, false); + mi = behModel->append(); mi->set_value(behavColumns.label, M("TP_LENSGEOM_SCALE")); appendBehavList(mi, M("TP_LENSGEOM_SCALE"), ADDSET_LENSGEOM_SCALE, true); diff --git a/rtgui/prsharpening.cc b/rtgui/prsharpening.cc index d3c936fa2..834e1f486 100644 --- a/rtgui/prsharpening.cc +++ b/rtgui/prsharpening.cc @@ -35,7 +35,7 @@ PrSharpening::PrSharpening () : FoldableToolPanel(this, TOOL_NAME, M("TP_PRSHARP milestones.push_back( GradientMilestone(0.0, 0.0, 0.0, 0.0) ); milestones.push_back( GradientMilestone(1.0, 1.0, 1.0, 1.0) ); - //setEnabledTooltipMarkup(M("TP_PRSHARPENING_TOOLTIP")); + setEnabledTooltipMarkup(M("TP_PRSHARPENING_TOOLTIP")); Gtk::Box* hb = Gtk::manage (new Gtk::Box ()); hb->show (); diff --git a/rtgui/resize.cc b/rtgui/resize.cc index de9f6b4d1..ca4221757 100644 --- a/rtgui/resize.cc +++ b/rtgui/resize.cc @@ -146,22 +146,22 @@ Resize::Resize () : FoldableToolPanel(this, TOOL_NAME, M("TP_RESIZE_LABEL"), fal w->set_digits (0); w->set_increments (1, 100); - w->set_range (32, MAX_SCALE * maxw); + w->set_range (MIN_SIZE, MAX_SCALE * maxw); w->set_value (800); // Doesn't seem to have any effect (overwritten in Resize::read) h->set_digits (0); h->set_increments (1, 100); - h->set_range (32, MAX_SCALE * maxh); + h->set_range (MIN_SIZE, MAX_SCALE * maxh); h->set_value (600); // Doesn't seem to have any effect (overwritten in Resize::read) le->set_digits (0); le->set_increments (1, 100); - le->set_range (32, MAX_SCALE * maxw); + le->set_range (MIN_SIZE, MAX_SCALE * maxw); le->set_value (900); se->set_digits (0); se->set_increments (1, 100); - se->set_range (32, MAX_SCALE * maxh); + se->set_range (MIN_SIZE, MAX_SCALE * maxh); se->set_value (900); wconn = w->signal_value_changed().connect ( sigc::mem_fun(*this, &Resize::entryWChanged), true); @@ -173,7 +173,6 @@ Resize::Resize () : FoldableToolPanel(this, TOOL_NAME, M("TP_RESIZE_LABEL"), fal sconn = spec->signal_changed().connect ( sigc::mem_fun(*this, &Resize::specChanged) ); getSubToolsContainer()->hide(); - getSubToolsContainer()->set_tooltip_markup (M("TP_PRSHARPENING_TOOLTIP")); show_all(); } diff --git a/rtgui/resize.h b/rtgui/resize.h index 674bbb34f..cd83b1418 100644 --- a/rtgui/resize.h +++ b/rtgui/resize.h @@ -22,7 +22,6 @@ #include "adjuster.h" #include "guiutils.h" -#include "guiutils.h" #include "toolpanel.h" class Resize final : @@ -33,6 +32,8 @@ class Resize final : { public: static const Glib::ustring TOOL_NAME; + static constexpr int MAX_SCALE = 16; // 16 to match the main preview max scale of 1600% + static constexpr int MIN_SIZE = 32; Resize (); ~Resize () override; @@ -85,6 +86,4 @@ private: sigc::connection sconn, aconn, wconn, hconn, leconn, seconn; bool wDirty, hDirty, leDirty, seDirty; IdleRegister idle_register; - - static constexpr int MAX_SCALE = 16; // 16 to match the main preview max scale of 1600% }; diff --git a/rtgui/toollocationpref.cc b/rtgui/toollocationpref.cc index 4352e1b07..d15f00de1 100644 --- a/rtgui/toollocationpref.cc +++ b/rtgui/toollocationpref.cc @@ -133,6 +133,8 @@ Glib::ustring getToolTitleKey(Tool tool) return "TP_RESIZE_LABEL"; case Tool::PR_SHARPENING: return "TP_PRSHARPENING_LABEL"; + case Tool::FRAMING: + return "TP_FRAMING_LABEL"; case Tool::CROP_TOOL: return "TP_CROP_LABEL"; case Tool::ICM: diff --git a/rtgui/toolpanelcoord.cc b/rtgui/toolpanelcoord.cc index 4dd1cca22..90e4b00be 100644 --- a/rtgui/toolpanelcoord.cc +++ b/rtgui/toolpanelcoord.cc @@ -164,6 +164,9 @@ const std::vector TRANSFORM_PANEL_TOOLS = { { .id = Tool::PR_SHARPENING, }, + { + .id = Tool::FRAMING, + }, }, }, { @@ -324,6 +327,7 @@ ToolPanelCoordinator::ToolPanelCoordinator (bool batch) : ipc (nullptr), favorit blackwhite = Gtk::manage(new BlackWhite()); resize = Gtk::manage(new Resize()); prsharpening = Gtk::manage(new PrSharpening()); + framing = Gtk::manage(new Framing()); crop = Gtk::manage(new Crop()); icm = Gtk::manage(new ICMPanel()); metadata = Gtk::manage(new MetaDataPanel()); @@ -638,6 +642,8 @@ std::string ToolPanelCoordinator::getToolName(Tool tool) return Resize::TOOL_NAME; case Tool::PR_SHARPENING: return PrSharpening::TOOL_NAME; + case Tool::FRAMING: + return Framing::TOOL_NAME; case Tool::CROP_TOOL: return Crop::TOOL_NAME; case Tool::ICM: @@ -1117,9 +1123,13 @@ void ToolPanelCoordinator::panelChanged(const rtengine::ProcEvent& event, const crop->write(params); resize->update(params->crop.enabled, params->crop.w, params->crop.h, ipc->getFullWidth(), ipc->getFullHeight()); resize->write(params); + framing->update(ipc->getFullWidth(), ipc->getFullHeight()); + framing->write(params); } else if (event == rtengine::EvCrop) { resize->update(params->crop.enabled, params->crop.w, params->crop.h); resize->write(params); + framing->update(ipc->getFullWidth(), ipc->getFullHeight()); + framing->write(params); } /* @@ -2001,6 +2011,8 @@ FoldableToolPanel *ToolPanelCoordinator::getFoldableToolPanel(Tool tool) const return resize; case Tool::PR_SHARPENING: return prsharpening; + case Tool::FRAMING: + return framing; case Tool::CROP_TOOL: return crop; case Tool::ICM: diff --git a/rtgui/toolpanelcoord.h b/rtgui/toolpanelcoord.h index 6801664f6..31044a9b0 100644 --- a/rtgui/toolpanelcoord.h +++ b/rtgui/toolpanelcoord.h @@ -45,6 +45,7 @@ #include "filmnegative.h" #include "filmsimulation.h" #include "flatfield.h" +#include "framing.h" #include "gradient.h" #include "guiutils.h" #include "hsvequalizer.h" @@ -131,6 +132,7 @@ protected: ChMixer* chmixer; BlackWhite* blackwhite; Resize* resize; + Framing* framing; PrSharpening* prsharpening; ICMPanel* icm; Crop* crop; @@ -282,6 +284,7 @@ public: BLACK_WHITE, RESIZE_TOOL, PR_SHARPENING, + FRAMING, CROP_TOOL, ICM, WAVELET,