New revision of my Exif library (v0.9.9)

I’ve just put up another revision of my Delphi Exif parsing code. This revision has two main themes:

  1. Sanity checks have been added to the parsing code, meaning every single TIFF offset is now checked. Connected to this, and by popular demand (or so it seems), the balance between accepting malformed metadata and raising an exception has now swung a bit towards the former.
  2. Better maker note support: specifically, the tag structures of Canon, Panasonic and Sony MakerNotes are now understood. The interpretation of maker note tag values is still left to the user however.

Other, more minor changes include:

  • Fixed typo in GPS direction tag setter which meant the value could never be changed.
  • Added memory leak fix to CCR.XMPUtils.pas suggested by David Hoyle.
  • Added delay loading semantics to the XMPPacket property of TCustomExifData, the idea being that attempts to read Exif tags should not ever lead to an EInvalidXMPPacket exception being raised. Equivalent behaviour has been built into the new maker note parser code too.
  • More helper methods of the TryGetXXXValue and ReadXXX kind.
  • Surfaced two interop IFD tags as properties on TCustomExifData.
  • Maker note data are now moved back to their original position on save if the OffsetSchema tag had been set. (Actually, this should have been the case for the previous release but for a bug, typing Inc where I meant Dec.)
  • Demos rejigged a bit — PanasonicMakerNoteView.exe removed (its functionality has been added to an improved ExifList.exe), and two new console ones added (CreateXMPSidecar.exe and PanaMakerPatch.exe). You can download compiled versions of the demos from here.

One final note — idly Googling, I’ve found that there’s at least one person around who believes it might be realistic to backport my code to Delphi 7. Two words of advice: don’t bother. You’ll just have one problem after another.

Advertisements

32 thoughts on “New revision of my Exif library (v0.9.9)

  1. Thank you very much for CCR Exif! It is great.

    I have a further desire: It would be fine, if CCR Exif could hold an existing IPTC-Section of an image. Actually IPTC will not be read and saved.

    (excuse my deficient English)
    Kind regards
    Bertram Hafner

    • Hi Bertram —
      If I read you correctly, you have a bug report rather than a feature request, since my code should only be touching Exif and XMP sections, leaving alone any other ones. Do you have any sample images where the IPTC section is being messed with when edited using TExifData?

  2. Hi Chris,
    yes, I have sample images. I open a jpg-File, process it, read exif-tags, change the thumbnail and perhaps some exif-tags and then save it. If IPTCs are existent bevor, thereby they are lost.
    I use TJpegImageEx.SaveToFile()
    Do I make a mistake?
    Here is a sample:
    http://www.bertram-hafner.de/tmpBilder/SamplePic.zip
    Thank you for your help.
    Bertram

    • Bertram — quickly looking at it, I’ve found the following:

      (1) The IPTC segment is not being deleted (tested using the ResavingTest demo).

      (2) The contents of the IPTC segment is not being changed (tested using the Jpeg Dump demo to retrieve the segment offsets, and a hex editor to compare what data exists at said offsets).

      (3) The IPTC segment is, however, liable to be moved, which is expected behaviour (TExifData enforces a segment order of JFIF header -> Exif data -> XMP data -> any other segment).

      (4) IrfanView, however, cannot read the moved IPTC segment, which implies IPTC data internally uses absolute rather than relative file offsets.

      (5) Properly fixing this would require my code to gain and apply knowledge of IPTC internals. I may do this in due course — it all depends on the complexity of IPTC data, and how much freely-available info there is on it. Would you know of any useful links (e.g. IPTC specification documents and/or existing open source IPTC parsers written in Delphi)…?

  3. Chris – I can refer some details of creating my sample:

    To my original picture with IrfanView I added IPTCs:
    Copyright = “Bertram Hafner”
    Keywords = “See Blatt grün”
    and saved it as Pic.jpg

    With any Editor I can see this section within Pic.jpg:
    “…õ¢Š›—/‰ÿÙ ÿí BPhotoshop 3.0 8BIM & See Blatt grünt Bertram HafnerÿÛ C …”

    Then I opened this pic with CCR-Exif, processed it and saved it with CCR-Exif as Pic_b.jpg.

    After this the copyright-notice and the keywords are lost.
    With an editor I can not find them, nor the entry “Photoshop”.
    (I did’nt use the program PS.)

  4. Chris,

    in the meantime I can locate the problem a little:

    If I save the JpegImageEx without without have processed the bitmap, then the IPTCs are OK.

    If I do this:

    JpegImageEx.LoadFromFile(…/Pic.jpg);
    bitmap.Assign(JpegImageEx);

    //process bitmap

    JpegImageEx.Assign(bitmap);
    JpegImageEx.SaveToFile(…/Pic_b.jpg);

    then my IPTCs are lost.

    Do I make a mistake?

    • ‘Do I make a mistake?’

      No. Read my earlier reply to you — it seems IPTC data internally uses absolute rather than relative offsets, meaning any change in the position of the IPTC segment invalidates the IPTC data, even if that data was not itself edited.

  5. On an early state of my software – i need version info of “CCR.exif” by – not creating a object – but just reading a “const”. Please can you include the “const” “Version” like this?
    Thanks – (Reason: I report all the Version-Info in a special page of my software, and via XMLRPC)

    —snip—
    unit CCR.Exif.Consts;

    interface

    const
    Version = ‘0.9.9’; // (2009-11-19)

    resourcestring
    SInvalidJPEGHeader = ‘JPEG header is not valid’;
    SFileIsNotAValidJPEG = ‘”%s” is not a valid JPEG file’;
    — snap —

  6. Hi Chris,

    please excuse my persistence:
    I think, the IPTCs are not only invaldate because of changing their position. But JpegImageEx eliminates them, they cannot be found with an hex-editor no longer.

    I tested it as follows:

    Pic.jpg (a picture without IPTCs)
    Pic_1.jpg (IPTC-keywords added with IrfanView)

    Then:
    1. JpegImageEx.LoadFromFile(Pic_1.jpg);
    2. bitmap.Assign(JpegImageEx);
    3. JpegImageEx.Assign(bitmap);
    4. JpegImageEx.SaveToFile(Pic_b.jpg);

    After this in Pic_b.jpg IPTCs cannot be found with an hex-editor.
    But, if I skip line 3, then IPTCs are existent and readable.

    Furthermore a friend of mine (he is developer of software) discovered,
    that Pic.jpg and Pic_b.jpg are exactly binary identic.

    Therefore we think, the method assign() eliminates the IPTC-section.

    Kind regards

    • Hi Bertram —

      Looking into it again, I take back my previous comment — my code *isn’t* corrupting any internal IPTC offsets, since there are no internal IPTC offsets to corrupt!

      Your issue is rather your copying to and from a bitmap, since an Assign call is supposed to duplicate what is being assigned, and since a bitmap can’t contain any metadata, no metadata *should* be left after such a roundtrip. In light of this, the bug in my code is that the loaded Exif tags aren’t cleared, not that other metadata isn’t preserved – cf. when another TJpegImageEx is assigned (the source image’s tags won’t be copied over).

      Naturally, this isn’t much help if your aim is to load a Jpeg, fiddle about with the image data, then resave it, preserving any metadata it had orginally. What needs to be done, in short, is for any potential metadata to be saved immediately before the Assign call so as it can be restored immediately afterwards. This is a bit fiddly, but… I’ve had a go at implementing it. Try this version of CCR.Exif.pas out, and use the new overload of Assign that takes a second parameter —

      procedure ShrinkImageTest(const JpegFile: string);
      var
        Bitmap: TBitmap;
        Jpeg: TJpegImageEx;
      begin
        Bitmap := nil;
        Jpeg := TJpegImageEx.Create;
        try
          Jpeg.LoadFromFile(JpegFile);
          Jpeg.ExifData.Comments := 'Metadata preserving demo';
          Bitmap := TBitmap.Create;
          Bitmap.SetSize(Jpeg.Width div 2, Jpeg.Height div 2);
          Bitmap.Canvas.StretchDraw(Rect(0, 0,
            Bitmap.Width, Bitmap.Height), Jpeg);
          Jpeg.Assign(Bitmap, [jaPreserveMetadata]);
          Jpeg.SaveToFile(ChangeFileExt(JpegFile, '') +
            ' (shrunk)' + ExtractFileExt(JpegFile));
        finally
          Jpeg.Free;
          Bitmap.Free;
        end;
      end;
      

      I would be grateful if you could tell me whether this solves your problem or not.

  7. Hi Chris,
    I have tried your v0.9.9 and I run into problems now with all images shot with Nikon and Kodak cameras, what means that I even could not read the standard exif header. Running your EXIFList demo project will throw an “invalid range” exception. Any clues why this does not work anymore? This worked with v0.9.7 without any problems. I could send you an image by mail, if you like…
    Regards, Stefan

    • Stefan — I’ve no idea without running it through the debugger (using the HTML report mode of Phil Harvey’s ExifTool may give you some ideas though). You can upload a few examples somewhere for me to download if you want.

      • Hi Chris, my bad!
        I had a strange mix up of old and newly built DCUs. Problems are now solved after I tried you precompiled examples, which worked fine. I first suspected Delphi 2009 to be responsible for that, but actually it was a user error. 😉

        Thanks anyway, Stefan

        BTW, I saw that you added a decoder for the Nikon Type 1 header. I will try to implement the (in nower days very common) Type 3 header.

      • Thanks for the update (simply doing a build rather than a compile has fixed it then?). With respect to the maker note format you mention, if this page (link) is correct, something like the following should do (I don’t have any example images to test against, so it may be wrong):

          TNikonType3MakerNote = class(TExifMakerNote)
          protected
            const HeaderStart: array[0..7] of AnsiChar =
              'Nikon'#0#2;
            class function FormatIsOK(Source: TExifTag;
              out HeaderSize: Integer): Boolean; override;
          end;
        
        class function TNikonType3MakerNote.FormatIsOK(Source:
          TExifTag; out HeaderSize: Integer): Boolean;
        begin
          HeaderSize := 18;
          Result := (Source.ElementCount > HeaderSize) and
            CompareMem(Source.Data, @HeaderStart,
            SizeOf(HeaderStart));
        end;
        

        Register it somewhere like this:

        TExifData.RegisterMakerNoteType(TNikonType3MakerNote,
           mtTestForFirst);
        
        • Did you use the RegisterMakerNoteType method anywhere in your code e.g. for the Nikon Type 1 or the Sony header? I cannot find any occurence like the one suggested or am I missing something?!
          BTW, “HeaderStart” should actually be “Header”, shouldn’t it?

    • OK, looks like there’s a bit more to it. The prospective TNikonType3MakerNote type needs its GetIFDInfo method overridden too:

      procedure TNikonType3MakerNote.GetIFDInfo(
        SourceTag: TExifTag; var ProbableEndianness: TEndianness;
        var DataOffsetsType: TExifDataOffsetsType);
      var
        SeekPtr: PAnsiChar;
      begin
        SeekPtr := SourceTag.Data;
        if (SeekPtr[10] = 'M') and (SeekPtr[11] = 'M') then
          ProbableEndianness := BigEndian
        else
          ProbableEndianness := SmallEndian;
        DataOffsetsType := doFromIFDStart;
      end;
      

      Moreover, TExifMakerNote.Create needs to be edited so that tags with ‘large’ data (> 4 bytes) are read correctly:
      1. Add as a local variable ‘InternalOffset: Int64;’
      2. Add the following immediately after the case statement:

        if FDataOffsetsType = doFromIFDStart then
          InternalOffset := -8
        else
          InternalOffset := Tags.Owner.OffsetSchema;
      

      3. Change the two Tag.Load calls to use InternalOffset rather than Tags.Owner.OffsetSchema.

      The things I do gratis…

      • Funny, I got it the Type 3 header decoding working this morning as well, with similar adaptions to your code. 🙂

        To give something back, I can offer an extended MakerNotes.ini with all details of Nikon Type 1 and 3 headers…

        As always many thanks for your library and code!

      • Stefan –

        HeaderStart/Header: I renamed the parameter after pasting it into the comment box just to make it make it fit the comment space better, so if I didn’t rename every instance, that would be an error, yes.

        RegisterMakerNoteType: the ‘ready-made’ maker note class types don’t have RegisterMakerNoteType called for them since I register them directly in the unit’s implementation section. That I originally said to call RegisterMakerNoteType was because I had assumed CCR.Exif.pas didn’t have a relevant bug that needed fixing, and thus, didn’t need editing for the new maker note type to be understood.

        Giving something back: an extended MakerNotes.ini would be great, thanks – have you written one aleady? (Connected to that, may I assume the email address you’ve entered is a real one…?)

        • Hi Chris,
          the Makernotes.ini is ready for you with all info that I could find on several web sites about the Nikon Type 1 and 3 headers.
          And yes – the email address is a real one. 🙂
          Regards, Stefan

  8. Hi Chris,

    your overloaded method TJpegImageEx.Assign() seems to work perfectly. Thank you for taking trouble!

    Regards, Bertram

  9. Hi Chris,

    relative to IPTCs still I have one question: If I want, how can I delete the IPTC-Section from a JpegImageEx?

    Thanks, Bertram

    • Bertram – if you’re not bothered about potentially losing private metadata added by Adobe Photoshop, you just need to remove all APP13 segments, i.e. all segments with a marker number of $ED. Something like this should do (untested):

      uses
        CCR.Exif.JpegUtils;
      
      procedure RemoveApp13sFromJPEG(JPEGImage: TJpegImage); 
      var
        InStream, OutStream: TMemoryStream;
        Segment: IFoundJPEGSegment;
        StartCopyFrom: Int64;
      
        procedure DoCopy(const EndPos, NewStartPosOffset: Int64);
        begin
          InStream.Position := StartCopyFrom;
          if (EndPos - StartCopyFrom) > 0 then
            OutStream.CopyFrom(InStream, EndPos - StartCopyFrom);
          StartCopyFrom := EndPos + NewStartPosOffset;
        end;
      begin
        OutStream := nil;
        InStream := TMemoryStream.Create;
        try
          JPEGImage.SaveToStream(InStream);
          InStream.Position := 0;
          StartCopyFrom := 0;
          for Segment in JPEGHeader(InStream, [$ED]) do
          begin
            if OutStream = nil then OutStream := TMemoryStream.Create;
            DoCopy(Segment.Offset, Segment.TotalSize);
          end;
          if OutStream <> nil then
          begin
            DoCopy(InStream.Size, 0);
            OutStream.Position := 0;
            JPEGImage.LoadFromStream(OutStream);
          end;
        finally
          OutStream.Free;
          InStream.Free;
        end;
      end;
      

      This may look a bit of work, but it’s only what TJpegImageEx would have to do internally for a hypothetical ‘RemoveIPTCData’ method.

  10. Hi Chris,

    thank you very much for this quick help! Inspite of Chrismas!

    I wish you and your familiy a Merry Chrismas! In Germany we say “Frohe Weihnachten!”

  11. Hi Chris,

    I’ve got a jpg-file, which produces ExifData.Lightsource = 255,
    while it should be in [-1..24]. I don’t know, wether it is of importance for you.

  12. Hello Chris,

    Thanks for all your work creating this code and making it available. Very nice.

    I’m just getting started with CCR Exif 0.9.9 with Delphi 2007. I am getting a “Range check error” during the LoadFromJPEG function if I create a simple test app. If I use your compiled ExifList.exe on the same jpeg file it works fine.

    Specifically, stepping through the code:
    In CCR.Exif line 1617’s Stream.ReadLongInt calls to StreamHelper.ReadLongInt,
    which calls another overloaded ReadLongInt. Within that function
    the Read procedure on line 116 (of CCR.Exif.StreamHelper) stores -1643642880 in Value.
    The following call to SwapLongWord(Value) on line 118 causes an ERangeError exception “Range check error” because SwapLongWord expects an unsigned int32.

    If, on line 118, I replace the SwapLongWord call with SwapLongInt, my test code runs ok.

    -Jeff

Comments are closed.