//--------------------------------------------------------------------------
// Program to pull the information out of various types of EXIF digital
// camera files and show it in a reasonably consistent way
//
// This module parses the very complicated exif structures.
//
// Matthias Wandel
//--------------------------------------------------------------------------
#include "jhead.h"
#include <math.h>
#include <ctype.h>
#include <utils/Log.h>
static unsigned char * DirWithThumbnailPtrs;
static double FocalplaneXRes;
static double FocalplaneUnits;
static int ExifImageWidth;
static int MotorolaOrder = 0;
// for fixing the rotation.
static void * OrientationPtr[2];
static int OrientationNumFormat[2];
int NumOrientations = 0;
// Define the line below to turn on poor man's debugging output
#undef SUPERDEBUG
#ifdef SUPERDEBUG
#define printf ALOGE
#endif
//--------------------------------------------------------------------------
// Table of Jpeg encoding process names
static const TagTable_t ProcessTable[] = {
{ M_SOF0, "Baseline", 0, 0},
{ M_SOF1, "Extended sequential", 0, 0},
{ M_SOF2, "Progressive", 0, 0},
{ M_SOF3, "Lossless", 0, 0},
{ M_SOF5, "Differential sequential", 0, 0},
{ M_SOF6, "Differential progressive", 0, 0},
{ M_SOF7, "Differential lossless", 0, 0},
{ M_SOF9, "Extended sequential, arithmetic coding", 0, 0},
{ M_SOF10, "Progressive, arithmetic coding", 0, 0},
{ M_SOF11, "Lossless, arithmetic coding", 0, 0},
{ M_SOF13, "Differential sequential, arithmetic coding", 0, 0},
{ M_SOF14, "Differential progressive, arithmetic coding", 0, 0},
{ M_SOF15, "Differential lossless, arithmetic coding", 0, 0},
};
#define PROCESS_TABLE_SIZE (sizeof(ProcessTable) / sizeof(TagTable_t))
// 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.
};
const int BytesPerFormat[] = {0,1,1,2,4,8,1,1,2,4,8,4,8};
//--------------------------------------------------------------------------
// Describes tag values
#define TAG_INTEROP_INDEX 0x0001
#define TAG_INTEROP_VERSION 0x0002
#define TAG_IMAGE_WIDTH 0x0100
#define TAG_IMAGE_LENGTH 0x0101
#define TAG_BITS_PER_SAMPLE 0x0102
#define TAG_COMPRESSION 0x0103
#define TAG_PHOTOMETRIC_INTERP 0x0106
#define TAG_FILL_ORDER 0x010A
#define TAG_DOCUMENT_NAME 0x010D
#define TAG_IMAGE_DESCRIPTION 0x010E
#define TAG_MAKE 0x010F
#define TAG_MODEL 0x0110
#define TAG_SRIP_OFFSET 0x0111
#define TAG_ORIENTATION 0x0112
#define TAG_SAMPLES_PER_PIXEL 0x0115
#define TAG_ROWS_PER_STRIP 0x0116
#define TAG_STRIP_BYTE_COUNTS 0x0117
#define TAG_X_RESOLUTION 0x011A
#define TAG_Y_RESOLUTION 0x011B
#define TAG_PLANAR_CONFIGURATION 0x011C
#define TAG_RESOLUTION_UNIT 0x0128
#define TAG_TRANSFER_FUNCTION 0x012D
#define TAG_SOFTWARE 0x0131
#define TAG_DATETIME 0x0132
#define TAG_ARTIST 0x013B
#define TAG_WHITE_POINT 0x013E
#define TAG_PRIMARY_CHROMATICITIES 0x013F
#define TAG_TRANSFER_RANGE 0x0156
#define TAG_JPEG_PROC 0x0200
#define TAG_THUMBNAIL_OFFSET 0x0201
#define TAG_THUMBNAIL_LENGTH 0x0202
#define TAG_Y_CB_CR_COEFFICIENTS 0x0211
#define TAG_Y_CB_CR_SUB_SAMPLING 0x0212
#define TAG_Y_CB_CR_POSITIONING 0x0213
#define TAG_REFERENCE_BLACK_WHITE 0x0214
#define TAG_RELATED_IMAGE_WIDTH 0x1001
#define TAG_RELATED_IMAGE_LENGTH 0x1002
#define TAG_CFA_REPEAT_PATTERN_DIM 0x828D
#define TAG_CFA_PATTERN1 0x828E
#define TAG_BATTERY_LEVEL 0x828F
#define TAG_COPYRIGHT 0x8298
#define TAG_EXPOSURETIME 0x829A
#define TAG_FNUMBER 0x829D
#define TAG_IPTC_NAA 0x83BB
#define TAG_EXIF_OFFSET 0x8769
#define TAG_INTER_COLOR_PROFILE 0x8773
#define TAG_EXPOSURE_PROGRAM 0x8822
#define TAG_SPECTRAL_SENSITIVITY 0x8824
#define TAG_GPSINFO 0x8825
#define TAG_ISO_EQUIVALENT 0x8827
#define TAG_OECF 0x8828
#define TAG_EXIF_VERSION 0x9000
#define TAG_DATETIME_ORIGINAL 0x9003
#define TAG_DATETIME_DIGITIZED 0x9004
#define TAG_COMPONENTS_CONFIG 0x9101
#define TAG_CPRS_BITS_PER_PIXEL 0x9102
#define TAG_SHUTTERSPEED 0x9201
#define TAG_APERTURE 0x9202
#define TAG_BRIGHTNESS_VALUE 0x9203
#define TAG_EXPOSURE_BIAS 0x9204
#define TAG_MAXAPERTURE 0x9205
#define TAG_SUBJECT_DISTANCE 0x9206
#define TAG_METERING_MODE 0x9207
#define TAG_LIGHT_SOURCE 0x9208
#define TAG_FLASH 0x9209
#define TAG_FOCALLENGTH 0x920A
#define TAG_MAKER_NOTE 0x927C
#define TAG_USERCOMMENT 0x9286
#define TAG_SUBSEC_TIME 0x9290
#define TAG_SUBSEC_TIME_ORIG 0x9291
#define TAG_SUBSEC_TIME_DIG 0x9292
#define TAG_WINXP_TITLE 0x9c9b // Windows XP - not part of exif standard.
#define TAG_WINXP_COMMENT 0x9c9c // Windows XP - not part of exif standard.
#define TAG_WINXP_AUTHOR 0x9c9d // Windows XP - not part of exif standard.
#define TAG_WINXP_KEYWORDS 0x9c9e // Windows XP - not part of exif standard.
#define TAG_WINXP_SUBJECT 0x9c9f // Windows XP - not part of exif standard.
#define TAG_FLASH_PIX_VERSION 0xA000
#define TAG_COLOR_SPACE 0xA001
#define TAG_EXIF_IMAGEWIDTH 0xA002
#define TAG_EXIF_IMAGELENGTH 0xA003
#define TAG_RELATED_AUDIO_FILE 0xA004
#define TAG_INTEROP_OFFSET 0xA005
#define TAG_FLASH_ENERGY 0xA20B
#define TAG_SPATIAL_FREQ_RESP 0xA20C
#define TAG_FOCAL_PLANE_XRES 0xA20E
#define TAG_FOCAL_PLANE_YRES 0xA20F
#define TAG_FOCAL_PLANE_UNITS 0xA210
#define TAG_SUBJECT_LOCATION 0xA214
#define TAG_EXPOSURE_INDEX 0xA215
#define TAG_SENSING_METHOD 0xA217
#define TAG_FILE_SOURCE 0xA300
#define TAG_SCENE_TYPE 0xA301
#define TAG_CFA_PATTERN 0xA302
#define TAG_CUSTOM_RENDERED 0xA401
#define TAG_EXPOSURE_MODE 0xA402
#define TAG_WHITEBALANCE 0xA403
#define TAG_DIGITALZOOMRATIO 0xA404
#define TAG_FOCALLENGTH_35MM 0xA405
#define TAG_SCENE_CAPTURE_TYPE 0xA406
#define TAG_GAIN_CONTROL 0xA407
#define TAG_CONTRAST 0xA408
#define TAG_SATURATION 0xA409
#define TAG_SHARPNESS 0xA40A
#define TAG_DISTANCE_RANGE 0xA40C
// TODO: replace the ", 0" values in this table with the correct format, e.g. ", FMT_USHORT"
static const TagTable_t TagTable[] = {
{ TAG_INTEROP_INDEX, "InteropIndex", 0, 0},
{ TAG_INTEROP_VERSION, "InteropVersion", 0, 0},
{ TAG_IMAGE_WIDTH, "ImageWidth", FMT_USHORT, 1},
{ TAG_IMAGE_LENGTH, "ImageLength", FMT_USHORT, 1},
{ TAG_BITS_PER_SAMPLE, "BitsPerSample", FMT_USHORT, 3},
{ TAG_COMPRESSION, "Compression", FMT_USHORT, 1},
{ TAG_PHOTOMETRIC_INTERP, "PhotometricInterpretation", FMT_USHORT, 1},
{ TAG_FILL_ORDER, "FillOrder", 0, 0},
{ TAG_DOCUMENT_NAME, "DocumentName", 0, 0},
{ TAG_IMAGE_DESCRIPTION, "ImageDescription", 0, 0 },
{ TAG_MAKE, "Make", FMT_STRING, -1},
{ TAG_MODEL, "Model", FMT_STRING, -1},
{ TAG_SRIP_OFFSET, "StripOffsets", FMT_USHORT, 1},
{ TAG_ORIENTATION, "Orientation", FMT_USHORT, 1},
{ TAG_SAMPLES_PER_PIXEL, "SamplesPerPixel", FMT_USHORT, 3},
{ TAG_ROWS_PER_STRIP, "RowsPerStrip", FMT_USHORT, 1},
{ TAG_STRIP_BYTE_COUNTS, "StripByteCounts", FMT_USHORT, 1},
{ TAG_X_RESOLUTION, "XResolution", FMT_URATIONAL, 1},
{ TAG_Y_RESOLUTION, "YResolution", FMT_URATIONAL, 1},
{ TAG_PLANAR_CONFIGURATION, "PlanarConfiguration", FMT_USHORT, 1},
{ TAG_RESOLUTION_UNIT, "ResolutionUnit", FMT_USHORT, 1},
{ TAG_TRANSFER_FUNCTION, "TransferFunction", FMT_USHORT, 768},
{ TAG_SOFTWARE, "Software", FMT_STRING, -1},
{ TAG_DATETIME, "DateTime", FMT_STRING, 20},
{ TAG_ARTIST, "Artist", FMT_STRING, -1},
{ TAG_WHITE_POINT, "WhitePoint", FMT_SRATIONAL, 2},
{ TAG_PRIMARY_CHROMATICITIES, "PrimaryChromaticities", FMT_SRATIONAL, 6},
{ TAG_TRANSFER_RANGE, "TransferRange", 0, 0},
{ TAG_JPEG_PROC, "JPEGProc", 0, 0},
{ TAG_THUMBNAIL_OFFSET, "ThumbnailOffset", 0, 0},
{ TAG_THUMBNAIL_LENGTH, "ThumbnailLength", 0, 0},
{ TAG_Y_CB_CR_COEFFICIENTS, "YCbCrCoefficients", FMT_SRATIONAL, 3},
{ TAG_Y_CB_CR_SUB_SAMPLING, "YCbCrSubSampling", FMT_USHORT, 2},
{ TAG_Y_CB_CR_POSITIONING, "YCbCrPositioning", FMT_USHORT, 1},
{ TAG_REFERENCE_BLACK_WHITE, "ReferenceBlackWhite", FMT_SRATIONAL, 6},
{ TAG_RELATED_IMAGE_WIDTH, "RelatedImageWidth", 0, 0},
{ TAG_RELATED_IMAGE_LENGTH, "RelatedImageLength", 0, 0},
{ TAG_CFA_REPEAT_PATTERN_DIM, "CFARepeatPatternDim", 0, 0},
{ TAG_CFA_PATTERN1, "CFAPattern", 0, 0},
{ TAG_BATTERY_LEVEL, "BatteryLevel", 0, 0},
{ TAG_COPYRIGHT, "Copyright", FMT_STRING, -1},
{ TAG_EXPOSURETIME, "ExposureTime", FMT_SRATIONAL, 1},
{ TAG_FNUMBER, "FNumber", FMT_SRATIONAL, 1},
{ TAG_IPTC_NAA, "IPTC/NAA", 0, 0},
{ TAG_EXIF_OFFSET, "ExifOffset", 0, 0},
{ TAG_INTER_COLOR_PROFILE, "InterColorProfile", 0, 0},
{ TAG_EXPOSURE_PROGRAM, "ExposureProgram", FMT_SSHORT, 1},
{ TAG_SPECTRAL_SENSITIVITY, "SpectralSensitivity", FMT_STRING, -1},
{ TAG_GPSINFO, "GPS Dir offset", 0, 0},
{ TAG_ISO_EQUIVALENT, "ISOSpeedRatings", FMT_SSHORT, -1},
{ TAG_OECF, "OECF", 0, 0},
{ TAG_EXIF_VERSION, "ExifVersion", FMT_BYTE, 4},
{ TAG_DATETIME_ORIGINAL, "DateTimeOriginal", FMT_STRING, 20},
{ TAG_DATETIME_DIGITIZED, "DateTimeDigitized", FMT_STRING, 20},
{ TAG_COMPONENTS_CONFIG, "ComponentsConfiguration", FMT_BYTE, 4},
{ TAG_CPRS_BITS_PER_PIXEL, "CompressedBitsPerPixel", FMT_SRATIONAL, 1},
{ TAG_SHUTTERSPEED, "ShutterSpeedValue", FMT_SRATIONAL, 1},
{ TAG_APERTURE, "ApertureValue", FMT_URATIONAL, 1},
{ TAG_BRIGHTNESS_VALUE, "BrightnessValue", FMT_SRATIONAL, 1},
{ TAG_EXPOSURE_BIAS, "ExposureBiasValue", FMT_SRATIONAL, 1},
{ TAG_MAXAPERTURE, "MaxApertureValue", FMT_URATIONAL, 1},
{ TAG_SUBJECT_DISTANCE, "SubjectDistance", FMT_URATIONAL, 1},
{ TAG_METERING_MODE, "MeteringMode", FMT_USHORT, 1},
{ TAG_LIGHT_SOURCE, "LightSource", FMT_USHORT, 1},
{ TAG_FLASH, "Flash", FMT_USHORT, 1},
{ TAG_FOCALLENGTH, "FocalLength", FMT_URATIONAL, 1},
{ TAG_MAKER_NOTE, "MakerNote", FMT_STRING, -1},
{ TAG_USERCOMMENT, "UserComment", FMT_STRING, -1},
{ TAG_SUBSEC_TIME, "SubSecTime", FMT_STRING, -1},
{ TAG_SUBSEC_TIME_ORIG, "SubSecTimeOriginal", FMT_STRING, -1},
{ TAG_SUBSEC_TIME_DIG, "SubSecTimeDigitized", FMT_STRING, -1},
{ TAG_WINXP_TITLE, "Windows-XP Title", 0, 0},
{ TAG_WINXP_COMMENT, "Windows-XP comment", 0, 0},
{ TAG_WINXP_AUTHOR, "Windows-XP author", 0, 0},
{ TAG_WINXP_KEYWORDS, "Windows-XP keywords", 0, 0},
{ TAG_WINXP_SUBJECT, "Windows-XP subject", 0, 0},
{ TAG_FLASH_PIX_VERSION, "FlashPixVersion", FMT_BYTE, 4},
{ TAG_COLOR_SPACE, "ColorSpace", FMT_USHORT, 1},
{ TAG_EXIF_IMAGEWIDTH, "ExifImageWidth", 0, 0},
{ TAG_EXIF_IMAGELENGTH, "ExifImageLength", 0, 0},
{ TAG_RELATED_AUDIO_FILE, "RelatedAudioFile", 0, 0},
{ TAG_INTEROP_OFFSET, "InteroperabilityOffset", 0, 0},
{ TAG_FLASH_ENERGY, "FlashEnergy", FMT_URATIONAL, 1},
{ TAG_SPATIAL_FREQ_RESP, "SpatialFrequencyResponse", FMT_STRING, -1},
{ TAG_FOCAL_PLANE_XRES, "FocalPlaneXResolution", FMT_URATIONAL, 1},
{ TAG_FOCAL_PLANE_YRES, "FocalPlaneYResolution", FMT_URATIONAL, 1},
{ TAG_FOCAL_PLANE_UNITS, "FocalPlaneResolutionUnit", FMT_USHORT, 1},
{ TAG_SUBJECT_LOCATION, "SubjectLocation", FMT_USHORT, 2},
{ TAG_EXPOSURE_INDEX, "ExposureIndex", FMT_URATIONAL, 1},
{ TAG_SENSING_METHOD, "SensingMethod", FMT_USHORT, 1},
{ TAG_FILE_SOURCE, "FileSource", 0, 1},
{ TAG_SCENE_TYPE, "SceneType", 0, 1},
{ TAG_CFA_PATTERN, "CFA Pattern", 0, -1},
{ TAG_CUSTOM_RENDERED, "CustomRendered", FMT_USHORT, 1},
{ TAG_EXPOSURE_MODE, "ExposureMode", FMT_USHORT, 1},
{ TAG_WHITEBALANCE, "WhiteBalance", FMT_USHORT, 1},
{ TAG_DIGITALZOOMRATIO, "DigitalZoomRatio", FMT_URATIONAL, 1},
{ TAG_FOCALLENGTH_35MM, "FocalLengthIn35mmFilm", FMT_USHORT, 1},
{ TAG_SCENE_CAPTURE_TYPE, "SceneCaptureType", FMT_USHORT, 1},
{ TAG_GAIN_CONTROL, "GainControl", FMT_URATIONAL, 1},
{ TAG_CONTRAST, "Contrast", FMT_USHORT, 1},
{ TAG_SATURATION, "Saturation", FMT_USHORT, 1},
{ TAG_SHARPNESS, "Sharpness", FMT_USHORT, 1},
{ TAG_DISTANCE_RANGE, "SubjectDistanceRange", FMT_USHORT, 1},
} ;
#define TAG_TABLE_SIZE (sizeof(TagTable) / sizeof(TagTable_t))
int TagNameToValue(const char* tagName)
{
unsigned int i;
for (i = 0; i < TAG_TABLE_SIZE; i++) {
if (strcmp(TagTable[i].Desc, tagName) == 0) {
printf("found tag %s val %d", TagTable[i].Desc, TagTable[i].Tag);
return TagTable[i].Tag;
}
}
printf("tag %s NOT FOUND", tagName);
return -1;
}
int IsDateTimeTag(unsigned short tag)
{
return ((tag == TAG_DATETIME)? TRUE: FALSE);
}
//--------------------------------------------------------------------------
// Convert a 16 bit unsigned value to file's native byte order
//--------------------------------------------------------------------------
static void Put16u(void * Short, unsigned short PutValue)
{
if (MotorolaOrder){
((uchar *)Short)[0] = (uchar)(PutValue>>8);
((uchar *)Short)[1] = (uchar)PutValue;
}else{
((uchar *)Short)[0] = (uchar)PutValue;
((uchar *)Short)[1] = (uchar)(PutValue>>8);
}
}
//--------------------------------------------------------------------------
// Convert a 16 bit unsigned value from file's native byte order
//--------------------------------------------------------------------------
int Get16u(void * Short)
{
if (MotorolaOrder){
return (((uchar *)Short)[0] << 8) | ((uchar *)Short)[1];
}else{
return (((uchar *)Short)[1] << 8) | ((uchar *)Short)[0];
}
}
//--------------------------------------------------------------------------
// Convert a 32 bit signed value from file's native byte order
//--------------------------------------------------------------------------
int Get32s(void * Long)
{
if (MotorolaOrder){
return ((( char *)Long)[0] << 24) | (((uchar *)Long)[1] << 16)
| (((uchar *)Long)[2] << 8 ) | (((uchar *)Long)[3] << 0 );
}else{
return ((( char *)Long)[3] << 24) | (((uchar *)Long)[2] << 16)
| (((uchar *)Long)[1] << 8 ) | (((uchar *)Long)[0] << 0 );
}
}
//--------------------------------------------------------------------------
// Convert a 32 bit unsigned value to file's native byte order
//--------------------------------------------------------------------------
void Put32u(void * Value, unsigned PutValue)
{
if (MotorolaOrder){
((uchar *)Value)[0] = (uchar)(PutValue>>24);
((uchar *)Value)[1] = (uchar)(PutValue>>16);
((uchar *)Value)[2] = (uchar)(PutValue>>8);
((uchar *)Value)[3] = (uchar)PutValue;
}else{
((uchar *)Value)[0] = (uchar)PutValue;
((uchar *)Value)[1] = (uchar)(PutValue>>8);
((uchar *)Value)[2] = (uchar)(PutValue>>16);
((uchar *)Value)[3] = (uchar)(PutValue>>24);
}
}
//--------------------------------------------------------------------------
// Convert a 32 bit unsigned value from file's native byte order
//--------------------------------------------------------------------------
unsigned Get32u(void * Long)
{
return (unsigned)Get32s(Long) & 0xffffffff;
}
//--------------------------------------------------------------------------
// Display a number as one of its many formats
//--------------------------------------------------------------------------
void PrintFormatNumber(void * ValuePtr, int Format, int ByteCount)
{
int s,n;
for(n=0;n<16;n++){
switch(Format){
case FMT_SBYTE:
case FMT_BYTE: printf("%02x",*(uchar *)ValuePtr); s=1; break;
case FMT_USHORT: printf("%d",Get16u(ValuePtr)); s=2; break;
case FMT_ULONG:
case FMT_SLONG: printf("%d",Get32s(ValuePtr)); s=4; break;
case FMT_SSHORT: printf("%hd",(signed short)Get16u(ValuePtr)); s=2; break;
case FMT_URATIONAL:
case FMT_SRATIONAL:
printf("%d/%d",Get32s(ValuePtr), Get32s(4+(char *)ValuePtr));
s = 8;
break;
case FMT_SINGLE: printf("%f",(double)*(float *)ValuePtr); s=8; break;
case FMT_DOUBLE: printf("%f",*(double *)ValuePtr); s=8; break;
default:
printf("Unknown format %d:", Format);
return;
}
ByteCount -= s;
if (ByteCount <= 0) break;
printf(", ");
ValuePtr = (void *)((char *)ValuePtr + s);
}
if (n >= 16) printf("...");
}
//--------------------------------------------------------------------------
// Evaluate number, be it int, rational, or float from directory.
//--------------------------------------------------------------------------
double ConvertAnyFormat(void * ValuePtr, int Format)
{
double Value;
Value = 0;
switch(Format){
case FMT_SBYTE: Value = *(signed char *)ValuePtr; break;
case FMT_BYTE: Value = *(uchar *)ValuePtr; break;
case FMT_USHORT: Value = Get16u(ValuePtr); break;
case FMT_ULONG: Value = Get32u(ValuePtr); break;
case FMT_URATIONAL:
case FMT_SRATIONAL:
{
int Num,Den;
Num = Get32s(ValuePtr);
Den = Get32s(4+(char *)ValuePtr);
if (Den == 0){
Value = 0;
}else{
Value = (double)Num/Den;
}
break;
}
case FMT_SSHORT: Value = (signed short)Get16u(ValuePtr); break;
case FMT_SLONG: Value = Get32s(ValuePtr); break;
// Not sure if this is correct (never seen float used in Exif format)
case FMT_SINGLE: Value = (double)*(float *)ValuePtr; break;
case FMT_DOUBLE: Value = *(double *)ValuePtr; break;
default:
ErrNonfatal("Illegal format code %d",Format,0);
}
return Value;
}
//--------------------------------------------------------------------------
// Convert a double value into a signed or unsigned rational number.
//--------------------------------------------------------------------------
static void float2urat(double value, unsigned int max, unsigned int *numerator,
unsigned int *denominator) {
if (value <= 0) {
*numerator = 0;
*denominator = 1;
return;
}
if (value > max) {
*numerator = max;
*denominator = 1;
return;
}
// For values less than 1e-9, scale as much as possible
if (value < 1e-9) {
unsigned int n = (unsigned int)(value * max);
if (n == 0) {
*numerator = 0;
*denominator = 1;
} else {
*numerator = n;
*denominator = max;
}
return;
}
// Try to use a denominator of 1e9, 1e8, ..., until the numerator fits
unsigned int d;
for (d = 1000000000; d >= 1; d /= 10) {
double s = value * d;
if (s <= max) {
// Remove the trailing zeros from both.
unsigned int n = (unsigned int)s;
while (n % 10 == 0 && d >= 10) {
n /= 10;
d /= 10;
}
*numerator = n;
*denominator = d;
return;
}
}
// Shouldn't reach here because the denominator 1 should work
// above. But just in case.
*numerator = 0;
*denominator = 1;
}
static void ConvertDoubleToURational(double value, unsigned int *numerator,
unsigned int *denominator) {
float2urat(value, 0xFFFFFFFFU, numerator, denominator);
}
static void ConvertDoubleToSRational(double value, int *numerator,
int *denominator) {
int negative = 0;
if (value < 0) {
value = -value;
negative = 1;
}
unsigned int n, d;
float2urat(value, 0x7FFFFFFFU, &n, &d);
*numerator = (int)n;
*denominator = (int)d;
if (negative) {
*numerator = -*numerator;
}
}
//--------------------------------------------------------------------------
// Process one of the nested EXIF directories.
//--------------------------------------------------------------------------
static void ProcessExifDir(unsigned char * DirStart, unsigned char * OffsetBase,
unsigned ExifLength, int NestingLevel)
{
int de;
int a;
int NumDirEntries;
unsigned ThumbnailOffset = 0;
unsigned ThumbnailSize = 0;
char IndentString[25];
printf("ProcessExifDir");
if (NestingLevel > 4){
ErrNonfatal("Maximum directory nesting exceeded (corrupt exif header)", 0,0);
return;
}
memset(IndentString, ' ', 25);
IndentString[NestingLevel * 4] = '\0';
NumDirEntries = Get16u(DirStart);
#define DIR_ENTRY_ADDR(Start, Entry) (Start+2+12*(Entry))
{
unsigned char * DirEnd;
DirEnd = DIR_ENTRY_ADDR(DirStart, NumDirEntries);
if (DirEnd+4 > (OffsetBase+ExifLength)){
if (DirEnd+2 == OffsetBase+ExifLength || DirEnd == OffsetBase+ExifLength){
// Version 1.3 of jhead would truncate a bit too much.
// This also caught later on as well.
}else{
ErrNonfatal("Illegally sized exif subdirectory (%d entries)",NumDirEntries,0);
return;
}
}
if (DumpExifMap){
printf("Map: %05d-%05d: Directory\n",(int)(DirStart-OffsetBase), (int)(DirEnd+4-OffsetBase));
}
}
if (ShowTags){
printf("(dir has %d entries)\n",NumDirEntries);
}
for (de=0;de<NumDirEntries;de++){
int Tag, Format, Components;
unsigned char * ValuePtr;
int ByteCount;
unsigned char * DirEntry;
DirEntry = DIR_ENTRY_ADDR(DirStart, de);
Tag = Get16u(DirEntry);
Format = Get16u(DirEntry+2);
Components = Get32u(DirEntry+4);
if ((Format-1) >= NUM_FORMATS) {
// (-1) catches illegal zero case as unsigned underflows to positive large.
ErrNonfatal("Illegal number format %d for tag %04x", Format, Tag);
continue;
}
if ((unsigned)Components > 0x10000){
ErrNonfatal("Illegal number of components %d for tag %04x", Components, Tag);
continue;
}
ByteCount = Components * BytesPerFormat[Format];
if (ByteCount > 4){
unsigned OffsetVal;
OffsetVal = Get32u(DirEntry+8);
// If its bigger than 4 bytes, the dir entry contains an offset.
if (OffsetVal+ByteCount > ExifLength){
// Bogus pointer offset and / or bytecount value
ErrNonfatal("Illegal value pointer for tag %04x", Tag,0);
continue;
}
ValuePtr = OffsetBase+OffsetVal;
if (OffsetVal > ImageInfo.LargestExifOffset){
ImageInfo.LargestExifOffset = OffsetVal;
}
if (DumpExifMap){
printf("Map: %05d-%05d: Data for tag %04x\n",OffsetVal, OffsetVal+ByteCount, Tag);
}
}else{
// 4 bytes or less and value is in the dir entry itself
ValuePtr = DirEntry+8;
}
if (Tag == TAG_MAKER_NOTE){
if (ShowTags){
printf("%s Maker note: ",IndentString);
}
ProcessMakerNote(ValuePtr, ByteCount, OffsetBase, ExifLength);
continue;
}
if (ShowTags){
// Show tag name
for (a=0;;a++){
if (a >= (int)TAG_TABLE_SIZE){
printf("%s", IndentString);
printf(" Unknown Tag %04x Value = ", Tag);
break;
}
if (TagTable[a].Tag == Tag){
printf("%s", IndentString);
printf(" %s = ",TagTable[a].Desc);
break;
}
}
// Show tag value.
switch(Format){
case FMT_BYTE:
if(ByteCount>1){
printf("%.*ls\n", ByteCount/2, (wchar_t *)ValuePtr);
}else{
PrintFormatNumber(ValuePtr, Format, ByteCount);
printf("\n");
}
break;
case FMT_UNDEFINED:
// Undefined is typically an ascii string.
case FMT_STRING:
// String arrays printed without function call (different from int arrays)
{
printf("\"%s\"", ValuePtr);
// int NoPrint = 0;
// printf("\"");
// for (a=0;a<ByteCount;a++){
// if (ValuePtr[a] >= 32){
// putchar(ValuePtr[a]);
// NoPrint = 0;
// }else{
// // Avoiding indicating too many unprintable characters of proprietary
// // bits of binary information this program may not know how to parse.
// if (!NoPrint && a != ByteCount-1){
// putchar('?');
// NoPrint = 1;
// }
// }
// }
// printf("\"\n");
}
break;
default:
// Handle arrays of numbers later (will there ever be?)
PrintFormatNumber(ValuePtr, Format, ByteCount);
printf("\n");
}
}
// Extract useful components of tag
switch(Tag){
case TAG_MAKE:
strncpy(ImageInfo.CameraMake, (char *)ValuePtr, ByteCount < 31 ? ByteCount : 31);
break;
case TAG_MODEL:
strncpy(ImageInfo.CameraModel, (char *)ValuePtr, ByteCount < 39 ? ByteCount : 39);
break;
case TAG_SUBSEC_TIME:
strlcpy(ImageInfo.SubSecTime, (char *)ValuePtr, sizeof(ImageInfo.SubSecTime));
break;
case TAG_SUBSEC_TIME_ORIG:
strlcpy(ImageInfo.SubSecTimeOrig, (char *)ValuePtr,
sizeof(ImageInfo.SubSecTimeOrig));
break;
case TAG_SUBSEC_TIME_DIG:
strlcpy(ImageInfo.SubSecTimeDig, (char *)ValuePtr,
sizeof(ImageInfo.SubSecTimeDig));
break;
case TAG_DATETIME_DIGITIZED:
strlcpy(ImageInfo.DigitizedTime, (char *)ValuePtr,
sizeof(ImageInfo.DigitizedTime));
if (ImageInfo.numDateTimeTags >= MAX_DATE_COPIES){
ErrNonfatal("More than %d date fields! This is nuts", MAX_DATE_COPIES, 0);
break;
}
ImageInfo.DateTimeOffsets[ImageInfo.numDateTimeTags++] =
(char *)ValuePtr - (char *)OffsetBase;
break;
case TAG_DATETIME_ORIGINAL:
// If we get a DATETIME_ORIGINAL, we use that one.
strncpy(ImageInfo.DateTime, (char *)ValuePtr, 19);
// Fallthru...
case TAG_DATETIME:
if (!isdigit(ImageInfo.DateTime[0])){
// If we don't already have a DATETIME_ORIGINAL, use whatever
// time fields we may have.
strncpy(ImageInfo.DateTime, (char *)ValuePtr, 19);
}
if (ImageInfo.numDateTimeTags >= MAX_DATE_COPIES){
ErrNonfatal("More than %d date fields! This is nuts", MAX_DATE_COPIES, 0);
break;
}
ImageInfo.DateTimeOffsets[ImageInfo.numDateTimeTags++] =
(char *)ValuePtr - (char *)OffsetBase;
break;
case TAG_WINXP_COMMENT:
if (ImageInfo.Comments[0]){ // We already have a jpeg comment.
// Already have a comment (probably windows comment), skip this one.
if (ShowTags) printf("Windows XP commend and other comment in header\n");
break; // Already have a windows comment, skip this one.
}
if (ByteCount > 1){
if (ByteCount > MAX_COMMENT_SIZE) ByteCount = MAX_COMMENT_SIZE;
memcpy(ImageInfo.Comments, ValuePtr, ByteCount);
ImageInfo.CommentWidchars = ByteCount/2;
}
break;
case TAG_USERCOMMENT:
if (ImageInfo.Comments[0]){ // We already have a jpeg comment.
// Already have a comment (probably windows comment), skip this one.
if (ShowTags) printf("Multiple comments in exif header\n");
break; // Already have a windows comment, skip this one.
}
// Comment is often padded with trailing spaces. Remove these first.
for (a=ByteCount;;){
a--;
if ((ValuePtr)[a] == ' '){
(ValuePtr)[a] = '\0';
}else{
break;
}
if (a == 0) break;
}
// Copy the comment
{
// We want to set copied comment length (msize) to be the
// minimum of:
// (1) The space still available in Exif
// (2) The given comment length (ByteCount)
// (3) MAX_COMMENT_SIZE - 1
int msiz = ExifLength - (ValuePtr-OffsetBase);
if (msiz > ByteCount) msiz = ByteCount;
if (msiz > MAX_COMMENT_SIZE - 1) msiz = MAX_COMMENT_SIZE - 1;
if (msiz > 5 && memcmp(ValuePtr, "ASCII", 5) == 0) {
for (a = 5; a < 10 && a < msiz; a++) {
int c = (ValuePtr)[a];
if (c != '\0' && c != ' ') {
strncpy(ImageInfo.Comments,
(char *)ValuePtr + a, msiz - a);
break;
}
}
} else {
strncpy(ImageInfo.Comments, (char *)ValuePtr, msiz);
}
}
break;
case TAG_FNUMBER:
// Simplest way of expressing aperture, so I trust it the most.
// (overwrite previously computd value if there is one)
ImageInfo.ApertureFNumber = (float)ConvertAnyFormat(ValuePtr, Format);
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 (ImageInfo.ApertureFNumber == 0){
ImageInfo.ApertureFNumber
= (float)exp(ConvertAnyFormat(ValuePtr, Format)*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.
ImageInfo.FocalLength.num = Get32u(ValuePtr);
ImageInfo.FocalLength.denom = Get32u(4+(char *)ValuePtr);
break;
case TAG_SUBJECT_DISTANCE:
// Inidcates the distacne the autofocus camera is focused to.
// Tends to be less accurate as distance increases.
ImageInfo.Distance = (float)ConvertAnyFormat(ValuePtr, Format);
break;
case TAG_EXPOSURETIME:
// Simplest way of expressing exposure time, so I trust it most.
// (overwrite previously computd value if there is one)
ImageInfo.ExposureTime = (float)ConvertAnyFormat(ValuePtr, Format);
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 (ImageInfo.ExposureTime == 0){
ImageInfo.ExposureTime
= (float)(1/exp(ConvertAnyFormat(ValuePtr, Format)*log(2)));
}
break;
case TAG_FLASH:
ImageInfo.FlashUsed=(int)ConvertAnyFormat(ValuePtr, Format);
break;
case TAG_ORIENTATION:
if (NumOrientations >= 2){
// Can have another orientation tag for the thumbnail, but if there's
// a third one, things are stringae.
ErrNonfatal("More than two orientation tags!",0,0);
break;
}
OrientationPtr[NumOrientations] = ValuePtr;
OrientationNumFormat[NumOrientations] = Format;
if (NumOrientations == 0){
ImageInfo.Orientation = (int)ConvertAnyFormat(ValuePtr, Format);
}
if (ImageInfo.Orientation < 0 || ImageInfo.Orientation > 8){
ErrNonfatal("Undefined rotation value %d", ImageInfo.Orientation, 0);
ImageInfo.Orientation = 0;
}
NumOrientations += 1;
break;
case TAG_EXIF_IMAGELENGTH:
case TAG_EXIF_IMAGEWIDTH:
// Use largest of height and width to deal with images that have been
// rotated to portrait format.
a = (int)ConvertAnyFormat(ValuePtr, Format);
if (ExifImageWidth < a) ExifImageWidth = a;
break;
case TAG_FOCAL_PLANE_XRES:
FocalplaneXRes = ConvertAnyFormat(ValuePtr, Format);
break;
case TAG_FOCAL_PLANE_UNITS:
switch((int)ConvertAnyFormat(ValuePtr, Format)){
case 1: FocalplaneUnits = 25.4; break; // 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 3: FocalplaneUnits = 10; break; // centimeter
case 4: FocalplaneUnits = 1; break; // millimeter
case 5: FocalplaneUnits = .001; break; // micrometer
}
break;
case TAG_EXPOSURE_BIAS:
ImageInfo.ExposureBias = (float)ConvertAnyFormat(ValuePtr, Format);
break;
case TAG_WHITEBALANCE:
ImageInfo.Whitebalance = (int)ConvertAnyFormat(ValuePtr, Format);
break;
case TAG_LIGHT_SOURCE:
ImageInfo.LightSource = (int)ConvertAnyFormat(ValuePtr, Format);
break;
case TAG_METERING_MODE:
ImageInfo.MeteringMode = (int)ConvertAnyFormat(ValuePtr, Format);
break;
case TAG_EXPOSURE_PROGRAM:
ImageInfo.ExposureProgram = (int)ConvertAnyFormat(ValuePtr, Format);
break;
case TAG_EXPOSURE_INDEX:
if (ImageInfo.ISOequivalent == 0){
// Exposure index and ISO equivalent are often used interchangeably,
// so we will do the same in jhead.
// http://photography.about.com/library/glossary/bldef_ei.htm
ImageInfo.ISOequivalent = (int)ConvertAnyFormat(ValuePtr, Format);
}
break;
case TAG_EXPOSURE_MODE:
ImageInfo.ExposureMode = (int)ConvertAnyFormat(ValuePtr, Format);
break;
case TAG_ISO_EQUIVALENT:
ImageInfo.ISOequivalent = (int)ConvertAnyFormat(ValuePtr, Format);
break;
case TAG_DIGITALZOOMRATIO:
ImageInfo.DigitalZoomRatio = (float)ConvertAnyFormat(ValuePtr, Format);
break;
case TAG_THUMBNAIL_OFFSET:
ThumbnailOffset = (unsigned)ConvertAnyFormat(ValuePtr, Format);
DirWithThumbnailPtrs = DirStart;
break;
case TAG_THUMBNAIL_LENGTH:
ThumbnailSize = (unsigned)ConvertAnyFormat(ValuePtr, Format);
ImageInfo.ThumbnailSizeOffset = ValuePtr-OffsetBase;
break;
case TAG_EXIF_OFFSET:
if (ShowTags) printf("%s Exif Dir:",IndentString);
case TAG_INTEROP_OFFSET:
if (Tag == TAG_INTEROP_OFFSET && ShowTags) printf("%s Interop Dir:",IndentString);
{
unsigned char * SubdirStart;
SubdirStart = OffsetBase + Get32u(ValuePtr);
if (SubdirStart < OffsetBase || SubdirStart > OffsetBase+ExifLength){
ErrNonfatal("Illegal exif or interop ofset directory link",0,0);
}else{
ProcessExifDir(SubdirStart, OffsetBase, ExifLength, NestingLevel+1);
}
continue;
}
break;
case TAG_GPSINFO:
if (ShowTags) printf("%s GPS info dir:",IndentString);
{
unsigned char * SubdirStart;
SubdirStart = OffsetBase + Get32u(ValuePtr);
if (SubdirStart < OffsetBase || SubdirStart > OffsetBase+ExifLength){
ErrNonfatal("Illegal GPS directory link",0,0);
}else{
ProcessGpsInfo(SubdirStart, ByteCount, OffsetBase, ExifLength);
}
continue;
}
break;
case TAG_FOCALLENGTH_35MM:
// The focal length equivalent 35 mm is a 2.2 tag (defined as of April 2002)
// if its present, use it to compute equivalent focal length instead of
// computing it from sensor geometry and actual focal length.
ImageInfo.FocalLength35mmEquiv = (unsigned)ConvertAnyFormat(ValuePtr, Format);
break;
case TAG_DISTANCE_RANGE:
// Three possible standard values:
// 1 = macro, 2 = close, 3 = distant
ImageInfo.DistanceRange = (int)ConvertAnyFormat(ValuePtr, Format);
break;
}
}
{
// 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!
unsigned char * SubdirStart;
unsigned Offset;
if (DIR_ENTRY_ADDR(DirStart, NumDirEntries) + 4 <= OffsetBase+ExifLength){
printf("DirStart %p offset from dirstart %d", DirStart, 2+12*NumDirEntries);
Offset = Get32u(DirStart+2+12*NumDirEntries);
if (Offset){
SubdirStart = OffsetBase + Offset;
if (SubdirStart > OffsetBase+ExifLength || SubdirStart < OffsetBase){
printf("SubdirStart %p OffsetBase %p ExifLength %d Offset %d",
SubdirStart, OffsetBase, ExifLength, Offset);
if (SubdirStart > OffsetBase && SubdirStart < OffsetBase+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 (ShowTags) printf("Thumbnail removed with Jhead 1.3 or earlier\n");
}else{
ErrNonfatal("Illegal subdirectory link",0,0);
}
}else{
if (SubdirStart <= OffsetBase+ExifLength){
if (ShowTags) printf("%s Continued directory ",IndentString);
ProcessExifDir(SubdirStart, OffsetBase, ExifLength, NestingLevel+1);
}
}
if (Offset > ImageInfo.LargestExifOffset){
ImageInfo.LargestExifOffset = Offset;
}
}
}else{
// The exif header ends before the last next directory pointer.
}
}
if (ThumbnailOffset){
ImageInfo.ThumbnailAtEnd = FALSE;
if (DumpExifMap){
printf("Map: %05d-%05d: Thumbnail\n",ThumbnailOffset, ThumbnailOffset+ThumbnailSize);
}
if (ThumbnailOffset <= ExifLength){
if (ThumbnailSize > ExifLength-ThumbnailOffset){
// If thumbnail extends past exif header, only save the part that
// actually exists. Canon's EOS viewer utility will do this - the
// thumbnail extracts ok with this hack.
ThumbnailSize = ExifLength-ThumbnailOffset;
if (ShowTags) printf("Thumbnail incorrectly placed in header\n");
}
// The thumbnail pointer appears to be valid. Store it.
ImageInfo.ThumbnailOffset = ThumbnailOffset;
ImageInfo.ThumbnailSize = ThumbnailSize;
if (ShowTags){
printf("Thumbnail size: %d bytes\n",ThumbnailSize);
}
}
}
printf("returning from ProcessExifDir");
}
//--------------------------------------------------------------------------
// Process a EXIF marker
// Describes all the drivel that most digital cameras include...
//--------------------------------------------------------------------------
void process_EXIF (unsigned char * ExifSection, unsigned int length)
{
int FirstOffset;
FocalplaneXRes = 0;
FocalplaneUnits = 0;
ExifImageWidth = 0;
NumOrientations = 0;
if (ShowTags){
printf("Exif header %d bytes long\n",length);
}
{ // Check the EXIF header component
static uchar ExifHeader[] = "Exif\0\0";
if (memcmp(ExifSection+2, ExifHeader,6)){
ErrNonfatal("Incorrect Exif header",0,0);
return;
}
}
if (memcmp(ExifSection+8,"II",2) == 0){
if (ShowTags) printf("Exif section in Intel order\n");
MotorolaOrder = 0;
}else{
if (memcmp(ExifSection+8,"MM",2) == 0){
if (ShowTags) printf("Exif section in Motorola order\n");
MotorolaOrder = 1;
}else{
ErrNonfatal("Invalid Exif alignment marker.",0,0);
return;
}
}
// Check the next value for correctness.
if (Get16u(ExifSection+10) != 0x2a){
ErrNonfatal("Invalid Exif start (1)",0,0);
return;
}
FirstOffset = Get32u(ExifSection+12);
if (FirstOffset < 8 || FirstOffset > 16){
// Usually set to 8, but other values valid too.
ErrNonfatal("Suspicious offset of first IFD value",0,0);
return;
}
DirWithThumbnailPtrs = NULL;
// First directory starts 16 bytes in. All offset are relative to 8 bytes in.
ProcessExifDir(ExifSection+8+FirstOffset, ExifSection+8, length-8, 0);
ImageInfo.ThumbnailAtEnd = ImageInfo.ThumbnailOffset >= ImageInfo.LargestExifOffset ? TRUE : FALSE;
#ifdef SUPERDEBUG
printf("Thumbnail %s end", (ImageInfo.ThumbnailAtEnd ? "at" : "NOT at"));
#endif
if (DumpExifMap){
unsigned a,b;
printf("Map: %05d- End of exif\n",length-8);
// for (a=0;a<length-8;a+= 10){
// printf("Map: %05d ",a);
// for (b=0;b<10;b++) printf(" %02x",*(ExifSection+8+a+b));
// printf("\n");
// }
for (a = 0; a < length - 8; ++a) {
unsigned char c = *(ExifSection+8+a);
unsigned pc = isprint(c) ? c : ' ';
printf("Map: %4d %02x %c", a, c, pc);
}
}
// Compute the CCD width, in millimeters.
if (FocalplaneXRes != 0){
// Note: With some cameras, its not possible to compute this correctly because
// they don't adjust the indicated focal plane resolution units when using less
// than maximum resolution, so the CCDWidth value comes out too small. Nothing
// that Jhad can do about it - its a camera problem.
ImageInfo.CCDWidth = (float)(ExifImageWidth * FocalplaneUnits / FocalplaneXRes);
if (ImageInfo.FocalLength.num != 0 && ImageInfo.FocalLength.denom != 0
&& ImageInfo.FocalLength35mmEquiv == 0){
// Compute 35 mm equivalent focal length based on sensor geometry if we haven't
// already got it explicitly from a tag.
ImageInfo.FocalLength35mmEquiv = (int)(
(double)ImageInfo.FocalLength.num / ImageInfo.FocalLength.denom
/ ImageInfo.CCDWidth * 36 + 0.5);
}
}
}
static const TagTable_t* TagToTagTableEntry(unsigned short tag)
{
unsigned int i;
for (i = 0; i < TAG_TABLE_SIZE; i++) {
if (TagTable[i].Tag == tag) {
printf("found tag %d", tag);
int format = TagTable[i].Format;
if (format == 0) {
printf("tag %s format not defined ***** YOU MUST ADD THE FORMAT TO THE TagTable in exif.c!!!!", TagTable[i].Desc);
return NULL;
}
return &TagTable[i];
}
}
printf("tag %d NOT FOUND", tag);
return NULL;
}
static void writeExifTagAndData(int tag,
int format,
long components,
long value,
int valueInString,
char* Buffer,
int* DirIndex,
int* DataWriteIndex) {
void* componentsPosition = NULL; // for saving component position
Put16u(Buffer+ (*DirIndex), tag); // Tag
Put16u(Buffer+(*DirIndex) + 2, format); // Format
if (format == FMT_STRING && components == -1) {
components = strlen((char*)value) + 1; // account for null terminator
if (components & 1) ++components; // no odd lengths
} else if (format == FMT_SSHORT && components == -1) {
// jhead only supports reading one SSHORT anyway
components = 1;
}
if (format == FMT_UNDEFINED && components == -1) {
// check if this UNDEFINED format is actually ASCII (as it usually is)
// if so, we can calculate the size
if(memcmp((char*)value, ExifAsciiPrefix, sizeof(ExifAsciiPrefix)) == 0) {
components = sizeof(ExifAsciiPrefix) +
strlen((char*)value + sizeof(ExifAsciiPrefix)) + 1;
if (components & 1) ++components; // no odd lengths
}
}
Put32u(Buffer+(*DirIndex) + 4, components); // Components
componentsPosition = Buffer+(*DirIndex) + 4; // components # can change for lists
printf("# components: %ld", components);
if (format == FMT_STRING) {
// short strings can fit right in the long, otherwise have to
// go in the data area
if (components <= 4) {
strcpy(Buffer+(*DirIndex) + 8, (char*)value);
} else {
Put32u(Buffer+(*DirIndex) + 8, (*DataWriteIndex)-8); // Pointer
printf("copying value %s to %d", (char*)value, (*DataWriteIndex));
strncpy(Buffer+(*DataWriteIndex), (char*)value, components);
(*DataWriteIndex) += components;
}
} else if ((format == FMT_UNDEFINED) &&
(memcmp((char*)value, ExifAsciiPrefix, sizeof(ExifAsciiPrefix)) == 0)) {
// short strings can fit right in the long, otherwise have to
// go in the data area
if (components <= 4) {
memcpy(Buffer+(*DirIndex) + 8, (char*)value, components);
} else {
Put32u(Buffer+(*DirIndex) + 8, (*DataWriteIndex)-8); // Pointer
printf("copying %s to %d", (char*)value + sizeof(ExifAsciiPrefix), (*DataWriteIndex));
memcpy(Buffer+(*DataWriteIndex), (char*)value, components);
(*DataWriteIndex) += components;
}
} else if (!valueInString) {
Put32u(Buffer+(*DirIndex) + 8, value); // Value
} else {
Put32u(Buffer+(*DirIndex) + 8, (*DataWriteIndex)-8); // Pointer
// Usually the separator is ',', but sometimes ':' is used, like
// TAG_GPS_TIMESTAMP.
char* curElement = strtok((char*)value, ",:");
int i;
// (components == -1) Need to handle lists with unknown length too
for (i = 0; ((i < components) || (components == -1)) && curElement != NULL; i++) {
#ifdef SUPERDEBUG
printf("processing component %s format %s", curElement, formatStr(format));
#endif
// elements are separated by commas
if (format == FMT_URATIONAL) {
unsigned int numerator;
unsigned int denominator;
char* separator = strchr(curElement, '/');
if (separator) {
numerator = atoi(curElement);
denominator = atoi(separator + 1);
} else {
double value = atof(curElement);
ConvertDoubleToURational(value, &numerator, &denominator);
}
Put32u(Buffer+(*DataWriteIndex), numerator);
Put32u(Buffer+(*DataWriteIndex) + 4, denominator);
(*DataWriteIndex) += 8;
} else if (format == FMT_SRATIONAL) {
int numerator;
int denominator;
char* separator = strchr(curElement, '/');
if (separator) {
numerator = atoi(curElement);
denominator = atoi(separator + 1);
} else {
double value = atof(curElement);
ConvertDoubleToSRational(value, &numerator, &denominator);
}
Put32u(Buffer+(*DataWriteIndex), numerator);
Put32u(Buffer+(*DataWriteIndex) + 4, denominator);
(*DataWriteIndex) += 8;
} else if ((components == -1) && ((format == FMT_USHORT) || (format == FMT_SSHORT))) {
// variable components need to go into data write area
value = atoi(curElement);
Put16u(Buffer+(*DataWriteIndex), value);
(*DataWriteIndex) += 4;
} else {
// TODO: doesn't handle multiple components yet -- if more than one, have to put in data write area.
value = atoi(curElement);
Put32u(Buffer+(*DirIndex) + 8, value); // Value
}
curElement = strtok(NULL, ",:");
}
if (components == -1) Put32u(componentsPosition, i); // update component # for unknowns
}
(*DirIndex) += 12;
}
#ifdef SUPERDEBUG
char* formatStr(int format) {
switch (format) {
case FMT_BYTE: return "FMT_BYTE"; break;
case FMT_STRING: return "FMT_STRING"; break;
case FMT_USHORT: return "FMT_USHORT"; break;
case FMT_ULONG: return "FMT_ULONG"; break;
case FMT_URATIONAL: return "FMT_URATIONAL"; break;
case FMT_SBYTE: return "FMT_SBYTE"; break;
case FMT_UNDEFINED: return "FMT_UNDEFINED"; break;
case FMT_SSHORT: return "FMT_SSHORT"; break;
case FMT_SLONG: return "FMT_SLONG"; break;
case FMT_SRATIONAL: return "FMT_SRATIONAL"; break;
case FMT_SINGLE: return "FMT_SINGLE"; break;
case FMT_DOUBLE: return "FMT_SINGLE"; break;
default: return "UNKNOWN";
}
}
#endif
//--------------------------------------------------------------------------
// Create minimal exif header - just date and thumbnail pointers,
// so that date and thumbnail may be filled later.
//--------------------------------------------------------------------------
static void create_EXIF_internal(ExifElement_t* elements, int exifTagCount, int gpsTagCount, int hasDateTimeTag, char* Buffer)
{
unsigned short NumEntries;
int DataWriteIndex;
int DirIndex;
int DirExifLink = 0;
#ifdef SUPERDEBUG
ALOGE("create_EXIF %d exif elements, %d gps elements", exifTagCount, gpsTagCount);
#endif
MotorolaOrder = 0;
memcpy(Buffer+2, "Exif\0\0II",8);
Put16u(Buffer+10, 0x2a);
DataWriteIndex = 16;
Put32u(Buffer+12, DataWriteIndex-8); // first IFD offset. Means start 16 bytes in.
{
DirIndex = DataWriteIndex;
NumEntries = 1 + exifTagCount; // the extra is the thumbnail
if (gpsTagCount) {
++NumEntries; // allow for the GPS info tag
}
if (!hasDateTimeTag) {
// We have to write extra date time tag. The entry number should be
// adjusted.
++NumEntries;
}
DataWriteIndex += 2 + NumEntries*12 + 4;
Put16u(Buffer+DirIndex, NumEntries); // Number of entries
DirIndex += 2;
// Entries go here...
if (!hasDateTimeTag) {
// Date/time entry
char* dateTime = NULL;
char dateBuf[20];
if (ImageInfo.numDateTimeTags) {
// If we had a pre-existing exif header, use time from that.
dateTime = ImageInfo.DateTime;
} else {
// Oterwise, use the file's timestamp.
FileTimeAsString(dateBuf);
dateTime = dateBuf;
}
writeExifTagAndData(TAG_DATETIME,
FMT_STRING,
20,
(long)(char*)dateBuf,
FALSE,
Buffer,
&DirIndex,
&DataWriteIndex);
}
if (exifTagCount > 0) {
int i;
for (i = 0; i < exifTagCount + gpsTagCount; i++) {
if (elements[i].GpsTag) {
continue;
}
const TagTable_t* entry = TagToTagTableEntry(elements[i].Tag);
if (entry == NULL) {
continue;
}
#ifdef SUPERDEBUG
ALOGE("create_EXIF saving tag %x value \"%s\"",elements[i].Tag, elements[i].Value);
#endif
writeExifTagAndData(elements[i].Tag,
entry->Format,
entry->DataLength,
(long)elements[i].Value,
TRUE,
Buffer,
&DirIndex,
&DataWriteIndex);
}
if (gpsTagCount) {
// Link to gps dir entry
writeExifTagAndData(TAG_GPSINFO,
FMT_ULONG,
1,
DataWriteIndex-8,
FALSE,
Buffer,
&DirIndex,
&DataWriteIndex);
}
// Link to exif dir entry
int exifDirPtr = DataWriteIndex-8;
if (gpsTagCount) {
exifDirPtr += 2 + gpsTagCount*12 + 4;
}
DirExifLink = DirIndex;
writeExifTagAndData(TAG_EXIF_OFFSET,
FMT_ULONG,
1,
exifDirPtr,
FALSE,
Buffer,
&DirIndex,
&DataWriteIndex);
}
// End of directory - contains optional link to continued directory.
Put32u(Buffer+DirIndex, 0);
printf("Ending Exif section DirIndex = %d DataWriteIndex %d", DirIndex, DataWriteIndex);
}
// GPS Section
if (gpsTagCount) {
DirIndex = DataWriteIndex;
printf("Starting GPS section DirIndex = %d", DirIndex);
NumEntries = gpsTagCount;
DataWriteIndex += 2 + NumEntries*12 + 4;
Put16u(Buffer+DirIndex, NumEntries); // Number of entries
DirIndex += 2;
{
int i;
for (i = 0; i < exifTagCount + gpsTagCount; i++) {
if (!elements[i].GpsTag) {
continue;
}
const TagTable_t* entry = GpsTagToTagTableEntry(elements[i].Tag);
if (entry == NULL) {
continue;
}
#ifdef SUPERDEBUG
ALOGE("create_EXIF saving GPS tag %x value \"%s\"",elements[i].Tag, elements[i].Value);
#endif
writeExifTagAndData(elements[i].Tag,
entry->Format,
entry->DataLength,
(long)elements[i].Value,
TRUE,
Buffer,
&DirIndex,
&DataWriteIndex);
}
}
// End of directory - contains optional link to continued directory.
Put32u(Buffer+DirIndex, 0);
printf("Ending GPS section DirIndex = %d DataWriteIndex %d", DirIndex, DataWriteIndex);
}
{
// Overwriting TAG_EXIF_OFFSET which links to this directory
Put32u(Buffer+DirExifLink+8, DataWriteIndex-8);
printf("Starting Thumbnail section DirIndex = %d", DirIndex);
DirIndex = DataWriteIndex;
NumEntries = 2;
DataWriteIndex += 2 + NumEntries*12 + 4;
Put16u(Buffer+DirIndex, NumEntries); // Number of entries
DirIndex += 2;
{
// Link to exif dir entry
writeExifTagAndData(TAG_THUMBNAIL_OFFSET,
FMT_ULONG,
1,
DataWriteIndex-8,
FALSE,
Buffer,
&DirIndex,
&DataWriteIndex);
}
{
// Link to exif dir entry
writeExifTagAndData(TAG_THUMBNAIL_LENGTH,
FMT_ULONG,
1,
0,
FALSE,
Buffer,
&DirIndex,
&DataWriteIndex);
}
// End of directory - contains optional link to continued directory.
Put32u(Buffer+DirIndex, 0);
printf("Ending Thumbnail section DirIndex = %d DataWriteIndex %d", DirIndex, DataWriteIndex);
}
Buffer[0] = (unsigned char)(DataWriteIndex >> 8);
Buffer[1] = (unsigned char)DataWriteIndex;
// Remove old exif section, if there was one.
RemoveSectionType(M_EXIF);
{
// Sections need malloced buffers, so do that now, especially because
// we now know how big it needs to be allocated.
unsigned char * NewBuf = malloc(DataWriteIndex);
if (NewBuf == NULL){
ErrFatal("Could not allocate memory");
}
memcpy(NewBuf, Buffer, DataWriteIndex);
CreateSection(M_EXIF, NewBuf, DataWriteIndex);
// Re-parse new exif section, now that its in place
// otherwise, we risk touching data that has already been freed.
process_EXIF(NewBuf, DataWriteIndex);
}
}
void create_EXIF(ExifElement_t* elements, int exifTagCount, int gpsTagCount, int hasDateTimeTag)
{
// It is hard to calculate exact necessary size for editing the exif
// header dynamically, so we are using the maximum size of EXIF, 64K
const int EXIF_MAX_SIZE = 1024*64;
char* Buffer = malloc(EXIF_MAX_SIZE);
if (Buffer != NULL) {
create_EXIF_internal(elements, exifTagCount, gpsTagCount, hasDateTimeTag, Buffer);
free(Buffer);
} else {
ErrFatal("Could not allocate memory");
}
}
//--------------------------------------------------------------------------
// Cler the rotation tag in the exif header to 1.
//--------------------------------------------------------------------------
const char * ClearOrientation(void)
{
int a;
if (NumOrientations == 0) return NULL;
for (a=0;a<NumOrientations;a++){
switch(OrientationNumFormat[a]){
case FMT_SBYTE:
case FMT_BYTE:
*(uchar *)(OrientationPtr[a]) = 1;
break;
case FMT_USHORT:
Put16u(OrientationPtr[a], 1);
break;
case FMT_ULONG:
case FMT_SLONG:
memset(OrientationPtr, 0, 4);
// Can't be bothered to write generic Put32 if I only use it once.
if (MotorolaOrder){
((uchar *)OrientationPtr[a])[3] = 1;
}else{
((uchar *)OrientationPtr[a])[0] = 1;
}
break;
default:
return NULL;
}
}
return OrientTab[ImageInfo.Orientation];
}
//--------------------------------------------------------------------------
// Remove thumbnail out of the exif image.
//--------------------------------------------------------------------------
int RemoveThumbnail(unsigned char * ExifSection)
{
if (!DirWithThumbnailPtrs ||
ImageInfo.ThumbnailOffset == 0 ||
ImageInfo.ThumbnailSize == 0){
// No thumbnail, or already deleted it.
return 0;
}
if (ImageInfo.ThumbnailAtEnd == FALSE){
ErrNonfatal("Thumbnail is not at end of header, can't chop it off", 0, 0);
return 0;
}
{
int de;
int NumDirEntries;
NumDirEntries = Get16u(DirWithThumbnailPtrs);
for (de=0;de<NumDirEntries;de++){
int Tag;
unsigned char * DirEntry;
DirEntry = DIR_ENTRY_ADDR(DirWithThumbnailPtrs, de);
Tag = Get16u(DirEntry);
if (Tag == TAG_THUMBNAIL_LENGTH){
// Set length to zero.
if (Get16u(DirEntry+2) != FMT_ULONG){
// non standard format encoding. Can't do it.
ErrNonfatal("Can't remove thumbnail", 0, 0);
return 0;
}
Put32u(DirEntry+8, 0);
}
}
}
// This is how far the non thumbnail data went.
return ImageInfo.ThumbnailOffset+8;
}
//--------------------------------------------------------------------------
// Convert exif time to Unix time structure
//--------------------------------------------------------------------------
int Exif2tm(struct tm * timeptr, char * ExifTime)
{
int a;
timeptr->tm_wday = -1;
// Check for format: YYYY:MM:DD HH:MM:SS format.
// Date and time normally separated by a space, but also seen a ':' there, so
// skip the middle space with '%*c' so it can be any character.
a = sscanf(ExifTime, "%d%*c%d%*c%d%*c%d:%d:%d",
&timeptr->tm_year, &timeptr->tm_mon, &timeptr->tm_mday,
&timeptr->tm_hour, &timeptr->tm_min, &timeptr->tm_sec);
if (a == 6){
timeptr->tm_isdst = -1;
timeptr->tm_mon -= 1; // Adjust for unix zero-based months
timeptr->tm_year -= 1900; // Adjust for year starting at 1900
return TRUE; // worked.
}
return FALSE; // Wasn't in Exif date format.
}
//--------------------------------------------------------------------------
// Show the collected image info, displaying camera F-stop and shutter speed
// in a consistent and legible fashion.
//--------------------------------------------------------------------------
void ShowImageInfo(int ShowFileInfo)
{
if (ShowFileInfo){
printf("File name : %s\n",ImageInfo.FileName);
printf("File size : %d bytes\n",ImageInfo.FileSize);
{
char Temp[20];
FileTimeAsString(Temp);
printf("File date : %s\n",Temp);
}
}
if (ImageInfo.CameraMake[0]){
printf("Camera make : %s\n",ImageInfo.CameraMake);
printf("Camera model : %s\n",ImageInfo.CameraModel);
}
if (ImageInfo.DateTime[0]){
printf("Date/Time : %s\n",ImageInfo.DateTime);
}
printf("Resolution : %d x %d\n",ImageInfo.Width, ImageInfo.Height);
if (ImageInfo.Orientation > 1){
// Only print orientation if one was supplied, and if its not 1 (normal orientation)
printf("Orientation : %s\n", OrientTab[ImageInfo.Orientation]);
}
if (ImageInfo.IsColor == 0){
printf("Color/bw : Black and white\n");
}
if (ImageInfo.FlashUsed >= 0){
if (ImageInfo.FlashUsed & 1){
printf("Flash used : Yes");
switch (ImageInfo.FlashUsed){
case 0x5: printf(" (Strobe light not detected)"); break;
case 0x7: printf(" (Strobe light detected) "); break;
case 0x9: printf(" (manual)"); break;
case 0xd: printf(" (manual, return light not detected)"); break;
case 0xf: printf(" (manual, return light detected)"); break;
case 0x19:printf(" (auto)"); break;
case 0x1d:printf(" (auto, return light not detected)"); break;
case 0x1f:printf(" (auto, return light detected)"); break;
case 0x41:printf(" (red eye reduction mode)"); break;
case 0x45:printf(" (red eye reduction mode return light not detected)"); break;
case 0x47:printf(" (red eye reduction mode return light detected)"); break;
case 0x49:printf(" (manual, red eye reduction mode)"); break;
case 0x4d:printf(" (manual, red eye reduction mode, return light not detected)"); break;
case 0x4f:printf(" (red eye reduction mode, return light detected)"); break;
case 0x59:printf(" (auto, red eye reduction mode)"); break;
case 0x5d:printf(" (auto, red eye reduction mode, return light not detected)"); break;
case 0x5f:printf(" (auto, red eye reduction mode, return light detected)"); break;
}
}else{
printf("Flash used : No");
switch (ImageInfo.FlashUsed){
case 0x18:printf(" (auto)"); break;
}
}
printf("\n");
}
if (ImageInfo.FocalLength.num != 0 && ImageInfo.FocalLength.denom != 0) {
printf("Focal length : %4.1fmm",(double)ImageInfo.FocalLength.num / ImageInfo.FocalLength.denom);
if (ImageInfo.FocalLength35mmEquiv){
printf(" (35mm equivalent: %dmm)", ImageInfo.FocalLength35mmEquiv);
}
printf("\n");
}
if (ImageInfo.DigitalZoomRatio > 1){
// Digital zoom used. Shame on you!
printf("Digital Zoom : %1.3fx\n", (double)ImageInfo.DigitalZoomRatio);
}
if (ImageInfo.CCDWidth){
printf("CCD width : %4.2fmm\n",(double)ImageInfo.CCDWidth);
}
if (ImageInfo.ExposureTime){
if (ImageInfo.ExposureTime < 0.010){
printf("Exposure time: %6.4f s ",(double)ImageInfo.ExposureTime);
}else{
printf("Exposure time: %5.3f s ",(double)ImageInfo.ExposureTime);
}
if (ImageInfo.ExposureTime <= 0.5){
printf(" (1/%d)",(int)(0.5 + 1/ImageInfo.ExposureTime));
}
printf("\n");
}
if (ImageInfo.ApertureFNumber){
printf("Aperture : f/%3.3f\n",(double)ImageInfo.ApertureFNumber);
}
if (ImageInfo.Distance){
if (ImageInfo.Distance < 0){
printf("Focus dist. : Infinite\n");
}else{
printf("Focus dist. : %4.2fm\n",(double)ImageInfo.Distance);
}
}
if (ImageInfo.ISOequivalent){
printf("ISO equiv. : %2d\n",(int)ImageInfo.ISOequivalent);
}
if (ImageInfo.ExposureBias){
// If exposure bias was specified, but set to zero, presumably its no bias at all,
// so only show it if its nonzero.
printf("Exposure bias: %4.2f\n",(double)ImageInfo.ExposureBias);
}
switch(ImageInfo.Whitebalance) {
case 1:
printf("Whitebalance : Manual\n");
break;
case 0:
printf("Whitebalance : Auto\n");
break;
}
//Quercus: 17-1-2004 Added LightSource, some cams return this, whitebalance or both
switch(ImageInfo.LightSource) {
case 1:
printf("Light Source : Daylight\n");
break;
case 2:
printf("Light Source : Fluorescent\n");
break;
case 3:
printf("Light Source : Incandescent\n");
break;
case 4:
printf("Light Source : Flash\n");
break;
case 9:
printf("Light Source : Fine weather\n");
break;
case 11:
printf("Light Source : Shade\n");
break;
default:; //Quercus: 17-1-2004 There are many more modes for this, check Exif2.2 specs
// If it just says 'unknown' or we don't know it, then
// don't bother showing it - it doesn't add any useful information.
}
if (ImageInfo.MeteringMode){ // 05-jan-2001 vcs
switch(ImageInfo.MeteringMode) {
case 2:
printf("Metering Mode: center weight\n");
break;
case 3:
printf("Metering Mode: spot\n");
break;
case 5:
printf("Metering Mode: matrix\n");
break;
}
}
if (ImageInfo.ExposureProgram){ // 05-jan-2001 vcs
switch(ImageInfo.ExposureProgram) {
case 1:
printf("Exposure : Manual\n");
break;
case 2:
printf("Exposure : program (auto)\n");
break;
case 3:
printf("Exposure : aperture priority (semi-auto)\n");
break;
case 4:
printf("Exposure : shutter priority (semi-auto)\n");
break;
case 5:
printf("Exposure : Creative Program (based towards depth of field)\n");
break;
case 6:
printf("Exposure : Action program (based towards fast shutter speed)\n");
break;
case 7:
printf("Exposure : Portrait Mode\n");
break;
case 8:
printf("Exposure : LandscapeMode \n");
break;
default:
break;
}
}
switch(ImageInfo.ExposureMode){
case 0: // Automatic (not worth cluttering up output for)
break;
case 1: printf("Exposure Mode: Manual\n");
break;
case 2: printf("Exposure Mode: Auto bracketing\n");
break;
}
if (ImageInfo.DistanceRange) {
printf("Focus range : ");
switch(ImageInfo.DistanceRange) {
case 1:
printf("macro");
break;
case 2:
printf("close");
break;
case 3:
printf("distant");
break;
}
printf("\n");
}
if (ImageInfo.Process != M_SOF0){
// don't show it if its the plain old boring 'baseline' process, but do
// show it if its something else, like 'progressive' (used on web sometimes)
int a;
for (a=0;;a++){
if (a >= (int)PROCESS_TABLE_SIZE){
// ran off the end of the table.
printf("Jpeg process : Unknown\n");
break;
}
if (ProcessTable[a].Tag == ImageInfo.Process){
printf("Jpeg process : %s\n",ProcessTable[a].Desc);
break;
}
}
}
if (ImageInfo.GpsInfoPresent){
printf("GPS Latitude : %s\n",ImageInfo.GpsLat);
printf("GPS Longitude: %s\n",ImageInfo.GpsLong);
if (ImageInfo.GpsAlt[0]) printf("GPS Altitude : %s\n",ImageInfo.GpsAlt);
}
// Print the comment. Print 'Comment:' for each new line of comment.
if (ImageInfo.Comments[0]){
int a,c;
printf("Comment : ");
if (!ImageInfo.CommentWidchars){
for (a=0;a<MAX_COMMENT_SIZE;a++){
c = ImageInfo.Comments[a];
if (c == '\0') break;
if (c == '\n'){
// Do not start a new line if the string ends with a carriage return.
if (ImageInfo.Comments[a+1] != '\0'){
printf("\nComment : ");
}else{
printf("\n");
}
}else{
putchar(c);
}
}
printf("\n");
}else{
printf("%.*ls\n", ImageInfo.CommentWidchars, (wchar_t *)ImageInfo.Comments);
}
}
if (ImageInfo.ThumbnailOffset){
printf("Map: %05d-%05d: Thumbnail\n",ImageInfo.ThumbnailOffset, ImageInfo.ThumbnailOffset+ImageInfo.ThumbnailSize);
} else {
printf("NO thumbnail");
}
}
//--------------------------------------------------------------------------
// Summarize highlights of image info on one line (suitable for grep-ing)
//--------------------------------------------------------------------------
void ShowConciseImageInfo(void)
{
printf("\"%s\"",ImageInfo.FileName);
printf(" %dx%d",ImageInfo.Width, ImageInfo.Height);
if (ImageInfo.ExposureTime){
if (ImageInfo.ExposureTime <= 0.5){
printf(" (1/%d)",(int)(0.5 + 1/ImageInfo.ExposureTime));
}else{
printf(" (%1.1f)",ImageInfo.ExposureTime);
}
}
if (ImageInfo.ApertureFNumber){
printf(" f/%3.1f",(double)ImageInfo.ApertureFNumber);
}
if (ImageInfo.FocalLength35mmEquiv){
printf(" f(35)=%dmm",ImageInfo.FocalLength35mmEquiv);
}
if (ImageInfo.FlashUsed >= 0 && ImageInfo.FlashUsed & 1){
printf(" (flash)");
}
if (ImageInfo.IsColor == 0){
printf(" (bw)");
}
printf("\n");
}