由于最近产品要求能够将网页转换为一整张图片,预估整个网页的高度大概有10多个屏幕的高度,因此需要添加一个网页转换为图片的功能。
如果是外部网页,可以用 Safari 浏览器的截长屏功能(需要iOS 13.0+)。但是,这次需要截取的内容是通过 APP 内的 UIWebView 显示的 HTML 网页,这就无法通过 iOS 自带的截屏功能实现了(都9012年了,iOS 怎么还没有截长屏功能?)。
绘制上下文生成图片,可以通过 UIGraphics 或者 CoreGraphics 实现。这两种方案的本质都是对 UIScrollView 进行截图操作,因此可以将这个实现方案类推至 UITableView 、 UICollectionView 等基于 UIScrollView 的控件中。
UIGraphics 实现起来比较简单,但是需要注意运行内存的释放问题。
CoreGraphics 速度快,占用空间手动释放,还可以使用异步实现,但是要花时间理解 CoreGraphics 的相关函数,需要对 CoreGraphics 绘制的图片进行翻转(这就涉及到 CoreGraphics 坐标系的问题)。
这两种方案中,更推荐使用 CoreGraphics 来实现截屏功能。
另外,在使用这两种方案的调试过程中,一定要时刻关注运行内存的变化。使用 iPhone 6 测试,当高度达到 20000 + 的时候,内存都会达到 450M 左右(上限 650M)。
@interface ViewController ()
@property(nonatomic, strong) UIWebView *webView;
@end
- (void)viewDidLoad {
[super viewDidLoad];
_webView = [[UIWebView alloc] initWithFrame:self.view.bounds];
// 百度的首页可以一直往下拉,刚好可以用来测试截长屏的功能
[_webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"http://www.baidu.com"]]];
[self.view addSubview:_webView];
}
/// 使用UIGraphics截取图片
- (UIImage *)getImageByUIGraphic {
// 为了防止内存不能被及时释放,导致系统内存告急,因此在外面加一层自动释放池
@autoreleasepool {
// 设置截图大小
UIScrollView *scrollView = self.webView.scrollView;
// 保存当前的偏移量
CGPoint previousContentOffset = scrollView.contentOffset;
CGRect previousFrame = scrollView.frame;
// 将偏移量设置为(0,0)
scrollView.contentOffset = CGPointZero;
scrollView.frame = CGRectMake(0, 0, scrollView.contentSize.width, scrollView.contentSize.height);
NSLog(@"-------- %f, %f", scrollView.frame.size.height, scrollView.contentSize.height);
// ---------- start -------------
// 指定大小、透明度和缩放
UIGraphicsBeginImageContextWithOptions(scrollView.contentSize, YES, 0);
// 在当前上下文中渲染出整个内容
[scrollView.layer renderInContext:UIGraphicsGetCurrentContext()];
// 截取当前上下文,生成Image
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
// 关闭图形上下文
UIGraphicsEndImageContext();
// ---------- end --------------
// 恢复偏移量
scrollView.contentOffset = previousContentOffset;
scrollView.frame = previousFrame;
return image;
}
}
/// 使用CoreGraphics截取图片
- (UIImage *)getImageByCoreGraphics {
// 设置截图大小
UIScrollView *scrollView = self.webView.scrollView;
// 保存当前的偏移量
CGPoint previousContentOffset = scrollView.contentOffset;
CGRect previousFrame = scrollView.frame;
// 将偏移量设置为(0,0)
scrollView.contentOffset = CGPointZero;
scrollView.frame = CGRectMake(0, 0, scrollView.contentSize.width, scrollView.contentSize.height);
// ---------- start -------------
// 申请绘制空间
unsigned char *imageBuffer = (unsigned char *)malloc(4 * scrollView.contentSize.width * scrollView.contentSize.height);
// 绘制上下文
CGContextRef imageContext = CGBitmapContextCreate(NULL, scrollView.contentSize.width, scrollView.contentSize.height, 8, scrollView.contentSize.width * 4, CGColorSpaceCreateDeviceRGB(), kCGImageAlphaPremultipliedLast);
// 将CoreGraphics转换成UIKit的坐标系,先向下移动整张图片的高度,然后垂直翻转
CGContextTranslateCTM(imageContext, 0.0f, scrollView.contentSize.height);
CGContextScaleCTM(imageContext, 1.0f, -1.0f);
// 渲染全部内容
[scrollView.layer renderInContext:imageContext];
// 生成图片
CGImageRef imageRef = CGBitmapContextCreateImage(imageContext);
// 转换为UIImage
UIImage *image = [UIImage imageWithCGImage:imageRef];
// 释放申请的空间
CGImageRelease(imageRef);
CGContextRelease(imageContext);
free(imageBuffer);
// ---------- end --------------
// 恢复偏移量
scrollView.contentOffset = previousContentOffset;
scrollView.frame = previousFrame;
return image;
}
在实现这个功能之前,需要在info.plist中添加“Privacy - Photo Library Usage Description”,否则会造成 APP 崩溃。
// 记得添加头文件
// #import
/// 保存图片到照片库
- (void)saveImage:(UIImage *)image {
NSLog(@"saveImage: %@",image);
[[PHPhotoLibrary sharedPhotoLibrary] performChanges:^{
[PHAssetChangeRequest creationRequestForAssetFromImage:image];
} completionHandler:^(BOOL success, NSError * _Nullable error) {
if (error) {
NSLog(@"保存失败: %@", error);
} else {
NSLog(@"保存成功");
}
}];
}
- (IBAction)buttonEvent:(id)sender {
//UIImage *image = [self getImageByUIGraphic];
UIImage *image = [self getImageByCoreGraphics];
if (!image) {
NSLog(@"image is nil");
return;
}
// 校验权限,保存图片
PHAuthorizationStatus photoLibraryStatus = [PHPhotoLibrary authorizationStatus];
if (photoLibraryStatus != PHAuthorizationStatusAuthorized) {
// 权限不足
[PHPhotoLibrary requestAuthorization:^(PHAuthorizationStatus status) {
if (status != PHAuthorizationStatusAuthorized) {
// 用户仍然不同意程序访问相册
NSLog(@"权限不足");
} else {
[self saveImage:image];
}
}];
} else {
[self saveImage:image];
}
}
在测试的过程中,发现有几次截取的图片最下面的部分为空白。经过反复测试,发现当百度网页中顶部的导航栏(包含“推荐”、“视频”、“娱乐”等选项的横向导航栏)消失的时候(即处于网页的顶部一定区域内),截图的下半部分全是空白,造成这个现象的具体原因尚未弄清。
下面比较一下几种方式生成的效果图。图(1)是通过 UIGraphic 方法绘制的图片,图(2)是通过 CoreGraphics 方法绘制的图片,图(3)是使用 Safari 浏览器中整页截图生成的 PDF ,转换而成的图片。