@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.
- package/CapawesomeCapacitorExif.podspec +17 -0
- package/LICENSE +21 -0
- package/Package.swift +28 -0
- package/README.md +322 -0
- package/android/build.gradle +60 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/exif/Exif.java +289 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/exif/ExifPlugin.java +120 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/exif/classes/CustomException.java +20 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/exif/classes/CustomExceptions.java +27 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/exif/classes/options/ReadExifOptions.java +36 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/exif/classes/options/RemoveExifOptions.java +43 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/exif/classes/options/WriteExifOptions.java +185 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/exif/classes/results/ReadExifResult.java +152 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/exif/interfaces/Callback.java +5 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/exif/interfaces/EmptyCallback.java +5 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/exif/interfaces/NonEmptyResultCallback.java +7 -0
- package/android/src/main/java/io/capawesome/capacitorjs/plugins/exif/interfaces/Result.java +7 -0
- package/android/src/main/res/.gitkeep +0 -0
- package/dist/docs.json +746 -0
- package/dist/esm/definitions.d.ts +375 -0
- package/dist/esm/definitions.js +32 -0
- package/dist/esm/definitions.js.map +1 -0
- package/dist/esm/index.d.ts +4 -0
- package/dist/esm/index.js +7 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/web.d.ts +7 -0
- package/dist/esm/web.js +13 -0
- package/dist/esm/web.js.map +1 -0
- package/dist/plugin.cjs.js +59 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/plugin.js +62 -0
- package/dist/plugin.js.map +1 -0
- package/ios/Plugin/Classes/Options/ReadExifOptions.swift +20 -0
- package/ios/Plugin/Classes/Options/RemoveExifOptions.swift +12 -0
- package/ios/Plugin/Classes/Options/WriteExifOptions.swift +49 -0
- package/ios/Plugin/Classes/Results/ReadExifResult.swift +75 -0
- package/ios/Plugin/Enums/CustomError.swift +54 -0
- package/ios/Plugin/Exif.swift +219 -0
- package/ios/Plugin/ExifPlugin.swift +83 -0
- package/ios/Plugin/Info.plist +24 -0
- package/ios/Plugin/Protocols/Result.swift +5 -0
- 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>
|
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
|
+
}
|