Merge branch 'develop' into comfy-ui

This commit is contained in:
Jonathan Thomas
2026-02-27 12:19:46 -06:00
13 changed files with 208 additions and 44 deletions

View File

@@ -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;
}
};

View File

@@ -274,7 +274,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)
@@ -1143,6 +1143,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))) {
@@ -2816,10 +2839,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();
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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
@@ -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();
}
}

View File

@@ -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

View File

@@ -64,6 +64,7 @@ set(OPENSHOT_TESTS
# ImageMagick related test files
if($CACHE{HAVE_IMAGEMAGICK})
list(APPEND OPENSHOT_TESTS
ImageReader
ImageWriter
)
endif()

View File

@@ -13,6 +13,10 @@
#include <sstream>
#include <memory>
#include <set>
#include <algorithm>
#include <cstdio>
#include <cstdlib>
#include <ctime>
#include "openshot_catch.h"
@@ -26,8 +30,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]" )
@@ -348,6 +360,68 @@ 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<int>(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);
std::srand(static_cast<unsigned int>(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.str(), 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<int>(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();
std::remove(jpg_path.str().c_str());
}
TEST_CASE( "verify parent Timeline", "[libopenshot][ffmpegreader]" )
{
// Create a reader

View File

@@ -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

View File

@@ -13,18 +13,30 @@
#include "openshot_catch.h"
#include <cstdlib>
#include <sstream>
#include <unistd.h>
#include <QDir>
#include <fstream>
#include <cstdio>
#include "Exceptions.h"
#include "Profiles.h"
static std::string test_output_profile_path(const std::string& base_name) {
std::stringstream path;
path << QDir::currentPath().toStdString() << "/" << base_name
<< "_" << getpid() << "_" << rand();
return path.str();
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]" )
@@ -101,6 +113,38 @@ 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 = get_temp_test_path("__openshot_missing_test_profile__");
std::remove(invalid_path.c_str());
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 = get_temp_test_path("openshot_invalid_profile_for_test.profile");
{
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;

View File

@@ -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]" )

View File

@@ -27,6 +27,7 @@
#include "Frame.h"
#include "Fraction.h"
#include "effects/Brightness.h"
#include "Exceptions.h"
#include "effects/Blur.h"
#include "effects/Negate.h"
@@ -95,6 +96,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);