如何打印所有 WKWebView On AND Offscreen 内容 OSX 和 iOS
How does one Print all WKWebView On AND Offscreen content OSX and iOS
这个问题是关于打印 WKWebView 的所有内容(包括屏幕外内容)。 目前(仍然是 iOS 10.2 或 OSX 10.12 ) 没有工作解决方案和 none 的 Whosebug 工作假设解决方案。仅当您自己验证可以打印屏幕外内容时,才在此处提供答案,如果确实如此,则提供工作示例代码。
我正在尝试打印 ALL WKWebView 或 WebView OSX 10.10 或更高版本(目前 运行 10.11.2)。例如,宽 html table 中的列在视图之外并偏向右侧。 OSX 的早期版本会自动分页并正确打印所有 html.
我已经尝试使用 Whosebug 和其他地方提供的解决方案。所有人基本上都在说同样的事情,就是像这样打印 documentView
:
[[NSPrintOperation printOperationWithView:_webView.mainFrame.frameView.documentView printInfo:pInfo] runOperation];
这在 10.10 中对 WKWebView 或 WebView 停止工作。如果你这样做:
[[NSPrintOperation printOperationWithView:_wkWebView printInfo:pInfo] runOperation];
您得到分页,但打印输出包含滚动条 WebView
,而另一个 WKWebView
为您提供空白页。
我在 Apple 文档中找不到任何关于在 OSX 上打印 WKWebView 的内容。我也找不到任何特定于 OSX 而不是 iOS.
的答案
有人知道如何在 OSX 上打印这些吗?
更新:这是 WebView [Radar:23159060](仍然开放 2/2018) 和 WKWebView 甚至没有似乎解决了 OSX 上的打印问题。在网上查看此 class 的开源后,我看到 all 的 classes与打印有关的任何内容都在仅支持平台的条件编译块中:iOS.
更新第二部分:令人惊讶的是,这个荒谬的错误存在于这个 class 的所有实现中,包括 iOS 上的实现!我觉得这很荒谬 still 尽管文档声明使用这个(而且只有这个 class ) 在支持 iOS 8 或更高版本的应用程序中。现在不可能在 iOS 或 OSX 上打印 WebView 的所有屏幕上和 屏幕外 内容.失败的苹果。是时候解决这个问题了!我们都知道史蒂夫会怎么说....
更新第三部分 :)- 更令人惊讶的是,这个问题从 10.15.2 开始并没有得到解决,并且在 4 年多的时间里出现!!!这个问题一直存在(Apple waaaaaake up up up up up ....)。考虑到他们在 iOS 土地上使用 WKWebView 变得非常咄咄逼人,甚至拒绝不使用的应用程序(除非你试图支持 iOS 7),这真是令人惊讶。
更新第四部分(2020 年......你能相信吗?!?):在大苏尔,这仍然是一个问题。我通过编写解决方法解决了这个问题,请参阅下面接受的答案:
printOperationWithPrintInfo:
不打印屏幕外或在水平或垂直方向上滚出视图的所有内容。但是,它确实使用了您的 Print CSS ,这比:
略有优势
- (void)takeSnapshotWithConfiguration:(WKSnapshotConfiguration *)snapshotConfiguration
completionHandler:(void (^)(NSImage *snapshotImage, NSError *error))completionHandler;
为了让它工作,我做了:
NSPrintInfo *pInfo = [[NSPrintInfo alloc] initWithDictionary:printInfoDict];
pInfo.horizontalPagination = NSPrintingPaginationModeAutomatic;
pInfo.verticalPagination = NSPrintingPaginationModeAutomatic;
pInfo.verticallyCentered = YES;
pInfo.horizontallyCentered = YES;
pInfo.orientation = NSPaperOrientationLandscape;
pInfo.leftMargin = 30;
pInfo.rightMargin = 30;
pInfo.topMargin = 30;
pInfo.bottomMargin = 30;
NSPrintOperation *po = [_webView printOperationWithPrintInfo:pInfo];
po.showsPrintPanel = YES;
po.showsProgressPanel = YES;
// Without the next line you get an exception. Also it seems to
// completely ignore the values in the rect. I tried changing them
// in both x and y direction to include content scrolled off screen.
// It had no effect whatsoever in either direction.
po.view.frame = _webView.bounds;
// [printOperation runOperation] DOES NOT WORK WITH WKWEBVIEW, use
[po runOperationModalForWindow:self.view.window delegate:self didRunSelector:@selector(printOperationDidRun:success:contextInfo:) contextInfo:nil];
**还有一些我不完全理解的事情。 wkWebView 的大小似乎无关紧要。如果我调整应用程序的大小以隐藏某些内容,它似乎仍然会在屏幕外抓取尽可能多的内容以适应指定的页面,但它似乎不知道如何对不适合页面大小的内容进行分页到其他页面。所以这似乎是问题所在。可能有一些方法可以解决这个问题,如果有人有线索 post 就在这里!!
在 iOS 中,我将 UIView.viewPrintFormatter()
传递给 UIActivityViewController
以允许用户打印所有内容
let myShare = [webView.viewPrintFormatter()]
let avc = UIActivityViewController(activityItems: myShare, applicationActivities: nil)
present(avc, animated: true, completion: nil)
我用尽了所有可能的方法来直接打印 WKWebView,但没有成功。我能想到的唯一解决方法是将网页转换为 PDF 对象,然后打印该对象。如果解决方法有效,我将更新代码。
这不是正确答案,因为 Apple 需要使用有效的打印方法或 .evaluateJavaScript("window.print()", completionHandler: nil)
提供正确答案
但我有一个愚蠢的解决方案 "works" 对我来说可能会帮助其他人解决这个问题。
第 1 步:抓住 HTML 并修复 <body> tag with <body onload='window.print()'>
。如果您从某个地方获取 html 而不是加载您自己的,您将需要使用一些正则表达式。这个我就不说了。
第 2 步:将 html 保存在某个文件中,并将完整路径保存在变量中。在我的示例中:文件名
第 3 步:将您的打印按钮连接到此代码:
RunCommand(command: "/usr/bin/open \(filename)")
请参阅下面的 RunCommand 代码。这留下了一个愚蠢的 safari window,但它可以在不保存到文件的情况下打印一些东西,然后记住你把它贴在哪里,这样你就可以自己用 Safari 打开它来进行打印。
func RunCommand(command: String) -> (success: Bool, result: String) {
let cc = command.components(separatedBy: " ")
let process = Process()
process.launchPath = cc[0]
var cp: [String] = []
for i in (1..<cc.count) {
cp.append(cc[i])
}
process.arguments = cp
let pipe = Pipe()
process.standardOutput = pipe
process.launch()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
if (data.count > 0) {
let output = String(data: data, encoding: String.Encoding.utf8)
// let output = NSString(data: data, encoding: String.Encoding.utf8.rawValue)
return (success: true, result: output!)
}
return (success: true, result: "")
}
如果 Apple 能够解决这个问题,那就太好了。我有一个内部应用程序,可以在 HTML 中完成所有报告,并且报告是您想要打印的内容。
我已经成功使用 SPI -[WKWebView _printOperationWithPrintInfo:] 通过了通常的 [NSPrintInfo sharedPrintInfo]。
请注意,您不能对返回的 NSPrintOperation 使用 -runOperation。您必须 使用 -runOperationModalForWindow:.... 这非常相似。问题出在 WebKit 内部,它需要一个 运行 runloop 和内部预览来了解页数。
它绝对适用于屏幕外内容,如果屏幕外的意思是“未完全显示在屏幕上”。我仍然有一个 WKWebView 显示在 window 中,但它非常小,只显示整个 webview 内容的一小部分(21 个 A4 页!)。希望这对您有所帮助!
PS:在 10.12、10.14 和 10.15 上测试。
代码是这样的:
SEL printSelector = NSSelectorFromString(@"_printOperationWithPrintInfo:"); // This is SPI on WKWebView. Apparently existing since 10.11 ?
NSMutableDictionary *printInfoDict = [[[NSPrintInfo sharedPrintInfo] dictionary] mutableCopy];
printInfoDict[NSPrintJobDisposition] = NSPrintSaveJob; // means you want a PDF file, not printing to a real printer.
printInfoDict[NSPrintJobSavingURL] = [NSURL fileURLWithPath:[@"~/Desktop/wkwebview_print_test.pdf" stringByExpandingTildeInPath]]; // path of the generated pdf file
printInfoDict[NSPrintDetailedErrorReporting] = @YES; // not necessary
// customize the layout of the "printing"
NSPrintInfo *customPrintInfo = [[NSPrintInfo alloc] initWithDictionary:printInfoDict];
[customPrintInfo setHorizontalPagination: NSPrintingPaginationModeAutomatic];
[customPrintInfo setVerticalPagination: NSPrintingPaginationModeAutomatic];
[customPrintInfo setVerticallyCentered:NO];
[customPrintInfo setHorizontallyCentered:NO];
customPrintInfo.leftMargin = 0;
customPrintInfo.rightMargin = 0;
customPrintInfo.topMargin = 5;
customPrintInfo.bottomMargin = 5;
NSPrintOperation *printOperation = (NSPrintOperation*) [_webView performSelector:printSelector withObject:customPrintInfo];
[printOperation setShowsPrintPanel:NO];
[printOperation setShowsProgressPanel:NO];
// BOOL printSuccess = [printOperation runOperation]; // THIS DOES NOT WORK WITH WKWEBVIEW! Use runOperationModalForWindow: instead (asynchronous)
[printOperation runOperationModalForWindow:self.window delegate:self didRunSelector:@selector(printOperationDidRun:success:contextInfo:) contextInfo:nil]; // THIS WILL WORK, but is async
5 年后,我设法解决了 原来的问题,这是由于 MacOS 11 实施WKWebView
printOperationWithPrintInfo
仍然无法正确处理滚出视图并向右滚动的内容。
根本问题似乎是剪辑区域边界之外的内容(尤其是右侧)没有得到正确处理。这可能是一个 WKWebView
错误,因为它似乎处理了垂直方向可见矩形下方的一些内容。
经过大量挖掘,发现其他人已经能够通过以下方式获取 NSView
的全部内容进行打印和正确分页:
- 视图分离(不在屏幕上)。
- 正在将框架设置为整个内容的大小。
- 然后在分离视图上调用 printWithPrintInfo。
我有一个解决方案的想法:
- 通过类别扩展
WKWebView
,该类别具有将所有内容作为图像块获取的功能。它通过 JavaScript 在 MacOS 上执行此操作,在 iOS 上通过操作与 WKWebView
关联的 UIScrollView
来获取完整的内容大小,然后将内容的各个部分滚动到可见区域并将其快照为图像块网格。
- 创建
NSView or UIView
的子类,以正确的关系绘制所有图块。
- 在分离视图上调用
printWithPrintInfo
。
在 MacOS 10.14+ iOS 13+
上运行良好
在两个平台上,所有输出都正确分页(iOS 需要使用包含在相关 GitHub 项目中的 UIPrintPageRenderer),您可以在预览中使用打开为 PDF 并保存它作为文件等
我遇到的唯一缺点是未使用 Print CSS,考虑到 Apple 目前对 Print CSS 的支持很少,这并不重要。
所有工作代码都在 GitHub 此处:Full working source for iOS and MacOS
此源已过期 请参阅 Github
Header
//
// WKWebView+UtilityFunctions.h
// Created by Clifford Ribaudo on 12/24/20.
//
#import <WebKit/WebKit.h>
#ifdef _MAC_OS_ // Up to user to determine how they know this
#define IMAGE_OBJ NSImage
#define VIEW_OBJ NSView
#else
#define IMAGE_OBJ UIImage
#define VIEW_OBJ UIView
#endif
@interface TiledImageView : VIEW_OBJ
{
NSArray *_imageTiles;
}
-(void)printWithPrintInfo:(NSPrintInfo *)pi;
-(instancetype)initWithFrame:(CGRect)frame imageTiles:(NSArray<NSArray *> *)imageTiles;
@end
@interface WKWebView (UtilityFunctions)
-(void)HTMLPageMetrics:(void (^)(CGSize htmlDocSize, CGSize visibleSize, NSError *error))completionHandler;
-(void)currentScrollXY:(void (^)(float x, float y, NSError *error))completionHandler;
-(void)scrollHTMLTo:(float)x topY:(float)y completionHandler:(void (^)(NSError *error))completionHandler;
-(void)imageTilesForHTMLPage:(CGSize)pageSize visbleRect:(CGSize)visibleRect imgData:(NSMutableArray<NSArray *> *)tileData completionHandler:(void (^)(NSError *error))completionHandler;
-(void)imageTile:(CGRect)imgRect fromPageOfSize:(CGSize)pageSize inViewOfSize:(CGSize)viewSize completionHandler:(void (^)(IMAGE_OBJ *tileImage, NSError *error))completionHandler;
@end
实施
//
// WKWebView+UtilityFunctions.m
// Created by Clifford Ribaudo on 12/24/20.
//
// Works with MacOS v10.14+ and ??iOS 13+
//
#import "WKWebView+UtilityFunctions.h"
@implementation TiledImageView
-(instancetype)initWithFrame:(CGRect)frame imageTiles:(NSArray<NSArray *> *)imageTiles
{
self = [super initWithFrame:NSRectFromCGRect(frame)];
if(self) {
_imageTiles = imageTiles;
}
return self;
}
-(BOOL)isFlipped {return YES;}
-(void)printWithPrintInfo:(NSPrintInfo *)pi
{
NSPrintOperation *po = [NSPrintOperation printOperationWithView:self];
po.printInfo = pi;
[po runOperation];
}
- (void)drawRect:(NSRect)rect
{
for(NSArray *imgData in _imageTiles)
{
NSRect drawRect = ((NSValue *)imgData[0]).rectValue;
IMAGE_OBJ *img = imgData[1];
[img drawInRect:drawRect];
}
}
@end
@implementation WKWebView (UtilityFunctions)
//
// Returns via Completion Handler:
// htmlDocSize - The size of the entire <HTML> element, visible or not
// visibleSize - The visible dimensions of the page, essentially WKWebView bounds minus HTML scroll bar dimensions
//
-(void)HTMLPageMetrics:(void (^)(CGSize htmlDocSize, CGSize visibleSize, NSError *error))completionHandler
{
//
// Anonymous Function - gets Size of entire HTML element and visible size.
// Result String = Full X, Full Y, Visible X, Visible Y
//
NSString *jsGetPageMetrics = @"(function(){return document.documentElement.scrollWidth + ',' + document.documentElement.scrollHeight + ',' + document.documentElement.clientWidth + ',' +document.documentElement.clientHeight;})();";
// Execute JS in WKWebView
[self evaluateJavaScript:jsGetPageMetrics completionHandler:^(id result, NSError *error)
{
CGSize htmlSize = CGSizeMake(0, 0);
CGSize visibleSize = CGSizeMake(0, 0);
if(!error && result)
{
NSArray<NSString *> *data = [[NSString stringWithFormat:@"%@", result] componentsSeparatedByString:@","];
htmlSize = CGSizeMake([data[0] floatValue], [data[1] floatValue]);
visibleSize = CGSizeMake([data[2] floatValue], [data[3] floatValue]);
}
else
NSLog(@"JS error getting page metrics: %@", error.description);
completionHandler(htmlSize, visibleSize, error);
}];
}
//
// Get <HTML> element current scroll position (x,y) and return to completeion handler:
// x = document.documentElement.scrollLeft
// y = document.documentElement.scrollTop
//
-(void)currentScrollXY:(void (^)(float X, float Y, NSError *error))completionHandler
{
NSString *jsGetPageMetrics = @"(function(){return document.documentElement.scrollLeft + ',' + document.documentElement.scrollTop;})();";
// Execute JS in WKWebView
[self evaluateJavaScript:jsGetPageMetrics completionHandler:^(id result, NSError *error) {
if(!error && result)
{
NSArray<NSString *> *data = [[NSString stringWithFormat:@"%@", result] componentsSeparatedByString:@","];
completionHandler([data[0] floatValue], [data[1] floatValue], error);
}
else {
NSLog(@"JS error getting page metrics: %@", error.localizedDescription);
completionHandler(0, 0, error);
}
}];
}
//
// Scroll the current HTML page to x, y using scrollTo(x,y) on the <HTML> element
// Optional Completion Handler to do something when scroll finished
//
-(void)scrollHTMLTo:(float)x topY:(float)y completionHandler:(void (^)(NSError *error))completionHandler
{
NSString *js = [NSString stringWithFormat:@"document.documentElement.scrollTo(%0.f, %0.f);", x, y];
// Execute JS in WKWebView
[self evaluateJavaScript:js completionHandler:^(id result, NSError *error)
{
dispatch_time_t delay = dispatch_time(DISPATCH_TIME_NOW, .25 * NSEC_PER_SEC);
dispatch_after(delay, dispatch_get_main_queue(), ^{
if(completionHandler) completionHandler(error);
});
if(error) NSLog(@"JS error scrollTo %@", error.localizedDescription);
}];
}
//
// Called Recursively until tiles are obtained for the entire pageRect.
// Tiles are the size of visibleRect (WKWebView.bounts) but can be smaller.
// tileData - Array of arrays holding CGRect & Img.
//
-(void)imageTilesForHTMLPage:(CGSize)pageSize visbleRect:(CGSize)visibleSize imgData:(NSMutableArray<NSArray *> *)tileData completionHandler:(void (^)(NSError *error))completionHandler
{
__block CGRect currentRect; // In coordinates of pageSize (full).
if(tileData.count == 0) { // No image tiles yet. Start at top left of html page for visible WKWebView bounds
currentRect.origin.x = currentRect.origin.y = 0.0;
currentRect.size = visibleSize;
}
else {
NSArray *lastTile = [tileData lastObject]; // Calculate what the next tile rect is or call handler if done.
CGRect lastTileRect;
#ifdef _MAC_OS_
lastTileRect = ((NSValue *)lastTile[0]).rectValue;
#else
lastTileRect = ((NSValue *)lastTile[0]).CGRectValue;
#endif
// Check if anything more to get to right of last tile
if((lastTileRect.origin.x + lastTileRect.size.width) < pageSize.width)
{
currentRect.origin.x = lastTileRect.origin.x + lastTileRect.size.width + 1; // Next x to right of last tile
currentRect.origin.y = lastTileRect.origin.y; // Works on all rows
currentRect.size.height = lastTileRect.size.height;
currentRect.size.width = pageSize.width - currentRect.origin.x; // Get width of next tile to right of last
if(currentRect.size.width > visibleSize.width) // If more tiles to right use visible width
currentRect.size.width = visibleSize.width;
}
else if((lastTileRect.origin.y + lastTileRect.size.height) < pageSize.height) // New Row
{
currentRect.origin.x = 0; // Reset x back to left side of hmtl
currentRect.size.width = visibleSize.width; // Reset width back to view width
currentRect.origin.y = lastTileRect.origin.y + lastTileRect.size.height + 1; // Get y below last row
currentRect.size.height = pageSize.height - currentRect.origin.y;
if(currentRect.size.height > visibleSize.height) // If more rows below use row height
currentRect.size.height = visibleSize.height;
}
else {
completionHandler(nil);
return;
}
}
[self imageTile:currentRect fromPageOfSize:pageSize inViewOfSize:visibleSize completionHandler:^(NSImage *tileImage, NSError *error)
{
if(error || !tileImage) {
NSLog(@"Error getting image tiles %@", error.description);
completionHandler(error);
return;
}
#ifdef _MAC_OS_
[tileData addObject:@[[NSValue valueWithRect:NSRectFromCGRect(currentRect)], tileImage]];
#else
[tileData addObject:@[[NSValue valueWithCGRect:currentRect], tileImage]];
#endif
[self imageTilesForHTMLPage:(CGSize)pageSize visbleRect:(CGSize)visibleSize imgData:(NSMutableArray<NSArray *> *)tileData completionHandler:completionHandler];
}];
}
//
// ImgRect = location of rect in full page size. Has to be translated into what is visible and where.
// pageSize = Full size of HTML page, visible or not.
// viewSize = essentially the wkwebview.bounds.size - HTML scroll bars.
//
-(void)imageTile:(CGRect)imgRect fromPageOfSize:(CGSize)pageSize inViewOfSize:(CGSize)viewSize completionHandler:(void (^)(IMAGE_OBJ *tileImage, NSError *error))completionHandler
{
float x = imgRect.origin.x; // Always do this to make the desired rect visible in the rect of viewSize
float y = imgRect.origin.y;
CGRect rectToGetFromView;
rectToGetFromView.origin.x = 0;
rectToGetFromView.origin.y = 0;
rectToGetFromView.size = imgRect.size;
// If img is smaller than the viewport, determine where it is after scroll
if(imgRect.size.width < viewSize.width)
rectToGetFromView.origin.x = viewSize.width - imgRect.size.width;
if(imgRect.size.height < viewSize.height)
rectToGetFromView.origin.y = viewSize.height - imgRect.size.height;
[self scrollHTMLTo:x topY:y completionHandler:^(NSError *error)
{
if(!error) {
WKSnapshotConfiguration *sc = [WKSnapshotConfiguration new];
sc.rect = rectToGetFromView;
[self takeSnapshotWithConfiguration:sc completionHandler:^(IMAGE_OBJ *img, NSError *error)
{
if(error) NSLog(@"Error snapshotting image tile: %@", error.description);
completionHandler(img, error);
}];
}
else {
NSLog(@"Error scrolling for next image tile %@", error.description);
completionHandler(nil, error);
}
}];
}
@end
用法
在为您的 WKWebView
处理打印的任何内容中使用类别,如下所示:
-(void)print:(id)sender
{
// Set this as per your needs
NSPrintInfo *pInfo = [NSPrintInfo sharedPrintInfo];
pInfo.verticallyCentered = YES;
pInfo.horizontallyCentered = NO;
pInfo.horizontalPagination = NSAutoPagination;
pInfo.verticalPagination = NSAutoPagination;
pInfo.orientation = NSPaperOrientationLandscape;
pInfo.bottomMargin = 30;
pInfo.topMargin = 30;
pInfo.leftMargin = 30;
pInfo.rightMargin = 30;
pInfo.scalingFactor = .60;
[_webView HTMLPageMetrics:^(CGSize htmlSize, CGSize visibleSize, NSError *error)
{
self->_imgTileData = [NSMutableArray new];
[self->_webView imageTilesForHTMLPage:htmlSize visbleRect:visibleSize imgData:self->_imgTileData completionHandler:^(NSError *error) {
if(!error) {
TiledImageView *tiv = [[TiledImageView alloc] initWithFrame:CGRectMake(0,0,htmlSize.width,htmlSize.height) imageTiles:self->_imgTileData];
[tiv printWithPrintInfo:pInfo];
}
}];
}
}
这是作为 Github 要点的代码:Above code
这是我的 swift 版本(针对 OSX 11 更新)@Altimac 的解决方案。
@available(OSX 11.0, *)
extension BrowserViewController
{
func print(pdf: Bool)
{
guard var printInfoDictionary = NSPrintInfo.shared.dictionary().mutableCopy() as? [NSPrintInfo.AttributeKey: Any] else { return }
printInfoDictionary[NSPrintInfo.AttributeKey.jobDisposition] = pdf ? NSPrintInfo.JobDisposition.preview : NSPrintInfo.JobDisposition.spool
printInfoDictionary[NSPrintInfo.AttributeKey.detailedErrorReporting] = true
let printInfo = NSPrintInfo(dictionary: printInfoDictionary)
printInfo.horizontalPagination = .automatic
printInfo.verticalPagination = .automatic
printInfo.isVerticallyCentered = false
printInfo.isHorizontallyCentered = false
printInfo.leftMargin = 10
printInfo.rightMargin = 10
printInfo.topMargin = 10
printInfo.bottomMargin = 10
let printOperation = theWebView.printOperation(with: printInfo)
printOperation.showsPrintPanel = false
printOperation.showsProgressPanel = false
printOperation.runModal(for: self.view.window!, delegate: self, didRun: nil, contextInfo: nil)
}
}
似乎至少对我有用。
这个问题是关于打印 WKWebView 的所有内容(包括屏幕外内容)。 目前(仍然是 iOS 10.2 或 OSX 10.12 ) 没有工作解决方案和 none 的 Whosebug 工作假设解决方案。仅当您自己验证可以打印屏幕外内容时,才在此处提供答案,如果确实如此,则提供工作示例代码。
我正在尝试打印 ALL WKWebView 或 WebView OSX 10.10 或更高版本(目前 运行 10.11.2)。例如,宽 html table 中的列在视图之外并偏向右侧。 OSX 的早期版本会自动分页并正确打印所有 html.
我已经尝试使用 Whosebug 和其他地方提供的解决方案。所有人基本上都在说同样的事情,就是像这样打印 documentView
:
[[NSPrintOperation printOperationWithView:_webView.mainFrame.frameView.documentView printInfo:pInfo] runOperation];
这在 10.10 中对 WKWebView 或 WebView 停止工作。如果你这样做:
[[NSPrintOperation printOperationWithView:_wkWebView printInfo:pInfo] runOperation];
您得到分页,但打印输出包含滚动条 WebView
,而另一个 WKWebView
为您提供空白页。
我在 Apple 文档中找不到任何关于在 OSX 上打印 WKWebView 的内容。我也找不到任何特定于 OSX 而不是 iOS.
的答案有人知道如何在 OSX 上打印这些吗?
更新:这是 WebView [Radar:23159060](仍然开放 2/2018) 和 WKWebView 甚至没有似乎解决了 OSX 上的打印问题。在网上查看此 class 的开源后,我看到 all 的 classes与打印有关的任何内容都在仅支持平台的条件编译块中:iOS.
更新第二部分:令人惊讶的是,这个荒谬的错误存在于这个 class 的所有实现中,包括 iOS 上的实现!我觉得这很荒谬 still 尽管文档声明使用这个(而且只有这个 class ) 在支持 iOS 8 或更高版本的应用程序中。现在不可能在 iOS 或 OSX 上打印 WebView 的所有屏幕上和 屏幕外 内容.失败的苹果。是时候解决这个问题了!我们都知道史蒂夫会怎么说....
更新第三部分 :)- 更令人惊讶的是,这个问题从 10.15.2 开始并没有得到解决,并且在 4 年多的时间里出现!!!这个问题一直存在(Apple waaaaaake up up up up up ....)。考虑到他们在 iOS 土地上使用 WKWebView 变得非常咄咄逼人,甚至拒绝不使用的应用程序(除非你试图支持 iOS 7),这真是令人惊讶。
更新第四部分(2020 年......你能相信吗?!?):在大苏尔,这仍然是一个问题。我通过编写解决方法解决了这个问题,请参阅下面接受的答案:
printOperationWithPrintInfo:
不打印屏幕外或在水平或垂直方向上滚出视图的所有内容。但是,它确实使用了您的 Print CSS ,这比:
略有优势- (void)takeSnapshotWithConfiguration:(WKSnapshotConfiguration *)snapshotConfiguration
completionHandler:(void (^)(NSImage *snapshotImage, NSError *error))completionHandler;
为了让它工作,我做了:
NSPrintInfo *pInfo = [[NSPrintInfo alloc] initWithDictionary:printInfoDict];
pInfo.horizontalPagination = NSPrintingPaginationModeAutomatic;
pInfo.verticalPagination = NSPrintingPaginationModeAutomatic;
pInfo.verticallyCentered = YES;
pInfo.horizontallyCentered = YES;
pInfo.orientation = NSPaperOrientationLandscape;
pInfo.leftMargin = 30;
pInfo.rightMargin = 30;
pInfo.topMargin = 30;
pInfo.bottomMargin = 30;
NSPrintOperation *po = [_webView printOperationWithPrintInfo:pInfo];
po.showsPrintPanel = YES;
po.showsProgressPanel = YES;
// Without the next line you get an exception. Also it seems to
// completely ignore the values in the rect. I tried changing them
// in both x and y direction to include content scrolled off screen.
// It had no effect whatsoever in either direction.
po.view.frame = _webView.bounds;
// [printOperation runOperation] DOES NOT WORK WITH WKWEBVIEW, use
[po runOperationModalForWindow:self.view.window delegate:self didRunSelector:@selector(printOperationDidRun:success:contextInfo:) contextInfo:nil];
**还有一些我不完全理解的事情。 wkWebView 的大小似乎无关紧要。如果我调整应用程序的大小以隐藏某些内容,它似乎仍然会在屏幕外抓取尽可能多的内容以适应指定的页面,但它似乎不知道如何对不适合页面大小的内容进行分页到其他页面。所以这似乎是问题所在。可能有一些方法可以解决这个问题,如果有人有线索 post 就在这里!!
在 iOS 中,我将 UIView.viewPrintFormatter()
传递给 UIActivityViewController
以允许用户打印所有内容
let myShare = [webView.viewPrintFormatter()]
let avc = UIActivityViewController(activityItems: myShare, applicationActivities: nil)
present(avc, animated: true, completion: nil)
我用尽了所有可能的方法来直接打印 WKWebView,但没有成功。我能想到的唯一解决方法是将网页转换为 PDF 对象,然后打印该对象。如果解决方法有效,我将更新代码。
这不是正确答案,因为 Apple 需要使用有效的打印方法或 .evaluateJavaScript("window.print()", completionHandler: nil)
但我有一个愚蠢的解决方案 "works" 对我来说可能会帮助其他人解决这个问题。
第 1 步:抓住 HTML 并修复 <body> tag with <body onload='window.print()'>
。如果您从某个地方获取 html 而不是加载您自己的,您将需要使用一些正则表达式。这个我就不说了。
第 2 步:将 html 保存在某个文件中,并将完整路径保存在变量中。在我的示例中:文件名
第 3 步:将您的打印按钮连接到此代码:
RunCommand(command: "/usr/bin/open \(filename)")
请参阅下面的 RunCommand 代码。这留下了一个愚蠢的 safari window,但它可以在不保存到文件的情况下打印一些东西,然后记住你把它贴在哪里,这样你就可以自己用 Safari 打开它来进行打印。
func RunCommand(command: String) -> (success: Bool, result: String) {
let cc = command.components(separatedBy: " ")
let process = Process()
process.launchPath = cc[0]
var cp: [String] = []
for i in (1..<cc.count) {
cp.append(cc[i])
}
process.arguments = cp
let pipe = Pipe()
process.standardOutput = pipe
process.launch()
let data = pipe.fileHandleForReading.readDataToEndOfFile()
if (data.count > 0) {
let output = String(data: data, encoding: String.Encoding.utf8)
// let output = NSString(data: data, encoding: String.Encoding.utf8.rawValue)
return (success: true, result: output!)
}
return (success: true, result: "")
}
如果 Apple 能够解决这个问题,那就太好了。我有一个内部应用程序,可以在 HTML 中完成所有报告,并且报告是您想要打印的内容。
我已经成功使用 SPI -[WKWebView _printOperationWithPrintInfo:] 通过了通常的 [NSPrintInfo sharedPrintInfo]。 请注意,您不能对返回的 NSPrintOperation 使用 -runOperation。您必须 使用 -runOperationModalForWindow:.... 这非常相似。问题出在 WebKit 内部,它需要一个 运行 runloop 和内部预览来了解页数。
它绝对适用于屏幕外内容,如果屏幕外的意思是“未完全显示在屏幕上”。我仍然有一个 WKWebView 显示在 window 中,但它非常小,只显示整个 webview 内容的一小部分(21 个 A4 页!)。希望这对您有所帮助!
PS:在 10.12、10.14 和 10.15 上测试。 代码是这样的:
SEL printSelector = NSSelectorFromString(@"_printOperationWithPrintInfo:"); // This is SPI on WKWebView. Apparently existing since 10.11 ?
NSMutableDictionary *printInfoDict = [[[NSPrintInfo sharedPrintInfo] dictionary] mutableCopy];
printInfoDict[NSPrintJobDisposition] = NSPrintSaveJob; // means you want a PDF file, not printing to a real printer.
printInfoDict[NSPrintJobSavingURL] = [NSURL fileURLWithPath:[@"~/Desktop/wkwebview_print_test.pdf" stringByExpandingTildeInPath]]; // path of the generated pdf file
printInfoDict[NSPrintDetailedErrorReporting] = @YES; // not necessary
// customize the layout of the "printing"
NSPrintInfo *customPrintInfo = [[NSPrintInfo alloc] initWithDictionary:printInfoDict];
[customPrintInfo setHorizontalPagination: NSPrintingPaginationModeAutomatic];
[customPrintInfo setVerticalPagination: NSPrintingPaginationModeAutomatic];
[customPrintInfo setVerticallyCentered:NO];
[customPrintInfo setHorizontallyCentered:NO];
customPrintInfo.leftMargin = 0;
customPrintInfo.rightMargin = 0;
customPrintInfo.topMargin = 5;
customPrintInfo.bottomMargin = 5;
NSPrintOperation *printOperation = (NSPrintOperation*) [_webView performSelector:printSelector withObject:customPrintInfo];
[printOperation setShowsPrintPanel:NO];
[printOperation setShowsProgressPanel:NO];
// BOOL printSuccess = [printOperation runOperation]; // THIS DOES NOT WORK WITH WKWEBVIEW! Use runOperationModalForWindow: instead (asynchronous)
[printOperation runOperationModalForWindow:self.window delegate:self didRunSelector:@selector(printOperationDidRun:success:contextInfo:) contextInfo:nil]; // THIS WILL WORK, but is async
5 年后,我设法解决了 原来的问题,这是由于 MacOS 11 实施WKWebView
printOperationWithPrintInfo
仍然无法正确处理滚出视图并向右滚动的内容。
根本问题似乎是剪辑区域边界之外的内容(尤其是右侧)没有得到正确处理。这可能是一个 WKWebView
错误,因为它似乎处理了垂直方向可见矩形下方的一些内容。
经过大量挖掘,发现其他人已经能够通过以下方式获取 NSView
的全部内容进行打印和正确分页:
- 视图分离(不在屏幕上)。
- 正在将框架设置为整个内容的大小。
- 然后在分离视图上调用 printWithPrintInfo。
我有一个解决方案的想法:
- 通过类别扩展
WKWebView
,该类别具有将所有内容作为图像块获取的功能。它通过 JavaScript 在 MacOS 上执行此操作,在 iOS 上通过操作与WKWebView
关联的UIScrollView
来获取完整的内容大小,然后将内容的各个部分滚动到可见区域并将其快照为图像块网格。 - 创建
NSView or UIView
的子类,以正确的关系绘制所有图块。 - 在分离视图上调用
printWithPrintInfo
。
在 MacOS 10.14+ iOS 13+
上运行良好在两个平台上,所有输出都正确分页(iOS 需要使用包含在相关 GitHub 项目中的 UIPrintPageRenderer),您可以在预览中使用打开为 PDF 并保存它作为文件等
我遇到的唯一缺点是未使用 Print CSS,考虑到 Apple 目前对 Print CSS 的支持很少,这并不重要。
所有工作代码都在 GitHub 此处:Full working source for iOS and MacOS
此源已过期 请参阅 Github
Header
//
// WKWebView+UtilityFunctions.h
// Created by Clifford Ribaudo on 12/24/20.
//
#import <WebKit/WebKit.h>
#ifdef _MAC_OS_ // Up to user to determine how they know this
#define IMAGE_OBJ NSImage
#define VIEW_OBJ NSView
#else
#define IMAGE_OBJ UIImage
#define VIEW_OBJ UIView
#endif
@interface TiledImageView : VIEW_OBJ
{
NSArray *_imageTiles;
}
-(void)printWithPrintInfo:(NSPrintInfo *)pi;
-(instancetype)initWithFrame:(CGRect)frame imageTiles:(NSArray<NSArray *> *)imageTiles;
@end
@interface WKWebView (UtilityFunctions)
-(void)HTMLPageMetrics:(void (^)(CGSize htmlDocSize, CGSize visibleSize, NSError *error))completionHandler;
-(void)currentScrollXY:(void (^)(float x, float y, NSError *error))completionHandler;
-(void)scrollHTMLTo:(float)x topY:(float)y completionHandler:(void (^)(NSError *error))completionHandler;
-(void)imageTilesForHTMLPage:(CGSize)pageSize visbleRect:(CGSize)visibleRect imgData:(NSMutableArray<NSArray *> *)tileData completionHandler:(void (^)(NSError *error))completionHandler;
-(void)imageTile:(CGRect)imgRect fromPageOfSize:(CGSize)pageSize inViewOfSize:(CGSize)viewSize completionHandler:(void (^)(IMAGE_OBJ *tileImage, NSError *error))completionHandler;
@end
实施
//
// WKWebView+UtilityFunctions.m
// Created by Clifford Ribaudo on 12/24/20.
//
// Works with MacOS v10.14+ and ??iOS 13+
//
#import "WKWebView+UtilityFunctions.h"
@implementation TiledImageView
-(instancetype)initWithFrame:(CGRect)frame imageTiles:(NSArray<NSArray *> *)imageTiles
{
self = [super initWithFrame:NSRectFromCGRect(frame)];
if(self) {
_imageTiles = imageTiles;
}
return self;
}
-(BOOL)isFlipped {return YES;}
-(void)printWithPrintInfo:(NSPrintInfo *)pi
{
NSPrintOperation *po = [NSPrintOperation printOperationWithView:self];
po.printInfo = pi;
[po runOperation];
}
- (void)drawRect:(NSRect)rect
{
for(NSArray *imgData in _imageTiles)
{
NSRect drawRect = ((NSValue *)imgData[0]).rectValue;
IMAGE_OBJ *img = imgData[1];
[img drawInRect:drawRect];
}
}
@end
@implementation WKWebView (UtilityFunctions)
//
// Returns via Completion Handler:
// htmlDocSize - The size of the entire <HTML> element, visible or not
// visibleSize - The visible dimensions of the page, essentially WKWebView bounds minus HTML scroll bar dimensions
//
-(void)HTMLPageMetrics:(void (^)(CGSize htmlDocSize, CGSize visibleSize, NSError *error))completionHandler
{
//
// Anonymous Function - gets Size of entire HTML element and visible size.
// Result String = Full X, Full Y, Visible X, Visible Y
//
NSString *jsGetPageMetrics = @"(function(){return document.documentElement.scrollWidth + ',' + document.documentElement.scrollHeight + ',' + document.documentElement.clientWidth + ',' +document.documentElement.clientHeight;})();";
// Execute JS in WKWebView
[self evaluateJavaScript:jsGetPageMetrics completionHandler:^(id result, NSError *error)
{
CGSize htmlSize = CGSizeMake(0, 0);
CGSize visibleSize = CGSizeMake(0, 0);
if(!error && result)
{
NSArray<NSString *> *data = [[NSString stringWithFormat:@"%@", result] componentsSeparatedByString:@","];
htmlSize = CGSizeMake([data[0] floatValue], [data[1] floatValue]);
visibleSize = CGSizeMake([data[2] floatValue], [data[3] floatValue]);
}
else
NSLog(@"JS error getting page metrics: %@", error.description);
completionHandler(htmlSize, visibleSize, error);
}];
}
//
// Get <HTML> element current scroll position (x,y) and return to completeion handler:
// x = document.documentElement.scrollLeft
// y = document.documentElement.scrollTop
//
-(void)currentScrollXY:(void (^)(float X, float Y, NSError *error))completionHandler
{
NSString *jsGetPageMetrics = @"(function(){return document.documentElement.scrollLeft + ',' + document.documentElement.scrollTop;})();";
// Execute JS in WKWebView
[self evaluateJavaScript:jsGetPageMetrics completionHandler:^(id result, NSError *error) {
if(!error && result)
{
NSArray<NSString *> *data = [[NSString stringWithFormat:@"%@", result] componentsSeparatedByString:@","];
completionHandler([data[0] floatValue], [data[1] floatValue], error);
}
else {
NSLog(@"JS error getting page metrics: %@", error.localizedDescription);
completionHandler(0, 0, error);
}
}];
}
//
// Scroll the current HTML page to x, y using scrollTo(x,y) on the <HTML> element
// Optional Completion Handler to do something when scroll finished
//
-(void)scrollHTMLTo:(float)x topY:(float)y completionHandler:(void (^)(NSError *error))completionHandler
{
NSString *js = [NSString stringWithFormat:@"document.documentElement.scrollTo(%0.f, %0.f);", x, y];
// Execute JS in WKWebView
[self evaluateJavaScript:js completionHandler:^(id result, NSError *error)
{
dispatch_time_t delay = dispatch_time(DISPATCH_TIME_NOW, .25 * NSEC_PER_SEC);
dispatch_after(delay, dispatch_get_main_queue(), ^{
if(completionHandler) completionHandler(error);
});
if(error) NSLog(@"JS error scrollTo %@", error.localizedDescription);
}];
}
//
// Called Recursively until tiles are obtained for the entire pageRect.
// Tiles are the size of visibleRect (WKWebView.bounts) but can be smaller.
// tileData - Array of arrays holding CGRect & Img.
//
-(void)imageTilesForHTMLPage:(CGSize)pageSize visbleRect:(CGSize)visibleSize imgData:(NSMutableArray<NSArray *> *)tileData completionHandler:(void (^)(NSError *error))completionHandler
{
__block CGRect currentRect; // In coordinates of pageSize (full).
if(tileData.count == 0) { // No image tiles yet. Start at top left of html page for visible WKWebView bounds
currentRect.origin.x = currentRect.origin.y = 0.0;
currentRect.size = visibleSize;
}
else {
NSArray *lastTile = [tileData lastObject]; // Calculate what the next tile rect is or call handler if done.
CGRect lastTileRect;
#ifdef _MAC_OS_
lastTileRect = ((NSValue *)lastTile[0]).rectValue;
#else
lastTileRect = ((NSValue *)lastTile[0]).CGRectValue;
#endif
// Check if anything more to get to right of last tile
if((lastTileRect.origin.x + lastTileRect.size.width) < pageSize.width)
{
currentRect.origin.x = lastTileRect.origin.x + lastTileRect.size.width + 1; // Next x to right of last tile
currentRect.origin.y = lastTileRect.origin.y; // Works on all rows
currentRect.size.height = lastTileRect.size.height;
currentRect.size.width = pageSize.width - currentRect.origin.x; // Get width of next tile to right of last
if(currentRect.size.width > visibleSize.width) // If more tiles to right use visible width
currentRect.size.width = visibleSize.width;
}
else if((lastTileRect.origin.y + lastTileRect.size.height) < pageSize.height) // New Row
{
currentRect.origin.x = 0; // Reset x back to left side of hmtl
currentRect.size.width = visibleSize.width; // Reset width back to view width
currentRect.origin.y = lastTileRect.origin.y + lastTileRect.size.height + 1; // Get y below last row
currentRect.size.height = pageSize.height - currentRect.origin.y;
if(currentRect.size.height > visibleSize.height) // If more rows below use row height
currentRect.size.height = visibleSize.height;
}
else {
completionHandler(nil);
return;
}
}
[self imageTile:currentRect fromPageOfSize:pageSize inViewOfSize:visibleSize completionHandler:^(NSImage *tileImage, NSError *error)
{
if(error || !tileImage) {
NSLog(@"Error getting image tiles %@", error.description);
completionHandler(error);
return;
}
#ifdef _MAC_OS_
[tileData addObject:@[[NSValue valueWithRect:NSRectFromCGRect(currentRect)], tileImage]];
#else
[tileData addObject:@[[NSValue valueWithCGRect:currentRect], tileImage]];
#endif
[self imageTilesForHTMLPage:(CGSize)pageSize visbleRect:(CGSize)visibleSize imgData:(NSMutableArray<NSArray *> *)tileData completionHandler:completionHandler];
}];
}
//
// ImgRect = location of rect in full page size. Has to be translated into what is visible and where.
// pageSize = Full size of HTML page, visible or not.
// viewSize = essentially the wkwebview.bounds.size - HTML scroll bars.
//
-(void)imageTile:(CGRect)imgRect fromPageOfSize:(CGSize)pageSize inViewOfSize:(CGSize)viewSize completionHandler:(void (^)(IMAGE_OBJ *tileImage, NSError *error))completionHandler
{
float x = imgRect.origin.x; // Always do this to make the desired rect visible in the rect of viewSize
float y = imgRect.origin.y;
CGRect rectToGetFromView;
rectToGetFromView.origin.x = 0;
rectToGetFromView.origin.y = 0;
rectToGetFromView.size = imgRect.size;
// If img is smaller than the viewport, determine where it is after scroll
if(imgRect.size.width < viewSize.width)
rectToGetFromView.origin.x = viewSize.width - imgRect.size.width;
if(imgRect.size.height < viewSize.height)
rectToGetFromView.origin.y = viewSize.height - imgRect.size.height;
[self scrollHTMLTo:x topY:y completionHandler:^(NSError *error)
{
if(!error) {
WKSnapshotConfiguration *sc = [WKSnapshotConfiguration new];
sc.rect = rectToGetFromView;
[self takeSnapshotWithConfiguration:sc completionHandler:^(IMAGE_OBJ *img, NSError *error)
{
if(error) NSLog(@"Error snapshotting image tile: %@", error.description);
completionHandler(img, error);
}];
}
else {
NSLog(@"Error scrolling for next image tile %@", error.description);
completionHandler(nil, error);
}
}];
}
@end
用法
在为您的 WKWebView
处理打印的任何内容中使用类别,如下所示:
-(void)print:(id)sender
{
// Set this as per your needs
NSPrintInfo *pInfo = [NSPrintInfo sharedPrintInfo];
pInfo.verticallyCentered = YES;
pInfo.horizontallyCentered = NO;
pInfo.horizontalPagination = NSAutoPagination;
pInfo.verticalPagination = NSAutoPagination;
pInfo.orientation = NSPaperOrientationLandscape;
pInfo.bottomMargin = 30;
pInfo.topMargin = 30;
pInfo.leftMargin = 30;
pInfo.rightMargin = 30;
pInfo.scalingFactor = .60;
[_webView HTMLPageMetrics:^(CGSize htmlSize, CGSize visibleSize, NSError *error)
{
self->_imgTileData = [NSMutableArray new];
[self->_webView imageTilesForHTMLPage:htmlSize visbleRect:visibleSize imgData:self->_imgTileData completionHandler:^(NSError *error) {
if(!error) {
TiledImageView *tiv = [[TiledImageView alloc] initWithFrame:CGRectMake(0,0,htmlSize.width,htmlSize.height) imageTiles:self->_imgTileData];
[tiv printWithPrintInfo:pInfo];
}
}];
}
}
这是作为 Github 要点的代码:Above code
这是我的 swift 版本(针对 OSX 11 更新)@Altimac 的解决方案。
@available(OSX 11.0, *)
extension BrowserViewController
{
func print(pdf: Bool)
{
guard var printInfoDictionary = NSPrintInfo.shared.dictionary().mutableCopy() as? [NSPrintInfo.AttributeKey: Any] else { return }
printInfoDictionary[NSPrintInfo.AttributeKey.jobDisposition] = pdf ? NSPrintInfo.JobDisposition.preview : NSPrintInfo.JobDisposition.spool
printInfoDictionary[NSPrintInfo.AttributeKey.detailedErrorReporting] = true
let printInfo = NSPrintInfo(dictionary: printInfoDictionary)
printInfo.horizontalPagination = .automatic
printInfo.verticalPagination = .automatic
printInfo.isVerticallyCentered = false
printInfo.isHorizontallyCentered = false
printInfo.leftMargin = 10
printInfo.rightMargin = 10
printInfo.topMargin = 10
printInfo.bottomMargin = 10
let printOperation = theWebView.printOperation(with: printInfo)
printOperation.showsPrintPanel = false
printOperation.showsProgressPanel = false
printOperation.runModal(for: self.view.window!, delegate: self, didRun: nil, contextInfo: nil)
}
}
似乎至少对我有用。