From ea9e774f1eb202582f4d59d3a7787f64db110a60 Mon Sep 17 00:00:00 2001 From: Jonathan Thomas Date: Thu, 28 Jan 2021 15:46:39 -0600 Subject: [PATCH 1/2] Fixing a big issue where SVG files are not correctly scaled to larger resolutions, for cases where the default size is smaller than the Timeline size (or preview size). Now SVG files are rescaled/re-rasterized larger when needed, and otherwise cached. --- examples/1F0CF.svg | 26 ++++ src/QtImageReader.cpp | 224 ++++++++++++++++++---------------- src/QtImageReader.h | 9 ++ tests/CMakeLists.txt | 1 + tests/QtImageReader_Tests.cpp | 107 ++++++++++++++++ 5 files changed, 262 insertions(+), 105 deletions(-) create mode 100644 examples/1F0CF.svg create mode 100644 tests/QtImageReader_Tests.cpp 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..283d45a2a 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,26 @@ 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())) { + bool loaded = false; - 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 + // Check for SVG files and rasterize them to QImages + if (path.toLower().endsWith(".svg") || path.toLower().endsWith(".svgz")) { + loaded = load_svg_path(path); + } - 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 +187,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) + From 71c6c23ec9e649b34f1a21d217ad3cdf7a37c5be Mon Sep 17 00:00:00 2001 From: Jonathan Thomas Date: Thu, 28 Jan 2021 15:54:52 -0600 Subject: [PATCH 2/2] Fixing scope issue and unused var --- src/QtImageReader.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/QtImageReader.cpp b/src/QtImageReader.cpp index 283d45a2a..01b023259 100644 --- a/src/QtImageReader.cpp +++ b/src/QtImageReader.cpp @@ -157,11 +157,9 @@ std::shared_ptr QtImageReader::GetFrame(int64_t requested_frame) // Scale image smaller (or use a previous scaled image) if (!cached_image || (max_size.width() != current_max_size.width() || max_size.height() != current_max_size.height())) { - bool loaded = false; - // Check for SVG files and rasterize them to QImages if (path.toLower().endsWith(".svg") || path.toLower().endsWith(".svgz")) { - loaded = load_svg_path(path); + load_svg_path(path); } // We need to resize the original image to a smaller image (for performance reasons)