svg: replaced nanosvg by librsvg for broader compatibility with SVG2 standard

This commit is contained in:
Lemmy
2026-05-03 19:49:39 -04:00
parent 0b0122653e
commit 49f9bce464
7 changed files with 187 additions and 4722 deletions
+51 -22
View File
@@ -4,37 +4,66 @@ Noctalia is made possible by the incredible work of many open-source projects an
## Design & Branding
- **MrDowntempo** - Creator of the Noctalia Owl and moon logo
- **[SaberJ2X](https://www.reddit.com/user/SaberJ64/)** - Creator of Talia, the Noctalia mascot
- **[Tabler icons](https://tabler.io/icons)** - The fantastic tabler icons.
## Runtime Dependencies
### Vendored Libraries
- **[fzy](https://github.com/jhawthorn/fzy)** - Fuzzy matching algorithm used for launcher ranking
### System Integration
- **[wlsunset](https://sr.ht/~kennylevinsen/wlsunset/)** - Night light and blue light filter support
- **[ddcutil](https://www.ddcutil.com/)** - External display brightness control
### Media & Audio
- **[gpu-screen-recorder](https://git.dec05eba.com/gpu-screen-recorder/about/)** - Hardware-accelerated screen recording
## Icons
- **[Tabler Icons](https://tabler.io/icons)** - Icon set used throughout the shell
- **[Riyan Resdian on Noun Project](https://thenounproject.com/creator/yaicon/)** - Plug icon
- **MrDowntempo** Creator of the Noctalia Owl and moon logo
- **[SaberJ2X](https://www.reddit.com/user/SaberJ64/)** Creator of Talia, the Noctalia mascot
- **[Tabler Icons](https://tabler.io/icons)** — Icon set used throughout the shell
- **[Riyan Resdian on Noun Project](https://thenounproject.com/creator/yaicon/)** — Plug icon
## Audio Assets
- **[Universfield on Pixabay](https://pixabay.com/users/universfield-28281460/)** - Notification sound effect
- **[Lucas McCallister on Freesound](http://www.freesound.org/samplesViewSingle.php?id=67091)** - Volume change feedback sound effect
- **[Universfield on Pixabay](https://pixabay.com/users/universfield-28281460/)** — Notification sound effect
- **[Lucas McCallister on Freesound](http://www.freesound.org/samplesViewSingle.php?id=67091)** — Volume change feedback sound effect
## System Libraries
Linked dynamically at runtime:
- **[Wayland](https://wayland.freedesktop.org/)** (`wayland-client`, `wayland-protocols`, `wayland-egl`) — Display protocol
- **[Mesa / EGL / GLES2](https://www.mesa3d.org/)** (or **[libepoxy](https://github.com/anholt/libepoxy)** as fallback) — OpenGL ES context and dispatch
- **[Cairo](https://www.cairographics.org/)** — 2D graphics surface used for text and SVG rasterization
- **[Pango](https://pango.gnome.org/)** / **PangoCairo** — Text layout and shaping
- **[FreeType](https://freetype.org/)** — Font rasterization
- **[Fontconfig](https://www.fontconfig.org/)** — Font discovery
- **[librsvg](https://wiki.gnome.org/Projects/LibRsvg)** — SVG rendering (filters, clipPaths, masks)
- **[GLib / GObject / GIO](https://gitlab.gnome.org/GNOME/glib)** — Core utilities used by Pango/Cairo/librsvg
- **[libxkbcommon](https://xkbcommon.org/)** — Keyboard handling
- **[sdbus-c++](https://github.com/Kistler-Group/sdbus-cpp)** — D-Bus client bindings
- **[PipeWire](https://pipewire.org/)** — Audio capture and playback
- **[libcurl](https://curl.se/libcurl/)** — HTTP client
- **[libwebp](https://developers.google.com/speed/webp)** — WebP decoding
- **[polkit](https://gitlab.freedesktop.org/polkit/polkit)** (`polkit-agent`, `polkit-gobject`) — Authentication agent
- **[Linux-PAM](https://github.com/linux-pam/linux-pam)** — Lockscreen authentication
## Vendored Libraries
Bundled in `third_party/` and built from source:
- **[dr_wav](https://github.com/mackron/dr_libs)** — Single-file WAV decoder (MIT-0 / public domain)
- **[fzy](https://github.com/jhawthorn/fzy)** — Fuzzy matching algorithm used by the launcher, search pickers, and other shell ranking (MIT)
- **[Luau](https://luau.org/)** — Lua dialect used for theme/template scripting (MIT)
- **[Material Color Utilities](https://github.com/material-foundation/material-color-utilities)** — Material 3 palette generation (Apache-2.0)
- **[nlohmann/json](https://github.com/nlohmann/json)** — JSON for Modern C++ (MIT)
- **[stb](https://github.com/nothings/stb)** — Single-file utilities, primarily image I/O (MIT / public domain)
- **[tinyexpr](https://github.com/codeplea/tinyexpr)** — Math expression parser (zlib)
- **[toml++](https://github.com/marzer/tomlplusplus)** — TOML parser (MIT)
- **[Wuffs](https://github.com/google/wuffs)** — Memory-safe image decoders (Apache-2.0)
## System Integration
External tools Noctalia integrates with at runtime when present:
- **[wlsunset](https://sr.ht/~kennylevinsen/wlsunset/)** — Night light and blue light filter
- **[ddcutil](https://www.ddcutil.com/)** — External display brightness control
- **[gpu-screen-recorder](https://git.dec05eba.com/gpu-screen-recorder/about/)** — Hardware-accelerated screen recording
## Special Thanks
- The **Wayland** community for building the future of Linux desktop graphics
- The **Niri**, **Hyprland**, **Sway**, **Labwc**, and **MangoWC** teams for their excellent Wayland compositors
- All the contributors and users who have helped make Noctalia better
## License
Noctalia is licensed under the MIT License. See [LICENSE](LICENSE) for details.
Each dependency listed above is governed by its own respective license. Please refer to their individual projects for licensing information.
+2 -2
View File
@@ -41,6 +41,7 @@ cairo_dep = dependency('cairo')
cairo_ft_dep = dependency('cairo-ft')
pango_dep = dependency('pango')
pangocairo_dep = dependency('pangocairo')
librsvg_dep = dependency('librsvg-2.0')
xkbcommon_dep = dependency('xkbcommon')
glib_dep = dependency('glib-2.0')
gobject_dep = dependency('gobject-2.0')
@@ -385,7 +386,6 @@ _noctalia_sources = files(
'src/render/core/shader_program.cpp',
'src/render/core/thumbnail_service.cpp',
'src/render/core/image_decoder.cpp',
'src/render/image_loaders.cpp',
'src/render/programs/glyph_program.cpp',
'src/render/programs/image_program.cpp',
'src/render/programs/linear_gradient_program.cpp',
@@ -626,7 +626,6 @@ endif
# ── Include directories ────────────────────────────────────────────────────────
_noctalia_inc = [
include_directories('src'),
include_directories('third_party/nanosvg'),
include_directories('third_party/tomlplusplus'),
include_directories('third_party/wuffs', is_system: true),
# nlohmann is header-only and uses GCC extensions in some paths; treat as system.
@@ -651,6 +650,7 @@ executable('noctalia',
cairo_ft_dep,
pango_dep,
pangocairo_dep,
librsvg_dep,
xkbcommon_dep,
glib_dep,
gobject_dep,
+134 -61
View File
@@ -1,23 +1,144 @@
#include "render/core/image_file_loader.h"
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wconversion"
#pragma GCC diagnostic ignored "-Wsign-conversion"
#pragma GCC diagnostic ignored "-Wfloat-conversion"
#pragma GCC diagnostic ignored "-Wold-style-cast"
#pragma GCC diagnostic ignored "-Wshadow"
#include <nanosvg.h>
#include <nanosvgrast.h>
#pragma GCC diagnostic pop
#include "render/core/image_decoder.h"
#include "util/file_utils.h"
#include <algorithm>
#include <cairo.h>
#include <cmath>
#include <fstream>
#include <cstdint>
#include <cstring>
#include <librsvg/rsvg.h>
namespace {} // namespace
namespace {
// Convert a cairo ARGB32 image surface (premultiplied BGRA on little-endian)
// into the non-premultiplied RGBA buffer the rest of the pipeline expects.
void argb32ToRgba(const unsigned char* src, int srcStride, std::uint8_t* dst, int width, int height) {
for (int y = 0; y < height; ++y) {
const std::uint32_t* row = reinterpret_cast<const std::uint32_t*>(src + (y * srcStride));
std::uint8_t* outRow = dst + (static_cast<std::size_t>(y) * static_cast<std::size_t>(width) * 4U);
for (int x = 0; x < width; ++x) {
const std::uint32_t pixel = row[x];
const std::uint8_t a = static_cast<std::uint8_t>((pixel >> 24) & 0xFF);
std::uint8_t r = static_cast<std::uint8_t>((pixel >> 16) & 0xFF);
std::uint8_t g = static_cast<std::uint8_t>((pixel >> 8) & 0xFF);
std::uint8_t b = static_cast<std::uint8_t>(pixel & 0xFF);
if (a != 0 && a != 255) {
// Un-premultiply, rounding to nearest.
r = static_cast<std::uint8_t>(std::min(255, ((r * 255) + (a / 2)) / a));
g = static_cast<std::uint8_t>(std::min(255, ((g * 255) + (a / 2)) / a));
b = static_cast<std::uint8_t>(std::min(255, ((b * 255) + (a / 2)) / a));
}
outRow[(x * 4) + 0] = r;
outRow[(x * 4) + 1] = g;
outRow[(x * 4) + 2] = b;
outRow[(x * 4) + 3] = a;
}
}
}
std::optional<LoadedImageFile> rasterizeSvg(const std::vector<std::uint8_t>& fileData, int targetSize,
std::string* errorMessage) {
GError* gerror = nullptr;
RsvgHandle* handle = rsvg_handle_new_from_data(fileData.data(), fileData.size(), &gerror);
if (handle == nullptr) {
if (errorMessage != nullptr) {
*errorMessage = std::string("failed to parse SVG: ") + (gerror != nullptr ? gerror->message : "unknown");
}
if (gerror != nullptr) {
g_error_free(gerror);
}
return std::nullopt;
}
// Determine intrinsic pixel size. Many real-world SVGs (e.g. viewBox-only)
// do not advertise pixel dimensions, so fall back to the viewBox or to a
// sensible default before computing the render scale.
gdouble intrinsicW = 0.0;
gdouble intrinsicH = 0.0;
gboolean hasIntrinsic = rsvg_handle_get_intrinsic_size_in_pixels(handle, &intrinsicW, &intrinsicH);
if (hasIntrinsic == FALSE || intrinsicW <= 0.0 || intrinsicH <= 0.0) {
gboolean outHasW = FALSE;
RsvgLength outW{};
gboolean outHasH = FALSE;
RsvgLength outH{};
gboolean outHasViewbox = FALSE;
RsvgRectangle outViewbox{};
rsvg_handle_get_intrinsic_dimensions(handle, &outHasW, &outW, &outHasH, &outH, &outHasViewbox, &outViewbox);
if (outHasViewbox == TRUE && outViewbox.width > 0.0 && outViewbox.height > 0.0) {
intrinsicW = outViewbox.width;
intrinsicH = outViewbox.height;
} else {
intrinsicW = 512.0;
intrinsicH = 512.0;
}
}
int width = static_cast<int>(std::round(intrinsicW));
int height = static_cast<int>(std::round(intrinsicH));
if (targetSize > 0) {
const double maxSide = std::max(intrinsicW, intrinsicH);
const double scale = static_cast<double>(targetSize) / maxSide;
width = std::max(1, static_cast<int>(std::round(intrinsicW * scale)));
height = std::max(1, static_cast<int>(std::round(intrinsicH * scale)));
}
if (width <= 0 || height <= 0) {
if (errorMessage != nullptr) {
*errorMessage = "invalid SVG dimensions";
}
g_object_unref(handle);
return std::nullopt;
}
cairo_surface_t* surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height);
if (cairo_surface_status(surface) != CAIRO_STATUS_SUCCESS) {
if (errorMessage != nullptr) {
*errorMessage = "failed to create cairo surface";
}
cairo_surface_destroy(surface);
g_object_unref(handle);
return std::nullopt;
}
cairo_t* cr = cairo_create(surface);
RsvgRectangle viewport{
.x = 0.0,
.y = 0.0,
.width = static_cast<double>(width),
.height = static_cast<double>(height),
};
GError* renderError = nullptr;
if (rsvg_handle_render_document(handle, cr, &viewport, &renderError) == FALSE) {
if (errorMessage != nullptr) {
*errorMessage =
std::string("failed to render SVG: ") + (renderError != nullptr ? renderError->message : "unknown");
}
if (renderError != nullptr) {
g_error_free(renderError);
}
cairo_destroy(cr);
cairo_surface_destroy(surface);
g_object_unref(handle);
return std::nullopt;
}
cairo_destroy(cr);
cairo_surface_flush(surface);
LoadedImageFile loaded{
.rgba = std::vector<std::uint8_t>(static_cast<std::size_t>(width) * static_cast<std::size_t>(height) * 4U),
.width = width,
.height = height,
};
argb32ToRgba(cairo_image_surface_get_data(surface), cairo_image_surface_get_stride(surface), loaded.rgba.data(),
width, height);
cairo_surface_destroy(surface);
g_object_unref(handle);
return loaded;
}
} // namespace
std::optional<LoadedImageFile> loadImageFile(const std::string& path, int targetSize, std::string* errorMessage) {
if (path.empty()) {
@@ -36,55 +157,7 @@ std::optional<LoadedImageFile> loadImageFile(const std::string& path, int target
}
if (path.ends_with(".svg") || path.ends_with(".SVG")) {
// nsvgParse needs a null-terminated mutable string.
fileData.push_back(0);
auto* image = nsvgParse(reinterpret_cast<char*>(fileData.data()), "px", 96.0f);
if (image == nullptr) {
if (errorMessage != nullptr) {
*errorMessage = "failed to parse SVG";
}
return std::nullopt;
}
int width = static_cast<int>(image->width);
int height = static_cast<int>(image->height);
if (targetSize > 0 && image->width > 0.0f && image->height > 0.0f) {
// Preserve source aspect ratio and constrain the longer side to targetSize.
const float maxSide = std::max(image->width, image->height);
const float scale = static_cast<float>(targetSize) / maxSide;
width = std::max(1, static_cast<int>(std::round(image->width * scale)));
height = std::max(1, static_cast<int>(std::round(image->height * scale)));
}
if (width <= 0 || height <= 0) {
if (errorMessage != nullptr) {
*errorMessage = "invalid SVG dimensions";
}
nsvgDelete(image);
return std::nullopt;
}
const float scaleX = static_cast<float>(width) / image->width;
const float scaleY = static_cast<float>(height) / image->height;
const float scale = std::min(scaleX, scaleY);
auto* rast = nsvgCreateRasterizer();
if (rast == nullptr) {
if (errorMessage != nullptr) {
*errorMessage = "failed to create SVG rasterizer";
}
nsvgDelete(image);
return std::nullopt;
}
LoadedImageFile loaded{
.rgba = std::vector<std::uint8_t>(static_cast<std::size_t>(width) * static_cast<std::size_t>(height) * 4U),
.width = width,
.height = height,
};
nsvgRasterize(rast, image, 0, 0, scale, loaded.rgba.data(), width, height, width * 4);
nsvgDeleteRasterizer(rast);
nsvgDelete(image);
return loaded;
return rasterizeSvg(fileData, targetSize, errorMessage);
}
if (auto decoded = decodeRasterImage(fileData.data(), fileData.size(), errorMessage)) {
-15
View File
@@ -1,15 +0,0 @@
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wconversion"
#pragma GCC diagnostic ignored "-Wsign-conversion"
#pragma GCC diagnostic ignored "-Wfloat-conversion"
#pragma GCC diagnostic ignored "-Wold-style-cast"
#pragma GCC diagnostic ignored "-Wshadow"
#pragma GCC diagnostic ignored "-Wunused-function"
#define NANOSVG_IMPLEMENTATION
#include <nanosvg.h>
#define NANOSVGRAST_IMPLEMENTATION
#include <nanosvgrast.h>
#pragma GCC diagnostic pop
-18
View File
@@ -1,18 +0,0 @@
Copyright (c) 2013-14 Mikko Mononen memon@inside.org
This software is provided 'as-is', without any express or implied
warranty. In no event will the authors be held liable for any damages
arising from the use of this software.
Permission is granted to anyone to use this software for any purpose,
including commercial applications, and to alter it and redistribute it
freely, subject to the following restrictions:
1. The origin of this software must not be misrepresented; you must not
claim that you wrote the original software. If you use this software
in a product, an acknowledgment in the product documentation would be
appreciated but is not required.
2. Altered source versions must be plainly marked as such, and must not be
misrepresented as being the original software.
3. This notice may not be removed or altered from any source distribution.
-3132
View File
File diff suppressed because it is too large Load Diff
-1472
View File
File diff suppressed because it is too large Load Diff