From 88ec0c9093920f5c1decb44cf0d64318083d4e67 Mon Sep 17 00:00:00 2001 From: Daniel Holth Date: Tue, 8 Jul 2025 17:30:32 -0400 Subject: [PATCH 1/3] jxl.c: Brotli-compress metadata Necessary Exiv2 feature was released some time ago. --- src/imageio/format/jxl.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/imageio/format/jxl.c b/src/imageio/format/jxl.c index c81b47d9dcbe..d96660f0331c 100644 --- a/src/imageio/format/jxl.c +++ b/src/imageio/format/jxl.c @@ -319,8 +319,8 @@ int write_image(struct dt_imageio_module_data_t *data, if(!exif_buf) JXL_FAIL("could not allocate Exif buffer of size %zu", (size_t)(exif_len + 4)); memmove(exif_buf + 4, exif, exif_len); - // Exiv2 < 0.28 doesn't support Brotli compressed boxes - LIBJXL_ASSERT(JxlEncoderAddBox(encoder, "Exif", exif_buf, exif_len + 4, JXL_FALSE)); + // Exiv2 >= 0.28 (released 2023-05-08) supports Brotli compressed boxes + LIBJXL_ASSERT(JxlEncoderAddBox(encoder, "Exif", exif_buf, exif_len + 4, JXL_TRUE)); } /* TODO: workaround; remove when exiv2 implements JXL BMFF write support and update flags() */ @@ -332,9 +332,9 @@ int write_image(struct dt_imageio_module_data_t *data, if(xmp_string && (xmp_len = strlen(xmp_string)) > 0) { - // Exiv2 < 0.28 doesn't support Brotli compressed boxes + // Exiv2 >= 0.28 (released 2023-05-08) supports Brotli compressed boxes LIBJXL_ASSERT(JxlEncoderAddBox(encoder, "xml ", - (const uint8_t *)xmp_string, xmp_len, JXL_FALSE)); + (const uint8_t *)xmp_string, xmp_len, JXL_TRUE)); } } From f40a8fb8c7eead9bcbaab8e6b24823dbd3b1c7b6 Mon Sep 17 00:00:00 2001 From: Daniel Holth Date: Thu, 10 Jul 2025 09:42:54 -0400 Subject: [PATCH 2/3] write exif data to separate exv file --- src/common/exif.cc | 21 +++++++++++ src/common/exif.h | 6 +++ src/imageio/format/jxl.c | 79 ++++++++++++++++++++++++++-------------- src/imageio/imageio.c | 2 +- 4 files changed, 79 insertions(+), 29 deletions(-) diff --git a/src/common/exif.cc b/src/common/exif.cc index d8f4b583ffbc..a106b9cf3ea0 100644 --- a/src/common/exif.cc +++ b/src/common/exif.cc @@ -2442,6 +2442,27 @@ gboolean dt_exif_read(dt_image_t *img, } } +int dt_exif_write_exv(uint8_t *blob, + uint32_t size, + const char *path, + const int compressed) +{ + try + { + std::unique_ptr image(Exiv2::ImageFactory::create(Exiv2::ImageType::exv, WIDEN(path))); + image->writeMetadata(); + } + catch(const Exiv2::AnyError &e) + { + dt_print(DT_DEBUG_IMAGEIO, + "[exiv2 dt_exif_write_blob] %s: %s", + path, + e.what()); + return 0; + } + return 1; +} + int dt_exif_write_blob(uint8_t *blob, uint32_t size, const char *path, diff --git a/src/common/exif.h b/src/common/exif.h index a679f402ba6e..dbd42c68d6ca 100644 --- a/src/common/exif.h +++ b/src/common/exif.h @@ -92,6 +92,12 @@ int dt_exif_read_blob(uint8_t **blob, const char *path, const dt_imgid_t imgid, /** Reads exif tags that are not cached in the database */ void dt_exif_img_check_additional_tags(dt_image_t *img, const char *filename); +/** create empty metadata file */ +int dt_exif_write_exv(uint8_t *blob, + uint32_t size, + const char *path, + const int compressed); + /** write blob to file exif. merges with existing exif information.*/ int dt_exif_write_blob(uint8_t *blob, uint32_t size, const char *path, const int compressed); diff --git a/src/imageio/format/jxl.c b/src/imageio/format/jxl.c index d96660f0331c..6103b3296b89 100644 --- a/src/imageio/format/jxl.c +++ b/src/imageio/format/jxl.c @@ -309,34 +309,34 @@ int write_image(struct dt_imageio_module_data_t *data, if(exif && exif_len > 0) LIBJXL_ASSERT(JxlEncoderUseBoxes(encoder)); - /* TODO: workaround; remove when exiv2 implements JXL BMFF write support and use dt_exif_write_blob() after - * closing file instead */ - if(exif && exif_len > 0) - { - // Prepend the 4 byte (zero) offset to the blob before writing - // (as required in the equivalent HEIF/JPEG XS Exif box specs) - exif_buf = g_try_malloc0(exif_len + 4); - if(!exif_buf) - JXL_FAIL("could not allocate Exif buffer of size %zu", (size_t)(exif_len + 4)); - memmove(exif_buf + 4, exif, exif_len); - // Exiv2 >= 0.28 (released 2023-05-08) supports Brotli compressed boxes - LIBJXL_ASSERT(JxlEncoderAddBox(encoder, "Exif", exif_buf, exif_len + 4, JXL_TRUE)); - } - - /* TODO: workaround; remove when exiv2 implements JXL BMFF write support and update flags() */ - /* TODO: workaround; uses valid exif as a way to indicate ALL metadata was requested */ - if(exif && exif_len > 0) - { - xmp_string = dt_exif_xmp_read_string(imgid); - size_t xmp_len; - if(xmp_string - && (xmp_len = strlen(xmp_string)) > 0) - { - // Exiv2 >= 0.28 (released 2023-05-08) supports Brotli compressed boxes - LIBJXL_ASSERT(JxlEncoderAddBox(encoder, "xml ", - (const uint8_t *)xmp_string, xmp_len, JXL_TRUE)); - } - } + // /* TODO: workaround; remove when exiv2 implements JXL BMFF write support and use dt_exif_write_blob() after + // * closing file instead */ + // if(exif && exif_len > 0) + // { + // // Prepend the 4 byte (zero) offset to the blob before writing + // // (as required in the equivalent HEIF/JPEG XS Exif box specs) + // exif_buf = g_try_malloc0(exif_len + 4); + // if(!exif_buf) + // JXL_FAIL("could not allocate Exif buffer of size %zu", (size_t)(exif_len + 4)); + // memmove(exif_buf + 4, exif, exif_len); + // // Exiv2 >= 0.28 (released 2023-05-08) supports Brotli compressed boxes + // LIBJXL_ASSERT(JxlEncoderAddBox(encoder, "Exif", exif_buf, exif_len + 4, JXL_TRUE)); + // } + + // /* TODO: workaround; remove when exiv2 implements JXL BMFF write support and update flags() */ + // /* TODO: workaround; uses valid exif as a way to indicate ALL metadata was requested */ + // if(exif && exif_len > 0) + // { + // xmp_string = dt_exif_xmp_read_string(imgid); + // size_t xmp_len; + // if(xmp_string + // && (xmp_len = strlen(xmp_string)) > 0) + // { + // // Exiv2 >= 0.28 (released 2023-05-08) supports Brotli compressed boxes + // LIBJXL_ASSERT(JxlEncoderAddBox(encoder, "xml ", + // (const uint8_t *)xmp_string, xmp_len, JXL_TRUE)); + // } + // } JxlPixelFormat pixel_format = { 3, JXL_TYPE_FLOAT, JXL_NATIVE_ENDIAN, 0 }; @@ -424,6 +424,29 @@ int write_image(struct dt_imageio_module_data_t *data, g_free(xmp_string); g_free(out_buf); + // #ifdef EXV_ENABLE_BMPP (exiv can read from, but not write to bmpp)s + // #ifdef EXV_HAVE_BROTLI (exiv supports brotli compression + + // move this before the image is written in imageio.c, then do all metadata + // processing on exv, then send final metadata to jxl codec. + char* exv_filename = g_malloc(strlen(filename) + 5); + snprintf(exv_filename, strlen(filename) + 5, "%s%s", filename, ".exv"); + + // empty metadata file (call write_blob inside there for us) + dt_exif_write_exv(exif, exif_len, exv_filename, 1); + + if(exif) { + dt_print(DT_DEBUG_ALWAYS, + "[jxl] exif_write_blob"); + dt_exif_write_blob(exif, exif_len, exv_filename, 1); + } else { + dt_print(DT_DEBUG_ALWAYS, + "[jxl] no exif_write_blob"); + } + + g_free(exv_filename); + // now read from exv, include exif blob in jxl + return ret; } diff --git a/src/imageio/imageio.c b/src/imageio/imageio.c index d36678c09762..bf719aa1c3e5 100644 --- a/src/imageio/imageio.c +++ b/src/imageio/imageio.c @@ -1436,7 +1436,7 @@ gboolean dt_imageio_export_with_flags(const dt_imgid_t imgid, if(!ignore_exif && (!strcmp(format->mime(NULL), "image/avif") || !strcmp(format->mime(NULL), "image/x-exr") - || !strcmp(format->mime(NULL), "image/jxl") + // || !strcmp(format->mime(NULL), "image/jxl") || !strcmp(format->mime(NULL), "image/x-xcf"))) { const int32_t meta_all = From f1f49a3b93b081bae07432c8af85b2f528e26871 Mon Sep 17 00:00:00 2001 From: Daniel Holth Date: Sat, 12 Jul 2025 08:53:03 -0400 Subject: [PATCH 3/3] write exif metadata before writing image --- src/imageio/format/jxl.c | 79 ++++++++++++++-------------------------- src/imageio/imageio.c | 49 +++++++++++++++++++------ 2 files changed, 65 insertions(+), 63 deletions(-) diff --git a/src/imageio/format/jxl.c b/src/imageio/format/jxl.c index 6103b3296b89..935768d5918d 100644 --- a/src/imageio/format/jxl.c +++ b/src/imageio/format/jxl.c @@ -311,34 +311,34 @@ int write_image(struct dt_imageio_module_data_t *data, // /* TODO: workaround; remove when exiv2 implements JXL BMFF write support and use dt_exif_write_blob() after // * closing file instead */ - // if(exif && exif_len > 0) - // { - // // Prepend the 4 byte (zero) offset to the blob before writing - // // (as required in the equivalent HEIF/JPEG XS Exif box specs) - // exif_buf = g_try_malloc0(exif_len + 4); - // if(!exif_buf) - // JXL_FAIL("could not allocate Exif buffer of size %zu", (size_t)(exif_len + 4)); - // memmove(exif_buf + 4, exif, exif_len); - // // Exiv2 >= 0.28 (released 2023-05-08) supports Brotli compressed boxes - // LIBJXL_ASSERT(JxlEncoderAddBox(encoder, "Exif", exif_buf, exif_len + 4, JXL_TRUE)); - // } - - // /* TODO: workaround; remove when exiv2 implements JXL BMFF write support and update flags() */ - // /* TODO: workaround; uses valid exif as a way to indicate ALL metadata was requested */ - // if(exif && exif_len > 0) - // { - // xmp_string = dt_exif_xmp_read_string(imgid); - // size_t xmp_len; - // if(xmp_string - // && (xmp_len = strlen(xmp_string)) > 0) - // { - // // Exiv2 >= 0.28 (released 2023-05-08) supports Brotli compressed boxes - // LIBJXL_ASSERT(JxlEncoderAddBox(encoder, "xml ", - // (const uint8_t *)xmp_string, xmp_len, JXL_TRUE)); - // } - // } - - JxlPixelFormat pixel_format = { 3, JXL_TYPE_FLOAT, JXL_NATIVE_ENDIAN, 0 }; + if(exif && exif_len > 0) + { + // Prepend the 4 byte (zero) offset to the blob before writing + // (as required in the equivalent HEIF/JPEG XS Exif box specs) + exif_buf = g_try_malloc0(exif_len + 4); + if(!exif_buf) + JXL_FAIL("could not allocate Exif buffer of size %zu", (size_t)(exif_len + 4)); + memmove(exif_buf + 4, exif, exif_len); + // Exiv2 >= 0.28 (released 2023-05-08) supports Brotli compressed boxes + LIBJXL_ASSERT(JxlEncoderAddBox(encoder, "Exif", exif_buf, exif_len + 4, JXL_TRUE)); + } + + /* TODO: workaround; remove when exiv2 implements JXL BMFF write support and update flags() */ + /* TODO: workaround; uses valid exif as a way to indicate ALL metadata was requested */ + if(exif && exif_len > 0) + { + xmp_string = dt_exif_xmp_read_string(imgid); + size_t xmp_len; + if(xmp_string + && (xmp_len = strlen(xmp_string)) > 0) + { + // Exiv2 >= 0.28 (released 2023-05-08) supports Brotli compressed boxes + LIBJXL_ASSERT(JxlEncoderAddBox(encoder, "xml ", + (const uint8_t *)xmp_string, xmp_len, JXL_TRUE)); + } + } + +f JxlPixelFormat pixel_format = { 3, JXL_TYPE_FLOAT, JXL_NATIVE_ENDIAN, 0 }; // Fix pixel stride const size_t pixels_size = width * height * 3 * sizeof(float); @@ -424,29 +424,6 @@ int write_image(struct dt_imageio_module_data_t *data, g_free(xmp_string); g_free(out_buf); - // #ifdef EXV_ENABLE_BMPP (exiv can read from, but not write to bmpp)s - // #ifdef EXV_HAVE_BROTLI (exiv supports brotli compression - - // move this before the image is written in imageio.c, then do all metadata - // processing on exv, then send final metadata to jxl codec. - char* exv_filename = g_malloc(strlen(filename) + 5); - snprintf(exv_filename, strlen(filename) + 5, "%s%s", filename, ".exv"); - - // empty metadata file (call write_blob inside there for us) - dt_exif_write_exv(exif, exif_len, exv_filename, 1); - - if(exif) { - dt_print(DT_DEBUG_ALWAYS, - "[jxl] exif_write_blob"); - dt_exif_write_blob(exif, exif_len, exv_filename, 1); - } else { - dt_print(DT_DEBUG_ALWAYS, - "[jxl] no exif_write_blob"); - } - - g_free(exv_filename); - // now read from exv, include exif blob in jxl - return ret; } diff --git a/src/imageio/imageio.c b/src/imageio/imageio.c index bf719aa1c3e5..5d4f2bdb443d 100644 --- a/src/imageio/imageio.c +++ b/src/imageio/imageio.c @@ -1446,6 +1446,40 @@ gboolean dt_imageio_export_with_flags(const dt_imgid_t imgid, md_flags_set = metadata ? (metadata->flags & meta_all) == meta_all : FALSE; } + uint8_t *exif_profile0 = NULL; // Exif data should be 65536 bytes + // max, but if original size is + // close to that, adding new tags + // could make it go over that... so + // let it be and see what happens + // when we write the image + char pathname[PATH_MAX] = { 0 }; + gboolean from_cache = TRUE; + dt_image_full_path(imgid, pathname, sizeof(pathname), &from_cache); + + // last param is dng mode, it's false here + const int length0 = dt_exif_read_blob(&exif_profile0, pathname, imgid, sRGB, + processed_width, processed_height, FALSE); + + char* exv_filename = g_malloc(strlen(filename) + 5); + snprintf(exv_filename, strlen(filename) + 5, "%s%s", filename, ".exv"); + + // empty metadata file (XXX call write_blob inside there for us) + dt_exif_write_exv(exif_profile0, length0, exv_filename, 1); + + // write data into metadata file + dt_exif_write_blob(exif_profile0, length0, exv_filename, 1); + + free(exif_profile0); + + /* now write xmp into that container, if possible */ + if(copy_metadata + && (format->flags(format_params) & FORMAT_FLAGS_SUPPORT_XMP)) + { + dt_exif_xmp_attach_export(imgid, exv_filename, metadata, &dev, &pipe); + // no need to cancel the export if this fail + } + + // write image including filtered metadata from .exv if(!ignore_exif && md_flags_set) { uint8_t *exif_profile = NULL; // Exif data should be 65536 bytes @@ -1454,12 +1488,8 @@ gboolean dt_imageio_export_with_flags(const dt_imgid_t imgid, // could make it go over that... so // let it be and see what happens // when we write the image - char pathname[PATH_MAX] = { 0 }; - gboolean from_cache = TRUE; - dt_image_full_path(imgid, pathname, sizeof(pathname), &from_cache); - // last param is dng mode, it's false here - const int length = dt_exif_read_blob(&exif_profile, pathname, imgid, sRGB, + const int length = dt_exif_read_blob(&exif_profile, exv_filename, imgid, sRGB, processed_width, processed_height, FALSE); res = (format->write_image(format_params, filename, outbuf, icc_type, @@ -1475,16 +1505,11 @@ gboolean dt_imageio_export_with_flags(const dt_imgid_t imgid, &pipe, export_masks)) != 0; } + g_free(exv_filename); + if(res) goto error; - /* now write xmp into that container, if possible */ - if(copy_metadata - && (format->flags(format_params) & FORMAT_FLAGS_SUPPORT_XMP)) - { - dt_exif_xmp_attach_export(imgid, filename, metadata, &dev, &pipe); - // no need to cancel the export if this fail - } dt_dev_pixelpipe_cleanup(&pipe); dt_dev_cleanup(&dev);