SVG support in native:image - #44
Open
unlocdavid wants to merge 4 commits into
Open
Conversation
coil-svg's SvgDecoder self-registers via Coil's ServiceLoader, so adding the dependency is the whole change — ImageRenderer needs no edit. SVG sources then flow through the same AsyncImage path as raster: fit modes, tint, corner clipping and the failure paths are unchanged. Selection is content-based (`<` at byte 0, `<svg` within 1024 bytes), so SVG is accepted from any source Coil already fetches, including base64 data URIs. Percent-encoded data URIs are not supported — Coil's DataUriFetcher requires the `;base64,` marker. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Tint, fitted-corner clipping, alt/accessibility and the fit-ordinal mapping all operate on `some View` rather than on `Image`, so they move out of NativeUIImageRenderer as file-scope ViewModifiers. No behaviour change — the same modifiers apply in the same order. This lets a second renderer reuse them without inheriting the Image-specific resize step, which is the one part that cannot be shared: `Image.resizable()` and a vector view's `resizable()` are unrelated methods on unrelated types. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
SVG sources take a SwiftDraw SVGView branch inside NativeUIImageRenderer rather than a separate element. Vectors are never rasterised — SVGView draws through a Canvas, so an 8pt viewBox at 96pt is exact instead of an upscaled bitmap, and no size cache or GeometryReader is needed. The branch reuses the tint and fitted-corner modifiers, so fit modes, tinting, corner clipping, alt text and the empty-on-failure behaviour match the raster path. object-none is expressed the same way too, by leaving the view unresized. Sources are recognised by prefix for inline markup and data URIs, and by extension for files and remote URLs — neither of the latter can be sniffed without reading them first. iOS therefore accepts percent-encoded data URIs and inline markup, which Coil on Android does not; Android in turn accepts SVG from any source it fetches regardless of extension. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The tint reached foregroundStyle, which only recolours a template image. A UIImage decoded from a file or a URL is `.original`, so the modifier had nothing to act on and tint_color was dropped without a warning. Android applies ColorFilter.tint regardless, so the same markup tinted on one platform and not the other. Switching the source to `.renderingMode(.template)` when a tint is set makes foregroundStyle effective. Untinted images keep their original rendering mode and are unaffected. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Member
|
This is great 👍🏼 Honestly, I love that this could be a core feature of this package, but because of the third-party dependencies I think it probably ought to be its own plugin That way, the developer opts into them because they know they need them rather than being encumbered with them regardless In any case, it's probably better as its own |
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.
Summary
<native:image>now draws SVG as well as raster. No new element, no new attribute — pointsrcat SVG content and it renders as a vector.Why
SVG is the format the assets an app cares most about already ship in.
tintColorrecolours it per theme instead of shipping a light and a dark copy.endroid/qr-code, which core already depends on — and they are read by a camera, so edge fidelity is the whole point. A bitmap rasterized at the wrong size blurs the quiet zone and the finder patterns, and scanning gets slower or fails outright. Drawn as a vector the code is exact at whatever size the screen gives it.Without this the workarounds are pre-rendering PNGs per density, or dropping to a web view to get an
<img>— which forfeits native layout and gestures for the sake of one graphic.Vectors are never rasterized. Android decodes through
coil-svg; iOS draws with SwiftDraw'sSVGView, a Canvas-backed view. Both stay sharp at any display size, so a smallviewBoxblown up is exact rather than an upscaled bitmap.fit,tintColor, corner radius andaltbehave exactly as they do for raster, because the SVG path reuses the same view modifiers.Screenshot of visual difference between SVG and small PNG icon:
Commits
6662a2ecoil-svgto the plugin manifest8d42c501a3d25790d2240tint_colorto raster imagesAndroid needs no renderer change at all.
coil-svgself-registers through Coil'sServiceLoader(SvgDecoderServiceLoaderTarget, enabled by default viaExtras.Key(default = true)and consumed atRealImageLoader.kt:269), so the whole Android side is one line innativephp.jsonandImageRenderer.ktis untouched.On the last commit
90d2240is not an SVG change, and it is here because it is the same one-line fix as the SVG path's.ImageTintModifierappliesforegroundStyle, which only recolours a template image. The SVG path opts in withSVGView.renderingMode(.template)when a tint is set, sotintColorworks on vectors. The raster path never opted in — aUIImagedecoded from a file or a URL is.original, so the shared tint modifier had nothing to act on and the tint was dropped silently. Android appliesColorFilter.tintregardless, so the same markup tinted on one platform and not the other.Shipping the SVG opt-in without the raster one would mean leaving two callers of the same modifier deliberately inconsistent. Untinted images are unaffected. If you would rather see it as its own PR, say so and I will lift it out.
Recognised sources
src.svgpath, orfile://…/x.svghttps://…/x.svgdata:image/svg+xml;base64,…data:image/svg+xml,%3Csvg…(percent-encoded)<svg …>markup passed assrcAndroid selects by content: Coil sniffs the bytes it fetched, so an SVG served without a
.svgextension renders anyway. iOS selects by prefix for markup and data URIs, and by extension for files and URLs, since neither can be inspected before it is read. On iOS oneSVGSourceclassification drives both the detection and the loading, so every source routed to the SVG branch is one the loader knows how to fetch.Both Android gaps are Coil's. Its
DataUriFetcherrequires the;base64,marker and returns nothing without it, and a bare markup string is not a source it fetches. Base64-encoded SVG data URIs work on both platforms.Platform notes
<text>renders on both. Font selection is the host's, so a family the device lacks is substituted rather than dropped. Convert text to outlines when the exact face matters.object-noneis honoured on Android and not on iOS. Android maps it toContentScale.None; iOS falls throughresolveContentMode's default and letterboxes, because SwiftUI'sContentModehas no member for "do not scale". This is existing iOS behaviour for raster images and the SVG path matches it rather than diverging. Fixing it is a separate PR — the expression is to omit.resizable(), and the alignment and density questions that come with it have nothing to do with SVG.object-containletterboxes to the leading edge on iOS and centres on Android. This predates SVG, applies to raster identically, and comes from the shared layout rather than the image renderer — so it is out of scope here and worth its own issue.Dependencies
io.coil-kt.coil3:coil-svg:3.1.0inandroid.dependencies.implementationswhitty/SwiftDraw0.29.0inios.dependencies.swift_packages— the first use of that manifest slot in this pluginBoth link unconditionally, so apps that never render an SVG still carry them. Worth a maintainer's opinion on whether either should be opt-in; there is no mechanism for optional plugin dependencies today.
Verification
Device-tested on both platforms against one screen that renders every SVG case beside an identical raster control, so a difference between columns is a pipeline bug and a difference between platforms is a decoder bug.
Covered: fit modes, tint, corner radius, gradient fills,
clip-pathintodefs,a 7.3:1 aspect ratio, missing
viewBox, an 8pxviewBoxupscaled to 96pt,<text>, local files, remote URLs, all three data-URI forms, malformed SVG, andmissing files.
Both renderers behave identically to their raster control in every case except the platform notes above.
The screen is not part of this PR, but it is attached as a zip so it can be dropped into any NativePHP app to reproduce the results — component, Blade view, fixtures in their directory structure, and an INSTALL.md with the route line and
the expected per-platform differences.
svg-test-screen.zip