mirror of
https://github.com/Nexoniarz/YAPF.git
synced 2026-11-03 13:04:26 +00:00
Compare commits
107 changed files with 295 additions and 25742 deletions
25
.gitignore
vendored
25
.gitignore
vendored
|
|
@ -1,25 +0,0 @@
|
||||||
# build outputs
|
|
||||||
/build/
|
|
||||||
/dist/
|
|
||||||
*.o
|
|
||||||
*.a
|
|
||||||
*.so
|
|
||||||
*.dylib
|
|
||||||
*.dll
|
|
||||||
*.exe
|
|
||||||
*.vsix
|
|
||||||
test_yapf_tmp.yapf
|
|
||||||
|
|
||||||
# generated: copied from bindings/js/yapf.js when packaging
|
|
||||||
/extensions/vscode/media/yapf.js
|
|
||||||
node_modules/
|
|
||||||
__pycache__/
|
|
||||||
*.pyc
|
|
||||||
|
|
||||||
# Rust
|
|
||||||
/bindings/rust/target/
|
|
||||||
/bindings/rust/Cargo.lock
|
|
||||||
# C# / Go / Java build outputs
|
|
||||||
bin/
|
|
||||||
obj/
|
|
||||||
*.class
|
|
||||||
214
LICENSE
214
LICENSE
|
|
@ -1,201 +1,21 @@
|
||||||
Apache License
|
MIT License
|
||||||
Version 2.0, January 2004
|
|
||||||
http://www.apache.org/licenses/
|
|
||||||
|
|
||||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
Copyright (c) 2026 Nexoniarz
|
||||||
|
|
||||||
1. Definitions.
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
"License" shall mean the terms and conditions for use, reproduction,
|
The above copyright notice and this permission notice shall be included in all
|
||||||
and distribution as defined by Sections 1 through 9 of this document.
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
"Licensor" shall mean the copyright owner or entity authorized by
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
the copyright owner that is granting the License.
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
"Legal Entity" shall mean the union of the acting entity and all
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
other entities that control, are controlled by, or are under common
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
control with that entity. For the purposes of this definition,
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
"control" means (i) the power, direct or indirect, to cause the
|
SOFTWARE.
|
||||||
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
65
Makefile
|
|
@ -1,65 +0,0 @@
|
||||||
# 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
457
README.md
|
|
@ -1,457 +0,0 @@
|
||||||
# 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).
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
# 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`
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
// 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");
|
|
||||||
}
|
|
||||||
|
|
@ -1,98 +0,0 @@
|
||||||
// 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
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
<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>
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
// 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");
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
# 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`).
|
|
||||||
|
|
@ -1,121 +0,0 @@
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
# 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`.
|
|
||||||
|
|
@ -1,47 +0,0 @@
|
||||||
// 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")
|
|
||||||
}
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
module github.com/Nexoniarz/YAPF/bindings/go
|
|
||||||
|
|
||||||
go 1.21
|
|
||||||
|
|
@ -1,111 +0,0 @@
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
// Builds the C library as part of this Go package (cgo compiles .c files here).
|
|
||||||
#include "../../yapf.c"
|
|
||||||
|
|
@ -1,46 +0,0 @@
|
||||||
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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
// 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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
# 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`
|
|
||||||
|
|
@ -1,121 +0,0 @@
|
||||||
// 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)); }
|
|
||||||
}
|
|
||||||
|
|
@ -1,43 +0,0 @@
|
||||||
# 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/`.
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
// 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}`);
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
{
|
|
||||||
"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.
|
|
@ -1,137 +0,0 @@
|
||||||
{
|
|
||||||
"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.
|
|
@ -1,91 +0,0 @@
|
||||||
/* 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
25
bindings/js/yapf.d.ts
vendored
|
|
@ -1,25 +0,0 @@
|
||||||
// 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
1168
bindings/js/yapf.js
File diff suppressed because it is too large
Load diff
|
|
@ -1,13 +0,0 @@
|
||||||
# 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`
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
-- 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)))
|
|
||||||
|
|
@ -1,71 +0,0 @@
|
||||||
-- 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
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
# 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).
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
"""
|
|
||||||
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.
|
|
@ -1,487 +0,0 @@
|
||||||
"""
|
|
||||||
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())
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
[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"
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
# 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`.
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
// 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());
|
|
||||||
}
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
// 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(())
|
|
||||||
}
|
|
||||||
|
|
@ -1,156 +0,0 @@
|
||||||
//! 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());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
# 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`.
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
// 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
263
build.bat
|
|
@ -1,263 +0,0 @@
|
||||||
@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
301
build.sh
|
|
@ -1,301 +0,0 @@
|
||||||
#!/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
39
default.nix
|
|
@ -1,39 +0,0 @@
|
||||||
# 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
450
docs/FORMAT.md
|
|
@ -1,450 +0,0 @@
|
||||||
# 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.
|
|
||||||
|
|
@ -1,153 +0,0 @@
|
||||||
# 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)`.
|
|
||||||
|
|
@ -1,62 +0,0 @@
|
||||||
# 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.
|
|
||||||
|
|
@ -1,61 +0,0 @@
|
||||||
/*
|
|
||||||
* 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;
|
|
||||||
}
|
|
||||||
|
|
@ -1,44 +0,0 @@
|
||||||
# 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`.
|
|
||||||
|
|
@ -1,59 +0,0 @@
|
||||||
#!/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()
|
|
||||||
|
|
@ -1,282 +0,0 @@
|
||||||
# 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)
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
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"
|
|
||||||
|
|
@ -1,45 +0,0 @@
|
||||||
# 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.**
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
// 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)');
|
|
||||||
|
|
@ -1,54 +0,0 @@
|
||||||
// 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();
|
|
||||||
};
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
{
|
|
||||||
"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" }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
@ -1,126 +0,0 @@
|
||||||
/*
|
|
||||||
* 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 };
|
|
||||||
}));
|
|
||||||
|
|
@ -1,136 +0,0 @@
|
||||||
<!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>
|
|
||||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,62 +0,0 @@
|
||||||
# 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.
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
# 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 ];
|
|
||||||
}
|
|
||||||
|
|
@ -1,420 +0,0 @@
|
||||||
/*
|
|
||||||
* 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)
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
# 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)
|
|
||||||
|
|
@ -1,56 +0,0 @@
|
||||||
# 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.
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
# 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 ];
|
|
||||||
}
|
|
||||||
|
|
@ -1,80 +0,0 @@
|
||||||
// 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"
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
{
|
|
||||||
"CacheThumbnail": true,
|
|
||||||
"KPlugin": {
|
|
||||||
"MimeTypes": ["image/x-yapf"],
|
|
||||||
"Name": "YAPF Images"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
# 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).
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
#!/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."
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
<?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>
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
[Thumbnailer Entry]
|
|
||||||
TryExec=yapf
|
|
||||||
Exec=yapf thumbnail %i %o %s
|
|
||||||
MimeType=image/x-yapf;
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
// 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();
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
@ -1,43 +0,0 @@
|
||||||
# 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.
|
|
||||||
|
|
@ -1,42 +0,0 @@
|
||||||
// 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)");
|
|
||||||
})();
|
|
||||||
|
|
@ -1,77 +0,0 @@
|
||||||
// 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";
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
# 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.**
|
|
||||||
|
|
@ -1,65 +0,0 @@
|
||||||
// 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.
|
Before Width: | Height: | Size: 12 KiB |
|
|
@ -1,26 +0,0 @@
|
||||||
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); }
|
|
||||||
|
|
@ -1,141 +0,0 @@
|
||||||
// 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
|
|
||||||
})();
|
|
||||||
|
|
@ -1,29 +0,0 @@
|
||||||
{
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
// 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
211
other/YAPF.SVG
|
|
@ -1,211 +0,0 @@
|
||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
|
||||||
|
|
||||||
<svg
|
|
||||||
width="512"
|
|
||||||
height="512"
|
|
||||||
viewBox="0 0 512 512"
|
|
||||||
version="1.1"
|
|
||||||
id="svg1"
|
|
||||||
inkscape:version="1.4.4 (dcaf3e7d9e, 2026-05-05)"
|
|
||||||
sodipodi:docname="YAPF.svg"
|
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
|
||||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg">
|
|
||||||
<sodipodi:namedview
|
|
||||||
id="namedview1"
|
|
||||||
pagecolor="#000000"
|
|
||||||
bordercolor="#000000"
|
|
||||||
borderopacity="0.25"
|
|
||||||
inkscape:showpageshadow="2"
|
|
||||||
inkscape:pageopacity="0.0"
|
|
||||||
inkscape:pagecheckerboard="0"
|
|
||||||
inkscape:deskcolor="#d1d1d1"
|
|
||||||
inkscape:document-units="px"
|
|
||||||
inkscape:zoom="0.7071068"
|
|
||||||
inkscape:cx="-70.710676"
|
|
||||||
inkscape:cy="25.455843"
|
|
||||||
inkscape:window-width="1920"
|
|
||||||
inkscape:window-height="1014"
|
|
||||||
inkscape:window-x="0"
|
|
||||||
inkscape:window-y="0"
|
|
||||||
inkscape:window-maximized="1"
|
|
||||||
inkscape:current-layer="layer1" />
|
|
||||||
<defs
|
|
||||||
id="defs1">
|
|
||||||
<linearGradient
|
|
||||||
id="linearGradient12"
|
|
||||||
inkscape:collect="always">
|
|
||||||
<stop
|
|
||||||
style="stop-color:#b2b2b2;stop-opacity:0.29873046;"
|
|
||||||
offset="0"
|
|
||||||
id="stop11" />
|
|
||||||
<stop
|
|
||||||
style="stop-color:#ffffff;stop-opacity:0.29689941;"
|
|
||||||
offset="1"
|
|
||||||
id="stop12" />
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient
|
|
||||||
id="linearGradient9"
|
|
||||||
inkscape:collect="always">
|
|
||||||
<stop
|
|
||||||
style="stop-color:#cc0000;stop-opacity:1;"
|
|
||||||
offset="0"
|
|
||||||
id="stop9" />
|
|
||||||
<stop
|
|
||||||
style="stop-color:#ff0101;stop-opacity:1;"
|
|
||||||
offset="1"
|
|
||||||
id="stop10" />
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient
|
|
||||||
id="linearGradient7"
|
|
||||||
inkscape:collect="always">
|
|
||||||
<stop
|
|
||||||
style="stop-color:#005dcc;stop-opacity:1;"
|
|
||||||
offset="0"
|
|
||||||
id="stop7" />
|
|
||||||
<stop
|
|
||||||
style="stop-color:#0173ff;stop-opacity:1;"
|
|
||||||
offset="1"
|
|
||||||
id="stop8" />
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient
|
|
||||||
id="linearGradient5"
|
|
||||||
inkscape:collect="always">
|
|
||||||
<stop
|
|
||||||
style="stop-color:#00cc00;stop-opacity:1;"
|
|
||||||
offset="0"
|
|
||||||
id="stop5" />
|
|
||||||
<stop
|
|
||||||
style="stop-color:#00fe00;stop-opacity:1;"
|
|
||||||
offset="1"
|
|
||||||
id="stop6" />
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient
|
|
||||||
inkscape:collect="always"
|
|
||||||
xlink:href="#linearGradient5"
|
|
||||||
id="linearGradient6"
|
|
||||||
x1="241.51726"
|
|
||||||
y1="191.04054"
|
|
||||||
x2="322.48398"
|
|
||||||
y2="280.9632"
|
|
||||||
gradientUnits="userSpaceOnUse" />
|
|
||||||
<linearGradient
|
|
||||||
inkscape:collect="always"
|
|
||||||
xlink:href="#linearGradient7"
|
|
||||||
id="linearGradient8"
|
|
||||||
x1="224.7276"
|
|
||||||
y1="314.2731"
|
|
||||||
x2="235.2737"
|
|
||||||
y2="193.73064"
|
|
||||||
gradientUnits="userSpaceOnUse" />
|
|
||||||
<linearGradient
|
|
||||||
inkscape:collect="always"
|
|
||||||
xlink:href="#linearGradient9"
|
|
||||||
id="linearGradient10"
|
|
||||||
x1="297.91324"
|
|
||||||
y1="183.58171"
|
|
||||||
x2="178.08795"
|
|
||||||
y2="200.42204"
|
|
||||||
gradientUnits="userSpaceOnUse" />
|
|
||||||
<linearGradient
|
|
||||||
inkscape:collect="always"
|
|
||||||
xlink:href="#linearGradient12"
|
|
||||||
id="linearGradient8-8"
|
|
||||||
x1="178.14076"
|
|
||||||
y1="285.16241"
|
|
||||||
x2="281.86047"
|
|
||||||
y2="222.84132"
|
|
||||||
gradientUnits="userSpaceOnUse"
|
|
||||||
gradientTransform="translate(23.169404,-30.5)" />
|
|
||||||
</defs>
|
|
||||||
<g
|
|
||||||
inkscape:label="YAPF | CC0 - Public Domain"
|
|
||||||
inkscape:groupmode="layer"
|
|
||||||
id="layer1">
|
|
||||||
<g
|
|
||||||
id="g13"
|
|
||||||
transform="matrix(2.0095459,0,0,2.0095459,-258.52167,-127.95876)"
|
|
||||||
inkscape:label="YAPF">
|
|
||||||
<g
|
|
||||||
id="g12"
|
|
||||||
inkscape:label="LOGO & TEXT"
|
|
||||||
transform="matrix(1.2422505,0,0,1.2422505,-61.21895,-95.418348)">
|
|
||||||
<g
|
|
||||||
id="g10"
|
|
||||||
transform="matrix(1.0061309,0,0,1,-2.0998398,6.6753885)"
|
|
||||||
inkscape:label="LOGO">
|
|
||||||
<rect
|
|
||||||
style="fill:url(#linearGradient10);fill-opacity:1;stroke-width:0.236333"
|
|
||||||
id="rect1-3"
|
|
||||||
width="121.00286"
|
|
||||||
height="121.00375"
|
|
||||||
x="177.49921"
|
|
||||||
y="131.5"
|
|
||||||
ry="60.501431"
|
|
||||||
inkscape:label="Red" />
|
|
||||||
<rect
|
|
||||||
style="fill:url(#linearGradient8);fill-opacity:1;stroke-width:0.236333"
|
|
||||||
id="rect1-3-5"
|
|
||||||
width="121.00286"
|
|
||||||
height="121.00375"
|
|
||||||
x="169.49921"
|
|
||||||
y="193.5"
|
|
||||||
ry="60.501431"
|
|
||||||
inkscape:label="Blue" />
|
|
||||||
<rect
|
|
||||||
style="fill:url(#linearGradient6);fill-opacity:1;stroke:none;stroke-width:0.236333"
|
|
||||||
id="rect1-3-5-3"
|
|
||||||
width="121.00286"
|
|
||||||
height="121.00375"
|
|
||||||
x="221.49921"
|
|
||||||
y="175.5"
|
|
||||||
ry="60.501431"
|
|
||||||
inkscape:label="Green" />
|
|
||||||
<rect
|
|
||||||
style="fill:url(#linearGradient8-8);fill-opacity:1;stroke-width:0.236333"
|
|
||||||
id="rect1-3-5-31"
|
|
||||||
width="121.00286"
|
|
||||||
height="121.00375"
|
|
||||||
x="192.66861"
|
|
||||||
y="163"
|
|
||||||
ry="60.501431"
|
|
||||||
inkscape:label="Alpha" />
|
|
||||||
</g>
|
|
||||||
<text
|
|
||||||
xml:space="preserve"
|
|
||||||
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:10.6695px;font-family:'Liberation Mono';-inkscape-font-specification:'Liberation Mono Bold';text-align:center;writing-mode:lr-tb;direction:ltr;text-anchor:middle;fill:#ffffff;fill-opacity:1;stroke-width:0.889127"
|
|
||||||
x="262.40924"
|
|
||||||
y="264.6781"
|
|
||||||
id="text2"
|
|
||||||
transform="scale(0.96401161,1.0373319)"
|
|
||||||
inkscape:label="TEXT"><tspan
|
|
||||||
sodipodi:role="line"
|
|
||||||
id="tspan2"
|
|
||||||
x="262.40924"
|
|
||||||
y="264.6781"
|
|
||||||
style="font-style:italic;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:10.6695px;font-family:'Liberation Mono';-inkscape-font-specification:'Liberation Mono Bold Italic';writing-mode:lr-tb;direction:ltr;fill:#ffffff;fill-opacity:1;stroke-width:0.889127">Yet Another </tspan><tspan
|
|
||||||
sodipodi:role="line"
|
|
||||||
x="262.40924"
|
|
||||||
y="278.01498"
|
|
||||||
style="font-style:italic;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:10.6695px;font-family:'Liberation Mono';-inkscape-font-specification:'Liberation Mono Bold Italic';writing-mode:lr-tb;direction:ltr;fill:#ffffff;fill-opacity:1;stroke-width:0.889127"
|
|
||||||
id="tspan1">Picture Format</tspan></text>
|
|
||||||
<text
|
|
||||||
xml:space="preserve"
|
|
||||||
style="font-style:italic;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:44.4681px;font-family:'Liberation Mono';-inkscape-font-specification:'Liberation Mono Bold Italic';writing-mode:lr-tb;direction:ltr;fill:#f9f9f9;stroke-width:3.70568"
|
|
||||||
x="170.51646"
|
|
||||||
y="284.09799"
|
|
||||||
id="text3"
|
|
||||||
transform="matrix(1.0205207,-0.13368391,0.07735525,0.96975873,0,0)"
|
|
||||||
inkscape:label="TEST2"><tspan
|
|
||||||
sodipodi:role="line"
|
|
||||||
id="tspan3"
|
|
||||||
style="fill:#f9f9f9;stroke-width:3.70568"
|
|
||||||
x="170.51646"
|
|
||||||
y="284.09799">YAPF</tspan></text>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 7.2 KiB |
BIN
other/YAPF.YAPF
BIN
other/YAPF.YAPF
Binary file not shown.
229
pikture.c
Normal file
229
pikture.c
Normal file
|
|
@ -0,0 +1,229 @@
|
||||||
|
/*
|
||||||
|
pikture.c
|
||||||
|
Native PIKTURE format encoder and decoder library.
|
||||||
|
Version: 100
|
||||||
|
License: MIT
|
||||||
|
Made by: Nexoniarz
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "pikture.h"
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#define PIKT_MAGIC "PIK!"
|
||||||
|
#define PIKT_VERSION 5
|
||||||
|
#define PIKT_HASH(r, g, b, a) (((r) * 3 + (g) * 5 + (b) * 7 + (a) * 11) % 64)
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint8_t r, g, b, a;
|
||||||
|
} pikt_pixel;
|
||||||
|
|
||||||
|
/* Reads a 16-bit unsigned integer in Big-Endian format */
|
||||||
|
static uint16_t read_u16_be(FILE* f) {
|
||||||
|
uint8_t bytes[2];
|
||||||
|
fread(bytes, 1, 2, f);
|
||||||
|
return (bytes[0] << 8) | bytes[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Writes a 16-bit unsigned integer in Big-Endian format */
|
||||||
|
static void write_u16_be(FILE* f, uint16_t value) {
|
||||||
|
uint8_t bytes[2] = { (value >> 8) & 0xFF, value & 0xFF };
|
||||||
|
fwrite(bytes, 1, 2, f);
|
||||||
|
}
|
||||||
|
|
||||||
|
pikture_t* pikture_load(const char* filename) {
|
||||||
|
FILE* f = fopen(filename, "rb");
|
||||||
|
if (!f) return NULL;
|
||||||
|
|
||||||
|
char magic[4];
|
||||||
|
fread(magic, 1, 4, f);
|
||||||
|
if (memcmp(magic, PIKT_MAGIC, 4) != 0) {
|
||||||
|
fclose(f);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t version;
|
||||||
|
fread(&version, 1, 1, f);
|
||||||
|
if (version != PIKT_VERSION) {
|
||||||
|
fclose(f);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
pikture_t* img = (pikture_t*)malloc(sizeof(pikture_t));
|
||||||
|
if (!img) {
|
||||||
|
fclose(f);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
img->width = read_u16_be(f);
|
||||||
|
img->height = read_u16_be(f);
|
||||||
|
|
||||||
|
uint8_t depth;
|
||||||
|
fread(&depth, 1, 1, f);
|
||||||
|
img->channels = 4;
|
||||||
|
|
||||||
|
size_t data_size = (size_t)img->width * img->height * 4;
|
||||||
|
img->pixels = (uint8_t*)malloc(data_size);
|
||||||
|
if (!img->pixels) {
|
||||||
|
free(img);
|
||||||
|
fclose(f);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
pikt_pixel index[64] = {0};
|
||||||
|
uint8_t r = 0, g = 0, b = 0, a = 255;
|
||||||
|
|
||||||
|
size_t ptr = 0;
|
||||||
|
while (ptr < data_size) {
|
||||||
|
int b0 = fgetc(f);
|
||||||
|
if (b0 == EOF) break;
|
||||||
|
|
||||||
|
if (b0 == 0xFE) {
|
||||||
|
r = fgetc(f);
|
||||||
|
g = fgetc(f);
|
||||||
|
b = fgetc(f);
|
||||||
|
} else if (b0 == 0xFF) {
|
||||||
|
r = fgetc(f);
|
||||||
|
g = fgetc(f);
|
||||||
|
b = fgetc(f);
|
||||||
|
a = fgetc(f);
|
||||||
|
} else {
|
||||||
|
uint8_t match = b0 & 0xC0;
|
||||||
|
if (match == 0x00) {
|
||||||
|
pikt_pixel p = index[b0 & 0x3F];
|
||||||
|
r = p.r; g = p.g; b = p.b; a = p.a;
|
||||||
|
} else if (match == 0x40) {
|
||||||
|
r = (uint8_t)(r + ((b0 >> 4) & 0x03) - 2);
|
||||||
|
g = (uint8_t)(g + ((b0 >> 2) & 0x03) - 2);
|
||||||
|
b = (uint8_t)(b + (b0 & 0x03) - 2);
|
||||||
|
} else if (match == 0x80) {
|
||||||
|
int b1 = fgetc(f);
|
||||||
|
int8_t dg = (b0 & 0x3F) - 32;
|
||||||
|
r = (uint8_t)(r + dg + ((b1 >> 4) & 0x0F) - 8);
|
||||||
|
g = (uint8_t)(g + dg);
|
||||||
|
b = (uint8_t)(b + dg + (b1 & 0x0F) - 8);
|
||||||
|
} else if (match == 0xC0) {
|
||||||
|
int run = (b0 & 0x3F) + 1;
|
||||||
|
for (int i = 0; i < run && ptr < data_size; i++) {
|
||||||
|
img->pixels[ptr++] = r;
|
||||||
|
img->pixels[ptr++] = g;
|
||||||
|
img->pixels[ptr++] = b;
|
||||||
|
img->pixels[ptr++] = a;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t idx_pos = PIKT_HASH(r, g, b, a);
|
||||||
|
index[idx_pos].r = r;
|
||||||
|
index[idx_pos].g = g;
|
||||||
|
index[idx_pos].b = b;
|
||||||
|
index[idx_pos].a = a;
|
||||||
|
|
||||||
|
if (ptr < data_size) {
|
||||||
|
img->pixels[ptr++] = r;
|
||||||
|
img->pixels[ptr++] = g;
|
||||||
|
img->pixels[ptr++] = b;
|
||||||
|
img->pixels[ptr++] = a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fclose(f);
|
||||||
|
return img;
|
||||||
|
}
|
||||||
|
|
||||||
|
int pikture_save(const char* filename, const pikture_t* img) {
|
||||||
|
if (!img || !img->pixels) return 0;
|
||||||
|
|
||||||
|
FILE* f = fopen(filename, "wb");
|
||||||
|
if (!f) return 0;
|
||||||
|
|
||||||
|
fwrite(PIKT_MAGIC, 1, 4, f);
|
||||||
|
uint8_t version = PIKT_VERSION;
|
||||||
|
fwrite(&version, 1, 1, f);
|
||||||
|
write_u16_be(f, img->width);
|
||||||
|
write_u16_be(f, img->height);
|
||||||
|
uint8_t depth = 32;
|
||||||
|
fwrite(&depth, 1, 1, f);
|
||||||
|
|
||||||
|
pikt_pixel index[64] = {0};
|
||||||
|
uint8_t prev_r = 0, prev_g = 0, prev_b = 0, prev_a = 255;
|
||||||
|
int run = 0;
|
||||||
|
|
||||||
|
size_t total_pixels = (size_t)img->width * img->height;
|
||||||
|
size_t px_idx = 0;
|
||||||
|
|
||||||
|
while (px_idx < total_pixels * 4) {
|
||||||
|
uint8_t r = img->pixels[px_idx++];
|
||||||
|
uint8_t g = img->pixels[px_idx++];
|
||||||
|
uint8_t b = img->pixels[px_idx++];
|
||||||
|
uint8_t a = img->pixels[px_idx++];
|
||||||
|
|
||||||
|
if (r == prev_r && g == prev_g && b == prev_b && a == prev_a) {
|
||||||
|
run++;
|
||||||
|
if (run == 62) {
|
||||||
|
fputc(0xC0 | (run - 1), f);
|
||||||
|
run = 0;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (run > 0) {
|
||||||
|
fputc(0xC0 | (run - 1), f);
|
||||||
|
run = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t idx_pos = PIKT_HASH(r, g, b, a);
|
||||||
|
if (index[idx_pos].r == r && index[idx_pos].g == g &&
|
||||||
|
index[idx_pos].b == b && index[idx_pos].a == a) {
|
||||||
|
fputc(0x00 | idx_pos, f);
|
||||||
|
} else {
|
||||||
|
index[idx_pos].r = r;
|
||||||
|
index[idx_pos].g = g;
|
||||||
|
index[idx_pos].b = b;
|
||||||
|
index[idx_pos].a = a;
|
||||||
|
|
||||||
|
if (a == prev_a) {
|
||||||
|
int8_t s_dr = (int8_t)(r - prev_r);
|
||||||
|
int8_t s_dg = (int8_t)(g - prev_g);
|
||||||
|
int8_t s_db = (int8_t)(b - prev_b);
|
||||||
|
|
||||||
|
if (s_dr >= -2 && s_dr <= 1 && s_dg >= -2 && s_dg <= 1 && s_db >= -2 && s_db <= 1) {
|
||||||
|
fputc(0x40 | ((s_dr + 2) << 4) | ((s_dg + 2) << 2) | (s_db + 2), f);
|
||||||
|
} else {
|
||||||
|
int8_t dr_dg = s_dr - s_dg;
|
||||||
|
int8_t db_dg = s_db - s_dg;
|
||||||
|
|
||||||
|
if (s_dg >= -32 && s_dg <= 31 && dr_dg >= -8 && dr_dg <= 7 && db_dg >= -8 && db_dg <= 7) {
|
||||||
|
fputc(0x80 | (s_dg + 32), f);
|
||||||
|
fputc(((dr_dg + 8) << 4) | (db_dg + 8), f);
|
||||||
|
} else {
|
||||||
|
fputc(0xFE, f);
|
||||||
|
fputc(r, f); fputc(g, f); fputc(b, f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fputc(0xFF, f);
|
||||||
|
fputc(r, f); fputc(g, f); fputc(b, f); fputc(a, f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
prev_r = r; prev_g = g; prev_b = b; prev_a = a;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (run > 0) {
|
||||||
|
fputc(0xC0 | (run - 1), f);
|
||||||
|
}
|
||||||
|
|
||||||
|
fclose(f);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
void pikture_free(pikture_t* img) {
|
||||||
|
if (img) {
|
||||||
|
if (img->pixels) {
|
||||||
|
free(img->pixels);
|
||||||
|
}
|
||||||
|
free(img);
|
||||||
|
}
|
||||||
|
}
|
||||||
45
pikture.h
Normal file
45
pikture.h
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
/*
|
||||||
|
pikture.h
|
||||||
|
Native PIKTURE format encoder and decoder library.
|
||||||
|
Version: 100
|
||||||
|
License: MIT
|
||||||
|
Made by: Nexoniarz
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef PIKTURE_H
|
||||||
|
#define PIKTURE_H
|
||||||
|
|
||||||
|
#include <stdint.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
|
/* Image data structure */
|
||||||
|
typedef struct {
|
||||||
|
uint16_t width;
|
||||||
|
uint16_t height;
|
||||||
|
uint8_t channels; /* Always 4 (RGBA) */
|
||||||
|
uint8_t* pixels; /* Raw RGBA pixel array */
|
||||||
|
} pikture_t;
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
extern "C" {
|
||||||
|
#endif
|
||||||
|
|
||||||
|
/* * Loads a PIKTURE image from the specified file path.
|
||||||
|
* Returns a pointer to a pikture_t struct, or NULL on failure.
|
||||||
|
*/
|
||||||
|
pikture_t* pikture_load(const char* filename);
|
||||||
|
|
||||||
|
/* * Saves a PIKTURE image to the specified file path.
|
||||||
|
* Returns 1 on success, 0 on failure.
|
||||||
|
*/
|
||||||
|
int pikture_save(const char* filename, const pikture_t* img);
|
||||||
|
|
||||||
|
/* * Frees the memory allocated for the PIKTURE image.
|
||||||
|
*/
|
||||||
|
void pikture_free(pikture_t* img);
|
||||||
|
|
||||||
|
#ifdef __cplusplus
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif /* PIKTURE_H */
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue