From ec6e322148843caa3c39cee28137a1471d25156e Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 4 Sep 2025 10:00:26 +0800 Subject: [PATCH] feat(png): add automatic gamma detection from PNG metadata for color-accurate quantization (#152) * Initial plan * Initial analysis of PNG quantization color distortion issue Co-authored-by: Brooooooklyn <3468483+Brooooooklyn@users.noreply.github.com> * fix(png): correct gamma handling in PNG quantization to prevent color distortion Co-authored-by: Brooooooklyn <3468483+Brooooooklyn@users.noreply.github.com> * feat(png): add configurable gamma parameter for non-sRGB images in quantization - Add optional gamma parameter to PngQuantOptions - Default to 0.45 (sRGB) but allow override for linear RGB (1.0) and other color spaces - Added comprehensive documentation with common gamma values - Addresses concern about non-sRGB source images Co-authored-by: Brooooooklyn <3468483+Brooooooklyn@users.noreply.github.com> * feat(png): add automatic gamma detection from PNG metadata in quantization Co-authored-by: Brooooooklyn <3468483+Brooooooklyn@users.noreply.github.com> * feat(png): remove manual gamma override, use only automatic detection Removed the gamma field from PngQuantOptions struct per feedback from @Brooooooklyn. Now only uses detect_gamma_from_png_info() for automatic gamma detection from PNG metadata (sRGB chunk, gAMA chunk, or sRGB fallback). Co-authored-by: Brooooooklyn <3468483+Brooooooklyn@users.noreply.github.com> * remove package-lock.json and add to gitignore Co-authored-by: Brooooooklyn <3468483+Brooooooklyn@users.noreply.github.com> * revert accidental yarn.lock format change and generated files Co-authored-by: Brooooooklyn <3468483+Brooooooklyn@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Brooooooklyn <3468483+Brooooooklyn@users.noreply.github.com> --- .gitignore | 3 ++- packages/binding/src/png.rs | 39 ++++++++++++++++++++++++++++++++++++- 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index ab5d123..50fc1d1 100644 --- a/.gitignore +++ b/.gitignore @@ -24,4 +24,5 @@ nasa-small.* output-overlay-png.png output-debian.jpeg *.wasm -packages/binding/npm \ No newline at end of file +packages/binding/npm +package-lock.json \ No newline at end of file diff --git a/packages/binding/src/png.rs b/packages/binding/src/png.rs index f771557..70f1d7b 100644 --- a/packages/binding/src/png.rs +++ b/packages/binding/src/png.rs @@ -255,6 +255,11 @@ fn png_quantize_inner(input: &[u8], options: &PngQuantOptions) -> Result if decoded_buf.len() < (width * height * 4) as usize { return Ok(input.to_vec()); } + + // Configure gamma for quantization - this affects color accuracy during palette generation + // Automatically detect gamma from PNG metadata (gAMA chunk, sRGB chunk, or sRGB fallback) + let gamma = detect_gamma_from_png_info(reader.info()); + let mut liq = imagequant::new(); liq .set_speed(options.speed.unwrap_or(5) as i32) @@ -266,7 +271,12 @@ fn png_quantize_inner(input: &[u8], options: &PngQuantOptions) -> Result ) .map_err(|err| Error::new(Status::GenericFailure, format!("{err}")))?; let mut img = liq - .new_image(decoded_buf.as_rgba(), width as usize, height as usize, 0.0) + .new_image( + decoded_buf.as_rgba(), + width as usize, + height as usize, + gamma + ) .map_err(|err| Error::new(Status::GenericFailure, format!("Create image failed {err}")))?; let mut quantization_result = liq .quantize(&mut img) @@ -295,6 +305,33 @@ fn png_quantize_inner(input: &[u8], options: &PngQuantOptions) -> Result Ok(output) } +/// Detects the appropriate gamma value for quantization from PNG metadata +/// +/// Priority order: +/// 1. If sRGB chunk is present, use sRGB gamma (reciprocal ~0.45) +/// 2. If gAMA chunk is present, use reciprocal of gamma value +/// 3. Default fallback to 0.45 (sRGB) +fn detect_gamma_from_png_info(info: &png::Info) -> f64 { + // sRGB color space implies gamma ~2.2, reciprocal ~0.45 + if info.srgb.is_some() { + return 0.45; + } + + // Check for gAMA chunk or source_gamma (same information, different representation) + if let Some(gamma) = info.source_gamma { + let gamma_value = gamma.into_value() as f64; + return 1.0 / gamma_value; + } + + if let Some(gama) = info.gama_chunk { + let gamma_value = gama.into_value() as f64; + return 1.0 / gamma_value; + } + + // Default fallback to sRGB + 0.45 +} + pub struct PngQuantTask { input: Uint8Array, options: PngQuantOptions,