mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-29 12:17:14 -06:00
Fix #787: raster exports keep translucent edges over opaque art opaque
The renderer composited in 8-bit integers, so where a partly transparent anti-aliased edge crossed opaque art (or a background colour) the result came out at alpha 254 on some pixels, and PNG, TIFF and WebP files were written with an alpha channel. Raster exports (and Save for Web) now composite in floating point (vello_cpu's f32 pipeline, enabled as a feature), offscreen groups, masks and patterns included; the canvas keeps the faster 8-bit path. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
1db82cbc02
commit
4099ebf3c9
@@ -87,7 +87,7 @@ impl RasterExportOptions {
|
||||
}
|
||||
|
||||
/// What the renderer draws for `format`: template layers and guides left out, over the background
|
||||
/// (white for a JPEG or a flat PSD without one).
|
||||
/// (white for a JPEG or a flat PSD without one), composited in floating point.
|
||||
pub fn render_options(&self, format: RasterFormat) -> RenderOptions {
|
||||
let flat = format == RasterFormat::Jpeg || (format == RasterFormat::Psd && !self.psd.layers);
|
||||
let background = self.background.or(flat.then_some([255; 3]));
|
||||
@@ -95,6 +95,8 @@ impl RasterExportOptions {
|
||||
background: background.map(|[r, g, b]| [r, g, b, 255]),
|
||||
skip_templates: true,
|
||||
anti_alias: self.anti_alias,
|
||||
// Opaque art stays exactly opaque where translucent edges cross it (#787).
|
||||
precise: true,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,6 +110,44 @@ fn backgrounds_are_opaque_or_transparent() {
|
||||
assert_eq!(webp.get_pixel(0, 0).0, [0, 0, 0, 255]);
|
||||
}
|
||||
|
||||
/// #787: translucent art over opaque art (or a background) stays exactly opaque, its anti-aliased
|
||||
/// edges too, so the file is written as RGB.
|
||||
#[test]
|
||||
fn translucent_edges_over_opaque_art_stay_opaque() {
|
||||
let mut d = Document::new(100.0, 100.0);
|
||||
let l = d.layers[0].id;
|
||||
let under = Node::path(
|
||||
d.alloc_id(),
|
||||
shapes::rectangle(Rect::new(0.0, 0.0, 100.0, 100.0)),
|
||||
Appearance::basic(Paint::solid(Color::BLACK), Paint::None, 0.0),
|
||||
);
|
||||
let mut over =
|
||||
Node::path(d.alloc_id(), shapes::ellipse(Rect::new(10.0, 10.0, 90.0, 90.0)), Appearance::basic(Paint::solid(Color::WHITE), Paint::None, 0.0));
|
||||
over.opacity = 0.5;
|
||||
let only_over = {
|
||||
let mut d = d.clone();
|
||||
d.insert(Some(l), 0, over.clone()).unwrap();
|
||||
d
|
||||
};
|
||||
d.insert(Some(l), 0, under).unwrap();
|
||||
d.insert(Some(l), 1, over).unwrap();
|
||||
// Over a black background at 72 ppi, and over an opaque rectangle at a scale that puts the
|
||||
// edges on fractions of a pixel.
|
||||
for (doc, o) in [
|
||||
(&only_over, RasterExportOptions { background: Some([0, 0, 0]), ..Default::default() }),
|
||||
(&d, RasterExportOptions { ppi: 72.0 * 0.3515625, ..Default::default() }),
|
||||
] {
|
||||
let png = Renderer::new().export_region(doc, doc.artboards[0].rect, RasterFormat::Png, &o).unwrap();
|
||||
let img = image::load_from_memory(&png).unwrap().to_rgba8();
|
||||
let below: Vec<_> = img.pixels().filter(|p| p[3] < 255).collect();
|
||||
assert!(below.is_empty(), "ppi {}: {} pixels below alpha 255, e.g. {:?}", o.ppi, below.len(), below.first());
|
||||
assert_eq!(png[25], 2, "IHDR colour type: RGB");
|
||||
}
|
||||
// The inside is half white over black.
|
||||
let img = export(&only_over, RasterFormat::Png, &RasterExportOptions { background: Some([0, 0, 0]), ..Default::default() });
|
||||
assert!(img.get_pixel(50, 50).0[..3].iter().all(|c| (127..=128).contains(c)), "{:?}", img.get_pixel(50, 50));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn interlaced_export_decodes_to_the_same_pixels() {
|
||||
let d = circle_doc();
|
||||
|
||||
Reference in New Issue
Block a user