diff options
author | Reizner Evgeniy <razrfalcon@gmail.com> | 2018-05-04 13:26:09 +0300 |
---|---|---|
committer | Reizner Evgeniy <razrfalcon@gmail.com> | 2018-05-04 13:26:09 +0300 |
commit | 44160ce8a1b8f31fe219fc6a8a122b5832fe8875 (patch) | |
tree | 4dbd5f89060a2a43e8f08ec0a3335b8f22d0a0ed /capi | |
parent | 5561b547c4ded7ec24ff22db62e8c3d3edc620a2 (diff) |
Added Qt wrapper for C-API.
A lot of small changes.
Diffstat (limited to 'capi')
-rw-r--r-- | capi/.gitignore | 1 | ||||
-rw-r--r-- | capi/include/Doxyfile | 5 | ||||
-rw-r--r-- | capi/include/resvg.h | 177 | ||||
-rw-r--r-- | capi/qt-wrapper/ResvgQt.cpp | 287 | ||||
-rw-r--r-- | capi/qt-wrapper/ResvgQt.h | 154 | ||||
-rw-r--r-- | capi/qtests/.gitignore | 2 | ||||
-rw-r--r-- | capi/qtests/invalid.svg | 1 | ||||
-rw-r--r-- | capi/qtests/test.svg | 8 | ||||
-rw-r--r-- | capi/qtests/test_renderFile_result.png | bin | 0 -> 2086 bytes | |||
-rw-r--r-- | capi/qtests/tests.pro | 26 | ||||
-rw-r--r-- | capi/qtests/tst_resvgqt.cpp | 87 | ||||
-rw-r--r-- | capi/src/lib.rs | 239 |
12 files changed, 806 insertions, 181 deletions
diff --git a/capi/.gitignore b/capi/.gitignore index eacffab..1e6eda2 100644 --- a/capi/.gitignore +++ b/capi/.gitignore @@ -1 +1,2 @@ /include/html +/include/Doxyfile diff --git a/capi/include/Doxyfile b/capi/include/Doxyfile deleted file mode 100644 index d143338..0000000 --- a/capi/include/Doxyfile +++ /dev/null @@ -1,5 +0,0 @@ -PROJECT_NAME = "resvg C-API" -OPTIMIZE_OUTPUT_FOR_C = YES -QUIET = YES -GENERATE_LATEX = NO -ENABLE_PREPROCESSING = NO diff --git a/capi/include/resvg.h b/capi/include/resvg.h index 55eb2f8..c7a7119 100644 --- a/capi/include/resvg.h +++ b/capi/include/resvg.h @@ -15,6 +15,7 @@ #include <stdbool.h> #include <stdint.h> +#include <stddef.h> #ifdef RESVG_CAIRO_BACKEND #include <cairo.h> @@ -44,6 +45,26 @@ typedef struct resvg_handle resvg_handle; typedef struct resvg_render_tree resvg_render_tree; /** + * @brief List of possible errors. + */ +typedef enum resvg_error { + /** Everything is ok. */ + RESVG_OK = 0, + /** Only UTF-8 content are supported. */ + RESVG_ERROR_NOT_AN_UTF8_STR, + /** Failed to open the provided file. */ + RESVG_ERROR_FILE_OPEN_FAILED, + /** Failed to write to the provided file. */ + RESVG_ERROR_FILE_WRITE_FAILED, + /** Only \b svg and \b svgz suffixes are supported. */ + RESVG_ERROR_INVALID_FILE_SUFFIX, + /** Compressed SVG must use the GZip algorithm. */ + RESVG_ERROR_MALFORMED_GZIP, + /** Failed to allocate an image. */ + RESVG_ERROR_NO_CANVAS, +} resvg_error; + +/** * @brief An RGB color representation. */ typedef struct resvg_color { @@ -76,19 +97,24 @@ typedef struct resvg_fit_to { * @brief Rendering options. */ typedef struct resvg_options { - /// SVG image path. Used to resolve relative image paths. + /** SVG image path. Used to resolve relative image paths. */ const char *path; - /// Output DPI. Default: 96. + /** Output DPI. Default: 96. */ double dpi; - /// Fits the image using specified options. - /// Default: \b RESVG_FIT_TO_ORIGINAL. + /** + * Fits the image using specified options. + * + * Default: \b RESVG_FIT_TO_ORIGINAL. + */ resvg_fit_to fit_to; - /// Draw background. Default: false. + /** Draw background. Default: false. */ bool draw_background; - /// Background color. + /** Background color. */ resvg_color background; - /// Keep named groups. If set to \b true, all non-empty - /// groups with \b id attribute will not be removed. + /** + * Keep named groups. If set to \b true, all non-empty + * groups with \b id attribute will not be removed. + */ bool keep_named_groups; } resvg_options; @@ -143,6 +169,8 @@ void resvg_destroy(resvg_handle *handle); * * Use it if you want to see any warnings. * + * Must be called only once. + * * All warnings will be printed to the \b stderr. */ void resvg_init_log(); @@ -168,85 +196,100 @@ void resvg_init_options(resvg_options *opt) * * .svg and .svgz files are supported. * + * See #resvg_is_image_empty for details. + * * @param file_path UTF-8 file path. * @param opt Rendering options. - * @param error The error string if NULL was returned. Should be destroyed via #resvg_error_msg_destroy. - * @return Parsed render tree. NULL on error. Should be destroyed via #resvg_rtree_destroy. + * @param tree Parsed render tree. Should be destroyed via #resvg_tree_destroy. + * @return #resvg_error */ -resvg_render_tree *resvg_parse_rtree_from_file(const char *file_path, - const resvg_options *opt, - char **error); +int resvg_parse_tree_from_file(const char *file_path, + const resvg_options *opt, + resvg_render_tree **tree); /** - * @brief Creates #resvg_render_tree from UTF-8 string. + * @brief Creates #resvg_render_tree from data. + * + * See #resvg_is_image_empty for details. * - * @param text UTF-8 string. + * @param data SVG data. Can contain SVG string or gzip compressed data. + * @param len Data length. * @param opt Rendering options. - * @param error The error string if NULL was returned. Should be destroyed via #resvg_error_msg_destroy. - * @return Parsed render tree. NULL on error. Should be destroyed via #resvg_rtree_destroy. + * @param tree Parsed render tree. Should be destroyed via #resvg_tree_destroy. + * @return #resvg_error */ -resvg_render_tree *resvg_parse_rtree_from_data(const char *text, - const resvg_options *opt, - char **error); +int resvg_parse_tree_from_data(const char *data, + const size_t len, + const resvg_options *opt, + resvg_render_tree **tree); + +/** + * @brief Checks that tree has any nodes. + * + * #resvg_parse_tree_from_file and #resvg_parse_tree_from_data methods + * will return an error only if a file does not exist or it has a non-UTF-8 encoding. + * All other errors will result in an empty tree with a 100x100px size. + * + * @param tree Render tree. + * @return Returns \b true if tree has any nodes. + */ +bool resvg_is_image_empty(const resvg_render_tree *tree); /** * @brief Returns an image size. * - * @param rtree Render tree. + * @param tree Render tree. * @return Image size. */ -resvg_size resvg_get_image_size(const resvg_render_tree *rtree); +resvg_size resvg_get_image_size(const resvg_render_tree *tree); /** * @brief Returns an image viewbox. * - * @param rtree Render tree. + * @param tree Render tree. * @return Image viewbox. */ -resvg_rect resvg_get_image_viewbox(const resvg_render_tree *rtree); +resvg_rect resvg_get_image_viewbox(const resvg_render_tree *tree); /** - * @brief Returns \b true if a node with such an ID exists. + * @brief Returns \b true if a renderable node with such an ID exists. * - * @param rtree Render tree. + * @param tree Render tree. * @param id Node's ID. UTF-8 string. - * @return \b true if a node exists. \b false if a node doesn't exist or ID isn't a UTF-8 string. + * @return \b true if a node exists. + * @return \b false if a node doesn't exist or ID isn't a UTF-8 string. + * @return \b false if a node exists, but not renderable. */ -bool resvg_node_exists(const resvg_render_tree *rtree, +bool resvg_node_exists(const resvg_render_tree *tree, const char *id); /** * @brief Returns node's transform by ID. * - * @param rtree Render tree. + * @param tree Render tree. * @param id Node's ID. UTF-8 string. * @param ts Node's transform. - * @return \b false if a node with such an ID does not exist or ID isn't a UTF-8 string. + * @return \b true if a node exists. + * @return \b false if a node doesn't exist or ID isn't a UTF-8 string. + * @return \b false if a node exists, but not renderable. */ -bool resvg_get_node_transform(const resvg_render_tree *rtree, +bool resvg_get_node_transform(const resvg_render_tree *tree, const char *id, resvg_transform *ts); /** * @brief Destroys the #resvg_render_tree. * - * @param rtree Render tree. - */ -void resvg_rtree_destroy(resvg_render_tree *rtree); - -/** - * @brief Destroys the error message. - * - * @param msg Error message. + * @param tree Render tree. */ -void resvg_error_msg_destroy(char *msg); +void resvg_tree_destroy(resvg_render_tree *tree); #ifdef RESVG_CAIRO_BACKEND /** * @brief Returns node's bounding box by ID. * - * @param rtree Render tree. + * @param tree Render tree. * @param opt Rendering options. * @param id Node's ID. * @param bbox Node's bounding box. @@ -254,7 +297,7 @@ void resvg_error_msg_destroy(char *msg); * @return \b false if ID isn't a UTF-8 string. * @return \b false if ID is an empty string */ -bool resvg_cairo_get_node_bbox(const resvg_render_tree *rtree, +bool resvg_cairo_get_node_bbox(const resvg_render_tree *tree, const resvg_options *opt, const char *id, resvg_rect *bbox); @@ -262,26 +305,24 @@ bool resvg_cairo_get_node_bbox(const resvg_render_tree *rtree, /** * @brief Renders the #resvg_render_tree to file. * - * @param rtree Render tree. + * @param tree Render tree. * @param opt Rendering options. * @param file_path File path. - * @return \b false if \b file_path isn't an UTF-8 string. - * @return \b false on "Out of memory". - * @return \b false on file write error. + * @return #resvg_error */ -bool resvg_cairo_render_to_image(const resvg_render_tree *rtree, - const resvg_options *opt, - const char *file_path); +int resvg_cairo_render_to_image(const resvg_render_tree *tree, + const resvg_options *opt, + const char *file_path); /** * @brief Renders the #resvg_render_tree to canvas. * - * @param rtree Render tree. + * @param tree Render tree. * @param opt Rendering options. * @param size Canvas size. * @param cr Canvas. */ -void resvg_cairo_render_to_canvas(const resvg_render_tree *rtree, +void resvg_cairo_render_to_canvas(const resvg_render_tree *tree, const resvg_options *opt, resvg_size size, cairo_t *cr); @@ -291,31 +332,31 @@ void resvg_cairo_render_to_canvas(const resvg_render_tree *rtree, * * Does nothing on error. * - * @param rtree Render tree. + * @param tree Render tree. * @param opt Rendering options. * @param size Canvas size. * @param id Node's ID. * @param cr Canvas. */ -void resvg_cairo_render_to_canvas_by_id(const resvg_render_tree *rtree, +void resvg_cairo_render_to_canvas_by_id(const resvg_render_tree *tree, const resvg_options *opt, resvg_size size, const char *id, cairo_t *cr); -#endif // RESVG_CAIRO_BACKEND +#endif /* RESVG_CAIRO_BACKEND */ #ifdef RESVG_QT_BACKEND /** * @brief Returns node's bounding box by ID. * - * @param rtree Render tree. + * @param tree Render tree. * @param opt Rendering options. * @param id Node's ID. * @param bbox Node's bounding box. * @return \b false if a node with such an ID does not exist, * ID is an empty string or ID isn't a UTF-8 string. */ -bool resvg_qt_get_node_bbox(const resvg_render_tree *rtree, +bool resvg_qt_get_node_bbox(const resvg_render_tree *tree, const resvg_options *opt, const char *id, resvg_rect *bbox); @@ -323,26 +364,24 @@ bool resvg_qt_get_node_bbox(const resvg_render_tree *rtree, /** * @brief Renders the #resvg_render_tree to file. * - * @param rtree Render tree. + * @param tree Render tree. * @param opt Rendering options. * @param file_path File path. - * @return \b false if \b file_path isn't an UTF-8 string. - * @return \b false on "Out of memory". - * @return \b false on file write error. + * @return #resvg_error */ -bool resvg_qt_render_to_image(const resvg_render_tree *rtree, - const resvg_options *opt, - const char *file_path); +int resvg_qt_render_to_image(const resvg_render_tree *tree, + const resvg_options *opt, + const char *file_path); /** * @brief Renders the #resvg_render_tree to canvas. * - * @param rtree Render tree. + * @param tree Render tree. * @param opt Rendering options. * @param size Canvas size. * @param painter Canvas. */ -void resvg_qt_render_to_canvas(const resvg_render_tree *rtree, +void resvg_qt_render_to_canvas(const resvg_render_tree *tree, const resvg_options *opt, resvg_size size, void *painter); @@ -352,17 +391,17 @@ void resvg_qt_render_to_canvas(const resvg_render_tree *rtree, * * Does nothing on error. * - * @param rtree Render tree. + * @param tree Render tree. * @param opt Rendering options. * @param size Canvas size. * @param id Node's ID. * @param painter Canvas. */ -void resvg_qt_render_to_canvas_by_id(const resvg_render_tree *rtree, +void resvg_qt_render_to_canvas_by_id(const resvg_render_tree *tree, const resvg_options *opt, resvg_size size, const char *id, void *painter); -#endif // RESVG_QT_BACKEND +#endif /* RESVG_QT_BACKEND */ -#endif // RESVG_H +#endif /* RESVG_H */ diff --git a/capi/qt-wrapper/ResvgQt.cpp b/capi/qt-wrapper/ResvgQt.cpp new file mode 100644 index 0000000..e9a1a62 --- /dev/null +++ b/capi/qt-wrapper/ResvgQt.cpp @@ -0,0 +1,287 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +#include <ResvgQt.h> + +extern "C" { +#define RESVG_QT_BACKEND +#include <resvg.h> +} + +#include <QGuiApplication> +#include <QScreen> +#include <QPainter> +#include <QFile> +#include <QDebug> + + +class ResvgRendererPrivate +{ +public: + ~ResvgRendererPrivate() + { + reset(); + } + + void reset() + { + if (tree) { + resvg_tree_destroy(tree); + tree = nullptr; + } + + if (opt.path) { + delete[] opt.path; // do not use free() because was allocated via qstrdup() + opt.path = NULL; + } + + viewBox = QRectF(); + errMsg = QString(); + } + + resvg_render_tree *tree = nullptr; + resvg_options opt; + QRectF viewBox; + QString errMsg; +}; + + +static void initOptions(resvg_options &opt) +{ + resvg_init_options(&opt); + + const auto screens = qApp->screens(); + if (!screens.isEmpty()) { + const auto screen = screens.at(0); + opt.dpi = screen->logicalDotsPerInch() * screen->devicePixelRatio(); + } +} + +static QString errorToString(const int err) +{ + switch (err) { + case RESVG_OK : + return QString(); break; + case RESVG_ERROR_NOT_AN_UTF8_STR : + return QLatin1Literal("The SVG content has not an UTF-8 encoding."); break; + case RESVG_ERROR_FILE_OPEN_FAILED : + return QLatin1Literal("Failed to open the file."); break; + case RESVG_ERROR_FILE_WRITE_FAILED : + return QLatin1Literal("Failed to write to the file."); break; + case RESVG_ERROR_INVALID_FILE_SUFFIX : + return QLatin1Literal("Invalid file suffix."); break; + case RESVG_ERROR_MALFORMED_GZIP : + return QLatin1Literal("Not a GZip compressed data."); break; + case RESVG_ERROR_NO_CANVAS : + return QLatin1Literal("Failed to allocate the canvas."); break; + } + + Q_UNREACHABLE(); +} + +ResvgRenderer::ResvgRenderer() + : d(new ResvgRendererPrivate()) +{ +} + +ResvgRenderer::ResvgRenderer(const QString &filePath) + : d(new ResvgRendererPrivate()) +{ + load(filePath); +} + +ResvgRenderer::ResvgRenderer(const QByteArray &data) + : d(new ResvgRendererPrivate()) +{ + load(data); +} + +ResvgRenderer::~ResvgRenderer() {} + +bool ResvgRenderer::load(const QString &filePath) +{ + // Check for Qt resource path. + if (filePath.startsWith(":/")) { + QFile file(filePath); + if (file.open(QFile::ReadOnly)) { + return load(file.readAll()); + } else { + return false; + } + } + + d->reset(); + + resvg_options opt; + initOptions(opt); + + const auto utf8Str = filePath.toUtf8(); + const auto rawFilePath = utf8Str.constData(); + opt.path = qstrdup(rawFilePath); + + resvg_render_tree *tree = NULL; + const auto err = resvg_parse_tree_from_file(opt.path, &opt, &tree); + if (err != RESVG_OK) { + d->errMsg = errorToString(err); + return false; + } + + d->tree = tree; + d->opt = opt; + + const auto r = resvg_get_image_viewbox(d->tree); + d->viewBox = QRectF(r.x, r.y, r.width, r.height); + + return true; +} + +bool ResvgRenderer::load(const QByteArray &data) +{ + d->reset(); + + resvg_options opt; + initOptions(opt); + + resvg_render_tree *tree = NULL; + resvg_parse_tree_from_data(data.constData(), data.size(), &opt, &tree); + + d->tree = tree; + d->opt = opt; + + const auto r = resvg_get_image_viewbox(d->tree); + d->viewBox = QRectF(r.x, r.y, r.width, r.height); + + return true; +} + +bool ResvgRenderer::isValid() const +{ + return d->tree; +} + +QString ResvgRenderer::errorString() const +{ + return d->errMsg; +} + +bool ResvgRenderer::isEmpty() const +{ + if (d->tree) + return !resvg_is_image_empty(d->tree); + else + return true; +} + +QSize ResvgRenderer::defaultSize() const +{ + return defaultSizeF().toSize(); +} + +QSizeF ResvgRenderer::defaultSizeF() const +{ + if (d->tree) + return d->viewBox.size(); + else + return QSizeF(); +} + +QRect ResvgRenderer::viewBox() const +{ + return viewBoxF().toRect(); +} + +QRectF ResvgRenderer::viewBoxF() const +{ + if (d->tree) + return d->viewBox; + else + return QRectF(); +} + +bool ResvgRenderer::elementExists(const QString &id) const +{ + if (d->tree) { + const auto utf8Str = id.toUtf8(); + const auto rawId = utf8Str.constData(); + return resvg_node_exists(d->tree, rawId); + } + + return false; +} + +QTransform ResvgRenderer::transformForElement(const QString &id) const +{ + if (d->tree) { + const auto utf8Str = id.toUtf8(); + const auto rawId = utf8Str.constData(); + resvg_transform ts; + if (resvg_get_node_transform(d->tree, rawId, &ts)) { + return QTransform(ts.a, ts.b, ts.c, ts.d, ts.e, ts.f); + } + } + + return QTransform(); +} + +void ResvgRenderer::render(QPainter *p) +{ + render(p, QRectF()); +} + +void ResvgRenderer::render(QPainter *p, const QRectF &bounds) +{ + if (!d->tree) + return; + + const auto r = bounds.isValid() ? bounds : p->viewport(); + + p->save(); + p->setRenderHint(QPainter::Antialiasing); + + const double sx = (double)r.width() / d->viewBox.width(); + const double sy = (double)r.height() / d->viewBox.height(); + + p->setTransform(QTransform(sx, 0, 0, sy, r.x(), r.y()), true); + + resvg_size imgSize { (uint)d->viewBox.width(), (uint)d->viewBox.height() }; + resvg_qt_render_to_canvas(d->tree, &d->opt, imgSize, p); + + p->restore(); +} + +void ResvgRenderer::render(QPainter *p, const QString &elementId, const QRectF &bounds) +{ + if (!d->tree) + return; + + const auto utf8Str = elementId.toUtf8(); + const auto rawId = utf8Str.constData(); + + resvg_rect bbox; + if (!resvg_qt_get_node_bbox(d->tree, &d->opt, rawId, &bbox)) { + qWarning() << QString("Element '%1' has no bounding box.").arg(elementId); + return; + } + + p->save(); + p->setRenderHint(QPainter::Antialiasing); + + const auto r = bounds.isValid() ? bounds : p->viewport(); + + const double sx = (double)r.width() / bbox.width; + const double sy = (double)r.height() / bbox.height; + p->setTransform(QTransform(sx, 0, 0, sy, bounds.x(), bounds.y()), true); + + resvg_size imgSize { (uint)bbox.width, (uint)bbox.height }; + resvg_qt_render_to_canvas_by_id(d->tree, &d->opt, imgSize, rawId, p); + + p->restore(); +} + +void ResvgRenderer::initLog() +{ + resvg_init_log(); +} diff --git a/capi/qt-wrapper/ResvgQt.h b/capi/qt-wrapper/ResvgQt.h new file mode 100644 index 0000000..2572c88 --- /dev/null +++ b/capi/qt-wrapper/ResvgQt.h @@ -0,0 +1,154 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +/** + * @file ResvgQt.h + * + * Qt wrapper for resvg C-API + */ + +#ifndef RESVGQT_H +#define RESVGQT_H + +#include <QString> +#include <QScopedPointer> +#include <QRectF> +#include <QTransform> + +class QPainter; + +class ResvgRendererPrivate; + +/** + * @brief QSvgRenderer-like wrapper for resvg C-API + */ +class ResvgRenderer { +public: + /** + * @brief Constructs a new renderer. + */ + ResvgRenderer(); + + /** + * @brief Constructs a new renderer and loads the contents of the SVG(Z) file. + * + * @param filePath File path. + */ + ResvgRenderer(const QString &filePath); + + /** + * @brief Constructs a new renderer and loads the SVG data. + * + * @param data SVG data. Must be already unzipped. + */ + ResvgRenderer(const QByteArray &data); + + /** + * @brief Destructs the renderer. + */ + ~ResvgRenderer(); + + /** + * @brief Loads the contents of the SVG(Z) file. + */ + bool load(const QString &filePath); + + /** + * @brief Loads the SVG data. + * + * @param data SVG data. Must be already unzipped. + */ + bool load(const QByteArray &data); + + /** + * @brief Returns \b true if the file or data were loaded successful. + */ + bool isValid() const; + + /** + * @brief Returns an underling error when #isValid is \b false. + */ + QString errorString() const; + + /** + * @brief Checks that underling tree has any nodes. + * + * #ResvgRenderer and #ResvgRenderer constructors + * will set an error only if a file does not exist or it has a non-UTF-8 encoding. + * All other errors will result in an empty tree with a 100x100px size. + * + * @return Returns \b true if tree has any nodes. + */ + bool isEmpty() const; + + /** + * @brief Returns an SVG size. + */ + QSize defaultSize() const; + + /** + * @brief Returns an SVG size. + */ + QSizeF defaultSizeF() const; + + /** + * @brief Returns an SVG viewbox. + */ + QRect viewBox() const; + + /** + * @brief Returns an SVG viewbox. + */ + QRectF viewBoxF() const; + + /** + * @brief Returns \b true if element with such an ID exists. + */ + bool elementExists(const QString &id) const; + + /** + * @brief Returns element's transform. + */ + QTransform transformForElement(const QString &id) const; + + /** + * @brief Renders the SVG data to canvas. + */ + void render(QPainter *p); + + /** + * @brief Renders the SVG data to canvas with the specified \b bounds. + * + * If the bounding rectangle is not specified + * the SVG file is mapped to the whole paint device. + */ + void render(QPainter *p, const QRectF &bounds); + + /** + * @brief Renders the given element with \b elementId on the specified \b bounds. + * + * If the bounding rectangle is not specified + * the SVG element is mapped to the whole paint device. + */ + void render(QPainter *p, const QString &elementId, + const QRectF &bounds = QRectF()); + + /** + * @brief Initializes the library log. + * + * Use it if you want to see any warnings. + * + * Must be called only once. + * + * All warnings will be printed to the \b stderr. + */ + static void initLog(); + +private: + QScopedPointer<ResvgRendererPrivate> d; +}; + +#endif // RESVGQT_H diff --git a/capi/qtests/.gitignore b/capi/qtests/.gitignore new file mode 100644 index 0000000..c1b81fb --- /dev/null +++ b/capi/qtests/.gitignore @@ -0,0 +1,2 @@ +tst_resvgqt* +test_renderFile.png diff --git a/capi/qtests/invalid.svg b/capi/qtests/invalid.svg new file mode 100644 index 0000000..96322cc --- /dev/null +++ b/capi/qtests/invalid.svg @@ -0,0 +1 @@ +<rect/> diff --git a/capi/qtests/test.svg b/capi/qtests/test.svg new file mode 100644 index 0000000..d75f48f --- /dev/null +++ b/capi/qtests/test.svg @@ -0,0 +1,8 @@ +<svg id="svg1" viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg"> + <pattern id="patt1" patternUnits="userSpaceOnUse" width="20" height="20"> + <rect id="rect1" x="0" y="0" width="10" height="10" fill="grey"/> + <rect id="rect2" x="10" y="10" width="10" height="10" fill="green"/> + </pattern> + <circle id="circle1" cx="50" cy="50" r="40" fill="url(#patt1)" + transform="scale(2)"/> +</svg> diff --git a/capi/qtests/test_renderFile_result.png b/capi/qtests/test_renderFile_result.png Binary files differnew file mode 100644 index 0000000..059903e --- /dev/null +++ b/capi/qtests/test_renderFile_result.png diff --git a/capi/qtests/tests.pro b/capi/qtests/tests.pro new file mode 100644 index 0000000..39c5a83 --- /dev/null +++ b/capi/qtests/tests.pro @@ -0,0 +1,26 @@ +QT += testlib + +TARGET = tst_resvgqt +CONFIG += console +CONFIG -= app_bundle + +TEMPLATE = app + +QMAKE_CXXFLAGS += -Wextra -Wpedantic + +QMAKE_CXXFLAGS += -fsanitize=address +QMAKE_LFLAGS += -fsanitize=address + +SOURCES += \ + tst_resvgqt.cpp \ + $$PWD/../qt-wrapper/ResvgQt.cpp + +DEFINES += SRCDIR=\\\"$$PWD\\\" + +CONFIG(release, debug|release): LIBS += -L$$PWD/../../target/release/ -lresvg +else:CONFIG(debug, debug|release): LIBS += -L$$PWD/../../target/debug/ -lresvg + +INCLUDEPATH += $$PWD/../include +DEPENDPATH += $$PWD/../include + +INCLUDEPATH += $$PWD/../qt-wrapper diff --git a/capi/qtests/tst_resvgqt.cpp b/capi/qtests/tst_resvgqt.cpp new file mode 100644 index 0000000..2a2fc75 --- /dev/null +++ b/capi/qtests/tst_resvgqt.cpp @@ -0,0 +1,87 @@ +#include <QString> +#include <QPainter> +#include <QtTest> + +#include <ResvgQt.h> + +class ResvgQtTests : public QObject +{ + Q_OBJECT + +private Q_SLOTS: + void test_parseFile(); + void test_parseInvalidFile(); + + void test_renderFile(); + + void test_elementExists(); + void test_transformForElement(); +}; + +static QString localPath(const QString &fileName) +{ + return QString("%1/%2").arg(SRCDIR).arg(fileName); +} + +void ResvgQtTests::test_parseFile() +{ + ResvgRenderer render(localPath("test.svg")); + QVERIFY(render.isValid()); + QVERIFY(!render.isEmpty()); + QCOMPARE(render.defaultSize(), QSize(200, 200)); +} + +void ResvgQtTests::test_parseInvalidFile() +{ + ResvgRenderer render(localPath("invalid.svg")); + QVERIFY(render.isValid()); + QCOMPARE(render.defaultSize(), QSize(100, 100)); + QVERIFY(!render.isEmpty()); +} + +void ResvgQtTests::test_renderFile() +{ + ResvgRenderer render(localPath("test.svg")); + QVERIFY(!render.isEmpty()); + QCOMPARE(render.defaultSize(), QSize(200, 200)); + + QImage img(render.defaultSize(), QImage::Format_ARGB32); + img.fill(Qt::transparent); + + QPainter p(&img); + render.render(&p); + p.end(); + + img.save("test_renderFile.png"); + + QCOMPARE(img, QImage(localPath("test_renderFile_result.png"))); +} + +void ResvgQtTests::test_elementExists() +{ + ResvgRenderer render(localPath("test.svg")); + QVERIFY(!render.isEmpty()); + + // Existing element. + QVERIFY(render.elementExists("circle1")); + + // Non-existing element. + QVERIFY(!render.elementExists("invalid")); + + // Non-renderable elements. + QVERIFY(!render.elementExists("rect1")); + QVERIFY(!render.elementExists("rect2")); + QVERIFY(!render.elementExists("patt1")); +} + +void ResvgQtTests::test_transformForElement() +{ + ResvgRenderer render(localPath("test.svg")); + QVERIFY(!render.isEmpty()); + QCOMPARE(render.transformForElement("circle1"), QTransform(2, 0, 0, 2, 0, 0)); + QCOMPARE(render.transformForElement("invalid"), QTransform()); +} + +QTEST_APPLESS_MAIN(ResvgQtTests) + +#include "tst_resvgqt.moc" diff --git a/capi/src/lib.rs b/capi/src/lib.rs index 882f948..b59a9fe 100644 --- a/capi/src/lib.rs +++ b/capi/src/lib.rs @@ -15,9 +15,9 @@ extern crate cairo_sys; use std::fmt; use std::path; -use std::ptr; -use std::ffi::{ CStr, CString }; +use std::ffi::CStr; use std::os::raw::c_char; +use std::slice; #[cfg(feature = "qt-backend")] use resvg::qt; @@ -27,6 +27,7 @@ use resvg::cairo; use resvg::usvg; use resvg::tree; +use resvg::tree::NodeExt; use resvg::geom::*; @@ -40,6 +41,16 @@ pub struct resvg_options { pub keep_named_groups: bool, } +enum ErrorId { + Ok = 0, + NotAnUtf8Str, + FileOpenFailed, + FileWriteFailed, + InvalidFileSuffix, + MalformedGZip, + NoCanvas, +} + #[repr(C)] pub struct resvg_color { pub r: u8, @@ -91,13 +102,6 @@ pub struct resvg_render_tree(resvg::tree::Tree); #[repr(C)] pub struct resvg_handle(resvg::InitObject); -macro_rules! on_err { - ($err:expr, $msg:expr) => ({ - let c_str = CString::new($msg).unwrap(); - unsafe { *$err = c_str.into_raw(); } - return ptr::null_mut(); - }) -} #[no_mangle] pub extern fn resvg_init() -> *mut resvg_handle { @@ -141,14 +145,14 @@ fn log_format(out: fern::FormatCallback, message: &fmt::Arguments, record: &log: } #[no_mangle] -pub extern fn resvg_parse_rtree_from_file( +pub extern fn resvg_parse_tree_from_file( file_path: *const c_char, opt: *const resvg_options, - error: *mut *mut c_char, -) -> *mut resvg_render_tree { + raw_tree: *mut *mut resvg_render_tree, +) -> i32 { let file_path = match cstr_to_str(file_path) { Some(v) => v, - None => on_err!(error, "Error: file path is not an UTF-8 string."), + None => return ErrorId::NotAnUtf8Str as i32, }; let opt = to_native_opt(unsafe { @@ -156,89 +160,86 @@ pub extern fn resvg_parse_rtree_from_file( &*opt }); - let rtree = match resvg::parse_rtree_from_file(file_path, &opt) { - Ok(rtree) => rtree, - Err(e) => on_err!(error, e.to_string()), + let tree = match resvg::parse_tree_from_file(file_path, &opt.usvg) { + Ok(tree) => tree, + Err(e) => return convert_error(e) as i32, }; - let rtree_box = Box::new(resvg_render_tree(rtree)); - Box::into_raw(rtree_box) + let tree_box = Box::new(resvg_render_tree(tree)); + unsafe { *raw_tree = Box::into_raw(tree_box); } + + ErrorId::Ok as i32 } #[no_mangle] -pub extern fn resvg_parse_rtree_from_data( - text: *const c_char, +pub extern fn resvg_parse_tree_from_data( + data: *const c_char, + len: usize, opt: *const resvg_options, - error: *mut *mut c_char, -) -> *mut resvg_render_tree { - let text = match cstr_to_str(text) { - Some(v) => v, - None => on_err!(error, "Error: SVG data is not an UTF-8 string."), - }; + raw_tree: *mut *mut resvg_render_tree, +) -> i32 { + let data = unsafe { slice::from_raw_parts(data as *const u8, len) }; let opt = to_native_opt(unsafe { assert!(!opt.is_null()); &*opt }); - let rtree = resvg::parse_rtree_from_data(text, &opt); + let tree = match resvg::parse_tree_from_data(data, &opt.usvg) { + Ok(tree) => tree, + Err(e) => return convert_error(e) as i32, + }; - let rtree_box = Box::new(resvg_render_tree(rtree)); - Box::into_raw(rtree_box) -} + let tree_box = Box::new(resvg_render_tree(tree)); + unsafe { *raw_tree = Box::into_raw(tree_box); } -#[no_mangle] -pub extern fn resvg_error_msg_destroy(msg: *mut c_char) { - unsafe { - assert!(!msg.is_null()); - CString::from_raw(msg) - }; + ErrorId::Ok as i32 } #[no_mangle] -pub extern fn resvg_rtree_destroy(rtree: *mut resvg_render_tree) { +pub extern fn resvg_tree_destroy(tree: *mut resvg_render_tree) { unsafe { - assert!(!rtree.is_null()); - Box::from_raw(rtree) + assert!(!tree.is_null()); + Box::from_raw(tree) }; } #[cfg(feature = "qt-backend")] #[no_mangle] pub extern fn resvg_qt_render_to_image( - rtree: *const resvg_render_tree, + tree: *const resvg_render_tree, opt: *const resvg_options, file_path: *const c_char, -) -> bool { +) -> i32 { let backend = Box::new(resvg::render_qt::Backend); - render_to_image(rtree, opt, file_path, backend) + render_to_image(tree, opt, file_path, backend) } #[cfg(feature = "cairo-backend")] #[no_mangle] pub extern fn resvg_cairo_render_to_image( - rtree: *const resvg_render_tree, + tree: *const resvg_render_tree, opt: *const resvg_options, file_path: *const c_char, -) -> bool { +) -> i32 { let backend = Box::new(resvg::render_cairo::Backend); - render_to_image(rtree, opt, file_path, backend) + render_to_image(tree, opt, file_path, backend) } fn render_to_image( - rtree: *const resvg_render_tree, + tree: *const resvg_render_tree, opt: *const resvg_options, file_path: *const c_char, backend: Box<resvg::Render>, -) -> bool { - let rtree = unsafe { - assert!(!rtree.is_null()); - &*rtree +) -> i32 { + let tree = unsafe { + assert!(!tree.is_null()); + &*tree }; let file_path = match cstr_to_str(file_path) { Some(v) => v, - None => return false, + None => return ErrorId::NotAnUtf8Str as i32, }; let opt = to_native_opt(unsafe { @@ -246,29 +247,31 @@ fn render_to_image( &*opt }); - let img = backend.render_to_image(&rtree.0, &opt); + let img = backend.render_to_image(&tree.0, &opt); let img = match img { - Ok(img) => img, - Err(e) => { - warn!("{}", e); - return false; + Some(img) => img, + None => { + return ErrorId::NoCanvas as i32; } }; - img.save(path::Path::new(file_path)) + match img.save(path::Path::new(file_path)) { + true => ErrorId::Ok as i32, + false => ErrorId::FileWriteFailed as i32, + } } #[cfg(feature = "qt-backend")] #[no_mangle] pub extern fn resvg_qt_render_to_canvas( - rtree: *const resvg_render_tree, + tree: *const resvg_render_tree, opt: *const resvg_options, size: resvg_size, painter: *mut qt::qtc_qpainter, ) { - let rtree = unsafe { - assert!(!rtree.is_null()); - &*rtree + let tree = unsafe { + assert!(!tree.is_null()); + &*tree }; let painter = unsafe { qt::Painter::from_raw(painter) }; @@ -278,20 +281,20 @@ pub extern fn resvg_qt_render_to_canvas( &*opt }); - resvg::render_qt::render_to_canvas(&rtree.0, &opt, size, &painter); + resvg::render_qt::render_to_canvas(&tree.0, &opt, size, &painter); } #[cfg(feature = "cairo-backend")] #[no_mangle] pub extern fn resvg_cairo_render_to_canvas( - rtree: *const resvg_render_tree, + tree: *const resvg_render_tree, opt: *const resvg_options, size: resvg_size, cr: *mut cairo_sys::cairo_t, ) { - let rtree = unsafe { - assert!(!rtree.is_null()); - &*rtree + let tree = unsafe { + assert!(!tree.is_null()); + &*tree }; use glib::translate::FromGlibPtrNone; @@ -304,21 +307,21 @@ pub extern fn resvg_cairo_render_to_canvas( &*opt }); - resvg::render_cairo::render_to_canvas(&rtree.0, &opt, size, &cr); + resvg::render_cairo::render_to_canvas(&tree.0, &opt, size, &cr); } #[cfg(feature = "qt-backend")] #[no_mangle] pub extern fn resvg_qt_render_to_canvas_by_id( - rtree: *const resvg_render_tree, + tree: *const resvg_render_tree, opt: *const resvg_options, size: resvg_size, id: *const c_char, painter: *mut qt::qtc_qpainter, ) { - let rtree = unsafe { - assert!(!rtree.is_null()); - &*rtree + let tree = unsafe { + assert!(!tree.is_null()); + &*tree }; let painter = unsafe { qt::Painter::from_raw(painter) }; @@ -338,7 +341,7 @@ pub extern fn resvg_qt_render_to_canvas_by_id( return; } - if let Some(node) = rtree.0.node_by_id(id) { + if let Some(node) = tree.0.node_by_id(id) { if let Some(bbox) = resvg::render_qt::calc_node_bbox(&node, &opt) { let vbox = tree::ViewBox { rect: bbox, @@ -357,15 +360,15 @@ pub extern fn resvg_qt_render_to_canvas_by_id( #[cfg(feature = "cairo-backend")] #[no_mangle] pub extern fn resvg_cairo_render_to_canvas_by_id( - rtree: *const resvg_render_tree, + tree: *const resvg_render_tree, opt: *const resvg_options, size: resvg_size, id: *const c_char, cr: *mut cairo_sys::cairo_t, ) { - let rtree = unsafe { - assert!(!rtree.is_null()); - &*rtree + let tree = unsafe { + assert!(!tree.is_null()); + &*tree }; let id = match cstr_to_str(id) { @@ -388,7 +391,7 @@ pub extern fn resvg_cairo_render_to_canvas_by_id( &*opt }); - if let Some(node) = rtree.0.node_by_id(id) { + if let Some(node) = tree.0.node_by_id(id) { if let Some(bbox) = resvg::render_cairo::calc_node_bbox(&node, &opt) { let vbox = tree::ViewBox { rect: bbox, @@ -406,14 +409,14 @@ pub extern fn resvg_cairo_render_to_canvas_by_id( #[no_mangle] pub extern fn resvg_get_image_size( - rtree: *const resvg_render_tree, + tree: *const resvg_render_tree, ) -> resvg_size { - let rtree = unsafe { - assert!(!rtree.is_null()); - &*rtree + let tree = unsafe { + assert!(!tree.is_null()); + &*tree }; - let size = rtree.0.svg_node().size; + let size = tree.0.svg_node().size; resvg_size { width: size.width as u32, @@ -423,14 +426,14 @@ pub extern fn resvg_get_image_size( #[no_mangle] pub extern fn resvg_get_image_viewbox( - rtree: *const resvg_render_tree, + tree: *const resvg_render_tree, ) -> resvg_rect { - let rtree = unsafe { - assert!(!rtree.is_null()); - &*rtree + let tree = unsafe { + assert!(!tree.is_null()); + &*tree }; - let r = rtree.0.svg_node().view_box.rect; + let r = tree.0.svg_node().view_box.rect; resvg_rect { x: r.x(), @@ -440,32 +443,44 @@ pub extern fn resvg_get_image_viewbox( } } +#[no_mangle] +pub extern fn resvg_is_image_empty( + tree: *const resvg_render_tree, +) -> bool { + let tree = unsafe { + assert!(!tree.is_null()); + &*tree + }; + + tree.0.root().has_children() +} + #[cfg(feature = "qt-backend")] #[no_mangle] pub extern fn resvg_qt_get_node_bbox( - rtree: *const resvg_render_tree, + tree: *const resvg_render_tree, opt: *const resvg_options, id: *const c_char, bbox: *mut resvg_rect, ) -> bool { let backend = Box::new(resvg::render_qt::Backend); - get_node_bbox(rtree, opt, id, bbox, backend) + get_node_bbox(tree, opt, id, bbox, backend) } #[cfg(feature = "cairo-backend")] #[no_mangle] pub extern fn resvg_cairo_get_node_bbox( - rtree: *const resvg_render_tree, + tree: *const resvg_render_tree, opt: *const resvg_options, id: *const c_char, bbox: *mut resvg_rect, ) -> bool { let backend = Box::new(resvg::render_cairo::Backend); - get_node_bbox(rtree, opt, id, bbox, backend) + get_node_bbox(tree, opt, id, bbox, backend) } fn get_node_bbox( - rtree: *const resvg_render_tree, + tree: *const resvg_render_tree, opt: *const resvg_options, id: *const c_char, bbox: *mut resvg_rect, @@ -484,9 +499,9 @@ fn get_node_bbox( return false; } - let rtree = unsafe { - assert!(!rtree.is_null()); - &*rtree + let tree = unsafe { + assert!(!tree.is_null()); + &*tree }; @@ -495,7 +510,7 @@ fn get_node_bbox( &*opt }); - match rtree.0.node_by_id(id) { + match tree.0.node_by_id(id) { Some(node) => { if let Some(r) = backend.calc_node_bbox(&node, &opt) { unsafe { @@ -519,7 +534,7 @@ fn get_node_bbox( #[no_mangle] pub extern fn resvg_node_exists( - rtree: *const resvg_render_tree, + tree: *const resvg_render_tree, id: *const c_char, ) -> bool { let id = match cstr_to_str(id) { @@ -530,17 +545,17 @@ pub extern fn resvg_node_exists( } }; - let rtree = unsafe { - assert!(!rtree.is_null()); - &*rtree + let tree = unsafe { + assert!(!tree.is_null()); + &*tree }; - rtree.0.node_by_id(id).is_some() + tree.0.node_by_id(id).is_some() } #[no_mangle] pub extern fn resvg_get_node_transform( - rtree: *const resvg_render_tree, + tree: *const resvg_render_tree, id: *const c_char, ts: *mut resvg_transform, ) -> bool { @@ -552,13 +567,14 @@ pub extern fn resvg_get_node_transform( } }; - let rtree = unsafe { - assert!(!rtree.is_null()); - &*rtree + let tree = unsafe { + assert!(!tree.is_null()); + &*tree }; - if let Some(node) = rtree.0.node_by_id(id) { - let abs_ts = resvg::utils::abs_transform(&node); + if let Some(node) = tree.0.node_by_id(id) { + let mut abs_ts = resvg::utils::abs_transform(&node); + abs_ts.append(&node.transform()); unsafe { (*ts).a = abs_ts.a; @@ -633,3 +649,12 @@ fn to_native_opt(opt: &resvg_options) -> resvg::Options { background, } } + +fn convert_error(e: usvg::Error) -> ErrorId { + match e { + usvg::Error::InvalidFileSuffix => ErrorId::InvalidFileSuffix, + usvg::Error::FileOpenFailed => ErrorId::FileOpenFailed, + usvg::Error::NotAnUtf8Str => ErrorId::NotAnUtf8Str, + usvg::Error::MalformedGZip => ErrorId::MalformedGZip, + } +} |