当前位置: 首页 > 工具软件 > Skia > 使用案例 >

skia 之canvas

濮阳宁
2023-12-01

canvas是skia的核心部分,skia的逻都是围绕skcanvas对象来组织管理的,通过skcanvas可以指定不同的渲染上下文、draw call(绘制命令)、以及绘制状态管理(如绘图矩阵、操作栈等)

skcanvas的状态

skcanvasskpaint共同提供了sksurface和skbasedevice上绘制的状态,skcanvas保存了所有操作的堆栈,通过 saverestore这两个方法来管理skcanvas的操作状态信息。

Backend

skcanvas是可以指定在特定的像素上进行绘制,为其提供这种能力主要是不同画布来提供的(sksurface),skcanvas本身也是一个基类,画布主要可以分为两种,Raster Surface 和 GPU Suface。
Raster Surface生成的canvas可以将绘制生成在cpu的内存上(如在不同的图片绘制)
CPU Surface 生成的canvas 可以内部通过opengl、webgl、vulkan来进行绘制(目前skia可以在webassembly技术在web上使用)
skia提供的backends(理解为不同的后端渲染设备),有以下几种:
raster cpu渲染(如将结果绘制在不同的图片上,可以理解为只要绘制在CPU的内存上,都可以使用这种方式)
gpu 绘制在GPU上,可以创建opengl、vulkan的绘制
skPdf 将结果绘制在PDF文档上
skPicture将结果绘制在显示列表中(类似于内存结构,下次绘制可以用来加速)
skSvg将结果绘制在Svg文档上。
skXPS将结果绘制在XPS文档上.

Raster Backend

Raster是通过指定一块内存,canvas会将draw calls绘制在一块CPU的内存上,这个内存一般来说是一个栅格图片.下面的代码有三种实现方式(指定图片、指定内存、指定窗口句柄Hwnd)来实现绘制。

#include "SkData.h"
#include "SkImage.h"
#include "SkStream.h"
#include "SkSurface.h"
//内部创建内存,将结果保存在一张图片中;
void raster(int width, int height,
            void (*draw)(SkCanvas*),
            const char* path) {
    sk_sp<SkSurface> rasterSurface =
            SkSurface::MakeRasterN32Premul(width, height);
    SkCanvas* rasterCanvas = rasterSurface->getCanvas();
    draw(rasterCanvas);
    sk_sp<SkImage> img(rasterSurface->makeImageSnapshot());
    if (!img) { return; }
    sk_sp<SkData> png(img->encode());
    if (!png) { return; }
    SkFILEWStream out(path);
    (void)out.write(png->data(), png->size());
}

//指定一片内存,将结果绘制在这个内存中

void raster(char* pixel,int width,int height,void (*draw)(SkCanvas*))
{
    const SkImageInfo image_info = SkImageInfo::Make(data->width(), data->height(),kN32_SkColorType, SkAlphaType::kPremul_SkAlphaType);
    //获取每一行的字节大小;
    const int bytes_per_line = sizeof(uint32_t)*width;
    auto canvas = SkCanvas::MakeRasterDirect(image_info, pixel, bytes_per_line);
    draw(canvas)//绘制回调;
}

//windows 下绑定窗口句柄的CPU绘制
#include <windows.h>

 SkAutoMalloc  fSurfaceMemory;
void renderWindows(Hwnd hwnd)
{
    //获取窗口大小;
    RECT rect;
    GetClientRect(fWnd, &rect);
    int w=rect.right-rect.left;
    int h=rect.bottom-rect.top;
     const size_t bmpSize = sizeof(BITMAPINFOHEADER) + w * h * sizeof(uint32_t);
    //创建一片内存与窗口的内存大小相同;
   
    fSurfaceMemory.reset(bmpSize);
    BITMAPINFO* bmpInfo = reinterpret_cast<BITMAPINFO*>(fSurfaceMemory.get());
    ZeroMemory(bmpInfo, sizeof(BITMAPINFO));
    bmpInfo->bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
    bmpInfo->bmiHeader.biWidth = w;
    bmpInfo->bmiHeader.biHeight = -h; // negative means top-down bitmap. Skia draws top-down.
    bmpInfo->bmiHeader.biPlanes = 1;
    bmpInfo->bmiHeader.biBitCount = 32;
    bmpInfo->bmiHeader.biCompression = BI_RGB;
    void* pixels = bmpInfo->bmiColors;

  
    SkImageInfo info = SkImageInfo::Make(w, h, fDisplayParams.fColorType, kPremul_SkAlphaType, fDisplayParams.fColorSpace);
    //将窗口的内存位置给到内存图片中.
    fBackbufferSurface = SkSurface::MakeRasterDirect(info, pixels, sizeof(uint32_t) * w);
}
//将结果同步到hwnd上;
void swapBuffers() {
    BITMAPINFO* bmpInfo = reinterpret_cast<BITMAPINFO*>(fSurfaceMemory.get());
    HDC dc = GetDC(fWnd);
    
    RECT rect;
    GetClientRect(fWnd, &rect);
    int w=rect.right-rect.left;
    int h=rect.bottom-rect.top;
    
    StretchDIBits(dc, 0, 0, w, h, 0, 0, w, h, bmpInfo->bmiColors, bmpInfo,
                  DIB_RGB_COLORS, SRCCOPY);
    ReleaseDC(fWnd, dc);
}

GPU Surface

必须有一个GrContext对象来管理gpu context,skia自身没有直接通过窗口创建上下文的方法,不过有一个示例的库(tools/sk_app)可以创建不同平台、Surface窗口的方法。

#include "GrContext.h"
#include "gl/GrGLInterface.h"
#include "SkData.h"
#include "SkImage.h"
#include "SkStream.h"
#include "SkSurface.h"

void gl_example(int width, int height, void (*draw)(SkCanvas*), const char* path) {
    // You've already created your OpenGL context and bound it.
    const GrGLInterface* interface = nullptr;
    // Leaving interface as null makes Skia extract pointers to OpenGL functions for the current
    // context in a platform-specific way. Alternatively, you may create your own GrGLInterface and
    // initialize it however you like to attach to an alternate OpenGL implementation or intercept
    // Skia's OpenGL calls.
    sk_sp<GrContext> context = GrContext::MakeGL(interface);
    SkImageInfo info = SkImageInfo:: MakeN32Premul(width, height);
    sk_sp<SkSurface> gpuSurface(
            SkSurface::MakeRenderTarget(context, SkBudgeted::kNo, info));
    if (!gpuSurface) {
        SkDebugf("SkSurface::MakeRenderTarget returned null\n");
        return;
    }
    SkCanvas* gpuCanvas = gpuSurface->getCanvas();
    draw(gpuCanvas);
    sk_sp<SkImage> img(gpuSurface->makeImageSnapshot());
    if (!img) { return; }
    sk_sp<SkData> png(img->encode());
    if (!png) { return; }
    SkFILEWStream out(path);
    (void)out.write(png->data(), png->size());
}

SkPicture

使用显示列表的方式有点类似opengl的 displaylist,即将绘制的结果存储起来,下次绘制的时候只需要调用drawcall绘制指令即可,不需要重新准备数据。只要不改变数据,画布的放大缩小等不需要重新更新数据绘制。

//使用SkPictureRecorder做记录
void picture(int width, int height  void (*draw)(SkCanvas*))
{
    SkPictureRecorder recorder;
    
    auto recording_canvas = recorder.beginRecording(SkIntToScalar(width),
                                                        SkIntToScalar(height));
    //调用普通的绘制方法即可;
    draw(recording_canvas);
    
    //将记录存储在canvas中
     auto picture = recorder.finishRecordingAsPicture();
}

//使用回放;
void drawPicture(sk_sp<SkPicture> picture)
{
     sk_sp<SkSurface> rasterSurface =
            SkSurface::MakeRasterN32Premul(width, height);
    SkCanvas* raster_canvas = rasterSurface->getCanvas();
    //使用playback绘制
    picture->playback(&raster_canvas);
}

SkSvg
通过canvas的方式输出Svg矢量图形,与绘制达到一样的效果(同样支持各种裁剪)

//将绘制结果保存到Svg文件中;
void drawSvg(int width,int height,void (*draw)(SkCanvas*),const char* path)
{
	const auto wStream = std::make_shared<SkFILEWStream>(path);
    
    auto canvas = SkSVGCanvas::Make(SkRect::MakeWH(width, height), wStream.get());
    
    draw(canvas);
    
    wStream->flush();
}

//读写svg文件并进行绘制(skia需要静态编译才能编译sksvg模块)
void drawSvg(skCavas* canvas,double x,double y,double width,double height)
{
    SkFILEStream text_stream(image_file.toLocal8Bit());
    SkTDArray<char> inData;
    inData.setCount((int)text_stream.getLength());
    size_t inLen = inData.count();

    text_stream.read(inData.begin(), inLen);
    text_stream.close();
    const auto svg_stream = SkMemoryStream::MakeDirect(inData.begin(), inLen);

    if(svg_stream == nullptr)
    {
        return { nullptr };
    }

    auto svg_dom = SkSVGDOM::Builder().make(*svg_stream);

    if(svg_dom == nullptr)
    {
        return ;
    }
    canvas->save();

	double widthRatio = 1;
	double heightRatio = 1;
	const double w=svg_dom->getRoot()->getWidth().value();
	const double h= svg_dom->getRoot()->getHeight().value();

	if(rect.isValid())
	{
		widthRatio = width /w ;
		heightRatio = rect.height() / h;
	}

    canvas->translate(x,y);
    
	canvas->scale(widthRatio, heightRatio);
	svg_dom->render(canvas);

	canvas->restore();
    
}

SkPDF 输出PDF矢量图形,PDF可以分页打印,也可以通过skPDFOjbect来扩展实现自定义信息的填写。SKPDF与SKXPS的使用方式基本一致;

void drawPDF(const char* pdf_name)
{
    const auto wStream = std::make_shared<SkFILEWStream>(byte_array.constData());
     SkPDF::Metadata metadata;
	sk_sp<SkDocument> pdf_doc = SkPDF::MakeDocument(wStream.get(), metadata);
    //
	auto pdf_canvas =pdf_doc->beginPage(width, height);
    //与正常方法一样调用绘制;
    skPaint _paint;
    pdf_canvas->drawLine(0,0,100,100,_paint);
    pdf_doc->endPage();
    wstream->flush();
    
    wstream->close();
}

 类似资料: