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