From 3d61fb85efa13203db2e4888c9e892833a164277 Mon Sep 17 00:00:00 2001 From: Tharo <17233964+Thar0@users.noreply.github.com> Date: Mon, 17 Feb 2025 22:09:42 +0000 Subject: [PATCH] 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 * 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 --- Makefile | 15 +- tools/.gitignore | 1 + tools/Makefile | 8 +- tools/assets/Makefile | 10 + tools/assets/n64texconv/.clang-format | 29 + tools/assets/n64texconv/.gitignore | 10 + tools/assets/n64texconv/LICENSE | 627 ++ tools/assets/n64texconv/Makefile | 77 + tools/assets/n64texconv/__init__.py | 472 ++ .../n64texconv/lib/libimagequant/COPYRIGHT | 641 ++ .../n64texconv/lib/libimagequant/blur.c | 132 + .../n64texconv/lib/libimagequant/blur.h | 8 + .../n64texconv/lib/libimagequant/kmeans.c | 119 + .../n64texconv/lib/libimagequant/kmeans.h | 18 + .../lib/libimagequant/libimagequant.c | 1814 +++++ .../lib/libimagequant/libimagequant.h | 151 + .../lib/libimagequant/libimagequant_private.h | 50 + .../n64texconv/lib/libimagequant/mediancut.c | 476 ++ .../n64texconv/lib/libimagequant/mediancut.h | 6 + .../n64texconv/lib/libimagequant/mempool.c | 70 + .../n64texconv/lib/libimagequant/mempool.h | 13 + .../n64texconv/lib/libimagequant/nearest.c | 230 + .../n64texconv/lib/libimagequant/nearest.h | 14 + .../assets/n64texconv/lib/libimagequant/pam.c | 351 + .../assets/n64texconv/lib/libimagequant/pam.h | 312 + .../n64texconv/lib/libimagequant/remap.c | 300 + .../n64texconv/lib/libimagequant/remap.h | 7 + tools/assets/n64texconv/lib/spng/LICENSE | 25 + tools/assets/n64texconv/lib/spng/spng.c | 6980 +++++++++++++++++ tools/assets/n64texconv/lib/spng/spng.h | 537 ++ tools/assets/n64texconv/src/LICENSE | 21 + tools/assets/n64texconv/src/app/main.c | 137 + .../n64texconv/src/libn64texconv/bin2c.c | 130 + .../n64texconv/src/libn64texconv/bin2c.h | 14 + .../n64texconv/src/libn64texconv/endian.h | 69 + .../n64texconv/src/libn64texconv/jfif.c | 76 + .../n64texconv/src/libn64texconv/jfif.h | 28 + .../n64texconv/src/libn64texconv/n64texconv.c | 1302 +++ .../n64texconv/src/libn64texconv/n64texconv.h | 122 + tools/bin2c.c | 174 + tools/endian.h | 67 + 41 files changed, 15634 insertions(+), 9 deletions(-) create mode 100644 tools/assets/Makefile create mode 100644 tools/assets/n64texconv/.clang-format create mode 100644 tools/assets/n64texconv/.gitignore create mode 100644 tools/assets/n64texconv/LICENSE create mode 100644 tools/assets/n64texconv/Makefile create mode 100644 tools/assets/n64texconv/__init__.py create mode 100644 tools/assets/n64texconv/lib/libimagequant/COPYRIGHT create mode 100644 tools/assets/n64texconv/lib/libimagequant/blur.c create mode 100644 tools/assets/n64texconv/lib/libimagequant/blur.h create mode 100644 tools/assets/n64texconv/lib/libimagequant/kmeans.c create mode 100644 tools/assets/n64texconv/lib/libimagequant/kmeans.h create mode 100644 tools/assets/n64texconv/lib/libimagequant/libimagequant.c create mode 100644 tools/assets/n64texconv/lib/libimagequant/libimagequant.h create mode 100644 tools/assets/n64texconv/lib/libimagequant/libimagequant_private.h create mode 100644 tools/assets/n64texconv/lib/libimagequant/mediancut.c create mode 100644 tools/assets/n64texconv/lib/libimagequant/mediancut.h create mode 100644 tools/assets/n64texconv/lib/libimagequant/mempool.c create mode 100644 tools/assets/n64texconv/lib/libimagequant/mempool.h create mode 100644 tools/assets/n64texconv/lib/libimagequant/nearest.c create mode 100644 tools/assets/n64texconv/lib/libimagequant/nearest.h create mode 100644 tools/assets/n64texconv/lib/libimagequant/pam.c create mode 100644 tools/assets/n64texconv/lib/libimagequant/pam.h create mode 100644 tools/assets/n64texconv/lib/libimagequant/remap.c create mode 100644 tools/assets/n64texconv/lib/libimagequant/remap.h create mode 100644 tools/assets/n64texconv/lib/spng/LICENSE create mode 100644 tools/assets/n64texconv/lib/spng/spng.c create mode 100644 tools/assets/n64texconv/lib/spng/spng.h create mode 100644 tools/assets/n64texconv/src/LICENSE create mode 100644 tools/assets/n64texconv/src/app/main.c create mode 100644 tools/assets/n64texconv/src/libn64texconv/bin2c.c create mode 100644 tools/assets/n64texconv/src/libn64texconv/bin2c.h create mode 100644 tools/assets/n64texconv/src/libn64texconv/endian.h create mode 100644 tools/assets/n64texconv/src/libn64texconv/jfif.c create mode 100644 tools/assets/n64texconv/src/libn64texconv/jfif.h create mode 100644 tools/assets/n64texconv/src/libn64texconv/n64texconv.c create mode 100644 tools/assets/n64texconv/src/libn64texconv/n64texconv.h create mode 100644 tools/bin2c.c create mode 100644 tools/endian.h diff --git a/Makefile b/Makefile index 37128eaf9a..31ab7fc8ae 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/tools/.gitignore b/tools/.gitignore index 8d3a321faa..5345868178 100644 --- a/tools/.gitignore +++ b/tools/.gitignore @@ -1,5 +1,6 @@ # Output files *.exe +bin2c elf2rom makeromfs mkdmadata diff --git a/tools/Makefile b/tools/Makefile index dae70625d5..6e311ab398 100644 --- a/tools/Makefile +++ b/tools/Makefile @@ -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 diff --git a/tools/assets/Makefile b/tools/assets/Makefile new file mode 100644 index 0000000000..d445d38794 --- /dev/null +++ b/tools/assets/Makefile @@ -0,0 +1,10 @@ +all: + $(MAKE) -C n64texconv + +clean: + $(MAKE) -C n64texconv clean + +distclean: clean + $(MAKE) -C n64texconv distclean + +.PHONY: all clean distclean diff --git a/tools/assets/n64texconv/.clang-format b/tools/assets/n64texconv/.clang-format new file mode 100644 index 0000000000..20dda610d7 --- /dev/null +++ b/tools/assets/n64texconv/.clang-format @@ -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'] diff --git a/tools/assets/n64texconv/.gitignore b/tools/assets/n64texconv/.gitignore new file mode 100644 index 0000000000..084dd7fab5 --- /dev/null +++ b/tools/assets/n64texconv/.gitignore @@ -0,0 +1,10 @@ +build/ + +libn64texconv.a +libn64texconv.dll +libn64texconv.so +n64texconv + +# Tests +*.png +*.bin diff --git a/tools/assets/n64texconv/LICENSE b/tools/assets/n64texconv/LICENSE new file mode 100644 index 0000000000..77c1ae07c8 --- /dev/null +++ b/tools/assets/n64texconv/LICENSE @@ -0,0 +1,627 @@ + +N64 texture converter (n64texconv). + +The library is under the MIT license (see src/LICENSE), but the app +links statically with libimagequant which is under the GPL. + +Copyright (C) 2025 ZeldaRET + + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. diff --git a/tools/assets/n64texconv/Makefile b/tools/assets/n64texconv/Makefile new file mode 100644 index 0000000000..fbe0300d77 --- /dev/null +++ b/tools/assets/n64texconv/Makefile @@ -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) diff --git a/tools/assets/n64texconv/__init__.py b/tools/assets/n64texconv/__init__.py new file mode 100644 index 0000000000..9e4ea1c510 --- /dev/null +++ b/tools/assets/n64texconv/__init__.py @@ -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 diff --git a/tools/assets/n64texconv/lib/libimagequant/COPYRIGHT b/tools/assets/n64texconv/lib/libimagequant/COPYRIGHT new file mode 100644 index 0000000000..4297b943e7 --- /dev/null +++ b/tools/assets/n64texconv/lib/libimagequant/COPYRIGHT @@ -0,0 +1,641 @@ + +libimagequant is derived from code by Jef Poskanzer and Greg Roelofs +licensed under pngquant's original license (at the end of this file), +and contains extensive changes and additions by Kornel Lesiński +licensed under GPL v3 or later. + +- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +libimagequant © 2009-2018 by Kornel Lesiński. + + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + +- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + +© 1989, 1991 by Jef Poskanzer. +© 1997, 2000, 2002 by Greg Roelofs. + +Permission to use, copy, modify, and distribute this software and its +documentation for any purpose and without fee is hereby granted, provided +that the above copyright notice appear in all copies and that both that +copyright notice and this permission notice appear in supporting +documentation. This software is provided "as is" without express or +implied warranty. diff --git a/tools/assets/n64texconv/lib/libimagequant/blur.c b/tools/assets/n64texconv/lib/libimagequant/blur.c new file mode 100644 index 0000000000..7f0a71672c --- /dev/null +++ b/tools/assets/n64texconv/lib/libimagequant/blur.c @@ -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 . +*/ + +#include "libimagequant.h" +#include "pam.h" +#include "blur.h" + +/* + Blurs image horizontally (width 2*size+1) and writes it transposed to dst (called twice gives 2d blur) + */ +static void transposing_1d_blur(unsigned char *restrict src, unsigned char *restrict dst, unsigned int width, unsigned int height, const unsigned int size) +{ + assert(size > 0); + + for(unsigned int j=0; j < height; j++) { + unsigned char *restrict row = src + j*width; + + // accumulate sum for pixels outside line + unsigned int sum; + sum = row[0]*size; + for(unsigned int i=0; i < size; i++) { + sum += row[i]; + } + + // blur with left side outside line + for(unsigned int i=0; i < size; i++) { + sum -= row[0]; + sum += row[i+size]; + + dst[i*height + j] = sum / (size*2); + } + + for(unsigned int i=size; i < width-size; i++) { + sum -= row[i-size]; + sum += row[i+size]; + + dst[i*height + j] = sum / (size*2); + } + + // blur with right side outside line + for(unsigned int i=width-size; i < width; i++) { + sum -= row[i-size]; + sum += row[width-1]; + + dst[i*height + j] = sum / (size*2); + } + } +} + +/** + * Picks maximum of neighboring pixels (blur + lighten) + */ +LIQ_PRIVATE void liq_max3(unsigned char *src, unsigned char *dst, unsigned int width, unsigned int height) +{ + for(unsigned int j=0; j < height; j++) { + const unsigned char *row = src + j*width, + *prevrow = src + (j > 1 ? j-1 : 0)*width, + *nextrow = src + MIN(height-1,j+1)*width; + + unsigned char prev,curr=row[0],next=row[0]; + + for(unsigned int i=0; i < width-1; i++) { + prev=curr; + curr=next; + next=row[i+1]; + + unsigned char t1 = MAX(prev,next); + unsigned char t2 = MAX(nextrow[i],prevrow[i]); + *dst++ = MAX(curr,MAX(t1,t2)); + } + unsigned char t1 = MAX(curr,next); + unsigned char t2 = MAX(nextrow[width-1],prevrow[width-1]); + *dst++ = MAX(t1,t2); + } +} + +/** + * Picks minimum of neighboring pixels (blur + darken) + */ +LIQ_PRIVATE void liq_min3(unsigned char *src, unsigned char *dst, unsigned int width, unsigned int height) +{ + for(unsigned int j=0; j < height; j++) { + const unsigned char *row = src + j*width, + *prevrow = src + (j > 1 ? j-1 : 0)*width, + *nextrow = src + MIN(height-1,j+1)*width; + + unsigned char prev,curr=row[0],next=row[0]; + + for(unsigned int i=0; i < width-1; i++) { + prev=curr; + curr=next; + next=row[i+1]; + + unsigned char t1 = MIN(prev,next); + unsigned char t2 = MIN(nextrow[i],prevrow[i]); + *dst++ = MIN(curr,MIN(t1,t2)); + } + unsigned char t1 = MIN(curr,next); + unsigned char t2 = MIN(nextrow[width-1],prevrow[width-1]); + *dst++ = MIN(t1,t2); + } +} + +/* + Filters src image and saves it to dst, overwriting tmp in the process. + Image must be width*height pixels high. Size controls radius of box blur. + */ +LIQ_PRIVATE void liq_blur(unsigned char *src, unsigned char *tmp, unsigned char *dst, unsigned int width, unsigned int height, unsigned int size) +{ + assert(size > 0); + if (width < 2*size+1 || height < 2*size+1) { + return; + } + transposing_1d_blur(src, tmp, width, height, size); + transposing_1d_blur(tmp, dst, height, width, size); +} diff --git a/tools/assets/n64texconv/lib/libimagequant/blur.h b/tools/assets/n64texconv/lib/libimagequant/blur.h new file mode 100644 index 0000000000..1e7781920b --- /dev/null +++ b/tools/assets/n64texconv/lib/libimagequant/blur.h @@ -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 diff --git a/tools/assets/n64texconv/lib/libimagequant/kmeans.c b/tools/assets/n64texconv/lib/libimagequant/kmeans.c new file mode 100644 index 0000000000..6d1a122879 --- /dev/null +++ b/tools/assets/n64texconv/lib/libimagequant/kmeans.c @@ -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 +#include + +#ifdef _OPENMP +#include +#else +#define omp_get_max_threads() 1 +#define omp_get_thread_num() 0 +#endif + +/* + * K-Means iteration: new palette color is computed from weighted average of colors that map to that palette entry. + */ +LIQ_PRIVATE void kmeans_init(const colormap *map, const unsigned int max_threads, kmeans_state average_color[]) +{ + memset(average_color, 0, sizeof(average_color[0])*(KMEANS_CACHE_LINE_GAP+map->colors)*max_threads); +} + +LIQ_PRIVATE void kmeans_update_color(const f_pixel acolor, const float value, const colormap *map, unsigned int match, const unsigned int thread, kmeans_state average_color[]) +{ + match += thread * (KMEANS_CACHE_LINE_GAP+map->colors); + average_color[match].a += acolor.a * value; + average_color[match].r += acolor.r * value; + average_color[match].g += acolor.g * value; + average_color[match].b += acolor.b * value; + average_color[match].total += value; +} + +LIQ_PRIVATE void kmeans_finalize(colormap *map, const unsigned int max_threads, const kmeans_state average_color[]) +{ + for (unsigned int i=0; i < map->colors; i++) { + double a=0, r=0, g=0, b=0, total=0; + + // Aggregate results from all threads + for(unsigned int t=0; t < max_threads; t++) { + const unsigned int offset = (KMEANS_CACHE_LINE_GAP+map->colors) * t + i; + + a += average_color[offset].a; + r += average_color[offset].r; + g += average_color[offset].g; + b += average_color[offset].b; + total += average_color[offset].total; + } + + if (!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; +} diff --git a/tools/assets/n64texconv/lib/libimagequant/kmeans.h b/tools/assets/n64texconv/lib/libimagequant/kmeans.h new file mode 100644 index 0000000000..78db4ccde4 --- /dev/null +++ b/tools/assets/n64texconv/lib/libimagequant/kmeans.h @@ -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 diff --git a/tools/assets/n64texconv/lib/libimagequant/libimagequant.c b/tools/assets/n64texconv/lib/libimagequant/libimagequant.c new file mode 100644 index 0000000000..632c242771 --- /dev/null +++ b/tools/assets/n64texconv/lib/libimagequant/libimagequant.c @@ -0,0 +1,1814 @@ +/* +** © 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 +#include +#include +#include +#include +#include +#include + +#if !(defined(__STDC_VERSION__) && __STDC_VERSION__ >= 199900L) && !(defined(_MSC_VER) && _MSC_VER >= 1800) +#error "This program requires C99, e.g. -std=c99 switch in GCC or it requires MSVC 18.0 or higher." +#error "Ignore torrent of syntax errors that may follow. It's only because compiler is set to use too old C version." +#endif + +#include "libimagequant.h" + +#include "pam.h" +#include "libimagequant_private.h" +#include "mediancut.h" +#include "blur.h" +#include "kmeans.h" +#include "remap.h" + +#define LIQ_HIGH_MEMORY_LIMIT (1<<26) /* avoid allocating buffers larger than 64MB */ + +// each structure has a pointer as a unique identifier that allows type checking at run time +static const char liq_attr_magic[] = "liq_attr"; +static const char liq_image_magic[] = "liq_image"; +static const char liq_result_magic[] = "liq_result"; +static const char liq_histogram_magic[] = "liq_histogram"; +static const char liq_remapping_result_magic[] = "liq_remapping_result"; +static const char liq_freed_magic[] = "free"; +#define CHECK_STRUCT_TYPE(attr, kind) liq_crash_if_invalid_handle_pointer_given((const liq_attr*)attr, kind ## _magic) +#define CHECK_USER_POINTER(ptr) liq_crash_if_invalid_pointer_given(ptr) + +struct liq_attr { + const char *magic_header; + void* (*malloc)(size_t); + void (*free)(void*); + + double target_mse, max_mse, kmeans_iteration_limit; + unsigned int max_colors, max_histogram_entries; + unsigned int min_posterization_output /* user setting */, min_posterization_input /* speed setting */; + unsigned int kmeans_iterations, feedback_loop_trials; + bool last_index_transparent, use_contrast_maps; + unsigned char use_dither_map; + unsigned char speed; + + unsigned char progress_stage1, progress_stage2, progress_stage3; + liq_progress_callback_function *progress_callback; + void *progress_callback_user_info; + + liq_log_callback_function *log_callback; + void *log_callback_user_info; + liq_log_flush_callback_function *log_flush_callback; + void *log_flush_callback_user_info; +}; + +struct liq_result { + const char *magic_header; + void* (*malloc)(size_t); + void (*free)(void*); + + liq_remapping_result *remapping; + colormap *palette; + liq_progress_callback_function *progress_callback; + void *progress_callback_user_info; + + liq_palette int_palette; + float dither_level; + double gamma, palette_error; + int min_posterization_output; + unsigned char use_dither_map; +}; + +struct liq_histogram { + const char *magic_header; + void* (*malloc)(size_t); + void (*free)(void*); + + struct acolorhash_table *acht; + double gamma; + f_pixel fixed_colors[256]; + unsigned short fixed_colors_count; + unsigned short ignorebits; + bool had_image_added; +}; + +static void contrast_maps(liq_image *image) LIQ_NONNULL; +static liq_error finalize_histogram(liq_histogram *input_hist, liq_attr *options, histogram **hist_output) LIQ_NONNULL; +static const liq_color *liq_image_get_row_rgba(liq_image *input_image, unsigned int row) LIQ_NONNULL; +static void liq_remapping_result_destroy(liq_remapping_result *result) LIQ_NONNULL; +static liq_error pngquant_quantize(histogram *hist, const liq_attr *options, const int fixed_colors_count, const f_pixel fixed_colors[], const double gamma, bool fixed_result_colors, liq_result **) LIQ_NONNULL; +static liq_error liq_histogram_quantize_internal(liq_histogram *input_hist, liq_attr *attr, bool fixed_result_colors, liq_result **result_output) LIQ_NONNULL; + +LIQ_NONNULL static void liq_verbose_printf(const liq_attr *context, const char *fmt, ...) +{ + if (context->log_callback) { + va_list va; + va_start(va, fmt); + int required_space = vsnprintf(NULL, 0, fmt, va)+1; // +\0 + va_end(va); + + LIQ_ARRAY(char, buf, required_space); + va_start(va, fmt); + vsnprintf(buf, required_space, fmt, va); + va_end(va); + + context->log_callback(context, buf, context->log_callback_user_info); + } +} + +LIQ_NONNULL inline static void verbose_print(const liq_attr *attr, const char *msg) +{ + if (attr->log_callback) { + attr->log_callback(attr, msg, attr->log_callback_user_info); + } +} + +LIQ_NONNULL static void liq_verbose_printf_flush(liq_attr *attr) +{ + if (attr->log_flush_callback) { + attr->log_flush_callback(attr, attr->log_flush_callback_user_info); + } +} + +LIQ_NONNULL static bool liq_progress(const liq_attr *attr, const float percent) +{ + return attr->progress_callback && !attr->progress_callback(percent, attr->progress_callback_user_info); +} + +LIQ_PRIVATE LIQ_NONNULL bool liq_remap_progress(const liq_remapping_result *quant, const float percent) +{ + return quant->progress_callback && !quant->progress_callback(percent, quant->progress_callback_user_info); +} + +#if USE_SSE +inline static bool is_sse_available() +{ +#if (defined(__x86_64__) || defined(__amd64) || defined(_WIN64)) + return true; +#elif _MSC_VER + int info[4]; + __cpuid(info, 1); + /* bool is implemented as a built-in type of size 1 in MSVC */ + return info[3] & (1<<26) ? true : false; +#else + int a,b,c,d; + cpuid(1, a, b, c, d); + return d & (1<<25); // edx bit 25 is set when SSE is present +#endif +} +#endif + +/* make it clear in backtrace when user-supplied handle points to invalid memory */ +NEVER_INLINE LIQ_EXPORT bool liq_crash_if_invalid_handle_pointer_given(const liq_attr *user_supplied_pointer, const char *const expected_magic_header); +LIQ_EXPORT bool liq_crash_if_invalid_handle_pointer_given(const liq_attr *user_supplied_pointer, const char *const expected_magic_header) +{ + if (!user_supplied_pointer) { + return false; + } + + if (user_supplied_pointer->magic_header == liq_freed_magic) { + fprintf(stderr, "%s used after being freed", expected_magic_header); + // this is not normal error handling, this is programmer error that should crash the program. + // program cannot safely continue if memory has been used after it's been freed. + // abort() is nasty, but security vulnerability may be worse. + abort(); + } + + return user_supplied_pointer->magic_header == expected_magic_header; +} + +NEVER_INLINE LIQ_EXPORT bool liq_crash_if_invalid_pointer_given(const void *pointer); +LIQ_EXPORT bool liq_crash_if_invalid_pointer_given(const void *pointer) +{ + if (!pointer) { + return false; + } + // Force a read from the given (potentially invalid) memory location in order to check early whether this crashes the program or not. + // It doesn't matter what value is read, the code here is just to shut the compiler up about unused read. + char test_access = *((volatile char *)pointer); + return test_access || true; +} + +LIQ_NONNULL static void liq_log_error(const liq_attr *attr, const char *msg) +{ + if (!CHECK_STRUCT_TYPE(attr, liq_attr)) return; + liq_verbose_printf(attr, " error: %s", msg); +} + +static double quality_to_mse(long quality) +{ + if (quality == 0) { + return MAX_DIFF; + } + if (quality == 100) { + return 0; + } + + // curve fudged to be roughly similar to quality of libjpeg + // except lowest 10 for really low number of colors + const double extra_low_quality_fudge = MAX(0,0.016/(0.001+quality) - 0.001); + // LIQ_WEIGHT_MSE is a fudge factor - reminder that colors are not in 0..1 range any more + return LIQ_WEIGHT_MSE * (extra_low_quality_fudge + 2.5/pow(210.0 + quality, 1.2) * (100.1-quality)/100.0); +} + +static unsigned int mse_to_quality(double mse) +{ + for(int i=100; i > 0; i--) { + if (mse <= quality_to_mse(i) + 0.000001) { // + epsilon for floating point errors + return i; + } + } + return 0; +} + +/** internally MSE is a sum of all channels with pixels 0..1 range, + but other software gives per-RGB-channel MSE for 0..255 range */ +static double mse_to_standard_mse(double mse) { + return (mse * 65536.0/6.0) / LIQ_WEIGHT_MSE; +} + +LIQ_EXPORT LIQ_NONNULL liq_error liq_set_quality(liq_attr* attr, int minimum, int target) +{ + if (!CHECK_STRUCT_TYPE(attr, liq_attr)) return LIQ_INVALID_POINTER; + if (target < 0 || target > 100 || target < minimum || minimum < 0) return LIQ_VALUE_OUT_OF_RANGE; + + attr->target_mse = quality_to_mse(target); + attr->max_mse = quality_to_mse(minimum); + return LIQ_OK; +} + +LIQ_EXPORT LIQ_NONNULL int liq_get_min_quality(const liq_attr *attr) +{ + if (!CHECK_STRUCT_TYPE(attr, liq_attr)) return -1; + return mse_to_quality(attr->max_mse); +} + +LIQ_EXPORT LIQ_NONNULL int liq_get_max_quality(const liq_attr *attr) +{ + if (!CHECK_STRUCT_TYPE(attr, liq_attr)) return -1; + return mse_to_quality(attr->target_mse); +} + + +LIQ_EXPORT LIQ_NONNULL liq_error liq_set_max_colors(liq_attr* attr, int colors) +{ + if (!CHECK_STRUCT_TYPE(attr, liq_attr)) return LIQ_INVALID_POINTER; + if (colors < 2 || colors > 256) return LIQ_VALUE_OUT_OF_RANGE; + + attr->max_colors = colors; + return LIQ_OK; +} + +LIQ_EXPORT LIQ_NONNULL int liq_get_max_colors(const liq_attr *attr) +{ + if (!CHECK_STRUCT_TYPE(attr, liq_attr)) return -1; + + return attr->max_colors; +} + +LIQ_EXPORT LIQ_NONNULL liq_error liq_set_min_posterization(liq_attr *attr, int bits) +{ + if (!CHECK_STRUCT_TYPE(attr, liq_attr)) return LIQ_INVALID_POINTER; + if (bits < 0 || bits > 4) return LIQ_VALUE_OUT_OF_RANGE; + + attr->min_posterization_output = bits; + return LIQ_OK; +} + +LIQ_EXPORT LIQ_NONNULL int liq_get_min_posterization(const liq_attr *attr) +{ + if (!CHECK_STRUCT_TYPE(attr, liq_attr)) return -1; + + return attr->min_posterization_output; +} + +LIQ_EXPORT LIQ_NONNULL liq_error liq_set_speed(liq_attr* attr, int speed) +{ + if (!CHECK_STRUCT_TYPE(attr, liq_attr)) return LIQ_INVALID_POINTER; + if (speed < 1 || speed > 10) return LIQ_VALUE_OUT_OF_RANGE; + + unsigned int iterations = MAX(8-speed, 0); + iterations += iterations * iterations/2; + attr->kmeans_iterations = iterations; + attr->kmeans_iteration_limit = 1.0/(double)(1<<(23-speed)); + attr->feedback_loop_trials = MAX(56-9*speed, 0); + + attr->max_histogram_entries = (1<<17) + (1<<18)*(10-speed); + attr->min_posterization_input = (speed >= 8) ? 1 : 0; + attr->use_dither_map = (speed <= (omp_get_max_threads() > 1 ? 7 : 5)); // parallelized dither map might speed up floyd remapping + if (attr->use_dither_map && speed < 3) { + attr->use_dither_map = 2; // always + } + attr->use_contrast_maps = (speed <= 7) || attr->use_dither_map; + attr->speed = speed; + + attr->progress_stage1 = attr->use_contrast_maps ? 20 : 8; + if (attr->feedback_loop_trials < 2) { + attr->progress_stage1 += 30; + } + attr->progress_stage3 = 50 / (1+speed); + attr->progress_stage2 = 100 - attr->progress_stage1 - attr->progress_stage3; + return LIQ_OK; +} + +LIQ_EXPORT LIQ_NONNULL int liq_get_speed(const liq_attr *attr) +{ + if (!CHECK_STRUCT_TYPE(attr, liq_attr)) return -1; + + return attr->speed; +} + +LIQ_EXPORT LIQ_NONNULL liq_error liq_set_output_gamma(liq_result* res, double gamma) +{ + if (!CHECK_STRUCT_TYPE(res, liq_result)) return LIQ_INVALID_POINTER; + if (gamma <= 0 || gamma >= 1.0) return LIQ_VALUE_OUT_OF_RANGE; + + if (res->remapping) { + liq_remapping_result_destroy(res->remapping); + res->remapping = NULL; + } + + res->gamma = gamma; + return LIQ_OK; +} + +LIQ_EXPORT LIQ_NONNULL liq_error liq_set_min_opacity(liq_attr* attr, int min) +{ + return LIQ_OK; +} + +LIQ_EXPORT LIQ_NONNULL int liq_get_min_opacity(const liq_attr *attr) +{ + return 0; +} + +LIQ_EXPORT LIQ_NONNULL void liq_set_last_index_transparent(liq_attr* attr, int is_last) +{ + if (!CHECK_STRUCT_TYPE(attr, liq_attr)) return; + + attr->last_index_transparent = !!is_last; +} + +LIQ_EXPORT void liq_attr_set_progress_callback(liq_attr *attr, liq_progress_callback_function *callback, void *user_info) +{ + if (!CHECK_STRUCT_TYPE(attr, liq_attr)) return; + + attr->progress_callback = callback; + attr->progress_callback_user_info = user_info; +} + +LIQ_EXPORT void liq_result_set_progress_callback(liq_result *result, liq_progress_callback_function *callback, void *user_info) +{ + if (!CHECK_STRUCT_TYPE(result, liq_result)) return; + + result->progress_callback = callback; + result->progress_callback_user_info = user_info; +} + +LIQ_EXPORT void liq_set_log_callback(liq_attr *attr, liq_log_callback_function *callback, void* user_info) +{ + if (!CHECK_STRUCT_TYPE(attr, liq_attr)) return; + + liq_verbose_printf_flush(attr); + attr->log_callback = callback; + attr->log_callback_user_info = user_info; +} + +LIQ_EXPORT void liq_set_log_flush_callback(liq_attr *attr, liq_log_flush_callback_function *callback, void* user_info) +{ + if (!CHECK_STRUCT_TYPE(attr, liq_attr)) return; + + attr->log_flush_callback = callback; + attr->log_flush_callback_user_info = user_info; +} + +LIQ_EXPORT liq_attr* liq_attr_create() +{ + return liq_attr_create_with_allocator(NULL, NULL); +} + +LIQ_EXPORT LIQ_NONNULL void liq_attr_destroy(liq_attr *attr) +{ + if (!CHECK_STRUCT_TYPE(attr, liq_attr)) { + return; + } + + liq_verbose_printf_flush(attr); + + attr->magic_header = liq_freed_magic; + attr->free(attr); +} + +LIQ_EXPORT LIQ_NONNULL liq_attr* liq_attr_copy(const liq_attr *orig) +{ + if (!CHECK_STRUCT_TYPE(orig, liq_attr)) { + return NULL; + } + + liq_attr *attr = orig->malloc(sizeof(liq_attr)); + if (!attr) return NULL; + *attr = *orig; + return attr; +} + +static void *liq_aligned_malloc(size_t size) +{ + unsigned char *ptr = malloc(size + 16); + if (!ptr) { + return NULL; + } + + uintptr_t offset = 16 - ((uintptr_t)ptr & 15); // also reserves 1 byte for ptr[-1] + ptr += offset; + assert(0 == (((uintptr_t)ptr) & 15)); + ptr[-1] = offset ^ 0x59; // store how much pointer was shifted to get the original for free() + return ptr; +} + +LIQ_NONNULL static void liq_aligned_free(void *inptr) +{ + unsigned char *ptr = inptr; + size_t offset = ptr[-1] ^ 0x59; + assert(offset > 0 && offset <= 16); + free(ptr - offset); +} + +LIQ_EXPORT liq_attr* liq_attr_create_with_allocator(void* (*custom_malloc)(size_t), void (*custom_free)(void*)) +{ +#if USE_SSE + if (!is_sse_available()) { + return NULL; + } +#endif + if (!custom_malloc && !custom_free) { + custom_malloc = liq_aligned_malloc; + custom_free = liq_aligned_free; + } else if (!custom_malloc != !custom_free) { + return NULL; // either specify both or none + } + + liq_attr *attr = custom_malloc(sizeof(liq_attr)); + if (!attr) return NULL; + *attr = (liq_attr) { + .magic_header = liq_attr_magic, + .malloc = custom_malloc, + .free = custom_free, + .max_colors = 256, + .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, 4); + return attr; +} + +LIQ_EXPORT LIQ_NONNULL liq_error liq_image_add_fixed_color(liq_image *img, liq_color color) +{ + if (!CHECK_STRUCT_TYPE(img, liq_image)) return LIQ_INVALID_POINTER; + if (img->fixed_colors_count > 255) return LIQ_UNSUPPORTED; + + float gamma_lut[256]; + to_f_set_gamma(gamma_lut, img->gamma); + img->fixed_colors[img->fixed_colors_count++] = rgba_to_f(gamma_lut, (liq_color){ + .r = color.r, + .g = color.g, + .b = color.b, + .a = color.a, + }); + return LIQ_OK; +} + +LIQ_NONNULL static liq_error liq_histogram_add_fixed_color_f(liq_histogram *hist, f_pixel color) +{ + if (hist->fixed_colors_count > 255) return LIQ_UNSUPPORTED; + + hist->fixed_colors[hist->fixed_colors_count++] = color; + return LIQ_OK; +} + +LIQ_EXPORT LIQ_NONNULL liq_error liq_histogram_add_fixed_color(liq_histogram *hist, liq_color color, double gamma) +{ + if (!CHECK_STRUCT_TYPE(hist, liq_histogram)) return LIQ_INVALID_POINTER; + + float gamma_lut[256]; + to_f_set_gamma(gamma_lut, gamma ? gamma : 0.45455); + const f_pixel px = rgba_to_f(gamma_lut, (liq_color){ + .r = color.r, + .g = color.g, + .b = color.b, + .a = color.a, + }); + return liq_histogram_add_fixed_color_f(hist, px); +} + +LIQ_NONNULL static bool liq_image_use_low_memory(liq_image *img) +{ + if (img->temp_f_row) { + img->free(img->temp_f_row); + } + img->temp_f_row = img->malloc(sizeof(img->f_pixels[0]) * LIQ_TEMP_ROW_WIDTH(img->width) * omp_get_max_threads()); + return img->temp_f_row != NULL; +} + +LIQ_NONNULL static bool liq_image_should_use_low_memory(liq_image *img, const bool low_memory_hint) +{ + return (size_t)img->width * (size_t)img->height > (low_memory_hint ? LIQ_HIGH_MEMORY_LIMIT/8 : LIQ_HIGH_MEMORY_LIMIT) / sizeof(f_pixel); // Watch out for integer overflow +} + +static liq_image *liq_image_create_internal(const liq_attr *attr, liq_color* rows[], liq_image_get_rgba_row_callback *row_callback, void *row_callback_user_info, int width, int height, double gamma) +{ + if (gamma < 0 || gamma > 1.0) { + liq_log_error(attr, "gamma must be >= 0 and <= 1 (try 1/gamma instead)"); + return NULL; + } + + if (!rows && !row_callback) { + liq_log_error(attr, "missing row data"); + return NULL; + } + + liq_image *img = attr->malloc(sizeof(liq_image)); + if (!img) return NULL; + *img = (liq_image){ + .magic_header = liq_image_magic, + .malloc = attr->malloc, + .free = attr->free, + .width = width, .height = height, + .gamma = gamma ? gamma : 0.45455, + .rows = rows, + .row_callback = row_callback, + .row_callback_user_info = row_callback_user_info, + }; + + if (!rows) { + img->temp_row = attr->malloc(sizeof(img->temp_row[0]) * LIQ_TEMP_ROW_WIDTH(width) * omp_get_max_threads()); + if (!img->temp_row) return NULL; + } + + // 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; + } + + return img; +} + +LIQ_EXPORT LIQ_NONNULL liq_error liq_image_set_memory_ownership(liq_image *img, int ownership_flags) +{ + if (!CHECK_STRUCT_TYPE(img, liq_image)) return LIQ_INVALID_POINTER; + if (!img->rows || !ownership_flags || (ownership_flags & ~(LIQ_OWN_ROWS|LIQ_OWN_PIXELS))) { + return LIQ_VALUE_OUT_OF_RANGE; + } + + if (ownership_flags & LIQ_OWN_ROWS) { + if (img->free_rows_internal) return LIQ_VALUE_OUT_OF_RANGE; + img->free_rows = true; + } + + if (ownership_flags & LIQ_OWN_PIXELS) { + img->free_pixels = true; + if (!img->pixels) { + // for simplicity of this API there's no explicit bitmap argument, + // so the row with the lowest address is assumed to be at the start of the bitmap + img->pixels = img->rows[0]; + for(unsigned int i=1; i < img->height; i++) { + img->pixels = MIN(img->pixels, img->rows[i]); + } + } + } + + return LIQ_OK; +} + +LIQ_NONNULL static void liq_image_free_maps(liq_image *input_image); +LIQ_NONNULL static void liq_image_free_dither_map(liq_image *input_image); +LIQ_NONNULL static void liq_image_free_importance_map(liq_image *input_image); + +LIQ_EXPORT LIQ_NONNULL liq_error liq_image_set_importance_map(liq_image *img, unsigned char importance_map[], size_t buffer_size, enum liq_ownership ownership) { + if (!CHECK_STRUCT_TYPE(img, liq_image)) return LIQ_INVALID_POINTER; + if (!CHECK_USER_POINTER(importance_map)) return LIQ_INVALID_POINTER; + + const size_t required_size = (size_t)img->width * (size_t)img->height; + if (buffer_size < required_size) { + return LIQ_BUFFER_TOO_SMALL; + } + + if (ownership == LIQ_COPY_PIXELS) { + unsigned char *tmp = img->malloc(required_size); + if (!tmp) { + return LIQ_OUT_OF_MEMORY; + } + memcpy(tmp, importance_map, required_size); + importance_map = tmp; + } else if (ownership != LIQ_OWN_PIXELS) { + return LIQ_UNSUPPORTED; + } + + liq_image_free_importance_map(img); + img->importance_map = importance_map; + + return LIQ_OK; +} + +LIQ_EXPORT LIQ_NONNULL liq_error liq_image_set_background(liq_image *img, liq_image *background) +{ + if (!CHECK_STRUCT_TYPE(img, liq_image)) return LIQ_INVALID_POINTER; + if (!CHECK_STRUCT_TYPE(background, liq_image)) return LIQ_INVALID_POINTER; + + if (background->background) { + return LIQ_UNSUPPORTED; + } + if (img->width != background->width || img->height != background->height) { + return LIQ_BUFFER_TOO_SMALL; + } + + if (img->background) { + liq_image_destroy(img->background); + } + + img->background = background; + liq_image_free_dither_map(img); // Force it to be re-analyzed with the background + + return LIQ_OK; +} + +LIQ_NONNULL static bool check_image_size(const liq_attr *attr, const int width, const int height) +{ + if (!CHECK_STRUCT_TYPE(attr, liq_attr)) { + return false; + } + + if (width <= 0 || height <= 0) { + liq_log_error(attr, "width and height must be > 0"); + return false; + } + + if (width > INT_MAX/sizeof(liq_color)/height || width > INT_MAX/16/sizeof(f_pixel) || height > INT_MAX/sizeof(size_t)) { + liq_log_error(attr, "image too large"); + return false; + } + return true; +} + +LIQ_EXPORT liq_image *liq_image_create_custom(const liq_attr *attr, liq_image_get_rgba_row_callback *row_callback, void* user_info, int width, int height, double gamma) +{ + if (!check_image_size(attr, width, height)) { + return NULL; + } + return liq_image_create_internal(attr, NULL, row_callback, user_info, width, height, gamma); +} + +LIQ_EXPORT liq_image *liq_image_create_rgba_rows(const liq_attr *attr, void *const rows[], int width, int height, double gamma) +{ + if (!check_image_size(attr, width, height)) { + return NULL; + } + + for(int i=0; i < height; i++) { + if (!CHECK_USER_POINTER(rows+i) || !CHECK_USER_POINTER(rows[i])) { + liq_log_error(attr, "invalid row pointers"); + return NULL; + } + } + return liq_image_create_internal(attr, (liq_color**)rows, NULL, NULL, width, height, gamma); +} + +LIQ_EXPORT LIQ_NONNULL liq_image *liq_image_create_rgba(const liq_attr *attr, const void* bitmap, int width, int height, double gamma) +{ + if (!check_image_size(attr, width, height)) { + return NULL; + } + if (!CHECK_USER_POINTER(bitmap)) { + liq_log_error(attr, "invalid bitmap pointer"); + return NULL; + } + + liq_color *const pixels = (liq_color *const)bitmap; + liq_color **rows = attr->malloc(sizeof(rows[0])*height); + if (!rows) return NULL; + + for(int i=0; i < height; i++) { + rows[i] = pixels + width * i; + } + + liq_image *image = liq_image_create_internal(attr, rows, NULL, NULL, width, height, gamma); + if (!image) { + attr->free(rows); + return NULL; + } + image->free_rows = true; + image->free_rows_internal = true; + return image; +} + +NEVER_INLINE LIQ_EXPORT void liq_executing_user_callback(liq_image_get_rgba_row_callback *callback, liq_color *temp_row, int row, int width, void *user_info); +LIQ_EXPORT void liq_executing_user_callback(liq_image_get_rgba_row_callback *callback, liq_color *temp_row, int row, int width, void *user_info) +{ + assert(callback); + assert(temp_row); + callback(temp_row, row, width, user_info); +} + +LIQ_NONNULL inline static bool liq_image_has_rgba_pixels(const liq_image *img) +{ + if (!CHECK_STRUCT_TYPE(img, liq_image)) { + return false; + } + return img->rows || (img->temp_row && img->row_callback); +} + +LIQ_NONNULL inline static bool liq_image_can_use_rgba_rows(const liq_image *img) +{ + assert(liq_image_has_rgba_pixels(img)); + return img->rows; +} + +LIQ_NONNULL static const liq_color *liq_image_get_row_rgba(liq_image *img, unsigned int row) +{ + if (liq_image_can_use_rgba_rows(img)) { + return img->rows[row]; + } + + assert(img->temp_row); + liq_color *temp_row = img->temp_row + LIQ_TEMP_ROW_WIDTH(img->width) * omp_get_thread_num(); + if (img->rows) { + memcpy(temp_row, img->rows[row], img->width * sizeof(temp_row[0])); + } else { + liq_executing_user_callback(img->row_callback, (liq_color*)temp_row, row, img->width, img->row_callback_user_info); + } + + return temp_row; +} + +LIQ_NONNULL static void convert_row_to_f(liq_image *img, f_pixel *row_f_pixels, const unsigned int row, const float gamma_lut[]) +{ + assert(row_f_pixels); + assert(!USE_SSE || 0 == ((uintptr_t)row_f_pixels & 15)); + + const liq_color *const row_pixels = liq_image_get_row_rgba(img, row); + + for(unsigned int col=0; col < img->width; col++) { + row_f_pixels[col] = rgba_to_f(gamma_lut, row_pixels[col]); + } +} + +LIQ_PRIVATE LIQ_NONNULL bool liq_image_get_row_f_init(liq_image *img) +{ + assert(omp_get_thread_num() == 0); + if (img->f_pixels) { + return true; + } + 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) { + return liq_image_use_low_memory(img); + } + + if (!liq_image_has_rgba_pixels(img)) { + return false; + } + + 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 true; +} + +LIQ_PRIVATE LIQ_NONNULL const f_pixel *liq_image_get_row_f(liq_image *img, unsigned int row) +{ + if (!img->f_pixels) { + assert(img->temp_f_row); // init should have done that + float gamma_lut[256]; + to_f_set_gamma(gamma_lut, img->gamma); + f_pixel *row_for_thread = img->temp_f_row + LIQ_TEMP_ROW_WIDTH(img->width) * omp_get_thread_num(); + convert_row_to_f(img, row_for_thread, row, gamma_lut); + return row_for_thread; + } + return img->f_pixels + img->width * row; +} + +LIQ_EXPORT LIQ_NONNULL int liq_image_get_width(const liq_image *input_image) +{ + if (!CHECK_STRUCT_TYPE(input_image, liq_image)) return -1; + return input_image->width; +} + +LIQ_EXPORT LIQ_NONNULL int liq_image_get_height(const liq_image *input_image) +{ + if (!CHECK_STRUCT_TYPE(input_image, liq_image)) return -1; + return input_image->height; +} + +typedef void free_func(void*); + +LIQ_NONNULL static free_func *get_default_image_free_func(liq_image *img) +{ + // When default allocator is used then user-supplied pointers must be freed with free() + if (img->free != liq_aligned_free) { + return img->free; + } + return free; +} + +LIQ_NONNULL static free_func *get_default_rows_free_func(liq_image *img) +{ + // When default allocator is used then user-supplied pointers must be freed with free() + if (img->free_rows_internal || img->free != liq_aligned_free) { + return img->free; + } + return free; +} + +LIQ_NONNULL static void liq_image_free_rgba_source(liq_image *input_image) +{ + if (input_image->free_pixels && input_image->pixels) { + get_default_image_free_func(input_image)(input_image->pixels); + input_image->pixels = NULL; + } + + if (input_image->free_rows && input_image->rows) { + get_default_rows_free_func(input_image)(input_image->rows); + input_image->rows = NULL; + } +} + +LIQ_NONNULL static void liq_image_free_importance_map(liq_image *input_image) { + if (input_image->importance_map) { + input_image->free(input_image->importance_map); + input_image->importance_map = NULL; + } +} + +LIQ_NONNULL static void liq_image_free_maps(liq_image *input_image) { + liq_image_free_importance_map(input_image); + + if (input_image->edges) { + input_image->free(input_image->edges); + input_image->edges = NULL; + } + liq_image_free_dither_map(input_image); +} + +LIQ_NONNULL static void liq_image_free_dither_map(liq_image *input_image) { + if (input_image->dither_map) { + input_image->free(input_image->dither_map); + input_image->dither_map = NULL; + } +} + +LIQ_EXPORT LIQ_NONNULL void liq_image_destroy(liq_image *input_image) +{ + if (!CHECK_STRUCT_TYPE(input_image, liq_image)) return; + + liq_image_free_rgba_source(input_image); + + liq_image_free_maps(input_image); + + 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); + } + + if (input_image->background) { + liq_image_destroy(input_image->background); + } + + input_image->magic_header = liq_freed_magic; + input_image->free(input_image); +} + +LIQ_EXPORT liq_histogram* liq_histogram_create(const liq_attr* attr) +{ + if (!CHECK_STRUCT_TYPE(attr, liq_attr)) { + return NULL; + } + + liq_histogram *hist = attr->malloc(sizeof(liq_histogram)); + if (!hist) return NULL; + *hist = (liq_histogram) { + .magic_header = liq_histogram_magic, + .malloc = attr->malloc, + .free = attr->free, + + .ignorebits = MAX(attr->min_posterization_output, attr->min_posterization_input), + }; + return hist; +} + +LIQ_EXPORT LIQ_NONNULL void liq_histogram_destroy(liq_histogram *hist) +{ + if (!CHECK_STRUCT_TYPE(hist, liq_histogram)) return; + hist->magic_header = liq_freed_magic; + + pam_freeacolorhash(hist->acht); + hist->free(hist); +} + +LIQ_EXPORT LIQ_NONNULL liq_result *liq_quantize_image(liq_attr *attr, liq_image *img) +{ + liq_result *res; + if (LIQ_OK != liq_image_quantize(img, attr, &res)) { + return NULL; + } + return res; +} + +LIQ_EXPORT LIQ_NONNULL liq_error liq_image_quantize(liq_image *const img, liq_attr *const attr, liq_result **result_output) +{ + if (!CHECK_STRUCT_TYPE(attr, liq_attr)) return LIQ_INVALID_POINTER; + if (!liq_image_has_rgba_pixels(img)) { + return LIQ_UNSUPPORTED; + } + + liq_histogram *hist = liq_histogram_create(attr); + if (!hist) { + return LIQ_OUT_OF_MEMORY; + } + liq_error err = liq_histogram_add_image(hist, attr, img); + if (LIQ_OK != err) { + liq_histogram_destroy(hist); + return err; + } + + err = liq_histogram_quantize_internal(hist, attr, false, result_output); + liq_histogram_destroy(hist); + + return err; +} + +LIQ_EXPORT LIQ_NONNULL liq_error liq_histogram_quantize(liq_histogram *input_hist, liq_attr *attr, liq_result **result_output) { + return liq_histogram_quantize_internal(input_hist, attr, true, result_output); +} + +LIQ_NONNULL static liq_error liq_histogram_quantize_internal(liq_histogram *input_hist, liq_attr *attr, bool fixed_result_colors, liq_result **result_output) +{ + if (!CHECK_USER_POINTER(result_output)) return LIQ_INVALID_POINTER; + *result_output = NULL; + + if (!CHECK_STRUCT_TYPE(attr, liq_attr)) return LIQ_INVALID_POINTER; + if (!CHECK_STRUCT_TYPE(input_hist, liq_histogram)) return LIQ_INVALID_POINTER; + + if (liq_progress(attr, 0)) return LIQ_ABORTED; + + histogram *hist; + liq_error err = finalize_histogram(input_hist, attr, &hist); + if (err != LIQ_OK) { + return err; + } + + err = pngquant_quantize(hist, attr, input_hist->fixed_colors_count, input_hist->fixed_colors, input_hist->gamma, fixed_result_colors, result_output); + pam_freeacolorhist(hist); + + return err; +} + +LIQ_EXPORT LIQ_NONNULL liq_error liq_set_dithering_level(liq_result *res, float dither_level) +{ + if (!CHECK_STRUCT_TYPE(res, liq_result)) return LIQ_INVALID_POINTER; + + if (res->remapping) { + liq_remapping_result_destroy(res->remapping); + res->remapping = NULL; + } + + if (dither_level < 0 || dither_level > 1.0f) return LIQ_VALUE_OUT_OF_RANGE; + res->dither_level = dither_level; + return LIQ_OK; +} + +LIQ_NONNULL static liq_remapping_result *liq_remapping_result_create(liq_result *result) +{ + if (!CHECK_STRUCT_TYPE(result, liq_result)) { + return NULL; + } + + liq_remapping_result *res = result->malloc(sizeof(liq_remapping_result)); + if (!res) return NULL; + *res = (liq_remapping_result) { + .magic_header = liq_remapping_result_magic, + .malloc = result->malloc, + .free = result->free, + .dither_level = result->dither_level, + .use_dither_map = result->use_dither_map, + .palette_error = result->palette_error, + .gamma = result->gamma, + .palette = pam_duplicate_colormap(result->palette), + .progress_callback = result->progress_callback, + .progress_callback_user_info = result->progress_callback_user_info, + .progress_stage1 = result->use_dither_map ? 20 : 0, + }; + return res; +} + +LIQ_EXPORT LIQ_NONNULL double liq_get_output_gamma(const liq_result *result) +{ + if (!CHECK_STRUCT_TYPE(result, liq_result)) return -1; + + return result->gamma; +} + +LIQ_NONNULL static void liq_remapping_result_destroy(liq_remapping_result *result) +{ + if (!CHECK_STRUCT_TYPE(result, liq_remapping_result)) return; + + if (result->palette) pam_freecolormap(result->palette); + if (result->pixels) result->free(result->pixels); + + result->magic_header = liq_freed_magic; + result->free(result); +} + +LIQ_EXPORT LIQ_NONNULL void liq_result_destroy(liq_result *res) +{ + if (!CHECK_STRUCT_TYPE(res, liq_result)) return; + + memset(&res->int_palette, 0, sizeof(liq_palette)); + + if (res->remapping) { + memset(&res->remapping->int_palette, 0, sizeof(liq_palette)); + liq_remapping_result_destroy(res->remapping); + } + + pam_freecolormap(res->palette); + + res->magic_header = liq_freed_magic; + res->free(res); +} + + +LIQ_EXPORT LIQ_NONNULL double liq_get_quantization_error(const liq_result *result) { + if (!CHECK_STRUCT_TYPE(result, liq_result)) return -1; + + if (result->palette_error >= 0) { + return mse_to_standard_mse(result->palette_error); + } + + return -1; +} + +LIQ_EXPORT LIQ_NONNULL double liq_get_remapping_error(const liq_result *result) { + if (!CHECK_STRUCT_TYPE(result, liq_result)) return -1; + + if (result->remapping && result->remapping->palette_error >= 0) { + return mse_to_standard_mse(result->remapping->palette_error); + } + + return -1; +} + +LIQ_EXPORT LIQ_NONNULL int liq_get_quantization_quality(const liq_result *result) { + if (!CHECK_STRUCT_TYPE(result, liq_result)) return -1; + + if (result->palette_error >= 0) { + return mse_to_quality(result->palette_error); + } + + return -1; +} + +LIQ_EXPORT LIQ_NONNULL int liq_get_remapping_quality(const liq_result *result) { + if (!CHECK_STRUCT_TYPE(result, liq_result)) return -1; + + if (result->remapping && result->remapping->palette_error >= 0) { + return mse_to_quality(result->remapping->palette_error); + } + + return -1; +} + +LIQ_NONNULL static int compare_popularity(const void *ch1, const void *ch2) +{ + const float v1 = ((const colormap_item*)ch1)->popularity; + const float v2 = ((const colormap_item*)ch2)->popularity; + return v1 > v2 ? -1 : 1; +} + +LIQ_NONNULL static void sort_palette_qsort(colormap *map, int start, int nelem) +{ + if (!nelem) return; + qsort(map->palette + start, nelem, sizeof(map->palette[0]), compare_popularity); +} + +#define SWAP_PALETTE(map, a,b) { \ + const colormap_item tmp = (map)->palette[(a)]; \ + (map)->palette[(a)] = (map)->palette[(b)]; \ + (map)->palette[(b)] = tmp; } + +LIQ_NONNULL static void sort_palette(colormap *map, const liq_attr *options) +{ + /* + ** Step 3.5 [GRR]: remap the palette colors so that all entries with + ** the maximal alpha value (i.e., fully opaque) are at the end and can + ** therefore be omitted from the tRNS chunk. + */ + if (options->last_index_transparent) { + for(unsigned int i=0; i < map->colors; i++) { + if (map->palette[i].acolor.a < MIN_OPAQUE_A) { + const unsigned int old = i, transparent_dest = map->colors-1; + + SWAP_PALETTE(map, transparent_dest, old); + + /* colors sorted by popularity make pngs slightly more compressible */ + sort_palette_qsort(map, 0, map->colors-1); + return; + } + } + } + + unsigned int non_fixed_colors = 0; + for(unsigned int i = 0; i < map->colors; i++) { + if (map->palette[i].fixed) { + break; + } + non_fixed_colors++; + } + + /* move transparent colors to the beginning to shrink trns chunk */ + unsigned int num_transparent = 0; + for(unsigned int i = 0; i < non_fixed_colors; i++) { + if (map->palette[i].acolor.a < 255.f/256.f * LIQ_WEIGHT_A) { + // current transparent color is swapped with earlier opaque one + if (i != num_transparent) { + SWAP_PALETTE(map, num_transparent, i); + i--; + } + num_transparent++; + } + } + + liq_verbose_printf(options, " eliminated opaque tRNS-chunk entries...%d entr%s transparent", num_transparent, (num_transparent == 1)? "y" : "ies"); + + /* colors sorted by popularity make pngs slightly more compressible + * opaque and transparent are sorted separately + */ + sort_palette_qsort(map, 0, num_transparent); + sort_palette_qsort(map, num_transparent, non_fixed_colors - num_transparent); + + if (non_fixed_colors > 9 && map->colors > 16) { + SWAP_PALETTE(map, 7, 1); // slightly improves compression + SWAP_PALETTE(map, 8, 2); + SWAP_PALETTE(map, 9, 3); + } +} + +inline static unsigned int posterize_channel(unsigned int color, unsigned int bits) +{ + return (color & ~((1<> (8-bits)); +} + +LIQ_NONNULL static void set_rounded_palette(liq_palette *const dest, colormap *const map, const double gamma, unsigned int posterize) +{ + float gamma_lut[256]; + to_f_set_gamma(gamma_lut, gamma); + + dest->count = map->colors; + for(unsigned int x = 0; x < map->colors; ++x) { + liq_color px = f_to_rgb(gamma, map->palette[x].acolor); + + px.r = posterize_channel(px.r, posterize); + px.g = posterize_channel(px.g, posterize); + px.b = posterize_channel(px.b, posterize); + px.a = posterize_channel(px.a, posterize); + + map->palette[x].acolor = rgba_to_f(gamma_lut, px); /* saves rounding error introduced by to_rgb, which makes remapping & dithering more accurate */ + + if (!px.a && !map->palette[x].fixed) { + px.r = 71; px.g = 112; px.b = 76; + } + + dest->entries[x] = (liq_color){.r=px.r,.g=px.g,.b=px.b,.a=px.a}; + } +} + +LIQ_EXPORT LIQ_NONNULL const liq_palette *liq_get_palette(liq_result *result) +{ + if (!CHECK_STRUCT_TYPE(result, liq_result)) return NULL; + + if (result->remapping && result->remapping->int_palette.count) { + return &result->remapping->int_palette; + } + + if (!result->int_palette.count) { + set_rounded_palette(&result->int_palette, result->palette, result->gamma, result->min_posterization_output); + } + return &result->int_palette; +} + +LIQ_EXPORT LIQ_NONNULL liq_error liq_histogram_add_colors(liq_histogram *input_hist, const liq_attr *options, const liq_histogram_entry entries[], int num_entries, double gamma) +{ + if (!CHECK_STRUCT_TYPE(options, liq_attr)) return LIQ_INVALID_POINTER; + if (!CHECK_STRUCT_TYPE(input_hist, liq_histogram)) return LIQ_INVALID_POINTER; + if (!CHECK_USER_POINTER(entries)) return LIQ_INVALID_POINTER; + if (gamma < 0 || gamma >= 1.0) return LIQ_VALUE_OUT_OF_RANGE; + if (num_entries <= 0 || num_entries > 1<<30) return LIQ_VALUE_OUT_OF_RANGE; + + if (input_hist->ignorebits > 0 && input_hist->had_image_added) { + return LIQ_UNSUPPORTED; + } + input_hist->ignorebits = 0; + + input_hist->had_image_added = true; + input_hist->gamma = gamma ? gamma : 0.45455; + + if (!input_hist->acht) { + input_hist->acht = pam_allocacolorhash(~0, num_entries*num_entries, 0, options->malloc, options->free); + if (!input_hist->acht) { + return LIQ_OUT_OF_MEMORY; + } + } + // Fake image size. It's only for hash size estimates. + if (!input_hist->acht->cols) { + input_hist->acht->cols = num_entries; + } + input_hist->acht->rows += num_entries; + + const unsigned int hash_size = input_hist->acht->hash_size; + for(int i=0; i < num_entries; i++) { + const liq_color rgba = { + .r = entries[i].color.r, + .g = entries[i].color.g, + .b = entries[i].color.b, + .a = entries[i].color.a, + }; + union rgba_as_int px = {rgba}; + unsigned int hash; + if (px.rgba.a) { + hash = px.l % hash_size; + } else { + hash=0; px.l=0; + } + if (!pam_add_to_hash(input_hist->acht, hash, entries[i].count, px, i, num_entries)) { + return LIQ_OUT_OF_MEMORY; + } + } + + return LIQ_OK; +} + +LIQ_EXPORT LIQ_NONNULL liq_error liq_histogram_add_image(liq_histogram *input_hist, const liq_attr *options, liq_image *input_image) +{ + if (!CHECK_STRUCT_TYPE(options, liq_attr)) return LIQ_INVALID_POINTER; + if (!CHECK_STRUCT_TYPE(input_hist, liq_histogram)) return LIQ_INVALID_POINTER; + if (!CHECK_STRUCT_TYPE(input_image, liq_image)) return LIQ_INVALID_POINTER; + + const unsigned int cols = input_image->width, rows = input_image->height; + + if (!input_image->importance_map && options->use_contrast_maps) { + contrast_maps(input_image); + } + + input_hist->gamma = input_image->gamma; + + for(int i = 0; i < input_image->fixed_colors_count; i++) { + liq_error res = liq_histogram_add_fixed_color_f(input_hist, input_image->fixed_colors[i]); + if (res != LIQ_OK) { + return res; + } + } + + /* + ** Step 2: attempt to make a histogram of the colors, unclustered. + ** If at first we don't succeed, increase ignorebits to increase color + ** coherence and try again. + */ + + if (liq_progress(options, options->progress_stage1 * 0.4f)) { + return LIQ_ABORTED; + } + + const bool all_rows_at_once = liq_image_can_use_rgba_rows(input_image); + + // Usual solution is to start from scratch when limit is exceeded, but that's not possible if it's not + // the first image added + const unsigned int max_histogram_entries = input_hist->had_image_added ? ~0 : options->max_histogram_entries; + do { + if (!input_hist->acht) { + input_hist->acht = pam_allocacolorhash(max_histogram_entries, rows*cols, input_hist->ignorebits, options->malloc, options->free); + } + if (!input_hist->acht) return LIQ_OUT_OF_MEMORY; + + // histogram uses noise contrast map for importance. Color accuracy in noisy areas is not very important. + // noise map does not include edges to avoid ruining anti-aliasing + for(unsigned int row=0; row < rows; row++) { + bool added_ok; + if (all_rows_at_once) { + added_ok = pam_computeacolorhash(input_hist->acht, (const liq_color *const *)input_image->rows, cols, rows, input_image->importance_map); + if (added_ok) break; + } else { + const liq_color* rows_p[1] = { liq_image_get_row_rgba(input_image, row) }; + added_ok = pam_computeacolorhash(input_hist->acht, rows_p, cols, 1, input_image->importance_map ? &input_image->importance_map[row * cols] : NULL); + } + if (!added_ok) { + input_hist->ignorebits++; + liq_verbose_printf(options, " too many colors! Scaling colors to improve clustering... %d", input_hist->ignorebits); + pam_freeacolorhash(input_hist->acht); + input_hist->acht = NULL; + if (liq_progress(options, options->progress_stage1 * 0.6f)) return LIQ_ABORTED; + break; + } + } + } while(!input_hist->acht); + + input_hist->had_image_added = true; + + liq_image_free_importance_map(input_image); + + if (input_image->free_pixels && input_image->f_pixels) { + liq_image_free_rgba_source(input_image); // bow can free the RGBA source if copy has been made in f_pixels + } + + return LIQ_OK; +} + +LIQ_NONNULL static liq_error finalize_histogram(liq_histogram *input_hist, liq_attr *options, histogram **hist_output) +{ + if (liq_progress(options, options->progress_stage1 * 0.9f)) { + return LIQ_ABORTED; + } + + if (!input_hist->acht) { + return LIQ_BITMAP_NOT_AVAILABLE; + } + + histogram *hist = pam_acolorhashtoacolorhist(input_hist->acht, input_hist->gamma, options->malloc, options->free); + pam_freeacolorhash(input_hist->acht); + input_hist->acht = NULL; + + if (!hist) { + return LIQ_OUT_OF_MEMORY; + } + liq_verbose_printf(options, " made histogram...%d colors found", hist->size); + remove_fixed_colors_from_histogram(hist, input_hist->fixed_colors_count, input_hist->fixed_colors, options->target_mse); + + *hist_output = hist; + return LIQ_OK; +} + +/** + Builds two maps: + importance_map - approximation of areas with high-frequency noise, except straight edges. 1=flat, 0=noisy. + edges - noise map including all edges + */ +LIQ_NONNULL static void contrast_maps(liq_image *image) +{ + const unsigned int cols = image->width, rows = image->height; + if (cols < 4 || rows < 4 || (3*cols*rows) > LIQ_HIGH_MEMORY_LIMIT) { + return; + } + + unsigned char *restrict noise = image->importance_map ? image->importance_map : image->malloc(cols*rows); + image->importance_map = NULL; + unsigned char *restrict edges = image->edges ? image->edges : image->malloc(cols*rows); + image->edges = NULL; + + unsigned char *restrict tmp = image->malloc(cols*rows); + + if (!noise || !edges || !tmp || !liq_image_get_row_f_init(image)) { + image->free(noise); + image->free(edges); + image->free(tmp); + return; + } + + const f_pixel *curr_row, *prev_row, *next_row; + curr_row = prev_row = next_row = liq_image_get_row_f(image, 0); + + for (unsigned int j=0; j < rows; j++) { + prev_row = curr_row; + curr_row = next_row; + next_row = liq_image_get_row_f(image, MIN(rows-1,j+1)); + + f_pixel prev, curr = curr_row[0], next=curr; + for (unsigned int i=0; i < cols; i++) { + prev=curr; + curr=next; + next = curr_row[MIN(cols-1,i+1)]; + + // contrast is difference between pixels neighbouring horizontally and vertically + const float a = fabsf(prev.a+next.a - curr.a*2.f), + r = fabsf(prev.r+next.r - curr.r*2.f), + g = fabsf(prev.g+next.g - curr.g*2.f), + b = fabsf(prev.b+next.b - curr.b*2.f); + + const f_pixel prevl = prev_row[i]; + const f_pixel nextl = next_row[i]; + + const float a1 = fabsf(prevl.a+nextl.a - curr.a*2.f), + r1 = fabsf(prevl.r+nextl.r - curr.r*2.f), + g1 = fabsf(prevl.g+nextl.g - curr.g*2.f), + b1 = fabsf(prevl.b+nextl.b - curr.b*2.f); + + const float horiz = MAX(MAX(a,r),MAX(g,b)); + const float vert = MAX(MAX(a1,r1),MAX(g1,b1)); + const float edge = MAX(horiz,vert); + float z = edge - fabsf(horiz-vert)*.5f; + z = 1.f - MAX(z,MIN(horiz,vert)); + z *= z; // noise is amplified + z *= z; + // 85 is about 1/3rd of weight (not 0, because noisy pixels still need to be included, just not as precisely). + const unsigned int z_int = 80 + (unsigned int)(z * 176.f); + noise[j*cols+i] = MIN(z_int, 255); + const int e_int = 255 - (int)(edge * 256.f); + edges[j*cols+i] = e_int > 0 ? MIN(e_int, 255) : 0; + } + } + + // noise areas are shrunk and then expanded to remove thin edges from the map + liq_max3(noise, tmp, cols, rows); + liq_max3(tmp, noise, cols, rows); + + liq_blur(noise, tmp, noise, cols, rows, 3); + + liq_max3(noise, tmp, cols, rows); + + liq_min3(tmp, noise, cols, rows); + liq_min3(noise, tmp, cols, rows); + liq_min3(tmp, noise, cols, rows); + + liq_min3(edges, tmp, cols, rows); + liq_max3(tmp, edges, cols, rows); + for(unsigned int i=0; i < cols*rows; i++) edges[i] = MIN(noise[i], edges[i]); + + image->free(tmp); + + image->importance_map = noise; + image->edges = edges; +} + +/** + * Builds map of neighbor pixels mapped to the same palette entry + * + * For efficiency/simplicity it mainly looks for same consecutive pixels horizontally + * and peeks 1 pixel above/below. Full 2d algorithm doesn't improve it significantly. + * Correct flood fill doesn't have visually good properties. + */ +LIQ_NONNULL static void update_dither_map(liq_image *input_image, unsigned char *const *const row_pointers, colormap *map) +{ + 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 (input_image->background && map->palette[px].acolor.a < MIN_OPAQUE_A) { + // Transparency may or may not create an edge. When there's an explicit background set, assume no edge. + continue; + } + + if (px != lastpixel || col == width-1) { + int neighbor_count = 10 * (col-lastcol); + + unsigned int i=lastcol; + while(i < col) { + if (row > 0) { + unsigned char pixelabove = row_pointers[row-1][i]; + if (pixelabove == lastpixel) neighbor_count += 15; + } + if (row < height-1) { + unsigned char pixelbelow = row_pointers[row+1][i]; + if (pixelbelow == lastpixel) neighbor_count += 15; + } + i++; + } + + while(lastcol <= col) { + int e = edges[row*width + lastcol]; + edges[row*width + lastcol++] = (e+128) * (255.f/(255+128)) * (1.f - 20.f / (20 + neighbor_count)); + } + lastpixel = px; + } + } + } + input_image->dither_map = input_image->edges; + input_image->edges = NULL; +} + +/** + * Palette can be NULL, in which case it creates a new palette from scratch. + */ +static colormap *add_fixed_colors_to_palette(colormap *palette, const int max_colors, const f_pixel fixed_colors[], const int fixed_colors_count, void* (*malloc)(size_t), void (*free)(void*)) +{ + if (!fixed_colors_count) return palette; + + colormap *newpal = pam_colormap(MIN(max_colors, (palette ? palette->colors : 0) + fixed_colors_count), malloc, free); + unsigned int i=0; + if (palette && fixed_colors_count < max_colors) { + unsigned int palette_max = MIN(palette->colors, max_colors - fixed_colors_count); + for(; i < palette_max; i++) { + newpal->palette[i] = palette->palette[i]; + } + } + for(int j=0; j < MIN(max_colors, fixed_colors_count); j++) { + newpal->palette[i++] = (colormap_item){ + .acolor = fixed_colors[j], + .fixed = true, + }; + } + if (palette) pam_freecolormap(palette); + return newpal; +} + +LIQ_NONNULL static void adjust_histogram_callback(hist_item *item, float diff) +{ + item->adjusted_weight = (item->perceptual_weight + 2.f * item->adjusted_weight) * (0.5f + diff); +} + +/** + Repeats mediancut with different histogram weights to find palette with minimum error. + + feedback_loop_trials controls how long the search will take. < 0 skips the iteration. + */ +static colormap *find_best_palette(histogram *hist, const liq_attr *options, const double max_mse, const f_pixel fixed_colors[], const unsigned int fixed_colors_count, double *palette_error_p) +{ + unsigned int max_colors = options->max_colors; + + // if output is posterized it doesn't make sense to aim for perfrect colors, so increase target_mse + // at this point actual gamma is not set, so very conservative posterization estimate is used + const double target_mse = MIN(max_mse, MAX(options->target_mse, pow((1<min_posterization_output)/1024.0, 2))); + int feedback_loop_trials = options->feedback_loop_trials; + if (hist->size > 5000) {feedback_loop_trials = (feedback_loop_trials*3 + 3)/4;} + if (hist->size > 25000) {feedback_loop_trials = (feedback_loop_trials*3 + 3)/4;} + if (hist->size > 50000) {feedback_loop_trials = (feedback_loop_trials*3 + 3)/4;} + if (hist->size > 100000) {feedback_loop_trials = (feedback_loop_trials*3 + 3)/4;} + colormap *acolormap = NULL; + double least_error = MAX_DIFF; + double target_mse_overshoot = feedback_loop_trials>0 ? 1.05 : 1.0; + const float total_trials = (float)(feedback_loop_trials>0?feedback_loop_trials:1); + int fails_in_a_row=0; + + do { + colormap *newmap; + if (hist->size && fixed_colors_count < max_colors) { + newmap = mediancut(hist, max_colors-fixed_colors_count, target_mse * target_mse_overshoot, MAX(MAX(45.0/65536.0, target_mse), least_error)*1.2, + options->malloc, options->free); + } else { + feedback_loop_trials = 0; + newmap = NULL; + } + newmap = add_fixed_colors_to_palette(newmap, max_colors, fixed_colors, fixed_colors_count, options->malloc, options->free); + if (!newmap) { + return NULL; + } + + if (feedback_loop_trials <= 0) { + return newmap; + } + + // after palette has been created, total error (MSE) is calculated to keep the best palette + // at the same time K-Means iteration is done to improve the palette + // and histogram weights are adjusted based on remapping error to give more weight to poorly matched colors + + const bool first_run_of_target_mse = !acolormap && target_mse > 0; + double total_error = kmeans_do_iteration(hist, newmap, first_run_of_target_mse ? NULL : adjust_histogram_callback, omp_get_max_threads()); + + // goal is to increase quality or to reduce number of colors used if quality is good enough + if (!acolormap || total_error < least_error || (total_error <= target_mse && newmap->colors < max_colors)) { + if (acolormap) pam_freecolormap(acolormap); + acolormap = newmap; + + if (total_error < target_mse && total_error > 0) { + // K-Means iteration improves quality above what mediancut aims for + // this compensates for it, making mediancut aim for worse + target_mse_overshoot = MIN(target_mse_overshoot*1.25, target_mse/total_error); + } + + least_error = total_error; + + // if number of colors could be reduced, try to keep it that way + // but allow extra color as a bit of wiggle room in case quality can be improved too + max_colors = MIN(newmap->colors+1, max_colors); + + feedback_loop_trials -= 1; // asymptotic improvement could make it go on forever + fails_in_a_row = 0; + } else { + fails_in_a_row++; + target_mse_overshoot = 1.0; + + // if error is really bad, it's unlikely to improve, so end sooner + feedback_loop_trials -= 5 + fails_in_a_row; + pam_freecolormap(newmap); + } + + float fraction_done = 1.f-MAX(0.f, feedback_loop_trials/total_trials); + if (liq_progress(options, options->progress_stage1 + fraction_done * options->progress_stage2)) break; + liq_verbose_printf(options, " selecting colors...%d%%", (int)(100.f * fraction_done)); + } + while(feedback_loop_trials > 0); + + *palette_error_p = least_error; + return acolormap; +} + +LIQ_NONNULL static liq_error pngquant_quantize(histogram *hist, const liq_attr *options, const int fixed_colors_count, const f_pixel fixed_colors[], const double gamma, bool fixed_result_colors, liq_result **result_output) +{ + colormap *acolormap; + double palette_error = -1; + + assert((verbose_print(options, "SLOW debug checks enabled. Recompile with NDEBUG for normal operation."),1)); + + const bool few_input_colors = hist->size+fixed_colors_count <= options->max_colors; + + if (liq_progress(options, options->progress_stage1)) return LIQ_ABORTED; + + // If image has few colors to begin with (and no quality degradation is required) + // then it's possible to skip quantization entirely + if (few_input_colors && options->target_mse == 0) { + colormap *hist_pal = histogram_to_palette(hist, options->malloc, options->free); + acolormap = add_fixed_colors_to_palette(hist_pal, options->max_colors, fixed_colors, fixed_colors_count, options->malloc, options->free); + palette_error = 0; + } else { + const double max_mse = options->max_mse * (few_input_colors ? 0.33 : 1.0); // when degrading image that's already paletted, require much higher improvement, since pal2pal often looks bad and there's little gain + acolormap = find_best_palette(hist, options, max_mse, fixed_colors, fixed_colors_count, &palette_error); + if (!acolormap) { + return LIQ_VALUE_OUT_OF_RANGE; + } + + // K-Means iteration approaches local minimum for the palette + double iteration_limit = options->kmeans_iteration_limit; + unsigned int iterations = options->kmeans_iterations; + + if (!iterations && palette_error < 0 && max_mse < MAX_DIFF) iterations = 1; // otherwise total error is never calculated and MSE limit won't work + + if (iterations) { + // likely_colormap_index (used and set in kmeans_do_iteration) can't point to index outside colormap + hist_reset_colors(hist, acolormap->colors); + + if (hist->size > 5000) {iterations = (iterations*3 + 3)/4;} + if (hist->size > 25000) {iterations = (iterations*3 + 3)/4;} + if (hist->size > 50000) {iterations = (iterations*3 + 3)/4;} + if (hist->size > 100000) {iterations = (iterations*3 + 3)/4; iteration_limit *= 2;} + + verbose_print(options, " moving colormap towards local minimum"); + + double previous_palette_error = MAX_DIFF; + + for(unsigned int i=0; i < iterations; i++) { + palette_error = kmeans_do_iteration(hist, acolormap, NULL, omp_get_max_threads()); + + if (liq_progress(options, options->progress_stage1 + options->progress_stage2 + (i * options->progress_stage3 * 0.9f) / iterations)) { + break; + } + + if (fabs(previous_palette_error-palette_error) < iteration_limit) { + break; + } + + if (palette_error > max_mse*1.5) { // probably hopeless + if (palette_error > max_mse*3.0) break; // definitely hopeless + i++; + } + + previous_palette_error = palette_error; + } + } + + if (palette_error > max_mse) { + liq_verbose_printf(options, " image degradation MSE=%.3f (Q=%d) exceeded limit of %.3f (%d)", + mse_to_standard_mse(palette_error), mse_to_quality(palette_error), + mse_to_standard_mse(max_mse), mse_to_quality(max_mse)); + pam_freecolormap(acolormap); + return LIQ_QUALITY_TOO_LOW; + } + } + + if (liq_progress(options, options->progress_stage1 + options->progress_stage2 + options->progress_stage3 * 0.95f)) { + pam_freecolormap(acolormap); + return LIQ_ABORTED; + } + + sort_palette(acolormap, options); + + // If palette was created from a multi-image histogram, + // then it shouldn't be optimized for one image during remapping + if (fixed_result_colors) { + for(unsigned int i=0; i < acolormap->colors; i++) { + acolormap->palette[i].fixed = true; + } + } + + liq_result *result = options->malloc(sizeof(liq_result)); + if (!result) return LIQ_OUT_OF_MEMORY; + *result = (liq_result){ + .magic_header = liq_result_magic, + .malloc = options->malloc, + .free = options->free, + .palette = acolormap, + .palette_error = palette_error, + .use_dither_map = options->use_dither_map, + .gamma = gamma, + .min_posterization_output = options->min_posterization_output, + }; + *result_output = result; + return LIQ_OK; +} + +LIQ_EXPORT LIQ_NONNULL liq_error liq_write_remapped_image(liq_result *result, liq_image *input_image, void *buffer, size_t buffer_size) +{ + if (!CHECK_STRUCT_TYPE(result, liq_result)) { + return LIQ_INVALID_POINTER; + } + if (!CHECK_STRUCT_TYPE(input_image, liq_image)) { + return LIQ_INVALID_POINTER; + } + if (!CHECK_USER_POINTER(buffer)) { + return LIQ_INVALID_POINTER; + } + + const size_t required_size = (size_t)input_image->width * (size_t)input_image->height; + if (buffer_size < required_size) { + return LIQ_BUFFER_TOO_SMALL; + } + + unsigned char **rows = input_image->malloc(input_image->height * sizeof(unsigned char *)); + unsigned char *buffer_bytes = buffer; + for(unsigned int i=0; i < input_image->height; i++) { + rows[i] = &buffer_bytes[input_image->width * i]; + } + + liq_error err = liq_write_remapped_image_rows(result, input_image, rows); + input_image->free(rows); + return err; +} + +LIQ_EXPORT LIQ_NONNULL liq_error liq_write_remapped_image_rows(liq_result *quant, liq_image *input_image, unsigned char **row_pointers) +{ + if (!CHECK_STRUCT_TYPE(quant, liq_result)) return LIQ_INVALID_POINTER; + if (!CHECK_STRUCT_TYPE(input_image, liq_image)) return LIQ_INVALID_POINTER; + for(unsigned int i=0; i < input_image->height; i++) { + if (!CHECK_USER_POINTER(row_pointers+i) || !CHECK_USER_POINTER(row_pointers[i])) return LIQ_INVALID_POINTER; + } + + if (quant->remapping) { + liq_remapping_result_destroy(quant->remapping); + } + liq_remapping_result *const result = quant->remapping = liq_remapping_result_create(quant); + if (!result) return LIQ_OUT_OF_MEMORY; + + if (!input_image->edges && !input_image->dither_map && quant->use_dither_map) { + contrast_maps(input_image); + } + + if (liq_remap_progress(result, result->progress_stage1 * 0.25f)) { + return LIQ_ABORTED; + } + + /* + ** Step 4: map the colors in the image to their closest match in the + ** new colormap, and write 'em out. + */ + + float remapping_error = result->palette_error; + if (result->dither_level == 0) { + set_rounded_palette(&result->int_palette, result->palette, result->gamma, quant->min_posterization_output); + remapping_error = remap_to_palette(input_image, row_pointers, result->palette); + } else { + const bool is_image_huge = (input_image->width * input_image->height) > 2000 * 2000; + const bool allow_dither_map = result->use_dither_map == 2 || (!is_image_huge && result->use_dither_map); + const bool generate_dither_map = allow_dither_map && (input_image->edges && !input_image->dither_map); + if (generate_dither_map) { + // If dithering (with dither map) is required, this image is used to find areas that require dithering + remapping_error = remap_to_palette(input_image, row_pointers, result->palette); + update_dither_map(input_image, row_pointers, result->palette); + } + + if (liq_remap_progress(result, result->progress_stage1 * 0.5f)) { + return LIQ_ABORTED; + } + + // remapping above was the last chance to do K-Means iteration, hence the final palette is set after remapping + set_rounded_palette(&result->int_palette, result->palette, result->gamma, quant->min_posterization_output); + + if (!remap_to_palette_floyd(input_image, row_pointers, result, MAX(remapping_error*2.4f, 8.f/256.f), generate_dither_map)) { + return LIQ_ABORTED; + } + } + + // remapping error from dithered image is absurd, so always non-dithered value is used + // palette_error includes some perceptual weighting from histogram which is closer correlated with dssim + // so that should be used when possible. + if (result->palette_error < 0) { + result->palette_error = remapping_error; + } + + return LIQ_OK; +} + +LIQ_EXPORT int liq_version() { + return LIQ_VERSION; +} diff --git a/tools/assets/n64texconv/lib/libimagequant/libimagequant.h b/tools/assets/n64texconv/lib/libimagequant/libimagequant.h new file mode 100644 index 0000000000..e4763ab38a --- /dev/null +++ b/tools/assets/n64texconv/lib/libimagequant/libimagequant.h @@ -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 + +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 diff --git a/tools/assets/n64texconv/lib/libimagequant/libimagequant_private.h b/tools/assets/n64texconv/lib/libimagequant/libimagequant_private.h new file mode 100644 index 0000000000..50e2c18a33 --- /dev/null +++ b/tools/assets/n64texconv/lib/libimagequant/libimagequant_private.h @@ -0,0 +1,50 @@ +#ifdef _OPENMP +#include +#define LIQ_TEMP_ROW_WIDTH(img_width) (((img_width) | 15) + 1) /* keep alignment & leave space between rows to avoid cache line contention */ +#else +#define LIQ_TEMP_ROW_WIDTH(img_width) (img_width) +#define omp_get_max_threads() 1 +#define omp_get_thread_num() 0 +#endif + +struct liq_image { + const char *magic_header; + void* (*malloc)(size_t); + void (*free)(void*); + + f_pixel *f_pixels; + liq_color **rows; + double gamma; + unsigned int width, height; + unsigned char *importance_map, *edges, *dither_map; + liq_color *pixels, *temp_row; + f_pixel *temp_f_row; + liq_image_get_rgba_row_callback *row_callback; + void *row_callback_user_info; + liq_image *background; + f_pixel fixed_colors[256]; + unsigned short fixed_colors_count; + bool free_pixels, free_rows, free_rows_internal; +}; + +typedef struct liq_remapping_result { + const char *magic_header; + void* (*malloc)(size_t); + void (*free)(void*); + + unsigned char *pixels; + colormap *palette; + liq_progress_callback_function *progress_callback; + void *progress_callback_user_info; + + liq_palette int_palette; + double gamma, palette_error; + float dither_level; + unsigned char use_dither_map; + unsigned char progress_stage1; +} liq_remapping_result; + + +LIQ_PRIVATE bool liq_image_get_row_f_init(liq_image *img) LIQ_NONNULL; +LIQ_PRIVATE const f_pixel *liq_image_get_row_f(liq_image *input_image, unsigned int row) LIQ_NONNULL; +LIQ_PRIVATE bool liq_remap_progress(const liq_remapping_result *quant, const float percent) LIQ_NONNULL; diff --git a/tools/assets/n64texconv/lib/libimagequant/mediancut.c b/tools/assets/n64texconv/lib/libimagequant/mediancut.c new file mode 100644 index 0000000000..cc241b8582 --- /dev/null +++ b/tools/assets/n64texconv/lib/libimagequant/mediancut.c @@ -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 +#include + +#include "libimagequant.h" +#include "pam.h" +#include "mediancut.h" + +#define index_of_channel(ch) (offsetof(f_pixel,ch)/sizeof(float)) + +static f_pixel averagepixels(unsigned int clrs, const hist_item achv[]); + +struct box { + f_pixel color; + f_pixel variance; + double sum, total_error, max_error; + unsigned int ind; + unsigned int colors; +}; + +/** 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 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}; +} diff --git a/tools/assets/n64texconv/lib/libimagequant/mediancut.h b/tools/assets/n64texconv/lib/libimagequant/mediancut.h new file mode 100644 index 0000000000..9a4cb534b5 --- /dev/null +++ b/tools/assets/n64texconv/lib/libimagequant/mediancut.h @@ -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 diff --git a/tools/assets/n64texconv/lib/libimagequant/mempool.c b/tools/assets/n64texconv/lib/libimagequant/mempool.c new file mode 100644 index 0000000000..cd49f59a24 --- /dev/null +++ b/tools/assets/n64texconv/lib/libimagequant/mempool.c @@ -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 +#include +#include + +#define ALIGN_MASK 15UL +#define MEMPOOL_RESERVED ((sizeof(struct mempool)+ALIGN_MASK) & ~ALIGN_MASK) + +struct mempool { + unsigned int used, size; + void* (*malloc)(size_t); + void (*free)(void*); + struct mempool *next; +}; +LIQ_PRIVATE void* mempool_create(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; + } +} diff --git a/tools/assets/n64texconv/lib/libimagequant/mempool.h b/tools/assets/n64texconv/lib/libimagequant/mempool.h new file mode 100644 index 0000000000..9b7333b117 --- /dev/null +++ b/tools/assets/n64texconv/lib/libimagequant/mempool.h @@ -0,0 +1,13 @@ +#ifndef MEMPOOL_H +#define MEMPOOL_H + +#include + +struct mempool; +typedef struct mempool *mempoolptr; + +LIQ_PRIVATE void* mempool_create(mempoolptr *mptr, const unsigned int size, unsigned int capacity, void* (*malloc)(size_t), void (*free)(void*)); +LIQ_PRIVATE void* mempool_alloc(mempoolptr *mptr, const unsigned int size, const unsigned int capacity); +LIQ_PRIVATE void mempool_destroy(mempoolptr m); + +#endif diff --git a/tools/assets/n64texconv/lib/libimagequant/nearest.c b/tools/assets/n64texconv/lib/libimagequant/nearest.c new file mode 100644 index 0000000000..7c8ee6af0a --- /dev/null +++ b/tools/assets/n64texconv/lib/libimagequant/nearest.c @@ -0,0 +1,230 @@ +/* +** © 2009-2015 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 "pam.h" +#include "nearest.h" +#include "mempool.h" +#include + +typedef struct vp_sort_tmp { + float distance_squared; + unsigned int idx; +} vp_sort_tmp; + +typedef struct vp_search_tmp { + float distance; + float distance_squared; + unsigned int idx; + int exclude; +} vp_search_tmp; + +struct leaf { + f_pixel color; + unsigned int idx; +}; + +typedef struct vp_node { + struct vp_node *near, *far; + f_pixel vantage_point; + float radius, radius_squared; + struct leaf *rest; + unsigned short idx; + unsigned short restcount; +} vp_node; + +struct nearest_map { + vp_node *root; + const colormap_item *palette; + float nearest_other_color_dist[256]; + mempoolptr mempool; +}; + +static void vp_search_node(const vp_node *node, const f_pixel *const needle, vp_search_tmp *const best_candidate); + +static int vp_compare_distance(const void *ap, const void *bp) { + float a = ((const vp_sort_tmp*)ap)->distance_squared; + float b = ((const vp_sort_tmp*)bp)->distance_squared; + return a > b ? 1 : -1; +} + +static void vp_sort_indexes_by_distance(const f_pixel vantage_point, vp_sort_tmp indexes[], int num_indexes, const colormap_item items[]) { + for(int i=0; i < num_indexes; i++) { + indexes[i].distance_squared = colordifference(vantage_point, items[indexes[i].idx].acolor); + } + qsort(indexes, num_indexes, sizeof(indexes[0]), vp_compare_distance); +} + +/* + * Usually it should pick farthest point, but picking most popular point seems to make search quicker anyway + */ +static int vp_find_best_vantage_point_index(vp_sort_tmp indexes[], int num_indexes, const colormap_item items[]) { + int best = 0; + float best_popularity = items[indexes[0].idx].popularity; + for(int i = 1; i < num_indexes; i++) { + if (items[indexes[i].idx].popularity > best_popularity) { + best_popularity = items[indexes[i].idx].popularity; + best = i; + } + } + return best; +} + +static vp_node *vp_create_node(mempoolptr *m, vp_sort_tmp indexes[], int num_indexes, const colormap_item items[]) { + if (num_indexes <= 0) { + return NULL; + } + + vp_node *node = mempool_alloc(m, sizeof(node[0]), 0); + + if (num_indexes == 1) { + *node = (vp_node){ + .vantage_point = items[indexes[0].idx].acolor, + .idx = indexes[0].idx, + .radius = MAX_DIFF, + .radius_squared = MAX_DIFF, + }; + return node; + } + + const int ref = vp_find_best_vantage_point_index(indexes, num_indexes, items); + const int ref_idx = indexes[ref].idx; + + // Removes the `ref_idx` item from remaining items, because it's included in the current node + num_indexes -= 1; + indexes[ref] = indexes[num_indexes]; + + vp_sort_indexes_by_distance(items[ref_idx].acolor, indexes, num_indexes, items); + + // Remaining items are split by the median distance + const int half_idx = num_indexes/2; + + *node = (vp_node){ + .vantage_point = items[ref_idx].acolor, + .idx = ref_idx, + .radius = sqrtf(indexes[half_idx].distance_squared), + .radius_squared = indexes[half_idx].distance_squared, + }; + if (num_indexes < 7) { + node->rest = mempool_alloc(m, sizeof(node->rest[0]) * num_indexes, 0); + node->restcount = num_indexes; + for(int i=0; i < num_indexes; i++) { + node->rest[i].idx = indexes[i].idx; + node->rest[i].color = items[indexes[i].idx].acolor; + } + } else { + node->near = vp_create_node(m, indexes, half_idx, items); + node->far = vp_create_node(m, &indexes[half_idx], num_indexes - half_idx, items); + } + + return node; +} + +LIQ_PRIVATE struct nearest_map *nearest_init(const colormap *map) { + mempoolptr m = NULL; + struct nearest_map *handle = mempool_create(&m, sizeof(handle[0]), sizeof(handle[0]) + sizeof(vp_node)*map->colors+16, map->malloc, map->free); + + LIQ_ARRAY(vp_sort_tmp, indexes, map->colors); + + for(unsigned int i=0; i < map->colors; i++) { + indexes[i].idx = i; + } + + vp_node *root = vp_create_node(&m, indexes, map->colors, map->palette); + *handle = (struct nearest_map){ + .root = root, + .palette = map->palette, + .mempool = m, + }; + + for(unsigned int i=0; i < map->colors; i++) { + vp_search_tmp best = { + .distance = MAX_DIFF, + .distance_squared = MAX_DIFF, + .exclude = i, + }; + vp_search_node(root, &map->palette[i].acolor, &best); + handle->nearest_other_color_dist[i] = best.distance * best.distance / 4.f; // half of squared distance + } + + return handle; +} + +static void vp_search_node(const vp_node *node, const f_pixel *const needle, vp_search_tmp *const best_candidate) { + do { + const float distance_squared = colordifference(node->vantage_point, *needle); + const float distance = sqrtf(distance_squared); + + if (distance_squared < best_candidate->distance_squared && best_candidate->exclude != node->idx) { + best_candidate->distance = distance; + best_candidate->distance_squared = distance_squared; + best_candidate->idx = node->idx; + } + + if (node->restcount) { + for(int i=0; i < node->restcount; i++) { + const float distance_squared = colordifference(node->rest[i].color, *needle); + if (distance_squared < best_candidate->distance_squared && best_candidate->exclude != node->rest[i].idx) { + best_candidate->distance = sqrtf(distance_squared); + best_candidate->distance_squared = distance_squared; + best_candidate->idx = node->rest[i].idx; + } + } + return; + } + + // Recurse towards most likely candidate first to narrow best candidate's distance as soon as possible + if (distance_squared < node->radius_squared) { + if (node->near) { + vp_search_node(node->near, needle, best_candidate); + } + // The best node (final answer) may be just ouside the radius, but not farther than + // the best distance we know so far. The vp_search_node above should have narrowed + // best_candidate->distance, so this path is rarely taken. + if (node->far && distance >= node->radius - best_candidate->distance) { + node = node->far; // Fast tail recursion + } else { + return; + } + } else { + if (node->far) { + vp_search_node(node->far, needle, best_candidate); + } + if (node->near && distance <= node->radius + best_candidate->distance) { + node = node->near; // Fast tail recursion + } else { + return; + } + } + } while(true); +} + +LIQ_PRIVATE unsigned int nearest_search(const struct nearest_map *handle, const f_pixel *px, const int likely_colormap_index, float *diff) { + const float guess_diff = colordifference(handle->palette[likely_colormap_index].acolor, *px); + if (guess_diff < handle->nearest_other_color_dist[likely_colormap_index]) { + if (diff) *diff = guess_diff; + return likely_colormap_index; + } + + vp_search_tmp best_candidate = { + .distance = sqrtf(guess_diff), + .distance_squared = guess_diff, + .idx = likely_colormap_index, + .exclude = -1, + }; + vp_search_node(handle->root, px, &best_candidate); + if (diff) { + *diff = best_candidate.distance * best_candidate.distance; + } + return best_candidate.idx; +} + +LIQ_PRIVATE void nearest_free(struct nearest_map *centroids) +{ + mempool_destroy(centroids->mempool); +} diff --git a/tools/assets/n64texconv/lib/libimagequant/nearest.h b/tools/assets/n64texconv/lib/libimagequant/nearest.h new file mode 100644 index 0000000000..10a0a2c1b9 --- /dev/null +++ b/tools/assets/n64texconv/lib/libimagequant/nearest.h @@ -0,0 +1,14 @@ +// +// nearest.h +// pngquant +// + +#ifndef NEAREST_H +#define NEAREST_H + +struct nearest_map; +LIQ_PRIVATE struct nearest_map *nearest_init(const colormap *palette); +LIQ_PRIVATE unsigned int nearest_search(const struct nearest_map *map, const f_pixel *px, const int palette_index_guess, float *diff); +LIQ_PRIVATE void nearest_free(struct nearest_map *map); + +#endif diff --git a/tools/assets/n64texconv/lib/libimagequant/pam.c b/tools/assets/n64texconv/lib/libimagequant/pam.c new file mode 100644 index 0000000000..5d955e1eb1 --- /dev/null +++ b/tools/assets/n64texconv/lib/libimagequant/pam.c @@ -0,0 +1,351 @@ +/* pam.c - pam (portable alpha map) utility library +** +** © 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 +#include + +#include "libimagequant.h" +#include "pam.h" +#include "mempool.h" + +LIQ_PRIVATE bool pam_computeacolorhash(struct acolorhash_table *acht, const liq_color *const pixels[], unsigned int cols, unsigned int rows, const unsigned char *importance_map) +{ + const unsigned int ignorebits = acht->ignorebits; + const unsigned int channel_mask = 255U>>ignorebits<>ignorebits) ^ 0xFFU; + const unsigned int posterize_mask = channel_mask << 24 | channel_mask << 16 | channel_mask << 8 | channel_mask; + const unsigned int posterize_high_mask = channel_hmask << 24 | channel_hmask << 16 | channel_hmask << 8 | channel_hmask; + + const unsigned int hash_size = acht->hash_size; + + /* Go through the entire image, building a hash table of colors. */ + for(unsigned int row = 0; row < rows; ++row) { + + for(unsigned int col = 0; col < cols; ++col) { + unsigned int boost; + + // RGBA color is casted to long for easier hasing/comparisons + union rgba_as_int px = {pixels[row][col]}; + unsigned int hash; + if (px.rgba.a) { + // mask posterizes all 4 channels in one go + px.l = (px.l & posterize_mask) | ((px.l & posterize_high_mask) >> (8-ignorebits)); + // fancier hashing algorithms didn't improve much + hash = px.l % hash_size; + + if (importance_map) { + boost = *importance_map++; + } else { + boost = 255; + } + } else { + // "dirty alpha" has different RGBA values that end up being the same fully transparent color + px.l=0; hash=0; + + boost = 2000; + if (importance_map) { + importance_map++; + } + } + + if (!pam_add_to_hash(acht, hash, boost, px, row, rows)) { + return false; + } + } + } + acht->cols = cols; + acht->rows += rows; + return true; +} + +LIQ_PRIVATE bool pam_add_to_hash(struct acolorhash_table *acht, unsigned int hash, unsigned int boost, union rgba_as_int px, unsigned int row, unsigned int rows) +{ + /* head of the hash function stores first 2 colors inline (achl->used = 1..2), + to reduce number of allocations of achl->other_items. + */ + struct acolorhist_arr_head *achl = &acht->buckets[hash]; + if (achl->inline1.color.l == px.l && achl->used) { + achl->inline1.perceptual_weight += boost; + return true; + } + if (achl->used) { + if (achl->used > 1) { + if (achl->inline2.color.l == px.l) { + achl->inline2.perceptual_weight += boost; + return true; + } + // other items are stored as an array (which gets reallocated if needed) + struct acolorhist_arr_item *other_items = achl->other_items; + unsigned int i = 0; + for (; i < achl->used-2; i++) { + if (other_items[i].color.l == px.l) { + other_items[i].perceptual_weight += boost; + return true; + } + } + + // the array was allocated with spare items + if (i < achl->capacity) { + other_items[i] = (struct acolorhist_arr_item){ + .color = px, + .perceptual_weight = boost, + }; + achl->used++; + ++acht->colors; + return true; + } + + if (++acht->colors > acht->maxcolors) { + return false; + } + + struct acolorhist_arr_item *new_items; + unsigned int capacity; + if (!other_items) { // there was no array previously, alloc "small" array + capacity = 8; + if (acht->freestackp <= 0) { + // estimate how many colors are going to be + headroom + const size_t mempool_size = ((acht->rows + rows-row) * 2 * acht->colors / (acht->rows + row + 1) + 1024) * sizeof(struct acolorhist_arr_item); + new_items = mempool_alloc(&acht->mempool, sizeof(struct acolorhist_arr_item)*capacity, mempool_size); + } else { + // freestack stores previously freed (reallocated) arrays that can be reused + // (all pesimistically assumed to be capacity = 8) + new_items = acht->freestack[--acht->freestackp]; + } + } else { + const unsigned int stacksize = sizeof(acht->freestack)/sizeof(acht->freestack[0]); + + // simply reallocs and copies array to larger capacity + capacity = achl->capacity*2 + 16; + if (acht->freestackp < stacksize-1) { + acht->freestack[acht->freestackp++] = other_items; + } + const size_t mempool_size = ((acht->rows + rows-row) * 2 * acht->colors / (acht->rows + row + 1) + 32*capacity) * sizeof(struct acolorhist_arr_item); + new_items = mempool_alloc(&acht->mempool, sizeof(struct acolorhist_arr_item)*capacity, mempool_size); + if (!new_items) return false; + memcpy(new_items, other_items, sizeof(other_items[0])*achl->capacity); + } + + achl->other_items = new_items; + achl->capacity = capacity; + new_items[i] = (struct acolorhist_arr_item){ + .color = px, + .perceptual_weight = boost, + }; + achl->used++; + } else { + // these are elses for first checks whether first and second inline-stored colors are used + achl->inline2.color.l = px.l; + achl->inline2.perceptual_weight = boost; + achl->used = 2; + ++acht->colors; + } + } else { + achl->inline1.color.l = px.l; + achl->inline1.perceptual_weight = boost; + achl->used = 1; + ++acht->colors; + } + return true; +} + +LIQ_PRIVATE struct acolorhash_table *pam_allocacolorhash(unsigned int maxcolors, unsigned int surface, unsigned int ignorebits, void* (*malloc)(size_t), void (*free)(void*)) +{ + const size_t estimated_colors = MIN(maxcolors, surface/(ignorebits + (surface > 512*512 ? 6 : 5))); + const size_t hash_size = estimated_colors < 66000 ? 6673 : (estimated_colors < 200000 ? 12011 : 24019); + + mempoolptr m = NULL; + const size_t buckets_size = hash_size * sizeof(struct acolorhist_arr_head); + const size_t mempool_size = sizeof(struct acolorhash_table) + buckets_size + estimated_colors * sizeof(struct acolorhist_arr_item); + struct acolorhash_table *t = mempool_create(&m, sizeof(*t) + buckets_size, mempool_size, malloc, free); + if (!t) return NULL; + *t = (struct acolorhash_table){ + .mempool = m, + .hash_size = hash_size, + .maxcolors = maxcolors, + .ignorebits = ignorebits, + }; + memset(t->buckets, 0, buckets_size); + return t; +} + +ALWAYS_INLINE static float pam_add_to_hist(struct temp_hist_item achv[], unsigned int *j, const struct acolorhist_arr_item *entry, const float max_perceptual_weight, int counts[]) +{ + if (entry->perceptual_weight == 0 && *j > 0) { + return 0; + } + const liq_color px = entry->color.rgba; + achv[*j].color = px; + const short cluster = ((px.r>>7)<<3) | ((px.g>>7)<<2) | ((px.b>>7)<<1) | (px.a>>7); + counts[cluster]++; + achv[*j].cluster = cluster; + const float w = MIN(entry->perceptual_weight/170.f, max_perceptual_weight); + achv[*j].weight = w; + *j += 1; + return w; +} + +LIQ_PRIVATE histogram *pam_acolorhashtoacolorhist(const struct acolorhash_table *acht, const double gamma, void* (*malloc)(size_t), void (*free)(void*)) +{ + histogram *hist = malloc(sizeof(hist[0])); + if (!hist || !acht) return NULL; + *hist = (histogram){ + .achv = malloc(MAX(1,acht->colors) * sizeof(hist->achv[0])), + .size = acht->colors, + .free = free, + .ignorebits = acht->ignorebits, + }; + if (!hist->achv) return NULL; + + /// Clusters form initial boxes for quantization, to ensure extreme colors are better represented + int counts[LIQ_MAXCLUSTER] = {}; + struct temp_hist_item *temp = malloc(MAX(1, acht->colors) * sizeof(temp[0])); + + /* Limit perceptual weight to 1/10th of the image surface area to prevent + a single color from dominating all others. */ + float max_perceptual_weight = 0.1f * acht->cols * acht->rows; + double total_weight = 0; + + unsigned int j=0; + for(unsigned int i=0; i < acht->hash_size; ++i) { + const struct acolorhist_arr_head *const achl = &acht->buckets[i]; + if (achl->used) { + total_weight += pam_add_to_hist(temp, &j, &achl->inline1, max_perceptual_weight, counts); + + if (achl->used > 1) { + total_weight += pam_add_to_hist(temp, &j, &achl->inline2, max_perceptual_weight, counts); + + for(unsigned int k=0; k < achl->used-2; k++) { + total_weight += pam_add_to_hist(temp, &j, &achl->other_items[k], max_perceptual_weight, counts); + } + } + } + } + hist->total_perceptual_weight = total_weight; + + int begin = 0; + for(int i=0; i < LIQ_MAXCLUSTER; i++) { + hist->boxes[i].begin = begin; + hist->boxes[i].end = begin; + begin = begin + counts[i]; + } + + hist->size = j; + hist->total_perceptual_weight = total_weight; + for(unsigned int k=0; k < hist->size; k++) { + hist->achv[k].tmp.likely_colormap_index = 0; + } + if (!j) { + free(temp); + pam_freeacolorhist(hist); + return NULL; + } + + float gamma_lut[256]; + to_f_set_gamma(gamma_lut, gamma); + for(int i=0; i < hist->size; i++) { + int j = hist->boxes[temp[i].cluster].end++; + hist->achv[j].acolor = rgba_to_f(gamma_lut, temp[i].color); + hist->achv[j].perceptual_weight = temp[i].weight; + hist->achv[j].adjusted_weight = temp[i].weight; + } + free(temp); + + return hist; +} + + +LIQ_PRIVATE void pam_freeacolorhash(struct acolorhash_table *acht) +{ + if (acht) { + mempool_destroy(acht->mempool); + } +} + +LIQ_PRIVATE void pam_freeacolorhist(histogram *hist) +{ + hist->free(hist->achv); + hist->free(hist); +} + +LIQ_PRIVATE LIQ_NONNULL colormap *pam_colormap(unsigned int colors, void* (*malloc)(size_t), void (*free)(void*)) +{ + assert(colors > 0 && colors < 65536); + + colormap *map; + const size_t colors_size = colors * sizeof(map->palette[0]); + map = malloc(sizeof(colormap) + colors_size); + if (!map) return NULL; + *map = (colormap){ + .malloc = malloc, + .free = free, + .colors = colors, + }; + memset(map->palette, 0, colors_size); + return map; +} + +LIQ_PRIVATE colormap *pam_duplicate_colormap(colormap *map) +{ + colormap *dupe = pam_colormap(map->colors, map->malloc, map->free); + for(unsigned int i=0; i < map->colors; i++) { + dupe->palette[i] = map->palette[i]; + } + return dupe; +} + +LIQ_PRIVATE void pam_freecolormap(colormap *c) +{ + c->free(c); +} + +LIQ_PRIVATE void to_f_set_gamma(float gamma_lut[], const double gamma) +{ + for(int i=0; i < 256; i++) { + gamma_lut[i] = pow((double)i/255.0, internal_gamma/gamma); + } +} + + +/* fixed colors are always included in the palette, so it would be wasteful to duplicate them in palette from histogram */ +LIQ_PRIVATE LIQ_NONNULL void remove_fixed_colors_from_histogram(histogram *hist, const int fixed_colors_count, const f_pixel fixed_colors[], const float target_mse) +{ + const float max_difference = MAX(target_mse/2.f, 2.f/256.f/256.f); + if (fixed_colors_count) { + for(int j=0; j < hist->size; j++) { + for(unsigned int i=0; i < fixed_colors_count; i++) { + if (colordifference(hist->achv[j].acolor, fixed_colors[i]) < max_difference) { + hist->achv[j] = hist->achv[--hist->size]; // remove color from histogram by overwriting with the last entry + j--; break; // continue searching histogram + } + } + } + } +} + +LIQ_PRIVATE LIQ_NONNULL colormap *histogram_to_palette(const histogram *hist, void* (*malloc)(size_t), void (*free)(void*)) { + if (!hist->size) { + return NULL; + } + colormap *acolormap = pam_colormap(hist->size, malloc, free); + for(unsigned int i=0; i < hist->size; i++) { + acolormap->palette[i].acolor = hist->achv[i].acolor; + acolormap->palette[i].popularity = hist->achv[i].perceptual_weight; + } + return acolormap; +} + +LIQ_PRIVATE LIQ_NONNULL void hist_reset_colors(const histogram *hist, const unsigned int colors) { + // likely_colormap_index (used and set in kmeans_do_iteration) can't point to index outside colormap + if (colors < 256) for(unsigned int j=0; j < hist->size; j++) { + if (hist->achv[j].tmp.likely_colormap_index >= colors) { + hist->achv[j].tmp.likely_colormap_index = 0; // actual value doesn't matter, as the guess is out of date anyway + } + } +} diff --git a/tools/assets/n64texconv/lib/libimagequant/pam.h b/tools/assets/n64texconv/lib/libimagequant/pam.h new file mode 100644 index 0000000000..4ab4e0de21 --- /dev/null +++ b/tools/assets/n64texconv/lib/libimagequant/pam.h @@ -0,0 +1,312 @@ +/* pam.h - pam (portable alpha map) utility library + ** + ** Colormap routines. + ** + ** Copyright (C) 1989, 1991 by Jef Poskanzer. + ** Copyright (C) 1997 by Greg Roelofs. + ** + ** Permission to use, copy, modify, and distribute this software and its + ** documentation for any purpose and without fee is hereby granted, provided + ** that the above copyright notice appear in all copies and that both that + ** copyright notice and this permission notice appear in supporting + ** documentation. This software is provided "as is" without express or + ** implied warranty. + */ + +#ifndef PAM_H +#define PAM_H + +// accidental debug assertions make color search much slower, +// so force assertions off if there's no explicit setting +#if !defined(NDEBUG) && !defined(DEBUG) +#define NDEBUG +#endif + +#include +#include +#include +#include + +#ifndef MAX +# define MAX(a,b) ((a) > (b)? (a) : (b)) +# define MIN(a,b) ((a) < (b)? (a) : (b)) +#endif + +#define MAX_DIFF 1e20 + +#ifndef USE_SSE +# if defined(__SSE__) && (defined(__amd64__) || defined(__X86_64__) || defined(_WIN64) || defined(WIN32) || defined(__WIN32__)) +# define USE_SSE 1 +# else +# define USE_SSE 0 +# endif +#endif + +#if USE_SSE +# include +# ifdef _MSC_VER +# include +# define SSE_ALIGN +# else +# define SSE_ALIGN __attribute__ ((aligned (16))) +# if defined(__i386__) && defined(__PIC__) +# define cpuid(func,ax,bx,cx,dx)\ + __asm__ __volatile__ ( \ + "push %%ebx\n" \ + "cpuid\n" \ + "mov %%ebx, %1\n" \ + "pop %%ebx\n" \ + : "=a" (ax), "=r" (bx), "=c" (cx), "=d" (dx) \ + : "a" (func)); +# else +# define cpuid(func,ax,bx,cx,dx)\ + __asm__ __volatile__ ("cpuid":\ + "=a" (ax), "=b" (bx), "=c" (cx), "=d" (dx) : "a" (func)); +# endif +#endif +#else +# define SSE_ALIGN +#endif + +#ifndef _MSC_VER +#define LIQ_ARRAY(type, var, count) type var[count] +#else +#define LIQ_ARRAY(type, var, count) type* var = (type*)_alloca(sizeof(type)*(count)) +#endif + +#if defined(__GNUC__) || defined (__llvm__) +#define ALWAYS_INLINE __attribute__((always_inline)) inline +#define NEVER_INLINE __attribute__ ((noinline)) +#elif defined(_MSC_VER) +#define inline __inline +#define restrict __restrict +#define ALWAYS_INLINE __forceinline +#define NEVER_INLINE __declspec(noinline) +#else +#define ALWAYS_INLINE inline +#define NEVER_INLINE +#endif + +/* from pam.h */ + +typedef struct { + float a, r, g, b; +} SSE_ALIGN f_pixel; + +static const float internal_gamma = 0.57f; + +LIQ_PRIVATE void to_f_set_gamma(float gamma_lut[], const double gamma); + +#define MIN_OPAQUE_A (1.f / 256.f * LIQ_WEIGHT_A) + +#define LIQ_WEIGHT_A 0.625f +#define LIQ_WEIGHT_R 0.5f +#define LIQ_WEIGHT_G 1.0f +#define LIQ_WEIGHT_B 0.45f +#define LIQ_WEIGHT_MSE 0.45 // fudge factor for compensating that colors aren't 0..1 range + +/** + Converts 8-bit color to internal gamma and premultiplied alpha. + (premultiplied color space is much better for blending of semitransparent colors) + */ +ALWAYS_INLINE static f_pixel rgba_to_f(const float gamma_lut[], const liq_color px); +inline static f_pixel rgba_to_f(const float gamma_lut[], const liq_color px) +{ + float a = px.a/255.f; + + return (f_pixel) { + .a = a * LIQ_WEIGHT_A, + .r = gamma_lut[px.r] * LIQ_WEIGHT_R * a, + .g = gamma_lut[px.g] * LIQ_WEIGHT_G * a, + .b = gamma_lut[px.b] * LIQ_WEIGHT_B * a, + }; +} + +inline static liq_color f_to_rgb(const float gamma, const f_pixel px) +{ + if (px.a < MIN_OPAQUE_A) { + return (liq_color){0,0,0,0}; + } + + float r = (LIQ_WEIGHT_A / LIQ_WEIGHT_R) * px.r / px.a, + g = (LIQ_WEIGHT_A / LIQ_WEIGHT_G) * px.g / px.a, + b = (LIQ_WEIGHT_A / LIQ_WEIGHT_B) * px.b / px.a; + + r = powf(r, gamma/internal_gamma); + g = powf(g, gamma/internal_gamma); + b = powf(b, gamma/internal_gamma); + + // 256, because numbers are in range 1..255.9999… rounded down + r *= 256.f; + g *= 256.f; + b *= 256.f; + float a = (256.f / LIQ_WEIGHT_A) * px.a; + + return (liq_color){ + .r = r>=255.f ? 255 : r, + .g = g>=255.f ? 255 : g, + .b = b>=255.f ? 255 : b, + .a = a>=255.f ? 255 : a, + }; +} + +ALWAYS_INLINE static float colordifference_ch(const float x, const float y, const float alphas); +inline static float colordifference_ch(const float x, const float y, const float alphas) +{ + // maximum of channel blended on white, and blended on black + // premultiplied alpha and backgrounds 0/1 shorten the formula + const float black = x-y, white = black+alphas; + return MAX(black*black, white*white); +} + +ALWAYS_INLINE static float colordifference_stdc(const f_pixel px, const f_pixel py); +inline static float colordifference_stdc(const f_pixel px, const f_pixel py) +{ + // px_b.rgb = px.rgb + 0*(1-px.a) // blend px on black + // px_b.a = px.a + 1*(1-px.a) + // px_w.rgb = px.rgb + 1*(1-px.a) // blend px on white + // px_w.a = px.a + 1*(1-px.a) + + // px_b.rgb = px.rgb // difference same as in opaque RGB + // px_b.a = 1 + // px_w.rgb = px.rgb - px.a // difference simplifies to formula below + // px_w.a = 1 + + // (px.rgb - px.a) - (py.rgb - py.a) + // (px.rgb - py.rgb) + (py.a - px.a) + + const float alphas = py.a-px.a; + return colordifference_ch(px.r, py.r, alphas) + + colordifference_ch(px.g, py.g, alphas) + + colordifference_ch(px.b, py.b, alphas); +} + +ALWAYS_INLINE static float colordifference(f_pixel px, f_pixel py); +inline static float colordifference(f_pixel px, f_pixel py) +{ +#if USE_SSE +#ifdef _MSC_VER + /* In MSVC we cannot use the align attribute in parameters. + * This is used a lot, so we just use an unaligned load. + * Also the compiler incorrectly inlines vpx and vpy without + * the volatile when optimization is applied for x86_64. */ + const volatile __m128 vpx = _mm_loadu_ps((const float*)&px); + const volatile __m128 vpy = _mm_loadu_ps((const float*)&py); +#else + const __m128 vpx = _mm_load_ps((const float*)&px); + const __m128 vpy = _mm_load_ps((const float*)&py); +#endif + + // y.a - x.a + __m128 alphas = _mm_sub_ss(vpy, vpx); + alphas = _mm_shuffle_ps(alphas,alphas,0); // copy first to all four + + __m128 onblack = _mm_sub_ps(vpx, vpy); // x - y + __m128 onwhite = _mm_add_ps(onblack, alphas); // x - y + (y.a - x.a) + + onblack = _mm_mul_ps(onblack, onblack); + onwhite = _mm_mul_ps(onwhite, onwhite); + const __m128 max = _mm_max_ps(onwhite, onblack); + + // add rgb, not a + const __m128 maxhl = _mm_movehl_ps(max, max); + const __m128 tmp = _mm_add_ps(max, maxhl); + const __m128 sum = _mm_add_ss(maxhl, _mm_shuffle_ps(tmp, tmp, 1)); + + const float res = _mm_cvtss_f32(sum); + assert(fabs(res - colordifference_stdc(px,py)) < 0.001); + return res; +#else + return colordifference_stdc(px,py); +#endif +} + +/* from pamcmap.h */ +union rgba_as_int { + liq_color rgba; + unsigned int l; +}; + +typedef struct { + f_pixel acolor; + float adjusted_weight, // perceptual weight changed to tweak how mediancut selects colors + perceptual_weight; // number of pixels weighted by importance of different areas of the picture + + float color_weight; // these two change every time histogram subset is sorted + union { + unsigned int sort_value; + unsigned char likely_colormap_index; + } tmp; +} hist_item; + +#define LIQ_MAXCLUSTER 16 + +struct temp_hist_item { + liq_color color; + float weight; + short cluster; +}; + +struct histogram_box { + int begin, end; +}; + +typedef struct { + hist_item *achv; + void (*free)(void*); + double total_perceptual_weight; + unsigned int size; + unsigned int ignorebits; + struct histogram_box boxes[LIQ_MAXCLUSTER]; +} histogram; + +typedef struct { + f_pixel acolor; + float popularity; + bool fixed; // if true it's user-supplied and must not be changed (e.g in K-Means iteration) +} colormap_item; + +typedef struct colormap { + unsigned int colors; + void* (*malloc)(size_t); + void (*free)(void*); + colormap_item palette[]; +} colormap; + +struct acolorhist_arr_item { + union rgba_as_int color; + unsigned int perceptual_weight; +}; + +struct acolorhist_arr_head { + struct acolorhist_arr_item inline1, inline2; + unsigned int used, capacity; + struct acolorhist_arr_item *other_items; +}; + +struct acolorhash_table { + struct mempool *mempool; + unsigned int ignorebits, maxcolors, colors, cols, rows; + unsigned int hash_size; + unsigned int freestackp; + struct acolorhist_arr_item *freestack[512]; + struct acolorhist_arr_head buckets[]; +}; + +LIQ_PRIVATE void pam_freeacolorhash(struct acolorhash_table *acht); +LIQ_PRIVATE struct acolorhash_table *pam_allocacolorhash(unsigned int maxcolors, unsigned int surface, unsigned int ignorebits, void* (*malloc)(size_t), void (*free)(void*)); +LIQ_PRIVATE histogram *pam_acolorhashtoacolorhist(const struct acolorhash_table *acht, const double gamma, void* (*malloc)(size_t), void (*free)(void*)); +LIQ_PRIVATE bool pam_computeacolorhash(struct acolorhash_table *acht, const liq_color *const pixels[], unsigned int cols, unsigned int rows, const unsigned char *importance_map); +LIQ_PRIVATE bool pam_add_to_hash(struct acolorhash_table *acht, unsigned int hash, unsigned int boost, union rgba_as_int px, unsigned int row, unsigned int rows); + +LIQ_PRIVATE void pam_freeacolorhist(histogram *h); + +LIQ_PRIVATE colormap *pam_colormap(unsigned int colors, void* (*malloc)(size_t), void (*free)(void*)) LIQ_NONNULL; +LIQ_PRIVATE colormap *pam_duplicate_colormap(colormap *map) LIQ_NONNULL; +LIQ_PRIVATE void pam_freecolormap(colormap *c); + +LIQ_PRIVATE void remove_fixed_colors_from_histogram(histogram *hist, const int fixed_colors_count, const f_pixel fixed_colors[], const float target_mse) LIQ_NONNULL; +LIQ_PRIVATE colormap *histogram_to_palette(const histogram *hist, void* (*malloc)(size_t), void (*free)(void*)) LIQ_NONNULL; +LIQ_PRIVATE void hist_reset_colors(const histogram *hist, const unsigned int colors) LIQ_NONNULL; + +#endif diff --git a/tools/assets/n64texconv/lib/libimagequant/remap.c b/tools/assets/n64texconv/lib/libimagequant/remap.c new file mode 100644 index 0000000000..4495810132 --- /dev/null +++ b/tools/assets/n64texconv/lib/libimagequant/remap.c @@ -0,0 +1,300 @@ +#include +#include + +#include "libimagequant.h" +#include "pam.h" +#include "libimagequant_private.h" + +#include "nearest.h" +#include "kmeans.h" + +LIQ_PRIVATE LIQ_NONNULL float remap_to_palette(liq_image *const input_image, unsigned char *const *const output_pixels, colormap *const map) +{ + const int rows = input_image->height; + const unsigned int cols = input_image->width; + double remapping_error=0; + + if (!liq_image_get_row_f_init(input_image)) { + return -1; + } + if (input_image->background && !liq_image_get_row_f_init(input_image->background)) { + return -1; + } + + const colormap_item *acolormap = map->palette; + + struct nearest_map *const n = nearest_init(map); + liq_image *background = input_image->background; + const int transparent_index = background ? nearest_search(n, &(f_pixel){0,0,0,0}, 0, NULL) : -1; + if (background && acolormap[transparent_index].acolor.a > 1.f/256.f) { + // palette unsuitable for using the bg + background = NULL; + } + + + const unsigned int max_threads = omp_get_max_threads(); + LIQ_ARRAY(kmeans_state, average_color, (KMEANS_CACHE_LINE_GAP+map->colors) * max_threads); + kmeans_init(map, max_threads, average_color); + +#if __GNUC__ >= 9 || __clang__ + #pragma omp parallel for if (rows*cols > 3000) \ + schedule(static) default(none) shared(background,acolormap,average_color,cols,input_image,map,n,output_pixels,rows,transparent_index) reduction(+:remapping_error) +#endif + for(int row = 0; row < rows; ++row) { + const f_pixel *const row_pixels = liq_image_get_row_f(input_image, row); + const f_pixel *const bg_pixels = background && acolormap[transparent_index].acolor.a < MIN_OPAQUE_A ? liq_image_get_row_f(background, row) : NULL; + + unsigned int last_match=0; + for(unsigned int col = 0; col < cols; ++col) { + float diff; + last_match = nearest_search(n, &row_pixels[col], last_match, &diff); + if (bg_pixels) { + float bg_diff = colordifference(bg_pixels[col], acolormap[last_match].acolor); + if (bg_diff <= diff) { + diff = bg_diff; + last_match = transparent_index; + } + } + output_pixels[row][col] = last_match; + + remapping_error += diff; + if (last_match != transparent_index) { + kmeans_update_color(row_pixels[col], 1.0, map, last_match, omp_get_thread_num(), average_color); + } + } + } + + kmeans_finalize(map, max_threads, average_color); + + nearest_free(n); + + return remapping_error / (input_image->width * input_image->height); +} + +inline static f_pixel get_dithered_pixel(const float dither_level, const float max_dither_error, const f_pixel thiserr, const f_pixel px) +{ + /* Use Floyd-Steinberg errors to adjust actual color. */ + const float sr = thiserr.r * dither_level, + sg = thiserr.g * dither_level, + sb = thiserr.b * dither_level, + sa = thiserr.a * dither_level; + + float ratio = 1.0; + const float max_overflow = 1.1f; + const float max_underflow = -0.1f; + + // allowing some overflow prevents undithered bands caused by clamping of all channels + if (px.r + sr > max_overflow) ratio = MIN(ratio, (max_overflow -px.r)/sr); + else { if (px.r + sr < max_underflow) ratio = MIN(ratio, (max_underflow-px.r)/sr); } + if (px.g + sg > max_overflow) ratio = MIN(ratio, (max_overflow -px.g)/sg); + else { if (px.g + sg < max_underflow) ratio = MIN(ratio, (max_underflow-px.g)/sg); } + if (px.b + sb > max_overflow) ratio = MIN(ratio, (max_overflow -px.b)/sb); + else { if (px.b + sb < max_underflow) ratio = MIN(ratio, (max_underflow-px.b)/sb); } + + float a = px.a + sa; + if (a > 1.f) { a = 1.f; } + else if (a < 0) { a = 0; } + + // If dithering error is crazy high, don't propagate it that much + // This prevents crazy geen pixels popping out of the blue (or red or black! ;) + const float dither_error = sr*sr + sg*sg + sb*sb + sa*sa; + if (dither_error > max_dither_error) { + ratio *= 0.8f; + } else if (dither_error < 2.f/256.f/256.f) { + // don't dither areas that don't have noticeable error — makes file smaller + return px; + } + + return (f_pixel) { + .r=px.r + sr * ratio, + .g=px.g + sg * ratio, + .b=px.b + sb * ratio, + .a=a, + }; +} + +/** + Uses edge/noise map to apply dithering only to flat areas. Dithering on edges creates jagged lines, and noisy areas are "naturally" dithered. + + If output_image_is_remapped is true, only pixels noticeably changed by error diffusion will be written to output image. + */ +LIQ_PRIVATE LIQ_NONNULL bool remap_to_palette_floyd(liq_image *input_image, unsigned char *const output_pixels[], liq_remapping_result *quant, const float max_dither_error, const bool output_image_is_remapped) +{ + const int rows = input_image->height, cols = input_image->width; + const unsigned char *dither_map = quant->use_dither_map ? (input_image->dither_map ? input_image->dither_map : input_image->edges) : NULL; + + const colormap *map = quant->palette; + const colormap_item *acolormap = map->palette; + + if (!liq_image_get_row_f_init(input_image)) { + return false; + } + if (input_image->background && !liq_image_get_row_f_init(input_image->background)) { + return false; + } + + /* Initialize Floyd-Steinberg error vectors. */ + const size_t errwidth = cols+2; + f_pixel *restrict thiserr = input_image->malloc(errwidth * sizeof(thiserr[0]) * 2); // +2 saves from checking out of bounds access + if (!thiserr) return false; + f_pixel *restrict nexterr = thiserr + errwidth; + memset(thiserr, 0, errwidth * sizeof(thiserr[0])); + + bool ok = true; + struct nearest_map *const n = nearest_init(map); + liq_image *background = input_image->background; + const int transparent_index = background ? nearest_search(n, &(f_pixel){0,0,0,0}, 0, NULL) : -1; + if (background && acolormap[transparent_index].acolor.a > 1.f/256.f) { + // palette unsuitable for using the bg + background = NULL; + } + + // response to this value is non-linear and without it any value < 0.8 would give almost no dithering + float base_dithering_level = quant->dither_level; + base_dithering_level = 1.f - (1.f-base_dithering_level)*(1.f-base_dithering_level); + + if (dither_map) { + base_dithering_level *= 1.f/255.f; // convert byte to float + } + base_dithering_level *= 15.f/16.f; // prevent small errors from accumulating + + int fs_direction = 1; + unsigned int last_match=0; + for (int row = 0; row < rows; ++row) { + if (liq_remap_progress(quant, quant->progress_stage1 + row * (100.f - quant->progress_stage1) / rows)) { + ok = false; + break; + } + + memset(nexterr, 0, errwidth * sizeof(nexterr[0])); + + int col = (fs_direction > 0) ? 0 : (cols - 1); + const f_pixel *const row_pixels = liq_image_get_row_f(input_image, row); + const f_pixel *const bg_pixels = background && acolormap[transparent_index].acolor.a < MIN_OPAQUE_A ? liq_image_get_row_f(background, row) : NULL; + int undithered_bg_used = 0; + + do { + float dither_level = base_dithering_level; + if (dither_map) { + dither_level *= dither_map[row*cols + col]; + } + + const f_pixel spx = get_dithered_pixel(dither_level, max_dither_error, thiserr[col + 1], row_pixels[col]); + + const unsigned int guessed_match = output_image_is_remapped ? output_pixels[row][col] : last_match; + float dither_diff; + last_match = nearest_search(n, &spx, guessed_match, &dither_diff); + f_pixel output_px = acolormap[last_match].acolor; + // this is for animgifs + if (bg_pixels) { + // if the background makes better match *with* dithering, it's a definitive win + float bg_for_dither_diff = colordifference(spx, bg_pixels[col]); + if (bg_for_dither_diff <= dither_diff) { + output_px = bg_pixels[col]; + last_match = transparent_index; + } else if (undithered_bg_used > 1) { + // the undithered fallback can cause artifacts when too many undithered pixels accumulate a big dithering error + // so periodically ignore undithered fallback to prevent that + undithered_bg_used = 0; + } else { + // if dithering is not applied, there's a high risk of creating artifacts (flat areas, error accumulating badly), + // OTOH poor dithering disturbs static backgrounds and creates oscilalting frames that break backgrounds + // back and forth in two differently bad ways + float max_diff = colordifference(row_pixels[col], bg_pixels[col]); + float dithered_diff = colordifference(row_pixels[col], output_px); + // if dithering is worse than natural difference between frames + // (this rule dithers moving areas, but does not dither static areas) + if (dithered_diff > max_diff) { + // then see if an undithered color is closer to the ideal + float undithered_diff = colordifference(row_pixels[col], acolormap[guessed_match].acolor); + if (undithered_diff < max_diff) { + undithered_bg_used++; + output_px = acolormap[guessed_match].acolor; + last_match = guessed_match; + } + } + } + } + + output_pixels[row][col] = last_match; + + f_pixel err = { + .r = (spx.r - output_px.r), + .g = (spx.g - output_px.g), + .b = (spx.b - output_px.b), + .a = (spx.a - output_px.a), + }; + + // If dithering error is crazy high, don't propagate it that much + // This prevents crazy geen pixels popping out of the blue (or red or black! ;) + if (err.r*err.r + err.g*err.g + err.b*err.b + err.a*err.a > max_dither_error) { + err.r *= 0.75f; + err.g *= 0.75f; + err.b *= 0.75f; + err.a *= 0.75f; + } + + /* Propagate Floyd-Steinberg error terms. */ + if (fs_direction > 0) { + thiserr[col + 2].a += err.a * (7.f/16.f); + thiserr[col + 2].r += err.r * (7.f/16.f); + thiserr[col + 2].g += err.g * (7.f/16.f); + thiserr[col + 2].b += err.b * (7.f/16.f); + + nexterr[col + 2].a = err.a * (1.f/16.f); + nexterr[col + 2].r = err.r * (1.f/16.f); + nexterr[col + 2].g = err.g * (1.f/16.f); + nexterr[col + 2].b = err.b * (1.f/16.f); + + nexterr[col + 1].a += err.a * (5.f/16.f); + nexterr[col + 1].r += err.r * (5.f/16.f); + nexterr[col + 1].g += err.g * (5.f/16.f); + nexterr[col + 1].b += err.b * (5.f/16.f); + + nexterr[col ].a += err.a * (3.f/16.f); + nexterr[col ].r += err.r * (3.f/16.f); + nexterr[col ].g += err.g * (3.f/16.f); + nexterr[col ].b += err.b * (3.f/16.f); + + } else { + thiserr[col ].a += err.a * (7.f/16.f); + thiserr[col ].r += err.r * (7.f/16.f); + thiserr[col ].g += err.g * (7.f/16.f); + thiserr[col ].b += err.b * (7.f/16.f); + + nexterr[col ].a = err.a * (1.f/16.f); + nexterr[col ].r = err.r * (1.f/16.f); + nexterr[col ].g = err.g * (1.f/16.f); + nexterr[col ].b = err.b * (1.f/16.f); + + nexterr[col + 1].a += err.a * (5.f/16.f); + nexterr[col + 1].r += err.r * (5.f/16.f); + nexterr[col + 1].g += err.g * (5.f/16.f); + nexterr[col + 1].b += err.b * (5.f/16.f); + + nexterr[col + 2].a += err.a * (3.f/16.f); + nexterr[col + 2].r += err.r * (3.f/16.f); + nexterr[col + 2].g += err.g * (3.f/16.f); + nexterr[col + 2].b += err.b * (3.f/16.f); + } + + // remapping is done in zig-zag + col += fs_direction; + if (fs_direction > 0) { + if (col >= cols) break; + } else { + if (col < 0) break; + } + } while(1); + + f_pixel *const temperr = thiserr; + thiserr = nexterr; + nexterr = temperr; + fs_direction = -fs_direction; + } + + input_image->free(MIN(thiserr, nexterr)); // MIN because pointers were swapped + nearest_free(n); + + return ok; +} diff --git a/tools/assets/n64texconv/lib/libimagequant/remap.h b/tools/assets/n64texconv/lib/libimagequant/remap.h new file mode 100644 index 0000000000..59d509b7a2 --- /dev/null +++ b/tools/assets/n64texconv/lib/libimagequant/remap.h @@ -0,0 +1,7 @@ +#ifndef REMAP_H +#define REMAP_H + +LIQ_PRIVATE float remap_to_palette(liq_image *const input_image, unsigned char *const *const output_pixels, colormap *const map) LIQ_NONNULL; +LIQ_PRIVATE bool remap_to_palette_floyd(liq_image *input_image, unsigned char *const output_pixels[], liq_remapping_result *quant, const float max_dither_error, const bool output_image_is_remapped) LIQ_NONNULL; + +#endif diff --git a/tools/assets/n64texconv/lib/spng/LICENSE b/tools/assets/n64texconv/lib/spng/LICENSE new file mode 100644 index 0000000000..05234f0ea3 --- /dev/null +++ b/tools/assets/n64texconv/lib/spng/LICENSE @@ -0,0 +1,25 @@ +BSD 2-Clause License + +Copyright (c) 2018-2023, Randy +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/tools/assets/n64texconv/lib/spng/spng.c b/tools/assets/n64texconv/lib/spng/spng.c new file mode 100644 index 0000000000..b22b7110a1 --- /dev/null +++ b/tools/assets/n64texconv/lib/spng/spng.c @@ -0,0 +1,6980 @@ +/* SPDX-License-Identifier: (BSD-2-Clause AND libpng-2.0) */ + +#define SPNG__BUILD + +#include "spng.h" + +#include +#include +#include +#include + +#define ZLIB_CONST + +#ifdef __FRAMAC__ + #define SPNG_DISABLE_OPT + #include "tests/framac_stubs.h" +#else + #ifdef SPNG_USE_MINIZ + #include + #else + #include + #endif +#endif + +#ifdef SPNG_MULTITHREADING + #include +#endif + +/* Not build options, edit at your own risk! */ +#define SPNG_READ_SIZE (8192) +#define SPNG_WRITE_SIZE SPNG_READ_SIZE +#define SPNG_MAX_CHUNK_COUNT (1000) + +#define SPNG_TARGET_CLONES(x) + +#ifndef SPNG_DISABLE_OPT + + #if defined(__i386__) || defined(__x86_64__) || defined(_M_IX86) || defined(_M_X64) + #define SPNG_X86 + + #if defined(__x86_64__) || defined(_M_X64) + #define SPNG_X86_64 + #endif + + #elif defined(__aarch64__) || defined(_M_ARM64) /* || defined(__ARM_NEON) */ + #define SPNG_ARM /* NOTE: only arm64 builds are tested! */ + #else + #pragma message "disabling SIMD optimizations for unknown target" + #define SPNG_DISABLE_OPT + #endif + + #if defined(SPNG_X86_64) && defined(SPNG_ENABLE_TARGET_CLONES) + #undef SPNG_TARGET_CLONES + #define SPNG_TARGET_CLONES(x) __attribute__((target_clones(x))) + #else + #define SPNG_TARGET_CLONES(x) + #endif + + #ifndef SPNG_DISABLE_OPT + static void defilter_sub3(size_t rowbytes, unsigned char *row); + static void defilter_sub4(size_t rowbytes, unsigned char *row); + static void defilter_avg3(size_t rowbytes, unsigned char *row, const unsigned char *prev); + static void defilter_avg4(size_t rowbytes, unsigned char *row, const unsigned char *prev); + static void defilter_paeth3(size_t rowbytes, unsigned char *row, const unsigned char *prev); + static void defilter_paeth4(size_t rowbytes, unsigned char *row, const unsigned char *prev); + + #if defined(SPNG_ARM) + static uint32_t expand_palette_rgba8_neon(unsigned char *row, const unsigned char *scanline, const unsigned char *plte, uint32_t width); + static uint32_t expand_palette_rgb8_neon(unsigned char *row, const unsigned char *scanline, const unsigned char *plte, uint32_t width); + #endif + #endif +#endif + +#if defined(_MSC_VER) + #pragma warning(push) + #pragma warning(disable: 4244) +#endif + +#if (defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__) || defined(__BIG_ENDIAN__) + #define SPNG_BIG_ENDIAN +#else + #define SPNG_LITTLE_ENDIAN +#endif + +enum spng_state +{ + SPNG_STATE_INVALID = 0, + SPNG_STATE_INIT = 1, /* No PNG buffer/stream is set */ + SPNG_STATE_INPUT, /* Decoder input PNG was set */ + SPNG_STATE_OUTPUT = SPNG_STATE_INPUT, /* Encoder output was set */ + SPNG_STATE_IHDR, /* IHDR was read/written */ + SPNG_STATE_FIRST_IDAT, /* Encoded up to / reached first IDAT */ + SPNG_STATE_DECODE_INIT, /* Decoder is ready for progressive reads */ + SPNG_STATE_ENCODE_INIT = SPNG_STATE_DECODE_INIT, + SPNG_STATE_EOI, /* Reached the last scanline/row */ + SPNG_STATE_LAST_IDAT, /* Reached last IDAT, set at end of decode_image() */ + SPNG_STATE_AFTER_IDAT, /* */ + SPNG_STATE_IEND, /* Reached IEND */ +}; + +enum spng__internal +{ + SPNG__IO_SIGNAL = 1 << 9, + SPNG__CTX_FLAGS_ALL = (SPNG_CTX_IGNORE_ADLER32 | SPNG_CTX_ENCODER) +}; + +#define SPNG_STR(x) _SPNG_STR(x) +#define _SPNG_STR(x) #x + +#define SPNG_VERSION_STRING SPNG_STR(SPNG_VERSION_MAJOR) "." \ + SPNG_STR(SPNG_VERSION_MINOR) "." \ + SPNG_STR(SPNG_VERSION_PATCH) + +#define SPNG_GET_CHUNK_BOILERPLATE(chunk) \ + if(ctx == NULL) return 1; \ + int ret = read_chunks(ctx, 0); \ + if(ret) return ret; \ + if(!ctx->stored.chunk) return SPNG_ECHUNKAVAIL; \ + if(chunk == NULL) return 1 + +#define SPNG_SET_CHUNK_BOILERPLATE(chunk) \ + if(ctx == NULL || chunk == NULL) return 1; \ + if(ctx->data == NULL && !ctx->encode_only) return SPNG_ENOSRC; \ + int ret = read_chunks(ctx, 0); \ + if(ret) return ret + +/* Determine if the spng_option can be overriden/optimized */ +#define spng__optimize(option) (ctx->optimize_option & (1 << option)) + +struct spng_subimage +{ + uint32_t width; + uint32_t height; + size_t out_width; /* byte width based on output format */ + size_t scanline_width; +}; + +struct spng_text2 +{ + int type; + char *keyword; + char *text; + + size_t text_length; + + uint8_t compression_flag; /* iTXt only */ + char *language_tag; /* iTXt only */ + char *translated_keyword; /* iTXt only */ + + size_t cache_usage; + char user_keyword_storage[80]; +}; + +struct decode_flags +{ + unsigned apply_trns: 1; + unsigned apply_gamma: 1; + unsigned use_sbit: 1; + unsigned indexed: 1; + unsigned do_scaling: 1; + unsigned interlaced: 1; + unsigned same_layout: 1; + unsigned zerocopy: 1; + unsigned unpack: 1; +}; + +struct encode_flags +{ + unsigned interlace: 1; + unsigned same_layout: 1; + unsigned to_bigendian: 1; + unsigned progressive: 1; + unsigned finalize: 1; + + enum spng_filter_choice filter_choice; +}; + +struct spng_chunk_bitfield +{ + unsigned ihdr: 1; + unsigned plte: 1; + unsigned chrm: 1; + unsigned iccp: 1; + unsigned gama: 1; + unsigned sbit: 1; + unsigned srgb: 1; + unsigned text: 1; + unsigned bkgd: 1; + unsigned hist: 1; + unsigned trns: 1; + unsigned phys: 1; + unsigned splt: 1; + unsigned time: 1; + unsigned offs: 1; + unsigned exif: 1; + unsigned unknown: 1; +}; + +/* Packed sample iterator */ +struct spng__iter +{ + const uint8_t mask; + unsigned shift_amount; + const unsigned initial_shift, bit_depth; + const unsigned char *samples; +}; + +union spng__decode_plte +{ + struct spng_plte_entry rgba[256]; + unsigned char rgb[256 * 3]; + unsigned char raw[256 * 4]; + uint32_t align_this; +}; + +struct spng__zlib_options +{ + int compression_level; + int window_bits; + int mem_level; + int strategy; + int data_type; +}; + +typedef void spng__undo(spng_ctx *ctx); + +struct spng_ctx +{ + size_t data_size; + size_t bytes_read; + size_t stream_buf_size; + unsigned char *stream_buf; + const unsigned char *data; + + /* User-defined pointers for streaming */ + spng_read_fn *read_fn; + spng_write_fn *write_fn; + void *stream_user_ptr; + + /* Used for buffer reads */ + const unsigned char *png_base; + size_t bytes_left; + size_t last_read_size; + + /* Used for encoding */ + int user_owns_out_png; + unsigned char *out_png; + unsigned char *write_ptr; + size_t out_png_size; + size_t bytes_encoded; + + /* These are updated by read/write_header()/read_chunk_bytes() */ + struct spng_chunk current_chunk; + uint32_t cur_chunk_bytes_left; + uint32_t cur_actual_crc; + + struct spng_alloc alloc; + + enum spng_ctx_flags flags; + enum spng_format fmt; + + enum spng_state state; + + unsigned streaming: 1; + unsigned internal_buffer: 1; /* encoding to internal buffer */ + + unsigned inflate: 1; + unsigned deflate: 1; + unsigned encode_only: 1; + unsigned strict: 1; + unsigned discard: 1; + unsigned skip_crc: 1; + unsigned keep_unknown: 1; + unsigned prev_was_idat: 1; + + struct spng__zlib_options image_options; + struct spng__zlib_options text_options; + + spng__undo *undo; + + /* input file contains this chunk */ + struct spng_chunk_bitfield file; + + /* chunk was stored with spng_set_*() */ + struct spng_chunk_bitfield user; + + /* chunk was stored by reading or with spng_set_*() */ + struct spng_chunk_bitfield stored; + + /* used to reset the above in case of an error */ + struct spng_chunk_bitfield prev_stored; + + struct spng_chunk first_idat, last_idat; + + uint32_t max_width, max_height; + + size_t max_chunk_size; + size_t chunk_cache_limit; + size_t chunk_cache_usage; + uint32_t chunk_count_limit; + uint32_t chunk_count_total; + + int crc_action_critical; + int crc_action_ancillary; + + uint32_t optimize_option; + + struct spng_ihdr ihdr; + + struct spng_plte plte; + + struct spng_chrm_int chrm_int; + struct spng_iccp iccp; + + uint32_t gama; + + struct spng_sbit sbit; + + uint8_t srgb_rendering_intent; + + uint32_t n_text; + struct spng_text2 *text_list; + + struct spng_bkgd bkgd; + struct spng_hist hist; + struct spng_trns trns; + struct spng_phys phys; + + uint32_t n_splt; + struct spng_splt *splt_list; + + struct spng_time time; + struct spng_offs offs; + struct spng_exif exif; + + uint32_t n_chunks; + struct spng_unknown_chunk *chunk_list; + + struct spng_subimage subimage[7]; + + z_stream zstream; + unsigned char *scanline_buf, *prev_scanline_buf, *row_buf, *filtered_scanline_buf; + unsigned char *scanline, *prev_scanline, *row, *filtered_scanline; + + /* based on fmt */ + size_t image_size; /* may be zero */ + size_t image_width; + + unsigned bytes_per_pixel; /* derived from ihdr */ + unsigned pixel_size; /* derived from spng_format+ihdr */ + int widest_pass; + int last_pass; /* last non-empty pass */ + + uint16_t *gamma_lut; /* points to either _lut8 or _lut16 */ + uint16_t *gamma_lut16; + uint16_t gamma_lut8[256]; + unsigned char trns_px[8]; + union spng__decode_plte decode_plte; + struct spng_sbit decode_sb; + struct decode_flags decode_flags; + struct spng_row_info row_info; + + struct encode_flags encode_flags; +}; + +static const uint32_t spng_u32max = INT32_MAX; + +static const uint32_t adam7_x_start[7] = { 0, 4, 0, 2, 0, 1, 0 }; +static const uint32_t adam7_y_start[7] = { 0, 0, 4, 0, 2, 0, 1 }; +static const uint32_t adam7_x_delta[7] = { 8, 8, 4, 4, 2, 2, 1 }; +static const uint32_t adam7_y_delta[7] = { 8, 8, 8, 4, 4, 2, 2 }; + +static const uint8_t spng_signature[8] = { 137, 80, 78, 71, 13, 10, 26, 10 }; + +static const uint8_t type_ihdr[4] = { 73, 72, 68, 82 }; +static const uint8_t type_plte[4] = { 80, 76, 84, 69 }; +static const uint8_t type_idat[4] = { 73, 68, 65, 84 }; +static const uint8_t type_iend[4] = { 73, 69, 78, 68 }; + +static const uint8_t type_trns[4] = { 116, 82, 78, 83 }; +static const uint8_t type_chrm[4] = { 99, 72, 82, 77 }; +static const uint8_t type_gama[4] = { 103, 65, 77, 65 }; +static const uint8_t type_iccp[4] = { 105, 67, 67, 80 }; +static const uint8_t type_sbit[4] = { 115, 66, 73, 84 }; +static const uint8_t type_srgb[4] = { 115, 82, 71, 66 }; +static const uint8_t type_text[4] = { 116, 69, 88, 116 }; +static const uint8_t type_ztxt[4] = { 122, 84, 88, 116 }; +static const uint8_t type_itxt[4] = { 105, 84, 88, 116 }; +static const uint8_t type_bkgd[4] = { 98, 75, 71, 68 }; +static const uint8_t type_hist[4] = { 104, 73, 83, 84 }; +static const uint8_t type_phys[4] = { 112, 72, 89, 115 }; +static const uint8_t type_splt[4] = { 115, 80, 76, 84 }; +static const uint8_t type_time[4] = { 116, 73, 77, 69 }; + +static const uint8_t type_offs[4] = { 111, 70, 70, 115 }; +static const uint8_t type_exif[4] = { 101, 88, 73, 102 }; + +static inline void *spng__malloc(spng_ctx *ctx, size_t size) +{ + return ctx->alloc.malloc_fn(size); +} + +static inline void *spng__calloc(spng_ctx *ctx, size_t nmemb, size_t size) +{ + return ctx->alloc.calloc_fn(nmemb, size); +} + +static inline void *spng__realloc(spng_ctx *ctx, void *ptr, size_t size) +{ + return ctx->alloc.realloc_fn(ptr, size); +} + +static inline void spng__free(spng_ctx *ctx, void *ptr) +{ + ctx->alloc.free_fn(ptr); +} + +#if defined(SPNG_USE_MINIZ) +static void *spng__zalloc(void *opaque, size_t items, size_t size) +#else +static void *spng__zalloc(void *opaque, uInt items, uInt size) +#endif +{ + spng_ctx *ctx = opaque; + + if(size > SIZE_MAX / items) return NULL; + + size_t len = (size_t)items * size; + + return spng__malloc(ctx, len); +} + +static void spng__zfree(void *opqaue, void *ptr) +{ + spng_ctx *ctx = opqaue; + spng__free(ctx, ptr); +} + +static inline uint16_t read_u16(const void *src) +{ + const unsigned char *data = src; + + return (data[0] & 0xFFU) << 8 | (data[1] & 0xFFU); +} + +static inline uint32_t read_u32(const void *src) +{ + const unsigned char *data = src; + + return (data[0] & 0xFFUL) << 24 | (data[1] & 0xFFUL) << 16 | + (data[2] & 0xFFUL) << 8 | (data[3] & 0xFFUL); +} + +static inline int32_t read_s32(const void *src) +{ + int32_t ret = (int32_t)read_u32(src); + + return ret; +} + +static inline void write_u16(void *dest, uint16_t x) +{ + unsigned char *data = dest; + + data[0] = x >> 8; + data[1] = x & 0xFF; +} + +static inline void write_u32(void *dest, uint32_t x) +{ + unsigned char *data = dest; + + data[0] = (x >> 24); + data[1] = (x >> 16) & 0xFF; + data[2] = (x >> 8) & 0xFF; + data[3] = x & 0xFF; +} + +static inline void write_s32(void *dest, int32_t x) +{ + uint32_t n = x; + write_u32(dest, n); +} + +/* Returns an iterator for 1,2,4,8-bit samples */ +static struct spng__iter spng__iter_init(unsigned bit_depth, const unsigned char *samples) +{ + struct spng__iter iter = + { + .mask = (uint32_t)(1 << bit_depth) - 1, + .shift_amount = 8 - bit_depth, + .initial_shift = 8 - bit_depth, + .bit_depth = bit_depth, + .samples = samples + }; + + return iter; +} + +/* Returns the current sample unpacked, iterates to the next one */ +static inline uint8_t get_sample(struct spng__iter *iter) +{ + uint8_t x = (iter->samples[0] >> iter->shift_amount) & iter->mask; + + iter->shift_amount -= iter->bit_depth; + + if(iter->shift_amount > 7) + { + iter->shift_amount = iter->initial_shift; + iter->samples++; + } + + return x; +} + +static void u16_row_to_host(void *row, size_t size) +{ + uint16_t *px = row; + size_t i, n = size / 2; + + for(i=0; i < n; i++) + { + px[i] = read_u16(&px[i]); + } +} + +static void u16_row_to_bigendian(void *row, size_t size) +{ + uint16_t *px = (uint16_t*)row; + size_t i, n = size / 2; + + for(i=0; i < n; i++) + { + write_u16(&px[i], px[i]); + } +} + +static void rgb8_row_to_rgba8(const unsigned char *row, unsigned char *out, uint32_t n) +{ + uint32_t i; + for(i=0; i < n; i++) + { + memcpy(out + i * 4, row + i * 3, 3); + out[i*4+3] = 255; + } +} + +static unsigned num_channels(const struct spng_ihdr *ihdr) +{ + switch(ihdr->color_type) + { + case SPNG_COLOR_TYPE_TRUECOLOR: return 3; + case SPNG_COLOR_TYPE_GRAYSCALE_ALPHA: return 2; + case SPNG_COLOR_TYPE_TRUECOLOR_ALPHA: return 4; + case SPNG_COLOR_TYPE_GRAYSCALE: + case SPNG_COLOR_TYPE_INDEXED: + return 1; + default: return 0; + } +} + +/* Calculate scanline width in bits, round up to the nearest byte */ +static int calculate_scanline_width(const struct spng_ihdr *ihdr, uint32_t width, size_t *scanline_width) +{ + if(ihdr == NULL || !width) return SPNG_EINTERNAL; + + size_t res = num_channels(ihdr) * ihdr->bit_depth; + + if(res > SIZE_MAX / width) return SPNG_EOVERFLOW; + res = res * width; + + res += 15; /* Filter byte + 7 for rounding */ + + if(res < 15) return SPNG_EOVERFLOW; + + res /= 8; + + if(res > UINT32_MAX) return SPNG_EOVERFLOW; + + *scanline_width = res; + + return 0; +} + +static int calculate_subimages(struct spng_ctx *ctx) +{ + if(ctx == NULL) return SPNG_EINTERNAL; + + struct spng_ihdr *ihdr = &ctx->ihdr; + struct spng_subimage *sub = ctx->subimage; + + if(ihdr->interlace_method == 1) + { + sub[0].width = (ihdr->width + 7) >> 3; + sub[0].height = (ihdr->height + 7) >> 3; + sub[1].width = (ihdr->width + 3) >> 3; + sub[1].height = (ihdr->height + 7) >> 3; + sub[2].width = (ihdr->width + 3) >> 2; + sub[2].height = (ihdr->height + 3) >> 3; + sub[3].width = (ihdr->width + 1) >> 2; + sub[3].height = (ihdr->height + 3) >> 2; + sub[4].width = (ihdr->width + 1) >> 1; + sub[4].height = (ihdr->height + 1) >> 2; + sub[5].width = ihdr->width >> 1; + sub[5].height = (ihdr->height + 1) >> 1; + sub[6].width = ihdr->width; + sub[6].height = ihdr->height >> 1; + } + else + { + sub[0].width = ihdr->width; + sub[0].height = ihdr->height; + } + + int i; + for(i=0; i < 7; i++) + { + if(sub[i].width == 0 || sub[i].height == 0) continue; + + int ret = calculate_scanline_width(ihdr, sub[i].width, &sub[i].scanline_width); + if(ret) return ret; + + if(sub[ctx->widest_pass].scanline_width < sub[i].scanline_width) ctx->widest_pass = i; + + ctx->last_pass = i; + } + + return 0; +} + +static int check_decode_fmt(const struct spng_ihdr *ihdr, const int fmt) +{ + switch(fmt) + { + case SPNG_FMT_RGBA8: + case SPNG_FMT_RGBA16: + case SPNG_FMT_RGB8: + case SPNG_FMT_PNG: + case SPNG_FMT_RAW: + return 0; + case SPNG_FMT_G8: + case SPNG_FMT_GA8: + if(ihdr->color_type == SPNG_COLOR_TYPE_GRAYSCALE && ihdr->bit_depth <= 8) return 0; + else return SPNG_EFMT; + case SPNG_FMT_GA16: + if(ihdr->color_type == SPNG_COLOR_TYPE_GRAYSCALE && ihdr->bit_depth == 16) return 0; + else return SPNG_EFMT; + default: return SPNG_EFMT; + } +} + +static int calculate_image_width(const struct spng_ihdr *ihdr, int fmt, size_t *len) +{ + if(ihdr == NULL || len == NULL) return SPNG_EINTERNAL; + + size_t res = ihdr->width; + unsigned bytes_per_pixel; + + switch(fmt) + { + case SPNG_FMT_RGBA8: + case SPNG_FMT_GA16: + bytes_per_pixel = 4; + break; + case SPNG_FMT_RGBA16: + bytes_per_pixel = 8; + break; + case SPNG_FMT_RGB8: + bytes_per_pixel = 3; + break; + case SPNG_FMT_PNG: + case SPNG_FMT_RAW: + { + int ret = calculate_scanline_width(ihdr, ihdr->width, &res); + if(ret) return ret; + + res -= 1; /* exclude filter byte */ + bytes_per_pixel = 1; + break; + } + case SPNG_FMT_G8: + bytes_per_pixel = 1; + break; + case SPNG_FMT_GA8: + bytes_per_pixel = 2; + break; + default: return SPNG_EINTERNAL; + } + + if(res > SIZE_MAX / bytes_per_pixel) return SPNG_EOVERFLOW; + res = res * bytes_per_pixel; + + *len = res; + + return 0; +} + +static int calculate_image_size(const struct spng_ihdr *ihdr, int fmt, size_t *len) +{ + if(ihdr == NULL || len == NULL) return SPNG_EINTERNAL; + + size_t res = 0; + + int ret = calculate_image_width(ihdr, fmt, &res); + if(ret) return ret; + + if(res > SIZE_MAX / ihdr->height) return SPNG_EOVERFLOW; + res = res * ihdr->height; + + *len = res; + + return 0; +} + +static int increase_cache_usage(spng_ctx *ctx, size_t bytes, int new_chunk) +{ + if(ctx == NULL || !bytes) return SPNG_EINTERNAL; + + if(new_chunk) + { + ctx->chunk_count_total++; + if(ctx->chunk_count_total < 1) return SPNG_EOVERFLOW; + + if(ctx->chunk_count_total > ctx->chunk_count_limit) return SPNG_ECHUNK_LIMITS; + } + + size_t new_usage = ctx->chunk_cache_usage + bytes; + + if(new_usage < ctx->chunk_cache_usage) return SPNG_EOVERFLOW; + + if(new_usage > ctx->chunk_cache_limit) return SPNG_ECHUNK_LIMITS; + + ctx->chunk_cache_usage = new_usage; + + return 0; +} + +static int decrease_cache_usage(spng_ctx *ctx, size_t usage) +{ + if(ctx == NULL || !usage) return SPNG_EINTERNAL; + if(usage > ctx->chunk_cache_usage) return SPNG_EINTERNAL; + + ctx->chunk_cache_usage -= usage; + + return 0; +} + +static int is_critical_chunk(struct spng_chunk *chunk) +{ + if(chunk == NULL) return 0; + if((chunk->type[0] & (1 << 5)) == 0) return 1; + + return 0; +} + +static int decode_err(spng_ctx *ctx, int err) +{ + ctx->state = SPNG_STATE_INVALID; + + return err; +} + +static int encode_err(spng_ctx *ctx, int err) +{ + ctx->state = SPNG_STATE_INVALID; + + return err; +} + +static inline int read_data(spng_ctx *ctx, size_t bytes) +{ + if(ctx == NULL) return SPNG_EINTERNAL; + if(!bytes) return 0; + + if(ctx->streaming && (bytes > SPNG_READ_SIZE)) return SPNG_EINTERNAL; + + int ret = ctx->read_fn(ctx, ctx->stream_user_ptr, ctx->stream_buf, bytes); + + if(ret) + { + if(ret > 0 || ret < SPNG_IO_ERROR) ret = SPNG_IO_ERROR; + + return ret; + } + + ctx->bytes_read += bytes; + if(ctx->bytes_read < bytes) return SPNG_EOVERFLOW; + + return 0; +} + +/* Ensure there is enough space for encoding starting at ctx->write_ptr */ +static int require_bytes(spng_ctx *ctx, size_t bytes) +{ + if(ctx == NULL) return SPNG_EINTERNAL; + + if(ctx->streaming) + { + if(bytes > ctx->stream_buf_size) + { + size_t new_size = ctx->stream_buf_size; + + /* Start at default IDAT size + header + crc */ + if(new_size < (SPNG_WRITE_SIZE + 12)) new_size = SPNG_WRITE_SIZE + 12; + + if(new_size < bytes) new_size = bytes; + + void *temp = spng__realloc(ctx, ctx->stream_buf, new_size); + + if(temp == NULL) return encode_err(ctx, SPNG_EMEM); + + ctx->stream_buf = temp; + ctx->stream_buf_size = bytes; + ctx->write_ptr = ctx->stream_buf; + } + + return 0; + } + + if(!ctx->internal_buffer) return SPNG_ENODST; + + size_t required = ctx->bytes_encoded + bytes; + if(required < bytes) return SPNG_EOVERFLOW; + + if(required > ctx->out_png_size) + { + size_t new_size = ctx->out_png_size; + + /* Start with a size that doesn't require a realloc() 100% of the time */ + if(new_size < (SPNG_WRITE_SIZE * 2)) new_size = SPNG_WRITE_SIZE * 2; + + /* Prefer the next power of two over the requested size */ + while(new_size < required) + { + if(new_size / SIZE_MAX > 2) return encode_err(ctx, SPNG_EOVERFLOW); + + new_size *= 2; + } + + void *temp = spng__realloc(ctx, ctx->out_png, new_size); + + if(temp == NULL) return encode_err(ctx, SPNG_EMEM); + + ctx->out_png = temp; + ctx->out_png_size = new_size; + ctx->write_ptr = ctx->out_png + ctx->bytes_encoded; + } + + return 0; +} + +static int write_data(spng_ctx *ctx, const void *data, size_t bytes) +{ + if(ctx == NULL) return SPNG_EINTERNAL; + if(!bytes) return 0; + + if(ctx->streaming) + { + if(bytes > SPNG_WRITE_SIZE) return SPNG_EINTERNAL; + + int ret = ctx->write_fn(ctx, ctx->stream_user_ptr, (void*)data, bytes); + + if(ret) + { + if(ret > 0 || ret < SPNG_IO_ERROR) ret = SPNG_IO_ERROR; + + return encode_err(ctx, ret); + } + } + else + { + int ret = require_bytes(ctx, bytes); + if(ret) return encode_err(ctx, ret); + + memcpy(ctx->write_ptr, data, bytes); + + ctx->write_ptr += bytes; + } + + ctx->bytes_encoded += bytes; + if(ctx->bytes_encoded < bytes) return SPNG_EOVERFLOW; + + return 0; +} + +static int write_header(spng_ctx *ctx, const uint8_t chunk_type[4], size_t chunk_length, unsigned char **data) +{ + if(ctx == NULL || chunk_type == NULL) return SPNG_EINTERNAL; + if(chunk_length > spng_u32max) return SPNG_EINTERNAL; + + size_t total = chunk_length + 12; + + int ret = require_bytes(ctx, total); + if(ret) return ret; + + uint32_t crc = crc32(0, NULL, 0); + ctx->current_chunk.crc = crc32(crc, chunk_type, 4); + + memcpy(&ctx->current_chunk.type, chunk_type, 4); + ctx->current_chunk.length = (uint32_t)chunk_length; + + if(!data) return SPNG_EINTERNAL; + + if(ctx->streaming) *data = ctx->stream_buf + 8; + else *data = ctx->write_ptr + 8; + + return 0; +} + +static int trim_chunk(spng_ctx *ctx, uint32_t length) +{ + if(length > spng_u32max) return SPNG_EINTERNAL; + if(length > ctx->current_chunk.length) return SPNG_EINTERNAL; + + ctx->current_chunk.length = length; + + return 0; +} + +static int finish_chunk(spng_ctx *ctx) +{ + if(ctx == NULL) return SPNG_EINTERNAL; + + struct spng_chunk *chunk = &ctx->current_chunk; + + unsigned char *header; + unsigned char *chunk_data; + + if(ctx->streaming) + { + chunk_data = ctx->stream_buf + 8; + header = ctx->stream_buf; + } + else + { + chunk_data = ctx->write_ptr + 8; + header = ctx->write_ptr; + } + + write_u32(header, chunk->length); + memcpy(header + 4, chunk->type, 4); + + chunk->crc = crc32(chunk->crc, chunk_data, chunk->length); + + write_u32(chunk_data + chunk->length, chunk->crc); + + if(ctx->streaming) + { + const unsigned char *ptr = ctx->stream_buf; + uint32_t bytes_left = chunk->length + 12; + uint32_t len = 0; + + while(bytes_left) + { + ptr += len; + len = SPNG_WRITE_SIZE; + + if(len > bytes_left) len = bytes_left; + + int ret = write_data(ctx, ptr, len); + if(ret) return ret; + + bytes_left -= len; + } + } + else + { + ctx->bytes_encoded += chunk->length; + if(ctx->bytes_encoded < chunk->length) return SPNG_EOVERFLOW; + + ctx->bytes_encoded += 12; + if(ctx->bytes_encoded < 12) return SPNG_EOVERFLOW; + + ctx->write_ptr += chunk->length + 12; + } + + return 0; +} + +static int write_chunk(spng_ctx *ctx, const uint8_t type[4], const void *data, size_t length) +{ + if(ctx == NULL || type == NULL) return SPNG_EINTERNAL; + if(length && data == NULL) return SPNG_EINTERNAL; + + unsigned char *write_ptr; + + int ret = write_header(ctx, type, length, &write_ptr); + if(ret) return ret; + + if(length) memcpy(write_ptr, data, length); + + return finish_chunk(ctx); +} + +static int write_iend(spng_ctx *ctx) +{ + unsigned char iend_chunk[12] = { 0, 0, 0, 0, 73, 69, 78, 68, 174, 66, 96, 130 }; + return write_data(ctx, iend_chunk, 12); +} + +static int write_unknown_chunks(spng_ctx *ctx, enum spng_location location) +{ + if(!ctx->stored.unknown) return 0; + + const struct spng_unknown_chunk *chunk = ctx->chunk_list; + + uint32_t i; + for(i=0; i < ctx->n_chunks; i++, chunk++) + { + if(chunk->location != location) continue; + + int ret = write_chunk(ctx, chunk->type, chunk->data, chunk->length); + if(ret) return ret; + } + + return 0; +} + +/* Read and check the current chunk's crc, + returns -SPNG_CRC_DISCARD if the chunk should be discarded */ +static inline int read_and_check_crc(spng_ctx *ctx) +{ + if(ctx == NULL) return SPNG_EINTERNAL; + + int ret; + ret = read_data(ctx, 4); + if(ret) return ret; + + ctx->current_chunk.crc = read_u32(ctx->data); + + if(ctx->skip_crc) return 0; + + if(ctx->cur_actual_crc != ctx->current_chunk.crc) + { + if(is_critical_chunk(&ctx->current_chunk)) + { + if(ctx->crc_action_critical == SPNG_CRC_USE) return 0; + } + else + { + if(ctx->crc_action_ancillary == SPNG_CRC_USE) return 0; + if(ctx->crc_action_ancillary == SPNG_CRC_DISCARD) return -SPNG_CRC_DISCARD; + } + + return SPNG_ECHUNK_CRC; + } + + return 0; +} + +/* Read and validate the current chunk's crc and the next chunk header */ +static inline int read_header(spng_ctx *ctx) +{ + if(ctx == NULL) return SPNG_EINTERNAL; + + int ret; + struct spng_chunk chunk = { 0 }; + + ret = read_and_check_crc(ctx); + if(ret) + { + if(ret == -SPNG_CRC_DISCARD) + { + ctx->discard = 1; + } + else return ret; + } + + ret = read_data(ctx, 8); + if(ret) return ret; + + chunk.offset = ctx->bytes_read - 8; + + chunk.length = read_u32(ctx->data); + + memcpy(&chunk.type, ctx->data + 4, 4); + + if(chunk.length > spng_u32max) return SPNG_ECHUNK_STDLEN; + + ctx->cur_chunk_bytes_left = chunk.length; + + if(is_critical_chunk(&chunk) && ctx->crc_action_critical == SPNG_CRC_USE) ctx->skip_crc = 1; + else if(ctx->crc_action_ancillary == SPNG_CRC_USE) ctx->skip_crc = 1; + else ctx->skip_crc = 0; + + if(!ctx->skip_crc) + { + ctx->cur_actual_crc = crc32(0, NULL, 0); + ctx->cur_actual_crc = crc32(ctx->cur_actual_crc, chunk.type, 4); + } + + ctx->current_chunk = chunk; + + return 0; +} + +/* Read chunk bytes and update crc */ +static int read_chunk_bytes(spng_ctx *ctx, uint32_t bytes) +{ + if(ctx == NULL) return SPNG_EINTERNAL; + if(!ctx->cur_chunk_bytes_left || !bytes) return SPNG_EINTERNAL; + if(bytes > ctx->cur_chunk_bytes_left) return SPNG_EINTERNAL; /* XXX: more specific error? */ + + int ret; + + ret = read_data(ctx, bytes); + if(ret) return ret; + + if(!ctx->skip_crc) ctx->cur_actual_crc = crc32(ctx->cur_actual_crc, ctx->data, bytes); + + ctx->cur_chunk_bytes_left -= bytes; + + return ret; +} + +/* read_chunk_bytes() + read_data() with custom output buffer */ +static int read_chunk_bytes2(spng_ctx *ctx, void *out, uint32_t bytes) +{ + if(ctx == NULL) return SPNG_EINTERNAL; + if(!ctx->cur_chunk_bytes_left || !bytes) return SPNG_EINTERNAL; + if(bytes > ctx->cur_chunk_bytes_left) return SPNG_EINTERNAL; /* XXX: more specific error? */ + + int ret; + uint32_t len = bytes; + + if(ctx->streaming && len > SPNG_READ_SIZE) len = SPNG_READ_SIZE; + + while(bytes) + { + if(len > bytes) len = bytes; + + ret = ctx->read_fn(ctx, ctx->stream_user_ptr, out, len); + if(ret) return ret; + + if(!ctx->streaming) memcpy(out, ctx->data, len); + + ctx->bytes_read += len; + if(ctx->bytes_read < len) return SPNG_EOVERFLOW; + + if(!ctx->skip_crc) ctx->cur_actual_crc = crc32(ctx->cur_actual_crc, out, len); + + ctx->cur_chunk_bytes_left -= len; + + out = (char*)out + len; + bytes -= len; + len = SPNG_READ_SIZE; + } + + return 0; +} + +static int discard_chunk_bytes(spng_ctx *ctx, uint32_t bytes) +{ + if(ctx == NULL) return SPNG_EINTERNAL; + if(!bytes) return 0; + + int ret; + + if(ctx->streaming) /* Do small, consecutive reads */ + { + while(bytes) + { + uint32_t len = SPNG_READ_SIZE; + + if(len > bytes) len = bytes; + + ret = read_chunk_bytes(ctx, len); + if(ret) return ret; + + bytes -= len; + } + } + else + { + ret = read_chunk_bytes(ctx, bytes); + if(ret) return ret; + } + + return 0; +} + +static int spng__inflate_init(spng_ctx *ctx, int window_bits) +{ + if(ctx->zstream.state) inflateEnd(&ctx->zstream); + + ctx->inflate = 1; + + ctx->zstream.zalloc = spng__zalloc; + ctx->zstream.zfree = spng__zfree; + ctx->zstream.opaque = ctx; + + if(inflateInit2(&ctx->zstream, window_bits) != Z_OK) return SPNG_EZLIB_INIT; + +#if ZLIB_VERNUM >= 0x1290 && !defined(SPNG_USE_MINIZ) + + int validate = 1; + + if(ctx->flags & SPNG_CTX_IGNORE_ADLER32) validate = 0; + + if(is_critical_chunk(&ctx->current_chunk)) + { + if(ctx->crc_action_critical == SPNG_CRC_USE) validate = 0; + } + else /* ancillary */ + { + if(ctx->crc_action_ancillary == SPNG_CRC_USE) validate = 0; + } + + if(inflateValidate(&ctx->zstream, validate)) return SPNG_EZLIB_INIT; + +#else /* This requires zlib >= 1.2.11 */ + #pragma message ("inflateValidate() not available, SPNG_CTX_IGNORE_ADLER32 will be ignored") +#endif + + return 0; +} + +static int spng__deflate_init(spng_ctx *ctx, struct spng__zlib_options *options) +{ + if(ctx->zstream.state) deflateEnd(&ctx->zstream); + + ctx->deflate = 1; + + z_stream *zstream = &ctx->zstream; + zstream->zalloc = spng__zalloc; + zstream->zfree = spng__zfree; + zstream->opaque = ctx; + zstream->data_type = options->data_type; + + int ret = deflateInit2(zstream, options->compression_level, Z_DEFLATED, options->window_bits, options->mem_level, options->strategy); + + if(ret != Z_OK) return SPNG_EZLIB_INIT; + + return 0; +} + +/* Inflate a zlib stream starting with start_buf if non-NULL, + continuing from the datastream till an end marker, + allocating and writing the inflated stream to *out, + leaving "extra" bytes at the end, final buffer length is *len. + + Takes into account the chunk size and cache limits. +*/ +static int spng__inflate_stream(spng_ctx *ctx, char **out, size_t *len, size_t extra, const void *start_buf, size_t start_len) +{ + int ret = spng__inflate_init(ctx, 15); + if(ret) return ret; + + size_t max = ctx->chunk_cache_limit - ctx->chunk_cache_usage; + + if(ctx->max_chunk_size < max) max = ctx->max_chunk_size; + + if(extra > max) return SPNG_ECHUNK_LIMITS; + max -= extra; + + uint32_t read_size; + size_t size = 8 * 1024; + void *t, *buf = spng__malloc(ctx, size); + + if(buf == NULL) return SPNG_EMEM; + + z_stream *stream = &ctx->zstream; + + if(start_buf != NULL && start_len) + { + stream->avail_in = (uInt)start_len; + stream->next_in = start_buf; + } + else + { + stream->avail_in = 0; + stream->next_in = NULL; + } + + stream->avail_out = (uInt)size; + stream->next_out = buf; + + while(ret != Z_STREAM_END) + { + ret = inflate(stream, Z_NO_FLUSH); + + if(ret == Z_STREAM_END) break; + + if(ret != Z_OK && ret != Z_BUF_ERROR) + { + ret = SPNG_EZLIB; + goto err; + } + + if(!stream->avail_out) /* Resize buffer */ + { + /* overflow or reached chunk/cache limit */ + if( (2 > SIZE_MAX / size) || (size > max / 2) ) + { + ret = SPNG_ECHUNK_LIMITS; + goto err; + } + + size *= 2; + + t = spng__realloc(ctx, buf, size); + if(t == NULL) goto mem; + + buf = t; + + stream->avail_out = (uInt)size / 2; + stream->next_out = (unsigned char*)buf + size / 2; + } + else if(!stream->avail_in) /* Read more chunk bytes */ + { + read_size = ctx->cur_chunk_bytes_left; + if(ctx->streaming && read_size > SPNG_READ_SIZE) read_size = SPNG_READ_SIZE; + + ret = read_chunk_bytes(ctx, read_size); + + if(ret) + { + if(!read_size) ret = SPNG_EZLIB; + + goto err; + } + + stream->avail_in = read_size; + stream->next_in = ctx->data; + } + } + + size = stream->total_out; + + if(!size) + { + ret = SPNG_EZLIB; + goto err; + } + + size += extra; + if(size < extra) goto mem; + + t = spng__realloc(ctx, buf, size); + if(t == NULL) goto mem; + + buf = t; + + (void)increase_cache_usage(ctx, size, 0); + + *out = buf; + *len = size; + + return 0; + +mem: + ret = SPNG_EMEM; +err: + spng__free(ctx, buf); + return ret; +} + +/* Read at least one byte from the IDAT stream */ +static int read_idat_bytes(spng_ctx *ctx, uint32_t *bytes_read) +{ + if(ctx == NULL || bytes_read == NULL) return SPNG_EINTERNAL; + if(memcmp(ctx->current_chunk.type, type_idat, 4)) return SPNG_EIDAT_TOO_SHORT; + + int ret; + uint32_t len; + + while(!ctx->cur_chunk_bytes_left) + { + ret = read_header(ctx); + if(ret) return ret; + + if(memcmp(ctx->current_chunk.type, type_idat, 4)) return SPNG_EIDAT_TOO_SHORT; + } + + if(ctx->streaming) + {/* TODO: estimate bytes to read for progressive reads */ + len = SPNG_READ_SIZE; + if(len > ctx->cur_chunk_bytes_left) len = ctx->cur_chunk_bytes_left; + } + else len = ctx->current_chunk.length; + + ret = read_chunk_bytes(ctx, len); + + *bytes_read = len; + + return ret; +} + +static int read_scanline_bytes(spng_ctx *ctx, unsigned char *dest, size_t len) +{ + if(ctx == NULL || dest == NULL) return SPNG_EINTERNAL; + + int ret = Z_OK; + uint32_t bytes_read; + + z_stream *zstream = &ctx->zstream; + + zstream->avail_out = (uInt)len; + zstream->next_out = dest; + + while(zstream->avail_out != 0) + { + ret = inflate(zstream, Z_NO_FLUSH); + + if(ret == Z_OK) continue; + + if(ret == Z_STREAM_END) /* Reached an end-marker */ + { + if(zstream->avail_out != 0) return SPNG_EIDAT_TOO_SHORT; + } + else if(ret == Z_BUF_ERROR) /* Read more IDAT bytes */ + { + ret = read_idat_bytes(ctx, &bytes_read); + if(ret) return ret; + + zstream->avail_in = bytes_read; + zstream->next_in = ctx->data; + } + else return SPNG_EIDAT_STREAM; + } + + return 0; +} + +static uint8_t paeth(uint8_t a, uint8_t b, uint8_t c) +{ + int16_t p = a + b - c; + int16_t pa = abs(p - a); + int16_t pb = abs(p - b); + int16_t pc = abs(p - c); + + if(pa <= pb && pa <= pc) return a; + else if(pb <= pc) return b; + + return c; +} + +SPNG_TARGET_CLONES("default,avx2") +static void defilter_up(size_t bytes, unsigned char *row, const unsigned char *prev) +{ + size_t i; + for(i=0; i < bytes; i++) + { + row[i] += prev[i]; + } +} + +/* Defilter *scanline in-place. + *prev_scanline and *scanline should point to the first pixel, + scanline_width is the width of the scanline including the filter byte. +*/ +static int defilter_scanline(const unsigned char *prev_scanline, unsigned char *scanline, + size_t scanline_width, unsigned bytes_per_pixel, unsigned filter) +{ + if(prev_scanline == NULL || scanline == NULL || !scanline_width) return SPNG_EINTERNAL; + + size_t i; + scanline_width--; + + if(filter == 0) return 0; + +#ifndef SPNG_DISABLE_OPT + if(filter == SPNG_FILTER_UP) goto no_opt; + + if(bytes_per_pixel == 4) + { + if(filter == SPNG_FILTER_SUB) + defilter_sub4(scanline_width, scanline); + else if(filter == SPNG_FILTER_AVERAGE) + defilter_avg4(scanline_width, scanline, prev_scanline); + else if(filter == SPNG_FILTER_PAETH) + defilter_paeth4(scanline_width, scanline, prev_scanline); + else return SPNG_EFILTER; + + return 0; + } + else if(bytes_per_pixel == 3) + { + if(filter == SPNG_FILTER_SUB) + defilter_sub3(scanline_width, scanline); + else if(filter == SPNG_FILTER_AVERAGE) + defilter_avg3(scanline_width, scanline, prev_scanline); + else if(filter == SPNG_FILTER_PAETH) + defilter_paeth3(scanline_width, scanline, prev_scanline); + else return SPNG_EFILTER; + + return 0; + } +no_opt: +#endif + + if(filter == SPNG_FILTER_UP) + { + defilter_up(scanline_width, scanline, prev_scanline); + return 0; + } + + for(i=0; i < scanline_width; i++) + { + uint8_t x, a, b, c; + + if(i >= bytes_per_pixel) + { + a = scanline[i - bytes_per_pixel]; + b = prev_scanline[i]; + c = prev_scanline[i - bytes_per_pixel]; + } + else /* First pixel in row */ + { + a = 0; + b = prev_scanline[i]; + c = 0; + } + + x = scanline[i]; + + switch(filter) + { + case SPNG_FILTER_SUB: + { + x = x + a; + break; + } + case SPNG_FILTER_AVERAGE: + { + uint16_t avg = (a + b) / 2; + x = x + avg; + break; + } + case SPNG_FILTER_PAETH: + { + x = x + paeth(a,b,c); + break; + } + } + + scanline[i] = x; + } + + return 0; +} + +static int filter_scanline(unsigned char *filtered, const unsigned char *prev_scanline, const unsigned char *scanline, + size_t scanline_width, unsigned bytes_per_pixel, const unsigned filter) +{ + if(prev_scanline == NULL || scanline == NULL || scanline_width <= 1) return SPNG_EINTERNAL; + + if(filter > 4) return SPNG_EFILTER; + if(filter == 0) return 0; + + scanline_width--; + + uint32_t i; + for(i=0; i < scanline_width; i++) + { + uint8_t x, a, b, c; + + if(i >= bytes_per_pixel) + { + a = scanline[i - bytes_per_pixel]; + b = prev_scanline[i]; + c = prev_scanline[i - bytes_per_pixel]; + } + else /* first pixel in row */ + { + a = 0; + b = prev_scanline[i]; + c = 0; + } + + x = scanline[i]; + + switch(filter) + { + case SPNG_FILTER_SUB: + { + x = x - a; + break; + } + case SPNG_FILTER_UP: + { + x = x - b; + break; + } + case SPNG_FILTER_AVERAGE: + { + uint16_t avg = (a + b) / 2; + x = x - avg; + break; + } + case SPNG_FILTER_PAETH: + { + x = x - paeth(a,b,c); + break; + } + } + + filtered[i] = x; + } + + return 0; +} + +static int32_t filter_sum(const unsigned char *prev_scanline, const unsigned char *scanline, + size_t size, unsigned bytes_per_pixel, const unsigned filter) +{ + /* prevent potential over/underflow, bails out at a width of ~8M pixels for RGBA8 */ + if(size > (INT32_MAX / 128)) return INT32_MAX; + + uint32_t i; + int32_t sum = 0; + uint8_t x, a, b, c; + + for(i=0; i < size; i++) + { + if(i >= bytes_per_pixel) + { + a = scanline[i - bytes_per_pixel]; + b = prev_scanline[i]; + c = prev_scanline[i - bytes_per_pixel]; + } + else /* first pixel in row */ + { + a = 0; + b = prev_scanline[i]; + c = 0; + } + + x = scanline[i]; + + switch(filter) + { + case SPNG_FILTER_NONE: + { + break; + } + case SPNG_FILTER_SUB: + { + x = x - a; + break; + } + case SPNG_FILTER_UP: + { + x = x - b; + break; + } + case SPNG_FILTER_AVERAGE: + { + uint16_t avg = (a + b) / 2; + x = x - avg; + break; + } + case SPNG_FILTER_PAETH: + { + x = x - paeth(a,b,c); + break; + } + } + + sum += 128 - abs((int)x - 128); + } + + return sum; +} + +static unsigned get_best_filter(const unsigned char *prev_scanline, const unsigned char *scanline, + size_t scanline_width, unsigned bytes_per_pixel, const int choices) +{ + if(!choices) return SPNG_FILTER_NONE; + + scanline_width--; + + int i; + unsigned int best_filter = 0; + enum spng_filter_choice flag; + int32_t sum, best_score = INT32_MAX; + int32_t filter_scores[5] = { INT32_MAX, INT32_MAX, INT32_MAX, INT32_MAX, INT32_MAX }; + + if( !(choices & (choices - 1)) ) + {/* only one choice/bit is set */ + for(i=0; i < 5; i++) + { + if(choices == 1 << (i + 3)) return i; + } + } + + for(i=0; i < 5; i++) + { + flag = 1 << (i + 3); + + if(choices & flag) sum = filter_sum(prev_scanline, scanline, scanline_width, bytes_per_pixel, i); + else continue; + + filter_scores[i] = abs(sum); + + if(filter_scores[i] < best_score) + { + best_score = filter_scores[i]; + best_filter = i; + } + } + + return best_filter; +} + +/* Scale "sbits" significant bits in "sample" from "bit_depth" to "target" + + "bit_depth" must be a valid PNG depth + "sbits" must be less than or equal to "bit_depth" + "target" must be between 1 and 16 +*/ +static uint16_t sample_to_target(uint16_t sample, unsigned bit_depth, unsigned sbits, unsigned target) +{ + if(bit_depth == sbits) + { + if(target == sbits) return sample; /* No scaling */ + }/* bit_depth > sbits */ + else sample = sample >> (bit_depth - sbits); /* Shift significant bits to bottom */ + + /* Downscale */ + if(target < sbits) return sample >> (sbits - target); + + /* Upscale using left bit replication */ + int8_t shift_amount = target - sbits; + uint16_t sample_bits = sample; + sample = 0; + + while(shift_amount >= 0) + { + sample = sample | (sample_bits << shift_amount); + shift_amount -= sbits; + } + + int8_t partial = shift_amount + (int8_t)sbits; + + if(partial != 0) sample = sample | (sample_bits >> abs(shift_amount)); + + return sample; +} + +static inline void gamma_correct_row(unsigned char *row, uint32_t pixels, int fmt, const uint16_t *gamma_lut) +{ + uint32_t i; + + if(fmt == SPNG_FMT_RGBA8) + { + unsigned char *px; + for(i=0; i < pixels; i++) + { + px = row + i * 4; + + px[0] = gamma_lut[px[0]]; + px[1] = gamma_lut[px[1]]; + px[2] = gamma_lut[px[2]]; + } + } + else if(fmt == SPNG_FMT_RGBA16) + { + for(i=0; i < pixels; i++) + { + uint16_t px[4]; + memcpy(px, row + i * 8, 8); + + px[0] = gamma_lut[px[0]]; + px[1] = gamma_lut[px[1]]; + px[2] = gamma_lut[px[2]]; + + memcpy(row + i * 8, px, 8); + } + } + else if(fmt == SPNG_FMT_RGB8) + { + unsigned char *px; + for(i=0; i < pixels; i++) + { + px = row + i * 3; + + px[0] = gamma_lut[px[0]]; + px[1] = gamma_lut[px[1]]; + px[2] = gamma_lut[px[2]]; + } + } +} + +/* Apply transparency to output row */ +static inline void trns_row(unsigned char *row, + const unsigned char *scanline, + const unsigned char *trns, + unsigned scanline_stride, + struct spng_ihdr *ihdr, + uint32_t pixels, + int fmt) +{ + uint32_t i; + unsigned row_stride; + unsigned depth = ihdr->bit_depth; + + if(fmt == SPNG_FMT_RGBA8) + { + if(ihdr->color_type == SPNG_COLOR_TYPE_GRAYSCALE) return; /* already applied in the decoding loop */ + + row_stride = 4; + for(i=0; i < pixels; i++, scanline+=scanline_stride, row+=row_stride) + { + if(!memcmp(scanline, trns, scanline_stride)) row[3] = 0; + } + } + else if(fmt == SPNG_FMT_RGBA16) + { + if(ihdr->color_type == SPNG_COLOR_TYPE_GRAYSCALE) return; /* already applied in the decoding loop */ + + row_stride = 8; + for(i=0; i < pixels; i++, scanline+=scanline_stride, row+=row_stride) + { + if(!memcmp(scanline, trns, scanline_stride)) memset(row + 6, 0, 2); + } + } + else if(fmt == SPNG_FMT_GA8) + { + row_stride = 2; + + if(depth == 16) + { + for(i=0; i < pixels; i++, scanline+=scanline_stride, row+=row_stride) + { + if(!memcmp(scanline, trns, scanline_stride)) memset(row + 1, 0, 1); + } + } + else /* depth <= 8 */ + { + struct spng__iter iter = spng__iter_init(depth, scanline); + + for(i=0; i < pixels; i++, row+=row_stride) + { + if(trns[0] == get_sample(&iter)) row[1] = 0; + } + } + } + else if(fmt == SPNG_FMT_GA16) + { + row_stride = 4; + + if(depth == 16) + { + for(i=0; i< pixels; i++, scanline+=scanline_stride, row+=row_stride) + { + if(!memcmp(scanline, trns, 2)) memset(row + 2, 0, 2); + } + } + else + { + struct spng__iter iter = spng__iter_init(depth, scanline); + + for(i=0; i< pixels; i++, row+=row_stride) + { + if(trns[0] == get_sample(&iter)) memset(row + 2, 0, 2); + } + } + } + else return; +} + +static inline void scale_row(unsigned char *row, uint32_t pixels, int fmt, unsigned depth, const struct spng_sbit *sbit) +{ + uint32_t i; + + if(fmt == SPNG_FMT_RGBA8) + { + unsigned char px[4]; + for(i=0; i < pixels; i++) + { + memcpy(px, row + i * 4, 4); + + px[0] = sample_to_target(px[0], depth, sbit->red_bits, 8); + px[1] = sample_to_target(px[1], depth, sbit->green_bits, 8); + px[2] = sample_to_target(px[2], depth, sbit->blue_bits, 8); + px[3] = sample_to_target(px[3], depth, sbit->alpha_bits, 8); + + memcpy(row + i * 4, px, 4); + } + } + else if(fmt == SPNG_FMT_RGBA16) + { + uint16_t px[4]; + for(i=0; i < pixels; i++) + { + memcpy(px, row + i * 8, 8); + + px[0] = sample_to_target(px[0], depth, sbit->red_bits, 16); + px[1] = sample_to_target(px[1], depth, sbit->green_bits, 16); + px[2] = sample_to_target(px[2], depth, sbit->blue_bits, 16); + px[3] = sample_to_target(px[3], depth, sbit->alpha_bits, 16); + + memcpy(row + i * 8, px, 8); + } + } + else if(fmt == SPNG_FMT_RGB8) + { + unsigned char px[4]; + for(i=0; i < pixels; i++) + { + memcpy(px, row + i * 3, 3); + + px[0] = sample_to_target(px[0], depth, sbit->red_bits, 8); + px[1] = sample_to_target(px[1], depth, sbit->green_bits, 8); + px[2] = sample_to_target(px[2], depth, sbit->blue_bits, 8); + + memcpy(row + i * 3, px, 3); + } + } + else if(fmt == SPNG_FMT_G8) + { + for(i=0; i < pixels; i++) + { + row[i] = sample_to_target(row[i], depth, sbit->grayscale_bits, 8); + } + } + else if(fmt == SPNG_FMT_GA8) + { + for(i=0; i < pixels; i++) + { + row[i*2] = sample_to_target(row[i*2], depth, sbit->grayscale_bits, 8); + } + } +} + +/* Expand to *row using 8-bit palette indices from *scanline */ +static void expand_row(unsigned char *row, + const unsigned char *scanline, + const union spng__decode_plte *decode_plte, + uint32_t width, + int fmt) +{ + uint32_t i = 0; + unsigned char *px; + unsigned char entry; + const struct spng_plte_entry *plte = decode_plte->rgba; + +#if defined(SPNG_ARM) + if(fmt == SPNG_FMT_RGBA8) i = expand_palette_rgba8_neon(row, scanline, decode_plte->raw, width); + else if(fmt == SPNG_FMT_RGB8) + { + i = expand_palette_rgb8_neon(row, scanline, decode_plte->raw, width); + + for(; i < width; i++) + {/* In this case the LUT is 3 bytes packed */ + px = row + i * 3; + entry = scanline[i]; + px[0] = decode_plte->raw[entry * 3 + 0]; + px[1] = decode_plte->raw[entry * 3 + 1]; + px[2] = decode_plte->raw[entry * 3 + 2]; + } + return; + } +#endif + + if(fmt == SPNG_FMT_RGBA8) + { + for(; i < width; i++) + { + px = row + i * 4; + entry = scanline[i]; + px[0] = plte[entry].red; + px[1] = plte[entry].green; + px[2] = plte[entry].blue; + px[3] = plte[entry].alpha; + } + } + else if(fmt == SPNG_FMT_RGB8) + { + for(; i < width; i++) + { + px = row + i * 3; + entry = scanline[i]; + px[0] = plte[entry].red; + px[1] = plte[entry].green; + px[2] = plte[entry].blue; + } + } +} + +/* Unpack 1/2/4/8-bit samples to G8/GA8/GA16 or G16 -> GA16 */ +static void unpack_scanline(unsigned char *out, const unsigned char *scanline, uint32_t width, unsigned bit_depth, int fmt) +{ + struct spng__iter iter = spng__iter_init(bit_depth, scanline); + uint32_t i; + uint16_t sample, alpha = 65535; + + + if(fmt == SPNG_FMT_GA8) goto ga8; + else if(fmt == SPNG_FMT_GA16) goto ga16; + + /* 1/2/4-bit -> 8-bit */ + for(i=0; i < width; i++) out[i] = get_sample(&iter); + + return; + +ga8: + /* 1/2/4/8-bit -> GA8 */ + for(i=0; i < width; i++) + { + out[i*2] = get_sample(&iter); + out[i*2 + 1] = 255; + } + + return; + +ga16: + + /* 16 -> GA16 */ + if(bit_depth == 16) + { + for(i=0; i < width; i++) + { + memcpy(out + i * 4, scanline + i * 2, 2); + memcpy(out + i * 4 + 2, &alpha, 2); + } + return; + } + + /* 1/2/4/8-bit -> GA16 */ + for(i=0; i < width; i++) + { + sample = get_sample(&iter); + memcpy(out + i * 4, &sample, 2); + memcpy(out + i * 4 + 2, &alpha, 2); + } +} + +static int check_ihdr(const struct spng_ihdr *ihdr, uint32_t max_width, uint32_t max_height) +{ + if(ihdr->width > spng_u32max || !ihdr->width) return SPNG_EWIDTH; + if(ihdr->height > spng_u32max || !ihdr->height) return SPNG_EHEIGHT; + + if(ihdr->width > max_width) return SPNG_EUSER_WIDTH; + if(ihdr->height > max_height) return SPNG_EUSER_HEIGHT; + + switch(ihdr->color_type) + { + case SPNG_COLOR_TYPE_GRAYSCALE: + { + if( !(ihdr->bit_depth == 1 || ihdr->bit_depth == 2 || + ihdr->bit_depth == 4 || ihdr->bit_depth == 8 || + ihdr->bit_depth == 16) ) + return SPNG_EBIT_DEPTH; + + break; + } + case SPNG_COLOR_TYPE_TRUECOLOR: + case SPNG_COLOR_TYPE_GRAYSCALE_ALPHA: + case SPNG_COLOR_TYPE_TRUECOLOR_ALPHA: + { + if( !(ihdr->bit_depth == 8 || ihdr->bit_depth == 16) ) + return SPNG_EBIT_DEPTH; + + break; + } + case SPNG_COLOR_TYPE_INDEXED: + { + if( !(ihdr->bit_depth == 1 || ihdr->bit_depth == 2 || + ihdr->bit_depth == 4 || ihdr->bit_depth == 8) ) + return SPNG_EBIT_DEPTH; + + break; + } + default: return SPNG_ECOLOR_TYPE; + } + + if(ihdr->compression_method) return SPNG_ECOMPRESSION_METHOD; + if(ihdr->filter_method) return SPNG_EFILTER_METHOD; + + if(ihdr->interlace_method > 1) return SPNG_EINTERLACE_METHOD; + + return 0; +} + +static int check_plte(const struct spng_plte *plte, const struct spng_ihdr *ihdr) +{ + if(plte == NULL || ihdr == NULL) return 1; + + if(plte->n_entries == 0) return 1; + if(plte->n_entries > 256) return 1; + + if(ihdr->color_type == SPNG_COLOR_TYPE_INDEXED) + { + if(plte->n_entries > (1U << ihdr->bit_depth)) return 1; + } + + return 0; +} + +static int check_sbit(const struct spng_sbit *sbit, const struct spng_ihdr *ihdr) +{ + if(sbit == NULL || ihdr == NULL) return 1; + + if(ihdr->color_type == 0) + { + if(sbit->grayscale_bits == 0) return SPNG_ESBIT; + if(sbit->grayscale_bits > ihdr->bit_depth) return SPNG_ESBIT; + } + else if(ihdr->color_type == 2 || ihdr->color_type == 3) + { + if(sbit->red_bits == 0) return SPNG_ESBIT; + if(sbit->green_bits == 0) return SPNG_ESBIT; + if(sbit->blue_bits == 0) return SPNG_ESBIT; + + uint8_t bit_depth; + if(ihdr->color_type == 3) bit_depth = 8; + else bit_depth = ihdr->bit_depth; + + if(sbit->red_bits > bit_depth) return SPNG_ESBIT; + if(sbit->green_bits > bit_depth) return SPNG_ESBIT; + if(sbit->blue_bits > bit_depth) return SPNG_ESBIT; + } + else if(ihdr->color_type == 4) + { + if(sbit->grayscale_bits == 0) return SPNG_ESBIT; + if(sbit->alpha_bits == 0) return SPNG_ESBIT; + + if(sbit->grayscale_bits > ihdr->bit_depth) return SPNG_ESBIT; + if(sbit->alpha_bits > ihdr->bit_depth) return SPNG_ESBIT; + } + else if(ihdr->color_type == 6) + { + if(sbit->red_bits == 0) return SPNG_ESBIT; + if(sbit->green_bits == 0) return SPNG_ESBIT; + if(sbit->blue_bits == 0) return SPNG_ESBIT; + if(sbit->alpha_bits == 0) return SPNG_ESBIT; + + if(sbit->red_bits > ihdr->bit_depth) return SPNG_ESBIT; + if(sbit->green_bits > ihdr->bit_depth) return SPNG_ESBIT; + if(sbit->blue_bits > ihdr->bit_depth) return SPNG_ESBIT; + if(sbit->alpha_bits > ihdr->bit_depth) return SPNG_ESBIT; + } + + return 0; +} + +static int check_chrm_int(const struct spng_chrm_int *chrm_int) +{ + if(chrm_int == NULL) return 1; + + if(chrm_int->white_point_x > spng_u32max || + chrm_int->white_point_y > spng_u32max || + chrm_int->red_x > spng_u32max || + chrm_int->red_y > spng_u32max || + chrm_int->green_x > spng_u32max || + chrm_int->green_y > spng_u32max || + chrm_int->blue_x > spng_u32max || + chrm_int->blue_y > spng_u32max) return SPNG_ECHRM; + + return 0; +} + +static int check_phys(const struct spng_phys *phys) +{ + if(phys == NULL) return 1; + + if(phys->unit_specifier > 1) return SPNG_EPHYS; + + if(phys->ppu_x > spng_u32max) return SPNG_EPHYS; + if(phys->ppu_y > spng_u32max) return SPNG_EPHYS; + + return 0; +} + +static int check_time(const struct spng_time *time) +{ + if(time == NULL) return 1; + + if(time->month == 0 || time->month > 12) return 1; + if(time->day == 0 || time->day > 31) return 1; + if(time->hour > 23) return 1; + if(time->minute > 59) return 1; + if(time->second > 60) return 1; + + return 0; +} + +static int check_offs(const struct spng_offs *offs) +{ + if(offs == NULL) return 1; + + if(offs->unit_specifier > 1) return 1; + + return 0; +} + +static int check_exif(const struct spng_exif *exif) +{ + if(exif == NULL) return 1; + if(exif->data == NULL) return 1; + + if(exif->length < 4) return SPNG_ECHUNK_SIZE; + if(exif->length > spng_u32max) return SPNG_ECHUNK_STDLEN; + + const uint8_t exif_le[4] = { 73, 73, 42, 0 }; + const uint8_t exif_be[4] = { 77, 77, 0, 42 }; + + if(memcmp(exif->data, exif_le, 4) && memcmp(exif->data, exif_be, 4)) return 1; + + return 0; +} + +/* Validate PNG keyword */ +static int check_png_keyword(const char *str) +{ + if(str == NULL) return 1; + size_t len = strlen(str); + const char *end = str + len; + + if(!len) return 1; + if(len > 79) return 1; + if(str[0] == ' ') return 1; /* Leading space */ + if(end[-1] == ' ') return 1; /* Trailing space */ + if(strstr(str, " ") != NULL) return 1; /* Consecutive spaces */ + + uint8_t c; + while(str != end) + { + memcpy(&c, str, 1); + + if( (c >= 32 && c <= 126) || (c >= 161) ) str++; + else return 1; /* Invalid character */ + } + + return 0; +} + +/* Validate PNG text *str up to 'len' bytes */ +static int check_png_text(const char *str, size_t len) +{/* XXX: are consecutive newlines permitted? */ + if(str == NULL || len == 0) return 1; + + uint8_t c; + size_t i = 0; + while(i < len) + { + memcpy(&c, str + i, 1); + + if( (c >= 32 && c <= 126) || (c >= 161) || c == 10) i++; + else return 1; /* Invalid character */ + } + + return 0; +} + +/* Returns non-zero for standard chunks which are stored without allocating memory */ +static int is_small_chunk(uint8_t type[4]) +{ + if(!memcmp(type, type_plte, 4)) return 1; + else if(!memcmp(type, type_chrm, 4)) return 1; + else if(!memcmp(type, type_gama, 4)) return 1; + else if(!memcmp(type, type_sbit, 4)) return 1; + else if(!memcmp(type, type_srgb, 4)) return 1; + else if(!memcmp(type, type_bkgd, 4)) return 1; + else if(!memcmp(type, type_trns, 4)) return 1; + else if(!memcmp(type, type_hist, 4)) return 1; + else if(!memcmp(type, type_phys, 4)) return 1; + else if(!memcmp(type, type_time, 4)) return 1; + else if(!memcmp(type, type_offs, 4)) return 1; + else return 0; +} + +static int read_ihdr(spng_ctx *ctx) +{ + int ret; + struct spng_chunk *chunk = &ctx->current_chunk; + const unsigned char *data; + + chunk->offset = 8; + chunk->length = 13; + size_t sizeof_sig_ihdr = 29; + + ret = read_data(ctx, sizeof_sig_ihdr); + if(ret) return ret; + + data = ctx->data; + + if(memcmp(data, spng_signature, sizeof(spng_signature))) return SPNG_ESIGNATURE; + + chunk->length = read_u32(data + 8); + memcpy(&chunk->type, data + 12, 4); + + if(chunk->length != 13) return SPNG_EIHDR_SIZE; + if(memcmp(chunk->type, type_ihdr, 4)) return SPNG_ENOIHDR; + + ctx->cur_actual_crc = crc32(0, NULL, 0); + ctx->cur_actual_crc = crc32(ctx->cur_actual_crc, data + 12, 17); + + ctx->ihdr.width = read_u32(data + 16); + ctx->ihdr.height = read_u32(data + 20); + ctx->ihdr.bit_depth = data[24]; + ctx->ihdr.color_type = data[25]; + ctx->ihdr.compression_method = data[26]; + ctx->ihdr.filter_method = data[27]; + ctx->ihdr.interlace_method = data[28]; + + ret = check_ihdr(&ctx->ihdr, ctx->max_width, ctx->max_height); + if(ret) return ret; + + ctx->file.ihdr = 1; + ctx->stored.ihdr = 1; + + if(ctx->ihdr.bit_depth < 8) ctx->bytes_per_pixel = 1; + else ctx->bytes_per_pixel = num_channels(&ctx->ihdr) * (ctx->ihdr.bit_depth / 8); + + ret = calculate_subimages(ctx); + if(ret) return ret; + + return 0; +} + +static void splt_undo(spng_ctx *ctx) +{ + struct spng_splt *splt = &ctx->splt_list[ctx->n_splt - 1]; + + spng__free(ctx, splt->entries); + + decrease_cache_usage(ctx, sizeof(struct spng_splt)); + decrease_cache_usage(ctx, splt->n_entries * sizeof(struct spng_splt_entry)); + + splt->entries = NULL; + + ctx->n_splt--; +} + +static void text_undo(spng_ctx *ctx) +{ + struct spng_text2 *text = &ctx->text_list[ctx->n_text - 1]; + + spng__free(ctx, text->keyword); + if(text->compression_flag) spng__free(ctx, text->text); + + decrease_cache_usage(ctx, text->cache_usage); + decrease_cache_usage(ctx, sizeof(struct spng_text2)); + + text->keyword = NULL; + text->text = NULL; + + ctx->n_text--; +} + +static void chunk_undo(spng_ctx *ctx) +{ + struct spng_unknown_chunk *chunk = &ctx->chunk_list[ctx->n_chunks - 1]; + + spng__free(ctx, chunk->data); + + decrease_cache_usage(ctx, chunk->length); + decrease_cache_usage(ctx, sizeof(struct spng_unknown_chunk)); + + chunk->data = NULL; + + ctx->n_chunks--; +} + +static int read_non_idat_chunks(spng_ctx *ctx) +{ + int ret; + struct spng_chunk chunk; + const unsigned char *data; + + ctx->discard = 0; + ctx->undo = NULL; + ctx->prev_stored = ctx->stored; + + while( !(ret = read_header(ctx))) + { + if(ctx->discard) + { + if(ctx->undo) ctx->undo(ctx); + + ctx->stored = ctx->prev_stored; + } + + ctx->discard = 0; + ctx->undo = NULL; + + ctx->prev_stored = ctx->stored; + chunk = ctx->current_chunk; + + if(!memcmp(chunk.type, type_idat, 4)) + { + if(ctx->state < SPNG_STATE_FIRST_IDAT) + { + if(ctx->ihdr.color_type == 3 && !ctx->stored.plte) return SPNG_ENOPLTE; + + ctx->first_idat = chunk; + return 0; + } + + if(ctx->prev_was_idat) + { + /* Ignore extra IDAT's */ + ret = discard_chunk_bytes(ctx, chunk.length); + if(ret) return ret; + + continue; + } + else return SPNG_ECHUNK_POS; /* IDAT chunk not at the end of the IDAT sequence */ + } + + ctx->prev_was_idat = 0; + + if(is_small_chunk(chunk.type)) + { + /* None of the known chunks can be zero length */ + if(!chunk.length) return SPNG_ECHUNK_SIZE; + + /* The largest of these chunks is PLTE with 256 entries */ + ret = read_chunk_bytes(ctx, chunk.length > 768 ? 768 : chunk.length); + if(ret) return ret; + } + + data = ctx->data; + + if(is_critical_chunk(&chunk)) + { + if(!memcmp(chunk.type, type_plte, 4)) + { + if(ctx->file.trns || ctx->file.hist || ctx->file.bkgd) return SPNG_ECHUNK_POS; + if(chunk.length % 3 != 0) return SPNG_ECHUNK_SIZE; + + ctx->plte.n_entries = chunk.length / 3; + + if(check_plte(&ctx->plte, &ctx->ihdr)) return SPNG_ECHUNK_SIZE; /* XXX: EPLTE? */ + + size_t i; + for(i=0; i < ctx->plte.n_entries; i++) + { + ctx->plte.entries[i].red = data[i * 3]; + ctx->plte.entries[i].green = data[i * 3 + 1]; + ctx->plte.entries[i].blue = data[i * 3 + 2]; + } + + ctx->file.plte = 1; + ctx->stored.plte = 1; + } + else if(!memcmp(chunk.type, type_iend, 4)) + { + if(ctx->state == SPNG_STATE_AFTER_IDAT) + { + if(chunk.length) return SPNG_ECHUNK_SIZE; + + ret = read_and_check_crc(ctx); + if(ret == -SPNG_CRC_DISCARD) ret = 0; + + return ret; + } + else return SPNG_ECHUNK_POS; + } + else if(!memcmp(chunk.type, type_ihdr, 4)) return SPNG_ECHUNK_POS; + else return SPNG_ECHUNK_UNKNOWN_CRITICAL; + } + else if(!memcmp(chunk.type, type_chrm, 4)) /* Ancillary chunks */ + { + if(ctx->file.plte) return SPNG_ECHUNK_POS; + if(ctx->state == SPNG_STATE_AFTER_IDAT) return SPNG_ECHUNK_POS; + if(ctx->file.chrm) return SPNG_EDUP_CHRM; + + if(chunk.length != 32) return SPNG_ECHUNK_SIZE; + + ctx->chrm_int.white_point_x = read_u32(data); + ctx->chrm_int.white_point_y = read_u32(data + 4); + ctx->chrm_int.red_x = read_u32(data + 8); + ctx->chrm_int.red_y = read_u32(data + 12); + ctx->chrm_int.green_x = read_u32(data + 16); + ctx->chrm_int.green_y = read_u32(data + 20); + ctx->chrm_int.blue_x = read_u32(data + 24); + ctx->chrm_int.blue_y = read_u32(data + 28); + + if(check_chrm_int(&ctx->chrm_int)) return SPNG_ECHRM; + + ctx->file.chrm = 1; + ctx->stored.chrm = 1; + } + else if(!memcmp(chunk.type, type_gama, 4)) + { + if(ctx->file.plte) return SPNG_ECHUNK_POS; + if(ctx->state == SPNG_STATE_AFTER_IDAT) return SPNG_ECHUNK_POS; + if(ctx->file.gama) return SPNG_EDUP_GAMA; + + if(chunk.length != 4) return SPNG_ECHUNK_SIZE; + + ctx->gama = read_u32(data); + + if(!ctx->gama) return SPNG_EGAMA; + if(ctx->gama > spng_u32max) return SPNG_EGAMA; + + ctx->file.gama = 1; + ctx->stored.gama = 1; + } + else if(!memcmp(chunk.type, type_sbit, 4)) + { + if(ctx->file.plte) return SPNG_ECHUNK_POS; + if(ctx->state == SPNG_STATE_AFTER_IDAT) return SPNG_ECHUNK_POS; + if(ctx->file.sbit) return SPNG_EDUP_SBIT; + + if(ctx->ihdr.color_type == 0) + { + if(chunk.length != 1) return SPNG_ECHUNK_SIZE; + + ctx->sbit.grayscale_bits = data[0]; + } + else if(ctx->ihdr.color_type == 2 || ctx->ihdr.color_type == 3) + { + if(chunk.length != 3) return SPNG_ECHUNK_SIZE; + + ctx->sbit.red_bits = data[0]; + ctx->sbit.green_bits = data[1]; + ctx->sbit.blue_bits = data[2]; + } + else if(ctx->ihdr.color_type == 4) + { + if(chunk.length != 2) return SPNG_ECHUNK_SIZE; + + ctx->sbit.grayscale_bits = data[0]; + ctx->sbit.alpha_bits = data[1]; + } + else if(ctx->ihdr.color_type == 6) + { + if(chunk.length != 4) return SPNG_ECHUNK_SIZE; + + ctx->sbit.red_bits = data[0]; + ctx->sbit.green_bits = data[1]; + ctx->sbit.blue_bits = data[2]; + ctx->sbit.alpha_bits = data[3]; + } + + if(check_sbit(&ctx->sbit, &ctx->ihdr)) return SPNG_ESBIT; + + ctx->file.sbit = 1; + ctx->stored.sbit = 1; + } + else if(!memcmp(chunk.type, type_srgb, 4)) + { + if(ctx->file.plte) return SPNG_ECHUNK_POS; + if(ctx->state == SPNG_STATE_AFTER_IDAT) return SPNG_ECHUNK_POS; + if(ctx->file.srgb) return SPNG_EDUP_SRGB; + + if(chunk.length != 1) return SPNG_ECHUNK_SIZE; + + ctx->srgb_rendering_intent = data[0]; + + if(ctx->srgb_rendering_intent > 3) return SPNG_ESRGB; + + ctx->file.srgb = 1; + ctx->stored.srgb = 1; + } + else if(!memcmp(chunk.type, type_bkgd, 4)) + { + if(ctx->state == SPNG_STATE_AFTER_IDAT) return SPNG_ECHUNK_POS; + if(ctx->file.bkgd) return SPNG_EDUP_BKGD; + + if(ctx->ihdr.color_type == 0 || ctx->ihdr.color_type == 4) + { + if(chunk.length != 2) return SPNG_ECHUNK_SIZE; + + ctx->bkgd.gray = read_u16(data); + } + else if(ctx->ihdr.color_type == 2 || ctx->ihdr.color_type == 6) + { + if(chunk.length != 6) return SPNG_ECHUNK_SIZE; + + ctx->bkgd.red = read_u16(data); + ctx->bkgd.green = read_u16(data + 2); + ctx->bkgd.blue = read_u16(data + 4); + } + else if(ctx->ihdr.color_type == 3) + { + if(chunk.length != 1) return SPNG_ECHUNK_SIZE; + if(!ctx->file.plte) return SPNG_EBKGD_NO_PLTE; + + ctx->bkgd.plte_index = data[0]; + if(ctx->bkgd.plte_index >= ctx->plte.n_entries) return SPNG_EBKGD_PLTE_IDX; + } + + ctx->file.bkgd = 1; + ctx->stored.bkgd = 1; + } + else if(!memcmp(chunk.type, type_trns, 4)) + { + if(ctx->state == SPNG_STATE_AFTER_IDAT) return SPNG_ECHUNK_POS; + if(ctx->file.trns) return SPNG_EDUP_TRNS; + if(!chunk.length) return SPNG_ECHUNK_SIZE; + + if(ctx->ihdr.color_type == 0) + { + if(chunk.length != 2) return SPNG_ECHUNK_SIZE; + + ctx->trns.gray = read_u16(data); + } + else if(ctx->ihdr.color_type == 2) + { + if(chunk.length != 6) return SPNG_ECHUNK_SIZE; + + ctx->trns.red = read_u16(data); + ctx->trns.green = read_u16(data + 2); + ctx->trns.blue = read_u16(data + 4); + } + else if(ctx->ihdr.color_type == 3) + { + if(chunk.length > ctx->plte.n_entries) return SPNG_ECHUNK_SIZE; + if(!ctx->file.plte) return SPNG_ETRNS_NO_PLTE; + + memcpy(ctx->trns.type3_alpha, data, chunk.length); + ctx->trns.n_type3_entries = chunk.length; + } + + if(ctx->ihdr.color_type == 4 || ctx->ihdr.color_type == 6) return SPNG_ETRNS_COLOR_TYPE; + + ctx->file.trns = 1; + ctx->stored.trns = 1; + } + else if(!memcmp(chunk.type, type_hist, 4)) + { + if(!ctx->file.plte) return SPNG_EHIST_NO_PLTE; + if(ctx->state == SPNG_STATE_AFTER_IDAT) return SPNG_ECHUNK_POS; + if(ctx->file.hist) return SPNG_EDUP_HIST; + + if( (chunk.length / 2) != (ctx->plte.n_entries) ) return SPNG_ECHUNK_SIZE; + + size_t k; + for(k=0; k < (chunk.length / 2); k++) + { + ctx->hist.frequency[k] = read_u16(data + k*2); + } + + ctx->file.hist = 1; + ctx->stored.hist = 1; + } + else if(!memcmp(chunk.type, type_phys, 4)) + { + if(ctx->state == SPNG_STATE_AFTER_IDAT) return SPNG_ECHUNK_POS; + if(ctx->file.phys) return SPNG_EDUP_PHYS; + + if(chunk.length != 9) return SPNG_ECHUNK_SIZE; + + ctx->phys.ppu_x = read_u32(data); + ctx->phys.ppu_y = read_u32(data + 4); + ctx->phys.unit_specifier = data[8]; + + if(check_phys(&ctx->phys)) return SPNG_EPHYS; + + ctx->file.phys = 1; + ctx->stored.phys = 1; + } + else if(!memcmp(chunk.type, type_time, 4)) + { + if(ctx->file.time) return SPNG_EDUP_TIME; + + if(chunk.length != 7) return SPNG_ECHUNK_SIZE; + + struct spng_time time; + + time.year = read_u16(data); + time.month = data[2]; + time.day = data[3]; + time.hour = data[4]; + time.minute = data[5]; + time.second = data[6]; + + if(check_time(&time)) return SPNG_ETIME; + + ctx->file.time = 1; + + if(!ctx->user.time) ctx->time = time; + + ctx->stored.time = 1; + } + else if(!memcmp(chunk.type, type_offs, 4)) + { + if(ctx->state == SPNG_STATE_AFTER_IDAT) return SPNG_ECHUNK_POS; + if(ctx->file.offs) return SPNG_EDUP_OFFS; + + if(chunk.length != 9) return SPNG_ECHUNK_SIZE; + + ctx->offs.x = read_s32(data); + ctx->offs.y = read_s32(data + 4); + ctx->offs.unit_specifier = data[8]; + + if(check_offs(&ctx->offs)) return SPNG_EOFFS; + + ctx->file.offs = 1; + ctx->stored.offs = 1; + } + else /* Arbitrary-length chunk */ + { + + if(!memcmp(chunk.type, type_exif, 4)) + { + if(ctx->file.exif) return SPNG_EDUP_EXIF; + if(!chunk.length) return SPNG_EEXIF; + + ctx->file.exif = 1; + + if(ctx->user.exif) goto discard; + + if(increase_cache_usage(ctx, chunk.length, 1)) return SPNG_ECHUNK_LIMITS; + + struct spng_exif exif; + + exif.length = chunk.length; + + exif.data = spng__malloc(ctx, chunk.length); + if(exif.data == NULL) return SPNG_EMEM; + + ret = read_chunk_bytes2(ctx, exif.data, chunk.length); + if(ret) + { + spng__free(ctx, exif.data); + return ret; + } + + if(check_exif(&exif)) + { + spng__free(ctx, exif.data); + return SPNG_EEXIF; + } + + ctx->exif = exif; + + ctx->stored.exif = 1; + } + else if(!memcmp(chunk.type, type_iccp, 4)) + {/* TODO: add test file with color profile */ + if(ctx->file.plte) return SPNG_ECHUNK_POS; + if(ctx->state == SPNG_STATE_AFTER_IDAT) return SPNG_ECHUNK_POS; + if(ctx->file.iccp) return SPNG_EDUP_ICCP; + if(!chunk.length) return SPNG_ECHUNK_SIZE; + + ctx->file.iccp = 1; + + uint32_t peek_bytes = 81 > chunk.length ? chunk.length : 81; + + ret = read_chunk_bytes(ctx, peek_bytes); + if(ret) return ret; + + unsigned char *keyword_nul = memchr(ctx->data, '\0', peek_bytes); + if(keyword_nul == NULL) return SPNG_EICCP_NAME; + + uint32_t keyword_len = keyword_nul - ctx->data; + + if(keyword_len > 79) return SPNG_EICCP_NAME; + + memcpy(ctx->iccp.profile_name, ctx->data, keyword_len); + + if(check_png_keyword(ctx->iccp.profile_name)) return SPNG_EICCP_NAME; + + if(chunk.length < (keyword_len + 2)) return SPNG_ECHUNK_SIZE; + + if(ctx->data[keyword_len + 1] != 0) return SPNG_EICCP_COMPRESSION_METHOD; + + ret = spng__inflate_stream(ctx, &ctx->iccp.profile, &ctx->iccp.profile_len, 0, ctx->data + keyword_len + 2, peek_bytes - (keyword_len + 2)); + + if(ret) return ret; + + ctx->stored.iccp = 1; + } + else if(!memcmp(chunk.type, type_text, 4) || + !memcmp(chunk.type, type_ztxt, 4) || + !memcmp(chunk.type, type_itxt, 4)) + { + if(!chunk.length) return SPNG_ECHUNK_SIZE; + + ctx->file.text = 1; + + if(ctx->user.text) goto discard; + + if(increase_cache_usage(ctx, sizeof(struct spng_text2), 1)) return SPNG_ECHUNK_LIMITS; + + ctx->n_text++; + if(ctx->n_text < 1) return SPNG_EOVERFLOW; + if(sizeof(struct spng_text2) > SIZE_MAX / ctx->n_text) return SPNG_EOVERFLOW; + + void *buf = spng__realloc(ctx, ctx->text_list, ctx->n_text * sizeof(struct spng_text2)); + if(buf == NULL) return SPNG_EMEM; + ctx->text_list = buf; + + struct spng_text2 *text = &ctx->text_list[ctx->n_text - 1]; + memset(text, 0, sizeof(struct spng_text2)); + + ctx->undo = text_undo; + + uint32_t text_offset = 0, language_tag_offset = 0, translated_keyword_offset = 0; + uint32_t peek_bytes = 256; /* enough for 3 80-byte keywords and some text bytes */ + uint32_t keyword_len; + + if(peek_bytes > chunk.length) peek_bytes = chunk.length; + + ret = read_chunk_bytes(ctx, peek_bytes); + if(ret) return ret; + + data = ctx->data; + + const unsigned char *zlib_stream = NULL; + const unsigned char *peek_end = data + peek_bytes; + const unsigned char *keyword_nul = memchr(data, 0, chunk.length > 80 ? 80 : chunk.length); + + if(keyword_nul == NULL) return SPNG_ETEXT_KEYWORD; + + keyword_len = keyword_nul - data; + + if(!memcmp(chunk.type, type_text, 4)) + { + text->type = SPNG_TEXT; + + text->text_length = chunk.length - keyword_len - 1; + + text_offset = keyword_len; + + /* increment past nul if there is a text field */ + if(text->text_length) text_offset++; + } + else if(!memcmp(chunk.type, type_ztxt, 4)) + { + text->type = SPNG_ZTXT; + + if((peek_bytes - keyword_len) <= 2) return SPNG_EZTXT; + + if(keyword_nul[1]) return SPNG_EZTXT_COMPRESSION_METHOD; + + text->compression_flag = 1; + + text_offset = keyword_len + 2; + } + else if(!memcmp(chunk.type, type_itxt, 4)) + { + text->type = SPNG_ITXT; + + /* at least two 1-byte fields, two >=0 length strings, and one byte of (compressed) text */ + if((peek_bytes - keyword_len) < 5) return SPNG_EITXT; + + text->compression_flag = keyword_nul[1]; + + if(text->compression_flag > 1) return SPNG_EITXT_COMPRESSION_FLAG; + + if(keyword_nul[2]) return SPNG_EITXT_COMPRESSION_METHOD; + + language_tag_offset = keyword_len + 3; + + const unsigned char *term; + term = memchr(data + language_tag_offset, 0, peek_bytes - language_tag_offset); + if(term == NULL) return SPNG_EITXT_LANG_TAG; + + if((peek_end - term) < 2) return SPNG_EITXT; + + translated_keyword_offset = term - data + 1; + + zlib_stream = memchr(data + translated_keyword_offset, 0, peek_bytes - translated_keyword_offset); + if(zlib_stream == NULL) return SPNG_EITXT; + if(zlib_stream == peek_end) return SPNG_EITXT; + + text_offset = zlib_stream - data + 1; + text->text_length = chunk.length - text_offset; + } + else return SPNG_EINTERNAL; + + + if(text->compression_flag) + { + /* cache usage = peek_bytes + decompressed text size + nul */ + if(increase_cache_usage(ctx, peek_bytes, 0)) return SPNG_ECHUNK_LIMITS; + + text->keyword = spng__calloc(ctx, 1, peek_bytes); + if(text->keyword == NULL) return SPNG_EMEM; + + memcpy(text->keyword, data, peek_bytes); + + zlib_stream = ctx->data + text_offset; + + ret = spng__inflate_stream(ctx, &text->text, &text->text_length, 1, zlib_stream, peek_bytes - text_offset); + + if(ret) return ret; + + text->text[text->text_length - 1] = '\0'; + text->cache_usage = text->text_length + peek_bytes; + } + else + { + if(increase_cache_usage(ctx, chunk.length + 1, 0)) return SPNG_ECHUNK_LIMITS; + + text->keyword = spng__malloc(ctx, chunk.length + 1); + if(text->keyword == NULL) return SPNG_EMEM; + + memcpy(text->keyword, data, peek_bytes); + + if(chunk.length > peek_bytes) + { + ret = read_chunk_bytes2(ctx, text->keyword + peek_bytes, chunk.length - peek_bytes); + if(ret) return ret; + } + + text->text = text->keyword + text_offset; + + text->text_length = chunk.length - text_offset; + + text->text[text->text_length] = '\0'; + text->cache_usage = chunk.length + 1; + } + + if(check_png_keyword(text->keyword)) return SPNG_ETEXT_KEYWORD; + + text->text_length = strlen(text->text); + + if(text->type != SPNG_ITXT) + { + language_tag_offset = keyword_len; + translated_keyword_offset = keyword_len; + + if(ctx->strict && check_png_text(text->text, text->text_length)) + { + if(text->type == SPNG_ZTXT) return SPNG_EZTXT; + else return SPNG_ETEXT; + } + } + + text->language_tag = text->keyword + language_tag_offset; + text->translated_keyword = text->keyword + translated_keyword_offset; + + ctx->stored.text = 1; + } + else if(!memcmp(chunk.type, type_splt, 4)) + { + if(ctx->state == SPNG_STATE_AFTER_IDAT) return SPNG_ECHUNK_POS; + if(ctx->user.splt) goto discard; /* XXX: could check profile names for uniqueness */ + if(!chunk.length) return SPNG_ECHUNK_SIZE; + + ctx->file.splt = 1; + + /* chunk.length + sizeof(struct spng_splt) + splt->n_entries * sizeof(struct spng_splt_entry) */ + if(increase_cache_usage(ctx, chunk.length + sizeof(struct spng_splt), 1)) return SPNG_ECHUNK_LIMITS; + + ctx->n_splt++; + if(ctx->n_splt < 1) return SPNG_EOVERFLOW; + if(sizeof(struct spng_splt) > SIZE_MAX / ctx->n_splt) return SPNG_EOVERFLOW; + + void *buf = spng__realloc(ctx, ctx->splt_list, ctx->n_splt * sizeof(struct spng_splt)); + if(buf == NULL) return SPNG_EMEM; + ctx->splt_list = buf; + + struct spng_splt *splt = &ctx->splt_list[ctx->n_splt - 1]; + + memset(splt, 0, sizeof(struct spng_splt)); + + ctx->undo = splt_undo; + + void *t = spng__malloc(ctx, chunk.length); + if(t == NULL) return SPNG_EMEM; + + splt->entries = t; /* simplifies error handling */ + data = t; + + ret = read_chunk_bytes2(ctx, t, chunk.length); + if(ret) return ret; + + uint32_t keyword_len = chunk.length < 80 ? chunk.length : 80; + + const unsigned char *keyword_nul = memchr(data, 0, keyword_len); + if(keyword_nul == NULL) return SPNG_ESPLT_NAME; + + keyword_len = keyword_nul - data; + + memcpy(splt->name, data, keyword_len); + + if(check_png_keyword(splt->name)) return SPNG_ESPLT_NAME; + + uint32_t j; + for(j=0; j < (ctx->n_splt - 1); j++) + { + if(!strcmp(ctx->splt_list[j].name, splt->name)) return SPNG_ESPLT_DUP_NAME; + } + + if( (chunk.length - keyword_len) <= 2) return SPNG_ECHUNK_SIZE; + + splt->sample_depth = data[keyword_len + 1]; + + uint32_t entries_len = chunk.length - keyword_len - 2; + if(!entries_len) return SPNG_ECHUNK_SIZE; + + if(splt->sample_depth == 16) + { + if(entries_len % 10 != 0) return SPNG_ECHUNK_SIZE; + splt->n_entries = entries_len / 10; + } + else if(splt->sample_depth == 8) + { + if(entries_len % 6 != 0) return SPNG_ECHUNK_SIZE; + splt->n_entries = entries_len / 6; + } + else return SPNG_ESPLT_DEPTH; + + if(!splt->n_entries) return SPNG_ECHUNK_SIZE; + + size_t list_size = splt->n_entries; + + if(list_size > SIZE_MAX / sizeof(struct spng_splt_entry)) return SPNG_EOVERFLOW; + + list_size *= sizeof(struct spng_splt_entry); + + if(increase_cache_usage(ctx, list_size, 0)) return SPNG_ECHUNK_LIMITS; + + splt->entries = spng__malloc(ctx, list_size); + if(splt->entries == NULL) + { + spng__free(ctx, t); + return SPNG_EMEM; + } + + data = (unsigned char*)t + keyword_len + 2; + + uint32_t k; + if(splt->sample_depth == 16) + { + for(k=0; k < splt->n_entries; k++) + { + splt->entries[k].red = read_u16(data + k * 10); + splt->entries[k].green = read_u16(data + k * 10 + 2); + splt->entries[k].blue = read_u16(data + k * 10 + 4); + splt->entries[k].alpha = read_u16(data + k * 10 + 6); + splt->entries[k].frequency = read_u16(data + k * 10 + 8); + } + } + else if(splt->sample_depth == 8) + { + for(k=0; k < splt->n_entries; k++) + { + splt->entries[k].red = data[k * 6]; + splt->entries[k].green = data[k * 6 + 1]; + splt->entries[k].blue = data[k * 6 + 2]; + splt->entries[k].alpha = data[k * 6 + 3]; + splt->entries[k].frequency = read_u16(data + k * 6 + 4); + } + } + + spng__free(ctx, t); + decrease_cache_usage(ctx, chunk.length); + + ctx->stored.splt = 1; + } + else /* Unknown chunk */ + { + ctx->file.unknown = 1; + + if(!ctx->keep_unknown) goto discard; + if(ctx->user.unknown) goto discard; + + if(increase_cache_usage(ctx, chunk.length + sizeof(struct spng_unknown_chunk), 1)) return SPNG_ECHUNK_LIMITS; + + ctx->n_chunks++; + if(ctx->n_chunks < 1) return SPNG_EOVERFLOW; + if(sizeof(struct spng_unknown_chunk) > SIZE_MAX / ctx->n_chunks) return SPNG_EOVERFLOW; + + void *buf = spng__realloc(ctx, ctx->chunk_list, ctx->n_chunks * sizeof(struct spng_unknown_chunk)); + if(buf == NULL) return SPNG_EMEM; + ctx->chunk_list = buf; + + struct spng_unknown_chunk *chunkp = &ctx->chunk_list[ctx->n_chunks - 1]; + + memset(chunkp, 0, sizeof(struct spng_unknown_chunk)); + + ctx->undo = chunk_undo; + + memcpy(chunkp->type, chunk.type, 4); + + if(ctx->state < SPNG_STATE_FIRST_IDAT) + { + if(ctx->file.plte) chunkp->location = SPNG_AFTER_PLTE; + else chunkp->location = SPNG_AFTER_IHDR; + } + else if(ctx->state >= SPNG_STATE_AFTER_IDAT) chunkp->location = SPNG_AFTER_IDAT; + + if(chunk.length > 0) + { + void *t = spng__malloc(ctx, chunk.length); + if(t == NULL) return SPNG_EMEM; + + ret = read_chunk_bytes2(ctx, t, chunk.length); + if(ret) + { + spng__free(ctx, t); + return ret; + } + + chunkp->length = chunk.length; + chunkp->data = t; + } + + ctx->stored.unknown = 1; + } + +discard: + ret = discard_chunk_bytes(ctx, ctx->cur_chunk_bytes_left); + if(ret) return ret; + } + + } + + return ret; +} + +/* Read chunks before or after the IDAT chunks depending on state */ +static int read_chunks(spng_ctx *ctx, int only_ihdr) +{ + if(ctx == NULL) return SPNG_EINTERNAL; + if(!ctx->state) return SPNG_EBADSTATE; + if(ctx->data == NULL) + { + if(ctx->encode_only) return 0; + else return SPNG_EINTERNAL; + } + + int ret = 0; + + if(ctx->state == SPNG_STATE_INPUT) + { + ret = read_ihdr(ctx); + + if(ret) return decode_err(ctx, ret); + + ctx->state = SPNG_STATE_IHDR; + } + + if(only_ihdr) return 0; + + if(ctx->state == SPNG_STATE_EOI) + { + ctx->state = SPNG_STATE_AFTER_IDAT; + ctx->prev_was_idat = 1; + } + + while(ctx->state < SPNG_STATE_FIRST_IDAT || ctx->state == SPNG_STATE_AFTER_IDAT) + { + ret = read_non_idat_chunks(ctx); + + if(!ret) + { + if(ctx->state < SPNG_STATE_FIRST_IDAT) ctx->state = SPNG_STATE_FIRST_IDAT; + else if(ctx->state == SPNG_STATE_AFTER_IDAT) ctx->state = SPNG_STATE_IEND; + } + else + { + switch(ret) + { + case SPNG_ECHUNK_POS: + case SPNG_ECHUNK_SIZE: /* size != expected size, SPNG_ECHUNK_STDLEN = invalid size */ + case SPNG_EDUP_PLTE: + case SPNG_EDUP_CHRM: + case SPNG_EDUP_GAMA: + case SPNG_EDUP_ICCP: + case SPNG_EDUP_SBIT: + case SPNG_EDUP_SRGB: + case SPNG_EDUP_BKGD: + case SPNG_EDUP_HIST: + case SPNG_EDUP_TRNS: + case SPNG_EDUP_PHYS: + case SPNG_EDUP_TIME: + case SPNG_EDUP_OFFS: + case SPNG_EDUP_EXIF: + case SPNG_ECHRM: + case SPNG_ETRNS_COLOR_TYPE: + case SPNG_ETRNS_NO_PLTE: + case SPNG_EGAMA: + case SPNG_EICCP_NAME: + case SPNG_EICCP_COMPRESSION_METHOD: + case SPNG_ESBIT: + case SPNG_ESRGB: + case SPNG_ETEXT: + case SPNG_ETEXT_KEYWORD: + case SPNG_EZTXT: + case SPNG_EZTXT_COMPRESSION_METHOD: + case SPNG_EITXT: + case SPNG_EITXT_COMPRESSION_FLAG: + case SPNG_EITXT_COMPRESSION_METHOD: + case SPNG_EITXT_LANG_TAG: + case SPNG_EITXT_TRANSLATED_KEY: + case SPNG_EBKGD_NO_PLTE: + case SPNG_EBKGD_PLTE_IDX: + case SPNG_EHIST_NO_PLTE: + case SPNG_EPHYS: + case SPNG_ESPLT_NAME: + case SPNG_ESPLT_DUP_NAME: + case SPNG_ESPLT_DEPTH: + case SPNG_ETIME: + case SPNG_EOFFS: + case SPNG_EEXIF: + case SPNG_EZLIB: + { + if(!ctx->strict && !is_critical_chunk(&ctx->current_chunk)) + { + ret = discard_chunk_bytes(ctx, ctx->cur_chunk_bytes_left); + if(ret) return decode_err(ctx, ret); + + if(ctx->undo) ctx->undo(ctx); + + ctx->stored = ctx->prev_stored; + + ctx->discard = 0; + ctx->undo = NULL; + + continue; + } + else return decode_err(ctx, ret); + + break; + } + default: return decode_err(ctx, ret); + } + } + } + + return ret; +} + +static int read_scanline(spng_ctx *ctx) +{ + int ret, pass = ctx->row_info.pass; + struct spng_row_info *ri = &ctx->row_info; + const struct spng_subimage *sub = ctx->subimage; + size_t scanline_width = sub[pass].scanline_width; + uint32_t scanline_idx = ri->scanline_idx; + + uint8_t next_filter = 0; + + if(scanline_idx == (sub[pass].height - 1) && ri->pass == ctx->last_pass) + { + ret = read_scanline_bytes(ctx, ctx->scanline, scanline_width - 1); + } + else + { + ret = read_scanline_bytes(ctx, ctx->scanline, scanline_width); + if(ret) return ret; + + next_filter = ctx->scanline[scanline_width - 1]; + if(next_filter > 4) ret = SPNG_EFILTER; + } + + if(ret) return ret; + + if(!scanline_idx && ri->filter > 1) + { + /* prev_scanline is all zeros for the first scanline */ + memset(ctx->prev_scanline, 0, scanline_width); + } + + if(ctx->ihdr.bit_depth == 16 && ctx->fmt != SPNG_FMT_RAW) u16_row_to_host(ctx->scanline, scanline_width - 1); + + ret = defilter_scanline(ctx->prev_scanline, ctx->scanline, scanline_width, ctx->bytes_per_pixel, ri->filter); + if(ret) return ret; + + ri->filter = next_filter; + + return 0; +} + +static int update_row_info(spng_ctx *ctx) +{ + int interlacing = ctx->ihdr.interlace_method; + struct spng_row_info *ri = &ctx->row_info; + const struct spng_subimage *sub = ctx->subimage; + + if(ri->scanline_idx == (sub[ri->pass].height - 1)) /* Last scanline */ + { + if(ri->pass == ctx->last_pass) + { + ctx->state = SPNG_STATE_EOI; + + return SPNG_EOI; + } + + ri->scanline_idx = 0; + ri->pass++; + + /* Skip empty passes */ + while( (!sub[ri->pass].width || !sub[ri->pass].height) && (ri->pass < ctx->last_pass)) ri->pass++; + } + else + { + ri->row_num++; + ri->scanline_idx++; + } + + if(interlacing) ri->row_num = adam7_y_start[ri->pass] + ri->scanline_idx * adam7_y_delta[ri->pass]; + + return 0; +} + +int spng_decode_scanline(spng_ctx *ctx, void *out, size_t len) +{ + if(ctx == NULL || out == NULL) return 1; + + if(ctx->state >= SPNG_STATE_EOI) return SPNG_EOI; + + struct decode_flags f = ctx->decode_flags; + + struct spng_row_info *ri = &ctx->row_info; + const struct spng_subimage *sub = ctx->subimage; + + const struct spng_ihdr *ihdr = &ctx->ihdr; + const uint16_t *gamma_lut = ctx->gamma_lut; + unsigned char *trns_px = ctx->trns_px; + const struct spng_sbit *sb = &ctx->decode_sb; + const struct spng_plte_entry *plte = ctx->decode_plte.rgba; + struct spng__iter iter = spng__iter_init(ihdr->bit_depth, ctx->scanline); + + const unsigned char *scanline; + + const int pass = ri->pass; + const int fmt = ctx->fmt; + const size_t scanline_width = sub[pass].scanline_width; + const uint32_t width = sub[pass].width; + uint32_t k; + uint8_t r_8, g_8, b_8, a_8, gray_8; + uint16_t r_16, g_16, b_16, a_16, gray_16; + r_8=0; g_8=0; b_8=0; a_8=0; gray_8=0; + r_16=0; g_16=0; b_16=0; a_16=0; gray_16=0; + size_t pixel_size = 4; /* SPNG_FMT_RGBA8 */ + size_t pixel_offset = 0; + unsigned char *pixel; + unsigned processing_depth = ihdr->bit_depth; + + if(f.indexed) processing_depth = 8; + + if(fmt == SPNG_FMT_RGBA16) pixel_size = 8; + else if(fmt == SPNG_FMT_RGB8) pixel_size = 3; + + if(len < sub[pass].out_width) return SPNG_EBUFSIZ; + + int ret = read_scanline(ctx); + + if(ret) return decode_err(ctx, ret); + + scanline = ctx->scanline; + + for(k=0; k < width; k++) + { + pixel = (unsigned char*)out + pixel_offset; + pixel_offset += pixel_size; + + if(f.same_layout) + { + if(f.zerocopy) break; + + memcpy(out, scanline, scanline_width - 1); + break; + } + + if(f.unpack) + { + unpack_scanline(out, scanline, width, ihdr->bit_depth, fmt); + break; + } + + if(ihdr->color_type == SPNG_COLOR_TYPE_TRUECOLOR) + { + if(ihdr->bit_depth == 16) + { + memcpy(&r_16, scanline + (k * 6), 2); + memcpy(&g_16, scanline + (k * 6) + 2, 2); + memcpy(&b_16, scanline + (k * 6) + 4, 2); + + a_16 = 65535; + } + else /* == 8 */ + { + if(fmt == SPNG_FMT_RGBA8) + { + rgb8_row_to_rgba8(scanline, out, width); + break; + } + + r_8 = scanline[k * 3]; + g_8 = scanline[k * 3 + 1]; + b_8 = scanline[k * 3 + 2]; + + a_8 = 255; + } + } + else if(ihdr->color_type == SPNG_COLOR_TYPE_INDEXED) + { + uint8_t entry = 0; + + if(ihdr->bit_depth == 8) + { + if(fmt & (SPNG_FMT_RGBA8 | SPNG_FMT_RGB8)) + { + expand_row(out, scanline, &ctx->decode_plte, width, fmt); + break; + } + + entry = scanline[k]; + } + else /* < 8 */ + { + entry = get_sample(&iter); + } + + if(fmt & (SPNG_FMT_RGBA8 | SPNG_FMT_RGB8)) + { + pixel[0] = plte[entry].red; + pixel[1] = plte[entry].green; + pixel[2] = plte[entry].blue; + if(fmt == SPNG_FMT_RGBA8) pixel[3] = plte[entry].alpha; + + continue; + } + else /* RGBA16 */ + { + r_16 = plte[entry].red; + g_16 = plte[entry].green; + b_16 = plte[entry].blue; + a_16 = plte[entry].alpha; + + r_16 = (r_16 << 8) | r_16; + g_16 = (g_16 << 8) | g_16; + b_16 = (b_16 << 8) | b_16; + a_16 = (a_16 << 8) | a_16; + + memcpy(pixel, &r_16, 2); + memcpy(pixel + 2, &g_16, 2); + memcpy(pixel + 4, &b_16, 2); + memcpy(pixel + 6, &a_16, 2); + + continue; + } + } + else if(ihdr->color_type == SPNG_COLOR_TYPE_TRUECOLOR_ALPHA) + { + if(ihdr->bit_depth == 16) + { + memcpy(&r_16, scanline + (k * 8), 2); + memcpy(&g_16, scanline + (k * 8) + 2, 2); + memcpy(&b_16, scanline + (k * 8) + 4, 2); + memcpy(&a_16, scanline + (k * 8) + 6, 2); + } + else /* == 8 */ + { + r_8 = scanline[k * 4]; + g_8 = scanline[k * 4 + 1]; + b_8 = scanline[k * 4 + 2]; + a_8 = scanline[k * 4 + 3]; + } + } + else if(ihdr->color_type == SPNG_COLOR_TYPE_GRAYSCALE) + { + if(ihdr->bit_depth == 16) + { + memcpy(&gray_16, scanline + k * 2, 2); + + if(f.apply_trns && ctx->trns.gray == gray_16) a_16 = 0; + else a_16 = 65535; + + r_16 = gray_16; + g_16 = gray_16; + b_16 = gray_16; + } + else /* <= 8 */ + { + gray_8 = get_sample(&iter); + + if(f.apply_trns && ctx->trns.gray == gray_8) a_8 = 0; + else a_8 = 255; + + r_8 = gray_8; g_8 = gray_8; b_8 = gray_8; + } + } + else if(ihdr->color_type == SPNG_COLOR_TYPE_GRAYSCALE_ALPHA) + { + if(ihdr->bit_depth == 16) + { + memcpy(&gray_16, scanline + (k * 4), 2); + memcpy(&a_16, scanline + (k * 4) + 2, 2); + + r_16 = gray_16; + g_16 = gray_16; + b_16 = gray_16; + } + else /* == 8 */ + { + gray_8 = scanline[k * 2]; + a_8 = scanline[k * 2 + 1]; + + r_8 = gray_8; + g_8 = gray_8; + b_8 = gray_8; + } + } + + + if(fmt & (SPNG_FMT_RGBA8 | SPNG_FMT_RGB8)) + { + if(ihdr->bit_depth == 16) + { + r_8 = r_16 >> 8; + g_8 = g_16 >> 8; + b_8 = b_16 >> 8; + a_8 = a_16 >> 8; + } + + pixel[0] = r_8; + pixel[1] = g_8; + pixel[2] = b_8; + + if(fmt == SPNG_FMT_RGBA8) pixel[3] = a_8; + } + else if(fmt == SPNG_FMT_RGBA16) + { + if(ihdr->bit_depth != 16) + { + r_16 = r_8; + g_16 = g_8; + b_16 = b_8; + a_16 = a_8; + } + + memcpy(pixel, &r_16, 2); + memcpy(pixel + 2, &g_16, 2); + memcpy(pixel + 4, &b_16, 2); + memcpy(pixel + 6, &a_16, 2); + } + }/* for(k=0; k < width; k++) */ + + if(f.apply_trns) trns_row(out, scanline, trns_px, ctx->bytes_per_pixel, &ctx->ihdr, width, fmt); + + if(f.do_scaling) scale_row(out, width, fmt, processing_depth, sb); + + if(f.apply_gamma) gamma_correct_row(out, width, fmt, gamma_lut); + + /* The previous scanline is always defiltered */ + void *t = ctx->prev_scanline; + ctx->prev_scanline = ctx->scanline; + ctx->scanline = t; + + ret = update_row_info(ctx); + + if(ret == SPNG_EOI) + { + if(ctx->cur_chunk_bytes_left) /* zlib stream ended before an IDAT chunk boundary */ + {/* Discard the rest of the chunk */ + int error = discard_chunk_bytes(ctx, ctx->cur_chunk_bytes_left); + if(error) return decode_err(ctx, error); + } + + ctx->last_idat = ctx->current_chunk; + } + + return ret; +} + +int spng_decode_row(spng_ctx *ctx, void *out, size_t len) +{ + if(ctx == NULL || out == NULL) return 1; + if(ctx->state >= SPNG_STATE_EOI) return SPNG_EOI; + if(len < ctx->image_width) return SPNG_EBUFSIZ; + + const struct spng_ihdr *ihdr = &ctx->ihdr; + int ret, pass = ctx->row_info.pass; + unsigned char *outptr = out; + + if(!ihdr->interlace_method || pass == 6) return spng_decode_scanline(ctx, out, len); + + ret = spng_decode_scanline(ctx, ctx->row, ctx->image_width); + if(ret && ret != SPNG_EOI) return ret; + + uint32_t k; + unsigned pixel_size = 4; /* RGBA8 */ + if(ctx->fmt == SPNG_FMT_RGBA16) pixel_size = 8; + else if(ctx->fmt == SPNG_FMT_RGB8) pixel_size = 3; + else if(ctx->fmt == SPNG_FMT_G8) pixel_size = 1; + else if(ctx->fmt == SPNG_FMT_GA8) pixel_size = 2; + else if(ctx->fmt & (SPNG_FMT_PNG | SPNG_FMT_RAW)) + { + if(ihdr->bit_depth < 8) + { + struct spng__iter iter = spng__iter_init(ihdr->bit_depth, ctx->row); + const uint8_t samples_per_byte = 8 / ihdr->bit_depth; + uint8_t sample; + + for(k=0; k < ctx->subimage[pass].width; k++) + { + sample = get_sample(&iter); + + size_t ioffset = adam7_x_start[pass] + k * adam7_x_delta[pass]; + + sample = sample << (iter.initial_shift - ioffset * ihdr->bit_depth % 8); + + ioffset /= samples_per_byte; + + outptr[ioffset] |= sample; + } + + return 0; + } + else pixel_size = ctx->bytes_per_pixel; + } + + for(k=0; k < ctx->subimage[pass].width; k++) + { + size_t ioffset = (adam7_x_start[pass] + (size_t) k * adam7_x_delta[pass]) * pixel_size; + + memcpy(outptr + ioffset, ctx->row + k * pixel_size, pixel_size); + } + + return 0; +} + +int spng_decode_chunks(spng_ctx *ctx) +{ + if(ctx == NULL) return 1; + if(ctx->encode_only) return SPNG_ECTXTYPE; + if(ctx->state < SPNG_STATE_INPUT) return SPNG_ENOSRC; + if(ctx->state == SPNG_STATE_IEND) return 0; + + return read_chunks(ctx, 0); +} + +int spng_decode_image(spng_ctx *ctx, void *out, size_t len, int fmt, int flags) +{ + if(ctx == NULL) return 1; + if(ctx->encode_only) return SPNG_ECTXTYPE; + if(ctx->state >= SPNG_STATE_EOI) return SPNG_EOI; + + const struct spng_ihdr *ihdr = &ctx->ihdr; + + int ret = read_chunks(ctx, 0); + if(ret) return decode_err(ctx, ret); + + ret = check_decode_fmt(ihdr, fmt); + if(ret) return ret; + + ret = calculate_image_width(ihdr, fmt, &ctx->image_width); + if(ret) return decode_err(ctx, ret); + + if(ctx->image_width > SIZE_MAX / ihdr->height) ctx->image_size = 0; /* overflow */ + else ctx->image_size = ctx->image_width * ihdr->height; + + if( !(flags & SPNG_DECODE_PROGRESSIVE) ) + { + if(out == NULL) return 1; + if(!ctx->image_size) return SPNG_EOVERFLOW; + if(len < ctx->image_size) return SPNG_EBUFSIZ; + } + + uint32_t bytes_read = 0; + + ret = read_idat_bytes(ctx, &bytes_read); + if(ret) return decode_err(ctx, ret); + + if(bytes_read > 1) + { + int valid = read_u16(ctx->data) % 31 ? 0 : 1; + + unsigned flg = ctx->data[1]; + unsigned flevel = flg >> 6; + int compression_level = Z_DEFAULT_COMPRESSION; + + if(flevel == 0) compression_level = 0; /* fastest */ + else if(flevel == 1) compression_level = 1; /* fast */ + else if(flevel == 2) compression_level = 6; /* default */ + else if(flevel == 3) compression_level = 9; /* slowest, max compression */ + + if(valid) ctx->image_options.compression_level = compression_level; + } + + ret = spng__inflate_init(ctx, ctx->image_options.window_bits); + if(ret) return decode_err(ctx, ret); + + ctx->zstream.avail_in = bytes_read; + ctx->zstream.next_in = ctx->data; + + size_t scanline_buf_size = ctx->subimage[ctx->widest_pass].scanline_width; + + scanline_buf_size += 32; + + if(scanline_buf_size < 32) return SPNG_EOVERFLOW; + + ctx->scanline_buf = spng__malloc(ctx, scanline_buf_size); + ctx->prev_scanline_buf = spng__malloc(ctx, scanline_buf_size); + + ctx->scanline = ctx->scanline_buf; + ctx->prev_scanline = ctx->prev_scanline_buf; + + struct decode_flags f = {0}; + + ctx->fmt = fmt; + + if(ihdr->color_type == SPNG_COLOR_TYPE_INDEXED) f.indexed = 1; + + unsigned processing_depth = ihdr->bit_depth; + + if(f.indexed) processing_depth = 8; + + if(ihdr->interlace_method) + { + f.interlaced = 1; + ctx->row_buf = spng__malloc(ctx, ctx->image_width); + ctx->row = ctx->row_buf; + + if(ctx->row == NULL) return decode_err(ctx, SPNG_EMEM); + } + + if(ctx->scanline == NULL || ctx->prev_scanline == NULL) + { + return decode_err(ctx, SPNG_EMEM); + } + + f.do_scaling = 1; + if(f.indexed) f.do_scaling = 0; + + unsigned depth_target = 8; /* FMT_RGBA8, G8 */ + if(fmt == SPNG_FMT_RGBA16) depth_target = 16; + + if(flags & SPNG_DECODE_TRNS && ctx->stored.trns) f.apply_trns = 1; + else flags &= ~SPNG_DECODE_TRNS; + + if(ihdr->color_type == SPNG_COLOR_TYPE_GRAYSCALE_ALPHA || + ihdr->color_type == SPNG_COLOR_TYPE_TRUECOLOR_ALPHA) flags &= ~SPNG_DECODE_TRNS; + + if(flags & SPNG_DECODE_GAMMA && ctx->stored.gama) f.apply_gamma = 1; + else flags &= ~SPNG_DECODE_GAMMA; + + if(flags & SPNG_DECODE_USE_SBIT && ctx->stored.sbit) f.use_sbit = 1; + else flags &= ~SPNG_DECODE_USE_SBIT; + + if(fmt & (SPNG_FMT_RGBA8 | SPNG_FMT_RGBA16)) + { + if(ihdr->color_type == SPNG_COLOR_TYPE_TRUECOLOR_ALPHA && + ihdr->bit_depth == depth_target) f.same_layout = 1; + } + else if(fmt == SPNG_FMT_RGB8) + { + if(ihdr->color_type == SPNG_COLOR_TYPE_TRUECOLOR && + ihdr->bit_depth == depth_target) f.same_layout = 1; + + f.apply_trns = 0; /* not applicable */ + } + else if(fmt & (SPNG_FMT_PNG | SPNG_FMT_RAW)) + { + f.same_layout = 1; + f.do_scaling = 0; + f.apply_gamma = 0; /* for now */ + f.apply_trns = 0; + } + else if(fmt == SPNG_FMT_G8 && ihdr->color_type == SPNG_COLOR_TYPE_GRAYSCALE && ihdr->bit_depth <= 8) + { + if(ihdr->bit_depth == depth_target) f.same_layout = 1; + else if(ihdr->bit_depth < 8) f.unpack = 1; + + f.apply_trns = 0; + } + else if(fmt == SPNG_FMT_GA8 && ihdr->color_type == SPNG_COLOR_TYPE_GRAYSCALE && ihdr->bit_depth <= 8) + { + if(ihdr->color_type == SPNG_COLOR_TYPE_GRAYSCALE_ALPHA && + ihdr->bit_depth == depth_target) f.same_layout = 1; + else if(ihdr->bit_depth <= 8) f.unpack = 1; + } + else if(fmt == SPNG_FMT_GA16 && ihdr->color_type == SPNG_COLOR_TYPE_GRAYSCALE && ihdr->bit_depth == 16) + { + if(ihdr->color_type == SPNG_COLOR_TYPE_GRAYSCALE_ALPHA && + ihdr->bit_depth == depth_target) f.same_layout = 1; + else if(ihdr->bit_depth == 16) f.unpack = 1; + } + + /*if(f.same_layout && !flags && !f.interlaced) f.zerocopy = 1;*/ + + uint16_t *gamma_lut = NULL; + + if(f.apply_gamma) + { + float file_gamma = (float)ctx->gama / 100000.0f; + float max; + + unsigned lut_entries; + + if(fmt & (SPNG_FMT_RGBA8 | SPNG_FMT_RGB8)) + { + lut_entries = 256; + max = 255.0f; + + gamma_lut = ctx->gamma_lut8; + ctx->gamma_lut = ctx->gamma_lut8; + } + else /* SPNG_FMT_RGBA16 */ + { + lut_entries = 65536; + max = 65535.0f; + + ctx->gamma_lut16 = spng__malloc(ctx, lut_entries * sizeof(uint16_t)); + if(ctx->gamma_lut16 == NULL) return decode_err(ctx, SPNG_EMEM); + + gamma_lut = ctx->gamma_lut16; + ctx->gamma_lut = ctx->gamma_lut16; + } + + float screen_gamma = 2.2f; + float exponent = file_gamma * screen_gamma; + + if(FP_ZERO == fpclassify(exponent)) return decode_err(ctx, SPNG_EGAMA); + + exponent = 1.0f / exponent; + + unsigned i; + for(i=0; i < lut_entries; i++) + { + float c = pow((float)i / max, exponent) * max; + if(c > max) c = max; + + gamma_lut[i] = (uint16_t)c; + } + } + + struct spng_sbit *sb = &ctx->decode_sb; + + sb->red_bits = processing_depth; + sb->green_bits = processing_depth; + sb->blue_bits = processing_depth; + sb->alpha_bits = processing_depth; + sb->grayscale_bits = processing_depth; + + if(f.use_sbit) + { + if(ihdr->color_type == 0) + { + sb->grayscale_bits = ctx->sbit.grayscale_bits; + sb->alpha_bits = ihdr->bit_depth; + } + else if(ihdr->color_type == 2 || ihdr->color_type == 3) + { + sb->red_bits = ctx->sbit.red_bits; + sb->green_bits = ctx->sbit.green_bits; + sb->blue_bits = ctx->sbit.blue_bits; + sb->alpha_bits = ihdr->bit_depth; + } + else if(ihdr->color_type == 4) + { + sb->grayscale_bits = ctx->sbit.grayscale_bits; + sb->alpha_bits = ctx->sbit.alpha_bits; + } + else /* == 6 */ + { + sb->red_bits = ctx->sbit.red_bits; + sb->green_bits = ctx->sbit.green_bits; + sb->blue_bits = ctx->sbit.blue_bits; + sb->alpha_bits = ctx->sbit.alpha_bits; + } + } + + if(ihdr->bit_depth == 16 && fmt & (SPNG_FMT_RGBA8 | SPNG_FMT_RGB8)) + {/* samples are scaled down by 8 bits in the decode loop */ + sb->red_bits -= 8; + sb->green_bits -= 8; + sb->blue_bits -= 8; + sb->alpha_bits -= 8; + sb->grayscale_bits -= 8; + + processing_depth = 8; + } + + /* Prevent infinite loops in sample_to_target() */ + if(!depth_target || depth_target > 16 || + !processing_depth || processing_depth > 16 || + !sb->grayscale_bits || sb->grayscale_bits > processing_depth || + !sb->alpha_bits || sb->alpha_bits > processing_depth || + !sb->red_bits || sb->red_bits > processing_depth || + !sb->green_bits || sb->green_bits > processing_depth || + !sb->blue_bits || sb->blue_bits > processing_depth) + { + return decode_err(ctx, SPNG_ESBIT); + } + + if(sb->red_bits == sb->green_bits && + sb->green_bits == sb->blue_bits && + sb->blue_bits == sb->alpha_bits && + sb->alpha_bits == processing_depth && + processing_depth == depth_target) f.do_scaling = 0; + + struct spng_plte_entry *plte = ctx->decode_plte.rgba; + + /* Pre-process palette entries */ + if(f.indexed) + { + uint8_t red, green, blue, alpha; + + uint32_t i; + for(i=0; i < 256; i++) + { + if(f.apply_trns && i < ctx->trns.n_type3_entries) + ctx->plte.entries[i].alpha = ctx->trns.type3_alpha[i]; + else + ctx->plte.entries[i].alpha = 255; + + red = sample_to_target(ctx->plte.entries[i].red, 8, sb->red_bits, 8); + green = sample_to_target(ctx->plte.entries[i].green, 8, sb->green_bits, 8); + blue = sample_to_target(ctx->plte.entries[i].blue, 8, sb->blue_bits, 8); + alpha = sample_to_target(ctx->plte.entries[i].alpha, 8, sb->alpha_bits, 8); + +#if defined(SPNG_ARM) + if(fmt == SPNG_FMT_RGB8 && ihdr->bit_depth == 8) + {/* Working with 3 bytes at a time is more of an ARM thing */ + ctx->decode_plte.rgb[i * 3 + 0] = red; + ctx->decode_plte.rgb[i * 3 + 1] = green; + ctx->decode_plte.rgb[i * 3 + 2] = blue; + continue; + } +#endif + plte[i].red = red; + plte[i].green = green; + plte[i].blue = blue; + plte[i].alpha = alpha; + } + + f.apply_trns = 0; + } + + unsigned char *trns_px = ctx->trns_px; + + if(f.apply_trns) + { + uint16_t mask = ~0; + if(ctx->ihdr.bit_depth < 16) mask = (1 << ctx->ihdr.bit_depth) - 1; + + if(fmt & (SPNG_FMT_RGBA8 | SPNG_FMT_RGBA16)) + { + if(ihdr->color_type == SPNG_COLOR_TYPE_TRUECOLOR) + { + if(ihdr->bit_depth == 16) + { + memcpy(trns_px, &ctx->trns.red, 2); + memcpy(trns_px + 2, &ctx->trns.green, 2); + memcpy(trns_px + 4, &ctx->trns.blue, 2); + } + else + { + trns_px[0] = ctx->trns.red & mask; + trns_px[1] = ctx->trns.green & mask; + trns_px[2] = ctx->trns.blue & mask; + } + } + } + else if(ihdr->color_type == SPNG_COLOR_TYPE_GRAYSCALE) // fmt == SPNG_FMT_GA8 && + { + if(ihdr->bit_depth == 16) + { + memcpy(trns_px, &ctx->trns.gray, 2); + } + else + { + trns_px[0] = ctx->trns.gray & mask; + } + } + } + + ctx->decode_flags = f; + + ctx->state = SPNG_STATE_DECODE_INIT; + + struct spng_row_info *ri = &ctx->row_info; + struct spng_subimage *sub = ctx->subimage; + + while(!sub[ri->pass].width || !sub[ri->pass].height) ri->pass++; + + if(f.interlaced) ri->row_num = adam7_y_start[ri->pass]; + + unsigned pixel_size = 4; /* SPNG_FMT_RGBA8 */ + + if(fmt == SPNG_FMT_RGBA16) pixel_size = 8; + else if(fmt == SPNG_FMT_RGB8) pixel_size = 3; + else if(fmt == SPNG_FMT_G8) pixel_size = 1; + else if(fmt == SPNG_FMT_GA8) pixel_size = 2; + + int i; + for(i=ri->pass; i <= ctx->last_pass; i++) + { + if(!sub[i].scanline_width) continue; + + if(fmt & (SPNG_FMT_PNG | SPNG_FMT_RAW)) sub[i].out_width = sub[i].scanline_width - 1; + else sub[i].out_width = (size_t)sub[i].width * pixel_size; + + if(sub[i].out_width > UINT32_MAX) return decode_err(ctx, SPNG_EOVERFLOW); + } + + /* Read the first filter byte, offsetting all reads by 1 byte. + The scanlines will be aligned with the start of the array with + the next scanline's filter byte at the end, + the last scanline will end up being 1 byte "shorter". */ + ret = read_scanline_bytes(ctx, &ri->filter, 1); + if(ret) return decode_err(ctx, ret); + + if(ri->filter > 4) return decode_err(ctx, SPNG_EFILTER); + + if(flags & SPNG_DECODE_PROGRESSIVE) + { + return 0; + } + + do + { + size_t ioffset = ri->row_num * ctx->image_width; + + ret = spng_decode_row(ctx, (unsigned char*)out + ioffset, ctx->image_width); + }while(!ret); + + if(ret != SPNG_EOI) return decode_err(ctx, ret); + + return 0; +} + +int spng_get_row_info(spng_ctx *ctx, struct spng_row_info *row_info) +{ + if(ctx == NULL || row_info == NULL || ctx->state < SPNG_STATE_DECODE_INIT) return 1; + + if(ctx->state >= SPNG_STATE_EOI) return SPNG_EOI; + + *row_info = ctx->row_info; + + return 0; +} + +static int write_chunks_before_idat(spng_ctx *ctx) +{ + if(ctx == NULL) return SPNG_EINTERNAL; + if(!ctx->encode_only) return SPNG_EINTERNAL; + if(!ctx->stored.ihdr) return SPNG_EINTERNAL; + + int ret; + uint32_t i; + size_t length; + const struct spng_ihdr *ihdr = &ctx->ihdr; + unsigned char *data = ctx->decode_plte.raw; + + ret = write_data(ctx, spng_signature, 8); + if(ret) return ret; + + write_u32(data, ihdr->width); + write_u32(data + 4, ihdr->height); + data[8] = ihdr->bit_depth; + data[9] = ihdr->color_type; + data[10] = ihdr->compression_method; + data[11] = ihdr->filter_method; + data[12] = ihdr->interlace_method; + + ret = write_chunk(ctx, type_ihdr, data, 13); + if(ret) return ret; + + if(ctx->stored.chrm) + { + write_u32(data, ctx->chrm_int.white_point_x); + write_u32(data + 4, ctx->chrm_int.white_point_y); + write_u32(data + 8, ctx->chrm_int.red_x); + write_u32(data + 12, ctx->chrm_int.red_y); + write_u32(data + 16, ctx->chrm_int.green_x); + write_u32(data + 20, ctx->chrm_int.green_y); + write_u32(data + 24, ctx->chrm_int.blue_x); + write_u32(data + 28, ctx->chrm_int.blue_y); + + ret = write_chunk(ctx, type_chrm, data, 32); + if(ret) return ret; + } + + if(ctx->stored.gama) + { + write_u32(data, ctx->gama); + + ret = write_chunk(ctx, type_gama, data, 4); + if(ret) return ret; + } + + if(ctx->stored.iccp) + { + uLongf dest_len = compressBound((uLong)ctx->iccp.profile_len); + + Bytef *buf = spng__malloc(ctx, dest_len); + if(buf == NULL) return SPNG_EMEM; + + ret = compress2(buf, &dest_len, (void*)ctx->iccp.profile, (uLong)ctx->iccp.profile_len, Z_DEFAULT_COMPRESSION); + + if(ret != Z_OK) + { + spng__free(ctx, buf); + return SPNG_EZLIB; + } + + size_t name_len = strlen(ctx->iccp.profile_name); + + length = name_len + 2; + length += dest_len; + + if(dest_len > length) return SPNG_EOVERFLOW; + + unsigned char *cdata = NULL; + + ret = write_header(ctx, type_iccp, length, &cdata); + + if(ret) + { + spng__free(ctx, buf); + return ret; + } + + memcpy(cdata, ctx->iccp.profile_name, name_len + 1); + cdata[name_len + 1] = 0; /* compression method */ + memcpy(cdata + name_len + 2, buf, dest_len); + + spng__free(ctx, buf); + + ret = finish_chunk(ctx); + if(ret) return ret; + } + + if(ctx->stored.sbit) + { + switch(ctx->ihdr.color_type) + { + case SPNG_COLOR_TYPE_GRAYSCALE: + { + length = 1; + + data[0] = ctx->sbit.grayscale_bits; + + break; + } + case SPNG_COLOR_TYPE_TRUECOLOR: + case SPNG_COLOR_TYPE_INDEXED: + { + length = 3; + + data[0] = ctx->sbit.red_bits; + data[1] = ctx->sbit.green_bits; + data[2] = ctx->sbit.blue_bits; + + break; + } + case SPNG_COLOR_TYPE_GRAYSCALE_ALPHA: + { + length = 2; + + data[0] = ctx->sbit.grayscale_bits; + data[1] = ctx->sbit.alpha_bits; + + break; + } + case SPNG_COLOR_TYPE_TRUECOLOR_ALPHA: + { + length = 4; + + data[0] = ctx->sbit.red_bits; + data[1] = ctx->sbit.green_bits; + data[2] = ctx->sbit.blue_bits; + data[3] = ctx->sbit.alpha_bits; + + break; + } + default: return SPNG_EINTERNAL; + } + + ret = write_chunk(ctx, type_sbit, data, length); + if(ret) return ret; + } + + if(ctx->stored.srgb) + { + ret = write_chunk(ctx, type_srgb, &ctx->srgb_rendering_intent, 1); + if(ret) return ret; + } + + ret = write_unknown_chunks(ctx, SPNG_AFTER_IHDR); + if(ret) return ret; + + if(ctx->stored.plte) + { + for(i=0; i < ctx->plte.n_entries; i++) + { + data[i * 3 + 0] = ctx->plte.entries[i].red; + data[i * 3 + 1] = ctx->plte.entries[i].green; + data[i * 3 + 2] = ctx->plte.entries[i].blue; + } + + ret = write_chunk(ctx, type_plte, data, ctx->plte.n_entries * 3); + if(ret) return ret; + } + + if(ctx->stored.bkgd) + { + switch(ctx->ihdr.color_type) + { + case SPNG_COLOR_TYPE_GRAYSCALE: + case SPNG_COLOR_TYPE_GRAYSCALE_ALPHA: + { + length = 2; + + write_u16(data, ctx->bkgd.gray); + + break; + } + case SPNG_COLOR_TYPE_TRUECOLOR: + case SPNG_COLOR_TYPE_TRUECOLOR_ALPHA: + { + length = 6; + + write_u16(data, ctx->bkgd.red); + write_u16(data + 2, ctx->bkgd.green); + write_u16(data + 4, ctx->bkgd.blue); + + break; + } + case SPNG_COLOR_TYPE_INDEXED: + { + length = 1; + + data[0] = ctx->bkgd.plte_index; + + break; + } + default: return SPNG_EINTERNAL; + } + + ret = write_chunk(ctx, type_bkgd, data, length); + if(ret) return ret; + } + + if(ctx->stored.hist) + { + length = ctx->plte.n_entries * 2; + + for(i=0; i < ctx->plte.n_entries; i++) + { + write_u16(data + i * 2, ctx->hist.frequency[i]); + } + + ret = write_chunk(ctx, type_hist, data, length); + if(ret) return ret; + } + + if(ctx->stored.trns) + { + if(ctx->ihdr.color_type == SPNG_COLOR_TYPE_GRAYSCALE) + { + write_u16(data, ctx->trns.gray); + + ret = write_chunk(ctx, type_trns, data, 2); + } + else if(ctx->ihdr.color_type == SPNG_COLOR_TYPE_TRUECOLOR) + { + write_u16(data, ctx->trns.red); + write_u16(data + 2, ctx->trns.green); + write_u16(data + 4, ctx->trns.blue); + + ret = write_chunk(ctx, type_trns, data, 6); + } + else if(ctx->ihdr.color_type == SPNG_COLOR_TYPE_INDEXED) + { + ret = write_chunk(ctx, type_trns, ctx->trns.type3_alpha, ctx->trns.n_type3_entries); + } + + if(ret) return ret; + } + + if(ctx->stored.phys) + { + write_u32(data, ctx->phys.ppu_x); + write_u32(data + 4, ctx->phys.ppu_y); + data[8] = ctx->phys.unit_specifier; + + ret = write_chunk(ctx, type_phys, data, 9); + if(ret) return ret; + } + + if(ctx->stored.splt) + { + const struct spng_splt *splt; + unsigned char *cdata = NULL; + + uint32_t k; + for(i=0; i < ctx->n_splt; i++) + { + splt = &ctx->splt_list[i]; + + size_t name_len = strlen(splt->name); + length = name_len + 1; + + if(splt->sample_depth == 8) length += splt->n_entries * 6 + 1; + else if(splt->sample_depth == 16) length += splt->n_entries * 10 + 1; + + ret = write_header(ctx, type_splt, length, &cdata); + if(ret) return ret; + + memcpy(cdata, splt->name, name_len + 1); + cdata += name_len + 2; + cdata[-1] = splt->sample_depth; + + if(splt->sample_depth == 8) + { + for(k=0; k < splt->n_entries; k++) + { + cdata[k * 6 + 0] = splt->entries[k].red; + cdata[k * 6 + 1] = splt->entries[k].green; + cdata[k * 6 + 2] = splt->entries[k].blue; + cdata[k * 6 + 3] = splt->entries[k].alpha; + write_u16(cdata + k * 6 + 4, splt->entries[k].frequency); + } + } + else if(splt->sample_depth == 16) + { + for(k=0; k < splt->n_entries; k++) + { + write_u16(cdata + k * 10 + 0, splt->entries[k].red); + write_u16(cdata + k * 10 + 2, splt->entries[k].green); + write_u16(cdata + k * 10 + 4, splt->entries[k].blue); + write_u16(cdata + k * 10 + 6, splt->entries[k].alpha); + write_u16(cdata + k * 10 + 8, splt->entries[k].frequency); + } + } + + ret = finish_chunk(ctx); + if(ret) return ret; + } + } + + if(ctx->stored.time) + { + write_u16(data, ctx->time.year); + data[2] = ctx->time.month; + data[3] = ctx->time.day; + data[4] = ctx->time.hour; + data[5] = ctx->time.minute; + data[6] = ctx->time.second; + + ret = write_chunk(ctx, type_time, data, 7); + if(ret) return ret; + } + + if(ctx->stored.text) + { + unsigned char *cdata = NULL; + const struct spng_text2 *text; + const uint8_t *text_type_array[4] = { 0, type_text, type_ztxt, type_itxt }; + + for(i=0; i < ctx->n_text; i++) + { + text = &ctx->text_list[i]; + + const uint8_t *text_chunk_type = text_type_array[text->type]; + Bytef *compressed_text = NULL; + size_t keyword_len = 0; + size_t language_tag_len = 0; + size_t translated_keyword_len = 0; + size_t compressed_length = 0; + size_t text_length = 0; + + keyword_len = strlen(text->keyword); + text_length = strlen(text->text); + + length = keyword_len + 1; + + if(text->type == SPNG_ZTXT) + { + length += 1; /* compression method */ + } + else if(text->type == SPNG_ITXT) + { + if(!text->language_tag || !text->translated_keyword) return SPNG_EINTERNAL; + + language_tag_len = strlen(text->language_tag); + translated_keyword_len = strlen(text->translated_keyword); + + length += language_tag_len; + if(length < language_tag_len) return SPNG_EOVERFLOW; + + length += translated_keyword_len; + if(length < translated_keyword_len) return SPNG_EOVERFLOW; + + length += 4; /* compression flag + method + nul for the two strings */ + if(length < 4) return SPNG_EOVERFLOW; + } + + if(text->compression_flag) + { + ret = spng__deflate_init(ctx, &ctx->text_options); + if(ret) return ret; + + z_stream *zstream = &ctx->zstream; + uLongf dest_len = deflateBound(zstream, (uLong)text_length); + + compressed_text = spng__malloc(ctx, dest_len); + + if(compressed_text == NULL) return SPNG_EMEM; + + zstream->next_in = (void*)text->text; + zstream->avail_in = (uInt)text_length; + + zstream->next_out = compressed_text; + zstream->avail_out = dest_len; + + ret = deflate(zstream, Z_FINISH); + + if(ret != Z_STREAM_END) + { + spng__free(ctx, compressed_text); + return SPNG_EZLIB; + } + + compressed_length = zstream->total_out; + + length += compressed_length; + if(length < compressed_length) return SPNG_EOVERFLOW; + } + else + { + text_length = strlen(text->text); + + length += text_length; + if(length < text_length) return SPNG_EOVERFLOW; + } + + ret = write_header(ctx, text_chunk_type, length, &cdata); + if(ret) + { + spng__free(ctx, compressed_text); + return ret; + } + + memcpy(cdata, text->keyword, keyword_len + 1); + cdata += keyword_len + 1; + + if(text->type == SPNG_ITXT) + { + cdata[0] = text->compression_flag; + cdata[1] = 0; /* compression method */ + cdata += 2; + + memcpy(cdata, text->language_tag, language_tag_len + 1); + cdata += language_tag_len + 1; + + memcpy(cdata, text->translated_keyword, translated_keyword_len + 1); + cdata += translated_keyword_len + 1; + } + else if(text->type == SPNG_ZTXT) + { + cdata[0] = 0; /* compression method */ + cdata++; + } + + if(text->compression_flag) memcpy(cdata, compressed_text, compressed_length); + else memcpy(cdata, text->text, text_length); + + spng__free(ctx, compressed_text); + + ret = finish_chunk(ctx); + if(ret) return ret; + } + } + + if(ctx->stored.offs) + { + write_s32(data, ctx->offs.x); + write_s32(data + 4, ctx->offs.y); + data[8] = ctx->offs.unit_specifier; + + ret = write_chunk(ctx, type_offs, data, 9); + if(ret) return ret; + } + + if(ctx->stored.exif) + { + ret = write_chunk(ctx, type_exif, ctx->exif.data, ctx->exif.length); + if(ret) return ret; + } + + ret = write_unknown_chunks(ctx, SPNG_AFTER_PLTE); + if(ret) return ret; + + return 0; +} + +static int write_chunks_after_idat(spng_ctx *ctx) +{ + if(ctx == NULL) return SPNG_EINTERNAL; + + int ret = write_unknown_chunks(ctx, SPNG_AFTER_IDAT); + if(ret) return ret; + + return write_iend(ctx); +} + +/* Compress and write scanline to IDAT stream */ +static int write_idat_bytes(spng_ctx *ctx, const void *scanline, size_t len, int flush) +{ + if(ctx == NULL || scanline == NULL) return SPNG_EINTERNAL; + if(len > UINT_MAX) return SPNG_EINTERNAL; + + int ret = 0; + unsigned char *data = NULL; + z_stream *zstream = &ctx->zstream; + uint32_t idat_length = SPNG_WRITE_SIZE; + + zstream->next_in = scanline; + zstream->avail_in = (uInt)len; + + do + { + ret = deflate(zstream, flush); + + if(zstream->avail_out == 0) + { + ret = finish_chunk(ctx); + if(ret) return encode_err(ctx, ret); + + ret = write_header(ctx, type_idat, idat_length, &data); + if(ret) return encode_err(ctx, ret); + + zstream->next_out = data; + zstream->avail_out = idat_length; + } + + }while(zstream->avail_in); + + if(ret != Z_OK) return SPNG_EZLIB; + + return 0; +} + +static int finish_idat(spng_ctx *ctx) +{ + int ret = 0; + unsigned char *data = NULL; + z_stream *zstream = &ctx->zstream; + uint32_t idat_length = SPNG_WRITE_SIZE; + + while(ret != Z_STREAM_END) + { + ret = deflate(zstream, Z_FINISH); + + if(ret) + { + if(ret == Z_STREAM_END) break; + + if(ret != Z_BUF_ERROR) return SPNG_EZLIB; + } + + if(zstream->avail_out == 0) + { + ret = finish_chunk(ctx); + if(ret) return encode_err(ctx, ret); + + ret = write_header(ctx, type_idat, idat_length, &data); + if(ret) return encode_err(ctx, ret); + + zstream->next_out = data; + zstream->avail_out = idat_length; + } + } + + uint32_t trimmed_length = idat_length - zstream->avail_out; + + ret = trim_chunk(ctx, trimmed_length); + if(ret) return ret; + + return finish_chunk(ctx); +} + +static int encode_scanline(spng_ctx *ctx, const void *scanline, size_t len) +{ + if(ctx == NULL || scanline == NULL) return SPNG_EINTERNAL; + + int ret, pass = ctx->row_info.pass; + uint8_t filter = 0; + struct spng_row_info *ri = &ctx->row_info; + const struct spng_subimage *sub = ctx->subimage; + struct encode_flags f = ctx->encode_flags; + unsigned char *filtered_scanline = ctx->filtered_scanline; + size_t scanline_width = sub[pass].scanline_width; + + if(len < scanline_width - 1) return SPNG_EINTERNAL; + + /* encode_row() interlaces directly to ctx->scanline */ + if(scanline != ctx->scanline) memcpy(ctx->scanline, scanline, scanline_width - 1); + + if(f.to_bigendian) u16_row_to_bigendian(ctx->scanline, scanline_width - 1); + const int requires_previous = f.filter_choice & (SPNG_FILTER_CHOICE_UP | SPNG_FILTER_CHOICE_AVG | SPNG_FILTER_CHOICE_PAETH); + + /* XXX: exclude 'requires_previous' filters by default for first scanline? */ + if(!ri->scanline_idx && requires_previous) + { + /* prev_scanline is all zeros for the first scanline */ + memset(ctx->prev_scanline, 0, scanline_width); + } + + filter = get_best_filter(ctx->prev_scanline, ctx->scanline, scanline_width, ctx->bytes_per_pixel, f.filter_choice); + + if(!filter) filtered_scanline = ctx->scanline; + + filtered_scanline[-1] = filter; + + if(filter) + { + ret = filter_scanline(filtered_scanline, ctx->prev_scanline, ctx->scanline, scanline_width, ctx->bytes_per_pixel, filter); + if(ret) return encode_err(ctx, ret); + } + + ret = write_idat_bytes(ctx, filtered_scanline - 1, scanline_width, Z_NO_FLUSH); + if(ret) return encode_err(ctx, ret); + + /* The previous scanline is always unfiltered */ + void *t = ctx->prev_scanline; + ctx->prev_scanline = ctx->scanline; + ctx->scanline = t; + + ret = update_row_info(ctx); + + if(ret == SPNG_EOI) + { + int error = finish_idat(ctx); + if(error) encode_err(ctx, error); + + if(f.finalize) + { + error = spng_encode_chunks(ctx); + if(error) return encode_err(ctx, error); + } + } + + return ret; +} + +static int encode_row(spng_ctx *ctx, const void *row, size_t len) +{ + if(ctx == NULL || row == NULL) return SPNG_EINTERNAL; + + const int pass = ctx->row_info.pass; + + if(!ctx->ihdr.interlace_method || pass == 6) return encode_scanline(ctx, row, len); + + uint32_t k; + const unsigned pixel_size = ctx->pixel_size; + const unsigned bit_depth = ctx->ihdr.bit_depth; + + if(bit_depth < 8) + { + const unsigned samples_per_byte = 8 / bit_depth; + const uint8_t mask = (1 << bit_depth) - 1; + const unsigned initial_shift = 8 - bit_depth; + unsigned shift_amount = initial_shift; + + unsigned char *scanline = ctx->scanline; + const unsigned char *row_uc = row; + uint8_t sample; + + memset(scanline, 0, ctx->subimage[pass].scanline_width); + + for(k=0; k < ctx->subimage[pass].width; k++) + { + size_t ioffset = adam7_x_start[pass] + k * adam7_x_delta[pass]; + + sample = row_uc[ioffset / samples_per_byte]; + + sample = sample >> (initial_shift - ioffset * bit_depth % 8); + sample = sample & mask; + sample = sample << shift_amount; + + scanline[0] |= sample; + + shift_amount -= bit_depth; + + if(shift_amount > 7) + { + shift_amount = initial_shift; + scanline++; + } + } + + return encode_scanline(ctx, ctx->scanline, len); + } + + for(k=0; k < ctx->subimage[pass].width; k++) + { + size_t ioffset = (adam7_x_start[pass] + (size_t) k * adam7_x_delta[pass]) * pixel_size; + + memcpy(ctx->scanline + k * pixel_size, (unsigned char*)row + ioffset, pixel_size); + } + + return encode_scanline(ctx, ctx->scanline, len); +} + +int spng_encode_scanline(spng_ctx *ctx, const void *scanline, size_t len) +{ + if(ctx == NULL || scanline == NULL) return SPNG_EINVAL; + if(ctx->state >= SPNG_STATE_EOI) return SPNG_EOI; + if(len < (ctx->subimage[ctx->row_info.pass].scanline_width -1) ) return SPNG_EBUFSIZ; + + return encode_scanline(ctx, scanline, len); +} + +int spng_encode_row(spng_ctx *ctx, const void *row, size_t len) +{ + if(ctx == NULL || row == NULL) return SPNG_EINVAL; + if(ctx->state >= SPNG_STATE_EOI) return SPNG_EOI; + if(len < ctx->image_width) return SPNG_EBUFSIZ; + + return encode_row(ctx, row, len); +} + +int spng_encode_chunks(spng_ctx *ctx) +{ + if(ctx == NULL) return 1; + if(!ctx->state) return SPNG_EBADSTATE; + if(ctx->state < SPNG_STATE_OUTPUT) return SPNG_ENODST; + if(!ctx->encode_only) return SPNG_ECTXTYPE; + + int ret = 0; + + if(ctx->state < SPNG_STATE_FIRST_IDAT) + { + if(!ctx->stored.ihdr) return SPNG_ENOIHDR; + + ret = write_chunks_before_idat(ctx); + if(ret) return encode_err(ctx, ret); + + ctx->state = SPNG_STATE_FIRST_IDAT; + } + else if(ctx->state == SPNG_STATE_FIRST_IDAT) + { + return 0; + } + else if(ctx->state == SPNG_STATE_EOI) + { + ret = write_chunks_after_idat(ctx); + if(ret) return encode_err(ctx, ret); + + ctx->state = SPNG_STATE_IEND; + } + else return SPNG_EOPSTATE; + + return 0; +} + +int spng_encode_image(spng_ctx *ctx, const void *img, size_t len, int fmt, int flags) +{ + if(ctx == NULL) return 1; + if(!ctx->state) return SPNG_EBADSTATE; + if(!ctx->encode_only) return SPNG_ECTXTYPE; + if(!ctx->stored.ihdr) return SPNG_ENOIHDR; + if( !(fmt == SPNG_FMT_PNG || fmt == SPNG_FMT_RAW) ) return SPNG_EFMT; + + int ret = 0; + const struct spng_ihdr *ihdr = &ctx->ihdr; + struct encode_flags *encode_flags = &ctx->encode_flags; + + if(ihdr->color_type == SPNG_COLOR_TYPE_INDEXED && !ctx->stored.plte) return SPNG_ENOPLTE; + + ret = calculate_image_width(ihdr, fmt, &ctx->image_width); + if(ret) return encode_err(ctx, ret); + + if(ctx->image_width > SIZE_MAX / ihdr->height) ctx->image_size = 0; /* overflow */ + else ctx->image_size = ctx->image_width * ihdr->height; + + if( !(flags & SPNG_ENCODE_PROGRESSIVE) ) + { + if(img == NULL) return 1; + if(!ctx->image_size) return SPNG_EOVERFLOW; + if(len != ctx->image_size) return SPNG_EBUFSIZ; + } + + ret = spng_encode_chunks(ctx); + if(ret) return encode_err(ctx, ret); + + ret = calculate_subimages(ctx); + if(ret) return encode_err(ctx, ret); + + if(ihdr->bit_depth < 8) ctx->bytes_per_pixel = 1; + else ctx->bytes_per_pixel = num_channels(ihdr) * (ihdr->bit_depth / 8); + + if(spng__optimize(SPNG_FILTER_CHOICE)) + { + /* Filtering would make no difference */ + if(!ctx->image_options.compression_level) + { + encode_flags->filter_choice = SPNG_DISABLE_FILTERING; + } + + /* Palette indices and low bit-depth images do not benefit from filtering */ + if(ihdr->color_type == SPNG_COLOR_TYPE_INDEXED || ihdr->bit_depth < 8) + { + encode_flags->filter_choice = SPNG_DISABLE_FILTERING; + } + } + + /* This is technically the same as disabling filtering */ + if(encode_flags->filter_choice == SPNG_FILTER_CHOICE_NONE) + { + encode_flags->filter_choice = SPNG_DISABLE_FILTERING; + } + + if(!encode_flags->filter_choice && spng__optimize(SPNG_IMG_COMPRESSION_STRATEGY)) + { + ctx->image_options.strategy = Z_DEFAULT_STRATEGY; + } + + ret = spng__deflate_init(ctx, &ctx->image_options); + if(ret) return encode_err(ctx, ret); + + size_t scanline_buf_size = ctx->subimage[ctx->widest_pass].scanline_width; + + scanline_buf_size += 32; + + if(scanline_buf_size < 32) return SPNG_EOVERFLOW; + + ctx->scanline_buf = spng__malloc(ctx, scanline_buf_size); + ctx->prev_scanline_buf = spng__malloc(ctx, scanline_buf_size); + + if(ctx->scanline_buf == NULL || ctx->prev_scanline_buf == NULL) return encode_err(ctx, SPNG_EMEM); + + /* Maintain alignment for pixels, filter at [-1] */ + ctx->scanline = ctx->scanline_buf + 16; + ctx->prev_scanline = ctx->prev_scanline_buf + 16; + + if(encode_flags->filter_choice) + { + ctx->filtered_scanline_buf = spng__malloc(ctx, scanline_buf_size); + if(ctx->filtered_scanline_buf == NULL) return encode_err(ctx, SPNG_EMEM); + + ctx->filtered_scanline = ctx->filtered_scanline_buf + 16; + } + + struct spng_subimage *sub = ctx->subimage; + struct spng_row_info *ri = &ctx->row_info; + + ctx->fmt = fmt; + + z_stream *zstream = &ctx->zstream; + zstream->avail_out = SPNG_WRITE_SIZE; + + ret = write_header(ctx, type_idat, zstream->avail_out, &zstream->next_out); + if(ret) return encode_err(ctx, ret); + + if(ihdr->interlace_method) encode_flags->interlace = 1; + + if(fmt & (SPNG_FMT_PNG | SPNG_FMT_RAW) ) encode_flags->same_layout = 1; + + if(ihdr->bit_depth == 16 && fmt != SPNG_FMT_RAW) encode_flags->to_bigendian = 1; + + if(flags & SPNG_ENCODE_FINALIZE) encode_flags->finalize = 1; + + while(!sub[ri->pass].width || !sub[ri->pass].height) ri->pass++; + + if(encode_flags->interlace) ri->row_num = adam7_y_start[ri->pass]; + + ctx->pixel_size = 4; /* SPNG_FMT_RGBA8 */ + + if(fmt == SPNG_FMT_RGBA16) ctx->pixel_size = 8; + else if(fmt == SPNG_FMT_RGB8) ctx->pixel_size = 3; + else if(fmt == SPNG_FMT_G8) ctx->pixel_size = 1; + else if(fmt == SPNG_FMT_GA8) ctx->pixel_size = 2; + else if(fmt & (SPNG_FMT_PNG | SPNG_FMT_RAW)) ctx->pixel_size = ctx->bytes_per_pixel; + + ctx->state = SPNG_STATE_ENCODE_INIT; + + if(flags & SPNG_ENCODE_PROGRESSIVE) + { + encode_flags->progressive = 1; + + return 0; + } + + do + { + size_t ioffset = ri->row_num * ctx->image_width; + + ret = encode_row(ctx, (unsigned char*)img + ioffset, ctx->image_width); + + }while(!ret); + + if(ret != SPNG_EOI) return encode_err(ctx, ret); + + return 0; +} + +spng_ctx *spng_ctx_new(int flags) +{ + struct spng_alloc alloc = + { + .malloc_fn = malloc, + .realloc_fn = realloc, + .calloc_fn = calloc, + .free_fn = free + }; + + return spng_ctx_new2(&alloc, flags); +} + +spng_ctx *spng_ctx_new2(struct spng_alloc *alloc, int flags) +{ + if(alloc == NULL) return NULL; + if(flags != (flags & SPNG__CTX_FLAGS_ALL)) return NULL; + + if(alloc->malloc_fn == NULL) return NULL; + if(alloc->realloc_fn == NULL) return NULL; + if(alloc->calloc_fn == NULL) return NULL; + if(alloc->free_fn == NULL) return NULL; + + spng_ctx *ctx = alloc->calloc_fn(1, sizeof(spng_ctx)); + if(ctx == NULL) return NULL; + + ctx->alloc = *alloc; + + ctx->max_width = spng_u32max; + ctx->max_height = spng_u32max; + + ctx->max_chunk_size = spng_u32max; + ctx->chunk_cache_limit = SIZE_MAX; + ctx->chunk_count_limit = SPNG_MAX_CHUNK_COUNT; + + ctx->state = SPNG_STATE_INIT; + + ctx->crc_action_critical = SPNG_CRC_ERROR; + ctx->crc_action_ancillary = SPNG_CRC_DISCARD; + + const struct spng__zlib_options image_defaults = + { + .compression_level = Z_DEFAULT_COMPRESSION, + .window_bits = 15, + .mem_level = 8, + .strategy = Z_FILTERED, + .data_type = 0 /* Z_BINARY */ + }; + + const struct spng__zlib_options text_defaults = + { + .compression_level = Z_DEFAULT_COMPRESSION, + .window_bits = 15, + .mem_level = 8, + .strategy = Z_DEFAULT_STRATEGY, + .data_type = 1 /* Z_TEXT */ + }; + + ctx->image_options = image_defaults; + ctx->text_options = text_defaults; + + ctx->optimize_option = ~0; + ctx->encode_flags.filter_choice = SPNG_FILTER_CHOICE_ALL; + + ctx->flags = flags; + + if(flags & SPNG_CTX_ENCODER) ctx->encode_only = 1; + + return ctx; +} + +void spng_ctx_free(spng_ctx *ctx) +{ + if(ctx == NULL) return; + + if(ctx->streaming && ctx->stream_buf != NULL) spng__free(ctx, ctx->stream_buf); + + if(!ctx->user.exif) spng__free(ctx, ctx->exif.data); + + if(!ctx->user.iccp) spng__free(ctx, ctx->iccp.profile); + + uint32_t i; + + if(ctx->splt_list != NULL && !ctx->user.splt) + { + for(i=0; i < ctx->n_splt; i++) + { + spng__free(ctx, ctx->splt_list[i].entries); + } + spng__free(ctx, ctx->splt_list); + } + + if(ctx->text_list != NULL) + { + for(i=0; i< ctx->n_text; i++) + { + if(ctx->user.text) break; + + spng__free(ctx, ctx->text_list[i].keyword); + if(ctx->text_list[i].compression_flag) spng__free(ctx, ctx->text_list[i].text); + } + spng__free(ctx, ctx->text_list); + } + + if(ctx->chunk_list != NULL && !ctx->user.unknown) + { + for(i=0; i< ctx->n_chunks; i++) + { + spng__free(ctx, ctx->chunk_list[i].data); + } + spng__free(ctx, ctx->chunk_list); + } + + if(ctx->deflate) deflateEnd(&ctx->zstream); + else inflateEnd(&ctx->zstream); + + if(!ctx->user_owns_out_png) spng__free(ctx, ctx->out_png); + + spng__free(ctx, ctx->gamma_lut16); + + spng__free(ctx, ctx->row_buf); + spng__free(ctx, ctx->scanline_buf); + spng__free(ctx, ctx->prev_scanline_buf); + spng__free(ctx, ctx->filtered_scanline_buf); + + spng_free_fn *free_fn = ctx->alloc.free_fn; + + memset(ctx, 0, sizeof(spng_ctx)); + + free_fn(ctx); +} + +static int buffer_read_fn(spng_ctx *ctx, void *user, void *data, size_t n) +{ + if(n > ctx->bytes_left) return SPNG_IO_EOF; + + (void)user; + (void)data; + ctx->data = ctx->data + ctx->last_read_size; + + ctx->last_read_size = n; + ctx->bytes_left -= n; + + return 0; +} + +static int file_read_fn(spng_ctx *ctx, void *user, void *data, size_t n) +{ + FILE *file = user; + (void)ctx; + + if(fread(data, n, 1, file) != 1) + { + if(feof(file)) return SPNG_IO_EOF; + else return SPNG_IO_ERROR; + } + + return 0; +} + +static int file_write_fn(spng_ctx *ctx, void *user, void *data, size_t n) +{ + FILE *file = user; + (void)ctx; + + if(fwrite(data, n, 1, file) != 1) return SPNG_IO_ERROR; + + return 0; +} + +int spng_set_png_buffer(spng_ctx *ctx, const void *buf, size_t size) +{ + if(ctx == NULL || buf == NULL) return 1; + if(!ctx->state) return SPNG_EBADSTATE; + if(ctx->encode_only) return SPNG_ECTXTYPE; /* not supported */ + + if(ctx->data != NULL) return SPNG_EBUF_SET; + + ctx->data = buf; + ctx->png_base = buf; + ctx->data_size = size; + ctx->bytes_left = size; + + ctx->read_fn = buffer_read_fn; + + ctx->state = SPNG_STATE_INPUT; + + return 0; +} + +int spng_set_png_stream(spng_ctx *ctx, spng_rw_fn *rw_func, void *user) +{ + if(ctx == NULL || rw_func == NULL) return 1; + if(!ctx->state) return SPNG_EBADSTATE; + + /* SPNG_STATE_OUTPUT shares the same value */ + if(ctx->state >= SPNG_STATE_INPUT) return SPNG_EBUF_SET; + + if(ctx->encode_only) + { + if(ctx->out_png != NULL) return SPNG_EBUF_SET; + + ctx->write_fn = rw_func; + ctx->write_ptr = ctx->stream_buf; + + ctx->state = SPNG_STATE_OUTPUT; + } + else + { + ctx->stream_buf = spng__malloc(ctx, SPNG_READ_SIZE); + if(ctx->stream_buf == NULL) return SPNG_EMEM; + + ctx->read_fn = rw_func; + ctx->data = ctx->stream_buf; + ctx->data_size = SPNG_READ_SIZE; + + ctx->state = SPNG_STATE_INPUT; + } + + ctx->stream_user_ptr = user; + + ctx->streaming = 1; + + return 0; +} + +int spng_set_png_file(spng_ctx *ctx, FILE *file) +{ + if(file == NULL) return 1; + + if(ctx->encode_only) return spng_set_png_stream(ctx, file_write_fn, file); + + return spng_set_png_stream(ctx, file_read_fn, file); +} + +void *spng_get_png_buffer(spng_ctx *ctx, size_t *len, int *error) +{ + int tmp = 0; + error = error ? error : &tmp; + *error = 0; + + if(ctx == NULL || !len) *error = SPNG_EINVAL; + + if(*error) return NULL; + + if(!ctx->encode_only) *error = SPNG_ECTXTYPE; + else if(!ctx->state) *error = SPNG_EBADSTATE; + else if(!ctx->internal_buffer) *error = SPNG_EOPSTATE; + else if(ctx->state < SPNG_STATE_EOI) *error = SPNG_EOPSTATE; + else if(ctx->state != SPNG_STATE_IEND) *error = SPNG_ENOTFINAL; + + if(*error) return NULL; + + ctx->user_owns_out_png = 1; + + *len = ctx->bytes_encoded; + + return ctx->out_png; +} + +int spng_set_image_limits(spng_ctx *ctx, uint32_t width, uint32_t height) +{ + if(ctx == NULL) return 1; + + if(width > spng_u32max || height > spng_u32max) return 1; + + ctx->max_width = width; + ctx->max_height = height; + + return 0; +} + +int spng_get_image_limits(spng_ctx *ctx, uint32_t *width, uint32_t *height) +{ + if(ctx == NULL || width == NULL || height == NULL) return 1; + + *width = ctx->max_width; + *height = ctx->max_height; + + return 0; +} + +int spng_set_chunk_limits(spng_ctx *ctx, size_t chunk_size, size_t cache_limit) +{ + if(ctx == NULL || chunk_size > spng_u32max || chunk_size > cache_limit) return 1; + + ctx->max_chunk_size = chunk_size; + + ctx->chunk_cache_limit = cache_limit; + + return 0; +} + +int spng_get_chunk_limits(spng_ctx *ctx, size_t *chunk_size, size_t *cache_limit) +{ + if(ctx == NULL || chunk_size == NULL || cache_limit == NULL) return 1; + + *chunk_size = ctx->max_chunk_size; + + *cache_limit = ctx->chunk_cache_limit; + + return 0; +} + +int spng_set_crc_action(spng_ctx *ctx, int critical, int ancillary) +{ + if(ctx == NULL) return 1; + if(ctx->encode_only) return SPNG_ECTXTYPE; + + if(critical > 2 || critical < 0) return 1; + if(ancillary > 2 || ancillary < 0) return 1; + + if(critical == SPNG_CRC_DISCARD) return 1; + + ctx->crc_action_critical = critical; + ctx->crc_action_ancillary = ancillary; + + return 0; +} + +int spng_set_option(spng_ctx *ctx, enum spng_option option, int value) +{ + if(ctx == NULL) return 1; + if(!ctx->state) return SPNG_EBADSTATE; + + switch(option) + { + case SPNG_KEEP_UNKNOWN_CHUNKS: + { + ctx->keep_unknown = value ? 1 : 0; + break; + } + case SPNG_IMG_COMPRESSION_LEVEL: + { + ctx->image_options.compression_level = value; + break; + } + case SPNG_IMG_WINDOW_BITS: + { + ctx->image_options.window_bits = value; + break; + } + case SPNG_IMG_MEM_LEVEL: + { + ctx->image_options.mem_level = value; + break; + } + case SPNG_IMG_COMPRESSION_STRATEGY: + { + ctx->image_options.strategy = value; + break; + } + case SPNG_TEXT_COMPRESSION_LEVEL: + { + ctx->text_options.compression_level = value; + break; + } + case SPNG_TEXT_WINDOW_BITS: + { + ctx->text_options.window_bits = value; + break; + } + case SPNG_TEXT_MEM_LEVEL: + { + ctx->text_options.mem_level = value; + break; + } + case SPNG_TEXT_COMPRESSION_STRATEGY: + { + ctx->text_options.strategy = value; + break; + } + case SPNG_FILTER_CHOICE: + { + if(value & ~SPNG_FILTER_CHOICE_ALL) return 1; + ctx->encode_flags.filter_choice = value; + break; + } + case SPNG_CHUNK_COUNT_LIMIT: + { + if(value < 0) return 1; + if(value > (int)ctx->chunk_count_total) return 1; + ctx->chunk_count_limit = value; + break; + } + case SPNG_ENCODE_TO_BUFFER: + { + if(value < 0) return 1; + if(!ctx->encode_only) return SPNG_ECTXTYPE; + if(ctx->state >= SPNG_STATE_OUTPUT) return SPNG_EOPSTATE; + + if(!value) break; + + ctx->internal_buffer = 1; + ctx->state = SPNG_STATE_OUTPUT; + + break; + } + default: return 1; + } + + /* Option can no longer be overriden by the library */ + if(option < 32) ctx->optimize_option &= ~(1 << option); + + return 0; +} + +int spng_get_option(spng_ctx *ctx, enum spng_option option, int *value) +{ + if(ctx == NULL || value == NULL) return 1; + if(!ctx->state) return SPNG_EBADSTATE; + + switch(option) + { + case SPNG_KEEP_UNKNOWN_CHUNKS: + { + *value = ctx->keep_unknown; + break; + } + case SPNG_IMG_COMPRESSION_LEVEL: + { + *value = ctx->image_options.compression_level; + break; + } + case SPNG_IMG_WINDOW_BITS: + { + *value = ctx->image_options.window_bits; + break; + } + case SPNG_IMG_MEM_LEVEL: + { + *value = ctx->image_options.mem_level; + break; + } + case SPNG_IMG_COMPRESSION_STRATEGY: + { + *value = ctx->image_options.strategy; + break; + } + case SPNG_TEXT_COMPRESSION_LEVEL: + { + *value = ctx->text_options.compression_level; + break; + } + case SPNG_TEXT_WINDOW_BITS: + { + *value = ctx->text_options.window_bits; + break; + } + case SPNG_TEXT_MEM_LEVEL: + { + *value = ctx->text_options.mem_level; + break; + } + case SPNG_TEXT_COMPRESSION_STRATEGY: + { + *value = ctx->text_options.strategy; + break; + } + case SPNG_FILTER_CHOICE: + { + *value = ctx->encode_flags.filter_choice; + break; + } + case SPNG_CHUNK_COUNT_LIMIT: + { + *value = ctx->chunk_count_limit; + break; + } + case SPNG_ENCODE_TO_BUFFER: + { + if(ctx->internal_buffer) *value = 1; + else *value = 0; + + break; + } + default: return 1; + } + + return 0; +} + +int spng_decoded_image_size(spng_ctx *ctx, int fmt, size_t *len) +{ + if(ctx == NULL || len == NULL) return 1; + + int ret = read_chunks(ctx, 1); + if(ret) return ret; + + ret = check_decode_fmt(&ctx->ihdr, fmt); + if(ret) return ret; + + return calculate_image_size(&ctx->ihdr, fmt, len); +} + +int spng_get_ihdr(spng_ctx *ctx, struct spng_ihdr *ihdr) +{ + if(ctx == NULL) return 1; + int ret = read_chunks(ctx, 1); + if(ret) return ret; + if(ihdr == NULL) return 1; + + *ihdr = ctx->ihdr; + + return 0; +} + +int spng_get_plte(spng_ctx *ctx, struct spng_plte *plte) +{ + SPNG_GET_CHUNK_BOILERPLATE(plte); + + *plte = ctx->plte; + + return 0; +} + +int spng_get_trns(spng_ctx *ctx, struct spng_trns *trns) +{ + SPNG_GET_CHUNK_BOILERPLATE(trns); + + *trns = ctx->trns; + + return 0; +} + +int spng_get_chrm(spng_ctx *ctx, struct spng_chrm *chrm) +{ + SPNG_GET_CHUNK_BOILERPLATE(chrm); + + chrm->white_point_x = (double)ctx->chrm_int.white_point_x / 100000.0; + chrm->white_point_y = (double)ctx->chrm_int.white_point_y / 100000.0; + chrm->red_x = (double)ctx->chrm_int.red_x / 100000.0; + chrm->red_y = (double)ctx->chrm_int.red_y / 100000.0; + chrm->blue_y = (double)ctx->chrm_int.blue_y / 100000.0; + chrm->blue_x = (double)ctx->chrm_int.blue_x / 100000.0; + chrm->green_x = (double)ctx->chrm_int.green_x / 100000.0; + chrm->green_y = (double)ctx->chrm_int.green_y / 100000.0; + + return 0; +} + +int spng_get_chrm_int(spng_ctx *ctx, struct spng_chrm_int *chrm) +{ + SPNG_GET_CHUNK_BOILERPLATE(chrm); + + *chrm = ctx->chrm_int; + + return 0; +} + +int spng_get_gama(spng_ctx *ctx, double *gamma) +{ + double *gama = gamma; + SPNG_GET_CHUNK_BOILERPLATE(gama); + + *gama = (double)ctx->gama / 100000.0; + + return 0; +} + +int spng_get_gama_int(spng_ctx *ctx, uint32_t *gama_int) +{ + uint32_t *gama = gama_int; + SPNG_GET_CHUNK_BOILERPLATE(gama); + + *gama_int = ctx->gama; + + return 0; +} + +int spng_get_iccp(spng_ctx *ctx, struct spng_iccp *iccp) +{ + SPNG_GET_CHUNK_BOILERPLATE(iccp); + + *iccp = ctx->iccp; + + return 0; +} + +int spng_get_sbit(spng_ctx *ctx, struct spng_sbit *sbit) +{ + SPNG_GET_CHUNK_BOILERPLATE(sbit); + + *sbit = ctx->sbit; + + return 0; +} + +int spng_get_srgb(spng_ctx *ctx, uint8_t *rendering_intent) +{ + uint8_t *srgb = rendering_intent; + SPNG_GET_CHUNK_BOILERPLATE(srgb); + + *srgb = ctx->srgb_rendering_intent; + + return 0; +} + +int spng_get_text(spng_ctx *ctx, struct spng_text *text, uint32_t *n_text) +{ + if(ctx == NULL) return 1; + int ret = read_chunks(ctx, 0); + if(ret) return ret; + if(!ctx->stored.text) return SPNG_ECHUNKAVAIL; + if(n_text == NULL) return 1; + + if(text == NULL) + { + *n_text = ctx->n_text; + return 0; + } + + if(*n_text < ctx->n_text) return 1; + + uint32_t i; + for(i=0; i< ctx->n_text; i++) + { + text[i].type = ctx->text_list[i].type; + memcpy(&text[i].keyword, ctx->text_list[i].keyword, strlen(ctx->text_list[i].keyword) + 1); + text[i].compression_method = 0; + text[i].compression_flag = ctx->text_list[i].compression_flag; + text[i].language_tag = ctx->text_list[i].language_tag; + text[i].translated_keyword = ctx->text_list[i].translated_keyword; + text[i].length = ctx->text_list[i].text_length; + text[i].text = ctx->text_list[i].text; + } + + return ret; +} + +int spng_get_bkgd(spng_ctx *ctx, struct spng_bkgd *bkgd) +{ + SPNG_GET_CHUNK_BOILERPLATE(bkgd); + + *bkgd = ctx->bkgd; + + return 0; +} + +int spng_get_hist(spng_ctx *ctx, struct spng_hist *hist) +{ + SPNG_GET_CHUNK_BOILERPLATE(hist); + + *hist = ctx->hist; + + return 0; +} + +int spng_get_phys(spng_ctx *ctx, struct spng_phys *phys) +{ + SPNG_GET_CHUNK_BOILERPLATE(phys); + + *phys = ctx->phys; + + return 0; +} + +int spng_get_splt(spng_ctx *ctx, struct spng_splt *splt, uint32_t *n_splt) +{ + if(ctx == NULL) return 1; + int ret = read_chunks(ctx, 0); + if(ret) return ret; + if(!ctx->stored.splt) return SPNG_ECHUNKAVAIL; + if(n_splt == NULL) return 1; + + if(splt == NULL) + { + *n_splt = ctx->n_splt; + return 0; + } + + if(*n_splt < ctx->n_splt) return 1; + + memcpy(splt, ctx->splt_list, ctx->n_splt * sizeof(struct spng_splt)); + + return 0; +} + +int spng_get_time(spng_ctx *ctx, struct spng_time *time) +{ + SPNG_GET_CHUNK_BOILERPLATE(time); + + *time = ctx->time; + + return 0; +} + +int spng_get_unknown_chunks(spng_ctx *ctx, struct spng_unknown_chunk *chunks, uint32_t *n_chunks) +{ + if(ctx == NULL) return 1; + int ret = read_chunks(ctx, 0); + if(ret) return ret; + if(!ctx->stored.unknown) return SPNG_ECHUNKAVAIL; + if(n_chunks == NULL) return 1; + + if(chunks == NULL) + { + *n_chunks = ctx->n_chunks; + return 0; + } + + if(*n_chunks < ctx->n_chunks) return 1; + + memcpy(chunks, ctx->chunk_list, sizeof(struct spng_unknown_chunk)); + + return 0; +} + +int spng_get_offs(spng_ctx *ctx, struct spng_offs *offs) +{ + SPNG_GET_CHUNK_BOILERPLATE(offs); + + *offs = ctx->offs; + + return 0; +} + +int spng_get_exif(spng_ctx *ctx, struct spng_exif *exif) +{ + SPNG_GET_CHUNK_BOILERPLATE(exif); + + *exif = ctx->exif; + + return 0; +} + +int spng_set_ihdr(spng_ctx *ctx, struct spng_ihdr *ihdr) +{ + SPNG_SET_CHUNK_BOILERPLATE(ihdr); + + if(ctx->stored.ihdr) return 1; + + ret = check_ihdr(ihdr, ctx->max_width, ctx->max_height); + if(ret) return ret; + + ctx->ihdr = *ihdr; + + ctx->stored.ihdr = 1; + ctx->user.ihdr = 1; + + return 0; +} + +int spng_set_plte(spng_ctx *ctx, struct spng_plte *plte) +{ + SPNG_SET_CHUNK_BOILERPLATE(plte); + + if(!ctx->stored.ihdr) return 1; + + if(check_plte(plte, &ctx->ihdr)) return 1; + + ctx->plte.n_entries = plte->n_entries; + + memcpy(ctx->plte.entries, plte->entries, plte->n_entries * sizeof(struct spng_plte_entry)); + + ctx->stored.plte = 1; + ctx->user.plte = 1; + + return 0; +} + +int spng_set_trns(spng_ctx *ctx, struct spng_trns *trns) +{ + SPNG_SET_CHUNK_BOILERPLATE(trns); + + if(!ctx->stored.ihdr) return SPNG_ENOIHDR; + + if(ctx->ihdr.color_type == SPNG_COLOR_TYPE_GRAYSCALE) + { + ctx->trns.gray = trns->gray; + } + else if(ctx->ihdr.color_type == SPNG_COLOR_TYPE_TRUECOLOR) + { + ctx->trns.red = trns->red; + ctx->trns.green = trns->green; + ctx->trns.blue = trns->blue; + } + else if(ctx->ihdr.color_type == SPNG_COLOR_TYPE_INDEXED) + { + if(!ctx->stored.plte) return SPNG_ETRNS_NO_PLTE; + if(trns->n_type3_entries > ctx->plte.n_entries) return 1; + + ctx->trns.n_type3_entries = trns->n_type3_entries; + memcpy(ctx->trns.type3_alpha, trns->type3_alpha, trns->n_type3_entries); + } + else return SPNG_ETRNS_COLOR_TYPE; + + ctx->stored.trns = 1; + ctx->user.trns = 1; + + return 0; +} + +int spng_set_chrm(spng_ctx *ctx, struct spng_chrm *chrm) +{ + SPNG_SET_CHUNK_BOILERPLATE(chrm); + + struct spng_chrm_int chrm_int; + + chrm_int.white_point_x = (uint32_t)(chrm->white_point_x * 100000.0); + chrm_int.white_point_y = (uint32_t)(chrm->white_point_y * 100000.0); + chrm_int.red_x = (uint32_t)(chrm->red_x * 100000.0); + chrm_int.red_y = (uint32_t)(chrm->red_y * 100000.0); + chrm_int.green_x = (uint32_t)(chrm->green_x * 100000.0); + chrm_int.green_y = (uint32_t)(chrm->green_y * 100000.0); + chrm_int.blue_x = (uint32_t)(chrm->blue_x * 100000.0); + chrm_int.blue_y = (uint32_t)(chrm->blue_y * 100000.0); + + if(check_chrm_int(&chrm_int)) return SPNG_ECHRM; + + ctx->chrm_int = chrm_int; + + ctx->stored.chrm = 1; + ctx->user.chrm = 1; + + return 0; +} + +int spng_set_chrm_int(spng_ctx *ctx, struct spng_chrm_int *chrm_int) +{ + SPNG_SET_CHUNK_BOILERPLATE(chrm_int); + + if(check_chrm_int(chrm_int)) return SPNG_ECHRM; + + ctx->chrm_int = *chrm_int; + + ctx->stored.chrm = 1; + ctx->user.chrm = 1; + + return 0; +} + +int spng_set_gama(spng_ctx *ctx, double gamma) +{ + SPNG_SET_CHUNK_BOILERPLATE(ctx); + + uint32_t gama = gamma * 100000.0; + + if(!gama) return 1; + if(gama > spng_u32max) return 1; + + ctx->gama = gama; + + ctx->stored.gama = 1; + ctx->user.gama = 1; + + return 0; +} + +int spng_set_gama_int(spng_ctx *ctx, uint32_t gamma) +{ + SPNG_SET_CHUNK_BOILERPLATE(ctx); + + if(!gamma) return 1; + if(gamma > spng_u32max) return 1; + + ctx->gama = gamma; + + ctx->stored.gama = 1; + ctx->user.gama = 1; + + return 0; +} + +int spng_set_iccp(spng_ctx *ctx, struct spng_iccp *iccp) +{ + SPNG_SET_CHUNK_BOILERPLATE(iccp); + + if(check_png_keyword(iccp->profile_name)) return SPNG_EICCP_NAME; + if(!iccp->profile_len) return SPNG_ECHUNK_SIZE; + if(iccp->profile_len > spng_u32max) return SPNG_ECHUNK_STDLEN; + + if(ctx->iccp.profile && !ctx->user.iccp) spng__free(ctx, ctx->iccp.profile); + + ctx->iccp = *iccp; + + ctx->stored.iccp = 1; + ctx->user.iccp = 1; + + return 0; +} + +int spng_set_sbit(spng_ctx *ctx, struct spng_sbit *sbit) +{ + SPNG_SET_CHUNK_BOILERPLATE(sbit); + + if(check_sbit(sbit, &ctx->ihdr)) return 1; + + if(!ctx->stored.ihdr) return 1; + + ctx->sbit = *sbit; + + ctx->stored.sbit = 1; + ctx->user.sbit = 1; + + return 0; +} + +int spng_set_srgb(spng_ctx *ctx, uint8_t rendering_intent) +{ + SPNG_SET_CHUNK_BOILERPLATE(ctx); + + if(rendering_intent > 3) return 1; + + ctx->srgb_rendering_intent = rendering_intent; + + ctx->stored.srgb = 1; + ctx->user.srgb = 1; + + return 0; +} + +int spng_set_text(spng_ctx *ctx, struct spng_text *text, uint32_t n_text) +{ + if(!n_text) return 1; + SPNG_SET_CHUNK_BOILERPLATE(text); + + uint32_t i; + for(i=0; i < n_text; i++) + { + if(check_png_keyword(text[i].keyword)) return SPNG_ETEXT_KEYWORD; + if(!text[i].length) return 1; + if(text[i].length > UINT_MAX) return 1; + if(text[i].text == NULL) return 1; + + if(text[i].type == SPNG_TEXT) + { + if(ctx->strict && check_png_text(text[i].text, text[i].length)) return 1; + } + else if(text[i].type == SPNG_ZTXT) + { + if(ctx->strict && check_png_text(text[i].text, text[i].length)) return 1; + + if(text[i].compression_method != 0) return SPNG_EZTXT_COMPRESSION_METHOD; + } + else if(text[i].type == SPNG_ITXT) + { + if(text[i].compression_flag > 1) return SPNG_EITXT_COMPRESSION_FLAG; + if(text[i].compression_method != 0) return SPNG_EITXT_COMPRESSION_METHOD; + if(text[i].language_tag == NULL) return SPNG_EITXT_LANG_TAG; + if(text[i].translated_keyword == NULL) return SPNG_EITXT_TRANSLATED_KEY; + } + else return 1; + + } + + struct spng_text2 *text_list = spng__calloc(ctx, sizeof(struct spng_text2), n_text); + + if(!text_list) return SPNG_EMEM; + + if(ctx->text_list != NULL) + { + for(i=0; i < ctx->n_text; i++) + { + if(ctx->user.text) break; + + spng__free(ctx, ctx->text_list[i].keyword); + if(ctx->text_list[i].compression_flag) spng__free(ctx, ctx->text_list[i].text); + } + spng__free(ctx, ctx->text_list); + } + + for(i=0; i < n_text; i++) + { + text_list[i].type = text[i].type; + /* Prevent issues with spng_text.keyword[80] going out of scope */ + text_list[i].keyword = text_list[i].user_keyword_storage; + memcpy(text_list[i].user_keyword_storage, text[i].keyword, strlen(text[i].keyword)); + text_list[i].text = text[i].text; + text_list[i].text_length = text[i].length; + + if(text[i].type == SPNG_ZTXT) + { + text_list[i].compression_flag = 1; + } + else if(text[i].type == SPNG_ITXT) + { + text_list[i].compression_flag = text[i].compression_flag; + text_list[i].language_tag = text[i].language_tag; + text_list[i].translated_keyword = text[i].translated_keyword; + } + } + + ctx->text_list = text_list; + ctx->n_text = n_text; + + ctx->stored.text = 1; + ctx->user.text = 1; + + return 0; +} + +int spng_set_bkgd(spng_ctx *ctx, struct spng_bkgd *bkgd) +{ + SPNG_SET_CHUNK_BOILERPLATE(bkgd); + + if(!ctx->stored.ihdr) return 1; + + if(ctx->ihdr.color_type == 0 || ctx->ihdr.color_type == 4) + { + ctx->bkgd.gray = bkgd->gray; + } + else if(ctx->ihdr.color_type == 2 || ctx->ihdr.color_type == 6) + { + ctx->bkgd.red = bkgd->red; + ctx->bkgd.green = bkgd->green; + ctx->bkgd.blue = bkgd->blue; + } + else if(ctx->ihdr.color_type == 3) + { + if(!ctx->stored.plte) return SPNG_EBKGD_NO_PLTE; + if(bkgd->plte_index >= ctx->plte.n_entries) return SPNG_EBKGD_PLTE_IDX; + + ctx->bkgd.plte_index = bkgd->plte_index; + } + + ctx->stored.bkgd = 1; + ctx->user.bkgd = 1; + + return 0; +} + +int spng_set_hist(spng_ctx *ctx, struct spng_hist *hist) +{ + SPNG_SET_CHUNK_BOILERPLATE(hist); + + if(!ctx->stored.plte) return SPNG_EHIST_NO_PLTE; + + ctx->hist = *hist; + + ctx->stored.hist = 1; + ctx->user.hist = 1; + + return 0; +} + +int spng_set_phys(spng_ctx *ctx, struct spng_phys *phys) +{ + SPNG_SET_CHUNK_BOILERPLATE(phys); + + if(check_phys(phys)) return SPNG_EPHYS; + + ctx->phys = *phys; + + ctx->stored.phys = 1; + ctx->user.phys = 1; + + return 0; +} + +int spng_set_splt(spng_ctx *ctx, struct spng_splt *splt, uint32_t n_splt) +{ + if(!n_splt) return 1; + SPNG_SET_CHUNK_BOILERPLATE(splt); + + uint32_t i; + for(i=0; i < n_splt; i++) + { + if(check_png_keyword(splt[i].name)) return SPNG_ESPLT_NAME; + if( !(splt[i].sample_depth == 8 || splt[i].sample_depth == 16) ) return SPNG_ESPLT_DEPTH; + } + + if(ctx->stored.splt && !ctx->user.splt) + { + for(i=0; i < ctx->n_splt; i++) + { + if(ctx->splt_list[i].entries != NULL) spng__free(ctx, ctx->splt_list[i].entries); + } + spng__free(ctx, ctx->splt_list); + } + + ctx->splt_list = splt; + ctx->n_splt = n_splt; + + ctx->stored.splt = 1; + ctx->user.splt = 1; + + return 0; +} + +int spng_set_time(spng_ctx *ctx, struct spng_time *time) +{ + SPNG_SET_CHUNK_BOILERPLATE(time); + + if(check_time(time)) return SPNG_ETIME; + + ctx->time = *time; + + ctx->stored.time = 1; + ctx->user.time = 1; + + return 0; +} + +int spng_set_unknown_chunks(spng_ctx *ctx, struct spng_unknown_chunk *chunks, uint32_t n_chunks) +{ + if(!n_chunks) return 1; + SPNG_SET_CHUNK_BOILERPLATE(chunks); + + uint32_t i; + for(i=0; i < n_chunks; i++) + { + if(chunks[i].length > spng_u32max) return SPNG_ECHUNK_STDLEN; + if(chunks[i].length && chunks[i].data == NULL) return 1; + + switch(chunks[i].location) + { + case SPNG_AFTER_IHDR: + case SPNG_AFTER_PLTE: + case SPNG_AFTER_IDAT: + break; + default: return SPNG_ECHUNK_POS; + } + } + + if(ctx->stored.unknown && !ctx->user.unknown) + { + for(i=0; i < ctx->n_chunks; i++) + { + spng__free(ctx, ctx->chunk_list[i].data); + } + spng__free(ctx, ctx->chunk_list); + } + + ctx->chunk_list = chunks; + ctx->n_chunks = n_chunks; + + ctx->stored.unknown = 1; + ctx->user.unknown = 1; + + return 0; +} + +int spng_set_offs(spng_ctx *ctx, struct spng_offs *offs) +{ + SPNG_SET_CHUNK_BOILERPLATE(offs); + + if(check_offs(offs)) return SPNG_EOFFS; + + ctx->offs = *offs; + + ctx->stored.offs = 1; + ctx->user.offs = 1; + + return 0; +} + +int spng_set_exif(spng_ctx *ctx, struct spng_exif *exif) +{ + SPNG_SET_CHUNK_BOILERPLATE(exif); + + if(check_exif(exif)) return SPNG_EEXIF; + + if(ctx->exif.data != NULL && !ctx->user.exif) spng__free(ctx, ctx->exif.data); + + ctx->exif = *exif; + + ctx->stored.exif = 1; + ctx->user.exif = 1; + + return 0; +} + +const char *spng_strerror(int err) +{ + switch(err) + { + case SPNG_IO_EOF: return "end of stream"; + case SPNG_IO_ERROR: return "stream error"; + case SPNG_OK: return "success"; + case SPNG_EINVAL: return "invalid argument"; + case SPNG_EMEM: return "out of memory"; + case SPNG_EOVERFLOW: return "arithmetic overflow"; + case SPNG_ESIGNATURE: return "invalid signature"; + case SPNG_EWIDTH: return "invalid image width"; + case SPNG_EHEIGHT: return "invalid image height"; + case SPNG_EUSER_WIDTH: return "image width exceeds user limit"; + case SPNG_EUSER_HEIGHT: return "image height exceeds user limit"; + case SPNG_EBIT_DEPTH: return "invalid bit depth"; + case SPNG_ECOLOR_TYPE: return "invalid color type"; + case SPNG_ECOMPRESSION_METHOD: return "invalid compression method"; + case SPNG_EFILTER_METHOD: return "invalid filter method"; + case SPNG_EINTERLACE_METHOD: return "invalid interlace method"; + case SPNG_EIHDR_SIZE: return "invalid IHDR chunk size"; + case SPNG_ENOIHDR: return "missing IHDR chunk"; + case SPNG_ECHUNK_POS: return "invalid chunk position"; + case SPNG_ECHUNK_SIZE: return "invalid chunk length"; + case SPNG_ECHUNK_CRC: return "invalid chunk checksum"; + case SPNG_ECHUNK_TYPE: return "invalid chunk type"; + case SPNG_ECHUNK_UNKNOWN_CRITICAL: return "unknown critical chunk"; + case SPNG_EDUP_PLTE: return "duplicate PLTE chunk"; + case SPNG_EDUP_CHRM: return "duplicate cHRM chunk"; + case SPNG_EDUP_GAMA: return "duplicate gAMA chunk"; + case SPNG_EDUP_ICCP: return "duplicate iCCP chunk"; + case SPNG_EDUP_SBIT: return "duplicate sBIT chunk"; + case SPNG_EDUP_SRGB: return "duplicate sRGB chunk"; + case SPNG_EDUP_BKGD: return "duplicate bKGD chunk"; + case SPNG_EDUP_HIST: return "duplicate hIST chunk"; + case SPNG_EDUP_TRNS: return "duplicate tRNS chunk"; + case SPNG_EDUP_PHYS: return "duplicate pHYs chunk"; + case SPNG_EDUP_TIME: return "duplicate tIME chunk"; + case SPNG_EDUP_OFFS: return "duplicate oFFs chunk"; + case SPNG_EDUP_EXIF: return "duplicate eXIf chunk"; + case SPNG_ECHRM: return "invalid cHRM chunk"; + case SPNG_EPLTE_IDX: return "invalid palette (PLTE) index"; + case SPNG_ETRNS_COLOR_TYPE: return "tRNS chunk with incompatible color type"; + case SPNG_ETRNS_NO_PLTE: return "missing palette (PLTE) for tRNS chunk"; + case SPNG_EGAMA: return "invalid gAMA chunk"; + case SPNG_EICCP_NAME: return "invalid iCCP profile name"; + case SPNG_EICCP_COMPRESSION_METHOD: return "invalid iCCP compression method"; + case SPNG_ESBIT: return "invalid sBIT chunk"; + case SPNG_ESRGB: return "invalid sRGB chunk"; + case SPNG_ETEXT: return "invalid tEXt chunk"; + case SPNG_ETEXT_KEYWORD: return "invalid tEXt keyword"; + case SPNG_EZTXT: return "invalid zTXt chunk"; + case SPNG_EZTXT_COMPRESSION_METHOD: return "invalid zTXt compression method"; + case SPNG_EITXT: return "invalid iTXt chunk"; + case SPNG_EITXT_COMPRESSION_FLAG: return "invalid iTXt compression flag"; + case SPNG_EITXT_COMPRESSION_METHOD: return "invalid iTXt compression method"; + case SPNG_EITXT_LANG_TAG: return "invalid iTXt language tag"; + case SPNG_EITXT_TRANSLATED_KEY: return "invalid iTXt translated key"; + case SPNG_EBKGD_NO_PLTE: return "missing palette for bKGD chunk"; + case SPNG_EBKGD_PLTE_IDX: return "invalid palette index for bKGD chunk"; + case SPNG_EHIST_NO_PLTE: return "missing palette for hIST chunk"; + case SPNG_EPHYS: return "invalid pHYs chunk"; + case SPNG_ESPLT_NAME: return "invalid suggested palette name"; + case SPNG_ESPLT_DUP_NAME: return "duplicate suggested palette (sPLT) name"; + case SPNG_ESPLT_DEPTH: return "invalid suggested palette (sPLT) sample depth"; + case SPNG_ETIME: return "invalid tIME chunk"; + case SPNG_EOFFS: return "invalid oFFs chunk"; + case SPNG_EEXIF: return "invalid eXIf chunk"; + case SPNG_EIDAT_TOO_SHORT: return "IDAT stream too short"; + case SPNG_EIDAT_STREAM: return "IDAT stream error"; + case SPNG_EZLIB: return "zlib error"; + case SPNG_EFILTER: return "invalid scanline filter"; + case SPNG_EBUFSIZ: return "invalid buffer size"; + case SPNG_EIO: return "i/o error"; + case SPNG_EOF: return "end of file"; + case SPNG_EBUF_SET: return "buffer already set"; + case SPNG_EBADSTATE: return "non-recoverable state"; + case SPNG_EFMT: return "invalid format"; + case SPNG_EFLAGS: return "invalid flags"; + case SPNG_ECHUNKAVAIL: return "chunk not available"; + case SPNG_ENCODE_ONLY: return "encode only context"; + case SPNG_EOI: return "reached end-of-image state"; + case SPNG_ENOPLTE: return "missing PLTE for indexed image"; + case SPNG_ECHUNK_LIMITS: return "reached chunk/cache limits"; + case SPNG_EZLIB_INIT: return "zlib init error"; + case SPNG_ECHUNK_STDLEN: return "chunk exceeds maximum standard length"; + case SPNG_EINTERNAL: return "internal error"; + case SPNG_ECTXTYPE: return "invalid operation for context type"; + case SPNG_ENOSRC: return "source PNG not set"; + case SPNG_ENODST: return "PNG output not set"; + case SPNG_EOPSTATE: return "invalid operation for state"; + case SPNG_ENOTFINAL: return "PNG not finalized"; + default: return "unknown error"; + } +} + +const char *spng_version_string(void) +{ + return SPNG_VERSION_STRING; +} + +#if defined(_MSC_VER) + #pragma warning(pop) +#endif + +/* The following SIMD optimizations are derived from libpng source code. */ + +/* +* PNG Reference Library License version 2 +* +* Copyright (c) 1995-2019 The PNG Reference Library Authors. +* Copyright (c) 2018-2019 Cosmin Truta. +* Copyright (c) 2000-2002, 2004, 2006-2018 Glenn Randers-Pehrson. +* Copyright (c) 1996-1997 Andreas Dilger. +* Copyright (c) 1995-1996 Guy Eric Schalnat, Group 42, Inc. +* +* The software is supplied "as is", without warranty of any kind, +* express or implied, including, without limitation, the warranties +* of merchantability, fitness for a particular purpose, title, and +* non-infringement. In no event shall the Copyright owners, or +* anyone distributing the software, be liable for any damages or +* other liability, whether in contract, tort or otherwise, arising +* from, out of, or in connection with the software, or the use or +* other dealings in the software, even if advised of the possibility +* of such damage. +* +* Permission is hereby granted to use, copy, modify, and distribute +* this software, or portions hereof, for any purpose, without fee, +* subject to the following restrictions: +* +* 1. The origin of this software must not be misrepresented; you +* must not claim that you wrote the original software. If you +* use this software in a product, an acknowledgment in the product +* documentation would be appreciated, but is not required. +* +* 2. Altered source versions must be plainly marked as such, and must +* not be misrepresented as being the original software. +* +* 3. This Copyright notice may not be removed or altered from any +* source or altered source distribution. +*/ + +#if defined(SPNG_X86) + +#ifndef SPNG_SSE + #define SPNG_SSE 1 +#endif + +#if defined(__GNUC__) && !defined(__clang__) + #if SPNG_SSE == 3 + #pragma GCC target("ssse3") + #elif SPNG_SSE == 4 + #pragma GCC target("sse4.1") + #else + #pragma GCC target("sse2") + #endif +#endif + +/* SSE2 optimised filter functions + * Derived from filter_neon_intrinsics.c + * + * Copyright (c) 2018 Cosmin Truta + * Copyright (c) 2016-2017 Glenn Randers-Pehrson + * Written by Mike Klein and Matt Sarett + * Derived from arm/filter_neon_intrinsics.c + * + * This code is derived from libpng source code. + * For conditions of distribution and use, see the disclaimer + * and license above. + */ + +#include +#include +#include + +/* Functions in this file look at most 3 pixels (a,b,c) to predict the 4th (d). + * They're positioned like this: + * prev: c b + * row: a d + * The Sub filter predicts d=a, Avg d=(a+b)/2, and Paeth predicts d to be + * whichever of a, b, or c is closest to p=a+b-c. + */ + +static __m128i load4(const void* p) +{ + int tmp; + memcpy(&tmp, p, sizeof(tmp)); + return _mm_cvtsi32_si128(tmp); +} + +static void store4(void* p, __m128i v) +{ + int tmp = _mm_cvtsi128_si32(v); + memcpy(p, &tmp, sizeof(int)); +} + +static __m128i load3(const void* p) +{ + uint32_t tmp = 0; + memcpy(&tmp, p, 3); + return _mm_cvtsi32_si128(tmp); +} + +static void store3(void* p, __m128i v) +{ + int tmp = _mm_cvtsi128_si32(v); + memcpy(p, &tmp, 3); +} + +static void defilter_sub3(size_t rowbytes, unsigned char *row) +{ + /* The Sub filter predicts each pixel as the previous pixel, a. + * There is no pixel to the left of the first pixel. It's encoded directly. + * That works with our main loop if we just say that left pixel was zero. + */ + size_t rb = rowbytes; + + __m128i a, d = _mm_setzero_si128(); + + while(rb >= 4) + { + a = d; d = load4(row); + d = _mm_add_epi8(d, a); + store3(row, d); + + row += 3; + rb -= 3; + } + + if(rb > 0) + { + a = d; d = load3(row); + d = _mm_add_epi8(d, a); + store3(row, d); + } +} + +static void defilter_sub4(size_t rowbytes, unsigned char *row) +{ + /* The Sub filter predicts each pixel as the previous pixel, a. + * There is no pixel to the left of the first pixel. It's encoded directly. + * That works with our main loop if we just say that left pixel was zero. + */ + size_t rb = rowbytes+4; + + __m128i a, d = _mm_setzero_si128(); + + while(rb > 4) + { + a = d; d = load4(row); + d = _mm_add_epi8(d, a); + store4(row, d); + + row += 4; + rb -= 4; + } +} + +static void defilter_avg3(size_t rowbytes, unsigned char *row, const unsigned char *prev) +{ + /* The Avg filter predicts each pixel as the (truncated) average of a and b. + * There's no pixel to the left of the first pixel. Luckily, it's + * predicted to be half of the pixel above it. So again, this works + * perfectly with our loop if we make sure a starts at zero. + */ + + size_t rb = rowbytes; + + const __m128i zero = _mm_setzero_si128(); + + __m128i b; + __m128i a, d = zero; + + while(rb >= 4) + { + __m128i avg; + b = load4(prev); + a = d; d = load4(row ); + + /* PNG requires a truncating average, so we can't just use _mm_avg_epu8 */ + avg = _mm_avg_epu8(a,b); + /* ...but we can fix it up by subtracting off 1 if it rounded up. */ + avg = _mm_sub_epi8(avg, _mm_and_si128(_mm_xor_si128(a, b), + _mm_set1_epi8(1))); + d = _mm_add_epi8(d, avg); + store3(row, d); + + prev += 3; + row += 3; + rb -= 3; + } + + if(rb > 0) + { + __m128i avg; + b = load3(prev); + a = d; d = load3(row ); + + /* PNG requires a truncating average, so we can't just use _mm_avg_epu8 */ + avg = _mm_avg_epu8(a, b); + /* ...but we can fix it up by subtracting off 1 if it rounded up. */ + avg = _mm_sub_epi8(avg, _mm_and_si128(_mm_xor_si128(a, b), + _mm_set1_epi8(1))); + + d = _mm_add_epi8(d, avg); + store3(row, d); + } +} + +static void defilter_avg4(size_t rowbytes, unsigned char *row, const unsigned char *prev) +{ + /* The Avg filter predicts each pixel as the (truncated) average of a and b. + * There's no pixel to the left of the first pixel. Luckily, it's + * predicted to be half of the pixel above it. So again, this works + * perfectly with our loop if we make sure a starts at zero. + */ + size_t rb = rowbytes+4; + + const __m128i zero = _mm_setzero_si128(); + __m128i b; + __m128i a, d = zero; + + while(rb > 4) + { + __m128i avg; + b = load4(prev); + a = d; d = load4(row ); + + /* PNG requires a truncating average, so we can't just use _mm_avg_epu8 */ + avg = _mm_avg_epu8(a,b); + /* ...but we can fix it up by subtracting off 1 if it rounded up. */ + avg = _mm_sub_epi8(avg, _mm_and_si128(_mm_xor_si128(a, b), + _mm_set1_epi8(1))); + + d = _mm_add_epi8(d, avg); + store4(row, d); + + prev += 4; + row += 4; + rb -= 4; + } +} + +/* Returns |x| for 16-bit lanes. */ +#if (SPNG_SSE >= 3) && !defined(_MSC_VER) +__attribute__((target("ssse3"))) +#endif +static __m128i abs_i16(__m128i x) +{ +#if SPNG_SSE >= 3 + return _mm_abs_epi16(x); +#else + /* Read this all as, return x<0 ? -x : x. + * To negate two's complement, you flip all the bits then add 1. + */ + __m128i is_negative = _mm_cmplt_epi16(x, _mm_setzero_si128()); + + /* Flip negative lanes. */ + x = _mm_xor_si128(x, is_negative); + + /* +1 to negative lanes, else +0. */ + x = _mm_sub_epi16(x, is_negative); + return x; +#endif +} + +/* Bytewise c ? t : e. */ +static __m128i if_then_else(__m128i c, __m128i t, __m128i e) +{ +#if SPNG_SSE >= 4 + return _mm_blendv_epi8(e, t, c); +#else + return _mm_or_si128(_mm_and_si128(c, t), _mm_andnot_si128(c, e)); +#endif +} + +static void defilter_paeth3(size_t rowbytes, unsigned char *row, const unsigned char *prev) +{ + /* Paeth tries to predict pixel d using the pixel to the left of it, a, + * and two pixels from the previous row, b and c: + * prev: c b + * row: a d + * The Paeth function predicts d to be whichever of a, b, or c is nearest to + * p=a+b-c. + * + * The first pixel has no left context, and so uses an Up filter, p = b. + * This works naturally with our main loop's p = a+b-c if we force a and c + * to zero. + * Here we zero b and d, which become c and a respectively at the start of + * the loop. + */ + size_t rb = rowbytes; + const __m128i zero = _mm_setzero_si128(); + __m128i c, b = zero, + a, d = zero; + + while(rb >= 4) + { + /* It's easiest to do this math (particularly, deal with pc) with 16-bit + * intermediates. + */ + __m128i pa,pb,pc,smallest,nearest; + c = b; b = _mm_unpacklo_epi8(load4(prev), zero); + a = d; d = _mm_unpacklo_epi8(load4(row ), zero); + + /* (p-a) == (a+b-c - a) == (b-c) */ + + pa = _mm_sub_epi16(b, c); + + /* (p-b) == (a+b-c - b) == (a-c) */ + pb = _mm_sub_epi16(a, c); + + /* (p-c) == (a+b-c - c) == (a+b-c-c) == (b-c)+(a-c) */ + pc = _mm_add_epi16(pa, pb); + + pa = abs_i16(pa); /* |p-a| */ + pb = abs_i16(pb); /* |p-b| */ + pc = abs_i16(pc); /* |p-c| */ + + smallest = _mm_min_epi16(pc, _mm_min_epi16(pa, pb)); + + /* Paeth breaks ties favoring a over b over c. */ + nearest = if_then_else(_mm_cmpeq_epi16(smallest, pa), a, + if_then_else(_mm_cmpeq_epi16(smallest, pb), b, c)); + + /* Note `_epi8`: we need addition to wrap modulo 255. */ + d = _mm_add_epi8(d, nearest); + store3(row, _mm_packus_epi16(d, d)); + + prev += 3; + row += 3; + rb -= 3; + } + + if(rb > 0) + { + /* It's easiest to do this math (particularly, deal with pc) with 16-bit + * intermediates. + */ + __m128i pa, pb, pc, smallest, nearest; + c = b; b = _mm_unpacklo_epi8(load3(prev), zero); + a = d; d = _mm_unpacklo_epi8(load3(row ), zero); + + /* (p-a) == (a+b-c - a) == (b-c) */ + pa = _mm_sub_epi16(b, c); + + /* (p-b) == (a+b-c - b) == (a-c) */ + pb = _mm_sub_epi16(a, c); + + /* (p-c) == (a+b-c - c) == (a+b-c-c) == (b-c)+(a-c) */ + pc = _mm_add_epi16(pa, pb); + + pa = abs_i16(pa); /* |p-a| */ + pb = abs_i16(pb); /* |p-b| */ + pc = abs_i16(pc); /* |p-c| */ + + smallest = _mm_min_epi16(pc, _mm_min_epi16(pa, pb)); + + /* Paeth breaks ties favoring a over b over c. */ + nearest = if_then_else(_mm_cmpeq_epi16(smallest, pa), a, + if_then_else(_mm_cmpeq_epi16(smallest, pb), b, c)); + + /* Note `_epi8`: we need addition to wrap modulo 255. */ + d = _mm_add_epi8(d, nearest); + store3(row, _mm_packus_epi16(d, d)); + } +} + +static void defilter_paeth4(size_t rowbytes, unsigned char *row, const unsigned char *prev) +{ + /* Paeth tries to predict pixel d using the pixel to the left of it, a, + * and two pixels from the previous row, b and c: + * prev: c b + * row: a d + * The Paeth function predicts d to be whichever of a, b, or c is nearest to + * p=a+b-c. + * + * The first pixel has no left context, and so uses an Up filter, p = b. + * This works naturally with our main loop's p = a+b-c if we force a and c + * to zero. + * Here we zero b and d, which become c and a respectively at the start of + * the loop. + */ + size_t rb = rowbytes+4; + + const __m128i zero = _mm_setzero_si128(); + __m128i pa, pb, pc, smallest, nearest; + __m128i c, b = zero, + a, d = zero; + + while(rb > 4) + { + /* It's easiest to do this math (particularly, deal with pc) with 16-bit + * intermediates. + */ + c = b; b = _mm_unpacklo_epi8(load4(prev), zero); + a = d; d = _mm_unpacklo_epi8(load4(row ), zero); + + /* (p-a) == (a+b-c - a) == (b-c) */ + pa = _mm_sub_epi16(b, c); + + /* (p-b) == (a+b-c - b) == (a-c) */ + pb = _mm_sub_epi16(a, c); + + /* (p-c) == (a+b-c - c) == (a+b-c-c) == (b-c)+(a-c) */ + pc = _mm_add_epi16(pa, pb); + + pa = abs_i16(pa); /* |p-a| */ + pb = abs_i16(pb); /* |p-b| */ + pc = abs_i16(pc); /* |p-c| */ + + smallest = _mm_min_epi16(pc, _mm_min_epi16(pa, pb)); + + /* Paeth breaks ties favoring a over b over c. */ + nearest = if_then_else(_mm_cmpeq_epi16(smallest, pa), a, + if_then_else(_mm_cmpeq_epi16(smallest, pb), b, c)); + + /* Note `_epi8`: we need addition to wrap modulo 255. */ + d = _mm_add_epi8(d, nearest); + store4(row, _mm_packus_epi16(d, d)); + + prev += 4; + row += 4; + rb -= 4; + } +} + +#endif /* SPNG_X86 */ + + +#if defined(SPNG_ARM) + +/* NEON optimised filter functions + * Derived from filter_neon_intrinsics.c + * + * Copyright (c) 2018 Cosmin Truta + * Copyright (c) 2014,2016 Glenn Randers-Pehrson + * Written by James Yu , October 2013. + * Based on filter_neon.S, written by Mans Rullgard, 2011. + * + * This code is derived from libpng source code. + * For conditions of distribution and use, see the disclaimer + * and license in this file. + */ + +#define png_aligncast(type, value) ((void*)(value)) +#define png_aligncastconst(type, value) ((const void*)(value)) + +/* libpng row pointers are not necessarily aligned to any particular boundary, + * however this code will only work with appropriate alignment. mips/mips_init.c + * checks for this (and will not compile unless it is done). This code uses + * variants of png_aligncast to avoid compiler warnings. + */ +#define png_ptr(type,pointer) png_aligncast(type *,pointer) +#define png_ptrc(type,pointer) png_aligncastconst(const type *,pointer) + +/* The following relies on a variable 'temp_pointer' being declared with type + * 'type'. This is written this way just to hide the GCC strict aliasing + * warning; note that the code is safe because there never is an alias between + * the input and output pointers. + */ +#define png_ldr(type,pointer)\ + (temp_pointer = png_ptr(type,pointer), *temp_pointer) + + +#if defined(_MSC_VER) && !defined(__clang__) && defined(_M_ARM64) + #include +#else + #include +#endif + +static void defilter_sub3(size_t rowbytes, unsigned char *row) +{ + unsigned char *rp = row; + unsigned char *rp_stop = row + rowbytes; + + uint8x16_t vtmp = vld1q_u8(rp); + uint8x8x2_t *vrpt = png_ptr(uint8x8x2_t, &vtmp); + uint8x8x2_t vrp = *vrpt; + + uint8x8x4_t vdest; + vdest.val[3] = vdup_n_u8(0); + + for (; rp < rp_stop;) + { + uint8x8_t vtmp1, vtmp2; + uint32x2_t *temp_pointer; + + vtmp1 = vext_u8(vrp.val[0], vrp.val[1], 3); + vdest.val[0] = vadd_u8(vdest.val[3], vrp.val[0]); + vtmp2 = vext_u8(vrp.val[0], vrp.val[1], 6); + vdest.val[1] = vadd_u8(vdest.val[0], vtmp1); + + vtmp1 = vext_u8(vrp.val[1], vrp.val[1], 1); + vdest.val[2] = vadd_u8(vdest.val[1], vtmp2); + vdest.val[3] = vadd_u8(vdest.val[2], vtmp1); + + vtmp = vld1q_u8(rp + 12); + vrpt = png_ptr(uint8x8x2_t, &vtmp); + vrp = *vrpt; + + vst1_lane_u32(png_ptr(uint32_t,rp), png_ldr(uint32x2_t,&vdest.val[0]), 0); + rp += 3; + vst1_lane_u32(png_ptr(uint32_t,rp), png_ldr(uint32x2_t,&vdest.val[1]), 0); + rp += 3; + vst1_lane_u32(png_ptr(uint32_t,rp), png_ldr(uint32x2_t,&vdest.val[2]), 0); + rp += 3; + vst1_lane_u32(png_ptr(uint32_t,rp), png_ldr(uint32x2_t,&vdest.val[3]), 0); + rp += 3; + } +} + +static void defilter_sub4(size_t rowbytes, unsigned char *row) +{ + unsigned char *rp = row; + unsigned char *rp_stop = row + rowbytes; + + uint8x8x4_t vdest; + vdest.val[3] = vdup_n_u8(0); + + for (; rp < rp_stop; rp += 16) + { + uint32x2x4_t vtmp = vld4_u32(png_ptr(uint32_t,rp)); + uint8x8x4_t *vrpt = png_ptr(uint8x8x4_t,&vtmp); + uint8x8x4_t vrp = *vrpt; + uint32x2x4_t *temp_pointer; + uint32x2x4_t vdest_val; + + vdest.val[0] = vadd_u8(vdest.val[3], vrp.val[0]); + vdest.val[1] = vadd_u8(vdest.val[0], vrp.val[1]); + vdest.val[2] = vadd_u8(vdest.val[1], vrp.val[2]); + vdest.val[3] = vadd_u8(vdest.val[2], vrp.val[3]); + + vdest_val = png_ldr(uint32x2x4_t, &vdest); + vst4_lane_u32(png_ptr(uint32_t,rp), vdest_val, 0); + } +} + +static void defilter_avg3(size_t rowbytes, unsigned char *row, const unsigned char *prev_row) +{ + unsigned char *rp = row; + const unsigned char *pp = prev_row; + unsigned char *rp_stop = row + rowbytes; + + uint8x16_t vtmp; + uint8x8x2_t *vrpt; + uint8x8x2_t vrp; + uint8x8x4_t vdest; + vdest.val[3] = vdup_n_u8(0); + + vtmp = vld1q_u8(rp); + vrpt = png_ptr(uint8x8x2_t,&vtmp); + vrp = *vrpt; + + for (; rp < rp_stop; pp += 12) + { + uint8x8_t vtmp1, vtmp2, vtmp3; + + uint8x8x2_t *vppt; + uint8x8x2_t vpp; + + uint32x2_t *temp_pointer; + + vtmp = vld1q_u8(pp); + vppt = png_ptr(uint8x8x2_t,&vtmp); + vpp = *vppt; + + vtmp1 = vext_u8(vrp.val[0], vrp.val[1], 3); + vdest.val[0] = vhadd_u8(vdest.val[3], vpp.val[0]); + vdest.val[0] = vadd_u8(vdest.val[0], vrp.val[0]); + + vtmp2 = vext_u8(vpp.val[0], vpp.val[1], 3); + vtmp3 = vext_u8(vrp.val[0], vrp.val[1], 6); + vdest.val[1] = vhadd_u8(vdest.val[0], vtmp2); + vdest.val[1] = vadd_u8(vdest.val[1], vtmp1); + + vtmp2 = vext_u8(vpp.val[0], vpp.val[1], 6); + vtmp1 = vext_u8(vrp.val[1], vrp.val[1], 1); + + vtmp = vld1q_u8(rp + 12); + vrpt = png_ptr(uint8x8x2_t,&vtmp); + vrp = *vrpt; + + vdest.val[2] = vhadd_u8(vdest.val[1], vtmp2); + vdest.val[2] = vadd_u8(vdest.val[2], vtmp3); + + vtmp2 = vext_u8(vpp.val[1], vpp.val[1], 1); + + vdest.val[3] = vhadd_u8(vdest.val[2], vtmp2); + vdest.val[3] = vadd_u8(vdest.val[3], vtmp1); + + vst1_lane_u32(png_ptr(uint32_t,rp), png_ldr(uint32x2_t,&vdest.val[0]), 0); + rp += 3; + vst1_lane_u32(png_ptr(uint32_t,rp), png_ldr(uint32x2_t,&vdest.val[1]), 0); + rp += 3; + vst1_lane_u32(png_ptr(uint32_t,rp), png_ldr(uint32x2_t,&vdest.val[2]), 0); + rp += 3; + vst1_lane_u32(png_ptr(uint32_t,rp), png_ldr(uint32x2_t,&vdest.val[3]), 0); + rp += 3; + } +} + +static void defilter_avg4(size_t rowbytes, unsigned char *row, const unsigned char *prev_row) +{ + unsigned char *rp = row; + unsigned char *rp_stop = row + rowbytes; + const unsigned char *pp = prev_row; + + uint8x8x4_t vdest; + vdest.val[3] = vdup_n_u8(0); + + for (; rp < rp_stop; rp += 16, pp += 16) + { + uint32x2x4_t vtmp; + uint8x8x4_t *vrpt, *vppt; + uint8x8x4_t vrp, vpp; + uint32x2x4_t *temp_pointer; + uint32x2x4_t vdest_val; + + vtmp = vld4_u32(png_ptr(uint32_t,rp)); + vrpt = png_ptr(uint8x8x4_t,&vtmp); + vrp = *vrpt; + vtmp = vld4_u32(png_ptrc(uint32_t,pp)); + vppt = png_ptr(uint8x8x4_t,&vtmp); + vpp = *vppt; + + vdest.val[0] = vhadd_u8(vdest.val[3], vpp.val[0]); + vdest.val[0] = vadd_u8(vdest.val[0], vrp.val[0]); + vdest.val[1] = vhadd_u8(vdest.val[0], vpp.val[1]); + vdest.val[1] = vadd_u8(vdest.val[1], vrp.val[1]); + vdest.val[2] = vhadd_u8(vdest.val[1], vpp.val[2]); + vdest.val[2] = vadd_u8(vdest.val[2], vrp.val[2]); + vdest.val[3] = vhadd_u8(vdest.val[2], vpp.val[3]); + vdest.val[3] = vadd_u8(vdest.val[3], vrp.val[3]); + + vdest_val = png_ldr(uint32x2x4_t, &vdest); + vst4_lane_u32(png_ptr(uint32_t,rp), vdest_val, 0); + } +} + +static uint8x8_t paeth_arm(uint8x8_t a, uint8x8_t b, uint8x8_t c) +{ + uint8x8_t d, e; + uint16x8_t p1, pa, pb, pc; + + p1 = vaddl_u8(a, b); /* a + b */ + pc = vaddl_u8(c, c); /* c * 2 */ + pa = vabdl_u8(b, c); /* pa */ + pb = vabdl_u8(a, c); /* pb */ + pc = vabdq_u16(p1, pc); /* pc */ + + p1 = vcleq_u16(pa, pb); /* pa <= pb */ + pa = vcleq_u16(pa, pc); /* pa <= pc */ + pb = vcleq_u16(pb, pc); /* pb <= pc */ + + p1 = vandq_u16(p1, pa); /* pa <= pb && pa <= pc */ + + d = vmovn_u16(pb); + e = vmovn_u16(p1); + + d = vbsl_u8(d, b, c); + e = vbsl_u8(e, a, d); + + return e; +} + +static void defilter_paeth3(size_t rowbytes, unsigned char *row, const unsigned char *prev_row) +{ + unsigned char *rp = row; + const unsigned char *pp = prev_row; + unsigned char *rp_stop = row + rowbytes; + + uint8x16_t vtmp; + uint8x8x2_t *vrpt; + uint8x8x2_t vrp; + uint8x8_t vlast = vdup_n_u8(0); + uint8x8x4_t vdest; + vdest.val[3] = vdup_n_u8(0); + + vtmp = vld1q_u8(rp); + vrpt = png_ptr(uint8x8x2_t,&vtmp); + vrp = *vrpt; + + for (; rp < rp_stop; pp += 12) + { + uint8x8x2_t *vppt; + uint8x8x2_t vpp; + uint8x8_t vtmp1, vtmp2, vtmp3; + uint32x2_t *temp_pointer; + + vtmp = vld1q_u8(pp); + vppt = png_ptr(uint8x8x2_t,&vtmp); + vpp = *vppt; + + vdest.val[0] = paeth_arm(vdest.val[3], vpp.val[0], vlast); + vdest.val[0] = vadd_u8(vdest.val[0], vrp.val[0]); + + vtmp1 = vext_u8(vrp.val[0], vrp.val[1], 3); + vtmp2 = vext_u8(vpp.val[0], vpp.val[1], 3); + vdest.val[1] = paeth_arm(vdest.val[0], vtmp2, vpp.val[0]); + vdest.val[1] = vadd_u8(vdest.val[1], vtmp1); + + vtmp1 = vext_u8(vrp.val[0], vrp.val[1], 6); + vtmp3 = vext_u8(vpp.val[0], vpp.val[1], 6); + vdest.val[2] = paeth_arm(vdest.val[1], vtmp3, vtmp2); + vdest.val[2] = vadd_u8(vdest.val[2], vtmp1); + + vtmp1 = vext_u8(vrp.val[1], vrp.val[1], 1); + vtmp2 = vext_u8(vpp.val[1], vpp.val[1], 1); + + vtmp = vld1q_u8(rp + 12); + vrpt = png_ptr(uint8x8x2_t,&vtmp); + vrp = *vrpt; + + vdest.val[3] = paeth_arm(vdest.val[2], vtmp2, vtmp3); + vdest.val[3] = vadd_u8(vdest.val[3], vtmp1); + + vlast = vtmp2; + + vst1_lane_u32(png_ptr(uint32_t,rp), png_ldr(uint32x2_t,&vdest.val[0]), 0); + rp += 3; + vst1_lane_u32(png_ptr(uint32_t,rp), png_ldr(uint32x2_t,&vdest.val[1]), 0); + rp += 3; + vst1_lane_u32(png_ptr(uint32_t,rp), png_ldr(uint32x2_t,&vdest.val[2]), 0); + rp += 3; + vst1_lane_u32(png_ptr(uint32_t,rp), png_ldr(uint32x2_t,&vdest.val[3]), 0); + rp += 3; + } +} + +static void defilter_paeth4(size_t rowbytes, unsigned char *row, const unsigned char *prev_row) +{ + unsigned char *rp = row; + unsigned char *rp_stop = row + rowbytes; + const unsigned char *pp = prev_row; + + uint8x8_t vlast = vdup_n_u8(0); + uint8x8x4_t vdest; + vdest.val[3] = vdup_n_u8(0); + + for (; rp < rp_stop; rp += 16, pp += 16) + { + uint32x2x4_t vtmp; + uint8x8x4_t *vrpt, *vppt; + uint8x8x4_t vrp, vpp; + uint32x2x4_t *temp_pointer; + uint32x2x4_t vdest_val; + + vtmp = vld4_u32(png_ptr(uint32_t,rp)); + vrpt = png_ptr(uint8x8x4_t,&vtmp); + vrp = *vrpt; + vtmp = vld4_u32(png_ptrc(uint32_t,pp)); + vppt = png_ptr(uint8x8x4_t,&vtmp); + vpp = *vppt; + + vdest.val[0] = paeth_arm(vdest.val[3], vpp.val[0], vlast); + vdest.val[0] = vadd_u8(vdest.val[0], vrp.val[0]); + vdest.val[1] = paeth_arm(vdest.val[0], vpp.val[1], vpp.val[0]); + vdest.val[1] = vadd_u8(vdest.val[1], vrp.val[1]); + vdest.val[2] = paeth_arm(vdest.val[1], vpp.val[2], vpp.val[1]); + vdest.val[2] = vadd_u8(vdest.val[2], vrp.val[2]); + vdest.val[3] = paeth_arm(vdest.val[2], vpp.val[3], vpp.val[2]); + vdest.val[3] = vadd_u8(vdest.val[3], vrp.val[3]); + + vlast = vpp.val[3]; + + vdest_val = png_ldr(uint32x2x4_t, &vdest); + vst4_lane_u32(png_ptr(uint32_t,rp), vdest_val, 0); + } +} + +/* NEON optimised palette expansion functions + * Derived from palette_neon_intrinsics.c + * + * Copyright (c) 2018-2019 Cosmin Truta + * Copyright (c) 2017-2018 Arm Holdings. All rights reserved. + * Written by Richard Townsend , February 2017. + * + * This code is derived from libpng source code. + * For conditions of distribution and use, see the disclaimer + * and license in this file. + * + * Related: https://developer.arm.com/documentation/101964/latest/Color-palette-expansion + * + * The functions were refactored to iterate forward. + * + */ + +/* Expands a palettized row into RGBA8. */ +static uint32_t expand_palette_rgba8_neon(unsigned char *row, const unsigned char *scanline, const unsigned char *plte, uint32_t width) +{ + const uint32_t scanline_stride = 4; + const uint32_t row_stride = scanline_stride * 4; + const uint32_t count = width / scanline_stride; + const uint32_t *palette = (const uint32_t*)plte; + + if(!count) return 0; + + uint32_t i; + uint32x4_t cur; + for(i=0; i < count; i++, scanline += scanline_stride) + { + cur = vld1q_dup_u32 (palette + scanline[0]); + cur = vld1q_lane_u32(palette + scanline[1], cur, 1); + cur = vld1q_lane_u32(palette + scanline[2], cur, 2); + cur = vld1q_lane_u32(palette + scanline[3], cur, 3); + vst1q_u32((uint32_t*)(row + i * row_stride), cur); + } + + return count * scanline_stride; +} + +/* Expands a palettized row into RGB8. */ +static uint32_t expand_palette_rgb8_neon(unsigned char *row, const unsigned char *scanline, const unsigned char *plte, uint32_t width) +{ + const uint32_t scanline_stride = 8; + const uint32_t row_stride = scanline_stride * 3; + const uint32_t count = width / scanline_stride; + + if(!count) return 0; + + uint32_t i; + uint8x8x3_t cur; + for(i=0; i < count; i++, scanline += scanline_stride) + { + cur = vld3_dup_u8 (plte + 3 * scanline[0]); + cur = vld3_lane_u8(plte + 3 * scanline[1], cur, 1); + cur = vld3_lane_u8(plte + 3 * scanline[2], cur, 2); + cur = vld3_lane_u8(plte + 3 * scanline[3], cur, 3); + cur = vld3_lane_u8(plte + 3 * scanline[4], cur, 4); + cur = vld3_lane_u8(plte + 3 * scanline[5], cur, 5); + cur = vld3_lane_u8(plte + 3 * scanline[6], cur, 6); + cur = vld3_lane_u8(plte + 3 * scanline[7], cur, 7); + vst3_u8(row + i * row_stride, cur); + } + + return count * scanline_stride; +} + +#endif /* SPNG_ARM */ diff --git a/tools/assets/n64texconv/lib/spng/spng.h b/tools/assets/n64texconv/lib/spng/spng.h new file mode 100644 index 0000000000..8f946337bf --- /dev/null +++ b/tools/assets/n64texconv/lib/spng/spng.h @@ -0,0 +1,537 @@ +/* SPDX-License-Identifier: BSD-2-Clause */ +#ifndef SPNG_H +#define SPNG_H + +#ifdef __cplusplus +extern "C" { +#endif + +#if (defined(_WIN32) || defined(__CYGWIN__)) && !defined(SPNG_STATIC) + #if defined(SPNG__BUILD) + #define SPNG_API __declspec(dllexport) + #else + #define SPNG_API __declspec(dllimport) + #endif +#else + #define SPNG_API +#endif + +#if defined(_MSC_VER) + #define SPNG_CDECL __cdecl +#else + #define SPNG_CDECL +#endif + +#include +#include +#include + +#define SPNG_VERSION_MAJOR 0 +#define SPNG_VERSION_MINOR 7 +#define SPNG_VERSION_PATCH 4 + +enum spng_errno +{ + SPNG_IO_ERROR = -2, + SPNG_IO_EOF = -1, + SPNG_OK = 0, + SPNG_EINVAL, + SPNG_EMEM, + SPNG_EOVERFLOW, + SPNG_ESIGNATURE, + SPNG_EWIDTH, + SPNG_EHEIGHT, + SPNG_EUSER_WIDTH, + SPNG_EUSER_HEIGHT, + SPNG_EBIT_DEPTH, + SPNG_ECOLOR_TYPE, + SPNG_ECOMPRESSION_METHOD, + SPNG_EFILTER_METHOD, + SPNG_EINTERLACE_METHOD, + SPNG_EIHDR_SIZE, + SPNG_ENOIHDR, + SPNG_ECHUNK_POS, + SPNG_ECHUNK_SIZE, + SPNG_ECHUNK_CRC, + SPNG_ECHUNK_TYPE, + SPNG_ECHUNK_UNKNOWN_CRITICAL, + SPNG_EDUP_PLTE, + SPNG_EDUP_CHRM, + SPNG_EDUP_GAMA, + SPNG_EDUP_ICCP, + SPNG_EDUP_SBIT, + SPNG_EDUP_SRGB, + SPNG_EDUP_BKGD, + SPNG_EDUP_HIST, + SPNG_EDUP_TRNS, + SPNG_EDUP_PHYS, + SPNG_EDUP_TIME, + SPNG_EDUP_OFFS, + SPNG_EDUP_EXIF, + SPNG_ECHRM, + SPNG_EPLTE_IDX, + SPNG_ETRNS_COLOR_TYPE, + SPNG_ETRNS_NO_PLTE, + SPNG_EGAMA, + SPNG_EICCP_NAME, + SPNG_EICCP_COMPRESSION_METHOD, + SPNG_ESBIT, + SPNG_ESRGB, + SPNG_ETEXT, + SPNG_ETEXT_KEYWORD, + SPNG_EZTXT, + SPNG_EZTXT_COMPRESSION_METHOD, + SPNG_EITXT, + SPNG_EITXT_COMPRESSION_FLAG, + SPNG_EITXT_COMPRESSION_METHOD, + SPNG_EITXT_LANG_TAG, + SPNG_EITXT_TRANSLATED_KEY, + SPNG_EBKGD_NO_PLTE, + SPNG_EBKGD_PLTE_IDX, + SPNG_EHIST_NO_PLTE, + SPNG_EPHYS, + SPNG_ESPLT_NAME, + SPNG_ESPLT_DUP_NAME, + SPNG_ESPLT_DEPTH, + SPNG_ETIME, + SPNG_EOFFS, + SPNG_EEXIF, + SPNG_EIDAT_TOO_SHORT, + SPNG_EIDAT_STREAM, + SPNG_EZLIB, + SPNG_EFILTER, + SPNG_EBUFSIZ, + SPNG_EIO, + SPNG_EOF, + SPNG_EBUF_SET, + SPNG_EBADSTATE, + SPNG_EFMT, + SPNG_EFLAGS, + SPNG_ECHUNKAVAIL, + SPNG_ENCODE_ONLY, + SPNG_EOI, + SPNG_ENOPLTE, + SPNG_ECHUNK_LIMITS, + SPNG_EZLIB_INIT, + SPNG_ECHUNK_STDLEN, + SPNG_EINTERNAL, + SPNG_ECTXTYPE, + SPNG_ENOSRC, + SPNG_ENODST, + SPNG_EOPSTATE, + SPNG_ENOTFINAL, +}; + +enum spng_text_type +{ + SPNG_TEXT = 1, + SPNG_ZTXT = 2, + SPNG_ITXT = 3 +}; + +enum spng_color_type +{ + SPNG_COLOR_TYPE_GRAYSCALE = 0, + SPNG_COLOR_TYPE_TRUECOLOR = 2, + SPNG_COLOR_TYPE_INDEXED = 3, + SPNG_COLOR_TYPE_GRAYSCALE_ALPHA = 4, + SPNG_COLOR_TYPE_TRUECOLOR_ALPHA = 6 +}; + +enum spng_filter +{ + SPNG_FILTER_NONE = 0, + SPNG_FILTER_SUB = 1, + SPNG_FILTER_UP = 2, + SPNG_FILTER_AVERAGE = 3, + SPNG_FILTER_PAETH = 4 +}; + +enum spng_filter_choice +{ + SPNG_DISABLE_FILTERING = 0, + SPNG_FILTER_CHOICE_NONE = 8, + SPNG_FILTER_CHOICE_SUB = 16, + SPNG_FILTER_CHOICE_UP = 32, + SPNG_FILTER_CHOICE_AVG = 64, + SPNG_FILTER_CHOICE_PAETH = 128, + SPNG_FILTER_CHOICE_ALL = (8|16|32|64|128) +}; + +enum spng_interlace_method +{ + SPNG_INTERLACE_NONE = 0, + SPNG_INTERLACE_ADAM7 = 1 +}; + +/* Channels are always in byte-order */ +enum spng_format +{ + SPNG_FMT_RGBA8 = 1, + SPNG_FMT_RGBA16 = 2, + SPNG_FMT_RGB8 = 4, + + /* Partially implemented, see documentation */ + SPNG_FMT_GA8 = 16, + SPNG_FMT_GA16 = 32, + SPNG_FMT_G8 = 64, + + /* No conversion or scaling */ + SPNG_FMT_PNG = 256, + SPNG_FMT_RAW = 512 /* big-endian (everything else is host-endian) */ +}; + +enum spng_ctx_flags +{ + SPNG_CTX_IGNORE_ADLER32 = 1, /* Ignore checksum in DEFLATE streams */ + SPNG_CTX_ENCODER = 2 /* Create an encoder context */ +}; + +enum spng_decode_flags +{ + SPNG_DECODE_USE_TRNS = 1, /* Deprecated */ + SPNG_DECODE_USE_GAMA = 2, /* Deprecated */ + SPNG_DECODE_USE_SBIT = 8, /* Undocumented */ + + SPNG_DECODE_TRNS = 1, /* Apply transparency */ + SPNG_DECODE_GAMMA = 2, /* Apply gamma correction */ + SPNG_DECODE_PROGRESSIVE = 256 /* Initialize for progressive reads */ +}; + +enum spng_crc_action +{ + /* Default for critical chunks */ + SPNG_CRC_ERROR = 0, + + /* Discard chunk, invalid for critical chunks. + Since v0.6.2: default for ancillary chunks */ + SPNG_CRC_DISCARD = 1, + + /* Ignore and don't calculate checksum. + Since v0.6.2: also ignores checksums in DEFLATE streams */ + SPNG_CRC_USE = 2 +}; + +enum spng_encode_flags +{ + SPNG_ENCODE_PROGRESSIVE = 1, /* Initialize for progressive writes */ + SPNG_ENCODE_FINALIZE = 2, /* Finalize PNG after encoding image */ +}; + +struct spng_ihdr +{ + uint32_t width; + uint32_t height; + uint8_t bit_depth; + uint8_t color_type; + uint8_t compression_method; + uint8_t filter_method; + uint8_t interlace_method; +}; + +struct spng_plte_entry +{ + uint8_t red; + uint8_t green; + uint8_t blue; + + uint8_t alpha; /* Reserved for internal use */ +}; + +struct spng_plte +{ + uint32_t n_entries; + struct spng_plte_entry entries[256]; +}; + +struct spng_trns +{ + uint16_t gray; + + uint16_t red; + uint16_t green; + uint16_t blue; + + uint32_t n_type3_entries; + uint8_t type3_alpha[256]; +}; + +struct spng_chrm_int +{ + uint32_t white_point_x; + uint32_t white_point_y; + uint32_t red_x; + uint32_t red_y; + uint32_t green_x; + uint32_t green_y; + uint32_t blue_x; + uint32_t blue_y; +}; + +struct spng_chrm +{ + double white_point_x; + double white_point_y; + double red_x; + double red_y; + double green_x; + double green_y; + double blue_x; + double blue_y; +}; + +struct spng_iccp +{ + char profile_name[80]; + size_t profile_len; + char *profile; +}; + +struct spng_sbit +{ + uint8_t grayscale_bits; + uint8_t red_bits; + uint8_t green_bits; + uint8_t blue_bits; + uint8_t alpha_bits; +}; + +struct spng_text +{ + char keyword[80]; + int type; + + size_t length; + char *text; + + uint8_t compression_flag; /* iTXt only */ + uint8_t compression_method; /* iTXt, ztXt only */ + char *language_tag; /* iTXt only */ + char *translated_keyword; /* iTXt only */ +}; + +struct spng_bkgd +{ + uint16_t gray; /* Only for gray/gray alpha */ + uint16_t red; + uint16_t green; + uint16_t blue; + uint16_t plte_index; /* Only for indexed color */ +}; + +struct spng_hist +{ + uint16_t frequency[256]; +}; + +struct spng_phys +{ + uint32_t ppu_x, ppu_y; + uint8_t unit_specifier; +}; + +struct spng_splt_entry +{ + uint16_t red; + uint16_t green; + uint16_t blue; + uint16_t alpha; + uint16_t frequency; +}; + +struct spng_splt +{ + char name[80]; + uint8_t sample_depth; + uint32_t n_entries; + struct spng_splt_entry *entries; +}; + +struct spng_time +{ + uint16_t year; + uint8_t month; + uint8_t day; + uint8_t hour; + uint8_t minute; + uint8_t second; +}; + +struct spng_offs +{ + int32_t x, y; + uint8_t unit_specifier; +}; + +struct spng_exif +{ + size_t length; + char *data; +}; + +struct spng_chunk +{ + size_t offset; + uint32_t length; + uint8_t type[4]; + uint32_t crc; +}; + +enum spng_location +{ + SPNG_AFTER_IHDR = 1, + SPNG_AFTER_PLTE = 2, + SPNG_AFTER_IDAT = 8, +}; + +struct spng_unknown_chunk +{ + uint8_t type[4]; + size_t length; + void *data; + enum spng_location location; +}; + +enum spng_option +{ + SPNG_KEEP_UNKNOWN_CHUNKS = 1, + + SPNG_IMG_COMPRESSION_LEVEL, + SPNG_IMG_WINDOW_BITS, + SPNG_IMG_MEM_LEVEL, + SPNG_IMG_COMPRESSION_STRATEGY, + + SPNG_TEXT_COMPRESSION_LEVEL, + SPNG_TEXT_WINDOW_BITS, + SPNG_TEXT_MEM_LEVEL, + SPNG_TEXT_COMPRESSION_STRATEGY, + + SPNG_FILTER_CHOICE, + SPNG_CHUNK_COUNT_LIMIT, + SPNG_ENCODE_TO_BUFFER, +}; + +typedef void* SPNG_CDECL spng_malloc_fn(size_t size); +typedef void* SPNG_CDECL spng_realloc_fn(void* ptr, size_t size); +typedef void* SPNG_CDECL spng_calloc_fn(size_t count, size_t size); +typedef void SPNG_CDECL spng_free_fn(void* ptr); + +struct spng_alloc +{ + spng_malloc_fn *malloc_fn; + spng_realloc_fn *realloc_fn; + spng_calloc_fn *calloc_fn; + spng_free_fn *free_fn; +}; + +struct spng_row_info +{ + uint32_t scanline_idx; + uint32_t row_num; /* deinterlaced row index */ + int pass; + uint8_t filter; +}; + +typedef struct spng_ctx spng_ctx; + +typedef int spng_read_fn(spng_ctx *ctx, void *user, void *dest, size_t length); +typedef int spng_write_fn(spng_ctx *ctx, void *user, void *src, size_t length); + +typedef int spng_rw_fn(spng_ctx *ctx, void *user, void *dst_src, size_t length); + +SPNG_API spng_ctx *spng_ctx_new(int flags); +SPNG_API spng_ctx *spng_ctx_new2(struct spng_alloc *alloc, int flags); +SPNG_API void spng_ctx_free(spng_ctx *ctx); + +SPNG_API int spng_set_png_buffer(spng_ctx *ctx, const void *buf, size_t size); +SPNG_API int spng_set_png_stream(spng_ctx *ctx, spng_rw_fn *rw_func, void *user); +SPNG_API int spng_set_png_file(spng_ctx *ctx, FILE *file); + +SPNG_API void *spng_get_png_buffer(spng_ctx *ctx, size_t *len, int *error); + +SPNG_API int spng_set_image_limits(spng_ctx *ctx, uint32_t width, uint32_t height); +SPNG_API int spng_get_image_limits(spng_ctx *ctx, uint32_t *width, uint32_t *height); + +SPNG_API int spng_set_chunk_limits(spng_ctx *ctx, size_t chunk_size, size_t cache_size); +SPNG_API int spng_get_chunk_limits(spng_ctx *ctx, size_t *chunk_size, size_t *cache_size); + +SPNG_API int spng_set_crc_action(spng_ctx *ctx, int critical, int ancillary); + +SPNG_API int spng_set_option(spng_ctx *ctx, enum spng_option option, int value); +SPNG_API int spng_get_option(spng_ctx *ctx, enum spng_option option, int *value); + +SPNG_API int spng_decoded_image_size(spng_ctx *ctx, int fmt, size_t *len); + +/* Decode */ +SPNG_API int spng_decode_image(spng_ctx *ctx, void *out, size_t len, int fmt, int flags); + +/* Progressive decode */ +SPNG_API int spng_decode_scanline(spng_ctx *ctx, void *out, size_t len); +SPNG_API int spng_decode_row(spng_ctx *ctx, void *out, size_t len); +SPNG_API int spng_decode_chunks(spng_ctx *ctx); + +/* Encode/decode */ +SPNG_API int spng_get_row_info(spng_ctx *ctx, struct spng_row_info *row_info); + +/* Encode */ +SPNG_API int spng_encode_image(spng_ctx *ctx, const void *img, size_t len, int fmt, int flags); + +/* Progressive encode */ +SPNG_API int spng_encode_scanline(spng_ctx *ctx, const void *scanline, size_t len); +SPNG_API int spng_encode_row(spng_ctx *ctx, const void *row, size_t len); +SPNG_API int spng_encode_chunks(spng_ctx *ctx); + +SPNG_API int spng_get_ihdr(spng_ctx *ctx, struct spng_ihdr *ihdr); +SPNG_API int spng_get_plte(spng_ctx *ctx, struct spng_plte *plte); +SPNG_API int spng_get_trns(spng_ctx *ctx, struct spng_trns *trns); +SPNG_API int spng_get_chrm(spng_ctx *ctx, struct spng_chrm *chrm); +SPNG_API int spng_get_chrm_int(spng_ctx *ctx, struct spng_chrm_int *chrm_int); +SPNG_API int spng_get_gama(spng_ctx *ctx, double *gamma); +SPNG_API int spng_get_gama_int(spng_ctx *ctx, uint32_t *gama_int); +SPNG_API int spng_get_iccp(spng_ctx *ctx, struct spng_iccp *iccp); +SPNG_API int spng_get_sbit(spng_ctx *ctx, struct spng_sbit *sbit); +SPNG_API int spng_get_srgb(spng_ctx *ctx, uint8_t *rendering_intent); +SPNG_API int spng_get_text(spng_ctx *ctx, struct spng_text *text, uint32_t *n_text); +SPNG_API int spng_get_bkgd(spng_ctx *ctx, struct spng_bkgd *bkgd); +SPNG_API int spng_get_hist(spng_ctx *ctx, struct spng_hist *hist); +SPNG_API int spng_get_phys(spng_ctx *ctx, struct spng_phys *phys); +SPNG_API int spng_get_splt(spng_ctx *ctx, struct spng_splt *splt, uint32_t *n_splt); +SPNG_API int spng_get_time(spng_ctx *ctx, struct spng_time *time); +SPNG_API int spng_get_unknown_chunks(spng_ctx *ctx, struct spng_unknown_chunk *chunks, uint32_t *n_chunks); + +/* Official extensions */ +SPNG_API int spng_get_offs(spng_ctx *ctx, struct spng_offs *offs); +SPNG_API int spng_get_exif(spng_ctx *ctx, struct spng_exif *exif); + + +SPNG_API int spng_set_ihdr(spng_ctx *ctx, struct spng_ihdr *ihdr); +SPNG_API int spng_set_plte(spng_ctx *ctx, struct spng_plte *plte); +SPNG_API int spng_set_trns(spng_ctx *ctx, struct spng_trns *trns); +SPNG_API int spng_set_chrm(spng_ctx *ctx, struct spng_chrm *chrm); +SPNG_API int spng_set_chrm_int(spng_ctx *ctx, struct spng_chrm_int *chrm_int); +SPNG_API int spng_set_gama(spng_ctx *ctx, double gamma); +SPNG_API int spng_set_gama_int(spng_ctx *ctx, uint32_t gamma); +SPNG_API int spng_set_iccp(spng_ctx *ctx, struct spng_iccp *iccp); +SPNG_API int spng_set_sbit(spng_ctx *ctx, struct spng_sbit *sbit); +SPNG_API int spng_set_srgb(spng_ctx *ctx, uint8_t rendering_intent); +SPNG_API int spng_set_text(spng_ctx *ctx, struct spng_text *text, uint32_t n_text); +SPNG_API int spng_set_bkgd(spng_ctx *ctx, struct spng_bkgd *bkgd); +SPNG_API int spng_set_hist(spng_ctx *ctx, struct spng_hist *hist); +SPNG_API int spng_set_phys(spng_ctx *ctx, struct spng_phys *phys); +SPNG_API int spng_set_splt(spng_ctx *ctx, struct spng_splt *splt, uint32_t n_splt); +SPNG_API int spng_set_time(spng_ctx *ctx, struct spng_time *time); +SPNG_API int spng_set_unknown_chunks(spng_ctx *ctx, struct spng_unknown_chunk *chunks, uint32_t n_chunks); + +/* Official extensions */ +SPNG_API int spng_set_offs(spng_ctx *ctx, struct spng_offs *offs); +SPNG_API int spng_set_exif(spng_ctx *ctx, struct spng_exif *exif); + + +SPNG_API const char *spng_strerror(int err); +SPNG_API const char *spng_version_string(void); + +#ifdef __cplusplus +} +#endif + +#endif /* SPNG_H */ diff --git a/tools/assets/n64texconv/src/LICENSE b/tools/assets/n64texconv/src/LICENSE new file mode 100644 index 0000000000..3c68e6a959 --- /dev/null +++ b/tools/assets/n64texconv/src/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 ZeldaRET + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/tools/assets/n64texconv/src/app/main.c b/tools/assets/n64texconv/src/app/main.c new file mode 100644 index 0000000000..9829c512c1 --- /dev/null +++ b/tools/assets/n64texconv/src/app/main.c @@ -0,0 +1,137 @@ +/* SPDX-FileCopyrightText: Copyright (C) 2025 ZeldaRET */ +/* SPDX-License-Identifier: MIT */ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define NORETURN __attribute__((noreturn)) +#define ARRLEN(a) (sizeof(a) / sizeof((a)[0])) +#define strequ(s1, s2) (strcmp((s1), (s2)) == 0) + +#include "../libn64texconv/n64texconv.h" +#include "../libn64texconv/jfif.h" + +static bool +is_regular_file(const char *path) +{ + struct stat sb; + stat(path, &sb); + return S_ISREG(sb.st_mode); +} + +static NORETURN void +usage(const char *progname) +{ + fprintf(stderr, + "n64texconv: Convert an input png to N64 data in the desired format.\n" + "Usage: %s [pal_out.c]\n" + " Valid types: i4 / i8 / ci4 / ci8 / ia4 / ia8 / ia16 / rgba16 / rgba32 / JFIF\n", + progname); + exit(EXIT_FAILURE); +} + +static const struct fmt_info { + const char *name; + int fmt; + int siz; +} fmt_map[] = { + // clang-format off + { "i4", G_IM_FMT_I, G_IM_SIZ_4b, }, + { "i8", G_IM_FMT_I, G_IM_SIZ_8b, }, + { "ci4", G_IM_FMT_CI, G_IM_SIZ_4b, }, + { "ci8", G_IM_FMT_CI, G_IM_SIZ_8b, }, + { "ia4", G_IM_FMT_IA, G_IM_SIZ_4b, }, + { "ia8", G_IM_FMT_IA, G_IM_SIZ_8b, }, + { "ia16", G_IM_FMT_IA, G_IM_SIZ_16b, }, + { "rgba16", G_IM_FMT_RGBA, G_IM_SIZ_16b, }, + { "rgba32", G_IM_FMT_RGBA, G_IM_SIZ_32b, }, + // clang-format on +}; + +int +main(int argc, char **argv) +{ + const char *progname = argv[0]; + const char *fmt; + const char *array_fmt; + const char *in; + const char *out; + const char *pal_out; + + if (argc < 5) + usage(progname); + + fmt = argv[1]; + array_fmt = argv[2]; + in = argv[3]; + out = argv[4]; + pal_out = (argc > 5) ? argv[5] : NULL; + + unsigned int byte_width = (strequ(array_fmt, "u32") ? 4 : 8); + + if (!is_regular_file(in)) { + fprintf(stderr, "Could not open input file %s\n", in); + return EXIT_FAILURE; + } + + if (strequ(fmt, "JFIF")) { + struct JFIF *jfif = jfif_fromfile(in, JFIF_BUFFER_SIZE); + if (jfif == NULL) { + fprintf(stderr, "Could not open input file %s\n", in); + return EXIT_FAILURE; + } + if (jfif_to_c_file(out, jfif, JFIF_BUFFER_SIZE)) { + fprintf(stderr, "Could not save output C file %s\n", out); + return EXIT_FAILURE; + } + jfif_free(jfif); + } else { + int rv; + const struct fmt_info *fmt_info = NULL; + for (size_t i = 0; i < ARRLEN(fmt_map); i++) { + if (strequ(fmt, fmt_map[i].name)) { + fmt_info = &fmt_map[i]; + break; + } + } + if (fmt_info == NULL) { + fprintf(stderr, "Error: Invalid fmt %s\n", fmt); + return EXIT_FAILURE; + } + + struct n64_image *img = n64texconv_image_from_png(in, fmt_info->fmt, fmt_info->siz, G_IM_FMT_RGBA); + if (img == NULL) { + fprintf(stderr, "Could not open input file %s\n", in); + return EXIT_FAILURE; + } + + if (img->pal != NULL) { + if (pal_out == NULL) { + fprintf(stderr, "Input file %s is color indexed, a palette output C file must be provided.\n", in); + usage(progname); + } + + if (rv = n64texconv_palette_to_c_file(pal_out, img->pal, false, byte_width), rv != 0) { + fprintf(stderr, "Could not save output C file %s (error %d)\n", pal_out, rv); + return EXIT_FAILURE; + } + } + + if (rv = n64texconv_image_to_c_file(out, img, false, false, byte_width), rv != 0) { + fprintf(stderr, "Could not save output C file %s (error %d)\n", out, rv); + return EXIT_FAILURE; + } + + if (img->pal != NULL) + n64texconv_palette_free(img->pal); + n64texconv_image_free(img); + } + + return EXIT_SUCCESS; +} diff --git a/tools/assets/n64texconv/src/libn64texconv/bin2c.c b/tools/assets/n64texconv/src/libn64texconv/bin2c.c new file mode 100644 index 0000000000..c5d9475bbf --- /dev/null +++ b/tools/assets/n64texconv/src/libn64texconv/bin2c.c @@ -0,0 +1,130 @@ +/* SPDX-FileCopyrightText: Copyright (C) 2025 ZeldaRET */ +/* SPDX-License-Identifier: MIT */ +#include +#include +#include +#include +#include +#include +#include +#include + +#include "bin2c.h" +#include "endian.h" + +#define BYTES_PER_ROW 32 +#define LINE_MASK (BYTES_PER_ROW - 1) + +int +bin2c(char **out, size_t *size_out, void *bin, size_t size, size_t pad_to_size, unsigned int byte_width) +{ + assert(out != NULL); + assert(size_out != NULL); + assert(bin != NULL); + + if (byte_width != 1 && byte_width != 2 && byte_width != 4 && byte_width != 8) + return -2; + + size_t end_size = (pad_to_size > size) ? pad_to_size : size; + + if ((end_size & (byte_width - 1)) != 0) + return -3; + + size_t size_out_ = (1 + 1 + 2 * byte_width + 1 + 1) * ((end_size + byte_width - 1) / byte_width) + 2; + char *out_ = malloc(size_out_); + if (out_ == NULL) + return -1; + + char *pos = out_; + bool was_newline = false; + for (size_t p = 0; p < size; p += byte_width) { + size_t rem = byte_width; + if (rem > size - p) // For any remaining unaligned data, rest will be padded with 0 + rem = size - p; + + // Read input + uint64_t d = 0; + memcpy(&d, &((uint8_t *)bin)[p], rem); + + // Byteswap + shift + d = be64toh(d) >> (64 - 8 * byte_width); + + // Write output + was_newline = (((p + byte_width) & LINE_MASK) == 0); + char end = was_newline ? '\n' : ' '; + pos += sprintf(pos, "0x%0*" PRIX64 ",%c", 2 * byte_width, d, end); + } + + for (size_t p = (size + byte_width - 1) & ~(byte_width - 1); p < pad_to_size; p += byte_width) { + was_newline = (((p + byte_width) & LINE_MASK) == 0); + char end = was_newline ? '\n' : ' '; + pos += sprintf(pos, "0x%0*" PRIX64 ",%c", 2 * byte_width, (uint64_t)0, end); + } + + if (!was_newline) + *pos++ = '\n'; + + *pos++ = '\0'; + + *out = out_; + *size_out = size_out_; + return 0; +} + +int +bin2c_file(const char *out_path, void *bin, size_t size, size_t pad_to_size, unsigned int byte_width) +{ + assert(out_path != NULL); + assert(bin != NULL); + + if (byte_width != 1 && byte_width != 2 && byte_width != 4 && byte_width != 8) + return -2; + + size_t end_size = (pad_to_size > size) ? pad_to_size : size; + + if ((end_size & (byte_width - 1)) != 0) + return -3; + + FILE *of = fopen(out_path, "w"); + if (of == NULL) + return -1; + + bool was_newline = false; + for (size_t p = 0; p < size; p += byte_width) { + size_t rem = byte_width; + if (rem > size - p) // For any remaining unaligned data, rest will be padded with 0 + rem = size - p; + + // Read input + uint64_t d = 0; + memcpy(&d, &((uint8_t *)bin)[p], rem); + + // Byteswap + shift + d = be64toh(d) >> (64 - 8 * byte_width); + + // Write output + was_newline = (((p + byte_width) & LINE_MASK) == 0); + char end = was_newline ? '\n' : ' '; + + if (fprintf(of, "0x%0*" PRIX64 ",%c", 2 * byte_width, d, end) < 0) + goto error_post_open; + } + + for (size_t p = (size + byte_width - 1) & ~(byte_width - 1); p < pad_to_size; p += byte_width) { + was_newline = (((p + byte_width) & LINE_MASK) == 0); + char end = was_newline ? '\n' : ' '; + if (fprintf(of, "0x%0*" PRIX64 ",%c", 2 * byte_width, (uint64_t)0, end) < 0) + goto error_post_open; + } + + if (!was_newline) + fputs("\n", of); + + fclose(of); + return 0; +error_post_open: + fclose(of); + if (remove(out_path) != 0) + fprintf(stderr, "error calling remove(\"%s\"): %s", out_path, strerror(errno)); + return -4; +} diff --git a/tools/assets/n64texconv/src/libn64texconv/bin2c.h b/tools/assets/n64texconv/src/libn64texconv/bin2c.h new file mode 100644 index 0000000000..f34a65baf4 --- /dev/null +++ b/tools/assets/n64texconv/src/libn64texconv/bin2c.h @@ -0,0 +1,14 @@ +/* SPDX-FileCopyrightText: Copyright (C) 2025 ZeldaRET */ +/* SPDX-License-Identifier: MIT */ +#ifndef BIN2C_H +#define BIN2C_H + +#include + +int +bin2c(char **out, size_t *size_out, void *bin, size_t size, size_t pad_to_size, unsigned int byte_width); + +int +bin2c_file(const char *out_path, void *bin, size_t size, size_t pad_to_size, unsigned int byte_width); + +#endif diff --git a/tools/assets/n64texconv/src/libn64texconv/endian.h b/tools/assets/n64texconv/src/libn64texconv/endian.h new file mode 100644 index 0000000000..e457b8664d --- /dev/null +++ b/tools/assets/n64texconv/src/libn64texconv/endian.h @@ -0,0 +1,69 @@ +/* SPDX-FileCopyrightText: Copyright (C) 2025 ZeldaRET */ +/* SPDX-License-Identifier: MIT */ +#ifndef ENDIAN_H +#define ENDIAN_H + +#if defined(__linux__) || defined(__CYGWIN__) +#include +#elif defined(__APPLE__) +#include + +#define htobe16(x) OSSwapHostToBigInt16(x) +#define htole16(x) OSSwapHostToLittleInt16(x) +#define be16toh(x) OSSwapBigToHostInt16(x) +#define le16toh(x) OSSwapLittleToHostInt16(x) + +#define htobe32(x) OSSwapHostToBigInt32(x) +#define htole32(x) OSSwapHostToLittleInt32(x) +#define be32toh(x) OSSwapBigToHostInt32(x) +#define le32toh(x) OSSwapLittleToHostInt32(x) + +#define htobe64(x) OSSwapHostToBigInt64(x) +#define htole64(x) OSSwapHostToLittleInt64(x) +#define be64toh(x) OSSwapBigToHostInt64(x) +#define le64toh(x) OSSwapLittleToHostInt64(x) +#else + +#if !defined(__BYTE_ORDER__) +#error "No endian define provided by compiler" +#endif + +#if (__BYTE_ORDER__ == __ORDER_BIG_ENDIAN__) + +#define htobe16(x) (x) +#define htole16(x) __builtin_bswap16(x) +#define be16toh(x) (x) +#define le16toh(x) __builtin_bswap16(x) + +#define htobe32(x) (x) +#define htole32(x) __builtin_bswap32(x) +#define be32toh(x) (x) +#define le32toh(x) __builtin_bswap32(x) + +#define htobe64(x) (x) +#define htole64(x) __builtin_bswap64(x) +#define be64toh(x) (x) +#define le64toh(x) __builtin_bswap64(x) + +#else + +#define htobe16(x) __builtin_bswap16(x) +#define htole16(x) (x) +#define be16toh(x) __builtin_bswap16(x) +#define le16toh(x) (x) + +#define htobe32(x) __builtin_bswap32(x) +#define htole32(x) (x) +#define be32toh(x) __builtin_bswap32(x) +#define le32toh(x) (x) + +#define htobe64(x) __builtin_bswap64(x) +#define htole64(x) (x) +#define be64toh(x) __builtin_bswap64(x) +#define le64toh(x) (x) + +#endif + +#endif + +#endif diff --git a/tools/assets/n64texconv/src/libn64texconv/jfif.c b/tools/assets/n64texconv/src/libn64texconv/jfif.c new file mode 100644 index 0000000000..645614c890 --- /dev/null +++ b/tools/assets/n64texconv/src/libn64texconv/jfif.c @@ -0,0 +1,76 @@ +/* SPDX-FileCopyrightText: Copyright (C) 2025 ZeldaRET */ +/* SPDX-License-Identifier: MIT */ +#include +#include +#include +#include +#include + +#include "endian.h" +#include "bin2c.h" +#include "jfif.h" + +struct JFIF * +jfif_fromfile(const char *path, size_t max_size) +{ + assert(path != NULL); + FILE *f = fopen(path, "rb"); + if (f == NULL) + return NULL; + + fseek(f, 0, SEEK_END); + size_t data_size = ftell(f); + fseek(f, 0, SEEK_SET); + + struct JFIF *jfif = malloc(((sizeof(struct JFIF) + 3) & ~3) + data_size); + if (jfif != NULL) { + jfif->data = (void *)(jfif + 1); + jfif->data_size = data_size; + + if (fread(jfif->data, 1, data_size, f) != data_size) { + free(jfif); + jfif = NULL; + } else { + uint8_t *data8 = jfif->data; + uint16_t *data16 = jfif->data; + uint32_t *data32 = jfif->data; + + if (be32toh(data32[0]) != 0xFFD8FFE0) + printf("[Warning] Missing JPEG marker\n"); + if (data8[6] != 'J' || data8[7] != 'F' || data8[8] != 'I' || data8[9] != 'F') + printf("[Warning] Not JFIF\n"); + if (data8[11] != 0x01 || data8[12] != 0x01) + printf("[Warning] Not JFIF version 1.01\n"); + if (be16toh(data16[10]) != 0xFFDB) + printf("[Warning] Data before DQT\n"); + if (jfif->data_size > max_size) + printf("[Warning] JFIF image too large\n"); + } + } + + fclose(f); + return jfif; +} + +void +jfif_free(struct JFIF *jfif) +{ + assert(jfif != NULL); + free(jfif); +} + +int +jfif_to_c(char **out, size_t *size_out, struct JFIF *jfif, size_t pad_to_size) +{ + assert(out != NULL); + assert(size_out != NULL); + return bin2c(out, size_out, jfif->data, jfif->data_size, pad_to_size, 8); +} + +int +jfif_to_c_file(const char *out_path, struct JFIF *jfif, size_t pad_to_size) +{ + assert(out_path != NULL); + assert(jfif != NULL); + return bin2c_file(out_path, jfif->data, jfif->data_size, pad_to_size, 8); +} diff --git a/tools/assets/n64texconv/src/libn64texconv/jfif.h b/tools/assets/n64texconv/src/libn64texconv/jfif.h new file mode 100644 index 0000000000..d094d54788 --- /dev/null +++ b/tools/assets/n64texconv/src/libn64texconv/jfif.h @@ -0,0 +1,28 @@ +/* SPDX-FileCopyrightText: Copyright (C) 2025 ZeldaRET */ +/* SPDX-License-Identifier: MIT */ +#ifndef JFIF_H +#define JFIF_H + +#include +#include + +struct JFIF { + void *data; + size_t data_size; +}; + +#define JFIF_BUFFER_SIZE (320 * 240 * sizeof(uint16_t)) + +struct JFIF * +jfif_fromfile(const char *path, size_t max_size); + +void +jfif_free(struct JFIF *jfif); + +int +jfif_to_c(char **out, size_t *size_out, struct JFIF *jfif, size_t pad_to_size); + +int +jfif_to_c_file(const char *out_path, struct JFIF *jfif, size_t pad_to_size); + +#endif diff --git a/tools/assets/n64texconv/src/libn64texconv/n64texconv.c b/tools/assets/n64texconv/src/libn64texconv/n64texconv.c new file mode 100644 index 0000000000..06caf36f60 --- /dev/null +++ b/tools/assets/n64texconv/src/libn64texconv/n64texconv.c @@ -0,0 +1,1302 @@ +/* SPDX-FileCopyrightText: Copyright (C) 2025 ZeldaRET */ +/* SPDX-License-Identifier: MIT */ +#include +#include +#include +#include +#include +#include +#include +#include + +#include "spng.h" +#include "libimagequant.h" +#include "endian.h" +#include "bin2c.h" +#include "n64texconv.h" + +// ******************************************************************************************************************** +// Helpers +// ******************************************************************************************************************** + +#define UNUSED __attribute__((unused)) + +#define ALIGN8(x) (((x) + 7) & ~7) + +#define ABS(x) (((x) < 0) ? -(x) : (x)) + +static uint8_t +texel_to_greyscale(uint8_t r, uint8_t g, uint8_t b) +{ + // Approximately (0.2126729, 0.7151522, 0.0721750) in 1.8 fixed point + // Same as Blender: + // https://github.com/blender/blender/blob/594f47ecd2d5367ca936cf6fc6ec8168c2b360d0/intern/cycles/render/shader.cpp#L387 + return (r * 54 + g * 183 + b * 19 + 128) / 256; +} + +UNUSED static void +texels_to_greyscale(struct color *out, struct color *in, size_t ntexels, bool alpha) +{ + assert(out != NULL); + assert(in != NULL); + + for (size_t i = 0; i < ntexels; i++) { + struct color color = in[i]; + uint8_t grey = texel_to_greyscale(color.r, color.g, color.b); + uint8_t a = color.a; + out[i].r = grey; + out[i].g = grey; + out[i].b = grey; + out[i].a = (alpha) ? a : grey; + } +} + +static void +palette_dim(size_t count, size_t *width, size_t *height) +{ + assert(width != NULL); + assert(height != NULL); + + size_t best_width = count; + size_t best_height = 1; + size_t best_diff = count - 1; + + if (count >= 4) { + // Find a factorization of count for which the two factors are close to equal, and width >= height. + // This makes the output palette image approximately square. + for (size_t i = 2; i <= (size_t)sqrtf(count); i++) { + size_t div = count / i; + + size_t new_diff = ABS((signed)(div - i)); + if (div * i == count && new_diff < best_diff) { + // (div, i) is a factorization and is closer to a square than before + best_width = div; + best_height = i; + best_diff = new_diff; + } + } + } + *width = best_width; + *height = best_height; +} + +static void +n64texconv_quantize(uint8_t *out_indices, struct color *out_pal, size_t *out_pal_count, struct color *texels, + size_t width, size_t height, unsigned int max_colors, float dither_level) +{ + assert(out_indices != NULL); + assert(out_pal != NULL); + assert(out_pal_count != NULL); + assert(texels != NULL); + + // Set options + liq_attr *attr = liq_attr_create(); + liq_set_max_colors(attr, max_colors); + + // Quantize + liq_image *img = liq_image_create_rgba(attr, (void *)texels, width, height, 0.0); + liq_result *result; + liq_error err = liq_image_quantize(img, attr, &result); + assert(err == LIQ_OK); + + // Write output bitmap + liq_set_dithering_level(result, dither_level); + liq_write_remapped_image(result, img, (void *)out_indices, width * height); + + // Write output palette + const liq_palette *pal = liq_get_palette(result); + assert(pal->count <= max_colors); + *out_pal_count = pal->count; + memcpy(out_pal, pal->entries, pal->count * 4); + + // Cleanup + liq_result_destroy(result); + liq_image_destroy(img); + liq_attr_destroy(attr); +} + +/** + * out_indices, out_pal, out_pal_count, texels, widths, heights are all arrays of size num_images + * texels[i] and out_indices[i] are arrays of size widths[i] * heights[i] + */ +UNUSED static int +n64texconv_quantize_shared(uint8_t **out_indices, struct color *out_pal, size_t *out_pal_count, struct color **texels, + size_t *widths, size_t *heights, size_t num_images, unsigned int max_colors, + float dither_level) +{ + assert(out_indices != NULL); + assert(out_pal != NULL); + assert(out_pal_count != NULL); + assert(texels != NULL); + assert(widths != NULL); + assert(heights != NULL); + + int rv = 0; + + // Set options + liq_attr *attr = liq_attr_create(); + liq_set_max_colors(attr, max_colors); + + // Create histogram + liq_histogram *hist = liq_histogram_create(attr); + + // Add images to histogram + liq_image *images[num_images]; + for (size_t i = 0; i < num_images; i++) { + images[i] = liq_image_create_rgba(attr, (void *)texels[i], widths[i], heights[i], 0); + liq_histogram_add_image(hist, attr, images[i]); + } + + // Simultaneous quantization + liq_result *result; + liq_error err = liq_histogram_quantize(hist, attr, &result); + if (err != LIQ_OK) { + rv = -1; + goto exit; + } + + // Remap images + liq_set_dithering_level(result, dither_level); + for (size_t i = 0; i < num_images; i++) { + liq_write_remapped_image(result, images[i], (void *)out_indices[i], widths[i] * heights[i]); + } + + // Write output palette + const liq_palette *pal = liq_get_palette(result); + assert(pal->count <= max_colors); + *out_pal_count = pal->count; + memcpy(out_pal, pal->entries, pal->count * 4); + + // Cleanup + liq_result_destroy(result); +exit: + for (size_t i = 0; i < num_images; i++) { + liq_image_destroy(images[i]); + } + liq_histogram_destroy(hist); + liq_attr_destroy(attr); + return rv; +} + + + +// ******************************************************************************************************************** +// Format Packing and Unpacking +// ******************************************************************************************************************** + +#define UNPACK_FUNC(fmt_name) static int fmt_name##_unpack(struct color *out, void *in, size_t ntexels) + +#define UNPACK_CI_FUNC(fmt_name) static int fmt_name##_unpack(uint8_t *out, void *in, size_t ntexels, size_t npal) + +#define PACK_FUNC(fmt_name) static int fmt_name##_pack(void *out, struct color *in, size_t ntexels) + +#define PACK_CI_FUNC(fmt_name) static int fmt_name##_pack(void *out, uint8_t *in, size_t ntexels, size_t npal) + +/* RGBA16 */ + +UNPACK_FUNC(rgba16) +{ + uint16_t *in16 = in; + + for (size_t n = 0; n < ntexels; n++) { + uint16_t c = be16toh(in16[n]); + uint8_t r = (c >> 11) & 0b11111; + uint8_t g = (c >> 6) & 0b11111; + uint8_t b = (c >> 1) & 0b11111; + uint8_t a = (c >> 0) & 1; + out[n].r = (r << 3) | (r >> 2); + out[n].g = (g << 3) | (g >> 2); + out[n].b = (b << 3) | (b >> 2); + out[n].a = 255 * a; + } + return 0; +} + +PACK_FUNC(rgba16) +{ + uint16_t *out16 = out; + + for (size_t n = 0; n < ntexels; n++) { + uint8_t r = (in[n].r >> 3) & 0b11111; + uint8_t g = (in[n].g >> 3) & 0b11111; + uint8_t b = (in[n].b >> 3) & 0b11111; + uint8_t a = in[n].a != 0; + out16[n] = htobe16((r << 11) | (g << 6) | (b << 1) | a); + } + return 0; +} + +/* RGBA32 */ + +UNPACK_FUNC(rgba32) +{ + uint32_t *in32 = in; + + for (size_t n = 0; n < ntexels; n++) { + uint32_t c = be32toh(in32[n]); + out[n].r = (c >> 24) & 0xFF; + out[n].g = (c >> 16) & 0xFF; + out[n].b = (c >> 8) & 0xFF; + out[n].a = (c >> 0) & 0xFF; + } + return 0; +} + +PACK_FUNC(rgba32) +{ + uint32_t *out32 = out; + + for (size_t n = 0; n < ntexels; n++) { + out32[n] = htobe32((in[n].r << 24) | (in[n].g << 16) | (in[n].b << 8) | in[n].a); + } + return 0; +} + +/* CI4 */ + +UNPACK_CI_FUNC(ci4) +{ + uint8_t *in8 = in; + + unsigned sft = 4; + for (size_t n = 0; n < ntexels; n++, sft ^= 4) { + uint8_t c = (in8[n >> 1] >> sft) & 0xF; + if (!(c < npal)) + return -1; // palette too small for image data + out[n] = c; + } + return 0; +} + +PACK_CI_FUNC(ci4) +{ + uint8_t *out8 = out; + + unsigned sft = 4; + for (size_t n = 0; n < ntexels; n++, sft ^= 4) { + uint8_t c = in[n]; + if (!(c < npal)) + return -1; // palette too small for image data + if (sft == 4) + out8[n >> 1] = 0; + out8[n >> 1] |= c << sft; + } + return 0; +} + +/* CI8 */ + +UNPACK_CI_FUNC(ci8) +{ + uint8_t *in8 = in; + + for (size_t n = 0; n < ntexels; n++) { + uint8_t c = in8[n] & 0xFF; + if (!(c < npal)) + return -1; // palette too small for image data + out[n] = c; + } + return 0; +} + +PACK_CI_FUNC(ci8) +{ + uint8_t *out8 = out; + + for (size_t n = 0; n < ntexels; n++) { + uint8_t c = in[n]; + if (!(c < npal)) + return -1; // palette too small for image data + out8[n] = c; + } + return 0; +} + +/* IA4 */ + +UNPACK_FUNC(ia4) +{ + uint8_t *in8 = in; + + unsigned sft = 4; + for (size_t n = 0; n < ntexels; n++, sft ^= 4) { + uint8_t c = (in8[n >> 1] >> sft) & 0xF; + uint8_t i = c & 0b1110; + uint8_t a = c & 0b0001; + i = (i << 4) | (i << 1) | (i >> 2); + a = 255 * a; + out[n].r = i; + out[n].g = i; + out[n].b = i; + out[n].a = a; + } + return 0; +} + +PACK_FUNC(ia4) +{ + uint8_t *out8 = out; + + unsigned sft = 4; + for (size_t n = 0; n < ntexels; n++, sft ^= 4) { + uint8_t g = texel_to_greyscale(in[n].r, in[n].g, in[n].b); + uint8_t i = (g >> 5) & 0b111; + uint8_t a = in[n].a != 0; + if (sft == 4) + out8[n >> 1] = 0; + out8[n >> 1] |= ((i << 1) | a) << sft; + } + return 0; +} + +/* IA8 */ + +UNPACK_FUNC(ia8) +{ + uint8_t *in8 = in; + + for (size_t n = 0; n < ntexels; n++) { + uint8_t c = in8[n]; + uint8_t i = (c >> 4) & 0xF; + uint8_t a = (c >> 0) & 0xF; + i = (i << 4) | i; + a = (a << 4) | a; + out[n].r = i; + out[n].g = i; + out[n].b = i; + out[n].a = a; + } + return 0; +} + +PACK_FUNC(ia8) +{ + uint8_t *out8 = out; + + for (size_t n = 0; n < ntexels; n++) { + uint8_t g = texel_to_greyscale(in[n].r, in[n].g, in[n].b); + uint8_t i = (g >> 4) & 0xF; + uint8_t a = (in[n].a >> 4) & 0xF; + out8[n] = (i << 4) | a; + } + return 0; +} + +/* IA16 */ + +UNPACK_FUNC(ia16) +{ + uint16_t *in16 = in; + + for (size_t n = 0; n < ntexels; n++) { + uint16_t c = be16toh(in16[n]); + uint8_t i = (c >> 8) & 0xFF; + uint8_t a = (c >> 0) & 0xFF; + out[n].r = i; + out[n].g = i; + out[n].b = i; + out[n].a = a; + } + return 0; +} + +PACK_FUNC(ia16) +{ + uint16_t *out16 = out; + + for (size_t n = 0; n < ntexels; n++) { + uint8_t i = texel_to_greyscale(in[n].r, in[n].g, in[n].b); + uint8_t a = in[n].a; + out16[n] = htobe16((i << 8) | a); + } + return 0; +} + +/* I4 */ + +UNPACK_FUNC(i4) +{ + uint8_t *in8 = in; + + unsigned sft = 4; + for (size_t n = 0; n < ntexels; n++, sft ^= 4) { + uint8_t i = (in8[n >> 1] >> sft) & 0xF; + i = (i << 4) | i; + out[n].r = i; + out[n].g = i; + out[n].b = i; + out[n].a = i; + } + return 0; +} + +PACK_FUNC(i4) +{ + uint8_t *out8 = out; + + unsigned sft = 4; + for (size_t n = 0; n < ntexels; n++, sft ^= 4) { + uint8_t g = texel_to_greyscale(in[n].r, in[n].g, in[n].b); + uint8_t i = (g >> 4) & 0xF; + if (sft == 4) + out8[n >> 1] = 0; + out8[n >> 1] |= i << sft; + } + return 0; +} + +/* I8 */ + +UNPACK_FUNC(i8) +{ + uint8_t *in8 = in; + + for (size_t n = 0; n < ntexels; n++) { + uint8_t i = in8[n]; + out[n].r = i; + out[n].g = i; + out[n].b = i; + out[n].a = i; + } + return 0; +} + +PACK_FUNC(i8) +{ + uint8_t *out8 = out; + + for (size_t n = 0; n < ntexels; n++) { + uint8_t i = texel_to_greyscale(in[n].r, in[n].g, in[n].b); + out8[n] = i; + } + return 0; +} + +struct texel_layout { + const char *name; + union { + int (*pack)(void *out, struct color *in, size_t ntexels); + int (*pack_ci)(void *out, uint8_t *in, size_t ntexels, size_t npal); + }; + union { + int (*unpack)(struct color *out, void *in, size_t ntexels); + int (*unpack_ci)(uint8_t *out, void *in, size_t ntexels, size_t npal); + }; +}; + +#define FMT_SIZ(fmt, siz) (((fmt) << 2) | (siz)) +struct texel_layout texel_layouts[32] = { + [FMT_SIZ(G_IM_FMT_RGBA, G_IM_SIZ_16b)] = { "rgba16", .pack = rgba16_pack, .unpack = rgba16_unpack, }, + [FMT_SIZ(G_IM_FMT_RGBA, G_IM_SIZ_32b)] = { "rgba32", .pack = rgba32_pack, .unpack = rgba32_unpack, }, + [FMT_SIZ(G_IM_FMT_CI, G_IM_SIZ_4b) ] = { "ci4", .pack_ci = ci4_pack, .unpack_ci = ci4_unpack, }, + [FMT_SIZ(G_IM_FMT_CI, G_IM_SIZ_8b) ] = { "ci8", .pack_ci = ci8_pack, .unpack_ci = ci8_unpack, }, + [FMT_SIZ(G_IM_FMT_IA, G_IM_SIZ_4b) ] = { "ia4", .pack = ia4_pack, .unpack = ia4_unpack, }, + [FMT_SIZ(G_IM_FMT_IA, G_IM_SIZ_8b) ] = { "ia8", .pack = ia8_pack, .unpack = ia8_unpack, }, + [FMT_SIZ(G_IM_FMT_IA, G_IM_SIZ_16b)] = { "ia16", .pack = ia16_pack, .unpack = ia16_unpack, }, + [FMT_SIZ(G_IM_FMT_I, G_IM_SIZ_4b) ] = { "i4", .pack = i4_pack, .unpack = i4_unpack, }, + [FMT_SIZ(G_IM_FMT_I, G_IM_SIZ_8b) ] = { "i8", .pack = i8_pack, .unpack = i8_unpack, }, +}; + +static int +n64texconv_data_sync(struct color *texels, uint8_t *color_indices, size_t ntexels, int fmt, int siz, size_t pal_count) +{ + assert(texels != NULL); + + void *temp = malloc(texel_size_bytes(ntexels, siz)); + if (temp == NULL) + return -1; + + struct texel_layout *layout = &texel_layouts[FMT_SIZ(fmt, siz)]; + assert(layout != NULL); + if (fmt == G_IM_FMT_CI) { + assert(color_indices != NULL); + layout->pack_ci(temp, color_indices, ntexels, pal_count); + layout->unpack_ci(color_indices, temp, ntexels, pal_count); + // TODO update texels from color indices too? + } else { + layout->pack(temp, texels, ntexels); + layout->unpack(texels, temp, ntexels); + } + + free(temp); + return 0; +} + + + +// ******************************************************************************************************************** +// Palette +// ******************************************************************************************************************** + +static int +n64texconv_palette_sync(struct n64_palette *pal) +{ + assert(pal != NULL); + return n64texconv_data_sync(pal->texels, NULL, pal->count, pal->fmt, G_IM_SIZ_16b, 0); +} + +struct n64_palette * +n64texconv_palette_new(size_t count, int fmt) +{ + if (fmt != G_IM_FMT_RGBA && fmt != G_IM_FMT_IA) + return NULL; + + struct n64_palette *pal = malloc(sizeof(struct n64_palette) + count * sizeof(struct color)); + if (pal == NULL) + return NULL; + + pal->texels = (struct color *)(pal + 1); + pal->fmt = fmt; + pal->count = count; + return pal; +} + +void +n64texconv_palette_free(struct n64_palette *pal) +{ + assert(pal != NULL); + free(pal); +} + +struct n64_palette * +n64texconv_palette_copy(struct n64_palette *pal) +{ + assert(pal != NULL); + // Create new palette + struct n64_palette *new_pal = n64texconv_palette_new(pal->count, pal->fmt); + if (new_pal == NULL) + return NULL; + + // Copy texels + memcpy(new_pal->texels, pal->texels, new_pal->count * sizeof(struct color)); + return new_pal; +} + +struct n64_palette * +n64texconv_palette_reformat(struct n64_palette *pal, int fmt) +{ + assert(pal != NULL); + + // If the target format and source format are the same, just copy + if (pal->fmt == fmt) + return n64texconv_palette_copy(pal); + + // Create new palette + struct n64_palette *new_pal = n64texconv_palette_new(pal->count, pal->fmt); + if (new_pal == NULL) + return NULL; + + n64texconv_palette_sync(new_pal); + return new_pal; +} + +struct n64_palette * +n64texconv_palette_from_png(const char *path, int fmt) +{ + assert(path != NULL); + + FILE *png = fopen(path, "rb"); + if (png == NULL) + return NULL; + + spng_ctx *ctx = spng_ctx_new(0); + assert(ctx != NULL); + + int rv; + rv = spng_set_png_file(ctx, png); + assert(rv == 0); + + size_t ntexels; + rv = spng_decoded_image_size(ctx, SPNG_FMT_RGBA8, &ntexels); + assert(rv == 0); + + struct n64_palette *pal = n64texconv_palette_new(ntexels, fmt); + if (pal == NULL) + return NULL; + + rv = spng_decode_image(ctx, (void *)pal->texels, ntexels, SPNG_FMT_RGBA8, 0); + assert(rv == 0); + for (size_t i = 0; i < ntexels; i++) { + pal->texels[i].w = htobe32(pal->texels[i].w); + } + + spng_ctx_free(ctx); + fclose(png); + + rv = n64texconv_palette_sync(pal); + assert(rv == 0); + return pal; +} + +struct n64_palette * +n64texconv_palette_from_bin(void *data, size_t count, int fmt) +{ + assert(data != NULL); + + // Reserve new palette + struct n64_palette *pal = n64texconv_palette_new(count, fmt); + if (pal == NULL) + return NULL; + + // Unpack data + // The format is always valid at this point since the above NULL check + // would be hit if an invalid palette format was passed in. + texel_layouts[FMT_SIZ(fmt, G_IM_SIZ_16b)].unpack(pal->texels, data, count); + return pal; +} + +int +n64texconv_palette_to_png(const char *outpath, struct n64_palette *pal) +{ + assert(outpath != NULL); + assert(pal != NULL); + + // Compute arbitrary dimensions for palette + size_t width, height; + palette_dim(pal->count, &width, &height); + + // Sync data to format + + int rv; + rv = n64texconv_palette_sync(pal); + assert(rv == 0); + + // Write texels + + struct spng_ctx *ctx = spng_ctx_new(SPNG_CTX_ENCODER); + if (ctx == NULL) + return -1; + + FILE *png = fopen(outpath, "wb"); + if (png == NULL) + return -2; + + rv = spng_set_png_file(ctx, png); + assert(rv == 0); + rv = spng_set_image_limits(ctx, width, height); + assert(rv == 0); + struct spng_ihdr ihdr = { + .width = width, + .height = height, + .bit_depth = 8, + .color_type = SPNG_COLOR_TYPE_TRUECOLOR_ALPHA, + .compression_method = 0, + .filter_method = 0, + .interlace_method = 0, + }; + rv = spng_set_ihdr(ctx, &ihdr); + assert(rv == 0); + rv = spng_encode_image(ctx, pal->texels, pal->count * sizeof(struct color), SPNG_FMT_PNG, SPNG_ENCODE_FINALIZE); + assert(rv == 0); + spng_ctx_free(ctx); + + fclose(png); + return 0; +} + +void * +n64texconv_palette_to_bin(struct n64_palette *pal, bool pad_to_8b) +{ + assert(pal != NULL); + + // Prepare output buffer + size_t size_unaligned = texel_size_bytes(pal->count, G_IM_SIZ_16b); + size_t size_aligned = ALIGN8(size_unaligned); + void *out = malloc(size_aligned); + if (out == NULL) + return NULL; + + // Pack into output format + assert(pal->fmt == G_IM_FMT_RGBA || pal->fmt == G_IM_FMT_IA); + texel_layouts[FMT_SIZ(pal->fmt, G_IM_SIZ_16b)].pack(out, pal->texels, pal->count); + + // Pad to 8-byte alignment if requested + if (pad_to_8b && size_aligned != size_unaligned) + memset((uint8_t *)out + size_unaligned, 0, size_aligned - size_unaligned); + + return out; +} + +int +n64texconv_palette_to_c(char **out, size_t *size_out, struct n64_palette *pal, bool pad_to_8b, unsigned int byte_width) +{ + assert(out != NULL); + assert(size_out != NULL); + assert(pal != NULL); + + size_t nbytes = texel_size_bytes(pal->count, G_IM_SIZ_16b); + void *bin = n64texconv_palette_to_bin(pal, pad_to_8b); + if (bin == NULL) + return -1; + nbytes = pad_to_8b ? ALIGN8(nbytes) : nbytes; + int rv = bin2c(out, size_out, bin, nbytes, 0, byte_width); + free(bin); + return rv; +} + +int +n64texconv_palette_to_c_file(const char *out_path, struct n64_palette *pal, bool pad_to_8b, unsigned int byte_width) +{ + assert(out_path != NULL); + assert(pal != NULL); + + size_t nbytes = texel_size_bytes(pal->count, G_IM_SIZ_16b); + void *bin = n64texconv_palette_to_bin(pal, pad_to_8b); + if (bin == NULL) + return -1; + nbytes = pad_to_8b ? ALIGN8(nbytes) : nbytes; + int rv = bin2c_file(out_path, bin, nbytes, 0, byte_width); + free(bin); + return rv; +} + + + +// ******************************************************************************************************************** +// Image +// ******************************************************************************************************************** + +static int +n64texconv_image_sync(struct n64_image *img) +{ + assert(img != NULL); + return n64texconv_data_sync(img->texels, img->color_indices, img->width * img->height, img->fmt, img->siz, + (img->pal != NULL) ? img->pal->count : 0); +} + +struct n64_image * +n64texconv_image_new(size_t width, size_t height, int fmt, int siz, struct n64_palette *pal) +{ + // TODO if fmt is CI, assert(pal != NULL) ? + if (fmt >= FMT_MAX || siz >= SIZ_MAX) + return NULL; + + size_t ntexels = width * height; + struct n64_image *img = malloc(sizeof(struct n64_image) + ntexels * (sizeof(struct color) + sizeof(uint8_t))); + if (img == NULL) + return NULL; + + struct color *texels = (void *)((uint8_t *)img + sizeof(struct n64_image)); + uint8_t *color_indices = (fmt == G_IM_FMT_CI) ? (uint8_t *)(texels + ntexels) : NULL; + + img->width = width; + img->height = height; + img->fmt = fmt; + img->siz = siz; + img->pal = pal; + img->texels = texels; + img->color_indices = color_indices; + return img; +} + +void +n64texconv_image_free(struct n64_image *img) +{ + assert(img != NULL); + free(img); +} + +struct n64_image * +n64texconv_image_copy(struct n64_image *img) +{ + if (img == NULL) + return NULL; + + // Create new image + struct n64_image *new_img = n64texconv_image_new(img->width, img->height, img->fmt, img->siz, img->pal); + if (new_img == NULL) + return NULL; + + // Copy texels/index data + size_t ntexels = new_img->width * new_img->height; + memcpy(new_img->texels, img->texels, ntexels * sizeof(struct color)); + if (img->fmt == G_IM_FMT_CI) // == new_img->fmt + memcpy(new_img->color_indices, img->color_indices, ntexels * sizeof(uint8_t)); + return new_img; +} + +/** + * Reads in the png image at `path`. + * + * For all choices of `fmt`, the png data is converted to a common RGBA32 format and stored that way, but may be + * subject to various edits described below. + * + * - If `fmt` is None (default), the best format and texel size are determined from the RGBA32 data, which is then + * processed accordingly (described below). + * + * - If `fmt` is RGBA, the RGBA32 texels are left as-is, with possible reduction if `siz` is 16-bit. + * + * - If `fmt` is greyscale (I or IA), the RGBA32 texels are converted to greyscale. If the texels were already + * greyscale this operation will leave them unaffected. + * + * - If `fmt` is CI and the png is an indexed png, the palette and indices in the png are saved in addition to the + * texels themselves. This is required for a roundtrip conversion of 'bin -> png -> file -> png -> bin' to match. + * + * - If `fmt` is CI and the png is not indexed, the image is quantized and a palette is generated automatically. + */ +struct n64_image * +n64texconv_image_from_png(const char *path, int fmt, int siz, int pal_fmt) +{ + assert(path != NULL); + + FILE *png = fopen(path, "rb"); + if (png == NULL) + return NULL; // Input file not found + + spng_ctx *ctx = spng_ctx_new(0); + int rv = spng_set_png_file(ctx, png); + assert(rv == 0); + + struct spng_ihdr ihdr; + rv = spng_get_ihdr(ctx, &ihdr); + assert(rv == 0); + + size_t nbytes; + rv = spng_decoded_image_size(ctx, SPNG_FMT_RGBA8, &nbytes); + assert(rv == 0); + + uint32_t width = ihdr.width; + uint32_t height = ihdr.height; + + struct n64_image *img = n64texconv_image_new(width, height, fmt, siz, NULL); + if (img == NULL) + goto error_post_open_png; + + struct n64_palette *pal = NULL; + + if (fmt == G_IM_FMT_CI) { + assert(siz == G_IM_SIZ_4b || siz == G_IM_SIZ_8b); + assert(pal_fmt != FMT_NONE); + + size_t max_colors = (siz == G_IM_SIZ_8b) ? 256 : 16; + + if (ihdr.color_type == SPNG_COLOR_TYPE_INDEXED) { + // Input is an indexed png, obtain palette and color indices + struct spng_plte plte; + rv = spng_get_plte(ctx, &plte); + assert(rv == 0); // must have a palette chunk if it's indexed + + // Palette should not have 0 entries + if (plte.n_entries == 0) + goto error_post_create_img; + + // TODO ZAPD always writes 256-color palettes which breaks this, enable it when we can +#if 0 + // Palette must have sufficiently few colors for the target format. If there are too + // many, requantize to the maximum amount for the target format. + if (plte.n_entries > max_colors) + goto requantize; +#endif + + pal = n64texconv_palette_new(plte.n_entries, pal_fmt); + if (pal == NULL) + goto error_post_create_img; + + struct spng_trns trns; + rv = spng_get_trns(ctx, &trns); + if (rv == 0) { + // Read palette transparency from here + assert(trns.n_type3_entries == plte.n_entries); + for (size_t i = 0; i < plte.n_entries; i++) { + pal->texels[i].r = plte.entries[i].red; + pal->texels[i].g = plte.entries[i].green; + pal->texels[i].b = plte.entries[i].blue; + pal->texels[i].a = trns.type3_alpha[i]; + } + } else { + // Treat as opaque if trns is not present + for (size_t i = 0; i < plte.n_entries; i++) { + pal->texels[i].r = plte.entries[i].red; + pal->texels[i].g = plte.entries[i].green; + pal->texels[i].b = plte.entries[i].blue; + pal->texels[i].a = 0xFF; + } + } + + // Fill color indices + size_t nidxbytes; + rv = spng_decoded_image_size(ctx, SPNG_FMT_RAW, &nidxbytes); + assert(rv == 0); + assert(nidxbytes * 4 == nbytes); + rv = spng_decode_image(ctx, (void *)img->color_indices, nidxbytes, SPNG_FMT_RAW, 0); + assert(rv == 0); + } else { + // Input is not an indexed png, quantize and generate palette +#if 0 + requantize: // Input is an indexed png but has too many colors, requantize with new palette +#endif + rv = spng_decode_image(ctx, (void *)img->texels, nbytes, SPNG_FMT_RGBA8, 0); + assert(rv == 0); + + pal = n64texconv_palette_new(max_colors, pal_fmt); + if (pal == NULL) + goto error_post_create_img; + + n64texconv_quantize(img->color_indices, pal->texels, &pal->count, img->texels, width, height, max_colors, + 0.5f); + } + + // Populate texels from color indices and palette + for (size_t i = 0; i < width * height; i++) + img->texels[i] = pal->texels[img->color_indices[i]]; + } else { + rv = spng_decode_image(ctx, (void *)img->texels, nbytes, SPNG_FMT_RGBA8, 0); + assert(rv == 0); + } + spng_ctx_free(ctx); + fclose(png); + + img->pal = pal; + + n64texconv_image_sync(img); + return img; + +error_post_create_img: + n64texconv_image_free(img); +error_post_open_png: + spng_ctx_free(ctx); + fclose(png); + return NULL; +} + +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) +{ + assert(data != NULL); + assert((fmt == G_IM_FMT_CI && pal != NULL) || (fmt != G_IM_FMT_CI && pal == NULL)); + + size_t ntexels = width * height; + struct n64_image *img = n64texconv_image_new(width, height, fmt, siz, pal); + if (img == NULL) + return NULL; + + if (preswapped) { + // TODO unswap data + } + + struct texel_layout *layout = &texel_layouts[FMT_SIZ(fmt, siz)]; + assert(layout != NULL); + + if (fmt == G_IM_FMT_CI) { + layout->unpack_ci(img->color_indices, data, ntexels, img->pal->count); + for (size_t i = 0; i < ntexels; i++) + img->texels[i].w = pal->texels[img->color_indices[i]].w; + } else { + layout->unpack(img->texels, data, ntexels); + } + return img; +} + +/** + * Given an existing image, create a new image with the new format and size. + * + * This operation is very often a lossy process, do not expect matching round-trips through reformat; depending on the + * target format quality is often lost, sometimes severely. + * + * This process is typically for converting truecolor png to the optimal N64 format if the format was left unspecified + * when reading in the png, in a matching context the format should always be explicitly specified so that no + * reformatting is done. + */ +struct n64_image * +n64texconv_image_reformat(struct n64_image *img, int fmt, int siz, struct n64_palette *new_pal) +{ + assert(img != NULL); + + if (fmt == FMT_NONE || siz == SIZ_NONE) { + // TODO determine optimal format for data + } + + size_t width = img->width; + size_t height = img->height; + size_t ntexels = width * height; + + struct n64_image *new_img = n64texconv_image_new(width, height, fmt, siz, NULL); + if (new_img == NULL) + return NULL; + + bool src_ci = img->fmt == G_IM_FMT_CI; + bool dst_ci = fmt == G_IM_FMT_CI; + bool siz_changed = siz != img->siz; + + if (dst_ci && (!src_ci || siz_changed)) { + // Converting non-CI to CI, or CI->CI of different size + + size_t max_colors = (siz == G_IM_SIZ_8b) ? 256 : 16; + + // Set color indices pointer if coming from non-CI + if (new_img->color_indices == NULL) + new_img->color_indices = (void *)(new_img->texels + ntexels); + + if (new_pal != NULL) { + // Use the provided new palette + new_img->pal = new_pal; + + // Make sure the new palette is of an appropriate size for the bit depth + if (new_pal->count > max_colors) { + n64texconv_image_free(new_img); + return NULL; + } + + // Map to the palette + for (size_t i = 0; i < width * height; i++) { + for (size_t j = 0; j < new_pal->count; j++) { + if (new_img->texels[i].w == new_pal->texels[j].w) { + new_img->color_indices[i] = j; + goto got; + } + } + // Failed to find texel in palette + n64texconv_image_free(new_img); + return NULL; + got:; + } + } else { + // Generate a new palette + struct n64_palette *pal = n64texconv_palette_new(max_colors, G_IM_FMT_RGBA); // TODO IA palettes + if (pal == NULL) { + n64texconv_image_free(new_img); + return NULL; + } + new_img->pal = pal; + + // Quantize and fill color indices + n64texconv_quantize(new_img->color_indices, pal->texels, &pal->count, new_img->texels, width, height, + max_colors, 0.5f); + // Replace old texels + for (size_t i = 0; i < width * height; i++) { + new_img->texels[i] = pal->texels[new_img->color_indices[i]]; + } + } + } else { + // Copy texels + memcpy(new_img->texels, img->texels, ntexels * sizeof(struct color)); + if (dst_ci) + memcpy(new_img->color_indices, img->color_indices, ntexels * sizeof(uint8_t)); + } + + // Invalidate color indices if non-CI + if (!dst_ci) + new_img->color_indices = NULL; + + // Sync texels to format + n64texconv_image_sync(new_img); + //#pragma GCC diagnostic push + //#pragma GCC diagnostic ignored "-Wanalyzer-malloc-leak" + // Annoyingly the GCC static analyzer considers pal to be leaked here, but it's reachable by the assignment + // to new_img... + return new_img; + //#pragma GCC diagnostic pop +} + +/** + * Writes this texture out to a png file. If this texture is CI and was read in from binary or from an indexed png it + * will preserve the color indices and palette of the original if it has not since been reformatted in order to + * facilitate matching in a roundtrip of 'bin -> png -> file -> png -> bin'. + */ +int +n64texconv_image_to_png(const char *outpath, struct n64_image *img, bool intensity_alpha) +{ + assert(outpath != NULL); + assert(img != NULL); + + // For intensity images, we don't always want to write alpha (but sometimes we do), intensity images can be either + // RGB channel or Alpha channel but often not both. The `intensity_alpha` option determines whether to write PNGs + // with or without alpha, both will roundtrip identically. + bool has_alpha = (img->fmt != G_IM_FMT_I) || intensity_alpha; + + size_t ntexels = img->width * img->height; + + if (img->fmt == G_IM_FMT_CI) { + assert(img->color_indices != NULL); // CI textures must have color indices + assert(img->pal != NULL); // Writing CI to png must have a palette supplied + n64texconv_palette_sync(img->pal); + } + + // Ensure output is visually precise + n64texconv_image_sync(img); + + // Write the png, either truecolor or indexed + + FILE *png = fopen(outpath, "wb"); + if (png == NULL) + return -1; + struct spng_ctx *ctx = spng_ctx_new(SPNG_CTX_ENCODER); + int rv = spng_set_png_file(ctx, png); + assert(rv == 0); + rv = spng_set_image_limits(ctx, img->width, img->height); + assert(rv == 0); + + uint8_t color_type; + if (img->fmt == G_IM_FMT_CI) { + color_type = SPNG_COLOR_TYPE_INDEXED; + } else { + color_type = (has_alpha) ? SPNG_COLOR_TYPE_TRUECOLOR_ALPHA : SPNG_COLOR_TYPE_TRUECOLOR; + } + + struct spng_ihdr ihdr = { + .width = img->width, + .height = img->height, + .bit_depth = 8, + .color_type = color_type, + .compression_method = 0, + .filter_method = 0, + .interlace_method = 0, + }; + rv = spng_set_ihdr(ctx, &ihdr); + assert(rv == 0); + + if (img->fmt == G_IM_FMT_CI) { + struct n64_palette *pal = img->pal; + assert(pal != NULL); + + // Ensure palette is visually precise + rv = n64texconv_palette_sync(pal); + assert(rv == 0); + + // Write palette + struct spng_plte plte = { 0 }; + plte.n_entries = pal->count; + assert(pal->count <= 256); + memcpy(plte.entries, pal->texels, pal->count * 4); + rv = spng_set_plte(ctx, &plte); + assert(rv == 0); + + // Write palette alphas + struct spng_trns trns = { 0 }; + trns.n_type3_entries = pal->count; + for (size_t i = 0; i < pal->count; i++) { + trns.type3_alpha[i] = pal->texels[i].a; + } + rv = spng_set_trns(ctx, &trns); + assert(rv == 0); + + // Write color indices + rv = spng_encode_image(ctx, img->color_indices, ntexels * sizeof(uint8_t), SPNG_FMT_PNG, SPNG_ENCODE_FINALIZE); + assert(rv == 0); + } else { + void *out = img->texels; + size_t outsize = ntexels * sizeof(struct color); + if (!has_alpha) { + uint8_t *rgb = malloc(ntexels * 3); + assert(rgb != NULL); + size_t j = 0; + for (size_t i = 0; i < ntexels; i++) { + rgb[j++] = img->texels[i].r; + rgb[j++] = img->texels[i].g; + rgb[j++] = img->texels[i].b; + } + out = rgb; + outsize = ntexels * 3; + } + // Write texels + rv = spng_encode_image(ctx, out, outsize, SPNG_FMT_PNG, SPNG_ENCODE_FINALIZE); + assert(rv == 0); + if (!has_alpha) + free(out); + } + + spng_ctx_free(ctx); + fclose(png); + + return 0; +} + +void * +n64texconv_image_to_bin(struct n64_image *img, bool pad_to_8b, bool preswap) +{ + assert(img != NULL); + + size_t ntexels = img->width * img->height; + int fmt = img->fmt; + int siz = img->siz; + + // Reserve output buffer + size_t size_bytes = texel_size_bytes(ntexels, siz); + size_t size_bytes_aligned = ALIGN8(size_bytes); + void *out = malloc(size_bytes_aligned); + if (out == NULL) + return NULL; + + // Pack into output format + struct texel_layout *layout = &texel_layouts[FMT_SIZ(fmt, siz)]; + assert(layout != NULL); + if (fmt == G_IM_FMT_CI) { + assert(img->pal != NULL); + layout->pack_ci(out, img->color_indices, ntexels, img->pal->count); + } else { + assert(img->pal == NULL); + layout->pack(out, img->texels, ntexels); + } + + // Pad to 8-byte alignment if requested + if (pad_to_8b && size_bytes != size_bytes_aligned) { + memset((uint8_t *)out + size_bytes, 0, size_bytes_aligned - size_bytes); + } + + // Perform dxt=0 word swapping if requested + if (preswap) { + // TODO implement preswap + } + + return out; +} + +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) +{ + assert(out != NULL); + assert(size_out != NULL); + assert(img != NULL); + + size_t nbytes = texel_size_bytes(img->width * img->height, img->siz); + void *bin = n64texconv_image_to_bin(img, pad_to_8b, preswap); + if (bin == NULL) + return -1; + nbytes = pad_to_8b ? ALIGN8(nbytes) : nbytes; + int rv = bin2c(out, size_out, bin, nbytes, 0, byte_width); + free(bin); + return rv; +} + +int +n64texconv_image_to_c_file(const char *out_path, struct n64_image *img, bool pad_to_8b, bool preswap, + unsigned int byte_width) +{ + assert(out_path != NULL); + assert(img != NULL); + + size_t nbytes = texel_size_bytes(img->width * img->height, img->siz); + void *bin = n64texconv_image_to_bin(img, pad_to_8b, preswap); + if (bin == NULL) + return -1; + nbytes = pad_to_8b ? ALIGN8(nbytes) : nbytes; + int rv = bin2c_file(out_path, bin, nbytes, 0, byte_width); + free(bin); + return rv; +} + + + +// ******************************************************************************************************************** +// Others +// ******************************************************************************************************************** + +const char * +n64texconv_png_extension(struct n64_image *img) +{ + assert(img != NULL); + + struct texel_layout *layout = &texel_layouts[FMT_SIZ(img->fmt, img->siz)]; + assert(layout != NULL); + return layout->name; +} + +/* This is just for the python interface */ +void +n64texconv_free(void *p) +{ + assert(p != NULL); + free(p); +} diff --git a/tools/assets/n64texconv/src/libn64texconv/n64texconv.h b/tools/assets/n64texconv/src/libn64texconv/n64texconv.h new file mode 100644 index 0000000000..1c2dd98fcc --- /dev/null +++ b/tools/assets/n64texconv/src/libn64texconv/n64texconv.h @@ -0,0 +1,122 @@ +/* SPDX-FileCopyrightText: Copyright (C) 2025 ZeldaRET */ +/* SPDX-License-Identifier: MIT */ +#ifndef N64TEXCONV_H +#define N64TEXCONV_H + +#include +#include +#include + +#define FMT_NONE -1 +#define FMT_MAX 5 +#define G_IM_FMT_RGBA 0 +#define G_IM_FMT_YUV 1 +#define G_IM_FMT_CI 2 +#define G_IM_FMT_IA 3 +#define G_IM_FMT_I 4 + +#define SIZ_NONE -1 +#define SIZ_MAX 4 +#define G_IM_SIZ_4b 0 +#define G_IM_SIZ_8b 1 +#define G_IM_SIZ_16b 2 +#define G_IM_SIZ_32b 3 + +struct color { + union { + struct { + uint8_t r, g, b, a; + }; + uint32_t w; + }; +}; + +static inline __attribute__((always_inline)) size_t +texel_size_bytes(size_t ntexels, int siz) +{ + return (siz == G_IM_SIZ_4b) ? (ntexels / 2) : (ntexels * ((1 << (unsigned)siz) >> 1)); +} + +struct n64_palette { + struct color *texels; + int fmt; + size_t count; +}; + +struct n64_palette * +n64texconv_palette_new(size_t count, int fmt); + +void +n64texconv_palette_free(struct n64_palette *pal); + +struct n64_palette * +n64texconv_palette_copy(struct n64_palette *pal); + +struct n64_palette * +n64texconv_palette_reformat(struct n64_palette *pal, int fmt); + +struct n64_palette * +n64texconv_palette_from_png(const char *path, int fmt); + +struct n64_palette * +n64texconv_palette_from_bin(void *data, size_t count, int fmt); + +int +n64texconv_palette_to_png(const char *outpath, struct n64_palette *pal); + +void * +n64texconv_palette_to_bin(struct n64_palette *pal, bool pad_to_8b); + +int +n64texconv_palette_to_c(char **out, size_t *size_out, struct n64_palette *pal, bool pad_to_8b, unsigned int byte_width); + +int +n64texconv_palette_to_c_file(const char *out_path, struct n64_palette *pal, bool pad_to_8b, unsigned int byte_width); + +struct n64_image { + size_t width; + size_t height; + int fmt; + int siz; + struct n64_palette *pal; + struct color *texels; + uint8_t *color_indices; +}; + +struct n64_image * +n64texconv_image_new(size_t width, size_t height, int fmt, int siz, struct n64_palette *pal); + +void +n64texconv_image_free(struct n64_image *img); + +struct n64_image * +n64texconv_image_copy(struct n64_image *img); + +struct n64_image * +n64texconv_image_from_png(const char *path, int fmt, int siz, int pal_fmt); + +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); + +struct n64_image * +n64texconv_image_reformat(struct n64_image *img, int fmt, int siz, struct n64_palette *pal); + +int +n64texconv_image_to_png(const char *outpath, struct n64_image *img, bool intensity_alpha); + +void * +n64texconv_image_to_bin(struct n64_image *img, bool pad_to_8b, bool preswap); + +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); + +int +n64texconv_image_to_c_file(const char *out_path, struct n64_image *img, bool pad_to_8b, bool preswap, + unsigned int byte_width); + +const char * +n64texconv_png_extension(struct n64_image *img); + +#endif diff --git a/tools/bin2c.c b/tools/bin2c.c new file mode 100644 index 0000000000..3faa3be54c --- /dev/null +++ b/tools/bin2c.c @@ -0,0 +1,174 @@ +/* SPDX-FileCopyrightText: (C) 2025 ZeldaRET */ +/* SPDX-License-Identifier: MIT */ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "endian.h" + +#define NORETURN __attribute__((noreturn)) + +static void NORETURN +verror(const char *fmt, va_list args) +{ + fputs("\x1b[91merror\x1b[97m: ", stderr); + vfprintf(stderr, fmt, args); + fputs("\x1b[0m", stderr); + exit(EXIT_FAILURE); +} + +static void NORETURN +error(const char *fmt, ...) +{ + va_list args; + va_start(args, fmt); + verror(fmt, args); + va_end(args); +} + +static void NORETURN +error_post_open(const char *path_to_rm, FILE *file_to_rm, const char *fmt, ...) +{ + // cleanup output file + fclose(file_to_rm); + if (remove(path_to_rm) != 0) + fprintf(stderr, "error calling remove(): %s", strerror(errno)); + + // error as normal + va_list args; + va_start(args, fmt); + verror(fmt, args); + va_end(args); +} + +static void NORETURN +usage(const char *progname) +{ + fprintf(stderr, + "usage: %s -t [-pad] input.bin output.inc.c" "\n" + " fmt must be one of { 1, 2, 4, 8 }" "\n" + " if pad, align to fmt by filling with 0s, otherwise error if input is not aligned" "\n", + progname); + exit(EXIT_FAILURE); +} + +#define BYTES_PER_ROW 32 +#define LINE_MASK (BYTES_PER_ROW - 1) + +int +main(int argc, char **argv) +{ + const char *progname = argv[0]; + const char *inpath = NULL; + const char *outpath = NULL; + const char *fmt_opt = NULL; + int fmt = 0; + bool pad = false; + + // Collect arguments + + int narg = 0; + for (int i = 1; i < argc; i++) { + if (argv[i][0] == '-') { + if (strcmp(argv[i], "-t") == 0) { + if (++i == argc) + usage(progname); + fmt_opt = argv[i]; + char *end; + fmt = strtol(argv[i], &end, 10); + if (end != &argv[i][strlen(argv[i])]) + error("Invalid base 10 integer %s\n", argv[i]); + } else if (strcmp(argv[i], "-pad") == 0) { + pad = true; + } else { + usage(progname); + } + } else { + switch (narg) { + case 0: + inpath = argv[i]; + break; + case 1: + outpath = argv[i]; + break; + default: + usage(progname); + break; + } + narg++; + } + } + + // Check arguments + + if (inpath == NULL || outpath == NULL || fmt_opt == NULL) + usage(progname); + + if (fmt != 1 && fmt != 2 && fmt != 4 && fmt != 8) + error("Invalid fmt option '%s'. Valid options: [1, 2, 4, 8]\n", fmt_opt); + + // Open the input binary file + + FILE *infile = fopen(inpath, "rb"); + if (infile == NULL) + error("Failed to open input file '%s' for reading: %s\n", inpath, strerror(errno)); + + // Get size + + if (fseek(infile, 0, SEEK_END) != 0) + error("Could not ascertain input file size, could not seek to end: %s\n", strerror(errno)); + size_t file_size = ftell(infile); + if (fseek(infile, 0, SEEK_SET) != 0) + error("Could not ascertain input file size, could not seek to start: %s\n", strerror(errno)); + + // Check alignment + + if ((file_size & (fmt - 1)) != 0 && !pad) { + // Not aligned to data size and don't pad, error + error("Input file '%s' size (%lu) is not aligned to %d bytes\n", inpath, file_size, fmt); + } + + // Open the output text file + + FILE *outfile = fopen(outpath, "w"); + if (outfile == NULL) + error("Failed to open output file '%s' for writing: %s\n", outpath, strerror(errno)); + + // Write data. If the input binary size was not aligned we either don't get this far or the option + // to pad with 0s is set. + + bool was_newline = false; + for (size_t p = 0; p < file_size; p += fmt) { + size_t rem = fmt; + if (rem > file_size - p) // For any remaining unaligned data, rest will be padded with 0 + rem = file_size - p; + + // Read input + uint64_t d = 0; + if (fread(&d, 1, rem, infile) != rem) + error_post_open(outpath, outfile, "Error reading from input file '%s': %s\n", inpath, strerror(errno)); + + // Byteswap + shift + d = be64toh(d) >> (64 - 8 * fmt); + + // Write output + bool was_newline = (((p + fmt) & LINE_MASK) == 0); + char end = was_newline ? '\n' : ' '; + if (fprintf(outfile, "0x%0*" PRIX64 ",%c", 2 * fmt, d, end) < 0) + error_post_open(outpath, outfile, "Error writing to output file '%s': %s\n", outpath, strerror(errno)); + } + if (!was_newline) { + if (fputs("\n", outfile) == EOF) + error_post_open(outpath, outfile, "Error writing to output file '%s': %s\n", outpath, strerror(errno)); + } + + fclose(infile); + fclose(outfile); + return EXIT_SUCCESS; +} diff --git a/tools/endian.h b/tools/endian.h new file mode 100644 index 0000000000..d69e2e6670 --- /dev/null +++ b/tools/endian.h @@ -0,0 +1,67 @@ +#ifndef ENDIAN_H_ +#define ENDIAN_H_ + +#if defined(__linux__) || defined(__CYGWIN__) +#include +#elif defined(__APPLE__) +#include + +#define htobe16(x) OSSwapHostToBigInt16(x) +#define htole16(x) OSSwapHostToLittleInt16(x) +#define be16toh(x) OSSwapBigToHostInt16(x) +#define le16toh(x) OSSwapLittleToHostInt16(x) + +#define htobe32(x) OSSwapHostToBigInt32(x) +#define htole32(x) OSSwapHostToLittleInt32(x) +#define be32toh(x) OSSwapBigToHostInt32(x) +#define le32toh(x) OSSwapLittleToHostInt32(x) + +#define htobe64(x) OSSwapHostToBigInt64(x) +#define htole64(x) OSSwapHostToLittleInt64(x) +#define be64toh(x) OSSwapBigToHostInt64(x) +#define le64toh(x) OSSwapLittleToHostInt64(x) +#else + +#if !defined(__BYTE_ORDER__) +#error "No endian define provided by compiler" +#endif + +#if (__BYTE_ORDER__ == __ORDER_BIG_ENDIAN__) + +#define htobe16(x) (x) +#define htole16(x) __builtin_bswap16(x) +#define be16toh(x) (x) +#define le16toh(x) __builtin_bswap16(x) + +#define htobe32(x) (x) +#define htole32(x) __builtin_bswap32(x) +#define be32toh(x) (x) +#define le32toh(x) __builtin_bswap32(x) + +#define htobe64(x) (x) +#define htole64(x) __builtin_bswap64(x) +#define be64toh(x) (x) +#define le64toh(x) __builtin_bswap64(x) + +#else + +#define htobe16(x) __builtin_bswap16(x) +#define htole16(x) (x) +#define be16toh(x) __builtin_bswap16(x) +#define le16toh(x) (x) + +#define htobe32(x) __builtin_bswap32(x) +#define htole32(x) (x) +#define be32toh(x) __builtin_bswap32(x) +#define le32toh(x) (x) + +#define htobe64(x) __builtin_bswap64(x) +#define htole64(x) (x) +#define be64toh(x) __builtin_bswap64(x) +#define le64toh(x) (x) + +#endif + +#endif + +#endif