mirror of
https://github.com/Nexoniarz/YAPF.git
synced 2026-11-03 13:04:26 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
06a4e7db9e |
||
|
|
11ebb12270 |
||
|
|
0ba1f0af17 |
||
|
|
fdbd0a6b8f |
||
|
|
a6c032d824 |
||
|
|
83588d8e2a |
||
|
|
7b1af7f275 |
||
|
|
fb7212c62b |
||
|
|
6afd0488aa |
||
|
|
a226b06368 |
||
|
|
5642b8c85d |
||
|
|
d7f494a0c9 | ||
|
|
2ca726e3af | ||
|
|
b9b8c93a22 |
||
|
|
0a2b234169 |
||
|
|
56b1837b76 |
||
|
|
624d95fe71 |
||
|
|
e5f076cf8a |
||
|
|
d5e01b9db9 |
||
|
|
64e18fd593 |
||
|
|
6ec9d0b7f0 |
||
|
|
55fc84ca49 |
||
|
|
510ba7fd42 |
||
|
|
d218fbaa8f |
||
|
|
01f5e4f9fe |
||
|
|
f4754d81fd |
||
|
|
021dd6b1ae |
||
|
|
af3793b453 |
||
|
|
8c11a2f426 |
||
|
|
b17a7219cf |
||
|
|
c98090a1f3 |
||
|
|
f021609a4f |
||
|
|
2b5679806f |
||
|
|
e6406519f2 |
||
|
|
31da3f5ac5 |
||
|
|
39e3959038 |
||
|
|
d0dbd3ecf5 |
||
|
|
75fd56ccc9 |
||
|
|
9d5405e679 |
||
|
|
0a8ab71de9 |
||
|
|
9f6beedb1c |
||
|
|
2927009b5f |
||
|
|
2991f6faba |
||
|
|
867b29561c |
||
|
|
96c2f451ec |
||
|
|
1854c6f482 |
||
|
|
9477a9955a |
||
|
|
e65859c880 |
||
|
|
63dbe54400 |
||
|
|
6d96de00dd |
||
|
|
1ed497c872 |
||
|
|
8056537dc5 |
||
|
|
98b4d65798 |
||
|
|
9786bbe917 |
||
|
|
737deaf2d5 |
||
|
|
05bbaabedc |
||
|
|
71dfc2a073 |
||
|
|
95f269d4fc |
||
|
|
374e4e4ff1 |
||
|
|
79a46b3ddf |
||
|
|
0bd5fc6f4c |
||
|
|
0ffc53fc0a |
||
|
|
5826fa0813 |
||
|
|
6a9d921be3 |
||
|
|
a58550e74f |
||
|
|
21c70ba18e |
107 changed files with 25742 additions and 295 deletions
25
.gitignore
vendored
Normal file
25
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
# build outputs
|
||||||
|
/build/
|
||||||
|
/dist/
|
||||||
|
*.o
|
||||||
|
*.a
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
*.dll
|
||||||
|
*.exe
|
||||||
|
*.vsix
|
||||||
|
test_yapf_tmp.yapf
|
||||||
|
|
||||||
|
# generated: copied from bindings/js/yapf.js when packaging
|
||||||
|
/extensions/vscode/media/yapf.js
|
||||||
|
node_modules/
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
|
||||||
|
# Rust
|
||||||
|
/bindings/rust/target/
|
||||||
|
/bindings/rust/Cargo.lock
|
||||||
|
# C# / Go / Java build outputs
|
||||||
|
bin/
|
||||||
|
obj/
|
||||||
|
*.class
|
||||||
214
LICENSE
214
LICENSE
|
|
@ -1,21 +1,201 @@
|
||||||
MIT License
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
Copyright (c) 2026 Nexoniarz
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
1. Definitions.
|
||||||
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
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
copies or substantial portions of the Software.
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
the copyright owner that is granting the License.
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
other entities that control, are controlled by, or are under common
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
control with that entity. For the purposes of this definition,
|
||||||
SOFTWARE.
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright [yyyy] [name of copyright owner]
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
|
|
||||||
65
Makefile
Normal file
65
Makefile
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
# YAPF — simple build for Linux, macOS and other Unix systems.
|
||||||
|
#
|
||||||
|
# make library (static + shared) and the `yapf` tool, in build/
|
||||||
|
# make test run the C test suite (and the JS one if node is present)
|
||||||
|
# sudo make install install header, libraries and tool under $(PREFIX)
|
||||||
|
# make NO_SIMD=1 plain C only
|
||||||
|
#
|
||||||
|
# Windows: use build.bat (LLVM) or any MinGW / MSVC compiler on yapf.c.
|
||||||
|
|
||||||
|
CC ?= cc
|
||||||
|
CFLAGS ?= -O2
|
||||||
|
PREFIX ?= /usr/local
|
||||||
|
BUILD ?= build
|
||||||
|
|
||||||
|
YAPF_CFLAGS = -std=c99 -Wall -Wextra $(CFLAGS) $(if $(NO_SIMD),-DYAPF_NO_SIMD)
|
||||||
|
LDLIBS = -lm -pthread
|
||||||
|
|
||||||
|
ifeq ($(shell uname -s),Darwin)
|
||||||
|
SHARED = libyapf.dylib
|
||||||
|
SHARED_FLAGS = -dynamiclib -install_name $(PREFIX)/lib/libyapf.dylib
|
||||||
|
else
|
||||||
|
SHARED = libyapf.so
|
||||||
|
SHARED_FLAGS = -shared
|
||||||
|
endif
|
||||||
|
|
||||||
|
all: $(BUILD)/libyapf.a $(BUILD)/$(SHARED) $(BUILD)/yapf
|
||||||
|
|
||||||
|
$(BUILD):
|
||||||
|
mkdir -p $(BUILD)
|
||||||
|
|
||||||
|
$(BUILD)/yapf.o: yapf.c yapf.h | $(BUILD)
|
||||||
|
$(CC) $(YAPF_CFLAGS) -fPIC -c yapf.c -o $@
|
||||||
|
|
||||||
|
$(BUILD)/libyapf.a: $(BUILD)/yapf.o
|
||||||
|
$(AR) rcs $@ $^
|
||||||
|
|
||||||
|
$(BUILD)/$(SHARED): $(BUILD)/yapf.o
|
||||||
|
$(CC) $(SHARED_FLAGS) $^ -o $@ -pthread
|
||||||
|
|
||||||
|
$(BUILD)/yapf: tools/yapf_cli.c yapf.c yapf.h | $(BUILD)
|
||||||
|
$(CC) $(YAPF_CFLAGS) tools/yapf_cli.c yapf.c -o $@ $(LDLIBS)
|
||||||
|
|
||||||
|
$(BUILD)/test_yapf: tests/test_yapf.c yapf.c yapf.h | $(BUILD)
|
||||||
|
$(CC) $(YAPF_CFLAGS) tests/test_yapf.c yapf.c -o $@ $(LDLIBS)
|
||||||
|
|
||||||
|
test: $(BUILD)/test_yapf
|
||||||
|
$(BUILD)/test_yapf other/YAPF.YAPF
|
||||||
|
@if command -v node >/dev/null 2>&1; then node bindings/js/test.js; \
|
||||||
|
else echo "(node not found: skipped the JavaScript tests)"; fi
|
||||||
|
|
||||||
|
install: all
|
||||||
|
install -d $(DESTDIR)$(PREFIX)/include $(DESTDIR)$(PREFIX)/lib $(DESTDIR)$(PREFIX)/bin
|
||||||
|
install -m 644 yapf.h $(DESTDIR)$(PREFIX)/include/yapf.h
|
||||||
|
install -m 644 $(BUILD)/libyapf.a $(DESTDIR)$(PREFIX)/lib/libyapf.a
|
||||||
|
install -m 755 $(BUILD)/$(SHARED) $(DESTDIR)$(PREFIX)/lib/$(SHARED)
|
||||||
|
install -m 755 $(BUILD)/yapf $(DESTDIR)$(PREFIX)/bin/yapf
|
||||||
|
|
||||||
|
uninstall:
|
||||||
|
rm -f $(DESTDIR)$(PREFIX)/include/yapf.h $(DESTDIR)$(PREFIX)/lib/libyapf.a \
|
||||||
|
$(DESTDIR)$(PREFIX)/lib/$(SHARED) $(DESTDIR)$(PREFIX)/bin/yapf
|
||||||
|
|
||||||
|
clean:
|
||||||
|
rm -rf $(BUILD)
|
||||||
|
|
||||||
|
.PHONY: all test install uninstall clean
|
||||||
457
README.md
Normal file
457
README.md
Normal file
|
|
@ -0,0 +1,457 @@
|
||||||
|
# YAPF — Yet Another Picture Format
|
||||||
|
|
||||||
|
**Lossless images that decode several times faster than PNG — on one core.**
|
||||||
|
|
||||||
|
YAPF is a lossless image format for photos, UI and game assets, built for
|
||||||
|
decode speed first. There is no entropy coder, no bit reader and no context
|
||||||
|
model: every structure is byte aligned, and the decoder runs short loops that
|
||||||
|
modern CPUs execute at memory speed — with SSE / NEON where available. The
|
||||||
|
encoder does the hard work, so the decoder only follows instructions.
|
||||||
|
|
||||||
|
- **5–12× faster decoding than PNG** (stb_image), single-threaded; a 4K image
|
||||||
|
decodes in about 6–7 ms (around 5 GB/s). Multithreaded decoding is optional.
|
||||||
|
- **Smaller than PNG** on UI, icons and screenshots; 20–45 % larger on photos
|
||||||
|
and natural textures — the price of having no entropy coder.
|
||||||
|
- **Made for games and tools too**: mipmaps, GPU format hint, sRGB /
|
||||||
|
premultiplied flags, and random access to 64×64 tiles.
|
||||||
|
- **A real texture format (YAPF 2)**: GPU-ready BC1–BC7 / BC6H / ETC2 /
|
||||||
|
ASTC blocks, 16-bit and float pixels, cubemaps, arrays and volumes,
|
||||||
|
channel meanings (normal X/Y, roughness, AO, …), coverage-preserving
|
||||||
|
mips, small-mips-first streaming, per-tile checksums, notes and
|
||||||
|
animation — see [Textures](#textures-yapf-2).
|
||||||
|
- **Tiny and portable**: two C99 files, no dependencies. Plus a CLI, Python,
|
||||||
|
JavaScript and plugins for the tools you already use.
|
||||||
|
|
||||||
|
Two format versions: **version 1** (an 8-bit image, the first stable
|
||||||
|
format) and **version 2** (the texture container). Plain 8-bit images are
|
||||||
|
still written as version 1, so every existing YAPF reader keeps working.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Use it in…
|
||||||
|
|
||||||
|
| Where | What you get | Guide | Status |
|
||||||
|
| :---- | :----------- | :---- | :----- |
|
||||||
|
| **C / C++** | `yapf.h` + `yapf.c`, drop into any project (compiles as C or C++) | [below](#c--c) | ✅ tested — see [platforms](#platforms) |
|
||||||
|
| **Command line** | convert PNG/JPG/TGA/BMP/PSD/GIF ↔ YAPF, `compare`, `info`, `bench`, `thumbnail` | [below](#command-line) | ✅ tested on Linux and Windows |
|
||||||
|
| **Your language** | Python, C#, Rust, Go, Java, Zig, Lua, JavaScript / TypeScript, … | [docs/LANGUAGES.md](docs/LANGUAGES.md) | ✅ every binding tested |
|
||||||
|
| **GIMP 3** | open, export (flattened, optional mipmaps) | [extensions/gimp](extensions/gimp) | ✅ tested in GIMP 3.0.8 |
|
||||||
|
| **Blender 4.2+** | import, export, drag & drop, image as plane | [extensions/blender](extensions/blender) | ✅ tested in Blender 5.1 |
|
||||||
|
| **KDE Dolphin** | thumbnails for `.yapf` files | [extensions/kde](extensions/kde) | ⚠️ tested through KDE's plugin loader (KF6); not yet inside a running Dolphin |
|
||||||
|
| **Linux desktop** | file type + thumbnails in GNOME Files, Nemo, Caja, Thunar | [extensions/linux](extensions/linux) | ⚠️ thumbnail command tested; not yet in a file manager |
|
||||||
|
| **VS Code** | image preview with zoom and mip picker | [extensions/vscode](extensions/vscode) | ⚠️ packaged; preview code tested in a browser engine (Chromium); **not yet run inside VS Code** |
|
||||||
|
| **Figma** | import `.yapf`, export selection as `.yapf` | [extensions/figma](extensions/figma) | ⚠️ plugin UI tested in a browser engine with a simulated Figma; **not yet run inside Figma** |
|
||||||
|
| **Photoshop** | File → Scripts → Open YAPF / Save as YAPF | [extensions/photoshop](extensions/photoshop) | ⚠️ **syntax-checked only; not yet run in Photoshop** |
|
||||||
|
|
||||||
|
Pre-built downloads (command-line tool and libraries for Windows, macOS and
|
||||||
|
Linux, plus the plugin packages) are published on the
|
||||||
|
[Releases](https://github.com/Nexoniarz/YAPF/releases) page; everything can
|
||||||
|
also be built from source as described below.
|
||||||
|
|
||||||
|
### Platforms
|
||||||
|
|
||||||
|
| Platform | Status |
|
||||||
|
| :------- | :----- |
|
||||||
|
| Linux x64 | ✅ built and fully tested |
|
||||||
|
| Windows x64 | ✅ built and fully tested (MinGW and Zig builds, run under Wine) |
|
||||||
|
| Linux ARM64 | ✅ built and fully tested (NEON, run under qemu) |
|
||||||
|
| Windows ARM64 | ⚠️ built only, not run |
|
||||||
|
| macOS x64 / ARM64 | ⚠️ **cross-compiled only, not yet run on a Mac** |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Command line
|
||||||
|
|
||||||
|
```sh
|
||||||
|
yapf photo.png photo.yapf # encode (formats by file extension)
|
||||||
|
yapf texture.png texture.yapf --mips # with a full mip chain
|
||||||
|
yapf photo.yapf photo.png # decode to PNG / JPG / BMP / TGA
|
||||||
|
yapf texture.yapf mip2.png --mip 2 # export one mip level
|
||||||
|
yapf info photo.yapf # size, channels, mips, flags, ratio
|
||||||
|
yapf bench photo.yapf # decode speed, 1 thread and all cores
|
||||||
|
yapf thumbnail photo.yapf thumb.png 256
|
||||||
|
yapf compare photo.png ui.png # size and decode speed vs the original format
|
||||||
|
```
|
||||||
|
|
||||||
|
Options when writing `.yapf`: `--mips`, `--linear` (pixels are not sRGB),
|
||||||
|
`--premult` (alpha is premultiplied), and the texture options below.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Textures (YAPF 2)
|
||||||
|
|
||||||
|
Version 2 turns YAPF into a texture container for engines, designed our own
|
||||||
|
way rather than copied from KTX2 or DDS (the design and the full
|
||||||
|
specification are in [docs/FORMAT.md](docs/FORMAT.md)):
|
||||||
|
|
||||||
|
| Need | YAPF 2 |
|
||||||
|
| :--- | :----- |
|
||||||
|
| **GPU-compressed payloads** | BC1, BC2, BC3, BC4, BC5, BC6H, BC7, ETC2 / EAC and ASTC 4×4–12×12 blocks, stored as the GPU takes them. Built-in encoders for BC1, BC3, BC4, BC5, BC7 and BC6H; or bring blocks from your own encoder. |
|
||||||
|
| **More than 8 bits** | lossless 16-bit, half and float pixels, coded as byte planes with the same fast machinery |
|
||||||
|
| **Cubes, arrays, volumes** | cubemaps and cube arrays (reflection probes, skies), texture arrays (terrain layers), 3D textures (colour-grading LUTs) |
|
||||||
|
| **Channel meaning** | per channel: normal X/Y/Z, roughness, metalness, AO, height, opacity, …; OpenGL or DirectX normal convention; linear channels inside sRGB images |
|
||||||
|
| **Streaming** | small mips are stored first: a file prefix is a complete low-resolution texture (`yapf info` prints the prefix sizes); single 64 × 64 tiles decode on their own for virtual texturing |
|
||||||
|
| **Alpha-tested mips** | `--coverage REF` keeps fences and foliage from thinning out with distance; the filter used is recorded |
|
||||||
|
| **Entropy coding (optional)** | `--entropy`: shared per-surface Huffman code books with a neighbourhood context — about 10 % smaller photos, about 3× smaller GPU blocks on disk; tiles stay independent |
|
||||||
|
| **Metadata** | notes (author, licence, source, tool, ICC profile, orientation, DPI, HDR light levels, your own tags), colour primaries and transfer (sRGB, Display P3, BT.2020, PQ, HLG, …) |
|
||||||
|
| **Integrity** | yapf-sum checksums on the header, tables and every tile; *needs* bits make an older decoder refuse cleanly what it cannot read |
|
||||||
|
| **Animation** | frames with per-frame durations and a loop count |
|
||||||
|
|
||||||
|
```sh
|
||||||
|
yapf albedo.png albedo.yapf --format bc7 --mips # GPU-ready colour
|
||||||
|
yapf normal.png normal.yapf --format bc5 --mips --normal dx # normal map, DirectX Y
|
||||||
|
yapf orm.png orm.yapf --map ao,rough,metal --linear --mips # packed masks
|
||||||
|
yapf foliage.png foliage.yapf --format bc7 --coverage 128 # alpha-tested mips
|
||||||
|
yapf sky.hdr sky.yapf --format bc6h --mips # HDR, no RGBM tricks
|
||||||
|
yapf sky.hdr sky_lossless.yapf # lossless half float
|
||||||
|
yapf pack cube px.png nx.png py.png ny.png pz.png nz.png probe.yapf --mips
|
||||||
|
yapf pack array grass.png rock.png sand.png terrain.yapf --format bc7 --mips
|
||||||
|
yapf pack volume lut_00.png lut_01.png ... grading.yapf --linear
|
||||||
|
yapf pack anim f0.png f1.png f2.png fx.yapf --frame-ms 50
|
||||||
|
yapf albedo.png albedo.yapf --entropy --author "Keel" --license CC-BY-4.0
|
||||||
|
yapf info albedo.yapf # everything in the header, notes, stream prefix sizes
|
||||||
|
yapf verify albedo.yapf # check every checksum
|
||||||
|
yapf probe.yapf face2.png --face 2 --mip 1 # export one surface
|
||||||
|
```
|
||||||
|
|
||||||
|
In C:
|
||||||
|
|
||||||
|
```c
|
||||||
|
int err;
|
||||||
|
yapf_texture_t *t = yapf_tex_load("albedo.yapf", 0, 0, &err);
|
||||||
|
for (uint32_t m = 0; m < t->mip_levels; m++) {
|
||||||
|
uint32_t s = yapf_tex_surface(t, 0, 0, 0, m), w, h;
|
||||||
|
yapf_tex_mip_size(t, m, &w, &h, NULL);
|
||||||
|
if (t->payload == YAPF_PAYLOAD_BLOCKS) /* e.g. YAPF_GPU_BC7 */
|
||||||
|
glCompressedTexImage2D(GL_TEXTURE_2D, m, gl_format(t->format, t->flags),
|
||||||
|
w, h, 0, (GLsizei)yapf_tex_surface_bytes(t, m), t->data[s]);
|
||||||
|
}
|
||||||
|
yapf_tex_free(t);
|
||||||
|
```
|
||||||
|
|
||||||
|
Every extension opens version 2 files: GIMP (at full 16-bit / float
|
||||||
|
precision, faces and frames as layers), Blender (HDR as float images),
|
||||||
|
KDE and Linux thumbnails, VS Code (with face / layer / frame / slice
|
||||||
|
pickers), Figma and Photoshop. The Python binding (`yapf.load_texture`)
|
||||||
|
and the JavaScript port (`YAPF.decodeTexture`) read every texture; the
|
||||||
|
other bindings wrap the v1 functions, which also read 8-bit version 2
|
||||||
|
files (2D, cube face 0, array layer 0).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Benchmark
|
||||||
|
|
||||||
|
`yapf compare` on a mixed set of images: one binary (`make`, gcc -O2) for
|
||||||
|
both formats, PNG decoded with stb_image, one core, AMD Ryzen 7 5700G.
|
||||||
|
Every result is checked to decode back to the exact original pixels.
|
||||||
|
|
||||||
|
| Image | PNG | YAPF | YAPF / PNG size | PNG decode | YAPF decode | Faster |
|
||||||
|
| :---- | --: | ---: | --------------: | ---------: | ----------: | -----: |
|
||||||
|
| UI screenshot 261×430 | 176.6 KB | 143.7 KB | **81 %** | 2.05 ms | 0.17 ms | **12×** |
|
||||||
|
| UI screenshot 305×529 | 201.5 KB | 185.0 KB | **92 %** | 2.55 ms | 0.24 ms | **11×** |
|
||||||
|
| App icon 128×128 | 24.9 KB | 23.1 KB | **93 %** | 0.30 ms | 0.026 ms | **11×** |
|
||||||
|
| Wallpaper 3840×2160 | 1.77 MB | 2.21 MB | 125 % | 73.7 ms | 6.8 ms | **11×** |
|
||||||
|
| Rock texture 512×512 | 182.6 KB | 221.8 KB | 121 % | 1.64 ms | 0.17 ms | **10×** |
|
||||||
|
| Wood texture 512×512 | 185.8 KB | 234.1 KB | 126 % | 1.64 ms | 0.17 ms | **10×** |
|
||||||
|
| Canvas texture 512×512 | 171.5 KB | 245.6 KB | 143 % | 1.36 ms | 0.16 ms | **9×** |
|
||||||
|
| YAPF logo 512×512 | 60.5 KB | 89.3 KB | 148 % | 1.25 ms | 0.25 ms | **5×** |
|
||||||
|
|
||||||
|
Smaller than PNG on UI-style images, 20–50 % larger on photos and textures
|
||||||
|
(the price of having no entropy coder), and 5–12× faster to decode. More
|
||||||
|
in [examples/](examples) — and run `yapf compare` on your own images.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## C / C++
|
||||||
|
|
||||||
|
The library is two files. Add them to your project:
|
||||||
|
|
||||||
|
```
|
||||||
|
yapf.h — public API and the complete format specification
|
||||||
|
yapf.c — encoder and decoder
|
||||||
|
```
|
||||||
|
|
||||||
|
```c
|
||||||
|
#include "yapf.h"
|
||||||
|
|
||||||
|
yapf_image_t *img = yapf_load("texture.yapf"); /* or yapf_load_memory(buf, len) */
|
||||||
|
if (img) {
|
||||||
|
/* img->pixels: width × height × channels bytes, rows top to bottom */
|
||||||
|
upload_texture(img->width, img->height, img->channels, img->pixels);
|
||||||
|
yapf_free(img);
|
||||||
|
}
|
||||||
|
|
||||||
|
yapf_image_t out = { .width = w, .height = h, .channels = 4,
|
||||||
|
.mip_levels = 1, .pixels = rgba, .flags = YAPF_FLAG_SRGB };
|
||||||
|
yapf_save("out.yapf", &out);
|
||||||
|
```
|
||||||
|
|
||||||
|
Compile with any C99 compiler (`-pthread` on Unix for the `_mt` loaders):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cc -O2 -std=c99 main.c yapf.c -o app -pthread
|
||||||
|
```
|
||||||
|
|
||||||
|
SIMD is on by default: SSSE3 on x86 / x64 (chosen at run time with CPUID,
|
||||||
|
so binaries still run on older CPUs) and NEON on ARM64, with GCC, Clang,
|
||||||
|
MSVC and MinGW.
|
||||||
|
|
||||||
|
| Define | Effect |
|
||||||
|
| :------------------ | :----- |
|
||||||
|
| `-DYAPF_NO_SIMD` | plain C only |
|
||||||
|
| `-DYAPF_NO_THREADS` | no threads; the `_mt` loaders decode on the calling thread, and only the C standard library is needed |
|
||||||
|
|
||||||
|
## API Reference
|
||||||
|
|
||||||
|
### Types
|
||||||
|
|
||||||
|
```c
|
||||||
|
typedef struct {
|
||||||
|
uint32_t width; /* pixels per row */
|
||||||
|
uint32_t height; /* rows */
|
||||||
|
uint8_t channels; /* 1 gray / 2 gray+alpha / 3 RGB / 4 RGBA */
|
||||||
|
uint8_t gpu_format; /* YAPF_GPU_* hint */
|
||||||
|
uint8_t flags; /* YAPF_FLAG_* (premultiplied alpha, sRGB) */
|
||||||
|
uint8_t mip_levels; /* 1 = base only, up to 16 */
|
||||||
|
uint8_t *pixels; /* base level, malloc-owned */
|
||||||
|
uint8_t **mips; /* mips[0] == pixels, mips[1..] smaller levels */
|
||||||
|
} yapf_image_t;
|
||||||
|
```
|
||||||
|
|
||||||
|
`pixels` is a tightly packed, row-major byte array of `width × height × channels`
|
||||||
|
bytes. Mip level *m* is `max(1, width >> m) × max(1, height >> m)`. The
|
||||||
|
layout per pixel matches the `channels` value:
|
||||||
|
|
||||||
|
| `channels` | Layout per pixel |
|
||||||
|
| :--------- | :--------------- |
|
||||||
|
| 1 | `Y` |
|
||||||
|
| 2 | `Y A` |
|
||||||
|
| 3 | `R G B` |
|
||||||
|
| 4 | `R G B A` |
|
||||||
|
|
||||||
|
### Functions
|
||||||
|
|
||||||
|
```c
|
||||||
|
yapf_image_t *yapf_load(const char *filename);
|
||||||
|
yapf_image_t *yapf_load_memory(const void *buffer, size_t size);
|
||||||
|
```
|
||||||
|
Decode a YAPF file from disk or from memory (e.g. a game archive). Return a
|
||||||
|
heap-allocated image on success, `NULL` on any failure (file not found,
|
||||||
|
invalid or corrupt data, out of memory). Corrupt input is rejected, never
|
||||||
|
read out of bounds.
|
||||||
|
|
||||||
|
```c
|
||||||
|
yapf_image_t *yapf_load_mt(const char *filename, int threads);
|
||||||
|
yapf_image_t *yapf_load_memory_mt(const void *buffer, size_t size, int threads);
|
||||||
|
```
|
||||||
|
Same, decoding tiles on up to `threads` threads (`0` = one per CPU core).
|
||||||
|
The output is identical to the single-threaded loaders.
|
||||||
|
|
||||||
|
```c
|
||||||
|
int yapf_save(const char *filename, const yapf_image_t *img);
|
||||||
|
```
|
||||||
|
Encodes an image to disk. Writes `img->mip_levels`
|
||||||
|
levels; `img->mips` may be `NULL` when `mip_levels == 1`. Returns `YAPF_OK`
|
||||||
|
(0) on success or a negative `YAPF_ERR_*` code on failure.
|
||||||
|
|
||||||
|
```c
|
||||||
|
int yapf_encode(const yapf_image_t *img, void **out_data, size_t *out_size);
|
||||||
|
void yapf_free_buffer(void *data);
|
||||||
|
```
|
||||||
|
Encodes into memory: `*out_data` receives a complete `.yapf` file of
|
||||||
|
`*out_size` bytes, to be released with `yapf_free_buffer()`.
|
||||||
|
|
||||||
|
```c
|
||||||
|
void yapf_free(yapf_image_t *img);
|
||||||
|
```
|
||||||
|
Releases memory returned by the loaders. Passing `NULL` is safe.
|
||||||
|
|
||||||
|
### Textures (version 2)
|
||||||
|
|
||||||
|
```c
|
||||||
|
yapf_texture_t *yapf_tex_load(const char *file, int flags, int threads, int *err);
|
||||||
|
yapf_texture_t *yapf_tex_load_memory(const void *buf, size_t size, int flags, int threads, int *err);
|
||||||
|
int yapf_tex_save(const char *file, const yapf_texture_t *t, int flags);
|
||||||
|
int yapf_tex_encode(const yapf_texture_t *t, int flags, void **out, size_t *size);
|
||||||
|
yapf_texture_t *yapf_tex_create(const yapf_texture_t *desc); /* zeroed storage */
|
||||||
|
void yapf_tex_free(yapf_texture_t *t);
|
||||||
|
```
|
||||||
|
A texture describes its shape (width, height, depth, layers, frames,
|
||||||
|
cube / volume, mips), storage (pixels with u8 / u16 / half / float samples,
|
||||||
|
or GPU blocks), meaning (channel map, linear mask, normal convention,
|
||||||
|
primaries, transfer, mip filter, alpha reference) and holds `data[s]` per
|
||||||
|
surface `s = yapf_tex_surface(t, frame, layer, face, mip)`. Load flag
|
||||||
|
`YAPF_LOAD_TRUST` skips checksum checks; encode flag `YAPF_ENC_ENTROPY`
|
||||||
|
allows the entropy stage.
|
||||||
|
|
||||||
|
```c
|
||||||
|
yapf_texture_t *yapf_tex_open(const void *buf, size_t avail, int flags, int *err);
|
||||||
|
uint64_t yapf_tex_stream_bytes(const yapf_texture_t *t, uint32_t first_mip);
|
||||||
|
int yapf_tex_decode_surface(yapf_texture_t *t, const void *buf, size_t avail, uint32_t s, int threads);
|
||||||
|
int yapf_tex_decode_tile(const yapf_texture_t *t, const void *buf, size_t avail, uint32_t s,
|
||||||
|
uint32_t tx, uint32_t ty, uint32_t slice, void *out, size_t stride,
|
||||||
|
uint32_t *x, uint32_t *y, uint32_t *w, uint32_t *h);
|
||||||
|
```
|
||||||
|
Streaming and virtual texturing: open a file from its first bytes, ask how
|
||||||
|
many bytes hold mip *m* and smaller, decode surfaces as their bytes arrive
|
||||||
|
(`YAPF_ERR_TRUNCATED` until then), or decode single tiles.
|
||||||
|
|
||||||
|
```c
|
||||||
|
int yapf_tex_build_mips(yapf_texture_t *t, uint32_t levels, uint8_t alpha_ref);
|
||||||
|
int yapf_tex_compress(const yapf_texture_t *src, uint8_t format, yapf_texture_t **out);
|
||||||
|
int yapf_blocks_decode(uint8_t format, const void *blocks, uint32_t w, uint32_t h, void *out);
|
||||||
|
int yapf_tex_add_note(yapf_texture_t *t, const char tag[4], const void *data, uint32_t size);
|
||||||
|
const yapf_note_t *yapf_tex_find_note(const yapf_texture_t *t, const char tag[4]);
|
||||||
|
```
|
||||||
|
Mip chains (linear-light sRGB, optional coverage preservation), the
|
||||||
|
built-in BCn encoders, BC1–BC7 decoding for previews, and notes. The
|
||||||
|
complete list, with sizes and helpers, is in [`yapf.h`](yapf.h).
|
||||||
|
|
||||||
|
### Return codes
|
||||||
|
|
||||||
|
| Constant | Value | Meaning |
|
||||||
|
| :----------------- | ----: | :----------------------------------- |
|
||||||
|
| `YAPF_OK` | 0 | Success |
|
||||||
|
| `YAPF_ERR_INVALID` | –1 | NULL or logically invalid argument |
|
||||||
|
| `YAPF_ERR_IO` | –2 | File could not be opened or written |
|
||||||
|
| `YAPF_ERR_OOM` | –3 | Out of memory |
|
||||||
|
| `YAPF_ERR_CORRUPT` | –4 | Corrupt or not a YAPF file |
|
||||||
|
| `YAPF_ERR_UNSUPPORTED` | –5 | Needs a feature this decoder lacks |
|
||||||
|
| `YAPF_ERR_CHECKSUM` | –6 | A checksum does not match (v2) |
|
||||||
|
| `YAPF_ERR_TRUNCATED` | –7 | The data is not (yet) all there |
|
||||||
|
|
||||||
|
### Channel constants
|
||||||
|
|
||||||
|
```c
|
||||||
|
YAPF_CHANNELS_GRAY 1
|
||||||
|
YAPF_CHANNELS_GRAY_ALPHA 2
|
||||||
|
YAPF_CHANNELS_RGB 3
|
||||||
|
YAPF_CHANNELS_RGBA 4
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
| System | Command | Output |
|
||||||
|
| :----- | :------ | :----- |
|
||||||
|
| Linux, macOS, BSD | `make` · `make test` · `sudo make install` | `build/`: `libyapf.a`, `libyapf.so` / `.dylib`, `yapf` |
|
||||||
|
| NixOS / Nix | `nix-build` (or `nix-env -if .`) — see [extensions/linux](extensions/linux#nixos) | `result/`: `bin/yapf`, `lib/`, `include/`, MIME type, thumbnailer |
|
||||||
|
| Linux, macOS (LLVM) | `./build.sh` | `dist/<platform>/` incl. macOS universal binaries |
|
||||||
|
| Windows (LLVM) | `build.bat` | `dist\windows-x64\`, `dist\windows-arm64\`: `yapf.dll`, `yapf.lib`, `libyapf.a`, `yapf.exe` |
|
||||||
|
| Windows (Visual Studio) | `cl /O2 /c yapf.c` and add `yapf.obj` to your project | |
|
||||||
|
| All platforms at once | `tools/build_release.sh` (needs [zig](https://ziglang.org)) | `dist/release/`: every download on the Releases page |
|
||||||
|
|
||||||
|
The command-line tool is `tools/yapf_cli.c` + `yapf.c`; it uses the public
|
||||||
|
domain [stb](https://github.com/nothings/stb) headers in `third_party/`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## How it works
|
||||||
|
|
||||||
|
1. **Two colour transforms** — per 64×64 tile, YCoCg-R or subtract-green
|
||||||
|
(`G, R−G, B−G`), whichever codes smaller. Both run mod 256, so every
|
||||||
|
plane stays one byte per sample.
|
||||||
|
2. **Twelve row filters** — per band of 8 rows: Left, Gradient (`L + A − D`),
|
||||||
|
Up-right, Up-average, or the row 1–8 rows above (catches repeating
|
||||||
|
textures and dither patterns). None depends on the previous sample by
|
||||||
|
more than one add, and the vertical ones decode as plain vector code.
|
||||||
|
3. **Groups of 8** — residuals are packed in groups of 8 at the group's bit
|
||||||
|
width: 8 values × *w* bits = exactly *w* bytes, so every group is byte
|
||||||
|
aligned and unpacks with one 64-bit load (or one SIMD shuffle).
|
||||||
|
4. **4-bit control codes** — one per group, or one per run of up to 64 groups
|
||||||
|
repeating the previous width; the encoder picks the cheapest sequence with
|
||||||
|
a small dynamic program.
|
||||||
|
5. **Validate first, then run** — all sizes in a tile are checked before the
|
||||||
|
hot loops start, so the loops need no bounds checks. Corrupt files are
|
||||||
|
rejected, never read out of bounds.
|
||||||
|
6. **Constant and stored tiles** — a plane that is constant in a tile costs
|
||||||
|
one byte; a tile that would not shrink is stored raw (noise never grows).
|
||||||
|
7. **Independent tiles** — any tile can be decoded on its own, in any order,
|
||||||
|
on any thread.
|
||||||
|
|
||||||
|
The version 1 specification is the comment at the top of [`yapf.h`](yapf.h);
|
||||||
|
version 2 (textures) is specified in [docs/FORMAT.md](docs/FORMAT.md). Version 1:
|
||||||
|
|
||||||
|
```
|
||||||
|
HEADER (20 bytes) "YAPF", version 1, channels, GPU hint, mip count,
|
||||||
|
flags, 3 reserved, width u32, height u32
|
||||||
|
MIP INDEX per level: offset u32, size u32
|
||||||
|
LEVEL DATA tile_size u32 × tile_count, then the tiles
|
||||||
|
TILE (64×64) header byte: stored raw, or constant-plane mask + colour transform
|
||||||
|
per plane: filter byte per 8 rows, control nibbles,
|
||||||
|
w bytes per group of 8 values
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make test # C suite: 2631 checks, plus JavaScript if node is installed
|
||||||
|
node bindings/js/test.js # JavaScript: round-trips, sample file, corrupt input
|
||||||
|
```
|
||||||
|
|
||||||
|
What has been verified:
|
||||||
|
|
||||||
|
- exact round-trips for 1–4 channels, odd sizes from 1×1 up, mip chains,
|
||||||
|
single- and multi-threaded decoding, in-memory and file encoding;
|
||||||
|
- the same tests pass on Linux x64, Windows x64 (run under Wine) and Linux
|
||||||
|
ARM64 (NEON, run under qemu), with and without SIMD, compiled as C and as
|
||||||
|
C++, and under AddressSanitizer, UndefinedBehaviorSanitizer and
|
||||||
|
ThreadSanitizer;
|
||||||
|
- thousands of corrupted and truncated files are rejected without crashing;
|
||||||
|
- version 2: lossless round-trips of u8 / u16 / half / float pixels with 1–4
|
||||||
|
channels, with and without entropy coding; cubes, cube arrays, arrays,
|
||||||
|
volumes and animations with notes and every description field; GPU block
|
||||||
|
payloads; checksum, needs-bit and truncation handling; streaming from file
|
||||||
|
prefixes and single-tile decoding; BCn encoder quality; linear-light and
|
||||||
|
coverage-preserving mips; fuzzing of entropy-coded and packed-block files;
|
||||||
|
- the BC1–BC7 decoders were checked against an independent reference decoder
|
||||||
|
(bcdec) on 300,000 random blocks: BC6H and BC7 bit-exact, BC1–BC5 within
|
||||||
|
the ±1 rounding the formats allow (a development check, not part of
|
||||||
|
`make test`);
|
||||||
|
- every language binding runs its example; JavaScript, Python, C++, C#,
|
||||||
|
Rust, Go, Java and Lua re-encode the sample to byte-identical output;
|
||||||
|
- the GIMP plugin in GIMP 3.0.8 and the Blender extension in Blender 5.1;
|
||||||
|
- the KDE thumbnailer through KDE's own plugin loader.
|
||||||
|
|
||||||
|
**Not yet verified:** running on a Mac (macOS builds are cross-compiled
|
||||||
|
only), Windows on ARM, and running inside Photoshop, VS Code, Figma or a
|
||||||
|
live Dolphin / GNOME file manager. The version 2 code has been run on
|
||||||
|
Linux x64 (with and without SIMD, as C and C++, under ASan, UBSan and
|
||||||
|
TSan); its NEON path is compiled for ARM64 but has not been run yet, and
|
||||||
|
the Windows builds have not been rerun. ETC2 / EAC / ASTC blocks are
|
||||||
|
stored and loaded, but there is no built-in encoder or preview decoder
|
||||||
|
for them.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Project layout
|
||||||
|
|
||||||
|
```
|
||||||
|
yapf.h, yapf.c the library (v1 spec in yapf.h)
|
||||||
|
docs/FORMAT.md the full format specification, version 2
|
||||||
|
tools/yapf_cli.c the `yapf` command-line tool
|
||||||
|
tools/build_release.sh builds every download for all platforms
|
||||||
|
tests/test_yapf.c C test suite
|
||||||
|
examples/ speed and size comparison, C example
|
||||||
|
docs/LANGUAGES.md using YAPF from other languages
|
||||||
|
bindings/ cpp, python, csharp, rust, go, java, zig, lua, js
|
||||||
|
extensions/gimp/ GIMP 3 plugin
|
||||||
|
extensions/blender/ Blender extension
|
||||||
|
extensions/kde/ Dolphin thumbnail plugin (KF6)
|
||||||
|
extensions/linux/ MIME type and freedesktop thumbnailer
|
||||||
|
extensions/vscode/ VS Code preview
|
||||||
|
extensions/figma/ Figma plugin
|
||||||
|
extensions/photoshop/ Photoshop scripts
|
||||||
|
third_party/ stb_image / stb_image_write (public domain, CLI only)
|
||||||
|
other/YAPF.YAPF sample image (the YAPF logo)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Apache License 2.0 — see [LICENSE](LICENSE). `third_party/` contains
|
||||||
|
stb_image and stb_image_write by Sean Barrett (public domain / MIT).
|
||||||
18
bindings/cpp/README.md
Normal file
18
bindings/cpp/README.md
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
# YAPF for C++
|
||||||
|
|
||||||
|
`yapf.hpp` is a header-only C++17 wrapper: RAII, exceptions, `std::vector`.
|
||||||
|
Add `../../yapf.c` to your project and include the header.
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include "yapf.hpp"
|
||||||
|
|
||||||
|
yapf::Image img = yapf::Image::load("texture.yapf"); // or Image::decode(bytes)
|
||||||
|
upload(img.width(), img.height(), img.channels(), img.pixels());
|
||||||
|
|
||||||
|
std::vector<uint8_t> file = yapf::Image(256, 256, 4, rgba).encode();
|
||||||
|
```
|
||||||
|
|
||||||
|
`yapf.c` compiles as C or as C++, so you can simply add it to a C++ project
|
||||||
|
(Visual Studio, CMake, …).
|
||||||
|
|
||||||
|
Example: `g++ -O2 -std=c++17 example.cpp -x c++ ../../yapf.c -o example -pthread && ./example`
|
||||||
33
bindings/cpp/example.cpp
Normal file
33
bindings/cpp/example.cpp
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
// g++ -O2 -std=c++17 example.cpp ../../yapf.c -o example -pthread && ./example ../../other/YAPF.YAPF
|
||||||
|
#include "yapf.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <chrono>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <fstream>
|
||||||
|
#include <iterator>
|
||||||
|
|
||||||
|
int main(int argc, char **argv) {
|
||||||
|
const char *path = argc > 1 ? argv[1] : "../../other/YAPF.YAPF";
|
||||||
|
std::ifstream f(path, std::ios::binary);
|
||||||
|
std::vector<uint8_t> bytes((std::istreambuf_iterator<char>(f)), {});
|
||||||
|
|
||||||
|
yapf::Image img = yapf::Image::decode(bytes);
|
||||||
|
double ms = 1e9; // best of 10, as in a running program
|
||||||
|
for (int i = 0; i < 10; i++) {
|
||||||
|
auto t0 = std::chrono::steady_clock::now();
|
||||||
|
img = yapf::Image::decode(bytes);
|
||||||
|
ms = std::min(ms, std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now() - t0).count());
|
||||||
|
}
|
||||||
|
std::printf("%s: %ux%u, %u channels, decoded in %.2f ms, file is %.1f%% of raw\n", path,
|
||||||
|
img.width(), img.height(), img.channels(), ms, 100.0 * bytes.size() / img.size());
|
||||||
|
|
||||||
|
std::vector<uint8_t> again = img.encode();
|
||||||
|
std::printf("re-encoded: %zu bytes, identical to the file: %s\n", again.size(),
|
||||||
|
again == bytes ? "yes" : "no");
|
||||||
|
|
||||||
|
std::vector<uint8_t> gray(64 * 64);
|
||||||
|
for (size_t i = 0; i < gray.size(); i++) gray[i] = uint8_t(i % 64 * 4);
|
||||||
|
yapf::Image(64, 64, 1, gray.data()).save("gray.yapf");
|
||||||
|
std::printf("wrote gray.yapf\n");
|
||||||
|
}
|
||||||
98
bindings/cpp/yapf.hpp
Normal file
98
bindings/cpp/yapf.hpp
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
// yapf.hpp — header-only C++17 wrapper for YAPF. Copyright 2026 Nexoniarz — Apache 2.0.
|
||||||
|
//
|
||||||
|
// #include "yapf.hpp" // and compile/link ../../yapf.c
|
||||||
|
// yapf::Image img = yapf::Image::load("texture.yapf");
|
||||||
|
// img.width(); img.height(); img.channels(); img.pixels(); // span of bytes
|
||||||
|
// std::vector<uint8_t> file = img.encode();
|
||||||
|
// yapf::Image mine(256, 256, 4, my_rgba_bytes); mine.save("out.yapf");
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
#include "../../yapf.h"
|
||||||
|
}
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstring>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <string>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace yapf {
|
||||||
|
|
||||||
|
struct Error : std::runtime_error {
|
||||||
|
using std::runtime_error::runtime_error;
|
||||||
|
};
|
||||||
|
|
||||||
|
class Image {
|
||||||
|
public:
|
||||||
|
/* Decode a file or a memory buffer; threads = 0 uses every core, 1 the calling thread. */
|
||||||
|
static Image load(const std::string &path, int threads = 1) {
|
||||||
|
yapf_image_t *p = yapf_load_mt(path.c_str(), threads);
|
||||||
|
if (!p) throw Error("YAPF: cannot load " + path);
|
||||||
|
return Image(p);
|
||||||
|
}
|
||||||
|
static Image decode(const void *data, size_t size, int threads = 1) {
|
||||||
|
yapf_image_t *p = yapf_load_memory_mt(data, size, threads);
|
||||||
|
if (!p) throw Error("YAPF: invalid or corrupt data");
|
||||||
|
return Image(p);
|
||||||
|
}
|
||||||
|
static Image decode(const std::vector<uint8_t> &bytes, int threads = 1) {
|
||||||
|
return decode(bytes.data(), bytes.size(), threads);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* A new image from your pixels (copied). channels: 1 gray, 2 gray+A, 3 RGB, 4 RGBA. */
|
||||||
|
Image(uint32_t width, uint32_t height, uint8_t channels, const uint8_t *pixels,
|
||||||
|
uint8_t flags = YAPF_FLAG_SRGB)
|
||||||
|
: own_(pixels, pixels + size_t(width) * height * channels) {
|
||||||
|
std::memset(&view_, 0, sizeof(view_));
|
||||||
|
view_.width = width; view_.height = height; view_.channels = channels;
|
||||||
|
view_.flags = flags; view_.mip_levels = 1;
|
||||||
|
view_.gpu_format = channels == 4 ? YAPF_GPU_SRGB8_A8 : channels == 3 ? YAPF_GPU_SRGB8
|
||||||
|
: channels == 2 ? YAPF_GPU_RG8 : YAPF_GPU_R8;
|
||||||
|
view_.pixels = own_.data();
|
||||||
|
}
|
||||||
|
|
||||||
|
Image(Image &&o) noexcept : img_(std::exchange(o.img_, nullptr)), own_(std::move(o.own_)), view_(o.view_) {
|
||||||
|
if (!img_) view_.pixels = own_.data();
|
||||||
|
}
|
||||||
|
Image &operator=(Image &&o) noexcept {
|
||||||
|
if (this != &o) { yapf_free(img_); img_ = std::exchange(o.img_, nullptr); own_ = std::move(o.own_);
|
||||||
|
view_ = o.view_; if (!img_) view_.pixels = own_.data(); }
|
||||||
|
return *this;
|
||||||
|
}
|
||||||
|
Image(const Image &) = delete;
|
||||||
|
Image &operator=(const Image &) = delete;
|
||||||
|
~Image() { yapf_free(img_); }
|
||||||
|
|
||||||
|
uint32_t width() const { return raw().width; }
|
||||||
|
uint32_t height() const { return raw().height; }
|
||||||
|
uint8_t channels() const { return raw().channels; }
|
||||||
|
uint8_t flags() const { return raw().flags; }
|
||||||
|
uint8_t mipLevels() const { return raw().mip_levels; }
|
||||||
|
const uint8_t *pixels() const { return raw().pixels; }
|
||||||
|
size_t size() const { return size_t(width()) * height() * channels(); }
|
||||||
|
const uint8_t *mip(int level) const { return raw().mips ? raw().mips[level] : raw().pixels; }
|
||||||
|
|
||||||
|
std::vector<uint8_t> encode() const {
|
||||||
|
void *data = nullptr;
|
||||||
|
size_t n = 0;
|
||||||
|
if (yapf_encode(&raw(), &data, &n) != YAPF_OK) throw Error("YAPF: encoding failed");
|
||||||
|
std::vector<uint8_t> out(static_cast<uint8_t *>(data), static_cast<uint8_t *>(data) + n);
|
||||||
|
yapf_free_buffer(data);
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
void save(const std::string &path) const {
|
||||||
|
if (yapf_save(path.c_str(), &raw()) != YAPF_OK) throw Error("YAPF: cannot write " + path);
|
||||||
|
}
|
||||||
|
|
||||||
|
const yapf_image_t &raw() const { return img_ ? *img_ : view_; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
explicit Image(yapf_image_t *p) : img_(p) { std::memset(&view_, 0, sizeof(view_)); }
|
||||||
|
yapf_image_t *img_ = nullptr; // decoded by the library
|
||||||
|
std::vector<uint8_t> own_; // or pixels we own
|
||||||
|
yapf_image_t view_{};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace yapf
|
||||||
11
bindings/csharp/Example/Example.csproj
Normal file
11
bindings/csharp/Example/Example.csproj
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>Exe</OutputType>
|
||||||
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Include="../Yapf.cs" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
25
bindings/csharp/Example/Program.cs
Normal file
25
bindings/csharp/Example/Program.cs
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
// dotnet run -- ../../../other/YAPF.YAPF (with yapf.dll / libyapf.so on the library path)
|
||||||
|
using System.Diagnostics;
|
||||||
|
using Yapf;
|
||||||
|
|
||||||
|
string path = args.Length > 0 ? args[0] : "../../../other/YAPF.YAPF";
|
||||||
|
byte[] file = File.ReadAllBytes(path);
|
||||||
|
|
||||||
|
var img = YapfImage.Decode(file); // first call also loads the library / JIT
|
||||||
|
double ms = double.MaxValue; // best of 10, as in a running program
|
||||||
|
for (int i = 0; i < 10; i++)
|
||||||
|
{
|
||||||
|
var sw = Stopwatch.StartNew();
|
||||||
|
img = YapfImage.Decode(file);
|
||||||
|
ms = Math.Min(ms, sw.Elapsed.TotalMilliseconds);
|
||||||
|
}
|
||||||
|
Console.WriteLine($"{path}: {img.Width}x{img.Height}, {img.Channels} channels, decoded in {ms:F2} ms");
|
||||||
|
Console.WriteLine($"file is {file.Length} bytes, {100.0 * file.Length / img.Pixels.Length:F1}% of the raw pixels");
|
||||||
|
|
||||||
|
byte[] again = img.Encode();
|
||||||
|
Console.WriteLine($"re-encoded: {again.Length} bytes, identical: {again.AsSpan().SequenceEqual(file)}");
|
||||||
|
|
||||||
|
var rgba = new byte[64 * 64 * 4];
|
||||||
|
for (int i = 0; i < rgba.Length; i += 4) { rgba[i] = (byte)(i / 4 % 64 * 4); rgba[i + 1] = 100; rgba[i + 2] = 200; rgba[i + 3] = 255; }
|
||||||
|
new YapfImage(64, 64, 4, rgba).Save("csharp.yapf");
|
||||||
|
Console.WriteLine("wrote csharp.yapf");
|
||||||
19
bindings/csharp/README.md
Normal file
19
bindings/csharp/README.md
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
# YAPF for C# / .NET
|
||||||
|
|
||||||
|
`Yapf.cs` is a single-file P/Invoke wrapper for .NET 6 or later (tested with
|
||||||
|
.NET 8). It should also work in Unity with the native library in
|
||||||
|
`Assets/Plugins`, but that is not tested yet. Add it to your project and put
|
||||||
|
the native library next to your program: `yapf.dll` (Windows),
|
||||||
|
`libyapf.so` (Linux) or `libyapf.dylib` (macOS) — from the
|
||||||
|
[Releases](https://github.com/Nexoniarz/YAPF/releases) or `make`.
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using Yapf;
|
||||||
|
|
||||||
|
var img = YapfImage.Load("texture.yapf"); // or YapfImage.Decode(bytes)
|
||||||
|
byte[] pixels = img.Pixels; // Width * Height * Channels
|
||||||
|
new YapfImage(256, 256, 4, rgba).Save("out.yapf"); // or .Encode() → byte[]
|
||||||
|
```
|
||||||
|
|
||||||
|
Example: `cd Example && dotnet run -- ../../../other/YAPF.YAPF`
|
||||||
|
(Linux: `LD_LIBRARY_PATH=../../../build dotnet run …` after `make`).
|
||||||
121
bindings/csharp/Yapf.cs
Normal file
121
bindings/csharp/Yapf.cs
Normal file
|
|
@ -0,0 +1,121 @@
|
||||||
|
// Yapf.cs — C# bindings for YAPF (P/Invoke). Copyright 2026 Nexoniarz — Apache 2.0.
|
||||||
|
//
|
||||||
|
// Needs the native library next to your program or on the library path:
|
||||||
|
// yapf.dll (Windows), libyapf.so (Linux), libyapf.dylib (macOS).
|
||||||
|
//
|
||||||
|
// var img = YapfImage.Load("texture.yapf");
|
||||||
|
// byte[] pixels = img.Pixels; // Width * Height * Channels, rows top to bottom
|
||||||
|
// byte[] file = new YapfImage(256, 256, 4, myRgba).Encode();
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace Yapf
|
||||||
|
{
|
||||||
|
public sealed class YapfException : Exception
|
||||||
|
{
|
||||||
|
public YapfException(string message) : base(message) { }
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class YapfImage
|
||||||
|
{
|
||||||
|
public const byte FlagPremultipliedAlpha = 1, FlagSrgb = 2;
|
||||||
|
|
||||||
|
public int Width { get; }
|
||||||
|
public int Height { get; }
|
||||||
|
public int Channels { get; } // 1 gray, 2 gray+alpha, 3 RGB, 4 RGBA
|
||||||
|
public byte Flags { get; }
|
||||||
|
public byte GpuFormat { get; }
|
||||||
|
public byte[] Pixels => Mips[0];
|
||||||
|
public byte[][] Mips { get; } // Mips[0] is the full-size image
|
||||||
|
|
||||||
|
public YapfImage(int width, int height, int channels, byte[] pixels,
|
||||||
|
byte flags = FlagSrgb, byte[][]? mips = null)
|
||||||
|
{
|
||||||
|
if (pixels.Length != width * height * channels)
|
||||||
|
throw new ArgumentException("pixels must hold width * height * channels bytes");
|
||||||
|
Width = width; Height = height; Channels = channels; Flags = flags;
|
||||||
|
GpuFormat = channels switch { 4 => (byte)4, 3 => (byte)5, 2 => (byte)2, _ => (byte)3 };
|
||||||
|
Mips = mips ?? new[] { pixels };
|
||||||
|
Mips[0] = pixels;
|
||||||
|
}
|
||||||
|
|
||||||
|
private YapfImage(Native n)
|
||||||
|
{
|
||||||
|
Width = (int)n.width; Height = (int)n.height; Channels = n.channels;
|
||||||
|
Flags = n.flags; GpuFormat = n.gpu_format;
|
||||||
|
Mips = new byte[n.mip_levels][];
|
||||||
|
for (int m = 0; m < n.mip_levels; m++)
|
||||||
|
{
|
||||||
|
int w = Math.Max(1, Width >> m), h = Math.Max(1, Height >> m);
|
||||||
|
Mips[m] = new byte[w * h * Channels];
|
||||||
|
Marshal.Copy(Marshal.ReadIntPtr(n.mips, m * IntPtr.Size), Mips[m], 0, Mips[m].Length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Decode YAPF bytes. threads: 1 = calling thread, 0 = all cores.</summary>
|
||||||
|
public static YapfImage Decode(byte[] data, int threads = 1)
|
||||||
|
{
|
||||||
|
IntPtr p = NativeMethods.yapf_load_memory_mt(data, (UIntPtr)data.Length, threads);
|
||||||
|
if (p == IntPtr.Zero) throw new YapfException("not a valid YAPF file");
|
||||||
|
try { return new YapfImage(Marshal.PtrToStructure<Native>(p)); }
|
||||||
|
finally { NativeMethods.yapf_free(p); }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static YapfImage Load(string path, int threads = 1) => Decode(File.ReadAllBytes(path), threads);
|
||||||
|
|
||||||
|
/// <summary>Encode to the bytes of a .yapf file.</summary>
|
||||||
|
public byte[] Encode()
|
||||||
|
{
|
||||||
|
var handles = new GCHandle[Mips.Length];
|
||||||
|
IntPtr table = Marshal.AllocHGlobal(IntPtr.Size * Mips.Length);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
for (int m = 0; m < Mips.Length; m++)
|
||||||
|
{
|
||||||
|
handles[m] = GCHandle.Alloc(Mips[m], GCHandleType.Pinned);
|
||||||
|
Marshal.WriteIntPtr(table, m * IntPtr.Size, handles[m].AddrOfPinnedObject());
|
||||||
|
}
|
||||||
|
var n = new Native
|
||||||
|
{
|
||||||
|
width = (uint)Width, height = (uint)Height, channels = (byte)Channels,
|
||||||
|
gpu_format = GpuFormat, flags = Flags, mip_levels = (byte)Mips.Length,
|
||||||
|
pixels = handles[0].AddrOfPinnedObject(), mips = table,
|
||||||
|
};
|
||||||
|
int rc = NativeMethods.yapf_encode(ref n, out IntPtr data, out UIntPtr size);
|
||||||
|
if (rc != 0) throw new YapfException($"yapf_encode failed ({rc})");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var bytes = new byte[(int)size];
|
||||||
|
Marshal.Copy(data, bytes, 0, bytes.Length);
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
finally { NativeMethods.yapf_free_buffer(data); }
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
foreach (var h in handles) if (h.IsAllocated) h.Free();
|
||||||
|
Marshal.FreeHGlobal(table);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Save(string path) => File.WriteAllBytes(path, Encode());
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
private struct Native
|
||||||
|
{
|
||||||
|
public uint width, height;
|
||||||
|
public byte channels, gpu_format, flags, mip_levels;
|
||||||
|
public IntPtr pixels, mips;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class NativeMethods
|
||||||
|
{
|
||||||
|
private const string Lib = "yapf";
|
||||||
|
[DllImport(Lib)] public static extern IntPtr yapf_load_memory_mt(byte[] data, UIntPtr size, int threads);
|
||||||
|
[DllImport(Lib)] public static extern void yapf_free(IntPtr img);
|
||||||
|
[DllImport(Lib)] public static extern int yapf_encode(ref Native img, out IntPtr data, out UIntPtr size);
|
||||||
|
[DllImport(Lib)] public static extern void yapf_free_buffer(IntPtr data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
17
bindings/go/README.md
Normal file
17
bindings/go/README.md
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
# YAPF for Go
|
||||||
|
|
||||||
|
A cgo package that builds the C library itself (needs a C compiler, as any
|
||||||
|
cgo package).
|
||||||
|
|
||||||
|
```sh
|
||||||
|
go get github.com/Nexoniarz/YAPF/bindings/go
|
||||||
|
```
|
||||||
|
|
||||||
|
```go
|
||||||
|
import yapf "github.com/Nexoniarz/YAPF/bindings/go"
|
||||||
|
|
||||||
|
img, err := yapf.Load("texture.yapf") // or yapf.Decode(bytes)
|
||||||
|
data, err := yapf.Encode(&yapf.Image{Width: 256, Height: 256, Channels: 4, Pixels: rgba})
|
||||||
|
```
|
||||||
|
|
||||||
|
Example: `go run ./example ../../other/YAPF.YAPF` · tests: `go test`.
|
||||||
47
bindings/go/example/main.go
Normal file
47
bindings/go/example/main.go
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
// go run ./example ../../other/YAPF.YAPF
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
yapf "github.com/Nexoniarz/YAPF/bindings/go"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
path := "../../other/YAPF.YAPF"
|
||||||
|
if len(os.Args) > 1 {
|
||||||
|
path = os.Args[1]
|
||||||
|
}
|
||||||
|
file, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
img, err := yapf.Decode(file)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
best := time.Hour // best of 10, as in a running program
|
||||||
|
for i := 0; i < 10; i++ {
|
||||||
|
t := time.Now()
|
||||||
|
img, _ = yapf.Decode(file)
|
||||||
|
best = min(best, time.Since(t))
|
||||||
|
}
|
||||||
|
fmt.Printf("%s: %dx%d, %d channels, decoded in %.2f ms\n", path, img.Width, img.Height,
|
||||||
|
img.Channels, float64(best.Microseconds())/1000)
|
||||||
|
fmt.Printf("file is %d bytes, %.1f%% of the raw pixels\n", len(file), 100*float64(len(file))/float64(len(img.Pixels)))
|
||||||
|
|
||||||
|
again, _ := yapf.Encode(img)
|
||||||
|
fmt.Printf("re-encoded: %d bytes, identical: %v\n", len(again), bytes.Equal(again, file))
|
||||||
|
|
||||||
|
px := make([]byte, 128*128)
|
||||||
|
for i := range px {
|
||||||
|
px[i] = byte(i % 128 * 2)
|
||||||
|
}
|
||||||
|
if err := yapf.Save("go.yapf", &yapf.Image{Width: 128, Height: 128, Channels: 1, Flags: yapf.FlagSRGB, Pixels: px}); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
fmt.Println("wrote go.yapf")
|
||||||
|
}
|
||||||
3
bindings/go/go.mod
Normal file
3
bindings/go/go.mod
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
module github.com/Nexoniarz/YAPF/bindings/go
|
||||||
|
|
||||||
|
go 1.21
|
||||||
111
bindings/go/yapf.go
Normal file
111
bindings/go/yapf.go
Normal file
|
|
@ -0,0 +1,111 @@
|
||||||
|
// Package yapf reads and writes YAPF lossless images (cgo bindings to the C library).
|
||||||
|
//
|
||||||
|
// img, err := yapf.Load("texture.yapf")
|
||||||
|
// data, err := yapf.Encode(&yapf.Image{Width: 256, Height: 256, Channels: 4, Pixels: rgba})
|
||||||
|
package yapf
|
||||||
|
|
||||||
|
/*
|
||||||
|
#cgo CFLAGS: -O2 -std=c99 -I${SRCDIR}/../..
|
||||||
|
#cgo !windows LDFLAGS: -lm -lpthread
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include "yapf.h"
|
||||||
|
*/
|
||||||
|
import "C"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
FlagPremultAlpha = 1
|
||||||
|
FlagSRGB = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrInvalid = errors.New("yapf: not a valid YAPF file")
|
||||||
|
|
||||||
|
// Image holds Width*Height*Channels bytes in Pixels, rows top to bottom.
|
||||||
|
// Channels: 1 gray, 2 gray+alpha, 3 RGB, 4 RGBA. Mips holds the smaller
|
||||||
|
// mip levels (1, 2, …), if any.
|
||||||
|
type Image struct {
|
||||||
|
Width, Height int
|
||||||
|
Channels int
|
||||||
|
Flags uint8
|
||||||
|
GPUFormat uint8
|
||||||
|
Pixels []byte
|
||||||
|
Mips [][]byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode decodes YAPF bytes on the calling thread; DecodeThreads(data, 0) uses every core.
|
||||||
|
func Decode(data []byte) (*Image, error) { return DecodeThreads(data, 1) }
|
||||||
|
|
||||||
|
func DecodeThreads(data []byte, threads int) (*Image, error) {
|
||||||
|
if len(data) == 0 {
|
||||||
|
return nil, ErrInvalid
|
||||||
|
}
|
||||||
|
p := C.yapf_load_memory_mt(unsafe.Pointer(&data[0]), C.size_t(len(data)), C.int(threads))
|
||||||
|
if p == nil {
|
||||||
|
return nil, ErrInvalid
|
||||||
|
}
|
||||||
|
defer C.yapf_free(p)
|
||||||
|
img := &Image{Width: int(p.width), Height: int(p.height), Channels: int(p.channels),
|
||||||
|
Flags: uint8(p.flags), GPUFormat: uint8(p.gpu_format)}
|
||||||
|
mips := unsafe.Slice(p.mips, int(p.mip_levels))
|
||||||
|
for m := range mips {
|
||||||
|
w, h := max(1, img.Width>>m), max(1, img.Height>>m)
|
||||||
|
level := C.GoBytes(unsafe.Pointer(mips[m]), C.int(w*h*img.Channels))
|
||||||
|
if m == 0 {
|
||||||
|
img.Pixels = level
|
||||||
|
} else {
|
||||||
|
img.Mips = append(img.Mips, level)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return img, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Load(path string) (*Image, error) {
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return Decode(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode returns the bytes of a .yapf file.
|
||||||
|
func Encode(img *Image) ([]byte, error) {
|
||||||
|
if img.Channels < 1 || img.Channels > 4 || len(img.Pixels) != img.Width*img.Height*img.Channels {
|
||||||
|
return nil, errors.New("yapf: pixels must hold Width*Height*Channels bytes")
|
||||||
|
}
|
||||||
|
// Copy the levels into C memory: cgo forbids passing Go pointers inside C structs.
|
||||||
|
levels := append([][]byte{img.Pixels}, img.Mips...)
|
||||||
|
table := (**C.uint8_t)(C.malloc(C.size_t(len(levels)) * C.size_t(unsafe.Sizeof(uintptr(0)))))
|
||||||
|
defer C.free(unsafe.Pointer(table))
|
||||||
|
ptrs := unsafe.Slice(table, len(levels))
|
||||||
|
for i, l := range levels {
|
||||||
|
ptrs[i] = (*C.uint8_t)(C.CBytes(l))
|
||||||
|
defer C.free(unsafe.Pointer(ptrs[i]))
|
||||||
|
}
|
||||||
|
gpu := img.GPUFormat
|
||||||
|
if gpu == 0 {
|
||||||
|
gpu = map[int]uint8{4: 4, 3: 5, 2: 2, 1: 3}[img.Channels]
|
||||||
|
}
|
||||||
|
raw := C.yapf_image_t{width: C.uint32_t(img.Width), height: C.uint32_t(img.Height),
|
||||||
|
channels: C.uint8_t(img.Channels), gpu_format: C.uint8_t(gpu), flags: C.uint8_t(img.Flags),
|
||||||
|
mip_levels: C.uint8_t(len(levels)), pixels: ptrs[0], mips: table}
|
||||||
|
var data unsafe.Pointer
|
||||||
|
var size C.size_t
|
||||||
|
if rc := C.yapf_encode(&raw, &data, &size); rc != 0 {
|
||||||
|
return nil, errors.New("yapf: encoding failed")
|
||||||
|
}
|
||||||
|
defer C.yapf_free_buffer(data)
|
||||||
|
return C.GoBytes(data, C.int(size)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Save(path string, img *Image) error {
|
||||||
|
data, err := Encode(img)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return os.WriteFile(path, data, 0o644)
|
||||||
|
}
|
||||||
2
bindings/go/yapf_c.c
Normal file
2
bindings/go/yapf_c.c
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
// Builds the C library as part of this Go package (cgo compiles .c files here).
|
||||||
|
#include "../../yapf.c"
|
||||||
46
bindings/go/yapf_test.go
Normal file
46
bindings/go/yapf_test.go
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
package yapf
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRoundtrip(t *testing.T) {
|
||||||
|
for ch := 1; ch <= 4; ch++ {
|
||||||
|
w, h := 97, 61
|
||||||
|
px := make([]byte, w*h*ch)
|
||||||
|
for i := range px {
|
||||||
|
px[i] = byte(i*7 ^ i>>5)
|
||||||
|
}
|
||||||
|
data, err := Encode(&Image{Width: w, Height: h, Channels: ch, Flags: FlagSRGB, Pixels: px})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
back, err := Decode(data)
|
||||||
|
if err != nil || !bytes.Equal(back.Pixels, px) {
|
||||||
|
t.Fatalf("roundtrip failed for %d channels", ch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSampleReencodesIdentically(t *testing.T) {
|
||||||
|
file, err := os.ReadFile("../../other/YAPF.YAPF")
|
||||||
|
if err != nil {
|
||||||
|
t.Skip("sample not found")
|
||||||
|
}
|
||||||
|
img, err := Decode(file)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
again, _ := Encode(img)
|
||||||
|
if !bytes.Equal(again, file) {
|
||||||
|
t.Fatal("re-encoded sample differs from the C encoder's output")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRejectsGarbage(t *testing.T) {
|
||||||
|
if _, err := Decode([]byte("not an image")); err == nil {
|
||||||
|
t.Fatal("garbage was accepted")
|
||||||
|
}
|
||||||
|
}
|
||||||
29
bindings/java/Example.java
Normal file
29
bindings/java/Example.java
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
// java --enable-native-access=ALL-UNNAMED -Dyapf.library=../../build/libyapf.so Example.java ../../other/YAPF.YAPF
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
public class Example {
|
||||||
|
public static void main(String[] args) throws Exception {
|
||||||
|
Path path = Path.of(args.length > 0 ? args[0] : "../../other/YAPF.YAPF");
|
||||||
|
byte[] file = Files.readAllBytes(path);
|
||||||
|
|
||||||
|
Yapf.Image img = Yapf.decode(file); // first call also loads the library
|
||||||
|
double ms = Double.MAX_VALUE; // best of 10, as in a running program
|
||||||
|
for (int i = 0; i < 10; i++) {
|
||||||
|
long t = System.nanoTime();
|
||||||
|
img = Yapf.decode(file);
|
||||||
|
ms = Math.min(ms, (System.nanoTime() - t) / 1e6);
|
||||||
|
}
|
||||||
|
System.out.printf("%s: %dx%d, %d channels, decoded in %.2f ms%n", path, img.width(), img.height(), img.channels(), ms);
|
||||||
|
System.out.printf("file is %d bytes, %.1f%% of the raw pixels%n", file.length, 100.0 * file.length / img.pixels().length);
|
||||||
|
|
||||||
|
byte[] again = Yapf.encode(img);
|
||||||
|
System.out.printf("re-encoded: %d bytes, identical: %b%n", again.length, Arrays.equals(again, file));
|
||||||
|
|
||||||
|
byte[] rgb = new byte[64 * 64 * 3];
|
||||||
|
for (int i = 0; i < rgb.length; i++) rgb[i] = (byte) (i * 3);
|
||||||
|
Yapf.save(Path.of("java.yapf"), new Yapf.Image(64, 64, 3, rgb));
|
||||||
|
System.out.println("wrote java.yapf");
|
||||||
|
}
|
||||||
|
}
|
||||||
16
bindings/java/README.md
Normal file
16
bindings/java/README.md
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
# YAPF for Java (and Kotlin)
|
||||||
|
|
||||||
|
`Yapf.java` uses the Foreign Function & Memory API of **Java 22+** — no JNI,
|
||||||
|
no native glue to build; just the YAPF shared library (`yapf.dll`,
|
||||||
|
`libyapf.so`, `libyapf.dylib`) on the library path or passed with
|
||||||
|
`-Dyapf.library=/path/to/lib`.
|
||||||
|
|
||||||
|
```java
|
||||||
|
Yapf.Image img = Yapf.load(Path.of("texture.yapf")); // or Yapf.decode(bytes)
|
||||||
|
byte[] file = Yapf.encode(new Yapf.Image(256, 256, 4, rgba));
|
||||||
|
```
|
||||||
|
|
||||||
|
Kotlin can call the same class directly. Run with
|
||||||
|
`--enable-native-access=ALL-UNNAMED` to silence the native-access warning.
|
||||||
|
|
||||||
|
Example: `java --enable-native-access=ALL-UNNAMED -Dyapf.library=../../build/libyapf.so Example.java`
|
||||||
121
bindings/java/Yapf.java
Normal file
121
bindings/java/Yapf.java
Normal file
|
|
@ -0,0 +1,121 @@
|
||||||
|
// Yapf.java — YAPF for Java 22+ via the Foreign Function & Memory API (no JNI).
|
||||||
|
// Copyright 2026 Nexoniarz — Apache License 2.0.
|
||||||
|
//
|
||||||
|
// Needs the native library: yapf.dll / libyapf.so / libyapf.dylib on the
|
||||||
|
// library path, or its full path in the system property -Dyapf.library=...
|
||||||
|
// Run with --enable-native-access=ALL-UNNAMED to avoid the warning.
|
||||||
|
//
|
||||||
|
// Yapf.Image img = Yapf.load(Path.of("texture.yapf"));
|
||||||
|
// byte[] file = Yapf.encode(new Yapf.Image(256, 256, 4, rgba));
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.lang.foreign.*;
|
||||||
|
import java.lang.invoke.MethodHandle;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
import static java.lang.foreign.ValueLayout.*;
|
||||||
|
|
||||||
|
public final class Yapf {
|
||||||
|
public static final int FLAG_PREMULT_ALPHA = 1, FLAG_SRGB = 2;
|
||||||
|
|
||||||
|
/** pixels: width*height*channels bytes, rows top to bottom. mips[0] == pixels. */
|
||||||
|
public record Image(int width, int height, int channels, int flags, int gpuFormat, byte[][] mips) {
|
||||||
|
public Image(int width, int height, int channels, byte[] pixels) {
|
||||||
|
this(width, height, channels, FLAG_SRGB, switch (channels) { case 4 -> 4; case 3 -> 5; case 2 -> 2; default -> 3; },
|
||||||
|
new byte[][] { pixels });
|
||||||
|
}
|
||||||
|
public byte[] pixels() { return mips[0]; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// yapf_image_t: u32 width, u32 height, u8 channels, gpu_format, flags, mip_levels, ptr pixels, ptr mips
|
||||||
|
private static final StructLayout IMAGE = MemoryLayout.structLayout(
|
||||||
|
JAVA_INT.withName("width"), JAVA_INT.withName("height"),
|
||||||
|
JAVA_BYTE.withName("channels"), JAVA_BYTE.withName("gpu_format"),
|
||||||
|
JAVA_BYTE.withName("flags"), JAVA_BYTE.withName("mip_levels"),
|
||||||
|
MemoryLayout.paddingLayout(4), ADDRESS.withName("pixels"), ADDRESS.withName("mips"));
|
||||||
|
|
||||||
|
private static final MethodHandle LOAD, FREE, ENCODE, FREE_BUFFER;
|
||||||
|
static {
|
||||||
|
Linker linker = Linker.nativeLinker();
|
||||||
|
String custom = System.getProperty("yapf.library");
|
||||||
|
SymbolLookup lib = custom != null
|
||||||
|
? SymbolLookup.libraryLookup(Path.of(custom), Arena.global())
|
||||||
|
: SymbolLookup.libraryLookup(System.mapLibraryName("yapf"), Arena.global());
|
||||||
|
LOAD = linker.downcallHandle(lib.find("yapf_load_memory_mt").orElseThrow(),
|
||||||
|
FunctionDescriptor.of(ADDRESS, ADDRESS, JAVA_LONG, JAVA_INT));
|
||||||
|
FREE = linker.downcallHandle(lib.find("yapf_free").orElseThrow(), FunctionDescriptor.ofVoid(ADDRESS));
|
||||||
|
ENCODE = linker.downcallHandle(lib.find("yapf_encode").orElseThrow(),
|
||||||
|
FunctionDescriptor.of(JAVA_INT, ADDRESS, ADDRESS, ADDRESS));
|
||||||
|
FREE_BUFFER = linker.downcallHandle(lib.find("yapf_free_buffer").orElseThrow(), FunctionDescriptor.ofVoid(ADDRESS));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Yapf() {}
|
||||||
|
|
||||||
|
/** Decode YAPF bytes; threads = 1 decodes on this thread, 0 uses every core. */
|
||||||
|
public static Image decode(byte[] data, int threads) {
|
||||||
|
try (Arena arena = Arena.ofConfined()) {
|
||||||
|
MemorySegment in = arena.allocateFrom(JAVA_BYTE, data);
|
||||||
|
MemorySegment p = (MemorySegment) LOAD.invokeExact(in, (long) data.length, threads);
|
||||||
|
if (p.equals(MemorySegment.NULL)) throw new IllegalArgumentException("not a valid YAPF file");
|
||||||
|
try {
|
||||||
|
MemorySegment s = p.reinterpret(IMAGE.byteSize());
|
||||||
|
int w = s.get(JAVA_INT, 0), h = s.get(JAVA_INT, 4);
|
||||||
|
int ch = Byte.toUnsignedInt(s.get(JAVA_BYTE, 8)), levels = Byte.toUnsignedInt(s.get(JAVA_BYTE, 11));
|
||||||
|
MemorySegment table = s.get(ADDRESS, 24).reinterpret(ADDRESS.byteSize() * levels);
|
||||||
|
byte[][] mips = new byte[levels][];
|
||||||
|
for (int m = 0; m < levels; m++) {
|
||||||
|
long n = (long) Math.max(1, w >> m) * Math.max(1, h >> m) * ch;
|
||||||
|
mips[m] = table.getAtIndex(ADDRESS, m).reinterpret(n).toArray(JAVA_BYTE);
|
||||||
|
}
|
||||||
|
return new Image(w, h, ch, Byte.toUnsignedInt(s.get(JAVA_BYTE, 10)),
|
||||||
|
Byte.toUnsignedInt(s.get(JAVA_BYTE, 9)), mips);
|
||||||
|
} finally {
|
||||||
|
FREE.invokeExact(p);
|
||||||
|
}
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
throw e;
|
||||||
|
} catch (Throwable t) {
|
||||||
|
throw new IllegalStateException(t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Image decode(byte[] data) { return decode(data, 1); }
|
||||||
|
|
||||||
|
public static Image load(Path path) throws IOException { return decode(Files.readAllBytes(path)); }
|
||||||
|
|
||||||
|
/** Encode to the bytes of a .yapf file. */
|
||||||
|
public static byte[] encode(Image img) {
|
||||||
|
if (img.pixels().length != img.width() * img.height() * img.channels())
|
||||||
|
throw new IllegalArgumentException("pixels must hold width*height*channels bytes");
|
||||||
|
try (Arena arena = Arena.ofConfined()) {
|
||||||
|
byte[][] mips = img.mips();
|
||||||
|
MemorySegment table = arena.allocate(ADDRESS, mips.length);
|
||||||
|
for (int m = 0; m < mips.length; m++) table.setAtIndex(ADDRESS, m, arena.allocateFrom(JAVA_BYTE, mips[m]));
|
||||||
|
MemorySegment s = arena.allocate(IMAGE);
|
||||||
|
s.set(JAVA_INT, 0, img.width());
|
||||||
|
s.set(JAVA_INT, 4, img.height());
|
||||||
|
s.set(JAVA_BYTE, 8, (byte) img.channels());
|
||||||
|
s.set(JAVA_BYTE, 9, (byte) img.gpuFormat());
|
||||||
|
s.set(JAVA_BYTE, 10, (byte) img.flags());
|
||||||
|
s.set(JAVA_BYTE, 11, (byte) mips.length);
|
||||||
|
s.set(ADDRESS, 16, table.getAtIndex(ADDRESS, 0));
|
||||||
|
s.set(ADDRESS, 24, table);
|
||||||
|
MemorySegment outData = arena.allocate(ADDRESS), outSize = arena.allocate(JAVA_LONG);
|
||||||
|
int rc = (int) ENCODE.invokeExact(s, outData, outSize);
|
||||||
|
if (rc != 0) throw new IllegalStateException("yapf_encode failed (" + rc + ")");
|
||||||
|
MemorySegment data = outData.get(ADDRESS, 0);
|
||||||
|
try {
|
||||||
|
return data.reinterpret(outSize.get(JAVA_LONG, 0)).toArray(JAVA_BYTE);
|
||||||
|
} finally {
|
||||||
|
FREE_BUFFER.invokeExact(data);
|
||||||
|
}
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
throw e;
|
||||||
|
} catch (Throwable t) {
|
||||||
|
throw new IllegalStateException(t);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void save(Path path, Image img) throws IOException { Files.write(path, encode(img)); }
|
||||||
|
}
|
||||||
43
bindings/js/README.md
Normal file
43
bindings/js/README.md
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
# yapf.js — YAPF in JavaScript
|
||||||
|
|
||||||
|
Encoder (version 1) and decoder (versions 1 and 2) in one dependency-free file. Works in
|
||||||
|
browsers, Node.js, web workers, Figma plugins and VS Code webviews, and
|
||||||
|
produces exactly the same bytes as the C library.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const YAPF = require('./yapf.js'); // or <script src="yapf.js"> → window.YAPF
|
||||||
|
|
||||||
|
const img = YAPF.decode(bytes); // Uint8Array → image
|
||||||
|
// img = { width, height, channels, pixels, gpuFormat, flags, mipLevels, mips }
|
||||||
|
|
||||||
|
const file = YAPF.encode({ width, height, channels: 4, pixels }); // → Uint8Array
|
||||||
|
|
||||||
|
// draw on a canvas
|
||||||
|
ctx.putImageData(new ImageData(YAPF.toRGBA(img), img.width, img.height), 0, 0);
|
||||||
|
```
|
||||||
|
|
||||||
|
`decode` throws an `Error` for corrupt or unsupported data. Speed: a 4K
|
||||||
|
image decodes in about 150 ms in Node / Chrome (the C library: 6 ms).
|
||||||
|
|
||||||
|
TypeScript types are in `yapf.d.ts`.
|
||||||
|
|
||||||
|
Example: `node example.mjs ../../other/YAPF.YAPF` · tests: `node test.js`.
|
||||||
|
|
||||||
|
## Textures (YAPF 2)
|
||||||
|
|
||||||
|
```js
|
||||||
|
const tex = YAPF.decodeTexture(bytes); // any v1 / v2 file
|
||||||
|
// tex: { width, height, depth, layers, frames, faces, shape, mipLevels,
|
||||||
|
// payload, format, sample, channels, flags, channelMap, normal,
|
||||||
|
// primaries, transfer, frameUs, notes, surfaces: [...] }
|
||||||
|
const s = YAPF.surfaceIndex(tex, frame, layer, face, mip);
|
||||||
|
const view = YAPF.textureToRGBA(tex, s); // { width, height, rgba } for a canvas
|
||||||
|
```
|
||||||
|
|
||||||
|
`surfaces[s]` is a `Uint8Array` (8-bit pixels or GPU blocks, ready for
|
||||||
|
WebGL / WebGPU compressed textures), `Uint16Array` (16-bit, half) or
|
||||||
|
`Float32Array`. Checksums are verified (`{ verify: false }` skips them);
|
||||||
|
errors are `YAPF.YapfError` with a `code` (`corrupt`, `checksum`,
|
||||||
|
`truncated`, `not-yapf`, `version`, `unsupported`). `decode()` also reads
|
||||||
|
8-bit version 2 files. The decoder is checked against the C library on the
|
||||||
|
files in `test-data/`.
|
||||||
19
bindings/js/example.mjs
Normal file
19
bindings/js/example.mjs
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
// node example.mjs ../../other/YAPF.YAPF
|
||||||
|
import { readFileSync } from 'node:fs';
|
||||||
|
import { createRequire } from 'node:module';
|
||||||
|
const YAPF = createRequire(import.meta.url)('./yapf.js');
|
||||||
|
|
||||||
|
const path = process.argv[2] ?? '../../other/YAPF.YAPF';
|
||||||
|
const file = readFileSync(path);
|
||||||
|
|
||||||
|
let img = YAPF.decode(file);
|
||||||
|
let best = Infinity; // best of 10 decodes
|
||||||
|
for (let i = 0; i < 10; i++) {
|
||||||
|
const t = performance.now();
|
||||||
|
img = YAPF.decode(file);
|
||||||
|
best = Math.min(best, performance.now() - t);
|
||||||
|
}
|
||||||
|
console.log(`${path}: ${img.width}x${img.height}, ${img.channels} channels, decoded in ${best.toFixed(2)} ms (pure JavaScript)`);
|
||||||
|
console.log(`file is ${file.length} bytes, ${(100 * file.length / img.pixels.length).toFixed(1)}% of the raw pixels`);
|
||||||
|
const again = YAPF.encode(img);
|
||||||
|
console.log(`re-encoded: ${again.length} bytes, identical: ${Buffer.compare(Buffer.from(again), file) === 0}`);
|
||||||
12
bindings/js/package.json
Normal file
12
bindings/js/package.json
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"name": "yapf-image",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "YAPF v1 lossless image format: encoder and decoder in plain JavaScript, no dependencies",
|
||||||
|
"main": "yapf.js",
|
||||||
|
"types": "yapf.d.ts",
|
||||||
|
"files": ["yapf.js", "yapf.d.ts", "README.md"],
|
||||||
|
"scripts": { "test": "node test.js" },
|
||||||
|
"keywords": ["image", "codec", "lossless", "yapf", "texture"],
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"repository": { "type": "git", "url": "https://github.com/Nexoniarz/YAPF.git", "directory": "bindings/js" }
|
||||||
|
}
|
||||||
BIN
bindings/js/test-data/v2-animation-notes.yapf
Normal file
BIN
bindings/js/test-data/v2-animation-notes.yapf
Normal file
Binary file not shown.
BIN
bindings/js/test-data/v2-bc3-packed.yapf
Normal file
BIN
bindings/js/test-data/v2-bc3-packed.yapf
Normal file
Binary file not shown.
BIN
bindings/js/test-data/v2-bc5-packed.yapf
Normal file
BIN
bindings/js/test-data/v2-bc5-packed.yapf
Normal file
Binary file not shown.
BIN
bindings/js/test-data/v2-bc6h-encoded.yapf
Normal file
BIN
bindings/js/test-data/v2-bc6h-encoded.yapf
Normal file
Binary file not shown.
BIN
bindings/js/test-data/v2-bc6h-random-blocks.yapf
Normal file
BIN
bindings/js/test-data/v2-bc6h-random-blocks.yapf
Normal file
Binary file not shown.
BIN
bindings/js/test-data/v2-bc7-encoded.yapf
Normal file
BIN
bindings/js/test-data/v2-bc7-encoded.yapf
Normal file
Binary file not shown.
BIN
bindings/js/test-data/v2-bc7-random-blocks.yapf
Normal file
BIN
bindings/js/test-data/v2-bc7-random-blocks.yapf
Normal file
Binary file not shown.
BIN
bindings/js/test-data/v2-cube-mips.yapf
Normal file
BIN
bindings/js/test-data/v2-cube-mips.yapf
Normal file
Binary file not shown.
137
bindings/js/test-data/v2-expected.json
Normal file
137
bindings/js/test-data/v2-expected.json
Normal file
|
|
@ -0,0 +1,137 @@
|
||||||
|
{
|
||||||
|
"v2-rgba8.yapf": {
|
||||||
|
"surfaces": [
|
||||||
|
480599072
|
||||||
|
],
|
||||||
|
"preview0": 480599072
|
||||||
|
},
|
||||||
|
"v2-float-gray-entropy.yapf": {
|
||||||
|
"surfaces": [
|
||||||
|
1656156314
|
||||||
|
],
|
||||||
|
"preview0": 2474033295
|
||||||
|
},
|
||||||
|
"v2-half-gray-alpha-entropy.yapf": {
|
||||||
|
"surfaces": [
|
||||||
|
2599040047
|
||||||
|
],
|
||||||
|
"preview0": 297007736
|
||||||
|
},
|
||||||
|
"v2-rgba8-entropy.yapf": {
|
||||||
|
"surfaces": [
|
||||||
|
554695116
|
||||||
|
],
|
||||||
|
"preview0": 554695116
|
||||||
|
},
|
||||||
|
"v2-u16-rgba-entropy.yapf": {
|
||||||
|
"surfaces": [
|
||||||
|
3416063949
|
||||||
|
],
|
||||||
|
"preview0": 1518500476
|
||||||
|
},
|
||||||
|
"v2-cube-mips.yapf": {
|
||||||
|
"surfaces": [
|
||||||
|
3098789294,
|
||||||
|
2090642189,
|
||||||
|
3178169037,
|
||||||
|
3641419477,
|
||||||
|
333399741,
|
||||||
|
2333110333,
|
||||||
|
1703812753,
|
||||||
|
3098789294,
|
||||||
|
2090642189,
|
||||||
|
3178169037,
|
||||||
|
3641419477,
|
||||||
|
333399741,
|
||||||
|
2333110333,
|
||||||
|
1703812753,
|
||||||
|
3098789294,
|
||||||
|
2090642189,
|
||||||
|
3178169037,
|
||||||
|
3641419477,
|
||||||
|
333399741,
|
||||||
|
2333110333,
|
||||||
|
1703812753,
|
||||||
|
3098789294,
|
||||||
|
2090642189,
|
||||||
|
3178169037,
|
||||||
|
3641419477,
|
||||||
|
333399741,
|
||||||
|
2333110333,
|
||||||
|
1703812753,
|
||||||
|
3098789294,
|
||||||
|
2090642189,
|
||||||
|
3178169037,
|
||||||
|
3641419477,
|
||||||
|
333399741,
|
||||||
|
2333110333,
|
||||||
|
1703812753,
|
||||||
|
3098789294,
|
||||||
|
2090642189,
|
||||||
|
3178169037,
|
||||||
|
3641419477,
|
||||||
|
333399741,
|
||||||
|
2333110333,
|
||||||
|
1703812753
|
||||||
|
],
|
||||||
|
"preview0": 3098789294
|
||||||
|
},
|
||||||
|
"v2-u16-volume.yapf": {
|
||||||
|
"surfaces": [
|
||||||
|
1504297975,
|
||||||
|
2857113671,
|
||||||
|
3272367306,
|
||||||
|
4248642449
|
||||||
|
],
|
||||||
|
"preview0": 3611091147
|
||||||
|
},
|
||||||
|
"v2-animation-notes.yapf": {
|
||||||
|
"surfaces": [
|
||||||
|
2496952478,
|
||||||
|
2084186354,
|
||||||
|
2971951804,
|
||||||
|
4204487618
|
||||||
|
],
|
||||||
|
"preview0": 2496952478
|
||||||
|
},
|
||||||
|
"v2-bc6h-random-blocks.yapf": {
|
||||||
|
"surfaces": [
|
||||||
|
2051803746,
|
||||||
|
562394057,
|
||||||
|
2093502260
|
||||||
|
],
|
||||||
|
"preview0": 2386146506
|
||||||
|
},
|
||||||
|
"v2-bc7-random-blocks.yapf": {
|
||||||
|
"surfaces": [
|
||||||
|
3714498223,
|
||||||
|
242396130,
|
||||||
|
2835116443
|
||||||
|
],
|
||||||
|
"preview0": 4042788133
|
||||||
|
},
|
||||||
|
"v2-bc3-packed.yapf": {
|
||||||
|
"surfaces": [
|
||||||
|
1798067479
|
||||||
|
],
|
||||||
|
"preview0": 3027945807
|
||||||
|
},
|
||||||
|
"v2-bc5-packed.yapf": {
|
||||||
|
"surfaces": [
|
||||||
|
1681957115
|
||||||
|
],
|
||||||
|
"preview0": 1079658926
|
||||||
|
},
|
||||||
|
"v2-bc7-encoded.yapf": {
|
||||||
|
"surfaces": [
|
||||||
|
3415378546
|
||||||
|
],
|
||||||
|
"preview0": 851871445
|
||||||
|
},
|
||||||
|
"v2-bc6h-encoded.yapf": {
|
||||||
|
"surfaces": [
|
||||||
|
3372163902
|
||||||
|
],
|
||||||
|
"preview0": 89762387
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
bindings/js/test-data/v2-float-gray-entropy.yapf
Normal file
BIN
bindings/js/test-data/v2-float-gray-entropy.yapf
Normal file
Binary file not shown.
BIN
bindings/js/test-data/v2-half-gray-alpha-entropy.yapf
Normal file
BIN
bindings/js/test-data/v2-half-gray-alpha-entropy.yapf
Normal file
Binary file not shown.
BIN
bindings/js/test-data/v2-rgba8-entropy.yapf
Normal file
BIN
bindings/js/test-data/v2-rgba8-entropy.yapf
Normal file
Binary file not shown.
BIN
bindings/js/test-data/v2-rgba8.yapf
Normal file
BIN
bindings/js/test-data/v2-rgba8.yapf
Normal file
Binary file not shown.
BIN
bindings/js/test-data/v2-u16-rgba-entropy.yapf
Normal file
BIN
bindings/js/test-data/v2-u16-rgba-entropy.yapf
Normal file
Binary file not shown.
BIN
bindings/js/test-data/v2-u16-volume.yapf
Normal file
BIN
bindings/js/test-data/v2-u16-volume.yapf
Normal file
Binary file not shown.
91
bindings/js/test.js
Normal file
91
bindings/js/test.js
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
/* node bindings/js/test.js — round-trips, the sample file, and corrupt input. */
|
||||||
|
'use strict';
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const YAPF = require('./yapf.js');
|
||||||
|
|
||||||
|
let tests = 0, fails = 0;
|
||||||
|
const check = (ok, msg) => { tests++; if (!ok) { fails++; console.log('FAIL:', msg); } };
|
||||||
|
|
||||||
|
let seed = 12345;
|
||||||
|
const rnd = () => (seed = (Math.imul(seed, 1103515245) + 12345) >>> 0) >>> 8;
|
||||||
|
|
||||||
|
function fill(w, h, ch, kind) {
|
||||||
|
const p = new Uint8Array(w * h * ch);
|
||||||
|
for (let y = 0; y < h; y++) for (let x = 0; x < w; x++) for (let c = 0; c < ch; c++) {
|
||||||
|
const i = (y * w + x) * ch + c;
|
||||||
|
p[i] = kind === 0 ? rnd() : kind === 1 ? x * 3 + y * 5 + c * 40
|
||||||
|
: kind === 2 ? (((x / 7 | 0) + (y / 5 | 0) + c) & 1 ? 200 : 30)
|
||||||
|
: kind === 3 ? ((x * x + y * y) / 50 | 0) + rnd() % 5
|
||||||
|
: c === ch - 1 ? 255 : x ^ y;
|
||||||
|
}
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [w, h] of [[1, 1], [7, 1], [1, 9], [63, 65], [64, 64], [97, 61], [200, 130]])
|
||||||
|
for (let ch = 1; ch <= 4; ch++) for (let kind = 0; kind < 5; kind++) {
|
||||||
|
const pixels = fill(w, h, ch, kind);
|
||||||
|
const back = YAPF.decode(YAPF.encode({ width: w, height: h, channels: ch, pixels }));
|
||||||
|
check(back.width === w && back.height === h && back.channels === ch &&
|
||||||
|
Buffer.compare(Buffer.from(back.pixels), Buffer.from(pixels)) === 0,
|
||||||
|
`roundtrip ${w}x${h} ch${ch} kind${kind}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The bundled sample: decode to the reference pixels, re-encode to the same bytes. */
|
||||||
|
const samplePath = path.join(__dirname, '..', '..', 'other', 'YAPF.YAPF');
|
||||||
|
if (fs.existsSync(samplePath)) {
|
||||||
|
const file = fs.readFileSync(samplePath);
|
||||||
|
const img = YAPF.decode(file);
|
||||||
|
let h = 2166136261;
|
||||||
|
for (const v of img.pixels) h = Math.imul(h ^ v, 16777619) >>> 0;
|
||||||
|
check(h === 0x8891c2b4, 'sample decodes to the reference pixels');
|
||||||
|
check(Buffer.compare(Buffer.from(YAPF.encode(img)), file) === 0,
|
||||||
|
'sample re-encodes to byte-identical output (matches the C encoder)');
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Corrupt input must throw, never hang or return garbage silently. */
|
||||||
|
const good = YAPF.encode({ width: 130, height: 70, channels: 4, pixels: fill(130, 70, 4, 3) });
|
||||||
|
let threwOnTruncation = true;
|
||||||
|
for (let i = 0; i < 2000; i++) {
|
||||||
|
const bad = good.slice(0, i % 3 === 1 ? rnd() % good.length : good.length);
|
||||||
|
if (i % 3 !== 1) bad[rnd() % bad.length] ^= 1 << (rnd() % 8);
|
||||||
|
try { YAPF.decode(bad); if (i % 3 === 1) threwOnTruncation = false; } catch (e) { /* expected */ }
|
||||||
|
}
|
||||||
|
check(threwOnTruncation, 'truncated files are rejected');
|
||||||
|
|
||||||
|
/* Version 2 fixtures written by the C library: every surface and the
|
||||||
|
* preview must match what C decodes (FNV-1a hashes in v2-expected.json). */
|
||||||
|
const fnv = (u8) => { let h = 2166136261; for (const v of u8) h = Math.imul(h ^ v, 16777619) >>> 0; return h; };
|
||||||
|
const dataDir = path.join(__dirname, 'test-data');
|
||||||
|
const expected = JSON.parse(fs.readFileSync(path.join(dataDir, 'v2-expected.json'), 'utf8'));
|
||||||
|
for (const [name, want] of Object.entries(expected)) {
|
||||||
|
const bytes = fs.readFileSync(path.join(dataDir, name));
|
||||||
|
let tex = null;
|
||||||
|
try { tex = YAPF.decodeTexture(bytes); } catch (e) { check(false, `${name}: ${e.message}`); continue; }
|
||||||
|
const got = tex.surfaces.map((s) => fnv(new Uint8Array(s.buffer, s.byteOffset, s.byteLength)));
|
||||||
|
check(JSON.stringify(got) === JSON.stringify(want.surfaces), `${name}: surfaces match the C decoder`);
|
||||||
|
if (want.preview0 !== null) {
|
||||||
|
const view = YAPF.textureToRGBA(tex, 0, 0);
|
||||||
|
/* float previews may differ by 1 in rounding; compare only exact kinds */
|
||||||
|
if (tex.payload === 1 ? tex.format !== 0x47 && tex.format !== 0x48 : tex.sample < 2)
|
||||||
|
check(fnv(view.rgba) === want.preview0, `${name}: preview matches the C library`);
|
||||||
|
}
|
||||||
|
/* damage must be caught by the checksums */
|
||||||
|
const broken = Uint8Array.from(bytes);
|
||||||
|
broken[broken.length - 3] ^= 0x10;
|
||||||
|
let code = null;
|
||||||
|
try { YAPF.decodeTexture(broken); } catch (e) { code = e.code; }
|
||||||
|
check(code === 'checksum', `${name}: a damaged byte is reported as a checksum error`);
|
||||||
|
}
|
||||||
|
{ /* v1 decode() reads 8-bit v2 files and names what it cannot read */
|
||||||
|
const cube = YAPF.decode(fs.readFileSync(path.join(dataDir, 'v2-cube-mips.yapf')));
|
||||||
|
check(cube.mipLevels === 7 && cube.width === 64, 'decode() reads face 0 of a v2 cube');
|
||||||
|
let msg = '';
|
||||||
|
try { YAPF.decode(fs.readFileSync(path.join(dataDir, 'v2-bc7-encoded.yapf'))); } catch (e) { msg = e.message; }
|
||||||
|
check(/decodeTexture/.test(msg), 'decode() points GPU-block files to decodeTexture');
|
||||||
|
const yapfSum = (t) => YAPF.checksum(new TextEncoder().encode(t));
|
||||||
|
check(yapfSum('') === 0xd7550ed0 && yapfSum('YAPF') === 0xc8cc41d4, 'yapf-sum test vectors');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`${tests} tests, ${fails} failures`);
|
||||||
|
process.exit(fails ? 1 : 0);
|
||||||
25
bindings/js/yapf.d.ts
vendored
Normal file
25
bindings/js/yapf.d.ts
vendored
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
// TypeScript declarations for yapf.js
|
||||||
|
export interface YapfImage {
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
/** 1 gray, 2 gray + alpha, 3 RGB, 4 RGBA */
|
||||||
|
channels: 1 | 2 | 3 | 4;
|
||||||
|
/** width * height * channels bytes, rows top to bottom */
|
||||||
|
pixels: Uint8Array;
|
||||||
|
gpuFormat?: number;
|
||||||
|
flags?: number;
|
||||||
|
mipLevels?: number;
|
||||||
|
/** mips[0] is the full-size image */
|
||||||
|
mips?: Uint8Array[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Decode a .yapf file; throws on corrupt or unsupported data. */
|
||||||
|
export function decode(bytes: Uint8Array | ArrayBuffer): Required<YapfImage>;
|
||||||
|
/** Encode an image to the bytes of a .yapf file (identical to the C encoder). */
|
||||||
|
export function encode(image: YapfImage): Uint8Array;
|
||||||
|
/** Any channel count → RGBA, ready for `new ImageData(rgba, w, h)`. */
|
||||||
|
export function toRGBA(image: YapfImage, level?: number): Uint8ClampedArray;
|
||||||
|
|
||||||
|
export const GPU: { RGBA8: 0; RGB8: 1; RG8: 2; R8: 3; SRGB8_A8: 4; SRGB8: 5 };
|
||||||
|
export const FLAG: { PREMULT_ALPHA: 1; SRGB: 2 };
|
||||||
|
export const VERSION: 1;
|
||||||
1168
bindings/js/yapf.js
Normal file
1168
bindings/js/yapf.js
Normal file
File diff suppressed because it is too large
Load diff
13
bindings/lua/README.md
Normal file
13
bindings/lua/README.md
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
# YAPF for Lua (LuaJIT)
|
||||||
|
|
||||||
|
`yapf.lua` uses LuaJIT's FFI with the YAPF shared library (`libyapf.so`,
|
||||||
|
`yapf.dll`, `libyapf.dylib`, or the path in `$YAPF_LIBRARY`). Works in
|
||||||
|
LÖVE (love2d), which runs on LuaJIT.
|
||||||
|
|
||||||
|
```lua
|
||||||
|
local yapf = require("yapf")
|
||||||
|
local img = yapf.load("texture.yapf") -- width, height, channels, pixels (string)
|
||||||
|
yapf.save("out.yapf", img)
|
||||||
|
```
|
||||||
|
|
||||||
|
Example: `YAPF_LIBRARY=../../build/libyapf.so luajit example.lua`
|
||||||
17
bindings/lua/example.lua
Normal file
17
bindings/lua/example.lua
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
-- YAPF_LIBRARY=../../build/libyapf.so luajit example.lua ../../other/YAPF.YAPF
|
||||||
|
package.path = "./?.lua;" .. package.path
|
||||||
|
local yapf = require("yapf")
|
||||||
|
local path = arg[1] or "../../other/YAPF.YAPF"
|
||||||
|
local f = assert(io.open(path, "rb")); local data = f:read("*a"); f:close()
|
||||||
|
|
||||||
|
local img = assert(yapf.decode(data))
|
||||||
|
local best = math.huge -- best of 10 decodes
|
||||||
|
for _ = 1, 10 do
|
||||||
|
local t = os.clock()
|
||||||
|
img = yapf.decode(data)
|
||||||
|
best = math.min(best, os.clock() - t)
|
||||||
|
end
|
||||||
|
print(string.format("%s: %dx%d, %d channels, decoded in %.2f ms", path, img.width, img.height, img.channels, best * 1000))
|
||||||
|
print(string.format("file is %d bytes, %.1f%% of the raw pixels", #data, 100 * #data / #img.pixels))
|
||||||
|
local again = yapf.encode(img)
|
||||||
|
print(string.format("re-encoded: %d bytes, identical: %s", #again, tostring(again == data)))
|
||||||
71
bindings/lua/yapf.lua
Normal file
71
bindings/lua/yapf.lua
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
-- yapf.lua — YAPF for LuaJIT via its FFI. Copyright 2026 Nexoniarz — Apache 2.0.
|
||||||
|
-- local yapf = require("yapf")
|
||||||
|
-- local img = yapf.load("texture.yapf") -- img.width, img.height, img.channels, img.pixels (Lua string)
|
||||||
|
-- local bytes = yapf.encode(img) -- or yapf.save(path, img)
|
||||||
|
-- Loads libyapf.so / yapf.dll / libyapf.dylib, or the path in $YAPF_LIBRARY.
|
||||||
|
local ffi = require("ffi")
|
||||||
|
|
||||||
|
ffi.cdef [[
|
||||||
|
typedef struct {
|
||||||
|
uint32_t width, height;
|
||||||
|
uint8_t channels, gpu_format, flags, mip_levels;
|
||||||
|
uint8_t *pixels;
|
||||||
|
uint8_t **mips;
|
||||||
|
} yapf_image_t;
|
||||||
|
yapf_image_t *yapf_load_memory_mt(const void *buffer, size_t size, int threads);
|
||||||
|
void yapf_free(yapf_image_t *img);
|
||||||
|
int yapf_encode(const yapf_image_t *img, void **out_data, size_t *out_size);
|
||||||
|
void yapf_free_buffer(void *data);
|
||||||
|
]]
|
||||||
|
|
||||||
|
local lib = ffi.load(os.getenv("YAPF_LIBRARY") or "yapf")
|
||||||
|
local M = { FLAG_PREMULT_ALPHA = 1, FLAG_SRGB = 2 }
|
||||||
|
|
||||||
|
-- Decode a Lua string holding a .yapf file. threads: 1 (default) or 0 for all cores.
|
||||||
|
function M.decode(data, threads)
|
||||||
|
local p = lib.yapf_load_memory_mt(data, #data, threads or 1)
|
||||||
|
if p == nil then return nil, "not a valid YAPF file" end
|
||||||
|
local img = {
|
||||||
|
width = p.width, height = p.height, channels = p.channels,
|
||||||
|
flags = p.flags, gpu_format = p.gpu_format,
|
||||||
|
pixels = ffi.string(p.pixels, p.width * p.height * p.channels),
|
||||||
|
}
|
||||||
|
lib.yapf_free(p)
|
||||||
|
return img
|
||||||
|
end
|
||||||
|
|
||||||
|
function M.load(path)
|
||||||
|
local f, err = io.open(path, "rb")
|
||||||
|
if not f then return nil, err end
|
||||||
|
local data = f:read("*a")
|
||||||
|
f:close()
|
||||||
|
return M.decode(data)
|
||||||
|
end
|
||||||
|
|
||||||
|
-- img: { width, height, channels, pixels = string } → string with the .yapf bytes
|
||||||
|
function M.encode(img)
|
||||||
|
local raw = ffi.new("yapf_image_t")
|
||||||
|
local px = ffi.new("uint8_t[?]", #img.pixels)
|
||||||
|
ffi.copy(px, img.pixels, #img.pixels)
|
||||||
|
local table = ffi.new("uint8_t*[1]", px)
|
||||||
|
raw.width, raw.height, raw.channels = img.width, img.height, img.channels
|
||||||
|
raw.flags = img.flags or M.FLAG_SRGB
|
||||||
|
raw.gpu_format = img.gpu_format or ({ 3, 2, 5, 4 })[img.channels]
|
||||||
|
raw.mip_levels, raw.pixels, raw.mips = 1, px, table
|
||||||
|
local out, size = ffi.new("void*[1]"), ffi.new("size_t[1]")
|
||||||
|
if lib.yapf_encode(raw, out, size) ~= 0 then return nil, "encoding failed" end
|
||||||
|
local bytes = ffi.string(out[0], size[0])
|
||||||
|
lib.yapf_free_buffer(out[0])
|
||||||
|
return bytes
|
||||||
|
end
|
||||||
|
|
||||||
|
function M.save(path, img)
|
||||||
|
local bytes, err = M.encode(img)
|
||||||
|
if not bytes then return nil, err end
|
||||||
|
local f = assert(io.open(path, "wb"))
|
||||||
|
f:write(bytes)
|
||||||
|
f:close()
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
return M
|
||||||
39
bindings/python/README.md
Normal file
39
bindings/python/README.md
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
# yapf.py — YAPF in Python
|
||||||
|
|
||||||
|
```python
|
||||||
|
import yapf
|
||||||
|
|
||||||
|
img = yapf.load("texture.yapf") # or yapf.decode(data)
|
||||||
|
print(img.width, img.height, img.channels, img.mip_levels)
|
||||||
|
img.pixels # bytes, rows top to bottom
|
||||||
|
|
||||||
|
yapf.save("copy.yapf", img) # or yapf.encode(img) -> bytes
|
||||||
|
yapf.save("new.yapf", yapf.Image(w, h, 4, rgba_bytes))
|
||||||
|
```
|
||||||
|
|
||||||
|
It uses the C library through `ctypes` when it finds one:
|
||||||
|
|
||||||
|
1. the file named by the `YAPF_LIBRARY` environment variable,
|
||||||
|
2. `libyapf.so` / `libyapf.dylib` / `yapf.dll` next to `yapf.py`, or in
|
||||||
|
`lib/<platform>/` beside it (e.g. `lib/windows-x64/yapf.dll`),
|
||||||
|
3. the system library path (`sudo make install` installs one).
|
||||||
|
|
||||||
|
Without the library it falls back to a pure-Python decoder that needs
|
||||||
|
`numpy` (decoding only; about 1.4 s for a 4K image). `yapf.backend()`
|
||||||
|
tells you which one is in use.
|
||||||
|
|
||||||
|
Example: `YAPF_LIBRARY=../../build/libyapf.so python3 example.py ../../other/YAPF.YAPF`
|
||||||
|
|
||||||
|
## Textures (YAPF 2)
|
||||||
|
|
||||||
|
```python
|
||||||
|
tex = yapf.load_texture("albedo.yapf") # any v1 / v2 file, needs the C library
|
||||||
|
tex.payload, tex.format, tex.sample, tex.channels, tex.mip_levels
|
||||||
|
tex.shape, tex.layers, tex.frames, tex.channel_map, tex.normal, tex.notes
|
||||||
|
raw = tex.surface(frame=0, layer=0, face=0, mip=0) # bytes as stored (GPU blocks / samples)
|
||||||
|
rgba = tex.rgba_float() # numpy float32 (h, w, 4) for display
|
||||||
|
```
|
||||||
|
|
||||||
|
`yapf.load` / `yapf.decode` still return 8-bit images; on a texture they
|
||||||
|
cannot represent they raise `YapfError` saying so. Errors name the
|
||||||
|
reason (damaged, truncated, not YAPF, newer version).
|
||||||
33
bindings/python/example.py
Normal file
33
bindings/python/example.py
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
"""
|
||||||
|
example.py — YAPF from Python.
|
||||||
|
|
||||||
|
make # builds build/libyapf.so (or get it from Releases)
|
||||||
|
YAPF_LIBRARY=../../build/libyapf.so python3 example.py ../../other/YAPF.YAPF
|
||||||
|
"""
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
import yapf
|
||||||
|
|
||||||
|
path = sys.argv[1] if len(sys.argv) > 1 else "../../other/YAPF.YAPF"
|
||||||
|
data = open(path, "rb").read()
|
||||||
|
|
||||||
|
img = yapf.decode(data)
|
||||||
|
ms = float("inf") # best of 10, as in a running program
|
||||||
|
for _ in range(10):
|
||||||
|
t = time.perf_counter()
|
||||||
|
img = yapf.decode(data)
|
||||||
|
ms = min(ms, (time.perf_counter() - t) * 1000)
|
||||||
|
raw = img.width * img.height * img.channels
|
||||||
|
print("%s: %dx%d, %d channels, decoded in %.2f ms (%s backend)"
|
||||||
|
% (path, img.width, img.height, img.channels, ms, yapf.backend()))
|
||||||
|
print("file is %d bytes, %.1f%% of the raw pixels" % (len(data), 100.0 * len(data) / raw))
|
||||||
|
|
||||||
|
if yapf.backend() == "c":
|
||||||
|
again = yapf.encode(img)
|
||||||
|
print("re-encoded:", len(again), "bytes, identical:", again == data)
|
||||||
|
|
||||||
|
w = h = 128 # your own pixels → .yapf
|
||||||
|
rgb = bytes(v for y in range(h) for x in range(w) for v in (x * 2, y * 2, 128))
|
||||||
|
yapf.save("gradient.yapf", yapf.Image(w, h, 3, rgb))
|
||||||
|
print("wrote gradient.yapf")
|
||||||
BIN
bindings/python/gradient.yapf
Normal file
BIN
bindings/python/gradient.yapf
Normal file
Binary file not shown.
487
bindings/python/yapf.py
Normal file
487
bindings/python/yapf.py
Normal file
|
|
@ -0,0 +1,487 @@
|
||||||
|
"""
|
||||||
|
yapf.py — Python bindings for YAPF images (v1) and textures (v2).
|
||||||
|
|
||||||
|
Copyright 2026 Nexoniarz — Apache License 2.0.
|
||||||
|
|
||||||
|
import yapf
|
||||||
|
img = yapf.load("texture.yapf") # or yapf.decode(data)
|
||||||
|
img.width, img.height, img.channels # pixels: bytes, row-major
|
||||||
|
yapf.save("out.yapf", img) # or yapf.encode(img) -> bytes
|
||||||
|
|
||||||
|
tex = yapf.load_texture("albedo.yapf") # any v1 / v2 file (C library)
|
||||||
|
tex.payload, tex.format, tex.sample # GPU blocks, 16-bit, float, ...
|
||||||
|
raw = tex.surface(mip=0) # bytes as stored (blocks or samples)
|
||||||
|
rgba = tex.rgba_float() # numpy float32 (h, w, 4) for display
|
||||||
|
|
||||||
|
Uses the C library through ctypes when one can be found (fast, can
|
||||||
|
encode); otherwise falls back to a pure-Python decoder that needs numpy
|
||||||
|
(decode only). The library is looked up in this order:
|
||||||
|
|
||||||
|
1. the path in the YAPF_LIBRARY environment variable
|
||||||
|
2. next to this file: libyapf.so / libyapf.dylib / yapf.dll
|
||||||
|
(also in a platform subfolder such as lib/linux-x64/)
|
||||||
|
3. the system library path
|
||||||
|
|
||||||
|
Build the library with: cc -O2 -shared -fPIC yapf.c -o libyapf.so -pthread
|
||||||
|
"""
|
||||||
|
|
||||||
|
import ctypes
|
||||||
|
import os
|
||||||
|
import platform
|
||||||
|
import struct
|
||||||
|
import sys
|
||||||
|
|
||||||
|
__all__ = ["Image", "Texture", "load", "save", "decode", "encode", "backend",
|
||||||
|
"load_texture", "decode_texture",
|
||||||
|
"GPU_RGBA8", "GPU_RGB8", "GPU_RG8", "GPU_R8", "GPU_SRGB8_A8", "GPU_SRGB8",
|
||||||
|
"FLAG_PREMULT_ALPHA", "FLAG_SRGB", "YapfError"]
|
||||||
|
|
||||||
|
GPU_RGBA8, GPU_RGB8, GPU_RG8, GPU_R8, GPU_SRGB8_A8, GPU_SRGB8 = range(6)
|
||||||
|
GPU_BC1, GPU_BC2, GPU_BC3, GPU_BC4, GPU_BC4_SNORM, GPU_BC5, GPU_BC5_SNORM = range(0x40, 0x47)
|
||||||
|
GPU_BC6H, GPU_BC6H_SF, GPU_BC7 = 0x47, 0x48, 0x49
|
||||||
|
FLAG_PREMULT_ALPHA, FLAG_SRGB, FLAG_COVERAGE = 1, 2, 4
|
||||||
|
PAYLOAD_PIXELS, PAYLOAD_BLOCKS = 0, 1
|
||||||
|
SAMPLE_U8, SAMPLE_U16, SAMPLE_F16, SAMPLE_F32 = 0, 1, 2, 3
|
||||||
|
SHAPE_2D, SHAPE_CUBE, SHAPE_VOLUME = 0, 1, 2
|
||||||
|
|
||||||
|
_ERRORS = {-1: "invalid argument", -2: "cannot read the file", -3: "out of memory",
|
||||||
|
-4: "corrupt or not a YAPF file", -5: "needs a feature this library does not have",
|
||||||
|
-6: "damaged: a checksum does not match", -7: "the file is truncated"}
|
||||||
|
|
||||||
|
|
||||||
|
class YapfError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Image:
|
||||||
|
"""A decoded image. `pixels` holds width*height*channels bytes, rows top
|
||||||
|
to bottom; `mips` holds every mip level (mips[0] is pixels)."""
|
||||||
|
|
||||||
|
def __init__(self, width, height, channels, pixels,
|
||||||
|
gpu_format=None, flags=FLAG_SRGB, mips=None):
|
||||||
|
self.width, self.height, self.channels = int(width), int(height), int(channels)
|
||||||
|
self.pixels = bytes(pixels)
|
||||||
|
if gpu_format is None:
|
||||||
|
gpu_format = {1: GPU_R8, 2: GPU_RG8, 3: GPU_SRGB8, 4: GPU_SRGB8_A8}[self.channels]
|
||||||
|
self.gpu_format, self.flags = gpu_format, flags
|
||||||
|
self.mips = [self.pixels] + [bytes(m) for m in (mips or [])[1:]]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def mip_levels(self):
|
||||||
|
return len(self.mips)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "<yapf.Image %dx%d, %d channel(s), %d mip level(s)>" % (
|
||||||
|
self.width, self.height, self.channels, self.mip_levels)
|
||||||
|
|
||||||
|
|
||||||
|
# ── ctypes backend ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
class _CImage(ctypes.Structure):
|
||||||
|
_fields_ = [("width", ctypes.c_uint32), ("height", ctypes.c_uint32),
|
||||||
|
("channels", ctypes.c_uint8), ("gpu_format", ctypes.c_uint8),
|
||||||
|
("flags", ctypes.c_uint8), ("mip_levels", ctypes.c_uint8),
|
||||||
|
("pixels", ctypes.POINTER(ctypes.c_uint8)),
|
||||||
|
("mips", ctypes.POINTER(ctypes.POINTER(ctypes.c_uint8)))]
|
||||||
|
|
||||||
|
|
||||||
|
class _CNote(ctypes.Structure):
|
||||||
|
_fields_ = [("tag", ctypes.c_char * 4), ("size", ctypes.c_uint32),
|
||||||
|
("data", ctypes.POINTER(ctypes.c_uint8))]
|
||||||
|
|
||||||
|
|
||||||
|
class _CTexture(ctypes.Structure):
|
||||||
|
_fields_ = [("width", ctypes.c_uint32), ("height", ctypes.c_uint32), ("depth", ctypes.c_uint32),
|
||||||
|
("layers", ctypes.c_uint16), ("frames", ctypes.c_uint16),
|
||||||
|
("shape", ctypes.c_uint8), ("mip_levels", ctypes.c_uint8),
|
||||||
|
("payload", ctypes.c_uint8), ("format", ctypes.c_uint8),
|
||||||
|
("sample", ctypes.c_uint8), ("channels", ctypes.c_uint8), ("flags", ctypes.c_uint8),
|
||||||
|
("channel_map", ctypes.c_uint8 * 4), ("linear_mask", ctypes.c_uint8),
|
||||||
|
("normal", ctypes.c_uint8), ("primaries", ctypes.c_uint8), ("transfer", ctypes.c_uint8),
|
||||||
|
("mip_filter", ctypes.c_uint8), ("alpha_ref", ctypes.c_uint8),
|
||||||
|
("loop_count", ctypes.c_uint16),
|
||||||
|
("frame_us", ctypes.POINTER(ctypes.c_uint32)),
|
||||||
|
("note_count", ctypes.c_uint32), ("notes", ctypes.POINTER(_CNote)),
|
||||||
|
("data", ctypes.POINTER(ctypes.c_void_p)), ("internal_", ctypes.c_void_p)]
|
||||||
|
|
||||||
|
|
||||||
|
def _platform_dir():
|
||||||
|
machine = platform.machine().lower()
|
||||||
|
arch = "arm64" if machine in ("arm64", "aarch64") else "x64"
|
||||||
|
system = {"win32": "windows", "darwin": "macos"}.get(sys.platform, "linux")
|
||||||
|
return "%s-%s" % (system, arch)
|
||||||
|
|
||||||
|
|
||||||
|
def _find_library():
|
||||||
|
names = {"win32": ["yapf.dll", "libyapf.dll"],
|
||||||
|
"darwin": ["libyapf.dylib"]}.get(sys.platform, ["libyapf.so"])
|
||||||
|
here = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
candidates = []
|
||||||
|
if os.environ.get("YAPF_LIBRARY"):
|
||||||
|
candidates.append(os.environ["YAPF_LIBRARY"])
|
||||||
|
for folder in (here, os.path.join(here, "lib", _platform_dir())):
|
||||||
|
candidates += [os.path.join(folder, n) for n in names]
|
||||||
|
candidates += names
|
||||||
|
for path in candidates:
|
||||||
|
try:
|
||||||
|
lib = ctypes.CDLL(path)
|
||||||
|
lib.yapf_load_memory_mt.restype = ctypes.POINTER(_CImage)
|
||||||
|
lib.yapf_load_memory_mt.argtypes = [ctypes.c_char_p, ctypes.c_size_t, ctypes.c_int]
|
||||||
|
lib.yapf_free.argtypes = [ctypes.POINTER(_CImage)]
|
||||||
|
lib.yapf_save.argtypes = [ctypes.c_char_p, ctypes.POINTER(_CImage)]
|
||||||
|
lib.yapf_encode.argtypes = [ctypes.POINTER(_CImage), ctypes.POINTER(ctypes.c_void_p),
|
||||||
|
ctypes.POINTER(ctypes.c_size_t)]
|
||||||
|
lib.yapf_free_buffer.argtypes = [ctypes.c_void_p]
|
||||||
|
try: # version 2 (texture) API
|
||||||
|
T = ctypes.POINTER(_CTexture)
|
||||||
|
lib.yapf_tex_load_memory.restype = T
|
||||||
|
lib.yapf_tex_load_memory.argtypes = [ctypes.c_char_p, ctypes.c_size_t, ctypes.c_int,
|
||||||
|
ctypes.c_int, ctypes.POINTER(ctypes.c_int)]
|
||||||
|
lib.yapf_tex_free.argtypes = [T]
|
||||||
|
lib.yapf_tex_surface_bytes.restype = ctypes.c_size_t
|
||||||
|
lib.yapf_tex_surface_bytes.argtypes = [T, ctypes.c_uint32]
|
||||||
|
lib.yapf_tex_to_rgba8.argtypes = [T, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_void_p]
|
||||||
|
lib.yapf_blocks_decode.argtypes = [ctypes.c_uint8, ctypes.c_void_p, ctypes.c_uint32,
|
||||||
|
ctypes.c_uint32, ctypes.c_void_p]
|
||||||
|
lib.has_textures = True
|
||||||
|
except AttributeError: # an older library: v1 only
|
||||||
|
lib.has_textures = False
|
||||||
|
return lib
|
||||||
|
except (OSError, AttributeError):
|
||||||
|
continue
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
_lib = _find_library()
|
||||||
|
|
||||||
|
|
||||||
|
def backend():
|
||||||
|
"""'c' when the C library is used, 'python' for the fallback decoder."""
|
||||||
|
return "c" if _lib else "python"
|
||||||
|
|
||||||
|
|
||||||
|
def _c_decode(data):
|
||||||
|
data = bytes(data)
|
||||||
|
p = _lib.yapf_load_memory_mt(data, len(data), 0)
|
||||||
|
if not p:
|
||||||
|
raise YapfError(_why(data))
|
||||||
|
try:
|
||||||
|
c = p.contents
|
||||||
|
mips = []
|
||||||
|
for m in range(c.mip_levels):
|
||||||
|
w, h = max(1, c.width >> m), max(1, c.height >> m)
|
||||||
|
mips.append(ctypes.string_at(c.mips[m], w * h * c.channels))
|
||||||
|
return Image(c.width, c.height, c.channels, mips[0], c.gpu_format, c.flags, mips)
|
||||||
|
finally:
|
||||||
|
_lib.yapf_free(p)
|
||||||
|
|
||||||
|
|
||||||
|
def _c_encode(img):
|
||||||
|
bufs = [ctypes.create_string_buffer(m, len(m)) for m in img.mips]
|
||||||
|
ptrs = (ctypes.POINTER(ctypes.c_uint8) * len(bufs))(
|
||||||
|
*[ctypes.cast(b, ctypes.POINTER(ctypes.c_uint8)) for b in bufs])
|
||||||
|
c = _CImage(img.width, img.height, img.channels, img.gpu_format, img.flags,
|
||||||
|
len(bufs), ptrs[0], ptrs)
|
||||||
|
data, size = ctypes.c_void_p(), ctypes.c_size_t()
|
||||||
|
rc = _lib.yapf_encode(ctypes.byref(c), ctypes.byref(data), ctypes.byref(size))
|
||||||
|
if rc != 0:
|
||||||
|
raise YapfError("yapf_encode failed with code %d" % rc)
|
||||||
|
try:
|
||||||
|
return ctypes.string_at(data, size.value)
|
||||||
|
finally:
|
||||||
|
_lib.yapf_free_buffer(data)
|
||||||
|
|
||||||
|
|
||||||
|
# ── pure-Python fallback decoder (numpy) ──────────────────────────────
|
||||||
|
|
||||||
|
_NIB_GROUPS = [1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 3, 4, 8, 16, 32, 64]
|
||||||
|
|
||||||
|
|
||||||
|
def _py_decode(data):
|
||||||
|
try:
|
||||||
|
import numpy as np
|
||||||
|
except ImportError:
|
||||||
|
raise YapfError("the YAPF C library was not found and numpy is not "
|
||||||
|
"installed; build the library (see yapf.py) or install numpy")
|
||||||
|
b = np.frombuffer(bytes(data), dtype=np.uint8)
|
||||||
|
if len(b) < 20 or bytes(b[:4]) != b"YAPF":
|
||||||
|
raise YapfError("not a YAPF file")
|
||||||
|
if b[4] == 2:
|
||||||
|
raise YapfError("this is a YAPF 2 file; reading it needs the YAPF native library, "
|
||||||
|
"which was not found (the pure-Python decoder reads version 1 only)")
|
||||||
|
if b[4] != 1:
|
||||||
|
raise YapfError("unsupported YAPF version %d" % b[4])
|
||||||
|
ch, gpu, mip_levels, flags = int(b[5]), int(b[6]), int(b[7]), int(b[8])
|
||||||
|
W, H = struct.unpack_from("<II", bytes(b[12:20]))
|
||||||
|
if not (1 <= ch <= 4 and 1 <= mip_levels <= 16 and 1 <= W <= 65535 and 1 <= H <= 65535):
|
||||||
|
raise YapfError("corrupt header")
|
||||||
|
|
||||||
|
def bad():
|
||||||
|
raise YapfError("corrupt YAPF data")
|
||||||
|
|
||||||
|
mips = []
|
||||||
|
for m in range(mip_levels):
|
||||||
|
off, ln = struct.unpack_from("<II", bytes(b[20 + 8 * m: 28 + 8 * m]))
|
||||||
|
mw, mh = max(1, W >> m), max(1, H >> m)
|
||||||
|
if off > len(b) or ln > len(b) - off:
|
||||||
|
bad()
|
||||||
|
out = np.zeros((mh, mw, ch), dtype=np.uint8)
|
||||||
|
tcx, tcy = (mw + 63) // 64, (mh + 63) // 64
|
||||||
|
sizes = np.frombuffer(bytes(b[off: off + 4 * tcx * tcy]), dtype="<u4")
|
||||||
|
pos = off + 4 * tcx * tcy
|
||||||
|
for t in range(tcx * tcy):
|
||||||
|
tx, ty = (t % tcx) * 64, (t // tcx) * 64
|
||||||
|
tw, th = min(64, mw - tx), min(64, mh - ty)
|
||||||
|
end = pos + int(sizes[t])
|
||||||
|
if end > off + ln:
|
||||||
|
bad()
|
||||||
|
try:
|
||||||
|
out[ty:ty + th, tx:tx + tw] = _py_tile(np, b, pos, end, tw, th, ch, bad)
|
||||||
|
except (IndexError, ValueError):
|
||||||
|
bad()
|
||||||
|
pos = end
|
||||||
|
mips.append(out.tobytes())
|
||||||
|
return Image(W, H, ch, mips[0], gpu, flags, mips)
|
||||||
|
|
||||||
|
|
||||||
|
def _py_tile(np, b, p, end, tw, th, ch, bad):
|
||||||
|
hdr = int(b[p]); p += 1
|
||||||
|
if hdr == 0x80:
|
||||||
|
if end - p != tw * th * ch:
|
||||||
|
bad()
|
||||||
|
return b[p:end].reshape(th, tw, ch)
|
||||||
|
if hdr & 0xE0:
|
||||||
|
bad()
|
||||||
|
xf, mask = hdr & 0x10, hdr & 0x0F
|
||||||
|
planes = np.zeros((ch, th, tw), dtype=np.uint8)
|
||||||
|
for c in range(ch):
|
||||||
|
if mask >> c & 1:
|
||||||
|
planes[c, :, :] = b[p]; p += 1
|
||||||
|
for c in range(ch):
|
||||||
|
if mask >> c & 1:
|
||||||
|
continue
|
||||||
|
nb, ng = (th + 7) // 8, (tw * th + 7) // 8
|
||||||
|
filt = b[p:p + nb]; p += nb
|
||||||
|
widths, g, hi, w = [], 0, 0, 0
|
||||||
|
while g < ng:
|
||||||
|
code = (int(b[p]) >> (4 * hi)) & 15
|
||||||
|
p += hi; hi ^= 1
|
||||||
|
if code < 9:
|
||||||
|
w = code
|
||||||
|
n = _NIB_GROUPS[code]
|
||||||
|
widths += [w] * n; g += n
|
||||||
|
if g != ng:
|
||||||
|
bad()
|
||||||
|
if hi:
|
||||||
|
p += 1
|
||||||
|
z = np.zeros(ng * 8, dtype=np.uint8)
|
||||||
|
widths = np.array(widths, dtype=np.int64)
|
||||||
|
starts = p + np.concatenate(([0], np.cumsum(widths)[:-1]))
|
||||||
|
p += int(widths.sum())
|
||||||
|
if p > end:
|
||||||
|
bad()
|
||||||
|
for wv in range(1, 9):
|
||||||
|
sel = np.nonzero(widths == wv)[0]
|
||||||
|
if not len(sel):
|
||||||
|
continue
|
||||||
|
raw = b[starts[sel][:, None] + np.arange(wv)].astype(np.uint64)
|
||||||
|
word = (raw << (8 * np.arange(wv, dtype=np.uint64))).sum(axis=1, dtype=np.uint64)
|
||||||
|
shifts = np.arange(8, dtype=np.uint64) * np.uint64(wv)
|
||||||
|
vals = (word[:, None] >> shifts) & np.uint64((1 << wv) - 1)
|
||||||
|
z[(sel[:, None] * 8 + np.arange(8)).ravel()] = vals.ravel().astype(np.uint8)
|
||||||
|
r = ((z[:tw * th] >> 1) ^ (0 - (z[:tw * th] & 1)).astype(np.uint8)).reshape(th, tw).astype(np.int64)
|
||||||
|
P = planes[c].astype(np.int64)
|
||||||
|
for y in range(th):
|
||||||
|
f = int(filt[y // 8])
|
||||||
|
U = P[y - 1] if y else np.zeros(tw, np.int64)
|
||||||
|
if f == 0:
|
||||||
|
P[y] = np.cumsum(r[y]) & 255
|
||||||
|
elif f == 1:
|
||||||
|
inc = r[y] + U - np.concatenate(([0], U[:-1]))
|
||||||
|
P[y] = np.cumsum(inc) & 255
|
||||||
|
elif f in (2, 3):
|
||||||
|
AR = np.concatenate((U[1:], U[-1:]))
|
||||||
|
pred = AR if f == 2 else (U + AR) >> 1
|
||||||
|
P[y] = (pred + r[y]) & 255
|
||||||
|
elif 16 <= f < 24:
|
||||||
|
k = f - 15
|
||||||
|
K = P[y - k] if y >= k else np.zeros(tw, np.int64)
|
||||||
|
P[y] = (K + r[y]) & 255
|
||||||
|
else:
|
||||||
|
bad()
|
||||||
|
planes[c] = P.astype(np.uint8)
|
||||||
|
if p != end:
|
||||||
|
bad()
|
||||||
|
if ch < 3:
|
||||||
|
return planes.transpose(1, 2, 0)
|
||||||
|
P0, P1, P2 = (planes[i].astype(np.int64) for i in range(3))
|
||||||
|
if xf:
|
||||||
|
R, G, B = P1 + P0 - 128, P0, P2 + P0 - 128
|
||||||
|
else:
|
||||||
|
co, cg = (P1 - 128) & 255, (P2 - 128) & 255
|
||||||
|
s8 = lambda v: np.where(v > 127, v - 256, v)
|
||||||
|
t = (P0 - (s8(cg) >> 1)) & 255
|
||||||
|
B = (t - (s8(co) >> 1)) & 255
|
||||||
|
R, G = B + co, cg + t
|
||||||
|
out = [R & 255, G & 255, B & 255] + ([planes[3].astype(np.int64)] if ch == 4 else [])
|
||||||
|
return np.stack(out, axis=-1).astype(np.uint8)
|
||||||
|
|
||||||
|
|
||||||
|
# ── textures (version 2, C library) ──────────────────────────────────
|
||||||
|
|
||||||
|
def _why(data):
|
||||||
|
"""Why `decode` could not read data, in words."""
|
||||||
|
if len(data) < 5 or data[:4] != b"YAPF":
|
||||||
|
return "not a YAPF file"
|
||||||
|
if data[4] not in (1, 2):
|
||||||
|
return "YAPF format version %d is newer than this library" % data[4]
|
||||||
|
if _lib and getattr(_lib, "has_textures", False):
|
||||||
|
err = ctypes.c_int(0)
|
||||||
|
p = _lib.yapf_tex_load_memory(data, len(data), 0, 1, ctypes.byref(err))
|
||||||
|
if p:
|
||||||
|
_lib.yapf_tex_free(p)
|
||||||
|
return ("this is a YAPF 2 texture (GPU blocks, 16-bit / float or a volume) "
|
||||||
|
"that yapf.decode cannot return as 8-bit pixels; use yapf.load_texture")
|
||||||
|
return _ERRORS.get(err.value, "error %d" % err.value)
|
||||||
|
if data[4] == 2:
|
||||||
|
return "YAPF 2 files need the YAPF native library (not found)"
|
||||||
|
return "corrupt YAPF file"
|
||||||
|
|
||||||
|
|
||||||
|
class Texture:
|
||||||
|
"""A decoded YAPF texture (version 1 or 2). Description fields mirror
|
||||||
|
yapf_texture_t; `surfaces[s]` holds the bytes of surface s as stored
|
||||||
|
(GPU blocks, or samples in native byte order), numbered
|
||||||
|
s = ((frame * layers + layer) * faces + face) * mip_levels + mip."""
|
||||||
|
|
||||||
|
def __init__(self, c, lib):
|
||||||
|
for f, _ in _CTexture._fields_:
|
||||||
|
if f not in ("frame_us", "notes", "data", "internal_", "channel_map", "note_count"):
|
||||||
|
setattr(self, f, getattr(c, f))
|
||||||
|
self.channel_map = list(c.channel_map)
|
||||||
|
self.faces = 6 if c.shape == SHAPE_CUBE else 1
|
||||||
|
self.frame_us = [c.frame_us[i] for i in range(c.frames)] if c.frame_us else []
|
||||||
|
self.notes = [(c.notes[i].tag.decode("latin-1"), ctypes.string_at(c.notes[i].data, c.notes[i].size))
|
||||||
|
for i in range(c.note_count)]
|
||||||
|
count = c.frames * c.layers * self.faces * c.mip_levels
|
||||||
|
self.surfaces = [ctypes.string_at(c.data[s], lib.yapf_tex_surface_bytes(ctypes.byref(c), s % c.mip_levels))
|
||||||
|
for s in range(count)]
|
||||||
|
|
||||||
|
def surface_index(self, frame=0, layer=0, face=0, mip=0):
|
||||||
|
return ((frame * self.layers + layer) * self.faces + face) * self.mip_levels + mip
|
||||||
|
|
||||||
|
def surface(self, frame=0, layer=0, face=0, mip=0):
|
||||||
|
return self.surfaces[self.surface_index(frame, layer, face, mip)]
|
||||||
|
|
||||||
|
def mip_size(self, mip=0):
|
||||||
|
d = max(1, self.depth >> mip) if self.shape == SHAPE_VOLUME else 1
|
||||||
|
return max(1, self.width >> mip), max(1, self.height >> mip), d
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_hdr(self):
|
||||||
|
"""Float / half samples or BC6H blocks: values may go above 1."""
|
||||||
|
if self.payload == PAYLOAD_BLOCKS:
|
||||||
|
return self.format in (GPU_BC6H, GPU_BC6H_SF)
|
||||||
|
return self.sample in (SAMPLE_F16, SAMPLE_F32)
|
||||||
|
|
||||||
|
def rgba_float(self, frame=0, layer=0, face=0, mip=0, slice=0):
|
||||||
|
"""One image as numpy float32 (height, width, 4), rows top to bottom:
|
||||||
|
8 / 16-bit scaled to 0..1 (still in their stored colour space),
|
||||||
|
floats as stored, GPU blocks decoded. Needs numpy."""
|
||||||
|
import numpy as np
|
||||||
|
w, h, d = self.mip_size(mip)
|
||||||
|
raw = self.surface(frame, layer, face, mip)
|
||||||
|
raw = raw[len(raw) // d * slice:len(raw) // d * (slice + 1)]
|
||||||
|
out = np.ones((h, w, 4), dtype=np.float32)
|
||||||
|
if self.payload == PAYLOAD_BLOCKS:
|
||||||
|
hdr = self.format in (GPU_BC6H, GPU_BC6H_SF)
|
||||||
|
buf = ctypes.create_string_buffer(w * h * (6 if hdr else 4))
|
||||||
|
rc = _lib.yapf_blocks_decode(self.format, raw, w, h, buf)
|
||||||
|
if rc != 0:
|
||||||
|
raise YapfError("GPU block format 0x%02x cannot be decoded here" % self.format)
|
||||||
|
if hdr:
|
||||||
|
out[..., :3] = np.frombuffer(buf.raw, dtype=np.float16).reshape(h, w, 3)
|
||||||
|
else:
|
||||||
|
out[...] = np.frombuffer(buf.raw, dtype=np.uint8).reshape(h, w, 4) / 255.0
|
||||||
|
if self.channels == 3:
|
||||||
|
out[..., 3] = 1.0
|
||||||
|
return out
|
||||||
|
dt = {SAMPLE_U8: np.uint8, SAMPLE_U16: np.uint16, SAMPLE_F16: np.float16, SAMPLE_F32: np.float32}[self.sample]
|
||||||
|
ch = self.channels
|
||||||
|
src = np.frombuffer(raw, dtype=dt).reshape(h, w, ch).astype(np.float32)
|
||||||
|
if self.sample == SAMPLE_U8:
|
||||||
|
src /= 255.0
|
||||||
|
elif self.sample == SAMPLE_U16:
|
||||||
|
src /= 65535.0
|
||||||
|
out[..., :3] = src[..., :3] if ch >= 3 else src[..., :1]
|
||||||
|
if ch in (2, 4):
|
||||||
|
out[..., 3] = src[..., ch - 1]
|
||||||
|
return out
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
w, h, d = self.mip_size(0)
|
||||||
|
return "<yapf.Texture %dx%d%s, %d mip(s), %s>" % (
|
||||||
|
w, h, "x%d" % d if d > 1 else "", self.mip_levels,
|
||||||
|
"blocks 0x%02x" % self.format if self.payload == PAYLOAD_BLOCKS
|
||||||
|
else "%d ch %s" % (self.channels, ["u8", "u16", "f16", "f32"][self.sample]))
|
||||||
|
|
||||||
|
|
||||||
|
def decode_texture(data, verify=True):
|
||||||
|
"""Decode any YAPF file (v1 or v2) into a Texture. Needs the C library."""
|
||||||
|
if not (_lib and getattr(_lib, "has_textures", False)):
|
||||||
|
raise YapfError("YAPF textures need the YAPF native library (version 2), which was not found")
|
||||||
|
data = bytes(data)
|
||||||
|
err = ctypes.c_int(0)
|
||||||
|
p = _lib.yapf_tex_load_memory(data, len(data), 0 if verify else 1, 0, ctypes.byref(err))
|
||||||
|
if not p:
|
||||||
|
raise YapfError(_why(data) if len(data) < 5 or data[:4] != b"YAPF"
|
||||||
|
else _ERRORS.get(err.value, "error %d" % err.value))
|
||||||
|
try:
|
||||||
|
return Texture(p.contents, _lib)
|
||||||
|
finally:
|
||||||
|
_lib.yapf_tex_free(p)
|
||||||
|
|
||||||
|
|
||||||
|
def load_texture(path, verify=True):
|
||||||
|
"""Read and decode any .yapf file into a Texture."""
|
||||||
|
with open(path, "rb") as f:
|
||||||
|
return decode_texture(f.read(), verify)
|
||||||
|
|
||||||
|
|
||||||
|
# ── public API ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def decode(data):
|
||||||
|
"""Decode YAPF bytes into an Image."""
|
||||||
|
return _c_decode(data) if _lib else _py_decode(data)
|
||||||
|
|
||||||
|
|
||||||
|
def load(path):
|
||||||
|
"""Read and decode a .yapf file."""
|
||||||
|
with open(path, "rb") as f:
|
||||||
|
return decode(f.read())
|
||||||
|
|
||||||
|
|
||||||
|
def encode(img):
|
||||||
|
"""Encode an Image (or anything with width/height/channels/pixels) to
|
||||||
|
YAPF bytes. Needs the C library."""
|
||||||
|
if not _lib:
|
||||||
|
raise YapfError("saving .yapf needs the YAPF native library, which was not found "
|
||||||
|
"for this platform; see https://github.com/Nexoniarz/YAPF#python")
|
||||||
|
if not isinstance(img, Image):
|
||||||
|
img = Image(img.width, img.height, img.channels, img.pixels)
|
||||||
|
expected = img.width * img.height * img.channels
|
||||||
|
if len(img.pixels) != expected:
|
||||||
|
raise YapfError("expected %d pixel bytes, got %d" % (expected, len(img.pixels)))
|
||||||
|
return _c_encode(img)
|
||||||
|
|
||||||
|
|
||||||
|
def save(path, img):
|
||||||
|
"""Encode an Image to a .yapf file. Needs the C library."""
|
||||||
|
data = encode(img)
|
||||||
|
with open(path, "wb") as f:
|
||||||
|
f.write(data)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
for path in sys.argv[1:]:
|
||||||
|
print(path, load(path), "backend:", backend())
|
||||||
12
bindings/rust/Cargo.toml
Normal file
12
bindings/rust/Cargo.toml
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
[package]
|
||||||
|
name = "yapf"
|
||||||
|
version = "1.0.0"
|
||||||
|
edition = "2021"
|
||||||
|
description = "YAPF lossless images: fast byte-aligned decoder and encoder (bindings to the C library)"
|
||||||
|
license = "Apache-2.0"
|
||||||
|
repository = "https://github.com/Nexoniarz/YAPF"
|
||||||
|
links = "yapf"
|
||||||
|
build = "build.rs"
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
cc = "1"
|
||||||
18
bindings/rust/README.md
Normal file
18
bindings/rust/README.md
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
# YAPF for Rust
|
||||||
|
|
||||||
|
A crate that compiles the C library with the `cc` crate, so `cargo build`
|
||||||
|
works on Windows, macOS and Linux with no extra steps.
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[dependencies]
|
||||||
|
yapf = { git = "https://github.com/Nexoniarz/YAPF" }
|
||||||
|
```
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let img = yapf::Image::load("texture.yapf")?; // or Image::decode(&bytes)
|
||||||
|
println!("{}x{}, {} channels", img.width, img.height, img.channels);
|
||||||
|
yapf::Image::new(256, 256, 4, rgba).save("out.yapf")?; // or .encode() → Vec<u8>
|
||||||
|
```
|
||||||
|
|
||||||
|
Example: `cargo run --release --example demo -- ../../other/YAPF.YAPF` ·
|
||||||
|
tests: `cargo test`.
|
||||||
12
bindings/rust/build.rs
Normal file
12
bindings/rust/build.rs
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
// Compiles the C library that lives two folders up.
|
||||||
|
fn main() {
|
||||||
|
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../..");
|
||||||
|
cc::Build::new()
|
||||||
|
.file(root.join("yapf.c"))
|
||||||
|
.include(&root)
|
||||||
|
.flag_if_supported("-std=c99")
|
||||||
|
.opt_level(2)
|
||||||
|
.compile("yapf");
|
||||||
|
println!("cargo:rerun-if-changed={}", root.join("yapf.c").display());
|
||||||
|
println!("cargo:rerun-if-changed={}", root.join("yapf.h").display());
|
||||||
|
}
|
||||||
26
bindings/rust/examples/demo.rs
Normal file
26
bindings/rust/examples/demo.rs
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
// cargo run --release --example demo -- ../../other/YAPF.YAPF
|
||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
|
fn main() -> Result<(), yapf::Error> {
|
||||||
|
let path = std::env::args().nth(1).unwrap_or_else(|| "../../other/YAPF.YAPF".into());
|
||||||
|
let file = std::fs::read(&path)?;
|
||||||
|
|
||||||
|
let mut img = yapf::Image::decode(&file)?;
|
||||||
|
let mut best = f64::MAX; // best of 10, as in a running program
|
||||||
|
for _ in 0..10 {
|
||||||
|
let t = Instant::now();
|
||||||
|
img = yapf::Image::decode(&file)?;
|
||||||
|
best = best.min(t.elapsed().as_secs_f64() * 1e3);
|
||||||
|
}
|
||||||
|
println!("{path}: {}x{}, {} channels, decoded in {best:.2} ms", img.width, img.height, img.channels);
|
||||||
|
println!("file is {} bytes, {:.1}% of the raw pixels",
|
||||||
|
file.len(), 100.0 * file.len() as f64 / img.pixels.len() as f64);
|
||||||
|
|
||||||
|
let again = img.encode()?;
|
||||||
|
println!("re-encoded: {} bytes, identical: {}", again.len(), again == file);
|
||||||
|
|
||||||
|
let px: Vec<u8> = (0..128 * 128).flat_map(|i| [(i % 128 * 2) as u8, (i / 128 * 2) as u8, 128]).collect();
|
||||||
|
yapf::Image::new(128, 128, 3, px).save("rust.yapf")?;
|
||||||
|
println!("wrote rust.yapf");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
156
bindings/rust/src/lib.rs
Normal file
156
bindings/rust/src/lib.rs
Normal file
|
|
@ -0,0 +1,156 @@
|
||||||
|
//! YAPF lossless images for Rust.
|
||||||
|
//!
|
||||||
|
//! ```no_run
|
||||||
|
//! let img = yapf::Image::load("texture.yapf")?;
|
||||||
|
//! println!("{}x{} with {} channels", img.width, img.height, img.channels);
|
||||||
|
//! let bytes = img.encode()?;
|
||||||
|
//! # Ok::<(), yapf::Error>(())
|
||||||
|
//! ```
|
||||||
|
use std::ffi::{c_int, c_void};
|
||||||
|
use std::fmt;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
#[repr(C)]
|
||||||
|
struct RawImage {
|
||||||
|
width: u32,
|
||||||
|
height: u32,
|
||||||
|
channels: u8,
|
||||||
|
gpu_format: u8,
|
||||||
|
flags: u8,
|
||||||
|
mip_levels: u8,
|
||||||
|
pixels: *mut u8,
|
||||||
|
mips: *mut *mut u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
fn yapf_load_memory_mt(buffer: *const c_void, size: usize, threads: c_int) -> *mut RawImage;
|
||||||
|
fn yapf_free(img: *mut RawImage);
|
||||||
|
fn yapf_encode(img: *const RawImage, out_data: *mut *mut c_void, out_size: *mut usize) -> c_int;
|
||||||
|
fn yapf_free_buffer(data: *mut c_void);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const FLAG_PREMULT_ALPHA: u8 = 1;
|
||||||
|
pub const FLAG_SRGB: u8 = 2;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Error {
|
||||||
|
/// Not a YAPF v1 file, or corrupt.
|
||||||
|
Invalid,
|
||||||
|
/// The encoder rejected the image (bad size or channel count) or ran out of memory.
|
||||||
|
Encode(i32),
|
||||||
|
Io(std::io::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for Error {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Error::Invalid => write!(f, "not a valid YAPF file"),
|
||||||
|
Error::Encode(c) => write!(f, "YAPF encoding failed (code {c})"),
|
||||||
|
Error::Io(e) => write!(f, "{e}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl std::error::Error for Error {}
|
||||||
|
impl From<std::io::Error> for Error {
|
||||||
|
fn from(e: std::io::Error) -> Self { Error::Io(e) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A decoded image: `pixels` holds `width * height * channels` bytes, rows top to bottom.
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub struct Image {
|
||||||
|
pub width: u32,
|
||||||
|
pub height: u32,
|
||||||
|
/// 1 gray, 2 gray + alpha, 3 RGB, 4 RGBA
|
||||||
|
pub channels: u8,
|
||||||
|
pub flags: u8,
|
||||||
|
pub gpu_format: u8,
|
||||||
|
pub pixels: Vec<u8>,
|
||||||
|
/// Smaller mip levels (1, 2, …); empty when the file stores only the base image.
|
||||||
|
pub mips: Vec<Vec<u8>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Image {
|
||||||
|
pub fn new(width: u32, height: u32, channels: u8, pixels: Vec<u8>) -> Image {
|
||||||
|
let gpu_format = match channels { 4 => 4, 3 => 5, 2 => 2, _ => 3 };
|
||||||
|
Image { width, height, channels, flags: FLAG_SRGB, gpu_format, pixels, mips: Vec::new() }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Decode YAPF bytes; `threads` = 1 decodes on this thread, 0 uses every core.
|
||||||
|
pub fn decode_with_threads(data: &[u8], threads: i32) -> Result<Image, Error> {
|
||||||
|
unsafe {
|
||||||
|
let p = yapf_load_memory_mt(data.as_ptr().cast(), data.len(), threads);
|
||||||
|
if p.is_null() {
|
||||||
|
return Err(Error::Invalid);
|
||||||
|
}
|
||||||
|
let r = &*p;
|
||||||
|
let level = |m: usize| {
|
||||||
|
let w = (r.width >> m).max(1) as usize;
|
||||||
|
let h = (r.height >> m).max(1) as usize;
|
||||||
|
std::slice::from_raw_parts(*r.mips.add(m), w * h * r.channels as usize).to_vec()
|
||||||
|
};
|
||||||
|
let img = Image {
|
||||||
|
width: r.width, height: r.height, channels: r.channels, flags: r.flags,
|
||||||
|
gpu_format: r.gpu_format, pixels: level(0),
|
||||||
|
mips: (1..r.mip_levels as usize).map(level).collect(),
|
||||||
|
};
|
||||||
|
yapf_free(p);
|
||||||
|
Ok(img)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn decode(data: &[u8]) -> Result<Image, Error> { Image::decode_with_threads(data, 1) }
|
||||||
|
|
||||||
|
pub fn load<P: AsRef<Path>>(path: P) -> Result<Image, Error> {
|
||||||
|
Image::decode(&std::fs::read(path)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Encode to the bytes of a `.yapf` file.
|
||||||
|
pub fn encode(&self) -> Result<Vec<u8>, Error> {
|
||||||
|
let mut levels: Vec<*mut u8> = std::iter::once(self.pixels.as_ptr() as *mut u8)
|
||||||
|
.chain(self.mips.iter().map(|m| m.as_ptr() as *mut u8))
|
||||||
|
.collect();
|
||||||
|
let raw = RawImage {
|
||||||
|
width: self.width, height: self.height, channels: self.channels,
|
||||||
|
gpu_format: self.gpu_format, flags: self.flags, mip_levels: levels.len() as u8,
|
||||||
|
pixels: levels[0], mips: levels.as_mut_ptr(),
|
||||||
|
};
|
||||||
|
if self.pixels.len() != self.width as usize * self.height as usize * self.channels as usize {
|
||||||
|
return Err(Error::Encode(-1));
|
||||||
|
}
|
||||||
|
let mut data: *mut c_void = std::ptr::null_mut();
|
||||||
|
let mut size = 0usize;
|
||||||
|
let rc = unsafe { yapf_encode(&raw, &mut data, &mut size) };
|
||||||
|
if rc != 0 {
|
||||||
|
return Err(Error::Encode(rc));
|
||||||
|
}
|
||||||
|
let out = unsafe { std::slice::from_raw_parts(data as *const u8, size).to_vec() };
|
||||||
|
unsafe { yapf_free_buffer(data) };
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn save<P: AsRef<Path>>(&self, path: P) -> Result<(), Error> {
|
||||||
|
std::fs::write(path, self.encode()?)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn roundtrip() {
|
||||||
|
for ch in 1..=4u8 {
|
||||||
|
let (w, h) = (97u32, 61u32);
|
||||||
|
let px: Vec<u8> = (0..w * h * ch as u32).map(|i| (i * 7 ^ i >> 5) as u8).collect();
|
||||||
|
let img = Image::new(w, h, ch, px);
|
||||||
|
let back = Image::decode(&img.encode().unwrap()).unwrap();
|
||||||
|
assert_eq!(back, img);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_garbage() {
|
||||||
|
assert!(Image::decode(b"not an image").is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
13
bindings/zig/README.md
Normal file
13
bindings/zig/README.md
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
# YAPF for Zig
|
||||||
|
|
||||||
|
Zig imports `yapf.h` directly — no bindings needed:
|
||||||
|
|
||||||
|
```zig
|
||||||
|
const c = @cImport(@cInclude("yapf.h"));
|
||||||
|
const img = c.yapf_load("texture.yapf") orelse return error.CannotLoad;
|
||||||
|
defer c.yapf_free(img);
|
||||||
|
```
|
||||||
|
|
||||||
|
Add `yapf.c` to your build (`exe.addCSourceFile(.{ .file = b.path("yapf.c") })`,
|
||||||
|
`exe.linkLibC()`), or run the example:
|
||||||
|
`zig run example.zig ../../yapf.c -I../.. -lc -O ReleaseFast`.
|
||||||
33
bindings/zig/example.zig
Normal file
33
bindings/zig/example.zig
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
// YAPF from Zig: the C header is imported directly.
|
||||||
|
// zig run example.zig ../../yapf.c -I../.. -lc -O ReleaseFast
|
||||||
|
// (Uses only the C API plus std.debug.print, so it works across Zig versions.)
|
||||||
|
const std = @import("std");
|
||||||
|
const c = @cImport({
|
||||||
|
@cInclude("yapf.h");
|
||||||
|
@cInclude("time.h");
|
||||||
|
});
|
||||||
|
|
||||||
|
pub fn main() !void {
|
||||||
|
const path: [:0]const u8 = "../../other/YAPF.YAPF";
|
||||||
|
|
||||||
|
var img: *c.yapf_image_t = c.yapf_load(path.ptr) orelse return error.CannotLoad;
|
||||||
|
var best: c.clock_t = std.math.maxInt(c.clock_t); // best of 10 decodes
|
||||||
|
for (0..10) |_| {
|
||||||
|
const t = c.clock();
|
||||||
|
const again = c.yapf_load(path.ptr) orelse return error.CannotLoad;
|
||||||
|
best = @min(best, c.clock() - t);
|
||||||
|
c.yapf_free(img);
|
||||||
|
img = again;
|
||||||
|
}
|
||||||
|
defer c.yapf_free(img);
|
||||||
|
std.debug.print("{s}: {d}x{d}, {d} channels, decoded in {d:.2} ms\n", .{
|
||||||
|
path, img.width, img.height, img.channels, @as(f64, @floatFromInt(best)) * 1000.0 / @as(f64, @floatFromInt(c.CLOCKS_PER_SEC)),
|
||||||
|
});
|
||||||
|
|
||||||
|
var data: ?*anyopaque = null;
|
||||||
|
var size: usize = 0;
|
||||||
|
if (c.yapf_encode(img, &data, &size) != c.YAPF_OK) return error.EncodeFailed;
|
||||||
|
defer c.yapf_free_buffer(data);
|
||||||
|
const raw = @as(f64, @floatFromInt(img.width * img.height * img.channels));
|
||||||
|
std.debug.print("encoded: {d} bytes, {d:.1}% of the raw pixels\n", .{ size, 100.0 * @as(f64, @floatFromInt(size)) / raw });
|
||||||
|
}
|
||||||
263
build.bat
Normal file
263
build.bat
Normal file
|
|
@ -0,0 +1,263 @@
|
||||||
|
@echo off
|
||||||
|
setlocal enabledelayedexpansion
|
||||||
|
:: ============================================================
|
||||||
|
:: YAPF build script — Clang / LLVM only
|
||||||
|
:: Windows x64 and ARM64
|
||||||
|
:: ============================================================
|
||||||
|
::
|
||||||
|
:: Usage:
|
||||||
|
:: build.bat [options]
|
||||||
|
::
|
||||||
|
:: Options:
|
||||||
|
:: --debug Debug build (no optimisation, full symbols)
|
||||||
|
:: --no-simd Plain C only (SIMD is on by default, picked at run time)
|
||||||
|
:: --static-only Build only static libraries
|
||||||
|
:: --shared-only Build only shared libraries (DLL)
|
||||||
|
:: --help Show this message
|
||||||
|
::
|
||||||
|
:: Requirements:
|
||||||
|
:: clang from https://github.com/llvm/llvm-project/releases
|
||||||
|
:: or installed via: winget install LLVM.LLVM
|
||||||
|
:: llvm-ar bundled with LLVM
|
||||||
|
:: llvm-dlltool bundled with LLVM (generates .lib import library)
|
||||||
|
:: lld-link bundled with LLVM (linker, used via clang -fuse-ld=lld)
|
||||||
|
::
|
||||||
|
:: All tools must be on PATH. The LLVM installer adds them automatically
|
||||||
|
:: when "Add LLVM to the system PATH" is selected.
|
||||||
|
:: ============================================================
|
||||||
|
|
||||||
|
:: ── ANSI colour support (Windows 10 1511+, Windows Terminal) ────────
|
||||||
|
for /f "tokens=4-5 delims=. " %%i in ('ver') do set WIN_VER=%%i.%%j
|
||||||
|
set ANSI_OK=0
|
||||||
|
if !WIN_VER! GEQ 10.0 set ANSI_OK=1
|
||||||
|
|
||||||
|
if !ANSI_OK!==1 (
|
||||||
|
set "C_RED=[31m"
|
||||||
|
set "C_GRN=[32m"
|
||||||
|
set "C_YLW=[33m"
|
||||||
|
set "C_CYN=[36m"
|
||||||
|
set "C_BLD=[1m"
|
||||||
|
set "C_RST=[0m"
|
||||||
|
) else (
|
||||||
|
set "C_RED=" & set "C_GRN=" & set "C_YLW="
|
||||||
|
set "C_CYN=" & set "C_BLD=" & set "C_RST="
|
||||||
|
)
|
||||||
|
|
||||||
|
:: ── Option parsing ───────────────────────────────────────────────────
|
||||||
|
set OPT_DEBUG=0
|
||||||
|
set OPT_NOSIMD=0
|
||||||
|
set OPT_STATIC_ONLY=0
|
||||||
|
set OPT_SHARED_ONLY=0
|
||||||
|
|
||||||
|
for %%A in (%*) do (
|
||||||
|
if /i "%%A"=="--debug" set OPT_DEBUG=1
|
||||||
|
if /i "%%A"=="--no-simd" set OPT_NOSIMD=1
|
||||||
|
if /i "%%A"=="--static-only" set OPT_STATIC_ONLY=1
|
||||||
|
if /i "%%A"=="--shared-only" set OPT_SHARED_ONLY=1
|
||||||
|
if /i "%%A"=="--help" goto :show_help
|
||||||
|
)
|
||||||
|
|
||||||
|
if !OPT_STATIC_ONLY!==1 if !OPT_SHARED_ONLY!==1 (
|
||||||
|
echo !C_RED![fail]!C_RST! --static-only and --shared-only are mutually exclusive.
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
goto :after_help
|
||||||
|
|
||||||
|
:show_help
|
||||||
|
findstr /n "^" "%~f0" | findstr /r "^[3-9]:" | findstr /r "^1[0-9]:" > nul
|
||||||
|
more +2 "%~f0" | findstr /r "^::" | findstr /v "^:: ====" | ^
|
||||||
|
for /f "delims=:" %%L in ('findstr /n "^"') do echo %%L
|
||||||
|
exit /b 0
|
||||||
|
|
||||||
|
:after_help
|
||||||
|
|
||||||
|
:: ── Tool detection ───────────────────────────────────────────────────
|
||||||
|
echo.
|
||||||
|
echo !C_BLD!──── Detecting tools ────!C_RST!
|
||||||
|
|
||||||
|
where clang >nul 2>&1
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo !C_RED![fail]!C_RST! clang not found.
|
||||||
|
echo Install LLVM: winget install LLVM.LLVM
|
||||||
|
echo Then add it to PATH and reopen this terminal.
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
for /f "delims=" %%V in ('clang --version 2^>^&1 ^| findstr /r "clang version"') do (
|
||||||
|
echo !C_CYN![info]!C_RST! Compiler : %%V
|
||||||
|
)
|
||||||
|
|
||||||
|
where llvm-ar >nul 2>&1
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo !C_RED![fail]!C_RST! llvm-ar not found. Make sure LLVM is fully installed.
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
echo !C_CYN![info]!C_RST! Archiver : llvm-ar (LLVM)
|
||||||
|
|
||||||
|
set HAVE_DLLTOOL=0
|
||||||
|
where llvm-dlltool >nul 2>&1
|
||||||
|
if not errorlevel 1 (
|
||||||
|
set HAVE_DLLTOOL=1
|
||||||
|
echo !C_CYN![info]!C_RST! DllTool : llvm-dlltool (LLVM)
|
||||||
|
) else (
|
||||||
|
echo !C_YLW![warn]!C_RST! llvm-dlltool not found — .lib import library will be skipped
|
||||||
|
)
|
||||||
|
|
||||||
|
:: ── Compiler flags ───────────────────────────────────────────────────
|
||||||
|
set CFLAGS=-std=c99 -Wall -Wextra -Wpedantic
|
||||||
|
set LFLAGS=-fuse-ld=lld
|
||||||
|
|
||||||
|
if !OPT_DEBUG!==1 (
|
||||||
|
set CFLAGS=!CFLAGS! -O0 -g
|
||||||
|
echo !C_CYN![info]!C_RST! Mode : debug
|
||||||
|
) else (
|
||||||
|
set CFLAGS=!CFLAGS! -O3 -DNDEBUG
|
||||||
|
set LFLAGS=!LFLAGS! -Wl,/OPT:REF,/OPT:ICF
|
||||||
|
echo !C_CYN![info]!C_RST! Mode : release
|
||||||
|
)
|
||||||
|
|
||||||
|
if !OPT_NOSIMD!==1 (
|
||||||
|
set CFLAGS=!CFLAGS! -DYAPF_NO_SIMD
|
||||||
|
echo !C_CYN![info]!C_RST! SIMD : disabled
|
||||||
|
) else (
|
||||||
|
echo !C_CYN![info]!C_RST! SIMD : SSSE3 / NEON, chosen at run time
|
||||||
|
)
|
||||||
|
|
||||||
|
set BUILD_SHARED=1
|
||||||
|
set BUILD_STATIC=1
|
||||||
|
if !OPT_STATIC_ONLY!==1 set BUILD_SHARED=0
|
||||||
|
if !OPT_SHARED_ONLY!==1 set BUILD_STATIC=0
|
||||||
|
|
||||||
|
:: ── Output directories ───────────────────────────────────────────────
|
||||||
|
if not exist dist\include mkdir dist\include
|
||||||
|
if not exist dist\windows-x64 mkdir dist\windows-x64
|
||||||
|
if not exist dist\windows-arm64 mkdir dist\windows-arm64
|
||||||
|
|
||||||
|
copy /y yapf.h dist\include\yapf.h >nul
|
||||||
|
echo !C_GRN![ ok ]!C_RST! Header ^→ dist\include\yapf.h
|
||||||
|
|
||||||
|
:: ── Write module definition file ─────────────────────────────────────
|
||||||
|
:: Used by llvm-dlltool to generate a MSVC-compatible import library.
|
||||||
|
(
|
||||||
|
echo LIBRARY yapf
|
||||||
|
echo EXPORTS
|
||||||
|
echo yapf_load
|
||||||
|
echo yapf_load_memory
|
||||||
|
echo yapf_load_mt
|
||||||
|
echo yapf_load_memory_mt
|
||||||
|
echo yapf_save
|
||||||
|
echo yapf_encode
|
||||||
|
echo yapf_free
|
||||||
|
echo yapf_free_buffer
|
||||||
|
) > dist\_yapf.def
|
||||||
|
|
||||||
|
:: ====================================================================
|
||||||
|
:: Build: Windows x64
|
||||||
|
:: ====================================================================
|
||||||
|
echo.
|
||||||
|
echo !C_BLD!──── Building Windows x64 ────!C_RST!
|
||||||
|
|
||||||
|
if !BUILD_SHARED!==1 (
|
||||||
|
clang --target=x86_64-w64-windows-gnu !CFLAGS! !LFLAGS! ^
|
||||||
|
-shared yapf.c -o dist\windows-x64\yapf.dll
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo !C_RED![fail]!C_RST! Windows x64 shared build failed.
|
||||||
|
del /q dist\_yapf.def 2>nul
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
echo !C_GRN![ ok ]!C_RST! dist\windows-x64\yapf.dll
|
||||||
|
|
||||||
|
if !HAVE_DLLTOOL!==1 (
|
||||||
|
llvm-dlltool -m i386:x86-64 -D yapf.dll ^
|
||||||
|
-d dist\_yapf.def -l dist\windows-x64\yapf.lib
|
||||||
|
if not errorlevel 1 (
|
||||||
|
echo !C_GRN![ ok ]!C_RST! dist\windows-x64\yapf.lib (import library)
|
||||||
|
) else (
|
||||||
|
echo !C_YLW![warn]!C_RST! Import library generation failed (non-fatal)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if !BUILD_STATIC!==1 (
|
||||||
|
clang --target=x86_64-w64-windows-gnu !CFLAGS! ^
|
||||||
|
-c yapf.c -o dist\windows-x64\_yapf.obj
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo !C_RED![fail]!C_RST! Windows x64 static compile failed.
|
||||||
|
del /q dist\_yapf.def dist\windows-x64\_yapf.obj 2>nul
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
llvm-ar rcs dist\windows-x64\libyapf.a dist\windows-x64\_yapf.obj
|
||||||
|
del /q dist\windows-x64\_yapf.obj
|
||||||
|
echo !C_GRN![ ok ]!C_RST! dist\windows-x64\libyapf.a (static)
|
||||||
|
)
|
||||||
|
|
||||||
|
:: Command-line tool
|
||||||
|
clang --target=x86_64-w64-windows-gnu !CFLAGS! !LFLAGS! -Wno-unused-function ^
|
||||||
|
tools\yapf_cli.c yapf.c -o dist\windows-x64\yapf.exe
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo !C_YLW![warn]!C_RST! yapf.exe build failed (non-fatal)
|
||||||
|
) else (
|
||||||
|
echo !C_GRN![ ok ]!C_RST! dist\windows-x64\yapf.exe (command-line tool)
|
||||||
|
)
|
||||||
|
|
||||||
|
:: ====================================================================
|
||||||
|
:: Build: Windows ARM64
|
||||||
|
:: ====================================================================
|
||||||
|
echo.
|
||||||
|
echo !C_BLD!──── Building Windows ARM64 ────!C_RST!
|
||||||
|
|
||||||
|
:: Probe whether Clang can target ARM64 Windows without errors.
|
||||||
|
echo int x; | clang --target=aarch64-w64-windows-gnu -x c - -c -o nul >nul 2>&1
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo !C_YLW![warn]!C_RST! ARM64 target not available in this Clang build — skipping.
|
||||||
|
echo If you need ARM64, install a full LLVM release from llvm.org.
|
||||||
|
goto :skip_arm64
|
||||||
|
)
|
||||||
|
|
||||||
|
if !BUILD_SHARED!==1 (
|
||||||
|
clang --target=aarch64-w64-windows-gnu !CFLAGS! !LFLAGS! ^
|
||||||
|
-shared yapf.c -o dist\windows-arm64\yapf.dll
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo !C_YLW![warn]!C_RST! Windows ARM64 shared build failed — skipping.
|
||||||
|
goto :skip_arm64
|
||||||
|
)
|
||||||
|
echo !C_GRN![ ok ]!C_RST! dist\windows-arm64\yapf.dll
|
||||||
|
|
||||||
|
if !HAVE_DLLTOOL!==1 (
|
||||||
|
llvm-dlltool -m arm64 -D yapf.dll ^
|
||||||
|
-d dist\_yapf.def -l dist\windows-arm64\yapf.lib
|
||||||
|
if not errorlevel 1 (
|
||||||
|
echo !C_GRN![ ok ]!C_RST! dist\windows-arm64\yapf.lib (import library)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if !BUILD_STATIC!==1 (
|
||||||
|
clang --target=aarch64-w64-windows-gnu !CFLAGS! ^
|
||||||
|
-c yapf.c -o dist\windows-arm64\_yapf.obj
|
||||||
|
if not errorlevel 1 (
|
||||||
|
llvm-ar rcs dist\windows-arm64\libyapf.a dist\windows-arm64\_yapf.obj
|
||||||
|
del /q dist\windows-arm64\_yapf.obj
|
||||||
|
echo !C_GRN![ ok ]!C_RST! dist\windows-arm64\libyapf.a (static)
|
||||||
|
) else (
|
||||||
|
echo !C_YLW![warn]!C_RST! Windows ARM64 static compile failed — skipping.
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
clang --target=aarch64-w64-windows-gnu !CFLAGS! !LFLAGS! -Wno-unused-function ^
|
||||||
|
tools\yapf_cli.c yapf.c -o dist\windows-arm64\yapf.exe
|
||||||
|
if not errorlevel 1 echo !C_GRN![ ok ]!C_RST! dist\windows-arm64\yapf.exe (command-line tool)
|
||||||
|
|
||||||
|
:skip_arm64
|
||||||
|
|
||||||
|
:: ── Cleanup temp files ───────────────────────────────────────────────
|
||||||
|
del /q dist\_yapf.def 2>nul
|
||||||
|
|
||||||
|
:: ── Summary ──────────────────────────────────────────────────────────
|
||||||
|
echo.
|
||||||
|
echo !C_BLD!──── Summary ────!C_RST!
|
||||||
|
for /r dist %%F in (*) do (
|
||||||
|
echo !C_GRN!%%F!C_RST!
|
||||||
|
)
|
||||||
|
echo.
|
||||||
|
echo !C_GRN![ ok ]!C_RST! Done.
|
||||||
|
endlocal
|
||||||
301
build.sh
Executable file
301
build.sh
Executable file
|
|
@ -0,0 +1,301 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
# ============================================================
|
||||||
|
# YAPF build script — Clang / LLVM only
|
||||||
|
# ============================================================
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ./build.sh [options]
|
||||||
|
#
|
||||||
|
# Options:
|
||||||
|
# --debug Debug build: no optimisation, full symbols
|
||||||
|
# --no-simd Plain C only (SIMD is on by default, picked at run time)
|
||||||
|
# --static-only Build only static libraries
|
||||||
|
# --shared-only Build only shared libraries
|
||||||
|
# --help Show this message
|
||||||
|
#
|
||||||
|
# macOS → produces universal (arm64 + x86_64) .dylib and .a
|
||||||
|
# Linux → produces native .so + .a; attempts arm64 cross-compile
|
||||||
|
# if clang supports the aarch64-linux-gnu target and
|
||||||
|
# lld is available. Install the cross sysroot with:
|
||||||
|
# Debian/Ubuntu: sudo apt install gcc-aarch64-linux-gnu
|
||||||
|
# Fedora: sudo dnf install gcc-aarch64-linux-gnu
|
||||||
|
# ============================================================
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# ── ANSI colours (silenced when stdout is not a terminal) ────────────
|
||||||
|
if [[ -t 1 ]]; then
|
||||||
|
RED=$'\033[0;31m' GRN=$'\033[0;32m' YLW=$'\033[1;33m'
|
||||||
|
CYN=$'\033[0;36m' BLD=$'\033[1m' RST=$'\033[0m'
|
||||||
|
else
|
||||||
|
RED='' GRN='' YLW='' CYN='' BLD='' RST=''
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Helpers ──────────────────────────────────────────────────────────
|
||||||
|
info() { echo "${CYN}[info]${RST} $*"; }
|
||||||
|
ok() { echo "${GRN}[ ok ]${RST} $*"; }
|
||||||
|
warn() { echo "${YLW}[warn]${RST} $*" >&2; }
|
||||||
|
die() { echo "${RED}[fail]${RST} $*" >&2; exit 1; }
|
||||||
|
step() { echo; echo "${BLD}──── $* ────${RST}"; }
|
||||||
|
|
||||||
|
# ── Option parsing ───────────────────────────────────────────────────
|
||||||
|
OPT_DEBUG=0
|
||||||
|
OPT_NOSIMD=0
|
||||||
|
OPT_STATIC_ONLY=0
|
||||||
|
OPT_SHARED_ONLY=0
|
||||||
|
|
||||||
|
for arg in "$@"; do
|
||||||
|
case "$arg" in
|
||||||
|
--debug) OPT_DEBUG=1 ;;
|
||||||
|
--no-simd) OPT_NOSIMD=1 ;;
|
||||||
|
--simd) ;; # SIMD is the default now; kept for old scripts
|
||||||
|
--static-only) OPT_STATIC_ONLY=1 ;;
|
||||||
|
--shared-only) OPT_SHARED_ONLY=1 ;;
|
||||||
|
--help)
|
||||||
|
sed -n '3,30p' "$0" | sed 's/^# \?//'
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*) die "Unknown option: $arg (try --help)" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ $OPT_STATIC_ONLY -eq 1 && $OPT_SHARED_ONLY -eq 1 ]]; then
|
||||||
|
die "--static-only and --shared-only are mutually exclusive"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Tool detection ───────────────────────────────────────────────────
|
||||||
|
step "Detecting tools"
|
||||||
|
|
||||||
|
require() {
|
||||||
|
command -v "$1" &>/dev/null || die "$1 not found. $2"
|
||||||
|
}
|
||||||
|
|
||||||
|
require clang "Install LLVM: https://releases.llvm.org/"
|
||||||
|
|
||||||
|
CLANG=$(command -v clang)
|
||||||
|
CLANG_VER=$("$CLANG" --version | head -1)
|
||||||
|
info "Compiler : $CLANG_VER"
|
||||||
|
|
||||||
|
# Prefer LLVM's archiver; fall back to the system one.
|
||||||
|
if command -v llvm-ar &>/dev/null; then AR=$(command -v llvm-ar)
|
||||||
|
elif command -v ar &>/dev/null; then AR=$(command -v ar)
|
||||||
|
else die "No archiver found (llvm-ar or ar)"
|
||||||
|
fi
|
||||||
|
info "Archiver : $AR"
|
||||||
|
|
||||||
|
STRIP=""
|
||||||
|
if command -v llvm-strip &>/dev/null; then STRIP=$(command -v llvm-strip)
|
||||||
|
elif command -v strip &>/dev/null; then STRIP=$(command -v strip)
|
||||||
|
fi
|
||||||
|
[[ -n "$STRIP" ]] && info "Strip : $STRIP"
|
||||||
|
|
||||||
|
# ── Compiler flags ───────────────────────────────────────────────────
|
||||||
|
CFLAGS="-std=c99 -Wall -Wextra -Wpedantic -fPIC -pthread"
|
||||||
|
LFLAGS=""
|
||||||
|
|
||||||
|
if [[ $OPT_DEBUG -eq 1 ]]; then
|
||||||
|
CFLAGS="$CFLAGS -O0 -g"
|
||||||
|
info "Mode : debug"
|
||||||
|
else
|
||||||
|
CFLAGS="$CFLAGS -O3 -DNDEBUG"
|
||||||
|
LFLAGS="$LFLAGS -Wl,-s" # strip symbols from shared libs
|
||||||
|
info "Mode : release"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ $OPT_NOSIMD -eq 1 ]]; then CFLAGS="$CFLAGS -DYAPF_NO_SIMD"; info "SIMD : disabled"
|
||||||
|
else info "SIMD : SSSE3 / NEON, chosen at run time"; fi
|
||||||
|
|
||||||
|
# ── Detect OS ────────────────────────────────────────────────────────
|
||||||
|
OS=$(uname -s)
|
||||||
|
HOST_ARCH=$(uname -m)
|
||||||
|
info "Host : $OS / $HOST_ARCH"
|
||||||
|
|
||||||
|
# ── Output layout ────────────────────────────────────────────────────
|
||||||
|
mkdir -p dist/include
|
||||||
|
|
||||||
|
cp yapf.h dist/include/yapf.h
|
||||||
|
ok "Header → dist/include/yapf.h"
|
||||||
|
|
||||||
|
BUILD_SHARED=$([[ $OPT_STATIC_ONLY -eq 0 ]] && echo 1 || echo 0)
|
||||||
|
BUILD_STATIC=$([[ $OPT_SHARED_ONLY -eq 0 ]] && echo 1 || echo 0)
|
||||||
|
|
||||||
|
# ── Build helpers ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
# compile_shared <target-triple> <out-path>
|
||||||
|
compile_shared() {
|
||||||
|
local triple="$1" out="$2"
|
||||||
|
"$CLANG" $CFLAGS $LFLAGS --target="$triple" -shared yapf.c -o "$out"
|
||||||
|
}
|
||||||
|
|
||||||
|
# compile_static <target-triple> <out-path>
|
||||||
|
compile_static() {
|
||||||
|
local triple="$1" out="$2" obj
|
||||||
|
obj="${out%.a}.o"
|
||||||
|
"$CLANG" $CFLAGS --target="$triple" -c yapf.c -o "$obj"
|
||||||
|
"$AR" rcs "$out" "$obj"
|
||||||
|
rm -f "$obj"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── macOS build ──────────────────────────────────────────────────────
|
||||||
|
if [[ "$OS" == "Darwin" ]]; then
|
||||||
|
|
||||||
|
require lipo "lipo is part of Xcode Command Line Tools: xcode-select --install"
|
||||||
|
LIPO=$(command -v lipo)
|
||||||
|
|
||||||
|
mkdir -p dist/macos
|
||||||
|
|
||||||
|
step "Building macOS arm64"
|
||||||
|
if [[ $BUILD_SHARED -eq 1 ]]; then
|
||||||
|
compile_shared arm64-apple-macos11 /tmp/_yapf_arm64.dylib
|
||||||
|
ok "arm64 dylib"
|
||||||
|
fi
|
||||||
|
if [[ $BUILD_STATIC -eq 1 ]]; then
|
||||||
|
compile_static arm64-apple-macos11 /tmp/_yapf_arm64.a
|
||||||
|
ok "arm64 static"
|
||||||
|
fi
|
||||||
|
|
||||||
|
step "Building macOS x86_64"
|
||||||
|
if [[ $BUILD_SHARED -eq 1 ]]; then
|
||||||
|
compile_shared x86_64-apple-macos10.15 /tmp/_yapf_x64.dylib
|
||||||
|
ok "x86_64 dylib"
|
||||||
|
fi
|
||||||
|
if [[ $BUILD_STATIC -eq 1 ]]; then
|
||||||
|
compile_static x86_64-apple-macos10.15 /tmp/_yapf_x64.a
|
||||||
|
ok "x86_64 static"
|
||||||
|
fi
|
||||||
|
|
||||||
|
step "Creating universal binaries (lipo)"
|
||||||
|
if [[ $BUILD_SHARED -eq 1 ]]; then
|
||||||
|
"$LIPO" -create /tmp/_yapf_arm64.dylib /tmp/_yapf_x64.dylib \
|
||||||
|
-output dist/macos/libyapf.dylib
|
||||||
|
rm -f /tmp/_yapf_arm64.dylib /tmp/_yapf_x64.dylib
|
||||||
|
ok "dist/macos/libyapf.dylib (universal)"
|
||||||
|
fi
|
||||||
|
if [[ $BUILD_STATIC -eq 1 ]]; then
|
||||||
|
"$LIPO" -create /tmp/_yapf_arm64.a /tmp/_yapf_x64.a \
|
||||||
|
-output dist/macos/libyapf.a
|
||||||
|
rm -f /tmp/_yapf_arm64.a /tmp/_yapf_x64.a
|
||||||
|
ok "dist/macos/libyapf.a (universal)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
step "Building the yapf command-line tool"
|
||||||
|
"$CLANG" $CFLAGS --target=arm64-apple-macos11 tools/yapf_cli.c yapf.c -o /tmp/_yapf_cli_arm64 -lm
|
||||||
|
"$CLANG" $CFLAGS --target=x86_64-apple-macos10.15 tools/yapf_cli.c yapf.c -o /tmp/_yapf_cli_x64 -lm
|
||||||
|
"$LIPO" -create /tmp/_yapf_cli_arm64 /tmp/_yapf_cli_x64 -output dist/macos/yapf
|
||||||
|
rm -f /tmp/_yapf_cli_arm64 /tmp/_yapf_cli_x64
|
||||||
|
ok "dist/macos/yapf (universal)"
|
||||||
|
|
||||||
|
# ── Linux build ──────────────────────────────────────────────────────
|
||||||
|
elif [[ "$OS" == "Linux" ]]; then
|
||||||
|
|
||||||
|
# Normalise arch name to the triple component clang expects.
|
||||||
|
case "$HOST_ARCH" in
|
||||||
|
x86_64) NATIVE_TRIPLE="x86_64-linux-gnu" ;;
|
||||||
|
aarch64) NATIVE_TRIPLE="aarch64-linux-gnu" ;;
|
||||||
|
armv7*) NATIVE_TRIPLE="armv7-linux-gnueabihf" ;;
|
||||||
|
*) NATIVE_TRIPLE="$HOST_ARCH-linux-gnu" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Friendly dir name (x86_64 → linux-x64, aarch64 → linux-arm64, etc.)
|
||||||
|
case "$HOST_ARCH" in
|
||||||
|
x86_64) NATIVE_DIR="linux-x64" ;;
|
||||||
|
aarch64) NATIVE_DIR="linux-arm64" ;;
|
||||||
|
*) NATIVE_DIR="linux-$HOST_ARCH" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
mkdir -p "dist/$NATIVE_DIR"
|
||||||
|
|
||||||
|
step "Building Linux $HOST_ARCH (native)"
|
||||||
|
if [[ $BUILD_SHARED -eq 1 ]]; then
|
||||||
|
compile_shared "$NATIVE_TRIPLE" "dist/$NATIVE_DIR/libyapf.so"
|
||||||
|
ok "dist/$NATIVE_DIR/libyapf.so"
|
||||||
|
fi
|
||||||
|
if [[ $BUILD_STATIC -eq 1 ]]; then
|
||||||
|
compile_static "$NATIVE_TRIPLE" "dist/$NATIVE_DIR/libyapf.a"
|
||||||
|
ok "dist/$NATIVE_DIR/libyapf.a"
|
||||||
|
fi
|
||||||
|
"$CLANG" $CFLAGS --target="$NATIVE_TRIPLE" tools/yapf_cli.c yapf.c -o "dist/$NATIVE_DIR/yapf" -lm
|
||||||
|
ok "dist/$NATIVE_DIR/yapf (command-line tool)"
|
||||||
|
|
||||||
|
# ── Cross-compile to the other common arch ────────────────────────
|
||||||
|
if [[ "$HOST_ARCH" == "x86_64" ]]; then
|
||||||
|
CROSS_TRIPLE="aarch64-linux-gnu"
|
||||||
|
CROSS_DIR="linux-arm64"
|
||||||
|
elif [[ "$HOST_ARCH" == "aarch64" ]]; then
|
||||||
|
CROSS_TRIPLE="x86_64-linux-gnu"
|
||||||
|
CROSS_DIR="linux-x64"
|
||||||
|
else
|
||||||
|
CROSS_TRIPLE=""
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$CROSS_TRIPLE" ]]; then
|
||||||
|
step "Cross-compiling Linux ${CROSS_DIR##linux-} (${CROSS_TRIPLE})"
|
||||||
|
|
||||||
|
# Probe by actually compiling a file that exercises the C standard
|
||||||
|
# library headers — a bare "int x;" succeeds even with a broken
|
||||||
|
# sysroot because it never touches any include paths.
|
||||||
|
CROSS_PROBE_SRC=$'#include <stdio.h>\nint x;\n'
|
||||||
|
CROSS_PROBE_OK=0
|
||||||
|
if printf '%s' "$CROSS_PROBE_SRC" | \
|
||||||
|
"$CLANG" --target="$CROSS_TRIPLE" -fuse-ld=lld \
|
||||||
|
-x c - -c -o /dev/null 2>/dev/null; then
|
||||||
|
CROSS_PROBE_OK=1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ $CROSS_PROBE_OK -eq 0 ]]; then
|
||||||
|
warn "Cross-sysroot for ${CROSS_TRIPLE} not available — skipping."
|
||||||
|
warn "To enable: install the cross toolchain for your distro:"
|
||||||
|
warn " Debian/Ubuntu: sudo apt install gcc-aarch64-linux-gnu lld"
|
||||||
|
warn " Fedora: sudo dnf install gcc-aarch64-linux-gnu lld"
|
||||||
|
warn " Arch: sudo pacman -S aarch64-linux-gnu-gcc lld"
|
||||||
|
else
|
||||||
|
mkdir -p "dist/$CROSS_DIR"
|
||||||
|
# Disable set -e locally so a cross-compile failure is non-fatal.
|
||||||
|
set +e
|
||||||
|
CROSS_BUILD_OK=1
|
||||||
|
|
||||||
|
if [[ $BUILD_SHARED -eq 1 ]]; then
|
||||||
|
"$CLANG" $CFLAGS $LFLAGS --target="$CROSS_TRIPLE" -fuse-ld=lld \
|
||||||
|
-shared yapf.c -o "dist/$CROSS_DIR/libyapf.so" 2>/tmp/_yapf_cross_err
|
||||||
|
if [[ $? -eq 0 ]]; then
|
||||||
|
ok "dist/$CROSS_DIR/libyapf.so"
|
||||||
|
else
|
||||||
|
warn "Shared cross-compile failed:"; cat /tmp/_yapf_cross_err >&2
|
||||||
|
CROSS_BUILD_OK=0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ $BUILD_STATIC -eq 1 ]]; then
|
||||||
|
"$CLANG" $CFLAGS --target="$CROSS_TRIPLE" -fuse-ld=lld \
|
||||||
|
-c yapf.c -o /tmp/_yapf_cross.o 2>/tmp/_yapf_cross_err
|
||||||
|
if [[ $? -eq 0 ]]; then
|
||||||
|
"$AR" rcs "dist/$CROSS_DIR/libyapf.a" /tmp/_yapf_cross.o
|
||||||
|
rm -f /tmp/_yapf_cross.o
|
||||||
|
ok "dist/$CROSS_DIR/libyapf.a"
|
||||||
|
else
|
||||||
|
warn "Static cross-compile failed:"; cat /tmp/_yapf_cross_err >&2
|
||||||
|
CROSS_BUILD_OK=0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -f /tmp/_yapf_cross_err
|
||||||
|
set -e
|
||||||
|
|
||||||
|
if [[ $CROSS_BUILD_OK -eq 0 ]]; then
|
||||||
|
warn "Cross-compile produced partial results — check warnings above."
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
else
|
||||||
|
die "Unsupported OS: $OS. Use build.bat on Windows."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# ── Summary ──────────────────────────────────────────────────────────
|
||||||
|
step "Summary"
|
||||||
|
find dist -type f | sort | while IFS= read -r f; do
|
||||||
|
size=$(du -sh "$f" 2>/dev/null | cut -f1)
|
||||||
|
printf " ${GRN}%-44s${RST} %s\n" "$f" "$size"
|
||||||
|
done
|
||||||
|
echo
|
||||||
|
ok "Done."
|
||||||
39
default.nix
Normal file
39
default.nix
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
# YAPF for Nix / NixOS: the `yapf` tool, libyapf (static + shared) and
|
||||||
|
# yapf.h, plus the image/x-yapf MIME type and a freedesktop thumbnailer.
|
||||||
|
#
|
||||||
|
# nix-build build ./result
|
||||||
|
# nix-env -if . install for your user
|
||||||
|
# NixOS: environment.systemPackages = [ (pkgs.callPackage /path/to/YAPF { }) ];
|
||||||
|
{ pkgs ? import <nixpkgs> { }, lib ? pkgs.lib, stdenv ? pkgs.stdenv }:
|
||||||
|
|
||||||
|
stdenv.mkDerivation {
|
||||||
|
pname = "yapf";
|
||||||
|
version = "2.0";
|
||||||
|
|
||||||
|
src = lib.cleanSourceWith {
|
||||||
|
src = ./.;
|
||||||
|
filter = path: type:
|
||||||
|
let base = baseNameOf path; in
|
||||||
|
!(lib.elem base [ "build" "dist" "result" ".git" "node_modules" ]);
|
||||||
|
};
|
||||||
|
|
||||||
|
makeFlags = [ "PREFIX=$(out)" ];
|
||||||
|
doCheck = true;
|
||||||
|
checkTarget = "test";
|
||||||
|
|
||||||
|
postInstall = ''
|
||||||
|
install -Dm644 extensions/linux/yapf-mime.xml $out/share/mime/packages/yapf.xml
|
||||||
|
install -Dm644 extensions/linux/yapf.thumbnailer $out/share/thumbnailers/yapf.thumbnailer
|
||||||
|
substituteInPlace $out/share/thumbnailers/yapf.thumbnailer \
|
||||||
|
--replace-fail "TryExec=yapf" "TryExec=$out/bin/yapf" \
|
||||||
|
--replace-fail "Exec=yapf " "Exec=$out/bin/yapf "
|
||||||
|
'';
|
||||||
|
|
||||||
|
meta = {
|
||||||
|
description = "YAPF: fast lossless images and GPU-ready textures";
|
||||||
|
homepage = "https://github.com/Nexoniarz/YAPF";
|
||||||
|
license = lib.licenses.asl20;
|
||||||
|
platforms = lib.platforms.unix;
|
||||||
|
mainProgram = "yapf";
|
||||||
|
};
|
||||||
|
}
|
||||||
450
docs/FORMAT.md
Normal file
450
docs/FORMAT.md
Normal file
|
|
@ -0,0 +1,450 @@
|
||||||
|
# YAPF file format
|
||||||
|
|
||||||
|
This is the complete specification of YAPF files. `yapf.c` is the
|
||||||
|
reference implementation; where this text and the code disagree, that is a
|
||||||
|
bug in one of them.
|
||||||
|
|
||||||
|
YAPF has two versions:
|
||||||
|
|
||||||
|
| Version | What it holds | Who writes it |
|
||||||
|
| :------ | :------------ | :------------ |
|
||||||
|
| **1** | one 8-bit image (1–4 channels) with optional mips | `yapf_save` / `yapf_encode`, and the CLI for plain 8-bit images |
|
||||||
|
| **2** | a texture: GPU blocks or lossless pixels in 8/16-bit or float, cubes, arrays, volumes, animation, channel meanings, colour description, notes, checksums, optional entropy coding | `yapf_tex_save` / `yapf_tex_encode`, and the CLI whenever a v2 feature is used |
|
||||||
|
|
||||||
|
Version 1 is specified at the top of [`yapf.h`](../yapf.h) and is not
|
||||||
|
repeated here, except that version 2 reuses its tiles, filters and plane
|
||||||
|
payloads (the "group form") byte for byte.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Design: how YAPF 2 does things its own way
|
||||||
|
|
||||||
|
YAPF 2 is not a wrapper around someone else's container. A few ideas
|
||||||
|
carry the whole format:
|
||||||
|
|
||||||
|
1. **Everything is tiles.** Pixels are cut into 64 × 64 tiles, GPU
|
||||||
|
blocks into tiles of 16 × 16 blocks. Each tile has its own size and
|
||||||
|
checksum in a table, so any tile can be found, checked and decoded on its
|
||||||
|
own: for multithreaded loading, for partial updates, and for virtual
|
||||||
|
texturing / "megatexture" streaming.
|
||||||
|
2. **Small mips first.** Surface data is stored from the smallest mip
|
||||||
|
to the largest. The first *N* bytes of a file are a complete, lower
|
||||||
|
resolution texture, so an engine can show something after reading a
|
||||||
|
few kilobytes and stream in the large mips later. The header carries
|
||||||
|
the file size, so a loader knows how much is still to come.
|
||||||
|
3. **Needs, not versions.** New capabilities are announced by *needs*
|
||||||
|
bits. A decoder refuses a file with a needs bit it does not know, and
|
||||||
|
can ignore everything else safely. No more version bumps that break
|
||||||
|
every reader.
|
||||||
|
4. **Wide samples are byte planes.** 16-bit and float samples are split
|
||||||
|
into byte planes (high bytes, low bytes) and coded with the same fast
|
||||||
|
8-bit machinery as version 1: smooth high bytes compress very well, and
|
||||||
|
the decoder stays branch-light and SIMD friendly.
|
||||||
|
5. **Shared code books.** The optional entropy stage keeps its Huffman
|
||||||
|
tables once per surface, keyed by a small neighbourhood context, so
|
||||||
|
tiles stay independent and their own headers stay tiny. For GPU
|
||||||
|
blocks the tables are per byte lane, so endpoint bytes and index bytes
|
||||||
|
are coded separately.
|
||||||
|
6. **Meaning travels with the data.** Channel meanings, the normal-map
|
||||||
|
convention, linear channels inside sRGB images, colour primaries and
|
||||||
|
transfer, and how the mips were made are header fields, not file-name
|
||||||
|
suffixes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Conventions
|
||||||
|
|
||||||
|
All integers are little-endian. `u8`, `u16`, `u32` and `u64` are unsigned
|
||||||
|
integers of that many bits. Offsets are from the start of the file.
|
||||||
|
|
||||||
|
### yapf-sum
|
||||||
|
|
||||||
|
The checksum of a byte string of length *n*. `K = 0x9E3779B97F4A7C15`;
|
||||||
|
all arithmetic is modulo 2⁶⁴.
|
||||||
|
|
||||||
|
```
|
||||||
|
lane[i] = (i + 1)·K XOR n for i = 0, 1, 2, 3
|
||||||
|
for each 8-byte word w_j of the input (little-endian; the last word is
|
||||||
|
padded with zero bytes):
|
||||||
|
a = lane[j mod 4]
|
||||||
|
a = (a XOR w_j) · K
|
||||||
|
a = a XOR (a >> 32)
|
||||||
|
lane[j mod 4] = a
|
||||||
|
h = lane[0] XOR rotl(lane[1], 16) XOR rotl(lane[2], 32) XOR rotl(lane[3], 48)
|
||||||
|
h = h · K
|
||||||
|
h = h XOR (h >> 32)
|
||||||
|
result = low 32 bits of h
|
||||||
|
```
|
||||||
|
|
||||||
|
Test vectors: `""` → `0xd7550ed0`, `"YAPF"` → `0xc8cc41d4`,
|
||||||
|
`"The quick brown fox jumps over the lazy dog"` → `0xbce4f410`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Version 2 layout
|
||||||
|
|
||||||
|
```
|
||||||
|
HEADER 64 bytes
|
||||||
|
SURFACE TABLE 16 bytes per surface
|
||||||
|
FRAME TABLE 4 bytes per frame, only when frames > 1
|
||||||
|
NOTES notes size bytes
|
||||||
|
SURFACE DATA in stream order: smallest mip first
|
||||||
|
```
|
||||||
|
|
||||||
|
The *front matter* is everything before the surface data:
|
||||||
|
`64 + 16·surfaces + (frames > 1 ? 4·frames : 0) + notes size` bytes.
|
||||||
|
|
||||||
|
### Header
|
||||||
|
|
||||||
|
| Offset | Size | Field | Notes |
|
||||||
|
| -----: | ---: | :---- | :---- |
|
||||||
|
| 0x00 | 4 | magic | `"YAPF"` |
|
||||||
|
| 0x04 | 1 | version | 2 |
|
||||||
|
| 0x05 | 1 | channels | 1–4 |
|
||||||
|
| 0x06 | 1 | format | block format (block payload) or GPU hint (pixel payload); see [GPU formats](#gpu-formats) |
|
||||||
|
| 0x07 | 1 | mip levels | 1–16 |
|
||||||
|
| 0x08 | 1 | flags | bit 0 premultiplied alpha, bit 1 sRGB, bit 2 coverage-preserving mips; other bits 0 |
|
||||||
|
| 0x09 | 1 | payload | 0 pixels (lossless tiles), 1 GPU blocks |
|
||||||
|
| 0x0A | 1 | sample | pixel payload: 0 u8, 1 u16, 2 half float, 3 float. Block payload: type of the decoded texels (0, or 2 for BC6H) |
|
||||||
|
| 0x0B | 1 | shape | 0 2D, 1 cube, 2 volume |
|
||||||
|
| 0x0C | 4 | width | 1–65535 |
|
||||||
|
| 0x10 | 4 | height | 1–65535; cubes: width = height |
|
||||||
|
| 0x14 | 4 | depth | volumes: 1–65535; otherwise 1 |
|
||||||
|
| 0x18 | 2 | layers | ≥ 1 (array layers; with a cube, a cube array); volumes: 1 |
|
||||||
|
| 0x1A | 2 | frames | ≥ 1 (animation frames) |
|
||||||
|
| 0x1C | 4 | needs | see [Needs](#needs) |
|
||||||
|
| 0x20 | 4 | channel map | meaning of channels 0–3, see [Channel meanings](#channel-meanings) |
|
||||||
|
| 0x24 | 1 | linear mask | bit *c*: channel *c* is linear even when the sRGB flag is set |
|
||||||
|
| 0x25 | 1 | normal | bits 0–1 convention (0 none, 1 OpenGL +Y, 2 DirectX −Y); bits 2–3 encoding (0 XYZ, 1 XY with Z reconstructed, 2 octahedral) |
|
||||||
|
| 0x26 | 1 | primaries | 0 unspecified, 1 BT.709/sRGB, 2 Display P3, 3 BT.2020, 4 ACES AP0, 5 ACEScg (AP1), 6 Adobe RGB |
|
||||||
|
| 0x27 | 1 | transfer | 0 unspecified (the sRGB flag decides), 1 sRGB, 2 linear, 3 gamma 2.2, 4 PQ (ST 2084), 5 HLG, 6 BT.1886 |
|
||||||
|
| 0x28 | 1 | mip filter | bits 0–6: 0 unknown, 1 box, 2 tent, 3 Kaiser, 4 Lanczos, 5 Mitchell, 6 authored; bit 7: sRGB channels were filtered in linear light |
|
||||||
|
| 0x29 | 1 | alpha ref | with the coverage flag: alpha-test reference 0–255 |
|
||||||
|
| 0x2A | 2 | loop count | animation repeats, 0 = forever |
|
||||||
|
| 0x2C | 4 | notes size | bytes of the notes area |
|
||||||
|
| 0x30 | 8 | file size | total bytes of the file |
|
||||||
|
| 0x38 | 4 | tables check | yapf-sum of bytes [0x40, front matter end) |
|
||||||
|
| 0x3C | 4 | header check | yapf-sum of bytes [0x00, 0x3C) |
|
||||||
|
|
||||||
|
The header check covers the tables check, so the whole front matter is
|
||||||
|
protected.
|
||||||
|
|
||||||
|
### Surfaces
|
||||||
|
|
||||||
|
A *surface* is one mip level of one face of one layer of one frame. With
|
||||||
|
`faces = 6` for cubes (order +X, −X, +Y, −Y, +Z, −Z) and 1 otherwise,
|
||||||
|
surface number
|
||||||
|
|
||||||
|
```
|
||||||
|
s = ((frame · layers + layer) · faces + face) · mip_levels + mip
|
||||||
|
```
|
||||||
|
|
||||||
|
Mip *m* is `max(1, width >> m) × max(1, height >> m)`, and for volumes
|
||||||
|
`max(1, depth >> m)` slices deep. A volume surface holds all its slices.
|
||||||
|
|
||||||
|
**Surface table entry** (16 bytes, one per surface, in surface order):
|
||||||
|
|
||||||
|
| Offset | Size | Field |
|
||||||
|
| -----: | ---: | :---- |
|
||||||
|
| +0 | 8 | offset of the surface data |
|
||||||
|
| +8 | 4 | size of the surface data |
|
||||||
|
| +12 | 4 | tile-table check: yapf-sum of the surface's book size, book and tile table |
|
||||||
|
|
||||||
|
**Stream order.** Writers store surface data for mip `mip_levels − 1`
|
||||||
|
first (all frames, layers and faces in surface order), then the next larger
|
||||||
|
mip, and so on, ending with mip 0. Readers must use the offsets and must not
|
||||||
|
rely on the order. The number of leading bytes that hold every surface of
|
||||||
|
mip *m* and smaller is the largest `offset + size` over those surfaces.
|
||||||
|
|
||||||
|
### Frame table
|
||||||
|
|
||||||
|
When `frames > 1`: one `u32` per frame, its display time in microseconds.
|
||||||
|
|
||||||
|
### Notes
|
||||||
|
|
||||||
|
A sequence of entries that exactly fills the notes area:
|
||||||
|
|
||||||
|
```
|
||||||
|
tag 4 bytes
|
||||||
|
length u32
|
||||||
|
data length bytes
|
||||||
|
```
|
||||||
|
|
||||||
|
| Tag | Content |
|
||||||
|
| :-- | :------ |
|
||||||
|
| `AUTH` | author (UTF-8) |
|
||||||
|
| `LICN` | licence (UTF-8, e.g. an SPDX id) |
|
||||||
|
| `SRCE` | source / origin (UTF-8, e.g. a URL or path) |
|
||||||
|
| `TOOL` | the tool that wrote the file (UTF-8) |
|
||||||
|
| `TITL` | title (UTF-8) |
|
||||||
|
| `DESC` | description (UTF-8) |
|
||||||
|
| `DATE` | date, ISO 8601 (UTF-8) |
|
||||||
|
| `ICCP` | an ICC colour profile |
|
||||||
|
| `ORNT` | orientation, 1 byte: bits 0–1 clockwise quarter turns, bit 2 mirror left–right after turning |
|
||||||
|
| `DPI ` | 2 × u32: horizontal and vertical dots per inch × 1000 |
|
||||||
|
| `CLL ` | 2 × u16: maximum content light level, maximum frame-average light level (cd/m²) |
|
||||||
|
|
||||||
|
Tags that begin with a lowercase letter belong to applications (for
|
||||||
|
example `keel`). Readers ignore tags they do not know. Tags may repeat;
|
||||||
|
order is kept.
|
||||||
|
|
||||||
|
### Needs
|
||||||
|
|
||||||
|
| Bit | Name | Set when |
|
||||||
|
| --: | :--- | :------- |
|
||||||
|
| 0 | WIDE | pixel payload with sample ≠ u8 |
|
||||||
|
| 1 | BLOCKS | block payload |
|
||||||
|
| 2 | ENTROPY | any surface has a code book |
|
||||||
|
| 3 | LAYOUT | shape ≠ 2D, or layers > 1 |
|
||||||
|
| 4 | FRAMES | frames > 1 |
|
||||||
|
|
||||||
|
A decoder must refuse a file that sets a needs bit it does not implement
|
||||||
|
(this library: any bit above 4). A file that uses a feature without its
|
||||||
|
bit is corrupt. Everything that is not a needs feature — channel map,
|
||||||
|
colour fields, mip filter, notes, frame times — may be ignored.
|
||||||
|
|
||||||
|
A decoder that only implements 8-bit 2D pixels can therefore read every
|
||||||
|
version 2 file whose needs are 0, and cleanly refuse the rest.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Surface data
|
||||||
|
|
||||||
|
```
|
||||||
|
book size u32 0 when the surface has no code book
|
||||||
|
book book size bytes
|
||||||
|
tile table 8 bytes per tile: u32 size (bit 31: packed), u32 yapf-sum of the tile bytes
|
||||||
|
tiles back to back; their sizes must add up exactly to the rest of the surface
|
||||||
|
```
|
||||||
|
|
||||||
|
Tiles are numbered slice by slice, rows top to bottom, tiles left to right.
|
||||||
|
For pixels the grid is `ceil(w / 64) × ceil(h / 64)` tiles per slice (edge
|
||||||
|
tiles are smaller, no padding); for blocks it is
|
||||||
|
`ceil(bx / 16) × ceil(by / 16)` tiles of 16 × 16 blocks, where
|
||||||
|
`bx = ceil(w / block width)` and `by = ceil(h / block height)`.
|
||||||
|
|
||||||
|
Bit 31 of a tile size (*packed*) is allowed only in block tiles of a
|
||||||
|
surface with a code book.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Pixel tiles
|
||||||
|
|
||||||
|
Let *B* be the bytes per sample (1, 2 or 4), *ch* the channels, and the
|
||||||
|
tile `tw × th` pixels.
|
||||||
|
|
||||||
|
### Tile header
|
||||||
|
|
||||||
|
```
|
||||||
|
byte 0 0x80 stored: the raw samples follow, interleaved in
|
||||||
|
channel order, each B bytes little-endian;
|
||||||
|
exactly tw·th·ch·B bytes
|
||||||
|
otherwise coded:
|
||||||
|
bits 0–3 B = 1: bit c set = plane c is constant; B > 1: 0
|
||||||
|
bit 4 colour transform subtract-green
|
||||||
|
bit 5 an entropy mask follows
|
||||||
|
bit 6 no colour transform
|
||||||
|
bit 7 0
|
||||||
|
bits 4 and 6 are exclusive and need ch >= 3; with neither
|
||||||
|
set, an RGB(A) tile uses YCoCg-R
|
||||||
|
B > 1 u16 constant mask, bit p = plane p is constant
|
||||||
|
bit 5 entropy mask: u8 (B = 1) or u16 (B > 1); bit p = plane p is
|
||||||
|
entropy coded. It must not include constant planes.
|
||||||
|
then one value byte per constant plane, in plane order;
|
||||||
|
one payload per other plane, in plane order;
|
||||||
|
the tile ends exactly where its last payload ends
|
||||||
|
```
|
||||||
|
|
||||||
|
With B = 1 and neither bit 5 nor bit 6 set, a coded tile is exactly a
|
||||||
|
version 1 tile.
|
||||||
|
|
||||||
|
### Planes
|
||||||
|
|
||||||
|
The colour transform turns R, G, B (+ A) into transformed channels; it is
|
||||||
|
skipped for 1–2 channels and with bit 6.
|
||||||
|
|
||||||
|
* **YCoCg-R** at the sample width *n* = 8·B bits, all arithmetic mod 2ⁿ,
|
||||||
|
`>>` an arithmetic shift of the *n*-bit two's complement value, `h = 2ⁿ⁻¹`:
|
||||||
|
|
||||||
|
```
|
||||||
|
forward: o = R − B; t = B + (o >> 1); q = G − t;
|
||||||
|
Y = t + (q >> 1); Co = o + h; Cg = q + h
|
||||||
|
inverse: o = Co − h; q = Cg − h; t = Y − (q >> 1);
|
||||||
|
G = q + t; B = t − (o >> 1); R = B + o
|
||||||
|
```
|
||||||
|
* **subtract-green**: `G, R − G + h, B − G + h` (+ A), mod 2ⁿ.
|
||||||
|
|
||||||
|
Before the transform, float samples (half or float) are mapped to
|
||||||
|
integers that keep their order: if the sign bit is set every bit is
|
||||||
|
flipped, otherwise the sign bit is set. The decoder undoes this after the
|
||||||
|
inverse transform.
|
||||||
|
|
||||||
|
Transformed channel *c* gives B planes: plane `c·B + k` holds byte
|
||||||
|
`B − 1 − k` of the value (the most significant byte first). For B = 1
|
||||||
|
the planes are the transformed channels.
|
||||||
|
|
||||||
|
### Plane payloads
|
||||||
|
|
||||||
|
A plane payload has one of two forms, chosen per plane by the entropy mask.
|
||||||
|
|
||||||
|
**Group form** — exactly the version 1 plane payload: a filter byte per
|
||||||
|
band of 8 rows, 4-bit control codes, and groups of 8 residuals at the
|
||||||
|
group's bit width. See `yapf.h`.
|
||||||
|
|
||||||
|
**Entropy form**
|
||||||
|
|
||||||
|
```
|
||||||
|
filter[ceil(th / 8)] as in the group form
|
||||||
|
u16 L bytes of the bit stream
|
||||||
|
L bytes the folded residuals of the plane in raster order
|
||||||
|
```
|
||||||
|
|
||||||
|
Each residual is coded with table `(slot, context)` of the surface's code
|
||||||
|
book:
|
||||||
|
|
||||||
|
* `slot = t · 16 + p`, where *p* is the plane and *t* is 1 with
|
||||||
|
subtract-green, 2 with no transform, else 0;
|
||||||
|
* `context = min(6, floor(log2(s + 1)))`, `s = left + above`, the folded
|
||||||
|
residuals of the left and upper neighbours in this plane (0 outside the
|
||||||
|
tile) — so 0, 1–2, 3–6, 7–14, 15–30, 31–62, 63 and up.
|
||||||
|
|
||||||
|
The stream must end in its last byte. Folded residuals then go through
|
||||||
|
the filters exactly as in the group form.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Block tiles
|
||||||
|
|
||||||
|
A block tile of `tbw × tbh` blocks of *S* bytes is either
|
||||||
|
|
||||||
|
* **raw**: the blocks, rows top to bottom, exactly `tbw·tbh·S` bytes — as
|
||||||
|
the GPU takes them; or
|
||||||
|
* **packed** (bit 31 of its size): one bit stream holding byte 0 of every
|
||||||
|
block (in raster order), then byte 1 of every block, …, byte S − 1.
|
||||||
|
Byte lane *k* is coded with table `(slot k, context 0)` of the surface's
|
||||||
|
code book. The stream must end in the last byte of the tile.
|
||||||
|
|
||||||
|
Decoded block surfaces are row-major blocks, slice after slice, ready for
|
||||||
|
`glCompressedTexImage*` / `vkCmdCopyBufferToImage`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Code book
|
||||||
|
|
||||||
|
```
|
||||||
|
u16 count number of tables, 1–336
|
||||||
|
count × {
|
||||||
|
u8 slot 0–47
|
||||||
|
u8 context 0–6
|
||||||
|
code table
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Each `(slot, context)` pair appears at most once. The book must fill its
|
||||||
|
size exactly.
|
||||||
|
|
||||||
|
**Code table**: `u8 n − 1`, then *n* 4-bit code lengths (symbols
|
||||||
|
0 … n − 1, low nibble first, an odd count padded with a zero nibble).
|
||||||
|
Length 0 = no code; otherwise 1–11. At least one code; Kraft sum ≤ 1.
|
||||||
|
Codes are canonical — by length, then by symbol, the first code of each
|
||||||
|
length one more than the last code of the previous length, shifted left —
|
||||||
|
and stored least significant bit first (the first bit of a code is the
|
||||||
|
lowest unread bit of the stream), so a decoder can look up 11 bits at a
|
||||||
|
time.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## GPU formats
|
||||||
|
|
||||||
|
Pixel payload hints (`format` field):
|
||||||
|
|
||||||
|
| Value | Name | Vulkan |
|
||||||
|
| ----: | :--- | :----- |
|
||||||
|
| 0x00 | RGBA8 | R8G8B8A8_UNORM |
|
||||||
|
| 0x01 | RGB8 | R8G8B8_UNORM |
|
||||||
|
| 0x02 | RG8 | R8G8_UNORM |
|
||||||
|
| 0x03 | R8 | R8_UNORM |
|
||||||
|
| 0x04 | SRGB8_A8 | R8G8B8A8_SRGB |
|
||||||
|
| 0x05 | SRGB8 | R8G8B8_SRGB |
|
||||||
|
| 0x06–0x09 | RGBA16, RGB16, RG16, R16 | R16G16B16A16_UNORM … R16_UNORM |
|
||||||
|
| 0x0A–0x0D | RGBA16F, RGB16F, RG16F, R16F | R16G16B16A16_SFLOAT … R16_SFLOAT |
|
||||||
|
| 0x0E–0x11 | RGBA32F, RGB32F, RG32F, R32F | R32G32B32A32_SFLOAT … R32_SFLOAT |
|
||||||
|
|
||||||
|
Block formats (block payload). With the sRGB flag, use the `_SRGB`
|
||||||
|
variant where one exists.
|
||||||
|
|
||||||
|
| Value | Name | Block | Bytes | Vulkan / DXGI |
|
||||||
|
| ----: | :--- | :---: | ----: | :------------ |
|
||||||
|
| 0x40 | BC1 | 4×4 | 8 | BC1_RGB(A)_UNORM / BC1_UNORM |
|
||||||
|
| 0x41 | BC2 | 4×4 | 16 | BC2_UNORM |
|
||||||
|
| 0x42 | BC3 | 4×4 | 16 | BC3_UNORM |
|
||||||
|
| 0x43 | BC4 | 4×4 | 8 | BC4_UNORM |
|
||||||
|
| 0x44 | BC4_SNORM | 4×4 | 8 | BC4_SNORM |
|
||||||
|
| 0x45 | BC5 | 4×4 | 16 | BC5_UNORM |
|
||||||
|
| 0x46 | BC5_SNORM | 4×4 | 16 | BC5_SNORM |
|
||||||
|
| 0x47 | BC6H | 4×4 | 16 | BC6H_UFLOAT / BC6H_UF16 |
|
||||||
|
| 0x48 | BC6H_SF | 4×4 | 16 | BC6H_SFLOAT / BC6H_SF16 |
|
||||||
|
| 0x49 | BC7 | 4×4 | 16 | BC7_UNORM |
|
||||||
|
| 0x50 | ETC2_RGB8 | 4×4 | 8 | ETC2_R8G8B8_UNORM |
|
||||||
|
| 0x51 | ETC2_RGB8A1 | 4×4 | 8 | ETC2_R8G8B8A1_UNORM |
|
||||||
|
| 0x52 | ETC2_RGBA8 | 4×4 | 16 | ETC2_R8G8B8A8_UNORM |
|
||||||
|
| 0x53 | EAC_R11 | 4×4 | 8 | EAC_R11_UNORM |
|
||||||
|
| 0x54 | EAC_R11_SNORM | 4×4 | 8 | EAC_R11_SNORM |
|
||||||
|
| 0x55 | EAC_RG11 | 4×4 | 16 | EAC_R11G11_UNORM |
|
||||||
|
| 0x56 | EAC_RG11_SNORM | 4×4 | 16 | EAC_R11G11_SNORM |
|
||||||
|
| 0x60–0x6D | ASTC 4×4, 5×4, 5×5, 6×5, 6×6, 8×5, 8×6, 8×8, 10×5, 10×6, 10×8, 10×10, 12×10, 12×12 | as named | 16 | ASTC_*_UNORM (HDR: transfer linear, sample half) |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Channel meanings
|
||||||
|
|
||||||
|
| Value | Meaning | Value | Meaning |
|
||||||
|
| ----: | :------ | ----: | :------ |
|
||||||
|
| 0 | default for the channel count (Y / YA / RGB / RGBA) | 11 | metalness |
|
||||||
|
| 1 | red | 12 | ambient occlusion |
|
||||||
|
| 2 | green | 13 | height |
|
||||||
|
| 3 | blue | 14 | specular |
|
||||||
|
| 4 | alpha | 15 | emissive |
|
||||||
|
| 5 | luma | 16 | opacity (alpha-test mask) |
|
||||||
|
| 6 | normal X | 17 | mask |
|
||||||
|
| 7 | normal Y | 18 | cavity |
|
||||||
|
| 8 | normal Z | 19 | thickness |
|
||||||
|
| 9 | roughness | 20 | id |
|
||||||
|
| 10 | gloss (1 − roughness) | 255 | unused |
|
||||||
|
|
||||||
|
Alpha (an explicit 4 or 16, or the last channel of a 2- or 4-channel image
|
||||||
|
with default meanings) is always linear. The linear mask marks further
|
||||||
|
channels that are linear inside an sRGB image, such as masks packed next to
|
||||||
|
colour.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Mips
|
||||||
|
|
||||||
|
`mip filter` records how the mips were made. This library's
|
||||||
|
`yapf_tex_build_mips` uses a 2 × 2 box (2 × 2 × 2 for volumes, edges
|
||||||
|
repeat), averages sRGB 8-bit channels in linear light (bit 7), and with an
|
||||||
|
alpha reference rescales alpha on every mip so that the share of texels
|
||||||
|
with alpha *above* the reference stays that of mip 0 (coverage flag,
|
||||||
|
`alpha ref`). Alpha-tested foliage and fences then keep their density at
|
||||||
|
a distance instead of thinning out.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Validation summary
|
||||||
|
|
||||||
|
A decoder rejects a file when:
|
||||||
|
|
||||||
|
* the magic, version, a size, shape or count is out of range, or a cube is
|
||||||
|
not square;
|
||||||
|
* the header or tables check does not match (unless the reader was told to
|
||||||
|
trust the file);
|
||||||
|
* a needs bit is unknown (*unsupported*), or a feature is used without its
|
||||||
|
needs bit (*corrupt*);
|
||||||
|
* a surface lies outside the file, its book is malformed or present without
|
||||||
|
ENTROPY, or its tiles do not fill it exactly;
|
||||||
|
* a tile's yapf-sum does not match, or the tile does not decode to exactly
|
||||||
|
its size (reserved bits, invalid filters, codes, masks or lengths).
|
||||||
|
|
||||||
|
Because every structure is validated before use, corrupt or hostile input
|
||||||
|
is rejected, never read out of bounds.
|
||||||
153
docs/LANGUAGES.md
Normal file
153
docs/LANGUAGES.md
Normal file
|
|
@ -0,0 +1,153 @@
|
||||||
|
# Using YAPF from your language
|
||||||
|
|
||||||
|
YAPF is a small C library with six functions, so every language that can
|
||||||
|
call C can use it. Ready-made bindings, each with a runnable example:
|
||||||
|
|
||||||
|
| Language | Folder | How it works | Tested | Decode¹ |
|
||||||
|
| :------- | :----- | :----------- | :----- | ------: |
|
||||||
|
| **C** | [`yapf.h`](../yapf.h), [`examples/c`](../examples/c) | the library itself | ✅ | 0.26 ms |
|
||||||
|
| **C++** | [`bindings/cpp`](../bindings/cpp) | header-only RAII wrapper (C++17) | ✅ g++ | 0.25 ms |
|
||||||
|
| **Python** | [`bindings/python`](../bindings/python) | ctypes; numpy fallback without the library | ✅ CPython 3.13 | 0.23 ms |
|
||||||
|
| **C# / .NET** | [`bindings/csharp`](../bindings/csharp) | P/Invoke, single file | ✅ .NET 8 | 0.30 ms |
|
||||||
|
| **Rust** | [`bindings/rust`](../bindings/rust) | crate, builds the C code with `cc` | ✅ Rust 1.95 | 0.28 ms |
|
||||||
|
| **Go** | [`bindings/go`](../bindings/go) | cgo package | ✅ Go 1.26 | 0.31 ms |
|
||||||
|
| **Java / Kotlin** | [`bindings/java`](../bindings/java) | Foreign Function & Memory API (Java 22+) | ✅ JDK 25 | 0.42 ms |
|
||||||
|
| **Zig** | [`bindings/zig`](../bindings/zig) | `@cImport` of `yapf.h` | ✅ Zig 0.16 | 0.28 ms |
|
||||||
|
| **Lua** | [`bindings/lua`](../bindings/lua) | LuaJIT FFI (also LÖVE) | ✅ LuaJIT 2.1 | 0.28 ms |
|
||||||
|
| **JavaScript / TypeScript** | [`bindings/js`](../bindings/js) | pure JS port, `.d.ts` types | ✅ Node 24, Chromium | 5.9 ms |
|
||||||
|
|
||||||
|
¹ Decoding the 512×512 RGBA YAPF logo (`other/YAPF.YAPF`), best of 10 after
|
||||||
|
a warm-up, on an AMD Ryzen 7 5700G. Every binding except JavaScript runs
|
||||||
|
the same native code, so they are all about equally fast; the JavaScript
|
||||||
|
port needs no native library at all. Every example also re-encodes the
|
||||||
|
image and checks that the result is byte-identical to the original file.
|
||||||
|
|
||||||
|
**YAPF 2 textures.** Most bindings wrap the version 1 API (`yapf_load`,
|
||||||
|
`yapf_save`, …). Through it they also read 8-bit version 2 files (2D, or
|
||||||
|
face 0 / layer 0 of cubes and arrays). GPU blocks, 16-bit / float pixels,
|
||||||
|
volumes and the other version 2 features are available from C and C++
|
||||||
|
through the `yapf_tex_*` functions in [`yapf.h`](../yapf.h) (see
|
||||||
|
[FORMAT.md](FORMAT.md)); any language with a C FFI can call them the same
|
||||||
|
way. Python (`yapf.load_texture`) and JavaScript (`YAPF.decodeTexture`)
|
||||||
|
read every version 2 texture; the JavaScript port writes version 1.
|
||||||
|
|
||||||
|
## Getting the native library
|
||||||
|
|
||||||
|
Bindings other than C, C++, Rust, Go, Zig and JavaScript load the shared
|
||||||
|
library at run time:
|
||||||
|
|
||||||
|
| System | File | Get it |
|
||||||
|
| :------ | :-------------- | :----- |
|
||||||
|
| Windows | `yapf.dll` | [Releases](https://github.com/Nexoniarz/YAPF/releases) or `build.bat` |
|
||||||
|
| Linux | `libyapf.so` | Releases, `make`, or `sudo make install` |
|
||||||
|
| macOS | `libyapf.dylib` | Releases or `make` |
|
||||||
|
|
||||||
|
Put it next to your program, on the library path (`PATH`,
|
||||||
|
`LD_LIBRARY_PATH`, `DYLD_LIBRARY_PATH`), or pass its path explicitly
|
||||||
|
(`YAPF_LIBRARY` for Python and Lua, `-Dyapf.library=` for Java).
|
||||||
|
|
||||||
|
## Quick look
|
||||||
|
|
||||||
|
**C**
|
||||||
|
```c
|
||||||
|
yapf_image_t *img = yapf_load("texture.yapf");
|
||||||
|
/* img->width, img->height, img->channels, img->pixels */
|
||||||
|
yapf_free(img);
|
||||||
|
```
|
||||||
|
|
||||||
|
**C++**
|
||||||
|
```cpp
|
||||||
|
auto img = yapf::Image::load("texture.yapf");
|
||||||
|
auto bytes = img.encode();
|
||||||
|
```
|
||||||
|
|
||||||
|
**Python**
|
||||||
|
```python
|
||||||
|
img = yapf.load("texture.yapf")
|
||||||
|
yapf.save("copy.yapf", img)
|
||||||
|
```
|
||||||
|
|
||||||
|
**C#**
|
||||||
|
```csharp
|
||||||
|
var img = YapfImage.Load("texture.yapf");
|
||||||
|
File.WriteAllBytes("copy.yapf", img.Encode());
|
||||||
|
```
|
||||||
|
|
||||||
|
**Rust**
|
||||||
|
```rust
|
||||||
|
let img = yapf::Image::load("texture.yapf")?;
|
||||||
|
img.save("copy.yapf")?;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Go**
|
||||||
|
```go
|
||||||
|
img, err := yapf.Load("texture.yapf")
|
||||||
|
data, err := yapf.Encode(img)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Java**
|
||||||
|
```java
|
||||||
|
Yapf.Image img = Yapf.load(Path.of("texture.yapf"));
|
||||||
|
Yapf.save(Path.of("copy.yapf"), img);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Zig**
|
||||||
|
```zig
|
||||||
|
const img = c.yapf_load("texture.yapf") orelse return error.CannotLoad;
|
||||||
|
defer c.yapf_free(img);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Lua**
|
||||||
|
```lua
|
||||||
|
local img = yapf.load("texture.yapf")
|
||||||
|
yapf.save("copy.yapf", img)
|
||||||
|
```
|
||||||
|
|
||||||
|
**JavaScript**
|
||||||
|
```js
|
||||||
|
const img = YAPF.decode(bytes);
|
||||||
|
const file = YAPF.encode(img);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Any other language
|
||||||
|
|
||||||
|
Swift, Kotlin/Native, Dart, PHP, Ruby, Julia, Nim, D, Odin, Haskell, … —
|
||||||
|
anything with a C foreign-function interface can call the library
|
||||||
|
directly. These are the whole API:
|
||||||
|
|
||||||
|
```c
|
||||||
|
typedef struct {
|
||||||
|
uint32_t width, height;
|
||||||
|
uint8_t channels; /* 1 gray, 2 gray+alpha, 3 RGB, 4 RGBA */
|
||||||
|
uint8_t gpu_format; /* hint, see yapf.h */
|
||||||
|
uint8_t flags; /* 1 premultiplied alpha, 2 sRGB */
|
||||||
|
uint8_t mip_levels;
|
||||||
|
uint8_t *pixels; /* width*height*channels bytes, rows top to bottom */
|
||||||
|
uint8_t **mips; /* mips[0] == pixels */
|
||||||
|
} yapf_image_t; /* 32 bytes on 64-bit systems: pixels at offset 16, mips at 24 */
|
||||||
|
|
||||||
|
yapf_image_t *yapf_load_memory_mt(const void *data, size_t size, int threads); /* threads: 1 or 0 = all cores */
|
||||||
|
void yapf_free(yapf_image_t *img);
|
||||||
|
int yapf_encode(const yapf_image_t *img, void **out_data, size_t *out_size);
|
||||||
|
void yapf_free_buffer(void *data);
|
||||||
|
yapf_image_t *yapf_load(const char *path);
|
||||||
|
int yapf_save(const char *path, const yapf_image_t *img);
|
||||||
|
```
|
||||||
|
|
||||||
|
Returned images and buffers must be released with `yapf_free` /
|
||||||
|
`yapf_free_buffer` from the same library. Ready-made bindings for these
|
||||||
|
languages are not included yet; the snippets below show the starting point
|
||||||
|
and are **not tested**.
|
||||||
|
|
||||||
|
**Swift** — add a module map for `yapf.h` (or a bridging header) and compile
|
||||||
|
`yapf.c` in the target; then `let img = yapf_load(path)` works as is.
|
||||||
|
|
||||||
|
**Dart** — `dart:ffi`: `DynamicLibrary.open('libyapf.so')` and
|
||||||
|
`lookupFunction<Pointer<YapfImage> Function(Pointer<Uint8>, IntPtr, Int32), …>('yapf_load_memory_mt')`.
|
||||||
|
|
||||||
|
**PHP 7.4+** — `$ffi = FFI::cdef(file_get_contents('yapf_api.h'), 'libyapf.so');`
|
||||||
|
with the declarations above in `yapf_api.h`.
|
||||||
|
|
||||||
|
**Ruby** — `Fiddle::Function.new(lib['yapf_load'], [Fiddle::TYPE_VOIDP], Fiddle::TYPE_VOIDP)`.
|
||||||
|
|
||||||
|
**Julia** — `ccall((:yapf_load, "libyapf"), Ptr{Cvoid}, (Cstring,), path)`.
|
||||||
62
examples/README.md
Normal file
62
examples/README.md
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
# Examples: how fast, how small
|
||||||
|
|
||||||
|
## Try it on your own images
|
||||||
|
|
||||||
|
```sh
|
||||||
|
make # builds build/yapf
|
||||||
|
build/yapf compare photo.png ui.png # any PNG / JPG / TGA / BMP
|
||||||
|
```
|
||||||
|
|
||||||
|
`yapf compare` encodes each image to YAPF in memory, checks that it decodes
|
||||||
|
back to exactly the same pixels, and times decoding the original format
|
||||||
|
against YAPF on one core and on all cores:
|
||||||
|
|
||||||
|
```
|
||||||
|
ui_reverb.png (261 x 430, RGBA)
|
||||||
|
raw 438.4 KB
|
||||||
|
PNG 176.6 KB 40.3% of raw decode 2.049 ms
|
||||||
|
YAPF 143.7 KB 32.8% of raw decode 0.170 ms 12.1x faster, 81% of the PNG size
|
||||||
|
YAPF on all cores: decode 0.176 ms (2.5 GB/s), encode 8 ms, lossless: yes
|
||||||
|
```
|
||||||
|
|
||||||
|
## Results on a mixed set of images
|
||||||
|
|
||||||
|
AMD Ryzen 7 5700G, `make` (gcc -O2), PNG decoded with stb_image. The test
|
||||||
|
images (a 4K wallpaper, UI screenshots, an app icon and seamless textures
|
||||||
|
from open-source projects) are not redistributed here; run `yapf compare`
|
||||||
|
on your own files to reproduce.
|
||||||
|
|
||||||
|
| Image | PNG | YAPF | YAPF / PNG size | PNG decode | YAPF decode | Faster |
|
||||||
|
| :---- | --: | ---: | --------------: | ---------: | ----------: | -----: |
|
||||||
|
| UI screenshot 261×430 | 176.6 KB | 143.7 KB | **81 %** | 2.05 ms | 0.17 ms | **12×** |
|
||||||
|
| UI screenshot 305×529 | 201.5 KB | 185.0 KB | **92 %** | 2.55 ms | 0.24 ms | **11×** |
|
||||||
|
| App icon 128×128 | 24.9 KB | 23.1 KB | **93 %** | 0.30 ms | 0.026 ms | **11×** |
|
||||||
|
| Wallpaper 3840×2160 | 1.77 MB | 2.21 MB | 125 % | 73.7 ms | 6.8 ms | **11×** |
|
||||||
|
| Rock texture 512×512 | 182.6 KB | 221.8 KB | 121 % | 1.64 ms | 0.17 ms | **10×** |
|
||||||
|
| Wood texture 512×512 | 185.8 KB | 234.1 KB | 126 % | 1.64 ms | 0.17 ms | **10×** |
|
||||||
|
| Canvas texture 512×512 | 171.5 KB | 245.6 KB | 143 % | 1.36 ms | 0.16 ms | **9×** |
|
||||||
|
| YAPF logo 512×512 | 60.5 KB | 89.3 KB | 148 % | 1.25 ms | 0.25 ms | **5×** |
|
||||||
|
|
||||||
|
In short: **smaller than PNG on UI-style images, 20–50 % larger on photos
|
||||||
|
and textures, and 5–12× faster to decode** — always lossless. The decoder
|
||||||
|
reaches 5–6 GB/s on large images on one core, and more with
|
||||||
|
`yapf_load_mt`.
|
||||||
|
|
||||||
|
## Code examples
|
||||||
|
|
||||||
|
| Language | Example |
|
||||||
|
| :------- | :------ |
|
||||||
|
| C | [`examples/c/example.c`](c/example.c) |
|
||||||
|
| C++ | [`bindings/cpp/example.cpp`](../bindings/cpp/example.cpp) |
|
||||||
|
| Python | [`bindings/python/example.py`](../bindings/python/example.py) |
|
||||||
|
| C# | [`bindings/csharp/Example`](../bindings/csharp/Example) |
|
||||||
|
| Rust | [`bindings/rust/examples/demo.rs`](../bindings/rust/examples/demo.rs) |
|
||||||
|
| Go | [`bindings/go/example`](../bindings/go/example) |
|
||||||
|
| Java | [`bindings/java/Example.java`](../bindings/java/Example.java) |
|
||||||
|
| Zig | [`bindings/zig/example.zig`](../bindings/zig/example.zig) |
|
||||||
|
| Lua | [`bindings/lua/example.lua`](../bindings/lua/example.lua) |
|
||||||
|
| JavaScript | [`bindings/js/example.mjs`](../bindings/js/example.mjs) |
|
||||||
|
|
||||||
|
Each one loads a `.yapf` file, prints its size and decode time, re-encodes
|
||||||
|
it (byte-identical to the file) and writes a new image from its own pixels.
|
||||||
|
See [docs/LANGUAGES.md](../docs/LANGUAGES.md) for setup in each language.
|
||||||
61
examples/c/example.c
Normal file
61
examples/c/example.c
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
/*
|
||||||
|
* example.c — load, inspect, time and write YAPF images from C.
|
||||||
|
*
|
||||||
|
* cc -O2 -std=c99 -I../.. example.c ../../yapf.c -o example -pthread
|
||||||
|
* ./example ../../other/YAPF.YAPF
|
||||||
|
*/
|
||||||
|
#include "yapf.h"
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <time.h>
|
||||||
|
|
||||||
|
static double seconds(void) { return (double)clock() / CLOCKS_PER_SEC; }
|
||||||
|
|
||||||
|
int main(int argc, char **argv) {
|
||||||
|
const char *path = argc > 1 ? argv[1] : "../../other/YAPF.YAPF";
|
||||||
|
|
||||||
|
/* 1. Load a file (yapf_load_memory works the same on a buffer). */
|
||||||
|
yapf_image_t *img = yapf_load(path);
|
||||||
|
if (!img) { fprintf(stderr, "cannot load %s\n", path); return 1; }
|
||||||
|
double t = 1e9; /* decode time: best of 10 */
|
||||||
|
for (int i = 0; i < 10; i++) {
|
||||||
|
double t0 = seconds();
|
||||||
|
yapf_free(yapf_load(path));
|
||||||
|
t0 = seconds() - t0;
|
||||||
|
if (t0 < t) t = t0;
|
||||||
|
}
|
||||||
|
printf("%s: %u x %u, %u channels, %u mip level(s), decoded in %.2f ms\n",
|
||||||
|
path, img->width, img->height, img->channels, img->mip_levels, t * 1e3);
|
||||||
|
|
||||||
|
/* 2. Pixels: width * height * channels bytes, rows top to bottom. */
|
||||||
|
const uint8_t *p = img->pixels;
|
||||||
|
printf("top-left pixel: %u %u %u %u\n", p[0], p[1], p[2], img->channels == 4 ? p[3] : 255);
|
||||||
|
|
||||||
|
/* 3. Encode to memory and to a file. */
|
||||||
|
void *data = NULL;
|
||||||
|
size_t size = 0;
|
||||||
|
if (yapf_encode(img, &data, &size) == YAPF_OK) {
|
||||||
|
printf("re-encoded: %zu bytes (%.1f%% of raw)\n", size,
|
||||||
|
100.0 * size / ((double)img->width * img->height * img->channels));
|
||||||
|
yapf_free_buffer(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 4. Create an image from your own pixels. */
|
||||||
|
enum { W = 256, H = 256 };
|
||||||
|
static uint8_t rgba[W * H * 4];
|
||||||
|
for (int y = 0; y < H; y++)
|
||||||
|
for (int x = 0; x < W; x++) {
|
||||||
|
uint8_t *q = rgba + (y * W + x) * 4;
|
||||||
|
q[0] = (uint8_t)x; q[1] = (uint8_t)y; q[2] = 128; q[3] = 255;
|
||||||
|
}
|
||||||
|
yapf_image_t mine;
|
||||||
|
memset(&mine, 0, sizeof(mine));
|
||||||
|
mine.width = W; mine.height = H; mine.channels = 4; mine.mip_levels = 1;
|
||||||
|
mine.pixels = rgba; mine.flags = YAPF_FLAG_SRGB; mine.gpu_format = YAPF_GPU_SRGB8_A8;
|
||||||
|
if (yapf_save("gradient.yapf", &mine) == YAPF_OK) printf("wrote gradient.yapf\n");
|
||||||
|
|
||||||
|
yapf_free(img);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
44
extensions/blender/README.md
Normal file
44
extensions/blender/README.md
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
# YAPF for Blender
|
||||||
|
|
||||||
|
Import and export `.yapf` images in **Blender 4.2 or later** (tested with
|
||||||
|
Blender 5.1).
|
||||||
|
|
||||||
|
- **File → Import → YAPF Image (.yapf)** — optionally as a **plane** with a
|
||||||
|
material that shows the image.
|
||||||
|
- **Image Editor → Image → Open YAPF… / Save as YAPF…** (with mipmaps option).
|
||||||
|
- **Drag and drop** `.yapf` files into the 3D Viewport (creates a plane), the
|
||||||
|
Image Editor or a node editor.
|
||||||
|
|
||||||
|
Blender cannot read `.yapf` on its own, so imported images are **packed into
|
||||||
|
the .blend** and survive saving and reopening.
|
||||||
|
|
||||||
|
Every YAPF file opens, version 1 and 2: 8-bit images; GPU blocks (BC1–BC7,
|
||||||
|
BC6H) decoded; 16-bit, half and float textures as **float images** (HDR
|
||||||
|
values above 1 are kept, packed as EXR); for cubes, arrays and animations
|
||||||
|
the first image. Normal maps follow Blender's OpenGL convention: DirectX
|
||||||
|
normals get their green flipped, and X/Y-only normals (such as BC5) get Z
|
||||||
|
rebuilt. Version 2 files need the bundled native library; the pure-Python
|
||||||
|
fallback reads version 1 only. Tested in Blender 4.4.3 (nixpkgs).
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
1. Get `yapf_image-blender.zip` from the
|
||||||
|
[Releases](https://github.com/Nexoniarz/YAPF/releases), or build it (below).
|
||||||
|
2. Blender → **Edit → Preferences → Get Extensions → ⌄ → Install from Disk…**
|
||||||
|
and pick the zip.
|
||||||
|
|
||||||
|
The zip contains the native YAPF library for Windows, macOS and Linux (x64
|
||||||
|
and ARM64). Tested in Blender 5.1 on Linux; the Windows library is tested
|
||||||
|
outside Blender, and the macOS libraries are cross-compiled and **not yet run
|
||||||
|
on a Mac**. If no library matches your system, the extension still
|
||||||
|
**imports** using a pure-Python decoder (numpy, bundled with Blender), but
|
||||||
|
cannot export.
|
||||||
|
|
||||||
|
## Build the zip
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python3 extensions/blender/build.py # library for this machine only
|
||||||
|
tools/build_release.sh # all platforms (needs zig)
|
||||||
|
```
|
||||||
|
|
||||||
|
Both write `dist/yapf_image-blender.zip`.
|
||||||
59
extensions/blender/build.py
Normal file
59
extensions/blender/build.py
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Package the Blender extension: python3 extensions/blender/build.py [libs_dir]
|
||||||
|
|
||||||
|
Writes dist/yapf_image-blender.zip, ready for Blender's
|
||||||
|
Edit > Preferences > Get Extensions > Install from Disk.
|
||||||
|
|
||||||
|
libs_dir (optional) holds prebuilt libraries as <platform>/<file>, e.g.
|
||||||
|
linux-x64/libyapf.so, windows-x64/yapf.dll, macos-arm64/libyapf.dylib
|
||||||
|
(tools/build_libs.sh makes them). Without libraries the extension still
|
||||||
|
imports .yapf files using numpy, but cannot export. If no libs_dir is
|
||||||
|
given, a library for this machine is built with `cc` when available.
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import zipfile
|
||||||
|
|
||||||
|
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
|
||||||
|
SRC = os.path.join(ROOT, "extensions", "blender", "yapf_image")
|
||||||
|
sys.path.insert(0, os.path.join(ROOT, "bindings", "python"))
|
||||||
|
import yapf # noqa: E402 (for the platform folder name)
|
||||||
|
|
||||||
|
|
||||||
|
def host_lib(tmp):
|
||||||
|
name = {"win32": "yapf.dll", "darwin": "libyapf.dylib"}.get(sys.platform, "libyapf.so")
|
||||||
|
out = os.path.join(tmp, yapf._platform_dir(), name)
|
||||||
|
os.makedirs(os.path.dirname(out), exist_ok=True)
|
||||||
|
cc = shutil.which("cc") or shutil.which("gcc") or shutil.which("clang")
|
||||||
|
if not cc:
|
||||||
|
return None
|
||||||
|
cmd = [cc, "-O2", "-std=c99", "-shared", "-fPIC", os.path.join(ROOT, "yapf.c"), "-o", out, "-pthread"]
|
||||||
|
return tmp if subprocess.call(cmd) == 0 else None
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
tmp = tempfile.mkdtemp()
|
||||||
|
libs = sys.argv[1] if len(sys.argv) > 1 else host_lib(tmp)
|
||||||
|
os.makedirs(os.path.join(ROOT, "dist"), exist_ok=True)
|
||||||
|
out = os.path.join(ROOT, "dist", "yapf_image-blender.zip")
|
||||||
|
with zipfile.ZipFile(out, "w", zipfile.ZIP_DEFLATED) as z:
|
||||||
|
for f in ("__init__.py", "blender_manifest.toml"):
|
||||||
|
z.write(os.path.join(SRC, f), f)
|
||||||
|
z.write(os.path.join(ROOT, "bindings", "python", "yapf.py"), "yapf.py")
|
||||||
|
z.write(os.path.join(ROOT, "LICENSE"), "LICENSE")
|
||||||
|
if libs:
|
||||||
|
for plat in sorted(os.listdir(libs)):
|
||||||
|
for f in sorted(os.listdir(os.path.join(libs, plat))):
|
||||||
|
if f.endswith((".so", ".dll", ".dylib")):
|
||||||
|
z.write(os.path.join(libs, plat, f), "lib/%s/%s" % (plat, f))
|
||||||
|
print(" bundled lib/%s/%s" % (plat, f))
|
||||||
|
shutil.rmtree(tmp, ignore_errors=True)
|
||||||
|
print("wrote", out)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
282
extensions/blender/yapf_image/__init__.py
Normal file
282
extensions/blender/yapf_image/__init__.py
Normal file
|
|
@ -0,0 +1,282 @@
|
||||||
|
# YAPF image support for Blender.
|
||||||
|
#
|
||||||
|
# Copyright 2026 Nexoniarz — Apache License 2.0.
|
||||||
|
#
|
||||||
|
# File > Import > YAPF Image (.yapf) open an image, optionally as a plane
|
||||||
|
# Image Editor > Image > Open YAPF… open into the Image Editor
|
||||||
|
# Image Editor > Image > Save as YAPF… export the current image
|
||||||
|
# Drag a .yapf file into the 3D Viewport, Image Editor or a node editor.
|
||||||
|
#
|
||||||
|
# Blender cannot read .yapf files by itself, so imported images are packed
|
||||||
|
# into the .blend file; they survive saving and reopening.
|
||||||
|
|
||||||
|
bl_info = {
|
||||||
|
"name": "YAPF Image Format",
|
||||||
|
"author": "Nexoniarz",
|
||||||
|
"version": (1, 0, 0),
|
||||||
|
"blender": (4, 2, 0),
|
||||||
|
"location": "File > Import > YAPF Image, Image Editor > Image menu",
|
||||||
|
"description": "Import and export YAPF (.yapf) lossless images",
|
||||||
|
"doc_url": "https://github.com/Nexoniarz/YAPF",
|
||||||
|
"category": "Import-Export",
|
||||||
|
}
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
import bpy
|
||||||
|
import numpy as np
|
||||||
|
from bpy.props import BoolProperty, CollectionProperty, StringProperty
|
||||||
|
from bpy_extras.io_utils import ExportHelper, ImportHelper
|
||||||
|
|
||||||
|
from . import yapf
|
||||||
|
|
||||||
|
|
||||||
|
# ── conversion helpers ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
def _set_colorspace(bimg, names):
|
||||||
|
for n in names: # names differ between versions
|
||||||
|
try:
|
||||||
|
bimg.colorspace_settings.name = n
|
||||||
|
return
|
||||||
|
except TypeError:
|
||||||
|
continue
|
||||||
|
|
||||||
|
|
||||||
|
def _read(filepath):
|
||||||
|
"""(rgba float32 h×w×4, has_alpha, flags, float_data, texture or None)."""
|
||||||
|
if yapf.backend() == "c" and getattr(yapf._lib, "has_textures", False):
|
||||||
|
tex = yapf.load_texture(filepath) # every v1 / v2 file
|
||||||
|
rgba = tex.rgba_float() # frame 0, layer 0, face +X, mip 0
|
||||||
|
if tex.normal & 3 == 2: # DirectX (-Y) → Blender's OpenGL (+Y)
|
||||||
|
rgba[..., 1] = 1.0 - rgba[..., 1]
|
||||||
|
if tex.normal & 4 or (tex.normal and tex.channels == 2): # XY: rebuild Z
|
||||||
|
x, y = rgba[..., 0] * 2.0 - 1.0, rgba[..., 1] * 2.0 - 1.0
|
||||||
|
rgba[..., 2] = (np.sqrt(np.clip(1.0 - x * x - y * y, 0.0, 1.0)) + 1.0) * 0.5
|
||||||
|
alpha = tex.channels in (2, 4) and not tex.normal
|
||||||
|
wide = tex.is_hdr or (tex.payload == yapf.PAYLOAD_PIXELS and tex.sample == yapf.SAMPLE_U16)
|
||||||
|
return rgba, alpha, tex.flags, wide, tex
|
||||||
|
img = yapf.load(filepath) # pure-Python decoder: v1 only
|
||||||
|
w, h, ch = img.width, img.height, img.channels
|
||||||
|
src = np.frombuffer(img.pixels, dtype=np.uint8).reshape(h, w, ch).astype(np.float32) / 255.0
|
||||||
|
rgba = np.ones((h, w, 4), dtype=np.float32)
|
||||||
|
rgba[..., :3] = src[..., :3] if ch >= 3 else src[..., :1]
|
||||||
|
if ch in (2, 4):
|
||||||
|
rgba[..., 3] = src[..., ch - 1]
|
||||||
|
return rgba, ch in (2, 4), img.flags, False, None
|
||||||
|
|
||||||
|
|
||||||
|
def load_image(filepath):
|
||||||
|
"""Read a .yapf file into a packed bpy.types.Image. 16-bit, half,
|
||||||
|
float and BC6H textures become float images (packed as EXR so nothing
|
||||||
|
is lost); GPU blocks are decoded."""
|
||||||
|
rgba, alpha, flags, wide, tex = _read(filepath)
|
||||||
|
h, w = rgba.shape[:2]
|
||||||
|
|
||||||
|
name = os.path.basename(filepath)
|
||||||
|
bimg = bpy.data.images.new(name, w, h, alpha=alpha, float_buffer=wide)
|
||||||
|
if tex is not None and tex.is_hdr:
|
||||||
|
_set_colorspace(bimg, ("Linear Rec.709", "Linear"))
|
||||||
|
elif flags & yapf.FLAG_SRGB:
|
||||||
|
_set_colorspace(bimg, ("sRGB",))
|
||||||
|
else:
|
||||||
|
_set_colorspace(bimg, ("Non-Color",))
|
||||||
|
if flags & yapf.FLAG_PREMULT_ALPHA:
|
||||||
|
bimg.alpha_mode = "PREMUL"
|
||||||
|
bimg.pixels.foreach_set(rgba[::-1].ravel()) # Blender rows go bottom-up
|
||||||
|
bimg.file_format = "OPEN_EXR" if wide else "PNG"
|
||||||
|
bimg.pack() # keep it inside the .blend
|
||||||
|
bimg.use_fake_user = True # …even before anything uses it
|
||||||
|
bimg["yapf_source"] = filepath
|
||||||
|
bimg["yapf_alpha"] = alpha
|
||||||
|
return bimg
|
||||||
|
|
||||||
|
|
||||||
|
def save_image(bimg, filepath, mips=False):
|
||||||
|
"""Write a bpy.types.Image to a .yapf file."""
|
||||||
|
w, h = bimg.size
|
||||||
|
if w == 0 or h == 0:
|
||||||
|
raise yapf.YapfError("image '%s' has no pixels" % bimg.name)
|
||||||
|
buf = np.empty(w * h * 4, dtype=np.float32)
|
||||||
|
bimg.pixels.foreach_get(buf)
|
||||||
|
px = (np.clip(buf.reshape(h, w, 4)[::-1], 0.0, 1.0) * 255.0 + 0.5).astype(np.uint8)
|
||||||
|
has_alpha = bool((px[..., 3] != 255).any())
|
||||||
|
ch = 4 if has_alpha else 3
|
||||||
|
px = np.ascontiguousarray(px[..., :ch])
|
||||||
|
|
||||||
|
srgb = bimg.colorspace_settings.name not in ("Non-Color", "Linear", "Linear Rec.709", "Raw")
|
||||||
|
flags = (yapf.FLAG_SRGB if srgb else 0) | (yapf.FLAG_PREMULT_ALPHA if bimg.alpha_mode == "PREMUL" else 0)
|
||||||
|
levels = [px]
|
||||||
|
if mips:
|
||||||
|
cur = px.astype(np.uint32)
|
||||||
|
while cur.shape[0] > 1 or cur.shape[1] > 1:
|
||||||
|
hh, ww = max(1, cur.shape[0] // 2), max(1, cur.shape[1] // 2)
|
||||||
|
p = np.pad(cur, ((0, cur.shape[0] % 2), (0, cur.shape[1] % 2), (0, 0)), mode="edge")
|
||||||
|
cur = (p[0::2, 0::2] + p[1::2, 0::2] + p[0::2, 1::2] + p[1::2, 1::2] + 2) // 4
|
||||||
|
cur = cur[:hh, :ww]
|
||||||
|
levels.append(cur.astype(np.uint8))
|
||||||
|
if len(levels) == 16:
|
||||||
|
break
|
||||||
|
out = yapf.Image(w, h, ch, levels[0].tobytes(), flags=flags,
|
||||||
|
mips=[l.tobytes() for l in levels])
|
||||||
|
if srgb:
|
||||||
|
out.gpu_format = yapf.GPU_SRGB8_A8 if ch == 4 else yapf.GPU_SRGB8
|
||||||
|
else:
|
||||||
|
out.gpu_format = yapf.GPU_RGBA8 if ch == 4 else yapf.GPU_RGB8
|
||||||
|
yapf.save(filepath, out)
|
||||||
|
|
||||||
|
|
||||||
|
def add_image_plane(context, bimg):
|
||||||
|
"""A plane with the image's aspect ratio and a material that shows it."""
|
||||||
|
w, h = bimg.size
|
||||||
|
bpy.ops.mesh.primitive_plane_add(size=1.0, location=context.scene.cursor.location)
|
||||||
|
obj = context.active_object
|
||||||
|
obj.name = os.path.splitext(bimg.name)[0]
|
||||||
|
obj.scale = (w / max(w, h), h / max(w, h), 1.0)
|
||||||
|
|
||||||
|
mat = bpy.data.materials.new(obj.name)
|
||||||
|
mat.use_nodes = True
|
||||||
|
nodes, links = mat.node_tree.nodes, mat.node_tree.links
|
||||||
|
bsdf = next(n for n in nodes if n.type == "BSDF_PRINCIPLED")
|
||||||
|
tex = nodes.new("ShaderNodeTexImage")
|
||||||
|
tex.image = bimg
|
||||||
|
tex.location = (bsdf.location.x - 350, bsdf.location.y)
|
||||||
|
links.new(tex.outputs["Color"], bsdf.inputs["Base Color"])
|
||||||
|
if bimg.get("yapf_alpha"):
|
||||||
|
links.new(tex.outputs["Alpha"], bsdf.inputs["Alpha"])
|
||||||
|
obj.data.materials.append(mat)
|
||||||
|
return obj
|
||||||
|
|
||||||
|
|
||||||
|
def show_in_image_editor(context, bimg):
|
||||||
|
for area in context.screen.areas if context.screen else []:
|
||||||
|
if area.type == "IMAGE_EDITOR":
|
||||||
|
area.spaces.active.image = bimg
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
# ── operators ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
class IMPORT_IMAGE_OT_yapf(bpy.types.Operator, ImportHelper):
|
||||||
|
"""Import YAPF (.yapf) images"""
|
||||||
|
bl_idname = "import_image.yapf"
|
||||||
|
bl_label = "Import YAPF Image"
|
||||||
|
bl_options = {"REGISTER", "UNDO"}
|
||||||
|
|
||||||
|
filename_ext = ".yapf"
|
||||||
|
filter_glob: StringProperty(default="*.yapf", options={"HIDDEN"})
|
||||||
|
files: CollectionProperty(type=bpy.types.OperatorFileListElement,
|
||||||
|
options={"HIDDEN", "SKIP_SAVE"})
|
||||||
|
directory: StringProperty(subtype="DIR_PATH", options={"HIDDEN", "SKIP_SAVE"})
|
||||||
|
as_plane: BoolProperty(name="Create Plane",
|
||||||
|
description="Add a plane showing the image to the scene",
|
||||||
|
default=False)
|
||||||
|
|
||||||
|
def paths(self):
|
||||||
|
if self.files and self.directory:
|
||||||
|
return [os.path.join(self.directory, f.name) for f in self.files if f.name]
|
||||||
|
return [self.filepath]
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
done = 0
|
||||||
|
for path in self.paths():
|
||||||
|
try:
|
||||||
|
bimg = load_image(path)
|
||||||
|
except (OSError, yapf.YapfError) as e:
|
||||||
|
self.report({"ERROR"}, "%s: %s" % (os.path.basename(path), e))
|
||||||
|
continue
|
||||||
|
done += 1
|
||||||
|
if self.as_plane and context.mode == "OBJECT":
|
||||||
|
add_image_plane(context, bimg)
|
||||||
|
else:
|
||||||
|
show_in_image_editor(context, bimg)
|
||||||
|
if done:
|
||||||
|
self.report({"INFO"}, "Imported %d YAPF image(s)" % done)
|
||||||
|
return {"FINISHED"} if done else {"CANCELLED"}
|
||||||
|
|
||||||
|
def invoke(self, context, event):
|
||||||
|
# Dropped files arrive with filepath/directory already set.
|
||||||
|
if self.directory and self.files:
|
||||||
|
if context.area and context.area.type == "VIEW_3D":
|
||||||
|
self.as_plane = True
|
||||||
|
return self.execute(context)
|
||||||
|
return ImportHelper.invoke(self, context, event)
|
||||||
|
|
||||||
|
|
||||||
|
class EXPORT_IMAGE_OT_yapf(bpy.types.Operator, ExportHelper):
|
||||||
|
"""Save the current image as a YAPF (.yapf) file"""
|
||||||
|
bl_idname = "export_image.yapf"
|
||||||
|
bl_label = "Save as YAPF"
|
||||||
|
|
||||||
|
filename_ext = ".yapf"
|
||||||
|
filter_glob: StringProperty(default="*.yapf", options={"HIDDEN"})
|
||||||
|
image_name: StringProperty(name="Image", description="Image to save (default: the one in the Image Editor)")
|
||||||
|
mips: BoolProperty(name="Store Mipmaps", description="Also store a full mip chain", default=False)
|
||||||
|
|
||||||
|
def target(self, context):
|
||||||
|
if self.image_name:
|
||||||
|
return bpy.data.images.get(self.image_name)
|
||||||
|
space = context.space_data
|
||||||
|
return getattr(space, "image", None)
|
||||||
|
|
||||||
|
def invoke(self, context, event):
|
||||||
|
img = self.target(context)
|
||||||
|
if img is None:
|
||||||
|
self.report({"ERROR"}, "Open an image in the Image Editor first")
|
||||||
|
return {"CANCELLED"}
|
||||||
|
self.image_name = img.name
|
||||||
|
self.filepath = os.path.splitext(img.name)[0] + ".yapf"
|
||||||
|
return ExportHelper.invoke(self, context, event)
|
||||||
|
|
||||||
|
def execute(self, context):
|
||||||
|
img = self.target(context)
|
||||||
|
if img is None:
|
||||||
|
self.report({"ERROR"}, "No image to save")
|
||||||
|
return {"CANCELLED"}
|
||||||
|
try:
|
||||||
|
save_image(img, self.filepath, self.mips)
|
||||||
|
except (OSError, yapf.YapfError) as e:
|
||||||
|
self.report({"ERROR"}, str(e))
|
||||||
|
return {"CANCELLED"}
|
||||||
|
self.report({"INFO"}, "Saved %s" % os.path.basename(self.filepath))
|
||||||
|
return {"FINISHED"}
|
||||||
|
|
||||||
|
|
||||||
|
class IMAGE_FH_yapf(bpy.types.FileHandler):
|
||||||
|
"""Drag and drop .yapf files into Blender"""
|
||||||
|
bl_idname = "IMAGE_FH_yapf"
|
||||||
|
bl_label = "YAPF Image"
|
||||||
|
bl_import_operator = "import_image.yapf"
|
||||||
|
bl_file_extensions = ".yapf"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def poll_drop(cls, context):
|
||||||
|
return context.area is not None and context.area.type in {
|
||||||
|
"VIEW_3D", "IMAGE_EDITOR", "NODE_EDITOR"}
|
||||||
|
|
||||||
|
|
||||||
|
def menu_import(self, context):
|
||||||
|
self.layout.operator(IMPORT_IMAGE_OT_yapf.bl_idname, text="YAPF Image (.yapf)")
|
||||||
|
|
||||||
|
|
||||||
|
def menu_image(self, context):
|
||||||
|
self.layout.separator()
|
||||||
|
self.layout.operator(IMPORT_IMAGE_OT_yapf.bl_idname, text="Open YAPF…")
|
||||||
|
self.layout.operator(EXPORT_IMAGE_OT_yapf.bl_idname, text="Save as YAPF…")
|
||||||
|
|
||||||
|
|
||||||
|
classes = (IMPORT_IMAGE_OT_yapf, EXPORT_IMAGE_OT_yapf, IMAGE_FH_yapf)
|
||||||
|
|
||||||
|
|
||||||
|
def register():
|
||||||
|
for c in classes:
|
||||||
|
bpy.utils.register_class(c)
|
||||||
|
bpy.types.TOPBAR_MT_file_import.append(menu_import)
|
||||||
|
bpy.types.IMAGE_MT_image.append(menu_image)
|
||||||
|
|
||||||
|
|
||||||
|
def unregister():
|
||||||
|
bpy.types.IMAGE_MT_image.remove(menu_image)
|
||||||
|
bpy.types.TOPBAR_MT_file_import.remove(menu_import)
|
||||||
|
for c in reversed(classes):
|
||||||
|
bpy.utils.unregister_class(c)
|
||||||
16
extensions/blender/yapf_image/blender_manifest.toml
Normal file
16
extensions/blender/yapf_image/blender_manifest.toml
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
schema_version = "1.0.0"
|
||||||
|
|
||||||
|
id = "yapf_image"
|
||||||
|
version = "1.0.0"
|
||||||
|
name = "YAPF Image Format"
|
||||||
|
tagline = "Import and export YAPF lossless images"
|
||||||
|
maintainer = "Nexoniarz"
|
||||||
|
type = "add-on"
|
||||||
|
website = "https://github.com/Nexoniarz/YAPF"
|
||||||
|
tags = ["Import-Export"]
|
||||||
|
|
||||||
|
blender_version_min = "4.2.0"
|
||||||
|
license = ["SPDX:Apache-2.0"]
|
||||||
|
|
||||||
|
[permissions]
|
||||||
|
files = "Read and write .yapf image files"
|
||||||
45
extensions/figma/README.md
Normal file
45
extensions/figma/README.md
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
# YAPF for Figma
|
||||||
|
|
||||||
|
- **Plugins → YAPF Image Format → Import YAPF image…** — choose or drop
|
||||||
|
`.yapf` files; each becomes a rectangle filled with the image.
|
||||||
|
- **Plugins → YAPF Image Format → Export selection as YAPF…** — every
|
||||||
|
selected layer is exported at 1×, 2× or 4× and downloaded as `.yapf`.
|
||||||
|
|
||||||
|
Both directions are pixel-exact: the plugin carries its own small PNG
|
||||||
|
encoder/decoder instead of going through a canvas, which would alter
|
||||||
|
semi-transparent pixels.
|
||||||
|
|
||||||
|
Import reads every YAPF file, version 1 and 2. 8-bit images keep their
|
||||||
|
exact pixels; GPU blocks (BC1–BC7, BC6H), 16-bit and float textures are
|
||||||
|
converted to 8-bit for display (Figma images are 8-bit); cubes, arrays,
|
||||||
|
volumes and animations bring their first image. Damaged files are
|
||||||
|
reported as such.
|
||||||
|
|
||||||
|
## Install (development plugin)
|
||||||
|
|
||||||
|
1. Use the **Figma desktop app**.
|
||||||
|
2. **Plugins → Development → Import plugin from manifest…** and choose
|
||||||
|
`extensions/figma/manifest.json` (or the one inside `yapf-figma.zip`
|
||||||
|
from the [Releases](https://github.com/Nexoniarz/YAPF/releases)).
|
||||||
|
3. Run it from **Plugins → Development → YAPF Image Format**.
|
||||||
|
|
||||||
|
To publish it to the Figma Community, use **Publish** in the same menu;
|
||||||
|
Figma then assigns the plugin its real `id`.
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
`ui.html` is generated: it inlines `bindings/js/yapf.js` and `src/png.js`
|
||||||
|
into `src/ui.src.html`, because Figma loads the UI as a single file.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
node build.js
|
||||||
|
```
|
||||||
|
|
||||||
|
Figma limits images to 4096 × 4096; larger `.yapf` files report an error.
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
The plugin UI was tested in a browser engine (headless Chromium) with a
|
||||||
|
simulated Figma main thread: import hands Figma a pixel-exact PNG, and
|
||||||
|
export downloads a pixel-exact `.yapf`. `code.js` is syntax-checked.
|
||||||
|
**It has not yet been run inside Figma itself.**
|
||||||
9
extensions/figma/build.js
Normal file
9
extensions/figma/build.js
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
// node build.js — inlines the YAPF and PNG codecs into ui.html (Figma needs one file).
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const read = (p) => fs.readFileSync(path.join(__dirname, p), 'utf8');
|
||||||
|
const html = read('src/ui.src.html')
|
||||||
|
.replace('/*__YAPF_JS__*/', () => read('../../bindings/js/yapf.js'))
|
||||||
|
.replace('/*__PNG_JS__*/', () => read('src/png.js'));
|
||||||
|
fs.writeFileSync(path.join(__dirname, 'ui.html'), html);
|
||||||
|
console.log('wrote ui.html (' + html.length + ' bytes)');
|
||||||
54
extensions/figma/code.js
Normal file
54
extensions/figma/code.js
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
// YAPF for Figma — plugin main thread (has the document, no DOM).
|
||||||
|
// Copyright 2026 Nexoniarz — Apache License 2.0.
|
||||||
|
// Decoding/encoding happens in the UI (ui.html), which has a DOM.
|
||||||
|
|
||||||
|
figma.showUI(__html__, { width: 380, height: 330, themeColors: true });
|
||||||
|
figma.ui.postMessage({ type: 'mode', mode: figma.command || 'import' });
|
||||||
|
|
||||||
|
function sendSelection() {
|
||||||
|
const nodes = figma.currentPage.selection.filter((n) => 'exportAsync' in n);
|
||||||
|
figma.ui.postMessage({ type: 'selection', names: nodes.map((n) => n.name) });
|
||||||
|
}
|
||||||
|
figma.on('selectionchange', sendSelection);
|
||||||
|
sendSelection();
|
||||||
|
|
||||||
|
figma.ui.onmessage = async (msg) => {
|
||||||
|
if (msg.type === 'import') {
|
||||||
|
// msg.images: [{ name, width, height, png: Uint8Array }]
|
||||||
|
const created = [];
|
||||||
|
const center = figma.viewport.center;
|
||||||
|
let x = center.x;
|
||||||
|
for (const im of msg.images) {
|
||||||
|
try {
|
||||||
|
const image = figma.createImage(im.png);
|
||||||
|
const rect = figma.createRectangle();
|
||||||
|
rect.name = im.name;
|
||||||
|
rect.resize(im.width, im.height);
|
||||||
|
rect.fills = [{ type: 'IMAGE', imageHash: image.hash, scaleMode: 'FILL' }];
|
||||||
|
rect.x = Math.round(x - (created.length ? 0 : im.width / 2));
|
||||||
|
rect.y = Math.round(center.y - im.height / 2);
|
||||||
|
x = rect.x + im.width + 40;
|
||||||
|
figma.currentPage.appendChild(rect);
|
||||||
|
created.push(rect);
|
||||||
|
} catch (e) {
|
||||||
|
figma.notify(im.name + ': ' + (e.message || e) + ' (Figma accepts images up to 4096×4096)', { error: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (created.length) {
|
||||||
|
figma.currentPage.selection = created;
|
||||||
|
figma.viewport.scrollAndZoomIntoView(created);
|
||||||
|
figma.notify('Imported ' + created.length + ' YAPF image' + (created.length > 1 ? 's' : ''));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg.type === 'export') {
|
||||||
|
const nodes = figma.currentPage.selection.filter((n) => 'exportAsync' in n);
|
||||||
|
if (!nodes.length) { figma.notify('Select one or more layers to export', { error: true }); return; }
|
||||||
|
for (const node of nodes) {
|
||||||
|
const png = await node.exportAsync({ format: 'PNG', constraint: { type: 'SCALE', value: msg.scale || 1 } });
|
||||||
|
figma.ui.postMessage({ type: 'exported', name: node.name, png });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg.type === 'close') figma.closePlugin();
|
||||||
|
};
|
||||||
14
extensions/figma/manifest.json
Normal file
14
extensions/figma/manifest.json
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
{
|
||||||
|
"name": "YAPF Image Format",
|
||||||
|
"id": "yapf-image-format",
|
||||||
|
"api": "1.0.0",
|
||||||
|
"main": "code.js",
|
||||||
|
"ui": "ui.html",
|
||||||
|
"editorType": ["figma", "figjam"],
|
||||||
|
"documentAccess": "dynamic-page",
|
||||||
|
"networkAccess": { "allowedDomains": ["none"] },
|
||||||
|
"menu": [
|
||||||
|
{ "name": "Import YAPF image…", "command": "import" },
|
||||||
|
{ "name": "Export selection as YAPF…", "command": "export" }
|
||||||
|
]
|
||||||
|
}
|
||||||
126
extensions/figma/src/png.js
Normal file
126
extensions/figma/src/png.js
Normal file
|
|
@ -0,0 +1,126 @@
|
||||||
|
/*
|
||||||
|
* png.js — exact 8-bit PNG encode / decode for the browser and Node 18+.
|
||||||
|
* Uses the platform's CompressionStream / DecompressionStream for zlib.
|
||||||
|
* Canvas round-trips premultiply alpha and are not lossless; this is.
|
||||||
|
* Copyright 2026 Nexoniarz — Apache License 2.0.
|
||||||
|
*/
|
||||||
|
(function (root, factory) {
|
||||||
|
if (typeof module === 'object' && module.exports) module.exports = factory();
|
||||||
|
else root.PNG = factory();
|
||||||
|
}(typeof self !== 'undefined' ? self : this, function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const CRC = new Uint32Array(256);
|
||||||
|
for (let n = 0; n < 256; n++) {
|
||||||
|
let c = n;
|
||||||
|
for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
||||||
|
CRC[n] = c >>> 0;
|
||||||
|
}
|
||||||
|
function crc32(bytes, start, end) {
|
||||||
|
let c = 0xffffffff;
|
||||||
|
for (let i = start; i < end; i++) c = CRC[(c ^ bytes[i]) & 255] ^ (c >>> 8);
|
||||||
|
return (c ^ 0xffffffff) >>> 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function zlib(data, mode) {
|
||||||
|
const stream = new Blob([data]).stream().pipeThrough(
|
||||||
|
mode === 'deflate' ? new CompressionStream('deflate') : new DecompressionStream('deflate'));
|
||||||
|
return new Uint8Array(await new Response(stream).arrayBuffer());
|
||||||
|
}
|
||||||
|
|
||||||
|
const COLOR_TYPE = { 1: 0, 2: 4, 3: 2, 4: 6 }; // channels → PNG colour type
|
||||||
|
const CHANNELS = { 0: 1, 4: 2, 2: 3, 6: 4 };
|
||||||
|
|
||||||
|
function paeth(a, b, c) {
|
||||||
|
const p = a + b - c, pa = Math.abs(p - a), pb = Math.abs(p - b), pc = Math.abs(p - c);
|
||||||
|
return pa <= pb && pa <= pc ? a : pb <= pc ? b : c;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* pixels: Uint8Array of w*h*ch bytes, rows top to bottom. */
|
||||||
|
async function encode(pixels, w, h, ch) {
|
||||||
|
const stride = w * ch, raw = new Uint8Array((stride + 1) * h), row = new Uint8Array(stride);
|
||||||
|
for (let y = 0; y < h; y++) {
|
||||||
|
const cur = pixels.subarray(y * stride, (y + 1) * stride);
|
||||||
|
const prev = y ? pixels.subarray((y - 1) * stride, y * stride) : null;
|
||||||
|
let best = 0, bestSum = Infinity;
|
||||||
|
for (let f = 0; f < 5; f++) { // pick the filter with the smallest sum
|
||||||
|
let sum = 0;
|
||||||
|
for (let i = 0; i < stride; i++) {
|
||||||
|
const a = i >= ch ? cur[i - ch] : 0, b = prev ? prev[i] : 0, c = prev && i >= ch ? prev[i - ch] : 0;
|
||||||
|
const v = (cur[i] - [0, a, b, (a + b) >> 1, paeth(a, b, c)][f]) & 255;
|
||||||
|
sum += v < 128 ? v : 256 - v;
|
||||||
|
}
|
||||||
|
if (sum < bestSum) { bestSum = sum; best = f; }
|
||||||
|
}
|
||||||
|
const o = y * (stride + 1);
|
||||||
|
raw[o] = best;
|
||||||
|
for (let i = 0; i < stride; i++) {
|
||||||
|
const a = i >= ch ? cur[i - ch] : 0, b = prev ? prev[i] : 0, c = prev && i >= ch ? prev[i - ch] : 0;
|
||||||
|
row[i] = (cur[i] - [0, a, b, (a + b) >> 1, paeth(a, b, c)][best]) & 255;
|
||||||
|
}
|
||||||
|
raw.set(row, o + 1);
|
||||||
|
}
|
||||||
|
const idat = await zlib(raw, 'deflate');
|
||||||
|
const chunks = [];
|
||||||
|
const chunk = (type, data) => {
|
||||||
|
const c = new Uint8Array(12 + data.length), dv = new DataView(c.buffer);
|
||||||
|
dv.setUint32(0, data.length);
|
||||||
|
for (let i = 0; i < 4; i++) c[4 + i] = type.charCodeAt(i);
|
||||||
|
c.set(data, 8);
|
||||||
|
dv.setUint32(8 + data.length, crc32(c, 4, 8 + data.length));
|
||||||
|
chunks.push(c);
|
||||||
|
};
|
||||||
|
const ihdr = new Uint8Array(13), dv = new DataView(ihdr.buffer);
|
||||||
|
dv.setUint32(0, w); dv.setUint32(4, h);
|
||||||
|
ihdr.set([8, COLOR_TYPE[ch], 0, 0, 0], 8);
|
||||||
|
chunk('IHDR', ihdr); chunk('IDAT', idat); chunk('IEND', new Uint8Array(0));
|
||||||
|
const total = 8 + chunks.reduce((n, c) => n + c.length, 0), out = new Uint8Array(total);
|
||||||
|
out.set([137, 80, 78, 71, 13, 10, 26, 10]);
|
||||||
|
let p = 8;
|
||||||
|
for (const c of chunks) { out.set(c, p); p += c.length; }
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Returns { width, height, channels, pixels } or throws for PNGs it does
|
||||||
|
* not handle (16-bit, interlaced); callers can fall back to a canvas. */
|
||||||
|
async function decode(bytes) {
|
||||||
|
const sig = [137, 80, 78, 71, 13, 10, 26, 10];
|
||||||
|
if (bytes.length < 8 || sig.some((v, i) => bytes[i] !== v)) throw new Error('not a PNG');
|
||||||
|
const dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
||||||
|
let w = 0, h = 0, depth = 0, type = 0, interlace = 0, palette = null, trns = null;
|
||||||
|
const idat = [];
|
||||||
|
for (let p = 8; p + 8 <= bytes.length;) {
|
||||||
|
const len = dv.getUint32(p), name = String.fromCharCode(...bytes.subarray(p + 4, p + 8));
|
||||||
|
const data = bytes.subarray(p + 8, p + 8 + len);
|
||||||
|
if (name === 'IHDR') { w = dv.getUint32(p + 8); h = dv.getUint32(p + 12); depth = data[8]; type = data[9]; interlace = data[12]; }
|
||||||
|
else if (name === 'PLTE') palette = data;
|
||||||
|
else if (name === 'tRNS') trns = data;
|
||||||
|
else if (name === 'IDAT') idat.push(data);
|
||||||
|
else if (name === 'IEND') break;
|
||||||
|
p += 12 + len;
|
||||||
|
}
|
||||||
|
if (depth !== 8 || interlace !== 0 || !(type in CHANNELS || type === 3))
|
||||||
|
throw new Error('unsupported PNG (needs 8-bit, non-interlaced)');
|
||||||
|
const inCh = type === 3 ? 1 : CHANNELS[type], stride = w * inCh;
|
||||||
|
const raw = await zlib(new Uint8Array(await new Blob(idat).arrayBuffer()), 'inflate');
|
||||||
|
const px = new Uint8Array(stride * h);
|
||||||
|
for (let y = 0; y < h; y++) {
|
||||||
|
const f = raw[y * (stride + 1)], src = y * (stride + 1) + 1, o = y * stride;
|
||||||
|
for (let i = 0; i < stride; i++) {
|
||||||
|
const a = i >= inCh ? px[o + i - inCh] : 0, b = y ? px[o - stride + i] : 0;
|
||||||
|
const c = y && i >= inCh ? px[o - stride + i - inCh] : 0;
|
||||||
|
px[o + i] = (raw[src + i] + [0, a, b, (a + b) >> 1, paeth(a, b, c)][f]) & 255;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (type !== 3) return { width: w, height: h, channels: inCh, pixels: px };
|
||||||
|
const ch = trns ? 4 : 3, out = new Uint8Array(w * h * ch); // expand a palette
|
||||||
|
for (let i = 0; i < w * h; i++) {
|
||||||
|
const k = px[i];
|
||||||
|
out.set(palette.subarray(k * 3, k * 3 + 3), i * ch);
|
||||||
|
if (trns) out[i * ch + 3] = k < trns.length ? trns[k] : 255;
|
||||||
|
}
|
||||||
|
return { width: w, height: h, channels: ch, pixels: out };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { encode, decode };
|
||||||
|
}));
|
||||||
136
extensions/figma/src/ui.src.html
Normal file
136
extensions/figma/src/ui.src.html
Normal file
|
|
@ -0,0 +1,136 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<style>
|
||||||
|
body { font: 12px Inter, system-ui, sans-serif; margin: 0; padding: 14px;
|
||||||
|
color: var(--figma-color-text, #333); background: var(--figma-color-bg, #fff); }
|
||||||
|
h2 { font-size: 13px; margin: 0 0 10px; }
|
||||||
|
.tabs { display: flex; gap: 6px; margin-bottom: 12px; }
|
||||||
|
.tabs button { flex: 1; }
|
||||||
|
button { font: inherit; padding: 7px 10px; border-radius: 6px; cursor: pointer;
|
||||||
|
border: 1px solid var(--figma-color-border, #ccc);
|
||||||
|
background: var(--figma-color-bg-secondary, #f5f5f5); color: inherit; }
|
||||||
|
button.primary, .tabs button.on { background: var(--figma-color-bg-brand, #0d99ff);
|
||||||
|
color: var(--figma-color-text-onbrand, #fff); border-color: transparent; }
|
||||||
|
#drop { border: 2px dashed var(--figma-color-border, #ccc); border-radius: 8px; padding: 28px 10px;
|
||||||
|
text-align: center; cursor: pointer; color: var(--figma-color-text-secondary, #777); }
|
||||||
|
#drop.hover { border-color: var(--figma-color-border-brand, #0d99ff); }
|
||||||
|
#status { margin-top: 10px; min-height: 32px; color: var(--figma-color-text-secondary, #777);
|
||||||
|
white-space: pre-line; }
|
||||||
|
.row { display: flex; gap: 8px; align-items: center; margin: 8px 0; }
|
||||||
|
select { font: inherit; }
|
||||||
|
[hidden] { display: none !important; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="tabs">
|
||||||
|
<button id="tab-import">Import</button>
|
||||||
|
<button id="tab-export">Export</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section id="import">
|
||||||
|
<h2>Import YAPF images</h2>
|
||||||
|
<div id="drop">Drop .yapf files here<br>or click to choose</div>
|
||||||
|
<input id="file" type="file" accept=".yapf" multiple hidden>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section id="export" hidden>
|
||||||
|
<h2>Export selection as YAPF</h2>
|
||||||
|
<div id="sel">Nothing selected</div>
|
||||||
|
<div class="row">Scale
|
||||||
|
<select id="scale"><option>1</option><option>2</option><option>4</option></select>
|
||||||
|
</div>
|
||||||
|
<button id="go" class="primary">Export</button>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div id="status"></div>
|
||||||
|
|
||||||
|
<script>/*__YAPF_JS__*/</script>
|
||||||
|
<script>/*__PNG_JS__*/</script>
|
||||||
|
<script>
|
||||||
|
const $ = (id) => document.getElementById(id);
|
||||||
|
const post = (m) => parent.postMessage({ pluginMessage: m }, '*');
|
||||||
|
const status = (t) => { $('status').textContent = t; };
|
||||||
|
|
||||||
|
function tab(name) {
|
||||||
|
$('import').hidden = name !== 'import';
|
||||||
|
$('export').hidden = name !== 'export';
|
||||||
|
$('tab-import').classList.toggle('on', name === 'import');
|
||||||
|
$('tab-export').classList.toggle('on', name === 'export');
|
||||||
|
status('');
|
||||||
|
}
|
||||||
|
$('tab-import').onclick = () => tab('import');
|
||||||
|
$('tab-export').onclick = () => tab('export');
|
||||||
|
|
||||||
|
/* Import: .yapf → pixels → exact PNG → Figma image */
|
||||||
|
/* Any YAPF file → 8-bit pixels. 8-bit images keep their exact pixels and
|
||||||
|
* channels; GPU blocks, 16-bit and float textures are converted for display
|
||||||
|
* (RGBA). Cubes, arrays, volumes and animations give their first image. */
|
||||||
|
function yapfToPixels(tex) {
|
||||||
|
const w = tex.width, h = tex.height;
|
||||||
|
if (tex.payload === 0 && tex.sample === 0)
|
||||||
|
return { width: w, height: h, channels: tex.channels,
|
||||||
|
pixels: tex.surfaces[0].subarray(0, w * h * tex.channels) };
|
||||||
|
const v = YAPF.textureToRGBA(tex, 0, 0);
|
||||||
|
return { width: w, height: h, channels: 4, pixels: new Uint8Array(v.rgba.buffer),
|
||||||
|
note: tex.payload === 1 ? 'GPU blocks decoded' : 'converted to 8-bit' };
|
||||||
|
}
|
||||||
|
async function importFiles(files) {
|
||||||
|
const images = [];
|
||||||
|
for (const f of files) {
|
||||||
|
try {
|
||||||
|
const img = yapfToPixels(YAPF.decodeTexture(new Uint8Array(await f.arrayBuffer())));
|
||||||
|
const png = await PNG.encode(img.pixels, img.width, img.height, img.channels);
|
||||||
|
images.push({ name: f.name.replace(/\.yapf$/i, ''), width: img.width, height: img.height, png });
|
||||||
|
status('Decoded ' + f.name + ' (' + img.width + '×' + img.height + (img.note ? ', ' + img.note : '') + ')');
|
||||||
|
} catch (e) {
|
||||||
|
status(f.name + ': ' + e.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (images.length) post({ type: 'import', images });
|
||||||
|
}
|
||||||
|
$('drop').onclick = () => $('file').click();
|
||||||
|
$('file').onchange = (e) => importFiles([...e.target.files]);
|
||||||
|
$('drop').ondragover = (e) => { e.preventDefault(); $('drop').classList.add('hover'); };
|
||||||
|
$('drop').ondragleave = () => $('drop').classList.remove('hover');
|
||||||
|
$('drop').ondrop = (e) => { e.preventDefault(); $('drop').classList.remove('hover'); importFiles([...e.dataTransfer.files]); };
|
||||||
|
|
||||||
|
/* Export: Figma PNG → exact pixels → .yapf download */
|
||||||
|
$('go').onclick = () => { status('Exporting…'); post({ type: 'export', scale: +$('scale').value }); };
|
||||||
|
|
||||||
|
async function exported(name, pngBytes) {
|
||||||
|
let img = await PNG.decode(pngBytes);
|
||||||
|
let { pixels, channels } = img;
|
||||||
|
if (channels === 4) { // drop alpha when fully opaque
|
||||||
|
let opaque = true;
|
||||||
|
for (let i = 3; i < pixels.length && opaque; i += 4) opaque = pixels[i] === 255;
|
||||||
|
if (opaque) {
|
||||||
|
const rgb = new Uint8Array(img.width * img.height * 3);
|
||||||
|
for (let i = 0, j = 0; i < pixels.length; i += 4, j += 3) rgb.set(pixels.subarray(i, i + 3), j);
|
||||||
|
pixels = rgb; channels = 3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const file = YAPF.encode({ width: img.width, height: img.height, channels, pixels,
|
||||||
|
flags: YAPF.FLAG.SRGB,
|
||||||
|
gpuFormat: channels === 4 ? YAPF.GPU.SRGB8_A8 : YAPF.GPU.SRGB8 });
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = URL.createObjectURL(new Blob([file], { type: 'image/x-yapf' }));
|
||||||
|
a.download = name.replace(/[\\/:*?"<>|]/g, '_') + '.yapf';
|
||||||
|
document.body.appendChild(a); a.click(); a.remove();
|
||||||
|
status('Saved ' + a.download + '\n' + img.width + '×' + img.height + ', ' +
|
||||||
|
(file.length / 1024).toFixed(1) + ' KB (' +
|
||||||
|
(100 * file.length / (img.width * img.height * channels)).toFixed(1) + '% of raw)');
|
||||||
|
}
|
||||||
|
|
||||||
|
onmessage = (e) => {
|
||||||
|
const m = e.data.pluginMessage;
|
||||||
|
if (!m) return;
|
||||||
|
if (m.type === 'mode') tab(m.mode === 'export' ? 'export' : 'import');
|
||||||
|
if (m.type === 'selection')
|
||||||
|
$('sel').textContent = m.names.length ? 'Selected: ' + m.names.join(', ') : 'Nothing selected';
|
||||||
|
if (m.type === 'exported') exported(m.name, m.png).catch((err) => status(m.name + ': ' + err.message));
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
1430
extensions/figma/ui.html
Normal file
1430
extensions/figma/ui.html
Normal file
File diff suppressed because it is too large
Load diff
62
extensions/gimp/README.md
Normal file
62
extensions/gimp/README.md
Normal file
|
|
@ -0,0 +1,62 @@
|
||||||
|
# YAPF for GIMP
|
||||||
|
|
||||||
|
Open and export `.yapf` images in **GIMP 3.0 or later**.
|
||||||
|
|
||||||
|
- **File → Open** reads every `.yapf` file, version 1 and 2: Gray, Gray +
|
||||||
|
Alpha, RGB, RGBA in 8-bit, 16-bit, half and float precision; GPU blocks
|
||||||
|
(BC1–BC7, BC6H HDR) decoded for viewing; cube faces, array layers, volume
|
||||||
|
slices and animation frames as separate layers. When a file cannot be
|
||||||
|
opened, the message says why (damaged, truncated, not YAPF, newer format).
|
||||||
|
- **File → Export As… → `name.yapf`** writes the visible image (layers are
|
||||||
|
flattened into a copy; your document is untouched), with an optional
|
||||||
|
**Store mipmaps** checkbox for game engines.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
You need GIMP 3 with its development files (`gimptool`).
|
||||||
|
|
||||||
|
| System | Development files |
|
||||||
|
| :-------------- | :-------------------------------------------------- |
|
||||||
|
| Arch / Manjaro | `sudo pacman -S gimp` (includes them) |
|
||||||
|
| Debian / Ubuntu | `sudo apt install libgimp-3.0-dev` (GIMP 3 packages) |
|
||||||
|
| Fedora | `sudo dnf install gimp-devel` |
|
||||||
|
| NixOS | `nix-shell gimp-shell.nix` (in this folder), then build inside that shell |
|
||||||
|
| Windows | MSYS2: `pacman -S mingw-w64-ucrt-x86_64-gimp` |
|
||||||
|
| macOS | build GIMP 3 from source or use MacPorts/Homebrew |
|
||||||
|
|
||||||
|
Then, from this folder:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
gimptool --install yapf-gimp.c
|
||||||
|
```
|
||||||
|
|
||||||
|
That compiles the plugin (it pulls in `../../yapf.c` itself) and installs it
|
||||||
|
into your personal GIMP plug-ins folder. Restart GIMP.
|
||||||
|
|
||||||
|
On NixOS, build with the same nixpkgs channel your GIMP comes from (the
|
||||||
|
plugin links against that GIMP's libraries), and build it again after a
|
||||||
|
GIMP update. Your GIMP must be GIMP 3: `environment.systemPackages = [
|
||||||
|
pkgs.gimp3 ];`, or `nix-shell -p gimp3 --run gimp` to try it.
|
||||||
|
|
||||||
|
### Manual build
|
||||||
|
|
||||||
|
```sh
|
||||||
|
gcc -O2 $(pkg-config --cflags gimpui-3.0) yapf-gimp.c $(pkg-config --libs gimpui-3.0) -o yapf-gimp
|
||||||
|
```
|
||||||
|
|
||||||
|
Copy `yapf-gimp` to `<GIMP config>/plug-ins/yapf-gimp/yapf-gimp`, where the
|
||||||
|
GIMP config folder is `~/.config/GIMP/3.0` on Linux,
|
||||||
|
`%APPDATA%\GIMP\3.0` on Windows and
|
||||||
|
`~/Library/Application Support/GIMP/3.0` on macOS.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Pixels are saved with the sRGB flag, because GIMP's 8-bit buffers are
|
||||||
|
gamma encoded. Files marked linear load as linear.
|
||||||
|
- When a file has mipmaps, GIMP opens the full-size image.
|
||||||
|
- ETC2 / EAC / ASTC blocks cannot be shown yet. Export writes 8-bit
|
||||||
|
version 1 files, as before.
|
||||||
|
- Tested with GIMP 3.0.8 on Linux: opening matches the reference decoder
|
||||||
|
pixel for pixel, and export equals GIMP's own flattened PNG of the image.
|
||||||
|
The version 2 loader was tested in GIMP 3.0.6 (nixpkgs 25.05, headless):
|
||||||
|
BC7, BC5, BC6H, half float, 16-bit, cube, animation and a damaged file.
|
||||||
7
extensions/gimp/gimp-shell.nix
Normal file
7
extensions/gimp/gimp-shell.nix
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
# Development shell for building the plugin on NixOS: nix-shell gimp-shell.nix
|
||||||
|
# `gimp3` is GIMP 3 on every channel (on unstable it is an alias of `gimp`).
|
||||||
|
with import <nixpkgs> {};
|
||||||
|
mkShell {
|
||||||
|
nativeBuildInputs = [ pkg-config ];
|
||||||
|
buildInputs = [ gimp3 gimp3.dev gtk3 glib gdk-pixbuf gegl babl cairo pango ];
|
||||||
|
}
|
||||||
420
extensions/gimp/yapf-gimp.c
Normal file
420
extensions/gimp/yapf-gimp.c
Normal file
|
|
@ -0,0 +1,420 @@
|
||||||
|
/*
|
||||||
|
* yapf-gimp.c — YAPF format plugin for GIMP.
|
||||||
|
*
|
||||||
|
* This plugin is an independent work provided under the Apache License 2.0.
|
||||||
|
* It interfaces with GIMP, which is licensed under GPLv3. Users may use,
|
||||||
|
* modify, and distribute this plugin independently of GIMP's licensing terms.
|
||||||
|
*
|
||||||
|
* Copyright 2026 Nexoniarz
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*
|
||||||
|
* -------------------------------------------------------------------------
|
||||||
|
*
|
||||||
|
* Compilation (from this folder):
|
||||||
|
* gimptool --install yapf-gimp.c
|
||||||
|
*
|
||||||
|
* yapf.c (two folders up) is embedded via #include below because gimptool
|
||||||
|
* compiles a single source file. Do NOT link it separately.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <libgimp/gimp.h>
|
||||||
|
#include <libgimp/gimpui.h>
|
||||||
|
|
||||||
|
/* Pull in the entire encoder/decoder as a single translation unit. */
|
||||||
|
#include "../../yapf.c"
|
||||||
|
|
||||||
|
/* ── GIMP procedure names ────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
#define LOAD_PROC "file-yapf-load"
|
||||||
|
#define SAVE_PROC "file-yapf-save"
|
||||||
|
#define PLUG_IN_BINARY "yapf-gimp"
|
||||||
|
|
||||||
|
/* ── Plugin type boilerplate ─────────────────────────────────────────── */
|
||||||
|
|
||||||
|
struct _YapfPlugin { GimpPlugIn parent_instance; };
|
||||||
|
|
||||||
|
#define YAPF_PLUGIN_TYPE (yapf_plugin_get_type())
|
||||||
|
G_DECLARE_FINAL_TYPE(YapfPlugin, yapf_plugin, YAPF, PLUGIN, GimpPlugIn)
|
||||||
|
|
||||||
|
static GList *yapf_plugin_query_procedures (GimpPlugIn *plug_in);
|
||||||
|
static GimpProcedure *yapf_plugin_create_procedure (GimpPlugIn *plug_in,
|
||||||
|
const gchar *name);
|
||||||
|
|
||||||
|
static GimpValueArray *yapf_load_run (GimpProcedure *procedure,
|
||||||
|
GimpRunMode run_mode,
|
||||||
|
GFile *file,
|
||||||
|
GimpMetadata *metadata,
|
||||||
|
GimpMetadataLoadFlags *flags,
|
||||||
|
GimpProcedureConfig *config,
|
||||||
|
gpointer run_data);
|
||||||
|
static GimpValueArray *yapf_save_run (GimpProcedure *procedure,
|
||||||
|
GimpRunMode run_mode,
|
||||||
|
GimpImage *image,
|
||||||
|
GFile *file,
|
||||||
|
GimpExportOptions *options,
|
||||||
|
GimpMetadata *metadata,
|
||||||
|
GimpProcedureConfig *config,
|
||||||
|
gpointer run_data);
|
||||||
|
|
||||||
|
G_DEFINE_TYPE(YapfPlugin, yapf_plugin, GIMP_TYPE_PLUG_IN)
|
||||||
|
|
||||||
|
static void yapf_plugin_class_init(YapfPluginClass *klass) {
|
||||||
|
GimpPlugInClass *plug_in_class = GIMP_PLUG_IN_CLASS(klass);
|
||||||
|
plug_in_class->query_procedures = yapf_plugin_query_procedures;
|
||||||
|
plug_in_class->create_procedure = yapf_plugin_create_procedure;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void yapf_plugin_init(YapfPlugin *plugin) { (void)plugin; }
|
||||||
|
|
||||||
|
/* ── Procedure list ──────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
static GList *yapf_plugin_query_procedures(GimpPlugIn *plug_in) {
|
||||||
|
(void)plug_in;
|
||||||
|
GList *list = NULL;
|
||||||
|
list = g_list_append(list, g_strdup(LOAD_PROC));
|
||||||
|
list = g_list_append(list, g_strdup(SAVE_PROC));
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Procedure creation ──────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
static GimpProcedure *yapf_plugin_create_procedure(GimpPlugIn *plug_in,
|
||||||
|
const gchar *name) {
|
||||||
|
GimpProcedure *procedure = NULL;
|
||||||
|
|
||||||
|
if (g_strcmp0(name, LOAD_PROC) == 0) {
|
||||||
|
procedure = gimp_load_procedure_new(plug_in, name,
|
||||||
|
GIMP_PDB_PROC_TYPE_PLUGIN,
|
||||||
|
yapf_load_run, NULL, NULL);
|
||||||
|
gimp_procedure_set_menu_label(procedure, "YAPF image");
|
||||||
|
gimp_procedure_set_documentation(procedure,
|
||||||
|
"Loads a YAPF (Yet Another Picture Format) image",
|
||||||
|
"Loads a YAPF image from disk", NULL);
|
||||||
|
gimp_procedure_set_attribution(procedure,
|
||||||
|
"Nexoniarz", "Nexoniarz", "2026");
|
||||||
|
gimp_file_procedure_set_mime_types(GIMP_FILE_PROCEDURE(procedure),
|
||||||
|
"image/x-yapf");
|
||||||
|
gimp_file_procedure_set_extensions(GIMP_FILE_PROCEDURE(procedure),
|
||||||
|
"yapf");
|
||||||
|
gimp_file_procedure_set_magics(GIMP_FILE_PROCEDURE(procedure),
|
||||||
|
"0,string,YAPF");
|
||||||
|
|
||||||
|
} else if (g_strcmp0(name, SAVE_PROC) == 0) {
|
||||||
|
procedure = gimp_export_procedure_new(plug_in, name,
|
||||||
|
GIMP_PDB_PROC_TYPE_PLUGIN, TRUE,
|
||||||
|
(GimpRunExportFunc)yapf_save_run, NULL, NULL);
|
||||||
|
gimp_procedure_set_image_types(procedure, "RGB, RGBA, GRAY, GRAYA");
|
||||||
|
gimp_procedure_set_menu_label(procedure, "YAPF image");
|
||||||
|
gimp_procedure_set_documentation(procedure,
|
||||||
|
"Exports a YAPF (Yet Another Picture Format) image",
|
||||||
|
"Exports a YAPF image to disk", NULL);
|
||||||
|
gimp_procedure_set_attribution(procedure,
|
||||||
|
"Nexoniarz", "Nexoniarz", "2026");
|
||||||
|
gimp_file_procedure_set_mime_types(GIMP_FILE_PROCEDURE(procedure),
|
||||||
|
"image/x-yapf");
|
||||||
|
gimp_file_procedure_set_extensions(GIMP_FILE_PROCEDURE(procedure),
|
||||||
|
"yapf");
|
||||||
|
/* GIMP flattens / converts a copy of the image for us as needed. */
|
||||||
|
gimp_export_procedure_set_capabilities(GIMP_EXPORT_PROCEDURE(procedure),
|
||||||
|
GIMP_EXPORT_CAN_HANDLE_RGB | GIMP_EXPORT_CAN_HANDLE_GRAY |
|
||||||
|
GIMP_EXPORT_CAN_HANDLE_ALPHA, NULL, NULL, NULL);
|
||||||
|
gimp_procedure_add_boolean_argument(procedure, "mipmaps",
|
||||||
|
"Store _mipmaps",
|
||||||
|
"Also store a full mip chain (for game engines and GPUs)",
|
||||||
|
FALSE, G_PARAM_READWRITE);
|
||||||
|
}
|
||||||
|
|
||||||
|
return procedure;
|
||||||
|
}
|
||||||
|
|
||||||
|
static GimpValueArray *yapf_error(GimpProcedure *procedure, const gchar *msg);
|
||||||
|
|
||||||
|
/* ── Load handler ────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
/* Why a file could not be opened, in words. */
|
||||||
|
static gchar *yapf_load_reason(const gchar *path, int err) {
|
||||||
|
FILE *f = fopen(path, "rb");
|
||||||
|
uint8_t h[8] = {0};
|
||||||
|
size_t n = f ? fread(h, 1, sizeof(h), f) : 0;
|
||||||
|
if (f) fclose(f);
|
||||||
|
if (!f || err == YAPF_ERR_IO)
|
||||||
|
return g_strdup("The file cannot be read.");
|
||||||
|
if (n < 5 || memcmp(h, "YAPF", 4) != 0)
|
||||||
|
return g_strdup("This is not a YAPF file (it does not start with \"YAPF\").");
|
||||||
|
if (h[4] != 1 && h[4] != 2)
|
||||||
|
return g_strdup_printf("This YAPF file is format version %u; this plug-in reads "
|
||||||
|
"versions 1 and 2. Update the plug-in.", h[4]);
|
||||||
|
switch (err) {
|
||||||
|
case YAPF_ERR_UNSUPPORTED:
|
||||||
|
return g_strdup("This YAPF file needs a feature this plug-in does not have yet. "
|
||||||
|
"Update the plug-in.");
|
||||||
|
case YAPF_ERR_CHECKSUM:
|
||||||
|
return g_strdup("This YAPF file is damaged: a checksum does not match.");
|
||||||
|
case YAPF_ERR_TRUNCATED:
|
||||||
|
return g_strdup("This YAPF file is incomplete (truncated).");
|
||||||
|
case YAPF_ERR_OOM:
|
||||||
|
return g_strdup("Out of memory.");
|
||||||
|
default:
|
||||||
|
return g_strdup_printf("This YAPF file (version %u) is corrupt, or was written by a "
|
||||||
|
"tool that does not follow the YAPF specification.", h[4]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static const gchar *yapf_face_name(uint32_t f) {
|
||||||
|
static const gchar *n[6] = { "+X", "-X", "+Y", "-Y", "+Z", "-Z" };
|
||||||
|
return f < 6 ? n[f] : "?";
|
||||||
|
}
|
||||||
|
|
||||||
|
static GimpValueArray *yapf_load_run(
|
||||||
|
GimpProcedure *procedure,
|
||||||
|
GimpRunMode run_mode,
|
||||||
|
GFile *file,
|
||||||
|
GimpMetadata *metadata,
|
||||||
|
GimpMetadataLoadFlags *flags,
|
||||||
|
GimpProcedureConfig *config,
|
||||||
|
gpointer run_data)
|
||||||
|
{
|
||||||
|
(void)run_mode; (void)metadata; (void)flags; (void)config; (void)run_data;
|
||||||
|
|
||||||
|
gchar *path = g_file_get_path(file);
|
||||||
|
int err = 0;
|
||||||
|
yapf_texture_t *t = yapf_tex_load(path, 0, 0, &err); /* v1 and v2, all cores */
|
||||||
|
if (!t) {
|
||||||
|
gchar *why = yapf_load_reason(path, err);
|
||||||
|
g_free(path);
|
||||||
|
GError *e = g_error_new(GIMP_PLUG_IN_ERROR, 0, "Failed to load YAPF file: %s", why);
|
||||||
|
g_free(why);
|
||||||
|
return gimp_procedure_new_return_values(procedure, GIMP_PDB_EXECUTION_ERROR, e);
|
||||||
|
}
|
||||||
|
g_free(path);
|
||||||
|
|
||||||
|
/* What the pixels look like after decoding: GPU blocks decode to
|
||||||
|
* RGBA 8-bit, BC6H to RGB half float. */
|
||||||
|
gboolean blocks = t->payload == YAPF_PAYLOAD_BLOCKS;
|
||||||
|
gboolean bc6 = blocks && (t->format == YAPF_GPU_BC6H || t->format == YAPF_GPU_BC6H_SF);
|
||||||
|
if (blocks && t->format >= YAPF_GPU_ETC2_RGB8) {
|
||||||
|
yapf_tex_free(t);
|
||||||
|
return yapf_error(procedure, "Failed to load YAPF file: it holds ETC2 / EAC / ASTC "
|
||||||
|
"GPU blocks, which this plug-in cannot show yet.");
|
||||||
|
}
|
||||||
|
int ch = blocks ? (bc6 ? 3 : 4) : t->channels;
|
||||||
|
int sample = blocks ? (bc6 ? YAPF_SAMPLE_F16 : YAPF_SAMPLE_U8) : t->sample;
|
||||||
|
/* sRGB data is gamma encoded; floats are always linear */
|
||||||
|
gboolean gamma = (t->flags & YAPF_FLAG_SRGB) &&
|
||||||
|
(sample == YAPF_SAMPLE_U8 || sample == YAPF_SAMPLE_U16);
|
||||||
|
|
||||||
|
static const gchar *chn[2][4] = { { "Y", "YA", "RGB", "RGBA" }, { "Y'", "Y'A", "R'G'B'", "R'G'B'A" } };
|
||||||
|
static const gchar *typ[4] = { "u8", "u16", "half", "float" };
|
||||||
|
gchar *fmt_name = g_strdup_printf("%s %s", chn[gamma ? 1 : 0][ch - 1], typ[sample]);
|
||||||
|
const Babl *fmt = babl_format(fmt_name);
|
||||||
|
g_free(fmt_name);
|
||||||
|
|
||||||
|
GimpPrecision prec;
|
||||||
|
switch (sample) {
|
||||||
|
case YAPF_SAMPLE_U16: prec = gamma ? GIMP_PRECISION_U16_NON_LINEAR : GIMP_PRECISION_U16_LINEAR; break;
|
||||||
|
case YAPF_SAMPLE_F16: prec = GIMP_PRECISION_HALF_LINEAR; break;
|
||||||
|
case YAPF_SAMPLE_F32: prec = GIMP_PRECISION_FLOAT_LINEAR; break;
|
||||||
|
default: prec = gamma ? GIMP_PRECISION_U8_NON_LINEAR : GIMP_PRECISION_U8_LINEAR; break;
|
||||||
|
}
|
||||||
|
GimpImageBaseType base = ch <= 2 ? GIMP_GRAY : GIMP_RGB;
|
||||||
|
GimpImageType ltype = ch == 1 ? GIMP_GRAY_IMAGE : ch == 2 ? GIMP_GRAYA_IMAGE
|
||||||
|
: ch == 3 ? GIMP_RGB_IMAGE : GIMP_RGBA_IMAGE;
|
||||||
|
|
||||||
|
/* Mip 0 of every frame, layer, face and slice becomes one GIMP layer
|
||||||
|
* (the first at the bottom, as GIMP animations expect). */
|
||||||
|
uint32_t w, h, d, faces = yapf_tex_faces(t);
|
||||||
|
yapf_tex_mip_size(t, 0, &w, &h, &d);
|
||||||
|
GimpImage *image = gimp_image_new_with_precision(w, h, base, prec);
|
||||||
|
size_t texel = (size_t)ch * yapf_sample_bytes((uint8_t)sample);
|
||||||
|
void *tmp = blocks ? g_malloc((size_t)w * h * texel) : NULL;
|
||||||
|
gint count = (gint)(t->frames * t->layers * faces * d);
|
||||||
|
|
||||||
|
for (uint32_t fr = 0; fr < t->frames; fr++)
|
||||||
|
for (uint32_t ly = 0; ly < t->layers; ly++)
|
||||||
|
for (uint32_t fc = 0; fc < faces; fc++)
|
||||||
|
for (uint32_t z = 0; z < d; z++) {
|
||||||
|
uint32_t s = yapf_tex_surface(t, fr, ly, fc, 0);
|
||||||
|
size_t slice = yapf_tex_surface_bytes(t, 0) / d;
|
||||||
|
const uint8_t *src = (const uint8_t *)t->data[s] + slice * z;
|
||||||
|
if (blocks) {
|
||||||
|
yapf_blocks_decode(t->format, src, w, h, tmp);
|
||||||
|
src = (const uint8_t *)tmp;
|
||||||
|
}
|
||||||
|
GString *name = g_string_new(NULL);
|
||||||
|
if (t->frames > 1) {
|
||||||
|
g_string_append_printf(name, "Frame %u", fr + 1);
|
||||||
|
if (t->frame_us)
|
||||||
|
g_string_append_printf(name, " (%ums)", (t->frame_us[fr] + 500) / 1000);
|
||||||
|
}
|
||||||
|
if (t->layers > 1) g_string_append_printf(name, "%sLayer %u", name->len ? ", " : "", ly);
|
||||||
|
if (faces > 1) g_string_append_printf(name, "%sFace %s", name->len ? ", " : "", yapf_face_name(fc));
|
||||||
|
if (d > 1) g_string_append_printf(name, "%sSlice %u", name->len ? ", " : "", z);
|
||||||
|
if (!name->len) g_string_append(name, "Background");
|
||||||
|
|
||||||
|
GimpLayer *layer = gimp_layer_new(image, name->str, (gint)w, (gint)h, ltype,
|
||||||
|
100.0, gimp_image_get_default_new_layer_mode(image));
|
||||||
|
g_string_free(name, TRUE);
|
||||||
|
gimp_image_insert_layer(image, layer, NULL, 0);
|
||||||
|
if (count > 1 && !(fr == 0 && ly == 0 && fc == 0 && z == 0))
|
||||||
|
gimp_item_set_visible(GIMP_ITEM(layer), FALSE);
|
||||||
|
GeglBuffer *buffer = gimp_drawable_get_buffer(GIMP_DRAWABLE(layer));
|
||||||
|
gegl_buffer_set(buffer, GEGL_RECTANGLE(0, 0, (gint)w, (gint)h), 0, fmt,
|
||||||
|
src, GEGL_AUTO_ROWSTRIDE);
|
||||||
|
g_object_unref(buffer);
|
||||||
|
}
|
||||||
|
g_free(tmp);
|
||||||
|
yapf_tex_free(t);
|
||||||
|
|
||||||
|
GimpValueArray *ret = gimp_procedure_new_return_values(procedure,
|
||||||
|
GIMP_PDB_SUCCESS, NULL);
|
||||||
|
g_value_set_object(gimp_value_array_index(ret, 1), image);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ── Save handler ────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
/* Full mip chain with a 2×2 box filter (edges clamp for odd sizes).
|
||||||
|
* Fills img->mips / img->mip_levels; mips[0] aliases img->pixels. */
|
||||||
|
static gboolean yapf_build_mips(yapf_image_t *img) {
|
||||||
|
int levels = 1;
|
||||||
|
while (levels < (int)YAPF_MAX_MIPS &&
|
||||||
|
((img->width >> levels) > 0 || (img->height >> levels) > 0))
|
||||||
|
levels++;
|
||||||
|
img->mips = (uint8_t **)calloc((size_t)levels, sizeof(uint8_t *));
|
||||||
|
if (!img->mips) return FALSE;
|
||||||
|
img->mips[0] = img->pixels;
|
||||||
|
img->mip_levels = (uint8_t)levels;
|
||||||
|
|
||||||
|
int ch = img->channels;
|
||||||
|
for (int m = 1; m < levels; m++) {
|
||||||
|
uint32_t pw = MAX(1u, img->width >> (m - 1)), ph = MAX(1u, img->height >> (m - 1));
|
||||||
|
uint32_t w = MAX(1u, img->width >> m), h = MAX(1u, img->height >> m);
|
||||||
|
const uint8_t *src = img->mips[m - 1];
|
||||||
|
uint8_t *dst = (uint8_t *)malloc((size_t)w * h * ch);
|
||||||
|
if (!dst) return FALSE;
|
||||||
|
for (uint32_t y = 0; y < h; y++)
|
||||||
|
for (uint32_t x = 0; x < w; x++)
|
||||||
|
for (int c = 0; c < ch; c++) {
|
||||||
|
uint32_t x0 = MIN(2 * x, pw - 1), x1 = MIN(2 * x + 1, pw - 1);
|
||||||
|
uint32_t y0 = MIN(2 * y, ph - 1), y1 = MIN(2 * y + 1, ph - 1);
|
||||||
|
unsigned sum = src[((size_t)y0 * pw + x0) * ch + c] + src[((size_t)y0 * pw + x1) * ch + c]
|
||||||
|
+ src[((size_t)y1 * pw + x0) * ch + c] + src[((size_t)y1 * pw + x1) * ch + c];
|
||||||
|
dst[((size_t)y * w + x) * ch + c] = (uint8_t)((sum + 2) / 4);
|
||||||
|
}
|
||||||
|
img->mips[m] = dst;
|
||||||
|
}
|
||||||
|
return TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
static GimpValueArray *yapf_error(GimpProcedure *procedure, const gchar *msg) {
|
||||||
|
return gimp_procedure_new_return_values(procedure, GIMP_PDB_EXECUTION_ERROR,
|
||||||
|
g_error_new_literal(GIMP_PLUG_IN_ERROR, 0, msg));
|
||||||
|
}
|
||||||
|
|
||||||
|
static GimpValueArray *yapf_save_run(
|
||||||
|
GimpProcedure *procedure,
|
||||||
|
GimpRunMode run_mode,
|
||||||
|
GimpImage *image,
|
||||||
|
GFile *file,
|
||||||
|
GimpExportOptions *options,
|
||||||
|
GimpMetadata *metadata,
|
||||||
|
GimpProcedureConfig *config,
|
||||||
|
gpointer run_data)
|
||||||
|
{
|
||||||
|
(void)metadata; (void)run_data;
|
||||||
|
|
||||||
|
if (run_mode == GIMP_RUN_INTERACTIVE) {
|
||||||
|
gimp_ui_init(PLUG_IN_BINARY);
|
||||||
|
GtkWidget *dialog = gimp_export_procedure_dialog_new(
|
||||||
|
GIMP_EXPORT_PROCEDURE(procedure), config, image);
|
||||||
|
gimp_procedure_dialog_fill(GIMP_PROCEDURE_DIALOG(dialog), NULL);
|
||||||
|
gboolean ok = gimp_procedure_dialog_run(GIMP_PROCEDURE_DIALOG(dialog));
|
||||||
|
gtk_widget_destroy(dialog);
|
||||||
|
if (!ok)
|
||||||
|
return gimp_procedure_new_return_values(procedure, GIMP_PDB_CANCEL, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
gboolean mipmaps = FALSE;
|
||||||
|
g_object_get(config, "mipmaps", &mipmaps, NULL);
|
||||||
|
|
||||||
|
/* A flattened copy when the image has several layers; the visible
|
||||||
|
* result is what gets exported. */
|
||||||
|
GimpExportReturn export = gimp_export_options_get_image(options, &image);
|
||||||
|
GList *layers = gimp_image_list_layers(image);
|
||||||
|
if (!layers) {
|
||||||
|
if (export == GIMP_EXPORT_EXPORT) gimp_image_delete(image);
|
||||||
|
return yapf_error(procedure, "No layers to export.");
|
||||||
|
}
|
||||||
|
GimpDrawable *drawable = GIMP_DRAWABLE(layers->data);
|
||||||
|
g_list_free(layers);
|
||||||
|
|
||||||
|
gboolean has_alpha = gimp_drawable_has_alpha(drawable);
|
||||||
|
gboolean is_gray = gimp_drawable_is_gray(drawable);
|
||||||
|
const gchar *babl_fmt;
|
||||||
|
uint8_t ch, gpu_fmt;
|
||||||
|
|
||||||
|
/* GIMP's 8-bit buffers are gamma encoded, so pixels are written as sRGB. */
|
||||||
|
if (is_gray) {
|
||||||
|
ch = has_alpha ? YAPF_CHANNELS_GRAY_ALPHA : YAPF_CHANNELS_GRAY;
|
||||||
|
babl_fmt = has_alpha ? "Y'A u8" : "Y' u8";
|
||||||
|
gpu_fmt = has_alpha ? YAPF_GPU_RG8 : YAPF_GPU_R8;
|
||||||
|
} else {
|
||||||
|
ch = has_alpha ? YAPF_CHANNELS_RGBA : YAPF_CHANNELS_RGB;
|
||||||
|
babl_fmt = has_alpha ? "R'G'B'A u8" : "R'G'B' u8";
|
||||||
|
gpu_fmt = has_alpha ? YAPF_GPU_SRGB8_A8 : YAPF_GPU_SRGB8;
|
||||||
|
}
|
||||||
|
|
||||||
|
gint w = gimp_drawable_get_width(drawable);
|
||||||
|
gint h = gimp_drawable_get_height(drawable);
|
||||||
|
uint8_t *pixels = (uint8_t *)malloc((size_t)w * (size_t)h * ch);
|
||||||
|
if (!pixels) {
|
||||||
|
if (export == GIMP_EXPORT_EXPORT) gimp_image_delete(image);
|
||||||
|
return yapf_error(procedure, "Out of memory.");
|
||||||
|
}
|
||||||
|
|
||||||
|
GeglBuffer *buffer = gimp_drawable_get_buffer(drawable);
|
||||||
|
gegl_buffer_get(buffer, GEGL_RECTANGLE(0, 0, w, h), 1.0,
|
||||||
|
babl_format(babl_fmt), pixels,
|
||||||
|
GEGL_AUTO_ROWSTRIDE, GEGL_ABYSS_NONE);
|
||||||
|
g_object_unref(buffer);
|
||||||
|
if (export == GIMP_EXPORT_EXPORT) gimp_image_delete(image);
|
||||||
|
|
||||||
|
yapf_image_t img;
|
||||||
|
memset(&img, 0, sizeof(img));
|
||||||
|
img.width = (uint32_t)w;
|
||||||
|
img.height = (uint32_t)h;
|
||||||
|
img.channels = ch;
|
||||||
|
img.gpu_format = gpu_fmt;
|
||||||
|
img.flags = YAPF_FLAG_SRGB;
|
||||||
|
img.mip_levels = 1;
|
||||||
|
img.pixels = pixels;
|
||||||
|
|
||||||
|
int result = YAPF_ERR_OOM;
|
||||||
|
if (!mipmaps || yapf_build_mips(&img)) {
|
||||||
|
gchar *path = g_file_get_path(file);
|
||||||
|
result = yapf_save(path, &img);
|
||||||
|
g_free(path);
|
||||||
|
}
|
||||||
|
if (img.mips) {
|
||||||
|
for (int m = 1; m < img.mip_levels; m++) free(img.mips[m]);
|
||||||
|
free(img.mips);
|
||||||
|
}
|
||||||
|
free(pixels);
|
||||||
|
|
||||||
|
if (result != YAPF_OK)
|
||||||
|
return yapf_error(procedure, "Failed to export YAPF file.");
|
||||||
|
return gimp_procedure_new_return_values(procedure, GIMP_PDB_SUCCESS, NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
GIMP_MAIN(YAPF_PLUGIN_TYPE)
|
||||||
26
extensions/kde/CMakeLists.txt
Normal file
26
extensions/kde/CMakeLists.txt
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
# YAPF thumbnails for KDE Dolphin (KF6 KIO thumbnail plugin).
|
||||||
|
# cmake -B build -DCMAKE_BUILD_TYPE=Release
|
||||||
|
# cmake --build build
|
||||||
|
# sudo cmake --install build (or see README.md for a per-user install)
|
||||||
|
cmake_minimum_required(VERSION 3.16)
|
||||||
|
project(yapfthumbnail LANGUAGES C CXX)
|
||||||
|
|
||||||
|
find_package(ECM 6.0 REQUIRED NO_MODULE)
|
||||||
|
set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH})
|
||||||
|
include(KDEInstallDirs)
|
||||||
|
include(KDECMakeSettings)
|
||||||
|
include(KDECompilerSettings NO_POLICY_SCOPE)
|
||||||
|
|
||||||
|
find_package(Qt6 6.5 REQUIRED COMPONENTS Core Gui)
|
||||||
|
find_package(KF6 6.0 REQUIRED COMPONENTS KIO)
|
||||||
|
find_package(Threads REQUIRED)
|
||||||
|
|
||||||
|
kcoreaddons_add_plugin(yapfthumbnail
|
||||||
|
SOURCES yapfthumbnail.cpp ../../yapf.c
|
||||||
|
INSTALL_NAMESPACE "kf6/thumbcreator")
|
||||||
|
set_source_files_properties(../../yapf.c PROPERTIES LANGUAGE C)
|
||||||
|
target_include_directories(yapfthumbnail PRIVATE ../..)
|
||||||
|
target_link_libraries(yapfthumbnail PRIVATE KF6::KIOGui Qt6::Gui Threads::Threads)
|
||||||
|
|
||||||
|
# Register the image/x-yapf file type so Dolphin recognises .yapf files.
|
||||||
|
install(FILES ../linux/yapf-mime.xml DESTINATION ${KDE_INSTALL_MIMEDIR} RENAME yapf.xml)
|
||||||
56
extensions/kde/README.md
Normal file
56
extensions/kde/README.md
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
# YAPF thumbnails for KDE (Dolphin)
|
||||||
|
|
||||||
|
Shows previews of `.yapf` files in **Dolphin**, the KDE file dialogs and
|
||||||
|
anything else that uses KIO thumbnails. KDE Frameworks 6 / Plasma 6.
|
||||||
|
Files with mipmaps are thumbnailed from the nearest mip level, so even huge
|
||||||
|
textures preview instantly.
|
||||||
|
|
||||||
|
## Build and install
|
||||||
|
|
||||||
|
You need CMake, a C/C++ compiler and the KDE development packages:
|
||||||
|
|
||||||
|
| System | Packages |
|
||||||
|
| :-------------- | :------- |
|
||||||
|
| Arch / Manjaro | `sudo pacman -S cmake extra-cmake-modules kio` |
|
||||||
|
| Debian / Ubuntu | `sudo apt install cmake extra-cmake-modules libkf6kio-dev qt6-base-dev` |
|
||||||
|
| Fedora | `sudo dnf install cmake extra-cmake-modules kf6-kio-devel qt6-qtbase-devel` |
|
||||||
|
| openSUSE | `sudo zypper install cmake kf6-extra-cmake-modules kf6-kio-devel qt6-base-devel` |
|
||||||
|
| NixOS | `nix-shell kde-shell.nix` (in this folder) |
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd extensions/kde
|
||||||
|
cmake -B build -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr
|
||||||
|
cmake --build build
|
||||||
|
sudo cmake --install build
|
||||||
|
update-mime-database /usr/share/mime
|
||||||
|
```
|
||||||
|
|
||||||
|
This installs the plugin (`kf6/thumbcreator/yapfthumbnail.so`) and the
|
||||||
|
`image/x-yapf` file type. Then in Dolphin: **Settings → Configure Dolphin →
|
||||||
|
Interface → Previews**, tick **YAPF Images**, and turn previews on.
|
||||||
|
|
||||||
|
### Without root (per user, also NixOS)
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cmake -B build -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=$HOME/.local
|
||||||
|
cmake --build build && cmake --install build
|
||||||
|
update-mime-database ~/.local/share/mime
|
||||||
|
mkdir -p ~/.config/plasma-workspace/env
|
||||||
|
echo 'export QT_PLUGIN_PATH="$HOME/.local/lib/plugins:$QT_PLUGIN_PATH"' > ~/.config/plasma-workspace/env/yapf.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Log out and back in, then enable **YAPF Images** in Dolphin's preview settings.
|
||||||
|
(Some distributions use `lib64/plugins` or `lib/qt6/plugins`; `cmake
|
||||||
|
--install` prints the exact folder.)
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Built against KF6 / Qt 6 and tested by loading the plugin through KDE's
|
||||||
|
plugin factory and creating thumbnails (RGBA, RGB, mipmapped, wide images;
|
||||||
|
corrupt files are rejected). Not yet tried inside a running Dolphin.
|
||||||
|
|
||||||
|
Reads YAPF version 1 and 2: GPU blocks (BC1–BC7, BC6H), 16-bit and float
|
||||||
|
textures, cubes (face +X), arrays, volumes and animations (the first
|
||||||
|
image). Only the mip level the thumbnail needs is decoded. Tested with
|
||||||
|
KF6 from nixpkgs 25.05 on BC7, BC5, BC6H, half float, 16-bit, cube and
|
||||||
|
animation files. ETC2 / ASTC files get no thumbnail.
|
||||||
6
extensions/kde/kde-shell.nix
Normal file
6
extensions/kde/kde-shell.nix
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
# Development shell for building the plugin on NixOS: nix-shell kde-shell.nix
|
||||||
|
with import <nixpkgs> {};
|
||||||
|
mkShell {
|
||||||
|
nativeBuildInputs = [ cmake kdePackages.extra-cmake-modules pkg-config ];
|
||||||
|
buildInputs = [ kdePackages.kio kdePackages.qtbase kdePackages.kcoreaddons ];
|
||||||
|
}
|
||||||
80
extensions/kde/yapfthumbnail.cpp
Normal file
80
extensions/kde/yapfthumbnail.cpp
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
// YAPF thumbnails for KDE Dolphin and other KIO file dialogs.
|
||||||
|
// Copyright 2026 Nexoniarz — Apache License 2.0.
|
||||||
|
|
||||||
|
#include <KIO/ThumbnailCreator>
|
||||||
|
#include <KPluginFactory>
|
||||||
|
#include <QFile>
|
||||||
|
#include <QImage>
|
||||||
|
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
#include "yapf.h"
|
||||||
|
}
|
||||||
|
|
||||||
|
class YapfThumbnailCreator : public KIO::ThumbnailCreator
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
public:
|
||||||
|
YapfThumbnailCreator(QObject *parent, const QVariantList &args)
|
||||||
|
: KIO::ThumbnailCreator(parent, args)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
KIO::ThumbnailResult create(const KIO::ThumbnailRequest &request) override
|
||||||
|
{
|
||||||
|
QFile file(request.url().toLocalFile());
|
||||||
|
if (!file.open(QIODevice::ReadOnly)) {
|
||||||
|
return KIO::ThumbnailResult::fail();
|
||||||
|
}
|
||||||
|
const QByteArray data = file.readAll();
|
||||||
|
|
||||||
|
// Version 1 and 2: read the tables, then decode only the mip we
|
||||||
|
// need (frame 0, layer 0, face 0, slice 0).
|
||||||
|
int err = 0;
|
||||||
|
yapf_texture_t *tex = yapf_tex_open(data.constData(), size_t(data.size()), 0, &err);
|
||||||
|
if (!tex) {
|
||||||
|
return KIO::ThumbnailResult::fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
// The smallest stored mip that still covers the requested size;
|
||||||
|
// Qt scales the rest.
|
||||||
|
const QSize want = request.targetSize();
|
||||||
|
uint32_t m = 0, w = 0, h = 0;
|
||||||
|
while (m + 1 < tex->mip_levels) {
|
||||||
|
yapf_tex_mip_size(tex, m + 1, &w, &h, nullptr);
|
||||||
|
if (int(w) < want.width() || int(h) < want.height()) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
++m;
|
||||||
|
}
|
||||||
|
yapf_tex_mip_size(tex, m, &w, &h, nullptr);
|
||||||
|
const uint32_t s = yapf_tex_surface(tex, 0, 0, 0, m);
|
||||||
|
|
||||||
|
QImage image(int(w), int(h), (tex->flags & YAPF_FLAG_PREMULT_ALPHA) ? QImage::Format_RGBA8888_Premultiplied
|
||||||
|
: QImage::Format_RGBA8888);
|
||||||
|
bool ok = !image.isNull() &&
|
||||||
|
yapf_tex_decode_surface(tex, data.constData(), size_t(data.size()), s, 0) == YAPF_OK;
|
||||||
|
if (ok) {
|
||||||
|
// RGBA 8-bit rows (blocks decoded, 16-bit / float converted)
|
||||||
|
QByteArray rgba(int(size_t(w) * h * 4), Qt::Uninitialized);
|
||||||
|
ok = yapf_tex_to_rgba8(tex, s, 0, reinterpret_cast<uint8_t *>(rgba.data())) == YAPF_OK;
|
||||||
|
for (int y = 0; ok && y < int(h); ++y) {
|
||||||
|
memcpy(image.scanLine(y), rgba.constData() + size_t(y) * w * 4, size_t(w) * 4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
yapf_tex_free(tex);
|
||||||
|
if (!ok) {
|
||||||
|
return KIO::ThumbnailResult::fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (image.width() > want.width() || image.height() > want.height()) {
|
||||||
|
image = image.scaled(want, Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
||||||
|
}
|
||||||
|
return KIO::ThumbnailResult::pass(image);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
K_PLUGIN_CLASS_WITH_JSON(YapfThumbnailCreator, "yapfthumbnail.json")
|
||||||
|
|
||||||
|
#include "yapfthumbnail.moc"
|
||||||
7
extensions/kde/yapfthumbnail.json
Normal file
7
extensions/kde/yapfthumbnail.json
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"CacheThumbnail": true,
|
||||||
|
"KPlugin": {
|
||||||
|
"MimeTypes": ["image/x-yapf"],
|
||||||
|
"Name": "YAPF Images"
|
||||||
|
}
|
||||||
|
}
|
||||||
34
extensions/linux/README.md
Normal file
34
extensions/linux/README.md
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
# Linux desktop integration
|
||||||
|
|
||||||
|
Makes file managers recognise `.yapf` files (MIME type `image/x-yapf`) and
|
||||||
|
show **thumbnails** for them.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sh extensions/linux/install.sh # for your user
|
||||||
|
sudo sh extensions/linux/install.sh --system
|
||||||
|
```
|
||||||
|
|
||||||
|
Thumbnails are made by `yapf thumbnail` (from the `yapf` tool, which must be
|
||||||
|
on your `PATH` — `sudo make install` puts it in `/usr/local/bin`). They work
|
||||||
|
in file managers that use freedesktop thumbnailers: GNOME Files (Nautilus),
|
||||||
|
Nemo, Caja, and Thunar (with tumbler). KDE Dolphin uses its own plugin
|
||||||
|
system and is not covered yet.
|
||||||
|
|
||||||
|
Thumbnails cover every YAPF file the tool reads: version 1 and 2, GPU
|
||||||
|
blocks (BC1–BC7, BC6H), 16-bit and float textures, and the first image of
|
||||||
|
cubes, arrays and animations. KDE Dolphin: see [../kde](../kde).
|
||||||
|
|
||||||
|
### NixOS
|
||||||
|
|
||||||
|
`/usr/share` is not used on NixOS. Install the package from the
|
||||||
|
repository's `default.nix` instead — it carries the MIME type and a
|
||||||
|
thumbnailer that calls `yapf` by its full store path:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
# configuration.nix
|
||||||
|
environment.systemPackages = [ (pkgs.callPackage /path/to/YAPF { }) ];
|
||||||
|
environment.pathsToLink = [ "/share/thumbnailers" ]; # if your desktop does not link it already
|
||||||
|
```
|
||||||
|
|
||||||
|
For your user only: `nix-env -if /path/to/YAPF`, then
|
||||||
|
`sh extensions/linux/install.sh` (the per-user install).
|
||||||
18
extensions/linux/install.sh
Executable file
18
extensions/linux/install.sh
Executable file
|
|
@ -0,0 +1,18 @@
|
||||||
|
#!/bin/sh
|
||||||
|
# Registers the YAPF MIME type and thumbnailer for the current user, so file
|
||||||
|
# managers that use freedesktop thumbnailers (GNOME Files, Nemo, Caja,
|
||||||
|
# Thunar with tumbler) show .yapf previews. Needs `yapf` on your PATH.
|
||||||
|
# sh extensions/linux/install.sh (current user)
|
||||||
|
# sudo sh extensions/linux/install.sh --system
|
||||||
|
set -e
|
||||||
|
here=$(cd "$(dirname "$0")" && pwd)
|
||||||
|
if [ "$1" = "--system" ]; then data=/usr/share; else data=${XDG_DATA_HOME:-$HOME/.local/share}; fi
|
||||||
|
|
||||||
|
command -v yapf >/dev/null 2>&1 || echo "warning: 'yapf' is not on your PATH; thumbnails need it" >&2
|
||||||
|
|
||||||
|
mkdir -p "$data/mime/packages" "$data/thumbnailers"
|
||||||
|
cp "$here/yapf-mime.xml" "$data/mime/packages/yapf.xml"
|
||||||
|
cp "$here/yapf.thumbnailer" "$data/thumbnailers/yapf.thumbnailer"
|
||||||
|
update-mime-database "$data/mime" >/dev/null 2>&1 || true
|
||||||
|
echo "Installed the image/x-yapf MIME type and thumbnailer into $data"
|
||||||
|
echo "Restart your file manager (e.g. 'nautilus -q') and clear ~/.cache/thumbnails/fail if needed."
|
||||||
12
extensions/linux/yapf-mime.xml
Normal file
12
extensions/linux/yapf-mime.xml
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!-- Registers .yapf files as image/x-yapf (shared-mime-info). -->
|
||||||
|
<mime-info xmlns="http://www.freedesktop.org/standards/shared-mime-info">
|
||||||
|
<mime-type type="image/x-yapf">
|
||||||
|
<comment>YAPF image</comment>
|
||||||
|
<generic-icon name="image-x-generic"/>
|
||||||
|
<magic priority="60">
|
||||||
|
<match type="string" value="YAPF" offset="0"/>
|
||||||
|
</magic>
|
||||||
|
<glob pattern="*.yapf"/>
|
||||||
|
</mime-type>
|
||||||
|
</mime-info>
|
||||||
4
extensions/linux/yapf.thumbnailer
Normal file
4
extensions/linux/yapf.thumbnailer
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
[Thumbnailer Entry]
|
||||||
|
TryExec=yapf
|
||||||
|
Exec=yapf thumbnail %i %o %s
|
||||||
|
MimeType=image/x-yapf;
|
||||||
30
extensions/photoshop/Open YAPF.jsx
Normal file
30
extensions/photoshop/Open YAPF.jsx
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
// Open YAPF — opens .yapf images in Photoshop.
|
||||||
|
// Copyright 2026 Nexoniarz — Apache License 2.0.
|
||||||
|
// Install: copy this folder's .jsx / .jsxinc files (and yapf / yapf.exe)
|
||||||
|
// into Photoshop's Presets/Scripts folder; then File > Scripts > Open YAPF.
|
||||||
|
|
||||||
|
#target photoshop
|
||||||
|
#include "yapf_common.jsxinc"
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
var filter = YAPF_IS_WINDOWS ? "YAPF images:*.yapf,All files:*.*"
|
||||||
|
: function (f) { return f instanceof Folder || /\.yapf$/i.test(f.name); };
|
||||||
|
var files = File.openDialog("Open YAPF image", filter, true);
|
||||||
|
if (!files) return;
|
||||||
|
if (!(files instanceof Array)) files = [files];
|
||||||
|
|
||||||
|
for (var i = 0; i < files.length; i++) {
|
||||||
|
// 8 / 16-bit images come through PNG (16 bits are kept); half,
|
||||||
|
// float and BC6H HDR textures through Radiance .hdr (32-bit).
|
||||||
|
var tmp = yapfTempFile(yapfIsHdr(files[i]) ? ".hdr" : ".png");
|
||||||
|
if (!yapfRun([files[i].fsName, tmp.fsName], tmp)) {
|
||||||
|
alert("Could not open " + files[i].name + ".\n\n" + yapfFailText());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var doc = app.open(tmp);
|
||||||
|
// The temporary PNG is no longer needed once the document is open;
|
||||||
|
// use File > Scripts > Save as YAPF to write a .yapf again.
|
||||||
|
doc.info.caption = files[i].fsName;
|
||||||
|
tmp.remove();
|
||||||
|
}
|
||||||
|
})();
|
||||||
43
extensions/photoshop/README.md
Normal file
43
extensions/photoshop/README.md
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
# YAPF for Photoshop
|
||||||
|
|
||||||
|
Two scripts that appear under **File → Scripts**:
|
||||||
|
|
||||||
|
- **Open YAPF** — opens one or more `.yapf` files as documents.
|
||||||
|
- **Save as YAPF** — saves the visible image of the active document (a
|
||||||
|
flattened 8-bit copy; your document is not changed), keeping
|
||||||
|
transparency, with an optional mip chain.
|
||||||
|
|
||||||
|
They convert through the `yapf` command-line tool, so Photoshop needs no
|
||||||
|
native plugin.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
1. Download `yapf-<your platform>.zip` from the
|
||||||
|
[Releases](https://github.com/Nexoniarz/YAPF/releases) (or build the tool:
|
||||||
|
`make` → `build/yapf`).
|
||||||
|
2. Copy `Open YAPF.jsx`, `Save as YAPF.jsx`, `yapf_common.jsxinc` **and**
|
||||||
|
`yapf` / `yapf.exe` into Photoshop's scripts folder:
|
||||||
|
- Windows: `C:\Program Files\Adobe\Adobe Photoshop <version>\Presets\Scripts`
|
||||||
|
- macOS: `/Applications/Adobe Photoshop <version>/Presets/Scripts`
|
||||||
|
3. Restart Photoshop. The scripts are under **File → Scripts**; you can give
|
||||||
|
them keyboard shortcuts in **Edit → Keyboard Shortcuts** or record them
|
||||||
|
in Actions.
|
||||||
|
|
||||||
|
Instead of copying `yapf` next to the scripts you can put it on your `PATH`.
|
||||||
|
|
||||||
|
## What opens how
|
||||||
|
|
||||||
|
Every YAPF file the `yapf` tool reads (version 1 and 2): 8-bit images open
|
||||||
|
as 8-bit documents, 16-bit textures as 16-bit documents, half / float and
|
||||||
|
BC6H HDR textures as 32-bit documents (through Radiance `.hdr`), other GPU
|
||||||
|
blocks (BC1–BC7) decoded to 8-bit. Cubes, arrays and animations open their
|
||||||
|
first image. When a file cannot be opened, the alert shows the tool's
|
||||||
|
reason (for example a damaged file). Saving keeps 8 or 16 bits per
|
||||||
|
channel.
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
The scripts are written for Photoshop CC's ExtendScript and are not yet
|
||||||
|
tested inside Photoshop (the converter itself is fully tested). A native
|
||||||
|
format plugin (`.8bi` / `.plugin`, so `.yapf` shows up directly in the Open
|
||||||
|
and Save dialogs) needs Adobe's Photoshop SDK and is planned.
|
||||||
42
extensions/photoshop/Save as YAPF.jsx
Normal file
42
extensions/photoshop/Save as YAPF.jsx
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
// Save as YAPF — writes the active document as a .yapf image.
|
||||||
|
// Copyright 2026 Nexoniarz — Apache License 2.0.
|
||||||
|
// The visible result is saved: a flattened 8-bit RGB (or grayscale) copy,
|
||||||
|
// transparency kept. Your document is not modified.
|
||||||
|
|
||||||
|
#target photoshop
|
||||||
|
#include "yapf_common.jsxinc"
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
if (!app.documents.length) { alert("Open a document first."); return; }
|
||||||
|
var doc = app.activeDocument;
|
||||||
|
|
||||||
|
var base = doc.name.replace(/\.[^.]+$/, "");
|
||||||
|
var start = new File((doc.saved && doc.path ? doc.path.fsName : Folder.desktop.fsName) + "/" + base + ".yapf");
|
||||||
|
var out = start.saveDlg("Save as YAPF", YAPF_IS_WINDOWS ? "YAPF images:*.yapf" : undefined);
|
||||||
|
if (!out) return;
|
||||||
|
if (!/\.yapf$/i.test(out.name)) out = new File(out.fsName + ".yapf");
|
||||||
|
var mips = confirm("Also store mipmaps (for game engines / GPUs)?", true, "YAPF");
|
||||||
|
|
||||||
|
var tmp = yapfTempFile(".png");
|
||||||
|
var copy = doc.duplicate(base + " (YAPF export)", true); // merged copy
|
||||||
|
try {
|
||||||
|
if (copy.mode !== DocumentMode.RGB && copy.mode !== DocumentMode.GRAYSCALE)
|
||||||
|
copy.changeMode(ChangeMode.RGB);
|
||||||
|
// 8 and 16 bits per channel are kept (16-bit becomes a 16-bit
|
||||||
|
// YAPF texture); 32-bit documents are saved as 16-bit.
|
||||||
|
if (copy.bitsPerChannel === BitsPerChannelType.THIRTYTWO)
|
||||||
|
copy.bitsPerChannel = BitsPerChannelType.SIXTEEN;
|
||||||
|
var opts = new PNGSaveOptions();
|
||||||
|
opts.interlaced = false;
|
||||||
|
copy.saveAs(tmp, opts, true, Extension.LOWERCASE);
|
||||||
|
} finally {
|
||||||
|
copy.close(SaveOptions.DONOTSAVECHANGES);
|
||||||
|
}
|
||||||
|
|
||||||
|
var args = [tmp.fsName, out.fsName];
|
||||||
|
if (mips) args.push("--mips");
|
||||||
|
var ok = yapfRun(args, out);
|
||||||
|
tmp.remove();
|
||||||
|
if (!ok) { alert("Could not write " + out.name + ".\n\n" + yapfFailText()); return; }
|
||||||
|
alert("Saved " + out.fsName + " (" + Math.round(out.length / 1024) + " KB)");
|
||||||
|
})();
|
||||||
77
extensions/photoshop/yapf_common.jsxinc
Normal file
77
extensions/photoshop/yapf_common.jsxinc
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
// Shared helpers for the YAPF Photoshop scripts.
|
||||||
|
// Copyright 2026 Nexoniarz — Apache License 2.0.
|
||||||
|
//
|
||||||
|
// The scripts convert through the `yapf` command-line tool. It is looked
|
||||||
|
// up next to these scripts first (yapf.exe / yapf), then on the PATH.
|
||||||
|
|
||||||
|
var YAPF_IS_WINDOWS = $.os.indexOf("Windows") !== -1;
|
||||||
|
|
||||||
|
function yapfQuote(s) {
|
||||||
|
return '"' + String(s).replace(/"/g, '\\"') + '"';
|
||||||
|
}
|
||||||
|
|
||||||
|
function yapfTool() {
|
||||||
|
var here = new File($.fileName).parent;
|
||||||
|
var names = YAPF_IS_WINDOWS ? ["yapf.exe"] : ["yapf"];
|
||||||
|
for (var i = 0; i < names.length; i++) {
|
||||||
|
var f = new File(here.fsName + "/" + names[i]);
|
||||||
|
if (f.exists) return f.fsName;
|
||||||
|
}
|
||||||
|
return names[0]; // rely on the PATH
|
||||||
|
}
|
||||||
|
|
||||||
|
// Runs the tool with its output (stdout) going to `stdoutFile` when given.
|
||||||
|
// Returns the tool's error text ("" when it printed none).
|
||||||
|
function yapfExec(args, stdoutFile) {
|
||||||
|
var errFile = yapfTempFile(".txt");
|
||||||
|
var cmd = yapfQuote(yapfTool());
|
||||||
|
for (var i = 0; i < args.length; i++) cmd += " " + (args[i].charAt(0) === "-" ? args[i] : yapfQuote(args[i]));
|
||||||
|
if (stdoutFile) cmd += " > " + yapfQuote(stdoutFile.fsName);
|
||||||
|
cmd += " 2> " + yapfQuote(errFile.fsName);
|
||||||
|
if (YAPF_IS_WINDOWS) cmd = '"' + cmd + '"'; // cmd.exe strips one pair of quotes
|
||||||
|
app.system(cmd);
|
||||||
|
var text = yapfReadText(errFile);
|
||||||
|
errFile.remove();
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
function yapfReadText(f) {
|
||||||
|
if (!f.exists || !f.open("r")) return "";
|
||||||
|
var t = f.read();
|
||||||
|
f.close();
|
||||||
|
return t.replace(/\s+$/, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Runs the tool; returns true when `expectOut` was produced. The tool's
|
||||||
|
// own message (why it failed) is left in yapfLastError.
|
||||||
|
var yapfLastError = "";
|
||||||
|
function yapfRun(args, expectOut) {
|
||||||
|
if (expectOut.exists) expectOut.remove();
|
||||||
|
yapfLastError = yapfExec(args, null);
|
||||||
|
return expectOut.exists;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The tool's message, or the help text when the tool could not be run.
|
||||||
|
function yapfFailText() {
|
||||||
|
return /yapf: /.test(yapfLastError) ? yapfLastError : YAPF_TOOL_HELP;
|
||||||
|
}
|
||||||
|
|
||||||
|
// True when a .yapf file holds float / HDR data (half, float, BC6H), so it
|
||||||
|
// should open as a 32-bit document instead of a clamped 8-bit one.
|
||||||
|
function yapfIsHdr(file) {
|
||||||
|
var info = yapfTempFile(".txt");
|
||||||
|
yapfExec(["info", file.fsName], info);
|
||||||
|
var text = yapfReadText(info);
|
||||||
|
info.remove();
|
||||||
|
return /payload\s*:[^\n]*(half float|, float|BC6H)/.test(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
function yapfTempFile(ext) {
|
||||||
|
return new File(Folder.temp.fsName + "/yapf_" + new Date().getTime() + "_" +
|
||||||
|
Math.floor(Math.random() * 1e6) + ext);
|
||||||
|
}
|
||||||
|
|
||||||
|
var YAPF_TOOL_HELP =
|
||||||
|
"The 'yapf' command-line tool was not found or failed.\n\n" +
|
||||||
|
"Put yapf" + (YAPF_IS_WINDOWS ? ".exe" : "") + " in the same folder as this script, " +
|
||||||
|
"or add it to your PATH.\nDownload or build it: https://github.com/Nexoniarz/YAPF";
|
||||||
39
extensions/vscode/README.md
Normal file
39
extensions/vscode/README.md
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
# YAPF Image Preview for VS Code
|
||||||
|
|
||||||
|
Click any `.yapf` file in VS Code to see the image.
|
||||||
|
|
||||||
|
- Checkerboard behind transparent pixels.
|
||||||
|
- **Click** to switch between *fit to window* and *100 %*; **Ctrl + scroll**
|
||||||
|
to zoom (pixels stay sharp when enlarged).
|
||||||
|
- Opens every YAPF file, version 1 and 2: GPU blocks (BC1–BC7, BC6H)
|
||||||
|
decoded, 16-bit and float textures shown in 8-bit (floats sRGB encoded).
|
||||||
|
- Pickers for **mip level**, cube **face**, array **layer**, animation
|
||||||
|
**frame** and volume **slice**.
|
||||||
|
- Info bar: size, shape, storage (e.g. *GPU BC7*, *RGB half float*), sRGB /
|
||||||
|
linear, normal-map convention, format version, file size, decode time.
|
||||||
|
Damaged files say so (checksum mismatch).
|
||||||
|
- Reloads automatically when the file changes on disk.
|
||||||
|
|
||||||
|
Decoding runs in plain JavaScript inside the preview; no native code.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
- From a release: download `yapf-preview.vsix`, then in VS Code run
|
||||||
|
**Extensions: Install from VSIX…** (or `code --install-extension yapf-preview.vsix`).
|
||||||
|
- From source (needs Node.js):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd extensions/vscode
|
||||||
|
npm run package # → yapf-preview-1.0.0.vsix
|
||||||
|
code --install-extension yapf-preview-1.0.0.vsix
|
||||||
|
```
|
||||||
|
|
||||||
|
Should work in VS Code 1.75+ and VS Code–based editors such as VSCodium.
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
The `.vsix` packages cleanly, and the preview code was tested in a browser
|
||||||
|
engine (headless Chromium) with real files: correct pixels, mip picker,
|
||||||
|
error message for corrupt files; the YAPF 2 preview was tested the same
|
||||||
|
way (BC7, BC5, BC6H, half float, 16-bit, cube, animation, damaged and
|
||||||
|
non-YAPF files). **It has not yet been run inside VS Code itself.**
|
||||||
65
extensions/vscode/extension.js
Normal file
65
extensions/vscode/extension.js
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
// YAPF image preview for VS Code — a read-only custom editor for *.yapf.
|
||||||
|
// Copyright 2026 Nexoniarz — Apache License 2.0.
|
||||||
|
const vscode = require('vscode');
|
||||||
|
|
||||||
|
class YapfPreviewProvider {
|
||||||
|
constructor(context) { this.context = context; }
|
||||||
|
|
||||||
|
openCustomDocument(uri) { return { uri, dispose() {} }; }
|
||||||
|
|
||||||
|
async resolveCustomEditor(document, panel) {
|
||||||
|
const media = vscode.Uri.joinPath(this.context.extensionUri, 'media');
|
||||||
|
panel.webview.options = { enableScripts: true, localResourceRoots: [media] };
|
||||||
|
panel.webview.html = this.html(panel.webview, media);
|
||||||
|
|
||||||
|
const send = async () => {
|
||||||
|
try {
|
||||||
|
const bytes = await vscode.workspace.fs.readFile(document.uri);
|
||||||
|
panel.webview.postMessage({ type: 'file', name: document.uri.path.split('/').pop(), bytes });
|
||||||
|
} catch (e) {
|
||||||
|
panel.webview.postMessage({ type: 'error', message: String(e.message || e) });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
panel.webview.onDidReceiveMessage((m) => { if (m.type === 'ready') send(); });
|
||||||
|
|
||||||
|
// Reload when the file changes on disk.
|
||||||
|
const folder = vscode.Uri.joinPath(document.uri, '..');
|
||||||
|
const name = document.uri.path.split('/').pop();
|
||||||
|
const watcher = vscode.workspace.createFileSystemWatcher(new vscode.RelativePattern(folder, name));
|
||||||
|
watcher.onDidChange(send);
|
||||||
|
panel.onDidDispose(() => watcher.dispose());
|
||||||
|
}
|
||||||
|
|
||||||
|
html(webview, media) {
|
||||||
|
const uri = (f) => webview.asWebviewUri(vscode.Uri.joinPath(media, f));
|
||||||
|
const nonce = Math.random().toString(36).slice(2) + Date.now().toString(36);
|
||||||
|
return `<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta http-equiv="Content-Security-Policy"
|
||||||
|
content="default-src 'none'; img-src ${webview.cspSource} data: blob:; style-src ${webview.cspSource}; script-src 'nonce-${nonce}';">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<link rel="stylesheet" href="${uri('preview.css')}">
|
||||||
|
<title>YAPF Preview</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="bar"><span id="info">Loading…</span><span id="controls"></span></div>
|
||||||
|
<div id="stage"><canvas id="canvas"></canvas></div>
|
||||||
|
<div id="error" hidden></div>
|
||||||
|
<script nonce="${nonce}" src="${uri('yapf.js')}"></script>
|
||||||
|
<script nonce="${nonce}" src="${uri('preview.js')}"></script>
|
||||||
|
</body>
|
||||||
|
</html>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function activate(context) {
|
||||||
|
context.subscriptions.push(vscode.window.registerCustomEditorProvider(
|
||||||
|
'yapf.preview', new YapfPreviewProvider(context),
|
||||||
|
{ webviewOptions: { retainContextWhenHidden: true }, supportsMultipleEditorsPerDocument: true }));
|
||||||
|
}
|
||||||
|
|
||||||
|
function deactivate() {}
|
||||||
|
|
||||||
|
module.exports = { activate, deactivate };
|
||||||
BIN
extensions/vscode/media/icon.png
Normal file
BIN
extensions/vscode/media/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
26
extensions/vscode/media/preview.css
Normal file
26
extensions/vscode/media/preview.css
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
html, body { height: 100%; margin: 0; }
|
||||||
|
body {
|
||||||
|
display: flex; flex-direction: column;
|
||||||
|
background: var(--vscode-editor-background, #1e1e1e);
|
||||||
|
color: var(--vscode-foreground, #ccc);
|
||||||
|
font: 12px var(--vscode-font-family, system-ui, sans-serif);
|
||||||
|
}
|
||||||
|
#bar {
|
||||||
|
display: flex; justify-content: space-between; align-items: center; gap: 12px;
|
||||||
|
padding: 6px 12px; border-bottom: 1px solid var(--vscode-panel-border, #333);
|
||||||
|
}
|
||||||
|
#bar select {
|
||||||
|
background: var(--vscode-dropdown-background, #3c3c3c);
|
||||||
|
color: var(--vscode-dropdown-foreground, #ccc);
|
||||||
|
border: 1px solid var(--vscode-dropdown-border, #555);
|
||||||
|
}
|
||||||
|
#stage { flex: 1; overflow: auto; display: grid; place-items: center; padding: 16px; cursor: zoom-in; }
|
||||||
|
body:not(.fit) #stage { cursor: zoom-out; }
|
||||||
|
canvas {
|
||||||
|
background-color: #fff;
|
||||||
|
background-image: conic-gradient(#ccc 25%, #fff 0 50%, #ccc 0 75%, #fff 0);
|
||||||
|
background-size: 16px 16px;
|
||||||
|
box-shadow: 0 0 0 1px var(--vscode-panel-border, #333);
|
||||||
|
}
|
||||||
|
canvas.pixelated { image-rendering: pixelated; }
|
||||||
|
#error { padding: 16px; color: var(--vscode-errorForeground, #f48771); }
|
||||||
141
extensions/vscode/media/preview.js
Normal file
141
extensions/vscode/media/preview.js
Normal file
|
|
@ -0,0 +1,141 @@
|
||||||
|
// Webview side of the YAPF preview: decode, draw, zoom, surface pickers, info.
|
||||||
|
(function () {
|
||||||
|
const vscode = typeof acquireVsCodeApi === 'function' ? acquireVsCodeApi() : null;
|
||||||
|
const canvas = document.getElementById('canvas');
|
||||||
|
const info = document.getElementById('info');
|
||||||
|
const controls = document.getElementById('controls');
|
||||||
|
const errorBox = document.getElementById('error');
|
||||||
|
const PIXEL_HINTS = ['RGBA8', 'RGB8', 'RG8', 'R8', 'SRGB8_A8', 'SRGB8', 'RGBA16', 'RGB16', 'RG16', 'R16',
|
||||||
|
'RGBA16F', 'RGB16F', 'RG16F', 'R16F', 'RGBA32F', 'RGB32F', 'RG32F', 'R32F'];
|
||||||
|
const BLOCKS = { 0x40: 'BC1', 0x41: 'BC2', 0x42: 'BC3', 0x43: 'BC4', 0x44: 'BC4 snorm', 0x45: 'BC5',
|
||||||
|
0x46: 'BC5 snorm', 0x47: 'BC6H', 0x48: 'BC6H signed', 0x49: 'BC7', 0x50: 'ETC2 RGB',
|
||||||
|
0x51: 'ETC2 RGB A1', 0x52: 'ETC2 RGBA', 0x53: 'EAC R11', 0x54: 'EAC R11 snorm',
|
||||||
|
0x55: 'EAC RG11', 0x56: 'EAC RG11 snorm' };
|
||||||
|
['4×4', '5×4', '5×5', '6×5', '6×6', '8×5', '8×6', '8×8', '10×5', '10×6', '10×8', '10×10', '12×10', '12×12']
|
||||||
|
.forEach((n, i) => { BLOCKS[0x60 + i] = 'ASTC ' + n; });
|
||||||
|
const CH_NAMES = ['', 'Gray', 'Gray + Alpha', 'RGB', 'RGBA'];
|
||||||
|
const SAMPLES = ['8-bit', '16-bit', 'half float', 'float'];
|
||||||
|
const FACES = ['+X', '−X', '+Y', '−Y', '+Z', '−Z'];
|
||||||
|
|
||||||
|
let tex = null, fit = true, zoom = 1;
|
||||||
|
const sel = { mip: 0, face: 0, layer: 0, frame: 0, slice: 0 };
|
||||||
|
|
||||||
|
function kb(n) { return n < 1024 * 1024 ? (n / 1024).toFixed(1) + ' KB' : (n / 1048576).toFixed(2) + ' MB'; }
|
||||||
|
|
||||||
|
function mipDims(m) {
|
||||||
|
return [Math.max(1, tex.width >>> m), Math.max(1, tex.height >>> m),
|
||||||
|
tex.shape === 2 ? Math.max(1, tex.depth >>> m) : 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
function draw() {
|
||||||
|
const s = YAPF.surfaceIndex(tex, sel.frame, sel.layer, sel.face, sel.mip);
|
||||||
|
sel.slice = Math.min(sel.slice, mipDims(sel.mip)[2] - 1);
|
||||||
|
try {
|
||||||
|
const view = YAPF.textureToRGBA(tex, s, sel.slice);
|
||||||
|
canvas.width = view.width; canvas.height = view.height;
|
||||||
|
canvas.getContext('2d').putImageData(new ImageData(view.rgba, view.width, view.height), 0, 0);
|
||||||
|
errorBox.hidden = true;
|
||||||
|
} catch (e) {
|
||||||
|
errorBox.hidden = false;
|
||||||
|
errorBox.textContent = e.message;
|
||||||
|
}
|
||||||
|
layout();
|
||||||
|
}
|
||||||
|
|
||||||
|
function layout() {
|
||||||
|
if (!tex) return;
|
||||||
|
const w = canvas.width, h = canvas.height;
|
||||||
|
const stage = document.getElementById('stage');
|
||||||
|
const scale = fit ? Math.min(1, (stage.clientWidth - 32) / w, (stage.clientHeight - 32) / h) : zoom;
|
||||||
|
canvas.style.width = Math.max(1, Math.round(w * scale)) + 'px';
|
||||||
|
canvas.style.height = Math.max(1, Math.round(h * scale)) + 'px';
|
||||||
|
canvas.classList.toggle('pixelated', scale >= 2);
|
||||||
|
document.body.classList.toggle('fit', fit);
|
||||||
|
stage.title = fit ? 'Click for 100%, Ctrl+scroll to zoom' : 'Click to fit';
|
||||||
|
}
|
||||||
|
|
||||||
|
function picker(key, count, label) {
|
||||||
|
if (count <= 1) return;
|
||||||
|
const el = document.createElement('select');
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
const o = document.createElement('option');
|
||||||
|
o.value = i;
|
||||||
|
o.textContent = label(i);
|
||||||
|
el.appendChild(o);
|
||||||
|
}
|
||||||
|
sel[key] = Math.min(sel[key], count - 1);
|
||||||
|
el.value = sel[key];
|
||||||
|
el.onchange = () => { sel[key] = +el.value; if (key === 'mip') buildControls(); draw(); };
|
||||||
|
controls.appendChild(el);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildControls() {
|
||||||
|
controls.textContent = '';
|
||||||
|
picker('mip', tex.mipLevels, (m) => { const [w, h, d] = mipDims(m); return 'Mip ' + m + ' (' + w + '×' + h + (d > 1 ? '×' + d : '') + ')'; });
|
||||||
|
picker('face', tex.faces, (f) => 'Face ' + FACES[f]);
|
||||||
|
picker('layer', tex.layers, (l) => 'Layer ' + l);
|
||||||
|
picker('frame', tex.frames, (f) => 'Frame ' + (f + 1) + (tex.frameUs[f] ? ' (' + Math.round(tex.frameUs[f] / 1000) + ' ms)' : ''));
|
||||||
|
picker('slice', mipDims(sel.mip)[2], (z) => 'Slice ' + z);
|
||||||
|
}
|
||||||
|
|
||||||
|
function show(name, bytes) {
|
||||||
|
errorBox.hidden = true;
|
||||||
|
const t = performance.now();
|
||||||
|
try {
|
||||||
|
tex = YAPF.decodeTexture(bytes);
|
||||||
|
} catch (e) {
|
||||||
|
tex = null;
|
||||||
|
errorBox.hidden = false;
|
||||||
|
errorBox.textContent = name + ': ' + e.message.replace(/^YAPF: /, '');
|
||||||
|
info.textContent = '';
|
||||||
|
controls.textContent = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const ms = performance.now() - t;
|
||||||
|
let raw = 0;
|
||||||
|
for (let m = 0; m < tex.mipLevels; m++) {
|
||||||
|
const [w, h, d] = mipDims(m);
|
||||||
|
raw += w * h * d * (tex.payload ? 4 : tex.channels * [1, 2, 2, 4][tex.sample]);
|
||||||
|
}
|
||||||
|
raw *= tex.frames * tex.layers * tex.faces;
|
||||||
|
const [w0, h0, d0] = mipDims(0);
|
||||||
|
const shape = tex.shape === 1 ? (tex.layers > 1 ? 'cube array of ' + tex.layers : 'cubemap')
|
||||||
|
: tex.shape === 2 ? 'volume' : tex.layers > 1 ? 'array of ' + tex.layers : '';
|
||||||
|
const storage = tex.payload === 1 ? 'GPU ' + (BLOCKS[tex.format] || '?')
|
||||||
|
: CH_NAMES[tex.channels] + ' ' + SAMPLES[tex.sample] + ', GPU ' + (PIXEL_HINTS[tex.format] || '?');
|
||||||
|
const parts = [
|
||||||
|
w0 + ' × ' + h0 + (d0 > 1 ? ' × ' + d0 : ''),
|
||||||
|
shape,
|
||||||
|
tex.frames > 1 ? tex.frames + ' frames' : '',
|
||||||
|
storage,
|
||||||
|
(tex.flags & 2 ? 'sRGB' : 'linear') + (tex.flags & 1 ? ', premultiplied' : ''),
|
||||||
|
tex.normal ? 'normal map (' + ((tex.normal & 3) === 2 ? 'DirectX' : (tex.normal & 3) === 1 ? 'OpenGL' : '?') + ')' : '',
|
||||||
|
tex.version === 2 ? 'YAPF 2' : 'YAPF 1',
|
||||||
|
kb(bytes.length) + (tex.payload ? '' : ' (' + (100 * bytes.length / raw).toFixed(1) + '% of raw)'),
|
||||||
|
'decoded in ' + ms.toFixed(1) + ' ms',
|
||||||
|
];
|
||||||
|
const author = tex.notes.find((n) => n.tag === 'AUTH');
|
||||||
|
if (author) parts.push('by ' + new TextDecoder().decode(author.data));
|
||||||
|
info.textContent = parts.filter(Boolean).join(' · ');
|
||||||
|
buildControls();
|
||||||
|
draw();
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('stage').addEventListener('click', () => { fit = !fit; zoom = 1; layout(); });
|
||||||
|
document.getElementById('stage').addEventListener('wheel', (e) => {
|
||||||
|
if (!e.ctrlKey || !tex) return;
|
||||||
|
e.preventDefault();
|
||||||
|
fit = false;
|
||||||
|
zoom = Math.min(64, Math.max(1 / 64, zoom * (e.deltaY < 0 ? 1.25 : 0.8)));
|
||||||
|
layout();
|
||||||
|
}, { passive: false });
|
||||||
|
window.addEventListener('resize', layout);
|
||||||
|
|
||||||
|
window.addEventListener('message', (e) => {
|
||||||
|
const m = e.data;
|
||||||
|
if (m.type === 'file') show(m.name, m.bytes instanceof Uint8Array ? m.bytes : new Uint8Array(m.bytes));
|
||||||
|
if (m.type === 'error') { errorBox.hidden = false; errorBox.textContent = m.message; }
|
||||||
|
});
|
||||||
|
if (vscode) vscode.postMessage({ type: 'ready' });
|
||||||
|
window.yapfPreviewShow = show; // used by the browser test page
|
||||||
|
})();
|
||||||
29
extensions/vscode/package.json
Normal file
29
extensions/vscode/package.json
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
{
|
||||||
|
"name": "yapf-preview",
|
||||||
|
"displayName": "YAPF Image Preview",
|
||||||
|
"description": "View YAPF (.yapf) lossless images inside VS Code",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"publisher": "Nexoniarz",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"icon": "media/icon.png",
|
||||||
|
"repository": { "type": "git", "url": "https://github.com/Nexoniarz/YAPF.git" },
|
||||||
|
"engines": { "vscode": "^1.75.0" },
|
||||||
|
"categories": ["Visualization", "Other"],
|
||||||
|
"keywords": ["yapf", "image", "texture", "preview", "viewer"],
|
||||||
|
"main": "./extension.js",
|
||||||
|
"activationEvents": [],
|
||||||
|
"contributes": {
|
||||||
|
"customEditors": [
|
||||||
|
{
|
||||||
|
"viewType": "yapf.preview",
|
||||||
|
"displayName": "YAPF Image Preview",
|
||||||
|
"selector": [{ "filenamePattern": "*.yapf" }],
|
||||||
|
"priority": "default"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"vscode:prepublish": "node scripts/copy-codec.js",
|
||||||
|
"package": "node scripts/copy-codec.js && npx --yes @vscode/vsce package --allow-missing-repository"
|
||||||
|
}
|
||||||
|
}
|
||||||
6
extensions/vscode/scripts/copy-codec.js
Normal file
6
extensions/vscode/scripts/copy-codec.js
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
// Copies the shared JavaScript codec into media/ so the extension is self-contained.
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const src = path.join(__dirname, '..', '..', '..', 'bindings', 'js', 'yapf.js');
|
||||||
|
fs.copyFileSync(src, path.join(__dirname, '..', 'media', 'yapf.js'));
|
||||||
|
console.log('copied', path.relative(process.cwd(), src), '-> media/yapf.js');
|
||||||
211
other/YAPF.SVG
Normal file
211
other/YAPF.SVG
Normal file
|
|
@ -0,0 +1,211 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="512"
|
||||||
|
height="512"
|
||||||
|
viewBox="0 0 512 512"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
inkscape:version="1.4.4 (dcaf3e7d9e, 2026-05-05)"
|
||||||
|
sodipodi:docname="YAPF.svg"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview1"
|
||||||
|
pagecolor="#000000"
|
||||||
|
bordercolor="#000000"
|
||||||
|
borderopacity="0.25"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:document-units="px"
|
||||||
|
inkscape:zoom="0.7071068"
|
||||||
|
inkscape:cx="-70.710676"
|
||||||
|
inkscape:cy="25.455843"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="1014"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="0"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="layer1" />
|
||||||
|
<defs
|
||||||
|
id="defs1">
|
||||||
|
<linearGradient
|
||||||
|
id="linearGradient12"
|
||||||
|
inkscape:collect="always">
|
||||||
|
<stop
|
||||||
|
style="stop-color:#b2b2b2;stop-opacity:0.29873046;"
|
||||||
|
offset="0"
|
||||||
|
id="stop11" />
|
||||||
|
<stop
|
||||||
|
style="stop-color:#ffffff;stop-opacity:0.29689941;"
|
||||||
|
offset="1"
|
||||||
|
id="stop12" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
id="linearGradient9"
|
||||||
|
inkscape:collect="always">
|
||||||
|
<stop
|
||||||
|
style="stop-color:#cc0000;stop-opacity:1;"
|
||||||
|
offset="0"
|
||||||
|
id="stop9" />
|
||||||
|
<stop
|
||||||
|
style="stop-color:#ff0101;stop-opacity:1;"
|
||||||
|
offset="1"
|
||||||
|
id="stop10" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
id="linearGradient7"
|
||||||
|
inkscape:collect="always">
|
||||||
|
<stop
|
||||||
|
style="stop-color:#005dcc;stop-opacity:1;"
|
||||||
|
offset="0"
|
||||||
|
id="stop7" />
|
||||||
|
<stop
|
||||||
|
style="stop-color:#0173ff;stop-opacity:1;"
|
||||||
|
offset="1"
|
||||||
|
id="stop8" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
id="linearGradient5"
|
||||||
|
inkscape:collect="always">
|
||||||
|
<stop
|
||||||
|
style="stop-color:#00cc00;stop-opacity:1;"
|
||||||
|
offset="0"
|
||||||
|
id="stop5" />
|
||||||
|
<stop
|
||||||
|
style="stop-color:#00fe00;stop-opacity:1;"
|
||||||
|
offset="1"
|
||||||
|
id="stop6" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
inkscape:collect="always"
|
||||||
|
xlink:href="#linearGradient5"
|
||||||
|
id="linearGradient6"
|
||||||
|
x1="241.51726"
|
||||||
|
y1="191.04054"
|
||||||
|
x2="322.48398"
|
||||||
|
y2="280.9632"
|
||||||
|
gradientUnits="userSpaceOnUse" />
|
||||||
|
<linearGradient
|
||||||
|
inkscape:collect="always"
|
||||||
|
xlink:href="#linearGradient7"
|
||||||
|
id="linearGradient8"
|
||||||
|
x1="224.7276"
|
||||||
|
y1="314.2731"
|
||||||
|
x2="235.2737"
|
||||||
|
y2="193.73064"
|
||||||
|
gradientUnits="userSpaceOnUse" />
|
||||||
|
<linearGradient
|
||||||
|
inkscape:collect="always"
|
||||||
|
xlink:href="#linearGradient9"
|
||||||
|
id="linearGradient10"
|
||||||
|
x1="297.91324"
|
||||||
|
y1="183.58171"
|
||||||
|
x2="178.08795"
|
||||||
|
y2="200.42204"
|
||||||
|
gradientUnits="userSpaceOnUse" />
|
||||||
|
<linearGradient
|
||||||
|
inkscape:collect="always"
|
||||||
|
xlink:href="#linearGradient12"
|
||||||
|
id="linearGradient8-8"
|
||||||
|
x1="178.14076"
|
||||||
|
y1="285.16241"
|
||||||
|
x2="281.86047"
|
||||||
|
y2="222.84132"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
gradientTransform="translate(23.169404,-30.5)" />
|
||||||
|
</defs>
|
||||||
|
<g
|
||||||
|
inkscape:label="YAPF | CC0 - Public Domain"
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer1">
|
||||||
|
<g
|
||||||
|
id="g13"
|
||||||
|
transform="matrix(2.0095459,0,0,2.0095459,-258.52167,-127.95876)"
|
||||||
|
inkscape:label="YAPF">
|
||||||
|
<g
|
||||||
|
id="g12"
|
||||||
|
inkscape:label="LOGO & TEXT"
|
||||||
|
transform="matrix(1.2422505,0,0,1.2422505,-61.21895,-95.418348)">
|
||||||
|
<g
|
||||||
|
id="g10"
|
||||||
|
transform="matrix(1.0061309,0,0,1,-2.0998398,6.6753885)"
|
||||||
|
inkscape:label="LOGO">
|
||||||
|
<rect
|
||||||
|
style="fill:url(#linearGradient10);fill-opacity:1;stroke-width:0.236333"
|
||||||
|
id="rect1-3"
|
||||||
|
width="121.00286"
|
||||||
|
height="121.00375"
|
||||||
|
x="177.49921"
|
||||||
|
y="131.5"
|
||||||
|
ry="60.501431"
|
||||||
|
inkscape:label="Red" />
|
||||||
|
<rect
|
||||||
|
style="fill:url(#linearGradient8);fill-opacity:1;stroke-width:0.236333"
|
||||||
|
id="rect1-3-5"
|
||||||
|
width="121.00286"
|
||||||
|
height="121.00375"
|
||||||
|
x="169.49921"
|
||||||
|
y="193.5"
|
||||||
|
ry="60.501431"
|
||||||
|
inkscape:label="Blue" />
|
||||||
|
<rect
|
||||||
|
style="fill:url(#linearGradient6);fill-opacity:1;stroke:none;stroke-width:0.236333"
|
||||||
|
id="rect1-3-5-3"
|
||||||
|
width="121.00286"
|
||||||
|
height="121.00375"
|
||||||
|
x="221.49921"
|
||||||
|
y="175.5"
|
||||||
|
ry="60.501431"
|
||||||
|
inkscape:label="Green" />
|
||||||
|
<rect
|
||||||
|
style="fill:url(#linearGradient8-8);fill-opacity:1;stroke-width:0.236333"
|
||||||
|
id="rect1-3-5-31"
|
||||||
|
width="121.00286"
|
||||||
|
height="121.00375"
|
||||||
|
x="192.66861"
|
||||||
|
y="163"
|
||||||
|
ry="60.501431"
|
||||||
|
inkscape:label="Alpha" />
|
||||||
|
</g>
|
||||||
|
<text
|
||||||
|
xml:space="preserve"
|
||||||
|
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:10.6695px;font-family:'Liberation Mono';-inkscape-font-specification:'Liberation Mono Bold';text-align:center;writing-mode:lr-tb;direction:ltr;text-anchor:middle;fill:#ffffff;fill-opacity:1;stroke-width:0.889127"
|
||||||
|
x="262.40924"
|
||||||
|
y="264.6781"
|
||||||
|
id="text2"
|
||||||
|
transform="scale(0.96401161,1.0373319)"
|
||||||
|
inkscape:label="TEXT"><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
id="tspan2"
|
||||||
|
x="262.40924"
|
||||||
|
y="264.6781"
|
||||||
|
style="font-style:italic;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:10.6695px;font-family:'Liberation Mono';-inkscape-font-specification:'Liberation Mono Bold Italic';writing-mode:lr-tb;direction:ltr;fill:#ffffff;fill-opacity:1;stroke-width:0.889127">Yet Another </tspan><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
x="262.40924"
|
||||||
|
y="278.01498"
|
||||||
|
style="font-style:italic;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:10.6695px;font-family:'Liberation Mono';-inkscape-font-specification:'Liberation Mono Bold Italic';writing-mode:lr-tb;direction:ltr;fill:#ffffff;fill-opacity:1;stroke-width:0.889127"
|
||||||
|
id="tspan1">Picture Format</tspan></text>
|
||||||
|
<text
|
||||||
|
xml:space="preserve"
|
||||||
|
style="font-style:italic;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:44.4681px;font-family:'Liberation Mono';-inkscape-font-specification:'Liberation Mono Bold Italic';writing-mode:lr-tb;direction:ltr;fill:#f9f9f9;stroke-width:3.70568"
|
||||||
|
x="170.51646"
|
||||||
|
y="284.09799"
|
||||||
|
id="text3"
|
||||||
|
transform="matrix(1.0205207,-0.13368391,0.07735525,0.96975873,0,0)"
|
||||||
|
inkscape:label="TEST2"><tspan
|
||||||
|
sodipodi:role="line"
|
||||||
|
id="tspan3"
|
||||||
|
style="fill:#f9f9f9;stroke-width:3.70568"
|
||||||
|
x="170.51646"
|
||||||
|
y="284.09799">YAPF</tspan></text>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 7.2 KiB |
BIN
other/YAPF.YAPF
Normal file
BIN
other/YAPF.YAPF
Normal file
Binary file not shown.
229
pikture.c
229
pikture.c
|
|
@ -1,229 +0,0 @@
|
||||||
/*
|
|
||||||
pikture.c
|
|
||||||
Native PIKTURE format encoder and decoder library.
|
|
||||||
Version: 100
|
|
||||||
License: MIT
|
|
||||||
Made by: Nexoniarz
|
|
||||||
*/
|
|
||||||
|
|
||||||
#include "pikture.h"
|
|
||||||
#include <stdio.h>
|
|
||||||
#include <stdlib.h>
|
|
||||||
#include <string.h>
|
|
||||||
|
|
||||||
#define PIKT_MAGIC "PIK!"
|
|
||||||
#define PIKT_VERSION 5
|
|
||||||
#define PIKT_HASH(r, g, b, a) (((r) * 3 + (g) * 5 + (b) * 7 + (a) * 11) % 64)
|
|
||||||
|
|
||||||
typedef struct {
|
|
||||||
uint8_t r, g, b, a;
|
|
||||||
} pikt_pixel;
|
|
||||||
|
|
||||||
/* Reads a 16-bit unsigned integer in Big-Endian format */
|
|
||||||
static uint16_t read_u16_be(FILE* f) {
|
|
||||||
uint8_t bytes[2];
|
|
||||||
fread(bytes, 1, 2, f);
|
|
||||||
return (bytes[0] << 8) | bytes[1];
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Writes a 16-bit unsigned integer in Big-Endian format */
|
|
||||||
static void write_u16_be(FILE* f, uint16_t value) {
|
|
||||||
uint8_t bytes[2] = { (value >> 8) & 0xFF, value & 0xFF };
|
|
||||||
fwrite(bytes, 1, 2, f);
|
|
||||||
}
|
|
||||||
|
|
||||||
pikture_t* pikture_load(const char* filename) {
|
|
||||||
FILE* f = fopen(filename, "rb");
|
|
||||||
if (!f) return NULL;
|
|
||||||
|
|
||||||
char magic[4];
|
|
||||||
fread(magic, 1, 4, f);
|
|
||||||
if (memcmp(magic, PIKT_MAGIC, 4) != 0) {
|
|
||||||
fclose(f);
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint8_t version;
|
|
||||||
fread(&version, 1, 1, f);
|
|
||||||
if (version != PIKT_VERSION) {
|
|
||||||
fclose(f);
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
pikture_t* img = (pikture_t*)malloc(sizeof(pikture_t));
|
|
||||||
if (!img) {
|
|
||||||
fclose(f);
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
img->width = read_u16_be(f);
|
|
||||||
img->height = read_u16_be(f);
|
|
||||||
|
|
||||||
uint8_t depth;
|
|
||||||
fread(&depth, 1, 1, f);
|
|
||||||
img->channels = 4;
|
|
||||||
|
|
||||||
size_t data_size = (size_t)img->width * img->height * 4;
|
|
||||||
img->pixels = (uint8_t*)malloc(data_size);
|
|
||||||
if (!img->pixels) {
|
|
||||||
free(img);
|
|
||||||
fclose(f);
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
pikt_pixel index[64] = {0};
|
|
||||||
uint8_t r = 0, g = 0, b = 0, a = 255;
|
|
||||||
|
|
||||||
size_t ptr = 0;
|
|
||||||
while (ptr < data_size) {
|
|
||||||
int b0 = fgetc(f);
|
|
||||||
if (b0 == EOF) break;
|
|
||||||
|
|
||||||
if (b0 == 0xFE) {
|
|
||||||
r = fgetc(f);
|
|
||||||
g = fgetc(f);
|
|
||||||
b = fgetc(f);
|
|
||||||
} else if (b0 == 0xFF) {
|
|
||||||
r = fgetc(f);
|
|
||||||
g = fgetc(f);
|
|
||||||
b = fgetc(f);
|
|
||||||
a = fgetc(f);
|
|
||||||
} else {
|
|
||||||
uint8_t match = b0 & 0xC0;
|
|
||||||
if (match == 0x00) {
|
|
||||||
pikt_pixel p = index[b0 & 0x3F];
|
|
||||||
r = p.r; g = p.g; b = p.b; a = p.a;
|
|
||||||
} else if (match == 0x40) {
|
|
||||||
r = (uint8_t)(r + ((b0 >> 4) & 0x03) - 2);
|
|
||||||
g = (uint8_t)(g + ((b0 >> 2) & 0x03) - 2);
|
|
||||||
b = (uint8_t)(b + (b0 & 0x03) - 2);
|
|
||||||
} else if (match == 0x80) {
|
|
||||||
int b1 = fgetc(f);
|
|
||||||
int8_t dg = (b0 & 0x3F) - 32;
|
|
||||||
r = (uint8_t)(r + dg + ((b1 >> 4) & 0x0F) - 8);
|
|
||||||
g = (uint8_t)(g + dg);
|
|
||||||
b = (uint8_t)(b + dg + (b1 & 0x0F) - 8);
|
|
||||||
} else if (match == 0xC0) {
|
|
||||||
int run = (b0 & 0x3F) + 1;
|
|
||||||
for (int i = 0; i < run && ptr < data_size; i++) {
|
|
||||||
img->pixels[ptr++] = r;
|
|
||||||
img->pixels[ptr++] = g;
|
|
||||||
img->pixels[ptr++] = b;
|
|
||||||
img->pixels[ptr++] = a;
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
uint8_t idx_pos = PIKT_HASH(r, g, b, a);
|
|
||||||
index[idx_pos].r = r;
|
|
||||||
index[idx_pos].g = g;
|
|
||||||
index[idx_pos].b = b;
|
|
||||||
index[idx_pos].a = a;
|
|
||||||
|
|
||||||
if (ptr < data_size) {
|
|
||||||
img->pixels[ptr++] = r;
|
|
||||||
img->pixels[ptr++] = g;
|
|
||||||
img->pixels[ptr++] = b;
|
|
||||||
img->pixels[ptr++] = a;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fclose(f);
|
|
||||||
return img;
|
|
||||||
}
|
|
||||||
|
|
||||||
int pikture_save(const char* filename, const pikture_t* img) {
|
|
||||||
if (!img || !img->pixels) return 0;
|
|
||||||
|
|
||||||
FILE* f = fopen(filename, "wb");
|
|
||||||
if (!f) return 0;
|
|
||||||
|
|
||||||
fwrite(PIKT_MAGIC, 1, 4, f);
|
|
||||||
uint8_t version = PIKT_VERSION;
|
|
||||||
fwrite(&version, 1, 1, f);
|
|
||||||
write_u16_be(f, img->width);
|
|
||||||
write_u16_be(f, img->height);
|
|
||||||
uint8_t depth = 32;
|
|
||||||
fwrite(&depth, 1, 1, f);
|
|
||||||
|
|
||||||
pikt_pixel index[64] = {0};
|
|
||||||
uint8_t prev_r = 0, prev_g = 0, prev_b = 0, prev_a = 255;
|
|
||||||
int run = 0;
|
|
||||||
|
|
||||||
size_t total_pixels = (size_t)img->width * img->height;
|
|
||||||
size_t px_idx = 0;
|
|
||||||
|
|
||||||
while (px_idx < total_pixels * 4) {
|
|
||||||
uint8_t r = img->pixels[px_idx++];
|
|
||||||
uint8_t g = img->pixels[px_idx++];
|
|
||||||
uint8_t b = img->pixels[px_idx++];
|
|
||||||
uint8_t a = img->pixels[px_idx++];
|
|
||||||
|
|
||||||
if (r == prev_r && g == prev_g && b == prev_b && a == prev_a) {
|
|
||||||
run++;
|
|
||||||
if (run == 62) {
|
|
||||||
fputc(0xC0 | (run - 1), f);
|
|
||||||
run = 0;
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (run > 0) {
|
|
||||||
fputc(0xC0 | (run - 1), f);
|
|
||||||
run = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint8_t idx_pos = PIKT_HASH(r, g, b, a);
|
|
||||||
if (index[idx_pos].r == r && index[idx_pos].g == g &&
|
|
||||||
index[idx_pos].b == b && index[idx_pos].a == a) {
|
|
||||||
fputc(0x00 | idx_pos, f);
|
|
||||||
} else {
|
|
||||||
index[idx_pos].r = r;
|
|
||||||
index[idx_pos].g = g;
|
|
||||||
index[idx_pos].b = b;
|
|
||||||
index[idx_pos].a = a;
|
|
||||||
|
|
||||||
if (a == prev_a) {
|
|
||||||
int8_t s_dr = (int8_t)(r - prev_r);
|
|
||||||
int8_t s_dg = (int8_t)(g - prev_g);
|
|
||||||
int8_t s_db = (int8_t)(b - prev_b);
|
|
||||||
|
|
||||||
if (s_dr >= -2 && s_dr <= 1 && s_dg >= -2 && s_dg <= 1 && s_db >= -2 && s_db <= 1) {
|
|
||||||
fputc(0x40 | ((s_dr + 2) << 4) | ((s_dg + 2) << 2) | (s_db + 2), f);
|
|
||||||
} else {
|
|
||||||
int8_t dr_dg = s_dr - s_dg;
|
|
||||||
int8_t db_dg = s_db - s_dg;
|
|
||||||
|
|
||||||
if (s_dg >= -32 && s_dg <= 31 && dr_dg >= -8 && dr_dg <= 7 && db_dg >= -8 && db_dg <= 7) {
|
|
||||||
fputc(0x80 | (s_dg + 32), f);
|
|
||||||
fputc(((dr_dg + 8) << 4) | (db_dg + 8), f);
|
|
||||||
} else {
|
|
||||||
fputc(0xFE, f);
|
|
||||||
fputc(r, f); fputc(g, f); fputc(b, f);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
fputc(0xFF, f);
|
|
||||||
fputc(r, f); fputc(g, f); fputc(b, f); fputc(a, f);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
prev_r = r; prev_g = g; prev_b = b; prev_a = a;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (run > 0) {
|
|
||||||
fputc(0xC0 | (run - 1), f);
|
|
||||||
}
|
|
||||||
|
|
||||||
fclose(f);
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
void pikture_free(pikture_t* img) {
|
|
||||||
if (img) {
|
|
||||||
if (img->pixels) {
|
|
||||||
free(img->pixels);
|
|
||||||
}
|
|
||||||
free(img);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
45
pikture.h
45
pikture.h
|
|
@ -1,45 +0,0 @@
|
||||||
/*
|
|
||||||
pikture.h
|
|
||||||
Native PIKTURE format encoder and decoder library.
|
|
||||||
Version: 100
|
|
||||||
License: MIT
|
|
||||||
Made by: Nexoniarz
|
|
||||||
*/
|
|
||||||
|
|
||||||
#ifndef PIKTURE_H
|
|
||||||
#define PIKTURE_H
|
|
||||||
|
|
||||||
#include <stdint.h>
|
|
||||||
#include <stddef.h>
|
|
||||||
|
|
||||||
/* Image data structure */
|
|
||||||
typedef struct {
|
|
||||||
uint16_t width;
|
|
||||||
uint16_t height;
|
|
||||||
uint8_t channels; /* Always 4 (RGBA) */
|
|
||||||
uint8_t* pixels; /* Raw RGBA pixel array */
|
|
||||||
} pikture_t;
|
|
||||||
|
|
||||||
#ifdef __cplusplus
|
|
||||||
extern "C" {
|
|
||||||
#endif
|
|
||||||
|
|
||||||
/* * Loads a PIKTURE image from the specified file path.
|
|
||||||
* Returns a pointer to a pikture_t struct, or NULL on failure.
|
|
||||||
*/
|
|
||||||
pikture_t* pikture_load(const char* filename);
|
|
||||||
|
|
||||||
/* * Saves a PIKTURE image to the specified file path.
|
|
||||||
* Returns 1 on success, 0 on failure.
|
|
||||||
*/
|
|
||||||
int pikture_save(const char* filename, const pikture_t* img);
|
|
||||||
|
|
||||||
/* * Frees the memory allocated for the PIKTURE image.
|
|
||||||
*/
|
|
||||||
void pikture_free(pikture_t* img);
|
|
||||||
|
|
||||||
#ifdef __cplusplus
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#endif /* PIKTURE_H */
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue