/* GDK - The GIMP Drawing Kit
* Copyright (C) 2021 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 .
*/
#include "config.h"
#include "gdkpngprivate.h"
#include "gdktexture.h"
#include "gdktextureprivate.h"
#include "gdkmemorytextureprivate.h"
#include "gsk/ngl/fp16private.h"
#include
#include
/* The main difference between the png load/save code here and
* gdk-pixbuf is that we can support loading 16-bit data in the
* future, and we can extract gamma and colorspace information
* to produce linear, color-corrected data.
*/
/* {{{ Callbacks */
/* No sigsetjmp on Windows */
#ifndef HAVE_SIGSETJMP
#define sigjmp_buf jmp_buf
#define sigsetjmp(jb, x) setjmp(jb)
#define siglongjmp longjmp
#endif
typedef struct
{
guchar *data;
gsize size;
gsize position;
} png_io;
static void
png_read_func (png_structp png,
png_bytep data,
png_size_t size)
{
png_io *io;
io = png_get_io_ptr (png);
if (io->position + size > io->size)
png_error (png, "Read past EOF");
memcpy (data, io->data + io->position, size);
io->position += size;
}
static void
png_write_func (png_structp png,
png_bytep data,
png_size_t size)
{
png_io *io;
io = png_get_io_ptr (png);
if (io->position > io->size ||
io->size - io->position < size)
{
io->size = io->position + size;
io->data = g_realloc (io->data, io->size);
}
memcpy (io->data + io->position, data, size);
io->position += size;
}
static void
png_flush_func (png_structp png)
{
}
static png_voidp
png_malloc_callback (png_structp o,
png_size_t size)
{
return g_try_malloc (size);
}
static void
png_free_callback (png_structp o,
png_voidp x)
{
g_free (x);
}
static void
png_simple_error_callback (png_structp png,
png_const_charp error_msg)
{
GError **error = png_get_error_ptr (png);
if (error && !*error)
g_set_error (error,
GDK_TEXTURE_ERROR, GDK_TEXTURE_ERROR_CORRUPT_IMAGE,
"Error reading png (%s)", error_msg);
longjmp (png_jmpbuf (png), 1);
}
static void
png_simple_warning_callback (png_structp png,
png_const_charp error_msg)
{
}
/* }}} */
/* {{{ Format conversion */
static void
unpremultiply (guchar *data,
int width,
int height)
{
gsize x, y;
for (y = 0; y < height; y++)
{
for (x = 0; x < width; x++)
{
guchar *b = &data[x * 4];
guint32 pixel;
guchar alpha;
memcpy (&pixel, b, sizeof (guint32));
alpha = (pixel & 0xff000000) >> 24;
if (alpha == 0)
{
b[0] = 0;
b[1] = 0;
b[2] = 0;
b[3] = 0;
}
else
{
b[0] = (((pixel & 0x00ff0000) >> 16) * 255 + alpha / 2) / alpha;
b[1] = (((pixel & 0x0000ff00) >> 8) * 255 + alpha / 2) / alpha;
b[2] = (((pixel & 0x000000ff) >> 0) * 255 + alpha / 2) / alpha;
b[3] = alpha;
}
}
data += width * 4;
}
}
static void
unpremultiply_float_to_16bit (guchar *data,
int width,
int height)
{
gsize x, y;
float *src = (float *)data;;
guint16 *dest = (guint16 *)data;
for (y = 0; y < height; y++)
{
for (x = 0; x < width; x++)
{
float r, g, b, a;
r = src[0];
g = src[1];
b = src[2];
a = src[3];
if (a == 0)
{
dest[0] = 0;
dest[1] = 0;
dest[2] = 0;
dest[3] = 0;
}
else
{
dest[0] = (guint16) CLAMP (65536.f * r / a, 0.f, 65535.f);
dest[1] = (guint16) CLAMP (65536.f * g / a, 0.f, 65535.f);
dest[2] = (guint16) CLAMP (65536.f * b / a, 0.f, 65535.f);
dest[3] = (guint16) CLAMP (65536.f * a, 0.f, 65535.f);
}
dest += 4;
src += 4;
}
}
}
static inline int
multiply_alpha (int alpha, int color)
{
int temp = (alpha * color) + 0x80;
return ((temp + (temp >> 8)) >> 8);
}
static void
premultiply_data (png_structp png,
png_row_infop row_info,
png_bytep data)
{
unsigned int i;
for (i = 0; i < row_info->rowbytes; i += 4)
{
uint8_t *base = &data[i];
uint8_t alpha = base[3];
uint32_t p;
if (alpha == 0)
{
p = 0;
}
else
{
uint8_t red = base[0];
uint8_t green = base[1];
uint8_t blue = base[2];
if (alpha != 0xff)
{
red = multiply_alpha (alpha, red);
green = multiply_alpha (alpha, green);
blue = multiply_alpha (alpha, blue);
}
p = ((uint32_t)alpha << 24) | (red << 16) | (green << 8) | (blue << 0);
}
memcpy (base, &p, sizeof (uint32_t));
}
}
static void
convert_bytes_to_data (png_structp png,
png_row_infop row_info,
png_bytep data)
{
unsigned int i;
for (i = 0; i < row_info->rowbytes; i += 4)
{
uint8_t *base = &data[i];
uint8_t red = base[0];
uint8_t green = base[1];
uint8_t blue = base[2];
uint32_t pixel;
pixel = (0xffu << 24) | (red << 16) | (green << 8) | (blue << 0);
memcpy (base, &pixel, sizeof (uint32_t));
}
}
static void
premultiply_16bit (guchar *data,
int width,
int height,
int stride)
{
gsize x, y;
guint16 *src;
for (y = 0; y < height; y++)
{
src = (guint16 *)data;
for (x = 0; x < width; x++)
{
float alpha = src[x * 4 + 3] / 65535.f;
src[x * 4 ] = (guint16) CLAMP (src[x * 4 ] * alpha, 0.f, 65535.f);
src[x * 4 + 1] = (guint16) CLAMP (src[x * 4 + 1] * alpha, 0.f, 65535.f);
src[x * 4 + 2] = (guint16) CLAMP (src[x * 4 + 2] * alpha, 0.f, 65535.f);
}
data += stride;
}
}
/* }}} */
/* {{{ Public API */
GdkTexture *
gdk_load_png (GBytes *bytes,
GError **error)
{
png_io io;
png_struct *png = NULL;
png_info *info;
guint width, height;
int depth, color_type;
int interlace, stride;
GdkMemoryFormat format;
guchar *buffer = NULL;
guchar **row_pointers = NULL;
GBytes *out_bytes;
GdkTexture *texture;
int bpp;
io.data = (guchar *)g_bytes_get_data (bytes, &io.size);
io.position = 0;
png = png_create_read_struct_2 (PNG_LIBPNG_VER_STRING,
error,
png_simple_error_callback,
png_simple_warning_callback,
NULL,
png_malloc_callback,
png_free_callback);
if (png == NULL)
{
g_set_error (error,
GDK_TEXTURE_ERROR, GDK_TEXTURE_ERROR_INSUFFICIENT_MEMORY,
"Failed to parse png image");
return NULL;
}
info = png_create_info_struct (png);
if (info == NULL)
{
png_destroy_read_struct (&png, NULL, NULL);
g_set_error (error,
GDK_TEXTURE_ERROR, GDK_TEXTURE_ERROR_INSUFFICIENT_MEMORY,
"Failed to parse png image");
return NULL;
}
png_set_read_fn (png, &io, png_read_func);
if (sigsetjmp (png_jmpbuf (png), 1))
{
g_free (buffer);
g_free (row_pointers);
png_destroy_read_struct (&png, &info, NULL);
return NULL;
}
png_read_info (png, info);
png_get_IHDR (png, info,
&width, &height, &depth,
&color_type, &interlace, NULL, NULL);
if (color_type == PNG_COLOR_TYPE_PALETTE)
png_set_palette_to_rgb (png);
if (color_type == PNG_COLOR_TYPE_GRAY)
png_set_expand_gray_1_2_4_to_8 (png);
if (png_get_valid (png, info, PNG_INFO_tRNS))
png_set_tRNS_to_alpha (png);
if (depth == 8)
png_set_filler (png, 0xff, PNG_FILLER_AFTER);
if (depth < 8)
png_set_packing (png);
if (color_type == PNG_COLOR_TYPE_GRAY ||
color_type == PNG_COLOR_TYPE_GRAY_ALPHA)
png_set_gray_to_rgb (png);
if (interlace != PNG_INTERLACE_NONE)
png_set_interlace_handling (png);
png_read_update_info (png, info);
png_get_IHDR (png, info,
&width, &height, &depth,
&color_type, &interlace, NULL, NULL);
if ((depth != 8 && depth != 16) ||
!(color_type == PNG_COLOR_TYPE_RGB ||
color_type == PNG_COLOR_TYPE_RGB_ALPHA))
{
png_destroy_read_struct (&png, &info, NULL);
g_set_error (error,
GDK_TEXTURE_ERROR, GDK_TEXTURE_ERROR_UNSUPPORTED,
"Failed to parse png image");
return NULL;
}
switch (color_type)
{
case PNG_COLOR_TYPE_RGB_ALPHA:
if (depth == 8)
{
format = GDK_MEMORY_DEFAULT;
png_set_read_user_transform_fn (png, premultiply_data);
}
else
{
format = GDK_MEMORY_R16G16B16A16_PREMULTIPLIED;
#if G_BYTE_ORDER == G_LITTLE_ENDIAN
png_set_swap (png);
#endif
}
break;
case PNG_COLOR_TYPE_RGB:
if (depth == 8)
{
format = GDK_MEMORY_DEFAULT;
png_set_read_user_transform_fn (png, convert_bytes_to_data);
}
else
{
format = GDK_MEMORY_R16G16B16;
#if G_BYTE_ORDER == G_LITTLE_ENDIAN
png_set_swap (png);
#endif
}
break;
default:
g_assert_not_reached ();
}
bpp = gdk_memory_format_bytes_per_pixel (format);
stride = width * bpp;
if (stride % 8)
stride += 8 - stride % 8;
buffer = g_try_malloc_n (height, stride);
row_pointers = g_try_malloc_n (height, sizeof (char *));
if (!buffer || !row_pointers)
{
g_free (buffer);
g_free (row_pointers);
png_destroy_read_struct (&png, &info, NULL);
g_set_error_literal (error,
GDK_TEXTURE_ERROR, GDK_TEXTURE_ERROR_INSUFFICIENT_MEMORY,
"Not enough memory to load png");
return NULL;
}
for (int i = 0; i < height; i++)
row_pointers[i] = &buffer[i * stride];
png_read_image (png, row_pointers);
png_read_end (png, info);
if (format == GDK_MEMORY_R16G16B16A16_PREMULTIPLIED)
premultiply_16bit (buffer, width, height, stride);
out_bytes = g_bytes_new_take (buffer, height * stride);
texture = gdk_memory_texture_new (width, height, format, out_bytes, stride);
g_bytes_unref (out_bytes);
g_free (row_pointers);
png_destroy_read_struct (&png, &info, NULL);
return texture;
}
GBytes *
gdk_save_png (GdkTexture *texture)
{
png_struct *png = NULL;
png_info *info;
png_io io = { NULL, 0, 0 };
guint width, height, stride;
guchar *data = NULL;
guchar *row;
int y;
GdkTexture *mtexture;
GdkMemoryFormat format;
int png_format;
int depth;
width = gdk_texture_get_width (texture);
height = gdk_texture_get_height (texture);
mtexture = gdk_texture_download_texture (texture);
format = gdk_memory_texture_get_format (GDK_MEMORY_TEXTURE (mtexture));
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:
stride = width * 4;
data = g_malloc_n (stride, height);
gdk_texture_download (mtexture, data, stride);
unpremultiply (data, width, height);
png_format = PNG_COLOR_TYPE_RGB_ALPHA;
depth = 8;
break;
case GDK_MEMORY_R16G16B16:
case GDK_MEMORY_R16G16B16A16_PREMULTIPLIED:
case GDK_MEMORY_R16G16B16_FLOAT:
case GDK_MEMORY_R16G16B16A16_FLOAT_PREMULTIPLIED:
case GDK_MEMORY_R32G32B32_FLOAT:
case GDK_MEMORY_R32G32B32A32_FLOAT_PREMULTIPLIED:
data = g_malloc_n (width * 16, height);
gdk_texture_download_float (mtexture, (float *)data, width * 4);
unpremultiply_float_to_16bit (data, width, height);
png_format = PNG_COLOR_TYPE_RGB_ALPHA;
stride = width * 8;
depth = 16;
break;
case GDK_MEMORY_N_FORMATS:
default:
g_assert_not_reached ();
}
png = png_create_write_struct_2 (PNG_LIBPNG_VER_STRING, NULL,
png_simple_error_callback,
png_simple_warning_callback,
NULL,
png_malloc_callback,
png_free_callback);
if (!png)
return NULL;
info = png_create_info_struct (png);
if (!info)
{
png_destroy_read_struct (&png, NULL, NULL);
return NULL;
}
if (sigsetjmp (png_jmpbuf (png), 1))
{
g_free (data);
g_free (io.data);
png_destroy_read_struct (&png, &info, NULL);
return NULL;
}
png_set_write_fn (png, &io, png_write_func, png_flush_func);
png_set_IHDR (png, info, width, height, depth,
png_format,
PNG_INTERLACE_NONE,
PNG_COMPRESSION_TYPE_DEFAULT,
PNG_FILTER_TYPE_DEFAULT);
png_write_info (png, info);
#if G_BYTE_ORDER == G_LITTLE_ENDIAN
png_set_swap (png);
#endif
for (y = 0, row = data; y < height; y++, row += stride)
png_write_rows (png, &row, 1);
png_write_end (png, info);
png_destroy_write_struct (&png, &info);
g_free (data);
return g_bytes_new_take (io.data, io.size);
}
/* }}} */
/* vim:set foldmethod=marker expandtab: */