Almost fully colour-managed Wuffs (BMP, GIF, PNG)

This commit is contained in:
Přemysl Eric Janouch 2021-12-25 21:36:23 +01:00
parent 5e4476ff71
commit ccf15bc8ae
Signed by: p
GPG Key ID: A0420B94F92B9493
2 changed files with 99 additions and 34 deletions

View File

@ -6,7 +6,7 @@ raw photos, HEIC, AVIF, WebP, SVG, X11 cursors and TIFF, or whatever gdk-pixbuf
loads. loads.
Its development status can be summarized as '`beta`'. E.g., colour management Its development status can be summarized as '`beta`'. E.g., colour management
is partial, and certain GIFs animate wrong. is a bit incomplete, and certain GIFs animate wrong.
Non-goals Non-goals
--------- ---------

131
fiv-io.c
View File

@ -198,6 +198,14 @@ fiv_io_profile_new_sRGB(void)
#endif #endif
} }
static FivIoProfile
fiv_io_profile_new_from_bytes(GBytes *bytes)
{
gsize len = 0;
gconstpointer p = g_bytes_get_data(bytes, &len);
return fiv_io_profile_new(p, len);
}
void void
fiv_io_profile_free(FivIoProfile self) fiv_io_profile_free(FivIoProfile self)
{ {
@ -211,8 +219,10 @@ fiv_io_profile_free(FivIoProfile self)
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
// TODO(p): In general, try to use CAIRO_FORMAT_RGB30 or CAIRO_FORMAT_RGBA128F. // TODO(p): In general, try to use CAIRO_FORMAT_RGB30 or CAIRO_FORMAT_RGBA128F.
#define FIV_IO_LCMS2_ARGB \ #define FIV_IO_LCMS2_ARGB32 \
(G_BYTE_ORDER == G_LITTLE_ENDIAN ? TYPE_BGRA_8 : TYPE_ARGB_8) (G_BYTE_ORDER == G_LITTLE_ENDIAN ? TYPE_BGRA_8 : TYPE_ARGB_8)
#define FIV_IO_LCMS2_4X16LE \
(G_BYTE_ORDER == G_LITTLE_ENDIAN ? TYPE_BGRA_16 : TYPE_BGRA_16_SE)
// CAIRO_STRIDE_ALIGNMENT is 4 bytes, so there will be no padding with // CAIRO_STRIDE_ALIGNMENT is 4 bytes, so there will be no padding with
// ARGB/BGRA/XRGB/BGRX. // ARGB/BGRA/XRGB/BGRX.
@ -254,7 +264,7 @@ fiv_io_profile_cmyk(
cmsHTRANSFORM transform = NULL; cmsHTRANSFORM transform = NULL;
if (source && target) { if (source && target) {
transform = cmsCreateTransform(source, TYPE_CMYK_8_REV, target, transform = cmsCreateTransform(source, TYPE_CMYK_8_REV, target,
FIV_IO_LCMS2_ARGB, INTENT_PERCEPTUAL, 0); FIV_IO_LCMS2_ARGB32, INTENT_PERCEPTUAL, 0);
} }
if (transform) { if (transform) {
cmsDoTransform(transform, data, data, w * h); cmsDoTransform(transform, data, data, w * h);
@ -266,18 +276,16 @@ fiv_io_profile_cmyk(
} }
static void static void
fiv_io_profile_xrgb32( fiv_io_profile_xrgb32_direct(unsigned char *data, int w, int h,
cairo_surface_t *surface, FivIoProfile source, FivIoProfile target) FivIoProfile source, FivIoProfile target)
{ {
#ifndef HAVE_LCMS2 #ifndef HAVE_LCMS2
(void) surface; (void) data;
(void) w;
(void) h;
(void) source; (void) source;
(void) target; (void) target;
#else #else
unsigned char *data = cairo_image_surface_get_data(surface);
int w = cairo_image_surface_get_width(surface);
int h = cairo_image_surface_get_height(surface);
// TODO(p): We should make this optional. // TODO(p): We should make this optional.
cmsHPROFILE src_fallback = NULL; cmsHPROFILE src_fallback = NULL;
if (target && !source) if (target && !source)
@ -285,8 +293,48 @@ fiv_io_profile_xrgb32(
cmsHTRANSFORM transform = NULL; cmsHTRANSFORM transform = NULL;
if (source && target) { if (source && target) {
transform = cmsCreateTransform(source, FIV_IO_LCMS2_ARGB, target, transform = cmsCreateTransform(source, FIV_IO_LCMS2_ARGB32, target,
FIV_IO_LCMS2_ARGB, INTENT_PERCEPTUAL, 0); FIV_IO_LCMS2_ARGB32, INTENT_PERCEPTUAL, 0);
}
if (transform) {
cmsDoTransform(transform, data, data, w * h);
cmsDeleteTransform(transform);
}
if (src_fallback)
cmsCloseProfile(src_fallback);
#endif
}
static void
fiv_io_profile_xrgb32(
cairo_surface_t *surface, FivIoProfile source, FivIoProfile target)
{
unsigned char *data = cairo_image_surface_get_data(surface);
int w = cairo_image_surface_get_width(surface);
int h = cairo_image_surface_get_height(surface);
fiv_io_profile_xrgb32_direct(data, w, h, source, target);
}
static void
fiv_io_profile_4x16le_direct(
unsigned char *data, int w, int h, FivIoProfile source, FivIoProfile target)
{
#ifndef HAVE_LCMS2
(void) data;
(void) w;
(void) h;
(void) source;
(void) target;
#else
// TODO(p): We should make this optional.
cmsHPROFILE src_fallback = NULL;
if (target && !source)
source = src_fallback = cmsCreate_sRGBProfile();
cmsHTRANSFORM transform = NULL;
if (source && target) {
transform = cmsCreateTransform(source, FIV_IO_LCMS2_4X16LE, target,
FIV_IO_LCMS2_4X16LE, INTENT_PERCEPTUAL, 0);
} }
if (transform) { if (transform) {
cmsDoTransform(transform, data, data, w * h); cmsDoTransform(transform, data, data, w * h);
@ -303,14 +351,11 @@ static void
fiv_io_profile_xrgb32_page(cairo_surface_t *page, FivIoProfile target) fiv_io_profile_xrgb32_page(cairo_surface_t *page, FivIoProfile target)
{ {
GBytes *bytes = NULL; GBytes *bytes = NULL;
gsize len = 0;
gconstpointer p = NULL;
FivIoProfile source = NULL; FivIoProfile source = NULL;
if ((bytes = cairo_surface_get_user_data(page, &fiv_io_key_icc)) && if ((bytes = cairo_surface_get_user_data(page, &fiv_io_key_icc)))
(p = g_bytes_get_data(bytes, &len))) source = fiv_io_profile_new_from_bytes(bytes);
source = fiv_io_profile_new(p, len);
// TODO(p): Animations need to be composited in a linear colour space. // TODO(p): All animations need to be composited in a linear colour space.
for (cairo_surface_t *frame = page; frame != NULL; for (cairo_surface_t *frame = page; frame != NULL;
frame = cairo_surface_get_user_data(frame, &fiv_io_key_frame_next)) frame = cairo_surface_get_user_data(frame, &fiv_io_key_frame_next))
fiv_io_profile_xrgb32(frame, source, target); fiv_io_profile_xrgb32(frame, source, target);
@ -348,6 +393,8 @@ fiv_io_premultiply_argb32(cairo_surface_t *surface)
int h = cairo_image_surface_get_height(surface); int h = cairo_image_surface_get_height(surface);
unsigned char *data = cairo_image_surface_get_data(surface); unsigned char *data = cairo_image_surface_get_data(surface);
int stride = cairo_image_surface_get_stride(surface); int stride = cairo_image_surface_get_stride(surface);
if (cairo_image_surface_get_format(surface) != CAIRO_FORMAT_ARGB32)
return;
for (int y = 0; y < h; y++) { for (int y = 0; y < h; y++) {
uint32_t *dstp = (uint32_t *) (data + stride * y); uint32_t *dstp = (uint32_t *) (data + stride * y);
@ -454,6 +501,9 @@ struct load_wuffs_frame_context {
GBytes *meta_iccp; ///< Reference-counted ICC profile GBytes *meta_iccp; ///< Reference-counted ICC profile
GBytes *meta_xmp; ///< Reference-counted XMP GBytes *meta_xmp; ///< Reference-counted XMP
FivIoProfile target; ///< Target device profile, if any
FivIoProfile source; ///< Source colour profile, if any
cairo_surface_t *result; ///< The resulting surface (referenced) cairo_surface_t *result; ///< The resulting surface (referenced)
cairo_surface_t *result_tail; ///< The final animation frame cairo_surface_t *result_tail; ///< The final animation frame
}; };
@ -487,13 +537,8 @@ load_wuffs_frame(struct load_wuffs_frame_context *ctx, GError **error)
unsigned char *surface_data = cairo_image_surface_get_data(surface); unsigned char *surface_data = cairo_image_surface_get_data(surface);
int surface_stride = cairo_image_surface_get_stride(surface); int surface_stride = cairo_image_surface_get_stride(surface);
wuffs_base__pixel_buffer pb = {0}; wuffs_base__pixel_buffer pb = {0};
if (ctx->expand_16_float) { if (ctx->expand_16_float || ctx->pack_16_10) {
uint32_t targetbuf_size = ctx->height * ctx->width * 64; uint32_t targetbuf_size = ctx->height * ctx->width * 8;
targetbuf = g_malloc(targetbuf_size);
status = wuffs_base__pixel_buffer__set_from_slice(&pb, &ctx->cfg.pixcfg,
wuffs_base__make_slice_u8(targetbuf, targetbuf_size));
} else if (ctx->pack_16_10) {
uint32_t targetbuf_size = ctx->height * ctx->width * 16;
targetbuf = g_malloc(targetbuf_size); targetbuf = g_malloc(targetbuf_size);
status = wuffs_base__pixel_buffer__set_from_slice(&pb, &ctx->cfg.pixcfg, status = wuffs_base__pixel_buffer__set_from_slice(&pb, &ctx->cfg.pixcfg,
wuffs_base__make_slice_u8(targetbuf, targetbuf_size)); wuffs_base__make_slice_u8(targetbuf, targetbuf_size));
@ -517,6 +562,18 @@ load_wuffs_frame(struct load_wuffs_frame_context *ctx, GError **error)
goto fail; goto fail;
} }
if (ctx->target) {
if (ctx->expand_16_float || ctx->pack_16_10) {
fiv_io_profile_4x16le_direct(
targetbuf, ctx->width, ctx->height, ctx->source, ctx->target);
// The first one premultiplies below, the second doesn't need to.
} else {
fiv_io_profile_xrgb32_direct(surface_data, ctx->width, ctx->height,
ctx->source, ctx->target);
fiv_io_premultiply_argb32(surface);
}
}
if (ctx->expand_16_float) { if (ctx->expand_16_float) {
g_debug("Wuffs to Cairo RGBA128F"); g_debug("Wuffs to Cairo RGBA128F");
uint16_t *in = (uint16_t *) targetbuf; uint16_t *in = (uint16_t *) targetbuf;
@ -561,6 +618,7 @@ load_wuffs_frame(struct load_wuffs_frame_context *ctx, GError **error)
// Apply that frame's disposal method. // Apply that frame's disposal method.
wuffs_base__rect_ie_u32 bounds = wuffs_base__rect_ie_u32 bounds =
wuffs_base__frame_config__bounds(&ctx->last_fc); wuffs_base__frame_config__bounds(&ctx->last_fc);
// TODO(p): This field needs to be colour-managed.
wuffs_base__color_u32_argb_premul bg = wuffs_base__color_u32_argb_premul bg =
wuffs_base__frame_config__background_color(&ctx->last_fc); wuffs_base__frame_config__background_color(&ctx->last_fc);
@ -650,10 +708,11 @@ fail:
// is pure C, and a good reference. I can't use the auxiliary libraries, // is pure C, and a good reference. I can't use the auxiliary libraries,
// since they depend on C++, which is undesirable. // since they depend on C++, which is undesirable.
static cairo_surface_t * static cairo_surface_t *
open_wuffs( open_wuffs(wuffs_base__image_decoder *dec, wuffs_base__io_buffer src,
wuffs_base__image_decoder *dec, wuffs_base__io_buffer src, GError **error) FivIoProfile profile, GError **error)
{ {
struct load_wuffs_frame_context ctx = {.dec = dec, .src = &src}; struct load_wuffs_frame_context ctx = {
.dec = dec, .src = &src, .target = profile};
// TODO(p): PNG also has sRGB and gAMA, as well as text chunks (Wuffs #58). // TODO(p): PNG also has sRGB and gAMA, as well as text chunks (Wuffs #58).
// The former two use WUFFS_BASE__MORE_INFORMATION__FLAVOR__METADATA_PARSED. // The former two use WUFFS_BASE__MORE_INFORMATION__FLAVOR__METADATA_PARSED.
@ -720,13 +779,16 @@ open_wuffs(
goto fail; goto fail;
} }
if (ctx.target && ctx.meta_iccp)
ctx.source = fiv_io_profile_new_from_bytes(ctx.meta_iccp);
// Wuffs maps tRNS to BGRA in `decoder.decode_trns?`, we should be fine. // Wuffs maps tRNS to BGRA in `decoder.decode_trns?`, we should be fine.
// wuffs_base__pixel_format__transparency() doesn't reflect the image file. // wuffs_base__pixel_format__transparency() doesn't reflect the image file.
// TODO(p): See if wuffs_base__image_config__first_frame_is_opaque() causes // TODO(p): See if wuffs_base__image_config__first_frame_is_opaque() causes
// issues with animations, and eventually ensure an alpha-capable format. // issues with animations, and eventually ensure an alpha-capable format.
bool opaque = wuffs_base__image_config__first_frame_is_opaque(&ctx.cfg); bool opaque = wuffs_base__image_config__first_frame_is_opaque(&ctx.cfg);
// Wuffs' API is kind of awful--we want to catch deep RGB and deep grey. // Wuffs' API is kind of awful--we want to catch wide RGB and wide grey.
wuffs_base__pixel_format srcfmt = wuffs_base__pixel_format srcfmt =
wuffs_base__pixel_config__pixel_format(&ctx.cfg.pixcfg); wuffs_base__pixel_config__pixel_format(&ctx.cfg.pixcfg);
uint32_t bpp = wuffs_base__pixel_format__bits_per_pixel(&srcfmt); uint32_t bpp = wuffs_base__pixel_format__bits_per_pixel(&srcfmt);
@ -744,7 +806,7 @@ open_wuffs(
// XXX: WUFFS_BASE__PIXEL_FORMAT__ARGB_PREMUL is not expressible, only RGBA. // XXX: WUFFS_BASE__PIXEL_FORMAT__ARGB_PREMUL is not expressible, only RGBA.
// Wuffs doesn't support big-endian architectures at all, we might want to // Wuffs doesn't support big-endian architectures at all, we might want to
// fall back to spng in such cases, or do a second conversion. // fall back to spng in such cases, or do a second conversion.
uint32_t wuffs_format = WUFFS_BASE__PIXEL_FORMAT__BGRA_PREMUL; uint32_t wuffs_format = WUFFS_BASE__PIXEL_FORMAT__BGRA_NONPREMUL;
// CAIRO_FORMAT_ARGB32: "The 32-bit quantities are stored native-endian. // CAIRO_FORMAT_ARGB32: "The 32-bit quantities are stored native-endian.
// Pre-multiplied alpha is used." CAIRO_FORMAT_RGB{24,30} are analogous. // Pre-multiplied alpha is used." CAIRO_FORMAT_RGB{24,30} are analogous.
@ -764,8 +826,9 @@ open_wuffs(
ctx.cairo_format = CAIRO_FORMAT_RGB30; ctx.cairo_format = CAIRO_FORMAT_RGB30;
} else if (opaque) { } else if (opaque) {
// BGRX doesn't have as wide swizzler support, namely in GIF. // BGRX doesn't have as wide swizzler support, namely in GIF.
wuffs_format = WUFFS_BASE__PIXEL_FORMAT__BGRA_NONPREMUL;
ctx.cairo_format = CAIRO_FORMAT_RGB24; ctx.cairo_format = CAIRO_FORMAT_RGB24;
} else if (!ctx.target) {
wuffs_format = WUFFS_BASE__PIXEL_FORMAT__BGRA_PREMUL;
} }
wuffs_base__pixel_config__set(&ctx.cfg.pixcfg, wuffs_format, wuffs_base__pixel_config__set(&ctx.cfg.pixcfg, wuffs_format,
@ -794,6 +857,7 @@ fail:
g_clear_pointer(&ctx.meta_exif, g_bytes_unref); g_clear_pointer(&ctx.meta_exif, g_bytes_unref);
g_clear_pointer(&ctx.meta_iccp, g_bytes_unref); g_clear_pointer(&ctx.meta_iccp, g_bytes_unref);
g_clear_pointer(&ctx.meta_xmp, g_bytes_unref); g_clear_pointer(&ctx.meta_xmp, g_bytes_unref);
g_clear_pointer(&ctx.source, fiv_io_profile_free);
return ctx.result; return ctx.result;
} }
@ -807,10 +871,11 @@ open_wuffs_using(wuffs_base__image_decoder *(*allocate)(),
return NULL; return NULL;
} }
cairo_surface_t *surface = open_wuffs( cairo_surface_t *surface =
dec, wuffs_base__ptr_u8__reader((uint8_t *) data, len, TRUE), error); open_wuffs(dec, wuffs_base__ptr_u8__reader((uint8_t *) data, len, TRUE),
profile, error);
free(dec); free(dec);
return fiv_io_profile_finalize(surface, profile); return surface;
} }
// --- JPEG -------------------------------------------------------------------- // --- JPEG --------------------------------------------------------------------