Add n64texconv and bin2c tools to convert extracted .png and .bin to C arrays during build (#2477)

* n64texconv and bin2c

* mv tools/n64texconv tools/assets/

* fix

* more light fixes

* Silence -Wshadow for libimagequant

* Add reference counting gc for palette objects in python bindings

* Fix missing alignment in n64texconv_*_to_c functions

Co-authored-by: Dragorn421 <Dragorn421@users.noreply.github.com>

* Check palette size in n64texconv_image_from_png

* accept memoryview as well as bytes for binary data

* minimal doc on n64texconv_quantize_shared

* fix a buffer size passed to libimagequant

* assert pal count <= 256 on png write

* Disable palette size check for input pngs, ZAPD fails the check

* No OpenMP for clang

* When reading an indexed png into a CI format, requantize if there are too many colors for the target texel size

---------

Co-authored-by: Dragorn421 <Dragorn421@users.noreply.github.com>
This commit is contained in:
Tharo
2025-02-17 22:09:42 +00:00
committed by GitHub
parent ec30dcbe4e
commit 3d61fb85ef
41 changed files with 15634 additions and 9 deletions

View File

@@ -320,7 +320,8 @@ CPP := gcc -E
MKLDSCRIPT := tools/mkldscript
MKDMADATA := tools/mkdmadata
ELF2ROM := tools/elf2rom
ZAPD := tools/ZAPD/ZAPD.out
BIN2C := tools/bin2c
N64TEXCONV := tools/assets/n64texconv/n64texconv
FADO := tools/fado/fado.elf
PYTHON ?= $(VENV)/bin/python3
@@ -987,22 +988,22 @@ $(BUILD_DIR)/src/overlays/%_reloc.o: $(BUILD_DIR)/spec
$(AS) $(ASFLAGS) $(@:.o=.s) -o $@
$(BUILD_DIR)/assets/%.inc.c: assets/%.png
$(ZAPD) btex -eh -tt $(subst .,,$(suffix $*)) -i $< -o $@
$(N64TEXCONV) $(subst .,,$(suffix $*)) "$(findstring u32,$(subst .,,$(suffix $(basename $*))))" $< $@ $(@:.inc.c=.pal.inc.c)
$(BUILD_DIR)/assets/%.inc.c: $(EXTRACTED_DIR)/assets/%.png
$(ZAPD) btex -eh -tt $(subst .,,$(suffix $*)) -i $< -o $@
$(N64TEXCONV) $(subst .,,$(suffix $*)) "$(findstring u32,$(subst .,,$(suffix $(basename $*))))" $< $@ $(@:.inc.c=.pal.inc.c)
$(BUILD_DIR)/assets/%.bin.inc.c: assets/%.bin
$(ZAPD) bblb -eh -i $< -o $@
$(BIN2C) -t 1 $< $@
$(BUILD_DIR)/assets/%.bin.inc.c: $(EXTRACTED_DIR)/assets/%.bin
$(ZAPD) bblb -eh -i $< -o $@
$(BIN2C) -t 1 $< $@
$(BUILD_DIR)/assets/%.jpg.inc.c: assets/%.jpg
$(ZAPD) bren -eh -i $< -o $@
$(N64TEXCONV) JFIF "" $< $@
$(BUILD_DIR)/assets/%.jpg.inc.c: $(EXTRACTED_DIR)/assets/%.jpg
$(ZAPD) bren -eh -i $< -o $@
$(N64TEXCONV) JFIF "" $< $@
# Audio

1
tools/.gitignore vendored
View File

@@ -1,5 +1,6 @@
# Output files
*.exe
bin2c
elf2rom
makeromfs
mkdmadata

View File

@@ -1,5 +1,5 @@
CFLAGS := -Wall -Wextra -pedantic -std=c99 -g -O2
PROGRAMS := elf2rom makeromfs mkdmadata mkldscript preprocess_pragmas reloc_prereq vtxdis
CFLAGS := -Wall -Wextra -pedantic -std=gnu99 -g -O2
PROGRAMS := bin2c elf2rom makeromfs mkdmadata mkldscript preprocess_pragmas reloc_prereq vtxdis
UNAME_S := $(shell uname -s)
ifeq ($(UNAME_S),Linux)
@@ -39,6 +39,7 @@ all: $(PROGRAMS) $(IDO_RECOMP_5_3_DIR) $(IDO_RECOMP_7_1_DIR) $(EGCS_DIR)
$(MAKE) -C fado
$(MAKE) -C audio
$(MAKE) -C com-plugin
$(MAKE) -C assets
clean:
$(RM) $(PROGRAMS) $(addsuffix .exe,$(PROGRAMS))
@@ -47,13 +48,16 @@ clean:
$(MAKE) -C fado clean
$(MAKE) -C audio clean
$(MAKE) -C com-plugin clean
$(MAKE) -C assets clean
distclean: clean
$(MAKE) -C audio distclean
$(MAKE) -C assets distclean
.PHONY: all clean distclean
elf2rom_SOURCES := elf2rom.c elf32.c n64chksum.c util.c
bin2c_SOURCES := bin2c.c
makeromfs_SOURCES := makeromfs.c n64chksum.c util.c
mkdmadata_SOURCES := mkdmadata.c spec.c util.c
mkldscript_SOURCES := mkldscript.c spec.c util.c

10
tools/assets/Makefile Normal file
View File

@@ -0,0 +1,10 @@
all:
$(MAKE) -C n64texconv
clean:
$(MAKE) -C n64texconv clean
distclean: clean
$(MAKE) -C n64texconv distclean
.PHONY: all clean distclean

View File

@@ -0,0 +1,29 @@
IndentWidth: 4
Language: Cpp
UseTab: Never
ColumnLimit: 120
PointerAlignment: Right
BreakBeforeBraces: Linux
AlwaysBreakAfterReturnType: TopLevel
AlignArrayOfStructures: Left
SpaceAfterCStyleCast: false
SpaceBeforeParens: ControlStatementsExceptControlMacros
Cpp11BracedListStyle: false
IndentCaseLabels: true
BinPackArguments: true
BinPackParameters: true
AlignAfterOpenBracket: Align
AlignOperands: true
BreakBeforeTernaryOperators: true
BreakBeforeBinaryOperators: None
AllowShortBlocksOnASingleLine: true
AllowShortIfStatementsOnASingleLine: false
AllowShortLoopsOnASingleLine: false
AllowShortCaseLabelsOnASingleLine: false
AllowShortFunctionsOnASingleLine: false
AllowShortEnumsOnASingleLine: false
AlignEscapedNewlines: Left
AlignTrailingComments: true
SortIncludes: false
AlignConsecutiveMacros: Consecutive
ForEachMacros: ['LL_FOREACH']

10
tools/assets/n64texconv/.gitignore vendored Normal file
View File

@@ -0,0 +1,10 @@
build/
libn64texconv.a
libn64texconv.dll
libn64texconv.so
n64texconv
# Tests
*.png
*.bin

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,77 @@
BUILD_DIR := build
# Targets
LIB := libn64texconv.a
SOLIB := libn64texconv.so
APP := n64texconv
INC := -Ilib/spng -Ilib/libimagequant
CC := gcc
WFLAGS := -Wall -Wextra -Wshadow
ifeq ($(shell $(CC) --version | grep clang),)
ARCHFLAGS := -march=native -mtune=native
OMPFLAGS := -fopenmp
else
ARCHFLAGS :=
OMPFLAGS :=
WFLAGS += -Wno-unknown-pragmas
endif
CFLAGS := $(WFLAGS) $(ARCHFLAGS) -MD -MMD -std=gnu11 -fPIC -ffunction-sections -fdata-sections $(INC)
OPTFLAGS := -O3
LDFLAGS :=
LDLIBS := $(OMPFLAGS) -lz -lm
AR := ar
ARFLAGS := rcs
SRC_DIRS := $(shell find src -type d -not -path src/app)
LIB_DIRS := $(shell find lib -type d)
APP_SRC_DIRS := $(shell find src/app -type d)
C_FILES := $(foreach dir,$(SRC_DIRS) $(LIB_DIRS),$(wildcard $(dir)/*.c))
O_FILES := $(foreach f,$(C_FILES:.c=.o),$(BUILD_DIR)/$f)
DEP_FILES := $(foreach f,$(O_FILES:.o=.d),$f)
APP_C_FILES := $(foreach dir,$(APP_SRC_DIRS),$(wildcard $(dir)/*.c))
APP_O_FILES := $(foreach f,$(APP_C_FILES:.c=.o),$(BUILD_DIR)/$f)
APP_DEP_FILES := $(foreach f,$(APP_O_FILES:.o=.d),$f)
FMT_C_FILES := $(foreach dir,$(SRC_DIRS) $(APP_SRC_DIRS),$(wildcard $(dir)/*.c))
FMT_H_FILES := $(foreach dir,$(SRC_DIRS) $(APP_SRC_DIRS),$(wildcard $(dir)/*.h))
FMT_FILES := $(FMT_C_FILES) $(FMT_H_FILES)
CLANG_FORMAT := clang-format-14
FORMAT_ARGS := -i -style=file
$(shell mkdir -p $(BUILD_DIR) $(foreach dir,$(SRC_DIRS) $(LIB_DIRS) $(APP_SRC_DIRS),$(BUILD_DIR)/$(dir)))
$(BUILD_DIR)/lib/libimagequant/%.o: CFLAGS += $(OMPFLAGS) -Wno-sign-compare -Wno-unused-parameter -Wno-shadow
.PHONY: all clean distclean format
all: $(LIB) $(SOLIB) $(APP)
clean:
$(RM) -r $(LIB) $(SOLIB) $(APP) $(BUILD_DIR)
distclean: clean
format:
$(CLANG_FORMAT) $(FORMAT_ARGS) $(FMT_FILES)
$(LIB): $(O_FILES)
$(AR) $(ARFLAGS) $@ $^
$(SOLIB): $(O_FILES)
$(CC) -shared $^ $(LDLIBS) -o $@
$(APP): $(APP_O_FILES) $(LIB)
$(CC) $(LDFLAGS) $^ $(LDLIBS) -o $@
$(BUILD_DIR)/%.o: %.c
$(CC) $(CFLAGS) $(OPTFLAGS) -c $< -o $@
-include $(DEP_FILES) $(APP_DEP_FILES)

View File

@@ -0,0 +1,472 @@
# SPDX-FileCopyrightText: © 2025 ZeldaRET
# SPDX-License-Identifier: MIT
import os, sys
from ctypes import CDLL, Structure, POINTER, string_at, byref, cast
from ctypes import c_void_p, c_char_p, c_uint8, c_uint, c_bool, c_int, c_size_t
from typing import Optional
def ctypes_buffer_to_string(buffer) -> str:
return buffer.value.decode('utf-8')
def ctypes_pointer_to_bytes(ptr : c_void_p, size : int) -> bytes:
return string_at(ptr, size)
def deref(ptr):
if ptr:
return ptr.contents
return None
ln64texconv = CDLL(os.path.join(os.path.dirname(__file__), "libn64texconv.so"))
class RefCounter:
def __init__(self) -> None:
self.ref_counts = {}
def add_ref(self, ptr):
if not isinstance(ptr, POINTER(N64Palette)):
ptr = cast(ptr, POINTER(N64Palette))
if not ptr:
return
key = int.from_bytes(ptr, byteorder=sys.byteorder, signed=False)
if key not in self.ref_counts:
self.ref_counts[key] = 1
else:
self.ref_counts[key] += 1
def num_refs(self, ptr):
if not isinstance(ptr, POINTER(N64Palette)):
ptr = cast(ptr, POINTER(N64Palette))
if not ptr:
return 0
key = int.from_bytes(ptr, byteorder=sys.byteorder, signed=False)
if key not in self.ref_counts:
return 0
return self.ref_counts[key]
def rm_ref(self, ptr, free_func):
if not isinstance(ptr, POINTER(N64Palette)):
ptr = cast(ptr, POINTER(N64Palette))
if not ptr:
return
key = int.from_bytes(ptr, byteorder=sys.byteorder, signed=False)
assert key in self.ref_counts
count = self.ref_counts.pop(key)
count -= 1
if count == 0:
free_func(ptr)
else:
self.ref_counts[key] = count
# Simple reference counter for C allocations
_object_refcount = RefCounter()
#
# Private
#
# void n64texconv_free(void *p);
ln64texconv.n64texconv_free.argtypes = [c_void_p]
ln64texconv.n64texconv_free.restype = None
#
# bin2c.h
#
def bin2c(data : bytes | memoryview, pad_to_size : int = 0, byte_width : int = 8) -> Optional[str]:
if byte_width not in (1, 2, 4, 8):
raise ValueError("Invalid byte width, must be 1, 2, 4 or 8")
buffer = (c_uint8 * len(data)).from_buffer_copy(data)
ptr = c_char_p(None)
size = c_size_t(0)
if ln64texconv.bin2c(byref(ptr), byref(size), buffer, len(data), pad_to_size, byte_width) != 0:
return None
s = ctypes_buffer_to_string(ptr)
ln64texconv.n64texconv_free(ptr)
return s
def bin2c_file(out_path : str, data : bytes | memoryview, pad_to_size : int = 0, byte_width : int = 8) -> bool:
if byte_width not in (1, 2, 4, 8):
raise ValueError("Invalid byte width, must be 1, 2, 4 or 8")
buffer = (c_uint8 * len(data)).from_buffer_copy(data)
return ln64texconv.bin2c_file(out_path.encode("utf-8"), buffer, len(data), pad_to_size, byte_width) == 0
# int bin2c(char **out, size_t *size_out, void *bin, size_t size, size_t pad_to_size, unsigned int byte_width);
ln64texconv.bin2c.argtypes = [POINTER(c_char_p), POINTER(c_size_t), c_void_p, c_size_t, c_size_t, c_uint]
ln64texconv.bin2c.restype = c_int
# int bin2c_file(const char *out_path, void *bin, size_t size, size_t pad_to_size, unsigned int byte_width);
ln64texconv.bin2c_file.argtypes = [c_char_p, c_void_p, c_size_t, c_size_t, c_uint]
ln64texconv.bin2c_file.restype = c_int
#
# jfif.h
#
# struct JFIF
class JFIF(Structure):
_fields_ = [
("data", c_void_p),
("data_size", c_size_t),
]
# JFIF_BUFFER_SIZE
BUFFER_SIZE = 320 * 240 * 2
@staticmethod
def fromfile(path : str, max_size : int = BUFFER_SIZE) -> Optional["JFIF"]:
if not os.path.isfile(path):
raise ValueError(f"Cannot open \"{path}\", is not a file")
return deref(ln64texconv.jfif_fromfile(path.encode("utf-8"), max_size))
def to_c(self, pad_to_size : int = BUFFER_SIZE) -> Optional[str]:
ptr = c_char_p(None)
size = c_size_t(0)
if ln64texconv.jfif_to_c(byref(ptr), byref(size), byref(self), pad_to_size) != 0:
return None
s = ctypes_buffer_to_string(ptr)
ln64texconv.n64texconv_free(ptr)
return s
def to_c_file(self, out_path : str, pad_to_size : int = BUFFER_SIZE) -> bool:
return ln64texconv.jfif_to_c_file(out_path.encode("utf-8"), byref(self), pad_to_size) == 0
def __del__(self):
ln64texconv.jfif_free(byref(self))
# struct JFIF *jfif_fromfile(const char *path, size_t max_size);
ln64texconv.jfif_fromfile.argtypes = [c_char_p, c_size_t]
ln64texconv.jfif_fromfile.restype = POINTER(JFIF)
# void jfif_free(struct JFIF *jfif);
ln64texconv.jfif_free.argtypes = [POINTER(JFIF)]
ln64texconv.jfif_free.restype = None
# int jfif_to_c(char **out, size_t *size_out, struct JFIF *jfif, size_t pad_to_size)
ln64texconv.jfif_to_c.argtypes = [POINTER(c_char_p), POINTER(c_size_t), POINTER(JFIF), c_size_t]
ln64texconv.jfif_to_c.restype = c_int
# int jfif_to_c_file(const char *out_path, struct JFIF *jfif, size_t pad_to_size);
ln64texconv.jfif_to_c_file.argtypes = [c_char_p, POINTER(JFIF), c_size_t]
ln64texconv.jfif_to_c_file.restype = c_int
#
# n64texconv.h
#
FMT_NONE = -1
FMT_MAX = 5
G_IM_FMT_RGBA = 0
G_IM_FMT_YUV = 1
G_IM_FMT_CI = 2
G_IM_FMT_IA = 3
G_IM_FMT_I = 4
SIZ_NONE = -1
SIZ_MAX = 4
G_IM_SIZ_4b = 0
G_IM_SIZ_8b = 1
G_IM_SIZ_16b = 2
G_IM_SIZ_32b = 3
def fmt_name(fmt : int):
return {
G_IM_FMT_RGBA : "G_IM_FMT_RGBA",
G_IM_FMT_YUV : "G_IM_FMT_YUV",
G_IM_FMT_CI : "G_IM_FMT_CI",
G_IM_FMT_IA : "G_IM_FMT_IA",
G_IM_FMT_I : "G_IM_FMT_I",
}.get(fmt, str(fmt))
def siz_name(siz : int):
return {
G_IM_SIZ_4b : "G_IM_SIZ_4b",
G_IM_SIZ_8b : "G_IM_SIZ_8b",
G_IM_SIZ_16b : "G_IM_SIZ_16b",
G_IM_SIZ_32b : "G_IM_SIZ_32b",
}.get(siz, str(siz))
VALID_FORMAT_COMBINATIONS = (
(G_IM_FMT_RGBA, G_IM_SIZ_16b),
(G_IM_FMT_RGBA, G_IM_SIZ_32b),
(G_IM_FMT_CI, G_IM_SIZ_4b),
(G_IM_FMT_CI, G_IM_SIZ_8b),
(G_IM_FMT_IA, G_IM_SIZ_4b),
(G_IM_FMT_IA, G_IM_SIZ_8b),
(G_IM_FMT_IA, G_IM_SIZ_16b),
(G_IM_FMT_I, G_IM_SIZ_4b),
(G_IM_FMT_I, G_IM_SIZ_8b),
)
# struct color
class Color(Structure):
_fields_ = [
("r", c_uint8),
("g", c_uint8),
("b", c_uint8),
("a", c_uint8),
]
# static inline size_t texel_size_bytes(size_t ntexels, int siz)
def texel_size_bytes(ntexels : int, siz : int):
return (ntexels // 2) if (siz == G_IM_SIZ_4b) else (ntexels * ((1 << siz) >> 1))
# struct n64_palette
class N64Palette(Structure):
_fields_ = [
("texels", POINTER(Color)),
("fmt", c_int),
("count", c_size_t),
]
@staticmethod
def new(count : int, fmt : int) -> Optional["N64Palette"]:
if fmt not in (G_IM_FMT_RGBA, G_IM_FMT_IA):
raise ValueError("Palette format must be either G_IM_FMT_RGBA or G_IM_FMT_IA")
if count > 256:
raise ValueError("The largest possible palette size is 256")
pal = ln64texconv.n64texconv_palette_new(count, fmt)
_object_refcount.add_ref(pal)
return deref(pal)
def __del__(self):
# Free the underlying palette structure only if the refcount is 0
_object_refcount.rm_ref(byref(self), ln64texconv.n64texconv_palette_free)
def copy(self) -> Optional["N64Palette"]:
pal = ln64texconv.n64texconv_palette_copy(byref(self))
_object_refcount.add_ref(pal)
return deref(pal)
def reformat(self, fmt : int) -> Optional["N64Palette"]:
if fmt not in (G_IM_FMT_RGBA, G_IM_FMT_IA):
raise ValueError("Palette format must be either G_IM_FMT_RGBA or G_IM_FMT_IA")
pal = ln64texconv.n64texconv_palette_reformat(byref(self), fmt)
_object_refcount.add_ref(pal)
return deref(pal)
@staticmethod
def from_png(path : str, fmt : int) -> Optional["N64Palette"]:
if fmt not in (G_IM_FMT_RGBA, G_IM_FMT_IA):
raise ValueError("Palette format must be either G_IM_FMT_RGBA or G_IM_FMT_IA")
if not os.path.isfile(path):
raise ValueError(f"Cannot open \"{path}\", is not a file")
pal = ln64texconv.n64texconv_palette_from_png(path.encode("utf-8"), fmt)
_object_refcount.add_ref(pal)
return deref(pal)
@staticmethod
def from_bin(data : bytes | memoryview, fmt : int) -> Optional["N64Palette"]:
if fmt not in (G_IM_FMT_RGBA, G_IM_FMT_IA):
raise ValueError("Palette format must be either G_IM_FMT_RGBA or G_IM_FMT_IA")
buffer = (c_uint8 * len(data)).from_buffer_copy(data)
pal = ln64texconv.n64texconv_palette_from_bin(buffer, len(data) // 2, fmt)
_object_refcount.add_ref(pal)
return deref(pal)
def to_png(self, outpath : str) -> bool:
return ln64texconv.n64texconv_palette_to_png(outpath.encode("utf-8"), byref(self)) == 0
def to_bin(self, pad_to_8b : bool) -> Optional[bytes]:
nbytes = texel_size_bytes(self.count, G_IM_SIZ_16b)
if pad_to_8b:
nbytes = (nbytes + 7) & ~7
ptr = ln64texconv.n64texconv_palette_to_bin(byref(self), pad_to_8b)
if not ptr:
return None
data = ctypes_pointer_to_bytes(ptr, nbytes)
ln64texconv.n64texconv_free(ptr)
return data
def to_c(self, pad_to_8b : bool, byte_width : int) -> Optional[str]:
ptr = c_char_p(None)
size = c_size_t(0)
if ln64texconv.n64texconv_palette_to_c(byref(ptr), byref(size), byref(self), pad_to_8b, byte_width) != 0:
return None
s = ctypes_buffer_to_string(ptr)
ln64texconv.n64texconv_free(ptr)
return s
def to_c_file(self, out_path : str, pad_to_8b : bool, byte_width : int) -> bool:
return ln64texconv.n64texconv_palette_to_c_file(out_path.encode("utf-8"), byref(self), pad_to_8b, byte_width) == 0
# struct n64_palette *n64texconv_palette_new(size_t count, int fmt);
ln64texconv.n64texconv_palette_new.argtypes = [c_size_t, c_int]
ln64texconv.n64texconv_palette_new.restype = POINTER(N64Palette)
# void n64texconv_palette_free(struct n64_palette *pal);
ln64texconv.n64texconv_palette_free.argtypes = [POINTER(N64Palette)]
ln64texconv.n64texconv_palette_free.restype = None
# struct n64_palette *n64texconv_palette_copy(struct n64_palette *pal);
ln64texconv.n64texconv_palette_copy.argtypes = [POINTER(N64Palette)]
ln64texconv.n64texconv_palette_copy.restype = POINTER(N64Palette)
# struct n64_palette *n64texconv_palette_reformat(struct n64_palette *pal, int fmt);
ln64texconv.n64texconv_palette_reformat.argtypes = [POINTER(N64Palette), c_int]
ln64texconv.n64texconv_palette_reformat.restype = POINTER(N64Palette)
# struct n64_palette *n64texconv_palette_from_png(const char *path, int fmt);
ln64texconv.n64texconv_palette_from_png.argtypes = [c_char_p, c_int]
ln64texconv.n64texconv_palette_from_png.restype = POINTER(N64Palette)
# struct n64_palette *n64texconv_palette_from_bin(void *data, size_t count, int fmt);
ln64texconv.n64texconv_palette_from_bin.argtypes = [c_void_p, c_size_t, c_int]
ln64texconv.n64texconv_palette_from_bin.restype = POINTER(N64Palette)
# int n64texconv_palette_to_png(const char *outpath, struct n64_palette *pal);
ln64texconv.n64texconv_palette_to_png.argtypes = [c_char_p, POINTER(N64Palette)]
ln64texconv.n64texconv_palette_to_png.restype = c_int
# void *n64texconv_palette_to_bin(struct n64_palette *pal, bool pad_to_8b);
ln64texconv.n64texconv_palette_to_bin.argtypes = [POINTER(N64Palette), c_bool]
ln64texconv.n64texconv_palette_to_bin.restype = c_void_p
# int n64texconv_palette_to_c(char **out, size_t *size_out, struct n64_palette *pal, bool pad_to_8b, unsigned int byte_width);
ln64texconv.n64texconv_palette_to_c.argtypes = [POINTER(c_char_p), POINTER(c_size_t), POINTER(N64Palette), c_bool, c_uint]
ln64texconv.n64texconv_palette_to_c.restype = c_int
# int n64texconv_palette_to_c_file(const char *out_path, struct n64_palette *pal, bool pad_to_8b, unsigned int byte_width);
ln64texconv.n64texconv_palette_to_c_file.argtypes = [c_char_p, POINTER(N64Palette), c_bool, c_uint]
ln64texconv.n64texconv_palette_to_c_file.restype = c_int
# struct n64_image
class N64Image(Structure):
_fields_ = [
("width", c_size_t),
("height", c_size_t),
("fmt", c_int),
("siz", c_int),
("pal", POINTER(N64Palette)),
("texels", POINTER(Color)),
("color_indices", POINTER(c_uint8)),
]
def get_palette(self) -> Optional[N64Palette]:
return deref(self.pal)
@staticmethod
def new(width : int, height : int, fmt : int, siz : int, pal : N64Palette = None) -> Optional["N64Image"]:
if not any((fmt, siz) == fmtsiz for fmtsiz in VALID_FORMAT_COMBINATIONS):
raise ValueError(f"Invalid fmt/siz combination ({fmt_name(fmt)}, {siz_name(siz)})")
if pal is not None:
_object_refcount.add_ref(byref(pal))
return deref(ln64texconv.n64texconv_image_new(width, height, fmt, siz, pal))
def __del__(self):
ln64texconv.n64texconv_image_free(byref(self))
# Also free the palette if the reference count drops to 0
_object_refcount.rm_ref(self.pal, ln64texconv.n64texconv_palette_free)
def copy(self) -> Optional["N64Image"]:
_object_refcount.add_ref(self.pal)
return deref(ln64texconv.n64texconv_image_copy(byref(self)))
@staticmethod
def from_png(path : str, fmt : int, siz : int, pal_fmt : int = FMT_NONE) -> Optional["N64Image"]:
if not os.path.isfile(path):
raise ValueError(f"Cannot open \"{path}\", is not a file")
if not any((fmt, siz) == fmtsiz for fmtsiz in VALID_FORMAT_COMBINATIONS):
raise ValueError(f"Invalid fmt/siz combination ({fmt_name(fmt)}, {siz_name(siz)})")
if fmt == G_IM_FMT_CI and pal_fmt not in (G_IM_FMT_RGBA, G_IM_FMT_IA):
raise ValueError(f"Invalid palette format {fmt_name(pal_fmt)}, must be either G_IM_FMT_RGBA or G_IM_FMT_IA")
img = deref(ln64texconv.n64texconv_image_from_png(path.encode("utf-8"), fmt, siz, pal_fmt))
_object_refcount.add_ref(img.pal)
return img
@staticmethod
def from_bin(data : bytes | memoryview, width : int, height : int, fmt : int, siz : int, pal : Optional[N64Palette] = None,
preswapped : bool = False) -> Optional["N64Image"]:
if not any((fmt, siz) == fmtsiz for fmtsiz in VALID_FORMAT_COMBINATIONS):
raise ValueError(f"Invalid fmt/siz combination ({fmt_name(fmt)}, {siz_name(siz)})")
expected_size = texel_size_bytes(width * height, siz)
if len(data) < expected_size:
raise ValueError(f"Not enough data to extract the specified image. " +
f"Expected at least 0x{expected_size:X} bytes but only got 0x{len(data):X} bytes")
buffer = (c_uint8 * len(data)).from_buffer_copy(data)
if pal:
pal = byref(pal)
_object_refcount.add_ref(pal)
img = ln64texconv.n64texconv_image_from_bin(buffer, width, height, fmt, siz, pal, preswapped)
return deref(img)
def reformat(self, fmt : int, siz : int, pal : Optional[N64Palette] = None) -> Optional["N64Image"]:
if not any((fmt, siz) == fmtsiz for fmtsiz in VALID_FORMAT_COMBINATIONS):
raise ValueError(f"Invalid fmt/siz combination ({fmt_name(fmt)}, {siz_name(siz)})")
if pal:
pal = byref(pal)
_object_refcount.add_ref(pal)
return deref(ln64texconv.n64texconv_image_reformat(byref(self), fmt, siz, pal))
def to_png(self, outpath : str, intensity_alpha : bool) -> bool:
return ln64texconv.n64texconv_image_to_png(outpath.encode("utf-8"), byref(self), intensity_alpha) == 0
def to_bin(self, pad_to_8b : bool, preswap : bool) -> Optional[bytes]:
nbytes = texel_size_bytes(self.width * self.height, self.siz)
if pad_to_8b:
nbytes = (nbytes + 7) & ~7
ptr = ln64texconv.n64texconv_image_to_bin(byref(self), pad_to_8b, preswap)
if not ptr:
return None
data = ctypes_pointer_to_bytes(ptr, nbytes)
ln64texconv.n64texconv_free(ptr)
return data
def to_c(self, pad_to_8b : bool, preswap : bool, byte_width : int) -> Optional[str]:
ptr = c_char_p(None)
size = c_size_t(0)
if ln64texconv.n64texconv_image_to_c(byref(ptr), byref(size), byref(self), pad_to_8b, preswap, byte_width) != 0:
return None
s = ctypes_buffer_to_string(ptr)
ln64texconv.n64texconv_free(ptr)
return s
def to_c_file(self, out_path : str, pad_to_8b : bool, preswap : bool, byte_width : int) -> bool:
return ln64texconv.n64texconv_image_to_c_file(out_path.encode("utf-8"), byref(self), pad_to_8b, preswap, byte_width) == 0
def png_extension(self) -> str:
return ln64texconv.n64texconv_png_extension(byref(self)).decode("utf-8")
# struct n64_image *n64texconv_image_new(size_t width, size_t height, int fmt, int siz, struct n64_palette *pal);
ln64texconv.n64texconv_image_new.argtypes = [c_size_t, c_size_t, c_int, c_int, POINTER(N64Palette)]
ln64texconv.n64texconv_image_new.restype = POINTER(N64Image)
# void n64texconv_image_free(struct n64_image *img);
ln64texconv.n64texconv_image_free.argtypes = [POINTER(N64Image)]
ln64texconv.n64texconv_image_free.restype = None
# struct n64_image *n64texconv_image_copy(struct n64_image *img);
ln64texconv.n64texconv_image_copy.argtypes = [POINTER(N64Image)]
ln64texconv.n64texconv_image_copy.restype = POINTER(N64Image)
# struct n64_image *n64texconv_image_from_png(const char *path, int fmt, int siz, int pal_fmt);
ln64texconv.n64texconv_image_from_png.argtypes = [c_char_p, c_int, c_int, c_int]
ln64texconv.n64texconv_image_from_png.restype = POINTER(N64Image)
# struct n64_image *n64texconv_image_from_bin(void *data, size_t width, size_t height, int fmt, int siz, struct n64_palette *pal, bool preswapped);
ln64texconv.n64texconv_image_from_bin.argtypes = [c_void_p, c_size_t, c_size_t, c_int, c_int, POINTER(N64Palette), c_bool]
ln64texconv.n64texconv_image_from_bin.restype = POINTER(N64Image)
# struct n64_image *n64texconv_image_reformat(struct n64_image *img, int fmt, int siz, struct n64_palette *pal);
ln64texconv.n64texconv_image_reformat.argtypes = [POINTER(N64Image), c_int, c_int, POINTER(N64Palette)]
ln64texconv.n64texconv_image_reformat.restype = POINTER(N64Image)
# int n64texconv_image_to_png(const char *outpath, struct n64_image *img, bool intensity_alpha);
ln64texconv.n64texconv_image_to_png.argtypes = [c_char_p, POINTER(N64Image), c_bool]
ln64texconv.n64texconv_image_to_png.restype = c_int
# void *n64texconv_image_to_bin(struct n64_image *img, bool pad_to_8b, bool preswap);
ln64texconv.n64texconv_image_to_bin.argtypes = [POINTER(N64Image), c_bool, c_bool]
ln64texconv.n64texconv_image_to_bin.restype = c_void_p
# int n64texconv_image_to_c(char **out, size_t *size_out, struct n64_image *img, bool pad_to_8b, bool preswap, unsigned int byte_width);
ln64texconv.n64texconv_image_to_c.argtypes = [POINTER(c_char_p), POINTER(c_size_t), POINTER(N64Image), c_bool, c_bool, c_uint]
ln64texconv.n64texconv_image_to_c.restype = c_int
# int n64texconv_image_to_c_file(const char *out_path, struct n64_image *img, bool pad_to_8b, bool preswap, unsigned int byte_width);
ln64texconv.n64texconv_image_to_c_file.argtypes = [c_char_p, POINTER(N64Image), c_bool, c_bool, c_uint]
ln64texconv.n64texconv_image_to_c_file.restype = c_int
# const char *n64texconv_png_extension(struct n64_image *img);
ln64texconv.n64texconv_png_extension.argtypes = [POINTER(N64Image)]
ln64texconv.n64texconv_png_extension.restype = c_char_p

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,132 @@
/*
© 2011-2015 by Kornel Lesiński.
This file is part of libimagequant.
libimagequant is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
libimagequant is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with libimagequant. If not, see <http://www.gnu.org/licenses/>.
*/
#include "libimagequant.h"
#include "pam.h"
#include "blur.h"
/*
Blurs image horizontally (width 2*size+1) and writes it transposed to dst (called twice gives 2d blur)
*/
static void transposing_1d_blur(unsigned char *restrict src, unsigned char *restrict dst, unsigned int width, unsigned int height, const unsigned int size)
{
assert(size > 0);
for(unsigned int j=0; j < height; j++) {
unsigned char *restrict row = src + j*width;
// accumulate sum for pixels outside line
unsigned int sum;
sum = row[0]*size;
for(unsigned int i=0; i < size; i++) {
sum += row[i];
}
// blur with left side outside line
for(unsigned int i=0; i < size; i++) {
sum -= row[0];
sum += row[i+size];
dst[i*height + j] = sum / (size*2);
}
for(unsigned int i=size; i < width-size; i++) {
sum -= row[i-size];
sum += row[i+size];
dst[i*height + j] = sum / (size*2);
}
// blur with right side outside line
for(unsigned int i=width-size; i < width; i++) {
sum -= row[i-size];
sum += row[width-1];
dst[i*height + j] = sum / (size*2);
}
}
}
/**
* Picks maximum of neighboring pixels (blur + lighten)
*/
LIQ_PRIVATE void liq_max3(unsigned char *src, unsigned char *dst, unsigned int width, unsigned int height)
{
for(unsigned int j=0; j < height; j++) {
const unsigned char *row = src + j*width,
*prevrow = src + (j > 1 ? j-1 : 0)*width,
*nextrow = src + MIN(height-1,j+1)*width;
unsigned char prev,curr=row[0],next=row[0];
for(unsigned int i=0; i < width-1; i++) {
prev=curr;
curr=next;
next=row[i+1];
unsigned char t1 = MAX(prev,next);
unsigned char t2 = MAX(nextrow[i],prevrow[i]);
*dst++ = MAX(curr,MAX(t1,t2));
}
unsigned char t1 = MAX(curr,next);
unsigned char t2 = MAX(nextrow[width-1],prevrow[width-1]);
*dst++ = MAX(t1,t2);
}
}
/**
* Picks minimum of neighboring pixels (blur + darken)
*/
LIQ_PRIVATE void liq_min3(unsigned char *src, unsigned char *dst, unsigned int width, unsigned int height)
{
for(unsigned int j=0; j < height; j++) {
const unsigned char *row = src + j*width,
*prevrow = src + (j > 1 ? j-1 : 0)*width,
*nextrow = src + MIN(height-1,j+1)*width;
unsigned char prev,curr=row[0],next=row[0];
for(unsigned int i=0; i < width-1; i++) {
prev=curr;
curr=next;
next=row[i+1];
unsigned char t1 = MIN(prev,next);
unsigned char t2 = MIN(nextrow[i],prevrow[i]);
*dst++ = MIN(curr,MIN(t1,t2));
}
unsigned char t1 = MIN(curr,next);
unsigned char t2 = MIN(nextrow[width-1],prevrow[width-1]);
*dst++ = MIN(t1,t2);
}
}
/*
Filters src image and saves it to dst, overwriting tmp in the process.
Image must be width*height pixels high. Size controls radius of box blur.
*/
LIQ_PRIVATE void liq_blur(unsigned char *src, unsigned char *tmp, unsigned char *dst, unsigned int width, unsigned int height, unsigned int size)
{
assert(size > 0);
if (width < 2*size+1 || height < 2*size+1) {
return;
}
transposing_1d_blur(src, tmp, width, height, size);
transposing_1d_blur(tmp, dst, height, width, size);
}

View File

@@ -0,0 +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

View File

@@ -0,0 +1,119 @@
/*
** © 2011-2016 by Kornel Lesiński.
** See COPYRIGHT file for license.
*/
#include "libimagequant.h"
#include "pam.h"
#include "kmeans.h"
#include "nearest.h"
#include <stdlib.h>
#include <string.h>
#ifdef _OPENMP
#include <omp.h>
#else
#define omp_get_max_threads() 1
#define omp_get_thread_num() 0
#endif
/*
* K-Means iteration: new palette color is computed from weighted average of colors that map to that palette entry.
*/
LIQ_PRIVATE void kmeans_init(const colormap *map, const unsigned int max_threads, kmeans_state average_color[])
{
memset(average_color, 0, sizeof(average_color[0])*(KMEANS_CACHE_LINE_GAP+map->colors)*max_threads);
}
LIQ_PRIVATE void kmeans_update_color(const f_pixel acolor, const float value, const colormap *map, unsigned int match, const unsigned int thread, kmeans_state average_color[])
{
match += thread * (KMEANS_CACHE_LINE_GAP+map->colors);
average_color[match].a += acolor.a * value;
average_color[match].r += acolor.r * value;
average_color[match].g += acolor.g * value;
average_color[match].b += acolor.b * value;
average_color[match].total += value;
}
LIQ_PRIVATE void kmeans_finalize(colormap *map, const unsigned int max_threads, const kmeans_state average_color[])
{
for (unsigned int i=0; i < map->colors; i++) {
double a=0, r=0, g=0, b=0, total=0;
// Aggregate results from all threads
for(unsigned int t=0; t < max_threads; t++) {
const unsigned int offset = (KMEANS_CACHE_LINE_GAP+map->colors) * t + i;
a += average_color[offset].a;
r += average_color[offset].r;
g += average_color[offset].g;
b += average_color[offset].b;
total += average_color[offset].total;
}
if (!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, unsigned int 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);
hist_item *const achv = hist->achv;
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;
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;
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;
kmeans_update_color(px, achv[j].adjusted_weight, map, match, omp_get_thread_num(), average_color);
}
nearest_free(n);
kmeans_finalize(map, max_threads, average_color);
return total_diff / hist->total_perceptual_weight;
}

View File

@@ -0,0 +1,18 @@
#ifndef KMEANS_H
#define KMEANS_H
// Spread memory touched by different threads at least 64B apart which I assume is the cache line size. This should avoid memory write contention.
#define KMEANS_CACHE_LINE_GAP ((64+sizeof(kmeans_state)-1)/sizeof(kmeans_state))
typedef struct {
double a, r, g, b, total;
} kmeans_state;
typedef void (*kmeans_callback)(hist_item *item, float diff);
LIQ_PRIVATE void kmeans_init(const colormap *map, const unsigned int max_threads, kmeans_state state[]);
LIQ_PRIVATE void kmeans_update_color(const f_pixel acolor, const float value, const colormap *map, unsigned int match, const unsigned int thread, kmeans_state average_color[]);
LIQ_PRIVATE void kmeans_finalize(colormap *map, const unsigned int max_threads, const kmeans_state state[]);
LIQ_PRIVATE double kmeans_do_iteration(histogram *hist, colormap *const map, kmeans_callback callback, const unsigned int max_threads);
#endif

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,151 @@
/*
* https://pngquant.org
*/
#ifndef LIBIMAGEQUANT_H
#define LIBIMAGEQUANT_H
#ifdef IMAGEQUANT_EXPORTS
#define LIQ_EXPORT __declspec(dllexport)
#endif
#ifndef LIQ_EXPORT
#define LIQ_EXPORT extern
#endif
#define LIQ_VERSION 21800
#define LIQ_VERSION_STRING "2.18.0"
#ifndef LIQ_PRIVATE
#if defined(__GNUC__) || defined (__llvm__)
#define LIQ_PRIVATE __attribute__((visibility("hidden")))
#define LIQ_NONNULL __attribute__((nonnull))
#define LIQ_USERESULT __attribute__((warn_unused_result))
#else
#define LIQ_PRIVATE
#define LIQ_NONNULL
#define LIQ_USERESULT
#endif
#endif
#ifdef __cplusplus
extern "C" {
#endif
#include <stddef.h>
typedef struct liq_attr liq_attr;
typedef struct liq_image liq_image;
typedef struct liq_result liq_result;
typedef struct liq_histogram liq_histogram;
typedef struct liq_color {
unsigned char r, g, b, a;
} liq_color;
typedef struct liq_palette {
unsigned int count;
liq_color entries[256];
} liq_palette;
typedef enum liq_error {
LIQ_OK = 0,
LIQ_QUALITY_TOO_LOW = 99,
LIQ_VALUE_OUT_OF_RANGE = 100,
LIQ_OUT_OF_MEMORY,
LIQ_ABORTED,
LIQ_BITMAP_NOT_AVAILABLE,
LIQ_BUFFER_TOO_SMALL,
LIQ_INVALID_POINTER,
LIQ_UNSUPPORTED,
} liq_error;
enum liq_ownership {
LIQ_OWN_ROWS=4,
LIQ_OWN_PIXELS=8,
LIQ_COPY_PIXELS=16,
};
typedef struct liq_histogram_entry {
liq_color color;
unsigned int count;
} liq_histogram_entry;
LIQ_EXPORT LIQ_USERESULT liq_attr* liq_attr_create(void);
LIQ_EXPORT LIQ_USERESULT liq_attr* liq_attr_create_with_allocator(void* (*malloc)(size_t), void (*free)(void*));
LIQ_EXPORT LIQ_USERESULT liq_attr* liq_attr_copy(const liq_attr *orig) LIQ_NONNULL;
LIQ_EXPORT void liq_attr_destroy(liq_attr *attr) LIQ_NONNULL;
LIQ_EXPORT LIQ_USERESULT liq_histogram* liq_histogram_create(const liq_attr* attr);
LIQ_EXPORT liq_error liq_histogram_add_image(liq_histogram *hist, const liq_attr *attr, liq_image* image) LIQ_NONNULL;
LIQ_EXPORT liq_error liq_histogram_add_colors(liq_histogram *hist, const liq_attr *attr, const liq_histogram_entry entries[], int num_entries, double gamma) LIQ_NONNULL;
LIQ_EXPORT liq_error liq_histogram_add_fixed_color(liq_histogram *hist, liq_color color, double gamma) LIQ_NONNULL;
LIQ_EXPORT void liq_histogram_destroy(liq_histogram *hist) LIQ_NONNULL;
LIQ_EXPORT liq_error liq_set_max_colors(liq_attr* attr, int colors) LIQ_NONNULL;
LIQ_EXPORT LIQ_USERESULT int liq_get_max_colors(const liq_attr* attr) LIQ_NONNULL;
LIQ_EXPORT liq_error liq_set_speed(liq_attr* attr, int speed) LIQ_NONNULL;
LIQ_EXPORT LIQ_USERESULT int liq_get_speed(const liq_attr* attr) LIQ_NONNULL;
LIQ_EXPORT liq_error liq_set_min_opacity(liq_attr* attr, int min) LIQ_NONNULL;
LIQ_EXPORT LIQ_USERESULT int liq_get_min_opacity(const liq_attr* attr) LIQ_NONNULL;
LIQ_EXPORT liq_error liq_set_min_posterization(liq_attr* attr, int bits) LIQ_NONNULL;
LIQ_EXPORT LIQ_USERESULT int liq_get_min_posterization(const liq_attr* attr) LIQ_NONNULL;
LIQ_EXPORT liq_error liq_set_quality(liq_attr* attr, int minimum, int maximum) LIQ_NONNULL;
LIQ_EXPORT LIQ_USERESULT int liq_get_min_quality(const liq_attr* attr) LIQ_NONNULL;
LIQ_EXPORT LIQ_USERESULT int liq_get_max_quality(const liq_attr* attr) LIQ_NONNULL;
LIQ_EXPORT void liq_set_last_index_transparent(liq_attr* attr, int is_last) LIQ_NONNULL;
typedef void liq_log_callback_function(const liq_attr*, const char *message, void* user_info);
typedef void liq_log_flush_callback_function(const liq_attr*, void* user_info);
LIQ_EXPORT void liq_set_log_callback(liq_attr*, liq_log_callback_function*, void* user_info);
LIQ_EXPORT void liq_set_log_flush_callback(liq_attr*, liq_log_flush_callback_function*, void* user_info);
typedef int liq_progress_callback_function(float progress_percent, void* user_info);
LIQ_EXPORT void liq_attr_set_progress_callback(liq_attr*, liq_progress_callback_function*, void* user_info);
LIQ_EXPORT void liq_result_set_progress_callback(liq_result*, liq_progress_callback_function*, void* user_info);
// The rows and their data are not modified. The type of `rows` is non-const only due to a bug in C's typesystem design.
LIQ_EXPORT LIQ_USERESULT liq_image *liq_image_create_rgba_rows(const liq_attr *attr, void *const rows[], int width, int height, double gamma) LIQ_NONNULL;
LIQ_EXPORT LIQ_USERESULT liq_image *liq_image_create_rgba(const liq_attr *attr, const void *bitmap, int width, int height, double gamma) LIQ_NONNULL;
typedef void liq_image_get_rgba_row_callback(liq_color row_out[], int row, int width, void* user_info);
LIQ_EXPORT LIQ_USERESULT liq_image *liq_image_create_custom(const liq_attr *attr, liq_image_get_rgba_row_callback *row_callback, void* user_info, int width, int height, double gamma);
LIQ_EXPORT liq_error liq_image_set_memory_ownership(liq_image *image, int ownership_flags) LIQ_NONNULL;
LIQ_EXPORT liq_error liq_image_set_background(liq_image *img, liq_image *background_image) LIQ_NONNULL;
LIQ_EXPORT liq_error liq_image_set_importance_map(liq_image *img, unsigned char buffer[], size_t buffer_size, enum liq_ownership memory_handling) LIQ_NONNULL;
LIQ_EXPORT liq_error liq_image_add_fixed_color(liq_image *img, liq_color color) LIQ_NONNULL;
LIQ_EXPORT LIQ_USERESULT int liq_image_get_width(const liq_image *img) LIQ_NONNULL;
LIQ_EXPORT LIQ_USERESULT int liq_image_get_height(const liq_image *img) LIQ_NONNULL;
LIQ_EXPORT void liq_image_destroy(liq_image *img) LIQ_NONNULL;
LIQ_EXPORT LIQ_USERESULT liq_error liq_histogram_quantize(liq_histogram *const input_hist, liq_attr *const options, liq_result **result_output) LIQ_NONNULL;
LIQ_EXPORT LIQ_USERESULT liq_error liq_image_quantize(liq_image *const input_image, liq_attr *const options, liq_result **result_output) LIQ_NONNULL;
LIQ_EXPORT liq_error liq_set_dithering_level(liq_result *res, float dither_level) LIQ_NONNULL;
LIQ_EXPORT liq_error liq_set_output_gamma(liq_result* res, double gamma) LIQ_NONNULL;
LIQ_EXPORT LIQ_USERESULT double liq_get_output_gamma(const liq_result *result) LIQ_NONNULL;
LIQ_EXPORT LIQ_USERESULT const liq_palette *liq_get_palette(liq_result *result) LIQ_NONNULL;
LIQ_EXPORT liq_error liq_write_remapped_image(liq_result *result, liq_image *input_image, void *buffer, size_t buffer_size) LIQ_NONNULL;
LIQ_EXPORT liq_error liq_write_remapped_image_rows(liq_result *result, liq_image *input_image, unsigned char **row_pointers) LIQ_NONNULL;
LIQ_EXPORT double liq_get_quantization_error(const liq_result *result) LIQ_NONNULL;
LIQ_EXPORT int liq_get_quantization_quality(const liq_result *result) LIQ_NONNULL;
LIQ_EXPORT double liq_get_remapping_error(const liq_result *result) LIQ_NONNULL;
LIQ_EXPORT int liq_get_remapping_quality(const liq_result *result) LIQ_NONNULL;
LIQ_EXPORT void liq_result_destroy(liq_result *) LIQ_NONNULL;
LIQ_EXPORT int liq_version(void);
// Deprecated
LIQ_EXPORT LIQ_USERESULT liq_result *liq_quantize_image(liq_attr *options, liq_image *input_image) LIQ_NONNULL;
#ifdef __cplusplus
}
#endif
#endif

View File

@@ -0,0 +1,50 @@
#ifdef _OPENMP
#include <omp.h>
#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;

View File

@@ -0,0 +1,476 @@
/*
** © 2009-2018 by Kornel Lesiński.
** © 1989, 1991 by Jef Poskanzer.
** © 1997, 2000, 2002 by Greg Roelofs; based on an idea by Stefan Schneider.
**
** See COPYRIGHT file for license.
*/
#include <stdlib.h>
#include <stddef.h>
#include "libimagequant.h"
#include "pam.h"
#include "mediancut.h"
#define index_of_channel(ch) (offsetof(f_pixel,ch)/sizeof(float))
static f_pixel averagepixels(unsigned int clrs, const hist_item achv[]);
struct box {
f_pixel color;
f_pixel variance;
double sum, total_error, max_error;
unsigned int ind;
unsigned int colors;
};
/** 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)
{
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 += (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,
.r = variancer,
.g = varianceg,
.b = varianceb,
};
}
static double box_max_error(const hist_item achv[], const struct box *box)
{
f_pixel mean = box->color;
double max_error = 0;
for(unsigned int i = 0; i < box->colors; ++i) {
const double diff = colordifference(mean, achv[box->ind + i].acolor);
if (diff > max_error) {
max_error = diff;
}
}
return max_error;
}
ALWAYS_INLINE static double color_weight(f_pixel median, hist_item h);
static inline void hist_item_swap(hist_item *l, hist_item *r)
{
if (l != r) {
hist_item t = *l;
*l = *r;
*r = t;
}
}
ALWAYS_INLINE static unsigned int qsort_pivot(const hist_item *const base, const unsigned int len);
inline static unsigned int qsort_pivot(const hist_item *const base, const unsigned int len)
{
if (len < 32) {
return len/2;
}
const unsigned int aidx=8, bidx=len/2, cidx=len-1;
const unsigned int a=base[aidx].tmp.sort_value, b=base[bidx].tmp.sort_value, c=base[cidx].tmp.sort_value;
return (a < b) ? ((b < c) ? bidx : ((a < c) ? cidx : aidx ))
: ((b > c) ? bidx : ((a < c) ? aidx : cidx ));
}
ALWAYS_INLINE static unsigned int qsort_partition(hist_item *const base, const unsigned int len);
inline static unsigned int qsort_partition(hist_item *const base, const unsigned int len)
{
unsigned int l = 1, r = len;
if (len >= 8) {
hist_item_swap(&base[0], &base[qsort_pivot(base,len)]);
}
const unsigned int pivot_value = base[0].tmp.sort_value;
while (l < r) {
if (base[l].tmp.sort_value >= pivot_value) {
l++;
} else {
while(l < --r && base[r].tmp.sort_value <= pivot_value) {}
hist_item_swap(&base[l], &base[r]);
}
}
l--;
hist_item_swap(&base[0], &base[l]);
return l;
}
/** quick select algorithm */
static void hist_item_sort_range(hist_item base[], unsigned int len, unsigned int sort_start)
{
for(;;) {
const unsigned int l = qsort_partition(base, len), r = l+1;
if (l > 0 && sort_start < l) {
len = l;
}
else if (r < len && sort_start > r) {
base += r; len -= r; sort_start -= r;
}
else break;
}
}
/** sorts array to make sum of weights lower than halfvar one side, returns index of the edge between <halfvar and >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
double tmpsum = 0.;
for(unsigned int t = 0; t <= l && tmpsum < halfvar; ++t) tmpsum += base[t].color_weight;
// the split is on the left part
if (tmpsum >= halfvar) {
if (l > 0) {
len = l;
continue;
} else {
// reached the end of left part
return base_idx;
}
}
// process the right part
halfvar -= tmpsum;
if (len > r) {
base += r;
base_idx += r;
len -= r; // tail-recursive "call"
} else {
// reached the end of the right part
return base_idx + len;
}
} while(1);
}
static f_pixel get_median(const struct box *b, hist_item achv[]);
typedef struct {
unsigned int chan; float variance;
} channelvariance;
static int comparevariance(const void *ch1, const void *ch2)
{
return ((const channelvariance*)ch1)->variance > ((const channelvariance*)ch2)->variance ? -1 :
(((const channelvariance*)ch1)->variance < ((const channelvariance*)ch2)->variance ? 1 : 0);
}
/** Finds which channels need to be sorted first and preproceses achv for fast sort */
static double prepare_sort(struct box *b, hist_item achv[])
{
/*
** Sort dimensions by their variance, and then sort colors first by dimension with highest variance
*/
channelvariance channels[4] = {
{index_of_channel(a), b->variance.a},
{index_of_channel(r), b->variance.r},
{index_of_channel(g), b->variance.g},
{index_of_channel(b), b->variance.b},
};
qsort(channels, 4, sizeof(channels[0]), comparevariance);
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
// with different histogram weights, I don't want sort randomness to influence outcome.
achv[ind1 + i].tmp.sort_value = ((unsigned int)(chans[channels[0].chan]*65535.0)<<16) |
(unsigned int)((chans[channels[2].chan] + chans[channels[1].chan]/2.0 + chans[channels[3].chan]/4.0)*65535.0);
}
const f_pixel median = get_median(b, achv);
// box will be split to make color_weight of each side even
const unsigned int ind = b->ind, end = ind+b->colors;
double totalvar = 0;
#pragma omp parallel for if (end - ind > 15000) \
schedule(static) default(shared) reduction(+:totalvar)
for(unsigned int j=ind; j < end; j++) totalvar += (achv[j].color_weight = color_weight(median, achv[j]));
return totalvar / 2.0;
}
/** finds median in unsorted set by sorting only minimum required */
static f_pixel get_median(const struct box *b, hist_item achv[])
{
const unsigned int median_start = (b->colors-1)/2;
hist_item_sort_range(&(achv[b->ind]), b->colors,
median_start);
if (b->colors&1) return achv[b->ind + median_start].acolor;
// technically the second color is not guaranteed to be sorted correctly
// but most of the time it is good enough to be useful
return averagepixels(2, &achv[b->ind + median_start]);
}
/*
** Find the best splittable box. -1 if no boxes are splittable.
*/
static int best_splittable_box(struct box bv[], unsigned int boxes, const double max_mse)
{
int bi=-1; double maxsum=0;
for(unsigned int i=0; i < boxes; i++) {
if (bv[i].colors < 2) {
continue;
}
// looks only at max variance, because it's only going to split by it
const double cv = MAX(bv[i].variance.r, MAX(bv[i].variance.g,bv[i].variance.b));
double thissum = bv[i].sum * MAX(bv[i].variance.a, cv);
if (bv[i].max_error > max_mse) {
thissum = thissum* bv[i].max_error/max_mse;
}
if (thissum > maxsum) {
maxsum = thissum;
bi = i;
}
}
return bi;
}
inline static double color_weight(f_pixel median, hist_item h)
{
float diff = colordifference(median, h.acolor);
return sqrt(diff) * (sqrt(1.0+h.adjusted_weight)-1.0);
}
static void set_colormap_from_boxes(colormap *map, struct box bv[], unsigned int boxes, hist_item *achv);
static void adjust_histogram(hist_item *achv, const struct box bv[], unsigned int boxes);
static double box_error(const struct box *box, const hist_item achv[])
{
f_pixel avg = box->color;
double total_error=0;
for (unsigned int i = 0; i < box->colors; ++i) {
total_error += colordifference(avg, achv[box->ind + i].acolor) * achv[box->ind + i].perceptual_weight;
}
return total_error;
}
static bool total_box_error_below_target(double target_mse, struct box bv[], unsigned int boxes, const histogram *hist)
{
target_mse *= hist->total_perceptual_weight;
double total_error=0;
for(unsigned int i=0; i < boxes; i++) {
// error is (re)calculated lazily
if (bv[i].total_error >= 0) {
total_error += bv[i].total_error;
}
if (total_error > target_mse) return false;
}
for(unsigned int i=0; i < boxes; i++) {
if (bv[i].total_error < 0) {
bv[i].total_error = box_error(&bv[i], hist->achv);
total_error += bv[i].total_error;
}
if (total_error > target_mse) return false;
}
return true;
}
static void box_init(struct box *box, const hist_item *achv, const unsigned int ind, const unsigned int colors, const double sum) {
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]);
box->variance = box_variance(achv, box);
box->max_error = box_max_error(achv, box);
}
/*
** Here is the fun part, the median-cut colormap generator. This is based
** on Paul Heckbert's paper, "Color Image Quantization for Frame Buffer
** Display," SIGGRAPH 1982 Proceedings, page 297.
*/
LIQ_PRIVATE colormap *mediancut(histogram *hist, unsigned int newcolors, const double target_mse, const double max_mse, void* (*malloc)(size_t), void (*free)(void*))
{
hist_item *achv = hist->achv;
struct box bv[newcolors+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;
}
double sum = 0;
for(int i=begin; i < end; i++) {
sum += achv[i].adjusted_weight;
}
box_init(&bv[boxes], achv, begin, end-begin, sum);
boxes++;
}
assert(boxes < newcolors);
/*
** Main loop: split boxes until we have enough.
*/
while (boxes < newcolors) {
// first splits boxes that exceed quality limit (to have colors for things like odd green pixel),
// later raises the limit to allow large smooth areas/gradients get colors.
const double current_max_mse = max_mse + (boxes/(double)newcolors)*16.0*max_mse;
const int bi = best_splittable_box(bv, boxes, current_max_mse);
if (bi < 0) {
break; /* ran out of colors! */
}
unsigned int indx = bv[bi].ind;
unsigned int clrs = bv[bi].colors;
/*
Classic implementation tries to get even number of colors or pixels in each subdivision.
Here, instead of popularity I use (sqrt(popularity)*variance) metric.
Each subdivision balances number of pixels (popular colors) and low variance -
boxes can be large if they have similar colors. Later boxes with high variance
will be more likely to be split.
Median used as expected value gives much better results than mean.
*/
const double halfvar = prepare_sort(&bv[bi], achv);
// 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.
unsigned int break_at = hist_item_sort_halfvar(&achv[indx], clrs, halfvar);
break_at = MIN(clrs-1, break_at + 1);
/*
** Split the box.
*/
double sm = bv[bi].sum;
double lowersum = 0;
for(unsigned int i=0; i < break_at; i++) lowersum += achv[indx + i].adjusted_weight;
box_init(&bv[bi], achv, indx, break_at, lowersum);
box_init(&bv[boxes], achv, indx + break_at, clrs - break_at, sm - lowersum);
++boxes;
if (total_box_error_below_target(target_mse, bv, boxes, hist)) {
break;
}
}
colormap *map = pam_colormap(boxes, malloc, free);
set_colormap_from_boxes(map, bv, boxes, achv);
adjust_histogram(achv, bv, boxes);
return map;
}
static void set_colormap_from_boxes(colormap *map, struct box* bv, unsigned int boxes, hist_item *achv)
{
/*
** Ok, we've got enough boxes. Now choose a representative color for
** each box. There are a number of possible ways to make this choice.
** One would be to choose the center of the box; this ignores any structure
** within the boxes. Another method would be to average all the colors in
** the box - this is the method specified in Heckbert's paper.
*/
for(unsigned int bi = 0; bi < boxes; ++bi) {
map->palette[bi].acolor = bv[bi].color;
/* store total color popularity (perceptual_weight is approximation of it) */
map->palette[bi].popularity = 0;
for(unsigned int i=bv[bi].ind; i < bv[bi].ind+bv[bi].colors; i++) {
map->palette[bi].popularity += achv[i].perceptual_weight;
}
}
}
/* increase histogram popularity by difference from the final color (this is used as part of feedback loop) */
static void adjust_histogram(hist_item *achv, const struct box* bv, unsigned int boxes)
{
for(unsigned int bi = 0; bi < boxes; ++bi) {
for(unsigned int i=bv[bi].ind; i < bv[bi].ind+bv[bi].colors; i++) {
achv[i].tmp.likely_colormap_index = bi;
}
}
}
static f_pixel averagepixels(unsigned int clrs, const hist_item achv[])
{
double r = 0, g = 0, b = 0, a = 0, sum = 0;
#pragma omp parallel for if (clrs > 25000) \
schedule(static) default(shared) reduction(+:a) reduction(+:r) reduction(+:g) reduction(+:b) reduction(+:sum)
for(unsigned int i = 0; i < clrs; i++) {
const f_pixel px = achv[i].acolor;
const double weight = achv[i].adjusted_weight;
sum += weight;
a += px.a * weight;
r += px.r * weight;
g += px.g * weight;
b += px.b * weight;
}
if (sum) {
a /= sum;
r /= sum;
g /= sum;
b /= sum;
}
assert(!isnan(r) && !isnan(g) && !isnan(b) && !isnan(a));
return (f_pixel){.r=r, .g=g, .b=b, .a=a};
}

View File

@@ -0,0 +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

View File

@@ -0,0 +1,70 @@
/*
** © 2009-2017 by Kornel Lesiński.
** © 1989, 1991 by Jef Poskanzer.
** © 1997, 2000, 2002 by Greg Roelofs; based on an idea by Stefan Schneider.
**
** See COPYRIGHT file for license.
*/
#include "libimagequant.h"
#include "mempool.h"
#include <stdlib.h>
#include <stdint.h>
#include <assert.h>
#define ALIGN_MASK 15UL
#define MEMPOOL_RESERVED ((sizeof(struct mempool)+ALIGN_MASK) & ~ALIGN_MASK)
struct mempool {
unsigned int used, size;
void* (*malloc)(size_t);
void (*free)(void*);
struct mempool *next;
};
LIQ_PRIVATE void* mempool_create(mempoolptr *mptr, const unsigned int size, unsigned int max_size, void* (*malloc)(size_t), void (*free)(void*))
{
if (*mptr && ((*mptr)->used+size) <= (*mptr)->size) {
unsigned int prevused = (*mptr)->used;
(*mptr)->used += (size+15UL) & ~0xFUL;
return ((char*)(*mptr)) + prevused;
}
mempoolptr old = *mptr;
if (!max_size) max_size = (1<<17);
max_size = size+ALIGN_MASK > max_size ? size+ALIGN_MASK : max_size;
*mptr = malloc(MEMPOOL_RESERVED + max_size);
if (!*mptr) return NULL;
**mptr = (struct mempool){
.malloc = malloc,
.free = free,
.size = MEMPOOL_RESERVED + max_size,
.used = sizeof(struct mempool),
.next = old,
};
uintptr_t mptr_used_start = (uintptr_t)(*mptr) + (*mptr)->used;
(*mptr)->used += (ALIGN_MASK + 1 - (mptr_used_start & ALIGN_MASK)) & ALIGN_MASK; // reserve bytes required to make subsequent allocations aligned
assert(!(((uintptr_t)(*mptr) + (*mptr)->used) & ALIGN_MASK));
return mempool_alloc(mptr, size, size);
}
LIQ_PRIVATE void* mempool_alloc(mempoolptr *mptr, const unsigned int size, const unsigned int max_size)
{
if (((*mptr)->used+size) <= (*mptr)->size) {
unsigned int prevused = (*mptr)->used;
(*mptr)->used += (size + ALIGN_MASK) & ~ALIGN_MASK;
return ((char*)(*mptr)) + prevused;
}
return mempool_create(mptr, size, max_size, (*mptr)->malloc, (*mptr)->free);
}
LIQ_PRIVATE void mempool_destroy(mempoolptr m)
{
while (m) {
mempoolptr next = m->next;
m->free(m);
m = next;
}
}

Some files were not shown because too many files have changed in this diff Show More