AGB  ·  Datenschutz  ·  Impressum  







Anmelden
Nützliche Links
Registrieren
Zurück Delphi-PRAXiS Programmierung allgemein Multimedia Delphi TPicture, TJPegImage, TBitmap, TBitmap32 und Threads ...
Thema durchsuchen
Ansicht
Themen-Optionen

TPicture, TJPegImage, TBitmap, TBitmap32 und Threads ...

Ein Thema von Gausi · begonnen am 26. Sep 2019 · letzter Beitrag vom 11. Okt 2019
Antwort Antwort
Benutzerbild von Gausi
Gausi

Registriert seit: 17. Jul 2005
847 Beiträge
 
Delphi 11 Alexandria
 
#1

TPicture, TJPegImage, TBitmap, TBitmap32 und Threads ...

  Alt 26. Sep 2019, 09:54
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.

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?
The angels have the phone box.
  Mit Zitat antworten Zitat
TiGü

Registriert seit: 6. Apr 2011
Ort: Berlin
3.060 Beiträge
 
Delphi 10.4 Sydney
 
#2

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

  Alt 26. Sep 2019, 16:12
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
  Mit Zitat antworten Zitat
Benutzerbild von Gausi
Gausi

Registriert seit: 17. Jul 2005
847 Beiträge
 
Delphi 11 Alexandria
 
#3

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

  Alt 27. Sep 2019, 09:17
Nicht ganz die Antwort, die ich haben wollte, da ich dann wieder den Code umschreiben müsste, aber gut.

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)?
The angels have the phone box.
  Mit Zitat antworten Zitat
TiGü

Registriert seit: 6. Apr 2011
Ort: Berlin
3.060 Beiträge
 
Delphi 10.4 Sydney
 
#4

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

  Alt 27. Sep 2019, 10:11
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 Dest is TBitmap , zur Übergabe an die Zwischenablage und zum Zeichnen.
Des Weiteren ist die 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.
  Mit Zitat antworten Zitat
Benutzerbild von Gausi
Gausi

Registriert seit: 17. Jul 2005
847 Beiträge
 
Delphi 11 Alexandria
 
#5

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

  Alt 27. Sep 2019, 14:26
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.

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.
The angels have the phone box.

Geändert von Gausi (27. Sep 2019 um 21:06 Uhr)
  Mit Zitat antworten Zitat
Benutzerbild von Gausi
Gausi

Registriert seit: 17. Jul 2005
847 Beiträge
 
Delphi 11 Alexandria
 
#6

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

  Alt 28. Sep 2019, 20:16
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.

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;
The angels have the phone box.
  Mit Zitat antworten Zitat
Benutzerbild von Gausi
Gausi

Registriert seit: 17. Jul 2005
847 Beiträge
 
Delphi 11 Alexandria
 
#7

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

  Alt 11. Okt 2019, 09:47
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 IWICImagingFactory = interface(IUnknown) aus der Unit Winapi.Wincodec.


Aufgerufen wird die Methode dann über

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?
The angels have the phone box.
  Mit Zitat antworten Zitat
TiGü

Registriert seit: 6. Apr 2011
Ort: Berlin
3.060 Beiträge
 
Delphi 10.4 Sydney
 
#8

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

  Alt 11. Okt 2019, 11:45
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!

Oder ist der ganze Ansatz kompletter Murks?
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?
  Mit Zitat antworten Zitat
Benutzerbild von Gausi
Gausi

Registriert seit: 17. Jul 2005
847 Beiträge
 
Delphi 11 Alexandria
 
#9

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

  Alt 11. Okt 2019, 12:50
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".

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".
The angels have the phone box.
  Mit Zitat antworten Zitat
Antwort Antwort


Forumregeln

Es ist dir nicht erlaubt, neue Themen zu verfassen.
Es ist dir nicht erlaubt, auf Beiträge zu antworten.
Es ist dir nicht erlaubt, Anhänge hochzuladen.
Es ist dir nicht erlaubt, deine Beiträge zu bearbeiten.

BB-Code ist an.
Smileys sind an.
[IMG] Code ist an.
HTML-Code ist aus.
Trackbacks are an
Pingbacks are an
Refbacks are aus

Gehe zu:

Impressum · AGB · Datenschutz · Nach oben
Alle Zeitangaben in WEZ +1. Es ist jetzt 07:03 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