13

I am trying to capture the image that the webview is displaying to the user, so I can some color analysis of the web page. When I try to get the image from it's parent, I am basically getting a white box, even though the page has rendered:

func makeImageSnapshot()-> (NSImage)
{


    let imgSize = self.view.bounds.size
    let bir = self.viewbitmapImageRepForCachingDisplayInRect(self.webView!.view.bounds)

    bir.size = imgSize
    self.webView.cacheDisplayInRect(self.view.bounds, toBitmapImageRep:bir)

    let image = NSImage(size:imgSize)
    image.addRepresentation(bir)
    self.image = image

    return image
}

func saveSnapshot()
{
    let imgRep = self.image!.representations[0]
    let data = imgRep.representationUsingType(NSBitmapImageFileType.NSPNGFileType, properties: nil)
    data.writeToFile("/tmp/file.png", atomically: false)

}

It looks to me like I can't get access to the properties of the actual view (in this case the bounds) inside of the webView. When I try to access it, the compiler barfs:

/Users/josh/Canary/MacOsCanary/canary/canary/Modules/Overview/Overview.swift:55:37: '(NSView!, stringForToolTip: NSToolTipTag, point: NSPoint, userData: UnsafePointer<()>) -> String!' does not have a member named 'bounds'

My guess is that this is happening due to the extensions approach used by OS X and iOS. Any ideas, or should I just go back to using the legacy WebView?

nhgrif
  • 61,578
  • 25
  • 134
  • 173
Josh Prismon
  • 131
  • 1
  • 3
  • You're trying to access "bounds" on a String!. – nhgrif Jul 13 '14 at 22:44
  • @Josh have you found a solution for iOS? Currently i'm using snapshotViewAfterScreenUpdates. But I have to call it after the webview is rendered, so I wait for a 0.1 secs in didFinishNavigation and call snapshotViewAfterScreenUpdates. Not very convinient and reliably( – sidslog Dec 06 '14 at 23:42
  • Please refer to this answer: https://stackoverflow.com/a/50962265/3659227 – Maverick Jun 21 '18 at 09:22

4 Answers4

2

I realise the question was for Mac OS X, but I found this page whilst searching for an iOS solution. My answer below doesn't work on Mac OS X as the drawViewHierarchyInRect() API call is currently iOS only, but I put it here for reference for other iOS searchers.

This Stackoverflow answer solved it for me on iOS 8 with a WKWebView. That answer's sample code is in Objective-C but the Swift equivalent to go in a UIView sub-class or extension would be along the lines of the code below. The code ignores the return value of drawViewHierarchyInRect(), but you may want to pay attention to it.

func imageSnapshot() -> UIImage
{
    UIGraphicsBeginImageContextWithOptions(self.bounds.size, true, 0);
    self.drawViewHierarchyInRect(self.bounds, afterScreenUpdates: true);
    let snapshotImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return snapshotImage;
}
Community
  • 1
  • 1
user2067021
  • 4,399
  • 37
  • 44
1

Swift 3

extension WKWebView {
    func screenshot() -> UIImage? {
        UIGraphicsBeginImageContextWithOptions(self.bounds.size, true, 0);
        self.drawHierarchy(in: self.bounds, afterScreenUpdates: true);
        let snapshotImage = UIGraphicsGetImageFromCurrentImageContext();
        UIGraphicsEndImageContext();
        return snapshotImage;
    }
}

Note: This solution only works on iOS.

quemeful
  • 9,542
  • 4
  • 60
  • 69
0

Found myself in the same boat today but found a solution (by using private APIs).

If you're not targeting the App Store and generally are not afraid of using private APIs, here's a way to capture screenshots of WKWebView's on OS X:

https://github.com/lemonmojo/WKWebView-Screenshot

lemonmojo
  • 733
  • 5
  • 20
0

You will need to have access to a target writeable place - the snapshotURL ie.., such as the desktop, so we provide a handler for that:

func registerSnaphotsURL(_ sender: NSMenuItem, handler: @escaping (URL) -> Void) {
    var targetURL : URL

    //  1st around authenticate and cache sandbox data if needed
    if isSandboxed, desktopData == nil {
        targetURL =
            UserSettings.SnapshotsURL.value.count == 0
                ? getDesktopDirectory()
                : URL.init(fileURLWithPath: UserSettings.SnapshotsURL.value, isDirectory: true)
        
        let openPanel = NSOpenPanel()
        openPanel.message = "Authorize access to "
        openPanel.prompt = "Authorize"
        openPanel.canChooseFiles = false
        openPanel.canChooseDirectories = true
        openPanel.canCreateDirectories = true
        openPanel.directoryURL = targetURL
        openPanel.begin() { (result) -> Void in
            if (result == .OK) {
                targetURL = openPanel.url!
                
                //  Since we do not have data, clear any bookmark
                
                if self.storeBookmark(url: targetURL, options: self.rwOptions) {
                    self.desktopData = self.bookmarks[targetURL]
                    UserSettings.SnapshotsURL.value = targetURL.absoluteString
                    if !self.saveBookmarks() {
                        print("Yoink, unable to save snapshot bookmark")
                    }

                    self.desktopData = self.bookmarks[targetURL]
                    handler(targetURL)
                }
            }
            else
            {
                return
            }
        }
    }
    else
    {
        targetURL =
            UserSettings.SnapshotsURL.value.count == 0
                ? getDesktopDirectory()
                : URL.init(fileURLWithPath: UserSettings.SnapshotsURL.value, isDirectory: true)
        handler(targetURL)
    }
}

we wanted to allow single (view controller) and all current views (app delegate) so two actions in their respective files, both making use of the register handler.

App Delegate

@objc @IBAction func snapshotAllPress(_ sender: NSMenuItem) {
    registerSnaphotsURL(sender) { (snapshotURL) in
        //  If we have a return object just call them, else notify all
        if let wvc : WebViewController = sender.representedObject as? WebViewController {
            sender.representedObject = snapshotURL
            wvc.snapshot(sender)
        }
        else
        {
            sender.representedObject = snapshotURL
            let notif = Notification(name: Notification.Name(rawValue: "SnapshotAll"), object: sender)
            NotificationCenter.default.post(notif)
        }
    }
}

View Controller

func viewDidLoad() {
    NotificationCenter.default.addObserver(
        self,
        selector: #selector(WebViewController.snapshotAll(_:)),
        name: NSNotification.Name(rawValue: "SnapshotAll"),
        object: nil)
}

@objc func snapshotAll(_ note: Notification) {
    snapshot(note.object as! NSMenuItem)
}

view singleton action

@objc @IBAction func snapshotPress(_ sender: NSMenuItem) {
    guard let url = webView.url, url != webView.homeURL else { return }
    guard let snapshotURL = sender.representedObject as? URL else {
        //  Dispatch to app delegate to handle a singleton
        sender.representedObject = self
        appDelegate.snapshotAllPress(sender)
        return
    }
    
    sender.representedObject = snapshotURL
    snapshot(sender)
}

the webView interaction to capture an image

@objc func snapshot(_ sender: NSMenuItem) {
    guard let url = webView.url, url != webView.homeURL else { return }
    guard var snapshotURL = sender.representedObject as? URL else { return }
    
    //  URL has only destination, so add name and extension
    let filename = String(format: "%@ Shapshot at %@",
                          (url.lastPathComponent as NSString).deletingPathExtension,
                          String.prettyStamp())
    snapshotURL.appendPathComponent(filename)
    snapshotURL = snapshotURL.appendingPathExtension("png")
    
    webView.takeSnapshot(with: nil) { image, error in
        if let image = image {
            self.webImageView.image = image
            DispatchQueue.main.async {
                self.processSnapshotImage(image, to: snapshotURL)
            }
        }
        else
        {
            self.userAlertMessage("Failed taking snapshot", info: error?.localizedDescription)
            self.webImageView.image = nil
        }
    }
}

and the capture to the targeted area

func processSnapshotImage(_ image: NSImage, to snapshotURL: URL) {
    guard let tiffData = image.tiffRepresentation else { NSSound(named: "Sosumi")?.play(); return }
    let bitmapImageRep = NSBitmapImageRep(data: tiffData)

    do
    {
        try bitmapImageRep?.representation(using: .png, properties: [:])?.write(to: snapshotURL)
        // https://developer.apple.com/library/archive/qa/qa1913/_index.html
        if let asset = NSDataAsset(name:"Grab") {

            do {
                // Use NSDataAsset's data property to access the audio file stored in Sound.
                let player = try AVAudioPlayer(data:asset.data, fileTypeHint:"caf")
                // Play the above sound file.
                player.play()
            } catch {
                print("no sound for you")
            }
        }
        if snapshotURL.hideFileExtensionInPath(), let name = snapshotURL.lastPathComponent.removingPercentEncoding {
            print("snapshot => \(name)")
        }
    } catch let error {
        appDelegate.userAlertMessage("Snapshot failed", info: error.localizedDescription)
    }
}
slashlos
  • 913
  • 9
  • 17