From b066739bfdb04408dc34cef450261ff60d631bb7 Mon Sep 17 00:00:00 2001 From: Henner Zeller Date: Sat, 16 Dec 2023 07:04:35 -0800 Subject: [PATCH] Render SVGs with librsvg This uses the librsvg library to render SVG images, which results in much higher quality output than we get from graphicsmagick. Issues #122 --- .github/workflows/macos.yml | 1 + .github/workflows/ubuntu.yml | 13 ++++- CMakeLists.txt | 16 ++++-- README.md | 3 + shell.nix | 7 +++ src/CMakeLists.txt | 6 ++ src/framebuffer.h | 2 +- src/image-source.cc | 8 +++ src/svg-image-source.cc | 103 +++++++++++++++++++++++++++++++++++ src/svg-image-source.h | 50 +++++++++++++++++ src/timg.cc | 98 ++++++++++++++++++--------------- 11 files changed, 255 insertions(+), 52 deletions(-) create mode 100644 src/svg-image-source.cc create mode 100644 src/svg-image-source.h diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index 428776d..26ce8a0 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -17,6 +17,7 @@ jobs: brew install libdeflate libsixel brew install ffmpeg jpeg-turbo libexif libpng brew install openslide + brew install librsvg cairo brew install pandoc - name: Get the Source uses: actions/checkout@v3 diff --git a/.github/workflows/ubuntu.yml b/.github/workflows/ubuntu.yml index 001bdbf..cfc3895 100644 --- a/.github/workflows/ubuntu.yml +++ b/.github/workflows/ubuntu.yml @@ -36,7 +36,7 @@ jobs: run: | mkdir build-limitdep cd build-limitdep - cmake .. -DWITH_VIDEO_DECODING=Off -DWITH_VIDEO_DEVICE=Off -DWITH_OPENSLIDE_SUPPORT=Off -DWITH_GRAPHICSMAGICK=Off -DWITH_TURBOJPEG=Off -DWITH_LIBSIXEL=Off + cmake .. -DWITH_VIDEO_DECODING=Off -DWITH_VIDEO_DEVICE=Off -DWITH_OPENSLIDE_SUPPORT=Off -DWITH_GRAPHICSMAGICK=Off -DWITH_TURBOJPEG=Off -DWITH_RSVG=Off -DWITH_LIBSIXEL=Off make -k - name: Install Full Dependencies @@ -45,6 +45,7 @@ jobs: sudo apt install libgraphicsmagick++-dev sudo apt install libturbojpeg-dev libexif-dev sudo apt install libsixel-dev + sudo apt install librsvg2-dev libcairo-dev sudo apt install libavcodec-dev libavformat-dev libavdevice-dev sudo apt install libopenslide-dev sudo apt install pandoc @@ -53,9 +54,17 @@ jobs: run: | mkdir build cd build - cmake .. -DWITH_VIDEO_DECODING=On -DWITH_VIDEO_DEVICE=On -DWITH_OPENSLIDE_SUPPORT=On -DWITH_STB_IMAGE=On -DWITH_LIBSIXEL=On + cmake .. -DWITH_VIDEO_DECODING=On -DWITH_VIDEO_DEVICE=On -DWITH_OPENSLIDE_SUPPORT=On -DWITH_STB_IMAGE=On -DWITH_RSVG=On -DWITH_LIBSIXEL=On make -k + - name: Version string with low dependencies + run: | + echo "Limited dependency version string" + build-limitdep/src/timg --version + + echo "All dependencies version string" + build/src/timg --version + CodeFormatting: if: false # currently, there is no clang-format-13 in ubuntu latest runs-on: ubuntu-latest diff --git a/CMakeLists.txt b/CMakeLists.txt index b9eed25..8fffbc2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,18 +4,21 @@ project(timg VERSION 1.5.3 LANGUAGES CXX) option(WITH_VIDEO_DECODING "Enables video decoding feature" ON) option(WITH_VIDEO_DEVICE "Enables reading videos from devices e.g. v4l2 (requires WITH_VIDEO_DECODING)" ON) -option(WITH_OPENSLIDE_SUPPORT "Enables support to scientific OpenSlide formats" OFF) # Options that should be typically on, but could be disabled for special # applications where less dependencies are required option(WITH_GRAPHICSMAGICK "Enable general image loading with Graphicsmagick. You typically want this." ON) option(WITH_TURBOJPEG "Optimized JPEG loading. You typically want this." ON) -option(WITH_LIBSIXEL "Provide sixel output which is supported by some older terminals such as xterm" ON) - +option(WITH_RSVG "Use librsvg to open SVG images." ON) option(WITH_STB_IMAGE "Use STB image, a self-contained albeit limited image loading and lower quality. Use if WITH_GRAPHICSMAGICK is not possible and want to limit dependencies. Default on to be used as fallback." ON) - option(WITH_QOI_IMAGE "QOI image format" ON) +# Compile-time option for specialized +option(WITH_OPENSLIDE_SUPPORT "Enables support to scientific OpenSlide formats" OFF) + +# Output formats +option(WITH_LIBSIXEL "Provide sixel output which is supported by some older terminals such as xterm" ON) + # Note: The version string can be ammended with -DDISTRIBUTION_VERSION, see src/timg-version.h.in option(TIMG_VERSION_FROM_GIT "Get the program version from the git repository" ON) @@ -52,6 +55,11 @@ if(WITH_GRAPHICSMAGICK) pkg_check_modules(GRAPHICSMAGICKXX IMPORTED_TARGET REQUIRED GraphicsMagick++) endif() +if(WITH_RSVG) + pkg_check_modules(RSVG REQUIRED IMPORTED_TARGET librsvg-2.0) + pkg_check_modules(CAIRO REQUIRED IMPORTED_TARGET cairo) +endif() + if(WITH_OPENSLIDE_SUPPORT) pkg_check_modules(OPENSLIDE IMPORTED_TARGET REQUIRED openslide) pkg_check_modules(AVUTIL REQUIRED IMPORTED_TARGET libavutil) diff --git a/README.md b/README.md index c73c73b..98bba76 100644 --- a/README.md +++ b/README.md @@ -558,6 +558,9 @@ compile-time choices: typically want this **ON** (default). * **`WITH_TURBOJPEG`** If enabled, uses this for faster jpeg file loading. You typically want this **ON** (default). + * **`WITH_RSVG`** High-quality SVG renderer. Needs librsvg and cairo. + If not compiled-in, will fallback to GraphicsMagick, but that typically + results in lower quality renderings. Typically want this **ON** (default). * **`WITH_OPENSLIDE_SUPPORT`** Openslide is an image format used in scientific applications. Rarely used, so default off, switch ON if needed. * **`WITH_QOI_IMAGE`** Allow decoding of Quite Ok Image format [QOI]. Small diff --git a/shell.nix b/shell.nix index ce5f5ff..7da38ec 100644 --- a/shell.nix +++ b/shell.nix @@ -16,6 +16,13 @@ pkgs.mkShell { ffmpeg libexif libsixel + librsvg cairo + + # Don't include qoi and stb by default to see if the cmake + # fallback to third_party/ works. + #qoi + #stb + openslide pandoc clang-tools_13 # clang-format diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 2edf9ec..95ea076 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -57,6 +57,12 @@ if(WITH_GRAPHICSMAGICK) target_link_libraries(timg PkgConfig::GRAPHICSMAGICKXX) endif() +if(WITH_RSVG) + target_sources(timg PUBLIC svg-image-source.h svg-image-source.cc) + target_compile_definitions(timg PUBLIC WITH_TIMG_RSVG) + target_link_libraries(timg PkgConfig::RSVG PkgConfig::CAIRO) +endif() + if(WITH_TURBOJPEG) target_sources(timg PUBLIC jpeg-source.h jpeg-source.cc) target_compile_definitions(timg PUBLIC WITH_TIMG_JPEG) diff --git a/src/framebuffer.h b/src/framebuffer.h index 624128e..515e9ab 100644 --- a/src/framebuffer.h +++ b/src/framebuffer.h @@ -26,7 +26,7 @@ namespace timg { struct rgba_t { uint8_t r, g, b; // Color components, gamma corrected (non-linear) - uint8_t a; // Alpha channel. Linear. + uint8_t a; // Alpha channel. Linear. [transparent..opaque]=0..255 inline bool operator==(const rgba_t &that) const { // Using memcmp() slower, so force uint-compare with type-punning. diff --git a/src/image-source.cc b/src/image-source.cc index 1d21d6b..4f1f1f4 100644 --- a/src/image-source.cc +++ b/src/image-source.cc @@ -34,6 +34,7 @@ #include "openslide-source.h" #include "qoi-image-source.h" #include "stb-image-source.h" +#include "svg-image-source.h" #include "video-display.h" namespace timg { @@ -176,6 +177,13 @@ ImageSource *ImageSource::Create(const std::string &filename, } #endif +#ifdef WITH_TIMG_RSVG + result.reset(new SVGImageSource(filename)); + if (result->LoadAndScale(options, frame_offset, frame_count)) { + return result.release(); + } +#endif + #ifdef WITH_TIMG_GRPAPHICSMAGICK result.reset(new ImageLoader(filename)); if (result->LoadAndScale(options, frame_offset, frame_count)) { diff --git a/src/svg-image-source.cc b/src/svg-image-source.cc new file mode 100644 index 0000000..a3e2ba0 --- /dev/null +++ b/src/svg-image-source.cc @@ -0,0 +1,103 @@ +// -*- mode: c++; c-basic-offset: 4; indent-tabs-mode: nil; -*- +// (c) 2023 Henner Zeller +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation version 2. +// +// This program 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 General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see + +#include "svg-image-source.h" + +#include +#include +#include + +#include "framebuffer.h" + +namespace timg { + +std::string SVGImageSource::FormatTitle( + const std::string &format_string) const { + return FormatFromParameters(format_string, filename_, (int)orig_width_, + (int)orig_height_, "svg"); +} + +bool SVGImageSource::LoadAndScale(const DisplayOptions &opts, int, int) { + options_ = opts; + RsvgHandle *svg = rsvg_handle_new_from_file(filename_.c_str(), nullptr); + if (!svg) return false; + + RsvgRectangle viewbox; + gboolean out_has_width, out_has_height, out_has_viewbox; + RsvgLength svg_width, svg_height; + rsvg_handle_get_intrinsic_dimensions(svg, &out_has_width, &svg_width, + &out_has_height, &svg_height, + &out_has_viewbox, &viewbox); + if (out_has_viewbox) { + orig_width_ = viewbox.width; + orig_height_ = viewbox.height; + } + else if (out_has_width && out_has_height) { + // We ignore the unit, but this will still result in proper aspect ratio + orig_width_ = svg_width.length; + orig_height_ = svg_height.length; + } + + int target_width; + int target_height; + CalcScaleToFitDisplay(orig_width_, orig_height_, opts, false, &target_width, + &target_height); + + const auto kCairoFormat = CAIRO_FORMAT_ARGB32; + int stride = cairo_format_stride_for_width(kCairoFormat, target_width); + image_.reset(new timg::Framebuffer(stride / 4, target_height)); + + cairo_surface_t *surface = cairo_image_surface_create_for_data( + (uint8_t *)image_->begin(), kCairoFormat, target_width, target_height, + stride); + cairo_t *cr = cairo_create(surface); + + RsvgRectangle viewport = { + .x = 0.0, + .y = 0.0, + .width = (double)target_width, + .height = (double)target_height, + }; + + bool success = rsvg_handle_render_document(svg, cr, &viewport, nullptr); + cairo_destroy(cr); + cairo_surface_destroy(surface); + g_object_unref(svg); + + // TODO: if (int(stride / sizeof(rgba_t)) != target_width) : copy over + + // If requested, merge background with pattern. + image_->AlphaComposeBackground( + options_.bgcolor_getter, options_.bg_pattern_color, + options_.pattern_size * options_.cell_x_px, + options_.pattern_size * options_.cell_y_px / 2); + + return success; +} + +int SVGImageSource::IndentationIfCentered( + const timg::Framebuffer &image) const { + return options_.center_horizontally ? (options_.width - image.width()) / 2 + : 0; +} + +void SVGImageSource::SendFrames(const Duration &duration, int loops, + const volatile sig_atomic_t &interrupt_received, + const Renderer::WriteFramebufferFun &sink) { + sink(IndentationIfCentered(*image_), 0, *image_, SeqType::FrameImmediate, + {}); +} + +} // namespace timg diff --git a/src/svg-image-source.h b/src/svg-image-source.h new file mode 100644 index 0000000..b9d5be5 --- /dev/null +++ b/src/svg-image-source.h @@ -0,0 +1,50 @@ +// -*- mode: c++; c-basic-offset: 4; indent-tabs-mode: nil; -*- +// (c) 2023 Henner Zeller +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation version 2. +// +// This program 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 General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see + +#ifndef SVG_SOURCE_H_ +#define SVG_SOURCE_H_ + +#include + +#include "display-options.h" +#include "image-source.h" +#include "terminal-canvas.h" + +namespace timg { +class SVGImageSource final : public ImageSource { +public: + explicit SVGImageSource(const std::string &filename) + : ImageSource(filename) {} + + bool LoadAndScale(const DisplayOptions &options, int frame_offset, + int frame_count) final; + + void SendFrames(const Duration &duration, int loops, + const volatile sig_atomic_t &interrupt_received, + const Renderer::WriteFramebufferFun &sink) final; + + std::string FormatTitle(const std::string &format_string) const final; + +private: + int IndentationIfCentered(const timg::Framebuffer &image) const; + + DisplayOptions options_; + double orig_width_, orig_height_; + std::unique_ptr image_; +}; + +} // namespace timg + +#endif // QOI_SOURCE_H_ diff --git a/src/timg.cc b/src/timg.cc index 102b586..dfeba83 100644 --- a/src/timg.cc +++ b/src/timg.cc @@ -46,11 +46,15 @@ #endif #ifdef WITH_TIMG_GRPAPHICSMAGICK # include + # include "image-display.h" #endif #ifdef WITH_TIMG_SIXEL # include #endif +#ifdef WITH_TIMG_RSVG +# include +#endif #include #include @@ -415,6 +419,51 @@ static const char *PixelationToString(Pixelation p) { return ""; // Make compiler happy. } +// Print our version and various version numbers from our dependencies. +static int PrintVersion() { + fprintf(stderr, "timg " TIMG_VERSION + " \n" + "Copyright (c) 2016..2023 Henner Zeller. " + "This program is free software; license GPL 2.0.\n\n"); +#ifdef WITH_TIMG_GRPAPHICSMAGICK + fprintf(stderr, "Image decoding %s\n", timg::ImageLoader::VersionInfo()); +#endif +#ifdef WITH_TIMG_OPENSLIDE_SUPPORT + fprintf(stderr, "Openslide %s\n", timg::OpenSlideSource::VersionInfo()); +#endif +#ifdef WITH_TIMG_JPEG + fprintf(stderr, "Turbo JPEG\n"); +#endif +#ifdef WITH_TIMG_RSVG + fprintf(stderr, "librsvg %d.%d.%d\n", LIBRSVG_MAJOR_VERSION, + LIBRSVG_MINOR_VERSION, LIBRSVG_MICRO_VERSION); +#endif +#ifdef WITH_TIMG_QOI + fprintf(stderr, "QOI image loading\n"); +#endif +#ifdef WITH_TIMG_STB + fprintf(stderr, + "STB image loading" +# ifdef WITH_TIMG_GRPAPHICSMAGICK + // If we have graphics magic, that will take images first, + // so STB will only really be called as fallback. + " fallback" +# endif + "\n"); +#endif + fprintf(stderr, "swscale %s\n", AV_STRINGIFY(LIBSWSCALE_VERSION)); +#ifdef WITH_TIMG_VIDEO + fprintf(stderr, "Video decoding %s\n", timg::VideoLoader::VersionInfo()); +#endif + fprintf(stderr, + "Half, quarter, iterm2, and kitty graphics output: " + "timg builtin.\n"); +#ifdef WITH_TIMG_SIXEL + fprintf(stderr, "Libsixel version %s\n", LIBSIXEL_VERSION); +#endif + return 0; +} + int main(int argc, char *argv[]) { #ifdef WITH_TIMG_GRPAPHICSMAGICK Magick::InitializeMagick(*argv); @@ -655,48 +704,7 @@ int main(int argc, char *argv[]) { break; case 'E': present.hide_cursor = false; break; case 'W': display_opts.fill_width = true; break; - case OPT_VERSION: - fprintf(stderr, - "timg " TIMG_VERSION - " \n" - "Copyright (c) 2016..2023 Henner Zeller. " - "This program is free software; license GPL 2.0.\n\n"); -#ifdef WITH_TIMG_GRPAPHICSMAGICK - fprintf(stderr, "Image decoding %s\n", - timg::ImageLoader::VersionInfo()); -#endif -#ifdef WITH_TIMG_OPENSLIDE_SUPPORT - fprintf(stderr, "Openslide %s\n", - timg::OpenSlideSource::VersionInfo()); -#endif -#ifdef WITH_TIMG_JPEG - fprintf(stderr, "Turbo JPEG\n"); -#endif -#ifdef WITH_TIMG_QOI - fprintf(stderr, "QOI image loading\n"); -#endif -#ifdef WITH_TIMG_STB - fprintf(stderr, - "STB image loading" -# ifdef WITH_TIMG_GRPAPHICSMAGICK - // If we have graphics magic, that will take images first, - // so STB will only really be called as fallback. - " fallback" -# endif - "\n"); -#endif - fprintf(stderr, "swscale %s\n", AV_STRINGIFY(LIBSWSCALE_VERSION)); -#ifdef WITH_TIMG_VIDEO - fprintf(stderr, "Video decoding %s\n", - timg::VideoLoader::VersionInfo()); -#endif - fprintf(stderr, - "Half, quarter, iterm2, and kitty graphics output: " - "timg builtin.\n"); -#ifdef WITH_TIMG_SIXEL - fprintf(stderr, "Libsixel version %s\n", LIBSIXEL_VERSION); -#endif - return 0; + case OPT_VERSION: return PrintVersion(); case OPT_TITLE: display_opts.show_title = !display_opts.show_title; if (optarg) display_opts.title_format = optarg; @@ -807,13 +815,13 @@ int main(int argc, char *argv[]) { present.pixelation = Pixelation::kKittyGraphics; break; case timg::GraphicsProtocol::kSixel: -# ifdef WITH_TIMG_SIXEL +#ifdef WITH_TIMG_SIXEL present.pixelation = Pixelation::kSixelGraphics; present.sixel_cursor_workaround = graphics_info.known_broken_sixel_cursor_placement; -# else +#else present.pixelation = Pixelation::kQuarterBlock; -# endif +#endif break; case timg::GraphicsProtocol::kNone: break; }