Just a couple of weeks ago I wrote about resizing images on iOS: Resizing UIImages with aspect fill on iOS. As I am currently building a macOS, iOS, watchOS app I realized that I need a multiplatform image resizer. As this app deals with full size photos then I need to resize those photos for avoiding excessive memory usage. Apple’s CoreGraphics framework provides APIs what are compatible with all the before mentioned platforms. Therefore, let’s revisit the image scaler created in Resizing UIImages with aspect fill on iOS and let’s refactor it to use purely multiplatform supported CGImages.
We’ll skip the part which discusses calculating a rectangle in the original image which is then resized for assuring the image is resized equally on both axes. It is important because the aspect ratio of the original image most of the time does not equal to the aspect ratio of the target size. Let’s now take a look at cropping and resizing the original image. Cropping is easy to do because CGImage already has a convenient cropping(to:) method. On the other hand, resizing requires setting up a CGContext with the same parameters as the cropped image but the pixel size must be set to the target size. When we now draw the cropped image to the context, then CoreGraphics takes care of resizing the image.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import CoreGraphics | |
struct ImageScaler { | |
static func scaleToFill(_ image: CGImage, from fromRect: CGRect = .zero, in targetSize: CGSize) -> CGImage? { | |
let imageSize = CGSize(width: image.width, height: image.height) | |
let rect = fromRect.isEmpty ? CGRect(origin: .zero, size: imageSize) : fromRect | |
let scaledRect = rect.scaled(toFillSize: targetSize) | |
return scale(image, fromRect: scaledRect, in: targetSize) | |
} | |
private static func scale(_ image: CGImage, fromRect: CGRect = .zero, in targetSize: CGSize) -> CGImage? { | |
let makeCroppedCGImage: (CGImage) -> CGImage? = { cgImage in | |
guard !fromRect.isEmpty else { return cgImage } | |
return cgImage.cropping(to: fromRect) | |
} | |
guard let croppedImage = makeCroppedCGImage(image) else { return nil } | |
let context = CGContext(data: nil, | |
width: Int(targetSize.width), | |
height: Int(targetSize.height), | |
bitsPerComponent: croppedImage.bitsPerComponent, | |
bytesPerRow: croppedImage.bytesPerRow, | |
space: croppedImage.colorSpace ?? CGColorSpace(name: CGColorSpace.sRGB)!, | |
bitmapInfo: croppedImage.bitmapInfo.rawValue) | |
context?.interpolationQuality = .high | |
context?.draw(croppedImage, in: CGRect(origin: .zero, size: targetSize)) | |
return context?.makeImage() | |
} | |
} |
I have created a multiplatform sample app which just has a single SwiftUI view shared with iOS and macOS (it is not a Mac Catalyst app). Because we use CGImages then all the code can be shared. The view just loads a full size photo, scales it, and displays it. In the example view we have resized the image by taking account the current displayScale. Depending on the displayScale value we need to make the pixel size of the CGImage larger by the factor of the displayScale. For example, if the point size is 200×200 points, displayScale is 3.0, then the pixel size of the CGImage needs to be 600×600. This will give us a nice and crisp end result when the image is rendered.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
struct ContentView: View { | |
@Environment(\.displayScale) var displayScale: CGFloat | |
@StateObject var viewModel = ViewModel() | |
var body: some View { | |
VStack { | |
if viewModel.cgImage != nil { | |
Image(viewModel.cgImage!, | |
scale: displayScale, | |
orientation: .up, | |
label: Text("photo") | |
) | |
} | |
} | |
.padding() | |
.onAppear(perform: { viewModel.prepareImage(withScale: displayScale) }) | |
} | |
} | |
extension ContentView { | |
final class ViewModel: ObservableObject { | |
static let queue = DispatchQueue(label: "com.augmentedcode.imageloader") | |
@Published var cgImage: CGImage? | |
func prepareImage(withScale displayScale: CGFloat) { | |
Self.queue.async { | |
guard let url = Bundle.main.url(forResource: "ExamplePhoto", withExtension: "jpeg") else { return } | |
guard let source = CGImageSourceCreateWithURL(url as CFURL, nil) else { return } | |
guard let image = CGImageSourceCreateImageAtIndex(source, 0, nil) else { return } | |
let targetSize = CGSize(width: CGFloat(200.0) * displayScale, height: CGFloat(200.0) * displayScale) | |
let scaledImage = ImageScaler.scaleToFill(image, in: targetSize) | |
DispatchQueue.main.async { | |
self.cgImage = scaledImage | |
} | |
} | |
} | |
} | |
} |
Summary
CoreGraphics is available on multiple Apple platforms and it provides tools for cropping and resizing images. With just a little bit of refactoring we have turned UIImage based image scaler into CoreGraphics backed scaler which can be used on multiple platforms.
If this was helpful, please let me know on Mastodon@toomasvahter or Twitter @toomasvahter. Feel free to subscribe to RSS feed. Thank you for reading.
Project
CoreGraphicsImageScaler (Xcode 12.2)