Add exif support
This commit is contained in:
parent
660eaa2819
commit
2ebfe762f0
|
@ -24,7 +24,7 @@
|
||||||
F8210DE52966E160001D9973 /* Color+SystemColors.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8210DE42966E160001D9973 /* Color+SystemColors.swift */; };
|
F8210DE52966E160001D9973 /* Color+SystemColors.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8210DE42966E160001D9973 /* Color+SystemColors.swift */; };
|
||||||
F8210DE72966E1D1001D9973 /* Color+Assets.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8210DE62966E1D1001D9973 /* Color+Assets.swift */; };
|
F8210DE72966E1D1001D9973 /* Color+Assets.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8210DE62966E1D1001D9973 /* Color+Assets.swift */; };
|
||||||
F8210DEA2966E4F9001D9973 /* AnimatePlaceholderModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8210DE92966E4F9001D9973 /* AnimatePlaceholderModifier.swift */; };
|
F8210DEA2966E4F9001D9973 /* AnimatePlaceholderModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8210DE92966E4F9001D9973 /* AnimatePlaceholderModifier.swift */; };
|
||||||
F8341F90295C636C009C8EE6 /* UIImage+Exif.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8341F8F295C636C009C8EE6 /* UIImage+Exif.swift */; };
|
F8341F90295C636C009C8EE6 /* Data+Exif.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8341F8F295C636C009C8EE6 /* Data+Exif.swift */; };
|
||||||
F8341F92295C63BB009C8EE6 /* ImageStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8341F91295C63BB009C8EE6 /* ImageStatus.swift */; };
|
F8341F92295C63BB009C8EE6 /* ImageStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8341F91295C63BB009C8EE6 /* ImageStatus.swift */; };
|
||||||
F83901A6295D8EC000456AE2 /* LabelIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = F83901A5295D8EC000456AE2 /* LabelIcon.swift */; };
|
F83901A6295D8EC000456AE2 /* LabelIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = F83901A5295D8EC000456AE2 /* LabelIcon.swift */; };
|
||||||
F85D4971296402DC00751DF7 /* AuthorizationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85D4970296402DC00751DF7 /* AuthorizationService.swift */; };
|
F85D4971296402DC00751DF7 /* AuthorizationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85D4970296402DC00751DF7 /* AuthorizationService.swift */; };
|
||||||
|
@ -75,6 +75,8 @@
|
||||||
F8A93D7E2965FD89001D8331 /* UserProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8A93D7D2965FD89001D8331 /* UserProfileView.swift */; };
|
F8A93D7E2965FD89001D8331 /* UserProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8A93D7D2965FD89001D8331 /* UserProfileView.swift */; };
|
||||||
F8A93D802965FED4001D8331 /* AccountService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8A93D7F2965FED4001D8331 /* AccountService.swift */; };
|
F8A93D802965FED4001D8331 /* AccountService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8A93D7F2965FED4001D8331 /* AccountService.swift */; };
|
||||||
F8A93D822965FF5D001D8331 /* MastodonClientAuthenticated+Account.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8A93D812965FF5D001D8331 /* MastodonClientAuthenticated+Account.swift */; };
|
F8A93D822965FF5D001D8331 /* MastodonClientAuthenticated+Account.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8A93D812965FF5D001D8331 /* MastodonClientAuthenticated+Account.swift */; };
|
||||||
|
F8C14392296AF0B3001FE31D /* String+Exif.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8C14391296AF0B3001FE31D /* String+Exif.swift */; };
|
||||||
|
F8C14394296AF21B001FE31D /* Double+Round.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8C14393296AF21B001FE31D /* Double+Round.swift */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
|
@ -92,7 +94,7 @@
|
||||||
F8210DE42966E160001D9973 /* Color+SystemColors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+SystemColors.swift"; sourceTree = "<group>"; };
|
F8210DE42966E160001D9973 /* Color+SystemColors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+SystemColors.swift"; sourceTree = "<group>"; };
|
||||||
F8210DE62966E1D1001D9973 /* Color+Assets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+Assets.swift"; sourceTree = "<group>"; };
|
F8210DE62966E1D1001D9973 /* Color+Assets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Color+Assets.swift"; sourceTree = "<group>"; };
|
||||||
F8210DE92966E4F9001D9973 /* AnimatePlaceholderModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnimatePlaceholderModifier.swift; sourceTree = "<group>"; };
|
F8210DE92966E4F9001D9973 /* AnimatePlaceholderModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AnimatePlaceholderModifier.swift; sourceTree = "<group>"; };
|
||||||
F8341F8F295C636C009C8EE6 /* UIImage+Exif.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+Exif.swift"; sourceTree = "<group>"; };
|
F8341F8F295C636C009C8EE6 /* Data+Exif.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+Exif.swift"; sourceTree = "<group>"; };
|
||||||
F8341F91295C63BB009C8EE6 /* ImageStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageStatus.swift; sourceTree = "<group>"; };
|
F8341F91295C63BB009C8EE6 /* ImageStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageStatus.swift; sourceTree = "<group>"; };
|
||||||
F83901A5295D8EC000456AE2 /* LabelIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabelIcon.swift; sourceTree = "<group>"; };
|
F83901A5295D8EC000456AE2 /* LabelIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabelIcon.swift; sourceTree = "<group>"; };
|
||||||
F85D4970296402DC00751DF7 /* AuthorizationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthorizationService.swift; sourceTree = "<group>"; };
|
F85D4970296402DC00751DF7 /* AuthorizationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthorizationService.swift; sourceTree = "<group>"; };
|
||||||
|
@ -145,6 +147,8 @@
|
||||||
F8A93D7D2965FD89001D8331 /* UserProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileView.swift; sourceTree = "<group>"; };
|
F8A93D7D2965FD89001D8331 /* UserProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserProfileView.swift; sourceTree = "<group>"; };
|
||||||
F8A93D7F2965FED4001D8331 /* AccountService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountService.swift; sourceTree = "<group>"; };
|
F8A93D7F2965FED4001D8331 /* AccountService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountService.swift; sourceTree = "<group>"; };
|
||||||
F8A93D812965FF5D001D8331 /* MastodonClientAuthenticated+Account.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MastodonClientAuthenticated+Account.swift"; sourceTree = "<group>"; };
|
F8A93D812965FF5D001D8331 /* MastodonClientAuthenticated+Account.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MastodonClientAuthenticated+Account.swift"; sourceTree = "<group>"; };
|
||||||
|
F8C14391296AF0B3001FE31D /* String+Exif.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Exif.swift"; sourceTree = "<group>"; };
|
||||||
|
F8C14393296AF21B001FE31D /* Double+Round.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Double+Round.swift"; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
|
@ -192,13 +196,15 @@
|
||||||
F8341F94295C63FE009C8EE6 /* Extensions */ = {
|
F8341F94295C63FE009C8EE6 /* Extensions */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
F8341F8F295C636C009C8EE6 /* UIImage+Exif.swift */,
|
F8341F8F295C636C009C8EE6 /* Data+Exif.swift */,
|
||||||
F85D4980296417F700751DF7 /* MastodonClientAuthenticated+Context.swift */,
|
F85D4980296417F700751DF7 /* MastodonClientAuthenticated+Context.swift */,
|
||||||
F8A93D812965FF5D001D8331 /* MastodonClientAuthenticated+Account.swift */,
|
F8A93D812965FF5D001D8331 /* MastodonClientAuthenticated+Account.swift */,
|
||||||
F85D49862964334100751DF7 /* String+Date.swift */,
|
F85D49862964334100751DF7 /* String+Date.swift */,
|
||||||
|
F8C14391296AF0B3001FE31D /* String+Exif.swift */,
|
||||||
F8210DE22966D256001D9973 /* Status+StatusData.swift */,
|
F8210DE22966D256001D9973 /* Status+StatusData.swift */,
|
||||||
F8210DE42966E160001D9973 /* Color+SystemColors.swift */,
|
F8210DE42966E160001D9973 /* Color+SystemColors.swift */,
|
||||||
F8210DE62966E1D1001D9973 /* Color+Assets.swift */,
|
F8210DE62966E1D1001D9973 /* Color+Assets.swift */,
|
||||||
|
F8C14393296AF21B001FE31D /* Double+Round.swift */,
|
||||||
);
|
);
|
||||||
path = Extensions;
|
path = Extensions;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -452,7 +458,7 @@
|
||||||
F800480A2961EA1900E6868A /* AttachmentDataHandler.swift in Sources */,
|
F800480A2961EA1900E6868A /* AttachmentDataHandler.swift in Sources */,
|
||||||
F80048032961850500E6868A /* AttachmentData+CoreDataClass.swift in Sources */,
|
F80048032961850500E6868A /* AttachmentData+CoreDataClass.swift in Sources */,
|
||||||
F897978D2968369600B22335 /* HapticService.swift in Sources */,
|
F897978D2968369600B22335 /* HapticService.swift in Sources */,
|
||||||
F8341F90295C636C009C8EE6 /* UIImage+Exif.swift in Sources */,
|
F8341F90295C636C009C8EE6 /* Data+Exif.swift in Sources */,
|
||||||
F8A93D7E2965FD89001D8331 /* UserProfileView.swift in Sources */,
|
F8A93D7E2965FD89001D8331 /* UserProfileView.swift in Sources */,
|
||||||
F85D4981296417F700751DF7 /* MastodonClientAuthenticated+Context.swift in Sources */,
|
F85D4981296417F700751DF7 /* MastodonClientAuthenticated+Context.swift in Sources */,
|
||||||
F88C246E295C37B80006098B /* MainView.swift in Sources */,
|
F88C246E295C37B80006098B /* MainView.swift in Sources */,
|
||||||
|
@ -469,6 +475,7 @@
|
||||||
F88ABD9429687CA4004EF61E /* ComposeView.swift in Sources */,
|
F88ABD9429687CA4004EF61E /* ComposeView.swift in Sources */,
|
||||||
F85D497D29640D5900751DF7 /* InteractionRow.swift in Sources */,
|
F85D497D29640D5900751DF7 /* InteractionRow.swift in Sources */,
|
||||||
F866F6A729604629002E8F88 /* SignInView.swift in Sources */,
|
F866F6A729604629002E8F88 /* SignInView.swift in Sources */,
|
||||||
|
F8C14392296AF0B3001FE31D /* String+Exif.swift in Sources */,
|
||||||
F88C246C295C37B80006098B /* VernissageApp.swift in Sources */,
|
F88C246C295C37B80006098B /* VernissageApp.swift in Sources */,
|
||||||
F85D4971296402DC00751DF7 /* AuthorizationService.swift in Sources */,
|
F85D4971296402DC00751DF7 /* AuthorizationService.swift in Sources */,
|
||||||
F88FAD25295F3FF7009B20C9 /* FederatedFeedView.swift in Sources */,
|
F88FAD25295F3FF7009B20C9 /* FederatedFeedView.swift in Sources */,
|
||||||
|
@ -477,6 +484,7 @@
|
||||||
F897978F29684BCB00B22335 /* LoadingView.swift in Sources */,
|
F897978F29684BCB00B22335 /* LoadingView.swift in Sources */,
|
||||||
F88FAD2D295F4AD7009B20C9 /* ApplicationState.swift in Sources */,
|
F88FAD2D295F4AD7009B20C9 /* ApplicationState.swift in Sources */,
|
||||||
F866F6A1296040A8002E8F88 /* ApplicationSettings+CoreDataProperties.swift in Sources */,
|
F866F6A1296040A8002E8F88 /* ApplicationSettings+CoreDataProperties.swift in Sources */,
|
||||||
|
F8C14394296AF21B001FE31D /* Double+Round.swift in Sources */,
|
||||||
F866F6AE29606367002E8F88 /* ApplicationViewMode.swift in Sources */,
|
F866F6AE29606367002E8F88 /* ApplicationViewMode.swift in Sources */,
|
||||||
F8A93D802965FED4001D8331 /* AccountService.swift in Sources */,
|
F8A93D802965FED4001D8331 /* AccountService.swift in Sources */,
|
||||||
F866F6AA29605AFA002E8F88 /* SceneDelegate.swift in Sources */,
|
F866F6AA29605AFA002E8F88 /* SceneDelegate.swift in Sources */,
|
||||||
|
|
|
@ -3,13 +3,10 @@
|
||||||
// Copyright © 2023 Marcin Czachurski and the repository contributors.
|
// Copyright © 2023 Marcin Czachurski and the repository contributors.
|
||||||
// Licensed under the MIT License.
|
// Licensed under the MIT License.
|
||||||
//
|
//
|
||||||
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import CoreData
|
import CoreData
|
||||||
|
|
||||||
|
|
||||||
extension AttachmentData {
|
extension AttachmentData {
|
||||||
|
|
||||||
@nonobjc public class func fetchRequest() -> NSFetchRequest<AttachmentData> {
|
@nonobjc public class func fetchRequest() -> NSFetchRequest<AttachmentData> {
|
||||||
|
@ -25,10 +22,13 @@ extension AttachmentData {
|
||||||
@NSManaged public var text: String?
|
@NSManaged public var text: String?
|
||||||
@NSManaged public var type: String
|
@NSManaged public var type: String
|
||||||
@NSManaged public var url: URL
|
@NSManaged public var url: URL
|
||||||
|
@NSManaged public var exifCamera: String?
|
||||||
|
@NSManaged public var exifLens: String?
|
||||||
|
@NSManaged public var exifExposure: String?
|
||||||
|
@NSManaged public var exifCreatedDate: String?
|
||||||
@NSManaged public var statusRelation: StatusData?
|
@NSManaged public var statusRelation: StatusData?
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension AttachmentData : Identifiable {
|
extension AttachmentData : Identifiable {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,100 @@
|
||||||
|
//
|
||||||
|
// https://mczachurski.dev
|
||||||
|
// Copyright © 2022 Marcin Czachurski and the repository contributors.
|
||||||
|
// Licensed under the MIT License.
|
||||||
|
//
|
||||||
|
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
public extension Dictionary<String, Any> {
|
||||||
|
func getExifValue(_ key: String) -> String? {
|
||||||
|
if let value = self[key] as? String {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
if let dictionary = self[key] as? [String: Any], let value = dictionary[key] {
|
||||||
|
return value as? String
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension Data {
|
||||||
|
func getExifData() -> [String: Any]? {
|
||||||
|
var imageInfo: [String: Any]? = nil
|
||||||
|
|
||||||
|
guard let imageSource = CGImageSourceCreateWithData(self as CFData, nil),
|
||||||
|
let metadata = CGImageSourceCopyMetadataAtIndex(imageSource, 0, nil),
|
||||||
|
let tags = CGImageMetadataCopyTags(metadata) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
imageInfo = self.readMetadataTagArr(tagArr: tags)
|
||||||
|
return imageInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reads the Arrays of tags and convert them into a dictionary of [String: Any].
|
||||||
|
private func readMetadataTagArr(tagArr: CFArray) -> [String: Any]? {
|
||||||
|
var result = [String: Any]()
|
||||||
|
|
||||||
|
for (_, tag) in (tagArr as NSArray).enumerated() {
|
||||||
|
let tagMetadata = tag as! CGImageMetadataTag
|
||||||
|
if let cfName = CGImageMetadataTagCopyName(tagMetadata) {
|
||||||
|
let name = String(cfName)
|
||||||
|
result[name] = self.readMetadataTag(metadataTag: tagMetadata)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Convert CGImageMetadataTag to a dictionary of [String: Any].
|
||||||
|
private func readMetadataTag(metadataTag: CGImageMetadataTag) -> [String: Any] {
|
||||||
|
var result = [String: Any]()
|
||||||
|
guard let cfName = CGImageMetadataTagCopyName(metadataTag) else { return result }
|
||||||
|
let name = String(cfName)
|
||||||
|
let value = CGImageMetadataTagCopyValue(metadataTag)
|
||||||
|
|
||||||
|
/// checking the type of `value` object and then performing respective operation on `value`
|
||||||
|
if CFGetTypeID(value) == CFStringGetTypeID() {
|
||||||
|
let valueStr = String(value as! CFString)
|
||||||
|
result[name] = valueStr
|
||||||
|
} else if CFGetTypeID(value) == CFDictionaryGetTypeID() {
|
||||||
|
let nsDict: NSDictionary = value as! CFDictionary
|
||||||
|
result[name] = self.getDictionary(from: nsDict)
|
||||||
|
} else if CFGetTypeID(value) == CFArrayGetTypeID() {
|
||||||
|
let valueArr: NSArray = value as! CFArray
|
||||||
|
for (_, item) in valueArr.enumerated() {
|
||||||
|
let tagMetadata = item as! CGImageMetadataTag
|
||||||
|
result[name] = self.readMetadataTag(metadataTag: tagMetadata)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// when the data was of some other type
|
||||||
|
let descriptionString: CFString = CFCopyDescription(value);
|
||||||
|
let str = String(descriptionString)
|
||||||
|
result[name] = str
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converting CGImage Metadata dictionary to [String: Any]
|
||||||
|
private func getDictionary(from nsDict: NSDictionary) -> [String: Any] {
|
||||||
|
var subDictionary = [String: Any]()
|
||||||
|
for (key, val) in nsDict {
|
||||||
|
guard let key = key as? String else { continue }
|
||||||
|
let tempDict: [String: Any] = [key: val]
|
||||||
|
if JSONSerialization.isValidJSONObject(tempDict) {
|
||||||
|
subDictionary[key] = val
|
||||||
|
} else {
|
||||||
|
let mData = val as! CGImageMetadataTag
|
||||||
|
let tempDict: [String: Any] = [key: self.readMetadataTag(metadataTag: mData)]
|
||||||
|
if JSONSerialization.isValidJSONObject(tempDict) {
|
||||||
|
subDictionary[key] = tempDict
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return subDictionary
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
//
|
||||||
|
// https://mczachurski.dev
|
||||||
|
// Copyright © 2023 Marcin Czachurski and the repository contributors.
|
||||||
|
// Licensed under the MIT License.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension Double {
|
||||||
|
func rounded(toPlaces places:Int) -> Double {
|
||||||
|
let divisor = pow(10.0, Double(places))
|
||||||
|
return (self * divisor).rounded() / divisor
|
||||||
|
}
|
||||||
|
}
|
|
@ -26,7 +26,28 @@ extension Status {
|
||||||
attachmentData.statusId = statusData.id
|
attachmentData.statusId = statusData.id
|
||||||
attachmentData.data = imageData
|
attachmentData.data = imageData
|
||||||
|
|
||||||
// TODO: read exif informatio
|
// Read exif information.
|
||||||
|
if let exifProperties = imageData.getExifData() {
|
||||||
|
if let make = exifProperties.getExifValue("Make"), let model = exifProperties.getExifValue("Model") {
|
||||||
|
attachmentData.exifCamera = "\(make) \(model)"
|
||||||
|
}
|
||||||
|
|
||||||
|
// "Lens" or "Lens Model"
|
||||||
|
if let lens = exifProperties.getExifValue("Lens") {
|
||||||
|
attachmentData.exifLens = lens
|
||||||
|
}
|
||||||
|
|
||||||
|
if let createData = exifProperties.getExifValue("CreateDate") {
|
||||||
|
attachmentData.exifCreatedDate = createData
|
||||||
|
}
|
||||||
|
|
||||||
|
if let focalLenIn35mmFilm = exifProperties.getExifValue("FocalLenIn35mmFilm"),
|
||||||
|
let fNumber = exifProperties.getExifValue("FNumber")?.calculateExifNumber(),
|
||||||
|
let exposureTime = exifProperties.getExifValue("ExposureTime"),
|
||||||
|
let photographicSensitivity = exifProperties.getExifValue("PhotographicSensitivity") {
|
||||||
|
attachmentData.exifExposure = "\(focalLenIn35mmFilm)mm, f/\(fNumber), \(exposureTime)s, ISO \(photographicSensitivity)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
attachmentData.statusRelation = statusData
|
attachmentData.statusRelation = statusData
|
||||||
statusData.addToAttachmentRelation(attachmentData)
|
statusData.addToAttachmentRelation(attachmentData)
|
||||||
|
|
|
@ -0,0 +1,27 @@
|
||||||
|
//
|
||||||
|
// https://mczachurski.dev
|
||||||
|
// Copyright © 2023 Marcin Czachurski and the repository contributors.
|
||||||
|
// Licensed under the MIT License.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension String {
|
||||||
|
func calculateExifNumber() -> String? {
|
||||||
|
guard self.contains("/") else {
|
||||||
|
return self
|
||||||
|
}
|
||||||
|
|
||||||
|
let parts = self.split(separator: "/")
|
||||||
|
guard parts.count == 2 else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if let first = Int(parts[0]), let second = Int(parts[1]) {
|
||||||
|
let calculated = Double(first) / Double(second)
|
||||||
|
return String(calculated.rounded(toPlaces: 2))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,27 +0,0 @@
|
||||||
//
|
|
||||||
// https://mczachurski.dev
|
|
||||||
// Copyright © 2022 Marcin Czachurski and the repository contributors.
|
|
||||||
// Licensed under the MIT License.
|
|
||||||
//
|
|
||||||
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import UIKit
|
|
||||||
|
|
||||||
public extension UIImage {
|
|
||||||
func getExifData() -> CFDictionary? {
|
|
||||||
var exifData: CFDictionary? = nil
|
|
||||||
|
|
||||||
if let data = self.jpegData(compressionQuality: 1.0) {
|
|
||||||
data.withUnsafeBytes {
|
|
||||||
let bytes = $0.baseAddress?.assumingMemoryBound(to: UInt8.self)
|
|
||||||
if let cfData = CFDataCreate(kCFAllocatorDefault, bytes, data.count),
|
|
||||||
let source = CGImageSourceCreateWithData(cfData, nil) {
|
|
||||||
exifData = CGImageSourceCopyPropertiesAtIndex(source, 0, nil)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return exifData
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -48,7 +48,7 @@ public class AuthorizationService {
|
||||||
|
|
||||||
// Create application (we will get clientId amd clientSecret).
|
// Create application (we will get clientId amd clientSecret).
|
||||||
let oAuthApp = try await client.createApp(
|
let oAuthApp = try await client.createApp(
|
||||||
named: "Photofed",
|
named: "Vernissage",
|
||||||
redirectUri: "oauth-vernissage://oauth-callback/mastodon",
|
redirectUri: "oauth-vernissage://oauth-callback/mastodon",
|
||||||
scopes: Scopes(["read", "write", "follow", "push"]),
|
scopes: Scopes(["read", "write", "follow", "push"]),
|
||||||
website: baseUrl)
|
website: baseUrl)
|
||||||
|
|
|
@ -91,13 +91,34 @@ public class TimelineService {
|
||||||
|
|
||||||
// Save attachment in database.
|
// Save attachment in database.
|
||||||
let attachmentData = statusData.attachments().first { item in item.id == attachment.id }
|
let attachmentData = statusData.attachments().first { item in item.id == attachment.id }
|
||||||
?? AttachmentDataHandler.shared.createAttachmnentDataEntity(viewContext: backgroundContext)
|
?? AttachmentDataHandler.shared.createAttachmnentDataEntity(viewContext: backgroundContext)
|
||||||
|
|
||||||
attachmentData.copyFrom(attachment)
|
attachmentData.copyFrom(attachment)
|
||||||
attachmentData.statusId = statusData.id
|
attachmentData.statusId = statusData.id
|
||||||
attachmentData.data = imageData
|
attachmentData.data = imageData
|
||||||
|
|
||||||
// TODO: read exif information
|
// Read exif information.
|
||||||
|
if let exifProperties = imageData.getExifData() {
|
||||||
|
if let make = exifProperties.getExifValue("Make"), let model = exifProperties.getExifValue("Model") {
|
||||||
|
attachmentData.exifCamera = "\(make) \(model)"
|
||||||
|
}
|
||||||
|
|
||||||
|
// "Lens" or "Lens Model"
|
||||||
|
if let lens = exifProperties.getExifValue("Lens") {
|
||||||
|
attachmentData.exifLens = lens
|
||||||
|
}
|
||||||
|
|
||||||
|
if let createData = exifProperties.getExifValue("CreateDate") {
|
||||||
|
attachmentData.exifCreatedDate = createData
|
||||||
|
}
|
||||||
|
|
||||||
|
if let focalLenIn35mmFilm = exifProperties.getExifValue("FocalLenIn35mmFilm"),
|
||||||
|
let fNumber = exifProperties.getExifValue("FNumber")?.calculateExifNumber(),
|
||||||
|
let exposureTime = exifProperties.getExifValue("ExposureTime"),
|
||||||
|
let photographicSensitivity = exifProperties.getExifValue("PhotographicSensitivity") {
|
||||||
|
attachmentData.exifExposure = "\(focalLenIn35mmFilm)mm, f/\(fNumber), \(exposureTime)s, ISO \(photographicSensitivity)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if attachmentData.isInserted {
|
if attachmentData.isInserted {
|
||||||
attachmentData.statusRelation = statusData
|
attachmentData.statusRelation = statusData
|
||||||
|
|
|
@ -27,6 +27,10 @@
|
||||||
<entity name="AttachmentData" representedClassName="AttachmentData" syncable="YES">
|
<entity name="AttachmentData" representedClassName="AttachmentData" syncable="YES">
|
||||||
<attribute name="blurhash" optional="YES" attributeType="String"/>
|
<attribute name="blurhash" optional="YES" attributeType="String"/>
|
||||||
<attribute name="data" attributeType="Binary" allowsExternalBinaryDataStorage="YES"/>
|
<attribute name="data" attributeType="Binary" allowsExternalBinaryDataStorage="YES"/>
|
||||||
|
<attribute name="exifCamera" optional="YES" attributeType="String"/>
|
||||||
|
<attribute name="exifCreatedDate" optional="YES" attributeType="String"/>
|
||||||
|
<attribute name="exifExposure" optional="YES" attributeType="String"/>
|
||||||
|
<attribute name="exifLens" optional="YES" attributeType="String"/>
|
||||||
<attribute name="id" attributeType="String"/>
|
<attribute name="id" attributeType="String"/>
|
||||||
<attribute name="previewUrl" optional="YES" attributeType="URI"/>
|
<attribute name="previewUrl" optional="YES" attributeType="URI"/>
|
||||||
<attribute name="remoteUrl" optional="YES" attributeType="URI"/>
|
<attribute name="remoteUrl" optional="YES" attributeType="URI"/>
|
||||||
|
|
|
@ -33,7 +33,7 @@ struct VernissageApp: App {
|
||||||
.environmentObject(applicationState)
|
.environmentObject(applicationState)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.task {
|
.task {
|
||||||
await AuthorizationService.shared.verifyAccount({ accountData in
|
await AuthorizationService.shared.verifyAccount({ accountData in
|
||||||
guard let accountData = accountData else {
|
guard let accountData = accountData else {
|
||||||
self.applicationViewMode = .signIn
|
self.applicationViewMode = .signIn
|
||||||
|
@ -43,9 +43,6 @@ struct VernissageApp: App {
|
||||||
self.applicationState.accountData = accountData
|
self.applicationState.accountData = accountData
|
||||||
self.applicationViewMode = .mainView
|
self.applicationViewMode = .mainView
|
||||||
})
|
})
|
||||||
|
|
||||||
URLCache.shared.memoryCapacity = 10_000_000 // ~10 MB memory space
|
|
||||||
URLCache.shared.diskCapacity = 1_000_000_000 // ~1GB disk cache space
|
|
||||||
}
|
}
|
||||||
.navigationViewStyle(.stack)
|
.navigationViewStyle(.stack)
|
||||||
.onReceive(NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)) { _ in
|
.onReceive(NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)) { _ in
|
||||||
|
|
|
@ -30,8 +30,10 @@ struct SignInView: View {
|
||||||
Button("Go") {
|
Button("Go") {
|
||||||
Task {
|
Task {
|
||||||
try await AuthorizationService.shared.signIn(serverAddress: serverAddress, { accountData in
|
try await AuthorizationService.shared.signIn(serverAddress: serverAddress, { accountData in
|
||||||
self.applicationState.accountData = accountData
|
DispatchQueue.main.async {
|
||||||
onSignInStateChenge(.mainView)
|
self.applicationState.accountData = accountData
|
||||||
|
onSignInStateChenge(.mainView)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,11 +15,18 @@ struct StatusView: View {
|
||||||
@State private var showCompose = false
|
@State private var showCompose = false
|
||||||
@State private var statusData: StatusData?
|
@State private var statusData: StatusData?
|
||||||
|
|
||||||
|
@State private var exifCamera: String?
|
||||||
|
@State private var exifExposure: String?
|
||||||
|
@State private var exifCreatedDate: String?
|
||||||
|
@State private var exifLens: String?
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ScrollView {
|
ScrollView {
|
||||||
if let statusData = self.statusData {
|
if let statusData = self.statusData {
|
||||||
VStack (alignment: .leading) {
|
VStack (alignment: .leading) {
|
||||||
ImagesCarousel(attachments: statusData.attachments())
|
ImagesCarousel(attachments: statusData.attachments()) { attachmentData in
|
||||||
|
self.setAttachment(attachmentData)
|
||||||
|
}
|
||||||
|
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
NavigationLink(destination: UserProfileView(
|
NavigationLink(destination: UserProfileView(
|
||||||
|
@ -36,10 +43,10 @@ struct StatusView: View {
|
||||||
.padding(.leading, -4)
|
.padding(.leading, -4)
|
||||||
|
|
||||||
VStack (alignment: .leading) {
|
VStack (alignment: .leading) {
|
||||||
LabelIcon(iconName: "camera", value: "SONY ILCE-7M3")
|
LabelIcon(iconName: "camera", value: self.exifCamera)
|
||||||
LabelIcon(iconName: "camera.aperture", value: "Viltrox 24mm F1.8 E")
|
LabelIcon(iconName: "camera.aperture", value: self.exifLens)
|
||||||
LabelIcon(iconName: "timelapse", value: "24.0 mm, f/1.8, 1/640s, ISO 100")
|
LabelIcon(iconName: "timelapse", value: self.exifExposure)
|
||||||
LabelIcon(iconName: "calendar", value: "2 Oct 2022")
|
LabelIcon(iconName: "calendar", value: self.exifCreatedDate?.toDate(.isoDateTimeSec)?.formatted())
|
||||||
}
|
}
|
||||||
.padding(.bottom, 2)
|
.padding(.bottom, 2)
|
||||||
.foregroundColor(.lightGrayColor)
|
.foregroundColor(.lightGrayColor)
|
||||||
|
@ -127,6 +134,13 @@ struct StatusView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func setAttachment(_ attachmentData: AttachmentData) {
|
||||||
|
exifCamera = attachmentData.exifCamera
|
||||||
|
exifExposure = attachmentData.exifExposure
|
||||||
|
exifCreatedDate = attachmentData.exifCreatedDate
|
||||||
|
exifLens = attachmentData.exifLens
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct StatusView_Previews: PreviewProvider {
|
struct StatusView_Previews: PreviewProvider {
|
||||||
|
|
|
@ -9,20 +9,30 @@ import SwiftUI
|
||||||
struct ImagesCarousel: View {
|
struct ImagesCarousel: View {
|
||||||
@State public var attachments: [AttachmentData]
|
@State public var attachments: [AttachmentData]
|
||||||
@State private var height: Double = 0.0
|
@State private var height: Double = 0.0
|
||||||
|
@State private var selectedAttachmentId = ""
|
||||||
|
|
||||||
|
var onAttachmentChange: (_ attachmentData: AttachmentData) -> Void?
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
TabView {
|
TabView(selection: $selectedAttachmentId) {
|
||||||
ForEach(attachments, id: \.self) { attachment in
|
ForEach(attachments, id: \.id) { attachment in
|
||||||
if let image = UIImage(data: attachment.data) {
|
if let image = UIImage(data: attachment.data) {
|
||||||
Image(uiImage: image)
|
Image(uiImage: image)
|
||||||
.resizable()
|
.resizable()
|
||||||
.aspectRatio(contentMode: .fit)
|
.aspectRatio(contentMode: .fit)
|
||||||
|
.tag(attachment.id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.frame(height: CGFloat(self.height))
|
.frame(height: CGFloat(self.height))
|
||||||
.tabViewStyle(PageTabViewStyle())
|
.tabViewStyle(PageTabViewStyle())
|
||||||
|
.onChange(of: selectedAttachmentId, perform: { index in
|
||||||
|
if let attachment = attachments.first(where: { item in item.id == index }) {
|
||||||
|
onAttachmentChange(attachment)
|
||||||
|
}
|
||||||
|
})
|
||||||
.onAppear {
|
.onAppear {
|
||||||
|
self.selectedAttachmentId = self.attachments.first?.id ?? ""
|
||||||
self.calculateImageHeight()
|
self.calculateImageHeight()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -47,6 +57,8 @@ struct ImagesCarousel: View {
|
||||||
|
|
||||||
struct ImagesCarousel_Previews: PreviewProvider {
|
struct ImagesCarousel_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
ImagesCarousel(attachments: [])
|
ImagesCarousel(attachments: []) { attachmentData in
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,16 +8,20 @@ import SwiftUI
|
||||||
|
|
||||||
struct LabelIcon: View {
|
struct LabelIcon: View {
|
||||||
let iconName: String
|
let iconName: String
|
||||||
let value: String
|
let value: String?
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
HStack(alignment: .center) {
|
if let value {
|
||||||
Image(systemName: iconName)
|
HStack(alignment: .center) {
|
||||||
.frame(width: 30, alignment: .leading)
|
Image(systemName: iconName)
|
||||||
Text(value)
|
.frame(width: 30, alignment: .leading)
|
||||||
.font(.footnote)
|
Text(value)
|
||||||
|
.font(.footnote)
|
||||||
|
}
|
||||||
|
.padding(.vertical, 2)
|
||||||
|
} else {
|
||||||
|
EmptyView()
|
||||||
}
|
}
|
||||||
.padding(.vertical, 2)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue