Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How does one Print all WKWebView On AND Offscreen content OSX and iOS

This question is about printing ALL content (including off screen content) of WKWebView. Currently (still, as of iOS 10.2 or OSX 10.12) there is NO working solution and none of the supposed solutions on Stackoverflow work. Only provide an answer here if you have verified for yourself that you can print OFF SCREEN CONTENT, and if you did then provide the working example code.

I'm trying to print ALL the content of a WKWebView or WebView on OSX 10.10 or above (Currently running on 10.11.2). For example, a wide html table where columns are out of view and off to the right. Earlier versions of OSX would automatically paginate and correctly print all the html.

I've tried using the solutions available here on Stackoverflow and elsewhere. All essentially say the same thing which is to print the documentView like so:

[[NSPrintOperation printOperationWithView:_webView.mainFrame.frameView.documentView printInfo:pInfo] runOperation]; 

This stopped working for both WKWebView or WebView in 10.10. If you do this:

    [[NSPrintOperation printOperationWithView:_wkWebView printInfo:pInfo] runOperation]; 

You get pagination but the printout includes scroll bars WebView, and the other WKWebView gives you blank pages.

I can't find any mention whatsoever in Apple documentation about printing for WKWebView on OSX. Nor can I find any answer that is OSX specific and not iOS.

Does anyone have ANY idea how to print these on OSX?

UPDATE: This is a bug in WebView [Radar:23159060] (still open 2/2018) and WKWebView does not even appear to address printing on OSX. After examining the Open Source for this class on the net, I see that all of the classes that have anything to do with printing are in a conditional compilation block that only supports platform: iOS.

UPDATE Part Deux: Amazingly this ridiculous bug exists in ALL implementations of this class including those on iOS! I find it ridiculous that this is still not fixed at this late date despite the documentation's statement to use this (and only this class) in Apps that support iOS 8 or above. It is now IMPOSSIBLE to print all the on screen and off screen content of a WebView on either iOS or OSX. Fail Apple. Time to FIX THIS! We all know what Steve would've said about it....

UPDATE Part THREE :)- Yet still more amazing is that this issue is NOT resolved as of 10.15.2 and coming up on 4+ YEARS!!! this issue has been floating around (Apple waaaaaake up up up up....). It's kind of amazing considering they're getting very pushy about using WKWebView and over in iOS land even rejecting Apps that don't (unless you're trying to support iOS 7).

UPDATE Part FOUR (2020... can you believe it?!?): As of Big Sur, this is still an issue. I solved it by writing a work around see accepted answer below:

printOperationWithPrintInfo: 

DOES NOT print all content which is off screen or scrolled out of view in either the Horizontal or vertical direction. It does however use your Print CSS which is a slight advantage over:

- (void)takeSnapshotWithConfiguration:(WKSnapshotConfiguration *)snapshotConfiguration                  completionHandler:(void (^)(NSImage *snapshotImage, NSError *error))completionHandler; 

To get it to work I did:

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]; 

**Also there is something going on I don't fully understand. It doesn't seem to matter what size your wkWebView is. If I size the App to hide some of the content it still seems to grab as much that IS off screen as will fit on the page specified, but it doesn't seem to know how to paginate content that will not fit on the page size onto other pages. So that appears to be where the issue is. There may be some way around this and if anyone has a clue post it here!!

like image 323
Cliff Ribaudo Avatar asked Oct 24 '15 14:10

Cliff Ribaudo


2 Answers

I've successfully used the SPI -[WKWebView _printOperationWithPrintInfo:] passing the usual [NSPrintInfo sharedPrintInfo]. Note that you CAN'T use -runOperation on the returned NSPrintOperation. You must use -runOperationModalForWindow:.... which is quite similar. The problem resides in the WebKit internals that expects a running runloop and a preview to be made internally to know the number of pages.

It definitely works with offscreen content, if what you mean by offscreen is "not fully displayed on screen". I still have a WKWebView displayed in a window, but it's very tiny and only displays a very short fraction of the entire webview content (21 A4 pages!). Hope this helps!

PS: Tested on 10.12, 10.14 and 10.15. Code is like this:

     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 
like image 56
Altimac Avatar answered Oct 05 '22 19:10

Altimac


After 5 years I've managed to solve the original problem and which was forced by the fact that the MacOS 11 implementation of WKWebView printOperationWithPrintInfo still doesn't properly handle content scrolled out of view and off to the right.

The root issue seems to be that content outside the bounds of the clipping region (especially to the right) is not properly handled. This may be a WKWebView bug, because it seems to handle some content below the visible rect in the vertical direction.

After much digging, and seeing that others had been able to get the entire content of an NSView to print and properly paginate by having:

  • The view detached (not on screen).
  • Setting the frame to the size of the entire content.
  • Then calling printWithPrintInfo on the detached view.

I had an idea for a solution:

  1. Extend WKWebView via a Category with functions that get all the content as image tiles. It does this on MacOS via JavaScript and on iOS by manipulating the UIScrollView associated with the WKWebView to get the full content size and then scrolling the various parts of the content into the visible area and snapshotting it as a grid of image tiles.
  2. Create a subclass of NSView or UIView that draws all the tiles in their proper relation.
  3. Call printWithPrintInfo on the detached view.

It works well on MacOS 10.14+ iOS 13+

On both platforms all the output is properly paginated (iOS requires use of UIPrintPageRenderer which is included in the associated GitHub project) and you can use the open as PDF in Preview and save it as a file, etc.

The only drawback I've encountered is that Print CSS is NOT used, not that that matters much given that Apple's support for Print CSS is currently minimal.

All the working code is on GitHub here: Full working source for iOS and MacOS

THIS Source is Out of Date See Github

The 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 

The Implementation

// //  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 

Usage

Use the Category in whatever handles printing for your WKWebView like so:

-(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];             }         }];     } } 

Here is the code as a Github Gist: Above code

And from this WKWebView with content below and also scrolled off to the right: WKWebView with content Out of View

One gets this print dialog with proper pagination: enter image description here

like image 35
Cliff Ribaudo Avatar answered Oct 05 '22 19:10

Cliff Ribaudo