mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-14 05:24:44 +00:00
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:
@@ -154,18 +154,8 @@ pub fn quantize(rgba: &[u8], width: u32, height: u32, o: &PaletteOptions) -> Ind
|
||||
pub fn quantize_locked(rgba: &[u8], width: u32, height: u32, o: &PaletteOptions, locked: &[[u8; 3]]) -> Indexed {
|
||||
let px = rgba.as_chunks::<4>().0;
|
||||
let clear = |p: &[u8; 4]| o.transparency && p[3] < 128;
|
||||
let gray = matches!(o.reduction, Reduction::Gray | Reduction::BlackWhite);
|
||||
let matte = o.matte.unwrap_or([255; 3]);
|
||||
let blend = o.matte.is_some() || !o.transparency;
|
||||
// The opaque colours: blended over the matte, greys for the grey reductions.
|
||||
let colors: Vec<[u8; 3]> = px
|
||||
.iter()
|
||||
.map(|p| {
|
||||
let a = if blend { p[3] as u32 } else { 255 };
|
||||
let c = [0, 1, 2].map(|i| ((p[i] as u32 * a + matte[i] as u32 * (255 - a) + 127) / 255) as u8);
|
||||
if gray { [luma(c); 3] } else { c }
|
||||
})
|
||||
.collect();
|
||||
let colors = opaque_colors(px, o, matches!(o.reduction, Reduction::Gray | Reduction::BlackWhite));
|
||||
let has_clear = px.iter().any(clear);
|
||||
let k = (o.colors.clamp(2, 256) as usize - usize::from(has_clear)).max(1);
|
||||
let opaque = || colors.iter().zip(px).filter(|(_, p)| !clear(p)).map(|(c, _)| *c);
|
||||
@@ -195,11 +185,46 @@ pub fn quantize_locked(rgba: &[u8], width: u32, height: u32, o: &PaletteOptions,
|
||||
if palette.is_empty() {
|
||||
palette.push(matte);
|
||||
}
|
||||
finish(px, &colors, width, height, &palette, o, if exact { Dither::None } else { o.dither })
|
||||
}
|
||||
|
||||
/// [`quantize`] to the fixed `palette`: each pixel takes its nearest entry, with `o`'s dither,
|
||||
/// transparency and matte; `o.reduction` sets only the distance weights, and `o.colors` is not
|
||||
/// used. Only the first 255 colors of `palette` are used, leaving room for the transparent entry.
|
||||
/// Entries no pixel uses are left out.
|
||||
pub fn quantize_to(rgba: &[u8], width: u32, height: u32, o: &PaletteOptions, palette: &[[u8; 3]]) -> Indexed {
|
||||
let px = rgba.as_chunks::<4>().0;
|
||||
let colors = opaque_colors(px, o, false);
|
||||
let mut palette: Vec<[u8; 3]> = palette.iter().take(255).copied().collect();
|
||||
if palette.is_empty() {
|
||||
palette.push(o.matte.unwrap_or([255; 3]));
|
||||
}
|
||||
finish(px, &colors, width, height, &palette, o, o.dither)
|
||||
}
|
||||
|
||||
/// The color of each pixel when opaque: blended over the matte, and gray when `gray`.
|
||||
fn opaque_colors(px: &[[u8; 4]], o: &PaletteOptions, gray: bool) -> Vec<[u8; 3]> {
|
||||
let matte = o.matte.unwrap_or([255; 3]);
|
||||
let blend = o.matte.is_some() || !o.transparency;
|
||||
px.iter()
|
||||
.map(|p| {
|
||||
let a = if blend { p[3] as u32 } else { 255 };
|
||||
let c = [0, 1, 2].map(|i| ((p[i] as u32 * a + matte[i] as u32 * (255 - a) + 127) / 255) as u8);
|
||||
if gray { [luma(c); 3] } else { c }
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Map the pixels `px` (their opaque `colors`) to `palette` with `dither`, then drop the entries
|
||||
/// no pixel uses and put the transparent entry first when a pixel is transparent.
|
||||
fn finish(px: &[[u8; 4]], colors: &[[u8; 3]], width: u32, height: u32, palette: &[[u8; 3]], o: &PaletteOptions, dither: Dither) -> Indexed {
|
||||
let clear = |p: &[u8; 4]| o.transparency && p[3] < 128;
|
||||
let matte = o.matte.unwrap_or([255; 3]);
|
||||
let has_clear = px.iter().any(clear);
|
||||
let weights = o.reduction.weights();
|
||||
let dither = if exact { Dither::None } else { o.dither };
|
||||
// (A width past the pixels there are would size the error rows from a number nothing checked.)
|
||||
let mut indices =
|
||||
map(&colors, (width as usize).min(colors.len()), &palette, weights, dither, f32::from(o.dither_amount.min(100)) / 100.0, |i| clear(&px[i]));
|
||||
map(colors, (width as usize).min(colors.len()), palette, weights, dither, f32::from(o.dither_amount.min(100)) / 100.0, |i| clear(&px[i]));
|
||||
// Drop the entries no pixel uses (fixed palettes), keeping the order.
|
||||
let mut used = [false; 256];
|
||||
for (i, &ix) in indices.iter().enumerate() {
|
||||
|
||||
@@ -3,7 +3,7 @@ use vectorcraft_doc::{Appearance, Node};
|
||||
use vectorcraft_geom::shapes;
|
||||
|
||||
use super::png::{PngOptions, encode_indexed};
|
||||
use super::quantize::{Dither, Indexed, PaletteOptions, Reduction, quantize};
|
||||
use super::quantize::{Dither, Indexed, PaletteOptions, Reduction, quantize, quantize_to};
|
||||
use super::*;
|
||||
|
||||
/// A `w`×`h` opaque image of `f(x, y)` colours.
|
||||
@@ -93,6 +93,22 @@ fn fixed_palettes() {
|
||||
assert_eq!(bw.palette, [[0, 0, 0], [255, 255, 255]]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_given_palette_keeps_the_entries_pixels_use() {
|
||||
let px: Vec<u8> = [[250, 10, 10, 255], [10, 10, 240, 255], [0, 0, 0, 40], [240, 20, 30, 255]].concat();
|
||||
let given = [[0, 255, 0], [255, 0, 0], [0, 0, 255], [255, 255, 255]];
|
||||
let o = PaletteOptions { dither: Dither::None, matte: None, ..Default::default() };
|
||||
let ix = quantize_to(&px, 4, 1, &o, &given);
|
||||
assert_eq!(ix.transparent, Some(0));
|
||||
assert_eq!(ix.palette[1..], [[255, 0, 0], [0, 0, 255]], "unused entries left out, the order kept");
|
||||
assert_eq!(ix.indices, [1, 2, 0, 1]);
|
||||
// At most 255 of the given colors are used, leaving room for the transparent entry.
|
||||
let many: Vec<[u8; 3]> = (0..300u32).map(|i| [(i % 256) as u8, (i / 256) as u8, 0]).collect();
|
||||
let all = image(256, 2, |x, y| [x as u8, y as u8, 0]);
|
||||
let ix = quantize_to(&all, 256, 2, &o, &many);
|
||||
assert!(ix.palette.len() <= 255 && ix.palette.iter().all(|c| many[..255].contains(c)), "{}", ix.palette.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dithering_keeps_the_average_colour() {
|
||||
let px = image(64, 16, |x, _| [(x * 4) as u8; 3]);
|
||||
|
||||
Reference in New Issue
Block a user