iOS 카메라 개발기 - ProRAW·EXIF·PhotoKit 저장에서 걸린 것들
RAW 포맷 목록이 빈 배열이었고, DNG를 Data로 넘기면 요청 전체가 실패했다. 찍는 것보다 사진 앱에 제대로 넣는 게 더 까다로웠다.
egg2분 읽기
지난 편은 찍는 쪽 이야기였고 이번은 저장이다. 셔터를 누른 뒤부터 사진 앱에 한 장이 들어가기까지 걸린 것들을 모았다.
측정 기기는 iPhone 17 Pro, iOS 26.6.1이다.
RAW 포맷 목록이 비어 있었다
후면 트리플 카메라, 즉 가상 기기에서 availableRawPhotoPixelFormatTypes가 []였다. 베이어 RAW를 달라고 할 수가 없었다.
뭔가 다른 설정이 막고 있겠거니 싶었다. responsive capture를 끄고 videoOutput을 떼어봐도 그대로였다. isAppleProRAWEnabled = true를 켜고 나서야 목록에 l64r 하나가 들어왔다.
결국 이 구성에서 쓸 수 있는 RAW는 ProRAW뿐이었다. 켜도 프리뷰 30fps와 HEIF 크기는 그대로였고 DNG는 장당 약 15MB 나왔다.
변형마다 print를 찍어 좁혔다
사실 이 편에서 제일 남기고 싶은 건 찾은 방식 쪽이다. 후보를 하나 고치고 빌드하고 확인하고 되돌리는 대신, 한 번 실행하는 동안 세션 구성을 차례로 바꾸며 매 단계 목록을 찍었다.
로그를 줄이면 이런 모양이다.
0 기본 []
1 responsive off []
2 videoOutput 제거 []
3 ProRAW on [l64r]
빌드 한 번에 범인이 나왔다. 확인이 끝나면 프로브는 바로 뺀다.
DNG를 Data로 주면 요청 전체가 실패한다
HEIF와 DNG를 한 항목으로 넣으려고 .alternatePhoto 리소스에 DNG를 Data로 넘겼다. 결과는 PHPhotosErrorDomain 3300. DNG만 빠지는 게 아니라 요청 전체가 실패해서 HEIF도 안 들어갔다.
확장자가 붙은 임시 .dng 파일을 만들고 URL로 넘기니 통과했다. 사진 앱에는 HEIF에 RAW 배지가 붙은 한 항목으로 들어간다.
let url = FileManager.default.temporaryDirectory
.appendingPathComponent(UUID().uuidString)
.appendingPathExtension("dng")
try dngData.write(to: url)
try await PHPhotoLibrary.shared().performChanges {
let req = PHAssetCreationRequest.forAsset()
req.addResource(with: .photo, data: heifData, options: nil)
let opt = PHAssetResourceCreationOptions()
opt.shouldMoveFile = true
req.addResource(with: .alternatePhoto, fileURL: url, options: opt)
}
shouldMoveFile을 켜두면 임시 파일 정리도 사진 앱이 가져간다.
RAW를 같이 찍으면 델리게이트가 두 번 온다
didFinishProcessingPhoto가 HEIF용으로 한 번, RAW용으로 한 번 온다. 여기서 저장을 마무리하면 셔터 한 번에 저장이 두 번 돈다.
마무리는 셔터당 한 번만 오는 didFinishCaptureFor에서 한다. 두 번 오는 쪽에서는 결과를 모아두기만 한다.
다시 렌더하면 EXIF가 날아간다
셰이더로 다시 그리려고 CVPixelBuffer만 꺼내 렌더했더니 카메라, 렌즈, ISO, 노출 정보가 전부 빠진 사진이 나왔다. 픽셀만 들고 나왔으니 당연한 결과인데 저장한 사진을 열어보기 전까지 몰랐다.
photo.metadata를 버퍼와 함께 들고 다니다가 인코딩할 때 다시 싣는다. 이때 orientation을 그대로 실으면 1편의 90° 누운 사진이 된다.
위치는 두 파일에 따로 넣는다
위치 EXIF는 캡처 시점의 좌표로 넣는다. 그런데 DNG는 메타데이터가 파일에 구워져서 나오기 때문에 HEIF 쪽에만 넣으면 한 셔터의 두 파일이 서로 다른 말을 한다.
DNG는 fileDataRepresentation(with:)에 customizer를 넘겨서 같은 좌표를 끼워 넣었다.
오래된 위치 고정점은 버린다. 이동 중에 앱을 켜면 마지막 고정점이 몇 km 전일 수 있다. 아무래도 엉뚱한 위치가 박힌 사진보다는 위치가 없는 편이 나은 것 같다.
옛 날짜로 저장하면 사진이 사라진다
사진첩에서 불러온 사진을 가공해서 새로 저장할 때 원본 EXIF의 촬영 일시를 그대로 뒀다. 저장은 성공했는데 사진 앱에서 방금 저장한 사진이 안 보였다.
타임라인의 옛 날짜 자리에 꽂혀 있었다. 사용자 입장에서는 그냥 저장 실패다.
권한은 생각보다 적게 든다
저장만 할 거라면 추가 전용(add-only) 권한으로 충분하다. 사진을 고르는 PHPicker는 권한 자체가 필요 없다.
공유 시트 익스텐션은 받는 쪽이라 사진 권한이 아예 없다. 권한 요청 문구를 쓰기 전에 정말 필요한 권한인지부터 확인하는 게 순서였던 것 같다.
사진 한 장 저장하는 데 이렇게 걸릴 게 많을 줄은 몰랐다.. 다음 편은 화면으로 돌아간다. Metal 프리뷰가 버벅이던 이유들.
Filed under 개발