如何打印所有WKWebView On和Offscreen内容OSX和iOS

Cli*_*udo 25 printing macos ios osx-yosemite wkwebview

这个问题是关于打印WKWebView的所有内容(包括屏幕外内容).目前(仍然是iOS 10.2或OSX 10.12)没有工作解决方案,并且Stackoverflow上没有任何假设的解决方案.如果您已经验证了可以打印屏幕内容,那么只在这里提供答案,如果您这样做,则提供工作示例代码.

我正在尝试在OSX 10.10或更高版本上打印WKWebViewWebView的所有内容(当前在10.11.2上运行).例如,一个宽的html表,其中列不在视图范围内并且在右边.早期版本的OSX会自动分页并正确打印所有html.

我已尝试使用Stackoverflow和其他地方提供的解决方案.所有人基本上都说同样的东西是打印出来的documentView那样:

[[NSPrintOperation printOperationWithView:_webView.mainFrame.frameView.documentView printInfo:pInfo] runOperation];
Run Code Online (Sandbox Code Playgroud)

这在10.10中停止了WKWebViewWebView的工作.如果你这样做:

    [[NSPrintOperation printOperationWithView:_wkWebView printInfo:pInfo] runOperation];
Run Code Online (Sandbox Code Playgroud)

你得到分页,但打印输出包括滚动条WebView,另一个WKWebView给你空白页.

我在Apple文档中找不到关于在OSX上打印WKWebView的任何内容.我也找不到任何OSX特定的答案,而不是iOS.

有没有人知道如何在OSX上打印这些?

更新:这是WebView [雷达:23159060](仍然打开2/2018)中的一个错误,WKWebView甚至似乎无法解决OSX上的打印问题.在网上检查了这个类的开源之后,我看到所有与打印有关的类都在一个只支持平台的条件编译块中:iOS.

UPDATE Part Deux:令人惊讶的是,这个荒谬的bug存在于这个类的所有实现中,包括iOS上的那些!尽管文档声明在支持iOS 8或更高版本的应用程序中使用此(并且只有此类),但我发现这仍然没有在这个较晚的日期得到解决,这一点很荒谬.现在,在iOS或OSX上打印WebView的所有屏幕和屏幕外内容是不可能的.Apple失败了.是时候搞定了!我们都知道史蒂夫会对此说些什么......

注意:另外,我认为这个问题的根本原因还在于无法将WKWebView的所有内容保存为图像.曾经有一些技术可以与UIWebView一起使用,以将WebView的整个屏幕内容和屏幕外内容保存到图像中.我还没有找到一个有效的解决方案.

Cli*_*udo 3

5年后,我设法解决了最初的问题,这是由于MacOS 11实现WKWebView printOperationWithPrintInfo仍然无法正确处理滚动到视图之外并移至右侧的内容这一事实所迫。

根本问题似乎是剪辑区域边界之外(尤其是右侧)的内容未得到正确处理。这可能是一个WKWebView错误,因为它似乎处理垂直方向可见矩形下方的一些内容。

经过大量挖掘,并看到其他人已经能够NSView通过以下方式打印完整的内容并正确分页:

  • 视图分离(不在屏幕上)。
  • 将框架设置为整个内容的大小。
  • 然后在分离视图上调用printWithPrintInfo 。

我有一个解决方案的想法:

  1. WKWebView通过具有将所有内容作为图像图块的功能的类别进行扩展。它在 MacOS 上通过 JavaScript 执行此操作,在 iOS 上则通过操作UIScrollViewWKWebView获取完整内容大小,然后将内容的各个部分滚动到可见区域并将其快照为图像图块网格。
  2. 创建一个子类NSView or UIView,以正确的关系绘制所有图块。
  3. 调用printWithPrintInfo分离视图。

它在 MacOS 10.14+ iOS 13+ 上运行良好

在这两个平台上,所有输出都正确分页(iOS 需要使用 UIPrintPageRenderer,它包含在关联的 GitHub 项目中),您可以在预览中使用打开为 PDF 并将其另存为文件等。

我遇到的唯一缺点是没有使用 Print CSS,但这并不重要,因为 Apple 目前对 Print CSS 的支持很少。

所有工作代码都位于 GitHub 上:iOS 和 MacOS 的完整工作源代码

此源已过时 请参阅 Github

标题

//
//  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
Run Code Online (Sandbox Code Playgroud)

实施

//
//  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
Run Code Online (Sandbox Code Playgroud)

用法

在任何处理打印的内容中使用类别,如下所示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];
            }
        }];
    }
}
Run Code Online (Sandbox Code Playgroud)

以下是 Github Gist 中的代码:上面的代码

从此WKWebView内容开始,并向右滚动:WKWebView 内容不可见

可以得到带有正确分页的打印对话框: 在此输入图像描述