I’ve been working on a new application, called Capturebot, for the last few of months, and it’s finally ready for a public beta.
Capturebot is an image collection validator. It works a lot like a smart album or Finder search, but instead of filtering images for viewing it tests whether collections contain matching images. Capturebot monitors a session in real time and shows what collections have images that meet all of the given criteria. Unlike a smart album, Capturebot lets you drill down into each part of the profile to see how many images pass.
Validations can be set up for ensuring the correct number of selects, testing for an exposure bracket, using regex to match specific file name formats, and much more.
If you’re on a larger set, or are sharing the computer with an art director, the validation session can be shared over the network. This allows you to view the validation on the shoot machine from another computer.
Code signing never goes right for me. I feel like I have a pretty simple process:
- Sign with Xcode
- Make and sign a DMG
- Throw it up on a server
- Download it for testing
The next obvious step to to Google for the correct incantations of
spctl2 to verify that nothing was corrupted during the upload or download. Occasionally I’ll find an error, but usually the app bundle passes without issue.
As it happens, however, the error message is a little misleading. The files on disk are in fact correct, however at runtime the application runs afoul of Gatekeeper. Looking in Console reveals the true culprit:
File /Volumes/Capturebot/Capturebot.app/Contents/Frameworks/CaptureOneScripting.framework/Versions/A/CaptureOneScripting failed on rPathCmd /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx/ImageMetadata.framework/Versions/A/ImageMetadata
The application is attempting to load a file from outside of its bundle. In this case, a framework built with Swift Package Manager is linked against Xcode’s Swift library rather than the bundled copy.
The fix is straight forward. A quick trip to Build Settings to add
@executable_path/../Frameworks to Runtime Search Paths resolves the problem.
Apple’s newest product, the HomePod, is quintessential Apple industrial design: minimal as it can be while still having corporeal form. Given that last fact, I decided it would be the perfect product for a contrived little room scene. You know the one: everything perfectly arranged with tasteful design that only exists in a Restoration Hardware catalogue.
As it happened I had access to a nice looking set the weekend the HomePod launched, so all I had to do was show up make it look good and click away.
The star of any photo shoot: the ColorChecker
Unfortunately, I am not a prop stylist. I can get away with a tabletop set and some books, but propping a whole room (especially when you don’t have a whole room’s worth of props) turns out to be a little tricky. I fought the exceedingly minimal industrial loft style for quite some time before deciding to pull the plug and try for an even more minimal style.
The acoustically transparent foam is a moire magnet, which makes downsampling fun.
I still wanted the hard light from the room scene, but instead of long shadows from a simulated late-in-the-day sun I kept the shadow small and close.
I always find it fun peel back the base layer of any photo to see all that was done to it.1
Most of the work went into cleaning up all of the damage on the painted stripe in the foam core. Only a hint of the negative fill was needed to add some separation on the left edge.
After the release of Capture One 11 I set to work poring over all of the changes in the most important part of Capture One: its AppleScript library! Phase One made some great additions like including all of the basic ratings (star and colors), plus nearly full control over a variant’s adjustments. I’ve already built scripts that automate bulk processing that couldn’t have been done before.
However, one big change (that, admittedly, came in 10, which I skipped) is how collections are returned.
Capture One 10 & 11 return collections in a single array grouped by collection type:
- Session Folders
- Smart Albums
Differences with Capture One 9
Contrasted with Capture One 9, which only returned Session Albums and Session Favorites, all in their Capture One order.
(*A Album album*) (*B Smart Album smart album*) (*C Album album*) (*Macintosh HD:Users:edunn:Pictures:Test AS 9:Capture:A Folder: favorite*) (*Macintosh HD:Users:edunn:Pictures:Test AS 9:Capture:B Folder: favorite*) (*Macintosh HD:Users:edunn:Pictures:Test AS 9:Capture:C Folder: favorite*)
Favorites did not have names, only file paths, so the output above is a combination of the
Samples of collection ordering are returned by the following AppleScript:
tell front document of application "Capture One" repeat with i from 1 to the (count of collections) set c to item i in collections log (name of c as string) & " " & (kind of c) end repeat end tell
All of this applies only to Sessions, Catalogues are quite different with their own logic.
Whatever folders are assigned as Capture, Selects, etc. are returned in a fixed order of
The name of the actual folder is used, so if the Capture folder is set to
Foo it would be
However any given folder is only listed once. For instance, if the same folder is set to both
Selects the list returned will be:
(*Capture favorite*) (*Output favorite*) (*Trash favorite*)
Albums & Smart Albums
Normal Albums and Smart albums are grouped by type (albums first, smart albums second) and ordered by their sort order in Capture One.
- A Album
- B Smart Album
- C Album
- D Smart Album
- E Album
(*A Album album*) (*C Album album*) (*E Album album*) (*B Smart Album smart album*) (*D Smart Album smart album*)
Favorites are returned in order from Capture One, with some caveats. If a favorite is set to one of the Session Folders it will only be listed in its assigned type in the Session Folder section.
(*Capture favorite*) (*Selects favorite*) (*Output favorite*) (*Trash favorite*) (*A Folder favorite*) (*B Folder favorite*) (*C Folder favorite*)
If we set B Folder as the Capture Folder, however:
(*B Folder favorite*) (*Selects favorite*) (*Output favorite*) (*Trash favorite*) (*A Folder favorite*) (*C Folder favorite*)
This behavior, however, is not replicated with the standard Session Folders, and even worse isn’t consistent in how it works. When initially testing the default Capture folder appeared in the list as both the Capture Folder and a Favorite. Testing again, on a different machine running Build
18.104.22.1682 (fdb7335), I’m getting different behavior. This might be a bug.
Conclusion and Wishes
Once I figured out exactly what was going on (my initial theory was that favorites were being returned by access date) the logic makes sense. I don’t, however, like the new logic as it breaks one of my favorite scripts. In Capture One 9 I was able to reliably determine the position of the Capture Folder in the list of Favorites and move it up and down the list with a keyboard shortcut.
I would propose a hybrid between the old model and the new: list Session Folders both at the top and in place.
There is no good way to interact with network file systems in Swift without a third-party library. This leaves us needing to mount a share in order to use normal
FileManager and various
.write(…) methods. Unfortunately leaving the task of mounting a server to the User (and thus, the Finder) has a few problems.
The main hurdle is that the Finder doesn’t mound servers consistently. Behind the scenes directories are created at
/Volumes and the shares mounted to them. While they show up as expected to the user they don’t behave deterministically as far as UNIX paths are concerned. A network share I was testing left behind dozens of hidden folders with no good way to determine where the drive was actually mounted.
There are a few ways of programmatically mounting a network share.
The easiest option to mount a network share is to spin up an
AppleScript object and run
mount afp://someshare. While this works it does exactly what the Finder does, so it’s a no-go.
Ok, AppleScript is clunky anyway. That’s what
Process is for. Using
mount we can mount any file system anywhere we want! Sadly this was spitting out errors saying my destination path didn’t exist. I could copy the exact command into the Terminal and it would work. Moving right along…
Finally, there is
NetFS. This has almost no documentation outside of some header files buried in system directories. Fortunately it doesn’t require dropping down to Objective-C and it actually works.
There isn’t a reference page for any of the
NetFS methods, but if there was this is what one of them would look like. A network drive can be mounted either synchronously or asynchronously. I’ve documented the synchronous method, but the async version is similar.
Mounts a network file share at the specified file system node
NetFSMountURLSync( _ url: CFURL!, _ mountpath: CFURL!, _ user: CFString!, _ passwd: CFString!, _ open_options: CFMutableDictionary!, _ mount_options: CFMutableDictionary!, _ mountpoints: UnsafeMutablePointer<Unmanaged<CFArray>?>! ) -> Int32
url URL to mount, e.g. nfs://server/path
mountpath Path for the mountpoint
user Auth user name (overrides URL)
passwd Auth password (overrides URL)
open_options Options for session open (see below)
mount_options Options for mounting (see below)
mountpoints Array of mountpoints
Returns 0 if successful, or an error code from /usr/include/sys/errno.h
Most of the options are fairly self-explanatory. You can pass
nil for any parameters you don’t need. In fact, only the server URL is required.
If the server requires authentication and a username or password aren’t supplied the standard system prompt will open.
Information from the mount header file
The following dictionary keys for
open_options are supported:
kNetFSUseGuestKey: Bool Login as a guest user.
kNetFSAllowLoopbackKey: Bool Allow a loopback mount.
If this key is set to
UIOption: Suppress authentication dialog UI.
There are a number of options that can be passed in to
mount_options as an NSMutableDictionary.
Information from the NetFS header file
kNetFSMountFlagsKey There are two options for this key:
MNT_DONTBROWSEthe share won’t be visible to the user as a drive.
MNT_RDONLYthe share will be mounted as read-only.
Allow a mount from a dir beneath the share point. If this key is
true a subdirectory of the share will be mounted directly instead of mounting the root the the share.
Mount with “soft” failure semantics. If this key isn’t specified it defaults to
Network errors, e.g. timeouts, will be retried for a much shorter amount of time. If the network errors persist, then the mount will be force unmounted.
kNetFSMountAtMountDirKey: Bool Mount on the specified mountpath instead of below it.
Information from the error number header file
The header file contains far more error numbers than
NetFS returns, so you’ll just have to cross reference.