diff --git a/examples/1F0CF.svg b/examples/1F0CF.svg new file mode 100644 index 000000000..4b39d7dad --- /dev/null +++ b/examples/1F0CF.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/QtImageReader.cpp b/src/QtImageReader.cpp index 90af26a0d..01b023259 100644 --- a/src/QtImageReader.cpp +++ b/src/QtImageReader.cpp @@ -33,9 +33,11 @@ #include "Settings.h" #include "Clip.h" #include "CacheMemory.h" +#include "Timeline.h" #include #include #include +#include #if USE_RESVG == 1 // If defined and found in CMake, utilize the libresvg for parsing @@ -64,37 +66,20 @@ void QtImageReader::Open() // Open reader if not already open if (!is_open) { - bool success = true; bool loaded = false; -#if USE_RESVG == 1 - // If defined and found in CMake, utilize the libresvg for parsing - // SVG files and rasterizing them to QImages. - // Only use resvg for files ending in '.svg' or '.svgz' - if (path.toLower().endsWith(".svg") || path.toLower().endsWith(".svgz")) { - - ResvgRenderer renderer(path); - if (renderer.isValid()) { - - image = std::make_shared( - renderer.defaultSize(), QImage::Format_RGBA8888_Premultiplied); - image->fill(Qt::transparent); - - QPainter p(image.get()); - renderer.render(&p); - p.end(); - loaded = true; - } - } -#endif + // Check for SVG files and rasterizing them to QImages + if (path.toLower().endsWith(".svg") || path.toLower().endsWith(".svgz")) { + loaded = load_svg_path(path); + } if (!loaded) { // Attempt to open file using Qt's build in image processing capabilities image = std::make_shared(); - success = image->load(path); + loaded = image->load(path); } - if (!success) { + if (!loaded) { // raise exception throw InvalidFile("File could not be opened.", path.toStdString()); } @@ -167,94 +152,24 @@ std::shared_ptr QtImageReader::GetFrame(int64_t requested_frame) // Create a scoped lock, allowing only a single thread to run the following code at one time const GenericScopedLock lock(getFrameCriticalSection); - // Determine the max size of this source image (based on the timeline's size, the scaling mode, - // and the scaling keyframes). This is a performance improvement, to keep the images as small as possible, - // without losing quality. NOTE: We cannot go smaller than the timeline itself, or the add_layer timeline - // method will scale it back to timeline size before scaling it smaller again. This needs to be fixed in - // the future. - int max_width = info.width; - int max_height = info.height; - - Clip* parent = (Clip*) ParentClip(); - if (parent) { - if (parent->ParentTimeline()) { - // Set max width/height based on parent clip's timeline (if attached to a timeline) - max_width = parent->ParentTimeline()->preview_width; - max_height = parent->ParentTimeline()->preview_height; - } - if (parent->scale == SCALE_FIT || parent->scale == SCALE_STRETCH) { - // Best fit or Stretch scaling (based on max timeline size * scaling keyframes) - float max_scale_x = parent->scale_x.GetMaxPoint().co.Y; - float max_scale_y = parent->scale_y.GetMaxPoint().co.Y; - max_width = std::max(float(max_width), max_width * max_scale_x); - max_height = std::max(float(max_height), max_height * max_scale_y); - - } else if (parent->scale == SCALE_CROP) { - // Cropping scale mode (based on max timeline size * cropped size * scaling keyframes) - float max_scale_x = parent->scale_x.GetMaxPoint().co.Y; - float max_scale_y = parent->scale_y.GetMaxPoint().co.Y; - QSize width_size(max_width * max_scale_x, - round(max_width / (float(info.width) / float(info.height)))); - QSize height_size(round(max_height / (float(info.height) / float(info.width))), - max_height * max_scale_y); - // respect aspect ratio - if (width_size.width() >= max_width && width_size.height() >= max_height) { - max_width = std::max(max_width, width_size.width()); - max_height = std::max(max_height, width_size.height()); - } - else { - max_width = std::max(max_width, height_size.width()); - max_height = std::max(max_height, height_size.height()); - } - - } else { - // No scaling, use original image size (slower) - max_width = info.width; - max_height = info.height; - } - } + // Calculate max image size + QSize current_max_size = calculate_max_size(); // Scale image smaller (or use a previous scaled image) - if (!cached_image || (max_size.width() != max_width || max_size.height() != max_height)) { + if (!cached_image || (max_size.width() != current_max_size.width() || max_size.height() != current_max_size.height())) { + // Check for SVG files and rasterize them to QImages + if (path.toLower().endsWith(".svg") || path.toLower().endsWith(".svgz")) { + load_svg_path(path); + } - bool rendered = false; -#if USE_RESVG == 1 - // If defined and found in CMake, utilize the libresvg for parsing - // SVG files and rasterizing them to QImages. - // Only use resvg for files ending in '.svg' or '.svgz' - if (path.toLower().endsWith(".svg") || path.toLower().endsWith(".svgz")) { - - ResvgRenderer renderer(path); - if (renderer.isValid()) { - // Scale SVG size to keep aspect ratio, and fill the max_size as best as possible - QSize svg_size(renderer.defaultSize().width(), renderer.defaultSize().height()); - svg_size.scale(max_width, max_height, Qt::KeepAspectRatio); - - // Create empty QImage - cached_image = std::make_shared( - QSize(svg_size.width(), svg_size.height()), - QImage::Format_RGBA8888_Premultiplied); - cached_image->fill(Qt::transparent); - - // Render SVG into QImage - QPainter p(cached_image.get()); - renderer.render(&p); - p.end(); - rendered = true; - } - } -#endif - - if (!rendered) { - // We need to resize the original image to a smaller image (for performance reasons) - // Only do this once, to prevent tons of unneeded scaling operations - cached_image = std::make_shared(image->scaled( - max_width, max_height, Qt::KeepAspectRatio, Qt::SmoothTransformation)); - } + // We need to resize the original image to a smaller image (for performance reasons) + // Only do this once, to prevent tons of unneeded scaling operations + cached_image = std::make_shared(image->scaled( + current_max_size.width(), current_max_size.height(), Qt::KeepAspectRatio, Qt::SmoothTransformation)); // Set max size (to later determine if max_size is changed) - max_size.setWidth(max_width); - max_size.setHeight(max_height); + max_size.setWidth(current_max_size.width()); + max_size.setHeight(current_max_size.height()); } // Create or get frame object @@ -270,6 +185,103 @@ std::shared_ptr QtImageReader::GetFrame(int64_t requested_frame) return image_frame; } +// Calculate the max_size QSize, based on parent timeline and parent clip settings +QSize QtImageReader::calculate_max_size() { + // Get max project size + int max_width = info.width; + int max_height = info.height; + if (max_width == 0 || max_height == 0) { + // If no size determined yet, default to 4K + max_width = 1920; + max_height = 1080; + } + + Clip* parent = (Clip*) ParentClip(); + if (parent) { + if (parent->ParentTimeline()) { + // Set max width/height based on parent clip's timeline (if attached to a timeline) + max_width = parent->ParentTimeline()->preview_width; + max_height = parent->ParentTimeline()->preview_height; + } + if (parent->scale == SCALE_FIT || parent->scale == SCALE_STRETCH) { + // Best fit or Stretch scaling (based on max timeline size * scaling keyframes) + float max_scale_x = parent->scale_x.GetMaxPoint().co.Y; + float max_scale_y = parent->scale_y.GetMaxPoint().co.Y; + max_width = std::max(float(max_width), max_width * max_scale_x); + max_height = std::max(float(max_height), max_height * max_scale_y); + + } else if (parent->scale == SCALE_CROP) { + // Cropping scale mode (based on max timeline size * cropped size * scaling keyframes) + float max_scale_x = parent->scale_x.GetMaxPoint().co.Y; + float max_scale_y = parent->scale_y.GetMaxPoint().co.Y; + QSize width_size(max_width * max_scale_x, + round(max_width / (float(info.width) / float(info.height)))); + QSize height_size(round(max_height / (float(info.height) / float(info.width))), + max_height * max_scale_y); + // respect aspect ratio + if (width_size.width() >= max_width && width_size.height() >= max_height) { + max_width = std::max(max_width, width_size.width()); + max_height = std::max(max_height, width_size.height()); + } + else { + max_width = std::max(max_width, height_size.width()); + max_height = std::max(max_height, height_size.height()); + } + } + } + + // Return new QSize of the current max size + return QSize(max_width, max_height); +} + +// Load an SVG file with Resvg or fallback with Qt +bool QtImageReader::load_svg_path(QString) { + bool loaded = false; + + // Calculate max image size + QSize current_max_size = calculate_max_size(); + +#if USE_RESVG == 1 + // Use libresvg for parsing/rasterizing SVG + ResvgRenderer renderer(path); + if (renderer.isValid()) { + // Scale SVG size to keep aspect ratio, and fill the max_size as best as possible + QSize svg_size(renderer.defaultSize().width(), renderer.defaultSize().height()); + svg_size.scale(current_max_size.width(), current_max_size.height(), Qt::KeepAspectRatio); + + // Load SVG at max size + image = std::make_shared(svg_size, QImage::Format_RGBA8888_Premultiplied); + image->fill(Qt::transparent); + QPainter p(image.get()); + renderer.render(&p); + p.end(); + loaded = true; + } +#endif + + if (!loaded) { + // Use Qt for parsing/rasterizing SVG + image = std::make_shared(); + loaded = image->load(path); + + if (loaded && (image->width() < current_max_size.width() || image->height() < current_max_size.height())) { + // Load SVG into larger/project size (so image is not blurry) + QSize svg_size = image->size().scaled(current_max_size.width(), current_max_size.height(), Qt::KeepAspectRatio); + if (QCoreApplication::instance()) { + // Requires QApplication to be running (for QPixmap support) + // Re-rasterize SVG image to max size + image = std::make_shared(QIcon(path).pixmap(svg_size).toImage()); + } else { + // Scale image without re-rasterizing it (due to lack of QApplication) + image = std::make_shared(image->scaled( + svg_size.width(), svg_size.height(), Qt::KeepAspectRatio, Qt::SmoothTransformation)); + } + } + } + + return loaded; +} + // Generate JSON string of this object std::string QtImageReader::Json() const { diff --git a/src/QtImageReader.h b/src/QtImageReader.h index 1150b4636..a489168f0 100644 --- a/src/QtImageReader.h +++ b/src/QtImageReader.h @@ -73,6 +73,15 @@ namespace openshot bool is_open; ///> Is Reader opened QSize max_size; ///> Current max_size as calculated with Clip properties + /// Load an SVG file with Resvg or fallback with Qt + /// + /// @returns Success as a boolean + /// @param path The file path of the SVG file + bool load_svg_path(QString path); + + /// Calculate the max_size QSize, based on parent timeline and parent clip settings + QSize calculate_max_size(); + public: /// @brief Constructor for QtImageReader. /// diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index b949e8bb7..50a85dc90 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -73,6 +73,7 @@ set(OPENSHOT_TEST_FILES FrameMapper_Tests.cpp KeyFrame_Tests.cpp Point_Tests.cpp + QtImageReader_Tests.cpp Settings_Tests.cpp Timeline_Tests.cpp ) diff --git a/tests/QtImageReader_Tests.cpp b/tests/QtImageReader_Tests.cpp new file mode 100644 index 000000000..2fd78d5e4 --- /dev/null +++ b/tests/QtImageReader_Tests.cpp @@ -0,0 +1,107 @@ +/** + * @file + * @brief Unit tests for openshot::QtImageReader + * @author Jonathan Thomas + * + * @ref License + */ + +/* LICENSE + * + * Copyright (c) 2008-2019 OpenShot Studios, LLC + * . This file is part of + * OpenShot Library (libopenshot), an open-source project dedicated to + * delivering high quality video editing and animation solutions to the + * world. For more information visit . + * + * OpenShot Library (libopenshot) is free software: you can redistribute it + * and/or modify it under the terms of the GNU Lesser General Public License + * as published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * OpenShot Library (libopenshot) is distributed in the hope that it will be + * useful, but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with OpenShot Library. If not, see . + */ + +#include "UnitTest++.h" +// Prevent name clashes with juce::UnitTest +#define DONT_SET_USING_JUCE_NAMESPACE 1 +#include "QGuiApplication" +#include "OpenShot.h" + +using namespace std; +using namespace openshot; + +SUITE(QtImageReader) +{ + +TEST(Default_Constructor) +{ + // Check invalid path + CHECK_THROW(QtImageReader(""), InvalidFile); +} + +TEST(GetFrame_Before_Opening) +{ + // Create a reader + stringstream path; + path << TEST_MEDIA_PATH << "front.png"; + QtImageReader r(path.str()); + + // Check invalid path + CHECK_THROW(r.GetFrame(1), ReaderClosed); +} + +TEST(Check_SVG_Loading) +{ + // Create a reader + stringstream path; + path << TEST_MEDIA_PATH << "1F0CF.svg"; + QtImageReader r(path.str()); + r.Open(); + + // Get frame, with no Timeline or Clip + // Default SVG scaling sizes things to 1920x1080 + std::shared_ptr f = r.GetFrame(1); + CHECK_EQUAL(1080, f->GetImage()->width()); + CHECK_EQUAL(1080, f->GetImage()->height()); + + Fraction fps(30000,1000); + Timeline t1(640, 480, fps, 44100, 2, LAYOUT_STEREO); + + Clip clip1(path.str()); + clip1.Layer(1); + clip1.Position(0.0); // Delay the overlay by 0.05 seconds + clip1.End(10.0); // Make the duration of the overlay 1/2 second + + // Add clips + t1.AddClip(&clip1); + t1.Open(); + + // Get frame, with 640x480 Timeline + // Should scale to 480 + clip1.Reader()->Open(); + f = clip1.Reader()->GetFrame(2); + CHECK_EQUAL(480, f->GetImage()->width()); + CHECK_EQUAL(480, f->GetImage()->height()); + + // Add scale_x and scale_y. Should scale the square SVG + // by the largest scale keyframe (i.e. 4) + clip1.scale_x.AddPoint(1.0, 2.0, openshot::LINEAR); + clip1.scale_y.AddPoint(1.0, 2.0, openshot::LINEAR); + f = clip1.Reader()->GetFrame(3); + CHECK_EQUAL(480 * 2, f->GetImage()->width()); + CHECK_EQUAL(480 * 2, f->GetImage()->height()); + + // Close reader + t1.Close(); + r.Close(); +} + +} // SUITE(QtImageReader) +