AGB  ·  Impressum  







Anmelden
Nützliche Links
Registrieren
Zurück Delphi-PRAXiS Projekte RedeemerSVG.TSVGImage - Kleine SVG-Unit für Delphi mit GDI

RedeemerSVG.TSVGImage - Kleine SVG-Unit für Delphi mit GDI

Ein Thema von Redeemer · begonnen am 23. Aug 2017 · letzter Beitrag vom 1. Nov 2017
Antwort Antwort
Seite 4 von 4   « Erste     234
Redeemer
Registriert seit: 19. Jan 2009
Da ich für ein eigenes Projekt SVG-Unterstützung (für Tiny 2.0, statisch) brauchte, aber mit den bestehenden Lösungen nicht zufrieden war (viel zu viele benötigte fremde Pakete), habe ich mir kurzerhand selbst eine geschrieben, die nur mit den Delphi-Canvas-Funktionen und GDI (nicht GDI+) arbeitet. Der Quelltext ist relativ klein. Es werden mein eigener XML-Reader (wegen der vielen nicht zwingend zu schließenden Tags) und diverse Parser, z.B. für Style-Angaben, verwendet. Die eigentliche SVG-Klasse ist etwa 1000 Zeilen lang. Der mit Abstand größte Teil des Projekts, das mit Vanilla-Delphi ab 2009 (wegen Generics) kompiliert, ist die Tabelle mit den XML-Entities (was nicht mal Inkscape unterstützt)... Alles in allem kompiliert derzeit zu gerade mal 150 KiB und LZMA-gepackt irgendwo bei unter 30 KiB plus PNG-Image wenn ihr das nicht schon nutzt.

Funktionsweise:
  • Es werden zwei TPNGImages (ich arbeite gerne damit, später eventuell Umstellung auf TBitmap) erstellt, eine in RGB für Farbe und eine in Graustufen für Deckkraft. Diese sind derzeit 3× breiter und höher als das Zielbild. Außerdem wird ein SVG-Kontext erstellt. Dieser enthält alle Style-Informationen und wird weitergereicht.
  • Anschließend werden die SVG-Elemente verarbeitet:
    • Der Stil wird aus den Attributen und dem style-Attribut geladen. style überschreibt andere Attribute. SVGTiny kann standardgemäß keine richtigen Stylesheets lesen, nur style-Attribute.
    • Zunächst einmal wird ohne Transformationen, aber mit einer erhöhten Genauigkeit (32× breiter und höher, um auszugleichen, dass GDI nur Ganzzahlen kann) mit den GDI-Pfadfunktionen auf eins der PNG-Bilder gezeichnet (bzw. so getan, als ob).
    • Der SVG-Kontext wird in den Brush, Pen, Font und weitere Optionen (Umgang mit Überlappungen, später Gehrung) der beiden PNG-Canvases geschrieben. Wannimmer Farben gesetzt werden, wird beim DeckkraftPNG stattdessen clWhite geschrieben. (Es gibt zwar genau für das Zeichen immer in Weiß eine Funktion beim einfachen Pen (den auch TPen kapsel), aber nicht beim erweiterten ExtCreatePen-Pen.)
    • Der Pfad wird mit GetPath in ein tagPOINT-Array geladen und anschließend aus dem PNG gelöscht. Auch Formen und die für Text verwendete ExtTextOut-Funktion ergeben einen solchen Pfad.
    • Die Punkte des Pfades werden mit allen weiteren Transformationen (affine Abbildungen) versehen und anschließend auf beide PNGs wieder gezeichnet (PolyDraw ist die Umkehrfunktion von GetPath). Die Transformationen finden mit Extended-Gleitkommazahlen statt und haben wie erwähnt eine 32-fache Grund-Genauigkeit. Erst direkt vorm finalen Zeichnen wird gerundet, sodass eine sehr hohe Qualität entsteht, obwohl GDI nur mit Ganzzahlen arbeitet. PS: Der Name dieser Funktion und ihrer Unit stehen nicht zur Diskussion.
  • Die Scanline des Deckkraft-PNG wird in die AlphaScanline des Farb-PNGs geschrieben.
  • Das sich ergebende PNG wird mittels meines ungewichteten Downscale-Algorithmus um den Faktor 3 je Achse verkleinert, um ein Antialias zu erzeugen. Das Ergebnis ist ein PNG-Bild mit ausreichend hübschem Antialias (TSVGImage leitet sich derzeit von TPNGImage ab, irgendwann könnte man das vielleicht zu TBitmap ändern).

Nennenswerte Einschränkungen:
  • Keine Halbtransparenz (Transparenz gibt es aber), da GDI keine Halbtransparenz zeichnen kann. Ich hatte überlegt, eine 10-stufige Halbtransparenz mittels 3×3-Muster (hierbei wird völlige Transparenz unterstützt) zu implementieren, das sähe allerdings an Kanten etwas ungleichmäßig aus und das Überlagern mehrerer solcher Objekte ergäbe kein sinnvolles Ergebnis. Prinzipiell könnte man ein drittes PNG nehmen, darauf zeichnen und anschließend an den bemalten Stellen die Berechnungen mit der Scanline durchführen. Aber will man das? (Bevor jemand fragt: Der TPenMode pmMerge sorgt dafür, dass bei jedem RGB-Kanal der jeweils hellere Wert genommen wird.)
  • Keine Farbverläufe und Filter.
  • Diverse exotische Attribute und Eigenschaften, jeweils fast ausschließlich beim text-Element, werden nicht unterstützt.
  • Die path-Befehle Catmull-Rom (wird als Gerade dargestellt) und Bearing (wird ignoriert) aus SVG2 werden nicht unterstützt.
  • Das image-Element.
  • Metrische Einheiten.
Details siehe die beiliegende Excel-Tabelle.

Tipps:
  • Nach Lesen aller nötigen Informationen des Wurzel-svg-Elements kann die Ausgabegröße mittels Event beeinflusst werden, das SizeCallback heißt und global auf ein folgendes Event gesetzt wird:
    Code:
    type TSizeCallbackEvent = procedure (const Viewport: TRealRect; var Dimensions: TRealPoint) of object;
    So kann das Bild skaliert werden.
  • Die Datei RedeemerHypertextColorsX11.pas wird nicht benötigt. Sie liegt bloß für den Fall bei, falls ihr wie ich RedeemerHypertextColors.HTMLToColor anderswo mit X11-Unterstützung brauchen solltet.
  • EXE-Demo liegt bei.

Lizenz:
Die Nutzung ist kostenlos. Wer es in einem eigenen Produkt verwendet, das nicht für den Eigenbedarf ist, muss mir eine Nachricht schreiben. Weiterentwicklungen und Ableitungen der Klasse TSVGImage müssen mir auf Wunsch zur Verfügung gestellt werden.

Feedback ist gerne gesehen!

Download (ca. 300 KiB)

Beispielbild der Karte von Niedersachsen von TUBS (Wikipedia). Das Forum kann es leider nicht in der Originalgröße, ich habe es zugeschnitten.
Beispielbild der Flugzeugentführung der Landshut von Devilm25 (Wikipedia). Sie ist nicht ganz perfekt, man sieht ganz gut, was die Engine kann und was nicht. Ich frage mich gerade, warum zwischen der letzten und der aktuellen Version meiner Unit die Grenze zwischen Eritrea und Äthiopien verschwunden ist (sie fehlt allerdings auch bei anderen SVG-Viewern).
Miniaturansicht angehängter Grafiken
niedersachsen.png   strichelungen.png   entfuehrung-der-landshut.png  
2005 PE, 2009 PA, XE2 PA

Geändert von Redeemer (20. Okt 2017 um 21:58 Uhr)
 
TiGü

 
Delphi 10.1 Berlin Enterprise
 
#31
  Alt 23. Okt 2017, 12:05
So kannst du im LoadFromStream eine handvoll Millisekunden sparen:
Delphi-Quellcode:
  sl: TStringStream;
begin
  sl := TStringStream.Create('', TCustomUTF8Encoding.Create);
  try
    sl.LoadFromStream(Stream);
    XML := TRedeemerXML.Create(sl.DataString);
  finally
    sl.Free;
  end;
  Mit Zitat antworten Zitat
Redeemer

 
Delphi 2009 Professional
 
#32
  Alt 28. Okt 2017, 17:33
Kleine Frage mal an die, die die Klasse benutzen: Mit welchen Delphi-Versionen kompiliert die? Müsst ihr was anpassen?

Zitat:
Ich frage mich gerade, warum zwischen der letzten und der aktuellen Version meiner Unit die Grenze zwischen Eritrea und Äthiopien verschwunden ist (sie fehlt allerdings auch bei anderen SVG-Viewern).
War Eritrea im Jahre 1977 nicht ein Teil von Äthiopien?
Absolut korrekt, danke. Das erklärt's. Dann hat wohl der Ersteller der Karte die entsprechende Grenze mit irgendwas übergekritzelt, was zu der Zeit, als die Grenze da war, noch nicht unterstützt wurde.

So kannst du im LoadFromStream eine handvoll Millisekunden sparen:
Delphi-Quellcode:
  sl: TStringStream;
begin
  sl := TStringStream.Create('', TCustomUTF8Encoding.Create);
  try
    sl.LoadFromStream(Stream);
    XML := TRedeemerXML.Create(sl.DataString);
  finally
    sl.Free;
  end;
Brachte in meinem Test überhaupt selbst mit einer 6 MiB großen SVG-Datei keinen Unterschied, außer dass die EXE 1,5 KiB größer wurde. Gestört hat mich das mit TStringList auch schon. Mich stört auch, dass der String beim Laden aus einem TMemoryStream (was in dem Projekt, wofür ich es benötige, üblich ist) zeitweise dreimal im Speicher ist. TByteStream sah auch Interessant aus, aber ich benutze mit Pos und Copy einige Funktionen, die nur für Strings (genau so) funktionieren.
  Mit Zitat antworten Zitat
TiGü

 
Delphi 10.1 Berlin Enterprise
 
#33
  Alt 1. Nov 2017, 12:12
So kannst du im LoadFromStream eine handvoll Millisekunden sparen:
Delphi-Quellcode:
  sl: TStringStream;
begin
  sl := TStringStream.Create('', TCustomUTF8Encoding.Create);
  try
    sl.LoadFromStream(Stream);
    XML := TRedeemerXML.Create(sl.DataString);
  finally
    sl.Free;
  end;
Brachte in meinem Test überhaupt selbst mit einer 6 MiB großen SVG-Datei keinen Unterschied, außer dass die EXE 1,5 KiB größer wurde. Gestört hat mich das mit TStringList auch schon. Mich stört auch, dass der String beim Laden aus einem TMemoryStream (was in dem Projekt, wofür ich es benötige, üblich ist) zeitweise dreimal im Speicher ist. TByteStream sah auch Interessant aus, aber ich benutze mit Pos und Copy einige Funktionen, die nur für Strings (genau so) funktionieren.
Jo, mit einer handvoll Millisekunden meinte ich auch wirklich nur so 5 ms.
Wenn das gesamte Laden und Darstellen so 600 bis 700 ms dauert, ist das natürlich total schnurzpiepe, aber wenn man mit einfachen Mitteln hier optimieren kann...warum nicht?

Was du noch mal optimieren kannst wäre die Übergabe von TSVGContext.
Das Record mit allen Unter-Records ist ja 184 Byte groß. In den ganzen Handle...-Methoden übergibst du das ja sehr oft (mehrere hundert bis tausend mal) per by-value. Das heißt die 184 Byte werden immer kopiert.
Wenn du da deine Klasse optimieren könntest, wären definitiv noch Geschwindigkeitszuwächse zu erwarten.
  Mit Zitat antworten Zitat
Redeemer

 
Delphi 2009 Professional
 
#34
  Alt 1. Nov 2017, 12:56
Auch bei Dateien, die 2,5 Sekunden zum Laden brauchen, konnte ich keine Millisekunde Unterschied feststellen.

Der Context muss kopiert werden. In den Handle-Methoden werden Stile geladen, die nur den aktuellen Context (und Unteraufrufe) betreffen und danach wieder rückgängig gemacht werden müssen.
  Mit Zitat antworten Zitat
TiGü

 
Delphi 10.1 Berlin Enterprise
 
#35
  Alt 1. Nov 2017, 13:12
Der Context muss kopiert werden. In den Handle-Methoden werden Stile geladen, die nur den aktuellen Context (und Unteraufrufe) betreffen und danach wieder rückgängig gemacht werden müssen.
Kannst du das etwas ausführen? So sehr stehe ich im SVG parsen und verarbeiten ja nicht drin.

Könnte man - jetzt mal nur so theoretisch ins Blaue philosophiert - nicht einfach für jeden Style und/oder Figur (Text, Rect, Circle...) sich den passenden Context vorhalten?
Oder wie muss man sich die Zusammenhänge vorstellen?
Wenn der Context der in HandleTag übergeben wird und für die einzelnen verschiedenen CurrentTags eh verschieden ist, wäre ein Dictionary mit TSVGContext als class (4 Byte versus 184 Byte) nicht eine Überlegung wert?
  Mit Zitat antworten Zitat
Redeemer

 
Delphi 2009 Professional
 
#36
  Alt 1. Nov 2017, 13:42
HTML-Code:
<svg width="28" height="28">
  <g fill="yellow" stroke="black">
    <rect fill="red" x="3" y="3" width="10" height="10" />
    <g fill="blue">
      <rect fill="green" x="15" y="3" width="10" height="10" />
      <rect x="3" y="15" width="10" height="10" />
    </g>
    <rect x="15" y="15" width="10" height="10" />
  </g>
</svg>
(Ergibt das bunte Windows-Logo.)

Die Standardfüllung von SVG ist schwarz. Die erste Gruppe (g) setzt die Füllung für alle Kindelemente auf gelb und die Kontur auf schwarz. Das erste Rechteck setzt die Füllung auf rot, behält aber die Kontur bei. Nachdem das Rechteck gezeichnet ist, müssen alle Eigenschaften zurückgesetzt werden, die das Rechteck gesetzt hat. Die zweite Gruppe (g) setzt die Füllung für alle Kindelemente auf blau und behält die Kontur bei. Das erste Rechteck setzt die Füllung auf rot, behält aber die Kontur bei. Nachdem das Rechteck gezeichnet ist, müssen alle Eigenschaften zurückgesetzt werden, die das Rechteck gesetzt hat. Das dritte Rechteck (zweites der zweiten Gruppe) wird nun mit der Füllung der Gruppe gezeichnet. Diese wird allerdings sofort danach ebenfalls zurückgesetzt auf die Farbe Gelb der Elterngruppe. Diese wird für das letzte Rechteck benötigt. Nach dem Ende der Gruppe ist die Füllung wieder zurück auf Standard, also schwarz.

Der Kontext muss deshalb natürlich auf dem Stapel abgelegt werden. Soweit ich den Vortrag auf den Delphi-Tagen dieses Jahr richtig verstanden habe, wird da von Delphi optimiert, damit keine Dinge kopiert werden, die man nicht schreibt.
  Mit Zitat antworten Zitat
TiGü

 
Delphi 10.1 Berlin Enterprise
 
#37
  Alt 1. Nov 2017, 14:20
Verstehe...hm hm hm.
Man könnte so ein Recall-Ansatz versuchen und TSVGContext zur Klasse machen.

http://docwiki.embarcadero.com/Libra...hRecall.Create

Ob das ständige erzeugen und freigeben der Context-Recall-Klasse aber dann weniger zeitintensiv ist als das Kopieren des records...wer weiß?!
  Mit Zitat antworten Zitat
Redeemer

 
Delphi 2009 Professional
 
#38
  Alt 1. Nov 2017, 14:59
Wieso sollte ich etwas in TRecall speichern, was ich exakt ein einziges Mal verwende?
  Mit Zitat antworten Zitat
TiGü

 
Delphi 10.1 Berlin Enterprise
 
#39
  Alt 1. Nov 2017, 15:24
Recall würde die Werte der Klasseninstanz von TSVGContext (wenn es dann eine Klasse wird) wieder zurück schreiben, wenn sie in ReadStyle geändert wurden.
Ähnlich wie dem Verwerfen der Änderung auf der lokalen Record-Kopie.
Ob das dann am Ende schneller ist als mit record muss natürlich geprüft werden, dass kann ich so auch nicht sehen.
  Mit Zitat antworten Zitat
Themen-Optionen Thema durchsuchen
Thema durchsuchen:

Erweiterte Suche
Ansicht

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:

Nach oben
Alle Zeitangaben in WEZ +1. Es ist jetzt 23:18 Uhr.
Powered by vBulletin® Copyright ©2000 - 2018, Jelsoft Enterprises Ltd.
LinkBacks Enabled by vBSEO © 2011, Crawlability, Inc.
Delphi-PRAXiS (c) 2002 - 2017 by Daniel R. Wolf