Stream image materialization off disk and strip GPS losslessly - #25807
Stream image materialization off disk and strip GPS losslessly#25807jkmassel wants to merge 2 commits into
Conversation
Generated by 🚫 Danger |
|
| App Name | WordPress | |
| Configuration | Release-Alpha | |
| Build Number | 33311 | |
| Version | PR #25807 | |
| Bundle ID | org.wordpress.alpha | |
| Commit | dc911a4 | |
| Installation URL | 3g933bnlmmks0 |
|
| App Name | Jetpack | |
| Configuration | Release-Alpha | |
| Build Number | 33311 | |
| Version | PR #25807 | |
| Bundle ID | com.jetpack.alpha | |
| Commit | dc911a4 | |
| Installation URL | 5a07s9o55pvcg |
2776520 to
5a85126
Compare
5a85126 to
eef1474
Compare
MediaTransformer applies the upload policy's transforms — HEIC→JPEG conversion, resize, EXIF-orientation flatten, GPS/location strip, video duration cap and passthrough remux — streaming each result to disk through pure ImageIO/AVFoundation. It depends on no app code, so it lands as a leaf module: `MediaUploadPolicy` moves in from `WordPressMediaLibrary` (nothing there referenced it yet) and `MediaTransformerError` carries its own localized strings, scoped to the failures the engine actually throws. Nothing consumes it yet — the upload materializer that will comes separately — so `WordPressMediaLibrary` gains no dependency on it. `MediaTransformerTests` exercises the engine directly (47 tests) with UIKit-free CoreGraphics fixtures, registered in `WordPressUnitTests.xctestplan` so iOS CI runs it.
Add a `MediaTransformerTests` entry to the root `WordPressCrossPlatformModules` package so `swift test` builds and runs the transform engine on the macOS host — no Xcode, no simulator, no wordpress-rs.
eef1474 to
dc911a4
Compare


Stacked on #25620 (base branch
task/media-v2-upload-materializer) — the memory/quality follow-up from that PR's review thread.Summary
finalizeImageno longer pins the whole compressed image in RAM. Disk-backed sources stream through a URL-backedCGImageSource→CGImageDestination; the no-transform case is an APFS clone.UploadSourceMaterializertests pass, plus 4 new ones.Background
The review thread on #25620 flagged (comment on line 551) that we can't just move the picked file — it has to be processed to honor
MediaUploadPolicy(GPS strip, compression, resize). That's right. But processing the image does not require loading the whole file into memory. Every API below is back-deployed well under the iOS 17 floor:CGImageSourceCreateWithURL(4.0),CGImageDestinationCopyImageSource(7.0),kCGImageMetadataShouldExcludeGPS(8.0).Changes
1. Stream off a URL source and destination
finalizeImage(data:)becamefinalizeImage(input:)over a smallImageInput { case url(URL); case data(Data) }. The disk-backed callers (.file,.imagePlayground, the downloaded.remoteURLtemp) pass.url— ImageIO faults the compressed bytes in on demand and the encoder writes straight toCGImageDestinationCreateWithURL, so we never own a full-fileDataor anNSMutableDataoutput buffer. The photo-library (NSItemProvider→Data) and camera (UIImage→ JPEGData) sources have no URL, so they keep.data.No transform needed →
FileManager.copyItem(copy-on-write clone on APFS), zero bytes through RAM, replacing the olddata.write(to:)round-trip.2. Lossless GPS strip
The old
.stripGPS-without-.resizebranch did a fullCGImageSourceCreateImageAtIndexdecode and recompressed viakCGImageDestinationLossyCompressionQuality— just to delete a metadata block, silently degrading JPEG quality on every located photo.CGImageDestinationCopyImageSourcecopies the encoded image verbatim and rewrites only the metadata: no decode, no recompress, and the EXIF orientation tag stays paired with its pixels (the old "decode unrotated pixels, re-attach the orientation tag" hazard disappears).One correctness wrinkle worth flagging. The obvious
kCGImageMetadataShouldExcludeGPS: trueon its own is wrong twice over — I verified both against ImageIO before settling on the approach:DateTimeOriginal), not just GPS. ❌ The policy strips location only, and the resize path is tested to retain other EXIF, so a blanket exclude would be an inconsistent regression.So the strip copies the source
CGImageMetadata, removes the GPS tags, writes it back viakCGImageDestinationMetadata(this keeps the capture date and camera make), and then re-reads the output and confirms the GPS block is actually gone before trusting it. PNG — and any future container that ignores the rewrite — fails that check and falls back to the existing decode strip. Fail-closed: a required strip never silently ships the location.3. Transform paths decode from the URL source
Resize (
CGImageSourceCreateThumbnailAtIndex) and format conversion (CGImageDestinationAddImageFromSource) still decode — a resample or codec change must touch pixels — but now from a URL-backed source, so ImageIO DCT-scales / tiles instead of materializing the full-resolution bitmap, and writes straight to disk. The format-changing GPS strip (e.g. HEIC→JPEG + Remove Location) is the one strip case that still decodes, and it drops GPS from the carried-over properties as before.Test plan
Modules-Package→WordPressMediaLibraryTests/UploadSourceMaterializer: 40 tests pass on the iOS 26.0 simulator.DateTimeOriginalis retained.Related