Organising photos from iPhone’s shots on Swift vs Python

Alexey Bodnya
7 min readDec 3, 2021

--

I have a task to organise images from several iPhones and other devices (cameras, GoPro-s etc) in folders on my external SSD. I found some issues doing this with iPhone’s shots. Let’s check what solution I’ve made and how it looks right now.

Importing photos from iPhone

Most of my photos are not persisted on iPhone, they are shared to iCloud. iPhone contains only thumbnails of them. Also there are photo-series that are captured for example by timer or long press. Sometimes iPhone threats these photos as a serial of one and propose you to select only one and drop other. Eventually iPhone will just select the best picture for you. And it is not always really the best shot.

There are several ways to import images from iPhone

  • connect iPhone via cable to Macbook and use Photos to import photos. Here I have an issue with photos that are on iCloud and are not dowloaded on iPhone and are not presented here. This is the problem for me because I want to import all of them.
  • connect iPhone via cable to Macbook and use Image Capture to import photos. This one has the same issue. The shots that were not downloaded are missed here.
  • download images from iCloud.com web version. This one sounds good in theory, but really the photos are not in the original quality, they are compressed. I want to get original quality. Also not uploaded photos are not presented here. So skip this option as well.
  • share photos via AirDrop from iPhone. Using this option all the shots are shared in original quality, iPhone’s Photos app may crash, freeze and cancel downloading images while iPhone is locked, but still you can share ALL the photos in original quality. This is the way I am using now.

Organise images

So images are on your Macbook, well done! Let’s rename all the images and add prefix of the creation date, for example in yyyyMMdd format. We can use Finder’s batch renaming feature or 3rd-party app like Name Mangler.

Wait, what? All the photos has the “Created” equal to the date when they were downloaded. Hm… how will I mix photos from different sources with these wrong dates? There is a soultuon! Do you see here some extra field “Content created”? Yeah, that’s the key. Let’s figure out what it is.

Parsing Xattrs

Let’s use some Swift coding to check what is inside the file.

let attributes = try? FileManager.default.attributesOfItem(atPath: fileURL.path)

Let’s see what’s inside:

(lldb) po attributes
▿ 17 elements
▿ 0 : 2 elements
▿ key : NSFileAttributeKey
- _rawValue : NSFileHFSCreatorCode
- value : 0
▿ 1 : 2 elements
▿ key : NSFileAttributeKey
- _rawValue : NSFileExtendedAttributes
▿ value : 22 elements
▿ 0 : 2 elements
- key : com.apple.assetsd.originalFilename
- value : <494d475f 35323036 2e4a5047>
▿ 1 : 2 elements
- key : com.apple.lastuseddate#PS
- value : <c86a7261 00000000 c9dcf91c 00000000>
▿ 2 : 2 elements
- key : com.apple.assetsd.cloudAsset.UUID
- value : <32353639 44323443 2d353932 412d3438 35452d38 4539412d 35353737 43454134 32413136>
▿ 3 : 2 elements
- key : com.apple.assetsd.customLocation
- value : <627fd93d 79c04540 3c2cd49a e66d3040 9a999999 99991d40 00000000 00405040 00000000 00000000 a0ef0280 fe795340 00000000 00000000 7593e86a f052c341>
▿ 4 : 2 elements
- key : com.apple.assetsd.favorite
- value : <0000>
▿ 5 : 2 elements
- key : com.apple.assetsd.avalanche.type
- value : <0000>
▿ 6 : 2 elements
- key : com.apple.assetsd.assetType
- value : <0300>
▿ 7 : 2 elements
- key : com.apple.assetsd.trashed
- value : <0000>
▿ 8 : 2 elements
- key : com.apple.assetsd.importedBy
- value : <0100>
▿ 9 : 2 elements
- key : com.apple.assetsd.videoComplementVisibility
- value : <0000>
▿ 10 : 2 elements
- key : com.apple.assetsd.timeZoneOffset
- value : <201c0000>
▿ 11 : 2 elements
- key : com.apple.assetsd.timeZoneName
- value : <474d542b 30323030>
▿ 12 : 2 elements
- key : com.apple.assetsd.addedDate
- value : <62706c69 73743030 3341c352 f06b0236 b9080000 00000000 01010000 00000000 00010000 00000000 00000000 00000000 0011>
▿ 13 : 2 elements
- key : com.apple.cscachefs
- value : <66471265 3143574d a6697874 1320d50e 2f1908c8 04fb0000 00516a72 61000000 0083c76f 08000000 00a4bd3d 00000000 00018f12 33e25d0d 1cd576ff b8bd7482 2ff6caa4 26e6>
▿ 14 : 2 elements
- key : com.apple.assetsd.grouping.state
- value : <00000000 00000000>
▿ 15 : 2 elements
- key : com.apple.assetsd.hidden
- value : <0000>
▿ 16 : 2 elements
- key : com.apple.assetsd.UUID
- value : <2569d24c 592a485e 8e9a5577 cea42a16>
▿ 17 : 2 elements
- key : com.apple.metadata:kMDItemWhereFroms
- value : <62706c69 73743030 a201025d 416c6578 65792042 6f646e79 616f100f 0041006c 00650078 00650079 20190073 00200069 00500068 006f006e 0065080b 19000000 00000001 01000000 00000000 03000000 00000000 00000000 00000000 3a>
▿ 18 : 2 elements
- key : com.apple.assetsd.dbRebuildUuid
- value : <31423333 37424543 2d303233 312d3438 31442d39 3234352d 39453145 30324543 39313137>
▿ 19 : 2 elements
- key : com.apple.quarantine
- value : <30303833 3b363137 32366135 333b7368 6172696e 67643b44 38384539 3346352d 30354639 2d344430 382d4139 46322d38 46353839 35444244 374537>
▿ 20 : 2 elements
- key : com.apple.assetsd.deferredProcessing
- value : <0000>
▿ 21 : 2 elements
- key : com.apple.assetsd.customCreationDate
- value : <62706c69 73743030 3341c352 f06aa798 4b080000 00000000 01010000 00000000 00010000 00000000 00000000 00000000 0011>
▿ 2 : 2 elements
▿ key : NSFileAttributeKey
- _rawValue : NSFileExtensionHidden
- value : 0
▿ 3 : 2 elements
▿ key : NSFileAttributeKey
- _rawValue : NSFileSystemFileNumber
- value : 51953939
▿ 4 : 2 elements
▿ key : NSFileAttributeKey
- _rawValue : NSFileSystemNumber
- value : 16777224
▿ 5 : 2 elements
▿ key : NSFileAttributeKey
- _rawValue : NSFileOwnerAccountID
- value : 501
▿ 6 : 2 elements
▿ key : NSFileAttributeKey
- _rawValue : NSFileType
- value : NSFileTypeRegular
▿ 7 : 2 elements
▿ key : NSFileAttributeKey
- _rawValue : NSFileCreationDate
- value : 2021-10-22 07:37:53 +0000
▿ 8 : 2 elements
▿ key : NSFileAttributeKey
- _rawValue : NSFileModificationDate
- value : 2021-10-22 07:37:53 +0000
▿ 9 : 2 elements
▿ key : NSFileAttributeKey
- _rawValue : NSFileOwnerAccountName
- value : abodnya
▿ 10 : 2 elements
▿ key : NSFileAttributeKey
- _rawValue : NSFileSize
- value : 4046244
▿ 11 : 2 elements
▿ key : NSFileAttributeKey
- _rawValue : NSFileProtectionKey
- value : NSFileProtectionCompleteUntilFirstUserAuthentication
▿ 12 : 2 elements
▿ key : NSFileAttributeKey
- _rawValue : NSFileReferenceCount
- value : 1
▿ 13 : 2 elements
▿ key : NSFileAttributeKey
- _rawValue : NSFilePosixPermissions
- value : 384
▿ 14 : 2 elements
▿ key : NSFileAttributeKey
- _rawValue : NSFileHFSTypeCode
- value : 0
▿ 15 : 2 elements
▿ key : NSFileAttributeKey
- _rawValue : NSFileGroupOwnerAccountID
- value : 20
▿ 16 : 2 elements
▿ key : NSFileAttributeKey
- _rawValue : NSFileGroupOwnerAccountName
- value : staff

The NSFileCreationDate as expected shows us the date of downloading the image, but the NSFileExtendedAttributes contains some extra attributes that we can check. They are called Xattr, we can either use system xattr API with some wrappers like https://github.com/okla/swift-xattr or just parse them from this attributes dictionary manually by keys.

From all the xattrs, the most interesting for me are com.apple.assetsd.customCreationDate and com.apple.assetsd.addedDate. Both of them contain binary data, let’s check what’s inside via Hex editor.

The data does not look like the date but it is just at the first look. The most important what we can get from here is “bplist” keyword that tells us that this binary data is bplist or Apple’s binary property list. I can read this quite a long article https://medium.com/@karaiskc/understanding-apples-binary-property-list-format-281e6da00dbd but I am lazy, so let’s just google some implemented solution. Unfortunately, I didn’t find anything written in swift, but there is a library in Python that may parse bplists in Python objects. Let’s check it out https://github.com/cclgroupltd/ccl-bplist.

I saved the bplist data in file “date”, downloaded ccl_bplist.py file from Github, put them in one folder and launched Python interpitator.

Here I opened the file, loaded it via the downloaded library and it parsed the bplist in datetime structure which I converted in unixtime. Looks pretty nice, now let’s integrate it in our swift code.

Integrate Python in Swift

To do this I used PythonKit 3rd-party which I imported via Swift packages. I read this article to get the details https://towardsdatascience.com/from-swift-import-python-f2fc2a997d4.

In a few words, the flow is pretty easy. I put the Python code in a small separate script and put it in file parse.py.

import ccl_bplist
import time
import datetime
def parse():
f = open("date", "rb")
parsed = ccl_bplist.load(f)
unixtime = time.mktime(parsed.timetuple())
return unixtime

Both parse.py and ccl_bplist.py are placed in the app’s bundle and on the launch are copied to NSHomeDirectory().

After parsing each photo’s attributes and finding the appropriate bplist data, I persisted it in file called “date” in NSHomeDirectory() to get all 3 files in one place. Then I needed to execute Python scripts to get the unitxtime and convert it to Swift Date.

func parseDate(data: Data) -> Date? {
// Write temp file with binary plist
let dateUrl = URL(fileURLWithPath: NSHomeDirectory()).appendingPathComponent("date")
if FileManager.default.fileExists(atPath: dateUrl.path) {
try? FileManager.default.removeItem(at: dateUrl)
}
try? data.write(to: dateUrl)

// parse it with python
let sys = Python.import("sys")
sys.path.append(NSHomeDirectory())
let pythonScript = Python.import("parse")
let parsed = pythonScript.parse()

// convert to Date
guard let timestamp = Double(parsed.description) else {
return nil
}
let date = Date(timeIntervalSince1970: timestamp)
return date
}

That’s it. Let’s just add fallback to original “creation date” attribute for the case when the extended attributes are missed.

func creationDate(fileURL: URL) -> Date? {
if let data = try? Xattr.dataFor(named: "com.apple.assetsd.customCreationDate", atPath: fileURL.path), let result = parseDate(data: data) {
return result
} else if let data = try? Xattr.dataFor(named: "com.apple.assetsd.addedDate", atPath: fileURL.path), let result = parseDate(data: data) {
return result
} else if let attributes = try? FileManager.default.attributesOfItem(atPath: fileURL.path), let date = attributes[.creationDate] as? Date {
let components = Calendar.current.dateComponents([.day, .month, .year], from: date)
let today = Calendar.current.dateComponents([.day, .month, .year], from: Date())
if components.day == today.day && components.month == today.month && components.year == today.year {
// not today
return nil
} else {
return date
}
}
return nil
}

The final look

After adding a bit of SwiftUI, the final app looks like this:

If you would like to get all the codebase, you can check it out here: https://github.com/abodnyaUA/Photo-Renamer

--

--

Alexey Bodnya

Senior iOS Developer; Traveller; Husband & Dad; EV car owner