NSWindow view capture to image - macos

Update: Nov.6
Thanks to pointum I revised my question.
On 10.13, I'm trying to write a view snapshot function as general purpose NSView or window extension. Here's my take as a window delegate:
var snapshot : NSImage? {
get {
guard let window = self.window, let view = self.window!.contentView else { return nil }
var rect = view.bounds
rect = view.convert(rect, to: nil)
rect = window.convertToScreen(rect)
// Adjust for titlebar; kTitleUtility = 16, kTitleNormal = 22
let delta : CGFloat = CGFloat((window.styleMask.contains(.utilityWindow) ? kTitleUtility : kTitleNormal))
rect.origin.y += delta
rect.size.height += delta*2
Swift.print("rect: \(rect)")
let cgImage = CGWindowListCreateImage(rect, .optionIncludingWindow,
CGWindowID(window.windowNumber), .bestResolution)
let image = NSImage(cgImage: cgImage!, size: rect.size)
return image
}
}
to derive a 'flattened' snapshot of the window is what I'm after. Initially I'm using this image in a document icon drag.
It acts bizarrely. It seems to work initially - window in center, but subsequently the resulting image is different - smaller, especially when window is moved up or down in screen.
I think the rect capture is wrong ?

Adding to pointum's answer I came up with this:
var snapshot : NSImage? {
get {
guard let window = self.window, let view = self.window!.contentView else { return nil }
let inf = CGFloat(FP_INFINITE)
let null = CGRect(x: inf, y: inf, width: 0, height: 0)
let cgImage = CGWindowListCreateImage(null, .optionIncludingWindow,
CGWindowID(window.windowNumber), .bestResolution)
let image = NSImage(cgImage: cgImage!, size: view.bounds.size)
return image
}
}
As I only want / need a single window, specifying 'null' does the trick. Well all else fails, the docs, if you know where to look :o.

Use CGWindowListCreateImage:
let rect = /* view bounds converted to screen coordinates */
let image = CGWindowListCreateImage(rect, .optionIncludingWindow,
CGWindowID(window.windowNumber), .bestResolution)
To save the image use something like this:
let dest = CGImageDestinationCreateWithURL(url, "public.jpeg", 1, nil)
CGImageDestinationAddImage(destination, image, nil)
CGImageDestinationFinalize(destination)
Note that screen coordinates are flipped. From the docs:
The coordinates of the rectangle must be specified in screen coordinates, where the screen origin is in the upper-left corner of the main display and y-axis values increase downward

Related

Cropping NSImage with onscreen coordinates incorrect on different screen sizes

I'm trying to replicate macOS's screenshot functionality, dragging a selection onscreen to provide coordinates for cropping an image. I have it working fine on my desktop Mac (2560x1600), but testing on my laptop (2016 rMBP 15", 2880x1800), the cropped image is completely wrong. I don't understand why I'd get the right results on my desktop, but not on my laptop. I think it has something to do with the Quarts coordinates being different from Cocoa coordinates, seeing as how on the laptop, the resulting image seems like the coordinates are flipped on the Y-axis.
Here is the code I am using to generate the cropping CGRect:
# Segment used to draw the CAShapeLayer:
private func handleDragging(_ event: NSEvent) {
let mouseLoc = event.locationInWindow
if let point = self.startPoint,
let layer = self.shapeLayer {
let path = CGMutablePath()
path.move(to: point)
path.addLine(to: NSPoint(x: self.startPoint.x, y: mouseLoc.y))
path.addLine(to: mouseLoc)
path.addLine(to: NSPoint(x: mouseLoc.x, y: self.startPoint.y))
path.closeSubpath()
layer.path = path
self.selectionRect = path.boundingBox
}
}
private func startDragging(_ event: NSEvent) {
if let window = self.window,
let contentView = window.contentView,
let layer = contentView.layer,
!self.isDragging {
self.isDragging = true
self.startPoint = window.mouseLocationOutsideOfEventStream
shapeLayer = CAShapeLayer()
shapeLayer.lineWidth = 1.0
shapeLayer.fillColor = NSColor.white.withAlphaComponent(0.5).cgColor
shapeLayer.strokeColor = NSColor.systemGray.cgColor
layer.addSublayer(shapeLayer)
}
}
Then this is the code where I actually generate the screenshot and crop using the CGRect:
public func processResults(_ rect: CGRect) {
if let windowID = self.globalWindow?.windowNumber,
let screen = self.getScreenWithMouse(), rect.width > 5 && rect.height > 5 {
self.delegate?.processingResults()
let cgScreenshot = CGWindowListCreateImage(screen.frame, .optionOnScreenBelowWindow, CGWindowID(windowID), .bestResolution)
var rect2 = rect
rect2.origin.y = NSMaxY(self.getScreenWithMouse()!.frame) - NSMaxY(rect);
if let croppedCGScreenshot = cgScreenshot?.cropping(to: rect2) {
let rep = NSBitmapImageRep(cgImage: croppedCGScreenshot)
let image = NSImage()
image.addRepresentation(rep)
self.showPreviewWindow(image: image)
let requests = [self.getTextRecognitionRequest()]
let imageRequestHandler = VNImageRequestHandler(cgImage: croppedCGScreenshot, orientation: .up, options: [:])
DispatchQueue.global(qos: .userInitiated).async {
do {
try imageRequestHandler.perform(requests)
} catch let error {
print("Error: \(error)")
}
}
DispatchQueue.main.asyncAfter(deadline: .now() + 5.0) {
self.hidePreviewWindow()
}
}
}
self.globalWindow = nil
}
Not 15 minutes after I asked this question, I tried one more thing and it works!
Relevant snippet:
var correctedRect = rect
// Set the Y origin properly (counteracting the flipped Y-axis)
correctedRect.origin.y = screen.frame.height - rect.origin.y - rect.height;
// Checks if we're on another screen
if (screen.frame.origin.y < 0) {
correctedRect.origin.y = correctedRect.origin.y - screen.frame.origin.y
}
// Finally, correct the x origin (if we're on another screen, the origin will be larger than zero)
correctedRect.origin.x = correctedRect.origin.x + screen.frame.origin.x
// Generate the screenshot inside the requested rect
let cgScreenshot = CGWindowListCreateImage(correctedRect, .optionOnScreenBelowWindow, CGWindowID(windowID), .bestResolution)

How to crop an image with a selectable area in swift 4 or later?

I need some help with a function that I'd like to implement in my app.
I have a view with an image view with content mode in Aspect Fit. When I get an image from my library I would like to crop an area with an adjustable rectangle creating a new image.
I've looked for some exemple or online tutorial but I did not succeed.
Can anyone help me with that?
Here are the images from my View.
.
The simple solution is to just render the image view within a particular CGRect:
func snapshot(in imageView: UIImageView, rect: CGRect) -> UIImage {
return UIGraphicsImageRenderer(bounds: rect).image { _ in
imageView.drawHierarchy(in: imageView.bounds, afterScreenUpdates: true)
}
}
The limitation of that approach is that if the image is a considerably higher resolution than the image view could render (as is often the case when we use “aspect scale fit”), you’ll lose this additional precision.
If you want to preserve the resolution, you should convert the CGRect to coordinates with the image, in this case, assuming “aspect scale fit” (namely, centered and scaled so the whole image is shown):
func snapshot(in imageView: UIImageView, rect: CGRect) -> UIImage {
assert(imageView.contentMode == .scaleAspectFit)
let image = imageView.image!
// figure out what the scale is
let imageRatio = imageView.bounds.width / imageView.bounds.height
let imageViewRatio = image.size.width / image.size.height
let scale: CGFloat
if imageRatio > imageViewRatio {
scale = image.size.height / imageView.bounds.height
} else {
scale = image.size.width / imageView.bounds.width
}
// convert the `rect` into coordinates within the image, itself
let size = rect.size * scale
let origin = CGPoint(x: image.size.width / 2 - (imageView.bounds.midX - rect.minX) * scale,
y: image.size.height / 2 - (imageView.bounds.midY - rect.minY) * scale)
let scaledRect = CGRect(origin: origin, size: size)
// now render the image and grab the appropriate rectangle within
// the image’s coordinate system
let format = UIGraphicsImageRendererFormat()
format.scale = image.scale
format.opaque = false
return UIGraphicsImageRenderer(bounds: scaledRect, format: format).image { _ in
image.draw(at: .zero)
}
}
Using this extension:
extension CGSize {
static func * (lhs: CGSize, rhs: CGFloat) -> CGSize {
return CGSize(width: lhs.width * rhs, height: lhs.height * rhs)
}
}
That yields:
If I understand your question correctly there are two parts to your question:
An adjustable rectangle area over the image
Crop an UIImage
Break your google query and search for solution based on the above questions separately.
Or probably take help or use something like this:
iOS-Image-Crop-View

How to add UIImageViews to a UIStackView that is constrained to the centerXAnchor of a view?

I'm trying to add profile icons via UIImageViews to a UIStackView in order to keep the icons centered in a view. How would I go about adding UIImageViews of a fixed frame to a UIStackView and keep the UIStackView centered in the main view according to varying numbers of UIImageViews in the UIStackView?
let memberIcons: UIStackView = {
let iconView = UIStackView()
iconView.translatesAutoresizingMaskIntoConstraints = false
iconView.axis = .horizontal
iconView.spacing = 5
iconView.distribution = .equalSpacing
iconView.alignment = .center
return iconView
}()
for member in story!.members {
let circle = UIImageView()
circle.frame = CGRect(x: 0, y: 0, width: 36, height: 36)
circle.translatesAutoresizingMaskIntoConstraints = false
circle.layer.cornerRadius = CGFloat(circle.frame.width / 2)
circle.image = member.profilePicture
circle.contentMode = .scaleAspectFill
circle.clipsToBounds = true
memberIcons.addArrangedSubview(circle)
}
Because you set memberIcons.distribution = .equalSpace, the stack view will ask its subviews for their intrinsic sizes. When asked, the UIImage (i.e. circle) will calculate its intrinsic size as "image pixel size / scale", which is not what you want -- you want the image to be of fixed size (36 x 36).
Use Auto Layout on circle:
override func viewDidLoad() {
super.viewDidLoad()
view.addSubview(memberIcons)
memberIcons.centerXAnchor.constraint(equalTo: view.centerXAnchor).isActive = true
memberIcons.centerYAnchor.constraint(equalTo: view.centerYAnchor).isActive = true
// Limit the stack view's width to no more than 75% of the superview's width
// Adjust as needed
memberIcons.widthAnchor.constraint(lessThanOrEqualTo: view.widthAnchor, multiplier: 0.75).isActive = true
let width: CGFloat = 36.0
for member in story!.members {
// We don't care about the frame here as we're gonna use auto layout
let circle = UIImageView(frame: .zero)
circle.translatesAutoresizingMaskIntoConstraints = false
circle.layer.cornerRadius = width / 2
circle.image = member.profilePicture
circle.contentMode = .scaleAspectFill
circle.clipsToBounds = true
circle.layer.borderWidth = 1
circle.layer.borderColor = UIColor.lightGray.cgColor
memberIcons.addArrangedSubview(circle)
circle.widthAnchor.constraint(equalToConstant: width).isActive = true
circle.heightAnchor.constraint(equalToConstant: width).isActive = true
}
}
Result:
Because we limit the width of the UIStackView, there a maximum number of profile images you can add (7 in this case) before you get a bunch of auto layout error on the console. You can enclose the Stack View inside a Scroll View or use a Collection View for a matrix-like display.

how to create a zoom transition using custom navigation transition?

I'm new to navigation transition. I'm using Swift language for development.
What i'm trying to do is, whenever user click on the tableview cell the new view will open and it look like it coming from the image view of the cell.
I created a class of UIViewControllerAnimatedTransitioning.
class ThumbnailZoomTransitionAnimator: NSObject, UIViewControllerAnimatedTransitioning {
private let duration: TimeInterval = 0.5
var operation: UINavigationControllerOperation = .push
var thumbnailFrame = CGRect.zero
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return duration
}
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
let presenting = operation == .push
// Determine which is the master view and which is the detail view that we're navigating to and from. The container view will house the views for transition animation.
let containerView = transitionContext.containerView
guard let toView = transitionContext.view(forKey: UITransitionContextViewKey.to) else { return }
guard let fromView = transitionContext.view(forKey: UITransitionContextViewKey.from) else { return }
let storyFeedView = presenting ? fromView : toView
let storyDetailView = presenting ? toView : fromView
// Determine the starting frame of the detail view for the animation. When we're presenting, the detail view will grow out of the thumbnail frame. When we're dismissing, the detail view will shrink back into that same thumbnail frame.
var initialFrame = presenting ? thumbnailFrame : storyDetailView.frame
let finalFrame = presenting ? storyDetailView.frame : thumbnailFrame
// Resize the detail view to fit within the thumbnail's frame at the beginning of the push animation and at the end of the pop animation while maintaining it's inherent aspect ratio.
let initialFrameAspectRatio = initialFrame.width / initialFrame.height
let storyDetailAspectRatio = storyDetailView.frame.width / storyDetailView.frame.height
if initialFrameAspectRatio > storyDetailAspectRatio {
initialFrame.size = CGSize(width: initialFrame.height * storyDetailAspectRatio, height: initialFrame.height)
}
else {
initialFrame.size = CGSize(width: initialFrame.width, height: initialFrame.width / storyDetailAspectRatio)
}
let finalFrameAspectRatio = finalFrame.width / finalFrame.height
var resizedFinalFrame = finalFrame
if finalFrameAspectRatio > storyDetailAspectRatio {
resizedFinalFrame.size = CGSize(width: finalFrame.height * storyDetailAspectRatio, height: finalFrame.height)
}
else {
resizedFinalFrame.size = CGSize(width: finalFrame.width, height: finalFrame.width / storyDetailAspectRatio)
}
// Determine how much the detail view needs to grow or shrink.
let scaleFactor = resizedFinalFrame.width / initialFrame.width
let growScaleFactor = presenting ? scaleFactor: 1/scaleFactor
let shrinkScaleFactor = 1/growScaleFactor
if presenting {
// Shrink the detail view for the initial frame. The detail view will be scaled to CGAffineTransformIdentity below.
storyDetailView.transform = CGAffineTransform(scaleX: shrinkScaleFactor, y: shrinkScaleFactor)
storyDetailView.center = CGPoint(x: thumbnailFrame.midX, y: thumbnailFrame.midY)
storyDetailView.clipsToBounds = true
}
// Set the initial state of the alpha for the master and detail views so that we can fade them in and out during the animation.
storyDetailView.alpha = presenting ? 0 : 1
storyFeedView.alpha = presenting ? 1 : 0
// Add the view that we're transitioning to to the container view that houses the animation.
containerView.addSubview(toView)
containerView.bringSubview(toFront: storyDetailView)
// Animate the transition.
UIView.animate(withDuration: duration, delay: 0.0, usingSpringWithDamping: 1, initialSpringVelocity: 1.0, options: .curveEaseInOut, animations: {
// Fade the master and detail views in and out.
storyDetailView.alpha = presenting ? 1 : 0
storyFeedView.alpha = presenting ? 0 : 1
if presenting {
// Scale the master view in parallel with the detail view (which will grow to its inherent size). The translation gives the appearance that the anchor point for the zoom is the center of the thumbnail frame.
let scale = CGAffineTransform(scaleX: growScaleFactor, y: growScaleFactor)
let translate = CGAffineTransform(translationX: storyFeedView.frame.midX - self.thumbnailFrame.midX, y: storyFeedView.frame.midY - self.thumbnailFrame.midY)
storyFeedView.transform = translate.concatenating(scale)
storyDetailView.transform = .identity
}
else {
// Return the master view to its inherent size and position and shrink the detail view.
storyFeedView.transform = .identity
storyDetailView.transform = CGAffineTransform(scaleX: shrinkScaleFactor, y: shrinkScaleFactor)
}
// Move the detail view to the final frame position.
storyDetailView.center = CGPoint(x: finalFrame.midX, y: finalFrame.midY)
}) { finished in
transitionContext.completeTransition(finished)
}
}
}
When i click on the cell then new controller open. This functionality is working correctly. But whenever i'm going back then my previous controller get disappear.
Is i'm doing something wrong here ,
// Animate the transition.
UIView.animate(withDuration: duration, delay: 0.0, usingSpringWithDamping: 1, initialSpringVelocity: 1.0, options: .curveEaseInOut, animations: {
// Fade the master and detail views in and out.
storyDetailView.alpha = presenting ? 1 : 0
storyFeedView.alpha = presenting ? 0 : 1
if presenting {
// Scale the master view in parallel with the detail view (which will grow to its inherent size). The translation gives the appearance that the anchor point for the zoom is the center of the thumbnail frame.
let scale = CGAffineTransform(scaleX: growScaleFactor, y: growScaleFactor)
let translate = CGAffineTransform(translationX: storyFeedView.frame.midX - self.thumbnailFrame.midX, y: storyFeedView.frame.midY - self.thumbnailFrame.midY)
storyFeedView.transform = translate.concatenating(scale)
storyDetailView.transform = .identity
}
else {
// Return the master view to its inherent size and position and shrink the detail view.
storyFeedView.transform = .identity
storyDetailView.transform = CGAffineTransform(scaleX: shrinkScaleFactor, y: shrinkScaleFactor)
}
// Move the detail view to the final frame position.
storyDetailView.center = CGPoint(x: finalFrame.midX, y: finalFrame.midY)
}) { finished in
transitionContext.completeTransition(finished)
}
I'm trying to Scale the master view in parallel with the detail view (which will grow to its inherent size). The translation gives the appearance that the anchor point for the zoom is the center of the thumbnail frame.
It will give zoomIn ZoomOut effect when user go back to previous controller. But When i trying to do this then my previous controller disappear.
I wanted to do like zoom transition like apple do in their apps.

Resize image in NSTextView to fit

I have NSAttributedString objects with embedded images. These are being presented in NSTextViews. In iOS, I was able to resize the bounds of NSTextAttachment, and this makes the image fit.
extension NSTextAttachment {
func setImageWidth(width: CGFloat, range: NSRange) {
var thisImage = image
if thisImage == nil {
thisImage = imageForBounds(bounds, textContainer: nil, characterIndex: range.location)
}
if thisImage != nil {
let ratio = thisImage!.size.height / thisImage!.size.width
bounds = CGRectMake(bounds.origin.x, bounds.origin.y, width, ratio * width)
print("New Bounds: \(bounds)")
}
}
}
This code also runs on OSX, but it does not actually resize the image. Below you can see, there is a box of the correct size around the image, but the actual image overflows the box.
I have also followed the following guide: Implementing Rich Text with Images on OS X and iOS. This moves the code to subclasses, but has the same effect.
Any suggestions? Is there something besides NSTextAttachment.bounds that I should be adjusting?
UPDATE
I found that modifying the size component of NSImage works! However, it is now showing all my images upside, but at the correct size. :(
Solved!
extension NSImage {
func resizeToFit(containerWidth: CGFloat) {
var scaleFactor : CGFloat = 1.0
let currentWidth = self.size.width
let currentHeight = self.size.height
if currentWidth > containerWidth {
scaleFactor = (containerWidth * 0.9) / currentWidth
}
let newWidth = currentWidth * scaleFactor
let newHeight = currentHeight * scaleFactor
self.size = NSSize(width: newWidth, height: newHeight)
print("Size: \(size)")
}
}
As I mentioned in the update, you need to change the NSImage.size. The flip was coming from one of the subclasses I had left in there from the link in the question. Once I went back to the main classes, it works!

Resources