Edit Colors: convert embedded images to indexed color

Edit > Edit Colors > Convert to Indexed Color... (edit.colors.toIndexed)
stores the embedded images in the selection as indexed-color PNGs: a
palette of at most 256 RGB colors, each pixel the index of one. The
dialog previews live and OK is one undo step.

- The palette is chosen as for a PNG-8 export (selective, perceptual,
  adaptive, web, gray, black and white), as evenly spaced levels of red,
  green and blue (uniform), or from the document's swatches. Dithering
  is diffusion, pattern, noise or none, with an amount.
- Pixels under half opacity become one transparent entry; the others
  become opaque in their own color. Linked images, colorized images and
  color glyph images keep their pixels.
- An image that is already an indexed PNG and would show the same pixels
  is left as it is, and no undo step is recorded when no image changes.
- An image of more than 134,217,728 pixels (33,554,432 in the web app)
  is refused, read from its header before decoding.
- With Select All, locked and hidden objects that hold an image that is
  not an indexed PNG yet are counted in notChanged, without converting
  them.
- The quantizer gains quantize_to, which maps pixels to a given palette.

Docs: docs/mcp.md, docs/target-app-parity.md.
This commit is contained in:
dotCooCoo
2026-11-13 09:49:00 -08:00
parent f520e6f8c5
commit 81d19f1718
44 changed files with 770 additions and 23 deletions
+21
View File
@@ -194,6 +194,27 @@ impl ImageBlob {
.collect();
Some((w, h, cover))
}
/// The pixels as straight RGBA, row by row from the top, with the width and the height;
/// `None` when the image can't be decoded within [`MAX_RASTER_ALLOC`].
pub fn rgba(&self) -> Option<(u32, u32, Vec<u8>)> {
let img = rgba_within(&self.bytes, MAX_RASTER_ALLOC)?;
let (w, h) = img.dimensions();
Some((w, h, img.into_raw()))
}
/// The width and height in pixels the image's file says it has; `None` for data that isn't an
/// image the decoder reads. Reads only its header.
pub fn pixel_size(&self) -> Option<(u32, u32)> {
image::ImageReader::new(Cursor::new(self.bytes.as_slice())).with_guessed_format().ok()?.into_dimensions().ok()
}
/// Whether the image's file is a PNG that stores palette indices (an indexed-color image).
/// Reads only its header.
pub fn indexed_png(&self) -> bool {
// The signature, then the IHDR chunk: length, type, width, height, bit depth, color type.
self.bytes.starts_with(b"\x89PNG\r\n\x1a\n") && self.bytes.get(12..16) == Some(b"IHDR".as_slice()) && self.bytes.get(25) == Some(&3)
}
}
impl ImageBlob {