From dbb8afe6764f07bf62272e38d2a7ba5b27e1946e Mon Sep 17 00:00:00 2001
From: hnakashima <69759577+nakashima-hikaru@users.noreply.github.com>
Date: Sun, 29 Mar 2026 23:43:05 +0900
Subject: [PATCH] gpui: Fix BGRA conversion for SVG rendering (#52641)
### Description
Fixes swapped red/blue channels when rendering SVG images.
#### Describe the bug
When rendering a full-color SVG into an Image object using
Image::from_bytes(ImageFormat::Svg, ...) on macOS, the resulting bitmap
has its Red and Blue channels swapped. For example, a color specified as
#38BDF8 (Light Blue) in the SVG source appears as yellowish in the
rendered GPUI view.
#### Steps to reproduce
1. Create a GPUI application.
1. Generate or load an SVG string containing a specific color, for
example:
```xml
```
3. Load this SVG into an Image object:
```rust
let image = Arc::new(Image::from_bytes(
ImageFormat::Svg,
svg_string.into_bytes(),
));
```
4. Display this image in a view using an img() element.
#### Expected behavior
The rectangle should be rendered in **Light Blue (#38BDF8)**.
#### Actual behavior
The rectangle is rendered in **Yellowish Color (#F8BD38)**.
### Self-Review Checklist:
- [X] I've reviewed my own diff for quality, security, and reliability
- [X] Unsafe blocks (if any) have justifying comments
- [X] The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
- [X] Tests cover the new/changed behavior
- [X] Performance impact has been considered and is acceptable
### Closes #ISSUE
### Release Notes:
- Fixed swapped color channels when pasting SVG images from the
clipboard.
---------
Co-authored-by: MrSubidubi
---
crates/gpui/src/elements/img.rs | 2 +-
crates/gpui/src/platform.rs | 26 +++++++++++++++++++++-
crates/gpui/src/svg_renderer.rs | 7 ++----
crates/markdown/src/mermaid.rs | 3 +--
crates/svg_preview/src/svg_preview_view.rs | 2 +-
5 files changed, 30 insertions(+), 10 deletions(-)
diff --git a/crates/gpui/src/elements/img.rs b/crates/gpui/src/elements/img.rs
index 875f9e6dc1cc7d248f9e70488e52480dcca53fa3..ccf10d038c271ac54a0060b4c17c9de86ce9eb5c 100644
--- a/crates/gpui/src/elements/img.rs
+++ b/crates/gpui/src/elements/img.rs
@@ -697,7 +697,7 @@ impl Asset for ImageAssetLoader {
Ok(Arc::new(RenderImage::new(data)))
} else {
svg_renderer
- .render_single_frame(&bytes, 1.0, true)
+ .render_single_frame(&bytes, 1.0)
.map_err(Into::into)
}
}
diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs
index 9d672ce34df061b11dce3437101afc55d2b086c7..806a34040a4ec685c3d5c6ec01f47b5026e349a6 100644
--- a/crates/gpui/src/platform.rs
+++ b/crates/gpui/src/platform.rs
@@ -2108,7 +2108,7 @@ impl Image {
ImageFormat::Ico => frames_for_image(&self.bytes, image::ImageFormat::Ico)?,
ImageFormat::Svg => {
return svg_renderer
- .render_single_frame(&self.bytes, 1.0, false)
+ .render_single_frame(&self.bytes, 1.0)
.map_err(Into::into);
}
};
@@ -2190,6 +2190,30 @@ impl From for ClipboardString {
}
}
+#[cfg(test)]
+mod image_tests {
+ use super::*;
+ use std::sync::Arc;
+
+ #[test]
+ fn test_svg_image_to_image_data_converts_to_bgra() {
+ let image = Image::from_bytes(
+ ImageFormat::Svg,
+ br##""##
+ .to_vec(),
+ );
+
+ let render_image = image.to_image_data(SvgRenderer::new(Arc::new(()))).unwrap();
+ let bytes = render_image.as_bytes(0).unwrap();
+
+ for pixel in bytes.chunks_exact(4) {
+ assert_eq!(pixel, &[0xF8, 0xBD, 0x38, 0xFF]);
+ }
+ }
+}
+
#[cfg(all(test, any(target_os = "linux", target_os = "freebsd")))]
mod tests {
use super::*;
diff --git a/crates/gpui/src/svg_renderer.rs b/crates/gpui/src/svg_renderer.rs
index 217555e3b0e295d06e375e19d013e0b520118e0b..8653ab9b162031772ab29367b60ff988e33cd823 100644
--- a/crates/gpui/src/svg_renderer.rs
+++ b/crates/gpui/src/svg_renderer.rs
@@ -150,7 +150,6 @@ impl SvgRenderer {
&self,
bytes: &[u8],
scale_factor: f32,
- to_brga: bool,
) -> Result, usvg::Error> {
self.render_pixmap(
bytes,
@@ -161,10 +160,8 @@ impl SvgRenderer {
image::ImageBuffer::from_raw(pixmap.width(), pixmap.height(), pixmap.take())
.unwrap();
- if to_brga {
- for pixel in buffer.chunks_exact_mut(4) {
- swap_rgba_pa_to_bgra(pixel);
- }
+ for pixel in buffer.chunks_exact_mut(4) {
+ swap_rgba_pa_to_bgra(pixel);
}
let mut image = RenderImage::new(SmallVec::from_const([Frame::new(buffer)]));
diff --git a/crates/markdown/src/mermaid.rs b/crates/markdown/src/mermaid.rs
index 560a67787ff897c6f792c97fafdd9ed617c020e6..15f3de4d8e8c64010fe96846b05d75f012c5fc0d 100644
--- a/crates/markdown/src/mermaid.rs
+++ b/crates/markdown/src/mermaid.rs
@@ -106,7 +106,7 @@ impl CachedMermaidDiagram {
let svg_string = mermaid_rs_renderer::render(&contents.contents)?;
let scale = contents.scale as f32 / 100.0;
svg_renderer
- .render_single_frame(svg_string.as_bytes(), scale, true)
+ .render_single_frame(svg_string.as_bytes(), scale)
.map_err(|error| anyhow::anyhow!("{error}"))
})
.await;
@@ -325,7 +325,6 @@ mod tests {
.render_single_frame(
br#""#,
1.0,
- true,
)
.unwrap()
})
diff --git a/crates/svg_preview/src/svg_preview_view.rs b/crates/svg_preview/src/svg_preview_view.rs
index 1a001c6e18854428636626cc499e49433710a84d..259243b8ac7cd7d4122fc2f535d490b359442440 100644
--- a/crates/svg_preview/src/svg_preview_view.rs
+++ b/crates/svg_preview/src/svg_preview_view.rs
@@ -110,7 +110,7 @@ impl SvgPreviewView {
let renderer = cx.svg_renderer();
let content = buffer.read(cx).snapshot();
let background_task = cx.background_spawn(async move {
- renderer.render_single_frame(content.text().as_bytes(), SCALE_FACTOR, true)
+ renderer.render_single_frame(content.text().as_bytes(), SCALE_FACTOR)
});
self._refresh = cx.spawn_in(window, async move |this, cx| {