
















@@ -0,0 +1,187 @@
1+import CoreGraphics
2+import Foundation
3+import ImageIO
4+import Testing
5+import UniformTypeIdentifiers
6+@testable import OpenClawKit
7+8+struct ChatImageProcessorTests {
9+private func syntheticJPEG(width: Int, height: Int) throws -> Data {
10+guard
11+let context = CGContext(
12+ data: nil,
13+ width: width,
14+ height: height,
15+ bitsPerComponent: 8,
16+ bytesPerRow: width * 4,
17+ space: CGColorSpaceCreateDeviceRGB(),
18+ bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue)
19+else {
20+throw NSError(domain: "ChatImageProcessorTests", code: 1)
21+}
22+23+ context.setFillColor(CGColor(red: 0.8, green: 0.2, blue: 0.4, alpha: 1))
24+ context.fill(CGRect(x: 0, y: 0, width: width, height: height))
25+ context.setFillColor(CGColor(red: 0.1, green: 0.7, blue: 0.3, alpha: 1))
26+ context.fill(CGRect(x: 0, y: 0, width: width / 2, height: height / 2))
27+28+guard let image = context.makeImage() else {
29+throw NSError(domain: "ChatImageProcessorTests", code: 2)
30+}
31+32+let data = NSMutableData()
33+guard let destination = CGImageDestinationCreateWithData(data, UTType.jpeg.identifier as CFString, 1, nil)
34+else {
35+throw NSError(domain: "ChatImageProcessorTests", code: 3)
36+}
37+38+let properties: [CFString: Any] = [
39+ kCGImageDestinationLossyCompressionQuality: 0.95,
40+ kCGImagePropertyExifDictionary: [
41+ kCGImagePropertyExifDateTimeOriginal: "2026:04:20 16:30:00",
42+ kCGImagePropertyExifLensModel: "Leaky Lens 50mm f/1.4",
43+] as CFDictionary,
44+ kCGImagePropertyGPSDictionary: [
45+ kCGImagePropertyGPSLatitude: 60.02,
46+ kCGImagePropertyGPSLatitudeRef: "N",
47+ kCGImagePropertyGPSLongitude: 10.95,
48+ kCGImagePropertyGPSLongitudeRef: "E",
49+] as CFDictionary,
50+ kCGImagePropertyTIFFDictionary: [
51+ kCGImagePropertyTIFFMake: "LeakCorp",
52+ kCGImagePropertyTIFFModel: "Privacy-Leaker-1",
53+] as CFDictionary,
54+]
55+CGImageDestinationAddImage(destination, image, properties as CFDictionary)
56+guard CGImageDestinationFinalize(destination) else {
57+throw NSError(domain: "ChatImageProcessorTests", code: 4)
58+}
59+return data as Data
60+}
61+62+private func syntheticPNGWithAlpha(width: Int, height: Int) throws -> Data {
63+guard
64+let context = CGContext(
65+ data: nil,
66+ width: width,
67+ height: height,
68+ bitsPerComponent: 8,
69+ bytesPerRow: width * 4,
70+ space: CGColorSpaceCreateDeviceRGB(),
71+ bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue)
72+else {
73+throw NSError(domain: "ChatImageProcessorTests", code: 5)
74+}
75+76+ context.clear(CGRect(x: 0, y: 0, width: width, height: height))
77+ context.setFillColor(CGColor(red: 1, green: 0, blue: 0, alpha: 1))
78+ context.fill(CGRect(x: width / 4, y: height / 4, width: width / 2, height: height / 2))
79+80+guard let image = context.makeImage() else {
81+throw NSError(domain: "ChatImageProcessorTests", code: 6)
82+}
83+84+let data = NSMutableData()
85+guard let destination = CGImageDestinationCreateWithData(data, UTType.png.identifier as CFString, 1, nil)
86+else {
87+throw NSError(domain: "ChatImageProcessorTests", code: 7)
88+}
89+CGImageDestinationAddImage(destination, image, nil)
90+guard CGImageDestinationFinalize(destination) else {
91+throw NSError(domain: "ChatImageProcessorTests", code: 8)
92+}
93+return data as Data
94+}
95+96+private func properties(for data: Data) -> [CFString: Any] {
97+guard
98+let source = CGImageSourceCreateWithData(data as CFData, nil),
99+let properties = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as? [CFString: Any]
100+else {
101+return [:]
102+}
103+return properties
104+}
105+106+private func dimensions(for data: Data) -> (width: Int, height: Int)? {
107+let properties = self.properties(for: data)
108+guard
109+let width = properties[kCGImagePropertyPixelWidth] as? NSNumber,
110+let height = properties[kCGImagePropertyPixelHeight] as? NSNumber
111+else {
112+return nil
113+}
114+return (width.intValue, height.intValue)
115+}
116+117+@Test func `resizes landscape long edge to upload limit`() throws {
118+let source = try self.syntheticJPEG(width: 4000, height: 3000)
119+let output = try ChatImageProcessor.processForUpload(data: source)
120+let dimensions = try #require(self.dimensions(for: output))
121+122+ #expect(max(dimensions.width, dimensions.height) <= ChatImageProcessor.maxLongEdgePx)
123+ #expect(abs((Double(dimensions.width) / Double(dimensions.height)) - (4000.0 / 3000.0)) <= 0.02)
124+}
125+126+@Test func `resizes portrait long edge to upload limit`() throws {
127+let source = try self.syntheticJPEG(width: 3000, height: 4000)
128+let output = try ChatImageProcessor.processForUpload(data: source)
129+let dimensions = try #require(self.dimensions(for: output))
130+131+ #expect(max(dimensions.width, dimensions.height) <= ChatImageProcessor.maxLongEdgePx)
132+ #expect(abs((Double(dimensions.width) / Double(dimensions.height)) - (3000.0 / 4000.0)) <= 0.02)
133+}
134+135+@Test func `resizes narrow tall long edge to upload limit`() throws {
136+let source = try self.syntheticJPEG(width: 1080, height: 2400)
137+let output = try ChatImageProcessor.processForUpload(data: source)
138+let dimensions = try #require(self.dimensions(for: output))
139+140+ #expect(max(dimensions.width, dimensions.height) <= ChatImageProcessor.maxLongEdgePx)
141+ #expect(abs((Double(dimensions.width) / Double(dimensions.height)) - (1080.0 / 2400.0)) <= 0.02)
142+}
143+144+@Test func `small image is not upscaled`() throws {
145+let source = try self.syntheticJPEG(width: 400, height: 300)
146+let output = try ChatImageProcessor.processForUpload(data: source)
147+let dimensions = try #require(self.dimensions(for: output))
148+149+ #expect(max(dimensions.width, dimensions.height) <= 400)
150+}
151+152+@Test func `output fits payload budget`() throws {
153+let source = try self.syntheticJPEG(width: 4000, height: 3000)
154+let output = try ChatImageProcessor.processForUpload(data: source)
155+156+ #expect(output.count <= ChatImageProcessor.maxPayloadBytes)
157+}
158+159+@Test func `rejects non image data`() {
160+let garbage = Data("not an image".utf8)
161+162+ #expect(throws: ChatImageProcessor.ProcessError.self) {
163+ _ = try ChatImageProcessor.processForUpload(data: garbage)
164+}
165+}
166+167+@Test func `strips source metadata from output`() throws {
168+let source = try self.syntheticJPEG(width: 3000, height: 2000)
169+let output = try ChatImageProcessor.processForUpload(data: source)
170+let properties = self.properties(for: output)
171+let gps = properties[kCGImagePropertyGPSDictionary] as? [CFString: Any] ?? [:]
172+173+ #expect(gps.isEmpty)
174+for needle in ["Leaky Lens", "LeakCorp", "Privacy-Leaker", "2026:04:20"] {
175+ #expect(output.range(of: Data(needle.utf8)) == nil)
176+}
177+}
178+179+@Test func `flattens transparent sources to opaque JPEG`() throws {
180+let source = try self.syntheticPNGWithAlpha(width: 800, height: 600)
181+let output = try ChatImageProcessor.processForUpload(data: source)
182+let imageSource = try #require(CGImageSourceCreateWithData(output as CFData, nil))
183+let image = try #require(CGImageSourceCreateImageAtIndex(imageSource, 0, nil))
184+185+ #expect([.none, .noneSkipFirst, .noneSkipLast].contains(image.alphaInfo))
186+}
187+}
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。