You've already forked libopenshot
mirror of
https://github.com/OpenShot/libopenshot.git
synced 2026-03-02 08:53:52 -08:00
Merge branch 'develop' into comfy-ui
This commit is contained in:
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -64,6 +64,7 @@ set(OPENSHOT_TESTS
|
||||
# ImageMagick related test files
|
||||
if($CACHE{HAVE_IMAGEMAGICK})
|
||||
list(APPEND OPENSHOT_TESTS
|
||||
ImageReader
|
||||
ImageWriter
|
||||
)
|
||||
endif()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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]" )
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user