| //-------------------------------------------------------------------------- |
| // Program to pull the information out of various types of EXIF digital |
| // camera files and show it in a reasonably consistent way |
| // |
| // This module handles basic Jpeg file handling |
| // |
| // Matthias Wandel |
| //-------------------------------------------------------------------------- |
| #include <utils/Log.h> |
| #include "jhead.h" |
| |
| // Storage for simplified info extracted from file. |
| ImageInfo_t ImageInfo; |
| |
| |
| static Section_t * Sections = NULL; |
| static int SectionsAllocated; |
| static int SectionsRead; |
| static int HaveAll; |
| |
| // Define the line below to turn on poor man's debugging output |
| #undef SUPERDEBUG |
| |
| #ifdef SUPERDEBUG |
| #define printf LOGE |
| #endif |
| |
| |
| |
| #define PSEUDO_IMAGE_MARKER 0x123; // Extra value. |
| //-------------------------------------------------------------------------- |
| // Get 16 bits motorola order (always) for jpeg header stuff. |
| //-------------------------------------------------------------------------- |
| static int Get16m(const void * Short) |
| { |
| return (((uchar *)Short)[0] << 8) | ((uchar *)Short)[1]; |
| } |
| |
| |
| //-------------------------------------------------------------------------- |
| // Process a COM marker. |
| // We want to print out the marker contents as legible text; |
| // we must guard against random junk and varying newline representations. |
| //-------------------------------------------------------------------------- |
| static void process_COM (const uchar * Data, int length) |
| { |
| int ch; |
| char Comment[MAX_COMMENT_SIZE+1]; |
| int nch; |
| int a; |
| |
| nch = 0; |
| |
| if (length > MAX_COMMENT_SIZE) length = MAX_COMMENT_SIZE; // Truncate if it won't fit in our structure. |
| |
| for (a=2;a<length;a++){ |
| ch = Data[a]; |
| |
| if (ch == '\r' && Data[a+1] == '\n') continue; // Remove cr followed by lf. |
| |
| if (ch >= 32 || ch == '\n' || ch == '\t'){ |
| Comment[nch++] = (char)ch; |
| }else{ |
| Comment[nch++] = '?'; |
| } |
| } |
| |
| Comment[nch] = '\0'; // Null terminate |
| |
| if (ShowTags){ |
| printf("COM marker comment: %s\n",Comment); |
| } |
| |
| strcpy(ImageInfo.Comments,Comment); |
| ImageInfo.CommentWidchars = 0; |
| } |
| |
| |
| //-------------------------------------------------------------------------- |
| // Process a SOFn marker. This is useful for the image dimensions |
| //-------------------------------------------------------------------------- |
| static void process_SOFn (const uchar * Data, int marker) |
| { |
| int data_precision, num_components; |
| |
| data_precision = Data[2]; |
| ImageInfo.Height = Get16m(Data+3); |
| ImageInfo.Width = Get16m(Data+5); |
| num_components = Data[7]; |
| |
| if (num_components == 3){ |
| ImageInfo.IsColor = 1; |
| }else{ |
| ImageInfo.IsColor = 0; |
| } |
| |
| ImageInfo.Process = marker; |
| |
| if (ShowTags){ |
| printf("JPEG image is %uw * %uh, %d color components, %d bits per sample\n", |
| ImageInfo.Width, ImageInfo.Height, num_components, data_precision); |
| } |
| } |
| |
| |
| //-------------------------------------------------------------------------- |
| // Check sections array to see if it needs to be increased in size. |
| //-------------------------------------------------------------------------- |
| void CheckSectionsAllocated(void) |
| { |
| if (SectionsRead > SectionsAllocated){ |
| ErrFatal("allocation screwup"); |
| } |
| if (SectionsRead >= SectionsAllocated){ |
| SectionsAllocated += SectionsAllocated/2; |
| Sections = (Section_t *)realloc(Sections, sizeof(Section_t)*SectionsAllocated); |
| if (Sections == NULL){ |
| ErrFatal("could not allocate data for entire image"); |
| } |
| } |
| } |
| |
| |
| //-------------------------------------------------------------------------- |
| // Parse the marker stream until SOS or EOI is seen; |
| //-------------------------------------------------------------------------- |
| int ReadJpegSections (FILE * infile, ReadMode_t ReadMode) |
| { |
| int a; |
| int HaveCom = FALSE; |
| |
| a = fgetc(infile); |
| |
| if (a != 0xff || fgetc(infile) != M_SOI){ |
| return FALSE; |
| } |
| for(;;){ |
| int itemlen; |
| int marker = 0; |
| int ll,lh, got; |
| uchar * Data; |
| |
| CheckSectionsAllocated(); |
| |
| for (a=0;a<=16;a++){ |
| marker = fgetc(infile); |
| if (marker != 0xff) break; |
| |
| if (a >= 16){ |
| fprintf(stderr,"too many padding bytes\n"); |
| return FALSE; |
| } |
| } |
| |
| |
| Sections[SectionsRead].Type = marker; |
| |
| // Read the length of the section. |
| lh = fgetc(infile); |
| ll = fgetc(infile); |
| |
| itemlen = (lh << 8) | ll; |
| |
| if (itemlen < 2){ |
| // ErrFatal("invalid marker"); |
| LOGE("invalid marker"); |
| return FALSE; |
| } |
| |
| Sections[SectionsRead].Size = itemlen; |
| |
| Data = (uchar *)malloc(itemlen); |
| if (Data == NULL){ |
| // ErrFatal("Could not allocate memory"); |
| LOGE("Could not allocate memory"); |
| return 0; |
| } |
| Sections[SectionsRead].Data = Data; |
| |
| // Store first two pre-read bytes. |
| Data[0] = (uchar)lh; |
| Data[1] = (uchar)ll; |
| |
| got = fread(Data+2, 1, itemlen-2, infile); // Read the whole section. |
| if (got != itemlen-2){ |
| // ErrFatal("Premature end of file?"); |
| LOGE("Premature end of file?"); |
| return FALSE; |
| } |
| SectionsRead += 1; |
| |
| printf("reading marker %d", marker); |
| 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 cp, ep, size; |
| // Determine how much file is left. |
| cp = ftell(infile); |
| fseek(infile, 0, SEEK_END); |
| ep = ftell(infile); |
| fseek(infile, cp, SEEK_SET); |
| |
| size = ep-cp; |
| Data = (uchar *)malloc(size); |
| if (Data == NULL){ |
| // ErrFatal("could not allocate data for entire image"); |
| LOGE("could not allocate data for entire image"); |
| return FALSE; |
| } |
| |
| got = fread(Data, 1, size, infile); |
| if (got != size){ |
| // ErrFatal("could not read the rest of the image"); |
| LOGE("could not read the rest of the image"); |
| return FALSE; |
| } |
| |
| CheckSectionsAllocated(); |
| 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 |
| fprintf(stderr,"No image in jpeg!\n"); |
| return FALSE; |
| |
| case M_COM: // Comment section |
| if (HaveCom || ((ReadMode & READ_METADATA) == 0)){ |
| // Discard this section. |
| free(Sections[--SectionsRead].Data); |
| }else{ |
| process_COM(Data, itemlen); |
| HaveCom = TRUE; |
| } |
| 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: |
| // There can be different section using the same marker. |
| if (ReadMode & READ_METADATA){ |
| if (memcmp(Data+2, "Exif", 4) == 0){ |
| process_EXIF(Data, itemlen); |
| break; |
| }else if (memcmp(Data+2, "http:", 5) == 0){ |
| Sections[SectionsRead-1].Type = M_XMP; // Change tag for internal purposes. |
| if (ShowTags){ |
| printf("Image cotains XMP section, %d bytes long\n", itemlen); |
| if (ShowTags){ |
| ShowXmp(Sections[SectionsRead-1]); |
| } |
| } |
| break; |
| } |
| } |
| // Oterwise, discard this section. |
| free(Sections[--SectionsRead].Data); |
| break; |
| |
| case M_IPTC: |
| if (ReadMode & READ_METADATA){ |
| if (ShowTags){ |
| printf("Image cotains IPTC section, %d bytes long\n", itemlen); |
| } |
| // Note: We just store the IPTC section. Its relatively straightforward |
| // and we don't act on any part of it, so just display it at parse time. |
| }else{ |
| 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 DiscardData(void) |
| { |
| int a; |
| |
| for (a=0;a<SectionsRead;a++){ |
| free(Sections[a].Data); |
| } |
| |
| memset(&ImageInfo, 0, sizeof(ImageInfo)); |
| SectionsRead = 0; |
| HaveAll = 0; |
| } |
| |
| //-------------------------------------------------------------------------- |
| // Read image data. |
| //-------------------------------------------------------------------------- |
| int ReadJpegFile(const char * FileName, ReadMode_t ReadMode) |
| { |
| FILE * infile; |
| int ret; |
| |
| infile = fopen(FileName, "rb"); // Unix ignores 'b', windows needs it. |
| |
| if (infile == NULL) { |
| LOGE("can't open '%s'", FileName); |
| fprintf(stderr, "can't open '%s'\n", FileName); |
| return FALSE; |
| } |
| |
| // Scan the JPEG headers. |
| printf("ReadJpegSections"); |
| ret = ReadJpegSections(infile, ReadMode); |
| if (!ret){ |
| LOGE("Not JPEG: %s", FileName); |
| fprintf(stderr,"Not JPEG: %s\n",FileName); |
| } |
| |
| fclose(infile); |
| |
| if (ret == FALSE){ |
| DiscardData(); |
| } |
| return ret; |
| } |
| |
| |
| //-------------------------------------------------------------------------- |
| // Replace or remove exif thumbnail |
| //-------------------------------------------------------------------------- |
| int SaveThumbnail(char * ThumbFileName) |
| { |
| FILE * ThumbnailFile; |
| |
| if (ImageInfo.ThumbnailOffset == 0 || ImageInfo.ThumbnailSize == 0){ |
| fprintf(stderr,"Image contains no thumbnail\n"); |
| return FALSE; |
| } |
| |
| if (strcmp(ThumbFileName, "-") == 0){ |
| // A filename of '-' indicates thumbnail goes to stdout. |
| // This doesn't make much sense under Windows, so this feature is unix only. |
| ThumbnailFile = stdout; |
| }else{ |
| ThumbnailFile = fopen(ThumbFileName,"wb"); |
| } |
| |
| if (ThumbnailFile){ |
| uchar * ThumbnailPointer; |
| Section_t * ExifSection; |
| ExifSection = FindSection(M_EXIF); |
| ThumbnailPointer = ExifSection->Data+ImageInfo.ThumbnailOffset+8; |
| |
| fwrite(ThumbnailPointer, ImageInfo.ThumbnailSize ,1, ThumbnailFile); |
| fclose(ThumbnailFile); |
| return TRUE; |
| }else{ |
| // ErrFatal("Could not write thumbnail file"); |
| LOGE("Could not write thumbnail file"); |
| return FALSE; |
| } |
| } |
| |
| //-------------------------------------------------------------------------- |
| // Replace or remove exif thumbnail |
| //-------------------------------------------------------------------------- |
| int ReplaceThumbnail(const char * ThumbFileName) |
| { |
| FILE * ThumbnailFile; |
| int ThumbLen, NewExifSize; |
| Section_t * ExifSection; |
| uchar * ThumbnailPointer; |
| |
| if (ImageInfo.ThumbnailOffset == 0 || ImageInfo.ThumbnailAtEnd == FALSE){ |
| if (ThumbFileName == NULL){ |
| // Delete of nonexistent thumbnail (not even pointers present) |
| // No action, no error. |
| return FALSE; |
| } |
| |
| // Adding or removing of thumbnail is not possible - that would require rearranging |
| // of the exif header, which is risky, and jhad doesn't know how to do. |
| fprintf(stderr,"Image contains no thumbnail to replace - add is not possible\n"); |
| #ifdef SUPERDEBUG |
| LOGE("Image contains no thumbnail to replace - add is not possible\n"); |
| #endif |
| return FALSE; |
| } |
| |
| if (ThumbFileName){ |
| ThumbnailFile = fopen(ThumbFileName,"rb"); |
| |
| if (ThumbnailFile == NULL){ |
| //ErrFatal("Could not read thumbnail file"); |
| LOGE("Could not read thumbnail file"); |
| return FALSE; |
| } |
| |
| // get length |
| fseek(ThumbnailFile, 0, SEEK_END); |
| |
| ThumbLen = ftell(ThumbnailFile); |
| fseek(ThumbnailFile, 0, SEEK_SET); |
| |
| if (ThumbLen + ImageInfo.ThumbnailOffset > 0x10000-20){ |
| //ErrFatal("Thumbnail is too large to insert into exif header"); |
| LOGE("Thumbnail is too large to insert into exif header"); |
| return FALSE; |
| } |
| }else{ |
| if (ImageInfo.ThumbnailSize == 0){ |
| return FALSE; |
| } |
| |
| ThumbLen = 0; |
| ThumbnailFile = NULL; |
| } |
| |
| ExifSection = FindSection(M_EXIF); |
| |
| NewExifSize = ImageInfo.ThumbnailOffset+8+ThumbLen; |
| ExifSection->Data = (uchar *)realloc(ExifSection->Data, NewExifSize); |
| |
| ThumbnailPointer = ExifSection->Data+ImageInfo.ThumbnailOffset+8; |
| |
| if (ThumbnailFile){ |
| fread(ThumbnailPointer, ThumbLen, 1, ThumbnailFile); |
| fclose(ThumbnailFile); |
| } |
| |
| ImageInfo.ThumbnailSize = ThumbLen; |
| |
| Put32u(ExifSection->Data+ImageInfo.ThumbnailSizeOffset+8, ThumbLen); |
| |
| ExifSection->Data[0] = (uchar)(NewExifSize >> 8); |
| ExifSection->Data[1] = (uchar)NewExifSize; |
| ExifSection->Size = NewExifSize; |
| |
| #ifdef SUPERDEBUG |
| LOGE("ReplaceThumbnail successful thumblen %d", ThumbLen); |
| #endif |
| return TRUE; |
| } |
| |
| |
| //-------------------------------------------------------------------------- |
| // Discard everything but the exif and comment sections. |
| //-------------------------------------------------------------------------- |
| void DiscardAllButExif(void) |
| { |
| Section_t ExifKeeper; |
| Section_t CommentKeeper; |
| Section_t IptcKeeper; |
| Section_t XmpKeeper; |
| int a; |
| |
| memset(&ExifKeeper, 0, sizeof(ExifKeeper)); |
| memset(&CommentKeeper, 0, sizeof(CommentKeeper)); |
| memset(&IptcKeeper, 0, sizeof(IptcKeeper)); |
| memset(&XmpKeeper, 0, sizeof(IptcKeeper)); |
| |
| for (a=0;a<SectionsRead;a++){ |
| if (Sections[a].Type == M_EXIF && ExifKeeper.Type == 0){ |
| ExifKeeper = Sections[a]; |
| }else if (Sections[a].Type == M_XMP && XmpKeeper.Type == 0){ |
| XmpKeeper = Sections[a]; |
| }else if (Sections[a].Type == M_COM && CommentKeeper.Type == 0){ |
| CommentKeeper = Sections[a]; |
| }else if (Sections[a].Type == M_IPTC && IptcKeeper.Type == 0){ |
| IptcKeeper = Sections[a]; |
| }else{ |
| free(Sections[a].Data); |
| } |
| } |
| SectionsRead = 0; |
| if (ExifKeeper.Type){ |
| CheckSectionsAllocated(); |
| Sections[SectionsRead++] = ExifKeeper; |
| } |
| if (CommentKeeper.Type){ |
| CheckSectionsAllocated(); |
| Sections[SectionsRead++] = CommentKeeper; |
| } |
| if (IptcKeeper.Type){ |
| CheckSectionsAllocated(); |
| Sections[SectionsRead++] = IptcKeeper; |
| } |
| |
| if (XmpKeeper.Type){ |
| CheckSectionsAllocated(); |
| Sections[SectionsRead++] = XmpKeeper; |
| } |
| } |
| |
| //-------------------------------------------------------------------------- |
| // Write image data back to disk. |
| //-------------------------------------------------------------------------- |
| int WriteJpegFile(const char * FileName) |
| { |
| FILE * outfile; |
| int a; |
| |
| if (!HaveAll){ |
| LOGE("Can't write back - didn't read all"); |
| return FALSE; |
| } |
| |
| outfile = fopen(FileName,"wb"); |
| if (outfile == NULL){ |
| LOGE("Could not open file for write"); |
| return FALSE; |
| } |
| |
| // Initial static jpeg marker. |
| fputc(0xff,outfile); |
| fputc(0xd8,outfile); |
| |
| if (Sections[0].Type != M_EXIF && Sections[0].Type != M_JFIF){ |
| // The image must start with an exif or jfif marker. If we threw those away, create one. |
| static uchar JfifHead[18] = { |
| 0xff, M_JFIF, |
| 0x00, 0x10, 'J' , 'F' , 'I' , 'F' , 0x00, 0x01, |
| 0x01, 0x01, 0x01, 0x2C, 0x01, 0x2C, 0x00, 0x00 |
| }; |
| fwrite(JfifHead, 18, 1, outfile); |
| } |
| |
| // Write all the misc sections |
| for (a=0;a<SectionsRead-1;a++){ |
| fputc(0xff,outfile); |
| fputc((unsigned char)Sections[a].Type, outfile); |
| fwrite(Sections[a].Data, Sections[a].Size, 1, outfile); |
| } |
| |
| // Write the remaining image data. |
| fwrite(Sections[a].Data, Sections[a].Size, 1, outfile); |
| |
| fclose(outfile); |
| return TRUE; |
| } |
| |
| |
| //-------------------------------------------------------------------------- |
| // Check if image has exif header. |
| //-------------------------------------------------------------------------- |
| Section_t * FindSection(int SectionType) |
| { |
| int a; |
| |
| for (a=0;a<SectionsRead;a++){ |
| if (Sections[a].Type == SectionType){ |
| return &Sections[a]; |
| } |
| } |
| // Could not be found. |
| return NULL; |
| } |
| |
| //-------------------------------------------------------------------------- |
| // Remove a certain type of section. |
| //-------------------------------------------------------------------------- |
| int RemoveSectionType(int SectionType) |
| { |
| int a; |
| for (a=0;a<SectionsRead-1;a++){ |
| if (Sections[a].Type == SectionType){ |
| // Free up this section |
| free (Sections[a].Data); |
| // Move succeding sections back by one to close space in array. |
| memmove(Sections+a, Sections+a+1, sizeof(Section_t) * (SectionsRead-a)); |
| SectionsRead -= 1; |
| return TRUE; |
| } |
| } |
| return FALSE; |
| } |
| |
| //-------------------------------------------------------------------------- |
| // Remove sectons not part of image and not exif or comment sections. |
| //-------------------------------------------------------------------------- |
| int RemoveUnknownSections(void) |
| { |
| int a; |
| int Modified = FALSE; |
| for (a=0;a<SectionsRead-1;){ |
| switch(Sections[a].Type){ |
| 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: |
| case M_SOI: |
| case M_EOI: |
| case M_SOS: |
| case M_JFIF: |
| case M_EXIF: |
| case M_XMP: |
| case M_COM: |
| case M_DQT: |
| case M_DHT: |
| case M_DRI: |
| case M_IPTC: |
| // keep. |
| a++; |
| break; |
| default: |
| // Unknown. Delete. |
| free (Sections[a].Data); |
| // Move succeding sections back by one to close space in array. |
| memmove(Sections+a, Sections+a+1, sizeof(Section_t) * (SectionsRead-a)); |
| SectionsRead -= 1; |
| Modified = TRUE; |
| } |
| } |
| return Modified; |
| } |
| |
| //-------------------------------------------------------------------------- |
| // Add a section (assume it doesn't already exist) - used for |
| // adding comment sections and exif sections |
| //-------------------------------------------------------------------------- |
| Section_t * CreateSection(int SectionType, unsigned char * Data, int Size) |
| { |
| Section_t * NewSection; |
| int a; |
| int NewIndex; |
| NewIndex = 2; |
| |
| if (SectionType == M_EXIF) NewIndex = 0; // Exif alwas goes first! |
| |
| // Insert it in third position - seems like a safe place to put |
| // things like comments. |
| |
| if (SectionsRead < NewIndex){ |
| // ErrFatal("Too few sections!"); |
| LOGE("Too few sections!"); |
| return FALSE; |
| } |
| |
| CheckSectionsAllocated(); |
| for (a=SectionsRead;a>NewIndex;a--){ |
| Sections[a] = Sections[a-1]; |
| } |
| SectionsRead += 1; |
| |
| NewSection = Sections+NewIndex; |
| |
| NewSection->Type = SectionType; |
| NewSection->Size = Size; |
| NewSection->Data = Data; |
| |
| return NewSection; |
| } |
| |
| |
| //-------------------------------------------------------------------------- |
| // Initialisation. |
| //-------------------------------------------------------------------------- |
| void ResetJpgfile(void) |
| { |
| if (Sections == NULL){ |
| Sections = (Section_t *)malloc(sizeof(Section_t)*5); |
| SectionsAllocated = 5; |
| } |
| |
| SectionsRead = 0; |
| HaveAll = 0; |
| } |