diff --git a/rtengine/improcfun.h b/rtengine/improcfun.h index 6b15ac4d7..578704a50 100644 --- a/rtengine/improcfun.h +++ b/rtengine/improcfun.h @@ -213,12 +213,31 @@ 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; + 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..784d77055 100644 --- a/rtengine/ipresize.cc +++ b/rtengine/ipresize.cc @@ -33,6 +33,582 @@ # 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; + 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); +} + +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; + 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 / resize.size.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; + // frame_len = img_len + 2 * scale * img_len = (1 + 2 * scale) * img_len + double imgFrameScale = (1.0 + 2.0 * scale); + double imgBasis = frameBasis / imgFrameScale; + // border_len = (scale * img_len) + // = frame_len / (1 / scale + 2) + // = frame_len * scale / (1 + 2 * scale) + double borderBasis = frameBasis * scale / imgFrameScale; + + // 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; + + 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); + + if (borderOther < minBorderOther) { + maxImageOther = std::floor(frameOther - 2.0 * minBorderOther); + } + if (borderBasis < minBorderBasis) { + maxImageBasis = std::floor(frameBasis - 2.0 * minBorderBasis); + } + } + + 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 { + 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 { + // 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 +{ + auto length = [](double img, double border) { + return img + 2.0 * border; + }; + auto evalBorder = [](double side, double scale, double img) { + double otherSide = side * scale; + double totalBorder = otherSide - img; + return std::max(totalBorder, 0.0) * 0.5; + }; + + if (framing.borderSizingMethod == BorderSizing::FIXED_SIZE) { + return Dimensions(length(imgSize.width, framing.absWidth), + length(imgSize.height, framing.absHeight)); + } + + Side side = pickReferenceSide(framing, imgSize); + double aspectRatio = orientAspectRatio(framing, imgSize); + + // Compute the size with borders given the requested reference side length + // and aspect ratio + Dimensions borderSize; + double scale = framing.relativeBorderSize; + if (side == Side::WIDTH) { + borderSize.width = imgSize.width * scale; + double totalWidth = length(imgSize.width, borderSize.width); + borderSize.height = evalBorder(totalWidth, 1.0 / aspectRatio, imgSize.height); + } else { + borderSize.height = imgSize.height * scale; + double totalHeight = length(imgSize.height, borderSize.height); + borderSize.width = evalBorder(totalHeight, aspectRatio, imgSize.width); + } + + if (framing.minSizeEnabled) { + Dimensions minSize(static_cast(framing.minWidth), + static_cast(framing.minHeight)); + borderSize = clampToBBox(borderSize, minSize, OUTSIDE_BBOX); + } + + Dimensions framedSize(length(imgSize.width, borderSize.width), + length(imgSize.height, borderSize.height)); + + // 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); + if (framing.minSizeEnabled) { + Dimensions limit = imgSize; + 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 +909,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 +982,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 +990,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 +1034,35 @@ 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; } + +} // namespace rtengine \ No newline at end of file diff --git a/rtengine/simpleprocess.cc b/rtengine/simpleprocess.cc index fa15285a7..0c695c6a0 100644 --- a/rtengine/simpleprocess.cc +++ b/rtengine/simpleprocess.cc @@ -1893,20 +1893,50 @@ 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); + 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 +1956,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,14 +2015,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; + } } + // TODO: Blit framing border + image + Exiv2Metadata info(imgsrc->getFileName()); switch (params.metadata.mode) {