From 7efc48006a4af533ac8f21cdf250e86fe7120268 Mon Sep 17 00:00:00 2001 From: Jonathan Thomas Date: Mon, 23 Feb 2026 16:55:49 -0600 Subject: [PATCH 1/7] Modify all throw InvalidFile(...) callsites: each now has a distinct message string, and adding unit tests to verify this. So a crash line will now be more like: what(): FFmpegReader could not open media file. for file C:\...\TitleFileName%04d.png instead of only File could not be opened. --- src/Exceptions.h | 18 ++++++++++++------ src/FFmpegReader.cpp | 2 +- src/ImageReader.cpp | 2 +- src/Profiles.cpp | 4 ++-- src/QtImageReader.cpp | 2 +- src/Timeline.cpp | 2 +- tests/CMakeLists.txt | 1 + tests/FFmpegReader.cpp | 12 ++++++++++-- tests/ImageReader.cpp | 14 ++++++++++++++ tests/Profiles.cpp | 35 ++++++++++++++++++++++++++++++++++- tests/QtImageReader.cpp | 12 ++++++++++-- tests/Timeline.cpp | 14 ++++++++++++++ 12 files changed, 101 insertions(+), 17 deletions(-) diff --git a/src/Exceptions.h b/src/Exceptions.h index 9e2f2b83..60a55c14 100644 --- a/src/Exceptions.h +++ b/src/Exceptions.h @@ -62,15 +62,21 @@ namespace openshot { { public: std::string file_path; + std::string full_message; FileExceptionBase(std::string message, std::string file_path="") - : ExceptionBase(message), file_path(file_path) { } - virtual std::string py_message() const override { - // return complete message for Python exception handling - std::string out_msg(m_message + + : ExceptionBase(message), + file_path(file_path), + full_message(message + (file_path != "" ? " for file " + file_path - : "")); - return out_msg; + : "")) { } + virtual const char* what() const noexcept override { + // Include file path in native C++ exception output (stderr / terminate). + return full_message.c_str(); + } + virtual std::string py_message() const override { + // Keep Python exception output consistent with what(). + return full_message; } }; diff --git a/src/FFmpegReader.cpp b/src/FFmpegReader.cpp index 2ffa75c0..83731e05 100644 --- a/src/FFmpegReader.cpp +++ b/src/FFmpegReader.cpp @@ -248,7 +248,7 @@ void FFmpegReader::Open() { // Open video file if (avformat_open_input(&pFormatCtx, path.c_str(), NULL, NULL) != 0) - throw InvalidFile("File could not be opened.", path); + throw InvalidFile("FFmpegReader could not open media file.", path); // Retrieve stream information if (avformat_find_stream_info(pFormatCtx, NULL) < 0) diff --git a/src/ImageReader.cpp b/src/ImageReader.cpp index e3342a41..0b9c4abd 100644 --- a/src/ImageReader.cpp +++ b/src/ImageReader.cpp @@ -49,7 +49,7 @@ void ImageReader::Open() } catch (const Magick::Exception& e) { // raise exception - throw InvalidFile("File could not be opened.", path); + throw InvalidFile("ImageReader could not open image file.", path); } // Update image properties diff --git a/src/Profiles.cpp b/src/Profiles.cpp index d76557d4..088a6b1f 100644 --- a/src/Profiles.cpp +++ b/src/Profiles.cpp @@ -129,13 +129,13 @@ Profile::Profile(std::string path) { catch (const std::exception& e) { // Error parsing profile file - throw InvalidFile("Profile could not be found or loaded (or is invalid).", path); + throw InvalidFile("Profile file could not be parsed (invalid format or values).", path); } // Throw error if file was not read if (!read_file) // Error parsing profile file - throw InvalidFile("Profile could not be found or loaded (or is invalid).", path); + throw InvalidFile("Profile file could not be found or opened.", path); } // Return a formatted FPS diff --git a/src/QtImageReader.cpp b/src/QtImageReader.cpp index ffd20862..e11e813b 100644 --- a/src/QtImageReader.cpp +++ b/src/QtImageReader.cpp @@ -77,7 +77,7 @@ void QtImageReader::Open() if (!loaded) { // raise exception - throw InvalidFile("File could not be opened.", path.toStdString()); + throw InvalidFile("QtImageReader could not open image file.", path.toStdString()); } // Update image properties diff --git a/src/Timeline.cpp b/src/Timeline.cpp index 3cf0bd67..9b828067 100644 --- a/src/Timeline.cpp +++ b/src/Timeline.cpp @@ -102,7 +102,7 @@ Timeline::Timeline(const std::string& projectPath, bool convert_absolute_paths) // Check if path exists QFileInfo filePath(QString::fromStdString(path)); if (!filePath.exists()) { - throw InvalidFile("File could not be opened.", path); + throw InvalidFile("Timeline project file could not be opened.", path); } // Check OpenShot Install Path exists diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 50d71b3f..e1e0ed18 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -62,6 +62,7 @@ set(OPENSHOT_TESTS # ImageMagick related test files if($CACHE{HAVE_IMAGEMAGICK}) list(APPEND OPENSHOT_TESTS + ImageReader ImageWriter ) endif() diff --git a/tests/FFmpegReader.cpp b/tests/FFmpegReader.cpp index 0542df6b..84b01c5c 100644 --- a/tests/FFmpegReader.cpp +++ b/tests/FFmpegReader.cpp @@ -26,8 +26,16 @@ using namespace openshot; TEST_CASE( "Invalid_Path", "[libopenshot][ffmpegreader]" ) { - // Check invalid path - CHECK_THROWS_AS(FFmpegReader(""), InvalidFile); + // Check invalid path and error details + const std::string invalid_path = "/tmp/__openshot_missing_test_file__.mp4"; + try { + FFmpegReader r(invalid_path); + FAIL("Expected InvalidFile for missing media path"); + } catch (const InvalidFile& e) { + const std::string message = e.what(); + CHECK(message.find("FFmpegReader could not open media file.") != std::string::npos); + CHECK(message.find(invalid_path) != std::string::npos); + } } TEST_CASE( "GetFrame_Before_Opening", "[libopenshot][ffmpegreader]" ) diff --git a/tests/ImageReader.cpp b/tests/ImageReader.cpp index 9232fb08..8e81d08c 100644 --- a/tests/ImageReader.cpp +++ b/tests/ImageReader.cpp @@ -19,6 +19,20 @@ using namespace openshot; +TEST_CASE( "Invalid_Path_ImageReader", "[libopenshot][imagereader]" ) +{ + // Check invalid path and error details + const std::string invalid_path = "/tmp/__openshot_missing_test_file__.png"; + try { + ImageReader r(invalid_path); + FAIL("Expected InvalidFile for missing image path"); + } catch (const InvalidFile& e) { + const std::string message = e.what(); + CHECK(message.find("ImageReader could not open image file.") != std::string::npos); + CHECK(message.find(invalid_path) != std::string::npos); + } +} + TEST_CASE( "Duration_And_Length_ImageReader", "[libopenshot][imagereader]" ) { // Create a reader diff --git a/tests/Profiles.cpp b/tests/Profiles.cpp index 8e319a3b..677862c8 100644 --- a/tests/Profiles.cpp +++ b/tests/Profiles.cpp @@ -12,8 +12,10 @@ #include "openshot_catch.h" #include +#include +#include "Exceptions.h" #include "Profiles.h" TEST_CASE( "empty constructor", "[libopenshot][profile]" ) @@ -90,6 +92,37 @@ TEST_CASE( "constructor with example profiles", "[libopenshot][profile]" ) CHECK(p2.info.spherical == false); } +TEST_CASE( "invalid profile path message", "[libopenshot][profile]" ) +{ + const std::string invalid_path = "/tmp/__openshot_missing_test_profile__"; + try { + openshot::Profile p(invalid_path); + FAIL("Expected InvalidFile for missing profile path"); + } catch (const openshot::InvalidFile& e) { + const std::string message = e.what(); + CHECK(message.find("Profile file could not be found or opened.") != std::string::npos); + CHECK(message.find(invalid_path) != std::string::npos); + } +} + +TEST_CASE( "invalid profile parse message", "[libopenshot][profile]" ) +{ + const std::string invalid_profile = "/tmp/openshot_invalid_profile_for_test"; + { + std::ofstream f(invalid_profile); + f << "width=abc\n"; + } + + try { + openshot::Profile p(invalid_profile); + FAIL("Expected InvalidFile for malformed profile contents"); + } catch (const openshot::InvalidFile& e) { + const std::string message = e.what(); + CHECK(message.find("Profile file could not be parsed (invalid format or values).") != std::string::npos); + CHECK(message.find(invalid_profile) != std::string::npos); + } +} + TEST_CASE( "24 fps names", "[libopenshot][profile]" ) { std::stringstream path; @@ -232,4 +265,4 @@ TEST_CASE( "spherical profiles", "[libopenshot][profile]" ) p_non_spherical.info.spherical = false; CHECK_FALSE(p == p_non_spherical); -} \ No newline at end of file +} diff --git a/tests/QtImageReader.cpp b/tests/QtImageReader.cpp index e48839c3..6c39774b 100644 --- a/tests/QtImageReader.cpp +++ b/tests/QtImageReader.cpp @@ -25,8 +25,16 @@ using namespace openshot; TEST_CASE( "Default_Constructor", "[libopenshot][qtimagereader]" ) { - // Check invalid path - CHECK_THROWS_AS(QtImageReader(""), InvalidFile); + // Check invalid path and error details + const std::string invalid_path = "/tmp/__openshot_missing_test_file__.png"; + try { + QtImageReader r(invalid_path); + FAIL("Expected InvalidFile for missing image path"); + } catch (const InvalidFile& e) { + const std::string message = e.what(); + CHECK(message.find("QtImageReader could not open image file.") != std::string::npos); + CHECK(message.find(invalid_path) != std::string::npos); + } } TEST_CASE( "GetFrame_Before_Opening", "[libopenshot][qtimagereader]" ) diff --git a/tests/Timeline.cpp b/tests/Timeline.cpp index 0c90fbd7..d9c0f02d 100644 --- a/tests/Timeline.cpp +++ b/tests/Timeline.cpp @@ -23,6 +23,7 @@ #include "Clip.h" #include "Frame.h" #include "Fraction.h" +#include "Exceptions.h" #include "effects/Blur.h" #include "effects/Negate.h" @@ -44,6 +45,19 @@ TEST_CASE( "constructor", "[libopenshot][timeline]" ) CHECK(t2.info.height == 240); } +TEST_CASE( "project constructor invalid path message", "[libopenshot][timeline]" ) +{ + const std::string invalid_path = "/tmp/__openshot_missing_test_project__.osp"; + try { + Timeline t(invalid_path, true); + FAIL("Expected InvalidFile for missing timeline project path"); + } catch (const InvalidFile& e) { + const std::string message = e.what(); + CHECK(message.find("Timeline project file could not be opened.") != std::string::npos); + CHECK(message.find(invalid_path) != std::string::npos); + } +} + TEST_CASE( "Set Json and clear clips", "[libopenshot][timeline]" ) { Fraction fps(30000,1000); From 7c65748187e13f690043389c9d8c746757b0e372 Mon Sep 17 00:00:00 2001 From: Jonathan Thomas Date: Mon, 23 Feb 2026 17:25:23 -0600 Subject: [PATCH 2/7] Fixing unit test paths for windows builders --- tests/Profiles.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/Profiles.cpp b/tests/Profiles.cpp index 677862c8..eb26fe73 100644 --- a/tests/Profiles.cpp +++ b/tests/Profiles.cpp @@ -13,6 +13,8 @@ #include "openshot_catch.h" #include #include +#include +#include #include "Exceptions.h" @@ -94,7 +96,9 @@ TEST_CASE( "constructor with example profiles", "[libopenshot][profile]" ) TEST_CASE( "invalid profile path message", "[libopenshot][profile]" ) { - const std::string invalid_path = "/tmp/__openshot_missing_test_profile__"; + const std::string invalid_path = + (std::filesystem::temp_directory_path() / "__openshot_missing_test_profile__").string(); + std::remove(invalid_path.c_str()); try { openshot::Profile p(invalid_path); FAIL("Expected InvalidFile for missing profile path"); @@ -107,7 +111,8 @@ TEST_CASE( "invalid profile path message", "[libopenshot][profile]" ) TEST_CASE( "invalid profile parse message", "[libopenshot][profile]" ) { - const std::string invalid_profile = "/tmp/openshot_invalid_profile_for_test"; + const std::string invalid_profile = + (std::filesystem::temp_directory_path() / "openshot_invalid_profile_for_test.profile").string(); { std::ofstream f(invalid_profile); f << "width=abc\n"; From 18b08fe7c6c5a546e939511218380cf6a93c5c34 Mon Sep 17 00:00:00 2001 From: Jonathan Thomas Date: Mon, 23 Feb 2026 17:41:18 -0600 Subject: [PATCH 3/7] Fixing unit test paths for mac builders, crashing due to filesystem import (I think) --- tests/Profiles.cpp | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/tests/Profiles.cpp b/tests/Profiles.cpp index eb26fe73..13d1b94b 100644 --- a/tests/Profiles.cpp +++ b/tests/Profiles.cpp @@ -13,13 +13,32 @@ #include "openshot_catch.h" #include #include -#include #include +#include #include "Exceptions.h" #include "Profiles.h" +static std::string get_temp_test_path(const std::string& file_name) { +#ifdef _WIN32 + const char* base = std::getenv("TEMP"); + if (!base || !*base) { + base = std::getenv("TMP"); + } + if (!base || !*base) { + base = "."; + } + return std::string(base) + "\\" + file_name; +#else + const char* base = std::getenv("TMPDIR"); + if (!base || !*base) { + base = "/tmp"; + } + return std::string(base) + "/" + file_name; +#endif +} + TEST_CASE( "empty constructor", "[libopenshot][profile]" ) { openshot::Profile p1; @@ -96,8 +115,7 @@ TEST_CASE( "constructor with example profiles", "[libopenshot][profile]" ) TEST_CASE( "invalid profile path message", "[libopenshot][profile]" ) { - const std::string invalid_path = - (std::filesystem::temp_directory_path() / "__openshot_missing_test_profile__").string(); + const std::string invalid_path = get_temp_test_path("__openshot_missing_test_profile__"); std::remove(invalid_path.c_str()); try { openshot::Profile p(invalid_path); @@ -111,8 +129,7 @@ TEST_CASE( "invalid profile path message", "[libopenshot][profile]" ) TEST_CASE( "invalid profile parse message", "[libopenshot][profile]" ) { - const std::string invalid_profile = - (std::filesystem::temp_directory_path() / "openshot_invalid_profile_for_test.profile").string(); + const std::string invalid_profile = get_temp_test_path("openshot_invalid_profile_for_test.profile"); { std::ofstream f(invalid_profile); f << "width=abc\n"; From 9d3a3f18e5881381d2d21200ccd5bcbe457d4a0d Mon Sep 17 00:00:00 2001 From: Jonathan Thomas Date: Mon, 23 Feb 2026 22:40:44 -0600 Subject: [PATCH 4/7] Protecting Mask GetFrame from failure opening reader (Experimental for windows crash we are debugging) --- src/effects/Mask.cpp | 54 ++++++++++++++++++++++++++++++++------------ 1 file changed, 40 insertions(+), 14 deletions(-) diff --git a/src/effects/Mask.cpp b/src/effects/Mask.cpp index ea5fa887..cb49ec6e 100644 --- a/src/effects/Mask.cpp +++ b/src/effects/Mask.cpp @@ -18,6 +18,7 @@ #include "ChunkReader.h" #include "FFmpegReader.h" #include "QtImageReader.h" +#include "ZmqLogger.h" #include #ifdef USE_IMAGEMAGICK @@ -59,16 +60,28 @@ void Mask::init_effect_details() std::shared_ptr Mask::GetFrame(std::shared_ptr frame, int64_t frame_number) { // Get the mask image (from the mask reader) std::shared_ptr frame_image = frame->GetImage(); + bool mask_reader_failed = false; // Check if mask reader is open #pragma omp critical (open_mask_reader) { - if (reader && !reader->IsOpen()) - reader->Open(); + if (reader && !reader->IsOpen()) { + try { + reader->Open(); + } catch (const std::exception& e) { + // Invalid/missing mask source should never crash frame rendering. + ZmqLogger::Instance()->Log( + std::string("Mask::GetFrame unable to open mask reader: ") + e.what()); + delete reader; + reader = NULL; + needs_refresh = true; + mask_reader_failed = true; + } + } } // No reader (bail on applying the mask) - if (!reader) + if (!reader || mask_reader_failed) return frame; // Get mask image (if missing or different size than frame image) @@ -78,16 +91,29 @@ std::shared_ptr Mask::GetFrame(std::shared_ptr (original_mask && original_mask->size() != frame_image->size())) { // Only get mask if needed - auto mask_without_sizing = std::make_shared( - *reader->GetFrame(frame_number)->GetImage()); - - // Resize mask image to match frame size - original_mask = std::make_shared( - mask_without_sizing->scaled( - frame_image->width(), frame_image->height(), - Qt::IgnoreAspectRatio, Qt::SmoothTransformation)); + std::shared_ptr mask_without_sizing; + try { + mask_without_sizing = std::make_shared( + *reader->GetFrame(frame_number)->GetImage()); + } catch (const std::exception& e) { + ZmqLogger::Instance()->Log( + std::string("Mask::GetFrame unable to read mask frame: ") + e.what()); + delete reader; + reader = NULL; + needs_refresh = true; + mask_reader_failed = true; + } + if (!mask_reader_failed && mask_without_sizing) { + // Resize mask image to match frame size + original_mask = std::make_shared( + mask_without_sizing->scaled( + frame_image->width(), frame_image->height(), + Qt::IgnoreAspectRatio, Qt::SmoothTransformation)); + } } } + if (mask_reader_failed || !reader || !original_mask) + return frame; // Once we've done the necessary resizing, we no longer need to refresh again needs_refresh = false; @@ -230,21 +256,21 @@ void Mask::SetJsonValue(const Json::Value root) { if (type == "FFmpegReader") { // Create new reader - reader = new FFmpegReader(root["reader"]["path"].asString()); + reader = new FFmpegReader(root["reader"]["path"].asString(), false); reader->SetJsonValue(root["reader"]); #ifdef USE_IMAGEMAGICK } else if (type == "ImageReader") { // Create new reader - reader = new ImageReader(root["reader"]["path"].asString()); + reader = new ImageReader(root["reader"]["path"].asString(), false); reader->SetJsonValue(root["reader"]); #endif } else if (type == "QtImageReader") { // Create new reader - reader = new QtImageReader(root["reader"]["path"].asString()); + reader = new QtImageReader(root["reader"]["path"].asString(), false); reader->SetJsonValue(root["reader"]); } else if (type == "ChunkReader") { From f83802bdc07a0d184943bd6b8e5bdedf78e25931 Mon Sep 17 00:00:00 2001 From: Jonathan Thomas Date: Mon, 23 Feb 2026 22:51:32 -0600 Subject: [PATCH 5/7] Removing some code that re-inits readers and re-opens readers after SetJsonValue is called. I don't think this is needed anymore, but let's mark this as Experimental and risky commit though. --- src/FFmpegReader.cpp | 6 ------ src/FrameMapper.cpp | 7 ------- src/QtImageReader.cpp | 7 ------- 3 files changed, 20 deletions(-) diff --git a/src/FFmpegReader.cpp b/src/FFmpegReader.cpp index 83731e05..e0aac8b3 100644 --- a/src/FFmpegReader.cpp +++ b/src/FFmpegReader.cpp @@ -2769,10 +2769,4 @@ void FFmpegReader::SetJsonValue(const Json::Value root) { duration_strategy = DurationStrategy::LongestStream; } } - - // Re-Open path, and re-init everything (if needed) - if (is_open) { - Close(); - Open(); - } } diff --git a/src/FrameMapper.cpp b/src/FrameMapper.cpp index 5e678b5b..937f828b 100644 --- a/src/FrameMapper.cpp +++ b/src/FrameMapper.cpp @@ -800,13 +800,6 @@ void FrameMapper::SetJsonValue(const Json::Value root) { // Set parent data ReaderBase::SetJsonValue(root); - - // Re-Open path, and re-init everything (if needed) - if (reader) { - - Close(); - Open(); - } } // Change frame rate or audio mapping details diff --git a/src/QtImageReader.cpp b/src/QtImageReader.cpp index e11e813b..5504a808 100644 --- a/src/QtImageReader.cpp +++ b/src/QtImageReader.cpp @@ -367,11 +367,4 @@ void QtImageReader::SetJsonValue(const Json::Value root) { // Set data from Json (if key is found) if (!root["path"].isNull()) path = QString::fromStdString(root["path"].asString()); - - // Re-Open path, and re-init everything (if needed) - if (is_open) - { - Close(); - Open(); - } } From 89350a0419089f56a4508e411acfcadce09e158f Mon Sep 17 00:00:00 2001 From: Jonathan Thomas Date: Tue, 24 Feb 2026 12:06:42 -0600 Subject: [PATCH 6/7] Setting has_static_image correctly when FFmpegReader opens image formats and a video_length == 1, so we have consistent behavior in OpenShot regardless of QtImageReader or FFmpegReader, when opening images. --- src/FFmpegReader.cpp | 23 +++++++++++++++ tests/FFmpegReader.cpp | 63 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+) diff --git a/src/FFmpegReader.cpp b/src/FFmpegReader.cpp index e0aac8b3..79f761de 100644 --- a/src/FFmpegReader.cpp +++ b/src/FFmpegReader.cpp @@ -1117,6 +1117,29 @@ void FFmpegReader::UpdateVideoInfo() { ApplyDurationStrategy(); + // Normalize FFmpeg-decoded still images (e.g. JPG/JPEG) to match image-reader behavior. + // This keeps timing/flags consistent regardless of which reader path was used. + if (!info.has_single_image && audioStream < 0) { + const AVCodecID codec_id = AV_FIND_DECODER_CODEC_ID(pStream); + const bool likely_still_codec = + codec_id == AV_CODEC_ID_MJPEG || + codec_id == AV_CODEC_ID_PNG || + codec_id == AV_CODEC_ID_BMP || + codec_id == AV_CODEC_ID_TIFF || + codec_id == AV_CODEC_ID_WEBP || + codec_id == AV_CODEC_ID_JPEG2000; + const bool likely_image_demuxer = + pFormatCtx && pFormatCtx->iformat && pFormatCtx->iformat->name && + strstr(pFormatCtx->iformat->name, "image2"); + const bool single_frame_clip = info.video_length <= 1; + + if (single_frame_clip && (likely_still_codec || likely_image_demuxer)) { + info.has_single_image = true; + record_duration(video_stream_duration_seconds, 60 * 60 * 1); // 1 hour duration + ApplyDurationStrategy(); + } + } + // Add video metadata (if any) AVDictionaryEntry *tag = NULL; while ((tag = av_dict_get(pStream->metadata, "", tag, AV_DICT_IGNORE_SUFFIX))) { diff --git a/tests/FFmpegReader.cpp b/tests/FFmpegReader.cpp index 84b01c5c..7e85aff0 100644 --- a/tests/FFmpegReader.cpp +++ b/tests/FFmpegReader.cpp @@ -13,6 +13,8 @@ #include #include #include +#include +#include #include "openshot_catch.h" @@ -23,6 +25,7 @@ #include "Json.h" using namespace openshot; +namespace fs = std::filesystem; TEST_CASE( "Invalid_Path", "[libopenshot][ffmpegreader]" ) { @@ -356,6 +359,66 @@ TEST_CASE( "Multiple_Open_and_Close", "[libopenshot][ffmpegreader]" ) r.Close(); } +TEST_CASE( "Static_Image_PNG_Reports_Single_Image", "[libopenshot][ffmpegreader]" ) +{ + std::stringstream path; + path << TEST_MEDIA_PATH << "front.png"; + FFmpegReader r(path.str(), DurationStrategy::VideoPreferred); + r.Open(); + + CHECK(r.info.has_video); + CHECK_FALSE(r.info.has_audio); + CHECK(r.info.has_single_image); + CHECK(r.info.video_length > 1); + CHECK(r.info.duration > 1000.0f); + + auto f1 = r.GetFrame(1); + auto f2 = r.GetFrame(std::min(2, static_cast(r.info.video_length))); + CHECK(f1->CheckPixel(50, 50, + f2->GetPixels(50)[50 * 4 + 0], + f2->GetPixels(50)[50 * 4 + 1], + f2->GetPixels(50)[50 * 4 + 2], + f2->GetPixels(50)[50 * 4 + 3], + 0)); + + r.Close(); +} + +TEST_CASE( "Static_Image_JPG_Reports_Single_Image", "[libopenshot][ffmpegreader]" ) +{ + // Generate a JPG fixture at runtime from a known PNG frame. + std::stringstream png_path; + png_path << TEST_MEDIA_PATH << "front.png"; + FFmpegReader png_reader(png_path.str()); + png_reader.Open(); + + auto png_frame = png_reader.GetFrame(1); + const fs::path jpg_path = fs::temp_directory_path() / "libopenshot-static-image-test.jpg"; + REQUIRE(png_frame->GetImage()->save(jpg_path.string().c_str(), "JPG")); + png_reader.Close(); + + FFmpegReader jpg_reader(jpg_path.string(), DurationStrategy::VideoPreferred); + jpg_reader.Open(); + + CHECK(jpg_reader.info.has_video); + CHECK_FALSE(jpg_reader.info.has_audio); + CHECK(jpg_reader.info.has_single_image); + CHECK(jpg_reader.info.video_length > 1); + CHECK(jpg_reader.info.duration > 1000.0f); + + auto f1 = jpg_reader.GetFrame(1); + auto f2 = jpg_reader.GetFrame(std::min(2, static_cast(jpg_reader.info.video_length))); + CHECK(f1->CheckPixel(50, 50, + f2->GetPixels(50)[50 * 4 + 0], + f2->GetPixels(50)[50 * 4 + 1], + f2->GetPixels(50)[50 * 4 + 2], + f2->GetPixels(50)[50 * 4 + 3], + 2)); + + jpg_reader.Close(); + fs::remove(jpg_path); +} + TEST_CASE( "verify parent Timeline", "[libopenshot][ffmpegreader]" ) { // Create a reader From 1a44a6582c828380ef90748e4a6f68b253bb043d Mon Sep 17 00:00:00 2001 From: Jonathan Thomas Date: Tue, 24 Feb 2026 13:05:39 -0600 Subject: [PATCH 7/7] Fixing mac unit test failures due to filesystem dependency again,ughh. --- tests/FFmpegReader.cpp | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/tests/FFmpegReader.cpp b/tests/FFmpegReader.cpp index 7e85aff0..8c83d10e 100644 --- a/tests/FFmpegReader.cpp +++ b/tests/FFmpegReader.cpp @@ -14,7 +14,9 @@ #include #include #include -#include +#include +#include +#include #include "openshot_catch.h" @@ -25,7 +27,6 @@ #include "Json.h" using namespace openshot; -namespace fs = std::filesystem; TEST_CASE( "Invalid_Path", "[libopenshot][ffmpegreader]" ) { @@ -393,11 +394,13 @@ TEST_CASE( "Static_Image_JPG_Reports_Single_Image", "[libopenshot][ffmpegreader] png_reader.Open(); auto png_frame = png_reader.GetFrame(1); - const fs::path jpg_path = fs::temp_directory_path() / "libopenshot-static-image-test.jpg"; - REQUIRE(png_frame->GetImage()->save(jpg_path.string().c_str(), "JPG")); + std::srand(static_cast(std::time(nullptr))); + std::stringstream jpg_path; + jpg_path << "libopenshot-static-image-test-" << std::rand() << ".jpg"; + REQUIRE(png_frame->GetImage()->save(jpg_path.str().c_str(), "JPG")); png_reader.Close(); - FFmpegReader jpg_reader(jpg_path.string(), DurationStrategy::VideoPreferred); + FFmpegReader jpg_reader(jpg_path.str(), DurationStrategy::VideoPreferred); jpg_reader.Open(); CHECK(jpg_reader.info.has_video); @@ -416,7 +419,7 @@ TEST_CASE( "Static_Image_JPG_Reports_Single_Image", "[libopenshot][ffmpegreader] 2)); jpg_reader.Close(); - fs::remove(jpg_path); + std::remove(jpg_path.str().c_str()); } TEST_CASE( "verify parent Timeline", "[libopenshot][ffmpegreader]" )