about summary refs log tree commit diff
diff options
context:
space:
mode:
authorgiraffedata <giraffedata@9d0c8265-081b-0410-96cb-a4ca84ce46f8>2023-03-21 02:11:23 +0000
committergiraffedata <giraffedata@9d0c8265-081b-0410-96cb-a4ca84ce46f8>2023-03-21 02:11:23 +0000
commit2b2e88c44df8fe21456de2f8d21a89d172943f8f (patch)
treee0826490916d1e79f28abfffb3f25aca13635b98
parentb3cb451abd0d9718a6b0c7737bd02b7e4875be47 (diff)
downloadnetpbm-mirror-2b2e88c44df8fe21456de2f8d21a89d172943f8f.tar.gz
netpbm-mirror-2b2e88c44df8fe21456de2f8d21a89d172943f8f.tar.xz
netpbm-mirror-2b2e88c44df8fe21456de2f8d21a89d172943f8f.zip
cleanup
git-svn-id: http://svn.code.sf.net/p/netpbm/code/trunk@4521 9d0c8265-081b-0410-96cb-a4ca84ce46f8
-rw-r--r--converter/other/exif.c689
-rw-r--r--converter/other/exif.h79
-rw-r--r--converter/other/jpegtopnm.c2
3 files changed, 412 insertions, 358 deletions
diff --git a/converter/other/exif.c b/converter/other/exif.c
index 208eac59..c02aa9e3 100644
--- a/converter/other/exif.c
+++ b/converter/other/exif.c
@@ -7,20 +7,40 @@
   Bryan Henderson adapted it to Netpbm in September 2001.  Bryan
   added more of Wandel's code, from Jhead 1.9 dated December 2002 in
   January 2003.
+--------------------------------------------------------------------------*/
+
 
-  An EXIF header is a JFIF APP1 marker.  It is generated by some
-  digital cameras and contains information about the circumstances of
-  the creation of the image (camera settings, etc.).
+/*
+  N.B. "EXIF" refers to a whole image file format; some people think it is
+  just the format of the camera model, orientation, etc. data within the
+  image file.  EXIF is a subset of JFIF; an EXIF file is JFIF file
+  containing an EXIF header in the form of a JFIF APP1 marker.
 
-  The EXIF header uses the TIFF format, only it contains only tag
-  values and no actual image.
+  An EXIF header is generated by some digital cameras and contains information
+  about the circumstances of the creation of the image (camera settings,
+  etc.).
 
-  Note that the image format called EXIF is simply JFIF with an EXIF
-  header, i.e. a subformat of JFIF.
+  The EXIF header uses the TIFF format, only it contains only tag values and
+  no actual image.
+
+  Note that the image format called EXIF is simply JFIF with an EXIF header,
+  i.e. a subformat of JFIF.
 
   See the EXIF specs at http://exif.org (2001.09.01).
 
---------------------------------------------------------------------------*/
+  The basic format of the EXIF header is a sequence of IFDs (directories).  I
+  believe the first IFD is always for the main image and the 2nd IFD is for a
+  thumbnail image and is not present if there is no thumbnail image in the
+  file.
+
+  A directory is a sequence of tag/value pairs.
+
+  Each IFD can contain SubIFD, as the value of an EXIF Offset or Interop
+  Offset tag.
+
+*/
+
+
 #include "pm_config.h"
 #include <stdio.h>
 #include <stdlib.h>
@@ -42,6 +62,7 @@
 
 #include "pm_c_util.h"
 #include "pm.h"
+#include "mallocvar.h"
 #include "nstring.h"
 
 #include "exif.h"
@@ -262,58 +283,69 @@ get32u(const void * const data,
 
 
 
-static void
-printFormatNumber(FILE *       const fileP,
-                  const void * const valuePtr,
-                  int          const format,
-                  int          const byteCount,
-                  ByteOrder    const byteOrder) {
+static const char *
+numberTraceValue(const void * const valueP,
+                 int          const format,
+                 unsigned int const byteCt,
+                 ByteOrder    const byteOrder) {
 /*--------------------------------------------------------------------------
-   Display a number as one of its many formats
+   Format for display a number represented in any of the numeric formats
 --------------------------------------------------------------------------*/
+    const char * retval;
+
     switch(format) {
     case FMT_SBYTE:
     case FMT_BYTE:
-        fprintf(fileP, "%02x\n", *(unsigned char *)valuePtr);
+        pm_asprintf(&retval, "%02x", *(unsigned char *)valueP);
         break;
     case FMT_USHORT:
-        fprintf(fileP, "%d\n",get16u(valuePtr, byteOrder));
+        pm_asprintf(&retval, "%d",get16u(valueP, byteOrder));
         break;
     case FMT_ULONG:
     case FMT_SLONG:
-        fprintf(fileP, "%d\n",get32s(valuePtr, byteOrder));
+        pm_asprintf(&retval, "%d",get32s(valueP, byteOrder));
         break;
     case FMT_SSHORT:
-        fprintf(fileP, "%hd\n",(signed short)get16u(valuePtr, byteOrder));
+        pm_asprintf(&retval, "%hd",(signed short)get16u(valueP, byteOrder));
         break;
     case FMT_URATIONAL:
     case FMT_SRATIONAL:
-        fprintf(fileP, "%d/%d\n",get32s(valuePtr, byteOrder),
-                get32s(4+(char *)valuePtr, byteOrder));
+        pm_asprintf(&retval, "%d/%d",
+                    get32s(valueP, byteOrder),
+                    get32s(4+(char *)valueP,
+                           byteOrder));
         break;
     case FMT_SINGLE:
-        fprintf(fileP, "%f\n",(double)*(float *)valuePtr);
+        pm_asprintf(&retval, "%f",(double)*(float *)valueP);
         break;
     case FMT_DOUBLE:
-        fprintf(fileP, "%f\n",*(double *)valuePtr);
+        pm_asprintf(&retval, "%f",*(double *)valueP);
         break;
-    default:
-        fprintf(fileP, "Unknown format %d:", format);
-        {
-            unsigned int a;
-            for (a = 0; a < byteCount && a < 16; ++a)
-                printf("%02x", ((const unsigned char *)valuePtr)[a]);
+    default: {
+        char * hex;
+
+        MALLOCARRAY(hex, byteCt*2 + 1);
+        if (!hex)
+                retval = pm_strsol;
+        else {
+            unsigned int i;
+            for (i = 0; i < byteCt && i < 16; ++i) {
+                sprintf(&hex[i*2], "%02x",
+                        ((const unsigned char *)valueP)[i]);
+            }
+            pm_asprintf(&retval, "Unknown format %d: %s", format, hex);
         }
-        fprintf(fileP, "\n");
     }
+    }
+    return retval;
 }
 
 
 
 static double
-convertAnyFormat(const void * const valuePtr,
-                 int          const format,
-                 ByteOrder    const byteOrder) {
+numericValue(const void * const valuePtr,
+             int          const format,
+             ByteOrder    const byteOrder) {
 /*--------------------------------------------------------------------------
    Evaluate number, be it int, rational, or float from directory.
 --------------------------------------------------------------------------*/
@@ -359,62 +391,85 @@ convertAnyFormat(const void * const valuePtr,
 
 
 
+static const char *
+stringTraceValue(const unsigned char * const value,
+                 unsigned int          const valueSz) {
+
+    const char * retval;
+    char * buffer;
+
+    MALLOCARRAY(buffer, valueSz + 1);
+    if (!buffer)
+        retval = pm_strsol;
+    else {
+        unsigned int i;
+        bool noPrint;
+            /* We're in a sequence of unprintable characters.  We put one
+               '?' in the value for the whole sequence.
+            */
+        unsigned int outCursor;
+
+        outCursor = 0;  /* initial value */
+
+        for (i = 0, noPrint = false; i < valueSz; ++i) {
+            if (ISPRINT(value[i])) {
+                buffer[outCursor++] = value[i];
+                noPrint = false;
+            } else {
+                if (!noPrint) {
+                    buffer[outCursor++] = '?';
+                    noPrint = true;
+                }
+            }
+        }
+        buffer[outCursor++] = '\0';
+
+        retval = buffer;
+    }
+    return retval;
+}
+
+
+
 static void
 traceTag(int                   const tag,
          int                   const format,
-         const unsigned char * const valuePtr,
-         unsigned int          const byteCount,
+         const unsigned char * const value,
+         unsigned int          const valueSz,
          ByteOrder             const byteOrder) {
 
-    /* Show tag name */
-    unsigned int a;
-    bool found;
-    for (a = 0, found = false; !found; ++a) {
-        if (tagTable[a].tag == 0) {
-            fprintf(stderr, "  Unknown Tag %04x Value = ", tag);
-            found = true;
-        }
-        if (tagTable[a].tag == tag) {
-            fprintf(stderr, "    %s = ",tagTable[a].desc);
-            found = true;
-        }
+    const char * tagNm;
+    const char * tagValue;
+    unsigned int i;
+
+    for (i = 0, tagNm = NULL;tagTable[i].tag; ++i) {
+        if (tagTable[i].tag == tag)
+            tagNm = pm_strdup(tagTable[i].desc);
     }
 
+    if (!tagNm)
+        pm_asprintf(&tagNm, "Unknown Tag %04x", tag);
+
     /* Show tag value. */
-    switch(format) {
+    switch (format) {
 
     case FMT_UNDEFINED:
         /* Undefined is typically an ascii string. */
 
     case FMT_STRING: {
-        /* String arrays printed without function call
-           (different from int arrays)
-        */
-        bool noPrint;
+        tagValue = stringTraceValue(value, valueSz);
 
-        printf("\"");
-        for (a = 0, noPrint = false; a < byteCount; ++a) {
-            if (ISPRINT((valuePtr)[a])) {
-                fprintf(stderr, "%c", valuePtr[a]);
-                noPrint = false;
-            } else {
-                /* Avoiding indicating too many unprintable characters of
-                   proprietary bits of binary information this program may not
-                   know how to parse.
-                */
-                if (!noPrint) {
-                    fprintf(stderr, "?");
-                    noPrint = true;
-                }
-            }
-        }
         fprintf(stderr, "\"\n");
     } break;
 
     default:
         /* Handle arrays of numbers later (will there ever be?)*/
-        printFormatNumber(stderr, valuePtr, format, byteCount, byteOrder);
+        tagValue = numberTraceValue(value, format, valueSz, byteOrder);
     }
+    pm_message("%s = \"%s\"", tagNm, tagValue);
+
+    pm_strfree(tagValue);
+    pm_strfree(tagNm);
 }
 
 
@@ -422,13 +477,13 @@ traceTag(int                   const tag,
 /* Forward declaration for recursion */
 
 static void
-processExifDir(const unsigned char *  const exifData,
-               unsigned int           const exifLength,
-               unsigned int           const dirOffset,
-               exif_ImageInfo *       const imageInfoP,
-               ByteOrder              const byteOrder,
-               bool                   const wantTagTrace,
-               const unsigned char ** const lastExifRefdP);
+processIfd(const unsigned char *  const exifData,
+           unsigned int           const exifLength,
+           unsigned int           const dirOffset,
+           exif_ifd *             const ifdP,
+           ByteOrder              const byteOrder,
+           bool                   const wantTagTrace,
+           const unsigned char ** const lastExifRefdP);
 
 
 static void
@@ -437,7 +492,7 @@ processDirEntry(const unsigned char *  const dirEntry,
                 unsigned int           const exifLength,
                 ByteOrder              const byteOrder,
                 bool                   const wantTagTrace,
-                exif_ImageInfo *       const imageInfoP,
+                exif_ifd *             const ifdP,
                 unsigned int *         const thumbnailOffsetP,
                 unsigned int *         const thumbnailSizeP,
                 bool *                 const haveThumbnailP,
@@ -452,7 +507,7 @@ processDirEntry(const unsigned char *  const dirEntry,
            other types when used.  But we use it as a byte-by-byte cursor, so
            we declare it as a pointer to a generic byte here.
         */
-    unsigned int byteCount;
+    unsigned int valueSz;
 
     if ((format-1) >= NUM_FORMATS) {
         /* (-1) catches illegal zero case as unsigned underflows
@@ -462,17 +517,17 @@ processDirEntry(const unsigned char *  const dirEntry,
         return;
     }
 
-    byteCount = components * bytesPerFormat[format];
+    valueSz = components * bytesPerFormat[format];
 
-    if (byteCount > 4) {
+    if (valueSz > 4) {
         unsigned const offsetVal = get32u(&dirEntry[8], byteOrder);
         /* If its bigger than 4 bytes, the dir entry contains an offset.*/
-        if (offsetVal + byteCount > exifLength) {
+        if (offsetVal + valueSz > exifLength) {
             /* Bogus pointer offset and / or bytecount value */
             pm_message("Illegal pointer offset value in EXIF "
                        "for tag %04x.  "
                        "Offset %d bytes %d ExifLen %d\n",
-                       tag, offsetVal, byteCount, exifLength);
+                       tag, offsetVal, valueSz, exifLength);
             return;
         }
         valuePtr = &exifData[offsetVal];
@@ -481,16 +536,16 @@ processDirEntry(const unsigned char *  const dirEntry,
         valuePtr = &dirEntry[8];
     }
 
-    if (*lastExifRefdP < valuePtr + byteCount) {
+    if (*lastExifRefdP < valuePtr + valueSz) {
         /* Keep track of last byte in the exif header that was actually
            referenced.  That way, we know where the discardable thumbnail data
            begins.
         */
-        *lastExifRefdP = valuePtr + byteCount;
+        *lastExifRefdP = valuePtr + valueSz;
     }
 
     if (wantTagTrace)
-        traceTag(tag, format, valuePtr, byteCount, byteOrder);
+        traceTag(tag, format, valuePtr, valueSz, byteOrder);
 
     *haveThumbnailP = (tag == TAG_THUMBNAIL_OFFSET);
 
@@ -498,26 +553,25 @@ processDirEntry(const unsigned char *  const dirEntry,
     switch (tag) {
 
     case TAG_MAKE:
-        STRSCPY(imageInfoP->CameraMake, (const char*)valuePtr);
+        STRSCPY(ifdP->cameraMake, (const char*)valuePtr);
         break;
 
     case TAG_MODEL:
-        STRSCPY(imageInfoP->CameraModel, (const char*)valuePtr);
+        STRSCPY(ifdP->cameraModel, (const char*)valuePtr);
         break;
 
     case TAG_XRESOLUTION:
-        imageInfoP->XResolution =
-            convertAnyFormat(valuePtr, format, byteOrder);
+        ifdP->xResolution =
+            numericValue(valuePtr, format, byteOrder);
         break;
 
     case TAG_YRESOLUTION:
-        imageInfoP->YResolution =
-            convertAnyFormat(valuePtr, format, byteOrder);
+        ifdP->yResolution =
+            numericValue(valuePtr, format, byteOrder);
         break;
 
     case TAG_DATETIME_ORIGINAL:
-        STRSCPY(imageInfoP->DateTime, (const char*)valuePtr);
-        imageInfoP->DatePointer = (const char*)valuePtr;
+        STRSCPY(ifdP->dateTime, (const char*)valuePtr);
         break;
 
     case TAG_USERCOMMENT: {
@@ -530,7 +584,7 @@ processDirEntry(const unsigned char *  const dirEntry,
         unsigned int outCursor;
         unsigned int end;
 
-        for (end = byteCount; end > 0 && value[end] == ' '; --end);
+        for (end = valueSz; end > 0 && value[end] == ' '; --end);
 
         /* Skip "ASCII" if it is there */
         if (end >= 5 && memeq(value, "ASCII", 5))
@@ -541,7 +595,7 @@ processDirEntry(const unsigned char *  const dirEntry,
         /* Skip consecutive blanks and NULs */
 
         for (;
-             cursor < byteCount &&
+             cursor < valueSz &&
                  (value[cursor] == '\0' || value[cursor] == ' ');
              ++cursor);
 
@@ -550,17 +604,17 @@ processDirEntry(const unsigned char *  const dirEntry,
         for (outCursor = 0;
              cursor < end && outCursor < MAX_COMMENT-1;
              ++cursor)
-            imageInfoP->Comments[outCursor++] = value[cursor];
+            ifdP->comments[outCursor++] = value[cursor];
 
-        imageInfoP->Comments[outCursor++] = '\0';
+        ifdP->comments[outCursor++] = '\0';
     } break;
 
     case TAG_FNUMBER:
         /* Simplest way of expressing aperture, so I trust it the most.
            (overwrite previously computd value if there is one)
         */
-        imageInfoP->ApertureFNumber =
-            (float)convertAnyFormat(valuePtr, format, byteOrder);
+        ifdP->apertureFNumber =
+            (float)numericValue(valuePtr, format, byteOrder);
         break;
 
     case TAG_APERTURE:
@@ -568,9 +622,9 @@ processDirEntry(const unsigned char *  const dirEntry,
         /* More relevant info always comes earlier, so only use this field if
            we don't have appropriate aperture information yet.
         */
-        if (imageInfoP->ApertureFNumber == 0) {
-            imageInfoP->ApertureFNumber = (float)
-                exp(convertAnyFormat(valuePtr, format, byteOrder)
+        if (ifdP->apertureFNumber == 0) {
+            ifdP->apertureFNumber = (float)
+                exp(numericValue(valuePtr, format, byteOrder)
                     * log(2) * 0.5);
         }
         break;
@@ -580,16 +634,16 @@ processDirEntry(const unsigned char *  const dirEntry,
            as a function of how farthey are zoomed in.
         */
 
-        imageInfoP->FocalLength =
-            (float)convertAnyFormat(valuePtr, format, byteOrder);
+        ifdP->focalLength =
+            (float)numericValue(valuePtr, format, byteOrder);
         break;
 
     case TAG_SUBJECT_DISTANCE:
         /* Inidcates the distacne the autofocus camera is focused to.
            Tends to be less accurate as distance increases.
         */
-        imageInfoP->Distance =
-            (float)convertAnyFormat(valuePtr, format, byteOrder);
+        ifdP->distance =
+            (float)numericValue(valuePtr, format, byteOrder);
         break;
 
     case TAG_EXPOSURETIME:
@@ -597,8 +651,8 @@ processDirEntry(const unsigned char *  const dirEntry,
            trust it most.  (overwrite previously computd value
            if there is one)
         */
-        imageInfoP->ExposureTime =
-            (float)convertAnyFormat(valuePtr, format, byteOrder);
+        ifdP->exposureTime =
+            (float)numericValue(valuePtr, format, byteOrder);
         break;
 
     case TAG_SHUTTERSPEED:
@@ -606,29 +660,29 @@ processDirEntry(const unsigned char *  const dirEntry,
            so only use this value if we don't already have it
            from somewhere else.
         */
-        if (imageInfoP->ExposureTime == 0) {
-            imageInfoP->ExposureTime = (float)
-                (1/exp(convertAnyFormat(valuePtr, format, byteOrder)
+        if (ifdP->exposureTime == 0) {
+            ifdP->exposureTime = (float)
+                (1/exp(numericValue(valuePtr, format, byteOrder)
                        * log(2)));
         }
         break;
 
     case TAG_FLASH:
-        if ((int)convertAnyFormat(valuePtr, format, byteOrder) & 0x7) {
-            imageInfoP->FlashUsed = TRUE;
+        if ((int)numericValue(valuePtr, format, byteOrder) & 0x7) {
+            ifdP->flashUsed = TRUE;
         }else{
-            imageInfoP->FlashUsed = FALSE;
+            ifdP->flashUsed = FALSE;
         }
         break;
 
     case TAG_ORIENTATION:
-        imageInfoP->Orientation =
-            (int)convertAnyFormat(valuePtr, format, byteOrder);
-        if (imageInfoP->Orientation < 1 ||
-            imageInfoP->Orientation > 8) {
+        ifdP->orientation =
+            (int)numericValue(valuePtr, format, byteOrder);
+        if (ifdP->orientation < 1 ||
+            ifdP->orientation > 8) {
             pm_message("Undefined rotation value %d",
-                       imageInfoP->Orientation);
-            imageInfoP->Orientation = 0;
+                       ifdP->orientation);
+            ifdP->orientation = 0;
         }
         break;
 
@@ -639,16 +693,16 @@ processDirEntry(const unsigned char *  const dirEntry,
         */
         exifImageWidth =
             MIN(exifImageWidth,
-                (int)convertAnyFormat(valuePtr, format, byteOrder));
+                (int)numericValue(valuePtr, format, byteOrder));
         break;
 
     case TAG_FOCALPLANEXRES:
         haveXRes = true;
-        focalplaneXRes = convertAnyFormat(valuePtr, format, byteOrder);
+        focalplaneXRes = numericValue(valuePtr, format, byteOrder);
         break;
 
     case TAG_FOCALPLANEUNITS:
-        switch((int)convertAnyFormat(valuePtr, format, byteOrder)) {
+        switch((int)numericValue(valuePtr, format, byteOrder)) {
         case 1: focalplaneUnits = 25.4; break; /* 1 inch */
         case 2:
             /* According to the information I was using, 2
@@ -670,45 +724,45 @@ processDirEntry(const unsigned char *  const dirEntry,
         */
 
     case TAG_EXPOSURE_BIAS:
-        imageInfoP->ExposureBias =
-            (float) convertAnyFormat(valuePtr, format, byteOrder);
+        ifdP->exposureBias =
+            (float) numericValue(valuePtr, format, byteOrder);
         break;
 
     case TAG_WHITEBALANCE:
-        imageInfoP->Whitebalance =
-            (int)convertAnyFormat(valuePtr, format, byteOrder);
+        ifdP->whiteBalance =
+            (int)numericValue(valuePtr, format, byteOrder);
         break;
 
     case TAG_METERING_MODE:
-        imageInfoP->MeteringMode =
-            (int)convertAnyFormat(valuePtr, format, byteOrder);
+        ifdP->meteringMode =
+            (int)numericValue(valuePtr, format, byteOrder);
         break;
 
     case TAG_EXPOSURE_PROGRAM:
-        imageInfoP->ExposureProgram =
-            (int)convertAnyFormat(valuePtr, format, byteOrder);
+        ifdP->exposureProgram =
+            (int)numericValue(valuePtr, format, byteOrder);
         break;
 
     case TAG_ISO_EQUIVALENT:
-        imageInfoP->ISOequivalent =
-            (int)convertAnyFormat(valuePtr, format, byteOrder);
-        if ( imageInfoP->ISOequivalent < 50 )
-            imageInfoP->ISOequivalent *= 200;
+        ifdP->isoEquivalent =
+            (int)numericValue(valuePtr, format, byteOrder);
+        if ( ifdP->isoEquivalent < 50 )
+            ifdP->isoEquivalent *= 200;
         break;
 
     case TAG_COMPRESSION_LEVEL:
-        imageInfoP->CompressionLevel =
-            (int)convertAnyFormat(valuePtr, format, byteOrder);
+        ifdP->compressionLevel =
+            (int)numericValue(valuePtr, format, byteOrder);
         break;
 
     case TAG_THUMBNAIL_OFFSET:
         *thumbnailOffsetP = (unsigned int)
-            convertAnyFormat(valuePtr, format, byteOrder);
+            numericValue(valuePtr, format, byteOrder);
         break;
 
     case TAG_THUMBNAIL_LENGTH:
         *thumbnailSizeP = (unsigned int)
-            convertAnyFormat(valuePtr, format, byteOrder);
+            numericValue(valuePtr, format, byteOrder);
         break;
 
     case TAG_EXIF_OFFSET:
@@ -720,9 +774,9 @@ processDirEntry(const unsigned char *  const dirEntry,
                        "but Exif data is only %u bytes.",
                        subdirOffset, exifLength);
         else
-            processExifDir(exifData, exifLength, subdirOffset,
-                           imageInfoP, byteOrder, wantTagTrace,
-                           lastExifRefdP);
+            processIfd(exifData, exifLength, subdirOffset,
+                       ifdP, byteOrder, wantTagTrace,
+                       lastExifRefdP);
     } break;
     }
 }
@@ -730,15 +784,15 @@ processDirEntry(const unsigned char *  const dirEntry,
 
 
 static void
-processExifDir(const unsigned char *  const exifData,
-               unsigned int           const exifLength,
-               unsigned int           const dirOffset,
-               exif_ImageInfo *       const imageInfoP,
-               ByteOrder              const byteOrder,
-               bool                   const wantTagTrace,
-               const unsigned char ** const lastExifRefdP) {
+processIfd(const unsigned char *  const exifData,
+           unsigned int           const exifLength,
+           unsigned int           const dirOffset,
+           exif_ifd *             const ifdP,
+           ByteOrder              const byteOrder,
+           bool                   const wantTagTrace,
+           const unsigned char ** const lastExifRefdP) {
 /*--------------------------------------------------------------------------
-   Process one of the nested EXIF directories.
+   Process one of the nested EXIF IFDs (Image File Directory).
 --------------------------------------------------------------------------*/
     const unsigned char * const dirStart = exifData + dirOffset;
     unsigned int const numDirEntries = get16u(&dirStart[0], byteOrder);
@@ -778,7 +832,7 @@ processExifDir(const unsigned char *  const exifData,
 
     for (de = 0, haveThumbnail = false; de < numDirEntries; ++de)
         processDirEntry(DIR_ENTRY_ADDR(dirStart, de), exifData, exifLength,
-                        byteOrder, wantTagTrace, imageInfoP,
+                        byteOrder, wantTagTrace, ifdP,
                         &thumbnailOffset, &thumbnailSize, &haveThumbnail,
                         lastExifRefdP);
 
@@ -786,10 +840,8 @@ processExifDir(const unsigned char *  const exifData,
         dirWithThumbnailPtrs = dirStart;
 
     {
-        /* In addition to linking to subdirectories via exif tags,
-           there's also a potential link to another directory at the end
-           of each directory.  This has got to be the result of a
-           committee!
+        /* Recursively process the next directory in the chain, if there is
+           one
         */
         if (DIR_ENTRY_ADDR(dirStart, numDirEntries) + 4 <=
             exifData + exifLength) {
@@ -805,16 +857,16 @@ processExifDir(const unsigned char *  const exifData,
                            I'll just let it pass silently.
                         */
                         if (wantTagTrace)
-                            printf("Thumbnail removed with "
-                                   "Jhead 1.3 or earlier\n");
+                            pm_message("Thumbnail removed with "
+                                       "Jhead 1.3 or earlier");
                     } else {
                         pm_message("Illegal subdirectory link");
                     }
                 } else {
                     if (subdirOffset <= exifLength)
-                        processExifDir(exifData, exifLength, subdirOffset,
-                                       imageInfoP, byteOrder, wantTagTrace,
-                                       lastExifRefdP);
+                        processIfd(exifData, exifLength, subdirOffset,
+                                   ifdP, byteOrder, wantTagTrace,
+                                   lastExifRefdP);
                 }
             }
         } else {
@@ -825,8 +877,8 @@ processExifDir(const unsigned char *  const exifData,
     if (thumbnailSize && thumbnailOffset) {
         if (thumbnailSize + thumbnailOffset <= exifLength) {
             /* The thumbnail pointer appears to be valid.  Store it. */
-            imageInfoP->ThumbnailPointer = exifData + thumbnailOffset;
-            imageInfoP->ThumbnailSize = thumbnailSize;
+            ifdP->thumbnail = exifData + thumbnailOffset;
+            ifdP->thumbnailSize = thumbnailSize;
 
             if (wantTagTrace) {
                 fprintf(stderr, "Thumbnail size: %u bytes\n", thumbnailSize);
@@ -896,7 +948,8 @@ exif_parse(const unsigned char * const exifData,
             pm_message("Suspicious offset of first IFD value in Exif header");
         }
 
-        imageInfoP->Comments[0] = '\0';  /* Initial value - null string */
+        imageInfoP->mainImage.comments[0] = '\0';
+            /* Initial value - null string */
 
         haveXRes = FALSE;  /* Initial assumption */
         focalplaneUnits = 0;
@@ -905,16 +958,17 @@ exif_parse(const unsigned char * const exifData,
         lastExifRefd = exifData;
         dirWithThumbnailPtrs = NULL;
 
-        processExifDir(exifData, length, firstOffset,
-                       imageInfoP, byteOrder, wantTagTrace, &lastExifRefd);
+        processIfd(exifData, length, firstOffset,
+                   &imageInfoP->mainImage, byteOrder, wantTagTrace,
+                   &lastExifRefd);
 
         /* Compute the CCD width, in millimeters. */
-        if (haveXRes) {
-            imageInfoP->HaveCCDWidth = 1;
-            imageInfoP->CCDWidth =
+        if (haveXRes && exifImageWidth) {
+            imageInfoP->mainImage.haveCCDWidth = 1;
+            imageInfoP->mainImage.ccdWidth =
                     (float)(exifImageWidth * focalplaneUnits / focalplaneXRes);
         } else
-            imageInfoP->HaveCCDWidth = 0;
+            imageInfoP->mainImage.haveCCDWidth = 0;
 
         if (wantTagTrace) {
             fprintf(stderr,
@@ -926,47 +980,25 @@ exif_parse(const unsigned char * const exifData,
 
 
 
-void
-exif_showImageInfo(const exif_ImageInfo * const imageInfoP,
-                   FILE *                 const fileP) {
-/*--------------------------------------------------------------------------
-   Show the collected image info, displaying camera F-stop and shutter
-   speed in a consistent and legible fashion.
---------------------------------------------------------------------------*/
-    if (imageInfoP->CameraMake[0]) {
-        fprintf(fileP, "Camera make  : %s\n", imageInfoP->CameraMake);
-        fprintf(fileP, "Camera model : %s\n", imageInfoP->CameraModel);
+static void
+showIfd(const exif_ifd * const ifdP) {
+
+    if (ifdP->cameraMake[0]) {
+        pm_message("Camera make  : %s", ifdP->cameraMake);
+        pm_message("Camera model : %s", ifdP->cameraModel);
     }
-    if (imageInfoP->DateTime[0])
-        fprintf(fileP, "Date/Time    : %s\n", imageInfoP->DateTime);
-
-    fprintf(fileP, "Resolution   : %f x %f\n",
-            imageInfoP->XResolution, imageInfoP->YResolution);
-
-    if (imageInfoP->Orientation > 1) {
-
-        /* Print orientation only if one was supplied, and if its not
-           1 (normal orientation)
-
-           1 - The 0th row is at the visual top of the image
-               and the 0th column is the visual left-hand side.
-           2 - The 0th row is at the visual top of the image
-               and the 0th column is the visual right-hand side.
-           3 - The 0th row is at the visual bottom of the image
-               and the 0th column is the visual right-hand side.
-           4 - The 0th row is at the visual bottom of the image
-               and the 0th column is the visual left-hand side.
-           5 - The 0th row is the visual left-hand side of of the image
-               and the 0th column is the visual top.
-           6 - The 0th row is the visual right-hand side of of the image
-               and the 0th column is the visual top.
-           7 - The 0th row is the visual right-hand side of of the image
-               and the 0th column is the visual bottom.
-           8 - The 0th row is the visual left-hand side of of the image
-               and the 0th column is the visual bottom.
-
-           Note: The descriptions here are the same as the name of the
-           command line option to pass to jpegtran to right the image
+    if (ifdP->dateTime[0])
+        pm_message("Date/Time    : %s", ifdP->dateTime);
+
+    pm_message("Resolution   : %f x %f",
+               ifdP->xResolution, ifdP->yResolution);
+
+    if (ifdP->orientation > 1) {
+        /* Note that orientation is usually understood to be the orientation
+           of the camera, not of the image.  The top, bottom, left, and right
+           sides of an image are defined in the JFIF format.
+
+           But values such as "flip horizontal" make no sense for that.
         */
         static const char * orientTab[9] = {
             "Undefined",
@@ -980,144 +1012,157 @@ exif_showImageInfo(const exif_ImageInfo * const imageInfoP,
             "rotate 270",       /* rotate 270 to right it. */
         };
 
-        fprintf(fileP, "Orientation  : %s\n",
-                orientTab[imageInfoP->Orientation]);
+        pm_message("Camera orientation  : %s",
+                   orientTab[ifdP->orientation]);
     }
 
-    if (imageInfoP->IsColor == 0)
-        fprintf(fileP, "Color/bw     : Black and white\n");
+    if (ifdP->isColor == 0)
+        pm_message("Color/bw     : Black and white");
 
-    if (imageInfoP->FlashUsed >= 0)
-        fprintf(fileP, "Flash used   : %s\n",
-                imageInfoP->FlashUsed ? "Yes" :"No");
+    if (ifdP->flashUsed >= 0)
+        pm_message("Flash used   : %s",
+                   ifdP->flashUsed ? "Yes" :"No");
 
-    if (imageInfoP->FocalLength) {
-        fprintf(fileP, "Focal length : %4.1fmm",
-                (double)imageInfoP->FocalLength);
-        if (imageInfoP->HaveCCDWidth) {
-            fprintf(fileP, "  (35mm equivalent: %dmm)",
-                    (int)
-                    (imageInfoP->FocalLength/imageInfoP->CCDWidth*36 + 0.5));
-        }
-        fprintf(fileP, "\n");
+    if (ifdP->focalLength) {
+        const char * mm35equiv;
+
+        if (ifdP->haveCCDWidth) {
+            pm_asprintf(&mm35equiv, "  (35mm equivalent: %dmm)",
+                        (int) (ifdP->focalLength/ifdP->ccdWidth*36 + 0.5));
+        } else
+            mm35equiv = pm_strdup("");
+
+        pm_message("Focal length : %4.1fmm %s",
+                   (double)ifdP->focalLength, mm35equiv);
+
+        pm_strfree(mm35equiv);
     }
 
-    if (imageInfoP->HaveCCDWidth)
-        fprintf(fileP, "CCD width    : %2.4fmm\n",
-                (double)imageInfoP->CCDWidth);
+    if (ifdP->haveCCDWidth)
+        pm_message("CCD width    : %2.4fmm", (double)ifdP->ccdWidth);
 
-    if (imageInfoP->ExposureTime) {
-        if (imageInfoP->ExposureTime < 0.010) {
-            fprintf(fileP,
-                    "Exposure time: %6.4f s ",
-                    (double)imageInfoP->ExposureTime);
-        }else{
-            fprintf(fileP,
-                    "Exposure time: %5.3f s ",
-                    (double)imageInfoP->ExposureTime);
-        }
-        if (imageInfoP->ExposureTime <= 0.5) {
-            fprintf(fileP, " (1/%d)",(int)(0.5 + 1/imageInfoP->ExposureTime));
+    if (ifdP->exposureTime) {
+        const char * timeDisp;
+        const char * recipDisp;
+
+        if (ifdP->exposureTime < 0.010) {
+            pm_asprintf(&timeDisp, "%6.4f s", (double)ifdP->exposureTime);
+        } else {
+            pm_asprintf(&timeDisp, "%5.3f s", (double)ifdP->exposureTime);
         }
-        fprintf(fileP, "\n");
+        if (ifdP->exposureTime <= 0.5) {
+            pm_asprintf(&recipDisp, " (1/%d)",
+                        (int)(0.5 + 1/ifdP->exposureTime));
+        } else
+            recipDisp = pm_strdup("");
+
+        pm_message("Exposure time: %s %s", timeDisp, recipDisp);
+
+        pm_strfree(recipDisp);
+        pm_strfree(timeDisp);
     }
-    if (imageInfoP->ApertureFNumber) {
-        fprintf(fileP, "Aperture     : f/%3.1f\n",
-                (double)imageInfoP->ApertureFNumber);
+    if (ifdP->apertureFNumber) {
+        pm_message("Aperture     : f/%3.1f", (double)ifdP->apertureFNumber);
     }
-    if (imageInfoP->Distance) {
-        if (imageInfoP->Distance < 0) {
-            fprintf(fileP, "Focus dist.  : Infinite\n");
-        }else{
-            fprintf(fileP, "Focus dist.  :%5.2fm\n",
-                    (double)imageInfoP->Distance);
-        }
+    if (ifdP->distance) {
+        if (ifdP->distance < 0)
+            pm_message("Focus dist.  : Infinite");
+        else
+            pm_message("Focus dist.  :%5.2fm", (double)ifdP->distance);
     }
 
-    if (imageInfoP->ISOequivalent) { /* 05-jan-2001 vcs */
-        fprintf(fileP, "ISO equiv.   : %2d\n",(int)imageInfoP->ISOequivalent);
-    }
-    if (imageInfoP->ExposureBias) { /* 05-jan-2001 vcs */
-        fprintf(fileP, "Exposure bias:%4.2f\n",
-                (double)imageInfoP->ExposureBias);
-    }
+    if (ifdP->isoEquivalent)
+        pm_message("ISO equiv.   : %2d",(int)ifdP->isoEquivalent);
 
-    if (imageInfoP->Whitebalance) { /* 05-jan-2001 vcs */
-        switch(imageInfoP->Whitebalance) {
-        case 1:
-            fprintf(fileP, "Whitebalance : sunny\n");
-            break;
-        case 2:
-            fprintf(fileP, "Whitebalance : fluorescent\n");
-            break;
-        case 3:
-            fprintf(fileP, "Whitebalance : incandescent\n");
-            break;
-        default:
-            fprintf(fileP, "Whitebalance : cloudy\n");
+    if (ifdP->exposureBias)
+        pm_message("Exposure bias:%4.2f", (double)ifdP->exposureBias);
+
+    if (ifdP->whiteBalance) {
+        const char * whiteBalanceDisp;
+
+        switch(ifdP->whiteBalance) {
+        case 1:  whiteBalanceDisp = "sunny";         break;
+        case 2:  whiteBalanceDisp = "fluorescent";   break;
+        case 3:  whiteBalanceDisp = "incandescent";  break;
+        default: whiteBalanceDisp = "cloudy";        break;
         }
+        pm_message("Whitebalance : %s", whiteBalanceDisp);
     }
-    if (imageInfoP->MeteringMode) { /* 05-jan-2001 vcs */
-        switch(imageInfoP->MeteringMode) {
-        case 2:
-            fprintf(fileP, "Metering Mode: center weight\n");
-            break;
-        case 3:
-            fprintf(fileP, "Metering Mode: spot\n");
-            break;
-        case 5:
-            fprintf(fileP, "Metering Mode: matrix\n");
-            break;
+    if (ifdP->meteringMode) {
+        const char * meteringModeDisp;
+
+        switch(ifdP->meteringMode) {
+        case 2: meteringModeDisp = "center weight";  break;
+        case 3: meteringModeDisp = "spot";           break;
+        case 5: meteringModeDisp = "matrix";         break;
         }
+        pm_message("Metering Mode: %s", meteringModeDisp);
     }
-    if (imageInfoP->ExposureProgram) { /* 05-jan-2001 vcs */
-        switch(imageInfoP->ExposureProgram) {
-        case 2:
-            fprintf(fileP, "Exposure     : program (auto)\n");
-            break;
-        case 3:
-            fprintf(fileP, "Exposure     : aperture priority (semi-auto)\n");
-            break;
-        case 4:
-            fprintf(fileP, "Exposure     : shutter priority (semi-auto)\n");
-            break;
+    if (ifdP->exposureProgram) {
+        const char * exposureDisp;
+
+        switch(ifdP->exposureProgram) {
+        case 2: exposureDisp = "program (auto)";                break;
+        case 3: exposureDisp = "aperture priority (semi-auto)"; break;
+        case 4: exposureDisp = "shutter priority (semi-auto)";  break;
         }
+        pm_message("Exposure     : %s", exposureDisp);
     }
-    if (imageInfoP->CompressionLevel) { /* 05-jan-2001 vcs */
-        switch(imageInfoP->CompressionLevel) {
-        case 1:
-            fprintf(fileP, "Jpeg Quality  : basic\n");
-            break;
-        case 2:
-            fprintf(fileP, "Jpeg Quality  : normal\n");
-            break;
-        case 4:
-            fprintf(fileP, "Jpeg Quality  : fine\n");
-            break;
+    if (ifdP->compressionLevel) {
+        const char * jpegQualityDisp;
+
+        switch(ifdP->compressionLevel) {
+        case 1: jpegQualityDisp = "basic";  break;
+        case 2: jpegQualityDisp = "normal"; break;
+        case 4: jpegQualityDisp = "fine";   break;
        }
+        pm_message("Jpeg Quality  : %s", jpegQualityDisp);
     }
 
-    /* Print the comment. Print 'Comment:' for each new line of comment. */
-    if (imageInfoP->Comments[0]) {
-        unsigned int a;
-
-        fprintf(fileP, "Comment      : ");
-
-        for (a = 0; a < MAX_COMMENT && imageInfoP->Comments[a]; ++a) {
-            char const c = imageInfoP->Comments[a];
-            if (c == '\n') {
-                /* Do not start a new line if the string ends with a cr */
-                if (imageInfoP->Comments[a+1] != '\0')
-                    fprintf(fileP, "\nComment      : ");
-                else
-                    fprintf(fileP, "\n");
-            } else
-                putc(c, fileP);
+    if (ifdP->comments[0]) {
+        char * buffer;
+
+        MALLOCARRAY(buffer, strlen(ifdP->comments) + 1);
+
+        if (!buffer)
+            pm_message("Out of memory allocating a buffer for %u "
+                       "characters of comments",
+                       (unsigned)strlen(ifdP->comments));
+        else {
+            unsigned int i;
+            unsigned int outCursor;
+
+            strcpy(buffer, "Comment:  ");  /* Permanently in buffer */
+
+            outCursor = 10;  /* initial value */
+
+            for (i = 0; ifdP->comments[i]; ++i) {
+                char const c = ifdP->comments[i];
+                if (c == '\n') {
+                    buffer[outCursor++] = '\0';
+                    pm_message("%s", buffer);
+                    outCursor = 10;
+                } else
+                    buffer[outCursor++] = c;
+            }
+            if (outCursor > 10)
+                pm_message("%s", buffer);
+
+            free(buffer);
         }
-        fprintf(fileP, "\n");
     }
+}
+
+
+
+void
+exif_showImageInfo(const exif_ImageInfo * const imageInfoP) {
+/*--------------------------------------------------------------------------
+   Show the collected image info, displaying camera F-stop and shutter
+   speed in a consistent and legible fashion.
+--------------------------------------------------------------------------*/
 
-    fprintf(fileP, "\n");
+    showIfd(&imageInfoP->mainImage);
 }
 
 
diff --git a/converter/other/exif.h b/converter/other/exif.h
index 1a703694..d06a1185 100644
--- a/converter/other/exif.h
+++ b/converter/other/exif.h
@@ -12,50 +12,59 @@
 
 typedef struct {
 /*--------------------------------------------------------------------------
-  A structure of this type stores Exif header image elements in a simple
-  manner Used to store camera data as extracted from the various ways that it
-  can be stored in an exif header
+  A structure of this type contains the information from an EXIF header
+  Image File Directory (IFD).
+
+  It appears that some of these members are possible only for certain kinds of
+  IFD (e.g. ThumbnailSize does not appear in a legal IFD for a main image),
+  but we recognize all tags in all IFDs all the same.
+--------------------------------------------------------------------------*/
+    char  cameraMake   [32];
+    char  cameraModel  [40];
+    char  dateTime     [20];
+    float xResolution;
+    float yResolution;
+    int   orientation;
+    int   isColor;
+    int   flashUsed;
+    float focalLength;
+    float exposureTime;
+    float apertureFNumber;
+    float distance;
+    int   haveCCDWidth;  /* boolean */
+    float ccdWidth;
+    float exposureBias;
+    int   whiteBalance;
+    int   meteringMode;
+    int   exposureProgram;
+    int   isoEquivalent;
+    int   compressionLevel;
+    char  comments[MAX_COMMENT];
+
+    const unsigned char * thumbnail;  /* Pointer at the thumbnail */
+    unsigned thumbnailSize;     /* Size of thumbnail. */
+} exif_ifd;
+
+
+typedef struct {
+/*--------------------------------------------------------------------------
+  A structure of this type contains the information from an EXIF header.
 --------------------------------------------------------------------------*/
-    char  CameraMake   [32];
-    char  CameraModel  [40];
-    char  DateTime     [20];
-    float XResolution;
-    float YResolution;
-    int   Orientation;
-    int   IsColor;
-    int   FlashUsed;
-    float FocalLength;
-    float ExposureTime;
-    float ApertureFNumber;
-    float Distance;
-    int   HaveCCDWidth;  /* boolean */
-    float CCDWidth;
-    float ExposureBias;
-    int   Whitebalance;
-    int   MeteringMode;
-    int   ExposureProgram;
-    int   ISOequivalent;
-    int   CompressionLevel;
-    char  Comments[MAX_COMMENT];
-
-    const unsigned char * ThumbnailPointer;  /* Pointer at the thumbnail */
-    unsigned ThumbnailSize;     /* Size of thumbnail. */
-
-    const char * DatePointer;
+    exif_ifd mainImage;       /* aka IFD0 */
+    exif_ifd thumbnailImage;  /* aka IFD1 */
 } exif_ImageInfo;
 
 
 /* Prototypes for exif.c functions. */
 
-void 
-exif_parse(const unsigned char * const exifSection, 
+void
+exif_parse(const unsigned char * const exifSection,
            unsigned int          const length,
-           exif_ImageInfo *      const imageInfoP, 
+           exif_ImageInfo *      const imageInfoP,
            bool                  const wantTagTrace,
            const char **         const errorP);
 
-void 
-exif_showImageInfo(const exif_ImageInfo * const imageInfoP,
-                   FILE *                 const fileP);
+void
+exif_showImageInfo(const exif_ImageInfo * const imageInfoP);
 
 #endif
diff --git a/converter/other/jpegtopnm.c b/converter/other/jpegtopnm.c
index b7c94fde..35b7b9d0 100644
--- a/converter/other/jpegtopnm.c
+++ b/converter/other/jpegtopnm.c
@@ -671,7 +671,7 @@ printExifInfo(struct jpeg_marker_struct const marker,
         pm_message("EXIF header is invalid.  %s", error);
         pm_strfree(error);
     } else
-        exif_showImageInfo(&imageInfo, stderr);
+        exif_showImageInfo(&imageInfo);
 }