YAPF/extensions/gimp/yapf-gimp.c
Nexoniarz 2ca726e3af YAPF v1: fast byte-aligned format, SIMD, tools and plugins
Format (version 1, replaces the earlier experimental versions):
- per-tile YCoCg-R or subtract-green colour transform, 8-bit modular
- 12 row filters per 8-row band (Left, Gradient, Up-right, Up-average,
  Up 1-8 rows), none with more than a one-add serial dependency
- residuals in groups of 8 at the group's width (w bits x 8 = w bytes),
  4-bit control codes with repeat-previous-width runs chosen by DP
- constant and stored tiles, independent 64x64 tiles, mips, GPU hints
- all sizes validated before decoding; corrupt input is rejected

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

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 14:17:59 +02:00

350 lines
15 KiB
C
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*
* 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;
}
/* ── Load handler ────────────────────────────────────────────────────── */
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);
yapf_image_t *img = yapf_load_mt(path, 0); /* all cores */
g_free(path);
if (!img) {
GError *err = g_error_new(GIMP_PLUG_IN_ERROR, 0,
"Failed to load YAPF file.");
return gimp_procedure_new_return_values(procedure,
GIMP_PDB_EXECUTION_ERROR, err);
}
/* Map YAPF channel count to GIMP image / layer types.
* Use gamma-corrected babl formats (R'G'B') when the file carries the
* sRGB flag; fall back to linear (RGB) otherwise. */
gboolean is_srgb = (img->flags & YAPF_FLAG_SRGB) != 0;
GimpImageBaseType base_type = GIMP_RGB;
GimpImageType layer_type = GIMP_RGBA_IMAGE;
const gchar *babl_fmt = is_srgb ? "R'G'B'A u8" : "RGBA u8";
switch (img->channels) {
case YAPF_CHANNELS_GRAY:
base_type = GIMP_GRAY;
layer_type = GIMP_GRAY_IMAGE;
babl_fmt = is_srgb ? "Y' u8" : "Y u8";
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;
}
GimpImage *image = gimp_image_new(img->width, img->height, base_type);
GimpLayer *layer = gimp_layer_new(image, "Background",
img->width, img->height, layer_type,
100.0,
gimp_image_get_default_new_layer_mode(image));
gimp_image_insert_layer(image, layer, NULL, 0);
GeglBuffer *buffer = gimp_drawable_get_buffer(GIMP_DRAWABLE(layer));
gegl_buffer_set(buffer,
GEGL_RECTANGLE(0, 0, (gint)img->width, (gint)img->height),
0, babl_format(babl_fmt),
img->pixels, GEGL_AUTO_ROWSTRIDE);
g_object_unref(buffer);
yapf_free(img);
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)