| import OpenClawKit |
| import CoreGraphics |
| import ImageIO |
| import Testing |
| import UniformTypeIdentifiers |
|
|
| @Suite 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 |
| } |
|
|
| @Test 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) |
| } |
|
|
| @Test 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) |
| } |
|
|
| @Test func normalizesOrientationAndUsesOrientedWidthForMaxWidthPx() throws { |
| |
| 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) |
| } |
|
|
| @Test 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) |
| } |
| } |
|
|