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