/src/libavif/apps/shared/avifjpeg.c
Line | Count | Source |
1 | | // Copyright 2020 Joe Drago. All rights reserved. |
2 | | // SPDX-License-Identifier: BSD-2-Clause |
3 | | |
4 | | #include "avifjpeg.h" |
5 | | #include "avifexif.h" |
6 | | #include "avifutil.h" |
7 | | |
8 | | #include <assert.h> |
9 | | #include <ctype.h> |
10 | | #include <math.h> |
11 | | #include <setjmp.h> |
12 | | #include <stdint.h> |
13 | | #include <stdio.h> |
14 | | #include <stdlib.h> |
15 | | #include <string.h> |
16 | | |
17 | | #include "jpeglib.h" |
18 | | |
19 | | #include "iccjpeg.h" |
20 | | |
21 | | #if defined(AVIF_ENABLE_JPEG_GAIN_MAP_CONVERSION) |
22 | | #include <libxml/parser.h> |
23 | | #endif |
24 | | |
25 | 120k | #define AVIF_MIN(a, b) (((a) < (b)) ? (a) : (b)) |
26 | 381 | #define AVIF_MAX(a, b) (((a) > (b)) ? (a) : (b)) |
27 | | |
28 | | struct my_error_mgr |
29 | | { |
30 | | struct jpeg_error_mgr pub; |
31 | | jmp_buf setjmp_buffer; |
32 | | }; |
33 | | typedef struct my_error_mgr * my_error_ptr; |
34 | | static void my_error_exit(j_common_ptr cinfo) |
35 | 2.70k | { |
36 | 2.70k | my_error_ptr myerr = (my_error_ptr)cinfo->err; |
37 | 2.70k | (*cinfo->err->output_message)(cinfo); |
38 | 2.70k | longjmp(myerr->setjmp_buffer, 1); |
39 | 2.70k | } |
40 | | |
41 | | #if JPEG_LIB_VERSION >= 70 |
42 | | #define AVIF_LIBJPEG_DCT_v_scaled_size DCT_v_scaled_size |
43 | | #define AVIF_LIBJPEG_DCT_h_scaled_size DCT_h_scaled_size |
44 | | #else |
45 | 381 | #define AVIF_LIBJPEG_DCT_h_scaled_size DCT_scaled_size |
46 | 381 | #define AVIF_LIBJPEG_DCT_v_scaled_size DCT_scaled_size |
47 | | #endif |
48 | | |
49 | | // An internal function used by avifJPEGReadCopy(), this is the shared libjpeg decompression code |
50 | | // for all paths avifJPEGReadCopy() takes. |
51 | | static avifBool avifJPEGCopyPixels(avifImage * avif, uint32_t sizeLimit, struct jpeg_decompress_struct * cinfo) |
52 | 152 | { |
53 | 152 | cinfo->raw_data_out = TRUE; |
54 | 152 | jpeg_start_decompress(cinfo); |
55 | | |
56 | 152 | avif->width = cinfo->image_width; |
57 | 152 | avif->height = cinfo->image_height; |
58 | 152 | if (avif->width > sizeLimit / avif->height) { |
59 | 0 | return AVIF_FALSE; |
60 | 0 | } |
61 | | |
62 | 152 | JSAMPIMAGE buffer = (*cinfo->mem->alloc_small)((j_common_ptr)cinfo, JPOOL_IMAGE, sizeof(JSAMPARRAY) * cinfo->num_components); |
63 | | |
64 | | // lines of output image to be read per jpeg_read_raw_data call |
65 | 152 | int readLines = 0; |
66 | | // lines of samples to be read per call (for each channel) |
67 | 152 | int linesPerCall[3] = { 0, 0, 0 }; |
68 | | // expected count of sample lines (for each channel) |
69 | 152 | int targetRead[3] = { 0, 0, 0 }; |
70 | 533 | for (int i = 0; i < cinfo->num_components; ++i) { |
71 | 381 | jpeg_component_info * comp = &cinfo->comp_info[i]; |
72 | | |
73 | 381 | linesPerCall[i] = comp->v_samp_factor * comp->AVIF_LIBJPEG_DCT_v_scaled_size; |
74 | 381 | targetRead[i] = comp->downsampled_height; |
75 | 381 | buffer[i] = (*cinfo->mem->alloc_sarray)((j_common_ptr)cinfo, |
76 | 381 | JPOOL_IMAGE, |
77 | 381 | comp->width_in_blocks * comp->AVIF_LIBJPEG_DCT_h_scaled_size, |
78 | 381 | linesPerCall[i]); |
79 | 381 | readLines = AVIF_MAX(readLines, linesPerCall[i]); |
80 | 381 | } |
81 | | |
82 | 152 | avifImageFreePlanes(avif, AVIF_PLANES_ALL); // Free planes in case they were already allocated. |
83 | 152 | if (avifImageAllocatePlanes(avif, AVIF_PLANES_YUV) != AVIF_RESULT_OK) { |
84 | 0 | return AVIF_FALSE; |
85 | 0 | } |
86 | | |
87 | | // destination avif channel for each jpeg channel |
88 | 152 | avifChannelIndex targetChannel[3] = { AVIF_CHAN_Y, AVIF_CHAN_Y, AVIF_CHAN_Y }; |
89 | 152 | if (cinfo->jpeg_color_space == JCS_YCbCr) { |
90 | 118 | targetChannel[0] = AVIF_CHAN_Y; |
91 | 118 | targetChannel[1] = AVIF_CHAN_U; |
92 | 118 | targetChannel[2] = AVIF_CHAN_V; |
93 | 118 | } else if (cinfo->jpeg_color_space == JCS_GRAYSCALE) { |
94 | 3 | targetChannel[0] = AVIF_CHAN_Y; |
95 | 31 | } else { |
96 | | // cinfo->jpeg_color_space == JCS_RGB |
97 | 31 | targetChannel[0] = AVIF_CHAN_V; |
98 | 31 | targetChannel[1] = AVIF_CHAN_Y; |
99 | 31 | targetChannel[2] = AVIF_CHAN_U; |
100 | 31 | } |
101 | | |
102 | 152 | int workComponents = avif->yuvFormat == AVIF_PIXEL_FORMAT_YUV400 ? 1 : cinfo->num_components; |
103 | | |
104 | | // count of already-read lines (for each channel) |
105 | 152 | int alreadyRead[3] = { 0, 0, 0 }; |
106 | 81.3k | while (cinfo->output_scanline < cinfo->output_height) { |
107 | 81.1k | jpeg_read_raw_data(cinfo, buffer, readLines); |
108 | | |
109 | 202k | for (int i = 0; i < workComponents; ++i) { |
110 | 120k | int linesRead = AVIF_MIN(targetRead[i] - alreadyRead[i], linesPerCall[i]); |
111 | 1.49M | for (int j = 0; j < linesRead; ++j) { |
112 | 1.37M | memcpy(&avif->yuvPlanes[targetChannel[i]][avif->yuvRowBytes[targetChannel[i]] * (alreadyRead[i] + j)], |
113 | 1.37M | buffer[i][j], |
114 | 1.37M | avif->yuvRowBytes[targetChannel[i]]); |
115 | 1.37M | } |
116 | 120k | alreadyRead[i] += linesPerCall[i]; |
117 | 120k | } |
118 | 81.1k | } |
119 | 152 | return AVIF_TRUE; |
120 | 152 | } |
121 | | |
122 | | static avifBool avifJPEGHasCompatibleMatrixCoefficients(avifMatrixCoefficients matrixCoefficients) |
123 | 1.38k | { |
124 | 1.38k | switch (matrixCoefficients) { |
125 | 302 | case AVIF_MATRIX_COEFFICIENTS_BT470BG: |
126 | 580 | case AVIF_MATRIX_COEFFICIENTS_BT601: |
127 | | // JPEG always uses [Kr:0.299, Kb:0.114], which matches these MCs. |
128 | 580 | return AVIF_TRUE; |
129 | 1.38k | } |
130 | 801 | return AVIF_FALSE; |
131 | 1.38k | } |
132 | | |
133 | | // This attempts to copy the internal representation of the JPEG directly into avifImage without |
134 | | // YUV->RGB conversion. If it returns AVIF_FALSE, a typical RGB->YUV conversion is required. |
135 | | static avifBool avifJPEGReadCopy(avifImage * avif, uint32_t sizeLimit, struct jpeg_decompress_struct * cinfo) |
136 | 3.91k | { |
137 | 3.91k | if ((avif->depth != 8) || (avif->yuvRange != AVIF_RANGE_FULL)) { |
138 | 1.65k | return AVIF_FALSE; |
139 | 1.65k | } |
140 | | |
141 | 2.26k | if (cinfo->jpeg_color_space == JCS_YCbCr) { |
142 | | // Import from YUV: must use compatible matrixCoefficients. |
143 | 1.36k | if (avifJPEGHasCompatibleMatrixCoefficients(avif->matrixCoefficients)) { |
144 | | // YUV->YUV: require precise match for pixel format. |
145 | 577 | avifPixelFormat jpegFormat = AVIF_PIXEL_FORMAT_NONE; |
146 | 577 | if (cinfo->comp_info[0].h_samp_factor == 1 && cinfo->comp_info[0].v_samp_factor == 1 && |
147 | 127 | cinfo->comp_info[1].h_samp_factor == 1 && cinfo->comp_info[1].v_samp_factor == 1 && |
148 | 94 | cinfo->comp_info[2].h_samp_factor == 1 && cinfo->comp_info[2].v_samp_factor == 1) { |
149 | 78 | jpegFormat = AVIF_PIXEL_FORMAT_YUV444; |
150 | 499 | } else if (cinfo->comp_info[0].h_samp_factor == 2 && cinfo->comp_info[0].v_samp_factor == 1 && |
151 | 103 | cinfo->comp_info[1].h_samp_factor == 1 && cinfo->comp_info[1].v_samp_factor == 1 && |
152 | 85 | cinfo->comp_info[2].h_samp_factor == 1 && cinfo->comp_info[2].v_samp_factor == 1) { |
153 | 14 | jpegFormat = AVIF_PIXEL_FORMAT_YUV422; |
154 | 485 | } else if (cinfo->comp_info[0].h_samp_factor == 2 && cinfo->comp_info[0].v_samp_factor == 2 && |
155 | 119 | cinfo->comp_info[1].h_samp_factor == 1 && cinfo->comp_info[1].v_samp_factor == 1 && |
156 | 81 | cinfo->comp_info[2].h_samp_factor == 1 && cinfo->comp_info[2].v_samp_factor == 1) { |
157 | 64 | jpegFormat = AVIF_PIXEL_FORMAT_YUV420; |
158 | 64 | } |
159 | 577 | if (jpegFormat != AVIF_PIXEL_FORMAT_NONE) { |
160 | 156 | if (avif->yuvFormat == AVIF_PIXEL_FORMAT_NONE) { |
161 | | // The requested format is "auto": Adopt JPEG's internal format. |
162 | 0 | avif->yuvFormat = jpegFormat; |
163 | 0 | } |
164 | 156 | if (avif->yuvFormat == jpegFormat) { |
165 | 46 | cinfo->out_color_space = JCS_YCbCr; |
166 | 46 | return avifJPEGCopyPixels(avif, sizeLimit, cinfo); |
167 | 46 | } |
168 | 156 | } |
169 | | |
170 | | // YUV->Grayscale: subsample Y plane not allowed. |
171 | 531 | if ((avif->yuvFormat == AVIF_PIXEL_FORMAT_YUV400) && (cinfo->comp_info[0].h_samp_factor == cinfo->max_h_samp_factor && |
172 | 134 | cinfo->comp_info[0].v_samp_factor == cinfo->max_v_samp_factor)) { |
173 | 89 | cinfo->out_color_space = JCS_YCbCr; |
174 | 89 | return avifJPEGCopyPixels(avif, sizeLimit, cinfo); |
175 | 89 | } |
176 | 531 | } |
177 | 1.36k | } else if (cinfo->jpeg_color_space == JCS_GRAYSCALE) { |
178 | | // Import from Grayscale: subsample not allowed. |
179 | 12 | if ((cinfo->comp_info[0].h_samp_factor == cinfo->max_h_samp_factor && |
180 | 12 | cinfo->comp_info[0].v_samp_factor == cinfo->max_v_samp_factor)) { |
181 | | // Import to YUV/Grayscale: must use compatible matrixCoefficients. |
182 | 12 | if (avifJPEGHasCompatibleMatrixCoefficients(avif->matrixCoefficients) || |
183 | 9 | avif->matrixCoefficients == AVIF_MATRIX_COEFFICIENTS_UNSPECIFIED) { |
184 | | // Grayscale->Grayscale: direct copy. |
185 | 6 | if ((avif->yuvFormat == AVIF_PIXEL_FORMAT_YUV400) || (avif->yuvFormat == AVIF_PIXEL_FORMAT_NONE)) { |
186 | 1 | avif->yuvFormat = AVIF_PIXEL_FORMAT_YUV400; |
187 | 1 | cinfo->out_color_space = JCS_GRAYSCALE; |
188 | 1 | return avifJPEGCopyPixels(avif, sizeLimit, cinfo); |
189 | 1 | } |
190 | | |
191 | | // Grayscale->YUV: copy Y, fill UV with monochrome value. |
192 | 5 | if ((avif->yuvFormat == AVIF_PIXEL_FORMAT_YUV444) || (avif->yuvFormat == AVIF_PIXEL_FORMAT_YUV422) || |
193 | 5 | (avif->yuvFormat == AVIF_PIXEL_FORMAT_YUV420)) { |
194 | 5 | cinfo->out_color_space = JCS_GRAYSCALE; |
195 | 5 | if (!avifJPEGCopyPixels(avif, sizeLimit, cinfo)) { |
196 | 0 | return AVIF_FALSE; |
197 | 0 | } |
198 | | |
199 | 5 | uint32_t uvHeight = avifImagePlaneHeight(avif, AVIF_CHAN_U); |
200 | 5 | memset(avif->yuvPlanes[AVIF_CHAN_U], 128, (size_t)avif->yuvRowBytes[AVIF_CHAN_U] * uvHeight); |
201 | 5 | memset(avif->yuvPlanes[AVIF_CHAN_V], 128, (size_t)avif->yuvRowBytes[AVIF_CHAN_V] * uvHeight); |
202 | | |
203 | 5 | return AVIF_TRUE; |
204 | 5 | } |
205 | 5 | } |
206 | | |
207 | | // Grayscale->RGB: copy Y to G, duplicate to B and R. |
208 | 6 | if ((avif->matrixCoefficients == AVIF_MATRIX_COEFFICIENTS_IDENTITY) && |
209 | 4 | ((avif->yuvFormat == AVIF_PIXEL_FORMAT_YUV444) || (avif->yuvFormat == AVIF_PIXEL_FORMAT_NONE))) { |
210 | 2 | avif->yuvFormat = AVIF_PIXEL_FORMAT_YUV444; |
211 | 2 | cinfo->out_color_space = JCS_GRAYSCALE; |
212 | 2 | if (!avifJPEGCopyPixels(avif, sizeLimit, cinfo)) { |
213 | 0 | return AVIF_FALSE; |
214 | 0 | } |
215 | | |
216 | 2 | memcpy(avif->yuvPlanes[AVIF_CHAN_U], avif->yuvPlanes[AVIF_CHAN_Y], (size_t)avif->yuvRowBytes[AVIF_CHAN_U] * avif->height); |
217 | 2 | memcpy(avif->yuvPlanes[AVIF_CHAN_V], avif->yuvPlanes[AVIF_CHAN_Y], (size_t)avif->yuvRowBytes[AVIF_CHAN_V] * avif->height); |
218 | | |
219 | 2 | return AVIF_TRUE; |
220 | 2 | } |
221 | 6 | } |
222 | 881 | } else if (cinfo->jpeg_color_space == JCS_RGB) { |
223 | | // RGB->RGB: subsample not allowed. |
224 | 874 | if ((avif->matrixCoefficients == AVIF_MATRIX_COEFFICIENTS_IDENTITY) && |
225 | 166 | ((avif->yuvFormat == AVIF_PIXEL_FORMAT_YUV444) || (avif->yuvFormat == AVIF_PIXEL_FORMAT_NONE)) && |
226 | 100 | (cinfo->comp_info[0].h_samp_factor == 1 && cinfo->comp_info[0].v_samp_factor == 1 && |
227 | 32 | cinfo->comp_info[1].h_samp_factor == 1 && cinfo->comp_info[1].v_samp_factor == 1 && |
228 | 15 | cinfo->comp_info[2].h_samp_factor == 1 && cinfo->comp_info[2].v_samp_factor == 1)) { |
229 | 9 | avif->yuvFormat = AVIF_PIXEL_FORMAT_YUV444; |
230 | 9 | cinfo->out_color_space = JCS_RGB; |
231 | 9 | return avifJPEGCopyPixels(avif, sizeLimit, cinfo); |
232 | 9 | } |
233 | 874 | } |
234 | | |
235 | | // A typical RGB->YUV conversion is required. |
236 | 2.11k | return AVIF_FALSE; |
237 | 2.26k | } |
238 | | |
239 | | // Reads a 4-byte unsigned integer in big-endian format from the raw bitstream src. |
240 | | static uint32_t avifJPEGReadUint32BigEndian(const uint8_t * src) |
241 | 114 | { |
242 | 114 | return ((uint32_t)src[0] << 24) | ((uint32_t)src[1] << 16) | ((uint32_t)src[2] << 8) | ((uint32_t)src[3] << 0); |
243 | 114 | } |
244 | | |
245 | | // Returns the pointer in str to the first occurrence of substr. Returns NULL if substr cannot be found in str. |
246 | | static const uint8_t * avifJPEGFindSubstr(const uint8_t * str, size_t strLength, const uint8_t * substr, size_t substrLength) |
247 | 24 | { |
248 | 60.1k | for (size_t index = 0; index + substrLength <= strLength; ++index) { |
249 | 60.1k | if (!memcmp(&str[index], substr, substrLength)) { |
250 | 11 | return &str[index]; |
251 | 11 | } |
252 | 60.1k | } |
253 | 13 | return NULL; |
254 | 24 | } |
255 | | |
256 | 0 | #define AVIF_JPEG_MAX_MARKER_DATA_LENGTH 65533 |
257 | | |
258 | | // Exif tag |
259 | 777 | #define AVIF_JPEG_EXIF_HEADER "Exif\0\0" |
260 | 2.08k | #define AVIF_JPEG_EXIF_HEADER_LENGTH 6 |
261 | | |
262 | | // XMP tags |
263 | 818 | #define AVIF_JPEG_STANDARD_XMP_TAG "http://ns.adobe.com/xap/1.0/\0" |
264 | 1.84k | #define AVIF_JPEG_STANDARD_XMP_TAG_LENGTH 29 |
265 | 778 | #define AVIF_JPEG_EXTENDED_XMP_TAG "http://ns.adobe.com/xmp/extension/\0" |
266 | 2.00k | #define AVIF_JPEG_EXTENDED_XMP_TAG_LENGTH 35 |
267 | | |
268 | | // MPF tag (Multi-Picture Format) |
269 | | #define AVIF_JPEG_MPF_HEADER "MPF\0" |
270 | | #define AVIF_JPEG_MPF_HEADER_LENGTH 4 |
271 | | |
272 | | // One way of storing the Extended XMP GUID (generated by a camera for example). |
273 | 16 | #define AVIF_JPEG_XMP_NOTE_TAG "xmpNote:HasExtendedXMP=\"" |
274 | 32 | #define AVIF_JPEG_XMP_NOTE_TAG_LENGTH 24 |
275 | | // Another way of storing the Extended XMP GUID (generated by exiftool for example). |
276 | 8 | #define AVIF_JPEG_ALTERNATIVE_XMP_NOTE_TAG "<xmpNote:HasExtendedXMP>" |
277 | 16 | #define AVIF_JPEG_ALTERNATIVE_XMP_NOTE_TAG_LENGTH 24 |
278 | | |
279 | 2.79k | #define AVIF_JPEG_EXTENDED_XMP_GUID_LENGTH 32 |
280 | | |
281 | | // Offset in APP1 segment (skip tag + guid + size + offset). |
282 | 183 | #define AVIF_JPEG_OFFSET_TILL_EXTENDED_XMP (AVIF_JPEG_EXTENDED_XMP_TAG_LENGTH + AVIF_JPEG_EXTENDED_XMP_GUID_LENGTH + 4 + 4) |
283 | | |
284 | | #define AVIF_CHECK(A) \ |
285 | | do { \ |
286 | | if (!(A)) \ |
287 | | return AVIF_FALSE; \ |
288 | | } while (0) |
289 | | |
290 | | #if defined(AVIF_ENABLE_JPEG_GAIN_MAP_CONVERSION) |
291 | | |
292 | | // Reads a 4-byte unsigned integer in little-endian format from the raw bitstream src. |
293 | | static uint32_t avifJPEGReadUint32LittleEndian(const uint8_t * src) |
294 | | { |
295 | | return ((uint32_t)src[0] << 0) | ((uint32_t)src[1] << 8) | ((uint32_t)src[2] << 16) | ((uint32_t)src[3] << 24); |
296 | | } |
297 | | |
298 | | // Reads a 2-byte unsigned integer in big-endian format from the raw bitstream src. |
299 | | static uint16_t avifJPEGReadUint16BigEndian(const uint8_t * src) |
300 | | { |
301 | | return (uint16_t)((src[0] << 8) | (src[1] << 0)); |
302 | | } |
303 | | |
304 | | // Reads a 2-byte unsigned integer in little-endian format from the raw bitstream src. |
305 | | static uint16_t avifJPEGReadUint16LittleEndian(const uint8_t * src) |
306 | | { |
307 | | return (uint16_t)((src[0] << 0) | (src[1] << 8)); |
308 | | } |
309 | | |
310 | | // Reads 'numBytes' at 'offset', stores them in 'bytes' and increases 'offset'. |
311 | | static avifBool avifJPEGReadBytes(const avifROData * data, uint8_t * bytes, uint32_t * offset, uint32_t numBytes) |
312 | | { |
313 | | if ((UINT32_MAX - *offset) < numBytes || data->size < (*offset + numBytes)) { |
314 | | return AVIF_FALSE; |
315 | | } |
316 | | memcpy(bytes, &data->data[*offset], numBytes); |
317 | | *offset += numBytes; |
318 | | return AVIF_TRUE; |
319 | | } |
320 | | |
321 | | static avifBool avifJPEGReadU32(const avifROData * data, uint32_t * v, uint32_t * offset, avifBool isBigEndian) |
322 | | { |
323 | | uint8_t bytes[4]; |
324 | | AVIF_CHECK(avifJPEGReadBytes(data, bytes, offset, 4)); |
325 | | *v = isBigEndian ? avifJPEGReadUint32BigEndian(bytes) : avifJPEGReadUint32LittleEndian(bytes); |
326 | | return AVIF_TRUE; |
327 | | } |
328 | | |
329 | | static avifBool avifJPEGReadU16(const avifROData * data, uint16_t * v, uint32_t * offset, avifBool isBigEndian) |
330 | | { |
331 | | uint8_t bytes[2]; |
332 | | AVIF_CHECK(avifJPEGReadBytes(data, bytes, offset, 2)); |
333 | | *v = isBigEndian ? avifJPEGReadUint16BigEndian(bytes) : avifJPEGReadUint16LittleEndian(bytes); |
334 | | return AVIF_TRUE; |
335 | | } |
336 | | |
337 | | static avifBool avifJPEGReadInternal(FILE * f, |
338 | | const char * inputFilename, |
339 | | avifImage * avif, |
340 | | avifPixelFormat requestedFormat, |
341 | | uint32_t requestedDepth, |
342 | | avifChromaDownsampling chromaDownsampling, |
343 | | avifBool ignoreColorProfile, |
344 | | avifBool ignoreExif, |
345 | | avifBool ignoreXMP, |
346 | | avifBool ignoreGainMap, |
347 | | uint32_t sizeLimit); |
348 | | |
349 | | // Arbitrary max number of jpeg segments to parse before giving up. |
350 | | #define MAX_JPEG_SEGMENTS 100 |
351 | | |
352 | | // Finds the offset of the first MPF segment. Returns AVIF_TRUE if it was found. |
353 | | static avifBool avifJPEGFindMpfSegmentOffset(FILE * f, uint32_t * mpfOffset) |
354 | | { |
355 | | const long oldOffset = ftell(f); |
356 | | if (oldOffset < 0) { |
357 | | return AVIF_FALSE; |
358 | | } |
359 | | |
360 | | uint32_t offset = 2; // Skip the 2 byte SOI (Start Of Image) marker. |
361 | | if (fseek(f, offset, SEEK_SET) != 0) { |
362 | | return AVIF_FALSE; |
363 | | } |
364 | | |
365 | | uint8_t buffer[4]; |
366 | | int numSegments = 0; |
367 | | while (numSegments < MAX_JPEG_SEGMENTS) { |
368 | | ++numSegments; |
369 | | // Read the APP<n> segment marker (2 bytes) and the segment size (2 bytes). |
370 | | if (fread(buffer, 1, 4, f) != 4) { |
371 | | fseek(f, oldOffset, SEEK_SET); |
372 | | return AVIF_FALSE; // End of the file reached. |
373 | | } |
374 | | offset += 4; |
375 | | |
376 | | // Total APP<n> segment byte count, including the byte count value (2 bytes), but excluding the 2 byte APP<n> marker itself. |
377 | | const uint16_t segmentLength = avifJPEGReadUint16BigEndian(&buffer[2]); |
378 | | if (segmentLength < 2) { |
379 | | fseek(f, oldOffset, SEEK_SET); |
380 | | return AVIF_FALSE; // Invalid length. |
381 | | } else if (segmentLength < 2 + AVIF_JPEG_MPF_HEADER_LENGTH) { |
382 | | // Cannot be an MPF segment, skip to the next segment. |
383 | | offset += segmentLength - 2; |
384 | | if (fseek(f, offset, SEEK_SET) != 0) { |
385 | | fseek(f, oldOffset, SEEK_SET); |
386 | | return AVIF_FALSE; |
387 | | } |
388 | | continue; |
389 | | } |
390 | | |
391 | | uint8_t identifier[AVIF_JPEG_MPF_HEADER_LENGTH]; |
392 | | if (fread(identifier, 1, AVIF_JPEG_MPF_HEADER_LENGTH, f) != AVIF_JPEG_MPF_HEADER_LENGTH) { |
393 | | fseek(f, oldOffset, SEEK_SET); |
394 | | return AVIF_FALSE; // End of the file reached. |
395 | | } |
396 | | offset += AVIF_JPEG_MPF_HEADER_LENGTH; |
397 | | |
398 | | if (buffer[1] == (JPEG_APP0 + 2) && !memcmp(identifier, AVIF_JPEG_MPF_HEADER, AVIF_JPEG_MPF_HEADER_LENGTH)) { |
399 | | // MPF segment found. |
400 | | *mpfOffset = offset; |
401 | | fseek(f, oldOffset, SEEK_SET); |
402 | | return AVIF_TRUE; |
403 | | } |
404 | | |
405 | | // Skip to the next segment. |
406 | | offset += segmentLength - 2 - AVIF_JPEG_MPF_HEADER_LENGTH; |
407 | | if (fseek(f, offset, SEEK_SET) != 0) { |
408 | | fseek(f, oldOffset, SEEK_SET); |
409 | | return AVIF_FALSE; |
410 | | } |
411 | | } |
412 | | return AVIF_FALSE; |
413 | | } |
414 | | |
415 | | // Searches for a node called 'nameSpace:nodeName' in the children (or descendants if 'recursive' is set) of 'parentNode'. |
416 | | // Returns the first such node found (in depth first search). Returns NULL if no such node is found. |
417 | | static const xmlNode * avifJPEGFindXMLNodeByName(const xmlNode * parentNode, const char * nameSpace, const char * nodeName, avifBool recursive) |
418 | | { |
419 | | if (parentNode == NULL) { |
420 | | return NULL; |
421 | | } |
422 | | for (const xmlNode * node = parentNode->children; node != NULL; node = node->next) { |
423 | | if (node->ns != NULL && !xmlStrcmp(node->ns->href, (const xmlChar *)nameSpace) && |
424 | | !xmlStrcmp(node->name, (const xmlChar *)nodeName)) { |
425 | | return node; |
426 | | } else if (recursive) { |
427 | | const xmlNode * descendantNode = avifJPEGFindXMLNodeByName(node, nameSpace, nodeName, recursive); |
428 | | if (descendantNode != NULL) { |
429 | | return descendantNode; |
430 | | } |
431 | | } |
432 | | } |
433 | | return NULL; |
434 | | } |
435 | | |
436 | | #define XML_NAME_SPACE_GAIN_MAP "http://ns.adobe.com/hdr-gain-map/1.0/" |
437 | | #define XML_NAME_SPACE_RDF "http://www.w3.org/1999/02/22-rdf-syntax-ns#" |
438 | | #define XML_NAME_SPACE_XMP_NOTE "http://ns.adobe.com/xmp/note/" |
439 | | |
440 | | // Finds an 'rdf:Description' node containing a gain map version attribute (hdrgm:Version="1.0"). |
441 | | // Returns NULL if not found. |
442 | | static const xmlNode * avifJPEGFindGainMapXMPNode(const xmlNode * rootNode) |
443 | | { |
444 | | // See XMP specification https://github.com/adobe/XMP-Toolkit-SDK/blob/main/docs/XMPSpecificationPart1.pdf |
445 | | // ISO 16684-1:2011 7.1 "For this serialization, a single XMP packet shall be serialized using a single rdf:RDF XML element." |
446 | | // 7.3 "Other XML elements may appear around the rdf:RDF element." |
447 | | const xmlNode * rdfNode = avifJPEGFindXMLNodeByName(rootNode, XML_NAME_SPACE_RDF, "RDF", /*recursive=*/AVIF_TRUE); |
448 | | if (rdfNode == NULL) { |
449 | | return NULL; |
450 | | } |
451 | | for (const xmlNode * node = rdfNode->children; node != NULL; node = node->next) { |
452 | | // Loop through rdf:Description children. |
453 | | // 7.4 "A single XMP packet shall be serialized using a single rdf:RDF XML element. The rdf:RDF element content |
454 | | // shall consist of only zero or more rdf:Description elements." |
455 | | if (node->ns && !xmlStrcmp(node->ns->href, (const xmlChar *)XML_NAME_SPACE_RDF) && |
456 | | !xmlStrcmp(node->name, (const xmlChar *)"Description")) { |
457 | | // Look for the gain map version attribute: hdrgm:Version="1.0" |
458 | | for (xmlAttr * prop = node->properties; prop != NULL; prop = prop->next) { |
459 | | if (prop->ns && !xmlStrcmp(prop->ns->href, (const xmlChar *)XML_NAME_SPACE_GAIN_MAP) && |
460 | | !xmlStrcmp(prop->name, (const xmlChar *)"Version") && prop->children != NULL && |
461 | | !xmlStrcmp(prop->children->content, (const xmlChar *)"1.0")) { |
462 | | return node; |
463 | | } |
464 | | } |
465 | | } |
466 | | } |
467 | | return NULL; |
468 | | } |
469 | | |
470 | | // Returns true if there is an 'rdf:Description' node containing a gain map version attribute (hdrgm:Version="1.0"). |
471 | | // On the main image, this signals that the file also contains a gain map. |
472 | | // On a subsequent image, this signals that it is a gain map. |
473 | | static avifBool avifJPEGHasGainMapXMPNode(const uint8_t * xmpData, size_t xmpSize) |
474 | | { |
475 | | xmlDoc * document = xmlReadMemory((const char *)xmpData, (int)xmpSize, NULL, NULL, /*options=*/0); |
476 | | if (document == NULL) { |
477 | | return AVIF_FALSE; // Probably and out of memory error. |
478 | | } |
479 | | const xmlNode * rootNode = xmlDocGetRootElement(document); |
480 | | const xmlNode * node = avifJPEGFindGainMapXMPNode(rootNode); |
481 | | const avifBool found = (node != NULL); |
482 | | xmlFreeDoc(document); |
483 | | return found; |
484 | | } |
485 | | |
486 | | // Finds the value of a gain map metadata property, that can be either stored as an attribute of 'descriptionNode' |
487 | | // (which should point to a <rdf:Description> node) or as a child node. |
488 | | // 'maxValues' is the maximum number of expected values, and the size of the 'values' array. 'numValues' is set to the number |
489 | | // of values actually found (which may be smaller or larger, but only up to 'maxValues' are stored in 'values'). |
490 | | // Returns AVIF_TRUE if the property was found. |
491 | | static avifBool avifJPEGFindGainMapProperty(const xmlNode * descriptionNode, |
492 | | const char * propertyName, |
493 | | uint32_t maxValues, |
494 | | const char * values[], |
495 | | uint32_t * numValues) |
496 | | { |
497 | | *numValues = 0; |
498 | | |
499 | | // Search attributes. |
500 | | for (xmlAttr * prop = descriptionNode->properties; prop != NULL; prop = prop->next) { |
501 | | if (prop->ns && !xmlStrcmp(prop->ns->href, (const xmlChar *)XML_NAME_SPACE_GAIN_MAP) && |
502 | | !xmlStrcmp(prop->name, (const xmlChar *)propertyName) && prop->children != NULL && prop->children->content != NULL) { |
503 | | // Properties should have just one child containing the property's value |
504 | | // (in fact the 'children' field is documented as "the value of the property"). |
505 | | values[0] = (const char *)prop->children->content; |
506 | | *numValues = 1; |
507 | | return AVIF_TRUE; |
508 | | } |
509 | | } |
510 | | |
511 | | // Search child nodes. |
512 | | for (const xmlNode * node = descriptionNode->children; node != NULL; node = node->next) { |
513 | | if (node->ns && !xmlStrcmp(node->ns->href, (const xmlChar *)XML_NAME_SPACE_GAIN_MAP) && |
514 | | !xmlStrcmp(node->name, (const xmlChar *)propertyName) && node->children) { |
515 | | // Multiple values can be specified with a Seq tag: <rdf:Seq><rdf:li>value1</rdf:li><rdf:li>value2</rdf:li>...</rdf:Seq> |
516 | | const xmlNode * seq = avifJPEGFindXMLNodeByName(node, XML_NAME_SPACE_RDF, "Seq", /*recursive=*/AVIF_FALSE); |
517 | | if (seq) { |
518 | | for (xmlNode * seqChild = seq->children; seqChild; seqChild = seqChild->next) { |
519 | | if (!xmlStrcmp(seqChild->name, (const xmlChar *)"li") && seqChild->children != NULL && |
520 | | seqChild->children->content != NULL) { |
521 | | if (*numValues < maxValues) { |
522 | | values[*numValues] = (const char *)seqChild->children->content; |
523 | | } |
524 | | ++(*numValues); |
525 | | } |
526 | | } |
527 | | return *numValues > 0 ? AVIF_TRUE : AVIF_FALSE; |
528 | | } else if (node->children->next == NULL && node->children->type == XML_TEXT_NODE) { // Only one child and it's text. |
529 | | values[0] = (const char *)node->children->content; |
530 | | *numValues = 1; |
531 | | return AVIF_TRUE; |
532 | | } |
533 | | // We found a tag for this property but no valid content. |
534 | | return AVIF_FALSE; |
535 | | } |
536 | | } |
537 | | |
538 | | return AVIF_FALSE; // Property not found. |
539 | | } |
540 | | |
541 | | // Up to 3 values per property (one for each RGB channel). |
542 | | #define GAIN_MAP_PROPERTY_MAX_VALUES 3 |
543 | | |
544 | | // Looks for a given gain map property's double value(s), and if found, stores them in 'values'. |
545 | | // The 'values' array should have size at least 'numDoubles', and should be initialized with default |
546 | | // values for this property, since the array will be left untouched if the property is not found. |
547 | | // Returns AVIF_TRUE if the property was successfully parsed, or if it was not found, since all properties |
548 | | // are optional. Returns AVIF_FALSE in case of error (invalid metadata XMP). |
549 | | static avifBool avifJPEGFindGainMapPropertyDoubles(const xmlNode * descriptionNode, const char * propertyName, double * values, uint32_t numDoubles) |
550 | | { |
551 | | assert(numDoubles <= GAIN_MAP_PROPERTY_MAX_VALUES); |
552 | | const char * textValues[GAIN_MAP_PROPERTY_MAX_VALUES]; |
553 | | uint32_t numValues; |
554 | | if (!avifJPEGFindGainMapProperty(descriptionNode, propertyName, /*maxValues=*/numDoubles, &textValues[0], &numValues)) { |
555 | | return AVIF_TRUE; // Property was not found, but it's not an error since they're optional. |
556 | | } |
557 | | if (numValues != 1 && numValues != numDoubles) { |
558 | | return AVIF_FALSE; // Invalid, we expect either 1 or exactly numDoubles values. |
559 | | } |
560 | | for (uint32_t i = 0; i < numDoubles; ++i) { |
561 | | if (i >= numValues) { |
562 | | // If there is only 1 value, it's copied into the rest of the array. |
563 | | values[i] = values[i - 1]; |
564 | | } else { |
565 | | int charsRead; |
566 | | if (sscanf(textValues[i], "%lf%n", &values[i], &charsRead) < 1) { |
567 | | return AVIF_FALSE; // Was not able to parse the full string value as a double. |
568 | | } |
569 | | // Make sure that remaining characters (if any) are only whitespace. |
570 | | const int len = (int)strlen(textValues[i]); |
571 | | while (charsRead < len) { |
572 | | if (!isspace(textValues[i][charsRead])) { |
573 | | return AVIF_FALSE; // Invalid character. |
574 | | } |
575 | | ++charsRead; |
576 | | } |
577 | | } |
578 | | } |
579 | | |
580 | | return AVIF_TRUE; |
581 | | } |
582 | | |
583 | | static inline void SwapDoubles(double * x, double * y) |
584 | | { |
585 | | double tmp = *x; |
586 | | *x = *y; |
587 | | *y = tmp; |
588 | | } |
589 | | |
590 | | // Parses gain map metadata from XMP. |
591 | | // See https://developer.android.com/media/platform/hdr-image-format |
592 | | // Returns AVIF_TRUE if the gain map metadata was successfully read. |
593 | | static avifBool avifJPEGParseGainMapXMPProperties(const xmlNode * rootNode, avifGainMap * gainMap) |
594 | | { |
595 | | const xmlNode * descNode = avifJPEGFindGainMapXMPNode(rootNode); |
596 | | if (descNode == NULL) { |
597 | | return AVIF_FALSE; |
598 | | } |
599 | | |
600 | | double baseHdrHeadroom = 0.0; |
601 | | double alternateHdrHeadroom = 1.0; |
602 | | double gainMapMin[3] = { 0.0, 0.0, 0.0 }; |
603 | | double gainMapMax[3] = { 1.0, 1.0, 1.0 }; |
604 | | double gainMapGamma[3] = { 1.0, 1.0, 1.0 }; |
605 | | double baseOffset[3] = { 1.0 / 64.0, 1.0 / 64.0, 1.0 / 64.0 }; |
606 | | double alternateOffset[3] = { 1.0 / 64.0, 1.0 / 64.0, 1.0 / 64.0 }; |
607 | | AVIF_CHECK(avifJPEGFindGainMapPropertyDoubles(descNode, "HDRCapacityMin", &baseHdrHeadroom, /*numDoubles=*/1)); |
608 | | AVIF_CHECK(avifJPEGFindGainMapPropertyDoubles(descNode, "HDRCapacityMax", &alternateHdrHeadroom, /*numDoubles=*/1)); |
609 | | AVIF_CHECK(avifJPEGFindGainMapPropertyDoubles(descNode, "OffsetSDR", baseOffset, /*numDoubles=*/3)); |
610 | | AVIF_CHECK(avifJPEGFindGainMapPropertyDoubles(descNode, "OffsetHDR", alternateOffset, /*numDoubles=*/3)); |
611 | | AVIF_CHECK(avifJPEGFindGainMapPropertyDoubles(descNode, "GainMapMin", gainMapMin, /*numDoubles=*/3)); |
612 | | AVIF_CHECK(avifJPEGFindGainMapPropertyDoubles(descNode, "GainMapMax", gainMapMax, /*numDoubles=*/3)); |
613 | | AVIF_CHECK(avifJPEGFindGainMapPropertyDoubles(descNode, "Gamma", gainMapGamma, /*numDoubles=*/3)); |
614 | | |
615 | | AVIF_CHECK(alternateHdrHeadroom > baseHdrHeadroom); |
616 | | AVIF_CHECK(baseHdrHeadroom >= 0); |
617 | | for (int i = 0; i < 3; ++i) { |
618 | | AVIF_CHECK(gainMapMax[i] >= gainMapMin[i]); |
619 | | AVIF_CHECK(baseOffset[i] >= 0.0); |
620 | | AVIF_CHECK(alternateOffset[i] >= 0.0); |
621 | | AVIF_CHECK(gainMapGamma[i] > 0.0); |
622 | | } |
623 | | |
624 | | uint32_t numValues; |
625 | | const char * baseRenditionIsHDR; |
626 | | if (avifJPEGFindGainMapProperty(descNode, "BaseRenditionIsHDR", /*maxValues=*/1, &baseRenditionIsHDR, &numValues)) { |
627 | | if (!strcmp(baseRenditionIsHDR, "True")) { |
628 | | SwapDoubles(&baseHdrHeadroom, &alternateHdrHeadroom); |
629 | | for (int c = 0; c < 3; ++c) { |
630 | | SwapDoubles(&baseOffset[c], &alternateOffset[c]); |
631 | | } |
632 | | } else if (!strcmp(baseRenditionIsHDR, "False")) { |
633 | | } else { |
634 | | return AVIF_FALSE; // Unexpected value. |
635 | | } |
636 | | } |
637 | | |
638 | | for (int i = 0; i < 3; ++i) { |
639 | | AVIF_CHECK(avifDoubleToSignedFraction(gainMapMin[i], &gainMap->gainMapMin[i])); |
640 | | AVIF_CHECK(avifDoubleToSignedFraction(gainMapMax[i], &gainMap->gainMapMax[i])); |
641 | | AVIF_CHECK(avifDoubleToUnsignedFraction(gainMapGamma[i], &gainMap->gainMapGamma[i])); |
642 | | AVIF_CHECK(avifDoubleToSignedFraction(baseOffset[i], &gainMap->baseOffset[i])); |
643 | | AVIF_CHECK(avifDoubleToSignedFraction(alternateOffset[i], &gainMap->alternateOffset[i])); |
644 | | } |
645 | | AVIF_CHECK(avifDoubleToUnsignedFraction(baseHdrHeadroom, &gainMap->baseHdrHeadroom)); |
646 | | AVIF_CHECK(avifDoubleToUnsignedFraction(alternateHdrHeadroom, &gainMap->alternateHdrHeadroom)); |
647 | | // Not in the XMP metadata but both color spaces should be the same so this value doesn't matter. |
648 | | gainMap->useBaseColorSpace = AVIF_TRUE; |
649 | | |
650 | | return AVIF_TRUE; |
651 | | } |
652 | | |
653 | | // Parses gain map metadata from an XMP payload. |
654 | | // Returns AVIF_TRUE if the gain map metadata was successfully read. |
655 | | avifBool avifJPEGParseGainMapXMP(const uint8_t * xmpData, size_t xmpSize, avifGainMap * gainMap) |
656 | | { |
657 | | xmlDoc * document = xmlReadMemory((const char *)xmpData, (int)xmpSize, NULL, NULL, /*options=*/0); |
658 | | if (document == NULL) { |
659 | | return AVIF_FALSE; // Probably an out of memory error. |
660 | | } |
661 | | xmlNode * rootNode = xmlDocGetRootElement(document); |
662 | | const avifBool res = avifJPEGParseGainMapXMPProperties(rootNode, gainMap); |
663 | | xmlFreeDoc(document); |
664 | | return res; |
665 | | } |
666 | | |
667 | | // Parses an MPF (Multi-Picture File) JPEG metadata segment to find the location of other |
668 | | // images, and decodes the gain map image (as determined by having gain map XMP metadata) into 'avif'. |
669 | | // See CIPA DC-007-Translation-2021 Multi-Picture Format at https://www.cipa.jp/e/std/std-sec.html, |
670 | | // (in particular Figures 1 to 6) and https://developer.android.com/media/platform/hdr-image-format. |
671 | | // Returns AVIF_FALSE if no gain map was found. |
672 | | static avifBool avifJPEGExtractGainMapImageFromMpf(FILE * f, |
673 | | uint32_t sizeLimit, |
674 | | const avifROData * segmentData, |
675 | | avifImage * avif, |
676 | | avifChromaDownsampling chromaDownsampling) |
677 | | { |
678 | | uint32_t offset = 0; |
679 | | |
680 | | const uint8_t littleEndian[4] = { 0x49, 0x49, 0x2A, 0x00 }; // "II*\0" |
681 | | const uint8_t bigEndian[4] = { 0x4D, 0x4D, 0x00, 0x2A }; // "MM\0*" |
682 | | |
683 | | uint8_t endiannessTag[4]; |
684 | | AVIF_CHECK(avifJPEGReadBytes(segmentData, endiannessTag, &offset, 4)); |
685 | | |
686 | | avifBool isBigEndian; |
687 | | if (!memcmp(endiannessTag, bigEndian, 4)) { |
688 | | isBigEndian = AVIF_TRUE; |
689 | | } else if (!memcmp(endiannessTag, littleEndian, 4)) { |
690 | | isBigEndian = AVIF_FALSE; |
691 | | } else { |
692 | | return AVIF_FALSE; // Invalid endianness tag. |
693 | | } |
694 | | |
695 | | uint32_t offsetToFirstIfd; |
696 | | AVIF_CHECK(avifJPEGReadU32(segmentData, &offsetToFirstIfd, &offset, isBigEndian)); |
697 | | if (offsetToFirstIfd < offset) { |
698 | | return AVIF_FALSE; |
699 | | } |
700 | | offset = offsetToFirstIfd; |
701 | | |
702 | | // Read MP (Multi-Picture) tags. |
703 | | uint16_t mpTagCount; |
704 | | AVIF_CHECK(avifJPEGReadU16(segmentData, &mpTagCount, &offset, isBigEndian)); |
705 | | |
706 | | // See also https://www.media.mit.edu/pia/Research/deepview/exif.html |
707 | | uint32_t numImages = 0; |
708 | | uint32_t mpEntryOffset = 0; |
709 | | for (int mpTagIdx = 0; mpTagIdx < mpTagCount; ++mpTagIdx) { |
710 | | uint16_t tagId; |
711 | | AVIF_CHECK(avifJPEGReadU16(segmentData, &tagId, &offset, isBigEndian)); |
712 | | if (UINT32_MAX - offset < 2 + 4) { |
713 | | return AVIF_FALSE; |
714 | | } |
715 | | offset += 2; // Skip data format. |
716 | | offset += 4; // Skip num components. |
717 | | uint8_t valueBytes[4]; |
718 | | AVIF_CHECK(avifJPEGReadBytes(segmentData, valueBytes, &offset, 4)); |
719 | | const uint32_t value = isBigEndian ? avifJPEGReadUint32BigEndian(valueBytes) : avifJPEGReadUint32LittleEndian(valueBytes); |
720 | | |
721 | | switch (tagId) { // MPFVersion |
722 | | case 45056: // MPFVersion |
723 | | if (memcmp(valueBytes, "0100", 4)) { |
724 | | // Unexpected version. |
725 | | return AVIF_FALSE; |
726 | | } |
727 | | break; |
728 | | case 45057: // NumberOfImages |
729 | | numImages = value; |
730 | | break; |
731 | | case 45058: // MPEntry |
732 | | mpEntryOffset = value; |
733 | | break; |
734 | | case 45059: // ImageUIDList, unused |
735 | | case 45060: // TotalFrames, unused |
736 | | default: |
737 | | break; |
738 | | } |
739 | | } |
740 | | if (numImages < 2 || mpEntryOffset < offset) { |
741 | | return AVIF_FALSE; |
742 | | } |
743 | | offset = mpEntryOffset; |
744 | | |
745 | | uint32_t mpfSegmentOffset; |
746 | | AVIF_CHECK(avifJPEGFindMpfSegmentOffset(f, &mpfSegmentOffset)); |
747 | | |
748 | | for (uint32_t imageIdx = 0; imageIdx < numImages; ++imageIdx) { |
749 | | if (UINT32_MAX - offset < 4) { |
750 | | return AVIF_FALSE; |
751 | | } |
752 | | offset += 4; // Skip "Individual Image Attribute" |
753 | | uint32_t imageSize; |
754 | | AVIF_CHECK(avifJPEGReadU32(segmentData, &imageSize, &offset, isBigEndian)); |
755 | | uint32_t imageDataOffset; |
756 | | AVIF_CHECK(avifJPEGReadU32(segmentData, &imageDataOffset, &offset, isBigEndian)); |
757 | | |
758 | | if (UINT32_MAX - offset < 4) { |
759 | | return AVIF_FALSE; |
760 | | } |
761 | | offset += 4; // Skip "Dependent image Entry Number" (2 + 2 bytes) |
762 | | if (imageDataOffset == 0) { |
763 | | // 0 is a special value which indicates the first image. |
764 | | // Assume the first image cannot be the gain map and skip it. |
765 | | continue; |
766 | | } |
767 | | |
768 | | // Offsets are relative to the start of the MPF segment. Make them absolute. |
769 | | imageDataOffset += mpfSegmentOffset; |
770 | | if (fseek(f, imageDataOffset, SEEK_SET) != 0) { |
771 | | return AVIF_FALSE; |
772 | | } |
773 | | // Read the image and check its XMP to see if it's a gain map. |
774 | | // NOTE we decode all additional images until a gain map is found, even if some might not |
775 | | // be gain maps. This could be fixed by having a helper function to get just the XMP without |
776 | | // decoding the whole image. |
777 | | if (!avifJPEGReadInternal(f, |
778 | | "gain map", |
779 | | avif, |
780 | | /*requestedFormat=*/AVIF_PIXEL_FORMAT_NONE, // automatic |
781 | | /*requestedDepth=*/0, // automatic |
782 | | chromaDownsampling, |
783 | | /*ignoreColorProfile=*/AVIF_TRUE, |
784 | | /*ignoreExif=*/AVIF_TRUE, |
785 | | /*ignoreXMP=*/AVIF_FALSE, |
786 | | /*ignoreGainMap=*/AVIF_TRUE, |
787 | | sizeLimit)) { |
788 | | continue; |
789 | | } |
790 | | if (avifJPEGHasGainMapXMPNode(avif->xmp.data, avif->xmp.size)) { |
791 | | return AVIF_TRUE; |
792 | | } |
793 | | } |
794 | | |
795 | | return AVIF_FALSE; |
796 | | } |
797 | | |
798 | | // Tries to find and decode a gain map image and its metadata. |
799 | | // Looks for an MPF (Multi-Picture Format) segment then loops through the linked images to see |
800 | | // if one of them has gain map XMP metadata. |
801 | | // See CIPA DC-007-Translation-2021 Multi-Picture Format at https://www.cipa.jp/e/std/std-sec.html |
802 | | // and https://developer.android.com/media/platform/hdr-image-format |
803 | | // Returns AVIF_TRUE if a gain map was found. |
804 | | static avifBool avifJPEGExtractGainMapImage(FILE * f, |
805 | | uint32_t sizeLimit, |
806 | | struct jpeg_decompress_struct * cinfo, |
807 | | avifGainMap * gainMap, |
808 | | avifChromaDownsampling chromaDownsampling) |
809 | | { |
810 | | const avifROData tagMpf = { (const uint8_t *)AVIF_JPEG_MPF_HEADER, AVIF_JPEG_MPF_HEADER_LENGTH }; |
811 | | for (jpeg_saved_marker_ptr marker = cinfo->marker_list; marker != NULL; marker = marker->next) { |
812 | | // Note we assume there is only one MPF segment and only look at the first one. |
813 | | // Otherwise avifJPEGFindMpfSegmentOffset() would have to be modified to take the index of the |
814 | | // MPF segment whose offset to return. |
815 | | if ((marker->marker == (JPEG_APP0 + 2)) && (marker->data_length > tagMpf.size) && |
816 | | !memcmp(marker->data, tagMpf.data, tagMpf.size)) { |
817 | | avifImage * image = avifImageCreateEmpty(); |
818 | | // Set jpeg native matrix coefficients to allow copying YUV values directly. |
819 | | image->matrixCoefficients = AVIF_MATRIX_COEFFICIENTS_BT601; |
820 | | assert(avifJPEGHasCompatibleMatrixCoefficients(image->matrixCoefficients)); |
821 | | |
822 | | const avifROData mpfData = { (const uint8_t *)marker->data + tagMpf.size, marker->data_length - tagMpf.size }; |
823 | | if (!avifJPEGExtractGainMapImageFromMpf(f, sizeLimit, &mpfData, image, chromaDownsampling)) { |
824 | | if (f == stdin) { |
825 | | // Not supported because fseek doesn't work on stdin. |
826 | | fprintf(stderr, "Warning: gain map transcoding is not supported with sdtin\n"); |
827 | | } else { |
828 | | fprintf(stderr, "Note: XMP metadata indicated the presence of a gain map, but it could not be found or decoded\n"); |
829 | | } |
830 | | avifImageDestroy(image); |
831 | | return AVIF_FALSE; |
832 | | } |
833 | | if (!avifJPEGParseGainMapXMP(image->xmp.data, image->xmp.size, gainMap)) { |
834 | | fprintf(stderr, "Warning: failed to parse gain map metadata\n"); |
835 | | avifImageDestroy(image); |
836 | | return AVIF_FALSE; |
837 | | } |
838 | | |
839 | | gainMap->image = image; |
840 | | return AVIF_TRUE; |
841 | | } |
842 | | } |
843 | | return AVIF_FALSE; |
844 | | } |
845 | | |
846 | | // Merges the standard XMP data with the extended XMP data. |
847 | | // Returns AVIF_FALSE if an error occurred. |
848 | | static avifBool avifJPEGMergeXMP(const uint8_t * standardXMPData, |
849 | | uint32_t standardXMPSize, |
850 | | const avifRWData extendedXMP, |
851 | | avifBool foundAlternativeXMPNote, |
852 | | avifRWData * xmp) |
853 | | { |
854 | | // Initialize the XMP RDF. |
855 | | avifBool isValid = AVIF_TRUE; |
856 | | xmlDoc * extendedXMPDoc = NULL; |
857 | | xmlChar * xmlBuff = NULL; |
858 | | xmlDoc * xmpDoc = xmlReadMemory((const char *)standardXMPData, (int)standardXMPSize, "standard.xml", NULL, /*options=*/0); |
859 | | xmlNode * xmpRdf = (xmlNode *)avifJPEGFindXMLNodeByName(xmlDocGetRootElement(xmpDoc), |
860 | | XML_NAME_SPACE_RDF, |
861 | | "RDF", |
862 | | /*recursive=*/AVIF_TRUE); |
863 | | if (!xmpRdf) { |
864 | | fprintf(stderr, "XMP extraction failed: cannot find RDF node\n"); |
865 | | isValid = AVIF_FALSE; |
866 | | goto cleanup_xml; |
867 | | } |
868 | | // According to Adobe XMP Specification Part 3 section 1.1.3.1: |
869 | | // "A JPEG reader must [...] remove the xmpNote:HasExtendedXMP property." |
870 | | avifBool foundHasExtendedXMP = AVIF_FALSE; |
871 | | xmlNode * descNode = xmpRdf->children; |
872 | | while (!foundHasExtendedXMP && descNode != NULL) { |
873 | | if (descNode->type == XML_ELEMENT_NODE && descNode->ns != NULL && |
874 | | xmlStrcmp(descNode->ns->href, (const xmlChar *)XML_NAME_SPACE_RDF) == 0 && |
875 | | xmlStrcmp(descNode->name, (const xmlChar *)"Description") == 0) { |
876 | | // Remove the HasExtendedXMP property. |
877 | | if (foundAlternativeXMPNote) { |
878 | | xmlNodePtr cur = descNode->children; |
879 | | while (cur != NULL) { |
880 | | if (cur->type == XML_ELEMENT_NODE && cur->ns != NULL && xmlStrcmp(cur->name, (const xmlChar *)"HasExtendedXMP") == 0 && |
881 | | xmlStrcmp(cur->ns->href, (const xmlChar *)XML_NAME_SPACE_XMP_NOTE) == 0) { |
882 | | // We must Unlink and Free the node. |
883 | | xmlUnlinkNode(cur); |
884 | | xmlFreeNode(cur); |
885 | | foundHasExtendedXMP = AVIF_TRUE; |
886 | | break; |
887 | | } |
888 | | cur = cur->next; |
889 | | } |
890 | | } else { |
891 | | xmlAttrPtr attr = xmlHasNsProp(descNode, (const xmlChar *)"HasExtendedXMP", (const xmlChar *)XML_NAME_SPACE_XMP_NOTE); |
892 | | |
893 | | if (attr) { |
894 | | xmlRemoveProp(attr); |
895 | | foundHasExtendedXMP = AVIF_TRUE; |
896 | | break; |
897 | | } |
898 | | } |
899 | | } |
900 | | // Check next sibling in case there are multiple Descriptions. |
901 | | descNode = descNode->next; |
902 | | } |
903 | | if (!foundHasExtendedXMP) { |
904 | | fprintf(stderr, "XMP extraction failed: cannot find HasExtendedXMP property\n"); |
905 | | isValid = AVIF_FALSE; |
906 | | goto cleanup_xml; |
907 | | } |
908 | | |
909 | | // Read the extended XMP. |
910 | | extendedXMPDoc = xmlReadMemory((const char *)extendedXMP.data, |
911 | | (int)extendedXMP.size, |
912 | | "extended.xml", |
913 | | NULL, |
914 | | /*options=*/0); |
915 | | const xmlNode * extendedXMPRdf = avifJPEGFindXMLNodeByName(xmlDocGetRootElement(extendedXMPDoc), |
916 | | XML_NAME_SPACE_RDF, |
917 | | "RDF", |
918 | | /*recursive=*/AVIF_TRUE); |
919 | | if (!extendedXMPRdf) { |
920 | | fprintf(stderr, "XMP extraction failed: invalid standard XMP segment\n"); |
921 | | isValid = AVIF_FALSE; |
922 | | goto cleanup_xml; |
923 | | } |
924 | | // Copy the extended nodes over. |
925 | | xmlNode * cur = extendedXMPRdf->xmlChildrenNode; |
926 | | while (cur != NULL) { |
927 | | // Copy the child. |
928 | | xmlNode * childCopy = xmlDocCopyNode(cur, xmpDoc, 1); |
929 | | xmlAddChild(xmpRdf, childCopy); |
930 | | cur = cur->next; |
931 | | } |
932 | | |
933 | | // Dump the new XMP to avif->xmp. |
934 | | int buffer_size; |
935 | | xmlDocDumpFormatMemory(xmpDoc, &xmlBuff, &buffer_size, 1); |
936 | | if (xmlBuff == NULL) { |
937 | | fprintf(stderr, "Error: Could not dump XML to memory.\n"); |
938 | | isValid = AVIF_FALSE; |
939 | | goto cleanup_xml; |
940 | | } |
941 | | |
942 | | avifRWDataFree(xmp); |
943 | | if (avifRWDataRealloc(xmp, (size_t)buffer_size) != AVIF_RESULT_OK) { |
944 | | fprintf(stderr, "XMP copy failed: out of memory\n"); |
945 | | isValid = AVIF_FALSE; |
946 | | goto cleanup_xml; |
947 | | } |
948 | | memcpy(xmp->data, xmlBuff, buffer_size); |
949 | | cleanup_xml: |
950 | | xmlFreeDoc(xmpDoc); |
951 | | xmlFreeDoc(extendedXMPDoc); |
952 | | xmlFree(xmlBuff); |
953 | | return isValid; |
954 | | } |
955 | | |
956 | | #endif // AVIF_ENABLE_JPEG_GAIN_MAP_CONVERSION |
957 | | |
958 | | // Note on setjmp() and volatile variables: |
959 | | // |
960 | | // K & R, The C Programming Language 2nd Ed, p. 254 says: |
961 | | // ... Accessible objects have the values they had when longjmp was called, |
962 | | // except that non-volatile automatic variables in the function calling setjmp |
963 | | // become undefined if they were changed after the setjmp call. |
964 | | // |
965 | | // Therefore, 'iccData' is declared as volatile. 'rgb' should be declared as |
966 | | // volatile, but doing so would be inconvenient (try it) and since it is a |
967 | | // struct, the compiler is unlikely to put it in a register. 'ret' does not need |
968 | | // to be declared as volatile because it is not modified between setjmp and |
969 | | // longjmp. But GCC's -Wclobbered warning may have trouble figuring that out, so |
970 | | // we preemptively declare it as volatile. |
971 | | |
972 | | static avifBool avifJPEGReadInternal(FILE * f, |
973 | | const char * inputFilename, |
974 | | avifImage * avif, |
975 | | avifPixelFormat requestedFormat, |
976 | | uint32_t requestedDepth, |
977 | | avifChromaDownsampling chromaDownsampling, |
978 | | avifBool ignoreColorProfile, |
979 | | avifBool ignoreExif, |
980 | | avifBool ignoreXMP, |
981 | | avifBool ignoreGainMap, |
982 | | uint32_t sizeLimit) |
983 | 4.99k | { |
984 | 4.99k | volatile avifBool ret = AVIF_FALSE; |
985 | 4.99k | uint8_t * volatile iccData = NULL; |
986 | | |
987 | 4.99k | avifRGBImage rgb; |
988 | 4.99k | memset(&rgb, 0, sizeof(avifRGBImage)); |
989 | | |
990 | | // Extended XMP after concatenation of all extended XMP segments. |
991 | 4.99k | avifRWData extendedXMP = { NULL, 0 }; |
992 | | // Each byte set to 0 is a missing byte. Each byte set to 1 was read and copied to totalXMP. |
993 | 4.99k | avifRWData extendedXMPReadBytes = { NULL, 0 }; |
994 | | |
995 | 4.99k | struct my_error_mgr jerr; |
996 | 4.99k | struct jpeg_decompress_struct cinfo; |
997 | 4.99k | cinfo.err = jpeg_std_error(&jerr.pub); |
998 | 4.99k | jerr.pub.error_exit = my_error_exit; |
999 | 4.99k | if (setjmp(jerr.setjmp_buffer)) { |
1000 | 2.70k | goto cleanup; |
1001 | 2.70k | } |
1002 | | |
1003 | 4.99k | jpeg_create_decompress(&cinfo); |
1004 | | |
1005 | | // See also https://exiftool.org/TagNames/JPEG.html for the meaning of various APP<n> segments. |
1006 | 4.37k | if (!ignoreExif || !ignoreXMP || !ignoreGainMap) { |
1007 | | // Keep APP1 blocks, for Exif and XMP. |
1008 | 4.37k | jpeg_save_markers(&cinfo, JPEG_APP0 + 1, /*length_limit=*/0xFFFF); |
1009 | 4.37k | } |
1010 | 2.45k | if (!ignoreGainMap) { |
1011 | | // Keep APP2 blocks, for obtaining ICC and MPF data. |
1012 | 2.45k | jpeg_save_markers(&cinfo, JPEG_APP0 + 2, /*length_limit=*/0xFFFF); |
1013 | 2.45k | } |
1014 | | |
1015 | 2.57k | if (!ignoreColorProfile) { |
1016 | 2.57k | setup_read_icc_profile(&cinfo); |
1017 | 2.57k | } |
1018 | 2.29k | jpeg_stdio_src(&cinfo, f); |
1019 | 2.29k | jpeg_read_header(&cinfo, TRUE); |
1020 | | |
1021 | 2.29k | jpeg_calc_output_dimensions(&cinfo); |
1022 | 2.29k | if (cinfo.output_width > sizeLimit / cinfo.output_height) { |
1023 | 18 | fprintf(stderr, "Too big JPEG dimensions (%u x %u > %u px): %s\n", cinfo.output_width, cinfo.output_height, sizeLimit, inputFilename); |
1024 | 18 | goto cleanup; |
1025 | 18 | } |
1026 | | |
1027 | 2.27k | if (!ignoreColorProfile) { |
1028 | 2.03k | uint8_t * iccDataTmp; |
1029 | 2.03k | unsigned int iccDataLen; |
1030 | 2.03k | if (read_icc_profile(&cinfo, &iccDataTmp, &iccDataLen)) { |
1031 | 87 | iccData = iccDataTmp; |
1032 | 87 | const avifBool isGray = (cinfo.jpeg_color_space == JCS_GRAYSCALE); |
1033 | 87 | if (!isGray && (requestedFormat == AVIF_PIXEL_FORMAT_YUV400)) { |
1034 | 17 | fprintf(stderr, |
1035 | 17 | "The image contains a color ICC profile which is incompatible with the requested output " |
1036 | 17 | "format YUV400 (grayscale). Pass --ignore-icc to discard the ICC profile.\n"); |
1037 | 17 | goto cleanup; |
1038 | 17 | } |
1039 | 70 | if (isGray && requestedFormat != AVIF_PIXEL_FORMAT_YUV400) { |
1040 | 1 | fprintf(stderr, |
1041 | 1 | "The image contains a gray ICC profile which is incompatible with the requested output " |
1042 | 1 | "format YUV (color). Pass --ignore-icc to discard the ICC profile.\n"); |
1043 | 1 | goto cleanup; |
1044 | 1 | } |
1045 | 69 | if (avifImageSetProfileICC(avif, iccDataTmp, (size_t)iccDataLen) != AVIF_RESULT_OK) { |
1046 | 0 | fprintf(stderr, "Setting ICC profile failed: %s (out of memory)\n", inputFilename); |
1047 | 0 | goto cleanup; |
1048 | 0 | } |
1049 | 69 | } |
1050 | 2.03k | } |
1051 | | |
1052 | 2.25k | avif->yuvFormat = requestedFormat; // This may be AVIF_PIXEL_FORMAT_NONE, which is "auto" to avifJPEGReadCopy() |
1053 | 18.4E | avif->depth = requestedDepth ? requestedDepth : 8; |
1054 | | // JPEG doesn't have alpha. Prevent confusion. |
1055 | 2.25k | avif->alphaPremultiplied = AVIF_FALSE; |
1056 | | |
1057 | 2.25k | if (avifJPEGReadCopy(avif, sizeLimit, &cinfo)) { |
1058 | | // JPEG pixels were successfully copied without conversion. Notify the enduser. |
1059 | | |
1060 | 106 | assert(inputFilename); // JPEG read doesn't support stdin |
1061 | 106 | printf("Directly copied JPEG pixel data (no YUV conversion): %s\n", inputFilename); |
1062 | 2.15k | } else { |
1063 | | // JPEG pixels could not be copied without conversion. Request (converted) RGB pixels from |
1064 | | // libjpeg and convert to YUV with libavif instead. |
1065 | | |
1066 | 2.15k | cinfo.out_color_space = JCS_RGB; |
1067 | 2.15k | jpeg_start_decompress(&cinfo); |
1068 | | |
1069 | 2.15k | int row_stride = cinfo.output_width * cinfo.output_components; |
1070 | 2.15k | JSAMPARRAY buffer = (*cinfo.mem->alloc_sarray)((j_common_ptr)&cinfo, JPOOL_IMAGE, row_stride, 1); |
1071 | | |
1072 | 2.15k | avif->width = cinfo.output_width; |
1073 | 2.15k | avif->height = cinfo.output_height; |
1074 | 2.15k | if (avif->matrixCoefficients == AVIF_MATRIX_COEFFICIENTS_YCGCO_RO) { |
1075 | 0 | fprintf(stderr, "AVIF_MATRIX_COEFFICIENTS_YCGCO_RO cannot be used with JPEG because it has an even bit depth.\n"); |
1076 | 0 | goto cleanup; |
1077 | 0 | } |
1078 | 2.15k | if (avif->yuvFormat == AVIF_PIXEL_FORMAT_NONE) { |
1079 | | // Identity and YCgCo-R are only valid with YUV444. |
1080 | 0 | avif->yuvFormat = (avif->matrixCoefficients == AVIF_MATRIX_COEFFICIENTS_IDENTITY || |
1081 | 0 | avif->matrixCoefficients == AVIF_MATRIX_COEFFICIENTS_YCGCO_RE) |
1082 | 0 | ? AVIF_PIXEL_FORMAT_YUV444 |
1083 | 0 | : AVIF_APP_DEFAULT_PIXEL_FORMAT; |
1084 | 0 | } |
1085 | 18.4E | avif->depth = requestedDepth ? requestedDepth : 8; |
1086 | 2.15k | if (avif->matrixCoefficients == AVIF_MATRIX_COEFFICIENTS_YCGCO_RE) { |
1087 | 0 | if (requestedDepth && requestedDepth != 10) { |
1088 | 0 | fprintf(stderr, "Cannot request %u bits for YCgCo-Re as it uses 2 extra bits.\n", requestedDepth); |
1089 | 0 | goto cleanup; |
1090 | 0 | } |
1091 | 0 | avif->depth = 10; |
1092 | 0 | } |
1093 | 2.15k | avifRGBImageSetDefaults(&rgb, avif); |
1094 | 2.15k | rgb.format = AVIF_RGB_FORMAT_RGB; |
1095 | 2.15k | rgb.chromaDownsampling = chromaDownsampling; |
1096 | 2.15k | rgb.depth = 8; |
1097 | 2.15k | if (avifRGBImageAllocatePixels(&rgb) != AVIF_RESULT_OK) { |
1098 | 0 | fprintf(stderr, "Conversion to YUV failed: %s (out of memory)\n", inputFilename); |
1099 | 0 | goto cleanup; |
1100 | 0 | } |
1101 | | |
1102 | 2.15k | int row = 0; |
1103 | 27.1M | while (cinfo.output_scanline < cinfo.output_height) { |
1104 | 27.1M | jpeg_read_scanlines(&cinfo, buffer, 1); |
1105 | 27.1M | uint8_t * pixelRow = &rgb.pixels[row * rgb.rowBytes]; |
1106 | 27.1M | memcpy(pixelRow, buffer[0], rgb.rowBytes); |
1107 | 27.1M | ++row; |
1108 | 27.1M | } |
1109 | 2.15k | if (avifImageRGBToYUV(avif, &rgb) != AVIF_RESULT_OK) { |
1110 | 208 | fprintf(stderr, "Conversion to YUV failed: %s\n", inputFilename); |
1111 | 208 | goto cleanup; |
1112 | 208 | } |
1113 | 2.15k | } |
1114 | | |
1115 | 2.05k | if (!ignoreExif) { |
1116 | 1.12k | avifBool found = AVIF_FALSE; |
1117 | 2.86k | for (jpeg_saved_marker_ptr marker = cinfo.marker_list; marker != NULL; marker = marker->next) { |
1118 | 1.74k | if ((marker->marker == (JPEG_APP0 + 1)) && (marker->data_length > AVIF_JPEG_EXIF_HEADER_LENGTH) && |
1119 | 777 | !memcmp(marker->data, AVIF_JPEG_EXIF_HEADER, AVIF_JPEG_EXIF_HEADER_LENGTH)) { |
1120 | 174 | if (found) { |
1121 | | // TODO(yguyon): Implement instead of outputting an error. |
1122 | 0 | fprintf(stderr, "Exif extraction failed: unsupported Exif split into multiple segments or invalid multiple Exif segments\n"); |
1123 | 0 | goto cleanup; |
1124 | 0 | } |
1125 | | |
1126 | 174 | if (marker->data_length - AVIF_JPEG_EXIF_HEADER_LENGTH > sizeLimit) { |
1127 | 0 | fprintf(stderr, |
1128 | 0 | "Setting Exif metadata failed: Exif size is too large (%u > %u bytes): %s\n", |
1129 | 0 | marker->data_length - AVIF_JPEG_EXIF_HEADER_LENGTH, |
1130 | 0 | sizeLimit, |
1131 | 0 | inputFilename); |
1132 | 0 | goto cleanup; |
1133 | 0 | } |
1134 | | |
1135 | | // Exif orientation, if any, is imported to avif->irot/imir and kept in avif->exif. |
1136 | | // libheif has the same behavior, see |
1137 | | // https://github.com/strukturag/libheif/blob/ea78603d8e47096606813d221725621306789ff2/examples/heif_enc.cc#L403 |
1138 | 174 | if (avifImageSetMetadataExif(avif, |
1139 | 174 | marker->data + AVIF_JPEG_EXIF_HEADER_LENGTH, |
1140 | 174 | marker->data_length - AVIF_JPEG_EXIF_HEADER_LENGTH) != AVIF_RESULT_OK) { |
1141 | 0 | fprintf(stderr, "Setting Exif metadata failed: %s (out of memory)\n", inputFilename); |
1142 | 0 | goto cleanup; |
1143 | 0 | } |
1144 | 174 | found = AVIF_TRUE; |
1145 | 174 | } |
1146 | 1.74k | } |
1147 | 1.12k | } |
1148 | | |
1149 | 2.05k | avifBool readXMP = !ignoreXMP; |
1150 | | #if defined(AVIF_ENABLE_JPEG_GAIN_MAP_CONVERSION) |
1151 | | readXMP = readXMP || !ignoreGainMap; // Gain map metadata is in XMP. |
1152 | | #endif |
1153 | 2.05k | if (readXMP) { |
1154 | 1.13k | const uint8_t * standardXMPData = NULL; |
1155 | 1.13k | uint32_t standardXMPSize = 0; // At most 64kB as defined by Adobe XMP Specification Part 3. |
1156 | 2.70k | for (jpeg_saved_marker_ptr marker = cinfo.marker_list; marker != NULL; marker = marker->next) { |
1157 | 1.56k | if ((marker->marker == (JPEG_APP0 + 1)) && (marker->data_length > AVIF_JPEG_STANDARD_XMP_TAG_LENGTH) && |
1158 | 818 | !memcmp(marker->data, AVIF_JPEG_STANDARD_XMP_TAG, AVIF_JPEG_STANDARD_XMP_TAG_LENGTH)) { |
1159 | 95 | if (standardXMPData) { |
1160 | 1 | fprintf(stderr, "XMP extraction failed: invalid multiple standard XMP segments\n"); |
1161 | 1 | goto cleanup; |
1162 | 1 | } |
1163 | 94 | standardXMPData = marker->data + AVIF_JPEG_STANDARD_XMP_TAG_LENGTH; |
1164 | 94 | standardXMPSize = (uint32_t)(marker->data_length - AVIF_JPEG_STANDARD_XMP_TAG_LENGTH); |
1165 | 94 | } |
1166 | 1.56k | } |
1167 | | |
1168 | 1.13k | avifBool foundExtendedXMP = AVIF_FALSE; |
1169 | 1.13k | uint8_t extendedXMPGUID[AVIF_JPEG_EXTENDED_XMP_GUID_LENGTH]; // The value is common to all extended XMP segments. |
1170 | 2.62k | for (jpeg_saved_marker_ptr marker = cinfo.marker_list; marker != NULL; marker = marker->next) { |
1171 | 1.55k | if ((marker->marker == (JPEG_APP0 + 1)) && (marker->data_length > AVIF_JPEG_EXTENDED_XMP_TAG_LENGTH) && |
1172 | 778 | !memcmp(marker->data, AVIF_JPEG_EXTENDED_XMP_TAG, AVIF_JPEG_EXTENDED_XMP_TAG_LENGTH)) { |
1173 | 98 | if (!standardXMPData) { |
1174 | 8 | fprintf(stderr, "XMP extraction failed: extended XMP segment found, missing standard XMP segment\n"); |
1175 | 8 | goto cleanup; |
1176 | 8 | } |
1177 | | |
1178 | 90 | if (marker->data_length < AVIF_JPEG_OFFSET_TILL_EXTENDED_XMP) { |
1179 | 0 | fprintf(stderr, "XMP extraction failed: truncated extended XMP segment\n"); |
1180 | 0 | goto cleanup; |
1181 | 0 | } |
1182 | 90 | const uint8_t * guid = &marker->data[AVIF_JPEG_EXTENDED_XMP_TAG_LENGTH]; |
1183 | 2.43k | for (size_t c = 0; c < AVIF_JPEG_EXTENDED_XMP_GUID_LENGTH; ++c) { |
1184 | | // According to Adobe XMP Specification Part 3 section 1.1.3.1: |
1185 | | // "128-bit GUID stored as a 32-byte ASCII hex string, capital A-F, no null termination" |
1186 | | // Also allow lowercase since some cameras use lowercase. https://github.com/AOMediaCodec/libavif/issues/2755 |
1187 | 2.37k | if (!isxdigit(guid[c])) { |
1188 | 33 | fprintf(stderr, "XMP extraction failed: invalid XMP segment GUID\n"); |
1189 | 33 | goto cleanup; |
1190 | 33 | } |
1191 | 2.37k | } |
1192 | | // Size of the current extended segment. |
1193 | 57 | const size_t extendedXMPSize = marker->data_length - AVIF_JPEG_OFFSET_TILL_EXTENDED_XMP; |
1194 | | // Expected size of the sum of all extended segments. |
1195 | | // According to Adobe XMP Specification Part 3 section 1.1.3.1: |
1196 | | // "full length of the ExtendedXMP serialization as a 32-bit unsigned integer" |
1197 | 57 | const uint32_t totalExtendedXMPSize = |
1198 | 57 | avifJPEGReadUint32BigEndian(&marker->data[AVIF_JPEG_EXTENDED_XMP_TAG_LENGTH + AVIF_JPEG_EXTENDED_XMP_GUID_LENGTH]); |
1199 | | // Offset in totalXMP after standardXMP. |
1200 | | // According to Adobe XMP Specification Part 3 section 1.1.3.1: |
1201 | | // "offset of this portion as a 32-bit unsigned integer" |
1202 | 57 | const uint32_t extendedXMPOffset = avifJPEGReadUint32BigEndian( |
1203 | 57 | &marker->data[AVIF_JPEG_EXTENDED_XMP_TAG_LENGTH + AVIF_JPEG_EXTENDED_XMP_GUID_LENGTH + 4]); |
1204 | 57 | if (((uint64_t)standardXMPSize + totalExtendedXMPSize) > SIZE_MAX || |
1205 | 57 | ((uint64_t)standardXMPSize + totalExtendedXMPSize) > sizeLimit) { |
1206 | 12 | fprintf(stderr, |
1207 | 12 | "XMP extraction failed: total XMP size is too large (%u + %u > %u bytes): %s\n", |
1208 | 12 | standardXMPSize, |
1209 | 12 | totalExtendedXMPSize, |
1210 | 12 | sizeLimit, |
1211 | 12 | inputFilename); |
1212 | 12 | goto cleanup; |
1213 | 12 | } |
1214 | 45 | if ((extendedXMPSize == 0) || (((uint64_t)extendedXMPOffset + extendedXMPSize) > totalExtendedXMPSize)) { |
1215 | 6 | fprintf(stderr, "XMP extraction failed: invalid extended XMP segment size or offset\n"); |
1216 | 6 | goto cleanup; |
1217 | 6 | } |
1218 | 39 | if (foundExtendedXMP) { |
1219 | 14 | if (memcmp(guid, extendedXMPGUID, AVIF_JPEG_EXTENDED_XMP_GUID_LENGTH)) { |
1220 | 2 | fprintf(stderr, "XMP extraction failed: extended XMP segment GUID mismatch\n"); |
1221 | 2 | goto cleanup; |
1222 | 2 | } |
1223 | 12 | if (totalExtendedXMPSize != extendedXMP.size) { |
1224 | 1 | fprintf(stderr, "XMP extraction failed: extended XMP total size mismatch\n"); |
1225 | 1 | goto cleanup; |
1226 | 1 | } |
1227 | 25 | } else { |
1228 | 25 | memcpy(extendedXMPGUID, guid, AVIF_JPEG_EXTENDED_XMP_GUID_LENGTH); |
1229 | | |
1230 | | // Allocate the extended XMP and keep track of the bytes that were set. |
1231 | 25 | if (avifRWDataRealloc(&extendedXMP, (size_t)totalExtendedXMPSize) != AVIF_RESULT_OK || |
1232 | 25 | avifRWDataRealloc(&extendedXMPReadBytes, totalExtendedXMPSize) != AVIF_RESULT_OK) { |
1233 | 0 | fprintf(stderr, "XMP extraction failed: out of memory\n"); |
1234 | 0 | goto cleanup; |
1235 | 0 | } |
1236 | 25 | memset(extendedXMPReadBytes.data, 0, extendedXMPReadBytes.size); |
1237 | | |
1238 | 25 | foundExtendedXMP = AVIF_TRUE; |
1239 | 25 | } |
1240 | | // According to Adobe XMP Specification Part 3 section 1.1.3.1: |
1241 | | // "A robust JPEG reader should tolerate the marker segments in any order." |
1242 | 36 | memcpy(&extendedXMP.data[extendedXMPOffset], &marker->data[AVIF_JPEG_OFFSET_TILL_EXTENDED_XMP], extendedXMPSize); |
1243 | | |
1244 | | // Make sure no previously read data was overwritten by the current segment. |
1245 | 36 | if (memchr(&extendedXMPReadBytes.data[extendedXMPOffset], 1, extendedXMPSize)) { |
1246 | 1 | fprintf(stderr, "XMP extraction failed: overlapping extended XMP segments\n"); |
1247 | 1 | goto cleanup; |
1248 | 1 | } |
1249 | | // Keep track of the bytes that were set. |
1250 | 35 | memset(&extendedXMPReadBytes.data[extendedXMPOffset], 1, extendedXMPSize); |
1251 | 35 | } |
1252 | 1.55k | } |
1253 | | |
1254 | 1.07k | if (foundExtendedXMP) { |
1255 | | // Make sure there is no missing byte. |
1256 | 21 | if (memchr(extendedXMPReadBytes.data, 0, extendedXMPReadBytes.size)) { |
1257 | 5 | fprintf(stderr, "XMP extraction failed: missing extended XMP segments\n"); |
1258 | 5 | goto cleanup; |
1259 | 5 | } |
1260 | | |
1261 | | // According to Adobe XMP Specification Part 3 section 1.1.3.1: |
1262 | | // "A reader must incorporate only ExtendedXMP blocks whose GUID matches the value of xmpNote:HasExtendedXMP." |
1263 | 16 | uint8_t xmpNote[AVIF_JPEG_XMP_NOTE_TAG_LENGTH + AVIF_JPEG_EXTENDED_XMP_GUID_LENGTH]; |
1264 | 16 | memcpy(xmpNote, AVIF_JPEG_XMP_NOTE_TAG, AVIF_JPEG_XMP_NOTE_TAG_LENGTH); |
1265 | 16 | memcpy(xmpNote + AVIF_JPEG_XMP_NOTE_TAG_LENGTH, extendedXMPGUID, AVIF_JPEG_EXTENDED_XMP_GUID_LENGTH); |
1266 | 16 | avifBool foundAlternativeXMPNote; |
1267 | 16 | if (avifJPEGFindSubstr(standardXMPData, standardXMPSize, xmpNote, sizeof(xmpNote))) { |
1268 | 8 | foundAlternativeXMPNote = AVIF_FALSE; |
1269 | 8 | } else { |
1270 | | // Try the alternative before returning an error. |
1271 | 8 | uint8_t alternativeXmpNote[AVIF_JPEG_ALTERNATIVE_XMP_NOTE_TAG_LENGTH + AVIF_JPEG_EXTENDED_XMP_GUID_LENGTH]; |
1272 | 8 | memcpy(alternativeXmpNote, AVIF_JPEG_ALTERNATIVE_XMP_NOTE_TAG, AVIF_JPEG_ALTERNATIVE_XMP_NOTE_TAG_LENGTH); |
1273 | 8 | memcpy(alternativeXmpNote + AVIF_JPEG_ALTERNATIVE_XMP_NOTE_TAG_LENGTH, extendedXMPGUID, AVIF_JPEG_EXTENDED_XMP_GUID_LENGTH); |
1274 | 8 | if (!avifJPEGFindSubstr(standardXMPData, standardXMPSize, alternativeXmpNote, sizeof(alternativeXmpNote))) { |
1275 | 5 | fprintf(stderr, "XMP extraction failed: standard and extended XMP GUID mismatch\n"); |
1276 | 5 | goto cleanup; |
1277 | 5 | } |
1278 | 3 | foundAlternativeXMPNote = AVIF_TRUE; |
1279 | 3 | } |
1280 | 11 | (void)foundAlternativeXMPNote; |
1281 | | |
1282 | | #if defined(AVIF_ENABLE_JPEG_GAIN_MAP_CONVERSION) |
1283 | | if (!avifJPEGMergeXMP(standardXMPData, standardXMPSize, extendedXMP, foundAlternativeXMPNote, &avif->xmp)) { |
1284 | | goto cleanup; |
1285 | | } |
1286 | | #else |
1287 | 11 | fprintf(stderr, "WARNING: must be compiled with libxml2 to copy extended XMP properly\n"); |
1288 | 11 | avifRWDataFree(&avif->xmp); |
1289 | 11 | if (avifRWDataRealloc(&avif->xmp, (size_t)standardXMPSize + extendedXMP.size) != AVIF_RESULT_OK) { |
1290 | 0 | fprintf(stderr, "XMP copy failed: out of memory\n"); |
1291 | 0 | goto cleanup; |
1292 | 0 | } |
1293 | 11 | memcpy(avif->xmp.data, standardXMPData, standardXMPSize); |
1294 | 11 | memcpy(avif->xmp.data + standardXMPSize, extendedXMP.data, extendedXMP.size); |
1295 | 11 | #endif // AVIF_ENABLE_JPEG_GAIN_MAP_CONVERSION |
1296 | 1.05k | } else if (standardXMPData) { |
1297 | 17 | if (avifImageSetMetadataXMP(avif, standardXMPData, standardXMPSize) != AVIF_RESULT_OK) { |
1298 | 0 | fprintf(stderr, "XMP extraction failed: out of memory\n"); |
1299 | 0 | goto cleanup; |
1300 | 0 | } |
1301 | 17 | } |
1302 | 1.06k | avifImageFixXMP(avif); // Remove one trailing null character if any. |
1303 | 1.06k | } |
1304 | | |
1305 | | #if defined(AVIF_ENABLE_JPEG_GAIN_MAP_CONVERSION) |
1306 | | // The primary XMP block (for the main image) must contain a node with an hdrgm:Version field if and only if a gain map is present. |
1307 | | if (!ignoreGainMap && avifJPEGHasGainMapXMPNode(avif->xmp.data, avif->xmp.size)) { |
1308 | | avifGainMap * gainMap = avifGainMapCreate(); |
1309 | | if (gainMap == NULL) { |
1310 | | fprintf(stderr, "Creating gain map failed: out of memory\n"); |
1311 | | goto cleanup; |
1312 | | } |
1313 | | // Ignore the return value: continue even if we fail to find/parse/decode the gain map. |
1314 | | if (avifJPEGExtractGainMapImage(f, sizeLimit, &cinfo, gainMap, chromaDownsampling)) { |
1315 | | // Since jpeg doesn't provide this metadata, assume the values are the same as the base image |
1316 | | // with a PQ transfer curve. |
1317 | | gainMap->altColorPrimaries = avif->colorPrimaries; |
1318 | | gainMap->altTransferCharacteristics = AVIF_TRANSFER_CHARACTERISTICS_PQ; |
1319 | | gainMap->altMatrixCoefficients = avif->matrixCoefficients; |
1320 | | gainMap->altDepth = 8; |
1321 | | gainMap->altPlaneCount = |
1322 | | (avif->yuvFormat == AVIF_PIXEL_FORMAT_YUV400 && gainMap->image->yuvFormat == AVIF_PIXEL_FORMAT_YUV400) ? 1 : 3; |
1323 | | if (avif->icc.size > 0) { |
1324 | | // The base image's ICC should also apply to the alternage image. |
1325 | | if (avifRWDataSet(&gainMap->altICC, avif->icc.data, avif->icc.size) != AVIF_RESULT_OK) { |
1326 | | fprintf(stderr, "Setting gain map ICC profile failed: out of memory\n"); |
1327 | | goto cleanup; |
1328 | | } |
1329 | | } |
1330 | | avif->gainMap = gainMap; |
1331 | | } else { |
1332 | | avifGainMapDestroy(gainMap); |
1333 | | } |
1334 | | } |
1335 | | |
1336 | | if (avif->xmp.size > 0 && ignoreXMP) { |
1337 | | // Clear XMP in case we read it for something else (like gain map). |
1338 | | if (avifImageSetMetadataXMP(avif, NULL, 0) != AVIF_RESULT_OK) { |
1339 | | assert(AVIF_FALSE); |
1340 | | } |
1341 | | } |
1342 | | #endif // AVIF_ENABLE_JPEG_GAIN_MAP_CONVERSION |
1343 | 1.97k | jpeg_finish_decompress(&cinfo); |
1344 | 1.97k | ret = AVIF_TRUE; |
1345 | 4.99k | cleanup: |
1346 | 4.99k | jpeg_destroy_decompress(&cinfo); |
1347 | 4.99k | free(iccData); |
1348 | 4.99k | avifRGBImageFreePixels(&rgb); |
1349 | 4.99k | avifRWDataFree(&extendedXMP); |
1350 | 4.99k | avifRWDataFree(&extendedXMPReadBytes); |
1351 | 4.99k | return ret; |
1352 | 1.97k | } |
1353 | | |
1354 | | avifBool avifJPEGRead(const char * inputFilename, |
1355 | | avifImage * avif, |
1356 | | avifPixelFormat requestedFormat, |
1357 | | uint32_t requestedDepth, |
1358 | | avifChromaDownsampling chromaDownsampling, |
1359 | | avifBool ignoreColorProfile, |
1360 | | avifBool ignoreExif, |
1361 | | avifBool ignoreXMP, |
1362 | | avifBool ignoreGainMap, |
1363 | | uint32_t sizeLimit) |
1364 | 4.99k | { |
1365 | 4.99k | FILE * f; |
1366 | 4.99k | if (inputFilename) { |
1367 | 4.99k | f = fopen(inputFilename, "rb"); |
1368 | 4.99k | if (!f) { |
1369 | 0 | fprintf(stderr, "Can't open JPEG file for read: %s\n", inputFilename); |
1370 | 0 | return AVIF_FALSE; |
1371 | 0 | } |
1372 | 4.99k | } else { |
1373 | 0 | f = stdin; |
1374 | 0 | inputFilename = "(stdin)"; |
1375 | 0 | } |
1376 | 4.99k | const avifBool res = avifJPEGReadInternal(f, |
1377 | 4.99k | inputFilename, |
1378 | 4.99k | avif, |
1379 | 4.99k | requestedFormat, |
1380 | 4.99k | requestedDepth, |
1381 | 4.99k | chromaDownsampling, |
1382 | 4.99k | ignoreColorProfile, |
1383 | 4.99k | ignoreExif, |
1384 | 4.99k | ignoreXMP, |
1385 | 4.99k | ignoreGainMap, |
1386 | 4.99k | sizeLimit); |
1387 | 4.99k | if (f && f != stdin) { |
1388 | 4.99k | fclose(f); |
1389 | 4.99k | } |
1390 | 4.99k | return res; |
1391 | 4.99k | } |
1392 | | |
1393 | | avifBool avifJPEGWrite(const char * outputFilename, const avifImage * avif, int jpegQuality, avifChromaUpsampling chromaUpsampling) |
1394 | 0 | { |
1395 | 0 | avifBool ret = AVIF_FALSE; |
1396 | 0 | FILE * f = NULL; |
1397 | |
|
1398 | 0 | struct jpeg_compress_struct cinfo; |
1399 | 0 | struct jpeg_error_mgr jerr; |
1400 | 0 | JSAMPROW row_pointer[1]; |
1401 | 0 | cinfo.err = jpeg_std_error(&jerr); |
1402 | 0 | jpeg_create_compress(&cinfo); |
1403 | |
|
1404 | 0 | avifRGBImage rgb; |
1405 | 0 | avifRGBImageSetDefaults(&rgb, avif); |
1406 | 0 | rgb.format = avif->yuvFormat == AVIF_PIXEL_FORMAT_YUV400 ? AVIF_RGB_FORMAT_GRAY : AVIF_RGB_FORMAT_RGB; |
1407 | 0 | rgb.chromaUpsampling = chromaUpsampling; |
1408 | 0 | rgb.depth = 8; |
1409 | 0 | if (avifRGBImageAllocatePixels(&rgb) != AVIF_RESULT_OK) { |
1410 | 0 | fprintf(stderr, "Conversion to RGB failed: %s (out of memory)\n", outputFilename); |
1411 | 0 | goto cleanup; |
1412 | 0 | } |
1413 | 0 | if (avifImageYUVToRGB(avif, &rgb) != AVIF_RESULT_OK) { |
1414 | 0 | fprintf(stderr, "Conversion to RGB failed: %s\n", outputFilename); |
1415 | 0 | goto cleanup; |
1416 | 0 | } |
1417 | | |
1418 | 0 | f = fopen(outputFilename, "wb"); |
1419 | 0 | if (!f) { |
1420 | 0 | fprintf(stderr, "Can't open JPEG file for write: %s\n", outputFilename); |
1421 | 0 | goto cleanup; |
1422 | 0 | } |
1423 | | |
1424 | 0 | jpeg_stdio_dest(&cinfo, f); |
1425 | 0 | cinfo.image_width = avif->width; |
1426 | 0 | cinfo.image_height = avif->height; |
1427 | 0 | const avifBool isGray = avif->yuvFormat == AVIF_PIXEL_FORMAT_YUV400; |
1428 | 0 | cinfo.input_components = isGray ? 1 : 3; |
1429 | 0 | cinfo.in_color_space = isGray ? JCS_GRAYSCALE : JCS_RGB; |
1430 | 0 | jpeg_set_defaults(&cinfo); |
1431 | 0 | jpeg_set_quality(&cinfo, jpegQuality, TRUE); |
1432 | 0 | jpeg_start_compress(&cinfo, TRUE); |
1433 | |
|
1434 | 0 | if (avif->icc.data && (avif->icc.size > 0)) { |
1435 | | // TODO(yguyon): Use jpeg_write_icc_profile() instead? |
1436 | 0 | write_icc_profile(&cinfo, avif->icc.data, (unsigned int)avif->icc.size); |
1437 | 0 | } |
1438 | |
|
1439 | 0 | if (avif->transformFlags & AVIF_TRANSFORM_CLAP) { |
1440 | 0 | avifCropRect cropRect; |
1441 | 0 | avifDiagnostics diag; |
1442 | 0 | if (avifCropRectFromCleanApertureBox(&cropRect, &avif->clap, avif->width, avif->height, &diag) && |
1443 | 0 | (cropRect.x != 0 || cropRect.y != 0 || cropRect.width != avif->width || cropRect.height != avif->height)) { |
1444 | | // TODO: https://github.com/AOMediaCodec/libavif/issues/2427 - Implement. |
1445 | 0 | fprintf(stderr, |
1446 | 0 | "Warning: Clean Aperture values were ignored, the output image was NOT cropped to rectangle {%u,%u,%u,%u}\n", |
1447 | 0 | cropRect.x, |
1448 | 0 | cropRect.y, |
1449 | 0 | cropRect.width, |
1450 | 0 | cropRect.height); |
1451 | 0 | } |
1452 | 0 | } |
1453 | |
|
1454 | 0 | if (avif->exif.data && (avif->exif.size > 0)) { |
1455 | 0 | size_t exifTiffHeaderOffset; |
1456 | 0 | avifResult result = avifGetExifTiffHeaderOffset(avif->exif.data, avif->exif.size, &exifTiffHeaderOffset); |
1457 | 0 | if (result != AVIF_RESULT_OK) { |
1458 | 0 | fprintf(stderr, "Error writing JPEG metadata: %s\n", avifResultToString(result)); |
1459 | 0 | goto cleanup; |
1460 | 0 | } |
1461 | | |
1462 | 0 | avifRWData exif = { NULL, 0 }; |
1463 | 0 | if (avifRWDataRealloc(&exif, AVIF_JPEG_EXIF_HEADER_LENGTH + avif->exif.size - exifTiffHeaderOffset) != AVIF_RESULT_OK) { |
1464 | 0 | fprintf(stderr, "Error writing JPEG metadata: out of memory\n"); |
1465 | 0 | goto cleanup; |
1466 | 0 | } |
1467 | 0 | memcpy(exif.data, AVIF_JPEG_EXIF_HEADER, AVIF_JPEG_EXIF_HEADER_LENGTH); |
1468 | 0 | memcpy(exif.data + AVIF_JPEG_EXIF_HEADER_LENGTH, avif->exif.data + exifTiffHeaderOffset, avif->exif.size - exifTiffHeaderOffset); |
1469 | | // Make sure the Exif orientation matches the irot/imir values. |
1470 | | // libheif does not have the same behavior. The orientation is applied to samples and orientation data is discarded there, |
1471 | | // see https://github.com/strukturag/libheif/blob/ea78603d8e47096606813d221725621306789ff2/examples/encoder_jpeg.cc#L187 |
1472 | 0 | const uint8_t orientation = avifImageGetExifOrientationFromIrotImir(avif); |
1473 | 0 | result = avifSetExifOrientation(&exif, orientation); |
1474 | 0 | if (result != AVIF_RESULT_OK) { |
1475 | | // Ignore errors if the orientation is the default one because not being able to set Exif orientation now |
1476 | | // means a reader will not be able to parse it later either. |
1477 | 0 | if (orientation != 1) { |
1478 | 0 | fprintf(stderr, "Error writing JPEG metadata: %s\n", avifResultToString(result)); |
1479 | 0 | avifRWDataFree(&exif); |
1480 | 0 | goto cleanup; |
1481 | 0 | } |
1482 | 0 | } |
1483 | | |
1484 | 0 | avifROData remainingExif = { exif.data, exif.size }; |
1485 | 0 | while (remainingExif.size > AVIF_JPEG_MAX_MARKER_DATA_LENGTH) { |
1486 | 0 | jpeg_write_marker(&cinfo, JPEG_APP0 + 1, remainingExif.data, AVIF_JPEG_MAX_MARKER_DATA_LENGTH); |
1487 | 0 | remainingExif.data += AVIF_JPEG_MAX_MARKER_DATA_LENGTH; |
1488 | 0 | remainingExif.size -= AVIF_JPEG_MAX_MARKER_DATA_LENGTH; |
1489 | 0 | } |
1490 | 0 | jpeg_write_marker(&cinfo, JPEG_APP0 + 1, remainingExif.data, (unsigned int)remainingExif.size); |
1491 | 0 | avifRWDataFree(&exif); |
1492 | 0 | } else if (avifImageGetExifOrientationFromIrotImir(avif) != 1) { |
1493 | | // There is no Exif yet, but we need to store the orientation. |
1494 | | // TODO: https://github.com/AOMediaCodec/libavif/issues/2427 - Add a valid Exif payload or rotate the samples. |
1495 | 0 | fprintf(stderr, |
1496 | 0 | "Warning: Orientation %u was ignored, the output image was NOT rotated or mirrored\n", |
1497 | 0 | avifImageGetExifOrientationFromIrotImir(avif)); |
1498 | 0 | } |
1499 | | |
1500 | 0 | if (avif->xmp.data && (avif->xmp.size > 0)) { |
1501 | | // See XMP specification part 3. |
1502 | 0 | if (avif->xmp.size > 65502) { |
1503 | | // libheif just refuses to export JPEG with long XMP, see |
1504 | | // https://github.com/strukturag/libheif/blob/18291ddebc23c924440a8a3c9a7267fe3beb5901/examples/encoder_jpeg.cc#L227 |
1505 | | // But libheif also ignores extended XMP at reading, so converting a JPEG with extended XMP to HEIC and back to JPEG |
1506 | | // works, with the extended XMP part dropped, even if it had fit into a single JPEG marker. |
1507 | | |
1508 | | // In libavif the whole XMP payload is dropped if it exceeds a single JPEG marker size limit, with a warning. |
1509 | | // The advantage is that it keeps the whole XMP payload, including the extended part, if it fits into a single JPEG |
1510 | | // marker. This is acceptable because section 1.1.3.1 of XMP specification part 3 says |
1511 | | // "It is unusual for XMP to exceed 65502 bytes; typically, it is around 2 KB." |
1512 | 0 | fprintf(stderr, "Warning writing JPEG metadata: XMP payload is too big and was dropped\n"); |
1513 | 0 | } else { |
1514 | 0 | avifRWData xmp = { NULL, 0 }; |
1515 | 0 | if (avifRWDataRealloc(&xmp, AVIF_JPEG_STANDARD_XMP_TAG_LENGTH + avif->xmp.size) != AVIF_RESULT_OK) { |
1516 | 0 | fprintf(stderr, "Error writing JPEG metadata: out of memory\n"); |
1517 | 0 | goto cleanup; |
1518 | 0 | } |
1519 | 0 | memcpy(xmp.data, AVIF_JPEG_STANDARD_XMP_TAG, AVIF_JPEG_STANDARD_XMP_TAG_LENGTH); |
1520 | 0 | memcpy(xmp.data + AVIF_JPEG_STANDARD_XMP_TAG_LENGTH, avif->xmp.data, avif->xmp.size); |
1521 | 0 | jpeg_write_marker(&cinfo, JPEG_APP0 + 1, xmp.data, (unsigned int)xmp.size); |
1522 | 0 | avifRWDataFree(&xmp); |
1523 | 0 | } |
1524 | 0 | } |
1525 | | |
1526 | 0 | while (cinfo.next_scanline < cinfo.image_height) { |
1527 | 0 | row_pointer[0] = &rgb.pixels[cinfo.next_scanline * rgb.rowBytes]; |
1528 | 0 | (void)jpeg_write_scanlines(&cinfo, row_pointer, 1); |
1529 | 0 | } |
1530 | |
|
1531 | 0 | jpeg_finish_compress(&cinfo); |
1532 | 0 | ret = AVIF_TRUE; |
1533 | 0 | printf("Wrote JPEG: %s\n", outputFilename); |
1534 | 0 | cleanup: |
1535 | 0 | if (f) { |
1536 | 0 | fclose(f); |
1537 | 0 | } |
1538 | 0 | jpeg_destroy_compress(&cinfo); |
1539 | 0 | avifRGBImageFreePixels(&rgb); |
1540 | 0 | return ret; |
1541 | 0 | } |