From 84269b2ba21b2f996f5081ea3603457b0a699513 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C5=99emysl=20Eric=20Janouch?= Date: Tue, 23 Jan 2024 22:14:21 +0100 Subject: [PATCH] Load AdobeRGB Nikon JPEGs correctly --- LICENSE | 2 +- fiv-io.c | 127 ++++++++++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 122 insertions(+), 7 deletions(-) diff --git a/LICENSE b/LICENSE index 54d18a4..96d7caf 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2021 - 2023, Přemysl Eric Janouch +Copyright (c) 2021 - 2024, Přemysl Eric Janouch Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. diff --git a/fiv-io.c b/fiv-io.c index 46cc6c8..a526b2e 100644 --- a/fiv-io.c +++ b/fiv-io.c @@ -1,7 +1,7 @@ // // fiv-io.c: image operations // -// Copyright (c) 2021 - 2023, Přemysl Eric Janouch +// Copyright (c) 2021 - 2024, Přemysl Eric Janouch // // Permission to use, copy, modify, and/or distribute this software for any // purpose with or without fee is hereby granted. @@ -316,29 +316,44 @@ fiv_io_profile_new_sRGB(void) } FivIoProfile -fiv_io_profile_new_sRGB_gamma(double gamma) +fiv_io_profile_new_parametric( + double gamma, double whitepoint[2], double primaries[6]) { #ifdef HAVE_LCMS2 // TODO(p): Make sure to use the library in a thread-safe manner. cmsContext context = NULL; - static const cmsCIExyY D65 = {0.3127, 0.3290, 1.0}; - static const cmsCIExyYTRIPLE primaries = { - {0.6400, 0.3300, 1.0}, {0.3000, 0.6000, 1.0}, {0.1500, 0.0600, 1.0}}; + const cmsCIExyY cmsWP = {whitepoint[0], whitepoint[1], 1.0}; + const cmsCIExyYTRIPLE cmsP = { + {primaries[0], primaries[1], 1.0}, + {primaries[2], primaries[3], 1.0}, + {primaries[4], primaries[5], 1.0}, + }; + cmsToneCurve *curve = cmsBuildGamma(context, gamma); if (!curve) return NULL; cmsHPROFILE profile = cmsCreateRGBProfileTHR( - context, &D65, &primaries, (cmsToneCurve *[3]){curve, curve, curve}); + context, &cmsWP, &cmsP, (cmsToneCurve *[3]){curve, curve, curve}); cmsFreeToneCurve(curve); return profile; #else (void) gamma; + (void) whitepoint; + (void) primaries; return NULL; #endif } +FivIoProfile +fiv_io_profile_new_sRGB_gamma(double gamma) +{ + return fiv_io_profile_new_parametric(gamma, + (double[2]){0.3127, 0.3290}, + (double[6]){0.6400, 0.3300, 0.3000, 0.6000, 0.1500, 0.0600}); +} + static FivIoProfile fiv_io_profile_new_from_bytes(GBytes *bytes) { @@ -1329,6 +1344,100 @@ parse_mpf( // --- JPEG -------------------------------------------------------------------- +struct exif_profile { + double whitepoint[2]; ///< TIFF_WhitePoint + double primaries[6]; ///< TIFF_PrimaryChromaticities + enum Exif_ColorSpace colorspace; ///< Exif_ColorSpace + double gamma; ///< Exif_Gamma + + bool have_whitepoint; + bool have_primaries; + bool have_colorspace; + bool have_gamma; +}; + +static bool +parse_exif_profile_reals( + const struct tiffer *T, struct tiffer_entry *entry, double *out) +{ + while (tiffer_real(T, entry, out++)) + if (!tiffer_next_value(entry)) + return false; + return true; +} + +static void +parse_exif_profile_subifd( + struct exif_profile *params, const struct tiffer *T, uint32_t offset) +{ + struct tiffer subT = {}; + if (!tiffer_subifd(T, offset, &subT)) + return; + + struct tiffer_entry entry = {}; + while (tiffer_next_entry(&subT, &entry)) { + int64_t value = 0; + if (G_UNLIKELY(entry.tag == Exif_ColorSpace) && + entry.type == TIFFER_SHORT && entry.remaining_count == 1 && + tiffer_integer(&subT, &entry, &value)) { + params->have_colorspace = true; + params->colorspace = value; + } else if (G_UNLIKELY(entry.tag == Exif_Gamma) && + entry.type == TIFFER_RATIONAL && entry.remaining_count == 1 && + tiffer_real(&subT, &entry, ¶ms->gamma)) { + params->have_gamma = true; + } + } +} + +static FivIoProfile +parse_exif_profile(const void *data, size_t len) +{ + struct tiffer T = {}; + if (!tiffer_init(&T, (const uint8_t *) data, len) || !tiffer_next_ifd(&T)) + return NULL; + + struct exif_profile params = {}; + struct tiffer_entry entry = {}; + while (tiffer_next_entry(&T, &entry)) { + int64_t offset = 0; + if (G_UNLIKELY(entry.tag == TIFF_ExifIFDPointer) && + entry.type == TIFFER_LONG && entry.remaining_count == 1 && + tiffer_integer(&T, &entry, &offset) && + offset >= 0 && offset <= UINT32_MAX) { + parse_exif_profile_subifd(¶ms, &T, offset); + } else if (G_UNLIKELY(entry.tag == TIFF_WhitePoint) && + entry.type == TIFFER_RATIONAL && + entry.remaining_count == G_N_ELEMENTS(params.whitepoint)) { + params.have_whitepoint = + parse_exif_profile_reals(&T, &entry, params.whitepoint); + } else if (G_UNLIKELY(entry.tag == TIFF_PrimaryChromaticities) && + entry.type == TIFFER_RATIONAL && + entry.remaining_count == G_N_ELEMENTS(params.primaries)) { + params.have_primaries = + parse_exif_profile_reals(&T, &entry, params.primaries); + } + } + if (!params.have_colorspace) + return NULL; + + // If sRGB is claimed, assume all parameters are standard. + if (params.colorspace == Exif_ColorSpace_sRGB) + return fiv_io_profile_new_sRGB(); + + // AdobeRGB Nikon JPEGs provide all of these. + if (params.colorspace != Exif_ColorSpace_Uncalibrated || + !params.have_gamma || + !params.have_whitepoint || + !params.have_primaries) + return NULL; + + return fiv_io_profile_new_parametric( + params.gamma, params.whitepoint, params.primaries); +} + +// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + struct jpeg_metadata { GByteArray *exif; ///< Exif buffer or NULL GByteArray *icc; ///< ICC profile buffer or NULL @@ -1481,6 +1590,9 @@ load_jpeg_finalize(FivIoImage *image, bool cmyk, if (icc_profile) source = fiv_io_profile_new( g_bytes_get_data(icc_profile, NULL), g_bytes_get_size(icc_profile)); + else if (image->exif) + source = parse_exif_profile( + g_bytes_get_data(image->exif, NULL), g_bytes_get_size(image->exif)); if (cmyk) fiv_io_profile_cmyk(image, source, ctx->screen_profile); @@ -2123,6 +2235,9 @@ load_tiff_ep( orientation >= 1 && orientation <= 8) { image->orientation = orientation; } + + // XXX: AdobeRGB Nikon NEFs can only be distinguished by a ColorSpace tag + // from within their MakerNote. return image; }