【iOS】Quartz 2D之图片添加水印、裁剪、截屏

图片添加水印和截屏功能都是项目中经常用到的。微博的图片默认会加上一个自己id水印,虽然大部分情况下都是由后端加上的,但也有一些场景是需要App自己添加上去的。苹果手机截图是Home键+关机键同时按下,截取的是整个屏幕,能否让用户截取部分呢?

一、图片添加水印

水印一般是文字或图标,它的作用就是告诉用户图片从哪个地方来的(即版权)。图片上加水印的做法很简单,就是图片和水印在屏幕上渲染后,把上下文中所有内容合成生成一张新的图片。

1.1. 位图上下文

如果要生成一张图片,也会用到一个上下文。但和之前的图形上下文不一样,这里用到的是位图上下文,并且是需要手动开启的。开启位图上下文并指定尺寸,尺寸决定着生成的图片尺寸。

注意:手动开启的上下文,一定要手动关闭。

流程:

  • 手动开启一个位图上下文;
  • 把内容绘制到上下文;
  • 从上下文中生成一张新的图片(新的图片尺寸和上下文尺寸一样)
  • 关闭上下文

1.2. 示例

场景:图片上添加一个文字水印。

ViewController.m

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@interface ViewController ()

@property (weak, nonatomic) IBOutlet UIImageView *imageView;

@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];

UIImage *image = [UIImage imageNamed:@"avatar"];
// 1.手动开启一个位图上下文
UIGraphicsBeginImageContext(image.size);
// 2.把内容绘制到上下文
[image drawAtPoint:CGPointZero];
// 3.添加水印
NSString *watermark = @"@idbeny";
NSDictionary *watermarkAttriDict = @{
NSFontAttributeName : [UIFont systemFontOfSize:20.0],
NSForegroundColorAttributeName : [UIColor whiteColor]
};
CGSize watermark_size = [watermark boundingRectWithSize:CGSizeMake(image.size.width, image.size.height) options:NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading attributes:watermarkAttriDict context:nil].size;
[watermark drawInRect:CGRectMake(image.size.width-watermark_size.width, image.size.height-watermark_size.height, watermark_size.width, watermark_size.height) withAttributes:watermarkAttriDict];
// 4.从当前上下文生成一张新的图片
UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext();
// 5.关闭上下文
UIGraphicsEndImageContext();

self.imageView.image = newImage;
}

@end

效果:

开启上下文除了上面示例提到的一种方法,还有另外一种方法。

1
2
3
4
5
6
7
/*
* 开启上下文
* @param size 上下文大小
* @param opaque 不透明度
* @param scale 缩放比例
*/
void UIGraphicsBeginImageContextWithOptions(CGSize size, BOOL opaque, CGFloat scale);

opaque是不透明度,YES代表不透明,NO代表透明。

scale默认是0.0,但指的是跟随设备主屏幕的scale,即0.0 == [UIScreen mainScreen].scale

二、裁剪

图片的裁剪和生成图片类似。裁剪图片的时候应该先设置裁剪区域,再绘制图片,否则图片已经画上去就不能裁剪了。

2.1. 裁剪圆形图片

ViewController.m

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@interface ViewController ()

@property (weak, nonatomic) IBOutlet UIImageView *imageView;

@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];

UIImage *image = [UIImage imageNamed:@"avatar"];

// 1.开启位图上下文
UIGraphicsBeginImageContext(image.size);
// 2.绘制圆形路径
UIBezierPath *path = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(0.0, 0.0, image.size.width, image.size.height)];
// 3.把圆形路径设置为裁剪区域
[path addClip];
// 4.图片绘制到上下文
[image drawAtPoint:CGPointZero];
// 5.生成新图片
UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext();
// 6.关闭上下文
UIGraphicsEndImageContext();

self.imageView.image = newImage;
}

@end

效果:

注意:addClip仅对后面添加的内容有效,已经绘制到上下文中的内容不会被裁减。

2.2. 裁剪有边框的图片

带边框的图片本质就是背景一个有颜色的实心圆(大圆),再画一个裁剪路径(小圆),把图片绘制到上下文,最后生成的图片就是带边框的圆形裁剪图片。

ViewController.m

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@interface ViewController ()

@property (weak, nonatomic) IBOutlet UIImageView *imageView;

@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];

UIImage *image = [UIImage imageNamed:@"avatar"];

// 1.开启上下文
UIGraphicsBeginImageContext(image.size);
// 2.设置颜色填充的圆形
// 2.1 边框宽度
CGFloat border_w = 10.0;
UIBezierPath *borderPath = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(0.0, 0.0, image.size.width, image.size.height)];
// 2.2 边框颜色
[[UIColor yellowColor] set];
// 2.3 填充路径
[borderPath fill];
// 3.设置裁剪区域
UIBezierPath *clipPath = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(border_w, border_w, image.size.width-border_w*2, image.size.height-border_w*2)];
[clipPath addClip];
// 4.绘制图片
[image drawAtPoint:CGPointZero];
// 5.生成新图
UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext();
// 6.关闭上下文
UIGraphicsEndImageContext();

self.imageView.image = newImage;
}

@end

效果:

三、截屏

view上面的元素之所以能够显示出来,主要是layer层。所以如果想要截屏,就把layer层上面的内容渲染到上下文中,然后从上下文中获取图片(即生成一张图片)即可。

关键代码:[layer renderInContext:ctx]

示例:

场景:把上面的控制器截屏生成一张图片。

ViewController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (void)viewDidLoad {
[super viewDidLoad];
// 1.开启上下文
UIGraphicsBeginImageContextWithOptions(self.view.bounds.size, YES, 0.0);
// 2.获取上下文(此时的上下文是UIGraphicsBeginImageContext)
CGContextRef ctx = UIGraphicsGetCurrentContext();
// 3.把当控制球view的layer所有内容渲染到上下文
[self.view.layer renderInContext:ctx];
// 4.生成图片
UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext();

// 把图片保存到桌面,看下效果(注意:真机只能操作沙盒)
NSData *data = UIImagePNGRepresentation(newImage);
[data writeToFile:@"/Users/Developer/Desktop/screen_layer.png" atomically:YES];
}

效果(到桌面查看):

注意:UIGraphicsBeginImageContextWithOptionsUIGraphicsBeginImageContext的区别使用,在截屏的时候建议使用前者。

四、图片自定义区域裁剪和清除

利用手势可以保留区域内的图片,也可以利用手势删除图片一部分内容。

4.1. 自定义区域裁剪

思路:
位图上下文大小是整个imageView控件的大小,裁剪区域设为手势滑动过的遮罩层,再把imageView上的所有内容都渲染到当前位图上下文中,然后再从上下文获取最新图片就行了。

UI构建:

示例:
ViewController.m

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
@interface ViewController ()

@property (weak, nonatomic) IBOutlet UIImageView *imageView;
@property (weak, nonatomic) UIView *maskView; // 遮罩
@property (nonatomic) CGPoint start_point; // 开始触摸点

@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];
UIPanGestureRecognizer *panGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(panGestureActionHandle:)];
[self.imageView addGestureRecognizer:panGesture];
}

// 滑动手势处理
- (void)panGestureActionHandle:(UIPanGestureRecognizer *)gesture {
// 获取当前手指触摸点
CGPoint current_point = [gesture locationInView:gesture.view];
if (gesture.state == UIGestureRecognizerStateBegan) { // 滑动开始
self.start_point = current_point;
} else if (gesture.state == UIGestureRecognizerStateChanged) { // 滑动值改变
// 计算遮罩的位置和尺寸
CGPoint origin = self.start_point;
CGFloat width = current_point.x - self.start_point.x;
CGFloat height = current_point.y - self.start_point.y;
CGRect frame;
frame.origin = origin;
frame.size = CGSizeMake(width, height);
self.maskView.frame = frame;
} else { // 取消(滑动停止)
// 1.开启位图上下文
UIGraphicsBeginImageContextWithOptions(self.imageView.bounds.size, NO, 0.0);
// 2.裁剪
UIBezierPath *clipPath = [UIBezierPath bezierPathWithRect:self.maskView.frame];
[clipPath addClip];
// 裁剪还可以这样
// UIRectClip(self.maskView.frame);
// 3.图片渲染到上下文
CGContextRef ctx = UIGraphicsGetCurrentContext();
[self.imageView.layer renderInContext:ctx];
// 4.从位图上下文获取最新图片
UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext();
// 5.关闭上下文
UIGraphicsEndImageContext();

self.imageView.image = newImage;
[self.maskView removeFromSuperview];
}
}

- (UIView *)maskView {
if (!_maskView) {
UIView *maskView = [[UIView alloc] init];
maskView.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.4];
[self.view addSubview:maskView];
_maskView = maskView;
}
return _maskView;
}

@end

效果:

4.2. 自定义区域清除

思路:
和裁剪类似,当imageView渲染到上下文后,按照指定区域把上下文一部分清除就行了。

UI构建:和4.1一样

关键代码:CGContextClearRect(ctx, self.maskView.frame);

示例:
ViewController.m

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
@interface ViewController ()
{
CGFloat _maskView_w;
CGFloat _maskView_h;
}
@property (weak, nonatomic) IBOutlet UIImageView *imageView;
@property (weak, nonatomic) UIView *maskView; // 遮罩
@property (nonatomic) CGPoint start_point; // 开始触摸点

@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];
UIPanGestureRecognizer *panGesture = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(panGestureActionHandle:)];
[self.imageView addGestureRecognizer:panGesture];

_maskView_w = 20.0;
_maskView_h = 20.0;
}

// 滑动手势处理
- (void)panGestureActionHandle:(UIPanGestureRecognizer *)gesture {
// 1.获取当前手指触摸点
CGPoint current_point = [gesture locationInView:gesture.view];
// 2.设置清除遮罩的位置和尺寸(在触摸点的中心)
self.maskView.frame = CGRectMake(current_point.x - _maskView_w * 0.5, current_point.y - _maskView_h * 0.5, _maskView_w, _maskView_h);
// 3.开启上下文
UIGraphicsBeginImageContextWithOptions(self.imageView.bounds.size, NO, 0.0);
// 4.当前imageView所有内容渲染到位图上下文中
CGContextRef ctx = UIGraphicsGetCurrentContext();
[self.imageView.layer renderInContext:ctx];
// 5.裁剪上下文maskView所在的区域
CGContextClearRect(ctx, self.maskView.frame);
// 6.获取最新图片
UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext();
// 7.关闭上下文
UIGraphicsEndImageContext();

self.imageView.image = newImage;
}

- (UIView *)maskView {
if (!_maskView) {
UIView *maskView = [[UIView alloc] init];
maskView.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.4];
[self.view addSubview:maskView];
_maskView = maskView;
}
return _maskView;
}

@end

效果:

注意:上面的这种擦除方式仅仅作为对比和了解,实际开发过程中会使用贝塞尔曲线绘制路径,等待全部接收后再生成图片。