Einzelnen Beitrag anzeigen

Benmik

Registriert seit: 11. Apr 2009
542 Beiträge
 
Delphi 11 Alexandria
 
#66

AW: JPG-Datei drehen und speichern -> Verlust der Exif-Daten

  Alt 19. Jul 2020, 22:20
Ich wollte diese GDIPlus-Sache abschließen und habe mich nochmal reingekniet. Dabei ist mir auch wieder eingefallen, dass es ja mit IGDIPlus von Boian Mitov eine neuere und bessere Portierung gibt. Die letzte Änderung ist zwar von 2016, aber das ist wesentlich neuer als Prodigy von 2002.

Dann habe ich nochmal "Lossless transform of a JPEG image" von Microsoft gründlicher gelesen. Dort steht klipp und klar, dass es verlustfrei nur geht, wenn man mit EncoderTransformation speichert. Die Lösung mit GPImage.RotateFlip(RotateType) ist also immer verlustbehaftet. Des Weiteren darf EncoderParameters nur ein einziges Objekt in seinem Array haben. Das war aber schon im bisherigen Code richtig drin.

Ausgehend davon habe ich eine Routine erstellt, die alle JPG-Dateien im Verzeichnis anhand des Orientation Tags verlustfrei dreht. Zudem setzt sie das Orientation Tag auf "1" zurück und - als Schmankerl - setzt sie das Dateidatum auf das EXIF-Datum zurück. Leider ist die Umwandlung des PPropDItem.Value zu einem TDateTime noch etwas rustikal (aber sie funktioniert). Vermutlich gibt es an einigen Stellen noch etwas zu meckern, aber funktionieren tut das Ding. Speicherlecks gibt es laut FastMM5 auch nicht.

Auch habe ich ein bisschen optimiert; manche Werte wie die Buffergrößen etc. müssen nur einmal ermittelt werden und nicht bei jedem Datei-Durchlauf. Zudem habe ich die Interface-Variante von IGDPImage verwendet, so dass es kein Free gibt.

Da die Prozedur dadurch sehr lang wurde und ich ja sowieso ein großer Fan von Unterprozeduren bin, habe ich den Spaghetticode in handgerechte Abschnitte unterteilt.

Auf meinem gegenwärtigen System (Ryzen 5 3400G) braucht die Prozedur für 40 Dateien mit 245 MB etwa 30 Sekunden, also etwa 750 msec für eine. Das ist jetzt nicht so toll, aber man dreht ja auch nicht so oft.

Im Explorer werden die Vorschaubilder immer richtig angezeigt. Ober dieses GDI+ jetzt tatsächlich auch die Vorschaubilder mitdreht oder der Explorer sie selber erstellt, weiß ich nicht, aber es scheint in der Praxis egal zu sein.
Delphi-Quellcode:
uses ... IGDIPlus;

procedure DreheMitIGDIPlus;
Var i:integer;
  Orientation: UInt32;
  EXIFDatum:TDateTime;
  DatListe:TStringDynArray;
  IGDPImage: IGPImage;
  PPropOItem,PPropDItem: PGPPropertyItem;
  PropBufferSize,DateBufferSize: Cardinal;
  DreheXGrad: TIGPEncoderValue;
  EncoderParams: TIGPEncoderParameters;
  EncoderTransformValue:integer;
  TempDatname:string;
  EncoderCLSID: TGUID;
//--------------------------------------------------------------------------------------------------------------------------------------------------------------
  procedure Initialisiere;
  begin
    PropBufferSize := 0;
    PPropOItem := nil;
    EncoderParams.Count := 1;
    EncoderParams.Parameter[0].Guid := EncoderTransformation;
    EncoderParams.Parameter[0].DataType := EncoderParameterValueTypeLong;
    EncoderParams.Parameter[0].NumberOfValues := 1;
    GetEncoderClsid('image/jpeg', EncoderCLSID);
  end;
//--------------------------------------------------------------------------------------------------------------------------------------------------------------
  function BufferGesetzt:Boolean;
  begin
    If (PropBufferSize > 0) and (DateBufferSize > 0)
      then exit(True);
    PropBufferSize := IGDPImage.GetPropertyItemSize(GPPropertyTagOrientation);
    If PropBufferSize > 0
      then GetMem(PPropOItem, PropBufferSize);
    DateBufferSize := IGDPImage.GetPropertyItemSize(GPPropertyTagDateTime);
    If DateBufferSize > 0
      then GetMem(PPropDItem, DateBufferSize);
    Result := (PropBufferSize > 0) and (DateBufferSize > 0);
  end;
//--------------------------------------------------------------------------------------------------------------------------------------------------------------
  function BestimmeRotation(var DreheXGrad:TIGPEncoderValue):Boolean;
  var Orientation: UInt32;
  begin
    IGDPImage.GetPropertyItem(GPPropertyTagOrientation, PropBufferSize, PPropOItem);
    Orientation := PUInt32(PPropOItem.value)^;
    Case Orientation of
      3: DreheXGrad := EncoderValueTransformRotate180;
      6: DreheXGrad := EncoderValueTransformRotate90;
      8: DreheXGrad := EncoderValueTransformRotate270;
      else DreheXGrad := TIGPEncoderValue(100); // zum Testen "else DreheXGrad := EncoderValueTransformRotate90;" , wenn man keine Hochkant-Bilder hat
    end;
    Result := (DreheXGrad in [EncoderValueTransformRotate90..EncoderValueTransformRotate270]);
  end;
//--------------------------------------------------------------------------------------------------------------------------------------------------------------
  function BestimmeEXIFDatum:TDateTime;
  var DateOrig: array of Byte; TxtEXIFDatum:string;
  begin
    IGDPImage.GetPropertyItem(GPPropertyTagDateTime, DateBufferSize, PPropDItem);
    SetLength(DateOrig,DateBufferSize);
    Move(PPropDItem.Value,DateOrig[1],DateBufferSize);
    TxtEXIFDatum := Copy(TEncoding.ASCII.GetString(DateOrig),6,19); // Das ist nicht der Weisheit letzter Schluss
    Result :=
      EncodeDate(StrToInt(Copy(TxtEXIFDatum, 1, 4)),StrToInt(Copy(TxtEXIFDatum, 6, 2)),StrToInt(Copy(TxtEXIFDatum, 9, 2))) +
      EncodeTime(StrToInt(Copy(TxtEXIFDatum, 12, 2)),StrToInt(Copy(TxtEXIFDatum, 15, 2)),StrToInt(Copy(TxtEXIFDatum, 18, 2)), 0);
  end;
//--------------------------------------------------------------------------------------------------------------------------------------------------------------
  function SpeichereJPG:Boolean;
  begin
    Result := False;
    TempDatname := TPath.ChangeExtension(DatListe[i],'$$$');
    If TFile.Exists(TempDatname)
      then exit;
    IGDPImage.Save(WideString(TempDatname),EncoderCLSID,@EncoderParams);
    IGDPImage := nil;
    If TFile.Exists(TempDatname) then begin
      Result := DeleteFile(DatListe[i]);
      If Result
        then Result := RenameFile(TempDatname,DatListe[i])
        else DeleteFile(TempDatname);
    end;
  end;
//--------------------------------------------------------------------------------------------------------------------------------------------------------------
  function SetzeDatumAufEXIF(Datname:string;EXIFDatum:TDateTime):Boolean;
  begin
    Result := True;
    Try
      TFile.SetCreationTime(Datname,EXIFDatum);
      TFile.SetLastWriteTime(Datname,EXIFDatum);
    Except
      Result := False;
    End;
  end;
//--------------------------------------------------------------------------------------------------------------------------------------------------------------
  procedure RäumeAuf;
  begin
    FreeMem(PPropOItem);
    FreeMem(PPropDItem);
    IGDPImage := nil;
  end;
//--------------------------------------------------------------------------------------------------------------------------------------------------------------
begin
  Initialisiere;
  Try
    DatListe := TDirectory.GetFiles(Verz,'*.jpg');
    For i := 0 to High(Datliste) do begin // zum Testen auf "0 to 0" setzen
      IGDPImage := TIGPImage.Create(DatListe[i]);
      If not BufferGesetzt
        then exit;
      EXIFDatum := BestimmeEXIFDatum;
      If BestimmeRotation(DreheXGrad) then begin
        EncoderTransformValue := Ord(DreheXGrad);
        EncoderParams.Parameter[0].Value := @EncoderTransformValue;
        Orientation := 1; // zum Testen z.B. auf 3 setzen, wenn man keine Hochkant-Bilder hat
        PPropOItem.Value := @Orientation;
        IGDPImage.SetPropertyItem(PPropOItem^);
        If not SpeichereJPG
          then exit;
        SetzeDatumAufEXIF(DatListe[i],EXIFDatum);
      end;
      IGDPImage := nil;
    end;
  Finally
    RäumeAuf;
  end;
end;

Geändert von Benmik (19. Jul 2020 um 22:24 Uhr)
  Mit Zitat antworten Zitat