gstreamer/gst/dvbsubenc/libimagequant/libimagequant.c
Jan Schmidt 1cf3cae5e1 dvbsubenc: Add DVB Subtitle encoder
Add an element that converts AYUV video frames to a DVB
subpicture stream.

It's fairly simple for now. Later it would be good to support
input via a stream that contains only GstVideoOverlayComposition
meta.

The element searches each input video frame for the largest
sub-region containing non-transparent pixels and encodes that
as a single DVB subpicture region. It can also do palette
reduction of the input frames using code taken from
libimagequant.

There are various FIXME for potential improvements for now, but
it works.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-bad/-/merge_requests/1227>
2020-06-17 12:50:13 +10:00

2055 lines
60 KiB
C

/* pngquant.c - quantize the colors in an alphamap down to a specified number
**
** Copyright (C) 1989, 1991 by Jef Poskanzer.
** Copyright (C) 1997, 2000, 2002 by Greg Roelofs; based on an idea by
** Stefan Schneider.
** © 2009-2013 by Kornel Lesinski.
**
** Permission to use, copy, modify, and distribute this software and its
** documentation for any purpose and without fee is hereby granted, provided
** that the above copyright notice appear in all copies and that both that
** copyright notice and this permission notice appear in supporting
** documentation. This software is provided "as is" without express or
** implied warranty.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stdint.h>
#include <limits.h>
#if !(defined(__STDC_VERSION__) && __STDC_VERSION__ >= 199900L) && !(defined(_MSC_VER) && _MSC_VER >= 1800)
#error "This program requires C99, e.g. -std=c99 switch in GCC or it requires MSVC 18.0 or higher."
#error "Ignore torrent of syntax errors that may follow. It's only because compiler is set to use too old C version."
#endif
#ifdef _OPENMP
#include <omp.h>
#else
#define omp_get_max_threads() 1
#define omp_get_thread_num() 0
#endif
#include "libimagequant.h"
#include "pam.h"
#include "mediancut.h"
#include "nearest.h"
#include "blur.h"
#include "viter.h"
#define LIQ_HIGH_MEMORY_LIMIT (1<<26) /* avoid allocating buffers larger than 64MB */
// each structure has a pointer as a unique identifier that allows type checking at run time
static const char *const liq_attr_magic = "liq_attr", *const liq_image_magic =
"liq_image", *const liq_result_magic =
"liq_result", *const liq_remapping_result_magic =
"liq_remapping_result", *const liq_freed_magic = "free";
#define CHECK_STRUCT_TYPE(attr, kind) liq_crash_if_invalid_handle_pointer_given((const liq_attr*)attr, kind ## _magic)
#define CHECK_USER_POINTER(ptr) liq_crash_if_invalid_pointer_given(ptr)
struct liq_attr
{
const char *magic_header;
void *(*malloc) (size_t);
void (*free) (void *);
double target_mse, max_mse, voronoi_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 voronoi_iterations, feedback_loop_trials;
bool last_index_transparent, use_contrast_maps, use_dither_map, fast_palette;
unsigned int speed;
liq_log_callback_function *log_callback;
void *log_callback_user_info;
liq_log_flush_callback_function *log_flush_callback;
void *log_flush_callback_user_info;
};
struct liq_image
{
const char *magic_header;
void *(*malloc) (size_t);
void (*free) (void *);
f_pixel *f_pixels;
rgba_pixel **rows;
double gamma;
unsigned int width, height;
unsigned char *noise, *edges, *dither_map;
rgba_pixel *pixels, *temp_row;
f_pixel *temp_f_row;
liq_image_get_rgba_row_callback *row_callback;
void *row_callback_user_info;
float min_opaque_val;
f_pixel fixed_colors[256];
unsigned short fixed_colors_count;
bool free_pixels, free_rows, free_rows_internal;
};
typedef struct liq_remapping_result
{
const char *magic_header;
void *(*malloc) (size_t);
void (*free) (void *);
unsigned char *pixels;
colormap *palette;
liq_palette int_palette;
double gamma, palette_error;
float dither_level;
bool use_dither_map;
} liq_remapping_result;
struct liq_result
{
const char *magic_header;
void *(*malloc) (size_t);
void (*free) (void *);
liq_remapping_result *remapping;
colormap *palette;
liq_palette int_palette;
float dither_level;
double gamma, palette_error;
int min_posterization_output;
bool use_dither_map, fast_palette;
};
static liq_result *pngquant_quantize (histogram * hist,
const liq_attr * options, const liq_image * img);
static void modify_alpha (liq_image * input_image,
rgba_pixel * const row_pixels);
static void contrast_maps (liq_image * image);
static histogram *get_histogram (liq_image * input_image,
const liq_attr * options);
static const rgba_pixel *liq_image_get_row_rgba (liq_image * input_image,
unsigned int row);
static const f_pixel *liq_image_get_row_f (liq_image * input_image,
unsigned int row);
static void liq_remapping_result_destroy (liq_remapping_result * result);
static void
liq_verbose_printf (const liq_attr * context, const char *fmt, ...)
{
if (context->log_callback) {
va_list va;
int required_space;
char *buf;
va_start (va, fmt);
required_space = vsnprintf (NULL, 0, fmt, va) + 1; // +\0
va_end (va);
buf = g_alloca (required_space);
va_start (va, fmt);
vsnprintf (buf, required_space, fmt, va);
va_end (va);
context->log_callback (context, buf, context->log_callback_user_info);
}
}
inline static void
verbose_print (const liq_attr * attr, const char *msg)
{
if (attr->log_callback) {
attr->log_callback (attr, msg, attr->log_callback_user_info);
}
}
static void
liq_verbose_printf_flush (liq_attr * attr)
{
if (attr->log_flush_callback) {
attr->log_flush_callback (attr, attr->log_flush_callback_user_info);
}
}
#if USE_SSE
inline static bool
is_sse_available (void)
{
#if (defined(__x86_64__) || defined(__amd64))
return true;
#else
int a, b, c, d;
cpuid (1, a, b, c, d);
return d & (1 << 25); // edx bit 25 is set when SSE is present
#endif
}
#endif
/* make it clear in backtrace when user-supplied handle points to invalid memory */
NEVER_INLINE LIQ_EXPORT bool liq_crash_if_invalid_handle_pointer_given (const
liq_attr * user_supplied_pointer, const char *const expected_magic_header);
LIQ_EXPORT bool
liq_crash_if_invalid_handle_pointer_given (const liq_attr *
user_supplied_pointer, const char *const expected_magic_header)
{
if (!user_supplied_pointer) {
return false;
}
if (user_supplied_pointer->magic_header == liq_freed_magic) {
fprintf (stderr, "%s used after being freed", expected_magic_header);
// this is not normal error handling, this is programmer error that should crash the program.
// program cannot safely continue if memory has been used after it's been freed.
// abort() is nasty, but security vulnerability may be worse.
abort ();
}
return user_supplied_pointer->magic_header == expected_magic_header;
}
NEVER_INLINE LIQ_EXPORT bool liq_crash_if_invalid_pointer_given (void *pointer);
LIQ_EXPORT bool
liq_crash_if_invalid_pointer_given (void *pointer)
{
char test_access;
if (!pointer) {
return false;
}
// Force a read from the given (potentially invalid) memory location in order to check early whether this crashes the program or not.
// It doesn't matter what value is read, the code here is just to shut the compiler up about unused read.
test_access = *((volatile char *) pointer);
return test_access || true;
}
static void
liq_log_error (const liq_attr * attr, const char *msg)
{
if (!CHECK_STRUCT_TYPE (attr, liq_attr))
return;
liq_verbose_printf (attr, " error: %s", msg);
}
static double
quality_to_mse (long quality)
{
const double extra_low_quality_fudge =
MAX (0, 0.016 / (0.001 + quality) - 0.001);
if (quality == 0) {
return MAX_DIFF;
}
if (quality == 100) {
return 0;
}
// curve fudged to be roughly similar to quality of libjpeg
// except lowest 10 for really low number of colors
return extra_low_quality_fudge + 2.5 / pow (210.0 + quality,
1.2) * (100.1 - quality) / 100.0;
}
static unsigned int
mse_to_quality (double mse)
{
int i;
for (i = 100; i > 0; i--) {
if (mse <= quality_to_mse (i) + 0.000001) { // + epsilon for floating point errors
return i;
}
}
return 0;
}
LIQ_EXPORT liq_error
liq_set_quality (liq_attr * attr, int minimum, int target)
{
if (!CHECK_STRUCT_TYPE (attr, liq_attr))
return LIQ_INVALID_POINTER;
if (target < 0 || target > 100 || target < minimum || minimum < 0)
return LIQ_VALUE_OUT_OF_RANGE;
attr->target_mse = quality_to_mse (target);
attr->max_mse = quality_to_mse (minimum);
return LIQ_OK;
}
LIQ_EXPORT int
liq_get_min_quality (const liq_attr * attr)
{
if (!CHECK_STRUCT_TYPE (attr, liq_attr))
return -1;
return mse_to_quality (attr->max_mse);
}
LIQ_EXPORT int
liq_get_max_quality (const liq_attr * attr)
{
if (!CHECK_STRUCT_TYPE (attr, liq_attr))
return -1;
return mse_to_quality (attr->target_mse);
}
LIQ_EXPORT liq_error
liq_set_max_colors (liq_attr * attr, int colors)
{
if (!CHECK_STRUCT_TYPE (attr, liq_attr))
return LIQ_INVALID_POINTER;
if (colors < 2 || colors > 256)
return LIQ_VALUE_OUT_OF_RANGE;
attr->max_colors = colors;
return LIQ_OK;
}
LIQ_EXPORT int
liq_get_max_colors (const liq_attr * attr)
{
if (!CHECK_STRUCT_TYPE (attr, liq_attr))
return -1;
return attr->max_colors;
}
LIQ_EXPORT liq_error
liq_set_min_posterization (liq_attr * attr, int bits)
{
if (!CHECK_STRUCT_TYPE (attr, liq_attr))
return LIQ_INVALID_POINTER;
if (bits < 0 || bits > 4)
return LIQ_VALUE_OUT_OF_RANGE;
attr->min_posterization_output = bits;
return LIQ_OK;
}
LIQ_EXPORT int
liq_get_min_posterization (const liq_attr * attr)
{
if (!CHECK_STRUCT_TYPE (attr, liq_attr))
return -1;
return attr->min_posterization_output;
}
LIQ_EXPORT liq_error
liq_set_speed (liq_attr * attr, int speed)
{
int iterations;
if (!CHECK_STRUCT_TYPE (attr, liq_attr))
return LIQ_INVALID_POINTER;
if (speed < 1 || speed > 10)
return LIQ_VALUE_OUT_OF_RANGE;
iterations = MAX (8 - speed, 0);
iterations += iterations * iterations / 2;
attr->voronoi_iterations = iterations;
attr->voronoi_iteration_limit = 1.0 / (double) (1 << (23 - speed));
attr->feedback_loop_trials = MAX (56 - 9 * speed, 0);
attr->max_histogram_entries = (1 << 17) + (1 << 18) * (10 - speed);
attr->min_posterization_input = (speed >= 8) ? 1 : 0;
attr->fast_palette = (speed >= 7);
attr->use_dither_map = (speed <= (omp_get_max_threads () > 1 ? 7 : 5)); // parallelized dither map might speed up floyd remapping
attr->use_contrast_maps = (speed <= 7) || attr->use_dither_map;
attr->speed = speed;
return LIQ_OK;
}
LIQ_EXPORT int
liq_get_speed (const liq_attr * attr)
{
if (!CHECK_STRUCT_TYPE (attr, liq_attr))
return -1;
return attr->speed;
}
LIQ_EXPORT liq_error
liq_set_output_gamma (liq_result * res, double gamma)
{
if (!CHECK_STRUCT_TYPE (res, liq_result))
return LIQ_INVALID_POINTER;
if (gamma <= 0 || gamma >= 1.0)
return LIQ_VALUE_OUT_OF_RANGE;
if (res->remapping) {
liq_remapping_result_destroy (res->remapping);
res->remapping = NULL;
}
res->gamma = gamma;
return LIQ_OK;
}
LIQ_EXPORT liq_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 int
liq_get_min_opacity (const liq_attr * attr)
{
if (!CHECK_STRUCT_TYPE (attr, liq_attr))
return -1;
return MIN (255, 256.0 * attr->min_opaque_val);
}
LIQ_EXPORT void
liq_set_last_index_transparent (liq_attr * attr, int is_last)
{
if (!CHECK_STRUCT_TYPE (attr, liq_attr))
return;
attr->last_index_transparent = ! !is_last;
}
LIQ_EXPORT void
liq_set_log_callback (liq_attr * attr, liq_log_callback_function * callback,
void *user_info)
{
if (!CHECK_STRUCT_TYPE (attr, liq_attr))
return;
liq_verbose_printf_flush (attr);
attr->log_callback = callback;
attr->log_callback_user_info = user_info;
}
LIQ_EXPORT void
liq_set_log_flush_callback (liq_attr * attr,
liq_log_flush_callback_function * callback, void *user_info)
{
if (!CHECK_STRUCT_TYPE (attr, liq_attr))
return;
attr->log_flush_callback = callback;
attr->log_flush_callback_user_info = user_info;
}
LIQ_EXPORT liq_attr *
liq_attr_create (void)
{
return liq_attr_create_with_allocator (NULL, NULL);
}
LIQ_EXPORT void
liq_attr_destroy (liq_attr * attr)
{
if (!CHECK_STRUCT_TYPE (attr, liq_attr)) {
return;
}
liq_verbose_printf_flush (attr);
attr->magic_header = liq_freed_magic;
attr->free (attr);
}
LIQ_EXPORT liq_attr *
liq_attr_copy (liq_attr * orig)
{
liq_attr *attr;
if (!CHECK_STRUCT_TYPE (orig, liq_attr)) {
return NULL;
}
attr = orig->malloc (sizeof (liq_attr));
if (!attr)
return NULL;
*attr = *orig;
return attr;
}
static void *
liq_aligned_malloc (size_t size)
{
unsigned char *ptr = malloc (size + 16);
uintptr_t offset;
if (!ptr) {
return NULL;
}
offset = 16 - ((uintptr_t) ptr & 15); // also reserves 1 byte for ptr[-1]
ptr += offset;
assert (0 == (((uintptr_t) ptr) & 15));
ptr[-1] = offset ^ 0x59; // store how much pointer was shifted to get the original for free()
return ptr;
}
static void
liq_aligned_free (void *inptr)
{
unsigned char *ptr = inptr;
size_t offset = ptr[-1] ^ 0x59;
assert (offset > 0 && offset <= 16);
free (ptr - offset);
}
LIQ_EXPORT liq_attr *
liq_attr_create_with_allocator (void *(*custom_malloc) (size_t),
void (*custom_free) (void *))
{
liq_attr *attr;
#if USE_SSE
if (!is_sse_available ()) {
return NULL;
}
#endif
if (!custom_malloc && !custom_free) {
custom_malloc = liq_aligned_malloc;
custom_free = liq_aligned_free;
} else if (!custom_malloc != !custom_free) {
return NULL; // either specify both or none
}
attr = custom_malloc (sizeof (liq_attr));
if (!attr)
return NULL;
*attr = (liq_attr) {
.magic_header = liq_attr_magic,.malloc = custom_malloc,.free = custom_free,.max_colors = 256,.min_opaque_val = 1, // whether preserve opaque colors for IE (1.0=no, does not affect alpha)
.last_index_transparent = false, // puts transparent color at last index. This is workaround for blu-ray subtitles.
.target_mse = 0,.max_mse = MAX_DIFF,};
liq_set_speed (attr, 3);
return attr;
}
LIQ_EXPORT liq_error
liq_image_add_fixed_color (liq_image * img, liq_color color)
{
float gamma_lut[256];
rgba_pixel pix = (rgba_pixel) {
.r = color.r,
.g = color.g,
.b = color.b,
.a = color.a
};
if (!CHECK_STRUCT_TYPE (img, liq_image))
return LIQ_INVALID_POINTER;
if (img->fixed_colors_count > 255)
return LIQ_BUFFER_TOO_SMALL;
to_f_set_gamma (gamma_lut, img->gamma);
img->fixed_colors[img->fixed_colors_count++] = to_f (gamma_lut, pix);
return LIQ_OK;
}
static bool
liq_image_use_low_memory (liq_image * img)
{
img->temp_f_row =
img->malloc (sizeof (img->f_pixels[0]) * img->width *
omp_get_max_threads ());
return img->temp_f_row != NULL;
}
static bool
liq_image_should_use_low_memory (liq_image * img, const bool low_memory_hint)
{
return img->width * img->height > (low_memory_hint ? LIQ_HIGH_MEMORY_LIMIT / 8 : LIQ_HIGH_MEMORY_LIMIT) / sizeof (f_pixel); // Watch out for integer overflow
}
static liq_image *
liq_image_create_internal (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)
{
liq_image *img;
if (gamma < 0 || gamma > 1.0) {
liq_log_error (attr, "gamma must be >= 0 and <= 1 (try 1/gamma instead)");
return NULL;
}
if (!rows && !row_callback) {
liq_log_error (attr, "missing row data");
return NULL;
}
img = attr->malloc (sizeof (liq_image));
if (!img)
return NULL;
*img = (liq_image) {
.magic_header = liq_image_magic,.malloc = attr->malloc,.free =
attr->free,.width = width,.height = height,.gamma =
gamma ? gamma : 0.45455,.rows = rows,.row_callback =
row_callback,.row_callback_user_info =
row_callback_user_info,.min_opaque_val = attr->min_opaque_val,};
if (!rows || attr->min_opaque_val < 1.f) {
img->temp_row =
attr->malloc (sizeof (img->temp_row[0]) * width *
omp_get_max_threads ());
if (!img->temp_row)
return NULL;
}
// if image is huge or converted pixels are not likely to be reused then don't cache converted pixels
if (liq_image_should_use_low_memory (img, !img->temp_row
&& !attr->use_contrast_maps && !attr->use_dither_map)) {
verbose_print (attr, " conserving memory");
if (!liq_image_use_low_memory (img))
return NULL;
}
if (img->min_opaque_val < 1.f) {
verbose_print (attr,
" Working around IE6 bug by making image less transparent...");
}
return img;
}
LIQ_EXPORT liq_error
liq_image_set_memory_ownership (liq_image * img, int ownership_flags)
{
if (!CHECK_STRUCT_TYPE (img, liq_image))
return LIQ_INVALID_POINTER;
if (!img->rows || !ownership_flags
|| (ownership_flags & ~(LIQ_OWN_ROWS | LIQ_OWN_PIXELS))) {
return LIQ_VALUE_OUT_OF_RANGE;
}
if (ownership_flags & LIQ_OWN_ROWS) {
if (img->free_rows_internal)
return LIQ_VALUE_OUT_OF_RANGE;
img->free_rows = true;
}
if (ownership_flags & LIQ_OWN_PIXELS) {
img->free_pixels = true;
if (!img->pixels) {
// for simplicity of this API there's no explicit bitmap argument,
// so the row with the lowest address is assumed to be at the start of the bitmap
img->pixels = img->rows[0];
for (unsigned int i = 1; i < img->height; i++) {
img->pixels = MIN (img->pixels, img->rows[i]);
}
}
}
return LIQ_OK;
}
static bool
check_image_size (const liq_attr * attr, const int width, const int height)
{
if (!CHECK_STRUCT_TYPE (attr, liq_attr)) {
return false;
}
if (width <= 0 || height <= 0) {
liq_log_error (attr, "width and height must be > 0");
return false;
}
if (width > INT_MAX / height) {
liq_log_error (attr, "image too large");
return false;
}
return true;
}
LIQ_EXPORT liq_image *
liq_image_create_custom (liq_attr * attr,
liq_image_get_rgba_row_callback * row_callback, void *user_info, int width,
int height, double gamma)
{
if (!check_image_size (attr, width, height)) {
return NULL;
}
return liq_image_create_internal (attr, NULL, row_callback, user_info, width,
height, gamma);
}
LIQ_EXPORT liq_image *
liq_image_create_rgba_rows (liq_attr * attr, void *rows[], int width,
int height, double gamma)
{
if (!check_image_size (attr, width, height)) {
return NULL;
}
for (int i = 0; i < height; i++) {
if (!CHECK_USER_POINTER (rows + i) || !CHECK_USER_POINTER (rows[i])) {
liq_log_error (attr, "invalid row pointers");
return NULL;
}
}
return liq_image_create_internal (attr, (rgba_pixel **) rows, NULL, NULL,
width, height, gamma);
}
LIQ_EXPORT liq_image *
liq_image_create_rgba (liq_attr * attr, void *bitmap, int width, int height,
double gamma)
{
rgba_pixel *pixels;
rgba_pixel **rows;
liq_image *image;
if (!check_image_size (attr, width, height)) {
return NULL;
}
if (!CHECK_USER_POINTER (bitmap)) {
liq_log_error (attr, "invalid bitmap pointer");
return NULL;
}
pixels = bitmap;
rows = attr->malloc (sizeof (rows[0]) * height);
if (!rows)
return NULL;
for (int i = 0; i < height; i++) {
rows[i] = pixels + width * i;
}
image =
liq_image_create_internal (attr, rows, NULL, NULL, width, height, gamma);
image->free_rows = true;
image->free_rows_internal = true;
return image;
}
NEVER_INLINE LIQ_EXPORT void
liq_executing_user_callback (liq_image_get_rgba_row_callback * callback,
liq_color * temp_row, int row, int width, void *user_info);
LIQ_EXPORT void
liq_executing_user_callback (liq_image_get_rgba_row_callback * callback,
liq_color * temp_row, int row, int width, void *user_info)
{
assert (callback);
assert (temp_row);
callback (temp_row, row, width, user_info);
}
inline static bool
liq_image_can_use_rows (liq_image * img)
{
const bool iebug = img->min_opaque_val < 1.f;
return (img->rows && !iebug);
}
static const rgba_pixel *
liq_image_get_row_rgba (liq_image * img, unsigned int row)
{
rgba_pixel *temp_row;
if (liq_image_can_use_rows (img)) {
return img->rows[row];
}
assert (img->temp_row);
temp_row = img->temp_row + img->width * omp_get_thread_num ();
if (img->rows) {
memcpy (temp_row, img->rows[row], img->width * sizeof (temp_row[0]));
} else {
liq_executing_user_callback (img->row_callback, (liq_color *) temp_row, row,
img->width, img->row_callback_user_info);
}
if (img->min_opaque_val < 1.f)
modify_alpha (img, temp_row);
return temp_row;
}
static void
convert_row_to_f (liq_image * img, f_pixel * row_f_pixels,
const unsigned int row, const float gamma_lut[])
{
assert (row_f_pixels);
assert (!USE_SSE || 0 == ((uintptr_t) row_f_pixels & 15));
{
const rgba_pixel *const row_pixels = liq_image_get_row_rgba (img, row);
unsigned int col;
for (col = 0; col < img->width; col++) {
row_f_pixels[col] = to_f (gamma_lut, row_pixels[col]);
}
}
}
static const f_pixel *
liq_image_get_row_f (liq_image * img, unsigned int row)
{
if (!img->f_pixels) {
if (img->temp_f_row) {
float gamma_lut[256];
f_pixel *row_for_thread;
to_f_set_gamma (gamma_lut, img->gamma);
row_for_thread = img->temp_f_row + img->width * omp_get_thread_num ();
convert_row_to_f (img, row_for_thread, row, gamma_lut);
return row_for_thread;
}
assert (omp_get_thread_num () == 0);
if (!liq_image_should_use_low_memory (img, false)) {
img->f_pixels =
img->malloc (sizeof (img->f_pixels[0]) * img->width * img->height);
}
if (!img->f_pixels) {
if (!liq_image_use_low_memory (img))
return NULL;
return liq_image_get_row_f (img, row);
}
{
float gamma_lut[256];
to_f_set_gamma (gamma_lut, img->gamma);
for (unsigned int i = 0; i < img->height; i++) {
convert_row_to_f (img, &img->f_pixels[i * img->width], i, gamma_lut);
}
}
}
return img->f_pixels + img->width * row;
}
LIQ_EXPORT int
liq_image_get_width (const liq_image * input_image)
{
if (!CHECK_STRUCT_TYPE (input_image, liq_image))
return -1;
return input_image->width;
}
LIQ_EXPORT int
liq_image_get_height (const liq_image * input_image)
{
if (!CHECK_STRUCT_TYPE (input_image, liq_image))
return -1;
return input_image->height;
}
typedef void free_func (void *);
static free_func *
get_default_free_func (liq_image * img)
{
// When default allocator is used then user-supplied pointers must be freed with free()
if (img->free_rows_internal || img->free != liq_aligned_free) {
return img->free;
}
return free;
}
static void
liq_image_free_rgba_source (liq_image * input_image)
{
if (input_image->free_pixels && input_image->pixels) {
get_default_free_func (input_image) (input_image->pixels);
input_image->pixels = NULL;
}
if (input_image->free_rows && input_image->rows) {
get_default_free_func (input_image) (input_image->rows);
input_image->rows = NULL;
}
}
LIQ_EXPORT void
liq_image_destroy (liq_image * input_image)
{
if (!CHECK_STRUCT_TYPE (input_image, liq_image))
return;
liq_image_free_rgba_source (input_image);
if (input_image->noise) {
input_image->free (input_image->noise);
}
if (input_image->edges) {
input_image->free (input_image->edges);
}
if (input_image->dither_map) {
input_image->free (input_image->dither_map);
}
if (input_image->f_pixels) {
input_image->free (input_image->f_pixels);
}
if (input_image->temp_row) {
input_image->free (input_image->temp_row);
}
if (input_image->temp_f_row) {
input_image->free (input_image->temp_f_row);
}
input_image->magic_header = liq_freed_magic;
input_image->free (input_image);
}
LIQ_EXPORT liq_result *
liq_quantize_image (liq_attr * attr, liq_image * img)
{
histogram *hist;
liq_result *result;
if (!CHECK_STRUCT_TYPE (attr, liq_attr))
return NULL;
if (!CHECK_STRUCT_TYPE (img, liq_image)) {
liq_log_error (attr, "invalid image pointer");
return NULL;
}
hist = get_histogram (img, attr);
if (!hist) {
return NULL;
}
result = pngquant_quantize (hist, attr, img);
pam_freeacolorhist (hist);
return result;
}
LIQ_EXPORT liq_error
liq_set_dithering_level (liq_result * res, float dither_level)
{
if (!CHECK_STRUCT_TYPE (res, liq_result))
return LIQ_INVALID_POINTER;
if (res->remapping) {
liq_remapping_result_destroy (res->remapping);
res->remapping = NULL;
}
if (res->dither_level < 0 || res->dither_level > 1.0f)
return LIQ_VALUE_OUT_OF_RANGE;
res->dither_level = dither_level;
return LIQ_OK;
}
static liq_remapping_result *
liq_remapping_result_create (liq_result * result)
{
liq_remapping_result *res;
if (!CHECK_STRUCT_TYPE (result, liq_result)) {
return NULL;
}
res = result->malloc (sizeof (liq_remapping_result));
if (!res)
return NULL;
*res = (liq_remapping_result) {
.magic_header = liq_remapping_result_magic,.malloc = result->malloc,.free =
result->free,.dither_level = result->dither_level,.use_dither_map =
result->use_dither_map,.palette_error = result->palette_error,.gamma =
result->gamma,.palette = pam_duplicate_colormap (result->palette),};
return res;
}
LIQ_EXPORT double
liq_get_output_gamma (const liq_result * result)
{
if (!CHECK_STRUCT_TYPE (result, liq_result))
return -1;
return result->gamma;
}
static void
liq_remapping_result_destroy (liq_remapping_result * result)
{
if (!CHECK_STRUCT_TYPE (result, liq_remapping_result))
return;
if (result->palette)
pam_freecolormap (result->palette);
if (result->pixels)
result->free (result->pixels);
result->magic_header = liq_freed_magic;
result->free (result);
}
LIQ_EXPORT void
liq_result_destroy (liq_result * res)
{
if (!CHECK_STRUCT_TYPE (res, liq_result))
return;
memset (&res->int_palette, 0, sizeof (liq_palette));
if (res->remapping) {
memset (&res->remapping->int_palette, 0, sizeof (liq_palette));
liq_remapping_result_destroy (res->remapping);
}
pam_freecolormap (res->palette);
res->magic_header = liq_freed_magic;
res->free (res);
}
LIQ_EXPORT double
liq_get_quantization_error (liq_result * result)
{
if (!CHECK_STRUCT_TYPE (result, liq_result))
return -1;
if (result->palette_error >= 0) {
return result->palette_error * 65536.0 / 6.0;
}
if (result->remapping && result->remapping->palette_error >= 0) {
return result->remapping->palette_error * 65536.0 / 6.0;
}
return result->palette_error;
}
LIQ_EXPORT int
liq_get_quantization_quality (liq_result * result)
{
if (!CHECK_STRUCT_TYPE (result, liq_result))
return -1;
if (result->palette_error >= 0) {
return mse_to_quality (result->palette_error);
}
if (result->remapping && result->remapping->palette_error >= 0) {
return mse_to_quality (result->remapping->palette_error);
}
return result->palette_error;
}
static int
compare_popularity (const void *ch1, const void *ch2)
{
const float v1 = ((const colormap_item *) ch1)->popularity;
const float v2 = ((const colormap_item *) ch2)->popularity;
return v1 > v2 ? -1 : 1;
}
static void
sort_palette_qsort (colormap * map, int start, int nelem)
{
qsort (map->palette + start, nelem, sizeof (map->palette[0]),
compare_popularity);
}
#define SWAP_PALETTE(map, a,b) { \
const colormap_item tmp = (map)->palette[(a)]; \
(map)->palette[(a)] = (map)->palette[(b)]; \
(map)->palette[(b)] = tmp; }
static void
sort_palette (colormap * map, const liq_attr * options)
{
unsigned int i;
unsigned int num_transparent;
/*
** Step 3.5 [GRR]: remap the palette colors so that all entries with
** the maximal alpha value (i.e., fully opaque) are at the end and can
** therefore be omitted from the tRNS chunk.
*/
if (options->last_index_transparent) {
for (i = 0; i < map->colors; i++) {
if (map->palette[i].acolor.a < 1.0 / 256.0) {
const unsigned int old = i, transparent_dest = map->colors - 1;
SWAP_PALETTE (map, transparent_dest, old);
/* colors sorted by popularity make pngs slightly more compressible */
sort_palette_qsort (map, 0, map->colors - 1);
return;
}
}
}
/* move transparent colors to the beginning to shrink trns chunk */
num_transparent = 0;
for (i = 0; i < map->colors; i++) {
if (map->palette[i].acolor.a < 255.0 / 256.0) {
// current transparent color is swapped with earlier opaque one
if (i != num_transparent) {
SWAP_PALETTE (map, num_transparent, i);
i--;
}
num_transparent++;
}
}
liq_verbose_printf (options,
" eliminated opaque tRNS-chunk entries...%d entr%s transparent",
num_transparent, (num_transparent == 1) ? "y" : "ies");
/* colors sorted by popularity make pngs slightly more compressible
* opaque and transparent are sorted separately
*/
sort_palette_qsort (map, 0, num_transparent);
sort_palette_qsort (map, num_transparent, map->colors - num_transparent);
if (map->colors > 16) {
SWAP_PALETTE (map, 7, 1); // slightly improves compression
SWAP_PALETTE (map, 8, 2);
SWAP_PALETTE (map, 9, 3);
}
}
inline static unsigned int
posterize_channel (unsigned int color, unsigned int bits)
{
return (color & ~((1 << bits) - 1)) | (color >> (8 - bits));
}
static void
set_rounded_palette (liq_palette * const dest, colormap * const map,
const double gamma, unsigned int posterize)
{
float gamma_lut[256];
to_f_set_gamma (gamma_lut, gamma);
dest->count = map->colors;
for (unsigned int x = 0; x < map->colors; ++x) {
rgba_pixel px = to_rgb (gamma, map->palette[x].acolor);
px.r = posterize_channel (px.r, posterize);
px.g = posterize_channel (px.g, posterize);
px.b = posterize_channel (px.b, posterize);
px.a = posterize_channel (px.a, posterize);
map->palette[x].acolor = to_f (gamma_lut, px); /* saves rounding error introduced by to_rgb, which makes remapping & dithering more accurate */
if (!px.a) {
px.r = 'L';
px.g = 'i';
px.b = 'q';
}
dest->entries[x] = (liq_color) {
.r = px.r,.g = px.g,.b = px.b,.a = px.a};
}
}
LIQ_EXPORT const liq_palette *
liq_get_palette (liq_result * result)
{
if (!CHECK_STRUCT_TYPE (result, liq_result))
return NULL;
if (result->remapping && result->remapping->int_palette.count) {
return &result->remapping->int_palette;
}
if (!result->int_palette.count) {
set_rounded_palette (&result->int_palette, result->palette, result->gamma,
result->min_posterization_output);
}
return &result->int_palette;
}
#define MAX_THREADS 8
static float
remap_to_palette (liq_image * const input_image,
unsigned char *const *const output_pixels, colormap * const map,
const bool fast)
{
const int rows = input_image->height;
const unsigned int cols = input_image->width;
const float min_opaque_val = input_image->min_opaque_val;
double remapping_error = 0;
if (!liq_image_get_row_f (input_image, 0)) { // trigger lazy conversion
return -1;
}
{
struct nearest_map *const n = nearest_init (map, fast);
const unsigned int max_threads = MIN (MAX_THREADS, omp_get_max_threads ());
viter_state *average_color =
g_alloca (sizeof (viter_state) * (VITER_CACHE_LINE_GAP +
map->colors) * MAX_THREADS);
unsigned int row, col;
viter_init (map, max_threads, average_color);
#pragma omp parallel for if (rows*cols > 3000) \
schedule(static) default(none) shared(average_color) reduction(+:remapping_error)
for (row = 0; row < rows; ++row) {
const f_pixel *const row_pixels = liq_image_get_row_f (input_image, row);
unsigned int last_match = 0;
for (col = 0; col < cols; ++col) {
f_pixel px = row_pixels[col];
float diff;
output_pixels[row][col] = last_match =
nearest_search (n, px, last_match, min_opaque_val, &diff);
remapping_error += diff;
viter_update_color (px, 1.0, map, last_match, omp_get_thread_num (),
average_color);
}
}
viter_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 a;
float ratio = 1.0;
float dither_error;
// allowing some overflow prevents undithered bands caused by clamping of all channels
if (px.r + sr > 1.03)
ratio = MIN (ratio, (1.03 - px.r) / sr);
else if (px.r + sr < 0)
ratio = MIN (ratio, px.r / -sr);
if (px.g + sg > 1.03)
ratio = MIN (ratio, (1.03 - px.g) / sg);
else if (px.g + sg < 0)
ratio = MIN (ratio, px.g / -sg);
if (px.b + sb > 1.03)
ratio = MIN (ratio, (1.03 - px.b) / sb);
else if (px.b + sb < 0)
ratio = MIN (ratio, px.b / -sb);
a = px.a + sa;
if (a > 1.0) {
a = 1.0;
} else if (a < 0) {
a = 0;
}
// If dithering error is crazy high, don't propagate it that much
// This prevents crazy geen pixels popping out of the blue (or red or black! ;)
dither_error = sr * sr + sg * sg + sb * sb + sa * sa;
if (dither_error > max_dither_error) {
ratio *= 0.8;
} else if (dither_error < 2.f / 256.f / 256.f) {
// don't dither areas that don't have noticeable error — makes file smaller
return px;
}
return (f_pixel) {
.r = px.r + sr * ratio,.g = px.g + sg * ratio,.b = px.b + sb * ratio,.a = a,};
}
/**
Uses edge/noise map to apply dithering only to flat areas. Dithering on edges creates jagged lines, and noisy areas are "naturally" dithered.
If output_image_is_remapped is true, only pixels noticeably changed by error diffusion will be written to output image.
*/
static void
remap_to_palette_floyd (liq_image * input_image,
unsigned char *const output_pixels[], const colormap * map,
const float max_dither_error, const bool use_dither_map,
const bool output_image_is_remapped, float base_dithering_level)
{
const unsigned int rows = input_image->height, cols = input_image->width;
const unsigned char *dither_map =
use_dither_map ? (input_image->
dither_map ? input_image->dither_map : input_image->edges) : NULL;
const float min_opaque_val = input_image->min_opaque_val;
const colormap_item *acolormap = map->palette;
struct nearest_map *const n = nearest_init (map, false);
unsigned int col;
/* Initialize Floyd-Steinberg error vectors. */
f_pixel *restrict thiserr, *restrict nexterr;
thiserr = input_image->malloc ((cols + 2) * sizeof (*thiserr) * 2); // +2 saves from checking out of bounds access
nexterr = thiserr + (cols + 2);
srand (12345); /* deterministic dithering is better for comparing results */
if (!thiserr)
return;
for (col = 0; col < cols + 2; ++col) {
const double rand_max = RAND_MAX;
thiserr[col].r = ((double) rand () - rand_max / 2.0) / rand_max / 255.0;
thiserr[col].g = ((double) rand () - rand_max / 2.0) / rand_max / 255.0;
thiserr[col].b = ((double) rand () - rand_max / 2.0) / rand_max / 255.0;
thiserr[col].a = ((double) rand () - rand_max / 2.0) / rand_max / 255.0;
}
// response to this value is non-linear and without it any value < 0.8 would give almost no dithering
base_dithering_level =
1.0 - (1.0 - base_dithering_level) * (1.0 - base_dithering_level) * (1.0 -
base_dithering_level);
if (dither_map) {
base_dithering_level *= 1.0 / 255.0; // convert byte to float
}
base_dithering_level *= 15.0 / 16.0; // prevent small errors from accumulating
{
bool fs_direction = true;
unsigned int last_match = 0;
for (unsigned int row = 0; row < rows; ++row) {
unsigned int col = (fs_direction) ? 0 : (cols - 1);
const f_pixel *const row_pixels = liq_image_get_row_f (input_image, row);
memset (nexterr, 0, (cols + 2) * sizeof (*nexterr));
do {
float dither_level = base_dithering_level;
f_pixel spx, xp, err;
unsigned int guessed_match;
if (dither_map) {
dither_level *= dither_map[row * cols + col];
}
spx =
get_dithered_pixel (dither_level, max_dither_error,
thiserr[col + 1], row_pixels[col]);
guessed_match =
output_image_is_remapped ? output_pixels[row][col] : last_match;
output_pixels[row][col] = last_match =
nearest_search (n, spx, guessed_match, min_opaque_val, NULL);
xp = acolormap[last_match].acolor;
err.r = spx.r - xp.r;
err.g = spx.r - xp.g;
err.b = spx.r - xp.b;
err.a = spx.r - xp.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) {
dither_level *= 0.75;
}
{
const float colorimp =
(3.0f + acolormap[last_match].acolor.a) / 4.0f * dither_level;
err.r *= colorimp;
err.g *= colorimp;
err.b *= colorimp;
err.a *= dither_level;
}
/* Propagate Floyd-Steinberg error terms. */
if (fs_direction) {
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
if (fs_direction) {
++col;
if (col >= cols)
break;
} else {
if (col <= 0)
break;
--col;
}
} 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);
}
/* fixed colors are always included in the palette, so it would be wasteful to duplicate them in palette from histogram */
static void
remove_fixed_colors_from_histogram (histogram * hist,
const liq_image * input_image, const float target_mse)
{
const float max_difference = MAX (target_mse / 2.0, 2.0 / 256.0 / 256.0);
if (input_image->fixed_colors_count) {
for (int j = 0; j < hist->size; j++) {
for (unsigned int i = 0; i < input_image->fixed_colors_count; i++) {
if (colordifference (hist->achv[j].acolor,
input_image->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
}
}
}
}
}
/* histogram contains information how many times each color is present in the image, weighted by importance_map */
static histogram *
get_histogram (liq_image * input_image, const liq_attr * options)
{
unsigned int ignorebits =
MAX (options->min_posterization_output, options->min_posterization_input);
const unsigned int cols = input_image->width, rows = input_image->height;
if (!input_image->noise && options->use_contrast_maps) {
contrast_maps (input_image);
}
/*
** Step 2: attempt to make a histogram of the colors, unclustered.
** If at first we don't succeed, increase ignorebits to increase color
** coherence and try again.
*/
{
unsigned int maxcolors = options->max_histogram_entries;
struct acolorhash_table *acht;
const bool all_rows_at_once = liq_image_can_use_rows (input_image);
histogram *hist;
do {
acht =
pam_allocacolorhash (maxcolors, rows * cols, ignorebits,
options->malloc, options->free);
if (!acht)
return NULL;
// histogram uses noise contrast map for importance. Color accuracy in noisy areas is not very important.
// noise map does not include edges to avoid ruining anti-aliasing
for (unsigned int row = 0; row < rows; row++) {
bool added_ok;
if (all_rows_at_once) {
added_ok =
pam_computeacolorhash (acht,
(const rgba_pixel * const *) input_image->rows, cols, rows,
input_image->noise);
if (added_ok)
break;
} else {
const rgba_pixel *rows_p[1] =
{ liq_image_get_row_rgba (input_image, row) };
added_ok =
pam_computeacolorhash (acht, rows_p, cols, 1,
input_image->noise ? &input_image->noise[row * cols] : NULL);
}
if (!added_ok) {
ignorebits++;
liq_verbose_printf (options,
" too many colors! Scaling colors to improve clustering... %d",
ignorebits);
pam_freeacolorhash (acht);
acht = NULL;
break;
}
}
} while (!acht);
if (input_image->noise) {
input_image->free (input_image->noise);
input_image->noise = NULL;
}
if (input_image->free_pixels && input_image->f_pixels) {
liq_image_free_rgba_source (input_image); // bow can free the RGBA source if copy has been made in f_pixels
}
hist =
pam_acolorhashtoacolorhist (acht, input_image->gamma, options->malloc,
options->free);
pam_freeacolorhash (acht);
if (hist) {
liq_verbose_printf (options, " made histogram...%d colors found",
hist->size);
remove_fixed_colors_from_histogram (hist, input_image,
options->target_mse);
}
return hist;
}
}
static void
modify_alpha (liq_image * input_image, rgba_pixel * const row_pixels)
{
/* IE6 makes colors with even slightest transparency completely transparent,
thus to improve situation in IE, make colors that are less than ~10% transparent
completely opaque */
const float min_opaque_val = input_image->min_opaque_val;
const float almost_opaque_val = min_opaque_val * 169.f / 256.f;
const unsigned int almost_opaque_val_int =
(min_opaque_val * 169.f / 256.f) * 255.f;
for (unsigned int col = 0; col < input_image->width; col++) {
const rgba_pixel px = row_pixels[col];
/* ie bug: to avoid visible step caused by forced opaqueness, linearily raise opaqueness of almost-opaque colors */
if (px.a >= almost_opaque_val_int) {
float al = px.a / 255.f;
al = almost_opaque_val + (al - almost_opaque_val) * (1.f -
almost_opaque_val) / (min_opaque_val - almost_opaque_val);
al *= 256.f;
row_pixels[col].a = al >= 255.f ? 255 : al;
}
}
}
/**
Builds two maps:
noise - approximation of areas with high-frequency noise, except straight edges. 1=flat, 0=noisy.
edges - noise map including all edges
*/
static void
contrast_maps (liq_image * image)
{
const int cols = image->width, rows = image->height;
unsigned char *restrict noise, *restrict edges, *restrict tmp;
const f_pixel *curr_row, *prev_row, *next_row;
int i, j;
if (cols < 4 || rows < 4 || (3 * cols * rows) > LIQ_HIGH_MEMORY_LIMIT) {
return;
}
noise = image->malloc (cols * rows);
edges = image->malloc (cols * rows);
tmp = image->malloc (cols * rows);
if (!noise || !edges || !tmp) {
return;
}
curr_row = prev_row = next_row = liq_image_get_row_f (image, 0);
for (j = 0; j < rows; j++) {
f_pixel prev, curr, next;
prev_row = curr_row;
curr_row = next_row;
next_row = liq_image_get_row_f (image, MIN (rows - 1, j + 1));
curr = curr_row[0];
next = curr;
for (i = 0; i < cols; i++) {
prev = curr;
curr = next;
next = curr_row[MIN (cols - 1, i + 1)];
// contrast is difference between pixels neighbouring horizontally and vertically
{
const float a = fabsf (prev.a + next.a - curr.a * 2.f),
r = fabsf (prev.r + next.r - curr.r * 2.f),
g = fabsf (prev.g + next.g - curr.g * 2.f),
b = fabsf (prev.b + next.b - curr.b * 2.f);
const f_pixel prevl = prev_row[i];
const f_pixel nextl = next_row[i];
const float a1 = fabsf (prevl.a + nextl.a - curr.a * 2.f),
r1 = fabsf (prevl.r + nextl.r - curr.r * 2.f),
g1 = fabsf (prevl.g + nextl.g - curr.g * 2.f),
b1 = fabsf (prevl.b + nextl.b - curr.b * 2.f);
const float horiz = MAX (MAX (a, r), MAX (g, b));
const float vert = MAX (MAX (a1, r1), MAX (g1, b1));
const float edge = MAX (horiz, vert);
float z = edge - fabsf (horiz - vert) * .5f;
z = 1.f - MAX (z, MIN (horiz, vert));
z *= z; // noise is amplified
z *= z;
z *= 256.f;
noise[j * cols + i] = z < 256 ? z : 255;
z = (1.f - edge) * 256.f;
edges[j * cols + i] = z < 256 ? z : 255;
}
}
}
// noise areas are shrunk and then expanded to remove thin edges from the map
liq_max3 (noise, tmp, cols, rows);
liq_max3 (tmp, noise, cols, rows);
liq_blur (noise, tmp, noise, cols, rows, 3);
liq_max3 (noise, tmp, cols, rows);
liq_min3 (tmp, noise, cols, rows);
liq_min3 (noise, tmp, cols, rows);
liq_min3 (tmp, noise, cols, rows);
liq_min3 (edges, tmp, cols, rows);
liq_max3 (tmp, edges, cols, rows);
for (int i = 0; i < cols * rows; i++)
edges[i] = MIN (noise[i], edges[i]);
image->free (tmp);
image->noise = noise;
image->edges = edges;
}
/**
* Builds map of neighbor pixels mapped to the same palette entry
*
* For efficiency/simplicity it mainly looks for same consecutive pixels horizontally
* and peeks 1 pixel above/below. Full 2d algorithm doesn't improve it significantly.
* Correct flood fill doesn't have visually good properties.
*/
static void
update_dither_map (unsigned char *const *const row_pointers,
liq_image * input_image)
{
const unsigned int width = input_image->width;
const unsigned int height = input_image->height;
unsigned char *const edges = input_image->edges;
for (unsigned int row = 0; row < height; row++) {
unsigned char lastpixel = row_pointers[row][0];
unsigned int lastcol = 0;
for (unsigned int col = 1; col < width; col++) {
const unsigned char px = row_pointers[row][col];
if (px != lastpixel || col == width - 1) {
float neighbor_count = 2.5f + col - lastcol;
unsigned int i = lastcol;
while (i < col) {
if (row > 0) {
unsigned char pixelabove = row_pointers[row - 1][i];
if (pixelabove == lastpixel)
neighbor_count += 1.f;
}
if (row < height - 1) {
unsigned char pixelbelow = row_pointers[row + 1][i];
if (pixelbelow == lastpixel)
neighbor_count += 1.f;
}
i++;
}
while (lastcol <= col) {
float e = edges[row * width + lastcol] / 255.f;
e *= 1.f - 2.5f / neighbor_count;
edges[row * width + lastcol++] = e * 255.f;
}
lastpixel = px;
}
}
}
input_image->dither_map = input_image->edges;
input_image->edges = NULL;
}
static colormap *
add_fixed_colors_to_palette (colormap * palette, const int max_colors,
const f_pixel fixed_colors[], const int fixed_colors_count,
void *(*malloc) (size_t), void (*free) (void *))
{
colormap *newpal;
unsigned int i, palette_max;
int j;
if (!fixed_colors_count)
return palette;
newpal =
pam_colormap (MIN (max_colors,
(palette ? palette->colors : 0) + fixed_colors_count), malloc, free);
i = 0;
if (palette && fixed_colors_count < max_colors) {
palette_max = MIN (palette->colors, max_colors - fixed_colors_count);
for (; i < palette_max; i++) {
newpal->palette[i] = palette->palette[i];
}
}
for (j = 0; j < MIN (max_colors, fixed_colors_count); j++) {
newpal->palette[i++] = (colormap_item) {
.acolor = fixed_colors[j],.fixed = true,};
}
if (palette)
pam_freecolormap (palette);
return newpal;
}
static void
adjust_histogram_callback (hist_item * item, float diff)
{
item->adjusted_weight =
(item->perceptual_weight + item->adjusted_weight) * (sqrtf (1.f + diff));
}
/**
Repeats mediancut with different histogram weights to find palette with minimum error.
feedback_loop_trials controls how long the search will take. < 0 skips the iteration.
*/
static colormap *
find_best_palette (histogram * hist, const liq_attr * options,
const double max_mse, const f_pixel fixed_colors[],
const unsigned int fixed_colors_count, double *palette_error_p)
{
unsigned int max_colors = options->max_colors;
// if output is posterized it doesn't make sense to aim for perfrect colors, so increase target_mse
// at this point actual gamma is not set, so very conservative posterization estimate is used
const double target_mse = MIN (max_mse, MAX (options->target_mse,
pow ((1 << options->min_posterization_output) / 1024.0, 2)));
int feedback_loop_trials = options->feedback_loop_trials;
colormap *acolormap = NULL;
double least_error = MAX_DIFF;
double target_mse_overshoot = feedback_loop_trials > 0 ? 1.05 : 1.0;
const double percent =
(double) (feedback_loop_trials > 0 ? feedback_loop_trials : 1) / 100.0;
do {
colormap *newmap;
double total_error;
if (hist->size && fixed_colors_count < max_colors) {
newmap =
mediancut (hist, options->min_opaque_val,
max_colors - fixed_colors_count, target_mse * target_mse_overshoot,
MAX (MAX (90.0 / 65536.0, target_mse), least_error) * 1.2,
options->malloc, options->free);
} else {
feedback_loop_trials = 0;
newmap = NULL;
}
newmap =
add_fixed_colors_to_palette (newmap, max_colors, fixed_colors,
fixed_colors_count, options->malloc, options->free);
if (!newmap) {
return NULL;
}
if (feedback_loop_trials <= 0) {
return newmap;
}
// after palette has been created, total error (MSE) is calculated to keep the best palette
// at the same time Voronoi iteration is done to improve the palette
// and histogram weights are adjusted based on remapping error to give more weight to poorly matched colors
{
const bool first_run_of_target_mse = !acolormap && target_mse > 0;
total_error =
viter_do_iteration (hist, newmap, options->min_opaque_val,
first_run_of_target_mse ? NULL : adjust_histogram_callback, !acolormap
|| options->fast_palette);
}
// goal is to increase quality or to reduce number of colors used if quality is good enough
if (!acolormap || total_error < least_error || (total_error <= target_mse
&& newmap->colors < max_colors)) {
if (acolormap)
pam_freecolormap (acolormap);
acolormap = newmap;
if (total_error < target_mse && total_error > 0) {
// voronoi iteration improves quality above what mediancut aims for
// this compensates for it, making mediancut aim for worse
target_mse_overshoot =
MIN (target_mse_overshoot * 1.25, target_mse / total_error);
}
least_error = total_error;
// if number of colors could be reduced, try to keep it that way
// but allow extra color as a bit of wiggle room in case quality can be improved too
max_colors = MIN (newmap->colors + 1, max_colors);
feedback_loop_trials -= 1; // asymptotic improvement could make it go on forever
} else {
for (unsigned int j = 0; j < hist->size; j++) {
hist->achv[j].adjusted_weight =
(hist->achv[j].perceptual_weight +
hist->achv[j].adjusted_weight) / 2.0;
}
target_mse_overshoot = 1.0;
feedback_loop_trials -= 6;
// if error is really bad, it's unlikely to improve, so end sooner
if (total_error > least_error * 4)
feedback_loop_trials -= 3;
pam_freecolormap (newmap);
}
liq_verbose_printf (options, " selecting colors...%d%%", 100 - MAX (0,
(int) (feedback_loop_trials / percent)));
}
while (feedback_loop_trials > 0);
*palette_error_p = least_error;
return acolormap;
}
static liq_result *
pngquant_quantize (histogram * hist, const liq_attr * options,
const liq_image * img)
{
colormap *acolormap;
double palette_error = -1;
// no point having perfect match with imperfect colors (ignorebits > 0)
const bool fast_palette = options->fast_palette || hist->ignorebits > 0;
const bool few_input_colors =
hist->size + img->fixed_colors_count <= options->max_colors;
liq_result *result;
// 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 = 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;
}
acolormap =
add_fixed_colors_to_palette (acolormap, options->max_colors,
img->fixed_colors, img->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
const double iteration_limit = options->voronoi_iteration_limit;
unsigned int iterations = options->voronoi_iterations;
acolormap =
find_best_palette (hist, options, max_mse, img->fixed_colors,
img->fixed_colors_count, &palette_error);
if (!acolormap) {
return NULL;
}
// Voronoi iteration approaches local minimum for the palette
if (!iterations && palette_error < 0 && max_mse < MAX_DIFF)
iterations = 1; // otherwise total error is never calculated and MSE limit won't work
if (iterations) {
double previous_palette_error = MAX_DIFF;
unsigned int i;
// likely_colormap_index (used and set in viter_do_iteration) can't point to index outside colormap
if (acolormap->colors < 256)
for (unsigned int j = 0; j < hist->size; j++) {
if (hist->achv[j].tmp.likely_colormap_index >= acolormap->colors) {
hist->achv[j].tmp.likely_colormap_index = 0; // actual value doesn't matter, as the guess is out of date anyway
}
}
verbose_print (options, " moving colormap towards local minimum");
for (i = 0; i < iterations; i++) {
palette_error =
viter_do_iteration (hist, acolormap, options->min_opaque_val, NULL,
i == 0 || options->fast_palette);
if (fabs (previous_palette_error - palette_error) < iteration_limit) {
break;
}
if (palette_error > max_mse * 1.5) { // probably hopeless
if (palette_error > max_mse * 3.0)
break; // definitely hopeless
i++;
}
previous_palette_error = palette_error;
}
}
if (palette_error > max_mse) {
liq_verbose_printf (options,
" image degradation MSE=%.3f (Q=%d) exceeded limit of %.3f (%d)",
palette_error * 65536.0 / 6.0, mse_to_quality (palette_error),
max_mse * 65536.0 / 6.0, mse_to_quality (max_mse));
pam_freecolormap (acolormap);
return NULL;
}
}
sort_palette (acolormap, options);
result = options->malloc (sizeof (liq_result));
if (!result)
return NULL;
*result = (liq_result) {
.magic_header = liq_result_magic,.malloc = options->malloc,.free =
options->free,.palette = acolormap,.palette_error =
palette_error,.fast_palette = fast_palette,.use_dither_map =
options->use_dither_map,.gamma =
img->gamma,.min_posterization_output =
options->min_posterization_output,};
return result;
}
LIQ_EXPORT liq_error
liq_write_remapped_image (liq_result * result, liq_image * input_image,
void *buffer, size_t buffer_size)
{
size_t required_size;
unsigned char **rows;
unsigned char *buffer_bytes;
unsigned i;
if (!CHECK_STRUCT_TYPE (result, liq_result)) {
return LIQ_INVALID_POINTER;
}
if (!CHECK_STRUCT_TYPE (input_image, liq_image)) {
return LIQ_INVALID_POINTER;
}
if (!CHECK_USER_POINTER (buffer)) {
return LIQ_INVALID_POINTER;
}
required_size = input_image->width * input_image->height;
if (buffer_size < required_size) {
return LIQ_BUFFER_TOO_SMALL;
}
rows = g_alloca (sizeof (unsigned char *) * input_image->height);
buffer_bytes = buffer;
for (i = 0; i < input_image->height; i++) {
rows[i] = &buffer_bytes[input_image->width * i];
}
return liq_write_remapped_image_rows (result, input_image, rows);
}
LIQ_EXPORT liq_error
liq_write_remapped_image_rows (liq_result * quant, liq_image * input_image,
unsigned char **row_pointers)
{
unsigned int i;
liq_remapping_result *result;
float remapping_error;
if (!CHECK_STRUCT_TYPE (quant, liq_result))
return LIQ_INVALID_POINTER;
if (!CHECK_STRUCT_TYPE (input_image, liq_image))
return LIQ_INVALID_POINTER;
for (i = 0; i < input_image->height; i++) {
if (!CHECK_USER_POINTER (row_pointers + i)
|| !CHECK_USER_POINTER (row_pointers[i]))
return LIQ_INVALID_POINTER;
}
if (quant->remapping) {
liq_remapping_result_destroy (quant->remapping);
}
result = quant->remapping = liq_remapping_result_create (quant);
if (!result)
return LIQ_OUT_OF_MEMORY;
if (!input_image->edges && !input_image->dither_map && quant->use_dither_map) {
contrast_maps (input_image);
}
/*
** Step 4: map the colors in the image to their closest match in the
** new colormap, and write 'em out.
*/
remapping_error = result->palette_error;
if (result->dither_level == 0) {
set_rounded_palette (&result->int_palette, result->palette, result->gamma,
quant->min_posterization_output);
remapping_error =
remap_to_palette (input_image, row_pointers, result->palette,
quant->fast_palette);
} else {
const bool generate_dither_map = result->use_dither_map
&& (input_image->edges && !input_image->dither_map);
if (generate_dither_map) {
// If dithering (with dither map) is required, this image is used to find areas that require dithering
remapping_error =
remap_to_palette (input_image, row_pointers, result->palette,
quant->fast_palette);
update_dither_map (row_pointers, input_image);
}
// remapping above was the last chance to do voronoi iteration, hence the final palette is set after remapping
set_rounded_palette (&result->int_palette, result->palette, result->gamma,
quant->min_posterization_output);
remap_to_palette_floyd (input_image, row_pointers, result->palette,
MAX (remapping_error * 2.4, 16.f / 256.f), result->use_dither_map,
generate_dither_map, result->dither_level);
}
// remapping error from dithered image is absurd, so always non-dithered value is used
// palette_error includes some perceptual weighting from histogram which is closer correlated with dssim
// so that should be used when possible.
if (result->palette_error < 0) {
result->palette_error = remapping_error;
}
return LIQ_OK;
}
LIQ_EXPORT int
liq_version (void)
{
return LIQ_VERSION;
}