// SimulateFMScreening.
jsx
//
// This script simulates the effect of FM (Stochastic) screening
// by converting image channels to Bitmap mode using Diffusion Dither.
// It works non-destructively by operating on duplicates or split channels.
// Version: 1.0
#target photoshop
app.bringToFront(); // Bring Photoshop to the front
// Main function wrapped in an anonymous function for scope isolation
(function() {
// --- Basic Checks ---
// Check if a document is open
if (app.documents.length === 0) {
alert("Error: No document open.\nPlease open an image file first to
simulate FM screening.");
return; // Stop the script
}
var doc = app.activeDocument; // Get the currently active document
var startRulerUnits = app.preferences.rulerUnits; // Store original ruler units
app.preferences.rulerUnits = Units.PIXELS; // Use pixels for internal
consistency
// Check if the document mode is supported
var docMode = doc.mode;
if (docMode == DocumentMode.INDEXEDCOLOR || docMode == DocumentMode.DUOTONE ||
docMode == DocumentMode.MULTICHANNEL || docMode == DocumentMode.LAB) {
alert("Error: Unsupported document mode (" + docMode + ").\nPlease use
Grayscale, RGB, or CMYK images.");
app.preferences.rulerUnits = startRulerUnits; // Restore ruler units
before exiting
return;
}
if (docMode == DocumentMode.BITMAP) {
alert("Info: Image is already in Bitmap mode.\nNo further FM simulation
needed or possible with this script.");
app.preferences.rulerUnits = startRulerUnits; // Restore ruler units before
exiting
return;
}
// --- Configuration & User Input ---
var defaultResolution = 300; // Default DPI suggestion for the prompt
var outputResolution; // Variable to hold the user's choice
// Loop to get valid resolution input
while (true) {
var userInput = prompt(
"Enter desired output resolution (in DPI) for the FM simulation.\n" +
"This controls the fineness of the pattern (dot density).\n" +
"Higher values (e.g., 600, 1200) create finer textures.",
defaultResolution
);
if (userInput === null) { // User pressed Cancel
alert("Process cancelled by user.");
app.preferences.rulerUnits = startRulerUnits; // Restore ruler units
return; // Stop script
}
outputResolution = parseFloat(userInput);
if (!isNaN(outputResolution) && outputResolution > 0) {
break; // Valid input, exit loop
} else {
alert("Invalid input. Please enter a positive number for the resolution
(DPI).");
// Loop continues
}
}
// --- Core Function: Convert to Bitmap using Diffusion Dither ---
function convertToBitmapWithDiffusion(targetDoc, resolution) {
// This function takes a document and converts it to Bitmap
try {
// Ensure it's Grayscale first (required for Bitmap conversion)
if (targetDoc.mode !== DocumentMode.GRAYSCALE) {
targetDoc.changeMode(ChangeMode.GRAYSCALE); // Convert to Grayscale
}
// Define Bitmap Conversion options
var bitmapOptions = new BitmapConversionOptions();
bitmapOptions.method = BitmapConversionType.DIFFUSIONDITHER; // The key
step for FM simulation!
bitmapOptions.resolution = resolution; // Use the user-defined
resolution
// Perform the conversion
targetDoc.changeMode(ChangeMode.BITMAP, bitmapOptions);
// Rename the layer for clarity
try {
targetDoc.activeLayer.name = targetDoc.name.replace(/\.[^/.]+$/,
"") + " (FM Simulated)";
} catch(layerError) {
// Ignore error if layer renaming fails (e.g., background layer
issues)
// $.writeln("Note: Could not rename layer for " + targetDoc.name);
// For debugging
}
return true; // Indicate success
} catch (e) {
alert("Error during Bitmap conversion on document '" + targetDoc.name +
"':\n" + e.message);
// Attempt to close the potentially broken doc ONLY if it's a temporary
split channel document
// Check if the targetDoc is NOT the original document (or its direct
duplicate in grayscale case)
if (targetDoc !== doc && targetDoc.name !== (doc.name + " (FM
Simulated Grayscale)")) {
try {
targetDoc.close(SaveOptions.DONOTSAVECHANGES);
// $.writeln("Closed potentially corrupted temp doc: " +
targetDoc.name); // For debugging
} catch(closeError) {
// $.writeln("Could not close temp doc: " +
targetDoc.name); // For debugging
}
}
return false; // Indicate failure
}
}
// --- Main Processing Logic based on Color Mode ---
try { // Wrap main logic in try/catch for unexpected errors
if (docMode == DocumentMode.GRAYSCALE) {
// --- Grayscale Processing ---
alert("Processing Grayscale image.\nA duplicate document will be
created and converted.");
var dupDoc = doc.duplicate(doc.name + " (FM Simulated Grayscale)",
true); // Duplicate the document, including merged layers
app.activeDocument = dupDoc; // Make sure the duplicate is active
if (convertToBitmapWithDiffusion(dupDoc, outputResolution)) {
alert("Grayscale FM Simulation Complete.\nCheck the new document:
'" + dupDoc.name + "'");
} else {
alert("Grayscale FM Simulation Failed. The duplicate document
might be closed or in an error state.");
}
} else if (docMode == DocumentMode.RGB || docMode == DocumentMode.CMYK) {
// --- Color Processing (RGB/CMYK) ---
var colorSpaceName = (docMode == DocumentMode.RGB) ? "RGB" : "CMYK";
alert("Processing " + colorSpaceName + " image.\n" +
"1. A temporary duplicate will be made.\n" +
"2. Channels will be split into NEW Grayscale documents.\n" +
"3. Each new channel document will be converted to Bitmap.");
// Duplicate the original document BEFORE splitting channels.
// Splitting works on the active document and closes it.
var originalName = doc.name;
var tempDupName = originalName.replace(/\.[^/.]+$/, "") + " (Temp for
Split)"; // Create a base name for the temp doc
var dupDocForSplitting = doc.duplicate(tempDupName, true); // Make a
safe copy to split
app.activeDocument = dupDocForSplitting; // Ensure the duplicate is
active
try {
// Store expected channel names (important for identifying results)
var expectedChannelNames = [];
var channelObjects = dupDocForSplitting.componentChannels; // Get
only R,G,B or C,M,Y,K
for (var i = 0; i < channelObjects.length; i++) {
expectedChannelNames.push(channelObjects[i].name);
}
// Flatten the duplicate before splitting to ensure clean
separation
dupDocForSplitting.flatten();
// Execute the "Split Channels" command using its Type ID for
reliability
// This closes dupDocForSplitting and creates new grayscale docs.
app.runMenuItem(stringIDToTypeID("splitChannels"));
// Process the newly opened channel documents
var numChannels = expectedChannelNames.length;
var processedCount = 0;
var failedChannels = [];
// Loop through documents to find and process the split channels.
// It's safer to iterate a fixed number of times based on expected
channels
// and rely on Photoshop opening them predictably. We check names
for confirmation.
var openDocs = app.documents;
var baseNameForChannels = tempDupName; // The name base Photoshop
uses for split channels
// Iterate backwards through open documents, as new docs often
appear at the end
for (var i = openDocs.length - 1; i >= 0 && processedCount <
numChannels; i--) {
var channelDoc = openDocs[i];
var isExpectedChannel = false;
// Check if the document name matches the pattern
"BaseName_ChannelName"
for (var j = 0; j < expectedChannelNames.length; j++) {
var expectedName = baseNameForChannels + "_" +
expectedChannelNames[j];
// Allow for potential file extension added by Photoshop
(.tif, .psd etc.)
if (channelDoc.name.indexOf(expectedName) === 0 &&
channelDoc.mode === DocumentMode.GRAYSCALE) {
isExpectedChannel = true;
break;
}
}
if (isExpectedChannel) {
app.activeDocument = channelDoc; // Activate the channel
document
if (convertToBitmapWithDiffusion(channelDoc,
outputResolution)) {
processedCount++;
} else {
// Record failure but continue trying other channels
failedChannels.push(channelDoc.name);
}
// Remove the processed/failed channel name from expected
list to avoid processing duplicates if names are ambiguous
expectedChannelNames.splice(j, 1);
}
} // End loop through open documents
// --- Final Report for Color Images ---
if (processedCount === numChannels) {
alert(colorSpaceName + " FM Simulation Complete.\n" +
processedCount + " channel(s) were processed into
separate Bitmap documents.");
} else {
var alertMessage = "Warning: FM Simulation for " +
colorSpaceName + " image completed with issues.\n" +
"Processed " + processedCount + " out of " +
numChannels + " expected channels.\n";
if (failedChannels.length > 0) {
alertMessage += "\nFailed or could not process:\n- " +
failedChannels.join("\n- ");
}
if (expectedChannelNames.length > 0) {
alertMessage += "\nCould not find or match documents for
expected channels:\n- " + expectedChannelNames.join("\n- ");
}
alertMessage += "\nPlease check your open documents.";
alert(alertMessage);
}
} catch (splitError) {
alert("An error occurred during the channel splitting or processing
stage:\n" + splitError.message);
// The temporary duplicate might still be open if split failed
early
try {
// Attempt to find and close the temp duplicate if it exists
for(var k=0; k < app.documents.length; k++){
if(app.documents[k].name.indexOf(tempDupName) === 0){
app.documents[k].close(SaveOptions.DONOTSAVECHANGES);
break;
}
}
} catch(closeError) {} // Ignore errors during cleanup
}
} // End color processing block
} catch (mainError) {
alert("An unexpected error occurred in the main script:\n" +
mainError.message);
} finally {
// --- Cleanup ---
// Restore original ruler units regardless of success or failure
app.preferences.rulerUnits = startRulerUnits;
// $.writeln("Script finished. Restored ruler units."); // For debugging
}
})(); // End of anonymous function wrapper
```
**How to Use This Script:**
1. **Save:** Copy the code above. Paste it into a plain text editor (like Notepad
on Windows, TextEdit on Mac - in plain text mode, or a code editor like VS Code).
Save the file with a `.jsx` extension (e.g., `FMSimulator.jsx`).
2. **Place (Optional but Recommended):** For easy access, place the saved `.jsx`
file into your Photoshop Scripts folder:
* **Windows:** `C:\Program Files\Adobe\Adobe Photoshop [Your Version]\Presets\
Scripts\`
* **macOS:** `/Applications/Adobe Photoshop [Your Version]/Presets/Scripts/`
* (Replace `[Your Version]` with your specific Photoshop version, like `2024`
or `2025`).
* Restart Photoshop if it was running. The script will now appear directly in
the `File > Scripts` menu.
3. **Run:**
* Open the Grayscale, RGB, or CMYK image you want to process in Photoshop.
* Go to `File > Scripts`. If you placed the file in the Scripts folder, select
`FMSimulator` (or whatever you named it).
* If you didn't place it in the Scripts folder, select `Browse...` and navigate
to where you saved the `.jsx` file.
4. **Follow Prompts:** The script will ask for the desired output resolution
(DPI). Higher values create a finer, denser pattern, simulating higher LPI (lines
per inch) equivalents in traditional screening. Enter a value and click OK.
5. **Review Results:**
* **Grayscale:** A new document, named like `YourFileName (FM Simulated
Grayscale)`, will be created in Bitmap mode.
* **RGB/CMYK:** Several new Grayscale documents will be created, one for each
color channel (e.g., `YourFileName (Temp for Split)_Red`, `..._Green`, `..._Blue`).
Each of these will be in Bitmap mode, showing the FM simulation for that specific
channel. Your original color document remains untouched.
**Testing and Accuracy:**
* **Test Files:** Use images with smooth gradients, fine details, and solid color
areas to see how the simulation behaves. Test Grayscale, RGB, and CMYK files.
* **Resolution:** Experiment with different resolutions (e.g., 150, 300, 600, 1200
DPI) on the same image to understand its impact on the pattern's fineness.
* **Visual Check:** Zoom in closely on the resulting Bitmap images. You should see
only black and white pixels. The *density* of black pixels should correspond to the
darkness of the original image areas. The pattern should look somewhat random or
stochastic, not like the regular grid of traditional halftone dots.
* **Accuracy:** Remember, this is a *simulation* using Photoshop's Diffusion
Dither. It visually mimics the *principle* of FM screening very well but won't be a
mathematically perfect replica of a specific commercial FM screening technology
(like Kodak Staccato, Agfa CristalRaster, etc.). It's excellent for previewing the
effect or for creative purposes.
This script provides a reliable way to simulate FM screening directly within
Photoshop using its built-in capabiliti