Skip to content

Commit 2a82bed

Browse files
committed
Adding upscaling for crop effect + resize property - so cropping into higher resolution content does not become blurry.
1 parent bd59e6b commit 2a82bed

File tree

6 files changed

+132
-1
lines changed

6 files changed

+132
-1
lines changed

src/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ set(EFFECTS_SOURCES
115115
effects/ColorMap.cpp
116116
effects/ColorShift.cpp
117117
effects/Crop.cpp
118+
effects/CropHelpers.cpp
118119
effects/Deinterlace.cpp
119120
effects/Hue.cpp
120121
effects/LensFlare.cpp

src/FFmpegReader.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
#include <unistd.h>
2121

2222
#include "FFmpegUtilities.h"
23+
#include "effects/CropHelpers.h"
2324

2425
#include "FFmpegReader.h"
2526
#include "Exceptions.h"
@@ -1594,6 +1595,9 @@ void FFmpegReader::ProcessVideoPacket(int64_t requested_frame) {
15941595
max_width = info.width * max_scale_x * preview_ratio;
15951596
max_height = info.height * max_scale_y * preview_ratio;
15961597
}
1598+
1599+
// If a crop effect is resizing the image, request enough pixels to preserve detail
1600+
ApplyCropResizeScale(parent, info.width, info.height, max_width, max_height);
15971601
}
15981602

15991603
// Determine if image needs to be scaled (for performance reasons)

src/QtImageReader.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@
1616
#include "CacheMemory.h"
1717
#include "Exceptions.h"
1818
#include "Timeline.h"
19+
#include "effects/CropHelpers.h"
1920

21+
#include <algorithm>
2022
#include <QString>
2123
#include <QImage>
2224
#include <QPainter>
@@ -244,6 +246,9 @@ QSize QtImageReader::calculate_max_size() {
244246
max_width = info.width * max_scale_x * preview_ratio;
245247
max_height = info.height * max_scale_y * preview_ratio;
246248
}
249+
250+
// If a crop effect is resizing the image, request enough pixels to preserve detail
251+
ApplyCropResizeScale(parent, info.width, info.height, max_width, max_height);
247252
}
248253

249254
// Return new QSize of the current max size

src/effects/CropHelpers.cpp

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/**
2+
* @file
3+
* @brief Shared helpers for Crop effect scaling logic
4+
* @author Jonathan Thomas <jonathan@openshot.org>
5+
*
6+
* @ref License
7+
*/
8+
9+
// Copyright (c) 2008-2025 OpenShot Studios, LLC
10+
//
11+
// SPDX-License-Identifier: LGPL-3.0-or-later
12+
13+
#include "CropHelpers.h"
14+
15+
#include <algorithm>
16+
#include <cmath>
17+
#include <limits>
18+
19+
#include "../Clip.h"
20+
#include "Crop.h"
21+
22+
namespace openshot {
23+
24+
const Crop* FindResizingCropEffect(Clip* clip) {
25+
if (!clip) {
26+
return nullptr;
27+
}
28+
29+
for (auto effect : clip->Effects()) {
30+
if (auto crop_effect = dynamic_cast<Crop*>(effect)) {
31+
if (crop_effect->resize) {
32+
return crop_effect;
33+
}
34+
}
35+
}
36+
37+
return nullptr;
38+
}
39+
40+
void ApplyCropResizeScale(Clip* clip, int source_width, int source_height, int& max_width, int& max_height) {
41+
const Crop* crop_effect = FindResizingCropEffect(clip);
42+
if (!crop_effect) {
43+
return;
44+
}
45+
46+
const float max_left = crop_effect->left.GetMaxPoint().co.Y;
47+
const float max_right = crop_effect->right.GetMaxPoint().co.Y;
48+
const float max_top = crop_effect->top.GetMaxPoint().co.Y;
49+
const float max_bottom = crop_effect->bottom.GetMaxPoint().co.Y;
50+
51+
const float visible_width = std::max(0.01f, 1.0f - max_left - max_right);
52+
const float visible_height = std::max(0.01f, 1.0f - max_top - max_bottom);
53+
54+
const double scaled_width = std::ceil(max_width / visible_width);
55+
const double scaled_height = std::ceil(max_height / visible_height);
56+
57+
const double clamped_width = std::min<double>(source_width, scaled_width);
58+
const double clamped_height = std::min<double>(source_height, scaled_height);
59+
60+
max_width = static_cast<int>(std::min<double>(std::numeric_limits<int>::max(), clamped_width));
61+
max_height = static_cast<int>(std::min<double>(std::numeric_limits<int>::max(), clamped_height));
62+
}
63+
64+
} // namespace openshot

src/effects/CropHelpers.h

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/**
2+
* @file
3+
* @brief Shared helpers for Crop effect scaling logic
4+
* @author Jonathan Thomas <jonathan@openshot.org>
5+
*
6+
* @ref License
7+
*/
8+
9+
// Copyright (c) 2008-2025 OpenShot Studios, LLC
10+
//
11+
// SPDX-License-Identifier: LGPL-3.0-or-later
12+
13+
#ifndef OPENSHOT_CROP_HELPERS_H
14+
#define OPENSHOT_CROP_HELPERS_H
15+
16+
namespace openshot {
17+
18+
class Clip;
19+
class Crop;
20+
21+
/// Return the first Crop effect on this clip that has resize enabled (if any)
22+
const Crop* FindResizingCropEffect(Clip* clip);
23+
24+
/// Scale the requested max_width / max_height based on the Crop resize amount, capped by source size
25+
void ApplyCropResizeScale(Clip* clip, int source_width, int source_height, int& max_width, int& max_height);
26+
27+
} // namespace openshot
28+
29+
#endif // OPENSHOT_CROP_HELPERS_H

tests/Crop.cpp

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
* @ref License
88
*/
99

10-
// Copyright (c) 2008-2021 OpenShot Studios, LLC
10+
// Copyright (c) 2008-2025 OpenShot Studios, LLC
1111
//
1212
// SPDX-License-Identifier: LGPL-3.0-or-later
1313

@@ -16,7 +16,9 @@
1616
#include "openshot_catch.h"
1717

1818
#include "Frame.h"
19+
#include "Clip.h"
1920
#include "effects/Crop.h"
21+
#include "effects/CropHelpers.h"
2022

2123
#include <QColor>
2224
#include <QImage>
@@ -144,3 +146,29 @@ TEST_CASE( "x/y offsets", "[libopenshot][effect][crop]" )
144146
// so it becomes a transparent pixel
145147
CHECK(i->pixelColor(900, 360) == trans);
146148
}
149+
150+
TEST_CASE( "crop resize scaling helper", "[libopenshot][effect][crop][scaling]" )
151+
{
152+
// Build a clip with a resizing crop effect
153+
auto clip = std::make_unique<openshot::Clip>();
154+
auto crop = new openshot::Crop(openshot::Keyframe(0.0), openshot::Keyframe(0.0),
155+
openshot::Keyframe(0.0), openshot::Keyframe(0.75));
156+
crop->resize = true;
157+
clip->AddEffect(crop);
158+
159+
int max_width = 900;
160+
int max_height = 500;
161+
162+
// 25% of the height is visible, so request 4x height (capped at source size)
163+
openshot::ApplyCropResizeScale(clip.get(), 3840, 2160, max_width, max_height);
164+
CHECK(max_width == 900);
165+
CHECK(max_height == 2000);
166+
167+
// If scaling would exceed source, it should clamp at source dimensions
168+
crop->bottom = openshot::Keyframe(0.9); // visible height = 10%
169+
max_width = 900;
170+
max_height = 500;
171+
openshot::ApplyCropResizeScale(clip.get(), 1920, 1080, max_width, max_height);
172+
CHECK(max_width == 900); // width unchanged (no horizontal crop)
173+
CHECK(max_height == 1080); // scaled height would be 5000, so it clamps
174+
}

0 commit comments

Comments
 (0)