Delphi-PRAXiS

Delphi-PRAXiS (https://www.delphipraxis.net/forum.php)
-   Multimedia (https://www.delphipraxis.net/16-multimedia/)
-   -   Delphi TPicture, TJPegImage, TBitmap, TBitmap32 und Threads ... (https://www.delphipraxis.net/202085-tpicture-tjpegimage-tbitmap-tbitmap32-und-threads.html)

Gausi 26. Sep 2019 09:54

TPicture, TJPegImage, TBitmap, TBitmap32 und Threads ...
 
Vorab: Crossposting EE.

Mein Grundproblem ist das nicht-thread-sichere TBitmap. Also "nicht threadsicher" in dem Sinne, dass man TBitmap eigentlich überhaupt nicht in Threads abseits vom VCL-Thread nutzen sollte. Problematisch ist da wohl vor allem der Zugriff auf Canvas, der auf globale Konstrukte zurückgreift und daher absolut unsafe ist.

Als Alternative soll TBitmap32 aus der Sammlung Graphics32 threadsafe sein.

Soweit, so gut. Jetzt möchte ich aber im Kontext eines Threads Bilder laden (meistens Jpegs, gelegentlich PNGs), und diese verkleinert anderswo abspeichern - als Jpeg. Eine Klasse TJpegImage32 gibt es in dieser Sammlung nicht, und das übliche TJpegImage soll auch nicht threadsafe sein. Blöd.

Wenn man aber in den Code von TBitmap32 schaut, dann findet man da z.B. auch sowas
Delphi-Quellcode:
procedure TCustomBitmap32.LoadFromFile(const FileName: string);
var P: TPicture;
begin
   /// [ ... ]
  // if we got here, use the fallback approach via TPicture...
  P := TPicture.Create;
  try
    P.LoadFromFile(FileName);
    Assign(P);
  finally
    P.Free;
  end;
end;
Damit ist man über TPicture ja voll drin in den ganzen Nicht-Threadsicheren TGraphic-Klassen.

Bei der SaveToFile-Methode von TBitmap32 steht dann auch in der Doku, dass man für andere Formate als 32-Bit-Bitmaps eine entsprechende Ableitung von TGraphic nutzen soll ....

Oder heißt das, dass ich im sicheren Bereich bin, solange ich von den "alten" Klassen nur Assign (in beide Richtungen) und Load/Save-From/To-File/Stream nutze, und den ganzen Manipulationscode (verkleinern) über TBitmap32 laufen lasse?

Einfach testen und gucken, obs läuft, ist dabei ja nicht so eine gute Idee. Bei Threads ist das ja generell nicht vernünftig. Erschwerend kommt hinzu, dass mein eigentlich unsafer TBitmap-Code aktuell gut durchläuft. 15 Minuten rödeln, dabei 5000 Bilder umskalieren, während auf der Mainform auch fröhlich gemalt wird - kein Problem. Aber dem Braten trau ich nicht. :roll:

Und über einen Hintergrundthread ist das schon schöner, denn die Mainform bleibt dabei deutlich flüssiger bedienbar ...

Weiß da jemand mehr Bescheid? Oder hat einen guten Link zur Hand?

TiGü 26. Sep 2019 16:12

AW: TPicture, TJPegImage, TBitmap, TBitmap32 und Threads ...
 
Wenn du nur unter Windows unterwegs bist, dann könntest du auch zu Fuss per Windows Imaging Component alles erledigen:
https://docs.microsoft.com/en-us/win...32/wic/-wic-lh

Es gibt zwar ein TWICImage in Delphi, ich würde an deiner Stelle aber alles zu Fuß machen.

Weiterer Lesetipp-Link: https://www.delphipraxis.net/1282736-post7.html

Gausi 27. Sep 2019 09:17

AW: TPicture, TJPegImage, TBitmap, TBitmap32 und Threads ...
 
Nicht ganz die Antwort, die ich haben wollte, da ich dann wieder den Code umschreiben müsste, aber gut. :lol:

Den Code-Fetzen von Sherlock habe ich schon an anderer Stelle gefunden - da ging es bei mir um Alpha-Transparenzen beim Resize, was mit StretchBlt nicht unterstützt wird. Wäre also vielleicht gar keine so schlechte Idee.

Was spricht denn deiner Ansicht nach gegen TWICImage? Konzeptionelle Dinge wie "Probleme mit Threads", oder nur das übliche wie Overhead durch die Klassen-Kapselung und ggf. eingeschränkte Möglichkeiten (beim kurzen googlen z.B. fehlende Unterstützung für CompressionQuality)?

TiGü 27. Sep 2019 10:11

AW: TPicture, TJPegImage, TBitmap, TBitmap32 und Threads ...
 
Kurze Antwort: Alles drei!

1. Threadsicherheit: TWICImage liegt in der Vcl.Graphics Unit und damit im Vcl Namespace -> per se nicht threadsicher.
Benutzt intern TBitmap und TCanvas. Sie Implementierung: Für AssignTo wenn
Delphi-Quellcode:
Dest is TBitmap
, zur Übergabe an die Zwischenablage und zum Zeichnen.
Des Weiteren ist die
Delphi-Quellcode:
class var FImagingFactory: IWICImagingFactory;
nicht darauf ausgelegt bzw. der Getter dazu und ich würde mich nicht darauf verlassen.

2.Overhead. Das was du brauchst ist eine Threadeigene-Instanz der Factory (im Execute erzeugen!) und dann in einer Schleife deine Dateiliste abarbeiten und verkleinern.
Das SetSize führt bspw. bei TWICImage zu einer Exception, dass das nicht implementiert wurde (First chance exception at $758B3522. Exception class EInvalidGraphicOperation with message 'Cannot change the size of a WIC Image'. Process Project1.exe (12164))
Also sowas geht halt nicht:
Delphi-Quellcode:
procedure TForm1.FormCreate(Sender: TObject);
var
    MyProc: TProc;
begin
    MyProc := procedure
        var
            WicImage: TWICImage;
        begin
          CoInitializeEx(nil, COINIT_MULTITHREADED);
          WicImage := TWICImage.Create;
          WicImage.LoadFromFile('C:\Users\Du\Desktop\DeinBild.png');
          WicImage.SetSize(WicImage.Width div 2, WicImage.Height div 2);
          WicImage.SaveToFile('C:\Users\Du\Desktop\DeinBild2.png');
          WicImage.Free;
          CoUninitialize;
        end;
    TThread.CreateAnonymousThread(MyProc).Start;
end;
Dann kannst du dir den Code dafür selber schnell zusammenschreiben:
https://docs.microsoft.com/en-us/win...es-howto-scale

3. Eben, wenn du irgendwelche Sonderlocken hast, dann musst du die eh per Zugriff auf TWicImage.Handle und WicImage.ImagingFactory herzaubern. Dann kann man das auch gleich direkt machen.

Gausi 27. Sep 2019 14:26

AW: TPicture, TJPegImage, TBitmap, TBitmap32 und Threads ...
 
Ok, danke nochmal. Dann werde ich mich da mal reinfuxen. WinAPI-nahe Programmierung liegt mir nicht so, und C in Delphi übersetzen ist auch nicht so meine Stärke. Aber mit Querlesen der MS-Doku und dem Code von TWICImage werde ich da schon klar kommen, hoffe ich. So umfangreich ist mein Vorhaben ja nicht an der Stelle. :-D

Und dann kann ich mir auch die externe Bib (graphics32) im Code sparen ...

Edit: Sehr schön. Ich habe jetzt Quick&Dirty Code, der "funktioniert". Eingabe ist ein Stream mit Bilddaten (egal ob PNG, JPEG oder Bitmap), und Ausgabe ist eine auf die Zielgröße reduzierte JPEG-Datei. Stream als Eingabe passt mir da sehr gut, weil ich nicht immer nur Bilddateien skalieren möchte, sondern auch mal Bilder, die ich aus einem Container-Dateiformat raushole.

Muss ich morgen nochmal in Ruhe drübergucken, den Code ggf. etwas aufräumen und Fehlerbehandlung einbauen. Poste ich dann auch mal, damit es zu dem Problem ein weiteres Code-Beispiel gibt. :-D

Gausi 28. Sep 2019 20:16

AW: TPicture, TJPegImage, TBitmap, TBitmap32 und Threads ...
 
Hier jetzt mein aktueller Code dazu.

Sieht furchtbar lang aus, ist aber so ziemlich genau das, was auch beim TWICImage beim Laden und Speichern alles so gemacht wird, und was die WIC halt so verlangt. Besonders die Variablenliste kann einen erstmal schocken. :lol:

Zur Erklärung:
Parameter
  • aStream: Ein Stream mit Bilddaten (FileStream von einer Bilddatei, MemoryStream mit Bilddaten aus einem ID3-Tag, ...)
  • aFilename: Dateiname der Zieldatei (wird ggf. erstellt)
  • destWidth/destHeight: Zielgröße des Bildes. Das Originalbild wird so skaliert, dass es in das Rechteck destWidth*destHeight hineinpasst
  • aWICImagingFactory: eine WICImagingFactory, damit diese bei vielen Skalierungen nicht immer neu erstellt werden muss. Bei NIL wird eine lokale neu erzeugt.
  • Overwrite: Flag, das in meiner Anwendung gelegentlich gebraucht wird. Damit werden bereits vorhandene Dateien überschrieben, ansonsten wird abgebrochen - aber trotzdem "Erfolg" zurückgeliefert. Das ist in meinem Anwendungsfall so sinnvoll ;-)
Ausgabe:
  • True, falls das skalierte Bild erfolgreich erstellt wurde (oder ggf. bereits existiert)
Das Bildformat wird automatisch aus dem Stream ermittelt. Das Ausgabeformat ist immer JPEG.
Eine ggf. vorhandene WICImagingFactory muss im Kontext des Threads erzeugt werden, in dem die Funktion laufen soll.

Fehlerbehandlung könnte intensiver sein, und ein Rückgabewert mit mehr Info als "hat geklappt" wäre ggf. auch sinnvoll. Das ist dann aber dem geneigten Leser zur Übung überlassen.

Aber das sollte dann so ziemlich Threadsafe sein, komplett ohne VCL und TGraphic.

Delphi-Quellcode:
function ScalePicStreamToFile(aStream: TStream; aFilename: UnicodeString; destWidth, destHeight: Integer; aWICImagingFactory: IWICImagingFactory; OverWrite: Boolean = False): boolean;
var
    hr: HRESULT;
    isLocalFactory: Boolean;
    // for proper scaling
    xfactor, yfactor:double;
    origWidth, origHeight: Cardinal;
    newWidth, newHeight: Cardinal;
    // reading the source image
    SourceAdapter: IStream;
    BitmapDecoder: IWICBitmapDecoder;
    DecodeFrame: IWICBitmapFrameDecode;
    SourceBitmap: IWICBitmap;
    SourceScaler: IWICBitmapScaler;
    // writing the resized image
    DestStream: TMemoryStream;
    DestAdapter: IStream;
    DestWICStream: IWICStream;
    BitmapEncoder: IWICBitmapEncoder;
    EncodeFrame: IWICBitmapFrameEncode;
    Props: IPropertyBag2;
begin
    result := False;
    if Not Overwrite and FileExists(aFilename) then
    begin
        result := True;
        exit;
    end;

    isLocalFactory := (aWICImagingFactory = nil);
    if isLocalFactory then
        CoCreateInstance(CLSID_WICImagingFactory, nil, CLSCTX_INPROC_SERVER or
          CLSCTX_LOCAL_SERVER, IUnknown, aWICImagingFactory);

    // read the image data from stream
    SourceAdapter := TStreamAdapter.Create(aStream);
    hr := aWICImagingFactory.CreateDecoderFromStream(SourceAdapter, guid_null, WICDecodeMetadataCacheOnDemand, BitmapDecoder);
    if Succeeded(hr) then hr := BitmapDecoder.GetFrame(0, DecodeFrame);
    if Succeeded(hr) then hr := aWICImagingFactory.CreateBitmapFromSource(DecodeFrame, WICBitmapCacheOnLoad, SourceBitmap);
    if Succeeded(hr) then hr := SourceBitmap.GetSize(origWidth, origHeight);

    // calculate proper scaling
    xfactor:= (destWidth) / origWidth;
    yfactor:= (destHeight) / origHeight;
    if xfactor > yfactor then
    begin
        newWidth := round(origWidth * yfactor);
        newHeight := round(origHeight * yfactor);
    end else
    begin
        newWidth := round(origWidth * xfactor);
        newHeight := round(origHeight * xfactor);
    end;

    // scale the original image
    if Succeeded(hr) then hr := aWICImagingFactory.CreateBitmapScaler(SourceScaler);
    if Succeeded(hr) then hr := SourceScaler.Initialize(SourceBitmap, NewWidth, NewHeight, WICBitmapInterpolationModeFant);

    if Succeeded(hr) then
    begin
        // Reading and scaling the original image was successful.
        // Now try to save the scaled image
        DestStream := TMemoryStream.create;
        try
            // create new WICStream
            DestAdapter := TStreamAdapter.Create(DestStream);
            if Succeeded(hr) then hr := aWICImagingFactory.CreateStream(DestWICStream);
            if Succeeded(hr) then hr := DestWICStream.InitializeFromIStream(DestAdapter);
            // create and prepare JPEG-Encoder
            if Succeeded(hr) then hr := aWICImagingFactory.CreateEncoder(GUID_ContainerFormatJpeg, guid_null, BitmapEncoder);
            if Succeeded(hr) then hr := BitmapEncoder.Initialize(DestWICStream, WICBitmapEncoderNoCache);
            if Succeeded(hr) then hr := BitmapEncoder.CreateNewFrame(EncodeFrame, Props);
            if Succeeded(hr) then hr := EncodeFrame.Initialize(Props);
            if Succeeded(hr) then hr := EncodeFrame.SetSize(newWidth, newHeight);
            // write image data
            if Succeeded(hr) then hr := EncodeFrame.WriteSource(SourceScaler, nil);
            if Succeeded(hr) then hr := EncodeFrame.Commit;
            if Succeeded(hr) then hr := BitmapEncoder.Commit;
            // finally save the stream to the destination file
            if Succeeded(hr) then
                try
                    DestStream.SaveToFile(aFilename);
                    result := True;
                except
                    // silent exception here, but (try to) delete the destination file, if it exists
                    result := False;
                    if FileExists(aFilename) then DeleteFile(aFilename);
                end;
        finally
            DestStream.Free;
        end;
    end;

    if isLocalFactory then
        aWICImagingFactory._Release;
end;

Gausi 11. Okt 2019 09:47

AW: TPicture, TJPegImage, TBitmap, TBitmap32 und Threads ...
 
Ich muss hier nochmal nachfragen, weil ich die Interface-Geschichte noch nicht ganz verstanden habe, glaube ich.

Die Funktion aus dem letzten Posting (eigentlich eine private Methode einer großen Klasse, von der im Programm genau eine Instanz existiert) rufe ich mal aus einem Nebenthread auf, und mal aus dem VCL-Thread. Dabei nutze ich nicht TThread, sondern BeginThread, woraus dann die threaded Methoden aus der großen Klasse aufgerufen werden.

Um nicht jedesmal die Factory neu zu erstellen, habe ich dafür zwei private Member-Variablen in der Klasse
Delphi-Quellcode:
WICImagingFactory_VCL: IWICImagingFactory;
WICImagingFactory_ScanThread: IWICImagingFactory;
Mit
Delphi-Quellcode:
IWICImagingFactory = interface(IUnknown)
aus der Unit Winapi.Wincodec.


Aufgerufen wird die Methode dann über

Delphi-Quellcode:
ScalePicStreamToFile(aStream, aFilenname, 240, 240, GetProperImagingFactory(ScanMode))
Scanmode ist ein Aufzählungstyp und steuert "VCL oder Thread". Die Factory bekomme ich dann mit dieser privaten Methode, die bei Bedarf die Factory erstellt, und ansonsten die bestehende zurückliefert.

Delphi-Quellcode:
function TMyClass.GetProperImagingFactory(ScanMode: CoverScanThreadMode): IWICImagingFactory;
begin
    case ScanMode of
        tm_VCL: begin
            if WICImagingFactory_VCL = Nil then
                CoCreateInstance(CLSID_WICImagingFactory, nil, CLSCTX_INPROC_SERVER or
                    CLSCTX_LOCAL_SERVER, IUnknown, WICImagingFactory_VCL);
            result := WICImagingFactory_VCL;
        end;
        tm_Thread: begin
            if WICImagingFactory_ScanThread = Nil then
                CoCreateInstance(CLSID_WICImagingFactory, nil, CLSCTX_INPROC_SERVER or
                    CLSCTX_LOCAL_SERVER, IUnknown, WICImagingFactory_ScanThread);
            result := WICImagingFactory_ScanThread;

        end;
    end;
end;
Jetzt habe ich beim Thread das Problem, dass nach Ende des Threads die Factory nutzlos wird (sie muss wohl immer im Kontext des Threads erstellt werden, in dem sie genutzt wird). Daher muss ich die freigeben, und die Variable auf Nil setzen, damit beim nächsten Thread (es läuft aber immer nur einer nebenbei) wieder eine neue erstellt wird.

Das habe ich so gemacht
Delphi-Quellcode:
WICImagingFactory_ScanThread._Release
WICImagingFactory_ScanThread := Nil
Bei mir läuft das, bei vielen anderen knallt die Zuweisung auf Nil. So grob habe ich auch schon verstanden, warum: Wenn durch das Release der Referenzzähler Null wird, wird das Objekt dahinter freigegeben. Die Zuweisung auf Nil hingegen ruft intern wieder Release auf, aber das Objekt ist schon weg.

In der VCL-Komponente TWICImage ist diese Factory eine Class Var. Wenn ich den Code aus TWICImage.Destroy übernehme, komme ich auf
Delphi-Quellcode:
if WICImagingFactory_ScanThread._Release = 0 then
  Pointer(WICImagingFactory_ScanThread) := Nil;
Das funktioniert dann. Sehe ich das richtig, dass durch den Cast auf Pointer einfach nur die Variable auf NIL gesetzt wird, und die "Interface-Magic" dahinter nicht aktiviert wird, und somit das erreicht wird, was ich haben will? Nämlich dass das Objekt weg ist, und die Variable Nil ist?

Oder ist der ganze Ansatz kompletter Murks? :pale:

TiGü 11. Okt 2019 11:45

AW: TPicture, TJPegImage, TBitmap, TBitmap32 und Threads ...
 
Zitat:

Zitat von Gausi (Beitrag 1449485)
Das funktioniert dann. Sehe ich das richtig, dass durch den Cast auf Pointer einfach nur die Variable auf NIL gesetzt wird, und die "Interface-Magic" dahinter nicht aktiviert wird, und somit das erreicht wird, was ich haben will? Nämlich dass das Objekt weg ist, und die Variable Nil ist?

Ja!

Zitat:

Zitat von Gausi (Beitrag 1449485)
Oder ist der ganze Ansatz kompletter Murks? :pale:

Möglich, aber man muss sich immer Raum für Verbesserung lassen. Version 2.0 wirds richten. ;-)

Warum das direkte Arbeiten mit BeginThread und kein normaler Delphi-TThread? Historische Gründe?

Gausi 11. Okt 2019 12:50

AW: TPicture, TJPegImage, TBitmap, TBitmap32 und Threads ...
 
Zitat:

Zitat von TiGü (Beitrag 1449497)
Warum das direkte Arbeiten mit BeginThread und kein normaler Delphi-TThread? Historische Gründe?

Jep. Das Projekt dahinter ist mittlerweile im 15. Jahr und enthält einige Altlasten (begonnen mit Delphi 7, dann irgendwann der Unicode-Port zu Delphi 2009, jetzt 10.3 CE). Andere (auch alte) Teile finde ich aber immer noch "schön". :wink:

Das mit den Threads hat sich halt so entwickelt. An anderer Stelle nutze ich auch mal TThread. Besonders massive Vor- und Nachteile zwischen den beiden Varianten sehe ich nicht. Aufpassen, wenn der Thread auf den Daten arbeitet, die ggf. auch gerade angezeigt werden, muss man ohnehin.

Aber danke für die Bestätigung. Dann kann ich das so "ausliefern". :stupid:


Alle Zeitangaben in WEZ +1. Es ist jetzt 21:41 Uhr.

Powered by vBulletin® Copyright ©2000 - 2024, Jelsoft Enterprises Ltd.
LinkBacks Enabled by vBSEO © 2011, Crawlability, Inc.
Delphi-PRAXiS (c) 2002 - 2023 by Daniel R. Wolf, 2024 by Thomas Breitkreuz