From c68a18900539604e28b3304af32db8bf046e20f8 Mon Sep 17 00:00:00 2001 From: Matthias Clasen Date: Thu, 1 Aug 2024 06:35:25 -0400 Subject: [PATCH] Add an avif loder Add a loader for avif images. It uses libavif and produces dmabufs when /dev/udmabuf is available. Otherwise it produces YUVA memory textures. For saving, currently only YUV444 is supported. It would be nice to just save dmabufs as-is, but that requires more work. This is meant to facilitate testing of our yuv support, and therefore, it is not exposed via the public texture loading and saving apis. It is disabled by default and should *not* be enabled in distro builds. avif: Make it build outside of libgtk We want to use this code in the image tool, and we don't want to link it statically. avif loader fixes --- docs/reference/gtk/building.md | 7 + gdk/gdktexture.c | 2 + gdk/loaders/gdkavif.c | 865 +++++++++++++++++++++++++++++++++ gdk/loaders/gdkavifprivate.h | 28 ++ gdk/meson.build | 7 + meson.build | 4 + meson_options.txt | 5 + 7 files changed, 918 insertions(+) create mode 100644 gdk/loaders/gdkavif.c create mode 100644 gdk/loaders/gdkavifprivate.h diff --git a/docs/reference/gtk/building.md b/docs/reference/gtk/building.md index 0ec954853e..6de5d495ba 100644 --- a/docs/reference/gtk/building.md +++ b/docs/reference/gtk/building.md @@ -265,6 +265,13 @@ support in the file chooser. This option controls whether GTK should use colord for color calibration support in the cups print backend. +## `avif` + +This option controls whether GTK will use libavif for loading +textures from AVIF image files. Note that the purpose of this option +is to facilitate development of GTK. The support for this image format +is not guaranteed and may go away at any time. + ### `documentation`, `man-pages` and `screenshots` The *gi-docgen* package is used to generate the reference documentation diff --git a/gdk/gdktexture.c b/gdk/gdktexture.c index e4e7baa001..738e9cdb8c 100644 --- a/gdk/gdktexture.c +++ b/gdk/gdktexture.c @@ -983,6 +983,8 @@ gdk_texture_download_surface (GdkTexture *texture, * %CAIRO_FORMAT_ARGB32, so every downloaded pixel requires * 4 bytes of memory. * + * The downloaded data is converted to the sRGB color state. + * * Downloading a texture into a Cairo image surface: * ```c * surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32, diff --git a/gdk/loaders/gdkavif.c b/gdk/loaders/gdkavif.c new file mode 100644 index 0000000000..b3d66dc8c7 --- /dev/null +++ b/gdk/loaders/gdkavif.c @@ -0,0 +1,865 @@ +#include "config.h" + +#include "gdkavifprivate.h" +#include "gdkmemorytexturebuilder.h" +#include "gdktexture.h" +#include "gdkdisplay.h" +#include "gdkcicpparamsprivate.h" +#include "gdkcolorstateprivate.h" +#include "gdkmemoryformatprivate.h" +#include "gdkdmabuftexture.h" +#include "gdktexturedownloader.h" + +#include + +#ifdef HAVE_DMABUF + +#include "gdkdmabuftextureprivate.h" +#include "gdkdmabuftexturebuilder.h" +#include "gdkdmabuffourccprivate.h" + +/* NOTE: We build this code outside of libgtk, in tests and the image tool, so this + * code has to be a bit careful to avoid depending on private api, which can also + * be dragged in indirectly, via inlines. + */ + +#ifdef AVIF_DEBUG +#define DEBUG(fmt,...) g_log ("avif", G_LOG_LEVEL_DEBUG, fmt __VA_OPT__(,) __VA_ARGS__) +#else +#define DEBUG(fmt,...) +#endif + +/* {{{ udmabuf support */ + +#include +#include +#include +#include +#include +#include +#include +#include + +static int udmabuf_fd; + +static gboolean +udmabuf_initialize (GError **error) +{ + if (udmabuf_fd == 0) + { + udmabuf_fd = open ("/dev/udmabuf", O_RDWR); + if (udmabuf_fd == -1) + { + g_set_error (error, + G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to open /dev/udmabuf: %s", + g_strerror (errno)); + } + } + + return udmabuf_fd != -1; +} + +typedef struct +{ + int mem_fd; + int dmabuf_fd; + size_t size; + gpointer data; +} UDmabuf; + +static void +udmabuf_free (gpointer data) +{ + UDmabuf *udmabuf = data; + + munmap (udmabuf->data, udmabuf->size); + close (udmabuf->mem_fd); + close (udmabuf->dmabuf_fd); + + g_free (udmabuf); +} + +#define align(x,y) (((x) + (y) - 1) & ~((y) - 1)) + +static UDmabuf * +udmabuf_allocate (size_t size, + GError **error) +{ + int mem_fd = -1; + int dmabuf_fd = -1; + uint64_t alignment; + int res; + gpointer data; + UDmabuf *udmabuf; + + if (udmabuf_fd == -1) + { + g_set_error (error, + G_IO_ERROR, G_IO_ERROR_FAILED, + "udmabuf not available"); + goto fail; + } + + alignment = sysconf (_SC_PAGE_SIZE); + + size = align (size, alignment); + + mem_fd = memfd_create ("gtk", MFD_ALLOW_SEALING); + if (mem_fd == -1) + { + g_set_error (error, + G_IO_ERROR, G_IO_ERROR_FAILED, + "Failed to open /dev/udmabuf: %s", + g_strerror (errno)); + goto fail; + } + + res = ftruncate (mem_fd, size); + if (res == -1) + { + g_set_error (error, + G_IO_ERROR, G_IO_ERROR_FAILED, + "ftruncate failed: %s", + g_strerror (errno)); + goto fail; + } + + if (fcntl (mem_fd, F_ADD_SEALS, F_SEAL_SHRINK) < 0) + { + g_set_error (error, + G_IO_ERROR, G_IO_ERROR_FAILED, + "ftruncate failed: %s", + g_strerror (errno)); + goto fail; + } + + dmabuf_fd = ioctl (udmabuf_fd, + UDMABUF_CREATE, + &(struct udmabuf_create) { + .memfd = mem_fd, + .flags = UDMABUF_FLAGS_CLOEXEC, + .offset = 0, + .size = size + }); + + if (dmabuf_fd < 0) + { + g_set_error (error, + G_IO_ERROR, G_IO_ERROR_FAILED, + "UDMABUF_CREATE ioctl failed: %s", + g_strerror (errno)); + goto fail; + } + + data = mmap (NULL, size, PROT_WRITE | PROT_READ, MAP_SHARED, mem_fd, 0); + + if (!data) + { + g_set_error (error, + G_IO_ERROR, G_IO_ERROR_FAILED, + "mmap failed: %s", + g_strerror (errno)); + goto fail; + } + + udmabuf = g_new0 (UDmabuf, 1); + + udmabuf->mem_fd = mem_fd; + udmabuf->dmabuf_fd = dmabuf_fd; + udmabuf->size = size; + udmabuf->data = data; + + return udmabuf; + +fail: + if (mem_fd != -1) + close (mem_fd); + if (dmabuf_fd != -1) + close (dmabuf_fd); + + return NULL; +} + +/* }}} */ +/* {{{ dmabuf texture support */ + +static GdkTexture * +gdk_avif_create_dmabuf_texture (avifDecoder *decoder, + GdkColorState *color_state, + GError **error) +{ + guint fourcc = 0; + guint32 width, height, depth; + GdkDmabufTextureBuilder *builder; + UDmabuf *udmabuf; + gboolean combine_uv = FALSE; + guchar *data; + gsize size0, size1, size2; + GdkTexture *texture; + + if (!udmabuf_initialize (error)) + return NULL; + + width = decoder->image->width; + height = decoder->image->height; + depth = decoder->image->depth; + + if (decoder->image->alphaPlane) + { + g_set_error (error, + GDK_TEXTURE_ERROR, GDK_TEXTURE_ERROR_UNSUPPORTED_CONTENT, + "no yuv dmabuf with alpha"); + return NULL; + } + + switch (decoder->image->yuvFormat) + { + case AVIF_PIXEL_FORMAT_YUV444: + DEBUG ("load: format yuv444"); + if (depth == 8) + fourcc = DRM_FORMAT_YUV444; + break; + case AVIF_PIXEL_FORMAT_YUV422: + DEBUG ("load: format yuv422"); + if (depth == 8) + fourcc = DRM_FORMAT_YUV422; + break; + case AVIF_PIXEL_FORMAT_YUV420: + DEBUG ("load: format yuv420"); + combine_uv = TRUE; + if (depth == 8) + fourcc = DRM_FORMAT_NV12; + else if (depth == 10) + fourcc = DRM_FORMAT_P010; + else if (depth == 12) + fourcc = DRM_FORMAT_P012; + else if (depth == 16) + fourcc = DRM_FORMAT_P016; + break; + case AVIF_PIXEL_FORMAT_YUV400: + case AVIF_PIXEL_FORMAT_NONE: + case AVIF_PIXEL_FORMAT_COUNT: + default: + break; + } + + if (fourcc == 0) + { + const char *names[] = { "none", "yuv444", "yuv422", "yuv420", "yuv400" }; + + g_set_error (error, + GDK_TEXTURE_ERROR, GDK_TEXTURE_ERROR_UNSUPPORTED_CONTENT, + "unsupported pixel format %s, depth %u", + names[decoder->image->yuvFormat], decoder->image->depth); + return NULL; + } + + DEBUG ("load: use fourcc %.4s", (char *)&fourcc); + + builder = gdk_dmabuf_texture_builder_new (); + + gdk_dmabuf_texture_builder_set_display (builder, gdk_display_get_default ()); + gdk_dmabuf_texture_builder_set_width (builder, width); + gdk_dmabuf_texture_builder_set_height (builder, height); + gdk_dmabuf_texture_builder_set_color_state (builder, color_state); + gdk_dmabuf_texture_builder_set_fourcc (builder, fourcc); + gdk_dmabuf_texture_builder_set_modifier (builder, DRM_FORMAT_MOD_LINEAR); + gdk_dmabuf_texture_builder_set_premultiplied (builder, FALSE); + + if (combine_uv) + { + size0 = height * avifImagePlaneRowBytes (decoder->image, AVIF_CHAN_Y); + size1 = height / 2 * avifImagePlaneRowBytes (decoder->image, AVIF_CHAN_Y); + + udmabuf = udmabuf_allocate (size0 + size1, NULL); + + data = (guchar *) udmabuf->data; + + if (depth == 8) + { + memcpy (data, avifImagePlane (decoder->image, AVIF_CHAN_Y), size0); + for (int i = 0; i < height / 2; i++) + { + guchar *usrc = avifImagePlane (decoder->image, AVIF_CHAN_U) + i * avifImagePlaneRowBytes (decoder->image, AVIF_CHAN_U); + guchar *vsrc = avifImagePlane (decoder->image, AVIF_CHAN_V) + i * avifImagePlaneRowBytes (decoder->image, AVIF_CHAN_V); + guchar *dest = data + size0 + i * avifImagePlaneRowBytes (decoder->image, AVIF_CHAN_Y); + for (int j = 0; j < width / 2; j++) + { + dest[2*j] = usrc[j]; + dest[2*j+1] = vsrc[j]; + } + } + } + else + { + for (int i = 0; i < height; i++) + { + guint16 *src = (guint16 *) (avifImagePlane (decoder->image, AVIF_CHAN_Y) + i * avifImagePlaneRowBytes (decoder->image, AVIF_CHAN_Y)); + guint16 *dest = (guint16 *) (data + i * avifImagePlaneRowBytes (decoder->image, AVIF_CHAN_Y)); + for (int j = 0; j < width; j++) + dest[j] = src[j] << (16 - depth); + } + for (int i = 0; i < height / 2; i++) + { + guint16 *usrc = (guint16 *)(avifImagePlane (decoder->image, AVIF_CHAN_U) + i * avifImagePlaneRowBytes (decoder->image, AVIF_CHAN_U)); + guint16 *vsrc = (guint16 *)(avifImagePlane (decoder->image, AVIF_CHAN_V) + i * avifImagePlaneRowBytes (decoder->image, AVIF_CHAN_V)); + guint16 *dest = (guint16 *)(data + size0 + i * avifImagePlaneRowBytes (decoder->image, AVIF_CHAN_Y)); + for (int j = 0; j < width / 2; j++) + { + dest[2*j] = usrc[j] << (16 - depth); + dest[2*j+1] = vsrc[j] << (16 - depth); + } + } + } + + gdk_dmabuf_texture_builder_set_n_planes (builder, 2); + + gdk_dmabuf_texture_builder_set_fd (builder, 0, udmabuf->dmabuf_fd); + gdk_dmabuf_texture_builder_set_offset (builder, 0, 0); + gdk_dmabuf_texture_builder_set_stride (builder, 0, avifImagePlaneRowBytes (decoder->image, AVIF_CHAN_Y)); + + gdk_dmabuf_texture_builder_set_fd (builder, 1, udmabuf->dmabuf_fd); + gdk_dmabuf_texture_builder_set_offset (builder, 1, size0); + gdk_dmabuf_texture_builder_set_stride (builder, 1, avifImagePlaneRowBytes (decoder->image, AVIF_CHAN_Y)); + } + else + { + size0 = height * avifImagePlaneRowBytes (decoder->image, AVIF_CHAN_Y); + size1 = height * avifImagePlaneRowBytes (decoder->image, AVIF_CHAN_U); + size2 = height * avifImagePlaneRowBytes (decoder->image, AVIF_CHAN_V); + + udmabuf = udmabuf_allocate (size0 + size1 + size2, NULL); + + data = (guchar *) udmabuf->data; + memcpy (data, avifImagePlane (decoder->image, AVIF_CHAN_Y), size0); + memcpy (data + size0, avifImagePlane (decoder->image, AVIF_CHAN_U), size1); + memcpy (data + size0 + size1, avifImagePlane (decoder->image, AVIF_CHAN_V), size2); + + gdk_dmabuf_texture_builder_set_n_planes (builder, 3); + + gdk_dmabuf_texture_builder_set_fd (builder, 0, udmabuf->dmabuf_fd); + gdk_dmabuf_texture_builder_set_offset (builder, 0, 0); + gdk_dmabuf_texture_builder_set_stride (builder, 0, avifImagePlaneRowBytes (decoder->image, AVIF_CHAN_Y)); + + gdk_dmabuf_texture_builder_set_fd (builder, 1, udmabuf->dmabuf_fd); + gdk_dmabuf_texture_builder_set_offset (builder, 1, size0); + gdk_dmabuf_texture_builder_set_stride (builder, 1, avifImagePlaneRowBytes (decoder->image, AVIF_CHAN_U)); + + gdk_dmabuf_texture_builder_set_fd (builder, 2, udmabuf->dmabuf_fd); + gdk_dmabuf_texture_builder_set_offset (builder, 2, size0 + size1); + gdk_dmabuf_texture_builder_set_stride (builder, 2, avifImagePlaneRowBytes (decoder->image, AVIF_CHAN_V)); + } + + texture = gdk_dmabuf_texture_builder_build (builder, udmabuf_free, udmabuf, error); + + g_object_unref (builder); + + return texture; +} +#endif /* HAVE_DMABUF */ + +/* }}} */ +/* {{{ memory texture support */ + +static GdkTexture * +gdk_avif_create_memory_texture (avifDecoder *decoder, + GdkColorState *color_state, + GError **error) +{ + guint32 width, height, depth, stride, bpp; + GdkTexture *texture; + GdkMemoryTextureBuilder *builder; + guchar *data; + GBytes *bytes; + GdkMemoryFormat format = GDK_MEMORY_N_FORMATS; + int X_SUB, Y_SUB; + + width = decoder->image->width; + height = decoder->image->height; + depth = decoder->image->depth; + + switch (decoder->image->yuvFormat) + { + case AVIF_PIXEL_FORMAT_YUV444: + X_SUB = 1; Y_SUB = 1; + if (depth == 8) + { + format = GDK_MEMORY_R8G8B8A8; + bpp = 4; + } + else + { + format = GDK_MEMORY_R16G16B16A16; + bpp = 8; + } + break; + + case AVIF_PIXEL_FORMAT_YUV422: + X_SUB = 2; Y_SUB = 1; + if (depth == 8) + { + format = GDK_MEMORY_R8G8B8A8; + bpp = 8; + } + else + { + format = GDK_MEMORY_R16G16B16A16; + bpp = 8; + } + break; + + case AVIF_PIXEL_FORMAT_YUV420: + X_SUB = 2; Y_SUB = 2; + break; + + case AVIF_PIXEL_FORMAT_YUV400: + case AVIF_PIXEL_FORMAT_NONE: + case AVIF_PIXEL_FORMAT_COUNT: + if (depth == 8) + { + format = GDK_MEMORY_R8G8B8A8; + bpp = 4; + } + else + { + format = GDK_MEMORY_R16G16B16A16; + bpp = 8; + } + default: + break; + } + + if (format == GDK_MEMORY_N_FORMATS) + { + const char *names[] = { "none", "yuv444", "yuv422", "yuv420", "yuv400" }; + + g_set_error (error, + GDK_TEXTURE_ERROR, GDK_TEXTURE_ERROR_UNSUPPORTED_CONTENT, + "unsupported pixel format for YUVA %s, depth %u", + names[decoder->image->yuvFormat], depth); + return NULL; + } + + stride = width * bpp; + data = g_malloc (stride * height); + + if (depth == 8) + { + guchar *y_data = avifImagePlane (decoder->image, AVIF_CHAN_Y); + guchar *u_data = avifImagePlane (decoder->image, AVIF_CHAN_U); + guchar *v_data = avifImagePlane (decoder->image, AVIF_CHAN_V); + guchar *a_data = avifImagePlane (decoder->image, AVIF_CHAN_A); + guchar *dst_data = data; + gsize y_stride = avifImagePlaneRowBytes (decoder->image, AVIF_CHAN_Y); + gsize u_stride = avifImagePlaneRowBytes (decoder->image, AVIF_CHAN_U); + gsize v_stride = avifImagePlaneRowBytes (decoder->image, AVIF_CHAN_V); + gsize a_stride = avifImagePlaneRowBytes (decoder->image, AVIF_CHAN_A); + gsize dst_stride = stride; + + for (int y = 0; y < height; y += Y_SUB) + { + for (int x = 0; x < width; x += X_SUB) + { + guint y_, u_, v_, a_; + + u_ = u_data[x / X_SUB]; + v_ = v_data[x / X_SUB]; + + for (int ys = 0; ys < Y_SUB && y + ys < height; ys++) + for (int xs = 0; xs < X_SUB && x + xs < width; xs++) + { + guchar *dest = &dst_data[4 * (x + xs) + dst_stride * ys]; + + y_ = y_data[x + xs + y_stride * ys]; + a_ = a_data ? a_data[x + xs + a_stride * ys] : 0xff; + + dest[0] = y_; + dest[1] = u_; + dest[2] = v_; + dest[3] = a_; + } + } + + dst_data += Y_SUB * dst_stride; + y_data += Y_SUB * y_stride; + u_data += u_stride; + v_data += v_stride; + if (a_data) + a_data += a_stride; + } + } + else + { + guint16 *y_data = (guint16 *) avifImagePlane (decoder->image, AVIF_CHAN_Y); + guint16 *u_data = (guint16 *) avifImagePlane (decoder->image, AVIF_CHAN_U); + guint16 *v_data = (guint16 *) avifImagePlane (decoder->image, AVIF_CHAN_V); + guint16 *a_data = (guint16 *) avifImagePlane (decoder->image, AVIF_CHAN_A); + guint16 *dst_data = (guint16 *) data; + gsize y_stride = avifImagePlaneRowBytes (decoder->image, AVIF_CHAN_Y) / 2; + gsize u_stride = avifImagePlaneRowBytes (decoder->image, AVIF_CHAN_U) / 2; + gsize v_stride = avifImagePlaneRowBytes (decoder->image, AVIF_CHAN_V) / 2; + gsize a_stride = avifImagePlaneRowBytes (decoder->image, AVIF_CHAN_A) / 2; + gsize dst_stride = stride / 2; + + for (int y = 0; y < height; y += Y_SUB) + { + for (int x = 0; x < width; x += X_SUB) + { + guint y_, u_, v_, a_; + + u_ = u_data[x / X_SUB] << (16 - depth); + v_ = v_data[x / X_SUB] << (16 - depth); + + for (int ys = 0; ys < Y_SUB && y + ys < height; ys++) + for (int xs = 0; xs < X_SUB && x + xs < width; xs++) + { + guint16 *dest = &dst_data[4 * (x + xs) + dst_stride * ys]; + + y_ = y_data[x + xs + y_stride * ys] << (16 - depth); + a_ = (a_data ? a_data[x + xs + a_stride * ys] : 0xffff) << (16 - depth); + + dest[0] = y_ | (y_ >> depth); + dest[1] = u_ | (u_ >> depth); + dest[2] = v_ | (v_ >> depth); + dest[3] = a_ | (a_ >> depth); + } + } + + dst_data += Y_SUB * dst_stride; + y_data += Y_SUB * y_stride; + u_data += u_stride; + v_data += v_stride; + } + } + + builder = gdk_memory_texture_builder_new (); + + bytes = g_bytes_new_take (data, stride * height); + + gdk_memory_texture_builder_set_width (builder, width); + gdk_memory_texture_builder_set_height (builder, height); + gdk_memory_texture_builder_set_bytes (builder, bytes); + gdk_memory_texture_builder_set_stride (builder, stride); + gdk_memory_texture_builder_set_format (builder, format); + gdk_memory_texture_builder_set_color_state (builder, color_state); + + texture = gdk_memory_texture_builder_build (builder); + + g_bytes_unref (bytes); + g_object_unref (builder); + + return texture; +} + +/* }}} */ +/* {{{ Public API */ + +GdkTexture * +gdk_load_avif (GBytes *bytes, + GError **error) +{ + avifDecoder *decoder; + avifResult result; + GdkCicpParams *params; + GdkColorState *color_state = NULL; + GdkTexture *texture = NULL; + GError *local_error = NULL; + + decoder = avifDecoderCreate (); + result = avifDecoderSetIOMemory (decoder, + g_bytes_get_data (bytes, NULL), + g_bytes_get_size (bytes)); + if (result != AVIF_RESULT_OK) + { + g_set_error (error, + GDK_TEXTURE_ERROR, GDK_TEXTURE_ERROR_UNSUPPORTED_CONTENT, + "avifDecoderSetIOFile failed: %u", result); + goto fail; + } + + result = avifDecoderParse (decoder); + if (result != AVIF_RESULT_OK) + { + g_set_error (error, + G_IO_ERROR, + G_IO_ERROR_FAILED, + "avifDecoderParse failed: %u", result); + goto fail; + } + + result = avifDecoderNextImage (decoder); + if (result != AVIF_RESULT_OK) + { + g_set_error (error, + GDK_TEXTURE_ERROR, GDK_TEXTURE_ERROR_UNSUPPORTED_CONTENT, + "avifDecoderNextImage failed: %u", result); + goto fail; + } + + DEBUG ("load: depth %u", decoder->image->depth); + + DEBUG ("load: cicp %u/%u/%u/%u", + decoder->image->colorPrimaries, + decoder->image->transferCharacteristics, + decoder->image->matrixCoefficients, + decoder->image->yuvRange); + + params = gdk_cicp_params_new (); + gdk_cicp_params_set_color_primaries (params, decoder->image->colorPrimaries); + gdk_cicp_params_set_transfer_function (params, decoder->image->transferCharacteristics); + gdk_cicp_params_set_matrix_coefficients (params, decoder->image->matrixCoefficients); + gdk_cicp_params_set_range (params, decoder->image->yuvRange == AVIF_RANGE_LIMITED + ? GDK_CICP_RANGE_NARROW + : GDK_CICP_RANGE_FULL); + + color_state = gdk_cicp_params_build_color_state (params, error); + g_object_unref (params); + + if (!color_state) + goto fail; + +#ifdef HAVE_DMABUF + texture = gdk_avif_create_dmabuf_texture (decoder, color_state, &local_error); + + if (!texture) + { + DEBUG ("load: creating dmabuf failed: %s", local_error->message); + g_clear_error (&local_error); + } +#endif + + if (!texture) + texture = gdk_avif_create_memory_texture (decoder, color_state, error); + + (gdk_color_state_unref) (color_state); + +fail: + avifDecoderDestroy (decoder); + + return texture; +} + +static int +bytes_per_channel (GdkMemoryFormat format) +{ + switch (format) + { + case GDK_MEMORY_B8G8R8A8_PREMULTIPLIED: + case GDK_MEMORY_A8R8G8B8_PREMULTIPLIED: + case GDK_MEMORY_R8G8B8A8_PREMULTIPLIED: + case GDK_MEMORY_B8G8R8A8: + case GDK_MEMORY_A8R8G8B8: + case GDK_MEMORY_R8G8B8A8: + case GDK_MEMORY_A8B8G8R8: + case GDK_MEMORY_R8G8B8: + case GDK_MEMORY_B8G8R8: + case GDK_MEMORY_G8A8_PREMULTIPLIED: + case GDK_MEMORY_G8A8: + case GDK_MEMORY_G8: + case GDK_MEMORY_A8: + case GDK_MEMORY_A8B8G8R8_PREMULTIPLIED: + case GDK_MEMORY_B8G8R8X8: + case GDK_MEMORY_X8R8G8B8: + case GDK_MEMORY_R8G8B8X8: + case GDK_MEMORY_X8B8G8R8: + return 1; + case GDK_MEMORY_R16G16B16: + case GDK_MEMORY_R16G16B16A16_PREMULTIPLIED: + case GDK_MEMORY_R16G16B16A16: + case GDK_MEMORY_R16G16B16_FLOAT: + case GDK_MEMORY_R16G16B16A16_FLOAT_PREMULTIPLIED: + case GDK_MEMORY_R16G16B16A16_FLOAT: + case GDK_MEMORY_G16A16_PREMULTIPLIED: + case GDK_MEMORY_G16A16: + case GDK_MEMORY_G16: + case GDK_MEMORY_A16: + case GDK_MEMORY_A16_FLOAT: + return 2; + case GDK_MEMORY_R32G32B32_FLOAT: + case GDK_MEMORY_R32G32B32A32_FLOAT_PREMULTIPLIED: + case GDK_MEMORY_R32G32B32A32_FLOAT: + case GDK_MEMORY_A32_FLOAT: + return 4; + case GDK_MEMORY_N_FORMATS: + default: + g_assert_not_reached (); + } +} + +GBytes * +gdk_save_avif (GdkTexture *texture) +{ + uint32_t width, height, depth; + GdkColorState *color_state; + const GdkCicp *cicp; + avifEncoder *encoder; + avifImage *image = NULL; + avifRWData output = AVIF_DATA_EMPTY; + GdkTextureDownloader *downloader; + GBytes *bytes = NULL; + gsize stride; + GBytes *out_bytes = NULL; + + width = gdk_texture_get_width (texture); + height = gdk_texture_get_height (texture); + + if (bytes_per_channel (gdk_texture_get_format (texture)) == 1) + depth = 8; + else + depth = 12; + + color_state = gdk_texture_get_color_state (texture); + cicp = gdk_color_state_get_cicp (color_state); + + DEBUG ("save: depth %u", depth); + + DEBUG ("save: cicp %u/%u/%u/%u", + cicp->color_primaries, + cicp->transfer_function, + cicp->matrix_coefficients, + cicp->range); + + downloader = gdk_texture_downloader_new (texture); + if (depth == 8) + gdk_texture_downloader_set_format (downloader, GDK_MEMORY_R8G8B8A8); + else + gdk_texture_downloader_set_format (downloader, GDK_MEMORY_R16G16B16A16); + gdk_texture_downloader_set_color_state (downloader, gdk_texture_get_color_state (texture)); + + bytes = gdk_texture_downloader_download_bytes (downloader, &stride); + gdk_texture_downloader_free (downloader); + + if (cicp->matrix_coefficients != 0) + { + /* some form of yuv */ + image = avifImageCreate (width, height, depth, AVIF_PIXEL_FORMAT_YUV444); + image->colorPrimaries = cicp->color_primaries; + image->transferCharacteristics = cicp->transfer_function; + image->matrixCoefficients = cicp->matrix_coefficients; + image->yuvRange = cicp->range == GDK_CICP_RANGE_NARROW + ? AVIF_RANGE_LIMITED + : AVIF_RANGE_FULL; + + avifImageAllocatePlanes (image, AVIF_PLANES_YUV); + + if (depth == 8) + { + for (gsize y = 0; y < height; y++) + { + const guchar *orig = (guchar *) g_bytes_get_data (bytes, NULL) + y * stride; + guchar *y_ = avifImagePlane (image, AVIF_CHAN_Y) + y * avifImagePlaneRowBytes (image, AVIF_CHAN_Y); + guchar *u_ = avifImagePlane (image, AVIF_CHAN_U) + y * avifImagePlaneRowBytes (image, AVIF_CHAN_U); + guchar *v_ = avifImagePlane (image, AVIF_CHAN_V) + y * avifImagePlaneRowBytes (image, AVIF_CHAN_V); + + for (gsize x = 0; x < width; x++) + { + y_[x] = orig[4*x + 0]; + u_[x] = orig[4*x + 1]; + v_[x] = orig[4*x + 2]; + } + } + } + else + { + for (gsize y = 0; y < height; y++) + { + const guint16 *orig = (guint16 *) ((guchar *) g_bytes_get_data (bytes, NULL) + y * stride); + guint16 *y_ = (guint16 *) (avifImagePlane (image, AVIF_CHAN_Y) + y * avifImagePlaneRowBytes (image, AVIF_CHAN_Y)); + guint16 *u_ = (guint16 *) (avifImagePlane (image, AVIF_CHAN_U) + y * avifImagePlaneRowBytes (image, AVIF_CHAN_U)); + guint16 *v_ = (guint16 *) (avifImagePlane (image, AVIF_CHAN_V) + y * avifImagePlaneRowBytes (image, AVIF_CHAN_V)); + + for (gsize x = 0; x < width; x++) + { + y_[x] = orig[4*x + 0] >> (16 - depth); + u_[x] = orig[4*x + 1] >> (16 - depth); + v_[x] = orig[4*x + 2] >> (16 - depth); + } + } + } + } + else + { + avifRGBImage rgb; + + image = avifImageCreate (width, height, depth, AVIF_PIXEL_FORMAT_YUV444); + image->colorPrimaries = cicp->color_primaries; + image->transferCharacteristics = cicp->transfer_function; + image->matrixCoefficients = cicp->matrix_coefficients; + image->yuvRange = cicp->range == GDK_CICP_RANGE_NARROW + ? AVIF_RANGE_LIMITED + : AVIF_RANGE_FULL; + + avifRGBImageSetDefaults (&rgb, image); + avifRGBImageAllocatePixels (&rgb); + + if (depth == 8) + { + for (gsize y = 0; y < height; y++) + { + const guchar *orig = (guchar *) g_bytes_get_data (bytes, NULL) + y * stride; + guchar *pixels = rgb.pixels + y * rgb.rowBytes; + for (gsize x = 0; x < width; x++) + { + pixels[4*x + 0] = orig[4*x + 0]; + pixels[4*x + 1] = orig[4*x + 1]; + pixels[4*x + 2] = orig[4*x + 2]; + pixels[4*x + 3] = orig[4*x + 3]; + } + } + } + else + { + for (gsize y = 0; y < height; y++) + { + const guint16 *orig = (const guint16 *) ((guchar *) g_bytes_get_data (bytes, NULL) + y * stride); + guint16 *pixels = (guint16 *) (rgb.pixels + y * rgb.rowBytes); + for (gsize x = 0; x < width; x++) + { + pixels[4*x + 0] = orig[4*x + 0] >> (16 - depth); + pixels[4*x + 1] = orig[4*x + 1] >> (16 - depth); + pixels[4*x + 2] = orig[4*x + 2] >> (16 - depth); + pixels[4*x + 3] = orig[4*x + 3] >> (16 - depth); + } + } + } + + avifImageRGBToYUV(image, &rgb); + avifRGBImageFreePixels (&rgb); + } + + DEBUG ("save: cicp in image %u/%u/%u/%u", + image->colorPrimaries, + image->transferCharacteristics, + image->matrixCoefficients, + image->yuvRange); + + encoder = avifEncoderCreate (); + + if (avifEncoderWrite (encoder, image, &output) != AVIF_RESULT_OK) + goto fail; + + out_bytes = g_bytes_new_take (output.data, output.size); + +fail: + g_clear_pointer (&bytes, g_bytes_unref); + avifEncoderDestroy (encoder); + if (image) + avifImageDestroy (image); + + return out_bytes; +} + +gboolean +gdk_is_avif (GBytes *bytes) +{ + avifROData input; + + input.data = g_bytes_get_data (bytes, &input.size); + + return avifPeekCompatibleFileType (&input); +} + +/* }}} */ + +/* vim:set foldmethod=marker expandtab: */ diff --git a/gdk/loaders/gdkavifprivate.h b/gdk/loaders/gdkavifprivate.h new file mode 100644 index 0000000000..e449847108 --- /dev/null +++ b/gdk/loaders/gdkavifprivate.h @@ -0,0 +1,28 @@ +/* GDK - The GIMP Drawing Kit + * Copyright (C) 2024 Red Hat, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. If not, see . + */ + +#pragma once + +#include +#include + +GdkTexture *gdk_load_avif (GBytes *bytes, + GError **error); + +GBytes *gdk_save_avif (GdkTexture *texture); + +gboolean gdk_is_avif (GBytes *bytes); diff --git a/gdk/meson.build b/gdk/meson.build index 78609a9870..e7c7d393fd 100644 --- a/gdk/meson.build +++ b/gdk/meson.build @@ -74,6 +74,12 @@ gdk_public_sources = files([ 'loaders/gdkjpeg.c', ]) +if avif_dep.found() + gdk_public_sources += [ + 'loaders/gdkavif.c' + ] +endif + gdk_public_headers = files([ 'gdk.h', 'gdkapplaunchcontext.h', @@ -237,6 +243,7 @@ gdk_deps = [ png_dep, tiff_dep, jpeg_dep, + avif_dep, ] if profiler_enabled diff --git a/meson.build b/meson.build index e6c19f31d8..8b123dd857 100644 --- a/meson.build +++ b/meson.build @@ -417,6 +417,9 @@ pixbuf_dep = dependency('gdk-pixbuf-2.0', version: gdk_pixbuf_req, png_dep = dependency('libpng', 'png') tiff_dep = dependency('libtiff-4', 'tiff') jpeg_dep = dependency('libjpeg', 'jpeg') +avif_dep = dependency('libavif', required: get_option('avif')) + +cdata.set('HAVE_AVIF', avif_dep.found()) epoxy_dep = dependency('epoxy', version: epoxy_req) xkbdep = dependency('xkbcommon', version: xkbcommon_req, required: wayland_enabled) @@ -955,6 +958,7 @@ summary('Cloud support', cloudproviders_dep.found(), section: 'Features') summary('Sysprof support', libsysprof_capture_dep.found(), section: 'Features') summary('Colord support', colord_dep.found(), section: 'Features') summary('Tracker support', tracker3_dep.found(), section: 'Features') +summary('AVIF support', avif_dep.found(), section: 'Features') summary('Compiler', cc.get_id(), section: 'Toolchain') summary('Linker', cc.get_linker_id(), section: 'Toolchain') diff --git a/meson_options.txt b/meson_options.txt index 299e33d91d..9f480f89a2 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -52,6 +52,11 @@ option('print-cups', # Optional features +option('avif', + type: 'feature', + value: 'disabled', + description: 'Enable support for AVIF images, for GTK development') + option('vulkan', type: 'feature', value: 'enabled',