CCR Exif v1.5.1

What it is: a small class library to edit, create and delete Exif (and now IPTC) metadata in JPEG files, Exif being the sort of metadata that most digital cameras write.

Features: 100% pure Delphi code — doesn’t use (say) LibExif, LibTiff or GDI+; reads/writes both small- and big-endian data; surfaces both standard Exif and Windows Explorer tags; understands the tag structures of Canon and Panasonic maker notes; protects maker note offsets when saving; keeps any associated XMP data in sync when changing Exif tags; includes standalone IPTC and XMP reader/writer classes.

Limitations: freeform editing not implemented for maker note tags yet.

Basic usage:

uses
  CCR.Exif;

procedure ReadCameraMakeAndModel(const FileName: string;
  out Make, Model: string);
var
  ExifData: TExifData;
begin
  ExifData := TExifData.Create;
  try
    ExifData.LoadFromGraphic(FileName);
    Make := ExifData.CameraMake;
    Model := ExifData.CameraModel;
  finally
    ExifData.Free;
  end;
end;

The preferred way to edit metadata is to use a LoadFromGraphic -> property setting -> SaveToGraphic cycle on a relevant class instance — TExifData for Exif data, TIPTCData for IPTC, and TXMPPacket for XMP:

uses
  CCR.Exif;

procedure SetSomeExifTags(const FileName: string);
var
  ExifData: TExifData;
begin
  ExifData := TExifData.Create;
  try
    ExifData.LoadFromGraphic(FileName);
    ExifData.Subject := 'Wimbledon Tennis';
    ExifData.SetKeyWords(['tennis', 'Wimbledon', 'match', 'SW19']);
    //Exif uses the degrees/minutes/seconds GPS format
    ExifData.GPSLatitude.Assign(51, 26, 1.48, ltNorth);
    ExifData.GPSLongitude.Assign(0, 12, 50.63, lnWest);
    ExifData.SaveToGraphic(FileName);
  finally
    ExifData.Free;
  end;
end;

 

uses
  CCR.Exif.IPTC;

procedure SetIPTCHeadlineAndCredit(const FileName,
  NewHeadline, NewCredit: string);
var
  IPTCData: TIPTCData;
begin
  IPTCData := TIPTCData.Create;
  try
    IPTCData.LoadFromGraphic(FileName);
    IPTCData.Headline := NewHeadline;
    IPTCData.Credit := NewCredit;
    IPTCData.SaveToGraphic(FileName);
  finally
    IPTCData.Free;
  end;
end;

Lastly, say you want to delete all Exif tags with binary (‘undefined’) data in a certain file. To cycle through the tags, you use the for-in syntax; on each iteratrion, you can then check the data type and call Delete as required:

uses
  CCR.Exif.TiffUtils{not needed before v1.5.0}, CCR.Exif;

procedure StripBinaryTags(const FileName: string);
var
  ExifData: TExifData;
  Section: TExifSection;
  Tag: TExifTag;
begin
  ExifData := TExifData.Create;
  try
    ExifData.LoadFromGraphic(FileName);
    for Section in ExifData do
      for Tag in Section do
        if Tag.DataType = tdUndefined then
          Tag.Delete;
    ExifData.SaveToGraphic(FileName);
  finally
    ExifData.Free;
  end;
end;

Documentation: the ZIP and repository (see below) includes a PDF documenting many properties and freestanding routines. There’s also various demos included.

Supported Delphi versions: Delphi 2006 and up, preferably Delphi 2007 and up. Supports XE2′s OS X and Win64 targets, though not iOS since the Delphi compiler and RTL are required.

Licence: MPL 1.1 (text here).

Download: the code in the form of a Subversion repository is on Google Code herethis is the recommended way to get it. However, a ZIP file containing the latest stable release is also available as the top link here.

Bug reports: it’s not exactly the most optimum way of doing things for sure, but if you have any issues or suggestions, please add a comment to the post announcing the current release (the comments below concern the 0.9.0 and 0.9.5 releases). Unfortunately, the theme I’m using messes up the display of nested comments more than three levels deep, so please keep that in mind when deciding whether to reply to a previous comment or just add a new one.

36 thoughts on “CCR Exif v1.5.1

  1. Thanks man! Very impressive features described here. I’m planning to do a program on its base. Wish you a good luck and the library to be impoved and become the best one.

    • You’re welcome. If you have any bug reports or ideas for improvement, just post them here.

  2. I’ve been wanting to read Exif information in one of my apps (wall paper) for a while and have chosen your little libray over another as its more comprehensive and its seems great. However while coding in BDS2006 I’ve found 1 bug and 1 situation where I’ve had to change your code (I really think its a BDS bug) (plus a compiler problem noted in QC 30131 – but that’s not your problem).

    Whats the best way for me to show you the changes (their small and minor)?

    regards
    Dave.

    • Dave – if the changes are small, you could just post details of them here, otherwise you can email me the revised unit(s). If the latter, my address is just like yours, but ‘ccrolliston’ rather than your name (apologies for the opaqueness, but my account is still spam-free and I’d like it to stay that way!).

      That said, is one of the problems to do with inlining? It’s just that when I ran the code through the D2006 compiler myself, I had to conditionally define away one ‘inline’ declaration, and it wouldn’t surprise me if another is causing issues. What with the advanced records bug that QC report notes, I hope D2006′s enumerator support isn’t playing up too!

      • I understand fully about the e-mail address so the changes as as follows:
        1) This is a workaround for a bug in BDS2006 more than a problem with your code:
        Line: 286: TExifVersionString = String; // was String[4]
        Line: 2068: function TCustomExifData.GetExifVersion: TExifVersionString;
        Line: 2069: var
        Line: 2070: Tag: TExifTag;
        Line: 2071: begin
        Line: 2072: if FSections[esDetails].Find(ttExifVersion, Tag) and (Tag.DataType = tdUndefined) and
        Line: 2073: (Tag.ElementCount = 4) then
        Line: 2074: Begin // new line
        Line: 2075: SetLength(Result, 4); //new line to alloc ansi string
        Line: 2076: Move(Tag.Data^, Result[1], 4);
        Line: 2077: End // new line
        Line: 2078: else
        Line: 2079: Result := ”;
        Line: 2080: end;

        This resolved a problem I had when concatenating information to results where this would return a null string even when I knew there was valid exif data.

        2) I think this is just one of those bugs that gets missed when you possible copy and paste code…
        Line: 2453: function TCustomExifData.GetMeteringMode: TExifMeteringMode;
        Line: 2454: begin // esGeneral ttOrientation
        Line: 2455: Result := TExifMeteringMode(FSections[esDetails].GetWordValue(ttMeteringMode, 0, //: @bug DGH
        Line: 2456: Ord(emUnknown), Ord(Low(TExifMeteringMode)), Ord(High(TExifMeteringMode))));
        Line: 2457: end;

        This didn’t returns the right values so I changed the section to esDetails and the Tag ID to rrMetering and then I started to get the correct values.

        regards
        Dave.

  3. I should have said that these are in CCRExif.pas and I mistyped the last paragraph and the Tag ID should be ttMeteringMode.

    regards
    Dave.

    • Thanks – I’ve noticed a few other typos in my code too. In light of this and a few other things, I’ll have a general revision up soon.

  4. Hello again, Chris!

    Here are my suggestions:

    1. It would be great to preserve original image structure on open/save cycle with TExifData as careful as possible. In addition to other gains, this can be useful to prepare handling of file formats other that JPEG.

    2. Saving should be completely rewritten. Not every file contains JFIF header nor EXIF. It means, we need to introduce flags that points, is metadata (JFIF, EXIF, .. any other) present or not, and initialize them on loading from stream. SaveToStream method of TJpegImageEx must take into accouns that flags and writes or not corresponding data. This fixes the bug, when program corrupts an image w/o JFIF on saving..

    3. Sections may be deleted also. I thinks it’s not so hard to implement this.

    Thanks and best wishes to you from snowy Russia.

    • Valerian — regards from warm, sunny England! Thanks for the suggestions. Especially given it’s a bug, I’ll try to look into (2) especially soon-ish.

      That said, given you don’t appear to find TExifDataPatcher flexible enough for your needs, I take it a basic aim of yours is to add Exif data to an existing file when none was there already? You may have noted none of the demos illustrate this, from which you may further infer I wasn’t directly coding for it. I agree properly supporting this would be sensible though.

    • OK, looking at it last evening, I think I have a solution to (2) — check out the revised implementation of TJpegImageEx.SaveToStream here. I’ll probably add the code into TExifData (SaveToJPEG?) and have TJpegImageEx use that for the proper release.

      Regarding (3), I assume you have noticed how the section objects on TExifData have a Clear method? That said, I might well go for not writing out a section if it doesn’t contain any tags — after all, the existing code already doesn’t write out a thumbnail section if the Thumbnail.Empty returns True. A couple of caveats though: (i) what I term the ‘general’ section has to be written out, even if it has no tags, since it contains the pointer to the other sections; (ii) not writing out an empty section will save a grand total of 6 bytes exactly.

      • Thanks you!

        Did look at your correction cursory, therefore have to say nothing this time.

        But having more suggestions to implement:
        4. It would be nice to define all known markers. Check the table B.1 from JPEG Standard (JPEG ISO/IEC 10918-1 ITU-T Recommendation T.81) (DAC, SOF9-SOF11, APPn and some other need to be added).
        5. Markers can be found also after SOS. You should not stop parsing when it occurs. Intead, read and see frame header and skip an appropriate block.
        6. It can be several COM segment. I’m not sure for other markers, but they may also.
        7. The code should also check, that SOS is after SOFn markers. If not – raise an exception.
        8. More correct to output JFIF version in 1.02 form.

        • Valerian — you seem to be wanting a general JPEG file parser. Where it currently stands though, CCR.Exif.JpegUtils.pas just does enough for Exif support. That saiid –

          4. If you wish to define them yourself, that would be great — get them to me, and I’ll copy them into the next release.

          5. Yes, though I don’t know how to correctly parse a JPEG file after the SOS. (If you just remove the two lines in ParseJPEGHeader that bail out when the SOS is hit, you’ll find things don’t get parsed correctly, at least with many images.) Would you know how to it? That’s a genuine question by the way, not a bit of hand waving to fob you off — I was annoyed when what I thought was the proper way didn’t work!

          6. Yes, though I’m not sure what the issue is — COM segments don’t have anything to do with Exif, and if you just set ContinueParsing to True in the callback to ParseSegment, you’ll receive as many COM segments as can be found before the SOS. Is this not working for you? (If you’re unsure, there’s no need to code anything to find out — just run the Jpeg Dump demo and load a file with more than one COM segment into it. If there’s no bugs, all comments should be discovered.)

          7. Why? My code (especially now with the proposed revision to TJpegImageEx.SaveToStream) deliberately doesn’t muck about with any non-Exif stuff in the JPEG data it modifies.

          8. Again, with the revision to TJpegImageEx.SaveToStream, my code doesn’t set any JFIF version, but preserves whatever (if anything) was there before. That said, the reason I did what I did before was because that was what TJpegImage writes out when you create a new image. If you are looking for code that validates the structure of JPEG files, I’m afraid you’ll have to look elsewhere — at least, I’m not sure what the direct connection is here with reading and writing Exif metadata.

  5. Unfortunately I’ve found another error in your code (I think, at least the correction works against a known result).

    Line 2459: function TCustomExifData.GetOrientation: TTiffOrientation;
    Line 2460: begin // esDetails
    Line 2461: Result := TTiffOrientation(FSections[esGeneral].GetWordValue(ttOrientation, 0,
    Line 2462: Ord(toTopLeft), Ord(Low(TTiffOrientation)), Ord(High(TTiffOrientation))));
    Line 2463: end;

    i.e. Orientation should read the section esGeneral not esDetails.

    I’m just starting to write some unit tests for my own code and found this. You mention that you are going to update and release a new version – you may wish to wait a few days until I’ve finished writing my unit test just in case I find something else (I’m testing against a known output from Adobe Elements 5.0 / Windows Explorer in XP).

    regards
    Dave.

    • GetOrientation — you are correct. I actually caught this one myself when going through the code.

      Unit tests — ah yes…

      Delaying the revised version — well, it’s actually now up (the CodeCentral URL is the same as before). One thing you’ll need to change in your code is your use of ExifVersion — it’s now a class with an AsString property. Apart from fixing the bug you highlighted, the idea was to present the ExifVersion and GPSVersion tag data in a consistent manner, despite their being stored differently. There’s also a few other interface changes (see Changes.rtf), though not enough to require recoding the old demos, for example.

      • I’ve finished unit testing a known JPG against my code (and consequently yours) and I’ve not found anymore error although I can’t test GPS information.

        regards
        Dave.

  6. Pingback: New revision of CCR Exif (0.9.5) « Delphi Haven

  7. I tried your Screenshooter demo within D2009. It compiled okay but all all string properties in TExifData seemed to be cut if it saved to jpeg. Maybe there is sth. wrong in Unicode to Ascii conversion.

      • It’s 0.9.5

        I tried to change declaration of (e.g.) CameraMake from string to ShortString or UnicodeString (because TExifData.Author is working) but a string of “n/a” is always displaying only “n” – the firts letter.

      • I’ve got it by changing to:

        property CameraMake: UnicodeString index ttMake read GetGeneralWinString write SetGeneralWinString stored False;
        property CameraModel: UnicodeString index ttModel read GetGeneralWinString write SetGeneralWinString stored False;
        property Copyright: UnicodeString index ttCopyright read GetGeneralWinString write SetGeneralWinString stored False;
        Now the property dialog of XP is diplaying complete string.

        Finally the date property is wrong: “Picture is taken at” is always displaying 30.Nov.2001 02:00. Changing setter function to GetGeneralWinString was not enough.

        • Andreas — don’t do that, since the bug’s in TExifSection.SetStringValue (regression from 0.9.0). Please see my reply to your next comment.

      • To save the right date value I have make these changes:

        procedure TCustomExifData.SetAllDateTimeValues(const DateTime: TDateTime);
        var
        S: TiffString;
        SubTag: TExifTag;
        begin
        S := (DateTimeToExifString(DateTime));
        FSections[esGeneral].SetWindowsStringValue(ttDateTime, S);
        FSections[esDetails].SetWindowsStringValue(ttDateTimeOriginal, S);
        FSections[esDetails].SetWindowsStringValue(ttDateTimeDigitized, S);

        These function must be called too in

        function TExifSection.SetDateTimeValue

        to get it work within D2009.

        BTW, the date format is yyyy:mm:dd. Is it possible to usea localised format ( I would prefer dd.mm.yyyy for my needs)?

        Thanks for your great piece of code.

        • Andreas — no, that’s not the problem. You need to *just* do this: in TExifSection.SetStringValue, add a cast to TiffString in the UpdateData code:

          Result.UpdateData(tdAscii, ElemCount, PAnsiChar(TiffString(Value))^)

          Undo any other change you’ve made — you won’t be writing standard Exif data otherwise.

          With respect to your date question, the Exif date format is fixed. However, since I expose the dates as TDateTime values, you can convert them to any string format you want — in the demos, you’ll see I just call DateTimeToStr (the standard RTL function in SysUtils) to display the localised format.

          ‘Thanks for your great piece of code.’

          You’re welcome, and thanks for the bug report!

  8. Chris,

    I’ve eventually got around to downloading and testing your 0.9.5 code and unfortunately found some bug and a memory leak as follows:

    1) CCR.Exif.pas, Line 2084 needs to be
    FmtStr(Result, ‘%2d%d%d’, [Major, Minor, Release]);
    in order to return the same version string as your previous implementation, i.e.
    “0221″ and not “2021″.

    2) CCR.Exif.pas, Line 2266 needs to be added
    FGPSVersion.Free;
    to stop the memory leak.

    3) CCR.Exif.pas, Line 2653 need to be
    Result := Trim(TiffStr);
    in order to return a null string and be consistent with your previous version.

    4) CCR.Exif.pas, Line 2726 & 27 need to be
    Result := TTiffOrientation(FSections[esGeneral].GetWordValue(ttOrientation, 0,
    Ord(toTopLeft), Ord(Low(TTiffOrientation)), Ord(High(TTiffOrientation))));
    in order for correct the orienation bug, i.e. esGeneral not esDetails.

    I’ve also noticed that 2 properties no longer exists:
    1) DirectlyPhotographed
    2) SourceIsDigitalCamera
    Is this deliberate?

    regards
    Dave.

    • Dave – thanks for the further bug reports. To take them in turn:

      > 1) CCR.Exif.pas, Line 2084 needs to be
      > FmtStr(Result, ‘%2d%d%d’, [Major, Minor, Release]);
      > in order to return the same version string as your previous
      > implementation, i.e.
      > “0221″ and not “2021″.

      I think the issue here is somehere in your own code setting DecimalSeparator to ’0′ (DecimalSeparator being defined in SysUtils). For, the line you have modified was as thus:

      FmtStr(Result, ‘%d%s%d%d’, [Major, DecimalSeparator, Minor, Release]);

      For myself, I get ’2.21′ (other locales may return ’2,21′), and so should you, assuming DecimalSeparator is left untouched or only set to a sensible value.

      > 2) CCR.Exif.pas, Line 2266 needs to be added
      > FGPSVersion.Free;
      > to stop the memory leak.

      Correct.

      > 3) CCR.Exif.pas, Line 2653 need to be
      > Result := Trim(TiffStr);
      > in order to return a null string and be consistent with your previous
      > version.

      Actually, the original version needed this — you may recall this function was one of the ones that had a typo, causing the UserComment tag to never be read. Now, looking at the spec again, it seems this tag can legitimately be padded with spaces — quite possibly, your test images have UserComment tags with nothing but padding, leading to the nconsistency in what my code reports between the two main revisions. So, I do need to trim, but needed to all along!

      > 4) CCR.Exif.pas, Line 2726 & 27 need to be
      > Result :=
      > TTiffOrientation(FSections[esGeneral].GetWordValue(ttOrientation, 0,
      > Ord(toTopLeft), Ord(Low(TTiffOrientation)),
      > Ord(High(TTiffOrientation))));
      > in order for correct the orienation bug, i.e. esGeneral not esDetails.

      Grr, that should have been fixed (the setter is correct however).

      > I’ve also noticed that 2 properties no longer exists:
      > 1) DirectlyPhotographed
      > 2) SourceIsDigitalCamera
      > Is this deliberate?

      Yes – please see Changes.rtf in the ZIP, or alternatively, the post on the front page of this site announcing the 0.9.5 revision (http://delphihaven.wordpress.com/2009/06/01/new-revision-of-ccr-exif-0-9-5/). Basically, you should use SceneType and FileSource respectively instead.

      That said, a new revision should be up soon-ish, mainly involving a partial rewrite of the saving code, though I’ll definitely roll into it (2), (3) and (4) too.

      • Chris,

        Just on the ExifVersion, I would have got 2.21 from your code had I not changed it, however I was attempting to reproduce the results from your previous version which provided a string like “0220″ or “0221″. Looking at the Exif specification 2.21 they say the default should be “0220″ and that is the same as is reported from Adobe Element 5.0.

        At the end of the day the format of the information doesn’t really matter so long as it remains consistent. I process all your properties if the TExifData class in a formatting function to provide string representation of all property which then get displayed on the resulting wall paper picture (beside the image). I can change my code to re-interrupt the result the way I need it to be formatted.

        On the other stuff, yes I should have read the exam question (changes.rft) first :-) before re-coding after a lousy day and with beer in my hand.

        regards
        Dave.

        • Dave — if you wish to verify the property setter is storing the data correctly, you need to go one or two levels down. So, remembering the Exif Sub-IFD is in my code’s jargon the ‘details section’:

          uses CCR.Exif, CCR.Exif.TagIDs;

          function GetExifVersionRaw(ExifData: TExifData): AnsiString;
          var
          Tag: TExifTag;
          begin
          if (ExifData[esDetails].Find(ttExifVersion, Tag)) and (Tag.DataType = tdUndefined) then
          SetString(Result, PAnsiChar(Tag.Data), Tag.ElementCount)
          else
          Result := ”;
          end;

          ‘At the end of the day the format of the information doesn’t really matter so long as it remains consistent.’

          To be clear, none of the properties on TCustomExifData should be considered to report the underlying data exactly as the bytes are stored on disk, even if this is contingently the case (and will continue to be the case) for many of them. That said, for the case at hand, if you want the ExifVersion as a string in a specific format, you can do it yourself easily enough –

          with ExifData.ExifVersion do
          MyStr := Format(‘%.2d%d%d’, [Major, Minor, Release]);

  9. Pingback: New revision of CCR Exif (0.9.6) « Delphi Haven

  10. Pingback: New revision of CCR Exif (0.9.7) « Delphi Haven

  11. Pingback: New revision of my Exif library (v0.9.8a) « Delphi Haven

  12. Pingback: New revision of my Exif library (v0.9.9) « Delphi Haven

  13. Pingback: Small update to my Exif library (now v1.0.1a) « Delphi Haven

  14. Pingback: New version of my Exif parsing code (v1.1.1) « Delphi Haven

  15. Pingback: Small revision of my Exif code (v1.1.2) « Delphi Haven

  16. Pingback: Metadaten von Bildern auslesen - Delphi-PRAXiS

Comments are closed.