Saving ProRAW Next to HEIF: PhotoKit Error 3300 and Other Traps
The triple camera offered no RAW formats until Apple ProRAW was switched on, PhotoKit rejected a DNG passed as Data with error 3300, and re-rendering a photo silently dropped every EXIF field. Notes from getting one shutter press into the Photos library as HEIF plus RAW.
Yunseon Hong4 min read
Part three of notes from building Thirdstop, an iOS camera app, after orientation and settings that don’t apply. This one covers the last few steps of a shot: getting RAW out of the camera, and getting a HEIF and a DNG into the Photos library as a single item with the metadata intact.
Measured on an iPhone 17 Pro with iOS 26.6.1. API details were checked against Apple’s documentation on 11 September 2026.
The triple camera offered no RAW until ProRAW was on
On the back camera I use the virtual triple camera device, so the system can switch lenses as you zoom. On that device availableRawPhotoPixelFormatTypes was an empty array.
I suspected my own session setup first. Turning off responsive capture changed nothing. Removing the video data output changed nothing. Setting isAppleProRAWEnabled = true did it: the list went from [] to l64r, the Apple ProRAW format. On this device the answer to “can I get Bayer RAW from the triple camera” was no. The RAW you get is ProRAW.
Enabling it cost less than I expected. The preview stayed at 30 fps and the HEIF files stayed the same size. Each DNG came out around 15 MB.
Print every variant in one run
That diagnosis took one build, which is the only reason it did not take an afternoon. Instead of changing one thing, rebuilding and reinstalling three times, I wrote the variants into a single launch: configure the session, print the RAW format list, turn off responsive capture, print, remove the video output, print, enable ProRAW, print. One run on the phone and the culprit was on the last line.
A camera app has a slow edit loop because the interesting code only runs on a device. Anything that turns three device runs into one is worth writing, even if it gets deleted the same day.
Hand PhotoKit a DNG file, not DNG data
To keep the RAW and the processed photo together, you add the DNG as an .alternatePhoto resource on the same creation request as the HEIF. My first version passed the DNG as Data. The whole request failed with PHPhotosErrorDomain code 3300, including the HEIF that on its own was fine.
Writing the DNG to a temporary file with a .dng extension and passing the file URL worked:
try PHPhotoLibrary.shared().performChangesAndWait {
let request = PHAssetCreationRequest.forAsset()
request.addResource(with: .photo, data: heifData, options: nil)
let options = PHAssetResourceCreationOptions()
options.shouldMoveFile = true
request.addResource(with: .alternatePhoto, fileURL: dngURL, options: options)
}
shouldMoveFile lets Photos take the file instead of copying 15 MB. The result is one entry in the Photos app showing the HEIF, with a RAW badge.
With RAW on, the delegate fires twice per shot
photoOutput(_:didFinishProcessingPhoto:error:) came in twice per shutter press once RAW was enabled, once for the HEIF and once for the DNG. That is documented: “If you request capture in both RAW and processed formats, this method fires once for each format.” Anything that finishes off a shot in that callback runs twice.
The callback that arrives exactly once per shutter press is photoOutput(_:didFinishCaptureFor:error:), which Apple describes as the point where “no more delegate messages will be sent for this capture request.” Collect both photos as they come and do the saving there.
Re-rendering the pixels throws away the EXIF
The app renders its own color, so it takes the CVPixelBuffer out of each photo, draws it and encodes a new file. The first files that came out of that path had no camera model, no lens, no ISO, no exposure. The pixel buffer carries none of that. It lives in photo.metadata.
The fix is to carry photo.metadata alongside the buffer through the render and write it back when encoding. One field needs changing on the way: after the pixels have been turned upright, the orientation tag must be 1, as part one explains.
Location goes in at capture time, and into the DNG separately
GPS belongs to the moment of the shutter, not to the moment the save finishes. And the DNG comes out of the camera with its metadata already baked into the file, so writing location into the HEIF does nothing for it. I pass the same coordinates into the DNG through fileDataRepresentation(with:), so both files from one shutter press agree on where they were taken.
I also discard location fixes that are too old. Open the app while you are moving and the last fix the phone has may be several kilometers back.
An old capture date hides a new file
The app can import a photo from the library, apply a look and save the result as a new photo. My first version kept the original EXIF date. The Photos timeline sorts by that date, so the new photo was filed next to the original, months back, and the user could not find the thing they had just saved.
A new file gets the date it was made.
You need less permission than you think
Saving needs only add-only access to the photo library. Picking a photo with PHPickerViewController needs no photo permission at all. A share extension receives the photo from another app, so it never asks for photo access either. The only photo permission the app needs is add-only.
The 3300 error is the one I’d warn anyone about, because it failed the HEIF along with the DNG and pointed at neither. When a PhotoKit request fails as a whole, try adding one resource at a time. The resource that fails on its own is the one to look at.
Filed under Build log