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 (must appear in Keyword order). Additional keywords are modifiers:

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

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

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 seven 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), making all runtime variables available for set conditions
  6. Processes set statements — evaluates conditional assignments, builds custom variables
  7. Applies meta statements — writes IPTC fields to the document with all variables already resolved
  8. For each keyword in order:
    • Finds rules whose master keyword matches
    • Filters by aspect ratio (if rule specifies one)
    • Picks the most specific matching rule (more keywords = more specific; with ratio > without ratio; earlier in list wins ties)
    • Resizes (if rule has size:)
    • Runs all actions in the rule
  9. Removes matched keywords (if tidy option enabled)
  10. Saves and closes (if option enabled)

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. All set statements must be placed at the top of your recipe, before any rules. The script processes them in order, then substitutes the results into your rules.

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

Conditions and operators

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

List membership (== and != only):

set my_format = wide when {format} == [16x9, 21x9]
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.

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. Cannot be combined with and / or in the same sub-expression — use separate set lines for complex logic.

Logical operators: and (higher precedence than or), or

set grain = heavy when {size} >= 6000 and {format} == 2x3
set treatment = special when {mode} == color or {bitdepth} == 16

Use multiple set statements instead of parentheses for complex logic.

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

String comparisons: Compared lexicographically. Values with spaces work fine: set action_name = light grain when {size} < 4800

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

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, 3x2, 4x3, 3x4]
set format_type = square when {format} == [1x1]
set format_type = wide when {format} == [16x9, 21x9, xpan]
set format_type = medium format when {format} == [6x7, 4x5, 5x4]

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 (line 56) to emit per-image diagnostics. 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 at the top of FinishingTouches.jsx (lines 33–55):

// 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";

// 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 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 = false;

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.

Notes