apple_kit

Apple-native services for Radroots iOS and macOS apps
git clone https://radroots.dev/git/apple_kit.git
Log | Files | Refs | README | LICENSE

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 }