Skip to content

Commit 66f5e0f

Browse files
committed
feat(image): implement png_quantize
1 parent d0de72e commit 66f5e0f

File tree

6 files changed

+96
-3
lines changed

6 files changed

+96
-3
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,5 @@ Cargo.lock
1313
!.yarn/versions
1414
.turbo
1515
*.tsbuildinfo
16+
optimized-lossless.*
17+
quantized.png

optimize-test.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
const { readFileSync, writeFileSync } = require('fs')
22

3-
const { losslessCompressPng, compressJpeg } = require('./packages/binding')
3+
const { losslessCompressPng, compressJpeg, pngQuantize } = require('./packages/binding')
44

5-
writeFileSync('optimized-lossless.png', losslessCompressPng(readFileSync('./un-optimized.png')))
5+
const PNG = readFileSync('./un-optimized.png')
6+
7+
writeFileSync('optimized-lossless.png', losslessCompressPng(PNG))
8+
9+
writeFileSync('quantized.png', pngQuantize(PNG))
610

711
writeFileSync('optimized-lossless.jpg', compressJpeg(readFileSync('./un-optimized.jpg')))

packages/binding/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ oxipng_libdeflater = ["oxipng/libdeflater", "oxipng/parallel"]
1212
with_simd = ["mozjpeg-sys/nasm_simd_parallel_build"]
1313

1414
[dependencies]
15+
imagequant = "4.0.0-beta.8"
1516
libc = "0.2"
17+
lodepng = "3"
1618
napi = {version = "2", default-features = false, features = ["napi3"]}
1719
napi-derive = {version = "2", default-features = false, features = ["type-def"]}
1820

packages/binding/index.d.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,3 +64,10 @@ export interface JpegCompressOptions {
6464
optimizeScans?: boolean | undefined | null
6565
}
6666
export function compressJpeg(input: Buffer, options?: JpegCompressOptions | undefined | null): Buffer
67+
export interface PngQuantOptions {
68+
minQuality?: number | undefined | null
69+
maxQuality?: number | undefined | null
70+
speed?: number | undefined | null
71+
posterization?: number | undefined | null
72+
}
73+
export function pngQuantize(input: Buffer, options?: PngQuantOptions | undefined | null): Buffer

packages/binding/index.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,8 @@ if (!nativeBinding) {
221221
throw new Error(`Failed to load native binding`)
222222
}
223223

224-
const { losslessCompressPng, compressJpeg } = nativeBinding
224+
const { losslessCompressPng, compressJpeg, pngQuantize } = nativeBinding
225225

226226
module.exports.losslessCompressPng = losslessCompressPng
227227
module.exports.compressJpeg = compressJpeg
228+
module.exports.pngQuantize = pngQuantize

packages/binding/src/lib.rs

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,3 +183,80 @@ extern "C" fn silence_message(
183183
_level: std::os::raw::c_int,
184184
) {
185185
}
186+
187+
#[napi(object)]
188+
#[derive(Default)]
189+
pub struct PngQuantOptions {
190+
// default is 70
191+
pub min_quality: Option<u32>,
192+
// default is 99
193+
pub max_quality: Option<u32>,
194+
// 1- 10
195+
// Faster speeds generate images of lower quality, but may be useful for real-time generation of images.
196+
// default: 5
197+
pub speed: Option<u32>,
198+
// Number of least significant bits to ignore.
199+
// Useful for generating palettes for VGA, 15-bit textures, or other retro platforms.
200+
pub posterization: Option<u32>,
201+
}
202+
203+
#[napi]
204+
pub fn png_quantize(input: Buffer, options: Option<PngQuantOptions>) -> Result<Buffer> {
205+
let bitmap = lodepng::decode32(input.as_ref()).map_err(|err| {
206+
Error::new(
207+
Status::InvalidArg,
208+
format!("Decode png from buffer failed{}", err),
209+
)
210+
})?;
211+
let options = options.unwrap_or_default();
212+
let width = bitmap.width;
213+
let height = bitmap.height;
214+
let mut liq = imagequant::new();
215+
liq
216+
.set_speed(options.speed.unwrap_or(5) as i32)
217+
.map_err(|err| Error::new(Status::GenericFailure, format!("{}", err)))?;
218+
liq
219+
.set_quality(
220+
options.min_quality.unwrap_or(70) as u8,
221+
options.max_quality.unwrap_or(99) as u8,
222+
)
223+
.map_err(|err| Error::new(Status::GenericFailure, format!("{}", err)))?;
224+
let mut img = liq
225+
.new_image(
226+
bitmap.buffer.as_slice(),
227+
width as usize,
228+
height as usize,
229+
0.0,
230+
)
231+
.map_err(|err| {
232+
Error::new(
233+
Status::GenericFailure,
234+
format!("Create image failed {}", err),
235+
)
236+
})?;
237+
let mut quantization_result = liq
238+
.quantize(&mut img)
239+
.map_err(|err| Error::new(Status::GenericFailure, format!("quantize failed {}", err)))?;
240+
quantization_result
241+
.set_dithering_level(1.0)
242+
.map_err(|err| Error::new(Status::GenericFailure, format!("{}", err)))?;
243+
let (palette, pixels) = quantization_result
244+
.remapped(&mut img)
245+
.map_err(|err| Error::new(Status::GenericFailure, format!("remap failed {}", err)))?;
246+
let mut encoder = lodepng::Encoder::new();
247+
encoder.set_palette(palette.as_slice()).map_err(|err| {
248+
Error::new(
249+
Status::GenericFailure,
250+
format!("Set palette on png encoder {}", err),
251+
)
252+
})?;
253+
let output = encoder
254+
.encode(pixels.as_slice(), width, height)
255+
.map_err(|err| {
256+
Error::new(
257+
Status::GenericFailure,
258+
format!("Encode quantized png failed {}", err),
259+
)
260+
})?;
261+
Ok(output.into())
262+
}

0 commit comments

Comments
 (0)