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