diff --git a/src/codecs/ico/decoder.rs b/src/codecs/ico/decoder.rs index a738632d82..cc38d6f187 100644 --- a/src/codecs/ico/decoder.rs +++ b/src/codecs/ico/decoder.rs @@ -371,6 +371,22 @@ impl ImageDecoder for IcoDecoder { decoder.read_image_data(buf)?; + // Detect 0RGB. + // 0RGB is a 32bpp RGB format where the fourth channel is set to 0 and ignored. + // This channel is NOT alpha/transparency, but our BMP decoder doesn't know that + // and decodes it as RGBA. So we detect this and manually set alpha to 255. + // See: https://devblogs.microsoft.com/oldnewthing/20101019-00/?p=12503 + let rgba = buf.as_chunks_mut::<4>().0; + let is_0rgb = self.selected_entry.bits_per_pixel == 32 + && rgba.iter().all(|rgba| rgba[3] == 0); + if is_0rgb { + rgba.iter_mut().for_each(|rgba| rgba[3] = 255); + } + + // The AND mask is applied when the image is <32bpp or in 0RGB format. + // See: https://devblogs.microsoft.com/oldnewthing/20101021-00/?p=12483 + let should_apply_and_mask = is_0rgb || self.selected_entry.bits_per_pixel < 32; + let r = decoder.reader(); let image_end = r.stream_position()?; let data_end = self.reader_offset @@ -392,11 +408,7 @@ impl ImageDecoder for IcoDecoder { return Ok(DecodedImageAttributes::default()); } - // 32bpp BMPs already have a native alpha channel, so the - // AND mask is ignored. - // For lower bit depths, read and apply the AND mask. - if self.selected_entry.bits_per_pixel < 32 { - let rgba = buf.as_chunks_mut::<4>().0; + if should_apply_and_mask { let rows = rgba.chunks_exact_mut(width as usize); if rows.len() != height as usize { @@ -431,8 +443,7 @@ impl ImageDecoder for IcoDecoder { } else if data_end == image_end { // accept images with no mask data Ok(DecodedImageAttributes::default()) - } else if self.spec_strictness == SpecCompliance::Lenient - && self.selected_entry.bits_per_pixel >= 32 + } else if self.spec_strictness == SpecCompliance::Lenient && !should_apply_and_mask { // In lenient mode, we accept truncated mask data for 32bpp images // since they already have an alpha channel and we ignore the AND mask anyway. diff --git a/tests/images/ico/images/bmp_0rgb_with_and_mask.ico b/tests/images/ico/images/bmp_0rgb_with_and_mask.ico new file mode 100644 index 0000000000..c5bbdda65c Binary files /dev/null and b/tests/images/ico/images/bmp_0rgb_with_and_mask.ico differ diff --git a/tests/reference/ico/images/bmp-32bpp-conflicting-and-mask.ico.png b/tests/reference/ico/images/bmp-32bpp-conflicting-and-mask.ico.png new file mode 100644 index 0000000000..2b93672d51 Binary files /dev/null and b/tests/reference/ico/images/bmp-32bpp-conflicting-and-mask.ico.png differ diff --git a/tests/reference/ico/images/bmp_0rgb_with_and_mask.ico.png b/tests/reference/ico/images/bmp_0rgb_with_and_mask.ico.png new file mode 100644 index 0000000000..9488bf6058 Binary files /dev/null and b/tests/reference/ico/images/bmp_0rgb_with_and_mask.ico.png differ