Commit | Line | Data |
---|---|---|
f7450478 TC |
1 | #include "imexif.h" |
2 | #include <stdlib.h> | |
3 | #include <float.h> | |
4 | ||
5 | /* | |
6 | =head1 NAME | |
7 | ||
8 | imexif.c - EXIF support for Imager | |
9 | ||
10 | =head1 SYNOPSIS | |
11 | ||
12 | if (i_int_decode_exif(im, app1data, app1datasize)) { | |
13 | // exif block seen | |
14 | } | |
15 | ||
16 | =head1 DESCRIPTION | |
17 | ||
18 | This code provides a basic EXIF data decoder. It is intended to be | |
19 | called from the JPEG reader code when an APP1 data block is found, and | |
20 | will set tags in the supplied image. | |
21 | ||
22 | =cut | |
23 | */ | |
24 | ||
25 | typedef enum tiff_type_tag { | |
26 | tt_intel = 'I', | |
27 | tt_motorola = 'M' | |
28 | } tiff_type; | |
29 | ||
30 | typedef enum { | |
31 | ift_byte = 1, | |
32 | ift_ascii = 2, | |
33 | ift_short = 3, | |
34 | ift_long = 4, | |
35 | ift_rational = 5, | |
36 | ift_sbyte = 6, | |
37 | ift_undefined = 7, | |
38 | ift_sshort = 8, | |
39 | ift_slong = 9, | |
40 | ift_srational = 10, | |
41 | ift_float = 11, | |
42 | ift_double = 12, | |
43 | ift_last = 12 /* keep the same as the highest type code */ | |
44 | } ifd_entry_type; | |
45 | ||
46 | static int type_sizes[] = | |
47 | { | |
48 | 0, /* not used */ | |
49 | 1, /* byte */ | |
50 | 1, /* ascii */ | |
51 | 2, /* short */ | |
52 | 4, /* long */ | |
53 | 8, /* rational */ | |
54 | 1, /* sbyte */ | |
55 | 1, /* undefined */ | |
56 | 2, /* sshort */ | |
57 | 4, /* slong */ | |
58 | 8, /* srational */ | |
59 | 4, /* float */ | |
60 | 8, /* double */ | |
61 | }; | |
62 | ||
63 | typedef struct { | |
64 | int tag; | |
65 | int type; | |
66 | int count; | |
59957854 | 67 | int item_size; |
f7450478 TC |
68 | int size; |
69 | int offset; | |
70 | } ifd_entry; | |
71 | ||
72 | typedef struct { | |
73 | int tag; | |
74 | char const *name; | |
75 | } tag_map; | |
76 | ||
77 | typedef struct { | |
78 | int tag; | |
79 | char const *name; | |
80 | tag_map const *map; | |
81 | int map_count; | |
82 | } tag_value_map; | |
83 | ||
84 | #define PASTE(left, right) PASTE_(left, right) | |
85 | #define PASTE_(left, right) left##right | |
86 | #define QUOTE(value) #value | |
87 | ||
88 | #define VALUE_MAP_ENTRY(name) \ | |
89 | { \ | |
90 | PASTE(tag_, name), \ | |
91 | "exif_" QUOTE(name) "_name", \ | |
92 | PASTE(name, _values), \ | |
93 | ARRAY_COUNT(PASTE(name, _values)) \ | |
94 | } | |
95 | ||
96 | /* we don't process every tag */ | |
97 | #define tag_make 271 | |
98 | #define tag_model 272 | |
99 | #define tag_orientation 274 | |
100 | #define tag_x_resolution 282 | |
101 | #define tag_y_resolution 283 | |
102 | #define tag_resolution_unit 296 | |
103 | #define tag_copyright 33432 | |
104 | #define tag_software 305 | |
105 | #define tag_artist 315 | |
106 | #define tag_date_time 306 | |
107 | #define tag_image_description 270 | |
108 | ||
109 | #define tag_exif_ifd 34665 | |
110 | #define tag_gps_ifd 34853 | |
111 | ||
112 | #define resunit_none 1 | |
113 | #define resunit_inch 2 | |
114 | #define resunit_centimeter 3 | |
115 | ||
116 | /* tags from the EXIF ifd */ | |
117 | #define tag_exif_version 0x9000 | |
118 | #define tag_flashpix_version 0xA000 | |
119 | #define tag_color_space 0xA001 | |
120 | #define tag_component_configuration 0x9101 | |
121 | #define tag_component_bits_per_pixel 0x9102 | |
122 | #define tag_pixel_x_dimension 0xA002 | |
123 | #define tag_pixel_y_dimension 0xA003 | |
124 | #define tag_maker_note 0x927C | |
125 | #define tag_user_comment 0x9286 | |
126 | #define tag_related_sound_file 0xA004 | |
127 | #define tag_date_time_original 0x9003 | |
128 | #define tag_date_time_digitized 0x9004 | |
129 | #define tag_sub_sec_time 0x9290 | |
130 | #define tag_sub_sec_time_original 0x9291 | |
131 | #define tag_sub_sec_time_digitized 0x9292 | |
132 | #define tag_image_unique_id 0xA420 | |
133 | #define tag_exposure_time 0x829a | |
134 | #define tag_f_number 0x829D | |
135 | #define tag_exposure_program 0x8822 | |
136 | #define tag_spectral_sensitivity 0x8824 | |
59957854 | 137 | #define tag_iso_speed_ratings 0x8827 |
f7450478 TC |
138 | #define tag_oecf 0x8828 |
139 | #define tag_shutter_speed 0x9201 | |
140 | #define tag_aperture 0x9202 | |
141 | #define tag_brightness 0x9203 | |
142 | #define tag_exposure_bias 0x9204 | |
143 | #define tag_max_aperture 0x9205 | |
144 | #define tag_subject_distance 0x9206 | |
145 | #define tag_metering_mode 0x9207 | |
146 | #define tag_light_source 0x9208 | |
147 | #define tag_flash 0x9209 | |
148 | #define tag_focal_length 0x920a | |
149 | #define tag_subject_area 0x9214 | |
150 | #define tag_flash_energy 0xA20B | |
151 | #define tag_spatial_frequency_response 0xA20C | |
152 | #define tag_focal_plane_x_resolution 0xA20e | |
153 | #define tag_focal_plane_y_resolution 0xA20F | |
154 | #define tag_focal_plane_resolution_unit 0xA210 | |
155 | #define tag_subject_location 0xA214 | |
156 | #define tag_exposure_index 0xA215 | |
157 | #define tag_sensing_method 0xA217 | |
158 | #define tag_file_source 0xA300 | |
159 | #define tag_scene_type 0xA301 | |
160 | #define tag_cfa_pattern 0xA302 | |
161 | #define tag_custom_rendered 0xA401 | |
162 | #define tag_exposure_mode 0xA402 | |
163 | #define tag_white_balance 0xA403 | |
164 | #define tag_digital_zoom_ratio 0xA404 | |
165 | #define tag_focal_length_in_35mm_film 0xA405 | |
166 | #define tag_scene_capture_type 0xA406 | |
167 | #define tag_gain_control 0xA407 | |
168 | #define tag_contrast 0xA408 | |
169 | #define tag_saturation 0xA409 | |
170 | #define tag_sharpness 0xA40A | |
171 | #define tag_device_setting_description 0xA40B | |
172 | #define tag_subject_distance_range 0xA40C | |
173 | ||
59957854 TC |
174 | /* GPS tags */ |
175 | #define tag_gps_version_id 0 | |
176 | #define tag_gps_latitude_ref 1 | |
177 | #define tag_gps_latitude 2 | |
178 | #define tag_gps_longitude_ref 3 | |
179 | #define tag_gps_longitude 4 | |
180 | #define tag_gps_altitude_ref 5 | |
181 | #define tag_gps_altitude 6 | |
182 | #define tag_gps_time_stamp 7 | |
183 | #define tag_gps_satellites 8 | |
184 | #define tag_gps_status 9 | |
185 | #define tag_gps_measure_mode 10 | |
186 | #define tag_gps_dop 11 | |
187 | #define tag_gps_speed_ref 12 | |
188 | #define tag_gps_speed 13 | |
189 | #define tag_gps_track_ref 14 | |
190 | #define tag_gps_track 15 | |
191 | #define tag_gps_img_direction_ref 16 | |
192 | #define tag_gps_img_direction 17 | |
193 | #define tag_gps_map_datum 18 | |
194 | #define tag_gps_dest_latitude_ref 19 | |
195 | #define tag_gps_dest_latitude 20 | |
196 | #define tag_gps_dest_longitude_ref 21 | |
197 | #define tag_gps_dest_longitude 22 | |
198 | #define tag_gps_dest_bearing_ref 23 | |
199 | #define tag_gps_dest_bearing 24 | |
200 | #define tag_gps_dest_distance_ref 25 | |
201 | #define tag_gps_dest_distance 26 | |
202 | #define tag_gps_processing_method 27 | |
203 | #define tag_gps_area_information 28 | |
204 | #define tag_gps_date_stamp 29 | |
205 | #define tag_gps_differential 30 | |
206 | ||
f7450478 TC |
207 | /* don't use this on pointers */ |
208 | #define ARRAY_COUNT(array) (sizeof(array)/sizeof(*array)) | |
209 | ||
210 | /* in memory tiff structure */ | |
211 | typedef struct { | |
212 | /* the data we use as a tiff */ | |
213 | unsigned char *base; | |
214 | size_t size; | |
215 | ||
216 | /* intel or motorola byte order */ | |
217 | tiff_type type; | |
218 | ||
219 | /* initial ifd offset */ | |
220 | unsigned long first_ifd_offset; | |
221 | ||
222 | /* size (in entries) and data */ | |
223 | int ifd_size; | |
224 | ifd_entry *ifd; | |
225 | unsigned long next_ifd; | |
226 | } imtiff; | |
227 | ||
228 | static int tiff_init(imtiff *tiff, unsigned char *base, size_t length); | |
229 | static int tiff_load_ifd(imtiff *tiff, unsigned long offset); | |
230 | static void tiff_final(imtiff *tiff); | |
231 | static void tiff_clear_ifd(imtiff *tiff); | |
e4bf9335 | 232 | #if 0 /* currently unused, but that may change */ |
f7450478 TC |
233 | static int tiff_get_bytes(imtiff *tiff, unsigned char *to, size_t offset, |
234 | size_t count); | |
e4bf9335 | 235 | #endif |
f7450478 TC |
236 | static int tiff_get_tag_double(imtiff *, int index, double *result); |
237 | static int tiff_get_tag_int(imtiff *, int index, int *result); | |
238 | static unsigned tiff_get16(imtiff *, unsigned long offset); | |
239 | static unsigned tiff_get32(imtiff *, unsigned long offset); | |
240 | static int tiff_get16s(imtiff *, unsigned long offset); | |
241 | static int tiff_get32s(imtiff *, unsigned long offset); | |
242 | static double tiff_get_rat(imtiff *, unsigned long offset); | |
243 | static double tiff_get_rats(imtiff *, unsigned long offset); | |
59957854 | 244 | static void save_ifd0_tags(i_img *im, imtiff *tiff, unsigned long *exif_ifd_offset, unsigned long *gps_ifd_offset); |
f7450478 | 245 | static void save_exif_ifd_tags(i_img *im, imtiff *tiff); |
59957854 | 246 | static void save_gps_ifd_tags(i_img *im, imtiff *tiff); |
f7450478 TC |
247 | static void |
248 | copy_string_tags(i_img *im, imtiff *tiff, tag_map *map, int map_count); | |
249 | static void | |
250 | copy_int_tags(i_img *im, imtiff *tiff, tag_map *map, int map_count); | |
251 | static void | |
252 | copy_rat_tags(i_img *im, imtiff *tiff, tag_map *map, int map_count); | |
253 | static void | |
59957854 TC |
254 | copy_num_array_tags(i_img *im, imtiff *tiff, tag_map *map, int map_count); |
255 | static void | |
f7450478 TC |
256 | copy_name_tags(i_img *im, imtiff *tiff, tag_value_map *map, int map_count); |
257 | static void process_maker_note(i_img *im, imtiff *tiff, unsigned long offset, size_t size); | |
258 | ||
259 | /* | |
260 | =head1 PUBLIC FUNCTIONS | |
261 | ||
262 | These functions are available to other parts of Imager. They aren't | |
263 | intended to be called from outside of Imager. | |
264 | ||
265 | =over | |
266 | ||
267 | =item i_int_decode_exit | |
268 | ||
269 | i_int_decode_exif(im, data_base, data_size); | |
270 | ||
271 | The data from data_base for data_size bytes will be scanned for EXIF | |
272 | data. | |
273 | ||
274 | Any data found will be used to set tags in the supplied image. | |
275 | ||
276 | The intent is that invalid EXIF data will simply fail to set tags, and | |
277 | write to the log. In no case should this code exit when supplied | |
278 | invalid data. | |
279 | ||
280 | Returns true if an Exif header was seen. | |
281 | ||
282 | */ | |
283 | ||
284 | int | |
285 | i_int_decode_exif(i_img *im, unsigned char *data, size_t length) { | |
286 | imtiff tiff; | |
287 | unsigned long exif_ifd_offset = 0; | |
59957854 | 288 | unsigned long gps_ifd_offset = 0; |
f7450478 TC |
289 | /* basic checks - must start with "Exif\0\0" */ |
290 | ||
291 | if (length < 6 || memcmp(data, "Exif\0\0", 6) != 0) { | |
292 | return 0; | |
293 | } | |
294 | ||
295 | data += 6; | |
296 | length -= 6; | |
297 | ||
298 | if (!tiff_init(&tiff, data, length)) { | |
299 | mm_log((2, "Exif header found, but no valid TIFF header\n")); | |
300 | return 1; | |
301 | } | |
302 | if (!tiff_load_ifd(&tiff, tiff.first_ifd_offset)) { | |
303 | mm_log((2, "Exif header found, but could not load IFD 0\n")); | |
304 | tiff_final(&tiff); | |
305 | return 1; | |
306 | } | |
307 | ||
59957854 | 308 | save_ifd0_tags(im, &tiff, &exif_ifd_offset, &gps_ifd_offset); |
f7450478 TC |
309 | |
310 | if (exif_ifd_offset) { | |
311 | if (tiff_load_ifd(&tiff, exif_ifd_offset)) { | |
312 | save_exif_ifd_tags(im, &tiff); | |
313 | } | |
314 | else { | |
315 | mm_log((2, "Could not load Exif IFD\n")); | |
316 | } | |
317 | } | |
318 | ||
59957854 TC |
319 | if (gps_ifd_offset) { |
320 | if (tiff_load_ifd(&tiff, gps_ifd_offset)) { | |
321 | save_gps_ifd_tags(im, &tiff); | |
322 | } | |
323 | else { | |
324 | mm_log((2, "Could not load GPS IFD\n")); | |
325 | } | |
326 | } | |
327 | ||
f7450478 TC |
328 | tiff_final(&tiff); |
329 | ||
330 | return 1; | |
331 | } | |
332 | ||
333 | /* | |
334 | ||
335 | =back | |
336 | ||
337 | =head1 INTERNAL FUNCTIONS | |
338 | ||
339 | =head2 EXIF Processing | |
340 | ||
341 | =over | |
342 | ||
343 | =item save_ifd0_tags | |
344 | ||
59957854 | 345 | save_ifd0_tags(im, tiff, &exif_ifd_offset, &gps_ifd_offset) |
f7450478 TC |
346 | |
347 | Scans the currently loaded IFD for tags expected in IFD0 and sets them | |
348 | in the image. | |
349 | ||
350 | Sets *exif_ifd_offset to the offset of the EXIF IFD if found. | |
351 | ||
352 | =cut | |
353 | ||
354 | */ | |
355 | ||
356 | static tag_map ifd0_string_tags[] = | |
357 | { | |
af070d99 TC |
358 | { tag_make, "exif_make" }, |
359 | { tag_model, "exif_model" }, | |
360 | { tag_copyright, "exif_copyright" }, | |
361 | { tag_software, "exif_software" }, | |
362 | { tag_artist, "exif_artist" }, | |
363 | { tag_date_time, "exif_date_time" }, | |
364 | { tag_image_description, "exif_image_description" }, | |
f7450478 TC |
365 | }; |
366 | ||
367 | static const int ifd0_string_tag_count = ARRAY_COUNT(ifd0_string_tags); | |
368 | ||
369 | static tag_map ifd0_int_tags[] = | |
370 | { | |
371 | { tag_orientation, "exif_orientation", }, | |
372 | { tag_resolution_unit, "exif_resolution_unit" }, | |
373 | }; | |
374 | ||
375 | static const int ifd0_int_tag_count = ARRAY_COUNT(ifd0_int_tags); | |
376 | ||
377 | static tag_map ifd0_rat_tags[] = | |
378 | { | |
379 | { tag_x_resolution, "exif_x_resolution" }, | |
380 | { tag_y_resolution, "exif_y_resolution" }, | |
381 | }; | |
382 | ||
383 | static tag_map resolution_unit_values[] = | |
384 | { | |
385 | { 1, "none" }, | |
386 | { 2, "inches" }, | |
387 | { 3, "centimeters" }, | |
388 | }; | |
389 | ||
390 | static tag_value_map ifd0_values[] = | |
391 | { | |
392 | VALUE_MAP_ENTRY(resolution_unit), | |
393 | }; | |
394 | ||
395 | static void | |
59957854 TC |
396 | save_ifd0_tags(i_img *im, imtiff *tiff, unsigned long *exif_ifd_offset, |
397 | unsigned long *gps_ifd_offset) { | |
af070d99 | 398 | int tag_index; |
f7450478 TC |
399 | int work; |
400 | ifd_entry *entry; | |
401 | ||
402 | for (tag_index = 0, entry = tiff->ifd; | |
403 | tag_index < tiff->ifd_size; ++tag_index, ++entry) { | |
404 | switch (entry->tag) { | |
405 | case tag_exif_ifd: | |
406 | if (tiff_get_tag_int(tiff, tag_index, &work)) | |
407 | *exif_ifd_offset = work; | |
408 | break; | |
59957854 TC |
409 | |
410 | case tag_gps_ifd: | |
411 | if (tiff_get_tag_int(tiff, tag_index, &work)) | |
412 | *gps_ifd_offset = work; | |
413 | break; | |
f7450478 TC |
414 | } |
415 | } | |
416 | ||
417 | copy_string_tags(im, tiff, ifd0_string_tags, ifd0_string_tag_count); | |
418 | copy_int_tags(im, tiff, ifd0_int_tags, ifd0_int_tag_count); | |
419 | copy_rat_tags(im, tiff, ifd0_rat_tags, ARRAY_COUNT(ifd0_rat_tags)); | |
420 | copy_name_tags(im, tiff, ifd0_values, ARRAY_COUNT(ifd0_values)); | |
59957854 | 421 | /* copy_num_array_tags(im, tiff, ifd0_num_arrays, ARRAY_COUNT(ifd0_num_arrays)); */ |
f7450478 TC |
422 | } |
423 | ||
424 | /* | |
425 | =item save_exif_ifd_tags | |
426 | ||
427 | save_exif_ifd_tags(im, tiff) | |
428 | ||
429 | Scans the currently loaded IFD for the tags expected in the EXIF IFD | |
430 | and sets them as tags in the image. | |
431 | ||
432 | =cut | |
433 | ||
434 | */ | |
435 | ||
436 | static tag_map exif_ifd_string_tags[] = | |
437 | { | |
438 | { tag_exif_version, "exif_version", }, | |
439 | { tag_flashpix_version, "exif_flashpix_version", }, | |
440 | { tag_related_sound_file, "exif_related_sound_file", }, | |
441 | { tag_date_time_original, "exif_date_time_original", }, | |
442 | { tag_date_time_digitized, "exif_date_time_digitized", }, | |
443 | { tag_sub_sec_time, "exif_sub_sec_time" }, | |
444 | { tag_sub_sec_time_original, "exif_sub_sec_time_original" }, | |
445 | { tag_sub_sec_time_digitized, "exif_sub_sec_time_digitized" }, | |
446 | { tag_image_unique_id, "exif_image_unique_id" }, | |
447 | { tag_spectral_sensitivity, "exif_spectral_sensitivity" }, | |
448 | }; | |
449 | ||
450 | static const int exif_ifd_string_tag_count = ARRAY_COUNT(exif_ifd_string_tags); | |
451 | ||
452 | static tag_map exif_ifd_int_tags[] = | |
453 | { | |
454 | { tag_color_space, "exif_color_space" }, | |
455 | { tag_exposure_program, "exif_exposure_program" }, | |
f7450478 TC |
456 | { tag_metering_mode, "exif_metering_mode" }, |
457 | { tag_light_source, "exif_light_source" }, | |
458 | { tag_flash, "exif_flash" }, | |
459 | { tag_focal_plane_resolution_unit, "exif_focal_plane_resolution_unit" }, | |
460 | { tag_subject_location, "exif_subject_location" }, | |
461 | { tag_sensing_method, "exif_sensing_method" }, | |
462 | { tag_custom_rendered, "exif_custom_rendered" }, | |
463 | { tag_exposure_mode, "exif_exposure_mode" }, | |
464 | { tag_white_balance, "exif_white_balance" }, | |
465 | { tag_focal_length_in_35mm_film, "exif_focal_length_in_35mm_film" }, | |
466 | { tag_scene_capture_type, "exif_scene_capture_type" }, | |
467 | { tag_contrast, "exif_contrast" }, | |
468 | { tag_saturation, "exif_saturation" }, | |
469 | { tag_sharpness, "exif_sharpness" }, | |
470 | { tag_subject_distance_range, "exif_subject_distance_range" }, | |
471 | }; | |
472 | ||
473 | ||
474 | static const int exif_ifd_int_tag_count = ARRAY_COUNT(exif_ifd_int_tags); | |
475 | ||
476 | static tag_map exif_ifd_rat_tags[] = | |
477 | { | |
478 | { tag_exposure_time, "exif_exposure_time" }, | |
479 | { tag_f_number, "exif_f_number" }, | |
480 | { tag_shutter_speed, "exif_shutter_speed" }, | |
481 | { tag_aperture, "exif_aperture" }, | |
482 | { tag_brightness, "exif_brightness" }, | |
483 | { tag_exposure_bias, "exif_exposure_bias" }, | |
484 | { tag_max_aperture, "exif_max_aperture" }, | |
485 | { tag_subject_distance, "exif_subject_distance" }, | |
486 | { tag_focal_length, "exif_focal_length" }, | |
487 | { tag_flash_energy, "exif_flash_energy" }, | |
488 | { tag_focal_plane_x_resolution, "exif_focal_plane_x_resolution" }, | |
489 | { tag_focal_plane_y_resolution, "exif_focal_plane_y_resolution" }, | |
490 | { tag_exposure_index, "exif_exposure_index" }, | |
491 | { tag_digital_zoom_ratio, "exif_digital_zoom_ratio" }, | |
492 | { tag_gain_control, "exif_gain_control" }, | |
493 | }; | |
494 | ||
495 | static const int exif_ifd_rat_tag_count = ARRAY_COUNT(exif_ifd_rat_tags); | |
496 | ||
497 | static tag_map exposure_mode_values[] = | |
498 | { | |
499 | { 0, "Auto exposure" }, | |
500 | { 1, "Manual exposure" }, | |
501 | { 2, "Auto bracket" }, | |
502 | }; | |
503 | static tag_map color_space_values[] = | |
504 | { | |
505 | { 1, "sRGB" }, | |
506 | { 0xFFFF, "Uncalibrated" }, | |
507 | }; | |
508 | ||
509 | static tag_map exposure_program_values[] = | |
510 | { | |
511 | { 0, "Not defined" }, | |
512 | { 1, "Manual" }, | |
513 | { 2, "Normal program" }, | |
514 | { 3, "Aperture priority" }, | |
515 | { 4, "Shutter priority" }, | |
516 | { 5, "Creative program" }, | |
517 | { 6, "Action program" }, | |
518 | { 7, "Portrait mode" }, | |
519 | { 8, "Landscape mode" }, | |
520 | }; | |
521 | ||
522 | static tag_map metering_mode_values[] = | |
523 | { | |
524 | { 0, "unknown" }, | |
525 | { 1, "Average" }, | |
526 | { 2, "CenterWeightedAverage" }, | |
527 | { 3, "Spot" }, | |
528 | { 4, "MultiSpot" }, | |
529 | { 5, "Pattern" }, | |
530 | { 6, "Partial" }, | |
531 | { 255, "other" }, | |
532 | }; | |
533 | ||
534 | static tag_map light_source_values[] = | |
535 | { | |
536 | { 0, "unknown" }, | |
537 | { 1, "Daylight" }, | |
538 | { 2, "Fluorescent" }, | |
539 | { 3, "Tungsten (incandescent light)" }, | |
540 | { 4, "Flash" }, | |
541 | { 9, "Fine weather" }, | |
542 | { 10, "Cloudy weather" }, | |
543 | { 11, "Shade" }, | |
544 |