Handle a null surface from the image decoder callback - #1
Open
iurisilvio wants to merge 3 commits into
Open
Conversation
1 task
plutovg.h and lunasvg.h both include <cairo.h> since the port to cairo, but the build never looks for it, so a standalone `cmake .` fails on the first source file that pulls either header in. The zig build in node-canvas passes the include path itself, which is why this went unnoticed. Look cairo up with pkg-config and carry it on the targets: privately for plutovg's own sources, publicly for lunasvg, whose header hands out cairo_surface_t. The meson build is left alone. Its plutovg wrap still points at upstream sammycage/plutovg, which predates the cairo port, so cairo is not what stands between it and building this fork. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
loadImageResource() passed whatever createSurfaceForImage() returned straight to cairo_surface_set_user_data(). The callback has no way to report a format it cannot decode other than returning null, which is routine for an arbitrary document, so a `<image>` in an unsupported or corrupt format segfaulted the caller. Return a null Bitmap in that case, and in the case of a surface that came back in an error state, freeing the decoded data the surface never took ownership of. SVGImageElement::render() already skips a null image. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Covers the three things loadImageResource() can get back for an `<image>` element: nothing, a surface in an error state, and a usable surface. The first case segfaults without the previous commit; the last one checks the image is still drawn and that the decoded bytes stay attached to the surface. Runs under `ctest`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
iurisilvio
force-pushed
the
fix-null-surface-from-decoder
branch
from
August 20, 2026 11:22
c8fbe87 to
b03b0dd
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Loading an SVG whose
<image>is in a format the embedder cannot decode segfaults.loadImageResource()passes whatevercreateSurfaceForImage()returns straight tocairo_surface_set_user_data(). The callback has no way to signal "I cannot decode this" other than returning null — and for an arbitrary document that is routine, not exceptional — so the null goes to cairo and gets dereferenced. The same happens when no decoder is registered at all, sinceGraphicsCallbacks::createSurfaceForImage()returns null in that case too.Where it came from
3fd8ba9 ("node-canvas patch"), the cairo port. Upstream has never had this shape: it returns the plutovg surface straight from
loadImageResource(), andBitmapis null-safe by construction —plutovg_surface_t* m_surface{nullptr}plus theisNull()thatSVGImageElement::render()already checks.Worth saying because "just sync with upstream" would not help: this fork branched at 2872aff and upstream has exactly one commit since, an unrelated two-line parser change. The null-safety upstream gets for free lives in the plutovg path the port replaced.
Changes
Bitmapwhen the callback yields no surface, freeing the decoded data the surface never took ownership of.SVGImageElement::render()already skips a null image.cairo_surface_set_user_data()refuses to store on an error surface, sodatawould leak there as well.test/decoderfn.cpp, covering all three things the callback can hand back — nothing, an error surface, and a usable surface. The last case also checks the image is still drawn and that the decoded bytes stay attached to the surface, so the fix cannot quietly turn every embedded image into a no-op. Runs underctest.That last one is the odd commit out, so:
plutovg.handlunasvg.hhave both included<cairo.h>since the port to cairo, but the build never looked for it — node-canvas's zig build supplies the include path, socmake .here fails on the first file that pulls either header in. I left the meson build alone; its plutovg wrap still points at upstream sammycage/plutovg, which predates the cairo port, so cairo is not what stands between it and building this fork. I also did not touch.github/workflows/main.yml, since making its three OSes carry cairo is a bigger decision than this PR should make.Verification
macOS arm64, cairo 1.18.4 from homebrew:
With
source/svgelement.cppreverted to master, the same test binary:Reproduced first through node-canvas, which uses this fork and registers a decoder that returns null for anything it cannot sniff as PNG, JPEG, GIF or BMP — Automattic/node-canvas#2613 carries the same case as a JS-level test, and its full suite (308 tests) passes against this branch on Linux, macOS and Windows.