fcl-image/pasjpeg: handle Exif orientation flag automatically

(cherry picked from commit 05c45486e8)
This commit is contained in:
Ondrej Pokorny 2022-10-26 13:36:50 +02:00 committed by marcoonthegit
parent 0782e1832c
commit d261c00571
7 changed files with 351 additions and 8 deletions

View File

@ -24,7 +24,7 @@ unit FPReadJPEG;
interface
uses
Classes, SysUtils, FPImage, JPEGLib, JdAPImin, JDataSrc, JdAPIstd, JmoreCfg;
Classes, SysUtils, Types, FPImage, JPEGLib, JdAPImin, JDataSrc, JdAPIstd, JmoreCfg;
type
{ TFPReaderJPEG }
@ -79,6 +79,12 @@ type
implementation
type
TExifOrientation = ( // all angles are clockwise
eoUnknown, eoNormal, eoMirrorHor, eoRotate180, eoMirrorVert,
eoMirrorHorRot270, eoRotate90, eoMirrorHorRot90, eoRotate270
);
procedure ReadCompleteStreamToStream(SrcStream, DestStream: TStream;
StartSize: integer);
var
@ -166,6 +172,61 @@ end;
procedure TFPReaderJPEG.InternalRead(Str: TStream; Img: TFPCustomImage);
var
MemStream: TMemoryStream;
Orientation: TExifOrientation;
function TranslatePixel(const Px: TPoint): TPoint;
begin
case Orientation of
eoUnknown, eoNormal: Result := Px;
eoMirrorHor:
begin
Result.X := FInfo.output_width-1-Px.X;
Result.Y := Px.Y;
end;
eoRotate180:
begin
Result.X := FInfo.output_width-1-Px.X;
Result.Y := FInfo.output_height-1-Px.Y;
end;
eoMirrorVert:
begin
Result.X := Px.X;
Result.Y := FInfo.output_height-1-Px.Y;
end;
eoMirrorHorRot270:
begin
Result.X := Px.Y;
Result.Y := Px.X;
end;
eoRotate90:
begin
Result.X := FInfo.output_height-1-Px.Y;
Result.Y := Px.X;
end;
eoMirrorHorRot90:
begin
Result.X := FInfo.output_height-1-Px.Y;
Result.Y := FInfo.output_width-1-Px.X;
end;
eoRotate270:
begin
Result.X := Px.Y;
Result.Y := FInfo.output_width-1-Px.X;
end;
end;
end;
function TranslateSize(const Sz: TSize): TSize;
begin
case Orientation of
eoUnknown, eoNormal, eoMirrorHor, eoMirrorVert, eoRotate180: Result := Sz;
eoMirrorHorRot270, eoRotate90, eoMirrorHorRot90, eoRotate270:
begin
Result.Width := Sz.Height;
Result.Height := Sz.Width;
end;
end;
end;
procedure SetSource;
begin
@ -174,10 +235,19 @@ var
end;
procedure ReadHeader;
var
S: TSize;
begin
jpeg_read_header(@FInfo, TRUE);
FWidth := FInfo.image_width;
FHeight := FInfo.image_height;
if FInfo.saw_EXIF_marker and (FInfo.orientation >= Ord(Low(TExifOrientation))) and (FInfo.orientation <= Ord(High(TExifOrientation))) then
Orientation := TExifOrientation(FInfo.orientation)
else
Orientation := Low(TExifOrientation);
S := TranslateSize(TSize.Create(FInfo.image_width, FInfo.image_height));
FWidth := S.Width;
FHeight := S.Height;
FGrayscale := FInfo.jpeg_color_space = JCS_GRAYSCALE;
FProgressiveEncoding := jpeg_has_multiple_scans(@FInfo);
end;
@ -257,6 +327,14 @@ var
Result.alpha:=alphaOpaque;
end;
procedure ReadPixels;
procedure SetPixel(x, y: integer; const C: TFPColor);
var
P: TPoint;
begin
P := TPoint.Create(x,y);
P := TranslatePixel(P);
Img.Colors[P.x, P.y] := C;
end;
var
Continue: Boolean;
SampArray: JSAMPARRAY;
@ -287,7 +365,7 @@ var
Color.Green:=SampRow^[x*4+1];
Color.Blue:=SampRow^[x*4+2];
Color.alpha:=SampRow^[x*4+3];
Img.Colors[x,y]:=CorrectCMYK(Color);
SetPixel(x, y, CorrectCMYK(Color));
end
else
if (FInfo.jpeg_color_space = JCS_YCCK) then
@ -296,7 +374,7 @@ var
Color.Green:=SampRow^[x*4+1];
Color.Blue:=SampRow^[x*4+2];
Color.alpha:=SampRow^[x*4+3];
Img.Colors[x,y]:=CorrectYCCK(Color);
SetPixel(x, y, CorrectYCCK(Color));
end
else
if fgrayscale then begin
@ -305,7 +383,7 @@ var
Color.Red:=c;
Color.Green:=c;
Color.Blue:=c;
Img.Colors[x,y]:=Color;
SetPixel(x, y, Color);
end;
end
else begin
@ -313,7 +391,7 @@ var
Color.Red:=SampRow^[x*3+0] shl 8;
Color.Green:=SampRow^[x*3+1] shl 8;
Color.Blue:=SampRow^[x*3+2] shl 8;
Img.Colors[x,y]:=Color;
SetPixel(x, y, Color);
end;
end;
inc(y);
@ -328,7 +406,7 @@ var
jpeg_start_decompress(@FInfo);
Img.SetSize(FInfo.output_width,FInfo.output_height);
Img.SetSize(FWidth,FHeight);
GetMem(SampArray,SizeOf(JSAMPROW));
GetMem(SampRow,FInfo.output_width*FInfo.output_components);

View File

@ -90,12 +90,25 @@ const { JPEG marker codes }
M_ERROR = $100;
EXIF_TAG_PRIMARY = UINT32(1);
EXIF_TAGPARENT_PRIMARY = UINT32(EXIF_TAG_PRIMARY shl 16); // $00010000;
type
JPEG_MARKER = uint; { JPEG marker codes }
{ Private state }
type
jpeg_exif_ifd_record = packed record
tag_id: UINT16;
data_type: UINT16;
data_count: UINT32;
data_value: UINT32;
end;
{ Routine signature for application-supplied exif processing method. }
jpeg_exif_parser_method = function(cinfo : j_decompress_ptr; ifdRec: jpeg_exif_ifd_record; bigEndian: boolean; data: array of JOCTET; parent_tag_id: UINT32): boolean;
my_marker_ptr = ^my_marker_reader;
my_marker_reader = record
pub : jpeg_marker_reader; { public fields }
@ -104,6 +117,8 @@ type
process_COM : jpeg_marker_parser_method;
process_APPn : array[0..16-1] of jpeg_marker_parser_method;
handle_exif_tag : jpeg_exif_parser_method;
{ Limit on marker data length to save for each marker type }
length_limit_COM : uint;
length_limit_APPn : array[0..16-1] of uint;
@ -1487,6 +1502,7 @@ end; { get_dri }
const
APP0_DATA_LEN = 14; { Length of interesting data in APP0 }
APP1_HEADER_LEN = 14; { Length of data header in APP1 }
APP14_DATA_LEN = 12; { Length of interesting data in APP14 }
APPN_DATA_LEN = 14; { Must be the largest of the above!! }
@ -1582,6 +1598,195 @@ begin
end;
{LOCAL}
function handle_exif_marker(cinfo : j_decompress_ptr; ifdRec: jpeg_exif_ifd_record; bigEndian: boolean; data: array of JOCTET; parent_tag_id: UINT32): Boolean;
function FixEndian16(Value: UINT16): UINT16;
begin
if BigEndian then
Result := BEtoN(Value)
else
Result := LEtoN(Value);
end;
const
EXIF_TAG_ORIENTATION = EXIF_TAGPARENT_PRIMARY or $0112;
var
i: Integer;
orientation: UINT16;
begin
case (ifdRec.tag_id or parent_tag_id) of
EXIF_TAG_ORIENTATION:
begin
if Length(data)=SizeOf(UINT16) then
begin
move(data[0], orientation, Length(data));
cinfo^.orientation := FixEndian16(orientation);
end else
Exit(False);
end;
end;
Result := True;
end;
{LOCAL}
function examine_app1 (cinfo : j_decompress_ptr;
var header : array of JOCTET;
headerlen : uint;
var remaining : INT32;
datasrc : jpeg_source_mgr_ptr;
var next_input_byte : JOCTETptr;
var bytes_in_buffer : size_t): Boolean;
{ Read Exif marker.
headerlen is # of bytes at header[], remaining is length of rest of marker header.
}
var
BigEndian: Boolean;
Offset: UINT32;
const
TagElementSize: array[1..13] of Integer = (1, 1, 2, 4, 8, 1, 1, 2, 4, 8, 4, 8, 4);
function FixEndian16(Value: UINT16): UINT16;
begin
if BigEndian then
Result := BEtoN(Value)
else
Result := LEtoN(Value);
end;
function FixEndian32(Value: UINT32): UINT32;
begin
if BigEndian then
Result := BEtoN(Value)
else
Result := LEtoN(Value);
end;
function Read(const Buffer: Pointer; numtoread: uint): Boolean;
var
i: UINT32;
begin
Result := False;
if numtoread=0 then
Exit;
for i := 0 to numtoread-1 do
begin
{ if the offset is less than headerlen, there were more bytes read into the header than necessary }
{ this can happen when APPN_DATA_LEN>APP1_HEADER_LEN (e.g. due to a future source code change }
{ first read the bytes from header }
if Offset<headerlen then
begin
PByte(Buffer)[i] := header[Offset];
Inc(Offset);
end else
begin
{ Read a byte into b[i]. If must suspend, return FALSE. }
{ make a byte available.
Note we do *not* do INPUT_SYNC before calling fill_input_buffer,
but we must reload the local copies after a successful fill. }
if (bytes_in_buffer = 0) then
begin
if (not datasrc^.fill_input_buffer(cinfo)) then
exit(False);
{ Reload the local copies }
next_input_byte := datasrc^.next_input_byte;
bytes_in_buffer := datasrc^.bytes_in_buffer;
end;
Dec( bytes_in_buffer );
PByte(Buffer)[i] := GETJOCTET(next_input_byte^);
Inc(next_input_byte);
Dec(remaining);
end;
end;
Result := True;
end;
function Read16(out Value: UINT16): Boolean;
begin
Result := Read(@Value, SizeOf(UINT16));
if Result then
Value := FixEndian16(Value);
end;
var
Signature, numRecords: UINT16;
i, byteCount: UINT32;
ifdRec: jpeg_exif_ifd_record;
data: array of JOCTET;
begin
if (headerlen >= APP1_HEADER_LEN) and
(GETJOCTET(header[0]) = Ord('E')) and
(GETJOCTET(header[1]) = Ord('x')) and
(GETJOCTET(header[2]) = Ord('i')) and
(GETJOCTET(header[3]) = Ord('f')) and
(GETJOCTET(header[4]) = 0) and
(GETJOCTET(header[5]) = 0) then
begin
// Tiff header
if (GETJOCTET(header[6]) = Ord('M')) and
(GETJOCTET(header[7]) = Ord('M'))
then
BigEndian := True
else
if (GETJOCTET(header[6]) = Ord('I')) and
(GETJOCTET(header[7]) = Ord('I'))
then
BigEndian := False
else
Exit; // invalid
Signature := FixEndian16(GETJOCTET(header[8]) or (GETJOCTET(header[9]) shl 8));
if Signature<>42 then
Exit;
Offset := FixEndian32(GETJOCTET(header[10]) or (GETJOCTET(header[11]) shl 8) or (GETJOCTET(header[12]) shl (8*2)) or (GETJOCTET(header[13]) shl (8*3)));
Inc(Offset, 6); // get over Exif header
{ Found JFIF APP0 marker: save info }
cinfo^.saw_EXIF_marker := TRUE;
{ skip offset bytes }
if Offset>headerlen then
begin
datasrc^.next_input_byte := next_input_byte;
datasrc^.bytes_in_buffer := bytes_in_buffer;
cinfo^.src^.skip_input_data(cinfo, long(Offset-headerlen));
next_input_byte := datasrc^.next_input_byte;
bytes_in_buffer := datasrc^.bytes_in_buffer;
end;
// read data
if not Read16(numRecords) then
Exit(False);
for i:=1 to numRecords do
begin
if not Read(@ifdRec, SizeOf(jpeg_exif_ifd_record)) then
Exit;
if (ifdRec.tag_id = 0) and (ifdRec.data_type = 0) and (ifdRec.data_count = 0) and (ifdRec.data_value = 0) then // nothing to read
Continue;
if (ifdRec.tag_id = 0) and (ifdRec.data_type = 0) then // Unexpected end of directory (4 zero bytes), so breaking here.
Break;
ifdRec.tag_id := FixEndian16(ifdRec.tag_id);
ifdRec.data_type := FixEndian16(ifdRec.data_type);
ifdRec.data_count := FixEndian32(ifdRec.data_count);
byteCount := Integer(ifdRec.data_count) * TagElementSize[ifdRec.data_type];
if byteCount>0 then
begin
SetLength(data, bytecount);
if byteCount <= 4 then
begin
Move(ifdRec.data_value, data[0], byteCount)
end else
begin
//ToDo read at position ifdRec.data_value
continue; // for now ignore the tag
end;
my_marker_ptr(cinfo^.marker)^.handle_exif_tag(cinfo, ifdRec, BigEndian, data, EXIF_TAGPARENT_PRIMARY);
end;
end;
end;
end;
{LOCAL}
procedure examine_app14 (cinfo : j_decompress_ptr;
var data : array of JOCTET;
@ -1727,6 +1932,8 @@ begin
case (cinfo^.unread_marker) of
M_APP0:
examine_app0(cinfo, b, numtoread, length);
M_APP1:
examine_app1(cinfo, b, numtoread, length, datasrc, next_input_byte, bytes_in_buffer);
M_APP14:
examine_app14(cinfo, b, numtoread, length);
else
@ -2567,7 +2774,9 @@ begin
marker^.length_limit_APPn[i] := 0;
end;
marker^.process_APPn[0] := get_interesting_appn;
marker^.process_APPn[1] := get_interesting_appn;
marker^.process_APPn[14] := get_interesting_appn;
marker^.handle_exif_tag := handle_exif_marker;
{ Reset marker processing state }
reset_marker_reader(cinfo);
end; { jinit_marker_reader }

View File

@ -1200,6 +1200,9 @@ type
saw_Adobe_marker : boolean; { TRUE iff an Adobe APP14 marker was found }
Adobe_transform : UINT8; { Color transform code from Adobe marker }
saw_EXIF_marker : boolean; { TRUE if an Exif APP1 marker was found }
orientation : UINT16; { Exif orientation value }
CCIR601_sampling : boolean; { TRUE=first samples are cosited }
{ Aside from the specific data retained from APPn markers known to the

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -0,0 +1,2 @@
dots_5 RCDATA "dots-5.jpg"
dots_8 RCDATA "dots-8.jpg"

View File

@ -0,0 +1,51 @@
program timage_jpegorientation;
{$mode objfpc}{$H+}
{$R dots.rc}
uses
FPReadJPEG, FPImage, Classes, resource;
var
Bmp: TFPCompactImgRGBA8Bit;
S: TResourceStream;
Reader: TFPReaderJPEG;
function CheckColor(x, y: integer; r, g, b: Word): Boolean;
begin
Result := (Byte(Bmp.Colors[x, y].Red)=r) and (Byte(Bmp.Colors[x, y].Green)=g) and (Byte(Bmp.Colors[x, y].Blue)=b);
if not Result then
Writeln(Byte(Bmp.Colors[x, y].Red), ':', Byte(Bmp.Colors[x, y].Green), ':', Byte(Bmp.Colors[x, y].Blue));
end;
begin
Bmp := TFPCompactImgRGBA8Bit.Create(0, 0);
Reader := TFPReaderJPEG.Create;
S := TResourceStream.Create(HINSTANCE, 'dots_5', {$ifdef FPC_OS_UNICODE}PWideChar{$else}PChar{$endif}(RT_RCDATA));
Bmp.LoadFromStream(S, Reader);
if not CheckColor(0, 0, 0, 0, 254) then
Halt(1);
if not CheckColor(1, 0, 0, 255, 0) then
Halt(2);
if not CheckColor(0, 1, 255, 255, 0) then
Halt(3);
if not CheckColor(1, 1, 254, 0, 0) then
Halt(4);
S.Free;
S := TResourceStream.Create(HINSTANCE, 'dots_8', {$ifdef FPC_OS_UNICODE}PWideChar{$else}PChar{$endif}(RT_RCDATA));
Bmp.LoadFromStream(S, Reader);
if not CheckColor(0, 0, 255, 255, 0) then
Halt(5);
if not CheckColor(1, 0, 254, 0, 0) then
Halt(6);
if not CheckColor(0, 1, 0, 0, 254) then
Halt(7);
if not CheckColor(1, 1, 0, 255, 0) then
Halt(8);
S.Free;
Bmp.Free;
Reader.Free;
end.