Em 2013, enquanto trabalhava no serviço de fotos da GFRANQ, participei do desenvolvimento de um serviço da web de mesmo nome para publicação e processamento de fotos. Filtros e transformações foram definidos no arquivo com parâmetros e todo o processamento foi realizado no servidor. Durante o desenvolvimento do serviço, havia a necessidade de suportar essas transformações no lado do cliente para a visualização. Segundo Larry Wall, uma das virtudes de um programador é a preguiça. Portanto, como programadores verdadeiramente preguiçosos, pensamos na possibilidade de usar o mesmo código no servidor e no cliente. Todo o desenvolvimento foi realizado em C #. Depois de pesquisar as bibliotecas e algumas tentativas, concluímos com orgulho que isso era possível e começamos a escrever o código universal.

Por que este artigo é necessário? De fato, seis anos se passaram desde 2013 e muitas tecnologias perderam sua relevância, por exemplo, Script # . Por outro lado, novos surgiram. Por exemplo, Bridge.NET ou Blazor com base no sofisticado WebAssembly .
No entanto, algumas idéias ainda podem ser usadas. Neste artigo, tentei descrevê-los o mais detalhadamente possível. Espero que a menção de Silverlight e Flash cause um sorriso com uma pitada de nostalgia, e não um desejo de criticar as soluções antigas. De qualquer forma, eles contribuíram para o desenvolvimento da indústria da web.
Conteúdo
Objetivo
O desafio é implementar a colagem de fotos e a funcionalidade de edição de fotos com base em filtro no lado do cliente e, se possível, no lado do servidor. Para começar, abordarei como os filtros e colagens são implementados.
Descrição dos filtros
No contexto do nosso projeto, um filtro é uma série de ações realizadas no Photoshop e aplicadas a uma foto específica. Abaixo estão os exemplos de tais ações:
- Ajuste de brilho
- Ajuste de contraste
- Ajuste de saturação
- Ajuste de curvas de cores
- Mascaramento em diferentes modos
- Enquadramento
- ...
Precisamos de um certo formato para descrever essas ações. Claro, existem formatos comuns como JSON e XML, mas foi decidido criar nosso próprio formato pelos seguintes motivos:
- Necessidade de uma arquitetura de código independente da plataforma (.NET, JavaScript, WinPhone etc.)
- Necessidade de um formato não hierárquico simples de filtros, o que facilita a gravação de um analisador
- Dados XML e JSON consomem mais memória (nesse caso em particular)
Aqui está a aparência da sequência de ações para o filtro XPro Film :
Além de editar uma foto com um filtro, precisávamos cortar e girar a imagem. Sim, eu sabia que existem plugins jQuery para cortar e rotacionar imagens, mas eles pareciam estar sobrecarregados e divergir da arquitetura universal do projeto.
Descrição das colagens
Uma colagem é um arranjo de várias fotos em miniatura em uma foto inteira (com ou sem o uso de máscara). Também era necessário permitir aos usuários arrastar e soltar as imagens disponíveis na colagem, alterar sua posição e escala. Sua colagem pode ficar assim:
O recurso de colagem implica o uso de um formato simples para armazenar retângulos com coordenadas relativas de 0
a 1
, os endereços das fotos e os dados de modificação da imagem. As coordenadas relativas são usadas porque as mesmas transformações do lado do cliente são aplicadas a imagens de tamanho grande no lado do servidor.
Implementação
Tivemos que escolher a plataforma que permite aos usuários trabalhar com filtros e colagens
Existem várias tecnologias Rich Internet Application ( RIA ), como:
- Adobe flash
- Microsoft silverlight
- HTML 5 + JavaScript
- Cliente nativo
Por razões óbvias, Flash e HTML são as únicas tecnologias que merecem atenção, pois o restante delas não é compatível com várias plataformas. Além disso, o cliente Silverlight está começando a morrer. Embora eu realmente goste do conceito de sal NaCl, infelizmente, essa tecnologia é suportada apenas pelo navegador Chrome e ainda não se sabe quando será suportada (e será que será sempre suportada) por outros navegadores populares. Nota de 2019: será e o nome é WebAssembly .
A escolha foi feita em favor da plataforma HTML5 moderna e progressiva, cuja funcionalidade é atualmente suportada pelo iOS, em oposição ao Flash. Essa opção também se baseia no fato de existirem muitas bibliotecas, que permitem compilar o código C # em Javascript. Você também pode usar o Visual Studio para esse fim. Os detalhes são fornecidos abaixo.
Traduzindo C # para Javascript
O HTML 5 + JavaScript foi selecionado como plataforma na seção anterior. Portanto, deixa-nos uma questão de saber se é possível escrever um código C # universal que possa ser compilado em .NET e JavaScript.
Assim, foram encontradas várias bibliotecas para realizar a tarefa:
- Jsil
- Sharpkit
- Script #
- E alguns outros disponíveis no GitHub .
Como resultado, foi decidido usar o Script # devido ao fato de o JSIL trabalhar diretamente com assemblies e gerar código menos puro (embora ele suporte uma ampla variedade de recursos da linguagem C #) e o SharpKit é um produto comercial. Para uma comparação detalhada dessas ferramentas, consulte a pergunta sobre stackoverflow .
Em resumo, o ScriptSharp comparado ao JavaScript escrito manualmente tem os seguintes prós e contras:
Vantagens
- Possibilidade de escrever um código C # universal que possa ser compilado no .NET e em outras plataformas (WinPhone, Mono)
- Desenvolvimento em uma linguagem C # fortemente tipada que suporta OOP
- Suporte para recursos IDE (preenchimento automático e refatoração)
- Capacidade de detectar a maioria dos erros no estágio de compilação
Desvantagens
- Redundância e irregularidade do código JavaScript gerado (devido ao mscorlib).
- Suporte apenas para ISO-2 (sem sobrecarga de função ou inferência de tipo, extensão e genéricos)
Estrutura
O processo de compilação do mesmo código C # no .NET e Javascript pode ser ilustrado pelo seguinte esquema:
Embora .NET e HTML5 sejam tecnologias completamente diferentes, eles também têm recursos semelhantes. Isso também se aplica ao trabalho com gráficos. Por exemplo, o .NET suporta Bitmap , o JavaScript suporta o seu análogo - Canvas . O mesmo acontece com gráficos , contexto e matrizes de pixels. Para combinar tudo isso em um código, decidiu-se desenvolver a seguinte arquitetura:
Obviamente, não se limita a duas plataformas. Como acompanhamento, está planejado adicionar suporte ao WinPhone e, talvez, Android e iOS.
Note-se que existem dois tipos de operações gráficas:
- Usando funções da API (
DrawImage
, Arc
, MoveTo
, LineTo
). Alto desempenho e suporte à aceleração de hardware são vantagens competitivas importantes. A desvantagem é que eles podem ser implementados de maneira diferente em plataformas diferentes. - Pixel por pixel. Suporte para a implementação de quaisquer efeitos e cobertura entre plataformas estão entre os benefícios. A desvantagem é o baixo desempenho. No entanto, você pode atenuar as desvantagens por paralelização, sombreadores e tabelas pré-calculadas (discutiremos isso mais adiante na próxima seção sobre otimização).
Como você pode ver, a classe abstrata Graphics descreve todos os métodos para trabalhar com gráficos; esses métodos são implementados para várias plataformas na classe derivada. Os aliases a seguir foram escritos para abstrair também das classes Bitmap e Canvas. A versão WinPhone também usa um padrão de 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
Infelizmente, é impossível criar aliases para tipos e matrizes não seguros, ou seja, Alias para ponteiro (byte *) em C # :
using PixelArray = byte*, using PixelArray = byte[]
Para executar o processamento rápido de pixels usando código C # não gerenciado, ao mesmo tempo em que o compila no Script #, introduzimos o seguinte esquema com a ajuda de diretivas:
#if SCRIPTSHARP PixelArray data = context.GetPixelArray(); #elif DOTNET byte* data = context.GetPixelArray(); #endif
A matriz de data
é subseqüentemente usada para implementar várias operações de pixel por pixel (como mascaramento, olho de peixe, ajuste de saturação etc.), paralelamente e não.
Links para arquivos
Um projeto separado é adicionado à solução para cada plataforma, mas, é claro, Mono, Script # e até o Silverlight não podem se referir aos assemblies .NET habituais. Felizmente, o Visual Studio possui um mecanismo para adicionar links a arquivos, o que permite reutilizar o mesmo código em projetos diferentes.
As diretivas do compilador ( DOTNET
, SCRIPTSHARP
) são definidas nas propriedades do projeto em Símbolos de compilação condicional.
Notas sobre a implementação do .NET
As abstrações e aliases acima nos ajudaram a escrever o código C # com baixa redundância. Além disso, quero apontar os problemas com as plataformas .NET e JavaScript que enfrentamos ao desenvolver o código da solução.
Usando descarte
Observe que a inclusão de qualquer instância de uma classe C #, que implementa a interface IDisposable
, requer a chamada do método Dispose
ou a aplicação da instrução Using . Neste projeto, essas classes são Bitmap e Contexto. O que eu disse acima não é apenas a teoria, ele realmente tem uma aplicação prática: o processamento de um grande número de fotos de tamanho grande (até 2400 x 2400 dpi) no ASP.NET Developer Server x86 resultou em uma exceção de falta de memória. O problema foi resolvido depois de adicionar Dispose
nos lugares certos. Alguns outros conselhos úteis sobre manipulação de imagens são fornecidos no artigo a seguir: 20 Redimensionamento de armadilhas e vazamento de memória .NET: Para descartar ou não descartar, essa é a questão de 1 GB .
Usando bloqueio
Em JavaScript, há uma diferença entre a imagem já carregada com a tag img
, para a qual você pode especificar o evento de origem e de carregamento, e a tela com tags de canvas
, na qual é possível desenhar algo. No .NET esses elementos são representados pela mesma classe Bitmap
. Assim, os aliases Bitmap e Imagem no .NET apontam para a mesma classe System.Drawing.Bitmap
conforme mostrado acima.
No entanto, essa divisão em img
e canvas
em JavaScript também foi muito útil na versão .NET. O ponto é que os filtros usam máscaras pré-carregadas de diferentes threads; portanto, o padrão de bloqueio é necessário para evitar a exceção durante a sincronização (a imagem é copiada com bloqueio e o resultado é usado sem bloqueio):
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 }
Afinal, o bloqueio também deve ser usado ao acessar as propriedades de um objeto sincronizado (de fato, quaisquer propriedades são métodos).
Armazenando máscaras na memória
Para acelerar o processamento, todas as máscaras potencialmente usadas para filtros são carregadas na memória quando o servidor é iniciado. Independentemente do formato da máscara, o Bitmap carregado no servidor usa 4 * 2400 * 2400
ou ≈24 MB
de memória (o tamanho máximo da imagem é 2400 * 2400
; o número de bytes por pixel é 4). Todas as máscaras para filtros (≈30) e colagens (40) consumirão 1,5 GB - isso não é muito para o servidor; no entanto, conforme o número de máscaras aumenta, esse valor pode aumentar significativamente. No futuro, possivelmente usaremos técnicas de compactação para máscaras armazenadas na memória (nos formatos .jpg e .png) seguidas de descompactação quando necessário. Na verdade, o tamanho pode ser reduzido em até 300 vezes. Uma vantagem adicional dessa abordagem é que a cópia das imagens compactadas é mais rápida em comparação às grandes; portanto, a operação de bloqueio levará menos tempo e os threads serão bloqueados com menos frequência.
Notas sobre implementação de JavaScript
Minificação
Recusei-me a usar o termo "ofuscação" pelo seguinte motivo: este termo é pouco aplicável a uma linguagem de código-fonte totalmente aberto, que no nosso caso é JavaScript. No entanto, o anonimato dos identificadores pode atrapalhar a legibilidade e a lógica do código. E o mais importante, essa técnica reduzirá significativamente o tamanho do script (a versão compactada é de ≈80 KB).
Existem duas abordagens para a minificação do JavaScript:
- Minificação manual, que é executada no estágio de geração usando o ScriptSharp.
- Minificação automatizada, realizada após o estágio de geração usando ferramentas externas, como o Google Closure Compiler, Yui e outras ferramentas.
Minificação manual
Para encurtar os nomes dos métodos, classes e atributos, usamos essa sintaxe antes da declaração das entidades mencionadas acima. Obviamente, não é necessário fazer isso se você estiver trabalhando com métodos chamados de scripts e classes externos (público).
#if SCRIPTSHARP && !DEBUG [ScriptName("a0")] #endif
De qualquer forma, as variáveis locais não puderam ser minificadas. Essas construções poluem o código e prejudicam a legibilidade do código, o que também é uma séria desvantagem. No entanto, essa técnica pode reduzir significativamente a quantidade de código JavaScript gerado e atrapalhar também.
Outra desvantagem é que você precisa ficar de olho nesses nomes abreviados se eles renomearem os nomes de métodos e de campos (especialmente nomes substituídos nas classes filho), porque nesse caso o Script # não se importará com nomes repetitivos. No entanto, não permitirá classes duplicadas.
A propósito, a funcionalidade de minificação para métodos e campos privados e internos já foi adicionada à versão desenvolvida do Script #.
Minificação automatizada
Embora existam muitas ferramentas para minificação de JavaScript, usei o Google Closure Compiler por sua marca e boa qualidade de compactação. A desvantagem da ferramenta de minificação do Google é que ela não pode compactar arquivos CSS; por outro lado, a YUI enfrenta esse desafio com sucesso. De fato, o Script # também pode reduzir scripts, mas lida com esse desafio muito pior que o Google Closure.
A ferramenta de minificação do Google possui vários níveis de compactação: espaço em branco, simples e avançado. Escolhemos o nível Simples para o projeto; embora, o nível Avançado permita alcançar a qualidade máxima de compactação, requer código escrito dessa maneira para que os métodos sejam acessíveis de fora da classe. Essa minificação foi parcialmente realizada manualmente usando o Script #.
Modos de depuração e lançamento
As bibliotecas de depuração e versão foram adicionadas às páginas do ASP.NET da seguinte maneira:
<% 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> <% } %>
Neste projeto, reduzimos os scripts e os arquivos de descrição do filtro.
Propriedade crossOrigin
Para acessar os pixels de uma imagem em particular, precisamos primeiro convertê-la em tela. Mas isso pode levar a um erro CORS (Cross Origin Request Security). No nosso caso, o problema foi resolvido da seguinte maneira:
- Configurando o
crossOrigin = ''
no lado do servidor. - Adicionando um cabeçalho específico ao pacote HTTP no lado do servidor.
Como o ScriptSharp não suporta essa propriedade para elementos img, o seguinte código foi gravado:
[Imported] internal class AdvImage { [IntrinsicProperty] internal string CrossOrigin { get { return string.Empty; } set { } } }
Então, vamos usá-lo assim:
((AdvImage)(object)result).CrossOrigin = "";
Essa técnica permite adicionar qualquer recurso ao objeto sem erros de compilação. Particularmente, a propriedade wheelDelta
ainda não foi implementada no ScriptSharp (pelo menos na versão 0.7.5). Esta propriedade indica a quantidade da roda de rolagem, usada para criar colagens. É por isso que foi implementado dessa maneira. Um hack tão sujo com as propriedades não é bom; normalmente, você precisa fazer alterações no projeto. Mas, apenas para constar, ainda não descobri uma maneira de compilar o ScriptSharp a partir da fonte.
Essas imagens requerem que o servidor retorne os seguintes cabeçalhos em seus cabeçalhos de resposta (em Global.asax):
Response.AppendHeader("Access-Control-Allow-Origin", "\*");
Para obter mais informações sobre a segurança de solicitação de origem cruzada, visite http://enable-cors.org/ .
Otimizações
Usando os valores pré-calculados
Utilizamos a otimização para algumas operações, como ajuste de brilho, contraste e curvas de cores, através do cálculo preliminar dos componentes de cores resultantes (r, g, b) para todos os valores possíveis e uso adicional das matrizes obtidas para alterar diretamente as cores dos pixels. . Note-se que esse tipo de otimização é adequado apenas para operações nas quais a cor do pixel resultante não é afetada pelo pixel adjacente.
O cálculo dos componentes de cores resultantes para todos os valores possíveis:
for (int i = 0; i < 256; i++) { r[i] = ActionFuncR(i); g[i] = ActionFuncG(i); b[i] = ActionFuncB(i); }
O uso de componentes de cores pré-calculados:
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]]; }
Se essas operações da tabela forem uma por uma, não será necessário calcular imagens intermediárias - você pode passar apenas as matrizes do componente de cores. Como o código funcionava rapidamente no lado do cliente e do servidor, foi decidido deixar de lado a implementação dessa otimização. Além disso, a otimização causou algum comportamento indesejado. No entanto, darei uma lista da otimização:
Código original | Código otimizado |
`` cs // Cálculo de valores para a primeira tabela. for (int i = 0; i <256; i ++) { r [i] = ActionFunc1R (i); g [i] = ActionFunc1G (i); b [i] = ActionFunc1B (i); } // ...
// Cálculo da imagem intermediária resultante. for (int i = 0; i <data.Length; i + = 4) { dados [i] = r [dados [i]]; dados [i + 1] = g [dados [i + 1]]; dados [i + 2] = b [dados [i + 2]]; } // ...
// Cálculo de valores para a segunda tabela. for (int i = 0; i <256; i ++) { r [i] = ActionFunc2R (i); g [i] = ActionFunc2G (i); b [i] = ActionFunc2B (i); } // ...
// Cálculo da imagem resultante. for (int i = 0; i <data.Length; i + = 4) { dados [i] = r [dados [i]]; dados [i + 1] = g [dados [i + 1]]; dados [i + 2] = b [dados [i + 2]]; } `` ``
| `` cs // Cálculo de valores para a primeira tabela. for (int i = 0; i <256; i ++) { r [i] = ActionFunc1R (i); g [i] = ActionFunc1G (i); b [i] = ActionFunc1B (i); } // ...
// Cálculo de valores para a segunda tabela. tr = r.Clone (); tg = g.Clone (); clone (); for (int i = 0; i <256; i ++) { r [i] = tr [ActionFunc2R (i)]; g [i] = tg [ActionFunc2G (i)]; b [i] = t [ActionFunc2B (i)]; } // ...
// Cálculo da imagem resultante. for (int i = 0; i <data.Length; i + = 4) { dados [i] = r [dados [i]]; dados [i + 1] = g [dados [i + 1]]; dados [i + 2] = b [dados [i + 2]]; } `` ``
|
Mas mesmo isso não é tudo. Se você der uma olhada na tabela à direita, notará que novas matrizes são criadas usando o método Clone
. Na verdade, você pode simplesmente mudar os ponteiros para as novas e antigas matrizes, em vez de copiar a própria matriz (isso lembra a analogia do buffer duplo ).
Convertendo uma imagem em uma matriz de pixels
O criador de perfil JavaScript no Google Chrome revelou que a função GetImageData
(usada para converter a tela em uma matriz de pixels) é executada por tempo suficiente. A propósito, essas informações podem ser encontradas em vários artigos sobre otimização do Canvas em JavaScript.
No entanto, o número de chamadas dessa função pode ser minimizado. Ou seja, podemos usar a mesma matriz de pixels para operações pixel por pixel, por analogia com a otimização anterior.
Exemplos de código
Nos exemplos abaixo, fornecerei os fragmentos de código que achei interessantes e úteis. Para evitar que o artigo seja muito longo, ocultei os exemplos em um spoiler.
Geral
Detectando se uma string é um 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 }
Divisão inteira
internal static int Div(int n, int k) { int result = n / k; #if SCRIPTSHARP result = Math.Floor(n / k); #endif return result; }
Girando e invertendo uma imagem usando a tela e o bitmap
Observe que, no html5, as imagens de tela podem ser giradas 90 e 180 graus apenas usando matrizes, enquanto o .NET fornece funcionalidade aprimorada. Assim, uma função precisa e precisa para trabalhar com pixels foi escrita.
Também é importante notar que uma rotação de 90 graus de qualquer lado na versão .NET pode retornar resultados incorretos. Portanto, você precisa criar um novo Bitmap
depois de usar a função RotateFlip
.
Código fonte 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 }
Carregamento de imagem síncrona e assíncrona
Observe que, na versão Script #, especificamos uma função diferente CollageImageLoad
, que é chamada após o carregamento de uma imagem, enquanto na versão .NET esses processos ocorrem simultaneamente (de um sistema de arquivos ou da Internet).
Código fonte 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; }
Somente script #
Detectando o tipo e a versão de um navegador
Esta função é usada para determinar os recursos de arrastar e soltar em diferentes navegadores. Eu tentei usar o modernizr , mas ele retornou que o Safari e (no meu caso, era uma versão do Win) o IE9 o implementava. Na prática, esses navegadores falham ao implementar os recursos de arrastar e soltar corretamente.
Código fonte 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; } }
Renderizando uma linha tracejada
Este código é usado para um retângulo para cortar imagens. Obrigado pelas idéias a todos que responderam a esta pergunta no stackoverflow .
Código fonte 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++; } }
Animação de rotação
setInterval
função setInterval
é usada para implementar a animação de rotação de imagem. Observe que a imagem resultante é calculada durante a animação, para que não haja atrasos no final da animação.
Código fonte 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)); }
Conclusão
Este artigo descreve como a linguagem C # (combinando código não gerenciado e compilação para JavaScript) pode ser usada para criar uma solução realmente multiplataforma. Apesar do foco no .NET e JavaScript, a compilação para Android, iOS (usando Mono) e Windows Phone também é possível com base nessa abordagem, que, é claro, tem suas armadilhas. O código é um pouco redundante devido à sua universalidade, mas não afeta o desempenho, pois as operações gráficas geralmente levam muito mais tempo.