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).