Introducing Capturebot

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.

If you’d like to help test go download a copy of Capturebot and be sure to send in any feedback you have.

Code Signing Woes

Code signing never goes right for me. I feel like I have a pretty simple process:

  1. Export
  2. Sign with Xcode
  3. Make and sign a DMG
  4. Throw it up on a server
  5. Download it for testing
  6. 🤬

The next obvious step to to Google for the correct incantations of codesign1 and 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.

  1. codesign --verify --deep --verbose=4 

  2. spctl --assess 

HomePod

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.

Retouching

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.

  1. It’s like a far less interesting version of the ILM Star Wars effects reels

Capture One Collections & AppleScript

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:

  1. Session Folders
  2. Albums
  3. Smart Albums
  4. Favorites

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 name and file attributes.

Script

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.

Session Folders

Whatever folders are assigned as Capture, Selects, etc. are returned in a fixed order of

  1. Capture
  2. Selects
  3. Output
  4. Trash

The name of the actual folder is used, so if the Capture folder is set to Foo it would be (*Foo favorite*).

However any given folder is only listed once. For instance, if the same folder is set to both Capture and 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

Returns:

(*A Album album*)
(*C Album album*)
(*E Album album*)
(*B Smart Album smart album*)
(*D Smart Album smart album*)

Favorites

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.

For example:

  • A Folder
  • B Folder
  • C Folder

Will return:

(*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 11.0.0.282 (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.

Swift Network Shares

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.

Options

There are a few ways of programmatically mounting a network share.

AppleScript

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.

Shell

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…

NetFS

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.

NetFSMountURLSync(_:_:_:_:_:_:_)

Mounts a network file share at the specified file system node

Declaration

NetFSMountURLSync(
    _ url: CFURL!, 
    _ mountpath: CFURL!, 
    _ user: CFString!, 
    _ passwd: CFString!, 
    _ open_options: CFMutableDictionary!, 
    _ mount_options: CFMutableDictionary!, 
    _ mountpoints: UnsafeMutablePointer<Unmanaged<CFArray>?>!
    ) -> Int32

Parameters

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

Return Value

Returns 0 if successful, or an error code from /usr/include/sys/errno.h

Discussion

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.

Open Options

Information from the mount header file /usr/include/sys/mount.h

The following dictionary keys for open_options are supported:

kNetFSUseGuestKey: Bool Login as a guest user.

kNetFSAllowLoopbackKey: Bool Allow a loopback mount.

kNAUIOptionKey If this key is set to UIOption: Suppress authentication dialog UI.

Mount Options

There are a number of options that can be passed in to mount_options as an NSMutableDictionary.

Information from the NetFS header file /System/Library/Frameworks/NetFS.framework/Headers/NetFS.h

kNetFSMountFlagsKey There are two options for this key:

  • MNT_DONTBROWSE the share won’t be visible to the user as a drive.
  • MNT_RDONLY the share will be mounted as read-only.

kNetFSAllowSubMountsKey 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.

kNetFSSoftMountKey: Bool Mount with “soft” failure semantics. If this key isn’t specified it defaults to true

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.

Return Codes

Information from the error number header file /usr/include/sys/errno.h

The header file contains far more error numbers than NetFS returns, so you’ll just have to cross reference.