Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 76 additions & 26 deletions src/common/exif.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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)
Expand All @@ -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[] =
Expand Down
5 changes: 5 additions & 0 deletions src/common/exif.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
36 changes: 9 additions & 27 deletions src/imageio/format/jxl.c
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 };
Expand Down Expand Up @@ -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)
Expand Down
39 changes: 37 additions & 2 deletions src/imageio/imageio.c
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand 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
Expand Down