about summary refs log tree commit diff
diff options
context:
space:
mode:
authorgiraffedata <giraffedata@9d0c8265-081b-0410-96cb-a4ca84ce46f8>2023-03-25 00:22:30 +0000
committergiraffedata <giraffedata@9d0c8265-081b-0410-96cb-a4ca84ce46f8>2023-03-25 00:22:30 +0000
commit1b6e51a266008348ad93ed8b6ac9ec91b5024fea (patch)
tree3e8db9f13fb33464324c6986e7d80540a42a86c7
parentd934dea890631a08c908cdb3462d2a74dc06f4eb (diff)
downloadnetpbm-mirror-1b6e51a266008348ad93ed8b6ac9ec91b5024fea.tar.gz
netpbm-mirror-1b6e51a266008348ad93ed8b6ac9ec91b5024fea.tar.xz
netpbm-mirror-1b6e51a266008348ad93ed8b6ac9ec91b5024fea.zip
Release 10.86.38
git-svn-id: http://svn.code.sf.net/p/netpbm/code/stable@4534 9d0c8265-081b-0410-96cb-a4ca84ce46f8
-rw-r--r--converter/other/exif.c1413
-rw-r--r--converter/other/exif.h95
-rw-r--r--converter/other/jpegtopnm.c2
-rw-r--r--doc/HISTORY5
-rw-r--r--version.mk2
5 files changed, 893 insertions, 624 deletions
diff --git a/converter/other/exif.c b/converter/other/exif.c
index 1bfe4b2b..d1eb517f 100644
--- a/converter/other/exif.c
+++ b/converter/other/exif.c
@@ -8,19 +8,41 @@
   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.).
+  Bryan fundamentally rewrote it in March 2023 because it wasn't properly
+  dealing with the main image vs thumbnail IFDs.
+--------------------------------------------------------------------------*/
+
+/*
+  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>
@@ -30,6 +52,7 @@
 #include <errno.h>
 #include <limits.h>
 #include <ctype.h>
+#include <assert.h>
 
 #if MSVCRT
     #include <sys/utime.h>
@@ -42,20 +65,28 @@
 
 #include "pm_c_util.h"
 #include "pm.h"
+#include "mallocvar.h"
 #include "nstring.h"
 
 #include "exif.h"
 
-static const unsigned char * DirWithThumbnailPtrs;
-static double FocalplaneXRes;
-bool HaveXRes;
-static double FocalplaneUnits;
-static int ExifImageWidth;
+
+
+enum Orientation {
+    ORIENT_NORMAL,
+    ORIENT_FLIP_HORIZ,   /* left right reversed mirror */
+    ORIENT_ROTATE_180,   /* upside down */
+    ORIENT_FLIP_VERT,    /* upside down mirror */
+    ORIENT_TRANSPOSE,    /* Flipped about top-left <--> bottom-right axis*/
+    ORIENT_ROTATE_90,    /* rotate 90 cw to right it */
+    ORIENT_TRANSVERSE,   /* flipped about top-right <--> bottom-left axis */
+    ORIENT_ROTATE_270    /* rotate 270 to right it */
+};
 
 typedef struct {
-    unsigned short Tag;
-    const char * Desc;
-} TagTable;
+    unsigned short tag;
+    const char * desc;
+} TagTableEntry;
 
 
 
@@ -63,7 +94,7 @@ typedef struct {
 static int const bytesPerFormat[] = {0,1,1,2,4,8,1,1,2,4,8,4,8};
 #define NUM_FORMATS 12
 
-#define FMT_BYTE       1 
+#define FMT_BYTE       1
 #define FMT_STRING     2
 #define FMT_USHORT     3
 #define FMT_ULONG      4
@@ -119,7 +150,7 @@ static int const bytesPerFormat[] = {0,1,1,2,4,8,1,1,2,4,8,4,8};
 #define TAG_THUMBNAIL_OFFSET  0x0201
 #define TAG_THUMBNAIL_LENGTH  0x0202
 
-static TagTable const tagTable[] = {
+static TagTableEntry const tagTable[] = {
   {   0x100,   "ImageWidth"},
   {   0x101,   "ImageLength"},
   {   0x102,   "BitsPerSample"},
@@ -207,7 +238,7 @@ static TagTable const tagTable[] = {
 
 
 
-typedef enum { NORMAL, MOTOROLA } ByteOrder;
+typedef enum { ORDER_NORMAL, ORDER_MOTOROLA } ByteOrder;
 
 
 
@@ -217,11 +248,11 @@ get16u(const void * const data,
 /*--------------------------------------------------------------------------
    Convert a 16 bit unsigned value from file's native byte order
 --------------------------------------------------------------------------*/
-    if (byteOrder == MOTOROLA){
-        return (((const unsigned char *)data)[0] << 8) | 
+    if (byteOrder == ORDER_MOTOROLA) {
+        return (((const unsigned char *)data)[0] << 8) |
             ((const unsigned char *)data)[1];
-    }else{
-        return (((const unsigned char *)data)[1] << 8) | 
+    } else {
+        return (((const unsigned char *)data)[1] << 8) |
             ((const unsigned char *)data)[0];
     }
 }
@@ -234,17 +265,17 @@ get32s(const void * const data,
 /*--------------------------------------------------------------------------
    Convert a 32 bit signed value from file's native byte order
 --------------------------------------------------------------------------*/
-    if (byteOrder == MOTOROLA){
-        return  
+    if (byteOrder == ORDER_MOTOROLA) {
+        return
             (((const char *)data)[0] << 24) |
             (((const unsigned char *)data)[1] << 16) |
-            (((const unsigned char *)data)[2] << 8 ) | 
+            (((const unsigned char *)data)[2] << 8 ) |
             (((const unsigned char *)data)[3] << 0 );
     } else {
-        return  
+        return
             (((const char *)data)[3] << 24) |
             (((const unsigned char *)data)[2] << 16) |
-            (((const unsigned char *)data)[1] << 8 ) | 
+            (((const unsigned char *)data)[1] << 8 ) |
             (((const unsigned char *)data)[0] << 0 );
     }
 }
@@ -262,100 +293,150 @@ 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
 --------------------------------------------------------------------------*/
-    switch(Format){
+    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_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));
+    case FMT_SSHORT:
+        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));
+    case FMT_SRATIONAL:
+        pm_asprintf(&retval, "%d/%d",
+                    get32s(valueP, byteOrder),
+                    get32s(4+(char *)valueP,
+                           byteOrder));
         break;
-    case FMT_SINGLE:    
-        fprintf(fileP, "%f\n",(double)*(float *)ValuePtr);
+    case FMT_SINGLE:
+        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", ((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.
 --------------------------------------------------------------------------*/
-    double Value;
-    Value = 0;
+    double value;
 
-    switch(Format){
+    switch(format) {
     case FMT_SBYTE:
-        Value = *(signed char *)ValuePtr;
+        value = *(signed char *)valuePtr;
         break;
     case FMT_BYTE:
-        Value = *(unsigned char *)ValuePtr;
+        value = *(unsigned char *)valuePtr;
         break;
     case FMT_USHORT:
-        Value = get16u(ValuePtr, byteOrder);
+        value = get16u(valuePtr, byteOrder);
         break;
     case FMT_ULONG:
-        Value = get32u(ValuePtr, byteOrder);
+        value = get32u(valuePtr, byteOrder);
         break;
     case FMT_URATIONAL:
     case FMT_SRATIONAL: {
         int num, den;
-        num = get32s(ValuePtr, byteOrder);
-        den = get32s(4+(char *)ValuePtr, byteOrder);
-        Value = den == 0 ? 0 : (double)(num/den);
+        num = get32s(valuePtr, byteOrder);
+        den = get32s(4+(char *)valuePtr, byteOrder);
+        value = den == 0 ? 0 : (double)(num/den);
     } break;
     case FMT_SSHORT:
-        Value = (signed short)get16u(ValuePtr, byteOrder);
+        value = (signed short)get16u(valuePtr, byteOrder);
         break;
     case FMT_SLONG:
-        Value = get32s(ValuePtr, byteOrder);
+        value = get32s(valuePtr, byteOrder);
         break;
 
     /* Not sure if this is correct (never seen float used in Exif format) */
     case FMT_SINGLE:
-        Value = (double)*(float *)ValuePtr;
+        value = (double)*(float *)valuePtr;
         break;
     case FMT_DOUBLE:
-        Value = *(double *)ValuePtr;
+        value = *(double *)valuePtr;
         break;
     }
-    return Value;
+    return value;
+}
+
+
+
+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;
 }
 
 
@@ -363,72 +444,186 @@ convertAnyFormat(const void * const ValuePtr,
 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;
-        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");
+        tagValue = stringTraceValue(value, valueSz);
     } 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);
 }
 
 
 
+static void
+initializeIfd(exif_ifd * const ifdP) {
+
+    ifdP->cameraMake        = NULL;
+    ifdP->cameraModel       = NULL;
+    ifdP->dateTime          = NULL;
+    ifdP->xResolutionP      = NULL;
+    ifdP->yResolutionP      = NULL;
+    ifdP->orientationP      = NULL;
+    ifdP->isColorP          = NULL;
+    ifdP->flashP            = NULL;
+    ifdP->focalLengthP      = NULL;
+    ifdP->exposureTimeP     = NULL;
+    ifdP->shutterSpeedP     = NULL;
+    ifdP->apertureFNumberP  = NULL;
+    ifdP->distanceP         = NULL;
+    ifdP->exposureBiasP     = NULL;
+    ifdP->whiteBalanceP     = NULL;
+    ifdP->meteringModeP     = NULL;
+    ifdP->exposureProgramP  = NULL;
+    ifdP->isoEquivalentP    = NULL;
+    ifdP->compressionLevelP = NULL;
+    ifdP->comments          = NULL;
+    ifdP->thumbnailOffsetP  = NULL;
+    ifdP->thumbnailLengthP  = NULL;
+    ifdP->exifImageLengthP  = NULL;
+    ifdP->exifImageWidthP   = NULL;
+    ifdP->focalPlaneXResP   = NULL;
+    ifdP->focalPlaneUnitsP  = NULL;
+    ifdP->thumbnail         = NULL;
+}
+
+
+
+static void
+freeIfPresent(const void * const arg) {
+
+    if (arg)
+        free((void *)arg);
+}
+
+
+
+static void
+strfreeIfPresent(const char * const arg) {
+
+    if (arg)
+        pm_strfree(arg);
+}
+
+
+
+static void
+terminateIfd(exif_ifd * const ifdP) {
+
+    strfreeIfPresent(ifdP->cameraMake  );
+    strfreeIfPresent(ifdP->cameraModel );
+    strfreeIfPresent(ifdP->dateTime    );
+    strfreeIfPresent(ifdP->comments    );
+    freeIfPresent(ifdP->xResolutionP      );
+    freeIfPresent(ifdP->yResolutionP      );
+    freeIfPresent(ifdP->orientationP      );
+    freeIfPresent(ifdP->isColorP          );
+    freeIfPresent(ifdP->flashP            );
+    freeIfPresent(ifdP->focalLengthP      );
+    freeIfPresent(ifdP->exposureTimeP     );
+    freeIfPresent(ifdP->shutterSpeedP     );
+    freeIfPresent(ifdP->apertureFNumberP  );
+    freeIfPresent(ifdP->distanceP         );
+    freeIfPresent(ifdP->exposureBiasP     );
+    freeIfPresent(ifdP->whiteBalanceP     );
+    freeIfPresent(ifdP->meteringModeP     );
+    freeIfPresent(ifdP->exposureProgramP  );
+    freeIfPresent(ifdP->isoEquivalentP    );
+    freeIfPresent(ifdP->compressionLevelP );
+    freeIfPresent(ifdP->thumbnailOffsetP  );
+    freeIfPresent(ifdP->thumbnailLengthP  );
+    freeIfPresent(ifdP->exifImageLengthP  );
+    freeIfPresent(ifdP->exifImageWidthP   );
+    freeIfPresent(ifdP->focalPlaneXResP   );
+    freeIfPresent(ifdP->focalPlaneUnitsP  );
+}
+
+
+
+static const char *
+commentValue(const unsigned char * const valuePtr,
+             unsigned int          const valueSz) {
+
+    /* Olympus has this padded with trailing spaces.  We stop the copy
+       where those start.
+    */
+    const char * const value = (const char *)valuePtr;
+
+    const char * retval;
+    char * buffer;  /* malloc'ed */
+    unsigned int cursor;
+    unsigned int end;
+
+    for (end = valueSz; end > 0 && value[end] == ' '; --end);
+
+    /* Skip "ASCII" if it is there */
+    if (end >= 5 && memeq(value, "ASCII", 5))
+        cursor = 5;
+    else
+        cursor = 0;
+
+    /* Skip consecutive blanks and NULs */
+
+    for (;
+         cursor < valueSz &&
+             (value[cursor] == '\0' || value[cursor] == ' ');
+         ++cursor);
+
+    /* Copy the rest as the comment */
+
+    MALLOCARRAY(buffer, end - cursor + 1);
+    if (!buffer)
+        retval = pm_strsol;
+    else {
+        unsigned int outCursor;
+        for (outCursor = 0; cursor < end; ++cursor)
+            buffer[outCursor++] = value[cursor];
+
+        buffer[outCursor++] = '\0';
+
+        retval = buffer;
+    }
+    return retval;
+}
+
+
 /* 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);
+static void
+processIfd(const unsigned char *  const exifData,
+           unsigned int           const exifLength,
+           const unsigned char *  const ifdData,
+           ByteOrder              const byteOrder,
+           bool                   const wantTagTrace,
+           exif_ifd *             const ifdP,
+           const unsigned char ** const nextIfdPP,
+           const char **          const errorP);
 
 
 static void
@@ -437,11 +632,8 @@ processDirEntry(const unsigned char *  const dirEntry,
                 unsigned int           const exifLength,
                 ByteOrder              const byteOrder,
                 bool                   const wantTagTrace,
-                exif_ImageInfo *       const imageInfoP, 
-                unsigned int *         const thumbnailOffsetP,
-                unsigned int *         const thumbnailSizeP,
-                bool *                 const haveThumbnailP,
-                const unsigned char ** const lastExifRefdP) {
+                exif_ifd *             const ifdP,
+                const char **          const errorP) {
 
     int const tag        = get16u(&dirEntry[0], byteOrder);
     int const format     = get16u(&dirEntry[2], byteOrder);
@@ -452,27 +644,29 @@ 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;
+
+    *errorP = NULL;  /* initial assumption */
 
     if ((format-1) >= NUM_FORMATS) {
         /* (-1) catches illegal zero case as unsigned underflows
-           to positive large.  
+           to positive large.
         */
         pm_message("Illegal number format %d for tag %04x", format, tag);
         return;
     }
-        
-    byteCount = components * bytesPerFormat[format];
 
-    if (byteCount > 4){
+    valueSz = components * bytesPerFormat[format];
+
+    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,366 +675,361 @@ processDirEntry(const unsigned char *  const dirEntry,
         valuePtr = &dirEntry[8];
     }
 
-    if (*lastExifRefdP < valuePtr + byteCount){
-        /* 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;
-    }
-
     if (wantTagTrace)
-        traceTag(tag, format, valuePtr, byteCount, byteOrder);
-
-    *haveThumbnailP = (tag == TAG_THUMBNAIL_OFFSET);
-
+        traceTag(tag, format, valuePtr, valueSz, byteOrder);
+    /* TODO: Need to deal with nonterminated strings in tag value */
+    /* TODO: Deal with repeated tag */
     /* Extract useful components of tag */
-    switch (tag){
-
+    switch (tag) {
     case TAG_MAKE:
-        STRSCPY(imageInfoP->CameraMake, (const char*)valuePtr);
+        ifdP->cameraMake = pm_strdup((const char*)valuePtr);
         break;
 
     case TAG_MODEL:
-        STRSCPY(imageInfoP->CameraModel, (const char*)valuePtr);
+        ifdP->cameraModel = pm_strdup((const char*)valuePtr);
         break;
 
     case TAG_XRESOLUTION:
-        imageInfoP->XResolution = 
-            convertAnyFormat(valuePtr, format, byteOrder);
+        MALLOCVAR_NOFAIL(ifdP->xResolutionP);
+        *ifdP->xResolutionP = numericValue(valuePtr, format, byteOrder);
         break;
 
     case TAG_YRESOLUTION:
-        imageInfoP->YResolution = 
-            convertAnyFormat(valuePtr, format, byteOrder);
+        MALLOCVAR_NOFAIL(ifdP->yResolutionP);
+        *ifdP->yResolutionP = numericValue(valuePtr, format, byteOrder);
         break;
 
     case TAG_DATETIME_ORIGINAL:
-        STRSCPY(imageInfoP->DateTime, (const char*)valuePtr);
-        imageInfoP->DatePointer = (const char*)valuePtr;
+        ifdP->dateTime = pm_strdup((const char*)valuePtr);
         break;
 
-    case TAG_USERCOMMENT: {
-        /* Olympus has this padded with trailing spaces.  We stop the copy
-           where those start.
-        */
-        const char * const value = (const char *)valuePtr;
-
-        unsigned int cursor;
-        unsigned int outCursor;
-        unsigned int end;
-
-        for (end = byteCount; end > 0 && value[end] == ' '; --end);
-
-        /* Skip "ASCII" if it is there */
-        if (end >= 5 && MEMEQ(value, "ASCII", 5))
-            cursor = 5;
-        else
-            cursor = 0;
-
-        /* Skip consecutive blanks and NULs */
-
-        for (;
-             cursor < byteCount && 
-                 (value[cursor] == '\0' || value[cursor] == ' ');
-             ++cursor);
-
-        /* Copy the rest as the comment */
-
-        for (outCursor = 0;
-             cursor < end && outCursor < MAX_COMMENT-1;
-             ++cursor)
-            imageInfoP->Comments[outCursor++] = value[cursor];
-
-        imageInfoP->Comments[outCursor++] = '\0';
-    } break;
+    case TAG_USERCOMMENT:
+        ifdP->comments = commentValue(valuePtr, valueSz);
+        break;
 
     case TAG_FNUMBER:
         /* Simplest way of expressing aperture, so I trust it the most.
-           (overwrite previously computd value if there is one)
+           (replace any existing value, as it will be based on a less useful
+           tag that came earlier in the IFD).
         */
-        imageInfoP->ApertureFNumber = 
-            (float)convertAnyFormat(valuePtr, format, byteOrder);
+        if (ifdP->apertureFNumberP)
+            free(ifdP->apertureFNumberP);
+
+        MALLOCVAR_NOFAIL(ifdP->apertureFNumberP);
+        *ifdP->apertureFNumberP =
+            (float)numericValue(valuePtr, format, byteOrder);
         break;
 
     case TAG_APERTURE:
     case TAG_MAXAPERTURE:
-        /* More relevant info always comes earlier, so only use this field if
-           we don't have appropriate aperture information yet.
+        /* If we already have aperture information, it probably came from an
+           FNUMBER tag and is superior, so we leave it alone
         */
-        if (imageInfoP->ApertureFNumber == 0){
-            imageInfoP->ApertureFNumber = (float)
-                exp(convertAnyFormat(valuePtr, format, byteOrder)
+        if (!ifdP->apertureFNumberP) {
+            MALLOCVAR_NOFAIL(ifdP->apertureFNumberP);
+            *ifdP->apertureFNumberP = (float)
+                exp(numericValue(valuePtr, format, byteOrder)
                     * log(2) * 0.5);
         }
         break;
 
     case TAG_FOCALLENGTH:
         /* Nice digital cameras actually save the focal length
-           as a function of how farthey are zoomed in. 
+           as a function of how farthey are zoomed in.
         */
 
-        imageInfoP->FocalLength = 
-            (float)convertAnyFormat(valuePtr, format, byteOrder);
+        MALLOCVAR_NOFAIL(ifdP->focalLengthP);
+        *ifdP->focalLengthP =
+            (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);
+        MALLOCVAR_NOFAIL(ifdP->distanceP);
+        *ifdP->distanceP =
+            (float)numericValue(valuePtr, format, byteOrder);
         break;
 
     case TAG_EXPOSURETIME:
-        /* Simplest way of expressing exposure time, so I
-           trust it most.  (overwrite previously computd value
-           if there is one) 
-        */
-        imageInfoP->ExposureTime = 
-            (float)convertAnyFormat(valuePtr, format, byteOrder);
+        MALLOCVAR_NOFAIL(ifdP->exposureTimeP);
+        *ifdP->exposureTimeP =
+            (float)numericValue(valuePtr, format, byteOrder);
         break;
 
     case TAG_SHUTTERSPEED:
-        /* More complicated way of expressing exposure time,
-           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)
-                       * log(2)));
-        }
+        MALLOCVAR_NOFAIL(ifdP->shutterSpeedP);
+        *ifdP->shutterSpeedP =
+            1 << (unsigned int)numericValue(valuePtr, format, byteOrder);
         break;
 
-    case TAG_FLASH:
-        if ((int)convertAnyFormat(valuePtr, format, byteOrder) & 0x7){
-            imageInfoP->FlashUsed = TRUE;
-        }else{
-            imageInfoP->FlashUsed = FALSE;
-        }
-        break;
+    case TAG_FLASH: {
+        unsigned int const tagValue =
+            (unsigned int)numericValue(valuePtr, format, byteOrder);
+
+        MALLOCVAR_NOFAIL(ifdP->flashP);
+
+        *ifdP->flashP = ((tagValue & 0x7) != 0);
+
+    } break;
 
-    case TAG_ORIENTATION:
-        imageInfoP->Orientation = 
-            (int)convertAnyFormat(valuePtr, format, byteOrder);
-        if (imageInfoP->Orientation < 1 || 
-            imageInfoP->Orientation > 8){
-            pm_message("Undefined rotation value %d",
-                       imageInfoP->Orientation);
-            imageInfoP->Orientation = 0;
+    case TAG_ORIENTATION: {
+        unsigned int const tagValue =
+            (unsigned int)numericValue(valuePtr, format, byteOrder);
+
+        if (tagValue < 1 || tagValue > 8)
+            pm_asprintf(errorP, "Unrecognized orientation value %d",
+                        tagValue);
+        else {
+            MALLOCVAR_NOFAIL(ifdP->orientationP);
+
+            switch (tagValue) {
+            case 1: *ifdP->orientationP = ORIENT_NORMAL;     break;
+            case 2: *ifdP->orientationP = ORIENT_FLIP_HORIZ; break;
+            case 3: *ifdP->orientationP = ORIENT_ROTATE_180; break;
+            case 4: *ifdP->orientationP = ORIENT_FLIP_VERT;  break;
+            case 5: *ifdP->orientationP = ORIENT_TRANSPOSE;  break;
+            case 6: *ifdP->orientationP = ORIENT_ROTATE_90;  break;
+            case 7: *ifdP->orientationP = ORIENT_TRANSVERSE; break;
+            case 8: *ifdP->orientationP = ORIENT_ROTATE_270; break;
+            default:
+                assert(false);
+            }
         }
-        break;
+        } break;
 
     case TAG_EXIF_IMAGELENGTH:
+        MALLOCVAR_NOFAIL(ifdP->exifImageLengthP);
+        *ifdP->exifImageLengthP =
+            (unsigned int)numericValue(valuePtr, format, byteOrder);
+        break;
+
     case TAG_EXIF_IMAGEWIDTH:
-        /* Use largest of height and width to deal with images
-           that have been rotated to portrait format.  
-        */
-        ExifImageWidth =
-            MIN(ExifImageWidth,
-                (int)convertAnyFormat(valuePtr, format, byteOrder));
+        MALLOCVAR_NOFAIL(ifdP->exifImageWidthP);
+        *ifdP->exifImageWidthP =
+            (unsigned int)numericValue(valuePtr, format, byteOrder);
         break;
 
     case TAG_FOCALPLANEXRES:
-        HaveXRes = TRUE;
-        FocalplaneXRes = convertAnyFormat(valuePtr, format, byteOrder);
+        MALLOCVAR_NOFAIL(ifdP->focalPlaneXResP);
+        *ifdP->focalPlaneXResP = numericValue(valuePtr, format, byteOrder);
         break;
 
-    case TAG_FOCALPLANEUNITS:
-        switch((int)convertAnyFormat(valuePtr, format, byteOrder)){
-        case 1: FocalplaneUnits = 25.4; break; /* 1 inch */
-        case 2: 
-            /* According to the information I was using, 2
-               means meters.  But looking at the Cannon
-               powershot's files, inches is the only
-               sensible value.  
-            */
-            FocalplaneUnits = 25.4;
-            break;
+    case TAG_FOCALPLANEUNITS: {
+        int const tagValue = (int)numericValue(valuePtr, format, byteOrder);
 
-        case 3: FocalplaneUnits = 10;   break;  /* 1 centimeter*/
-        case 4: FocalplaneUnits = 1;    break;  /* 1 millimeter*/
-        case 5: FocalplaneUnits = .001; break;  /* 1 micrometer*/
+        if (tagValue < 1 || tagValue > 5) {
+            pm_asprintf(errorP, "Unrecognized FOCALPLANEUNITS value %d.  "
+                        "We know only 1, 2, 3, 4, and 5",
+                        tagValue);
+        } else {
+            MALLOCVAR_NOFAIL(ifdP->focalPlaneUnitsP);
+
+            switch (tagValue) {
+            case 1: *ifdP->focalPlaneUnitsP = 25.4;  break; /* 1 inch */
+            case 2: *ifdP->focalPlaneUnitsP = 100.0; break; /* 1 meter */
+            case 3: *ifdP->focalPlaneUnitsP = 10.0;  break;  /* 1 centimeter*/
+            case 4: *ifdP->focalPlaneUnitsP = 1.0;   break;  /* 1 millimeter*/
+            case 5: *ifdP->focalPlaneUnitsP = .001;  break;  /* 1 micrometer*/
+            }
+            /* According to the information I was using, 2 means meters.  But
+               looking at the Cannon powershot's files, inches is the only
+               sensible value.
+            */
         }
-        break;
+    } break;
 
         /* Remaining cases contributed by: Volker C. Schoech
            (schoech@gmx.de)
         */
 
     case TAG_EXPOSURE_BIAS:
-        imageInfoP->ExposureBias = 
-            (float) convertAnyFormat(valuePtr, format, byteOrder);
+        MALLOCVAR_NOFAIL(ifdP->exposureBiasP);
+        *ifdP->exposureBiasP =
+            (float) numericValue(valuePtr, format, byteOrder);
         break;
 
     case TAG_WHITEBALANCE:
-        imageInfoP->Whitebalance = 
-            (int)convertAnyFormat(valuePtr, format, byteOrder);
+        MALLOCVAR_NOFAIL(ifdP->whiteBalanceP);
+        *ifdP->whiteBalanceP = (int)numericValue(valuePtr, format, byteOrder);
         break;
 
     case TAG_METERING_MODE:
-        imageInfoP->MeteringMode = 
-            (int)convertAnyFormat(valuePtr, format, byteOrder);
+        MALLOCVAR_NOFAIL(ifdP->meteringModeP);
+        *ifdP->meteringModeP = (int)numericValue(valuePtr, format, byteOrder);
         break;
 
     case TAG_EXPOSURE_PROGRAM:
-        imageInfoP->ExposureProgram = 
-            (int)convertAnyFormat(valuePtr, format, byteOrder);
+        MALLOCVAR_NOFAIL(ifdP->exposureProgramP);
+        *ifdP->exposureProgramP =
+            (int)numericValue(valuePtr, format, byteOrder);
         break;
 
-    case TAG_ISO_EQUIVALENT:
-        imageInfoP->ISOequivalent = 
-            (int)convertAnyFormat(valuePtr, format, byteOrder);
-        if ( imageInfoP->ISOequivalent < 50 ) 
-            imageInfoP->ISOequivalent *= 200;
-        break;
+    case TAG_ISO_EQUIVALENT: {
+        int const tagValue = (int)numericValue(valuePtr, format, byteOrder);
+
+        MALLOCVAR_NOFAIL(ifdP->isoEquivalentP);
+        if (tagValue < 50)
+            *ifdP->isoEquivalentP = tagValue * 200;
+        else
+            *ifdP->isoEquivalentP = tagValue;
+    } break;
 
     case TAG_COMPRESSION_LEVEL:
-        imageInfoP->CompressionLevel = 
-            (int)convertAnyFormat(valuePtr, format, byteOrder);
+        MALLOCVAR_NOFAIL(ifdP->compressionLevelP);
+        *ifdP->compressionLevelP =
+            (int)numericValue(valuePtr, format, byteOrder);
         break;
 
     case TAG_THUMBNAIL_OFFSET:
-        *thumbnailOffsetP = (unsigned int)
-            convertAnyFormat(valuePtr, format, byteOrder);
+        MALLOCVAR_NOFAIL(ifdP->thumbnailOffsetP);
+        *ifdP->thumbnailOffsetP = (unsigned int)
+            numericValue(valuePtr, format, byteOrder);
         break;
 
     case TAG_THUMBNAIL_LENGTH:
-        *thumbnailSizeP = (unsigned int)
-            convertAnyFormat(valuePtr, format, byteOrder);
+        MALLOCVAR_NOFAIL(ifdP->thumbnailLengthP);
+        *ifdP->thumbnailLengthP = (unsigned int)
+            numericValue(valuePtr, format, byteOrder);
         break;
 
     case TAG_EXIF_OFFSET:
     case TAG_INTEROP_OFFSET: {
-        unsigned int const subdirOffset = get32u(valuePtr, byteOrder);
-        if (subdirOffset >= exifLength)
-            pm_message("Illegal exif or interop offset "
+        unsigned int const subIfdOffset = get32u(valuePtr, byteOrder);
+        if (subIfdOffset + 4 > exifLength)
+            pm_message("Invalid exif or interop offset "
                        "directory link.  Offset is %u, "
                        "but Exif data is only %u bytes.",
-                       subdirOffset, exifLength);
-        else
-            processExifDir(exifData, exifLength, subdirOffset, 
-                           imageInfoP, byteOrder, wantTagTrace,
-                           lastExifRefdP);
+                       subIfdOffset, exifLength);
+        else {
+            /* Process the chain of IFDs starting at 'subIfdOffset'.
+               Merge whatever tags are in them into *ifdP
+            */
+            const unsigned char * nextIfdP;
+
+            for (nextIfdP = exifData + subIfdOffset; nextIfdP;) {
+                const char * error;
+
+                if (wantTagTrace)
+                    pm_message("Processing subIFD");
+
+                processIfd(exifData, exifLength, exifData + subIfdOffset,
+                           byteOrder, wantTagTrace,
+                           ifdP, &nextIfdP, &error);
+
+                if (error) {
+                    pm_asprintf(errorP, "Failed to process "
+                                "ExifOffset/InteropOffset tag.  %s", error);
+                    pm_strfree(error);
+                }
+            }
+        }
     } break;
     }
 }
 
 
 
-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) {
+static void
+locateNextIfd(const unsigned char *  const exifData,
+              unsigned int           const exifLength,
+              ByteOrder              const byteOrder,
+              const unsigned char *  const nextIfdLinkPtr,
+              const unsigned char ** const nextIfdPP,
+              const char **          const errorP) {
+
+    if (nextIfdLinkPtr + 4 > exifData + exifLength)
+        pm_asprintf(errorP, "EXIF header ends before next-IFD link");
+    else {
+        if (nextIfdLinkPtr + 4 <= exifData + exifLength) {
+            unsigned int const nextIfdLink =
+                get32u(nextIfdLinkPtr, byteOrder);
+                /* Offset in EXIF header of next IFD, or zero if none */
+
+            if (nextIfdLink) {
+                if (nextIfdLink + 4 >= exifLength) {
+                    pm_asprintf(errorP, "Next IFD link is %u, "
+                                "but EXIF header is only %u bytes long",
+                                nextIfdLink, exifLength);
+                } else
+                    *nextIfdPP = exifData + nextIfdLink;
+            } else
+                *nextIfdPP = NULL;
+        }
+    }
+}
+
+
+
+static void
+processIfd(const unsigned char *  const exifData,
+           unsigned int           const exifLength,
+           const unsigned char *  const ifdData,
+           ByteOrder              const byteOrder,
+           bool                   const wantTagTrace,
+           exif_ifd *             const ifdP,
+           const unsigned char ** const nextIfdPP,
+           const char **          const errorP) {
 /*--------------------------------------------------------------------------
-   Process one of the nested EXIF directories.
+   Process one EXIF IFD (Image File Directory).
+
+   The text of the IFD is at 'ifdData'.
+
+   The text of the EXIF header of which the IFD is part is the 'exifLength'
+   bytes at 'exifData'.
+
+   Return the contents of the directory (tags) as *ifdP.
+
+   Return as *nextIfdPP a pointer into the EXIF header at 'exifData'
+   to the next IFD in the chain, or NULL if this is the last IFD.
 --------------------------------------------------------------------------*/
-    const unsigned char * const dirStart = exifData + dirOffset;
-    unsigned int const numDirEntries = get16u(&dirStart[0], byteOrder);
+    unsigned int const numDirEntries = get16u(&ifdData[0], byteOrder);
+
     unsigned int de;
-    bool haveThumbnail;
-    unsigned int thumbnailOffset;
-    unsigned int thumbnailSize;
-
-    #define DIR_ENTRY_ADDR(Start, Entry) (Start+2+12*(Entry))
-
-    {
-        const unsigned char * const dirEnd =
-            DIR_ENTRY_ADDR(dirStart, numDirEntries);
-        if (dirEnd + 4 > (exifData + exifLength)){
-            if (dirEnd + 2 == exifData + exifLength || 
-                dirEnd == exifData + exifLength){
-                /* Version 1.3 of jhead would truncate a bit too much.
-                   This also caught later on as well.
-                */
-            }else{
-                /* Note: Files that had thumbnails trimmed with jhead
-                   1.3 or earlier might trigger this.
-                */
-                pm_message("Illegal directory entry size");
-                return;
-            }
-        }
-        *lastExifRefdP = MAX(*lastExifRefdP, dirEnd);
-    }
+
+    *errorP = NULL;  /* initial value */
+
+    #define DIR_ENTRY_ADDR(Start, Entry) (Start + 2 + 12*(Entry))
 
     if (wantTagTrace)
-        pm_message("Directory with %d entries", numDirEntries);
-
-    haveThumbnail   = false;  /* initial value */
-    thumbnailOffset = 0;      /* initial value */
-    thumbnailSize   = 0;      /* initial value */
-
-    for (de = 0; de < numDirEntries; ++de)
-        processDirEntry(DIR_ENTRY_ADDR(dirStart, de), exifData, exifLength,
-                        byteOrder, wantTagTrace, imageInfoP,
-                        &thumbnailOffset, &thumbnailSize, &haveThumbnail,
-                        lastExifRefdP);
-
-    if (haveThumbnail)
-        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!  
-        */
-        if (DIR_ENTRY_ADDR(dirStart, numDirEntries) + 4 <= 
-            exifData + exifLength){
-            unsigned int const subdirOffset =
-                get32u(dirStart + 2 + 12*numDirEntries, byteOrder);
-            if (subdirOffset){
-                const unsigned char * const subdirStart =
-                    exifData + subdirOffset;
-                if (subdirStart > exifData + exifLength){
-                    if (subdirStart < exifData + exifLength + 20){
-                        /* Jhead 1.3 or earlier would crop the whole directory!
-                           As Jhead produces this form of format incorrectness,
-                           I'll just let it pass silently.
-                        */
-                        if (wantTagTrace) 
-                            printf("Thumbnail removed with "
-                                   "Jhead 1.3 or earlier\n");
-                    }else{
-                        pm_message("Illegal subdirectory link");
-                    }
-                }else{
-                    if (subdirOffset <= exifLength)
-                        processExifDir(exifData, exifLength, subdirOffset,
-                                       imageInfoP, byteOrder, wantTagTrace,
-                                       lastExifRefdP);
-                }
-            }
-        }else{
-            /* The exif header ends before the last next directory pointer. */
+        pm_message("Processing IFD at %p with %u entries",
+                   ifdData, numDirEntries);
+
+    for (de = 0; de < numDirEntries && !*errorP; ++de) {
+        const char * error;
+        processDirEntry(DIR_ENTRY_ADDR(ifdData, de), exifData, exifLength,
+                        byteOrder, wantTagTrace, ifdP,
+                        &error);
+
+        if (error) {
+            pm_asprintf(errorP, "Failed to process tag %u.  %s",
+                        de, error);
+            pm_strfree(error);
         }
     }
 
-    if (thumbnailSize && thumbnailOffset){
-        if (thumbnailSize + thumbnailOffset <= exifLength){
-            /* The thumbnail pointer appears to be valid.  Store it. */
-            imageInfoP->ThumbnailPointer = exifData + thumbnailOffset;
-            imageInfoP->ThumbnailSize = thumbnailSize;
+    locateNextIfd(exifData, exifLength, byteOrder,
+                  DIR_ENTRY_ADDR(ifdData, numDirEntries),
+                  nextIfdPP, errorP);
 
-            if (wantTagTrace){
-                fprintf(stderr, "Thumbnail size: %u bytes\n", thumbnailSize);
-            }
+    if (ifdP->thumbnailOffsetP && ifdP->thumbnailLengthP) {
+        if (*ifdP->thumbnailOffsetP + *ifdP->thumbnailLengthP <= exifLength) {
+            /* The thumbnail pointer appears to be valid.  Store it. */
+            ifdP->thumbnail     = exifData + *ifdP->thumbnailOffsetP;
+            ifdP->thumbnailSize = *ifdP->thumbnailLengthP;
         }
     }
+    if (wantTagTrace)
+        pm_message("Done processing IFD at %p", ifdData);
 }
 
 
 
-void 
+void
 exif_parse(const unsigned char * const exifData,
            unsigned int          const length,
-           exif_ImageInfo *      const imageInfoP, 
+           exif_ImageInfo *      const imageInfoP,
            bool                  const wantTagTrace,
            const char **         const errorP) {
 /*--------------------------------------------------------------------------
@@ -852,34 +1041,33 @@ exif_parse(const unsigned char * const exifData,
   'length' is the length of the Exif section.
 --------------------------------------------------------------------------*/
     ByteOrder byteOrder;
-    int FirstOffset;
-    const unsigned char * lastExifRefd;
+    unsigned int firstOffset;
 
     *errorP = NULL;  /* initial assumption */
 
     if (wantTagTrace)
-        fprintf(stderr, "Exif header %d bytes long\n",length);
+        pm_message("Exif header %u bytes long", length);
 
-    if (MEMEQ(exifData + 0, "II" , 2)) {
-        if (wantTagTrace) 
-            fprintf(stderr, "Exif header in Intel order\n");
-        byteOrder = NORMAL;
+    if (memeq(exifData + 0, "II" , 2)) {
+        if (wantTagTrace)
+            pm_message("Exif header in Intel order");
+        byteOrder = ORDER_NORMAL;
     } else {
-        if (MEMEQ(exifData + 0, "MM", 2)) {
-            if (wantTagTrace) 
-                fprintf(stderr, "Exif header in Motorola order\n");
-            byteOrder = MOTOROLA;
+        if (memeq(exifData + 0, "MM", 2)) {
+            if (wantTagTrace)
+                pm_message("Exif header in Motorola order");
+            byteOrder = ORDER_MOTOROLA;
         } else {
             pm_asprintf(errorP, "Invalid alignment marker in Exif "
                         "data.  First two bytes are '%c%c' (0x%02x%02x) "
-                        "instead of 'II' or 'MM'.", 
+                        "instead of 'II' or 'MM'.",
                         exifData[0], exifData[1], exifData[0], exifData[1]);
         }
     }
     if (!*errorP) {
         unsigned short const start = get16u(exifData + 2, byteOrder);
         /* Check the next value for correctness. */
-        if (start != 0x002a){
+        if (start != 0x002a) {
             pm_asprintf(errorP, "Invalid Exif header start.  "
                         "two bytes after the alignment marker "
                         "should be 0x002a, but is 0x%04x",
@@ -887,237 +1075,288 @@ exif_parse(const unsigned char * const exifData,
         }
     }
     if (!*errorP) {
-        FirstOffset = get32u(exifData + 4, byteOrder);
-        if (FirstOffset < 8 || FirstOffset > 16){
+        const char * error;
+        const unsigned char * nextIfdP;
+
+        firstOffset = get32u(exifData + 4, byteOrder);
+        if (firstOffset < 8 || firstOffset > 16) {
             /* I used to ensure this was set to 8 (website I used
-               indicated its 8) but PENTAX Optio 230 has it set
+               indicated it's 8) but PENTAX Optio 230 has it set
                differently, and uses it as offset. (Sept 11 2002)
-                */
+            */
             pm_message("Suspicious offset of first IFD value in Exif header");
         }
-        
-        imageInfoP->Comments[0] = '\0';  /* Initial value - null string */
-        
-        HaveXRes = FALSE;  /* Initial assumption */
-        FocalplaneUnits = 0;
-        ExifImageWidth = 0;
-        
-        lastExifRefd = exifData;
-        DirWithThumbnailPtrs = NULL;
-        
-        processExifDir(exifData, length, FirstOffset, 
-                       imageInfoP, byteOrder, wantTagTrace, &lastExifRefd);
-        
-        /* Compute the CCD width, in millimeters. */
-        if (HaveXRes){
-            imageInfoP->HaveCCDWidth = 1;
-            imageInfoP->CCDWidth = 
-                    (float)(ExifImageWidth * FocalplaneUnits / FocalplaneXRes);
-        } else
-            imageInfoP->HaveCCDWidth = 0;
-            
-        if (wantTagTrace){
-            fprintf(stderr, 
-                    "Non-settings part of Exif header: %lu bytes\n",
-                    (unsigned long)(exifData + length - lastExifRefd));
+
+        initializeIfd(&imageInfoP->mainImage);
+        initializeIfd(&imageInfoP->thumbnailImage);
+
+        if (wantTagTrace)
+            pm_message("Processing main image IFD (IFD0)");
+
+        processIfd(exifData, length, exifData + firstOffset, byteOrder,
+                   wantTagTrace,
+                   &imageInfoP->mainImage, &nextIfdP, &error);
+
+        if (error) {
+            pm_asprintf(errorP, "Failed to process main image IFD.  %s",
+                        error);
+            pm_strfree(error);
+        }
+
+        if (nextIfdP) {
+            const char * error;
+
+            if (wantTagTrace)
+                pm_message("Processing thumbnail IFD (IFD1)");
+
+            processIfd(exifData, length, nextIfdP, byteOrder,
+                       wantTagTrace,
+                       &imageInfoP->thumbnailImage, &nextIfdP, &error);
+
+            if (error) {
+                pm_asprintf(errorP,
+                            "Failed to process thumbnail image IFD.  %s",
+                            error);
+                pm_strfree(error);
+            } else {
+                if (nextIfdP) {
+                    pm_message("Ignoring third IFD in EXIF header because "
+                               "We understand only two -- one for the main "
+                               "image and one for the thumbnail");
+                }
+            }
+        }
+        if (!*errorP) {
+            /* Compute the CCD width, in millimeters. */
+            if (imageInfoP->mainImage.focalPlaneXResP &&
+                imageInfoP->mainImage.focalPlaneUnitsP &&
+                imageInfoP->mainImage.exifImageWidthP &&
+                imageInfoP->mainImage.exifImageLengthP) {
+
+                unsigned int const maxDim =
+                    MAX(*imageInfoP->mainImage.exifImageWidthP,
+                        *imageInfoP->mainImage.exifImageLengthP);
+
+                MALLOCVAR_NOFAIL(imageInfoP->ccdWidthP);
+                *imageInfoP->ccdWidthP =
+                    (float)(maxDim *
+                            *imageInfoP->mainImage.focalPlaneUnitsP /
+                            *imageInfoP->mainImage.focalPlaneXResP);
+            } else
+                imageInfoP->ccdWidthP = NULL;
         }
     }
 }
 
 
 
-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);
-    }
-    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) {
-
-        /* Only print orientation 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
-        */
-        static const char * OrientTab[9] = {
-            "Undefined",
-            "Normal",           /* 1 */
-            "flip horizontal",  /* left right reversed mirror */
-            "rotate 180",       /* 3 */
-            "flip vertical",    /* upside down mirror */
-            "transpose",    /* Flipped about top-left <--> bottom-right axis.*/
-            "rotate 90",        /* rotate 90 cw to right it. */
-            "transverse",   /* flipped about top-right <--> bottom-left axis */
-            "rotate 270",       /* rotate 270 to right it. */
-        };
-
-        fprintf(fileP, "Orientation  : %s\n", 
-                OrientTab[imageInfoP->Orientation]);
-    }
+static void
+showIfd(const exif_ifd * const ifdP) {
 
-    if (imageInfoP->IsColor == 0)
-        fprintf(fileP, "Color/bw     : Black and white\n");
+    if (ifdP->cameraMake)
+        pm_message("Camera make  : %s", ifdP->cameraMake);
 
-    if (imageInfoP->FlashUsed >= 0)
-        fprintf(fileP, "Flash used   : %s\n",
-                imageInfoP->FlashUsed ? "Yes" :"No");
+    if (ifdP->cameraModel)
+        pm_message("Camera model : %s", ifdP->cameraModel);
 
-    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->dateTime)
+        pm_message("Date/Time    : %s", ifdP->dateTime);
 
-    if (imageInfoP->HaveCCDWidth)
-        fprintf(fileP, "CCD width    : %2.4fmm\n",
-                (double)imageInfoP->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->xResolutionP && ifdP->yResolutionP)
+        pm_message("Resolution   : %f x %f",
+                   *ifdP->xResolutionP, *ifdP->yResolutionP);
+
+    if (ifdP->orientationP) {
+        /* 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.
+        */
+
+        const char * orientDisp;
+
+        switch (*ifdP->orientationP) {
+        case ORIENT_NORMAL:     orientDisp = "Normal";          break;
+        case ORIENT_FLIP_HORIZ: orientDisp = "Flip horizontal"; break;
+        case ORIENT_ROTATE_180: orientDisp = "Rotate 180";      break;
+        case ORIENT_FLIP_VERT:  orientDisp = "Flip vertical";   break;
+        case ORIENT_TRANSPOSE:  orientDisp = "Transpose";       break;
+        case ORIENT_ROTATE_90:  orientDisp = "Rotate 90";       break;
+        case ORIENT_TRANSVERSE: orientDisp = "Transverse";      break;
+        case ORIENT_ROTATE_270: orientDisp = "Rotate 270";      break;
         }
-        fprintf(fileP, "\n");
-    }
-    if (imageInfoP->ApertureFNumber){
-        fprintf(fileP, "Aperture     : f/%3.1f\n",
-                (double)imageInfoP->ApertureFNumber);
+
+        pm_message("Orientation  : %s", orientDisp);
     }
-    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->isColorP)
+        pm_message("Color/bw     : %s",
+                   *ifdP->isColorP ? "Color" : "Black and white");
+
+    if (ifdP->flashP)
+        pm_message("Flash used   : %s",
+                   *ifdP->flashP ? "Yes" :"No");
+
+    if (ifdP->exposureTimeP) {
+        const char * timeDisp;
+        const char * recipDisp;
+
+        if (*ifdP->exposureTimeP < 0.010) {
+            pm_asprintf(&timeDisp, "%6.4f s", *ifdP->exposureTimeP);
+        } else {
+            pm_asprintf(&timeDisp, "%5.3f s", *ifdP->exposureTimeP);
         }
+        /* We've seen the EXPOSURETIME tag be present but contain zero.
+           I don't know why.
+        */
+        if (*ifdP->exposureTimeP <= 0.5 && *ifdP->exposureTimeP > 0) {
+            pm_asprintf(&recipDisp, " (1/%d)",
+                        (int)(0.5 + 1 / *ifdP->exposureTimeP));
+        } else
+            recipDisp = pm_strdup("");
+
+        pm_message("Exposure time: %s %s", timeDisp, recipDisp);
+
+        pm_strfree(recipDisp);
+        pm_strfree(timeDisp);
     }
+    if (ifdP->shutterSpeedP)
+        pm_message("Shutter speed: 1/%u", *ifdP->shutterSpeedP);
 
-    if (imageInfoP->ISOequivalent){ /* 05-jan-2001 vcs */
-        fprintf(fileP, "ISO equiv.   : %2d\n",(int)imageInfoP->ISOequivalent);
+    if (ifdP->apertureFNumberP) {
+        pm_message("Aperture     : f/%3.1f", *ifdP->apertureFNumberP);
     }
-    if (imageInfoP->ExposureBias){ /* 05-jan-2001 vcs */
-        fprintf(fileP, "Exposure bias:%4.2f\n",
-                (double)imageInfoP->ExposureBias);
+    if (ifdP->distanceP) {
+        if (*ifdP->distanceP < 0)
+            pm_message("Focus dist.  : Infinite");
+        else
+            pm_message("Focus dist.  :%5.2fm", *ifdP->distanceP);
     }
-        
-    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->isoEquivalentP)
+        pm_message("ISO equiv.   : %2d", *ifdP->isoEquivalentP);
+
+    if (ifdP->exposureBiasP)
+        pm_message("Exposure bias: %4.2f", *ifdP->exposureBiasP);
+
+    if (ifdP->whiteBalanceP) {
+        const char * whiteBalanceDisp;
+
+        switch(*ifdP->whiteBalanceP) {
+        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->meteringModeP) {
+        const char * meteringModeDisp;
+
+        switch(*ifdP->meteringModeP) {
+        case 2:  meteringModeDisp = "center weight";  break;
+        case 3:  meteringModeDisp = "spot";           break;
+        case 5:  meteringModeDisp = "matrix";         break;
+        default: meteringModeDisp = "?";              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->exposureProgramP) {
+        const char * exposureDisp;
+
+        switch(*ifdP->exposureProgramP) {
+        case 2:  exposureDisp = "program (auto)";                break;
+        case 3:  exposureDisp = "aperture priority (semi-auto)"; break;
+        case 4:  exposureDisp = "shutter priority (semi-auto)";  break;
+        default: exposureDisp = "?";                              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->compressionLevelP) {
+        const char * jpegQualityDisp;
+
+        switch(*ifdP->compressionLevelP) {
+        case 1:  jpegQualityDisp = "basic";  break;
+        case 2:  jpegQualityDisp = "normal"; break;
+        case 4:  jpegQualityDisp = "fine";   break;
+        default: jpegQualityDisp = "?";      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;
+    if (ifdP->comments) {
+        char * buffer;
 
-        fprintf(fileP, "Comment      : ");
+        MALLOCARRAY(buffer, strlen(ifdP->comments) + 1);
 
-        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 (!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");
     }
+}
 
-    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.
+--------------------------------------------------------------------------*/
+    showIfd(&imageInfoP->mainImage);
+
+    if (imageInfoP->mainImage.focalLengthP) {
+        const char * mm35equiv;
+
+        if (imageInfoP->ccdWidthP) {
+            pm_asprintf(&mm35equiv, "  (35mm equivalent: %dmm)",
+                        (int) (*imageInfoP->mainImage.focalLengthP /
+                               *imageInfoP->ccdWidthP * 36 + 0.5));
+        } else
+            mm35equiv = pm_strdup("");
+
+        pm_message("Focal length : %4.1fmm %s",
+                   *imageInfoP->mainImage.focalLengthP, mm35equiv);
+
+        pm_strfree(mm35equiv);
+    }
+
+    if (imageInfoP->ccdWidthP)
+        pm_message("CCD width    : %2.4fmm", *imageInfoP->ccdWidthP);
 }
 
 
+
+void
+exif_terminateImageInfo(exif_ImageInfo * const imageInfoP) {
+
+    terminateIfd(&imageInfoP->mainImage);
+    terminateIfd(&imageInfoP->thumbnailImage);
+}
+
+
+
diff --git a/converter/other/exif.h b/converter/other/exif.h
index 57eb745b..37dcf240 100644
--- a/converter/other/exif.h
+++ b/converter/other/exif.h
@@ -10,52 +10,77 @@
     #define PATH_MAX _MAX_PATH
 #endif
 
+typedef struct {
 /*--------------------------------------------------------------------------
-  This structure 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.
 --------------------------------------------------------------------------*/
+    /* In all of the following members, a null pointer means "not present,"
+       which normally means the tag from which the information comes was
+       not present in the IFD.
+
+       The EXIF format might require certain tags to be present, but we
+       don't.
+    */
+    const char *   cameraMake;
+    const char *   cameraModel;
+    const char *   dateTime;
+    float *        xResolutionP;
+    float *        yResolutionP;
+    int *          orientationP;
+    int *          isColorP;
+    int *          flashP;
+    float *        focalLengthP;
+    float *        exposureTimeP;
+    unsigned int * shutterSpeedP;  /* e.g. 128 for 1/128 second */
+    float *        apertureFNumberP;
+    float *        distanceP;
+    float *        exposureBiasP;
+    int *          whiteBalanceP;
+    int *          meteringModeP;
+    int *          exposureProgramP;
+    int *          isoEquivalentP;
+    int *          compressionLevelP;
+    const char *   comments;
+    unsigned int * thumbnailOffsetP;
+    unsigned int * thumbnailLengthP;
+    unsigned int * exifImageLengthP;
+    unsigned int * exifImageWidthP;
+    double *       focalPlaneXResP;
+    double *       focalPlaneUnitsP;
+
+    const unsigned char * thumbnail;  /* Pointer at the thumbnail */
+    unsigned thumbnailSize;     /* Size of thumbnail. */
+} exif_ifd;
+
+
 typedef struct {
-    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;
+/*--------------------------------------------------------------------------
+  A structure of this type contains the information from an EXIF header.
+--------------------------------------------------------------------------*/
+    exif_ifd mainImage;       /* aka IFD0 */
+    exif_ifd thumbnailImage;  /* aka IFD1 */
+    float *  ccdWidthP;  /* NULL means none */
 } 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);
+
+void
+exif_terminateImageInfo(exif_ImageInfo * const imageInfoP);
 
 #endif
diff --git a/converter/other/jpegtopnm.c b/converter/other/jpegtopnm.c
index 98552c00..6357e859 100644
--- a/converter/other/jpegtopnm.c
+++ b/converter/other/jpegtopnm.c
@@ -666,7 +666,7 @@ print_exif_info(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);
 }
 
 
diff --git a/doc/HISTORY b/doc/HISTORY
index 1164daca..55a5e9db 100644
--- a/doc/HISTORY
+++ b/doc/HISTORY
@@ -4,6 +4,11 @@ Netpbm.
 CHANGE HISTORY 
 --------------
 
+23.03.25 BJH  Release 10.86.38
+
+              jpegtopnm: Many fixes to -dumpexif.  Always broken.
+              (-dumpexif was new in Netpbm 9.18 (September 2001))
+
 23.03.14 BJH  Release 10.86.37
 
               pamtopng: fix -chroma option: always rejected.  Always broken.
diff --git a/version.mk b/version.mk
index 6a91ce52..7aac55ea 100644
--- a/version.mk
+++ b/version.mk
@@ -1,3 +1,3 @@
 NETPBM_MAJOR_RELEASE = 10
 NETPBM_MINOR_RELEASE = 86
-NETPBM_POINT_RELEASE = 37
+NETPBM_POINT_RELEASE = 38