User manual for Finishing Touches

Install

Copy the script

Copy FinishingTouches.jsx to Photoshop’s scripts folder:

Restart Photoshop and confirm Finishing Touches appears in File > Automate.

Load the actions

In the Actions palette, click the hamburger icon (≡), choose Load Actions…, select all .atn files, and click Open.


Your first rule

A rule tells the script: “When you see this keyword, run these actions.”

keywords: export | actions: Sharpen for web @ My Actions

This means: for any image with the keyword export, run the action Sharpen for web from the action set My Actions.

That’s all you need to start. Keywords come from your image’s IPTC/EXIF metadata — set them in Lightroom, Bridge, or Photoshop via File > File Info > Keywords.


Match by aspect ratio

Add ratio: to run a rule only for images of a specific shape:

keywords: instagram | ratio: 1x1 | actions: Square crop @ Social
keywords: instagram | ratio: 2x3 | actions: Portrait crop @ Social

The script detects which ratio the image is closest to and applies the matching rule.

Available aspect ratios: 1x1, 6x7, 4x5, 4x3, 2x3, 16x9, xpan and 17x6


Resize before running actions

Add size: to resize the image before actions run. The value is always the short side in pixels:

keywords: web | ratio: 2x3 | size: 2048 | actions: Web sharpen @ Export

A portrait at 2048 short side becomes 2048 × 3072 pixels.


Chain multiple actions

Separate actions with ; to run several in sequence:

keywords: portfolio | ratio: 2x3 | size: 3600 | actions: Film look @ Color ; Edge glow @ Effects ; Sharpen @ Export

Actions run left to right.


Run multiple rules on one image

Tag an image with multiple keywords to run multiple rules. For example, tag with film, grain, and export to run all three rules in turn.

Controlling the order

Use the Keyword order field to set which rules run first:

film;grain;borders;export

All images process in this order regardless of what order keywords appear in the file.

Example:


More specific rules with modifiers

Add extra keywords to make a rule more specific. The first keyword is the master keyword. Additional keywords are modifiers. Keyword order controls when master-keyword families run, but it does not change which keyword is the master inside a rule:

keywords: grain | actions: Light grain @ Effects
keywords: grain ; heavy | actions: Heavy grain @ Effects

Example: ISO variants

keywords: film | actions: Normal grain @ Film
keywords: film ; iso1600 | actions: ISO 1600 grain @ Film
keywords: film ; iso3200 | actions: ISO 3200 grain @ Film

The script automatically picks the most specific rule that matches the image’s keywords.


Rules without keywords

Rules can match all images of a certain aspect ratio, independent of keywords:

ratio: 16x9 | size: 3840 | actions: Panorama crop @ Formats

Or match all images with a keyword, regardless of ratio:

keywords: logo | actions: Add watermark @ Branding

Automatic color and B&W detection

Enable Automatically Add Keywords ‘color’ or ‘bw’ in the dialog options. The script inspects each image and temporarily adds a virtual keyword (color or bw) for matching — without writing it to file metadata.

This lets you write rules like:

keywords: color | ratio: 2x3 | actions: Color film look @ Presets
keywords: bw | ratio: 2x3 | actions: B&W film look @ Presets

Workflow examples

Film photography

Goal: Apply film emulation, halation, and grain in order.

Keyword order: film;halation;grain;export

keywords: film ; portra400 | ratio: 2x3 | actions: Portra 400 look @ Film Emulation
keywords: film ; tri-x | ratio: 2x3 | actions: Tri-X look @ Film Emulation
keywords: halation | ratio: 2x3 | actions: Halation effect @ Effects
keywords: grain | actions: 35mm grain @ Texture
keywords: export | ratio: 2x3 | size: 3600 | actions: Sharpen and save @ Export

Tag an image with film, portra400, halation, grain, export → Portra look → Halation → Grain → Export.

Social media exports

Goal: Multiple output sizes from one source.

keywords: instagram | ratio: 1x1 | size: 1080 | actions: Square crop @ Social ; IG sharpen @ Social
keywords: instagram | ratio: 4x5 | size: 1080 | actions: Portrait crop @ Social ; IG sharpen @ Social
keywords: facebook | size: 2048 | actions: FB watermark @ Social ; FB sharpen @ Social

Tag with both instagram and facebook — both rules run.

Goal: Different sharpening for print and web from the same master.

keywords: print | ratio: 2x3 | size: 4800 | actions: Print sharpen @ Output
keywords: web | ratio: 2x3 | size: 2048 | actions: Web sharpen @ Output

Tag with both print and web — two versions are created.

Color vs black-and-white

Enable Automatically Add Keywords ‘color’ or ‘bw’ option, then:

keywords: color | ratio: 2x3 | actions: Color correction @ Grading
keywords: bw | ratio: 2x3 | actions: BW conversion @ Grading
keywords: grain ; color | actions: Color grain @ Texture
keywords: grain ; bw | actions: BW grain @ Texture

No manual color/bw tagging needed — the script detects it automatically.


Creating rules: dialog vs direct editing

There are two ways to create rules:

  1. Using the Rule Builder dialog (recommended as a starting point)
  2. Editing rules directly as text (for advanced or bulk editing)

The Rule Builder dialog

Open an image, run Finishing Touches from the Actions panel, and the Rule Builder dialog appears.

Dialog options

Saving and batch processing

  1. Open an image.
  2. Run Finishing Touches from the Actions panel.
  3. Build your rules and click Save settings to store the configuration in the action.
  4. When ready to batch-process, enable Save and Close when Actions are Completed, save the action, and create a droplet from it.

Editing rules as text

For bulk editing or pasting from another source, work directly in the right column of the dialog. You can always switch between building rules in the UI and editing text directly — the script uses whatever is in the recipe field when you save.


Complete rule syntax

Finishing Touches recipes have three kinds of lines:

  1. set statements — define custom variables
  2. meta statements — write IPTC/XMP metadata
  3. Rule lines — match an image and run actions

Blank lines and comments are ignored.

set grain = heavy when {size} >= 6000 else medium

meta copyrightNotice = © {year} Joakim Hertze

keywords: kw1 ; kw2 | ratio: 2x3 | size: 3600 | actions: Action1 @ Set1 ; Action2 @ Set2

Statement order

For readability, keep recipes in this order:

  1. set statements
  2. meta statements
  3. rule lines

That said, Finishing Touches collects all set statements before rule parsing and applies all meta statements before executing actions, so the language is forgiving about physical placement. Keeping them grouped at the top just makes recipes easier to maintain.

Rule fields in detail

Minimal rule:

keywords: export | actions: Sharpen for web @ My Actions

Rule with all fields:

keywords: film ; portra400 | ratio: 2x3 | size: 3600 | actions: Portra base @ Film ; Export sharpen @ Output

Ratio-only rule:

ratio: xpan | size: 4000 | actions: Panorama output @ Formats

Dynamic rule using variables:

keywords: export | ratio: {format} | size: {size} | actions: Sharpen {format} @ Export {size}

If fuzzy_action_whitespace = true in the script, Finishing Touches also ignores whitespace differences when locating actions and action sets at runtime. This is useful when an installed action contains accidental double spaces or similar spacing inconsistencies. Matching is still exact for the actual letters and numbers.

Comments and formatting

# Web export rules
keywords: web ; color | ratio: 2x3 | size: 2048 | actions: Color grade @ Web ; Sharpen @ Web
keywords: web ; bw | ratio: 2x3 | size: 2048 | actions: BW conversion @ Web ; Sharpen @ Web

# Social media
keywords: instagram | ratio: 1x1 | size: 1080 | actions: Square crop @ Social

How matching works

When the script processes an image:

  1. Reads keywords from IPTC/EXIF metadata (File info > Keywords)
  2. Classifies aspect ratio to one of eight categories
  3. Optionally adds color or bw virtual keyword (if detection enabled)
  4. Reorders keywords according to your Keyword order setting
  5. Builds runtime variables — including {keywords} (comma-joined, priority-ordered keyword list) and {flag} (when provided by FinishingChunks), making all runtime variables available for set conditions
  6. Processes set statements — evaluates conditional assignments and builds custom variables
  7. Replaces variables inside the recipe — custom variables first, then runtime variables
  8. Applies meta statements — writes IPTC fields to the document with all variables already resolved
  9. Parses rule lines — building the internal rule list from the processed recipe text
  10. For each keyword family in order:
    • Finds rules whose master keyword matches
    • Filters by aspect ratio (if rule specifies one)
    • Removes less-specific competing rules inside the same master-keyword family
    • Preserves rules from different master keywords so modifier-style stacks can still run together
    • Resizes (if rule has size:)
    • Runs all actions in the rule
  11. Removes matched keywords (if tidy option enabled)
  12. Saves and closes (if option enabled)

Master keywords and specificity

The first keyword in a rule is always the master keyword. Everything after it is a modifier.

keywords: grain | actions: Light grain @ Effects
keywords: grain ; heavy | actions: Heavy grain @ Effects
keywords: border ; white | actions: White border @ Borders

If an image has grain and heavy, the second rule beats the first because it is more specific within the same master-keyword family. But grain rules and border rules can still both run on the same image because they belong to different master-keyword families.


Runtime variables

Runtime variables let rules adapt to each image’s properties. Use them in any rule field.

Dynamic action names

Instead of separate rules for each size:

keywords: grain | ratio: 2x3 | size: {size} | actions: 35mm ISO 400 ({size} Color) @ Grain {size}

For a 6400px image this becomes:

keywords: grain | ratio: 2x3 | size: 6000 | actions: 35mm ISO 400 (6000 Color) @ Grain 6000

Format-agnostic rules

keywords: export | ratio: {format} | size: 2048 | actions: Sharpen {format} @ Export

Orientation-specific borders

keywords: border | actions: Add {orientation} border @ Borders

Custom variables

For more complex workflows, create your own variables using set statements. In practice, keep set statements near the top of your recipe so the recipe reads top-to-bottom: define values first, then write metadata, then describe the rules that use those values.

Basic syntax

set variable_name = value
set variable_name = value when condition
set variable_name = value when condition else fallback_value
set variable_name = value1 when condition1 else value2 when condition2 else fallback_value

Clauses are evaluated left to right. The first matching when wins. A bare value at the end with no when is the unconditional default.

The else fallback form is equivalent to two separate lines, but more compact:

# These two are identical:
set FG_mode = Color when {mode} == color else Monochrome

set FG_mode = Monochrome
set FG_mode = Color when {mode} == color

Use chained form when cases are mutually exclusive. Use separate set lines when later assignments should layer on top of earlier ones.

Variable naming

Defining and using variables

Use plain names when defining; use {curly braces} when referencing:

set grain = heavy grain
set output_size = 4096

keywords: process | actions: Apply {grain} @ Effects
keywords: export | size: {output_size} | actions: Sharpen @ Export

The condition mini-language

Conditions are used in set ... when ... clauses. They are deliberately compact, but they are more expressive than plain if this equals that checks.

Supported building blocks:

Truthy checks

You do not always need to compare explicitly.

set flagged = yes when {flag}
set has_lens_data = yes when {lens}
set missing_make = yes when not {make}

So these two are equivalent:

set crop_variant = special when {flag}
set crop_variant = special when {flag} == true

Conditions and operators

Comparison operators: == (or =), !=, <, >, <=, >=

Bare values in comparisons can be unquoted if they are simple single-token values:

set workflow = mono when {mode} == bw
set big = yes when {size} >= 6000

Use quotes when the value contains spaces or words such as and, or, when, or else that could otherwise be mistaken for syntax:

set camera_family = rangefinder when {model} == "Mamiya 7"
set film = favorite when {filename} == "Portra and rain"
set note = ok when {filename} == "before else after"
set cue = ok when {filename} == 'before when after'

List membership (== and != only):

set my_format = wide when {format} == [16x9, xpan, 17x6]
set standard = yes when {format} == [2x3, 4x5, 1x1]
set needs_special = yes when {format} != [2x3, 4x3]

Lists can contain any values separated by commas; spaces around values are trimmed.

Quoted list items are supported when a value contains spaces:

set favorite_camera = yes when {model} == ["Mamiya 7", "Leica M6", "Fuji GW690"]

contains / not contains — test whether {keywords} includes a specific keyword:

set treatment = dark when {keywords} contains halation
set safe = yes when {keywords} not contains overexposed

Matching is exact and case-sensitive. contains and not contains work on the comma-separated {keywords} list, so they are best used with actual keywords rather than free text.

Logical operators: not, and, or

set grain = heavy when {size} >= 6000 and {format} == 2x3
set treatment = special when {mode} == color or {bitdepth} == 16
set premium = yes when not ({mode} == bw or {keywords} contains proof)

Because parentheses are supported, you can write grouped expressions directly:

set export_variant = premium when ({size} >= 6000 and {mode} == color) or {flag}
set skip_halation = yes when not ({keywords} contains halation and {mode} == color)

Numeric comparisons: Numbers are compared mathematically — 3600 < 4800 is true.

String comparisons: Compared lexicographically.

set camera_tier = premium when {make} == Nikon
set camera_tier = premium when {model} == "Mamiya 7"

Practical condition examples

Use {flag} directly:

set crop_variant = special when {flag} else standard

Check whether metadata exists:

set camera_label = {model} when {model} else unknown camera
set lens_label = {lens} when {lens} else unknown lens

Combine keyword tests and numeric checks:

set export_profile = gallery when {keywords} contains portfolio and {size} >= 6000

Use parentheses to make intent explicit:

set effect_strength = strong when ({format} == xpan or {format} == 17x6) and {mode} == color

Use quotes for exact strings with spaces:

set camera_class = medium format when {model} == "Fuji GW690"

Multiple assignments to the same variable

The last matching condition wins. Always provide a default first:

set grain = medium
set grain = light when {size} < 4800
set grain = heavy when {size} >= 6000

Examples

Size-based processing tiers:

set tier = base
set tier = standard when {size} >= 4800
set tier = premium when {size} >= 6000

keywords: export | size: {size} | actions: Grade {tier} @ Color ; Sharpen @ Export

Format-specific naming:

set format_name = square when {format} == 1x1 else medium format when {format} == 6x7 else large format when {format} == 4x5 else 35mm portrait when {format} == 2x3 else widescreen when {format} == 16x9 else panoramic when {format} == xpan else unknown format

keywords: export | actions: Process {format_name} @ Workflows

Bit depth handling:

set workflow = standard
set workflow = high bit depth when {bitdepth} == 16
set workflow = float precision when {bitdepth} == 32

keywords: process | actions: {workflow} processing @ Advanced

Orientation-dependent effects:

set border_size = medium
set border_size = wide when {orientation} == landscape
set border_size = narrow when {orientation} == portrait

set vignette = standard vignette
set vignette = wide vignette when {orientation} == landscape

keywords: borders | actions: Add {border_size} border @ Borders
keywords: vignette | actions: Apply {vignette} @ Effects

Keyword-based branching with contains:

set halation_action = none
set halation_action = Add halation when {keywords} contains halation

set grain = heavy grain
set grain = light grain when {keywords} contains soft

keywords: film | actions: Apply {grain} @ Texture ; Apply {halation_action} @ Color

Complex conditional with logical operators:

set treatment = standard
set sharpening = normal
set treatment = premium when {size} >= 6000 and {mode} == color
set sharpening = extra sharp when {size} >= 6000 and {mode} == color
set aspect_treatment = wide format when {format} == 4x5 or {format} == xpan

keywords: process | actions: Process with {treatment} @ Workflows ; Apply {sharpening} @ Export

Parentheses and unary not:

set export_variant = client
set export_variant = archive when not ({keywords} contains proof or {mode} == bw)
set export_variant = flagged when {flag}

keywords: export | actions: Export {export_variant} @ Output

Quoted string comparisons:

set camera_profile = rangefinder when {model} == "Mamiya 7"
set camera_profile = wide-angle when {lens} == "24-70mm f/2.8"

keywords: process | actions: Apply {camera_profile} profile @ Cameras

Batch-flagged files from FinishingChunks:

set crop_variant = special when {flag} else standard

keywords: export | actions: Apply {crop_variant} crop @ Export

This is useful together with FinishingChunks’ flagInterval setting when you want every Nth file to take a different path through the recipe.

Complex multi-variable recipe:

set grain_amount = medium grain
set grain_action = Apply to layer

set grain_amount = light grain when {size} < 4800
set grain_amount = heavy grain when {size} >= 6000

set grain_action = Apply and blend when {format} == xpan
set grain_amount = panoramic grain when {format} == xpan

set grain_type = color grain when {mode} == color
set grain_type = bw grain when {mode} == bw

keywords: grain | actions: {grain_action} {grain_amount} {grain_type} @ Texture

List membership:

set format_type = standard when {format} == [2x3, 4x3]
set format_type = square when {format} == [1x1]
set format_type = wide when {format} == [16x9, xpan, 17x6]
set format_type = medium format when {format} == [6x7, 4x5]

set grain = heavy grain when {format} == [xpan, 16x9] and {size} >= 6000

keywords: process | actions: Process {format_type} @ Formats

Debugging

Set debug = true in the script to emit per-image diagnostics. Finishing Touches now writes the debug output to FinishingTouches-debug.log on your Desktop, with a temp-folder fallback if Desktop access is unavailable.

The log includes runtime variables, evaluated set clauses, matching rules, action execution, and tidy operations. ✓ means the clause was applied; ✗ means it did not apply (may include (matched earlier) when a later clause evaluated true but an earlier one already applied). Chained else ... when expressions are grouped together — all clauses for that variable appear under one heading and the first matching when wins.

Custom variables (set statements):
  {grain} = medium
    [✓] medium (default)
  {grain} = (unset)
    [✗] light grain when {size} < 4800
  {grain} = heavy grain
    [✓] heavy grain when {size} >= 6000

Debug output follows evaluation order: runtime variables first, then set statements in recipe order, then variable substitution into rules, then rule execution.

Error handling

If a {variable} is never defined, it remains literal and will cause Photoshop to fail when the script tries to locate an action or metadata field with that name. Always provide an unconditional default:

# Safe: always defined
set my_action = default action
set my_action = special action when {mode} == bw

# Risky: if size is 5000, neither condition matches → {grain} stays as {grain}
set grain = light when {size} < 4800
set grain = heavy when {size} >= 6000

Fix — option 1: Add an unconditional default on its own line before the conditionals.

Fix — option 2: Use a chained else ... when whose final clause is an unconditional fallback:

set mode_label = color when {mode} == color else mono
set iso = 1600 when {keywords} contains iso1600 and {mode} == color else 3200 when {keywords} contains iso3200 and {mode} == bw else 400

Limitations


Writing IPTC metadata

Use meta statements to write IPTC/XMP metadata fields to the document. Place them in the recipe alongside rules and set statements. All variable placeholders are resolved before meta lines are applied.

meta author = Joakim Hertze
meta copyrightNotice = © {year} Joakim Hertze
meta creatorWebsite = https://hertze.se
meta creatorEmail = my_funny@email.com
meta creatorCity = Staffanstorp
meta creatorCountry = Sweden

Note: Setting copyrightNotice automatically sets the copyright status to Copyrighted. To use a different status, add a meta copyrightStatus line after it — publicDomain sets it to false, unknown removes it entirely.

Supported fields

Contact info fields

IPTC contact info is written to the correct XMP struct automatically:


Customizing the script

Edit these variables near the top of FinishingTouches.jsx:

// Color mode keywords
var color_keyword = "color";
var bw_keyword = "bw";

// Format keywords for aspect ratios
var format_1x1 = "1x1";
var format_6x7 = "6x7";
var format_4x5 = "4x5";
var format_4x3 = "4x3";
var format_2x3 = "2x3";
var format_16x9 = "16x9";
var format_xpan = "xpan";
var format_17x6 = "17x6";

// Orientation keywords
var portrait_keyword = "portrait";
var landscape_keyword = "landscape";
var square_keyword = "square";

// Size breakpoints for {size} variable
var size_breakpoints = [3600,4800,6000,7200];

// If true, {size} snaps down to the nearest breakpoint below the short edge.
// If false (default), {size} snaps to the nearest breakpoint (up or down).
var size_round_down = false;

// When processing large or high-resolution files in batches, GPU memory (VRAM) can gradually fill, sometimes leaving colored blocks or mosaic-like tiles in saved files. 
// Enabling gpu_safe_mode = true triggers an automatic Purge -> All between steps, preventing these artifacts.
// This stabilizes output but slows processing, as Photoshop must reload data after each purge.
var gpu_safe_mode = true; 

// If true, action and action set lookup ignores whitespace differences
// and an optional .atn suffix on action set names.
var fuzzy_action_whitespace = true;

// Debug mode
var debug = true;

Why customize:

All these values are used throughout the script and in runtime variables like {format}, {mode}, and {orientation}.


Tips and best practices


TextMate support

The tools/ folder contains a TextMate bundle (FinishingTouches.tmbundle) that adds syntax highlighting and a recipe validator for .ftr files.

Installing the bundle

Double-click FinishingTouches.tmbundle in Finder — TextMate installs it automatically. Restart TextMate if it is already running.

Syntax highlighting and validation

Once the bundle is installed, any file with the .ftr extension is recognised as a Finishing Touches Recipe. The bundle includes a Validate Recipe command. Run it from the Bundles ▸ Finishing Touches menu (or assign a keyboard shortcut in the bundle editor). Results are shown in a new window.


FinishingChunks — better than droplets on macOS

FinishingChunks is a companion macOS utility for when you need to run Photoshop actions on a large folder of images. It processes images in small batches, restarting Photoshop between each batch to keep memory and GPU buffers under control. This leads to less scratch disc use, which may speed up batching on your setup. You use it exactly like you would a Photoshop droplet, including directly during Lightroom exports.

Building your own app from the source

You’ll find the AppleScript file FinishingChunks.applescript in the tools/FinishingChunks/ folder. You use this to build your own app.

  1. Double-click on FinishingChunks.applescript to open it in Script Editor (otherwise found in /Applications/Utilities/).
  2. Edit actionSet and actionName to match the Photoshop action you want to run (see below).
  3. Choose File > Export…
  4. Set File Format to Application.
  5. Make sure Stay open after run handler is unchecked and Startup screen is unchecked. Code signing is optional.
  6. Save it with a name of your choosing.

How to use it

Double-click on your app to launch it and pick a folder, or drag a folder (or individual files) straight onto it. The script finds all matching images, launches the Photoshop version you specified, runs the chosen Action on each file, restarts Photoshop between chunks, and writes a log next to the script.

When used as a droplet from Lightroom or other apps that send files one by one, incoming files are queued and picked up by a single background worker. That avoids AppleScript timeout errors while exports are still arriving.

First run — macOS security

The first time you run the app you will need to grant permission to control the Photoshop.

On recent macOS versions you may also need to grant the app access to protected folders such as Desktop or Documents. If macOS blocks access, open System Settings > Privacy & Security and enable access for your app under Files and Folders, or grant Full Disk Access if needed.

If macOS reports the app as broken, do this:

  1. Go to System Settings > Privacy & Security.
  2. Scroll down to the security section — you should see a message about FinishingChunks.app being blocked.
  3. Click Open Anyway, then confirm.

Settings

Open FinishingChunks.applescript and edit the properties at the top to match your setup:

Timing settings

If Photoshop is slow to open files or your Action takes a long time to save and close, increase these values:

Advanced queue timing

These lower-level settings mainly matter when the app is used like a droplet and files arrive one by one from Lightroom or another exporter:

You normally do not need to change these values unless you are tuning droplet-style workflows or recovering from stuck queued jobs.

Files written by the script

FinishingChunks writes a few runtime files besides your processed images:

The <app-hash> part is generated from the app’s own path, so separate copies of the app get separate queue and lock files.

These /tmp files are transient working files. They are used only for the queueing system, mainly in droplet-style one-file-at-a-time workflows, and are recreated automatically as needed.

Troubleshooting stale queue locks

If a previous run was aborted (for example, force-quit, crash, or power loss), the queue lock folder can sometimes be left behind. In that case, the next launch may log a message like:

Queue lock is already held | error 1 | mkdir: /tmp/finishingchunks-3abf6030481a2f11d14ae9886b0f6c45.lock: File exists

If this keeps repeating and no worker is actually running, close FinishingChunks and Photoshop, then manually clean up the old temp files:

  1. Open Finder and choose Go > Go to Folder…
  2. Enter /tmp and press Enter.
  3. Delete the stale FinishingChunks files for your app copy (for example finishingchunks-<app-hash>.lock, finishingchunks-<app-hash>.queue, and finishingchunks-<app-hash>.queue.batch).
  4. Start FinishingChunks again.

Only remove FinishingChunks temp items that match your app hash. If another active FinishingChunks worker is still running, deleting its lock can cause overlapping workers.

Notes