diff --git a/.github/workflows/call-build-deb.yml b/.github/workflows/call-build-deb.yml new file mode 100644 index 0000000..dcbe2ab --- /dev/null +++ b/.github/workflows/call-build-deb.yml @@ -0,0 +1,17 @@ +name: Call build-deb +on: + pull_request_target: + paths-ignore: + - ".github/workflows/**" + types: [ opened, closed, synchronize ] + +concurrency: + group: ${{ github.workflow }}-pull/${{ github.event.number }} + cancel-in-progress: true + +jobs: + check_job: + if: github.event.action != 'closed' || github.event.pull_request.merged + uses: deepin-community/.github/.github/workflows/build-deb.yml@master + secrets: + BridgeToken: ${{ secrets.BridgeToken }} diff --git a/.github/workflows/call-build-tag.yml b/.github/workflows/call-build-tag.yml new file mode 100644 index 0000000..d0a591a --- /dev/null +++ b/.github/workflows/call-build-tag.yml @@ -0,0 +1,14 @@ +name: tag build +on: + push: + tags: "*" + +concurrency: + group: ${{ github.workflow }} + cancel-in-progress: true + +jobs: + build: + uses: deepin-community/.github/.github/workflows/build-tag.yml@master + secrets: + BridgeToken: ${{ secrets.BridgeToken }} diff --git a/.gitignore b/.gitignore index 224e7f0..8551194 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,27 @@ -.pc/ +config.mk +imagequant.pc +*.lo +*.o +*.a +*.so.0 +*.so +*.bz2 +*.dylib +*.dylib.0 +*.jnilib +*.dSYM +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.? diff --git a/.travis.yml b/.travis.yml index c6264f3..1a45e51 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,7 +2,7 @@ matrix: include: - os: linux language: java - jdk: oraclejdk8 + jdk: oraclejdk9 env: CFLAGS="-fPIC" install: true script: make java diff --git a/CHANGELOG b/CHANGELOG index 8eac358..67333a2 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,46 @@ +version 4 +--------- + +- rewritten in Rust +- multi-threaded floyd-steinberg dithering gives 2x-3x remapping speed boost +- better handling of remapping on top of backgrounds (for GIFs, gif.ski) +- support for more than 256-colors (compile-time option) +- WASM compatibility +- the C API enables dithering by default + +Newer v3/v4 versions of the library are on crates.io: https://crates.io/crates/imagequant + +version 2.18 +------------ + - improved handling of images with very few pixels with very diverse colors + - added more perceptually-weighed color selection + - dropped problematic omp_set_nested() + - fixed a rare memory leak + +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() diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..d14f058 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,56 @@ +cmake_minimum_required(VERSION 2.6) +file(READ version.txt VERSION) + +project(imagequant C) + +if(${CMAKE_SYSTEM_PROCESSOR} STREQUAL ARM64) + option(BUILD_WITH_SSE "Use SSE" OFF) +else() + option(BUILD_WITH_SSE "Use SSE" ON) +endif() + +if(BUILD_WITH_SSE) + add_definitions(-DUSE_SSE=1) +endif() + +find_package(OpenMP) +if(OPENMP_FOUND) + set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${OpenMP_C_FLAGS}") + set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${OpenMP_CXX_FLAGS}") +endif() + +include_directories(${CMAKE_SOURCE_DIR}) + +if(CMAKE_COMPILER_IS_GNUCC) + add_compile_options("-std=c99") +endif() + +add_library(imagequant SHARED + libimagequant.c + blur.c + mediancut.c + mempool.c + nearest.c + pam.c + kmeans.c +) + +add_library(imagequant_a STATIC + libimagequant.c + blur.c + mediancut.c + mempool.c + nearest.c + pam.c + kmeans.c +) +set_target_properties(imagequant PROPERTIES SOVERSION 0 + VERSION 0.0) + +set(PREFIX ${CMAKE_INSTALL_PREFIX}) +configure_file(imagequant.pc.in imagequant.pc @ONLY) + +install(TARGETS imagequant LIBRARY DESTINATION ${LIB_INSTALL_DIR}) +install(FILES libimagequant.h DESTINATION include) +install(FILES ${CMAKE_BINARY_DIR}/libimagequant_a.a DESTINATION ${LIB_INSTALL_DIR} RENAME libimagequant.a) +install(FILES ${CMAKE_BINARY_DIR}/imagequant.pc DESTINATION ${LIB_INSTALL_DIR}/pkgconfig) diff --git a/Cargo.toml b/Cargo.toml index d9372b8..789aabb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,29 +1,30 @@ -# libimagequant is a pure C library. -# Rust/Cargo is entirely optional. You can also use ./configure && make +# Please upgrade to v4 of the library [package] +version = "3.1.4+sys2.18.0" authors = ["Kornel Lesiński "] -build = "rust/build.rs" +build = "rust-sys/build.rs" categories = ["external-ffi-bindings"] homepage = "https://pngquant.org/lib" -include = ["COPYRIGHT", "rust/*", "*.c", "*.h", "README.md", "Cargo.toml"] +include = ["COPYRIGHT", "rust-sys/*", "*.c", "*.h", "README.md", "Cargo.toml"] keywords = ["pngquant"] -license = "GPL-3.0+" +license = "GPL-3.0-or-later" links = "imagequant" name = "imagequant-sys" readme = "README.md" repository = "https://github.com/ImageOptim/libimagequant" -version = "2.12.2" 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" +edition = "2018" [build-dependencies] -cc = "1.0.17" +cc = "1.0.71" [dependencies] -rgb = "0.8.9" +rgb = "0.8.29" +bitflags = "1.3.2" [dependencies.openmp-sys] optional = true -version = "0.1.5" +version = "1.2.0" [features] default = ["sse"] @@ -34,5 +35,13 @@ sse = [] [lib] crate-type = ["cdylib", "staticlib", "lib"] name = "imagequant_sys" -path = "rust/libimagequant.rs" +path = "rust-sys/libimagequant.rs" doctest = false + +[dev-dependencies] +lodepng = "3.4.7" +imagequant = { path = "./rust-api" } + +[profile.bench] +debug = 1 +split-debuginfo = 'unpacked' diff --git a/Makefile b/Makefile index 2666b3d..548e2ac 100644 --- a/Makefile +++ b/Makefile @@ -19,7 +19,7 @@ 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 +OBJS = pam.o mediancut.o blur.o remap.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 @@ -65,7 +65,7 @@ libimagequant.dylib: $(SHAREDOBJS) $(OBJS): $(wildcard *.h) config.mk $(JNILIB): $(JAVAHEADERS) $(STATICLIB) org/pngquant/PngQuant.c - $(CC) -g $(CFLAGS) $(LDFLAGS) $(JAVAINCLUDE) -shared -o $@ $(STATICLIB) org/pngquant/PngQuant.c + $(CC) -g $(CFLAGS) $(LDFLAGS) $(JAVAINCLUDE) -shared -o $@ org/pngquant/PngQuant.c $(STATICLIB) $(JNIDLL) $(JNIDLLIMP): $(JAVAHEADERS) $(OBJS) org/pngquant/PngQuant.c $(CC) -fPIC -shared -I. $(JAVAINCLUDE) -o $(JNIDLL) $^ $(LDFLAGS) -Wl,--out-implib,$(JNIDLLIMP),--output-def,$(JNIDLLDEF) @@ -93,7 +93,7 @@ cargo: cargo test example: example.c lodepng.h lodepng.c $(STATICLIB) - $(CC) -g $(CFLAGS) -Wall example.c $(STATICLIB) -o example + $(CC) -g $(CFLAGS) -Wall example.c $(STATICLIB) -o example -lm lodepng.h: curl -o lodepng.h -L https://raw.githubusercontent.com/lvandeve/lodepng/master/lodepng.h @@ -104,6 +104,7 @@ lodepng.c: clean: rm -f $(OBJS) $(SHAREDOBJS) $(SHAREDLIBVER) $(SHAREDLIB) $(STATICLIB) $(TARFILE) $(DLL) '$(DLLIMP)' '$(DLLDEF)' rm -f $(JAVAHEADERS) $(JAVACLASSES) $(JNILIB) example + rm -rf target rust-api/target rust-sys/target distclean: clean rm -f config.mk @@ -114,7 +115,7 @@ install: all $(PKGCONFIG) install -d $(DESTDIR)$(PKGCONFIGDIR) install -d $(DESTDIR)$(INCLUDEDIR) install -m 644 $(STATICLIB) $(DESTDIR)$(LIBDIR)/$(STATICLIB) - install -m 644 $(SHAREDLIBVER) $(DESTDIR)$(LIBDIR)/$(SHAREDLIBVER) + install -m 755 $(SHAREDLIBVER) $(DESTDIR)$(LIBDIR)/$(SHAREDLIBVER) ln -sf $(SHAREDLIBVER) $(DESTDIR)$(LIBDIR)/$(SHAREDLIB) install -m 644 $(PKGCONFIG) $(DESTDIR)$(PKGCONFIGDIR)/$(PKGCONFIG) install -m 644 libimagequant.h $(DESTDIR)$(INCLUDEDIR)/libimagequant.h @@ -133,7 +134,7 @@ ifeq ($(filter %clean %distclean, $(MAKECMDGOALS)), ) endif $(PKGCONFIG): config.mk - sed 's|PREFIX|$(PREFIX)|;s|VERSION|$(VERSION)|' < imagequant.pc.in > $(PKGCONFIG) + sed 's|@PREFIX@|$(PREFIX)|;s|@VERSION@|$(VERSION)|' < imagequant.pc.in > $(PKGCONFIG) .PHONY: all static shared clean dist distclean dll java cargo .DELETE_ON_ERROR: diff --git a/README.md b/README.md index e81606b..dbb8cfe 100644 --- a/README.md +++ b/README.md @@ -7,9 +7,8 @@ It's powering [pngquant2](https://pngquant.org). 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. +* 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 older parts of the code. +* 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. ## Download @@ -50,10 +49,23 @@ 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: +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. Use Visual Studio **2015** and the [MSVC-compatible branch of the library](https://github.com/ImageOptim/libimagequant/tree/msvc). + +To build on Windows, install CMake and use it to generate a makefile/project for your build system. + +Build instructions + + mkdir build + cd build + cmake .. + cmake --build . - * 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. +To generate a 64-bit Visual Studio project instead: + + mkdir build + cd build + cmake -G "Visual Studio 15 2017 Win64" .. + cmake --build . ### Building as shared library @@ -303,7 +315,7 @@ Freeing `liq_result` also frees any `liq_palette` obtained from it. liq_error liq_set_speed(liq_attr* attr, int speed); -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. +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. @@ -326,15 +338,13 @@ 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. +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); -Returns the value set by `liq_set_min_opacity()`. +This function has been deprecated. ---- @@ -667,7 +677,9 @@ The library needs to sort unique colors present in the image. Although the sorti ### OpenMP -The library will parallelize some operations if compiled with OpenMP. +The library can parallelize some operations if compiled with OpenMP. + +GCC 9 or later is required for correct OpenMP support. Older compilers *will cause bugs* when OpenMP is enabled. You must not increase number of maximum threads after `liq_image` has been created, as it allocates some per-thread buffers. diff --git a/benches/bench.rs b/benches/bench.rs new file mode 100644 index 0000000..e75a220 --- /dev/null +++ b/benches/bench.rs @@ -0,0 +1,69 @@ +#![feature(test)] + +extern crate test; +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(&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![std::mem::MaybeUninit::uninit(); img.width * img.height]; + let mut liq = Attributes::new(); + liq.set_speed(10); + 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(0.); + b.iter(move || { + res.remap_into(&mut img, &mut buf).unwrap(); + res.remap_into(&mut img, &mut buf).unwrap(); + }) +} + +#[bench] +fn remap_floyd(b: &mut Bencher) { + let img = lodepng::decode32_file("/Users/kornel/Desktop/canvas.png").unwrap(); + let mut buf = vec![std::mem::MaybeUninit::uninit(); img.width * img.height]; + let mut liq = Attributes::new(); + liq.set_speed(10); + 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.); + 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); + 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); + 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.h b/blur.h index 06ae8cb..1e77819 100644 --- a/blur.h +++ b/blur.h @@ -1,4 +1,8 @@ +#ifndef BLUR_H +#define BLUR_H 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); + +#endif diff --git a/configure b/configure index d16a8a5..d701052 100755 --- a/configure +++ b/configure @@ -160,7 +160,7 @@ if [ -z "$DEBUG" ]; then cflags "-O3 -DNDEBUG" status "Debug" "no" else - cflags "-O1 -g" + cflags "-O1 -g -DDEBUG" status "Debug" "yes" fi @@ -213,8 +213,10 @@ fi # Cocoa if [[ "$OSTYPE" =~ "darwin" ]]; then - cflags "-mmacosx-version-min=10.7" - lflags "-mmacosx-version-min=10.7" + if [ -z "${MACOSX_DEPLOYMENT_TARGET+isset}" ]; then + cflags "-mmacosx-version-min=10.9" + lflags "-mmacosx-version-min=10.9" + fi fi if [[ "$OSTYPE" =~ "darwin" ]]; then @@ -223,8 +225,8 @@ if [[ "$OSTYPE" =~ "darwin" ]]; then # 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") + DIRS+=("$XCODE_PATH/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/include $XCODE_PATH/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/usr/lib") + DIRS+=("$XCODE_PATH/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.9.sdk/usr/include $XCODE_PATH/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.9.sdk/usr/lib") elif [[ "$OSTYPE" =~ "msys" ]]; then SOLIBSUFFIX=dll else diff --git a/debian/.gitignore b/debian/.gitignore new file mode 100644 index 0000000..6cd24e7 --- /dev/null +++ b/debian/.gitignore @@ -0,0 +1,6 @@ +/*.substvars +/.debhelper/ +/debhelper-build-stamp +/files +/libimagequant-dev/ +/libimagequant0/ diff --git a/debian/changelog b/debian/changelog index f9e8fa4..1236cb7 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,44 @@ +libimagequant (2.18.0-1) unstable; urgency=medium + + * New upstream version + * d/watch: + - Make sure to fetch only version 2.x series since 4.x is + rewritten in rust language + - Fix download name + * d/rules: add /usr/share/dpkg/buildtools.mk + Closes: #944439 + * Standards-Version: 4.6.2 (routine-update) + + -- Andreas Tille Tue, 14 Nov 2023 09:37:09 +0100 + +libimagequant (2.17.0-1) unstable; urgency=medium + + [ Barak A. Pearlmutter ] + * Set debhelper-compat version in Build-Depends. + * Set upstream metadata fields: Bug-Database, Bug-Submit, Repository, + Repository-Browse. + * Fix field name typos in debian/copyright (Commenst => Comment, Comments => + Comment). + * Get $(DEB_HOST_MULTIARCH) defined in debian/rules + * Set build-depends-package in debian/*.symbols file + * New upstream version + * Enable openmp (closes: #919757) + * Rules don't require root + * Remove Herbert Parentes Fortes Neto from uploaders (closes: #986943) + - Thanks for your work on Debian, Herbert! + * Merge patch from Helmut Grohne + - Fix FTCBFS: (Closes: #944439) + - Pass a suitable CC to the hand-written configure. + - Force decision for SSE based on HOST arch + + [ Andreas Tille ] + * Add Barak A. Pearlmutter as additional Uploader + * Drop default debian/gbp.conf + * Add salsa-ci file (routine-update) + * Add missing build dependency on dh addon. + + -- Andreas Tille Mon, 10 Jan 2022 16:26:40 +0100 + libimagequant (2.12.2-1.1) unstable; urgency=medium * Non-maintainer upload. diff --git a/debian/compat b/debian/compat deleted file mode 100644 index b4de394..0000000 --- a/debian/compat +++ /dev/null @@ -1 +0,0 @@ -11 diff --git a/debian/control b/debian/control index 28aaf44..52cdab5 100644 --- a/debian/control +++ b/debian/control @@ -1,12 +1,12 @@ Source: libimagequant Maintainer: Debian PhotoTools Maintainers -Uploaders: Herbert Parentes Fortes Neto , - Andreas Tille +Uploaders: Andreas Tille , + Barak A. Pearlmutter Section: graphics Priority: optional -Build-Depends: debhelper (>= 11), - d-shlibs -Standards-Version: 4.2.1 +Build-Depends: debhelper-compat (= 13), d-shlibs, debhelper +Standards-Version: 4.6.2 +Rules-Requires-Root: no Vcs-Browser: https://salsa.debian.org/debian-phototools-team/libimagequant Vcs-Git: https://salsa.debian.org/debian-phototools-team/libimagequant.git Homepage: https://github.com/ImageOptim/libimagequant diff --git a/debian/copyright b/debian/copyright index 4880e8c..5190aab 100644 --- a/debian/copyright +++ b/debian/copyright @@ -9,13 +9,13 @@ License: GPL-3.0+ Files: pam.h Copyright: 1989, 1991 Jef Poskanzer 1997 Greg Roelofs -Commenst: MIT Old Style License: MIT +Comment: MIT Old Style Files: example.c Copyright: ? -Comments: CC0 public domain License: CC0 +Comment: CC0 public domain Files: debian/* Copyright: 2018 Herbert Parentes Fortes Neto @@ -48,4 +48,3 @@ License: GPL-3.0+ . On Debian systems, the complete text of the GNU General Public License version 3 can be found in "/usr/share/common-licenses/GPL-3". - diff --git a/debian/libimagequant0.symbols b/debian/libimagequant0.symbols index 3cced79..31d603c 100644 --- a/debian/libimagequant0.symbols +++ b/debian/libimagequant0.symbols @@ -1,4 +1,5 @@ libimagequant.so.0 libimagequant0 #MINVER# +* Build-Depends-Package: libimagequant-dev liq_attr_copy@Base 2.11.10 liq_attr_create@Base 2.11.10 liq_attr_create_with_allocator@Base 2.11.10 diff --git a/debian/rules b/debian/rules index 88041d2..c92924a 100755 --- a/debian/rules +++ b/debian/rules @@ -2,20 +2,22 @@ #export DH_VERBOSE = 1 +include /usr/share/dpkg/default.mk + export DEB_BUILD_MAINT_OPTIONS = hardening=+all export DEB_CFLAGS_MAINT_APPEND=-D_FORTIFY_SOURCE=2 +include /usr/share/dpkg/buildtools.mk %: dh $@ -ifeq (,$(filter $(DEB_HOST_ARCH), amd64 x32)) override_dh_auto_configure: - dh_auto_configure -- --disable-sse -endif + dh_auto_configure -- \ + --with-openmp \ + 'CC=$(CC)' --$(if $(filter $(DEB_HOST_ARCH),amd64 x32),en,dis)able-sse -override_dh_install: - dh_install +execute_after_dh_install: d-shlibmove --commit \ --multiarch \ --devunversioned \ diff --git a/debian/salsa-ci.yml b/debian/salsa-ci.yml new file mode 100644 index 0000000..33c3a64 --- /dev/null +++ b/debian/salsa-ci.yml @@ -0,0 +1,4 @@ +--- +include: + - https://salsa.debian.org/salsa-ci-team/pipeline/raw/master/salsa-ci.yml + - https://salsa.debian.org/salsa-ci-team/pipeline/raw/master/pipeline-jobs.yml diff --git a/debian/upstream/metadata b/debian/upstream/metadata new file mode 100644 index 0000000..6e9fcba --- /dev/null +++ b/debian/upstream/metadata @@ -0,0 +1,5 @@ +--- +Bug-Database: https://github.com/ImageOptim/libimagequant/issues +Bug-Submit: https://github.com/ImageOptim/libimagequant/issues/new +Repository: https://github.com/ImageOptim/libimagequant.git +Repository-Browse: https://github.com/ImageOptim/libimagequant diff --git a/debian/watch b/debian/watch index 033c983..63e3dbb 100644 --- a/debian/watch +++ b/debian/watch @@ -1,3 +1,4 @@ version=4 -https://github.com/ImageOptim/libimagequant/releases .*/archive/@ANY_VERSION@@ARCHIVE_EXT@ +opts="filenamemangle=s%(?:.*?)?v?(\d[\d.]*)\.tar\.gz%@PACKAGE@-$1.tar.gz%" \ + https://github.com/ImageOptim/@PACKAGE@/tags .*/v?(2[\d.]+)@ARCHIVE_EXT@ diff --git a/imagequant.pc.in b/imagequant.pc.in index 980da8c..16935b7 100644 --- a/imagequant.pc.in +++ b/imagequant.pc.in @@ -1,10 +1,10 @@ -prefix=PREFIX +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 +Version: @VERSION@ Libs: -L${libdir} -limagequant Cflags: -I${includedir} diff --git a/kmeans.c b/kmeans.c index 7ee273d..6d1a122 100644 --- a/kmeans.c +++ b/kmeans.c @@ -51,21 +51,29 @@ LIQ_PRIVATE void kmeans_finalize(colormap *map, const unsigned int max_threads, 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, - }; + if (!map->palette[i].fixed) { map->palette[i].popularity = total; + if (total) { + map->palette[i].acolor = (f_pixel){ + .a = a / total, + .r = r / total, + .g = g / total, + .b = b / total, + }; + } else { + // if a color is useless, make a new one + // (it was supposed to be random, but Android NDK has problematic stdlib headers) + map->palette[i].acolor.a = map->palette[(i+1)%map->colors].acolor.a; + map->palette[i].acolor.r = map->palette[(i+2)%map->colors].acolor.r; + map->palette[i].acolor.g = map->palette[(i+3)%map->colors].acolor.g; + map->palette[i].acolor.b = map->palette[(i+4)%map->colors].acolor.b; + } } } } -LIQ_PRIVATE double kmeans_do_iteration(histogram *hist, colormap *const map, kmeans_callback callback) +LIQ_PRIVATE double kmeans_do_iteration(histogram *hist, colormap *const map, kmeans_callback callback, unsigned int max_threads) { - const unsigned int max_threads = omp_get_max_threads(); LIQ_ARRAY(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); @@ -73,17 +81,35 @@ LIQ_PRIVATE double kmeans_do_iteration(histogram *hist, colormap *const map, kme const int hist_size = hist->size; double total_diff=0; +#if __GNUC__ >= 9 || __clang__ + #pragma omp parallel for if (hist_size > 2000) \ + schedule(static) default(none) shared(achv,average_color,callback,hist_size,map,n) reduction(+:total_diff) +#else #pragma omp parallel for if (hist_size > 2000) \ schedule(static) default(none) shared(average_color,callback) reduction(+:total_diff) +#endif 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); + const f_pixel px = achv[j].acolor; + const unsigned int match = nearest_search(n, &px, 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) { + // Check how average diff would look like if there was dithering + const f_pixel remapped = map->palette[match].acolor; + nearest_search(n, &(f_pixel){ + .a = px.a + px.a - remapped.a, + .r = px.r + px.r - remapped.r, + .g = px.g + px.g - remapped.g, + .b = px.b + px.b - remapped.b, + }, match, &diff); + + callback(&achv[j], diff); + } + + total_diff += diff * achv[j].perceptual_weight; - if (callback) callback(&achv[j], diff); + kmeans_update_color(px, achv[j].adjusted_weight, map, match, omp_get_thread_num(), average_color); } nearest_free(n); diff --git a/kmeans.h b/kmeans.h index c51d7bb..3216ed3 100644 --- a/kmeans.h +++ b/kmeans.h @@ -14,6 +14,6 @@ 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); +LIQ_PRIVATE double kmeans_do_iteration(histogram *hist, colormap *const map, kmeans_callback callback, const unsigned int max_threads); #endif diff --git a/libimagequant-ios.xcodeproj/project.pbxproj b/libimagequant-ios.xcodeproj/project.pbxproj new file mode 100644 index 0000000..6733c0f --- /dev/null +++ b/libimagequant-ios.xcodeproj/project.pbxproj @@ -0,0 +1,232 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + 5F65D41723A04D7B003E3A5B /* nearest.c in Sources */ = {isa = PBXBuildFile; fileRef = 5F65D41023A04D7B003E3A5B /* nearest.c */; }; + 5F65D41823A04D7B003E3A5B /* pam.c in Sources */ = {isa = PBXBuildFile; fileRef = 5F65D41123A04D7B003E3A5B /* pam.c */; }; + 5F65D41923A04D7B003E3A5B /* mempool.c in Sources */ = {isa = PBXBuildFile; fileRef = 5F65D41223A04D7B003E3A5B /* mempool.c */; }; + 5F65D41A23A04D7B003E3A5B /* mediancut.c in Sources */ = {isa = PBXBuildFile; fileRef = 5F65D41323A04D7B003E3A5B /* mediancut.c */; }; + 5F65D41B23A04D7B003E3A5B /* kmeans.c in Sources */ = {isa = PBXBuildFile; fileRef = 5F65D41423A04D7B003E3A5B /* kmeans.c */; }; + 5F65D41C23A04D7B003E3A5B /* libimagequant.c in Sources */ = {isa = PBXBuildFile; fileRef = 5F65D41523A04D7B003E3A5B /* libimagequant.c */; }; + 5F65D41D23A04D7B003E3A5B /* blur.c in Sources */ = {isa = PBXBuildFile; fileRef = 5F65D41623A04D7B003E3A5B /* blur.c */; }; +/* End PBXBuildFile section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 5FB37C7723A04C7300942532 /* CopyFiles */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = "include/$(PRODUCT_NAME)"; + dstSubfolderSpec = 16; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 5F65D41023A04D7B003E3A5B /* nearest.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = nearest.c; sourceTree = SOURCE_ROOT; }; + 5F65D41123A04D7B003E3A5B /* pam.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = pam.c; sourceTree = SOURCE_ROOT; }; + 5F65D41223A04D7B003E3A5B /* mempool.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = mempool.c; sourceTree = SOURCE_ROOT; }; + 5F65D41323A04D7B003E3A5B /* mediancut.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = mediancut.c; sourceTree = SOURCE_ROOT; }; + 5F65D41423A04D7B003E3A5B /* kmeans.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = kmeans.c; sourceTree = SOURCE_ROOT; }; + 5F65D41523A04D7B003E3A5B /* libimagequant.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = libimagequant.c; sourceTree = SOURCE_ROOT; }; + 5F65D41623A04D7B003E3A5B /* blur.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = blur.c; sourceTree = SOURCE_ROOT; }; + 5F65D41E23A04D93003E3A5B /* libimagequant.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = libimagequant.h; sourceTree = SOURCE_ROOT; }; + 5FB37C7923A04C7300942532 /* libimagequant.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libimagequant.a; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 5FB37C7623A04C7300942532 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 5FB37C7023A04C7300942532 = { + isa = PBXGroup; + children = ( + 5FB37C7B23A04C7300942532 /* imagequant */, + 5FB37C7A23A04C7300942532 /* Products */, + ); + sourceTree = ""; + }; + 5FB37C7A23A04C7300942532 /* Products */ = { + isa = PBXGroup; + children = ( + 5FB37C7923A04C7300942532 /* libimagequant.a */, + ); + name = Products; + sourceTree = ""; + }; + 5FB37C7B23A04C7300942532 /* imagequant */ = { + isa = PBXGroup; + children = ( + 5F65D41E23A04D93003E3A5B /* libimagequant.h */, + 5F65D41623A04D7B003E3A5B /* blur.c */, + 5F65D41423A04D7B003E3A5B /* kmeans.c */, + 5F65D41523A04D7B003E3A5B /* libimagequant.c */, + 5F65D41323A04D7B003E3A5B /* mediancut.c */, + 5F65D41223A04D7B003E3A5B /* mempool.c */, + 5F65D41023A04D7B003E3A5B /* nearest.c */, + 5F65D41123A04D7B003E3A5B /* pam.c */, + ); + path = imagequant; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 5FB37C7823A04C7300942532 /* imagequant */ = { + isa = PBXNativeTarget; + buildConfigurationList = 5FB37C8223A04C7300942532 /* Build configuration list for PBXNativeTarget "imagequant" */; + buildPhases = ( + 5FB37C7523A04C7300942532 /* Sources */, + 5FB37C7623A04C7300942532 /* Frameworks */, + 5FB37C7723A04C7300942532 /* CopyFiles */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = imagequant; + productName = imagequant; + productReference = 5FB37C7923A04C7300942532 /* libimagequant.a */; + productType = "com.apple.product-type.library.static"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 5FB37C7123A04C7300942532 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1120; + ORGANIZATIONNAME = "ImageOptim Ltd."; + TargetAttributes = { + 5FB37C7823A04C7300942532 = { + CreatedOnToolsVersion = 11.2; + }; + }; + }; + buildConfigurationList = 5FB37C7423A04C7300942532 /* Build configuration list for PBXProject "libimagequant-ios" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 5FB37C7023A04C7300942532; + productRefGroup = 5FB37C7A23A04C7300942532 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 5FB37C7823A04C7300942532 /* imagequant */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXSourcesBuildPhase section */ + 5FB37C7523A04C7300942532 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 5F65D41B23A04D7B003E3A5B /* kmeans.c in Sources */, + 5F65D41923A04D7B003E3A5B /* mempool.c in Sources */, + 5F65D41823A04D7B003E3A5B /* pam.c in Sources */, + 5F65D41C23A04D7B003E3A5B /* libimagequant.c in Sources */, + 5F65D41A23A04D7B003E3A5B /* mediancut.c in Sources */, + 5F65D41723A04D7B003E3A5B /* nearest.c in Sources */, + 5F65D41D23A04D7B003E3A5B /* blur.c in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 5FB37C8023A04C7300942532 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + }; + name = Debug; + }; + 5FB37C8123A04C7300942532 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_PREPROCESSOR_DEFINITIONS = "NDEBUG=1"; + IPHONEOS_DEPLOYMENT_TARGET = 11.0; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 5FB37C8323A04C7300942532 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 5FB37C8423A04C7300942532 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 5FB37C7423A04C7300942532 /* Build configuration list for PBXProject "libimagequant-ios" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 5FB37C8023A04C7300942532 /* Debug */, + 5FB37C8123A04C7300942532 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 5FB37C8223A04C7300942532 /* Build configuration list for PBXNativeTarget "imagequant" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 5FB37C8323A04C7300942532 /* Debug */, + 5FB37C8423A04C7300942532 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 5FB37C7123A04C7300942532 /* Project object */; +} diff --git a/libimagequant-mac.xcodeproj/project.pbxproj b/libimagequant-mac.xcodeproj/project.pbxproj new file mode 100644 index 0000000..1526116 --- /dev/null +++ b/libimagequant-mac.xcodeproj/project.pbxproj @@ -0,0 +1,223 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + 5FF4F4CD23A04EF500A3FFB5 /* libimagequant.h in Headers */ = {isa = PBXBuildFile; fileRef = 5FF4F4C523A04EF500A3FFB5 /* libimagequant.h */; }; + 5FF4F4CE23A04EF500A3FFB5 /* libimagequant.c in Sources */ = {isa = PBXBuildFile; fileRef = 5FF4F4C623A04EF500A3FFB5 /* libimagequant.c */; }; + 5FF4F4CF23A04EF500A3FFB5 /* kmeans.c in Sources */ = {isa = PBXBuildFile; fileRef = 5FF4F4C723A04EF500A3FFB5 /* kmeans.c */; }; + 5FF4F4D023A04EF500A3FFB5 /* blur.c in Sources */ = {isa = PBXBuildFile; fileRef = 5FF4F4C823A04EF500A3FFB5 /* blur.c */; }; + 5FF4F4D123A04EF500A3FFB5 /* mempool.c in Sources */ = {isa = PBXBuildFile; fileRef = 5FF4F4C923A04EF500A3FFB5 /* mempool.c */; }; + 5FF4F4D223A04EF500A3FFB5 /* nearest.c in Sources */ = {isa = PBXBuildFile; fileRef = 5FF4F4CA23A04EF500A3FFB5 /* nearest.c */; }; + 5FF4F4D323A04EF500A3FFB5 /* pam.c in Sources */ = {isa = PBXBuildFile; fileRef = 5FF4F4CB23A04EF500A3FFB5 /* pam.c */; }; + 5FF4F4D423A04EF500A3FFB5 /* mediancut.c in Sources */ = {isa = PBXBuildFile; fileRef = 5FF4F4CC23A04EF500A3FFB5 /* mediancut.c */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 5FF4F4BE23A04ED500A3FFB5 /* libimagequant.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libimagequant.a; sourceTree = BUILT_PRODUCTS_DIR; }; + 5FF4F4C523A04EF500A3FFB5 /* libimagequant.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = libimagequant.h; path = ../../www/pngquant/lib/libimagequant.h; sourceTree = ""; }; + 5FF4F4C623A04EF500A3FFB5 /* libimagequant.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = libimagequant.c; path = ../../www/pngquant/lib/libimagequant.c; sourceTree = ""; }; + 5FF4F4C723A04EF500A3FFB5 /* kmeans.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = kmeans.c; path = ../../www/pngquant/lib/kmeans.c; sourceTree = ""; }; + 5FF4F4C823A04EF500A3FFB5 /* blur.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = blur.c; path = ../../www/pngquant/lib/blur.c; sourceTree = ""; }; + 5FF4F4C923A04EF500A3FFB5 /* mempool.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = mempool.c; path = ../../www/pngquant/lib/mempool.c; sourceTree = ""; }; + 5FF4F4CA23A04EF500A3FFB5 /* nearest.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = nearest.c; path = ../../www/pngquant/lib/nearest.c; sourceTree = ""; }; + 5FF4F4CB23A04EF500A3FFB5 /* pam.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = pam.c; path = ../../www/pngquant/lib/pam.c; sourceTree = ""; }; + 5FF4F4CC23A04EF500A3FFB5 /* mediancut.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = mediancut.c; path = ../../www/pngquant/lib/mediancut.c; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 5FF4F4BC23A04ED500A3FFB5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 5FF4F4B523A04ED500A3FFB5 = { + isa = PBXGroup; + children = ( + 5FF4F4C823A04EF500A3FFB5 /* blur.c */, + 5FF4F4C723A04EF500A3FFB5 /* kmeans.c */, + 5FF4F4C623A04EF500A3FFB5 /* libimagequant.c */, + 5FF4F4C523A04EF500A3FFB5 /* libimagequant.h */, + 5FF4F4CC23A04EF500A3FFB5 /* mediancut.c */, + 5FF4F4C923A04EF500A3FFB5 /* mempool.c */, + 5FF4F4CA23A04EF500A3FFB5 /* nearest.c */, + 5FF4F4CB23A04EF500A3FFB5 /* pam.c */, + 5FF4F4BF23A04ED500A3FFB5 /* Products */, + ); + sourceTree = ""; + }; + 5FF4F4BF23A04ED500A3FFB5 /* Products */ = { + isa = PBXGroup; + children = ( + 5FF4F4BE23A04ED500A3FFB5 /* libimagequant.a */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + 5FF4F4BA23A04ED500A3FFB5 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 5FF4F4CD23A04EF500A3FFB5 /* libimagequant.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + 5FF4F4BD23A04ED500A3FFB5 /* imagequant */ = { + isa = PBXNativeTarget; + buildConfigurationList = 5FF4F4C223A04ED500A3FFB5 /* Build configuration list for PBXNativeTarget "imagequant" */; + buildPhases = ( + 5FF4F4BA23A04ED500A3FFB5 /* Headers */, + 5FF4F4BB23A04ED500A3FFB5 /* Sources */, + 5FF4F4BC23A04ED500A3FFB5 /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = imagequant; + productName = imagequant; + productReference = 5FF4F4BE23A04ED500A3FFB5 /* libimagequant.a */; + productType = "com.apple.product-type.library.static"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 5FF4F4B623A04ED500A3FFB5 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 1120; + ORGANIZATIONNAME = "ImageOptim Ltd."; + TargetAttributes = { + 5FF4F4BD23A04ED500A3FFB5 = { + CreatedOnToolsVersion = 11.2; + }; + }; + }; + buildConfigurationList = 5FF4F4B923A04ED500A3FFB5 /* Build configuration list for PBXProject "imagequant" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 5FF4F4B523A04ED500A3FFB5; + productRefGroup = 5FF4F4BF23A04ED500A3FFB5 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 5FF4F4BD23A04ED500A3FFB5 /* imagequant */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXSourcesBuildPhase section */ + 5FF4F4BB23A04ED500A3FFB5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 5FF4F4D323A04EF500A3FFB5 /* pam.c in Sources */, + 5FF4F4D023A04EF500A3FFB5 /* blur.c in Sources */, + 5FF4F4D223A04EF500A3FFB5 /* nearest.c in Sources */, + 5FF4F4CF23A04EF500A3FFB5 /* kmeans.c in Sources */, + 5FF4F4CE23A04EF500A3FFB5 /* libimagequant.c in Sources */, + 5FF4F4D423A04EF500A3FFB5 /* mediancut.c in Sources */, + 5FF4F4D123A04EF500A3FFB5 /* mempool.c in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 5FF4F4C023A04ED500A3FFB5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + MACOSX_DEPLOYMENT_TARGET = 10.9; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + }; + name = Debug; + }; + 5FF4F4C123A04ED500A3FFB5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_PREPROCESSOR_DEFINITIONS = "NDEBUG=1"; + MACOSX_DEPLOYMENT_TARGET = 10.9; + SDKROOT = macosx; + }; + name = Release; + }; + 5FF4F4C323A04ED500A3FFB5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + EXECUTABLE_PREFIX = lib; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + }; + name = Debug; + }; + 5FF4F4C423A04ED500A3FFB5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + EXECUTABLE_PREFIX = lib; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 5FF4F4B923A04ED500A3FFB5 /* Build configuration list for PBXProject "imagequant" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 5FF4F4C023A04ED500A3FFB5 /* Debug */, + 5FF4F4C123A04ED500A3FFB5 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 5FF4F4C223A04ED500A3FFB5 /* Build configuration list for PBXNativeTarget "imagequant" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 5FF4F4C323A04ED500A3FFB5 /* Debug */, + 5FF4F4C423A04ED500A3FFB5 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 5FF4F4B623A04ED500A3FFB5 /* Project object */; +} diff --git a/libimagequant.c b/libimagequant.c index 3506564..632c242 100644 --- a/libimagequant.c +++ b/libimagequant.c @@ -19,22 +19,14 @@ #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 -#define LIQ_TEMP_ROW_WIDTH(img_width) (((img_width) | 15) + 1) /* keep alignment & leave space between rows to avoid cache line contention */ -#else -#define LIQ_TEMP_ROW_WIDTH(img_width) (img_width) -#define omp_get_max_threads() 1 -#define omp_get_thread_num() 0 -#endif - #include "libimagequant.h" #include "pam.h" +#include "libimagequant_private.h" #include "mediancut.h" -#include "nearest.h" #include "blur.h" #include "kmeans.h" +#include "remap.h" #define LIQ_HIGH_MEMORY_LIMIT (1<<26) /* avoid allocating buffers larger than 64MB */ @@ -54,7 +46,6 @@ struct liq_attr { 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; @@ -72,44 +63,6 @@ struct liq_attr { 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 *importance_map, *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; - liq_image *background; - 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; - unsigned char use_dither_map; - unsigned char progress_stage1; -} liq_remapping_result; - struct liq_result { const char *magic_header; void* (*malloc)(size_t); @@ -140,12 +93,9 @@ struct liq_histogram { 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 bool liq_image_get_row_f_init(liq_image *img) LIQ_NONNULL; -static const f_pixel *liq_image_get_row_f(liq_image *input_image, unsigned int row) LIQ_NONNULL; +static const liq_color *liq_image_get_row_rgba(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; @@ -186,7 +136,7 @@ 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) +LIQ_PRIVATE LIQ_NONNULL 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); } @@ -258,7 +208,8 @@ static double quality_to_mse(long quality) // 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; + // LIQ_WEIGHT_MSE is a fudge factor - reminder that colors are not in 0..1 range any more + return LIQ_WEIGHT_MSE * (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) @@ -274,7 +225,7 @@ static unsigned int mse_to_quality(double mse) /** 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; + return (mse * 65536.0/6.0) / LIQ_WEIGHT_MSE; } LIQ_EXPORT LIQ_NONNULL liq_error liq_set_quality(liq_attr* attr, int minimum, int target) @@ -384,18 +335,12 @@ LIQ_EXPORT LIQ_NONNULL liq_error liq_set_output_gamma(liq_result* res, double ga 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.f, 256.f * attr->min_opaque_val); + return 0; } LIQ_EXPORT LIQ_NONNULL void liq_set_last_index_transparent(liq_attr* attr, int is_last) @@ -510,7 +455,6 @@ LIQ_EXPORT liq_attr* liq_attr_create_with_allocator(void* (*custom_malloc)(size_ .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, @@ -526,7 +470,7 @@ LIQ_EXPORT LIQ_NONNULL liq_error liq_image_add_fixed_color(liq_image *img, liq_c 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){ + img->fixed_colors[img->fixed_colors_count++] = rgba_to_f(gamma_lut, (liq_color){ .r = color.r, .g = color.g, .b = color.b, @@ -549,7 +493,7 @@ LIQ_EXPORT LIQ_NONNULL liq_error liq_histogram_add_fixed_color(liq_histogram *hi float gamma_lut[256]; to_f_set_gamma(gamma_lut, gamma ? gamma : 0.45455); - const f_pixel px = rgba_to_f(gamma_lut, (rgba_pixel){ + const f_pixel px = rgba_to_f(gamma_lut, (liq_color){ .r = color.r, .g = color.g, .b = color.b, @@ -560,16 +504,19 @@ LIQ_EXPORT LIQ_NONNULL liq_error liq_histogram_add_fixed_color(liq_histogram *hi LIQ_NONNULL static bool liq_image_use_low_memory(liq_image *img) { + if (img->temp_f_row) { + img->free(img->temp_f_row); + } img->temp_f_row = img->malloc(sizeof(img->f_pixels[0]) * LIQ_TEMP_ROW_WIDTH(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 + return (size_t)img->width * (size_t)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) +static liq_image *liq_image_create_internal(const liq_attr *attr, liq_color* 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)"); @@ -592,10 +539,9 @@ static liq_image *liq_image_create_internal(const liq_attr *attr, rgba_pixel* ro .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) { + if (!rows) { img->temp_row = attr->malloc(sizeof(img->temp_row[0]) * LIQ_TEMP_ROW_WIDTH(width) * omp_get_max_threads()); if (!img->temp_row) return NULL; } @@ -606,10 +552,6 @@ static liq_image *liq_image_create_internal(const liq_attr *attr, rgba_pixel* ro 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; } @@ -641,13 +583,14 @@ LIQ_EXPORT LIQ_NONNULL liq_error liq_image_set_memory_ownership(liq_image *img, } LIQ_NONNULL static void liq_image_free_maps(liq_image *input_image); +LIQ_NONNULL static void liq_image_free_dither_map(liq_image *input_image); LIQ_NONNULL static void liq_image_free_importance_map(liq_image *input_image); LIQ_EXPORT LIQ_NONNULL liq_error liq_image_set_importance_map(liq_image *img, unsigned char importance_map[], size_t buffer_size, enum liq_ownership ownership) { if (!CHECK_STRUCT_TYPE(img, liq_image)) return LIQ_INVALID_POINTER; if (!CHECK_USER_POINTER(importance_map)) return LIQ_INVALID_POINTER; - const size_t required_size = img->width * img->height; + const size_t required_size = (size_t)img->width * (size_t)img->height; if (buffer_size < required_size) { return LIQ_BUFFER_TOO_SMALL; } @@ -686,7 +629,7 @@ LIQ_EXPORT LIQ_NONNULL liq_error liq_image_set_background(liq_image *img, liq_im } img->background = background; - liq_image_free_maps(img); // Force them to be re-analyzed with the background + liq_image_free_dither_map(img); // Force it to be re-analyzed with the background return LIQ_OK; } @@ -702,7 +645,7 @@ LIQ_NONNULL static bool check_image_size(const liq_attr *attr, const int width, return false; } - if (width > INT_MAX/sizeof(rgba_pixel)/height || width > INT_MAX/16/sizeof(f_pixel) || height > INT_MAX/sizeof(size_t)) { + if (width > INT_MAX/sizeof(liq_color)/height || width > INT_MAX/16/sizeof(f_pixel) || height > INT_MAX/sizeof(size_t)) { liq_log_error(attr, "image too large"); return false; } @@ -729,7 +672,7 @@ LIQ_EXPORT liq_image *liq_image_create_rgba_rows(const liq_attr *attr, void *con return NULL; } } - return liq_image_create_internal(attr, (rgba_pixel**)rows, NULL, NULL, width, height, gamma); + return liq_image_create_internal(attr, (liq_color**)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) @@ -742,8 +685,8 @@ LIQ_EXPORT LIQ_NONNULL liq_image *liq_image_create_rgba(const liq_attr *attr, co return NULL; } - rgba_pixel *const pixels = (rgba_pixel *const)bitmap; - rgba_pixel **rows = attr->malloc(sizeof(rows[0])*height); + liq_color *const pixels = (liq_color *const)bitmap; + liq_color **rows = attr->malloc(sizeof(rows[0])*height); if (!rows) return NULL; for(int i=0; i < height; i++) { @@ -779,26 +722,23 @@ LIQ_NONNULL inline static bool liq_image_has_rgba_pixels(const liq_image *img) 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); + return img->rows; } -LIQ_NONNULL static const rgba_pixel *liq_image_get_row_rgba(liq_image *img, unsigned int row) +LIQ_NONNULL static const liq_color *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 + LIQ_TEMP_ROW_WIDTH(img->width) * omp_get_thread_num(); + liq_color *temp_row = img->temp_row + LIQ_TEMP_ROW_WIDTH(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; } @@ -807,14 +747,14 @@ LIQ_NONNULL static void convert_row_to_f(liq_image *img, f_pixel *row_f_pixels, 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); + const liq_color *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 bool liq_image_get_row_f_init(liq_image *img) +LIQ_PRIVATE LIQ_NONNULL bool liq_image_get_row_f_init(liq_image *img) { assert(omp_get_thread_num() == 0); if (img->f_pixels) { @@ -839,7 +779,7 @@ LIQ_NONNULL static bool liq_image_get_row_f_init(liq_image *img) return true; } -LIQ_NONNULL static const f_pixel *liq_image_get_row_f(liq_image *img, unsigned int row) +LIQ_PRIVATE LIQ_NONNULL const f_pixel *liq_image_get_row_f(liq_image *img, unsigned int row) { if (!img->f_pixels) { assert(img->temp_f_row); // init should have done that @@ -866,7 +806,16 @@ LIQ_EXPORT LIQ_NONNULL int liq_image_get_height(const liq_image *input_image) typedef void free_func(void*); -LIQ_NONNULL static free_func *get_default_free_func(liq_image *img) +LIQ_NONNULL static free_func *get_default_image_free_func(liq_image *img) +{ + // When default allocator is used then user-supplied pointers must be freed with free() + if (img->free != liq_aligned_free) { + return img->free; + } + return free; +} + +LIQ_NONNULL static free_func *get_default_rows_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) { @@ -878,12 +827,12 @@ LIQ_NONNULL static free_func *get_default_free_func(liq_image *img) 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); + get_default_image_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); + get_default_rows_free_func(input_image)(input_image->rows); input_image->rows = NULL; } } @@ -902,7 +851,10 @@ LIQ_NONNULL static void liq_image_free_maps(liq_image *input_image) { input_image->free(input_image->edges); input_image->edges = NULL; } + liq_image_free_dither_map(input_image); +} +LIQ_NONNULL static void liq_image_free_dither_map(liq_image *input_image) { if (input_image->dither_map) { input_image->free(input_image->dither_map); input_image->dither_map = NULL; @@ -986,6 +938,7 @@ LIQ_EXPORT LIQ_NONNULL liq_error liq_image_quantize(liq_image *const img, liq_at } liq_error err = liq_histogram_add_image(hist, attr, img); if (LIQ_OK != err) { + liq_histogram_destroy(hist); return err; } @@ -1030,7 +983,7 @@ LIQ_EXPORT LIQ_NONNULL liq_error liq_set_dithering_level(liq_result *res, float res->remapping = NULL; } - if (res->dither_level < 0 || res->dither_level > 1.0f) return LIQ_VALUE_OUT_OF_RANGE; + if (dither_level < 0 || dither_level > 1.0f) return LIQ_VALUE_OUT_OF_RANGE; res->dither_level = dither_level; return LIQ_OK; } @@ -1162,7 +1115,7 @@ LIQ_NONNULL static void sort_palette(colormap *map, const liq_attr *options) */ if (options->last_index_transparent) { for(unsigned int i=0; i < map->colors; i++) { - if (map->palette[i].acolor.a < 1.f/256.f) { + if (map->palette[i].acolor.a < MIN_OPAQUE_A) { const unsigned int old = i, transparent_dest = map->colors-1; SWAP_PALETTE(map, transparent_dest, old); @@ -1185,7 +1138,7 @@ LIQ_NONNULL static void sort_palette(colormap *map, const liq_attr *options) /* 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.f/256.f) { + if (map->palette[i].acolor.a < 255.f/256.f * LIQ_WEIGHT_A) { // current transparent color is swapped with earlier opaque one if (i != num_transparent) { SWAP_PALETTE(map, num_transparent, i); @@ -1222,7 +1175,7 @@ LIQ_NONNULL static void set_rounded_palette(liq_palette *const dest, colormap *c dest->count = map->colors; for(unsigned int x = 0; x < map->colors; ++x) { - rgba_pixel px = f_to_rgb(gamma, map->palette[x].acolor); + liq_color px = f_to_rgb(gamma, map->palette[x].acolor); px.r = posterize_channel(px.r, posterize); px.g = posterize_channel(px.g, posterize); @@ -1253,268 +1206,6 @@ LIQ_EXPORT LIQ_NONNULL const liq_palette *liq_get_palette(liq_result *result) 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_init(input_image)) { - return -1; - } - if (input_image->background && !liq_image_get_row_f_init(input_image->background)) { - return -1; - } - - const colormap_item *acolormap = map->palette; - - struct nearest_map *const n = nearest_init(map); - const int transparent_index = input_image->background ? nearest_search(n, &(f_pixel){0,0,0,0}, 0, NULL) : 0; - - - const unsigned int max_threads = omp_get_max_threads(); - LIQ_ARRAY(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(acolormap) 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); - const f_pixel *const bg_pixels = input_image->background && acolormap[transparent_index].acolor.a < 1.f/256.f ? liq_image_get_row_f(input_image->background, row) : NULL; - - unsigned int last_match=0; - for(unsigned int col = 0; col < cols; ++col) { - float diff; - last_match = nearest_search(n, &row_pixels[col], last_match, &diff); - if (bg_pixels && colordifference(bg_pixels[col], acolormap[last_match].acolor) <= diff) { - last_match = transparent_index; - } - output_pixels[row][col] = last_match; - - 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.f) { a = 1.f; } - 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.8f; - } 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; - - if (!liq_image_get_row_f_init(input_image)) { - return false; - } - if (input_image->background && !liq_image_get_row_f_init(input_image->background)) { - return false; - } - - /* 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); - const int transparent_index = input_image->background ? nearest_search(n, &(f_pixel){0,0,0,0}, 0, NULL) : 0; - - // 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.f - (1.f-base_dithering_level)*(1.f-base_dithering_level); - - if (dither_map) { - base_dithering_level *= 1.f/255.f; // convert byte to float - } - base_dithering_level *= 15.f/16.f; // 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); - const f_pixel *const bg_pixels = input_image->background && acolormap[transparent_index].acolor.a < 1.f/256.f ? liq_image_get_row_f(input_image->background, row) : NULL; - - 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; - float diff; - last_match = nearest_search(n, &spx, guessed_match, &diff); - f_pixel output_px = acolormap[last_match].acolor; - if (bg_pixels && colordifference(bg_pixels[col], output_px) <= diff) { - output_px = bg_pixels[col]; - output_pixels[row][col] = transparent_index; - } else { - output_pixels[row][col] = last_match; - } - - 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.75f; - err.g *= 0.75f; - err.b *= 0.75f; - err.a *= 0.75f; - } - - /* 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.f, 2.f/256.f/256.f); - 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; @@ -1545,7 +1236,7 @@ LIQ_EXPORT LIQ_NONNULL liq_error liq_histogram_add_colors(liq_histogram *input_h const unsigned int hash_size = input_hist->acht->hash_size; for(int i=0; i < num_entries; i++) { - const rgba_pixel rgba = { + const liq_color rgba = { .r = entries[i].color.r, .g = entries[i].color.g, .b = entries[i].color.b, @@ -1613,10 +1304,10 @@ LIQ_EXPORT LIQ_NONNULL liq_error liq_histogram_add_image(liq_histogram *input_hi 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->importance_map); + added_ok = pam_computeacolorhash(input_hist->acht, (const liq_color *const *)input_image->rows, cols, rows, input_image->importance_map); if (added_ok) break; } else { - const rgba_pixel* rows_p[1] = { liq_image_get_row_rgba(input_image, row) }; + const liq_color* rows_p[1] = { liq_image_get_row_rgba(input_image, row) }; added_ok = pam_computeacolorhash(input_hist->acht, rows_p, cols, 1, input_image->importance_map ? &input_image->importance_map[row * cols] : NULL); } if (!added_ok) { @@ -1665,29 +1356,6 @@ LIQ_NONNULL static liq_error finalize_histogram(liq_histogram *input_hist, liq_a 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: importance_map - approximation of areas with high-frequency noise, except straight edges. 1=flat, 0=noisy. @@ -1750,7 +1418,7 @@ LIQ_NONNULL static void contrast_maps(liq_image *image) z *= z; // noise is amplified z *= z; // 85 is about 1/3rd of weight (not 0, because noisy pixels still need to be included, just not as precisely). - const unsigned int z_int = 85 + (unsigned int)(z * 171.f); + const unsigned int z_int = 80 + (unsigned int)(z * 176.f); noise[j*cols+i] = MIN(z_int, 255); const int e_int = 255 - (int)(edge * 256.f); edges[j*cols+i] = e_int > 0 ? MIN(e_int, 255) : 0; @@ -1798,7 +1466,7 @@ LIQ_NONNULL static void update_dither_map(liq_image *input_image, unsigned char for(unsigned int col=1; col < width; col++) { const unsigned char px = row_pointers[row][col]; - if (input_image->background && map->palette[px].acolor.a < 1.f/256.f) { + if (input_image->background && map->palette[px].acolor.a < MIN_OPAQUE_A) { // Transparency may or may not create an edge. When there's an explicit background set, assume no edge. continue; } @@ -1858,7 +1526,7 @@ static colormap *add_fixed_colors_to_palette(colormap *palette, const int max_co 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)); + item->adjusted_weight = (item->perceptual_weight + 2.f * item->adjusted_weight) * (0.5f + diff); } /** @@ -1882,6 +1550,7 @@ static colormap *find_best_palette(histogram *hist, const liq_attr *options, con 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); + int fails_in_a_row=0; do { colormap *newmap; @@ -1906,7 +1575,7 @@ static colormap *find_best_palette(histogram *hist, const liq_attr *options, con // 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); + double total_error = kmeans_do_iteration(hist, newmap, first_run_of_target_mse ? NULL : adjust_histogram_callback, omp_get_max_threads()); // 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)) { @@ -1926,15 +1595,13 @@ static colormap *find_best_palette(histogram *hist, const liq_attr *options, con max_colors = MIN(newmap->colors+1, max_colors); feedback_loop_trials -= 1; // asymptotic improvement could make it go on forever + fails_in_a_row = 0; } 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; - } - + fails_in_a_row++; 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; + feedback_loop_trials -= 5 + fails_in_a_row; pam_freecolormap(newmap); } @@ -1948,18 +1615,6 @@ static colormap *find_best_palette(histogram *hist, const liq_attr *options, con 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; @@ -1974,7 +1629,8 @@ LIQ_NONNULL static liq_error pngquant_quantize(histogram *hist, const liq_attr * // 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); + colormap *hist_pal = histogram_to_palette(hist, options->malloc, options->free); + acolormap = add_fixed_colors_to_palette(hist_pal, 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 @@ -1991,11 +1647,7 @@ LIQ_NONNULL static liq_error pngquant_quantize(histogram *hist, const liq_attr * 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 - } - } + hist_reset_colors(hist, acolormap->colors); if (hist->size > 5000) {iterations = (iterations*3 + 3)/4;} if (hist->size > 25000) {iterations = (iterations*3 + 3)/4;} @@ -2007,7 +1659,7 @@ LIQ_NONNULL static liq_error pngquant_quantize(histogram *hist, const liq_attr * double previous_palette_error = MAX_DIFF; for(unsigned int i=0; i < iterations; i++) { - palette_error = kmeans_do_iteration(hist, acolormap, NULL); + palette_error = kmeans_do_iteration(hist, acolormap, NULL, omp_get_max_threads()); if (liq_progress(options, options->progress_stage1 + options->progress_stage2 + (i * options->progress_stage3 * 0.9f) / iterations)) { break; @@ -2078,17 +1730,20 @@ LIQ_EXPORT LIQ_NONNULL liq_error liq_write_remapped_image(liq_result *result, li return LIQ_INVALID_POINTER; } - const size_t required_size = input_image->width * input_image->height; + const size_t required_size = (size_t)input_image->width * (size_t)input_image->height; if (buffer_size < required_size) { return LIQ_BUFFER_TOO_SMALL; } - LIQ_ARRAY(unsigned char *, rows, input_image->height); + unsigned char **rows = input_image->malloc(input_image->height * sizeof(unsigned char *)); 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_error err = liq_write_remapped_image_rows(result, input_image, rows); + input_image->free(rows); + return err; } LIQ_EXPORT LIQ_NONNULL liq_error liq_write_remapped_image_rows(liq_result *quant, liq_image *input_image, unsigned char **row_pointers) @@ -2139,7 +1794,7 @@ LIQ_EXPORT LIQ_NONNULL liq_error liq_write_remapped_image_rows(liq_result *quant // 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)) { + if (!remap_to_palette_floyd(input_image, row_pointers, result, MAX(remapping_error*2.4f, 8.f/256.f), generate_dither_map)) { return LIQ_ABORTED; } } diff --git a/libimagequant.h b/libimagequant.h index c8f84b5..e4763ab 100644 --- a/libimagequant.h +++ b/libimagequant.h @@ -13,8 +13,8 @@ #define LIQ_EXPORT extern #endif -#define LIQ_VERSION 21200 -#define LIQ_VERSION_STRING "2.12.2" +#define LIQ_VERSION 21800 +#define LIQ_VERSION_STRING "2.18.0" #ifndef LIQ_PRIVATE #if defined(__GNUC__) || defined (__llvm__) diff --git a/libimagequant_private.h b/libimagequant_private.h new file mode 100644 index 0000000..1919692 --- /dev/null +++ b/libimagequant_private.h @@ -0,0 +1,51 @@ + +#ifdef _OPENMP +#include +#define LIQ_TEMP_ROW_WIDTH(img_width) (((img_width) | 15) + 1) /* keep alignment & leave space between rows to avoid cache line contention */ +#else +#define LIQ_TEMP_ROW_WIDTH(img_width) (img_width) +#define omp_get_max_threads() 1 +#define omp_get_thread_num() 0 +#endif + +struct liq_image { + const char *magic_header; + void* (*malloc)(size_t); + void (*free)(void*); + + f_pixel *f_pixels; + liq_color **rows; + double gamma; + unsigned int width, height; + unsigned char *importance_map, *edges, *dither_map; + liq_color *pixels, *temp_row; + f_pixel *temp_f_row; + liq_image_get_rgba_row_callback *row_callback; + void *row_callback_user_info; + liq_image *background; + 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; + unsigned char use_dither_map; + unsigned char progress_stage1; +} liq_remapping_result; + + +LIQ_PRIVATE bool liq_image_get_row_f_init(liq_image *img) LIQ_NONNULL; +LIQ_PRIVATE const f_pixel *liq_image_get_row_f(liq_image *input_image, unsigned int row) LIQ_NONNULL; +LIQ_PRIVATE bool liq_remap_progress(const liq_remapping_result *quant, const float percent) LIQ_NONNULL; diff --git a/mediancut.c b/mediancut.c index 447a4af..cc241b8 100644 --- a/mediancut.c +++ b/mediancut.c @@ -25,34 +25,26 @@ struct box { 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; + const 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; + variancea += (mean.a - px.a)*(mean.a - px.a)*weight; + variancer += (mean.r - px.r)*(mean.r - px.r)*weight; + varianceg += (mean.g - px.g)*(mean.g - px.g)*weight; + varianceb += (mean.b - px.b)*(mean.b - px.b)*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), + .a = variancea, + .r = variancer, + .g = varianceg, + .b = varianceb, }; } @@ -133,35 +125,37 @@ static void hist_item_sort_range(hist_item base[], unsigned int len, unsigned in } } -/** 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) +/** sorts array to make sum of weights lower than halfvar one side, returns index of the edge between halfvar parts of the set */ +static unsigned int hist_item_sort_halfvar(hist_item base[], unsigned int len, double halfvar) { + unsigned int base_idx = 0; // track base-index 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; + double tmpsum = 0.; + for(unsigned int t = 0; t <= l && tmpsum < halfvar; ++t) tmpsum += base[t].color_weight; - if (tmpsum < halfvar) { - *lowervar = tmpsum; - } else { + // the split is on the left part + if (tmpsum >= halfvar) { if (l > 0) { - hist_item *res = hist_item_sort_halfvar(base, l, lowervar, halfvar); - if (res) return res; + len = l; + continue; } 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]; + // reached the end of left part + return base_idx; } } - + // process the right part + halfvar -= tmpsum; if (len > r) { - base += r; len -= r; // tail-recursive "call" + base += r; + base_idx += r; + len -= r; // tail-recursive "call" } else { - *lowervar += base[r].color_weight; - return (*lowervar > halfvar) ? &base[r] : NULL; + // reached the end of the right part + return base_idx + len; } } while(1); } @@ -195,8 +189,13 @@ static double prepare_sort(struct box *b, hist_item achv[]) const unsigned int ind1 = b->ind; const unsigned int colors = b->colors; +#if __GNUC__ >= 9 || __clang__ + #pragma omp parallel for if (colors > 25000) \ + schedule(static) default(none) shared(achv, channels, colors, ind1) +#else #pragma omp parallel for if (colors > 25000) \ schedule(static) default(none) shared(achv, channels) +#endif for(unsigned int i=0; i < colors; i++) { const float *chans = (const float *)&achv[ind1 + i].acolor; // Only the first channel really matters. When trying median cut many times @@ -305,15 +304,16 @@ static bool total_box_error_below_target(double target_mse, struct box bv[], uns } static void box_init(struct box *box, const hist_item *achv, const unsigned int ind, const unsigned int colors, const double sum) { + assert(colors > 0); + assert(sum > 0); + box->ind = ind; box->colors = colors; box->sum = sum; box->total_error = -1; box->color = averagepixels(colors, &achv[ind]); - #pragma omp task if (colors > 5000) box->variance = box_variance(achv, box); - #pragma omp task if (colors > 8000) box->max_error = box_max_error(achv, box); } @@ -325,23 +325,35 @@ static void box_init(struct box *box, const hist_item *achv, const unsigned int 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; - LIQ_ARRAY(struct box, bv, newcolors); - unsigned int boxes = 1; + struct box bv[newcolors+16]; + + assert(hist->boxes[0].begin == 0); + assert(hist->boxes[LIQ_MAXCLUSTER-1].end == hist->size); + + unsigned int boxes = 0; + for(int b=0; b < LIQ_MAXCLUSTER; b++) { + int begin = hist->boxes[b].begin; + int end = hist->boxes[b].end; + if (begin == end) { + continue; + } + + if (boxes >= newcolors/3) { + boxes = 0; + begin = 0; + end = hist->boxes[LIQ_MAXCLUSTER-1].end; + b = LIQ_MAXCLUSTER; + } - /* - ** Set up the initial box. - */ - #pragma omp parallel - #pragma omp single - { double sum = 0; - for(unsigned int i=0; i < hist->size; i++) { + for(int i=begin; i < end; i++) { sum += achv[i].adjusted_weight; } - #pragma omp taskgroup - { - box_init(&bv[0], achv, 0, hist->size, sum); - } + box_init(&bv[boxes], achv, begin, end-begin, sum); + boxes++; + } + + assert(boxes < newcolors); /* @@ -372,12 +384,11 @@ LIQ_PRIVATE colormap *mediancut(histogram *hist, unsigned int newcolors, const d */ 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); + unsigned int break_at = hist_item_sort_halfvar(&achv[indx], clrs, halfvar); + break_at = MIN(clrs-1, break_at + 1); /* ** Split the box. @@ -386,11 +397,8 @@ LIQ_PRIVATE colormap *mediancut(histogram *hist, unsigned int newcolors, const d double lowersum = 0; for(unsigned int i=0; i < break_at; i++) lowersum += achv[indx + i].adjusted_weight; - #pragma omp taskgroup - { - box_init(&bv[bi], achv, indx, break_at, lowersum); - box_init(&bv[boxes], achv, indx + break_at, clrs - break_at, sm - lowersum); - } + box_init(&bv[bi], achv, indx, break_at, lowersum); + box_init(&bv[boxes], achv, indx + break_at, clrs - break_at, sm - lowersum); ++boxes; @@ -398,7 +406,6 @@ LIQ_PRIVATE colormap *mediancut(histogram *hist, unsigned int newcolors, const d break; } } - } colormap *map = pam_colormap(boxes, malloc, free); set_colormap_from_boxes(map, bv, boxes, achv); diff --git a/mediancut.h b/mediancut.h index d97696c..9a4cb53 100644 --- a/mediancut.h +++ b/mediancut.h @@ -1,2 +1,6 @@ +#ifndef MEDIANCUT_H +#define MEDIANCUT_H LIQ_PRIVATE colormap *mediancut(histogram *hist, unsigned int newcolors, const double target_mse, const double max_mse, void* (*malloc)(size_t), void (*free)(void*)); + +#endif diff --git a/nearest.c b/nearest.c index aeb4dc1..7c8ee6a 100644 --- a/nearest.c +++ b/nearest.c @@ -19,15 +19,23 @@ typedef struct vp_sort_tmp { typedef struct vp_search_tmp { float distance; + float distance_squared; unsigned int idx; int exclude; } vp_search_tmp; +struct leaf { + f_pixel color; + unsigned int idx; +}; + typedef struct vp_node { struct vp_node *near, *far; f_pixel vantage_point; - float radius; - unsigned int idx; + float radius, radius_squared; + struct leaf *rest; + unsigned short idx; + unsigned short restcount; } vp_node; struct nearest_map { @@ -79,6 +87,7 @@ static vp_node *vp_create_node(mempoolptr *m, vp_sort_tmp indexes[], int num_ind .vantage_point = items[indexes[0].idx].acolor, .idx = indexes[0].idx, .radius = MAX_DIFF, + .radius_squared = MAX_DIFF, }; return node; } @@ -99,9 +108,19 @@ static vp_node *vp_create_node(mempoolptr *m, vp_sort_tmp indexes[], int num_ind .vantage_point = items[ref_idx].acolor, .idx = ref_idx, .radius = sqrtf(indexes[half_idx].distance_squared), + .radius_squared = 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); + if (num_indexes < 7) { + node->rest = mempool_alloc(m, sizeof(node->rest[0]) * num_indexes, 0); + node->restcount = num_indexes; + for(int i=0; i < num_indexes; i++) { + node->rest[i].idx = indexes[i].idx; + node->rest[i].color = items[indexes[i].idx].acolor; + } + } else { + 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; } @@ -126,10 +145,11 @@ LIQ_PRIVATE struct nearest_map *nearest_init(const colormap *map) { for(unsigned int i=0; i < map->colors; i++) { vp_search_tmp best = { .distance = MAX_DIFF, + .distance_squared = 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 + handle->nearest_other_color_dist[i] = best.distance * best.distance / 4.f; // half of squared distance } return handle; @@ -137,15 +157,29 @@ LIQ_PRIVATE struct nearest_map *nearest_init(const colormap *map) { 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)); + const float distance_squared = colordifference(node->vantage_point, *needle); + const float distance = sqrtf(distance_squared); - if (distance < best_candidate->distance && best_candidate->exclude != node->idx) { + if (distance_squared < best_candidate->distance_squared && best_candidate->exclude != node->idx) { best_candidate->distance = distance; + best_candidate->distance_squared = distance_squared; best_candidate->idx = node->idx; } + if (node->restcount) { + for(int i=0; i < node->restcount; i++) { + const float distance_squared = colordifference(node->rest[i].color, *needle); + if (distance_squared < best_candidate->distance_squared && best_candidate->exclude != node->rest[i].idx) { + best_candidate->distance = sqrtf(distance_squared); + best_candidate->distance_squared = distance_squared; + best_candidate->idx = node->rest[i].idx; + } + } + return; + } + // Recurse towards most likely candidate first to narrow best candidate's distance as soon as possible - if (distance < node->radius) { + if (distance_squared < node->radius_squared) { if (node->near) { vp_search_node(node->near, needle, best_candidate); } @@ -155,7 +189,7 @@ static void vp_search_node(const vp_node *node, const f_pixel *const needle, vp_ if (node->far && distance >= node->radius - best_candidate->distance) { node = node->far; // Fast tail recursion } else { - break; + return; } } else { if (node->far) { @@ -164,7 +198,7 @@ static void vp_search_node(const vp_node *node, const f_pixel *const needle, vp_ if (node->near && distance <= node->radius + best_candidate->distance) { node = node->near; // Fast tail recursion } else { - break; + return; } } } while(true); @@ -179,6 +213,7 @@ LIQ_PRIVATE unsigned int nearest_search(const struct nearest_map *handle, const vp_search_tmp best_candidate = { .distance = sqrtf(guess_diff), + .distance_squared = guess_diff, .idx = likely_colormap_index, .exclude = -1, }; diff --git a/nearest.h b/nearest.h index e20233b..10a0a2c 100644 --- a/nearest.h +++ b/nearest.h @@ -2,7 +2,13 @@ // nearest.h // pngquant // + +#ifndef NEAREST_H +#define NEAREST_H + 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); + +#endif diff --git a/pam.c b/pam.c index 660f829..5d955e1 100644 --- a/pam.c +++ b/pam.c @@ -14,7 +14,7 @@ #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) +LIQ_PRIVATE bool pam_computeacolorhash(struct acolorhash_table *acht, const liq_color *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<> (8-ignorebits)); // fancier hashing algorithms didn't improve much @@ -52,13 +44,20 @@ LIQ_PRIVATE bool pam_computeacolorhash(struct acolorhash_table *acht, const rgba } else { boost = 255; } + } else { + // "dirty alpha" has different RGBA values that end up being the same fully transparent color + px.l=0; hash=0; + + boost = 2000; + if (importance_map) { + importance_map++; + } } if (!pam_add_to_hash(acht, hash, boost, px, row, rows)) { return false; } } - } acht->cols = cols; acht->rows += rows; @@ -176,14 +175,18 @@ LIQ_PRIVATE struct acolorhash_table *pam_allocacolorhash(unsigned int maxcolors, 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) +ALWAYS_INLINE static float pam_add_to_hist(struct temp_hist_item achv[], unsigned int *j, const struct acolorhist_arr_item *entry, const float max_perceptual_weight, int counts[]) { - if (entry->perceptual_weight == 0) { + if (entry->perceptual_weight == 0 && *j > 0) { return 0; } - const float w = MIN(entry->perceptual_weight/128.f, max_perceptual_weight); - achv[*j].adjusted_weight = achv[*j].perceptual_weight = w; - achv[*j].acolor = rgba_to_f(gamma_lut, entry->color.rgba); + const liq_color px = entry->color.rgba; + achv[*j].color = px; + const short cluster = ((px.r>>7)<<3) | ((px.g>>7)<<2) | ((px.b>>7)<<1) | (px.a>>7); + counts[cluster]++; + achv[*j].cluster = cluster; + const float w = MIN(entry->perceptual_weight/170.f, max_perceptual_weight); + achv[*j].weight = w; *j += 1; return w; } @@ -200,8 +203,9 @@ LIQ_PRIVATE histogram *pam_acolorhashtoacolorhist(const struct acolorhash_table }; if (!hist->achv) return NULL; - float gamma_lut[256]; - to_f_set_gamma(gamma_lut, gamma); + /// Clusters form initial boxes for quantization, to ensure extreme colors are better represented + int counts[LIQ_MAXCLUSTER] = {}; + struct temp_hist_item *temp = malloc(MAX(1, acht->colors) * sizeof(temp[0])); /* Limit perceptual weight to 1/10th of the image surface area to prevent a single color from dominating all others. */ @@ -212,23 +216,47 @@ LIQ_PRIVATE histogram *pam_acolorhashtoacolorhist(const struct acolorhash_table for(unsigned int 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); + total_weight += pam_add_to_hist(temp, &j, &achl->inline1, max_perceptual_weight, counts); if (achl->used > 1) { - total_weight += pam_add_to_hist(gamma_lut, hist->achv, &j, &achl->inline2, max_perceptual_weight); + total_weight += pam_add_to_hist(temp, &j, &achl->inline2, max_perceptual_weight, counts); 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); + total_weight += pam_add_to_hist(temp, &j, &achl->other_items[k], max_perceptual_weight, counts); } } } } + hist->total_perceptual_weight = total_weight; + + int begin = 0; + for(int i=0; i < LIQ_MAXCLUSTER; i++) { + hist->boxes[i].begin = begin; + hist->boxes[i].end = begin; + begin = begin + counts[i]; + } + hist->size = j; hist->total_perceptual_weight = total_weight; + for(unsigned int k=0; k < hist->size; k++) { + hist->achv[k].tmp.likely_colormap_index = 0; + } if (!j) { + free(temp); pam_freeacolorhist(hist); return NULL; } + + float gamma_lut[256]; + to_f_set_gamma(gamma_lut, gamma); + for(int i=0; i < hist->size; i++) { + int j = hist->boxes[temp[i].cluster].end++; + hist->achv[j].acolor = rgba_to_f(gamma_lut, temp[i].color); + hist->achv[j].perceptual_weight = temp[i].weight; + hist->achv[j].adjusted_weight = temp[i].weight; + } + free(temp); + return hist; } @@ -246,7 +274,7 @@ LIQ_PRIVATE void pam_freeacolorhist(histogram *hist) hist->free(hist); } -LIQ_PRIVATE colormap *pam_colormap(unsigned int colors, void* (*malloc)(size_t), void (*free)(void*)) +LIQ_PRIVATE LIQ_NONNULL colormap *pam_colormap(unsigned int colors, void* (*malloc)(size_t), void (*free)(void*)) { assert(colors > 0 && colors < 65536); @@ -284,3 +312,40 @@ LIQ_PRIVATE void to_f_set_gamma(float gamma_lut[], const double gamma) } } + +/* fixed colors are always included in the palette, so it would be wasteful to duplicate them in palette from histogram */ +LIQ_PRIVATE LIQ_NONNULL 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.f, 2.f/256.f/256.f); + 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_PRIVATE LIQ_NONNULL colormap *histogram_to_palette(const histogram *hist, void* (*malloc)(size_t), void (*free)(void*)) { + if (!hist->size) { + return NULL; + } + colormap *acolormap = pam_colormap(hist->size, malloc, 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_PRIVATE LIQ_NONNULL void hist_reset_colors(const histogram *hist, const unsigned int colors) { + // likely_colormap_index (used and set in kmeans_do_iteration) can't point to index outside colormap + if (colors < 256) for(unsigned int j=0; j < hist->size; j++) { + if (hist->achv[j].tmp.likely_colormap_index >= colors) { + hist->achv[j].tmp.likely_colormap_index = 0; // actual value doesn't matter, as the guess is out of date anyway + } + } +} diff --git a/pam.h b/pam.h index 2ca4327..4ab4e0d 100644 --- a/pam.h +++ b/pam.h @@ -16,6 +16,12 @@ #ifndef PAM_H #define PAM_H +// accidental debug assertions make color search much slower, +// so force assertions off if there's no explicit setting +#if !defined(NDEBUG) && !defined(DEBUG) +#define NDEBUG +#endif + #include #include #include @@ -83,45 +89,48 @@ /* 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 float internal_gamma = 0.5499f; +static const float internal_gamma = 0.57f; LIQ_PRIVATE void to_f_set_gamma(float gamma_lut[], const double gamma); +#define MIN_OPAQUE_A (1.f / 256.f * LIQ_WEIGHT_A) + +#define LIQ_WEIGHT_A 0.625f +#define LIQ_WEIGHT_R 0.5f +#define LIQ_WEIGHT_G 1.0f +#define LIQ_WEIGHT_B 0.45f +#define LIQ_WEIGHT_MSE 0.45 // fudge factor for compensating that colors aren't 0..1 range + /** 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) +ALWAYS_INLINE static f_pixel rgba_to_f(const float gamma_lut[], const liq_color px); +inline static f_pixel rgba_to_f(const float gamma_lut[], const liq_color 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, + .a = a * LIQ_WEIGHT_A, + .r = gamma_lut[px.r] * LIQ_WEIGHT_R * a, + .g = gamma_lut[px.g] * LIQ_WEIGHT_G * a, + .b = gamma_lut[px.b] * LIQ_WEIGHT_B * a, }; } -inline static rgba_pixel f_to_rgb(const float gamma, const f_pixel px) +inline static liq_color f_to_rgb(const float gamma, const f_pixel px) { - if (px.a < 1.f/256.f) { - return (rgba_pixel){0,0,0,0}; + if (px.a < MIN_OPAQUE_A) { + return (liq_color){0,0,0,0}; } - float r = px.r / px.a, - g = px.g / px.a, - b = px.b / px.a, - a = px.a; + float r = (LIQ_WEIGHT_A / LIQ_WEIGHT_R) * px.r / px.a, + g = (LIQ_WEIGHT_A / LIQ_WEIGHT_G) * px.g / px.a, + b = (LIQ_WEIGHT_A / LIQ_WEIGHT_B) * px.b / px.a; r = powf(r, gamma/internal_gamma); g = powf(g, gamma/internal_gamma); @@ -131,9 +140,9 @@ inline static rgba_pixel f_to_rgb(const float gamma, const f_pixel px) r *= 256.f; g *= 256.f; b *= 256.f; - a *= 256.f; + float a = (256.f / LIQ_WEIGHT_A) * px.a; - return (rgba_pixel){ + return (liq_color){ .r = r>=255.f ? 255 : r, .g = g>=255.f ? 255 : g, .b = b>=255.f ? 255 : b, @@ -141,12 +150,12 @@ inline static rgba_pixel f_to_rgb(const float gamma, const f_pixel px) }; } -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) +ALWAYS_INLINE static float colordifference_ch(const float x, const float y, const float alphas); +inline static float colordifference_ch(const float x, const float y, const float 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; + const float black = x-y, white = black+alphas; return MAX(black*black, white*white); } @@ -166,7 +175,7 @@ inline static float colordifference_stdc(const f_pixel px, const f_pixel py) // (px.rgb - px.a) - (py.rgb - py.a) // (px.rgb - py.rgb) + (py.a - px.a) - const double alphas = py.a-px.a; + const float 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); @@ -176,8 +185,17 @@ ALWAYS_INLINE static float colordifference(f_pixel px, f_pixel py); inline static float colordifference(f_pixel px, f_pixel py) { #if USE_SSE +#ifdef _MSC_VER + /* In MSVC we cannot use the align attribute in parameters. + * This is used a lot, so we just use an unaligned load. + * Also the compiler incorrectly inlines vpx and vpy without + * the volatile when optimization is applied for x86_64. */ + const volatile __m128 vpx = _mm_loadu_ps((const float*)&px); + const volatile __m128 vpy = _mm_loadu_ps((const float*)&py); +#else const __m128 vpx = _mm_load_ps((const float*)&px); const __m128 vpy = _mm_load_ps((const float*)&py); +#endif // y.a - x.a __m128 alphas = _mm_sub_ss(vpy, vpx); @@ -205,7 +223,7 @@ inline static float colordifference(f_pixel px, f_pixel py) /* from pamcmap.h */ union rgba_as_int { - rgba_pixel rgba; + liq_color rgba; unsigned int l; }; @@ -221,12 +239,25 @@ typedef struct { } tmp; } hist_item; +#define LIQ_MAXCLUSTER 16 + +struct temp_hist_item { + liq_color color; + float weight; + short cluster; +}; + +struct histogram_box { + int begin, end; +}; + typedef struct { hist_item *achv; void (*free)(void*); double total_perceptual_weight; unsigned int size; unsigned int ignorebits; + struct histogram_box boxes[LIQ_MAXCLUSTER]; } histogram; typedef struct { @@ -265,13 +296,17 @@ struct acolorhash_table { 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_computeacolorhash(struct acolorhash_table *acht, const liq_color *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, unsigned int 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 colormap *pam_colormap(unsigned int colors, void* (*malloc)(size_t), void (*free)(void*)) LIQ_NONNULL; +LIQ_PRIVATE colormap *pam_duplicate_colormap(colormap *map) LIQ_NONNULL; LIQ_PRIVATE void pam_freecolormap(colormap *c); +LIQ_PRIVATE void remove_fixed_colors_from_histogram(histogram *hist, const int fixed_colors_count, const f_pixel fixed_colors[], const float target_mse) LIQ_NONNULL; +LIQ_PRIVATE colormap *histogram_to_palette(const histogram *hist, void* (*malloc)(size_t), void (*free)(void*)) LIQ_NONNULL; +LIQ_PRIVATE void hist_reset_colors(const histogram *hist, const unsigned int colors) LIQ_NONNULL; + #endif diff --git a/remap.c b/remap.c new file mode 100644 index 0000000..4495810 --- /dev/null +++ b/remap.c @@ -0,0 +1,300 @@ +#include +#include + +#include "libimagequant.h" +#include "pam.h" +#include "libimagequant_private.h" + +#include "nearest.h" +#include "kmeans.h" + +LIQ_PRIVATE LIQ_NONNULL 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_init(input_image)) { + return -1; + } + if (input_image->background && !liq_image_get_row_f_init(input_image->background)) { + return -1; + } + + const colormap_item *acolormap = map->palette; + + struct nearest_map *const n = nearest_init(map); + liq_image *background = input_image->background; + const int transparent_index = background ? nearest_search(n, &(f_pixel){0,0,0,0}, 0, NULL) : -1; + if (background && acolormap[transparent_index].acolor.a > 1.f/256.f) { + // palette unsuitable for using the bg + background = NULL; + } + + + const unsigned int max_threads = omp_get_max_threads(); + LIQ_ARRAY(kmeans_state, average_color, (KMEANS_CACHE_LINE_GAP+map->colors) * max_threads); + kmeans_init(map, max_threads, average_color); + +#if __GNUC__ >= 9 || __clang__ + #pragma omp parallel for if (rows*cols > 3000) \ + schedule(static) default(none) shared(background,acolormap,average_color,cols,input_image,map,n,output_pixels,rows,transparent_index) reduction(+:remapping_error) +#endif + for(int row = 0; row < rows; ++row) { + const f_pixel *const row_pixels = liq_image_get_row_f(input_image, row); + const f_pixel *const bg_pixels = background && acolormap[transparent_index].acolor.a < MIN_OPAQUE_A ? liq_image_get_row_f(background, row) : NULL; + + unsigned int last_match=0; + for(unsigned int col = 0; col < cols; ++col) { + float diff; + last_match = nearest_search(n, &row_pixels[col], last_match, &diff); + if (bg_pixels) { + float bg_diff = colordifference(bg_pixels[col], acolormap[last_match].acolor); + if (bg_diff <= diff) { + diff = bg_diff; + last_match = transparent_index; + } + } + output_pixels[row][col] = last_match; + + remapping_error += diff; + if (last_match != transparent_index) { + 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.f) { a = 1.f; } + 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.8f; + } 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_PRIVATE LIQ_NONNULL 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; + + if (!liq_image_get_row_f_init(input_image)) { + return false; + } + if (input_image->background && !liq_image_get_row_f_init(input_image->background)) { + return false; + } + + /* 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); + liq_image *background = input_image->background; + const int transparent_index = background ? nearest_search(n, &(f_pixel){0,0,0,0}, 0, NULL) : -1; + if (background && acolormap[transparent_index].acolor.a > 1.f/256.f) { + // palette unsuitable for using the bg + background = NULL; + } + + // 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.f - (1.f-base_dithering_level)*(1.f-base_dithering_level); + + if (dither_map) { + base_dithering_level *= 1.f/255.f; // convert byte to float + } + base_dithering_level *= 15.f/16.f; // 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); + const f_pixel *const bg_pixels = background && acolormap[transparent_index].acolor.a < MIN_OPAQUE_A ? liq_image_get_row_f(background, row) : NULL; + int undithered_bg_used = 0; + + 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; + float dither_diff; + last_match = nearest_search(n, &spx, guessed_match, &dither_diff); + f_pixel output_px = acolormap[last_match].acolor; + // this is for animgifs + if (bg_pixels) { + // if the background makes better match *with* dithering, it's a definitive win + float bg_for_dither_diff = colordifference(spx, bg_pixels[col]); + if (bg_for_dither_diff <= dither_diff) { + output_px = bg_pixels[col]; + last_match = 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 + float max_diff = colordifference(row_pixels[col], bg_pixels[col]); + float dithered_diff = colordifference(row_pixels[col], 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 + float undithered_diff = colordifference(row_pixels[col], acolormap[guessed_match].acolor); + if (undithered_diff < max_diff) { + undithered_bg_used++; + output_px = acolormap[guessed_match].acolor; + last_match = guessed_match; + } + } + } + } + + output_pixels[row][col] = last_match; + + 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.75f; + err.g *= 0.75f; + err.b *= 0.75f; + err.a *= 0.75f; + } + + /* 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; +} diff --git a/remap.h b/remap.h new file mode 100644 index 0000000..59d509b --- /dev/null +++ b/remap.h @@ -0,0 +1,7 @@ +#ifndef REMAP_H +#define REMAP_H + +LIQ_PRIVATE float remap_to_palette(liq_image *const input_image, unsigned char *const *const output_pixels, colormap *const map) LIQ_NONNULL; +LIQ_PRIVATE 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) LIQ_NONNULL; + +#endif diff --git a/rust-api/.gitignore b/rust-api/.gitignore new file mode 100644 index 0000000..2c96eb1 --- /dev/null +++ b/rust-api/.gitignore @@ -0,0 +1,2 @@ +target/ +Cargo.lock diff --git a/rust-api/COPYRIGHT b/rust-api/COPYRIGHT new file mode 100644 index 0000000..f247798 --- /dev/null +++ b/rust-api/COPYRIGHT @@ -0,0 +1,635 @@ +https://raw.githubusercontent.com/ImageOptim/libimagequant/master/COPYRIGHT + +libimagequant © 2009-2016 by Kornel Lesiński. + + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + +- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +© 1989, 1991 by Jef Poskanzer. +© 1997, 2000, 2002 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. diff --git a/rust-api/Cargo.toml b/rust-api/Cargo.toml new file mode 100644 index 0000000..f28bd09 --- /dev/null +++ b/rust-api/Cargo.toml @@ -0,0 +1,29 @@ +[package] +categories = ["multimedia::images"] +description = "Convert 24/32-bit images to 8-bit palette with alpha channel.\nBindings for libimagequant that powers pngquant lossy PNG compressor.\n\nDual-licensed like pngquant. See https://pngquant.org for details." +documentation = "https://docs.rs/imagequant" +homepage = "https://pngquant.org/lib/" +include = ["src/*", "examples/*", "COPYRIGHT", "Cargo.toml", "README.md"] +keywords = ["quantization", "palette", "image", "pngquant", "compression"] +authors = [ "Kornel " ] +license = "GPL-3.0-or-later" +name = "imagequant" +readme = "README.md" +repository = "https://github.com/ImageOptim/libimagequant" +version = "3.2.0" +edition = "2018" + +[dependencies] +fallible_collections = "0.4.3" +imagequant-sys = { version = "3.1.2", path = "../" } +libc = "0.2.102" +rgb = "0.8.27" + +[features] +default = ["sse"] +sse = ["imagequant-sys/sse"] +openmp = ["imagequant-sys/openmp"] +openmp-static = ["openmp", "imagequant-sys/openmp-static"] + +[lib] +name = "imagequant" diff --git a/rust-api/README.md b/rust-api/README.md new file mode 100644 index 0000000..22e412a --- /dev/null +++ b/rust-api/README.md @@ -0,0 +1,15 @@ +# [libimagequant](https://pngquant.org/lib/) bindings for [Rust](https://www.rust-lang.org/) + +Imagequant library converts RGBA images to 8-bit indexed images with palette, *including* alpha component. +It's ideal for generating tiny PNG images (although [image I/O](https://github.com/kornelski/lodepng-rust) isn't handled by the library itself). + +This wrapper makes the library usable from Rust. + +Rust API closely follows the C API, but is slightly OO-ified: + + liq_set_dithering_level(result, 1.0); + ↓ + result.set_dithering_level(1.0); + +For more details see [libimagequant documentation](https://pngquant.org/lib/) and [Rust function reference](https://kornelski.github.io/libimagequant-rust/imagequant/). + diff --git a/rust-api/examples/basic.rs b/rust-api/examples/basic.rs new file mode 100644 index 0000000..7f26866 --- /dev/null +++ b/rust-api/examples/basic.rs @@ -0,0 +1,32 @@ +// Don't forget to add -L . (or whatever dir has .rlib) to rustc! + +fn main() { + // Image loading/saving is outside scope of this library + let width = 10usize; + let height = 10usize; + let fakebitmap = vec![imagequant::RGBA {r:0, g:0, b:0, a:0}; width * height]; + + // http://pngquant.org/lib/ + + // Configure the library + let mut liq = imagequant::new(); + liq.set_speed(5); + liq.set_quality(70, 99); + + // Describe the bitmap + let ref mut img = 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); + + // You can reuse the result to generate several images with the same palette + let (palette, pixels) = res.remapped(img).unwrap(); + + println!("Done! Got palette {:?} and {} pixels with {}% quality", palette, pixels.len(), res.quantization_quality()); +} diff --git a/rust-api/src/lib.rs b/rust-api/src/lib.rs new file mode 100644 index 0000000..1ccc6d2 --- /dev/null +++ b/rust-api/src/lib.rs @@ -0,0 +1,850 @@ +//! https://pngquant.org/lib/ +//! +//! Converts RGBA images to 8-bit with alpha channel. +//! +//! This is based on imagequant library, which generates very high quality images. +//! +//! See `examples/` directory for example code. +#![doc(html_logo_url = "https://pngquant.org/pngquant-logo.png")] +#![warn(missing_docs)] + +pub use crate::ffi::liq_error; +pub use crate::ffi::liq_error::*; + +use fallible_collections::FallibleVec; +use imagequant_sys as ffi; +use std::ffi::CStr; +use std::fmt; +use std::marker::PhantomData; +use std::mem::MaybeUninit; +use std::mem; +use std::os::raw::{c_int, c_void, c_char}; +use std::ptr; +use std::ptr::NonNull; + +pub use rgb::RGBA8 as RGBA; + +/// Allocates all memory used by the library, like [`libc::malloc`]. +/// +/// Must return properly aligned memory (16-bytes on x86, pointer size on other architectures). +pub type MallocUnsafeFn = unsafe extern "C" fn(size: usize) -> *mut c_void; + +/// Frees all memory used by the library, like [`libc::free`]. +pub type FreeUnsafeFn = unsafe extern "C" fn(*mut c_void); + +/// 8-bit RGBA. This is the only color format used by the library. +pub type Color = ffi::liq_color; + +/// Number of pixels in a given color +/// +/// Used if you're building histogram manually. Otherwise see `add_image()` +pub type HistogramEntry = ffi::liq_histogram_entry; + +/// Print messages +pub type LogCallbackFn = Box; + +/// Result of [`ProgressCallbackFn`] +#[repr(C)] +pub enum ControlFlow { + /// Continue processing as normal + Continue = 1, + /// Abort processing and fail + Break = 0, +} + +/// Check progress and optionally abort +pub type ProgressCallbackFn = Box ControlFlow + Send>; + +/// Settings for the conversion process. Start here. +pub struct Attributes { + handle: NonNull, + malloc: MallocUnsafeFn, + free: FreeUnsafeFn, + log_callback: Option>, // Double boxed, because it's a fat ptr, and Attributes can be moved + progress_callback: Option>, +} + +/// Describes image dimensions for the library. +pub struct Image<'a> { + handle: NonNull, + /// Holds row pointers for images with stride + _marker: PhantomData<&'a [u8]>, +} + +/// Palette inside. +pub struct QuantizationResult { + handle: NonNull, +} + +/// Generate one shared palette for multiple images. +pub struct Histogram<'a> { + attr: &'a Attributes, + handle: NonNull, +} + +impl Drop for Attributes { + #[inline] + fn drop(&mut self) { + unsafe { + ffi::liq_attr_destroy(self.handle.as_mut()); + } + } +} + +impl<'a> Drop for Image<'a> { + #[inline] + fn drop(&mut self) { + unsafe { + ffi::liq_image_destroy(self.handle.as_mut()); + } + } +} + +impl Drop for QuantizationResult { + #[inline] + fn drop(&mut self) { + unsafe { + ffi::liq_result_destroy(self.handle.as_mut()); + } + } +} + +impl<'a> Drop for Histogram<'a> { + #[inline] + fn drop(&mut self) { + unsafe { + ffi::liq_histogram_destroy(self.handle.as_mut()); + } + } +} + +impl Clone for Attributes { + /// NB: it doesn't clone the log/progress callbacks! + #[inline] + fn clone(&self) -> Attributes { + unsafe { + let mut handle = NonNull::new(ffi::liq_attr_copy(self.handle.as_ref())).unwrap(); + if self.log_callback.is_some() { // can't be cloned + ffi::liq_set_log_callback(handle.as_mut(), None, ptr::null_mut()); + } + if self.progress_callback.is_some() { // can't be cloned + ffi::liq_attr_set_progress_callback(handle.as_mut(), None, ptr::null_mut()); + } + Attributes { + handle, + malloc: self.malloc, + free: self.free, + log_callback: None, + progress_callback: None, + } + } + } +} + +impl Default for Attributes { + #[inline(always)] + fn default() -> Attributes { + Attributes::new() + } +} + +impl Attributes { + /// New handle for library configuration + /// + /// See also `new_image()` + #[inline] + #[must_use] + pub fn new() -> Self { + let handle = unsafe { ffi::liq_attr_create() }; + Attributes { + handle: NonNull::new(handle).expect("SSE-capable CPU is required for this build."), + malloc: libc::malloc, + free: libc::free, + log_callback: None, + progress_callback: None, + } + } + + /// New handle for library configuration, with specified custom allocator for internal use. + /// + /// See also `new_image()` + /// + /// # Safety + /// + /// * `malloc` and `free` must behave according to their corresponding C specification. + /// * `malloc` must return properly aligned memory (16-bytes on x86, pointer-sized on other architectures). + #[inline] + #[must_use] + pub unsafe fn with_allocator(malloc: MallocUnsafeFn, free: FreeUnsafeFn) -> Self { + let handle = ffi::liq_attr_create_with_allocator(malloc, free); + Attributes { + handle: NonNull::new(handle).expect("SSE-capable CPU is required for this build."), + malloc, free, + log_callback: None, + progress_callback: None, + } + } + + /// It's better to use `set_quality()` + #[inline] + pub fn set_max_colors(&mut self, value: i32) -> liq_error { + unsafe { ffi::liq_set_max_colors(self.handle.as_mut(), value) } + } + + /// 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: i32) -> liq_error { + unsafe { ffi::liq_set_min_posterization(self.handle.as_mut(), value) } + } + + /// Returns number of bits of precision truncated + #[inline] + pub fn min_posterization(&mut self) -> i32 { + unsafe { ffi::liq_get_min_posterization(self.handle.as_ref()) } + } + + /// Range 0-100, roughly like JPEG. + /// + /// If minimum quality can't be met, quantization will fail. + /// + /// Default is min 0, max 100. + #[inline] + pub fn set_quality(&mut self, min: u32, max: u32) -> liq_error { + unsafe { ffi::liq_set_quality(self.handle.as_mut(), min as c_int, max as c_int) } + } + + /// Reads values set with `set_quality` + #[inline] + pub fn quality(&mut self) -> (u32, u32) { + unsafe { + (ffi::liq_get_min_quality(self.handle.as_ref()) as u32, + ffi::liq_get_max_quality(self.handle.as_ref()) as u32) + } + } + + /// 1-10. + /// + /// Faster speeds generate images of lower quality, but may be useful + /// for real-time generation of images. + #[inline] + pub fn set_speed(&mut self, value: i32) -> liq_error { + unsafe { ffi::liq_set_speed(self.handle.as_mut(), value) } + } + + /// Move transparent color to the last entry in the palette + /// + /// This is less efficient for PNG, but required by some broken software + #[inline] + pub fn set_last_index_transparent(&mut self, value: bool) { + unsafe { ffi::liq_set_last_index_transparent(self.handle.as_mut(), value as c_int) } + } + + /// Return currently set speed/quality trade-off setting + #[inline(always)] + #[must_use] + pub fn speed(&mut self) -> i32 { + unsafe { ffi::liq_get_speed(self.handle.as_ref()) } + } + + /// Return max number of colors set + #[inline(always)] + #[must_use] + pub fn max_colors(&mut self) -> i32 { + unsafe { ffi::liq_get_max_colors(self.handle.as_ref()) } + } + + /// 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<'a>(&self, bitmap: &'a [RGBA], width: usize, height: usize, gamma: f64) -> Result, liq_error> { + Image::new(self, bitmap, width, height, gamma) + } + + /// Stride is in pixels. Allows defining regions of larger images or images with padding without copying. + #[inline] + pub fn new_image_stride<'a>(&self, bitmap: &'a [RGBA], width: usize, height: usize, stride: usize, gamma: f64) -> Result, liq_error> { + Image::new_stride(self, bitmap, width, height, stride, gamma) + } + + /// Like `new_image_stride`, but makes a copy of the pixels + #[inline] + pub fn new_image_stride_copy(&self, bitmap: &[RGBA], width: usize, height: usize, stride: usize, gamma: f64) -> Result, liq_error> { + Image::new_stride_copy(self, bitmap, width, height, stride, gamma) + } + + /// Create new histogram + /// + /// Use to make one palette suitable for many images + #[inline(always)] + #[must_use] + pub fn new_histogram(&self) -> Histogram<'_> { + Histogram::new(&self) + } + + /// Generate palette for the image + pub fn quantize(&mut self, image: &Image<'_>) -> Result { + unsafe { + let mut h = ptr::null_mut(); + match ffi::liq_image_quantize(image.handle.as_ref(), self.handle.as_ref(), &mut h) { + liq_error::LIQ_OK if !h.is_null() => Ok(QuantizationResult { handle: NonNull::new_unchecked(h) }), + err => Err(err), + } + } + } + + /// 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(always)] + pub fn set_log_callback(&mut self, callback: F) { + self._set_log_callback(Box::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(always)] + pub fn set_progress_callback ControlFlow + Send + 'static>(&mut self, callback: F) { + self._set_progress_callback(Box::new(callback)) + } + + fn _set_log_callback(&mut self, callback: LogCallbackFn) { + let mut log_callback = Box::new(callback); + let log_callback_ref: &mut LogCallbackFn = &mut *log_callback; + unsafe { + ffi::liq_set_log_callback(self.handle.as_mut(), Some(call_log_callback), log_callback_ref as *mut LogCallbackFn as *mut c_void); + } + self.log_callback = Some(log_callback); + } + + fn _set_progress_callback(&mut self, callback: ProgressCallbackFn) { + let mut progress_callback = Box::new(callback); + let progress_callback_ref: &mut ProgressCallbackFn = &mut *progress_callback; + unsafe { + ffi::liq_attr_set_progress_callback(self.handle.as_mut(), Some(call_progress_callback), progress_callback_ref as *mut ProgressCallbackFn as *mut c_void); + } + self.progress_callback = Some(progress_callback); + } +} + +extern "C" fn call_log_callback(_liq: &ffi::liq_attr, msg: *const c_char, user_data: *mut c_void) { + unsafe { + let cb: &mut LogCallbackFn = match (user_data as *mut LogCallbackFn).as_mut() { + Some(cb) => cb, + None => return, + }; + match CStr::from_ptr(msg).to_str() { + Ok(msg) => cb(msg), + Err(_) => return, + }; + } +} + +extern "C" fn call_progress_callback(perc: f32, user_data: *mut c_void) -> c_int { + unsafe { + match (user_data as *mut ProgressCallbackFn).as_mut() { + Some(cb) => cb(perc) as _, + None => ControlFlow::Break as _, + } + } +} + +/// Start here: creates new handle for library configuration +#[inline(always)] +#[must_use] +pub fn new() -> Attributes { + Attributes::new() +} + +impl<'a> Histogram<'a> { + /// 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: &'a Attributes) -> Self { + Histogram { + attr, + handle: unsafe { NonNull::new(ffi::liq_histogram_create(attr.handle.as_ref())).unwrap() }, + } + } + + /// "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 total number of fixed colors exceeds 256, this function will fail with `LIQ_BUFFER_TOO_SMALL`. + #[inline] + pub fn add_image(&mut self, image: &mut Image<'_>) -> liq_error { + unsafe { ffi::liq_histogram_add_image(self.handle.as_mut(), self.attr.handle.as_ref(), image.handle.as_mut()) } + } + + /// 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. + #[inline] + pub fn add_colors(&mut self, colors: &[HistogramEntry], gamma: f64) -> liq_error { + unsafe { + ffi::liq_histogram_add_colors(self.handle.as_mut(), self.attr.handle.as_ref(), colors.as_ptr(), colors.len() as c_int, gamma) + } + } + + /// 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) -> Result { + unsafe { + let mut h = ptr::null_mut(); + match ffi::liq_histogram_quantize(self.handle.as_ref(), self.attr.handle.as_ref(), &mut h) { + liq_error::LIQ_OK if !h.is_null() => Ok(QuantizationResult { handle: NonNull::new_unchecked(h) }), + err => Err(err), + } + } + } +} + +/// Generate image row on the fly +/// +/// `output_row` is an array `width` RGBA elements wide. +/// `y` is the row (0-indexed) to write to the `output_row` +/// `user_data` is the data given to `Image::new_unsafe_fn()` +pub type ConvertRowUnsafeFn = unsafe extern "C" fn(output_row: *mut Color, y: c_int, width: c_int, user_data: *mut UserData); + +impl<'bitmap> Image<'bitmap> { + /// Describe dimensions of a slice of RGBA pixels. + /// + /// `bitmap` must be either `&[u8]` or a slice with one element per pixel (`&[RGBA]`). + /// + /// Use `0.` for gamma if the image is sRGB (most images are). + #[inline(always)] + pub fn new(attr: &Attributes, bitmap: &'bitmap [RGBA], width: usize, height: usize, gamma: f64) -> Result { + Self::new_stride(attr, bitmap, width, height, width, gamma) + } + + /// Generate rows on demand using a callback function. + /// + /// The callback function must be cheap (e.g. just byte-swap pixels). + /// It will be called multiple times per row. May be called in any order from any thread. + /// + /// The user data must be compatible with a primitive pointer + /// (i.e. not a slice, not a Trait object. `Box` it if you must). + #[inline] + pub fn new_unsafe_fn(attr: &Attributes, convert_row_fn: ConvertRowUnsafeFn, user_data: *mut CustomData, width: usize, height: usize, gamma: f64) -> Result { + let handle = NonNull::new(unsafe { + ffi::liq_image_create_custom(attr.handle.as_ref(), mem::transmute(convert_row_fn), user_data.cast(), width as c_int, height as c_int, gamma) + }).ok_or(LIQ_INVALID_POINTER)?; + Ok(Image { handle, _marker: PhantomData }) + } + + /// Stride is in pixels. Allows defining regions of larger images or images with padding without copying. + #[inline(always)] + pub fn new_stride(attr: &Attributes, bitmap: &'bitmap [RGBA], width: usize, height: usize, stride: usize, gamma: f64) -> Result { + // Type definition preserves the lifetime, so it's not unsafe + unsafe { Self::new_stride_internal(attr, bitmap, width, height, stride, gamma, false) } + } + + /// Create new image by copying `bitmap` to an internal buffer, so that it makes a self-contained type. + #[inline(always)] + pub fn new_stride_copy(attr: &Attributes, bitmap: &[RGBA], width: usize, height: usize, stride: usize, gamma: f64) -> Result, liq_error> { + // copy guarantees the image doesn't reference the bitmap any more + unsafe { Self::new_stride_internal(attr, bitmap, width, height, stride, gamma, true) } + } + + unsafe fn new_stride_internal<'varies>(attr: &Attributes, bitmap: &[RGBA], width: usize, height: usize, stride: usize, gamma: f64, copy: bool) -> Result, liq_error> { + if bitmap.len() < (stride * height + width - stride) { + eprintln!("Buffer length is {} bytes, which is not enough for {}×{}×4 RGBA bytes", bitmap.len()*4, stride, height); + return Err(LIQ_BUFFER_TOO_SMALL); + } + let (bitmap, ownership) = if copy { + let copied = (attr.malloc)(4 * bitmap.len()) as *mut RGBA; + ptr::copy_nonoverlapping(bitmap.as_ptr(), copied, bitmap.len()); + (copied as *const _, ffi::liq_ownership::LIQ_OWN_ROWS | ffi::liq_ownership::LIQ_OWN_PIXELS) + } else { + (bitmap.as_ptr(), ffi::liq_ownership::LIQ_OWN_ROWS) + }; + let rows = Self::malloc_image_rows(bitmap, stride, height, attr.malloc); + let h = NonNull::new(ffi::liq_image_create_rgba_rows(attr.handle.as_ref(), rows, width as c_int, height as c_int, gamma)); + let img = match h { + None => { + (attr.free)(rows.cast()); + return Err(LIQ_INVALID_POINTER); + } + Some(h) => { + Image { + handle: h, + _marker: PhantomData, + } + } + }; + match ffi::liq_image_set_memory_ownership(img.handle.as_ref(), ownership) { + LIQ_OK => Ok(img), + err => { + drop(img); + (attr.free)(rows.cast()); + Err(err) + }, + } + } + + /// For arbitrary stride libimagequant requires rows. It's most convenient if they're allocated using libc, + /// so they can be owned and freed automatically by the C library. + unsafe fn malloc_image_rows(bitmap: *const RGBA, stride: usize, height: usize, malloc: MallocUnsafeFn) -> *mut *const u8 { + let mut byte_ptr = bitmap as *const u8; + let stride_bytes = stride * 4; + let rows = malloc(mem::size_of::<*const u8>() * height) as *mut *const u8; + for y in 0..height { + *rows.add(y) = byte_ptr; + byte_ptr = byte_ptr.add(stride_bytes); + } + rows + } + + /// Width of the image in pixels + #[inline] + #[must_use] + pub fn width(&self) -> usize { + unsafe { ffi::liq_image_get_width(self.handle.as_ref()) as usize } + } + + /// Height of the image in pixels + #[inline] + #[must_use] + pub fn height(&self) -> usize { + unsafe { ffi::liq_image_get_height(self.handle.as_ref()) as usize } + } + + /// 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. + #[inline] + pub fn add_fixed_color(&mut self, color: ffi::liq_color) -> liq_error { + unsafe { ffi::liq_image_add_fixed_color(self.handle.as_mut(), color) } + } + + /// Remap pixels assuming they will be displayed on this background. + /// + /// 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 + #[inline] + pub fn set_background<'own, 'bg: 'own>(&'own mut self, background: Image<'bg>) -> Result<(), liq_error> { + unsafe { + ffi::liq_image_set_background(self.handle.as_mut(), background.into_raw()).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. + #[inline] + pub fn set_importance_map(&mut self, map: &[u8]) -> Result<(), liq_error> { + unsafe { + ffi::liq_image_set_importance_map(self.handle.as_mut(), map.as_ptr() as *mut _, map.len(), ffi::liq_ownership::LIQ_COPY_PIXELS).ok() + } + } + + #[inline] + fn into_raw(self) -> *mut ffi::liq_image { + let handle = self.handle; + mem::forget(self); + handle.as_ptr() + } +} + +impl QuantizationResult { + /// Set to 1.0 to get nice smooth image + #[inline] + pub fn set_dithering_level(&mut self, value: f32) -> liq_error { + unsafe { ffi::liq_set_dithering_level(self.handle.as_mut(), value) } + } + + /// The default is sRGB gamma (~1/2.2) + #[inline] + pub fn set_output_gamma(&mut self, value: f64) -> liq_error { + unsafe { ffi::liq_set_output_gamma(self.handle.as_mut(), value) } + } + + /// Approximate gamma correction value used for the output + /// + /// Colors are converted from input gamma to this gamma + #[inline] + #[must_use] + pub fn output_gamma(&mut self) -> f64 { + unsafe { ffi::liq_get_output_gamma(self.handle.as_ref()) } + } + + /// Number 0-100 guessing how nice the input image will look if remapped to this palette + #[inline] + #[must_use] + pub fn quantization_quality(&self) -> i32 { + unsafe { ffi::liq_get_quantization_quality(self.handle.as_ref()) as i32 } + } + + /// Approximate mean square error of the palette + #[inline] + #[must_use] + pub fn quantization_error(&self) -> Option { + match unsafe { ffi::liq_get_quantization_error(self.handle.as_ref()) } { + x if x < 0. => None, + x => Some(x), + } + } + + /// Final palette + /// + /// It's slighly better if you get palette from the `remapped()` call instead + #[must_use] + pub fn palette(&mut self) -> Vec { + let pal = self.palette_ref(); + let mut out: Vec = FallibleVec::try_with_capacity(pal.len()).unwrap(); + out.extend_from_slice(pal); + out + } + + /// Final palette (as a temporary slice) + /// + /// It's slighly better if you get palette from the `remapped()` call instead + /// + /// Use when ownership of the palette colors is not needed + #[inline] + pub fn palette_ref(&mut self) -> &[Color] { + unsafe { + let pal = &*ffi::liq_get_palette(self.handle.as_mut()); + std::slice::from_raw_parts(pal.entries.as_ptr(), (pal.count as usize).min(pal.entries.len())) + } + } + + /// Remap image into a `Vec` + /// + /// Returns the palette and a 1-byte-per-pixel uncompressed bitmap + pub fn remapped(&mut self, image: &mut Image<'_>) -> Result<(Vec, Vec), liq_error> { + let len = image.width() * image.height(); + // Capacity is essential here, as it creates uninitialized buffer + unsafe { + let mut buf: Vec = FallibleVec::try_with_capacity(len).map_err(|_| liq_error::LIQ_OUT_OF_MEMORY)?; + let uninit_slice = std::slice::from_raw_parts_mut(buf.as_ptr() as *mut MaybeUninit, buf.capacity()); + self.remap_into(image, uninit_slice)?; + buf.set_len(uninit_slice.len()); + Ok((self.palette(), buf)) + } + } + + /// Remap image into an existing buffer. + /// + /// This is a low-level call for use when existing memory has to be reused. Use `remapped()` if possible. + /// + /// Writes 1-byte-per-pixel uncompressed bitmap into the pre-allocated buffer. + /// + /// You should call `palette()` or `palette_ref()` _after_ this call, but not before it, + /// because remapping changes the palette. + #[inline] + pub fn remap_into(&mut self, image: &mut Image<'_>, output_buf: &mut [MaybeUninit]) -> Result<(), liq_error> { + unsafe { + match ffi::liq_write_remapped_image(self.handle.as_mut(), image.handle.as_mut(), output_buf.as_mut_ptr().cast(), output_buf.len()) { + LIQ_OK => Ok(()), + err => Err(err), + } + } + } +} + +impl fmt::Debug for QuantizationResult { + #[cold] + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "QuantizationResult(q={})", self.quantization_quality()) + } +} + +unsafe impl Send for Attributes {} +unsafe impl Send for QuantizationResult {} +unsafe impl<'bitmap> Send for Image<'bitmap> {} +unsafe impl<'a> Send for Histogram<'a> {} + +#[test] +fn copy_img() { + let tmp = vec![RGBA::new(1,2,3,4); 10*100]; + let liq = Attributes::new(); + let _ = liq.new_image_stride_copy(&tmp, 10, 100, 10, 0.).unwrap(); +} + +#[test] +fn takes_rgba() { + let liq = Attributes::new(); + + use rgb::RGBA8 as RGBA; + let img = vec![RGBA {r:0, g:0, b:0, a:0}; 8]; + + + liq.new_image(&img, 1, 1, 0.0).unwrap(); + liq.new_image(&img, 4, 2, 0.0).unwrap(); + liq.new_image(&img, 8, 1, 0.0).unwrap(); + assert!(liq.new_image(&img, 9, 1, 0.0).is_err()); + assert!(liq.new_image(&img, 4, 3, 0.0).is_err()); +} + +#[test] +fn histogram() { + let attr = Attributes::new(); + let mut hist = attr.new_histogram(); + + let bitmap1 = vec![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(&mut image1); + + let bitmap2 = vec![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(&mut image2); + + hist.add_colors(&[HistogramEntry{ + color: Color::new(255,128,255,128), + count: 10, + }], 0.0); + + let mut res = hist.quantize().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); + liq.set_quality(70, 99); + liq.set_min_posterization(1); + assert_eq!(1, liq.min_posterization()); + liq.set_min_posterization(0); + + use std::sync::atomic::Ordering::SeqCst; + use std::sync::atomic::AtomicBool; + use std::sync::Arc; + + let log_called = Arc::new(AtomicBool::new(false)); + let log_called2 = log_called.clone(); + liq.set_log_callback(move |_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 ref mut img = 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); + + // 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()); + assert_eq!(Color { r: 255, g: 255, b: 255, a: 255 }, palette[0]); + assert_eq!(Color { 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() { + use crate::ffi::liq_color as RGBA; + let mut liq = new(); + let bitmap = &[RGBA::new(255, 0, 0, 255), RGBA::new(0u8, 0, 255, 255)]; + let ref 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(img).unwrap(); + let pal = res.palette(); + assert_eq!(1, pal.len()); + 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(&b, 1, 1, 0.).unwrap(); + }).join().unwrap(); +} + +#[test] +fn callback_test() { + let mut called = 0; + let mut res = { + let mut a = new(); + unsafe extern "C" fn get_row(output_row: *mut Color, y: c_int, width: c_int, user_data: *mut i32) { + assert!(y >= 0 && y < 5); + assert_eq!(123, width); + for i in 0..width as isize { + let n = i as u8; + *output_row.offset(i as isize) = Color::new(n,n,n,n); + } + *user_data += 1; + } + let mut img = Image::new_unsafe_fn(&a, get_row, &mut called, 123, 5, 0.).unwrap(); + a.quantize(&mut img).unwrap() + }; + assert!(called > 5 && called < 50); + assert_eq!(123, res.palette().len()); +} + +#[test] +fn custom_allocator_test() { + // SAFETY: This is all in one thread. + static mut ALLOC_COUNTR: usize = 0; + static mut FREE_COUNTR: usize = 0; + + unsafe extern "C" fn test_malloc(size: usize) -> *mut c_void { + ALLOC_COUNTR += 1; + libc::malloc(size) + } + + unsafe extern "C" fn test_free(ptr: *mut c_void) { + FREE_COUNTR += 1; + libc::free(ptr) + } + + let liq = unsafe { Attributes::with_allocator(test_malloc, test_free) }; + assert_eq!(unsafe { ALLOC_COUNTR }, 1); + assert_eq!(unsafe { FREE_COUNTR }, 0); + + let liq2 = liq.clone(); + assert_eq!(liq.malloc, liq2.malloc); + assert_eq!(liq.free, liq2.free); + + drop(liq); + assert_eq!(unsafe { ALLOC_COUNTR }, 2); + assert_eq!(unsafe { FREE_COUNTR }, 1); + + drop(liq2); + assert_eq!(unsafe { ALLOC_COUNTR }, 2); + assert_eq!(unsafe { FREE_COUNTR }, 2); +} diff --git a/rust/build.rs b/rust-sys/build.rs similarity index 77% rename from rust/build.rs rename to rust-sys/build.rs index ed7a781..35b9de3 100644 --- a/rust/build.rs +++ b/rust-sys/build.rs @@ -4,20 +4,23 @@ extern crate cc; use std::env; -use std::path::PathBuf; use std::fs::canonicalize; +use std::path::PathBuf; fn main() { let mut cc = cc::Build::new(); let compiler = cc.get_compiler(); cc.warnings(false); - if env::var("PROFILE").map(|p|p != "debug").unwrap_or(true) { + if env::var("PROFILE").map(|p| p != "debug").unwrap_or(true) { cc.define("NDEBUG", Some("1")); + } else { + cc.define("DEBUG", Some("1")); } if cfg!(feature = "openmp") { - cc.flag(&env::var("DEP_OPENMP_FLAG").unwrap()); + env::var("DEP_OPENMP_FLAG").expect("openmp-sys failed") + .split(" ").for_each(|f| { cc.flag(f); }); } let target_arch = env::var("CARGO_CFG_TARGET_ARCH").expect("Needs CARGO_CFG_TARGET_ARCH"); @@ -38,6 +41,7 @@ fn main() { .file("msvc-dist/mediancut.c") .file("msvc-dist/mempool.c") .file("msvc-dist/pam.c") + .file("msvc-dist/remap.c") .file("msvc-dist/blur.c"); } else { // This is so that I don't forget to publish MSVC version as well @@ -54,8 +58,17 @@ fn main() { .file("mediancut.c") .file("mempool.c") .file("pam.c") + .file("remap.c") .file("blur.c"); } cc.compile("libimagequant.a"); + + if cfg!(feature = "openmp") { + if let Some(link) = env::var_os("DEP_OPENMP_CARGO_LINK_INSTRUCTIONS") { + for i in env::split_paths(&link) { + println!("cargo:{}", i.display()); + } + } + } } diff --git a/rust/libimagequant.rs b/rust-sys/libimagequant.rs similarity index 90% rename from rust/libimagequant.rs rename to rust-sys/libimagequant.rs index 6e195ea..c2aa625 100644 --- a/rust/libimagequant.rs +++ b/rust-sys/libimagequant.rs @@ -31,17 +31,14 @@ //! //! 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; #[cfg(feature = "openmp")] extern crate openmp_sys; -use std::os::raw::{c_int, c_uint, c_char, c_void}; use std::error; use std::fmt; -use std::error::Error; +use std::os::raw::{c_char, c_int, c_uint, c_void}; pub enum liq_attr {} pub enum liq_image {} @@ -63,12 +60,16 @@ pub enum liq_error { LIQ_UNSUPPORTED, } -#[repr(C)] -#[derive(Copy, Clone)] -pub enum liq_ownership { - LIQ_OWN_ROWS = 4, - LIQ_OWN_PIXELS = 8, - LIQ_COPY_PIXELS = 16, +bitflags::bitflags! { + #[repr(C)] + 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(C)] @@ -84,25 +85,21 @@ pub struct liq_histogram_entry { pub count: c_uint, } -impl error::Error for liq_error { - fn description(&self) -> &str { - match *self { +impl error::Error for liq_error {} + +impl fmt::Display for liq_error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match *self { liq_error::LIQ_OK => "OK", - liq_error::LIQ_QUALITY_TOO_LOW => "LIQ_QUALITY_TOO_LOW", + liq_error::LIQ_QUALITY_TOO_LOW => "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_ABORTED => "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()) + liq_error::LIQ_UNSUPPORTED => "UNSUPPORTED", + }) } } @@ -142,12 +139,11 @@ impl liq_error { } } -pub type liq_log_callback_function = Option; -pub type liq_log_flush_callback_function = Option; +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 = unsafe extern "C" fn(row_out: *mut liq_color, row: c_int, width: c_int, user_info: *mut c_void); -#[link(name="imagequant", kind="static")] extern "C" { /// Returns object that will hold initial settings (attributes) for the library. @@ -155,6 +151,7 @@ extern "C" { /// 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_create_with_allocator(malloc: unsafe extern "C" fn(usize) -> *mut c_void, free: unsafe extern "C" fn(*mut c_void)) -> *mut liq_attr; pub fn liq_attr_copy(orig: &liq_attr) -> *mut liq_attr; pub fn liq_attr_destroy(attr: &mut liq_attr); @@ -203,10 +200,10 @@ extern "C" { /// unsafe: It will crash if the owned memory wasn't allocated using `libc::malloc()` (or whatever allocator C side is using) pub fn liq_image_set_memory_ownership(image: &liq_image, own: liq_ownership) -> liq_error; - 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_set_log_callback(liq: &mut liq_attr, cb: liq_log_callback_function, user_info: *mut c_void); + pub fn liq_set_log_flush_callback(liq: &mut liq_attr, cb: liq_log_flush_callback_function, user_info: *mut c_void); + pub fn liq_attr_set_progress_callback(liq: &mut liq_attr, cb: liq_progress_callback_function, user_info: *mut c_void); + pub fn liq_result_set_progress_callback(arg1: &mut liq_result, cb: 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; /// Remap assuming the image will be always presented exactly on top of this background. /// @@ -291,6 +288,11 @@ extern "C" { pub fn liq_version() -> c_int; } +#[test] +fn ownership_bitflags() { + assert_eq!(4+16, (liq_ownership::LIQ_OWN_ROWS | liq_ownership::LIQ_COPY_PIXELS).bits()); +} + #[test] fn links_and_runs() { use std::ptr; diff --git a/tests/frame-6-a.png b/tests/frame-6-a.png new file mode 100644 index 0000000..8539d89 Binary files /dev/null and b/tests/frame-6-a.png differ diff --git a/tests/frame-6-bg.png b/tests/frame-6-bg.png new file mode 100644 index 0000000..8a97993 Binary files /dev/null and b/tests/frame-6-bg.png differ diff --git a/tests/frame-6-pal.png b/tests/frame-6-pal.png new file mode 100644 index 0000000..9aea8d6 Binary files /dev/null and b/tests/frame-6-pal.png differ diff --git a/tests/frame-7-a.png b/tests/frame-7-a.png new file mode 100644 index 0000000..8539d89 Binary files /dev/null and b/tests/frame-7-a.png differ diff --git a/tests/frame-7-bg.png b/tests/frame-7-bg.png new file mode 100644 index 0000000..8b1730c Binary files /dev/null and b/tests/frame-7-bg.png differ diff --git a/tests/frame-7-pal.png b/tests/frame-7-pal.png new file mode 100644 index 0000000..265e4df Binary files /dev/null and b/tests/frame-7-pal.png differ diff --git a/tests/tests.rs b/tests/tests.rs new file mode 100644 index 0000000..6c10bcb --- /dev/null +++ b/tests/tests.rs @@ -0,0 +1,87 @@ +use rgb::ComponentMap; +use imagequant::*; + + +#[test] +fn remap_bg6() { + let fg1 = lodepng::decode32_file("tests/frame-6-a.png").unwrap(); + let bg1 = lodepng::decode32_file("tests/frame-6-bg.png").unwrap(); + let pal = lodepng::decode32_file("tests/frame-6-pal.png").unwrap(); + + let mut attr = new(); + let mut fg = attr.new_image_stride(&fg1.buffer, fg1.width, fg1.height, fg1.width, 0.).unwrap(); + let bg = attr.new_image_stride(&bg1.buffer, bg1.width, bg1.height, bg1.width, 0.).unwrap(); + fg.set_background(bg).unwrap(); + for c in &pal.buffer { + fg.add_fixed_color(*c).unwrap(); + } + attr.set_max_colors(pal.buffer.len() as _).unwrap(); + let mut res = attr.quantize(&mut fg).unwrap(); + res.set_dithering_level(1.).unwrap(); + let (pal, idx) = res.remapped(&mut fg).unwrap(); + + let buf: Vec<_> = idx.iter().zip(bg1.buffer.iter()).map(|(px, bg)| { + let palpx = pal[*px as usize]; + if palpx.a > 0 { + palpx + } else { + *bg + } + }).collect(); + lodepng::encode32_file("/tmp/testr2-r6.png", &buf, fg.width(), fg.height()).unwrap(); + + assert!(idx.iter().zip(bg1.buffer.iter()).map(|(px, bg)| { + let palpx = pal[*px as usize]; + if palpx.a > 0 { + palpx + } else { + *bg + } + }).zip(&fg1.buffer).all(|(px, fg)| { + let d = px.map(|c| c as i16) - fg.map(|c| c as i16); + d.map(|c| (c as i32).pow(2) as u32); + d.r + d.g + d.b + d.a < 120 + })); +} + +#[test] +fn remap_bg7() { + let fg1 = lodepng::decode32_file("tests/frame-7-a.png").unwrap(); + let bg1 = lodepng::decode32_file("tests/frame-7-bg.png").unwrap(); + let pal = lodepng::decode32_file("tests/frame-7-pal.png").unwrap(); + + let mut attr = new(); + let mut fg = attr.new_image_stride(&fg1.buffer, fg1.width, fg1.height, fg1.width, 0.).unwrap(); + let bg = attr.new_image_stride(&bg1.buffer, bg1.width, bg1.height, bg1.width, 0.).unwrap(); + fg.set_background(bg).unwrap(); + for c in &pal.buffer { + fg.add_fixed_color(*c).unwrap(); + } + attr.set_max_colors(pal.buffer.len() as _).unwrap(); + let mut res = attr.quantize(&mut fg).unwrap(); + res.set_dithering_level(0.).unwrap(); + let (pal, idx) = res.remapped(&mut fg).unwrap(); + + let buf: Vec<_> = idx.iter().zip(bg1.buffer.iter()).map(|(px, bg)| { + let palpx = pal[*px as usize]; + if palpx.a > 0 { + palpx + } else { + *bg + } + }).collect(); + lodepng::encode32_file("/tmp/testr2-r7.png", &buf, fg.width(), fg.height()).unwrap(); + + assert!(idx.iter().zip(bg1.buffer.iter()).map(|(px, bg)| { + let palpx = pal[*px as usize]; + if palpx.a > 0 { + palpx + } else { + *bg + } + }).zip(&fg1.buffer).all(|(px, fg)| { + let d = px.map(|c| c as i16) - fg.map(|c| c as i16); + d.map(|c| (c as i32).pow(2) as u32); + d.r + d.g + d.b + d.a < 160 + })); +} diff --git a/version.txt b/version.txt new file mode 100644 index 0000000..3f8eb71 --- /dev/null +++ b/version.txt @@ -0,0 +1 @@ +2.17.1