En 2013, alors que je travaillais au service photo du GFRANQ, j'ai participé au développement d'un service web éponyme pour la publication et le traitement des photos. Des filtres et des transformations ont été définis dans le fichier avec des paramètres, et tout le traitement a été effectué sur le serveur. Lors du développement du service, il était nécessaire de prendre en charge ces transformations côté client pour l'aperçu. Selon Larry Wall, l'une des vertus d'un programmeur est la paresse. Par conséquent, en tant que programmeurs vraiment paresseux, nous avons pensé à la possibilité d'utiliser le même code à la fois côté serveur et côté client. L'ensemble du développement a été réalisé en C #. Après avoir recherché les bibliothèques et quelques tentatives, nous avons fièrement conclu que cela était possible et avons commencé à écrire le code universel.

Pourquoi cet article est-il nécessaire? En effet, 6 années se sont écoulées depuis 2013, et de nombreuses technologies ont perdu de leur pertinence, par exemple Script # . En revanche, de nouveaux sont apparus. Par exemple, Bridge.NET ou Blazor basé sur le WebAssembly de fantaisie.
Néanmoins, certaines idées peuvent encore être utilisées. Dans cet article, j'ai essayé de les décrire le plus en détail possible. J'espère que la mention de Silverlight et Flash provoquera un sourire avec un soupçon de nostalgie, et non une envie de critiquer les anciennes solutions. Quoi qu'il en soit, ils ont contribué au développement de l'industrie du Web.
Table des matières
Objectif
Le défi consiste à mettre en œuvre le collage de photos et la fonctionnalité de retouche photo basée sur un filtre côté client et, si possible, côté serveur également. Pour commencer, je vais couvrir comment les filtres et les collages sont mis en œuvre.
Description des filtres
Dans le cadre de notre projet, un filtre est une série d'actions réalisées dans Photoshop et appliquées à une photo particulière. Voici des exemples de telles actions:
- Réglage de la luminosité
- Réglage du contraste
- Réglage de la saturation
- Ajustement des courbes de couleur
- Masquage dans différents modes
- Encadrement
- ...
Nous avons besoin d'un certain format pour décrire ces actions. Bien sûr, il existe des formats courants tels que JSON et XML, mais il a été décidé de créer notre propre format pour les raisons suivantes:
- Besoin d'une architecture de code indépendante de la plateforme (.NET, JavaScript, WinPhone, etc.)
- Besoin d'un format simple non hiérarchique de filtres, ce qui facilite l'écriture d'un analyseur
- Les données XML et JSON consomment plus de mémoire (dans ce cas particulier)
Voici à quoi ressemble la séquence d'actions pour le filtre XPro Film :
Outre l'édition d'une photo avec un filtre, nous devions recadrer et faire pivoter l'image. Oui, je savais qu'il existe des plugins jQuery pour recadrer et faire pivoter les images, mais ils semblaient surchargés et s'écartaient de l'architecture universelle du projet.
Description des collages
Un collage est un arrangement de plusieurs photos miniaturisées en une seule photo entière (avec ou sans masque). Il était également nécessaire de permettre aux utilisateurs de glisser-déposer les images disponibles sur le collage, de modifier leur position et leur échelle. Votre collage peut ressembler à ceci:
La fonction de collage implique l'utilisation d'un format simple pour stocker des rectangles avec des coordonnées relatives de 0
à 1
, les adresses des photos et les données de modification d'image. Les coordonnées relatives sont utilisées car les mêmes transformations côté client sont appliquées aux images de grande taille côté serveur.
Implémentation
Nous avons dû choisir la plateforme permettant aux utilisateurs de travailler avec des filtres et des collages
Il existe plusieurs technologies Rich Internet Application ( RIA ) telles que:
- Adobe flash
- Microsoft silverlight
- HTML 5 + JavaScript
- Client natif
Pour des raisons évidentes, Flash et HTML sont les seules technologies qui méritent l'attention car les autres ne sont pas compatibles avec plusieurs plates-formes. De plus, le client Silverlight commence à mourir. Bien que j'aime vraiment le concept de du sel NaCl, malheureusement, cette technologie n'est prise en charge que par le navigateur Chrome et on ne sait pas encore quand elle sera prise en charge (et le sera jamais) par d'autres navigateurs populaires. Remarque à partir de 2019: il le sera et le nom est WebAssembly .
Le choix a été fait en faveur de la plate-forme HTML5 tendance et progressive, dont la fonctionnalité est actuellement prise en charge par iOS, par opposition à Flash. Ce choix est également basé sur le fait qu'il existe de nombreuses bibliothèques, qui vous permettent de compiler le code C # en Javascript. Vous pouvez également utiliser Visual Studio à cette fin. Les détails sont donnés ci-dessous.
Traduire C # en Javascript
HTML 5 + JavaScript a été sélectionné comme plateforme dans la section précédente. Cela nous laisse donc une question, s'il est possible d'écrire un code C # universel qui pourrait être compilé à la fois .NET et JavaScript.
Ainsi, un certain nombre de bibliothèques pour accomplir la tâche ont été trouvées:
- Jsil
- Sharpkit
- Script #
- Et quelques autres disponibles sur GitHub .
En conséquence, il a été décidé d'utiliser Script # car JSIL fonctionne directement avec les assemblys et génère du code moins pur (bien qu'il prenne en charge une plus large gamme de fonctionnalités du langage C #) et SharpKit est un produit commercial. Pour une comparaison détaillée de ces outils, voir la question sur stackoverflow .
Pour résumer, ScriptSharp par rapport à JavaScript écrit manuellement présente les avantages et les inconvénients suivants:
Les avantages
- Possibilité d'écrire un code C # universel pouvant être compilé sur .NET et d'autres plateformes (WinPhone, Mono)
- Développement dans un langage C # fortement typé supportant la POO
- Prise en charge des fonctionnalités IDE (autocomplétion et refactoring)
- Capacité à détecter la majorité des erreurs au stade de la compilation
Inconvénients
- Redondance et irrégularité du code JavaScript généré (en raison de mscorlib).
- Prise en charge ISO-2 uniquement (pas de surcharge de fonction ou d'inférence de type, d'extension et de génériques)
La structure
Le processus de compilation du même code C # en .NET et Javascript peut être illustré par le schéma suivant:
Bien que .NET et HTML5 soient des technologies complètement différentes, ils ont également des fonctionnalités similaires. Cela s'applique également au travail avec les graphiques. Par exemple, .NET prend en charge Bitmap , JavaScript prend en charge son analogue - Canvas . Il en va de même pour les graphiques , le contexte et les tableaux de pixels. Afin de tout combiner en un seul code, il a été décidé de développer l'architecture suivante:
Bien sûr, il n'est pas limité à deux plateformes. À titre de suivi, il est prévu d'ajouter la prise en charge de WinPhone, puis, peut-être, d'Android et d'iOS.
Il est à noter qu'il existe deux types d'opérations graphiques:
- Utilisation des fonctions API (
DrawImage
, Arc
, MoveTo
, LineTo
). Les performances élevées et la prise en charge de l'accélération matérielle sont des avantages compétitifs importants. L'inconvénient est qu'ils peuvent être implémentés différemment sur différentes plateformes. - Pixel par pixel. La prise en charge de la mise en œuvre de tous les effets et la couverture multiplateforme sont parmi les avantages. L'inconvénient est la faible performance. Cependant, vous pouvez atténuer les inconvénients par la parallélisation, les shaders et les tables précalculées (nous en discuterons plus loin dans la section suivante sur l'optimisation).
Comme vous pouvez le voir, la classe abstraite Graphics décrit toutes les méthodes de travail avec les graphiques; ces méthodes sont implémentées pour différentes plates-formes dans la classe dérivée. Les alias suivants ont également été écrits pour résumer des classes Bitmap et Canvas. La version WinPhone utilise également un modèle d'adaptateur .
Utiliser un alias
#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
Malheureusement, il est impossible de créer des alias pour les types et tableaux non sécurisés, en d'autres termes, Alias à pointer (octet *) en C # :
using PixelArray = byte*, using PixelArray = byte[]
Pour effectuer un traitement rapide des pixels à l'aide de code C # non géré, tout en le compilant en Script #, nous avons introduit le schéma suivant à l'aide de directives:
#if SCRIPTSHARP PixelArray data = context.GetPixelArray(); #elif DOTNET byte* data = context.GetPixelArray(); #endif
Le tableau de data
est ensuite utilisé pour implémenter diverses opérations pixel par pixel (telles que le masquage, le fisheye, le réglage de la saturation, etc.), parallélisées ou non.
Liens vers des fichiers
Un projet distinct est ajouté à la solution pour chaque plate-forme, mais, bien sûr, Mono, Script # et même Silverlight ne peuvent pas faire référence aux assemblys .NET habituels. Heureusement, Visual Studio dispose d'un mécanisme pour ajouter des liens vers des fichiers, ce qui vous permet de réutiliser le même code dans différents projets.
Les directives du compilateur ( DOTNET
, SCRIPTSHARP
) sont définies dans les propriétés du projet dans les symboles de compilation conditionnelle.
Remarques sur la mise en œuvre de .NET
Les abstractions et les alias ci-dessus nous ont aidés à écrire le code C # avec une faible redondance. De plus, je tiens à souligner les problèmes avec les plates-formes .NET et JavaScript que nous avons rencontrés lors du développement du code de la solution.
Utilisation de disposer
Veuillez noter que l'inclusion de toute instance d'une classe C #, qui implémente l'interface IDisposable
, nécessite d'appeler la méthode Dispose
ou d'appliquer l' instruction Using . Dans ce projet, ces classes sont Bitmap et Context. Ce que j'ai dit ci-dessus n'est pas seulement la théorie, il a en fait une application pratique: le traitement d'un grand nombre de photos de grande taille (jusqu'à 2400 x 2400 dpi) sur ASP.NET Developer Server x86 a entraîné une exception de mémoire insuffisante. Le problème a été résolu après avoir ajouté Dispose
aux bons endroits. D'autres conseils utiles sur la manipulation d'image sont donnés dans l'article suivant 20 Pièges de redimensionnement d'image et fuite de mémoire .NET: Pour éliminer ou ne pas éliminer, c'est la question de 1 Go .
Utilisation du verrou
En JavaScript, il existe une différence entre l'image déjà téléchargée avec la balise img
, pour laquelle vous pouvez spécifier la source et l'événement de chargement, et la toile marquée avec canvas
, sur laquelle vous pouvez dessiner quelque chose. Dans .NET, ces éléments sont représentés par la même classe Bitmap
. Ainsi, les alias Bitmap et Image dans .NET pointent vers la même classe System.Drawing.Bitmap
. Bitmap comme indiqué ci-dessus.
Néanmoins, cette division en img
et canvas
en JavaScript a également été très utile dans la version .NET. Le fait est que les filtres utilisent des masques préchargés de différents threads; ainsi, le motif de verrouillage est nécessaire pour éviter l'exception lors de la synchronisation (l'image est copiée avec verrouillage et le résultat est utilisé sans verrouillage):
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 }
Après tout, le verrouillage doit également être utilisé lors de l'accès aux propriétés d'un objet synchronisé (en fait, toutes les propriétés sont des méthodes).
Stockage des masques en mémoire
Pour accélérer le traitement, tous les masques potentiellement utilisés pour les filtres sont chargés en mémoire au démarrage du serveur. Quel que soit le format du masque, le bitmap téléchargé sur le serveur utilise 4 * 2400 * 2400
ou ≈24 MB
de mémoire (la taille d'image maximale est de 2400 * 2400
; le nombre d'octets par pixel est de 4). Tous les masques pour les filtres (≈30) et les collages (40) consommeront 1,5 Go - ce n'est pas beaucoup pour le serveur; cependant, à mesure que le nombre de masques augmente, cette quantité peut augmenter considérablement. À l'avenir, nous utiliserons éventuellement des techniques de compression pour les masques stockés en mémoire (aux formats .jpg et .png) suivies d'une décompression si nécessaire. En fait, la taille peut être réduite jusqu'à 300 fois. Un avantage supplémentaire de cette approche est que la copie des images compressées va plus vite que les grandes; ainsi, l'opération de verrouillage prendra moins de temps et les threads seront bloqués moins souvent.
Remarques sur la mise en œuvre de JavaScript
Minification
J'ai refusé d'utiliser le terme «obscurcissement» pour la raison suivante: ce terme est à peine applicable à un langage entièrement open-source, qui dans notre cas est JavaScript. Cependant, l'anonymisation des identifiants peut perturber la lisibilité et la logique du code. Et surtout, cette technique réduira considérablement la taille du script (la version compressée est de ≈80 Ko).
Il existe deux approches de la minification JavaScript:
- Minification manuelle, qui est effectuée au stade de la génération à l'aide de ScriptSharp.
- Minification automatisée, qui est effectuée après la phase de génération à l'aide d'outils externes tels que Google Closure Compiler, Yui et d'autres outils.
Minification manuelle
Pour raccourcir les noms des méthodes, classes et attributs, nous avons utilisé cette syntaxe avant la déclaration des entités mentionnées ci-dessus. Bien sûr, cela n'est pas nécessaire si vous travaillez avec des méthodes appelées à partir de scripts et de classes externes (publics).
#if SCRIPTSHARP && !DEBUG [ScriptName("a0")] #endif
Quoi qu'il en soit, les variables locales n'ont pas pu être minimisées. Ces constructions polluent le code et nuisent à la lisibilité du code, ce qui est également un sérieux inconvénient. Cependant, cette technique peut réduire considérablement la quantité de code JavaScript généré et le gâcher également.
Un autre inconvénient est que vous devez garder un œil sur ces noms courts s'ils renomment les noms de méthode et de champ (en particulier, les noms remplacés dans les classes enfants) car dans ce cas, Script # ne se souciera pas des noms répétitifs. Cependant, il n'autorisera pas les classes dupliquées.
Soit dit en passant, la fonctionnalité de minification des méthodes et des champs privés et internes a déjà été ajoutée à la version développée du Script #.
Minification automatisée
Bien qu'il existe de nombreux outils pour la minification JavaScript, j'ai utilisé le Google Closure Compiler pour sa marque et une bonne qualité de compression. L'inconvénient de l'outil de minification de Google est qu'il ne peut pas compresser les fichiers CSS; en revanche, YUI relève ce défi avec succès. En fait, Script # peut également réduire les scripts mais gère ce défi bien pire que Google Closure.
L'outil de minification de Google a plusieurs niveaux de compression: espace, simple et avancé. Nous avons choisi le niveau Simple pour le projet; bien que le niveau avancé nous permette d'atteindre la qualité de compression maximale, il nécessite du code écrit de telle manière que les méthodes soient accessibles de l'extérieur de la classe. Cette minification a été partiellement effectuée manuellement à l'aide de Script #.
Modes de débogage et de publication
Les bibliothèques de débogage et de publication ont été ajoutées aux pages ASP.NET comme suit:
<% 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> <% } %>
Dans ce projet, nous avons réduit à la fois les scripts et les fichiers de description des filtres.
crossOrigin, propriété
Pour accéder aux pixels d'une image particulière, nous devons d'abord la convertir en toile. Mais cela peut conduire à une erreur CORS (Cross Origin Request Security). Dans notre cas, le problème a été résolu comme suit:
- Définition de l'
crossOrigin = ''
côté serveur. - Ajout d'un en-tête spécifique au package HTTP côté serveur.
Étant donné que ScriptSharp ne prend pas en charge cette propriété pour les éléments img, le code suivant a été écrit:
[Imported] internal class AdvImage { [IntrinsicProperty] internal string CrossOrigin { get { return string.Empty; } set { } } }
Ensuite, nous l'utiliserons comme ceci:
((AdvImage)(object)result).CrossOrigin = "";
Cette technique vous permet d'ajouter n'importe quelle fonction à l'objet sans erreurs de compilation. En particulier, la propriété wheelDelta
n'est pas encore implémentée dans ScriptSharp (au moins dans la version 0.7.5). Cette propriété indique le montant de la molette de défilement, qui est utilisé pour créer des collages. C'est pourquoi il a été mis en œuvre de cette façon. Un tel hack sale avec les propriétés n'est pas bon; normalement, vous devez apporter des modifications au projet. Mais pour mémoire, je n'ai pas encore trouvé de moyen de compiler ScriptSharp à partir des sources.
De telles images nécessitent que le serveur renvoie les en-têtes suivants dans ses en-têtes de réponse (dans Global.asax):
Response.AppendHeader("Access-Control-Allow-Origin", "\*");
Pour plus d'informations sur Cross Origin Request Security, visitez http://enable-cors.org/ .
Optimisations
Utilisation des valeurs précalculées
Nous avons utilisé l'optimisation pour certaines opérations telles que la luminosité, le contraste et l'ajustement des courbes de couleur via le calcul préliminaire des composantes de couleur résultantes (r, g, b) pour toutes les valeurs possibles et une utilisation plus poussée des tableaux obtenus pour changer directement les couleurs des pixels . Il convient de noter que ce type d'optimisation ne convient que pour des opérations dans lesquelles la couleur du pixel résultant n'est pas affectée par le pixel adjacent.
Le calcul des composantes de couleur résultantes pour toutes les valeurs possibles:
for (int i = 0; i < 256; i++) { r[i] = ActionFuncR(i); g[i] = ActionFuncG(i); b[i] = ActionFuncB(i); }
L'utilisation de composants de couleur précalculés:
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]]; }
Si de telles opérations de table vont une par une, il n'est pas nécessaire de calculer des images intermédiaires - vous pouvez passer uniquement les tableaux de composants de couleur. Le code fonctionnant assez rapidement côté client et côté serveur, il a été décidé de mettre de côté l'implémentation de cette optimisation. En outre, l'optimisation a provoqué un comportement indésirable. Cependant, je vais vous donner une liste de l'optimisation:
Code d'origine | Code optimisé |
`` `cs // Calcul des valeurs pour la première table. pour (int i = 0; i <256; i ++) { r [i] = ActionFunc1R (i); g [i] = ActionFunc1G (i); b [i] = ActionFunc1B (i); } // ...
// Calcul de l'image intermédiaire résultante. pour (int i = 0; i <data.Length; i + = 4) { données [i] = r [données [i]]; données [i + 1] = g [données [i + 1]]; données [i + 2] = b [données [i + 2]]; } // ...
// Calcul des valeurs pour la deuxième table. pour (int i = 0; i <256; i ++) { r [i] = ActionFunc2R (i); g [i] = ActionFunc2G (i); b [i] = ActionFunc2B (i); } // ...
// Calcul de l'image résultante. pour (int i = 0; i <data.Length; i + = 4) { données [i] = r [données [i]]; données [i + 1] = g [données [i + 1]]; données [i + 2] = b [données [i + 2]]; } `` ``
| `` `cs // Calcul des valeurs pour la première table. pour (int i = 0; i <256; i ++) { r [i] = ActionFunc1R (i); g [i] = ActionFunc1G (i); b [i] = ActionFunc1B (i); } // ...
// Calcul des valeurs pour la deuxième table. tr = r.Clone (); tg = g.Clone (); tb = b.Clone (); pour (int i = 0; i <256; i ++) { r [i] = tr [ActionFunc2R (i)]; g [i] = tg [ActionFunc2G (i)]; b [i] = tb [ActionFunc2B (i)]; } // ...
// Calcul de l'image résultante. pour (int i = 0; i <data.Length; i + = 4) { données [i] = r [données [i]]; données [i + 1] = g [données [i + 1]]; données [i + 2] = b [données [i + 2]]; } `` ``
|
Mais même cela, ce n'est pas tout. Si vous regardez le tableau de droite, vous remarquerez que de nouveaux tableaux sont créés à l'aide de la méthode Clone
. En fait, vous pouvez simplement changer les pointeurs sur les anciens et les nouveaux tableaux au lieu de copier le tableau lui-même (cela rappelle l'analogie de la double mise en mémoire tampon ).
Conversion d'une image en un tableau de pixels
Le profileur JavaScript dans Google Chrome a révélé que la fonction GetImageData
(qui est utilisée pour convertir le canevas en tableau de pixels) fonctionne suffisamment longtemps. Soit dit en passant, ces informations peuvent être trouvées dans divers articles sur l'optimisation de Canvas en JavaScript.
Cependant, le nombre d'appels de cette fonction peut être minimisé. À savoir, nous pouvons utiliser le même tableau de pixels pour les opérations pixel par pixel, par analogie avec l'optimisation précédente.
Exemples de code
Dans les exemples ci-dessous, je fournirai les fragments de code que j'ai trouvé intéressants et utiles. Pour éviter que l'article ne soit trop long, j'ai caché les exemples sous un spoiler.
Général
Détecter si une chaîne est un nombre
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 }
Division entière
internal static int Div(int n, int k) { int result = n / k; #if SCRIPTSHARP result = Math.Floor(n / k); #endif return result; }
Rotation et retournement d'une image à l'aide du canevas et du bitmap
Veuillez noter qu'en html5, les images de canevas peuvent être pivotées de 90 et 180 degrés uniquement à l'aide de matrices, tandis que .NET offre des fonctionnalités améliorées. Ainsi, une fonction précise appropriée pour travailler avec les pixels a été écrite.
Il convient également de noter qu'une rotation de 90 degrés sur n'importe quel côté dans la version .NET peut renvoyer des résultats incorrects. Par conséquent, vous devez créer un nouveau Bitmap
après avoir utilisé la fonction RotateFlip
.
Code source 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 }
Chargement d'images synchrones et asynchrones
Notez que dans la version Script #, nous CollageImageLoad
une fonction différente CollageImageLoad
, qui est appelée après le chargement d'une image, tandis que dans la version .NET, ces processus se déroulent simultanément (à partir d'un système de fichiers ou d'Internet).
Code source 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; }
Script # uniquement
Détection du type et de la version d'un navigateur
Cette fonction est utilisée pour déterminer les capacités de glisser-déposer dans différents navigateurs. J'ai essayé d'utiliser modernizr , mais il est revenu que Safari et (dans mon cas, c'était une version Win) IE9 l'implémentent. En pratique, ces navigateurs ne parviennent pas à implémenter correctement les fonctionnalités de glisser-déposer.
Code source 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; } }
Rendu d'une ligne pointillée
Ce code est utilisé pour un rectangle de recadrage d'images. Merci pour les idées à tous ceux qui ont répondu à cette question sur stackoverflow .
Code source 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++; } }
Animation de rotation
setInterval
fonction setInterval
est utilisée pour implémenter une animation de rotation d'image. Notez que l'image résultante est calculée pendant l'animation afin qu'il n'y ait aucun décalage à la fin de l'animation.
Code source 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)); }
Conclusion
Cet article décrit comment le langage C # (combinant du code non managé et la compilation pour JavaScript) peut être utilisé pour créer une solution vraiment multiplateforme. Malgré l'accent mis sur .NET et JavaScript, la compilation sur Android, iOS (en utilisant Mono) et Windows Phone est également possible en fonction de cette approche, qui, bien sûr, a ses pièges. Le code est un peu redondant en raison de son universalité, mais il n'affecte pas les performances car les opérations graphiques prennent généralement beaucoup plus de temps.