From a6c032d824daef9ba734bce8dff1eb96b719b777 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 11 Oct 2026 09:40:30 +0000 Subject: [PATCH] Extensions read YAPF 2; Nix package - KDE thumbnailer: texture loader, decodes only the mip it needs; GPU blocks, 16-bit and float via the new yapf_tex_to_rgba8. Tested through KDE's plugin loader with KF6 from nixpkgs. - Photoshop: shows the tool's error message; HDR textures open as 32-bit (.hdr), 16-bit stays 16-bit; 16-bit documents save as 16-bit. - CLI: 16-bit PNG export, readable conversion errors. - default.nix: yapf tool, library, header, MIME type and thumbnailer (absolute store path); runs the test suite. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01DVrVthWN8GsmgHGUhhsCYm --- README.md | 1 + default.nix | 39 +++++++++++++ extensions/kde/README.md | 6 ++ extensions/kde/yapfthumbnail.cpp | 73 +++++++++++++------------ extensions/linux/README.md | 19 +++++++ extensions/photoshop/Open YAPF.jsx | 6 +- extensions/photoshop/README.md | 10 ++++ extensions/photoshop/Save as YAPF.jsx | 8 ++- extensions/photoshop/yapf_common.jsxinc | 42 +++++++++++++- tests/test_yapf.c | 30 ++++++++++ tools/yapf_cli.c | 72 ++++++++++++++++++++++-- yapf.c | 68 +++++++++++++++++++++++ yapf.h | 10 ++++ 13 files changed, 336 insertions(+), 48 deletions(-) create mode 100644 default.nix diff --git a/README.md b/README.md index 58a0f18..3b3315f 100644 --- a/README.md +++ b/README.md @@ -332,6 +332,7 @@ YAPF_CHANNELS_RGBA 4 | 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//` 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 | | diff --git a/default.nix b/default.nix new file mode 100644 index 0000000..fd79d78 --- /dev/null +++ b/default.nix @@ -0,0 +1,39 @@ +# YAPF for Nix / NixOS: the `yapf` tool, libyapf (static + shared) and +# yapf.h, plus the image/x-yapf MIME type and a freedesktop thumbnailer. +# +# nix-build build ./result +# nix-env -if . install for your user +# NixOS: environment.systemPackages = [ (pkgs.callPackage /path/to/YAPF { }) ]; +{ pkgs ? import { }, 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"; + }; +} diff --git a/extensions/kde/README.md b/extensions/kde/README.md index 6283557..f5b49bc 100644 --- a/extensions/kde/README.md +++ b/extensions/kde/README.md @@ -48,3 +48,9 @@ Log out and back in, then enable **YAPF Images** in Dolphin's preview settings. 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. diff --git a/extensions/kde/yapfthumbnail.cpp b/extensions/kde/yapfthumbnail.cpp index 6842a65..fe46adc 100644 --- a/extensions/kde/yapfthumbnail.cpp +++ b/extensions/kde/yapfthumbnail.cpp @@ -3,8 +3,11 @@ #include #include +#include #include +#include + extern "C" { #include "yapf.h" } @@ -20,52 +23,50 @@ public: KIO::ThumbnailResult create(const KIO::ThumbnailRequest &request) override { - const QByteArray path = request.url().toLocalFile().toLocal8Bit(); - yapf_image_t *img = yapf_load(path.constData()); - if (!img) { + 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(); } - // Start from the smallest stored mip level that still covers the - // requested size; Qt scales the rest. + // The smallest stored mip that still covers the requested size; + // Qt scales the rest. const QSize want = request.targetSize(); - int m = 0; - while (m + 1 < img->mip_levels && - int(img->width >> (m + 1)) >= want.width() && - int(img->height >> (m + 1)) >= want.height()) { + 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; } - const int w = qMax(1, int(img->width >> m)); - const int h = qMax(1, int(img->height >> m)); - const uchar *px = img->mips[m]; + yapf_tex_mip_size(tex, m, &w, &h, nullptr); + const uint32_t s = yapf_tex_surface(tex, 0, 0, 0, m); - QImage image; - switch (img->channels) { - case 1: - image = QImage(px, w, h, w, QImage::Format_Grayscale8).copy(); - break; - case 2: { // gray + alpha → RGBA - image = QImage(w, h, QImage::Format_RGBA8888); - for (int y = 0; y < h; ++y) { - const uchar *s = px + size_t(y) * w * 2; - uchar *d = image.scanLine(y); - for (int x = 0; x < w; ++x, s += 2, d += 4) { - d[0] = d[1] = d[2] = s[0]; - d[3] = s[1]; - } + 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(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); } - break; } - case 3: - image = QImage(px, w, h, w * 3, QImage::Format_RGB888).copy(); - break; - default: - image = QImage(px, w, h, w * 4, - (img->flags & YAPF_FLAG_PREMULT_ALPHA) ? QImage::Format_RGBA8888_Premultiplied - : QImage::Format_RGBA8888).copy(); - break; + yapf_tex_free(tex); + if (!ok) { + return KIO::ThumbnailResult::fail(); } - yapf_free(img); if (image.width() > want.width() || image.height() > want.height()) { image = image.scaled(want, Qt::KeepAspectRatio, Qt::SmoothTransformation); diff --git a/extensions/linux/README.md b/extensions/linux/README.md index 04c08f2..b3a4d86 100644 --- a/extensions/linux/README.md +++ b/extensions/linux/README.md @@ -13,3 +13,22 @@ 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). diff --git a/extensions/photoshop/Open YAPF.jsx b/extensions/photoshop/Open YAPF.jsx index 52860bf..addfb56 100644 --- a/extensions/photoshop/Open YAPF.jsx +++ b/extensions/photoshop/Open YAPF.jsx @@ -14,9 +14,11 @@ if (!(files instanceof Array)) files = [files]; for (var i = 0; i < files.length; i++) { - var tmp = yapfTempFile(".png"); + // 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" + YAPF_TOOL_HELP); + alert("Could not open " + files[i].name + ".\n\n" + yapfFailText()); return; } var doc = app.open(tmp); diff --git a/extensions/photoshop/README.md b/extensions/photoshop/README.md index 7cea292..7ce8fbf 100644 --- a/extensions/photoshop/README.md +++ b/extensions/photoshop/README.md @@ -25,6 +25,16 @@ native plugin. 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 diff --git a/extensions/photoshop/Save as YAPF.jsx b/extensions/photoshop/Save as YAPF.jsx index 1ca326c..05e7cf2 100644 --- a/extensions/photoshop/Save as YAPF.jsx +++ b/extensions/photoshop/Save as YAPF.jsx @@ -22,8 +22,10 @@ try { if (copy.mode !== DocumentMode.RGB && copy.mode !== DocumentMode.GRAYSCALE) copy.changeMode(ChangeMode.RGB); - if (copy.bitsPerChannel !== BitsPerChannelType.EIGHT) - copy.bitsPerChannel = BitsPerChannelType.EIGHT; + // 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); @@ -35,6 +37,6 @@ if (mips) args.push("--mips"); var ok = yapfRun(args, out); tmp.remove(); - if (!ok) { alert("Could not write " + out.name + ".\n\n" + YAPF_TOOL_HELP); return; } + if (!ok) { alert("Could not write " + out.name + ".\n\n" + yapfFailText()); return; } alert("Saved " + out.fsName + " (" + Math.round(out.length / 1024) + " KB)"); })(); diff --git a/extensions/photoshop/yapf_common.jsxinc b/extensions/photoshop/yapf_common.jsxinc index 2a77128..10e312e 100644 --- a/extensions/photoshop/yapf_common.jsxinc +++ b/extensions/photoshop/yapf_common.jsxinc @@ -20,16 +20,52 @@ function yapfTool() { return names[0]; // rely on the PATH } -// Runs the tool; returns true when `expectOut` was produced. -function yapfRun(args, expectOut) { +// 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 - if (expectOut.exists) expectOut.remove(); 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); diff --git a/tests/test_yapf.c b/tests/test_yapf.c index 000a457..c2952b2 100644 --- a/tests/test_yapf.c +++ b/tests/test_yapf.c @@ -632,6 +632,35 @@ static void v2_mips(void) { yapf_tex_free(t); } +static void v2_preview(void) { + uint8_t out[4 * 4 * 4]; + /* gray + alpha u16 */ + yapf_texture_t d = desc(4, 4, 2, YAPF_SAMPLE_U16); + yapf_texture_t *t = yapf_tex_create(&d); + uint16_t *p = (uint16_t *)t->data[0]; + for (int i = 0; i < 16; i++) { p[2 * i] = 65535; p[2 * i + 1] = 32896; } + int ok = yapf_tex_to_rgba8(t, 0, 0, out) == YAPF_OK && + out[0] == 255 && out[1] == 255 && out[2] == 255 && out[3] == 128; + yapf_tex_free(t); + /* linear half 0.2140 → sRGB 128, 2.0 clamps, alpha stays linear */ + d = desc(4, 4, 4, YAPF_SAMPLE_F16); + t = yapf_tex_create(&d); + p = (uint16_t *)t->data[0]; + for (int i = 0; i < 16; i++) { + p[4 * i] = yapf_float_to_half(0.2158605f); p[4 * i + 1] = yapf_float_to_half(2.0f); + p[4 * i + 2] = yapf_float_to_half(-1.0f); p[4 * i + 3] = yapf_float_to_half(0.5f); + } + ok = ok && yapf_tex_to_rgba8(t, 0, 0, out) == YAPF_OK && + out[0] == 128 && out[1] == 255 && out[2] == 0 && out[3] == 128; + /* BC7 and BC6H go through the block decoders */ + yapf_texture_t *c = NULL; + ok = ok && yapf_tex_compress(t, YAPF_GPU_BC6H, &c) == YAPF_OK && + yapf_tex_to_rgba8(c, 0, 0, out) == YAPF_OK && out[3] == 255 && out[1] == 255; + yapf_tex_free(c); + yapf_tex_free(t); + CHECK(ok, "yapf_tex_to_rgba8 converts every payload for display"); +} + static void v2_misc(void) { int ok = 1; for (uint32_t h = 0; h < 0x10000 && ok; h++) { @@ -697,6 +726,7 @@ int main(int argc, char **argv) { v2_gpu(); v2_mips(); v2_misc(); + v2_preview(); printf("%d tests, %d failures\n", tests, fails); return fails != 0; diff --git a/tools/yapf_cli.c b/tools/yapf_cli.c index af60926..b18330b 100644 --- a/tools/yapf_cli.c +++ b/tools/yapf_cli.c @@ -167,6 +167,8 @@ static int parse_map(const char *s, uint8_t *map) { return 1; } +static const char *err_name(int e); + /* ── Reading any input into a texture ────────────────────────────────── */ static yapf_texture_t *new_tex(uint32_t w, uint32_t h, int ch, int sample) { @@ -182,7 +184,7 @@ static yapf_texture_t *read_input(const char *path, const opts_t *o, int *from_y if (is_yapf(path)) { int err = 0; yapf_texture_t *t = yapf_tex_load(path, 0, 0, &err); - if (!t) fprintf(stderr, "yapf: cannot read %s (error %d)\n", path, err); + if (!t) fprintf(stderr, "yapf: cannot read %s: %s\n", path, err_name(err)); *from_yapf = 1; return t; } @@ -399,6 +401,58 @@ static uint8_t *to8(const float *f, size_t n) { return o; } +/* 16-bit PNG (stb_image_write only writes 8 bits): rows of big-endian + * samples, filter 0, compressed with stb's zlib. */ +static uint32_t png_crc(const uint8_t *p, size_t n, uint32_t c) { + c = ~c; + for (size_t i = 0; i < n; i++) { + c ^= p[i]; + for (int k = 0; k < 8; k++) c = (c >> 1) ^ (0xEDB88320u & (0u - (c & 1))); + } + return ~c; +} + +static void png_chunk(FILE *f, const char *type, const uint8_t *d, uint32_t n) { + uint8_t b[4] = { (uint8_t)(n >> 24), (uint8_t)(n >> 16), (uint8_t)(n >> 8), (uint8_t)n }; + fwrite(b, 1, 4, f); + fwrite(type, 1, 4, f); + if (n) fwrite(d, 1, n, f); + uint32_t c = png_crc(d, n, png_crc((const uint8_t *)type, 4, 0)); + uint8_t cb[4] = { (uint8_t)(c >> 24), (uint8_t)(c >> 16), (uint8_t)(c >> 8), (uint8_t)c }; + fwrite(cb, 1, 4, f); +} + +static int write_png16(const char *path, const uint16_t *px, int w, int h, int ch) { + size_t row = (size_t)w * ch * 2 + 1; + uint8_t *raw = (uint8_t *)malloc(row * h); + if (!raw) return 0; + for (int y = 0; y < h; y++) { + uint8_t *r = raw + row * y; + r[0] = 0; + for (int i = 0; i < w * ch; i++) { + uint16_t v = px[(size_t)y * w * ch + i]; + r[1 + 2 * i] = (uint8_t)(v >> 8); + r[2 + 2 * i] = (uint8_t)v; + } + } + int zlen = 0; + uint8_t *z = stbi_zlib_compress(raw, (int)(row * h), &zlen, 8); + free(raw); + FILE *f = z ? fopen(path, "wb") : NULL; + if (!f) { STBIW_FREE(z); return 0; } + static const uint8_t sig[8] = { 0x89, 'P', 'N', 'G', 13, 10, 26, 10 }; + static const uint8_t ctype[5] = { 0, 0, 4, 2, 6 }; + uint8_t ihdr[13] = { (uint8_t)(w >> 24), (uint8_t)(w >> 16), (uint8_t)(w >> 8), (uint8_t)w, + (uint8_t)(h >> 24), (uint8_t)(h >> 16), (uint8_t)(h >> 8), (uint8_t)h, + 16, ctype[ch], 0, 0, 0 }; + fwrite(sig, 1, 8, f); + png_chunk(f, "IHDR", ihdr, 13); + png_chunk(f, "IDAT", z, (uint32_t)zlen); + png_chunk(f, "IEND", NULL, 0); + STBIW_FREE(z); + return fclose(f) == 0; +} + static int write_image(const char *path, const void *px, int w, int h, int ch, int hdr) { if (ends_with(path, ".hdr")) { if (!hdr) { @@ -445,9 +499,19 @@ static int export_image(const yapf_texture_t *t, const char *in, const char *out : "no such slice, or out of memory"); return 1; } - if (t->sample == YAPF_SAMPLE_U16 && !ends_with(out, ".hdr")) - fprintf(stderr, "yapf: note: 16-bit samples written as 8-bit\n"); - int ok = write_image(out, px, (int)w, (int)h, ch, hdr); + int ok; + if (t->payload == YAPF_PAYLOAD_PIXELS && t->sample == YAPF_SAMPLE_U16 && ends_with(out, ".png")) { + /* keep all 16 bits */ + uint32_t m = s % t->mip_levels, d; + yapf_tex_mip_size(t, m, NULL, NULL, &d); + const uint16_t *src = (const uint16_t *)((const uint8_t *)t->data[s] + + yapf_tex_surface_bytes(t, m) / d * (uint32_t)o->slice); + ok = write_png16(out, src, (int)w, (int)h, t->channels); + } else { + if (t->sample == YAPF_SAMPLE_U16 && !ends_with(out, ".hdr")) + fprintf(stderr, "yapf: note: 16-bit samples written as 8-bit (use .png to keep them)\n"); + ok = write_image(out, px, (int)w, (int)h, ch, hdr); + } free(px); if (!ok) { fprintf(stderr, "yapf: cannot write %s\n", out); return 1; } return 0; diff --git a/yapf.c b/yapf.c index 5f49a19..281977e 100644 --- a/yapf.c +++ b/yapf.c @@ -4677,3 +4677,71 @@ int yapf_tex_build_mips(yapf_texture_t *tex, uint32_t levels, uint8_t alpha_ref) else tex->flags &= (uint8_t)~YAPF_FLAG_COVERAGE; return YAPF_OK; } + +/* ======================================================================== + * Previews + * ======================================================================== */ + +/* Linear 0..1 → sRGB code, exact to the 8-bit rounding (thresholds). */ +static uint8_t yapf__lin_to_code(const yapf__srgb_tab *t, float v) { + if (!(v > 0.0f)) return 0; /* also NaN */ + if (v >= 1.0f) return 255; + return (uint8_t)(yapf__lin_to_srgb(t, v) * 255.0f + 0.5f); +} + +int yapf_tex_to_rgba8(const yapf_texture_t *tex, uint32_t surface, uint32_t slice, uint8_t *out) { + uint32_t S; + if (!out || yapf__tex_check(tex, &S) != 0 || surface >= S || !tex->data || !tex->data[surface]) + return YAPF_ERR_INVALID; + uint32_t m = surface % tex->mip_levels, w, h, d; + yapf_tex_mip_size(tex, m, &w, &h, &d); + if (slice >= d) return YAPF_ERR_INVALID; + size_t n = (size_t)w * h; + const uint8_t *src = (const uint8_t *)tex->data[surface] + yapf_tex_surface_bytes(tex, m) / d * slice; + + int ch = tex->channels, sample = tex->sample; + uint16_t *half = NULL; + if (tex->payload == YAPF_PAYLOAD_BLOCKS) { + if (tex->format != YAPF_GPU_BC6H && tex->format != YAPF_GPU_BC6H_SF) { + int rc = yapf_blocks_decode(tex->format, src, w, h, out); + if (rc == YAPF_OK && tex->channels == 3) /* BC1 / BC7 without alpha */ + for (size_t i = 0; i < n; i++) out[i * 4 + 3] = 255; + return rc; + } + half = (uint16_t *)malloc(n * 3 * sizeof(uint16_t)); + if (!half) return YAPF_ERR_OOM; + yapf_blocks_decode(tex->format, src, w, h, half); + src = (const uint8_t *)half; + ch = 3; + sample = YAPF_SAMPLE_F16; + } + yapf__srgb_tab *st = NULL; + if (sample == YAPF_SAMPLE_F16 || sample == YAPF_SAMPLE_F32) { + st = (yapf__srgb_tab *)malloc(sizeof(yapf__srgb_tab)); + if (!st) { free(half); return YAPF_ERR_OOM; } + yapf__srgb_init(st); + } + size_t sb = yapf__sbytes(sample); + for (size_t i = 0; i < n; i++) { + uint8_t v[4] = { 0, 0, 0, 255 }; + for (int c = 0; c < ch; c++) { + uint32_t x = yapf__get_sample(src + (i * ch + c) * sb, sample); + int alpha = (ch == 2 && c == 1) || (ch == 4 && c == 3); + uint8_t b; + if (sample == YAPF_SAMPLE_U8) b = (uint8_t)x; + else if (sample == YAPF_SAMPLE_U16) b = (uint8_t)((x + 128) / 257); + else { + float f; + if (sample == YAPF_SAMPLE_F16) f = yapf_half_to_float((uint16_t)x); + else memcpy(&f, &x, 4); + b = alpha ? (uint8_t)yapf__clampi(f * 255.0f, 0, 255) : yapf__lin_to_code(st, f); + } + v[ch <= 2 ? (c == 0 ? 0 : 3) : c] = b; + } + if (ch <= 2) v[1] = v[2] = v[0]; + memcpy(out + i * 4, v, 4); + } + free(st); + free(half); + return YAPF_OK; +} diff --git a/yapf.h b/yapf.h index 5f57933..fdca43b 100644 --- a/yapf.h +++ b/yapf.h @@ -601,6 +601,16 @@ int yapf_tex_compress(const yapf_texture_t *src, uint8_t format, int yapf_blocks_decode(uint8_t format, const void *blocks, uint32_t w, uint32_t h, void *out); +/* + * yapf_tex_to_rgba8 — one slice of a decoded surface as 8-bit RGBA for + * display (thumbnails, previews): gray is expanded, 16-bit is rounded to + * 8 bits, float and half are clamped to 0..1 and sRGB encoded (they are + * linear), GPU blocks are decoded. out holds w × h × 4 bytes of that + * mip. Returns YAPF_ERR_UNSUPPORTED for ETC2 / EAC / ASTC blocks. + */ +int yapf_tex_to_rgba8(const yapf_texture_t *tex, uint32_t surface, uint32_t slice, + uint8_t *out); + /* yapf-sum, the checksum used by version 2 files. */ uint32_t yapf_checksum(const void *data, size_t size);