Código universal C # para NET y JavaScript

En 2013, mientras trabajaba en el servicio de fotograf√≠a GFRANQ, particip√© en el desarrollo de un servicio web hom√≥nimo para publicar y procesar fotos. Los filtros y las transformaciones se definieron en el archivo con par√°metros, y todo el procesamiento se realiz√≥ en el servidor. Durante el desarrollo del servicio, era necesario admitir estas transformaciones en el lado del cliente para la vista previa. Seg√ļn Larry Wall, una de las virtudes de un programador es la pereza. Por lo tanto, como programadores realmente vagos, pensamos en la posibilidad de usar el mismo c√≥digo tanto en el lado del servidor como en el del cliente. Todo el desarrollo se realiz√≥ en C #. Despu√©s de investigar las bibliotecas y un par de intentos, concluimos con orgullo que esto era posible y comenzamos a escribir el c√≥digo universal.



¬ŅPor qu√© se necesita este art√≠culo? De hecho, han pasado 6 a√Īos desde 2013, y muchas tecnolog√≠as han perdido su relevancia, por ejemplo, Script # . Por otro lado, han aparecido otros nuevos. Por ejemplo, Bridge.NET o Blazor basado en el elegante WebAssembly .


Sin embargo, algunas ideas todavía se pueden utilizar. En este artículo intenté describirlos lo más detallado posible. Espero que la mención de Silverlight y Flash provoque una sonrisa con un toque de nostalgia, y no un deseo de criticar las viejas soluciones. De todos modos, han contribuido al desarrollo de la industria web.


Contenido





Gol


El desafío es implementar el collage de fotos y la funcionalidad de edición de fotos basada en filtros en el lado del cliente y, si es posible, también en el lado del servidor. Para empezar, cubriré cómo se implementan los filtros y collages.


Descripción de filtros


En el contexto de nuestro proyecto, un filtro es una serie de acciones realizadas en Photoshop y aplicadas a una foto en particular. A continuación se muestran los ejemplos de tales acciones:


  • Ajuste de brillo
  • Ajuste de contraste
  • Ajuste de saturaci√≥n
  • Ajuste de curvas de color
  • Enmascaramiento en diferentes modos
  • Enmarcado
  • ...

Necesitamos un cierto formato para describir estas acciones. Claro, hay formatos comunes como JSON y XML, pero se decidió crear nuestro propio formato por las siguientes razones:


  • Necesidad de una arquitectura de c√≥digo independiente de la plataforma (.NET, JavaScript, WinPhone, etc.)
  • Se necesita un formato de filtros simple no jer√°rquico, que facilite la escritura de un analizador
  • Los datos XML y JSON consumen m√°s memoria (en este caso particular)

Así es como se ve la secuencia de acciones para el filtro de película XPro :



Además de editar una foto con un filtro, necesitábamos recortar y rotar la imagen. Sí, sabía que hay complementos de jQuery para recortar y rotar imágenes, pero parecían estar sobrecargados y desviarse de la arquitectura universal del proyecto.


Descripción de collages


Un collage es una disposición de varias fotos en miniatura en una sola foto completa (con o sin máscara). También era necesario permitir a los usuarios arrastrar y soltar las imágenes disponibles en el collage, cambiar su posición y escala. Su collage puede verse así:



La funci√≥n de collage implica el uso de un formato simple para almacenar rect√°ngulos con coordenadas relativas de 0 a 1 , las direcciones de las fotos y los datos de modificaci√≥n de la imagen. Las coordenadas relativas se utilizan porque las mismas transformaciones del lado del cliente se aplican a las im√°genes de gran tama√Īo en el lado del servidor.


Implementación


Tuvimos que elegir la plataforma que permitiera a los usuarios trabajar con filtros y collages.


Elegir una plataforma para el procesamiento de fotos


Existen varias tecnologías de aplicaciones de Internet enriquecidas ( RIA ) como:


  • Adobe flash
  • Microsoft Silverlight
  • HTML 5 + JavaScript
  • Cliente nativo

Por razones obvias, Flash y HTML son las √ļnicas tecnolog√≠as que merecen atenci√≥n, ya que el resto no son compatibles con plataformas cruzadas. Adem√°s, el cliente Silverlight est√° comenzando a morir. Aunque realmente me gusta el concepto de sal NaCl, desafortunadamente, esta tecnolog√≠a solo es compatible con el navegador Chrome y a√ļn no se sabe cu√°ndo ser√° compatible (y ser√° admitido) por otros navegadores populares. Nota de 2019: lo har√° y el nombre es WebAssembly .


La elección se hizo a favor de la plataforma HTML5 moderna y progresiva, cuya funcionalidad es actualmente compatible con iOS, en oposición a Flash. Esta elección también se basa en el hecho de que hay muchas bibliotecas, que le permiten compilar el código C # en Javascript. También puede usar Visual Studio para este propósito. Los detalles se dan a continuación.


Traducción de C # a Javascript


HTML 5 + JavaScript ha sido seleccionado como plataforma en la sección anterior. Por lo tanto, nos deja una pregunta, si es posible escribir un código C # universal que pueda compilarse tanto en .NET como en JavaScript.


Por lo tanto, se encontraron varias bibliotecas para realizar la tarea:


  • Jsil
  • Sharpkit
  • Script #
  • Y algunos otros disponibles en GitHub .

Como resultado, se decidió usar Script # debido al hecho de que JSIL trabaja directamente con ensamblajes y genera código menos puro (aunque admite una gama más amplia de características de lenguaje C #) y SharpKit es un producto comercial. Para una comparación detallada de estas herramientas, vea la pregunta sobre stackoverflow .


En resumen, ScriptSharp en comparación con JavaScript escrito manualmente tiene los siguientes pros y contras:


Ventajas


  • Posibilidad de escribir un c√≥digo C # universal que pueda compilarse en .NET y otras plataformas (WinPhone, Mono)
  • Desarrollo en un lenguaje C # fuertemente tipado que admite OOP
  • Soporte para funciones IDE (autocompletado y refactorizaci√≥n)
  • Capacidad para detectar la mayor√≠a de los errores en la etapa de compilaci√≥n

Desventajas


  • Redundancia e irregularidad del c√≥digo JavaScript generado (debido a mscorlib).
  • Compatibilidad solo con ISO-2 (sin sobrecarga de funciones o tipo, extensi√≥n e inferencia gen√©rica)

Estructura


El siguiente esquema puede ilustrar el proceso de compilación del mismo código C # en .NET y Javascript:


Traducción de C # a .NET y esquema de JavaScript

Aunque .NET y HTML5 son tecnologías completamente diferentes, también tienen características similares. Esto también se aplica al trabajo con gráficos. Por ejemplo, .NET admite Bitmap , JavaScript admite su análogo: Canvas . Lo mismo ocurre con los gráficos , el contexto y las matrices de píxeles. Para combinarlo todo en un código, se decidió desarrollar la siguiente arquitectura:


Contexto gr√°fico com√ļn de .NET y JavaScript

Por supuesto, no se limita a dos plataformas. Como seguimiento, se planea agregar soporte para WinPhone, y luego, tal vez, Android e iOS.


Cabe se√Īalar que hay dos tipos de operaciones gr√°ficas:


  • Uso de funciones API ( DrawImage , Arc , MoveTo , LineTo ). El alto rendimiento y el soporte para la aceleraci√≥n de hardware son importantes ventajas competitivas. El inconveniente es que se pueden implementar de manera diferente en diferentes plataformas.
  • Pixel por pixel. El soporte para la implementaci√≥n de cualquier efecto y la cobertura multiplataforma se encuentran entre los beneficios. La desventaja es el bajo rendimiento. Sin embargo, puede mitigar las desventajas mediante paralelizaci√≥n, sombreadores y tablas precalculadas (lo discutiremos m√°s adelante en la pr√≥xima secci√≥n sobre optimizaci√≥n).

Como puede ver, la clase abstracta Graphics describe todos los métodos para trabajar con gráficos; Estos métodos se implementan para varias plataformas en la clase derivada. Los siguientes alias también se escribieron para abstraer de las clases Bitmap y Canvas. La versión de WinPhone también usa un patrón adaptador .


Usando 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 

Desafortunadamente, es imposible crear alias para tipos y matrices inseguros, en otras palabras, Alias ‚Äč‚Äčpara puntero (byte *) en C # :


 using PixelArray = byte*, using PixelArray = byte[] 

Para realizar un procesamiento rápido de píxeles utilizando el código C # no administrado, al mismo tiempo que lo compilamos en Script #, presentamos el siguiente esquema con la ayuda de directivas:


 #if SCRIPTSHARP PixelArray data = context.GetPixelArray(); #elif DOTNET byte* data = context.GetPixelArray(); #endif 

La matriz de data se utiliza posteriormente para implementar varias operaciones de píxel por píxel (como enmascaramiento, ojo de pez, ajuste de saturación, etc.), tanto en paralelo como no.



Se agrega un proyecto separado a la solución para cada plataforma, pero, por supuesto, Mono, Script # e incluso Silverlight no pueden hacer referencia a los ensamblados habituales de .NET. Afortunadamente, Visual Studio tiene un mecanismo para agregar enlaces a archivos, que le permite reutilizar el mismo código en diferentes proyectos.


Las directivas del compilador ( DOTNET , SCRIPTSHARP ) se definen en las propiedades del proyecto en Símbolos de compilación condicional.


Notas sobre la implementación de .NET


Las abstracciones y alias anteriores nos ayudaron a escribir el c√≥digo C # con baja redundancia. Adem√°s, quiero se√Īalar los problemas con las plataformas .NET y JavaScript que enfrentamos al desarrollar el c√≥digo de la soluci√≥n.


Usando disponer


Tenga en cuenta que la inclusi√≥n de cualquier instancia de una clase C #, que implemente la interfaz IDisposable , requiere llamar al m√©todo Dispose o aplicar la instrucci√≥n Using . En este proyecto, estas clases son Bitmap y Contexto. Lo que he dicho anteriormente no es solo la teor√≠a, en realidad tiene una aplicaci√≥n pr√°ctica: el procesamiento de una gran cantidad de fotos de gran tama√Īo (hasta 2400 x 2400 ppp) en ASP.NET Developer Server x86 result√≥ en una excepci√≥n de memoria insuficiente. El problema se resolvi√≥ despu√©s de agregar Dispose en los lugares correctos. En el siguiente art√≠culo 20 se ofrecen otros consejos √ļtiles sobre la manipulaci√≥n de im√°genes en el art√≠culo 20 Errores de cambio de tama√Īo de imagen y p√©rdida de memoria .NET: Eliminar o no, esa es la pregunta de 1 GB .


Usando cerradura


En JavaScript, hay una diferencia entre la imagen ya cargada con la etiqueta img , para la que puede especificar el origen y el evento de carga, y el lienzo etiquetado con canvas , en el que puede dibujar algo. En .NET, estos elementos est√°n representados por la misma clase de Bitmap . Por lo tanto, los alias Bitmap e Image en .NET apuntan a la misma clase System.Drawing.Bitmap . Bitmap como se muestra arriba.


Sin embargo, esta divisi√≥n en img y canvas en JavaScript tambi√©n fue muy √ļtil en la versi√≥n .NET. El punto es que los filtros usan m√°scaras precargadas de diferentes hilos; por lo tanto, se requiere el patr√≥n de bloqueo para evitar la excepci√≥n durante la sincronizaci√≥n (la imagen se copia con bloqueo y el resultado se usa sin bloqueo):


 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 } 

Después de todo, el bloqueo también debe usarse al acceder a las propiedades de un objeto sincronizado (de hecho, cualquier propiedad es un método).


Almacenar m√°scaras en la memoria


Para acelerar el procesamiento, todas las m√°scaras potencialmente utilizadas para los filtros se cargan en la memoria cuando se inicia el servidor. ‚Čą24 MB del formato de la m√°scara, el mapa de bits cargado en el servidor usa 4 * 2400 * 2400 o ‚Čą24 MB de memoria (el tama√Īo m√°ximo de la imagen es 2400 * 2400 ; el n√ļmero de bytes por p√≠xel es 4). Todas las m√°scaras para filtros (‚Čą30) y collages (40) consumir√°n 1,5 GB, lo que no es mucho para el servidor; sin embargo, a medida que crece el n√ļmero de m√°scaras, esta cantidad puede aumentar significativamente. En el futuro, posiblemente utilizaremos t√©cnicas de compresi√≥n para las m√°scaras almacenadas en la memoria (en formatos .jpg y .png) seguidas de descompresi√≥n cuando sea necesario. En realidad, el tama√Īo se puede reducir hasta 300 veces. Una ventaja adicional de este enfoque es que la copia de las im√°genes comprimidas es m√°s r√°pida en comparaci√≥n con las grandes; por lo tanto, la operaci√≥n de bloqueo tomar√° menos tiempo y los hilos se bloquear√°n con menos frecuencia.


Notas sobre la implementación de JavaScript


Minificación


Me negu√© a usar el t√©rmino "ofuscaci√≥n" por la siguiente raz√≥n: este t√©rmino apenas es aplicable a un lenguaje de c√≥digo abierto, que en nuestro caso es JavaScript. Sin embargo, el anonimato de los identificadores puede alterar la legibilidad y la l√≥gica del c√≥digo. Y lo m√°s importante, esta t√©cnica reducir√° significativamente el tama√Īo del script (la versi√≥n comprimida es is80 KB).


Hay dos enfoques para la minificación de JavaScript:


  • Minificaci√≥n manual, que se realiza en la etapa de generaci√≥n usando ScriptSharp.
  • Minificaci√≥n automatizada, que se realiza despu√©s de la etapa de generaci√≥n utilizando herramientas externas como Google Closure Compiler, Yui y otras herramientas.

Minificación manual

Para acortar los nombres de m√©todos, clases y atributos, utilizamos esta sintaxis antes de la declaraci√≥n de las entidades mencionadas anteriormente. Por supuesto, no hay necesidad de hacerlo si est√° trabajando con m√©todos que se invocan desde scripts externos y clases (p√ļblicas).


 #if SCRIPTSHARP && !DEBUG [ScriptName("a0")] #endif 

De todos modos, las variables locales no pudieron ser minimizadas. Estas construcciones contaminan el código y perjudican la legibilidad del código, lo que también es una desventaja grave. Sin embargo, esta técnica puede reducir significativamente la cantidad de código JavaScript generado y también estropearlo.


Otra desventaja es que debe vigilar esos nombres cortos si cambian el nombre del método y los nombres de campo (especialmente, los nombres anulados en las clases secundarias) porque en este caso Script # no se preocupará por los nombres repetitivos. Sin embargo, no permitirá clases duplicadas.


Por cierto, la funcionalidad de minificación para métodos y campos privados e internos ya se agregó a la versión desarrollada del Script #.


Minificación Automatizada

Aunque hay muchas herramientas para la minificación de JavaScript, utilicé el Compilador de cierre de Google por su marca y su buena calidad de compresión. La desventaja de la herramienta de minificación de Google es que no puede comprimir archivos CSS; Por el contrario, YUI cumple con este desafío con éxito. De hecho, Script # también puede minimizar los scripts pero maneja este desafío mucho peor que Google Closure.


La herramienta de minificación de Google tiene varios niveles de compresión: espacios en blanco, simples y avanzados. Elegimos el nivel simple para el proyecto; aunque, el nivel Avanzado nos permite lograr la máxima calidad de compresión, requiere un código escrito de tal manera que los métodos sean accesibles desde fuera de la clase. Esta minificación se realizó parcialmente de forma manual utilizando Script #.


Modos de depuración y liberación


Las bibliotecas de depuración y lanzamiento se agregaron a las páginas ASP.NET de la siguiente manera:


 <% 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> <% } %> 

En este proyecto, minimizamos los scripts y los archivos de descripción de filtro.


Propiedad crossOrigin


Para acceder a los píxeles de alguna imagen en particular, primero debemos convertirla al lienzo. Pero esto puede conducir a un error de Seguridad de solicitud de origen cruzado (CORS). En nuestro caso, el problema se resolvió de la siguiente manera:


  • Establecer el crossOrigin = '' en el lado del servidor.
  • Agregar un encabezado espec√≠fico al paquete HTTP en el lado del servidor.

Como ScriptSharp no admite esta propiedad para elementos img, se escribió el siguiente código:


 [Imported] internal class AdvImage { [IntrinsicProperty] internal string CrossOrigin { get { return string.Empty; } set { } } } 

Luego, lo usaremos así:


 ((AdvImage)(object)result).CrossOrigin = ""; 

Esta t√©cnica le permite agregar cualquier caracter√≠stica al objeto sin errores de compilaci√≥n. En particular, la propiedad wheelDelta a√ļn no est√° implementada en ScriptSharp (al menos en la versi√≥n 0.7.5). Esta propiedad indica la cantidad de la rueda de desplazamiento, que se utiliza para crear collages. Por eso se implement√≥ de esta manera. Un truco tan sucio con las propiedades no es bueno; normalmente, necesita hacer cambios al proyecto. Pero solo para el registro, a√ļn no he descubierto una manera de compilar ScriptSharp desde la fuente.


Dichas im√°genes requieren que el servidor devuelva los siguientes encabezados en sus encabezados de respuesta (en Global.asax):


 Response.AppendHeader("Access-Control-Allow-Origin", "\*"); 

Para obtener más información sobre Cross Origin Request Security, visite http://enable-cors.org/ .


Optimizaciones


Usando los valores precalculados


Utilizamos la optimizaci√≥n para algunas operaciones, como el brillo, el contraste y el ajuste de las curvas de color mediante el c√°lculo preliminar de los componentes de color resultantes (r, g, b) para todos los valores posibles y el uso posterior de las matrices obtenidas para cambiar los colores de p√≠xeles directamente . Cabe se√Īalar que este tipo de optimizaci√≥n es adecuada solo para operaciones en las que el color del p√≠xel resultante no se ve afectado por el p√≠xel adyacente.


El c√°lculo de los componentes de color resultantes para todos los valores posibles:


 for (int i = 0; i < 256; i++) { r[i] = ActionFuncR(i); g[i] = ActionFuncG(i); b[i] = ActionFuncB(i); } 

El uso de componentes de color precalculados:


 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 tales operaciones de tabla van una por una, entonces no hay necesidad de calcular imágenes intermedias: puede pasar solo las matrices de componentes de color. Como el código funcionó bastante rápido tanto en el lado del cliente como del servidor, se decidió dejar a un lado la implementación de esta optimización. Además, la optimización causó algunos comportamientos no deseados. Sin embargo, le daré una lista de la optimización:


Código originalCódigo optimizado
`` `cs
// C√°lculo de valores para la primera tabla.
para (int i = 0; i <256; i ++)
{
r [i] = ActionFunc1R (i);
g [i] = ActionFunc1G (i);
b [i] = ActionFunc1B (i);
}
// ...

// C√°lculo de la imagen intermedia resultante.
para (int i = 0; i <data.Length; i + = 4)
{
datos [i] = r [datos [i]];
datos [i + 1] = g [datos [i + 1]];
datos [i + 2] = b [datos [i + 2]];
}
// ...

// C√°lculo de valores para la segunda tabla.
para (int i = 0; i <256; i ++)
{
r [i] = ActionFunc2R (i);
g [i] = ActionFunc2G (i);
b [i] = ActionFunc2B (i);
}
// ...

// C√°lculo de la imagen resultante.
para (int i = 0; i <data.Length; i + = 4)
{
datos [i] = r [datos [i]];
datos [i + 1] = g [datos [i + 1]];
datos [i + 2] = b [datos [i + 2]];
}
`` ``

`` `cs
// C√°lculo de valores para la primera tabla.
para (int i = 0; i <256; i ++)
{
r [i] = ActionFunc1R (i);
g [i] = ActionFunc1G (i);
b [i] = ActionFunc1B (i);
}
// ...

// C√°lculo de valores para la segunda tabla.
tr = r.Clone ();
tg = g.Clone ();
tb = b.Clone ();
para (int i = 0; i <256; i ++)
{
r [i] = tr [ActionFunc2R (i)];
g [i] = tg [ActionFunc2G (i)];
b [i] = tb [ActionFunc2B (i)];
}
// ...

// C√°lculo de la imagen resultante.
para (int i = 0; i <data.Length; i + = 4)
{
datos [i] = r [datos [i]];
datos [i + 1] = g [datos [i + 1]];
datos [i + 2] = b [datos [i + 2]];
}
`` ``


Pero incluso esto no es todo. Si observa la tabla de la derecha, notar√° que se crean nuevas matrices utilizando el m√©todo Clone . En realidad, puede cambiar los punteros a las matrices antiguas y nuevas en lugar de copiar la matriz en s√≠ (esto recuerda la analog√≠a del doble almacenamiento en b√ļfer ).


Convertir una imagen en una matriz de píxeles


El generador de perfiles de JavaScript en Google Chrome reveló que la función GetImageData (que se utiliza para convertir el lienzo en la matriz de píxeles) se ejecuta el tiempo suficiente. Esta información, por cierto, se puede encontrar en varios artículos sobre la optimización de Canvas en JavaScript.


Sin embargo, el n√ļmero de llamadas de esta funci√≥n se puede minimizar. Es decir, podemos usar la misma matriz de p√≠xeles para las operaciones de p√≠xel por p√≠xel, por analog√≠a con la optimizaci√≥n anterior.


Ejemplos de código


En los ejemplos a continuaci√≥n, proporcionar√© los fragmentos de c√≥digo que encontr√© interesantes y √ļtiles. Para evitar que el art√≠culo sea demasiado largo, he escondido los ejemplos debajo de un spoiler.


General


Detectar si una cadena es un n√ļmero


 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 } 

División entera


 internal static int Div(int n, int k) { int result = n / k; #if SCRIPTSHARP result = Math.Floor(n / k); #endif return result; } 

Girar y voltear una imagen usando Canvas y Bitmap


Tenga en cuenta que en las imágenes de lienzo html5 se pueden girar 90 y 180 grados solo con matrices, mientras que .NET proporciona una funcionalidad mejorada. Por lo tanto, se escribió una función precisa apropiada para trabajar con píxeles.


Tambi√©n vale la pena se√Īalar que una rotaci√≥n de 90 grados en cualquier lado en la versi√≥n .NET puede devolver resultados incorrectos. Por lo tanto, debe crear un nuevo Bitmap despu√©s de usar la funci√≥n RotateFlip .


Código fuente
 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 } 

Carga de imagen síncrona y asíncrona


Tenga en cuenta que en la versión Script # especificamos una función diferente CollageImageLoad , que se llama después de cargar una imagen, mientras que en la versión .NET estos procesos tienen lugar simultáneamente (desde un sistema de archivos o Internet).


Código fuente
 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; } 

Solo script #


Detectar el tipo y la versión de un navegador


Esta función se utiliza para determinar las capacidades de arrastrar y soltar en diferentes navegadores. Intenté usar modernizr , pero me devolvió ese Safari y (en mi caso, era una versión Win) IE9 lo implementó. En la práctica, estos navegadores no implementan las capacidades de arrastrar y soltar correctamente.


Código fuente
 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; } } 

Renderizado de una línea de trazos


Este código se usa para un rectángulo para recortar imágenes. Gracias por las ideas a todos los que respondieron a esta pregunta en stackoverflow .


Código fuente
 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++; } } 

Animación de rotación


setInterval función setInterval se usa para implementar la animación de rotación de imagen. Tenga en cuenta que la imagen resultante se calcula durante la animación para que no haya retrasos al final de la animación.


Código fuente
 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)); } 

Conclusión


Este artículo describe cómo el lenguaje C # (que combina código no administrado y compilación para JavaScript) puede usarse para crear una solución realmente multiplataforma. A pesar del enfoque en .NET y JavaScript, la compilación para Android, iOS (mediante Mono) y Windows Phone también es posible en base a este enfoque, que, por supuesto, tiene sus dificultades. El código es un poco redundante debido a su universalidad, pero no afecta el rendimiento ya que las operaciones gráficas generalmente toman mucho más tiempo.

Source: https://habr.com/ru/post/440342/


All Articles