//-------------------------------------------------------------------------- // Program to pull the information out of various types of EFIF digital // camera files and show it in a reasonably consistent way // // This module parses the very complicated exif structures. // // Matthias Wandel, Dec 1999 - August 2000 //-------------------------------------------------------------------------- #include "exif.h" static unsigned char * LastExifRefd; static int ExifSettingsLength; static double FocalplaneXRes; static double FocalplaneUnits; static int ExifImageWidth; static int MotorolaOrder = 0; static int SectionsRead; //static int HaveAll; //-------------------------------------------------------------------------- // Table of Jpeg encoding process names #define M_SOF0 0xC0 // Start Of Frame N #define M_SOF1 0xC1 // N indicates which compression process #define M_SOF2 0xC2 // Only SOF0-SOF2 are now in common use #define M_SOF3 0xC3 #define M_SOF5 0xC5 // NB: codes C4 and CC are NOT SOF markers #define M_SOF6 0xC6 #define M_SOF7 0xC7 #define M_SOF9 0xC9 #define M_SOF10 0xCA #define M_SOF11 0xCB #define M_SOF13 0xCD #define M_SOF14 0xCE #define M_SOF15 0xCF #define M_SOI 0xD8 // Start Of Image (beginning of datastream) #define M_EOI 0xD9 // End Of Image (end of datastream) #define M_SOS 0xDA // Start Of Scan (begins compressed data) #define M_JFIF 0xE0 // Jfif marker #define M_EXIF 0xE1 // Exif marker #define M_COM 0xFE // COMment TagTable_t ProcessTable[] = { { M_SOF0, (char *)"Baseline"}, { M_SOF1, (char *)"Extended sequential"}, { M_SOF2, (char *)"Progressive"}, { M_SOF3, (char *)"Lossless"}, { M_SOF5, (char *)"Differential sequential"}, { M_SOF6, (char *)"Differential progressive"}, { M_SOF7, (char *)"Differential lossless"}, { M_SOF9, (char *)"Extended sequential, arithmetic coding"}, { M_SOF10, (char *)"Progressive, arithmetic coding"}, { M_SOF11, (char *)"Lossless, arithmetic coding"}, { M_SOF13, (char *)"Differential sequential, arithmetic coding"}, { M_SOF14, (char *)"Differential progressive, arithmetic coding"}, { M_SOF15, (char *)"Differential lossless, arithmetic coding"}, { 0, (char *)"Unknown"} }; //-------------------------------------------------------------------------- // Describes format descriptor static int BytesPerFormat[] = {0,1,1,2,4,8,1,1,2,4,8,4,8}; #define NUM_FORMATS 12 #define FMT_BYTE 1 #define FMT_STRING 2 #define FMT_USHORT 3 #define FMT_ULONG 4 #define FMT_URATIONAL 5 #define FMT_SBYTE 6 #define FMT_UNDEFINED 7 #define FMT_SSHORT 8 #define FMT_SLONG 9 #define FMT_SRATIONAL 10 #define FMT_SINGLE 11 #define FMT_DOUBLE 12 //-------------------------------------------------------------------------- // Describes tag values #define TAG_EXIF_OFFSET 0x8769 #define TAG_INTEROP_OFFSET 0xa005 #define TAG_MAKE 0x010F #define TAG_MODEL 0x0110 #define TAG_ORIENTATION 0x0112 #define TAG_EXPOSURETIME 0x829A #define TAG_FNUMBER 0x829D #define TAG_SHUTTERSPEED 0x9201 #define TAG_APERTURE 0x9202 #define TAG_MAXAPERTURE 0x9205 #define TAG_FOCALLENGTH 0x920A #define TAG_DATETIME_ORIGINAL 0x9003 #define TAG_USERCOMMENT 0x9286 #define TAG_SUBJECT_DISTANCE 0x9206 #define TAG_FLASH 0x9209 #define TAG_FOCALPLANEXRES 0xa20E #define TAG_FOCALPLANEUNITS 0xa210 #define TAG_EXIF_IMAGEWIDTH 0xA002 #define TAG_EXIF_IMAGELENGTH 0xA003 // the following is added 05-jan-2001 vcs #define TAG_EXPOSURE_BIAS 0x9204 #define TAG_WHITEBALANCE 0x9208 #define TAG_METERING_MODE 0x9207 #define TAG_EXPOSURE_PROGRAM 0x8822 #define TAG_ISO_EQUIVALENT 0x8827 #define TAG_COMPRESSION_LEVEL 0x9102 #define TAG_THUMBNAIL_OFFSET 0x0201 #define TAG_THUMBNAIL_LENGTH 0x0202 /*static TagTable_t TagTable[] = { { 0x100, "ImageWidth"}, { 0x101, "ImageLength"}, { 0x102, "BitsPerSample"}, { 0x103, "Compression"}, { 0x106, "PhotometricInterpretation"}, { 0x10A, "FillOrder"}, { 0x10D, "DocumentName"}, { 0x10E, "ImageDescription"}, { 0x10F, "Make"}, { 0x110, "Model"}, { 0x111, "StripOffsets"}, { 0x112, "Orientation"}, { 0x115, "SamplesPerPixel"}, { 0x116, "RowsPerStrip"}, { 0x117, "StripByteCounts"}, { 0x11A, "XResolution"}, { 0x11B, "YResolution"}, { 0x11C, "PlanarConfiguration"}, { 0x128, "ResolutionUnit"}, { 0x12D, "TransferFunction"}, { 0x131, "Software"}, { 0x132, "DateTime"}, { 0x13B, "Artist"}, { 0x13E, "WhitePoint"}, { 0x13F, "PrimaryChromaticities"}, { 0x156, "TransferRange"}, { 0x200, "JPEGProc"}, { 0x201, "ThumbnailOffset"}, { 0x202, "ThumbnailLength"}, { 0x211, "YCbCrCoefficients"}, { 0x212, "YCbCrSubSampling"}, { 0x213, "YCbCrPositioning"}, { 0x214, "ReferenceBlackWhite"}, { 0x828D, "CFARepeatPatternDim"}, { 0x828E, "CFAPattern"}, { 0x828F, "BatteryLevel"}, { 0x8298, "Copyright"}, { 0x829A, "ExposureTime"}, { 0x829D, "FNumber"}, { 0x83BB, "IPTC/NAA"}, { 0x8769, "ExifOffset"}, { 0x8773, "InterColorProfile"}, { 0x8822, "ExposureProgram"}, { 0x8824, "SpectralSensitivity"}, { 0x8825, "GPSInfo"}, { 0x8827, "ISOSpeedRatings"}, { 0x8828, "OECF"}, { 0x9000, "ExifVersion"}, { 0x9003, "DateTimeOriginal"}, { 0x9004, "DateTimeDigitized"}, { 0x9101, "ComponentsConfiguration"}, { 0x9102, "CompressedBitsPerPixel"}, { 0x9201, "ShutterSpeedValue"}, { 0x9202, "ApertureValue"}, { 0x9203, "BrightnessValue"}, { 0x9204, "ExposureBiasValue"}, { 0x9205, "MaxApertureValue"}, { 0x9206, "SubjectDistance"}, { 0x9207, "MeteringMode"}, { 0x9208, "LightSource"}, { 0x9209, "Flash"}, { 0x920A, "FocalLength"}, { 0x927C, "MakerNote"}, { 0x9286, "UserComment"}, { 0x9290, "SubSecTime"}, { 0x9291, "SubSecTimeOriginal"}, { 0x9292, "SubSecTimeDigitized"}, { 0xA000, "FlashPixVersion"}, { 0xA001, "ColorSpace"}, { 0xA002, "ExifImageWidth"}, { 0xA003, "ExifImageLength"}, { 0xA005, "InteroperabilityOffset"}, { 0xA20B, "FlashEnergy"}, // 0x920B in TIFF/EP { 0xA20C, "SpatialFrequencyResponse"}, // 0x920C - - { 0xA20E, "FocalPlaneXResolution"}, // 0x920E - - { 0xA20F, "FocalPlaneYResolution"}, // 0x920F - - { 0xA210, "FocalPlaneResolutionUnit"}, // 0x9210 - - { 0xA214, "SubjectLocation"}, // 0x9214 - - { 0xA215, "ExposureIndex"}, // 0x9215 - - { 0xA217, "SensingMethod"}, // 0x9217 - - { 0xA300, "FileSource"}, { 0xA301, "SceneType"}, { 0, NULL} } ; */ //-------------------------------------------------------------------------- // Parse the marker stream until SOS or EOI is seen; //-------------------------------------------------------------------------- int MyExifData::ReadJpegSections (QFile & infile, ReadMode_t ReadMode) { int a; a = infile.getch(); if (a != 0xff || infile.getch() != M_SOI){ return false; } for(;SectionsRead < MAX_SECTIONS-1;){ int itemlen; int marker = 0; int ll,lh, got; uchar * Data; for (a=0;a<7;a++){ marker = infile.getch(); if (marker != 0xff) break; if (a >= 6){ kdDebug(7034) << "too many padding bytes\n"; return false; } } if (marker == 0xff){ // 0xff is legal padding, but if we get that many, something's wrong. return(false); } Sections[SectionsRead].Type = marker; // Read the length of the section. lh = infile.getch(); ll = infile.getch(); itemlen = (lh << 8) | ll; if (itemlen < 2){ return(false); } Sections[SectionsRead].Size = itemlen; Data = (uchar *)malloc(itemlen+1); // Add 1 to allow sticking a 0 at the end. if (Data == NULL){ return(false); } Sections[SectionsRead].Data = Data; // Store first two pre-read bytes. Data[0] = (uchar)lh; Data[1] = (uchar)ll; got = infile.readBlock((char*)Data+2, itemlen-2); // Read the whole section. if (got != itemlen-2){ return(false); } SectionsRead += 1; switch(marker){ case M_SOS: // stop before hitting compressed data // If reading entire image is requested, read the rest of the data. if (ReadMode & READ_IMAGE){ int size; size = infile.size()-infile.at(); Data = (uchar *)malloc(size); if (Data == NULL){ return(false); } got = infile.readBlock((char*)Data, size); if (got != size){ return(false); } Sections[SectionsRead].Data = Data; Sections[SectionsRead].Size = size; Sections[SectionsRead].Type = PSEUDO_IMAGE_MARKER; SectionsRead ++; //HaveAll = 1; } return true; case M_EOI: // in case it's a tables-only JPEG stream kdDebug(7034) << "No image in jpeg!\n"; return false; case M_COM: // Comment section // pieczy 2002-02-12 // now the User comment goes to UserComment // so we can store a Comment section also in READ_EXIF mode process_COM(Data, itemlen); break; case M_JFIF: // Regular jpegs always have this tag, exif images have the exif // marker instead, althogh ACDsee will write images with both markers. // this program will re-create this marker on absence of exif marker. // hence no need to keep the copy from the file. free(Sections[--SectionsRead].Data); break; case M_EXIF: // Seen files from some 'U-lead' software with Vivitar scanner // that uses marker 31 for non exif stuff. Thus make sure // it says 'Exif' in the section before treating it as exif. if ((ReadMode & READ_EXIF) && memcmp(Data+2, "Exif", 4) == 0){ if(!process_EXIF((uchar *)Data, itemlen)) return(false); }else{ // Discard this section. free(Sections[--SectionsRead].Data); } break; case M_SOF0: case M_SOF1: case M_SOF2: case M_SOF3: case M_SOF5: case M_SOF6: case M_SOF7: case M_SOF9: case M_SOF10: case M_SOF11: case M_SOF13: case M_SOF14: case M_SOF15: process_SOFn(Data, marker); break; default: // Skip any other sections. //if (ShowTags){ // printf("Jpeg section marker 0x%02x size %d\n",marker, itemlen); //} break; } } return true; } //-------------------------------------------------------------------------- // Discard read data. //-------------------------------------------------------------------------- void MyExifData::DiscardData(void) { int a; for (a=0;a (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{ // Note: Files that had thumbnails trimmed with jhead 1.3 or earlier // might trigger this. return(false); } } if (DirEnd < LastExifRefd) LastExifRefd = DirEnd; } for (de=0;de= NUM_FORMATS) { // (-1) catches illegal zero case as unsigned underflows to positive large. return(false); } 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 //printf("Offset %d bytes %d ExifLen %d\n",OffsetVal, ByteCount, ExifLength); return(false); } ValuePtr = OffsetBase+OffsetVal; }else{ // 4 bytes or less and value is in the dir entry itself ValuePtr = (unsigned char *)DirEntry+8; } if (LastExifRefd < 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. LastExifRefd = ValuePtr+ByteCount; } // Extract useful components of tag switch(Tag){ case TAG_MAKE: MyExifData::CameraMake = QString((char*)ValuePtr); break; case TAG_MODEL: MyExifData::CameraModel = QString((char*)ValuePtr); break; case TAG_ORIENTATION: Orientation = (int)ConvertAnyFormat(ValuePtr, Format); break; case TAG_DATETIME_ORIGINAL: DateTime = QString((char*)ValuePtr); break; case TAG_USERCOMMENT: // Olympus has this 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 if (memcmp(ValuePtr, "ASCII",5) == 0){ for (a=5;a<10;a++){ int c; c = (ValuePtr)[a]; if (c != '\0' && c != ' '){ //strncpy(ImageInfo.Comments, (const char*)(a+ValuePtr), 199); UserComment.sprintf("%s", (const char*)(a+ValuePtr)); break; } } }else{ //strncpy(ImageInfo.Comments, (const char*)ValuePtr, 199); UserComment.sprintf("%s", (const char*)ValuePtr); } break; case TAG_FNUMBER: // Simplest way of expressing aperture, so I trust it the most. // (overwrite previously computd value if there is one) MyExifData::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 (MyExifData::ApertureFNumber == 0){ MyExifData::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. MyExifData::FocalLength = (float)ConvertAnyFormat(ValuePtr, Format); break; case TAG_SUBJECT_DISTANCE: // Inidcates the distacne the autofocus camera is focused to. // Tends to be less accurate as distance increases. MyExifData::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) MyExifData::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 (MyExifData::ExposureTime == 0){ MyExifData::ExposureTime = (float)(1/exp(ConvertAnyFormat(ValuePtr, Format)*log(2))); } break; case TAG_FLASH: if (ConvertAnyFormat(ValuePtr, Format)){ MyExifData::FlashUsed = 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_FOCALPLANEXRES: FocalplaneXRes = ConvertAnyFormat(ValuePtr, Format); break; case TAG_FOCALPLANEUNITS: 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; // milimeter case 5: FocalplaneUnits = .001; break; // micrometer } break; // Remaining cases contributed by: Volker C. Schoech (schoech@gmx.de) case TAG_EXPOSURE_BIAS: MyExifData::ExposureBias = (float)ConvertAnyFormat(ValuePtr, Format); break; case TAG_WHITEBALANCE: MyExifData::Whitebalance = (int)ConvertAnyFormat(ValuePtr, Format); break; case TAG_METERING_MODE: MyExifData::MeteringMode = (int)ConvertAnyFormat(ValuePtr, Format); break; case TAG_EXPOSURE_PROGRAM: MyExifData::ExposureProgram = (int)ConvertAnyFormat(ValuePtr, Format); break; case TAG_ISO_EQUIVALENT: MyExifData::ISOequivalent = (int)ConvertAnyFormat(ValuePtr, Format); if ( MyExifData::ISOequivalent < 50 ) MyExifData::ISOequivalent *= 200; break; case TAG_COMPRESSION_LEVEL: MyExifData::CompressionLevel = (int)ConvertAnyFormat(ValuePtr, Format); break; case TAG_THUMBNAIL_OFFSET: ThumbnailOffset = (unsigned)ConvertAnyFormat(ValuePtr, Format); break; case TAG_THUMBNAIL_LENGTH: ThumbnailSize = (unsigned)ConvertAnyFormat(ValuePtr, Format); break; } if (Tag == TAG_EXIF_OFFSET || Tag == TAG_INTEROP_OFFSET){ unsigned char * SubdirStart; SubdirStart = OffsetBase + Get32u(ValuePtr); if (SubdirStart < OffsetBase || SubdirStart > OffsetBase+ExifLength){ return(false); } if(!ProcessExifDir(SubdirStart, OffsetBase, ExifLength)) return(false); continue; } } { // 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 comitee! unsigned char * SubdirStart; unsigned Offset; if (DIR_ENTRY_ADDR(DirStart, NumDirEntries) + 4 <= OffsetBase+ExifLength){ Offset = Get32u(DIR_ENTRY_ADDR(DirStart, NumDirEntries)); if (Offset){ SubdirStart = OffsetBase + Offset; if (SubdirStart > OffsetBase+ExifLength){ if (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 kdDebug(7034) << "Thumbnail removed with Jhead 1.3 or earlier\n"; }else{ return(false); } }else{ if (SubdirStart <= OffsetBase+ExifLength){ if(!ProcessExifDir(SubdirStart, OffsetBase, ExifLength)) return(false); } } } }else{ // The exif header ends before the last next directory pointer. } } if (ThumbnailSize && ThumbnailOffset){ if (ThumbnailSize + ThumbnailOffset <= ExifLength){ // The thumbnail pointer appears to be valid. Store it. Thumbnail.loadFromData(OffsetBase + ThumbnailOffset, ThumbnailSize, "JPEG"); } } return(true); } //-------------------------------------------------------------------------- // Process a COM marker. We want to leave the bytes unchanged. The // progam that displays this text may decide to remove blanks, convert // newlines, or otherwise modify the text. In particular we want to be // safe for passing utf-8 text. //-------------------------------------------------------------------------- void MyExifData::process_COM (const uchar * Data, int length) { QChar ch; int a; for (a=2;atm_wday = -1; // Check for format: YYYY:MM:DD HH:MM:SS format. a = sscanf(ExifTime, "%d:%d:%d %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. } //-------------------------------------------------------------------------- // Contructor for initialising //-------------------------------------------------------------------------- MyExifData::MyExifData() { MyExifData::Whitebalance = -1; MyExifData::MeteringMode = -1; MyExifData::FlashUsed = -1; Orientation = 0; Height = 0; Width = 0; IsColor = 0; Process = 0; FocalLength = 0; ExposureTime = 0; ApertureFNumber = 0; Distance = 0; CCDWidth = 0; ExposureBias = 0; ExposureProgram = 0; ISOequivalent = 0; CompressionLevel = 0; } //-------------------------------------------------------------------------- // process a EXIF jpeg file //-------------------------------------------------------------------------- bool MyExifData::scan(const QString & path) { int ret; QFile f(path); f.open(IO_ReadOnly); // Scan the JPEG headers. ret = ReadJpegSections(f, READ_EXIF); if (ret == false){ kdDebug(7034) << "Not JPEG file!\n"; DiscardData(); f.close(); return false; } f.close(); DiscardData(); //now make the strings clean, // for exmaple my Casio is a "QV-4000 " CameraMake = CameraMake.stripWhiteSpace(); CameraModel = CameraModel.stripWhiteSpace(); UserComment = UserComment.stripWhiteSpace(); Comment = Comment.stripWhiteSpace(); return true; }