From 5d8a5a2445632c834791c5f4bfe12ab9a6679901 Mon Sep 17 00:00:00 2001 From: Rajesh Gopinath Kumar <10174122+rajeshgk@users.noreply.github.com> Date: Wed, 14 Oct 2026 17:15:08 -0500 Subject: [PATCH] Fix caption stroke rendering order Draw caption outlines before filling glyphs so the stroke does not obscure the text interior. Add a pixel-based regression test that fails with the original rendering order. Fixes OpenShot/libopenshot#1033 --- src/effects/Caption.cpp | 8 +++++ tests/Caption.cpp | 72 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+) diff --git a/src/effects/Caption.cpp b/src/effects/Caption.cpp index c82790c0f6..6e96092c00 100644 --- a/src/effects/Caption.cpp +++ b/src/effects/Caption.cpp @@ -369,6 +369,14 @@ std::shared_ptr Caption::GetFrame(std::shared_ptr 0.0) { + painter.setBrush(Qt::NoBrush); + painter.drawPath(path); + } + + painter.setPen(Qt::NoPen); painter.setBrush(font_brush); painter.drawPath(path); } diff --git a/tests/Caption.cpp b/tests/Caption.cpp index f49363be56..d848c35a19 100644 --- a/tests/Caption.cpp +++ b/tests/Caption.cpp @@ -216,6 +216,78 @@ TEST_CASE("caption effect", "[libopenshot][caption]") { clip1.Close(); } + + SECTION("stroke before fill regression") { + // Compare the filled interior of caption glyphs with and + // without a thick outline. The outline must not darken + // pixels that belong to the original glyph fill. + const std::string text = + "00:00.000 --> 00:10.000\nOPEN"; + + openshot::Caption fill_only(text); + fill_only.color = openshot::Color("#ffffff"); + fill_only.stroke = openshot::Color("#000000"); + fill_only.stroke_width = openshot::Keyframe(0.0); + fill_only.font_size = openshot::Keyframe(110.0); + fill_only.top = openshot::Keyframe(0.2); + + openshot::Caption outlined(text); + outlined.color = openshot::Color("#ffffff"); + outlined.stroke = openshot::Color("#000000"); + outlined.stroke_width = openshot::Keyframe(16.0); + outlined.font_size = openshot::Keyframe(110.0); + outlined.top = openshot::Keyframe(0.2); + + // The timeline supplies the FPS used by caption cue timing. + openshot::Timeline timeline( + 600, 300, openshot::Fraction(24, 1), + 44100, 2, openshot::LAYOUT_STEREO); + + timeline.AddEffect(&fill_only); + + auto base_frame = std::make_shared( + 1, 600, 300, "#000000"); + auto base = fill_only.GetFrame(base_frame, 1); + + timeline.RemoveEffect(&fill_only); + timeline.AddEffect(&outlined); + + auto outlined_frame = std::make_shared( + 1, 600, 300, "#000000"); + auto result = outlined.GetFrame(outlined_frame, 1); + + int fill_pixels = 0; + int darkened_pixels = 0; + + for (int y = 0; y < 300; ++y) { + const unsigned char* baseline = base->GetPixels(y); + const unsigned char* actual = result->GetPixels(y); + + for (int x = 0; x < 600; ++x) { + const int index = x * 4; + + if (baseline[index] < 240 || + baseline[index + 1] < 240 || + baseline[index + 2] < 240) + continue; + + ++fill_pixels; + + if (actual[index] < baseline[index] - 10) + ++darkened_pixels; + } + } + + INFO("Fill pixels: " << fill_pixels); + INFO("Darkened fill pixels: " << darkened_pixels); + + REQUIRE(fill_pixels > 0); + CHECK(darkened_pixels == 0); + + timeline.RemoveEffect(&outlined); + timeline.Close(); + } + // Close QApplication app.quit(); }