RadrootsAppleImageDecode.swift (4931B)
1 import CoreGraphics 2 import Foundation 3 import ImageIO 4 import UniformTypeIdentifiers 5 6 /// Admission bounds for one actor-owned decode. The working-byte estimate 7 /// includes compressed input, an eight-bit source raster, and three derivative 8 /// rasters (thumbnail, color conversion and encoder input). ImageIO owns its 9 /// internal codec workspace; this is not a process RSS or allocator guarantee. 10 enum RadrootsAppleImageDecode { 11 static let maximumSourceDimension = 32768 12 static let maximumRasterBytes = 160_000_000 13 static let maximumWorkingBytes = 512 * 1024 * 1024 14 15 static func validateDimensions( 16 width: Int, height: Int, inputBytes: Int, maximumPixelCount: Int, maximumDimension: Int 17 ) throws { 18 guard (1 ... maximumSourceDimension).contains(width), 19 (1 ... maximumSourceDimension).contains(height), 20 (0 ... (40 * 1024 * 1024)).contains(inputBytes), 21 (1 ... 8192).contains(maximumDimension) 22 else { throw RadrootsAppleMediaPreparationError.invalidRequest } 23 // Dimension admission precedes all multiplication; these products fit 24 // Int on every supported 64-bit Apple target. 25 let pixels = width * height 26 let sourceBytes = pixels * 4 27 let derivativePixels = min(pixels, maximumDimension * maximumDimension) 28 guard pixels <= maximumPixelCount, sourceBytes <= maximumRasterBytes, 29 inputBytes + sourceBytes + derivativePixels * 4 * 3 <= maximumWorkingBytes 30 else { throw RadrootsAppleMediaPreparationError.invalidRequest } 31 } 32 33 static func normalizedImage(_ data: Data, request: RadrootsAppleImagePreparationRequest) throws -> CGImage { 34 try Task.checkCancellation() 35 guard !data.isEmpty, data.count <= request.maximumInputBytes, 36 let source = CGImageSourceCreateWithData( 37 data as CFData, 38 [kCGImageSourceShouldCache: false] as CFDictionary 39 ), 40 let type = CGImageSourceGetType(source) as String?, 41 [UTType.jpeg.identifier, UTType.png.identifier, UTType.heic.identifier, UTType.heif.identifier] 42 .contains(type), 43 CGImageSourceGetCount(source) == 1, 44 let properties = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as? [CFString: Any], 45 let widthNumber = properties[kCGImagePropertyPixelWidth] as? NSNumber, 46 let heightNumber = properties[kCGImagePropertyPixelHeight] as? NSNumber, 47 let width = Int(exactly: widthNumber.doubleValue), 48 let height = Int(exactly: heightNumber.doubleValue), 49 let depth = (properties[kCGImagePropertyDepth] as? NSNumber)?.intValue, 50 (1 ... 8).contains(depth) 51 else { throw RadrootsAppleMediaPreparationError.invalidRequest } 52 try validateDimensions(width: width, height: height, inputBytes: data.count, 53 maximumPixelCount: request.maximumPixelCount, maximumDimension: request.maximumDimension) 54 try Task.checkCancellation() 55 let options: [CFString: Any] = [ 56 kCGImageSourceCreateThumbnailFromImageAlways: true, 57 kCGImageSourceCreateThumbnailWithTransform: true, 58 kCGImageSourceShouldCacheImmediately: true, 59 kCGImageSourceShouldAllowFloat: false, 60 kCGImageSourceThumbnailMaxPixelSize: request.maximumDimension 61 ] 62 guard let image = CGImageSourceCreateThumbnailAtIndex(source, 0, options as CFDictionary), 63 image.width > 0, image.height > 0, 64 image.width <= request.maximumDimension, image.height <= request.maximumDimension, 65 image.width * image.height <= request.maximumPixelCount, 66 (1 ... 8).contains(image.bitsPerComponent), image.bitsPerPixel <= 32, 67 image.bytesPerRow > 0, image.bytesPerRow <= maximumRasterBytes / image.height 68 else { throw RadrootsAppleMediaPreparationError.invalidRequest } 69 try Task.checkCancellation() 70 // Render pixels into a standard space so source ICC/device profiles and 71 // image properties cannot ride along with the sanitized derivative. 72 guard let space = CGColorSpace(name: CGColorSpace.sRGB), 73 let context = CGContext(data: nil, width: image.width, height: image.height, 74 bitsPerComponent: 8, bytesPerRow: image.width * 4, space: space, 75 bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue) 76 else { throw RadrootsAppleMediaPreparationError.preparationFailure } 77 context.draw(image, in: CGRect(x: 0, y: 0, width: image.width, height: image.height)) 78 try Task.checkCancellation() 79 guard let normalized = context.makeImage() else { 80 throw RadrootsAppleMediaPreparationError.preparationFailure 81 } 82 return normalized 83 } 84 }