Procházet zdrojové kódy

fcl-image/pasjpeg: handle Exif orientation flag automatically

Ondrej Pokorny před 2 roky
rodič
revize
05c45486e8

+ 86 - 8
packages/fcl-image/src/fpreadjpeg.pas

@@ -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);

+ 209 - 0
packages/pasjpeg/src/jdmarker.pas

@@ -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 }

+ 3 - 0
packages/pasjpeg/src/jpeglib.pas

@@ -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ární
tests/test/packages/fcl-image/dots-5.jpg


binární
tests/test/packages/fcl-image/dots-8.jpg


+ 2 - 0
tests/test/packages/fcl-image/dots.rc

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

+ 51 - 0
tests/test/packages/fcl-image/timage_jpegorientation.pp

@@ -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.
+