Revised TJpegImageEx.SaveToStream

When coding TJpegImageEx.SaveToStream for CCR Exif, an implicit assumption of mine was that the image would be created by the underlying TJpegImage rather than loaded from a file, an assumption that leads to data corruption if the file does not have a JFIF segment like TJpegImage-authored ones do. Fixing this is easy enough though, I think — if I get no complaints, the following implementation will replace the current one in CCR Exif v0.9.6.

So, firstly, amend the ‘strict private’ section of TJpegImageEx to be as thus:

  strict private
    FChangedSinceLastLoad: Boolean; //as before
    FExifData: TExifData; //as before
    FOriginal: record
      ExifDataPos, ExifDataSize, JFIFDataPos, JFIFDataSize: Int64;
    end;
    procedure ParseJPEGHeaderCallback(MarkerNum: Byte; const PosOfDataInJpeg: Int64;
      Data: TMemoryStream; var ContinueParsing: Boolean);
    procedure ReloadExifData; //as before

Secondly, implement ParseJPEGHeaderCallback like this:

procedure TJpegImageEx.ParseJPEGHeaderCallback(MarkerNum: Byte;
  const PosOfDataInJpeg: Int64; Data: TMemoryStream; var ContinueParsing: Boolean);
begin
  case MarkerNum of
    jmJFIF:
    begin
      FOriginal.JFIFDataPos := PosOfDataInJpeg;
      FOriginal.JFIFDataSize := Data.Size;
    end;
    jmExif:
    begin
      FOriginal.ExifDataPos := PosOfDataInJpeg;
      FOriginal.ExifDataSize := Data.Size;
    end;
  end;
end;

And finally, edit SaveToStream to look like this:

procedure TJpegImageEx.SaveToStream(Stream: TStream);
const
  JpegFileMarkerSize = 2;
  SegmentHeaderSize = 4;
var
  ExifStream: TMemoryStream;
  FoundMarkers: TJPEGMarkers;
  MemStream: TMemoryStream;
  Pos: Int64;
begin
  if not FChangedSinceLastLoad or FExifData.Empty or Empty then
  begin
    inherited;
    Exit;
  end;
  MemStream := TMemoryStream.Create;
  try
    inherited SaveToStream(MemStream);
    MemStream.Position := 0;
    FoundMarkers := ParseJPEGHeader(MemStream, ParseJPEGHeaderCallback,
      [jmJFIF, jmExif], False) * [jmJFIF, jmExif]; //ensure return one of [], [jmJFIF], [jmExif] or [jmJFIF,jmExif]
    //send out everything up to where our Exif segment will be
    if jmExif in FoundMarkers then
      Stream.WriteBuffer(MemStream.Memory^, FOriginal.ExifDataPos - SegmentHeaderSize)
    else if FoundMarkers = [jmJFIF] then
      Stream.WriteBuffer(MemStream.Memory^, FOriginal.JFIFDataPos + FOriginal.JFIFDataSize)
    else
      Stream.WriteBuffer(MemStream.Memory^, JpegFileMarkerSize);
    //set up and send out the Exif segment
    ExifStream := TMemoryStream.Create;
    try
      FExifData.OnChange := nil;
      FExifData.ExifImageWidth := Width;
      FExifData.ExifImageHeight := Height;
      FExifData.SaveToStream(ExifStream);
      Stream.WriteByte(jmNewOrPadding);
      Stream.WriteByte(jmExif);
      Stream.WriteWord(ExifStream.Size + 2, BigEndian);
      Stream.WriteBuffer(ExifStream.Memory^, ExifStream.Size);
    finally
      FExifData.OnChange := Changed;
      ExifStream.Free;
    end;
    //send out the rest of the file
    if jmExif in FoundMarkers then
      Pos := FOriginal.ExifDataPos + FOriginal.ExifDataSize
    else if FoundMarkers = [jmJFIF] then
      Pos := FOriginal.JFIFDataPos + FOriginal.JFIFDataSize
    else
      Pos := JpegFileMarkerSize;
    Stream.WriteBuffer(PByteArray(MemStream.Memory)[Pos], MemStream.Size - Pos);
  finally
    MemStream.Free;
  end;
end;

The aim here is to replace the existing Exif segment if there is one; failing that, it is inserted after the existing JFIF segment; and if there isn’t a JFIF segment, it is inserted at the beginning, immediately after the JPEG file marker (which is where the Exif spec says it should go).

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s