diff --git a/android/capacitor/src/main/java/com/getcapacitor/plugin/Camera.java b/android/capacitor/src/main/java/com/getcapacitor/plugin/Camera.java index 81c4a05ba..4bc62e980 100644 --- a/android/capacitor/src/main/java/com/getcapacitor/plugin/Camera.java +++ b/android/capacitor/src/main/java/com/getcapacitor/plugin/Camera.java @@ -367,6 +367,7 @@ private void returnResult(PluginCall call, Bitmap bitmap, Uri u) { private void returnFileURI(PluginCall call, ExifWrapper exif, Bitmap bitmap, Uri u, ByteArrayOutputStream bitmapOutputStream) { Uri newUri = getTempImage(bitmap, u, bitmapOutputStream); + exif.copyExif(newUri.getPath()); if (newUri != null) { JSObject ret = new JSObject(); ret.put("format", "jpeg"); diff --git a/android/capacitor/src/main/java/com/getcapacitor/plugin/camera/ExifWrapper.java b/android/capacitor/src/main/java/com/getcapacitor/plugin/camera/ExifWrapper.java index 7e50d8947..01c9f6a0c 100644 --- a/android/capacitor/src/main/java/com/getcapacitor/plugin/camera/ExifWrapper.java +++ b/android/capacitor/src/main/java/com/getcapacitor/plugin/camera/ExifWrapper.java @@ -8,6 +8,160 @@ public class ExifWrapper { private final ExifInterface exif; + private final String[] attributes = new String[] { + TAG_APERTURE_VALUE, + TAG_ARTIST, + TAG_BITS_PER_SAMPLE, + TAG_BODY_SERIAL_NUMBER, + TAG_BRIGHTNESS_VALUE, + TAG_CAMERA_OWNER_NAME, + TAG_CFA_PATTERN, + TAG_COLOR_SPACE, + TAG_COMPONENTS_CONFIGURATION, + TAG_COMPRESSED_BITS_PER_PIXEL, + TAG_COMPRESSION, + TAG_CONTRAST, + TAG_COPYRIGHT, + TAG_CUSTOM_RENDERED, + TAG_DATETIME, + TAG_DATETIME_DIGITIZED, + TAG_DATETIME_ORIGINAL, + TAG_DEFAULT_CROP_SIZE, + TAG_DEVICE_SETTING_DESCRIPTION, + TAG_DIGITAL_ZOOM_RATIO, + TAG_DNG_VERSION, + TAG_EXIF_VERSION, + TAG_EXPOSURE_BIAS_VALUE, + TAG_EXPOSURE_INDEX, + TAG_EXPOSURE_MODE, + TAG_EXPOSURE_PROGRAM, + TAG_EXPOSURE_TIME, + TAG_FILE_SOURCE, + TAG_FLASH, + TAG_FLASHPIX_VERSION, + TAG_FLASH_ENERGY, + TAG_FOCAL_LENGTH, + TAG_FOCAL_LENGTH_IN_35MM_FILM, + TAG_FOCAL_PLANE_RESOLUTION_UNIT, + TAG_FOCAL_PLANE_X_RESOLUTION, + TAG_FOCAL_PLANE_Y_RESOLUTION, + TAG_F_NUMBER, + TAG_GAIN_CONTROL, + TAG_GAMMA, + TAG_GPS_ALTITUDE, + TAG_GPS_ALTITUDE_REF, + TAG_GPS_AREA_INFORMATION, + TAG_GPS_DATESTAMP, + TAG_GPS_DEST_BEARING, + TAG_GPS_DEST_BEARING_REF, + TAG_GPS_DEST_DISTANCE, + TAG_GPS_DEST_DISTANCE_REF, + TAG_GPS_DEST_LATITUDE, + TAG_GPS_DEST_LATITUDE_REF, + TAG_GPS_DEST_LONGITUDE, + TAG_GPS_DEST_LONGITUDE_REF, + TAG_GPS_DIFFERENTIAL, + TAG_GPS_DOP, + TAG_GPS_H_POSITIONING_ERROR, + TAG_GPS_IMG_DIRECTION, + TAG_GPS_IMG_DIRECTION_REF, + TAG_GPS_LATITUDE, + TAG_GPS_LATITUDE_REF, + TAG_GPS_LONGITUDE, + TAG_GPS_LONGITUDE_REF, + TAG_GPS_MAP_DATUM, + TAG_GPS_MEASURE_MODE, + TAG_GPS_PROCESSING_METHOD, + TAG_GPS_SATELLITES, + TAG_GPS_SPEED, + TAG_GPS_SPEED_REF, + TAG_GPS_STATUS, + TAG_GPS_TIMESTAMP, + TAG_GPS_TRACK, + TAG_GPS_TRACK_REF, + TAG_GPS_VERSION_ID, + TAG_IMAGE_DESCRIPTION, + TAG_IMAGE_LENGTH, + TAG_IMAGE_UNIQUE_ID, + TAG_IMAGE_WIDTH, + TAG_INTEROPERABILITY_INDEX, + TAG_ISO_SPEED, + TAG_ISO_SPEED_LATITUDE_YYY, + TAG_ISO_SPEED_LATITUDE_ZZZ, + TAG_JPEG_INTERCHANGE_FORMAT, + TAG_JPEG_INTERCHANGE_FORMAT_LENGTH, + TAG_LENS_MAKE, + TAG_LENS_MODEL, + TAG_LENS_SERIAL_NUMBER, + TAG_LENS_SPECIFICATION, + TAG_LIGHT_SOURCE, + TAG_MAKE, + TAG_MAKER_NOTE, + TAG_MAX_APERTURE_VALUE, + TAG_METERING_MODE, + TAG_MODEL, + TAG_NEW_SUBFILE_TYPE, + TAG_OECF, + TAG_OFFSET_TIME, + TAG_OFFSET_TIME_DIGITIZED, + TAG_OFFSET_TIME_ORIGINAL, + TAG_ORF_ASPECT_FRAME, + TAG_ORF_PREVIEW_IMAGE_LENGTH, + TAG_ORF_PREVIEW_IMAGE_START, + TAG_ORF_THUMBNAIL_IMAGE, + TAG_ORIENTATION, + TAG_PHOTOGRAPHIC_SENSITIVITY, + TAG_PHOTOMETRIC_INTERPRETATION, + TAG_PIXEL_X_DIMENSION, + TAG_PIXEL_Y_DIMENSION, + TAG_PLANAR_CONFIGURATION, + TAG_PRIMARY_CHROMATICITIES, + TAG_RECOMMENDED_EXPOSURE_INDEX, + TAG_REFERENCE_BLACK_WHITE, + TAG_RELATED_SOUND_FILE, + TAG_RESOLUTION_UNIT, + TAG_ROWS_PER_STRIP, + TAG_RW2_ISO, + TAG_RW2_JPG_FROM_RAW, + TAG_RW2_SENSOR_BOTTOM_BORDER, + TAG_RW2_SENSOR_LEFT_BORDER, + TAG_RW2_SENSOR_RIGHT_BORDER, + TAG_RW2_SENSOR_TOP_BORDER, + TAG_SAMPLES_PER_PIXEL, + TAG_SATURATION, + TAG_SCENE_CAPTURE_TYPE, + TAG_SCENE_TYPE, + TAG_SENSING_METHOD, + TAG_SENSITIVITY_TYPE, + TAG_SHARPNESS, + TAG_SHUTTER_SPEED_VALUE, + TAG_SOFTWARE, + TAG_SPATIAL_FREQUENCY_RESPONSE, + TAG_SPECTRAL_SENSITIVITY, + TAG_STANDARD_OUTPUT_SENSITIVITY, + TAG_STRIP_BYTE_COUNTS, + TAG_STRIP_OFFSETS, + TAG_SUBFILE_TYPE, + TAG_SUBJECT_AREA, + TAG_SUBJECT_DISTANCE, + TAG_SUBJECT_DISTANCE_RANGE, + TAG_SUBJECT_LOCATION, + TAG_SUBSEC_TIME, + TAG_SUBSEC_TIME_DIGITIZED, + TAG_SUBSEC_TIME_ORIGINAL, + TAG_THUMBNAIL_IMAGE_LENGTH, + TAG_THUMBNAIL_IMAGE_WIDTH, + TAG_TRANSFER_FUNCTION, + TAG_USER_COMMENT, + TAG_WHITE_BALANCE, + TAG_WHITE_POINT, + TAG_XMP, + TAG_X_RESOLUTION, + TAG_Y_CB_CR_COEFFICIENTS, + TAG_Y_CB_CR_POSITIONING, + TAG_Y_CB_CR_SUB_SAMPLING, + TAG_Y_RESOLUTION + }; public ExifWrapper(ExifInterface exif) { this.exif = exif; @@ -20,129 +174,9 @@ public JSObject toJson() { return ret; } - // Commented fields are for API 24. Left in to save someone the wrist damage later - - p(ret, TAG_APERTURE_VALUE); - /* - p(ret, TAG_ARTIST); - p(ret, TAG_BITS_PER_SAMPLE); - p(ret, TAG_BRIGHTNESS_VALUE); - p(ret, TAG_CFA_PATTERN); - p(ret, TAG_COLOR_SPACE); - p(ret, TAG_COMPONENTS_CONFIGURATION); - p(ret, TAG_COMPRESSED_BITS_PER_PIXEL); - p(ret, TAG_COMPRESSION); - p(ret, TAG_CONTRAST); - p(ret, TAG_COPYRIGHT); - */ - p(ret, TAG_DATETIME); - /* - p(ret, TAG_DATETIME_DIGITIZED); - p(ret, TAG_DATETIME_ORIGINAL); - p(ret, TAG_DEFAULT_CROP_SIZE); - p(ret, TAG_DEVICE_SETTING_DESCRIPTION); - p(ret, TAG_DIGITAL_ZOOM_RATIO); - p(ret, TAG_DNG_VERSION); - p(ret, TAG_EXIF_VERSION); - p(ret, TAG_EXPOSURE_BIAS_VALUE); - p(ret, TAG_EXPOSURE_INDEX); - p(ret, TAG_EXIF_VERSION); - p(ret, TAG_EXPOSURE_MODE); - p(ret, TAG_EXPOSURE_PROGRAM); - */ - p(ret, TAG_EXPOSURE_TIME); - // p(ret, TAG_F_NUMBER); - // p(ret, TAG_FILE_SOURCE); - p(ret, TAG_FLASH); - // p(ret, TAG_FLASH_ENERGY); - // p(ret, TAG_FLASHPIX_VERSION); - p(ret, TAG_FOCAL_LENGTH); - // p(ret, TAG_FOCAL_LENGTH_IN_35MM_FILM); - // p(ret, TAG_FOCAL_PLANE_RESOLUTION_UNIT); - p(ret, TAG_FOCAL_LENGTH); - // p(ret, TAG_GAIN_CONTROL); - p(ret, TAG_GPS_LATITUDE); - p(ret, TAG_GPS_LATITUDE_REF); - p(ret, TAG_GPS_LONGITUDE); - p(ret, TAG_GPS_LONGITUDE_REF); - p(ret, TAG_GPS_ALTITUDE); - p(ret, TAG_GPS_ALTITUDE_REF); - // p(ret, TAG_GPS_AREA_INFORMATION); - p(ret, TAG_GPS_DATESTAMP); - /* - API 24 - p(ret, TAG_GPS_DEST_BEARING); - p(ret, TAG_GPS_DEST_BEARING_REF); - p(ret, TAG_GPS_DEST_DISTANCE_REF); - p(ret, TAG_GPS_DEST_DISTANCE_REF); - p(ret, TAG_GPS_DEST_LATITUDE); - p(ret, TAG_GPS_DEST_LATITUDE_REF); - p(ret, TAG_GPS_DEST_LONGITUDE); - p(ret, TAG_GPS_DEST_LONGITUDE_REF); - p(ret, TAG_GPS_DIFFERENTIAL); - p(ret, TAG_GPS_DOP); - p(ret, TAG_GPS_IMG_DIRECTION); - p(ret, TAG_GPS_IMG_DIRECTION_REF); - p(ret, TAG_GPS_MAP_DATUM); - p(ret, TAG_GPS_MEASURE_MODE); - */ - p(ret, TAG_GPS_PROCESSING_METHOD); - /* - API 24 - p(ret, TAG_GPS_SATELLITES); - p(ret, TAG_GPS_SPEED); - p(ret, TAG_GPS_SPEED_REF); - p(ret, TAG_GPS_STATUS); - */ - p(ret, TAG_GPS_TIMESTAMP); - /* - API 24 - p(ret, TAG_GPS_TRACK); - p(ret, TAG_GPS_TRACK_REF); - p(ret, TAG_GPS_VERSION_ID); - p(ret, TAG_IMAGE_DESCRIPTION); - */ - p(ret, TAG_IMAGE_LENGTH); - // p(ret, TAG_IMAGE_UNIQUE_ID); - p(ret, TAG_IMAGE_WIDTH); - p(ret, TAG_ISO_SPEED); - /* - p(ret, TAG_INTEROPERABILITY_INDEX); - p(ret, TAG_ISO_SPEED_RATINGS); - p(ret, TAG_JPEG_INTERCHANGE_FORMAT); - p(ret, TAG_JPEG_INTERCHANGE_FORMAT_LENGTH); - p(ret, TAG_LIGHT_SOURCE); - */ - p(ret, TAG_MAKE); - /* - p(ret, TAG_MAKER_NOTE); - p(ret, TAG_MAX_APERTURE_VALUE); - p(ret, TAG_METERING_MODE); - */ - p(ret, TAG_MODEL); - /* - p(ret, TAG_NEW_SUBFILE_TYPE); - p(ret, TAG_OECF); - p(ret, TAG_ORF_ASPECT_FRAME); - p(ret, TAG_ORF_PREVIEW_IMAGE_LENGTH); - p(ret, TAG_ORF_PREVIEW_IMAGE_START); - */ - p(ret, TAG_ORIENTATION); - /* - p(ret, TAG_ORF_THUMBNAIL_IMAGE); - p(ret, TAG_PHOTOMETRIC_INTERPRETATION); - p(ret, TAG_PIXEL_X_DIMENSION); - p(ret, TAG_PIXEL_Y_DIMENSION); - p(ret, TAG_PLANAR_CONFIGURATION); - p(ret, TAG_PRIMARY_CHROMATICITIES); - p(ret, TAG_REFERENCE_BLACK_WHITE); - p(ret, TAG_RELATED_SOUND_FILE); - p(ret, TAG_RESOLUTION_UNIT); - p(ret, TAG_ROWS_PER_STRIP); - p(ret, TAG_RW2_ISO); - p(ret, TAG_RW2_JPG_FROM_RAW); - */ - p(ret, TAG_WHITE_BALANCE); + for (int i = 0; i < attributes.length; i++) { + p(ret, attributes[i]); + } return ret; } @@ -151,4 +185,17 @@ public void p(JSObject o, String tag) { String val = exif.getAttribute(tag); o.put(tag, val); } + + public void copyExif(String destFile) { + try { + ExifInterface destExif = new ExifInterface(destFile); + for (int i = 0; i < attributes.length; i++) { + String value = exif.getAttribute(attributes[i]); + if (value != null) { + destExif.setAttribute(attributes[i], value); + } + } + destExif.saveAttributes(); + } catch (Exception ex) {} + } } diff --git a/android/capacitor/src/main/java/com/getcapacitor/plugin/camera/ImageUtils.java b/android/capacitor/src/main/java/com/getcapacitor/plugin/camera/ImageUtils.java index a70977b13..4e282d847 100644 --- a/android/capacitor/src/main/java/com/getcapacitor/plugin/camera/ImageUtils.java +++ b/android/capacitor/src/main/java/com/getcapacitor/plugin/camera/ImageUtils.java @@ -117,7 +117,9 @@ public static Bitmap correctOrientation(final Context c, final Bitmap bitmap, fi if (orientation != 0) { Matrix matrix = new Matrix(); matrix.postRotate(orientation); - + ExifInterface exif = new ExifInterface(imageUri.getPath()); + exif.resetOrientation(); + exif.saveAttributes(); return transform(bitmap, matrix); } else { return bitmap; diff --git a/ios/Capacitor/Capacitor/Plugins/Camera.swift b/ios/Capacitor/Capacitor/Plugins/Camera.swift index 5674f3b0f..494b4307a 100644 --- a/ios/Capacitor/Capacitor/Plugins/Camera.swift +++ b/ios/Capacitor/Capacitor/Plugins/Camera.swift @@ -208,6 +208,52 @@ public class CAPCameraPlugin : CAPPlugin, UIImagePickerControllerDelegate, UINav public func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) { + if let processedImage = processImage(from: info) { + returnProcessedImage(processedImage) + } else { + self.call?.error("Error resizing image") + } + picker.dismiss(animated: true, completion: nil) + } + + func returnProcessedImage(_ processedImage: ProcessedImage) { + guard let jpeg = processedImage.generateJPEG(with: min(abs(CGFloat(settings.quality)) / 100.0, 1.0)) else { + self.call?.reject("Unable to convert image to jpeg") + return + } + + if settings.resultType == CameraResultType.base64.rawValue { + let base64String = jpeg.base64EncodedString() + + self.call?.success([ + "base64String": base64String, + "exif": processedImage.exifData, + "format": "jpeg" + ]) + } else if settings.resultType == CameraResultType.DATA_URL.rawValue { + let base64String = jpeg.base64EncodedString() + + self.call?.success([ + "dataUrl": "data:image/jpeg;base64," + base64String, + "exif": processedImage.exifData, + "format": "jpeg" + ]) + } else if settings.resultType == CameraResultType.uri.rawValue { + let path = try! saveTemporaryImage(jpeg) + guard let webPath = CAPFileManager.getPortablePath(host: bridge.getLocalUrl(), uri: URL(string: path)) else { + call?.reject("Unable to get portable path to file") + return + } + call?.success([ + "path": path, + "exif": processedImage.exifData, + "webPath": webPath, + "format": "jpeg" + ]) + } + } + + func processImage(from info: [UIImagePickerController.InfoKey: Any]) -> ProcessedImage? { var image: UIImage? var isEdited = false var isGallery = true @@ -221,8 +267,8 @@ public class CAPCameraPlugin : CAPPlugin, UIImagePickerControllerDelegate, UINav image = originalImage } - var imageMetadata: [AnyHashable: Any] = [:] - if let photoMetadata = info[UIImagePickerController.InfoKey.mediaMetadata] as? [AnyHashable: Any] { + var imageMetadata: [String: Any] = [:] + if let photoMetadata = info[UIImagePickerController.InfoKey.mediaMetadata] as? [String: Any] { imageMetadata = photoMetadata isGallery = false } @@ -232,16 +278,14 @@ public class CAPCameraPlugin : CAPPlugin, UIImagePickerControllerDelegate, UINav if settings.shouldResize { guard let convertedImage = resizeImage(image!, settings.preserveAspectRatio) else { - self.call?.error("Error resizing image") - return + return nil } image = convertedImage } if settings.shouldCorrectOrientation { guard let convertedImage = correctOrientation(image!) else { - self.call?.error("Error resizing image") - return + return nil } image = convertedImage } @@ -251,43 +295,12 @@ public class CAPCameraPlugin : CAPPlugin, UIImagePickerControllerDelegate, UINav UIImageWriteToSavedPhotosAlbum(image!, nil, nil, nil); } } - - guard let jpeg = image!.jpegData(compressionQuality: CGFloat(settings.quality/100)) else { - self.call?.error("Unable to convert image to jpeg") - return - } - if settings.resultType == CameraResultType.base64.rawValue { - let base64String = jpeg.base64EncodedString() - - self.call?.success([ - "base64String": base64String, - "exif": makeExif(imageMetadata) ?? [:], - "format": "jpeg" - ]) - } else if settings.resultType == CameraResultType.DATA_URL.rawValue { - let base64String = jpeg.base64EncodedString() - - self.call?.success([ - "dataUrl": "data:image/jpeg;base64," + base64String, - "exif": makeExif(imageMetadata) ?? [:], - "format": "jpeg" - ]) - } else if settings.resultType == CameraResultType.uri.rawValue { - let path = try! saveTemporaryImage(jpeg) - guard let webPath = CAPFileManager.getPortablePath(host: bridge.getLocalUrl(), uri: URL(string: path)) else { - call?.reject("Unable to get portable path to file") - return - } - call?.success([ - "path": path, - "exif": makeExif(imageMetadata) ?? [:], - "webPath": webPath, - "format": "jpeg" - ]) + var result = ProcessedImage(image: image!, metadata: imageMetadata) + if settings.shouldCorrectOrientation { + result.overwriteMetadataOrientation(to: 1) } - - picker.dismiss(animated: true, completion: nil) + return result } func metadataFromImageData(data: NSData)-> [String: Any]? { @@ -404,14 +417,14 @@ public class CAPCameraPlugin : CAPPlugin, UIImagePickerControllerDelegate, UINav } let hasPhotoLibraryUsage = dict["NSPhotoLibraryUsageDescription"] != nil if !hasPhotoLibraryUsage { - let docLink = DocLinks.NSPhotoLibraryUsageDescription - return "You are missing NSPhotoLibraryUsageDescription in your Info.plist file." + + let docLink = DocLinks.NSPhotoLibraryUsageDescription + return "You are missing NSPhotoLibraryUsageDescription in your Info.plist file." + " Camera will not function without it. Learn more: \(docLink.rawValue)" } let hasCameraUsage = dict["NSCameraUsageDescription"] != nil if !hasCameraUsage { - let docLink = DocLinks.NSCameraUsageDescription - return "You are missing NSCameraUsageDescription in your Info.plist file." + + let docLink = DocLinks.NSCameraUsageDescription + return "You are missing NSCameraUsageDescription in your Info.plist file." + " Camera will not function without it. Learn more: \(docLink.rawValue)" } } @@ -419,4 +432,53 @@ public class CAPCameraPlugin : CAPPlugin, UIImagePickerControllerDelegate, UINav return nil } + internal struct ProcessedImage { + var image: UIImage + var metadata: [String: Any] + + var exifData: [String: Any] { + var exifData = metadata["{Exif}"] as? [String: Any] + exifData?["Orientation"] = metadata["Orientation"] + exifData?["GPS"] = metadata["{GPS}"] + return exifData ?? [:] + } + + mutating func overwriteMetadataOrientation(to orientation: Int) { + replaceDictionaryOrientation(atNode: &metadata, to: orientation) + } + + func replaceDictionaryOrientation(atNode node: inout [String: Any], to orientation: Int) { + for key in node.keys { + if key == "Orientation", (node[key] as? Int) != nil { + node[key] = orientation + } else if var child = node[key] as? [String: Any] { + replaceDictionaryOrientation(atNode: &child, to: orientation) + node[key] = child + } + } + } + + func generateJPEG(with quality: CGFloat) -> Data? { + // convert the UIImage to a jpeg + guard let data = self.image.jpegData(compressionQuality: quality) else { + return nil + } + // define our jpeg data as an image source and get its type + guard let source = CGImageSourceCreateWithData(data as CFData, nil), let type = CGImageSourceGetType(source) else { + return data + } + // allocate an output buffer and create the destination to receive the new data + guard let output = NSMutableData(capacity: data.count), let destination = CGImageDestinationCreateWithData(output, type, 1, nil) else { + return data + } + // pipe the source into the destination while overwriting the metadata, this encodes the metadata information into the image + CGImageDestinationAddImageFromSource(destination, source, 0, self.metadata as CFDictionary) + // finish + guard CGImageDestinationFinalize(destination) else { + return data + } + return output as Data + } + } + }