diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..48d03b8 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,33 @@ +name: CI + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +env: + CARGO_TERM_COLOR: always + CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + - uses: actions-rs/cargo@v1 + with: + command: test + args: --no-default-features + - uses: actions-rs/cargo@v1 + with: + command: test + args: --all-features --all + - uses: actions-rs/cargo@v1 + with: + command: test + args: --all-features --all --release diff --git a/.gitignore b/.gitignore index ea91e24..800f661 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,9 @@ -config.mk +imagequant.pc *.lo *.o *.a *.so.0 +*.so *.bz2 *.dylib *.dylib.0 @@ -11,12 +12,7 @@ config.mk org/pngquant/*.class org/pngquant/*.h target/ -msvc-dist/org/ -msvc-dist/*.md -msvc-dist/Makefile* -msvc-dist/*.cs -msvc-dist/*.xml -msvc-dist/CHANGELOG -msvc-dist/COPYRIGHT -msvc-dist/configure -msvc-dist/.gitignore +quantized_example.png +example +lodepng.? +Cargo.lock diff --git a/CHANGELOG b/CHANGELOG index 5b5be39..de92d8e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,70 @@ +version 4.4 +----------- + - palette quality improvements + - Histogram::from_palette convenience method + +version 4.3 +----------- + - code quality and performance improvements + - copyable Image object + +version 4.2 +----------- + - rewritten and improved handling of fixed palette colors + - support for palettes larger than 256 colors + - fix for remapping when importance_map has lots of pixels with 0 importance + +version 4.1 +----------- + - improved dithering over preset background + - remap_into_vec method + - fix for images over 16 megapixels + +version 4.0 +----------- + - rewritten in Rust + - replaced Makefiles with Cargo + +version 2.17 +------------ + - quality improvement + - ARM64 build fix + +version 2.16 +------------ + - fixed LCMS2 error handling + +version 2.15 +------------ + - speed and quality improvements + +version 2.14 +------------ + - improved Rust API + - quality improvements for remapping overlays over a background + +version 2.13 +------------ + - support OpenMP in clang + - dropped old Internet Explorer workarounds + - speed and quality improvements + +version 2.12 +------------ + - new liq_histogram_add_fixed_color() + - faster for large/complex images + - workarounds for Microsoft's outdated C compiler + +version 2.11 +------------ + - new liq_image_set_background() for high-quality remapping of GIF frames + - new liq_image_set_importance_map() for controlling which parts of the image get more palette colors + - improved OpenMP support + +version 2.10 +----------- + - supports building with Rust/Cargo + version 2.9 ----------- - new liq_histogram_add_colors() diff --git a/COPYRIGHT b/COPYRIGHT index 3514d66..4297b94 100644 --- a/COPYRIGHT +++ b/COPYRIGHT @@ -2,11 +2,11 @@ libimagequant is derived from code by Jef Poskanzer and Greg Roelofs licensed under pngquant's original license (at the end of this file), and contains extensive changes and additions by Kornel Lesiński -licensed under GPL v3. +licensed under GPL v3 or later. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -libimagequant © 2009-2016 by Kornel Lesiński. +libimagequant © 2009-2018 by Kornel Lesiński. GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 diff --git a/Cargo.toml b/Cargo.toml index c0fa22f..93f6940 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,31 +1,62 @@ -# libimagequant is a pure C library. -# Rust/Cargo is entirely optional. You can also use ./configure && make [package] +name = "imagequant" +version = "4.5.0" +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." authors = ["Kornel Lesiński "] -build = "rust/build.rs" -categories = ["external-ffi-bindings"] +license = "GPL-3.0-or-later" homepage = "https://pngquant.org/lib" -include = ["COPYRIGHT", "rust/*", "*.c", "*.h", "README.md", "Cargo.toml"] -license = "GPL-3.0+" -links = "imagequant" -name = "imagequant-sys" -readme = "README.md" repository = "https://github.com/ImageOptim/libimagequant" -version = "2.10.0" -description = "Statically linked C part of imagequant library powering tools such as pngquant.\n\nThis library is dual-licensed like pngquant: either GPL or a commercial license. See website for details: https://pngquant.org" +documentation = "https://docs.rs/imagequant" +categories = ["multimedia::images"] +keywords = ["quantization", "palette", "pngquant", "compression", "gif"] +include = ["COPYRIGHT", "src/*.rs", "*.h", "README.md", "Cargo.toml"] +readme = "README.md" +edition = "2021" +rust-version = "1.65" -[build-dependencies] -gcc = "0.3.43" +[features] +default = ["threads", "std"] -[dependencies] -rgb = "0.5.8" +# libimagequant makes good use of multi-threading, so disabling threads has a significant performance peanalty +threads = ["dep:rayon", "dep:thread_local", "std"] -[features] -default = ["sse"] -sse = [] +# supports up to 2048 colors for palettes, but NOT FOR REMAPPING +large_palettes = [] + +# To opt-in you must disable the default features to disable `std` and `threads`, and also enable `no_std` +std = [] +no_std = ["dep:hashbrown"] + +# this is private and unstable for imagequant-sys only, do not use +_internal_c_ffi = [] + +[profile.release] +debug = false +strip = true +panic = "abort" + +[profile.dev] +panic = "abort" [lib] -crate-type = ["cdylib", "rlib", "staticlib"] -name = "imagequant_sys" -path = "rust/libimagequant.rs" doctest = false + +[dependencies] +arrayvec = { version = "0.7.4", default-features = false } +rgb = { version = "0.8.47", default-features = false, features = ["bytemuck"] } +rayon = { version = "1.10.0", optional = true } +thread_local = { version = "1.1.8", optional = true } +# Used only in no_std +hashbrown = { version = "0.15.4", optional = true, default-features = false } + +[dev-dependencies] +lodepng = "3.10" + +[workspace] +members = ["imagequant-sys", "imagequant-sys/c_test"] + +[package.metadata.release] +consolidate-commits = true +tag-message = "" +tag-prefix = "" +tag-name = "{{version}}" diff --git a/Makefile b/Makefile deleted file mode 100644 index 6ca2ed0..0000000 --- a/Makefile +++ /dev/null @@ -1,100 +0,0 @@ --include config.mk - -STATICLIB=libimagequant.a -SHAREDLIB=libimagequant.$(SOLIBSUFFIX) -SOVER=0 - -JNILIB=libimagequant.jnilib -DLL=imagequant.dll -DLLIMP=imagequant_dll.a -DLLDEF=imagequant_dll.def -JNIDLL=libimagequant.dll -JNIDLLIMP=libimagequant_dll.a -JNIDLLDEF=libimagequant_dll.def - -OBJS = pam.o mediancut.o blur.o mempool.o kmeans.o nearest.o libimagequant.o -SHAREDOBJS = $(subst .o,.lo,$(OBJS)) - -JAVACLASSES = org/pngquant/LiqObject.class org/pngquant/PngQuant.class org/pngquant/Image.class org/pngquant/Result.class -JAVAHEADERS = $(JAVACLASSES:.class=.h) -JAVAINCLUDE = -I'$(JAVA_HOME)/include' -I'$(JAVA_HOME)/include/linux' -I'$(JAVA_HOME)/include/win32' -I'$(JAVA_HOME)/include/darwin' - -DISTFILES = $(OBJS:.o=.c) *.h README.md CHANGELOG COPYRIGHT Makefile configure -TARNAME = libimagequant-$(VERSION) -TARFILE = $(TARNAME)-src.tar.bz2 - -all: static - -static: $(STATICLIB) - -shared: $(SHAREDLIB) - -dll: - $(MAKE) CFLAGSADD="-DIMAGEQUANT_EXPORTS" $(DLL) - -java: $(JNILIB) - -java-dll: - $(MAKE) CFLAGSADD="-DIMAGEQUANT_EXPORTS" $(JNIDLL) - -$(DLL) $(DLLIMP): $(OBJS) - $(CC) -fPIC -shared -o $(DLL) $^ $(LDFLAGS) -Wl,--out-implib,$(DLLIMP),--output-def,$(DLLDEF) - -$(STATICLIB): $(OBJS) - $(AR) $(ARFLAGS) $@ $^ - -$(SHAREDOBJS): - $(CC) -fPIC $(CFLAGS) -c $(@:.lo=.c) -o $@ - -libimagequant.so: $(SHAREDOBJS) - $(CC) -shared -Wl,-soname,$(SHAREDLIB).$(SOVER) -o $(SHAREDLIB).$(SOVER) $^ $(LDFLAGS) - ln -fs $(SHAREDLIB).$(SOVER) $(SHAREDLIB) - -libimagequant.dylib: $(SHAREDOBJS) - $(CC) -shared -o $(SHAREDLIB).$(SOVER) $^ $(LDFLAGS) - ln -fs $(SHAREDLIB).$(SOVER) $(SHAREDLIB) - -$(OBJS): $(wildcard *.h) config.mk - -$(JNILIB): $(JAVAHEADERS) $(STATICLIB) org/pngquant/PngQuant.c - $(CC) -g $(CFLAGS) $(LDFLAGS) $(JAVAINCLUDE) -shared -o $@ $(STATICLIB) org/pngquant/PngQuant.c - -$(JNIDLL) $(JNIDLLIMP): $(JAVAHEADERS) $(OBJS) org/pngquant/PngQuant.c - $(CC) -fPIC -shared -I. $(JAVAINCLUDE) -o $(JNIDLL) $^ $(LDFLAGS) -Wl,--out-implib,$(JNIDLLIMP),--output-def,$(JNIDLLDEF) - -$(JAVACLASSES): %.class: %.java - javac $< - -$(JAVAHEADERS): %.h: %.class - javah -o $@ $(subst /,., $(patsubst %.class,%,$<)) && touch $@ - -dist: $(TARFILE) cargo - -$(TARFILE): $(DISTFILES) - rm -rf $(TARFILE) $(TARNAME) - mkdir $(TARNAME) - cp $(DISTFILES) $(TARNAME) - tar -cjf $(TARFILE) --numeric-owner --exclude='._*' $(TARNAME) - rm -rf $(TARNAME) - -shasum $(TARFILE) - -cargo: - rm -rf msvc-dist - git clone . -b msvc msvc-dist - rm -rf msvc-dist/Cargo.toml msvc-dist/org - cargo test - -clean: - rm -f $(OBJS) $(SHAREDOBJS) $(SHAREDLIB).$(SOVER) $(SHAREDLIB) $(STATICLIB) $(TARFILE) $(DLL) '$(DLLIMP)' '$(DLLDEF)' - rm -f $(JAVAHEADERS) $(JAVACLASSES) $(JNILIB) - -distclean: clean - rm -f config.mk - -config.mk: -ifeq ($(filter %clean %distclean, $(MAKECMDGOALS)), ) - ./configure -endif - -.PHONY: all static shared clean dist distclean dll java cargo -.DELETE_ON_ERROR: diff --git a/README.md b/README.md index ba91c97..4cbb4c7 100644 --- a/README.md +++ b/README.md @@ -1,629 +1,115 @@ -# libimagequant—Image Quantization Library +# [libimagequant](https://pngquant.org/lib/) — Image Quantization Library -Small, portable C library for high-quality conversion of RGBA images to 8-bit indexed-color (palette) images. -It's powering [pngquant2](https://pngquant.org). +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). -## License - -Libimagequant is dual-licensed: - -* For Free/Libre Open Source Software it's available under [GPL v3 or later](https://raw.github.com/ImageOptim/libimagequant/master/COPYRIGHT) with additional copyright notices for older parts of the code. - -* For use in non-GPL software (e.g. closed-source or App Store distribution) please ask kornel@pngquant.org for a commercial license. - -## Download - -The [library](https://pngquant.org/lib) is currently a part of the [pngquant2 project](https://pngquant.org). [Repository](https://github.com/ImageOptim/libimagequant). - -## Compiling and Linking - -The library can be linked with ANSI C, C++, [Rust](https://github.com/pornel/libimagequant-rust) and [Java](https://github.com/ImageOptim/libimagequant/tree/master/org/pngquant) programs. It has no external dependencies. - -To build on Unix-like systems run: - - make static - -it will create `libimagequant.a` which you can link with your program. - - gcc yourprogram.c /path/to/libimagequant.a - -On BSD, use `gmake` (GNU make) rather than the native `make`. - -Alternatively you can compile the library with your program simply by including all `.c` files (and define `NDEBUG` to get a fast version): - - gcc -std=c99 -O3 -DNDEBUG libimagequant/*.c yourprogram.c - -### Rust - -In [Rust](https://www.rust-lang.org/) you can use Cargo to build the library. Add [`imagequant`](https://crates.io/crates/imagequant) to dependencies of Rust programs, or `cargo build` [`imagequant-sys`](https://crates.io/crates/imagequant-sys) to build `libimagequant.a` for any language. - -### Java JNI - -To build Java JNI interface, ensure `JAVA_HOME` is set to your JDK directory, and run: - - # export JAVA_HOME=$(locate include/jni.h) # you may need to set JAVA_HOME first - make java - -It will create `libimagequant.jnilib` and classes in `org/pngquant/`. - -On Windows run `make java-dll` and it'll create `libimagequant.dll` instead. - -### Compiling on Windows/Visual Studio - -The library can be compiled with any C compiler that has at least basic support for C99 (GCC, clang, ICC, C++ Builder, even Tiny C Compiler), but Visual Studio 2012 and older are not up to date with the 1999 C standard. There are 2 options for using `libimagequant` on Windows: - - * Use Visual Studio **2015** and an [MSVC-compatible branch of the library](https://github.com/ImageOptim/libimagequant/tree/msvc) - * Or use GCC from [MinGW](http://www.mingw.org) or [MSYS2](http://www.msys2.org/). Use GCC to build `libimagequant.a` (using the instructions above for Unix) and add it along with `libgcc.a` (shipped with the MinGW compiler) to your VC project. - -## Overview - -The basic flow is: - -1. Create attributes object and configure the library. -2. Create image object from RGBA pixels or data source. -3. Perform quantization (generate palette). -4. Store remapped image and final palette. -5. Free memory. - -Please note that libimagequant only handles raw uncompressed arrays of pixels in memory and is completely independent of any file format. - -

- - #include "libimagequant.h" - - liq_attr *attr = liq_attr_create(); - liq_image *image = liq_image_create_rgba(attr, example_bitmap_rgba, width, height, 0); - liq_result *res; - liq_image_quantize(image, attr, &res); - - liq_write_remapped_image(res, image, example_bitmap_8bpp, example_bitmap_size); - const liq_palette *pal = liq_get_palette(res); - - // Save the image and the palette now. - for(int i=0; i < pal->count; i++) { - example_copy_palette_entry(pal->entries[i]); - } - // You'll need a PNG library to write to a file. - example_write_image(example_bitmap_8bpp); - - liq_result_destroy(res); - liq_image_destroy(image); - liq_attr_destroy(attr); - -Functions returning `liq_error` return `LIQ_OK` (`0`) on success and non-zero on error. - -It's safe to pass `NULL` to any function accepting `liq_attr`, `liq_image`, `liq_result` (in that case the error code `LIQ_INVALID_POINTER` will be returned). These objects can be reused multiple times. - -There are 3 ways to create image object for quantization: - - * `liq_image_create_rgba()` for simple, contiguous RGBA pixel arrays (width×height×4 bytes large bitmap). - * `liq_image_create_rgba_rows()` for non-contiguous RGBA pixel arrays (that have padding between rows or reverse order, e.g. BMP). - * `liq_image_create_custom()` for RGB, ABGR, YUV and all other formats that can be converted on-the-fly to RGBA (you have to supply the conversion function). - -Note that "image" here means raw uncompressed pixels. If you have a compressed image file, such as PNG, you must use another library (e.g. libpng or lodepng) to decode it first. - -## Functions - ----- - - liq_attr* liq_attr_create(void); - -Returns object that will hold initial settings (attributes) for the library. The object should be freed using `liq_attr_destroy()` after it's no longer needed. - -Returns `NULL` in the unlikely case that the library cannot run on the current machine (e.g. the library has been compiled for SSE-capable x86 CPU and run on VIA C3 CPU). - ----- - - liq_error liq_set_max_colors(liq_attr* attr, int colors); - -Specifies maximum number of colors to use. The default is 256. Instead of setting a fixed limit it's better to use `liq_set_quality()`. - -The first argument is attributes object from `liq_attr_create()`. - -Returns `LIQ_VALUE_OUT_OF_RANGE` if number of colors is outside the range 2-256. - ----- - - int liq_get_max_colors(liq_attr* attr); - -Returns the value set by `liq_set_max_colors()`. - ----- - - liq_error liq_set_quality(liq_attr* attr, int minimum, int maximum); - -Quality is in range `0` (worst) to `100` (best) and values are analoguous to JPEG quality (i.e. `80` is usually good enough). - -Quantization will attempt to use the lowest number of colors needed to achieve `maximum` quality. `maximum` value of `100` is the default and means conversion as good as possible. - -If it's not possible to convert the image with at least `minimum` quality (i.e. 256 colors is not enough to meet the minimum quality), then `liq_image_quantize()` will fail. The default minumum is `0` (proceeds regardless of quality). - -Quality measures how well the generated palette fits image given to `liq_image_quantize()`. If a different image is remapped with `liq_write_remapped_image()` then actual quality may be different. - -Regardless of the quality settings the number of colors won't exceed the maximum (see `liq_set_max_colors()`). - -The first argument is attributes object from `liq_attr_create()`. - -Returns `LIQ_VALUE_OUT_OF_RANGE` if target is lower than minimum or any of them is outside the 0-100 range. -Returns `LIQ_INVALID_POINTER` if `attr` appears to be invalid. - - liq_attr *attr = liq_attr_create(); - liq_set_quality(attr, 50, 80); // use quality 80 if possible. Give up if quality drops below 50. - ----- - - int liq_get_min_quality(liq_attr* attr); - -Returns the lower bound set by `liq_set_quality()`. - ----- - - int liq_get_max_quality(liq_attr* attr); - -Returns the upper bound set by `liq_set_quality()`. - ----- - - liq_image *liq_image_create_rgba(liq_attr *attr, void* pixels, int width, int height, double gamma); - -Creates an object that represents the image pixels to be used for quantization and remapping. The pixel array must be contiguous run of RGBA pixels (alpha is the last component, 0 = transparent, 255 = opaque). - -The first argument is attributes object from `liq_attr_create()`. The same `attr` object should be used for the entire process, from creation of images to quantization. - -The `pixels` array must not be modified or freed until this object is freed with `liq_image_destroy()`. See also `liq_image_set_memory_ownership()`. - -`width` and `height` are dimensions in pixels. An image 10x10 pixel large will need a 400-byte array. - -`gamma` can be `0` for images with the typical 1/2.2 [gamma](https://en.wikipedia.org/wiki/Gamma_correction). -Otherwise `gamma` must be > 0 and < 1, e.g. `0.45455` (1/2.2) or `0.55555` (1/1.8). Generated palette will use the same gamma unless `liq_set_output_gamma()` is used. If `liq_set_output_gamma` is not used, then it only affects whether brighter or darker areas of the image will get more palette colors allocated. - -Returns `NULL` on failure, e.g. if `pixels` is `NULL` or `width`/`height` is <= 0. - ----- - - liq_image *liq_image_create_rgba_rows(liq_attr *attr, void* rows[], int width, int height, double gamma); - -Same as `liq_image_create_rgba()`, but takes an array of pointers to rows of pixels. This allows defining images with reversed rows (like in BMP), "stride" different than width or using only fragment of a larger bitmap, etc. - -The `rows` array must have at least `height` elements, and each row must be at least `width` RGBA pixels wide. - - unsigned char *bitmap = …; - void *rows = malloc(height * sizeof(void*)); - int bytes_per_row = width * 4 + padding; // stride - for(int i=0; i < height; i++) { - rows[i] = bitmap + i * bytes_per_row; - } - liq_image *img = liq_image_create_rgba_rows(attr, rows, width, height, 0); - // … - liq_image_destroy(img); - free(rows); - -The row pointers and pixels must not be modified or freed until this object is freed with `liq_image_destroy()` (you can change that with `liq_image_set_memory_ownership()`). - -See also `liq_image_create_rgba()` and `liq_image_create_custom()`. - ----- - - liq_error liq_image_quantize(liq_image *const input_image, liq_attr *const attr, liq_result **out_result); - -Performs quantization (palette generation) based on settings in `attr` (from `liq_attr_create()`) and pixels of the image. - -Returns `LIQ_OK` if quantization succeeds and sets `liq_result` pointer in `out_result`. The last argument is used for receiving the `result` object: - - liq_result *result; - if (LIQ_OK == liq_image_quantize(img, attr, &result)) { // Note &result - // result pointer is valid here - } - -Returns `LIQ_QUALITY_TOO_LOW` if quantization fails due to limit set in `liq_set_quality()`. - -See `liq_write_remapped_image()`. - -If you want to generate one palette for multiple images at once, see `liq_histogram_create()`. - ----- - - liq_error liq_set_dithering_level(liq_result *res, float dither_level); - -Enables/disables dithering in `liq_write_remapped_image()`. Dithering level must be between `0` and `1` (inclusive). Dithering level `0` enables fast non-dithered remapping. Otherwise a variation of Floyd-Steinberg error diffusion is used. - -Precision of the dithering algorithm depends on the speed setting, see `liq_set_speed()`. - -Returns `LIQ_VALUE_OUT_OF_RANGE` if the dithering level is outside the 0-1 range. - ----- - - liq_error liq_write_remapped_image(liq_result *result, liq_image *input_image, void *buffer, size_t buffer_size); - -Remaps the image to palette and writes its pixels to the given buffer, 1 pixel per byte. - -The buffer must be large enough to fit the entire image, i.e. width×height bytes large. For safety, pass the size of the buffer as `buffer_size`. - -For best performance call `liq_get_palette()` *after* this function, as palette is improved during remapping (except when `liq_histogram_quantize()` is used). - -Returns `LIQ_BUFFER_TOO_SMALL` if given size of the buffer is not enough to fit the entire image. - - int buffer_size = width*height; - char *buffer = malloc(buffer_size); - if (LIQ_OK == liq_write_remapped_image(result, input_image, buffer, buffer_size)) { - liq_palette *pal = liq_get_palette(result); - // save image - } - -See `liq_get_palette()`. - -The buffer is assumed to be contiguous, with rows ordered from top to bottom, and no gaps between rows. If you need to write rows with padding or upside-down order, then use `liq_write_remapped_image_rows()`. - -Please note that it only writes raw uncompressed pixels to memory. It does not perform any PNG compression. If you'd like to create a PNG file then you need to pass the raw pixel data to another library, e.g. libpng or lodepng. See `rwpng.c` in `pngquant` project for an example how to do that. - ----- - - const liq_palette *liq_get_palette(liq_result *result); - -Returns pointer to palette optimized for image that has been quantized or remapped (final refinements are applied to the palette during remapping). - -It's valid to call this method before remapping, if you don't plan to remap any images or want to use same palette for multiple images. - -`liq_palette->count` contains number of colors (up to 256), `liq_palette->entries[n]` contains RGBA value for nth palette color. - -The palette is **temporary and read-only**. You must copy the palette elsewhere *before* calling `liq_result_destroy()`. +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). -Returns `NULL` on error. +## Getting started in C ----- +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. - void liq_attr_destroy(liq_attr *); - void liq_image_destroy(liq_image *); - void liq_result_destroy(liq_result *); - void liq_histogram_destroy(liq_histogram *); +```bash +rustup update +git clone https://github.com/ImageOptim/libimagequant +cd libimagequant/imagequant-sys +cargo build --release --target-dir=target +# makes target/release/libimagequant_sys.a +``` -Releases memory owned by the given object. Object must not be used any more after it has been freed. +See [the C library API documentation](https://pngquant.org/lib/) and [the readme](https://lib.rs/imagequant-sys) for more info. -Freeing `liq_result` also frees any `liq_palette` obtained from it. +## Getting started in Rust -## Advanced Functions +Add to `Cargo.toml`: ----- +```bash +rustup update +cargo add imagequant +``` - liq_error liq_set_speed(liq_attr* attr, int speed); +[See docs.rs for the library API documentation](https://docs.rs/imagequant). -Higher speed levels disable expensive algorithms and reduce quantization precision. The default speed is `3`. Speed `1` gives marginally better quality at significant CPU cost. Speed `10` has usually 5% lower quality, but is 8 times faster than the default. - -High speeds combined with `liq_set_quality()` will use more colors than necessary and will be less likely to meet minimum required quality. - - - - - - -
Features dependent on speed
Noise-sensitive ditheringspeed 1 to 5
Forced posterization8-10 or if image has more than million colors
Quantization error known1-7 or if minimum quality is set
Additional quantization techniques1-6
- -Returns `LIQ_VALUE_OUT_OF_RANGE` if the speed is outside the 1-10 range. - ----- - - int liq_get_speed(liq_attr* attr); - -Returns the value set by `liq_set_speed()`. - ----- - - liq_error liq_set_min_opacity(liq_attr* attr, int min); - -Alpha values higher than this will be rounded to opaque. This is a workaround for Internet Explorer 6, but because this browser is not used any more, this option is deprecated and will be removed. The default is `255` (no change). - -Returns `LIQ_VALUE_OUT_OF_RANGE` if the value is outside the 0-255 range. - ----- - - int liq_get_min_opacity(liq_attr* attr); - -Returns the value set by `liq_set_min_opacity()`. - ----- - - liq_set_min_posterization(liq_attr* attr, int bits); - -Ignores given number of least significant bits in all channels, posterizing image to `2^bits` levels. `0` gives full quality. Use `2` for VGA or 16-bit RGB565 displays, `4` if image is going to be output on a RGB444/RGBA4444 display (e.g. low-quality textures on Android). - -Returns `LIQ_VALUE_OUT_OF_RANGE` if the value is outside the 0-4 range. - ----- - - int liq_get_min_posterization(liq_attr* attr); - -Returns the value set by `liq_set_min_posterization()`. - ----- - - liq_set_last_index_transparent(liq_attr* attr, int is_last); - -`0` (default) makes alpha colors sorted before opaque colors. Non-`0` mixes colors together except completely transparent color, which is moved to the end of the palette. This is a workaround for programs that blindly assume the last palette entry is transparent. - ----- - - liq_image *liq_image_create_custom(liq_attr *attr, liq_image_get_rgba_row_callback *row_callback, void *user_info, int width, int height, double gamma); - -

- - void image_get_rgba_row_callback(liq_color row_out[], int row_index, int width, void *user_info) { - for(int column_index=0; column_index < width; column_index++) { - row_out[column_index] = /* generate pixel at (row_index, column_index) */; - } - } - -Creates image object that will use callback to read image data. This allows on-the-fly conversion of images that are not in the RGBA color space. - -`user_info` value will be passed to the callback. It may be useful for storing pointer to program's internal representation of the image. - -The callback must read/generate `row_index`-th row and write its RGBA pixels to the `row_out` array. Row `width` is given for convenience and will always equal to image width. - -The callback will be called multiple times for each row. Quantization and remapping require at least two full passes over image data, so caching of callback's work makes no sense — in such case it's better to convert entire image and use `liq_image_create_rgba()` instead. - -To use RGB image: - - void rgb_to_rgba_callback(liq_color row_out[], int row_index, int width, void *user_info) { - unsigned char *rgb_row = ((unsigned char *)user_info) + 3*width*row_index; - - for(int i=0; i < width; i++) { - row_out[i].r = rgb_row[i*3]; - row_out[i].g = rgb_row[i*3+1]; - row_out[i].b = rgb_row[i*3+2]; - row_out[i].a = 255; - } - } - liq_image *img = liq_image_create_custom(attr, rgb_to_rgba_callback, rgb_bitmap, width, height, 0); - -The library doesn't support RGB bitmaps "natively", because supporting only single format allows compiler to inline more code, 4-byte pixel alignment is faster, and SSE instructions operate on 4 values at once, so alpha support is almost free. - ----- - - liq_error liq_image_set_memory_ownership(liq_image *image, int ownership_flags); - -Passes ownership of image pixel data and/or its rows array to the `liq_image` object, so you don't have to free it yourself. Memory owned by the object will be freed at its discretion with `free` function specified in `liq_attr_create_with_allocator()` (by default it's stdlib's `free()`). - -* `LIQ_OWN_PIXELS` makes pixel array owned by the object. The pixels will be freed automatically at any point when it's no longer needed. If you set this flag you must **not** free the pixel array yourself. If the image has been created with `liq_image_create_rgba_rows()` then the starting address of the array of pixels is assumed to be the lowest address of any row. - -* `LIQ_OWN_ROWS` makes array of row pointers (but not the pixels pointed by these rows) owned by the object. Rows will be freed when object is deallocated. If you set this flag you must **not** free the rows array yourself. This flag is valid only if the object has been created with `liq_image_create_rgba_rows()`. - -These flags can be combined with binary *or*, i.e. `LIQ_OWN_PIXELS | LIQ_OWN_ROWS`. - -This function must not be used if the image has been created with `liq_image_create_custom()`. - -Returns `LIQ_VALUE_OUT_OF_RANGE` if invalid flags are specified or the image object only takes pixels from a callback. - ----- - - liq_error liq_write_remapped_image_rows(liq_result *result, liq_image *input_image, unsigned char **row_pointers); - -Similar to `liq_write_remapped_image()`. Writes remapped image, at 1 byte per pixel, to each row pointed by `row_pointers` array. The array must have at least as many elements as height of the image, and each row must have at least as many bytes as width of the image. Rows must not overlap. - -For best performance call `liq_get_palette()` *after* this function, as remapping may change the palette (except when `liq_histogram_quantize()` is used). - -Returns `LIQ_INVALID_POINTER` if `result` or `input_image` is `NULL`. - ----- - - double liq_get_quantization_error(liq_result *result); - -Returns mean square error of quantization (square of difference between pixel values in the source image and its remapped version). Alpha channel, gamma correction and approximate importance of pixels is taken into account, so the result isn't exactly the mean square error of all channels. - -For most images MSE 1-5 is excellent. 7-10 is OK. 20-30 will have noticeable errors. 100 is awful. - -This function may return `-1` if the value is not available (this happens when a high speed has been requested, the image hasn't been remapped yet, and quality limit hasn't been set, see `liq_set_speed()` and `liq_set_quality()`). The value is not updated when multiple images are remapped, it applies only to the image used in `liq_image_quantize()` or the first image that has been remapped. See `liq_get_remapping_error()`. - ----- - - double liq_get_remapping_error(liq_result *result); - -Returns mean square error of last remapping done (square of difference between pixel values in the remapped image and its remapped version). Alpha channel and gamma correction are taken into account, so the result isn't exactly the mean square error of all channels. - -This function may return `-1` if the value is not available (this happens when a high speed has been requested or the image hasn't been remapped yet). - ----- - - double liq_get_quantization_quality(liq_result *result); - -Analoguous to `liq_get_quantization_error()`, but returns quantization error as quality value in the same 0-100 range that is used by `liq_set_quality()`. - -It may return `-1` if the value is not available (see note in `liq_get_quantization_error()`). - -This function can be used to add upper limit to quality options presented to the user, e.g. - - liq_attr *attr = liq_attr_create(); - liq_image *img = liq_image_create_rgba(…); - liq_result *res; - liq_image_quantize(img, attr, &res); - int max_attainable_quality = liq_get_quantization_quality(res); - printf("Please select quality between 0 and %d: ", max_attainable_quality); - int user_selected_quality = prompt(); - if (user_selected_quality < max_attainable_quality) { - liq_set_quality(user_selected_quality, 0); - liq_result_destroy(res); - liq_image_quantize(img, attr, &res); - } - liq_write_remapped_image(…); - ----- - - double liq_get_remapping_quality(liq_result *result); - -Analoguous to `liq_get_remapping_error()`, but returns quantization error as quality value in the same 0-100 range that is used by `liq_set_quality()`. - ----- - - void liq_set_log_callback(liq_attr*, liq_log_callback_function*, void *user_info); - -

- - void log_callback_function(const liq_attr*, const char *message, void *user_info) {} - ----- - - void liq_set_log_flush_callback(liq_attr*, liq_log_flush_callback_function*, void *user_info); -

- - void log_flush_callback_function(const liq_attr*, void *user_info) {} - -Sets up callback function to be called when the library reports status or errors. The callback must not call any library functions. - -`user_info` value will be passed through to the callback. It can be `NULL`. - -`NULL` callback clears the current callback. - -In the log callback the `message` is a zero-terminated string containing informative message to output. It is valid only until the callback returns, so you must copy it. - -`liq_set_log_flush_callback()` sets up callback function that will be called after the last log callback, which can be used to flush buffers and free resources used by the log callback. - ----- - - void liq_set_progress_callback(liq_attr*, liq_progress_callback_function*, void *user_info); - void liq_result_set_progress_callback(liq_result*, liq_progress_callback_function*, void *user_info); - -

- - int progress_callback_function(const liq_attr*, float progress_percent, void *user_info) {} - -Sets up callback function to be called while the library is processing images. The callback may abort processing by returning `0`. - -Setting callback to `NULL` clears the current callback. `liq_set_progress_callback` is for quantization progress, and `liq_result_set_progress_callback` is for remapping progress (currently only dithered remapping reports progress). - -`user_info` value will be passed through to the callback. It can be `NULL`. - -The callback must not call any library functions. - -`progress_percent` is a value between 0 and 100 that estimates how much of the current task has been done. - -The callback should return `1` to continue the operation, and `0` to abort current operation. - ----- - - liq_attr* liq_attr_create_with_allocator(void* (*malloc)(size_t), void (*free)(void*)); - -Same as `liq_attr_create`, but uses given `malloc` and `free` replacements to allocate all memory used by the library. - -The `malloc` function must return 16-byte aligned memory on x86 (and on other architectures memory aligned for `double` and pointers). Conversely, if your stdlib's `malloc` doesn't return appropriately aligned memory, you should use this function to provide aligned replacements. - ----- - - liq_attr* liq_attr_copy(liq_attr *orig); - -Creates an independent copy of `liq_attr`. The copy should also be freed using `liq_attr_destroy()`. - ---- - - liq_error liq_set_output_gamma(liq_result* res, double gamma); - -Sets gamma correction for generated palette and remapped image. Must be > 0 and < 1, e.g. `0.45455` for gamma 1/2.2 in PNG images. By default output gamma is same as gamma of the input image. - ----- - - int liq_image_get_width(const liq_image *img); - int liq_image_get_height(const liq_image *img); - double liq_get_output_gamma(const liq_result *result); - -Getters for `width`, `height` and `gamma` of the input image. - -If the input is invalid, these all return -1. - ---- - - liq_error liq_image_add_fixed_color(liq_image* img, liq_color color); - -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. - -RGB values of `liq_color` are assumed to have the same gamma as the image. - -It must be called before the image is quantized. - -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. - ---- - - int liq_version(); - -Returns version of the library as an integer. Same as `LIQ_VERSION`. Human-readable version is defined as `LIQ_VERSION_STRING`. - -## Multiple images with the same palette +## License -It's possible to efficiently generate a single palette that is optimal for multiple images, e.g. for an APNG animation. This is done by collecting statistics of images in a `liq_histogram` object. +Libimagequant is dual-licensed: - liq_attr *attr = liq_attr_create(); - liq_histogram *hist = liq_histogram_create(attr); +* 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. +* 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. - liq_image *image1 = liq_image_create_rgba(attr, example_bitmap_rgba1, width, height, 0); - liq_histogram_add_image(hist, attr, image1); +## Upgrading instructions - liq_image *image2 = liq_image_create_rgba(attr, example_bitmap_rgba2, width, height, 0); - liq_histogram_add_image(hist, attr, image2); +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). - liq_result *result; - liq_error err = liq_histogram_quantize(attr, hist, &result); - if (LIQ_OK == err) { - // result will contain shared palette best for both image1 and image2 - } +### C static library users ---- +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. - liq_histogram *liq_histogram_create(liq_attr *attr); +To build the library, install [Rust via rustup](https://rustup.rs), and run: -Creates histogram object that will be used to collect color statistics from multiple images. It must be freed using `liq_histogram_destroy()`. +```bash +rustup update +cd imagequant-sys +cargo build --release +``` -All options should be set on `attr` before the histogram object is created. Options changed later may not have effect. +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. +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). ---- +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`. - liq_error liq_histogram_add_image(liq_histogram *hist, liq_attr *attr, liq_image* image); +See [imagequant-sys readme](https://lib.rs/imagequant-sys#readme-building-for-c) for instructions how to make smaller builds. -"Learns" colors from the image, which will be later used to generate the palette. +### C dynamic library for package maintainers -After the image is added to the histogram it may be freed to save memory (but it's more efficient to keep the image object if it's going to be used for remapping). +If you're an application developer, please use the static linking option above — that option is much easier, and gives smaller executables. -Fixed colors added to the image are also added to the histogram. If total number of fixed colors exceeds 256, this function will fail with `LIQ_BUFFER_TOO_SMALL`. +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. ---- +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). - liq_error liq_histogram_add_colors(liq_histogram *hist, liq_attr *attr, liq_histogram_entry entries[], int num_entries, double gamma); +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}`. -Alternative to `liq_histogram_add_image()`. Intead of counting colors in an image, it directly takes an array of colors and their counts (see `liq_histogram_entry` in `libimagequant.h`). This function is only useful if you already have a histogram of the image from another source. +#### Building with `make` -For description of gamma, see `liq_image_create_rgba()`. +`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. ---- +#### Building with `cargo-c` - liq_error liq_histogram_quantize(liq_histogram *const hist, liq_attr *const attr, liq_result **out_result); +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. -Generates palette from the histogram. On success returns `LIQ_OK` and writes `liq_result*` pointer to `out_result`. Use it as follows: +```bash +rustup update +cd imagequant-sys +cargo install cargo-c +cargo cinstall --prefix=/usr/local --destdir=. +``` - liq_result *result; - liq_error err = liq_histogram_quantize(attr, hist, &result); - if (LIQ_OK == err) { - // Use result here to remap and get palette - } +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. -Returns `LIQ_QUALITY_TOO_LOW` if the palette is worse than limit set in `liq_set_quality()`. One histogram object can be quantized only once. +#### Interaction with pngquant -Palette generated using this function won't be improved during remapping. If you're generating palette for only one image, it's better to use `liq_image_quantize()`. +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. -## Multithreading +### Upgrading for Rust users -The library is stateless and doesn't use any global or thread-local storage. It doesn't use any locks. +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. -* Different threads can perform unrelated quantizations/remappings at the same time (e.g. each thread working on a different image). -* The same `liq_attr`, `liq_result`, etc. can be accessed from different threads, but not at the same time (e.g. you can create `liq_attr` in one thread and free it in another). +The `openmp` Cargo feature has been renamed to `threads`. -The library needs to sort unique colors present in the image. Although the sorting algorithm does few things to make stack usage minimal in typical cases, there is no guarantee against extremely degenerate cases, so threads should have automatically growing stack. +`.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. -### OpenMP +### Threads support and WASM -The library will parallelize some operations if compiled with OpenMP. +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. -You must not increase number of maximum threads after `liq_image` has been created, as it allocates some per-thread buffers. +Threads in WASM are experimental, and require [special handling](https://github.com/RReverser/wasm-bindgen-rayon). If you're targeting WASM, you'll most likely want to disable threads. -Callback of `liq_image_create_custom()` may be called from different threads at the same time. +To disable threads when using this library as a dependency, disable default features like this in `Cargo.toml`: -## Acknowledgements +```toml +[dependencies] +imagequant = { version = "4.0", default-features = false } +``` -Thanks to Irfan Skiljan for helping test the first version of the library. +When you compile the library directly, add `--no-default-features` flag instead. -The library is developed by [Kornel Lesiński](mailto:%20kornel@pngquant.org). diff --git a/benches/bench.rs b/benches/bench.rs new file mode 100644 index 0000000..73a15b1 --- /dev/null +++ b/benches/bench.rs @@ -0,0 +1,76 @@ +#![feature(test)] + +extern crate test; +use core::mem::MaybeUninit; +use test::Bencher; + +use imagequant::*; + +#[bench] +fn histogram(b: &mut Bencher) { + let img = lodepng::decode32_file("/Users/kornel/Desktop/canvas.png").unwrap(); + let liq = Attributes::new(); + b.iter(move || { + let mut img = liq.new_image(&*img.buffer, img.width, img.height, 0.).unwrap(); + let mut hist = Histogram::new(&liq); + hist.add_image(&liq, &mut img).unwrap(); + }); +} + +#[bench] +fn remap_ord(b: &mut Bencher) { + let img = lodepng::decode32_file("/Users/kornel/Desktop/canvas.png").unwrap(); + let mut buf = vec![MaybeUninit::uninit(); img.width * img.height]; + let mut liq = Attributes::new(); + liq.set_speed(10).unwrap(); + let mut img = liq.new_image(img.buffer, img.width, img.height, 0.).unwrap(); + liq.set_max_colors(256).unwrap(); + let mut res = liq.quantize(&mut img).unwrap(); + res.set_dithering_level(0.).unwrap(); + b.iter(move || { + res.remap_into(&mut img, &mut buf).unwrap(); + res.remap_into(&mut img, &mut buf).unwrap(); + }); +} + +#[bench] +fn kmeans(b: &mut Bencher) { + b.iter(_unstable_internal_kmeans_bench()); +} + +#[bench] +fn remap_floyd(b: &mut Bencher) { + let img = lodepng::decode32_file("/Users/kornel/Desktop/canvas.png").unwrap(); + let mut buf = vec![MaybeUninit::uninit(); img.width * img.height]; + let mut liq = Attributes::new(); + liq.set_speed(10).unwrap(); + let mut img = liq.new_image(img.buffer, img.width, img.height, 0.).unwrap(); + let mut res = liq.quantize(&mut img).unwrap(); + res.set_dithering_level(1.).unwrap(); + b.iter(move || { + res.remap_into(&mut img, &mut buf).unwrap(); + res.remap_into(&mut img, &mut buf).unwrap(); + }); +} + +#[bench] +fn quantize_s8(b: &mut Bencher) { + let img = lodepng::decode32_file("/Users/kornel/Desktop/canvas.png").unwrap(); + let mut liq = Attributes::new(); + liq.set_speed(8).unwrap(); + b.iter(move || { + let mut img = liq.new_image(&*img.buffer, img.width, img.height, 0.).unwrap(); + liq.quantize(&mut img).unwrap(); + }); +} + +#[bench] +fn quantize_s1(b: &mut Bencher) { + let img = lodepng::decode32_file("/Users/kornel/Desktop/canvas.png").unwrap(); + let mut liq = Attributes::new(); + liq.set_speed(1).unwrap(); + b.iter(move || { + let mut img = liq.new_image(&*img.buffer, img.width, img.height, 0.).unwrap(); + liq.quantize(&mut img).unwrap(); + }); +} diff --git a/blur.c b/blur.c deleted file mode 100644 index 7f0a716..0000000 --- a/blur.c +++ /dev/null @@ -1,132 +0,0 @@ -/* -© 2011-2015 by Kornel Lesiński. - -This file is part of libimagequant. - -libimagequant is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -libimagequant is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with libimagequant. If not, see . -*/ - -#include "libimagequant.h" -#include "pam.h" -#include "blur.h" - -/* - Blurs image horizontally (width 2*size+1) and writes it transposed to dst (called twice gives 2d blur) - */ -static void transposing_1d_blur(unsigned char *restrict src, unsigned char *restrict dst, unsigned int width, unsigned int height, const unsigned int size) -{ - assert(size > 0); - - for(unsigned int j=0; j < height; j++) { - unsigned char *restrict row = src + j*width; - - // accumulate sum for pixels outside line - unsigned int sum; - sum = row[0]*size; - for(unsigned int i=0; i < size; i++) { - sum += row[i]; - } - - // blur with left side outside line - for(unsigned int i=0; i < size; i++) { - sum -= row[0]; - sum += row[i+size]; - - dst[i*height + j] = sum / (size*2); - } - - for(unsigned int i=size; i < width-size; i++) { - sum -= row[i-size]; - sum += row[i+size]; - - dst[i*height + j] = sum / (size*2); - } - - // blur with right side outside line - for(unsigned int i=width-size; i < width; i++) { - sum -= row[i-size]; - sum += row[width-1]; - - dst[i*height + j] = sum / (size*2); - } - } -} - -/** - * Picks maximum of neighboring pixels (blur + lighten) - */ -LIQ_PRIVATE void liq_max3(unsigned char *src, unsigned char *dst, unsigned int width, unsigned int height) -{ - for(unsigned int j=0; j < height; j++) { - const unsigned char *row = src + j*width, - *prevrow = src + (j > 1 ? j-1 : 0)*width, - *nextrow = src + MIN(height-1,j+1)*width; - - unsigned char prev,curr=row[0],next=row[0]; - - for(unsigned int i=0; i < width-1; i++) { - prev=curr; - curr=next; - next=row[i+1]; - - unsigned char t1 = MAX(prev,next); - unsigned char t2 = MAX(nextrow[i],prevrow[i]); - *dst++ = MAX(curr,MAX(t1,t2)); - } - unsigned char t1 = MAX(curr,next); - unsigned char t2 = MAX(nextrow[width-1],prevrow[width-1]); - *dst++ = MAX(t1,t2); - } -} - -/** - * Picks minimum of neighboring pixels (blur + darken) - */ -LIQ_PRIVATE void liq_min3(unsigned char *src, unsigned char *dst, unsigned int width, unsigned int height) -{ - for(unsigned int j=0; j < height; j++) { - const unsigned char *row = src + j*width, - *prevrow = src + (j > 1 ? j-1 : 0)*width, - *nextrow = src + MIN(height-1,j+1)*width; - - unsigned char prev,curr=row[0],next=row[0]; - - for(unsigned int i=0; i < width-1; i++) { - prev=curr; - curr=next; - next=row[i+1]; - - unsigned char t1 = MIN(prev,next); - unsigned char t2 = MIN(nextrow[i],prevrow[i]); - *dst++ = MIN(curr,MIN(t1,t2)); - } - unsigned char t1 = MIN(curr,next); - unsigned char t2 = MIN(nextrow[width-1],prevrow[width-1]); - *dst++ = MIN(t1,t2); - } -} - -/* - Filters src image and saves it to dst, overwriting tmp in the process. - Image must be width*height pixels high. Size controls radius of box blur. - */ -LIQ_PRIVATE void liq_blur(unsigned char *src, unsigned char *tmp, unsigned char *dst, unsigned int width, unsigned int height, unsigned int size) -{ - assert(size > 0); - if (width < 2*size+1 || height < 2*size+1) { - return; - } - transposing_1d_blur(src, tmp, width, height, size); - transposing_1d_blur(tmp, dst, height, width, size); -} diff --git a/blur.h b/blur.h deleted file mode 100644 index 06ae8cb..0000000 --- a/blur.h +++ /dev/null @@ -1,4 +0,0 @@ - -LIQ_PRIVATE void liq_blur(unsigned char *src, unsigned char *tmp, unsigned char *dst, unsigned int width, unsigned int height, unsigned int size); -LIQ_PRIVATE void liq_max3(unsigned char *src, unsigned char *dst, unsigned int width, unsigned int height); -LIQ_PRIVATE void liq_min3(unsigned char *src, unsigned char *dst, unsigned int width, unsigned int height); diff --git a/configure b/configure deleted file mode 100755 index 4f5af7c..0000000 --- a/configure +++ /dev/null @@ -1,252 +0,0 @@ -#!/usr/bin/env bash - -CONFIG="config.mk" -PREFIX="/usr/local" -VERSION=$(grep LIQ_VERSION_STRING libimagequant.h | grep -Eo "2\.[0-9.]+") - -DEBUG= -QUIET=0 -SSE=auto -OPENMP= -EXTRA_CFLAGS= -EXTRA_LDFLAGS= - -# make gcc default compiler unless CC is already set -CC=${CC:-gcc} - -help() { - printf "%4s %s\n" "" "$1" -} - -for i in "$@"; do - case $i in - --help|-h) - echo - help "--prefix=

installation directory [$PREFIX]" - help "--extra-cflags= append to CFLAGS" - help "--extra-ldflags= append to LDFLAGS" - echo - help "--enable-debug" - help "--enable-sse/--disable-sse enable/disable SSE instructions" - echo - help "--with-openmp=static compile with multicore support" - echo - help "CC= use given compiler command" - help "CFLAGS= pass options to the compiler" - help "LDFLAGS= pass options to the linker" - echo - exit 0 - ;; - # Can be set before or after configure. Latter overrides former. - CC=*) - CC=${i#*=} - ;; - CFLAGS=*) - CFLAGS=${i#*=} - ;; - LDFLAGS=*) - LDFLAGS=${i#*=} - ;; - --enable-debug) - DEBUG=1 - ;; - --enable-sse) - SSE=1 - ;; - --disable-sse) - SSE=0 - ;; - --quiet) - QUIET=1 - ;; - '') - # allows a bash quirk - ;; - --with-openmp) - OPENMP=1 - ;; - --with-openmp=static) - OPENMP=static - ;; - --prefix=*) - PREFIX=${i#*=} - ;; - # can be used multiple times or in quotes to set multiple flags - --extra-cflags=*) - EXTRA_CFLAGS="$EXTRA_CFLAGS ${i#*=}" - ;; - --extra-ldflags=*) - EXTRA_LDFLAGS="$EXTRA_LDFLAGS ${i#*=}" - ;; - *) - echo "warning: unknown switch '${i%%=*}' (see $0 --help for the list)" - ;; - esac -done - -# If someone runs sudo make install as very first command, and configure later, -# $CONFIG cannot be overwritten, and must be deleted before continuing. -if [[ -f "$CONFIG" && ! -w "$CONFIG" ]]; then - echo "Cannot overwrite file $CONFIG! Please delete it." - exit 1 -fi - -cflags() { - CFLAGS="$CFLAGS $1" -} - -lflags() { - LDFLAGS="$LDFLAGS $1" -} - -status() { - if [ "$QUIET" -ne 1 ]; then - printf "%10s: %s\n" "$1" "$2" - fi -} - -# Append to CFLAGS if compiler supports flag, with optional prerequisite. -# Fails on errors and warnings. -conditional_cflags() { - if [ -z "$(echo | "$CC" -xc -S -o /dev/null $2 $1 - 2>&1)" ]; then - cflags "$1" - fi -} - -error() { - status "$1" "error ... $2" - echo - exit 1 -} - -if [ "$QUIET" -ne 1 ]; then - echo -fi - -# /tmp, because mingw has problems opening /dev/null and gives false negative -if ! echo "int main(){}" | "$CC" -xc -std=c99 -o /tmp/gcccheck - > /dev/null; then - error "Compiler" "$CC failed to compile anything (make sure it's installed and supports C99)" -fi - -status "Compiler" "$CC" - -# init flags -CFLAGS=${CFLAGS:--fno-math-errno -funroll-loops -fomit-frame-pointer -Wall} -cflags "-std=c99 -I." - -# DEBUG -if [ -z "$DEBUG" ]; then - cflags "-O3 -DNDEBUG" - status "Debug" "no" -else - cflags "-O1 -g" - status "Debug" "yes" -fi - -# SSE -if [ "$SSE" = 'auto' ]; then - SSE=0 - if type uname > /dev/null; then - if [[ "$(uname -m)" =~ "amd64" || "$(uname -m)" =~ "x86_64" || - "$(grep -E -m1 "^flags" /proc/cpuinfo)" =~ "sse" ]]; then - SSE=1 - fi - fi -fi - -if [ "$SSE" -eq 1 ]; then - status "SSE" "yes" - cflags "-DUSE_SSE=1" - cflags "-msse" - # Silence a later ICC warning due to -msse working slightly different. - conditional_cflags "-wd10121" - # Must be set explicitly for GCC on x86_32. Other compilers imply it. - conditional_cflags "-mfpmath=sse" "-msse" -elif [ "$SSE" -eq 0 ]; then - status "SSE" "no" - cflags "-DUSE_SSE=0" -fi - -# OpenMP -if [ -n "$OPENMP" ]; then - if [ "static" = "$OPENMP" ]; then - OPENMPFLAGS="-static-libgcc -Bstatic -fopenmp -Bdynamic" - else - OPENMPFLAGS="-fopenmp" - fi - if [[ "$("$CC" -xc -E $OPENMPFLAGS <(echo "#ifdef _OPENMP - #include - #endif") 2>&1)" =~ "omp_get_thread_num" ]]; then - cflags "$OPENMPFLAGS" - lflags "$OPENMPFLAGS" - status "OpenMP" "yes" - else - error "OpenMP" "not supported by compiler (please install a compiler that supports OpenMP (e.g. gcc) and specify it with the CC= argument)" - fi -else - # silence warnings about omp pragmas - cflags "-Wno-unknown-pragmas" - conditional_cflags "-wd3180" # ICC - status "OpenMP" "no" -fi - -# Cocoa -if [[ "$OSTYPE" =~ "darwin" ]]; then - cflags "-mmacosx-version-min=10.6" - lflags "-mmacosx-version-min=10.6" -fi - -if [[ "$OSTYPE" =~ "darwin" ]]; then - SOLIBSUFFIX=dylib - - # Search Developer SDK paths, since Apple seems to have dropped the standard Unixy ones - XCODE_CMD="xcode-select" - XCODE_PATH=$($XCODE_CMD -p) - DIRS+=("$XCODE_PATH/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.10.sdk/usr/include $XCODE_PATH/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.10.sdk/usr/lib") - DIRS+=("$XCODE_PATH/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.8.sdk/usr/include $XCODE_PATH/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.8.sdk/usr/lib") -elif [[ "$OSTYPE" =~ "msys" ]]; then - SOLIBSUFFIX=dll -else - SOLIBSUFFIX=so -fi - -if [ "$QUIET" -ne 1 ]; then - echo -fi - -# As of GCC 4.5, 387 fp math is significantly slower in C99 mode without this. -# Note: CPUs without SSE2 use 387 for doubles, even when SSE fp math is set. -conditional_cflags "-fexcess-precision=fast" - -# Intel C++ Compiler - -# ICC does usually only produce fast(er) code when it can optimize to the full -# capabilites of the (Intel) CPU. This is equivalent to -march=native for GCC. -conditional_cflags "-xHOST" - -# Disable unsafe fp optimizations and enforce fp precision as set in the source. -conditional_cflags "-fp-model source" - -# Silence a gold linker warning about string misalignment. -conditional_cflags "-falign-stack=maintain-16-byte" - -lflags "-lm" # Ubuntu requires this library last, issue #38 - -if [ -n "$EXTRA_CFLAGS" ]; then - cflags "$EXTRA_CFLAGS" -fi - -if [ -n "$EXTRA_LDFLAGS" ]; then - lflags "$EXTRA_LDFLAGS" -fi - -# Overwrite previous configuration. -echo " -# auto-generated by configure -PREFIX = $PREFIX -VERSION = $VERSION -CC = $CC -CFLAGS = $CFLAGS -LDFLAGS = $LDFLAGS -SOLIBSUFFIX = $SOLIBSUFFIX -" > "$CONFIG" diff --git a/examples/basic.rs b/examples/basic.rs new file mode 100644 index 0000000..9efe58e --- /dev/null +++ b/examples/basic.rs @@ -0,0 +1,34 @@ +//! + +fn main() { + // Image loading/saving is outside scope of this library + let width = 10; + let height = 10; + let fakebitmap = vec![imagequant::RGBA {r:100, g:200, b:250, a:255}; width * height]; + + // Configure the library + let mut liq = imagequant::new(); + liq.set_speed(5).unwrap(); + liq.set_quality(70, 99).unwrap(); + + // Describe the bitmap + let mut img = liq.new_image(&fakebitmap[..], width, height, 0.0).unwrap(); + + // The magic happens in quantize() + let mut res = match liq.quantize(&mut img) { + Ok(res) => res, + Err(err) => panic!("Quantization failed, because: {err:?}"), + }; + + // Enable dithering for subsequent remappings + res.set_dithering_level(1.0).unwrap(); + + // You can reuse the result to generate several images with the same palette + let (palette, pixels) = res.remapped(&mut img).unwrap(); + + println!( + "Done! Got palette {palette:?} and {} pixels with {}% quality", + pixels.len(), + res.quantization_quality().unwrap() + ); +} diff --git a/imagequant-sys/.gitignore b/imagequant-sys/.gitignore new file mode 100644 index 0000000..2f32155 --- /dev/null +++ b/imagequant-sys/.gitignore @@ -0,0 +1,2 @@ +usr/ +target/ diff --git a/imagequant-sys/COPYRIGHT b/imagequant-sys/COPYRIGHT new file mode 120000 index 0000000..dc5f40a --- /dev/null +++ b/imagequant-sys/COPYRIGHT @@ -0,0 +1 @@ +../COPYRIGHT \ No newline at end of file diff --git a/imagequant-sys/Cargo.toml b/imagequant-sys/Cargo.toml new file mode 100644 index 0000000..f77e05e --- /dev/null +++ b/imagequant-sys/Cargo.toml @@ -0,0 +1,76 @@ +[package] +name = "imagequant-sys" +version = "4.2.0" +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." +authors = ["Kornel Lesiński "] +license = "GPL-3.0-or-later" +homepage = "https://pngquant.org/lib" +repository = "https://github.com/ImageOptim/libimagequant" +documentation = "https://docs.rs/imagequant" +categories = ["multimedia::images"] +keywords = ["quantization", "palette", "image", "dither", "quant"] +include = ["COPYRIGHT", "src/*.rs", "build.rs", "*.h", "README.md", "Cargo.toml"] +readme = "README.md" +edition = "2021" +links = "imagequant" +build = "build.rs" +rust-version = "1.64" + +[features] +default = ["imagequant/default"] + +# libimagequant makes good use of multi-threading, so disabling threads has a significant performance peanalty +threads = ["imagequant/threads"] + +# To opt-in you must disable the default features to disable `std` and `threads`, and also enable `no_std` +std = ["imagequant/std"] +no_std = ["imagequant/no_std"] + +# Enable if you get errors like "no global memory allocator found" or "`#[panic_handler]` function required, but not found" +no_std_global_handlers = ["no_std"] + +# internal for cargo-c only +capi = [] + +[lib] +crate-type = ["staticlib", "lib"] +doctest = false +path = "src/ffi.rs" +name = "imagequant_sys" + +[dependencies] +imagequant = { path = "..", version = "4.5.0", default-features = false, features = ["_internal_c_ffi"] } +bitflags = "2.5" +libc = "0.2.153" + +[profile.release] +debug = false +strip = true +panic = "abort" + +[profile.dev] +panic = "abort" + +[package.metadata.capi.library] +name = "imagequant" +version = "0.4.1" + +[package.metadata.capi.pkg_config] +name = "imagequant" +filename = "imagequant" +description = "Convert 24/32-bit images to 8-bit palette with alpha channel." + +[package.metadata.capi.header] +name = "libimagequant" +subdirectory = "" +generation = false + +[package.metadata.capi.install.include] +asset = [{from = "libimagequant.h"}] + +[package.metadata.release] +consolidate-commits = true +tag-message = "" +tag-prefix = "" +tag-name = "{{version}}" + diff --git a/imagequant-sys/Makefile b/imagequant-sys/Makefile new file mode 100644 index 0000000..bfba261 --- /dev/null +++ b/imagequant-sys/Makefile @@ -0,0 +1,77 @@ +# You can configure these +PREFIX ?= /usr/local +LIBDIR ?= $(PREFIX)/lib +INCLUDEDIR ?= $(PREFIX)/include +PKGCONFIGDIR ?= $(LIBDIR)/pkgconfig +DESTDIR ?= "" + +VERSION=$(shell grep '^version = "4' Cargo.toml | grep -Eo "4\.[0-9.]+") +STATICLIB=libimagequant.a + +JNILIB=libimagequant.jnilib + +JAVACLASSES = org/pngquant/LiqObject.class org/pngquant/PngQuant.class org/pngquant/Image.class org/pngquant/Result.class +JAVAHEADERS = $(JAVACLASSES:.class=.h) +JAVAINCLUDE = -I'$(JAVA_HOME)/include' -I'$(JAVA_HOME)/include/linux' -I'$(JAVA_HOME)/include/win32' -I'$(JAVA_HOME)/include/darwin' + +PKGCONFIG = imagequant.pc + +all: static + +static: $(STATICLIB) + +java: $(JNILIB) + +$(STATICLIB): Cargo.toml + cargo build --release --lib --target-dir=../target + cp ../target/release/libimagequant_sys.a $(STATICLIB) + +$(JNILIB): $(JAVAHEADERS) $(STATICLIB) org/pngquant/PngQuant.c + # You may need to set LDFLAGS env var. See: cargo rustc -- --print native-static-libs + $(CC) -g $(CFLAGS) $(LDFLAGS) $(JAVAINCLUDE) -shared -o $@ org/pngquant/PngQuant.c $(STATICLIB) + +$(JAVACLASSES): %.class: %.java + javac $< + +$(JAVAHEADERS): %.h: %.class + javah -o $@ $(subst /,., $(patsubst %.class,%,$<)) && touch $@ + +example: example.c lodepng.h lodepng.c $(STATICLIB) + # remove -lpthread on Windows + # add -ldl on Linux + # You may need to set LDFLAGS env var. See: cargo rustc -- --print native-static-libs + $(CC) -g $(CFLAGS) -Wall example.c $(STATICLIB) -lm -lpthread $(LDFLAGS) -o example + +lodepng.h: + curl -o lodepng.h -L https://raw.githubusercontent.com/lvandeve/lodepng/master/lodepng.h + +lodepng.c: + curl -o lodepng.c -L https://raw.githubusercontent.com/lvandeve/lodepng/master/lodepng.cpp + +clean: + rm -f $(SHAREDLIBVER) $(SHAREDLIB) $(STATICLIB) + rm -f $(JAVAHEADERS) $(JAVACLASSES) $(JNILIB) example + rm -rf ../target + +distclean: clean + rm -f imagequant.pc + +install: all $(PKGCONFIG) + install -d $(DESTDIR)$(LIBDIR) + install -d $(DESTDIR)$(PKGCONFIGDIR) + install -d $(DESTDIR)$(INCLUDEDIR) + install -m 644 $(STATICLIB) $(DESTDIR)$(LIBDIR)/$(STATICLIB) + install -m 644 $(PKGCONFIG) $(DESTDIR)$(PKGCONFIGDIR)/$(PKGCONFIG) + install -m 644 libimagequant.h $(DESTDIR)$(INCLUDEDIR)/libimagequant.h + $(FIX_INSTALL_NAME) + +uninstall: + rm -f $(DESTDIR)$(LIBDIR)/$(STATICLIB) + rm -f $(DESTDIR)$(PKGCONFIGDIR)/$(PKGCONFIG) + rm -f $(DESTDIR)$(INCLUDEDIR)/libimagequant.h + +$(PKGCONFIG): Cargo.toml + sed 's|@PREFIX@|$(PREFIX)|;s|@VERSION@|$(VERSION)|' < imagequant.pc.in > $(PKGCONFIG) + +.PHONY: all static clean distclean java +.DELETE_ON_ERROR: diff --git a/imagequant-sys/README.md b/imagequant-sys/README.md new file mode 100644 index 0000000..675ab51 --- /dev/null +++ b/imagequant-sys/README.md @@ -0,0 +1,667 @@ +# [libimagequant](https://pngquant.org/lib/) — Image Quantization Library + +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). + +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). + +## License + +Libimagequant is dual-licensed: + +* 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. +* 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. + + +## Rust documentation + +Add to `Cargo.toml`: + +```toml +[dependencies] +imagequant = "4.4" +``` + +[See docs.rs for the library API documentation](https://docs.rs/imagequant). + +## C documentation + +### Building for C + +1. Get Rust 1.80 or later via [rustup](https://rustup.rs) and run `rustup update`. +2. `cd imagequant-sys` + + The C API is exposed by a separate package called [`imagequant-sys`](https://github.com/ImageOptim/libimagequant/tree/main/imagequant-sys). + +3. Run `cargo build --release` + + This will build `target/release/libimagequant_sys.a` or `target\release\libimagequant_sys.lib` that you can use for static linking. + +The repository includes an Xcode project file that can be used on iOS and macOS. + +#### Smaller static library + +Please don't worry about the size of the `.a` file. It includes a few unused objects, but it should add only ~500KB when linked, and less if you use other Rust libraries, because it reuses some common standard library code. + +Add `-Wl,--as-needed` and `-Wl,--gc-sections` to the linker flags (`LDFLAGS`) to ensure unused code is removed. + +If you use `clang`, then enable Link-Time-Optimization (`-flto`) option in your compiler and linker, and set `CARGO_PROFILE_RELEASE_LTO=true` env var when running `cargo build`. + +You can make the binary a bit smaller by building with a trimmed version of the standard library: + +```bash +rustup default nightly +rustup update +cargo clean +cargo +nightly build --release -Z build-std=std,panic_abort -Z build-std-features=panic_immediate_abort +``` + +It's also possible to remove Rust's standard library entirely by building with `--no-default-features --features no_std`, but , at the performance cost of losing multi-threading support. Add to `cargo build` invocations. + +#### Dynamic library + +If you want to build a C dynamic library (DLL, so, dylib), then the best is to use a helper tool: + +1. `cargo install cargo-c` +2. `cd imagequant-sys` +3. `cargo cinstall --destdir=.` + + This will build `./usr/local/lib/libimagequant.0.4.{so,dylib,dll}` + +You can also use regular `cargo build --release` if you know how to set `rpath`. Set `CARGO_PROFILE_RELEASE_LTO=true` env var for a smaller binary. + +### C API usage + +1. Create attributes object and configure the library. +2. Create image object from RGBA pixels or data source. +3. Perform quantization (generate palette). +4. Store remapped image and final palette. +5. Free memory. + +Please note that libimagequant only handles raw uncompressed arrays of pixels in memory and is completely independent of any file format. + + /* See imagequant-sys/example.c for the full code! */ + + #include "libimagequant.h" + + liq_attr *attr = liq_attr_create(); + liq_image *image = liq_image_create_rgba(attr, example_bitmap_rgba, width, height, 0); + liq_result *res; + liq_image_quantize(image, attr, &res); + + liq_write_remapped_image(res, image, example_bitmap_8bpp, example_bitmap_size); + const liq_palette *pal = liq_get_palette(res); + + // Save the image and the palette now. + for(int i=0; i < pal->count; i++) { + example_copy_palette_entry(pal->entries[i]); + } + // You'll need a PNG library to write to a file. + example_write_image(example_bitmap_8bpp); + + liq_result_destroy(res); + liq_image_destroy(image); + liq_attr_destroy(attr); + +Functions returning `liq_error` return `LIQ_OK` (`0`) on success and non-zero on error. + +It's safe to pass `NULL` to any function accepting `liq_attr`, `liq_image`, `liq_result` (in that case the error code `LIQ_INVALID_POINTER` will be returned). These objects can be reused multiple times. + +There are 3 ways to create image object for quantization: + + * `liq_image_create_rgba()` for simple, contiguous RGBA pixel arrays (width×height×4 bytes large bitmap). + * `liq_image_create_rgba_rows()` for non-contiguous RGBA pixel arrays (that have padding between rows or reverse order, e.g. BMP). + * `liq_image_create_custom()` for RGB, ABGR, YUV and all other formats that can be converted on-the-fly to RGBA (you have to supply the conversion function). + +Note that "image" here means raw uncompressed pixels. If you have a compressed image file, such as PNG, you must use another library (e.g. libpng or lodepng) to decode it first. + +You'll find full example code in "example.c" file in the library source directory. + +## C Functions + +---- + + liq_attr* liq_attr_create(void); + +Returns object that will hold initial settings (attributes) for the library. The object should be freed using `liq_attr_destroy()` after it's no longer needed. + +Returns `NULL` in the unlikely case that the library cannot run on the current machine (e.g. the library has been compiled for SSE-capable x86 CPU and run on VIA C3 CPU). + +---- + + liq_error liq_set_max_colors(liq_attr* attr, int colors); + +Specifies maximum number of colors to use. The default is 256. Instead of setting a fixed limit it's better to use `liq_set_quality()`. + +The first argument is attributes object from `liq_attr_create()`. + +Returns `LIQ_VALUE_OUT_OF_RANGE` if number of colors is outside the range 2-256. + +---- + + int liq_get_max_colors(liq_attr* attr); + +Returns the value set by `liq_set_max_colors()`. + +---- + + liq_error liq_set_quality(liq_attr* attr, int minimum, int maximum); + +Quality is in range `0` (worst) to `100` (best) and values are analoguous to JPEG quality (i.e. `80` is usually good enough). + +Quantization will attempt to use the lowest number of colors needed to achieve `maximum` quality. `maximum` value of `100` is the default and means conversion as good as possible. + +If it's not possible to convert the image with at least `minimum` quality (i.e. 256 colors is not enough to meet the minimum quality), then `liq_image_quantize()` will fail. The default minimum is `0` (proceeds regardless of quality). + +Quality measures how well the generated palette fits image given to `liq_image_quantize()`. If a different image is remapped with `liq_write_remapped_image()` then actual quality may be different. + +Regardless of the quality settings the number of colors won't exceed the maximum (see `liq_set_max_colors()`). + +The first argument is attributes object from `liq_attr_create()`. + +Returns `LIQ_VALUE_OUT_OF_RANGE` if target is lower than minimum or any of them is outside the 0-100 range. +Returns `LIQ_INVALID_POINTER` if `attr` appears to be invalid. + + liq_attr *attr = liq_attr_create(); + liq_set_quality(attr, 50, 80); // use quality 80 if possible. Give up if quality drops below 50. + +---- + + int liq_get_min_quality(liq_attr* attr); + +Returns the lower bound set by `liq_set_quality()`. + +---- + + int liq_get_max_quality(liq_attr* attr); + +Returns the upper bound set by `liq_set_quality()`. + +---- + + liq_image *liq_image_create_rgba(liq_attr *attr, void* pixels, int width, int height, double gamma); + +Creates an object that represents the image pixels to be used for quantization and remapping. The pixel array must be contiguous run of RGBA pixels (alpha is the last component, 0 = transparent, 255 = opaque). + +The first argument is attributes object from `liq_attr_create()`. The same `attr` object should be used for the entire process, from creation of images to quantization. + +The `pixels` array must not be modified or freed until this object is freed with `liq_image_destroy()`. See also `liq_image_set_memory_ownership()`. + +`width` and `height` are dimensions in pixels. An image 10x10 pixel large will need a 400-byte array. + +If the `gamma` argument is `0`, then the default of 1/2.2 [gamma](https://en.wikipedia.org/wiki/Gamma_correction) is assumed, which is good for most sRGB images. +Otherwise `gamma` must be > 0 and < 1, e.g. `0.45455` (1/2.2) or `0.55555` (1/1.8). Generated palette will use the same gamma unless `liq_set_output_gamma()` is used. If `liq_set_output_gamma` is not used, then it only affects whether brighter or darker areas of the image will get more palette colors allocated. + +Returns `NULL` on failure, e.g. if `pixels` is `NULL` or `width`/`height` is <= 0. + +---- + + liq_image *liq_image_create_rgba_rows(liq_attr *attr, void* rows[], int width, int height, double gamma); + +Same as `liq_image_create_rgba()`, but takes an array of pointers to rows of pixels. This allows defining images with reversed rows (like in BMP), "stride" different than width or using only fragment of a larger bitmap, etc. + +The `rows` array must have at least `height` elements, and each row must be at least `width` RGBA pixels wide. + + unsigned char *bitmap = …; + void *rows = malloc(height * sizeof(void*)); + int bytes_per_row = width * 4 + padding; // stride + for(int i=0; i < height; i++) { + rows[i] = bitmap + i * bytes_per_row; + } + liq_image *img = liq_image_create_rgba_rows(attr, rows, width, height, 0); + // … + liq_image_destroy(img); + free(rows); + +The row pointers and pixels must not be modified or freed until this object is freed with `liq_image_destroy()` (you can change that with `liq_image_set_memory_ownership()`). + +See also `liq_image_create_rgba()` and `liq_image_create_custom()`. + +---- + + liq_error liq_image_quantize(liq_image *const input_image, liq_attr *const attr, liq_result **out_result); + +Performs quantization (palette generation) based on settings in `attr` (from `liq_attr_create()`) and pixels of the image. + +Returns `LIQ_OK` if quantization succeeds and sets `liq_result` pointer in `out_result`. The last argument is used for receiving the `result` object: + + liq_result *result; + if (LIQ_OK == liq_image_quantize(img, attr, &result)) { // Note &result + // result pointer is valid here + } + +Returns `LIQ_QUALITY_TOO_LOW` if quantization fails due to limit set in `liq_set_quality()`. + +See `liq_write_remapped_image()`. + +If you want to generate one palette for multiple images at once, see `liq_histogram_create()`. + +---- + + liq_error liq_set_dithering_level(liq_result *res, float dither_level); + +Enables/disables dithering in `liq_write_remapped_image()`. Dithering level must be between `0` and `1` (inclusive). Dithering level `0` enables fast non-dithered remapping. Otherwise a variation of Floyd-Steinberg error diffusion is used. + +Precision of the dithering algorithm depends on the speed setting, see `liq_set_speed()`. + +Returns `LIQ_VALUE_OUT_OF_RANGE` if the dithering level is outside the 0-1 range. + +---- + + liq_error liq_write_remapped_image(liq_result *result, liq_image *input_image, void *buffer, size_t buffer_size); + +Remaps the image to palette and writes its pixels to the given buffer, 1 pixel per byte. + +The buffer must be large enough to fit the entire image, i.e. width×height bytes large. For safety, pass the size of the buffer as `buffer_size`. + +For best performance call `liq_get_palette()` *after* this function, as palette is improved during remapping (except when `liq_histogram_quantize()` is used). + +Returns `LIQ_BUFFER_TOO_SMALL` if given size of the buffer is not enough to fit the entire image. + + int buffer_size = width*height; + char *buffer = malloc(buffer_size); + if (LIQ_OK == liq_write_remapped_image(result, input_image, buffer, buffer_size)) { + liq_palette *pal = liq_get_palette(result); + // save image + } + +See `liq_get_palette()`. + +The buffer is assumed to be contiguous, with rows ordered from top to bottom, and no gaps between rows. If you need to write rows with padding or upside-down order, then use `liq_write_remapped_image_rows()`. + +Please note that it only writes raw uncompressed pixels to memory. It does not perform any PNG compression. If you'd like to create a PNG file then you need to pass the raw pixel data to another library, e.g. libpng or lodepng. See `rwpng.c` in `pngquant` project for an example how to do that. + +---- + + const liq_palette *liq_get_palette(liq_result *result); + +Returns pointer to palette optimized for image that has been quantized or remapped (final refinements are applied to the palette during remapping). + +It's valid to call this method before remapping, if you don't plan to remap any images or want to use same palette for multiple images. + +`liq_palette->count` contains number of colors (up to 256), `liq_palette->entries[n]` contains RGBA value for nth palette color. + +The palette is **temporary and read-only**. You must copy the palette elsewhere *before* calling `liq_result_destroy()`. + +Returns `NULL` on error. + +---- + + void liq_attr_destroy(liq_attr *); + void liq_image_destroy(liq_image *); + void liq_result_destroy(liq_result *); + void liq_histogram_destroy(liq_histogram *); + +Releases memory owned by the given object. Object must not be used any more after it has been freed. + +Freeing `liq_result` also frees any `liq_palette` obtained from it. + +## Advanced Functions + +---- + + liq_error liq_set_speed(liq_attr* attr, int speed); + +Higher speed levels disable expensive algorithms and reduce quantization precision. The default speed is `4`. Speed `1` gives marginally better quality at significant CPU cost. Speed `10` has usually 5% lower quality, but is 8 times faster than the default. + +High speeds combined with `liq_set_quality()` will use more colors than necessary and will be less likely to meet minimum required quality. + + + + + + +
Features dependent on speed
Noise-sensitive ditheringspeed 1 to 5
Forced posterization8-10 or if image has more than million colors
Quantization error known1-7 or if minimum quality is set
Additional quantization techniques1-6
+ +Returns `LIQ_VALUE_OUT_OF_RANGE` if the speed is outside the 1-10 range. + +---- + + int liq_get_speed(liq_attr* attr); + +Returns the value set by `liq_set_speed()`. + +---- + + liq_error liq_set_min_opacity(liq_attr* attr, int min); + +This was a workaround for Internet Explorer 6, but because this browser is not used any more, this option has been deprecated and removed. + +---- + + int liq_get_min_opacity(liq_attr* attr); + +This function has been deprecated. + +---- + + liq_set_min_posterization(liq_attr* attr, int bits); + +Ignores given number of least significant bits in all channels, posterizing image to `2^bits` levels. `0` gives full quality. Use `2` for VGA or 16-bit RGB565 displays, `4` if image is going to be output on a RGB444/RGBA4444 display (e.g. low-quality textures on Android). + +Returns `LIQ_VALUE_OUT_OF_RANGE` if the value is outside the 0-4 range. + +---- + + int liq_get_min_posterization(liq_attr* attr); + +Returns the value set by `liq_set_min_posterization()`. + +---- + + liq_set_last_index_transparent(liq_attr* attr, int is_last); + +`0` (default) makes alpha colors sorted before opaque colors. Non-`0` mixes colors together except completely transparent color, which is moved to the end of the palette. This is a workaround for programs that blindly assume the last palette entry is transparent. + +---- + + liq_image *liq_image_create_custom(liq_attr *attr, liq_image_get_rgba_row_callback *row_callback, void *user_info, int width, int height, double gamma); + +

+ + void image_get_rgba_row_callback(liq_color row_out[], int row_index, int width, void *user_info) { + for(int column_index=0; column_index < width; column_index++) { + row_out[column_index] = /* generate pixel at (row_index, column_index) */; + } + } + +Creates image object that will use callback to read image data. This allows on-the-fly conversion of images that are not in the RGBA color space. + +`user_info` value will be passed to the callback. It may be useful for storing pointer to program's internal representation of the image. + +The callback must read/generate `row_index`-th row and write its RGBA pixels to the `row_out` array. Row `width` is given for convenience and will always equal to image width. + +The callback will be called multiple times for each row. Quantization and remapping require at least two full passes over image data, so caching of callback's work makes no sense — in such case it's better to convert entire image and use `liq_image_create_rgba()` instead. + +To use RGB image: + + void rgb_to_rgba_callback(liq_color row_out[], int row_index, int width, void *user_info) { + unsigned char *rgb_row = ((unsigned char *)user_info) + 3*width*row_index; + + for(int i=0; i < width; i++) { + row_out[i].r = rgb_row[i*3]; + row_out[i].g = rgb_row[i*3+1]; + row_out[i].b = rgb_row[i*3+2]; + row_out[i].a = 255; + } + } + liq_image *img = liq_image_create_custom(attr, rgb_to_rgba_callback, rgb_bitmap, width, height, 0); + +The library doesn't support RGB bitmaps "natively", because supporting only single format allows compiler to inline more code, 4-byte pixel alignment is faster, and SSE instructions operate on 4 values at once, so alpha support is almost free. + +---- + + liq_error liq_image_set_memory_ownership(liq_image *image, int ownership_flags); + +Passes ownership of image pixel data and/or its rows array to the `liq_image` object, so you don't have to free it yourself. Memory owned by the object will be freed at its discretion with `free` function specified in `liq_attr_create_with_allocator()` (by default it's stdlib's `free()`). + +* `LIQ_OWN_PIXELS` makes pixel array owned by the object. The pixels will be freed automatically at any point when it's no longer needed. If you set this flag you must **not** free the pixel array yourself. If the image has been created with `liq_image_create_rgba_rows()` then the starting address of the array of pixels is assumed to be the lowest address of any row. + +* `LIQ_OWN_ROWS` makes array of row pointers (but not the pixels pointed by these rows) owned by the object. Rows will be freed when object is deallocated. If you set this flag you must **not** free the rows array yourself. This flag is valid only if the object has been created with `liq_image_create_rgba_rows()`. + +These flags can be combined with binary *or*, i.e. `LIQ_OWN_PIXELS | LIQ_OWN_ROWS`. + +This function must not be used if the image has been created with `liq_image_create_custom()`. + +Returns `LIQ_VALUE_OUT_OF_RANGE` if invalid flags are specified or the image object only takes pixels from a callback. + +---- + + liq_error liq_image_set_background(liq_image *image, liq_image *background_image); + +Analyze and remap this image with assumption that it will be always presented exactly on top of this background. + +When this image is remapped to a palette with a fully transparent color (use `liq_image_add_fixed_color()` to ensure this) pixels that are better represented by the background than the palette will be made transparent. This function can be used to improve quality of animated GIFs by setting previous animation frame as the background. + +This function takes full ownership of the background image, so you should **not** free the background object. It will be freed automatically together with the foreground image. + +Returns `LIQ_BUFFER_TOO_SMALL` if the background image has a different size than the foreground. + +---- + + liq_error liq_image_set_importance_map(liq_image *image, unsigned char map[], size_t buffer_size, liq_ownership ownership); + +Impotance map controls which areas of the image get more palette colors. Pixels corresponding to 0 values in the map are completely ignored. The higher the value the more weight is placed on the given pixel, giving it higher chance of influencing the final palette. + +The map is one byte per pixel and must have the same size as the image (width×height bytes). `buffer_size` argument is used to double-check that. + +If the `ownership` is `LIQ_COPY_PIXELS` then the `map` content be copied immediately (it's up to you to ensure the `map` memory is freed). + +If the `ownership` is `LIQ_OWN_PIXELS` then the `map` memory will be owned by the image and will be freed automatically when the image is freed. If a custom allocator has been set using `liq_attr_create_with_allocator()`, the `map` must be allocated using the same allocator. This option is deprecated. Use the Rust API or `LIQ_COPY_PIXELS` instead. + +Returns `LIQ_INVALID_POINTER` if any pointer is `NULL`, `LIQ_BUFFER_TOO_SMALL` if the `buffer_size` does not match the image size, and `LIQ_UNSUPPORTED` if `ownership` isn't a valid value. + +---- + + liq_error liq_write_remapped_image_rows(liq_result *result, liq_image *input_image, unsigned char **row_pointers); + +Similar to `liq_write_remapped_image()`. Writes remapped image, at 1 byte per pixel, to each row pointed by `row_pointers` array. The array must have at least as many elements as height of the image, and each row must have at least as many bytes as width of the image. Rows must not overlap. + +For best performance call `liq_get_palette()` *after* this function, as remapping may change the palette (except when `liq_histogram_quantize()` is used). + +Returns `LIQ_INVALID_POINTER` if `result` or `input_image` is `NULL`. + +---- + + double liq_get_quantization_error(liq_result *result); + +Returns mean square error of quantization (square of difference between pixel values in the source image and its remapped version). Alpha channel, gamma correction and approximate importance of pixels is taken into account, so the result isn't exactly the mean square error of all channels. + +For most images MSE 1-5 is excellent. 7-10 is OK. 20-30 will have noticeable errors. 100 is awful. + +This function may return `-1` if the value is not available (this happens when a high speed has been requested, the image hasn't been remapped yet, and quality limit hasn't been set, see `liq_set_speed()` and `liq_set_quality()`). The value is not updated when multiple images are remapped, it applies only to the image used in `liq_image_quantize()` or the first image that has been remapped. See `liq_get_remapping_error()`. + +---- + + double liq_get_remapping_error(liq_result *result); + +Returns mean square error of last remapping done (square of difference between pixel values in the remapped image and its remapped version). Alpha channel and gamma correction are taken into account, so the result isn't exactly the mean square error of all channels. + +This function may return `-1` if the value is not available (this happens when a high speed has been requested or the image hasn't been remapped yet). + +---- + + double liq_get_quantization_quality(liq_result *result); + +Analoguous to `liq_get_quantization_error()`, but returns quantization error as quality value in the same 0-100 range that is used by `liq_set_quality()`. + +It may return `-1` if the value is not available (see note in `liq_get_quantization_error()`). + +This function can be used to add upper limit to quality options presented to the user, e.g. + + liq_attr *attr = liq_attr_create(); + liq_image *img = liq_image_create_rgba(…); + liq_result *res; + liq_image_quantize(img, attr, &res); + int max_attainable_quality = liq_get_quantization_quality(res); + printf("Please select quality between 0 and %d: ", max_attainable_quality); + int user_selected_quality = prompt(); + if (user_selected_quality < max_attainable_quality) { + liq_set_quality(user_selected_quality, 0); + liq_result_destroy(res); + liq_image_quantize(img, attr, &res); + } + liq_write_remapped_image(…); + +---- + + double liq_get_remapping_quality(liq_result *result); + +Analoguous to `liq_get_remapping_error()`, but returns quantization error as quality value in the same 0-100 range that is used by `liq_set_quality()`. + +---- + + void liq_set_log_callback(liq_attr*, liq_log_callback_function*, void *user_info); + +

+ + void log_callback_function(const liq_attr*, const char *message, void *user_info) {} + +---- + + void liq_set_log_flush_callback(liq_attr*, liq_log_flush_callback_function*, void *user_info); +

+ + void log_flush_callback_function(const liq_attr*, void *user_info) {} + +Sets up callback function to be called when the library reports status or errors. The callback must not call any library functions. + +`user_info` value will be passed through to the callback. It can be `NULL`. + +`NULL` callback clears the current callback. + +In the log callback the `message` is a zero-terminated string containing informative message to output. It is valid only until the callback returns, so you must copy it. + +`liq_set_log_flush_callback()` sets up callback function that will be called after the last log callback, which can be used to flush buffers and free resources used by the log callback. + +---- + + void liq_set_progress_callback(liq_attr*, liq_progress_callback_function*, void *user_info); + void liq_result_set_progress_callback(liq_result*, liq_progress_callback_function*, void *user_info); + +

+ + int progress_callback_function(const liq_attr*, float progress_percent, void *user_info) {} + +Sets up callback function to be called while the library is processing images. The callback may abort processing by returning `0`. + +Setting callback to `NULL` clears the current callback. `liq_set_progress_callback` is for quantization progress, and `liq_result_set_progress_callback` is for remapping progress (currently only dithered remapping reports progress). + +`user_info` value will be passed through to the callback. It can be `NULL`. + +The callback must not call any library functions. + +`progress_percent` is a value between 0 and 100 that estimates how much of the current task has been done. + +The callback should return `1` to continue the operation, and `0` to abort current operation. + +---- + + liq_attr* liq_attr_create_with_allocator(void* (*malloc)(size_t), void (*free)(void*)); + +This function is deprecated. Same as `liq_attr_create`, but specifies `free` to use for `liq_image_set_memory_ownership`. The `malloc` argument is not used. + +The library will use Rust's [global allocator](https://doc.rust-lang.org/std/alloc/index.html). + +---- + + liq_attr* liq_attr_copy(liq_attr *orig); + +Creates an independent copy of `liq_attr`. The copy should also be freed using `liq_attr_destroy()`. + +--- + + liq_error liq_set_output_gamma(liq_result* res, double gamma); + +Sets gamma correction for generated palette and remapped image. Must be > 0 and < 1, e.g. `0.45455` for gamma 1/2.2 in PNG images. By default output gamma is same as gamma of the input image. + +---- + + int liq_image_get_width(const liq_image *img); + int liq_image_get_height(const liq_image *img); + double liq_get_output_gamma(const liq_result *result); + +Getters for `width`, `height` and `gamma` of the input image. + +If the input is invalid, these all return -1. + +--- + + liq_error liq_image_add_fixed_color(liq_image* img, liq_color color); + liq_error liq_histogram_add_fixed_color(liq_histogram* hist, liq_color color, double gamma); + +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. + +RGB values of `liq_color` are assumed to have the same gamma as the image. For the histogram function, the `gamma` can be `0` (see `liq_image_create_rgba()`). + +It must be called before the image is quantized. + +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. + +For histograms see also a more flexible `liq_histogram_add_colors()`. + +--- + + int liq_version(); + +Returns version of the library as an integer. Same as `LIQ_VERSION`. Human-readable version is defined as `LIQ_VERSION_STRING`. + +## Multiple images with the same palette + +It's possible to efficiently generate a single palette that is optimal for multiple images, e.g. for an APNG animation. This is done by collecting statistics of images in a `liq_histogram` object. + + liq_attr *attr = liq_attr_create(); + liq_histogram *hist = liq_histogram_create(attr); + + liq_image *image1 = liq_image_create_rgba(attr, example_bitmap_rgba1, width, height, 0); + liq_histogram_add_image(hist, attr, image1); + + liq_image *image2 = liq_image_create_rgba(attr, example_bitmap_rgba2, width, height, 0); + liq_histogram_add_image(hist, attr, image2); + + liq_result *result; + liq_error err = liq_histogram_quantize(attr, hist, &result); + if (LIQ_OK == err) { + // result will contain shared palette best for both image1 and image2 + } + +--- + + liq_histogram *liq_histogram_create(liq_attr *attr); + +Creates histogram object that will be used to collect color statistics from multiple images. It must be freed using `liq_histogram_destroy()`. + +All options should be set on `attr` before the histogram object is created. Options changed later may not have effect. + +--- + + liq_error liq_histogram_add_image(liq_histogram *hist, liq_attr *attr, liq_image* image); + +"Learns" colors from the image, which will be later used to generate the palette. + +After the image is added to the histogram it may be freed to save memory (but it's more efficient to keep the image object if it's going to be used for remapping). + +Fixed colors added to the image are also added to the histogram. If total number of fixed colors exceeds 256, this function will fail with `LIQ_BUFFER_TOO_SMALL`. + +--- + + liq_error liq_histogram_add_colors(liq_histogram *hist, liq_attr *attr, liq_histogram_entry entries[], int num_entries, double gamma); + +Alternative to `liq_histogram_add_image()`. Intead of counting colors in an image, it directly takes an array of colors and their counts (see `liq_histogram_entry` in `libimagequant.h`). This function is only useful if you already have a histogram of the image from another source. + +For description of gamma, see `liq_image_create_rgba()`. + +--- + + liq_error liq_histogram_quantize(liq_histogram *const hist, liq_attr *const attr, liq_result **out_result); + +Generates palette from the histogram. On success returns `LIQ_OK` and writes `liq_result*` pointer to `out_result`. Use it as follows: + + liq_result *result; + liq_error err = liq_histogram_quantize(attr, hist, &result); + if (LIQ_OK == err) { + // Use result here to remap and get palette + } + +Returns `LIQ_QUALITY_TOO_LOW` if the palette is worse than limit set in `liq_set_quality()`. One histogram object can be quantized only once. + +Palette generated using this function won't be improved during remapping. If you're generating palette for only one image, it's better to use `liq_image_quantize()`. + +## Multithreading + +* Different threads can perform unrelated quantizations/remappings at the same time (e.g. each thread working on a different image). +* The same `liq_attr`, `liq_result`, etc. can be accessed from different threads, but not at the same time (e.g. you can create `liq_attr` in one thread and free it in another). +* By default, this library uses threads internally. You can set `RAYON_NUM_THREADS` environmental variable to control the number of threads used. You can disable threads completely by compiling with `--no-default-features`. + +## Working with GIF + +The library can generate palettes for GIF images. To ensure correct transparency is used you need to preprocess the image yourself and replace alpha values other than 0 or 255 with one of these. + +For animated GIFs see `liq_image_set_background()` which remaps images for GIF's "keep" frame disposal method. See [gif.ski](https://gif.ski). + +## Cross-compilation + +You can compile the library for other platforms via `cargo build --target=…`. See `rustup target list` for the list of platforms. + +When compiling for WASM, you need to disable default features of this library (compile with `--no-default-features` flag). Otherwise it will use mult-threading, which requires [special handling in WASM](https://github.com/RReverser/wasm-bindgen-rayon). + +If you're cross-compiling a dynamic library (so/dylib/DLL), you may need to [configure a linker](https://doc.rust-lang.org/cargo/reference/config.html#target) for Cargo. For building for Android see [this tutorial](https://mozilla.github.io/firefox-browser-architecture/experiments/2017-09-21-rust-on-android.html) and [cargo-ndk](https://lib.rs/crates/cargo-ndk). diff --git a/imagequant-sys/build.rs b/imagequant-sys/build.rs new file mode 100644 index 0000000..78c382d --- /dev/null +++ b/imagequant-sys/build.rs @@ -0,0 +1,6 @@ +fn main() { + if cfg!(all(feature = "std", feature = "no_std")) { + println!("cargo:warning=both std and no_std features are enabled in imagequant-sys"); + } + println!("cargo:include={}", std::env::var("CARGO_MANIFEST_DIR").unwrap()); +} diff --git a/imagequant-sys/c_test/Cargo.toml b/imagequant-sys/c_test/Cargo.toml new file mode 100644 index 0000000..43633e6 --- /dev/null +++ b/imagequant-sys/c_test/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "c_test" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +doctest = false + +[package.metadata.release] +release = false + +[dependencies] +imagequant-sys = { version = "4.0.3", path = ".." } + +[build-dependencies] +cc = "1.1.7" diff --git a/imagequant-sys/c_test/build.rs b/imagequant-sys/c_test/build.rs new file mode 100644 index 0000000..522a8d6 --- /dev/null +++ b/imagequant-sys/c_test/build.rs @@ -0,0 +1,6 @@ +fn main() { + cc::Build::new() + .include("..") + .file("test.c") + .compile("imagequanttestbin"); +} diff --git a/imagequant-sys/c_test/src/lib.rs b/imagequant-sys/c_test/src/lib.rs new file mode 100644 index 0000000..f52c1b3 --- /dev/null +++ b/imagequant-sys/c_test/src/lib.rs @@ -0,0 +1,14 @@ +#[cfg(test)] +extern crate imagequant_sys; + +#[cfg(test)] +extern "C" { + fn run_liq_tests(); +} + +#[test] +fn c_test() { + unsafe { + run_liq_tests(); + } +} diff --git a/imagequant-sys/c_test/test.c b/imagequant-sys/c_test/test.c new file mode 100644 index 0000000..d2cc128 --- /dev/null +++ b/imagequant-sys/c_test/test.c @@ -0,0 +1,173 @@ +#undef NDEBUG +#include +#include "libimagequant.h" +#include +#include + +static char magic[] = "magic"; + +static void test_log_callback_function(const liq_attr *at, const char *message, void* user_info) { + assert(at); + assert(user_info == magic); + assert(message); + assert(strlen(message)); +} + +static int test_abort_callback(float progress_percent, void* user_info) { + assert(progress_percent >= 0.0 && progress_percent <= 100.0); + assert(user_info == magic); + return 0; +} + +static int progress_called = 0; +static int test_continue_callback(float progress_percent, void* user_info) { + assert(progress_percent >= 0.0 && progress_percent <= 100.0); + assert(user_info == magic); + progress_called++; + return 1; +} + +static void test_abort() { + liq_attr *attr = liq_attr_create(); + + unsigned char dummy[4] = {0}; + liq_image *img = liq_image_create_rgba(attr, dummy, 1, 1, 0); + + liq_attr_set_progress_callback(attr, test_abort_callback, magic); + + liq_result *res = liq_quantize_image(attr, img); + assert(!res); + + liq_attr_destroy(attr); +} + +static void test_zero_histogram() { + liq_attr *attr = liq_attr_create(); + liq_histogram *hist = liq_histogram_create(attr); + assert(hist); + + liq_result *res; + liq_error err = liq_histogram_quantize(hist, attr, &res); + assert(!res); + assert(err); + + liq_attr_destroy(attr); + liq_histogram_destroy(hist); +} + +static void test_histogram() { + liq_attr *attr = liq_attr_create(); + liq_histogram *hist = liq_histogram_create(attr); + assert(hist); + + const unsigned char dummy1[4] = {255,0,255,255}; + liq_image *const img1 = liq_image_create_rgba(attr, dummy1, 1, 1, 0); + assert(img1); + + const liq_error err1 = liq_histogram_add_image(hist, attr, img1); + assert(LIQ_OK == err1); + + const unsigned char dummy2[4] = {0,0,0,0}; + liq_image *const img2 = liq_image_create_rgba(attr, dummy2, 1, 1, 0); + assert(img2); + liq_image_add_fixed_color(img2, (liq_color){255,255,255,255}); + + + const liq_error err2 = liq_histogram_add_image(hist, attr, img2); + assert(LIQ_OK == err2); + + liq_image_destroy(img1); + liq_image_destroy(img2); + + liq_result *res; + liq_error err = liq_histogram_quantize(hist, attr, &res); + assert(LIQ_OK == err); + assert(res); + + liq_attr_destroy(attr); + + liq_histogram_destroy(hist); + + const liq_palette *pal = liq_get_palette(res); + assert(pal); + assert(pal->count == 3); + + liq_result_destroy(res); +} + +static void test_fixed_colors() { + liq_attr *attr = liq_attr_create(); + + liq_attr_set_progress_callback(attr, test_continue_callback, magic); + liq_set_log_callback(attr, test_log_callback_function, magic); + + unsigned char dummy[4] = {0}; + liq_image *img = liq_image_create_rgba(attr, dummy, 1, 1, 0); + assert(img); + + liq_image_add_fixed_color(img, (liq_color){0,0,0,0}); + + liq_result *res = liq_quantize_image(attr, img); + assert(res); + assert(progress_called); + + const liq_palette *pal = liq_get_palette(res); + assert(pal); + assert(pal->count == 1); + + liq_result_destroy(res); + liq_image_destroy(img); + liq_attr_destroy(attr); +} + +static void test_fixed_colors_order() { + liq_attr *attr = liq_attr_create(); + + unsigned char dummy[4] = {0}; + liq_image *img = liq_image_create_rgba(attr, dummy, 1, 1, 0); + assert(img); + + liq_color colors[17] = { + {0,0,0,0}, {1,1,1,1}, {2,2,2,2}, {3,3,3,3}, {4,4,4,4}, {5,4,4,4}, + {6,4,4,4}, {6,7,4,4}, {6,7,8,4}, {6,7,8,9}, {10,7,8,9}, {10,11,8,9}, + {10,11,12,9}, {10,11,12,13}, {14,11,12,13}, {14,15,12,13}, {253,254,255,254}, + }; + + for(int i=0; i < 17; i++) { + liq_image_add_fixed_color(img, colors[i]); + } + + liq_result *res = liq_quantize_image(attr, img); + assert(res); + + const liq_palette *pal = liq_get_palette(res); + assert(pal); + assert(pal->count == 17); + + for(int i=0; i < 17; i++) { + assert(pal->entries[i].r == colors[i].r); + assert(pal->entries[i].g == colors[i].g); + assert(pal->entries[i].b == colors[i].b); + assert(pal->entries[i].a == colors[i].a); + } + + liq_set_dithering_level(res, 1.0); + + char buf[1]; + assert(LIQ_OK == liq_write_remapped_image(res, img, buf, 1)); + + liq_result_set_progress_callback(res, test_abort_callback, magic); + assert(LIQ_ABORTED == liq_write_remapped_image(res, img, buf, 1)); + + liq_result_destroy(res); + liq_image_destroy(img); + liq_attr_destroy(attr); +} + +void run_liq_tests() { + test_fixed_colors(); + test_fixed_colors_order(); + test_abort(); + test_histogram(); + test_zero_histogram(); +} diff --git a/imagequant-sys/example.c b/imagequant-sys/example.c new file mode 100644 index 0000000..826a51f --- /dev/null +++ b/imagequant-sys/example.c @@ -0,0 +1,108 @@ +/** + +This is an example how to write your own simple pngquant using libimagequant. + +libimagequant works with any PNG library. This example uses lodepng, because it's easier to use than libpng. + +1. Get lodepng.c (download lodepng.cpp and rename it) and lodepng.h + from https://lodev.org/lodepng/ and put them in this directry + +2. Compile libimagequant (see README.md) + +3. Compile and run the example: + + gcc -O3 example.c libimagequant.a -o example + ./example truecolor_file.png + + +This example code can be freely copied under CC0 (public domain) license. +*/ + +#include "lodepng.h" // Get it from https://raw.githubusercontent.com/lvandeve/lodepng/master/lodepng.h +#include "lodepng.c" // Get it from https://raw.githubusercontent.com/lvandeve/lodepng/master/lodepng.cpp and rename +#include +#include +#include "libimagequant.h" + +int main(int argc, char *argv[]) { + if (argc < 2) { + fprintf(stderr, "Please specify a path to a PNG file\n"); + return EXIT_FAILURE; + } + + const char *input_png_file_path = argv[1]; + + // Load PNG file and decode it as raw RGBA pixels + // This uses lodepng library for PNG reading (not part of libimagequant) + + unsigned int width, height; + unsigned char *raw_rgba_pixels; + unsigned int status = lodepng_decode32_file(&raw_rgba_pixels, &width, &height, input_png_file_path); + if (status) { + fprintf(stderr, "Can't load %s: %s\n", input_png_file_path, lodepng_error_text(status)); + return EXIT_FAILURE; + } + + // Use libimagequant to make a palette for the RGBA pixels + + liq_attr *handle = liq_attr_create(); + liq_image *input_image = liq_image_create_rgba(handle, raw_rgba_pixels, width, height, 0); + // You could set more options here, like liq_set_quality + liq_result *quantization_result; + if (liq_image_quantize(input_image, handle, &quantization_result) != LIQ_OK) { + fprintf(stderr, "Quantization failed\n"); + return EXIT_FAILURE; + } + + // Use libimagequant to make new image pixels from the palette + + size_t pixels_size = width * height; + unsigned char *raw_8bit_pixels = malloc(pixels_size); + liq_set_dithering_level(quantization_result, 1.0); + + liq_write_remapped_image(quantization_result, input_image, raw_8bit_pixels, pixels_size); + const liq_palette *palette = liq_get_palette(quantization_result); + + // Save converted pixels as a PNG file + // This uses lodepng library for PNG writing (not part of libimagequant) + + LodePNGState state; + lodepng_state_init(&state); + state.info_raw.colortype = LCT_PALETTE; + state.info_raw.bitdepth = 8; + state.info_png.color.colortype = LCT_PALETTE; + state.info_png.color.bitdepth = 8; + + for(int i=0; i < palette->count; i++) { + lodepng_palette_add(&state.info_png.color, palette->entries[i].r, palette->entries[i].g, palette->entries[i].b, palette->entries[i].a); + lodepng_palette_add(&state.info_raw, palette->entries[i].r, palette->entries[i].g, palette->entries[i].b, palette->entries[i].a); + } + + unsigned char *output_file_data; + size_t output_file_size; + unsigned int out_status = lodepng_encode(&output_file_data, &output_file_size, raw_8bit_pixels, width, height, &state); + if (out_status) { + fprintf(stderr, "Can't encode image: %s\n", lodepng_error_text(out_status)); + return EXIT_FAILURE; + } + + const char *output_png_file_path = "quantized_example.png"; + FILE *fp = fopen(output_png_file_path, "wb"); + if (!fp) { + fprintf(stderr, "Unable to write to %s\n", output_png_file_path); + return EXIT_FAILURE; + } + fwrite(output_file_data, 1, output_file_size, fp); + fclose(fp); + + printf("Written %s\n", output_png_file_path); + + // Done. Free memory. + + liq_result_destroy(quantization_result); // Must be freed only after you're done using the palette + liq_image_destroy(input_image); + liq_attr_destroy(handle); + + free(raw_8bit_pixels); + lodepng_state_cleanup(&state); +} diff --git a/imagequant-sys/imagequant.pc.in b/imagequant-sys/imagequant.pc.in new file mode 100644 index 0000000..16935b7 --- /dev/null +++ b/imagequant-sys/imagequant.pc.in @@ -0,0 +1,10 @@ +prefix=@PREFIX@ +includedir=${prefix}/include +libdir=${prefix}/lib + +Name: imagequant +Description: Small, portable C library for high-quality conversion of RGBA images to 8-bit indexed-color (palette) images. +URL: https://pngquant.org/lib/ +Version: @VERSION@ +Libs: -L${libdir} -limagequant +Cflags: -I${includedir} diff --git a/imagequant-sys/imagequant.xcodeproj/project.pbxproj b/imagequant-sys/imagequant.xcodeproj/project.pbxproj new file mode 100644 index 0000000..c63ef8c --- /dev/null +++ b/imagequant-sys/imagequant.xcodeproj/project.pbxproj @@ -0,0 +1,222 @@ +// !$*UTF8*$! +{ + /* generated with cargo-xcode 1.11.0 */ + archiveVersion = 1; + classes = { + }; + objectVersion = 53; + objects = { + +/* Begin PBXBuildFile section */ + CA0074F714595B987CA78108 /* Cargo.toml in Sources */ = {isa = PBXBuildFile; fileRef = CAF9EDA9C64B3EF4668187A5 /* Cargo.toml */; settings = {COMPILER_FLAGS = "--lib"; }; }; +/* End PBXBuildFile section */ + +/* Begin PBXBuildRule section */ + CAF4EDA9C64BAC6C1400ACA8 /* PBXBuildRule */ = { + isa = PBXBuildRule; + compilerSpec = com.apple.compilers.proxy.script; + dependencyFile = "$(DERIVED_FILE_DIR)/$(ARCHS)-$(EXECUTABLE_NAME).d"; + filePatterns = "*/Cargo.toml"; + fileType = pattern.proxy; + inputFiles = ( + ); + isEditable = 0; + name = "Cargo project build"; + outputFiles = ( + "$(TARGET_BUILD_DIR)/$(EXECUTABLE_NAME)", + ); + runOncePerArchitecture = 0; + script = "# generated with cargo-xcode 1.11.0\nset -euo pipefail;\nexport PATH=\"$HOME/.cargo/bin:$PATH:/usr/local/bin:/opt/homebrew/bin\";\n# don't use ios/watchos linker for build scripts and proc macros\nexport CARGO_TARGET_AARCH64_APPLE_DARWIN_LINKER=/usr/bin/ld\nexport CARGO_TARGET_X86_64_APPLE_DARWIN_LINKER=/usr/bin/ld\nexport NO_COLOR=1\n\ncase \"$PLATFORM_NAME\" in\n \"macosx\")\n CARGO_XCODE_TARGET_OS=darwin\n if [ \"${IS_MACCATALYST-NO}\" = YES ]; then\n CARGO_XCODE_TARGET_OS=ios-macabi\n fi\n ;;\n \"iphoneos\") CARGO_XCODE_TARGET_OS=ios ;;\n \"iphonesimulator\") CARGO_XCODE_TARGET_OS=ios-sim ;;\n \"appletvos\" | \"appletvsimulator\") CARGO_XCODE_TARGET_OS=tvos ;;\n \"watchos\") CARGO_XCODE_TARGET_OS=watchos ;;\n \"watchsimulator\") CARGO_XCODE_TARGET_OS=watchos-sim ;;\n \"xros\") CARGO_XCODE_TARGET_OS=visionos ;;\n \"xrsimulator\") CARGO_XCODE_TARGET_OS=visionos-sim ;;\n *)\n CARGO_XCODE_TARGET_OS=\"$PLATFORM_NAME\"\n echo >&2 \"warning: cargo-xcode needs to be updated to handle $PLATFORM_NAME\"\n ;;\nesac\n\nCARGO_XCODE_TARGET_TRIPLES=\"\"\nCARGO_XCODE_TARGET_FLAGS=\"\"\nLIPO_ARGS=\"\"\nfor arch in $ARCHS; do\n if [[ \"$arch\" == \"arm64\" ]]; then arch=aarch64; fi\n if [[ \"$arch\" == \"i386\" && \"$CARGO_XCODE_TARGET_OS\" != \"ios\" ]]; then arch=i686; fi\n triple=\"${arch}-apple-$CARGO_XCODE_TARGET_OS\"\n CARGO_XCODE_TARGET_TRIPLES+=\" $triple\"\n CARGO_XCODE_TARGET_FLAGS+=\" --target=$triple\"\n LIPO_ARGS+=\"$CARGO_TARGET_DIR/$triple/$CARGO_XCODE_BUILD_PROFILE/$CARGO_XCODE_CARGO_FILE_NAME\n\"\ndone\n\necho >&2 \"Cargo $CARGO_XCODE_BUILD_PROFILE $ACTION for $PLATFORM_NAME $ARCHS =$CARGO_XCODE_TARGET_TRIPLES; using ${SDK_NAMES:-}. \\$PATH is:\"\ntr >&2 : '\\n' <<<\"$PATH\"\n\nif command -v rustup &> /dev/null; then\n for triple in $CARGO_XCODE_TARGET_TRIPLES; do\n if ! rustup target list --installed | grep -Eq \"^$triple$\"; then\n echo >&2 \"warning: this build requires rustup toolchain for $triple, but it isn't installed (will try rustup next)\"\n rustup target add \"$triple\" || {\n echo >&2 \"warning: can't install $triple, will try nightly -Zbuild-std\";\n OTHER_INPUT_FILE_FLAGS+=\" -Zbuild-std\";\n if [ -z \"${RUSTUP_TOOLCHAIN:-}\" ]; then\n export RUSTUP_TOOLCHAIN=nightly\n fi\n break;\n }\n fi\n done\nfi\n\nif [ \"$CARGO_XCODE_BUILD_PROFILE\" = release ]; then\n OTHER_INPUT_FILE_FLAGS=\"$OTHER_INPUT_FILE_FLAGS --release\"\nfi\n\nif [ \"$ACTION\" = clean ]; then\n cargo clean --verbose --manifest-path=\"$SCRIPT_INPUT_FILE\" $CARGO_XCODE_TARGET_FLAGS $OTHER_INPUT_FILE_FLAGS;\n rm -f \"$SCRIPT_OUTPUT_FILE_0\"\n exit 0\nfi\n\n{ cargo build --manifest-path=\"$SCRIPT_INPUT_FILE\" --features=\"${CARGO_XCODE_FEATURES:-}\" $CARGO_XCODE_TARGET_FLAGS $OTHER_INPUT_FILE_FLAGS --verbose --message-format=short 2>&1 | sed -E 's/^([^ :]+:[0-9]+:[0-9]+: error)/\\1: /' >&2; } || { echo >&2 \"$SCRIPT_INPUT_FILE: error: cargo-xcode project build failed; $CARGO_XCODE_TARGET_TRIPLES\"; exit 1; }\n\ntr '\\n' '\\0' <<<\"$LIPO_ARGS\" | xargs -0 lipo -create -output \"$SCRIPT_OUTPUT_FILE_0\"\n\nif [ ${LD_DYLIB_INSTALL_NAME:+1} ]; then\n install_name_tool -id \"$LD_DYLIB_INSTALL_NAME\" \"$SCRIPT_OUTPUT_FILE_0\"\nfi\n\nDEP_FILE_DST=\"$DERIVED_FILE_DIR/${ARCHS}-${EXECUTABLE_NAME}.d\"\necho \"\" > \"$DEP_FILE_DST\"\nfor triple in $CARGO_XCODE_TARGET_TRIPLES; do\n BUILT_SRC=\"$CARGO_TARGET_DIR/$triple/$CARGO_XCODE_BUILD_PROFILE/$CARGO_XCODE_CARGO_FILE_NAME\"\n\n # cargo generates a dep file, but for its own path, so append our rename to it\n DEP_FILE_SRC=\"$CARGO_TARGET_DIR/$triple/$CARGO_XCODE_BUILD_PROFILE/$CARGO_XCODE_CARGO_DEP_FILE_NAME\"\n if [ -f \"$DEP_FILE_SRC\" ]; then\n cat \"$DEP_FILE_SRC\" >> \"$DEP_FILE_DST\"\n fi\n echo >> \"$DEP_FILE_DST\" \"${SCRIPT_OUTPUT_FILE_0/ /\\\\ /}: ${BUILT_SRC/ /\\\\ /}\"\ndone\ncat \"$DEP_FILE_DST\"\n\necho \"success: $ACTION of $SCRIPT_OUTPUT_FILE_0 for $CARGO_XCODE_TARGET_TRIPLES\"\n"; + }; +/* End PBXBuildRule section */ + +/* Begin PBXFileReference section */ + CA00DA7A53FB47C0CE156F07 /* libimagequant.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libimagequant.a; sourceTree = BUILT_PRODUCTS_DIR; }; + CAF9EDA9C64B3EF4668187A5 /* Cargo.toml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = Cargo.toml; path = Cargo.toml; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXGroup section */ + CAF0EDA9C64BD65BC3C892A8 = { + isa = PBXGroup; + children = ( + CAF9EDA9C64B3EF4668187A5 /* Cargo.toml */, + CAF1EDA9C64B22869D176AE5 /* Products */, + CAF2EDA9C64B98AF0B5890DB /* Frameworks */, + ); + sourceTree = ""; + }; + CAF1EDA9C64B22869D176AE5 /* Products */ = { + isa = PBXGroup; + children = ( + CA00DA7A53FB47C0CE156F07 /* libimagequant.a */, + ); + name = Products; + sourceTree = ""; + }; + CAF2EDA9C64B98AF0B5890DB /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + CA00DA7A53FB5B987CA78108 /* imagequant_sys.a (static library) */ = { + isa = PBXNativeTarget; + buildConfigurationList = CA000042D4435B987CA78108 /* Build configuration list for PBXNativeTarget "imagequant_sys.a (static library)" */; + buildPhases = ( + CA007A177A415B987CA78108 /* Sources */, + ); + buildRules = ( + CAF4EDA9C64BAC6C1400ACA8 /* PBXBuildRule */, + ); + dependencies = ( + ); + name = "imagequant_sys.a (static library)"; + productName = libimagequant.a; + productReference = CA00DA7A53FB47C0CE156F07 /* libimagequant.a */; + productType = "com.apple.product-type.library.static"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + CAF3EDA9C64BE04653AD465F /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1510; + TargetAttributes = { + CA00DA7A53FB5B987CA78108 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Automatic; + }; + }; + }; + buildConfigurationList = CAF6EDA9C64B80E02D6C7F57 /* Build configuration list for PBXProject "imagequant" */; + compatibilityVersion = "Xcode 11.4"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = CAF0EDA9C64BD65BC3C892A8; + productRefGroup = CAF1EDA9C64B22869D176AE5 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + CA00DA7A53FB5B987CA78108 /* imagequant_sys.a (static library) */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXSourcesBuildPhase section */ + CA007A177A415B987CA78108 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + CA0074F714595B987CA78108 /* Cargo.toml in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + CA00E0FF0C645B987CA78108 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CARGO_XCODE_CARGO_DEP_FILE_NAME = libimagequant_sys.d; + CARGO_XCODE_CARGO_FILE_NAME = libimagequant_sys.a; + INSTALL_GROUP = ""; + INSTALL_MODE_FLAG = ""; + INSTALL_OWNER = ""; + PRODUCT_NAME = imagequant; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "xrsimulator xros watchsimulator watchos macosx iphonesimulator iphoneos driverkit appletvsimulator appletvos"; + }; + name = Release; + }; + CA00B3274B715B987CA78108 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CARGO_XCODE_CARGO_DEP_FILE_NAME = libimagequant_sys.d; + CARGO_XCODE_CARGO_FILE_NAME = libimagequant_sys.a; + INSTALL_GROUP = ""; + INSTALL_MODE_FLAG = ""; + INSTALL_OWNER = ""; + PRODUCT_NAME = imagequant; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "xrsimulator xros watchsimulator watchos macosx iphonesimulator iphoneos driverkit appletvsimulator appletvos"; + }; + name = Debug; + }; + CAF7A7C4F8A63CC16B37690B /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + "ADDITIONAL_SDKS[sdk=i*]" = macosx; + "ADDITIONAL_SDKS[sdk=w*]" = macosx; + "ADDITIONAL_SDKS[sdk=x*]" = macosx; + "ADDITIONAL_SDKS[sdk=a*]" = macosx; + ALWAYS_SEARCH_USER_PATHS = NO; + CARGO_TARGET_DIR = "$(PROJECT_TEMP_DIR)/cargo_target"; + CARGO_XCODE_BUILD_PROFILE = release; + CARGO_XCODE_FEATURES = ""; + CURRENT_PROJECT_VERSION = 4.1; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + MARKETING_VERSION = 4.1.0; + PRODUCT_NAME = "imagequant-sys"; + RUSTUP_TOOLCHAIN = ""; + SDKROOT = macosx; + SUPPORTS_MACCATALYST = YES; + }; + name = Release; + }; + CAF8A7C4F8A6228BE02872F8 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + "ADDITIONAL_SDKS[sdk=i*]" = macosx; + "ADDITIONAL_SDKS[sdk=w*]" = macosx; + "ADDITIONAL_SDKS[sdk=x*]" = macosx; + "ADDITIONAL_SDKS[sdk=a*]" = macosx; + ALWAYS_SEARCH_USER_PATHS = NO; + CARGO_TARGET_DIR = "$(PROJECT_TEMP_DIR)/cargo_target"; + CARGO_XCODE_BUILD_PROFILE = debug; + CARGO_XCODE_FEATURES = ""; + CURRENT_PROJECT_VERSION = 4.1; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + MARKETING_VERSION = 4.1.0; + ONLY_ACTIVE_ARCH = YES; + PRODUCT_NAME = "imagequant-sys"; + RUSTUP_TOOLCHAIN = ""; + SDKROOT = macosx; + SUPPORTS_MACCATALYST = YES; + }; + name = Debug; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + CA000042D4435B987CA78108 /* Build configuration list for PBXNativeTarget "imagequant_sys.a (static library)" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + CA00E0FF0C645B987CA78108 /* Release */, + CA00B3274B715B987CA78108 /* Debug */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + CAF6EDA9C64B80E02D6C7F57 /* Build configuration list for PBXProject "imagequant" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + CAF7A7C4F8A63CC16B37690B /* Release */, + CAF8A7C4F8A6228BE02872F8 /* Debug */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = CAF3EDA9C64BE04653AD465F /* Project object */; +} diff --git a/libimagequant.cs b/imagequant-sys/libimagequant.cs similarity index 97% rename from libimagequant.cs rename to imagequant-sys/libimagequant.cs index c0807bb..98ee53e 100644 --- a/libimagequant.cs +++ b/imagequant-sys/libimagequant.cs @@ -1,3 +1,9 @@ +/* +This is an example demonstrating use of libimagequant from C#. + +This example code can be freely copied under CC0 (public domain) license. +*/ + using System; using System.Collections.Generic; using System.Runtime.InteropServices; diff --git a/libimagequant.h b/imagequant-sys/libimagequant.h similarity index 89% rename from libimagequant.h rename to imagequant-sys/libimagequant.h index 2631e9b..c31c551 100644 --- a/libimagequant.h +++ b/imagequant-sys/libimagequant.h @@ -13,8 +13,8 @@ #define LIQ_EXPORT extern #endif -#define LIQ_VERSION 21001 -#define LIQ_VERSION_STRING "2.10.1" +#define LIQ_VERSION 40003 +#define LIQ_VERSION_STRING "4.0.3" #ifndef LIQ_PRIVATE #if defined(__GNUC__) || defined (__llvm__) @@ -60,7 +60,11 @@ typedef enum liq_error { LIQ_UNSUPPORTED, } liq_error; -enum liq_ownership {LIQ_OWN_ROWS=4, LIQ_OWN_PIXELS=8}; +enum liq_ownership { + LIQ_OWN_ROWS=4, + LIQ_OWN_PIXELS=8, + LIQ_COPY_PIXELS=16, +}; typedef struct liq_histogram_entry { liq_color color; @@ -68,13 +72,14 @@ typedef struct liq_histogram_entry { } liq_histogram_entry; LIQ_EXPORT LIQ_USERESULT liq_attr* liq_attr_create(void); -LIQ_EXPORT LIQ_USERESULT liq_attr* liq_attr_create_with_allocator(void* (*malloc)(size_t), void (*free)(void*)); +LIQ_EXPORT LIQ_USERESULT liq_attr* liq_attr_create_with_allocator(void* removed, void *unsupported); LIQ_EXPORT LIQ_USERESULT liq_attr* liq_attr_copy(const liq_attr *orig) LIQ_NONNULL; LIQ_EXPORT void liq_attr_destroy(liq_attr *attr) LIQ_NONNULL; LIQ_EXPORT LIQ_USERESULT liq_histogram* liq_histogram_create(const liq_attr* attr); LIQ_EXPORT liq_error liq_histogram_add_image(liq_histogram *hist, const liq_attr *attr, liq_image* image) LIQ_NONNULL; 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; +LIQ_EXPORT liq_error liq_histogram_add_fixed_color(liq_histogram *hist, liq_color color, double gamma) LIQ_NONNULL; LIQ_EXPORT void liq_histogram_destroy(liq_histogram *hist) LIQ_NONNULL; LIQ_EXPORT liq_error liq_set_max_colors(liq_attr* attr, int colors) LIQ_NONNULL; @@ -107,6 +112,8 @@ typedef void liq_image_get_rgba_row_callback(liq_color row_out[], int row, int w 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); LIQ_EXPORT liq_error liq_image_set_memory_ownership(liq_image *image, int ownership_flags) LIQ_NONNULL; +LIQ_EXPORT liq_error liq_image_set_background(liq_image *img, liq_image *background_image) LIQ_NONNULL; +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; LIQ_EXPORT liq_error liq_image_add_fixed_color(liq_image *img, liq_color color) LIQ_NONNULL; LIQ_EXPORT LIQ_USERESULT int liq_image_get_width(const liq_image *img) LIQ_NONNULL; LIQ_EXPORT LIQ_USERESULT int liq_image_get_height(const liq_image *img) LIQ_NONNULL; @@ -114,6 +121,7 @@ LIQ_EXPORT void liq_image_destroy(liq_image *img) LIQ_NONNULL; LIQ_EXPORT LIQ_USERESULT liq_error liq_histogram_quantize(liq_histogram *const input_hist, liq_attr *const options, liq_result **result_output) LIQ_NONNULL; LIQ_EXPORT LIQ_USERESULT liq_error liq_image_quantize(liq_image *const input_image, liq_attr *const options, liq_result **result_output) LIQ_NONNULL; +LIQ_EXPORT LIQ_USERESULT liq_error liq_result_from_palette(const liq_attr *options, const liq_color *palette, unsigned int palette_size, double gamma, liq_result **result_output) LIQ_NONNULL; LIQ_EXPORT liq_error liq_set_dithering_level(liq_result *res, float dither_level) LIQ_NONNULL; LIQ_EXPORT liq_error liq_set_output_gamma(liq_result* res, double gamma) LIQ_NONNULL; diff --git a/org/pngquant/Image.java b/imagequant-sys/org/pngquant/Image.java similarity index 100% rename from org/pngquant/Image.java rename to imagequant-sys/org/pngquant/Image.java diff --git a/org/pngquant/LiqObject.java b/imagequant-sys/org/pngquant/LiqObject.java similarity index 100% rename from org/pngquant/LiqObject.java rename to imagequant-sys/org/pngquant/LiqObject.java diff --git a/org/pngquant/PngQuant.c b/imagequant-sys/org/pngquant/PngQuant.c similarity index 100% rename from org/pngquant/PngQuant.c rename to imagequant-sys/org/pngquant/PngQuant.c diff --git a/org/pngquant/PngQuant.java b/imagequant-sys/org/pngquant/PngQuant.java similarity index 100% rename from org/pngquant/PngQuant.java rename to imagequant-sys/org/pngquant/PngQuant.java diff --git a/org/pngquant/PngQuantException.java b/imagequant-sys/org/pngquant/PngQuantException.java similarity index 100% rename from org/pngquant/PngQuantException.java rename to imagequant-sys/org/pngquant/PngQuantException.java diff --git a/org/pngquant/Result.java b/imagequant-sys/org/pngquant/Result.java similarity index 100% rename from org/pngquant/Result.java rename to imagequant-sys/org/pngquant/Result.java diff --git a/pom.xml b/imagequant-sys/pom.xml similarity index 97% rename from pom.xml rename to imagequant-sys/pom.xml index a1fc73d..8fb84cb 100644 --- a/pom.xml +++ b/imagequant-sys/pom.xml @@ -4,9 +4,9 @@ org.pngquant libimagequant jar - 2.1.1 + 4.1.0 pngquant - http://pngquant.org + https://pngquant.org . diff --git a/imagequant-sys/src/ffi.rs b/imagequant-sys/src/ffi.rs new file mode 100644 index 0000000..d75e4da --- /dev/null +++ b/imagequant-sys/src/ffi.rs @@ -0,0 +1,819 @@ +//! Exports API for C programs and C-FFI-compatible languages. See `libimagequant.h` or for C docs. +//! +//! This crate is not supposed to be used in Rust directly. For Rust, see the parent [imagequant](https://lib.rs/imagequant) crate. +#![cfg_attr(all(not(feature = "std"), feature = "no_std"), no_std)] + +#![allow(non_camel_case_types)] +#![allow(clippy::missing_safety_doc)] +#![allow(clippy::wildcard_imports)] +#![allow(clippy::items_after_statements)] +#![allow(clippy::cast_possible_truncation)] +#![allow(clippy::cast_possible_wrap)] + +#[cfg(all(not(feature = "std"), feature = "no_std"))] +extern crate alloc as std; + +use core::ffi::{c_char, c_int, c_uint, c_void}; +use core::mem::{ManuallyDrop, MaybeUninit}; +use core::{mem, ptr, slice}; +use imagequant::capi::*; +use imagequant::Error::LIQ_OK; +use imagequant::*; +use std::ffi::CString; +use std::boxed::Box; + +pub use imagequant::Error as liq_error; + +#[repr(C)] +pub struct liq_attr { + magic_header: MagicTag, + inner: Attributes, + c_api_free: unsafe extern "C" fn(*mut c_void), +} + +#[repr(C)] +pub struct liq_image<'pixels> { + magic_header: MagicTag, + inner: ManuallyDrop>, + c_api_free: unsafe extern "C" fn(*mut c_void), +} + +#[repr(C)] +pub struct liq_result { + magic_header: MagicTag, + inner: QuantizationResult, +} + +#[repr(C)] +pub struct liq_histogram { + magic_header: MagicTag, + inner: Histogram, +} + +pub type liq_palette = Palette; +pub type liq_histogram_entry = HistogramEntry; +pub type liq_color = RGBA; + +pub type liq_log_callback_function = unsafe extern "C" fn(liq: &liq_attr, message: *const c_char, user_info: AnySyncSendPtr); +pub type liq_log_flush_callback_function = unsafe extern "C" fn(liq: &liq_attr, user_info: AnySyncSendPtr); +pub type liq_progress_callback_function = unsafe extern "C" fn(progress_percent: f32, user_info: AnySyncSendPtr) -> c_int; +pub type liq_image_get_rgba_row_callback = unsafe extern "C" fn(row_out: *mut MaybeUninit, row: c_int, width: c_int, user_info: AnySyncSendPtr); + +bitflags::bitflags! { + #[repr(C)] + #[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Clone, Copy)] + pub struct liq_ownership: c_int { + /// Moves ownership of the rows array. It will free it using `free()` or custom allocator. + const LIQ_OWN_ROWS = 4; + /// Moves ownership of the pixel data. It will free it using `free()` or custom allocator. + const LIQ_OWN_PIXELS = 8; + /// Makes a copy of the pixels, so the `liq_image` is not tied to pixel's lifetime. + const LIQ_COPY_PIXELS = 16; + } +} + +#[repr(transparent)] +#[derive(PartialEq, Debug, Copy, Clone)] +pub(crate) struct MagicTag(*const u8); +// Safety: Rust overreacts about C pointers. Data behind this ptr isn't used. +unsafe impl Sync for MagicTag {} +unsafe impl Send for MagicTag {} + +pub(crate) static LIQ_ATTR_MAGIC: MagicTag = MagicTag(b"liq_attr_magic\0".as_ptr()); +pub(crate) static LIQ_IMAGE_MAGIC: MagicTag = MagicTag(b"liq_image_magic\0".as_ptr()); +pub(crate) static LIQ_RESULT_MAGIC: MagicTag = MagicTag(b"liq_result_magic\0".as_ptr()); +pub(crate) static LIQ_HISTOGRAM_MAGIC: MagicTag = MagicTag(b"liq_histogram_magic\0".as_ptr()); +pub(crate) static LIQ_FREED_MAGIC: MagicTag = MagicTag(b"liq_freed_magic\0".as_ptr()); + +#[no_mangle] +#[inline(never)] +unsafe extern "C" fn liq_received_invalid_pointer(ptr: *const u8) -> bool { + if ptr.is_null() { + return true; + } + let _ = ptr::read_volatile(ptr); + false +} + +macro_rules! bad_object { + ($obj:expr, $tag:expr) => {{ + let obj = &*$obj; + #[allow(unused_unsafe)] + #[allow(clippy::ptr_as_ptr)] + let bork = if cfg!(miri) { false } else { unsafe { liq_received_invalid_pointer((obj as *const _ as *const u8)) } }; + (bork || (($obj).magic_header != $tag)) + }}; +} + +impl Drop for liq_attr { + fn drop(&mut self) { + if bad_object!(self, LIQ_ATTR_MAGIC) { return; } + self.magic_header = LIQ_FREED_MAGIC; + } +} +impl Drop for liq_image<'_> { + fn drop(&mut self) { + if bad_object!(self, LIQ_IMAGE_MAGIC) { return; } + unsafe { ManuallyDrop::drop(&mut self.inner); } + self.magic_header = LIQ_FREED_MAGIC; + } +} +impl Drop for liq_result { + fn drop(&mut self) { + if bad_object!(self, LIQ_RESULT_MAGIC) { return; } + self.magic_header = LIQ_FREED_MAGIC; + } +} +impl Drop for liq_histogram { + fn drop(&mut self) { + if bad_object!(self, LIQ_HISTOGRAM_MAGIC) { return; } + self.magic_header = LIQ_FREED_MAGIC; + } +} + +#[no_mangle] +#[inline(never)] +pub extern "C" fn liq_version() -> c_uint { + LIQ_VERSION +} + +#[no_mangle] +#[inline(never)] +#[deprecated] +pub extern "C" fn liq_set_min_opacity(_: &mut liq_attr, _: c_int) -> liq_error { + LIQ_OK +} + +#[no_mangle] +#[inline(never)] +#[deprecated] +pub extern "C" fn liq_get_min_opacity(_: &liq_attr) -> c_int { + 0 +} + +#[no_mangle] +#[inline(never)] +pub extern "C" fn liq_set_last_index_transparent(attr: &mut liq_attr, is_last: c_int) { + if bad_object!(attr, LIQ_ATTR_MAGIC) { return; } + attr.inner.set_last_index_transparent(is_last != 0); +} + +#[no_mangle] +#[inline(never)] +pub extern "C" fn liq_get_palette(result: &mut liq_result) -> Option<&liq_palette> { + if bad_object!(result, LIQ_RESULT_MAGIC) { return None; } + Some(liq_get_palette_impl(&mut result.inner)) +} + +/// A `void*` pointer to any data, as long as it's thread-safe +#[repr(transparent)] +#[derive(Clone, Copy)] +pub struct AnySyncSendPtr(pub *mut c_void); + +impl Default for AnySyncSendPtr { + fn default() -> Self { + Self(ptr::null_mut()) + } +} + +/// C callback user is responsible for ensuring safety +unsafe impl Send for AnySyncSendPtr {} +unsafe impl Sync for AnySyncSendPtr {} + +#[no_mangle] +#[inline(never)] +pub unsafe extern "C" fn liq_attr_set_progress_callback(attr: &mut liq_attr, callback: liq_progress_callback_function, user_info: AnySyncSendPtr) { + if bad_object!(attr, LIQ_ATTR_MAGIC) { return; } + let cb = move |f| if callback(f, user_info) == 0 { ControlFlow::Break} else { ControlFlow::Continue}; + attr.inner.set_progress_callback(cb); +} + +#[no_mangle] +#[inline(never)] +pub unsafe extern "C" fn liq_result_set_progress_callback(result: &mut liq_result, callback: liq_progress_callback_function, user_info: AnySyncSendPtr) { + if bad_object!(result, LIQ_RESULT_MAGIC) { return; } + result.inner.set_progress_callback(move |f| if callback(f, user_info) == 0 { ControlFlow::Break} else { ControlFlow::Continue}); +} + +#[allow(clippy::cast_ptr_alignment)] +unsafe fn attr_to_liq_attr_ptr(ptr: &Attributes) -> &liq_attr { + let liq_attr = ptr::NonNull::::dangling(); + let outer_addr = ptr::addr_of!(*liq_attr.as_ptr()) as isize; + let inner_addr = ptr::addr_of!((*liq_attr.as_ptr()).inner) as isize; + + &*(ptr as *const Attributes).cast::().offset(outer_addr - inner_addr).cast::() +} + +#[no_mangle] +#[inline(never)] +pub unsafe extern "C" fn liq_set_log_callback(attr: &mut liq_attr, callback: liq_log_callback_function, user_info: AnySyncSendPtr) { + if bad_object!(attr, LIQ_ATTR_MAGIC) { return; } + attr.inner.set_log_callback(move |attr, msg| { + if let Ok(tmp) = CString::new(msg) { + callback(attr_to_liq_attr_ptr(attr), tmp.as_ptr(), user_info); + } + }); +} + +#[no_mangle] +#[inline(never)] +pub unsafe extern "C" fn liq_set_log_flush_callback(attr: &mut liq_attr, callback: liq_log_flush_callback_function, user_info: AnySyncSendPtr) { + if bad_object!(attr, LIQ_ATTR_MAGIC) { return; } + attr.inner.set_log_flush_callback(move |attr| callback(attr_to_liq_attr_ptr(attr), user_info)); +} + +#[no_mangle] +#[inline(never)] +pub extern "C" fn liq_set_max_colors(attr: &mut liq_attr, colors: c_uint) -> liq_error { + if bad_object!(attr, LIQ_ATTR_MAGIC) { return Error::InvalidPointer; } + attr.inner.set_max_colors(colors).err().unwrap_or(LIQ_OK) +} + +#[no_mangle] +#[inline(never)] +pub extern "C" fn liq_get_max_colors(attr: &liq_attr) -> c_uint { + if bad_object!(attr, LIQ_ATTR_MAGIC) { return !0; } + attr.inner.max_colors() +} + +#[no_mangle] +#[inline(never)] +pub extern "C" fn liq_set_min_posterization(attr: &mut liq_attr, bits: c_int) -> liq_error { + if bad_object!(attr, LIQ_ATTR_MAGIC) { return Error::InvalidPointer; } + attr.inner.set_min_posterization(bits as u8).err().unwrap_or(LIQ_OK) +} + +#[no_mangle] +#[inline(never)] +pub extern "C" fn liq_get_min_posterization(attr: &liq_attr) -> c_uint { + if bad_object!(attr, LIQ_ATTR_MAGIC) { return !0; } + attr.inner.min_posterization().into() +} + +#[no_mangle] +#[inline(never)] +pub extern "C" fn liq_set_speed(attr: &mut liq_attr, speed: c_int) -> liq_error { + attr.inner.set_speed(speed).err().unwrap_or(LIQ_OK) +} + +#[no_mangle] +#[inline(never)] +pub extern "C" fn liq_get_speed(attr: &liq_attr) -> c_uint { + if bad_object!(attr, LIQ_ATTR_MAGIC) { return !0; } + attr.inner.speed() +} + +#[no_mangle] +#[inline(never)] +pub extern "C" fn liq_set_quality(attr: &mut liq_attr, minimum: c_uint, target: c_uint) -> liq_error { + if bad_object!(attr, LIQ_ATTR_MAGIC) { return Error::InvalidPointer; } + attr.inner.set_quality(minimum as u8, target as u8).err().unwrap_or(LIQ_OK) +} + +#[no_mangle] +#[inline(never)] +pub extern "C" fn liq_get_min_quality(attr: &liq_attr) -> c_uint { + if bad_object!(attr, LIQ_ATTR_MAGIC) { return !0; } + attr.inner.quality().0.into() +} + +#[no_mangle] +#[inline(never)] +pub extern "C" fn liq_get_max_quality(attr: &liq_attr) -> c_uint { + if bad_object!(attr, LIQ_ATTR_MAGIC) { return !0; } + attr.inner.quality().1.into() +} + +#[no_mangle] +#[inline(never)] +pub extern "C" fn liq_quantize_image(attr: &mut liq_attr, img: &mut liq_image) -> Option> { + if bad_object!(attr, LIQ_ATTR_MAGIC) || + bad_object!(img, LIQ_IMAGE_MAGIC) { return None; } + let img = &mut img.inner; + let attr = &mut attr.inner; + + attr.quantize(img).ok().map(|inner| Box::new(liq_result { + magic_header: LIQ_RESULT_MAGIC, + inner, + })) +} + +#[no_mangle] +#[inline(never)] +pub unsafe extern "C" fn liq_write_remapped_image(result: &mut liq_result, input_image: &mut liq_image, buffer_bytes: *mut MaybeUninit, buffer_size: usize) -> liq_error { + if bad_object!(result, LIQ_RESULT_MAGIC) || + bad_object!(input_image, LIQ_IMAGE_MAGIC) { return Error::InvalidPointer; } + let input_image = &mut input_image.inner; + let result = &mut result.inner; + + if liq_received_invalid_pointer(buffer_bytes.cast()) { return Error::InvalidPointer; } + + let required_size = (input_image.width()) * (input_image.height()); + if buffer_size < required_size { return Error::BufferTooSmall; } + let buffer_bytes = slice::from_raw_parts_mut(buffer_bytes, required_size); + liq_write_remapped_image_impl(result, input_image, buffer_bytes).err().unwrap_or(LIQ_OK) +} + +#[no_mangle] +#[inline(never)] +pub unsafe extern "C" fn liq_write_remapped_image_rows(result: &mut liq_result, input_image: &mut liq_image, row_pointers: *mut *mut MaybeUninit) -> liq_error { + if bad_object!(result, LIQ_RESULT_MAGIC) || + bad_object!(input_image, LIQ_IMAGE_MAGIC) { return Error::InvalidPointer; } + let input_image = &mut input_image.inner; + let result = &mut result.inner; + + if liq_received_invalid_pointer(row_pointers.cast()) { return Error::InvalidPointer; } + let rows = slice::from_raw_parts_mut(row_pointers, input_image.height()); + + liq_write_remapped_image_rows_impl(result, input_image, rows).err().unwrap_or(LIQ_OK) +} + +#[no_mangle] +#[inline(never)] +pub extern "C" fn liq_image_add_fixed_color(img: &mut liq_image, color: liq_color) -> liq_error { + if bad_object!(img, LIQ_IMAGE_MAGIC) { return Error::InvalidPointer; } + img.inner.add_fixed_color(color).err().unwrap_or(LIQ_OK) +} + +#[no_mangle] +#[inline(never)] +pub extern "C" fn liq_histogram_add_fixed_color(hist: &mut liq_histogram, color: liq_color, gamma: f64) -> liq_error { + if bad_object!(hist, LIQ_HISTOGRAM_MAGIC) { return Error::InvalidPointer; } + let hist = &mut hist.inner; + + hist.add_fixed_color(color, gamma).err().unwrap_or(LIQ_OK) +} + +#[no_mangle] +#[inline(never)] +pub extern "C" fn liq_image_get_width(img: &liq_image) -> c_uint { + if bad_object!(img, LIQ_IMAGE_MAGIC) { return !0; } + img.inner.width() as _ +} + +#[no_mangle] +#[inline(never)] +pub extern "C" fn liq_image_get_height(img: &liq_image) -> c_uint { + if bad_object!(img, LIQ_IMAGE_MAGIC) { return !0; } + img.inner.height() as _ +} + +#[no_mangle] +#[inline(never)] +pub extern "C" fn liq_image_destroy(_: Option>) {} + +#[no_mangle] +#[inline(never)] +pub extern "C" fn liq_image_set_background<'pixels>(img: &mut liq_image<'pixels>, background: Box>) -> liq_error { + if bad_object!(img, LIQ_IMAGE_MAGIC) || + bad_object!(background, LIQ_IMAGE_MAGIC) { return Error::InvalidPointer; } + let background = unsafe { ManuallyDrop::take(&mut ManuallyDrop::new(background).inner) }; + img.inner.set_background(background).err().unwrap_or(LIQ_OK) +} + +#[no_mangle] +#[inline(never)] +pub unsafe extern "C" fn liq_image_set_importance_map(img: &mut liq_image, importance_map: *mut u8, buffer_size: usize, ownership: liq_ownership) -> liq_error { + if bad_object!(img, LIQ_IMAGE_MAGIC) { return Error::InvalidPointer; } + let free_fn = img.c_api_free; + let img = &mut img.inner; + + if buffer_size == 0 || liq_received_invalid_pointer(importance_map) { return Error::InvalidPointer; } + let required_size = img.width() * img.height(); + if buffer_size < required_size { + return Error::BufferTooSmall; + } + + let importance_map_slice = slice::from_raw_parts(importance_map, required_size); + if ownership == liq_ownership::LIQ_COPY_PIXELS { + img.set_importance_map(importance_map_slice).err().unwrap_or(LIQ_OK) + } else if ownership == liq_ownership::LIQ_OWN_PIXELS { + let copy: Box<[u8]> = importance_map_slice.into(); + free_fn(importance_map.cast()); + img.set_importance_map(copy).err().unwrap_or(LIQ_OK); + LIQ_OK + } else { + Error::Unsupported + } +} + +#[no_mangle] +#[inline(never)] +pub unsafe extern "C" fn liq_image_set_memory_ownership(img: &mut liq_image, ownership_flags: liq_ownership) -> liq_error { + if bad_object!(img, LIQ_IMAGE_MAGIC) { return Error::InvalidPointer; } + + let both = liq_ownership::LIQ_OWN_ROWS | liq_ownership::LIQ_OWN_PIXELS; + + if ownership_flags.is_empty() || (ownership_flags | both) != both { + return Error::ValueOutOfRange; + } + + let own_rows = ownership_flags.contains(liq_ownership::LIQ_OWN_ROWS); + let own_pixels = ownership_flags.contains(liq_ownership::LIQ_OWN_PIXELS); + liq_image_set_memory_ownership_impl(&mut img.inner, own_rows, own_pixels, img.c_api_free).err().unwrap_or(LIQ_OK) +} + +#[no_mangle] +#[inline(never)] +pub extern "C" fn liq_histogram_create(attr: &liq_attr) -> Option> { + if bad_object!(attr, LIQ_ATTR_MAGIC) { return None; } + Some(Box::new(liq_histogram { + magic_header: LIQ_HISTOGRAM_MAGIC, + inner: Histogram::new(&attr.inner), + })) +} + +#[no_mangle] +#[inline(never)] +pub extern "C" fn liq_histogram_destroy(_hist: Option>) {} + +#[no_mangle] +#[inline(never)] +#[deprecated(note = "custom allocators are no longer supported")] +pub extern "C" fn liq_attr_create_with_allocator(_unused: *mut c_void, free: unsafe extern "C" fn(*mut c_void)) -> Option> { + let attr = Box::new(liq_attr { + magic_header: LIQ_ATTR_MAGIC, + inner: Attributes::new(), + c_api_free: free, + }); + debug_assert_eq!(ptr::addr_of!(*attr), unsafe { attr_to_liq_attr_ptr(&attr.inner) } as *const liq_attr); + Some(attr) +} + +#[no_mangle] +#[inline(never)] +#[allow(deprecated)] +#[cfg_attr(all(feature = "std", feature = "no_std"), deprecated(note = "Cargo features configuration issue: both std and no_std features are enabled in imagequant-sys\nYou must disable default features to use no_std."))] +pub extern "C" fn liq_attr_create() -> Option> { + liq_attr_create_with_allocator(ptr::null_mut(), libc::free) +} + +#[no_mangle] +#[inline(never)] +pub extern "C" fn liq_attr_copy(attr: &liq_attr) -> Option> { + if bad_object!(attr, LIQ_ATTR_MAGIC) { return None; } + Some(Box::new(liq_attr { + magic_header: LIQ_ATTR_MAGIC, + inner: attr.inner.clone(), + c_api_free: attr.c_api_free, + })) +} + +#[no_mangle] +#[inline(never)] +pub extern "C" fn liq_attr_destroy(_attr: Option>) {} + +#[no_mangle] +#[inline(never)] +pub extern "C" fn liq_result_destroy(_res: Option>) {} + +#[no_mangle] +#[inline(never)] +pub extern "C" fn liq_set_output_gamma(result: &mut liq_result, gamma: f64) -> liq_error { + if bad_object!(result, LIQ_RESULT_MAGIC) { return Error::InvalidPointer; } + result.inner.set_output_gamma(gamma).err().unwrap_or(LIQ_OK) +} + +#[no_mangle] +#[inline(never)] +pub extern "C" fn liq_set_dithering_level(result: &mut liq_result, dither_level: f32) -> liq_error { + if bad_object!(result, LIQ_RESULT_MAGIC) { return Error::InvalidPointer; } + result.inner.set_dithering_level(dither_level).err().unwrap_or(LIQ_OK) +} + +#[no_mangle] +#[inline(never)] +pub extern "C" fn liq_get_output_gamma(result: &liq_result) -> f64 { + if bad_object!(result, LIQ_RESULT_MAGIC) { return -1.; } + result.inner.output_gamma() +} + +#[no_mangle] +#[inline(never)] +pub extern "C" fn liq_get_quantization_error(result: &liq_result) -> f64 { + if bad_object!(result, LIQ_RESULT_MAGIC) { return -1.; } + result.inner.quantization_error().unwrap_or(-1.) +} + +#[no_mangle] +#[inline(never)] +pub extern "C" fn liq_get_remapping_error(result: &liq_result) -> f64 { + if bad_object!(result, LIQ_RESULT_MAGIC) { return -1.; } + result.inner.remapping_error().unwrap_or(-1.) +} + +#[no_mangle] +#[inline(never)] +pub extern "C" fn liq_get_quantization_quality(result: &liq_result) -> c_int { + if bad_object!(result, LIQ_RESULT_MAGIC) { return -1; } + result.inner.quantization_quality().map_or(-1, c_int::from) +} + +#[no_mangle] +#[inline(never)] +pub extern "C" fn liq_get_remapping_quality(result: &liq_result) -> c_int { + if bad_object!(result, LIQ_RESULT_MAGIC) { return -1; } + result.inner.remapping_quality().map_or(-1, c_int::from) +} + +#[no_mangle] +#[inline(never)] +pub extern "C" fn liq_image_quantize(img: &mut liq_image, attr: &mut liq_attr, write_only_output: &mut MaybeUninit>>) -> liq_error { + if bad_object!(attr, LIQ_ATTR_MAGIC) || + bad_object!(img, LIQ_IMAGE_MAGIC) { return Error::InvalidPointer; } + let attr = &mut attr.inner; + let img = &mut img.inner; + + let res = attr.quantize(img) + .map(|inner| liq_result { + magic_header: LIQ_RESULT_MAGIC, + inner, + }); + store_boxed_result(res, write_only_output) +} + +#[no_mangle] +#[inline(never)] +pub extern "C" fn liq_histogram_quantize(hist: &mut liq_histogram, attr: &liq_attr, write_only_output: &mut MaybeUninit>>) -> liq_error { + if bad_object!(attr, LIQ_ATTR_MAGIC) || + bad_object!(hist, LIQ_HISTOGRAM_MAGIC) { return Error::InvalidPointer; } + let attr = &attr.inner; + let hist = &mut hist.inner; + + let res = hist.quantize(attr) + .map(|inner| liq_result { + magic_header: LIQ_RESULT_MAGIC, + inner, + }); + store_boxed_result(res, write_only_output) +} + +#[no_mangle] +#[inline(never)] +pub unsafe extern "C" fn liq_result_from_palette( + attr: &liq_attr, + palette: *const RGBA, + palette_size: c_uint, + gamma: f64, + write_only_output: &mut MaybeUninit>>, +) -> liq_error { + if bad_object!(attr, LIQ_ATTR_MAGIC) { + return Error::InvalidPointer; + } + let Ok(palette_size) = palette_size.try_into() else { + return Error::ValueOutOfRange; + }; + if liq_received_invalid_pointer(palette.cast()) { + return Error::InvalidPointer; + } + + let attr = &attr.inner; + let palette = slice::from_raw_parts(palette, palette_size); + + let res = QuantizationResult::from_palette(attr, palette, gamma).map(|inner| liq_result { + magic_header: LIQ_RESULT_MAGIC, + inner, + }); + store_boxed_result(res, write_only_output) +} + +#[inline] +fn store_boxed_result(res: Result, out: &mut MaybeUninit>>) -> liq_error { + match res { + Ok(res) => { out.write(Some(Box::new(res))); LIQ_OK }, + Err(err) => { out.write(None); err }, + } +} + +pub(crate) fn check_image_size(attr: &liq_attr, width: u32, height: u32) -> bool { + if bad_object!(attr, LIQ_ATTR_MAGIC) { return false; } + + if width == 0 || height == 0 { + return false; + } + + if width as usize > c_int::MAX as usize / mem::size_of::() / height as usize || + width as usize > c_int::MAX as usize / 16 / mem::size_of::() || + height as usize > c_int::MAX as usize / mem::size_of::() + { + return false; + } + true +} + +#[no_mangle] +#[inline(never)] +pub unsafe extern "C" fn liq_image_create_custom(attr: &liq_attr, row_callback: liq_image_get_rgba_row_callback, user_info: AnySyncSendPtr, width: c_uint, height: c_uint, gamma: f64) + -> Option>> { + let cb: Box], usize) + Send + Sync> = Box::new(move |row, y| row_callback(row.as_mut_ptr(), y as _, row.len() as _, user_info)); + liq_image_create_custom_impl(&attr.inner, cb, width as _, height as _, gamma) + .map(move |inner| Box::new(liq_image { + magic_header: LIQ_IMAGE_MAGIC, + inner: ManuallyDrop::new(inner), + c_api_free: attr.c_api_free, + })) +} + +#[no_mangle] +#[inline(never)] +pub unsafe extern "C" fn liq_image_create_rgba_rows<'rows>(attr: &liq_attr, rows: *const *const RGBA, width: c_uint, height: c_uint, gamma: f64) -> Option>> { + if !check_image_size(attr, width, height) { return None; } + if rows.is_null() { return None; } + let rows = slice::from_raw_parts(rows, height as _); + liq_image_create_rgba_rows_impl(&attr.inner, rows, width as _, height as _, gamma) + .map(move |inner| Box::new(liq_image { + magic_header: LIQ_IMAGE_MAGIC, + inner: ManuallyDrop::new(inner), + c_api_free: attr.c_api_free, + })) +} + +#[no_mangle] +#[inline(never)] +pub unsafe extern "C" fn liq_image_create_rgba<'pixels>(attr: &liq_attr, pixels: *const liq_color, width: c_uint, height: c_uint, gamma: f64) -> Option>> { + if liq_received_invalid_pointer(pixels.cast()) { return None; } + if !check_image_size(attr, width, height) { return None; } + let rows = (0..height as usize).map(move |i| pixels.add(width as usize * i)).collect(); + liq_image_create_rgba_bitmap_impl(&attr.inner, rows, width as _, height as _, gamma) + .map(move |inner| Box::new(liq_image { + magic_header: LIQ_IMAGE_MAGIC, + inner: ManuallyDrop::new(inner), + c_api_free: attr.c_api_free, + })) +} + +#[no_mangle] +#[inline(never)] +pub unsafe extern "C" fn liq_histogram_add_colors(input_hist: &mut liq_histogram, attr: &liq_attr, entries: *const HistogramEntry, num_entries: c_int, gamma: f64) -> liq_error { + if bad_object!(attr, LIQ_ATTR_MAGIC) || + bad_object!(input_hist, LIQ_HISTOGRAM_MAGIC) { return Error::InvalidPointer; } + let input_hist = &mut input_hist.inner; + if num_entries == 0 { + return LIQ_OK; + } + + let Ok(num_entries) = num_entries.try_into() else { + return Error::ValueOutOfRange; + }; + + if liq_received_invalid_pointer(entries.cast()) { return Error::InvalidPointer; } + + let entries = slice::from_raw_parts(entries, num_entries); + + input_hist.add_colors(entries, gamma).err().unwrap_or(LIQ_OK) +} + +#[no_mangle] +#[inline(never)] +pub extern "C" fn liq_histogram_add_image(input_hist: &mut liq_histogram, attr: &liq_attr, input_image: &mut liq_image) -> liq_error { + if bad_object!(attr, LIQ_ATTR_MAGIC) || + bad_object!(input_hist, LIQ_HISTOGRAM_MAGIC) || + bad_object!(input_image, LIQ_IMAGE_MAGIC) { return Error::InvalidPointer; } + let attr = &attr.inner; + let input_hist = &mut input_hist.inner; + let input_image = &mut input_image.inner; + + input_hist.add_image(attr, input_image).err().unwrap_or(LIQ_OK) +} + +/// This is just to exist in backtraces of crashes that aren't mine +#[no_mangle] +#[inline(never)] +pub unsafe extern "Rust" fn liq_executing_user_callback(callback: liq_image_get_rgba_row_callback, temp_row: &mut [MaybeUninit], row: usize, user_info: AnySyncSendPtr) { + callback(temp_row.as_mut_ptr(), row as _, temp_row.len() as _, user_info); +} + +#[test] +fn links_and_runs() { + unsafe { + assert!(liq_version() >= 40000); + let attr = liq_attr_create().unwrap(); + let mut hist = liq_histogram_create(&attr).unwrap(); + assert_eq!(LIQ_OK, liq_histogram_add_fixed_color(&mut hist, liq_color {r: 0, g: 0, b: 0, a: 0}, 0.)); + liq_histogram_add_colors(&mut hist, &attr, ptr::null(), 0, 0.); + + let mut res = MaybeUninit::uninit(); + + // this is fine, because there is 1 fixed color to generate + assert_eq!(LIQ_OK, liq_histogram_quantize(&mut hist, &attr, &mut res)); + let res = res.assume_init().unwrap(); + + liq_result_destroy(Some(res)); + liq_histogram_destroy(Some(hist)); + liq_attr_destroy(Some(attr)); + } +} + +#[test] +#[allow(deprecated)] +fn link_every_symbol() { + + let x = liq_attr_create as *const c_void as usize + + liq_attr_create_with_allocator as *const c_void as usize + + liq_attr_copy as *const c_void as usize + + liq_attr_destroy as *const c_void as usize + + liq_set_max_colors as *const c_void as usize + + liq_get_max_colors as *const c_void as usize + + liq_set_speed as *const c_void as usize + + liq_get_speed as *const c_void as usize + + liq_set_min_posterization as *const c_void as usize + + liq_get_min_posterization as *const c_void as usize + + liq_set_quality as *const c_void as usize + + liq_get_min_quality as *const c_void as usize + + liq_get_max_quality as *const c_void as usize + + liq_set_last_index_transparent as *const c_void as usize + + liq_image_create_rgba_rows as *const c_void as usize + + liq_image_create_rgba as *const c_void as usize + + liq_image_set_memory_ownership as *const c_void as usize + + liq_set_log_callback as *const c_void as usize + + liq_set_log_flush_callback as *const c_void as usize + + liq_attr_set_progress_callback as *const c_void as usize + + liq_result_set_progress_callback as *const c_void as usize + + liq_image_create_custom as *const c_void as usize + + liq_image_set_background as *const c_void as usize + + liq_image_set_importance_map as *const c_void as usize + + liq_image_add_fixed_color as *const c_void as usize + + liq_image_get_width as *const c_void as usize + + liq_image_get_height as *const c_void as usize + + liq_image_destroy as *const c_void as usize + + liq_histogram_create as *const c_void as usize + + liq_histogram_add_image as *const c_void as usize + + liq_histogram_add_colors as *const c_void as usize + + liq_histogram_add_fixed_color as *const c_void as usize + + liq_histogram_destroy as *const c_void as usize + + liq_quantize_image as *const c_void as usize + + liq_histogram_quantize as *const c_void as usize + + liq_image_quantize as *const c_void as usize + + liq_result_from_palette as *const c_void as usize + + liq_set_dithering_level as *const c_void as usize + + liq_set_output_gamma as *const c_void as usize + + liq_get_output_gamma as *const c_void as usize + + liq_get_palette as *const c_void as usize + + liq_write_remapped_image as *const c_void as usize + + liq_write_remapped_image_rows as *const c_void as usize + + liq_get_quantization_error as *const c_void as usize + + liq_get_quantization_quality as *const c_void as usize + + liq_result_destroy as *const c_void as usize + + liq_get_remapping_error as *const c_void as usize + + liq_get_remapping_quality as *const c_void as usize + + liq_version as *const c_void as usize; + assert_ne!(!0, x); +} + +#[test] +fn c_callback_test_c() { + let mut called = 0; + let mut res = unsafe { + let mut a = liq_attr_create().unwrap(); + unsafe extern "C" fn get_row(output_row: *mut MaybeUninit, y: c_int, width: c_int, user_data: AnySyncSendPtr) { + assert!((0..5).contains(&y)); + assert_eq!(123, width); + for i in 0..width as isize { + let n = i as u8; + (*output_row.offset(i)).write(RGBA::new(n, n, n, n)); + } + let user_data = user_data.0.cast::(); + *user_data += 1; + } + let mut img = liq_image_create_custom(&a, get_row, AnySyncSendPtr(ptr::addr_of_mut!(called).cast::()), 123, 5, 0.).unwrap(); + liq_quantize_image(&mut a, &mut img).unwrap() + }; + assert!(called > 5 && called < 50); + let pal = liq_get_palette(&mut res).unwrap(); + assert_eq!(123, pal.count); +} + +#[test] +fn ownership_bitflags() { + assert_eq!(4 + 16, (liq_ownership::LIQ_OWN_ROWS | liq_ownership::LIQ_COPY_PIXELS).bits()); +} + +#[cfg(all(feature = "no_std_global_handlers", not(feature = "std"), feature = "no_std"))] +#[cfg(not(test))] +mod no_std_global_handlers { + use std::alloc::{GlobalAlloc, Layout}; + + #[cfg(panic = "unwind")] + compile_error!("no_std imagequant-sys must be compiled with panic=abort\n\nset env var CARGO_PROFILE_DEV_PANIC=abort and CARGO_PROFILE_RELEASE_PANIC=abort\nor modify your Cargo.toml to add `panic=\"abort\"` to `[profile.release]` and `[profile.dev]`"); + + #[panic_handler] + fn panic(_info: &core::panic::PanicInfo) -> ! { + unsafe { libc::abort(); } + } + + #[global_allocator] + static GLOBAL_ALLOCATOR: Mallocator = Mallocator; + + #[derive(Default)] + pub struct Mallocator; + + unsafe impl GlobalAlloc for Mallocator { + unsafe fn alloc(&self, layout: Layout) -> *mut u8 { + libc::malloc(layout.size() as _).cast() + } + + unsafe fn dealloc(&self, ptr: *mut u8, _layout: Layout) { + libc::free(ptr.cast()); + } + } +} diff --git a/kmeans.c b/kmeans.c deleted file mode 100644 index f596009..0000000 --- a/kmeans.c +++ /dev/null @@ -1,107 +0,0 @@ -/* -© 2011-2016 by Kornel Lesiński. - -This file is part of libimagequant. - -libimagequant is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -libimagequant is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with libimagequant. If not, see . -*/ - -#include "libimagequant.h" -#include "pam.h" -#include "kmeans.h" -#include "nearest.h" -#include -#include - -#ifdef _OPENMP -#include -#else -#define omp_get_max_threads() 1 -#define omp_get_thread_num() 0 -#endif - -/* - * K-Means iteration: new palette color is computed from weighted average of colors that map to that palette entry. - */ -LIQ_PRIVATE void kmeans_init(const colormap *map, const unsigned int max_threads, kmeans_state average_color[]) -{ - memset(average_color, 0, sizeof(average_color[0])*(KMEANS_CACHE_LINE_GAP+map->colors)*max_threads); -} - -LIQ_PRIVATE void kmeans_update_color(const f_pixel acolor, const float value, const colormap *map, unsigned int match, const unsigned int thread, kmeans_state average_color[]) -{ - match += thread * (KMEANS_CACHE_LINE_GAP+map->colors); - average_color[match].a += acolor.a * value; - average_color[match].r += acolor.r * value; - average_color[match].g += acolor.g * value; - average_color[match].b += acolor.b * value; - average_color[match].total += value; -} - -LIQ_PRIVATE void kmeans_finalize(colormap *map, const unsigned int max_threads, const kmeans_state average_color[]) -{ - for (unsigned int i=0; i < map->colors; i++) { - double a=0, r=0, g=0, b=0, total=0; - - // Aggregate results from all threads - for(unsigned int t=0; t < max_threads; t++) { - const unsigned int offset = (KMEANS_CACHE_LINE_GAP+map->colors) * t + i; - - a += average_color[offset].a; - r += average_color[offset].r; - g += average_color[offset].g; - b += average_color[offset].b; - total += average_color[offset].total; - } - - if (total && !map->palette[i].fixed) { - map->palette[i].acolor = (f_pixel){ - .a = a / total, - .r = r / total, - .g = g / total, - .b = b / total, - }; - map->palette[i].popularity = total; - } - } -} - -LIQ_PRIVATE double kmeans_do_iteration(histogram *hist, colormap *const map, kmeans_callback callback) -{ - const unsigned int max_threads = omp_get_max_threads(); - kmeans_state average_color[(KMEANS_CACHE_LINE_GAP+map->colors) * max_threads]; - kmeans_init(map, max_threads, average_color); - struct nearest_map *const n = nearest_init(map); - hist_item *const achv = hist->achv; - const int hist_size = hist->size; - - double total_diff=0; - #pragma omp parallel for if (hist_size > 3000) \ - schedule(static) default(none) shared(average_color,callback) reduction(+:total_diff) - for(int j=0; j < hist_size; j++) { - float diff; - unsigned int match = nearest_search(n, &achv[j].acolor, achv[j].tmp.likely_colormap_index, &diff); - achv[j].tmp.likely_colormap_index = match; - total_diff += diff * achv[j].perceptual_weight; - - kmeans_update_color(achv[j].acolor, achv[j].perceptual_weight, map, match, omp_get_thread_num(), average_color); - - if (callback) callback(&achv[j], diff); - } - - nearest_free(n); - kmeans_finalize(map, max_threads, average_color); - - return total_diff / hist->total_perceptual_weight; -} diff --git a/kmeans.h b/kmeans.h deleted file mode 100644 index c51d7bb..0000000 --- a/kmeans.h +++ /dev/null @@ -1,19 +0,0 @@ - -#ifndef KMEANS_H -#define KMEANS_H - -// Spread memory touched by different threads at least 64B apart which I assume is the cache line size. This should avoid memory write contention. -#define KMEANS_CACHE_LINE_GAP ((64+sizeof(kmeans_state)-1)/sizeof(kmeans_state)) - -typedef struct { - double a, r, g, b, total; -} kmeans_state; - -typedef void (*kmeans_callback)(hist_item *item, float diff); - -LIQ_PRIVATE void kmeans_init(const colormap *map, const unsigned int max_threads, kmeans_state state[]); -LIQ_PRIVATE void kmeans_update_color(const f_pixel acolor, const float value, const colormap *map, unsigned int match, const unsigned int thread, kmeans_state average_color[]); -LIQ_PRIVATE void kmeans_finalize(colormap *map, const unsigned int max_threads, const kmeans_state state[]); -LIQ_PRIVATE double kmeans_do_iteration(histogram *hist, colormap *const map, kmeans_callback callback); - -#endif diff --git a/libimagequant.c b/libimagequant.c deleted file mode 100644 index 0e31fd1..0000000 --- a/libimagequant.c +++ /dev/null @@ -1,2031 +0,0 @@ -/* -** © 2009-2016 by Kornel Lesiński. -** -** This file is part of libimagequant. -** -** libimagequant is free software: you can redistribute it and/or modify -** it under the terms of the GNU General Public License as published by -** the Free Software Foundation, either version 3 of the License, or -** (at your option) any later version. -** -** libimagequant is distributed in the hope that it will be useful, -** but WITHOUT ANY WARRANTY; without even the implied warranty of -** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -** GNU General Public License for more details. -** -** You should have received a copy of the GNU General Public License -** along with libimagequant. If not, see . -*/ -/* Copyright (C) 1989, 1991 by Jef Poskanzer. -** Copyright (C) 1997, 2000, 2002 by Greg Roelofs; based on an idea by -** Stefan Schneider. -** -** Permission to use, copy, modify, and distribute this software and its -** documentation for any purpose and without fee is hereby granted, provided -** that the above copyright notice appear in all copies and that both that -** copyright notice and this permission notice appear in supporting -** documentation. This software is provided "as is" without express or -** implied warranty. -*/ - -#include -#include -#include -#include -#include -#include -#include - -#if !(defined(__STDC_VERSION__) && __STDC_VERSION__ >= 199900L) && !(defined(_MSC_VER) && _MSC_VER >= 1800) -#error "This program requires C99, e.g. -std=c99 switch in GCC or it requires MSVC 18.0 or higher." -#error "Ignore torrent of syntax errors that may follow. It's only because compiler is set to use too old C version." -#endif - -#ifdef _OPENMP -#include -#else -#define omp_get_max_threads() 1 -#define omp_get_thread_num() 0 -#endif - -#include "libimagequant.h" - -#include "pam.h" -#include "mediancut.h" -#include "nearest.h" -#include "blur.h" -#include "kmeans.h" - -#define LIQ_HIGH_MEMORY_LIMIT (1<<26) /* avoid allocating buffers larger than 64MB */ - -// each structure has a pointer as a unique identifier that allows type checking at run time -static const char liq_attr_magic[] = "liq_attr"; -static const char liq_image_magic[] = "liq_image"; -static const char liq_result_magic[] = "liq_result"; -static const char liq_histogram_magic[] = "liq_histogram"; -static const char liq_remapping_result_magic[] = "liq_remapping_result"; -static const char liq_freed_magic[] = "free"; -#define CHECK_STRUCT_TYPE(attr, kind) liq_crash_if_invalid_handle_pointer_given((const liq_attr*)attr, kind ## _magic) -#define CHECK_USER_POINTER(ptr) liq_crash_if_invalid_pointer_given(ptr) - -struct liq_attr { - const char *magic_header; - void* (*malloc)(size_t); - void (*free)(void*); - - double target_mse, max_mse, kmeans_iteration_limit; - float min_opaque_val; - unsigned int max_colors, max_histogram_entries; - unsigned int min_posterization_output /* user setting */, min_posterization_input /* speed setting */; - unsigned int kmeans_iterations, feedback_loop_trials; - bool last_index_transparent, use_contrast_maps, use_dither_map; - unsigned char speed; - - unsigned char progress_stage1, progress_stage2, progress_stage3; - liq_progress_callback_function *progress_callback; - void *progress_callback_user_info; - - liq_log_callback_function *log_callback; - void *log_callback_user_info; - liq_log_flush_callback_function *log_flush_callback; - void *log_flush_callback_user_info; -}; - -struct liq_image { - const char *magic_header; - void* (*malloc)(size_t); - void (*free)(void*); - - f_pixel *f_pixels; - rgba_pixel **rows; - double gamma; - unsigned int width, height; - unsigned char *noise, *edges, *dither_map; - rgba_pixel *pixels, *temp_row; - f_pixel *temp_f_row; - liq_image_get_rgba_row_callback *row_callback; - void *row_callback_user_info; - float min_opaque_val; - f_pixel fixed_colors[256]; - unsigned short fixed_colors_count; - bool free_pixels, free_rows, free_rows_internal; -}; - -typedef struct liq_remapping_result { - const char *magic_header; - void* (*malloc)(size_t); - void (*free)(void*); - - unsigned char *pixels; - colormap *palette; - liq_progress_callback_function *progress_callback; - void *progress_callback_user_info; - - liq_palette int_palette; - double gamma, palette_error; - float dither_level; - bool use_dither_map; unsigned char progress_stage1; -} liq_remapping_result; - -struct liq_result { - const char *magic_header; - void* (*malloc)(size_t); - void (*free)(void*); - - liq_remapping_result *remapping; - colormap *palette; - liq_progress_callback_function *progress_callback; - void *progress_callback_user_info; - - liq_palette int_palette; - float dither_level; - double gamma, palette_error; - int min_posterization_output; - bool use_dither_map; -}; - -struct liq_histogram { - const char *magic_header; - void* (*malloc)(size_t); - void (*free)(void*); - - struct acolorhash_table *acht; - double gamma; - f_pixel fixed_colors[256]; - unsigned short fixed_colors_count; - unsigned short ignorebits; - bool had_image_added; -}; - -static void modify_alpha(liq_image *input_image, rgba_pixel *const row_pixels) LIQ_NONNULL; -static void contrast_maps(liq_image *image) LIQ_NONNULL; -static liq_error finalize_histogram(liq_histogram *input_hist, liq_attr *options, histogram **hist_output) LIQ_NONNULL; -static const rgba_pixel *liq_image_get_row_rgba(liq_image *input_image, unsigned int row) LIQ_NONNULL; -static const f_pixel *liq_image_get_row_f(liq_image *input_image, unsigned int row) LIQ_NONNULL; -static void liq_remapping_result_destroy(liq_remapping_result *result) LIQ_NONNULL; -static liq_error pngquant_quantize(histogram *hist, const liq_attr *options, const int fixed_colors_count, const f_pixel fixed_colors[], const double gamma, bool fixed_result_colors, liq_result **) LIQ_NONNULL; -static liq_error liq_histogram_quantize_internal(liq_histogram *input_hist, liq_attr *attr, bool fixed_result_colors, liq_result **result_output) LIQ_NONNULL; - -LIQ_NONNULL static void liq_verbose_printf(const liq_attr *context, const char *fmt, ...) -{ - if (context->log_callback) { - va_list va; - va_start(va, fmt); - int required_space = vsnprintf(NULL, 0, fmt, va)+1; // +\0 - va_end(va); - - char buf[required_space]; - va_start(va, fmt); - vsnprintf(buf, required_space, fmt, va); - va_end(va); - - context->log_callback(context, buf, context->log_callback_user_info); - } -} - -LIQ_NONNULL inline static void verbose_print(const liq_attr *attr, const char *msg) -{ - if (attr->log_callback) { - attr->log_callback(attr, msg, attr->log_callback_user_info); - } -} - -LIQ_NONNULL static void liq_verbose_printf_flush(liq_attr *attr) -{ - if (attr->log_flush_callback) { - attr->log_flush_callback(attr, attr->log_flush_callback_user_info); - } -} - -LIQ_NONNULL static bool liq_progress(const liq_attr *attr, const float percent) -{ - return attr->progress_callback && !attr->progress_callback(percent, attr->progress_callback_user_info); -} - -LIQ_NONNULL static bool liq_remap_progress(const liq_remapping_result *quant, const float percent) -{ - return quant->progress_callback && !quant->progress_callback(percent, quant->progress_callback_user_info); -} - -#if USE_SSE -inline static bool is_sse_available() -{ -#if (defined(__x86_64__) || defined(__amd64)) - return true; -#else - int a,b,c,d; - cpuid(1, a, b, c, d); - return d & (1<<25); // edx bit 25 is set when SSE is present -#endif -} -#endif - -/* make it clear in backtrace when user-supplied handle points to invalid memory */ -NEVER_INLINE LIQ_EXPORT bool liq_crash_if_invalid_handle_pointer_given(const liq_attr *user_supplied_pointer, const char *const expected_magic_header); -LIQ_EXPORT bool liq_crash_if_invalid_handle_pointer_given(const liq_attr *user_supplied_pointer, const char *const expected_magic_header) -{ - if (!user_supplied_pointer) { - return false; - } - - if (user_supplied_pointer->magic_header == liq_freed_magic) { - fprintf(stderr, "%s used after being freed", expected_magic_header); - // this is not normal error handling, this is programmer error that should crash the program. - // program cannot safely continue if memory has been used after it's been freed. - // abort() is nasty, but security vulnerability may be worse. - abort(); - } - - return user_supplied_pointer->magic_header == expected_magic_header; -} - -NEVER_INLINE LIQ_EXPORT bool liq_crash_if_invalid_pointer_given(const void *pointer); -LIQ_EXPORT bool liq_crash_if_invalid_pointer_given(const void *pointer) -{ - if (!pointer) { - return false; - } - // Force a read from the given (potentially invalid) memory location in order to check early whether this crashes the program or not. - // It doesn't matter what value is read, the code here is just to shut the compiler up about unused read. - char test_access = *((volatile char *)pointer); - return test_access || true; -} - -LIQ_NONNULL static void liq_log_error(const liq_attr *attr, const char *msg) -{ - if (!CHECK_STRUCT_TYPE(attr, liq_attr)) return; - liq_verbose_printf(attr, " error: %s", msg); -} - -static double quality_to_mse(long quality) -{ - if (quality == 0) { - return MAX_DIFF; - } - if (quality == 100) { - return 0; - } - - // curve fudged to be roughly similar to quality of libjpeg - // except lowest 10 for really low number of colors - const double extra_low_quality_fudge = MAX(0,0.016/(0.001+quality) - 0.001); - return extra_low_quality_fudge + 2.5/pow(210.0 + quality, 1.2) * (100.1-quality)/100.0; -} - -static unsigned int mse_to_quality(double mse) -{ - for(int i=100; i > 0; i--) { - if (mse <= quality_to_mse(i) + 0.000001) { // + epsilon for floating point errors - return i; - } - } - return 0; -} - -/** internally MSE is a sum of all channels with pixels 0..1 range, - but other software gives per-RGB-channel MSE for 0..255 range */ -static double mse_to_standard_mse(double mse) { - return mse * 65536.0/6.0; -} - -LIQ_EXPORT LIQ_NONNULL liq_error liq_set_quality(liq_attr* attr, int minimum, int target) -{ - if (!CHECK_STRUCT_TYPE(attr, liq_attr)) return LIQ_INVALID_POINTER; - if (target < 0 || target > 100 || target < minimum || minimum < 0) return LIQ_VALUE_OUT_OF_RANGE; - - attr->target_mse = quality_to_mse(target); - attr->max_mse = quality_to_mse(minimum); - return LIQ_OK; -} - -LIQ_EXPORT LIQ_NONNULL int liq_get_min_quality(const liq_attr *attr) -{ - if (!CHECK_STRUCT_TYPE(attr, liq_attr)) return -1; - return mse_to_quality(attr->max_mse); -} - -LIQ_EXPORT LIQ_NONNULL int liq_get_max_quality(const liq_attr *attr) -{ - if (!CHECK_STRUCT_TYPE(attr, liq_attr)) return -1; - return mse_to_quality(attr->target_mse); -} - - -LIQ_EXPORT LIQ_NONNULL liq_error liq_set_max_colors(liq_attr* attr, int colors) -{ - if (!CHECK_STRUCT_TYPE(attr, liq_attr)) return LIQ_INVALID_POINTER; - if (colors < 2 || colors > 256) return LIQ_VALUE_OUT_OF_RANGE; - - attr->max_colors = colors; - return LIQ_OK; -} - -LIQ_EXPORT LIQ_NONNULL int liq_get_max_colors(const liq_attr *attr) -{ - if (!CHECK_STRUCT_TYPE(attr, liq_attr)) return -1; - - return attr->max_colors; -} - -LIQ_EXPORT LIQ_NONNULL liq_error liq_set_min_posterization(liq_attr *attr, int bits) -{ - if (!CHECK_STRUCT_TYPE(attr, liq_attr)) return LIQ_INVALID_POINTER; - if (bits < 0 || bits > 4) return LIQ_VALUE_OUT_OF_RANGE; - - attr->min_posterization_output = bits; - return LIQ_OK; -} - -LIQ_EXPORT LIQ_NONNULL int liq_get_min_posterization(const liq_attr *attr) -{ - if (!CHECK_STRUCT_TYPE(attr, liq_attr)) return -1; - - return attr->min_posterization_output; -} - -LIQ_EXPORT LIQ_NONNULL liq_error liq_set_speed(liq_attr* attr, int speed) -{ - if (!CHECK_STRUCT_TYPE(attr, liq_attr)) return LIQ_INVALID_POINTER; - if (speed < 1 || speed > 10) return LIQ_VALUE_OUT_OF_RANGE; - - unsigned int iterations = MAX(8-speed, 0); iterations += iterations * iterations/2; - attr->kmeans_iterations = iterations; - attr->kmeans_iteration_limit = 1.0/(double)(1<<(23-speed)); - attr->feedback_loop_trials = MAX(56-9*speed, 0); - - attr->max_histogram_entries = (1<<17) + (1<<18)*(10-speed); - attr->min_posterization_input = (speed >= 8) ? 1 : 0; - attr->use_dither_map = (speed <= (omp_get_max_threads() > 1 ? 7 : 5)); // parallelized dither map might speed up floyd remapping - attr->use_contrast_maps = (speed <= 7) || attr->use_dither_map; - attr->speed = speed; - - attr->progress_stage1 = attr->use_contrast_maps ? 20 : 8; - if (attr->feedback_loop_trials < 2) attr->progress_stage1 += 30; - attr->progress_stage3 = 50 / (1+speed); - attr->progress_stage2 = 100 - attr->progress_stage1 - attr->progress_stage3; - return LIQ_OK; -} - -LIQ_EXPORT LIQ_NONNULL int liq_get_speed(const liq_attr *attr) -{ - if (!CHECK_STRUCT_TYPE(attr, liq_attr)) return -1; - - return attr->speed; -} - -LIQ_EXPORT LIQ_NONNULL liq_error liq_set_output_gamma(liq_result* res, double gamma) -{ - if (!CHECK_STRUCT_TYPE(res, liq_result)) return LIQ_INVALID_POINTER; - if (gamma <= 0 || gamma >= 1.0) return LIQ_VALUE_OUT_OF_RANGE; - - if (res->remapping) { - liq_remapping_result_destroy(res->remapping); - res->remapping = NULL; - } - - res->gamma = gamma; - return LIQ_OK; -} - -LIQ_EXPORT LIQ_NONNULL liq_error liq_set_min_opacity(liq_attr* attr, int min) -{ - if (!CHECK_STRUCT_TYPE(attr, liq_attr)) return LIQ_INVALID_POINTER; - if (min < 0 || min > 255) return LIQ_VALUE_OUT_OF_RANGE; - - attr->min_opaque_val = (double)min/255.0; - return LIQ_OK; -} - -LIQ_EXPORT LIQ_NONNULL int liq_get_min_opacity(const liq_attr *attr) -{ - if (!CHECK_STRUCT_TYPE(attr, liq_attr)) return -1; - - return MIN(255, 256.0 * attr->min_opaque_val); -} - -LIQ_EXPORT LIQ_NONNULL void liq_set_last_index_transparent(liq_attr* attr, int is_last) -{ - if (!CHECK_STRUCT_TYPE(attr, liq_attr)) return; - - attr->last_index_transparent = !!is_last; -} - -LIQ_EXPORT void liq_attr_set_progress_callback(liq_attr *attr, liq_progress_callback_function *callback, void *user_info) -{ - if (!CHECK_STRUCT_TYPE(attr, liq_attr)) return; - - attr->progress_callback = callback; - attr->progress_callback_user_info = user_info; -} - -LIQ_EXPORT void liq_result_set_progress_callback(liq_result *result, liq_progress_callback_function *callback, void *user_info) -{ - if (!CHECK_STRUCT_TYPE(result, liq_result)) return; - - result->progress_callback = callback; - result->progress_callback_user_info = user_info; -} - -LIQ_EXPORT void liq_set_log_callback(liq_attr *attr, liq_log_callback_function *callback, void* user_info) -{ - if (!CHECK_STRUCT_TYPE(attr, liq_attr)) return; - - liq_verbose_printf_flush(attr); - attr->log_callback = callback; - attr->log_callback_user_info = user_info; -} - -LIQ_EXPORT void liq_set_log_flush_callback(liq_attr *attr, liq_log_flush_callback_function *callback, void* user_info) -{ - if (!CHECK_STRUCT_TYPE(attr, liq_attr)) return; - - attr->log_flush_callback = callback; - attr->log_flush_callback_user_info = user_info; -} - -LIQ_EXPORT liq_attr* liq_attr_create() -{ - return liq_attr_create_with_allocator(NULL, NULL); -} - -LIQ_EXPORT LIQ_NONNULL void liq_attr_destroy(liq_attr *attr) -{ - if (!CHECK_STRUCT_TYPE(attr, liq_attr)) { - return; - } - - liq_verbose_printf_flush(attr); - - attr->magic_header = liq_freed_magic; - attr->free(attr); -} - -LIQ_EXPORT LIQ_NONNULL liq_attr* liq_attr_copy(const liq_attr *orig) -{ - if (!CHECK_STRUCT_TYPE(orig, liq_attr)) { - return NULL; - } - - liq_attr *attr = orig->malloc(sizeof(liq_attr)); - if (!attr) return NULL; - *attr = *orig; - return attr; -} - -static void *liq_aligned_malloc(size_t size) -{ - unsigned char *ptr = malloc(size + 16); - if (!ptr) { - return NULL; - } - - uintptr_t offset = 16 - ((uintptr_t)ptr & 15); // also reserves 1 byte for ptr[-1] - ptr += offset; - assert(0 == (((uintptr_t)ptr) & 15)); - ptr[-1] = offset ^ 0x59; // store how much pointer was shifted to get the original for free() - return ptr; -} - -LIQ_NONNULL static void liq_aligned_free(void *inptr) -{ - unsigned char *ptr = inptr; - size_t offset = ptr[-1] ^ 0x59; - assert(offset > 0 && offset <= 16); - free(ptr - offset); -} - -LIQ_EXPORT liq_attr* liq_attr_create_with_allocator(void* (*custom_malloc)(size_t), void (*custom_free)(void*)) -{ -#if USE_SSE - if (!is_sse_available()) { - return NULL; - } -#endif - if (!custom_malloc && !custom_free) { - custom_malloc = liq_aligned_malloc; - custom_free = liq_aligned_free; - } else if (!custom_malloc != !custom_free) { - return NULL; // either specify both or none - } - - liq_attr *attr = custom_malloc(sizeof(liq_attr)); - if (!attr) return NULL; - *attr = (liq_attr) { - .magic_header = liq_attr_magic, - .malloc = custom_malloc, - .free = custom_free, - .max_colors = 256, - .min_opaque_val = 1, // whether preserve opaque colors for IE (1.0=no, does not affect alpha) - .last_index_transparent = false, // puts transparent color at last index. This is workaround for blu-ray subtitles. - .target_mse = 0, - .max_mse = MAX_DIFF, - }; - liq_set_speed(attr, 3); - return attr; -} - -LIQ_EXPORT LIQ_NONNULL liq_error liq_image_add_fixed_color(liq_image *img, liq_color color) -{ - if (!CHECK_STRUCT_TYPE(img, liq_image)) return LIQ_INVALID_POINTER; - if (img->fixed_colors_count > 255) return LIQ_UNSUPPORTED; - - float gamma_lut[256]; - to_f_set_gamma(gamma_lut, img->gamma); - img->fixed_colors[img->fixed_colors_count++] = rgba_to_f(gamma_lut, (rgba_pixel){ - .r = color.r, - .g = color.g, - .b = color.b, - .a = color.a, - }); - return LIQ_OK; -} - -LIQ_NONNULL liq_error liq_histogram_add_fixed_color(liq_histogram *hist, f_pixel color) -{ - if (hist->fixed_colors_count > 255) return LIQ_UNSUPPORTED; - - hist->fixed_colors[hist->fixed_colors_count++] = color; - return LIQ_OK; -} - -LIQ_NONNULL static bool liq_image_use_low_memory(liq_image *img) -{ - img->temp_f_row = img->malloc(sizeof(img->f_pixels[0]) * img->width * omp_get_max_threads()); - return img->temp_f_row != NULL; -} - -LIQ_NONNULL static bool liq_image_should_use_low_memory(liq_image *img, const bool low_memory_hint) -{ - return img->width * img->height > (low_memory_hint ? LIQ_HIGH_MEMORY_LIMIT/8 : LIQ_HIGH_MEMORY_LIMIT) / sizeof(f_pixel); // Watch out for integer overflow -} - -static liq_image *liq_image_create_internal(const liq_attr *attr, rgba_pixel* rows[], liq_image_get_rgba_row_callback *row_callback, void *row_callback_user_info, int width, int height, double gamma) -{ - if (gamma < 0 || gamma > 1.0) { - liq_log_error(attr, "gamma must be >= 0 and <= 1 (try 1/gamma instead)"); - return NULL; - } - - if (!rows && !row_callback) { - liq_log_error(attr, "missing row data"); - return NULL; - } - - liq_image *img = attr->malloc(sizeof(liq_image)); - if (!img) return NULL; - *img = (liq_image){ - .magic_header = liq_image_magic, - .malloc = attr->malloc, - .free = attr->free, - .width = width, .height = height, - .gamma = gamma ? gamma : 0.45455, - .rows = rows, - .row_callback = row_callback, - .row_callback_user_info = row_callback_user_info, - .min_opaque_val = attr->min_opaque_val, - }; - - if (!rows || attr->min_opaque_val < 1.f) { - img->temp_row = attr->malloc(sizeof(img->temp_row[0]) * width * omp_get_max_threads()); - if (!img->temp_row) return NULL; - } - - // if image is huge or converted pixels are not likely to be reused then don't cache converted pixels - if (liq_image_should_use_low_memory(img, !img->temp_row && !attr->use_contrast_maps && !attr->use_dither_map)) { - verbose_print(attr, " conserving memory"); - if (!liq_image_use_low_memory(img)) return NULL; - } - - if (img->min_opaque_val < 1.f) { - verbose_print(attr, " Working around IE6 bug by making image less transparent..."); - } - - return img; -} - -LIQ_EXPORT LIQ_NONNULL liq_error liq_image_set_memory_ownership(liq_image *img, int ownership_flags) -{ - if (!CHECK_STRUCT_TYPE(img, liq_image)) return LIQ_INVALID_POINTER; - if (!img->rows || !ownership_flags || (ownership_flags & ~(LIQ_OWN_ROWS|LIQ_OWN_PIXELS))) { - return LIQ_VALUE_OUT_OF_RANGE; - } - - if (ownership_flags & LIQ_OWN_ROWS) { - if (img->free_rows_internal) return LIQ_VALUE_OUT_OF_RANGE; - img->free_rows = true; - } - - if (ownership_flags & LIQ_OWN_PIXELS) { - img->free_pixels = true; - if (!img->pixels) { - // for simplicity of this API there's no explicit bitmap argument, - // so the row with the lowest address is assumed to be at the start of the bitmap - img->pixels = img->rows[0]; - for(unsigned int i=1; i < img->height; i++) { - img->pixels = MIN(img->pixels, img->rows[i]); - } - } - } - - return LIQ_OK; -} - -LIQ_NONNULL static bool check_image_size(const liq_attr *attr, const int width, const int height) -{ - if (!CHECK_STRUCT_TYPE(attr, liq_attr)) { - return false; - } - - if (width <= 0 || height <= 0) { - liq_log_error(attr, "width and height must be > 0"); - return false; - } - - if (width > INT_MAX/sizeof(rgba_pixel)/height || width > INT_MAX/16/sizeof(f_pixel) || height > INT_MAX/sizeof(size_t)) { - liq_log_error(attr, "image too large"); - return false; - } - return true; -} - -LIQ_EXPORT 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) -{ - if (!check_image_size(attr, width, height)) { - return NULL; - } - return liq_image_create_internal(attr, NULL, row_callback, user_info, width, height, gamma); -} - -LIQ_EXPORT liq_image *liq_image_create_rgba_rows(const liq_attr *attr, void *const rows[], int width, int height, double gamma) -{ - if (!check_image_size(attr, width, height)) { - return NULL; - } - - for(int i=0; i < height; i++) { - if (!CHECK_USER_POINTER(rows+i) || !CHECK_USER_POINTER(rows[i])) { - liq_log_error(attr, "invalid row pointers"); - return NULL; - } - } - return liq_image_create_internal(attr, (rgba_pixel**)rows, NULL, NULL, width, height, gamma); -} - -LIQ_EXPORT LIQ_NONNULL liq_image *liq_image_create_rgba(const liq_attr *attr, const void* bitmap, int width, int height, double gamma) -{ - if (!check_image_size(attr, width, height)) { - return NULL; - } - if (!CHECK_USER_POINTER(bitmap)) { - liq_log_error(attr, "invalid bitmap pointer"); - return NULL; - } - - rgba_pixel *const pixels = (rgba_pixel *const)bitmap; - rgba_pixel **rows = attr->malloc(sizeof(rows[0])*height); - if (!rows) return NULL; - - for(int i=0; i < height; i++) { - rows[i] = pixels + width * i; - } - - liq_image *image = liq_image_create_internal(attr, rows, NULL, NULL, width, height, gamma); - if (!image) { - attr->free(rows); - return NULL; - } - image->free_rows = true; - image->free_rows_internal = true; - return image; -} - -NEVER_INLINE LIQ_EXPORT void liq_executing_user_callback(liq_image_get_rgba_row_callback *callback, liq_color *temp_row, int row, int width, void *user_info); -LIQ_EXPORT void liq_executing_user_callback(liq_image_get_rgba_row_callback *callback, liq_color *temp_row, int row, int width, void *user_info) -{ - assert(callback); - assert(temp_row); - callback(temp_row, row, width, user_info); -} - -LIQ_NONNULL inline static bool liq_image_has_rgba_pixels(const liq_image *img) -{ - if (!CHECK_STRUCT_TYPE(img, liq_image)) { - return false; - } - return img->rows || (img->temp_row && img->row_callback); -} - -LIQ_NONNULL inline static bool liq_image_can_use_rgba_rows(const liq_image *img) -{ - assert(liq_image_has_rgba_pixels(img)); - - const bool iebug = img->min_opaque_val < 1.f; - return (img->rows && !iebug); -} - -LIQ_NONNULL static const rgba_pixel *liq_image_get_row_rgba(liq_image *img, unsigned int row) -{ - if (liq_image_can_use_rgba_rows(img)) { - return img->rows[row]; - } - - assert(img->temp_row); - rgba_pixel *temp_row = img->temp_row + img->width * omp_get_thread_num(); - if (img->rows) { - memcpy(temp_row, img->rows[row], img->width * sizeof(temp_row[0])); - } else { - liq_executing_user_callback(img->row_callback, (liq_color*)temp_row, row, img->width, img->row_callback_user_info); - } - - if (img->min_opaque_val < 1.f) modify_alpha(img, temp_row); - return temp_row; -} - -LIQ_NONNULL static void convert_row_to_f(liq_image *img, f_pixel *row_f_pixels, const unsigned int row, const float gamma_lut[]) -{ - assert(row_f_pixels); - assert(!USE_SSE || 0 == ((uintptr_t)row_f_pixels & 15)); - - const rgba_pixel *const row_pixels = liq_image_get_row_rgba(img, row); - - for(unsigned int col=0; col < img->width; col++) { - row_f_pixels[col] = rgba_to_f(gamma_lut, row_pixels[col]); - } -} - -LIQ_NONNULL static const f_pixel *liq_image_get_row_f(liq_image *img, unsigned int row) -{ - if (!img->f_pixels) { - if (img->temp_f_row) { - float gamma_lut[256]; - to_f_set_gamma(gamma_lut, img->gamma); - f_pixel *row_for_thread = img->temp_f_row + img->width * omp_get_thread_num(); - convert_row_to_f(img, row_for_thread, row, gamma_lut); - return row_for_thread; - } - - assert(omp_get_thread_num() == 0); - if (!liq_image_should_use_low_memory(img, false)) { - img->f_pixels = img->malloc(sizeof(img->f_pixels[0]) * img->width * img->height); - } - if (!img->f_pixels) { - if (!liq_image_use_low_memory(img)) return NULL; - return liq_image_get_row_f(img, row); - } - - float gamma_lut[256]; - to_f_set_gamma(gamma_lut, img->gamma); - for(unsigned int i=0; i < img->height; i++) { - convert_row_to_f(img, &img->f_pixels[i*img->width], i, gamma_lut); - } - } - return img->f_pixels + img->width * row; -} - -LIQ_EXPORT LIQ_NONNULL int liq_image_get_width(const liq_image *input_image) -{ - if (!CHECK_STRUCT_TYPE(input_image, liq_image)) return -1; - return input_image->width; -} - -LIQ_EXPORT LIQ_NONNULL int liq_image_get_height(const liq_image *input_image) -{ - if (!CHECK_STRUCT_TYPE(input_image, liq_image)) return -1; - return input_image->height; -} - -typedef void free_func(void*); - -LIQ_NONNULL static free_func *get_default_free_func(liq_image *img) -{ - // When default allocator is used then user-supplied pointers must be freed with free() - if (img->free_rows_internal || img->free != liq_aligned_free) { - return img->free; - } - return free; -} - -LIQ_NONNULL static void liq_image_free_rgba_source(liq_image *input_image) -{ - if (input_image->free_pixels && input_image->pixels) { - get_default_free_func(input_image)(input_image->pixels); - input_image->pixels = NULL; - } - - if (input_image->free_rows && input_image->rows) { - get_default_free_func(input_image)(input_image->rows); - input_image->rows = NULL; - } -} - -LIQ_EXPORT LIQ_NONNULL void liq_image_destroy(liq_image *input_image) -{ - if (!CHECK_STRUCT_TYPE(input_image, liq_image)) return; - - liq_image_free_rgba_source(input_image); - - if (input_image->noise) { - input_image->free(input_image->noise); - } - - if (input_image->edges) { - input_image->free(input_image->edges); - } - - if (input_image->dither_map) { - input_image->free(input_image->dither_map); - } - - if (input_image->f_pixels) { - input_image->free(input_image->f_pixels); - } - - if (input_image->temp_row) { - input_image->free(input_image->temp_row); - } - - if (input_image->temp_f_row) { - input_image->free(input_image->temp_f_row); - } - - input_image->magic_header = liq_freed_magic; - input_image->free(input_image); -} - -LIQ_EXPORT liq_histogram* liq_histogram_create(const liq_attr* attr) -{ - if (!CHECK_STRUCT_TYPE(attr, liq_attr)) { - return NULL; - } - - liq_histogram *hist = attr->malloc(sizeof(liq_histogram)); - if (!hist) return NULL; - *hist = (liq_histogram) { - .magic_header = liq_histogram_magic, - .malloc = attr->malloc, - .free = attr->free, - - .ignorebits = MAX(attr->min_posterization_output, attr->min_posterization_input), - }; - return hist; -} - -LIQ_EXPORT LIQ_NONNULL void liq_histogram_destroy(liq_histogram *hist) -{ - if (!CHECK_STRUCT_TYPE(hist, liq_histogram)) return; - hist->magic_header = liq_freed_magic; - - pam_freeacolorhash(hist->acht); - hist->free(hist); -} - -LIQ_EXPORT LIQ_NONNULL liq_result *liq_quantize_image(liq_attr *attr, liq_image *img) -{ - liq_result *res; - if (LIQ_OK != liq_image_quantize(img, attr, &res)) { - return NULL; - } - return res; -} - -LIQ_EXPORT LIQ_NONNULL liq_error liq_image_quantize(liq_image *const img, liq_attr *const attr, liq_result **result_output) -{ - if (!CHECK_STRUCT_TYPE(attr, liq_attr)) return LIQ_INVALID_POINTER; - if (!liq_image_has_rgba_pixels(img)) { - return LIQ_UNSUPPORTED; - } - - liq_histogram *hist = liq_histogram_create(attr); - if (!hist) { - return LIQ_OUT_OF_MEMORY; - } - liq_error err = liq_histogram_add_image(hist, attr, img); - if (LIQ_OK != err) { - return err; - } - - err = liq_histogram_quantize_internal(hist, attr, false, result_output); - liq_histogram_destroy(hist); - - return err; -} - -LIQ_EXPORT LIQ_NONNULL liq_error liq_histogram_quantize(liq_histogram *input_hist, liq_attr *attr, liq_result **result_output) { - return liq_histogram_quantize_internal(input_hist, attr, true, result_output); -} - -LIQ_NONNULL static liq_error liq_histogram_quantize_internal(liq_histogram *input_hist, liq_attr *attr, bool fixed_result_colors, liq_result **result_output) -{ - if (!CHECK_USER_POINTER(result_output)) return LIQ_INVALID_POINTER; - *result_output = NULL; - - if (!CHECK_STRUCT_TYPE(attr, liq_attr)) return LIQ_INVALID_POINTER; - if (!CHECK_STRUCT_TYPE(input_hist, liq_histogram)) return LIQ_INVALID_POINTER; - - if (liq_progress(attr, 0)) return LIQ_ABORTED; - - histogram *hist; - liq_error err = finalize_histogram(input_hist, attr, &hist); - if (err != LIQ_OK) { - return err; - } - - err = pngquant_quantize(hist, attr, input_hist->fixed_colors_count, input_hist->fixed_colors, input_hist->gamma, fixed_result_colors, result_output); - pam_freeacolorhist(hist); - - return err; -} - -LIQ_EXPORT LIQ_NONNULL liq_error liq_set_dithering_level(liq_result *res, float dither_level) -{ - if (!CHECK_STRUCT_TYPE(res, liq_result)) return LIQ_INVALID_POINTER; - - if (res->remapping) { - liq_remapping_result_destroy(res->remapping); - res->remapping = NULL; - } - - if (res->dither_level < 0 || res->dither_level > 1.0f) return LIQ_VALUE_OUT_OF_RANGE; - res->dither_level = dither_level; - return LIQ_OK; -} - -LIQ_NONNULL static liq_remapping_result *liq_remapping_result_create(liq_result *result) -{ - if (!CHECK_STRUCT_TYPE(result, liq_result)) { - return NULL; - } - - liq_remapping_result *res = result->malloc(sizeof(liq_remapping_result)); - if (!res) return NULL; - *res = (liq_remapping_result) { - .magic_header = liq_remapping_result_magic, - .malloc = result->malloc, - .free = result->free, - .dither_level = result->dither_level, - .use_dither_map = result->use_dither_map, - .palette_error = result->palette_error, - .gamma = result->gamma, - .palette = pam_duplicate_colormap(result->palette), - .progress_callback = result->progress_callback, - .progress_callback_user_info = result->progress_callback_user_info, - .progress_stage1 = result->use_dither_map ? 20 : 0, - }; - return res; -} - -LIQ_EXPORT LIQ_NONNULL double liq_get_output_gamma(const liq_result *result) -{ - if (!CHECK_STRUCT_TYPE(result, liq_result)) return -1; - - return result->gamma; -} - -LIQ_NONNULL static void liq_remapping_result_destroy(liq_remapping_result *result) -{ - if (!CHECK_STRUCT_TYPE(result, liq_remapping_result)) return; - - if (result->palette) pam_freecolormap(result->palette); - if (result->pixels) result->free(result->pixels); - - result->magic_header = liq_freed_magic; - result->free(result); -} - -LIQ_EXPORT LIQ_NONNULL void liq_result_destroy(liq_result *res) -{ - if (!CHECK_STRUCT_TYPE(res, liq_result)) return; - - memset(&res->int_palette, 0, sizeof(liq_palette)); - - if (res->remapping) { - memset(&res->remapping->int_palette, 0, sizeof(liq_palette)); - liq_remapping_result_destroy(res->remapping); - } - - pam_freecolormap(res->palette); - - res->magic_header = liq_freed_magic; - res->free(res); -} - - -LIQ_EXPORT LIQ_NONNULL double liq_get_quantization_error(const liq_result *result) { - if (!CHECK_STRUCT_TYPE(result, liq_result)) return -1; - - if (result->palette_error >= 0) { - return mse_to_standard_mse(result->palette_error); - } - - return -1; -} - -LIQ_EXPORT LIQ_NONNULL double liq_get_remapping_error(const liq_result *result) { - if (!CHECK_STRUCT_TYPE(result, liq_result)) return -1; - - if (result->remapping && result->remapping->palette_error >= 0) { - return mse_to_standard_mse(result->remapping->palette_error); - } - - return -1; -} - -LIQ_EXPORT LIQ_NONNULL int liq_get_quantization_quality(const liq_result *result) { - if (!CHECK_STRUCT_TYPE(result, liq_result)) return -1; - - if (result->palette_error >= 0) { - return mse_to_quality(result->palette_error); - } - - return -1; -} - -LIQ_EXPORT LIQ_NONNULL int liq_get_remapping_quality(const liq_result *result) { - if (!CHECK_STRUCT_TYPE(result, liq_result)) return -1; - - if (result->remapping && result->remapping->palette_error >= 0) { - return mse_to_quality(result->remapping->palette_error); - } - - return -1; -} - -LIQ_NONNULL static int compare_popularity(const void *ch1, const void *ch2) -{ - const float v1 = ((const colormap_item*)ch1)->popularity; - const float v2 = ((const colormap_item*)ch2)->popularity; - return v1 > v2 ? -1 : 1; -} - -LIQ_NONNULL static void sort_palette_qsort(colormap *map, int start, int nelem) -{ - if (!nelem) return; - qsort(map->palette + start, nelem, sizeof(map->palette[0]), compare_popularity); -} - -#define SWAP_PALETTE(map, a,b) { \ - const colormap_item tmp = (map)->palette[(a)]; \ - (map)->palette[(a)] = (map)->palette[(b)]; \ - (map)->palette[(b)] = tmp; } - -LIQ_NONNULL static void sort_palette(colormap *map, const liq_attr *options) -{ - /* - ** Step 3.5 [GRR]: remap the palette colors so that all entries with - ** the maximal alpha value (i.e., fully opaque) are at the end and can - ** therefore be omitted from the tRNS chunk. - */ - if (options->last_index_transparent) { - for(unsigned int i=0; i < map->colors; i++) { - if (map->palette[i].acolor.a < 1.0/256.0) { - const unsigned int old = i, transparent_dest = map->colors-1; - - SWAP_PALETTE(map, transparent_dest, old); - - /* colors sorted by popularity make pngs slightly more compressible */ - sort_palette_qsort(map, 0, map->colors-1); - return; - } - } - } - - unsigned int non_fixed_colors = 0; - for(unsigned int i = 0; i < map->colors; i++) { - if (map->palette[i].fixed) { - break; - } - non_fixed_colors++; - } - - /* move transparent colors to the beginning to shrink trns chunk */ - unsigned int num_transparent = 0; - for(unsigned int i = 0; i < non_fixed_colors; i++) { - if (map->palette[i].acolor.a < 255.0/256.0) { - // current transparent color is swapped with earlier opaque one - if (i != num_transparent) { - SWAP_PALETTE(map, num_transparent, i); - i--; - } - num_transparent++; - } - } - - liq_verbose_printf(options, " eliminated opaque tRNS-chunk entries...%d entr%s transparent", num_transparent, (num_transparent == 1)? "y" : "ies"); - - /* colors sorted by popularity make pngs slightly more compressible - * opaque and transparent are sorted separately - */ - sort_palette_qsort(map, 0, num_transparent); - sort_palette_qsort(map, num_transparent, non_fixed_colors - num_transparent); - - if (non_fixed_colors > 9 && map->colors > 16) { - SWAP_PALETTE(map, 7, 1); // slightly improves compression - SWAP_PALETTE(map, 8, 2); - SWAP_PALETTE(map, 9, 3); - } -} - -inline static unsigned int posterize_channel(unsigned int color, unsigned int bits) -{ - return (color & ~((1<> (8-bits)); -} - -LIQ_NONNULL static void set_rounded_palette(liq_palette *const dest, colormap *const map, const double gamma, unsigned int posterize) -{ - float gamma_lut[256]; - to_f_set_gamma(gamma_lut, gamma); - - dest->count = map->colors; - for(unsigned int x = 0; x < map->colors; ++x) { - rgba_pixel px = f_to_rgb(gamma, map->palette[x].acolor); - - px.r = posterize_channel(px.r, posterize); - px.g = posterize_channel(px.g, posterize); - px.b = posterize_channel(px.b, posterize); - px.a = posterize_channel(px.a, posterize); - - map->palette[x].acolor = rgba_to_f(gamma_lut, px); /* saves rounding error introduced by to_rgb, which makes remapping & dithering more accurate */ - - if (!px.a && !map->palette[x].fixed) { - px.r = 71; px.g = 112; px.b = 76; - } - - dest->entries[x] = (liq_color){.r=px.r,.g=px.g,.b=px.b,.a=px.a}; - } -} - -LIQ_EXPORT LIQ_NONNULL const liq_palette *liq_get_palette(liq_result *result) -{ - if (!CHECK_STRUCT_TYPE(result, liq_result)) return NULL; - - if (result->remapping && result->remapping->int_palette.count) { - return &result->remapping->int_palette; - } - - if (!result->int_palette.count) { - set_rounded_palette(&result->int_palette, result->palette, result->gamma, result->min_posterization_output); - } - return &result->int_palette; -} - -LIQ_NONNULL static float remap_to_palette(liq_image *const input_image, unsigned char *const *const output_pixels, colormap *const map) -{ - const int rows = input_image->height; - const unsigned int cols = input_image->width; - double remapping_error=0; - - if (!liq_image_get_row_f(input_image, 0)) { // trigger lazy conversion - return -1; - } - - struct nearest_map *const n = nearest_init(map); - - const unsigned int max_threads = omp_get_max_threads(); - kmeans_state average_color[(KMEANS_CACHE_LINE_GAP+map->colors) * max_threads]; - kmeans_init(map, max_threads, average_color); - - #pragma omp parallel for if (rows*cols > 3000) \ - schedule(static) default(none) shared(average_color) reduction(+:remapping_error) - for(int row = 0; row < rows; ++row) { - const f_pixel *const row_pixels = liq_image_get_row_f(input_image, row); - unsigned int last_match=0; - for(unsigned int col = 0; col < cols; ++col) { - float diff; - output_pixels[row][col] = last_match = nearest_search(n, &row_pixels[col], last_match, &diff); - - remapping_error += diff; - kmeans_update_color(row_pixels[col], 1.0, map, last_match, omp_get_thread_num(), average_color); - } - } - - kmeans_finalize(map, max_threads, average_color); - - nearest_free(n); - - return remapping_error / (input_image->width * input_image->height); -} - -inline static f_pixel get_dithered_pixel(const float dither_level, const float max_dither_error, const f_pixel thiserr, const f_pixel px) -{ - /* Use Floyd-Steinberg errors to adjust actual color. */ - const float sr = thiserr.r * dither_level, - sg = thiserr.g * dither_level, - sb = thiserr.b * dither_level, - sa = thiserr.a * dither_level; - - float ratio = 1.0; - const float max_overflow = 1.1f; - const float max_underflow = -0.1f; - - // allowing some overflow prevents undithered bands caused by clamping of all channels - if (px.r + sr > max_overflow) ratio = MIN(ratio, (max_overflow -px.r)/sr); - else { if (px.r + sr < max_underflow) ratio = MIN(ratio, (max_underflow-px.r)/sr); } - if (px.g + sg > max_overflow) ratio = MIN(ratio, (max_overflow -px.g)/sg); - else { if (px.g + sg < max_underflow) ratio = MIN(ratio, (max_underflow-px.g)/sg); } - if (px.b + sb > max_overflow) ratio = MIN(ratio, (max_overflow -px.b)/sb); - else { if (px.b + sb < max_underflow) ratio = MIN(ratio, (max_underflow-px.b)/sb); } - - float a = px.a + sa; - if (a > 1.0) { a = 1.0; } - else if (a < 0) { a = 0; } - - // If dithering error is crazy high, don't propagate it that much - // This prevents crazy geen pixels popping out of the blue (or red or black! ;) - const float dither_error = sr*sr + sg*sg + sb*sb + sa*sa; - if (dither_error > max_dither_error) { - ratio *= 0.8; - } else if (dither_error < 2.f/256.f/256.f) { - // don't dither areas that don't have noticeable error — makes file smaller - return px; - } - - return (f_pixel){ - .r=px.r + sr * ratio, - .g=px.g + sg * ratio, - .b=px.b + sb * ratio, - .a=a, - }; -} - -/** - Uses edge/noise map to apply dithering only to flat areas. Dithering on edges creates jagged lines, and noisy areas are "naturally" dithered. - - If output_image_is_remapped is true, only pixels noticeably changed by error diffusion will be written to output image. - */ -LIQ_NONNULL static bool remap_to_palette_floyd(liq_image *input_image, unsigned char *const output_pixels[], liq_remapping_result *quant, const float max_dither_error, const bool output_image_is_remapped) -{ - const int rows = input_image->height, cols = input_image->width; - const unsigned char *dither_map = quant->use_dither_map ? (input_image->dither_map ? input_image->dither_map : input_image->edges) : NULL; - - const colormap *map = quant->palette; - const colormap_item *acolormap = map->palette; - - /* Initialize Floyd-Steinberg error vectors. */ - const size_t errwidth = cols+2; - f_pixel *restrict thiserr = input_image->malloc(errwidth * sizeof(thiserr[0]) * 2); // +2 saves from checking out of bounds access - if (!thiserr) return false; - f_pixel *restrict nexterr = thiserr + errwidth; - memset(thiserr, 0, errwidth * sizeof(thiserr[0])); - - bool ok = true; - struct nearest_map *const n = nearest_init(map); - - // response to this value is non-linear and without it any value < 0.8 would give almost no dithering - float base_dithering_level = quant->dither_level; - base_dithering_level = 1.0 - (1.0-base_dithering_level)*(1.0-base_dithering_level); - - if (dither_map) { - base_dithering_level *= 1.0/255.0; // convert byte to float - } - base_dithering_level *= 15.0/16.0; // prevent small errors from accumulating - - int fs_direction = 1; - unsigned int last_match=0; - for (int row = 0; row < rows; ++row) { - if (liq_remap_progress(quant, quant->progress_stage1 + row * (100.f - quant->progress_stage1) / rows)) { - ok = false; - break; - } - - memset(nexterr, 0, errwidth * sizeof(nexterr[0])); - - int col = (fs_direction > 0) ? 0 : (cols - 1); - const f_pixel *const row_pixels = liq_image_get_row_f(input_image, row); - - do { - float dither_level = base_dithering_level; - if (dither_map) { - dither_level *= dither_map[row*cols + col]; - } - - const f_pixel spx = get_dithered_pixel(dither_level, max_dither_error, thiserr[col + 1], row_pixels[col]); - - const unsigned int guessed_match = output_image_is_remapped ? output_pixels[row][col] : last_match; - output_pixels[row][col] = last_match = nearest_search(n, &spx, guessed_match, NULL); - - const f_pixel output_px = acolormap[last_match].acolor; - f_pixel err = { - .r = (spx.r - output_px.r), - .g = (spx.g - output_px.g), - .b = (spx.b - output_px.b), - .a = (spx.a - output_px.a), - }; - - // If dithering error is crazy high, don't propagate it that much - // This prevents crazy geen pixels popping out of the blue (or red or black! ;) - if (err.r*err.r + err.g*err.g + err.b*err.b + err.a*err.a > max_dither_error) { - err.r *= 0.75; - err.g *= 0.75; - err.b *= 0.75; - err.a *= 0.75; - } - - /* Propagate Floyd-Steinberg error terms. */ - if (fs_direction > 0) { - thiserr[col + 2].a += err.a * (7.f/16.f); - thiserr[col + 2].r += err.r * (7.f/16.f); - thiserr[col + 2].g += err.g * (7.f/16.f); - thiserr[col + 2].b += err.b * (7.f/16.f); - - nexterr[col + 2].a = err.a * (1.f/16.f); - nexterr[col + 2].r = err.r * (1.f/16.f); - nexterr[col + 2].g = err.g * (1.f/16.f); - nexterr[col + 2].b = err.b * (1.f/16.f); - - nexterr[col + 1].a += err.a * (5.f/16.f); - nexterr[col + 1].r += err.r * (5.f/16.f); - nexterr[col + 1].g += err.g * (5.f/16.f); - nexterr[col + 1].b += err.b * (5.f/16.f); - - nexterr[col ].a += err.a * (3.f/16.f); - nexterr[col ].r += err.r * (3.f/16.f); - nexterr[col ].g += err.g * (3.f/16.f); - nexterr[col ].b += err.b * (3.f/16.f); - - } else { - thiserr[col ].a += err.a * (7.f/16.f); - thiserr[col ].r += err.r * (7.f/16.f); - thiserr[col ].g += err.g * (7.f/16.f); - thiserr[col ].b += err.b * (7.f/16.f); - - nexterr[col ].a = err.a * (1.f/16.f); - nexterr[col ].r = err.r * (1.f/16.f); - nexterr[col ].g = err.g * (1.f/16.f); - nexterr[col ].b = err.b * (1.f/16.f); - - nexterr[col + 1].a += err.a * (5.f/16.f); - nexterr[col + 1].r += err.r * (5.f/16.f); - nexterr[col + 1].g += err.g * (5.f/16.f); - nexterr[col + 1].b += err.b * (5.f/16.f); - - nexterr[col + 2].a += err.a * (3.f/16.f); - nexterr[col + 2].r += err.r * (3.f/16.f); - nexterr[col + 2].g += err.g * (3.f/16.f); - nexterr[col + 2].b += err.b * (3.f/16.f); - } - - // remapping is done in zig-zag - col += fs_direction; - if (fs_direction > 0) { - if (col >= cols) break; - } else { - if (col < 0) break; - } - } while(1); - - f_pixel *const temperr = thiserr; - thiserr = nexterr; - nexterr = temperr; - fs_direction = -fs_direction; - } - - input_image->free(MIN(thiserr, nexterr)); // MIN because pointers were swapped - nearest_free(n); - - return ok; -} - -/* fixed colors are always included in the palette, so it would be wasteful to duplicate them in palette from histogram */ -LIQ_NONNULL static void remove_fixed_colors_from_histogram(histogram *hist, const int fixed_colors_count, const f_pixel fixed_colors[], const float target_mse) -{ - const float max_difference = MAX(target_mse/2.0, 2.0/256.0/256.0); - if (fixed_colors_count) { - for(int j=0; j < hist->size; j++) { - for(unsigned int i=0; i < fixed_colors_count; i++) { - if (colordifference(hist->achv[j].acolor, fixed_colors[i]) < max_difference) { - hist->achv[j] = hist->achv[--hist->size]; // remove color from histogram by overwriting with the last entry - j--; break; // continue searching histogram - } - } - } - } -} - -LIQ_EXPORT LIQ_NONNULL liq_error liq_histogram_add_colors(liq_histogram *input_hist, const liq_attr *options, const liq_histogram_entry entries[], int num_entries, double gamma) -{ - if (!CHECK_STRUCT_TYPE(options, liq_attr)) return LIQ_INVALID_POINTER; - if (!CHECK_STRUCT_TYPE(input_hist, liq_histogram)) return LIQ_INVALID_POINTER; - if (!CHECK_USER_POINTER(entries)) return LIQ_INVALID_POINTER; - if (gamma < 0 || gamma >= 1.0) return LIQ_VALUE_OUT_OF_RANGE; - if (num_entries <= 0 || num_entries > 1<<30) return LIQ_VALUE_OUT_OF_RANGE; - - if (input_hist->ignorebits > 0 && input_hist->had_image_added) { - return LIQ_UNSUPPORTED; - } - input_hist->ignorebits = 0; - - input_hist->had_image_added = true; - input_hist->gamma = gamma ? gamma : 0.45455; - - if (!input_hist->acht) { - input_hist->acht = pam_allocacolorhash(~0, num_entries*num_entries, 0, options->malloc, options->free); - if (!input_hist->acht) { - return LIQ_OUT_OF_MEMORY; - } - } - // Fake image size. It's only for hash size estimates. - if (!input_hist->acht->cols) { - input_hist->acht->cols = num_entries; - } - input_hist->acht->rows += num_entries; - - const unsigned int hash_size = input_hist->acht->hash_size; - for(int i=0; i < num_entries; i++) { - const rgba_pixel rgba = { - .r = entries[i].color.r, - .g = entries[i].color.g, - .b = entries[i].color.b, - .a = entries[i].color.a, - }; - union rgba_as_int px = {rgba}; - unsigned int hash; - if (px.rgba.a) { - hash = px.l % hash_size; - } else { - hash=0; px.l=0; - } - if (!pam_add_to_hash(input_hist->acht, hash, entries[i].count, px, i, num_entries)) { - return LIQ_OUT_OF_MEMORY; - } - } - - return LIQ_OK; -} - -LIQ_EXPORT LIQ_NONNULL liq_error liq_histogram_add_image(liq_histogram *input_hist, const liq_attr *options, liq_image *input_image) -{ - if (!CHECK_STRUCT_TYPE(options, liq_attr)) return LIQ_INVALID_POINTER; - if (!CHECK_STRUCT_TYPE(input_hist, liq_histogram)) return LIQ_INVALID_POINTER; - if (!CHECK_STRUCT_TYPE(input_image, liq_image)) return LIQ_INVALID_POINTER; - - const unsigned int cols = input_image->width, rows = input_image->height; - - if (!input_image->noise && options->use_contrast_maps) { - contrast_maps(input_image); - } - - input_hist->gamma = input_image->gamma; - - for(int i = 0; i < input_image->fixed_colors_count; i++) { - liq_error res = liq_histogram_add_fixed_color(input_hist, input_image->fixed_colors[i]); - if (res != LIQ_OK) { - return res; - } - } - - /* - ** Step 2: attempt to make a histogram of the colors, unclustered. - ** If at first we don't succeed, increase ignorebits to increase color - ** coherence and try again. - */ - - if (liq_progress(options, options->progress_stage1 * 0.4f)) return LIQ_ABORTED; - - const bool all_rows_at_once = liq_image_can_use_rgba_rows(input_image); - - // Usual solution is to start from scratch when limit is exceeded, but that's not possible if it's not - // the first image added - const unsigned int max_histogram_entries = input_hist->had_image_added ? ~0 : options->max_histogram_entries; - do { - if (!input_hist->acht) { - input_hist->acht = pam_allocacolorhash(max_histogram_entries, rows*cols, input_hist->ignorebits, options->malloc, options->free); - } - if (!input_hist->acht) return LIQ_OUT_OF_MEMORY; - - // histogram uses noise contrast map for importance. Color accuracy in noisy areas is not very important. - // noise map does not include edges to avoid ruining anti-aliasing - for(unsigned int row=0; row < rows; row++) { - bool added_ok; - if (all_rows_at_once) { - added_ok = pam_computeacolorhash(input_hist->acht, (const rgba_pixel *const *)input_image->rows, cols, rows, input_image->noise); - if (added_ok) break; - } else { - const rgba_pixel* rows_p[1] = { liq_image_get_row_rgba(input_image, row) }; - added_ok = pam_computeacolorhash(input_hist->acht, rows_p, cols, 1, input_image->noise ? &input_image->noise[row * cols] : NULL); - } - if (!added_ok) { - input_hist->ignorebits++; - liq_verbose_printf(options, " too many colors! Scaling colors to improve clustering... %d", input_hist->ignorebits); - pam_freeacolorhash(input_hist->acht); - input_hist->acht = NULL; - if (liq_progress(options, options->progress_stage1 * 0.6f)) return LIQ_ABORTED; - break; - } - } - } while(!input_hist->acht); - - input_hist->had_image_added = true; - - if (input_image->noise) { - input_image->free(input_image->noise); - input_image->noise = NULL; - } - - if (input_image->free_pixels && input_image->f_pixels) { - liq_image_free_rgba_source(input_image); // bow can free the RGBA source if copy has been made in f_pixels - } - - return LIQ_OK; -} - -LIQ_NONNULL static liq_error finalize_histogram(liq_histogram *input_hist, liq_attr *options, histogram **hist_output) -{ - if (liq_progress(options, options->progress_stage1 * 0.9f)) { - return LIQ_ABORTED; - } - - if (!input_hist->acht) { - return LIQ_BITMAP_NOT_AVAILABLE; - } - - histogram *hist = pam_acolorhashtoacolorhist(input_hist->acht, input_hist->gamma, options->malloc, options->free); - pam_freeacolorhash(input_hist->acht); - input_hist->acht = NULL; - - if (!hist) { - return LIQ_OUT_OF_MEMORY; - } - liq_verbose_printf(options, " made histogram...%d colors found", hist->size); - remove_fixed_colors_from_histogram(hist, input_hist->fixed_colors_count, input_hist->fixed_colors, options->target_mse); - - *hist_output = hist; - return LIQ_OK; -} - -LIQ_NONNULL static void modify_alpha(liq_image *input_image, rgba_pixel *const row_pixels) -{ - /* IE6 makes colors with even slightest transparency completely transparent, - thus to improve situation in IE, make colors that are less than ~10% transparent - completely opaque */ - - const float min_opaque_val = input_image->min_opaque_val; - const float almost_opaque_val = min_opaque_val * 169.f/256.f; - const unsigned int almost_opaque_val_int = (min_opaque_val * 169.f/256.f)*255.f; - - for(unsigned int col = 0; col < input_image->width; col++) { - const rgba_pixel px = row_pixels[col]; - - /* ie bug: to avoid visible step caused by forced opaqueness, linearily raise opaqueness of almost-opaque colors */ - if (px.a >= almost_opaque_val_int) { - float al = px.a / 255.f; - al = almost_opaque_val + (al-almost_opaque_val) * (1.f-almost_opaque_val) / (min_opaque_val-almost_opaque_val); - al *= 256.f; - row_pixels[col].a = al >= 255.f ? 255 : al; - } - } -} - -/** - Builds two maps: - noise - approximation of areas with high-frequency noise, except straight edges. 1=flat, 0=noisy. - edges - noise map including all edges - */ -LIQ_NONNULL static void contrast_maps(liq_image *image) -{ - const unsigned int cols = image->width, rows = image->height; - if (cols < 4 || rows < 4 || (3*cols*rows) > LIQ_HIGH_MEMORY_LIMIT) { - return; - } - - unsigned char *restrict noise = image->noise ? image->noise : image->malloc(cols*rows); - image->noise = NULL; - unsigned char *restrict edges = image->edges ? image->edges : image->malloc(cols*rows); - image->edges = NULL; - - unsigned char *restrict tmp = image->malloc(cols*rows); - - if (!noise || !edges || !tmp) { - image->free(noise); - image->free(edges); - image->free(tmp); - return; - } - - const f_pixel *curr_row, *prev_row, *next_row; - curr_row = prev_row = next_row = liq_image_get_row_f(image, 0); - - for (unsigned int j=0; j < rows; j++) { - prev_row = curr_row; - curr_row = next_row; - next_row = liq_image_get_row_f(image, MIN(rows-1,j+1)); - - f_pixel prev, curr = curr_row[0], next=curr; - for (unsigned int i=0; i < cols; i++) { - prev=curr; - curr=next; - next = curr_row[MIN(cols-1,i+1)]; - - // contrast is difference between pixels neighbouring horizontally and vertically - const float a = fabsf(prev.a+next.a - curr.a*2.f), - r = fabsf(prev.r+next.r - curr.r*2.f), - g = fabsf(prev.g+next.g - curr.g*2.f), - b = fabsf(prev.b+next.b - curr.b*2.f); - - const f_pixel prevl = prev_row[i]; - const f_pixel nextl = next_row[i]; - - const float a1 = fabsf(prevl.a+nextl.a - curr.a*2.f), - r1 = fabsf(prevl.r+nextl.r - curr.r*2.f), - g1 = fabsf(prevl.g+nextl.g - curr.g*2.f), - b1 = fabsf(prevl.b+nextl.b - curr.b*2.f); - - const float horiz = MAX(MAX(a,r),MAX(g,b)); - const float vert = MAX(MAX(a1,r1),MAX(g1,b1)); - const float edge = MAX(horiz,vert); - float z = edge - fabsf(horiz-vert)*.5f; - z = 1.f - MAX(z,MIN(horiz,vert)); - z *= z; // noise is amplified - z *= z; - - z *= 256.f; - noise[j*cols+i] = z < 256 ? z : 255; - z = (1.f-edge)*256.f; - edges[j*cols+i] = z > 0 ? (z < 256 ? z : 255) : 0; - } - } - - // noise areas are shrunk and then expanded to remove thin edges from the map - liq_max3(noise, tmp, cols, rows); - liq_max3(tmp, noise, cols, rows); - - liq_blur(noise, tmp, noise, cols, rows, 3); - - liq_max3(noise, tmp, cols, rows); - - liq_min3(tmp, noise, cols, rows); - liq_min3(noise, tmp, cols, rows); - liq_min3(tmp, noise, cols, rows); - - liq_min3(edges, tmp, cols, rows); - liq_max3(tmp, edges, cols, rows); - for(unsigned int i=0; i < cols*rows; i++) edges[i] = MIN(noise[i], edges[i]); - - image->free(tmp); - - image->noise = noise; - image->edges = edges; -} - -/** - * Builds map of neighbor pixels mapped to the same palette entry - * - * For efficiency/simplicity it mainly looks for same consecutive pixels horizontally - * and peeks 1 pixel above/below. Full 2d algorithm doesn't improve it significantly. - * Correct flood fill doesn't have visually good properties. - */ -LIQ_NONNULL static void update_dither_map(unsigned char *const *const row_pointers, liq_image *input_image) -{ - const unsigned int width = input_image->width; - const unsigned int height = input_image->height; - unsigned char *const edges = input_image->edges; - - for(unsigned int row=0; row < height; row++) { - unsigned char lastpixel = row_pointers[row][0]; - unsigned int lastcol=0; - - for(unsigned int col=1; col < width; col++) { - const unsigned char px = row_pointers[row][col]; - - if (px != lastpixel || col == width-1) { - int neighbor_count = 10 * (col-lastcol); - - unsigned int i=lastcol; - while(i < col) { - if (row > 0) { - unsigned char pixelabove = row_pointers[row-1][i]; - if (pixelabove == lastpixel) neighbor_count += 15; - } - if (row < height-1) { - unsigned char pixelbelow = row_pointers[row+1][i]; - if (pixelbelow == lastpixel) neighbor_count += 15; - } - i++; - } - - while(lastcol <= col) { - int e = edges[row*width + lastcol]; - edges[row*width + lastcol++] = (e+128) * (255.f/(255+128)) * (1.f - 20.f / (20 + neighbor_count)); - } - lastpixel = px; - } - } - } - input_image->dither_map = input_image->edges; - input_image->edges = NULL; -} - -/** - * Palette can be NULL, in which case it creates a new palette from scratch. - */ -static colormap *add_fixed_colors_to_palette(colormap *palette, const int max_colors, const f_pixel fixed_colors[], const int fixed_colors_count, void* (*malloc)(size_t), void (*free)(void*)) -{ - if (!fixed_colors_count) return palette; - - colormap *newpal = pam_colormap(MIN(max_colors, (palette ? palette->colors : 0) + fixed_colors_count), malloc, free); - unsigned int i=0; - if (palette && fixed_colors_count < max_colors) { - unsigned int palette_max = MIN(palette->colors, max_colors - fixed_colors_count); - for(; i < palette_max; i++) { - newpal->palette[i] = palette->palette[i]; - } - } - for(int j=0; j < MIN(max_colors, fixed_colors_count); j++) { - newpal->palette[i++] = (colormap_item){ - .acolor = fixed_colors[j], - .fixed = true, - }; - } - if (palette) pam_freecolormap(palette); - return newpal; -} - -LIQ_NONNULL static void adjust_histogram_callback(hist_item *item, float diff) -{ - item->adjusted_weight = (item->perceptual_weight+item->adjusted_weight) * (sqrtf(1.f+diff)); -} - -/** - Repeats mediancut with different histogram weights to find palette with minimum error. - - feedback_loop_trials controls how long the search will take. < 0 skips the iteration. - */ -static colormap *find_best_palette(histogram *hist, const liq_attr *options, const double max_mse, const f_pixel fixed_colors[], const unsigned int fixed_colors_count, double *palette_error_p) -{ - unsigned int max_colors = options->max_colors; - - // if output is posterized it doesn't make sense to aim for perfrect colors, so increase target_mse - // at this point actual gamma is not set, so very conservative posterization estimate is used - const double target_mse = MIN(max_mse, MAX(options->target_mse, pow((1<min_posterization_output)/1024.0, 2))); - int feedback_loop_trials = options->feedback_loop_trials; - colormap *acolormap = NULL; - double least_error = MAX_DIFF; - double target_mse_overshoot = feedback_loop_trials>0 ? 1.05 : 1.0; - const float total_trials = (float)(feedback_loop_trials>0?feedback_loop_trials:1); - - do { - colormap *newmap; - if (hist->size && fixed_colors_count < max_colors) { - newmap = mediancut(hist, max_colors-fixed_colors_count, target_mse * target_mse_overshoot, MAX(MAX(45.0/65536.0, target_mse), least_error)*1.2, - options->malloc, options->free); - } else { - feedback_loop_trials = 0; - newmap = NULL; - } - newmap = add_fixed_colors_to_palette(newmap, max_colors, fixed_colors, fixed_colors_count, options->malloc, options->free); - if (!newmap) { - return NULL; - } - - if (feedback_loop_trials <= 0) { - return newmap; - } - - // after palette has been created, total error (MSE) is calculated to keep the best palette - // at the same time K-Means iteration is done to improve the palette - // and histogram weights are adjusted based on remapping error to give more weight to poorly matched colors - - const bool first_run_of_target_mse = !acolormap && target_mse > 0; - double total_error = kmeans_do_iteration(hist, newmap, first_run_of_target_mse ? NULL : adjust_histogram_callback); - - // goal is to increase quality or to reduce number of colors used if quality is good enough - if (!acolormap || total_error < least_error || (total_error <= target_mse && newmap->colors < max_colors)) { - if (acolormap) pam_freecolormap(acolormap); - acolormap = newmap; - - if (total_error < target_mse && total_error > 0) { - // K-Means iteration improves quality above what mediancut aims for - // this compensates for it, making mediancut aim for worse - target_mse_overshoot = MIN(target_mse_overshoot*1.25, target_mse/total_error); - } - - least_error = total_error; - - // if number of colors could be reduced, try to keep it that way - // but allow extra color as a bit of wiggle room in case quality can be improved too - max_colors = MIN(newmap->colors+1, max_colors); - - feedback_loop_trials -= 1; // asymptotic improvement could make it go on forever - } else { - for(unsigned int j=0; j < hist->size; j++) { - hist->achv[j].adjusted_weight = (hist->achv[j].perceptual_weight + hist->achv[j].adjusted_weight)/2.0; - } - - target_mse_overshoot = 1.0; - feedback_loop_trials -= 6; - // if error is really bad, it's unlikely to improve, so end sooner - if (total_error > least_error*4) feedback_loop_trials -= 3; - pam_freecolormap(newmap); - } - - float fraction_done = 1.f-MAX(0.f, feedback_loop_trials/total_trials); - if (liq_progress(options, options->progress_stage1 + fraction_done * options->progress_stage2)) break; - liq_verbose_printf(options, " selecting colors...%d%%", (int)(100.f * fraction_done)); - } - while(feedback_loop_trials > 0); - - *palette_error_p = least_error; - return acolormap; -} - -static colormap *histogram_to_palette(const histogram *hist, const liq_attr *options) { - if (!hist->size) { - return NULL; - } - colormap *acolormap = pam_colormap(hist->size, options->malloc, options->free); - for(unsigned int i=0; i < hist->size; i++) { - acolormap->palette[i].acolor = hist->achv[i].acolor; - acolormap->palette[i].popularity = hist->achv[i].perceptual_weight; - } - return acolormap; -} - -LIQ_NONNULL static liq_error pngquant_quantize(histogram *hist, const liq_attr *options, const int fixed_colors_count, const f_pixel fixed_colors[], const double gamma, bool fixed_result_colors, liq_result **result_output) -{ - colormap *acolormap; - double palette_error = -1; - - assert((verbose_print(options, "SLOW debug checks enabled. Recompile with NDEBUG for normal operation."),1)); - - const bool few_input_colors = hist->size+fixed_colors_count <= options->max_colors; - - if (liq_progress(options, options->progress_stage1)) return LIQ_ABORTED; - - // If image has few colors to begin with (and no quality degradation is required) - // then it's possible to skip quantization entirely - if (few_input_colors && options->target_mse == 0) { - acolormap = add_fixed_colors_to_palette(histogram_to_palette(hist, options), options->max_colors, fixed_colors, fixed_colors_count, options->malloc, options->free); - palette_error = 0; - } else { - const double max_mse = options->max_mse * (few_input_colors ? 0.33 : 1.0); // when degrading image that's already paletted, require much higher improvement, since pal2pal often looks bad and there's little gain - acolormap = find_best_palette(hist, options, max_mse, fixed_colors, fixed_colors_count, &palette_error); - if (!acolormap) { - return LIQ_VALUE_OUT_OF_RANGE; - } - - // K-Means iteration approaches local minimum for the palette - const double iteration_limit = options->kmeans_iteration_limit; - unsigned int iterations = options->kmeans_iterations; - - if (!iterations && palette_error < 0 && max_mse < MAX_DIFF) iterations = 1; // otherwise total error is never calculated and MSE limit won't work - - if (iterations) { - // likely_colormap_index (used and set in kmeans_do_iteration) can't point to index outside colormap - if (acolormap->colors < 256) for(unsigned int j=0; j < hist->size; j++) { - if (hist->achv[j].tmp.likely_colormap_index >= acolormap->colors) { - hist->achv[j].tmp.likely_colormap_index = 0; // actual value doesn't matter, as the guess is out of date anyway - } - } - - verbose_print(options, " moving colormap towards local minimum"); - - double previous_palette_error = MAX_DIFF; - - for(unsigned int i=0; i < iterations; i++) { - palette_error = kmeans_do_iteration(hist, acolormap, NULL); - - if (liq_progress(options, options->progress_stage1 + options->progress_stage2 + (i * options->progress_stage3 * 0.9f) / iterations)) { - break; - } - - if (fabs(previous_palette_error-palette_error) < iteration_limit) { - break; - } - - if (palette_error > max_mse*1.5) { // probably hopeless - if (palette_error > max_mse*3.0) break; // definitely hopeless - i++; - } - - previous_palette_error = palette_error; - } - } - - if (palette_error > max_mse) { - liq_verbose_printf(options, " image degradation MSE=%.3f (Q=%d) exceeded limit of %.3f (%d)", - mse_to_standard_mse(palette_error), mse_to_quality(palette_error), - mse_to_standard_mse(max_mse), mse_to_quality(max_mse)); - pam_freecolormap(acolormap); - return LIQ_QUALITY_TOO_LOW; - } - } - - if (liq_progress(options, options->progress_stage1 + options->progress_stage2 + options->progress_stage3 * 0.95f)) { - pam_freecolormap(acolormap); - return LIQ_ABORTED; - } - - sort_palette(acolormap, options); - - // If palette was created from a multi-image histogram, - // then it shouldn't be optimized for one image during remapping - if (fixed_result_colors) { - for(unsigned int i=0; i < acolormap->colors; i++) { - acolormap->palette[i].fixed = true; - } - } - - liq_result *result = options->malloc(sizeof(liq_result)); - if (!result) return LIQ_OUT_OF_MEMORY; - *result = (liq_result){ - .magic_header = liq_result_magic, - .malloc = options->malloc, - .free = options->free, - .palette = acolormap, - .palette_error = palette_error, - .use_dither_map = options->use_dither_map, - .gamma = gamma, - .min_posterization_output = options->min_posterization_output, - }; - *result_output = result; - return LIQ_OK; -} - -LIQ_EXPORT LIQ_NONNULL liq_error liq_write_remapped_image(liq_result *result, liq_image *input_image, void *buffer, size_t buffer_size) -{ - if (!CHECK_STRUCT_TYPE(result, liq_result)) { - return LIQ_INVALID_POINTER; - } - if (!CHECK_STRUCT_TYPE(input_image, liq_image)) { - return LIQ_INVALID_POINTER; - } - if (!CHECK_USER_POINTER(buffer)) { - return LIQ_INVALID_POINTER; - } - - const size_t required_size = input_image->width * input_image->height; - if (buffer_size < required_size) { - return LIQ_BUFFER_TOO_SMALL; - } - - unsigned char *rows[input_image->height]; - unsigned char *buffer_bytes = buffer; - for(unsigned int i=0; i < input_image->height; i++) { - rows[i] = &buffer_bytes[input_image->width * i]; - } - return liq_write_remapped_image_rows(result, input_image, rows); -} - -LIQ_EXPORT LIQ_NONNULL liq_error liq_write_remapped_image_rows(liq_result *quant, liq_image *input_image, unsigned char **row_pointers) -{ - if (!CHECK_STRUCT_TYPE(quant, liq_result)) return LIQ_INVALID_POINTER; - if (!CHECK_STRUCT_TYPE(input_image, liq_image)) return LIQ_INVALID_POINTER; - for(unsigned int i=0; i < input_image->height; i++) { - if (!CHECK_USER_POINTER(row_pointers+i) || !CHECK_USER_POINTER(row_pointers[i])) return LIQ_INVALID_POINTER; - } - - if (quant->remapping) { - liq_remapping_result_destroy(quant->remapping); - } - liq_remapping_result *const result = quant->remapping = liq_remapping_result_create(quant); - if (!result) return LIQ_OUT_OF_MEMORY; - - if (!input_image->edges && !input_image->dither_map && quant->use_dither_map) { - contrast_maps(input_image); - } - - if (liq_remap_progress(result, result->progress_stage1 * 0.25f)) { - return LIQ_ABORTED; - } - - /* - ** Step 4: map the colors in the image to their closest match in the - ** new colormap, and write 'em out. - */ - - float remapping_error = result->palette_error; - if (result->dither_level == 0) { - set_rounded_palette(&result->int_palette, result->palette, result->gamma, quant->min_posterization_output); - remapping_error = remap_to_palette(input_image, row_pointers, result->palette); - } else { - const bool generate_dither_map = result->use_dither_map && (input_image->edges && !input_image->dither_map); - if (generate_dither_map) { - // If dithering (with dither map) is required, this image is used to find areas that require dithering - remapping_error = remap_to_palette(input_image, row_pointers, result->palette); - update_dither_map(row_pointers, input_image); - } - - if (liq_remap_progress(result, result->progress_stage1 * 0.5f)) { - return LIQ_ABORTED; - } - - // remapping above was the last chance to do K-Means iteration, hence the final palette is set after remapping - set_rounded_palette(&result->int_palette, result->palette, result->gamma, quant->min_posterization_output); - - if (!remap_to_palette_floyd(input_image, row_pointers, result, MAX(remapping_error*2.4, 16.f/256.f), generate_dither_map)) { - return LIQ_ABORTED; - } - } - - // remapping error from dithered image is absurd, so always non-dithered value is used - // palette_error includes some perceptual weighting from histogram which is closer correlated with dssim - // so that should be used when possible. - if (result->palette_error < 0) { - result->palette_error = remapping_error; - } - - return LIQ_OK; -} - -LIQ_EXPORT int liq_version() { - return LIQ_VERSION; -} diff --git a/mediancut.c b/mediancut.c deleted file mode 100644 index 6e3e590..0000000 --- a/mediancut.c +++ /dev/null @@ -1,470 +0,0 @@ -/* -** © 2009-2015 by Kornel Lesiński. -** -** This file is part of libimagequant. -** -** libimagequant is free software: you can redistribute it and/or modify -** it under the terms of the GNU General Public License as published by -** the Free Software Foundation, either version 3 of the License, or -** (at your option) any later version. -** -** libimagequant is distributed in the hope that it will be useful, -** but WITHOUT ANY WARRANTY; without even the implied warranty of -** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -** GNU General Public License for more details. -** -** You should have received a copy of the GNU General Public License -** along with libimagequant. If not, see . -*/ -/* -** Copyright (C) 1989, 1991 by Jef Poskanzer. -** Copyright (C) 1997, 2000, 2002 by Greg Roelofs; based on an idea by -** Stefan Schneider. -** -** Permission to use, copy, modify, and distribute this software and its -** documentation for any purpose and without fee is hereby granted, provided -** that the above copyright notice appear in all copies and that both that -** copyright notice and this permission notice appear in supporting -** documentation. This software is provided "as is" without express or -** implied warranty. -*/ - -#include -#include - -#include "libimagequant.h" -#include "pam.h" -#include "mediancut.h" - -#define index_of_channel(ch) (offsetof(f_pixel,ch)/sizeof(float)) - -static f_pixel averagepixels(unsigned int clrs, const hist_item achv[]); - -struct box { - f_pixel color; - f_pixel variance; - double sum, total_error, max_error; - unsigned int ind; - unsigned int colors; -}; - -ALWAYS_INLINE static double variance_diff(double val, const double good_enough); -inline static double variance_diff(double val, const double good_enough) -{ - val *= val; - if (val < good_enough*good_enough) return val*0.25; - return val; -} - -/** Weighted per-channel variance of the box. It's used to decide which channel to split by */ -static f_pixel box_variance(const hist_item achv[], const struct box *box) -{ - f_pixel mean = box->color; - double variancea=0, variancer=0, varianceg=0, varianceb=0; - - for(unsigned int i = 0; i < box->colors; ++i) { - const f_pixel px = achv[box->ind + i].acolor; - double weight = achv[box->ind + i].adjusted_weight; - variancea += variance_diff(mean.a - px.a, 2.0/256.0)*weight; - variancer += variance_diff(mean.r - px.r, 1.0/256.0)*weight; - varianceg += variance_diff(mean.g - px.g, 1.0/256.0)*weight; - varianceb += variance_diff(mean.b - px.b, 1.0/256.0)*weight; - } - - return (f_pixel){ - .a = variancea*(4.0/16.0), - .r = variancer*(7.0/16.0), - .g = varianceg*(9.0/16.0), - .b = varianceb*(5.0/16.0), - }; -} - -static double box_max_error(const hist_item achv[], const struct box *box) -{ - f_pixel mean = box->color; - double max_error = 0; - - for(unsigned int i = 0; i < box->colors; ++i) { - const double diff = colordifference(mean, achv[box->ind + i].acolor); - if (diff > max_error) { - max_error = diff; - } - } - return max_error; -} - -ALWAYS_INLINE static double color_weight(f_pixel median, hist_item h); - -static inline void hist_item_swap(hist_item *l, hist_item *r) -{ - if (l != r) { - hist_item t = *l; - *l = *r; - *r = t; - } -} - -ALWAYS_INLINE static unsigned int qsort_pivot(const hist_item *const base, const unsigned int len); -inline static unsigned int qsort_pivot(const hist_item *const base, const unsigned int len) -{ - if (len < 32) { - return len/2; - } - - const unsigned int aidx=8, bidx=len/2, cidx=len-1; - const unsigned int a=base[aidx].tmp.sort_value, b=base[bidx].tmp.sort_value, c=base[cidx].tmp.sort_value; - return (a < b) ? ((b < c) ? bidx : ((a < c) ? cidx : aidx )) - : ((b > c) ? bidx : ((a < c) ? aidx : cidx )); -} - -ALWAYS_INLINE static unsigned int qsort_partition(hist_item *const base, const unsigned int len); -inline static unsigned int qsort_partition(hist_item *const base, const unsigned int len) -{ - unsigned int l = 1, r = len; - if (len >= 8) { - hist_item_swap(&base[0], &base[qsort_pivot(base,len)]); - } - - const unsigned int pivot_value = base[0].tmp.sort_value; - while (l < r) { - if (base[l].tmp.sort_value >= pivot_value) { - l++; - } else { - while(l < --r && base[r].tmp.sort_value <= pivot_value) {} - hist_item_swap(&base[l], &base[r]); - } - } - l--; - hist_item_swap(&base[0], &base[l]); - - return l; -} - -/** quick select algorithm */ -static void hist_item_sort_range(hist_item *base, unsigned int len, unsigned int sort_start) -{ - for(;;) { - const unsigned int l = qsort_partition(base, len), r = l+1; - - if (l > 0 && sort_start < l) { - len = l; - } - else if (r < len && sort_start > r) { - base += r; len -= r; sort_start -= r; - } - else break; - } -} - -/** sorts array to make sum of weights lower than halfvar one side, returns edge between halfvar parts of the set */ -static hist_item *hist_item_sort_halfvar(hist_item *base, unsigned int len, double *const lowervar, const double halfvar) -{ - do { - const unsigned int l = qsort_partition(base, len), r = l+1; - - // check if sum of left side is smaller than half, - // if it is, then it doesn't need to be sorted - unsigned int t = 0; double tmpsum = *lowervar; - while (t <= l && tmpsum < halfvar) tmpsum += base[t++].color_weight; - - if (tmpsum < halfvar) { - *lowervar = tmpsum; - } else { - if (l > 0) { - hist_item *res = hist_item_sort_halfvar(base, l, lowervar, halfvar); - if (res) return res; - } else { - // End of left recursion. This will be executed in order from the first element. - *lowervar += base[0].color_weight; - if (*lowervar > halfvar) return &base[0]; - } - } - - if (len > r) { - base += r; len -= r; // tail-recursive "call" - } else { - *lowervar += base[r].color_weight; - return (*lowervar > halfvar) ? &base[r] : NULL; - } - } while(1); -} - -static f_pixel get_median(const struct box *b, hist_item achv[]); - -typedef struct { - unsigned int chan; float variance; -} channelvariance; - -static int comparevariance(const void *ch1, const void *ch2) -{ - return ((const channelvariance*)ch1)->variance > ((const channelvariance*)ch2)->variance ? -1 : - (((const channelvariance*)ch1)->variance < ((const channelvariance*)ch2)->variance ? 1 : 0); -} - -/** Finds which channels need to be sorted first and preproceses achv for fast sort */ -static double prepare_sort(struct box *b, hist_item achv[]) -{ - /* - ** Sort dimensions by their variance, and then sort colors first by dimension with highest variance - */ - channelvariance channels[4] = { - {index_of_channel(a), b->variance.a}, - {index_of_channel(r), b->variance.r}, - {index_of_channel(g), b->variance.g}, - {index_of_channel(b), b->variance.b}, - }; - - qsort(channels, 4, sizeof(channels[0]), comparevariance); - - for(unsigned int i=0; i < b->colors; i++) { - const float *chans = (const float *)&achv[b->ind + i].acolor; - // Only the first channel really matters. When trying median cut many times - // with different histogram weights, I don't want sort randomness to influence outcome. - achv[b->ind + i].tmp.sort_value = ((unsigned int)(chans[channels[0].chan]*65535.0)<<16) | - (unsigned int)((chans[channels[2].chan] + chans[channels[1].chan]/2.0 + chans[channels[3].chan]/4.0)*65535.0); - } - - const f_pixel median = get_median(b, achv); - - // box will be split to make color_weight of each side even - const unsigned int ind = b->ind, end = ind+b->colors; - double totalvar = 0; - for(unsigned int j=ind; j < end; j++) totalvar += (achv[j].color_weight = color_weight(median, achv[j])); - return totalvar / 2.0; -} - -/** finds median in unsorted set by sorting only minimum required */ -static f_pixel get_median(const struct box *b, hist_item achv[]) -{ - const unsigned int median_start = (b->colors-1)/2; - - hist_item_sort_range(&(achv[b->ind]), b->colors, - median_start); - - if (b->colors&1) return achv[b->ind + median_start].acolor; - - // technically the second color is not guaranteed to be sorted correctly - // but most of the time it is good enough to be useful - return averagepixels(2, &achv[b->ind + median_start]); -} - -/* - ** Find the best splittable box. -1 if no boxes are splittable. - */ -static int best_splittable_box(struct box* bv, unsigned int boxes, const double max_mse) -{ - int bi=-1; double maxsum=0; - for(unsigned int i=0; i < boxes; i++) { - if (bv[i].colors < 2) { - continue; - } - - // looks only at max variance, because it's only going to split by it - const double cv = MAX(bv[i].variance.r, MAX(bv[i].variance.g,bv[i].variance.b)); - double thissum = bv[i].sum * MAX(bv[i].variance.a, cv); - - if (bv[i].max_error > max_mse) { - thissum = thissum* bv[i].max_error/max_mse; - } - - if (thissum > maxsum) { - maxsum = thissum; - bi = i; - } - } - return bi; -} - -inline static double color_weight(f_pixel median, hist_item h) -{ - float diff = colordifference(median, h.acolor); - return sqrt(diff) * (sqrt(1.0+h.adjusted_weight)-1.0); -} - -static void set_colormap_from_boxes(colormap *map, struct box* bv, unsigned int boxes, hist_item *achv); -static void adjust_histogram(hist_item *achv, const struct box* bv, unsigned int boxes); - -static double box_error(const struct box *box, const hist_item achv[]) -{ - f_pixel avg = box->color; - - double total_error=0; - for (unsigned int i = 0; i < box->colors; ++i) { - total_error += colordifference(avg, achv[box->ind + i].acolor) * achv[box->ind + i].perceptual_weight; - } - - return total_error; -} - - -static bool total_box_error_below_target(double target_mse, struct box bv[], unsigned int boxes, const histogram *hist) -{ - target_mse *= hist->total_perceptual_weight; - double total_error=0; - - for(unsigned int i=0; i < boxes; i++) { - // error is (re)calculated lazily - if (bv[i].total_error >= 0) { - total_error += bv[i].total_error; - } - if (total_error > target_mse) return false; - } - - for(unsigned int i=0; i < boxes; i++) { - if (bv[i].total_error < 0) { - bv[i].total_error = box_error(&bv[i], hist->achv); - total_error += bv[i].total_error; - } - if (total_error > target_mse) return false; - } - - return true; -} - -static void box_init(struct box *box, const hist_item *achv, const unsigned int ind, const unsigned int colors, const double sum) { - box->ind = ind; - box->colors = colors; - box->sum = sum; - box->total_error = -1; - box->color = averagepixels(colors, &achv[ind]); - box->variance = box_variance(achv, box); - box->max_error = box_max_error(achv, box); -} - -/* - ** Here is the fun part, the median-cut colormap generator. This is based - ** on Paul Heckbert's paper, "Color Image Quantization for Frame Buffer - ** Display," SIGGRAPH 1982 Proceedings, page 297. - */ -LIQ_PRIVATE colormap *mediancut(histogram *hist, unsigned int newcolors, const double target_mse, const double max_mse, void* (*malloc)(size_t), void (*free)(void*)) -{ - hist_item *achv = hist->achv; - struct box bv[newcolors]; - - /* - ** Set up the initial box. - */ - double sum = 0; - for(unsigned int i=0; i < hist->size; i++) { - sum += achv[i].adjusted_weight; - } - box_init(&bv[0], achv, 0, hist->size, sum); - - unsigned int boxes = 1; - - /* - ** Main loop: split boxes until we have enough. - */ - while (boxes < newcolors) { - - // first splits boxes that exceed quality limit (to have colors for things like odd green pixel), - // later raises the limit to allow large smooth areas/gradients get colors. - const double current_max_mse = max_mse + (boxes/(double)newcolors)*16.0*max_mse; - const int bi = best_splittable_box(bv, boxes, current_max_mse); - if (bi < 0) - break; /* ran out of colors! */ - - unsigned int indx = bv[bi].ind; - unsigned int clrs = bv[bi].colors; - - /* - Classic implementation tries to get even number of colors or pixels in each subdivision. - - Here, instead of popularity I use (sqrt(popularity)*variance) metric. - Each subdivision balances number of pixels (popular colors) and low variance - - boxes can be large if they have similar colors. Later boxes with high variance - will be more likely to be split. - - Median used as expected value gives much better results than mean. - */ - - const double halfvar = prepare_sort(&bv[bi], achv); - double lowervar=0; - - // hist_item_sort_halfvar sorts and sums lowervar at the same time - // returns item to break at …minus one, which does smell like an off-by-one error. - hist_item *break_p = hist_item_sort_halfvar(&achv[indx], clrs, &lowervar, halfvar); - unsigned int break_at = MIN(clrs-1, break_p - &achv[indx] + 1); - - /* - ** Split the box. - */ - double sm = bv[bi].sum; - double lowersum = 0; - for(unsigned int i=0; i < break_at; i++) lowersum += achv[indx + i].adjusted_weight; - - box_init(&bv[bi], achv, bv[bi].ind, break_at, lowersum); - box_init(&bv[boxes], achv, indx + break_at, clrs - break_at, sm - lowersum); - - ++boxes; - - if (total_box_error_below_target(target_mse, bv, boxes, hist)) { - break; - } - } - - colormap *map = pam_colormap(boxes, malloc, free); - set_colormap_from_boxes(map, bv, boxes, achv); - - adjust_histogram(achv, bv, boxes); - - return map; -} - -static void set_colormap_from_boxes(colormap *map, struct box* bv, unsigned int boxes, hist_item *achv) -{ - /* - ** Ok, we've got enough boxes. Now choose a representative color for - ** each box. There are a number of possible ways to make this choice. - ** One would be to choose the center of the box; this ignores any structure - ** within the boxes. Another method would be to average all the colors in - ** the box - this is the method specified in Heckbert's paper. - */ - - for(unsigned int bi = 0; bi < boxes; ++bi) { - map->palette[bi].acolor = bv[bi].color; - - /* store total color popularity (perceptual_weight is approximation of it) */ - map->palette[bi].popularity = 0; - for(unsigned int i=bv[bi].ind; i < bv[bi].ind+bv[bi].colors; i++) { - map->palette[bi].popularity += achv[i].perceptual_weight; - } - } -} - -/* increase histogram popularity by difference from the final color (this is used as part of feedback loop) */ -static void adjust_histogram(hist_item *achv, const struct box* bv, unsigned int boxes) -{ - for(unsigned int bi = 0; bi < boxes; ++bi) { - for(unsigned int i=bv[bi].ind; i < bv[bi].ind+bv[bi].colors; i++) { - achv[i].tmp.likely_colormap_index = bi; - } - } -} - -static f_pixel averagepixels(unsigned int clrs, const hist_item achv[]) -{ - double r = 0, g = 0, b = 0, a = 0, sum = 0; - - for(unsigned int i = 0; i < clrs; i++) { - const f_pixel px = achv[i].acolor; - const double weight = achv[i].adjusted_weight; - - sum += weight; - a += px.a * weight; - r += px.r * weight; - g += px.g * weight; - b += px.b * weight; - } - - if (sum) { - a /= sum; - r /= sum; - g /= sum; - b /= sum; - } - - assert(!isnan(r) && !isnan(g) && !isnan(b) && !isnan(a)); - - return (f_pixel){.r=r, .g=g, .b=b, .a=a}; -} diff --git a/mediancut.h b/mediancut.h deleted file mode 100644 index d97696c..0000000 --- a/mediancut.h +++ /dev/null @@ -1,2 +0,0 @@ - -LIQ_PRIVATE colormap *mediancut(histogram *hist, unsigned int newcolors, const double target_mse, const double max_mse, void* (*malloc)(size_t), void (*free)(void*)); diff --git a/mempool.c b/mempool.c deleted file mode 100644 index cd345a7..0000000 --- a/mempool.c +++ /dev/null @@ -1,81 +0,0 @@ -/* -© 2011-2016 by Kornel Lesiński. - -This file is part of libimagequant. - -libimagequant is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -libimagequant is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with libimagequant. If not, see . -*/ - -#include "libimagequant.h" -#include "mempool.h" -#include -#include -#include - -#define ALIGN_MASK 15UL -#define MEMPOOL_RESERVED ((sizeof(struct mempool)+ALIGN_MASK) & ~ALIGN_MASK) - -struct mempool { - unsigned int used, size; - void* (*malloc)(size_t); - void (*free)(void*); - struct mempool *next; -}; -LIQ_PRIVATE void* mempool_create(mempool *mptr, const unsigned int size, unsigned int max_size, void* (*malloc)(size_t), void (*free)(void*)) -{ - if (*mptr && ((*mptr)->used+size) <= (*mptr)->size) { - unsigned int prevused = (*mptr)->used; - (*mptr)->used += (size+15UL) & ~0xFUL; - return ((char*)(*mptr)) + prevused; - } - - mempool old = *mptr; - if (!max_size) max_size = (1<<17); - max_size = size+ALIGN_MASK > max_size ? size+ALIGN_MASK : max_size; - - *mptr = malloc(MEMPOOL_RESERVED + max_size); - if (!*mptr) return NULL; - **mptr = (struct mempool){ - .malloc = malloc, - .free = free, - .size = MEMPOOL_RESERVED + max_size, - .used = sizeof(struct mempool), - .next = old, - }; - uintptr_t mptr_used_start = (uintptr_t)(*mptr) + (*mptr)->used; - (*mptr)->used += (ALIGN_MASK + 1 - (mptr_used_start & ALIGN_MASK)) & ALIGN_MASK; // reserve bytes required to make subsequent allocations aligned - assert(!(((uintptr_t)(*mptr) + (*mptr)->used) & ALIGN_MASK)); - - return mempool_alloc(mptr, size, size); -} - -LIQ_PRIVATE void* mempool_alloc(mempool *mptr, const unsigned int size, const unsigned int max_size) -{ - if (((*mptr)->used+size) <= (*mptr)->size) { - unsigned int prevused = (*mptr)->used; - (*mptr)->used += (size + ALIGN_MASK) & ~ALIGN_MASK; - return ((char*)(*mptr)) + prevused; - } - - return mempool_create(mptr, size, max_size, (*mptr)->malloc, (*mptr)->free); -} - -LIQ_PRIVATE void mempool_destroy(mempool m) -{ - while (m) { - mempool next = m->next; - m->free(m); - m = next; - } -} diff --git a/mempool.h b/mempool.h deleted file mode 100644 index 0797e7c..0000000 --- a/mempool.h +++ /dev/null @@ -1,13 +0,0 @@ -#ifndef MEMPOOL_H -#define MEMPOOL_H - -#include - -struct mempool; -typedef struct mempool *mempool; - -LIQ_PRIVATE void* mempool_create(mempool *mptr, const unsigned int size, unsigned int capacity, void* (*malloc)(size_t), void (*free)(void*)); -LIQ_PRIVATE void* mempool_alloc(mempool *mptr, const unsigned int size, const unsigned int capacity); -LIQ_PRIVATE void mempool_destroy(mempool m); - -#endif diff --git a/nearest.c b/nearest.c deleted file mode 100644 index 2f1e926..0000000 --- a/nearest.c +++ /dev/null @@ -1,206 +0,0 @@ -/* -© 2011-2015 by Kornel Lesiński. - -This file is part of libimagequant. - -libimagequant is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -libimagequant is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with libimagequant. If not, see . -*/ - -#include "libimagequant.h" -#include "pam.h" -#include "nearest.h" -#include "mempool.h" -#include - -typedef struct vp_sort_tmp { - float distance_squared; - unsigned int idx; -} vp_sort_tmp; - -typedef struct vp_search_tmp { - float distance; - unsigned int idx; - int exclude; -} vp_search_tmp; - -typedef struct vp_node { - struct vp_node *near, *far; - f_pixel vantage_point; - float radius; - unsigned int idx; -} vp_node; - -struct nearest_map { - vp_node *root; - const colormap_item *palette; - float nearest_other_color_dist[256]; - mempool mempool; -}; - -static void vp_search_node(const vp_node *node, const f_pixel *const needle, vp_search_tmp *const best_candidate); - -static int vp_compare_distance(const void *ap, const void *bp) { - float a = ((const vp_sort_tmp*)ap)->distance_squared; - float b = ((const vp_sort_tmp*)bp)->distance_squared; - return a > b ? 1 : -1; -} - -static void vp_sort_indexes_by_distance(const f_pixel vantage_point, vp_sort_tmp *indexes, int num_indexes, const colormap_item items[]) { - for(int i=0; i < num_indexes; i++) { - indexes[i].distance_squared = colordifference(vantage_point, items[indexes[i].idx].acolor); - } - qsort(indexes, num_indexes, sizeof(indexes[0]), vp_compare_distance); -} - -/* - * Usually it should pick farthest point, but picking most popular point seems to make search quicker anyway - */ -static int vp_find_best_vantage_point_index(vp_sort_tmp *indexes, int num_indexes, const colormap_item items[]) { - int best = 0; - float best_popularity = items[indexes[0].idx].popularity; - for(int i = 1; i < num_indexes; i++) { - if (items[indexes[i].idx].popularity > best_popularity) { - best_popularity = items[indexes[i].idx].popularity; - best = i; - } - } - return best; -} - -static vp_node *vp_create_node(mempool *m, vp_sort_tmp *indexes, int num_indexes, const colormap_item items[]) { - if (num_indexes <= 0) { - return NULL; - } - - vp_node *node = mempool_alloc(m, sizeof(node[0]), 0); - - if (num_indexes == 1) { - *node = (vp_node){ - .vantage_point = items[indexes[0].idx].acolor, - .idx = indexes[0].idx, - .radius = MAX_DIFF, - }; - return node; - } - - const int ref = vp_find_best_vantage_point_index(indexes, num_indexes, items); - const int ref_idx = indexes[ref].idx; - - // Removes the `ref_idx` item from remaining items, because it's included in the current node - num_indexes -= 1; - indexes[ref] = indexes[num_indexes]; - - vp_sort_indexes_by_distance(items[ref_idx].acolor, indexes, num_indexes, items); - - // Remaining items are split by the median distance - const int half_idx = num_indexes/2; - - *node = (vp_node){ - .vantage_point = items[ref_idx].acolor, - .idx = ref_idx, - .radius = sqrtf(indexes[half_idx].distance_squared), - }; - node->near = vp_create_node(m, indexes, half_idx, items); - node->far = vp_create_node(m, &indexes[half_idx], num_indexes - half_idx, items); - - return node; -} - -LIQ_PRIVATE struct nearest_map *nearest_init(const colormap *map) { - mempool m = NULL; - struct nearest_map *handle = mempool_create(&m, sizeof(handle[0]), sizeof(handle[0]) + sizeof(vp_node)*map->colors+16, map->malloc, map->free); - - vp_sort_tmp indexes[map->colors]; - - for(unsigned int i=0; i < map->colors; i++) { - indexes[i].idx = i; - } - - vp_node *root = vp_create_node(&m, indexes, map->colors, map->palette); - *handle = (struct nearest_map){ - .root = root, - .palette = map->palette, - .mempool = m, - }; - - for(unsigned int i=0; i < map->colors; i++) { - vp_search_tmp best = { - .distance = MAX_DIFF, - .exclude = i, - }; - vp_search_node(root, &map->palette[i].acolor, &best); - handle->nearest_other_color_dist[i] = best.distance * best.distance / 4.0; // half of squared distance - } - - return handle; -} - -static void vp_search_node(const vp_node *node, const f_pixel *const needle, vp_search_tmp *const best_candidate) { - do { - const float distance = sqrtf(colordifference(node->vantage_point, *needle)); - - if (distance < best_candidate->distance && best_candidate->exclude != node->idx) { - best_candidate->distance = distance; - best_candidate->idx = node->idx; - } - - // Recurse towards most likely candidate first to narrow best candidate's distance as soon as possible - if (distance < node->radius) { - if (node->near) { - vp_search_node(node->near, needle, best_candidate); - } - // The best node (final answer) may be just ouside the radius, but not farther than - // the best distance we know so far. The vp_search_node above should have narrowed - // best_candidate->distance, so this path is rarely taken. - if (node->far && distance >= node->radius - best_candidate->distance) { - node = node->far; // Fast tail recursion - } else { - break; - } - } else { - if (node->far) { - vp_search_node(node->far, needle, best_candidate); - } - if (node->near && distance <= node->radius + best_candidate->distance) { - node = node->near; // Fast tail recursion - } else { - break; - } - } - } while(true); -} - -LIQ_PRIVATE unsigned int nearest_search(const struct nearest_map *handle, const f_pixel *px, const int likely_colormap_index, float *diff) { - const float guess_diff = colordifference(handle->palette[likely_colormap_index].acolor, *px); - if (guess_diff < handle->nearest_other_color_dist[likely_colormap_index]) { - if (diff) *diff = guess_diff; - return likely_colormap_index; - } - - vp_search_tmp best_candidate = { - .distance = sqrtf(guess_diff), - .idx = likely_colormap_index, - .exclude = -1, - }; - vp_search_node(handle->root, px, &best_candidate); - if (diff) { - *diff = best_candidate.distance * best_candidate.distance; - } - return best_candidate.idx; -} - -LIQ_PRIVATE void nearest_free(struct nearest_map *centroids) -{ - mempool_destroy(centroids->mempool); -} diff --git a/nearest.h b/nearest.h deleted file mode 100644 index e20233b..0000000 --- a/nearest.h +++ /dev/null @@ -1,8 +0,0 @@ -// -// nearest.h -// pngquant -// -struct nearest_map; -LIQ_PRIVATE struct nearest_map *nearest_init(const colormap *palette); -LIQ_PRIVATE unsigned int nearest_search(const struct nearest_map *map, const f_pixel *px, const int palette_index_guess, float *diff); -LIQ_PRIVATE void nearest_free(struct nearest_map *map); diff --git a/pam.c b/pam.c deleted file mode 100644 index 74165e1..0000000 --- a/pam.c +++ /dev/null @@ -1,276 +0,0 @@ -/* pam.c - pam (portable alpha map) utility library -** -** Copyright (C) 1989, 1991 by Jef Poskanzer. -** Copyright (C) 1997, 2000, 2002 by Greg Roelofs; based on an idea by -** Stefan Schneider. -** © 2009-2016 by Kornel Lesinski. -** -** Permission to use, copy, modify, and distribute this software and its -** documentation for any purpose and without fee is hereby granted, provided -** that the above copyright notice appear in all copies and that both that -** copyright notice and this permission notice appear in supporting -** documentation. This software is provided "as is" without express or -** implied warranty. -*/ - -#include -#include - -#include "libimagequant.h" -#include "pam.h" -#include "mempool.h" - -LIQ_PRIVATE bool pam_computeacolorhash(struct acolorhash_table *acht, const rgba_pixel *const pixels[], unsigned int cols, unsigned int rows, const unsigned char *importance_map) -{ - const unsigned int ignorebits = acht->ignorebits; - const unsigned int channel_mask = 255U>>ignorebits<>ignorebits) ^ 0xFFU; - const unsigned int posterize_mask = channel_mask << 24 | channel_mask << 16 | channel_mask << 8 | channel_mask; - const unsigned int posterize_high_mask = channel_hmask << 24 | channel_hmask << 16 | channel_hmask << 8 | channel_hmask; - - const unsigned int hash_size = acht->hash_size; - - /* Go through the entire image, building a hash table of colors. */ - for(unsigned int row = 0; row < rows; ++row) { - - float boost=1.0; - for(unsigned int col = 0; col < cols; ++col) { - if (importance_map) { - boost = 0.5f+ (double)*importance_map++/255.f; - } - - // RGBA color is casted to long for easier hasing/comparisons - union rgba_as_int px = {pixels[row][col]}; - unsigned int hash; - if (!px.rgba.a) { - // "dirty alpha" has different RGBA values that end up being the same fully transparent color - px.l=0; hash=0; - boost = 10; - } else { - // mask posterizes all 4 channels in one go - px.l = (px.l & posterize_mask) | ((px.l & posterize_high_mask) >> (8-ignorebits)); - // fancier hashing algorithms didn't improve much - hash = px.l % hash_size; - } - - if (!pam_add_to_hash(acht, hash, boost, px, row, rows)) { - return false; - } - } - - } - acht->cols = cols; - acht->rows += rows; - return true; -} - -LIQ_PRIVATE bool pam_add_to_hash(struct acolorhash_table *acht, unsigned int hash, float boost, union rgba_as_int px, unsigned int row, unsigned int rows) -{ - /* head of the hash function stores first 2 colors inline (achl->used = 1..2), - to reduce number of allocations of achl->other_items. - */ - struct acolorhist_arr_head *achl = &acht->buckets[hash]; - if (achl->inline1.color.l == px.l && achl->used) { - achl->inline1.perceptual_weight += boost; - return true; - } - if (achl->used) { - if (achl->used > 1) { - if (achl->inline2.color.l == px.l) { - achl->inline2.perceptual_weight += boost; - return true; - } - // other items are stored as an array (which gets reallocated if needed) - struct acolorhist_arr_item *other_items = achl->other_items; - unsigned int i = 0; - for (; i < achl->used-2; i++) { - if (other_items[i].color.l == px.l) { - other_items[i].perceptual_weight += boost; - return true; - } - } - - // the array was allocated with spare items - if (i < achl->capacity) { - other_items[i] = (struct acolorhist_arr_item){ - .color = px, - .perceptual_weight = boost, - }; - achl->used++; - ++acht->colors; - return true; - } - - if (++acht->colors > acht->maxcolors) { - return false; - } - - struct acolorhist_arr_item *new_items; - unsigned int capacity; - if (!other_items) { // there was no array previously, alloc "small" array - capacity = 8; - if (acht->freestackp <= 0) { - // estimate how many colors are going to be + headroom - const size_t mempool_size = ((acht->rows + rows-row) * 2 * acht->colors / (acht->rows + row + 1) + 1024) * sizeof(struct acolorhist_arr_item); - new_items = mempool_alloc(&acht->mempool, sizeof(struct acolorhist_arr_item)*capacity, mempool_size); - } else { - // freestack stores previously freed (reallocated) arrays that can be reused - // (all pesimistically assumed to be capacity = 8) - new_items = acht->freestack[--acht->freestackp]; - } - } else { - const unsigned int stacksize = sizeof(acht->freestack)/sizeof(acht->freestack[0]); - - // simply reallocs and copies array to larger capacity - capacity = achl->capacity*2 + 16; - if (acht->freestackp < stacksize-1) { - acht->freestack[acht->freestackp++] = other_items; - } - const size_t mempool_size = ((acht->rows + rows-row) * 2 * acht->colors / (acht->rows + row + 1) + 32*capacity) * sizeof(struct acolorhist_arr_item); - new_items = mempool_alloc(&acht->mempool, sizeof(struct acolorhist_arr_item)*capacity, mempool_size); - if (!new_items) return false; - memcpy(new_items, other_items, sizeof(other_items[0])*achl->capacity); - } - - achl->other_items = new_items; - achl->capacity = capacity; - new_items[i] = (struct acolorhist_arr_item){ - .color = px, - .perceptual_weight = boost, - }; - achl->used++; - } else { - // these are elses for first checks whether first and second inline-stored colors are used - achl->inline2.color.l = px.l; - achl->inline2.perceptual_weight = boost; - achl->used = 2; - ++acht->colors; - } - } else { - achl->inline1.color.l = px.l; - achl->inline1.perceptual_weight = boost; - achl->used = 1; - ++acht->colors; - } - return true; -} - -LIQ_PRIVATE struct acolorhash_table *pam_allocacolorhash(unsigned int maxcolors, unsigned int surface, unsigned int ignorebits, void* (*malloc)(size_t), void (*free)(void*)) -{ - const size_t estimated_colors = MIN(maxcolors, surface/(ignorebits + (surface > 512*512 ? 6 : 5))); - const size_t hash_size = estimated_colors < 66000 ? 6673 : (estimated_colors < 200000 ? 12011 : 24019); - - mempool m = NULL; - const size_t buckets_size = hash_size * sizeof(struct acolorhist_arr_head); - const size_t mempool_size = sizeof(struct acolorhash_table) + buckets_size + estimated_colors * sizeof(struct acolorhist_arr_item); - struct acolorhash_table *t = mempool_create(&m, sizeof(*t) + buckets_size, mempool_size, malloc, free); - if (!t) return NULL; - *t = (struct acolorhash_table){ - .mempool = m, - .hash_size = hash_size, - .maxcolors = maxcolors, - .ignorebits = ignorebits, - }; - memset(t->buckets, 0, buckets_size); - return t; -} - -ALWAYS_INLINE static float pam_add_to_hist(const float *gamma_lut, hist_item *achv, unsigned int j, const struct acolorhist_arr_item *entry, const float max_perceptual_weight) -{ - achv[j].acolor = rgba_to_f(gamma_lut, entry->color.rgba); - const float w = MIN(entry->perceptual_weight, max_perceptual_weight); - achv[j].adjusted_weight = achv[j].perceptual_weight = w; - return w; -} - -LIQ_PRIVATE histogram *pam_acolorhashtoacolorhist(const struct acolorhash_table *acht, const double gamma, void* (*malloc)(size_t), void (*free)(void*)) -{ - histogram *hist = malloc(sizeof(hist[0])); - if (!hist || !acht) return NULL; - *hist = (histogram){ - .achv = malloc(MAX(1,acht->colors) * sizeof(hist->achv[0])), - .size = acht->colors, - .free = free, - .ignorebits = acht->ignorebits, - }; - if (!hist->achv) return NULL; - - float gamma_lut[256]; - to_f_set_gamma(gamma_lut, gamma); - - /* Limit perceptual weight to 1/10th of the image surface area to prevent - a single color from dominating all others. */ - float max_perceptual_weight = 0.1f * acht->cols * acht->rows; - double total_weight = 0; - - for(unsigned int j=0, i=0; i < acht->hash_size; ++i) { - const struct acolorhist_arr_head *const achl = &acht->buckets[i]; - if (achl->used) { - total_weight += pam_add_to_hist(gamma_lut, hist->achv, j++, &achl->inline1, max_perceptual_weight); - - if (achl->used > 1) { - total_weight += pam_add_to_hist(gamma_lut, hist->achv, j++, &achl->inline2, max_perceptual_weight); - - for(unsigned int k=0; k < achl->used-2; k++) { - total_weight += pam_add_to_hist(gamma_lut, hist->achv, j++, &achl->other_items[k], max_perceptual_weight); - } - } - } - } - - hist->total_perceptual_weight = total_weight; - return hist; -} - - -LIQ_PRIVATE void pam_freeacolorhash(struct acolorhash_table *acht) -{ - if (acht) { - mempool_destroy(acht->mempool); - } -} - -LIQ_PRIVATE void pam_freeacolorhist(histogram *hist) -{ - hist->free(hist->achv); - hist->free(hist); -} - -LIQ_PRIVATE colormap *pam_colormap(unsigned int colors, void* (*malloc)(size_t), void (*free)(void*)) -{ - assert(colors > 0 && colors < 65536); - - colormap *map; - const size_t colors_size = colors * sizeof(map->palette[0]); - map = malloc(sizeof(colormap) + colors_size); - if (!map) return NULL; - *map = (colormap){ - .malloc = malloc, - .free = free, - .colors = colors, - }; - memset(map->palette, 0, colors_size); - return map; -} - -LIQ_PRIVATE colormap *pam_duplicate_colormap(colormap *map) -{ - colormap *dupe = pam_colormap(map->colors, map->malloc, map->free); - for(unsigned int i=0; i < map->colors; i++) { - dupe->palette[i] = map->palette[i]; - } - return dupe; -} - -LIQ_PRIVATE void pam_freecolormap(colormap *c) -{ - c->free(c); -} - -LIQ_PRIVATE void to_f_set_gamma(float gamma_lut[], const double gamma) -{ - for(int i=0; i < 256; i++) { - gamma_lut[i] = pow((double)i/255.0, internal_gamma/gamma); - } -} - diff --git a/pam.h b/pam.h deleted file mode 100644 index 6657a14..0000000 --- a/pam.h +++ /dev/null @@ -1,271 +0,0 @@ -/* pam.h - pam (portable alpha map) utility library - ** - ** Colormap routines. - ** - ** Copyright (C) 1989, 1991 by Jef Poskanzer. - ** Copyright (C) 1997 by Greg Roelofs. - ** - ** Permission to use, copy, modify, and distribute this software and its - ** documentation for any purpose and without fee is hereby granted, provided - ** that the above copyright notice appear in all copies and that both that - ** copyright notice and this permission notice appear in supporting - ** documentation. This software is provided "as is" without express or - ** implied warranty. - */ - -#ifndef PAM_H -#define PAM_H - -#include -#include -#include -#include - -#ifndef MAX -# define MAX(a,b) ((a) > (b)? (a) : (b)) -# define MIN(a,b) ((a) < (b)? (a) : (b)) -#endif - -#define MAX_DIFF 1e20 - -#ifndef USE_SSE -# if defined(__SSE__) && (defined(__amd64__) || defined(__X86_64__) || defined(_WIN64) || defined(WIN32) || defined(__WIN32__)) -# define USE_SSE 1 -# else -# define USE_SSE 0 -# endif -#endif - -#if USE_SSE -# include -# ifdef _MSC_VER -# include -# define SSE_ALIGN -# else -# define SSE_ALIGN __attribute__ ((aligned (16))) -# if defined(__i386__) && defined(__PIC__) -# define cpuid(func,ax,bx,cx,dx)\ - __asm__ __volatile__ ( \ - "push %%ebx\n" \ - "cpuid\n" \ - "mov %%ebx, %1\n" \ - "pop %%ebx\n" \ - : "=a" (ax), "=r" (bx), "=c" (cx), "=d" (dx) \ - : "a" (func)); -# else -# define cpuid(func,ax,bx,cx,dx)\ - __asm__ __volatile__ ("cpuid":\ - "=a" (ax), "=b" (bx), "=c" (cx), "=d" (dx) : "a" (func)); -# endif -#endif -#else -# define SSE_ALIGN -#endif - -#if defined(__GNUC__) || defined (__llvm__) -#define ALWAYS_INLINE __attribute__((always_inline)) inline -#define NEVER_INLINE __attribute__ ((noinline)) -#elif defined(_MSC_VER) -#define inline __inline -#define restrict __restrict -#define ALWAYS_INLINE __forceinline -#define NEVER_INLINE __declspec(noinline) -#else -#define ALWAYS_INLINE inline -#define NEVER_INLINE -#endif - -/* from pam.h */ - -typedef struct { - unsigned char r, g, b, a; -} rgba_pixel; - -typedef struct { - float a, r, g, b; -} SSE_ALIGN f_pixel; - -static const double internal_gamma = 0.5499; - -LIQ_PRIVATE void to_f_set_gamma(float gamma_lut[], const double gamma); - -/** - Converts 8-bit color to internal gamma and premultiplied alpha. - (premultiplied color space is much better for blending of semitransparent colors) - */ -ALWAYS_INLINE static f_pixel rgba_to_f(const float gamma_lut[], const rgba_pixel px); -inline static f_pixel rgba_to_f(const float gamma_lut[], const rgba_pixel px) -{ - float a = px.a/255.f; - - return (f_pixel) { - .a = a, - .r = gamma_lut[px.r]*a, - .g = gamma_lut[px.g]*a, - .b = gamma_lut[px.b]*a, - }; -} - -inline static rgba_pixel f_to_rgb(const float gamma, const f_pixel px) -{ - if (px.a < 1.f/256.f) { - return (rgba_pixel){0,0,0,0}; - } - - float r = px.r / px.a, - g = px.g / px.a, - b = px.b / px.a, - a = px.a; - - r = powf(r, gamma/internal_gamma); - g = powf(g, gamma/internal_gamma); - b = powf(b, gamma/internal_gamma); - - // 256, because numbers are in range 1..255.9999… rounded down - r *= 256.f; - g *= 256.f; - b *= 256.f; - a *= 256.f; - - return (rgba_pixel){ - .r = r>=255.f ? 255 : r, - .g = g>=255.f ? 255 : g, - .b = b>=255.f ? 255 : b, - .a = a>=255.f ? 255 : a, - }; -} - -ALWAYS_INLINE static double colordifference_ch(const double x, const double y, const double alphas); -inline static double colordifference_ch(const double x, const double y, const double alphas) -{ - // maximum of channel blended on white, and blended on black - // premultiplied alpha and backgrounds 0/1 shorten the formula - const double black = x-y, white = black+alphas; - return MAX(black*black, white*white); -} - -ALWAYS_INLINE static float colordifference_stdc(const f_pixel px, const f_pixel py); -inline static float colordifference_stdc(const f_pixel px, const f_pixel py) -{ - // px_b.rgb = px.rgb + 0*(1-px.a) // blend px on black - // px_b.a = px.a + 1*(1-px.a) - // px_w.rgb = px.rgb + 1*(1-px.a) // blend px on white - // px_w.a = px.a + 1*(1-px.a) - - // px_b.rgb = px.rgb // difference same as in opaque RGB - // px_b.a = 1 - // px_w.rgb = px.rgb - px.a // difference simplifies to formula below - // px_w.a = 1 - - // (px.rgb - px.a) - (py.rgb - py.a) - // (px.rgb - py.rgb) + (py.a - px.a) - - const double alphas = py.a-px.a; - return colordifference_ch(px.r, py.r, alphas) + - colordifference_ch(px.g, py.g, alphas) + - colordifference_ch(px.b, py.b, alphas); -} - -ALWAYS_INLINE static float colordifference(f_pixel px, f_pixel py); -inline static float colordifference(f_pixel px, f_pixel py) -{ -#if USE_SSE - const __m128 vpx = _mm_load_ps((const float*)&px); - const __m128 vpy = _mm_load_ps((const float*)&py); - - // y.a - x.a - __m128 alphas = _mm_sub_ss(vpy, vpx); - alphas = _mm_shuffle_ps(alphas,alphas,0); // copy first to all four - - __m128 onblack = _mm_sub_ps(vpx, vpy); // x - y - __m128 onwhite = _mm_add_ps(onblack, alphas); // x - y + (y.a - x.a) - - onblack = _mm_mul_ps(onblack, onblack); - onwhite = _mm_mul_ps(onwhite, onwhite); - const __m128 max = _mm_max_ps(onwhite, onblack); - - // add rgb, not a - const __m128 maxhl = _mm_movehl_ps(max, max); - const __m128 tmp = _mm_add_ps(max, maxhl); - const __m128 sum = _mm_add_ss(maxhl, _mm_shuffle_ps(tmp, tmp, 1)); - - const float res = _mm_cvtss_f32(sum); - assert(fabs(res - colordifference_stdc(px,py)) < 0.001); - return res; -#else - return colordifference_stdc(px,py); -#endif -} - -/* from pamcmap.h */ -union rgba_as_int { - rgba_pixel rgba; - unsigned int l; -}; - -typedef struct { - f_pixel acolor; - float adjusted_weight, // perceptual weight changed to tweak how mediancut selects colors - perceptual_weight; // number of pixels weighted by importance of different areas of the picture - - float color_weight; // these two change every time histogram subset is sorted - union { - unsigned int sort_value; - unsigned char likely_colormap_index; - } tmp; -} hist_item; - -typedef struct { - hist_item *achv; - void (*free)(void*); - double total_perceptual_weight; - unsigned int size; - unsigned int ignorebits; -} histogram; - -typedef struct { - f_pixel acolor; - float popularity; - bool fixed; // if true it's user-supplied and must not be changed (e.g in K-Means iteration) -} colormap_item; - -typedef struct colormap { - unsigned int colors; - void* (*malloc)(size_t); - void (*free)(void*); - colormap_item palette[]; -} colormap; - -struct acolorhist_arr_item { - union rgba_as_int color; - float perceptual_weight; -}; - -struct acolorhist_arr_head { - struct acolorhist_arr_item inline1, inline2; - unsigned int used, capacity; - struct acolorhist_arr_item *other_items; -}; - -struct acolorhash_table { - struct mempool *mempool; - unsigned int ignorebits, maxcolors, colors, cols, rows; - unsigned int hash_size; - unsigned int freestackp; - struct acolorhist_arr_item *freestack[512]; - struct acolorhist_arr_head buckets[]; -}; - -LIQ_PRIVATE void pam_freeacolorhash(struct acolorhash_table *acht); -LIQ_PRIVATE struct acolorhash_table *pam_allocacolorhash(unsigned int maxcolors, unsigned int surface, unsigned int ignorebits, void* (*malloc)(size_t), void (*free)(void*)); -LIQ_PRIVATE histogram *pam_acolorhashtoacolorhist(const struct acolorhash_table *acht, const double gamma, void* (*malloc)(size_t), void (*free)(void*)); -LIQ_PRIVATE bool pam_computeacolorhash(struct acolorhash_table *acht, const rgba_pixel *const pixels[], unsigned int cols, unsigned int rows, const unsigned char *importance_map); -LIQ_PRIVATE bool pam_add_to_hash(struct acolorhash_table *acht, unsigned int hash, float boost, union rgba_as_int px, unsigned int row, unsigned int rows); - -LIQ_PRIVATE void pam_freeacolorhist(histogram *h); - -LIQ_PRIVATE colormap *pam_colormap(unsigned int colors, void* (*malloc)(size_t), void (*free)(void*)); -LIQ_PRIVATE colormap *pam_duplicate_colormap(colormap *map); -LIQ_PRIVATE void pam_freecolormap(colormap *c); - -#endif diff --git a/rust/build.rs b/rust/build.rs deleted file mode 100644 index 76c63ee..0000000 --- a/rust/build.rs +++ /dev/null @@ -1,51 +0,0 @@ -//! This is a build script for Cargo https://crates.io/ -//! It produces a static library that can be used by C or Rust. - -extern crate gcc; - -use std::env; -use std::path::PathBuf; -use std::fs::canonicalize; - -fn main() { - let mut cc = gcc::Config::new(); - - if env::var("PROFILE").map(|p|p != "debug").unwrap_or(true) { - cc.define("NDEBUG", Some("1")); - } - - if cfg!(target_arch="x86_64") || - (cfg!(target_arch="x86") && cfg!(feature = "sse")) { - cc.define("USE_SSE", Some("1")); - } - - let outdated_c_compiler = env::var("TARGET").unwrap().contains("windows-msvc"); - let has_msvc_files = PathBuf::from("msvc-dist/libimagequant.c").exists(); - - if outdated_c_compiler && has_msvc_files { - println!("cargo:include={}", canonicalize("msvc-dist").unwrap().display()); - cc.file("msvc-dist/libimagequant.c") - .file("msvc-dist/nearest.c") - .file("msvc-dist/kmeans.c") - .file("msvc-dist/mediancut.c") - .file("msvc-dist/mempool.c") - .file("msvc-dist/pam.c") - .file("msvc-dist/blur.c"); - } else { - // This is so that I don't forget to publish MSVC version as well - if !has_msvc_files { - println!("cargo:warning=msvc-dist/ directory not present. MSVC builds may fail"); - } - println!("cargo:include={}", canonicalize(".").unwrap().display()); - cc.flag("-std=c99"); - cc.file("libimagequant.c") - .file("nearest.c") - .file("kmeans.c") - .file("mediancut.c") - .file("mempool.c") - .file("pam.c") - .file("blur.c"); - } - - cc.compile("libimagequant.a"); -} diff --git a/rust/libimagequant.rs b/rust/libimagequant.rs deleted file mode 100644 index 274c944..0000000 --- a/rust/libimagequant.rs +++ /dev/null @@ -1,251 +0,0 @@ -//! Small, portable C library for high-quality conversion of RGBA images to 8-bit indexed-color (palette) images. -//! It's powering [pngquant2](https://pngquant.org). -//! -//! This is a low-level crate exposing a C API. If you're looking for a Rust library, see [imagequant](https://crates.io/crates/imagequant). -//! -//! ## License -//! -//! Libimagequant is dual-licensed: -//! -//! * For Free/Libre Open Source Software it's available under [GPL v3 or later](https://raw.github.com/ImageOptim/libimagequant/master/COPYRIGHT) with additional copyright notices for older parts of the code. -//! -//! * For use in non-GPL software (e.g. closed-source or App Store distribution) please ask kornel@pngquant.org for a commercial license. -//! -//! ## Overview -//! -//! The basic flow is: -//! -//! 1. Create attributes object and configure the library. -//! 2. Create image object from RGBA pixels or data source. -//! 3. Perform quantization (generate palette). -//! 4. Store remapped image and final palette. -//! 5. Free memory. -//! -//! Please note that libimagequant only handles raw uncompressed arrays of pixels in memory and is completely independent of any file format. -//! -//! There are 3 ways to create image object for quantization: -//! -//! * `liq_image_create_rgba()` for simple, contiguous RGBA pixel arrays (width×height×4 bytes large bitmap). -//! * `liq_image_create_rgba_rows()` for non-contiguous RGBA pixel arrays (that have padding between rows or reverse order, e.g. BMP). -//! * `liq_image_create_custom()` for RGB, ABGR, YUV and all other formats that can be converted on-the-fly to RGBA (you have to supply the conversion function). -//! -//! Note that "image" here means raw uncompressed pixels. If you have a compressed image file, such as PNG, you must use another library (e.g. lodepng) to decode it first. - - -#![allow(non_camel_case_types)] -extern crate rgb; - -use std::os::raw::{c_int, c_uint, c_char, c_void}; -use std::error; -use std::fmt; -use std::error::Error; - -pub enum liq_attr {} -pub enum liq_image {} -pub enum liq_result {} -pub enum liq_histogram {} -pub type liq_color = rgb::RGBA8; - -#[repr(C)] -#[derive(Copy, Clone, Debug, Eq, PartialEq)] -pub enum liq_error { - LIQ_OK = 0, - LIQ_QUALITY_TOO_LOW = 99, - LIQ_VALUE_OUT_OF_RANGE = 100, - LIQ_OUT_OF_MEMORY, - LIQ_ABORTED, - LIQ_BITMAP_NOT_AVAILABLE, - LIQ_BUFFER_TOO_SMALL, - LIQ_INVALID_POINTER, - LIQ_UNSUPPORTED, -} - -#[repr(C)] -#[derive(Copy, Clone)] -pub enum liq_ownership { - LIQ_OWN_ROWS = 4, - LIQ_OWN_PIXELS = 8, -} - -#[repr(C)] -pub struct liq_palette { - pub count: c_int, - pub entries: [liq_color; 256], -} - -#[repr(C)] -#[derive(Debug, Copy, Clone)] -pub struct liq_histogram_entry { - pub color: liq_color, - pub count: c_uint, -} - -impl error::Error for liq_error { - fn description(&self) -> &str { - match *self { - liq_error::LIQ_OK => "OK", - liq_error::LIQ_QUALITY_TOO_LOW => "LIQ_QUALITY_TOO_LOW", - liq_error::LIQ_VALUE_OUT_OF_RANGE => "VALUE_OUT_OF_RANGE", - liq_error::LIQ_OUT_OF_MEMORY => "OUT_OF_MEMORY", - liq_error::LIQ_ABORTED => "LIQ_ABORTED", - liq_error::LIQ_BITMAP_NOT_AVAILABLE => "BITMAP_NOT_AVAILABLE", - liq_error::LIQ_BUFFER_TOO_SMALL => "BUFFER_TOO_SMALL", - liq_error::LIQ_INVALID_POINTER => "INVALID_POINTER", - liq_error::LIQ_UNSUPPORTED => "LIQ_UNSUPPORTED", - } - } -} - -impl fmt::Display for liq_error { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", self.description()) - } -} - -pub type liq_log_callback_function = Option; -pub type liq_log_flush_callback_function = Option; -pub type liq_progress_callback_function = Option c_int>; -pub type liq_image_get_rgba_row_callback = Option; -#[link(name="imagequant", kind="static")] -extern "C" { - - /// Returns object that will hold initial settings (attributes) for the library. - /// - /// The object should be freed using `liq_attr_destroy()` after it's no longer needed. - /// Returns `NULL` in the unlikely case that the library cannot run on the current machine (e.g. the library has been compiled for SSE-capable x86 CPU and run on VIA C3 CPU). - pub fn liq_attr_create() -> *mut liq_attr; - pub fn liq_attr_copy(orig: &liq_attr) -> *mut liq_attr; - pub fn liq_attr_destroy(attr: &mut liq_attr); - - /// Specifies maximum number of colors to use. - /// - /// The default is 256. Instead of setting a fixed limit it's better to use `liq_set_quality()`. - /// The first argument is attributes object from `liq_attr_create()`. - /// Returns `LIQ_VALUE_OUT_OF_RANGE` if number of colors is outside the range 2-256. - pub fn liq_set_max_colors(attr: &mut liq_attr, colors: c_int) -> liq_error; - pub fn liq_get_max_colors(attr: &liq_attr) -> c_int; - pub fn liq_set_speed(attr: &mut liq_attr, speed: c_int) -> liq_error; - pub fn liq_get_speed(attr: &liq_attr) -> c_int; - pub fn liq_set_min_posterization(attr: &mut liq_attr, bits: c_int) -> liq_error; - pub fn liq_get_min_posterization(attr: &liq_attr) -> c_int; - /// Quality is in range `0` (worst) to `100` (best) and values are analoguous to JPEG quality (i.e. `80` is usually good enough). - /// - /// Quantization will attempt to use the lowest number of colors needed to achieve `maximum` quality. `maximum` value of `100` is the default and means conversion as good as possible. - /// If it's not possible to convert the image with at least `minimum` quality (i.e. 256 colors is not enough to meet the minimum quality), then `liq_image_quantize()` will fail. The default minumum is `0` (proceeds regardless of quality). - /// Quality measures how well the generated palette fits image given to `liq_image_quantize()`. If a different image is remapped with `liq_write_remapped_image()` then actual quality may be different. - /// Regardless of the quality settings the number of colors won't exceed the maximum (see `liq_set_max_colors()`). - /// The first argument is attributes object from `liq_attr_create()`. - /// - /// Returns `LIQ_VALUE_OUT_OF_RANGE` if target is lower than minimum or any of them is outside the 0-100 range. - /// Returns `LIQ_INVALID_POINTER` if `attr` appears to be invalid. - pub fn liq_set_quality(attr: &mut liq_attr, minimum: c_int, maximum: c_int) -> liq_error; - pub fn liq_get_min_quality(attr: &liq_attr) -> c_int; - pub fn liq_get_max_quality(attr: &liq_attr) -> c_int; - pub fn liq_set_last_index_transparent(attr: &mut liq_attr, is_last: c_int); - - pub fn liq_image_create_rgba_rows(attr: &liq_attr, rows: *const *const u8, width: c_int, height: c_int, gamma: f64) -> *mut liq_image; - /// Creates an object that represents the image pixels to be used for quantization and remapping. - /// - /// The pixel array must be contiguous run of RGBA pixels (alpha is the last component, 0 = transparent, 255 = opaque). - /// - /// The first argument is attributes object from `liq_attr_create()`. The same `attr` object should be used for the entire process, from creation of images to quantization. - /// - /// The `pixels` array must not be modified or freed until this object is freed with `liq_image_destroy()`. See also `liq_image_set_memory_ownership()`. - /// - /// `width` and `height` are dimensions in pixels. An image 10x10 pixel large will need a 400-byte array. - /// - /// `gamma` can be `0` for images with the typical 1/2.2 [gamma](https://en.wikipedia.org/wiki/Gamma_correction). - /// Otherwise `gamma` must be > 0 and < 1, e.g. `0.45455` (1/2.2) or `0.55555` (1/1.8). Generated palette will use the same gamma unless `liq_set_output_gamma()` is used. If `liq_set_output_gamma` is not used, then it only affects whether brighter or darker areas of the image will get more palette colors allocated. - /// - /// Returns `NULL` on failure, e.g. if `pixels` is `NULL` or `width`/`height` is <= 0. - pub fn liq_image_create_rgba(attr: &liq_attr, bitmap: *const u8, width: c_int, height: c_int, gamma: f64) -> *mut liq_image; - - pub fn liq_set_log_callback(arg1: &mut liq_attr, arg2: liq_log_callback_function, user_info: *mut c_void); - pub fn liq_set_log_flush_callback(arg1: &mut liq_attr, arg2: liq_log_flush_callback_function, user_info: *mut c_void); - pub fn liq_attr_set_progress_callback(arg1: &mut liq_attr, arg2: liq_progress_callback_function, user_info: *mut c_void); - pub fn liq_result_set_progress_callback(arg1: &mut liq_result, arg2: liq_progress_callback_function, user_info: *mut c_void); - pub fn liq_image_create_custom(attr: &liq_attr, row_callback: liq_image_get_rgba_row_callback, user_info: *mut c_void, width: c_int, height: c_int, gamma: f64) -> *mut liq_image; - pub fn liq_image_add_fixed_color(img: &mut liq_image, color: liq_color) -> liq_error; - pub fn liq_image_get_width(img: &liq_image) -> c_int; - pub fn liq_image_get_height(img: &liq_image) -> c_int; - pub fn liq_image_destroy(img: &mut liq_image); - - pub fn liq_histogram_create(attr: &liq_attr) -> *mut liq_histogram; - pub fn liq_histogram_add_image(hist: &mut liq_histogram, attr: &liq_attr, image: &mut liq_image) -> liq_error; - pub fn liq_histogram_add_colors(hist: &mut liq_histogram, attr: &liq_attr, entries: *const liq_histogram_entry, num_entries: c_int, gamma: f64) -> liq_error; - pub fn liq_histogram_destroy(hist: &mut liq_histogram); - - /// Performs quantization (palette generation) based on settings in `attr` (from `liq_attr_create()`) and pixels of the image. - /// - /// Returns `LIQ_OK` if quantization succeeds and sets `liq_result` pointer in `out_result`. The last argument is used for receiving the `result` object: - /// - /// liq_result *result; - /// if (LIQ_OK == liq_image_quantize(img, attr, &result)) { // Note &result - /// // result pointer is valid here - /// } - /// - /// Returns `LIQ_QUALITY_TOO_LOW` if quantization fails due to limit set in `liq_set_quality()`. - /// - /// See `liq_write_remapped_image()`. - /// - /// If you want to generate one palette for multiple images at once, see `liq_histogram_create()`. - pub fn liq_quantize_image(options: &liq_attr, input_image: &liq_image) -> *mut liq_result; - pub fn liq_histogram_quantize(input_hist: &liq_histogram, options: &liq_attr, result_output: &mut *mut liq_result) -> liq_error; - pub fn liq_image_quantize(input_image: &liq_image, options: &liq_attr, result_output: &mut *mut liq_result) -> liq_error; - /// Enables/disables dithering in `liq_write_remapped_image()`. - /// - /// Dithering level must be between `0` and `1` (inclusive). Dithering level `0` enables fast non-dithered remapping. Otherwise a variation of Floyd-Steinberg error diffusion is used. - /// - /// Precision of the dithering algorithm depends on the speed setting, see `liq_set_speed()`. - /// - /// Returns `LIQ_VALUE_OUT_OF_RANGE` if the dithering level is outside the 0-1 range. - pub fn liq_set_dithering_level(res: &mut liq_result, dither_level: f32) -> liq_error; - pub fn liq_set_output_gamma(res: &mut liq_result, gamma: f64) -> liq_error; - pub fn liq_get_output_gamma(result: &liq_result) -> f64; - /// Returns pointer to palette optimized for image that has been quantized or remapped - /// (final refinements are applied to the palette during remapping). - /// - /// It's valid to call this method before remapping, if you don't plan to remap any images or want to use same palette for multiple images. - /// - /// `liq_palette->count` contains number of colors (up to 256), `liq_palette->entries[n]` contains RGBA value for nth palette color. - pub fn liq_get_palette(result: &mut liq_result) -> &liq_palette; - /// Remaps the image to palette and writes its pixels to the given buffer, 1 pixel per byte. - /// - /// The buffer must be large enough to fit the entire image, i.e. width×height bytes large. For safety, pass the size of the buffer as `buffer_size`. - /// - /// For best performance call `liq_get_palette()` *after* this function, as palette is improved during remapping (except when `liq_histogram_quantize()` is used). - /// - /// Returns `LIQ_BUFFER_TOO_SMALL` if given size of the buffer is not enough to fit the entire image. - /// - /// int buffer_size = width*height; - /// char *buffer = malloc(buffer_size); - /// if (LIQ_OK == liq_write_remapped_image(result, input_image, buffer, buffer_size)) { - /// liq_palette *pal = liq_get_palette(result); - /// // save image - /// } - /// - /// See `liq_get_palette()`. - /// - /// The buffer is assumed to be contiguous, with rows ordered from top to bottom, and no gaps between rows. If you need to write rows with padding or upside-down order, then use `liq_write_remapped_image_rows()`. - /// - /// Please note that it only writes raw uncompressed pixels to memory. It does not perform any PNG compression. If you'd like to create a PNG file then you need to pass the raw pixel data to another library, e.g. libpng or lodepng. See `rwpng.c` in `pngquant` project for an example how to do that. - pub fn liq_write_remapped_image(result: &mut liq_result, input_image: &mut liq_image, buffer: *mut u8, buffer_size: usize) -> liq_error; - pub fn liq_write_remapped_image_rows(result: &mut liq_result, input_image: &mut liq_image, row_pointers: *const *mut u8) -> liq_error; - pub fn liq_get_quantization_error(result: &liq_result) -> f64; - pub fn liq_get_quantization_quality(result: &liq_result) -> c_int; - - pub fn liq_result_destroy(res: &mut liq_result); - pub fn liq_get_remapping_error(result: &liq_result) -> f64; - pub fn liq_get_remapping_quality(result: &liq_result) -> c_int; - pub fn liq_version() -> c_int; -} - -#[test] -fn links() { - unsafe { - assert!(liq_version() >= 20901); - } -} diff --git a/src/attr.rs b/src/attr.rs new file mode 100644 index 0000000..41dc7f8 --- /dev/null +++ b/src/attr.rs @@ -0,0 +1,416 @@ +use crate::error::Error; +use crate::hist::Histogram; +use crate::image::Image; +use crate::pal::{PalLen, MAX_COLORS, RGBA}; +use crate::quant::{mse_to_quality, quality_to_mse, QuantizationResult}; +use crate::remap::DitherMapMode; +use std::sync::Arc; + +#[cfg(all(not(feature = "std"), feature = "no_std"))] +use crate::no_std_compat::*; + +/// Starting point and settings for the quantization process +#[derive(Clone)] +#[cfg_attr(all(feature = "std", feature = "no_std"), deprecated(note = "Cargo features configuration issue: both std and no_std features are enabled in imagequant\nYou must disable default features to use no_std."))] +pub struct Attributes { + pub(crate) max_colors: PalLen, + target_mse: f64, + max_mse: Option, + kmeans_iteration_limit: f64, + kmeans_iterations: u16, + feedback_loop_trials: u16, + pub(crate) max_histogram_entries: u32, + min_posterization_output: u8, + min_posterization_input: u8, + pub(crate) last_index_transparent: bool, + pub(crate) use_contrast_maps: bool, + pub(crate) single_threaded_dithering: bool, + pub(crate) use_dither_map: DitherMapMode, + speed: u8, + pub(crate) progress_stage1: u8, + pub(crate) progress_stage2: u8, + pub(crate) progress_stage3: u8, + + progress_callback: Option ControlFlow + Send + Sync>>, + log_callback: Option>, + log_flush_callback: Option>, +} + +impl Attributes { + /// New handle for library configuration + /// + /// See also [`Attributes::new_image()`] + #[inline] + #[must_use] + pub fn new() -> Self { + let mut attr = Self { + target_mse: 0., + max_mse: None, + max_colors: MAX_COLORS as PalLen, + last_index_transparent: false, + kmeans_iteration_limit: 0., + max_histogram_entries: 0, + min_posterization_output: 0, + min_posterization_input: 0, + kmeans_iterations: 0, + feedback_loop_trials: 0, + use_contrast_maps: false, + use_dither_map: DitherMapMode::None, + single_threaded_dithering: false, + speed: 0, + progress_stage1: 0, + progress_stage2: 0, + progress_stage3: 0, + progress_callback: None, + log_callback: None, + log_flush_callback: None, + }; + let _ = attr.set_speed(4); + attr + } + + /// Make an image from RGBA pixels. + /// + /// The `pixels` argument can be `Vec`, or `Box<[RGBA]>` or `&[RGBA]`. + /// See [`Attributes::new_image_borrowed`] for a non-copying alternative. + /// + /// Use 0.0 for gamma if the image is sRGB (most images are). + #[inline] + pub fn new_image(&self, pixels: VecRGBA, width: usize, height: usize, gamma: f64) -> Result, Error> where VecRGBA: Into> { + Image::new(self, pixels, width, height, gamma) + } + + /// Generate palette for the image + pub fn quantize(&self, image: &mut Image<'_>) -> Result { + let mut hist = Histogram::new(self); + hist.add_image(self, image)?; + hist.quantize_internal(self, false) + } + + /// It's better to use `set_quality()` + #[inline] + pub fn set_max_colors(&mut self, colors: u32) -> Result<(), Error> { + if !(2..=256).contains(&colors) { + return Err(Error::ValueOutOfRange); + } + self.max_colors = colors as PalLen; + Ok(()) + } + + /// Range 0-100, roughly like JPEG. + /// + /// If the minimum quality can't be met, the quantization will be aborted with an error. + /// + /// Default is min 0, max 100, which means best effort, and never aborts the process. + /// + /// If max is less than 100, the library will try to use fewer colors. + /// Images with fewer colors are not always smaller, due to increased dithering it causes. + pub fn set_quality(&mut self, minimum: u8, target: u8) -> Result<(), Error> { + if !(0..=100).contains(&target) || target < minimum { + return Err(Error::ValueOutOfRange); + } + if target < 30 { + self.verbose_print(" warning: quality set too low"); + } + self.target_mse = quality_to_mse(target); + self.max_mse = Some(quality_to_mse(minimum)); + Ok(()) + } + + /// 1-10. + /// + /// Faster speeds generate images of lower quality, but may be useful + /// for real-time generation of images. + /// + /// The default is 4. + #[inline] + pub fn set_speed(&mut self, value: i32) -> Result<(), Error> { + if !(1..=10).contains(&value) { + return Err(Error::ValueOutOfRange); + } + let mut iterations = (8 - value).max(0) as u16; + iterations += iterations * iterations / 2; + self.kmeans_iterations = iterations; + self.kmeans_iteration_limit = 1. / f64::from(1 << (23 - value)); + self.feedback_loop_trials = (56 - 9 * value).max(0) as _; + self.max_histogram_entries = ((1 << 17) + (1 << 18) * (10 - value)) as _; + self.min_posterization_input = if value >= 8 { 1 } else { 0 }; + self.use_dither_map = if value <= 6 { DitherMapMode::Enabled } else { DitherMapMode::None }; + if self.use_dither_map != DitherMapMode::None && value < 3 { + self.use_dither_map = DitherMapMode::Always; + } + self.use_contrast_maps = (value <= 7) || self.use_dither_map != DitherMapMode::None; + self.single_threaded_dithering = value == 1; + self.speed = value as u8; + self.progress_stage1 = if self.use_contrast_maps { 20 } else { 8 }; + if self.feedback_loop_trials < 2 { + self.progress_stage1 += 30; + } + self.progress_stage3 = (50 / (1 + value)) as u8; + self.progress_stage2 = 100 - self.progress_stage1 - self.progress_stage3; + Ok(()) + } + + /// Number of least significant bits to ignore. + /// + /// Useful for generating palettes for VGA, 15-bit textures, or other retro platforms. + #[inline] + pub fn set_min_posterization(&mut self, value: u8) -> Result<(), Error> { + if !(0..=4).contains(&value) { + return Err(Error::ValueOutOfRange); + } + self.min_posterization_output = value; + Ok(()) + } + + /// Returns number of bits of precision truncated + #[inline(always)] + #[must_use] + pub fn min_posterization(&self) -> u8 { + self.min_posterization_output + } + + /// Return currently set speed/quality trade-off setting + #[inline(always)] + #[must_use] + pub fn speed(&self) -> u32 { + self.speed.into() + } + + /// Return max number of colors set + #[inline(always)] + #[must_use] + pub fn max_colors(&self) -> u32 { + self.max_colors.into() + } + + /// Reads values set with `set_quality` + #[must_use] + pub fn quality(&self) -> (u8, u8) { + ( + self.max_mse.map_or(0, mse_to_quality), + mse_to_quality(self.target_mse), + ) + } + + /// Describe dimensions of a slice of RGBA pixels + /// + /// Use 0.0 for gamma if the image is sRGB (most images are). + #[inline] + pub fn new_image_borrowed<'pixels>(&self, bitmap: &'pixels [RGBA], width: usize, height: usize, gamma: f64) -> Result, Error> { + Image::new_borrowed(self, bitmap, width, height, gamma) + } + + /// Like `new_image_stride_borrowed`, but makes a copy of the pixels. + /// + /// The `pixels` argument can be `Vec`, or `Box<[RGBA]>` or `&[RGBA]`. + #[inline] + pub fn new_image_stride(&self, pixels: VecRGBA, width: usize, height: usize, stride: usize, gamma: f64) -> Result, Error> where VecRGBA: Into> { + Image::new_stride(self, pixels, width, height, stride, gamma) + } + + #[doc(hidden)] + #[deprecated(note = "use new_image_stride")] + #[cold] + pub fn new_image_stride_copy(&self, bitmap: &[RGBA], width: usize, height: usize, stride: usize, gamma: f64) -> Result, Error> { + self.new_image_stride(bitmap, width, height, stride, gamma) + } + + /// Set callback function to be called every time the library wants to print a message. + /// + /// To share data with the callback, use `Arc` or `Atomic*` types and `move ||` closures. + #[inline] + pub fn set_log_callback(&mut self, callback: F) { + self.verbose_printf_flush(); + self.log_callback = Some(Arc::new(callback)); + } + + /// Callback for flushing output (if you buffer messages, that's the time to flush those buffers) + #[inline] + pub fn set_log_flush_callback(&mut self, callback: F) { + self.verbose_printf_flush(); + self.log_flush_callback = Some(Arc::new(callback)); + } + + /// Set callback function to be called every time the library makes a progress. + /// It can be used to cancel operation early. + /// + /// To share data with the callback, use `Arc` or `Atomic*` types and `move ||` closures. + #[inline] + pub fn set_progress_callback ControlFlow + Send + Sync + 'static>(&mut self, callback: F) { + self.progress_callback = Some(Arc::new(callback)); + } + + /// Move transparent color to the last entry in the palette + /// + /// This is less efficient for PNG, but required by some broken software + #[inline(always)] + pub fn set_last_index_transparent(&mut self, is_last: bool) { + self.last_index_transparent = is_last; + } + + // true == abort + #[inline] + #[must_use] + pub(crate) fn progress(&self, percent: f32) -> bool { + if let Some(f) = &self.progress_callback { + f(percent) == ControlFlow::Break + } else { + false + } + } + + #[inline(always)] + pub(crate) fn verbose_print(&self, msg: impl AsRef) { + fn print_(a: &Attributes, msg: &str) { + if let Some(f) = &a.log_callback { + f(a, msg); + } + } + print_(self, msg.as_ref()); + } + + #[inline] + pub(crate) fn verbose_printf_flush(&self) { + if let Some(f) = &self.log_flush_callback { + f(self); + } + } + + #[must_use] + pub(crate) fn feedback_loop_trials(&self, hist_items: usize) -> u16 { + let mut feedback_loop_trials = self.feedback_loop_trials; + if hist_items > 5000 { + feedback_loop_trials = (feedback_loop_trials * 3 + 3) / 4; + } + if hist_items > 25000 { + feedback_loop_trials = (feedback_loop_trials * 3 + 3) / 4; + } + if hist_items > 50000 { + feedback_loop_trials = (feedback_loop_trials * 3 + 3) / 4; + } + if hist_items > 100_000 { + feedback_loop_trials = (feedback_loop_trials * 3 + 3) / 4; + } + feedback_loop_trials + } + + /// `max_mse`, `target_mse`, user asked for perfect quality + pub(crate) fn target_mse(&self, hist_items_len: usize) -> (Option, f64, bool) { + let max_mse = self.max_mse.map(|mse| mse * if hist_items_len <= MAX_COLORS { 0.33 } else { 1. }); + let aim_for_perfect_quality = self.target_mse == 0.; + let mut target_mse = self.target_mse.max((f64::from(1 << self.min_posterization_output) / 1024.).powi(2)); + if let Some(max_mse) = max_mse { + target_mse = target_mse.min(max_mse); + } + (max_mse, target_mse, aim_for_perfect_quality) + } + + /// returns iterations, `iteration_limit` + #[must_use] + pub(crate) fn kmeans_iterations(&self, hist_items_len: usize, palette_error_is_known: bool) -> (u16, f64) { + let mut iteration_limit = self.kmeans_iteration_limit; + let mut iterations = self.kmeans_iterations; + if hist_items_len > 5000 { + iterations = (iterations * 3 + 3) / 4; + } + if hist_items_len > 25000 { + iterations = (iterations * 3 + 3) / 4; + } + if hist_items_len > 50000 { + iterations = (iterations * 3 + 3) / 4; + } + if hist_items_len > 100_000 { + iterations = (iterations * 3 + 3) / 4; + iteration_limit *= 2.; + } + if iterations == 0 && !palette_error_is_known && self.max_mse.is_some() { + iterations = 1; + } + (iterations, iteration_limit) + } + + #[inline] + #[must_use] + pub(crate) fn posterize_bits(&self) -> u8 { + self.min_posterization_output.max(self.min_posterization_input) + } +} + +impl Drop for Attributes { + fn drop(&mut self) { + self.verbose_printf_flush(); + } +} + +impl Default for Attributes { + #[inline(always)] + fn default() -> Self { + Self::new() + } +} + +/// Result of callback in [`Attributes::set_progress_callback`] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[repr(C)] +pub enum ControlFlow { + /// Continue processing as normal + Continue = 1, + /// Abort processing and fail + Break = 0, +} + +#[test] +fn counters() { + let mut a = Attributes::new(); + a.set_speed(10).unwrap(); + let (iter, _) = a.kmeans_iterations(1000, false); + assert_eq!(iter, 0); + a.set_quality(80, 90).unwrap(); + let (iter, limit) = a.kmeans_iterations(1000, false); + assert_eq!(iter, 1); + assert!(limit > 0. && limit < 0.01); + + let (iter, _) = a.kmeans_iterations(1000, true); + assert_eq!(iter, 0); + + let mut a = Attributes::new(); + a.set_quality(50, 80).unwrap(); + + let (max_mse, target_mse, aim_perfect) = a.target_mse(10000); + let max_mse = max_mse.unwrap(); + assert!(!aim_perfect); + assert!(target_mse > 0. && target_mse < 0.01); + assert!(max_mse > 0. && max_mse > target_mse && max_mse < 0.01); +} + +#[test] +fn getset() { + let mut a = Attributes::new(); + assert!(a.set_quality(0, 101).is_err()); + assert!(a.set_quality(50, 49).is_err()); + assert!(a.feedback_loop_trials(1000) > 0); + + let (max_mse, target_mse, aim_perfect) = a.target_mse(10000); + assert!(aim_perfect); + assert!(target_mse < 0.0001); + assert_eq!(max_mse, None); + + a.set_speed(5).unwrap(); + assert_eq!(5, a.speed()); + assert!(a.set_speed(99).is_err()); + assert!(a.set_speed(0).is_err()); + + a.set_max_colors(5).unwrap(); + assert_eq!(5, a.max_colors()); + assert!(a.set_max_colors(0).is_err()); + + a.set_min_posterization(2).unwrap(); + assert_eq!(2, a.min_posterization()); + assert_eq!(2, a.posterize_bits()); + assert!(a.set_min_posterization(8).is_err()); + + let mut a = Attributes::new(); + a.set_speed(10).unwrap(); + assert_eq!(1, a.posterize_bits()); +} diff --git a/src/blur.rs b/src/blur.rs new file mode 100644 index 0000000..c6be1ff --- /dev/null +++ b/src/blur.rs @@ -0,0 +1,71 @@ +/// Blurs image horizontally (width 2*size+1) and writes it transposed to dst (called twice gives 2d blur) +#[inline(never)] +fn transposing_1d_blur(src: &[u8], dst: &mut [u8], width: usize, height: usize, size: u16) { + if width < 2 * size as usize + 1 || height < 2 * size as usize + 1 { + return; + } + + for (j, row) in src.chunks_exact(width).enumerate() { + let mut sum = u16::from(row[0]) * size; + for &v in &row[0..size as usize] { + sum += u16::from(v); + } + for i in 0..size as usize { + sum -= u16::from(row[0]); + sum += u16::from(row[i + size as usize]); + dst[i * height + j] = (sum / (size * 2)) as u8; + } + for i in size as usize..width - size as usize { + sum -= u16::from(row[i - size as usize]); + sum += u16::from(row[i + size as usize]); + dst[i * height + j] = (sum / (size * 2)) as u8; + } + for i in width - size as usize..width { + sum -= u16::from(row[i - size as usize]); + sum += u16::from(row[width - 1]); + dst[i * height + j] = (sum / (size * 2)) as u8; + } + } +} + +/// Picks maximum of neighboring pixels (blur + lighten) +#[inline(never)] +pub(crate) fn liq_max3(src: &[u8], dst: &mut [u8], width: usize, height: usize) { + liq_op3(src, dst, width, height, |a, b| a.max(b)); +} + +pub(crate) fn liq_op3(src: &[u8], dst: &mut [u8], width: usize, height: usize, op: impl Fn(u8, u8) -> u8) { + for j in 0..height { + let row = &src[j * width..][..width]; + let dst = &mut dst[j * width..][..width]; + let prevrow = &src[j.saturating_sub(1) * width..][..width]; + let nextrow = &src[(j + 1).min(height - 1) * width..][..width]; + let mut prev: u8; + let mut curr = row[0]; + let mut next = row[0]; + for i in 0..width - 1 { + prev = curr; + curr = next; + next = row[i + 1]; + let t1 = op(prev, next); + let t2 = op(nextrow[i], prevrow[i]); + dst[i] = op(curr, op(t1, t2)); + } + let t1 = op(curr, next); + let t2 = op(nextrow[width - 1], prevrow[width - 1]); + dst[width - 1] = op(curr, op(t1, t2)); + } +} + +/// Picks minimum of neighboring pixels (blur + darken) +#[inline(never)] +pub(crate) fn liq_min3(src: &[u8], dst: &mut [u8], width: usize, height: usize) { + liq_op3(src, dst, width, height, |a, b| a.min(b)); +} + +/// Filters src image and saves it to dst, overwriting tmp in the process. +/// Image must be width*height pixels high. Size controls radius of box blur. +pub(crate) fn liq_blur(src_dst: &mut [u8], tmp: &mut [u8], width: usize, height: usize, size: u16) { + transposing_1d_blur(src_dst, tmp, width, height, size); + transposing_1d_blur(tmp, src_dst, height, width, size); +} diff --git a/src/capi.rs b/src/capi.rs new file mode 100644 index 0000000..a1150cf --- /dev/null +++ b/src/capi.rs @@ -0,0 +1,61 @@ +//! These are internal unstable private helper methods for imagequant-sys. +//! For public stable a C FFI interface, see imagequant-sys crate instead. +#![allow(missing_docs)] +#![allow(clippy::missing_safety_doc)] + +#[cfg(all(not(feature = "std"), feature = "no_std"))] +use crate::no_std_compat::*; + +use crate::pal::Palette; +use crate::rows::RowCallback; +use crate::seacow::{Pointer, RowBitmapMut, SeaCow}; +use crate::{Attributes, Error, Image, QuantizationResult, RGBA}; +use core::ffi::c_void; +use core::mem::{self, MaybeUninit}; + +pub const LIQ_VERSION: u32 = 40202; + +#[must_use] +pub fn liq_get_palette_impl(r: &mut QuantizationResult) -> &Palette { + r.int_palette() +} + +#[must_use] +pub unsafe fn liq_image_create_rgba_rows_impl<'rows>(attr: &Attributes, rows: &'rows [*const RGBA], width: u32, height: u32, gamma: f64) -> Option> { + let rows = SeaCow::borrowed(&*(rows as *const [*const rgb::Rgba] as *const [Pointer>])); + let rows_slice = rows.as_slice(); + if rows_slice.iter().any(|r| r.0.is_null()) { + return None; + } + crate::image::Image::new_internal(attr, crate::rows::PixelsSource::Pixels { rows, pixels: None }, width, height, gamma).ok() +} + +#[must_use] +pub unsafe fn liq_image_create_rgba_bitmap_impl<'rows>(attr: &Attributes, rows: Box<[*const RGBA]>, width: u32, height: u32, gamma: f64) -> Option> { + let rows = SeaCow::boxed(mem::transmute::, Box<[Pointer]>>(rows)); + let rows_slice = rows.as_slice(); + if rows_slice.iter().any(|r| r.0.is_null()) { + return None; + } + crate::image::Image::new_internal(attr, crate::rows::PixelsSource::Pixels { rows, pixels: None }, width, height, gamma).ok() +} + +#[must_use] +pub unsafe fn liq_image_create_custom_impl<'rows>(attr: &Attributes, row_callback: Box>, width: u32, height: u32, gamma: f64) -> Option> { + Image::new_internal(attr, crate::rows::PixelsSource::Callback(row_callback), width, height, gamma).ok() +} + +pub unsafe fn liq_write_remapped_image_impl(result: &mut QuantizationResult, input_image: &mut Image, buffer_bytes: &mut [MaybeUninit]) -> Result<(), Error> { + let rows = RowBitmapMut::new_contiguous(buffer_bytes, input_image.width()); + result.write_remapped_image_rows_internal(input_image, rows) +} + +pub unsafe fn liq_write_remapped_image_rows_impl(result: &mut QuantizationResult, input_image: &mut Image, rows: &mut [*mut MaybeUninit]) -> Result<(), Error> { + let rows = RowBitmapMut::new(rows, input_image.width()); + result.write_remapped_image_rows_internal(input_image, rows) +} + +/// Not recommended +pub unsafe fn liq_image_set_memory_ownership_impl(image: &mut Image<'_>, own_rows: bool, own_pixels: bool, free_fn: unsafe extern "C" fn(*mut c_void)) -> Result<(), Error> { + image.px.set_memory_ownership(own_rows, own_pixels, free_fn) +} diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..26fb669 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,63 @@ +use core::{error, fmt}; +use std::collections::TryReserveError; +pub use Error::*; + +/// Error codes +#[cfg_attr(feature = "_internal_c_ffi", repr(C))] +#[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 +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +#[allow(non_camel_case_types)] +pub enum Error { + /// Not an error. Exists for back-compat with the C API + #[cfg(feature = "_internal_c_ffi")] + LIQ_OK = 0, + /// [`set_quality()`][crate::Attributes::set_quality] was used with a minimum quality, and the minimum could not be achieved + QualityTooLow = 99, + /// Function called with invalid arguments + ValueOutOfRange = 100, + /// Either the system/process really hit a limit, or some data like image size was ridiculously wrong. Could be a bug too + OutOfMemory, + /// Progress callback said to stop + Aborted, + /// Some terrible inconsistency happened + InternalError, + /// Slice needs to be bigger, or width/height needs to be smaller + BufferTooSmall, + /// NULL pointer or use-after-free in the C API + InvalidPointer, + /// Congratulations, you've discovered an edge case + Unsupported, +} + +impl error::Error for Error {} + +impl fmt::Display for Error { + #[cold] + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match *self { + #[cfg(feature = "_internal_c_ffi")] + Self::LIQ_OK => "OK", + Self::QualityTooLow => "QUALITY_TOO_LOW", + Self::ValueOutOfRange => "VALUE_OUT_OF_RANGE", + Self::OutOfMemory => "OUT_OF_MEMORY", + Self::Aborted => "ABORTED", + Self::InternalError => "INTERNAL_ERROR", + Self::BufferTooSmall => "BUFFER_TOO_SMALL", + Self::InvalidPointer => "INVALID_POINTER", + Self::Unsupported => "UNSUPPORTED", + }) + } +} + +impl From for Error { + fn from(e: core::convert::Infallible) -> Self { + match e {} + } +} + +impl From for Error { + #[cold] + fn from(_: TryReserveError) -> Self { + Self::OutOfMemory + } +} diff --git a/src/hist.rs b/src/hist.rs new file mode 100644 index 0000000..afe30f9 --- /dev/null +++ b/src/hist.rs @@ -0,0 +1,436 @@ +use crate::error::*; +use crate::image::Image; +use crate::pal::{f_pixel, gamma_lut, PalIndex, ARGBF, MAX_COLORS, RGBA}; +use crate::quant::QuantizationResult; +use crate::rows::{temp_buf, DynamicRows}; +use crate::Attributes; +use core::{fmt, hash, mem}; +use core::hash::Hash; + +#[cfg(all(not(feature = "std"), feature = "no_std"))] +use std::{format, boxed::Box, vec::Vec}; + +#[cfg(all(not(feature = "std"), feature = "no_std"))] +use hashbrown::{HashMap, HashSet}; + +#[cfg(not(all(not(feature = "std"), feature = "no_std")))] +use std::collections::{HashMap, HashSet}; + + +/// Number of pixels in a given color for [`Histogram::add_colors()`] +/// +/// Used for building a histogram manually. Otherwise see [`Histogram::add_image()`] +#[repr(C)] +#[derive(Debug, Copy, Clone)] +pub struct HistogramEntry { + /// The color + pub color: RGBA, + /// Importance of the color (e.g. number of occurrences) + pub count: u32, +} + +/// Generate one shared palette for multiple images +/// +/// If you're converting one image at a time, see [`Attributes::new_image`] instead +pub struct Histogram { + gamma: Option, + fixed_colors: FixedColorsSet, + + /// The key is the RGBA cast to u32 + /// The value is a (boosted) count or 0 if it's a fixed color + hashmap: HashMap, + + posterize_bits: u8, + max_histogram_entries: u32, +} + +pub(crate) type FixedColorsSet = HashSet; + +#[derive(Clone)] +pub(crate) struct HistItem { + pub color: f_pixel, + pub adjusted_weight: f32, + pub perceptual_weight: f32, + pub mc_color_weight: f32, + /// Reused: mc_sort_value during median cut, then likely_palette_index after + pub sort_val_pal_index_union: u32, +} + +impl HistItem { + #[inline(always)] + pub fn mc_sort_value(&self) -> u32 { + self.sort_val_pal_index_union + } + + #[inline(always)] + pub fn likely_palette_index(&self) -> PalIndex { + self.sort_val_pal_index_union as PalIndex + } + + pub(crate) fn set_likely_palette_index(&mut self, index: PalIndex) { + self.sort_val_pal_index_union = index as u32; + } +} + +impl fmt::Debug for HistItem { + #[cold] + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("HistItem") + .field("color", &self.color) + .field("adjusted_weight", &self.adjusted_weight) + .field("perceptual_weight", &self.perceptual_weight) + .field("color_weight", &self.mc_color_weight) + .finish() + } +} + +impl Histogram { + /// Creates histogram object that will be used to collect color statistics from multiple images. + /// + /// All options should be set on `attr` before the histogram object is created. Options changed later may not have effect. + #[inline] + #[must_use] + pub fn new(attr: &Attributes) -> Self { + Self { + posterize_bits: attr.posterize_bits(), + max_histogram_entries: attr.max_histogram_entries, + fixed_colors: HashSet::with_hasher(U32Hasher(0)), + hashmap: HashMap::with_hasher(U32Hasher(0)), + gamma: None, + } + } + + /// "Learns" colors from the image, which will be later used to generate the palette. + /// + /// Fixed colors added to the image are also added to the histogram. If the total number of fixed colors exceeds 256, + /// this function will fail with `LIQ_BUFFER_TOO_SMALL`. + #[inline(never)] + pub fn add_image(&mut self, attr: &Attributes, image: &mut Image) -> Result<(), Error> { + let width = image.width(); + let height = image.height(); + if image.importance_map.is_none() && attr.use_contrast_maps { + image.contrast_maps()?; + } + + self.gamma = image.gamma(); + + if !image.fixed_colors.is_empty() { + self.fixed_colors.extend(image.fixed_colors.iter().copied().enumerate().map(|(idx, rgba)| { + HashColor { rgba, index: idx as _ } + })); + } + + if attr.progress(f32::from(attr.progress_stage1) * 0.40) { + return Err(Aborted); // bow can free the RGBA source if copy has been made in f_pixels + } + + let posterize_bits = attr.posterize_bits(); + let surface_area = height * width; + let estimated_colors = (surface_area / (posterize_bits as usize + if surface_area > 512 * 512 { 7 } else { 5 })).min(250_000); + self.reserve(estimated_colors); + + self.add_pixel_rows(&image.px, image.importance_map.as_deref(), posterize_bits)?; + + Ok(()) + } + + /// Alternative to `add_image()`. Intead of counting colors in an image, it directly takes an array of colors and their counts. + /// + /// This function is only useful if you already have a histogram of the image from another source. + /// + /// The gamma may be 0 to mean sRGB. All calls to `add_colors` and `add_fixed_color` should use the same gamma value. + #[inline(never)] + pub fn add_colors(&mut self, entries: &[HistogramEntry], gamma: f64) -> Result<(), Error> { + if entries.is_empty() || entries.len() > 1 << 24 { + return Err(ValueOutOfRange); + } + + if !(0. ..1.).contains(&gamma) { + return Err(ValueOutOfRange); + } + + if self.gamma.is_none() && gamma > 0. { + self.gamma = Some(gamma); + } + + self.reserve(entries.len()); + + for e in entries { + self.add_color(e.color, e.count); + } + + Ok(()) + } + + /// Add a color guaranteed to be in the final palette + /// + /// The gamma may be 0 to mean sRGB. All calls to `add_colors` and `add_fixed_color` should use the same gamma value. + pub fn add_fixed_color(&mut self, rgba: RGBA, gamma: f64) -> Result<(), Error> { + if self.fixed_colors.len() >= MAX_COLORS { + return Err(Unsupported); + } + + if self.gamma.is_none() && gamma > 0. { + self.gamma = Some(gamma); + } + + let idx = self.fixed_colors.len(); + self.fixed_colors.insert(HashColor { rgba, index: idx as _ }); + + Ok(()) + } + + /// Generate palette for all images/colors added to the histogram. + /// + /// Palette generated using this function won't be improved during remapping. + /// If you're generating palette for only one image, it's better not to use the `Histogram`. + #[inline] + pub fn quantize(&mut self, attr: &Attributes) -> Result { + self.quantize_internal(attr, true) + } + + #[inline(never)] + pub(crate) fn quantize_internal(&mut self, attr: &Attributes, freeze_result_colors: bool) -> Result { + if self.hashmap.is_empty() && self.fixed_colors.is_empty() { + return Err(Unsupported); + } + + if attr.progress(0.) { return Err(Aborted); } + if attr.progress(f32::from(attr.progress_stage1) * 0.89) { + return Err(Aborted); + } + + let gamma = self.gamma.unwrap_or(0.45455); + let hist = self.finalize_builder(gamma).map_err(|_| OutOfMemory)?; + + attr.verbose_print(format!(" made histogram...{} colors found", hist.items.len())); + + QuantizationResult::new(attr, hist, freeze_result_colors, gamma) + } + + #[inline(always)] + fn add_color(&mut self, rgba: RGBA, boost: u32) { + if boost == 0 { + return; + } + + let px_int = if rgba.a != 0 { + self.posterize_mask() & unsafe { RGBAInt { rgba }.int } + } else { 0 }; + + self.hashmap.entry(px_int) + // it can overflow on images over 2^24 pixels large + .and_modify(move |e| e.0 = e.0.saturating_add(boost)) + .or_insert((boost, rgba)); + } + + fn reserve(&mut self, entries: usize) { + let new_entries = entries.saturating_sub(self.hashmap.len() / 3); // assume some will be dupes, if called multiple times + self.hashmap.reserve(new_entries); + } + + #[inline(always)] + const fn posterize_mask(&self) -> u32 { + let channel_mask = 255 << self.posterize_bits; + u32::from_ne_bytes([channel_mask, channel_mask, channel_mask, channel_mask]) + } + + /// optionallys et + fn init_posterize_bits(&mut self, posterize_bits: u8) { + if self.posterize_bits >= posterize_bits { + return; + } + self.posterize_bits = posterize_bits; + let new_posterize_mask = self.posterize_mask(); + + let new_size = (self.hashmap.len() / 3).max(self.hashmap.capacity() / 5); + let old_hashmap = mem::replace(&mut self.hashmap, HashMap::with_capacity_and_hasher(new_size, U32Hasher(0))); + self.hashmap.extend(old_hashmap.into_iter().map(move |(k, v)| { + (k & new_posterize_mask, v) + })); + } + + pub(crate) fn add_pixel_rows(&mut self, image: &DynamicRows<'_, '_>, importance_map: Option<&[u8]>, posterize_bits: u8) -> Result<(), Error> { + let width = image.width as usize; + let height = image.height as usize; + + debug_assert!(importance_map.map_or(true, |m| m.len() == image.width() * image.height())); + + let mut importance_map = importance_map.unwrap_or(&[]).chunks_exact(width).fuse(); + let image_iter = image.rgba_rows_iter()?; + + let mut temp_row = temp_buf(width)?; + for row in 0..height { + let pixels_row = &image_iter.row_rgba(&mut temp_row, row)[..width]; + let importance_map = importance_map.next().map(move |m| &m[..width]).unwrap_or(&[]); + for (col, px) in pixels_row.iter().copied().enumerate() { + let boost = importance_map.get(col).copied().unwrap_or(255); + self.add_color(px, boost.into()); + } + } + self.init_posterize_bits(posterize_bits); + + if self.hashmap.len() > self.max_histogram_entries as usize && self.posterize_bits < 3 { + self.init_posterize_bits(self.posterize_bits + 1); + } + Ok(()) + } + + pub(crate) fn finalize_builder(&mut self, gamma: f64) -> Result { + debug_assert!(gamma > 0.); + + // Fixed colors will be put into normal hashmap, but with very high weight, + // and temporarily 0 means this fixed max weight + for &HashColor { rgba, .. } in &self.fixed_colors { + let px_int = if rgba.a != 0 { + unsafe { RGBAInt { rgba }.int } + } else { 0 }; + + self.hashmap.insert(px_int, (0, rgba)); + } + + let mut temp = Vec::new(); + temp.try_reserve_exact(self.hashmap.len())?; + + let mut counts = [0; LIQ_MAXCLUSTER]; + temp.extend(self.hashmap.values().map(|&(boost, color)| { + let cluster_index = ((color.r >> 7) << 3) | ((color.g >> 7) << 2) | ((color.b >> 7) << 1) | (color.a >> 7); + counts[cluster_index as usize] += 1; + + // fixed colors result in weight == 0. + let weight = boost as f32; + TempHistItem { color, weight, cluster_index } + })); + + let mut clusters = [Cluster { begin: 0, end: 0 }; LIQ_MAXCLUSTER]; + let mut next_begin = 0; + for (cluster, count) in clusters.iter_mut().zip(counts) { + cluster.begin = next_begin; + cluster.end = next_begin; + next_begin += count; + } + + let mut items = Vec::new(); + items.try_reserve_exact(temp.len())?; + items.resize(temp.len(), HistItem { + color: if cfg!(debug_assertions) { f_pixel( ARGBF { r:f32::NAN, g:f32::NAN, b:f32::NAN, a:f32::NAN } ) } else { f_pixel::default() }, + adjusted_weight: if cfg!(debug_assertions) { f32::NAN } else { 0. }, + perceptual_weight: if cfg!(debug_assertions) { f32::NAN } else { 0. }, + mc_color_weight: if cfg!(debug_assertions) { f32::NAN } else { 0. }, + sort_val_pal_index_union: if cfg!(debug_assertions) { !0 } else { 0 }, + }); + let mut items = items.into_boxed_slice(); + + // Limit perceptual weight to 1/10th of the image surface area to prevent + // a single color from dominating all others. + let max_perceptual_weight = ((0.1 / 255.) * temp.iter().map(|t| f64::from(t.weight)).sum::()) as f32; + + let lut = gamma_lut(gamma); + let mut total_perceptual_weight = 0.; + for temp_item in temp { + let cluster = &mut clusters[temp_item.cluster_index as usize]; + let next_index = cluster.end as usize; + cluster.end += 1; + + // weight == 0 means it's a fixed color + let weight = if temp_item.weight > 0. { + (temp_item.weight * (1. / 255.)).min(max_perceptual_weight) + } else { + max_perceptual_weight * 16. + }; + total_perceptual_weight += f64::from(weight); + + items[next_index].color = f_pixel::from_rgba(&lut, temp_item.color); + items[next_index].perceptual_weight = weight; + items[next_index].adjusted_weight = weight; + } + + let mut fixed_colors: Vec<_> = self.fixed_colors.iter().collect(); + fixed_colors.sort_by_key(|c| c.index); // original order + let fixed_colors = fixed_colors.iter().map(|c| f_pixel::from_rgba(&lut, c.rgba)).collect(); + + Ok(HistogramInternal { items, total_perceptual_weight, clusters, fixed_colors }) + } +} + +#[derive(Copy, Clone)] +struct TempHistItem { + color: RGBA, + weight: f32, + cluster_index: u8, +} + +#[repr(C)] +union RGBAInt { + rgba: RGBA, + int: u32, +} + +/// Clusters form initial boxes for quantization, to ensure extreme colors are better represented +pub const LIQ_MAXCLUSTER: usize = 16; + +pub(crate) struct HistogramInternal { + pub items: Box<[HistItem]>, + pub total_perceptual_weight: f64, + pub clusters: [Cluster; LIQ_MAXCLUSTER], + pub fixed_colors: Box<[f_pixel]>, +} + +// Pre-grouped colors +#[derive(Copy, Clone, Debug)] +pub(crate) struct Cluster { + pub begin: u32, + pub end: u32, +} + +// Simple deterministic hasher for the color hashmap +impl hash::BuildHasher for U32Hasher { + type Hasher = Self; + + #[inline(always)] + fn build_hasher(&self) -> Self { + Self(0) + } +} + +pub(crate) struct U32Hasher(pub u32); +impl hash::Hasher for U32Hasher { + // magic constant from fxhash. For a single 32-bit key that's all it needs! + #[inline(always)] + fn finish(&self) -> u64 { u64::from(self.0).wrapping_mul(0x517cc1b727220a95) } + #[inline(always)] + fn write_u32(&mut self, i: u32) { self.0 = i; } + + fn write(&mut self, _bytes: &[u8]) { unimplemented!() } + fn write_u8(&mut self, _i: u8) { unimplemented!() } + fn write_u16(&mut self, _i: u16) { unimplemented!() } + fn write_u64(&mut self, _i: u64) { unimplemented!() } + fn write_u128(&mut self, _i: u128) { unimplemented!() } + fn write_usize(&mut self, _i: usize) { unimplemented!() } + fn write_i8(&mut self, _i: i8) { unimplemented!() } + fn write_i16(&mut self, _i: i16) { unimplemented!() } + fn write_i32(&mut self, _i: i32) { unimplemented!() } + fn write_i64(&mut self, _i: i64) { unimplemented!() } + fn write_i128(&mut self, _i: i128) { unimplemented!() } + fn write_isize(&mut self, _i: isize) { unimplemented!() } +} + +/// ignores the index +#[derive(PartialEq, Debug)] +pub(crate) struct HashColor { + pub rgba: RGBA, + pub index: PalIndex, +} + +#[allow(clippy::derived_hash_with_manual_eq)] +impl Hash for HashColor { + #[inline] + fn hash(&self, state: &mut H) { + let s: &[u8] = self.rgba.as_ref(); + u32::from_ne_bytes(s.try_into().unwrap()).hash(state); + } +} + +impl Eq for HashColor { + fn assert_receiver_is_total_eq(&self) {} +} diff --git a/src/image.rs b/src/image.rs new file mode 100644 index 0000000..0681ccf --- /dev/null +++ b/src/image.rs @@ -0,0 +1,349 @@ +use crate::attr::Attributes; +use crate::blur::{liq_blur, liq_max3, liq_min3}; +use crate::error::*; +use crate::pal::{f_pixel, PalF, PalIndexRemap, MAX_COLORS, RGBA}; +use crate::remap::DitherMapMode; +use crate::rows::{DynamicRows, PixelsSource}; +use crate::seacow::{RowBitmap, SeaCow}; +use crate::{PushInCapacity, LIQ_HIGH_MEMORY_LIMIT}; +use rgb::prelude::*; +use core::mem::{self, MaybeUninit}; + +#[cfg(all(not(feature = "std"), feature = "no_std"))] +use crate::no_std_compat::*; + +/// Describes image dimensions and pixels for the library +/// +/// Create one using [`Attributes::new_image()`]. +/// +/// All images are internally in the RGBA format. +#[derive(Clone)] +pub struct Image<'pixels> { + pub(crate) px: DynamicRows<'pixels, 'pixels>, + pub(crate) importance_map: Option>, + pub(crate) edges: Option>, + pub(crate) dither_map: Option>, + pub(crate) background: Option>>, + pub(crate) fixed_colors: Vec, +} + +impl<'pixels> Image<'pixels> { + /// Makes an image from RGBA pixels. + /// + /// See the [`rgb`] and [`bytemuck`](https://lib.rs/bytemuck) crates for making `[RGBA]` slices from `[u8]` slices. + /// + /// The `pixels` argument can be `Vec`, or `Box<[RGBA]>` or `&[RGBA]`. + /// + /// 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. + /// + /// Use `0.` for gamma if the image is sRGB (most images are). + #[inline(always)] + pub fn new(attr: &Attributes, pixels: VecRGBA, width: usize, height: usize, gamma: f64) -> Result where VecRGBA: Into> { + Self::new_stride(attr, pixels, width, height, width, gamma) + } + + /// Describe dimensions of a slice of RGBA pixels. + /// + /// Same as [`Image::new`], except it doesn't copy the pixels, but holds a temporary reference instead. + /// + /// If you want to supply RGB or ARGB pixels, use [`Image::new_fn`] to supply your own pixel-swapping function. + /// + /// See the [`rgb`] and [`bytemuck`](https://lib.rs/bytemuck) crates for making `[RGBA]` slices from `[u8]` slices. + /// + /// Use `0.` for gamma if the image is sRGB (most images are). + #[inline(always)] + pub fn new_borrowed(attr: &Attributes, pixels: &'pixels [RGBA], width: usize, height: usize, gamma: f64) -> Result { + Self::new_stride_borrowed(attr, pixels, width, height, width, gamma) + } + + /// Generate rows on demand using a callback function. + /// + /// 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). + /// The callback will be called multiple times per row. May be called from multiple threads at once. + /// + /// Use `0.` for gamma if the image is sRGB (most images are). + /// + /// ## Safety + /// + /// This function is marked as unsafe, because the callback function MUST initialize the entire row (call `write` on every `MaybeUninit` pixel). + /// + pub unsafe fn new_fn], usize) + Send + Sync>(attr: &Attributes, convert_row_fn: F, width: usize, height: usize, gamma: f64) -> Result { + let width = width.try_into().map_err(|_| ValueOutOfRange)?; + let height = height.try_into().map_err(|_| ValueOutOfRange)?; + Image::new_internal(attr, PixelsSource::Callback(Box::new(convert_row_fn)), width, height, gamma) + } + + pub(crate) fn free_histogram_inputs(&mut self) { + // importance_map must stay for remapping, because remap performs kmeans on potentially-unimportant pixels + self.px.free_histogram_inputs(); + } + + pub(crate) fn new_internal( + attr: &Attributes, + pixels: PixelsSource<'pixels, 'pixels>, + width: u32, + height: u32, + gamma: f64, + ) -> Result { + if !Self::check_image_size(width, height) { + return Err(ValueOutOfRange); + } + + if !(0. ..=1.).contains(&gamma) { + attr.verbose_print(" error: gamma must be >= 0 and <= 1 (try 1/gamma instead)"); + return Err(ValueOutOfRange); + } + let img = Image { + px: DynamicRows::new( + width, + height, + pixels, + if gamma > 0. { gamma } else { 0.45455 }, + ), + importance_map: None, + edges: None, + dither_map: None, + background: None, + fixed_colors: Vec::new(), + }; + // if image is huge or converted pixels are not likely to be reused then don't cache converted pixels + let low_memory_hint = !attr.use_contrast_maps && attr.use_dither_map == DitherMapMode::None; + let limit = if low_memory_hint { LIQ_HIGH_MEMORY_LIMIT / 8 } else { LIQ_HIGH_MEMORY_LIMIT } / mem::size_of::(); + if (img.width()) * (img.height()) > limit { + attr.verbose_print(" conserving memory"); // for simplicity of this API there's no explicit pixels argument, + } + Ok(img) + } + + fn check_image_size(width: u32, height: u32) -> bool { + if width == 0 || height == 0 { + return false; + } + if width.max(height) as usize > i32::MAX as usize || + width as usize > isize::MAX as usize / mem::size_of::() / height as usize { + return false; + } + true + } + + pub(crate) fn update_dither_map(&mut self, remapped_image: &RowBitmap<'_, PalIndexRemap>, palette: &PalF, uses_background: bool) -> Result<(), Error> { + if self.edges.is_none() { + self.contrast_maps()?; + } + let Some(mut edges) = self.edges.take() else { return Ok(()) }; + let colors = palette.as_slice(); + + let width = self.width(); + let mut prev_row: Option<&[_]> = None; + let mut rows = remapped_image.rows().zip(edges.chunks_exact_mut(width)).peekable(); + while let Some((this_row, edges)) = rows.next() { + let mut lastpixel = this_row[0]; + let mut lastcol = 0; + for (col, px) in this_row.iter().copied().enumerate().skip(1) { + if uses_background && colors[px as usize].is_fully_transparent() { + // Transparency may or may not create an edge. When there's an explicit background set, assume no edge. + continue; + } + if px != lastpixel || col == width - 1 { + let mut neighbor_count = 10 * (col - lastcol); + let mut i = lastcol; + while i < col { + if let Some(prev_row) = prev_row { + let pixelabove = prev_row[i]; + if pixelabove == lastpixel { neighbor_count += 15; } + } + if let Some((next_row, _)) = rows.peek() { + let pixelbelow = next_row[i]; + if pixelbelow == lastpixel { neighbor_count += 15; } + } + i += 1; + } + while lastcol <= col { + edges[lastcol] = (f32::from(u16::from(edges[lastcol]) + 128) + * (255. / (255 + 128) as f32) + * (1. - 20. / (20 + neighbor_count) as f32)) + as u8; + lastcol += 1; + } + lastpixel = px; + } + } + prev_row = Some(this_row); + } + self.dither_map = Some(edges); + Ok(()) + } + + /// Set which pixels are more important (and more likely to get a palette entry) + /// + /// The map must be `width`×`height` pixels large. Higher numbers = more important. + pub fn set_importance_map(&mut self, map: impl Into>) -> Result<(), Error> { + let map = map.into(); + if map.len() != self.width() * self.height() { + return Err(BufferTooSmall); + } + self.importance_map = Some(map); + Ok(()) + } + + /// Remap pixels assuming they will be displayed on this background. This is designed for GIF's "keep" mode. + /// + /// Pixels that match the background color will be made transparent if there's a fully transparent color available in the palette. + /// + /// The background image's pixels must outlive this image. + pub fn set_background(&mut self, background: Self) -> Result<(), Error> { + if background.background.is_some() { + return Err(Unsupported); + } + if self.px.width != background.px.width || self.px.height != background.px.height { + return Err(BufferTooSmall); + } + self.background = Some(Box::new(background)); + Ok(()) + } + + /// 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. + /// + /// The RGB values are assumed to have the same gamma as the image. + /// + /// It must be called before the image is quantized. + /// + /// 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. + pub fn add_fixed_color(&mut self, color: RGBA) -> Result<(), Error> { + if self.fixed_colors.len() >= MAX_COLORS { return Err(Unsupported); } + self.fixed_colors.try_reserve(1)?; + self.fixed_colors.push_in_cap(color); + Ok(()) + } + + /// Width of the image in pixels + #[must_use] + #[inline(always)] + pub const fn width(&self) -> usize { + self.px.width as _ + } + + /// Height of the image in pixels + #[must_use] + #[inline(always)] + pub const fn height(&self) -> usize { + self.px.height as _ + } + + #[inline(always)] + pub(crate) fn gamma(&self) -> Option { + if self.px.gamma > 0. { Some(self.px.gamma) } else { None } + } + + /// Builds two maps: + /// `importance_map` - approximation of areas with high-frequency noise, except straight edges. 1=flat, 0=noisy. + /// edges - noise map including all edges + pub(crate) fn contrast_maps(&mut self) -> Result<(), Error> { + let width = self.width(); + let height = self.height(); + if width < 4 || height < 4 || (3 * width * height) > LIQ_HIGH_MEMORY_LIMIT { + return Ok(()); // shrug + } + + let noise = if let Some(n) = self.importance_map.as_deref_mut() { n } else { + let vec = try_zero_vec(width * height)?; + self.importance_map.get_or_insert_with(move || vec.into_boxed_slice()) + }; + + let edges = if let Some(e) = self.edges.as_mut() { e } else { + let vec = try_zero_vec(width * height)?; + self.edges.get_or_insert_with(move || vec.into_boxed_slice()) + }; + + let mut rows_iter = self.px.all_rows_f()?.chunks_exact(width); + + let mut next_row = rows_iter.next().ok_or(Error::InvalidPointer)?; + let mut curr_row = next_row; + let mut prev_row; + + for (noise_row, edges_row) in noise[..width * height].chunks_exact_mut(width).zip(edges[..width * height].chunks_exact_mut(width)) { + prev_row = curr_row; + curr_row = next_row; + next_row = rows_iter.next().unwrap_or(next_row); + let mut prev; + let mut curr = curr_row[0].0; + let mut next = curr; + for i in 0..width { + prev = curr; + curr = next; + next = curr_row[(i + 1).min(width - 1)].0; + // contrast is difference between pixels neighbouring horizontally and vertically + let horiz = (prev + next - curr * 2.).map(f32::abs); // noise is amplified + let prevl = prev_row[i].0; + let nextl = next_row[i].0; + let vert = (prevl + nextl - curr * 2.).map(f32::abs); + let horiz = horiz.a.max(horiz.r).max(horiz.g.max(horiz.b)); + let vert = vert.a.max(vert.r).max(vert.g.max(vert.b)); + let edge = horiz.max(vert); + let mut z = (horiz - vert).abs().mul_add(-0.5, edge); + z = 1. - z.max(horiz.min(vert)); + z *= z; + z *= z; + // 85 is about 1/3rd of weight (not 0, because noisy pixels still need to be included, just not as precisely). + noise_row[i] = z.mul_add(176., 80.) as u8; + edges_row[i] = ((1. - edge) * 256.) as u8; + } + } + // noise areas are shrunk and then expanded to remove thin edges from the map + let mut tmp = try_zero_vec(width * height)?; + liq_max3(noise, &mut tmp, width, height); + liq_max3(&tmp, noise, width, height); + liq_blur(noise, &mut tmp, width, height, 3); + liq_max3(noise, &mut tmp, width, height); + liq_min3(&tmp, noise, width, height); + liq_min3(noise, &mut tmp, width, height); + liq_min3(&tmp, noise, width, height); + liq_min3(edges, &mut tmp, width, height); + liq_max3(&tmp, edges, width, height); + for (edges, noise) in edges.iter_mut().zip(noise) { + *edges = (*noise).min(*edges); + } + Ok(()) + } + + /// Stride is in pixels. Allows defining regions of larger images or images with padding without copying. The stride is in pixels. + /// + /// Otherwise the same as [`Image::new_borrowed`]. + #[inline(always)] + pub fn new_stride_borrowed(attr: &Attributes, pixels: &'pixels [RGBA], width: usize, height: usize, stride: usize, gamma: f64) -> Result { + Self::new_stride_internal(attr, SeaCow::borrowed(pixels), width, height, stride, gamma) + } + + /// Create new image by copying `pixels` to an internal buffer, so that it makes a self-contained type. + /// + /// The `pixels` argument can be `Vec`, or `Box<[RGBA]>` or `&[RGBA]`. + /// + /// Otherwise the same as [`Image::new_stride_borrowed`]. + #[inline] + pub fn new_stride(attr: &Attributes, pixels: VecRGBA, width: usize, height: usize, stride: usize, gamma: f64) -> Result, Error> where VecRGBA: Into> { + Self::new_stride_internal(attr, SeaCow::boxed(pixels.into()), width, height, stride, gamma) + } + + fn new_stride_internal<'a>(attr: &Attributes, pixels: SeaCow<'a, RGBA>, width: usize, height: usize, stride: usize, gamma: f64) -> Result, Error> { + let width = width.try_into().map_err(|_| ValueOutOfRange)?; + let height = height.try_into().map_err(|_| ValueOutOfRange)?; + let stride = stride.try_into().map_err(|_| ValueOutOfRange)?; + + let pixels_len = pixels.as_slice().len(); + let pixels_rows = match PixelsSource::for_pixels(pixels, width, height, stride) { + Ok(p) => p, + Err(e) => { + attr.verbose_print(format!("Buffer length is {} bytes, which is not enough for {}×{}×4 RGBA bytes", pixels_len * 4, stride, height)); + return Err(e); + }, + }; + Image::new_internal(attr, pixels_rows, width, height, gamma) + } +} + +fn try_zero_vec(len: usize) -> Result, Error> { + let mut vec = Vec::new(); + vec.try_reserve_exact(len)?; + vec.resize(len, 0); + Ok(vec) +} diff --git a/src/kmeans.rs b/src/kmeans.rs new file mode 100644 index 0000000..3545133 --- /dev/null +++ b/src/kmeans.rs @@ -0,0 +1,159 @@ +use crate::hist::{HistItem, HistogramInternal}; +use crate::nearest::Nearest; +use crate::pal::{f_pixel, PalF, PalIndex, PalPop}; +use crate::rayoff::*; +use crate::{CacheLineAlign, Error}; +use core::cell::RefCell; +use rgb::prelude::*; +use rgb::Argb; + +#[cfg(all(not(feature = "std"), feature = "no_std"))] +use crate::no_std_compat::*; + +/// K-Means iteration: new palette color is computed from weighted average of colors that map best to that palette entry. +// avoid false sharing +pub(crate) struct Kmeans { + averages: Vec, + weighed_diff_sum: f64, +} + +#[derive(Copy, Clone, Default)] +struct ColorAvg { + pub sum: Argb, + pub total: f64, +} + +impl Kmeans { + #[inline] + pub fn new(pal_len: usize) -> Result { + let mut averages = Vec::new(); + averages.try_reserve_exact(pal_len)?; + averages.resize(pal_len, ColorAvg::default()); + Ok(Self { + averages, + weighed_diff_sum: 0., + }) + } + + #[inline] + pub fn update_color(&mut self, px: f_pixel, value: f32, matched: PalIndex) { + let c = &mut self.averages[matched as usize]; + c.sum += (px.0 * value).map(f64::from); + c.total += f64::from(value); + } + + pub fn finalize(self, palette: &mut PalF) -> f64 { + for (avg, (color, pop)) in self.averages.iter().zip(palette.iter_mut()).filter(|(_, (_, pop))| !pop.is_fixed()) { + let total = avg.total; + *pop = PalPop::new(total as f32); + if total > 0. && color.a != 0. { + *color = avg.sum.map(move |c| (c / total) as f32).into(); + } + } + self.weighed_diff_sum + } + + #[inline(never)] + pub(crate) fn iteration(hist: &mut HistogramInternal, palette: &mut PalF, adjust_weight: bool) -> Result { + if hist.items.is_empty() { + return Ok(0.); + } + + let n = Nearest::new(palette)?; + let colors = palette.as_slice(); + let len = colors.len(); + + let tls = ThreadLocal::new(); + let total = hist.total_perceptual_weight; + + // chunk size is a trade-off between parallelization and overhead + hist.items.par_chunks_mut(256).for_each_init( + || tls.get_or(move || CacheLineAlign(RefCell::new(Self::new(len)))), + move |kmeans, batch| { + let Ok(mut tls) = kmeans.0.try_borrow_mut() else { + debug_assert!(false); + return; + }; + if let Ok(ref mut kmeans) = *tls { + kmeans.iterate_batch(batch, &n, colors, adjust_weight); + } + }); + + let diff = tls.into_iter() + .map(|c| c.0.into_inner()) + .reduce(Self::try_merge) + .transpose()? + .map_or(0., |kmeans| kmeans.finalize(palette) / total); + + replace_unused_colors(palette, hist)?; + Ok(diff) + } + + fn iterate_batch(&mut self, batch: &mut [HistItem], n: &Nearest, colors: &[f_pixel], adjust_weight: bool) { + self.weighed_diff_sum += batch.iter_mut().map(|item| { + let px = item.color; + let (matched, mut diff) = n.search(&px, item.likely_palette_index()); + item.set_likely_palette_index(matched); + if adjust_weight { + let remapped = colors[matched as usize]; + let (_, new_diff) = n.search(&f_pixel(px.0 + px.0 - remapped.0), matched); + debug_assert!(new_diff.is_finite()); + diff = new_diff; + item.adjusted_weight = 2.0f32.mul_add(item.adjusted_weight, item.perceptual_weight) * (0.5 + diff); + } + debug_assert!(f64::from(diff) < 1e20); + self.update_color(px, item.adjusted_weight, matched); + f64::from(diff * item.perceptual_weight) + }).sum::(); + } + + #[inline] + pub fn merge(mut self, new: Self) -> Self { + self.weighed_diff_sum += new.weighed_diff_sum; + self.averages.iter_mut().zip(new.averages).for_each(|(p, n)| { + p.sum += n.sum; + p.total += n.total; + }); + self + } + + #[inline] + pub fn try_merge(old: Result, new: Result) -> Result { + match (old, new) { + (Ok(old), Ok(new)) => Ok(Self::merge(old, new)), + (Err(e), _) | (_, Err(e)) => Err(e), + } + } +} + +/// kmeans may have merged or obsoleted some palette entries. +/// This replaces these entries with histogram colors that are currently least-fitting the palette. +fn replace_unused_colors(palette: &mut PalF, hist: &HistogramInternal) -> Result<(), Error> { + for pal_idx in 0..palette.len() { + let Some(pop) = palette.pop_as_slice().get(pal_idx) else { break }; + if pop.popularity() == 0. && !pop.is_fixed() { + let n = Nearest::new(palette)?; + let mut worst = None; + let mut worst_diff = 0.; + let colors = palette.as_slice(); + // the search is just for diff, ignoring adjusted_weight, + // because the palette already optimizes for the max weight, so it'd likely find another redundant entry. + for item in hist.items.iter() { + // the early reject avoids running full palette search for every entry + let may_be_worst = colors.get(item.likely_palette_index() as usize) + .map_or(true, |pal| pal.diff(&item.color) > worst_diff); + if may_be_worst { + let diff = n.search(&item.color, item.likely_palette_index()).1; + if diff > worst_diff { + worst_diff = diff; + worst = Some(item); + } + } + } + if let Some(worst) = worst { + palette.set(pal_idx, worst.color, PalPop::new(worst.adjusted_weight)); + } + } + } + Ok(()) +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..bd311a2 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,412 @@ +//! +//! +//! Converts RGBA images to 8-bit with alpha channel. +//! +//! See `examples/` directory for example code. +#![cfg_attr(all(not(feature = "std"), feature = "no_std"), no_std)] + +#![doc(html_logo_url = "https://pngquant.org/pngquant-logo.png")] +#![deny(missing_docs)] +#![allow(clippy::bool_to_int_with_if)] +#![allow(clippy::cast_possible_truncation)] +#![allow(clippy::doc_markdown)] +#![allow(clippy::if_not_else)] +#![allow(clippy::inline_always)] +#![allow(clippy::items_after_statements)] +#![allow(clippy::map_unwrap_or)] +#![allow(clippy::missing_errors_doc)] +#![allow(clippy::module_name_repetitions)] +#![allow(clippy::redundant_closure_for_method_calls)] +#![allow(clippy::unreadable_literal)] +#![allow(clippy::wildcard_imports)] +#![deny(clippy::semicolon_if_nothing_returned)] + +#[cfg(all(not(feature = "std"), feature = "no_std"))] +extern crate alloc as std; + +#[cfg(all(not(feature = "std"), feature = "no_std"))] +use std::vec::Vec; + +mod attr; +mod blur; +mod error; +mod hist; +mod image; +mod kmeans; +mod mediancut; +mod nearest; +mod pal; +mod quant; +mod remap; +mod rows; +mod seacow; + +#[cfg(not(feature = "threads"))] +mod rayoff; + +#[cfg(feature = "threads")] +mod rayoff { + pub(crate) fn num_cpus() -> usize { std::thread::available_parallelism().map(|n| n.get()).unwrap_or(1) } + pub(crate) use rayon::in_place_scope as scope; + pub(crate) use rayon::prelude::{ParallelBridge, ParallelIterator, ParallelSliceMut}; + pub(crate) use thread_local::ThreadLocal; +} + +#[cfg_attr(feature = "threads", repr(align(128)))] +pub(crate) struct CacheLineAlign(pub T); + +/// Use imagequant-sys crate instead +#[cfg(feature = "_internal_c_ffi")] +pub mod capi; + +use core::cmp::Ordering; + +pub use attr::{Attributes, ControlFlow}; +pub use error::Error; +pub use hist::{Histogram, HistogramEntry}; +pub use image::Image; +#[doc(hidden)] +pub use pal::Palette; +pub use pal::RGBA; +pub use quant::QuantizationResult; + +#[doc(hidden)] +#[deprecated(note = "Please use the imagequant::Error type. This will be removed")] +pub use error::Error as liq_error; + +const LIQ_HIGH_MEMORY_LIMIT: usize = 1 << 26; + +/// [Start here][Attributes]: creates new handle for library configuration +/// +/// See [`Attributes`] +#[inline(always)] +#[must_use] +pub fn new() -> Attributes { + Attributes::new() +} + +#[test] +fn copy_img() { + let tmp = vec![RGBA::new(1, 2, 3, 4); 10 * 100]; + let liq = Attributes::new(); + let _ = liq.new_image_stride(tmp, 10, 100, 10, 0.).unwrap(); +} + +#[test] +fn takes_rgba() { + let liq = Attributes::new(); + + let img = vec![RGBA { r: 0, g: 0, b: 0, a: 0 }; 8]; + + liq.new_image_borrowed(&img, 1, 1, 0.0).unwrap(); + liq.new_image_borrowed(&img, 4, 2, 0.0).unwrap(); + liq.new_image_borrowed(&img, 8, 1, 0.0).unwrap(); + assert!(liq.new_image_borrowed(&img, 9, 1, 0.0).is_err()); + assert!(liq.new_image_borrowed(&img, 4, 3, 0.0).is_err()); +} + +#[test] +fn histogram() { + let attr = Attributes::new(); + let mut hist = Histogram::new(&attr); + + let bitmap1 = [RGBA { r: 0, g: 0, b: 0, a: 0 }; 1]; + let mut image1 = attr.new_image(&bitmap1[..], 1, 1, 0.0).unwrap(); + hist.add_image(&attr, &mut image1).unwrap(); + + let bitmap2 = [RGBA { r: 255, g: 255, b: 255, a: 255 }; 1]; + let mut image2 = attr.new_image(&bitmap2[..], 1, 1, 0.0).unwrap(); + hist.add_image(&attr, &mut image2).unwrap(); + + hist.add_colors(&[HistogramEntry { + color: RGBA::new(255, 128, 255, 128), + count: 10, + }], 0.0).unwrap(); + + let mut res = hist.quantize(&attr).unwrap(); + let pal = res.palette(); + assert_eq!(3, pal.len()); +} + +#[test] +fn poke_it() { + let width = 10usize; + let height = 10usize; + let mut fakebitmap = vec![RGBA::new(255, 255, 255, 255); width * height]; + + fakebitmap[0].r = 0x55; + fakebitmap[0].g = 0x66; + fakebitmap[0].b = 0x77; + + // Configure the library + let mut liq = Attributes::new(); + liq.set_speed(5).unwrap(); + liq.set_quality(70, 99).unwrap(); + liq.set_min_posterization(1).unwrap(); + assert_eq!(1, liq.min_posterization()); + liq.set_min_posterization(0).unwrap(); + + use core::sync::atomic::AtomicBool; + use core::sync::atomic::Ordering::SeqCst; + use std::sync::Arc; + + let log_called = Arc::new(AtomicBool::new(false)); + let log_called2 = log_called.clone(); + liq.set_log_callback(move |_attr, _msg| { + log_called2.store(true, SeqCst); + }); + + let prog_called = Arc::new(AtomicBool::new(false)); + let prog_called2 = prog_called.clone(); + liq.set_progress_callback(move |_perc| { + prog_called2.store(true, SeqCst); + ControlFlow::Continue + }); + + // Describe the bitmap + let img = &mut liq.new_image(&fakebitmap[..], width, height, 0.0).unwrap(); + + // The magic happens in quantize() + let mut res = match liq.quantize(img) { + Ok(res) => res, + Err(err) => panic!("Quantization failed, because: {err:?}"), + }; + + // Enable dithering for subsequent remappings + res.set_dithering_level(1.0).unwrap(); + + // You can reuse the result to generate several images with the same palette + let (palette, pixels) = res.remapped(img).unwrap(); + + assert_eq!(width * height, pixels.len()); + assert_eq!(100, res.quantization_quality().unwrap()); + assert_eq!(RGBA { r: 255, g: 255, b: 255, a: 255 }, palette[0]); + assert_eq!(RGBA { r: 0x55, g: 0x66, b: 0x77, a: 255 }, palette[1]); + + assert!(log_called.load(SeqCst)); + assert!(prog_called.load(SeqCst)); +} + +#[test] +fn set_importance_map() { + let liq = new(); + let bitmap = &[RGBA::new(255, 0, 0, 255), RGBA::new(0u8, 0, 255, 255)]; + let mut img = liq.new_image(&bitmap[..], 2, 1, 0.).unwrap(); + let map = &[255, 0]; + img.set_importance_map(&map[..]).unwrap(); + let mut res = liq.quantize(&mut img).unwrap(); + let pal = res.palette(); + assert_eq!(1, pal.len(), "{pal:?}"); + assert_eq!(bitmap[0], pal[0]); +} + +#[test] +fn thread() { + let liq = Attributes::new(); + std::thread::spawn(move || { + let b = vec![RGBA::new(0, 0, 0, 0); 1]; + liq.new_image_borrowed(&b, 1, 1, 0.).unwrap(); + }).join().unwrap(); +} + +#[test] +fn r_callback_test() { + use core::mem::MaybeUninit; + use core::sync::atomic::AtomicU16; + use core::sync::atomic::Ordering::SeqCst; + use std::sync::Arc; + + let called = Arc::new(AtomicU16::new(0)); + let called2 = called.clone(); + let mut res = { + let a = new(); + let get_row = move |output_row: &mut [MaybeUninit], y: usize| { + assert!((0..5).contains(&y)); + assert_eq!(123, output_row.len()); + for (n, out) in output_row.iter_mut().enumerate() { + let n = n as u8; + out.write(RGBA::new(n, n, n, n)); + } + called2.fetch_add(1, SeqCst); + }; + let mut img = unsafe { + Image::new_fn(&a, get_row, 123, 5, 0.).unwrap() + }; + a.quantize(&mut img).unwrap() + }; + let called = called.load(SeqCst); + assert!(called > 5 && called < 50); + assert_eq!(123, res.palette().len()); +} + +#[test] +fn sizes() { + use core::mem::size_of; + use pal::{PalF, Palette}; + assert!(size_of::() < crate::pal::MAX_COLORS * (8 * 4) + 32, "{}", size_of::()); + assert!(size_of::() < size_of::() + size_of::() + 100, "{}", size_of::()); + assert!(size_of::() < 200); + assert!(size_of::() < 300); + assert!(size_of::() < 200); + assert!(size_of::() <= 32); +} + +#[doc(hidden)] +pub fn _unstable_internal_kmeans_bench() -> impl FnMut() { + use crate::pal::{PalF, PalPop}; + + let attr = new(); + let mut h = hist::Histogram::new(&attr); + + let e = (0..10000u32).map(|i| HistogramEntry { + count: i.wrapping_mul(17) % 12345, + color: RGBA::new(i as u8, (i.wrapping_mul(7) >> 2) as u8, (i.wrapping_mul(11) >> 11) as u8, 255), + }).collect::>(); + + h.add_colors(&e, 0.).unwrap(); + let mut hist = h.finalize_builder(0.45455).unwrap(); + + let lut = pal::gamma_lut(0.45455); + let mut p = PalF::new(); + for i in 0..=255 { + p.push(pal::f_pixel::from_rgba(&lut, RGBA::new(i | 7, i, i, 255)), PalPop::new(1.)); + } + + move || { + kmeans::Kmeans::iteration(&mut hist, &mut p, false).unwrap(); + } +} + +trait PushInCapacity { + fn push_in_cap(&mut self, val: T); +} + +impl PushInCapacity for Vec { + #[track_caller] + #[inline(always)] + fn push_in_cap(&mut self, val: T) { + debug_assert!(self.capacity() != self.len()); + if self.capacity() != self.len() { + self.push(val); + } + } +} + +/// Rust is too conservative about sorting floats. +/// This library uses only finite values, so they're sortable. +#[derive(Debug, PartialEq, PartialOrd, Copy, Clone)] +#[repr(transparent)] +struct OrdFloat(pub(crate) T); + +impl Eq for OrdFloat { +} + +impl Ord for OrdFloat { + #[inline] + fn cmp(&self, other: &Self) -> Ordering { + self.0.partial_cmp(&other.0).unwrap_or(Ordering::Equal) + } +} + +impl Eq for OrdFloat { +} + +impl Ord for OrdFloat { + #[inline] + fn cmp(&self, other: &Self) -> Ordering { + self.0.partial_cmp(&other.0).unwrap_or(Ordering::Equal) + } +} + +impl OrdFloat { + #[cfg_attr(debug_assertions, track_caller)] + pub fn new(v: f32) -> Self { + debug_assert!(v.is_finite()); + Self(v) + } +} + +impl OrdFloat { + #[cfg_attr(debug_assertions, track_caller)] + pub fn new64(v: f64) -> Self { + debug_assert!(v.is_finite()); + Self(v) + } +} + +#[test] +fn test_fixed_colors() { + let attr = Attributes::new(); + let mut h = Histogram::new(&attr); + let tmp = (0..128).map(|c| HistogramEntry { + color: RGBA::new(c,c,c,255), + count: 1, + }).collect::>(); + h.add_colors(&tmp, 0.).unwrap(); + for f in 200..255 { + h.add_fixed_color(RGBA::new(f, f, f, 255), 0.).unwrap(); + } + let mut r = h.quantize(&attr).unwrap(); + let pal = r.palette(); + + for (i, c) in (200..255).enumerate() { + assert_eq!(pal[i], RGBA::new(c, c, c, 255)); + } + + for c in 0..128 { + assert!(pal[55..].iter().any(|&p| p == RGBA::new(c, c, c, 255))); + } +} + +#[cfg(all(not(feature = "std"), feature = "no_std"))] +pub(crate) mod no_std_compat { + pub use std::boxed::Box; + pub use std::vec::Vec; + pub use std::format; + + extern "C" { + fn pow(_: f64, _: f64) -> f64; + fn powf(_: f32, _: f32) -> f32; + fn sqrt(_: f64) -> f64; + fn sqrtf(_: f32) -> f32; + } + + pub(crate) trait NoMath: Sized { + fn mul_add(self, mul: Self, add: Self) -> Self; + fn powi(self, n: u32) -> Self; + fn powf(self, e: Self) -> Self; + fn sqrt(self) -> Self; + } + + impl NoMath for f32 { + fn mul_add(self, mul: Self, add: Self) -> Self { + self * mul + add + } + fn powi(self, n: u32) -> Self { + assert_eq!(n, 2); + self * self + } + fn powf(self, e: Self) -> Self { + unsafe { powf(self, e) } + } + fn sqrt(self) -> Self { + unsafe { sqrtf(self) } + } + } + + impl NoMath for f64 { + fn mul_add(self, mul: Self, add: Self) -> Self { + self * mul + add + } + fn powi(self, n: u32) -> Self { + assert_eq!(n, 2); + self * self + } + fn powf(self, e: Self) -> Self { + unsafe { pow(self, e) } + } + fn sqrt(self) -> Self { + unsafe { sqrt(self) } + } + } +} diff --git a/src/mediancut.rs b/src/mediancut.rs new file mode 100644 index 0000000..508a2f8 --- /dev/null +++ b/src/mediancut.rs @@ -0,0 +1,335 @@ +use crate::hist::{HistItem, HistogramInternal}; +use crate::pal::{f_pixel, PalF, PalLen, PalPop, ARGBF}; +use crate::quant::quality_to_mse; +use crate::{Error, OrdFloat, PushInCapacity}; +use core::cmp::Reverse; +use rgb::prelude::*; + +#[cfg(all(not(feature = "std"), feature = "no_std"))] +use crate::no_std_compat::*; + +struct MedianCutter<'hist> { + boxes: Vec>, + hist_total_perceptual_weight: f64, + target_colors: PalLen, +} + +struct MBox<'hist> { + /// Histogram entries that fell into this bucket + pub colors: &'hist mut [HistItem], + /// Center color selected to represent the colors + pub avg_color: f_pixel, + /// Difference from the average color, per channel, weighed using `adjusted_weight` + pub variance: ARGBF, + pub adjusted_weight_sum: f64, + pub total_error: Option, + /// max color difference between avg_color and any histogram entry + pub max_error: f32, +} + +impl<'hist> MBox<'hist> { + pub fn new(hist: &'hist mut [HistItem]) -> Self { + let weight_sum = hist.iter().map(|item| { + debug_assert!(item.adjusted_weight.is_finite()); + debug_assert!(item.adjusted_weight > 0.); + f64::from(item.adjusted_weight) + }).sum(); + Self::new_inner(hist, weight_sum, weighed_average_color(hist)) + } + + fn from_split(hist: &'hist mut [HistItem], adjusted_weight_sum: f64) -> Self { + debug_assert!(!hist.is_empty()); + let avg_color = weighed_average_color(hist); + Self::new_inner(hist, adjusted_weight_sum, avg_color) + } + + fn new_inner(hist: &'hist mut [HistItem], adjusted_weight_sum: f64, avg_color: f_pixel) -> Self { + let (variance, max_error) = Self::box_stats(hist, avg_color); + debug_assert!(adjusted_weight_sum.is_finite()); + Self { + variance, + max_error, + avg_color, + colors: hist, + adjusted_weight_sum, + total_error: None, + } + } + + fn box_stats(hist: &[HistItem], avg_color: f_pixel) -> (ARGBF, f32) { + let mut variance = ARGBF::default(); + let mut max_error = 0.; + for item in hist { + variance += (avg_color.0 - item.color.0).map(|c| c * c) * item.adjusted_weight; + let diff = avg_color.diff(&item.color); + if diff > max_error { + max_error = diff; + } + } + (variance, max_error) + } + + pub fn compute_total_error(&mut self) -> f64 { + let avg = self.avg_color; + let e = self.colors.iter().map(move |a| f64::from(avg.diff(&a.color)) * f64::from(a.perceptual_weight)).sum::(); + self.total_error = Some(e); + e + } + + pub fn prepare_sort(&mut self) { + struct ChanVariance { + pub chan: usize, + pub variance: f32, + } + + // Sort dimensions by their variance, and then sort colors first by dimension with the highest variance + let vars: [f32; 4] = rgb::bytemuck::cast(self.variance); + let mut channels = [ + ChanVariance { chan: 0, variance: vars[0] }, + ChanVariance { chan: 1, variance: vars[1] }, + ChanVariance { chan: 2, variance: vars[2] }, + ChanVariance { chan: 3, variance: vars[3] }, + ]; + channels.sort_unstable_by_key(|ch| Reverse(OrdFloat::new(ch.variance))); + + for item in self.colors.iter_mut() { + let chans: [f32; 4] = rgb::bytemuck::cast(item.color.0); + // Only the first channel really matters. But other channels are included, because when trying median cut + // many times with different histogram weights, I don't want sort randomness to influence the outcome. + item.sort_val_pal_index_union = (u32::from((chans[channels[0].chan] * 65535.) as u16) << 16) + | u32::from(((chans[channels[2].chan] + chans[channels[1].chan] / 2. + chans[channels[3].chan] / 4.) * 65535.) as u16); // box will be split to make color_weight of each side even + } + } + + fn median_color(&mut self) -> f_pixel { + let len = self.colors.len(); + let (_, mid_item, _) = self.colors.select_nth_unstable_by_key(len / 2, |a| a.mc_sort_value()); + mid_item.color + } + + pub fn prepare_color_weight_total(&mut self) -> f64 { + let median = self.median_color(); + self.colors.iter_mut().map(move |a| { + let w = (median.diff(&a.color).sqrt() * (2. + a.adjusted_weight)).sqrt(); + debug_assert!(w.is_finite()); + a.mc_color_weight = w; + f64::from(w) + }) + .sum() + } + + #[inline] + pub fn split(mut self) -> [Self; 2] { + self.prepare_sort(); + let half_weight = self.prepare_color_weight_total() / 2.; + // yeah, there's some off-by-one error in there + let break_at = hist_item_sort_half(self.colors, half_weight).max(1); + + let (left, right) = self.colors.split_at_mut(break_at); + let left_sum = left.iter().map(|a| f64::from(a.adjusted_weight)).sum(); + let right_sum = self.adjusted_weight_sum - left_sum; + + [MBox::from_split(left, left_sum), + MBox::from_split(right, right_sum)] + } +} + + +/// LLVM 21 can't reliably optimize out bounds checks - it keeps forgetting the known range +/// of values at any non-trivial if/else. +/// When LLVM fails to optimize out checks, it's way better to have a fallback value than a panic. +/// The fallback doesn't have side effects and allows code reordering. +#[inline(always)] +fn mc_sort_value(base: &[HistItem], idx: usize) -> Option { + debug_assert!(base.get(idx).is_some()); + base.get(idx).map(|item| item.mc_sort_value()) +} + +#[inline] +fn qsort_pivot(base: &[HistItem]) -> Option { + let len = base.len(); + if len < 32 { + return Some(len / 2); + } + let mut pivots = [8, len / 2, len - 1]; + pivots.sort_unstable_by_key(move |&idx| mc_sort_value(base, idx).unwrap_or_default()); + // this is redundant, but tracking `!base.is_empty()` through `qsort_pivot()` is too much for LLVM + debug_assert!(pivots[1] < base.len()); + (pivots[1] < base.len()).then_some(pivots[1]) +} + +fn qsort_partition(base: &mut [HistItem]) -> Option { + let mut r = base.len(); + base.swap(qsort_pivot(base)?, 0); + let pivot_value = mc_sort_value(base, 0)?; + let mut l = 1; + while l < r { + if mc_sort_value(base, l)? >= pivot_value { + l += 1; + } else { + r -= 1; + while l < r && mc_sort_value(base, r)? <= pivot_value { + r -= 1; + } + debug_assert!(l < base.len() && r < base.len()); + if r >= base.len() { return None; } // always false, but LLVM needs this ;( + base.swap(l, r); + } + } + l -= 1; + if l >= base.len() { return None; } // always false, but LLVM needs this ;( + base.swap(l, 0); + Some(l) +} + +/// sorts the slice to make the sum of weights lower than `weight_half_sum` one side, +/// returns index of the edge between halfvar parts of the set +#[inline(never)] +fn hist_item_sort_half(mut base: &mut [HistItem], mut weight_half_sum: f64) -> usize { + let mut base_index = 0; + if base.is_empty() { + return 0; + } + loop { + let Some(partition) = qsort_partition(base) else { return base_index; }; + let Some((left, right)) = base.split_at_mut_checked(partition + 1) else { return base_index; }; // +1, because pivot stays on the left side + let left_sum = left.iter().map(|c| f64::from(c.mc_color_weight)).sum::(); + if left_sum >= weight_half_sum { + match left.get_mut(..partition) { // trim pivot point, avoid panic branch in [..] + Some(left) if !left.is_empty() => { base = left; continue; }, + _ => return base_index, + } + } + weight_half_sum -= left_sum; + base_index += left.len(); + if !right.is_empty() { + base = right; + } else { + return base_index; + } + } +} + +impl<'hist> MedianCutter<'hist> { + fn total_box_error_below_target(&mut self, mut target_mse: f64) -> bool { + target_mse *= self.hist_total_perceptual_weight; + let mut total_error = self.boxes.iter().filter_map(|mb| mb.total_error).sum::(); + if total_error > target_mse { + return false; + } + for mb in self.boxes.iter_mut().filter(|mb| mb.total_error.is_none()) { + total_error += mb.compute_total_error(); + if total_error > target_mse { + return false; + } + } + true + } + + pub fn new(hist: &'hist mut HistogramInternal, target_colors: PalLen) -> Result { + let hist_total_perceptual_weight = hist.total_perceptual_weight; + + debug_assert!(hist.clusters[0].begin == 0); + debug_assert!(hist.clusters.last().unwrap().end as usize == hist.items.len()); + + let mut hist_items = &mut hist.items[..]; + let mut boxes = Vec::new(); + boxes.try_reserve(target_colors as usize)?; + + let used_boxes = hist.clusters.iter().filter(|b| b.begin != b.end).count(); + if used_boxes <= target_colors as usize / 3 { + // boxes are guaranteed to be sorted + let mut prev_end = 0; + for b in hist.clusters.iter().filter(|b| b.begin != b.end) { + let begin = b.begin as usize; + debug_assert_eq!(begin, prev_end); + let end = b.end as usize; + prev_end = end; + let (this_box, rest) = hist_items.split_at_mut(end - begin); + hist_items = rest; + boxes.push_in_cap(MBox::new(this_box)); + } + } else { + boxes.push_in_cap(MBox::new(hist_items)); + } + + Ok(Self { + boxes, + hist_total_perceptual_weight, + target_colors, + }) + } + + fn into_palette(mut self) -> PalF { + let mut palette = PalF::new(); + + for (mbox, pal_index) in self.boxes.iter_mut().zip(0..) { + mbox.colors.iter_mut().for_each(move |a| a.set_likely_palette_index(pal_index)); + + // store total color popularity (perceptual_weight is approximation of it) + let pop = mbox.colors.iter().map(|a| f64::from(a.perceptual_weight)).sum::(); + let mut representative_color = mbox.avg_color; + if mbox.colors.len() > 2 { + representative_color = mbox.colors.iter().min_by_key(|a| OrdFloat::new(representative_color.diff(&a.color))).map(|a| a.color).unwrap_or_default(); + } + palette.push(representative_color, PalPop::new(pop as f32)); + } + palette + } + + fn cut(mut self, target_mse: f64, max_mse: f64) -> PalF { + let max_mse = max_mse.max(quality_to_mse(20)); + + while self.boxes.len() < self.target_colors as usize { + // first splits boxes that exceed quality limit (to have colors for things like odd green pixel), + // later raises the limit to allow large smooth areas/gradients get colors. + let fraction_done = self.boxes.len() as f64 / f64::from(self.target_colors); + let current_max_mse = (fraction_done * 16.).mul_add(max_mse, max_mse); + let Some(bi) = self.take_best_splittable_box(current_max_mse) else { + break + }; + + self.boxes.extend(bi.split()); + + if self.total_box_error_below_target(target_mse) { + break; + } + } + + self.into_palette() + } + + fn take_best_splittable_box(&mut self, max_mse: f64) -> Option> { + self.boxes.iter().enumerate() + .filter(|(_, mbox)| mbox.colors.len() > 1) + .map(move |(i, mbox)| { + let mut thissum = mbox.adjusted_weight_sum * mbox.variance.iter().map(|f| f as f64).sum::(); + if f64::from(mbox.max_error) > max_mse { + thissum = thissum * f64::from(mbox.max_error) / max_mse; + } + (i, thissum) + }) + .max_by_key(|&(_, thissum)| OrdFloat::new64(thissum)) + .map(|(i, _)| self.boxes.swap_remove(i)) + } +} + +#[inline(never)] +pub(crate) fn mediancut(hist: &mut HistogramInternal, target_colors: PalLen, target_mse: f64, max_mse_per_color: f64) -> Result { + Ok(MedianCutter::new(hist, target_colors)?.cut(target_mse, max_mse_per_color)) +} + +fn weighed_average_color(hist: &[HistItem]) -> f_pixel { + debug_assert!(!hist.is_empty()); + let mut t = f_pixel::default(); + let mut sum = 0.; + for c in hist { + sum += c.adjusted_weight; + t.0 += c.color.0 * c.adjusted_weight; + } + debug_assert!(sum.is_finite()); + if sum != 0. { + t.0 /= sum; + } + t +} diff --git a/src/nearest.rs b/src/nearest.rs new file mode 100644 index 0000000..68ce96c --- /dev/null +++ b/src/nearest.rs @@ -0,0 +1,221 @@ +use crate::pal::{f_pixel, PalF, PalIndex, MAX_COLORS}; +use crate::{Error, OrdFloat}; + +#[cfg(all(not(feature = "std"), feature = "no_std"))] +use crate::no_std_compat::*; + +impl<'pal> Nearest<'pal> { + #[inline(never)] + pub fn new(palette: &'pal PalF) -> Result { + if palette.len() > PalIndex::MAX as usize + 1 { + return Err(Error::Unsupported); + } + let mut indexes: Vec<_> = (0..palette.len()) + .map(|idx| MapIndex { idx: idx as _ }) + .collect(); + if indexes.is_empty() { + return Err(Error::Unsupported); + } + let mut handle = Nearest { + root: vp_create_node(&mut indexes, palette), + palette, + nearest_other_color_dist: [0.; MAX_COLORS], + }; + for (i, color) in palette.as_slice().iter().enumerate() { + let mut best = Visitor { + idx: 0, + distance: f32::MAX, + distance_squared: f32::MAX, + exclude: Some(i as PalIndex), + }; + vp_search_node(&handle.root, color, &mut best); + handle.nearest_other_color_dist[i] = best.distance_squared / 4.; + } + Ok(handle) + } +} + +impl Nearest<'_> { + #[inline] + pub fn search(&self, px: &f_pixel, likely_colormap_index: PalIndex) -> (PalIndex, f32) { + // The index may be invalid, so it needs to be checked + let mut best_candidate = if let Some(pal_px) = self.palette.as_slice().get(likely_colormap_index as usize) { + let guess_diff = px.diff(pal_px); + if guess_diff < self.nearest_other_color_dist[likely_colormap_index as usize] { + return (likely_colormap_index, guess_diff); + } + Visitor { + distance: guess_diff.sqrt(), + distance_squared: guess_diff, + idx: likely_colormap_index, + exclude: None, + } + } else { + Visitor { + distance: f32::INFINITY, + distance_squared: f32::INFINITY, + idx: 0, + exclude: None, + } + }; + + vp_search_node(&self.root, px, &mut best_candidate); + (best_candidate.idx, best_candidate.distance_squared) + } +} + +pub(crate) struct Nearest<'pal> { + root: Node, + palette: &'pal PalF, + nearest_other_color_dist: [f32; MAX_COLORS], +} + +pub struct MapIndex { + pub idx: PalIndex, +} + +pub struct Visitor { + pub distance: f32, + pub distance_squared: f32, + pub idx: PalIndex, + pub exclude: Option, +} + +impl Visitor { + #[inline] + fn visit(&mut self, distance: f32, distance_squared: f32, idx: PalIndex) { + if distance_squared < self.distance_squared && self.exclude != Some(idx) { + self.distance = distance; + self.distance_squared = distance_squared; + self.idx = idx; + } + } +} + +pub(crate) struct Node { + vantage_point: f_pixel, + inner: NodeInner, + idx: PalIndex, +} + +const LEAF_MAX_SIZE: usize = 6; + +enum NodeInner { + Nodes { + radius: f32, + radius_squared: f32, + near: Box, + far: Box, + }, + Leaf { + len: u8, + idxs: [PalIndex; LEAF_MAX_SIZE], + colors: Box<[f_pixel; LEAF_MAX_SIZE]>, + }, +} + +#[inline(never)] +fn vp_create_node(indexes: &mut [MapIndex], items: &PalF) -> Node { + debug_assert!(!indexes.is_empty()); + let palette = items.as_slice(); + + if indexes.len() <= 1 { + let idx = indexes.first().map(|i| i.idx).unwrap_or_default(); + return Node { + vantage_point: palette.get(usize::from(idx)).copied().unwrap_or_default(), + idx, + inner: NodeInner::Leaf { len: 0, idxs: [0; LEAF_MAX_SIZE], colors: Box::new([f_pixel::default(); LEAF_MAX_SIZE]) }, + }; + } + + let most_popular_item = indexes.iter().enumerate().max_by_key(move |(_, idx)| { + OrdFloat::new(items.pop_as_slice().get(usize::from(idx.idx)) + .map(|p| p.popularity()).unwrap_or_default()) + }).map(|(n, _)| n).unwrap_or_default(); + indexes.swap(most_popular_item, 0); + let (ref_, indexes) = indexes.split_first_mut().unwrap(); + + let vantage_point = palette.get(usize::from(ref_.idx)).copied().unwrap_or_default(); + indexes.sort_by_cached_key(move |i| { + OrdFloat::new(palette.get(usize::from(i.idx)) + .map(|px| vantage_point.diff(px)).unwrap_or_default()) + }); + + let num_indexes = indexes.len(); + + let inner = if num_indexes <= LEAF_MAX_SIZE { + let mut colors = [f_pixel::default(); LEAF_MAX_SIZE]; + let mut idxs = [Default::default(); LEAF_MAX_SIZE]; + + indexes.iter().zip(colors.iter_mut().zip(idxs.iter_mut())).for_each(|(i, (color, idx))| { + if let Some(c) = palette.get(usize::from(i.idx)) { + *idx = i.idx; + *color = *c; + } + }); + NodeInner::Leaf { + len: num_indexes as _, + idxs, + colors: Box::new(colors), + } + } else { + let half_index = num_indexes / 2; + let (near, far) = indexes.split_at_mut(half_index); + debug_assert!(!near.is_empty()); + debug_assert!(!far.is_empty()); + let radius_squared = palette.get(usize::from(far[0].idx)) + .map(|px| vantage_point.diff(px)).unwrap_or_default(); + let radius = radius_squared.sqrt(); + NodeInner::Nodes { + radius, radius_squared, + near: Box::new(vp_create_node(near, items)), + far: Box::new(vp_create_node(far, items)), + } + }; + + Node { + inner, + vantage_point, + idx: ref_.idx, + } +} + +#[inline(never)] +fn vp_search_node(mut node: &Node, needle: &f_pixel, best_candidate: &mut Visitor) { + loop { + let distance_squared = node.vantage_point.diff(needle); + let distance = distance_squared.sqrt(); + + best_candidate.visit(distance, distance_squared, node.idx); + + match node.inner { + NodeInner::Nodes { radius, radius_squared, ref near, ref far } => { + // Recurse towards most likely candidate first to narrow best candidate's distance as soon as possible + if distance_squared < radius_squared { + vp_search_node(near, needle, best_candidate); + // The best node (final answer) may be just ouside the radius, but not farther than + // the best distance we know so far. The vp_search_node above should have narrowed + // best_candidate->distance, so this path is rarely taken. + if distance >= radius - best_candidate.distance { + node = far; + continue; + } + } else { + vp_search_node(far, needle, best_candidate); + if distance <= radius + best_candidate.distance { + node = near; + continue; + } + } + break; + }, + NodeInner::Leaf { len: num, ref idxs, ref colors } => { + colors.iter().zip(idxs.iter().copied()).take(num as usize).for_each(|(color, idx)| { + let distance_squared = color.diff(needle); + best_candidate.visit(distance_squared.sqrt(), distance_squared, idx); + }); + break; + }, + } + } +} diff --git a/src/pal.rs b/src/pal.rs new file mode 100644 index 0000000..903131a --- /dev/null +++ b/src/pal.rs @@ -0,0 +1,521 @@ +use crate::OrdFloat; +use arrayvec::ArrayVec; +use core::iter; +use core::ops::{Deref, DerefMut}; +use rgb::prelude::*; + +#[cfg(all(not(feature = "std"), feature = "no_std"))] +use crate::no_std_compat::*; + +/// 8-bit RGBA in sRGB. This is the only color format *publicly* used by the library. +pub type RGBA = rgb::Rgba; + +#[allow(clippy::upper_case_acronyms)] +pub type ARGBF = rgb::Argb; + +const INTERNAL_GAMMA: f64 = 0.57; +const LIQ_WEIGHT_A: f32 = 0.625; +const LIQ_WEIGHT_R: f32 = 0.5; +const LIQ_WEIGHT_G: f32 = 1.; +const LIQ_WEIGHT_B: f32 = 0.45; + +/// This is a fudge factor - reminder that colors are not in 0..1 range any more +const LIQ_WEIGHT_MSE: f64 = 0.45; + +/// 4xf32 color using internal gamma. +/// +/// ARGB layout is important for x86 SIMD. +/// I've created the newtype wrapper to try a 16-byte alignment, but it didn't improve perf :( +#[cfg_attr( + any(target_arch = "x86_64", all(target_feature = "neon", target_arch = "aarch64"), all(target_arch = "wasm32", target_feature = "simd128")), + repr(C, align(16)) +)] +#[derive(Debug, Copy, Clone, Default, PartialEq)] +#[allow(non_camel_case_types)] +pub struct f_pixel(pub ARGBF); + +impl f_pixel { + #[cfg(not(any(target_arch = "x86_64", all(target_feature = "neon", target_arch = "aarch64"), all(target_arch = "wasm32", target_feature = "simd128"))))] + #[inline(always)] + pub fn diff(&self, other: &f_pixel) -> f32 { + let alphas = other.0.a - self.0.a; + let black = self.0 - other.0; + let white = ARGBF { + a: 0., + r: black.r + alphas, + g: black.g + alphas, + b: black.b + alphas, + }; + (black.r * black.r).max(white.r * white.r) + + (black.g * black.g).max(white.g * white.g) + + (black.b * black.b).max(white.b * white.b) + } + + #[cfg(all(target_arch = "wasm32", target_feature = "simd128"))] + #[inline(always)] + pub fn diff(&self, other: &f_pixel) -> f32 { + use core::arch::wasm32::*; + + let px = f32x4(self.0.a, self.0.r, self.0.g, self.0.b); + let py = f32x4(other.0.a, other.0.r, other.0.g, other.0.b); + + // y.a - x.a, then broadcast lane 0 to all four + let alpha_diff = f32x4_sub(py, px); + let alphas = f32x4_splat(f32x4_extract_lane::<0>(alpha_diff)); + + let mut onblack = f32x4_sub(px, py); // x - y + let mut onwhite = f32x4_add(onblack, alphas); // x - y + (y.a - x.a) + + onblack = f32x4_mul(onblack, onblack); + onwhite = f32x4_mul(onwhite, onwhite); + let max = f32x4_max(onwhite, onblack); + + // add rgb (lanes 1,2,3), not a (lane 0) + f32x4_extract_lane::<1>(max) + + f32x4_extract_lane::<2>(max) + + f32x4_extract_lane::<3>(max) + } + + #[cfg(all(target_feature = "neon", target_arch = "aarch64"))] + #[inline(always)] + pub fn diff(&self, other: &Self) -> f32 { + unsafe { + use core::arch::aarch64::*; + + let px = vld1q_f32((self as *const Self).cast::()); + let py = vld1q_f32((other as *const Self).cast::()); + + // y.a - x.a + let mut alphas = vsubq_f32(py, px); + alphas = vdupq_laneq_f32(alphas, 0); // copy first to all four + + let mut onblack = vsubq_f32(px, py); // x - y + let mut onwhite = vaddq_f32(onblack, alphas); // x - y + (y.a - x.a) + + onblack = vmulq_f32(onblack, onblack); + onwhite = vmulq_f32(onwhite, onwhite); + + let max = vmaxq_f32(onwhite, onblack); + + let mut max_r = [0.; 4]; + vst1q_f32(max_r.as_mut_ptr(), max); + + let mut max_gb = [0.; 4]; + vst1q_f32(max_gb.as_mut_ptr(), vpaddq_f32(max, max)); + + // add rgb, not a + + max_r[1] + max_gb[1] + } + } + + #[cfg(target_arch = "x86_64")] + #[inline(always)] + pub fn diff(&self, other: &f_pixel) -> f32 { + unsafe { + use core::arch::x86_64::*; + + let px = _mm_loadu_ps(self as *const f_pixel as *const f32); + let py = _mm_loadu_ps(other as *const f_pixel as *const f32); + + // y.a - x.a + let mut alphas = _mm_sub_ss(py, px); + alphas = _mm_shuffle_ps(alphas, alphas, 0); // copy first to all four + + let mut onblack = _mm_sub_ps(px, py); // x - y + let mut onwhite = _mm_add_ps(onblack, alphas); // x - y + (y.a - x.a) + + onblack = _mm_mul_ps(onblack, onblack); + onwhite = _mm_mul_ps(onwhite, onwhite); + let max = _mm_max_ps(onwhite, onblack); + + // the compiler is better at horizontal add than I am + let mut tmp = [0.; 4]; + _mm_storeu_ps(tmp.as_mut_ptr(), max); + + // add rgb, not a + let res = tmp[1] + tmp[2] + tmp[3]; + res + } + } + + #[inline] + pub(crate) fn to_rgb(self, gamma: f64) -> RGBA { + if self.is_fully_transparent() { + return RGBA::new(0, 0, 0, 0); + } + + let r = (f64::from(LIQ_WEIGHT_A) / f64::from(LIQ_WEIGHT_R)) as f32 * self.r / self.a; + let g = (f64::from(LIQ_WEIGHT_A) / f64::from(LIQ_WEIGHT_G)) as f32 * self.g / self.a; + let b = (f64::from(LIQ_WEIGHT_A) / f64::from(LIQ_WEIGHT_B)) as f32 * self.b / self.a; + + let gamma = (gamma / INTERNAL_GAMMA) as f32; + debug_assert!(gamma.is_finite()); + + // 256, because numbers are in range 1..255.9999… rounded down + RGBA { + r: (r.max(0.).powf(gamma) * 256.) as u8, + g: (g.max(0.).powf(gamma) * 256.) as u8, + b: (b.max(0.).powf(gamma) * 256.) as u8, + a: (self.a * (256. / f64::from(LIQ_WEIGHT_A)) as f32) as u8, + } + } + + pub fn from_rgba(gamma_lut: &[f32; 256], px: RGBA) -> Self { + let a = f32::from(px.a) / 255.; + Self(ARGBF { + a: a * LIQ_WEIGHT_A, + r: gamma_lut[px.r as usize] * LIQ_WEIGHT_R * a, + g: gamma_lut[px.g as usize] * LIQ_WEIGHT_G * a, + b: gamma_lut[px.b as usize] * LIQ_WEIGHT_B * a, + }) + } + + #[inline] + pub(crate) fn is_fully_transparent(self) -> bool { + self.a < (1. / 255. * f64::from(LIQ_WEIGHT_A)) as f32 + } + + #[inline] + pub(crate) fn is_fully_opaque(self) -> bool { + self.a >= (255. / 256. * f64::from(LIQ_WEIGHT_A)) as f32 + } +} + +impl Deref for f_pixel { + type Target = ARGBF; + + #[inline(always)] + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for f_pixel { + #[inline(always)] + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl From for f_pixel { + #[inline(always)] + fn from(x: ARGBF) -> Self { + Self(x) + } +} + +/// To keep the data dense, `is_fixed` is stuffed into the sign bit +#[derive(Copy, Clone, Debug)] +pub(crate) struct PalPop(f32); + +impl PalPop { + #[inline(always)] + pub fn is_fixed(self) -> bool { + self.0 < 0. + } + + #[must_use] + pub fn to_fixed(self) -> Self { + if self.0 < 0. { + return self; + } + Self(if self.0 > 0. { -self.0 } else { -1. }) + } + + #[inline] + #[cfg_attr(debug_assertions, track_caller)] + pub fn new(popularity: f32) -> Self { + debug_assert!(popularity >= 0.); + Self(popularity) + } + + #[inline(always)] + #[must_use] + pub fn popularity(self) -> f32 { + self.0.abs() + } +} + +#[cfg(feature = "large_palettes")] +pub type PalIndex = u16; + +#[cfg(not(feature = "large_palettes"))] +pub type PalIndex = u8; + +/// This could be increased to support > 256 colors in remapping too +pub type PalIndexRemap = u8; +pub type PalLen = u16; + +/// Palettes are stored on the stack, and really large ones will cause stack overflows +pub(crate) const MAX_COLORS: usize = if PalIndex::MAX == 255 { 256 } else { 2048 }; + +/// A palette of premultiplied ARGB 4xf32 colors in internal gamma +#[derive(Clone)] +pub(crate) struct PalF { + colors: ArrayVec, + pops: ArrayVec, +} + +impl PalF { + #[inline] + pub fn new() -> Self { + debug_assert!(PalIndex::MAX as usize + 1 >= MAX_COLORS); + debug_assert!(PalLen::MAX as usize >= MAX_COLORS); + Self { + colors: ArrayVec::default(), + pops: ArrayVec::default(), + } + } + + #[inline(always)] + pub fn push(&mut self, color: f_pixel, popularity: PalPop) { + self.pops.push(popularity); + self.colors.push(color); + } + + pub fn set(&mut self, idx: usize, color: f_pixel, popularity: PalPop) { + debug_assert!(idx < self.colors.len() && idx < self.pops.len()); + + if let Some(pops_idx) = self.pops.get_mut(idx) { + *pops_idx = popularity; + } + if let Some(colors_idx) = self.colors.get_mut(idx) { + *colors_idx = color; + } + } + + #[inline(always)] + pub fn as_slice(&self) -> &[f_pixel] { + &self.colors + } + + #[inline(always)] + pub fn pop_as_slice(&self) -> &[PalPop] { + &self.pops + } + + // this is max colors allowed by the user, not just max in the current (candidate/low-quality) palette + pub(crate) fn with_fixed_colors(mut self, max_colors: PalLen, fixed_colors: &[f_pixel]) -> Self { + if fixed_colors.is_empty() { + return self; + } + + // if using low quality, there's a chance mediancut won't create enough colors in the palette + let max_fixed_colors = fixed_colors.len().min(max_colors as usize); + if self.len() < max_fixed_colors { + let needs_extra = max_fixed_colors - self.len(); + self.colors.extend(fixed_colors.iter().copied().take(needs_extra)); + self.pops.extend(iter::repeat(PalPop::new(0.)).take(needs_extra)); + debug_assert_eq!(self.len(), max_fixed_colors); + } + + // since the fixed colors were in the histogram, expect them to be in the palette, + // and change closest existing one to be exact fixed + for (i, fixed_color) in fixed_colors.iter().enumerate().take(self.len()) { + let (best_idx, _) = self.colors.iter().enumerate().skip(i).min_by_key(|(_, pal_color)| { + // not using Nearest, because creation of the index may take longer than naive search once + OrdFloat::new(pal_color.diff(fixed_color)) + }).expect("logic bug in fixed colors, please report a bug"); + debug_assert!(best_idx >= i); + self.swap(i, best_idx); + self.set(i, *fixed_color, self.pops[i].to_fixed()); + } + + debug_assert!(self.colors.iter().zip(fixed_colors).all(|(p, f)| p == f)); + debug_assert!(self.pops.iter().take(fixed_colors.len()).all(|pop| pop.is_fixed())); + self + } + + #[inline(always)] + pub(crate) fn len(&self) -> usize { + debug_assert_eq!(self.colors.len(), self.pops.len()); + self.colors.len() + } + + #[inline(always)] + pub fn iter_mut(&mut self) -> impl Iterator { + let c = &mut self.colors[..]; + let pop = &mut self.pops[..c.len()]; + c.iter_mut().zip(pop) + } + + #[cfg_attr(debug_assertions, track_caller)] + pub(crate) fn swap(&mut self, a: usize, b: usize) { + self.colors.swap(a, b); + self.pops.swap(a, b); + } + + /// Also rounds the input pal + pub(crate) fn init_int_palette(&mut self, int_palette: &mut Palette, gamma: f64, posterize: u8) { + let lut = gamma_lut(gamma); + for ((f_color, f_pop), int_pal) in self.iter_mut().zip(&mut int_palette.entries) { + let mut px = f_color.to_rgb(gamma) + .map(move |c| posterize_channel(c, posterize)); + *f_color = f_pixel::from_rgba(&lut, px); + if px.a == 0 && !f_pop.is_fixed() { + px.r = 71u8; + px.g = 112u8; + px.b = 76u8; + } + *int_pal = px; + } + int_palette.count = self.len() as _; + } +} + +#[inline] +const fn posterize_channel(color: u8, bits: u8) -> u8 { + if bits == 0 { + color + } else { + (color & !((1 << bits) - 1)) | (color >> (8 - bits)) + } +} + +#[inline(always)] +pub fn gamma_lut(gamma: f64) -> [f32; 256] { + debug_assert!(gamma > 0.); + let mut tmp = [0.; 256]; + for (i, t) in tmp.iter_mut().enumerate() { + *t = ((i as f32) / 255.).powf((INTERNAL_GAMMA / gamma) as f32); + } + tmp +} + +/// MSE that assumes 0..1 channels scaled to MSE that we have in practice +#[inline] +pub(crate) fn unit_mse_to_internal_mse(internal_mse: f64) -> f64 { + LIQ_WEIGHT_MSE * internal_mse +} + +/// Internal MSE scaled to equivalent in 0..255 pixels +pub(crate) fn internal_mse_to_standard_mse(mse: f64) -> f64 { + (mse * 65536. / 6.) / LIQ_WEIGHT_MSE +} + +/// Not used in the Rust API. +/// RGBA colors obtained from [`QuantizationResult`](crate::QuantizationResult) +#[repr(C)] +#[derive(Clone)] +pub struct Palette { + /// Number of used colors in the `entries` + pub count: core::ffi::c_uint, + /// The colors, up to `count` + pub entries: [RGBA; MAX_COLORS], +} + +impl Deref for Palette { + type Target = [RGBA]; + + #[inline(always)] + fn deref(&self) -> &Self::Target { + self.as_slice() + } +} + +impl DerefMut for Palette { + #[inline(always)] + fn deref_mut(&mut self) -> &mut Self::Target { + self.as_mut_slice() + } +} + +impl Palette { + /// Palette colors + #[inline(always)] + #[must_use] + pub fn as_slice(&self) -> &[RGBA] { + &self.entries[..self.count as usize] + } + + #[inline(always)] + pub(crate) fn as_mut_slice(&mut self) -> &mut [RGBA] { + &mut self.entries[..self.count as usize] + } +} + +#[test] +fn diff_test() { + let a = f_pixel(ARGBF {a: 1., r: 0.2, g: 0.3, b: 0.5}); + let b = f_pixel(ARGBF {a: 1., r: 0.3, g: 0.3, b: 0.5}); + let c = f_pixel(ARGBF {a: 1., r: 1., g: 0.3, b: 0.5}); + let d = f_pixel(ARGBF {a: 0., r: 1., g: 0.3, b: 0.5}); + assert!(a.diff(&b) < b.diff(&c)); + assert!(c.diff(&b) < c.diff(&d)); + + let a = f_pixel(ARGBF {a: 1., b: 0.2, r: 0.3, g: 0.5}); + let b = f_pixel(ARGBF {a: 1., b: 0.3, r: 0.3, g: 0.5}); + let c = f_pixel(ARGBF {a: 1., b: 1., r: 0.3, g: 0.5}); + let d = f_pixel(ARGBF {a: 0., b: 1., r: 0.3, g: 0.5}); + assert!(a.diff(&b) < b.diff(&c)); + assert!(c.diff(&b) < c.diff(&d)); + + let a = f_pixel(ARGBF {a: 1., g: 0.2, b: 0.3, r: 0.5}); + let b = f_pixel(ARGBF {a: 1., g: 0.3, b: 0.3, r: 0.5}); + let c = f_pixel(ARGBF {a: 1., g: 1., b: 0.3, r: 0.5}); + let d = f_pixel(ARGBF {a: 0., g: 1., b: 0.3, r: 0.5}); + assert!(a.diff(&b) < b.diff(&c)); + assert!(c.diff(&b) < c.diff(&d)); +} + +#[test] +fn alpha_test() { + let gamma = gamma_lut(0.45455); + for (start, end) in [ + (RGBA::new(0,0,0,0), RGBA::new(0,0,0,2)), + (RGBA::new(0,0,0,253), RGBA::new(0,0,0,255)) + ] { + let start = f_pixel::from_rgba(&gamma, start).a as f64; + let end = f_pixel::from_rgba(&gamma, end).a as f64; + let range = end - start; + for i in 0..1000 { + let a = (start + ((i as f64) / 1000. * range)) as f32; + for a in [a, a.next_up(), a.next_down(), a+1e-6, a-1e-6] { + let px = f_pixel(ARGBF {a, g: 0., b: 0., r: 0.}); + let rgb = px.to_rgb(0.45455); + assert_eq!(rgb.a == 0, px.is_fully_transparent(), "not trns!? {px:?}, {rgb:?} {} {}", a / LIQ_WEIGHT_A, a / LIQ_WEIGHT_A * 255.); + assert_eq!(rgb.a == 255, px.is_fully_opaque(), "not opaque?! {px:?}, {rgb:?} {} {} {}", a / LIQ_WEIGHT_A, a / LIQ_WEIGHT_A * 255., a / LIQ_WEIGHT_A * 256.); + } + } + } +} + +#[test] +fn pal_test() { + let mut p = PalF::new(); + let gamma = gamma_lut(0.45455); + for i in 0..=255u8 { + let rgba = RGBA::new(i, i, i, 100 + i / 2); + p.push(f_pixel::from_rgba(&gamma, rgba), PalPop::new(1.)); + assert_eq!(i as usize + 1, p.len()); + assert_eq!(i as usize + 1, p.pop_as_slice().len()); + assert_eq!(i as usize + 1, p.as_slice().len()); + assert_eq!(i as usize + 1, p.colors.len()); + assert_eq!(i as usize + 1, p.pops.len()); + assert_eq!(i as usize + 1, p.iter_mut().count()); + } + + let mut int_pal = Palette { + count: 0, + entries: [RGBA::default(); MAX_COLORS], + }; + p.init_int_palette(&mut int_pal, 0.45455, 0); + + for i in 0..=255u8 { + let rgba = p.as_slice()[i as usize].to_rgb(0.45455); + assert_eq!(rgba, RGBA::new(i, i, i, 100 + i / 2)); + assert_eq!(int_pal[i as usize], RGBA::new(i, i, i, 100 + i / 2)); + } +} + +#[test] +#[cfg(feature = "large_palettes")] +fn largepal() { + let gamma = gamma_lut(0.5); + let mut p = PalF::new(); + for i in 0..1000 { + let rgba = RGBA::new(i as u8, (i/2) as u8, (i/4) as u8, 255); + p.push(f_pixel::from_rgba(&gamma, rgba), PalPop::new(1.)); + } +} diff --git a/src/quant.rs b/src/quant.rs new file mode 100644 index 0000000..54f2ad9 --- /dev/null +++ b/src/quant.rs @@ -0,0 +1,484 @@ +use crate::attr::{Attributes, ControlFlow}; +use crate::error::*; +use crate::hist::{Histogram, HistogramInternal}; +use crate::image::Image; +use crate::kmeans::Kmeans; +use crate::mediancut::mediancut; +use crate::pal::{PalF, PalIndexRemap, PalLen, PalPop, Palette, MAX_COLORS, RGBA}; +use crate::pal::{internal_mse_to_standard_mse, unit_mse_to_internal_mse}; +use crate::remap::{remap_to_palette, remap_to_palette_floyd, DitherMapMode, Remapped}; +use crate::seacow::RowBitmapMut; +use crate::OrdFloat; +use arrayvec::ArrayVec; +use core::cmp::Reverse; +use core::fmt; +use core::mem::MaybeUninit; + +#[cfg(all(not(feature = "std"), feature = "no_std"))] +use crate::no_std_compat::*; + +/// Remapping step, computed from [`Attributes::quantize()`] +pub struct QuantizationResult { + remapped: Option>, + pub(crate) palette: PalF, + progress_callback: Option ControlFlow + Send + Sync>>, + pub(crate) int_palette: Palette, + pub(crate) dither_level: f32, + pub(crate) gamma: f64, + pub(crate) palette_error: Option, + pub(crate) min_posterization_output: u8, + pub(crate) use_dither_map: DitherMapMode, + pub(crate) single_threaded_dithering: bool, +} + +impl QuantizationResult { + pub(crate) fn new(attr: &Attributes, hist: HistogramInternal, freeze_result_colors: bool, gamma: f64) -> Result { + if attr.progress(f32::from(attr.progress_stage1)) { return Err(Aborted); } + let (max_mse, target_mse, target_mse_is_zero) = attr.target_mse(hist.items.len()); + let (mut palette, palette_error) = find_best_palette(attr, target_mse, target_mse_is_zero, max_mse, hist)?; + if freeze_result_colors { + palette.iter_mut().for_each(|(_, p)| *p = p.to_fixed()); + } + if attr.progress(f32::from(attr.progress_stage3).mul_add(0.95, f32::from(attr.progress_stage1) + f32::from(attr.progress_stage2))) { + return Err(Aborted); + } + if let (Some(palette_error), Some(max_mse)) = (palette_error, max_mse) { + if palette_error > max_mse { + attr.verbose_print(format!( + " image degradation MSE={:0.3} (Q={}) exceeded limit of {:0.3} ({})", + internal_mse_to_standard_mse(palette_error), + mse_to_quality(palette_error), + internal_mse_to_standard_mse(max_mse), + mse_to_quality(max_mse) + )); + return Err(QualityTooLow); + } + } + + sort_palette(attr, &mut palette); + + Ok(Self { + palette, + gamma, + palette_error, + min_posterization_output: attr.min_posterization(), + use_dither_map: attr.use_dither_map, + remapped: None, + progress_callback: None, + int_palette: Palette { + count: 0, + entries: [RGBA::default(); MAX_COLORS], + }, + dither_level: 1., + single_threaded_dithering: attr.single_threaded_dithering, + }) + } + + /// This is 100% redundant and unnecessary. This work is done anyway when remap is called. + /// However, this can be called before calling `image.set_background()`, so it may allow better parallelization while the background is generated on another thread. + #[doc(hidden)] + pub fn optionally_prepare_for_dithering_with_background_set(&mut self, image: &mut Image<'_>, output_buf: &mut [MaybeUninit]) -> Result<(), Error> { + let mut output_pixels = RowBitmapMut::new_contiguous(output_buf, image.width()); + Self::optionally_generate_dither_map(self.use_dither_map, image, true, &mut output_pixels, &mut self.palette)?; + Ok(()) + } + + #[inline(never)] + pub(crate) fn write_remapped_image_rows_internal(&mut self, image: &mut Image, mut output_pixels: RowBitmapMut<'_, MaybeUninit>) -> Result<(), Error> { + let progress_stage1 = if self.use_dither_map != DitherMapMode::None { 20 } else { 0 }; + if self.remap_progress(progress_stage1 as f32 * 0.25) { + return Err(Error::Aborted); + } + + image.free_histogram_inputs(); + + let mut palette = self.palette.clone(); + let mut remapped = Box::new(Remapped { + int_palette: Palette { count: 0, entries: [RGBA::default(); MAX_COLORS] }, + palette_error: None, + }); + if self.dither_level == 0. { + palette.init_int_palette(&mut remapped.int_palette, self.gamma, self.min_posterization_output); + remapped.palette_error = Some(remap_to_palette(&mut image.px, image.background.as_deref_mut(), image.importance_map.as_deref(), &mut output_pixels, &mut palette)?.0); + } else { + let uses_background = image.background.is_some(); + let dither_map_error = Self::optionally_generate_dither_map(self.use_dither_map, image, uses_background, &mut output_pixels, &mut palette)?; + if self.remap_progress(progress_stage1 as f32 * 0.5) { + return Err(Error::Aborted); + } + + let output_image_is_remapped = dither_map_error.is_some(); + let palette_error = dither_map_error.or(self.palette_error); + + // remapping above was the last chance to do K-Means iteration, hence the final palette is set after remapping + palette.init_int_palette(&mut remapped.int_palette, self.gamma, self.min_posterization_output); + remapped.palette_error = palette_error; + let max_dither_error = (palette_error.unwrap_or(quality_to_mse(80)) * 2.4).max(quality_to_mse(35)) as f32; + remap_to_palette_floyd(image, output_pixels, &palette, self, max_dither_error, output_image_is_remapped)?; + } + self.remapped = Some(remapped); + Ok(()) + } + + fn optionally_generate_dither_map(use_dither_map: DitherMapMode, image: &mut Image<'_>, uses_background: bool, output_pixels: &mut RowBitmapMut<'_, MaybeUninit>, palette: &mut PalF) -> Result, Error> { + let is_image_huge = (image.px.width * image.px.height) > 2000 * 2000; + let allow_dither_map = use_dither_map == DitherMapMode::Always || (!is_image_huge && use_dither_map != DitherMapMode::None); + let generate_dither_map = allow_dither_map && image.dither_map.is_none(); + if !generate_dither_map { + return Ok(None); + } + + // If dithering (with dither map) is required, this image is used to find areas that require dithering + let (palette_error, row_pointers_remapped) = remap_to_palette(&mut image.px, None, image.importance_map.as_deref(), output_pixels, palette)?; + image.update_dither_map(&row_pointers_remapped, &*palette, uses_background)?; + Ok(Some(palette_error)) + } + + /// Set to 1.0 to get nice smooth image + pub fn set_dithering_level(&mut self, value: f32) -> Result<(), Error> { + if !(0. ..=1.).contains(&value) { + return Err(ValueOutOfRange); + } + + self.remapped = None; + self.dither_level = value; + Ok(()) + } + + /// The default is sRGB gamma (~1/2.2) + pub fn set_output_gamma(&mut self, value: f64) -> Result<(), Error> { + if value <= 0. || value >= 1. { + return Err(ValueOutOfRange); + } + + self.remapped = None; + self.gamma = value; + + Ok(()) + } + + /// Approximate gamma correction value used for the output + /// + /// Colors are converted from input gamma to this gamma + #[inline] + #[must_use] + pub fn output_gamma(&self) -> f64 { + self.gamma + } + + /// Number 0-100 guessing how nice the input image will look if remapped to this palette + #[must_use] + pub fn quantization_quality(&self) -> Option { + self.palette_error.map(mse_to_quality) + } + + /// Approximate mean square error of the palette + #[must_use] + pub fn quantization_error(&self) -> Option { + self.palette_error.map(internal_mse_to_standard_mse) + } + + /// Approximate mean square error of the palette used for the most recent remapping + #[must_use] + pub fn remapping_error(&self) -> Option { + self.remapped.as_ref() + .and_then(|re| re.palette_error) + .or(self.palette_error) + .map(internal_mse_to_standard_mse) + } + + /// Palette remapping error mapped back to 0-100 scale, same as the scale in [`Attributes::set_quality()`] + #[must_use] + pub fn remapping_quality(&self) -> Option { + self.remapped.as_ref() + .and_then(|re| re.palette_error) + .or(self.palette_error) + .map(mse_to_quality) + } + + /// The final palette + /// + /// It's slighly better if you get palette from the [`remapped()`][Self::remapped] call instead + #[inline] + #[must_use] + pub fn palette(&mut self) -> &[RGBA] { + self.int_palette().as_slice() + } + + pub(crate) fn int_palette(&mut self) -> &Palette { + if let Some(remap) = self.remapped.as_ref() { + debug_assert!(remap.int_palette.count > 0); + &remap.int_palette + } else { + if self.int_palette.count == 0 { + self.palette.init_int_palette(&mut self.int_palette, self.gamma, self.min_posterization_output); + } + &self.int_palette + } + } + + /// Callback called at various point of processing, which gets percentage of progress done, + /// and can return `ControlFlow::Break` to abort further processing + #[inline(always)] + pub fn set_progress_callback ControlFlow + Sync + Send + 'static>(&mut self, callback: F) { + self.progress_callback = Some(Box::new(callback)); + } + + // true == abort + pub(crate) fn remap_progress(&self, percent: f32) -> bool { + self.progress_callback.as_ref().map_or(false, |cb| cb(percent) == ControlFlow::Break) + } + + /// Remap image into a palette + indices. + /// + /// Returns the palette and a 1-byte-per-pixel uncompressed bitmap + pub fn remapped(&mut self, image: &mut Image<'_>) -> Result<(Vec, Vec), Error> { + let mut buf = Vec::new(); + let pal = self.remap_into_vec(image, &mut buf)?; + Ok((pal, buf)) + } + + /// Remap image into an existing buffer. Use [`remapped()`][Self::remapped] if you don't have a pre-allocated buffer to reuse. + /// + /// Writes 1-byte-per-pixel uncompressed bitmap into the `Vec`. + /// + /// Returns the palette. + #[inline] + pub fn remap_into_vec(&mut self, image: &mut Image<'_>, buf: &mut Vec) -> Result, Error> { + let len = image.width() * image.height(); + // Capacity is essential here, as it creates uninitialized buffer + unsafe { + buf.clear(); + buf.try_reserve_exact(len)?; + self.remap_into(image, &mut buf.spare_capacity_mut()[..len])?; + buf.set_len(len); + } + Ok(self.palette_vec()) + } + + /// Remap image into an existing buffer. + /// + /// This is a low-level call for use when existing memory has to be reused. Use [`remapped()`][Self::remapped] or [`remap_into_vec()`][Self::remap_into_vec] if possible. + /// + /// Writes 1-byte-per-pixel uncompressed bitmap into the pre-allocated buffer. + /// + /// You should call [`palette()`][Self::palette] _after_ this call, but not before it, + /// because remapping refines the palette. + #[inline] + pub fn remap_into(&mut self, image: &mut Image<'_>, output_buf: &mut [MaybeUninit]) -> Result<(), Error> { + let required_size = (image.width()) * (image.height()); + let output_buf = output_buf.get_mut(0..required_size).ok_or(BufferTooSmall)?; + + let rows = RowBitmapMut::new_contiguous(output_buf, image.width()); + self.write_remapped_image_rows_internal(image, rows) + } + + /// The final palette, copied. + /// + /// It's slighly better if you get palette from the [`remapped()`][Self::remapped] call instead + #[must_use] + pub fn palette_vec(&mut self) -> Vec { + let pal = self.palette(); + let mut out: Vec = Vec::new(); + if out.try_reserve_exact(pal.len()).is_ok() { + out.extend_from_slice(pal); + } + out + } + + /// Expected length of the palette + /// + /// Reads the length without finalizing the colors + pub fn palette_len(&mut self) -> usize { + self.palette.len() + } + + /// Shortcut for making [`Histogram`] with `add_fixed_color` + /// + /// Set `gamma` to `0.` for sRGB colors. + pub fn from_palette(attr: &Attributes, palette: &[RGBA], gamma: f64) -> Result { + if palette.len() > MAX_COLORS { + return Err(Unsupported); + } + + let mut hist = Histogram::new(attr); + for &c in palette { + hist.add_fixed_color(c, gamma)?; + } + hist.quantize(attr) + } + + /// Getter for the value set in [`Self::set_dithering_level`] + #[must_use] + pub fn dithering_level(&self) -> f32 { + self.dither_level + } +} + +impl Clone for QuantizationResult { + /// It will be without a progress callback + fn clone(&self) -> Self { + Self { + remapped: self.remapped.clone(), + palette: self.palette.clone(), + progress_callback: None, + int_palette: self.int_palette.clone(), + dither_level: self.dither_level, + gamma: self.gamma, + palette_error: self.palette_error, + min_posterization_output: self.min_posterization_output, + use_dither_map: self.use_dither_map, + single_threaded_dithering: self.single_threaded_dithering, + } + } +} + +fn sort_palette(attr: &Attributes, palette: &mut PalF) { + let last_index_transparent = attr.last_index_transparent; + + let mut tmp: ArrayVec<_, { MAX_COLORS }> = palette.iter_mut().map(|(c, p)| (*c, *p)).collect(); + tmp.sort_by_key(|(color, pop)| { + let trns = !color.is_fully_opaque(); + (trns == last_index_transparent, Reverse(OrdFloat::new(pop.popularity()))) + }); + palette.iter_mut().zip(tmp).for_each(|((dcol, dpop), (scol, spop))| { + *dcol = scol; + *dpop = spop; + }); + + if last_index_transparent { + let alpha_index = palette.as_slice().iter().enumerate() + .filter(|(_, c)| !c.is_fully_opaque()) + .min_by_key(|(_, c)| OrdFloat::new(c.a)) + .map(|(i, _)| i); + if let Some(alpha_index) = alpha_index { + let last_index = palette.as_slice().len() - 1; + palette.swap(last_index, alpha_index); + } + } else { + let num_transparent = palette.as_slice().iter().enumerate().rev() + .find(|(_, c)| !c.is_fully_opaque()) + .map(|(i, _)| i + 1); // num entries, not index + if let Some(num_transparent) = num_transparent { + attr.verbose_print(format!(" eliminated opaque tRNS-chunk entries...{} entr{} transparent", num_transparent, if num_transparent == 1 { "y" } else { "ies" })); + } + } +} + +impl fmt::Debug for QuantizationResult { + #[cold] + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "QuantizationResult(q={})", self.quantization_quality().unwrap_or(0)) + } +} + +/// Repeats mediancut with different histogram weights to find palette with minimum error. +/// +/// `feedback_loop_trials` controls how long the search will take. < 0 skips the iteration. +#[allow(clippy::or_fun_call)] +pub(crate) fn find_best_palette(attr: &Attributes, target_mse: f64, target_mse_is_zero: bool, max_mse: Option, mut hist: HistogramInternal) -> Result<(PalF, Option), Error> { + // hist.items includes fixed colors already + let few_input_colors = hist.items.len() <= attr.max_colors as usize; + // actual target_mse passed to this method has extra diff from posterization + if few_input_colors && target_mse_is_zero { + return Ok(palette_from_histogram(&hist, attr.max_colors)); + } + + let mut max_colors = attr.max_colors; + let total_trials = attr.feedback_loop_trials(hist.items.len()) as i16; + let mut trials_left = total_trials; + let mut best_palette = None; + let mut target_mse_overshoot = if total_trials > 0 { 1.05 } else { 1. }; + let mut fails_in_a_row = 0; + let mut palette_error = None; + let mut palette = loop { + let max_mse_per_color = target_mse.max(palette_error.unwrap_or(quality_to_mse(1))).max(quality_to_mse(51)) * 1.2; + let mut new_palette = mediancut(&mut hist, max_colors, target_mse * target_mse_overshoot, max_mse_per_color)? + .with_fixed_colors(attr.max_colors, &hist.fixed_colors); + + let stage_done = (f32::from(trials_left.max(0)) / f32::from(total_trials + 1)).mul_add(-(f32::from(trials_left.max(0)) / f32::from(total_trials + 1)), 1.); + let overall_done = stage_done.mul_add(f32::from(attr.progress_stage2), f32::from(attr.progress_stage1)); + attr.verbose_print(format!(" selecting colors...{}%", (100. * stage_done) as u8)); + + if trials_left <= 0 { break Some(new_palette); } + + let first_run_of_target_mse = best_palette.is_none() && target_mse > 0.; + let total_error = Kmeans::iteration(&mut hist, &mut new_palette, !first_run_of_target_mse)?; + if best_palette.is_none() || total_error < palette_error.unwrap_or(f64::MAX) || (total_error <= target_mse && new_palette.len() < max_colors as usize) { + if total_error < target_mse && total_error > 0. { + target_mse_overshoot = if (target_mse_overshoot * 1.25) < (target_mse / total_error) {target_mse_overshoot * 1.25 } else {target_mse / total_error }; // if number of colors could be reduced, try to keep it that way + } + palette_error = Some(total_error); + max_colors = max_colors.min(new_palette.len() as PalLen + 1); + trials_left -= 1; + fails_in_a_row = 0; + best_palette = Some(new_palette); + } else { + fails_in_a_row += 1; + target_mse_overshoot = 1.; + trials_left -= 5 + fails_in_a_row; + } + if attr.progress(overall_done) || trials_left <= 0 { + break best_palette; + } + }.ok_or(ValueOutOfRange)?; + + refine_palette(&mut palette, attr, &mut hist, max_mse, &mut palette_error)?; + + Ok((palette, palette_error)) +} + + +fn refine_palette(palette: &mut PalF, attr: &Attributes, hist: &mut HistogramInternal, max_mse: Option, palette_error: &mut Option) -> Result<(), Error> { + let (iterations, iteration_limit) = attr.kmeans_iterations(hist.items.len(), palette_error.is_some()); + if iterations > 0 { + attr.verbose_print(" moving colormap towards local minimum"); + let mut i = 0; + while i < iterations { + let stage_done = f32::from(i) / f32::from(iterations); + let overall_done = (stage_done * f32::from(attr.progress_stage3)).mul_add(0.89, f32::from(attr.progress_stage1) + f32::from(attr.progress_stage2)); + if attr.progress(overall_done) { + break; + } + + let pal_err = Kmeans::iteration(hist, palette, false)?; + debug_assert!(pal_err < 1e20); + let previous_palette_error = *palette_error; + *palette_error = Some(pal_err); + + if let Some(previous_palette_error) = previous_palette_error { + if (previous_palette_error - pal_err).abs() < iteration_limit { + break; + } + } + i += if pal_err > max_mse.unwrap_or(1e20) * 1.5 { 2 } else { 1 }; + } + } + Ok(()) +} + +#[cold] +fn palette_from_histogram(hist: &HistogramInternal, max_colors: PalLen) -> (PalF, Option) { + let mut hist_pal = PalF::new(); + for item in hist.items.iter() { + hist_pal.push(item.color, PalPop::new(item.perceptual_weight)); + } + + (hist_pal.with_fixed_colors(max_colors, &hist.fixed_colors), Some(0.)) +} + +pub(crate) fn quality_to_mse(quality: u8) -> f64 { + if quality == 0 { + return 1e20; // + epsilon for floating point errors + } + if quality >= 100 { return 0.; } + let extra_low_quality_fudge = (0.016 / (0.001 + f64::from(quality)) - 0.001).max(0.); + unit_mse_to_internal_mse(extra_low_quality_fudge + 2.5 / (210. + f64::from(quality)).powf(1.2) * (100.1 - f64::from(quality)) / 100.) +} + +pub(crate) fn mse_to_quality(mse: f64) -> u8 { + for i in (1..101).rev() { + if mse <= quality_to_mse(i) + 0.000001 { return i; } + } + 0 +} diff --git a/src/rayoff.rs b/src/rayoff.rs new file mode 100644 index 0000000..2ffa0d9 --- /dev/null +++ b/src/rayoff.rs @@ -0,0 +1,86 @@ +use core::slice::ChunksMut; +use core::cell::OnceCell; + +#[cfg(all(not(feature = "std"), feature = "no_std"))] +use std::boxed::Box; + +pub(crate) struct ThreadLocal(OnceCell); + +impl ThreadLocal { + #[inline(always)] + pub fn new() -> Self { + Self(OnceCell::new()) + } + + #[inline(always)] + pub fn get_or(&self, f: impl FnOnce() -> T) -> &T { + self.0.get_or_init(f) + } + + #[inline(always)] + pub fn get_or_try(&self, f: impl FnOnce() -> Result) -> Result<&T, core::convert::Infallible> { + // https://github.com/rust-lang/rust/issues/109737 + Ok(self.0.get_or_init(move || f().ok().unwrap())) + } +} + +impl IntoIterator for ThreadLocal { + type IntoIter = core::option::IntoIter; + type Item = T; + + #[inline(always)] + fn into_iter(mut self) -> Self::IntoIter { + self.0.take().into_iter() + } +} + +pub(crate) trait FakeRayonIter: Sized + Iterator { + fn par_bridge(self) -> Self { self } + fn for_each_init(self, init: I, mut cb: F) where I: FnOnce() -> T, F: FnMut(&mut T, Self::Item) { + let mut tmp = init(); + for item in self { + cb(&mut tmp, item); + } + } +} + + +impl FakeRayonIter for T where Self: Sized { +} + +pub(crate) trait FakeRayonIntoIter { + fn par_chunks_mut(&mut self, chunk_size: usize) -> ChunksMut<'_, T>; +} + +impl<'a, T> FakeRayonIntoIter for &'a mut [T] { + #[inline(always)] + fn par_chunks_mut(&mut self, chunk_size: usize) -> ChunksMut<'_, T> { + self.chunks_mut(chunk_size) + } +} + +impl<'a, T> FakeRayonIntoIter for Box<[T]> { + #[inline(always)] + fn par_chunks_mut(&mut self, chunk_size: usize) -> ChunksMut<'_, T> { + self.chunks_mut(chunk_size) + } +} + +pub(crate) struct SpawnMock; + +impl SpawnMock { + #[inline(always)] + pub fn spawn(&self, f: F) -> R where F: FnOnce(SpawnMock) -> R { + f(SpawnMock) + } +} + +#[inline(always)] +pub(crate) fn scope(f: F) -> R where F: FnOnce(SpawnMock) -> R { + f(SpawnMock) +} + +#[inline(always)] +pub(crate) fn num_cpus() -> usize { + 1 +} diff --git a/src/remap.rs b/src/remap.rs new file mode 100644 index 0000000..c0518bb --- /dev/null +++ b/src/remap.rs @@ -0,0 +1,353 @@ +use crate::error::Error; +use crate::image::Image; +use crate::kmeans::Kmeans; +use crate::nearest::Nearest; +use crate::pal::{f_pixel, PalF, PalIndexRemap, Palette, ARGBF}; +use crate::quant::QuantizationResult; +use crate::rayoff::*; +use crate::rows::{temp_buf, DynamicRows}; +use crate::seacow::{RowBitmap, RowBitmapMut}; +use crate::CacheLineAlign; +use core::cell::RefCell; +use core::mem::MaybeUninit; + +#[cfg(all(not(feature = "std"), feature = "no_std"))] +use crate::no_std_compat::*; + +#[repr(u8)] +#[derive(Eq, PartialEq, Clone, Copy)] +pub enum DitherMapMode { + None = 0, + Enabled = 1, + Always = 2, +} + +#[derive(Clone)] +pub(crate) struct Remapped { + pub(crate) int_palette: Palette, + pub(crate) palette_error: Option, +} + +#[inline(never)] +pub(crate) fn remap_to_palette<'x, 'b: 'x>(px: &mut DynamicRows, background: Option<&mut Image<'_>>, importance_map: Option<&[u8]>, output_pixels: &'x mut RowBitmapMut<'b, MaybeUninit>, palette: &mut PalF) -> Result<(f64, RowBitmap<'x, PalIndexRemap>), Error> { + let n = Nearest::new(palette)?; + let colors = palette.as_slice(); + let palette_len = colors.len(); + if palette_len > PalIndexRemap::MAX as usize + 1 { + return Err(Error::Unsupported); + } + + let tls = ThreadLocal::new(); + let width = px.width as usize; + let per_thread_buffers = move || -> Result<_, Error> { Ok(CacheLineAlign(RefCell::new((Kmeans::new(palette_len)?, temp_buf(width)?, temp_buf(width)?, temp_buf(width)?)))) }; + + let tls_tmp1 = tls.get_or_try(per_thread_buffers)?; + let mut tls_tmp = tls_tmp1.0.borrow_mut(); + + let input_rows = px.rows_iter(&mut tls_tmp.1)?; + let (background, transparent_index) = background.map(|background| { + (Some(background), n.search(&f_pixel::default(), 0).0 as PalIndexRemap) + }) + .filter(|&(_, transparent_index)| colors[usize::from(transparent_index)].is_fully_transparent()) + .unwrap_or((None, 0)); + let background = background.map(|bg| bg.px.rows_iter(&mut tls_tmp.1)).transpose()?; + + if background.is_some() { + tls_tmp.0.update_color(f_pixel::default(), 1., transparent_index as _); + } + + drop(tls_tmp); + + let remapping_error = output_pixels.rows_mut().enumerate().par_bridge().map(|(row, output_pixels_row)| { + let mut remapping_error = 0.; + #[allow(irrefutable_let_patterns)] + let Ok(tls_res) = tls.get_or_try(per_thread_buffers) else { return f64::NAN }; + let (kmeans, temp_row, temp_row_f, temp_row_f_bg) = &mut *tls_res.0.borrow_mut(); + + let output_pixels_row = &mut output_pixels_row[..width]; + let importance_map = importance_map.and_then(|m| m.get(row * width..)).unwrap_or(&[]); + let row_pixels = &input_rows.row_f_shared(temp_row, temp_row_f, row)[..width]; + let bg_pixels = if let Some(background) = &background { + &background.row_f_shared(temp_row, temp_row_f_bg, row)[..width] + } else { &[] }; + + let mut last_match = 0; + for (col, (inp, out)) in row_pixels.iter().zip(output_pixels_row).enumerate() { + let (matched, diff) = n.search(inp, last_match as _); + let matched = matched as PalIndexRemap; + last_match = matched; + if let Some(bg) = bg_pixels.get(col) { + let bg_diff = bg.diff(inp); + if bg_diff <= diff { + remapping_error += f64::from(bg_diff); + out.write(transparent_index); + continue; + } + } + remapping_error += f64::from(diff); + out.write(matched); + let importance = f32::from(importance_map.get(col).copied().unwrap_or(1)); + kmeans.update_color(*inp, importance, matched as _); + } + remapping_error + }) + .sum::(); + + if remapping_error.is_nan() { + return Err(Error::OutOfMemory); + } + + if let Some(kmeans) = tls.into_iter() + .map(|t| t.0.into_inner().0) + .reduce(Kmeans::merge) { kmeans.finalize(palette); } + + let remapping_error = remapping_error / f64::from(px.width * px.height); + Ok((remapping_error, unsafe { output_pixels.assume_init() })) +} + +fn get_dithered_pixel(dither_level: f32, max_dither_error: f32, thiserr: f_pixel, px: f_pixel) -> f_pixel { + let s = thiserr.0 * dither_level; + // This prevents gaudy green pixels popping out of the blue (or red or black! ;) + let dither_error = s.r.mul_add(s.r, s.g * s.g) + s.b.mul_add(s.b, s.a * s.a); + if dither_error < 2. / 256. / 256. { + // don't dither areas that don't have noticeable error — makes file smaller + return px; + } + + let mut ratio: f32 = 1.; + const MAX_OVERFLOW: f32 = 1.1; + const MAX_UNDERFLOW: f32 = -0.1; + // allowing some overflow prevents undithered bands caused by clamping of all channels + if px.r + s.r > MAX_OVERFLOW { + ratio = ratio.min((MAX_OVERFLOW - px.r) / s.r); + } else if px.r + s.r < MAX_UNDERFLOW { + ratio = ratio.min((MAX_UNDERFLOW - px.r) / s.r); + } + if px.g + s.g > MAX_OVERFLOW { + ratio = ratio.min((MAX_OVERFLOW - px.g) / s.g); + } else if px.g + s.g < MAX_UNDERFLOW { + ratio = ratio.min((MAX_UNDERFLOW - px.g) / s.g); + } + if px.b + s.b > MAX_OVERFLOW { + ratio = ratio.min((MAX_OVERFLOW - px.b) / s.b); + } else if px.b + s.b < MAX_UNDERFLOW { + ratio = ratio.min((MAX_UNDERFLOW - px.b) / s.b); + } + if dither_error > max_dither_error { + ratio *= 0.8; + } + f_pixel(ARGBF { + a: (px.a + s.a).clamp(0., 1.), + r: s.r.mul_add(ratio, px.r), + g: s.g.mul_add(ratio, px.g), + b: s.b.mul_add(ratio, px.b), + }) +} + +/// Uses edge/noise map to apply dithering only to flat areas. Dithering on edges creates jagged lines, and noisy areas are "naturally" dithered. +/// +/// If `output_image_is_remapped` is true, only pixels noticeably changed by error diffusion will be written to output image. +#[inline(never)] +pub(crate) fn remap_to_palette_floyd(input_image: &mut Image, mut output_pixels: RowBitmapMut<'_, MaybeUninit>, palette: &PalF, quant: &QuantizationResult, max_dither_error: f32, output_image_is_remapped: bool) -> Result<(), Error> { + let progress_stage1 = if quant.use_dither_map != DitherMapMode::None { 20 } else { 0 }; + + let width = input_image.width(); + let height = input_image.height(); + + let mut temp_row = temp_buf(width)?; + + let dither_map = if quant.use_dither_map != DitherMapMode::None { + input_image.dither_map.as_deref().or(input_image.edges.as_deref()).unwrap_or(&[]) + } else { + &[] + }; + + let n = Nearest::new(palette)?; + let palette = palette.as_slice(); + + let mut background = input_image.background.as_mut().map(|bg| { + bg.px.prepare_iter(&mut temp_row, true)?; + Ok::<_, Error>(&bg.px) + }).transpose()?; + + let transparent_index = if background.is_some() { n.search(&f_pixel::default(), 0).0 as PalIndexRemap } else { 0 }; + if background.is_some() && !palette[transparent_index as usize].is_fully_transparent() { + background = None; + } + // response to this value is non-linear and without it any value < 0.8 would give almost no dithering + let mut base_dithering_level = (1. - quant.dither_level).mul_add(-(1. - quant.dither_level), 1.) * (15. / 16.); // prevent small errors from accumulating + if !dither_map.is_empty() { + base_dithering_level *= 1. / 255.; // dither_map is in 0-255 scale + } + + // when using remapping on top of a background, lots of pixels may be transparent, making poor guesses + // (guesses are only for speed, don't affect visuals) + let guess_from_remapped_pixels = output_image_is_remapped && background.is_none(); + + input_image.px.prepare_iter(&mut temp_row, true)?; + let input_image_px = &input_image.px; + let n = &n; + + // Chunks have overhead, so should be big (more than 2 bring diminishing results). Chunks risk causing seams, so should be tall. + let num_chunks = if quant.single_threaded_dithering { 1 } else { (width * height / 524_288).min(height / 128).max(if height > 128 {2} else {1}).min(num_cpus()) }; + let chunks = output_pixels.chunks((height + num_chunks - 1) / num_chunks).map(CacheLineAlign); + scope(move |s| { + let mut chunk_start_row = 0; + for mut chunk in chunks { + let chunk_len = chunk.0.len(); + let mut temp_row = temp_buf(width)?; + let mut input_image_iter = input_image_px.rows_iter_prepared()?; + let mut background = background.map(|bg| bg.rows_iter_prepared()).transpose()?; + let mut diffusion = Vec::new(); + let errwidth = width + 2; // +2 saves from checking out of bounds access + diffusion.try_reserve_exact(errwidth * 2)?; + diffusion.resize(errwidth * 2, f_pixel::default()); + + // restart of dithering creates a seam. this does redundant work to init diffusion state, + // so that later chunks don't start from scratch + if chunk_start_row > 2 { + let mut discard_row = temp_buf(width)?; + for row in (chunk_start_row - 2) .. chunk_start_row { + let row_pixels = input_image_iter.row_f(&mut temp_row, row as _); + let bg_pixels = background.as_mut().map(|b| b.row_f(&mut temp_row, row as _)).unwrap_or(&[]); + let dither_map = dither_map.get(row * width .. row * width + width).unwrap_or(&[]); + let scan_forward = row & 1 == 0; + dither_row(row_pixels, &mut discard_row, width as u32, dither_map, base_dithering_level, max_dither_error, n, palette, transparent_index, bg_pixels, guess_from_remapped_pixels, &mut diffusion, scan_forward); + } + } + // parallel remap makes progress not very useful + if quant.remap_progress(progress_stage1 as f32 + chunk_start_row as f32 * (100. - progress_stage1 as f32) / height as f32) { + return Err(Error::Aborted); + } + s.spawn(move |_| { + for (chunk_row, output_pixels_row) in chunk.0.rows_mut().enumerate() { + let row = chunk_start_row + chunk_row; + let row_pixels = input_image_iter.row_f(&mut temp_row, row as _); + let bg_pixels = background.as_mut().map(|b| b.row_f(&mut temp_row, row as _)).unwrap_or(&[]); + let dither_map = dither_map.get(row * width .. row * width + width).unwrap_or(&[]); + let scan_forward = row & 1 == 0; + dither_row(row_pixels, output_pixels_row, width as u32, dither_map, base_dithering_level, max_dither_error, n, palette, transparent_index, bg_pixels, guess_from_remapped_pixels, &mut diffusion, scan_forward); + } + }); + chunk_start_row += chunk_len; + } + Ok(()) + }) +} + +#[inline(never)] +fn dither_row(row_pixels: &[f_pixel], output_pixels_row: &mut [MaybeUninit], width: u32, dither_map: &[u8], base_dithering_level: f32, max_dither_error: f32, n: &Nearest, palette: &[f_pixel], transparent_index: PalIndexRemap, bg_pixels: &[f_pixel], guess_from_remapped_pixels: bool, diffusion: &mut [f_pixel], even_row: bool) { + let width = width as usize; + assert_eq!(row_pixels.len(), width); + assert_eq!(output_pixels_row.len(), width); + + let (thiserr, nexterr) = { + // +2 saves from checking out of bounds access + let (d1, d2) = diffusion.split_at_mut(width + 2); + if even_row { (d1, d2) } else { (d2, d1) } + }; + + nexterr.fill_with(f_pixel::default); + + let mut undithered_bg_used = 0u8; + let mut last_match = 0; + for x in 0..width { + let col = if even_row { x } else { width - 1 - x }; + let thiserr = &mut thiserr[col .. col + 3]; + let nexterr = &mut nexterr[col .. col + 3]; + let input_px = row_pixels[col]; + + let mut dither_level = base_dithering_level; + if let Some(&l) = dither_map.get(col) { + dither_level *= f32::from(l); + } + + let spx = get_dithered_pixel(dither_level, max_dither_error, thiserr[1], input_px); + let guessed_match = if guess_from_remapped_pixels { + unsafe { output_pixels_row[col].assume_init() } + } else { + last_match + }; + let (matched, dither_diff) = n.search(&spx, guessed_match as _); + let mut matched = matched as PalIndexRemap; + last_match = matched as PalIndexRemap; + let mut output_px = palette[last_match as usize]; + if let Some(bg_pixel) = bg_pixels.get(col) { + // if the background makes better match *with* dithering, it's a definitive win + let bg_for_dither_diff = spx.diff(bg_pixel); + if bg_for_dither_diff <= dither_diff { + output_px = *bg_pixel; + matched = transparent_index; + } else if undithered_bg_used > 1 { + // the undithered fallback can cause artifacts when too many undithered pixels accumulate a big dithering error + // so periodically ignore undithered fallback to prevent that + undithered_bg_used = 0; + } else { + // if dithering is not applied, there's a high risk of creating artifacts (flat areas, error accumulating badly), + // OTOH poor dithering disturbs static backgrounds and creates oscilalting frames that break backgrounds + // back and forth in two differently bad ways + let max_diff = input_px.diff(bg_pixel); + let dithered_diff = input_px.diff(&output_px); + // if dithering is worse than natural difference between frames + // (this rule dithers moving areas, but does not dither static areas) + if dithered_diff > max_diff { + // then see if an undithered color is closer to the ideal + let guessed_px = palette[guessed_match as usize]; + let undithered_diff = input_px.diff(&guessed_px); // If dithering error is crazy high, don't propagate it that much + if undithered_diff < max_diff { + undithered_bg_used += 1; + output_px = guessed_px; + matched = guessed_match; + } + } + } + } + output_pixels_row[col].write(matched); + let mut err = spx.0 - output_px.0; + // This prevents weird green pixels popping out of the blue (or red or black! ;) + if err.r.mul_add(err.r, err.g * err.g) + err.b.mul_add(err.b, err.a * err.a) > max_dither_error { + err *= 0.75; + } + if even_row { + thiserr[2].0 += err * (7. / 16.); + nexterr[0].0 += err * (3. / 16.); + nexterr[1].0 += err * (5. / 16.); + nexterr[2].0 = err * (1. / 16.); + } else { + thiserr[0].0 += err * (7. / 16.); + nexterr[0].0 = err * (1. / 16.); + nexterr[1].0 += err * (5. / 16.); + nexterr[2].0 += err * (3. / 16.); + } + } +} + +#[test] +fn send() { + fn is_send() {} + + is_send::>>(); +} + +#[test] +fn background_to_nop() { + use crate::RGBA; + let pixels: Vec<_> = (0..200*200).map(|n| RGBA::new(n as u8, (n/17) as u8, (n/78) as u8, 255)).collect(); + + let mut attr = crate::new(); + let mut img = attr.new_image_borrowed(&pixels, 200, 200, 0.).unwrap(); + let img2 = attr.new_image_borrowed(&pixels, 200, 200, 0.).unwrap(); + img.set_background(img2).unwrap(); + img.add_fixed_color(RGBA::new(0,0,0,0)).unwrap(); + attr.set_max_colors(3).unwrap(); + let mut res = attr.quantize(&mut img).unwrap(); + res.set_dithering_level(0.).unwrap(); + let (_, idx) = res.remapped(&mut img).unwrap(); + let first = idx[0]; + assert!(idx.iter().all(|&x| x == first)); + + res.set_dithering_level(1.).unwrap(); + let (_, idx) = res.remapped(&mut img).unwrap(); + let first = idx[0]; + assert!(idx.iter().all(|&x| x == first)); +} diff --git a/src/rows.rs b/src/rows.rs new file mode 100644 index 0000000..5256e5f --- /dev/null +++ b/src/rows.rs @@ -0,0 +1,293 @@ +use crate::error::Error; +use crate::pal::{f_pixel, gamma_lut, RGBA}; +use crate::seacow::{Pointer, SeaCow}; +use crate::LIQ_HIGH_MEMORY_LIMIT; +use core::mem::{size_of, MaybeUninit}; +use core::slice; + +#[cfg(all(not(feature = "std"), feature = "no_std"))] +use std::{boxed::Box, vec::Vec}; + +pub(crate) type RowCallback<'a> = dyn Fn(&mut [MaybeUninit], usize) + Send + Sync + 'a; + +pub(crate) enum PixelsSource<'pixels, 'rows> { + /// The `pixels` field is never read, but it is used to store the rows. + #[allow(dead_code)] + Pixels { + rows: SeaCow<'rows, Pointer>, + pixels: Option>, + }, + Callback(Box>), +} + +impl<'pixels> PixelsSource<'pixels, '_> { + pub(crate) fn for_pixels(pixels: SeaCow<'pixels, RGBA>, width: u32, height: u32, stride: u32) -> Result { + if stride < width || height == 0 || width == 0 { + return Err(Error::ValueOutOfRange); + } + let stride = stride as usize; + let width = width as usize; + let height = height as usize; + + let slice = pixels.as_slice(); + let min_area = stride.checked_mul(height).and_then(|a| a.checked_add(width)).ok_or(Error::ValueOutOfRange)? - stride; + if slice.len() < min_area { + return Err(Error::BufferTooSmall); + } + + let rows = SeaCow::boxed(slice.chunks(stride).map(|row| Pointer(row.as_ptr())).take(height).collect()); + Ok(Self::Pixels { rows, pixels: Some(pixels) }) + } +} + +pub(crate) struct DynamicRows<'pixels, 'rows> { + pub(crate) width: u32, + pub(crate) height: u32, + f_pixels: Option>, + pixels: PixelsSource<'pixels, 'rows>, + pub(crate) gamma: f64, +} + +impl Clone for DynamicRows<'_, '_> { + fn clone(&self) -> Self { + Self { + width: self.width, + height: self.height, + f_pixels: self.f_pixels.clone(), + pixels: match &self.pixels { + PixelsSource::Pixels { rows, pixels } => PixelsSource::Pixels { + rows: rows.clone(), + pixels: pixels.clone(), + }, + PixelsSource::Callback(_) => { + let area = self.width as usize * self.height as usize; + let mut out = Vec::with_capacity(area); + let out_rows = out.spare_capacity_mut()[..area].chunks_exact_mut(self.width as usize); + for (i, row) in out_rows.enumerate() { + self.row_rgba(row, i); + } + unsafe { + out.set_len(area); + } + let pixels = SeaCow::boxed(out.into_boxed_slice()); + PixelsSource::for_pixels(pixels, self.width, self.height, self.width).unwrap() + }, + }, + gamma: self.gamma, + } + } +} + +pub(crate) struct DynamicRowsIter<'parent, 'pixels, 'rows> { + px: &'parent DynamicRows<'pixels, 'rows>, + temp_f_row: Option]>>, +} + +impl DynamicRowsIter<'_, '_, '_> { + #[must_use] + pub fn row_f<'px>(&'px mut self, temp_row: &mut [MaybeUninit], row: usize) -> &'px [f_pixel] { + debug_assert_eq!(temp_row.len(), self.px.width as usize); + if let Some(pixels) = self.px.f_pixels.as_ref() { + let start = self.px.width as usize * row; + &pixels[start..start + self.px.width as usize] + } else { + let lut = gamma_lut(self.px.gamma); + let row_pixels = self.px.row_rgba(temp_row, row); + + match self.temp_f_row.as_mut() { + Some(t) => DynamicRows::convert_row_to_f(t, row_pixels, &lut), + None => &mut [], // this can't happen + } + } + } + + #[must_use] + pub fn row_f_shared<'px>(&'px self, temp_row: &mut [MaybeUninit], temp_row_f: &'px mut [MaybeUninit], row: usize) -> &'px [f_pixel] { + if let Some(pixels) = self.px.f_pixels.as_ref() { + &pixels[self.px.width as usize * row..] + } else { + let lut = gamma_lut(self.px.gamma); + let row_pixels = self.px.row_rgba(temp_row, row); + + DynamicRows::convert_row_to_f(temp_row_f, row_pixels, &lut) + } + } + + #[must_use] + pub fn row_rgba<'px>(&'px self, temp_row: &'px mut [MaybeUninit], row: usize) -> &'px [RGBA] { + self.px.row_rgba(temp_row, row) + } +} + +impl<'pixels, 'rows> DynamicRows<'pixels, 'rows> { + #[inline] + pub(crate) fn new(width: u32, height: u32, pixels: PixelsSource<'pixels, 'rows>, gamma: f64) -> Self { + debug_assert!(gamma > 0.); + Self { width, height, f_pixels: None, pixels, gamma } + } + + fn row_rgba<'px>(&'px self, temp_row: &'px mut [MaybeUninit], row: usize) -> &'px [RGBA] { + match &self.pixels { + PixelsSource::Pixels { rows, .. } => unsafe { + slice::from_raw_parts(rows.as_slice()[row].0, self.width()) + }, + PixelsSource::Callback(cb) => { + cb(temp_row, row); + // cb needs to be marked as unsafe, since it's responsible for initialization :( + unsafe { slice_assume_init_mut(temp_row) } + }, + } + } + + fn convert_row_to_f<'f>(row_f_pixels: &'f mut [MaybeUninit], row_pixels: &[RGBA], gamma_lut: &[f32; 256]) -> &'f mut [f_pixel] { + assert_eq!(row_f_pixels.len(), row_pixels.len()); + for (dst, src) in row_f_pixels.iter_mut().zip(row_pixels) { + dst.write(f_pixel::from_rgba(gamma_lut, *src)); + } + // Safe, just initialized + unsafe { slice_assume_init_mut(row_f_pixels) } + } + + #[must_use] + fn should_use_low_memory(&self) -> bool { + self.width() * self.height() > LIQ_HIGH_MEMORY_LIMIT / size_of::() + } + + #[inline] + fn temp_f_row_for_iter(&self) -> Result]>>, Error> { + if self.f_pixels.is_some() { + return Ok(None); + } + Ok(Some(temp_buf(self.width())?)) + } + + pub fn prepare_iter(&mut self, temp_row: &mut [MaybeUninit], allow_steamed: bool) -> Result<(), Error> { + debug_assert_eq!(temp_row.len(), self.width as _); + + if self.f_pixels.is_some() || (allow_steamed && self.should_use_low_memory()) { + return Ok(()); + } + + let width = self.width(); + let lut = gamma_lut(self.gamma); + let mut f_pixels = temp_buf(width * self.height())?; + for (row, f_row) in f_pixels.chunks_exact_mut(width).enumerate() { + let row_pixels = self.row_rgba(temp_row, row); + Self::convert_row_to_f(f_row, row_pixels, &lut); + } + // just initialized + self.f_pixels = Some(unsafe { box_assume_init(f_pixels) }); + Ok(()) + } + + #[inline] + pub fn rows_iter(&mut self, temp_row: &mut [MaybeUninit]) -> Result, Error> { + self.prepare_iter(temp_row, true)?; + Ok(DynamicRowsIter { + temp_f_row: self.temp_f_row_for_iter()?, + px: self, + }) + } + + /// Call `prepare_iter()` first + #[inline] + pub fn rows_iter_prepared(&self) -> Result, Error> { + Ok(DynamicRowsIter { + temp_f_row: self.temp_f_row_for_iter()?, + px: self, + }) + } + + #[inline] + pub fn rgba_rows_iter(&self) -> Result, Error> { + // This happens when histogram image is recycled + if let PixelsSource::Pixels { rows, .. } = &self.pixels { + if rows.as_slice().is_empty() { + return Err(Error::Unsupported); + } + } + Ok(DynamicRowsIter { px: self, temp_f_row: None }) + } + + #[inline] + pub fn all_rows_f(&mut self) -> Result<&[f_pixel], Error> { + if self.f_pixels.is_some() { + return Ok(self.f_pixels.as_ref().unwrap()); // borrow-checker :( + } + self.prepare_iter(&mut temp_buf(self.width())?, false)?; + self.f_pixels.as_deref().ok_or(Error::Unsupported) + } + + /// Not recommended + #[cfg(feature = "_internal_c_ffi")] + pub(crate) unsafe fn set_memory_ownership(&mut self, own_rows: bool, own_pixels: bool, free_fn: unsafe extern "C" fn(*mut core::ffi::c_void)) -> Result<(), Error> { + if own_rows { + match &mut self.pixels { + PixelsSource::Pixels { rows, .. } => rows.make_owned(free_fn), + PixelsSource::Callback(_) => return Err(Error::ValueOutOfRange), + } + } + + if own_pixels { + let len = self.width() * self.height(); + match &mut self.pixels { + PixelsSource::Pixels { pixels: Some(pixels), .. } => pixels.make_owned(free_fn), + PixelsSource::Pixels { pixels, rows } => { + // the row with the lowest address is assumed to be at the start of the bitmap + let ptr = rows.as_slice().iter().map(|p| p.0).min().ok_or(Error::Unsupported)?; + *pixels = Some(SeaCow::c_owned(ptr.cast_mut(), len, free_fn)); + }, + PixelsSource::Callback(_) => return Err(Error::ValueOutOfRange), + } + } + Ok(()) + } + + pub fn free_histogram_inputs(&mut self) { + if self.f_pixels.is_some() { + self.pixels = PixelsSource::Pixels { + rows: SeaCow::borrowed(&[]), + pixels: None, + }; + } + } + + #[inline(always)] + #[must_use] + pub const fn width(&self) -> usize { + self.width as usize + } + + #[inline(always)] + #[must_use] + pub const fn height(&self) -> usize { + self.height as usize + } +} + +pub(crate) fn temp_buf(len: usize) -> Result]>, Error> { + let mut v = Vec::new(); + v.try_reserve_exact(len)?; + unsafe { v.set_len(len) }; + Ok(v.into_boxed_slice()) +} + +#[test] +fn send() { + fn is_send() {} + fn is_sync() {} + is_send::(); + is_sync::(); + is_send::(); + is_sync::(); +} + +#[inline(always)] +unsafe fn box_assume_init(s: Box<[MaybeUninit]>) -> Box<[T]> { + core::mem::transmute(s) +} + +#[inline(always)] +unsafe fn slice_assume_init_mut(s: &mut [MaybeUninit]) -> &mut [T] { + &mut *(s as *mut [MaybeUninit] as *mut [T]) +} diff --git a/src/seacow.rs b/src/seacow.rs new file mode 100644 index 0000000..f90dacf --- /dev/null +++ b/src/seacow.rs @@ -0,0 +1,203 @@ +use core::mem::{self, MaybeUninit}; +use core::slice; + +#[cfg(all(not(feature = "std"), feature = "no_std"))] +use std::{boxed::Box, vec::Vec}; + +#[cfg(feature = "_internal_c_ffi")] +use core::ffi::c_void; + +#[derive(Clone)] +pub struct SeaCow<'a, T> { + inner: SeaCowInner<'a, T>, +} + +unsafe impl Send for SeaCowInner<'_, T> {} +unsafe impl Sync for SeaCowInner<'_, T> {} + +/// Rust assumes `*const T` is never `Send`/`Sync`, but it can be. +/// This is fudge for https://github.com/rust-lang/rust/issues/93367 +#[repr(transparent)] +#[derive(Copy, Clone)] +pub(crate) struct Pointer(pub *const T); + +#[derive(Copy, Clone)] +#[repr(transparent)] +pub(crate) struct PointerMut(pub *mut T); + +unsafe impl Send for Pointer {} +unsafe impl Sync for Pointer {} +unsafe impl Send for PointerMut {} +unsafe impl Sync for PointerMut {} + +impl SeaCow<'static, T> { + #[inline] + #[must_use] + pub fn boxed(data: Box<[T]>) -> Self { + Self { inner: SeaCowInner::Boxed(data) } + } +} + +impl<'a, T> SeaCow<'a, T> { + #[inline] + #[must_use] + pub const fn borrowed(data: &'a [T]) -> Self { + Self { inner: SeaCowInner::Borrowed(data) } + } + + /// The pointer must be `malloc`-allocated + #[inline] + #[cfg(feature = "_internal_c_ffi")] + #[must_use] + pub unsafe fn c_owned(ptr: *mut T, len: usize, free_fn: unsafe extern "C" fn(*mut c_void)) -> Self { + debug_assert!(!ptr.is_null()); + debug_assert!(len > 0); + + Self { + inner: SeaCowInner::Owned { ptr, len, free_fn }, + } + } + + #[inline] + #[cfg(feature = "_internal_c_ffi")] + pub(crate) fn make_owned(&mut self, free_fn: unsafe extern "C" fn(*mut c_void)) { + if let SeaCowInner::Borrowed(slice) = self.inner { + self.inner = SeaCowInner::Owned { ptr: slice.as_ptr().cast_mut(), len: slice.len(), free_fn }; + } + } +} + +impl Clone for SeaCowInner<'_, T> { + #[inline(never)] + fn clone(&self) -> Self { + let slice = match self { + Self::Borrowed(data) => return Self::Borrowed(data), + #[cfg(feature = "_internal_c_ffi")] + Self::Owned { ptr, len, free_fn: _ } => unsafe { slice::from_raw_parts(*ptr, *len) }, + Self::Boxed(data) => &**data, + }; + let mut v = Vec::new(); + v.try_reserve_exact(slice.len()).unwrap(); + v.extend_from_slice(slice); + Self::Boxed(v.into_boxed_slice()) + } +} + +enum SeaCowInner<'a, T> { + #[cfg(feature = "_internal_c_ffi")] + Owned { ptr: *mut T, len: usize, free_fn: unsafe extern "C" fn(*mut c_void) }, + Borrowed(&'a [T]), + Boxed(Box<[T]>), +} + +#[cfg(feature = "_internal_c_ffi")] +impl Drop for SeaCowInner<'_, T> { + fn drop(&mut self) { + if let Self::Owned { ptr, free_fn, .. } = self { + unsafe { + (free_fn)((*ptr).cast()); + } + } + } +} + +impl SeaCow<'_, T> { + #[must_use] + pub fn as_slice(&self) -> &[T] { + match &self.inner { + #[cfg(feature = "_internal_c_ffi")] + SeaCowInner::Owned { ptr, len, .. } => unsafe { slice::from_raw_parts(*ptr, *len) }, + SeaCowInner::Borrowed(a) => a, + SeaCowInner::Boxed(x) => x, + } + } +} + +pub(crate) struct RowBitmap<'a, T> { + rows: &'a [Pointer], + width: usize, +} +unsafe impl Send for RowBitmap<'_, T> {} + +pub(crate) struct RowBitmapMut<'a, T> { + rows: MutCow<'a, [PointerMut]>, + width: usize, +} +unsafe impl Send for RowBitmapMut<'_, T> {} + +impl RowBitmapMut<'_, MaybeUninit> { + #[inline] + pub(crate) unsafe fn assume_init<'maybeowned>(&'maybeowned mut self) -> RowBitmap<'maybeowned, T> { + #[allow(clippy::transmute_ptr_to_ptr)] + RowBitmap { + width: self.width, + rows: mem::transmute::<&'maybeowned [PointerMut>], &'maybeowned [Pointer]>(self.rows.borrow_mut()), + } + } +} + +impl RowBitmap<'_, T> { + pub fn rows(&self) -> impl Iterator { + let width = self.width; + self.rows.iter().map(move |row| { + unsafe { slice::from_raw_parts(row.0, width) } + }) + } +} + +enum MutCow<'a, T: ?Sized> { + Owned(Box), + #[allow(dead_code)] /// This is optional, for FFI only + Borrowed(&'a mut T), +} + +impl MutCow<'_, T> { + #[must_use] + pub fn borrow_mut(&mut self) -> &mut T { + match self { + Self::Owned(a) => a, + Self::Borrowed(a) => a, + } + } +} + +impl<'a, T: Sync + Send + Copy + 'static> RowBitmapMut<'a, T> { + #[inline] + #[must_use] + pub fn new_contiguous(data: &mut [T], width: usize) -> Self { + Self { + rows: MutCow::Owned(data.chunks_exact_mut(width).map(|r| PointerMut(r.as_mut_ptr())).collect()), + width, + } + } + + /// Inner pointers must be valid for `'a` too, and at least `width` large each + #[inline] + #[cfg(feature = "_internal_c_ffi")] + #[must_use] + pub unsafe fn new(rows: &'a mut [*mut T], width: usize) -> Self { + Self { + rows: MutCow::Borrowed(&mut *(rows as *mut [*mut T] as *mut [PointerMut])), + width, + } + } + + pub fn rows_mut(&mut self) -> impl Iterator + Send { + let width = self.width; + self.rows.borrow_mut().iter().map(move |row| { + unsafe { slice::from_raw_parts_mut(row.0, width) } + }) + } + + pub(crate) fn chunks(&mut self, chunk_size: usize) -> impl Iterator> { + self.rows.borrow_mut().chunks_mut(chunk_size).map(|chunk| RowBitmapMut { + width: self.width, + rows: MutCow::Borrowed(chunk), + }) + } + + #[must_use] + pub(crate) fn len(&mut self) -> usize { + self.rows.borrow_mut().len() + } +}