Skip to content

Commit 228b9f6

Browse files
committed
jxlload, jxlsave: support EXIF and XMP metadata
1 parent c09d144 commit 228b9f6

File tree

2 files changed

+273
-3
lines changed

2 files changed

+273
-3
lines changed

libvips/foreign/jxlload.c

Lines changed: 186 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,10 @@ typedef struct _VipsForeignLoadJxl {
9191
JxlPixelFormat format;
9292
size_t icc_size;
9393
uint8_t *icc_data;
94+
size_t exif_size;
95+
uint8_t *exif_data;
96+
size_t xmp_size;
97+
uint8_t *xmp_data;
9498

9599
/* Decompress state.
96100
*/
@@ -102,6 +106,11 @@ typedef struct _VipsForeignLoadJxl {
102106
uint8_t input_buffer[INPUT_BUFFER_SIZE];
103107
size_t bytes_in_buffer;
104108

109+
/* Pointers to fields where box size and box data should be written to
110+
*/
111+
size_t *box_size;
112+
uint8_t **box_data;
113+
105114
/* Number of errors reported during load -- use this to block load of
106115
* corrupted images.
107116
*/
@@ -133,6 +142,8 @@ vips_foreign_load_jxl_dispose(GObject *gobject)
133142
VIPS_FREEF(JxlThreadParallelRunnerDestroy, jxl->runner);
134143
VIPS_FREEF(JxlDecoderDestroy, jxl->decoder);
135144
VIPS_FREE(jxl->icc_data);
145+
VIPS_FREE(jxl->exif_data);
146+
VIPS_FREE(jxl->xmp_data);
136147
VIPS_UNREF(jxl->source);
137148

138149
G_OBJECT_CLASS(vips_foreign_load_jxl_parent_class)->dispose(gobject);
@@ -191,6 +202,46 @@ vips_foreign_load_jxl_get_flags(VipsForeignLoad *load)
191202
return VIPS_FOREIGN_PARTIAL;
192203
}
193204

205+
static int
206+
vips_foreign_load_jxl_set_box_buffer(VipsForeignLoadJxl *jxl)
207+
{
208+
if (!jxl->box_data || !jxl->box_size)
209+
return 0;
210+
211+
VipsObjectClass *class = VIPS_OBJECT_GET_CLASS(jxl);
212+
213+
uint8_t *new_data;
214+
size_t new_size;
215+
size_t box_size = *jxl->box_size;
216+
217+
new_size = box_size + INPUT_BUFFER_SIZE;
218+
new_data = g_try_realloc(*jxl->box_data, new_size);
219+
220+
if (!new_data) {
221+
vips_error(class->nickname, "%s", _("out of memory"));
222+
return -1;
223+
}
224+
225+
*jxl->box_data = new_data;
226+
227+
JxlDecoderSetBoxBuffer(jxl->decoder,
228+
new_data + box_size, INPUT_BUFFER_SIZE);
229+
230+
return 0;
231+
}
232+
233+
static int
234+
vips_foreign_load_jxl_release_box_buffer(VipsForeignLoadJxl *jxl)
235+
{
236+
if (!jxl->box_data || !jxl->box_size)
237+
return 0;
238+
239+
size_t remaining = JxlDecoderReleaseBoxBuffer(jxl->decoder);
240+
*jxl->box_size += INPUT_BUFFER_SIZE - remaining;
241+
242+
return 0;
243+
}
244+
194245
static int
195246
vips_foreign_load_jxl_fill_input(VipsForeignLoadJxl *jxl,
196247
size_t bytes_remaining)
@@ -255,6 +306,10 @@ vips_foreign_load_jxl_print_status(JxlDecoderStatus status)
255306
printf("JXL_DEC_JPEG_NEED_MORE_OUTPUT\n");
256307
break;
257308

309+
case JXL_DEC_BOX_NEED_MORE_OUTPUT:
310+
printf("JXL_DEC_BOX_NEED_MORE_OUTPUT\n");
311+
break;
312+
258313
case JXL_DEC_BASIC_INFO:
259314
printf("JXL_DEC_BASIC_INFO\n");
260315
break;
@@ -287,6 +342,10 @@ vips_foreign_load_jxl_print_status(JxlDecoderStatus status)
287342
printf("JXL_DEC_JPEG_RECONSTRUCTION\n");
288343
break;
289344

345+
case JXL_DEC_BOX:
346+
printf("JXL_DEC_BOX\n");
347+
break;
348+
290349
default:
291350
printf("JXL_DEC_<unknown>\n");
292351
break;
@@ -390,6 +449,55 @@ vips_foreign_load_jxl_process(VipsForeignLoadJxl *jxl)
390449
return status;
391450
}
392451

452+
/* JPEG XL stores EXIF data without leading "Exif\0\0" with offset
453+
*/
454+
static int
455+
vips_foreign_load_jxl_fix_exif(VipsForeignLoadJxl *jxl)
456+
{
457+
VipsObjectClass *class = VIPS_OBJECT_GET_CLASS(jxl);
458+
459+
if (!jxl->exif_data || vips_isprefix("Exif", (char *) jxl->exif_data))
460+
return 0;
461+
462+
size_t old_size = jxl->exif_size;
463+
uint8_t *old_data = jxl->exif_data;
464+
size_t new_size = 0;
465+
uint8_t *new_data = NULL;
466+
467+
if (old_size >= 4) {
468+
/* Offset is stored in big-endian
469+
*/
470+
size_t offset = (old_data[0] << 3) + (old_data[1] << 2) +
471+
(old_data[2] << 1) + old_data[3];
472+
473+
if (offset < old_size - 4) {
474+
old_data += 4 + offset;
475+
old_size -= 4 + offset;
476+
477+
new_size = old_size + 6;
478+
new_data = g_malloc0(new_size);
479+
480+
if (!new_data) {
481+
vips_error(class->nickname, "%s", _("out of memory"));
482+
return -1;
483+
}
484+
485+
memcpy(new_data, "Exif\0\0", 6);
486+
memcpy(new_data + 6, old_data, old_size);
487+
}
488+
}
489+
490+
if (!new_data)
491+
g_warning("%s: invalid data in EXIF box", class->nickname);
492+
493+
g_free(jxl->exif_data);
494+
495+
jxl->exif_data = new_data;
496+
jxl->exif_size = new_size;
497+
498+
return 0;
499+
}
500+
393501
static int
394502
vips_foreign_load_jxl_set_header(VipsForeignLoadJxl *jxl, VipsImage *out)
395503
{
@@ -484,6 +592,24 @@ vips_foreign_load_jxl_set_header(VipsForeignLoadJxl *jxl, VipsImage *out)
484592
jxl->icc_size = 0;
485593
}
486594

595+
if (jxl->exif_data &&
596+
jxl->exif_size > 0) {
597+
vips_image_set_blob(out, VIPS_META_EXIF_NAME,
598+
(VipsCallbackFn) vips_area_free_cb,
599+
jxl->exif_data, jxl->exif_size);
600+
jxl->exif_data = NULL;
601+
jxl->exif_size = 0;
602+
}
603+
604+
if (jxl->xmp_data &&
605+
jxl->xmp_size > 0) {
606+
vips_image_set_blob(out, VIPS_META_XMP_NAME,
607+
(VipsCallbackFn) vips_area_free_cb,
608+
jxl->xmp_data, jxl->xmp_size);
609+
jxl->xmp_data = NULL;
610+
jxl->xmp_size = 0;
611+
}
612+
487613
vips_image_set_int(out,
488614
VIPS_META_ORIENTATION, jxl->info.orientation);
489615

@@ -499,6 +625,7 @@ vips_foreign_load_jxl_header(VipsForeignLoad *load)
499625
VipsForeignLoadJxl *jxl = (VipsForeignLoadJxl *) load;
500626

501627
JxlDecoderStatus status;
628+
JXL_BOOL decompress_boxes = JXL_TRUE;
502629

503630
#ifdef DEBUG
504631
printf("vips_foreign_load_jxl_header:\n");
@@ -510,12 +637,17 @@ vips_foreign_load_jxl_header(VipsForeignLoad *load)
510637
JxlDecoderRewind(jxl->decoder);
511638
if (JxlDecoderSubscribeEvents(jxl->decoder,
512639
JXL_DEC_COLOR_ENCODING |
513-
JXL_DEC_BASIC_INFO)) {
640+
JXL_DEC_BASIC_INFO |
641+
JXL_DEC_BOX |
642+
JXL_DEC_FRAME)) {
514643
vips_foreign_load_jxl_error(jxl,
515644
"JxlDecoderSubscribeEvents");
516645
return -1;
517646
}
518647

648+
if (JxlDecoderSetDecompressBoxes(jxl->decoder, JXL_TRUE) != JXL_DEC_SUCCESS)
649+
decompress_boxes = JXL_FALSE;
650+
519651
if (vips_foreign_load_jxl_fill_input(jxl, 0))
520652
return -1;
521653
JxlDecoderSetInput(jxl->decoder,
@@ -530,6 +662,49 @@ vips_foreign_load_jxl_header(VipsForeignLoad *load)
530662
"JxlDecoderProcessInput");
531663
return -1;
532664

665+
case JXL_DEC_BOX:
666+
/* Flush previous box data if any
667+
*/
668+
if (vips_foreign_load_jxl_release_box_buffer(jxl))
669+
return -1;
670+
671+
JxlBoxType type;
672+
if (JxlDecoderGetBoxType(
673+
jxl->decoder, type, decompress_boxes) != JXL_DEC_SUCCESS) {
674+
vips_foreign_load_jxl_error(jxl, "JxlDecoderGetBoxType");
675+
return -1;
676+
}
677+
678+
#ifdef DEBUG
679+
const char type_s[] = { type[0], type[1], type[2], type[3], 0 };
680+
printf("vips_foreign_load_jxl_header found box %s\n", type_s);
681+
#endif /*DEBUG*/
682+
683+
if (!memcmp(type, "Exif", 4)) {
684+
jxl->box_size = &jxl->exif_size;
685+
jxl->box_data = &jxl->exif_data;
686+
}
687+
else if (!memcmp(type, "xml ", 4)) {
688+
jxl->box_size = &jxl->xmp_size;
689+
jxl->box_data = &jxl->xmp_data;
690+
}
691+
else {
692+
jxl->box_size = NULL;
693+
jxl->box_data = NULL;
694+
}
695+
696+
if (vips_foreign_load_jxl_set_box_buffer(jxl))
697+
return -1;
698+
699+
break;
700+
701+
case JXL_DEC_BOX_NEED_MORE_OUTPUT:
702+
if (vips_foreign_load_jxl_release_box_buffer(jxl) ||
703+
vips_foreign_load_jxl_set_box_buffer(jxl))
704+
return -1;
705+
706+
break;
707+
533708
case JXL_DEC_BASIC_INFO:
534709
if (JxlDecoderGetBasicInfo(jxl->decoder,
535710
&jxl->info)) {
@@ -595,10 +770,18 @@ vips_foreign_load_jxl_header(VipsForeignLoad *load)
595770
default:
596771
break;
597772
}
598-
/* JXL_DEC_COLOR_ENCODING is always the last status signal before
773+
/* JXL_DEC_FRAME is always the last status signal before
599774
* pixel decoding starts.
600775
*/
601-
} while (status != JXL_DEC_COLOR_ENCODING);
776+
} while (status != JXL_DEC_FRAME);
777+
778+
/* Flush box data if any
779+
*/
780+
if (vips_foreign_load_jxl_release_box_buffer(jxl))
781+
return -1;
782+
783+
if (vips_foreign_load_jxl_fix_exif(jxl))
784+
return -1;
602785

603786
if (vips_foreign_load_jxl_set_header(jxl, load->out))
604787
return -1;

libvips/foreign/jxlsave.c

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,18 @@ typedef VipsForeignSaveClass VipsForeignSaveJxlClass;
107107
G_DEFINE_ABSTRACT_TYPE(VipsForeignSaveJxl, vips_foreign_save_jxl,
108108
VIPS_TYPE_FOREIGN_SAVE);
109109

110+
/* String-based metadata fields we add.
111+
*/
112+
typedef struct _VipsForeignSaveJxlMetadata {
113+
const char *name; /* as understood by libvips */
114+
JxlBoxType box_type; /* as understood by libjxl */
115+
} VipsForeignSaveJxlMetadata;
116+
117+
static VipsForeignSaveJxlMetadata libjxl_metadata[] = {
118+
{ VIPS_META_EXIF_NAME, "Exif" },
119+
{ VIPS_META_XMP_NAME, "xml " }
120+
};
121+
110122
static void
111123
vips_foreign_save_jxl_dispose(GObject *gobject)
112124
{
@@ -221,6 +233,78 @@ vips_foreign_save_jxl_print_status(JxlEncoderStatus status)
221233
}
222234
#endif /*DEBUG*/
223235

236+
static int
237+
vips_foreign_save_jxl_add_metadata(VipsForeignSaveJxl *jxl, VipsImage *in)
238+
{
239+
#ifdef HAVE_LIBJXL_0_7
240+
VipsObjectClass *class = VIPS_OBJECT_GET_CLASS(jxl);
241+
int i;
242+
243+
for (i = 0; i < VIPS_NUMBER(libjxl_metadata); i++)
244+
if (vips_image_get_typeof(in, libjxl_metadata[i].name)) {
245+
uint8_t *data;
246+
size_t length;
247+
248+
#ifdef DEBUG
249+
printf("attaching %s ..\n", libjxl_metadata[i].name);
250+
#endif /*DEBUG*/
251+
252+
if (vips_image_get_blob(in,
253+
libjxl_metadata[i].name, (const void **) &data, &length))
254+
return -1;
255+
256+
/* It's safe to call JxlEncoderUseBoxes multiple times
257+
*/
258+
if (JxlEncoderUseBoxes(jxl->encoder) != JXL_ENC_SUCCESS) {
259+
vips_foreign_save_jxl_error(jxl, "JxlEncoderUseBoxes");
260+
return -1;
261+
}
262+
263+
/* JPEG XL stores EXIF data without leading "Exif\0\0" with offset
264+
*/
265+
if (!strcmp(libjxl_metadata[i].name, VIPS_META_EXIF_NAME)) {
266+
if (length >= 6 && vips_isprefix("Exif", (char *) data)) {
267+
data = data + 6;
268+
length -= 6;
269+
}
270+
271+
size_t exif_size = length + 4;
272+
uint8_t *exif_data = g_malloc0(exif_size);
273+
274+
if (!exif_data) {
275+
vips_error(class->nickname, "%s", _("out of memory"));
276+
return -1;
277+
}
278+
279+
/* The first 4 bytes is offset which is 0 in this case
280+
*/
281+
memcpy(exif_data + 4, data, length);
282+
283+
if (JxlEncoderAddBox(jxl->encoder, libjxl_metadata[i].box_type,
284+
exif_data, exif_size, JXL_TRUE) != JXL_ENC_SUCCESS) {
285+
vips_foreign_save_jxl_error(jxl, "JxlEncoderAddBox");
286+
return -1;
287+
}
288+
289+
g_free(exif_data);
290+
}
291+
else {
292+
if (JxlEncoderAddBox(jxl->encoder, libjxl_metadata[i].box_type,
293+
data, length, JXL_TRUE) != JXL_ENC_SUCCESS) {
294+
vips_foreign_save_jxl_error(jxl, "JxlEncoderAddBox");
295+
return -1;
296+
}
297+
}
298+
}
299+
300+
/* It's safe to call JxlEncoderCloseBoxes even if we don't use boxes
301+
*/
302+
JxlEncoderCloseBoxes(jxl->encoder);
303+
#endif /*HAVE_LIBJXL_0_7*/
304+
305+
return 0;
306+
}
307+
224308
static int
225309
vips_foreign_save_jxl_build(VipsObject *object)
226310
{
@@ -409,6 +493,9 @@ vips_foreign_save_jxl_build(VipsObject *object)
409493
}
410494
}
411495

496+
if (vips_foreign_save_jxl_add_metadata(jxl, in))
497+
return -1;
498+
412499
/* Render the entire image in memory. libjxl seems to be missing
413500
* tile-based write at the moment.
414501
*/

0 commit comments

Comments
 (0)