From 27a0d2cbb23716a2f1998016883775d769bcf3b8 Mon Sep 17 00:00:00 2001 From: RunDevelopment Date: Wed, 3 Jun 2026 17:35:41 +0200 Subject: [PATCH 1/2] ICO: Handle 0RGB format --- src/codecs/ico/decoder.rs | 22 ++++++++++++++---- .../ico/images/bmp_0rgb_with_and_mask.ico | Bin 0 -> 1150 bytes .../bmp-32bpp-conflicting-and-mask.ico.png | Bin 0 -> 93 bytes .../ico/images/bmp_0rgb_with_and_mask.ico.png | Bin 0 -> 180 bytes 4 files changed, 17 insertions(+), 5 deletions(-) create mode 100644 tests/images/ico/images/bmp_0rgb_with_and_mask.ico create mode 100644 tests/reference/ico/images/bmp-32bpp-conflicting-and-mask.ico.png create mode 100644 tests/reference/ico/images/bmp_0rgb_with_and_mask.ico.png diff --git a/src/codecs/ico/decoder.rs b/src/codecs/ico/decoder.rs index 297b8db28e..825230c47d 100644 --- a/src/codecs/ico/decoder.rs +++ b/src/codecs/ico/decoder.rs @@ -375,11 +375,23 @@ 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; + // 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 bpp = self.selected_entry.bits_per_pixel; + let rgba = buf.as_chunks_mut::<4>().0; + let is_0rgb = bpp == 32 && rgba.iter().all(|rgba| rgba[3] == 0); + if is_0rgb { + for rgba in rgba.iter_mut() { + 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 + if bpp < 32 || is_0rgb { let rows = rgba.chunks_exact_mut(width as usize); if rows.len() != height as usize { 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 0000000000000000000000000000000000000000..c5bbdda65cef6514b0f3c236d8b302dd8e5f70df GIT binary patch literal 1150 zcmeH_F%H5o3`HLj2gt+u?H$LPq=p+%|`z7<(Sk;4LGQ@Ku_|8FD9aL|(y zdSAF5;RFl{A(6tB7*%Um&ROUac6Qe8Bi%P`Ck`_3%){?p^`sp}D|>-9d; sI`eFL_ZaK(R_EtEm;UX2_@xh;I*ncw+ZEO`%nz6*crHcvx`Tyz-nWOl=Kufz literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..2b93672d510b44b879daac4a88feadddedb1ffd2 GIT binary patch literal 93 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`DxNNmAr-gYo-q_;VBk2iL9V@E qPIE`RgcM`u4aI;K9v+^3EbIxrjEnZ`pZE+^&*16m=d#Wzp$P!zxfv+{ literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..9488bf6058b8bddaa5e98ddfd73160c22f95a054 GIT binary patch literal 180 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`)t)YnAr-fl1~c*VhrX>v+{gWcK!2h#+8+S@+OF!%uG@^|0c$7(KpTe_o|bH*vl0+ f3CL|&Tg|xQjkfN&Ct?{uXE1oW`njxgN@xNAaxO-A literal 0 HcmV?d00001 From 174552a2836e24702b13defdcd63f0fca77b0b96 Mon Sep 17 00:00:00 2001 From: RunDevelopment Date: Sat, 13 Jun 2026 11:40:26 +0200 Subject: [PATCH 2/2] Detect 0RGB unconditionally --- src/codecs/ico/decoder.rs | 37 ++++++++++++++++++------------------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/src/codecs/ico/decoder.rs b/src/codecs/ico/decoder.rs index 021e007057..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,23 +408,7 @@ impl ImageDecoder for IcoDecoder { return Ok(DecodedImageAttributes::default()); } - // 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 bpp = self.selected_entry.bits_per_pixel; - let rgba = buf.as_chunks_mut::<4>().0; - let is_0rgb = bpp == 32 && rgba.iter().all(|rgba| rgba[3] == 0); - if is_0rgb { - for rgba in rgba.iter_mut() { - 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 - if bpp < 32 || is_0rgb { + if should_apply_and_mask { let rows = rgba.chunks_exact_mut(width as usize); if rows.len() != height as usize { @@ -443,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.