@capawesome/capacitor-exif 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/CapawesomeCapacitorExif.podspec +17 -0
  2. package/LICENSE +21 -0
  3. package/Package.swift +28 -0
  4. package/README.md +322 -0
  5. package/android/build.gradle +60 -0
  6. package/android/src/main/AndroidManifest.xml +2 -0
  7. package/android/src/main/java/io/capawesome/capacitorjs/plugins/exif/Exif.java +289 -0
  8. package/android/src/main/java/io/capawesome/capacitorjs/plugins/exif/ExifPlugin.java +120 -0
  9. package/android/src/main/java/io/capawesome/capacitorjs/plugins/exif/classes/CustomException.java +20 -0
  10. package/android/src/main/java/io/capawesome/capacitorjs/plugins/exif/classes/CustomExceptions.java +27 -0
  11. package/android/src/main/java/io/capawesome/capacitorjs/plugins/exif/classes/options/ReadExifOptions.java +36 -0
  12. package/android/src/main/java/io/capawesome/capacitorjs/plugins/exif/classes/options/RemoveExifOptions.java +43 -0
  13. package/android/src/main/java/io/capawesome/capacitorjs/plugins/exif/classes/options/WriteExifOptions.java +185 -0
  14. package/android/src/main/java/io/capawesome/capacitorjs/plugins/exif/classes/results/ReadExifResult.java +152 -0
  15. package/android/src/main/java/io/capawesome/capacitorjs/plugins/exif/interfaces/Callback.java +5 -0
  16. package/android/src/main/java/io/capawesome/capacitorjs/plugins/exif/interfaces/EmptyCallback.java +5 -0
  17. package/android/src/main/java/io/capawesome/capacitorjs/plugins/exif/interfaces/NonEmptyResultCallback.java +7 -0
  18. package/android/src/main/java/io/capawesome/capacitorjs/plugins/exif/interfaces/Result.java +7 -0
  19. package/android/src/main/res/.gitkeep +0 -0
  20. package/dist/docs.json +746 -0
  21. package/dist/esm/definitions.d.ts +375 -0
  22. package/dist/esm/definitions.js +32 -0
  23. package/dist/esm/definitions.js.map +1 -0
  24. package/dist/esm/index.d.ts +4 -0
  25. package/dist/esm/index.js +7 -0
  26. package/dist/esm/index.js.map +1 -0
  27. package/dist/esm/web.d.ts +7 -0
  28. package/dist/esm/web.js +13 -0
  29. package/dist/esm/web.js.map +1 -0
  30. package/dist/plugin.cjs.js +59 -0
  31. package/dist/plugin.cjs.js.map +1 -0
  32. package/dist/plugin.js +62 -0
  33. package/dist/plugin.js.map +1 -0
  34. package/ios/Plugin/Classes/Options/ReadExifOptions.swift +20 -0
  35. package/ios/Plugin/Classes/Options/RemoveExifOptions.swift +12 -0
  36. package/ios/Plugin/Classes/Options/WriteExifOptions.swift +49 -0
  37. package/ios/Plugin/Classes/Results/ReadExifResult.swift +75 -0
  38. package/ios/Plugin/Enums/CustomError.swift +54 -0
  39. package/ios/Plugin/Exif.swift +219 -0
  40. package/ios/Plugin/ExifPlugin.swift +83 -0
  41. package/ios/Plugin/Info.plist +24 -0
  42. package/ios/Plugin/Protocols/Result.swift +5 -0
  43. package/package.json +97 -0
@@ -0,0 +1,49 @@
1
+ import Foundation
2
+ import Capacitor
3
+
4
+ @objc public class WriteExifOptions: NSObject {
5
+ let dateTimeOriginal: String?
6
+ let exposureTime: Double?
7
+ let fNumber: Double?
8
+ let flash: Bool?
9
+ let focalLength: Double?
10
+ let gpsAltitude: Double?
11
+ let gpsLatitude: Double?
12
+ let gpsLongitude: Double?
13
+ let iso: Int?
14
+ let lensModel: String?
15
+ let make: String?
16
+ let model: String?
17
+ let orientation: Int?
18
+ let software: String?
19
+ let url: URL
20
+
21
+ init(_ call: CAPPluginCall) throws {
22
+ let tags = try WriteExifOptions.getTagsFromCall(call)
23
+ self.dateTimeOriginal = tags["dateTimeOriginal"] as? String
24
+ self.exposureTime = (tags["exposureTime"] as? NSNumber)?.doubleValue
25
+ self.fNumber = (tags["fNumber"] as? NSNumber)?.doubleValue
26
+ self.flash = tags["flash"] as? Bool
27
+ self.focalLength = (tags["focalLength"] as? NSNumber)?.doubleValue
28
+ self.gpsAltitude = (tags["gpsAltitude"] as? NSNumber)?.doubleValue
29
+ self.gpsLatitude = (tags["gpsLatitude"] as? NSNumber)?.doubleValue
30
+ self.gpsLongitude = (tags["gpsLongitude"] as? NSNumber)?.doubleValue
31
+ self.iso = (tags["iso"] as? NSNumber)?.intValue
32
+ self.lensModel = tags["lensModel"] as? String
33
+ self.make = tags["make"] as? String
34
+ self.model = tags["model"] as? String
35
+ self.orientation = (tags["orientation"] as? NSNumber)?.intValue
36
+ self.software = tags["software"] as? String
37
+ self.url = try ReadExifOptions.getUrlFromCall(call)
38
+ if (gpsLatitude == nil) != (gpsLongitude == nil) {
39
+ throw CustomError.gpsCoordinatesIncomplete
40
+ }
41
+ }
42
+
43
+ private static func getTagsFromCall(_ call: CAPPluginCall) throws -> JSObject {
44
+ guard let tags = call.getObject("tags") else {
45
+ throw CustomError.tagsMissing
46
+ }
47
+ return tags
48
+ }
49
+ }
@@ -0,0 +1,75 @@
1
+ import Foundation
2
+ import Capacitor
3
+ import ImageIO
4
+
5
+ @objc public class ReadExifResult: NSObject, Result {
6
+ let properties: [String: Any]
7
+
8
+ init(properties: [String: Any]) {
9
+ self.properties = properties
10
+ }
11
+
12
+ // swiftlint:disable:next cyclomatic_complexity function_body_length
13
+ @objc public func toJSObject() -> AnyObject {
14
+ let exif = properties[kCGImagePropertyExifDictionary as String] as? [String: Any] ?? [:]
15
+ let gps = properties[kCGImagePropertyGPSDictionary as String] as? [String: Any] ?? [:]
16
+ let tiff = properties[kCGImagePropertyTIFFDictionary as String] as? [String: Any] ?? [:]
17
+ var tags = JSObject()
18
+ if let dateTimeOriginal = exif[kCGImagePropertyExifDateTimeOriginal as String] as? String {
19
+ tags["dateTimeOriginal"] = dateTimeOriginal
20
+ }
21
+ if let exposureTime = exif[kCGImagePropertyExifExposureTime as String] as? NSNumber {
22
+ tags["exposureTime"] = exposureTime.doubleValue
23
+ }
24
+ if let fNumber = exif[kCGImagePropertyExifFNumber as String] as? NSNumber {
25
+ tags["fNumber"] = fNumber.doubleValue
26
+ }
27
+ if let flash = exif[kCGImagePropertyExifFlash as String] as? NSNumber {
28
+ tags["flash"] = (flash.intValue & 1) == 1
29
+ }
30
+ if let focalLength = exif[kCGImagePropertyExifFocalLength as String] as? NSNumber {
31
+ tags["focalLength"] = focalLength.doubleValue
32
+ }
33
+ if let gpsAltitude = gps[kCGImagePropertyGPSAltitude as String] as? NSNumber {
34
+ let gpsAltitudeRef = gps[kCGImagePropertyGPSAltitudeRef as String] as? NSNumber
35
+ tags["gpsAltitude"] = gpsAltitudeRef?.intValue == 1 ? -gpsAltitude.doubleValue : gpsAltitude.doubleValue
36
+ }
37
+ if let gpsLatitude = gps[kCGImagePropertyGPSLatitude as String] as? NSNumber {
38
+ let gpsLatitudeRef = gps[kCGImagePropertyGPSLatitudeRef as String] as? String
39
+ tags["gpsLatitude"] = gpsLatitudeRef == "S" ? -gpsLatitude.doubleValue : gpsLatitude.doubleValue
40
+ }
41
+ if let gpsLongitude = gps[kCGImagePropertyGPSLongitude as String] as? NSNumber {
42
+ let gpsLongitudeRef = gps[kCGImagePropertyGPSLongitudeRef as String] as? String
43
+ tags["gpsLongitude"] = gpsLongitudeRef == "W" ? -gpsLongitude.doubleValue : gpsLongitude.doubleValue
44
+ }
45
+ if let iso = (exif[kCGImagePropertyExifISOSpeedRatings as String] as? [Any])?.first as? NSNumber {
46
+ tags["iso"] = iso.intValue
47
+ }
48
+ if let lensModel = exif[kCGImagePropertyExifLensModel as String] as? String {
49
+ tags["lensModel"] = lensModel
50
+ }
51
+ if let make = tiff[kCGImagePropertyTIFFMake as String] as? String {
52
+ tags["make"] = make
53
+ }
54
+ if let model = tiff[kCGImagePropertyTIFFModel as String] as? String {
55
+ tags["model"] = model
56
+ }
57
+ if let orientation = properties[kCGImagePropertyOrientation as String] as? NSNumber {
58
+ tags["orientation"] = orientation.intValue
59
+ } else if let orientation = tiff[kCGImagePropertyTIFFOrientation as String] as? NSNumber {
60
+ tags["orientation"] = orientation.intValue
61
+ }
62
+ if let pixelHeight = properties[kCGImagePropertyPixelHeight as String] as? NSNumber {
63
+ tags["pixelHeight"] = pixelHeight.intValue
64
+ }
65
+ if let pixelWidth = properties[kCGImagePropertyPixelWidth as String] as? NSNumber {
66
+ tags["pixelWidth"] = pixelWidth.intValue
67
+ }
68
+ if let software = tiff[kCGImagePropertyTIFFSoftware as String] as? String {
69
+ tags["software"] = software
70
+ }
71
+ var result = JSObject()
72
+ result["tags"] = tags
73
+ return result as AnyObject
74
+ }
75
+ }
@@ -0,0 +1,54 @@
1
+ import Foundation
2
+
3
+ enum CustomError: Error {
4
+ case fileNotFound
5
+ case gpsCoordinatesIncomplete
6
+ case pathMissing
7
+ case readFailed
8
+ case tagsMissing
9
+ case unsupportedFormat
10
+ case writeFailed
11
+
12
+ var code: String? {
13
+ switch self {
14
+ case .fileNotFound:
15
+ return "FILE_NOT_FOUND"
16
+ case .gpsCoordinatesIncomplete:
17
+ return nil
18
+ case .pathMissing:
19
+ return nil
20
+ case .readFailed:
21
+ return "READ_FAILED"
22
+ case .tagsMissing:
23
+ return nil
24
+ case .unsupportedFormat:
25
+ return "UNSUPPORTED_FORMAT"
26
+ case .writeFailed:
27
+ return "WRITE_FAILED"
28
+ }
29
+ }
30
+ }
31
+
32
+ extension CustomError: LocalizedError {
33
+ public var errorDescription: String? {
34
+ switch self {
35
+ case .fileNotFound:
36
+ return NSLocalizedString("The file was not found at the provided path.", comment: "fileNotFound")
37
+ case .gpsCoordinatesIncomplete:
38
+ return NSLocalizedString("gpsLatitude and gpsLongitude must be provided together.", comment: "gpsCoordinatesIncomplete")
39
+ case .pathMissing:
40
+ return NSLocalizedString("path must be provided.", comment: "pathMissing")
41
+ case .readFailed:
42
+ return NSLocalizedString("The EXIF metadata could not be read from the file.", comment: "readFailed")
43
+ case .tagsMissing:
44
+ return NSLocalizedString("tags must be provided.", comment: "tagsMissing")
45
+ case .unsupportedFormat:
46
+ return NSLocalizedString(
47
+ "The file format does not support writing or removing EXIF metadata on this platform.",
48
+ comment: "unsupportedFormat"
49
+ )
50
+ case .writeFailed:
51
+ return NSLocalizedString("The EXIF metadata could not be written to the file.", comment: "writeFailed")
52
+ }
53
+ }
54
+ }
@@ -0,0 +1,219 @@
1
+ import Foundation
2
+ import Capacitor
3
+ import ImageIO
4
+ import UniformTypeIdentifiers
5
+
6
+ @objc public class Exif: NSObject {
7
+ private static let exifExNamespace = "http://cipa.jp/exif/1.0/"
8
+ private static let exifExPrefix = "exifEX"
9
+
10
+ private let plugin: ExifPlugin
11
+
12
+ init(plugin: ExifPlugin) {
13
+ self.plugin = plugin
14
+ }
15
+
16
+ @objc public func readExif(_ options: ReadExifOptions, completion: @escaping (ReadExifResult?, Error?) -> Void) {
17
+ guard FileManager.default.fileExists(atPath: options.url.path) else {
18
+ completion(nil, CustomError.fileNotFound)
19
+ return
20
+ }
21
+ guard let source = CGImageSourceCreateWithURL(options.url as CFURL, nil),
22
+ let properties = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as? [String: Any] else {
23
+ completion(nil, CustomError.readFailed)
24
+ return
25
+ }
26
+ completion(ReadExifResult(properties: properties), nil)
27
+ }
28
+
29
+ @objc public func removeExif(_ options: RemoveExifOptions, completion: @escaping (Error?) -> Void) {
30
+ guard FileManager.default.fileExists(atPath: options.url.path) else {
31
+ completion(CustomError.fileNotFound)
32
+ return
33
+ }
34
+ guard let source = CGImageSourceCreateWithURL(options.url as CFURL, nil) else {
35
+ completion(CustomError.readFailed)
36
+ return
37
+ }
38
+ guard isWritableFormat(source) else {
39
+ completion(CustomError.unsupportedFormat)
40
+ return
41
+ }
42
+ let metadata = CGImageMetadataCreateMutable()
43
+ if options.keepOrientation, let orientation = getOrientation(of: source) {
44
+ setValue(metadata, kCGImagePropertyTIFFDictionary, kCGImagePropertyTIFFOrientation, orientation as CFNumber)
45
+ } else if isHeif(source) {
46
+ // HEIF files ignore an empty replacement metadata, so the orientation is reset to the default value instead.
47
+ setValue(metadata, kCGImagePropertyTIFFDictionary, kCGImagePropertyTIFFOrientation, 1 as CFNumber)
48
+ }
49
+ let destinationOptions: [CFString: Any] = [
50
+ kCGImageDestinationMetadata: metadata,
51
+ kCGImageMetadataShouldExcludeGPS: true,
52
+ kCGImageMetadataShouldExcludeXMP: true
53
+ ]
54
+ do {
55
+ try replaceImage(at: options.url, source: source, options: destinationOptions)
56
+ completion(nil)
57
+ } catch {
58
+ completion(error)
59
+ }
60
+ }
61
+
62
+ @objc public func writeExif(_ options: WriteExifOptions, completion: @escaping (Error?) -> Void) {
63
+ guard FileManager.default.fileExists(atPath: options.url.path) else {
64
+ completion(CustomError.fileNotFound)
65
+ return
66
+ }
67
+ guard let source = CGImageSourceCreateWithURL(options.url as CFURL, nil) else {
68
+ completion(CustomError.readFailed)
69
+ return
70
+ }
71
+ guard isWritableFormat(source) else {
72
+ completion(CustomError.unsupportedFormat)
73
+ return
74
+ }
75
+ var destinationOptions: [CFString: Any] = [:]
76
+ if isHeif(source) {
77
+ // HEIF files ignore merged metadata, so the source metadata is copied and replaced as a whole instead.
78
+ let metadata = createMutableMetadataCopy(of: source)
79
+ applyTags(options, to: metadata)
80
+ destinationOptions[kCGImageDestinationMetadata] = metadata
81
+ } else {
82
+ let metadata = CGImageMetadataCreateMutable()
83
+ applyTags(options, to: metadata)
84
+ destinationOptions[kCGImageDestinationMetadata] = metadata
85
+ destinationOptions[kCGImageDestinationMergeMetadata] = true
86
+ }
87
+ do {
88
+ try replaceImage(at: options.url, source: source, options: destinationOptions)
89
+ completion(nil)
90
+ } catch {
91
+ completion(error)
92
+ }
93
+ }
94
+
95
+ // swiftlint:disable:next cyclomatic_complexity
96
+ private func applyTags(_ options: WriteExifOptions, to metadata: CGMutableImageMetadata) {
97
+ if let dateTimeOriginal = options.dateTimeOriginal {
98
+ setValue(metadata, kCGImagePropertyExifDictionary, kCGImagePropertyExifDateTimeOriginal, dateTimeOriginal as CFString)
99
+ }
100
+ if let exposureTime = options.exposureTime {
101
+ setValue(metadata, kCGImagePropertyExifDictionary, kCGImagePropertyExifExposureTime, convertDoubleToRational(exposureTime) as CFString)
102
+ }
103
+ if let fNumber = options.fNumber {
104
+ setValue(metadata, kCGImagePropertyExifDictionary, kCGImagePropertyExifFNumber, convertDoubleToRational(fNumber) as CFString)
105
+ }
106
+ if let flash = options.flash {
107
+ setValue(metadata, kCGImagePropertyExifDictionary, kCGImagePropertyExifFlash, (flash ? 1 : 0) as CFNumber)
108
+ }
109
+ if let focalLength = options.focalLength {
110
+ setValue(metadata, kCGImagePropertyExifDictionary, kCGImagePropertyExifFocalLength, convertDoubleToRational(focalLength) as CFString)
111
+ }
112
+ if let gpsAltitude = options.gpsAltitude {
113
+ setValue(metadata, kCGImagePropertyGPSDictionary, kCGImagePropertyGPSAltitude, convertDoubleToRational(abs(gpsAltitude)) as CFString)
114
+ setValue(metadata, kCGImagePropertyGPSDictionary, kCGImagePropertyGPSAltitudeRef, (gpsAltitude < 0 ? 1 : 0) as CFNumber)
115
+ }
116
+ if let gpsLatitude = options.gpsLatitude, let gpsLongitude = options.gpsLongitude {
117
+ setValue(metadata, kCGImagePropertyGPSDictionary, kCGImagePropertyGPSLatitude, abs(gpsLatitude) as CFNumber)
118
+ setValue(metadata, kCGImagePropertyGPSDictionary, kCGImagePropertyGPSLatitudeRef, (gpsLatitude < 0 ? "S" : "N") as CFString)
119
+ setValue(metadata, kCGImagePropertyGPSDictionary, kCGImagePropertyGPSLongitude, abs(gpsLongitude) as CFNumber)
120
+ setValue(metadata, kCGImagePropertyGPSDictionary, kCGImagePropertyGPSLongitudeRef, (gpsLongitude < 0 ? "W" : "E") as CFString)
121
+ }
122
+ if let iso = options.iso {
123
+ // The ISO tag cannot be overwritten via the `exif` namespace, so the `exifEX` namespace is used as well.
124
+ _ = CGImageMetadataRemoveTagWithPath(metadata, nil, "exif:ISOSpeedRatings" as CFString)
125
+ setValue(metadata, kCGImagePropertyExifDictionary, kCGImagePropertyExifISOSpeedRatings, [iso] as CFArray)
126
+ setPhotographicSensitivity(iso, to: metadata)
127
+ }
128
+ if let lensModel = options.lensModel {
129
+ setValue(metadata, kCGImagePropertyExifDictionary, kCGImagePropertyExifLensModel, lensModel as CFString)
130
+ }
131
+ if let make = options.make {
132
+ setValue(metadata, kCGImagePropertyTIFFDictionary, kCGImagePropertyTIFFMake, make as CFString)
133
+ }
134
+ if let model = options.model {
135
+ setValue(metadata, kCGImagePropertyTIFFDictionary, kCGImagePropertyTIFFModel, model as CFString)
136
+ }
137
+ if let orientation = options.orientation {
138
+ setValue(metadata, kCGImagePropertyTIFFDictionary, kCGImagePropertyTIFFOrientation, orientation as CFNumber)
139
+ }
140
+ if let software = options.software {
141
+ setValue(metadata, kCGImagePropertyTIFFDictionary, kCGImagePropertyTIFFSoftware, software as CFString)
142
+ }
143
+ }
144
+
145
+ private func convertDoubleToRational(_ value: Double) -> String {
146
+ return "\(Int((value * 1000).rounded()))/1000"
147
+ }
148
+
149
+ private func createMutableMetadataCopy(of source: CGImageSource) -> CGMutableImageMetadata {
150
+ if let metadata = CGImageSourceCopyMetadataAtIndex(source, 0, nil), let copy = CGImageMetadataCreateMutableCopy(metadata) {
151
+ return copy
152
+ }
153
+ return CGImageMetadataCreateMutable()
154
+ }
155
+
156
+ private func getOrientation(of source: CGImageSource) -> Int? {
157
+ guard let properties = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) as? [String: Any] else {
158
+ return nil
159
+ }
160
+ if let orientation = properties[kCGImagePropertyOrientation as String] as? NSNumber {
161
+ return orientation.intValue
162
+ }
163
+ let tiff = properties[kCGImagePropertyTIFFDictionary as String] as? [String: Any]
164
+ if let orientation = tiff?[kCGImagePropertyTIFFOrientation as String] as? NSNumber {
165
+ return orientation.intValue
166
+ }
167
+ return nil
168
+ }
169
+
170
+ private func getUTType(of source: CGImageSource) -> UTType? {
171
+ guard let type = CGImageSourceGetType(source) else {
172
+ return nil
173
+ }
174
+ return UTType(type as String)
175
+ }
176
+
177
+ private func isHeif(_ source: CGImageSource) -> Bool {
178
+ guard let utType = getUTType(of: source) else {
179
+ return false
180
+ }
181
+ return utType.conforms(to: .heic) || utType.conforms(to: .heif)
182
+ }
183
+
184
+ private func isWritableFormat(_ source: CGImageSource) -> Bool {
185
+ guard let utType = getUTType(of: source) else {
186
+ return false
187
+ }
188
+ return utType.conforms(to: .jpeg) || utType.conforms(to: .heic) || utType.conforms(to: .heif)
189
+ }
190
+
191
+ private func replaceImage(at url: URL, source: CGImageSource, options: [CFString: Any]) throws {
192
+ guard let type = CGImageSourceGetType(source) else {
193
+ throw CustomError.unsupportedFormat
194
+ }
195
+ let temporaryUrl = url.deletingLastPathComponent().appendingPathComponent(UUID().uuidString)
196
+ guard let destination = CGImageDestinationCreateWithURL(temporaryUrl as CFURL, type, 1, nil) else {
197
+ throw CustomError.writeFailed
198
+ }
199
+ guard CGImageDestinationCopyImageSource(destination, source, options as CFDictionary, nil) else {
200
+ try? FileManager.default.removeItem(at: temporaryUrl)
201
+ throw CustomError.writeFailed
202
+ }
203
+ _ = try FileManager.default.replaceItemAt(url, withItemAt: temporaryUrl)
204
+ }
205
+
206
+ private func setPhotographicSensitivity(_ iso: Int, to metadata: CGMutableImageMetadata) {
207
+ let namespace = Exif.exifExNamespace as CFString
208
+ let prefix = Exif.exifExPrefix as CFString
209
+ _ = CGImageMetadataRegisterNamespaceForPrefix(metadata, namespace, prefix, nil)
210
+ guard let tag = CGImageMetadataTagCreate(namespace, prefix, "PhotographicSensitivity" as CFString, .string, String(iso) as CFString) else {
211
+ return
212
+ }
213
+ _ = CGImageMetadataSetTagWithPath(metadata, nil, "exifEX:PhotographicSensitivity" as CFString, tag)
214
+ }
215
+
216
+ private func setValue(_ metadata: CGMutableImageMetadata, _ dictionary: CFString, _ property: CFString, _ value: CFTypeRef) {
217
+ _ = CGImageMetadataSetValueMatchingImageProperty(metadata, dictionary, property, value)
218
+ }
219
+ }
@@ -0,0 +1,83 @@
1
+ import Foundation
2
+ import Capacitor
3
+
4
+ @objc(ExifPlugin)
5
+ public class ExifPlugin: CAPPlugin, CAPBridgedPlugin {
6
+ public let identifier = "ExifPlugin"
7
+ public let jsName = "Exif"
8
+ public let pluginMethods: [CAPPluginMethod] = [
9
+ CAPPluginMethod(name: "readExif", returnType: CAPPluginReturnPromise),
10
+ CAPPluginMethod(name: "removeExif", returnType: CAPPluginReturnPromise),
11
+ CAPPluginMethod(name: "writeExif", returnType: CAPPluginReturnPromise)
12
+ ]
13
+ public static let tag = "ExifPlugin"
14
+
15
+ private var implementation: Exif?
16
+
17
+ override public func load() {
18
+ self.implementation = Exif(plugin: self)
19
+ }
20
+
21
+ @objc func readExif(_ call: CAPPluginCall) {
22
+ do {
23
+ let options = try ReadExifOptions(call)
24
+ implementation?.readExif(options) { result, error in
25
+ if let error = error {
26
+ self.rejectCall(call, error)
27
+ return
28
+ }
29
+ self.resolveCall(call, result)
30
+ }
31
+ } catch {
32
+ rejectCall(call, error)
33
+ }
34
+ }
35
+
36
+ @objc func removeExif(_ call: CAPPluginCall) {
37
+ do {
38
+ let options = try RemoveExifOptions(call)
39
+ implementation?.removeExif(options) { error in
40
+ if let error = error {
41
+ self.rejectCall(call, error)
42
+ return
43
+ }
44
+ self.resolveCall(call)
45
+ }
46
+ } catch {
47
+ rejectCall(call, error)
48
+ }
49
+ }
50
+
51
+ @objc func writeExif(_ call: CAPPluginCall) {
52
+ do {
53
+ let options = try WriteExifOptions(call)
54
+ implementation?.writeExif(options) { error in
55
+ if let error = error {
56
+ self.rejectCall(call, error)
57
+ return
58
+ }
59
+ self.resolveCall(call)
60
+ }
61
+ } catch {
62
+ rejectCall(call, error)
63
+ }
64
+ }
65
+
66
+ private func rejectCall(_ call: CAPPluginCall, _ error: Error) {
67
+ CAPLog.print("[", ExifPlugin.tag, "] ", error)
68
+ let code = (error as? CustomError)?.code
69
+ call.reject(error.localizedDescription, code)
70
+ }
71
+
72
+ private func resolveCall(_ call: CAPPluginCall) {
73
+ call.resolve()
74
+ }
75
+
76
+ private func resolveCall(_ call: CAPPluginCall, _ result: Result?) {
77
+ if let result = result?.toJSObject() as? JSObject {
78
+ call.resolve(result)
79
+ } else {
80
+ call.resolve()
81
+ }
82
+ }
83
+ }
@@ -0,0 +1,24 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3
+ <plist version="1.0">
4
+ <dict>
5
+ <key>CFBundleDevelopmentRegion</key>
6
+ <string>$(DEVELOPMENT_LANGUAGE)</string>
7
+ <key>CFBundleExecutable</key>
8
+ <string>$(EXECUTABLE_NAME)</string>
9
+ <key>CFBundleIdentifier</key>
10
+ <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
11
+ <key>CFBundleInfoDictionaryVersion</key>
12
+ <string>6.0</string>
13
+ <key>CFBundleName</key>
14
+ <string>$(PRODUCT_NAME)</string>
15
+ <key>CFBundlePackageType</key>
16
+ <string>FMWK</string>
17
+ <key>CFBundleShortVersionString</key>
18
+ <string>1.0</string>
19
+ <key>CFBundleVersion</key>
20
+ <string>$(CURRENT_PROJECT_VERSION)</string>
21
+ <key>NSPrincipalClass</key>
22
+ <string></string>
23
+ </dict>
24
+ </plist>
@@ -0,0 +1,5 @@
1
+ import Capacitor
2
+
3
+ @objc public protocol Result {
4
+ @objc func toJSObject() -> AnyObject
5
+ }
package/package.json ADDED
@@ -0,0 +1,97 @@
1
+ {
2
+ "name": "@capawesome/capacitor-exif",
3
+ "version": "0.0.1",
4
+ "description": "Capacitor plugin to read, write and remove EXIF metadata from image files.",
5
+ "main": "dist/plugin.cjs.js",
6
+ "module": "dist/esm/index.js",
7
+ "types": "dist/esm/index.d.ts",
8
+ "unpkg": "dist/plugin.js",
9
+ "files": [
10
+ "android/src/main/",
11
+ "android/build.gradle",
12
+ "dist/",
13
+ "ios/Plugin/",
14
+ "CapawesomeCapacitorExif.podspec",
15
+ "Package.swift"
16
+ ],
17
+ "author": "Robin Genz <mail@robingenz.dev>",
18
+ "license": "MIT",
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/capawesome-team/capacitor-plugins.git"
22
+ },
23
+ "bugs": {
24
+ "url": "https://github.com/capawesome-team/capacitor-plugins/issues"
25
+ },
26
+ "funding": [
27
+ {
28
+ "type": "github",
29
+ "url": "https://github.com/sponsors/capawesome-team/"
30
+ },
31
+ {
32
+ "type": "opencollective",
33
+ "url": "https://opencollective.com/capawesome"
34
+ }
35
+ ],
36
+ "homepage": "https://capawesome.io/docs/sdks/capacitor/exif/",
37
+ "keywords": [
38
+ "capacitor",
39
+ "plugin",
40
+ "native",
41
+ "exif",
42
+ "metadata",
43
+ "gps",
44
+ "privacy",
45
+ "heic",
46
+ "jpeg"
47
+ ],
48
+ "scripts": {
49
+ "verify": "npm run verify:ios && npm run verify:android && npm run verify:web",
50
+ "verify:ios": "cd ios && pod install && xcodebuild -workspace Plugin.xcworkspace -scheme Plugin -destination generic/platform=iOS && cd ..",
51
+ "verify:android": "cd android && ./gradlew clean build test && cd ..",
52
+ "verify:web": "npm run build",
53
+ "lint": "npm run eslint && npm run prettier -- --check && npm run swiftlint -- lint",
54
+ "fmt": "npm run eslint -- --fix && npm run prettier -- --write && npm run swiftlint -- --fix --format",
55
+ "eslint": "eslint . --ext ts",
56
+ "prettier": "prettier \"**/*.{css,html,ts,js,java}\"",
57
+ "swiftlint": "node-swiftlint",
58
+ "docgen": "docgen --api ExifPlugin --output-readme README.md --output-json dist/docs.json",
59
+ "build": "npm run clean && npm run docgen && tsc && rollup -c rollup.config.mjs",
60
+ "clean": "rimraf ./dist",
61
+ "watch": "tsc --watch",
62
+ "ios:pod:install": "cd ios && pod install --repo-update && cd ..",
63
+ "ios:spm:install": "cd ios && swift package resolve && cd ..",
64
+ "prepublishOnly": "npm run build"
65
+ },
66
+ "devDependencies": {
67
+ "@capacitor/android": "8.0.0",
68
+ "@capacitor/cli": "8.0.0",
69
+ "@capacitor/core": "8.0.0",
70
+ "@capacitor/docgen": "0.3.1",
71
+ "@capacitor/ios": "8.0.0",
72
+ "@ionic/eslint-config": "0.4.0",
73
+ "eslint": "8.57.0",
74
+ "prettier-plugin-java": "2.6.7",
75
+ "rimraf": "6.1.2",
76
+ "rollup": "4.53.3",
77
+ "swiftlint": "2.0.0",
78
+ "typescript": "5.9.3"
79
+ },
80
+ "peerDependencies": {
81
+ "@capacitor/core": ">=8.0.0"
82
+ },
83
+ "eslintConfig": {
84
+ "extends": "@ionic/eslint-config/recommended"
85
+ },
86
+ "capacitor": {
87
+ "ios": {
88
+ "src": "ios"
89
+ },
90
+ "android": {
91
+ "src": "android"
92
+ }
93
+ },
94
+ "publishConfig": {
95
+ "access": "public"
96
+ }
97
+ }