diff --git a/src/common/exif.cc b/src/common/exif.cc index 6fd7b862ebcb..a67efa701820 100644 --- a/src/common/exif.cc +++ b/src/common/exif.cc @@ -2629,6 +2629,37 @@ gboolean dt_exif_read(dt_image_t *img, } } +// Filter unwanted Exif tags for export (thumbnails and pixel dimensions for non-compressed) +static void _filter_exif_for_export(Exiv2::ExifData &exifData, const int compressed) +{ + // Remove thumbnail + { + static const char *keys[] = { + "Exif.Thumbnail.Compression", + "Exif.Thumbnail.XResolution", + "Exif.Thumbnail.YResolution", + "Exif.Thumbnail.ResolutionUnit", + "Exif.Thumbnail.JPEGInterchangeFormat", + "Exif.Thumbnail.JPEGInterchangeFormatLength" + }; + static const guint n_keys = G_N_ELEMENTS(keys); + _remove_exif_keys(exifData, keys, n_keys); + } + + // Only compressed images may set PixelXDimension and PixelYDimension + if(!compressed) + { + static const char *keys[] = { + "Exif.Photo.PixelXDimension", + "Exif.Photo.PixelYDimension" + }; + static const guint n_keys = G_N_ELEMENTS(keys); + _remove_exif_keys(exifData, keys, n_keys); + } + + exifData.sortByTag(); +} + int dt_exif_write_blob(uint8_t *blob, uint32_t size, const char *path, @@ -2653,32 +2684,7 @@ int dt_exif_write_blob(uint8_t *blob, imgExifData.add(Exiv2::ExifKey(i->key()), &i->value()); } - { - // Remove thumbnail - static const char *keys[] = { - "Exif.Thumbnail.Compression", - "Exif.Thumbnail.XResolution", - "Exif.Thumbnail.YResolution", - "Exif.Thumbnail.ResolutionUnit", - "Exif.Thumbnail.JPEGInterchangeFormat", - "Exif.Thumbnail.JPEGInterchangeFormatLength" - }; - static const guint n_keys = G_N_ELEMENTS(keys); - _remove_exif_keys(imgExifData, keys, n_keys); - } - - // Only compressed images may set PixelXDimension and PixelYDimension. - if(!compressed) - { - static const char *keys[] = { - "Exif.Photo.PixelXDimension", - "Exif.Photo.PixelYDimension" - }; - static const guint n_keys = G_N_ELEMENTS(keys); - _remove_exif_keys(imgExifData, keys, n_keys); - } - - imgExifData.sortByTag(); + _filter_exif_for_export(imgExifData, compressed); write_metadata_threadsafe(image); } catch(const Exiv2::AnyError &e) @@ -2692,6 +2698,50 @@ int dt_exif_write_blob(uint8_t *blob, return 1; } +int dt_exif_write_blob_to_buffer(uint8_t *input_blob, + uint32_t input_size, + uint8_t **output_blob, + const int compressed) +{ + *output_blob = NULL; + + try + { + // Decode input Exif blob into memory + Exiv2::ExifData exifData; + Exiv2::ExifParser::decode(exifData, input_blob, input_size); + + _filter_exif_for_export(exifData, compressed); + + // Encode to buffer + Exiv2::Blob blob; + Exiv2::ExifParser::encode(blob, Exiv2::bigEndian, exifData); + + const size_t output_size = blob.size(); + *output_blob = (uint8_t *)g_malloc(output_size); + if(!*output_blob) + { + dt_print(DT_DEBUG_IMAGEIO, "[exif] could not allocate output buffer of size %zu", output_size); + return 0; + } + + memcpy(*output_blob, blob.data(), output_size); + return (int)output_size; + } + catch(const Exiv2::AnyError &e) + { + dt_print(DT_DEBUG_IMAGEIO, + "[exiv2 dt_exif_write_blob_to_buffer] %s", + e.what()); + if(*output_blob) + { + g_free(*output_blob); + *output_blob = NULL; + } + return 0; + } +} + static void _remove_exif_geotag(Exiv2::ExifData &exifData) { static const char *keys[] = diff --git a/src/common/exif.h b/src/common/exif.h index 8ee779928dcd..8f968ce20444 100644 --- a/src/common/exif.h +++ b/src/common/exif.h @@ -95,6 +95,11 @@ void dt_exif_img_check_additional_tags(dt_image_t *img, const char *filename); /** 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); +/** write filtered exif blob to memory buffer (in-memory filtering, no temporary files). + * caller must free output_blob with g_free(). + * returns size of output buffer, or 0 on error. */ +int dt_exif_write_blob_to_buffer(uint8_t *input_blob, uint32_t input_size, uint8_t **output_blob, const int compressed); + /** write xmp sidecar file. */ /** if force_write is FALSE, the current contents of the sidecar file are compared against what would be written, and the write is skipped if they are the same. This preserves the sidecar diff --git a/src/imageio/format/jxl.c b/src/imageio/format/jxl.c index d36c3b2f0833..eba7d132b20a 100644 --- a/src/imageio/format/jxl.c +++ b/src/imageio/format/jxl.c @@ -309,8 +309,8 @@ 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 */ + // Embed metadata into JXL BMFF container + // Note: exif must be provided already filtered from imageio.c if(exif && exif_len > 0) { // Prepend the 4 byte (zero) offset to the blob before writing @@ -319,23 +319,7 @@ 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)); - } - - /* 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 doesn't support Brotli compressed boxes - LIBJXL_ASSERT(JxlEncoderAddBox(encoder, "xml ", - (const uint8_t *)xmp_string, xmp_len, JXL_FALSE)); - } + LIBJXL_ASSERT(JxlEncoderAddBox(encoder, "Exif", exif_buf, exif_len + 4, JXL_TRUE)); } JxlPixelFormat pixel_format = { 3, JXL_TYPE_FLOAT, JXL_NATIVE_ENDIAN, 0 }; @@ -434,14 +418,12 @@ int levels(dt_imageio_module_data_t *data) int flags(dt_imageio_module_data_t *data) { - /* - * As of exiv2 0.27.5 there is no write support for the JXL BMFF format, - * so we do not return the XMP supported flag currently. - * Once exiv2 write support is there, the flag can be returned, and the - * direct XMP embedding workaround using JxlEncoderAddBox("xml ") above - * can be removed. - */ - return 0; /* FORMAT_FLAGS_SUPPORT_XMP; */ +#if defined(EXV_ENABLE_BMFF) && defined(EXV_HAVE_BROTLI) + // exiv2 >= 0.28 with JXL BMFF format and Brotli compression + return FORMAT_FLAGS_SUPPORT_XMP; +#else + return 0; +#endif } static inline int _bpp_to_enum(int bpp) diff --git a/src/imageio/imageio.c b/src/imageio/imageio.c index 79f17cace549..311d63a6c9c8 100644 --- a/src/imageio/imageio.c +++ b/src/imageio/imageio.c @@ -1477,7 +1477,6 @@ gboolean dt_imageio_export_with_flags(const dt_imgid_t imgid, && (!strcmp(format->mime(NULL), "image/avif") || !strcmp(format->mime(NULL), "image/heif") || !strcmp(format->mime(NULL), "image/x-exr") - || !strcmp(format->mime(NULL), "image/jxl") || !strcmp(format->mime(NULL), "image/x-xcf"))) { const int32_t meta_all = @@ -1487,7 +1486,43 @@ gboolean dt_imageio_export_with_flags(const dt_imgid_t imgid, md_flags_set = metadata ? (metadata->flags & meta_all) == meta_all : FALSE; } - if(!ignore_exif && md_flags_set) + // Some formats expect to add metadata in the encoder instead of re-opening the file with exiv2. + gboolean md_encoder_adds_exif = !strcmp(format->mime(NULL), "image/jxl"); + + if (!ignore_exif && md_encoder_adds_exif) + { + 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); + + // Read unfiltered exif from source image + uint8_t *exif_profile0 = NULL; + const int length0 = dt_exif_read_blob(&exif_profile0, pathname, imgid, sRGB, + processed_width, processed_height, FALSE); + + // Filter metadata in-memory (no temporary files needed) + uint8_t *filtered_exif = NULL; + int filtered_exif_len = 0; + if(!ignore_exif && md_flags_set && exif_profile0 && length0 > 0) + { + filtered_exif_len = dt_exif_write_blob_to_buffer(exif_profile0, length0, &filtered_exif, 1); + } + free(exif_profile0); + + // write image with filtered metadata + res = (format->write_image(format_params, filename, outbuf, icc_type, + icc_filename, filtered_exif, filtered_exif_len, imgid, + num, total, &pipe, export_masks)) != 0; + + g_free(filtered_exif); + } + else if(!ignore_exif && md_flags_set) { uint8_t *exif_profile = NULL; // Exif data should be 65536 bytes // max, but if original size is