0

I have a WKWebView that can visit arbitrary websites. When a user clicks on any HTML <img> element, I'd like it to transparently save that image (as a file) into the app tmp directory, ideally with standard title (img.png), and ideally such that it would overwrite each time.

Given that client-side JavaScript has no access to the filesystem, I expect that a fully automatic solution would involve FileManager; however, I don't know how I'd transmit the <img> data from the WKWebView to a FileManager instance. I wonder whether JavaScriptCore might need to be involved, to bridge the data between the two.

I see that semi-automatic solutions exist, through the use of a HTML download attribute, wherein the user is prompted with a 'Save As..." dialogue. This is not ideal, as I would like the action to be transparent and free of user error. However, it may prove to be the only option.

I am implementing this on both macOS and iOS, so I can accept a solution for either platform; I expect there will be little difference between the two.

Jamie Birch
  • 5,839
  • 1
  • 46
  • 60
  • If the _Web URL_ that you are accessing , does not provide the privilege to download the image ,then I don't think there is a way as `WKWebView` just loads the _Webpage_ and all the interactions inside the `WKWebView` is upto the **User** and the **Script** used inside that webpage for interaction and we don't have any _delegate_ inside `WKWebView` for tracking the user interactions with the _HTML tags_ inside the Webpage – Shubham Bakshi Oct 29 '18 at 09:21
  • I have already set up a JavaScript click handler that identifies whether a user has clicked upon a HTML `` element. I can call back to native using this; the question is rather about what code to call. – Jamie Birch Oct 29 '18 at 09:26
  • How will you send a message back from your _webpage_ to your _application_ that the user has tapped on a certain image ? – Shubham Bakshi Oct 29 '18 at 09:53
  • You can check these delegate methods for this : https://developer.apple.com/documentation/webkit/wknavigationdelegate – Shubham Bakshi Oct 29 '18 at 09:59
  • @ShubhamBakshi I'm using [WKScriptMessageHandler](https://developer.apple.com/documentation/webkit/wkscriptmessagehandler) to communicate from the WKWebView back to the native app. I inject scripts into the WKWebView pages via [WKUserContentController](https://developer.apple.com/documentation/webkit/wkusercontentcontroller) – one such script is the script that attaches the click handler that I mentioned. WKNavigationDelegate is less relevant here. – Jamie Birch Oct 29 '18 at 10:49

1 Answers1

0

May I propose the following :

  1. Prepare your view controller to receive messages from the javascript side of the WKWebView. I usually do that in the view controller's viewDidLoad.
  2. Load and execute a javascript in the web page that will add an onClick event to each img tag.
  3. In that event, you send a message back from javascript to Objective-C / Swift side with a Base64 encoded string of the image data as parameter
  4. In the Objective-C / Swift handler of the message, you transform that string in data and save it.

Step 1 & 2 :

- (void)    viewDidLoad
{
    [super viewDidLoad] ;
    WKUserContentController *controller = self.webView.configuration.userContentController ;

    //  Add self as scriptMessageHandler of the webView to receive messages from the scripts
    [controller addScriptMessageHandler:self
                                   name:@"imageHasBeenClicked"] ;

    //  Load script
    NSURL       *scriptURL      = <<... URL of your javascript (can be bundled in your app) ...>> ;
    NSString    *scriptString   = [NSString stringWithContentsOfURL:scriptURL
                                                           encoding:NSUTF8StringEncoding
                                                              error:NULL] ;
    WKUserScript    *script = [[WKUserScript alloc] initWithSource:scriptString
                                                     injectionTime:WKUserScriptInjectionTimeAtDocumentEnd
                                                  forMainFrameOnly:YES] ;
    [controller addUserScript:script] ;
}

Step 3 :

The javascript :

//  This function takes an image tab and encodes the image as BAse64
function getBase64Image(img)
{
    // Create an empty canvas element
    var canvas      = document.createElement("canvas") ;
    canvas.width    = img.width;
    canvas.height   = img.height;

    // Copy the image contents to the canvas
    var ctx         = canvas.getContext("2d") ;
    ctx.drawImage(img,0,0) ;

    // Get the data-URL formatted image, use PNG as JPG re-encode the image
    var dataURL     = canvas.toDataURL("image/png");

    //  Remove the initial marker so that we directly have NSData compatibility
    return dataURL.replace(/^data:image\/(png|jpg);base64,/,"");
}

//  Search for all img tags and add an onclick event that will encode the image
//  then, send it to the objective-c side
var imgList = document.getElementsByTagName("img") ;
for (var i = 0; i < imgList.length; i++)
{
    imgList[i].onclick = function()
    {
        var txt = getBase64Image(this) ;
        window.webkit.messageHandlers["imageHasBeenClicked"].postMessage(txt) ;
    } ;
}

Step 4 :

When the "imageHasBeenCLicked" message is received by the view controller, then convert the Base64 string into data and save it as an image file.

- (void)    userContentController:(WKUserContentController*)userContentController
      didReceiveScriptMessage:(WKScriptMessage*)message
{
    if ([message.name isEqualToString:@"imageHasBeenClicked"])
    {
        NSData *data    = [[NSData alloc] initWithBase64EncodedString:message.body
                                                          options:0] ;
       [data writeToFile:@"/toto.png"
              atomically:YES] ;
    }
}
AirXygène
  • 2,409
  • 15
  • 34
  • This method (although viable) means re-downloading an already-downloaded image. As the `` data has already been loaded into memory, I'm aiming to save the browser's existing copy of that data to disk. As stated in the question, I'm aiming to 'save' the image; not 'download' it. Sorry for the ambiguity. – Jamie Birch Oct 31 '18 at 10:16
  • Yes, agreed. So I modified my answer to avoid reloading the image. I go through an intermediate Base64 encoded string that I send to the objective-C side. – AirXygène Oct 31 '18 at 11:19
  • Unfortunately the solution needs to work despite cross-origin security (that's why I specified saving the image rather than anything involving a download). For example, running `getBase64Image()` on the main `` element in [this comment](https://stackoverflow.com/a/52677126/5951226) is interrupted by a SecurityError because of the call to `canvas.toDataURL()`. – Jamie Birch Oct 31 '18 at 11:38