Search code examples
swiftcocoaswift5

Cocoa: Capture Screen and scale image on saving in Swift


Below code I am using to capture screen in macOS application,

let img = CGDisplayCreateImage(CGMainDisplayID())

guard let destination = FileManager.default.urls(for: .downloadsDirectory,
    in: .userDomainMask).first?.appendingPathComponent("shot.jpg", isDirectory: false)
else {
    print("Unable to save captured image!")
    return
}
            
let properties: CFDictionary = [
    kCGImagePropertyPixelWidth: "900",
    kCGImagePropertyPixelHeight: "380"
] as CFDictionary
            
if let dest = CGImageDestinationCreateWithURL(destination as CFURL, kUTTypeJPEG, 1, properties) {
    CGImageDestinationAddImage(dest, img!, properties)
    CGImageDestinationFinalize(dest)
}
else {
    print("Unable to create captured image to the destination!")
}

I have to scale the image to particular size while saving. So, I used CFDictionary with width, heigh properties of the image. But It's seems I am doing it as wrong. Please help me to find out correct solution. Thank you!


Solution

  • First, you can't resize using CGImageDestinationCreateWithURL or CGImageDestinationAddImage. If you look at the docs here and here you will notice that neither kCGImagePropertyPixelWidth or kCGImagePropertyPixelHeight is supported.

    You will need to resize manually. You can use this tool, or modify it, if you find it helpful. It supports fill (stretch) and fit (scale while keeping the original aspect ratio) content modes. If you specify .fit it will center the drawing in the resulting image. If you specify .fill it will fill the whole space stretching whichever dimension it needs to.

    enum ImageResizer {
    
        enum ContentMode {
            case fill
            case fit
        }
    
        enum Error: Swift.Error {
            case badOriginal
            case resizeFailed
        }
    
        static func resize(_ source: CGImage, to targetSize: CGSize, mode: ContentMode) throws -> CGImage {
    
            let context = CGContext(
                data: nil,
                width: Int(targetSize.width),
                height: Int(targetSize.height),
                bitsPerComponent: source.bitsPerComponent,
                bytesPerRow: 0,
                space: source.colorSpace ?? CGColorSpace(name: CGColorSpace.sRGB)!,
                bitmapInfo: source.bitmapInfo.rawValue
            )
    
            guard let context = context else {
                throw Error.badOriginal
            }
    
            let drawingSize: CGSize
            switch mode {
            case .fill:
                drawingSize = targetSize
            case .fit:
                drawingSize = CGSize(width: source.width, height: source.height)
                    .scaledToFit(target: targetSize)
            }
    
            let drawRect = CGRect(origin: .zero, size: targetSize)
                .makeCenteredRect(withSize: drawingSize)
    
            context.interpolationQuality = .high
            context.draw(source, in: drawRect)
    
            guard let result = context.makeImage() else {
                throw Error.resizeFailed
            }
    
            return result
        }
    }
    

    ImageResizer depends on these CG extensions for scaling the source image and centering scaled image:

    extension CGSize {
    
        var maxDimension: CGFloat {
            Swift.max(width, height)
        }
    
        var minDimension: CGFloat {
            Swift.min(width, height)
        }
    
        func scaled(by scalar: CGFloat) -> CGSize {
            CGSize(width: width * scalar, height: height * scalar)
        }
    
        func scaleFactors(to target: CGSize) -> CGSize {
            CGSize(
                width: target.width / width,
                height: target.height / height
            )
        }
    
        func scaledToFit(target: CGSize) -> CGSize {
            return scaled(by: scaleFactors(to: target).minDimension)
        }
    }
    
    extension CGRect {
        func makeCenteredRect(withSize size: CGSize) -> CGRect {
            let origin = CGPoint(
                x: midX - size.width / 2.0,
                y: midY - size.height / 2.0
            )
            return CGRect(origin: origin, size: size)
        }
    }
    

    Also, make sure you set up permissions if you're going to save to .downloadsDirectory.