capacitor-community / camera-preview

Capacitor plugin that allows camera interaction from HTML code
MIT License
186 stars 157 forks source link

EXIF data for images taken on iOS devices #164

Open davehearne opened 3 years ago

davehearne commented 3 years ago

Hi, It looks like there is no EXIF data available for iOS, is there a way to turn this on?

EXIF data is available for android.

scanlop-pses commented 3 years ago

Facing the same issue. EXIF data not available on iOS

jcroucher commented 2 years ago

You need to manually add the metadata in

Someone can probably provide a better example than me but incase it helps something like this.

Grab the metadata

self.cameraController.captureImage { (image, metadata, depthData, error) in

then pass it through to your save function. Something like

    let options = metadata.mutableCopy() as! NSMutableDictionary

    CGImageDestinationAddImage(destination,
                               cgImage,
                               options)
davehearne commented 2 years ago

Hi @jcroucher , thanks for the reply.

I have tried this in CameraController.swift but I get an error on metadata and depthData.

I tried updating the function captureImage to pass 4 parameters however metadata and depthData were not recognized. Do these values need to initialized as a specific type?

Thanks in advance, Dave

jcroucher commented 2 years ago

Modify the captureImage function in CameraController file to be something like this. I have made a number of changes so I would recommend using it just as reference rather than copying the blocks as it may cause more issues

    func captureImage(completion: @escaping (UIImage?, NSDictionary?, AVDepthData?, Error?) -> Void) {
            guard let captureSession = captureSession, captureSession.isRunning else {
                completion(nil, nil, nil,  CameraControllerError.captureSessionIsMissing); return
            }

            let settings = AVCapturePhotoSettings(format: [AVVideoCodecKey: AVVideoCodecType.hevc])
            settings.isDepthDataDeliveryEnabled = photoOutput?.isDepthDataDeliverySupported ?? false
            settings.embedsDepthDataInPhoto = photoOutput?.isDepthDataDeliverySupported ?? false

            settings.flashMode = self.flashMode
            settings.isHighResolutionPhotoEnabled = self.highResolutionOutput

            self.photoOutput?.capturePhoto(with: settings, delegate: self)
            self.photoCaptureCompletionBlock = completion
        }

Note in my version I have the codec set to [AVVideoCodecKey: AVVideoCodecType.hevc] Leave this as jpeg if you don't want to use the heic format.

And a bit more code as an example. That method above is called from

        self.cameraController.captureImage { (image, metadata, depthData, error) in

            guard let image = image else {
                print(error ?? "Image capture error")
                guard let error = error else {
                    call.reject("Image capture error")
                    return
                }
                call.reject(error.localizedDescription)
                return
            }
            let imageDataHiec: Data?

            imageDataHiec = image.heic(metadata: metadata!, depthData: depthData!)
            .......

I save it out as a hiec, but the same would work for jpg

func heic(metadata: NSDictionary, depthData: AVDepthData) -> Data? {
    guard
        let mutableData = CFDataCreateMutable(nil, 0),
        let destination = CGImageDestinationCreateWithData(mutableData, "public.heic" as CFString, 1, nil),
        let cgImage = cgImage
    else { return nil }

    let options = metadata.mutableCopy() as! NSMutableDictionary
    options[kCGImagePropertyOrientation] = cgImageOrientation.rawValue

    CGImageDestinationAddImage(destination,
                               cgImage,
                               options)

    var auxDataType :NSString?
    let auxData = depthData.dictionaryRepresentation(forAuxiliaryDataType: &auxDataType)

    // Add auxiliary data to the image destination.
    if (auxData != nil && auxDataType != nil) {
        CGImageDestinationAddAuxiliaryDataInfo(destination, auxDataType!, auxData! as CFDictionary)
    }

    guard CGImageDestinationFinalize(destination) else { return nil }
    return mutableData as Data
}

Remove all the depth data bits if you don't need it. I need it for what I am doing, but it will just make your files bigger for no reason if you don't use it.

scanlop-pses commented 1 year ago

Finally revisiting the above issue regarding getting EXIF data for iOS Images. I have been following the information above provided by @jcroucher. However I have it a snag when trying to build it.

s func captureImage(completion: @escaping (UIImage?, NSDictionary?, Error?) -> Void) {
        guard let captureSession = captureSession, captureSession.isRunning else { completion(nil, nil, CameraControllerError.captureSessionIsMissing); return }
        let settings = AVCapturePhotoSettings()

        settings.flashMode = self.flashMode
        settings.isHighResolutionPhotoEnabled = self.highResolutionOutput;

        let currentDevice: UIDevice = .current
        let deviceOrientation: UIDeviceOrientation = currentDevice.orientation
        let statusBarOrientation = UIApplication.shared.statusBarOrientation
        if deviceOrientation == .portrait {
           self.photoOutput?.connection(with: AVMediaType.video)?.videoOrientation = AVCaptureVideoOrientation.portrait
        }else if (deviceOrientation == .landscapeLeft){
           self.photoOutput?.connection(with: AVMediaType.video)?.videoOrientation = AVCaptureVideoOrientation.landscapeRight
        }else if (deviceOrientation == .landscapeRight){
           self.photoOutput?.connection(with: AVMediaType.video)?.videoOrientation = AVCaptureVideoOrientation.landscapeLeft
        }else if (deviceOrientation == .portraitUpsideDown){
           self.photoOutput?.connection(with: AVMediaType.video)?.videoOrientation = AVCaptureVideoOrientation.portraitUpsideDown
        }else if (deviceOrientation == .faceUp || deviceOrientation == .faceDown){
            switch (statusBarOrientation) {
            case .portrait:
                self.photoOutput?.connection(with: AVMediaType.video)?.videoOrientation = AVCaptureVideoOrientation.portrait
            case .landscapeRight:
                self.photoOutput?.connection(with: AVMediaType.video)?.videoOrientation = AVCaptureVideoOrientation.landscapeRight
            case .landscapeLeft:
                self.photoOutput?.connection(with: AVMediaType.video)?.videoOrientation = AVCaptureVideoOrientation.landscapeLeft
            case .portraitUpsideDown:
                self.photoOutput?.connection(with: AVMediaType.video)?.videoOrientation = AVCaptureVideoOrientation.portraitUpsideDown
            default:
                self.photoOutput?.connection(with: AVMediaType.video)?.videoOrientation = AVCaptureVideoOrientation.portrait
            }
        }else {
           self.photoOutput?.connection(with: AVMediaType.video)?.videoOrientation = AVCaptureVideoOrientation.portrait
        }
        self.photoOutput?.capturePhoto(with: settings, delegate: self)
        self.photoCaptureCompletionBlock = completion

I get an error for the last line of this function, 'Type of expression is ambiguous without more context'

The only other changes I have made are in the Plugin:

@objc func capture(_ call: CAPPluginCall) {
        DispatchQueue.main.async {

        let quality: Int? = call.getInt("quality", 85)

        self.cameraController.captureImage { (image, metadata, error) in

            guard let image = image else {
                print(error ?? "Image capture error")
                guard let error = error else {
                    call.reject("Image capture error")
                    return
                }
                call.reject(error.localizedDescription)
                return
            }
            var imageData: Data?
            imageData = image.testFunction(metadata: metadata!)
            if (self.cameraPosition == "front") {
                let flippedImage = image.withHorizontallyFlippedOrientation()
                imageData = flippedImage.jpegData(compressionQuality: CGFloat(quality!/100))
            } else {
                imageData = image.jpegData(compressionQuality: CGFloat(quality!/100))
            }

            if (self.storeToFile == false){
                let imageBase64 = imageData?.base64EncodedString()
                call.resolve(["value": imageBase64!])
            }else{
                do{
                    let fileUrl=self.getTempFilePath()
                    try imageData?.write(to:fileUrl)
                    call.resolve(["value":fileUrl.absoluteString])
                }catch{
                    call.reject("error writing image to file")
                }
            }

        }
        }
    }

I also added this function to the extension UIImage:

func testFunction(metadata: NSDictionary) -> Data? {
        guard
                let mutableData = CFDataCreateMutable(nil, 0),
                let destination = CGImageDestinationCreateWithData(mutableData, "public.jpeg" as CFString, 1, nil),
                let cgImage = cgImage
            else { return nil }

            let options = metadata.mutableCopy() as! NSMutableDictionary

            CGImageDestinationAddImage(destination,
                                       cgImage,
                                       options)

            guard CGImageDestinationFinalize(destination) else { return nil }
            return mutableData as Data
    }

Any help appreciated. I am no swift developer, only being following what information is available.

ninaDeimos commented 1 year ago

Does anyone have a working solution for this?

ryaa commented 2 months ago

I am not sure if this is related to this issue, as the original message does not mention which EXIF metadata is missing. However, I also faced similar problem, where the GPS coordinates are missing in EXIF metadata of the photos taken on both iOS and Android platforms. In my case, it seems that the camera API does does not have direct access to location information, so if you want EXIF metadata with location in the final JPEG, you have to add it yourself. For example, here is similar feature added to cordova-plugin-camera plugin to enforce the EXIF metadata presence - see https://github.com/apache/cordova-plugin-camera/pull/525

However, I am not sure if this should be within the scope of this plugin so I decided to take another approach and created the capacitor plugin to add/read the required EXIF metadata - see https://github.com/capacitor-community/exif

I believe that this is better approach because: