创建 SkiaSharp 位图并在其上绘制
你已了解应用程序如何从 Web、应用程序资源以及用户的照片图库加载位图。 还可以在应用程序中创建新的位图。 最简单的方法涉及到 SKBitmap
的一个构造函数:
SKBitmap bitmap = new SKBitmap(width, height);
width
和 height
参数是整数,指定位图的像素尺寸。 此构造函数为每个像素创建一个带四个节点的全色位图:红色、绿色、蓝色和 alpha(不透明度)分量各一个字节。
创建新位图后,需要在位图图面上添加一些内容。 通常有两种方法来执行此操作:
- 使用标准
Canvas
绘图方法在位图上进行绘制。 - 直接访问像素位。
本文演示了第一种方法:
有关第二种方法,请参阅访问 SkiaSharp 位图像素一文。
在位图上绘制
在位图图面上绘图与在视频显示器上绘图相同。 若要在视频显示器上进行绘制,请从 PaintSurface
事件参数获取 SKCanvas
对象。 若要在位图上绘制,请使用 SKCanvas
构造函数创建 SKCanvas
对象:
SKCanvas canvas = new SKCanvas(bitmap);
在位图上绘制完成后,可以释放 SKCanvas
对象。 因此,SKCanvas
构造函数通常在 using
语句中调用:
using (SKCanvas canvas = new SKCanvas(bitmap))
{
··· // call drawing function
}
然后,可以显示位图。 稍后,程序可以基于同一位图创建新的 SKCanvas
对象,并在其上进行更多绘制。
示例应用程序中的“Hello Bitmap”页在位图上书写文本“Hello, Bitmap!”,然后多次显示该位图。
HelloBitmapPage
构造函数首先是创建一个 SKPaint
对象来显示文本。 它确定文本字符串的尺寸,并使用这些维度创建位图。 然后,它基于该位图创建一个 SKCanvas
对象,调用 Clear
,然后调用 DrawText
。 使用新位图进行调用 Clear
总是一个好主意,因为新创建的位图可能包含随机数据。
构造函数最后会创建一个 SKCanvasView
对象来显示位图:
public partial class HelloBitmapPage : ContentPage
{
const string TEXT = "Hello, Bitmap!";
SKBitmap helloBitmap;
public HelloBitmapPage()
{
Title = TEXT;
// Create bitmap and draw on it
using (SKPaint textPaint = new SKPaint { TextSize = 48 })
{
SKRect bounds = new SKRect();
textPaint.MeasureText(TEXT, ref bounds);
helloBitmap = new SKBitmap((int)bounds.Right,
(int)bounds.Height);
using (SKCanvas bitmapCanvas = new SKCanvas(helloBitmap))
{
bitmapCanvas.Clear();
bitmapCanvas.DrawText(TEXT, 0, -bounds.Top, textPaint);
}
}
// Create SKCanvasView to view result
SKCanvasView canvasView = new SKCanvasView();
canvasView.PaintSurface += OnCanvasViewPaintSurface;
Content = canvasView;
}
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear(SKColors.Aqua);
for (float y = 0; y < info.Height; y += helloBitmap.Height)
for (float x = 0; x < info.Width; x += helloBitmap.Width)
{
canvas.DrawBitmap(helloBitmap, x, y);
}
}
}
PaintSurface
处理程序在显示的行和列中多次呈现位图。 请注意,PaintSurface
处理程序中的 Clear
方法具有一个 SKColors.Aqua
参数,该参数为显示图面的背景着色:
出现水绿色背景表明位图是透明的,但文本除外。
清除和透明度
出现“Hello Bitmap”页面表明程序创建的位图是透明的,但黑色文本除外。 这就是为什么文本在水绿色显示图面上可见的原因。
SKCanvas
的 Clear
方法文档是这样描述它们的:“替换画布当前剪辑中的所有像素。”“替换”一词的使用揭示了这些方法的一个重要特征:SKCanvas
的所有绘制方法都会向现有显示图面添加内容。 Clear
方法会替换图面上已经有的内容。
Clear
提供两个不同的版本:
使用
SKColor
参数的Clear
方法将显示图面的像素替换为该颜色的像素。没有参数的
Clear
方法将像素替换为SKColors.Empty
颜色,该颜色是所有分量(红色、绿色、蓝色和 alpha)都设置为零的颜色。 这种颜色有时称为“透明黑色”。
在新位图上不带参数的调用 Clear
会将整个位图初始化为完全透明。 随后在位图上绘制的任何内容通常都不透明或部分不透明。
下面是要尝试的操作:在“Hello Bitmap”页面中,将应用于 bitmapCanvas
的 Clear
方法替换为以下方法:
bitmapCanvas.Clear(new SKColor(255, 0, 0, 128));
SKColor
构造函数参数的顺序为红色、绿色、蓝色和 alpha,其中每个值的范围可以介于 0 到 255 之间。 请记住,alpha 值为 0 表示透明,而 alpha 值为 255 表示不透明。
值 (255, 0, 0, 128) 将位图像素清除为不透明度为 50% 的红色像素。 这意味着位图背景是半透明的。 位图的半透明红色背景与显示图面的水绿色背景相结合,创建了灰色背景。
请尝试通过将以下赋值放在 SKPaint
初始值设定项中,将文本的颜色设置为透明黑色:
Color = new SKColor(0, 0, 0, 0)
你可能会认为,这个透明文本将创建位图的完全透明区域,通过这些区域你将看到显示图面的水绿色背景。 但事实并非如此。 文本绘制在位图上已有的内容的上面。 透明文本将完全不可见。
没有一个 Draw
方法能让位图变得更透明。 只有 Clear
可以做到。
位图颜色类型
最简单的 SKBitmap
构造函数允许指定位图的整数像素宽度和高度。 其他 SKBitmap
构造函数更为复杂。 这些构造函数需要两个枚举类型的参数:SKColorType
和 SKAlphaType
。 其他构造函数使用 SKImageInfo
结构来合并此信息。
SKColorType
枚举有 9 个成员。 其中每个成员描述了存储位图像素的一种特定方式:
Unknown
Alpha8
- 每个像素为 8 位,表示从完全透明到完全不透明的 alpha 值Rgb565
- 每个像素为 16 位,红色和蓝色为 5 位,绿色为 6 位Argb4444
- 每个像素为 16 位,alpha、红色、绿色和蓝色各 4 位Rgba8888
- 每个像素为 32 位,红色、绿色、蓝色和 alpha 各 8 位Bgra8888
- 每个像素为 32 位,蓝色、绿色、红色和 alpha 各 8 位Index8
- 每个像素为 8 位,表示对SKColorTable
的索引Gray8
- 每个像素是 8 位,表示从黑色到白色的灰色阴影RgbaF16
- 每个像素为 64 位,红色、绿色、蓝色和 alpha 采用 16 位浮点格式
每个像素为 32 像素(4 字节)的两种格式通常称为全色格式。 许多其他格式都是在视频显示器本身还不能显示全色时出现的。 颜色有限的位图对于这些显示器来说已经足够,并允许位图占用更少的内存空间。
如今,程序员几乎总是使用全色位图,而不用担心其他格式。 RgbaF16
格式是一个例外,它允许比全色格式更高的颜色分辨率。 但是,此格式用于特殊用途,例如医学成像,并且在与标准全色显示器一起使用时没有多大意义。
本系列文章将自身限制为在未指定 SKColorType
成员时默认使用 SKBitmap
颜色格式。 整个默认格式基于底层平台。 对于 Xamarin.Forms 支持的平台,默认颜色类型为:
Rgba8888
(用于 iOS 和 Android)Bgra8888
(用于 UWP)
唯一的区别是内存中 4 个字节的顺序,这只在直接访问像素位时才会成为问题。 在你阅读访问 SkiaSharp 位图像素的文章之前,这不会变得很重要。
SKAlphaType
枚举有四个成员:
Unknown
Opaque
- 位图没有透明度Premul
- 颜色分量预先乘以 alpha 分量Unpremul
- 颜色分量不会预先乘以 alpha 分量
下面是一个透明度为 50% 的 4 字节红色位图像素,按红色、绿色、蓝色、alpha 的顺序显示字节:
0xFF 0x00 0x00 0x80
当在显示图面上呈现包含半透明像素的位图时,每个位图像素的颜色分量必须乘以该像素的 alpha 值,并且显示图面的相应像素的颜色分量必须乘以 255 减去 alpha 值。 然后,可以将两个像素合并。 如果位图像素中的颜色分量已预先乘以 alpha 值,那么可以更快地呈现位图。 同样的红色像素以预乘格式存储如下:
0x80 0x00 0x00 0x80
这种性能改进就是默认使用 Premul
格式创建 SkiaSharp
位图的原因。 但是,只有在访问和操作像素位时,才有必要知道这一点。
在现有位图上绘制
无需创建新的位图来绘制它。 你也可以在现有位图上绘制。
“髭长尾猴”页面使用其构造函数加载 MonkeyFace.png 图像。 然后,它基于该位图创建一个 SKCanvas
对象,并使用 SKPaint
和 SKPath
对象在位图上绘制胡子:
public partial class MonkeyMoustachePage : ContentPage
{
SKBitmap monkeyBitmap;
public MonkeyMoustachePage()
{
Title = "Monkey Moustache";
monkeyBitmap = BitmapExtensions.LoadBitmapResource(GetType(),
"SkiaSharpFormsDemos.Media.MonkeyFace.png");
// Create canvas based on bitmap
using (SKCanvas canvas = new SKCanvas(monkeyBitmap))
{
using (SKPaint paint = new SKPaint())
{
paint.Style = SKPaintStyle.Stroke;
paint.Color = SKColors.Black;
paint.StrokeWidth = 24;
paint.StrokeCap = SKStrokeCap.Round;
using (SKPath path = new SKPath())
{
path.MoveTo(380, 390);
path.CubicTo(560, 390, 560, 280, 500, 280);
path.MoveTo(320, 390);
path.CubicTo(140, 390, 140, 280, 200, 280);
canvas.DrawPath(path, paint);
}
}
}
// Create SKCanvasView to view result
SKCanvasView canvasView = new SKCanvasView();
canvasView.PaintSurface += OnCanvasViewPaintSurface;
Content = canvasView;
}
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
canvas.DrawBitmap(monkeyBitmap, info.Rect, BitmapStretch.Uniform);
}
}
构造函数最后创建一个 SKCanvasView
,其 PaintSurface
处理程序只显示结果:
复制和修改位图
可用于在位图上绘制的 SKCanvas
方法包括 DrawBitmap
。 这意味着你可以在一个位图上绘制另一个位图,通常以某种方式修改它。
修改位图的最通用方法是访问实际像素位,可在访问 SkiaSharp 位图像素一文中了解此主题。 但是,还有其他许多技术可以修改位图,而不需要访问像素位。
示例应用程序附带的以下位图宽为 360 像素,高度为 480 像素:
假设你还没有获得左侧猴子的权限来发布这张照片。 一种解决方案是使用一种称为“像素化”的技术来模糊猴子的脸。 猴脸的像素将替换为颜色块,所以你无法分辨出特征。 颜色块通常派生自原始图像,方法是对这些块对应的像素的颜色求平均值。 但是,你不需要自己求平均值。 当你将位图复制到较小的像素维度时,会自动求平均值。
左侧猴子的脸占据大约一个 72 像素的正方形区域,左上角位于点 (112, 238) 处。 让我们将这个 72 像素的正方形区域替换为 9×9 的彩色块数组,每个块都是 8×8 像素的正方形。
“像素化图像”页面在该位图中加载,然后首先创建一个小的名为 faceBitmap
的 9 像素正方形位图。 这是复制猴脸放置的位置。 目标矩形是只有 9 像素的正方形,但源矩形是 72 像素的正方形。 每个 8x8 个源像素块都通过求颜色平均值合并到一个像素。
下一步是将原始位图复制到具有相同大小的名为 pixelizedBitmap
的新位图中。 然后,将小的 faceBitmap
复制到具有 72 像素目标矩形的位图上面,使 faceBitmap
的每个像素扩展到其大小的 8 倍:
public class PixelizedImagePage : ContentPage
{
SKBitmap pixelizedBitmap;
public PixelizedImagePage ()
{
Title = "Pixelize Image";
SKBitmap originalBitmap = BitmapExtensions.LoadBitmapResource(GetType(),
"SkiaSharpFormsDemos.Media.MountainClimbers.jpg");
// Create tiny bitmap for pixelized face
SKBitmap faceBitmap = new SKBitmap(9, 9);
// Copy subset of original bitmap to that
using (SKCanvas canvas = new SKCanvas(faceBitmap))
{
canvas.Clear();
canvas.DrawBitmap(originalBitmap,
new SKRect(112, 238, 184, 310), // source
new SKRect(0, 0, 9, 9)); // destination
}
// Create full-sized bitmap for copy
pixelizedBitmap = new SKBitmap(originalBitmap.Width, originalBitmap.Height);
using (SKCanvas canvas = new SKCanvas(pixelizedBitmap))
{
canvas.Clear();
// Draw original in full size
canvas.DrawBitmap(originalBitmap, new SKPoint());
// Draw tiny bitmap to cover face
canvas.DrawBitmap(faceBitmap,
new SKRect(112, 238, 184, 310)); // destination
}
// Create SKCanvasView to view result
SKCanvasView canvasView = new SKCanvasView();
canvasView.PaintSurface += OnCanvasViewPaintSurface;
Content = canvasView;
}
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
canvas.DrawBitmap(pixelizedBitmap, info.Rect, BitmapStretch.Uniform);
}
}
构造函数最后会创建一个 SKCanvasView
来显示结果:
旋转位图
另一个常见任务是旋转位图。 从 iPhone 或 iPad 照片图库检索位图时,这特别有用。 除非在拍摄照片时设备放在特定方向,否则图片可能是倒置或侧向的。
将位图倒置需要创建与第一个位图大小相同的另一位图,然后将变换设置为旋转 180 度,同时将第一个位图复制到第二个位图。 在该部分的所有示例中,bitmap
是需要旋转的 SKBitmap
对象:
SKBitmap rotatedBitmap = new SKBitmap(bitmap.Width, bitmap.Height);
using (SKCanvas canvas = new SKCanvas(rotatedBitmap))
{
canvas.Clear();
canvas.RotateDegrees(180, bitmap.Width / 2, bitmap.Height / 2);
canvas.DrawBitmap(bitmap, new SKPoint());
}
旋转 90 度时,需要交换高度和宽度来创建与原始位图大小不同的位图。 例如,如果原始位图宽 1200 像素且高 800 像素,那么旋转后的位图宽为 800 像素,高 1200 像素。 设置转换和旋转,使位图围绕其左上角旋转,然后移入视图。 (请记住,调用 Translate
和 RotateDegrees
方法的顺序与应用它们的方式相反。)下面是用于顺时针旋转 90 度的代码:
SKBitmap rotatedBitmap = new SKBitmap(bitmap.Height, bitmap.Width);
using (SKCanvas canvas = new SKCanvas(rotatedBitmap))
{
canvas.Clear();
canvas.Translate(bitmap.Height, 0);
canvas.RotateDegrees(90);
canvas.DrawBitmap(bitmap, new SKPoint());
}
下面是一个类似的函数,用于逆时针旋转 90 度:
SKBitmap rotatedBitmap = new SKBitmap(bitmap.Height, bitmap.Width);
using (SKCanvas canvas = new SKCanvas(rotatedBitmap))
{
canvas.Clear();
canvas.Translate(0, bitmap.Width);
canvas.RotateDegrees(-90);
canvas.DrawBitmap(bitmap, new SKPoint());
}
这两种方法在剪裁 SkiaSharp 位图文章的“照片拼图”页面中使用。
允许用户以 90 度增量旋转位图的程序只需实现一个函数即可旋转 90 度。 然后,通过重复执行这一个函数,用户可以 90 度的任何增量进行旋转。
程序还可以按任意量旋转位图。 一个简单的方法是修改旋转 180 度的函数,将 180 替换为通用化的 angle
变量:
SKBitmap rotatedBitmap = new SKBitmap(bitmap.Width, bitmap.Height);
using (SKCanvas canvas = new SKCanvas(rotatedBitmap))
{
canvas.Clear();
canvas.RotateDegrees(angle, bitmap.Width / 2, bitmap.Height / 2);
canvas.DrawBitmap(bitmap, new SKPoint());
}
然而,在一般情况下,此逻辑将剪掉旋转后的位图的四个角。 更好的方法是使用三角函数计算已旋转的位图的大小,以包括这些角。
这个三角函数显示在“位图旋转器”页中。 XAML 文件实例化 SKCanvasView
和 Slider
,范围为 0 到 360 度,其中 Label
显示当前值:
<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"
x:Class="SkiaSharpFormsDemos.Bitmaps.BitmapRotatorPage"
Title="Bitmap Rotator">
<StackLayout>
<skia:SKCanvasView x:Name="canvasView"
VerticalOptions="FillAndExpand"
PaintSurface="OnCanvasViewPaintSurface" />
<Slider x:Name="slider"
Maximum="360"
Margin="10, 0"
ValueChanged="OnSliderValueChanged" />
<Label Text="{Binding Source={x:Reference slider},
Path=Value,
StringFormat='Rotate by {0:F0}°'}"
HorizontalTextAlignment="Center" />
</StackLayout>
</ContentPage>
代码隐藏文件加载一个位图资源,并将其保存为名为 originalBitmap
的静态只读字段。 PaintSurface
处理程序中显示的位图是 rotatedBitmap
,它最初设置为 originalBitmap
:
public partial class BitmapRotatorPage : ContentPage
{
static readonly SKBitmap originalBitmap =
BitmapExtensions.LoadBitmapResource(typeof(BitmapRotatorPage),
"SkiaSharpFormsDemos.Media.Banana.jpg");
SKBitmap rotatedBitmap = originalBitmap;
public BitmapRotatorPage ()
{
InitializeComponent ();
}
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
canvas.DrawBitmap(rotatedBitmap, info.Rect, BitmapStretch.Uniform);
}
void OnSliderValueChanged(object sender, ValueChangedEventArgs args)
{
double angle = args.NewValue;
double radians = Math.PI * angle / 180;
float sine = (float)Math.Abs(Math.Sin(radians));
float cosine = (float)Math.Abs(Math.Cos(radians));
int originalWidth = originalBitmap.Width;
int originalHeight = originalBitmap.Height;
int rotatedWidth = (int)(cosine * originalWidth + sine * originalHeight);
int rotatedHeight = (int)(cosine * originalHeight + sine * originalWidth);
rotatedBitmap = new SKBitmap(rotatedWidth, rotatedHeight);
using (SKCanvas canvas = new SKCanvas(rotatedBitmap))
{
canvas.Clear(SKColors.LightPink);
canvas.Translate(rotatedWidth / 2, rotatedHeight / 2);
canvas.RotateDegrees((float)angle);
canvas.Translate(-originalWidth / 2, -originalHeight / 2);
canvas.DrawBitmap(originalBitmap, new SKPoint());
}
canvasView.InvalidateSurface();
}
}
Slider
的 ValueChanged
处理程序执行操作来根据旋转角度创建一个新的 rotatedBitmap
。 新的宽度和高度是基于原始宽度和高度的正弦和余弦的绝对值。 用于在旋转后的位图上绘制原始位图的变换将原始位图中心移动到原点,按指定的度数旋转它,然后将该中心转换为旋转后的位图的中心。 (Translate
和 RotateDegrees
方法的调用顺序与它们的应用方式相反。)
请注意,如果使用 Clear
方法,rotatedBitmap
的背景会变成浅粉色。 这仅仅是为了说明 rotatedBitmap
在显示器上的大小:
旋转后的位图刚好大到足以显示整个原始位图,但不能再大了。
翻转位图
经常在位图上执行的另一个操作是翻转。 从概念上讲,位图是通过位图的中心围绕垂直轴或水平轴在三维中旋转的。 垂直翻转会创建镜像图像。
示例应用程序中的“位图翻转器”页演示了这些过程。 XAML 文件包含一个 SKCanvasView
和两个按钮,这些按钮用于垂直和水平翻转:
<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"
x:Class="SkiaSharpFormsDemos.Bitmaps.BitmapFlipperPage"
Title="Bitmap Flipper">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<skia:SKCanvasView x:Name="canvasView"
Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2"
PaintSurface="OnCanvasViewPaintSurface" />
<Button Text="Flip Vertical"
Grid.Row="1" Grid.Column="0"
Margin="0, 10"
Clicked="OnFlipVerticalClicked" />
<Button Text="Flip Horizontal"
Grid.Row="1" Grid.Column="1"
Margin="0, 10"
Clicked="OnFlipHorizontalClicked" />
</Grid>
</ContentPage>
代码隐藏文件在按钮的 Clicked
处理程序中实现这两个操作:
public partial class BitmapFlipperPage : ContentPage
{
SKBitmap bitmap =
BitmapExtensions.LoadBitmapResource(typeof(BitmapRotatorPage),
"SkiaSharpFormsDemos.Media.SeatedMonkey.jpg");
public BitmapFlipperPage()
{
InitializeComponent();
}
void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
{
SKImageInfo info = args.Info;
SKSurface surface = args.Surface;
SKCanvas canvas = surface.Canvas;
canvas.Clear();
canvas.DrawBitmap(bitmap, info.Rect, BitmapStretch.Uniform);
}
void OnFlipVerticalClicked(object sender, ValueChangedEventArgs args)
{
SKBitmap flippedBitmap = new SKBitmap(bitmap.Width, bitmap.Height);
using (SKCanvas canvas = new SKCanvas(flippedBitmap))
{
canvas.Clear();
canvas.Scale(-1, 1, bitmap.Width / 2, 0);
canvas.DrawBitmap(bitmap, new SKPoint());
}
bitmap = flippedBitmap;
canvasView.InvalidateSurface();
}
void OnFlipHorizontalClicked(object sender, ValueChangedEventArgs args)
{
SKBitmap flippedBitmap = new SKBitmap(bitmap.Width, bitmap.Height);
using (SKCanvas canvas = new SKCanvas(flippedBitmap))
{
canvas.Clear();
canvas.Scale(1, -1, 0, bitmap.Height / 2);
canvas.DrawBitmap(bitmap, new SKPoint());
}
bitmap = flippedBitmap;
canvasView.InvalidateSurface();
}
}
垂直翻转通过水平缩放因子为 –1 的缩放变换来实现。 缩放中心是位图的垂直中心。 水平翻转是垂直缩放因子为 –1 的缩放变换。
就像你看到猴子衬衫上的字母是相反的,翻转与旋转不同。 但是,正如右侧的 UWP 屏幕截图所示,同时水平和垂直翻转相当于旋转 180 度:
可以使用类似技术处理的另一个常见任务是将位图裁剪为矩形子集。 相关描述可参阅剪裁 SkiaSharp 位图。