Compare commits

...
Author SHA1 Message Date
Nexoniarz
06a4e7db9e
Merge pull request #4
Extensions and bindings read YAPF 2; Nix package
2026-10-11 12:04:16 +02:00
Claude
11ebb12270
README: extensions and bindings read YAPF 2
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DVrVthWN8GsmgHGUhhsCYm
2026-10-11 09:57:52 +00:00
Claude
0ba1f0af17
JavaScript port decodes YAPF 2; VS Code and Figma open every file
yapf.js: decodeTexture (container, yapf-sum checks, code books with
context decoding, 16-bit / float planes, GPU block tiles), BC1-BC7 /
BC6H block decoding and textureToRGBA for display. Matches the C library
on 417 generated files (surfaces bit-exact) and survives fuzzing with
clean YapfErrors; fixtures in bindings/js/test-data keep it checked.

VS Code preview: pickers for mip, face, layer, frame and slice; storage,
normal-map convention and version in the info bar. Figma import:
textures converted to 8-bit, 8-bit images stay exact.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DVrVthWN8GsmgHGUhhsCYm
2026-10-11 09:57:52 +00:00
Claude
fdbd0a6b8f
Python binding and Blender read YAPF 2
yapf.load_texture / decode_texture expose every surface and the texture
description; rgba_float() for display (blocks decoded, 16-bit / float
kept). Blender imports 16-bit / HDR as float images packed as EXR,
decodes GPU blocks, follows the normal-map convention (DirectX flip, Z
rebuilt for XY normals). Errors say why. Tested in Blender 4.4.3.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DVrVthWN8GsmgHGUhhsCYm
2026-10-11 09:57:52 +00:00
Claude
a6c032d824
Extensions read YAPF 2; Nix package
- KDE thumbnailer: texture loader, decodes only the mip it needs; GPU
  blocks, 16-bit and float via the new yapf_tex_to_rgba8. Tested through
  KDE's plugin loader with KF6 from nixpkgs.
- Photoshop: shows the tool's error message; HDR textures open as 32-bit
  (.hdr), 16-bit stays 16-bit; 16-bit documents save as 16-bit.
- CLI: 16-bit PNG export, readable conversion errors.
- default.nix: yapf tool, library, header, MIME type and thumbnailer
  (absolute store path); runs the test suite.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DVrVthWN8GsmgHGUhhsCYm
2026-10-11 09:57:52 +00:00
Claude
83588d8e2a
GIMP plugin: open every YAPF 2 file and say why a file fails
Uses the texture loader: GPU blocks are decoded for viewing, 16-bit and
float files open at full precision, and cube faces, array layers, slices
and animation frames become layers. Load errors name the reason instead
of a generic failure.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DVrVthWN8GsmgHGUhhsCYm
2026-10-11 09:57:51 +00:00
Nexoniarz
7b1af7f275
Merge pull request #3
GIMP plugin: NixOS development shell
2026-10-11 11:22:06 +02:00
Claude
fb7212c62b
GIMP plugin: NixOS development shell
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DVrVthWN8GsmgHGUhhsCYm
2026-10-11 09:09:51 +00:00
Nexoniarz
6afd0488aa
Merge pull request #2
Add YAPF version 2: texture container with GPU blocks and metadata
2026-10-11 00:14:49 +02:00
Claude
a226b06368
YAPF 2: texture container with GPU blocks, wide samples and streaming
Adds format version 2 alongside version 1 (still written for plain 8-bit
images, so existing readers keep working). Full specification in
docs/FORMAT.md.

- GPU block payloads: BC1-BC7, BC6H, ETC2/EAC, ASTC 4x4-12x12, in tiles
  of 16x16 blocks. Built-in BC1/BC3/BC4/BC5/BC7/BC6H encoders and
  BC1-BC7 decoders (BC6H/BC7 bit-exact with a reference decoder).
- Lossless 16-bit, half and float pixels as byte planes over the 8-bit
  coder; order-preserving float mapping, transforms at sample width.
- Cubemaps, cube arrays, texture arrays, volumes, animation frames.
- Channel meanings, normal convention, linear mask, primaries/transfer,
  mip filter and coverage-preserving mips (yapf_tex_build_mips).
- Small mips stored first, file size in the header, surface/tile tables:
  decode from a file prefix and single tiles (virtual texturing).
- yapf-sum checksums on header, tables and every tile; needs bits so
  decoders refuse cleanly what they cannot read.
- Optional entropy stage: per-surface Huffman code books with a
  neighbourhood context (~10% smaller photos) and per-lane packing of
  GPU blocks (~3x smaller on disk); planes decoded interleaved.
- Notes: author, licence, source, tool, ICC and application tags.
- CLI: --format, --entropy, --coverage, --normal, --map, colour options,
  16-bit/HDR input, pack cube/array/volume/anim, info, verify, export.
- Tests for all of the above, including fuzzing; v1 API reads 8-bit v2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DVrVthWN8GsmgHGUhhsCYm
2026-10-10 21:57:14 +00:00
Nexoniarz
5642b8c85d
Merge YAPF v1: fast format, SIMD, tools, plugins and language bindings
YAPF v1: fast byte-aligned format, SIMD, tools and plugins
2026-10-03 14:58:59 +02:00
Nexoniarz
d7f494a0c9 Language bindings, KDE thumbnails, yapf compare, in-memory encoding
- yapf_encode() / yapf_free_buffer(): encode to memory; yapf_save uses it
- yapf.c also compiles as C++
- `yapf compare`: size and decode speed of the original format vs YAPF,
  with a lossless check
- bindings with tested examples: C++ (header-only), C# (P/Invoke),
  Rust (crate, builds the C code), Go (cgo), Java (FFM API, Java 22+),
  Zig (@cImport), LuaJIT (FFI), TypeScript types for the JS port;
  Python now encodes in memory
- docs/LANGUAGES.md and examples/ with measured results
- KDE Dolphin thumbnail plugin (KF6, CMake)
- README: honest status for Photoshop / VS Code / Figma (not run in the
  apps yet) and macOS (cross-compiled only), platform table

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 14:39:56 +02:00
Nexoniarz
2ca726e3af YAPF v1: fast byte-aligned format, SIMD, tools and plugins
Format (version 1, replaces the earlier experimental versions):
- per-tile YCoCg-R or subtract-green colour transform, 8-bit modular
- 12 row filters per 8-row band (Left, Gradient, Up-right, Up-average,
  Up 1-8 rows), none with more than a one-add serial dependency
- residuals in groups of 8 at the group's width (w bits x 8 = w bytes),
  4-bit control codes with repeat-previous-width runs chosen by DP
- constant and stored tiles, independent 64x64 tiles, mips, GPU hints
- all sizes validated before decoding; corrupt input is rejected

Library:
- SIMD on by default: SSSE3 (runtime CPUID check; GCC, Clang, MSVC,
  MinGW) and NEON on ARM64; -DYAPF_NO_SIMD for plain C
- yapf_load_memory, yapf_load_mt, yapf_load_memory_mt
- 6-10x faster decoding than PNG (stb_image) on one core; -O2 and -O3
  builds perform the same
- fixes for macOS (sysconf) and the Windows import library exports

Tools and integrations:
- `yapf` CLI: convert PNG/JPG/TGA/BMP/PSD/GIF <-> YAPF, info, bench,
  thumbnail, mips
- JavaScript and Python bindings (byte-identical to the C encoder)
- GIMP 3 plugin: exports the flattened image, optional mipmaps
- Blender extension: import (also as plane), export, drag and drop
- VS Code preview, Figma plugin, Photoshop scripts, Linux MIME type and
  thumbnailer
- Makefile, updated build.sh / build.bat, tools/build_release.sh
- tests/test_yapf.c (784 checks) and bindings/js/test.js

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 14:17:59 +02:00
Nexoniarz
b9b8c93a22
Add files via upload 2026-06-20 15:03:21 +02:00
Nexoniarz
0a2b234169
Update and rename pikture-gimp.c to yapf-gimp.c 2026-06-20 13:02:33 +00:00
Nexoniarz
56b1837b76
Update howtocompile.txt 2026-06-20 13:01:49 +00:00
Nexoniarz
624d95fe71
Delete other/Pikture.svg 2026-06-20 13:01:13 +00:00
Nexoniarz
e5f076cf8a
Delete other/Pikture.pik 2026-06-20 13:01:04 +00:00
Nexoniarz
d5e01b9db9
Add files via upload 2026-06-20 15:00:39 +02:00
Nexoniarz
64e18fd593
Delete README.md 2026-06-20 12:59:57 +00:00
Nexoniarz
6ec9d0b7f0
Delete pikture.h 2026-06-20 12:59:48 +00:00
Nexoniarz
55fc84ca49
Delete pikture.c 2026-06-20 12:59:41 +00:00
Nexoniarz
510ba7fd42
Update build.sh 2026-06-20 12:59:28 +00:00
Nexoniarz
d218fbaa8f
Update build.bat 2026-06-20 12:59:11 +00:00
Nexoniarz
01f5e4f9fe
Update pikture-gimp.c 2026-06-06 19:28:08 +00:00
Nexoniarz
f4754d81fd
Update pikture.h 2026-06-06 19:23:37 +00:00
Nexoniarz
021dd6b1ae
Update pikture.c 2026-06-06 19:23:23 +00:00
Nexoniarz
af3793b453
Update README.md 2026-06-06 19:22:13 +00:00
Nexoniarz
8c11a2f426
Update LICENSE 2026-06-06 19:21:41 +00:00
Nexoniarz
b17a7219cf
Add files via upload 2026-05-15 00:40:00 +02:00
Nexoniarz
c98090a1f3
Delete other/Pikture.pik 2026-05-14 22:39:47 +00:00
Nexoniarz
f021609a4f
Update pikture-gimp.c 2026-05-14 22:31:38 +00:00
Nexoniarz
2b5679806f
Update pikture.h 2026-05-14 22:31:19 +00:00
Nexoniarz
e6406519f2
Update pikture.c 2026-05-14 22:31:02 +00:00
Nexoniarz
31da3f5ac5
Update README.md 2026-05-14 22:22:35 +00:00
Nexoniarz
39e3959038
Update howtocompile.txt 2026-05-14 21:41:39 +00:00
Nexoniarz
d0dbd3ecf5
Update howtocompile.txt 2026-05-14 21:38:53 +00:00
Nexoniarz
75fd56ccc9
Update howtocompile.txt 2026-05-14 21:36:46 +00:00
Nexoniarz
9d5405e679
Update howtocompile.txt 2026-05-14 21:36:20 +00:00
Nexoniarz
0a8ab71de9
Rename howto.txt to howtocompile.txt 2026-05-14 21:35:09 +00:00
Nexoniarz
9f6beedb1c
Rename test to howto.txt 2026-05-14 21:34:54 +00:00
Nexoniarz
2927009b5f
Delete extensions/createdir 2026-05-14 21:34:34 +00:00
Nexoniarz
2991f6faba
Add files via upload 2026-05-14 23:34:22 +02:00
Nexoniarz
867b29561c
Create test 2026-05-14 21:33:36 +00:00
Nexoniarz
96c2f451ec
Create createdir 2026-05-14 21:33:17 +00:00
Nexoniarz
1854c6f482
Delete other/createdir 2026-05-14 21:30:34 +00:00
Nexoniarz
9477a9955a
Update README.md 2026-05-14 21:30:13 +00:00
Nexoniarz
e65859c880
Delete Pikture.svg 2026-05-14 21:29:46 +00:00
Nexoniarz
63dbe54400
Add files via upload 2026-05-14 23:29:31 +02:00
Nexoniarz
6d96de00dd
Create createdir 2026-05-14 21:29:04 +00:00
Nexoniarz
1ed497c872
Update README.md 2026-05-14 12:15:09 +00:00
Nexoniarz
8056537dc5
Update README.md 2026-05-14 12:14:42 +00:00
Nexoniarz
98b4d65798
Add files via upload 2026-05-14 14:13:13 +02:00
Nexoniarz
9786bbe917
Update README.md 2026-05-13 20:26:51 +00:00
Nexoniarz
737deaf2d5
Update build.bat 2026-05-13 20:25:34 +00:00
Nexoniarz
05bbaabedc
Update build.sh 2026-05-13 20:25:13 +00:00
Nexoniarz
71dfc2a073
Update README.md 2026-05-13 20:21:53 +00:00
Nexoniarz
95f269d4fc
Update README.md 2026-05-13 20:21:32 +00:00
Nexoniarz
374e4e4ff1
Update README.md 2026-05-13 20:21:04 +00:00
Nexoniarz
79a46b3ddf
Update pikture.h 2026-05-13 20:17:24 +00:00
Nexoniarz
0bd5fc6f4c
Update pikture.c 2026-05-13 20:16:59 +00:00
Nexoniarz
0ffc53fc0a
Update README.md 2026-05-12 17:59:11 +00:00
Nexoniarz
5826fa0813
Add files via upload 2026-05-12 03:16:20 +02:00
Nexoniarz
6a9d921be3
Update README.md 2026-05-12 01:14:13 +00:00
Nexoniarz
a58550e74f
Update README.md 2026-05-12 01:13:38 +00:00
Nexoniarz
21c70ba18e
Create README.md 2026-05-12 01:07:33 +00:00
107 changed files with 25742 additions and 295 deletions

25
.gitignore vendored Normal file
View 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
View file

@ -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
View 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
View 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
View 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
View 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
View 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

View 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>

View 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
View 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
View 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
View 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`.

View 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
View file

@ -0,0 +1,3 @@
module github.com/Nexoniarz/YAPF/bindings/go
go 1.21

111
bindings/go/yapf.go Normal file
View 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
View 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
View 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")
}
}

View 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
View 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
View 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
View 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
View 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
View 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" }
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View 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
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

91
bindings/js/test.js Normal file
View 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
View 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

File diff suppressed because it is too large Load diff

13
bindings/lua/README.md Normal file
View 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
View 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
View 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
View 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).

View 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")

Binary file not shown.

487
bindings/python/yapf.py Normal file
View 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
View 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
View 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
View 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());
}

View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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;
}

View 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`.

View 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()

View 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)

View 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"

View 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.**

View 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
View 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();
};

View 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
View 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 };
}));

View 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

File diff suppressed because it is too large Load diff

62
extensions/gimp/README.md Normal file
View 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.

View 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
View 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)

View 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
View 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.

View 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 ];
}

View 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"

View file

@ -0,0 +1,7 @@
{
"CacheThumbnail": true,
"KPlugin": {
"MimeTypes": ["image/x-yapf"],
"Name": "YAPF Images"
}
}

View 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
View 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."

View 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>

View file

@ -0,0 +1,4 @@
[Thumbnailer Entry]
TryExec=yapf
Exec=yapf thumbnail %i %o %s
MimeType=image/x-yapf;

View 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();
}
})();

View 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.

View 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)");
})();

View 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";

View 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.**

View 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 };

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View 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); }

View 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
})();

View 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"
}
}

View 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
View 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 &amp; 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

Binary file not shown.

229
pikture.c
View file

@ -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);
}
}

View file

@ -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