Salvando bitmaps do SkiaSharp em arquivos
Depois que um aplicativo SkiaSharp tiver criado ou modificado um bitmap, o aplicativo pode querer salvar o bitmap na biblioteca de fotos do usuário:
Essa tarefa abrange duas etapas:
- Convertendo o bitmap SkiaSharp em dados em um formato de arquivo específico, como JPEG ou PNG.
- Salvar o resultado na biblioteca de fotos usando código específico da plataforma.
Formatos de arquivo e codecs
A maioria dos formatos de arquivo bitmap populares de hoje usa compactação para reduzir o espaço de armazenamento. As duas grandes categorias de técnicas de compressão são chamadas de lossy e lossless. Esses termos indicam se o algoritmo de compactação resulta ou não na perda de dados.
O formato com perdas mais popular foi desenvolvido pelo Joint Photographic Experts Group e é chamado de JPEG. O algoritmo de compactação JPEG analisa a imagem usando uma ferramenta matemática chamada transformação discreta de cosseno e tenta remover dados que não são cruciais para preservar a fidelidade visual da imagem. O grau de compressão pode ser controlado com uma configuração geralmente chamada de qualidade. Configurações de qualidade mais alta resultam em arquivos maiores.
Em contraste, um algoritmo de compactação sem perdas analisa a imagem quanto à repetição e padrões de pixels que podem ser codificados de uma forma que reduz os dados, mas não resulta na perda de nenhuma informação. Os dados de bitmap originais podem ser restaurados inteiramente a partir do arquivo compactado. O principal formato de arquivo compactado sem perdas em uso atualmente é o PNG (Portable Network Graphics).
Geralmente, o JPEG é usado para fotografias, enquanto o PNG é usado para imagens que foram geradas manualmente ou por algoritmos. Qualquer algoritmo de compactação sem perdas que reduza o tamanho de alguns arquivos deve necessariamente aumentar o tamanho de outros. Felizmente, esse aumento de tamanho geralmente ocorre apenas para dados que contêm muitas informações aleatórias (ou aparentemente aleatórias).
Os algoritmos de compactação são complexos o suficiente para justificar dois termos que descrevem os processos de compactação e descompactação:
- decodificar — ler um formato de arquivo bitmap e descompactá-lo
- codificar — compactar o bitmap e gravar em um formato de arquivo bitmap
A SKBitmap
classe contém vários métodos nomeados Decode
que criam um SKBitmap
a partir de uma fonte compactada. Tudo o que é necessário é fornecer um nome de arquivo, fluxo ou matriz de bytes. O decodificador pode determinar o formato do arquivo e entregá-lo à função de decodificação interna adequada.
Além disso, a SKCodec
classe tem dois métodos nomeados Create
que podem criar um SKCodec
objeto a partir de uma fonte compactada e permitir que um aplicativo se envolva mais no processo de decodificação. (A SKCodec
classe é mostrada no artigo Animando Bitmaps SkiaSharp em conexão com a decodificação de um arquivo GIF animado.)
Ao codificar um bitmap, mais informações são necessárias: O codificador deve saber o formato de arquivo específico que o aplicativo deseja usar (JPEG ou PNG ou qualquer outra coisa). Se um formato com perdas for desejado, a codificação também deverá saber o nível de qualidade desejado.
A SKBitmap
classe define um Encode
método com a seguinte sintaxe:
public Boolean Encode (SKWStream dst, SKEncodedImageFormat format, Int32 quality)
Este método é descrito com mais detalhes em breve. O bitmap codificado é gravado em um fluxo gravável. (O 'W' significa SKWStream
"gravável".) O segundo e o terceiro argumentos especificam o formato do arquivo e (para formatos com perdas) a qualidade desejada variando de 0 a 100.
Além disso, as SKImage
classes and SKPixmap
também definem Encode
métodos que são um pouco mais versáteis e que você pode preferir. Você pode criar facilmente um SKImage
objeto a partir de um SKBitmap
objeto usando o método estático SKImage.FromBitmap
. Você pode obter um SKPixmap
objeto de um SKBitmap
objeto usando o PeekPixels
método.
Um dos Encode
métodos definidos por SKImage
não tem parâmetros e é salvo automaticamente em um formato PNG. Esse método sem parâmetros é muito fácil de usar.
Código específico da plataforma para salvar arquivos bitmap
Quando você codifica um SKBitmap
objeto em um formato de arquivo específico, geralmente você fica com um objeto de fluxo de algum tipo ou uma matriz de dados. Alguns dos Encode
métodos (incluindo aquele sem parâmetros definidos por SKImage
) retornam um SKData
objeto, que pode ser convertido em uma matriz de bytes usando o ToArray
método. Esses dados devem ser salvos em um arquivo.
Salvar em um arquivo no armazenamento local do aplicativo é bastante fácil porque você pode usar classes e métodos padrão System.IO
para essa tarefa. Essa técnica é demonstrada no artigo Animando Bitmaps SkiaSharp em conexão com a animação de uma série de bitmaps do conjunto de Mandelbrot.
Se você quiser que o arquivo seja compartilhado por outros aplicativos, ele deve ser salvo na biblioteca de fotos do usuário. Essa tarefa requer código específico da plataforma e o uso do Xamarin.FormsDependencyService
.
O projeto SkiaSharpFormsDemo no aplicativo de exemplo define uma IPhotoLibrary
interface usada com a DependencyService
classe. Isso define a sintaxe de um SavePhotoAsync
método:
public interface IPhotoLibrary
{
Task<Stream> PickPhotoAsync();
Task<bool> SavePhotoAsync(byte[] data, string folder, string filename);
}
Essa interface também define o PickPhotoAsync
método, que é usado para abrir o seletor de arquivos específico da plataforma para a biblioteca de fotos do dispositivo.
Para SavePhotoAsync
, o primeiro argumento é uma matriz de bytes que contém o bitmap já codificado em um formato de arquivo específico, como JPEG ou PNG. É possível que um aplicativo queira isolar todos os bitmaps criados em uma pasta específica, que é especificada no próximo parâmetro, seguido pelo nome do arquivo. O método retorna um booleano indicando sucesso ou não.
As seções a seguir discutem como SavePhotoAsync
é implementado em cada plataforma.
A implementação do iOS
A implementação do iOS usa SavePhotoAsync
o SaveToPhotosAlbum
método de UIImage
:
public class PhotoLibrary : IPhotoLibrary
{
···
public Task<bool> SavePhotoAsync(byte[] data, string folder, string filename)
{
NSData nsData = NSData.FromArray(data);
UIImage image = new UIImage(nsData);
TaskCompletionSource<bool> taskCompletionSource = new TaskCompletionSource<bool>();
image.SaveToPhotosAlbum((UIImage img, NSError error) =>
{
taskCompletionSource.SetResult(error == null);
});
return taskCompletionSource.Task;
}
}
Infelizmente, não há como especificar um nome de arquivo ou pasta para a imagem.
O arquivo Info.plist no projeto iOS requer uma chave indicando que ele adiciona imagens à biblioteca de fotos:
<key>NSPhotoLibraryAddUsageDescription</key>
<string>SkiaSharp Forms Demos adds images to your photo library</string>
Tomar cuidado! A chave de permissão para simplesmente acessar a biblioteca de fotos é muito semelhante, mas não a mesma:
<key>NSPhotoLibraryUsageDescription</key>
<string>SkiaSharp Forms Demos accesses your photo library</string>
A implementação do Android
A implementação do Android de first verifica se o folder
argumento é null
ou uma cadeia de SavePhotoAsync
caracteres vazia. Nesse caso, o bitmap é salvo no diretório raiz da biblioteca de fotos. Caso contrário, a pasta é obtida e, se não existir, é criada:
public class PhotoLibrary : IPhotoLibrary
{
···
public async Task<bool> SavePhotoAsync(byte[] data, string folder, string filename)
{
try
{
File picturesDirectory = Environment.GetExternalStoragePublicDirectory(Environment.DirectoryPictures);
File folderDirectory = picturesDirectory;
if (!string.IsNullOrEmpty(folder))
{
folderDirectory = new File(picturesDirectory, folder);
folderDirectory.Mkdirs();
}
using (File bitmapFile = new File(folderDirectory, filename))
{
bitmapFile.CreateNewFile();
using (FileOutputStream outputStream = new FileOutputStream(bitmapFile))
{
await outputStream.WriteAsync(data);
}
// Make sure it shows up in the Photos gallery promptly.
MediaScannerConnection.ScanFile(MainActivity.Instance,
new string[] { bitmapFile.Path },
new string[] { "image/png", "image/jpeg" }, null);
}
}
catch
{
return false;
}
return true;
}
}
A chamada para MediaScannerConnection.ScanFile
não é estritamente necessária, mas se você estiver testando seu programa verificando imediatamente a biblioteca de fotos, ela ajuda muito atualizando a visualização da galeria da biblioteca.
O arquivo AndroidManifest.xml requer a seguinte tag de permissão:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
A implementação da UWP
A implementação UWP de é muito semelhante em estrutura à implementação do SavePhotoAsync
Android:
public class PhotoLibrary : IPhotoLibrary
{
···
public async Task<bool> SavePhotoAsync(byte[] data, string folder, string filename)
{
StorageFolder picturesDirectory = KnownFolders.PicturesLibrary;
StorageFolder folderDirectory = picturesDirectory;
// Get the folder or create it if necessary
if (!string.IsNullOrEmpty(folder))
{
try
{
folderDirectory = await picturesDirectory.GetFolderAsync(folder);
}
catch
{ }
if (folderDirectory == null)
{
try
{
folderDirectory = await picturesDirectory.CreateFolderAsync(folder);
}
catch
{
return false;
}
}
}
try
{
// Create the file.
StorageFile storageFile = await folderDirectory.CreateFileAsync(filename,
CreationCollisionOption.GenerateUniqueName);
// Convert byte[] to Windows buffer and write it out.
IBuffer buffer = WindowsRuntimeBuffer.Create(data, 0, data.Length, data.Length);
await FileIO.WriteBufferAsync(storageFile, buffer);
}
catch
{
return false;
}
return true;
}
}
A seção Recursos do arquivo Package.appxmanifest requer a Biblioteca de Imagens.
Explorando os formatos de imagem
Aqui está o Encode
método de SKImage
novamente:
public Boolean Encode (SKWStream dst, SKEncodedImageFormat format, Int32 quality)
SKEncodedImageFormat
é uma enumeração com membros que se referem a onze formatos de arquivo bitmap, alguns dos quais são bastante obscuros:
Astc
— Compressão de textura escalável adaptávelBmp
— Bitmap do WindowsDng
— Negativo digital da AdobeGif
— Formato de intercâmbio gráficoIco
— Imagens de ícones do WindowsJpeg
— Grupo Conjunto de Peritos FotográficosKtx
— Formato de textura Khronos para OpenGLPkm
— Formato personalizado para GrafX2Png
— Gráficos de rede portáteisWbmp
— Formato de bitmap do protocolo de aplicativo sem fio (1 bit por pixel)Webp
— Formato Google WebP
Como você verá em breve, apenas três desses formatos de arquivo (Jpeg
, Png
e Webp
) são realmente suportados pelo SkiaSharp.
Para salvar um SKBitmap
objeto nomeado bitmap
na biblioteca de fotos do usuário, você também precisa de um membro da SKEncodedImageFormat
enumeração chamado imageFormat
e (para formatos com perdas) uma variável inteira quality
. Você pode usar o seguinte código para salvar esse bitmap em um arquivo com o nome filename
na folder
pasta:
using (MemoryStream memStream = new MemoryStream())
using (SKManagedWStream wstream = new SKManagedWStream(memStream))
{
bitmap.Encode(wstream, imageFormat, quality);
byte[] data = memStream.ToArray();
// Check the data array for content!
bool success = await DependencyService.Get<IPhotoLibrary>().SavePhotoAsync(data, folder, filename);
// Check return value for success!
}
A SKManagedWStream
classe deriva de SKWStream
(que significa "fluxo gravável"). O Encode
método grava o arquivo de bitmap codificado nesse fluxo. Os comentários nesse código referem-se a alguma verificação de erro que você pode precisar executar.
A página Salvar Formatos de Arquivo no aplicativo de exemplo usa código semelhante para permitir que você experimente salvar um bitmap nos vários formatos.
O arquivo XAML contém um SKCanvasView
que exibe um bitmap, enquanto o restante da página contém tudo o que o aplicativo precisa para chamar o Encode
método de SKBitmap
. Ele tem um Picker
para um membro da enumeração, um Slider
para o argumento de qualidade para formatos de bitmap com perdas, duas Entry
exibições para um nome de arquivo e um nome de SKEncodedImageFormat
pasta e um Button
para salvar o arquivo.
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:skia="clr-namespace:SkiaSharp;assembly=SkiaSharp"
xmlns:skiaforms="clr-namespace:SkiaSharp.Views.Forms;assembly=SkiaSharp.Views.Forms"
x:Class="SkiaSharpFormsDemos.Bitmaps.SaveFileFormatsPage"
Title="Save Bitmap Formats">
<StackLayout Margin="10">
<skiaforms:SKCanvasView PaintSurface="OnCanvasViewPaintSurface"
VerticalOptions="FillAndExpand" />
<Picker x:Name="formatPicker"
Title="image format"
SelectedIndexChanged="OnFormatPickerChanged">
<Picker.ItemsSource>
<x:Array Type="{x:Type skia:SKEncodedImageFormat}">
<x:Static Member="skia:SKEncodedImageFormat.Astc" />
<x:Static Member="skia:SKEncodedImageFormat.Bmp" />
<x:Static Member="skia:SKEncodedImageFormat.Dng" />
<x:Static Member="skia:SKEncodedImageFormat.Gif" />
<x:Static Member="skia:SKEncodedImageFormat.Ico" />
<x:Static Member="skia:SKEncodedImageFormat.Jpeg" />
<x:Static Member="skia:SKEncodedImageFormat.Ktx" />
<x:Static Member="skia:SKEncodedImageFormat.Pkm" />
<x:Static Member="skia:SKEncodedImageFormat.Png" />
<x:Static Member="skia:SKEncodedImageFormat.Wbmp" />
<x:Static Member="skia:SKEncodedImageFormat.Webp" />
</x:Array>
</Picker.ItemsSource>
</Picker>
<Slider x:Name="qualitySlider"
Maximum="100"
Value="50" />
<Label Text="{Binding Source={x:Reference qualitySlider},
Path=Value,
StringFormat='Quality = {0:F0}'}"
HorizontalTextAlignment="Center" />
<StackLayout Orientation="Horizontal">
<Label Text="Folder Name: "
VerticalOptions="Center" />
<Entry x:Name="folderNameEntry"
Text="SaveFileFormats"
HorizontalOptions="FillAndExpand" />
</StackLayout>
<StackLayout Orientation="Horizontal">
<Label Text="File Name: "
VerticalOptions="Center" />
<Entry x:Name="fileNameEntry"
Text="Sample.xxx"
HorizontalOptions="FillAndExpand" />
</StackLayout>
<Button Text="Save"
Clicked="OnButtonClicked">
<Button.Triggers>
<DataTrigger TargetType="Button"
Binding="{Binding Source={x:Reference formatPicker},
Path=SelectedIndex}"
Value="-1">
<Setter Property="IsEnabled" Value="False" />
</DataTrigger>
<DataTrigger TargetType="Button"
Binding="{Binding Source={x:Reference fileNameEntry},
Path=Text.Length}"
Value="0">
<Setter Property="IsEnabled" Value="False" />
</DataTrigger>
</Button.Triggers>
</Button>
<Label x:Name="statusLabel"
Text="OK"
Margin="10, 0" />
</StackLayout>
</ContentPage>
O arquivo code-behind carrega um recurso de bitmap e usa o SKCanvasView
para exibi-lo. Esse bitmap nunca muda. O SelectedIndexChanged
manipulador do Picker
modifica o nome do arquivo com uma extensão que é igual ao membro da enumeração:
public partial class SaveFileFormatsPage : ContentPage
{
SKBitmap bitmap = BitmapExtensions.LoadBitmapResource(typeof(SaveFileFormatsPage),
"SkiaSharpFormsDemos.Media.MonkeyFace.png");
public SaveFileFormatsPage ()
{
InitializeComponent ();
}
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
args.Surface.Canvas.DrawBitmap(bitmap, args.Info.Rect, BitmapStretch.Uniform);
}
void OnFormatPickerChanged(object sender, EventArgs args)
{
if (formatPicker.SelectedIndex != -1)
{
SKEncodedImageFormat imageFormat = (SKEncodedImageFormat)formatPicker.SelectedItem;
fileNameEntry.Text = Path.ChangeExtension(fileNameEntry.Text, imageFormat.ToString());
statusLabel.Text = "OK";
}
}
async void OnButtonClicked(object sender, EventArgs args)
{
SKEncodedImageFormat imageFormat = (SKEncodedImageFormat)formatPicker.SelectedItem;
int quality = (int)qualitySlider.Value;
using (MemoryStream memStream = new MemoryStream())
using (SKManagedWStream wstream = new SKManagedWStream(memStream))
{
bitmap.Encode(wstream, imageFormat, quality);
byte[] data = memStream.ToArray();
if (data == null)
{
statusLabel.Text = "Encode returned null";
}
else if (data.Length == 0)
{
statusLabel.Text = "Encode returned empty array";
}
else
{
bool success = await DependencyService.Get<IPhotoLibrary>().
SavePhotoAsync(data, folderNameEntry.Text, fileNameEntry.Text);
if (!success)
{
statusLabel.Text = "SavePhotoAsync return false";
}
else
{
statusLabel.Text = "Success!";
}
}
}
}
}
O Clicked
manipulador do Button
faz todo o trabalho real. Ele obtém dois argumentos para Encode
from the Picker
e Slider
, e usa o código mostrado anteriormente para criar um SKManagedWStream
para o Encode
método. As duas Entry
visualizações fornecem nomes de pastas e arquivos para o SavePhotoAsync
método.
A maior parte desse método é dedicada ao tratamento de problemas ou erros. Se Encode
criar uma matriz vazia, isso significa que o formato de arquivo específico não é suportado. Se SavePhotoAsync
retornar false
, o arquivo não foi salvo com êxito.
Aqui está o programa em execução:
Essa captura de tela mostra os únicos três formatos com suporte nessas plataformas:
- JPEG
- PNG
- Redes
Para todos os outros formatos, o método não grava Encode
nada no fluxo e a matriz de bytes resultante está vazia.
O bitmap que a página Salvar Formatos de Arquivo salva tem 600 pixels quadrados. Com 4 bytes por pixel, isso é um total de 1.440.000 bytes na memória. A tabela a seguir mostra o tamanho do arquivo para várias combinações de formato e qualidade de arquivo:
Formatar | Quality | Tamanho |
---|---|---|
PNG | N/D | 492 mil |
JPEG | 0 | 2,95 mil |
50 | 22,1 mil | |
100 | 206 mil | |
Redes | 0 | 2,71 mil |
50 | 11,9 mil | |
100 | 101 mil |
Você pode experimentar várias configurações de qualidade e examinar os resultados.
Salvando a arte da pintura a dedo
Um uso comum de um bitmap é em programas de desenho, onde ele funciona como algo chamado bitmap de sombra. Todo o desenho é retido no bitmap, que é exibido pelo programa. O bitmap também é útil para salvar o desenho.
O artigo Pintura a dedo no SkiaSharp demonstrou como usar o rastreamento de toque para implementar um programa primitivo de pintura com os dedos. O programa suportava apenas uma cor e apenas uma largura de traçado, mas mantinha todo o desenho em uma coleção de SKPath
objetos.
A página Pintura a dedo com salvar no exemplo também retém o desenho inteiro em uma coleção de objetos, mas também renderiza o desenho em um bitmap, que pode ser salvo em sua biblioteca de SKPath
fotos.
Grande parte deste programa é semelhante ao programa Finger Paint original. Um aprimoramento é que o arquivo XAML agora instancia botões rotulados como Limpar e Salvar:
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:skia="clr-namespace:SkiaSharp.Views.Forms;assembly=SkiaSharp.Views.Forms"
xmlns:tt="clr-namespace:TouchTracking"
x:Class="SkiaSharpFormsDemos.Bitmaps.FingerPaintSavePage"
Title="Finger Paint Save">
<StackLayout>
<Grid BackgroundColor="White"
VerticalOptions="FillAndExpand">
<skia:SKCanvasView x:Name="canvasView"
PaintSurface="OnCanvasViewPaintSurface" />
<Grid.Effects>
<tt:TouchEffect Capture="True"
TouchAction="OnTouchEffectAction" />
</Grid.Effects>
</Grid>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
</Grid>
<Button Text="Clear"
Grid.Row="0"
Margin="50, 5"
Clicked="OnClearButtonClicked" />
<Button Text="Save"
Grid.Row="1"
Margin="50, 5"
Clicked="OnSaveButtonClicked" />
</StackLayout>
</ContentPage>
O arquivo code-behind mantém um campo do tipo SKBitmap
chamado saveBitmap
. Esse bitmap é criado ou recriado no PaintSurface
manipulador sempre que o tamanho da superfície de exibição é alterado. Se o bitmap precisar ser recriado, o conteúdo do bitmap existente será copiado para o novo bitmap para que tudo seja retido, independentemente de como a superfície de exibição muda de tamanho:
public partial class FingerPaintSavePage : ContentPage
{
···
SKBitmap saveBitmap;
public FingerPaintSavePage ()
{
InitializeComponent ();
}
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
// Create bitmap the size of the display surface
if (saveBitmap == null)
{
saveBitmap = new SKBitmap(info.Width, info.Height);
}
// Or create new bitmap for a new size of display surface
else if (saveBitmap.Width < info.Width || saveBitmap.Height < info.Height)
{
SKBitmap newBitmap = new SKBitmap(Math.Max(saveBitmap.Width, info.Width),
Math.Max(saveBitmap.Height, info.Height));
using (SKCanvas newCanvas = new SKCanvas(newBitmap))
{
newCanvas.Clear();
newCanvas.DrawBitmap(saveBitmap, 0, 0);
}
saveBitmap = newBitmap;
}
// Render the bitmap
canvas.Clear();
canvas.DrawBitmap(saveBitmap, 0, 0);
}
···
}
O desenho feito pelo PaintSurface
manipulador ocorre no final e consiste apenas em renderizar o bitmap.
O processamento de toque é semelhante ao programa anterior. O programa mantém duas coleções, inProgressPaths
e completedPaths
, que contêm tudo o que o usuário desenhou desde a última vez que a tela foi apagada. Para cada evento de toque, o OnTouchEffectAction
manipulador chama UpdateBitmap
:
public partial class FingerPaintSavePage : ContentPage
{
Dictionary<long, SKPath> inProgressPaths = new Dictionary<long, SKPath>();
List<SKPath> completedPaths = new List<SKPath>();
SKPaint paint = new SKPaint
{
Style = SKPaintStyle.Stroke,
Color = SKColors.Blue,
StrokeWidth = 10,
StrokeCap = SKStrokeCap.Round,
StrokeJoin = SKStrokeJoin.Round
};
···
void OnTouchEffectAction(object sender, TouchActionEventArgs args)
{
switch (args.Type)
{
case TouchActionType.Pressed:
if (!inProgressPaths.ContainsKey(args.Id))
{
SKPath path = new SKPath();
path.MoveTo(ConvertToPixel(args.Location));
inProgressPaths.Add(args.Id, path);
UpdateBitmap();
}
break;
case TouchActionType.Moved:
if (inProgressPaths.ContainsKey(args.Id))
{
SKPath path = inProgressPaths[args.Id];
path.LineTo(ConvertToPixel(args.Location));
UpdateBitmap();
}
break;
case TouchActionType.Released:
if (inProgressPaths.ContainsKey(args.Id))
{
completedPaths.Add(inProgressPaths[args.Id]);
inProgressPaths.Remove(args.Id);
UpdateBitmap();
}
break;
case TouchActionType.Cancelled:
if (inProgressPaths.ContainsKey(args.Id))
{
inProgressPaths.Remove(args.Id);
UpdateBitmap();
}
break;
}
}
SKPoint ConvertToPixel(Point pt)
{
return new SKPoint((float)(canvasView.CanvasSize.Width * pt.X / canvasView.Width),
(float)(canvasView.CanvasSize.Height * pt.Y / canvasView.Height));
}
void UpdateBitmap()
{
using (SKCanvas saveBitmapCanvas = new SKCanvas(saveBitmap))
{
saveBitmapCanvas.Clear();
foreach (SKPath path in completedPaths)
{
saveBitmapCanvas.DrawPath(path, paint);
}
foreach (SKPath path in inProgressPaths.Values)
{
saveBitmapCanvas.DrawPath(path, paint);
}
}
canvasView.InvalidateSurface();
}
···
}
O UpdateBitmap
método redesenha saveBitmap
criando um novo SKCanvas
, limpando-o e, em seguida, renderizando todos os caminhos no bitmap. Ele conclui invalidando canvasView
para que o bitmap possa ser desenhado na tela.
Aqui estão os manipuladores para os dois botões. O botão Limpar limpa as coleções de caminhos, atualiza saveBitmap
(o que resulta na limpeza do bitmap) e invalida o SKCanvasView
:
public partial class FingerPaintSavePage : ContentPage
{
···
void OnClearButtonClicked(object sender, EventArgs args)
{
completedPaths.Clear();
inProgressPaths.Clear();
UpdateBitmap();
canvasView.InvalidateSurface();
}
async void OnSaveButtonClicked(object sender, EventArgs args)
{
using (SKImage image = SKImage.FromBitmap(saveBitmap))
{
SKData data = image.Encode();
DateTime dt = DateTime.Now;
string filename = String.Format("FingerPaint-{0:D4}{1:D2}{2:D2}-{3:D2}{4:D2}{5:D2}{6:D3}.png",
dt.Year, dt.Month, dt.Day, dt.Hour, dt.Minute, dt.Second, dt.Millisecond);
IPhotoLibrary photoLibrary = DependencyService.Get<IPhotoLibrary>();
bool result = await photoLibrary.SavePhotoAsync(data.ToArray(), "FingerPaint", filename);
if (!result)
{
await DisplayAlert("FingerPaint", "Artwork could not be saved. Sorry!", "OK");
}
}
}
}
O manipulador de botões Salvar usa o método simplificado Encode
do SKImage
. Esse método codifica usando o formato PNG. O SKImage
objeto é criado com base em saveBitmap
, e o SKData
objeto contém o arquivo PNG codificado.
O ToArray
método de SKData
obtém uma matriz de bytes. Isso é o que é passado para o SavePhotoAsync
método, juntamente com um nome de pasta fixo e um nome de arquivo exclusivo construído a partir da data e hora atuais.
Veja o programa em ação:
Uma técnica muito semelhante é usada na amostra. Este também é um programa de pintura a dedo, exceto que o usuário pinta em um disco giratório que reproduz os desenhos em seus outros quatro quadrantes. A cor da pintura a dedo muda conforme o disco está girando:
O botão Salvar da SpinPaint
classe é semelhante ao Finger Paint , pois salva a imagem em um nome de pasta fixo (SpainPaint) e um nome de arquivo construído a partir da data e hora.