GIMP plugin: open every YAPF 2 file and say why a file fails

Uses the texture loader: GPU blocks are decoded for viewing, 16-bit and
float files open at full precision, and cube faces, array layers, slices
and animation frames become layers. Load errors name the reason instead
of a generic failure.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DVrVthWN8GsmgHGUhhsCYm
This commit is contained in:
Claude 2026-10-11 09:30:22 +00:00
commit 83588d8e2a
No known key found for this signature in database
2 changed files with 129 additions and 51 deletions

View file

@ -2,7 +2,11 @@
Open and export `.yapf` images in **GIMP 3.0 or later**. Open and export `.yapf` images in **GIMP 3.0 or later**.
- **File → Open** reads `.yapf` (Gray, Gray + Alpha, RGB, RGBA). - **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 - **File → Export As… → `name.yapf`** writes the visible image (layers are
flattened into a copy; your document is untouched), with an optional flattened into a copy; your document is untouched), with an optional
**Store mipmaps** checkbox for game engines. **Store mipmaps** checkbox for game engines.
@ -50,5 +54,9 @@ GIMP config folder is `~/.config/GIMP/3.0` on Linux,
- Pixels are saved with the sRGB flag, because GIMP's 8-bit buffers are - Pixels are saved with the sRGB flag, because GIMP's 8-bit buffers are
gamma encoded. Files marked linear load as linear. gamma encoded. Files marked linear load as linear.
- When a file has mipmaps, GIMP opens the full-size image. - 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 - 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. pixel for pixel, and export equals GIMP's own flattened PNG of the image.
The version 2 loader was tested in GIMP 3.0.6 (nixpkgs 25.05, headless):
BC7, BC5, BC6H, half float, 16-bit, cube, animation and a damaged file.

View file

@ -138,8 +138,44 @@ static GimpProcedure *yapf_plugin_create_procedure(GimpPlugIn *plug_in,
return procedure; return procedure;
} }
static GimpValueArray *yapf_error(GimpProcedure *procedure, const gchar *msg);
/* ── Load handler ────────────────────────────────────────────────────── */ /* ── 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( static GimpValueArray *yapf_load_run(
GimpProcedure *procedure, GimpProcedure *procedure,
GimpRunMode run_mode, GimpRunMode run_mode,
@ -152,59 +188,93 @@ static GimpValueArray *yapf_load_run(
(void)run_mode; (void)metadata; (void)flags; (void)config; (void)run_data; (void)run_mode; (void)metadata; (void)flags; (void)config; (void)run_data;
gchar *path = g_file_get_path(file); gchar *path = g_file_get_path(file);
yapf_image_t *img = yapf_load_mt(path, 0); /* all cores */ 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); g_free(path);
if (!img) { /* What the pixels look like after decoding: GPU blocks decode to
GError *err = g_error_new(GIMP_PLUG_IN_ERROR, 0, * RGBA 8-bit, BC6H to RGB half float. */
"Failed to load YAPF file."); gboolean blocks = t->payload == YAPF_PAYLOAD_BLOCKS;
return gimp_procedure_new_return_values(procedure, gboolean bc6 = blocks && (t->format == YAPF_GPU_BC6H || t->format == YAPF_GPU_BC6H_SF);
GIMP_PDB_EXECUTION_ERROR, err); 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);
/* Map YAPF channel count to GIMP image / layer types. static const gchar *chn[2][4] = { { "Y", "YA", "RGB", "RGBA" }, { "Y'", "Y'A", "R'G'B'", "R'G'B'A" } };
* Use gamma-corrected babl formats (R'G'B') when the file carries the static const gchar *typ[4] = { "u8", "u16", "half", "float" };
* sRGB flag; fall back to linear (RGB) otherwise. */ gchar *fmt_name = g_strdup_printf("%s %s", chn[gamma ? 1 : 0][ch - 1], typ[sample]);
gboolean is_srgb = (img->flags & YAPF_FLAG_SRGB) != 0; const Babl *fmt = babl_format(fmt_name);
GimpImageBaseType base_type = GIMP_RGB; g_free(fmt_name);
GimpImageType layer_type = GIMP_RGBA_IMAGE;
const gchar *babl_fmt = is_srgb ? "R'G'B'A u8" : "RGBA u8";
switch (img->channels) { GimpPrecision prec;
case YAPF_CHANNELS_GRAY: switch (sample) {
base_type = GIMP_GRAY; case YAPF_SAMPLE_U16: prec = gamma ? GIMP_PRECISION_U16_NON_LINEAR : GIMP_PRECISION_U16_LINEAR; break;
layer_type = GIMP_GRAY_IMAGE; case YAPF_SAMPLE_F16: prec = GIMP_PRECISION_HALF_LINEAR; break;
babl_fmt = is_srgb ? "Y' u8" : "Y u8"; case YAPF_SAMPLE_F32: prec = GIMP_PRECISION_FLOAT_LINEAR; break;
break; default: prec = gamma ? GIMP_PRECISION_U8_NON_LINEAR : GIMP_PRECISION_U8_LINEAR; break;
case YAPF_CHANNELS_GRAY_ALPHA:
base_type = GIMP_GRAY;
layer_type = GIMP_GRAYA_IMAGE;
babl_fmt = is_srgb ? "Y'A u8" : "YA u8";
break;
case YAPF_CHANNELS_RGB:
base_type = GIMP_RGB;
layer_type = GIMP_RGB_IMAGE;
babl_fmt = is_srgb ? "R'G'B' u8" : "RGB u8";
break;
default: /* RGBA */
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;
GimpImage *image = gimp_image_new(img->width, img->height, base_type); /* Mip 0 of every frame, layer, face and slice becomes one GIMP layer
GimpLayer *layer = gimp_layer_new(image, "Background", * (the first at the bottom, as GIMP animations expect). */
img->width, img->height, layer_type, uint32_t w, h, d, faces = yapf_tex_faces(t);
100.0, yapf_tex_mip_size(t, 0, &w, &h, &d);
gimp_image_get_default_new_layer_mode(image)); 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); 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)); GeglBuffer *buffer = gimp_drawable_get_buffer(GIMP_DRAWABLE(layer));
gegl_buffer_set(buffer, gegl_buffer_set(buffer, GEGL_RECTANGLE(0, 0, (gint)w, (gint)h), 0, fmt,
GEGL_RECTANGLE(0, 0, (gint)img->width, (gint)img->height), src, GEGL_AUTO_ROWSTRIDE);
0, babl_format(babl_fmt),
img->pixels, GEGL_AUTO_ROWSTRIDE);
g_object_unref(buffer); g_object_unref(buffer);
}
yapf_free(img); g_free(tmp);
yapf_tex_free(t);
GimpValueArray *ret = gimp_procedure_new_return_values(procedure, GimpValueArray *ret = gimp_procedure_new_return_values(procedure,
GIMP_PDB_SUCCESS, NULL); GIMP_PDB_SUCCESS, NULL);