Während meiner Arbeit bei GFRANQ Photo Service war ich 2013 an der Entwicklung eines gleichnamigen Webdienstes für die Veröffentlichung und Verarbeitung von Fotos beteiligt. Filter und Transformationen wurden in der Datei mit Parametern definiert, und die gesamte Verarbeitung wurde auf dem Server ausgeführt. Während der Serviceentwicklung mussten diese Transformationen auf der Clientseite für die Vorschau unterstützt werden. Laut Larry Wall ist Faulheit eine der Tugenden eines Programmierers. Daher haben wir als wirklich faule Programmierer über die Möglichkeit nachgedacht, auf Server- und Clientseite denselben Code zu verwenden. Die gesamte Entwicklung wurde in C # durchgeführt. Nachdem wir die Bibliotheken recherchiert und einige Versuche unternommen hatten, kamen wir stolz zu dem Schluss, dass dies möglich war, und begannen, den universellen Code zu schreiben.

Warum wird dieser Artikel benötigt? In der Tat sind seit 2013 6 Jahre vergangen, und viele Technologien haben ihre Relevanz verloren, beispielsweise Script # . Auf der anderen Seite sind neue erschienen. Zum Beispiel Bridge.NET oder Blazor basierend auf der ausgefallenen WebAssembly .
Trotzdem können einige Ideen noch verwendet werden. In diesem Artikel habe ich versucht, sie so detailliert wie möglich zu beschreiben. Ich hoffe, dass die Erwähnung von Silverlight und Flash ein Lächeln mit einem Hauch von Nostalgie hervorruft und nicht den Wunsch, die alten Lösungen zu kritisieren. Auf jeden Fall haben sie zur Entwicklung der Webbranche beigetragen.
Inhalt
Ziel
Die Herausforderung besteht darin, Fotocollage- und filterbasierte Fotobearbeitungsfunktionen auf der Clientseite und, wenn möglich, auch auf der Serverseite zu implementieren. Zunächst werde ich erläutern, wie Filter und Collagen implementiert werden.
Beschreibung der Filter
Im Rahmen unseres Projekts besteht ein Filter aus einer Reihe von Aktionen, die in Photoshop ausgeführt und auf ein bestimmtes Foto angewendet werden. Nachfolgend finden Sie Beispiele für solche Aktionen:
- Helligkeitsanpassung
- Kontrasteinstellung
- Sättigungseinstellung
- Anpassung der Farbkurven
- Maskierung in verschiedenen Modi
- Rahmung
- ...
Wir brauchen ein bestimmtes Format, um diese Aktionen zu beschreiben. Sicher, es gibt gängige Formate wie JSON und XML, aber aus folgenden Gründen wurde beschlossen, ein eigenes Format zu erstellen:
- Notwendigkeit einer plattformunabhängigen Codearchitektur (.NET, JavaScript, WinPhone usw.)
- Notwendigkeit eines einfachen nicht hierarchischen Filterformats, das das Schreiben eines Parsers erleichtert
- XML- und JSON-Daten verbrauchen mehr Speicher (in diesem speziellen Fall)
So sieht die Abfolge der Aktionen für den XPro Film- Filter aus:
Neben der Bearbeitung eines Fotos mit einem Filter mussten wir das Bild zuschneiden und drehen. Ja, ich wusste, dass es jQuery-Plugins zum Zuschneiden und Drehen von Bildern gibt, aber sie schienen überladen zu sein und von der universellen Architektur des Projekts abzuweichen.
Beschreibung der Collagen
Eine Collage ist eine Anordnung mehrerer miniaturisierter Fotos zu einem ganzen Foto (mit oder ohne Maske). Außerdem mussten Benutzer verfügbare Bilder per Drag & Drop auf die Collage ziehen, ihre Position und ihren Maßstab ändern können. Ihre Collage könnte folgendermaßen aussehen:
Bei der Collagenfunktion wird ein einfaches Format zum Speichern von Rechtecken mit relativen Koordinaten von 0
bis 1
, den Adressen von Fotos und Bildänderungsdaten verwendet. Relative Koordinaten werden verwendet, da dieselben clientseitigen Transformationen auf große Bilder auf der Serverseite angewendet werden.
Implementierung
Wir mussten die Plattform auswählen, auf der Benutzer mit Filtern und Collagen arbeiten können
Es gibt verschiedene RIA- Technologien (Rich Internet Application) wie:
- Adobe Flash
- Microsoft Silverlight
- HTML 5 + JavaScript
- Native Client
Aus offensichtlichen Gründen sind Flash und HTML die einzigen Technologien, die Beachtung verdienen, da der Rest nicht plattformübergreifend kompatibel ist. Außerdem beginnt der Silverlight-Client zu sterben. Obwohl ich das Konzept von wirklich mag Salz NaCl, leider wird diese Technologie nur vom Chrome-Browser unterstützt und es ist noch nicht bekannt, wann sie von anderen gängigen Browsern unterstützt wird (und jemals unterstützt wird). Hinweis von 2019: Es wird und der Name ist WebAssembly .
Die Wahl fiel auf die trendige und fortschrittliche HTML5-Plattform, deren Funktionalität derzeit von iOS im Gegensatz zu Flash unterstützt wird. Diese Auswahl basiert auch auf der Tatsache, dass es viele Bibliotheken gibt, mit denen Sie den C # -Code in Javascript kompilieren können. Sie können zu diesem Zweck auch Visual Studio verwenden. Details sind unten angegeben.
C # in Javascript übersetzen
HTML 5 + JavaScript wurde im vorherigen Abschnitt als Plattform ausgewählt. Es bleibt also die Frage, ob es möglich ist, einen universellen C # -Code zu schreiben, der sowohl in .NET als auch in JavaScript kompiliert werden kann.
So wurde eine Reihe von Bibliotheken gefunden, um die Aufgabe zu erfüllen:
- Jsil
- Sharpkit
- Skript #
- Und einige andere auf GitHub verfügbar.
Aus diesem Grund wurde beschlossen, Script # zu verwenden, da JSIL direkt mit Assemblys arbeitet und weniger reinen Code generiert (obwohl es eine größere Auswahl an C # -Sprachenfunktionen unterstützt). SharpKit ist ein kommerzielles Produkt. Einen detaillierten Vergleich dieser Tools finden Sie in der Frage zum Stackoverflow .
Zusammenfassend hat ScriptSharp im Vergleich zu manuell geschriebenem JavaScript die folgenden Vor- und Nachteile:
Vorteile
- Möglichkeit, einen universellen C # -Code zu schreiben, der in .NET und andere Plattformen (WinPhone, Mono) kompiliert werden kann
- Entwicklung in einer stark typisierten C # -Sprache, die OOP unterstützt
- Unterstützung für IDE-Funktionen (Autocompletion und Refactoring)
- Fähigkeit, die meisten Fehler in der Kompilierungsphase zu erkennen
Nachteile
- Redundanz und Unregelmäßigkeit des generierten JavaScript-Codes (aufgrund von mscorlib).
- Unterstützung nur für ISO-2 (keine Funktionsüberladung oder Typ-, Erweiterungs- und generische Inferenz)
Struktur
Das Kompilieren desselben C # -Codes in .NET und Javascript kann durch das folgende Schema veranschaulicht werden:
Obwohl .NET und HTML5 völlig unterschiedliche Technologien sind, haben sie auch ähnliche Funktionen. Dies gilt auch für die Arbeit mit Grafiken. Zum Beispiel unterstützt .NET Bitmap , JavaScript unterstützt das analoge Canvas . Gleiches gilt für Grafiken , Kontext und Arrays von Pixeln. Um alles in einem Code zu kombinieren, wurde beschlossen, die folgende Architektur zu entwickeln:
Natürlich ist es nicht auf zwei Plattformen beschränkt. Im Anschluss ist geplant, Unterstützung für WinPhone und dann möglicherweise für Android und iOS hinzuzufügen.
Es ist zu beachten, dass es zwei Arten von Grafikoperationen gibt:
- Verwenden von API-Funktionen (
DrawImage
, Arc
, MoveTo
, LineTo
). Hohe Leistung und Unterstützung für die Hardwarebeschleunigung sind wichtige Wettbewerbsvorteile. Der Nachteil ist, dass sie auf verschiedenen Plattformen unterschiedlich implementiert werden können. - Pixel für Pixel. Die Unterstützung der Implementierung von Effekten und die plattformübergreifende Abdeckung gehören zu den Vorteilen. Der Nachteil ist die geringe Leistung. Sie können die Nachteile jedoch durch Parallelisierung, Shader und vorberechnete Tabellen verringern (wir werden dies im nächsten Abschnitt zur Optimierung weiter erläutern).
Wie Sie sehen können, beschreibt die abstrakte Klasse Grafik alle Methoden zum Arbeiten mit Grafiken. Diese Methoden sind für verschiedene Plattformen in der abgeleiteten Klasse implementiert. Die folgenden Aliase wurden auch als Zusammenfassung von Bitmap- und Canvas-Klassen geschrieben. Die WinPhone-Version verwendet auch ein Adaptermuster .
Alias verwenden
#if SCRIPTSHARP using System.Html; using System.Html.Media.Graphics; using System.Runtime.CompilerServices; using Bitmap = System.Html.CanvasElement; using Graphics = System.Html.Media.Graphics.CanvasContext2D; using ImageData = System.Html.Media.Graphics.ImageData; using Image = System.Html.ImageElement; #elif DOTNET using System.Drawing; using System.Drawing.Imaging; using System.Drawing.Drawing2D; using Bitmap = System.Drawing.Bitmap; using Graphics = System.Drawing.Graphics; using ImageData = System.Drawing.Imaging.BitmapData; using Image = System.Drawing.Bitmap; #endif
Leider ist es unmöglich, Aliase für unsichere Typen und Arrays zu erstellen, dh Alias für Zeiger (Byte *) in C # :
using PixelArray = byte*, using PixelArray = byte[]
Um eine schnelle Verarbeitung von Pixeln mit nicht verwaltetem C # -Code durchzuführen und ihn gleichzeitig in Script # zu kompilieren, haben wir mithilfe von Anweisungen das folgende Schema eingeführt:
#if SCRIPTSHARP PixelArray data = context.GetPixelArray(); #elif DOTNET byte* data = context.GetPixelArray(); #endif
Das Datenarray wird anschließend verwendet, um verschiedene Pixel-für-Pixel-Operationen (wie Maskierung, Fischaugen, Sättigungsanpassung usw.) sowohl parallelisiert als auch nicht parallelisiert zu implementieren.
Links zu Dateien
Der Lösung wird für jede Plattform ein separates Projekt hinzugefügt, aber natürlich können Mono, Script # und sogar Silverlight nicht auf die üblichen .NET-Assemblys verweisen. Glücklicherweise verfügt Visual Studio über einen Mechanismus zum Hinzufügen von Links zu Dateien, mit dem Sie denselben Code in verschiedenen Projekten wiederverwenden können.
Compiler-Direktiven ( DOTNET
, SCRIPTSHARP
) werden in den Projekteigenschaften unter Bedingte Kompilierungssymbole definiert.
Hinweise zur .NET-Implementierung
Die obigen Abstraktionen und Aliase haben uns geholfen, den C # -Code mit geringer Redundanz zu schreiben. Außerdem möchte ich auf die Probleme mit .NET- und JavaScript-Plattformen hinweisen, mit denen wir bei der Entwicklung des Lösungscodes konfrontiert waren.
Verwenden Sie entsorgen
Beachten Sie, dass für die Aufnahme einer Instanz einer C # -Klasse, die die IDisposable
Schnittstelle implementiert, die Dispose
Methode Dispose
oder die Using-Anweisung IDisposable
werden muss. In diesem Projekt sind diese Klassen Bitmap und Kontext. Was ich oben gesagt habe, ist nicht nur die Theorie, sondern hat auch eine praktische Anwendung: Die Verarbeitung einer großen Anzahl großer Fotos (bis zu 2400 x 2400 dpi) auf ASP.NET Developer Server x86 führte zu einer Ausnahme wegen Speichermangels. Das Problem wurde behoben, nachdem Dispose
an den richtigen Stellen hinzugefügt wurde. Einige weitere hilfreiche Hinweise zur Bildmanipulation finden Sie im folgenden Artikel. 20 Fallstricke bei der Größenänderung von Bildern und .NET-Speicherverlust: Entsorgen oder nicht entsorgen, das ist die 1-GB-Frage .
Schloss verwenden
In JavaScript gibt es einen Unterschied zwischen bereits hochgeladenen Bildern mit dem Tag img
, für die Sie die Quelle und das img
angeben können, und Canvas-Tags, auf denen Sie etwas zeichnen können. In .NET werden diese Elemente durch dieselbe Bitmap
Klasse dargestellt. Daher verweisen Aliase Bitmap und Image in .NET auf dieselbe Klasse System.Drawing.Bitmap
. Bitmap wie oben gezeigt.
Trotzdem war diese Aufteilung in img
und canvas
in JavaScript auch in der .NET-Version sehr hilfreich. Der Punkt ist, dass Filter vorinstallierte Masken aus verschiedenen Threads verwenden. Daher ist das Sperrmuster erforderlich, um die Ausnahme während der Synchronisation zu vermeiden (das Bild wird mit der Sperre kopiert und das Ergebnis wird ohne Sperre verwendet):
internal static Bitmap CloneImage(Image image) { #if SCRIPTSHARP Bitmap result = (Bitmap)Document.CreateElement("canvas"); result.Width = image.Width; result.Height = image.Height; Graphics context = (Graphics)result.GetContext(Rendering.Render2D); context.DrawImage(image, 0, 0); return result; #else Bitmap result; lock (image) result = new Bitmap(image); return result; #endif }
Schließlich sollte die Sperre auch beim Zugriff auf die Eigenschaften eines synchronisierten Objekts verwendet werden (tatsächlich sind alle Eigenschaften Methoden).
Speichern von Masken im Speicher
Um die Verarbeitung zu beschleunigen, werden beim Starten des Servers alle potenziell verwendeten Masken für Filter in den Speicher geladen. Unabhängig vom Format der Maske verwendet die auf den Server hochgeladene Bitmap 4 * 2400 * 2400
oder ≈24 MB
Speicher (die maximale Bildgröße beträgt 2400 * 2400
; die Anzahl der Bytes pro Pixel beträgt 4). Alle Masken für Filter (≈30) und Collagen (40) verbrauchen 1,5 GB - das ist für den Server nicht viel; Mit zunehmender Anzahl von Masken kann sich diese Menge jedoch erheblich erhöhen. In Zukunft werden wir möglicherweise Komprimierungstechniken für im Speicher gespeicherte Masken (in den Formaten .jpg und .png) verwenden, gefolgt von einer Dekomprimierung, falls erforderlich. Tatsächlich kann die Größe bis zu 300-mal reduziert werden. Ein zusätzlicher Vorteil dieses Ansatzes besteht darin, dass das Kopieren der komprimierten Bilder im Vergleich zu großen Bildern schneller erfolgt. Daher dauert der Sperrvorgang weniger lange und Threads werden seltener blockiert.
Hinweise zur JavaScript-Implementierung
Minimierung
Ich habe mich aus folgendem Grund geweigert, den Begriff "Verschleierung" zu verwenden: Dieser Begriff ist kaum auf eine vollständig Open-Source-Sprache anwendbar, in unserem Fall JavaScript. Die Anonymisierung von Bezeichnern kann jedoch die Lesbarkeit und Logik des Codes beeinträchtigen. Und am wichtigsten ist, dass diese Technik die Größe des Skripts erheblich reduziert (die komprimierte Version ist ca. 80 KB groß).
Es gibt zwei Ansätze zur JavaScript-Minimierung:
- Manuelle Minimierung, die in der Generierungsphase mit ScriptSharp durchgeführt wird.
- Automatisierte Minimierung, die nach der Generierungsphase mit externen Tools wie Google Closure Compiler, Yui und anderen Tools durchgeführt wird.
Manuelle Minimierung
Um die Namen von Methoden, Klassen und Attributen zu verkürzen, haben wir diese Syntax vor der Deklaration der oben genannten Entitäten verwendet. Dies ist natürlich nicht erforderlich, wenn Sie mit Methoden arbeiten, die von externen Skripten und Klassen (öffentlich) aufgerufen werden.
#if SCRIPTSHARP && !DEBUG [ScriptName("a0")] #endif
Auf jeden Fall konnten lokale Variablen nicht minimiert werden. Diese Konstrukte verschmutzen den Code und beeinträchtigen die Lesbarkeit des Codes, was ebenfalls ein schwerwiegender Nachteil ist. Diese Technik kann jedoch die Menge des generierten JavaScript-Codes erheblich reduzieren und ihn ebenfalls durcheinander bringen.
Ein weiterer Nachteil ist, dass Sie solche Kurznamen im Auge behalten müssen, wenn sie die Methoden- und Feldnamen umbenennen (insbesondere überschriebene Namen in den untergeordneten Klassen), da sich Script # in diesem Fall nicht um sich wiederholende Namen kümmert. Es werden jedoch keine doppelten Klassen zugelassen.
Übrigens wurde der entwickelten Version des Skripts # bereits eine Minimierungsfunktion für private und interne Methoden und Felder hinzugefügt.
Automatisierte Minimierung
Obwohl es viele Tools für die JavaScript-Minimierung gibt, habe ich den Google Closure Compiler für seine Marke und die gute Qualität der Komprimierung verwendet. Der Nachteil des Minifizierungstools von Google besteht darin, dass CSS-Dateien nicht komprimiert werden können. Im Gegensatz dazu begegnet YUI dieser Herausforderung erfolgreich. Tatsächlich kann Script # auch Skripte minimieren, bewältigt diese Herausforderung jedoch viel schlechter als Google Closure.
Das Minifizierungstool von Google verfügt über mehrere Komprimierungsstufen: Leerzeichen, einfache und erweiterte Funktionen. Wir haben für das Projekt die Stufe "Einfach" gewählt. Die erweiterte Stufe ermöglicht es uns zwar, die maximale Qualität der Komprimierung zu erreichen, erfordert jedoch Code, der so geschrieben ist, dass Methoden von außerhalb der Klasse zugänglich sind. Diese Minimierung wurde teilweise manuell mit Script # durchgeführt.
Debug- und Release-Modi
Debug- und Release-Bibliotheken wurden wie folgt zu ASP.NET-Seiten hinzugefügt:
<% if (Gfranq.JavaScriptFilters.HtmlHelper.IsDebug) { %> <script src="Scripts/mscorlib.debug.js" ></script> <script src="Scripts/imgProcLib.debug.js" ></script> <% } else { %> <script src="Scripts/mscorlib.js" ></script> <script src="Scripts/imgProcLib.js" ></script> <% } %>
In diesem Projekt haben wir sowohl Skripte als auch Filterbeschreibungsdateien minimiert.
crossOrigin-Eigenschaft
Um auf die Pixel eines bestimmten Bildes zugreifen zu können, müssen wir es zuerst in Leinwand konvertieren. Dies kann jedoch zu einem CORS-Fehler (Cross Origin Request Security) führen. In unserem Fall wurde das Problem wie folgt gelöst:
- Festlegen des Attributs
crossOrigin = ''
auf der Serverseite. - Hinzufügen eines bestimmten Headers zum HTTP-Paket auf der Serverseite.
Da ScriptSharp diese Eigenschaft für img-Elemente nicht unterstützt, wurde der folgende Code geschrieben:
[Imported] internal class AdvImage { [IntrinsicProperty] internal string CrossOrigin { get { return string.Empty; } set { } } }
Dann werden wir es so verwenden:
((AdvImage)(object)result).CrossOrigin = "";
Mit dieser Technik können Sie dem Objekt jede Funktion ohne Kompilierungsfehler hinzufügen. Insbesondere ist die wheelDelta
Eigenschaft in ScriptSharp noch nicht implementiert (zumindest in Version 0.7.5). Diese Eigenschaft gibt den Scrollradbetrag an, der zum Erstellen von Collagen verwendet wird. Deshalb wurde es so implementiert. Solch ein schmutziger Hack mit den Eigenschaften ist nicht gut; Normalerweise müssen Sie Änderungen am Projekt vornehmen. Aber nur zur Veranschaulichung, ich habe noch keinen Weg gefunden, ScriptSharp aus dem Quellcode zu kompilieren.
Für solche Images muss der Server die folgenden Header in seinen Antwortheadern (in Global.asax) zurückgeben:
Response.AppendHeader("Access-Control-Allow-Origin", "\*");
Weitere Informationen zur Cross Origin Request Security finden Sie unter http://enable-cors.org/ .
Optimierungen
Verwenden der vorberechneten Werte
Wir haben die Optimierung für einige Operationen wie die Anpassung von Helligkeit, Kontrast und Farbkurven über die vorläufige Berechnung der resultierenden Farbkomponenten (r, g, b) für alle möglichen Werte und die weitere Verwendung der erhaltenen Arrays verwendet, um die Farben von Pixeln direkt zu ändern . Es ist zu beachten, dass diese Art der Optimierung nur für Operationen geeignet ist, bei denen die Farbe des resultierenden Pixels nicht durch benachbarte Pixel beeinflusst wird.
Die Berechnung der resultierenden Farbkomponenten für alle möglichen Werte:
for (int i = 0; i < 256; i++) { r[i] = ActionFuncR(i); g[i] = ActionFuncG(i); b[i] = ActionFuncB(i); }
Verwendung vorberechneter Farbkomponenten:
for (int i = 0; i < data.Length; i += 4) { data[i] = r[data[i]]; data[i + 1] = g[data[i + 1]]; data[i + 2] = b[data[i + 2]]; }
Wenn solche Tabellenoperationen einzeln ausgeführt werden, müssen keine Zwischenbilder berechnet werden. Sie können nur die Farbkomponenten-Arrays übergeben. Da der Code sowohl auf Client- als auch auf Serverseite ziemlich schnell funktionierte, wurde beschlossen, die Implementierung dieser Optimierung beiseite zu legen. Darüber hinaus verursachte die Optimierung unerwünschtes Verhalten. Ich werde Ihnen jedoch eine Auflistung der Optimierung geben:
Originalcode | Optimierter Code |
`` `cs // Berechnung der Werte für die erste Tabelle. für (int i = 0; i <256; i ++) { r [i] = ActionFunc1R (i); g [i] = ActionFunc1G (i); b [i] = ActionFunc1B (i); }} // ...
// Berechnung des resultierenden Zwischenbildes. für (int i = 0; i <Datenlänge; i + = 4) { Daten [i] = r [Daten [i]]; Daten [i + 1] = g [Daten [i + 1]]; Daten [i + 2] = b [Daten [i + 2]]; }} // ...
// Berechnung der Werte für die zweite Tabelle. für (int i = 0; i <256; i ++) { r [i] = ActionFunc2R (i); g [i] = ActionFunc2G (i); b [i] = ActionFunc2B (i); }} // ...
// Berechnung des resultierenden Bildes. für (int i = 0; i <Datenlänge; i + = 4) { Daten [i] = r [Daten [i]]; Daten [i + 1] = g [Daten [i + 1]]; Daten [i + 2] = b [Daten [i + 2]]; }} `` ``
| `` `cs // Berechnung der Werte für die erste Tabelle. für (int i = 0; i <256; i ++) { r [i] = ActionFunc1R (i); g [i] = ActionFunc1G (i); b [i] = ActionFunc1B (i); }} // ...
// Berechnung der Werte für die zweite Tabelle. tr = r.Clone (); tg = g.Clone (); tb = b.Clone (); für (int i = 0; i <256; i ++) { r [i] = tr [ActionFunc2R (i)]; g [i] = tg [ActionFunc2G (i)]; b [i] = tb [ActionFunc2B (i)]; }} // ...
// Berechnung des resultierenden Bildes. für (int i = 0; i <Datenlänge; i + = 4) { Daten [i] = r [Daten [i]]; Daten [i + 1] = g [Daten [i + 1]]; Daten [i + 2] = b [Daten [i + 2]]; }} `` ``
|
Aber auch das ist noch nicht alles. Wenn Sie sich die Tabelle rechts ansehen, werden Sie feststellen, dass neue Arrays mit der Clone
Methode erstellt werden. Tatsächlich können Sie einfach die Zeiger auf das alte und das neue Array ändern, anstatt das Array selbst zu kopieren (dies erinnert an die Analogie der doppelten Pufferung ).
Konvertieren eines Bildes in ein Array von Pixeln
Der JavaScript-Profiler in Google Chrome hat ergeben, dass die Funktion GetImageData
(mit der die GetImageData
das GetImageData
konvertiert wird) lange genug ausgeführt wird. Diese Informationen finden Sie übrigens in verschiedenen Artikeln zur Canvas-Optimierung in JavaScript.
Die Anzahl der Aufrufe dieser Funktion kann jedoch minimiert werden. Wir können nämlich das gleiche Array von Pixeln für Pixel-für-Pixel-Operationen verwenden, analog zur vorherigen Optimierung.
Codebeispiele
In den folgenden Beispielen werde ich die Codefragmente bereitstellen, die ich interessant und nützlich fand. Damit der Artikel nicht zu lang wird, habe ich die Beispiele unter einem Spoiler versteckt.
Allgemein
Erkennen, ob eine Zeichenfolge eine Zahl ist
internal static bool IsNumeric(string n) { #if !SCRIPTSHARP return ((Number)int.Parse(n)).ToString() != "NaN"; #else double number; return double.TryParse(n, out number); #endif }
Ganzzahlige Division
internal static int Div(int n, int k) { int result = n / k; #if SCRIPTSHARP result = Math.Floor(n / k); #endif return result; }
Drehen und Spiegeln eines Bildes mithilfe von Canvas und Bitmap
Bitte beachten Sie, dass in HTML5 Leinwandbilder nur mit Matrizen um 90 und 180 Grad gedreht werden können, während .NET erweiterte Funktionen bietet. Daher wurde eine geeignete genaue Funktion zum Arbeiten mit Pixeln geschrieben.
Es ist auch erwähnenswert, dass eine seitliche 90-Grad-Drehung in der .NET-Version möglicherweise falsche Ergebnisse liefert. Daher müssen Sie nach Verwendung der RotateFlip
Funktion eine neue Bitmap
RotateFlip
.
Quellcode public static Bitmap RotateFlip(Bitmap bitmap, RotFlipType rotFlipType) { #if SCRIPTSHARP int t, i4, j4, w, h, c; if (rotFlipType == RotFlipType.RotateNoneFlipNone) return bitmap; GraphicsContext context; PixelArray data; if (rotFlipType == RotFlipType.RotateNoneFlipX) { context = GraphicsContext.GetContext(bitmap); data = context.GetPixelArray(); w = bitmap.Width; h = bitmap.Height; for (int i = 0; i < h; i++) { c = (i + 1) * w * 4 - 4; for (int j = 0; j < w / 2; j++) { i4 = (i * w + j) * 4; j4 = j * 4; t = (int)data[i4]; data[i4] = data[c - j4]; data[c - j4] = t; t = (int)data[i4 + 1]; data[i4 + 1] = data[c - j4 + 1]; data[c - j4 + 1] = t; t = (int)data[i4 + 2]; data[i4 + 2] = data[c - j4 + 2]; data[c - j4 + 2] = t; t = (int)data[i4 + 3]; data[i4 + 3] = data[c - j4 + 3]; data[c - j4 + 3] = t; } } context.PutImageData(); } else if (rotFlipType == RotFlipType.Rotate180FlipNone || rotFlipType == RotFlipType.Rotate180FlipX) { context = GraphicsContext.GetContext(bitmap); data = context.GetPixelArray(); w = bitmap.Width; h = bitmap.Height; c = w * 4 - 4; int dlength4 = data.Length - 4; for (int i = 0; i < data.Length / 4 / 2; i++) { i4 = i * 4; if (rotFlipType == RotFlipType.Rotate180FlipNone) j4 = i4; else j4 = (Math.Truncate((double)i / w) * w + (w - i % w)) * 4; t = (int)data[j4]; data[j4] = data[dlength4 - i4]; data[dlength4 - i4] = t; t = (int)data[j4 + 1]; data[j4 + 1] = data[dlength4 - i4 + 1]; data[dlength4 - i4 + 1] = t; t = (int)data[j4 + 2]; data[j4 + 2] = data[dlength4 - i4 + 2]; data[dlength4 - i4 + 2] = t; t = (int)data[j4 + 3]; data[j4 + 3] = data[dlength4 - i4 + 3]; data[dlength4 - i4 + 3] = t; } context.PutImageData(); } else { Bitmap tempBitmap = PrivateUtils.CreateCloneBitmap(bitmap); GraphicsContext tempContext = GraphicsContext.GetContext(tempBitmap); PixelArray temp = tempContext.GetPixelArray(); t = bitmap.Width; bitmap.Width = bitmap.Height; bitmap.Height = t; context = GraphicsContext.GetContext(bitmap); data = context.GetPixelArray(); w = tempBitmap.Width; h = tempBitmap.Height; if (rotFlipType == RotFlipType.Rotate90FlipNone || rotFlipType == RotFlipType.Rotate90FlipX) { c = w * h - w; for (int i = 0; i < temp.Length / 4; i++) { t = Math.Truncate((double)i / h); if (rotFlipType == RotFlipType.Rotate90FlipNone) i4 = i * 4; else i4 = (t * h + (h - i % h)) * 4; j4 = (c - w * (i % h) + t) * 4; //j4 = (w * (h - 1 - i4 % h) + i4 / h) * 4; data[i4] = temp[j4]; data[i4 + 1] = temp[j4 + 1]; data[i4 + 2] = temp[j4 + 2]; data[i4 + 3] = temp[j4 + 3]; } } else if (rotFlipType == RotFlipType.Rotate270FlipNone || rotFlipType == RotFlipType.Rotate270FlipX) { c = w - 1; for (int i = 0; i < temp.Length / 4; i++) { t = Math.Truncate((double)i / h); if (rotFlipType == RotFlipType.Rotate270FlipNone) i4 = i * 4; else i4 = (t * h + (h - i % h)) * 4; j4 = (c + w * (i % h) - t) * 4; // j4 = w * (1 + i4 % h) - i4 / h - 1; data[i4] = temp[j4]; data[i4 + 1] = temp[j4 + 1]; data[i4 + 2] = temp[j4 + 2]; data[i4 + 3] = temp[j4 + 3]; } } context.PutImageData(); } return bitmap; #elif DOTNET Bitmap result = null; switch (rotFlipType) { case RotFlipType.RotateNoneFlipNone: result = bitmap; break; case RotFlipType.Rotate90FlipNone: bitmap.RotateFlip(RotateFlipType.Rotate90FlipNone); result = new Image(bitmap); bitmap.Dispose(); break; case RotFlipType.Rotate270FlipNone: bitmap.RotateFlip(RotateFlipType.Rotate270FlipNone); result = new Image(bitmap); bitmap.Dispose(); break; case RotFlipType.Rotate180FlipNone: bitmap.RotateFlip(RotateFlipType.Rotate180FlipNone); result = bitmap; break; case RotFlipType.RotateNoneFlipX: bitmap.RotateFlip(RotateFlipType.RotateNoneFlipX); result = bitmap; break; case RotFlipType.Rotate90FlipX: bitmap.RotateFlip(RotateFlipType.Rotate90FlipX); result = new Image(bitmap); bitmap.Dispose(); break; case RotFlipType.Rotate180FlipX: bitmap.RotateFlip(RotateFlipType.Rotate180FlipX); result = bitmap; break; case RotFlipType.Rotate270FlipX: bitmap.RotateFlip(RotateFlipType.Rotate270FlipX); result = new Image(bitmap); bitmap.Dispose(); break; } return result; #endif }
Synchrones und asynchrones Laden von Bildern
Beachten Sie, dass wir in der Script # -Version eine andere Funktion CollageImageLoad
, die nach dem Laden eines Images aufgerufen wird, während diese Prozesse in der .NET-Version gleichzeitig stattfinden (aus einem Dateisystem oder dem Internet).
Quellcode public CollageData(string smallMaskPath, string bigMaskPath, List<CollageDataPart> dataParts) { SmallMaskImagePath = smallMaskPath; BigMaskImagePath = bigMaskPath; #if SCRIPTSHARP CurrentMask = PrivateUtils.CreateEmptyImage(); CurrentMask.AddEventListener("load", CollageImageLoad, false); CurrentMask.Src = CurrentMaskImagePath; #else CurrentMask = PrivateUtils.LoadBitmap(CurrentMaskImagePath); if (!CurrentMaskImagePath.Contains("http://") && !CurrentMaskImagePath.Contains("https://")) CurrentMask = Bitmap(CurrentMaskImagePath); else { var request = WebRequest.Create(CurrentMaskImagePath); using (var response = request.GetResponse()) using (var stream = response.GetResponseStream()) CurrentMask = (Bitmap)Bitmap.FromStream(stream); } #endif DataParts = dataParts; }
Nur Skript #
Erkennen des Typs und der Version eines Browsers
Diese Funktion wird verwendet, um die Drag & Drop-Funktionen in verschiedenen Browsern zu bestimmen. Ich habe versucht, modernizr zu verwenden, aber es wurde zurückgegeben, dass Safari und (in meinem Fall eine Win-Version) IE9 es implementieren. In der Praxis implementieren diese Browser Drag & Drop-Funktionen nicht korrekt.
Quellcode internal static string BrowserVersion { get { DetectBrowserTypeAndVersion(); return _browserVersion; } } private static void DetectBrowserTypeAndVersion() { if (!_browserDetected) { string userAgent = Window.Navigator.UserAgent.ToLowerCase(); if (userAgent.IndexOf("opera") != -1) _browser = BrowserType.Opera; else if (userAgent.IndexOf("chrome") != -1) _browser = BrowserType.Chrome; else if (userAgent.IndexOf("safari") != -1) _browser = BrowserType.Safari; else if (userAgent.IndexOf("firefox") != -1) _browser = BrowserType.Firefox; else if (userAgent.IndexOf("msie") != -1) { int numberIndex = userAgent.IndexOf("msie") + 5; _browser = BrowserType.IE; _browserVersion = userAgent.Substring(numberIndex, userAgent.IndexOf(';', numberIndex)); } else _browser = BrowserType.Unknown; _browserDetected = true; } }
Rendern einer Strichpunktlinie
Dieser Code wird für ein Rechteck zum Zuschneiden von Bildern verwendet. Vielen Dank für die Ideen an alle, die diese Frage zu stackoverflow beantwortet haben .
Quellcode internal static void DrawDahsedLine(GraphicsContext context, double x1, double y1, double x2, double y2, int[] dashArray) { if (dashArray == null) dashArray = new int[2] { 10, 5 }; int dashCount = dashArray.Length; double dx = x2 - x1; double dy = y2 - y1; bool xSlope = Math.Abs(dx) > Math.Abs(dy); double slope = xSlope ? dy / dx : dx / dy; context.MoveTo(x1, y1); double distRemaining = Math.Sqrt(dx * dx + dy * dy); int dashIndex = 0; while (distRemaining >= 0.1) { int dashLength = (int)Math.Min(distRemaining, dashArray[dashIndex % dashCount]); double step = Math.Sqrt(dashLength * dashLength / (1 + slope * slope)); if (xSlope) { if (dx < 0) step = -step; x1 += step; y1 += slope * step; } else { if (dy < 0) step = -step; x1 += slope * step; y1 += step; } if (dashIndex % 2 == 0) context.LineTo(x1, y1); else context.MoveTo(x1, y1); distRemaining -= dashLength; dashIndex++; } }
Rotationsanimation
setInterval
Funktion setInterval
wird verwendet, um eine Bildrotationsanimation zu implementieren. Beachten Sie, dass das Ergebnisbild während der Animation so berechnet wird, dass am Ende der Animation keine Verzögerungen auftreten.
Quellcode public void Rotate(bool cw) { if (!_rotating && !_flipping) { _rotating = true; _cw = cw; RotFlipType oldRotFlipType = _curRotFlipType; _curRotFlipType = RotateRotFlipValue(_curRotFlipType, _cw); int currentStep = 0; int stepCount = (int)(RotateFlipTimeSeconds * 1000 / StepTimeTicks); Bitmap result = null; _interval = Window.SetInterval(delegate() { if (currentStep < stepCount) { double absAngle = GetAngle(oldRotFlipType) + currentStep / stepCount * Math.PI / 2 * (_cw ? -1 : 1); DrawRotated(absAngle); currentStep++; } else { Window.ClearInterval(_interval); if (result != null) Draw(result); _rotating = false; } }, StepTimeTicks); result = GetCurrentTransformResult(); if (!_rotating) Draw(result); } } private void DrawRotated(double rotAngle) { _resultContext.FillColor = FillColor; _resultContext.FillRect(0, 0, _result.Width, _result.Height); _resultContext.Save(); _resultContext._graphics.Translate(_result.Width / 2, _result.Height / 2); _resultContext._graphics.Rotate(-rotAngle); _resultContext._graphics.Translate(-_origin.Width / 2, -_origin.Height / 2); _resultContext._graphics.DrawImage(_origin, 0, 0); _resultContext.Restore(); } private void Draw(Bitmap bitmap) { _resultContext.FillColor = FillColor; _resultContext.FillRect(0, 0, _result.Width, _result.Height); _resultContext.Draw2(bitmap, (int)((_result.Width - bitmap.Width) / 2), (int)((_result.Height - bitmap.Height) / 2)); }
Fazit
Dieser Artikel beschreibt, wie die C # -Sprache (Kombination von nicht verwaltetem Code und Kompilierung für JavaScript) verwendet werden kann, um eine wirklich plattformübergreifende Lösung zu erstellen. Trotz des Fokus auf .NET und JavaScript ist das Kompilieren auf Android, iOS (mit Mono) und Windows Phone auch auf der Grundlage dieses Ansatzes möglich, der natürlich seine Tücken hat. Der Code ist aufgrund seiner Universalität etwas redundant, beeinträchtigt jedoch nicht die Leistung, da Grafikvorgänge normalerweise erheblich länger dauern.