Detecting Text Overset in InDesign: A Minimal Script Before Automation

Update (2026-02-18):
Fixed a restore issue that could leave a black 1pt stroke in certain environments. Please use the updated script below.

Note:
In some localized versions of InDesign, the internal name of the built-in “None” swatch may differ. If the restore script does not correctly remove the stroke, verify the swatch name in your document and adjust the getNoneSwatch() function accordingly.

Context

In the previous article, I explained why text overset is not just a layout problem, but a scaling problem in multilingual InDesign workflows.

When overset handling depends on manual inspection and ad-hoc fixes, the workflow breaks down quickly as document count and language count increase.

This article is a follow-up.
It intentionally does not attempt to fix overset text.

Instead, it focuses on a more fundamental question:

How do you reliably see where overset exists before deciding what to do?

If you have not read the previous article yet, it provides important context on why overset detection itself must scale, not just the correction step.

Previous article:
https://www.linguist-coder.com/2026/01/scaling-multilingual-dtp-by-eliminating.html


Why Detection Still Matters (Even with Preflight)

InDesign already has a Preflight feature, and it does detect overset text.

The problem is not capability, but usability at scale.

  • Preflight reports issues as a list

  • This script visualizes issues directly on the page

Instead of scanning rows in a panel, you flip through pages and immediately see where overset exists.

Think of it as a visual pressure map for text.

Before fixing anything, this answers a more important question:

Are these a few isolated problems, or a structural issue that will not scale manually?


Design Philosophy of This Free Sample

This script is intentionally limited.

What it does

  • Scans all text frames in the document (including master spreads)

  • Detects overset text frames

  • Makes them visually obvious using stroke color and stroke weight

  • Stores minimal metadata for later cleanup

What it does not do

  • Automatically fix overset

  • Change text composition

  • Adjust fonts, tracking, or layout rules

This is deliberate.

Overset correction requires context and judgment.
Blind automation in this area is more likely to damage layouts than improve them.

This sample exists to support human decision-making, not replace it.


Why Stroke Color + Stroke Weight

Overset frames are marked by:

  • Changing the stroke color (e.g. a bright warning color such as red)

  • Increasing the stroke weight to 2pt

The fill is left untouched to avoid interfering with layout or readability.

Increasing stroke weight is intentional.
Without it, visibility is poor in dense layouts.

Important clarification:
This script does not change text content or layout decisions.

It only adds a temporary visual marker to help you identify where overset occurs.




Detection Script: Mark Overset Text Frames

#target indesign /* Detect overset text frames and mark them with a warning stroke. Marker is stored in TextFrame.label as: |LC_OVRSET_V1{c=<origStrokeColorToken>;w=<origStrokeWeight>} Where c=@NONE means "no stroke" (None). Note: - Some environments are sensitive to file encoding. If the script does not run, save as "UTF-8 with BOM". */ (function () { var MARK_KEY = "LC_OVRSET_V1"; var NONE_TOKEN = "@NONE"; var WARN_STROKE_WEIGHT = 2; // pt var WARN_SWATCH_NAME = "LC_Overset_Warn"; if (app.documents.length === 0) { alert("No documents are currently open."); return; } var doc = app.activeDocument; var frames = collectAllTextFrames(doc); var scanned = frames.length; var oversetFound = 0; var marked = 0; var alreadyMarked = 0; var skippedLocked = 0; var skippedErrors = 0; for (var i = 0; i < frames.length; i++) { var tf = frames[i]; if (!tf || !tf.isValid) continue; try { if (!tf.overflows) continue; oversetFound++; // Avoid label pollution on repeated runs if (tf.label && tf.label.indexOf("|" + MARK_KEY + "{") !== -1) { alreadyMarked++; continue; } if (isLockedOrOnLockedLayer(tf)) { skippedLocked++; continue; } // Capture original stroke var origColorToken = getStrokeColorToken(doc, tf, NONE_TOKEN); var origWeight = safeNumber(tf.strokeWeight, 0); // Apply warning stroke tf.strokeColor = getWarningSwatch(doc, WARN_SWATCH_NAME); tf.strokeWeight = WARN_STROKE_WEIGHT; // Store marker tf.label = (tf.label || "") + "|" + MARK_KEY + "{c=" + origColorToken + ";w=" + origWeight + "}"; marked++; } catch (e) { skippedErrors++; } } alert(buildSummary()); function buildSummary() { var lines = []; lines.push("Overset detection completed."); lines.push(""); lines.push("Text frames scanned: " + scanned); lines.push("Overset frames found: " + oversetFound); lines.push("Marked: " + marked); lines.push("Already marked: " + alreadyMarked); if (skippedLocked > 0 || skippedErrors > 0) { lines.push(""); lines.push("Warnings:"); if (skippedLocked > 0) lines.push("- Skipped (locked object or layer): " + skippedLocked); if (skippedErrors > 0) lines.push("- Skipped (other errors): " + skippedErrors); } return lines.join("\n"); } function collectAllTextFrames(doc) { var result = []; try { collectFromItems(doc.allPageItems, result); } catch (e) {} // Include master items try { for (var m = 0; m < doc.masterSpreads.length; m++) { try { collectFromItems(doc.masterSpreads[m].allPageItems, result); } catch (e2) {} } } catch (e3) {} return result; function collectFromItems(items, out) { var len = 0; try { len = items.length; } catch (e) { return; } for (var i = 0; i < len; i++) { try { var it = items[i]; if (it && it.isValid && it.constructor.name === "TextFrame") out.push(it); } catch (e2) {} } } } function isLockedOrOnLockedLayer(tf) { try { if (tf.locked) return true; } catch (e) {} try { if (tf.itemLayer && tf.itemLayer.locked) return true; } catch (e2) {} return false; } function getStrokeColorToken(doc, tf, noneToken) { try { var c = tf.strokeColor; if (isNoneLike(doc, c)) return noneToken; return (c && c.isValid && c.name) ? c.name : noneToken; } catch (e) { return noneToken; } } // Robust "None" detection (locale-safe enough for common environments) function isNoneLike(doc, colorObj) { if (!colorObj) return true; try { if (!colorObj.isValid) return true; } catch (e0) { return true; } // Name-based (works in your environment: "None") try { var n = colorObj.name; if (n === "None" || n === "[None]") return true; } catch (e1) {} // Identity-based fallback try { var noneSw = getNoneSwatch(doc); if (noneSw && noneSw.isValid && colorObj === noneSw) return true; } catch (e2) {} return false; } function getNoneSwatch(doc) { // Your environment uses "None", but keep both for portability. try { var s0 = doc.swatches.itemByName("None"); s0.name; return s0; } catch (e0) {} try { var s1 = doc.swatches.itemByName("[None]"); s1.name; return s1; } catch (e1) {} // Minimal scan fallback try { for (var i = 0; i < doc.swatches.length; i++) { var sw = doc.swatches[i]; if (!sw || !sw.isValid) continue; var nm = ""; try { nm = sw.name; } catch (e2) {} if (nm === "None" || nm === "[None]") return sw; } } catch (e3) {} return null; } function getWarningSwatch(doc, swatchName) { try { var s = doc.colors.itemByName(swatchName); s.name; // validate return s; } catch (e) { return doc.colors.add({ name: swatchName, model: ColorModel.PROCESS, space: ColorSpace.RGB, colorValue: [255, 0, 0] }); } } function safeNumber(n, fallback) { var v = parseFloat(n); return isNaN(v) ? fallback : v; } })();

Restore Script (Cleanup, Not Undo)

This restore script exists for cleanup, not for perfect rollback.

If your goal is simply to undo the script execution,
InDesign’s built-in “Revert” feature is the safest option.

The restore script is intended for two specific situations:

  • After you manually fix overset text and want to remove the visual markers

  • When you forgot to save before running the detection script

It removes only the visualization layer
(stroke color and stroke weight added by the detection script).

It does not attempt to restore all original appearance details
such as stroke style, tint, or other decorative attributes.

Implementation notes

The restore logic is written defensively to handle real-world InDesign documents:

  • Frames that became invalid (e.g. after ungrouping) are skipped safely

  • Locked objects and locked layers are not modified

  • Multiple markers from previous runs are fully removed

  • Frames that originally had no stroke are explicitly restored to “None”

This keeps the script predictable and avoids leaving warning styles behind.

About Undo support

For simplicity, this sample does not wrap changes in a single Undo step.

If you adapt this logic for production use,
wrapping the script with app.doScript(...) and UndoModes.ENTIRE_SCRIPT
is the standard approach to improve safety.

This sample intentionally avoids that step
to keep the focus on conceptual clarity rather than tool completeness.


Restore Script: Remove Overset Markers

#target indesign (function () { var MARK_KEY = "LC_OVRSET_V1"; var NONE_TOKEN = "@NONE"; if (app.documents.length === 0) { alert("No documents are currently open."); return; } var doc = app.activeDocument; var frames = collectAllTextFrames(doc); var scanned = frames.length; var restored = 0; var notMarked = 0; var skippedLocked = 0; var skippedErrors = 0; var failedSetNone = 0; for (var i = 0; i < frames.length; i++) { var tf = frames[i]; if (!tf || !tf.isValid) continue; try { var parsed = parseLastMark(tf.label, MARK_KEY); if (!parsed) { notMarked++; continue; } if (isLockedOrOnLockedLayer(tf)) { skippedLocked++; continue; } if (parsed.colorToken === NONE_TOKEN) { // Force None + 0 if (!setNoStroke(doc, tf)) { failedSetNone++; } } else { // Restore original color if resolvable var restoredColor = resolveColorOrSwatch(doc, parsed.colorToken); if (restoredColor) tf.strokeColor = restoredColor; if (!isNaN(parsed.weight)) tf.strokeWeight = parsed.weight; } tf.label = removeAllMarks(tf.label, MARK_KEY); restored++; } catch (e) { skippedErrors++; } } alert( "Restore completed.\n\n" + "Text frames scanned: " + scanned + "\n" + "Restored (marker removed): " + restored + "\n" + "Not marked: " + notMarked + "\n" + (skippedLocked ? ("Skipped locked: " + skippedLocked + "\n") : "") + (failedSetNone ? ("Failed to set None (kept stroke): " + failedSetNone + "\n") : "") + (skippedErrors ? ("Errors: " + skippedErrors + "\n") : "") ); function setNoStroke(doc, tf) { // Get "None" swatch fresh each time (environment-safe) var noneSw = getNoneSwatch(doc); if (!noneSw) { // If we can't even find None, at least set weight=0 try { tf.strokeWeight = 0; } catch (e0) {} return false; } // Apply try { tf.strokeColor = noneSw; } catch (e1) {} try { tf.strokeWeight = 0; } catch (e2) {} // Verify (some environments keep old strokeColor) try { var sc = tf.strokeColor; var nm = sc && sc.isValid ? sc.name : ""; if (nm === "None" || nm === "[None]" || nm === "なし" || nm === "[なし]") return true; } catch (e3) {} // Retry once try { tf.strokeColor = noneSw; } catch (e4) {} return true; } function collectAllTextFrames(doc) { var result = []; try { collectFromItems(doc.allPageItems, result); } catch (e) {} try { for (var m = 0; m < doc.masterSpreads.length; m++) { try { collectFromItems(doc.masterSpreads[m].allPageItems, result); } catch (e2) {} } } catch (e3) {} return result; function collectFromItems(items, out) { var len = 0; try { len = items.length; } catch (e) { return; } for (var i = 0; i < len; i++) { try { var it = items[i]; if (it && it.isValid && it.constructor.name === "TextFrame") out.push(it); } catch (e2) {} } } } function isLockedOrOnLockedLayer(tf) { try { if (tf.locked) return true; } catch (e) {} try { if (tf.itemLayer && tf.itemLayer.locked) return true; } catch (e2) {} return false; } function parseLastMark(label, key) { if (!label) return null; var needle = "|" + key + "{"; var pos = label.lastIndexOf(needle); if (pos === -1) return null; var end = label.indexOf("}", pos); if (end === -1) return null; var token = label.substring(pos + needle.length, end); var parts = token.split(";"); var colorToken = ""; var weight = NaN; for (var i = 0; i < parts.length; i++) { var kv = parts[i].split("="); if (kv.length !== 2) continue; if (kv[0] === "c") colorToken = kv[1]; if (kv[0] === "w") weight = parseFloat(kv[1]); } return { colorToken: colorToken, weight: weight }; } function removeAllMarks(label, key) { if (!label) return ""; var needle = "|" + key + "{"; var out = label; while (true) { var start = out.indexOf(needle); if (start === -1) break; var end = out.indexOf("}", start); if (end === -1) break; out = out.substring(0, start) + out.substring(end + 1); } return out; } function getNoneSwatch(doc) { // Your environment shows "None (Swatch)" so try "None" first. try { var s0 = doc.swatches.itemByName("None"); s0.name; return s0; } catch (e0) {} try { var s1 = doc.swatches.itemByName("[None]"); s1.name; return s1; } catch (e1) {} // Scan by name / constructor try { for (var i = 0; i < doc.swatches.length; i++) { var sw = doc.swatches[i]; if (!sw || !sw.isValid) continue; var nm = ""; var cn = ""; try { nm = sw.name; } catch (e2) {} try { cn = sw.constructor && sw.constructor.name ? sw.constructor.name : ""; } catch (e3) {} if (nm === "None" || nm === "[None]" || nm === "なし" || nm === "[なし]" || cn === "NoColor") return sw; } } catch (e4) {} return null; } function resolveColorOrSwatch(doc, name) { if (!name) return null; try { var c = doc.colors.itemByName(name); if (c && c.isValid) return c; } catch (e1) {} try { var s = doc.swatches.itemByName(name); if (s && s.isValid) return s; } catch (e2) {} return null; } })();

Closing Thoughts

Overset itself is not the real problem.

The real problem is mixing detection, judgment, and correction into a single step.

This free sample exists to separate those concerns —
not to fix everything, but to help you decide what deserves fixing at all.

That separation is what allows multilingual DTP workflows to scale.

Popular Posts