SwiftUI Image Processing (Zoom, Puzzle)

SwiftUI Image Processing (Zoom, Puzzle)

Using SwiftUI Core Graphics technology, similar to GDI+ drawing in C#

Last updated 8/21/2021 11:44 PM
沙漠尽头的狼
10 min read
Category
Swift UI
Tags
.NET C# puzzle zoom SwiftUI

Using SwiftUI Core Graphics technology, similar to C#'s GDI+ drawing, I won't go into specifics since I'm also a beginner. This article mainly shows screenshots and code. If you need the example code from this article, please scroll to the end to get it.

1、Image Scaling

  1. Full fill, deformed compression
  2. Center-crop scaling
  3. Proportional scaling

It's better to compare the three effects side by side, as shown below:

Original - Full fill deformed compression - Center-crop scaling - Proportional scaling

  1. First image: original
  2. Second image: full fill, deformed compression
  3. Third image: center-crop scaling
  4. Fourth image: proportional scaling

Images before and after scaling in the example can be exported.

2、Image Stitching

As the name suggests, combining multiple images into one. Below are the original beautiful images:

Multiple beautiful original images

After selection, preview in the interface:

Preview in the interface

Export the stitched image to see the effect:

Exported stitched image

3、Image Operation Methods

Finally, the code for image scaling and stitching:

import SwiftUI

struct ImageHelper {

    static let shared = ImageHelper()
    private init() {}

    // NSView to NSImage
    func imageFromView(cview: NSView) -> NSImage? {
        guard let bitmap: NSBitmapImageRep = cview.bitmapImageRepForCachingDisplay(in: cview.visibleRect) else { return nil }
        cview.cacheDisplay(in: cview.visibleRect, to: bitmap)
        let image: NSImage = NSImage(size: cview.frame.size)
        image.addRepresentation(bitmap)
        return image
    }

    // Save image to local file
    func saveImage(image: NSImage, fileName: String) -> Bool {
        guard var imageData = image.tiffRepresentation,
              let imageRep = NSBitmapImageRep(data: imageData) else { return false }
        if fileName.hasSuffix("jpg") {
            let quality: NSNumber = 0.85
            imageData = imageRep.representation(using: .jpeg, properties: [.compressionFactor: quality])!
        } else {
            imageData = imageRep.representation(using: .png, properties: [:])!
        }
        do {
            try imageData.write(to: URL(fileURLWithPath: fileName), options: .atomic)
            return true
        } catch {
            return false
        }
    }

    // Compress image by ratio
    func compressedImageDataWithImg(image: NSImage, rate: CGFloat) -> NSData? {
        guard let imageData = image.tiffRepresentation,
              let imageRep = NSBitmapImageRep(data: imageData) else { return nil }
        guard let data: Data = imageRep.representation(using: .jpeg, properties: [.compressionFactor: rate]) else { return nil }
        return data as NSData
    }

    // Full fill, deformed compression
    func resizeImage(sourceImage: NSImage, forSize size: NSSize) -> NSImage {
        let targetFrame: NSRect = NSMakeRect(0, 0, size.width, size.height)
        let sourceImageRep: NSImageRep = sourceImage.bestRepresentation(for: targetFrame, context: nil, hints: nil)!
        let targetImage: NSImage = NSImage(size: size)
        targetImage.lockFocus()
        sourceImageRep.draw(in: targetFrame)
        targetImage.unlockFocus()
        return targetImage
    }

    // Center-crop scaling to target size
    func resizeImage1(sourceImage: NSImage, forSize targetSize: CGSize) -> NSImage {
        let imageSize: CGSize = sourceImage.size
        let width: CGFloat = imageSize.width
        let height: CGFloat = imageSize.height
        let targetWidth: CGFloat = targetSize.width
        let targetHeight: CGFloat = targetSize.height
        var scaleFactor: CGFloat = 0.0
        let widthFactor: CGFloat = targetWidth / width
        let heightFactor: CGFloat = targetHeight / height
        scaleFactor = (widthFactor > heightFactor) ? widthFactor : heightFactor
        let readHeight: CGFloat = targetHeight / scaleFactor
        let readWidth: CGFloat = targetWidth / scaleFactor
        let readPoint: CGPoint = CGPoint(x: widthFactor > heightFactor ? 0 : (width - readWidth) * 0.5,
                                         y: widthFactor < heightFactor ? 0 : (height - readHeight) * 0.5)
        let newImage: NSImage = NSImage(size: targetSize)
        let thumbnailRect: CGRect = CGRect(x: 0, y: 0, width: targetSize.width, height: targetSize.height)
        let imageRect: NSRect = NSRect(x: readPoint.x, y: readPoint.y, width: readWidth, height: readHeight)
        newImage.lockFocus()
        sourceImage.draw(in: thumbnailRect, from: imageRect, operation: .copy, fraction: 1.0)
        newImage.unlockFocus()
        return newImage
    }

    // Proportional scaling
    func resizeImage2(sourceImage: NSImage, forSize targetSize: CGSize) -> NSImage {
        let imageSize: CGSize = sourceImage.size
        let width: CGFloat = imageSize.width
        let height: CGFloat = imageSize.height
        let targetWidth: CGFloat = targetSize.width
        let targetHeight: CGFloat = targetSize.height
        var scaleFactor: CGFloat = 0.0
        var scaledWidth: CGFloat = targetWidth
        var scaledHeight: CGFloat = targetHeight
        var thumbnailPoint: CGPoint = CGPoint(x: 0.0, y: 0.0)
        if __CGSizeEqualToSize(imageSize, targetSize) == false {
            let widthFactor: CGFloat = targetWidth / width
            let heightFactor: CGFloat = targetHeight / height
            scaleFactor = (widthFactor > heightFactor) ? widthFactor : heightFactor
            scaledWidth = ceil(width * scaleFactor)
            scaledHeight = ceil(height * scaleFactor)
            if widthFactor > heightFactor {
                thumbnailPoint.y = (targetHeight - scaledHeight) * 0.5
            } else if widthFactor < heightFactor {
                thumbnailPoint.x = (targetWidth - scaledWidth) * 0.5
            }
        }
        let newImage: NSImage = NSImage(size: NSSize(width: scaledWidth, height: scaledHeight))
        let thumbnailRect: CGRect = CGRect(x: thumbnailPoint.x, y: thumbnailPoint.y, width: scaledWidth, height: scaledHeight)
        let imageRect: NSRect = NSRect(x: 0.0, y: 0.0, width: width, height: height)
        newImage.lockFocus()
        sourceImage.draw(in: thumbnailRect, from: imageRect, operation: .copy, fraction: 1.0)
        newImage.unlockFocus()
        return newImage
    }

    // Compress image data to a specified size (KB)
    func compressImgData(imgData: NSData, toAimKB aimKB: NSInteger) -> NSData? {
        let aimRate: CGFloat = CGFloat(aimKB * 1000) / CGFloat(imgData.length)
        let imageRep: NSBitmapImageRep = NSBitmapImageRep(data: imgData as Data)!
        guard let data: Data = imageRep.representation(using: .jpeg, properties: [.compressionFactor: aimRate]) else { return nil }
        print("Final data size: \(CGFloat(data.count) / 1000), compression ratio: \(CGFloat(data.count) / CGFloat(imgData.length))")
        return data as NSData
    }

    // Combine multiple images into one
    func jointedImageWithImages(imgArray: [NSImage]) -> NSImage {
        var imgW: CGFloat = 0
        var imgH: CGFloat = 0
        for img in imgArray {
            imgW += img.size.width
            if imgH < img.size.height {
                imgH = img.size.height
            }
        }
        print("size: \(NSStringFromSize(NSSize(width: imgW, height: imgH)))")
        let togetherImg: NSImage = NSImage(size: NSSize(width: imgW, height: imgH))
        togetherImg.lockFocus()
        let imgContext: CGContext? = NSGraphicsContext.current?.cgContext
        var imgX: CGFloat = 0
        for imgItem in imgArray {
            if let img = imgItem as? NSImage {
                let imageRef: CGImage = self.getCGImageRefFromNSImage(image: img)!
                imgContext?.draw(imageRef, in: NSRect(x: imgX, y: 0, width: img.size.width, height: img.size.height))
                imgX += img.size.width
            }
        }
        togetherImg.unlockFocus()
        return togetherImg
    }

    // NSImage to CGImageRef
    func getCGImageRefFromNSImage(image: NSImage) -> CGImage? {
        let imageData: NSData? = image.tiffRepresentation as NSData?
        var imageRef: CGImage? = nil
        if let data = imageData {
            let imageSource: CGImageSource = CGImageSourceCreateWithData(data as CFData, nil)!
            imageRef = CGImageSourceCreateImageAtIndex(imageSource, 0, nil)
        }
        return imageRef
    }

    // CGImage to NSImage
    func getNSImageWithCGImageRef(imageRef: CGImage) -> NSImage? {
        return NSImage(cgImage: imageRef, size: NSSize(width: imageRef.width, height: imageRef.height))
    }

    // NSImage to CIImage
    func getCIImageWithNSImage(image: NSImage) -> CIImage? {
        guard let imageData = image.tiffRepresentation,
              let imageRep = NSBitmapImageRep(data: imageData) else { return nil }
        let ciImage: CIImage = CIImage(bitmapImageRep: imageRep)!
        let affineTransform: NSAffineTransform = NSAffineTransform()
        affineTransform.translateX(by: 0, yBy: 128)
        affineTransform.scaleX(by: 1, yBy: -1)
        let transform = CIFilter(name: "CIAffineTransform")!
        transform.setValue(ciImage, forKey: "inputImage")
        transform.setValue(affineTransform, forKey: "inputTransform")
        let result: CIImage? = transform.value(forKey: "outputImage") as? CIImage
        return result
    }
}

4、Sample Code

Interface layout and effect display code:

import SwiftUI

struct TestImageDemo: View {
    @State private var sourceImagePath: String?
    @State private var sourceImage: NSImage?
    @State private var sourceImageWidth: CGFloat = 0
    @State private var sourceImageHeight: CGFloat = 0
    @State private var resizeImage: NSImage?
    @State private var resizeImageWidth: String = "250"
    @State private var resizeImageHeight: String = "250"
    @State private var resize1Image: NSImage?
    @State private var resize1ImageWidth: String = "250"
    @State private var resize1ImageHeight: String = "250"
    @State private var resize2Image: NSImage?
    @State private var resize2ImageWidth: String = "250"
    @State private var resize2ImageHeight: String = "250"
    @State private var joinImage: NSImage?
    var body: some View {
        GeometryReader { reader in
            VStack {
                HStack {
                    Button("Select and display image scaling") { self.choiceResizeImage() }
                    Button("Select and display image stitching") { self.choiceJoinImage() }
                    Spacer()
                }
                HStack {
                    VStack {
                        if let sImage = sourceImage {
                            Section(header: Text("Original")) {
                                Image(nsImage: sImage)
                                    .resizable().aspectRatio(contentMode: .fit)
                                    .frame(width: reader.size.width / 2)
                                Text("\(self.sourceImageWidth)*\(self.sourceImageHeight)")
                                Button("Export") { self.saveImage(image: sImage) }
                            }
                        }
                        if let sImage = self.joinImage {
                            Section(header: Text("Stitched")) {
                                Image(nsImage: sImage)
                                    .resizable().aspectRatio(contentMode: .fit)
                                    .frame(width: reader.size.width)
                                Button("Export") { self.saveImage(image: sImage) }
                            }
                        }
                    }
                    VStack {
                        Section(header: Text("Full fill, deformed compression")) {
                            VStack {
                                Section(header: Text("Width:")) {
                                    TextField("Width", text: self.$resizeImageWidth)
                                }
                                Section(header: Text("Height:")) {
                                    TextField("Height", text: self.$resizeImageHeight)
                                }
                                if let sImage = resizeImage {
                                    Image(nsImage: sImage)
                                    Text("\(self.resizeImageWidth)*\(self.resizeImageHeight)")
                                    Button("Export") { self.saveImage(image: sImage) }
                                }
                            }
                        }
                    }
                    VStack {
                        Section(header: Text("Center-crop scaling")) {
                            VStack {
                                Section(header: Text("Width:")) {
                                    TextField("Width", text: self.$resize1ImageWidth)
                                }
                                Section(header: Text("Height:")) {
                                    TextField("Height", text: self.$resize1ImageHeight)
                                }
                                if let sImage = resize1Image {
                                    Image(nsImage: sImage)
                                    Text("\(self.resize1ImageWidth)*\(self.resize1ImageHeight)")
                                    Button("Export") { self.saveImage(image: sImage) }
                                }
                            }
                        }
                    }
                    VStack {
                        Section(header: Text("Proportional scaling")) {
                            VStack {
                                Section(header: Text("Width:")) {
                                    TextField("Width", text: self.$resize2ImageWidth)
                                }
                                Section(header: Text("Height:")) {
                                    TextField("Height", text: self.$resize2ImageHeight)
                                }
                                if let sImage = resize2Image {
                                    Image(nsImage: sImage)
                                    Text("\(self.resize2ImageWidth)*\(self.resize2ImageHeight)")
                                    Button("Export") { self.saveImage(image: sImage) }
                                }
                            }
                        }
                    }
                    Spacer()
                }
                Spacer()
            }
        }
    }

    private func choiceResizeImage() {
        let result: (fail: Bool, url: [URL?]?) = DialogProvider.shared.showOpenFileDialog(title: "", prompt: "", message: "Select image", directoryURL: URL(fileURLWithPath: ""), allowedFileTypes: ["png", "jpg", "jpeg"])
        if result.fail { return }
        if let urls = result.url, let url = urls[0] {
            self.sourceImagePath = url.path
            self.sourceImage = NSImage(contentsOf: URL(fileURLWithPath: self.sourceImagePath!))
            self.sourceImageWidth = (self.sourceImage?.size.width)!
            self.sourceImageHeight = (self.sourceImage?.size.height)!
            if let resizeWidth = Int(self.resizeImageWidth), let resizeHeight = Int(self.resizeImageHeight) {
                self.resizeImage = ImageHelper.shared.resizeImage(sourceImage: self.sourceImage!, forSize: CGSize(width: resizeWidth, height: resizeHeight))
            }
            if let resize1Width = Int(self.resize1ImageWidth), let resize1Height = Int(self.resize1ImageHeight) {
                self.resize1Image = ImageHelper.shared.resizeImage1(sourceImage: self.sourceImage!, forSize: CGSize(width: resize1Width, height: resize1Height))
            }
            if let resize2Width = Int(self.resize2ImageWidth), let resize2Height = Int(self.resize2ImageHeight) {
                self.resize2Image = ImageHelper.shared.resizeImage2(sourceImage: self.sourceImage!, forSize: CGSize(width: resize2Width, height: resize2Height))
            }
        }
    }

    private func choiceJoinImage() {
        let result: (fail: Bool, url: [URL?]?) = DialogProvider.shared.showOpenFileDialog(title: "", prompt: "", message: "Select images", directoryURL: URL(fileURLWithPath: ""), allowedFileTypes: ["png", "jpg", "jpeg"], allowsMultipleSelection: true)
        if result.fail { return }
        if let urls = result.url {
            var imgs: [NSImage] = []
            for url in urls {
                if let filePath = url?.path {
                    imgs.append(NSImage(contentsOf: URL(fileURLWithPath: filePath))!)
                }
            }
            if imgs.count > 0 {
                self.joinImage = ImageHelper.shared.jointedImageWithImages(imgArray: imgs)
            }
        }
    }

    private func saveImage(image: NSImage) {
        let result: (isOpenFail: Bool, url: URL?) = DialogProvider.shared.showSaveDialog(
            title: "Choose image save path",
            directoryURL: URL(fileURLWithPath: ""),
            prompt: "",
            message: "",
            allowedFileTypes: ["png"]
        )
        if result.isOpenFail || result.url == nil || result.url!.path.isEmpty { return }
        let exportImagePath = result.url!.path
        _ = ImageHelper.shared.saveImage(image: image, fileName: exportImagePath)
        NSWorkspace.shared.activateFileViewerSelecting([URL(fileURLWithPath: exportImagePath)])
    }
}

struct TestImageDemo_Previews: PreviewProvider {
    static var previews: some View {
        TestImageDemo()
    }
}

5、Conclusion

All code has been posted and uploaded to GitHub, see the notes below.


Example code for this article: https://github.com/dotnet9/MacTest/blob/main/src/macos_test/macos_test/TestImageDemo.swift

Reference article title: MAC Image NSImage Scaling, Combination, Compression and Conversion between CIImageRef and NSImage

Reference article link: https://www.freesion.com/article/774352759/

Keep Exploring

Related Reading

More Articles