Quartz2D –> 二维绘图发动机(一)

By - Last updated: 星期一, 七月 24, 2017
作者 寻形觅影 2017.07.21 13:29 字数 4455

The Quartz 2D API is easy to use and provides access to powerful features such as transparency layers, path-based drawing, offscreen rendering, advanced color management, anti-aliased rendering, and PDF document creation, display, and parsing.

 在官方文档介绍中就已经直述:Quartz 2D API易于使用和提供强大的特性,如透明层,基于路径画图,离屏渲染,先进的色彩管理,反锯齿的渲染,PDF文档创建、显示、解析。

一、Quartz 2D 可以完成的功能:

  • 图形绘制(绘图,自定义控件,生成图片等);
  • 在程序中提供图形编辑功能(图片剪辑等);
  • 创建或显示 bitmap images;
  • 与PDF文件相关操作(读取,创建,解析PDF文件等);

二、Quartz 2D 概述:

 Quartz 2D是一个二维图形绘制引擎,支持iOS环境和Mac OS X环境。我们可以使用Quartz 2D API来实现许多功能,如基本路径的绘制、透明度、描影、绘制阴影、透明层、颜色管理、反锯齿渲染、PDF文档生成和PDF元数据访问。
 在Mac OS X中,Quartz 2D可以与其它所有图形图像技术混合使用,如Core Image、Core Video、OpenGL、QuickTime。
 同样,在iOS中Quartz 2D可以使用所有可用的图形和动画技术,如核心动画-Core Animation , OpenGL ES , UIKit类。

1、页面(The Page)

 Quartz 2D在图像中使用了绘画者模型(painter’s model)。在绘画者模型中,每个连续的绘制操作都是将一个绘制层(a layer of ‘paint’)放置于一个画布(‘canvas’),通常称这个画布为(Page)。 Page上的绘图可以通过额外的绘制操作来叠加更多的绘图。意味着每一次绘制都是一层,然后按照顺序一层层的叠加到画板上。Page上的图形对象只能通过叠加更多的绘图来改变而不能直接修改。这个模型允许我们使用小的图元来构建复杂的图形。
 图1-1展示了绘画者模型如何工作。从图中可以看出不同的绘制顺序所产生的效果不一样。在绘画者模型中的顺序是很重要的。


Figure 1-1

 The Page可能是一个真正的纸(如果输出设备是一台打印机);这可能是一个虚拟的纸(如果输出设备是一个PDF文件),甚至可能是一个bitmap image。 The Page的确切性质取决于你所使用的特定的图形上下文(graphics context )。

2、绘制目标:图形上下文(Drawing Destinations: The Graphics Context)

 Graphics Context是一个不透明的数据类型(CGContextRef),用于封装Quartz绘制图像到输出设备的信息,如PDF文件、bitmap图片或者正在显示的窗口。(所谓 “不透明”是指 opaque,指颜色不可复合显示,与alpha是不一样的!这种数据模型是C的结构体,存储了渲染到屏幕上需要的一切信息。 Graphics Context中的信息包括在Page中的图像的图形绘制参数和设备相关的表现形式。Quartz中所有的对象都是绘制到或者被包含到一个Graphics Context中。
 我们可以将Graphics Context作为绘制目标,如图1-2所示。当用Quartz绘图时,所有设备相关的特性都包含在我们所使用的Graphics Context中。换句话说,我们可以简单地给Quartz绘图序列指定不同的Graphics Context,就可将相同的图像绘制到不同的设备上。我们不需要任何设备相关的计算;这些都由Quartz替我们完成。


Figure 1-2

Quartz 2D 提供了一下几种类型的Graphics Context:

  • Bitmap graphics context: 允许使用RGB colors、CMYK colors和 灰度模式。A bitmap 就是一个像素的矩形数组,每一个像素在图片上一个点。Bitmap 图片也称为采样图片。
  • PDF graphics context: 允许创建一个PDF文件,在PDF中,你的绘制会作为一系列的命令被保存。在这里有一些值得注意的PDF files 和 bitmaps 的不同:
    • PDF files可能包含不止一个页面,而 bitmaps只包含一个页面;
    • 当在不同设备中绘制页面时,结果图片是对于该设备显示性能来说最优的显示结果图片。
    • PDF本质上是与分辨率无关的,尺寸上的增加和减少都不会牺牲图像的细节。用户感知的一个bitmap图片的质量与 该bitmap的分辨率相关联。
  • Window graphics context: 允许在窗口内绘制。因为Quartz 2D是图形引擎,而不是窗口管理系统,所以你可以使用一个应用程序框架获得窗口的图形上下文。
  • Layer Context: (CGLayerRef) 是一个与另一个graphics context有关系的幕后绘制目的的上下文。它旨在当绘制layer层图形上下文时提供最佳性能而创建。Layer Context 在幕后绘制方面相对于Bitmap graphics context来说是一个更好的选择。
  • PostScript graphics context: 当你在Mac OS X情况下想要打印时,你发送内容内容到由printing framework管理管理的PostScript graphics context。iOS下不可用!
3、Quartz 2D 数据类型

 除了 Graphics Context 之外,Quartz 2D API还定义一些数据类型。由于这些API就Core Graphics框架的一部分,所以这些数据类型都是以CG开头的。
 Quartz 2D使用这些数据类型来创建对象,通过操作这些对象来获取特定的图形。图1-3例举了三个使用Quartz 2D的绘制操作所获得的图像。

  • 你可以通过创建一个PDF 页面对象,然后对图形上下文应用一个旋转的操作,同时请求Quartz 2D将这一页面绘制到图形上下文来实现旋转和显示一个PDF页面。
  • 你可以通过创建一个图案对象,定义组成图案的形状模型及当Quartz 2D绘制一个图形上下文时设置Quartz 2D作为颜料被应用于该图案去绘制一个图案。
  • 你可以通过创建一个底纹对象,同时提供一个确定在底纹上每一点的颜色的函数,然后请求Quartz 2D使用这一底纹作为填充颜色的这样一个轴向的或放射状的底纹,去填充某一块区域。

Figure 1-3

在Quartz 2D中可用的不透明数据类型包含以下几种:

  • CGPathRef :用于向量图,可创建路径,并进行填充(fill)或描画(stroke),用来绘制路径(注意带有ref后缀的一般都是绘制的画板);
  • CGImageRef: 用于描述你提供的bitmap images和基于样本据的bitmap图像遮罩;
  • CGLayerRef : 用于描述可用于重复绘制(如背景或图案)和幕后绘制(offscreen drawing)的绘画层(绘制layer,layer可复用,可离屏渲染);
  • CGPatternRef : 用于重绘;
  • CGShadingRef 和 CGGradientRef : 用于绘制渐变;
  • CGFunctionRef : 用于定义回调函数,该函数包含一个随机的浮点值参数。当为阴影创建渐变时使用该类型;
  • CGColorRef 和 CGColorSpaceRef : 用于告诉Quartz如何解释(描述)颜色(处理颜色);
  • CGImageSourceRef 和 CGImageDestinationRef : 用于在Quartz中移入移出数据;
  • CGFontRef : 用于绘制文本;
  • CGPDFDictionaryRef, CGPDFObjectRef, CGPDFPageRef, CGPDFStream, CGPDFStringRef 和 CGPDFArrayRef : 提供PDF文档元数据的访问;
  • CGPDFScannerRef 和 CGPDFContentStreamRef : 用于解析PDF元数据;
  • CGPSConverterRef : 用于将PostScript转换为PDF。它在iOS是不可用的!
4、Graphics States(图形状态)

 Quartz通过修改当前图形状态(current graphics state)来修改绘制操作的结果。图形状态包含用于绘制程序的参数。绘制程序根据这些绘图状态来决定如何渲染结果。例如,当你调用设置填充颜色的函数时,你将改变存储在当前绘图状态中的颜色值。当前图形状态常用的元素有:线宽(line width)、当前位置(position)、文本字体大小(font)。
 Graphics Context包含一个绘图状态栈。当Quartz创建一个Graphics Context时,栈为空。当保存图形状态时,Quartz将当前图形状态的一个副本压入栈中。当还原图形状态时,Quartz将栈顶的图形状态出栈。出栈的状态成为当前图形状态。
 使用函数CG_EXTERN void CGContextSaveGState(CGContextRef cg_nullable c)来保存图形状态,这里需要注意当前路径(The Current Path)不被认为是图形状态的一部分,不会保存路径。使用函数CG_EXTERN void CGContextRestoreGState(CGContextRef cg_nullable c)来出栈栈顶图形状态用以代替当前图形状态(还原图形状态)。

并不是当前绘制环境的所有方面都是图形状态的元素,下面列表中参数在保存图形状态时会被保存:

Parameters 参数
Current transformation matrix (CTM) 当前装换矩阵
Clipping area 剪裁区域
Line: width, join, cap, dash, miter limit 线的宽度、连接风格、线帽样式、线条缓冲类型(虚线样式)、斜接限制
Accuracy of curve estimation (flatness) 曲线平滑度
Anti-aliasing setting 反锯齿设置
Color: fill and stroke settings 颜色填充和描绘设置
Alpha value (transparency) 透明度
Rendering intent 渲染目标
Color space: fill and stroke settings 颜色空间填充和描绘设置
Text: font, font size, character spacing, text drawing mode 文本设置:字体、字体大小、字符间距、文字绘制模型
Blend mode 混合模型
5、Quartz 2D Coordinate Systems(Quartz 2D 坐标系统)

 该坐标系是以默认是以左下为圆点,是一个标准的数学坐标系(注意区别于UIKit坐标系),它定义了用于描述被绘制于画布上的对象的位置和大小的位置范围。


Figure 1-4

 因为不同的设备有不同的潜在成像能力,所以图形位置和尺寸必须在定义时使用与设备无关的方式。例如,一个显示设备的屏幕可能显示不超过每英寸96个像素,而打印机可能能够显示每英寸300个像素。如果您定义坐标系统在设备层面(在这个例子中,96像素或300像素),那么绘制于该空间的对象不可能在没有明显的失真的情况下复制在其他设备中,他们会出现太大或太小的情况。这显然不是我们想要的。
  Quartz使用当前变换矩阵(current transformation matrix又称为 CTM)将一个独立坐标系统(用户空间)映射到输出设备的坐标系统(设备空间)来实现设备独立性。矩阵是用于高效的描述一组相关方程的数学架构,CTM是一种被称为 affine transform (仿射变换)的特殊矩阵。关于仿射变换可以看一下这篇文章。但是CTM与文章中的略有不同,CTM主要变换的是 坐标系
 CTM还有一个次要目的:允许你通过转换来决定对象如何被绘制。例如,为了绘制一个旋转了45度的盒子,我们可以在绘制盒子之前旋转Page的坐标系统。Quartz使用旋转过的坐标系统来将盒子绘制到输出设备中。
 有一些技术在设置它们的graphics context时使用了不同于Quartz的默认坐标系统。当在这些坐标系统中显示Quartz绘制的图形时,Quartz的默认坐标系统必须进行转换。最常见的一种修改的坐标系统是原点位于左上角,而沿着y轴从上到下坐标值逐渐增大(即UIKit坐标系)。例如:UIView的-(void)drawRect:方法(这个方法在loadView、viewDidLoad方法后执行)中直接获取上下文的方法UIGraphicsGetCurrentContext()返回的图形上下文就是用的是这种坐标系。这是因为UIKit类已经对上下文进行了额外的修改以匹配UIKit的约定。但是有一些方法在录入 graphics context 时是使用的Quartz的默认坐标系统,例如CG_EXTERN void CGContextDrawImage(CGContextRef cg_nullable c, CGRect rect, CGImageRef cg_nullable image)方法将图片绘制于上下文中时就是使用的Quartz的默认坐标系统。特别的,patterns和shadows不被CTM影响,是单独进行调整以来匹配UIKit坐标系统。
注意: 转换坐标系前,应使用CG_EXTERN void CGContextSaveGState(CGContextRef cg_nullable c)保存当前上下文状态;然后当转换后一系列操作完成后再使用CG_EXTERN void CGContextRestoreGState(CGContextRef cg_nullable c)恢复之前保存的上下文状态

6、Memory Management: Object Ownership(内存管理:对象所有权)

 Quartz使用Core Foundation内存管理机制-引用计数。在Quartz中使用需要注意以下几点:

  • 如果你创建或复制一个对象,你拥有它,因此你必须释放它。一般来说,如果你从一个函数名包含“Create”或者“Copy”的函数中获得一个对象,当你使用结束时就必须释放对象,否则会导致内存泄漏。
  • 相反,如果使用不含有”Create”或“Copy”的函数获取一个对象,你将不会拥有对象的引用,不需要释放它。
  • 如果你尚未拥有该对象同时打算持有它的引用,则必须retain它并且在不需要时release掉。可以使用Quartz 2D的特有函数来指定retain和release该对象。例如,如果创建了一个CGColorspace对象,则使用函数CG_EXTERN CGColorSpaceRef cg_nullable CGColorSpaceRetain(CGColorSpaceRef cg_nullable space)CG_EXTERN void CGColorSpaceRelease(CGColorSpaceRef cg_nullable space)来retain和release对象。同样,可以使用Core Foundation的CFTypeRef CFRetain(CFTypeRef cf);void CFRelease(CFTypeRef cf);,但是注意不能传递NULL值给这些函数。

三、Graphics Contexts(图形上下文)

 一个Graphics Context表示一个绘制目标。它包含绘制系统用于完成绘制指令的绘制参数和设备相关信息。Graphics Context定义了基本的绘制属性,如颜色、裁减区域、线条宽度和样式信息、字体信息、混合模式等。
 我们可以通过几种方式来获取Graphics Context:Quartz提供的创建函数、Mac OS X框架或IOS的UIKit框架提供的函数。Quartz提供了多种Graphics Context的创建函数,包括bitmap和PDF,我们可以使用这些Graphics Context创建自定义的内容。
 本节主要介绍如何为不同的绘制目标创建Graphics Context。在代码中,我们用数据类型 — CGContextRef 来表示一个Graphics Context。当获得一个Graphics Context后,可以使用Quartz 2D函数在上下文(context)中进行绘制、完成操作(如平移)、修改图形状态参数(如线宽和填充颜色)等。

1、Drawing to a View Graphics Context in iOS

 在iOS应用程序中,如果要在屏幕上进行绘制,需要创建一个UIView对象,并实现它的- (void)drawRect:(CGRect)rect:方法。

- (void)drawRect:(CGRect)rect:方法调用的时机与使用注意:

  • drawRect:在UIViewController的loadViewviewDidLoad两方法之后调用;
  • 当view需要被刷新或者重绘,drawRect:方法就会被调用,但是如果在UIView初始化时没有设置frame,drawRect:将不会被自动调用!!!
  • 重绘时应该调用setNeedsDisplay方法,而不能直接调用drawRect:方法,setNeedsDisplay会自动调用drawRect:,如果设置UIView的contentMode属性值为UIViewContentModeRedraw,那么将在每次更改frame时自动调用drawRect:方法;
  • 当重写drawRect:方法后,作为配置的一部分,系统视图对象将为当前的绘图环境创建一个Graphics Context。我们可以通过调用UIGraphicsGetCurrentContext()函数来获取这个Graphics Context。
2、Creating a PDF Graphics Context

  当创建一个PDF Graphics Context并绘制时,Quartz将绘制操作记录为一系列的PDF绘制命令并写入文件中。我们需要提供一个PDF输出的位置及一个默认的media box(用于指定页面边界的长方形)。

Quartz 2D API提供了两个函数来创建PDF Graphics Context。

  • CG_EXTERN CGContextRef __nullable CGPDFContextCreateWithURL(CFURLRef cg_nullable url, const CGRect * __nullable mediaBox, CFDictionaryRef __nullable auxiliaryInfo)
  • CG_EXTERN CGContextRef __nullable CGPDFContextCreate(CGDataConsumerRef cg_nullable consumer, const CGRect *__nullable mediaBox, CFDictionaryRef __nullable auxiliaryInfo)

PS:示例代码

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor whiteColor];
    // create PDF
    NSArray * paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
    NSString * documentPath = paths[0];
    NSString * filePath = [documentPath stringByAppendingString:@"text_1.pdf"];
    NSLog(@"%@", filePath);
    const char * cFilePath = [filePath UTF8String];
    CFStringRef pathRef = CFStringCreateWithCString(kCFAllocatorDefault, cFilePath, kCFStringEncodingUTF8);
    CFURLRef urlRef = CFURLCreateWithFileSystemPath(NULL, pathRef, kCFURLPOSIXPathStyle, 0);
    // 凡是使用包含Create或者Copy字符的函数名创建的对象,在使用完后一定要释放!
    CFRelease(pathRef);
    // 这本字典包含额外的选项,主要用于签署的PDF
    CFMutableDictionaryRef muDicRef = CFDictionaryCreateMutable(NULL, 0, &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks);
    CFDictionarySetValue(muDicRef, kCGPDFContextTitle, CFSTR("My Text_1 PDF File"));
    CFDictionarySetValue(muDicRef, kCGPDFContextAuthor, CFSTR("xun mi"));
    CGRect mediaBoxRect = self.view.frame;
    CGContextRef pdfContextRef = CGPDFContextCreateWithURL(urlRef, &mediaBoxRect, muDicRef);
#pragma -------------> 另一种方法创建 PDF context
//    CGDataConsumerRef dataConsumer = CGDataConsumerCreateWithURL(urlRef);
//    CGContextRef pdfContextRef = CGPDFContextCreate(dataConsumer, &mediaBoxRect, muDicRef);
    CFRelease(urlRef);
    CFRelease(muDicRef);
#pragma -------------> 至此PDF context 创建完毕,下面就可以 drawing
    // 创建页面
    CGContextBeginPage(pdfContextRef, &mediaBoxRect);
    CGContextSetRGBFillColor(pdfContextRef, 1, 0, 0, 1);
    CGContextFillRect(pdfContextRef, CGRectMake(50, 200, 100, 100));
    CGContextSetRGBStrokeColor(pdfContextRef, 30.0/255, 79.0/255, 233.0/255, 1);
    CGContextStrokeRect(pdfContextRef, CGRectMake(75, 100, 50, 300));
    // 使某一区域可以响应超链接
    CFURLRef url = CFURLCreateWithString(NULL, CFSTR("http://www.baidu.com"), NULL);
    CGContextSetRGBFillColor (pdfContextRef, 0, 0, 1, 0.5);
    CGContextFillRect (pdfContextRef, CGRectMake (200, 200, 100, 200 ));
    CGPDFContextSetURLForRect(pdfContextRef, url, CGRectMake (200, 200, 100, 200 ));
    CFRelease(url);
    CGPDFContextEndPage(pdfContextRef);
    CGContextRelease(pdfContextRef);
}

生成的PDF文件,其中蓝色区域可以响应百度的链接。 3、Creating a Bitmap Graphics Context

 一个Bitmap Graphics Context接受一个指向内存缓存(包含位图存储空间)的指针,当你在一个Bitmap Graphics Context中绘制时,该缓存被更新。在释放Graphics Context后,你将得到一个我们指定像素格式的全新的Bitmap。
 在iOS应用程序一般使用UIKIT_EXTERN void UIGraphicsBeginImageContextWithOptions(CGSize size, BOOL opaque, CGFloat scale)而不是使用低等级的Quartz函数。如果使用Quartz创建一个后台bitmap,bitmap Graphics Context使用的坐标系统是Quartz默认的坐标系统。而使用UIGraphicsBeginImageContextWithOptions创建图形上下文,将会与UIKit坐标系统相匹配。这可以使你的应用程序使用相同的绘制代码而不需要担心坐标系统问题。虽然我们的应用程序可以手动调整CTM达到相同的效果,但这种做没有任何好处。
PS:示例代码

- (void)drawRect:(CGRect)rect {
// 这里最终绘制在了UIGraphicsGetCurrentContext()获取的上下文中,所以最终显示的坐标系与UIKit相同。
    CGContextRef currentContextRef = UIGraphicsGetCurrentContext();
    CGContextRef    context = NULL;
    CGColorSpaceRef colorSpace;
    void *          bitmapData;//一个指向内存目标的指针,该内存用于存储需要渲染的图形数据。内存块的大小至少需要(bytePerRow * height)字节
    int             bitmapByteCount; // 渲染图像所需总共的字节数
    int             bitmapBytesPerRow;
    CGFloat pixelsWide = 200;//指定位图的宽度,单位是像素(pixel)
    CGFloat pixelsHigh = 400;//指定位图的高度,单位是像素(pixel)
    bitmapBytesPerRow   = (pixelsWide * 4);//指定位图每行的字节数
    bitmapByteCount     = (bitmapBytesPerRow * pixelsHigh);
    colorSpace = CGColorSpaceCreateWithName(kCGColorSpaceGenericRGB);// 颜色空间
    bitmapData = calloc(bitmapByteCount, sizeof(uint8_t));
    if (bitmapData == NULL){
        fprintf (stderr, "Memory not allocated!");
    }else{
        context = CGBitmapContextCreate (bitmapData, pixelsWide, pixelsHigh, 8, bitmapBytesPerRow, colorSpace, kCGImageAlphaPremultipliedLast);
//        context =  CGBitmapContextCreateWithData(bitmapData, pixelsWide, pixelsHigh, 8, bitmapBytesPerRow, colorSpace, kCGImageAlphaPremultipliedLast, NULL, NULL);
        if (context== NULL){
            free (bitmapData);
            fprintf (stderr, "Context not created!");
        }
    }
    CGColorSpaceRelease(colorSpace);
    CGRect myBoundingBox = self.frame;
    CGContextSetRGBFillColor (context, 1, 0, 0, 1);
    CGContextFillRect (context, CGRectMake (0, 0, 200, 100 ));
    CGContextSetRGBFillColor (context, 0, 0, 1, 0.5);
    CGContextFillRect (context, CGRectMake (0, 0, 100, 200 ));
    CGImageRef myImage = CGBitmapContextCreateImage (context);
    CGContextDrawImage(currentContextRef, myBoundingBox, myImage);
    char *bitmapData_char = CGBitmapContextGetData(context);
    CGContextRelease (context);
    if (bitmapData_char) free(bitmapData_char);
    CGImageRelease(myImage);
}
    TextView_1 * view_c = [[TextView_1 alloc] initWithFrame:CGRectMake(0, 100, 100, 200)];
    [self.view addSubview:view_c];

使用UIGraphicsBeginImageContextWithOptions() 创建:

    UIGraphicsBeginImageContextWithOptions(CGSizeMake(100, 100), NO, 1.0);
    CGContextRef currentContext = UIGraphicsGetCurrentContext();
    UIBezierPath * bezierPath = [UIBezierPath bezierPath];
    [[UIColor redColor] set];
    [bezierPath moveToPoint:CGPointMake(0, 100)];
    [bezierPath addQuadCurveToPoint:CGPointMake(100, 100) controlPoint:CGPointMake(50, 0)];
    [bezierPath closePath];
    CGContextAddPath(currentContext, bezierPath.CGPath);
    CGContextDrawPath(currentContext, kCGPathStroke);
    UIImage * image = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    UIImageView * imageView = [[UIImageView alloc] initWithFrame:CGRectMake(150, 100, 100, 100)];
    imageView.image = image;
    imageView.layer.borderWidth = 1.0;
    imageView.layer.borderColor = [UIColor blackColor].CGColor;
    [self.view addSubview:imageView];

发表在 其他 • Tags: , ↑Top 文章来源