- added experimental EXIF decoding when reading JPEG files.
authorTony Cook <tony@develop=help.com>
Wed, 7 Dec 2005 03:38:48 +0000 (03:38 +0000)
committerTony Cook <tony@develop=help.com>
Wed, 7 Dec 2005 03:38:48 +0000 (03:38 +0000)
Changes
Imager.xs
MANIFEST
Makefile.PL
imexif.c [new file with mode: 0644]
imexif.h [new file with mode: 0644]
jpeg.c
lib/Imager/Files.pod
t/t101jpeg.t
testimg/exiftest.jpg [new file with mode: 0755]

diff --git a/Changes b/Changes
index f34a3f2..b4916b4 100644 (file)
--- a/Changes
+++ b/Changes
@@ -1207,6 +1207,7 @@ Revision history for Perl extension Imager.
   - add AUTHOR, REVISION.
 - add smoke test for nearest_color filter
 - added integer overflow checks to many memory allocation calls
+- added experimental EXIF decoding when reading JPEG files.
 
 =================================================================
 
index 13db3ec..da982a9 100644 (file)
--- a/Imager.xs
+++ b/Imager.xs
@@ -888,6 +888,12 @@ i_int_hlines_dump(i_int_hlines *hlines) {
 
 #endif
 
+#ifdef IMEXIF_ENABLE
+#define i_exif_enabled() 1
+#else
+#define i_exif_enabled() 0
+#endif
+
 MODULE = Imager                PACKAGE = Imager::Color PREFIX = ICL_
 
 Imager::Color
@@ -2107,6 +2113,8 @@ i_readjpeg_wiol(ig)
                     myfree(iptc_itext);
              }
 
+int
+i_exif_enabled()
 
 #endif
 
index bc8d60b..0e29eeb 100644 (file)
--- a/MANIFEST
+++ b/MANIFEST
@@ -53,6 +53,8 @@ hlines.c      Manage sets of horizontal line segments
 image.c
 image.h
 imagei.h
+imexif.c       Experimental JPEG EXIF decoding
+imexif.h
 img16.c
 imgdouble.c     Implements double/sample images
 imio.h
@@ -196,6 +198,7 @@ testimg/comp4.tif       4-bit/pixel paletted TIFF
 testimg/comp4bad.tif    corrupted 4-bit/pixel paletted TIFF
 testimg/comp8.bmp       Compressed 8-bit/pixel BMP
 testimg/comp8.tif       8-bit/pixel paletted TIFF
+testimg/exiftest.jpg   Test image for EXIF parsing
 testimg/expected.gif
 testimg/gimpgrad        A GIMP gradient file
 testimg/junk.ppm
index b7828fd..028f510 100644 (file)
@@ -30,6 +30,7 @@ my @disable;
 my @incpaths;
 my @libpaths;
 my $noprobe;
+my $noexif;
 GetOptions("help" => \$help,
            "enable=s" => \@enable,
            "disable=s" => \@disable,
@@ -37,7 +38,8 @@ GetOptions("help" => \$help,
            "libpath=s" => \@libpaths,
            "noprobe" => \$noprobe,
            "verbose|v" => \$VERBOSE,
-           "nolog" => \$NOLOG);
+           "nolog" => \$NOLOG,
+          "noexif" => \$noexif);
 
 if ($VERBOSE) { 
   print "Verbose mode\n"; 
@@ -49,6 +51,8 @@ if ($help) {
   usage();
 }
 
+my @defines;
+
 if ($NOLOG)   { print "Logging not compiled into module\n"; }
 else { 
   push @defines, [ IMAGER_LOG => 1, "Logging system" ];
@@ -83,8 +87,6 @@ elsif (@disable) {
   delete @formats{map { split /,/ } @disable};
 }
 
-my @defines;
-
 # Pick what libraries are used
 if ($MANUAL) {
   manual();
@@ -105,6 +107,11 @@ for my $frmkey (keys %formats) {
   $F_OBJECT .= ' '  .$frm->{objfiles};
   $lib_cflags   .= ' '  .$frm->{cflags} if $frm->{cflags};
 }
+unless ($noexif) {
+  print "EXIF support enabled\n";
+  push @defines, [ 'IMEXIF_ENABLE', 1, "Enable experimental EXIF support" ];
+  $F_OBJECT .= ' imexif.o';
+}
 
 $F_INC  = join ' ', map "-I$_", map / / ? qq{"$_"} : $_, 
   grep !exists $definc{$_}, @incs;
@@ -520,6 +527,7 @@ scalable.  It also supports a variety of other fonts.
 DOCS
                            code => \&freetype2_probe,
                           };
+
   # Make fix indent
   for (keys %formats) { $formats{$_}->{docs} =~ s/^\s+/  /mg; }
 }
diff --git a/imexif.c b/imexif.c
new file mode 100644 (file)
index 0000000..8d94986
--- /dev/null
+++ b/imexif.c
@@ -0,0 +1,1299 @@
+#include "imexif.h"
+#include <stdlib.h>
+#include <float.h>
+
+/*
+=head1 NAME
+
+imexif.c - EXIF support for Imager
+
+=head1 SYNOPSIS
+
+  if (i_int_decode_exif(im, app1data, app1datasize)) {
+    // exif block seen
+  }
+
+=head1 DESCRIPTION
+
+This code provides a basic EXIF data decoder.  It is intended to be
+called from the JPEG reader code when an APP1 data block is found, and
+will set tags in the supplied image.
+
+=cut
+*/
+
+typedef enum tiff_type_tag {
+  tt_intel = 'I',
+  tt_motorola = 'M'
+} tiff_type;
+
+typedef enum {
+  ift_byte = 1,
+  ift_ascii = 2,
+  ift_short = 3,
+  ift_long = 4,
+  ift_rational = 5,
+  ift_sbyte = 6,
+  ift_undefined = 7,
+  ift_sshort = 8,
+  ift_slong = 9,
+  ift_srational = 10,
+  ift_float = 11,
+  ift_double = 12,
+  ift_last = 12 /* keep the same as the highest type code */
+} ifd_entry_type;
+
+static int type_sizes[] =
+  {
+    0, /* not used */
+    1, /* byte */
+    1, /* ascii */
+    2, /* short */
+    4, /* long */
+    8, /* rational */
+    1, /* sbyte */
+    1, /* undefined */
+    2, /* sshort */
+    4, /* slong */
+    8, /* srational */
+    4, /* float */
+    8, /* double */
+  };
+
+typedef struct {
+  int tag;
+  int type;
+  int count;
+  int size;
+  int offset;
+} ifd_entry;
+
+typedef struct {
+  int tag;
+  char const *name;
+} tag_map;
+
+typedef struct {
+  int tag;
+  char const *name;
+  tag_map const *map;
+  int map_count;
+} tag_value_map;
+
+#define PASTE(left, right) PASTE_(left, right)
+#define PASTE_(left, right) left##right
+#define QUOTE(value) #value
+
+#define VALUE_MAP_ENTRY(name) \
+  { \
+    PASTE(tag_, name), \
+    "exif_" QUOTE(name) "_name", \
+    PASTE(name, _values), \
+    ARRAY_COUNT(PASTE(name, _values)) \
+  }
+
+/* we don't process every tag */
+#define tag_make 271
+#define tag_model 272
+#define tag_orientation 274
+#define tag_x_resolution 282
+#define tag_y_resolution 283
+#define tag_resolution_unit 296
+#define tag_copyright 33432
+#define tag_software 305
+#define tag_artist 315
+#define tag_date_time 306
+#define tag_image_description 270
+
+#define tag_exif_ifd 34665
+#define tag_gps_ifd 34853
+
+#define resunit_none 1
+#define resunit_inch 2
+#define resunit_centimeter 3
+
+/* tags from the EXIF ifd */
+#define tag_exif_version 0x9000
+#define tag_flashpix_version 0xA000
+#define tag_color_space 0xA001
+#define tag_component_configuration 0x9101
+#define tag_component_bits_per_pixel 0x9102
+#define tag_pixel_x_dimension 0xA002
+#define tag_pixel_y_dimension 0xA003
+#define tag_maker_note 0x927C
+#define tag_user_comment 0x9286
+#define tag_related_sound_file 0xA004
+#define tag_date_time_original 0x9003
+#define tag_date_time_digitized 0x9004
+#define tag_sub_sec_time 0x9290
+#define tag_sub_sec_time_original 0x9291
+#define tag_sub_sec_time_digitized 0x9292
+#define tag_image_unique_id 0xA420
+#define tag_exposure_time 0x829a
+#define tag_f_number 0x829D
+#define tag_exposure_program 0x8822
+#define tag_spectral_sensitivity 0x8824
+#define tag_iso_speed_rating 0x8827
+#define tag_oecf 0x8828
+#define tag_shutter_speed 0x9201
+#define tag_aperture 0x9202
+#define tag_brightness 0x9203
+#define tag_exposure_bias 0x9204
+#define tag_max_aperture 0x9205
+#define tag_subject_distance 0x9206
+#define tag_metering_mode 0x9207
+#define tag_light_source 0x9208
+#define tag_flash 0x9209
+#define tag_focal_length 0x920a
+#define tag_subject_area 0x9214
+#define tag_flash_energy 0xA20B
+#define tag_spatial_frequency_response 0xA20C
+#define tag_focal_plane_x_resolution 0xA20e
+#define tag_focal_plane_y_resolution 0xA20F
+#define tag_focal_plane_resolution_unit 0xA210
+#define tag_subject_location 0xA214
+#define tag_exposure_index 0xA215
+#define tag_sensing_method 0xA217
+#define tag_file_source 0xA300
+#define tag_scene_type 0xA301
+#define tag_cfa_pattern 0xA302
+#define tag_custom_rendered 0xA401
+#define tag_exposure_mode 0xA402
+#define tag_white_balance 0xA403
+#define tag_digital_zoom_ratio 0xA404
+#define tag_focal_length_in_35mm_film 0xA405
+#define tag_scene_capture_type 0xA406
+#define tag_gain_control 0xA407
+#define tag_contrast 0xA408
+#define tag_saturation 0xA409
+#define tag_sharpness 0xA40A
+#define tag_device_setting_description 0xA40B
+#define tag_subject_distance_range 0xA40C
+
+/* don't use this on pointers */
+#define ARRAY_COUNT(array) (sizeof(array)/sizeof(*array))
+
+/* in memory tiff structure */
+typedef struct {
+  /* the data we use as a tiff */
+  unsigned char *base;
+  size_t size;
+
+  /* intel or motorola byte order */
+  tiff_type type;
+
+  /* initial ifd offset */
+  unsigned long first_ifd_offset;
+  
+  /* size (in entries) and data */
+  int ifd_size; 
+  ifd_entry *ifd;
+  unsigned long next_ifd;
+} imtiff;
+
+static int tiff_init(imtiff *tiff, unsigned char *base, size_t length);
+static int tiff_load_ifd(imtiff *tiff, unsigned long offset);
+static void tiff_final(imtiff *tiff);
+static void tiff_clear_ifd(imtiff *tiff);
+static int tiff_get_bytes(imtiff *tiff, unsigned char *to, size_t offset, 
+                         size_t count);
+static int tiff_get_tag_double(imtiff *, int index, double *result);
+static int tiff_get_tag_int(imtiff *, int index, int *result);
+static unsigned tiff_get16(imtiff *, unsigned long offset);
+static unsigned tiff_get32(imtiff *, unsigned long offset);
+static int tiff_get16s(imtiff *, unsigned long offset);
+static int tiff_get32s(imtiff *, unsigned long offset);
+static double tiff_get_rat(imtiff *, unsigned long offset);
+static double tiff_get_rats(imtiff *, unsigned long offset);
+static void save_ifd0_tags(i_img *im, imtiff *tiff, unsigned long *exif_ifd_offset);
+static void save_exif_ifd_tags(i_img *im, imtiff *tiff);
+static void
+copy_string_tags(i_img *im, imtiff *tiff, tag_map *map, int map_count);
+static void
+copy_int_tags(i_img *im, imtiff *tiff, tag_map *map, int map_count);
+static void
+copy_rat_tags(i_img *im, imtiff *tiff, tag_map *map, int map_count);
+static void
+copy_name_tags(i_img *im, imtiff *tiff, tag_value_map *map, int map_count);
+static void process_maker_note(i_img *im, imtiff *tiff, unsigned long offset, size_t size);
+
+/*
+=head1 PUBLIC FUNCTIONS
+
+These functions are available to other parts of Imager.  They aren't
+intended to be called from outside of Imager.
+
+=over
+
+=item i_int_decode_exit
+
+i_int_decode_exif(im, data_base, data_size);
+
+The data from data_base for data_size bytes will be scanned for EXIF
+data.
+
+Any data found will be used to set tags in the supplied image.
+
+The intent is that invalid EXIF data will simply fail to set tags, and
+write to the log.  In no case should this code exit when supplied
+invalid data.
+
+Returns true if an Exif header was seen.
+
+*/
+
+int
+i_int_decode_exif(i_img *im, unsigned char *data, size_t length) {
+  imtiff tiff;
+  unsigned long exif_ifd_offset = 0;
+  unsigned long ifd1_offset;
+  /* basic checks - must start with "Exif\0\0" */
+
+  if (length < 6 || memcmp(data, "Exif\0\0", 6) != 0) {
+    return 0;
+  }
+
+  data += 6;
+  length -= 6;
+
+  if (!tiff_init(&tiff, data, length)) {
+    mm_log((2, "Exif header found, but no valid TIFF header\n"));
+    return 1;
+  }
+  if (!tiff_load_ifd(&tiff, tiff.first_ifd_offset)) {
+    mm_log((2, "Exif header found, but could not load IFD 0\n"));
+    tiff_final(&tiff);
+    return 1;
+  }
+
+  save_ifd0_tags(im, &tiff, &exif_ifd_offset);
+
+  if (exif_ifd_offset) {
+    if (tiff_load_ifd(&tiff, exif_ifd_offset)) {
+      save_exif_ifd_tags(im, &tiff);
+    }
+    else {
+      mm_log((2, "Could not load Exif IFD\n"));
+    }
+  }
+
+  tiff_final(&tiff);
+
+  return 1;
+}
+
+/*
+
+=back
+
+=head1 INTERNAL FUNCTIONS
+
+=head2 EXIF Processing 
+
+=over
+
+=item save_ifd0_tags
+
+save_ifd0_tags(im, tiff, &exif_ifd_offset)
+
+Scans the currently loaded IFD for tags expected in IFD0 and sets them
+in the image.
+
+Sets *exif_ifd_offset to the offset of the EXIF IFD if found.
+
+=cut
+
+*/
+
+static tag_map ifd0_string_tags[] =
+  {
+    tag_make, "exif_make",
+    tag_model, "exif_model",
+    tag_copyright, "exif_copyright",
+    tag_software, "exif_software",
+    tag_artist, "exif_artist",
+    tag_date_time, "exif_date_time",
+    tag_image_description, "exif_image_description",
+  };
+
+static const int ifd0_string_tag_count = ARRAY_COUNT(ifd0_string_tags);
+
+static tag_map ifd0_int_tags[] =
+  {
+    { tag_orientation, "exif_orientation", },
+    { tag_resolution_unit, "exif_resolution_unit" },
+  };
+
+static const int ifd0_int_tag_count = ARRAY_COUNT(ifd0_int_tags);
+
+static tag_map ifd0_rat_tags[] =
+  {
+    { tag_x_resolution, "exif_x_resolution" },
+    { tag_y_resolution, "exif_y_resolution" },
+  };
+
+static tag_map resolution_unit_values[] =
+  {
+    { 1, "none" },
+    { 2, "inches" },
+    { 3, "centimeters" },
+  };
+
+static tag_value_map ifd0_values[] =
+  {
+    VALUE_MAP_ENTRY(resolution_unit),
+  };
+
+static void
+save_ifd0_tags(i_img *im, imtiff *tiff, unsigned long *exif_ifd_offset) {
+  int i, tag_index;
+  int work;
+  ifd_entry *entry;
+
+  for (tag_index = 0, entry = tiff->ifd; 
+       tag_index < tiff->ifd_size; ++tag_index, ++entry) {
+    switch (entry->tag) {
+    case tag_exif_ifd:
+      if (tiff_get_tag_int(tiff, tag_index, &work))
+       *exif_ifd_offset = work;
+      break;
+    }
+  }
+
+  copy_string_tags(im, tiff, ifd0_string_tags, ifd0_string_tag_count);
+  copy_int_tags(im, tiff, ifd0_int_tags, ifd0_int_tag_count);
+  copy_rat_tags(im, tiff, ifd0_rat_tags, ARRAY_COUNT(ifd0_rat_tags));
+  copy_name_tags(im, tiff, ifd0_values, ARRAY_COUNT(ifd0_values));
+}
+
+/*
+=item save_exif_ifd_tags
+
+save_exif_ifd_tags(im, tiff)
+
+Scans the currently loaded IFD for the tags expected in the EXIF IFD
+and sets them as tags in the image.
+
+=cut
+
+*/
+
+static tag_map exif_ifd_string_tags[] =
+  {
+    { tag_exif_version, "exif_version", },
+    { tag_flashpix_version, "exif_flashpix_version", },
+    { tag_related_sound_file, "exif_related_sound_file", },
+    { tag_date_time_original, "exif_date_time_original", },
+    { tag_date_time_digitized, "exif_date_time_digitized", },
+    { tag_sub_sec_time, "exif_sub_sec_time" },
+    { tag_sub_sec_time_original, "exif_sub_sec_time_original" },
+    { tag_sub_sec_time_digitized, "exif_sub_sec_time_digitized" },
+    { tag_image_unique_id, "exif_image_unique_id" },
+    { tag_spectral_sensitivity, "exif_spectral_sensitivity" },
+  };
+
+static const int exif_ifd_string_tag_count = ARRAY_COUNT(exif_ifd_string_tags);
+
+static tag_map exif_ifd_int_tags[] =
+  {
+    { tag_color_space, "exif_color_space" },
+    { tag_exposure_program, "exif_exposure_program" },
+    { tag_iso_speed_rating, "exif_iso_speed_rating" },
+    { tag_metering_mode, "exif_metering_mode" },
+    { tag_light_source, "exif_light_source" },
+    { tag_flash, "exif_flash" },
+    { tag_focal_plane_resolution_unit, "exif_focal_plane_resolution_unit" },
+    { tag_subject_location, "exif_subject_location" },
+    { tag_sensing_method, "exif_sensing_method" },
+    { tag_custom_rendered, "exif_custom_rendered" },
+    { tag_exposure_mode, "exif_exposure_mode" },
+    { tag_white_balance, "exif_white_balance" },
+    { tag_focal_length_in_35mm_film, "exif_focal_length_in_35mm_film" },
+    { tag_scene_capture_type, "exif_scene_capture_type" },
+    { tag_contrast, "exif_contrast" },
+    { tag_saturation, "exif_saturation" },
+    { tag_sharpness, "exif_sharpness" },
+    { tag_subject_distance_range, "exif_subject_distance_range" },
+  };
+
+
+static const int exif_ifd_int_tag_count = ARRAY_COUNT(exif_ifd_int_tags);
+
+static tag_map exif_ifd_rat_tags[] =
+  {
+    { tag_exposure_time, "exif_exposure_time" },
+    { tag_f_number, "exif_f_number" },
+    { tag_shutter_speed, "exif_shutter_speed" },
+    { tag_aperture, "exif_aperture" },
+    { tag_brightness, "exif_brightness" },
+    { tag_exposure_bias, "exif_exposure_bias" },
+    { tag_max_aperture, "exif_max_aperture" },
+    { tag_subject_distance, "exif_subject_distance" },
+    { tag_focal_length, "exif_focal_length" },
+    { tag_flash_energy, "exif_flash_energy" },
+    { tag_focal_plane_x_resolution, "exif_focal_plane_x_resolution" },
+    { tag_focal_plane_y_resolution, "exif_focal_plane_y_resolution" },
+    { tag_exposure_index, "exif_exposure_index" },
+    { tag_digital_zoom_ratio, "exif_digital_zoom_ratio" },
+    { tag_gain_control, "exif_gain_control" },
+  };
+
+static const int exif_ifd_rat_tag_count = ARRAY_COUNT(exif_ifd_rat_tags);
+
+static tag_map exposure_mode_values[] =
+  {
+    { 0, "Auto exposure" },
+    { 1, "Manual exposure" },
+    { 2, "Auto bracket" },
+  };
+static tag_map color_space_values[] =
+  {
+    { 1, "sRGB" },
+    { 0xFFFF, "Uncalibrated" },
+  };
+
+static tag_map exposure_program_values[] =
+  {
+    { 0, "Not defined" },
+    { 1, "Manual" },
+    { 2, "Normal program" },
+    { 3, "Aperture priority" },
+    { 4, "Shutter priority" },
+    { 5, "Creative program" },
+    { 6, "Action program" },
+    { 7, "Portrait mode" },
+    { 8, "Landscape mode" },
+  };
+
+static tag_map metering_mode_values[] =
+  {
+    { 0, "unknown" },
+    { 1, "Average" },
+    { 2, "CenterWeightedAverage" },
+    { 3, "Spot" },
+    { 4, "MultiSpot" },
+    { 5, "Pattern" },
+    { 6, "Partial" },
+    { 255, "other" },
+  };
+
+static tag_map light_source_values[] =
+  {
+    { 0, "unknown" },
+    { 1, "Daylight" },
+    { 2, "Fluorescent" },
+    { 3, "Tungsten (incandescent light)" },
+    { 4, "Flash" },
+    { 9, "Fine weather" },
+    { 10, "Cloudy weather" },
+    { 11, "Shade" },
+    { 12, "Daylight fluorescent (D 5700 Ð 7100K)" },
+    { 13, "Day white fluorescent (N 4600 Ð 5400K)" },
+    { 14, "Cool white fluorescent (W 3900 Ð 4500K)" },
+    { 15, "White fluorescent (WW 3200 Ð 3700K)" },
+    { 17, "Standard light A" },
+    { 18, "Standard light B" },
+    { 19, "Standard light C" },
+    { 20, "D55" },
+    { 21, "D65" },
+    { 22, "D75" },
+    { 23, "D50" },
+    { 24, "ISO studio tungsten" },
+    { 255, "other light source" },
+  };
+
+static tag_map flash_values[] =
+  {
+    { 0x0000, "Flash did not fire." },
+    { 0x0001, "Flash fired." },
+    { 0x0005, "Strobe return light not detected." },
+    { 0x0007, "Strobe return light detected." },
+    { 0x0009, "Flash fired, compulsory flash mode" },
+    { 0x000D, "Flash fired, compulsory flash mode, return light not detected" },
+    { 0x000F, "Flash fired, compulsory flash mode, return light detected" },
+    { 0x0010, "Flash did not fire, compulsory flash mode" },
+    { 0x0018, "Flash did not fire, auto mode" },
+    { 0x0019, "Flash fired, auto mode" },
+    { 0x001D, "Flash fired, auto mode, return light not detected" },
+    { 0x001F, "Flash fired, auto mode, return light detected" },
+    { 0x0020, "No flash function" },
+    { 0x0041, "Flash fired, red-eye reduction mode" },
+    { 0x0045, "Flash fired, red-eye reduction mode, return light not detected" },
+    { 0x0047, "Flash fired, red-eye reduction mode, return light detected" },
+    { 0x0049, "Flash fired, compulsory flash mode, red-eye reduction mode" },
+    { 0x004D, "Flash fired, compulsory flash mode, red-eye reduction mode, return light not detected" },
+    { 0x004F, "Flash fired, compulsory flash mode, red-eye reduction mode, return light detected" },
+    { 0x0059, "Flash fired, auto mode, red-eye reduction mode" },
+    { 0x005D, "Flash fired, auto mode, return light not detected, red-eye reduction mode" },
+    { 0x005F, "Flash fired, auto mode, return light detected, red-eye reduction mode" },
+  };
+
+static tag_map sensing_method_values[] =
+  {
+    { 1, "Not defined" },
+    { 2, "One-chip color area sensor" },
+    { 3, "Two-chip color area sensor" },
+    { 4, "Three-chip color area sensor" },
+    { 5, "Color sequential area sensor" },
+    { 7, "Trilinear sensor" },
+    { 8, "Color sequential linear sensor" },
+  };
+
+static tag_map custom_rendered_values[] =
+  {
+    { 0, "Normal process" },
+    { 1, "Custom process" },
+  };
+
+static tag_map white_balance_values[] =
+  {
+    { 0, "Auto white balance" },
+    { 1, "Manual white balance" },
+  };
+
+static tag_map scene_capture_type_values[] =
+  {
+    { 0, "Standard" },
+    { 1, "Landscape" },
+    { 2, "Portrait" },
+    { 3, "Night scene" },
+  };
+
+static tag_map gain_control_values[] =
+  {
+    { 0, "None" },
+    { 1, "Low gain up" },
+    { 2, "High gain up" },
+    { 3, "Low gain down" },
+    { 4, "High gain down" },
+  };
+
+static tag_map contrast_values[] =
+  {
+    { 0, "Normal" },
+    { 1, "Soft" },
+    { 2, "Hard" },
+  };
+
+static tag_map saturation_values[] =
+  {
+    { 0, "Normal" },
+    { 1, "Low saturation" },
+    { 2, "High saturation" },
+  };
+
+static tag_map sharpness_values[] =
+  {
+    { 0, "Normal" },
+    { 1, "Soft" },
+    { 2, "Hard" },
+  };
+
+static tag_map subject_distance_range_values[] =
+  {
+    { 0, "unknown" },
+    { 1, "Macro" },
+    { 2, "Close view" },
+    { 3, "Distant view" },
+  };
+
+#define focal_plane_resolution_unit_values resolution_unit_values
+
+static tag_value_map exif_ifd_values[] =
+  {
+    VALUE_MAP_ENTRY(exposure_mode),
+    VALUE_MAP_ENTRY(color_space),
+    VALUE_MAP_ENTRY(exposure_program),
+    VALUE_MAP_ENTRY(metering_mode),
+    VALUE_MAP_ENTRY(light_source),
+    VALUE_MAP_ENTRY(flash),
+    VALUE_MAP_ENTRY(sensing_method),
+    VALUE_MAP_ENTRY(custom_rendered),
+    VALUE_MAP_ENTRY(white_balance),
+    VALUE_MAP_ENTRY(scene_capture_type),
+    VALUE_MAP_ENTRY(gain_control),
+    VALUE_MAP_ENTRY(contrast),
+    VALUE_MAP_ENTRY(saturation),
+    VALUE_MAP_ENTRY(sharpness),
+    VALUE_MAP_ENTRY(subject_distance_range),
+    VALUE_MAP_ENTRY(focal_plane_resolution_unit),
+  };
+
+static void
+save_exif_ifd_tags(i_img *im, imtiff *tiff) {
+  int i, tag_index;
+  int work;
+  ifd_entry *entry;
+  char *user_comment;
+  unsigned long maker_note_offset = 0;
+  size_t maker_note_size = 0;
+
+  for (tag_index = 0, entry = tiff->ifd; 
+       tag_index < tiff->ifd_size; ++tag_index, ++entry) {
+    switch (entry->tag) {
+    case tag_user_comment:
+      /* I don't want to trash the source, so work on a copy */
+      user_comment = mymalloc(entry->size);
+      memcpy(user_comment, tiff->base + entry->offset, entry->size);
+      /* the first 8 bytes indicate the encoding, make them into spaces
+        for better presentation */
+      for (i = 0; i < 8; ++i) {
+       if (user_comment[i] == '\0')
+         user_comment[i] = ' ';
+      }
+      /* find the actual end of the string */
+      while (i < entry->size && user_comment[i])
+       ++i;
+      i_tags_add(&im->tags, "exif_user_comment", 0, user_comment, i, 0);
+      myfree(user_comment);
+      break;
+
+    case tag_maker_note:
+      maker_note_offset = entry->offset;
+      maker_note_size = entry->size;
+      break;
+
+      /* the following aren't processed yet */
+    case tag_oecf:
+    case tag_spatial_frequency_response:
+    case tag_file_source:
+    case tag_scene_type:
+    case tag_cfa_pattern:
+    case tag_device_setting_description:
+    case tag_subject_area:
+      break;
+    }
+  }
+
+  copy_string_tags(im, tiff, exif_ifd_string_tags, exif_ifd_string_tag_count);
+  copy_int_tags(im, tiff, exif_ifd_int_tags, exif_ifd_int_tag_count);
+  copy_rat_tags(im, tiff, exif_ifd_rat_tags, exif_ifd_rat_tag_count);
+  copy_name_tags(im, tiff, exif_ifd_values, ARRAY_COUNT(exif_ifd_values));
+
+  /* This trashes the IFD - make sure it's done last */
+  if (maker_note_offset) {
+    process_maker_note(im, tiff, maker_note_offset, maker_note_size);
+  }
+}
+
+/*
+=item process_maker_note
+
+This is a stub for processing the maker note tag.
+
+Maker notes aren't covered by EXIF itself and in general aren't
+documented by the manufacturers.
+
+=cut
+*/
+
+static void
+process_maker_note(i_img *im, imtiff *tiff, unsigned long offset, size_t size) {
+  /* this will be added in a future release */
+}
+
+/*
+=back
+
+=head2 High level TIFF functions
+
+To avoid relying upon tifflib when we're not processing an image we
+have some simple in-memory TIFF file management.
+
+=over
+
+=item tiff_init
+
+imtiff tiff;
+if (tiff_init(tiff, data_base, data_size)) {
+  // success
+}
+
+Initialize the tiff data structure.
+
+Scans for the byte order and version markers, and stores the offset to
+the first IFD (IFD0 in EXIF) in first_ifd_offset.
+
+=cut
+*/
+
+static int
+tiff_init(imtiff *tiff, unsigned char *data, size_t length) {
+  int version;
+
+  tiff->base = data;
+  tiff->size = length;
+  if (length < 8) /* well... would have to be much bigger to be useful */
+    return 0;
+  if (data[0] == 'M' && data[1] == 'M')
+    tiff->type = tt_motorola;
+  else if (data[0] == 'I' && data[1] == 'I') 
+    tiff->type = tt_intel;
+  else
+    return 0; /* invalid header */
+
+  version = tiff_get16(tiff, 2);
+  if (version != 42)
+    return 0;
+
+  tiff->first_ifd_offset = tiff_get32(tiff, 4);
+  if (tiff->first_ifd_offset > length || tiff->first_ifd_offset < 8)
+    return 0;
+
+  tiff->ifd_size = 0;
+  tiff->ifd = NULL;
+  tiff->next_ifd = 0;
+
+  return 1;
+}
+
+/*
+=item tiff_final
+
+tiff_final(&tiff)
+
+Clean up the tiff structure initialized by tiff_init()
+
+=cut
+*/
+
+static void
+tiff_final(imtiff *tiff) {
+  tiff_clear_ifd(tiff);
+}
+
+/*
+=item tiff_load_ifd
+
+if (tiff_load_ifd(tiff, offset)) {
+  // process the ifd
+}
+
+Loads the IFD from the given offset into the tiff objects ifd.
+
+This can fail if the IFD extends beyond end of file, or if any data
+offsets combined with their sizes, extends beyond end of file.
+
+Returns true on success.
+
+=cut
+*/
+
+static int
+tiff_load_ifd(imtiff *tiff, unsigned long offset) {
+  unsigned count;
+  int ifd_size;
+  ifd_entry *entries = NULL;
+  int i;
+  unsigned long base;
+
+  tiff_clear_ifd(tiff);
+
+  /* rough check count + 1 entry + next offset */
+  if (offset + (2+12+4) > tiff->size) {
+    mm_log((2, "offset %uld beyond end off Exif block"));
+    return 0;
+  }
+
+  count = tiff_get16(tiff, offset);
+  
+  /* check we can fit the whole thing */
+  ifd_size = 2 + count * 12 + 4; /* count + count entries + next offset */
+  if (offset + ifd_size > tiff->size) {
+    mm_log((2, "offset %uld beyond end off Exif block"));
+    return 0;
+  }
+
+  entries = mymalloc(count * sizeof(ifd_entry));
+  memset(entries, 0, count * sizeof(ifd_entry));
+  base = offset + 2;
+  for (i = 0; i < count; ++i) {
+    ifd_entry *entry = entries + i;
+    entry->tag = tiff_get16(tiff, base);
+    entry->type = tiff_get16(tiff, base+2);
+    entry->count = tiff_get32(tiff, base+4);
+    if (entry->type >= 1 || entry->type <= ift_last) {
+      int data_size = type_sizes[entry->type] * entry->count;
+      entry->size = data_size;
+      if (data_size <= 4) {
+       entry->offset = base + 8;
+      }
+      else {
+       entry->offset = tiff_get32(tiff, base+8);
+       if (entry->offset + data_size > tiff->size) {
+         mm_log((2, "Invalid data offset processing IFD"));
+         myfree(entries);
+         return 0;
+       }
+      }
+    }
+    else {
+      entry->size = 0;
+      entry->offset = 0;
+    }
+    base += 12;
+  }
+
+  tiff->ifd_size = count;
+  tiff->ifd = entries;
+  tiff->next_ifd = tiff_get32(tiff, base);
+
+  return 1;
+}
+
+/*
+=item tiff_clear_ifd
+
+tiff_clear_ifd(tiff)
+
+Releases any memory associated with the stored IFD and resets the IFD
+pointers.
+
+This is called by tiff_load_ifd() and tiff_final().
+
+=cut
+*/
+
+static void
+tiff_clear_ifd(imtiff *tiff) {
+  int i;
+  if (tiff->ifd_size && tiff->ifd) {
+    myfree(tiff->ifd);
+    tiff->ifd_size = 0;
+    tiff->ifd = NULL;
+  }
+}
+
+/*
+=item tiff_get_tag_double
+
+  double value;
+  if (tiff_get_tag(tiff, index, &value)) {
+    // process value
+  }
+
+Attempts to retrieve a double value from the given index in the
+current IFD.
+
+The value must have a count of 1.
+
+=cut
+*/
+
+static int
+tiff_get_tag_double(imtiff *tiff, int index, double *result) {
+  ifd_entry *entry;
+  if (index < 0 || index >= tiff->ifd_size) {
+    m_fatal(3, "tiff_get_tag_double() index out of range");
+  }
+  
+  entry = tiff->ifd + index;
+  if (entry->count != 1) {
+    mm_log((3, "tiff_get_tag_double() called on tag with multiple values"));
+    return 0;
+  }
+
+  switch (entry->type) {
+  case ift_short:
+    *result = tiff_get16(tiff, entry->offset);
+    return 1;
+   
+  case ift_long:
+    *result = tiff_get32(tiff, entry->offset);
+    return 1;
+
+  case ift_rational:
+    *result = tiff_get_rat(tiff, entry->offset);
+    return 1;
+
+  case ift_sshort:
+    *result = tiff_get16s(tiff, entry->offset);
+    return 1;
+
+  case ift_slong:
+    *result = tiff_get32s(tiff, entry->offset);
+    return 1;
+
+  case ift_srational:
+    *result = tiff_get_rats(tiff, entry->offset);
+    return 1;
+  }
+
+  return 0;
+}
+
+/*
+=item tiff_get_tag_int
+
+  int value;
+  if (tiff_get_tag_int(tiff, index, &value)) {
+    // process value
+  }
+
+Attempts to retrieve an integer value from the given index in the
+current IFD.
+
+The value must have a count of 1.
+
+=cut
+*/
+
+static int
+tiff_get_tag_int(imtiff *tiff, int index, int *result) {
+  ifd_entry *entry;
+  if (index < 0 || index >= tiff->ifd_size) {
+    m_fatal(3, "tiff_get_tag_int() index out of range");
+  }
+  
+  entry = tiff->ifd + index;
+  if (entry->count != 1) {
+    mm_log((3, "tiff_get_tag_int() called on tag with multiple values"));
+    return 0;
+  }
+
+  switch (entry->type) {
+  case ift_short:
+    *result = tiff_get16(tiff, entry->offset);
+    return 1;
+   
+  case ift_long:
+    *result = tiff_get32(tiff, entry->offset);
+    return 1;
+
+  case ift_sshort:
+    *result = tiff_get16s(tiff, entry->offset);
+    return 1;
+
+  case ift_slong:
+    *result = tiff_get32s(tiff, entry->offset);
+    return 1;
+  }
+
+  return 0;
+}
+
+/*
+=back
+
+=head2 Table-based tag setters
+
+This set of functions checks for matches between the current IFD and
+tags supplied in an array, when there's a match it sets the
+appropriate tag in the image.
+
+=over
+
+=item copy_int_tags
+
+Scans the IFD for integer tags and sets them in the image,
+
+=cut
+*/
+
+static void
+copy_int_tags(i_img *im, imtiff *tiff, tag_map *map, int map_count) {
+  int i, tag_index;
+  ifd_entry *entry;
+
+  for (tag_index = 0, entry = tiff->ifd; 
+       tag_index < tiff->ifd_size; ++tag_index, ++entry) {
+    for (i = 0; i < map_count; ++i) {
+      int value;
+      if (map[i].tag == entry->tag
+         && tiff_get_tag_int(tiff, tag_index, &value)) {
+       i_tags_addn(&im->tags, map[i].name, 0, value);
+       break;
+      }
+    }
+  }
+}
+
+/*
+=item copy_rat_tags
+
+Scans the IFD for rational tags and sets them in the image.
+
+=cut
+*/
+
+static void
+copy_rat_tags(i_img *im, imtiff *tiff, tag_map *map, int map_count) {
+  int i, tag_index;
+  ifd_entry *entry;
+
+  for (tag_index = 0, entry = tiff->ifd; 
+       tag_index < tiff->ifd_size; ++tag_index, ++entry) {
+    for (i = 0; i < map_count; ++i) {
+      double value;
+      if (map[i].tag == entry->tag
+         && tiff_get_tag_double(tiff, tag_index, &value)) {
+       i_tags_set_float2(&im->tags, map[i].name, 0, value, 6);
+       break;
+      }
+    }
+  }
+}
+
+/*
+=item copy_string_tags
+
+Scans the IFD for string tags and sets them in the image.
+
+=cut
+*/
+
+static void
+copy_string_tags(i_img *im, imtiff *tiff, tag_map *map, int map_count) {
+  int i, tag_index;
+  ifd_entry *entry;
+
+  for (tag_index = 0, entry = tiff->ifd; 
+       tag_index < tiff->ifd_size; ++tag_index, ++entry) {
+    for (i = 0; i < map_count; ++i) {
+      if (map[i].tag == entry->tag) {
+       int len = entry->type == ift_ascii ? entry->size - 1 : entry->size;
+       i_tags_add(&im->tags, map[i].name, 0,
+                  tiff->base + entry->offset, len, 0);
+       break;
+      }
+    }
+  }
+}
+
+/*
+=item copy_name_tags
+
+This function maps integer values to descriptions for those values.
+
+In general we handle the integer value through copy_int_tags() and
+then the same tage with a "_name" suffix here.
+
+=cut
+*/
+
+static void
+copy_name_tags(i_img *im, imtiff *tiff, tag_value_map *map, int map_count) {
+  int i, j, tag_index;
+  ifd_entry *entry;
+
+  for (tag_index = 0, entry = tiff->ifd; 
+       tag_index < tiff->ifd_size; ++tag_index, ++entry) {
+    for (i = 0; i < map_count; ++i) {
+      int value;
+      if (map[i].tag == entry->tag
+         && tiff_get_tag_int(tiff, tag_index, &value)) {
+       tag_map const *found = NULL;
+       for (j = 0; j < map[i].map_count; ++j) {
+         if (value == map[i].map[j].tag) {
+           found = map[i].map + j;
+           break;
+         }
+       }
+       if (found) {
+         i_tags_add(&im->tags, map[i].name, 0, found->name, -1, 0);
+       }
+       break;
+      }
+    }
+  }
+}
+
+
+/*
+=back
+
+=head2 Low level data access functions
+
+These functions use the byte order in the tiff object to extract
+various types of data from the tiff data.
+
+These functions will abort if called with an out of range offset.
+
+The intent is that any offset checks should have been done by the caller.
+
+=over
+
+=item tiff_get16
+
+Retrieve a 16 bit unsigned integer from offset.
+
+=cut
+*/
+
+static unsigned
+tiff_get16(imtiff *tiff, unsigned long offset) {
+  if (offset + 2 > tiff->size)
+    m_fatal(3, "attempt to get16 at %uld in %uld image", offset, tiff->size);
+
+  if (tiff->type == tt_intel) 
+    return tiff->base[offset] + 0x100 * tiff->base[offset+1];
+  else
+    return tiff->base[offset+1] + 0x100 * tiff->base[offset];
+}
+
+/*
+=item tiff_get32
+
+Retrieve a 32-bit unsigned integer from offset.
+
+=cut
+*/
+
+static unsigned
+tiff_get32(imtiff *tiff, unsigned long offset) {
+  if (offset + 4 > tiff->size)
+    m_fatal(3, "attempt to get16 at %uld in %uld image", offset, tiff->size);
+
+  if (tiff->type == tt_intel) 
+    return tiff->base[offset] + 0x100 * tiff->base[offset+1] 
+      + 0x10000 * tiff->base[offset+2] + 0x1000000 * tiff->base[offset+3];
+  else
+    return tiff->base[offset+3] + 0x100 * tiff->base[offset+2]
+      + 0x10000 * tiff->base[offset+1] + 0x1000000 * tiff->base[offset];
+}
+
+/*
+=item tiff_get_bytes
+
+Retrieve a byte string from offset.
+
+This isn't used much, you can usually deal with the data in-situ.
+This is intended for use when you need to modify the data in some way.
+
+=cut
+*/
+
+static int
+tiff_get_bytes(imtiff *tiff, unsigned char *data, size_t offset, 
+              size_t size) {
+  if (offset + size > tiff->size)
+    return 0;
+
+  memcpy(data, tiff->base+offset, size);
+
+  return 1;
+}
+
+/*
+=item tiff_get16s
+
+Retrieve a 16-bit signed integer from offset.
+
+=cut
+*/
+
+static int
+tiff_get16s(imtiff *tiff, unsigned long offset) {
+  int result;
+
+  if (offset + 2 > tiff->size)
+    m_fatal(3, "attempt to get16 at %uld in %uld image", offset, tiff->size);
+
+  if (tiff->type == tt_intel) 
+    result = tiff->base[offset] + 0x100 * tiff->base[offset+1];
+  else
+    result = tiff->base[offset+1] + 0x100 * tiff->base[offset];
+
+  if (result > 0x7FFF)
+    result -= 0x10000;
+
+  return result;
+}
+
+/*
+=item tiff_get32s
+
+Retrieve a 32-bit signed integer from offset.
+
+=cut
+*/
+
+static int
+tiff_get32s(imtiff *tiff, unsigned long offset) {
+  unsigned work;
+
+  if (offset + 4 > tiff->size)
+    m_fatal(3, "attempt to get16 at %uld in %uld image", offset, tiff->size);
+
+  if (tiff->type == tt_intel) 
+    work = tiff->base[offset] + 0x100 * tiff->base[offset+1] 
+      + 0x10000 * tiff->base[offset+2] + 0x1000000 * tiff->base[offset+3];
+  else
+    work = tiff->base[offset+3] + 0x100 * tiff->base[offset+2]
+      + 0x10000 * tiff->base[offset+1] + 0x1000000 * tiff->base[offset];
+
+  /* not really needed on 32-bit int machines */
+  if (work > 0x7FFFFFFFUL)
+    return work - 0x80000000UL;
+  else
+    return work;
+}
+
+/*
+=item tiff_get_rat
+
+Retrieve an unsigned rational from offset.
+
+=cut
+*/
+
+static double
+tiff_get_rat(imtiff *tiff, unsigned long offset) {
+  unsigned long numer, denom;
+  if (offset + 8 > tiff->size)
+    m_fatal(3, "attempt to get_rat at %lu in %lu image", offset, tiff->size);
+
+  numer = tiff_get32(tiff, offset);
+  denom = tiff_get32(tiff, offset+4);
+
+  if (denom == 0) {
+    return -DBL_MAX;
+  }
+
+  return (double)numer / denom;
+}
+
+/*
+=item tiff_get_rats
+
+Retrieve an signed rational from offset.
+
+=cut
+*/
+
+static double
+tiff_get_rats(imtiff *tiff, unsigned long offset) {
+  long numer, denom;
+  if (offset + 8 > tiff->size)
+    m_fatal(3, "attempt to get_rat at %lu in %lu image", offset, tiff->size);
+
+  numer = tiff_get32s(tiff, offset);
+  denom = tiff_get32s(tiff, offset+4);
+
+  if (denom == 0) {
+    return -DBL_MAX;
+  }
+
+  return (double)numer / denom;
+}
+
+/*
+=back
+
+=head1 SEE ALSO
+
+L<Imager>, jpeg.c
+
+http://www.exif.org/
+
+=head1 AUTHOR
+
+Tony Cook <tony@imager.perl.org>
+
+=head1 REVISION
+
+$Revision$
+
+=cut
+*/
diff --git a/imexif.h b/imexif.h
new file mode 100644 (file)
index 0000000..6221ab3
--- /dev/null
+++ b/imexif.h
@@ -0,0 +1,10 @@
+/* imexif.h - interface to Exif handling */
+#ifndef IMAGER_IMEXIF_H
+#define IMAGER_IMEXIT_H
+
+#include <stddef.h>
+#include "imagei.h"
+
+extern int i_int_decode_exif(i_img *im, unsigned char *data, size_t length);
+
+#endif /* ifndef IMAGER_IMEXIF_H */
diff --git a/jpeg.c b/jpeg.c
index 84bc9e3..d6c7f03 100644 (file)
--- a/jpeg.c
+++ b/jpeg.c
@@ -32,8 +32,12 @@ Reads and writes JPEG images
 #include "jpeglib.h"
 #include "jerror.h"
 #include <errno.h>
+#ifdef IMEXIF_ENABLE
+#include "imexif.h"
+#endif
 
 #define JPEG_APP13       0xED    /* APP13 marker code */
+#define JPEG_APP1 (JPEG_APP0 + 1)
 #define JPGS 16384
 
 static unsigned char fake_eoi[]={(JOCTET) 0xFF,(JOCTET) JPEG_EOI};
@@ -341,11 +345,15 @@ my_error_exit (j_common_ptr cinfo) {
 i_img*
 i_readjpeg_wiol(io_glue *data, int length, char** iptc_itext, int *itlength) {
   i_img *im;
+#ifdef IMEXIF_ENABLE
+  int seen_exif = 0;
+#endif
 
   struct jpeg_decompress_struct cinfo;
   struct my_error_mgr jerr;
   JSAMPARRAY buffer;           /* Output row buffer */
   int row_stride;              /* physical row width in output buffer */
+  jpeg_saved_marker_ptr markerp;
 
   mm_log((1,"i_readjpeg_wiol(data 0x%p, length %d,iptc_itext 0x%p)\n", data, iptc_itext));
 
@@ -366,6 +374,8 @@ i_readjpeg_wiol(io_glue *data, int length, char** iptc_itext, int *itlength) {
   
   jpeg_create_decompress(&cinfo);
   jpeg_set_marker_processor(&cinfo, JPEG_APP13, APP13_handler);
+  jpeg_save_markers(&cinfo, JPEG_APP1, 0xFFFF);
+  jpeg_save_markers(&cinfo, JPEG_COM, 0xFFFF);
   jpeg_wiol_src(&cinfo, data, length);
 
   (void) jpeg_read_header(&cinfo, TRUE);
@@ -388,6 +398,23 @@ i_readjpeg_wiol(io_glue *data, int length, char** iptc_itext, int *itlength) {
     (void) jpeg_read_scanlines(&cinfo, buffer, 1);
     memcpy(im->idata+im->channels*im->xsize*(cinfo.output_scanline-1),buffer[0],row_stride);
   }
+
+  /* check for APP1 marker and save */
+  markerp = cinfo.marker_list;
+  while (markerp != NULL) {
+    if (markerp->marker == JPEG_COM) {
+      i_tags_add(&im->tags, "jpeg_comment", 0, markerp->data,
+                markerp->data_length, 0);
+    }
+#ifdef IMEXIF_ENABLE
+    else if (markerp->marker == JPEG_APP1 && !seen_exif) {
+      seen_exif = i_int_decode_exif(im, markerp->data, markerp->data_length);
+    }
+#endif      
+
+    markerp = markerp->next;
+  }
+
   (void) jpeg_finish_decompress(&cinfo);
   jpeg_destroy_decompress(&cinfo);
   *itlength=tlength;
index 95b7de0..3b370ac 100644 (file)
@@ -35,6 +35,10 @@ code snippet is sufficient:
 This will include some other information identifying libraries rather
 than file formats.
 
+=over 
+
+=item read
+
 Reading writing to and from files is simple, use the C<read()>
 method to read an image:
 
@@ -42,23 +46,31 @@ method to read an image:
   $img->read(file=>$filename, type=>$type)
     or die "Cannot read $filename: ", $img->errstr;
 
+=item write
+
 and the C<write()> method to write an image:
 
   $img->write(file=>$filename, type=>$type)
     or die "Cannot write $filename: ", $img->errstr;
 
+=item read_multi
+
 If you're reading from a format that supports multiple images per
 file, use the C<read_multi()> method:
 
   my @imgs = Imager->read_multi(file=>$filename, type=>$type)
     or die "Cannot read $filename: ", Imager->errstr;
 
+=item write_multi
+
 and if you want to write multiple images to a single file use the
 C<write_multi()> method:
 
   Imager->write_multi({ file=> $filename, type=>$type }, @images)
     or die "Cannot write $filename: ", Imager->errstr;
 
+=back
+
 If the I<filename> includes an extension that Imager recognizes, then
 you don't need the I<type>, but you may want to provide one anyway.
 See L</Guessing types> for information on controlling this
@@ -278,7 +290,54 @@ JPEG as a 3 channel image.
 
   $img->read(file=>'foo.jpg') or die $img->errstr;
 
-PNM does not support the spatial resolution tags.
+JPEG does not support the spatial resolution tags.
+
+If an APP1 block containing EXIF information is found, then any of the
+following tags can be set:
+
+=over
+
+exif_aperture exif_artist exif_brightness exif_color_space
+exif_contrast exif_copyright exif_custom_rendered exif_date_time
+exif_date_time_digitized exif_date_time_original
+exif_digital_zoom_ratio exif_exposure_bias exif_exposure_index
+exif_exposure_mode exif_exposure_program exif_exposure_time
+exif_f_number exif_flash exif_flash_energy exif_flashpix_version
+exif_focal_length exif_focal_length_in_35mm_film
+exif_focal_plane_resolution_unit exif_focal_plane_x_resolution
+exif_focal_plane_y_resolution exif_gain_control exif_image_description
+exif_image_unique_id exif_iso_speed_rating exif_make exif_max_aperture
+exif_metering_mode exif_model exif_orientation exif_related_sound_file
+exif_resolution_unit exif_saturation exif_scene_capture_type
+exif_sensing_method exif_sharpness exif_shutter_speed exif_software
+exif_spectral_sensitivity exif_sub_sec_time
+exif_sub_sec_time_digitized exif_sub_sec_time_original
+exif_subject_distance exif_subject_distance_range
+exif_subject_location exif_tag_light_source exif_user_comment
+exif_version exif_white_balance exif_x_resolution exif_y_resolution
+
+=back
+
+The following derived tags can also be set:
+
+=over
+
+exif_color_space_name exif_contrast_name exif_custom_rendered_name
+exif_exposure_mode_name exif_exposure_program_name exif_flash_name
+exif_focal_plane_resolution_unit_name exif_gain_control_name
+exif_light_source_name exif_metering_mode_name
+exif_resolution_unit_name exif_saturation_name
+exif_scene_capture_type_name exif_sensing_method_name
+exif_sharpness_name exif_subject_distance_range_name
+exif_white_balance_name
+
+=back
+
+The derived tags are for enumerated fields, when the value for the
+base field is valid then the text that appears in the EXIF
+specification for that value appears in the derived field.  So for
+example if C<exf_metering_mode> is C<5> then
+C<exif_metering_mode_name> is set to C<Pattern>.
 
 =head2 GIF (Graphics Interchange Format)
 
index b99f044..f0e428d 100644 (file)
@@ -2,7 +2,7 @@
 use strict;
 use lib 't';
 use Imager qw(:all);
-use Test::More tests => 24;
+use Test::More tests => 34;
 
 init_log("testout/t101jpeg.log",1);
 
@@ -110,5 +110,36 @@ if (!i_has_format("jpeg")) {
        "should succeed - just inside bytes limit");
     Imager->set_file_limits(reset=>1);
   }
+
+ SKIP:
+  {
+    # we don't test them all
+    my %expected_tags =
+      (
+       exif_date_time_original => "2005:11:25 00:00:00",
+       exif_flash => 0,
+       exif_image_description => "Imager Development Notes",
+       exif_make => "Canon",
+       exif_model => "CanoScan LiDE 35",
+       exif_resolution_unit => 2,
+       exif_resolution_unit_name => "inches",
+       exif_user_comment => "        Part of notes from reworking i_arc() and friends.",
+       exif_white_balance => 0,
+       exif_white_balance_name => "Auto white balance",
+      );
+
+    # exif tests
+    Imager::i_exif_enabled()
+       or skip("no exif support", scalar keys %expected_tags);
+
+    my $im = Imager->new;
+    $im->read(file=>"testimg/exiftest.jpg")
+      or skip("Could not read test image:".$im->errstr, scalar keys %expected_tags);
+
+    for my $key (keys %expected_tags) {
+      is($expected_tags{$key}, $im->tags(name => $key),
+        "test value of exif tag $key");
+    }
+  }
 }
 
diff --git a/testimg/exiftest.jpg b/testimg/exiftest.jpg
new file mode 100755 (executable)
index 0000000..59d5291
Binary files /dev/null and b/testimg/exiftest.jpg differ