diff --git a/crates/engine/src/cmd/file.rs b/crates/engine/src/cmd/file.rs index 26418f238..f9453cf51 100644 --- a/crates/engine/src/cmd/file.rs +++ b/crates/engine/src/cmd/file.rs @@ -411,7 +411,19 @@ pub fn export_file(doc: &Document, ids: &[NodeId], setting: &ExportSetting, scal // The render is in the document's profile; the setting's profile converts the pixels // (out-of-gamut colors clip) and tags the file. let profile = setting.color_profile.resolve(doc.color_profile); - doc.color_profile.conversion_to(profile).apply_pixels(pix.data_mut()); + let conversion = doc.color_profile.conversion_to(profile); + // JPG has no alpha: the render is flattened over white in the document's profile + // before the conversion, so translucent pixels match what the document shows. + if setting.format == ExportFormat::Jpg && !conversion.is_identity() { + for [r, g, b, a] in pix.data_mut().as_chunks_mut::<4>().0.iter_mut() { + let under = 255 - *a; + *r = r.saturating_add(under); + *g = g.saturating_add(under); + *b = b.saturating_add(under); + *a = 255; + } + } + conversion.apply_pixels(pix.data_mut()); Ok((figcraft_render::encode_export(&pix, setting.format, profile).map_err(EngineError::Other)?, Vec::new())) } } diff --git a/crates/engine/src/export_profile_tests.rs b/crates/engine/src/export_profile_tests.rs index 5b12b2a03..016aa7dae 100644 --- a/crates/engine/src/export_profile_tests.rs +++ b/crates/engine/src/export_profile_tests.rs @@ -115,6 +115,19 @@ fn jpg_and_webp_follow_the_setting() { } } +#[test] +fn a_translucent_jpg_is_flattened_before_its_conversion() { + let (mut s, r) = session(); + s.execute("fill.set", &json!({"ids": [r], "opacity": 0.5})).unwrap(); + // The sRGB document shows 50% red over white; the Display P3 JPG holds that color converted. + let flat = export(&mut s, json!({"ids": [r], "format": "jpg", "colorProfile": "srgb"})); + let [fr, fg, fb] = middle(&flat); + let shown = Color::new(f32::from(fr) / 255.0, f32::from(fg) / 255.0, f32::from(fb) / 255.0, 1.0); + let want = rgb8(ColorProfile::Srgb.conversion_to(ColorProfile::DisplayP3).apply(shown)); + let p3 = export(&mut s, json!({"ids": [r], "format": "jpg", "colorProfile": "displayP3"})); + assert!(close(middle(&p3), want, 2), "{:?} vs {want:?} (sRGB {:?})", middle(&p3), [fr, fg, fb]); +} + #[test] fn svg_pdf_and_xml_stay_srgb() { let (mut s, r) = session();