mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-26 18:56:55 -06:00
M4.49: PNG-8 and GIF export with a palette quantizer and dithering
document.export {format: "png8" | "gif"} reduces the art to a palette of at most `colors`
(2–256) entries: median cut over a 15-bit histogram refined by k-means (perceptual,
selective or adaptive weighting), or the web-safe, black-and-white or grey palettes; art
with that many colours or fewer keeps them exactly. Dithering is Floyd–Steinberg diffusion,
an 8×8 ordered pattern or noise, with an amount. Pixels under half opacity become one
transparent entry; partly transparent edges blend over the matte. PNG-8 is an indexed PNG
(PLTE, tRNS, 1–8 bits per pixel, Adam7); GIF is a GIF89a (optionally interlaced). Both are in
document.formats, Export As (PNG-8 / GIF Options dialogs) and Export for Screens.
document.formats' writable list and the MCP export tool's format enum gain gif and png8
(their tests list them).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
ba4ed2ff93
commit
ac77b52ad3
@@ -10,6 +10,7 @@ use vectorcraft_doc::{Artboard, Document};
|
||||
use vectorcraft_geom::Rect;
|
||||
use vectorcraft_render::AntiAlias;
|
||||
use vectorcraft_render::encode::jpeg::{self, JpegOptions};
|
||||
use vectorcraft_render::encode::quantize::{Dither, PaletteOptions, Reduction};
|
||||
use vectorcraft_render::encode::{RasterExportOptions, RasterFormat};
|
||||
|
||||
use super::super::*;
|
||||
@@ -144,6 +145,12 @@ struct RasterOptions {
|
||||
scans: Option<u8>,
|
||||
embed_icc: Option<bool>,
|
||||
image_map: Option<String>,
|
||||
colors: Option<u16>,
|
||||
reduction: Option<String>,
|
||||
dither: Option<String>,
|
||||
dither_amount: Option<u8>,
|
||||
transparency: Option<bool>,
|
||||
matte: Option<Value>,
|
||||
}
|
||||
|
||||
impl RasterOptions {
|
||||
@@ -169,6 +176,22 @@ impl RasterOptions {
|
||||
scans: self.scans.unwrap_or(3).clamp(*jpeg::SCANS.start(), *jpeg::SCANS.end()),
|
||||
embed_icc: self.embed_icc.unwrap_or(true),
|
||||
},
|
||||
palette: PaletteOptions {
|
||||
colors: self.colors.unwrap_or(256).clamp(2, 256),
|
||||
reduction: parse(
|
||||
self.reduction.as_deref(),
|
||||
Reduction::from_id,
|
||||
"reduction",
|
||||
"perceptual, selective, adaptive, web, blackWhite or gray",
|
||||
)?,
|
||||
dither: parse(self.dither.as_deref(), Dither::from_id, "dither", "none, diffusion, pattern or noise")?,
|
||||
dither_amount: self.dither_amount.unwrap_or(100).min(100),
|
||||
transparency: self.transparency.unwrap_or(true),
|
||||
matte: match &self.matte {
|
||||
Some(v) => background(v).map_err(|e| bad(C, format!("matte: {e}")))?,
|
||||
None => Some([255; 3]),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -291,7 +314,7 @@ pub fn encode_all(doc: &Document, format: &str, p: &Value) -> Result<Encoded> {
|
||||
let (bytes, warnings) = super::pdf::encode(C, doc, p)?;
|
||||
return Ok(Encoded { warnings, ..Encoded::one(bytes) });
|
||||
}
|
||||
"png" | "jpg" | "webp" => {
|
||||
"png" | "jpg" | "webp" | "gif" | "png8" => {
|
||||
let o: RasterOptions = options(f, p)?;
|
||||
// New Document → Background Contents: White makes the export opaque, unless `background`
|
||||
// says otherwise (JPEG has no alpha: white either way).
|
||||
@@ -301,6 +324,8 @@ pub fn encode_all(doc: &Document, format: &str, p: &Value) -> Result<Encoded> {
|
||||
let format = match f.id {
|
||||
"png" => RasterFormat::Png,
|
||||
"jpg" => RasterFormat::Jpeg,
|
||||
"gif" => RasterFormat::Gif,
|
||||
"png8" => RasterFormat::Png8,
|
||||
_ => RasterFormat::WebP,
|
||||
};
|
||||
// Use Artboards: one file per chosen artboard (default all); else the one chosen.
|
||||
@@ -322,7 +347,7 @@ pub fn encode_all(doc: &Document, format: &str, p: &Value) -> Result<Encoded> {
|
||||
enc.maps.extend(imagemap::build(doc, region, scale, map, enc.files.len()));
|
||||
let bytes = renderer.export_region(doc, region, format, &settings).map_err(EngineError::Other)?;
|
||||
// File Info as PNG text chunks.
|
||||
let bytes = if format == RasterFormat::Png {
|
||||
let bytes = if matches!(format, RasterFormat::Png | RasterFormat::Png8) {
|
||||
vectorcraft_render::encode::png::with_text(bytes, &doc.metadata.png_text(&doc.title))
|
||||
} else {
|
||||
bytes
|
||||
|
||||
Reference in New Issue
Block a user