mirror of
https://gitlab.com/freepascal.org/fpc/source.git
synced 2025-04-19 14:19:31 +02:00
fcl-image/pasjpeg: handle Exif orientation flag automatically
(cherry picked from commit 05c45486e8
)
This commit is contained in:
parent
0782e1832c
commit
d261c00571
@ -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);
|
||||
|
@ -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 }
|
||||
|
@ -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
|
||||
|
BIN
tests/test/packages/fcl-image/dots-5.jpg
Normal file
BIN
tests/test/packages/fcl-image/dots-5.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 11 KiB |
BIN
tests/test/packages/fcl-image/dots-8.jpg
Normal file
BIN
tests/test/packages/fcl-image/dots-8.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 11 KiB |
2
tests/test/packages/fcl-image/dots.rc
Normal file
2
tests/test/packages/fcl-image/dots.rc
Normal file
@ -0,0 +1,2 @@
|
||||
dots_5 RCDATA "dots-5.jpg"
|
||||
dots_8 RCDATA "dots-8.jpg"
|
51
tests/test/packages/fcl-image/timage_jpegorientation.pp
Normal file
51
tests/test/packages/fcl-image/timage_jpegorientation.pp
Normal 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.
|
||||
|
Loading…
Reference in New Issue
Block a user