|
1 | 1 | import AppKit |
| 2 | +import ImageIO |
| 3 | +import UniformTypeIdentifiers |
2 | 4 | import XCTest |
3 | 5 |
|
4 | 6 | final class PNDiskStoreTests: XCTestCase { |
@@ -189,6 +191,42 @@ final class PNDiskStoreTests: XCTestCase { |
189 | 191 | XCTAssertEqual(publicArticle?.attachments, []) |
190 | 192 | } |
191 | 193 |
|
| 194 | + func testDiskAttachmentsMatchAppPipeline() throws { |
| 195 | + let sandbox = try PNTestSandbox() |
| 196 | + defer { sandbox.cleanup() } |
| 197 | + |
| 198 | + let store = PNDiskStore(root: sandbox.libraryURL) |
| 199 | + let planet = try store.createPlanet(name: "Pipeline Planet", about: "", template: nil, avatar: nil) |
| 200 | + defer { _ = try? store.deletePlanet(planet) } |
| 201 | + |
| 202 | + // Primary video/audio classified by uniform type, video first. |
| 203 | + let movie = try sandbox.makeTextFixture(name: "clip.mov", contents: "fake") |
| 204 | + let song = try sandbox.makeTextFixture(name: "song.mp3", contents: "fake") |
| 205 | + let blob = try sandbox.makeTextFixture(name: "data.bin", contents: "fake") |
| 206 | + var article = try store.createArticle(planet: planet, title: "Media", content: "x", date: nil, attachments: [movie, song, blob]) |
| 207 | + XCTAssertEqual(article.videoFilename, "clip.mov") |
| 208 | + XCTAssertEqual(article.audioFilename, "song.mp3") |
| 209 | + |
| 210 | + // GPS metadata is stripped from JPEG attachments. |
| 211 | + let jpegWithGPS = try makeJPEGWithGPSFixture(named: "gps.jpg", in: sandbox) |
| 212 | + XCTAssertNotNil(gpsDictionary(at: jpegWithGPS), "Fixture should carry GPS metadata.") |
| 213 | + article = try store.addAttachments(planet: planet, article: article, attachments: [jpegWithGPS]) |
| 214 | + XCTAssertTrue(article.attachments?.contains("gps.jpg") == true) |
| 215 | + let publicJPEG = store.articlePublicPath(article, in: planet).appendingPathComponent("gps.jpg", isDirectory: false) |
| 216 | + XCTAssertNil(gpsDictionary(at: publicJPEG)) |
| 217 | + XCTAssertNotNil(NSBitmapImageRep(data: try Data(contentsOf: publicJPEG)), "Stripped JPEG should remain a valid image.") |
| 218 | + |
| 219 | + // HEIC attachments are converted to <basename>.jpg. |
| 220 | + guard let heic = makeHEICFixture(named: "photo.heic", in: sandbox) else { |
| 221 | + throw XCTSkip("HEIC encoding is unavailable on this machine.") |
| 222 | + } |
| 223 | + article = try store.addAttachments(planet: planet, article: article, attachments: [heic]) |
| 224 | + XCTAssertTrue(article.attachments?.contains("photo.jpg") == true) |
| 225 | + XCTAssertFalse(article.attachments?.contains("photo.heic") == true) |
| 226 | + let converted = store.articlePublicPath(article, in: planet).appendingPathComponent("photo.jpg", isDirectory: false) |
| 227 | + XCTAssertEqual(Array(try Data(contentsOf: converted).prefix(2)), [0xFF, 0xD8], "Converted file should be a JPEG.") |
| 228 | + } |
| 229 | + |
192 | 230 | func testPartialUUIDSelectorsResolveWithExactMatchPrecedence() throws { |
193 | 231 | let sandbox = try PNTestSandbox() |
194 | 232 | defer { sandbox.cleanup() } |
@@ -301,6 +339,64 @@ final class PNDiskStoreTests: XCTestCase { |
301 | 339 | XCTAssertEqual(parsed.arguments, ["planet", "list"]) |
302 | 340 | } |
303 | 341 |
|
| 342 | + private func makeJPEGWithGPSFixture(named name: String, in sandbox: PNTestSandbox) throws -> URL { |
| 343 | + let base = try sandbox.makeImageFixture(name: "base-\(name)", width: 40, height: 40) |
| 344 | + let data = try Data(contentsOf: base) |
| 345 | + guard let source = CGImageSourceCreateWithData(data as CFData, nil), |
| 346 | + let type = CGImageSourceGetType(source) |
| 347 | + else { |
| 348 | + throw PNError.diskError("Unable to read JPEG fixture source.") |
| 349 | + } |
| 350 | + let url = sandbox.root.appendingPathComponent(name, isDirectory: false) |
| 351 | + guard let destination = CGImageDestinationCreateWithURL(url as CFURL, type, 1, nil) else { |
| 352 | + throw PNError.diskError("Unable to create JPEG fixture destination.") |
| 353 | + } |
| 354 | + let gps: [CFString: Any] = [ |
| 355 | + kCGImagePropertyGPSLatitude: 37.7749, |
| 356 | + kCGImagePropertyGPSLongitude: 122.4194, |
| 357 | + ] |
| 358 | + CGImageDestinationAddImageFromSource(destination, source, 0, [kCGImagePropertyGPSDictionary: gps] as CFDictionary) |
| 359 | + guard CGImageDestinationFinalize(destination) else { |
| 360 | + throw PNError.diskError("Unable to write JPEG fixture with GPS metadata.") |
| 361 | + } |
| 362 | + return url |
| 363 | + } |
| 364 | + |
| 365 | + private func gpsDictionary(at url: URL) -> [CFString: Any]? { |
| 366 | + guard let source = CGImageSourceCreateWithURL(url as CFURL, nil), |
| 367 | + let properties = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as? [CFString: Any] |
| 368 | + else { |
| 369 | + return nil |
| 370 | + } |
| 371 | + return properties[kCGImagePropertyGPSDictionary] as? [CFString: Any] |
| 372 | + } |
| 373 | + |
| 374 | + private func makeHEICFixture(named name: String, in sandbox: PNTestSandbox) -> URL? { |
| 375 | + guard let bitmap = NSBitmapImageRep( |
| 376 | + bitmapDataPlanes: nil, |
| 377 | + pixelsWide: 40, |
| 378 | + pixelsHigh: 40, |
| 379 | + bitsPerSample: 8, |
| 380 | + samplesPerPixel: 4, |
| 381 | + hasAlpha: true, |
| 382 | + isPlanar: false, |
| 383 | + colorSpaceName: .deviceRGB, |
| 384 | + bytesPerRow: 0, |
| 385 | + bitsPerPixel: 0 |
| 386 | + ), let cgImage = bitmap.cgImage else { |
| 387 | + return nil |
| 388 | + } |
| 389 | + let url = sandbox.root.appendingPathComponent(name, isDirectory: false) |
| 390 | + guard let destination = CGImageDestinationCreateWithURL(url as CFURL, UTType.heic.identifier as CFString, 1, nil) else { |
| 391 | + return nil |
| 392 | + } |
| 393 | + CGImageDestinationAddImage(destination, cgImage, nil) |
| 394 | + guard CGImageDestinationFinalize(destination) else { |
| 395 | + return nil |
| 396 | + } |
| 397 | + return url |
| 398 | + } |
| 399 | + |
304 | 400 | private func writePlanetFixture(_ planet: PNPlanetRecord, in libraryURL: URL) throws { |
305 | 401 | let directory = libraryURL |
306 | 402 | .appendingPathComponent("My", isDirectory: true) |
|
0 commit comments