Merge PR #8776 into integration batch

This commit is contained in:
Federico Olivares
2026-10-24 00:51:23 -03:00
2 changed files with 65 additions and 5 deletions
+29 -5
View File
@@ -668,6 +668,9 @@ pub(crate) fn export_flat_with(doc: &Document, format: Format, opts: &ExportOpti
if format == Format::Tga {
img = tga_image(doc, img, opts.tga_bits, &mut warnings)?;
}
if format == Format::Dds {
img = dds_image(doc, img, &mut warnings)?;
}
encode_image(&img, format, opts, warnings)
}
@@ -706,7 +709,7 @@ fn tga_image(doc: &Document, img: Image, bits: Option<TgaBits>, warnings: &mut V
warnings.push("transparency composited over white; a 24-bit Targa has no alpha".into());
matte_over_white(&img)
}
(TgaBits::Bits32, [channel]) => tga_with_alpha_channel(doc, img, channel, warnings),
(TgaBits::Bits32, [channel]) => with_alpha_channel(doc, img, channel, "Targa", warnings),
(TgaBits::Bits32, alphas) => {
if !alphas.is_empty() {
warnings.push("Targa holds one alpha channel; with several, none was written (as in Photoshop)".into());
@@ -719,11 +722,32 @@ fn tga_image(doc: &Document, img: Image, bits: Option<TgaBits>, warnings: &mut V
}
}
/// A 32-bit Targa's image: the colours (composited over white where the layers are transparent)
/// and `channel`'s values as the alpha.
fn tga_with_alpha_channel(doc: &Document, img: Image, channel: &AlphaChannel, warnings: &mut Vec<String>) -> Result<Image, IoError> {
/// The image a DDS texture holds (#8563): as with a 32-bit Targa, the document's alpha channel
/// becomes the texture's alpha when it has exactly one (spot channels don't count), the colours
/// composited over white where the layers are transparent; BC1 keeps it as on/off, BC2 and BC3
/// in full. With none, the layers' transparency is the alpha as before; with several, none of
/// them is written. Grayscale textures keep their transparency.
fn dds_image(doc: &Document, img: Image, warnings: &mut Vec<String>) -> Result<Image, IoError> {
let alphas: Vec<&AlphaChannel> = doc.channels.iter().filter(|c| c.spot.is_none()).collect();
match alphas.as_slice() {
[] => Ok(img),
[channel] if img.layout().is_rgb() => with_alpha_channel(doc, img, channel, "texture", warnings),
[_] => {
warnings.push("a grayscale DDS keeps the layers' transparency; the alpha channel was not saved".into());
Ok(img)
}
_ => {
warnings.push("a DDS texture holds one alpha channel; with several, none was written".into());
Ok(img)
}
}
}
/// A 32-bit Targa's or a texture's image: the colours (composited over white where the layers are
/// transparent) and `channel`'s values as the alpha. `what` names the file in the warning.
fn with_alpha_channel(doc: &Document, img: Image, channel: &AlphaChannel, what: &str, warnings: &mut Vec<String>) -> Result<Image, IoError> {
let img = if img.layout().has_alpha() {
warnings.push("transparency composited over white; the alpha channel is the Targa's alpha".into());
warnings.push(format!("transparency composited over white; the alpha channel is the {what}'s alpha"));
matte_over_white(&img)?
} else {
img
+36
View File
@@ -1190,3 +1190,39 @@ fn a_stereo_png_keeps_its_stereo_mark_on_save() {
let plain = export(&single(ColorMode::Rgb, SampleType::U8, false), "png", &ExportOptions::default()).unwrap();
assert_eq!(photocraft_codecs::png_stereo(&plain.bytes), None);
}
/// #8563: a DDS export writes the document's alpha channel as the texture's alpha, as a 32-bit
/// Targa does: exactly in an uncompressed texture, to 4 bits in BC2, interpolated in BC3, and on
/// or off in BC1. With several alpha channels none is written, with a warning.
#[test]
fn dds_writes_the_alpha_channel_as_the_texture_alpha() {
use photocraft_codecs::DdsCompression;
let d = with_alpha_channel(single(ColorMode::Rgb, SampleType::U8, false), "Alpha 1");
for compression in DdsCompression::ALL {
let mut opts = ExportOptions::default();
opts.encode.dds.compression = compression;
let r = export(&d, "a.dds", &opts).unwrap();
let img = photocraft_codecs::decode(&r.bytes).expect("decode dds");
assert_eq!(img.layout(), photocraft_codecs::ChannelLayout::Rgba, "{compression:?}");
let (w, _) = img.dimensions();
let tolerance = match compression {
DdsCompression::None => 1.0,
DdsCompression::Bc2 => 9.0,
_ => 18.0,
};
for (i, px) in img.data().as_chunks::<4>().0.iter().enumerate() {
let x = i as u32 % w;
let want = alpha_value(x) * 255.0;
if compression == DdsCompression::Bc1 && x == 4 {
continue; // half opacity sits on BC1's on/off threshold
}
assert!((f32::from(px[3]) - want).abs() <= tolerance, "{compression:?} pixel {i}: alpha {} want {want}", px[3]);
}
}
// Two alpha channels: Photoshop's rule for Targa, none of them is written.
let two = with_alpha_channel(d.clone(), "Alpha 2");
let r = export(&two, "a.dds", &ExportOptions::default()).unwrap();
assert!(r.warnings.iter().any(|w| w.contains("several")), "{:?}", r.warnings);
let img = photocraft_codecs::decode(&r.bytes).expect("decode dds");
assert!(!img.layout().has_alpha() || img.data().as_chunks::<4>().0.iter().all(|p| p[3] == 255));
}