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