├── .github └── workflows │ └── ci.yml ├── .gitignore ├── CHANGELOG ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── COPYRIGHT ├── Cargo.toml ├── README.md ├── benches └── bench.rs ├── examples └── basic.rs ├── imagequant-sys ├── .gitignore ├── COPYRIGHT ├── Cargo.toml ├── Makefile ├── README.md ├── build.rs ├── c_test │ ├── Cargo.toml │ ├── build.rs │ ├── src │ │ └── lib.rs │ └── test.c ├── example.c ├── imagequant.pc.in ├── imagequant.xcodeproj │ └── project.pbxproj ├── libimagequant.cs ├── libimagequant.h ├── org │ └── pngquant │ │ ├── Image.java │ │ ├── LiqObject.java │ │ ├── PngQuant.c │ │ ├── PngQuant.java │ │ ├── PngQuantException.java │ │ └── Result.java ├── pom.xml └── src │ └── ffi.rs └── src ├── attr.rs ├── blur.rs ├── capi.rs ├── error.rs ├── hist.rs ├── image.rs ├── kmeans.rs ├── lib.rs ├── mediancut.rs ├── nearest.rs ├── pal.rs ├── quant.rs ├── rayoff.rs ├── remap.rs ├── rows.rs └── seacow.rs /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | version 4.2 2 | ----------- 3 | - rewritten and improved handling of fixed palette colors 4 | - support for palettes larger than 256 colors 5 | - fix for remapping when importance_map has lots of pixels with 0 importance 6 | 7 | version 4.1 8 | ----------- 9 | - improved dithering over preset background 10 | - remap_into_vec method 11 | - fix for images over 16 megapixels 12 | 13 | version 4.0 14 | ----------- 15 | - rewritten in Rust 16 | - replaced Makefiles with Cargo 17 | 18 | version 2.17 19 | ------------ 20 | - quality improvement 21 | - ARM64 build fix 22 | 23 | version 2.16 24 | ------------ 25 | - fixed LCMS2 error handling 26 | 27 | version 2.15 28 | ------------ 29 | - speed and quality improvements 30 | 31 | version 2.14 32 | ------------ 33 | - improved Rust API 34 | - quality improvements for remapping overlays over a background 35 | 36 | version 2.13 37 | ------------ 38 | - support OpenMP in clang 39 | - dropped old Internet Explorer workarounds 40 | - speed and quality improvements 41 | 42 | version 2.12 43 | ------------ 44 | - new liq_histogram_add_fixed_color() 45 | - faster for large/complex images 46 | - workarounds for Microsoft's outdated C compiler 47 | 48 | version 2.11 49 | ------------ 50 | - new liq_image_set_background() for high-quality remapping of GIF frames 51 | - new liq_image_set_importance_map() for controlling which parts of the image get more palette colors 52 | - improved OpenMP support 53 | 54 | version 2.10 55 | ----------- 56 | - supports building with Rust/Cargo 57 | 58 | version 2.9 59 | ----------- 60 | - new liq_histogram_add_colors() 61 | 62 | version 2.8 63 | ----------- 64 | - standalone version 65 | - added Java interface (JNI) 66 | - new API for remapping multiple images to a single shared palette 67 | 68 | version 2.7 69 | ----------- 70 | - improved dithering of saturated and semitransparent colors 71 | - libimagequant reports detailed progress and supports aborting of operations via callbacks 72 | - fixed order of log output when using openmp 73 | 74 | version 2.5 75 | ----------- 76 | - replaced color search algorithm with vantage point tree, which is much faster and more reliable 77 | - deprecated IE6 workaround 78 | - warn when compiled without color profile support 79 | - improved support for predefined colors in libimagequant 80 | 81 | version 2.4 82 | ----------- 83 | - fixed remapping of bright colors when dithering 84 | - added libimagequant API to add fixed preset colors to the palette 85 | 86 | version 2.3 87 | ----------- 88 | - added ./configure script for better support of Intel C compiler and dependencies [thanks to pdknsk] 89 | - tweaked quality metric to better estimate quality of images with large solid backgrounds [thanks to Rolf Timmermans] 90 | - avoid applying quality setting to images that use palette already 91 | 92 | version 2.2 93 | ----------- 94 | - OpenMP acceleration 95 | - improved support for Intel C Compiler, speedup in 32-bit GCC, and some workarounds for Visual Studio's incomplete C support 96 | 97 | version 2.1 98 | ----------- 99 | - option to generate posterized output (for use with 16-bit textures) 100 | 101 | version 2.0 102 | ----------- 103 | - refactored codebase into pngquant and standalone libimagequant 104 | - reduced memory usage by further 30% (and more for very large images) 105 | - less precise remapping improving speed by 25% in higher speed settings 106 | - fixed regression in dithering of alpha channel 107 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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.3.4" 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"] 19 | threads = ["dep:rayon", "dep:thread_local"] 20 | # supports up to 2048 colors for palettes, but NOT FOR REMAPPING 21 | large_palettes = [] 22 | 23 | # this is private and unstable for imagequant-sys only, do not use 24 | _internal_c_ffi = [] 25 | 26 | [profile.release] 27 | debug = false 28 | panic = "abort" 29 | 30 | [lib] 31 | doctest = false 32 | 33 | [dependencies] 34 | arrayvec = "0.7.4" 35 | rgb = { version = "0.8.47", default-features = false, features = ["bytemuck"] } 36 | rayon = { version = "1.10.0", optional = true } 37 | thread_local = { version = "1.1.8", optional = true } 38 | once_cell = "1.19.0" 39 | 40 | [dev-dependencies] 41 | lodepng = "3.10" 42 | 43 | [workspace] 44 | members = ["imagequant-sys", "imagequant-sys/c_test"] 45 | 46 | [package.metadata.release] 47 | consolidate-commits = true 48 | tag-message = "" 49 | tag-prefix = "" 50 | tag-name = "{{version}}" 51 | -------------------------------------------------------------------------------- /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 imagequant-sys 15 | cargo build --release 16 | # makes target/release/libimagequant_sys.a 17 | ``` 18 | 19 | See [the C library documentation for more details](https://pngquant.org/lib/). 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 | ### C dynamic library for package maintainers 61 | 62 | If you're an application developer, please use the static linking option above — that option is much easier, and gives smaller executables. 63 | 64 | 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. 65 | 66 | 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). 67 | 68 | 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}`. 69 | 70 | #### Building with `make` 71 | 72 | `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. 73 | 74 | #### Building with `cargo-c` 75 | 76 | 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. 77 | 78 | ```bash 79 | rustup update 80 | cd imagequant-sys 81 | cargo install cargo-c 82 | cargo cinstall --prefix=/usr/local --destdir=. 83 | ``` 84 | 85 | 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. 86 | 87 | #### Interaction with pngquant 88 | 89 | 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. 90 | 91 | ### Upgrading for Rust users 92 | 93 | 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. 94 | 95 | The `openmp` Cargo feature has been renamed to `threads`. 96 | 97 | `.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. 98 | 99 | ### Threads support and WASM 100 | 101 | 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. 102 | 103 | Threads in WASM are experimental, and require [special handling](https://github.com/GoogleChromeLabs/wasm-bindgen-rayon). If you're targeting WASM, you'll most likely want to disable threads. 104 | 105 | To disable threads when using this library as a dependency, disable default features like this in `Cargo.toml`: 106 | 107 | ```toml 108 | [dependencies] 109 | imagequant = { version = "4.0", default-features = false } 110 | ``` 111 | 112 | When you compile the library directly, add `--no-default-features` flag instead. 113 | 114 | -------------------------------------------------------------------------------- /benches/bench.rs: -------------------------------------------------------------------------------- 1 | #![feature(test)] 2 | 3 | extern crate test; 4 | use test::Bencher; 5 | 6 | use imagequant::*; 7 | 8 | #[bench] 9 | fn histogram(b: &mut Bencher) { 10 | let img = lodepng::decode32_file("/Users/kornel/Desktop/canvas.png").unwrap(); 11 | let liq = Attributes::new(); 12 | b.iter(move || { 13 | let mut img = liq.new_image(&*img.buffer, img.width, img.height, 0.).unwrap(); 14 | let mut hist = Histogram::new(&liq); 15 | hist.add_image(&liq, &mut img).unwrap(); 16 | }); 17 | } 18 | 19 | #[bench] 20 | fn remap_ord(b: &mut Bencher) { 21 | let img = lodepng::decode32_file("/Users/kornel/Desktop/canvas.png").unwrap(); 22 | let mut buf = vec![std::mem::MaybeUninit::uninit(); img.width * img.height]; 23 | let mut liq = Attributes::new(); 24 | liq.set_speed(10).unwrap(); 25 | let mut img = liq.new_image(img.buffer, img.width, img.height, 0.).unwrap(); 26 | liq.set_max_colors(256).unwrap(); 27 | let mut res = liq.quantize(&mut img).unwrap(); 28 | res.set_dithering_level(0.).unwrap(); 29 | b.iter(move || { 30 | res.remap_into(&mut img, &mut buf).unwrap(); 31 | res.remap_into(&mut img, &mut buf).unwrap(); 32 | }); 33 | } 34 | 35 | #[bench] 36 | fn kmeans(b: &mut Bencher) { 37 | b.iter(_unstable_internal_kmeans_bench()); 38 | } 39 | 40 | #[bench] 41 | fn remap_floyd(b: &mut Bencher) { 42 | let img = lodepng::decode32_file("/Users/kornel/Desktop/canvas.png").unwrap(); 43 | let mut buf = vec![std::mem::MaybeUninit::uninit(); img.width * img.height]; 44 | let mut liq = Attributes::new(); 45 | liq.set_speed(10).unwrap(); 46 | let mut img = liq.new_image(img.buffer, img.width, img.height, 0.).unwrap(); 47 | let mut res = liq.quantize(&mut img).unwrap(); 48 | res.set_dithering_level(1.).unwrap(); 49 | b.iter(move || { 50 | res.remap_into(&mut img, &mut buf).unwrap(); 51 | res.remap_into(&mut img, &mut buf).unwrap(); 52 | }); 53 | } 54 | 55 | #[bench] 56 | fn quantize_s8(b: &mut Bencher) { 57 | let img = lodepng::decode32_file("/Users/kornel/Desktop/canvas.png").unwrap(); 58 | let mut liq = Attributes::new(); 59 | liq.set_speed(8).unwrap(); 60 | b.iter(move || { 61 | let mut img = liq.new_image(&*img.buffer, img.width, img.height, 0.).unwrap(); 62 | liq.quantize(&mut img).unwrap(); 63 | }); 64 | } 65 | 66 | #[bench] 67 | fn quantize_s1(b: &mut Bencher) { 68 | let img = lodepng::decode32_file("/Users/kornel/Desktop/canvas.png").unwrap(); 69 | let mut liq = Attributes::new(); 70 | liq.set_speed(1).unwrap(); 71 | b.iter(move || { 72 | let mut img = liq.new_image(&*img.buffer, img.width, img.height, 0.).unwrap(); 73 | liq.quantize(&mut img).unwrap(); 74 | }); 75 | } 76 | -------------------------------------------------------------------------------- /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!("Done! Got palette {palette:?} and {} pixels with {}% quality", pixels.len(), res.quantization_quality().unwrap()); 30 | } 31 | -------------------------------------------------------------------------------- /imagequant-sys/.gitignore: -------------------------------------------------------------------------------- 1 | usr/ 2 | target/ 3 | -------------------------------------------------------------------------------- /imagequant-sys/COPYRIGHT: -------------------------------------------------------------------------------- 1 | ../COPYRIGHT -------------------------------------------------------------------------------- /imagequant-sys/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "imagequant-sys" 3 | version = "4.0.4" 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.63" 18 | 19 | [lib] 20 | crate-type = ["staticlib", "lib"] 21 | doctest = false 22 | path = "src/ffi.rs" 23 | name = "imagequant_sys" 24 | 25 | [dependencies] 26 | imagequant = { path = "..", version = "4.2.1", default-features = false, features = ["_internal_c_ffi"] } 27 | bitflags = "2.5" 28 | libc = "0.2.153" 29 | 30 | [package.metadata.capi.library] 31 | name = "imagequant" 32 | version = "0.4.0" 33 | 34 | [package.metadata.capi.pkg_config] 35 | name = "imagequant" 36 | filename = "imagequant" 37 | description = "Convert 24/32-bit images to 8-bit palette with alpha channel." 38 | 39 | [package.metadata.capi.header] 40 | name = "libimagequant" 41 | subdirectory = "" 42 | generation = false 43 | 44 | [package.metadata.capi.install.include] 45 | asset = [{from = "libimagequant.h"}] 46 | 47 | [package.metadata.release] 48 | consolidate-commits = true 49 | tag-message = "" 50 | tag-prefix = "" 51 | tag-name = "{{version}}" 52 | 53 | [features] 54 | # internal for cargo-c only 55 | capi = [] 56 | default = ["imagequant/default"] 57 | threads = ["imagequant/threads"] 58 | -------------------------------------------------------------------------------- /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 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/build.rs: -------------------------------------------------------------------------------- 1 | fn main() { 2 | println!("cargo:include={}", std::env::var("CARGO_MANIFEST_DIR").unwrap()); 3 | } 4 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/imagequant.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | /* generated with cargo-xcode 1.4.3 */ 4 | archiveVersion = 1; 5 | classes = { 6 | }; 7 | objectVersion = 53; 8 | objects = { 9 | /* Begin PBXBuildFile section */ 10 | 11 | CA60077B6400A6A410610A27 /* Cargo.toml in Sources */ = { 12 | isa = PBXBuildFile; 13 | fileRef = CA6091AFFEBB3EF4668187A5 /* Cargo.toml */; 14 | settings = { 15 | COMPILER_FLAGS = "--lib"; /* == OTHER_INPUT_FILE_FLAGS */ 16 | }; 17 | }; 18 | 19 | /* End PBXBuildFile section */ 20 | 21 | /* Begin PBXBuildRule section */ 22 | CA6091AFFEBBAC6C1400ACA8 /* PBXBuildRule */ = { 23 | isa = PBXBuildRule; 24 | compilerSpec = com.apple.compilers.proxy.script; 25 | dependencyFile = "$(DERIVED_FILE_DIR)/$(CARGO_XCODE_TARGET_ARCH)-$(EXECUTABLE_NAME).d"; 26 | filePatterns = "*/Cargo.toml"; /* must contain asterisk */ 27 | fileType = pattern.proxy; 28 | inputFiles = (); 29 | isEditable = 0; 30 | name = "Cargo project build"; 31 | outputFiles = ( 32 | "$(OBJECT_FILE_DIR)/$(CARGO_XCODE_TARGET_ARCH)-$(EXECUTABLE_NAME)", 33 | ); 34 | script = "# generated with cargo-xcode 1.4.3\n\nset -eu; export PATH=$PATH:~/.cargo/bin:/usr/local/bin;\nif [ \"${IS_MACCATALYST-NO}\" = YES ]; then\n CARGO_XCODE_TARGET_TRIPLE=\"${CARGO_XCODE_TARGET_ARCH}-apple-ios-macabi\"\nelse\n CARGO_XCODE_TARGET_TRIPLE=\"${CARGO_XCODE_TARGET_ARCH}-apple-${CARGO_XCODE_TARGET_OS}\"\nfi\nif [ \"$CARGO_XCODE_TARGET_OS\" != \"darwin\" ]; then\n PATH=\"${PATH/\\/Contents\\/Developer\\/Toolchains\\/XcodeDefault.xctoolchain\\/usr\\/bin:/xcode-provided-ld-cant-link-lSystem-for-the-host-build-script:}\"\nfi\nPATH=\"$PATH:/opt/homebrew/bin\" # Rust projects often depend on extra tools like nasm, which Xcode lacks\nif [ \"$CARGO_XCODE_BUILD_MODE\" == release ]; then\n OTHER_INPUT_FILE_FLAGS=\"${OTHER_INPUT_FILE_FLAGS} --release\"\nfi\nif command -v rustup &> /dev/null; then\n if ! rustup target list --installed | egrep -q \"${CARGO_XCODE_TARGET_TRIPLE}\"; then\n echo \"warning: this build requires rustup toolchain for $CARGO_XCODE_TARGET_TRIPLE, but it isn\'t installed\"\n rustup target add \"${CARGO_XCODE_TARGET_TRIPLE}\" || echo >&2 \"warning: can\'t install $CARGO_XCODE_TARGET_TRIPLE\"\n fi\nfi\nif [ \"$ACTION\" = clean ]; then\n ( set -x; cargo clean --manifest-path=\"$SCRIPT_INPUT_FILE\" ${OTHER_INPUT_FILE_FLAGS} --target=\"${CARGO_XCODE_TARGET_TRIPLE}\"; );\nelse\n ( set -x; cargo build --manifest-path=\"$SCRIPT_INPUT_FILE\" --features=\"${CARGO_XCODE_FEATURES:-}\" ${OTHER_INPUT_FILE_FLAGS} --target=\"${CARGO_XCODE_TARGET_TRIPLE}\"; );\nfi\n# it\'s too hard to explain Cargo\'s actual exe path to Xcode build graph, so hardlink to a known-good path instead\nBUILT_SRC=\"${CARGO_TARGET_DIR}/${CARGO_XCODE_TARGET_TRIPLE}/${CARGO_XCODE_BUILD_MODE}/${CARGO_XCODE_CARGO_FILE_NAME}\"\nln -f -- \"$BUILT_SRC\" \"$SCRIPT_OUTPUT_FILE_0\"\n\n# xcode generates dep file, but for its own path, so append our rename to it\nDEP_FILE_SRC=\"${CARGO_TARGET_DIR}/${CARGO_XCODE_TARGET_TRIPLE}/${CARGO_XCODE_BUILD_MODE}/${CARGO_XCODE_CARGO_DEP_FILE_NAME}\"\nif [ -f \"$DEP_FILE_SRC\" ]; then\n DEP_FILE_DST=\"${DERIVED_FILE_DIR}/${CARGO_XCODE_TARGET_ARCH}-${EXECUTABLE_NAME}.d\"\n cp -f \"$DEP_FILE_SRC\" \"$DEP_FILE_DST\"\n echo >> \"$DEP_FILE_DST\" \"$SCRIPT_OUTPUT_FILE_0: $BUILT_SRC\"\nfi\n\n# lipo script needs to know all the platform-specific files that have been built\n# archs is in the file name, so that paths don\'t stay around after archs change\n# must match input for LipoScript\nFILE_LIST=\"${DERIVED_FILE_DIR}/${ARCHS}-${EXECUTABLE_NAME}.xcfilelist\"\ntouch \"$FILE_LIST\"\nif ! egrep -q \"$SCRIPT_OUTPUT_FILE_0\" \"$FILE_LIST\" ; then\n echo >> \"$FILE_LIST\" \"$SCRIPT_OUTPUT_FILE_0\"\nfi\n"; 35 | }; 36 | /* End PBXBuildRule section */ 37 | 38 | /* Begin PBXFileReference section */ 39 | 40 | CA60A7F2A4D647C0CE156F07 /* staticlib */ = { 41 | isa = PBXFileReference; 42 | explicitFileType = "archive.ar"; 43 | includeInIndex = 0; 44 | name = "libimagequant_sys_static.a"; 45 | sourceTree = TARGET_BUILD_DIR; 46 | }; 47 | CA6091AFFEBB3EF4668187A5 /* Cargo.toml */ = { 48 | isa = PBXFileReference; 49 | lastKnownFileType = text; 50 | fileEncoding = 4; 51 | name = "Cargo.toml"; 52 | path = "Cargo.toml"; 53 | sourceTree = ""; 54 | }; 55 | /* Rust needs libresolv */ 56 | ADDEDBA66A6E1 = { 57 | isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; 58 | name = libresolv.tbd; path = usr/lib/libresolv.tbd; sourceTree = SDKROOT; 59 | }; 60 | 61 | /* End PBXFileReference section */ 62 | 63 | /* Begin PBXGroup section */ 64 | CA6091AFFEBB98AF0B5890DB /* Frameworks */ = { 65 | isa = PBXGroup; 66 | children = ( 67 | ADDEDBA66A6E2, 68 | 69 | ); 70 | name = Frameworks; 71 | sourceTree = ""; 72 | }; 73 | 74 | 75 | ADDEDBA66A6E2 /* Required for static linking */ = { 76 | isa = PBXGroup; 77 | children = ( 78 | ADDEDBA66A6E1 79 | ); 80 | name = "Required for static linking"; 81 | sourceTree = ""; 82 | }; 83 | 84 | CA6091AFFEBB22869D176AE5 /* Products */ = { 85 | isa = PBXGroup; 86 | children = ( 87 | CA60A7F2A4D647C0CE156F07, 88 | 89 | ); 90 | name = Products; 91 | sourceTree = ""; 92 | }; 93 | 94 | CA6091AFFEBBD65BC3C892A8 /* Main */ = { 95 | isa = PBXGroup; 96 | children = ( 97 | CA6091AFFEBB3EF4668187A5, 98 | CA6091AFFEBB22869D176AE5, 99 | CA6091AFFEBB98AF0B5890DB, 100 | 101 | ); 102 | sourceTree = ""; 103 | }; 104 | 105 | /* End PBXGroup section */ 106 | 107 | /* Begin PBXNativeTarget section */ 108 | CA60A7F2A4D6A6A410610A27 /* imagequant_sys-staticlib */ = { 109 | isa = PBXNativeTarget; 110 | buildConfigurationList = CA601449CA94A6A410610A27; 111 | buildPhases = ( 112 | CA607796DFF0A6A410610A27 /* Sources */, 113 | CA6091AFFEBBAF6EBB7F357C /* Universal Binary lipo */, 114 | ); 115 | buildRules = ( 116 | CA6091AFFEBBAC6C1400ACA8 /* PBXBuildRule */, 117 | ); 118 | dependencies = ( 119 | ); 120 | name = "imagequant_sys-staticlib"; 121 | productName = "libimagequant_sys_static.a"; 122 | productReference = CA60A7F2A4D647C0CE156F07; 123 | productType = "com.apple.product-type.library.static"; 124 | }; 125 | 126 | /* End PBXNativeTarget section */ 127 | 128 | CA607796DFF0A6A410610A27 = { 129 | isa = PBXSourcesBuildPhase; 130 | buildActionMask = 2147483647; 131 | files = ( 132 | CA60077B6400A6A410610A27 133 | ); 134 | runOnlyForDeploymentPostprocessing = 0; 135 | }; 136 | 137 | CA601449CA94A6A410610A27 /* staticlib */ = { 138 | isa = XCConfigurationList; 139 | buildConfigurations = ( 140 | CA60994CA1EBA6A410610A27 /* Release */, 141 | CA60FBBF8BCDA6A410610A27 /* Debug */, 142 | ); 143 | defaultConfigurationIsVisible = 0; 144 | defaultConfigurationName = Release; 145 | }; 146 | CA60994CA1EBA6A410610A27 /* staticlib */ = { 147 | isa = XCBuildConfiguration; 148 | buildSettings = { 149 | PRODUCT_NAME = "imagequant_sys_static"; 150 | "CARGO_XCODE_CARGO_FILE_NAME" = "libimagequant_sys.a"; 151 | "CARGO_XCODE_CARGO_DEP_FILE_NAME" = "libimagequant_sys.d"; 152 | SUPPORTED_PLATFORMS = "macosx iphonesimulator iphoneos appletvsimulator appletvos"; 153 | SKIP_INSTALL = YES; 154 | INSTALL_GROUP = ""; 155 | INSTALL_MODE_FLAG = ""; 156 | INSTALL_OWNER = ""; 157 | }; 158 | name = Release; 159 | }; 160 | CA60FBBF8BCDA6A410610A27 /* staticlib */ = { 161 | isa = XCBuildConfiguration; 162 | buildSettings = { 163 | PRODUCT_NAME = "imagequant_sys_static"; 164 | "CARGO_XCODE_CARGO_FILE_NAME" = "libimagequant_sys.a"; 165 | "CARGO_XCODE_CARGO_DEP_FILE_NAME" = "libimagequant_sys.d"; 166 | SUPPORTED_PLATFORMS = "macosx iphonesimulator iphoneos appletvsimulator appletvos"; 167 | SKIP_INSTALL = YES; 168 | INSTALL_GROUP = ""; 169 | INSTALL_MODE_FLAG = ""; 170 | INSTALL_OWNER = ""; 171 | }; 172 | name = Debug; 173 | }; 174 | 175 | CA6091AFFEBBAF6EBB7F357C /* LipoScript */ = { 176 | name = "Universal Binary lipo"; 177 | isa = PBXShellScriptBuildPhase; 178 | buildActionMask = 2147483647; 179 | files = (); 180 | inputFileListPaths = (); 181 | inputPaths = ( 182 | "$(DERIVED_FILE_DIR)/$(ARCHS)-$(EXECUTABLE_NAME).xcfilelist", 183 | ); 184 | outputFileListPaths = (); 185 | outputPaths = ( 186 | "$(TARGET_BUILD_DIR)/$(EXECUTABLE_PATH)" 187 | ); 188 | runOnlyForDeploymentPostprocessing = 0; 189 | shellPath = /bin/sh; 190 | shellScript = "# generated with cargo-xcode 1.4.3\nset -eux; cat \"$DERIVED_FILE_DIR/$ARCHS-$EXECUTABLE_NAME.xcfilelist\" | tr '\\n' '\\0' | xargs -0 lipo -create -output \"$TARGET_BUILD_DIR/$EXECUTABLE_PATH\""; 191 | }; 192 | 193 | CA6091AFFEBB80E02D6C7F57 = { 194 | isa = XCConfigurationList; 195 | buildConfigurations = ( 196 | CA60B3CFE6713CC16B37690B /* Release */, 197 | CA60B3CFE671228BE02872F8 /* Debug */, 198 | ); 199 | defaultConfigurationIsVisible = 0; 200 | defaultConfigurationName = Release; 201 | }; 202 | 203 | CA60B3CFE6713CC16B37690B = { 204 | isa = XCBuildConfiguration; 205 | buildSettings = { 206 | 207 | ALWAYS_SEARCH_USER_PATHS = NO; 208 | SUPPORTS_MACCATALYST = YES; 209 | CARGO_TARGET_DIR = "$(PROJECT_TEMP_DIR)/cargo_target"; /* for cargo */ 210 | CARGO_XCODE_FEATURES = ""; /* configure yourself */ 211 | "CARGO_XCODE_TARGET_ARCH[arch=arm64*]" = "aarch64"; 212 | "CARGO_XCODE_TARGET_ARCH[arch=x86_64*]" = "x86_64"; /* catalyst adds h suffix */ 213 | "CARGO_XCODE_TARGET_ARCH[arch=i386]" = "i686"; 214 | "CARGO_XCODE_TARGET_OS[sdk=macosx*]" = "darwin"; 215 | "CARGO_XCODE_TARGET_OS[sdk=iphonesimulator*]" = "ios-sim"; 216 | "CARGO_XCODE_TARGET_OS[sdk=iphonesimulator*][arch=x86_64*]" = "ios"; 217 | "CARGO_XCODE_TARGET_OS[sdk=iphoneos*]" = "ios"; 218 | "CARGO_XCODE_TARGET_OS[sdk=appletvsimulator*]" = "tvos"; 219 | "CARGO_XCODE_TARGET_OS[sdk=appletvos*]" = "tvos"; 220 | PRODUCT_NAME = "imagequant-sys"; 221 | SDKROOT = macosx; 222 | 223 | "CARGO_XCODE_BUILD_MODE" = "release"; /* for xcode scripts */ 224 | }; 225 | name = Release; 226 | }; 227 | 228 | CA60B3CFE671228BE02872F8 = { 229 | isa = XCBuildConfiguration; 230 | buildSettings = { 231 | 232 | ALWAYS_SEARCH_USER_PATHS = NO; 233 | SUPPORTS_MACCATALYST = YES; 234 | CARGO_TARGET_DIR = "$(PROJECT_TEMP_DIR)/cargo_target"; /* for cargo */ 235 | CARGO_XCODE_FEATURES = ""; /* configure yourself */ 236 | "CARGO_XCODE_TARGET_ARCH[arch=arm64*]" = "aarch64"; 237 | "CARGO_XCODE_TARGET_ARCH[arch=x86_64*]" = "x86_64"; /* catalyst adds h suffix */ 238 | "CARGO_XCODE_TARGET_ARCH[arch=i386]" = "i686"; 239 | "CARGO_XCODE_TARGET_OS[sdk=macosx*]" = "darwin"; 240 | "CARGO_XCODE_TARGET_OS[sdk=iphonesimulator*]" = "ios-sim"; 241 | "CARGO_XCODE_TARGET_OS[sdk=iphonesimulator*][arch=x86_64*]" = "ios"; 242 | "CARGO_XCODE_TARGET_OS[sdk=iphoneos*]" = "ios"; 243 | "CARGO_XCODE_TARGET_OS[sdk=appletvsimulator*]" = "tvos"; 244 | "CARGO_XCODE_TARGET_OS[sdk=appletvos*]" = "tvos"; 245 | PRODUCT_NAME = "imagequant-sys"; 246 | SDKROOT = macosx; 247 | 248 | "CARGO_XCODE_BUILD_MODE" = "debug"; /* for xcode scripts */ 249 | ONLY_ACTIVE_ARCH = YES; 250 | }; 251 | name = Debug; 252 | }; 253 | 254 | CA6091AFFEBBE04653AD465F = { 255 | isa = PBXProject; 256 | attributes = { 257 | LastUpgradeCheck = 1300; 258 | TargetAttributes = { 259 | CA60A7F2A4D6A6A410610A27 = { 260 | CreatedOnToolsVersion = 9.2; 261 | ProvisioningStyle = Automatic; 262 | }; 263 | }; 264 | }; 265 | buildConfigurationList = CA6091AFFEBB80E02D6C7F57; 266 | compatibilityVersion = "Xcode 11.4"; 267 | developmentRegion = en; 268 | hasScannedForEncodings = 0; 269 | knownRegions = ( 270 | en, 271 | Base, 272 | ); 273 | mainGroup = CA6091AFFEBBD65BC3C892A8; 274 | productRefGroup = CA6091AFFEBB22869D176AE5 /* Products */; 275 | projectDirPath = ""; 276 | projectRoot = ""; 277 | targets = ( 278 | CA60A7F2A4D6A6A410610A27, 279 | 280 | ); 281 | }; 282 | 283 | }; 284 | rootObject = CA6091AFFEBBE04653AD465F; 285 | } 286 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | 125 | LIQ_EXPORT liq_error liq_set_dithering_level(liq_result *res, float dither_level) LIQ_NONNULL; 126 | LIQ_EXPORT liq_error liq_set_output_gamma(liq_result* res, double gamma) LIQ_NONNULL; 127 | LIQ_EXPORT LIQ_USERESULT double liq_get_output_gamma(const liq_result *result) LIQ_NONNULL; 128 | 129 | LIQ_EXPORT LIQ_USERESULT const liq_palette *liq_get_palette(liq_result *result) LIQ_NONNULL; 130 | 131 | LIQ_EXPORT liq_error liq_write_remapped_image(liq_result *result, liq_image *input_image, void *buffer, size_t buffer_size) LIQ_NONNULL; 132 | LIQ_EXPORT liq_error liq_write_remapped_image_rows(liq_result *result, liq_image *input_image, unsigned char **row_pointers) LIQ_NONNULL; 133 | 134 | LIQ_EXPORT double liq_get_quantization_error(const liq_result *result) LIQ_NONNULL; 135 | LIQ_EXPORT int liq_get_quantization_quality(const liq_result *result) LIQ_NONNULL; 136 | LIQ_EXPORT double liq_get_remapping_error(const liq_result *result) LIQ_NONNULL; 137 | LIQ_EXPORT int liq_get_remapping_quality(const liq_result *result) LIQ_NONNULL; 138 | 139 | LIQ_EXPORT void liq_result_destroy(liq_result *) LIQ_NONNULL; 140 | 141 | LIQ_EXPORT int liq_version(void); 142 | 143 | 144 | // Deprecated 145 | LIQ_EXPORT LIQ_USERESULT liq_result *liq_quantize_image(liq_attr *options, liq_image *input_image) LIQ_NONNULL; 146 | 147 | #ifdef __cplusplus 148 | } 149 | #endif 150 | 151 | #endif 152 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/org/pngquant/PngQuantException.java: -------------------------------------------------------------------------------- 1 | package org.pngquant; 2 | 3 | public class PngQuantException extends Exception { 4 | } 5 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /imagequant-sys/pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | org.pngquant 5 | libimagequant 6 | jar 7 | 4.0.3 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 | -------------------------------------------------------------------------------- /src/attr.rs: -------------------------------------------------------------------------------- 1 | use crate::error::Error; 2 | use crate::hist::Histogram; 3 | use crate::image::Image; 4 | use crate::pal::{PalLen, MAX_COLORS, RGBA}; 5 | use crate::quant::{mse_to_quality, quality_to_mse, QuantizationResult}; 6 | use crate::remap::DitherMapMode; 7 | use std::sync::Arc; 8 | 9 | /// Starting point and settings for the quantization process 10 | #[derive(Clone)] 11 | pub struct Attributes { 12 | pub(crate) max_colors: PalLen, 13 | target_mse: f64, 14 | max_mse: Option, 15 | kmeans_iteration_limit: f64, 16 | kmeans_iterations: u16, 17 | feedback_loop_trials: u16, 18 | pub(crate) max_histogram_entries: u32, 19 | min_posterization_output: u8, 20 | min_posterization_input: u8, 21 | pub(crate) last_index_transparent: bool, 22 | pub(crate) use_contrast_maps: bool, 23 | pub(crate) single_threaded_dithering: bool, 24 | pub(crate) use_dither_map: DitherMapMode, 25 | speed: u8, 26 | pub(crate) progress_stage1: u8, 27 | pub(crate) progress_stage2: u8, 28 | pub(crate) progress_stage3: u8, 29 | 30 | progress_callback: Option ControlFlow + Send + Sync>>, 31 | log_callback: Option>, 32 | log_flush_callback: Option>, 33 | } 34 | 35 | impl Attributes { 36 | /// New handle for library configuration 37 | /// 38 | /// See also [`Attributes::new_image()`] 39 | #[inline] 40 | #[must_use] 41 | pub fn new() -> Self { 42 | let mut attr = Self { 43 | target_mse: 0., 44 | max_mse: None, 45 | max_colors: MAX_COLORS as PalLen, 46 | last_index_transparent: false, 47 | kmeans_iteration_limit: 0., 48 | max_histogram_entries: 0, 49 | min_posterization_output: 0, 50 | min_posterization_input: 0, 51 | kmeans_iterations: 0, 52 | feedback_loop_trials: 0, 53 | use_contrast_maps: false, 54 | use_dither_map: DitherMapMode::None, 55 | single_threaded_dithering: false, 56 | speed: 0, 57 | progress_stage1: 0, 58 | progress_stage2: 0, 59 | progress_stage3: 0, 60 | progress_callback: None, 61 | log_callback: None, 62 | log_flush_callback: None, 63 | }; 64 | let _ = attr.set_speed(4); 65 | attr 66 | } 67 | 68 | /// Make an image from RGBA pixels. 69 | /// 70 | /// The `pixels` argument can be `Vec`, or `Box<[RGBA]>` or `&[RGBA]`. 71 | /// See [`Attributes::new_image_borrowed`] for a non-copying alternative. 72 | /// 73 | /// Use 0.0 for gamma if the image is sRGB (most images are). 74 | #[inline] 75 | pub fn new_image(&self, pixels: VecRGBA, width: usize, height: usize, gamma: f64) -> Result, Error> where VecRGBA: Into> { 76 | Image::new(self, pixels, width, height, gamma) 77 | } 78 | 79 | /// Generate palette for the image 80 | pub fn quantize(&self, image: &mut Image<'_>) -> Result { 81 | let mut hist = Histogram::new(self); 82 | hist.add_image(self, image)?; 83 | hist.quantize_internal(self, false) 84 | } 85 | 86 | /// It's better to use `set_quality()` 87 | #[inline] 88 | pub fn set_max_colors(&mut self, colors: u32) -> Result<(), Error> { 89 | if !(2..=256).contains(&colors) { 90 | return Err(Error::ValueOutOfRange); 91 | } 92 | self.max_colors = colors as PalLen; 93 | Ok(()) 94 | } 95 | 96 | /// Range 0-100, roughly like JPEG. 97 | /// 98 | /// If the minimum quality can't be met, the quantization will be aborted with an error. 99 | /// 100 | /// Default is min 0, max 100, which means best effort, and never aborts the process. 101 | /// 102 | /// If max is less than 100, the library will try to use fewer colors. 103 | /// Images with fewer colors are not always smaller, due to increased dithering it causes. 104 | pub fn set_quality(&mut self, minimum: u8, target: u8) -> Result<(), Error> { 105 | if !(0..=100).contains(&target) || target < minimum { 106 | return Err(Error::ValueOutOfRange); 107 | } 108 | if target < 30 { 109 | self.verbose_print(" warning: quality set too low"); 110 | } 111 | self.target_mse = quality_to_mse(target); 112 | self.max_mse = Some(quality_to_mse(minimum)); 113 | Ok(()) 114 | } 115 | 116 | /// 1-10. 117 | /// 118 | /// Faster speeds generate images of lower quality, but may be useful 119 | /// for real-time generation of images. 120 | /// 121 | /// The default is 4. 122 | #[inline] 123 | pub fn set_speed(&mut self, value: i32) -> Result<(), Error> { 124 | if !(1..=10).contains(&value) { 125 | return Err(Error::ValueOutOfRange); 126 | } 127 | let mut iterations = (8 - value).max(0) as u16; 128 | iterations += iterations * iterations / 2; 129 | self.kmeans_iterations = iterations; 130 | self.kmeans_iteration_limit = 1. / f64::from(1 << (23 - value)); 131 | self.feedback_loop_trials = (56 - 9 * value).max(0) as _; 132 | self.max_histogram_entries = ((1 << 17) + (1 << 18) * (10 - value)) as _; 133 | self.min_posterization_input = if value >= 8 { 1 } else { 0 }; 134 | self.use_dither_map = if value <= 6 { DitherMapMode::Enabled } else { DitherMapMode::None }; 135 | if self.use_dither_map != DitherMapMode::None && value < 3 { 136 | self.use_dither_map = DitherMapMode::Always; 137 | } 138 | self.use_contrast_maps = (value <= 7) || self.use_dither_map != DitherMapMode::None; 139 | self.single_threaded_dithering = value == 1; 140 | self.speed = value as u8; 141 | self.progress_stage1 = if self.use_contrast_maps { 20 } else { 8 }; 142 | if self.feedback_loop_trials < 2 { 143 | self.progress_stage1 += 30; 144 | } 145 | self.progress_stage3 = (50 / (1 + value)) as u8; 146 | self.progress_stage2 = 100 - self.progress_stage1 - self.progress_stage3; 147 | Ok(()) 148 | } 149 | 150 | /// Number of least significant bits to ignore. 151 | /// 152 | /// Useful for generating palettes for VGA, 15-bit textures, or other retro platforms. 153 | #[inline] 154 | pub fn set_min_posterization(&mut self, value: u8) -> Result<(), Error> { 155 | if !(0..=4).contains(&value) { 156 | return Err(Error::ValueOutOfRange); 157 | } 158 | self.min_posterization_output = value; 159 | Ok(()) 160 | } 161 | 162 | /// Returns number of bits of precision truncated 163 | #[inline(always)] 164 | #[must_use] 165 | pub fn min_posterization(&self) -> u8 { 166 | self.min_posterization_output 167 | } 168 | 169 | /// Return currently set speed/quality trade-off setting 170 | #[inline(always)] 171 | #[must_use] 172 | pub fn speed(&self) -> u32 { 173 | self.speed.into() 174 | } 175 | 176 | /// Return max number of colors set 177 | #[inline(always)] 178 | #[must_use] 179 | pub fn max_colors(&self) -> u32 { 180 | self.max_colors.into() 181 | } 182 | 183 | /// Reads values set with `set_quality` 184 | #[must_use] 185 | pub fn quality(&self) -> (u8, u8) { 186 | ( 187 | self.max_mse.map_or(0, mse_to_quality), 188 | mse_to_quality(self.target_mse), 189 | ) 190 | } 191 | 192 | /// Describe dimensions of a slice of RGBA pixels 193 | /// 194 | /// Use 0.0 for gamma if the image is sRGB (most images are). 195 | #[inline] 196 | pub fn new_image_borrowed<'pixels>(&self, bitmap: &'pixels [RGBA], width: usize, height: usize, gamma: f64) -> Result, Error> { 197 | Image::new_borrowed(self, bitmap, width, height, gamma) 198 | } 199 | 200 | /// Like `new_image_stride_borrowed`, but makes a copy of the pixels. 201 | /// 202 | /// The `pixels` argument can be `Vec`, or `Box<[RGBA]>` or `&[RGBA]`. 203 | #[inline] 204 | pub fn new_image_stride(&self, pixels: VecRGBA, width: usize, height: usize, stride: usize, gamma: f64) -> Result, Error> where VecRGBA: Into> { 205 | Image::new_stride(self, pixels, width, height, stride, gamma) 206 | } 207 | 208 | #[doc(hidden)] 209 | #[deprecated(note = "use new_image_stride")] 210 | #[cold] 211 | pub fn new_image_stride_copy(&self, bitmap: &[RGBA], width: usize, height: usize, stride: usize, gamma: f64) -> Result, Error> { 212 | self.new_image_stride(bitmap, width, height, stride, gamma) 213 | } 214 | 215 | /// Set callback function to be called every time the library wants to print a message. 216 | /// 217 | /// To share data with the callback, use `Arc` or `Atomic*` types and `move ||` closures. 218 | #[inline] 219 | pub fn set_log_callback(&mut self, callback: F) { 220 | self.verbose_printf_flush(); 221 | self.log_callback = Some(Arc::new(callback)); 222 | } 223 | 224 | /// Callback for flushing output (if you buffer messages, that's the time to flush those buffers) 225 | #[inline] 226 | pub fn set_log_flush_callback(&mut self, callback: F) { 227 | self.verbose_printf_flush(); 228 | self.log_flush_callback = Some(Arc::new(callback)); 229 | } 230 | 231 | /// Set callback function to be called every time the library makes a progress. 232 | /// It can be used to cancel operation early. 233 | /// 234 | /// To share data with the callback, use `Arc` or `Atomic*` types and `move ||` closures. 235 | #[inline] 236 | pub fn set_progress_callback ControlFlow + Send + Sync + 'static>(&mut self, callback: F) { 237 | self.progress_callback = Some(Arc::new(callback)); 238 | } 239 | 240 | /// Move transparent color to the last entry in the palette 241 | /// 242 | /// This is less efficient for PNG, but required by some broken software 243 | #[inline(always)] 244 | pub fn set_last_index_transparent(&mut self, is_last: bool) { 245 | self.last_index_transparent = is_last; 246 | } 247 | 248 | // true == abort 249 | #[inline] 250 | #[must_use] 251 | pub(crate) fn progress(&self, percent: f32) -> bool { 252 | if let Some(f) = &self.progress_callback { 253 | f(percent) == ControlFlow::Break 254 | } else { 255 | false 256 | } 257 | } 258 | 259 | #[inline(always)] 260 | pub(crate) fn verbose_print(&self, msg: impl AsRef) { 261 | fn print_(a: &Attributes, msg: &str) { 262 | if let Some(f) = &a.log_callback { 263 | f(a, msg); 264 | } 265 | } 266 | print_(self, msg.as_ref()); 267 | } 268 | 269 | #[inline] 270 | pub(crate) fn verbose_printf_flush(&self) { 271 | if let Some(f) = &self.log_flush_callback { 272 | f(self); 273 | } 274 | } 275 | 276 | #[must_use] 277 | pub(crate) fn feedback_loop_trials(&self, hist_items: usize) -> u16 { 278 | let mut feedback_loop_trials = self.feedback_loop_trials; 279 | if hist_items > 5000 { 280 | feedback_loop_trials = (feedback_loop_trials * 3 + 3) / 4; 281 | } 282 | if hist_items > 25000 { 283 | feedback_loop_trials = (feedback_loop_trials * 3 + 3) / 4; 284 | } 285 | if hist_items > 50000 { 286 | feedback_loop_trials = (feedback_loop_trials * 3 + 3) / 4; 287 | } 288 | if hist_items > 100_000 { 289 | feedback_loop_trials = (feedback_loop_trials * 3 + 3) / 4; 290 | } 291 | feedback_loop_trials 292 | } 293 | 294 | /// `max_mse`, `target_mse`, user asked for perfect quality 295 | pub(crate) fn target_mse(&self, hist_items_len: usize) -> (Option, f64, bool) { 296 | let max_mse = self.max_mse.map(|mse| mse * if hist_items_len <= MAX_COLORS { 0.33 } else { 1. }); 297 | let aim_for_perfect_quality = self.target_mse == 0.; 298 | let mut target_mse = self.target_mse.max((f64::from(1 << self.min_posterization_output) / 1024.).powi(2)); 299 | if let Some(max_mse) = max_mse { 300 | target_mse = target_mse.min(max_mse); 301 | } 302 | (max_mse, target_mse, aim_for_perfect_quality) 303 | } 304 | 305 | /// returns iterations, `iteration_limit` 306 | #[must_use] 307 | pub(crate) fn kmeans_iterations(&self, hist_items_len: usize, palette_error_is_known: bool) -> (u16, f64) { 308 | let mut iteration_limit = self.kmeans_iteration_limit; 309 | let mut iterations = self.kmeans_iterations; 310 | if hist_items_len > 5000 { 311 | iterations = (iterations * 3 + 3) / 4; 312 | } 313 | if hist_items_len > 25000 { 314 | iterations = (iterations * 3 + 3) / 4; 315 | } 316 | if hist_items_len > 50000 { 317 | iterations = (iterations * 3 + 3) / 4; 318 | } 319 | if hist_items_len > 100_000 { 320 | iterations = (iterations * 3 + 3) / 4; 321 | iteration_limit *= 2.; 322 | } 323 | if iterations == 0 && !palette_error_is_known && self.max_mse.is_some() { 324 | iterations = 1; 325 | } 326 | (iterations, iteration_limit) 327 | } 328 | 329 | #[inline] 330 | #[must_use] 331 | pub(crate) fn posterize_bits(&self) -> u8 { 332 | self.min_posterization_output.max(self.min_posterization_input) 333 | } 334 | } 335 | 336 | impl Drop for Attributes { 337 | fn drop(&mut self) { 338 | self.verbose_printf_flush(); 339 | } 340 | } 341 | 342 | impl Default for Attributes { 343 | #[inline(always)] 344 | fn default() -> Self { 345 | Self::new() 346 | } 347 | } 348 | 349 | /// Result of callback in [`Attributes::set_progress_callback`] 350 | #[derive(Clone, Copy, Debug, Eq, PartialEq)] 351 | #[repr(C)] 352 | pub enum ControlFlow { 353 | /// Continue processing as normal 354 | Continue = 1, 355 | /// Abort processing and fail 356 | Break = 0, 357 | } 358 | 359 | #[test] 360 | fn counters() { 361 | let mut a = Attributes::new(); 362 | a.set_speed(10).unwrap(); 363 | let (iter, _) = a.kmeans_iterations(1000, false); 364 | assert_eq!(iter, 0); 365 | a.set_quality(80, 90).unwrap(); 366 | let (iter, limit) = a.kmeans_iterations(1000, false); 367 | assert_eq!(iter, 1); 368 | assert!(limit > 0. && limit < 0.01); 369 | 370 | let (iter, _) = a.kmeans_iterations(1000, true); 371 | assert_eq!(iter, 0); 372 | 373 | let mut a = Attributes::new(); 374 | a.set_quality(50, 80).unwrap(); 375 | 376 | let (max_mse, target_mse, aim_perfect) = a.target_mse(10000); 377 | let max_mse = max_mse.unwrap(); 378 | assert!(!aim_perfect); 379 | assert!(target_mse > 0. && target_mse < 0.01); 380 | assert!(max_mse > 0. && max_mse > target_mse && max_mse < 0.01); 381 | } 382 | 383 | #[test] 384 | fn getset() { 385 | let mut a = Attributes::new(); 386 | assert!(a.set_quality(0, 101).is_err()); 387 | assert!(a.set_quality(50, 49).is_err()); 388 | assert!(a.feedback_loop_trials(1000) > 0); 389 | 390 | let (max_mse, target_mse, aim_perfect) = a.target_mse(10000); 391 | assert!(aim_perfect); 392 | assert!(target_mse < 0.0001); 393 | assert_eq!(max_mse, None); 394 | 395 | a.set_speed(5).unwrap(); 396 | assert_eq!(5, a.speed()); 397 | assert!(a.set_speed(99).is_err()); 398 | assert!(a.set_speed(0).is_err()); 399 | 400 | a.set_max_colors(5).unwrap(); 401 | assert_eq!(5, a.max_colors()); 402 | assert!(a.set_max_colors(0).is_err()); 403 | 404 | a.set_min_posterization(2).unwrap(); 405 | assert_eq!(2, a.min_posterization()); 406 | assert_eq!(2, a.posterize_bits()); 407 | assert!(a.set_min_posterization(8).is_err()); 408 | 409 | let mut a = Attributes::new(); 410 | a.set_speed(10).unwrap(); 411 | assert_eq!(1, a.posterize_bits()); 412 | } 413 | -------------------------------------------------------------------------------- /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 | use crate::rows::RowCallback; 7 | use crate::Attributes; 8 | use crate::Error; 9 | use crate::Image; 10 | use crate::pal::Palette; 11 | use crate::QuantizationResult; 12 | use crate::RGBA; 13 | use crate::seacow::{Pointer, RowBitmapMut, SeaCow}; 14 | use std::mem::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(std::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 std::os::raw::c_void)) -> Result<(), Error> { 60 | image.px.set_memory_ownership(own_rows, own_pixels, free_fn) 61 | } 62 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use std::collections::TryReserveError; 2 | use std::fmt; 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 std::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 | #[cold] 54 | fn from(_: TryReserveError) -> Self { 55 | Self::OutOfMemory 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/hist.rs: -------------------------------------------------------------------------------- 1 | use crate::error::*; 2 | use crate::image::Image; 3 | use crate::pal::{f_pixel, gamma_lut, PalIndex, ARGBF, MAX_COLORS, RGBA}; 4 | use crate::quant::QuantizationResult; 5 | use crate::rows::{temp_buf, DynamicRows}; 6 | use crate::Attributes; 7 | use std::collections::{HashMap, HashSet}; 8 | use std::fmt; 9 | use std::hash::Hash; 10 | 11 | /// Number of pixels in a given color for [`Histogram::add_colors()`] 12 | /// 13 | /// Used for building a histogram manually. Otherwise see [`Histogram::add_image()`] 14 | #[repr(C)] 15 | #[derive(Debug, Copy, Clone)] 16 | pub struct HistogramEntry { 17 | /// The color 18 | pub color: RGBA, 19 | /// Importance of the color (e.g. number of occurrences) 20 | pub count: u32, 21 | } 22 | 23 | /// Generate one shared palette for multiple images 24 | /// 25 | /// If you're converting one image at a time, see [`Attributes::new_image`] instead 26 | pub struct Histogram { 27 | gamma: Option, 28 | fixed_colors: FixedColorsSet, 29 | 30 | /// The key is the RGBA cast to u32 31 | /// The value is a (boosted) count or 0 if it's a fixed color 32 | hashmap: HashMap, 33 | 34 | posterize_bits: u8, 35 | max_histogram_entries: u32, 36 | } 37 | 38 | pub(crate) type FixedColorsSet = HashSet; 39 | 40 | #[derive(Clone)] 41 | pub(crate) struct HistItem { 42 | pub color: f_pixel, 43 | pub adjusted_weight: f32, 44 | pub perceptual_weight: f32, 45 | /// temporary in median cut 46 | pub mc_color_weight: f32, 47 | pub tmp: HistSortTmp, 48 | } 49 | 50 | impl HistItem { 51 | // Safety: just an int, and it's been initialized when constructing the object 52 | #[inline(always)] 53 | pub fn mc_sort_value(&self) -> u32 { 54 | unsafe { self.tmp.mc_sort_value } 55 | } 56 | 57 | // The u32 has been initialized when constructing the object, and u8/u16 is smaller than that 58 | #[inline(always)] 59 | pub fn likely_palette_index(&self) -> PalIndex { 60 | assert!(std::mem::size_of::() <= std::mem::size_of::()); 61 | unsafe { self.tmp.likely_palette_index } 62 | } 63 | } 64 | 65 | impl fmt::Debug for HistItem { 66 | #[cold] 67 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 68 | f.debug_struct("HistItem") 69 | .field("color", &self.color) 70 | .field("adjusted_weight", &self.adjusted_weight) 71 | .field("perceptual_weight", &self.perceptual_weight) 72 | .field("color_weight", &self.mc_color_weight) 73 | .finish() 74 | } 75 | } 76 | 77 | #[repr(C)] 78 | #[derive(Clone, Copy)] 79 | pub union HistSortTmp { 80 | pub mc_sort_value: u32, 81 | pub likely_palette_index: PalIndex, 82 | } 83 | 84 | impl Histogram { 85 | /// Creates histogram object that will be used to collect color statistics from multiple images. 86 | /// 87 | /// All options should be set on `attr` before the histogram object is created. Options changed later may not have effect. 88 | #[inline] 89 | #[must_use] 90 | pub fn new(attr: &Attributes) -> Self { 91 | Self { 92 | posterize_bits: attr.posterize_bits(), 93 | max_histogram_entries: attr.max_histogram_entries, 94 | fixed_colors: HashSet::with_hasher(U32Hasher(0)), 95 | hashmap: HashMap::with_hasher(U32Hasher(0)), 96 | gamma: None, 97 | } 98 | } 99 | 100 | /// "Learns" colors from the image, which will be later used to generate the palette. 101 | /// 102 | /// Fixed colors added to the image are also added to the histogram. If the total number of fixed colors exceeds 256, 103 | /// this function will fail with `LIQ_BUFFER_TOO_SMALL`. 104 | #[inline(never)] 105 | pub fn add_image(&mut self, attr: &Attributes, image: &mut Image) -> Result<(), Error> { 106 | let width = image.width(); 107 | let height = image.height(); 108 | if image.importance_map.is_none() && attr.use_contrast_maps { 109 | image.contrast_maps()?; 110 | } 111 | 112 | self.gamma = image.gamma(); 113 | 114 | if !image.fixed_colors.is_empty() { 115 | self.fixed_colors.extend(image.fixed_colors.iter().copied().enumerate().map(|(idx, rgba)| { 116 | HashColor { rgba, index: idx as _ } 117 | })); 118 | } 119 | 120 | if attr.progress(f32::from(attr.progress_stage1) * 0.40) { 121 | return Err(Aborted); // bow can free the RGBA source if copy has been made in f_pixels 122 | } 123 | 124 | let posterize_bits = attr.posterize_bits(); 125 | let surface_area = height * width; 126 | let estimated_colors = (surface_area / (posterize_bits as usize + if surface_area > 512 * 512 { 7 } else { 5 })).min(250_000); 127 | self.reserve(estimated_colors); 128 | 129 | self.add_pixel_rows(&image.px, image.importance_map.as_deref(), posterize_bits)?; 130 | 131 | Ok(()) 132 | } 133 | 134 | /// Alternative to `add_image()`. Intead of counting colors in an image, it directly takes an array of colors and their counts. 135 | /// 136 | /// This function is only useful if you already have a histogram of the image from another source. 137 | /// 138 | /// The gamma may be 0 to mean sRGB. All calls to `add_colors` and `add_fixed_color` should use the same gamma value. 139 | #[inline(never)] 140 | pub fn add_colors(&mut self, entries: &[HistogramEntry], gamma: f64) -> Result<(), Error> { 141 | if entries.is_empty() || entries.len() > 1 << 24 { 142 | return Err(ValueOutOfRange); 143 | } 144 | 145 | if !(0. ..1.).contains(&gamma) { 146 | return Err(ValueOutOfRange); 147 | } 148 | 149 | if self.gamma.is_none() && gamma > 0. { 150 | self.gamma = Some(gamma); 151 | } 152 | 153 | self.reserve(entries.len()); 154 | 155 | for e in entries { 156 | self.add_color(e.color, e.count); 157 | } 158 | 159 | Ok(()) 160 | } 161 | 162 | /// Add a color guaranteed to be in the final palette 163 | /// 164 | /// The gamma may be 0 to mean sRGB. All calls to `add_colors` and `add_fixed_color` should use the same gamma value. 165 | pub fn add_fixed_color(&mut self, rgba: RGBA, gamma: f64) -> Result<(), Error> { 166 | if self.fixed_colors.len() >= MAX_COLORS { 167 | return Err(Unsupported); 168 | } 169 | 170 | if self.gamma.is_none() && gamma > 0. { 171 | self.gamma = Some(gamma); 172 | } 173 | 174 | let idx = self.fixed_colors.len(); 175 | self.fixed_colors.insert(HashColor { rgba, index: idx as _ }); 176 | 177 | Ok(()) 178 | } 179 | 180 | /// Generate palette for all images/colors added to the histogram. 181 | /// 182 | /// Palette generated using this function won't be improved during remapping. 183 | /// If you're generating palette for only one image, it's better not to use the `Histogram`. 184 | #[inline] 185 | pub fn quantize(&mut self, attr: &Attributes) -> Result { 186 | self.quantize_internal(attr, true) 187 | } 188 | 189 | #[inline(never)] 190 | pub(crate) fn quantize_internal(&mut self, attr: &Attributes, freeze_result_colors: bool) -> Result { 191 | if self.hashmap.is_empty() && self.fixed_colors.is_empty() { 192 | return Err(Unsupported); 193 | } 194 | 195 | if attr.progress(0.) { return Err(Aborted); } 196 | if attr.progress(f32::from(attr.progress_stage1) * 0.89) { 197 | return Err(Aborted); 198 | } 199 | 200 | let gamma = self.gamma.unwrap_or(0.45455); 201 | let hist = self.finalize_builder(gamma).map_err(|_| OutOfMemory)?; 202 | 203 | attr.verbose_print(format!(" made histogram...{} colors found", hist.items.len())); 204 | 205 | QuantizationResult::new(attr, hist, freeze_result_colors, gamma) 206 | } 207 | 208 | #[inline(always)] 209 | fn add_color(&mut self, rgba: RGBA, boost: u32) { 210 | if boost == 0 { 211 | return; 212 | } 213 | 214 | let px_int = if rgba.a != 0 { 215 | self.posterize_mask() & unsafe { RGBAInt { rgba }.int } 216 | } else { 0 }; 217 | 218 | self.hashmap.entry(px_int) 219 | // it can overflow on images over 2^24 pixels large 220 | .and_modify(move |e| e.0 = e.0.saturating_add(boost)) 221 | .or_insert((boost, rgba)); 222 | } 223 | 224 | fn reserve(&mut self, entries: usize) { 225 | let new_entries = entries.saturating_sub(self.hashmap.len() / 3); // assume some will be dupes, if called multiple times 226 | self.hashmap.reserve(new_entries); 227 | } 228 | 229 | #[inline(always)] 230 | const fn posterize_mask(&self) -> u32 { 231 | let channel_mask = 255 << self.posterize_bits; 232 | u32::from_ne_bytes([channel_mask, channel_mask, channel_mask, channel_mask]) 233 | } 234 | 235 | /// optionallys et 236 | fn init_posterize_bits(&mut self, posterize_bits: u8) { 237 | if self.posterize_bits >= posterize_bits { 238 | return; 239 | } 240 | self.posterize_bits = posterize_bits; 241 | let new_posterize_mask = self.posterize_mask(); 242 | 243 | let new_size = (self.hashmap.len()/3).max(self.hashmap.capacity()/5); 244 | let old_hashmap = std::mem::replace(&mut self.hashmap, HashMap::with_capacity_and_hasher(new_size, U32Hasher(0))); 245 | self.hashmap.extend(old_hashmap.into_iter().map(move |(k, v)| { 246 | (k & new_posterize_mask, v) 247 | })); 248 | } 249 | 250 | pub(crate) fn add_pixel_rows(&mut self, image: &DynamicRows<'_, '_>, importance_map: Option<&[u8]>, posterize_bits: u8) -> Result<(), Error> { 251 | let width = image.width as usize; 252 | let height = image.height as usize; 253 | 254 | debug_assert!(importance_map.map_or(true, |m| m.len() == image.width() * image.height())); 255 | 256 | let mut importance_map = importance_map.unwrap_or(&[]).chunks_exact(width).fuse(); 257 | let image_iter = image.rgba_rows_iter()?; 258 | 259 | let mut temp_row = temp_buf(width)?; 260 | for row in 0..height { 261 | let pixels_row = &image_iter.row_rgba(&mut temp_row, row)[..width]; 262 | let importance_map = importance_map.next().map(move |m| &m[..width]).unwrap_or(&[]); 263 | for (col, px) in pixels_row.iter().copied().enumerate() { 264 | let boost = importance_map.get(col).copied().unwrap_or(255); 265 | self.add_color(px, boost.into()); 266 | } 267 | } 268 | self.init_posterize_bits(posterize_bits); 269 | 270 | if self.hashmap.len() > self.max_histogram_entries as usize && self.posterize_bits < 3 { 271 | self.init_posterize_bits(self.posterize_bits + 1); 272 | } 273 | Ok(()) 274 | } 275 | 276 | pub(crate) fn finalize_builder(&mut self, gamma: f64) -> Result { 277 | debug_assert!(gamma > 0.); 278 | 279 | // Fixed colors will be put into normal hashmap, but with very high weight, 280 | // and temporarily 0 means this fixed max weight 281 | for &HashColor { rgba, .. } in &self.fixed_colors { 282 | let px_int = if rgba.a != 0 { 283 | unsafe { RGBAInt { rgba }.int } 284 | } else { 0 }; 285 | 286 | self.hashmap.insert(px_int, (0, rgba)); 287 | } 288 | 289 | let mut temp = Vec::new(); 290 | temp.try_reserve_exact(self.hashmap.len())?; 291 | 292 | let mut counts = [0; LIQ_MAXCLUSTER]; 293 | temp.extend(self.hashmap.values().map(|&(boost, color)| { 294 | let cluster_index = ((color.r >> 7) << 3) | ((color.g >> 7) << 2) | ((color.b >> 7) << 1) | (color.a >> 7); 295 | counts[cluster_index as usize] += 1; 296 | 297 | // fixed colors result in weight == 0. 298 | let weight = boost as f32; 299 | TempHistItem { color, weight, cluster_index } 300 | })); 301 | 302 | let mut clusters = [Cluster { begin: 0, end: 0 }; LIQ_MAXCLUSTER]; 303 | let mut next_begin = 0; 304 | for (cluster, count) in clusters.iter_mut().zip(counts) { 305 | cluster.begin = next_begin; 306 | cluster.end = next_begin; 307 | next_begin += count; 308 | } 309 | 310 | let mut items = Vec::new(); 311 | items.try_reserve_exact(temp.len())?; 312 | items.resize(temp.len(), HistItem { 313 | color: if cfg!(debug_assertions) { f_pixel( ARGBF { r:f32::NAN, g:f32::NAN, b:f32::NAN, a:f32::NAN } ) } else { f_pixel::default() }, 314 | adjusted_weight: if cfg!(debug_assertions) { f32::NAN } else { 0. }, 315 | perceptual_weight: if cfg!(debug_assertions) { f32::NAN } else { 0. }, 316 | mc_color_weight: if cfg!(debug_assertions) { f32::NAN } else { 0. }, 317 | tmp: HistSortTmp { mc_sort_value: if cfg!(debug_assertions) { !0 } else { 0 } }, 318 | }); 319 | let mut items = items.into_boxed_slice(); 320 | 321 | // Limit perceptual weight to 1/10th of the image surface area to prevent 322 | // a single color from dominating all others. 323 | let max_perceptual_weight = 0.1 * (temp.iter().map(|t| f64::from(t.weight)).sum::() / 256.) as f32; 324 | 325 | let lut = gamma_lut(gamma); 326 | let mut total_perceptual_weight = 0.; 327 | for temp_item in temp { 328 | let cluster = &mut clusters[temp_item.cluster_index as usize]; 329 | let next_index = cluster.end as usize; 330 | cluster.end += 1; 331 | 332 | // weight == 0 means it's a fixed color 333 | let weight = if temp_item.weight > 0. { 334 | (temp_item.weight * (1. / 256.)).min(max_perceptual_weight) 335 | } else { 336 | max_perceptual_weight * 10. 337 | }; 338 | total_perceptual_weight += f64::from(weight); 339 | 340 | items[next_index].color = f_pixel::from_rgba(&lut, temp_item.color); 341 | items[next_index].perceptual_weight = weight; 342 | items[next_index].adjusted_weight = weight; 343 | } 344 | 345 | let mut fixed_colors: Vec<_> = self.fixed_colors.iter().collect(); 346 | fixed_colors.sort_by_key(|c| c.index); // original order 347 | let fixed_colors = fixed_colors.iter().map(|c| f_pixel::from_rgba(&lut, c.rgba)).collect(); 348 | 349 | Ok(HistogramInternal { items, total_perceptual_weight, clusters, fixed_colors }) 350 | } 351 | } 352 | 353 | #[derive(Copy, Clone)] 354 | struct TempHistItem { 355 | color: RGBA, 356 | weight: f32, 357 | cluster_index: u8, 358 | } 359 | 360 | #[repr(C)] 361 | union RGBAInt { 362 | rgba: RGBA, 363 | int: u32, 364 | } 365 | 366 | /// Clusters form initial boxes for quantization, to ensure extreme colors are better represented 367 | pub const LIQ_MAXCLUSTER: usize = 16; 368 | 369 | pub(crate) struct HistogramInternal { 370 | pub items: Box<[HistItem]>, 371 | pub total_perceptual_weight: f64, 372 | pub clusters: [Cluster; LIQ_MAXCLUSTER], 373 | pub fixed_colors: Box<[f_pixel]>, 374 | } 375 | 376 | // Pre-grouped colors 377 | #[derive(Copy, Clone, Debug)] 378 | pub(crate) struct Cluster { 379 | pub begin: u32, 380 | pub end: u32, 381 | } 382 | 383 | // Simple deterministic hasher for the color hashmap 384 | impl std::hash::BuildHasher for U32Hasher { 385 | type Hasher = Self; 386 | 387 | #[inline(always)] 388 | fn build_hasher(&self) -> Self { 389 | Self(0) 390 | } 391 | } 392 | 393 | pub(crate) struct U32Hasher(pub u32); 394 | impl std::hash::Hasher for U32Hasher { 395 | // magic constant from fxhash. For a single 32-bit key that's all it needs! 396 | #[inline(always)] 397 | fn finish(&self) -> u64 { u64::from(self.0).wrapping_mul(0x517cc1b727220a95) } 398 | #[inline(always)] 399 | fn write_u32(&mut self, i: u32) { self.0 = i; } 400 | 401 | fn write(&mut self, _bytes: &[u8]) { unimplemented!() } 402 | fn write_u8(&mut self, _i: u8) { unimplemented!() } 403 | fn write_u16(&mut self, _i: u16) { unimplemented!() } 404 | fn write_u64(&mut self, _i: u64) { unimplemented!() } 405 | fn write_u128(&mut self, _i: u128) { unimplemented!() } 406 | fn write_usize(&mut self, _i: usize) { unimplemented!() } 407 | fn write_i8(&mut self, _i: i8) { unimplemented!() } 408 | fn write_i16(&mut self, _i: i16) { unimplemented!() } 409 | fn write_i32(&mut self, _i: i32) { unimplemented!() } 410 | fn write_i64(&mut self, _i: i64) { unimplemented!() } 411 | fn write_i128(&mut self, _i: i128) { unimplemented!() } 412 | fn write_isize(&mut self, _i: isize) { unimplemented!() } 413 | } 414 | 415 | /// ignores the index 416 | #[derive(PartialEq, Debug)] 417 | pub(crate) struct HashColor { 418 | pub rgba: RGBA, 419 | pub index: PalIndex, 420 | } 421 | 422 | #[allow(clippy::derived_hash_with_manual_eq)] 423 | impl Hash for HashColor { 424 | #[inline] 425 | fn hash(&self, state: &mut H) { 426 | let s: &[u8] = self.rgba.as_ref(); 427 | u32::from_ne_bytes(s.try_into().unwrap()).hash(state); 428 | } 429 | } 430 | 431 | impl Eq for HashColor { 432 | fn assert_receiver_is_total_eq(&self) {} 433 | } 434 | -------------------------------------------------------------------------------- /src/image.rs: -------------------------------------------------------------------------------- 1 | use crate::attr::Attributes; 2 | use crate::blur::{liq_blur, liq_max3, liq_min3}; 3 | use crate::error::*; 4 | use crate::pal::{f_pixel, PalF, PalIndexRemap, MAX_COLORS, MIN_OPAQUE_A, RGBA}; 5 | use crate::remap::DitherMapMode; 6 | use crate::rows::{DynamicRows, PixelsSource}; 7 | use crate::PushInCapacity; 8 | use crate::LIQ_HIGH_MEMORY_LIMIT; 9 | use crate::seacow::{RowBitmap, SeaCow}; 10 | use rgb::prelude::*; 11 | use std::mem::MaybeUninit; 12 | 13 | /// Describes image dimensions and pixels for the library 14 | /// 15 | /// Create one using [`Attributes::new_image()`]. 16 | /// 17 | /// All images are internally in the RGBA format. 18 | #[derive(Clone)] 19 | pub struct Image<'pixels> { 20 | pub(crate) px: DynamicRows<'pixels, 'pixels>, 21 | pub(crate) importance_map: Option>, 22 | pub(crate) edges: Option>, 23 | pub(crate) dither_map: Option>, 24 | pub(crate) background: Option>>, 25 | pub(crate) fixed_colors: Vec, 26 | } 27 | 28 | impl<'pixels> Image<'pixels> { 29 | /// Makes an image from RGBA pixels. 30 | /// 31 | /// See the [`rgb`] and [`bytemuck`](https://lib.rs/bytemuck) crates for making `[RGBA]` slices from `[u8]` slices. 32 | /// 33 | /// The `pixels` argument can be `Vec`, or `Box<[RGBA]>` or `&[RGBA]`. 34 | /// 35 | /// If you want to supply RGB or ARGB pixels, convert them to RGBA first, or use [`Image::new_fn`] to supply your own pixel-swapping function. 36 | /// 37 | /// Use `0.` for gamma if the image is sRGB (most images are). 38 | #[inline(always)] 39 | pub fn new(attr: &Attributes, pixels: VecRGBA, width: usize, height: usize, gamma: f64) -> Result where VecRGBA: Into> { 40 | Self::new_stride(attr, pixels, width, height, width, gamma) 41 | } 42 | 43 | /// Describe dimensions of a slice of RGBA pixels. 44 | /// 45 | /// Same as [`Image::new`], except it doesn't copy the pixels, but holds a temporary reference instead. 46 | /// 47 | /// If you want to supply RGB or ARGB pixels, use [`Image::new_fn`] to supply your own pixel-swapping function. 48 | /// 49 | /// See the [`rgb`] and [`bytemuck`](https://lib.rs/bytemuck) crates for making `[RGBA]` slices from `[u8]` slices. 50 | /// 51 | /// Use `0.` for gamma if the image is sRGB (most images are). 52 | #[inline(always)] 53 | pub fn new_borrowed(attr: &Attributes, pixels: &'pixels [RGBA], width: usize, height: usize, gamma: f64) -> Result { 54 | Self::new_stride_borrowed(attr, pixels, width, height, width, gamma) 55 | } 56 | 57 | /// Generate rows on demand using a callback function. 58 | /// 59 | /// The callback function should be cheap (e.g. just byte-swap pixels). The parameters are: line of RGBA pixels (slice's len is equal to image width), and row number (0-indexed). 60 | /// The callback will be called multiple times per row. May be called from multiple threads at once. 61 | /// 62 | /// Use `0.` for gamma if the image is sRGB (most images are). 63 | /// 64 | /// ## Safety 65 | /// 66 | /// This function is marked as unsafe, because the callback function MUST initialize the entire row (call `write` on every `MaybeUninit` pixel). 67 | /// 68 | pub unsafe fn new_fn], usize) + Send + Sync>(attr: &Attributes, convert_row_fn: F, width: usize, height: usize, gamma: f64) -> Result { 69 | let width = width.try_into().map_err(|_| ValueOutOfRange)?; 70 | let height = height.try_into().map_err(|_| ValueOutOfRange)?; 71 | Image::new_internal(attr, PixelsSource::Callback(Box::new(convert_row_fn)), width, height, gamma) 72 | } 73 | 74 | pub(crate) fn free_histogram_inputs(&mut self) { 75 | // importance_map must stay for remapping, because remap performs kmeans on potentially-unimportant pixels 76 | self.px.free_histogram_inputs(); 77 | } 78 | 79 | pub(crate) fn new_internal( 80 | attr: &Attributes, 81 | pixels: PixelsSource<'pixels, 'pixels>, 82 | width: u32, 83 | height: u32, 84 | gamma: f64, 85 | ) -> Result { 86 | if !Self::check_image_size(width, height) { 87 | return Err(ValueOutOfRange); 88 | } 89 | 90 | if !(0. ..=1.).contains(&gamma) { 91 | attr.verbose_print(" error: gamma must be >= 0 and <= 1 (try 1/gamma instead)"); 92 | return Err(ValueOutOfRange); 93 | } 94 | let img = Image { 95 | px: DynamicRows::new( 96 | width, 97 | height, 98 | pixels, 99 | if gamma > 0. { gamma } else { 0.45455 }, 100 | ), 101 | importance_map: None, 102 | edges: None, 103 | dither_map: None, 104 | background: None, 105 | fixed_colors: Vec::new(), 106 | }; 107 | // if image is huge or converted pixels are not likely to be reused then don't cache converted pixels 108 | let low_memory_hint = !attr.use_contrast_maps && attr.use_dither_map == DitherMapMode::None; 109 | let limit = if low_memory_hint { LIQ_HIGH_MEMORY_LIMIT / 8 } else { LIQ_HIGH_MEMORY_LIMIT } / std::mem::size_of::(); 110 | if (img.width()) * (img.height()) > limit { 111 | attr.verbose_print(" conserving memory"); // for simplicity of this API there's no explicit pixels argument, 112 | } 113 | Ok(img) 114 | } 115 | 116 | fn check_image_size(width: u32, height: u32) -> bool { 117 | if width == 0 || height == 0 { 118 | return false; 119 | } 120 | if width.max(height) as usize > i32::MAX as usize || 121 | width as usize > isize::MAX as usize / std::mem::size_of::() / height as usize { 122 | return false; 123 | } 124 | true 125 | } 126 | 127 | pub(crate) fn update_dither_map(&mut self, remapped_image: &RowBitmap<'_, PalIndexRemap>, palette: &PalF, uses_background: bool) -> Result<(), Error> { 128 | if self.edges.is_none() { 129 | self.contrast_maps()?; 130 | } 131 | let Some(mut edges) = self.edges.take() else { return Ok(()) }; 132 | let colors = palette.as_slice(); 133 | 134 | let width = self.width(); 135 | let mut prev_row: Option<&[_]> = None; 136 | let mut rows = remapped_image.rows().zip(edges.chunks_exact_mut(width)).peekable(); 137 | while let Some((this_row, edges)) = rows.next() { 138 | let mut lastpixel = this_row[0]; 139 | let mut lastcol = 0; 140 | for (col, px) in this_row.iter().copied().enumerate().skip(1) { 141 | if uses_background && (colors[px as usize]).a < MIN_OPAQUE_A { 142 | // Transparency may or may not create an edge. When there's an explicit background set, assume no edge. 143 | continue; 144 | } 145 | if px != lastpixel || col == width - 1 { 146 | let mut neighbor_count = 10 * (col - lastcol); 147 | let mut i = lastcol; 148 | while i < col { 149 | if let Some(prev_row) = prev_row { 150 | let pixelabove = prev_row[i]; 151 | if pixelabove == lastpixel { neighbor_count += 15; }; 152 | } 153 | if let Some((next_row, _)) = rows.peek() { 154 | let pixelbelow = next_row[i]; 155 | if pixelbelow == lastpixel { neighbor_count += 15; }; 156 | } 157 | i += 1; 158 | } 159 | while lastcol <= col { 160 | edges[lastcol] = (f32::from(u16::from(edges[lastcol]) + 128) 161 | * (255. / (255 + 128) as f32) 162 | * (1. - 20. / (20 + neighbor_count) as f32)) 163 | as u8; 164 | lastcol += 1; 165 | } 166 | lastpixel = px; 167 | } 168 | } 169 | prev_row = Some(this_row); 170 | } 171 | self.dither_map = Some(edges); 172 | Ok(()) 173 | } 174 | 175 | /// Set which pixels are more important (and more likely to get a palette entry) 176 | /// 177 | /// The map must be `width`×`height` pixels large. Higher numbers = more important. 178 | pub fn set_importance_map(&mut self, map: impl Into>) -> Result<(), Error> { 179 | let map = map.into(); 180 | if map.len() != self.width() * self.height() { 181 | return Err(BufferTooSmall); 182 | } 183 | self.importance_map = Some(map); 184 | Ok(()) 185 | } 186 | 187 | /// Remap pixels assuming they will be displayed on this background. This is designed for GIF's "keep" mode. 188 | /// 189 | /// Pixels that match the background color will be made transparent if there's a fully transparent color available in the palette. 190 | /// 191 | /// The background image's pixels must outlive this image. 192 | pub fn set_background(&mut self, background: Self) -> Result<(), Error> { 193 | if background.background.is_some() { 194 | return Err(Unsupported); 195 | } 196 | if self.px.width != background.px.width || self.px.height != background.px.height { 197 | return Err(BufferTooSmall); 198 | } 199 | self.background = Some(Box::new(background)); 200 | Ok(()) 201 | } 202 | 203 | /// Reserves a color in the output palette created from this image. It behaves as if the given color was used in the image and was very important. 204 | /// 205 | /// The RGB values are assumed to have the same gamma as the image. 206 | /// 207 | /// It must be called before the image is quantized. 208 | /// 209 | /// Returns error if more than 256 colors are added. If image is quantized to fewer colors than the number of fixed colors added, then excess fixed colors will be ignored. 210 | pub fn add_fixed_color(&mut self, color: RGBA) -> Result<(), Error> { 211 | if self.fixed_colors.len() >= MAX_COLORS { return Err(Unsupported); } 212 | self.fixed_colors.try_reserve(1)?; 213 | self.fixed_colors.push_in_cap(color); 214 | Ok(()) 215 | } 216 | 217 | /// Width of the image in pixels 218 | #[must_use] 219 | #[inline(always)] 220 | pub const fn width(&self) -> usize { 221 | self.px.width as _ 222 | } 223 | 224 | /// Height of the image in pixels 225 | #[must_use] 226 | #[inline(always)] 227 | pub const fn height(&self) -> usize { 228 | self.px.height as _ 229 | } 230 | 231 | #[inline(always)] 232 | pub(crate) fn gamma(&self) -> Option { 233 | if self.px.gamma > 0. { Some(self.px.gamma) } else { None } 234 | } 235 | 236 | /// Builds two maps: 237 | /// `importance_map` - approximation of areas with high-frequency noise, except straight edges. 1=flat, 0=noisy. 238 | /// edges - noise map including all edges 239 | pub(crate) fn contrast_maps(&mut self) -> Result<(), Error> { 240 | let width = self.width(); 241 | let height = self.height(); 242 | if width < 4 || height < 4 || (3 * width * height) > LIQ_HIGH_MEMORY_LIMIT { 243 | return Ok(()); // shrug 244 | } 245 | 246 | let noise = if let Some(n) = self.importance_map.as_deref_mut() { n } else { 247 | let vec = try_zero_vec(width * height)?; 248 | self.importance_map.get_or_insert_with(move || vec.into_boxed_slice()) 249 | }; 250 | 251 | let edges = if let Some(e) = self.edges.as_mut() { e } else { 252 | let vec = try_zero_vec(width * height)?; 253 | self.edges.get_or_insert_with(move || vec.into_boxed_slice()) 254 | }; 255 | 256 | let mut rows_iter = self.px.all_rows_f()?.chunks_exact(width); 257 | 258 | let mut next_row = rows_iter.next().ok_or(Error::InvalidPointer)?; 259 | let mut curr_row = next_row; 260 | let mut prev_row; 261 | 262 | for (noise_row, edges_row) in noise[..width * height].chunks_exact_mut(width).zip(edges[..width * height].chunks_exact_mut(width)) { 263 | prev_row = curr_row; 264 | curr_row = next_row; 265 | next_row = rows_iter.next().unwrap_or(next_row); 266 | let mut prev; 267 | let mut curr = curr_row[0].0; 268 | let mut next = curr; 269 | for i in 0..width { 270 | prev = curr; 271 | curr = next; 272 | next = curr_row[(i + 1).min(width - 1)].0; 273 | // contrast is difference between pixels neighbouring horizontally and vertically 274 | let horiz = (prev + next - curr * 2.).map(f32::abs); // noise is amplified 275 | let prevl = prev_row[i].0; 276 | let nextl = next_row[i].0; 277 | let vert = (prevl + nextl - curr * 2.).map(f32::abs); 278 | let horiz = horiz.a.max(horiz.r).max(horiz.g.max(horiz.b)); 279 | let vert = vert.a.max(vert.r).max(vert.g.max(vert.b)); 280 | let edge = horiz.max(vert); 281 | let mut z = (horiz - vert).abs().mul_add(-0.5, edge); 282 | z = 1. - z.max(horiz.min(vert)); 283 | z *= z; 284 | z *= z; 285 | // 85 is about 1/3rd of weight (not 0, because noisy pixels still need to be included, just not as precisely). 286 | noise_row[i] = z.mul_add(176., 80.) as u8; 287 | edges_row[i] = ((1. - edge) * 256.) as u8; 288 | } 289 | } 290 | // noise areas are shrunk and then expanded to remove thin edges from the map 291 | let mut tmp = try_zero_vec(width * height)?; 292 | liq_max3(noise, &mut tmp, width, height); 293 | liq_max3(&tmp, noise, width, height); 294 | liq_blur(noise, &mut tmp, width, height, 3); 295 | liq_max3(noise, &mut tmp, width, height); 296 | liq_min3(&tmp, noise, width, height); 297 | liq_min3(noise, &mut tmp, width, height); 298 | liq_min3(&tmp, noise, width, height); 299 | liq_min3(edges, &mut tmp, width, height); 300 | liq_max3(&tmp, edges, width, height); 301 | for (edges, noise) in edges.iter_mut().zip(noise) { 302 | *edges = (*noise).min(*edges); 303 | } 304 | Ok(()) 305 | } 306 | 307 | /// Stride is in pixels. Allows defining regions of larger images or images with padding without copying. The stride is in pixels. 308 | /// 309 | /// Otherwise the same as [`Image::new_borrowed`]. 310 | #[inline(always)] 311 | pub fn new_stride_borrowed(attr: &Attributes, pixels: &'pixels [RGBA], width: usize, height: usize, stride: usize, gamma: f64) -> Result { 312 | Self::new_stride_internal(attr, SeaCow::borrowed(pixels), width, height, stride, gamma) 313 | } 314 | 315 | /// Create new image by copying `pixels` to an internal buffer, so that it makes a self-contained type. 316 | /// 317 | /// The `pixels` argument can be `Vec`, or `Box<[RGBA]>` or `&[RGBA]`. 318 | /// 319 | /// Otherwise the same as [`Image::new_stride_borrowed`]. 320 | #[inline] 321 | pub fn new_stride(attr: &Attributes, pixels: VecRGBA, width: usize, height: usize, stride: usize, gamma: f64) -> Result, Error> where VecRGBA: Into> { 322 | Self::new_stride_internal(attr, SeaCow::boxed(pixels.into()), width, height, stride, gamma) 323 | } 324 | 325 | fn new_stride_internal<'a>(attr: &Attributes, pixels: SeaCow<'a, RGBA>, width: usize, height: usize, stride: usize, gamma: f64) -> Result, Error> { 326 | let width = width.try_into().map_err(|_| ValueOutOfRange)?; 327 | let height = height.try_into().map_err(|_| ValueOutOfRange)?; 328 | let stride = stride.try_into().map_err(|_| ValueOutOfRange)?; 329 | 330 | let pixels_len = pixels.as_slice().len(); 331 | let pixels_rows = match PixelsSource::for_pixels(pixels, width, height, stride) { 332 | Ok(p) => p, 333 | Err(e) => { 334 | attr.verbose_print(format!("Buffer length is {} bytes, which is not enough for {}×{}×4 RGBA bytes", pixels_len * 4, stride, height)); 335 | return Err(e); 336 | }, 337 | }; 338 | Image::new_internal(attr, pixels_rows, width, height, gamma) 339 | } 340 | } 341 | 342 | fn try_zero_vec(len: usize) -> Result, Error> { 343 | let mut vec = Vec::new(); 344 | vec.try_reserve_exact(len)?; 345 | vec.resize(len, 0); 346 | Ok(vec) 347 | } 348 | -------------------------------------------------------------------------------- /src/kmeans.rs: -------------------------------------------------------------------------------- 1 | use crate::CacheLineAlign; 2 | use crate::hist::{HistItem, HistogramInternal}; 3 | use crate::nearest::Nearest; 4 | use crate::pal::{f_pixel, PalF, PalIndex, PalPop}; 5 | use crate::rayoff::*; 6 | use crate::Error; 7 | use rgb::Argb; 8 | use rgb::prelude::*; 9 | use std::cell::RefCell; 10 | 11 | /// K-Means iteration: new palette color is computed from weighted average of colors that map best to that palette entry. 12 | // avoid false sharing 13 | pub(crate) struct Kmeans { 14 | averages: Vec, 15 | weighed_diff_sum: f64, 16 | } 17 | 18 | #[derive(Copy, Clone, Default)] 19 | struct ColorAvg { 20 | pub sum: Argb, 21 | pub total: f64, 22 | } 23 | 24 | impl Kmeans { 25 | #[inline] 26 | pub fn new(pal_len: usize) -> Result { 27 | let mut averages = Vec::new(); 28 | averages.try_reserve_exact(pal_len)?; 29 | averages.resize(pal_len, ColorAvg::default()); 30 | Ok(Self { 31 | averages, 32 | weighed_diff_sum: 0., 33 | }) 34 | } 35 | 36 | #[inline] 37 | pub fn update_color(&mut self, px: f_pixel, value: f32, matched: PalIndex) { 38 | let c = &mut self.averages[matched as usize]; 39 | c.sum += (px.0 * value).map(f64::from); 40 | c.total += f64::from(value); 41 | } 42 | 43 | pub fn finalize(self, palette: &mut PalF) -> f64 { 44 | for (avg, (color, pop)) in self.averages.iter().zip(palette.iter_mut()).filter(|(_, (_, pop))| !pop.is_fixed()) { 45 | let total = avg.total; 46 | *pop = PalPop::new(total as f32); 47 | if total > 0. && color.a != 0. { 48 | *color = avg.sum.map(move |c| (c / total) as f32).into(); 49 | } 50 | } 51 | self.weighed_diff_sum 52 | } 53 | 54 | #[inline(never)] 55 | pub(crate) fn iteration(hist: &mut HistogramInternal, palette: &mut PalF, adjust_weight: bool) -> Result { 56 | if hist.items.is_empty() { 57 | return Ok(0.); 58 | } 59 | 60 | let n = Nearest::new(palette)?; 61 | let colors = palette.as_slice(); 62 | let len = colors.len(); 63 | 64 | let tls = ThreadLocal::new(); 65 | let total = hist.total_perceptual_weight; 66 | 67 | // chunk size is a trade-off between parallelization and overhead 68 | hist.items.par_chunks_mut(256).for_each({ 69 | let tls = &tls; 70 | move |batch| { 71 | let kmeans = tls.get_or(move || CacheLineAlign(RefCell::new(Self::new(len)))); 72 | if let Ok(ref mut kmeans) = *kmeans.0.borrow_mut() { 73 | kmeans.iterate_batch(batch, &n, colors, adjust_weight); 74 | } 75 | } 76 | }); 77 | 78 | let diff = tls.into_iter() 79 | .map(|c| c.0.into_inner()) 80 | .reduce(Self::try_merge) 81 | .transpose()? 82 | .map_or(0., |kmeans| kmeans.finalize(palette) / total); 83 | 84 | replace_unused_colors(palette, hist)?; 85 | Ok(diff) 86 | } 87 | 88 | fn iterate_batch(&mut self, batch: &mut [HistItem], n: &Nearest, colors: &[f_pixel], adjust_weight: bool) { 89 | self.weighed_diff_sum += batch.iter_mut().map(|item| { 90 | let px = item.color; 91 | let (matched, mut diff) = n.search(&px, item.likely_palette_index()); 92 | item.tmp.likely_palette_index = matched; 93 | if adjust_weight { 94 | let remapped = colors[matched as usize]; 95 | let (_, new_diff) = n.search(&f_pixel(px.0 + px.0 - remapped.0), matched); 96 | diff = new_diff; 97 | item.adjusted_weight = 2.0f32.mul_add(item.adjusted_weight, item.perceptual_weight) * (0.5 + diff); 98 | } 99 | debug_assert!(f64::from(diff) < 1e20); 100 | self.update_color(px, item.adjusted_weight, matched); 101 | f64::from(diff * item.perceptual_weight) 102 | }).sum::(); 103 | } 104 | 105 | #[inline] 106 | pub fn merge(mut self, new: Self) -> Self { 107 | self.weighed_diff_sum += new.weighed_diff_sum; 108 | self.averages.iter_mut().zip(new.averages).for_each(|(p, n)| { 109 | p.sum += n.sum; 110 | p.total += n.total; 111 | }); 112 | self 113 | } 114 | 115 | #[inline] 116 | pub fn try_merge(old: Result, new: Result) -> Result { 117 | match (old, new) { 118 | (Ok(old), Ok(new)) => Ok(Self::merge(old, new)), 119 | (Err(e), _) | (_, Err(e)) => Err(e), 120 | } 121 | } 122 | } 123 | 124 | /// kmeans may have merged or obsoleted some palette entries. 125 | /// This replaces these entries with histogram colors that are currently least-fitting the palette. 126 | fn replace_unused_colors(palette: &mut PalF, hist: &HistogramInternal) -> Result<(), Error> { 127 | for pal_idx in 0..palette.len() { 128 | let pop = palette.pop_as_slice()[pal_idx]; 129 | if pop.popularity() == 0. && !pop.is_fixed() { 130 | let n = Nearest::new(palette)?; 131 | let mut worst = None; 132 | let mut worst_diff = 0.; 133 | let colors = palette.as_slice(); 134 | // the search is just for diff, ignoring adjusted_weight, 135 | // because the palette already optimizes for the max weight, so it'd likely find another redundant entry. 136 | for item in hist.items.iter() { 137 | // the early reject avoids running full palette search for every entry 138 | let may_be_worst = colors.get(item.likely_palette_index() as usize) 139 | .map_or(true, |pal| pal.diff(&item.color) > worst_diff); 140 | if may_be_worst { 141 | let diff = n.search(&item.color, item.likely_palette_index()).1; 142 | if diff > worst_diff { 143 | worst_diff = diff; 144 | worst = Some(item); 145 | } 146 | } 147 | } 148 | if let Some(worst) = worst { 149 | palette.set(pal_idx, worst.color, PalPop::new(worst.adjusted_weight)); 150 | } 151 | } 152 | } 153 | Ok(()) 154 | } 155 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! 2 | //! 3 | //! Converts RGBA images to 8-bit with alpha channel. 4 | //! 5 | //! See `examples/` directory for example code. 6 | #![doc(html_logo_url = "https://pngquant.org/pngquant-logo.png")] 7 | #![deny(missing_docs)] 8 | #![allow(clippy::bool_to_int_with_if)] 9 | #![allow(clippy::cast_possible_truncation)] 10 | #![allow(clippy::doc_markdown)] 11 | #![allow(clippy::if_not_else)] 12 | #![allow(clippy::inline_always)] 13 | #![allow(clippy::items_after_statements)] 14 | #![allow(clippy::map_unwrap_or)] 15 | #![allow(clippy::missing_errors_doc)] 16 | #![allow(clippy::module_name_repetitions)] 17 | #![allow(clippy::redundant_closure_for_method_calls)] 18 | #![allow(clippy::unreadable_literal)] 19 | #![allow(clippy::wildcard_imports)] 20 | #![deny(clippy::semicolon_if_nothing_returned)] 21 | 22 | mod attr; 23 | mod blur; 24 | mod error; 25 | mod hist; 26 | mod image; 27 | mod kmeans; 28 | mod mediancut; 29 | mod nearest; 30 | mod pal; 31 | mod quant; 32 | mod remap; 33 | mod rows; 34 | mod seacow; 35 | 36 | #[cfg(not(feature = "threads"))] 37 | mod rayoff; 38 | 39 | #[cfg(feature = "threads")] 40 | mod rayoff { 41 | pub(crate) fn num_cpus() -> usize { std::thread::available_parallelism().map(|n| n.get()).unwrap_or(1) } 42 | pub(crate) use rayon::prelude::{ParallelBridge, ParallelIterator, ParallelSliceMut}; 43 | pub(crate) use rayon::in_place_scope as scope; 44 | pub(crate) use thread_local::ThreadLocal; 45 | } 46 | 47 | #[cfg_attr(feature = "threads", repr(align(128)))] 48 | pub(crate) struct CacheLineAlign(pub T); 49 | 50 | /// Use imagequant-sys crate instead 51 | #[cfg(feature = "_internal_c_ffi")] 52 | pub mod capi; 53 | 54 | pub use attr::{Attributes, ControlFlow}; 55 | pub use error::Error; 56 | pub use hist::{Histogram, HistogramEntry}; 57 | pub use image::Image; 58 | #[doc(hidden)] 59 | pub use pal::Palette; 60 | pub use pal::RGBA; 61 | pub use quant::QuantizationResult; 62 | 63 | #[doc(hidden)] 64 | #[deprecated(note = "Please use the imagequant::Error type. This will be removed")] 65 | pub use error::Error as liq_error; 66 | 67 | const LIQ_HIGH_MEMORY_LIMIT: usize = 1 << 26; 68 | 69 | /// [Start here][Attributes]: creates new handle for library configuration 70 | /// 71 | /// See [`Attributes`] 72 | #[inline(always)] 73 | #[must_use] 74 | pub fn new() -> Attributes { 75 | Attributes::new() 76 | } 77 | 78 | #[test] 79 | fn copy_img() { 80 | let tmp = vec![RGBA::new(1, 2, 3, 4); 10 * 100]; 81 | let liq = Attributes::new(); 82 | let _ = liq.new_image_stride(tmp, 10, 100, 10, 0.).unwrap(); 83 | } 84 | 85 | #[test] 86 | fn takes_rgba() { 87 | let liq = Attributes::new(); 88 | 89 | let img = vec![RGBA { r: 0, g: 0, b: 0, a: 0 }; 8]; 90 | 91 | liq.new_image_borrowed(&img, 1, 1, 0.0).unwrap(); 92 | liq.new_image_borrowed(&img, 4, 2, 0.0).unwrap(); 93 | liq.new_image_borrowed(&img, 8, 1, 0.0).unwrap(); 94 | assert!(liq.new_image_borrowed(&img, 9, 1, 0.0).is_err()); 95 | assert!(liq.new_image_borrowed(&img, 4, 3, 0.0).is_err()); 96 | } 97 | 98 | #[test] 99 | fn histogram() { 100 | let attr = Attributes::new(); 101 | let mut hist = Histogram::new(&attr); 102 | 103 | let bitmap1 = [RGBA { r: 0, g: 0, b: 0, a: 0 }; 1]; 104 | let mut image1 = attr.new_image(&bitmap1[..], 1, 1, 0.0).unwrap(); 105 | hist.add_image(&attr, &mut image1).unwrap(); 106 | 107 | let bitmap2 = [RGBA { r: 255, g: 255, b: 255, a: 255 }; 1]; 108 | let mut image2 = attr.new_image(&bitmap2[..], 1, 1, 0.0).unwrap(); 109 | hist.add_image(&attr, &mut image2).unwrap(); 110 | 111 | hist.add_colors(&[HistogramEntry { 112 | color: RGBA::new(255, 128, 255, 128), 113 | count: 10, 114 | }], 0.0).unwrap(); 115 | 116 | let mut res = hist.quantize(&attr).unwrap(); 117 | let pal = res.palette(); 118 | assert_eq!(3, pal.len()); 119 | } 120 | 121 | #[test] 122 | fn poke_it() { 123 | let width = 10usize; 124 | let height = 10usize; 125 | let mut fakebitmap = vec![RGBA::new(255, 255, 255, 255); width * height]; 126 | 127 | fakebitmap[0].r = 0x55; 128 | fakebitmap[0].g = 0x66; 129 | fakebitmap[0].b = 0x77; 130 | 131 | // Configure the library 132 | let mut liq = Attributes::new(); 133 | liq.set_speed(5).unwrap(); 134 | liq.set_quality(70, 99).unwrap(); 135 | liq.set_min_posterization(1).unwrap(); 136 | assert_eq!(1, liq.min_posterization()); 137 | liq.set_min_posterization(0).unwrap(); 138 | 139 | use std::sync::atomic::AtomicBool; 140 | use std::sync::atomic::Ordering::SeqCst; 141 | use std::sync::Arc; 142 | 143 | let log_called = Arc::new(AtomicBool::new(false)); 144 | let log_called2 = log_called.clone(); 145 | liq.set_log_callback(move |_attr, _msg| { 146 | log_called2.store(true, SeqCst); 147 | }); 148 | 149 | let prog_called = Arc::new(AtomicBool::new(false)); 150 | let prog_called2 = prog_called.clone(); 151 | liq.set_progress_callback(move |_perc| { 152 | prog_called2.store(true, SeqCst); 153 | ControlFlow::Continue 154 | }); 155 | 156 | // Describe the bitmap 157 | let img = &mut liq.new_image(&fakebitmap[..], width, height, 0.0).unwrap(); 158 | 159 | // The magic happens in quantize() 160 | let mut res = match liq.quantize(img) { 161 | Ok(res) => res, 162 | Err(err) => panic!("Quantization failed, because: {err:?}"), 163 | }; 164 | 165 | // Enable dithering for subsequent remappings 166 | res.set_dithering_level(1.0).unwrap(); 167 | 168 | // You can reuse the result to generate several images with the same palette 169 | let (palette, pixels) = res.remapped(img).unwrap(); 170 | 171 | assert_eq!(width * height, pixels.len()); 172 | assert_eq!(100, res.quantization_quality().unwrap()); 173 | assert_eq!(RGBA { r: 255, g: 255, b: 255, a: 255 }, palette[0]); 174 | assert_eq!(RGBA { r: 0x55, g: 0x66, b: 0x77, a: 255 }, palette[1]); 175 | 176 | assert!(log_called.load(SeqCst)); 177 | assert!(prog_called.load(SeqCst)); 178 | } 179 | 180 | #[test] 181 | fn set_importance_map() { 182 | let liq = new(); 183 | let bitmap = &[RGBA::new(255, 0, 0, 255), RGBA::new(0u8, 0, 255, 255)]; 184 | let mut img = liq.new_image(&bitmap[..], 2, 1, 0.).unwrap(); 185 | let map = &[255, 0]; 186 | img.set_importance_map(&map[..]).unwrap(); 187 | let mut res = liq.quantize(&mut img).unwrap(); 188 | let pal = res.palette(); 189 | assert_eq!(1, pal.len(), "{pal:?}"); 190 | assert_eq!(bitmap[0], pal[0]); 191 | } 192 | 193 | #[test] 194 | fn thread() { 195 | let liq = Attributes::new(); 196 | std::thread::spawn(move || { 197 | let b = vec![RGBA::new(0, 0, 0, 0); 1]; 198 | liq.new_image_borrowed(&b, 1, 1, 0.).unwrap(); 199 | }).join().unwrap(); 200 | } 201 | 202 | #[test] 203 | fn r_callback_test() { 204 | use std::mem::MaybeUninit; 205 | use std::sync::atomic::AtomicU16; 206 | use std::sync::atomic::Ordering::SeqCst; 207 | use std::sync::Arc; 208 | 209 | let called = Arc::new(AtomicU16::new(0)); 210 | let called2 = called.clone(); 211 | let mut res = { 212 | let a = new(); 213 | let get_row = move |output_row: &mut [MaybeUninit], y: usize| { 214 | assert!((0..5).contains(&y)); 215 | assert_eq!(123, output_row.len()); 216 | for (n, out) in output_row.iter_mut().enumerate() { 217 | let n = n as u8; 218 | out.write(RGBA::new(n, n, n, n)); 219 | } 220 | called2.fetch_add(1, SeqCst); 221 | }; 222 | let mut img = unsafe { 223 | Image::new_fn(&a, get_row, 123, 5, 0.).unwrap() 224 | }; 225 | a.quantize(&mut img).unwrap() 226 | }; 227 | let called = called.load(SeqCst); 228 | assert!(called > 5 && called < 50); 229 | assert_eq!(123, res.palette().len()); 230 | } 231 | 232 | #[test] 233 | fn sizes() { 234 | use pal::PalF; 235 | use pal::Palette; 236 | assert!(std::mem::size_of::() < crate::pal::MAX_COLORS*(8*4)+32, "{}", std::mem::size_of::()); 237 | assert!(std::mem::size_of::() < std::mem::size_of::() + std::mem::size_of::() + 100, "{}", std::mem::size_of::()); 238 | assert!(std::mem::size_of::() < 200); 239 | assert!(std::mem::size_of::() < 300); 240 | assert!(std::mem::size_of::() < 200); 241 | assert!(std::mem::size_of::() <= 32); 242 | } 243 | 244 | #[doc(hidden)] 245 | pub fn _unstable_internal_kmeans_bench() -> impl FnMut() { 246 | use crate::pal::{PalF, PalPop}; 247 | 248 | let attr = new(); 249 | let mut h = hist::Histogram::new(&attr); 250 | 251 | let e = (0..10000u32).map(|i| HistogramEntry { 252 | count: i.wrapping_mul(17) % 12345, 253 | color: RGBA::new(i as u8, (i.wrapping_mul(7) >> 2) as u8, (i.wrapping_mul(11) >> 11) as u8, 255), 254 | }).collect::>(); 255 | 256 | h.add_colors(&e, 0.).unwrap(); 257 | let mut hist = h.finalize_builder(0.45455).unwrap(); 258 | 259 | let lut = pal::gamma_lut(0.45455); 260 | let mut p = PalF::new(); 261 | for i in 0..=255 { 262 | p.push(pal::f_pixel::from_rgba(&lut, RGBA::new(i|7, i, i, 255)), PalPop::new(1.)); 263 | } 264 | 265 | move || { 266 | kmeans::Kmeans::iteration(&mut hist, &mut p, false).unwrap(); 267 | } 268 | } 269 | 270 | trait PushInCapacity { 271 | fn push_in_cap(&mut self, val: T); 272 | } 273 | 274 | impl PushInCapacity for Vec { 275 | #[track_caller] 276 | #[inline(always)] 277 | fn push_in_cap(&mut self, val: T) { 278 | debug_assert!(self.capacity() != self.len()); 279 | if self.capacity() != self.len() { 280 | self.push(val); 281 | } 282 | } 283 | } 284 | 285 | /// Rust is too conservative about sorting floats. 286 | /// This library uses only finite values, so they're sortable. 287 | #[derive(Debug, PartialEq, PartialOrd, Copy, Clone)] 288 | #[repr(transparent)] 289 | struct OrdFloat(pub(crate) T); 290 | 291 | impl Eq for OrdFloat { 292 | } 293 | 294 | impl Ord for OrdFloat { 295 | #[inline] 296 | fn cmp(&self, other: &Self) -> std::cmp::Ordering { 297 | self.0.partial_cmp(&other.0).unwrap_or(std::cmp::Ordering::Equal) 298 | } 299 | } 300 | 301 | impl Eq for OrdFloat { 302 | } 303 | 304 | impl Ord for OrdFloat { 305 | #[inline] 306 | fn cmp(&self, other: &Self) -> std::cmp::Ordering { 307 | self.0.partial_cmp(&other.0).unwrap_or(std::cmp::Ordering::Equal) 308 | } 309 | } 310 | 311 | impl OrdFloat { 312 | pub fn new(v: f32) -> Self { 313 | debug_assert!(v.is_finite()); 314 | Self(v) 315 | } 316 | } 317 | 318 | impl OrdFloat { 319 | pub fn new64(v: f64) -> Self { 320 | debug_assert!(v.is_finite()); 321 | Self(v) 322 | } 323 | } 324 | 325 | #[test] 326 | fn test_fixed_colors() { 327 | let attr = Attributes::new(); 328 | let mut h = Histogram::new(&attr); 329 | let tmp = (0..128).map(|c| HistogramEntry { 330 | color: RGBA::new(c,c,c,255), 331 | count: 1, 332 | }).collect::>(); 333 | h.add_colors(&tmp, 0.).unwrap(); 334 | for f in 200..255 { 335 | h.add_fixed_color(RGBA::new(f, f, f, 255), 0.).unwrap(); 336 | } 337 | let mut r = h.quantize(&attr).unwrap(); 338 | let pal = r.palette(); 339 | 340 | for (i, c) in (200..255).enumerate() { 341 | assert_eq!(pal[i], RGBA::new(c, c, c, 255)); 342 | } 343 | 344 | for c in 0..128 { 345 | assert!(pal[55..].iter().any(|&p| p == RGBA::new(c, c, c, 255))); 346 | } 347 | } 348 | -------------------------------------------------------------------------------- /src/mediancut.rs: -------------------------------------------------------------------------------- 1 | use crate::hist::{HistItem, HistogramInternal}; 2 | use crate::pal::{f_pixel, PalF, PalLen, PalPop, ARGBF}; 3 | use crate::quant::quality_to_mse; 4 | use crate::PushInCapacity; 5 | use crate::{Error, OrdFloat}; 6 | use rgb::prelude::*; 7 | use std::cmp::Reverse; 8 | 9 | struct MedianCutter<'hist> { 10 | boxes: Vec>, 11 | hist_total_perceptual_weight: f64, 12 | target_colors: PalLen, 13 | } 14 | 15 | struct MBox<'hist> { 16 | /// Histogram entries that fell into this bucket 17 | pub colors: &'hist mut [HistItem], 18 | /// Center color selected to represent the colors 19 | pub avg_color: f_pixel, 20 | /// Difference from the average color, per channel, weighed using `adjusted_weight` 21 | pub variance: ARGBF, 22 | pub adjusted_weight_sum: f64, 23 | pub total_error: Option, 24 | /// max color difference between avg_color and any histogram entry 25 | pub max_error: f32, 26 | } 27 | 28 | impl<'hist> MBox<'hist> { 29 | pub fn new(hist: &'hist mut [HistItem]) -> Self { 30 | let weight_sum = hist.iter().map(|a| { 31 | debug_assert!(a.adjusted_weight.is_finite()); 32 | debug_assert!(a.adjusted_weight > 0.); 33 | f64::from(a.adjusted_weight) 34 | }).sum(); 35 | Self::new_c(hist, weight_sum, weighed_average_color(hist)) 36 | } 37 | 38 | fn new_s(hist: &'hist mut [HistItem], adjusted_weight_sum: f64, other_boxes: &[MBox<'_>]) -> Self { 39 | debug_assert!(!hist.is_empty()); 40 | let mut avg_color = weighed_average_color(hist); 41 | // It's possible that an average color will end up being bad for every entry, 42 | // so prefer picking actual colors so that at least one histogram entry will be satisfied. 43 | if (hist.len() < 500 && hist.len() > 2) || Self::is_useless_color(avg_color, hist, other_boxes) { 44 | avg_color = hist.iter().min_by_key(|a| OrdFloat::new(avg_color.diff(&a.color))).map(|a| a.color).unwrap_or_default(); 45 | } 46 | Self::new_c(hist, adjusted_weight_sum, avg_color) 47 | } 48 | 49 | fn new_c(hist: &'hist mut [HistItem], adjusted_weight_sum: f64, avg_color: f_pixel) -> Self { 50 | let (variance, max_error) = Self::box_stats(hist, avg_color); 51 | Self { 52 | variance, 53 | max_error, 54 | avg_color, 55 | colors: hist, 56 | adjusted_weight_sum, 57 | total_error: None, 58 | } 59 | } 60 | 61 | /// It's possible that the average color is useless 62 | fn is_useless_color(new_avg_color: f_pixel, colors: &[HistItem], other_boxes: &[MBox<'_>]) -> bool { 63 | colors.iter().all(move |c| { 64 | let own_box_diff = new_avg_color.diff(&c.color); 65 | let other_box_is_better = other_boxes.iter() 66 | .any(move |other| other.avg_color.diff(&c.color) < own_box_diff); 67 | 68 | other_box_is_better 69 | }) 70 | } 71 | 72 | fn box_stats(hist: &[HistItem], avg_color: f_pixel) -> (ARGBF, f32) { 73 | let mut variance = ARGBF::default(); 74 | let mut max_error = 0.; 75 | for a in hist { 76 | variance += (avg_color.0 - a.color.0).map(|c| c * c) * a.adjusted_weight; 77 | let diff = avg_color.diff(&a.color); 78 | if diff > max_error { 79 | max_error = diff; 80 | } 81 | } 82 | (variance, max_error) 83 | } 84 | 85 | pub fn compute_total_error(&mut self) -> f64 { 86 | let avg = self.avg_color; 87 | let e = self.colors.iter().map(move |a| f64::from(avg.diff(&a.color)) * f64::from(a.perceptual_weight)).sum::(); 88 | self.total_error = Some(e); 89 | e 90 | } 91 | 92 | pub fn prepare_sort(&mut self) { 93 | struct ChanVariance { 94 | pub chan: usize, 95 | pub variance: f32, 96 | } 97 | 98 | // Sort dimensions by their variance, and then sort colors first by dimension with the highest variance 99 | let vars: [f32; 4] = rgb::bytemuck::cast(self.variance); 100 | let mut channels = [ 101 | ChanVariance { chan: 0, variance: vars[0] }, 102 | ChanVariance { chan: 1, variance: vars[1] }, 103 | ChanVariance { chan: 2, variance: vars[2] }, 104 | ChanVariance { chan: 3, variance: vars[3] }, 105 | ]; 106 | channels.sort_by_key(|a| Reverse(OrdFloat::new(a.variance))); 107 | 108 | for a in self.colors.iter_mut() { 109 | let chans: [f32; 4] = rgb::bytemuck::cast(a.color.0); 110 | // Only the first channel really matters. But other channels are included, because when trying median cut 111 | // many times with different histogram weights, I don't want sort randomness to influence the outcome. 112 | a.tmp.mc_sort_value = (((chans[channels[0].chan] * 65535.) as u32) << 16) 113 | | ((chans[channels[2].chan] + chans[channels[1].chan] / 2. + chans[channels[3].chan] / 4.) * 65535.) as u32; // box will be split to make color_weight of each side even 114 | } 115 | } 116 | 117 | fn median_color(&mut self) -> f_pixel { 118 | let len = self.colors.len(); 119 | let (_, mid_item, _) = self.colors.select_nth_unstable_by_key(len/2, |a| a.mc_sort_value()); 120 | mid_item.color 121 | } 122 | 123 | pub fn prepare_color_weight_total(&mut self) -> f64 { 124 | let median = self.median_color(); 125 | self.colors.iter_mut().map(move |a| { 126 | let w = median.diff(&a.color).sqrt() * (1. + a.adjusted_weight).sqrt(); 127 | debug_assert!(w.is_finite()); 128 | a.mc_color_weight = w; 129 | f64::from(w) 130 | }) 131 | .sum() 132 | } 133 | 134 | #[inline] 135 | pub fn split(mut self, other_boxes: &[MBox<'_>]) -> [Self; 2] { 136 | self.prepare_sort(); 137 | let half_weight = self.prepare_color_weight_total() / 2.; 138 | // yeah, there's some off-by-one error in there 139 | let break_at = hist_item_sort_half(self.colors, half_weight).max(1); 140 | 141 | let (left, right) = self.colors.split_at_mut(break_at); 142 | let left_sum = left.iter().map(|a| f64::from(a.adjusted_weight)).sum(); 143 | let right_sum = self.adjusted_weight_sum - left_sum; 144 | 145 | [MBox::new_s(left, left_sum, other_boxes), 146 | MBox::new_s(right, right_sum, other_boxes)] 147 | } 148 | } 149 | 150 | #[inline] 151 | fn qsort_pivot(base: &[HistItem]) -> usize { 152 | let len = base.len(); 153 | if len < 32 { 154 | return len / 2; 155 | } 156 | let mut pivots = [8, len / 2, len - 1]; 157 | // LLVM can't see it's in bounds :( 158 | pivots.sort_unstable_by_key(move |&idx| unsafe { 159 | debug_assert!(base.get(idx).is_some()); 160 | base.get_unchecked(idx) 161 | }.mc_sort_value()); 162 | pivots[1] 163 | } 164 | 165 | fn qsort_partition(base: &mut [HistItem]) -> usize { 166 | let mut r = base.len(); 167 | base.swap(qsort_pivot(base), 0); 168 | let pivot_value = base[0].mc_sort_value(); 169 | let mut l = 1; 170 | while l < r { 171 | if base[l].mc_sort_value() >= pivot_value { 172 | l += 1; 173 | } else { 174 | r -= 1; 175 | while l < r && base[r].mc_sort_value() <= pivot_value { r -= 1; } 176 | base.swap(l, r); 177 | } 178 | } 179 | l -= 1; 180 | base.swap(l, 0); 181 | l 182 | } 183 | 184 | /// sorts the slice to make the sum of weights lower than `weight_half_sum` one side, 185 | /// returns index of the edge between halfvar parts of the set 186 | #[inline(never)] 187 | fn hist_item_sort_half(mut base: &mut [HistItem], mut weight_half_sum: f64) -> usize { 188 | let mut base_index = 0; 189 | if base.is_empty() { return 0; } 190 | loop { 191 | let partition = qsort_partition(base); 192 | let (left, right) = base.split_at_mut(partition + 1); // +1, because pivot stays on the left side 193 | let left_sum = left.iter().map(|c| f64::from(c.mc_color_weight)).sum::(); 194 | if left_sum >= weight_half_sum { 195 | match left.get_mut(..partition) { // trim pivot point, avoid panick branch in [] 196 | Some(left) if !left.is_empty() => { base = left; continue; }, 197 | _ => return base_index, 198 | } 199 | } 200 | weight_half_sum -= left_sum; 201 | base_index += left.len(); 202 | if !right.is_empty() { 203 | base = right; 204 | } else { 205 | return base_index; 206 | } 207 | } 208 | } 209 | 210 | impl<'hist> MedianCutter<'hist> { 211 | fn total_box_error_below_target(&mut self, mut target_mse: f64) -> bool { 212 | target_mse *= self.hist_total_perceptual_weight; 213 | let mut total_error = self.boxes.iter().filter_map(|mb| mb.total_error).sum::(); 214 | if total_error > target_mse { 215 | return false; 216 | } 217 | for mb in self.boxes.iter_mut().filter(|mb| mb.total_error.is_none()) { 218 | total_error += mb.compute_total_error(); 219 | if total_error > target_mse { 220 | return false; 221 | } 222 | } 223 | true 224 | } 225 | 226 | pub fn new(hist: &'hist mut HistogramInternal, target_colors: PalLen) -> Result { 227 | let hist_total_perceptual_weight = hist.total_perceptual_weight; 228 | 229 | debug_assert!(hist.clusters[0].begin == 0); 230 | debug_assert!(hist.clusters.last().unwrap().end as usize == hist.items.len()); 231 | 232 | let mut hist_items = &mut hist.items[..]; 233 | let mut boxes = Vec::new(); 234 | boxes.try_reserve(target_colors as usize)?; 235 | 236 | let used_boxes = hist.clusters.iter().filter(|b| b.begin != b.end).count(); 237 | if used_boxes <= target_colors as usize / 3 { 238 | // boxes are guaranteed to be sorted 239 | let mut prev_end = 0; 240 | for b in hist.clusters.iter().filter(|b| b.begin != b.end) { 241 | let begin = b.begin as usize; 242 | debug_assert_eq!(begin, prev_end); 243 | let end = b.end as usize; 244 | prev_end = end; 245 | let (this_box, rest) = hist_items.split_at_mut(end - begin); 246 | hist_items = rest; 247 | boxes.push_in_cap(MBox::new(this_box)); 248 | } 249 | } else { 250 | boxes.push_in_cap(MBox::new(hist_items)); 251 | }; 252 | 253 | Ok(Self { 254 | boxes, 255 | hist_total_perceptual_weight, 256 | target_colors, 257 | }) 258 | } 259 | 260 | fn into_palette(mut self) -> PalF { 261 | let mut palette = PalF::new(); 262 | 263 | for (i, b) in self.boxes.iter_mut().enumerate() { 264 | b.colors.iter_mut().for_each(move |a| a.tmp.likely_palette_index = i as _); 265 | 266 | // store total color popularity (perceptual_weight is approximation of it) 267 | let pop = b.colors.iter().map(|a| f64::from(a.perceptual_weight)).sum::(); 268 | palette.push(b.avg_color, PalPop::new(pop as f32)); 269 | } 270 | palette 271 | } 272 | 273 | fn cut(mut self, target_mse: f64, max_mse: f64) -> PalF { 274 | let max_mse = max_mse.max(quality_to_mse(20)); 275 | 276 | while self.boxes.len() < self.target_colors as usize { 277 | // first splits boxes that exceed quality limit (to have colors for things like odd green pixel), 278 | // later raises the limit to allow large smooth areas/gradients get colors. 279 | let fraction_done = self.boxes.len() as f64 / f64::from(self.target_colors); 280 | let current_max_mse = (fraction_done * 16.).mul_add(max_mse, max_mse); 281 | let Some(bi) = self.take_best_splittable_box(current_max_mse) else { 282 | break 283 | }; 284 | 285 | self.boxes.extend(bi.split(&self.boxes)); 286 | 287 | if self.total_box_error_below_target(target_mse) { 288 | break; 289 | } 290 | } 291 | 292 | self.into_palette() 293 | } 294 | 295 | fn take_best_splittable_box(&mut self, max_mse: f64) -> Option> { 296 | self.boxes.iter().enumerate() 297 | .filter(|(_, b)| b.colors.len() > 1) 298 | .map(move |(i, b)| { 299 | let cv = b.variance.r.max(b.variance.g).max(b.variance.b); 300 | let mut thissum = b.adjusted_weight_sum * f64::from(cv.max(b.variance.a)); 301 | if f64::from(b.max_error) > max_mse { 302 | thissum = thissum * f64::from(b.max_error) / max_mse; 303 | } 304 | (i, thissum) 305 | }) 306 | .max_by_key(|&(_, thissum)| OrdFloat::new64(thissum)) 307 | .map(|(i, _)| self.boxes.swap_remove(i)) 308 | } 309 | } 310 | 311 | #[inline(never)] 312 | pub(crate) fn mediancut(hist: &mut HistogramInternal, target_colors: PalLen, target_mse: f64, max_mse_per_color: f64) -> Result { 313 | Ok(MedianCutter::new(hist, target_colors)?.cut(target_mse, max_mse_per_color)) 314 | } 315 | 316 | fn weighed_average_color(hist: &[HistItem]) -> f_pixel { 317 | debug_assert!(!hist.is_empty()); 318 | let mut t = f_pixel::default(); 319 | let mut sum = 0.; 320 | for c in hist { 321 | sum += c.adjusted_weight; 322 | t.0 += c.color.0 * c.adjusted_weight; 323 | } 324 | if sum != 0. { 325 | t.0 /= sum; 326 | } 327 | t 328 | } 329 | -------------------------------------------------------------------------------- /src/nearest.rs: -------------------------------------------------------------------------------- 1 | use crate::pal::{f_pixel, PalF, PalIndex, MAX_COLORS}; 2 | use crate::{Error, OrdFloat}; 3 | 4 | impl<'pal> Nearest<'pal> { 5 | #[inline(never)] 6 | pub fn new(palette: &'pal PalF) -> Result { 7 | if palette.len() > PalIndex::MAX as usize + 1 { 8 | return Err(Error::Unsupported); 9 | } 10 | let mut indexes: Vec<_> = (0..palette.len()) 11 | .map(|idx| MapIndex { idx: idx as _ }) 12 | .collect(); 13 | if indexes.is_empty() { 14 | return Err(Error::Unsupported); 15 | } 16 | let mut handle = Nearest { 17 | root: vp_create_node(&mut indexes, palette), 18 | palette, 19 | nearest_other_color_dist: [0.; MAX_COLORS], 20 | }; 21 | for (i, color) in palette.as_slice().iter().enumerate() { 22 | let mut best = Visitor { 23 | idx: 0, distance: f32::MAX, distance_squared: f32::MAX, 24 | exclude: Some(i as PalIndex), 25 | }; 26 | vp_search_node(&handle.root, color, &mut best); 27 | handle.nearest_other_color_dist[i] = best.distance_squared / 4.; 28 | } 29 | Ok(handle) 30 | } 31 | } 32 | 33 | impl Nearest<'_> { 34 | #[inline] 35 | pub fn search(&self, px: &f_pixel, likely_colormap_index: PalIndex) -> (PalIndex, f32) { 36 | // The index may be invalid, so it needs to be checked 37 | let mut best_candidate = if let Some(pal_px) = self.palette.as_slice().get(likely_colormap_index as usize) { 38 | let guess_diff = px.diff(pal_px); 39 | if guess_diff < self.nearest_other_color_dist[likely_colormap_index as usize] { 40 | return (likely_colormap_index, guess_diff); 41 | } 42 | Visitor { 43 | distance: guess_diff.sqrt(), 44 | distance_squared: guess_diff, 45 | idx: likely_colormap_index, 46 | exclude: None, 47 | } 48 | } else { 49 | Visitor { distance: f32::INFINITY, distance_squared: f32::INFINITY, idx: 0, exclude: None, } 50 | }; 51 | 52 | vp_search_node(&self.root, px, &mut best_candidate); 53 | (best_candidate.idx, best_candidate.distance_squared) 54 | } 55 | } 56 | 57 | pub(crate) struct Nearest<'pal> { 58 | root: Node, 59 | palette: &'pal PalF, 60 | nearest_other_color_dist: [f32; MAX_COLORS], 61 | } 62 | 63 | pub struct MapIndex { 64 | pub idx: PalIndex, 65 | } 66 | 67 | pub struct Visitor { 68 | pub distance: f32, 69 | pub distance_squared: f32, 70 | pub idx: PalIndex, 71 | pub exclude: Option, 72 | } 73 | 74 | impl Visitor { 75 | #[inline] 76 | fn visit(&mut self, distance: f32, distance_squared: f32, idx: PalIndex) { 77 | if distance_squared < self.distance_squared && self.exclude != Some(idx) { 78 | self.distance = distance; 79 | self.distance_squared = distance_squared; 80 | self.idx = idx; 81 | } 82 | } 83 | } 84 | 85 | pub(crate) struct Node { 86 | vantage_point: f_pixel, 87 | inner: NodeInner, 88 | idx: PalIndex, 89 | } 90 | 91 | const LEAF_MAX_SIZE: usize = 6; 92 | 93 | enum NodeInner { 94 | Nodes { 95 | radius: f32, 96 | radius_squared: f32, 97 | near: Box, 98 | far: Box, 99 | }, 100 | Leaf { 101 | len: u8, 102 | idxs: [PalIndex; LEAF_MAX_SIZE], 103 | colors: Box<[f_pixel; LEAF_MAX_SIZE]>, 104 | }, 105 | } 106 | 107 | #[inline(never)] 108 | fn vp_create_node(indexes: &mut [MapIndex], items: &PalF) -> Node { 109 | debug_assert!(!indexes.is_empty()); 110 | let palette = items.as_slice(); 111 | 112 | if indexes.len() <= 1 { 113 | let idx = indexes.first().map(|i| i.idx).unwrap_or_default(); 114 | return Node { 115 | vantage_point: palette.get(usize::from(idx)).copied().unwrap_or_default(), 116 | idx, 117 | inner: NodeInner::Leaf { len: 0, idxs: [0; LEAF_MAX_SIZE], colors: Box::new([f_pixel::default(); LEAF_MAX_SIZE]) }, 118 | }; 119 | } 120 | 121 | let most_popular_item = indexes.iter().enumerate().max_by_key(move |(_, idx)| { 122 | OrdFloat::new(items.pop_as_slice().get(usize::from(idx.idx)) 123 | .map(|p| p.popularity()).unwrap_or_default()) 124 | }).map(|(n, _)| n).unwrap_or_default(); 125 | indexes.swap(most_popular_item, 0); 126 | let (ref_, indexes) = indexes.split_first_mut().unwrap(); 127 | 128 | let vantage_point = palette.get(usize::from(ref_.idx)).copied().unwrap_or_default(); 129 | indexes.sort_by_cached_key(move |i| { 130 | OrdFloat::new(palette.get(usize::from(i.idx)) 131 | .map(|px| vantage_point.diff(px)).unwrap_or_default()) 132 | }); 133 | 134 | let num_indexes = indexes.len(); 135 | 136 | let inner = if num_indexes <= LEAF_MAX_SIZE { 137 | let mut colors = [f_pixel::default(); LEAF_MAX_SIZE]; 138 | let mut idxs = [Default::default(); LEAF_MAX_SIZE]; 139 | 140 | indexes.iter().zip(colors.iter_mut().zip(idxs.iter_mut())).for_each(|(i, (color, idx))| { 141 | if let Some(c) = palette.get(usize::from(i.idx)) { 142 | *idx = i.idx; 143 | *color = *c; 144 | } 145 | }); 146 | NodeInner::Leaf { 147 | len: num_indexes as _, 148 | idxs, 149 | colors: Box::new(colors), 150 | } 151 | } else { 152 | let half_index = num_indexes / 2; 153 | let (near, far) = indexes.split_at_mut(half_index); 154 | debug_assert!(!near.is_empty()); 155 | debug_assert!(!far.is_empty()); 156 | let radius_squared = palette.get(usize::from(far[0].idx)) 157 | .map(|px| vantage_point.diff(px)).unwrap_or_default(); 158 | let radius = radius_squared.sqrt(); 159 | NodeInner::Nodes { 160 | radius, radius_squared, 161 | near: Box::new(vp_create_node(near, items)), 162 | far: Box::new(vp_create_node(far, items)), 163 | } 164 | }; 165 | 166 | Node { 167 | inner, 168 | vantage_point, 169 | idx: ref_.idx, 170 | } 171 | } 172 | 173 | #[inline(never)] 174 | fn vp_search_node(mut node: &Node, needle: &f_pixel, best_candidate: &mut Visitor) { 175 | loop { 176 | let distance_squared = node.vantage_point.diff(needle); 177 | let distance = distance_squared.sqrt(); 178 | 179 | best_candidate.visit(distance, distance_squared, node.idx); 180 | 181 | match node.inner { 182 | NodeInner::Nodes { radius, radius_squared, ref near, ref far } => { 183 | // Recurse towards most likely candidate first to narrow best candidate's distance as soon as possible 184 | if distance_squared < radius_squared { 185 | vp_search_node(near, needle, best_candidate); 186 | // The best node (final answer) may be just ouside the radius, but not farther than 187 | // the best distance we know so far. The vp_search_node above should have narrowed 188 | // best_candidate->distance, so this path is rarely taken. 189 | if distance >= radius - best_candidate.distance { 190 | node = far; 191 | continue; 192 | } 193 | } else { 194 | vp_search_node(far, needle, best_candidate); 195 | if distance <= radius + best_candidate.distance { 196 | node = near; 197 | continue; 198 | } 199 | } 200 | break; 201 | }, 202 | NodeInner::Leaf { len: num, ref idxs, ref colors } => { 203 | colors.iter().zip(idxs.iter().copied()).take(num as usize).for_each(|(color, idx)| { 204 | let distance_squared = color.diff(needle); 205 | best_candidate.visit(distance_squared.sqrt(), distance_squared, idx); 206 | }); 207 | break; 208 | }, 209 | } 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /src/pal.rs: -------------------------------------------------------------------------------- 1 | use crate::OrdFloat; 2 | use arrayvec::ArrayVec; 3 | use rgb::prelude::*; 4 | use std::ops::{Deref, DerefMut}; 5 | 6 | /// 8-bit RGBA in sRGB. This is the only color format *publicly* used by the library. 7 | pub type RGBA = rgb::Rgba; 8 | 9 | #[allow(clippy::upper_case_acronyms)] 10 | pub type ARGBF = rgb::Argb; 11 | 12 | pub const INTERNAL_GAMMA: f64 = 0.57; 13 | pub const LIQ_WEIGHT_A: f32 = 0.625; 14 | pub const LIQ_WEIGHT_R: f32 = 0.5; 15 | pub const LIQ_WEIGHT_G: f32 = 1.; 16 | pub const LIQ_WEIGHT_B: f32 = 0.45; 17 | 18 | /// This is a fudge factor - reminder that colors are not in 0..1 range any more 19 | pub const LIQ_WEIGHT_MSE: f64 = 0.45; 20 | 21 | pub const MIN_OPAQUE_A: f32 = 1. / 256. * LIQ_WEIGHT_A; 22 | pub const MAX_TRANSP_A: f32 = 255. / 256. * LIQ_WEIGHT_A; 23 | 24 | /// 4xf32 color using internal gamma. 25 | /// 26 | /// ARGB layout is important for x86 SIMD. 27 | /// I've created the newtype wrapper to try a 16-byte alignment, but it didn't improve perf :( 28 | #[cfg_attr( 29 | any(target_arch = "x86_64", all(target_feature = "neon", target_arch = "aarch64")), 30 | repr(C, align(16)) 31 | )] 32 | #[derive(Debug, Copy, Clone, Default, PartialEq)] 33 | #[allow(non_camel_case_types)] 34 | pub struct f_pixel(pub ARGBF); 35 | 36 | impl f_pixel { 37 | #[cfg(not(any(target_arch = "x86_64", all(target_feature = "neon", target_arch = "aarch64"))))] 38 | #[inline(always)] 39 | pub fn diff(&self, other: &f_pixel) -> f32 { 40 | let alphas = other.0.a - self.0.a; 41 | let black = self.0 - other.0; 42 | let white = ARGBF { 43 | a: 0., 44 | r: black.r + alphas, 45 | g: black.g + alphas, 46 | b: black.b + alphas, 47 | }; 48 | (black.r * black.r).max(white.r * white.r) + 49 | (black.g * black.g).max(white.g * white.g) + 50 | (black.b * black.b).max(white.b * white.b) 51 | } 52 | 53 | #[cfg(all(target_feature = "neon", target_arch = "aarch64"))] 54 | #[inline(always)] 55 | pub fn diff(&self, other: &Self) -> f32 { 56 | unsafe { 57 | use std::arch::aarch64::*; 58 | 59 | let px = vld1q_f32((self as *const Self).cast::()); 60 | let py = vld1q_f32((other as *const Self).cast::()); 61 | 62 | // y.a - x.a 63 | let mut alphas = vsubq_f32(py, px); 64 | alphas = vdupq_laneq_f32(alphas, 0); // copy first to all four 65 | 66 | let mut onblack = vsubq_f32(px, py); // x - y 67 | let mut onwhite = vaddq_f32(onblack, alphas); // x - y + (y.a - x.a) 68 | 69 | onblack = vmulq_f32(onblack, onblack); 70 | onwhite = vmulq_f32(onwhite, onwhite); 71 | 72 | let max = vmaxq_f32(onwhite, onblack); 73 | 74 | let mut max_r = [0.; 4]; 75 | vst1q_f32(max_r.as_mut_ptr(), max); 76 | 77 | let mut max_gb = [0.; 4]; 78 | vst1q_f32(max_gb.as_mut_ptr(), vpaddq_f32(max, max)); 79 | 80 | // add rgb, not a 81 | 82 | max_r[1] + max_gb[1] 83 | } 84 | } 85 | 86 | #[cfg(target_arch = "x86_64")] 87 | #[inline(always)] 88 | pub fn diff(&self, other: &f_pixel) -> f32 { 89 | unsafe { 90 | use std::arch::x86_64::*; 91 | 92 | let px = _mm_loadu_ps(self as *const f_pixel as *const f32); 93 | let py = _mm_loadu_ps(other as *const f_pixel as *const f32); 94 | 95 | // y.a - x.a 96 | let mut alphas = _mm_sub_ss(py, px); 97 | alphas = _mm_shuffle_ps(alphas, alphas, 0); // copy first to all four 98 | 99 | let mut onblack = _mm_sub_ps(px, py); // x - y 100 | let mut onwhite = _mm_add_ps(onblack, alphas); // x - y + (y.a - x.a) 101 | 102 | onblack = _mm_mul_ps(onblack, onblack); 103 | onwhite = _mm_mul_ps(onwhite, onwhite); 104 | let max = _mm_max_ps(onwhite, onblack); 105 | 106 | // the compiler is better at horizontal add than I am 107 | let mut tmp = [0.; 4]; 108 | _mm_storeu_ps(tmp.as_mut_ptr(), max); 109 | 110 | // add rgb, not a 111 | let res = tmp[1] + tmp[2] + tmp[3]; 112 | res 113 | } 114 | } 115 | 116 | #[inline] 117 | pub(crate) fn to_rgb(self, gamma: f64) -> RGBA { 118 | if self.a < MIN_OPAQUE_A { 119 | return RGBA::new(0, 0, 0, 0); 120 | } 121 | 122 | let r = (LIQ_WEIGHT_A / LIQ_WEIGHT_R) * self.r / self.a; 123 | let g = (LIQ_WEIGHT_A / LIQ_WEIGHT_G) * self.g / self.a; 124 | let b = (LIQ_WEIGHT_A / LIQ_WEIGHT_B) * self.b / self.a; 125 | let a = (256. / LIQ_WEIGHT_A) * self.a; 126 | 127 | let gamma = (gamma / INTERNAL_GAMMA) as f32; 128 | debug_assert!(gamma.is_finite()); 129 | 130 | // 256, because numbers are in range 1..255.9999… rounded down 131 | RGBA { 132 | r: (r.powf(gamma) * 256.) as u8, 133 | g: (g.powf(gamma) * 256.) as u8, 134 | b: (b.powf(gamma) * 256.) as u8, 135 | a: a as u8, 136 | } 137 | } 138 | 139 | pub fn from_rgba(gamma_lut: &[f32; 256], px: RGBA) -> Self { 140 | let a = f32::from(px.a) / 255.; 141 | Self(ARGBF { 142 | a: a * LIQ_WEIGHT_A, 143 | r: gamma_lut[px.r as usize] * LIQ_WEIGHT_R * a, 144 | g: gamma_lut[px.g as usize] * LIQ_WEIGHT_G * a, 145 | b: gamma_lut[px.b as usize] * LIQ_WEIGHT_B * a, 146 | }) 147 | } 148 | } 149 | 150 | impl Deref for f_pixel { 151 | type Target = ARGBF; 152 | 153 | #[inline(always)] 154 | fn deref(&self) -> &Self::Target { 155 | &self.0 156 | } 157 | } 158 | 159 | impl DerefMut for f_pixel { 160 | #[inline(always)] 161 | fn deref_mut(&mut self) -> &mut Self::Target { 162 | &mut self.0 163 | } 164 | } 165 | 166 | impl From for f_pixel { 167 | #[inline(always)] 168 | fn from(x: ARGBF) -> Self { 169 | Self(x) 170 | } 171 | } 172 | 173 | /// To keep the data dense, `is_fixed` is stuffed into the sign bit 174 | #[derive(Copy, Clone, Debug)] 175 | pub(crate) struct PalPop(f32); 176 | 177 | impl PalPop { 178 | #[inline(always)] 179 | pub fn is_fixed(self) -> bool { 180 | self.0 < 0. 181 | } 182 | 183 | #[must_use] 184 | pub fn to_fixed(self) -> Self { 185 | if self.0 < 0. { 186 | return self; 187 | } 188 | Self(if self.0 > 0. { -self.0 } else { -1. }) 189 | } 190 | 191 | #[inline] 192 | #[cfg_attr(debug_assertions, track_caller)] 193 | pub fn new(popularity: f32) -> Self { 194 | debug_assert!(popularity >= 0.); 195 | Self(popularity) 196 | } 197 | 198 | #[inline(always)] 199 | #[must_use] 200 | pub fn popularity(self) -> f32 { 201 | self.0.abs() 202 | } 203 | } 204 | 205 | #[cfg(feature = "large_palettes")] 206 | pub type PalIndex = u16; 207 | 208 | #[cfg(not(feature = "large_palettes"))] 209 | pub type PalIndex = u8; 210 | 211 | /// This could be increased to support > 256 colors in remapping too 212 | pub type PalIndexRemap = u8; 213 | pub type PalLen = u16; 214 | 215 | /// Palettes are stored on the stack, and really large ones will cause stack overflows 216 | pub(crate) const MAX_COLORS: usize = if PalIndex::MAX == 255 { 256 } else { 2048 }; 217 | 218 | /// A palette of premultiplied ARGB 4xf32 colors in internal gamma 219 | #[derive(Clone)] 220 | pub(crate) struct PalF { 221 | colors: ArrayVec, 222 | pops: ArrayVec, 223 | } 224 | 225 | impl PalF { 226 | #[inline] 227 | pub fn new() -> Self { 228 | debug_assert!(PalIndex::MAX as usize + 1 >= MAX_COLORS); 229 | debug_assert!(PalLen::MAX as usize >= MAX_COLORS); 230 | Self { 231 | colors: ArrayVec::default(), 232 | pops: ArrayVec::default(), 233 | } 234 | } 235 | 236 | #[inline(always)] 237 | pub fn push(&mut self, color: f_pixel, popularity: PalPop) { 238 | self.pops.push(popularity); 239 | self.colors.push(color); 240 | } 241 | 242 | pub fn set(&mut self, idx: usize, color: f_pixel, popularity: PalPop) { 243 | self.colors[idx] = color; 244 | self.pops[idx] = popularity; 245 | } 246 | 247 | #[inline(always)] 248 | pub fn as_slice(&self) -> &[f_pixel] { 249 | &self.colors 250 | } 251 | 252 | #[inline(always)] 253 | pub fn pop_as_slice(&self) -> &[PalPop] { 254 | &self.pops 255 | } 256 | 257 | // this is max colors allowed by the user, not just max in the current (candidate/low-quality) palette 258 | pub(crate) fn with_fixed_colors(mut self, max_colors: PalLen, fixed_colors: &[f_pixel]) -> Self { 259 | if fixed_colors.is_empty() { 260 | return self; 261 | } 262 | 263 | // if using low quality, there's a chance mediancut won't create enough colors in the palette 264 | let max_fixed_colors = fixed_colors.len().min(max_colors as usize); 265 | if self.len() < max_fixed_colors { 266 | let needs_extra = max_fixed_colors - self.len(); 267 | self.colors.extend(fixed_colors.iter().copied().take(needs_extra)); 268 | self.pops.extend(std::iter::repeat(PalPop::new(0.)).take(needs_extra)); 269 | debug_assert_eq!(self.len(), max_fixed_colors); 270 | } 271 | 272 | // since the fixed colors were in the histogram, expect them to be in the palette, 273 | // and change closest existing one to be exact fixed 274 | for (i, fixed_color) in fixed_colors.iter().enumerate().take(self.len()) { 275 | let (best_idx, _) = self.colors.iter().enumerate().skip(i).min_by_key(|(_, pal_color)| { 276 | // not using Nearest, because creation of the index may take longer than naive search once 277 | OrdFloat::new(pal_color.diff(fixed_color)) 278 | }).expect("logic bug in fixed colors, please report a bug"); 279 | debug_assert!(best_idx >= i); 280 | self.swap(i, best_idx); 281 | self.set(i, *fixed_color, self.pops[i].to_fixed()); 282 | } 283 | 284 | debug_assert!(self.colors.iter().zip(fixed_colors).all(|(p, f)| p == f)); 285 | debug_assert!(self.pops.iter().take(fixed_colors.len()).all(|pop| pop.is_fixed())); 286 | self 287 | } 288 | 289 | #[inline(always)] 290 | pub(crate) fn len(&self) -> usize { 291 | debug_assert_eq!(self.colors.len(), self.pops.len()); 292 | self.colors.len() 293 | } 294 | 295 | #[inline(always)] 296 | pub fn iter_mut(&mut self) -> impl Iterator { 297 | let c = &mut self.colors[..]; 298 | let pop = &mut self.pops[..c.len()]; 299 | c.iter_mut().zip(pop) 300 | } 301 | 302 | #[cfg_attr(debug_assertions, track_caller)] 303 | pub(crate) fn swap(&mut self, a: usize, b: usize) { 304 | self.colors.swap(a, b); 305 | self.pops.swap(a, b); 306 | } 307 | 308 | /// Also rounds the input pal 309 | pub(crate) fn init_int_palette(&mut self, int_palette: &mut Palette, gamma: f64, posterize: u8) { 310 | let lut = gamma_lut(gamma); 311 | for ((f_color, f_pop), int_pal) in self.iter_mut().zip(&mut int_palette.entries) { 312 | let mut px = f_color.to_rgb(gamma) 313 | .map(move |c| posterize_channel(c, posterize)); 314 | *f_color = f_pixel::from_rgba(&lut, px); 315 | if px.a == 0 && !f_pop.is_fixed() { 316 | px.r = 71u8; 317 | px.g = 112u8; 318 | px.b = 76u8; 319 | } 320 | *int_pal = px; 321 | } 322 | int_palette.count = self.len() as _; 323 | } 324 | } 325 | 326 | #[inline] 327 | const fn posterize_channel(color: u8, bits: u8) -> u8 { 328 | if bits == 0 { 329 | color 330 | } else { 331 | (color & !((1 << bits) - 1)) | (color >> (8 - bits)) 332 | } 333 | } 334 | 335 | #[inline(always)] 336 | pub fn gamma_lut(gamma: f64) -> [f32; 256] { 337 | debug_assert!(gamma > 0.); 338 | let mut tmp = [0.; 256]; 339 | for (i, t) in tmp.iter_mut().enumerate() { 340 | *t = ((i as f32) / 255.).powf((INTERNAL_GAMMA / gamma) as f32); 341 | } 342 | tmp 343 | } 344 | 345 | /// Not used in the Rust API. 346 | /// RGBA colors obtained from [`QuantizationResult`](crate::QuantizationResult) 347 | #[repr(C)] 348 | #[derive(Clone)] 349 | pub struct Palette { 350 | /// Number of used colors in the `entries` 351 | pub count: std::os::raw::c_uint, 352 | /// The colors, up to `count` 353 | pub entries: [RGBA; MAX_COLORS], 354 | } 355 | 356 | impl std::ops::Deref for Palette { 357 | type Target = [RGBA]; 358 | 359 | #[inline(always)] 360 | fn deref(&self) -> &Self::Target { 361 | self.as_slice() 362 | } 363 | } 364 | 365 | impl std::ops::DerefMut for Palette { 366 | #[inline(always)] 367 | fn deref_mut(&mut self) -> &mut Self::Target { 368 | self.as_mut_slice() 369 | } 370 | } 371 | 372 | impl Palette { 373 | /// Palette colors 374 | #[inline(always)] 375 | #[must_use] 376 | pub fn as_slice(&self) -> &[RGBA] { 377 | &self.entries[..self.count as usize] 378 | } 379 | 380 | #[inline(always)] 381 | pub(crate) fn as_mut_slice(&mut self) -> &mut [RGBA] { 382 | &mut self.entries[..self.count as usize] 383 | } 384 | } 385 | 386 | #[test] 387 | fn diff_test() { 388 | let a = f_pixel(ARGBF {a: 1., r: 0.2, g: 0.3, b: 0.5}); 389 | let b = f_pixel(ARGBF {a: 1., r: 0.3, g: 0.3, b: 0.5}); 390 | let c = f_pixel(ARGBF {a: 1., r: 1., g: 0.3, b: 0.5}); 391 | let d = f_pixel(ARGBF {a: 0., r: 1., g: 0.3, b: 0.5}); 392 | assert!(a.diff(&b) < b.diff(&c)); 393 | assert!(c.diff(&b) < c.diff(&d)); 394 | 395 | let a = f_pixel(ARGBF {a: 1., b: 0.2, r: 0.3, g: 0.5}); 396 | let b = f_pixel(ARGBF {a: 1., b: 0.3, r: 0.3, g: 0.5}); 397 | let c = f_pixel(ARGBF {a: 1., b: 1., r: 0.3, g: 0.5}); 398 | let d = f_pixel(ARGBF {a: 0., b: 1., r: 0.3, g: 0.5}); 399 | assert!(a.diff(&b) < b.diff(&c)); 400 | assert!(c.diff(&b) < c.diff(&d)); 401 | 402 | let a = f_pixel(ARGBF {a: 1., g: 0.2, b: 0.3, r: 0.5}); 403 | let b = f_pixel(ARGBF {a: 1., g: 0.3, b: 0.3, r: 0.5}); 404 | let c = f_pixel(ARGBF {a: 1., g: 1., b: 0.3, r: 0.5}); 405 | let d = f_pixel(ARGBF {a: 0., g: 1., b: 0.3, r: 0.5}); 406 | assert!(a.diff(&b) < b.diff(&c)); 407 | assert!(c.diff(&b) < c.diff(&d)); 408 | } 409 | 410 | #[test] 411 | fn pal_test() { 412 | let mut p = PalF::new(); 413 | let gamma = gamma_lut(0.45455); 414 | for i in 0..=255u8 { 415 | let rgba = RGBA::new(i, i, i, 100 + i / 2); 416 | p.push(f_pixel::from_rgba(&gamma, rgba), PalPop::new(1.)); 417 | assert_eq!(i as usize + 1, p.len()); 418 | assert_eq!(i as usize + 1, p.pop_as_slice().len()); 419 | assert_eq!(i as usize + 1, p.as_slice().len()); 420 | assert_eq!(i as usize + 1, p.colors.len()); 421 | assert_eq!(i as usize + 1, p.pops.len()); 422 | assert_eq!(i as usize + 1, p.iter_mut().count()); 423 | } 424 | 425 | let mut int_pal = Palette { 426 | count: 0, 427 | entries: [RGBA::default(); MAX_COLORS], 428 | }; 429 | p.init_int_palette(&mut int_pal, 0.45455, 0); 430 | 431 | for i in 0..=255u8 { 432 | let rgba = p.as_slice()[i as usize].to_rgb(0.45455); 433 | assert_eq!(rgba, RGBA::new(i, i, i, 100 + i / 2)); 434 | assert_eq!(int_pal[i as usize], RGBA::new(i, i, i, 100 + i / 2)); 435 | } 436 | } 437 | 438 | #[test] 439 | #[cfg(feature = "large_palettes")] 440 | fn largepal() { 441 | let gamma = gamma_lut(0.5); 442 | let mut p = PalF::new(); 443 | for i in 0..1000 { 444 | let rgba = RGBA::new(i as u8, (i/2) as u8, (i/4) as u8, 255); 445 | p.push(f_pixel::from_rgba(&gamma, rgba), PalPop::new(1.)); 446 | } 447 | } 448 | -------------------------------------------------------------------------------- /src/rayoff.rs: -------------------------------------------------------------------------------- 1 | use once_cell::unsync::OnceCell; 2 | use std::slice::ChunksMut; 3 | 4 | pub(crate) struct ThreadLocal(OnceCell); 5 | 6 | impl ThreadLocal { 7 | #[inline(always)] 8 | pub fn new() -> Self { 9 | Self(OnceCell::new()) 10 | } 11 | 12 | #[inline(always)] 13 | pub fn get_or(&self, f: impl FnOnce() -> T) -> &T { 14 | self.0.get_or_init(f) 15 | } 16 | 17 | #[inline(always)] 18 | pub fn get_or_try(&self, f: impl FnOnce() -> Result) -> Result<&T, E> { 19 | self.0.get_or_try_init(f) 20 | } 21 | } 22 | 23 | impl IntoIterator for ThreadLocal { 24 | type IntoIter = std::option::IntoIter; 25 | type Item = T; 26 | 27 | #[inline(always)] 28 | fn into_iter(mut self) -> Self::IntoIter { 29 | self.0.take().into_iter() 30 | } 31 | } 32 | 33 | pub(crate) trait FakeRayonIter: Sized { 34 | fn par_bridge(self) -> Self; 35 | } 36 | 37 | 38 | impl FakeRayonIter for T where Self: Sized { 39 | fn par_bridge(self) -> Self { self } 40 | } 41 | 42 | pub(crate) trait FakeRayonIntoIter { 43 | fn par_chunks_mut(&mut self, chunk_size: usize) -> ChunksMut; 44 | } 45 | 46 | impl<'a, T> FakeRayonIntoIter for &'a mut [T] { 47 | #[inline(always)] 48 | fn par_chunks_mut(&mut self, chunk_size: usize) -> ChunksMut { 49 | self.chunks_mut(chunk_size) 50 | } 51 | } 52 | 53 | impl<'a, T> FakeRayonIntoIter for Box<[T]> { 54 | #[inline(always)] 55 | fn par_chunks_mut(&mut self, chunk_size: usize) -> ChunksMut { 56 | self.chunks_mut(chunk_size) 57 | } 58 | } 59 | 60 | pub(crate) struct SpawnMock; 61 | 62 | impl SpawnMock { 63 | #[inline(always)] 64 | pub fn spawn(&self, f: F) -> R where F: FnOnce(SpawnMock) -> R { 65 | f(SpawnMock) 66 | } 67 | } 68 | 69 | #[inline(always)] 70 | pub(crate) fn scope(f: F) -> R where F: FnOnce(SpawnMock) -> R { 71 | f(SpawnMock) 72 | } 73 | 74 | #[inline(always)] 75 | pub(crate) fn num_cpus() -> usize { 76 | 1 77 | } 78 | -------------------------------------------------------------------------------- /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 std::mem::MaybeUninit; 6 | 7 | pub(crate) type RowCallback<'a> = dyn Fn(&mut [MaybeUninit], usize) + Send + Sync + 'a; 8 | 9 | pub(crate) enum PixelsSource<'pixels, 'rows> { 10 | /// The `pixels` field is never read, but it is used to store the rows. 11 | #[allow(dead_code)] 12 | Pixels { 13 | rows: SeaCow<'rows, Pointer>, 14 | pixels: Option>, 15 | }, 16 | Callback(Box>), 17 | } 18 | 19 | impl<'pixels> PixelsSource<'pixels, '_> { 20 | pub(crate) fn for_pixels(pixels: SeaCow<'pixels, RGBA>, width: u32, height: u32, stride: u32) -> Result { 21 | if stride < width || height == 0 || width == 0 { 22 | return Err(Error::ValueOutOfRange); 23 | } 24 | let stride = stride as usize; 25 | let width = width as usize; 26 | let height = height as usize; 27 | 28 | let slice = pixels.as_slice(); 29 | let min_area = stride.checked_mul(height).and_then(|a| a.checked_add(width)).ok_or(Error::ValueOutOfRange)? - stride; 30 | if slice.len() < min_area { 31 | return Err(Error::BufferTooSmall); 32 | } 33 | 34 | let rows = SeaCow::boxed(slice.chunks(stride).map(|row| Pointer(row.as_ptr())).take(height).collect()); 35 | Ok(Self::Pixels { rows, pixels: Some(pixels) }) 36 | } 37 | } 38 | 39 | pub(crate) struct DynamicRows<'pixels, 'rows> { 40 | pub(crate) width: u32, 41 | pub(crate) height: u32, 42 | f_pixels: Option>, 43 | pixels: PixelsSource<'pixels, 'rows>, 44 | pub(crate) gamma: f64, 45 | } 46 | 47 | impl Clone for DynamicRows<'_, '_> { 48 | fn clone(&self) -> Self { 49 | Self { 50 | width: self.width, 51 | height: self.height, 52 | f_pixels: self.f_pixels.clone(), 53 | pixels: match &self.pixels { 54 | PixelsSource::Pixels { rows, pixels } => PixelsSource::Pixels { 55 | rows: rows.clone(), 56 | pixels: pixels.clone(), 57 | }, 58 | PixelsSource::Callback(_) => { 59 | let area = self.width as usize * self.height as usize; 60 | let mut out = Vec::with_capacity(area); 61 | let out_rows = out.spare_capacity_mut()[..area].chunks_exact_mut(self.width as usize); 62 | for (i, row) in out_rows.enumerate() { 63 | self.row_rgba(row, i); 64 | } 65 | unsafe { 66 | out.set_len(area); 67 | } 68 | let pixels = SeaCow::boxed(out.into_boxed_slice()); 69 | PixelsSource::for_pixels(pixels, self.width, self.height, self.width).unwrap() 70 | }, 71 | }, 72 | gamma: self.gamma, 73 | } 74 | } 75 | } 76 | 77 | pub(crate) struct DynamicRowsIter<'parent, 'pixels, 'rows> { 78 | px: &'parent DynamicRows<'pixels, 'rows>, 79 | temp_f_row: Option]>>, 80 | } 81 | 82 | impl DynamicRowsIter<'_, '_, '_> { 83 | #[must_use] 84 | pub fn row_f<'px>(&'px mut self, temp_row: &mut [MaybeUninit], row: usize) -> &'px [f_pixel] { 85 | debug_assert_eq!(temp_row.len(), self.px.width as usize); 86 | if let Some(pixels) = self.px.f_pixels.as_ref() { 87 | let start = self.px.width as usize * row; 88 | &pixels[start..start + self.px.width as usize] 89 | } else { 90 | let lut = gamma_lut(self.px.gamma); 91 | let row_pixels = self.px.row_rgba(temp_row, row); 92 | 93 | match self.temp_f_row.as_mut() { 94 | Some(t) => DynamicRows::convert_row_to_f(t, row_pixels, &lut), 95 | None => &mut [], // this can't happen 96 | } 97 | } 98 | } 99 | 100 | #[must_use] 101 | pub fn row_f_shared<'px>(&'px self, temp_row: &mut [MaybeUninit], temp_row_f: &'px mut [MaybeUninit], row: usize) -> &'px [f_pixel] { 102 | if let Some(pixels) = self.px.f_pixels.as_ref() { 103 | &pixels[self.px.width as usize * row..] 104 | } else { 105 | let lut = gamma_lut(self.px.gamma); 106 | let row_pixels = self.px.row_rgba(temp_row, row); 107 | 108 | DynamicRows::convert_row_to_f(temp_row_f, row_pixels, &lut) 109 | } 110 | } 111 | 112 | #[must_use] 113 | pub fn row_rgba<'px>(&'px self, temp_row: &'px mut [MaybeUninit], row: usize) -> &'px [RGBA] { 114 | self.px.row_rgba(temp_row, row) 115 | } 116 | } 117 | 118 | impl<'pixels, 'rows> DynamicRows<'pixels, 'rows> { 119 | #[inline] 120 | pub(crate) fn new(width: u32, height: u32, pixels: PixelsSource<'pixels, 'rows>, gamma: f64) -> Self { 121 | debug_assert!(gamma > 0.); 122 | Self { width, height, f_pixels: None, pixels, gamma } 123 | } 124 | 125 | fn row_rgba<'px>(&'px self, temp_row: &'px mut [MaybeUninit], row: usize) -> &'px [RGBA] { 126 | match &self.pixels { 127 | PixelsSource::Pixels { rows, .. } => unsafe { 128 | std::slice::from_raw_parts(rows.as_slice()[row].0, self.width()) 129 | }, 130 | PixelsSource::Callback(cb) => { 131 | cb(temp_row, row); 132 | // cb needs to be marked as unsafe, since it's responsible for initialization :( 133 | unsafe { slice_assume_init_mut(temp_row) } 134 | }, 135 | } 136 | } 137 | 138 | fn convert_row_to_f<'f>(row_f_pixels: &'f mut [MaybeUninit], row_pixels: &[RGBA], gamma_lut: &[f32; 256]) -> &'f mut [f_pixel] { 139 | assert_eq!(row_f_pixels.len(), row_pixels.len()); 140 | for (dst, src) in row_f_pixels.iter_mut().zip(row_pixels) { 141 | dst.write(f_pixel::from_rgba(gamma_lut, *src)); 142 | } 143 | // Safe, just initialized 144 | unsafe { slice_assume_init_mut(row_f_pixels) } 145 | } 146 | 147 | #[must_use] 148 | fn should_use_low_memory(&self) -> bool { 149 | self.width() * self.height() > LIQ_HIGH_MEMORY_LIMIT / std::mem::size_of::() 150 | } 151 | 152 | #[inline] 153 | fn temp_f_row_for_iter(&self) -> Result]>>, Error> { 154 | if self.f_pixels.is_some() { 155 | return Ok(None); 156 | } 157 | Ok(Some(temp_buf(self.width())?)) 158 | } 159 | 160 | pub fn prepare_iter(&mut self, temp_row: &mut [MaybeUninit], allow_steamed: bool) -> Result<(), Error> { 161 | debug_assert_eq!(temp_row.len(), self.width as _); 162 | 163 | if self.f_pixels.is_some() || (allow_steamed && self.should_use_low_memory()) { 164 | return Ok(()); 165 | } 166 | 167 | let width = self.width(); 168 | let lut = gamma_lut(self.gamma); 169 | let mut f_pixels = temp_buf(width * self.height())?; 170 | for (row, f_row) in f_pixels.chunks_exact_mut(width).enumerate() { 171 | let row_pixels = self.row_rgba(temp_row, row); 172 | Self::convert_row_to_f(f_row, row_pixels, &lut); 173 | } 174 | // just initialized 175 | self.f_pixels = Some(unsafe { box_assume_init(f_pixels) }); 176 | Ok(()) 177 | } 178 | 179 | #[inline] 180 | pub fn rows_iter(&mut self, temp_row: &mut [MaybeUninit]) -> Result, Error> { 181 | self.prepare_iter(temp_row, true)?; 182 | Ok(DynamicRowsIter { 183 | temp_f_row: self.temp_f_row_for_iter()?, 184 | px: self, 185 | }) 186 | } 187 | 188 | /// Call `prepare_iter()` first 189 | #[inline] 190 | pub fn rows_iter_prepared(&self) -> Result, Error> { 191 | Ok(DynamicRowsIter { 192 | temp_f_row: self.temp_f_row_for_iter()?, 193 | px: self, 194 | }) 195 | } 196 | 197 | #[inline] 198 | pub fn rgba_rows_iter(&self) -> Result, Error> { 199 | // This happens when histogram image is recycled 200 | if let PixelsSource::Pixels { rows, .. } = &self.pixels { 201 | if rows.as_slice().is_empty() { 202 | return Err(Error::Unsupported); 203 | } 204 | } 205 | Ok(DynamicRowsIter { px: self, temp_f_row: None }) 206 | } 207 | 208 | #[inline] 209 | pub fn all_rows_f(&mut self) -> Result<&[f_pixel], Error> { 210 | if self.f_pixels.is_some() { 211 | return Ok(self.f_pixels.as_ref().unwrap()); // borrow-checker :( 212 | } 213 | self.prepare_iter(&mut temp_buf(self.width())?, false)?; 214 | self.f_pixels.as_deref().ok_or(Error::Unsupported) 215 | } 216 | 217 | /// Not recommended 218 | #[cfg(feature = "_internal_c_ffi")] 219 | pub(crate) unsafe fn set_memory_ownership(&mut self, own_rows: bool, own_pixels: bool, free_fn: unsafe extern "C" fn(*mut std::os::raw::c_void)) -> Result<(), Error> { 220 | if own_rows { 221 | match &mut self.pixels { 222 | PixelsSource::Pixels { rows, .. } => rows.make_owned(free_fn), 223 | PixelsSource::Callback(_) => return Err(Error::ValueOutOfRange), 224 | } 225 | } 226 | 227 | if own_pixels { 228 | let len = self.width() * self.height(); 229 | match &mut self.pixels { 230 | PixelsSource::Pixels { pixels: Some(pixels), .. } => pixels.make_owned(free_fn), 231 | PixelsSource::Pixels { pixels, rows } => { 232 | // the row with the lowest address is assumed to be at the start of the bitmap 233 | let ptr = rows.as_slice().iter().map(|p| p.0).min().ok_or(Error::Unsupported)?; 234 | *pixels = Some(SeaCow::c_owned(ptr.cast_mut(), len, free_fn)); 235 | }, 236 | PixelsSource::Callback(_) => return Err(Error::ValueOutOfRange), 237 | } 238 | } 239 | Ok(()) 240 | } 241 | 242 | pub fn free_histogram_inputs(&mut self) { 243 | if self.f_pixels.is_some() { 244 | self.pixels = PixelsSource::Pixels { 245 | rows: SeaCow::borrowed(&[]), 246 | pixels: None, 247 | }; 248 | } 249 | } 250 | 251 | #[inline(always)] 252 | #[must_use] 253 | pub const fn width(&self) -> usize { 254 | self.width as usize 255 | } 256 | 257 | #[inline(always)] 258 | #[must_use] 259 | pub const fn height(&self) -> usize { 260 | self.height as usize 261 | } 262 | } 263 | 264 | pub(crate) fn temp_buf(len: usize) -> Result]>, Error> { 265 | let mut v = Vec::new(); 266 | v.try_reserve_exact(len)?; 267 | unsafe { v.set_len(len) }; 268 | Ok(v.into_boxed_slice()) 269 | } 270 | 271 | #[test] 272 | fn send() { 273 | fn is_send() {} 274 | fn is_sync() {} 275 | is_send::(); 276 | is_sync::(); 277 | is_send::(); 278 | is_sync::(); 279 | } 280 | 281 | #[inline(always)] 282 | unsafe fn box_assume_init(s: Box<[MaybeUninit]>) -> Box<[T]> { 283 | std::mem::transmute(s) 284 | } 285 | 286 | #[inline(always)] 287 | unsafe fn slice_assume_init_mut(s: &mut [MaybeUninit]) -> &mut [T] { 288 | &mut *(s as *mut [MaybeUninit] as *mut [T]) 289 | } 290 | -------------------------------------------------------------------------------- /src/seacow.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "_internal_c_ffi")] 2 | use std::os::raw::c_void; 3 | use std::mem::MaybeUninit; 4 | 5 | #[derive(Clone)] 6 | pub struct SeaCow<'a, T> { 7 | inner: SeaCowInner<'a, T>, 8 | } 9 | 10 | unsafe impl Send for SeaCowInner<'_, T> {} 11 | unsafe impl Sync for SeaCowInner<'_, T> {} 12 | 13 | /// Rust assumes `*const T` is never `Send`/`Sync`, but it can be. 14 | /// This is fudge for https://github.com/rust-lang/rust/issues/93367 15 | #[repr(transparent)] 16 | #[derive(Copy, Clone)] 17 | pub(crate) struct Pointer(pub *const T); 18 | 19 | #[derive(Copy, Clone)] 20 | #[repr(transparent)] 21 | pub(crate) struct PointerMut(pub *mut T); 22 | 23 | unsafe impl Send for Pointer {} 24 | unsafe impl Sync for Pointer {} 25 | unsafe impl Send for PointerMut {} 26 | unsafe impl Sync for PointerMut {} 27 | 28 | impl SeaCow<'static, T> { 29 | #[inline] 30 | #[must_use] 31 | pub fn boxed(data: Box<[T]>) -> Self { 32 | Self { inner: SeaCowInner::Boxed(data) } 33 | } 34 | } 35 | 36 | impl<'a, T> SeaCow<'a, T> { 37 | #[inline] 38 | #[must_use] 39 | pub const fn borrowed(data: &'a [T]) -> Self { 40 | Self { inner: SeaCowInner::Borrowed(data) } 41 | } 42 | 43 | /// The pointer must be `malloc`-allocated 44 | #[inline] 45 | #[cfg(feature = "_internal_c_ffi")] 46 | #[must_use] 47 | pub unsafe fn c_owned(ptr: *mut T, len: usize, free_fn: unsafe extern "C" fn(*mut c_void)) -> Self { 48 | debug_assert!(!ptr.is_null()); 49 | debug_assert!(len > 0); 50 | 51 | Self { 52 | inner: SeaCowInner::Owned { ptr, len, free_fn }, 53 | } 54 | } 55 | 56 | #[inline] 57 | #[cfg(feature = "_internal_c_ffi")] 58 | pub(crate) fn make_owned(&mut self, free_fn: unsafe extern "C" fn(*mut c_void)) { 59 | if let SeaCowInner::Borrowed(slice) = self.inner { 60 | self.inner = SeaCowInner::Owned { ptr: slice.as_ptr().cast_mut(), len: slice.len(), free_fn }; 61 | } 62 | } 63 | } 64 | 65 | impl Clone for SeaCowInner<'_, T> { 66 | #[inline(never)] 67 | fn clone(&self) -> Self { 68 | let slice = match self { 69 | Self::Borrowed(data) => return Self::Borrowed(data), 70 | #[cfg(feature = "_internal_c_ffi")] 71 | Self::Owned { ptr, len, free_fn: _ } => unsafe { std::slice::from_raw_parts(*ptr, *len) }, 72 | Self::Boxed(data) => &**data, 73 | }; 74 | let mut v = Vec::new(); 75 | v.try_reserve_exact(slice.len()).unwrap(); 76 | v.extend_from_slice(slice); 77 | Self::Boxed(v.into_boxed_slice()) 78 | } 79 | } 80 | 81 | enum SeaCowInner<'a, T> { 82 | #[cfg(feature = "_internal_c_ffi")] 83 | Owned { ptr: *mut T, len: usize, free_fn: unsafe extern "C" fn(*mut c_void) }, 84 | Borrowed(&'a [T]), 85 | Boxed(Box<[T]>), 86 | } 87 | 88 | #[cfg(feature = "_internal_c_ffi")] 89 | impl Drop for SeaCowInner<'_, T> { 90 | fn drop(&mut self) { 91 | if let Self::Owned { ptr, free_fn, .. } = self { 92 | unsafe { 93 | (free_fn)((*ptr).cast()); 94 | } 95 | } 96 | } 97 | } 98 | 99 | impl SeaCow<'_, T> { 100 | #[must_use] 101 | pub fn as_slice(&self) -> &[T] { 102 | match &self.inner { 103 | #[cfg(feature = "_internal_c_ffi")] 104 | SeaCowInner::Owned { ptr, len, .. } => unsafe { std::slice::from_raw_parts(*ptr, *len) }, 105 | SeaCowInner::Borrowed(a) => a, 106 | SeaCowInner::Boxed(x) => x, 107 | } 108 | } 109 | } 110 | 111 | pub(crate) struct RowBitmap<'a, T> { 112 | rows: &'a [Pointer], 113 | width: usize, 114 | } 115 | unsafe impl Send for RowBitmap<'_, T> {} 116 | 117 | pub(crate) struct RowBitmapMut<'a, T> { 118 | rows: MutCow<'a, [PointerMut]>, 119 | width: usize, 120 | } 121 | unsafe impl Send for RowBitmapMut<'_, T> {} 122 | 123 | impl RowBitmapMut<'_, MaybeUninit> { 124 | #[inline] 125 | pub(crate) unsafe fn assume_init<'maybeowned>(&'maybeowned mut self) -> RowBitmap<'maybeowned, T> { 126 | #[allow(clippy::transmute_ptr_to_ptr)] 127 | RowBitmap { 128 | width: self.width, 129 | rows: std::mem::transmute::<&'maybeowned [PointerMut>], &'maybeowned [Pointer]>(self.rows.borrow_mut()), 130 | } 131 | } 132 | } 133 | 134 | impl RowBitmap<'_, T> { 135 | pub fn rows(&self) -> impl Iterator { 136 | let width = self.width; 137 | self.rows.iter().map(move |row| { 138 | unsafe { std::slice::from_raw_parts(row.0, width) } 139 | }) 140 | } 141 | } 142 | 143 | enum MutCow<'a, T: ?Sized> { 144 | Owned(Box), 145 | #[allow(dead_code)] /// This is optional, for FFI only 146 | Borrowed(&'a mut T), 147 | } 148 | 149 | impl MutCow<'_, T> { 150 | #[must_use] 151 | pub fn borrow_mut(&mut self) -> &mut T { 152 | match self { 153 | Self::Owned(a) => a, 154 | Self::Borrowed(a) => a, 155 | } 156 | } 157 | } 158 | 159 | impl<'a, T: Sync + Send + Copy + 'static> RowBitmapMut<'a, T> { 160 | #[inline] 161 | #[must_use] 162 | pub fn new_contiguous(data: &mut [T], width: usize) -> Self { 163 | Self { 164 | rows: MutCow::Owned(data.chunks_exact_mut(width).map(|r| PointerMut(r.as_mut_ptr())).collect()), 165 | width, 166 | } 167 | } 168 | 169 | /// Inner pointers must be valid for `'a` too, and at least `width` large each 170 | #[inline] 171 | #[cfg(feature = "_internal_c_ffi")] 172 | #[must_use] 173 | pub unsafe fn new(rows: &'a mut [*mut T], width: usize) -> Self { 174 | Self { 175 | rows: MutCow::Borrowed(&mut *(rows as *mut [*mut T] as *mut [PointerMut])), 176 | width, 177 | } 178 | } 179 | 180 | pub fn rows_mut(&mut self) -> impl Iterator + Send { 181 | let width = self.width; 182 | self.rows.borrow_mut().iter().map(move |row| { 183 | unsafe { std::slice::from_raw_parts_mut(row.0, width) } 184 | }) 185 | } 186 | 187 | pub(crate) fn chunks(&mut self, chunk_size: usize) -> impl Iterator> { 188 | self.rows.borrow_mut().chunks_mut(chunk_size).map(|chunk| RowBitmapMut { 189 | width: self.width, 190 | rows: MutCow::Borrowed(chunk), 191 | }) 192 | } 193 | 194 | #[must_use] 195 | pub(crate) fn len(&mut self) -> usize { 196 | self.rows.borrow_mut().len() 197 | } 198 | } 199 | --------------------------------------------------------------------------------