AppleScriptObjC: Saving an NSVisualEffectView to PNG

I have an AppleScriptObjC applet that imports an image into an NSImageView, overlays an NSVisualEffectView, and then adds both views to an NSView. This all works fine. My problem arises when attempting to save the NSView to disk as a PNG; using NSBitmapImageRep a PNG is created but it only contains the NSImage with no NSVisualEffectView overlay. I’m assuming that using NSBitmapImageRep is the root of the problem, but despite much Googling I’m stumped as to how to successfully export an NSView that includes an NSVisualEffectView.

This is the handler I’m currently using to save the NSView (named “theContainerView”):

on saveImage()
	theContainerView's lockFocus()
	set theRep to (NSBitmapImageRep's alloc()'s initWithFocusedViewRect:{{0, 0}, {600, 600}})
	theContainerView's unlockFocus()
	set theData to (theRep's representationUsingType:NSPNGFileType |properties|:(missing value))
	set classicPath to (choose file name default location (path to desktop) default name "Saved.png")
	set posixPath to POSIX path of classicPath
	theData's writeToFile:posixPath atomically:true
end saveImage

Any help would be much appreciated.

Given that NSVisualEffectView is for interface effects, it may well be that there’s no obvious way to do what you want.

What kind of effect are you creating that requires an NSVisualEffectView?

Thanks for your reply, Shane. I’m using NSVisualEffectView to overlay a semi-transparent blurred band across an image, with the band containing text that has NSAppearance properties applied to it. Ultimately I want to export the NSView as a bitmap image that I can then use in a design project. Here’s my work-in-progress script:

use AppleScript version "2.4"
use scripting additions
use framework "Foundation"
use framework "AppKit"

property NSFont : a reference to current application's NSFont
property NSView : a reference to current application's NSView
property NSColor : a reference to current application's NSColor
property NSImage : a reference to current application's NSImage
property NSButton : a reference to current application's NSButton
property NSWindow : a reference to current application's NSWindow
property NSTextView : a reference to current application's NSTextView
property NSImageView : a reference to current application's NSImageView
property NSAppearance : a reference to current application's NSAppearance
property NSPNGFileType : a reference to current application's NSPNGFileType
property NSVisualEffectView : a reference to current application's NSVisualEffectView
property NSBitmapImageRep : a reference to current application's NSBitmapImageRep
property NSFontWeightHeavy : a reference to current application's NSFontWeightHeavy
property NSTextAlignmentCenter : a reference to current application's NSTextAlignmentCenter
property NSBackingStoreBuffered : a reference to current application's NSBackingStoreBuffered

property theContainerView : missing value

# define image
set theImagePath to POSIX path of (path to desktop folder) & "Sunflower.png"
set theImage to (NSImage's alloc()'s initWithContentsOfFile:theImagePath)

# define image view
set theImageView to (NSImageView's alloc()'s initWithFrame:{{0, 0}, {600, 600}})
theImageView's setImage:theImage

# define text view
set theTextView to (NSTextView's alloc's initWithFrame:{{10, 0}, {580, 105}})
theTextView's setFont:(NSFont's systemFontOfSize:80 weight:NSFontWeightHeavy)
theTextView's setAlignment:NSTextAlignmentCenter
theTextView's setTextColor:(NSColor's tertiaryLabelColor)
theTextView's setAppearance:(NSAppearance's vibrantLightAppearance)
theTextView's setAllowsVibrancy:true
theTextView's setBackgroundColor:(NSColor's clearColor)
theTextView's setString:"Sunflower"
theTextView's setEditable:false

# define overlay view & add text view
set theOverlayView to (NSVisualEffectView's alloc()'s initWithFrame:{{0, 250}, {600, 100}})
theOverlayView's setBlendingMode:1
theOverlayView's setMaterial:1 # light material
theOverlayView's setState:1
theOverlayView's addSubview:theTextView

# define container view & add image view & overlay view
set theContainerView to (NSView's alloc()'s initWithFrame:{{0, 38}, {600, 600}})
theContainerView's addSubview:theImageView
theContainerView's addSubview:theOverlayView

# define 'Save Image' button
set theButton to (NSButton's alloc()'s initWithFrame:{{246, 5}, {110, 25}})
theButton's setTitle:"Save Image"
theButton's setBezelStyle:1
theButton's setTarget:me
theButton's setAction:"saveImage"

# define window & add container view & button view
set theWindow to (NSWindow's alloc()'s initWithContentRect:{{0, 0}, {600, 638}} styleMask:3 backing:NSBackingStoreBuffered defer:true)
theWindow's |center|()
theWindow's setTitle:"Sunflower"
theWindow's contentView()'s addSubview:theContainerView
theWindow's contentView()'s addSubview:theButton
theWindow's orderFrontRegardless()
theWindow's makeKeyAndOrderFront:me

on saveImage()
	theContainerView's lockFocus()
	set theRep to (NSBitmapImageRep's alloc()'s initWithFocusedViewRect:{{0, 0}, {600, 600}})
	theContainerView's unlockFocus()
	set theData to (theRep's representationUsingType:NSPNGFileType |properties|:(missing value))
	set classicPath to (choose file name default location (path to desktop) default name "Saved.png")
	set posixPath to POSIX path of classicPath
	theData's writeToFile:posixPath atomically:true
end saveImage

Up to now I’ve been cheating by simply taking a screenshot, but I was wondering/hoping that there was a more legitimate/efficient way of saving the NSView and its contents as an image.

1 Like

I wonder if your problem is that you’re not actually drawing your view, so the effects never get applied to the image.

I did some change in your code and use pdf as output… maybe not what your want but I was able to output a image from NSView… from the NSVisualEffectView method you used.

use AppleScript version "2.4"
use scripting additions
use framework "Foundation"
use framework "AppKit"

property NSFont : a reference to current application's NSFont
property NSView : a reference to current application's NSView
property NSColor : a reference to current application's NSColor
property NSImage : a reference to current application's NSImage
property NSButton : a reference to current application's NSButton
property NSWindow : a reference to current application's NSWindow
property NSTextView : a reference to current application's NSTextView
property NSImageView : a reference to current application's NSImageView
property NSAppearance : a reference to current application's NSAppearance
property NSPNGFileType : a reference to current application's NSPNGFileType
property NSVisualEffectView : a reference to current application's NSVisualEffectView
property NSBitmapImageRep : a reference to current application's NSBitmapImageRep
property NSFontWeightHeavy : a reference to current application's NSFontWeightHeavy
property NSTextAlignmentCenter : a reference to current application's NSTextAlignmentCenter
property NSBackingStoreBuffered : a reference to current application's NSBackingStoreBuffered

property theContainerView : missing value

# define image
set theImagePath to POSIX path of (path to desktop folder) & "Sunflower.png"
set theImage to (NSImage's alloc()'s initWithContentsOfFile:theImagePath)

# define image view
set theImageView to (NSImageView's alloc()'s initWithFrame:{{0, 0}, {600, 600}})
theImageView's setImage:theImage

# define text view
set theTextView to (NSTextView's alloc's initWithFrame:{{10, 0}, {580, 105}})
theTextView's setFont:(NSFont's systemFontOfSize:80 weight:NSFontWeightHeavy)
theTextView's setAlignment:NSTextAlignmentCenter
theTextView's setTextColor:(NSColor's tertiaryLabelColor)
theTextView's setAppearance:(NSAppearance's vibrantLightAppearance)
theTextView's setAllowsVibrancy:true
theTextView's setBackgroundColor:(NSColor's clearColor)
theTextView's setString:"Sunflower"
theTextView's setEditable:false

# define overlay view & add text view
set theOverlayView to (NSVisualEffectView's alloc()'s initWithFrame:{{0, 250}, {600, 100}})
theOverlayView's setBlendingMode:(current application's NSVisualEffectBlendingModeBehindWindow)
theOverlayView's setMaterial:1 # light material
theOverlayView's addSubview:theTextView

# define container view & add image view & overlay view
set theContainerView to (NSView's alloc()'s initWithFrame:{{0, 38}, {600, 600}})
theContainerView's addSubview:theImageView
theContainerView's addSubview:theOverlayView

# define 'Save Image' button
set theButton to (NSButton's alloc()'s initWithFrame:{{246, 5}, {110, 25}})
theButton's setTitle:"Save Image"
theButton's setBezelStyle:1
theButton's setTarget:me
theButton's setAction:"saveImage:"

# define window & add container view & button view
set theWindow to (NSWindow's alloc()'s initWithContentRect:{{0, 0}, {600, 638}} styleMask:3 backing:NSBackingStoreBuffered defer:true)
theWindow's |center|()
theWindow's setTitle:"Sunflower"
theWindow's contentView()'s addSubview:theContainerView
theWindow's contentView()'s addSubview:theButton
theWindow's orderFrontRegardless()
theWindow's makeKeyAndOrderFront:me

on saveImage:sender
	-- theContainerView's lockFocus()
	-- set theRep to (NSBitmapImageRep's alloc()'s initWithFocusedViewRect:{{0, 0}, {600, 600}})
	-- theContainerView's unlockFocus()
	set theData to theContainerView's dataWithPDFInsideRect:(theContainerView's |bounds|())
	theData's writeToFile:(POSIX path of (path to desktop) & "saved.pdf") atomically:true
end saveImage:

Thanks for your reply, Fredrik.

I notice that you changed the NSVisualEffectBlendingMode to BehindWindow; however this needs to be set to WithinWindow for the NSVisualEffectView to be applied to the image rather than the window’s background, and doing so produces a PDF that, like my own attempt, is missing the text and NSVisualEffectView overlay.

If I run your script as is, I get a PDF that does have the text (although without the NSAppearance effect), but is still missing the NSVisualEffectView overlay.

I’m beginning to think that perhaps Shane is correct in his theory that the NSVisualEffectView is never actually applied to the image and exists only as interface “decoration” as it were, and thus cannot be written to a file. In my research I came across a StackOverflow post that said the following:

The problem with NSVisualEffectView is that it does not draw anything. Instead, it defines a region that tells the WindowServer process to do its vibrancy stuff in that region. This happens after your app draws its views. Therefore a compositing operation in your app cannot take into account the pixels that the window server draws later. In short, this cannot be done.

So… case closed?

Just a digression from the problem you’ve already outlined: I was getting the error “NSWindow drag regions should only be invalidated on the Main Thread!” and was able to fix the issue by tweaking the code with a try statement and enclosing the view hierarchy code within a handler

***
property params : missing value

if current application's NSThread's isMainThread() as boolean then
	my runCode:params
else
	my performSelectorOnMainThread:"runCode:" withObject:params waitUntilDone:true
end if

on runCode:params
        # define image, imageView, textView, overlay, button, container, window, etc as above
end runCode:

on saveImage:sender
   ***
end saveImage:

it’s quite possible that my suggestion will totally miss the point is isn’t applicable in this case, but:

can you maybe just take a screenshot of whatever you draw using the screencapture shell command and save it to file?