Spaces:
Paused
Paused
| import OpenClawKit | |
| import CoreGraphics | |
| import ImageIO | |
| import Testing | |
| import UniformTypeIdentifiers | |
| struct JPEGTranscoderTests { | |
| private func makeSolidJPEG(width: Int, height: Int, orientation: Int? = nil) throws -> Data { | |
| let cs = CGColorSpaceCreateDeviceRGB() | |
| let bitmapInfo = CGImageAlphaInfo.premultipliedLast.rawValue | |
| guard | |
| let ctx = CGContext( | |
| data: nil, | |
| width: width, | |
| height: height, | |
| bitsPerComponent: 8, | |
| bytesPerRow: 0, | |
| space: cs, | |
| bitmapInfo: bitmapInfo) | |
| else { | |
| throw NSError(domain: "JPEGTranscoderTests", code: 1) | |
| } | |
| ctx.setFillColor(red: 1, green: 0, blue: 0, alpha: 1) | |
| ctx.fill(CGRect(x: 0, y: 0, width: width, height: height)) | |
| guard let img = ctx.makeImage() else { | |
| throw NSError(domain: "JPEGTranscoderTests", code: 5) | |
| } | |
| let out = NSMutableData() | |
| guard let dest = CGImageDestinationCreateWithData(out, UTType.jpeg.identifier as CFString, 1, nil) else { | |
| throw NSError(domain: "JPEGTranscoderTests", code: 2) | |
| } | |
| var props: [CFString: Any] = [ | |
| kCGImageDestinationLossyCompressionQuality: 1.0, | |
| ] | |
| if let orientation { | |
| props[kCGImagePropertyOrientation] = orientation | |
| } | |
| CGImageDestinationAddImage(dest, img, props as CFDictionary) | |
| guard CGImageDestinationFinalize(dest) else { | |
| throw NSError(domain: "JPEGTranscoderTests", code: 3) | |
| } | |
| return out as Data | |
| } | |
| private func makeNoiseJPEG(width: Int, height: Int) throws -> Data { | |
| let bytesPerPixel = 4 | |
| let byteCount = width * height * bytesPerPixel | |
| var data = Data(count: byteCount) | |
| let cs = CGColorSpaceCreateDeviceRGB() | |
| let bitmapInfo = CGImageAlphaInfo.premultipliedLast.rawValue | |
| let out = try data.withUnsafeMutableBytes { rawBuffer -> Data in | |
| guard let base = rawBuffer.baseAddress?.assumingMemoryBound(to: UInt8.self) else { | |
| throw NSError(domain: "JPEGTranscoderTests", code: 6) | |
| } | |
| for idx in 0..<byteCount { | |
| base[idx] = UInt8.random(in: 0...255) | |
| } | |
| guard | |
| let ctx = CGContext( | |
| data: base, | |
| width: width, | |
| height: height, | |
| bitsPerComponent: 8, | |
| bytesPerRow: width * bytesPerPixel, | |
| space: cs, | |
| bitmapInfo: bitmapInfo) | |
| else { | |
| throw NSError(domain: "JPEGTranscoderTests", code: 7) | |
| } | |
| guard let img = ctx.makeImage() else { | |
| throw NSError(domain: "JPEGTranscoderTests", code: 8) | |
| } | |
| let encoded = NSMutableData() | |
| guard let dest = CGImageDestinationCreateWithData(encoded, UTType.jpeg.identifier as CFString, 1, nil) | |
| else { | |
| throw NSError(domain: "JPEGTranscoderTests", code: 9) | |
| } | |
| CGImageDestinationAddImage(dest, img, nil) | |
| guard CGImageDestinationFinalize(dest) else { | |
| throw NSError(domain: "JPEGTranscoderTests", code: 10) | |
| } | |
| return encoded as Data | |
| } | |
| return out | |
| } | |
| func downscalesToMaxWidthPx() throws { | |
| let input = try makeSolidJPEG(width: 2000, height: 1000) | |
| let out = try JPEGTranscoder.transcodeToJPEG(imageData: input, maxWidthPx: 1600, quality: 0.9) | |
| #expect(out.widthPx == 1600) | |
| #expect(abs(out.heightPx - 800) <= 1) | |
| #expect(out.data.count > 0) | |
| } | |
| func doesNotUpscaleWhenSmallerThanMaxWidthPx() throws { | |
| let input = try makeSolidJPEG(width: 800, height: 600) | |
| let out = try JPEGTranscoder.transcodeToJPEG(imageData: input, maxWidthPx: 1600, quality: 0.9) | |
| #expect(out.widthPx == 800) | |
| #expect(out.heightPx == 600) | |
| } | |
| func normalizesOrientationAndUsesOrientedWidthForMaxWidthPx() throws { | |
| // Encode a landscape image but mark it rotated 90° (orientation 6). Oriented width becomes 1000. | |
| let input = try makeSolidJPEG(width: 2000, height: 1000, orientation: 6) | |
| let out = try JPEGTranscoder.transcodeToJPEG(imageData: input, maxWidthPx: 1600, quality: 0.9) | |
| #expect(out.widthPx == 1000) | |
| #expect(out.heightPx == 2000) | |
| } | |
| func respectsMaxBytes() throws { | |
| let input = try makeNoiseJPEG(width: 1600, height: 1200) | |
| let out = try JPEGTranscoder.transcodeToJPEG( | |
| imageData: input, | |
| maxWidthPx: 1600, | |
| quality: 0.95, | |
| maxBytes: 180_000) | |
| #expect(out.data.count <= 180_000) | |
| } | |
| } | |