├── imagequant-sys
├── COPYRIGHT
├── .gitignore
├── org
│ └── pngquant
│ │ ├── PngQuantException.java
│ │ ├── LiqObject.java
│ │ ├── Image.java
│ │ ├── Result.java
│ │ ├── PngQuant.java
│ │ └── PngQuant.c
├── c_test
│ ├── build.rs
│ ├── src
│ │ └── lib.rs
│ ├── Cargo.toml
│ └── test.c
├── build.rs
├── imagequant.pc.in
├── Cargo.toml
├── Makefile
├── pom.xml
├── example.c
├── libimagequant.cs
├── libimagequant.h
└── imagequant.xcodeproj
│ └── project.pbxproj
├── .gitignore
├── .github
└── workflows
│ └── ci.yml
├── examples
└── basic.rs
├── CONTRIBUTING.md
├── Cargo.toml
├── src
├── error.rs
├── rayoff.rs
├── blur.rs
├── capi.rs
├── kmeans.rs
├── seacow.rs
├── nearest.rs
├── rows.rs
├── mediancut.rs
├── lib.rs
├── attr.rs
├── image.rs
├── remap.rs
└── hist.rs
├── benches
└── bench.rs
├── CODE_OF_CONDUCT.md
├── CHANGELOG
└── README.md
/imagequant-sys/COPYRIGHT:
--------------------------------------------------------------------------------
1 | ../COPYRIGHT
--------------------------------------------------------------------------------
/imagequant-sys/.gitignore:
--------------------------------------------------------------------------------
1 | usr/
2 | target/
3 |
--------------------------------------------------------------------------------
/imagequant-sys/org/pngquant/PngQuantException.java:
--------------------------------------------------------------------------------
1 | package org.pngquant;
2 |
3 | public class PngQuantException extends Exception {
4 | }
5 |
--------------------------------------------------------------------------------
/imagequant-sys/c_test/build.rs:
--------------------------------------------------------------------------------
1 | fn main() {
2 | cc::Build::new()
3 | .include("..")
4 | .file("test.c")
5 | .compile("imagequanttestbin");
6 | }
7 |
--------------------------------------------------------------------------------
/imagequant-sys/c_test/src/lib.rs:
--------------------------------------------------------------------------------
1 | #[cfg(test)]
2 | extern crate imagequant_sys;
3 |
4 | #[cfg(test)]
5 | extern "C" {
6 | fn run_liq_tests();
7 | }
8 |
9 | #[test]
10 | fn c_test() {
11 | unsafe {
12 | run_liq_tests();
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | imagequant.pc
2 | *.lo
3 | *.o
4 | *.a
5 | *.so.0
6 | *.so
7 | *.bz2
8 | *.dylib
9 | *.dylib.0
10 | *.jnilib
11 | *.dSYM
12 | org/pngquant/*.class
13 | org/pngquant/*.h
14 | target/
15 | quantized_example.png
16 | example
17 | lodepng.?
18 | Cargo.lock
19 |
--------------------------------------------------------------------------------
/imagequant-sys/build.rs:
--------------------------------------------------------------------------------
1 | fn main() {
2 | if cfg!(all(feature = "std", feature = "no_std")) {
3 | println!("cargo:warning=both std and no_std features are enabled in imagequant-sys");
4 | }
5 | println!("cargo:include={}", std::env::var("CARGO_MANIFEST_DIR").unwrap());
6 | }
7 |
--------------------------------------------------------------------------------
/imagequant-sys/c_test/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "c_test"
3 | version = "0.1.0"
4 | edition = "2021"
5 | publish = false
6 |
7 | [lib]
8 | doctest = false
9 |
10 | [package.metadata.release]
11 | release = false
12 |
13 | [dependencies]
14 | imagequant-sys = { version = "4.0.3", path = ".." }
15 |
16 | [build-dependencies]
17 | cc = "1.1.7"
18 |
--------------------------------------------------------------------------------
/imagequant-sys/imagequant.pc.in:
--------------------------------------------------------------------------------
1 | prefix=@PREFIX@
2 | includedir=${prefix}/include
3 | libdir=${prefix}/lib
4 |
5 | Name: imagequant
6 | Description: Small, portable C library for high-quality conversion of RGBA images to 8-bit indexed-color (palette) images.
7 | URL: https://pngquant.org/lib/
8 | Version: @VERSION@
9 | Libs: -L${libdir} -limagequant
10 | Cflags: -I${includedir}
11 |
--------------------------------------------------------------------------------
/imagequant-sys/org/pngquant/LiqObject.java:
--------------------------------------------------------------------------------
1 | package org.pngquant;
2 |
3 | abstract class LiqObject {
4 | static {
5 | // libimagequant.jnilib or libimagequant.so must be in java.library.path
6 | System.loadLibrary("imagequant");
7 | }
8 |
9 | long handle;
10 |
11 | /**
12 | * Free memory used by the library. The object must not be used after this call.
13 | */
14 | abstract public void close();
15 |
16 | protected void finalize() throws Throwable {
17 | close();
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | push:
5 | branches: [ main ]
6 | pull_request:
7 | branches: [ main ]
8 |
9 | env:
10 | CARGO_TERM_COLOR: always
11 | CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse
12 |
13 | jobs:
14 | build:
15 | runs-on: ubuntu-latest
16 | steps:
17 | - uses: actions/checkout@v4
18 | - uses: actions-rs/toolchain@v1
19 | with:
20 | profile: minimal
21 | toolchain: stable
22 | - uses: actions-rs/cargo@v1
23 | with:
24 | command: test
25 | args: --no-default-features
26 | - uses: actions-rs/cargo@v1
27 | with:
28 | command: test
29 | args: --all-features --all
30 | - uses: actions-rs/cargo@v1
31 | with:
32 | command: test
33 | args: --all-features --all --release
34 |
--------------------------------------------------------------------------------
/examples/basic.rs:
--------------------------------------------------------------------------------
1 | //!
2 |
3 | fn main() {
4 | // Image loading/saving is outside scope of this library
5 | let width = 10;
6 | let height = 10;
7 | let fakebitmap = vec![imagequant::RGBA {r:100, g:200, b:250, a:255}; width * height];
8 |
9 | // Configure the library
10 | let mut liq = imagequant::new();
11 | liq.set_speed(5).unwrap();
12 | liq.set_quality(70, 99).unwrap();
13 |
14 | // Describe the bitmap
15 | let mut img = liq.new_image(&fakebitmap[..], width, height, 0.0).unwrap();
16 |
17 | // The magic happens in quantize()
18 | let mut res = match liq.quantize(&mut img) {
19 | Ok(res) => res,
20 | Err(err) => panic!("Quantization failed, because: {err:?}"),
21 | };
22 |
23 | // Enable dithering for subsequent remappings
24 | res.set_dithering_level(1.0).unwrap();
25 |
26 | // You can reuse the result to generate several images with the same palette
27 | let (palette, pixels) = res.remapped(&mut img).unwrap();
28 |
29 | println!(
30 | "Done! Got palette {palette:?} and {} pixels with {}% quality",
31 | pixels.len(),
32 | res.quantization_quality().unwrap()
33 | );
34 | }
35 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 |
2 | Thank you for contributing! pngquant and libimagequant are licensed under multiple
3 | licenses, so to make things clear, I'm accepting contributions as licensed under
4 | the BSD 2-clause license:
5 |
6 | Redistribution and use in source and binary forms, with or without modification,
7 | are permitted provided that the following conditions are met:
8 |
9 | 1. Redistributions of source code must retain the above copyright notice,
10 | this list of conditions and the following disclaimer.
11 |
12 | 2. Redistributions in binary form must reproduce the above copyright notice,
13 | this list of conditions and the following disclaimer in the documentation
14 | and/or other materials provided with the distribution.
15 |
16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26 |
27 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "imagequant"
3 | version = "4.5.0"
4 | description = "Convert 24/32-bit images to 8-bit palette with alpha channel.\nFor lossy PNG compression and high-quality GIF images\nDual-licensed like pngquant. See https://pngquant.org for details."
5 | authors = ["Kornel Lesiński "]
6 | license = "GPL-3.0-or-later"
7 | homepage = "https://pngquant.org/lib"
8 | repository = "https://github.com/ImageOptim/libimagequant"
9 | documentation = "https://docs.rs/imagequant"
10 | categories = ["multimedia::images"]
11 | keywords = ["quantization", "palette", "pngquant", "compression", "gif"]
12 | include = ["COPYRIGHT", "src/*.rs", "*.h", "README.md", "Cargo.toml"]
13 | readme = "README.md"
14 | edition = "2021"
15 | rust-version = "1.65"
16 |
17 | [features]
18 | default = ["threads", "std"]
19 |
20 | # libimagequant makes good use of multi-threading, so disabling threads has a significant performance peanalty
21 | threads = ["dep:rayon", "dep:thread_local", "std"]
22 |
23 | # supports up to 2048 colors for palettes, but NOT FOR REMAPPING
24 | large_palettes = []
25 |
26 | # To opt-in you must disable the default features to disable `std` and `threads`, and also enable `no_std`
27 | std = []
28 | no_std = ["dep:hashbrown"]
29 |
30 | # this is private and unstable for imagequant-sys only, do not use
31 | _internal_c_ffi = []
32 |
33 | [profile.release]
34 | debug = false
35 | strip = true
36 | panic = "abort"
37 |
38 | [profile.dev]
39 | panic = "abort"
40 |
41 | [lib]
42 | doctest = false
43 |
44 | [dependencies]
45 | arrayvec = { version = "0.7.4", default-features = false }
46 | rgb = { version = "0.8.47", default-features = false, features = ["bytemuck"] }
47 | rayon = { version = "1.10.0", optional = true }
48 | thread_local = { version = "1.1.8", optional = true }
49 | # Used only in no_std
50 | hashbrown = { version = "0.15.4", optional = true, default-features = false }
51 |
52 | [dev-dependencies]
53 | lodepng = "3.10"
54 |
55 | [workspace]
56 | members = ["imagequant-sys", "imagequant-sys/c_test"]
57 |
58 | [package.metadata.release]
59 | consolidate-commits = true
60 | tag-message = ""
61 | tag-prefix = ""
62 | tag-name = "{{version}}"
63 |
--------------------------------------------------------------------------------
/src/error.rs:
--------------------------------------------------------------------------------
1 | use core::{error, fmt};
2 | use std::collections::TryReserveError;
3 | pub use Error::*;
4 |
5 | /// Error codes
6 | #[cfg_attr(feature = "_internal_c_ffi", repr(C))]
7 | #[cfg_attr(not(feature = "_internal_c_ffi"), non_exhaustive)] // it's meant to be always set, but Rust complains for a good but unrelated reason
8 | #[derive(Copy, Clone, Debug, Eq, PartialEq)]
9 | #[allow(non_camel_case_types)]
10 | pub enum Error {
11 | /// Not an error. Exists for back-compat with the C API
12 | #[cfg(feature = "_internal_c_ffi")]
13 | LIQ_OK = 0,
14 | /// [`set_quality()`][crate::Attributes::set_quality] was used with a minimum quality, and the minimum could not be achieved
15 | QualityTooLow = 99,
16 | /// Function called with invalid arguments
17 | ValueOutOfRange = 100,
18 | /// Either the system/process really hit a limit, or some data like image size was ridiculously wrong. Could be a bug too
19 | OutOfMemory,
20 | /// Progress callback said to stop
21 | Aborted,
22 | /// Some terrible inconsistency happened
23 | InternalError,
24 | /// Slice needs to be bigger, or width/height needs to be smaller
25 | BufferTooSmall,
26 | /// NULL pointer or use-after-free in the C API
27 | InvalidPointer,
28 | /// Congratulations, you've discovered an edge case
29 | Unsupported,
30 | }
31 |
32 | impl error::Error for Error {}
33 |
34 | impl fmt::Display for Error {
35 | #[cold]
36 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37 | f.write_str(match *self {
38 | #[cfg(feature = "_internal_c_ffi")]
39 | Self::LIQ_OK => "OK",
40 | Self::QualityTooLow => "QUALITY_TOO_LOW",
41 | Self::ValueOutOfRange => "VALUE_OUT_OF_RANGE",
42 | Self::OutOfMemory => "OUT_OF_MEMORY",
43 | Self::Aborted => "ABORTED",
44 | Self::InternalError => "INTERNAL_ERROR",
45 | Self::BufferTooSmall => "BUFFER_TOO_SMALL",
46 | Self::InvalidPointer => "INVALID_POINTER",
47 | Self::Unsupported => "UNSUPPORTED",
48 | })
49 | }
50 | }
51 |
52 | impl From for Error {
53 | fn from(e: core::convert::Infallible) -> Self {
54 | match e {}
55 | }
56 | }
57 |
58 | impl From for Error {
59 | #[cold]
60 | fn from(_: TryReserveError) -> Self {
61 | Self::OutOfMemory
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/src/rayoff.rs:
--------------------------------------------------------------------------------
1 | use core::slice::ChunksMut;
2 | use core::cell::OnceCell;
3 |
4 | #[cfg(all(not(feature = "std"), feature = "no_std"))]
5 | use std::boxed::Box;
6 |
7 | pub(crate) struct ThreadLocal(OnceCell);
8 |
9 | impl ThreadLocal {
10 | #[inline(always)]
11 | pub fn new() -> Self {
12 | Self(OnceCell::new())
13 | }
14 |
15 | #[inline(always)]
16 | pub fn get_or(&self, f: impl FnOnce() -> T) -> &T {
17 | self.0.get_or_init(f)
18 | }
19 |
20 | #[inline(always)]
21 | pub fn get_or_try(&self, f: impl FnOnce() -> Result) -> Result<&T, core::convert::Infallible> {
22 | // https://github.com/rust-lang/rust/issues/109737
23 | Ok(self.0.get_or_init(move || f().ok().unwrap()))
24 | }
25 | }
26 |
27 | impl IntoIterator for ThreadLocal {
28 | type IntoIter = core::option::IntoIter;
29 | type Item = T;
30 |
31 | #[inline(always)]
32 | fn into_iter(mut self) -> Self::IntoIter {
33 | self.0.take().into_iter()
34 | }
35 | }
36 |
37 | pub(crate) trait FakeRayonIter: Sized + Iterator {
38 | fn par_bridge(self) -> Self { self }
39 | fn for_each_init(self, init: I, mut cb: F) where I: FnOnce() -> T, F: FnMut(&mut T, Self::Item) {
40 | let mut tmp = init();
41 | for item in self {
42 | cb(&mut tmp, item);
43 | }
44 | }
45 | }
46 |
47 |
48 | impl FakeRayonIter for T where Self: Sized {
49 | }
50 |
51 | pub(crate) trait FakeRayonIntoIter {
52 | fn par_chunks_mut(&mut self, chunk_size: usize) -> ChunksMut<'_, T>;
53 | }
54 |
55 | impl<'a, T> FakeRayonIntoIter for &'a mut [T] {
56 | #[inline(always)]
57 | fn par_chunks_mut(&mut self, chunk_size: usize) -> ChunksMut<'_, T> {
58 | self.chunks_mut(chunk_size)
59 | }
60 | }
61 |
62 | impl<'a, T> FakeRayonIntoIter for Box<[T]> {
63 | #[inline(always)]
64 | fn par_chunks_mut(&mut self, chunk_size: usize) -> ChunksMut<'_, T> {
65 | self.chunks_mut(chunk_size)
66 | }
67 | }
68 |
69 | pub(crate) struct SpawnMock;
70 |
71 | impl SpawnMock {
72 | #[inline(always)]
73 | pub fn spawn(&self, f: F) -> R where F: FnOnce(SpawnMock) -> R {
74 | f(SpawnMock)
75 | }
76 | }
77 |
78 | #[inline(always)]
79 | pub(crate) fn scope(f: F) -> R where F: FnOnce(SpawnMock) -> R {
80 | f(SpawnMock)
81 | }
82 |
83 | #[inline(always)]
84 | pub(crate) fn num_cpus() -> usize {
85 | 1
86 | }
87 |
--------------------------------------------------------------------------------
/imagequant-sys/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "imagequant-sys"
3 | version = "4.2.0"
4 | description = "Convert 24/32-bit images to 8-bit palette with alpha channel.\nC API/FFI libimagequant that powers pngquant lossy PNG compressor.\n\nDual-licensed like pngquant. See https://pngquant.org for details."
5 | authors = ["Kornel Lesiński "]
6 | license = "GPL-3.0-or-later"
7 | homepage = "https://pngquant.org/lib"
8 | repository = "https://github.com/ImageOptim/libimagequant"
9 | documentation = "https://docs.rs/imagequant"
10 | categories = ["multimedia::images"]
11 | keywords = ["quantization", "palette", "image", "dither", "quant"]
12 | include = ["COPYRIGHT", "src/*.rs", "build.rs", "*.h", "README.md", "Cargo.toml"]
13 | readme = "README.md"
14 | edition = "2021"
15 | links = "imagequant"
16 | build = "build.rs"
17 | rust-version = "1.64"
18 |
19 | [features]
20 | default = ["imagequant/default"]
21 |
22 | # libimagequant makes good use of multi-threading, so disabling threads has a significant performance peanalty
23 | threads = ["imagequant/threads"]
24 |
25 | # To opt-in you must disable the default features to disable `std` and `threads`, and also enable `no_std`
26 | std = ["imagequant/std"]
27 | no_std = ["imagequant/no_std"]
28 |
29 | # Enable if you get errors like "no global memory allocator found" or "`#[panic_handler]` function required, but not found"
30 | no_std_global_handlers = ["no_std"]
31 |
32 | # internal for cargo-c only
33 | capi = []
34 |
35 | [lib]
36 | crate-type = ["staticlib", "lib"]
37 | doctest = false
38 | path = "src/ffi.rs"
39 | name = "imagequant_sys"
40 |
41 | [dependencies]
42 | imagequant = { path = "..", version = "4.5.0", default-features = false, features = ["_internal_c_ffi"] }
43 | bitflags = "2.5"
44 | libc = "0.2.153"
45 |
46 | [profile.release]
47 | debug = false
48 | strip = true
49 | panic = "abort"
50 |
51 | [profile.dev]
52 | panic = "abort"
53 |
54 | [package.metadata.capi.library]
55 | name = "imagequant"
56 | version = "0.4.1"
57 |
58 | [package.metadata.capi.pkg_config]
59 | name = "imagequant"
60 | filename = "imagequant"
61 | description = "Convert 24/32-bit images to 8-bit palette with alpha channel."
62 |
63 | [package.metadata.capi.header]
64 | name = "libimagequant"
65 | subdirectory = ""
66 | generation = false
67 |
68 | [package.metadata.capi.install.include]
69 | asset = [{from = "libimagequant.h"}]
70 |
71 | [package.metadata.release]
72 | consolidate-commits = true
73 | tag-message = ""
74 | tag-prefix = ""
75 | tag-name = "{{version}}"
76 |
77 |
--------------------------------------------------------------------------------
/benches/bench.rs:
--------------------------------------------------------------------------------
1 | #![feature(test)]
2 |
3 | extern crate test;
4 | use core::mem::MaybeUninit;
5 | use test::Bencher;
6 |
7 | use imagequant::*;
8 |
9 | #[bench]
10 | fn histogram(b: &mut Bencher) {
11 | let img = lodepng::decode32_file("/Users/kornel/Desktop/canvas.png").unwrap();
12 | let liq = Attributes::new();
13 | b.iter(move || {
14 | let mut img = liq.new_image(&*img.buffer, img.width, img.height, 0.).unwrap();
15 | let mut hist = Histogram::new(&liq);
16 | hist.add_image(&liq, &mut img).unwrap();
17 | });
18 | }
19 |
20 | #[bench]
21 | fn remap_ord(b: &mut Bencher) {
22 | let img = lodepng::decode32_file("/Users/kornel/Desktop/canvas.png").unwrap();
23 | let mut buf = vec![MaybeUninit::uninit(); img.width * img.height];
24 | let mut liq = Attributes::new();
25 | liq.set_speed(10).unwrap();
26 | let mut img = liq.new_image(img.buffer, img.width, img.height, 0.).unwrap();
27 | liq.set_max_colors(256).unwrap();
28 | let mut res = liq.quantize(&mut img).unwrap();
29 | res.set_dithering_level(0.).unwrap();
30 | b.iter(move || {
31 | res.remap_into(&mut img, &mut buf).unwrap();
32 | res.remap_into(&mut img, &mut buf).unwrap();
33 | });
34 | }
35 |
36 | #[bench]
37 | fn kmeans(b: &mut Bencher) {
38 | b.iter(_unstable_internal_kmeans_bench());
39 | }
40 |
41 | #[bench]
42 | fn remap_floyd(b: &mut Bencher) {
43 | let img = lodepng::decode32_file("/Users/kornel/Desktop/canvas.png").unwrap();
44 | let mut buf = vec![MaybeUninit::uninit(); img.width * img.height];
45 | let mut liq = Attributes::new();
46 | liq.set_speed(10).unwrap();
47 | let mut img = liq.new_image(img.buffer, img.width, img.height, 0.).unwrap();
48 | let mut res = liq.quantize(&mut img).unwrap();
49 | res.set_dithering_level(1.).unwrap();
50 | b.iter(move || {
51 | res.remap_into(&mut img, &mut buf).unwrap();
52 | res.remap_into(&mut img, &mut buf).unwrap();
53 | });
54 | }
55 |
56 | #[bench]
57 | fn quantize_s8(b: &mut Bencher) {
58 | let img = lodepng::decode32_file("/Users/kornel/Desktop/canvas.png").unwrap();
59 | let mut liq = Attributes::new();
60 | liq.set_speed(8).unwrap();
61 | b.iter(move || {
62 | let mut img = liq.new_image(&*img.buffer, img.width, img.height, 0.).unwrap();
63 | liq.quantize(&mut img).unwrap();
64 | });
65 | }
66 |
67 | #[bench]
68 | fn quantize_s1(b: &mut Bencher) {
69 | let img = lodepng::decode32_file("/Users/kornel/Desktop/canvas.png").unwrap();
70 | let mut liq = Attributes::new();
71 | liq.set_speed(1).unwrap();
72 | b.iter(move || {
73 | let mut img = liq.new_image(&*img.buffer, img.width, img.height, 0.).unwrap();
74 | liq.quantize(&mut img).unwrap();
75 | });
76 | }
77 |
--------------------------------------------------------------------------------
/imagequant-sys/Makefile:
--------------------------------------------------------------------------------
1 | # You can configure these
2 | PREFIX ?= /usr/local
3 | LIBDIR ?= $(PREFIX)/lib
4 | INCLUDEDIR ?= $(PREFIX)/include
5 | PKGCONFIGDIR ?= $(LIBDIR)/pkgconfig
6 | DESTDIR ?= ""
7 |
8 | VERSION=$(shell grep '^version = "4' Cargo.toml | grep -Eo "4\.[0-9.]+")
9 | STATICLIB=libimagequant.a
10 |
11 | JNILIB=libimagequant.jnilib
12 |
13 | JAVACLASSES = org/pngquant/LiqObject.class org/pngquant/PngQuant.class org/pngquant/Image.class org/pngquant/Result.class
14 | JAVAHEADERS = $(JAVACLASSES:.class=.h)
15 | JAVAINCLUDE = -I'$(JAVA_HOME)/include' -I'$(JAVA_HOME)/include/linux' -I'$(JAVA_HOME)/include/win32' -I'$(JAVA_HOME)/include/darwin'
16 |
17 | PKGCONFIG = imagequant.pc
18 |
19 | all: static
20 |
21 | static: $(STATICLIB)
22 |
23 | java: $(JNILIB)
24 |
25 | $(STATICLIB): Cargo.toml
26 | cargo build --release --lib --target-dir=../target
27 | cp ../target/release/libimagequant_sys.a $(STATICLIB)
28 |
29 | $(JNILIB): $(JAVAHEADERS) $(STATICLIB) org/pngquant/PngQuant.c
30 | # You may need to set LDFLAGS env var. See: cargo rustc -- --print native-static-libs
31 | $(CC) -g $(CFLAGS) $(LDFLAGS) $(JAVAINCLUDE) -shared -o $@ org/pngquant/PngQuant.c $(STATICLIB)
32 |
33 | $(JAVACLASSES): %.class: %.java
34 | javac $<
35 |
36 | $(JAVAHEADERS): %.h: %.class
37 | javah -o $@ $(subst /,., $(patsubst %.class,%,$<)) && touch $@
38 |
39 | example: example.c lodepng.h lodepng.c $(STATICLIB)
40 | # remove -lpthread on Windows
41 | # add -ldl on Linux
42 | # You may need to set LDFLAGS env var. See: cargo rustc -- --print native-static-libs
43 | $(CC) -g $(CFLAGS) -Wall example.c $(STATICLIB) -lm -lpthread $(LDFLAGS) -o example
44 |
45 | lodepng.h:
46 | curl -o lodepng.h -L https://raw.githubusercontent.com/lvandeve/lodepng/master/lodepng.h
47 |
48 | lodepng.c:
49 | curl -o lodepng.c -L https://raw.githubusercontent.com/lvandeve/lodepng/master/lodepng.cpp
50 |
51 | clean:
52 | rm -f $(SHAREDLIBVER) $(SHAREDLIB) $(STATICLIB)
53 | rm -f $(JAVAHEADERS) $(JAVACLASSES) $(JNILIB) example
54 | rm -rf ../target
55 |
56 | distclean: clean
57 | rm -f imagequant.pc
58 |
59 | install: all $(PKGCONFIG)
60 | install -d $(DESTDIR)$(LIBDIR)
61 | install -d $(DESTDIR)$(PKGCONFIGDIR)
62 | install -d $(DESTDIR)$(INCLUDEDIR)
63 | install -m 644 $(STATICLIB) $(DESTDIR)$(LIBDIR)/$(STATICLIB)
64 | install -m 644 $(PKGCONFIG) $(DESTDIR)$(PKGCONFIGDIR)/$(PKGCONFIG)
65 | install -m 644 libimagequant.h $(DESTDIR)$(INCLUDEDIR)/libimagequant.h
66 | $(FIX_INSTALL_NAME)
67 |
68 | uninstall:
69 | rm -f $(DESTDIR)$(LIBDIR)/$(STATICLIB)
70 | rm -f $(DESTDIR)$(PKGCONFIGDIR)/$(PKGCONFIG)
71 | rm -f $(DESTDIR)$(INCLUDEDIR)/libimagequant.h
72 |
73 | $(PKGCONFIG): Cargo.toml
74 | sed 's|@PREFIX@|$(PREFIX)|;s|@VERSION@|$(VERSION)|' < imagequant.pc.in > $(PKGCONFIG)
75 |
76 | .PHONY: all static clean distclean java
77 | .DELETE_ON_ERROR:
78 |
--------------------------------------------------------------------------------
/imagequant-sys/pom.xml:
--------------------------------------------------------------------------------
1 |
3 | 4.0.0
4 | org.pngquant
5 | libimagequant
6 | jar
7 | 4.1.0
8 | pngquant
9 | https://pngquant.org
10 |
11 | .
12 |
13 |
14 | org.codehaus.mojo
15 | exec-maven-plugin
16 | 1.1
17 |
18 |
19 | build
20 | compile
21 | exec
22 |
23 | make
24 |
25 | -j8
26 | USE_SSE=1
27 | java
28 |
29 |
30 |
31 |
32 | clean
33 | clean
34 | exec
35 |
36 | make
37 |
38 | clean
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 | mac-x64
49 |
50 |
51 | Mac
52 | x64
53 |
54 |
55 |
56 |
57 | org.pngquant
58 | libimagequant-jni
59 | 1
60 | jnilib
61 | mac-x64
62 |
63 |
64 |
65 |
66 | linux-x64
67 |
68 |
69 | unix
70 | Linux
71 | x64
72 |
73 |
74 |
75 |
76 | org.pngquant
77 | libimagequant-jni
78 | so
79 | 1
80 | linux-x64
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
--------------------------------------------------------------------------------
/imagequant-sys/org/pngquant/Image.java:
--------------------------------------------------------------------------------
1 | package org.pngquant;
2 |
3 | import org.pngquant.*;
4 | import java.awt.image.*;
5 |
6 | /**
7 | * PngQuant's representation of an Image constructed from BufferedImage.
8 | */
9 | public class Image extends LiqObject {
10 |
11 | /**
12 | * Converts BufferedImage to internal representation (pixel data is copied).
13 | * It's best to use BufferedImage in RGB/RGBA format backed by DataBufferByte.
14 | * Throws if conversion fails.
15 | */
16 | public Image(BufferedImage image) throws PngQuantException {
17 | this(new PngQuant(), image);
18 | }
19 |
20 | public Image(PngQuant attr, BufferedImage image) throws PngQuantException {
21 | handle = handleFromImage(attr, image);
22 |
23 | if (handle == 0) {
24 | BufferedImage converted = new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_4BYTE_ABGR);
25 | converted.getGraphics().drawImage(image, 0, 0, null);
26 | handle = handleFromImage(attr, converted);
27 |
28 | if (handle == 0) {
29 | throw new PngQuantException();
30 | }
31 | }
32 | }
33 |
34 | /**
35 | * Guarantees presence of the given color in the palette (subject to setMaxColors())
36 | * if this image is used for quantization.
37 | */
38 | public native boolean addFixedColor(int r, int g, int b, int a);
39 | public boolean addFixedColor(int r, int g, int b) {
40 | return addFixedColor(r, g, b, 255);
41 | }
42 | public native int getWidth();
43 | public native int getHeight();
44 |
45 | public void close() {
46 | if (handle != 0) {
47 | liq_image_destroy(handle);
48 | handle = 0;
49 | }
50 | }
51 |
52 | private static long handleFromImage(PngQuant attr, BufferedImage image) {
53 | // The JNI wrapper will accept non-premultiplied ABGR and BGR only.
54 | int type = image.getType();
55 | if (type != BufferedImage.TYPE_3BYTE_BGR &&
56 | type != BufferedImage.TYPE_4BYTE_ABGR &&
57 | type != BufferedImage.TYPE_4BYTE_ABGR_PRE) return 0;
58 |
59 | WritableRaster raster = image.getRaster();
60 | ColorModel color = image.getColorModel();
61 | if (type == BufferedImage.TYPE_4BYTE_ABGR_PRE) color.coerceData(raster, false);
62 |
63 | DataBuffer buffer = raster.getDataBuffer();
64 | if (buffer instanceof DataBufferByte) {
65 | byte[] imageData = ((DataBufferByte)buffer).getData();
66 | return liq_image_create(attr.handle, imageData,
67 | raster.getWidth(), raster.getHeight(), color.getNumComponents());
68 | }
69 | return 0;
70 | }
71 |
72 | private static native long liq_image_create(long attr, byte[] bitmap, int width, int height, int components);
73 | private static native void liq_image_destroy(long handle);
74 | }
75 |
--------------------------------------------------------------------------------
/src/blur.rs:
--------------------------------------------------------------------------------
1 | /// Blurs image horizontally (width 2*size+1) and writes it transposed to dst (called twice gives 2d blur)
2 | #[inline(never)]
3 | fn transposing_1d_blur(src: &[u8], dst: &mut [u8], width: usize, height: usize, size: u16) {
4 | if width < 2 * size as usize + 1 || height < 2 * size as usize + 1 {
5 | return;
6 | }
7 |
8 | for (j, row) in src.chunks_exact(width).enumerate() {
9 | let mut sum = u16::from(row[0]) * size;
10 | for &v in &row[0..size as usize] {
11 | sum += u16::from(v);
12 | }
13 | for i in 0..size as usize {
14 | sum -= u16::from(row[0]);
15 | sum += u16::from(row[i + size as usize]);
16 | dst[i * height + j] = (sum / (size * 2)) as u8;
17 | }
18 | for i in size as usize..width - size as usize {
19 | sum -= u16::from(row[i - size as usize]);
20 | sum += u16::from(row[i + size as usize]);
21 | dst[i * height + j] = (sum / (size * 2)) as u8;
22 | }
23 | for i in width - size as usize..width {
24 | sum -= u16::from(row[i - size as usize]);
25 | sum += u16::from(row[width - 1]);
26 | dst[i * height + j] = (sum / (size * 2)) as u8;
27 | }
28 | }
29 | }
30 |
31 | /// Picks maximum of neighboring pixels (blur + lighten)
32 | #[inline(never)]
33 | pub(crate) fn liq_max3(src: &[u8], dst: &mut [u8], width: usize, height: usize) {
34 | liq_op3(src, dst, width, height, |a, b| a.max(b));
35 | }
36 |
37 | pub(crate) fn liq_op3(src: &[u8], dst: &mut [u8], width: usize, height: usize, op: impl Fn(u8, u8) -> u8) {
38 | for j in 0..height {
39 | let row = &src[j * width..][..width];
40 | let dst = &mut dst[j * width..][..width];
41 | let prevrow = &src[j.saturating_sub(1) * width..][..width];
42 | let nextrow = &src[(j + 1).min(height - 1) * width..][..width];
43 | let mut prev: u8;
44 | let mut curr = row[0];
45 | let mut next = row[0];
46 | for i in 0..width - 1 {
47 | prev = curr;
48 | curr = next;
49 | next = row[i + 1];
50 | let t1 = op(prev, next);
51 | let t2 = op(nextrow[i], prevrow[i]);
52 | dst[i] = op(curr, op(t1, t2));
53 | }
54 | let t1 = op(curr, next);
55 | let t2 = op(nextrow[width - 1], prevrow[width - 1]);
56 | dst[width - 1] = op(curr, op(t1, t2));
57 | }
58 | }
59 |
60 | /// Picks minimum of neighboring pixels (blur + darken)
61 | #[inline(never)]
62 | pub(crate) fn liq_min3(src: &[u8], dst: &mut [u8], width: usize, height: usize) {
63 | liq_op3(src, dst, width, height, |a, b| a.min(b));
64 | }
65 |
66 | /// Filters src image and saves it to dst, overwriting tmp in the process.
67 | /// Image must be width*height pixels high. Size controls radius of box blur.
68 | pub(crate) fn liq_blur(src_dst: &mut [u8], tmp: &mut [u8], width: usize, height: usize, size: u16) {
69 | transposing_1d_blur(src_dst, tmp, width, height, size);
70 | transposing_1d_blur(tmp, src_dst, height, width, size);
71 | }
72 |
--------------------------------------------------------------------------------
/src/capi.rs:
--------------------------------------------------------------------------------
1 | //! These are internal unstable private helper methods for imagequant-sys.
2 | //! For public stable a C FFI interface, see imagequant-sys crate instead.
3 | #![allow(missing_docs)]
4 | #![allow(clippy::missing_safety_doc)]
5 |
6 | #[cfg(all(not(feature = "std"), feature = "no_std"))]
7 | use crate::no_std_compat::*;
8 |
9 | use crate::pal::Palette;
10 | use crate::rows::RowCallback;
11 | use crate::seacow::{Pointer, RowBitmapMut, SeaCow};
12 | use crate::{Attributes, Error, Image, QuantizationResult, RGBA};
13 | use core::ffi::c_void;
14 | use core::mem::{self, MaybeUninit};
15 |
16 | pub const LIQ_VERSION: u32 = 40202;
17 |
18 | #[must_use]
19 | pub fn liq_get_palette_impl(r: &mut QuantizationResult) -> &Palette {
20 | r.int_palette()
21 | }
22 |
23 | #[must_use]
24 | pub unsafe fn liq_image_create_rgba_rows_impl<'rows>(attr: &Attributes, rows: &'rows [*const RGBA], width: u32, height: u32, gamma: f64) -> Option> {
25 | let rows = SeaCow::borrowed(&*(rows as *const [*const rgb::Rgba] as *const [Pointer>]));
26 | let rows_slice = rows.as_slice();
27 | if rows_slice.iter().any(|r| r.0.is_null()) {
28 | return None;
29 | }
30 | crate::image::Image::new_internal(attr, crate::rows::PixelsSource::Pixels { rows, pixels: None }, width, height, gamma).ok()
31 | }
32 |
33 | #[must_use]
34 | pub unsafe fn liq_image_create_rgba_bitmap_impl<'rows>(attr: &Attributes, rows: Box<[*const RGBA]>, width: u32, height: u32, gamma: f64) -> Option> {
35 | let rows = SeaCow::boxed(mem::transmute::, Box<[Pointer]>>(rows));
36 | let rows_slice = rows.as_slice();
37 | if rows_slice.iter().any(|r| r.0.is_null()) {
38 | return None;
39 | }
40 | crate::image::Image::new_internal(attr, crate::rows::PixelsSource::Pixels { rows, pixels: None }, width, height, gamma).ok()
41 | }
42 |
43 | #[must_use]
44 | pub unsafe fn liq_image_create_custom_impl<'rows>(attr: &Attributes, row_callback: Box>, width: u32, height: u32, gamma: f64) -> Option> {
45 | Image::new_internal(attr, crate::rows::PixelsSource::Callback(row_callback), width, height, gamma).ok()
46 | }
47 |
48 | pub unsafe fn liq_write_remapped_image_impl(result: &mut QuantizationResult, input_image: &mut Image, buffer_bytes: &mut [MaybeUninit]) -> Result<(), Error> {
49 | let rows = RowBitmapMut::new_contiguous(buffer_bytes, input_image.width());
50 | result.write_remapped_image_rows_internal(input_image, rows)
51 | }
52 |
53 | pub unsafe fn liq_write_remapped_image_rows_impl(result: &mut QuantizationResult, input_image: &mut Image, rows: &mut [*mut MaybeUninit]) -> Result<(), Error> {
54 | let rows = RowBitmapMut::new(rows, input_image.width());
55 | result.write_remapped_image_rows_internal(input_image, rows)
56 | }
57 |
58 | /// Not recommended
59 | pub unsafe fn liq_image_set_memory_ownership_impl(image: &mut Image<'_>, own_rows: bool, own_pixels: bool, free_fn: unsafe extern "C" fn(*mut c_void)) -> Result<(), Error> {
60 | image.px.set_memory_ownership(own_rows, own_pixels, free_fn)
61 | }
62 |
--------------------------------------------------------------------------------
/imagequant-sys/org/pngquant/Result.java:
--------------------------------------------------------------------------------
1 | package org.pngquant;
2 |
3 | import org.pngquant.*;
4 | import java.awt.image.*;
5 |
6 | /**
7 | * Quantization result that holds palette and options for remapping.
8 | */
9 | public class Result extends LiqObject {
10 |
11 | /**
12 | * Throws when quantization fails (e.g. due to failing to achieve minimum quality)
13 | */
14 | public Result(PngQuant pngquant, Image image) throws PngQuantException {
15 | handle = liq_quantize_image(pngquant.handle, image.handle);
16 | if (handle == 0) {
17 | throw new PngQuantException();
18 | }
19 | }
20 |
21 | /**
22 | * @return BufferedImage remapped to palette this Result has been created with or null on failure.
23 | */
24 | public BufferedImage getRemapped(Image orig_image) {
25 | byte[] pal = liq_get_palette(handle);
26 | IndexColorModel color = new IndexColorModel(8, pal.length/4, pal, 0, true);
27 | BufferedImage img = new BufferedImage(
28 | orig_image.getWidth(), orig_image.getHeight(),
29 | BufferedImage.TYPE_BYTE_INDEXED, color);
30 |
31 | byte[] data = get8bitDataFromImage(img);
32 | if (data == null) return null;
33 |
34 | if (!liq_write_remapped_image(handle, orig_image.handle, data)) return null;
35 |
36 | return img;
37 | }
38 |
39 | /**
40 | * Dithering strength. Floyd-Steinberg is always used and in
41 | * speed settings 1-5 high-quality adaptive dithering is used.
42 | * @see PngQuant.setSpeed()
43 | * @link http://pngquant.org/lib/#liq_set_dithering_level
44 | *
45 | * @param dither_level Dithering in range 0 (none) and 1 (full)
46 | */
47 | public native boolean setDitheringLevel(float dither_level);
48 |
49 | /**
50 | * The default is 0.45455 (1/2.2) which is PNG's approximation of sRGB.
51 | */
52 | public native boolean setGamma(double gamma);
53 | public native double getGamma();
54 |
55 | /**
56 | * Mean Square Error of remapping of image used to create this result.
57 | * @link http://pngquant.org/lib/#liq_get_quantization_error
58 | *
59 | * @return MSE or -1 if not available
60 | */
61 | public native double getMeanSquareError();
62 |
63 | /**
64 | * @link http://pngquant.org/lib/#liq_get_quantization_quality
65 | * @return Actually achieved quality in 0-100 range on scale compatible with PngQuant.setQuality()
66 | */
67 | public native int getQuality();
68 |
69 | public void close() {
70 | if (handle != 0) {
71 | liq_result_destroy(handle);
72 | handle = 0;
73 | }
74 | }
75 |
76 | private static byte[] get8bitDataFromImage(BufferedImage image) {
77 | if (image.getType() == BufferedImage.TYPE_BYTE_INDEXED) {
78 | DataBuffer buffer = image.getRaster().getDataBuffer();
79 | if (buffer instanceof DataBufferByte) {
80 | return ((DataBufferByte)buffer).getData();
81 | }
82 | }
83 | return null;
84 | }
85 |
86 | private static native byte[] liq_get_palette(long handle);
87 | private static native long liq_quantize_image(long attr, long image);
88 | private static native boolean liq_write_remapped_image(long handle, long image, byte[] buffer);
89 | private static native void liq_result_destroy(long handle);
90 | }
91 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, gender identity and expression, level of experience,
9 | nationality, personal appearance, race, religion, or sexual identity and
10 | orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | * Using welcoming and inclusive language
18 | * Being respectful of differing viewpoints and experiences
19 | * Gracefully accepting constructive criticism
20 | * Focusing on what is best for the community
21 | * Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | * The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | * Trolling, insulting/derogatory comments, and personal or political attacks
28 | * Public or private harassment
29 | * Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | * Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at kornel@geekhood.net. All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at [http://contributor-covenant.org/version/1/4][version]
72 |
73 | [homepage]: http://contributor-covenant.org
74 | [version]: http://contributor-covenant.org/version/1/4/
75 |
--------------------------------------------------------------------------------
/CHANGELOG:
--------------------------------------------------------------------------------
1 | version 4.4
2 | -----------
3 | - palette quality improvements
4 | - Histogram::from_palette convenience method
5 |
6 | version 4.3
7 | -----------
8 | - code quality and performance improvements
9 | - copyable Image object
10 |
11 | version 4.2
12 | -----------
13 | - rewritten and improved handling of fixed palette colors
14 | - support for palettes larger than 256 colors
15 | - fix for remapping when importance_map has lots of pixels with 0 importance
16 |
17 | version 4.1
18 | -----------
19 | - improved dithering over preset background
20 | - remap_into_vec method
21 | - fix for images over 16 megapixels
22 |
23 | version 4.0
24 | -----------
25 | - rewritten in Rust
26 | - replaced Makefiles with Cargo
27 |
28 | version 2.17
29 | ------------
30 | - quality improvement
31 | - ARM64 build fix
32 |
33 | version 2.16
34 | ------------
35 | - fixed LCMS2 error handling
36 |
37 | version 2.15
38 | ------------
39 | - speed and quality improvements
40 |
41 | version 2.14
42 | ------------
43 | - improved Rust API
44 | - quality improvements for remapping overlays over a background
45 |
46 | version 2.13
47 | ------------
48 | - support OpenMP in clang
49 | - dropped old Internet Explorer workarounds
50 | - speed and quality improvements
51 |
52 | version 2.12
53 | ------------
54 | - new liq_histogram_add_fixed_color()
55 | - faster for large/complex images
56 | - workarounds for Microsoft's outdated C compiler
57 |
58 | version 2.11
59 | ------------
60 | - new liq_image_set_background() for high-quality remapping of GIF frames
61 | - new liq_image_set_importance_map() for controlling which parts of the image get more palette colors
62 | - improved OpenMP support
63 |
64 | version 2.10
65 | -----------
66 | - supports building with Rust/Cargo
67 |
68 | version 2.9
69 | -----------
70 | - new liq_histogram_add_colors()
71 |
72 | version 2.8
73 | -----------
74 | - standalone version
75 | - added Java interface (JNI)
76 | - new API for remapping multiple images to a single shared palette
77 |
78 | version 2.7
79 | -----------
80 | - improved dithering of saturated and semitransparent colors
81 | - libimagequant reports detailed progress and supports aborting of operations via callbacks
82 | - fixed order of log output when using openmp
83 |
84 | version 2.5
85 | -----------
86 | - replaced color search algorithm with vantage point tree, which is much faster and more reliable
87 | - deprecated IE6 workaround
88 | - warn when compiled without color profile support
89 | - improved support for predefined colors in libimagequant
90 |
91 | version 2.4
92 | -----------
93 | - fixed remapping of bright colors when dithering
94 | - added libimagequant API to add fixed preset colors to the palette
95 |
96 | version 2.3
97 | -----------
98 | - added ./configure script for better support of Intel C compiler and dependencies [thanks to pdknsk]
99 | - tweaked quality metric to better estimate quality of images with large solid backgrounds [thanks to Rolf Timmermans]
100 | - avoid applying quality setting to images that use palette already
101 |
102 | version 2.2
103 | -----------
104 | - OpenMP acceleration
105 | - improved support for Intel C Compiler, speedup in 32-bit GCC, and some workarounds for Visual Studio's incomplete C support
106 |
107 | version 2.1
108 | -----------
109 | - option to generate posterized output (for use with 16-bit textures)
110 |
111 | version 2.0
112 | -----------
113 | - refactored codebase into pngquant and standalone libimagequant
114 | - reduced memory usage by further 30% (and more for very large images)
115 | - less precise remapping improving speed by 25% in higher speed settings
116 | - fixed regression in dithering of alpha channel
117 |
--------------------------------------------------------------------------------
/imagequant-sys/org/pngquant/PngQuant.java:
--------------------------------------------------------------------------------
1 | package org.pngquant;
2 |
3 | import org.pngquant.*;
4 | import java.awt.image.*;
5 |
6 | /**
7 | * Starting point for the library. Holds configuration. Equivalent of liq_attr* in libimagequant.
8 | */
9 | public class PngQuant extends LiqObject {
10 |
11 | /**
12 | * Single instance can be "recycled" for many remappings.
13 | */
14 | public PngQuant() {
15 | handle = liq_attr_create();
16 | }
17 |
18 | public PngQuant(PngQuant other) {
19 | handle = liq_attr_copy(other.handle);
20 | }
21 |
22 | /**
23 | * 1-shot quantization and remapping with current settings.
24 | * @see quantize()
25 | *
26 | * @return 8-bit indexed image or null on failure
27 | */
28 | public BufferedImage getRemapped(BufferedImage bufimg) {
29 | try {
30 | Image liqimg = new Image(this, bufimg);
31 | BufferedImage remapped = getRemapped(liqimg);
32 | liqimg.close();
33 | return remapped;
34 | } catch(PngQuantException e) {
35 | return null;
36 | }
37 | }
38 |
39 | /** @return remapped image or null on failure */
40 | public BufferedImage getRemapped(Image liqimg) {
41 | Result result = quantize(liqimg);
42 | if (result == null) return null;
43 | BufferedImage remapped = result.getRemapped(liqimg);
44 | result.close();
45 | return remapped;
46 | }
47 |
48 | /**
49 | * Performs quantization (chooses optimal palette for the given Image).
50 | * Returned object can be used to customize remapping and reused to remap other images to the same palette.
51 | * @link http://pngquant.org/lib/#liq_quantize_image
52 | *
53 | * @return null on failure
54 | */
55 | public Result quantize(Image img) {
56 | try {
57 | return new Result(this, img);
58 | } catch(PngQuantException e) {
59 | return null;
60 | }
61 | }
62 |
63 | /**
64 | * Remapped images won't use more than given number of colors (may use less if setQuality() is used)
65 | *
66 | * @link http://pngquant.org/lib/#liq_set_max_colors
67 | */
68 | public native boolean setMaxColors(int colors);
69 |
70 | /**
71 | * Equivalent of setQuality(target/2, target)
72 | *
73 | * @link http://pngquant.org/lib/#liq_set_quality
74 | */
75 | public native boolean setQuality(int target);
76 |
77 | /**
78 | * Quality in range 0-100. Quantization will fail if minimum quality cannot
79 | * be achieved with given number of colors.
80 | *
81 | * @link http://pngquant.org/lib/#liq_set_quality
82 | */
83 | public native boolean setQuality(int min, int max);
84 |
85 | /**
86 | * Speed in range 1 (slowest) and 11 (fastest). 3 is the optimum.
87 | * Higher speeds quantize quicker, but at cost of quality and sometimes larger images.
88 | *
89 | * @link http://pngquant.org/lib/#liq_set_speed
90 | */
91 | public native boolean setSpeed(int speed);
92 |
93 | /**
94 | * Reduces color precision by truncating number of least significant bits.
95 | * Slightly improves speed and helps generating images for low-fidelity displays/textures.
96 | *
97 | * @link http://pngquant.org/lib/#liq_set_min_posterization
98 | */
99 | public native boolean setMinPosterization(int bits);
100 |
101 | public void close() {
102 | if (handle != 0) {
103 | liq_attr_destroy(handle);
104 | handle = 0;
105 | }
106 | }
107 |
108 | private static native long liq_attr_create();
109 | private static native long liq_attr_copy(long orig);
110 | private static native void liq_attr_destroy(long handle);
111 | }
112 |
--------------------------------------------------------------------------------
/imagequant-sys/example.c:
--------------------------------------------------------------------------------
1 | /**
2 |
3 | This is an example how to write your own simple pngquant using libimagequant.
4 |
5 | libimagequant works with any PNG library. This example uses lodepng, because it's easier to use than libpng.
6 |
7 | 1. Get lodepng.c (download lodepng.cpp and rename it) and lodepng.h
8 | from https://lodev.org/lodepng/ and put them in this directry
9 |
10 | 2. Compile libimagequant (see README.md)
11 |
12 | 3. Compile and run the example:
13 |
14 | gcc -O3 example.c libimagequant.a -o example
15 | ./example truecolor_file.png
16 |
17 |
18 | This example code can be freely copied under CC0 (public domain) license.
19 | */
20 |
21 | #include "lodepng.h" // Get it from https://raw.githubusercontent.com/lvandeve/lodepng/master/lodepng.h
22 | #include "lodepng.c" // Get it from https://raw.githubusercontent.com/lvandeve/lodepng/master/lodepng.cpp and rename
23 | #include
24 | #include
25 | #include "libimagequant.h"
26 |
27 | int main(int argc, char *argv[]) {
28 | if (argc < 2) {
29 | fprintf(stderr, "Please specify a path to a PNG file\n");
30 | return EXIT_FAILURE;
31 | }
32 |
33 | const char *input_png_file_path = argv[1];
34 |
35 | // Load PNG file and decode it as raw RGBA pixels
36 | // This uses lodepng library for PNG reading (not part of libimagequant)
37 |
38 | unsigned int width, height;
39 | unsigned char *raw_rgba_pixels;
40 | unsigned int status = lodepng_decode32_file(&raw_rgba_pixels, &width, &height, input_png_file_path);
41 | if (status) {
42 | fprintf(stderr, "Can't load %s: %s\n", input_png_file_path, lodepng_error_text(status));
43 | return EXIT_FAILURE;
44 | }
45 |
46 | // Use libimagequant to make a palette for the RGBA pixels
47 |
48 | liq_attr *handle = liq_attr_create();
49 | liq_image *input_image = liq_image_create_rgba(handle, raw_rgba_pixels, width, height, 0);
50 | // You could set more options here, like liq_set_quality
51 | liq_result *quantization_result;
52 | if (liq_image_quantize(input_image, handle, &quantization_result) != LIQ_OK) {
53 | fprintf(stderr, "Quantization failed\n");
54 | return EXIT_FAILURE;
55 | }
56 |
57 | // Use libimagequant to make new image pixels from the palette
58 |
59 | size_t pixels_size = width * height;
60 | unsigned char *raw_8bit_pixels = malloc(pixels_size);
61 | liq_set_dithering_level(quantization_result, 1.0);
62 |
63 | liq_write_remapped_image(quantization_result, input_image, raw_8bit_pixels, pixels_size);
64 | const liq_palette *palette = liq_get_palette(quantization_result);
65 |
66 | // Save converted pixels as a PNG file
67 | // This uses lodepng library for PNG writing (not part of libimagequant)
68 |
69 | LodePNGState state;
70 | lodepng_state_init(&state);
71 | state.info_raw.colortype = LCT_PALETTE;
72 | state.info_raw.bitdepth = 8;
73 | state.info_png.color.colortype = LCT_PALETTE;
74 | state.info_png.color.bitdepth = 8;
75 |
76 | for(int i=0; i < palette->count; i++) {
77 | lodepng_palette_add(&state.info_png.color, palette->entries[i].r, palette->entries[i].g, palette->entries[i].b, palette->entries[i].a);
78 | lodepng_palette_add(&state.info_raw, palette->entries[i].r, palette->entries[i].g, palette->entries[i].b, palette->entries[i].a);
79 | }
80 |
81 | unsigned char *output_file_data;
82 | size_t output_file_size;
83 | unsigned int out_status = lodepng_encode(&output_file_data, &output_file_size, raw_8bit_pixels, width, height, &state);
84 | if (out_status) {
85 | fprintf(stderr, "Can't encode image: %s\n", lodepng_error_text(out_status));
86 | return EXIT_FAILURE;
87 | }
88 |
89 | const char *output_png_file_path = "quantized_example.png";
90 | FILE *fp = fopen(output_png_file_path, "wb");
91 | if (!fp) {
92 | fprintf(stderr, "Unable to write to %s\n", output_png_file_path);
93 | return EXIT_FAILURE;
94 | }
95 | fwrite(output_file_data, 1, output_file_size, fp);
96 | fclose(fp);
97 |
98 | printf("Written %s\n", output_png_file_path);
99 |
100 | // Done. Free memory.
101 |
102 | liq_result_destroy(quantization_result); // Must be freed only after you're done using the palette
103 | liq_image_destroy(input_image);
104 | liq_attr_destroy(handle);
105 |
106 | free(raw_8bit_pixels);
107 | lodepng_state_cleanup(&state);
108 | }
109 |
--------------------------------------------------------------------------------
/imagequant-sys/c_test/test.c:
--------------------------------------------------------------------------------
1 | #undef NDEBUG
2 | #include
3 | #include "libimagequant.h"
4 | #include
5 | #include
6 |
7 | static char magic[] = "magic";
8 |
9 | static void test_log_callback_function(const liq_attr *at, const char *message, void* user_info) {
10 | assert(at);
11 | assert(user_info == magic);
12 | assert(message);
13 | assert(strlen(message));
14 | }
15 |
16 | static int test_abort_callback(float progress_percent, void* user_info) {
17 | assert(progress_percent >= 0.0 && progress_percent <= 100.0);
18 | assert(user_info == magic);
19 | return 0;
20 | }
21 |
22 | static int progress_called = 0;
23 | static int test_continue_callback(float progress_percent, void* user_info) {
24 | assert(progress_percent >= 0.0 && progress_percent <= 100.0);
25 | assert(user_info == magic);
26 | progress_called++;
27 | return 1;
28 | }
29 |
30 | static void test_abort() {
31 | liq_attr *attr = liq_attr_create();
32 |
33 | unsigned char dummy[4] = {0};
34 | liq_image *img = liq_image_create_rgba(attr, dummy, 1, 1, 0);
35 |
36 | liq_attr_set_progress_callback(attr, test_abort_callback, magic);
37 |
38 | liq_result *res = liq_quantize_image(attr, img);
39 | assert(!res);
40 |
41 | liq_attr_destroy(attr);
42 | }
43 |
44 | static void test_zero_histogram() {
45 | liq_attr *attr = liq_attr_create();
46 | liq_histogram *hist = liq_histogram_create(attr);
47 | assert(hist);
48 |
49 | liq_result *res;
50 | liq_error err = liq_histogram_quantize(hist, attr, &res);
51 | assert(!res);
52 | assert(err);
53 |
54 | liq_attr_destroy(attr);
55 | liq_histogram_destroy(hist);
56 | }
57 |
58 | static void test_histogram() {
59 | liq_attr *attr = liq_attr_create();
60 | liq_histogram *hist = liq_histogram_create(attr);
61 | assert(hist);
62 |
63 | const unsigned char dummy1[4] = {255,0,255,255};
64 | liq_image *const img1 = liq_image_create_rgba(attr, dummy1, 1, 1, 0);
65 | assert(img1);
66 |
67 | const liq_error err1 = liq_histogram_add_image(hist, attr, img1);
68 | assert(LIQ_OK == err1);
69 |
70 | const unsigned char dummy2[4] = {0,0,0,0};
71 | liq_image *const img2 = liq_image_create_rgba(attr, dummy2, 1, 1, 0);
72 | assert(img2);
73 | liq_image_add_fixed_color(img2, (liq_color){255,255,255,255});
74 |
75 |
76 | const liq_error err2 = liq_histogram_add_image(hist, attr, img2);
77 | assert(LIQ_OK == err2);
78 |
79 | liq_image_destroy(img1);
80 | liq_image_destroy(img2);
81 |
82 | liq_result *res;
83 | liq_error err = liq_histogram_quantize(hist, attr, &res);
84 | assert(LIQ_OK == err);
85 | assert(res);
86 |
87 | liq_attr_destroy(attr);
88 |
89 | liq_histogram_destroy(hist);
90 |
91 | const liq_palette *pal = liq_get_palette(res);
92 | assert(pal);
93 | assert(pal->count == 3);
94 |
95 | liq_result_destroy(res);
96 | }
97 |
98 | static void test_fixed_colors() {
99 | liq_attr *attr = liq_attr_create();
100 |
101 | liq_attr_set_progress_callback(attr, test_continue_callback, magic);
102 | liq_set_log_callback(attr, test_log_callback_function, magic);
103 |
104 | unsigned char dummy[4] = {0};
105 | liq_image *img = liq_image_create_rgba(attr, dummy, 1, 1, 0);
106 | assert(img);
107 |
108 | liq_image_add_fixed_color(img, (liq_color){0,0,0,0});
109 |
110 | liq_result *res = liq_quantize_image(attr, img);
111 | assert(res);
112 | assert(progress_called);
113 |
114 | const liq_palette *pal = liq_get_palette(res);
115 | assert(pal);
116 | assert(pal->count == 1);
117 |
118 | liq_result_destroy(res);
119 | liq_image_destroy(img);
120 | liq_attr_destroy(attr);
121 | }
122 |
123 | static void test_fixed_colors_order() {
124 | liq_attr *attr = liq_attr_create();
125 |
126 | unsigned char dummy[4] = {0};
127 | liq_image *img = liq_image_create_rgba(attr, dummy, 1, 1, 0);
128 | assert(img);
129 |
130 | liq_color colors[17] = {
131 | {0,0,0,0}, {1,1,1,1}, {2,2,2,2}, {3,3,3,3}, {4,4,4,4}, {5,4,4,4},
132 | {6,4,4,4}, {6,7,4,4}, {6,7,8,4}, {6,7,8,9}, {10,7,8,9}, {10,11,8,9},
133 | {10,11,12,9}, {10,11,12,13}, {14,11,12,13}, {14,15,12,13}, {253,254,255,254},
134 | };
135 |
136 | for(int i=0; i < 17; i++) {
137 | liq_image_add_fixed_color(img, colors[i]);
138 | }
139 |
140 | liq_result *res = liq_quantize_image(attr, img);
141 | assert(res);
142 |
143 | const liq_palette *pal = liq_get_palette(res);
144 | assert(pal);
145 | assert(pal->count == 17);
146 |
147 | for(int i=0; i < 17; i++) {
148 | assert(pal->entries[i].r == colors[i].r);
149 | assert(pal->entries[i].g == colors[i].g);
150 | assert(pal->entries[i].b == colors[i].b);
151 | assert(pal->entries[i].a == colors[i].a);
152 | }
153 |
154 | liq_set_dithering_level(res, 1.0);
155 |
156 | char buf[1];
157 | assert(LIQ_OK == liq_write_remapped_image(res, img, buf, 1));
158 |
159 | liq_result_set_progress_callback(res, test_abort_callback, magic);
160 | assert(LIQ_ABORTED == liq_write_remapped_image(res, img, buf, 1));
161 |
162 | liq_result_destroy(res);
163 | liq_image_destroy(img);
164 | liq_attr_destroy(attr);
165 | }
166 |
167 | void run_liq_tests() {
168 | test_fixed_colors();
169 | test_fixed_colors_order();
170 | test_abort();
171 | test_histogram();
172 | test_zero_histogram();
173 | }
174 |
--------------------------------------------------------------------------------
/src/kmeans.rs:
--------------------------------------------------------------------------------
1 | use crate::hist::{HistItem, HistogramInternal};
2 | use crate::nearest::Nearest;
3 | use crate::pal::{f_pixel, PalF, PalIndex, PalPop};
4 | use crate::rayoff::*;
5 | use crate::{CacheLineAlign, Error};
6 | use core::cell::RefCell;
7 | use rgb::prelude::*;
8 | use rgb::Argb;
9 |
10 | #[cfg(all(not(feature = "std"), feature = "no_std"))]
11 | use crate::no_std_compat::*;
12 |
13 | /// K-Means iteration: new palette color is computed from weighted average of colors that map best to that palette entry.
14 | // avoid false sharing
15 | pub(crate) struct Kmeans {
16 | averages: Vec,
17 | weighed_diff_sum: f64,
18 | }
19 |
20 | #[derive(Copy, Clone, Default)]
21 | struct ColorAvg {
22 | pub sum: Argb,
23 | pub total: f64,
24 | }
25 |
26 | impl Kmeans {
27 | #[inline]
28 | pub fn new(pal_len: usize) -> Result {
29 | let mut averages = Vec::new();
30 | averages.try_reserve_exact(pal_len)?;
31 | averages.resize(pal_len, ColorAvg::default());
32 | Ok(Self {
33 | averages,
34 | weighed_diff_sum: 0.,
35 | })
36 | }
37 |
38 | #[inline]
39 | pub fn update_color(&mut self, px: f_pixel, value: f32, matched: PalIndex) {
40 | let c = &mut self.averages[matched as usize];
41 | c.sum += (px.0 * value).map(f64::from);
42 | c.total += f64::from(value);
43 | }
44 |
45 | pub fn finalize(self, palette: &mut PalF) -> f64 {
46 | for (avg, (color, pop)) in self.averages.iter().zip(palette.iter_mut()).filter(|(_, (_, pop))| !pop.is_fixed()) {
47 | let total = avg.total;
48 | *pop = PalPop::new(total as f32);
49 | if total > 0. && color.a != 0. {
50 | *color = avg.sum.map(move |c| (c / total) as f32).into();
51 | }
52 | }
53 | self.weighed_diff_sum
54 | }
55 |
56 | #[inline(never)]
57 | pub(crate) fn iteration(hist: &mut HistogramInternal, palette: &mut PalF, adjust_weight: bool) -> Result {
58 | if hist.items.is_empty() {
59 | return Ok(0.);
60 | }
61 |
62 | let n = Nearest::new(palette)?;
63 | let colors = palette.as_slice();
64 | let len = colors.len();
65 |
66 | let tls = ThreadLocal::new();
67 | let total = hist.total_perceptual_weight;
68 |
69 | // chunk size is a trade-off between parallelization and overhead
70 | hist.items.par_chunks_mut(256).for_each_init(
71 | || tls.get_or(move || CacheLineAlign(RefCell::new(Self::new(len)))),
72 | move |kmeans, batch| {
73 | let Ok(mut tls) = kmeans.0.try_borrow_mut() else {
74 | debug_assert!(false);
75 | return;
76 | };
77 | if let Ok(ref mut kmeans) = *tls {
78 | kmeans.iterate_batch(batch, &n, colors, adjust_weight);
79 | }
80 | });
81 |
82 | let diff = tls.into_iter()
83 | .map(|c| c.0.into_inner())
84 | .reduce(Self::try_merge)
85 | .transpose()?
86 | .map_or(0., |kmeans| kmeans.finalize(palette) / total);
87 |
88 | replace_unused_colors(palette, hist)?;
89 | Ok(diff)
90 | }
91 |
92 | fn iterate_batch(&mut self, batch: &mut [HistItem], n: &Nearest, colors: &[f_pixel], adjust_weight: bool) {
93 | self.weighed_diff_sum += batch.iter_mut().map(|item| {
94 | let px = item.color;
95 | let (matched, mut diff) = n.search(&px, item.likely_palette_index());
96 | item.tmp.likely_palette_index = matched;
97 | if adjust_weight {
98 | let remapped = colors[matched as usize];
99 | let (_, new_diff) = n.search(&f_pixel(px.0 + px.0 - remapped.0), matched);
100 | debug_assert!(new_diff.is_finite());
101 | diff = new_diff;
102 | item.adjusted_weight = 2.0f32.mul_add(item.adjusted_weight, item.perceptual_weight) * (0.5 + diff);
103 | }
104 | debug_assert!(f64::from(diff) < 1e20);
105 | self.update_color(px, item.adjusted_weight, matched);
106 | f64::from(diff * item.perceptual_weight)
107 | }).sum::();
108 | }
109 |
110 | #[inline]
111 | pub fn merge(mut self, new: Self) -> Self {
112 | self.weighed_diff_sum += new.weighed_diff_sum;
113 | self.averages.iter_mut().zip(new.averages).for_each(|(p, n)| {
114 | p.sum += n.sum;
115 | p.total += n.total;
116 | });
117 | self
118 | }
119 |
120 | #[inline]
121 | pub fn try_merge(old: Result, new: Result) -> Result {
122 | match (old, new) {
123 | (Ok(old), Ok(new)) => Ok(Self::merge(old, new)),
124 | (Err(e), _) | (_, Err(e)) => Err(e),
125 | }
126 | }
127 | }
128 |
129 | /// kmeans may have merged or obsoleted some palette entries.
130 | /// This replaces these entries with histogram colors that are currently least-fitting the palette.
131 | fn replace_unused_colors(palette: &mut PalF, hist: &HistogramInternal) -> Result<(), Error> {
132 | for pal_idx in 0..palette.len() {
133 | let Some(pop) = palette.pop_as_slice().get(pal_idx) else { break };
134 | if pop.popularity() == 0. && !pop.is_fixed() {
135 | let n = Nearest::new(palette)?;
136 | let mut worst = None;
137 | let mut worst_diff = 0.;
138 | let colors = palette.as_slice();
139 | // the search is just for diff, ignoring adjusted_weight,
140 | // because the palette already optimizes for the max weight, so it'd likely find another redundant entry.
141 | for item in hist.items.iter() {
142 | // the early reject avoids running full palette search for every entry
143 | let may_be_worst = colors.get(item.likely_palette_index() as usize)
144 | .map_or(true, |pal| pal.diff(&item.color) > worst_diff);
145 | if may_be_worst {
146 | let diff = n.search(&item.color, item.likely_palette_index()).1;
147 | if diff > worst_diff {
148 | worst_diff = diff;
149 | worst = Some(item);
150 | }
151 | }
152 | }
153 | if let Some(worst) = worst {
154 | palette.set(pal_idx, worst.color, PalPop::new(worst.adjusted_weight));
155 | }
156 | }
157 | }
158 | Ok(())
159 | }
160 |
--------------------------------------------------------------------------------
/src/seacow.rs:
--------------------------------------------------------------------------------
1 | use core::mem::{self, MaybeUninit};
2 | use core::slice;
3 |
4 | #[cfg(all(not(feature = "std"), feature = "no_std"))]
5 | use std::{boxed::Box, vec::Vec};
6 |
7 | #[cfg(feature = "_internal_c_ffi")]
8 | use core::ffi::c_void;
9 |
10 | #[derive(Clone)]
11 | pub struct SeaCow<'a, T> {
12 | inner: SeaCowInner<'a, T>,
13 | }
14 |
15 | unsafe impl Send for SeaCowInner<'_, T> {}
16 | unsafe impl Sync for SeaCowInner<'_, T> {}
17 |
18 | /// Rust assumes `*const T` is never `Send`/`Sync`, but it can be.
19 | /// This is fudge for https://github.com/rust-lang/rust/issues/93367
20 | #[repr(transparent)]
21 | #[derive(Copy, Clone)]
22 | pub(crate) struct Pointer(pub *const T);
23 |
24 | #[derive(Copy, Clone)]
25 | #[repr(transparent)]
26 | pub(crate) struct PointerMut(pub *mut T);
27 |
28 | unsafe impl Send for Pointer {}
29 | unsafe impl Sync for Pointer {}
30 | unsafe impl Send for PointerMut {}
31 | unsafe impl Sync for PointerMut {}
32 |
33 | impl SeaCow<'static, T> {
34 | #[inline]
35 | #[must_use]
36 | pub fn boxed(data: Box<[T]>) -> Self {
37 | Self { inner: SeaCowInner::Boxed(data) }
38 | }
39 | }
40 |
41 | impl<'a, T> SeaCow<'a, T> {
42 | #[inline]
43 | #[must_use]
44 | pub const fn borrowed(data: &'a [T]) -> Self {
45 | Self { inner: SeaCowInner::Borrowed(data) }
46 | }
47 |
48 | /// The pointer must be `malloc`-allocated
49 | #[inline]
50 | #[cfg(feature = "_internal_c_ffi")]
51 | #[must_use]
52 | pub unsafe fn c_owned(ptr: *mut T, len: usize, free_fn: unsafe extern "C" fn(*mut c_void)) -> Self {
53 | debug_assert!(!ptr.is_null());
54 | debug_assert!(len > 0);
55 |
56 | Self {
57 | inner: SeaCowInner::Owned { ptr, len, free_fn },
58 | }
59 | }
60 |
61 | #[inline]
62 | #[cfg(feature = "_internal_c_ffi")]
63 | pub(crate) fn make_owned(&mut self, free_fn: unsafe extern "C" fn(*mut c_void)) {
64 | if let SeaCowInner::Borrowed(slice) = self.inner {
65 | self.inner = SeaCowInner::Owned { ptr: slice.as_ptr().cast_mut(), len: slice.len(), free_fn };
66 | }
67 | }
68 | }
69 |
70 | impl Clone for SeaCowInner<'_, T> {
71 | #[inline(never)]
72 | fn clone(&self) -> Self {
73 | let slice = match self {
74 | Self::Borrowed(data) => return Self::Borrowed(data),
75 | #[cfg(feature = "_internal_c_ffi")]
76 | Self::Owned { ptr, len, free_fn: _ } => unsafe { slice::from_raw_parts(*ptr, *len) },
77 | Self::Boxed(data) => &**data,
78 | };
79 | let mut v = Vec::new();
80 | v.try_reserve_exact(slice.len()).unwrap();
81 | v.extend_from_slice(slice);
82 | Self::Boxed(v.into_boxed_slice())
83 | }
84 | }
85 |
86 | enum SeaCowInner<'a, T> {
87 | #[cfg(feature = "_internal_c_ffi")]
88 | Owned { ptr: *mut T, len: usize, free_fn: unsafe extern "C" fn(*mut c_void) },
89 | Borrowed(&'a [T]),
90 | Boxed(Box<[T]>),
91 | }
92 |
93 | #[cfg(feature = "_internal_c_ffi")]
94 | impl Drop for SeaCowInner<'_, T> {
95 | fn drop(&mut self) {
96 | if let Self::Owned { ptr, free_fn, .. } = self {
97 | unsafe {
98 | (free_fn)((*ptr).cast());
99 | }
100 | }
101 | }
102 | }
103 |
104 | impl SeaCow<'_, T> {
105 | #[must_use]
106 | pub fn as_slice(&self) -> &[T] {
107 | match &self.inner {
108 | #[cfg(feature = "_internal_c_ffi")]
109 | SeaCowInner::Owned { ptr, len, .. } => unsafe { slice::from_raw_parts(*ptr, *len) },
110 | SeaCowInner::Borrowed(a) => a,
111 | SeaCowInner::Boxed(x) => x,
112 | }
113 | }
114 | }
115 |
116 | pub(crate) struct RowBitmap<'a, T> {
117 | rows: &'a [Pointer],
118 | width: usize,
119 | }
120 | unsafe impl Send for RowBitmap<'_, T> {}
121 |
122 | pub(crate) struct RowBitmapMut<'a, T> {
123 | rows: MutCow<'a, [PointerMut]>,
124 | width: usize,
125 | }
126 | unsafe impl Send for RowBitmapMut<'_, T> {}
127 |
128 | impl RowBitmapMut<'_, MaybeUninit> {
129 | #[inline]
130 | pub(crate) unsafe fn assume_init<'maybeowned>(&'maybeowned mut self) -> RowBitmap<'maybeowned, T> {
131 | #[allow(clippy::transmute_ptr_to_ptr)]
132 | RowBitmap {
133 | width: self.width,
134 | rows: mem::transmute::<&'maybeowned [PointerMut>], &'maybeowned [Pointer]>(self.rows.borrow_mut()),
135 | }
136 | }
137 | }
138 |
139 | impl RowBitmap<'_, T> {
140 | pub fn rows(&self) -> impl Iterator- {
141 | let width = self.width;
142 | self.rows.iter().map(move |row| {
143 | unsafe { slice::from_raw_parts(row.0, width) }
144 | })
145 | }
146 | }
147 |
148 | enum MutCow<'a, T: ?Sized> {
149 | Owned(Box),
150 | #[allow(dead_code)] /// This is optional, for FFI only
151 | Borrowed(&'a mut T),
152 | }
153 |
154 | impl MutCow<'_, T> {
155 | #[must_use]
156 | pub fn borrow_mut(&mut self) -> &mut T {
157 | match self {
158 | Self::Owned(a) => a,
159 | Self::Borrowed(a) => a,
160 | }
161 | }
162 | }
163 |
164 | impl<'a, T: Sync + Send + Copy + 'static> RowBitmapMut<'a, T> {
165 | #[inline]
166 | #[must_use]
167 | pub fn new_contiguous(data: &mut [T], width: usize) -> Self {
168 | Self {
169 | rows: MutCow::Owned(data.chunks_exact_mut(width).map(|r| PointerMut(r.as_mut_ptr())).collect()),
170 | width,
171 | }
172 | }
173 |
174 | /// Inner pointers must be valid for `'a` too, and at least `width` large each
175 | #[inline]
176 | #[cfg(feature = "_internal_c_ffi")]
177 | #[must_use]
178 | pub unsafe fn new(rows: &'a mut [*mut T], width: usize) -> Self {
179 | Self {
180 | rows: MutCow::Borrowed(&mut *(rows as *mut [*mut T] as *mut [PointerMut])),
181 | width,
182 | }
183 | }
184 |
185 | pub fn rows_mut(&mut self) -> impl Iterator
- + Send {
186 | let width = self.width;
187 | self.rows.borrow_mut().iter().map(move |row| {
188 | unsafe { slice::from_raw_parts_mut(row.0, width) }
189 | })
190 | }
191 |
192 | pub(crate) fn chunks(&mut self, chunk_size: usize) -> impl Iterator
- > {
193 | self.rows.borrow_mut().chunks_mut(chunk_size).map(|chunk| RowBitmapMut {
194 | width: self.width,
195 | rows: MutCow::Borrowed(chunk),
196 | })
197 | }
198 |
199 | #[must_use]
200 | pub(crate) fn len(&mut self) -> usize {
201 | self.rows.borrow_mut().len()
202 | }
203 | }
204 |
--------------------------------------------------------------------------------
/imagequant-sys/org/pngquant/PngQuant.c:
--------------------------------------------------------------------------------
1 | #include "org/pngquant/PngQuant.h"
2 | #include "org/pngquant/Image.h"
3 | #include "org/pngquant/Result.h"
4 | #include "libimagequant.h"
5 | #include
6 |
7 | typedef struct {
8 | liq_image *image;
9 | jbyte *data;
10 | } liq_jni_image;
11 |
12 | static void *handle(JNIEnv *env, jobject obj) {
13 | jlong h = (*env)->GetLongField(env, obj, (*env)->GetFieldID(env, (*env)->GetObjectClass(env, obj), "handle", "J"));
14 | return (void*)h;
15 | }
16 |
17 | JNIEXPORT jlong JNICALL Java_org_pngquant_PngQuant_liq_1attr_1create(JNIEnv *env, jclass class) {
18 | return (jlong)liq_attr_create();
19 | }
20 |
21 | JNIEXPORT jlong JNICALL Java_org_pngquant_PngQuant_liq_1attr_1copy(JNIEnv *env, jclass class, jlong attr) {
22 | return (jlong)liq_attr_copy((liq_attr*)attr);
23 | }
24 |
25 | JNIEXPORT void JNICALL Java_org_pngquant_PngQuant_liq_1attr_1destroy(JNIEnv *env, jclass class, jlong attr) {
26 | return liq_attr_destroy((liq_attr*)attr);
27 | }
28 |
29 | JNIEXPORT jboolean JNICALL Java_org_pngquant_PngQuant_setMaxColors(JNIEnv *env, jobject obj, jint colors) {
30 | return LIQ_OK == liq_set_max_colors(handle(env, obj), colors);
31 | }
32 |
33 | JNIEXPORT jboolean JNICALL Java_org_pngquant_PngQuant_setSpeed(JNIEnv *env, jobject obj, jint speed) {
34 | return LIQ_OK == liq_set_speed(handle(env, obj), speed);
35 | }
36 |
37 | JNIEXPORT jboolean JNICALL Java_org_pngquant_PngQuant_setMinPosterization(JNIEnv *env, jobject obj, jint p) {
38 | return LIQ_OK == liq_set_min_posterization(handle(env, obj), p);
39 | }
40 |
41 | JNIEXPORT jboolean JNICALL Java_org_pngquant_PngQuant_setQuality__I(JNIEnv *env, jobject obj, jint q) {
42 | return LIQ_OK == liq_set_quality(handle(env, obj), q/2, q);
43 | }
44 |
45 | JNIEXPORT jboolean JNICALL Java_org_pngquant_PngQuant_setQuality__II(JNIEnv *env, jobject obj, jint qmin, jint qmax) {
46 | return LIQ_OK == liq_set_quality(handle(env, obj), qmin, qmax);
47 | }
48 |
49 | static void convert_abgr(liq_color row_out[], int row_index, int width, void* user_info) {
50 | liq_jni_image *jniimg = user_info;
51 | int column_index;
52 | for(column_index=0; column_index < width; column_index++) {
53 | row_out[column_index].r = jniimg->data[4*(width*row_index + column_index) + 3];
54 | row_out[column_index].g = jniimg->data[4*(width*row_index + column_index) + 2];
55 | row_out[column_index].b = jniimg->data[4*(width*row_index + column_index) + 1];
56 | row_out[column_index].a = jniimg->data[4*(width*row_index + column_index) + 0];
57 | }
58 | }
59 |
60 | static void convert_bgr(liq_color row_out[], int row_index, int width, void* user_info) {
61 | liq_jni_image *jniimg = user_info;
62 | int column_index;
63 | for(column_index=0; column_index < width; column_index++) {
64 | row_out[column_index].r = jniimg->data[3*(width*row_index + column_index) + 2];
65 | row_out[column_index].g = jniimg->data[3*(width*row_index + column_index) + 1];
66 | row_out[column_index].b = jniimg->data[3*(width*row_index + column_index) + 0];
67 | row_out[column_index].a = 255;
68 | }
69 | }
70 |
71 | JNIEXPORT jlong JNICALL Java_org_pngquant_Image_liq_1image_1create(JNIEnv *env, jclass class, jlong attr, jbyteArray bytearray, jint w, jint h, jint components) {
72 | /* liq_image needs to be wrapped to keep track of allocated buffer */
73 | liq_jni_image *jniimg = malloc(sizeof(liq_jni_image));
74 |
75 | /* copying buffer, since ReleaseByteArrayElements was crashing when called from finalize() */
76 | jsize size = (*env)->GetArrayLength(env, bytearray);
77 | jniimg->data = malloc(size);
78 | (*env)->GetByteArrayRegion(env, bytearray, 0, size, jniimg->data);
79 |
80 | jniimg->image = liq_image_create_custom((liq_attr*)attr, components == 4 ? convert_abgr : convert_bgr, jniimg, w, h, 0);
81 |
82 | if (!jniimg->image) {
83 | free(jniimg->data);
84 | free(jniimg);
85 | return 0;
86 | }
87 | return (jlong)jniimg;
88 | }
89 |
90 | JNIEXPORT jboolean JNICALL Java_org_pngquant_Image_addFixedColor(JNIEnv *env, jobject obj, jint r, jint g, jint b, jint a) {
91 | liq_color c = {r,g,b,a};
92 | return LIQ_OK == liq_image_add_fixed_color(((liq_jni_image*)handle(env,obj))->image, c);
93 | }
94 |
95 | JNIEXPORT jint JNICALL Java_org_pngquant_Image_getWidth(JNIEnv *env, jobject obj) {
96 | return liq_image_get_width(((liq_jni_image*)handle(env,obj))->image);
97 | }
98 |
99 | JNIEXPORT jint JNICALL Java_org_pngquant_Image_getHeight(JNIEnv *env, jobject obj) {
100 | return liq_image_get_height(((liq_jni_image*)handle(env,obj))->image);
101 | }
102 |
103 | JNIEXPORT void JNICALL Java_org_pngquant_Image_liq_1image_1destroy(JNIEnv *env, jclass class, jlong handle) {
104 | liq_jni_image *jniimg = (liq_jni_image*)handle;
105 | liq_image_destroy(jniimg->image);
106 | free(jniimg->data);
107 | free(jniimg);
108 | }
109 |
110 | JNIEXPORT jlong JNICALL Java_org_pngquant_Result_liq_1quantize_1image(JNIEnv *env, jclass class, jlong attr, jlong handle) {
111 | return (jlong)liq_quantize_image((liq_attr*)attr, ((liq_jni_image*)handle)->image);
112 | }
113 |
114 | JNIEXPORT jboolean JNICALL Java_org_pngquant_Result_setDitheringLevel(JNIEnv *env, jobject obj, jfloat l) {
115 | return LIQ_OK == liq_set_dithering_level(handle(env, obj), l);
116 | }
117 |
118 | JNIEXPORT jboolean JNICALL Java_org_pngquant_Result_setGamma(JNIEnv *env, jobject obj, jdouble gamma) {
119 | return LIQ_OK == liq_set_output_gamma(handle(env, obj), gamma);
120 | }
121 |
122 | JNIEXPORT jdouble JNICALL Java_org_pngquant_Result_getGamma(JNIEnv *env, jobject obj) {
123 | return liq_get_output_gamma(handle(env, obj));
124 | }
125 |
126 | JNIEXPORT jboolean JNICALL Java_org_pngquant_Result_liq_1write_1remapped_1image(JNIEnv *env, jclass class, jlong result, jlong image_handle, jbyteArray bytearray) {
127 | jsize size = (*env)->GetArrayLength(env, bytearray);
128 |
129 | jbyte *bitmap = (*env)->GetByteArrayElements(env, bytearray, 0);
130 | liq_error err = liq_write_remapped_image((liq_result*)result, ((liq_jni_image*)image_handle)->image, bitmap, size);
131 | (*env)->ReleaseByteArrayElements(env, bytearray, bitmap, 0);
132 |
133 | return LIQ_OK == err;
134 | }
135 |
136 | JNIEXPORT jdouble JNICALL Java_org_pngquant_Result_getMeanSquareError(JNIEnv *env, jobject obj) {
137 | return liq_get_quantization_error(handle(env, obj));
138 | }
139 |
140 | JNIEXPORT jint JNICALL Java_org_pngquant_Result_getQuality(JNIEnv *env, jobject obj) {
141 | return liq_get_quantization_quality(handle(env, obj));
142 | }
143 |
144 | JNIEXPORT void JNICALL Java_org_pngquant_Result_liq_1result_1destroy(JNIEnv *env, jclass class, jlong result) {
145 | return liq_result_destroy((liq_result*)result);
146 | }
147 |
148 | JNIEXPORT jbyteArray JNICALL Java_org_pngquant_Result_liq_1get_1palette(JNIEnv *env, jclass class, jlong result) {
149 | const liq_palette *pal = liq_get_palette((liq_result*)result);
150 | jbyteArray arr = (*env)->NewByteArray(env, pal->count * 4);
151 | int i;
152 | for(i=0; i < pal->count; i++) {
153 | (*env)->SetByteArrayRegion(env, arr, i*4, 4, ((jbyte*)&pal->entries[i]));
154 | }
155 | return arr;
156 | }
157 |
--------------------------------------------------------------------------------
/imagequant-sys/libimagequant.cs:
--------------------------------------------------------------------------------
1 | /*
2 | This is an example demonstrating use of libimagequant from C#.
3 |
4 | This example code can be freely copied under CC0 (public domain) license.
5 | */
6 |
7 | using System;
8 | using System.Collections.Generic;
9 | using System.Runtime.InteropServices;
10 |
11 | [StructLayout(LayoutKind.Sequential)]
12 | struct liq_color
13 | {
14 | public byte r, g, b, a;
15 | };
16 |
17 | [StructLayout(LayoutKind.Sequential)]
18 | struct liq_palette
19 | {
20 | public int count;
21 | [MarshalAs(UnmanagedType.ByValArray, SizeConst = 256)]
22 | public liq_color[] entries;
23 | };
24 |
25 | enum liq_error
26 | {
27 | LIQ_OK = 0,
28 | LIQ_QUALITY_TOO_LOW = 99,
29 | LIQ_VALUE_OUT_OF_RANGE = 100,
30 | LIQ_OUT_OF_MEMORY,
31 | LIQ_ABORTED,
32 | LIQ_BITMAP_NOT_AVAILABLE,
33 | LIQ_BUFFER_TOO_SMALL,
34 | LIQ_INVALID_POINTER,
35 | };
36 |
37 | namespace liq
38 | {
39 | using liq_attr_ptr = IntPtr;
40 | using liq_image_ptr = IntPtr;
41 | using liq_result_ptr = IntPtr;
42 | using size_t = UIntPtr;
43 |
44 | class Liq
45 | {
46 | [DllImport(@"imagequant.dll")]
47 | public static extern liq_attr_ptr liq_attr_create();
48 | [DllImport(@"imagequant.dll")]
49 | public static extern liq_attr_ptr liq_attr_copy(liq_attr_ptr attr);
50 | [DllImport(@"imagequant.dll")]
51 | public static extern void liq_attr_destroy(liq_attr_ptr attr);
52 |
53 | [DllImport(@"imagequant.dll")]
54 | public static extern liq_error liq_set_max_colors(liq_attr_ptr attr, int colors);
55 | [DllImport(@"imagequant.dll")]
56 | public static extern int liq_get_max_colors(liq_attr_ptr attr);
57 | [DllImport(@"imagequant.dll")]
58 | public static extern liq_error liq_set_speed(liq_attr_ptr attr, int speed);
59 | [DllImport(@"imagequant.dll")]
60 | public static extern int liq_get_speed(liq_attr_ptr attr);
61 | [DllImport(@"imagequant.dll")]
62 | public static extern liq_error liq_set_min_opacity(liq_attr_ptr attr, int min);
63 | [DllImport(@"imagequant.dll")]
64 | public static extern int liq_get_min_opacity(liq_attr_ptr attr);
65 | [DllImport(@"imagequant.dll")]
66 | public static extern liq_error liq_set_min_posterization(liq_attr_ptr attr, int bits);
67 | [DllImport(@"imagequant.dll")]
68 | public static extern int liq_get_min_posterization(liq_attr_ptr attr);
69 | [DllImport(@"imagequant.dll")]
70 | public static extern liq_error liq_set_quality(liq_attr_ptr attr, int minimum, int maximum);
71 | [DllImport(@"imagequant.dll")]
72 | public static extern int liq_get_min_quality(liq_attr_ptr attr);
73 | [DllImport(@"imagequant.dll")]
74 | public static extern int liq_get_max_quality(liq_attr_ptr attr);
75 | [DllImport(@"imagequant.dll")]
76 | public static extern void liq_set_last_index_transparent(liq_attr_ptr attr, int is_last);
77 |
78 | [DllImport(@"imagequant.dll")]
79 | public static extern liq_image_ptr liq_image_create_rgba(liq_attr_ptr attr, [In, MarshalAs(UnmanagedType.LPArray)] byte[] bitmap, int width, int height, double gamma);
80 |
81 | [DllImport(@"imagequant.dll")]
82 | public static extern liq_error liq_image_set_memory_ownership(liq_image_ptr image, int ownership_flags);
83 | [DllImport(@"imagequant.dll")]
84 | public static extern liq_error liq_image_add_fixed_color(liq_image_ptr img, liq_color color);
85 | [DllImport(@"imagequant.dll")]
86 | public static extern int liq_image_get_width(liq_image_ptr img);
87 | [DllImport(@"imagequant.dll")]
88 | public static extern int liq_image_get_height(liq_image_ptr img);
89 | [DllImport(@"imagequant.dll")]
90 | public static extern void liq_image_destroy(liq_image_ptr img);
91 |
92 | [DllImport(@"imagequant.dll")]
93 | public static extern liq_result_ptr liq_quantize_image(liq_attr_ptr attr, liq_image_ptr input_image);
94 |
95 | [DllImport(@"imagequant.dll")]
96 | public static extern liq_error liq_set_dithering_level(liq_result_ptr res, float dither_level);
97 | [DllImport(@"imagequant.dll")]
98 | public static extern liq_error liq_set_output_gamma(liq_result_ptr res, double gamma);
99 | [DllImport(@"imagequant.dll")]
100 | public static extern double liq_get_output_gamma(liq_result_ptr res);
101 |
102 | [DllImport(@"imagequant.dll")]
103 | public static extern IntPtr liq_get_palette(liq_result_ptr res);
104 |
105 | [DllImport(@"imagequant.dll")]
106 | public static extern liq_error liq_write_remapped_image(liq_result_ptr res, liq_image_ptr input_image, [Out, MarshalAs(UnmanagedType.LPArray)] byte[] buffer, size_t buffer_size);
107 |
108 | [DllImport(@"imagequant.dll")]
109 | public static extern double liq_get_quantization_error(liq_result_ptr res);
110 | [DllImport(@"imagequant.dll")]
111 | public static extern int liq_get_quantization_quality(liq_result_ptr res);
112 | [DllImport(@"imagequant.dll")]
113 | public static extern double liq_get_remapping_error(liq_result_ptr res);
114 | [DllImport(@"imagequant.dll")]
115 | public static extern int liq_get_remapping_quality(liq_result_ptr res);
116 |
117 | [DllImport(@"imagequant.dll")]
118 | public static extern void liq_result_destroy(liq_result_ptr res);
119 |
120 | [DllImport(@"imagequant.dll")]
121 | public static extern int liq_version();
122 |
123 | static void Main(string[] args)
124 | {
125 | Console.WriteLine("library version: {0}", liq_version());
126 |
127 | int width = 3;
128 | int height = 1;
129 |
130 | var attr = liq_attr_create();
131 | if (attr == IntPtr.Zero) throw new Exception("can't create attr");
132 |
133 | byte[] bitmap = { // R, G, B, A, R, G, B, A, ...
134 | 111, 222, 33, 255,
135 | 255, 0, 255, 255,
136 | 255, 0, 255, 255,
137 | };
138 | var img = liq_image_create_rgba(attr, bitmap, width, height, 0);
139 | if (img == IntPtr.Zero) throw new Exception("can't create image");
140 |
141 | var res = liq_quantize_image(attr, img);
142 | if (res == IntPtr.Zero) throw new Exception("can't quantize image");
143 |
144 | var buffer_size = width * height;
145 | var remapped = new byte[buffer_size];
146 |
147 | var err = liq_write_remapped_image(res, img, remapped, (UIntPtr)buffer_size);
148 | if (err != liq_error.LIQ_OK)
149 | {
150 | throw new Exception("remapping error");
151 | }
152 |
153 | Console.WriteLine("first pixel is {0}th palette entry", remapped[0]);
154 |
155 | liq_palette pal = (liq_palette)Marshal.PtrToStructure(liq_get_palette(res), typeof(liq_palette));
156 |
157 | Console.WriteLine("palette entries: {0}; red of first entry: {1}", pal.count, pal.entries[0].r);
158 |
159 | liq_image_destroy(img);
160 | liq_result_destroy(res);
161 | liq_attr_destroy(attr);
162 | }
163 | }
164 | }
165 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # [libimagequant](https://pngquant.org/lib/) — Image Quantization Library
2 |
3 | Imagequant library converts RGBA images to palette-based 8-bit indexed images, *including* alpha component. It's ideal for generating tiny PNG images and [nice-looking GIFs](https://gif.ski).
4 |
5 | Image encoding/decoding isn't handled by the library itself, bring your own encoder. If you're looking for a command-line tool, see [pngquant](https://pngquant.org).
6 |
7 | ## Getting started in C
8 |
9 | This library can be used in C programs via [imagequant-sys](https://github.com/ImageOptim/libimagequant/tree/main/imagequant-sys) [Rust](https://www.rust-lang.org/) package.
10 |
11 | ```bash
12 | rustup update
13 | git clone https://github.com/ImageOptim/libimagequant
14 | cd libimagequant/imagequant-sys
15 | cargo build --release --target-dir=target
16 | # makes target/release/libimagequant_sys.a
17 | ```
18 |
19 | See [the C library API documentation](https://pngquant.org/lib/) and [the readme](https://lib.rs/imagequant-sys) for more info.
20 |
21 | ## Getting started in Rust
22 |
23 | Add to `Cargo.toml`:
24 |
25 | ```bash
26 | rustup update
27 | cargo add imagequant
28 | ```
29 |
30 | [See docs.rs for the library API documentation](https://docs.rs/imagequant).
31 |
32 | ## License
33 |
34 | Libimagequant is dual-licensed:
35 |
36 | * For Free/Libre Open Source Software it's available under GPL v3 or later with additional [copyright notices](https://raw.github.com/ImageOptim/libimagequant/master/COPYRIGHT) for historical reasons.
37 | * For use in closed-source software, AppStore distribution, and other non-GPL uses, you can [obtain a commercial license](https://supso.org/projects/pngquant). Feel free to ask kornel@pngquant.org for details and custom licensing terms if you need them.
38 |
39 | ## Upgrading instructions
40 |
41 | libimagequant v2 used to be a C library. libimagequant v4 is written entirely in Rust, but still exports the same C interface for C programs. You will need to install Rust 1.70+ to build it, and adjust your build commands. If you do not want to upgrade, you can keep using [the C version of the library](https://github.com/imageoptim/libimagequant/tree/2.x) in the `2.x` branch of the [repo](https://github.com/ImageOptim/libimagequant).
42 |
43 | ### C static library users
44 |
45 | Files for C/C++ are now in the `imagequant-sys/` subdirectory, not in the root of the repo. There is no `configure && make` any more.
46 |
47 | To build the library, install [Rust via rustup](https://rustup.rs), and run:
48 |
49 | ```bash
50 | rustup update
51 | cd imagequant-sys
52 | cargo build --release
53 | ```
54 |
55 | It produces `target/release/libimagequant_sys.a` static library. The API, ABI, and header files remain the same, so everything else should work the same.
56 | If you're building for macOS or iOS, see included xcodeproj file (add it as a [subproject](https://gitlab.com/kornelski/cargo-xcode#usage) to yours).
57 |
58 | If you're building for Android, run `rustup target add aarch64-linux-android; cargo build --release --target aarch64-linux-android` and use `target/aarch64-linux-android/release/libimagequant_sys.a`. Same for cross-compiling to other platforms. See `rustup target list`.
59 |
60 | See [imagequant-sys readme](https://lib.rs/imagequant-sys#readme-building-for-c) for instructions how to make smaller builds.
61 |
62 | ### C dynamic library for package maintainers
63 |
64 | If you're an application developer, please use the static linking option above — that option is much easier, and gives smaller executables.
65 |
66 | The API and ABI of this library remains the same. It has the same sover, so it can be a drop-in replacement for the previous C version.
67 |
68 | This library is now a typical Rust/Cargo library. If you want to set up [off-line builds](https://doc.rust-lang.org/cargo/faq.html#how-can-cargo-work-offline) or [override dependencies](https://doc.rust-lang.org/cargo/reference/overriding-dependencies.html), it works the same as for every other Rust project. See [Cargo docs](https://doc.rust-lang.org/cargo/) for things like [`cargo fetch`](https://doc.rust-lang.org/cargo/commands/cargo-fetch.html) or [`cargo vendor`](https://doc.rust-lang.org/cargo/commands/cargo-vendor.html) (but I don't recommend vendoring).
69 |
70 | If you want to build a dynamic library, but aren't bothered by soname and rpath being wrong, modify `imagequant-sys/Cargo.toml` and add `"cdylib"` to the existing `crate-type` property, and then `cargo build --release` will do its usual half-finished job and build `target/release/libimagequant.{so,dylib,dll}`.
71 |
72 | #### Building with `make`
73 |
74 | `configure && make` is gone. I hoped I could build a dynamic library just by wrapping the static library, but apparently that won't work, so I can't easily recreate the old `make install`. I wish there was a more standard and lightweight solution than using the `cargo-c` tool, so if you're good at wrangling linker flags and symbol visibility, please send pull requests.
75 |
76 | #### Building with `cargo-c`
77 |
78 | The [`cargo-c`](https://lib.rs/cargo-c) tool knows how to build and link so/dylib properly, and generates an accurate pkg-config file, so it's de-facto required for a correct system-wide install of a dynamic library.
79 |
80 | ```bash
81 | rustup update
82 | cd imagequant-sys
83 | cargo install cargo-c
84 | cargo cinstall --prefix=/usr/local --destdir=.
85 | ```
86 |
87 | This makes Rust 1.70 and `cargo-c` package a build-time dependency. No runtime deps (apart from Cargo-internal ones). OpenMP has been dropped entirely.
88 |
89 | #### Interaction with pngquant
90 |
91 | pngquant v2 can use this library as a dynamic library. However, pngquant v4 does not support unbundling. It uses this library as a Cargo dependency via its Rust-native interface. The shared libimagequant library exports only a stable ABI for C programs, and this interface is not useful for Rust programs.
92 |
93 | ### Upgrading for Rust users
94 |
95 | If you've used the [`imagequant-sys`](https://lib.rs/imagequant-sys) crate, switch to the higher-level [`imagequant`](https://lib.rs/imagequant) crate. The `imagequant` v4 is almost entirely backwards-compatible, with small changes that the Rust compiler will point out (e.g. changed use of `c_int` to `u32`). See [docs](https://docs.rs/imagequant). Please fix any deprecation warnings you may get, because the deprecated functions will be removed.
96 |
97 | The `openmp` Cargo feature has been renamed to `threads`.
98 |
99 | `.new_image()` can now take ownership of its argument to avoid copying. If you get an error that `From<&Vec>` is not implemented, then either don't pass by reference (moves, avoids copying), or call `.as_slice()` on it (to copy the pixels), or use `.new_image_borrowed()` method instead.
100 |
101 | ### Threads support and WASM
102 |
103 | By default, when the `threads` Cargo feature is enabled, this library uses multi-threading. Number of threads can be controlled via `RAYON_NUM_THREADS` environment variable.
104 |
105 | Threads in WASM are experimental, and require [special handling](https://github.com/RReverser/wasm-bindgen-rayon). If you're targeting WASM, you'll most likely want to disable threads.
106 |
107 | To disable threads when using this library as a dependency, disable default features like this in `Cargo.toml`:
108 |
109 | ```toml
110 | [dependencies]
111 | imagequant = { version = "4.0", default-features = false }
112 | ```
113 |
114 | When you compile the library directly, add `--no-default-features` flag instead.
115 |
116 |
--------------------------------------------------------------------------------
/imagequant-sys/libimagequant.h:
--------------------------------------------------------------------------------
1 | /*
2 | * https://pngquant.org
3 | */
4 |
5 | #ifndef LIBIMAGEQUANT_H
6 | #define LIBIMAGEQUANT_H
7 |
8 | #ifdef IMAGEQUANT_EXPORTS
9 | #define LIQ_EXPORT __declspec(dllexport)
10 | #endif
11 |
12 | #ifndef LIQ_EXPORT
13 | #define LIQ_EXPORT extern
14 | #endif
15 |
16 | #define LIQ_VERSION 40003
17 | #define LIQ_VERSION_STRING "4.0.3"
18 |
19 | #ifndef LIQ_PRIVATE
20 | #if defined(__GNUC__) || defined (__llvm__)
21 | #define LIQ_PRIVATE __attribute__((visibility("hidden")))
22 | #define LIQ_NONNULL __attribute__((nonnull))
23 | #define LIQ_USERESULT __attribute__((warn_unused_result))
24 | #else
25 | #define LIQ_PRIVATE
26 | #define LIQ_NONNULL
27 | #define LIQ_USERESULT
28 | #endif
29 | #endif
30 |
31 | #ifdef __cplusplus
32 | extern "C" {
33 | #endif
34 |
35 | #include
36 |
37 | typedef struct liq_attr liq_attr;
38 | typedef struct liq_image liq_image;
39 | typedef struct liq_result liq_result;
40 | typedef struct liq_histogram liq_histogram;
41 |
42 | typedef struct liq_color {
43 | unsigned char r, g, b, a;
44 | } liq_color;
45 |
46 | typedef struct liq_palette {
47 | unsigned int count;
48 | liq_color entries[256];
49 | } liq_palette;
50 |
51 | typedef enum liq_error {
52 | LIQ_OK = 0,
53 | LIQ_QUALITY_TOO_LOW = 99,
54 | LIQ_VALUE_OUT_OF_RANGE = 100,
55 | LIQ_OUT_OF_MEMORY,
56 | LIQ_ABORTED,
57 | LIQ_BITMAP_NOT_AVAILABLE,
58 | LIQ_BUFFER_TOO_SMALL,
59 | LIQ_INVALID_POINTER,
60 | LIQ_UNSUPPORTED,
61 | } liq_error;
62 |
63 | enum liq_ownership {
64 | LIQ_OWN_ROWS=4,
65 | LIQ_OWN_PIXELS=8,
66 | LIQ_COPY_PIXELS=16,
67 | };
68 |
69 | typedef struct liq_histogram_entry {
70 | liq_color color;
71 | unsigned int count;
72 | } liq_histogram_entry;
73 |
74 | LIQ_EXPORT LIQ_USERESULT liq_attr* liq_attr_create(void);
75 | LIQ_EXPORT LIQ_USERESULT liq_attr* liq_attr_create_with_allocator(void* removed, void *unsupported);
76 | LIQ_EXPORT LIQ_USERESULT liq_attr* liq_attr_copy(const liq_attr *orig) LIQ_NONNULL;
77 | LIQ_EXPORT void liq_attr_destroy(liq_attr *attr) LIQ_NONNULL;
78 |
79 | LIQ_EXPORT LIQ_USERESULT liq_histogram* liq_histogram_create(const liq_attr* attr);
80 | LIQ_EXPORT liq_error liq_histogram_add_image(liq_histogram *hist, const liq_attr *attr, liq_image* image) LIQ_NONNULL;
81 | LIQ_EXPORT liq_error liq_histogram_add_colors(liq_histogram *hist, const liq_attr *attr, const liq_histogram_entry entries[], int num_entries, double gamma) LIQ_NONNULL;
82 | LIQ_EXPORT liq_error liq_histogram_add_fixed_color(liq_histogram *hist, liq_color color, double gamma) LIQ_NONNULL;
83 | LIQ_EXPORT void liq_histogram_destroy(liq_histogram *hist) LIQ_NONNULL;
84 |
85 | LIQ_EXPORT liq_error liq_set_max_colors(liq_attr* attr, int colors) LIQ_NONNULL;
86 | LIQ_EXPORT LIQ_USERESULT int liq_get_max_colors(const liq_attr* attr) LIQ_NONNULL;
87 | LIQ_EXPORT liq_error liq_set_speed(liq_attr* attr, int speed) LIQ_NONNULL;
88 | LIQ_EXPORT LIQ_USERESULT int liq_get_speed(const liq_attr* attr) LIQ_NONNULL;
89 | LIQ_EXPORT liq_error liq_set_min_opacity(liq_attr* attr, int min) LIQ_NONNULL;
90 | LIQ_EXPORT LIQ_USERESULT int liq_get_min_opacity(const liq_attr* attr) LIQ_NONNULL;
91 | LIQ_EXPORT liq_error liq_set_min_posterization(liq_attr* attr, int bits) LIQ_NONNULL;
92 | LIQ_EXPORT LIQ_USERESULT int liq_get_min_posterization(const liq_attr* attr) LIQ_NONNULL;
93 | LIQ_EXPORT liq_error liq_set_quality(liq_attr* attr, int minimum, int maximum) LIQ_NONNULL;
94 | LIQ_EXPORT LIQ_USERESULT int liq_get_min_quality(const liq_attr* attr) LIQ_NONNULL;
95 | LIQ_EXPORT LIQ_USERESULT int liq_get_max_quality(const liq_attr* attr) LIQ_NONNULL;
96 | LIQ_EXPORT void liq_set_last_index_transparent(liq_attr* attr, int is_last) LIQ_NONNULL;
97 |
98 | typedef void liq_log_callback_function(const liq_attr*, const char *message, void* user_info);
99 | typedef void liq_log_flush_callback_function(const liq_attr*, void* user_info);
100 | LIQ_EXPORT void liq_set_log_callback(liq_attr*, liq_log_callback_function*, void* user_info);
101 | LIQ_EXPORT void liq_set_log_flush_callback(liq_attr*, liq_log_flush_callback_function*, void* user_info);
102 |
103 | typedef int liq_progress_callback_function(float progress_percent, void* user_info);
104 | LIQ_EXPORT void liq_attr_set_progress_callback(liq_attr*, liq_progress_callback_function*, void* user_info);
105 | LIQ_EXPORT void liq_result_set_progress_callback(liq_result*, liq_progress_callback_function*, void* user_info);
106 |
107 | // The rows and their data are not modified. The type of `rows` is non-const only due to a bug in C's typesystem design.
108 | LIQ_EXPORT LIQ_USERESULT liq_image *liq_image_create_rgba_rows(const liq_attr *attr, void *const rows[], int width, int height, double gamma) LIQ_NONNULL;
109 | LIQ_EXPORT LIQ_USERESULT liq_image *liq_image_create_rgba(const liq_attr *attr, const void *bitmap, int width, int height, double gamma) LIQ_NONNULL;
110 |
111 | typedef void liq_image_get_rgba_row_callback(liq_color row_out[], int row, int width, void* user_info);
112 | LIQ_EXPORT LIQ_USERESULT liq_image *liq_image_create_custom(const liq_attr *attr, liq_image_get_rgba_row_callback *row_callback, void* user_info, int width, int height, double gamma);
113 |
114 | LIQ_EXPORT liq_error liq_image_set_memory_ownership(liq_image *image, int ownership_flags) LIQ_NONNULL;
115 | LIQ_EXPORT liq_error liq_image_set_background(liq_image *img, liq_image *background_image) LIQ_NONNULL;
116 | LIQ_EXPORT liq_error liq_image_set_importance_map(liq_image *img, unsigned char buffer[], size_t buffer_size, enum liq_ownership memory_handling) LIQ_NONNULL;
117 | LIQ_EXPORT liq_error liq_image_add_fixed_color(liq_image *img, liq_color color) LIQ_NONNULL;
118 | LIQ_EXPORT LIQ_USERESULT int liq_image_get_width(const liq_image *img) LIQ_NONNULL;
119 | LIQ_EXPORT LIQ_USERESULT int liq_image_get_height(const liq_image *img) LIQ_NONNULL;
120 | LIQ_EXPORT void liq_image_destroy(liq_image *img) LIQ_NONNULL;
121 |
122 | LIQ_EXPORT LIQ_USERESULT liq_error liq_histogram_quantize(liq_histogram *const input_hist, liq_attr *const options, liq_result **result_output) LIQ_NONNULL;
123 | LIQ_EXPORT LIQ_USERESULT liq_error liq_image_quantize(liq_image *const input_image, liq_attr *const options, liq_result **result_output) LIQ_NONNULL;
124 | LIQ_EXPORT LIQ_USERESULT liq_error liq_result_from_palette(const liq_attr *options, const liq_color *palette, unsigned int palette_size, double gamma, liq_result **result_output) LIQ_NONNULL;
125 |
126 | LIQ_EXPORT liq_error liq_set_dithering_level(liq_result *res, float dither_level) LIQ_NONNULL;
127 | LIQ_EXPORT liq_error liq_set_output_gamma(liq_result* res, double gamma) LIQ_NONNULL;
128 | LIQ_EXPORT LIQ_USERESULT double liq_get_output_gamma(const liq_result *result) LIQ_NONNULL;
129 |
130 | LIQ_EXPORT LIQ_USERESULT const liq_palette *liq_get_palette(liq_result *result) LIQ_NONNULL;
131 |
132 | LIQ_EXPORT liq_error liq_write_remapped_image(liq_result *result, liq_image *input_image, void *buffer, size_t buffer_size) LIQ_NONNULL;
133 | LIQ_EXPORT liq_error liq_write_remapped_image_rows(liq_result *result, liq_image *input_image, unsigned char **row_pointers) LIQ_NONNULL;
134 |
135 | LIQ_EXPORT double liq_get_quantization_error(const liq_result *result) LIQ_NONNULL;
136 | LIQ_EXPORT int liq_get_quantization_quality(const liq_result *result) LIQ_NONNULL;
137 | LIQ_EXPORT double liq_get_remapping_error(const liq_result *result) LIQ_NONNULL;
138 | LIQ_EXPORT int liq_get_remapping_quality(const liq_result *result) LIQ_NONNULL;
139 |
140 | LIQ_EXPORT void liq_result_destroy(liq_result *) LIQ_NONNULL;
141 |
142 | LIQ_EXPORT int liq_version(void);
143 |
144 |
145 | // Deprecated
146 | LIQ_EXPORT LIQ_USERESULT liq_result *liq_quantize_image(liq_attr *options, liq_image *input_image) LIQ_NONNULL;
147 |
148 | #ifdef __cplusplus
149 | }
150 | #endif
151 |
152 | #endif
153 |
--------------------------------------------------------------------------------
/src/nearest.rs:
--------------------------------------------------------------------------------
1 | use crate::pal::{f_pixel, PalF, PalIndex, MAX_COLORS};
2 | use crate::{Error, OrdFloat};
3 |
4 | #[cfg(all(not(feature = "std"), feature = "no_std"))]
5 | use crate::no_std_compat::*;
6 |
7 | impl<'pal> Nearest<'pal> {
8 | #[inline(never)]
9 | pub fn new(palette: &'pal PalF) -> Result {
10 | if palette.len() > PalIndex::MAX as usize + 1 {
11 | return Err(Error::Unsupported);
12 | }
13 | let mut indexes: Vec<_> = (0..palette.len())
14 | .map(|idx| MapIndex { idx: idx as _ })
15 | .collect();
16 | if indexes.is_empty() {
17 | return Err(Error::Unsupported);
18 | }
19 | let mut handle = Nearest {
20 | root: vp_create_node(&mut indexes, palette),
21 | palette,
22 | nearest_other_color_dist: [0.; MAX_COLORS],
23 | };
24 | for (i, color) in palette.as_slice().iter().enumerate() {
25 | let mut best = Visitor {
26 | idx: 0,
27 | distance: f32::MAX,
28 | distance_squared: f32::MAX,
29 | exclude: Some(i as PalIndex),
30 | };
31 | vp_search_node(&handle.root, color, &mut best);
32 | handle.nearest_other_color_dist[i] = best.distance_squared / 4.;
33 | }
34 | Ok(handle)
35 | }
36 | }
37 |
38 | impl Nearest<'_> {
39 | #[inline]
40 | pub fn search(&self, px: &f_pixel, likely_colormap_index: PalIndex) -> (PalIndex, f32) {
41 | // The index may be invalid, so it needs to be checked
42 | let mut best_candidate = if let Some(pal_px) = self.palette.as_slice().get(likely_colormap_index as usize) {
43 | let guess_diff = px.diff(pal_px);
44 | if guess_diff < self.nearest_other_color_dist[likely_colormap_index as usize] {
45 | return (likely_colormap_index, guess_diff);
46 | }
47 | Visitor {
48 | distance: guess_diff.sqrt(),
49 | distance_squared: guess_diff,
50 | idx: likely_colormap_index,
51 | exclude: None,
52 | }
53 | } else {
54 | Visitor {
55 | distance: f32::INFINITY,
56 | distance_squared: f32::INFINITY,
57 | idx: 0,
58 | exclude: None,
59 | }
60 | };
61 |
62 | vp_search_node(&self.root, px, &mut best_candidate);
63 | (best_candidate.idx, best_candidate.distance_squared)
64 | }
65 | }
66 |
67 | pub(crate) struct Nearest<'pal> {
68 | root: Node,
69 | palette: &'pal PalF,
70 | nearest_other_color_dist: [f32; MAX_COLORS],
71 | }
72 |
73 | pub struct MapIndex {
74 | pub idx: PalIndex,
75 | }
76 |
77 | pub struct Visitor {
78 | pub distance: f32,
79 | pub distance_squared: f32,
80 | pub idx: PalIndex,
81 | pub exclude: Option,
82 | }
83 |
84 | impl Visitor {
85 | #[inline]
86 | fn visit(&mut self, distance: f32, distance_squared: f32, idx: PalIndex) {
87 | if distance_squared < self.distance_squared && self.exclude != Some(idx) {
88 | self.distance = distance;
89 | self.distance_squared = distance_squared;
90 | self.idx = idx;
91 | }
92 | }
93 | }
94 |
95 | pub(crate) struct Node {
96 | vantage_point: f_pixel,
97 | inner: NodeInner,
98 | idx: PalIndex,
99 | }
100 |
101 | const LEAF_MAX_SIZE: usize = 6;
102 |
103 | enum NodeInner {
104 | Nodes {
105 | radius: f32,
106 | radius_squared: f32,
107 | near: Box,
108 | far: Box,
109 | },
110 | Leaf {
111 | len: u8,
112 | idxs: [PalIndex; LEAF_MAX_SIZE],
113 | colors: Box<[f_pixel; LEAF_MAX_SIZE]>,
114 | },
115 | }
116 |
117 | #[inline(never)]
118 | fn vp_create_node(indexes: &mut [MapIndex], items: &PalF) -> Node {
119 | debug_assert!(!indexes.is_empty());
120 | let palette = items.as_slice();
121 |
122 | if indexes.len() <= 1 {
123 | let idx = indexes.first().map(|i| i.idx).unwrap_or_default();
124 | return Node {
125 | vantage_point: palette.get(usize::from(idx)).copied().unwrap_or_default(),
126 | idx,
127 | inner: NodeInner::Leaf { len: 0, idxs: [0; LEAF_MAX_SIZE], colors: Box::new([f_pixel::default(); LEAF_MAX_SIZE]) },
128 | };
129 | }
130 |
131 | let most_popular_item = indexes.iter().enumerate().max_by_key(move |(_, idx)| {
132 | OrdFloat::new(items.pop_as_slice().get(usize::from(idx.idx))
133 | .map(|p| p.popularity()).unwrap_or_default())
134 | }).map(|(n, _)| n).unwrap_or_default();
135 | indexes.swap(most_popular_item, 0);
136 | let (ref_, indexes) = indexes.split_first_mut().unwrap();
137 |
138 | let vantage_point = palette.get(usize::from(ref_.idx)).copied().unwrap_or_default();
139 | indexes.sort_by_cached_key(move |i| {
140 | OrdFloat::new(palette.get(usize::from(i.idx))
141 | .map(|px| vantage_point.diff(px)).unwrap_or_default())
142 | });
143 |
144 | let num_indexes = indexes.len();
145 |
146 | let inner = if num_indexes <= LEAF_MAX_SIZE {
147 | let mut colors = [f_pixel::default(); LEAF_MAX_SIZE];
148 | let mut idxs = [Default::default(); LEAF_MAX_SIZE];
149 |
150 | indexes.iter().zip(colors.iter_mut().zip(idxs.iter_mut())).for_each(|(i, (color, idx))| {
151 | if let Some(c) = palette.get(usize::from(i.idx)) {
152 | *idx = i.idx;
153 | *color = *c;
154 | }
155 | });
156 | NodeInner::Leaf {
157 | len: num_indexes as _,
158 | idxs,
159 | colors: Box::new(colors),
160 | }
161 | } else {
162 | let half_index = num_indexes / 2;
163 | let (near, far) = indexes.split_at_mut(half_index);
164 | debug_assert!(!near.is_empty());
165 | debug_assert!(!far.is_empty());
166 | let radius_squared = palette.get(usize::from(far[0].idx))
167 | .map(|px| vantage_point.diff(px)).unwrap_or_default();
168 | let radius = radius_squared.sqrt();
169 | NodeInner::Nodes {
170 | radius, radius_squared,
171 | near: Box::new(vp_create_node(near, items)),
172 | far: Box::new(vp_create_node(far, items)),
173 | }
174 | };
175 |
176 | Node {
177 | inner,
178 | vantage_point,
179 | idx: ref_.idx,
180 | }
181 | }
182 |
183 | #[inline(never)]
184 | fn vp_search_node(mut node: &Node, needle: &f_pixel, best_candidate: &mut Visitor) {
185 | loop {
186 | let distance_squared = node.vantage_point.diff(needle);
187 | let distance = distance_squared.sqrt();
188 |
189 | best_candidate.visit(distance, distance_squared, node.idx);
190 |
191 | match node.inner {
192 | NodeInner::Nodes { radius, radius_squared, ref near, ref far } => {
193 | // Recurse towards most likely candidate first to narrow best candidate's distance as soon as possible
194 | if distance_squared < radius_squared {
195 | vp_search_node(near, needle, best_candidate);
196 | // The best node (final answer) may be just ouside the radius, but not farther than
197 | // the best distance we know so far. The vp_search_node above should have narrowed
198 | // best_candidate->distance, so this path is rarely taken.
199 | if distance >= radius - best_candidate.distance {
200 | node = far;
201 | continue;
202 | }
203 | } else {
204 | vp_search_node(far, needle, best_candidate);
205 | if distance <= radius + best_candidate.distance {
206 | node = near;
207 | continue;
208 | }
209 | }
210 | break;
211 | },
212 | NodeInner::Leaf { len: num, ref idxs, ref colors } => {
213 | colors.iter().zip(idxs.iter().copied()).take(num as usize).for_each(|(color, idx)| {
214 | let distance_squared = color.diff(needle);
215 | best_candidate.visit(distance_squared.sqrt(), distance_squared, idx);
216 | });
217 | break;
218 | },
219 | }
220 | }
221 | }
222 |
--------------------------------------------------------------------------------
/src/rows.rs:
--------------------------------------------------------------------------------
1 | use crate::error::Error;
2 | use crate::pal::{f_pixel, gamma_lut, RGBA};
3 | use crate::seacow::{Pointer, SeaCow};
4 | use crate::LIQ_HIGH_MEMORY_LIMIT;
5 | use core::mem::{size_of, MaybeUninit};
6 | use core::slice;
7 |
8 | #[cfg(all(not(feature = "std"), feature = "no_std"))]
9 | use std::{boxed::Box, vec::Vec};
10 |
11 | pub(crate) type RowCallback<'a> = dyn Fn(&mut [MaybeUninit], usize) + Send + Sync + 'a;
12 |
13 | pub(crate) enum PixelsSource<'pixels, 'rows> {
14 | /// The `pixels` field is never read, but it is used to store the rows.
15 | #[allow(dead_code)]
16 | Pixels {
17 | rows: SeaCow<'rows, Pointer>,
18 | pixels: Option>,
19 | },
20 | Callback(Box>),
21 | }
22 |
23 | impl<'pixels> PixelsSource<'pixels, '_> {
24 | pub(crate) fn for_pixels(pixels: SeaCow<'pixels, RGBA>, width: u32, height: u32, stride: u32) -> Result {
25 | if stride < width || height == 0 || width == 0 {
26 | return Err(Error::ValueOutOfRange);
27 | }
28 | let stride = stride as usize;
29 | let width = width as usize;
30 | let height = height as usize;
31 |
32 | let slice = pixels.as_slice();
33 | let min_area = stride.checked_mul(height).and_then(|a| a.checked_add(width)).ok_or(Error::ValueOutOfRange)? - stride;
34 | if slice.len() < min_area {
35 | return Err(Error::BufferTooSmall);
36 | }
37 |
38 | let rows = SeaCow::boxed(slice.chunks(stride).map(|row| Pointer(row.as_ptr())).take(height).collect());
39 | Ok(Self::Pixels { rows, pixels: Some(pixels) })
40 | }
41 | }
42 |
43 | pub(crate) struct DynamicRows<'pixels, 'rows> {
44 | pub(crate) width: u32,
45 | pub(crate) height: u32,
46 | f_pixels: Option>,
47 | pixels: PixelsSource<'pixels, 'rows>,
48 | pub(crate) gamma: f64,
49 | }
50 |
51 | impl Clone for DynamicRows<'_, '_> {
52 | fn clone(&self) -> Self {
53 | Self {
54 | width: self.width,
55 | height: self.height,
56 | f_pixels: self.f_pixels.clone(),
57 | pixels: match &self.pixels {
58 | PixelsSource::Pixels { rows, pixels } => PixelsSource::Pixels {
59 | rows: rows.clone(),
60 | pixels: pixels.clone(),
61 | },
62 | PixelsSource::Callback(_) => {
63 | let area = self.width as usize * self.height as usize;
64 | let mut out = Vec::with_capacity(area);
65 | let out_rows = out.spare_capacity_mut()[..area].chunks_exact_mut(self.width as usize);
66 | for (i, row) in out_rows.enumerate() {
67 | self.row_rgba(row, i);
68 | }
69 | unsafe {
70 | out.set_len(area);
71 | }
72 | let pixels = SeaCow::boxed(out.into_boxed_slice());
73 | PixelsSource::for_pixels(pixels, self.width, self.height, self.width).unwrap()
74 | },
75 | },
76 | gamma: self.gamma,
77 | }
78 | }
79 | }
80 |
81 | pub(crate) struct DynamicRowsIter<'parent, 'pixels, 'rows> {
82 | px: &'parent DynamicRows<'pixels, 'rows>,
83 | temp_f_row: Option]>>,
84 | }
85 |
86 | impl DynamicRowsIter<'_, '_, '_> {
87 | #[must_use]
88 | pub fn row_f<'px>(&'px mut self, temp_row: &mut [MaybeUninit], row: usize) -> &'px [f_pixel] {
89 | debug_assert_eq!(temp_row.len(), self.px.width as usize);
90 | if let Some(pixels) = self.px.f_pixels.as_ref() {
91 | let start = self.px.width as usize * row;
92 | &pixels[start..start + self.px.width as usize]
93 | } else {
94 | let lut = gamma_lut(self.px.gamma);
95 | let row_pixels = self.px.row_rgba(temp_row, row);
96 |
97 | match self.temp_f_row.as_mut() {
98 | Some(t) => DynamicRows::convert_row_to_f(t, row_pixels, &lut),
99 | None => &mut [], // this can't happen
100 | }
101 | }
102 | }
103 |
104 | #[must_use]
105 | pub fn row_f_shared<'px>(&'px self, temp_row: &mut [MaybeUninit], temp_row_f: &'px mut [MaybeUninit], row: usize) -> &'px [f_pixel] {
106 | if let Some(pixels) = self.px.f_pixels.as_ref() {
107 | &pixels[self.px.width as usize * row..]
108 | } else {
109 | let lut = gamma_lut(self.px.gamma);
110 | let row_pixels = self.px.row_rgba(temp_row, row);
111 |
112 | DynamicRows::convert_row_to_f(temp_row_f, row_pixels, &lut)
113 | }
114 | }
115 |
116 | #[must_use]
117 | pub fn row_rgba<'px>(&'px self, temp_row: &'px mut [MaybeUninit], row: usize) -> &'px [RGBA] {
118 | self.px.row_rgba(temp_row, row)
119 | }
120 | }
121 |
122 | impl<'pixels, 'rows> DynamicRows<'pixels, 'rows> {
123 | #[inline]
124 | pub(crate) fn new(width: u32, height: u32, pixels: PixelsSource<'pixels, 'rows>, gamma: f64) -> Self {
125 | debug_assert!(gamma > 0.);
126 | Self { width, height, f_pixels: None, pixels, gamma }
127 | }
128 |
129 | fn row_rgba<'px>(&'px self, temp_row: &'px mut [MaybeUninit], row: usize) -> &'px [RGBA] {
130 | match &self.pixels {
131 | PixelsSource::Pixels { rows, .. } => unsafe {
132 | slice::from_raw_parts(rows.as_slice()[row].0, self.width())
133 | },
134 | PixelsSource::Callback(cb) => {
135 | cb(temp_row, row);
136 | // cb needs to be marked as unsafe, since it's responsible for initialization :(
137 | unsafe { slice_assume_init_mut(temp_row) }
138 | },
139 | }
140 | }
141 |
142 | fn convert_row_to_f<'f>(row_f_pixels: &'f mut [MaybeUninit], row_pixels: &[RGBA], gamma_lut: &[f32; 256]) -> &'f mut [f_pixel] {
143 | assert_eq!(row_f_pixels.len(), row_pixels.len());
144 | for (dst, src) in row_f_pixels.iter_mut().zip(row_pixels) {
145 | dst.write(f_pixel::from_rgba(gamma_lut, *src));
146 | }
147 | // Safe, just initialized
148 | unsafe { slice_assume_init_mut(row_f_pixels) }
149 | }
150 |
151 | #[must_use]
152 | fn should_use_low_memory(&self) -> bool {
153 | self.width() * self.height() > LIQ_HIGH_MEMORY_LIMIT / size_of::()
154 | }
155 |
156 | #[inline]
157 | fn temp_f_row_for_iter(&self) -> Result