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.

This commit is contained in:
Jonathan Thomas
2021-01-28 15:46:39 -06:00
parent cb816e154f
commit ea9e774f1e
5 changed files with 262 additions and 105 deletions

26
examples/1F0CF.svg Normal file
View File

@@ -0,0 +1,26 @@
<svg id="emoji" viewBox="0 0 72 72" xmlns="http://www.w3.org/2000/svg">
<g id="color">
<rect x="16.5" y="4.8" width="39" height="62.4" fill="#d0cfce"/>
<polygon fill="#ea5a47" points="35.806 48.602 26.843 51.115 27.93 46.615 21.283 46.466 24.239 44.005 28.268 42.502 45.313 42.78 50.338 46.309 44.057 46.615 45.501 51.115 35.806 48.602"/>
<path fill="#d22f27" d="M41.9248,50.1883c.6289.8354,3.5763.9269,3.5763.9269l-1.4445-4.5,6.2818-.3059L45.3127,42.78,42.3879,42.69C41.7716,43.053,41.2959,49.3528,41.9248,50.1883Z"/>
<path fill="#ea5a47" d="M21.0209,23.286c-.6734.15,3.9411-6.4248,11.4258-2.5271,0,0,4.5-10.7789,14.4109-6.7508,0,0-8.1695,6.2635-6.0268,5.9931,8.2578-1.042,9.3575,4.3845,9.3575,4.3845C40.1169,21.5471,45.81,29.9874,45.81,29.9874c-6.8537-6.8408-14.4739-5.7178-19.9542-.7242C25.856,29.2632,28.82,21.5471,21.0209,23.286Z"/>
<path fill="#d22f27" d="M40.8308,20.0012c8.2578-1.042,9.3575,4.3845,9.3575,4.3845C40.1169,21.5471,45.81,29.9874,45.81,29.9874c-2.0246-2.0207-6.0321-2.3908-6.1648-4.0666C39.386,22.6467,39.321,20.1917,40.8308,20.0012Z"/>
</g>
<g id="skin">
<path fill="#fcea2b" d="M44.8886,28.89c-.2972-2.0864-11.3522-6.9564-18.0832-.6564,0,0-.2566,4.3777-.2566,5.312,0,6.3731,4.1333,11.54,9.2319,11.54s9.2316-5.1665,9.2316-11.54A26.4689,26.4689,0,0,0,44.8886,28.89Z"/>
</g>
<g id="line">
<rect x="16.5" y="4.8" width="39" height="62.4" fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"/>
<path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21.0209,23.286c-.6734.15,3.9411-6.4248,11.4258-2.5271,0,0,4.5-10.7789,14.4109-6.7508,0,0-8.1695,6.2635-6.0268,5.9931,8.2578-1.042,9.3575,4.3845,9.3575,4.3845C40.1169,21.5471,45.81,29.9874,45.81,29.9874c-6.8537-6.8408-14.4739-5.7178-19.9542-.7242C25.856,29.2632,28.82,21.5471,21.0209,23.286Z"/>
<path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M25.9961,42.78c-1.211.0039-6.32,3.4383-6.32,5.0027,0,0,6.5691-1.77,8.2538-1.1678,0,0-1.939,3.8151-1.8187,5.8608,0,0,6.3717-3.1424,9.8822-3.1424"/>
<path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M45.99,42.78c1.211.0039,6.32,3.4383,6.32,5.0027,0,0-6.5692-1.77-8.2539-1.1678,0,0,1.9391,3.8151,1.8187,5.8608,0,0-6.3717-3.1424-9.8822-3.1424"/>
<path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M23.37,7.35v4.5a2.5,2.5,0,0,1-2.5,2.5h0a2.4908,2.4908,0,0,1-1.6728-.642"/>
<path fill="none" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M48.63,64.2165v-4.5a2.5,2.5,0,0,1,2.5-2.5h0a2.4908,2.4908,0,0,1,1.6728.642"/>
<circle cx="50.782" cy="27.4753" r="1.9488"/>
<circle cx="48.8332" cy="16.4818" r="1.9488"/>
<circle cx="20.6797" cy="26.4668" r="1.9488"/>
<path d="M41.7716,31.0968a1.9489,1.9489,0,1,1-1.9488-1.9489,1.95,1.95,0,0,1,1.9488,1.9489"/>
<path d="M33.9763,31.0968a1.9489,1.9489,0,1,1-1.9489-1.9489,1.95,1.95,0,0,1,1.9489,1.9489"/>
<path d="M35.925,40.8433a7.4566,7.4566,0,0,1-3.3591-.836.9745.9745,0,1,1,.8717-1.7432,5.24,5.24,0,0,0,4.9748,0,.9745.9745,0,1,1,.8717,1.7432A7.4566,7.4566,0,0,1,35.925,40.8433Z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@@ -33,9 +33,11 @@
#include "Settings.h"
#include "Clip.h"
#include "CacheMemory.h"
#include "Timeline.h"
#include <QtCore/QString>
#include <QtGui/QImage>
#include <QtGui/QPainter>
#include <QtGui/QIcon>
#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<QImage>(
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<QImage>();
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<Frame> 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<CriticalSection> 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")) {
// Check for SVG files and rasterize them to QImages
if (path.toLower().endsWith(".svg") || path.toLower().endsWith(".svgz")) {
loaded = load_svg_path(path);
}
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<QImage>(
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<QImage>(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<QImage>(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<Frame> 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<QImage>(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<QImage>();
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<QImage>(QIcon(path).pixmap(svg_size).toImage());
} else {
// Scale image without re-rasterizing it (due to lack of QApplication)
image = std::make_shared<QImage>(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 {

View File

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

View File

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

View File

@@ -0,0 +1,107 @@
/**
* @file
* @brief Unit tests for openshot::QtImageReader
* @author Jonathan Thomas <jonathan@openshot.org>
*
* @ref License
*/
/* LICENSE
*
* Copyright (c) 2008-2019 OpenShot Studios, LLC
* <http://www.openshotstudios.com/>. 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 <http://www.openshot.org/>.
*
* 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 <http://www.gnu.org/licenses/>.
*/
#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<Frame> 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)