New version of my Exif parsing code (v1.1.1)

I’ve put up a slight revision on my Exif and IPTC parsing code. The changes aren’t drastic —

  • Tweaked default behaviour of file name variants of TCustomExifData.SaveToJPEG and TIPTCData.SaveToJPEG to write to a memory stream first. This protects against the case of when the file exists, but is not a valid JPEG file (before, the file would just get wiped on the exception being raised).
  • Added EnsureEnumsInRange property to TCustomExifData. When True (the default), enumeration tag property values will definitely be in the declared range — if the stored value is otherwise, it will be reported as missing unless you get at the raw data directly.
  • Slightly modified TXMPPacket.TryLoadFromStream to support ExifTool’s not-quite-XMP XML dump format.

As before, it can be downloaded from CodeCentral here.

[Edit: now on Google Code here; ZIPs for people who don’t like Subversion here. Since this post was written, I’ve released v1.1.2 see here, where comments will remain open until the next release.]

Advertisements

34 thoughts on “New version of my Exif parsing code (v1.1.1)

  1. Hi. First of all, thanks for an excellent component. I’ve noticed using the JPegDump demo that most of my images include an “aux:” section which contains the model of lens used. Could you tell me how to access this? Is there a way to “search” for a section and extract the data from it?

    Many thanks and sorry if I’m being a bit blind to the obvious.

    • Ken —

      On the basis of a picture of Caernarfon Castle on your website, I can say it’s part of the XMP packet, rather than the actual Exif segment (if you scroll up a bit in the Jpeg Dump output memo, you should see the ‘— XMP —‘ headline).

      In code, you can access it either via the XMPPacket property of TJpegImageEx, or a standalone instance of TXMPPacket. Unfortunately, I don’t predefine the relevant schema URI (an XMP packet being made up of ‘schemas’ each identified by a distinct URI, the schemas themselves then being made up of ‘properties’). Nonetheless, you can write a little helper function —

      uses CCR.Exif.XMPUtils;
      
      function GetXMPLensID(XmpPacket: TXMPPacket): string;
      const
        ExifAuxURI = 'http://ns.adobe.com/exif/1.0/aux/';
      var
        Schema: TXMPSchema;
        Prop: TXMPProperty;
      begin
        if XmpPacket.FindSchema(ExifAuxURI, Schema) and Schema.FindProperty('LensID', Prop) then
          Result := Prop.ReadValue
        else
          Result := '';
      end;
      

      Then, in use —

      procedure TForm1.Button1Click(Sender: TObject);
      var
        XMPPacket: TXMPPacket;
      begin
        XMPPacket := TXMPPacket.Create;
        try
          XMPPacket.LoadFromJPEG('C:\Users\CCR\Documents\caernarfon_2008-05-03_10-55-59.jpg');
          ShowMessage(GetXMPLensID(XMPPacket));
        finally
          XMPPacket.Free;
        end;
      end;
      

      That said, I doubt your camera is writing this — instead, Photoshop is when you resave the JPEG. That in mind, it would be better to get the original data rather than the XMP copy, though I’m not sure where Photoshop is getting it from. It’s possible (probable?) it comes from the Exif maker note, but from the photos on your website at least, it appears Photoshop is removing any maker notes.

      • Many thanks for your quick response and for taking the trouble to delve into this. I think the original data is put in the EXIF data wherever the camera manufacturer fancies putting it (I used a PHP unit to look at Canon and Nikon raws and they don’t match up in this regard) so it’s easier let Photoshop or Lightroom figure it out.

        Thanks again for an excellent bit of coding and for solving my problem.

  2. Hi,

    First, I wanted to thank you for the great work you did on the EXIF code.
    Second, I have a small question: I am trying to use your “StripJPEGMetadata” example project to remove all the EXIF data from am image. The code passes well, and I get a message telling me the Exif data was remove, but when I look at the file, I see that all the data is still there.

    Do you have an idea why it happens?
    I can send you the image I am trying to remove the EXIF.

    Thanks,
    Guy.

    • Comment moved to here and replied by email. In short: by all means give me a sample image. In fact, that’s the best way by far.

    • OK, for anyone else coming across this: I’ve just uploaded a slightly revised version of CCR.Exif.JPEGUtils.pas to Google Code that (I hope) fixes the issue in question (direct link). Sample image at hand, this came down to very large Exif segments (in fact very large JPEG segments as such) not having their total size being computed correctly. Simple integer overflow issue…

    • I’m not going to at this moment in time, but if anyone (like your good self for example) wants to volunteer, feel free… The INI file is just an INI file, and the maker note type detection, while done in code, is extensible — you create a subclass of TExifMakerNote and register it by calling TExifData.RegisterMakerNoteType.

  3. Hi. I’m trying to read Microsofts People Tagging data that was added with the Live Photo Gallery 2011, and it seems the xmp packet does not have a correct format (as compared with what other pages say it should be). This is an example of the section:

    0.156250, 0.439583, 0.256250, 0.168750
    Lenora Crichlow

    0.346875, 0.172917, 0.253125, 0.170833

    0.584375, 0.464583, 0.259375, 0.170833
    Sinead Keenan

    I think the problem is that the bag items do not have a Description property, and thus are not recognized as structures. I’m guessing this is a bug in the MS product, but can you add some workaround in the component?

    Thx.

    • The xml may have been removed in the comment, tell me if you need any more info or to send it by email.

      • Posting XMP will work if you wrap it in appropriate ‘sourcecode’ tags (see http://en.support.wordpress.com/code/posting-source-code/).

        That said, on the substantive point, in the version of the library I’m working on now, I’ve rewritten the XMP loading code somewhat, which may have fixed the problem you’re having (‘may’ being the operative word though). Check out your inbox, assuming the email address you entered when posting your comments is real.

  4. Hi,

    is it possible to add Exif GPS Data to an Image? I try image.ExifData.GPSLongitude.Degrees := …
    but Delphi said it is read-only.

    Thanks and best regards,
    Steve

    • Steve — sure, just use the Assign methods of the GPSLatitude and GPSLongitude properties:

      with TExifData.Create do
      try
        LoadFromJPEG(ImageFile);
        GPSLatitude.Assign(51, 25, 32, ltNorth);
        GPSLongitude.Assign(0, 12, 29, lnEast);
        SaveToJPEG(ImageFile);
      finally
        Free;
      end;
      

      Note the slightly revised Google Code version (which is very slightly revised in the case of CCR.Exif.pas specifically) adds an extra overload or two here — in the Code Central version, you’ll need to call the overloads that take all TExifFraction values to set with more precision [Assign(TExifFraction.Create(Multiplier, Divisor), …].

  5. Hi,

    thanks for your fast answer. One last: I save the values as an extended with … myval := exif.GPSLatitude.Degrees.Quotient; . Is there a function for converting to the right (Longword?!) format for ExifData.GPSLatitude.Assign(myval, 25, 32, ltNorth);

    Best regards,
    Steve

    • Exif uses fractions in the form of a pair of unsigned 32 bit ints (LongWords in Delphi) rather than decimals, which is sensible really since it preserves accuracy. If you want to convert a decimal to a fraction, well that’s a general programming problem with a standard solution, insofar as a solution can be found (just Google or Bing it).

      That said, if you check out the Google Code version of my code (http://code.google.com/p/ccr-exif/source/browse/trunk/CCR.Exif.pas) you’ll see I’ve added overloads that take a Currency value for the Second parameter (the Currency type is really an integer under the hood, so no precision is lost in the conversion) —

      with TExifData.Create do
      try
        LoadFromJPEG(ImageFile);
        GPSLatitude.Assign(51, 25, 32.1, ltNorth); //secs now a decimal
        GPSLongitude.Assign(0, 12, 29.2, lnEast); //secs now a decimal
        SaveToJPEG(ImageFile);
      finally
        Free;
      end;
      

      The value is passed to a new constructor for TExifFraction that does the actual conversion from the decimal to the fraction.

      However, there is also another (and separate) issue: GPS coordinate values can be expressed in two different ways. If all you have is a single decimal value (rather than separate values for the degrees, minutes and seconds), then you’ll need to convert it to the other form first (see http://www.fcc.gov/mb/audio/bickel/DDDMMSS-decimal.html).

  6. Hi,
    Thanks for the great work you are doing.

    Is there a support of removing EXIF data from TIFF files? something like: “RemoveMetadataFromJPEG” and “RemoveJPEGSegments” but for TIIF files.

    • Hi Guy

      Thanks for the complement. WRT TIFF files, the short answer is ‘no’, though by commenting out a single line (no. 3898 to be exact in the Google Code version of CCR.Exif.pas), you get read (but not write or rewrite) support in TExifData (use LoadFromStream). The reason is that Exif metadata is in the format of a TIFF file, only with a small extra header and without image data. I am in fact looking at revising the code to support TIFF files properly, but rewriting TIFF files generically is a pain in the behind, precisely because of dodgy extensions to the format such as Exif…

        • I haven’t, no, but then my revised (private) code is mostly working now. ‘Mostly’ is the operative word though, which isn’t really enough for public assumption. Have you used ImageEn much yourself?

  7. hello,

    ccr exif raise an exception when it read the exif of some “good” jpeg image that can correctly be loaded in TJPEGImage. these jpeg was made under Mac

    the exception is raise here :

    procedure LoadTiffInfo(Stream: TStream; var Info: TTiffInfo);
    begin
    //[snip (CR) -- no need to copy and paste the whole method]
    

    exemple of such image:

    the code i use in my software :

      ExifData:= TExifData.Create;
      try
        ExifData.LoadFromJPEG(ImageJPGStream);
        ...
    

    thanks by advance
    stephane

    • Thanks for the sample. The short explanation of the exception is that the image contains a relevant ‘segment’ (i.e., data block) with the Exif header, but doesn’t actually contain any Exif data. If the JPEG were truly ‘good’, it wouldn’t claim to have Exif metadata when it doesn’t…

      That said, try the Google Code version of CCR.Exif.pas – it should fix your problem WRT reading at least (http://code.google.com/p/ccr-exif/source/browse/trunk/CCR.Exif.pas).

      • hmm, but i try already the google version … i just see that you just commit this code :

        function HasExifHeader(Stream: TStream; MovePosOnSuccess: Boolean = False): Boolean;
        var
        Buffer: array[0..4] of Word;
        BytesRead: Integer;
        begin
        Result := False;
        BytesRead := Stream.Read(Buffer, SizeOf(Buffer));
        if (BytesRead = SizeOf(Buffer)) and CompareMem(@Buffer, @ExifHeader, SizeOf(ExifHeader)) then
        case Buffer[3] of
        TiffSmallEndianCode, TiffBigEndianCode:
        Result := (Buffer[4] = TiffMagicNum) or (Buffer[4] = TiffMagicNumBigEndian);
        end;
        if Result and MovePosOnSuccess then
        Stream.Seek(-4, soCurrent)
        else
        Stream.Seek(-BytesRead, soCurrent)
        end;

        is this is the fix ?

        thanks by advance

        • also, in the modification you do

          if Result and MovePosOnSuccess then
          Stream.Seek(-4, soCurrent)
          else
          Stream.Seek(-BytesRead, soCurrent)

          are you sure you not mean

          if NOT MovePosOnSuccess then …

          because Stream.Seek(-4, soCurrent) look like you get back the pos to his orignal position !

          • “are you sure you not mean”

            I am perfectly sure.

            “because Stream.Seek(-4, soCurrent) look like you get back the pos to his orignal position !”

            No it doesn’t. If it did, why would I bother with the conditional in the first place, given the other code path is to unwind the stream by however many bytes were read? (Hint: compare the number of bytes the original version would read on a ‘success’.)

            More to the point, before rushing to rubbish the change, have you actually tried it?

        • “is this is the fix ?”

          Correct, once you understand this is a workaround for badly written JPEG files. Seriously though, there’s no need to paste huge chunks of code that I myself have written.

  8. Hi Chris,

    there are pictures (for example here: http://www.bertram-hafner.de/tmp/SDIM0003.jpg), which cause an error (“nicht genügend Arbeitsspeicher”, in English perhaps it is “out of memory”?) in this case:
    1. JpegImageEx.LoadFromFile(Filename)
    2. process the bitmap
    3. JpegImageEx.Assign(Bitmap, [jaPreserveMetaData])
    4. JpegImageEx.SaveToFile(Filename)

    Thanks,
    Bertram

Comments are closed.