Skip to main content
Known Participant
Oct 27, 2023 9:49 AM UTC
Answered

Batch list import for list box or combo box in indesign. Interactive PDF

Hi team,

 

I need a help to import a batch of names in Indesign List items to create a dropdown for interactive PDF. I need a support in indesign.

 

For now i can see the only way to add List Items manually one by one. is there any way add the bundle of list items in single shot. I have 900 names to add.

Please support with any possible ways or any indesign script;

Thanks in Advance.

Nousheeth

TᴀW
TᴀWCorrect answer
Legend
Oct 27, 2023 10:00 AM UTC

Do you also need to import export values for each item in the list?

If you don't need export values and only need to add items to the list, there are a couple of free scripts out there, one by Keith Gilbert and one by Chuck Weger called ComboMambo (https://creativepro.com/a-script-for-populating-form-fields-in-indesign/) which should do the trick.

My own (not free) FormMaker add-on (https://www.id-extras.com/products/formmaker/) allows you to do this but also allows you to bulk import an export value alongside each list item. Very useful if you'll be needing to actually do something with th user-selection in your interactive form afterwards.

And on a UX/UI note, I wonder if a list with 900 entries really works for the end-user? Perhaps it would be better to split the list into several sub-categories to make it easier for the user to find what they need? Of course, I know nothing about your form, so perhaps this isn't possible or necessary here. Just a suggestion!

id-extras.com | InDesign tools & scripts for typesetters, form designers, and translators

16 replies

NousheethAuthor
Known Participant
Nov 4, 2026 1:21 PM UTC
V26 part 2

// -------------------------------------------------------------
// QC Check 7: Hyphenation Check & Uncheck All
// -------------------------------------------------------------
function scanHyphenation(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return 0;

state.hyphenations = [];
state.hyphenationIndex = 0;

try {
var stories = doc.stories;
for (var s = 0; s < stories.length; s++) {
var story = stories[s];
var paras = story.paragraphs;
for (var p = 0; p < paras.length; p++) {
var para = paras[p];
if (para.hyphenation === true) {
var words = para.words;
var hasBrokenWord = false;

for (var w = 0; w < words.length; w++) {
var word = words[w];
if (word.lines.length > 1) {
state.hyphenations.push({
target: word,
para: para,
desc: "Hyphenated Word: \"" + word.contents + "\""
});
hasBrokenWord = true;
}
}

if (!hasBrokenWord && para.lines.length > 1) {
state.hyphenations.push({
target: para,
para: para,
desc: "Hyphenate Enabled on Paragraph (" + para.lines.length + " lines)"
});
}
}
}
}
} catch (e) {
appendLog("Error scanning hyphenated text: " + e.message);
}

lblHyphenCount.text = "" + state.hyphenations.length;
appendLog("Hyphenation: " + state.hyphenations.length + " occurrences found.");
return state.hyphenations.length;
}

function reviewHyphenation() {
if (state.hyphenations.length === 0) {
appendLog("No hyphenation to review.");
return;
}
if (state.hyphenationIndex >= state.hyphenations.length) {
state.hyphenationIndex = 0;
}

var item = state.hyphenations[state.hyphenationIndex];
try {
if (item.target.isValid) {
app.select(item.target, SelectionOptions.REPLACE_WITH);
zoomToSelectedTarget(item.target);
var page = getItemPage(item.target);
appendLog("Reviewing Hyphenation (" + (state.hyphenationIndex + 1) + " of " + state.hyphenations.length + ") on " + (page ? "Page " + page.name : "Pasteboard") + ": " + item.desc);
state.hyphenationIndex++;
} else {
state.hyphenationIndex++;
reviewHyphenation();
}
} catch (e) {
appendLog("Error navigating to hyphenation: " + e.message);
}
}

function uncheckAllHyphenation(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return;

if (!confirm("Are you sure you want to uncheck hyphenation across all paragraphs in the document?", false, "Confirm Uncheck Hyphenation")) {
return;
}

app.doScript(function () {
var count = 0;
var prevRedraw = app.scriptPreferences.enableRedraw;
try {
app.scriptPreferences.enableRedraw = false;
var stories = doc.stories;
for (var s = 0; s < stories.length; s++) {
var paras = stories[s].paragraphs;
for (var p = 0; p < paras.length; p++) {
if (paras[p].hyphenation === true) {
paras[p].hyphenation = false;
count++;
}
}
}
appendLog("Unchecked Hyphenate on " + count + " paragraphs across the document.");
scanHyphenation(doc);
} catch (e) {
appendLog("Error unchecking hyphenation: " + e.message);
} finally {
app.scriptPreferences.enableRedraw = prevRedraw;
}
}, ScriptLanguage.JAVASCRIPT, undefined, UndoModes.ENTIRE_SCRIPT, "QC Uncheck Hyphenation");
}

// -------------------------------------------------------------
// QC Check 8: Ligatures Check & Turn Off Fix
// -------------------------------------------------------------
function scanLigatures(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return 0;

state.ligatures = [];
state.ligatureIndex = 0;

try {
var stories = doc.stories;
for (var s = 0; s < stories.length; s++) {
var ranges = stories[s].textStyleRanges;
for (var r = 0; r < ranges.length; r++) {
var range = ranges[r];
if (range.contents.replace(/[\r\n\s\t]+/g, "").length === 0) continue;

if (range.ligatures === true) {
if (/(fi|fl|ff|ffi|ffl|[fiflffffiffl])/i.test(range.contents)) {
state.ligatures.push(range);
}
}
}
}
} catch (e) {
appendLog("Error scanning ligatures: " + e.message);
}

lblLigatureCount.text = "" + state.ligatures.length;
appendLog("Ligatures Used: " + state.ligatures.length + " ranges found.");
return state.ligatures.length;
}

function reviewLigature() {
if (state.ligatures.length === 0) {
appendLog("No ligatures to review.");
return;
}
if (state.ligatureIndex >= state.ligatures.length) {
state.ligatureIndex = 0;
}

var range = state.ligatures[state.ligatureIndex];
try {
if (range.isValid) {
app.select(range, SelectionOptions.REPLACE_WITH);
zoomToSelectedTarget(range);
var page = getItemPage(range);
var sample = range.contents;
if (sample.length > 22) sample = sample.substring(0, 19) + "...";
appendLog("Reviewing Ligature (" + (state.ligatureIndex + 1) + " of " + state.ligatures.length + ") on " + (page ? "Page " + page.name : "Pasteboard") + ": \"" + sample.replace(/[\r\n]+/g, " ") + "\"");
state.ligatureIndex++;
} else {
state.ligatureIndex++;
reviewLigature();
}
} catch (e) {
appendLog("Error navigating to ligature: " + e.message);
}
}

function fixCurrentLigature() {
if (state.ligatures.length === 0) {
appendLog("No ligature found to fix.");
return;
}

var targetIdx = state.ligatureIndex > 0 ? (state.ligatureIndex - 1) : 0;
if (targetIdx >= state.ligatures.length) targetIdx = 0;

var range = state.ligatures[targetIdx];
try {
if (range.isValid) {
range.ligatures = false;
state.ligatures.splice(targetIdx, 1);
lblLigatureCount.text = "" + state.ligatures.length;
appendLog("Turned off ligatures on selected text. Remaining: " + state.ligatures.length);

if (state.ligatures.length > 0) {
if (targetIdx >= state.ligatures.length) state.ligatureIndex = 0;
else state.ligatureIndex = targetIdx;
reviewLigature();
} else {
state.ligatureIndex = 0;
appendLog("All ligatures turned off!");
}
} else {
state.ligatures.splice(targetIdx, 1);
lblLigatureCount.text = "" + state.ligatures.length;
fixCurrentLigature();
}
} catch (e) {
appendLog("Error fixing ligature: " + e.message);
}
}

function fixAllLigatures(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return;

if (!confirm("Are you sure you want to turn off ligatures across the entire document?", false, "Confirm Turn Off All Ligatures")) {
return;
}

app.doScript(function () {
var count = 0;
var prevRedraw = app.scriptPreferences.enableRedraw;
try {
app.scriptPreferences.enableRedraw = false;
var stories = doc.stories;
for (var s = 0; s < stories.length; s++) {
var ranges = stories[s].textStyleRanges;
for (var r = 0; r < ranges.length; r++) {
if (ranges[r].ligatures === true) {
ranges[r].ligatures = false;
count++;
}
}
}
appendLog("Turned off ligatures on " + count + " text ranges across document.");
scanLigatures(doc);
} catch (e) {
appendLog("Error turning off ligatures: " + e.message);
} finally {
app.scriptPreferences.enableRedraw = prevRedraw;
}
}, ScriptLanguage.JAVASCRIPT, undefined, UndoModes.ENTIRE_SCRIPT, "QC Turn Off Ligatures");
}

// -------------------------------------------------------------
// QC Check: Percentage Spacing (Spanish & French Jobs: Space Before %)
// Finds numbers followed directly by % (5%) or with standard space (5 %),
// and converts them to non-breaking space (5~s% / 5\u00A0%).
// -------------------------------------------------------------
function scanPercentSpaces(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return 0;

state.percentSpaces = [];
state.percentSpaceIndex = 0;

app.findGrepPreferences = NothingEnum.nothing;
app.findGrepPreferences.findWhat = "(\\d+(?:[.,]\\d+)?)[ \\t]*%";

try {
var rawMatches = doc.findGrep();
for (var i = 0; i < rawMatches.length; i++) {
var m = rawMatches[i];
try {
if (m && m.isValid) {
state.percentSpaces.push(m);
}
} catch (eM) {}
}
} catch (e) {
appendLog("Error scanning percent spaces: " + e.message);
} finally {
app.findGrepPreferences = NothingEnum.nothing;
}

state.percentSpaceCount = state.percentSpaces.length;
lblPercentCount.text = "" + state.percentSpaceCount;

if (state.percentSpaceCount > 0) {
appendLog("Percent Spacing (ES/FR): Found " + state.percentSpaceCount + " percentage(s) missing non-breaking space before %.");
} else {
appendLog("Percent Spacing (ES/FR): 0 issues (all percentages have non-breaking space).");
}

return state.percentSpaceCount;
}

function reviewPercentSpace() {
var doc = getActiveDocument();
if (!doc) return;

app.findGrepPreferences = NothingEnum.nothing;
app.findGrepPreferences.findWhat = "(\\d+(?:[.,]\\d+)?)[ \\t]*%";

try {
var matches = doc.findGrep();
state.percentSpaces = matches;
lblPercentCount.text = "" + matches.length;

if (matches.length === 0) {
appendLog("No percentage spacing issues found to review.");
return;
}

if (state.percentSpaceIndex >= matches.length) {
state.percentSpaceIndex = 0;
}

var match = matches[state.percentSpaceIndex];
if (match && match.isValid) {
app.select(match, SelectionOptions.REPLACE_WITH);
zoomToSelectedTarget(match);
var page = getItemPage(match);
var matchTxt = "";
try { matchTxt = match.contents; } catch (eT) {}
appendLog("Reviewing % Spacing (" + (state.percentSpaceIndex + 1) + " of " + matches.length + ") on " + (page ? "Page " + page.name : "Pasteboard") + ': "' + matchTxt + '"');
state.percentSpaceIndex = (state.percentSpaceIndex + 1) % matches.length;
}
} catch (e) {
appendLog("Error navigating to percentage issue: " + e.message);
} finally {
app.findGrepPreferences = NothingEnum.nothing;
}
}

function fixCurrentPercentSpace() {
var doc = getActiveDocument();
if (!doc) return;

app.doScript(function () {
var target = null;

// 1. Check if user already highlighted a percentage issue (via Find)
if (app.selection.length > 0 && app.selection[0].hasOwnProperty("contents")) {
var sel = app.selection[0];
try {
// Safety check: Selection MUST match number + optional space + %
if (typeof sel.contents === "string" && /(\d+(?:[.,]\d+)?)[ \t]*%/.test(sel.contents)) {
target = sel;
}
} catch (eSel) {}
}

// 2. If not selected or wasn't a percentage, query the live document fresh
if (!target) {
app.findGrepPreferences = NothingEnum.nothing;
app.findGrepPreferences.findWhat = "(\\d+(?:[.,]\\d+)?)[ \\t]*%";
try {
var freshMatches = doc.findGrep();
if (freshMatches.length > 0) {
target = freshMatches[0];
}
} catch (eGrep) {} finally {
app.findGrepPreferences = NothingEnum.nothing;
}
}

if (!target || !target.isValid) {
appendLog("No percentage spacing issue found to fix.");
lblPercentCount.text = "0";
return;
}

var page = getItemPage(target);
var pageName = page ? "Page " + page.name : "Pasteboard";
var origText = target.contents;

// Replace with number + non-breaking space (\u00A0) + %
var num = origText.replace(/[ \t]*%$/, "");
target.contents = num + "\u00A0%";
appendLog('Fixed % spacing on ' + pageName + ': "' + origText + '" -> "' + num + ' %" (non-breaking space).');

// 3. Highlight and zoom to the next percentage issue immediately
app.findGrepPreferences = NothingEnum.nothing;
app.findGrepPreferences.findWhat = "(\\d+(?:[.,]\\d+)?)[ \\t]*%";
try {
var remaining = doc.findGrep();
lblPercentCount.text = "" + remaining.length;
if (remaining.length > 0) {
var nextMatch = remaining[0];
app.select(nextMatch, SelectionOptions.REPLACE_WITH);
zoomToSelectedTarget(nextMatch);
var nextPage = getItemPage(nextMatch);
appendLog("Next % spacing issue highlighted on " + (nextPage ? "Page " + nextPage.name : "Pasteboard") + ". Remaining: " + remaining.length);
} else {
lblPercentCount.text = "0";
appendLog("All percentage spacing issues resolved!");
}
} catch (eRem) {} finally {
app.findGrepPreferences = NothingEnum.nothing;
}
}, ScriptLanguage.JAVASCRIPT, undefined, UndoModes.ENTIRE_SCRIPT, "QC Fix % Spacing");
}

function fixAllPercentSpaces(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return;

if (!confirm("Are you sure you want to add a non-breaking space before all percentage signs (%) across the entire document?\n(Example: 5% -> 5 %, 10% -> 10 %)", false, "Confirm Fix All % Spacing")) {
return;
}

app.doScript(function () {
app.findGrepPreferences = NothingEnum.nothing;
app.changeGrepPreferences = NothingEnum.nothing;
app.findGrepPreferences.findWhat = "(\\d+(?:[.,]\\d+)?)[ \\t]*%";
app.changeGrepPreferences.changeTo = "$1~s%";

try {
var changed = doc.changeGrep();
appendLog("Added non-breaking space to " + changed.length + " percentage occurrence(s).");
scanPercentSpaces(doc);
} catch (e) {
appendLog("Error fixing % spacing: " + e.message);
} finally {
app.findGrepPreferences = NothingEnum.nothing;
app.changeGrepPreferences = NothingEnum.nothing;
}
}, ScriptLanguage.JAVASCRIPT, undefined, UndoModes.ENTIRE_SCRIPT, "QC Fix All % Spacing");
}

// -------------------------------------------------------------
// Zoom Helper (Maintains user's current zoom level like native Find/Change)
// -------------------------------------------------------------
function zoomToSelectedTarget(target) {
try {
var loc = getItemLocation(target);
var curZoom = 100;
try {
curZoom = app.activeWindow.zoomPercentage;
} catch (eZ) {}

// 1. Switch active spread if target is on a different spread
if (loc.spread && loc.spread.isValid) {
if (app.activeWindow.activeSpread !== loc.spread) {
app.activeWindow.activeSpread = loc.spread;
}
}

// 2. Set active page on that spread
if (loc.page && loc.page.isValid) {
app.activeWindow.activePage = loc.page;
}

// 3. Center viewport on the selected item while keeping current zoom level
try {
var act = null;
try { act = app.menuActions.itemByName("$ID/Fit Selection in Window"); } catch (e1) {}
if (!act || !act.isValid) {
try { act = app.menuActions.itemByName("Fit Selection in Window"); } catch (e2) {}
}
if (act && act.isValid && act.enabled) {
act.invoke();
}
} catch (eFit) {}

// 4. Restore the user's exact zoom level (e.g. 150%, 200%, etc.)
// so it never forces "fit page" or "fit frame", behaving exactly like native Find/Change!
if (curZoom && curZoom > 0) {
app.activeWindow.zoomPercentage = curZoom;
}
} catch (e) {}
}

// -------------------------------------------------------------
// Full QC Runner & Pristine Reset
// -------------------------------------------------------------
function pristineReset() {
state.doubleSpaces = [];
state.doubleSpaceIndex = 0;
state.widows = [];
state.widowIndex = 0;
state.percentSpaces = [];
state.percentSpaceIndex = 0;
state.percentSpaceCount = 0;
state.imageFramesWithFill = [];
state.imageFrameIndex = 0;
state.blackIssues = [];
state.blackIssueIndex = 0;
state.disallowedUrls = [];
state.disallowedUrlIndex = 0;
state.hyphenations = [];
state.hyphenationIndex = 0;
state.ligatures = [];
state.ligatureIndex = 0;
state.unusedSwatches = [];
state.unusedSwatchesCount = 0;

lblUnusedCount.text = "0";
lblDoubleSpaceCount.text = "0";
lblWidowCount.text = "0";
lblPercentCount.text = "0";
lblImageFrameCount.text = "0";
lblBlackColorCount.text = "0";
lblUrlCount.text = "0";
lblHyphenCount.text = "0";
lblLigatureCount.text = "0";

// Reset all checkboxes to ON by default
try {
chkDouble.value = true;
chkWidow.value = true;
chkHyphen.value = true;
chkLigature.value = true;
chkPercent.value = true;
chkSwatches.value = true;
chkBlack.value = true;
chkImgFill.value = true;
chkUrls.value = true;
} catch (eChk) {}

var doc = getActiveDocument();
lblDocName.text = "Document: " + (doc ? doc.name : "None");
lblStatus.text = "Status: Ready";

try {
app.select(NothingEnum.nothing);
} catch (e) {}

reportBox.text = "QC Master Dashboard ready.\nClick 'Run Full QC' or individual [Scan] buttons to begin.\n";
}

function runFullQC() {
var doc = getActiveDocument();
if (!doc) {
alert("Please open an InDesign document before running QC.", "No Document");
return;
}

lblDocName.text = "Document: " + doc.name;
lblStatus.text = "Status: Running...";

var prevRedraw = app.scriptPreferences.enableRedraw;
try {
// Speed optimization: suppress screen redraws during multi-page audits (50+ pages)
app.scriptPreferences.enableRedraw = false;

var cSwatches = 0;
var cDouble = 0;
var cWidows = 0;
var cPercent = 0;
var cImgFill = 0;
var cBlack = 0;
var cUrls = 0;
var cHyphens = 0;
var cLigatures = 0;
var activeChecks = 0;

if (chkSwatches.value) {
addUnnamedColors(doc);
cSwatches = scanUnusedSwatches(doc);
activeChecks++;
}
if (chkDouble.value) {
cDouble = scanDoubleSpaces(doc);
activeChecks++;
}
if (chkWidow.value) {
cWidows = scanWidows(doc);
activeChecks++;
}
if (chkPercent.value) {
cPercent = scanPercentSpaces(doc);
activeChecks++;
}
if (chkHyphen.value) {
cHyphens = scanHyphenation(doc);
activeChecks++;
}
if (chkLigature.value) {
cLigatures = scanLigatures(doc);
activeChecks++;
}
if (chkBlack.value) {
cBlack = scanBlackColorIssues(doc);
activeChecks++;
}
if (chkImgFill.value) {
cImgFill = scanImageFramesWithFill(doc);
activeChecks++;
}
if (chkUrls.value) {
cUrls = scanDisallowedUrls(doc);
activeChecks++;
}

if (activeChecks === 0) {
lblStatus.text = "Status: No checks selected";
appendLog("No QC checks are selected. Please check at least one box.");
alert("No QC checks are selected. Please check at least one box.", "No Checks Selected");
return;
}

var total = cSwatches + cDouble + cWidows + cPercent + cImgFill + cBlack + cUrls + cHyphens + cLigatures;
lblStatus.text = "Status: Complete (" + total + " issues in " + activeChecks + " checks)";

// Post summary header at the top
appendLog("=========================================");
appendLog("QC Complete for: " + doc.name + " (" + total + " total issues in " + activeChecks + " active checks)");
appendLog("=========================================");
} catch (eQC) {
lblStatus.text = "Status: Error";
appendLog("Error during Full QC: " + eQC.message);
} finally {
app.scriptPreferences.enableRedraw = prevRedraw;
}
}

function refreshQC() {
var doc = getActiveDocument();
if (!doc) {
lblDocName.text = "Document: None";
lblStatus.text = "Status: No document open";
return;
}
lblDocName.text = "Document: " + doc.name;
lblStatus.text = "Status: Ready";
runFullQC();
}

function exportReport() {
var reportContent = reportBox.text;
if (!reportContent || reportContent.length === 0) {
alert("The report log is empty. Run QC before exporting.", "Empty Log");
return;
}

var doc = getActiveDocument();
var defaultPath = "~/Desktop";
if (doc && doc.saved) {
defaultPath = doc.filePath;
}

var fileName = (doc ? doc.name.replace(/\.indd$/i, "") : "Document") + "_QC_Report.txt";
var file = new File(defaultPath + "/" + fileName).saveDlg("Save QC Audit Report", "Text File:*.txt");
if (file) {
var exportHeader = "========================================================================\n" +
"ADOBE INDESIGN PREPRESS AUDIT REPORT\n" +
"Tool: QC Master Dashboard v2.4\n" +
"Attribution: " + _0xauth + "\n" +
"Timestamp: " + new Date().toString() + "\n" +
"========================================================================\n\n";
file.encoding = "UTF-8";
file.open("w");
file.write(exportHeader + reportContent);
file.close();
appendLog("Report successfully saved to: " + file.fsName);
}
}

// -------------------------------------------------------------
// UI Construction (Compact 2-Column Layout)
// -------------------------------------------------------------
if ($.global.qcmaster_win && $.global.qcmaster_win instanceof Window) {
try { $.global.qcmaster_win.close(); } catch (eW) {}
}
var win = new Window("palette", "QC Master Dashboard v2.5 | Prepress Audit & Fix Hub", undefined, { resizeable: true });
$.global.qcmaster_win = win;
win.orientation = "column";
win.alignChildren = ["fill", "top"];
win.margins = 10;
win.spacing = 6;

// --- Compact Header: Document Information & Actions ---
var pnlHeader = win.add("panel", undefined, "Document & Actions");
pnlHeader.orientation = "column";
pnlHeader.alignChildren = ["fill", "top"];
pnlHeader.margins = 8;
pnlHeader.spacing = 6;

var headerRow1 = pnlHeader.add("group");
headerRow1.orientation = "row";
headerRow1.alignChildren = ["left", "center"];
headerRow1.spacing = 16;

var initialDoc = getActiveDocument();
var docNameText = initialDoc ? initialDoc.name : "No active document";
var lblDocName = headerRow1.add("statictext", undefined, "Document: " + docNameText);
lblDocName.preferredSize.width = 340;
var lblStatus = headerRow1.add("statictext", undefined, "Status: Ready");
lblStatus.preferredSize.width = 250;

var headerRow2 = pnlHeader.add("group");
headerRow2.orientation = "row";
headerRow2.alignChildren = ["left", "center"];
headerRow2.spacing = 6;

var btnRunQC = headerRow2.add("button", undefined, "Run Full QC");
btnRunQC.preferredSize = [100, 24];

var btnReset = headerRow2.add("button", undefined, "Reset");
btnReset.preferredSize = [55, 24];

var btnRefresh = headerRow2.add("button", undefined, "Refresh");
btnRefresh.preferredSize = [65, 24];

var btnSelectAll = headerRow2.add("button", undefined, "Check All");
btnSelectAll.preferredSize = [75, 24];

var btnDeselectAll = headerRow2.add("button", undefined, "Uncheck All");
btnDeselectAll.preferredSize = [85, 24];

var btnExport = headerRow2.add("button", undefined, "Export Report");
btnExport.preferredSize = [95, 24];

// --- Main 2-Column Container ---
var mainCols = win.add("group");
mainCols.orientation = "row";
mainCols.alignChildren = ["fill", "top"];
mainCols.spacing = 8;

// =============================================================
// COLUMN 1: Text & Typography
// =============================================================
var col1 = mainCols.add("panel", undefined, "Typography & Text");
col1.orientation = "column";
col1.alignChildren = ["fill", "top"];
col1.preferredSize.width = 360;
col1.margins = 8;
col1.spacing = 6;

// Row 1.1: Double Spaces
var rowDouble = col1.add("group");
rowDouble.orientation = "row";
rowDouble.alignChildren = ["left", "center"];
rowDouble.spacing = 3;
var chkDouble = rowDouble.add("checkbox", undefined, "Double Space:");
chkDouble.value = true;
chkDouble.preferredSize.width = 132;
var lblDoubleSpaceCount = rowDouble.add("statictext", undefined, "0");
lblDoubleSpaceCount.preferredSize.width = 24;
var btnScanDouble = rowDouble.add("button", undefined, "Scan");
btnScanDouble.preferredSize = [38, 21];
var btnReviewDoubleSpace = rowDouble.add("button", undefined, "Find");
btnReviewDoubleSpace.preferredSize = [38, 21];
var btnFixCurrentDoubleSpace = rowDouble.add("button", undefined, "Fix");
btnFixCurrentDoubleSpace.preferredSize = [32, 21];
var btnFixDoubleSpaces = rowDouble.add("button", undefined, "All");
btnFixDoubleSpaces.preferredSize = [32, 21];

// Row 1.2: Widow Check (Scan | Find | Fix | All)
var rowWidow = col1.add("group");
rowWidow.orientation = "row";
rowWidow.alignChildren = ["left", "center"];
rowWidow.spacing = 3;
var chkWidow = rowWidow.add("checkbox", undefined, "Widows:");
chkWidow.value = true;
chkWidow.preferredSize.width = 132;
var lblWidowCount = rowWidow.add("statictext", undefined, "0");
lblWidowCount.preferredSize.width = 24;
var btnScanWidow = rowWidow.add("button", undefined, "Scan");
btnScanWidow.preferredSize = [38, 21];
var btnReviewWidow = rowWidow.add("button", undefined, "Find");
btnReviewWidow.preferredSize = [38, 21];
var btnFixCurrentWidow = rowWidow.add("button", undefined, "Fix");
btnFixCurrentWidow.preferredSize = [32, 21];
var btnFixAllWidow = rowWidow.add("button", undefined, "All");
btnFixAllWidow.preferredSize = [32, 21];

// Row 1.3: Hyphenated Lines & Uncheck Option
var rowHyphen = col1.add("group");
rowHyphen.orientation = "row";
rowHyphen.alignChildren = ["left", "center"];
rowHyphen.spacing = 3;
var chkHyphen = rowHyphen.add("checkbox", undefined, "Hyphenation:");
chkHyphen.value = true;
chkHyphen.preferredSize.width = 132;
var lblHyphenCount = rowHyphen.add("statictext", undefined, "0");
lblHyphenCount.preferredSize.width = 24;
var btnScanHyphen = rowHyphen.add("button", undefined, "Scan");
btnScanHyphen.preferredSize = [38, 21];
var btnReviewHyphen = rowHyphen.add("button", undefined, "Find");
btnReviewHyphen.preferredSize = [38, 21];
var btnUncheckHyphen = rowHyphen.add("button", undefined, "Uncheck");
btnUncheckHyphen.preferredSize = [56, 21];

// Row 1.4: Ligatures Used & Fix
var rowLigature = col1.add("group");
rowLigature.orientation = "row";
rowLigature.alignChildren = ["left", "center"];
rowLigature.spacing = 3;
var chkLigature = rowLigature.add("checkbox", undefined, "Ligatures:");
chkLigature.value = true;
chkLigature.preferredSize.width = 132;
var lblLigatureCount = rowLigature.add("statictext", undefined, "0");
lblLigatureCount.preferredSize.width = 24;
var btnScanLigature = rowLigature.add("button", undefined, "Scan");
btnScanLigature.preferredSize = [38, 21];
var btnReviewLigature = rowLigature.add("button", undefined, "Find");
btnReviewLigature.preferredSize = [38, 21];
var btnFixLigature = rowLigature.add("button", undefined, "Off");
btnFixLigature.preferredSize = [32, 21];
var btnAllLigature = rowLigature.add("button", undefined, "All");
btnAllLigature.preferredSize = [32, 21];

// Row 1.5: Space Before % (Spanish & French Prepress Standards: Scan | Find | Fix | All)
var rowPercent = col1.add("group");
rowPercent.orientation = "row";
rowPercent.alignChildren = ["left", "center"];
rowPercent.spacing = 3;
var chkPercent = rowPercent.add("checkbox", undefined, "Space Before %:");
chkPercent.value = true;
chkPercent.preferredSize.width = 132;
var lblPercentCount = rowPercent.add("statictext", undefined, "0");
lblPercentCount.preferredSize.width = 24;
var btnScanPercent = rowPercent.add("button", undefined, "Scan");
btnScanPercent.preferredSize = [38, 21];
var btnReviewPercent = rowPercent.add("button", undefined, "Find");
btnReviewPercent.preferredSize = [38, 21];
var btnFixCurrentPercent = rowPercent.add("button", undefined, "Fix");
btnFixCurrentPercent.preferredSize = [32, 21];
var btnFixAllPercent = rowPercent.add("button", undefined, "All");
btnFixAllPercent.preferredSize = [32, 21];

// =============================================================
// COLUMN 2: Colors, Assets & Links
// =============================================================
var col2 = mainCols.add("panel", undefined, "Colors, Frames & Links");
col2.orientation = "column";
col2.alignChildren = ["fill", "top"];
col2.preferredSize.width = 360;
col2.margins = 8;
col2.spacing = 8;

// Row 2.1: Unused Swatches
var rowSwatches = col2.add("group");
rowSwatches.orientation = "row";
rowSwatches.alignChildren = ["left", "center"];
rowSwatches.spacing = 3;
var chkSwatches = rowSwatches.add("checkbox", undefined, "Swatches:");
chkSwatches.value = true;
chkSwatches.preferredSize.width = 105;
var lblUnusedCount = rowSwatches.add("statictext", undefined, "0");
lblUnusedCount.preferredSize.width = 24;
var btnScanSwatches = rowSwatches.add("button", undefined, "Scan");
btnScanSwatches.preferredSize = [38, 21];
var btnRemoveSwatches = rowSwatches.add("button", undefined, "Remove All");
btnRemoveSwatches.preferredSize = [80, 21];

// Row 2.2: Black Color Issues (Strict 100% Solid & 80% Tint Only; No 4-Color Black)
var rowBlack = col2.add("group");
rowBlack.orientation = "row";
rowBlack.alignChildren = ["left", "center"];
rowBlack.spacing = 3;
var chkBlack = rowBlack.add("checkbox", undefined, "Black Issues:");
chkBlack.value = true;
chkBlack.preferredSize.width = 100;
var lblBlackColorCount = rowBlack.add("statictext", undefined, "0");
lblBlackColorCount.preferredSize.width = 20;
var btnScanBlack = rowBlack.add("button", undefined, "Scan");
btnScanBlack.preferredSize = [35, 21];
var btnReviewBlack = rowBlack.add("button", undefined, "Find");
btnReviewBlack.preferredSize = [35, 21];
var btnFix80Black = rowBlack.add("button", undefined, "FIX80");
btnFix80Black.preferredSize = [42, 21];
var btnFix100Black = rowBlack.add("button", undefined, "FIX100");
btnFix100Black.preferredSize = [46, 21];
var btnFixAllBlack = rowBlack.add("button", undefined, "All");
btnFixAllBlack.preferredSize = [28, 21];

// Row 2.3: Image Frames with Fill (Find | Clear)
var rowImgFill = col2.add("group");
rowImgFill.orientation = "row";
rowImgFill.alignChildren = ["left", "center"];
rowImgFill.spacing = 3;
var chkImgFill = rowImgFill.add("checkbox", undefined, "Img Fill Color:");
chkImgFill.value = true;
chkImgFill.preferredSize.width = 105;
var lblImageFrameCount = rowImgFill.add("statictext", undefined, "0");
lblImageFrameCount.preferredSize.width = 24;
var btnScanImgFill = rowImgFill.add("button", undefined, "Scan");
btnScanImgFill.preferredSize = [38, 21];
var btnReviewImageFrame = rowImgFill.add("button", undefined, "Find");
btnReviewImageFrame.preferredSize = [38, 21];
var btnClearImgFill = rowImgFill.add("button", undefined, "Clear");
btnClearImgFill.preferredSize = [40, 21];

// Row 2.4: Disallowed URLs (.in / com/in)
var rowUrls = col2.add("group");
rowUrls.orientation = "row";
rowUrls.alignChildren = ["left", "center"];
rowUrls.spacing = 3;
var chkUrls = rowUrls.add("checkbox", undefined, "URLs (.in):");
chkUrls.value = true;
chkUrls.preferredSize.width = 105;
var lblUrlCount = rowUrls.add("statictext", undefined, "0");
lblUrlCount.preferredSize.width = 24;
var btnScanUrls = rowUrls.add("button", undefined, "Scan");
btnScanUrls.preferredSize = [38, 21];
var btnReviewUrl = rowUrls.add("button", undefined, "Find");
btnReviewUrl.preferredSize = [38, 21];

// --- Panel: QC Report Log ---
var pnlReport = win.add("panel", undefined, "QC Report");
pnlReport.alignChildren = ["fill", "fill"];
pnlReport.margins = 6;

var reportBox = pnlReport.add("edittext", undefined, "QC Master Dashboard ready.\nClick 'Run Full QC' or individual [Scan] buttons to begin.\n", {
multiline: true,
readonly: true,
scrollable: true
});
reportBox.preferredSize = [720, 85];

// --- Footer: Credits & Buttons ---
var footerGroup = win.add("group");
footerGroup.orientation = "row";
footerGroup.alignChildren = ["fill", "center"];
footerGroup.alignment = ["fill", "bottom"];

var lblCredits = footerGroup.add("statictext", undefined, _0xauth);
lblCredits.preferredSize.width = 525;

var btnClearLog = footerGroup.add("button", undefined, "Clear Log");
btnClearLog.preferredSize = [75, 23];
var btnClose = footerGroup.add("button", undefined, "Close");
btnClose.preferredSize = [65, 23];

// -------------------------------------------------------------
// Event Listeners
// -------------------------------------------------------------
btnRunQC.onClick = function () { runFullQC(); };
btnReset.onClick = function () { pristineReset(); };
btnRefresh.onClick = function () { refreshQC(); };
btnExport.onClick = function () { exportReport(); };

// Selection Toggle Listeners
btnSelectAll.onClick = function () {
chkDouble.value = true;
chkWidow.value = true;
chkHyphen.value = true;
chkLigature.value = true;
chkPercent.value = true;
chkSwatches.value = true;
chkBlack.value = true;
chkImgFill.value = true;
chkUrls.value = true;
};

btnDeselectAll.onClick = function () {
chkDouble.value = false;
chkWidow.value = false;
chkHyphen.value = false;
chkLigature.value = false;
chkPercent.value = false;
chkSwatches.value = false;
chkBlack.value = false;
chkImgFill.value = false;
chkUrls.value = false;
};

// Individual Scan Listeners
btnScanDouble.onClick = function () { scanDoubleSpaces(); };
btnScanWidow.onClick = function () { scanWidows(); };
btnScanHyphen.onClick = function () { scanHyphenation(); };
btnScanLigature.onClick = function () { scanLigatures(); };
btnScanPercent.onClick = function () { scanPercentSpaces(); };
btnScanSwatches.onClick = function () { scanUnusedSwatches(); };
btnScanBlack.onClick = function () { scanBlackColorIssues(); };
btnScanImgFill.onClick = function () { scanImageFramesWithFill(); };
btnScanUrls.onClick = function () { scanDisallowedUrls(); };

// Action Listeners
btnReviewDoubleSpace.onClick = function () { reviewDoubleSpace(); };
btnFixCurrentDoubleSpace.onClick = function () { fixCurrentDoubleSpace(); };
btnFixDoubleSpaces.onClick = function () { fixAllDoubleSpaces(); };

btnReviewWidow.onClick = function () { reviewWidow(); };
btnFixCurrentWidow.onClick = function () { fixCurrentWidow(); };
btnFixAllWidow.onClick = function () { fixAllWidows(); };

btnReviewHyphen.onClick = function () { reviewHyphenation(); };
btnUncheckHyphen.onClick = function () { uncheckAllHyphenation(); };

btnReviewLigature.onClick = function () { reviewLigature(); };
btnFixLigature.onClick = function () { fixCurrentLigature(); };
btnAllLigature.onClick = function () { fixAllLigatures(); };

btnReviewPercent.onClick = function () { reviewPercentSpace(); };
btnFixCurrentPercent.onClick = function () { fixCurrentPercentSpace(); };
btnFixAllPercent.onClick = function () { fixAllPercentSpaces(); };

btnRemoveSwatches.onClick = function () { removeAllUnusedSwatches(); };
btnReviewBlack.onClick = function () { reviewBlackColorIssue(); };
btnFix80Black.onClick = function () { fixCurrentBlackIssue(80); };
btnFix100Black.onClick = function () { fixCurrentBlackIssue(100); };
btnFixAllBlack.onClick = function () { fixAllBlackIssues(); };

btnReviewImageFrame.onClick = function () { reviewImageFrame(); };
btnClearImgFill.onClick = function () { clearCurrentImgFill(); };

btnReviewUrl.onClick = function () { reviewDisallowedUrl(); };

btnClearLog.onClick = function () { reportBox.text = ""; };
btnClose.onClick = function () {
try { win.close(); } catch (e) {}
$.global.qcmaster_win = null;
};

// Secondary Anti-Tamper Verification: Ensure UI footer matches authenticated metadata
if (lblCredits.text !== _0xauth) {
alert("SECURITY ALERT:\nUI copyright tampering detected.\n\nExecution has been terminated.", "Security Violation");
exit();
}

// Show modeless palette
win.center();
win.show();
})();

 

Translate
NousheethAuthor
Known Participant
Nov 4, 2026 1:20 PM UTC
#target indesign
#targetengine "qcmaster_engine"

/**
* =========================================================================
* QC Master Dashboard v2.5 | Prepress Audit & Fix Hub
* Print Production Preflight & Audit Script for Adobe InDesign
* Compatible with InDesign CC 2022 through CC 2025+
* © 2026 Developed by GCoE Creative Studio | Contact at Nousheeth_iqbal@ajg.com
* Master Source Code: 100% Unencrypted with Cryptographic Anti-Tamper Shield
* =========================================================================
*/

(function () {
// -----------------------------------------------------------------
// 1. Intellectual Property & Anti-Tamper Attribution Shield
// -----------------------------------------------------------------
// Cryptographic integrity check: Enforces registered copyright metadata.
// If the attribution string is modified, stripped, or reassigned, the
// hash validation fails and script execution immediately terminates.
var _0xauth = "\u00A9 2026 Developed by GCoE Creative Studio | Contact at Nousheeth_iqbal@ajg.com";
var _0xcalc = 0;
for (var _0xi = 0; _0xi < _0xauth.length; _0xi++) {
_0xcalc = ((_0xcalc << 5) - _0xcalc) + _0xauth.charCodeAt(_0xi);
_0xcalc |= 0;
}
if (_0xcalc !== 712451348) {
alert("SECURITY ALERT:\nUnauthorized modification or copyright tampering detected.\n\nThis intellectual property is registered to:\nGCoE Creative Studio (Contact: Nousheeth_iqbal@ajg.com).\n\nExecution has been terminated.", "Security Violation");
exit();
}

// -------------------------------------------------------------
// Global Storage for Issue Navigation
// -------------------------------------------------------------
var state = {
doc: null,
doubleSpaces: [],
doubleSpaceIndex: 0,
widows: [],
widowIndex: 0,
percentSpaces: [],
percentSpaceIndex: 0,
percentSpaceCount: 0,
imageFramesWithFill: [],
imageFrameIndex: 0,
blackIssues: [],
blackIssueIndex: 0,
disallowedUrls: [],
disallowedUrlIndex: 0,
hyphenations: [],
hyphenationIndex: 0,
ligatures: [],
ligatureIndex: 0,
unusedSwatches: [],
unusedSwatchesCount: 0
};

function getActiveDocument() {
if (app.documents.length === 0) {
return null;
}
return app.activeDocument;
}

// -------------------------------------------------------------
// Helper: Logging (Newest / Most Recent Entry at the Top)
// -------------------------------------------------------------
function appendLog(txt) {
var time = new Date();
var timeStr = "[" + pad(time.getHours()) + ":" + pad(time.getMinutes()) + ":" + pad(time.getSeconds()) + "] ";
var newEntry = timeStr + txt + "\n";
var currentText = reportBox.text;

if (currentText.length > 25000) {
currentText = currentText.substring(0, 20000);
}

reportBox.text = newEntry + currentText;
}

function pad(n) {
return (n < 10 ? "0" : "") + n;
}

function getItemLocation(target) {
var page = null;
var spread = null;
if (!target) return { page: null, spread: null };

try {
// Case 1: Text target (Paragraph, Line, Word, Character, TextStyleRange, Match)
var tf = null;
try {
if (target.parentTextFrames && target.parentTextFrames.length > 0) {
tf = target.parentTextFrames[0];
}
} catch (eTf) {}

var cur = tf ? tf : target;

// Direct parentPage check
try {
if (cur.parentPage && cur.parentPage.isValid) {
page = cur.parentPage;
}
} catch (ePp) {}

// Ascend hierarchy to find Page and Spread
var node = cur;
while (node) {
try {
if (!page && node.constructor.name === "Page") {
page = node;
}
if (node.constructor.name === "Spread") {
spread = node;
break;
}
if (node.constructor.name === "Document") {
break;
}
} catch (eNode) {}
try {
node = node.parent;
} catch (ePar) {
break;
}
}

// Derive spread from page if needed
if (page && page.isValid && (!spread || !spread.isValid)) {
try {
if (page.parent && page.parent.constructor.name === "Spread") {
spread = page.parent;
}
} catch (eSp) {}
}
} catch (e) {}

return { page: page, spread: spread };
}

function getItemPage(target) {
return getItemLocation(target).page;
}

// -------------------------------------------------------------
// Helper: Automatically Add Unnamed Colors to Swatches Panel
// -------------------------------------------------------------
function addUnnamedColors(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return 0;

try {
if (app.documents.length > 0 && app.activeDocument !== doc) {
app.activeDocument = doc;
}
} catch (eDoc) {}

var beforeCount = doc.swatches.length;
try {
var act = null;
try {
act = app.menuActions.itemByName("$ID/Add Unnamed Colors");
} catch (e1) {}

if (!act || !act.isValid) {
try {
act = app.menuActions.itemByName("Add Unnamed Colors");
} catch (e2) {}
}

if (act && act.isValid && act.enabled) {
act.invoke();
}
} catch (e) {}

var afterCount = doc.swatches.length;
var added = afterCount - beforeCount;
if (added > 0) {
appendLog("Added " + added + " unnamed color(s) to Swatches panel.");
}
return added;
}

// -------------------------------------------------------------
// QC Check 1: Unused Swatches (Auto-adds unnamed colors first)
// -------------------------------------------------------------
function scanUnusedSwatches(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return 0;

// Auto-add any unnamed colors applied to text/objects into the Swatches panel
addUnnamedColors(doc);

state.unusedSwatches = [];
var swatchNames = [];

try {
var rawUnused = doc.unusedSwatches;
var rawUnusedMap = {};
for (var u = 0; u < rawUnused.length; u++) {
try {
rawUnusedMap[rawUnused[u].name] = true;
} catch (eU) {}
}

var panelSwatches = doc.swatches;
for (var i = 0; i < panelSwatches.length; i++) {
var sw = panelSwatches[i];
var swName = sw.name;

// Protect default system swatches
if (swName === "None" || swName === "Registration" || swName === "Black" || swName === "Paper" ||
swName === "[None]" || swName === "[Registration]" || swName === "[Black]" || swName === "[Paper]") {
continue;
}

if (rawUnusedMap[swName]) {
state.unusedSwatches.push(sw);
swatchNames.push(swName);
}
}
} catch (e) {
appendLog("Error scanning unused swatches: " + e.message);
}

state.unusedSwatchesCount = state.unusedSwatches.length;
lblUnusedCount.text = "" + state.unusedSwatchesCount;

if (state.unusedSwatchesCount > 0) {
appendLog("Unused Swatches in Panel (" + state.unusedSwatchesCount + "): " + swatchNames.join(", "));
} else {
appendLog("Swatches: 0 unused swatches in Swatches panel.");
}

return state.unusedSwatchesCount;
}

function removeAllUnusedSwatches(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return;

if (state.unusedSwatches.length === 0) {
scanUnusedSwatches(doc);
}
if (state.unusedSwatches.length === 0) {
appendLog("No unused swatches to remove.");
return;
}

if (!confirm("Are you sure you want to remove all " + state.unusedSwatches.length + " unused swatch(es) from the Swatches panel?", false, "Confirm Remove Unused Swatches")) {
return;
}

app.doScript(function () {
var count = 0;
try {
for (var i = state.unusedSwatches.length - 1; i >= 0; i--) {
var sw = state.unusedSwatches[i];
try {
if (sw && sw.isValid) {
sw.remove();
count++;
}
} catch (err) {}
}
appendLog("Removed " + count + " unused swatches from Swatches panel.");
scanUnusedSwatches(doc);
} catch (e) {
appendLog("Error removing swatches: " + e.message);
}
}, ScriptLanguage.JAVASCRIPT, undefined, UndoModes.ENTIRE_SCRIPT, "QC Remove Unused Swatches");
}

// -------------------------------------------------------------
// QC Check 2: Double Spaces (Scan | Review | Fix | All)
// -------------------------------------------------------------
function scanDoubleSpaces(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return 0;

state.doubleSpaces = [];
state.doubleSpaceIndex = 0;

app.findGrepPreferences = NothingEnum.nothing;
app.changeGrepPreferences = NothingEnum.nothing;
app.findGrepPreferences.findWhat = "\\x{0020}{2,}";

var count = 0;
try {
var matches = doc.findGrep();
count = matches.length;
} catch (e) {
appendLog("Error scanning double spaces: " + e.message);
} finally {
app.findGrepPreferences = NothingEnum.nothing;
app.changeGrepPreferences = NothingEnum.nothing;
}

lblDoubleSpaceCount.text = "" + count;
appendLog("Double Spaces: " + count + " found.");
return count;
}

function reviewDoubleSpace() {
var doc = getActiveDocument();
if (!doc) return;

app.findGrepPreferences = NothingEnum.nothing;
app.findGrepPreferences.findWhat = "\\x{0020}{2,}";

try {
var matches = doc.findGrep();
lblDoubleSpaceCount.text = "" + matches.length;

if (matches.length === 0) {
appendLog("No double spaces found in document.");
return;
}

if (state.doubleSpaceIndex >= matches.length) {
state.doubleSpaceIndex = 0;
}

var match = matches[state.doubleSpaceIndex];
if (match && match.isValid) {
app.select(match, SelectionOptions.REPLACE_WITH);
zoomToSelectedTarget(match);
var page = getItemPage(match);
appendLog("Reviewing Double Space (" + (state.doubleSpaceIndex + 1) + " of " + matches.length + ") on " + (page ? "Page " + page.name : "Pasteboard"));
state.doubleSpaceIndex = (state.doubleSpaceIndex + 1) % matches.length;
}
} catch (e) {
appendLog("Error navigating to double space: " + e.message);
} finally {
app.findGrepPreferences = NothingEnum.nothing;
}
}

function fixCurrentDoubleSpace() {
var doc = getActiveDocument();
if (!doc) return;

app.doScript(function () {
var target = null;

// 1. Check if user already highlighted a double space (via Find)
if (app.selection.length > 0 && app.selection[0].hasOwnProperty("contents")) {
var sel = app.selection[0];
try {
// Strict safety check: Selection MUST consist solely of 2 or more space characters!
// This 100% prevents any adjacent letters from ever being touched or deleted.
if (typeof sel.contents === "string" && sel.contents.length >= 2 && /^ +$/.test(sel.contents)) {
target = sel;
}
} catch (eSel) {}
}

// 2. If not selected or selection wasn't pure spaces, query the live document fresh
if (!target) {
app.findGrepPreferences = NothingEnum.nothing;
app.findGrepPreferences.findWhat = "\\x{0020}{2,}";
try {
var freshMatches = doc.findGrep();
if (freshMatches.length > 0) {
target = freshMatches[0];
}
} catch (eGrep) {} finally {
app.findGrepPreferences = NothingEnum.nothing;
}
}

if (!target || !target.isValid) {
appendLog("No double space found to fix.");
lblDoubleSpaceCount.text = "0";
return;
}

var page = getItemPage(target);
var pageName = page ? "Page " + page.name : "Pasteboard";

// Replace ONLY the verified space characters with a single space
target.contents = " ";
appendLog("Fixed 1 double space on " + pageName + ".");

// 3. Highlight and zoom to the next double space immediately (like native Change/Find)
app.findGrepPreferences = NothingEnum.nothing;
app.findGrepPreferences.findWhat = "\\x{0020}{2,}";
try {
var remaining = doc.findGrep();
lblDoubleSpaceCount.text = "" + remaining.length;
if (remaining.length > 0) {
var nextMatch = remaining[0];
app.select(nextMatch, SelectionOptions.REPLACE_WITH);
zoomToSelectedTarget(nextMatch);
var nextPage = getItemPage(nextMatch);
appendLog("Next Double Space highlighted on " + (nextPage ? "Page " + nextPage.name : "Pasteboard") + ". Remaining: " + remaining.length);
} else {
lblDoubleSpaceCount.text = "0";
appendLog("All double spaces resolved!");
}
} catch (eRem) {} finally {
app.findGrepPreferences = NothingEnum.nothing;
}
}, ScriptLanguage.JAVASCRIPT, undefined, UndoModes.ENTIRE_SCRIPT, "QC Fix Double Space");
}

function fixAllDoubleSpaces(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return;

if (!confirm("Are you sure you want to replace all double spaces across the entire document?", false, "Confirm Fix All Double Spaces")) {
return;
}

app.doScript(function () {
app.findGrepPreferences = NothingEnum.nothing;
app.changeGrepPreferences = NothingEnum.nothing;
app.findGrepPreferences.findWhat = "\\x{0020}{2,}";
app.changeGrepPreferences.changeTo = " ";

try {
var changed = doc.changeGrep();
appendLog("Replaced " + changed.length + " double space occurrences with single space.");
scanDoubleSpaces(doc);
} catch (e) {
appendLog("Error fixing double spaces: " + e.message);
} finally {
app.findGrepPreferences = NothingEnum.nothing;
app.changeGrepPreferences = NothingEnum.nothing;
}
}, ScriptLanguage.JAVASCRIPT, undefined, UndoModes.ENTIRE_SCRIPT, "QC Fix All Double Spaces");
}

// -------------------------------------------------------------
// QC Check 3: Widow Check (Excludes 1-word/bullet paragraphs)
// -------------------------------------------------------------
function scanWidows(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return 0;

state.widows = [];
state.widowIndex = 0;

try {
var stories = doc.stories;
for (var s = 0; s < stories.length; s++) {
var story = stories[s];
var paras = story.paragraphs;
for (var p = 0; p < paras.length; p++) {
var para = paras[p];

// Multi-line paragraph check: must have at least 2 lines and at least 3 words total
// (Excludes 1-word bullets and 2-word icon descriptions like "Icon\nLabel")
if (para.lines.length >= 2 && para.words.length >= 3) {
// Skip if it is a bullet/numbered list item with < 4 words
if (para.bulletsAndNumberingListType !== ListType.NO_LIST && para.words.length < 4) {
continue;
}

var lastLine = para.lines.lastItem();
var lastLineClean = lastLine.contents.replace(/[\r\n\s\t]+$/, "");
var wordsInLastLine = lastLine.words;

// Check the line before the last line
var prevLine = para.lines.item(para.lines.length - 2);

// If the line was broken using a manual Forced Line Break (Shift + Enter / Soft Return),
// this is an intentional manual line break, NOT an automated typographic widow!
if (/[\u000A\n]/.test(prevLine.contents)) {
continue;
}

// A widow occurs if the last line has only 1 word, or is a very short stub (<= 8 chars)
if (wordsInLastLine.length === 1 || (lastLineClean.length > 0 && lastLineClean.length <= 8)) {
state.widows.push(para);
}
}
}
}
} catch (e) {
appendLog("Error scanning widows: " + e.message);
}

lblWidowCount.text = "" + state.widows.length;
appendLog("Widows: " + state.widows.length + " found.");
return state.widows.length;
}

function reviewWidow() {
if (state.widows.length === 0) {
appendLog("No widows to review.");
return;
}
if (state.widowIndex >= state.widows.length) {
state.widowIndex = 0;
}

var para = state.widows[state.widowIndex];
try {
if (para.isValid) {
var lastLine = para.lines.lastItem();
app.select(lastLine, SelectionOptions.REPLACE_WITH);
zoomToSelectedTarget(lastLine);
var page = getItemPage(para);
appendLog("Reviewing Widow (" + (state.widowIndex + 1) + " of " + state.widows.length + ") on " + (page ? "Page " + page.name : "Pasteboard") + ": \"" + lastLine.contents.replace(/[\r\n]+$/, "") + "\"");
state.widowIndex++;
} else {
state.widowIndex++;
reviewWidow();
}
} catch (e) {
appendLog("Error selecting widow: " + e.message);
}
}

function fixCurrentWidow() {
if (state.widows.length === 0) {
appendLog("No widow found to fix.");
return;
}

var targetIdx = state.widowIndex > 0 ? (state.widowIndex - 1) : 0;
if (targetIdx >= state.widows.length) {
targetIdx = 0;
}

var para = state.widows[targetIdx];
try {
if (para && para.isValid && para.words.length >= 2) {
var words = para.words;
var lastWord = words.lastItem();
var wordBeforeLast = words.item(words.length - 2);

var story = para.parentStory;
var lastCharOfPrev = wordBeforeLast.characters.lastItem();
var firstCharOfLast = lastWord.characters.firstItem();

var startIdx = lastCharOfPrev.index + 1;
var endIdx = firstCharOfLast.index - 1;

if (startIdx <= endIdx) {
var spaceRange = story.characters.itemByRange(startIdx, endIdx);
if (spaceRange.isValid) {
spaceRange.contents = SpecialCharacters.NONBREAKING_SPACE;
}
} else {
wordBeforeLast.insertionPoints.lastItem().contents = SpecialCharacters.NONBREAKING_SPACE;
}

var page = getItemPage(para);
state.widows.splice(targetIdx, 1);
lblWidowCount.text = "" + state.widows.length;
appendLog("Fixed 1 widow on " + (page ? "Page " + page.name : "Pasteboard") + ". Remaining: " + state.widows.length);

if (state.widows.length > 0) {
if (targetIdx >= state.widows.length) {
state.widowIndex = 0;
} else {
state.widowIndex = targetIdx;
}
reviewWidow();
} else {
lblStatus.text = "Status: Widows Cleared";
}
} else {
state.widows.splice(targetIdx, 1);
lblWidowCount.text = "" + state.widows.length;
if (state.widows.length > 0) reviewWidow();
}
} catch (e) {
appendLog("Error fixing widow: " + e.message);
}
}

function fixAllWidows(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return;

if (state.widows.length === 0) {
scanWidows(doc);
}
if (state.widows.length === 0) {
appendLog("No widows to fix.");
return;
}

if (!confirm("Are you sure you want to fix all " + state.widows.length + " detected widow(s) across the document?", false, "Confirm Fix All Widows")) {
return;
}

app.doScript(function () {
var fixedCount = 0;
var prevRedraw = app.scriptPreferences.enableRedraw;

try {
app.scriptPreferences.enableRedraw = false;
for (var i = 0; i < state.widows.length; i++) {
var para = state.widows[i];
try {
if (para && para.isValid && para.words.length >= 2) {
var words = para.words;
var lastWord = words.lastItem();
var wordBeforeLast = words.item(words.length - 2);

var story = para.parentStory;
var lastCharOfPrev = wordBeforeLast.characters.lastItem();
var firstCharOfLast = lastWord.characters.firstItem();

var startIdx = lastCharOfPrev.index + 1;
var endIdx = firstCharOfLast.index - 1;

if (startIdx <= endIdx) {
var spaceRange = story.characters.itemByRange(startIdx, endIdx);
if (spaceRange.isValid) {
spaceRange.contents = SpecialCharacters.NONBREAKING_SPACE;
fixedCount++;
}
} else {
wordBeforeLast.insertionPoints.lastItem().contents = SpecialCharacters.NONBREAKING_SPACE;
fixedCount++;
}
}
} catch (eOne) {}
}
} catch (eAll) {
appendLog("Error fixing widows: " + eAll.message);
} finally {
app.scriptPreferences.enableRedraw = prevRedraw;
}

appendLog("Inserted non-breaking space on " + fixedCount + " paragraph(s) to eliminate widows.");
scanWidows(doc);
}, ScriptLanguage.JAVASCRIPT, undefined, UndoModes.ENTIRE_SCRIPT, "QC Fix All Widows");
}

function fixWidows(doc) {
fixAllWidows(doc);
}

// -------------------------------------------------------------
// QC Check 4: Image Frames with Fill Color (Safe DOM check)
// -------------------------------------------------------------
function scanImageFramesWithFill(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return 0;

state.imageFramesWithFill = [];
state.imageFrameIndex = 0;

try {
var noneSwatch = doc.swatches.itemByName("None");
var seen = {};

var graphics = doc.allGraphics;
for (var i = 0; i < graphics.length; i++) {
var g = graphics[i];
var frame = g.parent;
if (frame && frame.isValid) {
var frameId = "" + frame.id;
if (!seen[frameId]) {
try {
var fc = frame.fillColor;
if (fc && fc.isValid) {
var fcName = fc.name;
if (fcName !== "None" && fcName !== "[None]" && fcName !== "" && fc !== noneSwatch) {
var fTint = -1;
try { fTint = frame.fillTint; } catch (errT) {}
if (fTint === -1 || fTint > 0) {
seen[frameId] = true;
state.imageFramesWithFill.push({
frame: frame,
fillName: fcName,
tint: fTint
});
}
}
}
} catch (errF) {}
}
}
}

var allItems = doc.allPageItems;
for (var j = 0; j < allItems.length; j++) {
var item = allItems[j];
try {
var hasG = false;
try {
if (item.allGraphics && item.allGraphics.length > 0) hasG = true;
else if (item.graphics && item.graphics.length > 0) hasG = true;
} catch (eG) {}

if (hasG) {
var itemId = "" + item.id;
if (!seen[itemId]) {
var ifc = item.fillColor;
if (ifc && ifc.isValid) {
var ifcName = ifc.name;
if (ifcName !== "None" && ifcName !== "[None]" && ifcName !== "" && ifc !== noneSwatch) {
var itint = -1;
try { itint = item.fillTint; } catch (eT) {}
if (itint === -1 || itint > 0) {
seen[itemId] = true;
state.imageFramesWithFill.push({
frame: item,
fillName: ifcName,
tint: itint
});
}
}
}
}
}
} catch (eItem) {}
}
} catch (e) {
appendLog("Error checking image frames: " + e.message);
}

lblImageFrameCount.text = "" + state.imageFramesWithFill.length;
appendLog("Image Frames with Fill Color: " + state.imageFramesWithFill.length + " found.");
return state.imageFramesWithFill.length;
}

function reviewImageFrame() {
if (state.imageFramesWithFill.length === 0) {
appendLog("No image frames with fill color found.");
return;
}
if (state.imageFrameIndex >= state.imageFramesWithFill.length) {
state.imageFrameIndex = 0;
}

var item = state.imageFramesWithFill[state.imageFrameIndex];
var frame = item.frame;
try {
if (frame && frame.isValid) {
app.select(frame, SelectionOptions.REPLACE_WITH);
zoomToSelectedTarget(frame);
var page = getItemPage(frame);
appendLog("Reviewing Image Frame (" + (state.imageFrameIndex + 1) + " of " + state.imageFramesWithFill.length + ") on " + (page ? "Page " + page.name : "Pasteboard") + " [Fill: " + item.fillName + "]");
state.imageFrameIndex++;
} else {
state.imageFrameIndex++;
reviewImageFrame();
}
} catch (e) {
appendLog("Error selecting frame: " + e.message);
}
}

function clearCurrentImgFill() {
if (state.imageFramesWithFill.length === 0) {
appendLog("No image frame fill to clear.");
return;
}

var targetIdx = state.imageFrameIndex > 0 ? (state.imageFrameIndex - 1) : 0;
if (targetIdx >= state.imageFramesWithFill.length) targetIdx = 0;

var item = state.imageFramesWithFill[targetIdx];
var frame = item.frame;
try {
if (frame && frame.isValid) {
var doc = getActiveDocument();
frame.fillColor = doc.swatches.itemByName("None");
var page = getItemPage(frame);
state.imageFramesWithFill.splice(targetIdx, 1);
lblImageFrameCount.text = "" + state.imageFramesWithFill.length;
appendLog("Cleared fill color on Image Frame (" + (page ? "Page " + page.name : "Pasteboard") + "). Remaining: " + state.imageFramesWithFill.length);

if (state.imageFramesWithFill.length > 0) {
if (targetIdx >= state.imageFramesWithFill.length) state.imageFrameIndex = 0;
else state.imageFrameIndex = targetIdx;
reviewImageFrame();
} else {
state.imageFrameIndex = 0;
appendLog("All image frame fill colors cleared!");
}
} else {
state.imageFramesWithFill.splice(targetIdx, 1);
lblImageFrameCount.text = "" + state.imageFramesWithFill.length;
clearCurrentImgFill();
}
} catch (e) {
appendLog("Error clearing frame fill: " + e.message);
}
}

// -------------------------------------------------------------
// QC Check 5: Black Color Issues (Strict Tint 80% or 100% Solid Only; No Rich Black)
// -------------------------------------------------------------
function getCmykString(swatch) {
try {
if (swatch && swatch.isValid && swatch.space === ColorSpace.CMYK) {
var cv = swatch.colorValue;
if (cv && cv.length >= 4) {
return "C=" + Math.round(cv[0]) + " M=" + Math.round(cv[1]) + " Y=" + Math.round(cv[2]) + " K=" + Math.round(cv[3]);
}
}
} catch (e) {}
return "";
}

function isRichBlack(swatch) {
if (!swatch || !swatch.isValid) return false;
var name = "";
try { name = (swatch.name || "").toLowerCase(); } catch (eN) {}

if (name === "[none]" || name === "none" ||
name === "[paper]" || name === "paper") {
return false;
}

// Explicitly named rich blacks / 4-color blacks / registration
if (name === "[registration]" || name === "registration" ||
name.indexOf("rich black") !== -1 || name.indexOf("4 color black") !== -1 ||
name.indexOf("4-color black") !== -1 || name.indexOf("4-colour black") !== -1 ||
name.indexOf("cool black") !== -1 || name.indexOf("warm black") !== -1 ||
name.indexOf("super black") !== -1) {
return true;
}

try {
if (swatch.space === ColorSpace.CMYK) {
var cv = swatch.colorValue;
if (cv && cv.length >= 4) {
var c = cv[0], m = cv[1], y = cv[2], k = cv[3];

// Pure 1-color black (C=0, M=0, Y=0) is handled separately
if (c === 0 && m === 0 && y === 0) {
return false;
}

// 4-Color Black:
// 1) Black channel is dominant (K >= 85%) and process inks are present (C+M+Y >= 15%)
// e.g. C=91 M=86 Y=49 K=100 (from company standard), C=60 M=40 Y=40 K=100, etc.
// 2) OR K >= 98% and any process ink (C+M+Y >= 5%) is present
if ((k >= 85 && (c + m + y) >= 15) || (k >= 98 && (c + m + y) >= 5)) {
return true;
}
}
}
} catch (eCv) {}
return false;
}

function isBlackColor(swatch) {
if (!swatch || !swatch.isValid) return false;
var name = "";
try { name = swatch.name; } catch (eN) {}
if (name === "Black" || name === "[Black]") {
return true;
}
if (isRichBlack(swatch)) {
return true;
}
try {
if (swatch.space === ColorSpace.CMYK) {
var cv = swatch.colorValue;
if (cv && cv.length >= 4) {
// Pure K (1-color black: C=0, M=0, Y=0, and K>0)
if (cv[0] === 0 && cv[1] === 0 && cv[2] === 0 && cv[3] > 0) {
return true;
}
}
}
} catch (e) {}
return false;
}

function getEffectiveTint(swatch, appliedTint) {
var baseK = 100;
try {
if (swatch.space === ColorSpace.CMYK) {
var cv = swatch.colorValue;
if (cv && cv[0] === 0 && cv[1] === 0 && cv[2] === 0) {
baseK = cv[3];
}
}
} catch (e) {}
if (appliedTint === -1) {
return Math.round(baseK);
}
return Math.round((baseK * appliedTint) / 100);
}

function isApprovedBlack(swatch, appliedTint) {
if (!swatch || !swatch.isValid) return false;

// Rich Black (4-color black) is strictly NOT allowed!
if (isRichBlack(swatch)) {
return false;
}

var effK = getEffectiveTint(swatch, appliedTint);
// As per standard: ONLY 100% Solid Black and 80% Black Tint are approved!
// 70%, 75%, 85%, 90% (or any other tint) are strictly NOT acceptable.
return (effK === 100 || effK === 80);
}

function scanBlackColorIssues(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return 0;

// Auto-add unnamed colors so unnamed tints of black get identified as swatches
addUnnamedColors(doc);

state.blackIssues = [];
state.blackIssueIndex = 0;

try {
// 1. Text scans
var stories = doc.stories;
for (var s = 0; s < stories.length; s++) {
var story = stories[s];
var ranges = story.textStyleRanges;
for (var r = 0; r < ranges.length; r++) {
var range = ranges[r];
if (range.contents.replace(/[\r\n\s\t]+/g, "").length === 0) {
continue;
}

try {
var fc = range.fillColor;
if (fc && fc.isValid && isBlackColor(fc)) {
if (!isApprovedBlack(fc, range.fillTint)) {
var isRich = isRichBlack(fc);
var cmykInfo = isRich ? getCmykString(fc) : "";
var effK = isRich ? "4-Color" : getEffectiveTint(fc, range.fillTint);
var sampleText = range.contents;
if (sampleText.length > 20) {
sampleText = sampleText.substring(0, 18) + "...";
}
var descText = isRich ?
("Text Fill 4-Color Black (" + (cmykInfo ? cmykInfo + " " : "") + "- Not Allowed) [\"" + sampleText.replace(/[\r\n]+/g, " ") + "\"]") :
("Text Fill Black " + effK + "% (Approved: 100% or 80% only) [\"" + sampleText.replace(/[\r\n]+/g, " ") + "\"]");

state.blackIssues.push({
target: range,
type: "textFill",
effK: isRich ? 100 : effK,
isRich: isRich,
desc: descText
});
}
}
} catch (eF) {}

try {
if (range.strokeWeight > 0) {
var sc = range.strokeColor;
if (sc && sc.isValid && isBlackColor(sc)) {
if (!isApprovedBlack(sc, range.strokeTint)) {
var isRichS = isRichBlack(sc);
var cmykInfoS = isRichS ? getCmykString(sc) : "";
var effStrokeK = isRichS ? "4-Color" : getEffectiveTint(sc, range.strokeTint);
var descStroke = isRichS ?
("Text Stroke 4-Color Black (" + (cmykInfoS ? cmykInfoS + " " : "") + "- Not Allowed)") :
("Text Stroke Black " + effStrokeK + "% (Approved: 100% or 80% only)");

state.blackIssues.push({
target: range,
type: "textStroke",
effK: isRichS ? 100 : effStrokeK,
isRich: isRichS,
desc: descStroke
});
}
}
}
} catch (eS) {}
}
}

// 2. PageItems scans
var pageItems = doc.allPageItems;
for (var i = 0; i < pageItems.length; i++) {
var item = pageItems[i];

try {
var ifc = item.fillColor;
if (ifc && ifc.isValid && isBlackColor(ifc)) {
var ift = -1;
try { ift = item.fillTint; } catch (eT) {}
if (!isApprovedBlack(ifc, ift)) {
var isRichIF = isRichBlack(ifc);
var cmykInfoIF = isRichIF ? getCmykString(ifc) : "";
var effItemK = isRichIF ? "4-Color" : getEffectiveTint(ifc, ift);
var descIF = isRichIF ?
(item.constructor.name + " Fill 4-Color Black (" + (cmykInfoIF ? cmykInfoIF + " " : "") + "- Not Allowed)") :
(item.constructor.name + " Fill Black " + effItemK + "% (Approved: 100% or 80% only)");

state.blackIssues.push({
target: item,
type: "itemFill",
effK: isRichIF ? 100 : effItemK,
isRich: isRichIF,
desc: descIF
});
}
}
} catch (eIF) {}

try {
var isw = 0;
try { isw = item.strokeWeight; } catch (eSW) {}
if (isw > 0) {
var isc = item.strokeColor;
if (isc && isc.isValid && isBlackColor(isc)) {
var ist = -1;
try { ist = item.strokeTint; } catch (eST) {}
if (!isApprovedBlack(isc, ist)) {
var isRichIS = isRichBlack(isc);
var cmykInfoIS = isRichIS ? getCmykString(isc) : "";
var effItemStrokeK = isRichIS ? "4-Color" : getEffectiveTint(isc, ist);
var descIS = isRichIS ?
(item.constructor.name + " Stroke 4-Color Black (" + (cmykInfoIS ? cmykInfoIS + " " : "") + "- Not Allowed)") :
(item.constructor.name + " Stroke Black " + effItemStrokeK + "% (Approved: 100% or 80% only)");

state.blackIssues.push({
target: item,
type: "itemStroke",
effK: isRichIS ? 100 : effItemStrokeK,
isRich: isRichIS,
desc: descIS
});
}
}
}
} catch (eIS) {}
}
} catch (e) {
appendLog("Error scanning black color issues: " + e.message);
}

lblBlackColorCount.text = "" + state.blackIssues.length;
appendLog("Black Color Issues: " + state.blackIssues.length + " found.");
return state.blackIssues.length;
}

function reviewBlackColorIssue() {
if (state.blackIssues.length === 0) {
appendLog("No black color issues to review.");
return;
}
if (state.blackIssueIndex >= state.blackIssues.length) {
state.blackIssueIndex = 0;
}

var issue = state.blackIssues[state.blackIssueIndex];
try {
if (issue.target.isValid) {
app.select(issue.target, SelectionOptions.REPLACE_WITH);
zoomToSelectedTarget(issue.target);
var page = getItemPage(issue.target);
var pageName = page ? "Page " + page.name : "Pasteboard";
appendLog("Reviewing Black Issue (" + (state.blackIssueIndex + 1) + " of " + state.blackIssues.length + ") on " + pageName + ": " + issue.desc);
state.blackIssueIndex++;
} else {
state.blackIssueIndex++;
reviewBlackColorIssue();
}
} catch (e) {
appendLog("Error navigating to black issue: " + e.message);
}
}

function fixCurrentBlackIssue(targetTint) {
if (typeof targetTint !== "number") {
targetTint = 100;
}
if (state.blackIssues.length === 0) {
appendLog("No black color issue found to fix.");
return;
}

var targetIdx = state.blackIssueIndex > 0 ? (state.blackIssueIndex - 1) : 0;
if (targetIdx >= state.blackIssues.length) {
targetIdx = 0;
}

var issue = state.blackIssues[targetIdx];
var doc = getActiveDocument();
if (!doc) return;

var blackSwatch = doc.swatches.itemByName("[Black]");
if (!blackSwatch || !blackSwatch.isValid) {
blackSwatch = doc.swatches.itemByName("Black");
}

try {
if (issue.target && issue.target.isValid) {
if (issue.type === "textFill") {
issue.target.fillColor = blackSwatch;
issue.target.fillTint = targetTint;
} else if (issue.type === "textStroke") {
issue.target.strokeColor = blackSwatch;
issue.target.strokeTint = targetTint;
} else if (issue.type === "itemFill") {
issue.target.fillColor = blackSwatch;
try { issue.target.fillTint = targetTint; } catch (eT1) {}
} else if (issue.type === "itemStroke") {
issue.target.strokeColor = blackSwatch;
try { issue.target.strokeTint = targetTint; } catch (eT2) {}
}

var page = getItemPage(issue.target);
var pageName = page ? "Page " + page.name : "Pasteboard";
state.blackIssues.splice(targetIdx, 1);
lblBlackColorCount.text = "" + state.blackIssues.length;
appendLog("Fixed 1 Black issue to " + targetTint + "% pure Black on " + pageName + ". Remaining: " + state.blackIssues.length);

if (state.blackIssues.length > 0) {
if (targetIdx >= state.blackIssues.length) {
state.blackIssueIndex = 0;
} else {
state.blackIssueIndex = targetIdx;
}
reviewBlackColorIssue();
} else {
lblStatus.text = "Status: Black Issues Cleared";
}
} else {
state.blackIssues.splice(targetIdx, 1);
lblBlackColorCount.text = "" + state.blackIssues.length;
if (state.blackIssues.length > 0) reviewBlackColorIssue();
}
} catch (e) {
appendLog("Error fixing black issue: " + e.message);
}
}

function promptFixAllBlackChoice(count) {
var dlg = new Window("dialog", "Fix All Black Issues");
dlg.orientation = "column";
dlg.alignChildren = ["fill", "top"];
dlg.margins = 16;
dlg.spacing = 10;

var titleTxt = dlg.add("statictext", undefined, "Found " + count + " unapproved black color issue(s).");
titleTxt.graphics.font = ScriptUI.newFont(titleTxt.graphics.font.name, "Bold", 12);

var infoTxt = dlg.add("statictext", undefined, "Standard requires pure Black at either 80% Tint or 100% Solid.\nChoose which approved tint to convert ALL issues to:", { multiline: true });
infoTxt.preferredSize.width = 340;

var btnGroup = dlg.add("group");
btnGroup.orientation = "row";
btnGroup.alignment = ["center", "center"];
btnGroup.spacing = 8;

var choice = null;

var btn80 = btnGroup.add("button", undefined, "All to 80% Black");
btn80.preferredSize = [125, 26];
btn80.onClick = function () {
choice = 80;
dlg.close();
};

var btn100 = btnGroup.add("button", undefined, "All to 100% Black");
btn100.preferredSize = [135, 26];
btn100.onClick = function () {
choice = 100;
dlg.close();
};

var btnCancel = btnGroup.add("button", undefined, "Cancel", { name: "cancel" });
btnCancel.preferredSize = [70, 26];
btnCancel.onClick = function () {
choice = null;
dlg.close();
};

dlg.show();
return choice;
}

function fixAllBlackIssues(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return;

if (state.blackIssues.length === 0) {
scanBlackColorIssues(doc);
}
if (state.blackIssues.length === 0) {
appendLog("No black color issues to fix.");
return;
}

var chosenTint = promptFixAllBlackChoice(state.blackIssues.length);
if (chosenTint === null) {
appendLog("Fix All Black cancelled.");
return;
}

app.doScript(function () {
var blackSwatch = doc.swatches.itemByName("[Black]");
if (!blackSwatch || !blackSwatch.isValid) {
blackSwatch = doc.swatches.itemByName("Black");
}

var fixedCount = 0;
var prevRedraw = app.scriptPreferences.enableRedraw;
try {
app.scriptPreferences.enableRedraw = false;
for (var i = 0; i < state.blackIssues.length; i++) {
var issue = state.blackIssues[i];
try {
if (issue.target && issue.target.isValid) {
if (issue.type === "textFill") {
issue.target.fillColor = blackSwatch;
issue.target.fillTint = chosenTint;
fixedCount++;
} else if (issue.type === "textStroke") {
issue.target.strokeColor = blackSwatch;
issue.target.strokeTint = chosenTint;
fixedCount++;
} else if (issue.type === "itemFill") {
issue.target.fillColor = blackSwatch;
try { issue.target.fillTint = chosenTint; } catch (eT1) {}
fixedCount++;
} else if (issue.type === "itemStroke") {
issue.target.strokeColor = blackSwatch;
try { issue.target.strokeTint = chosenTint; } catch (eT2) {}
fixedCount++;
}
}
} catch (eOne) {}
}
appendLog("Fixed " + fixedCount + " black issue(s) across document to " + chosenTint + "% pure Black.");
scanBlackColorIssues(doc);
} catch (eAll) {
appendLog("Error fixing all black issues: " + eAll.message);
} finally {
app.scriptPreferences.enableRedraw = prevRedraw;
}
}, ScriptLanguage.JAVASCRIPT, undefined, UndoModes.ENTIRE_SCRIPT, "QC Fix All Black Issues to " + chosenTint + "%");
}

// -------------------------------------------------------------
// QC Check 6: Indian / Disallowed URLs (.in / com/in)
// -------------------------------------------------------------
function isDisallowedIndianUrl(url) {
if (!url) return false;
var cleanUrl = url.toLowerCase();
if (/(?:\.in|\.co\.in|\.org\.in|\.net\.in)(?:[\/\?#]|$)/i.test(cleanUrl)) {
return true;
}
if (/(?:com\/in|\/en-in|\/in)(?:[\/\?#]|$)/i.test(cleanUrl)) {
return true;
}
return false;
}

function scanDisallowedUrls(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return 0;

state.disallowedUrls = [];
state.disallowedUrlIndex = 0;

try {
var hyperlinks = doc.hyperlinks;
for (var h = 0; h < hyperlinks.length; h++) {
var hl = hyperlinks[h];
var dest = hl.destination;
if (dest && dest.hasOwnProperty("destinationURL")) {
var url = dest.destinationURL;
if (isDisallowedIndianUrl(url)) {
var sourceItem = null;
if (hl.source) {
if (hl.source.hasOwnProperty("sourceText")) {
sourceItem = hl.source.sourceText;
} else if (hl.source.hasOwnProperty("sourcePageItem")) {
sourceItem = hl.source.sourcePageItem;
}
}
state.disallowedUrls.push({
url: url,
target: sourceItem,
type: "Hyperlink Object"
});
}
}
}

app.findGrepPreferences = NothingEnum.nothing;
app.findGrepPreferences.findWhat = "(?i)\\b(?:https?:\\/\\/|www\\.)[^\\s<>'\"\\)\\]]+";

var textMatches = doc.findGrep();
for (var m = 0; m < textMatches.length; m++) {
var matchText = textMatches[m].contents;
if (isDisallowedIndianUrl(matchText)) {
state.disallowedUrls.push({
url: matchText,
target: textMatches[m],
type: "Text URL"
});
}
}
} catch (e) {
appendLog("Error scanning disallowed URLs: " + e.message);
} finally {
app.findGrepPreferences = NothingEnum.nothing;
}

lblUrlCount.text = "" + state.disallowedUrls.length;
appendLog("Disallowed URLs (.in): " + state.disallowedUrls.length + " found.");
return state.disallowedUrls.length;
}

function reviewDisallowedUrl() {
if (state.disallowedUrls.length === 0) {
appendLog("No disallowed URLs to review.");
return;
}
if (state.disallowedUrlIndex >= state.disallowedUrls.length) {
state.disallowedUrlIndex = 0;
}

var item = state.disallowedUrls[state.disallowedUrlIndex];
try {
if (item.target && item.target.isValid) {
app.select(item.target, SelectionOptions.REPLACE_WITH);
zoomToSelectedTarget(item.target);
var page = getItemPage(item.target);
var pageName = page ? "Page " + page.name : "Pasteboard";
appendLog("Reviewing URL (" + (state.disallowedUrlIndex + 1) + " of " + state.disallowedUrls.length + ") on " + pageName + " [" + item.type + "]: " + item.url);
state.disallowedUrlIndex++;
} else {
appendLog("URL (" + (state.disallowedUrlIndex + 1) + " of " + state.disallowedUrls.length + "): " + item.url);
state.disallowedUrlIndex++;
if (state.disallowedUrlIndex < state.disallowedUrls.length) {
reviewDisallowedUrl();
}
}
} catch (e) {
appendLog("Error navigating to disallowed URL: " + e.message);
}
}




V26 part1

 

Translate
NousheethAuthor
Known Participant
Nov 3, 2026 6:34 PM UTC

V2.5

v25.2

#target indesign
#targetengine "qcmaster_engine"

/**
* QC Master Dashboard v2.4
* Print Production Preflight & Audit Script for Adobe InDesign
* Compatible with InDesign CC 2022 through CC 2025+
* © 2026 Developed by GCoE Creative Studio | Contact at Nousheeth_iqbal@ajg.com
*/

(function () {
// -------------------------------------------------------------
// Global Storage for Issue Navigation
// -------------------------------------------------------------
var state = {
doc: null,
doubleSpaces: [],
doubleSpaceIndex: 0,
widows: [],
widowIndex: 0,
imageFramesWithFill: [],
imageFrameIndex: 0,
blackIssues: [],
blackIssueIndex: 0,
disallowedUrls: [],
disallowedUrlIndex: 0,
hyphenations: [],
hyphenationIndex: 0,
ligatures: [],
ligatureIndex: 0,
unusedSwatches: [],
unusedSwatchesCount: 0
};

function getActiveDocument() {
if (app.documents.length === 0) {
return null;
}
return app.activeDocument;
}

// -------------------------------------------------------------
// Helper: Logging (Newest / Most Recent Entry at the Top)
// -------------------------------------------------------------
function appendLog(txt) {
var time = new Date();
var timeStr = "[" + pad(time.getHours()) + ":" + pad(time.getMinutes()) + ":" + pad(time.getSeconds()) + "] ";
var newEntry = timeStr + txt + "\n";
var currentText = reportBox.text;

if (currentText.length > 25000) {
currentText = currentText.substring(0, 20000);
}

reportBox.text = newEntry + currentText;
}

function pad(n) {
return (n < 10 ? "0" : "") + n;
}

function getItemLocation(target) {
var page = null;
var spread = null;
if (!target) return { page: null, spread: null };

try {
// Case 1: Text target (Paragraph, Line, Word, Character, TextStyleRange, Match)
var tf = null;
try {
if (target.parentTextFrames && target.parentTextFrames.length > 0) {
tf = target.parentTextFrames[0];
}
} catch (eTf) {}

var cur = tf ? tf : target;

// Direct parentPage check
try {
if (cur.parentPage && cur.parentPage.isValid) {
page = cur.parentPage;
}
} catch (ePp) {}

// Ascend hierarchy to find Page and Spread
var node = cur;
while (node) {
try {
if (!page && node.constructor.name === "Page") {
page = node;
}
if (node.constructor.name === "Spread") {
spread = node;
break;
}
if (node.constructor.name === "Document") {
break;
}
} catch (eNode) {}
try {
node = node.parent;
} catch (ePar) {
break;
}
}

// Derive spread from page if needed
if (page && page.isValid && (!spread || !spread.isValid)) {
try {
if (page.parent && page.parent.constructor.name === "Spread") {
spread = page.parent;
}
} catch (eSp) {}
}
} catch (e) {}

return { page: page, spread: spread };
}

function getItemPage(target) {
return getItemLocation(target).page;
}

// -------------------------------------------------------------
// Helper: Automatically Add Unnamed Colors to Swatches Panel
// -------------------------------------------------------------
function addUnnamedColors(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return 0;

try {
if (app.documents.length > 0 && app.activeDocument !== doc) {
app.activeDocument = doc;
}
} catch (eDoc) {}

var beforeCount = doc.swatches.length;
try {
var act = null;
try {
act = app.menuActions.itemByName("$ID/Add Unnamed Colors");
} catch (e1) {}

if (!act || !act.isValid) {
try {
act = app.menuActions.itemByName("Add Unnamed Colors");
} catch (e2) {}
}

if (act && act.isValid && act.enabled) {
act.invoke();
}
} catch (e) {}

var afterCount = doc.swatches.length;
var added = afterCount - beforeCount;
if (added > 0) {
appendLog("Added " + added + " unnamed color(s) to Swatches panel.");
}
return added;
}

// -------------------------------------------------------------
// QC Check 1: Unused Swatches (Auto-adds unnamed colors first)
// -------------------------------------------------------------
function scanUnusedSwatches(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return 0;

// Auto-add any unnamed colors applied to text/objects into the Swatches panel
addUnnamedColors(doc);

state.unusedSwatches = [];
var swatchNames = [];

try {
var rawUnused = doc.unusedSwatches;
var rawUnusedMap = {};
for (var u = 0; u < rawUnused.length; u++) {
try {
rawUnusedMap[rawUnused[u].name] = true;
} catch (eU) {}
}

var panelSwatches = doc.swatches;
for (var i = 0; i < panelSwatches.length; i++) {
var sw = panelSwatches[i];
var swName = sw.name;

// Protect default system swatches
if (swName === "None" || swName === "Registration" || swName === "Black" || swName === "Paper" ||
swName === "[None]" || swName === "[Registration]" || swName === "[Black]" || swName === "[Paper]") {
continue;
}

if (rawUnusedMap[swName]) {
state.unusedSwatches.push(sw);
swatchNames.push(swName);
}
}
} catch (e) {
appendLog("Error scanning unused swatches: " + e.message);
}

state.unusedSwatchesCount = state.unusedSwatches.length;
lblUnusedCount.text = "" + state.unusedSwatchesCount;

if (state.unusedSwatchesCount > 0) {
appendLog("Unused Swatches in Panel (" + state.unusedSwatchesCount + "): " + swatchNames.join(", "));
} else {
appendLog("Swatches: 0 unused swatches in Swatches panel.");
}

return state.unusedSwatchesCount;
}

function removeAllUnusedSwatches(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return;

if (state.unusedSwatches.length === 0) {
scanUnusedSwatches(doc);
}
if (state.unusedSwatches.length === 0) {
appendLog("No unused swatches to remove.");
return;
}

if (!confirm("Are you sure you want to remove all " + state.unusedSwatches.length + " unused swatch(es) from the Swatches panel?", false, "Confirm Remove Unused Swatches")) {
return;
}

app.doScript(function () {
var count = 0;
try {
for (var i = state.unusedSwatches.length - 1; i >= 0; i--) {
var sw = state.unusedSwatches[i];
try {
if (sw && sw.isValid) {
sw.remove();
count++;
}
} catch (err) {}
}
appendLog("Removed " + count + " unused swatches from Swatches panel.");
scanUnusedSwatches(doc);
} catch (e) {
appendLog("Error removing swatches: " + e.message);
}
}, ScriptLanguage.JAVASCRIPT, undefined, UndoModes.ENTIRE_SCRIPT, "QC Remove Unused Swatches");
}

// -------------------------------------------------------------
// QC Check 2: Double Spaces (Scan | Review | Fix | All)
// -------------------------------------------------------------
function scanDoubleSpaces(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return 0;

state.doubleSpaces = [];
state.doubleSpaceIndex = 0;

app.findGrepPreferences = NothingEnum.nothing;
app.changeGrepPreferences = NothingEnum.nothing;
app.findGrepPreferences.findWhat = "\\x{0020}{2,}";

var count = 0;
try {
var matches = doc.findGrep();
count = matches.length;
} catch (e) {
appendLog("Error scanning double spaces: " + e.message);
} finally {
app.findGrepPreferences = NothingEnum.nothing;
app.changeGrepPreferences = NothingEnum.nothing;
}

lblDoubleSpaceCount.text = "" + count;
appendLog("Double Spaces: " + count + " found.");
return count;
}

function reviewDoubleSpace() {
var doc = getActiveDocument();
if (!doc) return;

app.findGrepPreferences = NothingEnum.nothing;
app.findGrepPreferences.findWhat = "\\x{0020}{2,}";

try {
var matches = doc.findGrep();
lblDoubleSpaceCount.text = "" + matches.length;

if (matches.length === 0) {
appendLog("No double spaces found in document.");
return;
}

if (state.doubleSpaceIndex >= matches.length) {
state.doubleSpaceIndex = 0;
}

var match = matches[state.doubleSpaceIndex];
if (match && match.isValid) {
app.select(match, SelectionOptions.REPLACE_WITH);
zoomToSelectedTarget(match);
var page = getItemPage(match);
appendLog("Reviewing Double Space (" + (state.doubleSpaceIndex + 1) + " of " + matches.length + ") on " + (page ? "Page " + page.name : "Pasteboard"));
state.doubleSpaceIndex = (state.doubleSpaceIndex + 1) % matches.length;
}
} catch (e) {
appendLog("Error navigating to double space: " + e.message);
} finally {
app.findGrepPreferences = NothingEnum.nothing;
}
}

function fixCurrentDoubleSpace() {
var doc = getActiveDocument();
if (!doc) return;

app.doScript(function () {
var target = null;

// 1. Check if user already highlighted a double space (via Find)
if (app.selection.length > 0 && app.selection[0].hasOwnProperty("contents")) {
var sel = app.selection[0];
try {
// Strict safety check: Selection MUST consist solely of 2 or more space characters!
// This 100% prevents any adjacent letters from ever being touched or deleted.
if (typeof sel.contents === "string" && sel.contents.length >= 2 && /^ +$/.test(sel.contents)) {
target = sel;
}
} catch (eSel) {}
}

// 2. If not selected or selection wasn't pure spaces, query the live document fresh
if (!target) {
app.findGrepPreferences = NothingEnum.nothing;
app.findGrepPreferences.findWhat = "\\x{0020}{2,}";
try {
var freshMatches = doc.findGrep();
if (freshMatches.length > 0) {
target = freshMatches[0];
}
} catch (eGrep) {} finally {
app.findGrepPreferences = NothingEnum.nothing;
}
}

if (!target || !target.isValid) {
appendLog("No double space found to fix.");
lblDoubleSpaceCount.text = "0";
return;
}

var page = getItemPage(target);
var pageName = page ? "Page " + page.name : "Pasteboard";

// Replace ONLY the verified space characters with a single space
target.contents = " ";
appendLog("Fixed 1 double space on " + pageName + ".");

// 3. Highlight and zoom to the next double space immediately (like native Change/Find)
app.findGrepPreferences = NothingEnum.nothing;
app.findGrepPreferences.findWhat = "\\x{0020}{2,}";
try {
var remaining = doc.findGrep();
lblDoubleSpaceCount.text = "" + remaining.length;
if (remaining.length > 0) {
var nextMatch = remaining[0];
app.select(nextMatch, SelectionOptions.REPLACE_WITH);
zoomToSelectedTarget(nextMatch);
var nextPage = getItemPage(nextMatch);
appendLog("Next Double Space highlighted on " + (nextPage ? "Page " + nextPage.name : "Pasteboard") + ". Remaining: " + remaining.length);
} else {
lblDoubleSpaceCount.text = "0";
appendLog("All double spaces resolved!");
}
} catch (eRem) {} finally {
app.findGrepPreferences = NothingEnum.nothing;
}
}, ScriptLanguage.JAVASCRIPT, undefined, UndoModes.ENTIRE_SCRIPT, "QC Fix Double Space");
}

function fixAllDoubleSpaces(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return;

if (!confirm("Are you sure you want to replace all double spaces across the entire document?", false, "Confirm Fix All Double Spaces")) {
return;
}

app.doScript(function () {
app.findGrepPreferences = NothingEnum.nothing;
app.changeGrepPreferences = NothingEnum.nothing;
app.findGrepPreferences.findWhat = "\\x{0020}{2,}";
app.changeGrepPreferences.changeTo = " ";

try {
var changed = doc.changeGrep();
appendLog("Replaced " + changed.length + " double space occurrences with single space.");
scanDoubleSpaces(doc);
} catch (e) {
appendLog("Error fixing double spaces: " + e.message);
} finally {
app.findGrepPreferences = NothingEnum.nothing;
app.changeGrepPreferences = NothingEnum.nothing;
}
}, ScriptLanguage.JAVASCRIPT, undefined, UndoModes.ENTIRE_SCRIPT, "QC Fix All Double Spaces");
}

// -------------------------------------------------------------
// QC Check 3: Widow Check (Excludes 1-word/bullet paragraphs)
// -------------------------------------------------------------
function scanWidows(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return 0;

state.widows = [];
state.widowIndex = 0;

try {
var stories = doc.stories;
for (var s = 0; s < stories.length; s++) {
var story = stories[s];
var paras = story.paragraphs;
for (var p = 0; p < paras.length; p++) {
var para = paras[p];

// Multi-line paragraph check: must have at least 2 lines and at least 3 words total
// (Excludes 1-word bullets and 2-word icon descriptions like "Icon\nLabel")
if (para.lines.length >= 2 && para.words.length >= 3) {
// Skip if it is a bullet/numbered list item with < 4 words
if (para.bulletsAndNumberingListType !== ListType.NO_LIST && para.words.length < 4) {
continue;
}

var lastLine = para.lines.lastItem();
var lastLineClean = lastLine.contents.replace(/[\r\n\s\t]+$/, "");
var wordsInLastLine = lastLine.words;

// Check the line before the last line
var prevLine = para.lines.item(para.lines.length - 2);

// If the line was broken using a manual Forced Line Break (Shift + Enter / Soft Return),
// this is an intentional manual line break, NOT an automated typographic widow!
if (/[\u000A\n]/.test(prevLine.contents)) {
continue;
}

// A widow occurs if the last line has only 1 word, or is a very short stub (<= 8 chars)
if (wordsInLastLine.length === 1 || (lastLineClean.length > 0 && lastLineClean.length <= 8)) {
state.widows.push(para);
}
}
}
}
} catch (e) {
appendLog("Error scanning widows: " + e.message);
}

lblWidowCount.text = "" + state.widows.length;
appendLog("Widows: " + state.widows.length + " found.");
return state.widows.length;
}

function reviewWidow() {
if (state.widows.length === 0) {
appendLog("No widows to review.");
return;
}
if (state.widowIndex >= state.widows.length) {
state.widowIndex = 0;
}

var para = state.widows[state.widowIndex];
try {
if (para.isValid) {
var lastLine = para.lines.lastItem();
app.select(lastLine, SelectionOptions.REPLACE_WITH);
zoomToSelectedTarget(lastLine);
var page = getItemPage(para);
appendLog("Reviewing Widow (" + (state.widowIndex + 1) + " of " + state.widows.length + ") on " + (page ? "Page " + page.name : "Pasteboard") + ": \"" + lastLine.contents.replace(/[\r\n]+$/, "") + "\"");
state.widowIndex++;
} else {
state.widowIndex++;
reviewWidow();
}
} catch (e) {
appendLog("Error selecting widow: " + e.message);
}
}

function fixCurrentWidow() {
if (state.widows.length === 0) {
appendLog("No widow found to fix.");
return;
}

var targetIdx = state.widowIndex > 0 ? (state.widowIndex - 1) : 0;
if (targetIdx >= state.widows.length) {
targetIdx = 0;
}

var para = state.widows[targetIdx];
try {
if (para && para.isValid && para.words.length >= 2) {
var words = para.words;
var lastWord = words.lastItem();
var wordBeforeLast = words.item(words.length - 2);

var story = para.parentStory;
var lastCharOfPrev = wordBeforeLast.characters.lastItem();
var firstCharOfLast = lastWord.characters.firstItem();

var startIdx = lastCharOfPrev.index + 1;
var endIdx = firstCharOfLast.index - 1;

if (startIdx <= endIdx) {
var spaceRange = story.characters.itemByRange(startIdx, endIdx);
if (spaceRange.isValid) {
spaceRange.contents = SpecialCharacters.NONBREAKING_SPACE;
}
} else {
wordBeforeLast.insertionPoints.lastItem().contents = SpecialCharacters.NONBREAKING_SPACE;
}

var page = getItemPage(para);
state.widows.splice(targetIdx, 1);
lblWidowCount.text = "" + state.widows.length;
appendLog("Fixed 1 widow on " + (page ? "Page " + page.name : "Pasteboard") + ". Remaining: " + state.widows.length);

if (state.widows.length > 0) {
if (targetIdx >= state.widows.length) {
state.widowIndex = 0;
} else {
state.widowIndex = targetIdx;
}
reviewWidow();
} else {
lblStatus.text = "Status: Widows Cleared";
}
} else {
state.widows.splice(targetIdx, 1);
lblWidowCount.text = "" + state.widows.length;
if (state.widows.length > 0) reviewWidow();
}
} catch (e) {
appendLog("Error fixing widow: " + e.message);
}
}

function fixAllWidows(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return;

if (state.widows.length === 0) {
scanWidows(doc);
}
if (state.widows.length === 0) {
appendLog("No widows to fix.");
return;
}

if (!confirm("Are you sure you want to fix all " + state.widows.length + " detected widow(s) across the document?", false, "Confirm Fix All Widows")) {
return;
}

app.doScript(function () {
var fixedCount = 0;
var prevRedraw = app.scriptPreferences.enableRedraw;

try {
app.scriptPreferences.enableRedraw = false;
for (var i = 0; i < state.widows.length; i++) {
var para = state.widows[i];
try {
if (para && para.isValid && para.words.length >= 2) {
var words = para.words;
var lastWord = words.lastItem();
var wordBeforeLast = words.item(words.length - 2);

var story = para.parentStory;
var lastCharOfPrev = wordBeforeLast.characters.lastItem();
var firstCharOfLast = lastWord.characters.firstItem();

var startIdx = lastCharOfPrev.index + 1;
var endIdx = firstCharOfLast.index - 1;

if (startIdx <= endIdx) {
var spaceRange = story.characters.itemByRange(startIdx, endIdx);
if (spaceRange.isValid) {
spaceRange.contents = SpecialCharacters.NONBREAKING_SPACE;
fixedCount++;
}
} else {
wordBeforeLast.insertionPoints.lastItem().contents = SpecialCharacters.NONBREAKING_SPACE;
fixedCount++;
}
}
} catch (eOne) {}
}
} catch (eAll) {
appendLog("Error fixing widows: " + eAll.message);
} finally {
app.scriptPreferences.enableRedraw = prevRedraw;
}

appendLog("Inserted non-breaking space on " + fixedCount + " paragraph(s) to eliminate widows.");
scanWidows(doc);
}, ScriptLanguage.JAVASCRIPT, undefined, UndoModes.ENTIRE_SCRIPT, "QC Fix All Widows");
}

function fixWidows(doc) {
fixAllWidows(doc);
}

// -------------------------------------------------------------
// QC Check 4: Image Frames with Fill Color (Safe DOM check)
// -------------------------------------------------------------
function scanImageFramesWithFill(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return 0;

state.imageFramesWithFill = [];
state.imageFrameIndex = 0;

try {
var noneSwatch = doc.swatches.itemByName("None");
var seen = {};

var graphics = doc.allGraphics;
for (var i = 0; i < graphics.length; i++) {
var g = graphics[i];
var frame = g.parent;
if (frame && frame.isValid) {
var frameId = "" + frame.id;
if (!seen[frameId]) {
try {
var fc = frame.fillColor;
if (fc && fc.isValid) {
var fcName = fc.name;
if (fcName !== "None" && fcName !== "[None]" && fcName !== "" && fc !== noneSwatch) {
var fTint = -1;
try { fTint = frame.fillTint; } catch (errT) {}
if (fTint === -1 || fTint > 0) {
seen[frameId] = true;
state.imageFramesWithFill.push({
frame: frame,
fillName: fcName,
tint: fTint
});
}
}
}
} catch (errF) {}
}
}
}

var allItems = doc.allPageItems;
for (var j = 0; j < allItems.length; j++) {
var item = allItems[j];
try {
var hasG = false;
try {
if (item.allGraphics && item.allGraphics.length > 0) hasG = true;
else if (item.graphics && item.graphics.length > 0) hasG = true;
} catch (eG) {}

if (hasG) {
var itemId = "" + item.id;
if (!seen[itemId]) {
var ifc = item.fillColor;
if (ifc && ifc.isValid) {
var ifcName = ifc.name;
if (ifcName !== "None" && ifcName !== "[None]" && ifcName !== "" && ifc !== noneSwatch) {
var itint = -1;
try { itint = item.fillTint; } catch (eT) {}
if (itint === -1 || itint > 0) {
seen[itemId] = true;
state.imageFramesWithFill.push({
frame: item,
fillName: ifcName,
tint: itint
});
}
}
}
}
}
} catch (eItem) {}
}
} catch (e) {
appendLog("Error checking image frames: " + e.message);
}

lblImageFrameCount.text = "" + state.imageFramesWithFill.length;
appendLog("Image Frames with Fill Color: " + state.imageFramesWithFill.length + " found.");
return state.imageFramesWithFill.length;
}

function reviewImageFrame() {
if (state.imageFramesWithFill.length === 0) {
appendLog("No image frames with fill color found.");
return;
}
if (state.imageFrameIndex >= state.imageFramesWithFill.length) {
state.imageFrameIndex = 0;
}

var item = state.imageFramesWithFill[state.imageFrameIndex];
var frame = item.frame;
try {
if (frame && frame.isValid) {
app.select(frame, SelectionOptions.REPLACE_WITH);
zoomToSelectedTarget(frame);
var page = getItemPage(frame);
appendLog("Reviewing Image Frame (" + (state.imageFrameIndex + 1) + " of " + state.imageFramesWithFill.length + ") on " + (page ? "Page " + page.name : "Pasteboard") + " [Fill: " + item.fillName + "]");
state.imageFrameIndex++;
} else {
state.imageFrameIndex++;
reviewImageFrame();
}
} catch (e) {
appendLog("Error selecting frame: " + e.message);
}
}

function clearCurrentImgFill() {
if (state.imageFramesWithFill.length === 0) {
appendLog("No image frame fill to clear.");
return;
}

var targetIdx = state.imageFrameIndex > 0 ? (state.imageFrameIndex - 1) : 0;
if (targetIdx >= state.imageFramesWithFill.length) targetIdx = 0;

var item = state.imageFramesWithFill[targetIdx];
var frame = item.frame;
try {
if (frame && frame.isValid) {
var doc = getActiveDocument();
frame.fillColor = doc.swatches.itemByName("None");
var page = getItemPage(frame);
state.imageFramesWithFill.splice(targetIdx, 1);
lblImageFrameCount.text = "" + state.imageFramesWithFill.length;
appendLog("Cleared fill color on Image Frame (" + (page ? "Page " + page.name : "Pasteboard") + "). Remaining: " + state.imageFramesWithFill.length);

if (state.imageFramesWithFill.length > 0) {
if (targetIdx >= state.imageFramesWithFill.length) state.imageFrameIndex = 0;
else state.imageFrameIndex = targetIdx;
reviewImageFrame();
} else {
state.imageFrameIndex = 0;
appendLog("All image frame fill colors cleared!");
}
} else {
state.imageFramesWithFill.splice(targetIdx, 1);
lblImageFrameCount.text = "" + state.imageFramesWithFill.length;
clearCurrentImgFill();
}
} catch (e) {
appendLog("Error clearing frame fill: " + e.message);
}
}

// -------------------------------------------------------------
// QC Check 5: Black Color Issues (Strict Tint 80% or 100% Solid Only; No Rich Black)
// -------------------------------------------------------------
function getCmykString(swatch) {
try {
if (swatch && swatch.isValid && swatch.space === ColorSpace.CMYK) {
var cv = swatch.colorValue;
if (cv && cv.length >= 4) {
return "C=" + Math.round(cv[0]) + " M=" + Math.round(cv[1]) + " Y=" + Math.round(cv[2]) + " K=" + Math.round(cv[3]);
}
}
} catch (e) {}
return "";
}

function isRichBlack(swatch) {
if (!swatch || !swatch.isValid) return false;
var name = "";
try { name = (swatch.name || "").toLowerCase(); } catch (eN) {}

if (name === "[none]" || name === "none" ||
name === "[paper]" || name === "paper") {
return false;
}

// Explicitly named rich blacks / 4-color blacks / registration
if (name === "[registration]" || name === "registration" ||
name.indexOf("rich black") !== -1 || name.indexOf("4 color black") !== -1 ||
name.indexOf("4-color black") !== -1 || name.indexOf("4-colour black") !== -1 ||
name.indexOf("cool black") !== -1 || name.indexOf("warm black") !== -1 ||
name.indexOf("super black") !== -1) {
return true;
}

try {
if (swatch.space === ColorSpace.CMYK) {
var cv = swatch.colorValue;
if (cv && cv.length >= 4) {
var c = cv[0], m = cv[1], y = cv[2], k = cv[3];

// Pure 1-color black (C=0, M=0, Y=0) is handled separately
if (c === 0 && m === 0 && y === 0) {
return false;
}

// 4-Color Black:
// 1) Black channel is dominant (K >= 85%) and process inks are present (C+M+Y >= 15%)
// e.g. C=91 M=86 Y=49 K=100 (from company standard), C=60 M=40 Y=40 K=100, etc.
// 2) OR K >= 98% and any process ink (C+M+Y >= 5%) is present
if ((k >= 85 && (c + m + y) >= 15) || (k >= 98 && (c + m + y) >= 5)) {
return true;
}
}
}
} catch (eCv) {}
return false;
}

function isBlackColor(swatch) {
if (!swatch || !swatch.isValid) return false;
var name = "";
try { name = swatch.name; } catch (eN) {}
if (name === "Black" || name === "[Black]") {
return true;
}
if (isRichBlack(swatch)) {
return true;
}
try {
if (swatch.space === ColorSpace.CMYK) {
var cv = swatch.colorValue;
if (cv && cv.length >= 4) {
// Pure K (1-color black: C=0, M=0, Y=0, and K>0)
if (cv[0] === 0 && cv[1] === 0 && cv[2] === 0 && cv[3] > 0) {
return true;
}
}
}
} catch (e) {}
return false;
}

function getEffectiveTint(swatch, appliedTint) {
var baseK = 100;
try {
if (swatch.space === ColorSpace.CMYK) {
var cv = swatch.colorValue;
if (cv && cv[0] === 0 && cv[1] === 0 && cv[2] === 0) {
baseK = cv[3];
}
}
} catch (e) {}
if (appliedTint === -1) {
return Math.round(baseK);
}
return Math.round((baseK * appliedTint) / 100);
}

function isApprovedBlack(swatch, appliedTint) {
if (!swatch || !swatch.isValid) return false;

// Rich Black (4-color black) is strictly NOT allowed!
if (isRichBlack(swatch)) {
return false;
}

var effK = getEffectiveTint(swatch, appliedTint);
// As per standard: ONLY 100% Solid Black and 80% Black Tint are approved!
// 70%, 75%, 85%, 90% (or any other tint) are strictly NOT acceptable.
return (effK === 100 || effK === 80);
}

function scanBlackColorIssues(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return 0;

// Auto-add unnamed colors so unnamed tints of black get identified as swatches
addUnnamedColors(doc);

state.blackIssues = [];
state.blackIssueIndex = 0;

try {
// 1. Text scans
var stories = doc.stories;
for (var s = 0; s < stories.length; s++) {
var story = stories[s];
var ranges = story.textStyleRanges;
for (var r = 0; r < ranges.length; r++) {
var range = ranges[r];
if (range.contents.replace(/[\r\n\s\t]+/g, "").length === 0) {
continue;
}

try {
var fc = range.fillColor;
if (fc && fc.isValid && isBlackColor(fc)) {
if (!isApprovedBlack(fc, range.fillTint)) {
var isRich = isRichBlack(fc);
var cmykInfo = isRich ? getCmykString(fc) : "";
var effK = isRich ? "4-Color" : getEffectiveTint(fc, range.fillTint);
var sampleText = range.contents;
if (sampleText.length > 20) {
sampleText = sampleText.substring(0, 18) + "...";
}
var descText = isRich ?
("Text Fill 4-Color Black (" + (cmykInfo ? cmykInfo + " " : "") + "- Not Allowed) [\"" + sampleText.replace(/[\r\n]+/g, " ") + "\"]") :
("Text Fill Black " + effK + "% (Approved: 100% or 80% only) [\"" + sampleText.replace(/[\r\n]+/g, " ") + "\"]");

state.blackIssues.push({
target: range,
type: "textFill",
effK: isRich ? 100 : effK,
isRich: isRich,
desc: descText
});
}
}
} catch (eF) {}

try {
if (range.strokeWeight > 0) {
var sc = range.strokeColor;
if (sc && sc.isValid && isBlackColor(sc)) {
if (!isApprovedBlack(sc, range.strokeTint)) {
var isRichS = isRichBlack(sc);
var cmykInfoS = isRichS ? getCmykString(sc) : "";
var effStrokeK = isRichS ? "4-Color" : getEffectiveTint(sc, range.strokeTint);
var descStroke = isRichS ?
("Text Stroke 4-Color Black (" + (cmykInfoS ? cmykInfoS + " " : "") + "- Not Allowed)") :
("Text Stroke Black " + effStrokeK + "% (Approved: 100% or 80% only)");

state.blackIssues.push({
target: range,
type: "textStroke",
effK: isRichS ? 100 : effStrokeK,
isRich: isRichS,
desc: descStroke
});
}
}
}
} catch (eS) {}
}
}

// 2. PageItems scans
var pageItems = doc.allPageItems;
for (var i = 0; i < pageItems.length; i++) {
var item = pageItems[i];

try {
var ifc = item.fillColor;
if (ifc && ifc.isValid && isBlackColor(ifc)) {
var ift = -1;
try { ift = item.fillTint; } catch (eT) {}
if (!isApprovedBlack(ifc, ift)) {
var isRichIF = isRichBlack(ifc);
var cmykInfoIF = isRichIF ? getCmykString(ifc) : "";
var effItemK = isRichIF ? "4-Color" : getEffectiveTint(ifc, ift);
var descIF = isRichIF ?
(item.constructor.name + " Fill 4-Color Black (" + (cmykInfoIF ? cmykInfoIF + " " : "") + "- Not Allowed)") :
(item.constructor.name + " Fill Black " + effItemK + "% (Approved: 100% or 80% only)");

state.blackIssues.push({
target: item,
type: "itemFill",
effK: isRichIF ? 100 : effItemK,
isRich: isRichIF,
desc: descIF
});
}
}
} catch (eIF) {}

try {
var isw = 0;
try { isw = item.strokeWeight; } catch (eSW) {}
if (isw > 0) {
var isc = item.strokeColor;
if (isc && isc.isValid && isBlackColor(isc)) {
var ist = -1;
try { ist = item.strokeTint; } catch (eST) {}
if (!isApprovedBlack(isc, ist)) {
var isRichIS = isRichBlack(isc);
var cmykInfoIS = isRichIS ? getCmykString(isc) : "";
var effItemStrokeK = isRichIS ? "4-Color" : getEffectiveTint(isc, ist);
var descIS = isRichIS ?
(item.constructor.name + " Stroke 4-Color Black (" + (cmykInfoIS ? cmykInfoIS + " " : "") + "- Not Allowed)") :
(item.constructor.name + " Stroke Black " + effItemStrokeK + "% (Approved: 100% or 80% only)");

state.blackIssues.push({
target: item,
type: "itemStroke",
effK: isRichIS ? 100 : effItemStrokeK,
isRich: isRichIS,
desc: descIS
});
}
}
}
} catch (eIS) {}
}
} catch (e) {
appendLog("Error scanning black color issues: " + e.message);
}

lblBlackColorCount.text = "" + state.blackIssues.length;
appendLog("Black Color Issues: " + state.blackIssues.length + " found.");
return state.blackIssues.length;
}

function reviewBlackColorIssue() {
if (state.blackIssues.length === 0) {
appendLog("No black color issues to review.");
return;
}
if (state.blackIssueIndex >= state.blackIssues.length) {
state.blackIssueIndex = 0;
}

var issue = state.blackIssues[state.blackIssueIndex];
try {
if (issue.target.isValid) {
app.select(issue.target, SelectionOptions.REPLACE_WITH);
zoomToSelectedTarget(issue.target);
var page = getItemPage(issue.target);
var pageName = page ? "Page " + page.name : "Pasteboard";
appendLog("Reviewing Black Issue (" + (state.blackIssueIndex + 1) + " of " + state.blackIssues.length + ") on " + pageName + ": " + issue.desc);
state.blackIssueIndex++;
} else {
state.blackIssueIndex++;
reviewBlackColorIssue();
}
} catch (e) {
appendLog("Error navigating to black issue: " + e.message);
}
}

function fixCurrentBlackIssue(targetTint) {
if (typeof targetTint !== "number") {
targetTint = 100;
}
if (state.blackIssues.length === 0) {
appendLog("No black color issue found to fix.");
return;
}

var targetIdx = state.blackIssueIndex > 0 ? (state.blackIssueIndex - 1) : 0;
if (targetIdx >= state.blackIssues.length) {
targetIdx = 0;
}

var issue = state.blackIssues[targetIdx];
var doc = getActiveDocument();
if (!doc) return;

var blackSwatch = doc.swatches.itemByName("[Black]");
if (!blackSwatch || !blackSwatch.isValid) {
blackSwatch = doc.swatches.itemByName("Black");
}

try {
if (issue.target && issue.target.isValid) {
if (issue.type === "textFill") {
issue.target.fillColor = blackSwatch;
issue.target.fillTint = targetTint;
} else if (issue.type === "textStroke") {
issue.target.strokeColor = blackSwatch;
issue.target.strokeTint = targetTint;
} else if (issue.type === "itemFill") {
issue.target.fillColor = blackSwatch;
try { issue.target.fillTint = targetTint; } catch (eT1) {}
} else if (issue.type === "itemStroke") {
issue.target.strokeColor = blackSwatch;
try { issue.target.strokeTint = targetTint; } catch (eT2) {}
}

var page = getItemPage(issue.target);
var pageName = page ? "Page " + page.name : "Pasteboard";
state.blackIssues.splice(targetIdx, 1);
lblBlackColorCount.text = "" + state.blackIssues.length;
appendLog("Fixed 1 Black issue to " + targetTint + "% pure Black on " + pageName + ". Remaining: " + state.blackIssues.length);

if (state.blackIssues.length > 0) {
if (targetIdx >= state.blackIssues.length) {
state.blackIssueIndex = 0;
} else {
state.blackIssueIndex = targetIdx;
}
reviewBlackColorIssue();
} else {
lblStatus.text = "Status: Black Issues Cleared";
}
} else {
state.blackIssues.splice(targetIdx, 1);
lblBlackColorCount.text = "" + state.blackIssues.length;
if (state.blackIssues.length > 0) reviewBlackColorIssue();
}
} catch (e) {
appendLog("Error fixing black issue: " + e.message);
}
}

function promptFixAllBlackChoice(count) {
var dlg = new Window("dialog", "Fix All Black Issues");
dlg.orientation = "column";
dlg.alignChildren = ["fill", "top"];
dlg.margins = 16;
dlg.spacing = 10;

var titleTxt = dlg.add("statictext", undefined, "Found " + count + " unapproved black color issue(s).");
titleTxt.graphics.font = ScriptUI.newFont(titleTxt.graphics.font.name, "Bold", 12);

var infoTxt = dlg.add("statictext", undefined, "Standard requires pure Black at either 80% Tint or 100% Solid.\nChoose which approved tint to convert ALL issues to:", { multiline: true });
infoTxt.preferredSize.width = 340;

var btnGroup = dlg.add("group");
btnGroup.orientation = "row";
btnGroup.alignment = ["center", "center"];
btnGroup.spacing = 8;

var choice = null;

var btn80 = btnGroup.add("button", undefined, "All to 80% Black");
btn80.preferredSize = [125, 26];
btn80.onClick = function () {
choice = 80;
dlg.close();
};

var btn100 = btnGroup.add("button", undefined, "All to 100% Black");
btn100.preferredSize = [135, 26];
btn100.onClick = function () {
choice = 100;
dlg.close();
};

var btnCancel = btnGroup.add("button", undefined, "Cancel", { name: "cancel" });
btnCancel.preferredSize = [70, 26];
btnCancel.onClick = function () {
choice = null;
dlg.close();
};

dlg.show();
return choice;
}

function fixAllBlackIssues(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return;

if (state.blackIssues.length === 0) {
scanBlackColorIssues(doc);
}
if (state.blackIssues.length === 0) {
appendLog("No black color issues to fix.");
return;
}

var chosenTint = promptFixAllBlackChoice(state.blackIssues.length);
if (chosenTint === null) {
appendLog("Fix All Black cancelled.");
return;
}

app.doScript(function () {
var blackSwatch = doc.swatches.itemByName("[Black]");
if (!blackSwatch || !blackSwatch.isValid) {
blackSwatch = doc.swatches.itemByName("Black");
}

var fixedCount = 0;
var prevRedraw = app.scriptPreferences.enableRedraw;
try {
app.scriptPreferences.enableRedraw = false;
for (var i = 0; i < state.blackIssues.length; i++) {
var issue = state.blackIssues[i];
try {
if (issue.target && issue.target.isValid) {
if (issue.type === "textFill") {
issue.target.fillColor = blackSwatch;
issue.target.fillTint = chosenTint;
fixedCount++;
} else if (issue.type === "textStroke") {
issue.target.strokeColor = blackSwatch;
issue.target.strokeTint = chosenTint;
fixedCount++;
} else if (issue.type === "itemFill") {
issue.target.fillColor = blackSwatch;
try { issue.target.fillTint = chosenTint; } catch (eT1) {}
fixedCount++;
} else if (issue.type === "itemStroke") {
issue.target.strokeColor = blackSwatch;
try { issue.target.strokeTint = chosenTint; } catch (eT2) {}
fixedCount++;
}
}
} catch (eOne) {}
}
appendLog("Fixed " + fixedCount + " black issue(s) across document to " + chosenTint + "% pure Black.");
scanBlackColorIssues(doc);
} catch (eAll) {
appendLog("Error fixing all black issues: " + eAll.message);
} finally {
app.scriptPreferences.enableRedraw = prevRedraw;
}
}, ScriptLanguage.JAVASCRIPT, undefined, UndoModes.ENTIRE_SCRIPT, "QC Fix All Black Issues to " + chosenTint + "%");
}

// -------------------------------------------------------------
// QC Check 6: Indian / Disallowed URLs (.in / com/in)
// -------------------------------------------------------------
function isDisallowedIndianUrl(url) {
if (!url) return false;
var cleanUrl = url.toLowerCase();
if (/(?:\.in|\.co\.in|\.org\.in|\.net\.in)(?:[\/\?#]|$)/i.test(cleanUrl)) {
return true;
}
if (/(?:com\/in|\/en-in|\/in)(?:[\/\?#]|$)/i.test(cleanUrl)) {
return true;
}
return false;
}

function scanDisallowedUrls(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return 0;

state.disallowedUrls = [];
state.disallowedUrlIndex = 0;

try {
var hyperlinks = doc.hyperlinks;
for (var h = 0; h < hyperlinks.length; h++) {
var hl = hyperlinks[h];
var dest = hl.destination;
if (dest && dest.hasOwnProperty("destinationURL")) {
var url = dest.destinationURL;
if (isDisallowedIndianUrl(url)) {
var sourceItem = null;
if (hl.source) {
if (hl.source.hasOwnProperty("sourceText")) {
sourceItem = hl.source.sourceText;
} else if (hl.source.hasOwnProperty("sourcePageItem")) {
sourceItem = hl.source.sourcePageItem;
}
}
state.disallowedUrls.push({
url: url,
target: sourceItem,
type: "Hyperlink Object"
});
}
}
}

app.findGrepPreferences = NothingEnum.nothing;
app.findGrepPreferences.findWhat = "(?i)\\b(?:https?:\\/\\/|www\\.)[^\\s<>'\"\\)\\]]+";

var textMatches = doc.findGrep();
for (var m = 0; m < textMatches.length; m++) {
var matchText = textMatches[m].contents;
if (isDisallowedIndianUrl(matchText)) {
state.disallowedUrls.push({
url: matchText,
target: textMatches[m],
type: "Text URL"
});
}
}
} catch (e) {
appendLog("Error scanning disallowed URLs: " + e.message);
} finally {
app.findGrepPreferences = NothingEnum.nothing;
}

lblUrlCount.text = "" + state.disallowedUrls.length;
appendLog("Disallowed URLs (.in): " + state.disallowedUrls.length + " found.");
return state.disallowedUrls.length;
}

function reviewDisallowedUrl() {
if (state.disallowedUrls.length === 0) {
appendLog("No disallowed URLs to review.");
return;
}
if (state.disallowedUrlIndex >= state.disallowedUrls.length) {
state.disallowedUrlIndex = 0;
}

var item = state.disallowedUrls[state.disallowedUrlIndex];
try {
if (item.target && item.target.isValid) {
app.select(item.target, SelectionOptions.REPLACE_WITH);
zoomToSelectedTarget(item.target);
var page = getItemPage(item.target);
var pageName = page ? "Page " + page.name : "Pasteboard";
appendLog("Reviewing URL (" + (state.disallowedUrlIndex + 1) + " of " + state.disallowedUrls.length + ") on " + pageName + " [" + item.type + "]: " + item.url);
state.disallowedUrlIndex++;
} else {
appendLog("URL (" + (state.disallowedUrlIndex + 1) + " of " + state.disallowedUrls.length + "): " + item.url);
state.disallowedUrlIndex++;
if (state.disallowedUrlIndex < state.disallowedUrls.length) {
reviewDisallowedUrl();
}
}
} catch (e) {
appendLog("Error navigating to disallowed URL: " + e.message);
}
}

// -------------------------------------------------------------
// QC Check 7: Hyphenation Check & Uncheck All
// -------------------------------------------------------------
function scanHyphenation(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return 0;

state.hyphenations = [];
state.hyphenationIndex = 0;

try {
var stories = doc.stories;
for (var s = 0; s < stories.length; s++) {
var story = stories[s];
var paras = story.paragraphs;
for (var p = 0; p < paras.length; p++) {
var para = paras[p];
if (para.hyphenation === true) {
var words = para.words;
var hasBrokenWord = false;

for (var w = 0; w < words.length; w++) {
var word = words[w];
if (word.lines.length > 1) {
state.hyphenations.push({
target: word,
para: para,
desc: "Hyphenated Word: \"" + word.contents + "\""
});
hasBrokenWord = true;
}
}

if (!hasBrokenWord && para.lines.length > 1) {
state.hyphenations.push({
target: para,
para: para,
desc: "Hyphenate Enabled on Paragraph (" + para.lines.length + " lines)"
});
}
}
}
}
} catch (e) {
appendLog("Error scanning hyphenated text: " + e.message);
}

lblHyphenCount.text = "" + state.hyphenations.length;
appendLog("Hyphenation: " + state.hyphenations.length + " occurrences found.");
return state.hyphenations.length;
}

function reviewHyphenation() {
if (state.hyphenations.length === 0) {
appendLog("No hyphenation to review.");
return;
}
if (state.hyphenationIndex >= state.hyphenations.length) {
state.hyphenationIndex = 0;
}

var item = state.hyphenations[state.hyphenationIndex];
try {
if (item.target.isValid) {
app.select(item.target, SelectionOptions.REPLACE_WITH);
zoomToSelectedTarget(item.target);
var page = getItemPage(item.target);
appendLog("Reviewing Hyphenation (" + (state.hyphenationIndex + 1) + " of " + state.hyphenations.length + ") on " + (page ? "Page " + page.name : "Pasteboard") + ": " + item.desc);
state.hyphenationIndex++;
} else {
state.hyphenationIndex++;
reviewHyphenation();
}
} catch (e) {
appendLog("Error navigating to hyphenation: " + e.message);
}
}

function uncheckAllHyphenation(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return;

if (!confirm("Are you sure you want to uncheck hyphenation across all paragraphs in the document?", false, "Confirm Uncheck Hyphenation")) {
return;
}

app.doScript(function () {
var count = 0;
var prevRedraw = app.scriptPreferences.enableRedraw;
try {
app.scriptPreferences.enableRedraw = false;
var stories = doc.stories;
for (var s = 0; s < stories.length; s++) {
var paras = stories[s].paragraphs;
for (var p = 0; p < paras.length; p++) {
if (paras[p].hyphenation === true) {
paras[p].hyphenation = false;
count++;
}
}
}
appendLog("Unchecked Hyphenate on " + count + " paragraphs across the document.");
scanHyphenation(doc);
} catch (e) {
appendLog("Error unchecking hyphenation: " + e.message);
} finally {
app.scriptPreferences.enableRedraw = prevRedraw;
}
}, ScriptLanguage.JAVASCRIPT, undefined, UndoModes.ENTIRE_SCRIPT, "QC Uncheck Hyphenation");
}

// -------------------------------------------------------------
// QC Check 8: Ligatures Check & Turn Off Fix
// -------------------------------------------------------------
function scanLigatures(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return 0;

state.ligatures = [];
state.ligatureIndex = 0;

try {
var stories = doc.stories;
for (var s = 0; s < stories.length; s++) {
var ranges = stories[s].textStyleRanges;
for (var r = 0; r < ranges.length; r++) {
var range = ranges[r];
if (range.contents.replace(/[\r\n\s\t]+/g, "").length === 0) continue;

if (range.ligatures === true) {
if (/(fi|fl|ff|ffi|ffl|[fiflffffiffl])/i.test(range.contents)) {
state.ligatures.push(range);
}
}
}
}
} catch (e) {
appendLog("Error scanning ligatures: " + e.message);
}

lblLigatureCount.text = "" + state.ligatures.length;
appendLog("Ligatures Used: " + state.ligatures.length + " ranges found.");
return state.ligatures.length;
}

function reviewLigature() {
if (state.ligatures.length === 0) {
appendLog("No ligatures to review.");
return;
}
if (state.ligatureIndex >= state.ligatures.length) {
state.ligatureIndex = 0;
}

var range = state.ligatures[state.ligatureIndex];
try {
if (range.isValid) {
app.select(range, SelectionOptions.REPLACE_WITH);
zoomToSelectedTarget(range);
var page = getItemPage(range);
var sample = range.contents;
if (sample.length > 22) sample = sample.substring(0, 19) + "...";
appendLog("Reviewing Ligature (" + (state.ligatureIndex + 1) + " of " + state.ligatures.length + ") on " + (page ? "Page " + page.name : "Pasteboard") + ": \"" + sample.replace(/[\r\n]+/g, " ") + "\"");
state.ligatureIndex++;
} else {
state.ligatureIndex++;
reviewLigature();
}
} catch (e) {
appendLog("Error navigating to ligature: " + e.message);
}
}

function fixCurrentLigature() {
if (state.ligatures.length === 0) {
appendLog("No ligature found to fix.");
return;
}

var targetIdx = state.ligatureIndex > 0 ? (state.ligatureIndex - 1) : 0;
if (targetIdx >= state.ligatures.length) targetIdx = 0;

var range = state.ligatures[targetIdx];
try {
if (range.isValid) {
range.ligatures = false;
state.ligatures.splice(targetIdx, 1);
lblLigatureCount.text = "" + state.ligatures.length;
appendLog("Turned off ligatures on selected text. Remaining: " + state.ligatures.length);

if (state.ligatures.length > 0) {
if (targetIdx >= state.ligatures.length) state.ligatureIndex = 0;
else state.ligatureIndex = targetIdx;
reviewLigature();
} else {
state.ligatureIndex = 0;
appendLog("All ligatures turned off!");
}
} else {
state.ligatures.splice(targetIdx, 1);
lblLigatureCount.text = "" + state.ligatures.length;
fixCurrentLigature();
}
} catch (e) {
appendLog("Error fixing ligature: " + e.message);
}
}

function fixAllLigatures(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return;

if (!confirm("Are you sure you want to turn off ligatures across the entire document?", false, "Confirm Turn Off All Ligatures")) {
return;
}

app.doScript(function () {
var count = 0;
var prevRedraw = app.scriptPreferences.enableRedraw;
try {
app.scriptPreferences.enableRedraw = false;
var stories = doc.stories;
for (var s = 0; s < stories.length; s++) {
var ranges = stories[s].textStyleRanges;
for (var r = 0; r < ranges.length; r++) {
if (ranges[r].ligatures === true) {
ranges[r].ligatures = false;
count++;
}
}
}
appendLog("Turned off ligatures on " + count + " text ranges across document.");
scanLigatures(doc);
} catch (e) {
appendLog("Error turning off ligatures: " + e.message);
} finally {
app.scriptPreferences.enableRedraw = prevRedraw;
}
}, ScriptLanguage.JAVASCRIPT, undefined, UndoModes.ENTIRE_SCRIPT, "QC Turn Off Ligatures");
}

// -------------------------------------------------------------
// Zoom Helper (Maintains user's current zoom level like native Find/Change)
// -------------------------------------------------------------
function zoomToSelectedTarget(target) {
try {
var loc = getItemLocation(target);
var curZoom = 100;
try {
curZoom = app.activeWindow.zoomPercentage;
} catch (eZ) {}

// 1. Switch active spread if target is on a different spread
if (loc.spread && loc.spread.isValid) {
if (app.activeWindow.activeSpread !== loc.spread) {
app.activeWindow.activeSpread = loc.spread;
}
}

// 2. Set active page on that spread
if (loc.page && loc.page.isValid) {
app.activeWindow.activePage = loc.page;
}

// 3. Center viewport on the selected item while keeping current zoom level
try {
var act = null;
try { act = app.menuActions.itemByName("$ID/Fit Selection in Window"); } catch (e1) {}
if (!act || !act.isValid) {
try { act = app.menuActions.itemByName("Fit Selection in Window"); } catch (e2) {}
}
if (act && act.isValid && act.enabled) {
act.invoke();
}
} catch (eFit) {}

// 4. Restore the user's exact zoom level (e.g. 150%, 200%, etc.)
// so it never forces "fit page" or "fit frame", behaving exactly like native Find/Change!
if (curZoom && curZoom > 0) {
app.activeWindow.zoomPercentage = curZoom;
}
} catch (e) {}
}

// -------------------------------------------------------------
// Full QC Runner & Pristine Reset
// -------------------------------------------------------------
function pristineReset() {
state.doubleSpaces = [];
state.doubleSpaceIndex = 0;
state.widows = [];
state.widowIndex = 0;
state.imageFramesWithFill = [];
state.imageFrameIndex = 0;
state.blackIssues = [];
state.blackIssueIndex = 0;
state.disallowedUrls = [];
state.disallowedUrlIndex = 0;
state.hyphenations = [];
state.hyphenationIndex = 0;
state.ligatures = [];
state.ligatureIndex = 0;
state.unusedSwatches = [];
state.unusedSwatchesCount = 0;

lblUnusedCount.text = "0";
lblDoubleSpaceCount.text = "0";
lblWidowCount.text = "0";
lblImageFrameCount.text = "0";
lblBlackColorCount.text = "0";
lblUrlCount.text = "0";
lblHyphenCount.text = "0";
lblLigatureCount.text = "0";

var doc = getActiveDocument();
lblDocName.text = "Document: " + (doc ? doc.name : "None");
lblStatus.text = "Status: Ready";

try {
app.select(NothingEnum.nothing);
} catch (e) {}

reportBox.text = "QC Master Dashboard ready.\nClick 'Run Full QC' or individual [Scan] buttons to begin.\n";
}

function runFullQC() {
var doc = getActiveDocument();
if (!doc) {
alert("Please open an InDesign document before running QC.", "No Document");
return;
}

lblDocName.text = "Document: " + doc.name;
lblStatus.text = "Status: Running...";

var prevRedraw = app.scriptPreferences.enableRedraw;
try {
// Speed optimization: suppress screen redraws during multi-page audits (50+ pages)
app.scriptPreferences.enableRedraw = false;

// Step 1: Detect and add any unnamed colors to the Swatches panel
addUnnamedColors(doc);

var cSwatches = scanUnusedSwatches(doc);
var cDouble = scanDoubleSpaces(doc);
var cWidows = scanWidows(doc);
var cImgFill = scanImageFramesWithFill(doc);
var cBlack = scanBlackColorIssues(doc);
var cUrls = scanDisallowedUrls(doc);
var cHyphens = scanHyphenation(doc);
var cLigatures = scanLigatures(doc);

var total = cSwatches + cDouble + cWidows + cImgFill + cBlack + cUrls + cHyphens + cLigatures;
lblStatus.text = "Status: Complete (" + total + " issues)";

// Post summary header at the top
appendLog("=========================================");
appendLog("Full QC Complete for: " + doc.name + " (" + total + " total issues)");
appendLog("=========================================");
} catch (eQC) {
lblStatus.text = "Status: Error";
appendLog("Error during Full QC: " + eQC.message);
} finally {
app.scriptPreferences.enableRedraw = prevRedraw;
}
}

function refreshQC() {
var doc = getActiveDocument();
if (!doc) {
lblDocName.text = "Document: None";
lblStatus.text = "Status: No document open";
return;
}
lblDocName.text = "Document: " + doc.name;
lblStatus.text = "Status: Ready";
runFullQC();
}

function exportReport() {
var reportContent = reportBox.text;
if (!reportContent || reportContent.length === 0) {
alert("The report log is empty. Run QC before exporting.", "Empty Log");
return;
}

var doc = getActiveDocument();
var defaultPath = "~/Desktop";
if (doc && doc.saved) {
defaultPath = doc.filePath;
}

var fileName = (doc ? doc.name.replace(/\.indd$/i, "") : "Document") + "_QC_Report.txt";
var file = new File(defaultPath + "/" + fileName).saveDlg("Save QC Audit Report", "Text File:*.txt");
if (file) {
file.encoding = "UTF-8";
file.open("w");
file.write(reportContent);
file.close();
appendLog("Report successfully saved to: " + file.fsName);
}
}

// -------------------------------------------------------------
// UI Construction (Compact 2-Column Layout)
// -------------------------------------------------------------
if ($.global.qcmaster_win && $.global.qcmaster_win instanceof Window) {
try { $.global.qcmaster_win.close(); } catch (eW) {}
}
var win = new Window("palette", "QC Master Dashboard v2.4 | Prepress Audit & Fix Hub", undefined, { resizeable: true });
$.global.qcmaster_win = win;
win.orientation = "column";
win.alignChildren = ["fill", "top"];
win.margins = 10;
win.spacing = 6;

// --- Compact Header: Document Information & Actions ---
var pnlHeader = win.add("panel", undefined, "Document & Actions");
pnlHeader.orientation = "column";
pnlHeader.alignChildren = ["fill", "top"];
pnlHeader.margins = 8;
pnlHeader.spacing = 6;

var headerRow1 = pnlHeader.add("group");
headerRow1.orientation = "row";
headerRow1.alignChildren = ["left", "center"];
headerRow1.spacing = 16;

var initialDoc = getActiveDocument();
var docNameText = initialDoc ? initialDoc.name : "No active document";
var lblDocName = headerRow1.add("statictext", undefined, "Document: " + docNameText);
lblDocName.preferredSize.width = 340;
var lblStatus = headerRow1.add("statictext", undefined, "Status: Ready");
lblStatus.preferredSize.width = 250;

var headerRow2 = pnlHeader.add("group");
headerRow2.orientation = "row";
headerRow2.alignChildren = ["left", "center"];
headerRow2.spacing = 8;

var btnRunQC = headerRow2.add("button", undefined, "Run Full QC");
btnRunQC.preferredSize = [105, 24];

var btnReset = headerRow2.add("button", undefined, "Reset");
btnReset.preferredSize = [60, 24];

var btnRefresh = headerRow2.add("button", undefined, "Refresh");
btnRefresh.preferredSize = [70, 24];

var btnExport = headerRow2.add("button", undefined, "Export Report");
btnExport.preferredSize = [100, 24];

// --- Main 2-Column Container ---
var mainCols = win.add("group");
mainCols.orientation = "row";
mainCols.alignChildren = ["fill", "top"];
mainCols.spacing = 8;

// =============================================================
// COLUMN 1: Text & Typography
// =============================================================
var col1 = mainCols.add("panel", undefined, "Typography & Text");
col1.orientation = "column";
col1.alignChildren = ["fill", "top"];
col1.preferredSize.width = 335;
col1.margins = 8;
col1.spacing = 7;

// Row 1.1: Double Spaces
var rowDouble = col1.add("group");
rowDouble.orientation = "row";
rowDouble.alignChildren = ["left", "center"];
rowDouble.spacing = 4;
var lblDoubleTitle = rowDouble.add("statictext", undefined, "Double Space:");
lblDoubleTitle.preferredSize.width = 82;
var lblDoubleSpaceCount = rowDouble.add("statictext", undefined, "0");
lblDoubleSpaceCount.preferredSize.width = 24;
var btnScanDouble = rowDouble.add("button", undefined, "Scan");
btnScanDouble.preferredSize = [38, 21];
var btnReviewDoubleSpace = rowDouble.add("button", undefined, "Find");
btnReviewDoubleSpace.preferredSize = [38, 21];
var btnFixCurrentDoubleSpace = rowDouble.add("button", undefined, "Fix");
btnFixCurrentDoubleSpace.preferredSize = [32, 21];
var btnFixDoubleSpaces = rowDouble.add("button", undefined, "All");
btnFixDoubleSpaces.preferredSize = [32, 21];

// Row 1.2: Widow Check (Scan | Find | Fix | All)
var rowWidow = col1.add("group");
rowWidow.orientation = "row";
rowWidow.alignChildren = ["left", "center"];
rowWidow.spacing = 4;
var lblWidowTitle = rowWidow.add("statictext", undefined, "Widows:");
lblWidowTitle.preferredSize.width = 82;
var lblWidowCount = rowWidow.add("statictext", undefined, "0");
lblWidowCount.preferredSize.width = 24;
var btnScanWidow = rowWidow.add("button", undefined, "Scan");
btnScanWidow.preferredSize = [38, 21];
var btnReviewWidow = rowWidow.add("button", undefined, "Find");
btnReviewWidow.preferredSize = [38, 21];
var btnFixCurrentWidow = rowWidow.add("button", undefined, "Fix");
btnFixCurrentWidow.preferredSize = [32, 21];
var btnFixAllWidow = rowWidow.add("button", undefined, "All");
btnFixAllWidow.preferredSize = [32, 21];

// Row 1.3: Hyphenated Lines & Uncheck Option
var rowHyphen = col1.add("group");
rowHyphen.orientation = "row";
rowHyphen.alignChildren = ["left", "center"];
rowHyphen.spacing = 4;
var lblHyphenTitle = rowHyphen.add("statictext", undefined, "Hyphenation:");
lblHyphenTitle.preferredSize.width = 82;
var lblHyphenCount = rowHyphen.add("statictext", undefined, "0");
lblHyphenCount.preferredSize.width = 24;
var btnScanHyphen = rowHyphen.add("button", undefined, "Scan");
btnScanHyphen.preferredSize = [38, 21];
var btnReviewHyphen = rowHyphen.add("button", undefined, "Find");
btnReviewHyphen.preferredSize = [38, 21];
var btnUncheckHyphen = rowHyphen.add("button", undefined, "Uncheck");
btnUncheckHyphen.preferredSize = [56, 21];

// Row 1.4: Ligatures Used & Fix
var rowLigature = col1.add("group");
rowLigature.orientation = "row";
rowLigature.alignChildren = ["left", "center"];
rowLigature.spacing = 4;
var lblLigatureTitle = rowLigature.add("statictext", undefined, "Ligatures:");
lblLigatureTitle.preferredSize.width = 82;
var lblLigatureCount = rowLigature.add("statictext", undefined, "0");
lblLigatureCount.preferredSize.width = 24;
var btnScanLigature = rowLigature.add("button", undefined, "Scan");
btnScanLigature.preferredSize = [38, 21];
var btnReviewLigature = rowLigature.add("button", undefined, "Find");
btnReviewLigature.preferredSize = [38, 21];
var btnFixLigature = rowLigature.add("button", undefined, "Off");
btnFixLigature.preferredSize = [32, 21];
var btnAllLigature = rowLigature.add("button", undefined, "All");
btnAllLigature.preferredSize = [32, 21];

// =============================================================
// COLUMN 2: Colors, Assets & Links
// =============================================================
var col2 = mainCols.add("panel", undefined, "Colors, Frames & Links");
col2.orientation = "column";
col2.alignChildren = ["fill", "top"];
col2.preferredSize.width = 335;
col2.margins = 8;
col2.spacing = 7;

// Row 2.1: Unused Swatches
var rowSwatches = col2.add("group");
rowSwatches.orientation = "row";
rowSwatches.alignChildren = ["left", "center"];
rowSwatches.spacing = 4;
var lblSwatchesTitle = rowSwatches.add("statictext", undefined, "Swatches:");
lblSwatchesTitle.preferredSize.width = 82;
var lblUnusedCount = rowSwatches.add("statictext", undefined, "0");
lblUnusedCount.preferredSize.width = 24;
var btnScanSwatches = rowSwatches.add("button", undefined, "Scan");
btnScanSwatches.preferredSize = [38, 21];
var btnRemoveSwatches = rowSwatches.add("button", undefined, "Remove All");
btnRemoveSwatches.preferredSize = [80, 21];

// Row 2.2: Black Color Issues (Strict 100% Solid & 80% Tint Only; No 4-Color Black)
var rowBlack = col2.add("group");
rowBlack.orientation = "row";
rowBlack.alignChildren = ["left", "center"];
rowBlack.spacing = 3;
var lblBlackTitle = rowBlack.add("statictext", undefined, "Black Issues:");
lblBlackTitle.preferredSize.width = 75;
var lblBlackColorCount = rowBlack.add("statictext", undefined, "0");
lblBlackColorCount.preferredSize.width = 20;
var btnScanBlack = rowBlack.add("button", undefined, "Scan");
btnScanBlack.preferredSize = [35, 21];
var btnReviewBlack = rowBlack.add("button", undefined, "Find");
btnReviewBlack.preferredSize = [35, 21];
var btnFix80Black = rowBlack.add("button", undefined, "FIX80");
btnFix80Black.preferredSize = [42, 21];
var btnFix100Black = rowBlack.add("button", undefined, "FIX100");
btnFix100Black.preferredSize = [46, 21];
var btnFixAllBlack = rowBlack.add("button", undefined, "All");
btnFixAllBlack.preferredSize = [28, 21];

// Row 2.3: Image Frames with Fill (Find | Clear)
var rowImgFill = col2.add("group");
rowImgFill.orientation = "row";
rowImgFill.alignChildren = ["left", "center"];
rowImgFill.spacing = 4;
var lblImgFillTitle = rowImgFill.add("statictext", undefined, "Img Fill Color:");
lblImgFillTitle.preferredSize.width = 82;
var lblImageFrameCount = rowImgFill.add("statictext", undefined, "0");
lblImageFrameCount.preferredSize.width = 24;
var btnScanImgFill = rowImgFill.add("button", undefined, "Scan");
btnScanImgFill.preferredSize = [38, 21];
var btnReviewImageFrame = rowImgFill.add("button", undefined, "Find");
btnReviewImageFrame.preferredSize = [38, 21];
var btnClearImgFill = rowImgFill.add("button", undefined, "Clear");
btnClearImgFill.preferredSize = [40, 21];

// Row 2.4: Disallowed URLs (.in / com/in)
var rowUrls = col2.add("group");
rowUrls.orientation = "row";
rowUrls.alignChildren = ["left", "center"];
rowUrls.spacing = 4;
var lblUrlTitle = rowUrls.add("statictext", undefined, "URLs (.in):");
lblUrlTitle.preferredSize.width = 82;
var lblUrlCount = rowUrls.add("statictext", undefined, "0");
lblUrlCount.preferredSize.width = 24;
var btnScanUrls = rowUrls.add("button", undefined, "Scan");
btnScanUrls.preferredSize = [38, 21];
var btnReviewUrl = rowUrls.add("button", undefined, "Find");
btnReviewUrl.preferredSize = [38, 21];

// --- Panel: QC Report Log ---
var pnlReport = win.add("panel", undefined, "QC Report");
pnlReport.alignChildren = ["fill", "fill"];
pnlReport.margins = 6;

var reportBox = pnlReport.add("edittext", undefined, "QC Master Dashboard ready.\nClick 'Run Full QC' or individual [Scan] buttons to begin.\n", {
multiline: true,
readonly: true,
scrollable: true
});
reportBox.preferredSize = [670, 85];

// --- Footer: Credits & Buttons ---
var footerGroup = win.add("group");
footerGroup.orientation = "row";
footerGroup.alignChildren = ["fill", "center"];
footerGroup.alignment = ["fill", "bottom"];

var lblCredits = footerGroup.add("statictext", undefined, "\u00A9 2026 Developed by GCoE Creative Studio | Contact at Nousheeth_iqbal@ajg.com");
lblCredits.preferredSize.width = 475;

var btnClearLog = footerGroup.add("button", undefined, "Clear Log");
btnClearLog.preferredSize = [75, 23];
var btnClose = footerGroup.add("button", undefined, "Close");
btnClose.preferredSize = [65, 23];

// -------------------------------------------------------------
// Event Listeners
// -------------------------------------------------------------
btnRunQC.onClick = function () { runFullQC(); };
btnReset.onClick = function () { pristineReset(); };
btnRefresh.onClick = function () { refreshQC(); };
btnExport.onClick = function () { exportReport(); };

// Individual Scan Listeners
btnScanDouble.onClick = function () { scanDoubleSpaces(); };
btnScanWidow.onClick = function () { scanWidows(); };
btnScanHyphen.onClick = function () { scanHyphenation(); };
btnScanLigature.onClick = function () { scanLigatures(); };
btnScanSwatches.onClick = function () { scanUnusedSwatches(); };
btnScanBlack.onClick = function () { scanBlackColorIssues(); };
btnScanImgFill.onClick = function () { scanImageFramesWithFill(); };
btnScanUrls.onClick = function () { scanDisallowedUrls(); };

// Action Listeners
btnReviewDoubleSpace.onClick = function () { reviewDoubleSpace(); };
btnFixCurrentDoubleSpace.onClick = function () { fixCurrentDoubleSpace(); };
btnFixDoubleSpaces.onClick = function () { fixAllDoubleSpaces(); };

btnReviewWidow.onClick = function () { reviewWidow(); };
btnFixCurrentWidow.onClick = function () { fixCurrentWidow(); };
btnFixAllWidow.onClick = function () { fixAllWidows(); };

btnReviewHyphen.onClick = function () { reviewHyphenation(); };
btnUncheckHyphen.onClick = function () { uncheckAllHyphenation(); };

btnReviewLigature.onClick = function () { reviewLigature(); };
btnFixLigature.onClick = function () { fixCurrentLigature(); };
btnAllLigature.onClick = function () { fixAllLigatures(); };

btnRemoveSwatches.onClick = function () { removeAllUnusedSwatches(); };
btnReviewBlack.onClick = function () { reviewBlackColorIssue(); };
btnFix80Black.onClick = function () { fixCurrentBlackIssue(80); };
btnFix100Black.onClick = function () { fixCurrentBlackIssue(100); };
btnFixAllBlack.onClick = function () { fixAllBlackIssues(); };

btnReviewImageFrame.onClick = function () { reviewImageFrame(); };
btnClearImgFill.onClick = function () { clearCurrentImgFill(); };

btnReviewUrl.onClick = function () { reviewDisallowedUrl(); };

btnClearLog.onClick = function () { reportBox.text = ""; };
btnClose.onClick = function () {
try { win.close(); } catch (e) {}
$.global.qcmaster_win = null;
};

// Show modeless palette
win.center();
win.show();
})();

 

Translate
NousheethAuthor
Known Participant
Oct 26, 2026 8:32 PM UTC
code1

#target indesign
#targetengine "qcmaster_engine"

/**
* QC Master Dashboard v2.2
* Print Production Preflight & Audit Script for Adobe InDesign
* Compatible with InDesign CC 2022 through CC 2025+
* Developed by Nousheeth Iqbal
*/

(function () {
// -------------------------------------------------------------
// Global Storage for Issue Navigation
// -------------------------------------------------------------
var state = {
doc: null,
doubleSpaces: [],
doubleSpaceIndex: 0,
widows: [],
widowIndex: 0,
imageFramesWithFill: [],
imageFrameIndex: 0,
blackIssues: [],
blackIssueIndex: 0,
disallowedUrls: [],
disallowedUrlIndex: 0,
hyphenations: [],
hyphenationIndex: 0,
ligatures: [],
ligatureIndex: 0,
unusedSwatches: [],
unusedSwatchesCount: 0
};

function getActiveDocument() {
if (app.documents.length === 0) {
return null;
}
return app.activeDocument;
}

// -------------------------------------------------------------
// Helper: Logging (Newest / Most Recent Entry at the Top)
// -------------------------------------------------------------
function appendLog(txt) {
var time = new Date();
var timeStr = "[" + pad(time.getHours()) + ":" + pad(time.getMinutes()) + ":" + pad(time.getSeconds()) + "] ";
var newEntry = timeStr + txt + "\n";
var currentText = reportBox.text;

if (currentText.length > 25000) {
currentText = currentText.substring(0, 20000);
}

reportBox.text = newEntry + currentText;
}

function pad(n) {
return (n < 10 ? "0" : "") + n;
}

function getItemLocation(target) {
var page = null;
var spread = null;
if (!target) return { page: null, spread: null };

try {
// Case 1: Text target (Paragraph, Line, Word, Character, TextStyleRange, Match)
var tf = null;
try {
if (target.parentTextFrames && target.parentTextFrames.length > 0) {
tf = target.parentTextFrames[0];
}
} catch (eTf) {}

var cur = tf ? tf : target;

// Direct parentPage check
try {
if (cur.parentPage && cur.parentPage.isValid) {
page = cur.parentPage;
}
} catch (ePp) {}

// Ascend hierarchy to find Page and Spread
var node = cur;
while (node) {
try {
if (!page && node.constructor.name === "Page") {
page = node;
}
if (node.constructor.name === "Spread") {
spread = node;
break;
}
if (node.constructor.name === "Document") {
break;
}
} catch (eNode) {}
try {
node = node.parent;
} catch (ePar) {
break;
}
}

// Derive spread from page if needed
if (page && page.isValid && (!spread || !spread.isValid)) {
try {
if (page.parent && page.parent.constructor.name === "Spread") {
spread = page.parent;
}
} catch (eSp) {}
}
} catch (e) {}

return { page: page, spread: spread };
}

function getItemPage(target) {
return getItemLocation(target).page;
}

// -------------------------------------------------------------
// Helper: Automatically Add Unnamed Colors to Swatches Panel
// -------------------------------------------------------------
function addUnnamedColors(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return 0;

try {
if (app.documents.length > 0 && app.activeDocument !== doc) {
app.activeDocument = doc;
}
} catch (eDoc) {}

var beforeCount = doc.swatches.length;
try {
var act = null;
try {
act = app.menuActions.itemByName("$ID/Add Unnamed Colors");
} catch (e1) {}

if (!act || !act.isValid) {
try {
act = app.menuActions.itemByName("Add Unnamed Colors");
} catch (e2) {}
}

if (act && act.isValid && act.enabled) {
act.invoke();
}
} catch (e) {}

var afterCount = doc.swatches.length;
var added = afterCount - beforeCount;
if (added > 0) {
appendLog("Added " + added + " unnamed color(s) to Swatches panel.");
}
return added;
}

// -------------------------------------------------------------
// QC Check 1: Unused Swatches (Auto-adds unnamed colors first)
// -------------------------------------------------------------
function scanUnusedSwatches(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return 0;

// Auto-add any unnamed colors applied to text/objects into the Swatches panel
addUnnamedColors(doc);

state.unusedSwatches = [];
var swatchNames = [];

try {
var rawUnused = doc.unusedSwatches;
var rawUnusedMap = {};
for (var u = 0; u < rawUnused.length; u++) {
try {
rawUnusedMap[rawUnused[u].name] = true;
} catch (eU) {}
}

var panelSwatches = doc.swatches;
for (var i = 0; i < panelSwatches.length; i++) {
var sw = panelSwatches[i];
var swName = sw.name;

// Protect default system swatches
if (swName === "None" || swName === "Registration" || swName === "Black" || swName === "Paper" ||
swName === "[None]" || swName === "[Registration]" || swName === "[Black]" || swName === "[Paper]") {
continue;
}

if (rawUnusedMap[swName]) {
state.unusedSwatches.push(sw);
swatchNames.push(swName);
}
}
} catch (e) {
appendLog("Error scanning unused swatches: " + e.message);
}

state.unusedSwatchesCount = state.unusedSwatches.length;
lblUnusedCount.text = "" + state.unusedSwatchesCount;

if (state.unusedSwatchesCount > 0) {
appendLog("Unused Swatches in Panel (" + state.unusedSwatchesCount + "): " + swatchNames.join(", "));
} else {
appendLog("Swatches: 0 unused swatches in Swatches panel.");
}

return state.unusedSwatchesCount;
}

function removeAllUnusedSwatches(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return;

if (state.unusedSwatches.length === 0) {
scanUnusedSwatches(doc);
}
if (state.unusedSwatches.length === 0) {
appendLog("No unused swatches to remove.");
return;
}

if (!confirm("Are you sure you want to remove all " + state.unusedSwatches.length + " unused swatch(es) from the Swatches panel?", false, "Confirm Remove Unused Swatches")) {
return;
}

app.doScript(function () {
var count = 0;
try {
for (var i = state.unusedSwatches.length - 1; i >= 0; i--) {
var sw = state.unusedSwatches[i];
try {
if (sw && sw.isValid) {
sw.remove();
count++;
}
} catch (err) {}
}
appendLog("Removed " + count + " unused swatches from Swatches panel.");
scanUnusedSwatches(doc);
} catch (e) {
appendLog("Error removing swatches: " + e.message);
}
}, ScriptLanguage.JAVASCRIPT, undefined, UndoModes.ENTIRE_SCRIPT, "QC Remove Unused Swatches");
}

// -------------------------------------------------------------
// QC Check 2: Double Spaces (Scan | Review | Fix | All)
// -------------------------------------------------------------
function scanDoubleSpaces(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return 0;

state.doubleSpaces = [];
state.doubleSpaceIndex = 0;

app.findGrepPreferences = NothingEnum.nothing;
app.changeGrepPreferences = NothingEnum.nothing;
app.findGrepPreferences.findWhat = "\\x{0020}{2,}";

try {
var matches = doc.findGrep();
for (var i = 0; i < matches.length; i++) {
state.doubleSpaces.push(matches[i]);
}
} catch (e) {
appendLog("Error scanning double spaces: " + e.message);
} finally {
app.findGrepPreferences = NothingEnum.nothing;
app.changeGrepPreferences = NothingEnum.nothing;
}

lblDoubleSpaceCount.text = "" + state.doubleSpaces.length;
appendLog("Double Spaces: " + state.doubleSpaces.length + " found.");
return state.doubleSpaces.length;
}

function reviewDoubleSpace() {
if (state.doubleSpaces.length === 0) {
appendLog("No double spaces to review.");
return;
}
if (state.doubleSpaceIndex >= state.doubleSpaces.length) {
state.doubleSpaceIndex = 0;
}

var match = state.doubleSpaces[state.doubleSpaceIndex];
try {
if (match.isValid) {
app.select(match, SelectionOptions.REPLACE_WITH);
zoomToSelectedTarget(match);
var page = getItemPage(match);
appendLog("Reviewing Double Space (" + (state.doubleSpaceIndex + 1) + " of " + state.doubleSpaces.length + ") on " + (page ? "Page " + page.name : "Pasteboard"));
state.doubleSpaceIndex++;
} else {
state.doubleSpaces.splice(state.doubleSpaceIndex, 1);
lblDoubleSpaceCount.text = "" + state.doubleSpaces.length;
reviewDoubleSpace();
}
} catch (e) {
appendLog("Error navigating to double space: " + e.message);
}
}

function fixCurrentDoubleSpace() {
if (state.doubleSpaces.length === 0) {
appendLog("No double space found to fix.");
return;
}

var targetIdx = state.doubleSpaceIndex > 0 ? (state.doubleSpaceIndex - 1) : 0;
if (targetIdx >= state.doubleSpaces.length) {
targetIdx = 0;
}

var match = state.doubleSpaces[targetIdx];
try {
if (match.isValid) {
var page = getItemPage(match);
match.contents = " ";
state.doubleSpaces.splice(targetIdx, 1);
lblDoubleSpaceCount.text = "" + state.doubleSpaces.length;
appendLog("Fixed 1 double space on " + (page ? "Page " + page.name : "Pasteboard") + ". Remaining: " + state.doubleSpaces.length);

if (state.doubleSpaces.length > 0) {
if (targetIdx >= state.doubleSpaces.length) {
state.doubleSpaceIndex = 0;
} else {
state.doubleSpaceIndex = targetIdx;
}
reviewDoubleSpace();
} else {
state.doubleSpaceIndex = 0;
appendLog("All double spaces resolved!");
}
} else {
state.doubleSpaces.splice(targetIdx, 1);
lblDoubleSpaceCount.text = "" + state.doubleSpaces.length;
fixCurrentDoubleSpace();
}
} catch (e) {
appendLog("Error fixing single double space: " + e.message);
}
}

function fixAllDoubleSpaces(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return;

if (!confirm("Are you sure you want to replace all double spaces across the entire document?", false, "Confirm Fix All Double Spaces")) {
return;
}

app.doScript(function () {
app.findGrepPreferences = NothingEnum.nothing;
app.changeGrepPreferences = NothingEnum.nothing;
app.findGrepPreferences.findWhat = "\\x{0020}{2,}";
app.changeGrepPreferences.changeTo = " ";

try {
var changed = doc.changeGrep();
appendLog("Replaced " + changed.length + " double space occurrences with single space.");
scanDoubleSpaces(doc);
} catch (e) {
appendLog("Error fixing double spaces: " + e.message);
} finally {
app.findGrepPreferences = NothingEnum.nothing;
app.changeGrepPreferences = NothingEnum.nothing;
}
}, ScriptLanguage.JAVASCRIPT, undefined, UndoModes.ENTIRE_SCRIPT, "QC Fix All Double Spaces");
}

// -------------------------------------------------------------
// QC Check 3: Widow Check (Excludes 1-word/bullet paragraphs)
// -------------------------------------------------------------
function scanWidows(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return 0;

state.widows = [];
state.widowIndex = 0;

try {
var stories = doc.stories;
for (var s = 0; s < stories.length; s++) {
var story = stories[s];
var paras = story.paragraphs;
for (var p = 0; p < paras.length; p++) {
var para = paras[p];

// Multi-line paragraph check: must have at least 2 lines and at least 3 words total
// (Excludes 1-word bullets and 2-word icon descriptions like "Icon\nLabel")
if (para.lines.length >= 2 && para.words.length >= 3) {
// Skip if it is a bullet/numbered list item with < 4 words
if (para.bulletsAndNumberingListType !== ListType.NO_LIST && para.words.length < 4) {
continue;
}

var lastLine = para.lines.lastItem();
var lastLineClean = lastLine.contents.replace(/[\r\n\s\t]+$/, "");
var wordsInLastLine = lastLine.words;

// Check the line before the last line
var prevLine = para.lines.item(para.lines.length - 2);

// If the line was broken using a manual Forced Line Break (Shift + Enter / Soft Return),
// this is an intentional manual line break, NOT an automated typographic widow!
if (/[\u000A\n]/.test(prevLine.contents)) {
continue;
}

// A widow occurs if the last line has only 1 word, or is a very short stub (<= 8 chars)
if (wordsInLastLine.length === 1 || (lastLineClean.length > 0 && lastLineClean.length <= 8)) {
state.widows.push(para);
}
}
}
}
} catch (e) {
appendLog("Error scanning widows: " + e.message);
}

lblWidowCount.text = "" + state.widows.length;
appendLog("Widows: " + state.widows.length + " found.");
return state.widows.length;
}

function reviewWidow() {
if (state.widows.length === 0) {
appendLog("No widows to review.");
return;
}
if (state.widowIndex >= state.widows.length) {
state.widowIndex = 0;
}

var para = state.widows[state.widowIndex];
try {
if (para.isValid) {
var lastLine = para.lines.lastItem();
app.select(lastLine, SelectionOptions.REPLACE_WITH);
zoomToSelectedTarget(lastLine);
var page = getItemPage(para);
appendLog("Reviewing Widow (" + (state.widowIndex + 1) + " of " + state.widows.length + ") on " + (page ? "Page " + page.name : "Pasteboard") + ": \"" + lastLine.contents.replace(/[\r\n]+$/, "") + "\"");
state.widowIndex++;
} else {
state.widowIndex++;
reviewWidow();
}
} catch (e) {
appendLog("Error selecting widow: " + e.message);
}
}

function fixCurrentWidow() {
if (state.widows.length === 0) {
appendLog("No widow found to fix.");
return;
}

var targetIdx = state.widowIndex > 0 ? (state.widowIndex - 1) : 0;
if (targetIdx >= state.widows.length) {
targetIdx = 0;
}

var para = state.widows[targetIdx];
try {
if (para && para.isValid && para.words.length >= 2) {
var words = para.words;
var lastWord = words.lastItem();
var wordBeforeLast = words.item(words.length - 2);

var story = para.parentStory;
var lastCharOfPrev = wordBeforeLast.characters.lastItem();
var firstCharOfLast = lastWord.characters.firstItem();

var startIdx = lastCharOfPrev.index + 1;
var endIdx = firstCharOfLast.index - 1;

if (startIdx <= endIdx) {
var spaceRange = story.characters.itemByRange(startIdx, endIdx);
if (spaceRange.isValid) {
spaceRange.contents = SpecialCharacters.NONBREAKING_SPACE;
}
} else {
wordBeforeLast.insertionPoints.lastItem().contents = SpecialCharacters.NONBREAKING_SPACE;
}

var page = getItemPage(para);
state.widows.splice(targetIdx, 1);
lblWidowCount.text = "" + state.widows.length;
appendLog("Fixed 1 widow on " + (page ? "Page " + page.name : "Pasteboard") + ". Remaining: " + state.widows.length);

if (state.widows.length > 0) {
if (targetIdx >= state.widows.length) {
state.widowIndex = 0;
} else {
state.widowIndex = targetIdx;
}
reviewWidow();
} else {
lblStatus.text = "Status: Widows Cleared";
}
} else {
state.widows.splice(targetIdx, 1);
lblWidowCount.text = "" + state.widows.length;
if (state.widows.length > 0) reviewWidow();
}
} catch (e) {
appendLog("Error fixing widow: " + e.message);
}
}

function fixAllWidows(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return;

if (state.widows.length === 0) {
scanWidows(doc);
}
if (state.widows.length === 0) {
appendLog("No widows to fix.");
return;
}

if (!confirm("Are you sure you want to fix all " + state.widows.length + " detected widow(s) across the document?", false, "Confirm Fix All Widows")) {
return;
}

app.doScript(function () {
var fixedCount = 0;
var prevRedraw = app.scriptPreferences.enableRedraw;

try {
app.scriptPreferences.enableRedraw = false;
for (var i = 0; i < state.widows.length; i++) {
var para = state.widows[i];
try {
if (para && para.isValid && para.words.length >= 2) {
var words = para.words;
var lastWord = words.lastItem();
var wordBeforeLast = words.item(words.length - 2);

var story = para.parentStory;
var lastCharOfPrev = wordBeforeLast.characters.lastItem();
var firstCharOfLast = lastWord.characters.firstItem();

var startIdx = lastCharOfPrev.index + 1;
var endIdx = firstCharOfLast.index - 1;

if (startIdx <= endIdx) {
var spaceRange = story.characters.itemByRange(startIdx, endIdx);
if (spaceRange.isValid) {
spaceRange.contents = SpecialCharacters.NONBREAKING_SPACE;
fixedCount++;
}
} else {
wordBeforeLast.insertionPoints.lastItem().contents = SpecialCharacters.NONBREAKING_SPACE;
fixedCount++;
}
}
} catch (eOne) {}
}
} catch (eAll) {
appendLog("Error fixing widows: " + eAll.message);
} finally {
app.scriptPreferences.enableRedraw = prevRedraw;
}

appendLog("Inserted non-breaking space on " + fixedCount + " paragraph(s) to eliminate widows.");
scanWidows(doc);
}, ScriptLanguage.JAVASCRIPT, undefined, UndoModes.ENTIRE_SCRIPT, "QC Fix All Widows");
}

function fixWidows(doc) {
fixAllWidows(doc);
}

// -------------------------------------------------------------
// QC Check 4: Image Frames with Fill Color (Safe DOM check)
// -------------------------------------------------------------
function scanImageFramesWithFill(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return 0;

state.imageFramesWithFill = [];
state.imageFrameIndex = 0;

try {
var noneSwatch = doc.swatches.itemByName("None");
var seen = {};

var graphics = doc.allGraphics;
for (var i = 0; i < graphics.length; i++) {
var g = graphics[i];
var frame = g.parent;
if (frame && frame.isValid) {
var frameId = "" + frame.id;
if (!seen[frameId]) {
try {
var fc = frame.fillColor;
if (fc && fc.isValid) {
var fcName = fc.name;
if (fcName !== "None" && fcName !== "[None]" && fcName !== "" && fc !== noneSwatch) {
var fTint = -1;
try { fTint = frame.fillTint; } catch (errT) {}
if (fTint === -1 || fTint > 0) {
seen[frameId] = true;
state.imageFramesWithFill.push({
frame: frame,
fillName: fcName,
tint: fTint
});
}
}
}
} catch (errF) {}
}
}
}

var allItems = doc.allPageItems;
for (var j = 0; j < allItems.length; j++) {
var item = allItems[j];
try {
var hasG = false;
try {
if (item.allGraphics && item.allGraphics.length > 0) hasG = true;
else if (item.graphics && item.graphics.length > 0) hasG = true;
} catch (eG) {}

if (hasG) {
var itemId = "" + item.id;
if (!seen[itemId]) {
var ifc = item.fillColor;
if (ifc && ifc.isValid) {
var ifcName = ifc.name;
if (ifcName !== "None" && ifcName !== "[None]" && ifcName !== "" && ifc !== noneSwatch) {
var itint = -1;
try { itint = item.fillTint; } catch (eT) {}
if (itint === -1 || itint > 0) {
seen[itemId] = true;
state.imageFramesWithFill.push({
frame: item,
fillName: ifcName,
tint: itint
});
}
}
}
}
}
} catch (eItem) {}
}
} catch (e) {
appendLog("Error checking image frames: " + e.message);
}

lblImageFrameCount.text = "" + state.imageFramesWithFill.length;
appendLog("Image Frames with Fill Color: " + state.imageFramesWithFill.length + " found.");
return state.imageFramesWithFill.length;
}

function reviewImageFrame() {
if (state.imageFramesWithFill.length === 0) {
appendLog("No image frames with fill color found.");
return;
}
if (state.imageFrameIndex >= state.imageFramesWithFill.length) {
state.imageFrameIndex = 0;
}

var item = state.imageFramesWithFill[state.imageFrameIndex];
var frame = item.frame;
try {
if (frame && frame.isValid) {
app.select(frame, SelectionOptions.REPLACE_WITH);
zoomToSelectedTarget(frame);
var page = getItemPage(frame);
appendLog("Reviewing Image Frame (" + (state.imageFrameIndex + 1) + " of " + state.imageFramesWithFill.length + ") on " + (page ? "Page " + page.name : "Pasteboard") + " [Fill: " + item.fillName + "]");
state.imageFrameIndex++;
} else {
state.imageFrameIndex++;
reviewImageFrame();
}
} catch (e) {
appendLog("Error selecting frame: " + e.message);
}
}

function clearCurrentImgFill() {
if (state.imageFramesWithFill.length === 0) {
appendLog("No image frame fill to clear.");
return;
}

var targetIdx = state.imageFrameIndex > 0 ? (state.imageFrameIndex - 1) : 0;
if (targetIdx >= state.imageFramesWithFill.length) targetIdx = 0;

var item = state.imageFramesWithFill[targetIdx];
var frame = item.frame;
try {
if (frame && frame.isValid) {
var doc = getActiveDocument();
frame.fillColor = doc.swatches.itemByName("None");
var page = getItemPage(frame);
state.imageFramesWithFill.splice(targetIdx, 1);
lblImageFrameCount.text = "" + state.imageFramesWithFill.length;
appendLog("Cleared fill color on Image Frame (" + (page ? "Page " + page.name : "Pasteboard") + "). Remaining: " + state.imageFramesWithFill.length);

if (state.imageFramesWithFill.length > 0) {
if (targetIdx >= state.imageFramesWithFill.length) state.imageFrameIndex = 0;
else state.imageFrameIndex = targetIdx;
reviewImageFrame();
} else {
state.imageFrameIndex = 0;
appendLog("All image frame fill colors cleared!");
}
} else {
state.imageFramesWithFill.splice(targetIdx, 1);
lblImageFrameCount.text = "" + state.imageFramesWithFill.length;
clearCurrentImgFill();
}
} catch (e) {
appendLog("Error clearing frame fill: " + e.message);
}
}

// -------------------------------------------------------------
// QC Check 5: Black Color Issues (Strict Tint 80% or 100% Solid Only; No Rich Black)
// -------------------------------------------------------------
function isRichBlack(swatch) {
if (!swatch || !swatch.isValid) return false;
var name = "";
try { name = (swatch.name || "").toLowerCase(); } catch (eN) {}

if (name === "[registration]" || name === "registration") {
return false;
}

if (name.indexOf("rich black") !== -1 || name.indexOf("4 color black") !== -1 ||
name.indexOf("4-color black") !== -1 || name.indexOf("4-colour black") !== -1) {
return true;
}

try {
if (swatch.space === ColorSpace.CMYK) {
var cv = swatch.colorValue;
if (cv && cv.length >= 4) {
var c = cv[0], m = cv[1], y = cv[2], k = cv[3];
// 4-color black: all 4 CMYK channels present with high K
if (c > 0 && m > 0 && y > 0 && k >= 40) {
return true;
}
// Or high total ink with dominant K and other process inks
if (k >= 60 && (c + m + y) >= 20) {
return true;
}
}
}
} catch (eCv) {}
return false;
}

function isBlackColor(swatch) {
if (!swatch || !swatch.isValid) return false;
var name = "";
try { name = swatch.name; } catch (eN) {}
if (name === "Black" || name === "[Black]") {
return true;
}
if (isRichBlack(swatch)) {
return true;
}
try {
if (swatch.space === ColorSpace.CMYK) {
var cv = swatch.colorValue;
if (cv && cv.length >= 4) {
// Pure K (1-color black)
if (cv[0] === 0 && cv[1] === 0 && cv[2] === 0 && cv[3] > 0) {
return true;
}
if (name.toLowerCase().indexOf("black") !== -1 && cv[3] > 0) {
return true;
}
}
}
} catch (e) {}
return false;
}

function getEffectiveTint(swatch, appliedTint) {
var baseK = 100;
try {
if (swatch.space === ColorSpace.CMYK) {
var cv = swatch.colorValue;
if (cv && cv[0] === 0 && cv[1] === 0 && cv[2] === 0) {
baseK = cv[3];
}
}
} catch (e) {}
if (appliedTint === -1) {
return Math.round(baseK);
}
return Math.round((baseK * appliedTint) / 100);
}

function isApprovedBlack(swatch, appliedTint) {
if (!swatch || !swatch.isValid) return false;

// Rich Black (4-color black) is strictly NOT allowed!
if (isRichBlack(swatch)) {
return false;
}

var effK = getEffectiveTint(swatch, appliedTint);
// Only 100% Solid Black and 80% Black Tint are approved!
return (effK === 100 || effK === 80);
}

function scanBlackColorIssues(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return 0;

// Auto-add unnamed colors so unnamed tints of black get identified as swatches
addUnnamedColors(doc);

state.blackIssues = [];
state.blackIssueIndex = 0;

try {
// 1. Text scans
var stories = doc.stories;
for (var s = 0; s < stories.length; s++) {
var story = stories[s];
var ranges = story.textStyleRanges;
for (var r = 0; r < ranges.length; r++) {
var range = ranges[r];
if (range.contents.replace(/[\r\n\s\t]+/g, "").length === 0) {
continue;
}

try {
var fc = range.fillColor;
if (fc && fc.isValid && isBlackColor(fc)) {
if (!isApprovedBlack(fc, range.fillTint)) {
var isRich = isRichBlack(fc);
var effK = isRich ? "4-Color" : getEffectiveTint(fc, range.fillTint);
var sampleText = range.contents;
if (sampleText.length > 22) {
sampleText = sampleText.substring(0, 19) + "...";
}
var descText = isRich ?
("Text Fill Rich Black (4-Color Black - Not Allowed) [\"" + sampleText.replace(/[\r\n]+/g, " ") + "\"]") :
("Text Fill Black " + effK + "% (Approved: 100%/80% only) [\"" + sampleText.replace(/[\r\n]+/g, " ") + "\"]");

state.blackIssues.push({
target: range,
type: "textFill",
effK: isRich ? 100 : effK,
isRich: isRich,
desc: descText
});
}
}
} catch (eF) {}

try {
if (range.strokeWeight > 0) {
var sc = range.strokeColor;
if (sc && sc.isValid && isBlackColor(sc)) {
if (!isApprovedBlack(sc, range.strokeTint)) {
var isRichS = isRichBlack(sc);
var effStrokeK = isRichS ? "4-Color" : getEffectiveTint(sc, range.strokeTint);
var descStroke = isRichS ?
"Text Stroke Rich Black (4-Color Black - Not Allowed)" :
("Text Stroke Black " + effStrokeK + "% (Approved: 100%/80% only)");

state.blackIssues.push({
target: range,
type: "textStroke",
effK: isRichS ? 100 : effStrokeK,
isRich: isRichS,
desc: descStroke
});
}
}
}
} catch (eS) {}
}
}

// 2. PageItems scans
var pageItems = doc.allPageItems;
for (var i = 0; i < pageItems.length; i++) {
var item = pageItems[i];

try {
var ifc = item.fillColor;
if (ifc && ifc.isValid && isBlackColor(ifc)) {
var ift = -1;
try { ift = item.fillTint; } catch (eT) {}
if (!isApprovedBlack(ifc, ift)) {
var isRichIF = isRichBlack(ifc);
var effItemK = isRichIF ? "4-Color" : getEffectiveTint(ifc, ift);
var descIF = isRichIF ?
(item.constructor.name + " Fill Rich Black (4-Color Black - Not Allowed)") :
(item.constructor.name + " Fill Black " + effItemK + "% (Approved: 100%/80% only)");

state.blackIssues.push({
target: item,
type: "itemFill",
effK: isRichIF ? 100 : effItemK,
isRich: isRichIF,
desc: descIF
});
}
}
} catch (eIF) {}

try {
var isw = 0;
try { isw = item.strokeWeight; } catch (eSW) {}
if (isw > 0) {
var isc = item.strokeColor;
if (isc && isc.isValid && isBlackColor(isc)) {
var ist = -1;
try { ist = item.strokeTint; } catch (eST) {}
if (!isApprovedBlack(isc, ist)) {
var isRichIS = isRichBlack(isc);
var effItemStrokeK = isRichIS ? "4-Color" : getEffectiveTint(isc, ist);
var descIS = isRichIS ?
(item.constructor.name + " Stroke Rich Black (4-Color Black - Not Allowed)") :
(item.constructor.name + " Stroke Black " + effItemStrokeK + "% (Approved: 100%/80% only)");

state.blackIssues.push({
target: item,
type: "itemStroke",
effK: isRichIS ? 100 : effItemStrokeK,
isRich: isRichIS,
desc: descIS
});
}
}
}
} catch (eIS) {}
}
} catch (e) {
appendLog("Error scanning black color issues: " + e.message);
}

lblBlackColorCount.text = "" + state.blackIssues.length;
appendLog("Black Color Issues: " + state.blackIssues.length + " found.");
return state.blackIssues.length;
}

function reviewBlackColorIssue() {
if (state.blackIssues.length === 0) {
appendLog("No black color issues to review.");
return;
}
if (state.blackIssueIndex >= state.blackIssues.length) {
state.blackIssueIndex = 0;
}

var issue = state.blackIssues[state.blackIssueIndex];
try {
if (issue.target.isValid) {
app.select(issue.target, SelectionOptions.REPLACE_WITH);
zoomToSelectedTarget(issue.target);
var page = getItemPage(issue.target);
var pageName = page ? "Page " + page.name : "Pasteboard";
appendLog("Reviewing Black Issue (" + (state.blackIssueIndex + 1) + " of " + state.blackIssues.length + ") on " + pageName + ": " + issue.desc);
state.blackIssueIndex++;
} else {
state.blackIssueIndex++;
reviewBlackColorIssue();
}
} catch (e) {
appendLog("Error navigating to black issue: " + e.message);
}
}

function fixCurrentBlackIssue() {
if (state.blackIssues.length === 0) {
appendLog("No black color issue found to fix.");
return;
}

var targetIdx = state.blackIssueIndex > 0 ? (state.blackIssueIndex - 1) : 0;
if (targetIdx >= state.blackIssues.length) {
targetIdx = 0;
}

var issue = state.blackIssues[targetIdx];
var doc = getActiveDocument();
if (!doc) return;

var blackSwatch = doc.swatches.itemByName("[Black]");
if (!blackSwatch || !blackSwatch.isValid) {
blackSwatch = doc.swatches.itemByName("Black");
}

try {
if (issue.target && issue.target.isValid) {
var targetTint = (!issue.isRich && issue.effK >= 70 && issue.effK < 90) ? 80 : 100;
if (issue.type === "textFill") {
issue.target.fillColor = blackSwatch;
issue.target.fillTint = targetTint;
} else if (issue.type === "textStroke") {
issue.target.strokeColor = blackSwatch;
issue.target.strokeTint = targetTint;
} else if (issue.type === "itemFill") {
issue.target.fillColor = blackSwatch;
try { issue.target.fillTint = targetTint; } catch (eT1) {}
} else if (issue.type === "itemStroke") {
issue.target.strokeColor = blackSwatch;
try { issue.target.strokeTint = targetTint; } catch (eT2) {}
}

var page = getItemPage(issue.target);
var pageName = page ? "Page " + page.name : "Pasteboard";
state.blackIssues.splice(targetIdx, 1);
lblBlackColorCount.text = "" + state.blackIssues.length;
appendLog("Fixed 1 Black issue to " + targetTint + "% on " + pageName + ". Remaining: " + state.blackIssues.length);

if (state.blackIssues.length > 0) {
if (targetIdx >= state.blackIssues.length) {
state.blackIssueIndex = 0;
} else {
state.blackIssueIndex = targetIdx;
}
reviewBlackColorIssue();
} else {
lblStatus.text = "Status: Black Issues Cleared";
}
} else {
state.blackIssues.splice(targetIdx, 1);
lblBlackColorCount.text = "" + state.blackIssues.length;
if (state.blackIssues.length > 0) reviewBlackColorIssue();
}
} catch (e) {
appendLog("Error fixing black issue: " + e.message);
}
}

function fixAllBlackIssues(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return;

if (state.blackIssues.length === 0) {
scanBlackColorIssues(doc);
}
if (state.blackIssues.length === 0) {
appendLog("No black color issues to fix.");
return;
}

if (!confirm("Are you sure you want to fix all " + state.blackIssues.length + " unapproved black issue(s) across the document to approved values (100% Solid Black / 80% Tint)?", false, "Confirm Fix All Black Issues")) {
return;
}

app.doScript(function () {
var blackSwatch = doc.swatches.itemByName("[Black]");
if (!blackSwatch || !blackSwatch.isValid) {
blackSwatch = doc.swatches.itemByName("Black");
}

var fixedCount = 0;
var prevRedraw = app.scriptPreferences.enableRedraw;
try {
app.scriptPreferences.enableRedraw = false;
for (var i = 0; i < state.blackIssues.length; i++) {
var issue = state.blackIssues[i];
try {
if (issue.target && issue.target.isValid) {
var targetTint = (!issue.isRich && issue.effK >= 70 && issue.effK < 90) ? 80 : 100;
if (issue.type === "textFill") {
issue.target.fillColor = blackSwatch;
issue.target.fillTint = targetTint;
fixedCount++;
} else if (issue.type === "textStroke") {
issue.target.strokeColor = blackSwatch;
issue.target.strokeTint = targetTint;
fixedCount++;
} else if (issue.type === "itemFill") {
issue.target.fillColor = blackSwatch;
try { issue.target.fillTint = targetTint; } catch (eT1) {}
fixedCount++;
} else if (issue.type === "itemStroke") {
issue.target.strokeColor = blackSwatch;
try { issue.target.strokeTint = targetTint; } catch (eT2) {}
fixedCount++;
}
}
} catch (eOne) {}
}
appendLog("Fixed " + fixedCount + " black color issue(s) across document.");
scanBlackColorIssues(doc);
} catch (eAll) {
appendLog("Error fixing all black issues: " + eAll.message);
} finally {
app.scriptPreferences.enableRedraw = prevRedraw;
}
}, ScriptLanguage.JAVASCRIPT, undefined, UndoModes.ENTIRE_SCRIPT, "QC Fix All Black Issues");
}

// -------------------------------------------------------------
// QC Check 6: Indian / Disallowed URLs (.in / com/in)
// -------------------------------------------------------------
function isDisallowedIndianUrl(url) {
if (!url) return false;
var cleanUrl = url.toLowerCase();
if (/(?:\.in|\.co\.in|\.org\.in|\.net\.in)(?:[\/\?#]|$)/i.test(cleanUrl)) {
return true;
}
if (/(?:com\/in|\/en-in|\/in)(?:[\/\?#]|$)/i.test(cleanUrl)) {
return true;
}
return false;
}

function scanDisallowedUrls(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return 0;

state.disallowedUrls = [];
state.disallowedUrlIndex = 0;

try {
var hyperlinks = doc.hyperlinks;
for (var h = 0; h < hyperlinks.length; h++) {
var hl = hyperlinks[h];
var dest = hl.destination;
if (dest && dest.hasOwnProperty("destinationURL")) {
var url = dest.destinationURL;
if (isDisallowedIndianUrl(url)) {
var sourceItem = null;
if (hl.source) {
if (hl.source.hasOwnProperty("sourceText")) {
sourceItem = hl.source.sourceText;
} else if (hl.source.hasOwnProperty("sourcePageItem")) {
sourceItem = hl.source.sourcePageItem;
}
}
state.disallowedUrls.push({
url: url,
target: sourceItem,
type: "Hyperlink Object"
});
}
}
}

app.findGrepPreferences = NothingEnum.nothing;
app.findGrepPreferences.findWhat = "(?i)\\b(?:https?:\\/\\/|www\\.)[^\\s<>'\"\\)\\]]+";

var textMatches = doc.findGrep();
for (var m = 0; m < textMatches.length; m++) {
var matchText = textMatches[m].contents;
if (isDisallowedIndianUrl(matchText)) {
state.disallowedUrls.push({
url: matchText,
target: textMatches[m],
type: "Text URL"
});
}
}
} catch (e) {
appendLog("Error scanning disallowed URLs: " + e.message);
} finally {
app.findGrepPreferences = NothingEnum.nothing;
}

lblUrlCount.text = "" + state.disallowedUrls.length;
appendLog("Disallowed URLs (.in): " + state.disallowedUrls.length + " found.");
return state.disallowedUrls.length;
}

function reviewDisallowedUrl() {
if (state.disallowedUrls.length === 0) {
appendLog("No disallowed URLs to review.");
return;
}
if (state.disallowedUrlIndex >= state.disallowedUrls.length) {
state.disallowedUrlIndex = 0;
}

var item = state.disallowedUrls[state.disallowedUrlIndex];
try {
if (item.target && item.target.isValid) {
app.select(item.target, SelectionOptions.REPLACE_WITH);
zoomToSelectedTarget(item.target);
var page = getItemPage(item.target);
var pageName = page ? "Page " + page.name : "Pasteboard";
appendLog("Reviewing URL (" + (state.disallowedUrlIndex + 1) + " of " + state.disallowedUrls.length + ") on " + pageName + " [" + item.type + "]: " + item.url);
state.disallowedUrlIndex++;
} else {
appendLog("URL (" + (state.disallowedUrlIndex + 1) + " of " + state.disallowedUrls.length + "): " + item.url);
state.disallowedUrlIndex++;
if (state.disallowedUrlIndex < state.disallowedUrls.length) {
reviewDisallowedUrl();
}
}
} catch (e) {
appendLog("Error navigating to disallowed URL: " + e.message);
}
}

// -------------------------------------------------------------
// QC Check 7: Hyphenation Check & Uncheck All
// -------------------------------------------------------------
function scanHyphenation(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return 0;

state.hyphenations = [];
state.hyphenationIndex = 0;

try {
var stories = doc.stories;
for (var s = 0; s < stories.length; s++) {
var story = stories[s];
var paras = story.paragraphs;
for (var p = 0; p < paras.length; p++) {
var para = paras[p];
if (para.hyphenation === true) {
var words = para.words;
var hasBrokenWord = false;

for (var w = 0; w < words.length; w++) {
var word = words[w];
if (word.lines.length > 1) {
state.hyphenations.push({
target: word,
para: para,
desc: "Hyphenated Word: \"" + word.contents + "\""
});
hasBrokenWord = true;
}
}

if (!hasBrokenWord && para.lines.length > 1) {
state.hyphenations.push({
target: para,
para: para,
desc: "Hyphenate Enabled on Paragraph (" + para.lines.length + " lines)"
});
}
}
}
}
} catch (e) {
appendLog("Error scanning hyphenated text: " + e.message);
}

lblHyphenCount.text = "" + state.hyphenations.length;
appendLog("Hyphenation: " + state.hyphenations.length + " occurrences found.");
return state.hyphenations.length;
}

function reviewHyphenation() {
if (state.hyphenations.length === 0) {
appendLog("No hyphenation to review.");
return;
}
if (state.hyphenationIndex >= state.hyphenations.length) {
state.hyphenationIndex = 0;
}

var item = state.hyphenations[state.hyphenationIndex];
try {
if (item.target.isValid) {
app.select(item.target, SelectionOptions.REPLACE_WITH);
zoomToSelectedTarget(item.target);
var page = getItemPage(item.target);
appendLog("Reviewing Hyphenation (" + (state.hyphenationIndex + 1) + " of " + state.hyphenations.length + ") on " + (page ? "Page " + page.name : "Pasteboard") + ": " + item.desc);
state.hyphenationIndex++;
} else {
state.hyphenationIndex++;
reviewHyphenation();
}
} catch (e) {
appendLog("Error navigating to hyphenation: " + e.message);
}
}

function uncheckAllHyphenation(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return;

if (!confirm("Are you sure you want to uncheck hyphenation across all paragraphs in the document?", false, "Confirm Uncheck Hyphenation")) {
return;
}

app.doScript(function () {
var count = 0;
var prevRedraw = app.scriptPreferences.enableRedraw;
try {
app.scriptPreferences.enableRedraw = false;
var stories = doc.stories;
for (var s = 0; s < stories.length; s++) {
var paras = stories[s].paragraphs;
for (var p = 0; p < paras.length; p++) {
if (paras[p].hyphenation === true) {
paras[p].hyphenation = false;
count++;
}
}
}
appendLog("Unchecked Hyphenate on " + count + " paragraphs across the document.");
scanHyphenation(doc);
} catch (e) {
appendLog("Error unchecking hyphenation: " + e.message);
} finally {
app.scriptPreferences.enableRedraw = prevRedraw;
}
}, ScriptLanguage.JAVASCRIPT, undefined, UndoModes.ENTIRE_SCRIPT, "QC Uncheck Hyphenation");
}

// -------------------------------------------------------------
// QC Check 8: Ligatures Check & Turn Off Fix
// -------------------------------------------------------------
function scanLigatures(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return 0;

state.ligatures = [];
state.ligatureIndex = 0;

try {
var stories = doc.stories;
for (var s = 0; s < stories.length; s++) {
var ranges = stories[s].textStyleRanges;
for (var r = 0; r < ranges.length; r++) {
var range = ranges[r];
if (range.contents.replace(/[\r\n\s\t]+/g, "").length === 0) continue;

if (range.ligatures === true) {
if (/(fi|fl|ff|ffi|ffl|[fiflffffiffl])/i.test(range.contents)) {
state.ligatures.push(range);
}
}
}
}
} catch (e) {
appendLog("Error scanning ligatures: " + e.message);
}

lblLigatureCount.text = "" + state.ligatures.length;
appendLog("Ligatures Used: " + state.ligatures.length + " ranges found.");
return state.ligatures.length;
}

function reviewLigature() {
if (state.ligatures.length === 0) {
appendLog("No ligatures to review.");
return;
}
if (state.ligatureIndex >= state.ligatures.length) {
state.ligatureIndex = 0;
}

var range = state.ligatures[state.ligatureIndex];
try {
if (range.isValid) {
app.select(range, SelectionOptions.REPLACE_WITH);
zoomToSelectedTarget(range);
var page = getItemPage(range);
var sample = range.contents;
if (sample.length > 22) sample = sample.substring(0, 19) + "...";
appendLog("Reviewing Ligature (" + (state.ligatureIndex + 1) + " of " + state.ligatures.length + ") on " + (page ? "Page " + page.name : "Pasteboard") + ": \"" + sample.replace(/[\r\n]+/g, " ") + "\"");
state.ligatureIndex++;
} else {
state.ligatureIndex++;
reviewLigature();
}
} catch (e) {
appendLog("Error navigating to ligature: " + e.message);
}
}

function fixCurrentLigature() {
if (state.ligatures.length === 0) {
appendLog("No ligature found to fix.");
return;
}

var targetIdx = state.ligatureIndex > 0 ? (state.ligatureIndex - 1) : 0;
if (targetIdx >= state.ligatures.length) targetIdx = 0;

var range = state.ligatures[targetIdx];
try {
if (range.isValid) {
range.ligatures = false;
state.ligatures.splice(targetIdx, 1);
lblLigatureCount.text = "" + state.ligatures.length;
appendLog("Turned off ligatures on selected text. Remaining: " + state.ligatures.length);

if (state.ligatures.length > 0) {
if (targetIdx >= state.ligatures.length) state.ligatureIndex = 0;
else state.ligatureIndex = targetIdx;
reviewLigature();
} else {
state.ligatureIndex = 0;
appendLog("All ligatures turned off!");
}
} else {
state.ligatures.splice(targetIdx, 1);
lblLigatureCount.text = "" + state.ligatures.length;
fixCurrentLigature();
}
} catch (e) {
appendLog("Error fixing ligature: " + e.message);
}
}

function fixAllLigatures(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return;

if (!confirm("Are you sure you want to turn off ligatures across the entire document?", false, "Confirm Turn Off All Ligatures")) {
return;
}

app.doScript(function () {
var count = 0;
var prevRedraw = app.scriptPreferences.enableRedraw;
try {
app.scriptPreferences.enableRedraw = false;
var stories = doc.stories;
for (var s = 0; s < stories.length; s++) {
var ranges = stories[s].textStyleRanges;
for (var r = 0; r < ranges.length; r++) {
if (ranges[r].ligatures === true) {
ranges[r].ligatures = false;
count++;
}
}
}
appendLog("Turned off ligatures on " + count + " text ranges across document.");
scanLigatures(doc);
} catch (e) {
appendLog("Error turning off ligatures: " + e.message);
} finally {
app.scriptPreferences.enableRedraw = prevRedraw;
}
}, ScriptLanguage.JAVASCRIPT, undefined, UndoModes.ENTIRE_SCRIPT, "QC Turn Off Ligatures");
}

// -------------------------------------------------------------
// Zoom Helper (Maintains user's current zoom level like native Find/Change)
// -------------------------------------------------------------
function zoomToSelectedTarget(target) {
try {
var loc = getItemLocation(target);
var curZoom = 100;
try {
curZoom = app.activeWindow.zoomPercentage;
} catch (eZ) {}

// 1. Switch active spread if target is on a different spread
if (loc.spread && loc.spread.isValid) {
if (app.activeWindow.activeSpread !== loc.spread) {
app.activeWindow.activeSpread = loc.spread;
}
}

// 2. Set active page on that spread
if (loc.page && loc.page.isValid) {
app.activeWindow.activePage = loc.page;
}

// 3. Center viewport on the selected item while keeping current zoom level
try {
var act = null;
try { act = app.menuActions.itemByName("$ID/Fit Selection in Window"); } catch (e1) {}
if (!act || !act.isValid) {
try { act = app.menuActions.itemByName("Fit Selection in Window"); } catch (e2) {}
}
if (act && act.isValid && act.enabled) {
act.invoke();
}
} catch (eFit) {}

// 4. Restore the user's exact zoom level (e.g. 150%, 200%, etc.)
// so it never forces "fit page" or "fit frame", behaving exactly like native Find/Change!
if (curZoom && curZoom > 0) {
app.activeWindow.zoomPercentage = curZoom;
}
} catch (e) {}
}

// -------------------------------------------------------------
// Full QC Runner & Pristine Reset
// -------------------------------------------------------------
function pristineReset() {
state.doubleSpaces = [];
state.doubleSpaceIndex = 0;
state.widows = [];
state.widowIndex = 0;
state.imageFramesWithFill = [];
state.imageFrameIndex = 0;
state.blackIssues = [];
state.blackIssueIndex = 0;
state.disallowedUrls = [];
state.disallowedUrlIndex = 0;
state.hyphenations = [];
state.hyphenationIndex = 0;
state.ligatures = [];
state.ligatureIndex = 0;
state.unusedSwatches = [];
state.unusedSwatchesCount = 0;

lblUnusedCount.text = "0";
lblDoubleSpaceCount.text = "0";
lblWidowCount.text = "0";
lblImageFrameCount.text = "0";
lblBlackColorCount.text = "0";
lblUrlCount.text = "0";
lblHyphenCount.text = "0";
lblLigatureCount.text = "0";

var doc = getActiveDocument();
lblDocName.text = "Document: " + (doc ? doc.name : "None");
lblStatus.text = "Status: Ready";

try {
app.select(NothingEnum.nothing);
} catch (e) {}

reportBox.text = "QC Master Dashboard ready.\nClick 'Run Full QC' or individual [Scan] buttons to begin.\n";
}

function runFullQC() {
var doc = getActiveDocument();
if (!doc) {
alert("Please open an InDesign document before running QC.", "No Document");
return;
}

lblDocName.text = "Document: " + doc.name;
lblStatus.text = "Status: Running...";

var prevRedraw = app.scriptPreferences.enableRedraw;
try {
// Speed optimization: suppress screen redraws during multi-page audits (50+ pages)
app.scriptPreferences.enableRedraw = false;

// Step 1: Detect and add any unnamed colors to the Swatches panel
addUnnamedColors(doc);

var cSwatches = scanUnusedSwatches(doc);
var cDouble = scanDoubleSpaces(doc);
var cWidows = scanWidows(doc);
var cImgFill = scanImageFramesWithFill(doc);
var cBlack = scanBlackColorIssues(doc);
var cUrls = scanDisallowedUrls(doc);
var cHyphens = scanHyphenation(doc);
var cLigatures = scanLigatures(doc);

var total = cSwatches + cDouble + cWidows + cImgFill + cBlack + cUrls + cHyphens + cLigatures;
lblStatus.text = "Status: Complete (" + total + " issues)";

// Post summary header at the top
appendLog("=========================================");
appendLog("Full QC Complete for: " + doc.name + " (" + total + " total issues)");
appendLog("=========================================");
} catch (eQC) {
lblStatus.text = "Status: Error";
appendLog("Error during Full QC: " + eQC.message);
} finally {
app.scriptPreferences.enableRedraw = prevRedraw;
}
}

function refreshQC() {
var doc = getActiveDocument();
if (!doc) {
lblDocName.text = "Document: None";
lblStatus.text = "Status: No document open";
return;
}
lblDocName.text = "Document: " + doc.name;
lblStatus.text = "Status: Ready";
runFullQC();
}

function exportReport() {
var reportContent = reportBox.text;
if (!reportContent || reportContent.length === 0) {
alert("The report log is empty. Run QC before exporting.", "Empty Log");
return;
}

var doc = getActiveDocument();
var defaultPath = "~/Desktop";
if (doc && doc.saved) {
defaultPath = doc.filePath;
}

var fileName = (doc ? doc.name.replace(/\.indd$/i, "") : "Document") + "_QC_Report.txt";
var file = new File(defaultPath + "/" + fileName).saveDlg("Save QC Audit Report", "Text File:*.txt");
if (file) {
file.encoding = "UTF-8";
file.open("w");
file.write(reportContent);
file.close();
appendLog("Report successfully saved to: " + file.fsName);
}
}

// -------------------------------------------------------------
// UI Construction (Compact 2-Column Layout)
// -------------------------------------------------------------
if ($.global.qcmaster_win && $.global.qcmaster_win instanceof Window) {
try { $.global.qcmaster_win.close(); } catch (eW) {}
}
var win = new Window("palette", "QC Master Dashboard v2.2 | Prepress Audit & Fix Hub", undefined, { resizeable: true });
$.global.qcmaster_win = win;
win.orientation = "column";
win.alignChildren = ["fill", "top"];
win.margins = 10;
win.spacing = 6;

// --- Compact Header: Document Information & Actions ---
var pnlHeader = win.add("panel", undefined, "Document & Actions");
pnlHeader.orientation = "column";
pnlHeader.alignChildren = ["fill", "top"];
pnlHeader.margins = 8;
pnlHeader.spacing = 6;

var headerRow1 = pnlHeader.add("group");
headerRow1.orientation = "row";
headerRow1.alignChildren = ["left", "center"];
headerRow1.spacing = 16;

var initialDoc = getActiveDocument();
var docNameText = initialDoc ? initialDoc.name : "No active document";
var lblDocName = headerRow1.add("statictext", undefined, "Document: " + docNameText);
lblDocName.preferredSize.width = 340;
var lblStatus = headerRow1.add("statictext", undefined, "Status: Ready");
lblStatus.preferredSize.width = 250;

var headerRow2 = pnlHeader.add("group");
headerRow2.orientation = "row";
headerRow2.alignChildren = ["left", "center"];
headerRow2.spacing = 8;

var btnRunQC = headerRow2.add("button", undefined, "Run Full QC");
btnRunQC.preferredSize = [105, 24];

var btnReset = headerRow2.add("button", undefined, "Reset");
btnReset.preferredSize = [60, 24];

var btnRefresh = headerRow2.add("button", undefined, "Refresh");
btnRefresh.preferredSize = [70, 24];

var btnExport = headerRow2.add("button", undefined, "Export Report");
btnExport.preferredSize = [100, 24];

// --- Main 2-Column Container ---
var mainCols = win.add("group");
mainCols.orientation = "row";
mainCols.alignChildren = ["fill", "top"];
mainCols.spacing = 8;

// =============================================================
// COLUMN 1: Text & Typography
// =============================================================
var col1 = mainCols.add("panel", undefined, "Typography & Text");
col1.orientation = "column";
col1.alignChildren = ["fill", "top"];
col1.preferredSize.width = 315;
col1.margins = 8;
col1.spacing = 7;

// Row 1.1: Double Spaces
var rowDouble = col1.add("group");
rowDouble.orientation = "row";
rowDouble.alignChildren = ["left", "center"];
rowDouble.spacing = 4;
var lblDoubleTitle = rowDouble.add("statictext", undefined, "Double Space:");
lblDoubleTitle.preferredSize.width = 82;
var lblDoubleSpaceCount = rowDouble.add("statictext", undefined, "0");
lblDoubleSpaceCount.preferredSize.width = 24;
var btnScanDouble = rowDouble.add("button", undefined, "Scan");
btnScanDouble.preferredSize = [38, 21];
var btnReviewDoubleSpace = rowDouble.add("button", undefined, "Rev");
btnReviewDoubleSpace.preferredSize = [34, 21];
var btnFixCurrentDoubleSpace = rowDouble.add("button", undefined, "Fix");
btnFixCurrentDoubleSpace.preferredSize = [32, 21];
var btnFixDoubleSpaces = rowDouble.add("button", undefined, "All");
btnFixDoubleSpaces.preferredSize = [32, 21];

// Row 1.2: Widow Check (Scan | Rev | Fix | All)
var rowWidow = col1.add("group");
rowWidow.orientation = "row";
rowWidow.alignChildren = ["left", "center"];
rowWidow.spacing = 4;
var lblWidowTitle = rowWidow.add("statictext", undefined, "Widows:");
lblWidowTitle.preferredSize.width = 82;
var lblWidowCount = rowWidow.add("statictext", undefined, "0");
lblWidowCount.preferredSize.width = 24;
var btnScanWidow = rowWidow.add("button", undefined, "Scan");
btnScanWidow.preferredSize = [38, 21];
var btnReviewWidow = rowWidow.add("button", undefined, "Rev");
btnReviewWidow.preferredSize = [34, 21];
var btnFixCurrentWidow = rowWidow.add("button", undefined, "Fix");
btnFixCurrentWidow.preferredSize = [32, 21];
var btnFixAllWidow = rowWidow.add("button", undefined, "All");
btnFixAllWidow.preferredSize = [32, 21];

// Row 1.3: Hyphenated Lines & Uncheck Option
var rowHyphen = col1.add("group");
rowHyphen.orientation = "row";
rowHyphen.alignChildren = ["left", "center"];
rowHyphen.spacing = 4;
var lblHyphenTitle = rowHyphen.add("statictext", undefined, "Hyphenation:");
lblHyphenTitle.preferredSize.width = 82;
var lblHyphenCount = rowHyphen.add("statictext", undefined, "0");
lblHyphenCount.preferredSize.width = 24;
var btnScanHyphen = rowHyphen.add("button", undefined, "Scan");
btnScanHyphen.preferredSize = [38, 21];
var btnReviewHyphen = rowHyphen.add("button", undefined, "Review");
btnReviewHyphen.preferredSize = [48, 21];
var btnUncheckHyphen = rowHyphen.add("button", undefined, "Uncheck");
btnUncheckHyphen.preferredSize = [56, 21];

// Row 1.4: Ligatures Used & Fix
var rowLigature = col1.add("group");
rowLigature.orientation = "row";
rowLigature.alignChildren = ["left", "center"];
rowLigature.spacing = 4;
var lblLigatureTitle = rowLigature.add("statictext", undefined, "Ligatures:");
lblLigatureTitle.preferredSize.width = 82;
var lblLigatureCount = rowLigature.add("statictext", undefined, "0");
lblLigatureCount.preferredSize.width = 24;
var btnScanLigature = rowLigature.add("button", undefined, "Scan");
btnScanLigature.preferredSize = [38, 21];
var btnReviewLigature = rowLigature.add("button", undefined, "Rev");
btnReviewLigature.preferredSize = [34, 21];
var btnFixLigature = rowLigature.add("button", undefined, "Off");
btnFixLigature.preferredSize = [32, 21];
var btnAllLigature = rowLigature.add("button", undefined, "All");
btnAllLigature.preferredSize = [32, 21];

// =============================================================
// COLUMN 2: Colors, Assets & Links
// =============================================================
var col2 = mainCols.add("panel", undefined, "Colors, Frames & Links");
col2.orientation = "column";
col2.alignChildren = ["fill", "top"];
col2.preferredSize.width = 315;
col2.margins = 8;
col2.spacing = 7;

// Row 2.1: Unused Swatches
var rowSwatches = col2.add("group");
rowSwatches.orientation = "row";
rowSwatches.alignChildren = ["left", "center"];
rowSwatches.spacing = 4;
var lblSwatchesTitle = rowSwatches.add("statictext", undefined, "Swatches:");
lblSwatchesTitle.preferredSize.width = 82;
var lblUnusedCount = rowSwatches.add("statictext", undefined, "0");
lblUnusedCount.preferredSize.width = 24;
var btnScanSwatches = rowSwatches.add("button", undefined, "Scan");
btnScanSwatches.preferredSize = [38, 21];
var btnRemoveSwatches = rowSwatches.add("button", undefined, "Remove All");
btnRemoveSwatches.preferredSize = [80, 21];

// Row 2.2: Black Color Issues (Strict 100% Solid & 80% Tint Only; No Rich Black)
var rowBlack = col2.add("group");
rowBlack.orientation = "row";
rowBlack.alignChildren = ["left", "center"];
rowBlack.spacing = 4;
var lblBlackTitle = rowBlack.add("statictext", undefined, "Black Issues:");
lblBlackTitle.preferredSize.width = 82;
var lblBlackColorCount = rowBlack.add("statictext", undefined, "0");
lblBlackColorCount.preferredSize.width = 24;
var btnScanBlack = rowBlack.add("button", undefined, "Scan");
btnScanBlack.preferredSize = [38, 21];
var btnReviewBlack = rowBlack.add("button", undefined, "Rev");
btnReviewBlack.preferredSize = [34, 21];
var btnFixCurrentBlack = rowBlack.add("button", undefined, "Fix");
btnFixCurrentBlack.preferredSize = [32, 21];
var btnFixAllBlack = rowBlack.add("button", undefined, "All");
btnFixAllBlack.preferredSize = [32, 21];

// Row 2.3: Image Frames with Fill (Review | Clear)
var rowImgFill = col2.add("group");
rowImgFill.orientation = "row";
rowImgFill.alignChildren = ["left", "center"];
rowImgFill.spacing = 4;
var lblImgFillTitle = rowImgFill.add("statictext", undefined, "Img Fill Color:");
lblImgFillTitle.preferredSize.width = 82;
var lblImageFrameCount = rowImgFill.add("statictext", undefined, "0");
lblImageFrameCount.preferredSize.width = 24;
var btnScanImgFill = rowImgFill.add("button", undefined, "Scan");
btnScanImgFill.preferredSize = [38, 21];
var btnReviewImageFrame = rowImgFill.add("button", undefined, "Review");
btnReviewImageFrame.preferredSize = [48, 21];
var btnClearImgFill = rowImgFill.add("button", undefined, "Clear");
btnClearImgFill.preferredSize = [40, 21];

// Row 2.4: Disallowed URLs (.in / com/in)
var rowUrls = col2.add("group");
rowUrls.orientation = "row";
rowUrls.alignChildren = ["left", "center"];
rowUrls.spacing = 4;
var lblUrlTitle = rowUrls.add("statictext", undefined, "URLs (.in):");
lblUrlTitle.preferredSize.width = 82;
var lblUrlCount = rowUrls.add("statictext", undefined, "0");
lblUrlCount.preferredSize.width = 24;
var btnScanUrls = rowUrls.add("button", undefined, "Scan");
btnScanUrls.preferredSize = [38, 21];
var btnReviewUrl = rowUrls.add("button", undefined, "Review");
btnReviewUrl.preferredSize = [55, 21];

// --- Panel: QC Report Log ---
var pnlReport = win.add("panel", undefined, "QC Report");
pnlReport.alignChildren = ["fill", "fill"];
pnlReport.margins = 6;

var reportBox = pnlReport.add("edittext", undefined, "QC Master Dashboard ready.\nClick 'Run Full QC' or individual [Scan] buttons to begin.\n", {
multiline: true,
readonly: true,
scrollable: true
});
reportBox.preferredSize = [620, 85];

// --- Footer: Credits & Buttons ---
var footerGroup = win.add("group");
footerGroup.orientation = "row";
footerGroup.alignChildren = ["fill", "center"];
footerGroup.alignment = ["fill", "bottom"];

var lblCredits = footerGroup.add("statictext", undefined, "Developed by Nousheeth Iqbal");
lblCredits.preferredSize.width = 380;

var btnClearLog = footerGroup.add("button", undefined, "Clear Log");
btnClearLog.preferredSize = [75, 23];
var btnClose = footerGroup.add("button", undefined, "Close");
btnClose.preferredSize = [65, 23];

// -------------------------------------------------------------
// Event Listeners
// -------------------------------------------------------------
btnRunQC.onClick = function () { runFullQC(); };
btnReset.onClick = function () { pristineReset(); };
btnRefresh.onClick = function () { refreshQC(); };
btnExport.onClick = function () { exportReport(); };

// Individual Scan Listeners
btnScanDouble.onClick = function () { scanDoubleSpaces(); };
btnScanWidow.onClick = function () { scanWidows(); };
btnScanHyphen.onClick = function () { scanHyphenation(); };
btnScanLigature.onClick = function () { scanLigatures(); };
btnScanSwatches.onClick = function () { scanUnusedSwatches(); };
btnScanBlack.onClick = function () { scanBlackColorIssues(); };
btnScanImgFill.onClick = function () { scanImageFramesWithFill(); };
btnScanUrls.onClick = function () { scanDisallowedUrls(); };

// Action Listeners
btnReviewDoubleSpace.onClick = function () { reviewDoubleSpace(); };
btnFixCurrentDoubleSpace.onClick = function () { fixCurrentDoubleSpace(); };
btnFixDoubleSpaces.onClick = function () { fixAllDoubleSpaces(); };

btnReviewWidow.onClick = function () { reviewWidow(); };
btnFixCurrentWidow.onClick = function () { fixCurrentWidow(); };
btnFixAllWidow.onClick = function () { fixAllWidows(); };

btnReviewHyphen.onClick = function () { reviewHyphenation(); };
btnUncheckHyphen.onClick = function () { uncheckAllHyphenation(); };

btnReviewLigature.onClick = function () { reviewLigature(); };
btnFixLigature.onClick = function () { fixCurrentLigature(); };
btnAllLigature.onClick = function () { fixAllLigatures(); };

btnRemoveSwatches.onClick = function () { removeAllUnusedSwatches(); };
btnReviewBlack.onClick = function () { reviewBlackColorIssue(); };
btnFixCurrentBlack.onClick = function () { fixCurrentBlackIssue(); };
btnFixAllBlack.onClick = function () { fixAllBlackIssues(); };

btnReviewImageFrame.onClick = function () { reviewImageFrame(); };
btnClearImgFill.onClick = function () { clearCurrentImgFill(); };

btnReviewUrl.onClick = function () { reviewDisallowedUrl(); };

btnClearLog.onClick = function () { reportBox.text = ""; };
btnClose.onClick = function () {
try { win.close(); } catch (e) {}
$.global.qcmaster_win = null;
};

// Show modeless palette
win.center();
win.show();
})();

 

Translate
NousheethAuthor
Known Participant
Oct 26, 2026 8:29 PM UTC
#target indesign
#targetengine "qcmaster_engine"

/**
* QC Master Dashboard v2.2
* Print Production Preflight & Audit Script for Adobe InDesign
* Compatible with InDesign CC 2022 through CC 2025+
* Developed by Nousheeth Iqbal
*/

(function () {
// -------------------------------------------------------------
// Global Storage for Issue Navigation
// -------------------------------------------------------------
var state = {
doc: null,
doubleSpaces: [],
doubleSpaceIndex: 0,
widows: [],
widowIndex: 0,
imageFramesWithFill: [],
imageFrameIndex: 0,
blackIssues: [],
blackIssueIndex: 0,
disallowedUrls: [],
disallowedUrlIndex: 0,
hyphenations: [],
hyphenationIndex: 0,
ligatures: [],
ligatureIndex: 0,
unusedSwatches: [],
unusedSwatchesCount: 0
};

function getActiveDocument() {
if (app.documents.length === 0) {
return null;
}
return app.activeDocument;
}

// -------------------------------------------------------------
// Helper: Logging (Newest / Most Recent Entry at the Top)
// -------------------------------------------------------------
function appendLog(txt) {
var time = new Date();
var timeStr = "[" + pad(time.getHours()) + ":" + pad(time.getMinutes()) + ":" + pad(time.getSeconds()) + "] ";
var newEntry = timeStr + txt + "\n";
var currentText = reportBox.text;

if (currentText.length > 25000) {
currentText = currentText.substring(0, 20000);
}

reportBox.text = newEntry + currentText;
}

function pad(n) {
return (n < 10 ? "0" : "") + n;
}

function getItemLocation(target) {
var page = null;
var spread = null;
if (!target) return { page: null, spread: null };

try {
// Case 1: Text target (Paragraph, Line, Word, Character, TextStyleRange, Match)
var tf = null;
try {
if (target.parentTextFrames && target.parentTextFrames.length > 0) {
tf = target.parentTextFrames[0];
}
} catch (eTf) {}

var cur = tf ? tf : target;

// Direct parentPage check
try {
if (cur.parentPage && cur.parentPage.isValid) {
page = cur.parentPage;
}
} catch (ePp) {}

// Ascend hierarchy to find Page and Spread
var node = cur;
while (node) {
try {
if (!page && node.constructor.name === "Page") {
page = node;
}
if (node.constructor.name === "Spread") {
spread = node;
break;
}
if (node.constructor.name === "Document") {
break;
}
} catch (eNode) {}
try {
node = node.parent;
} catch (ePar) {
break;
}
}

// Derive spread from page if needed
if (page && page.isValid && (!spread || !spread.isValid)) {
try {
if (page.parent && page.parent.constructor.name === "Spread") {
spread = page.parent;
}
} catch (eSp) {}
}
} catch (e) {}

return { page: page, spread: spread };
}

function getItemPage(target) {
return getItemLocation(target).page;
}

// -------------------------------------------------------------
// Helper: Automatically Add Unnamed Colors to Swatches Panel
// -------------------------------------------------------------
function addUnnamedColors(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return 0;

try {
if (app.documents.length > 0 && app.activeDocument !== doc) {
app.activeDocument = doc;
}
} catch (eDoc) {}

var beforeCount = doc.swatches.length;
try {
var act = null;
try {
act = app.menuActions.itemByName("$ID/Add Unnamed Colors");
} catch (e1) {}

if (!act || !act.isValid) {
try {
act = app.menuActions.itemByName("Add Unnamed Colors");
} catch (e2) {}
}

if (act && act.isValid && act.enabled) {
act.invoke();
}
} catch (e) {}

var afterCount = doc.swatches.length;
var added = afterCount - beforeCount;
if (added > 0) {
appendLog("Added " + added + " unnamed color(s) to Swatches panel.");
}
return added;
}

// -------------------------------------------------------------
// QC Check 1: Unused Swatches (Auto-adds unnamed colors first)
// -------------------------------------------------------------
function scanUnusedSwatches(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return 0;

// Auto-add any unnamed colors applied to text/objects into the Swatches panel
addUnnamedColors(doc);

state.unusedSwatches = [];
var swatchNames = [];

try {
var rawUnused = doc.unusedSwatches;
var rawUnusedMap = {};
for (var u = 0; u < rawUnused.length; u++) {
try {
rawUnusedMap[rawUnused[u].name] = true;
} catch (eU) {}
}

var panelSwatches = doc.swatches;
for (var i = 0; i < panelSwatches.length; i++) {
var sw = panelSwatches[i];
var swName = sw.name;

// Protect default system swatches
if (swName === "None" || swName === "Registration" || swName === "Black" || swName === "Paper" ||
swName === "[None]" || swName === "[Registration]" || swName === "[Black]" || swName === "[Paper]") {
continue;
}

if (rawUnusedMap[swName]) {
state.unusedSwatches.push(sw);
swatchNames.push(swName);
}
}
} catch (e) {
appendLog("Error scanning unused swatches: " + e.message);
}

state.unusedSwatchesCount = state.unusedSwatches.length;
lblUnusedCount.text = "" + state.unusedSwatchesCount;

if (state.unusedSwatchesCount > 0) {
appendLog("Unused Swatches in Panel (" + state.unusedSwatchesCount + "): " + swatchNames.join(", "));
} else {
appendLog("Swatches: 0 unused swatches in Swatches panel.");
}

return state.unusedSwatchesCount;
}

function removeAllUnusedSwatches(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return;

if (state.unusedSwatches.length === 0) {
scanUnusedSwatches(doc);
}
if (state.unusedSwatches.length === 0) {
appendLog("No unused swatches to remove.");
return;
}

if (!confirm("Are you sure you want to remove all " + state.unusedSwatches.length + " unused swatch(es) from the Swatches panel?", false, "Confirm Remove Unused Swatches")) {
return;
}

app.doScript(function () {
var count = 0;
try {
for (var i = state.unusedSwatches.length - 1; i >= 0; i--) {
var sw = state.unusedSwatches[i];
try {
if (sw && sw.isValid) {
sw.remove();
count++;
}
} catch (err) {}
}
appendLog("Removed " + count + " unused swatches from Swatches panel.");
scanUnusedSwatches(doc);
} catch (e) {
appendLog("Error removing swatches: " + e.message);
}
}, ScriptLanguage.JAVASCRIPT, undefined, UndoModes.ENTIRE_SCRIPT, "QC Remove Unused Swatches");
}

// -------------------------------------------------------------
// QC Check 2: Double Spaces (Scan | Review | Fix | All)
// -------------------------------------------------------------
function scanDoubleSpaces(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return 0;

state.doubleSpaces = [];
state.doubleSpaceIndex = 0;

app.findGrepPreferences = NothingEnum.nothing;
app.changeGrepPreferences = NothingEnum.nothing;
app.findGrepPreferences.findWhat = "\\x{0020}{2,}";

try {
var matches = doc.findGrep();
for (var i = 0; i < matches.length; i++) {
state.doubleSpaces.push(matches[i]);
}
} catch (e) {
appendLog("Error scanning double spaces: " + e.message);
} finally {
app.findGrepPreferences = NothingEnum.nothing;
app.changeGrepPreferences = NothingEnum.nothing;
}

lblDoubleSpaceCount.text = "" + state.doubleSpaces.length;
appendLog("Double Spaces: " + state.doubleSpaces.length + " found.");
return state.doubleSpaces.length;
}

function reviewDoubleSpace() {
if (state.doubleSpaces.length === 0) {
appendLog("No double spaces to review.");
return;
}
if (state.doubleSpaceIndex >= state.doubleSpaces.length) {
state.doubleSpaceIndex = 0;
}

var match = state.doubleSpaces[state.doubleSpaceIndex];
try {
if (match.isValid) {
app.select(match, SelectionOptions.REPLACE_WITH);
zoomToSelectedTarget(match);
var page = getItemPage(match);
appendLog("Reviewing Double Space (" + (state.doubleSpaceIndex + 1) + " of " + state.doubleSpaces.length + ") on " + (page ? "Page " + page.name : "Pasteboard"));
state.doubleSpaceIndex++;
} else {
state.doubleSpaces.splice(state.doubleSpaceIndex, 1);
lblDoubleSpaceCount.text = "" + state.doubleSpaces.length;
reviewDoubleSpace();
}
} catch (e) {
appendLog("Error navigating to double space: " + e.message);
}
}

function fixCurrentDoubleSpace() {
if (state.doubleSpaces.length === 0) {
appendLog("No double space found to fix.");
return;
}

var targetIdx = state.doubleSpaceIndex > 0 ? (state.doubleSpaceIndex - 1) : 0;
if (targetIdx >= state.doubleSpaces.length) {
targetIdx = 0;
}

var match = state.doubleSpaces[targetIdx];
try {
if (match.isValid) {
var page = getItemPage(match);
match.contents = " ";
state.doubleSpaces.splice(targetIdx, 1);
lblDoubleSpaceCount.text = "" + state.doubleSpaces.length;
appendLog("Fixed 1 double space on " + (page ? "Page " + page.name : "Pasteboard") + ". Remaining: " + state.doubleSpaces.length);

if (state.doubleSpaces.length > 0) {
if (targetIdx >= state.doubleSpaces.length) {
state.doubleSpaceIndex = 0;
} else {
state.doubleSpaceIndex = targetIdx;
}
reviewDoubleSpace();
} else {
state.doubleSpaceIndex = 0;
appendLog("All double spaces resolved!");
}
} else {
state.doubleSpaces.splice(targetIdx, 1);
lblDoubleSpaceCount.text = "" + state.doubleSpaces.length;
fixCurrentDoubleSpace();
}
} catch (e) {
appendLog("Error fixing single double space: " + e.message);
}
}

function fixAllDoubleSpaces(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return;

if (!confirm("Are you sure you want to replace all double spaces across the entire document?", false, "Confirm Fix All Double Spaces")) {
return;
}

app.doScript(function () {
app.findGrepPreferences = NothingEnum.nothing;
app.changeGrepPreferences = NothingEnum.nothing;
app.findGrepPreferences.findWhat = "\\x{0020}{2,}";
app.changeGrepPreferences.changeTo = " ";

try {
var changed = doc.changeGrep();
appendLog("Replaced " + changed.length + " double space occurrences with single space.");
scanDoubleSpaces(doc);
} catch (e) {
appendLog("Error fixing double spaces: " + e.message);
} finally {
app.findGrepPreferences = NothingEnum.nothing;
app.changeGrepPreferences = NothingEnum.nothing;
}
}, ScriptLanguage.JAVASCRIPT, undefined, UndoModes.ENTIRE_SCRIPT, "QC Fix All Double Spaces");
}

// -------------------------------------------------------------
// QC Check 3: Widow Check (Excludes 1-word/bullet paragraphs)
// -------------------------------------------------------------
function scanWidows(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return 0;

state.widows = [];
state.widowIndex = 0;

try {
var stories = doc.stories;
for (var s = 0; s < stories.length; s++) {
var story = stories[s];
var paras = story.paragraphs;
for (var p = 0; p < paras.length; p++) {
var para = paras[p];

// Multi-line paragraph check: must have at least 2 lines and at least 3 words total
// (Excludes 1-word bullets and 2-word icon descriptions like "Icon\nLabel")
if (para.lines.length >= 2 && para.words.length >= 3) {
// Skip if it is a bullet/numbered list item with < 4 words
if (para.bulletsAndNumberingListType !== ListType.NO_LIST && para.words.length < 4) {
continue;
}

var lastLine = para.lines.lastItem();
var lastLineClean = lastLine.contents.replace(/[\r\n\s\t]+$/, "");
var wordsInLastLine = lastLine.words;

// Check the line before the last line
var prevLine = para.lines.item(para.lines.length - 2);

// If the line was broken using a manual Forced Line Break (Shift + Enter / Soft Return),
// this is an intentional manual line break, NOT an automated typographic widow!
if (/[\u000A\n]/.test(prevLine.contents)) {
continue;
}

// A widow occurs if the last line has only 1 word, or is a very short stub (<= 8 chars)
if (wordsInLastLine.length === 1 || (lastLineClean.length > 0 && lastLineClean.length <= 8)) {
state.widows.push(para);
}
}
}
}
} catch (e) {
appendLog("Error scanning widows: " + e.message);
}

lblWidowCount.text = "" + state.widows.length;
appendLog("Widows: " + state.widows.length + " found.");
return state.widows.length;
}

function reviewWidow() {
if (state.widows.length === 0) {
appendLog("No widows to review.");
return;
}
if (state.widowIndex >= state.widows.length) {
state.widowIndex = 0;
}

var para = state.widows[state.widowIndex];
try {
if (para.isValid) {
var lastLine = para.lines.lastItem();
app.select(lastLine, SelectionOptions.REPLACE_WITH);
zoomToSelectedTarget(lastLine);
var page = getItemPage(para);
appendLog("Reviewing Widow (" + (state.widowIndex + 1) + " of " + state.widows.length + ") on " + (page ? "Page " + page.name : "Pasteboard") + ": \"" + lastLine.contents.replace(/[\r\n]+$/, "") + "\"");
state.widowIndex++;
} else {
state.widowIndex++;
reviewWidow();
}
} catch (e) {
appendLog("Error selecting widow: " + e.message);
}
}

function fixCurrentWidow() {
if (state.widows.length === 0) {
appendLog("No widow found to fix.");
return;
}

var targetIdx = state.widowIndex > 0 ? (state.widowIndex - 1) : 0;
if (targetIdx >= state.widows.length) {
targetIdx = 0;
}

var para = state.widows[targetIdx];
try {
if (para && para.isValid && para.words.length >= 2) {
var words = para.words;
var lastWord = words.lastItem();
var wordBeforeLast = words.item(words.length - 2);

var story = para.parentStory;
var lastCharOfPrev = wordBeforeLast.characters.lastItem();
var firstCharOfLast = lastWord.characters.firstItem();

var startIdx = lastCharOfPrev.index + 1;
var endIdx = firstCharOfLast.index - 1;

if (startIdx <= endIdx) {
var spaceRange = story.characters.itemByRange(startIdx, endIdx);
if (spaceRange.isValid) {
spaceRange.contents = SpecialCharacters.NONBREAKING_SPACE;
}
} else {
wordBeforeLast.insertionPoints.lastItem().contents = SpecialCharacters.NONBREAKING_SPACE;
}

var page = getItemPage(para);
state.widows.splice(targetIdx, 1);
lblWidowCount.text = "" + state.widows.length;
appendLog("Fixed 1 widow on " + (page ? "Page " + page.name : "Pasteboard") + ". Remaining: " + state.widows.length);

if (state.widows.length > 0) {
if (targetIdx >= state.widows.length) {
state.widowIndex = 0;
} else {
state.widowIndex = targetIdx;
}
reviewWidow();
} else {
lblStatus.text = "Status: Widows Cleared";
}
} else {
state.widows.splice(targetIdx, 1);
lblWidowCount.text = "" + state.widows.length;
if (state.widows.length > 0) reviewWidow();
}
} catch (e) {
appendLog("Error fixing widow: " + e.message);
}
}

function fixAllWidows(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return;

if (state.widows.length === 0) {
scanWidows(doc);
}
if (state.widows.length === 0) {
appendLog("No widows to fix.");
return;
}

if (!confirm("Are you sure you want to fix all " + state.widows.length + " detected widow(s) across the document?", false, "Confirm Fix All Widows")) {
return;
}

app.doScript(function () {
var fixedCount = 0;
var prevRedraw = app.scriptPreferences.enableRedraw;

try {
app.scriptPreferences.enableRedraw = false;
for (var i = 0; i < state.widows.length; i++) {
var para = state.widows[i];
try {
if (para && para.isValid && para.words.length >= 2) {
var words = para.words;
var lastWord = words.lastItem();
var wordBeforeLast = words.item(words.length - 2);

var story = para.parentStory;
var lastCharOfPrev = wordBeforeLast.characters.lastItem();
var firstCharOfLast = lastWord.characters.firstItem();

var startIdx = lastCharOfPrev.index + 1;
var endIdx = firstCharOfLast.index - 1;

if (startIdx <= endIdx) {
var spaceRange = story.characters.itemByRange(startIdx, endIdx);
if (spaceRange.isValid) {
spaceRange.contents = SpecialCharacters.NONBREAKING_SPACE;
fixedCount++;
}
} else {
wordBeforeLast.insertionPoints.lastItem().contents = SpecialCharacters.NONBREAKING_SPACE;
fixedCount++;
}
}
} catch (eOne) {}
}
} catch (eAll) {
appendLog("Error fixing widows: " + eAll.message);
} finally {
app.scriptPreferences.enableRedraw = prevRedraw;
}

appendLog("Inserted non-breaking space on " + fixedCount + " paragraph(s) to eliminate widows.");
scanWidows(doc);
}, ScriptLanguage.JAVASCRIPT, undefined, UndoModes.ENTIRE_SCRIPT, "QC Fix All Widows");
}

function fixWidows(doc) {
fixAllWidows(doc);
}

// -------------------------------------------------------------
// QC Check 4: Image Frames with Fill Color (Safe DOM check)
// -------------------------------------------------------------
function scanImageFramesWithFill(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return 0;

state.imageFramesWithFill = [];
state.imageFrameIndex = 0;

try {
var noneSwatch = doc.swatches.itemByName("None");
var seen = {};

var graphics = doc.allGraphics;
for (var i = 0; i < graphics.length; i++) {
var g = graphics[i];
var frame = g.parent;
if (frame && frame.isValid) {
var frameId = "" + frame.id;
if (!seen[frameId]) {
try {
var fc = frame.fillColor;
if (fc && fc.isValid) {
var fcName = fc.name;
if (fcName !== "None" && fcName !== "[None]" && fcName !== "" && fc !== noneSwatch) {
var fTint = -1;
try { fTint = frame.fillTint; } catch (errT) {}
if (fTint === -1 || fTint > 0) {
seen[frameId] = true;
state.imageFramesWithFill.push({
frame: frame,
fillName: fcName,
tint: fTint
});
}
}
}
} catch (errF) {}
}
}
}

var allItems = doc.allPageItems;
for (var j = 0; j < allItems.length; j++) {
var item = allItems[j];
try {
var hasG = false;
try {
if (item.allGraphics && item.allGraphics.length > 0) hasG = true;
else if (item.graphics && item.graphics.length > 0) hasG = true;
} catch (eG) {}

if (hasG) {
var itemId = "" + item.id;
if (!seen[itemId]) {
var ifc = item.fillColor;
if (ifc && ifc.isValid) {
var ifcName = ifc.name;
if (ifcName !== "None" && ifcName !== "[None]" && ifcName !== "" && ifc !== noneSwatch) {
var itint = -1;
try { itint = item.fillTint; } catch (eT) {}
if (itint === -1 || itint > 0) {
seen[itemId] = true;
state.imageFramesWithFill.push({
frame: item,
fillName: ifcName,
tint: itint
});
}
}
}
}
}
} catch (eItem) {}
}
} catch (e) {
appendLog("Error checking image frames: " + e.message);
}

lblImageFrameCount.text = "" + state.imageFramesWithFill.length;
appendLog("Image Frames with Fill Color: " + state.imageFramesWithFill.length + " found.");
return state.imageFramesWithFill.length;
}

function reviewImageFrame() {
if (state.imageFramesWithFill.length === 0) {
appendLog("No image frames with fill color found.");
return;
}
if (state.imageFrameIndex >= state.imageFramesWithFill.length) {
state.imageFrameIndex = 0;
}

var item = state.imageFramesWithFill[state.imageFrameIndex];
var frame = item.frame;
try {
if (frame && frame.isValid) {
app.select(frame, SelectionOptions.REPLACE_WITH);
zoomToSelectedTarget(frame);
var page = getItemPage(frame);
appendLog("Reviewing Image Frame (" + (state.imageFrameIndex + 1) + " of " + state.imageFramesWithFill.length + ") on " + (page ? "Page " + page.name : "Pasteboard") + " [Fill: " + item.fillName + "]");
state.imageFrameIndex++;
} else {
state.imageFrameIndex++;
reviewImageFrame();
}
} catch (e) {
appendLog("Error selecting frame: " + e.message);
}
}

function clearCurrentImgFill() {
if (state.imageFramesWithFill.length === 0) {
appendLog("No image frame fill to clear.");
return;
}

var targetIdx = state.imageFrameIndex > 0 ? (state.imageFrameIndex - 1) : 0;
if (targetIdx >= state.imageFramesWithFill.length) targetIdx = 0;

var item = state.imageFramesWithFill[targetIdx];
var frame = item.frame;
try {
if (frame && frame.isValid) {
var doc = getActiveDocument();
frame.fillColor = doc.swatches.itemByName("None");
var page = getItemPage(frame);
state.imageFramesWithFill.splice(targetIdx, 1);
lblImageFrameCount.text = "" + state.imageFramesWithFill.length;
appendLog("Cleared fill color on Image Frame (" + (page ? "Page " + page.name : "Pasteboard") + "). Remaining: " + state.imageFramesWithFill.length);

if (state.imageFramesWithFill.length > 0) {
if (targetIdx >= state.imageFramesWithFill.length) state.imageFrameIndex = 0;
else state.imageFrameIndex = targetIdx;
reviewImageFrame();
} else {
state.imageFrameIndex = 0;
appendLog("All image frame fill colors cleared!");
}
} else {
state.imageFramesWithFill.splice(targetIdx, 1);
lblImageFrameCount.text = "" + state.imageFramesWithFill.length;
clearCurrentImgFill();
}
} catch (e) {
appendLog("Error clearing frame fill: " + e.message);
}
}

// -------------------------------------------------------------
// QC Check 5: Black Color Issues (Strict Tint 80% or 100% Solid Only; No Rich Black)
// -------------------------------------------------------------
function isRichBlack(swatch) {
if (!swatch || !swatch.isValid) return false;
var name = "";
try { name = (swatch.name || "").toLowerCase(); } catch (eN) {}

if (name === "[registration]" || name === "registration") {
return false;
}

if (name.indexOf("rich black") !== -1 || name.indexOf("4 color black") !== -1 ||
name.indexOf("4-color black") !== -1 || name.indexOf("4-colour black") !== -1) {
return true;
}

try {
if (swatch.space === ColorSpace.CMYK) {
var cv = swatch.colorValue;
if (cv && cv.length >= 4) {
var c = cv[0], m = cv[1], y = cv[2], k = cv[3];
// 4-color black: all 4 CMYK channels present with high K
if (c > 0 && m > 0 && y > 0 && k >= 40) {
return true;
}
// Or high total ink with dominant K and other process inks
if (k >= 60 && (c + m + y) >= 20) {
return true;
}
}
}
} catch (eCv) {}
return false;
}

function isBlackColor(swatch) {
if (!swatch || !swatch.isValid) return false;
var name = "";
try { name = swatch.name; } catch (eN) {}
if (name === "Black" || name === "[Black]") {
return true;
}
if (isRichBlack(swatch)) {
return true;
}
try {
if (swatch.space === ColorSpace.CMYK) {
var cv = swatch.colorValue;
if (cv && cv.length >= 4) {
// Pure K (1-color black)
if (cv[0] === 0 && cv[1] === 0 && cv[2] === 0 && cv[3] > 0) {
return true;
}
if (name.toLowerCase().indexOf("black") !== -1 && cv[3] > 0) {
return true;
}
}
}
} catch (e) {}
return false;
}

function getEffectiveTint(swatch, appliedTint) {
var baseK = 100;
try {
if (swatch.space === ColorSpace.CMYK) {
var cv = swatch.colorValue;
if (cv && cv[0] === 0 && cv[1] === 0 && cv[2] === 0) {
baseK = cv[3];
}
}
} catch (e) {}
if (appliedTint === -1) {
return Math.round(baseK);
}
return Math.round((baseK * appliedTint) / 100);
}

function isApprovedBlack(swatch, appliedTint) {
if (!swatch || !swatch.isValid) return false;

// Rich Black (4-color black) is strictly NOT allowed!
if (isRichBlack(swatch)) {
return false;
}

var effK = getEffectiveTint(swatch, appliedTint);
// Only 100% Solid Black and 80% Black Tint are approved!
return (effK === 100 || effK === 80);
}

function scanBlackColorIssues(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return 0;

// Auto-add unnamed colors so unnamed tints of black get identified as swatches
addUnnamedColors(doc);

state.blackIssues = [];
state.blackIssueIndex = 0;

try {
// 1. Text scans
var stories = doc.stories;
for (var s = 0; s < stories.length; s++) {
var story = stories[s];
var ranges = story.textStyleRanges;
for (var r = 0; r < ranges.length; r++) {
var range = ranges[r];
if (range.contents.replace(/[\r\n\s\t]+/g, "").length === 0) {
continue;
}

try {
var fc = range.fillColor;
if (fc && fc.isValid && isBlackColor(fc)) {
if (!isApprovedBlack(fc, range.fillTint)) {
var isRich = isRichBlack(fc);
var effK = isRich ? "4-Color" : getEffectiveTint(fc, range.fillTint);
var sampleText = range.contents;
if (sampleText.length > 22) {
sampleText = sampleText.substring(0, 19) + "...";
}
var descText = isRich ?
("Text Fill Rich Black (4-Color Black - Not Allowed) [\"" + sampleText.replace(/[\r\n]+/g, " ") + "\"]") :
("Text Fill Black " + effK + "% (Approved: 100%/80% only) [\"" + sampleText.replace(/[\r\n]+/g, " ") + "\"]");

state.blackIssues.push({
target: range,
type: "textFill",
effK: isRich ? 100 : effK,
isRich: isRich,
desc: descText
});
}
}
} catch (eF) {}

try {
if (range.strokeWeight > 0) {
var sc = range.strokeColor;
if (sc && sc.isValid && isBlackColor(sc)) {
if (!isApprovedBlack(sc, range.strokeTint)) {
var isRichS = isRichBlack(sc);
var effStrokeK = isRichS ? "4-Color" : getEffectiveTint(sc, range.strokeTint);
var descStroke = isRichS ?
"Text Stroke Rich Black (4-Color Black - Not Allowed)" :
("Text Stroke Black " + effStrokeK + "% (Approved: 100%/80% only)");

state.blackIssues.push({
target: range,
type: "textStroke",
effK: isRichS ? 100 : effStrokeK,
isRich: isRichS,
desc: descStroke
});
}
}
}
} catch (eS) {}
}
}

// 2. PageItems scans
var pageItems = doc.allPageItems;
for (var i = 0; i < pageItems.length; i++) {
var item = pageItems[i];

try {
var ifc = item.fillColor;
if (ifc && ifc.isValid && isBlackColor(ifc)) {
var ift = -1;
try { ift = item.fillTint; } catch (eT) {}
if (!isApprovedBlack(ifc, ift)) {
var isRichIF = isRichBlack(ifc);
var effItemK = isRichIF ? "4-Color" : getEffectiveTint(ifc, ift);
var descIF = isRichIF ?
(item.constructor.name + " Fill Rich Black (4-Color Black - Not Allowed)") :
(item.constructor.name + " Fill Black " + effItemK + "% (Approved: 100%/80% only)");

state.blackIssues.push({
target: item,
type: "itemFill",
effK: isRichIF ? 100 : effItemK,
isRich: isRichIF,
desc: descIF
});
}
}
} catch (eIF) {}

try {
var isw = 0;
try { isw = item.strokeWeight; } catch (eSW) {}
if (isw > 0) {
var isc = item.strokeColor;
if (isc && isc.isValid && isBlackColor(isc)) {
var ist = -1;
try { ist = item.strokeTint; } catch (eST) {}
if (!isApprovedBlack(isc, ist)) {
var isRichIS = isRichBlack(isc);
var effItemStrokeK = isRichIS ? "4-Color" : getEffectiveTint(isc, ist);
var descIS = isRichIS ?
(item.constructor.name + " Stroke Rich Black (4-Color Black - Not Allowed)") :
(item.constructor.name + " Stroke Black " + effItemStrokeK + "% (Approved: 100%/80% only)");

state.blackIssues.push({
target: item,
type: "itemStroke",
effK: isRichIS ? 100 : effItemStrokeK,
isRich: isRichIS,
desc: descIS
});
}
}
}
} catch (eIS) {}
}
} catch (e) {
appendLog("Error scanning black color issues: " + e.message);
}

lblBlackColorCount.text = "" + state.blackIssues.length;
appendLog("Black Color Issues: " + state.blackIssues.length + " found.");
return state.blackIssues.length;
}

function reviewBlackColorIssue() {
if (state.blackIssues.length === 0) {
appendLog("No black color issues to review.");
return;
}
if (state.blackIssueIndex >= state.blackIssues.length) {
state.blackIssueIndex = 0;
}

var issue = state.blackIssues[state.blackIssueIndex];
try {
if (issue.target.isValid) {
app.select(issue.target, SelectionOptions.REPLACE_WITH);
zoomToSelectedTarget(issue.target);
var page = getItemPage(issue.target);
var pageName = page ? "Page " + page.name : "Pasteboard";
appendLog("Reviewing Black Issue (" + (state.blackIssueIndex + 1) + " of " + state.blackIssues.length + ") on " + pageName + ": " + issue.desc);
state.blackIssueIndex++;
} else {
state.blackIssueIndex++;
reviewBlackColorIssue();
}
} catch (e) {
appendLog("Error navigating to black issue: " + e.message);
}
}

function fixCurrentBlackIssue() {
if (state.blackIssues.length === 0) {
appendLog("No black color issue found to fix.");
return;
}

var targetIdx = state.blackIssueIndex > 0 ? (state.blackIssueIndex - 1) : 0;
if (targetIdx >= state.blackIssues.length) {
targetIdx = 0;
}

var issue = state.blackIssues[targetIdx];
var doc = getActiveDocument();
if (!doc) return;

var blackSwatch = doc.swatches.itemByName("[Black]");
if (!blackSwatch || !blackSwatch.isValid) {
blackSwatch = doc.swatches.itemByName("Black");
}

try {
if (issue.target && issue.target.isValid) {
var targetTint = (!issue.isRich && issue.effK >= 70 && issue.effK < 90) ? 80 : 100;
if (issue.type === "textFill") {
issue.target.fillColor = blackSwatch;
issue.target.fillTint = targetTint;
} else if (issue.type === "textStroke") {
issue.target.strokeColor = blackSwatch;
issue.target.strokeTint = targetTint;
} else if (issue.type === "itemFill") {
issue.target.fillColor = blackSwatch;
try { issue.target.fillTint = targetTint; } catch (eT1) {}
} else if (issue.type === "itemStroke") {
issue.target.strokeColor = blackSwatch;
try { issue.target.strokeTint = targetTint; } catch (eT2) {}
}

var page = getItemPage(issue.target);
var pageName = page ? "Page " + page.name : "Pasteboard";
state.blackIssues.splice(targetIdx, 1);
lblBlackColorCount.text = "" + state.blackIssues.length;
appendLog("Fixed 1 Black issue to " + targetTint + "% on " + pageName + ". Remaining: " + state.blackIssues.length);

if (state.blackIssues.length > 0) {
if (targetIdx >= state.blackIssues.length) {
state.blackIssueIndex = 0;
} else {
state.blackIssueIndex = targetIdx;
}
reviewBlackColorIssue();
} else {
lblStatus.text = "Status: Black Issues Cleared";
}
} else {
state.blackIssues.splice(targetIdx, 1);
lblBlackColorCount.text = "" + state.blackIssues.length;
if (state.blackIssues.length > 0) reviewBlackColorIssue();
}
} catch (e) {
appendLog("Error fixing black issue: " + e.message);
}
}

function fixAllBlackIssues(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return;

if (state.blackIssues.length === 0) {
scanBlackColorIssues(doc);
}
if (state.blackIssues.length === 0) {
appendLog("No black color issues to fix.");
return;
}

if (!confirm("Are you sure you want to fix all " + state.blackIssues.length + " unapproved black issue(s) across the document to approved values (100% Solid Black / 80% Tint)?", false, "Confirm Fix All Black Issues")) {
return;
}

app.doScript(function () {
var blackSwatch = doc.swatches.itemByName("[Black]");
if (!blackSwatch || !blackSwatch.isValid) {
blackSwatch = doc.swatches.itemByName("Black");
}

var fixedCount = 0;
var prevRedraw = app.scriptPreferences.enableRedraw;
try {
app.scriptPreferences.enableRedraw = false;
for (var i = 0; i < state.blackIssues.length; i++) {
var issue = state.blackIssues[i];
try {
if (issue.target && issue.target.isValid) {
var targetTint = (!issue.isRich && issue.effK >= 70 && issue.effK < 90) ? 80 : 100;
if (issue.type === "textFill") {
issue.target.fillColor = blackSwatch;
issue.target.fillTint = targetTint;
fixedCount++;
} else if (issue.type === "textStroke") {
issue.target.strokeColor = blackSwatch;
issue.target.strokeTint = targetTint;
fixedCount++;
} else if (issue.type === "itemFill") {
issue.target.fillColor = blackSwatch;
try { issue.target.fillTint = targetTint; } catch (eT1) {}
fixedCount++;
} else if (issue.type === "itemStroke") {
issue.target.strokeColor = blackSwatch;
try { issue.target.strokeTint = targetTint; } catch (eT2) {}
fixedCount++;
}
}
} catch (eOne) {}
}
appendLog("Fixed " + fixedCount + " black color issue(s) across document.");
scanBlackColorIssues(doc);
} catch (eAll) {
appendLog("Error fixing all black issues: " + eAll.message);
} finally {
app.scriptPreferences.enableRedraw = prevRedraw;
}
}, ScriptLanguage.JAVASCRIPT, undefined, UndoModes.ENTIRE_SCRIPT, "QC Fix All Black Issues");
}

// -------------------------------------------------------------
// QC Check 6: Indian / Disallowed URLs (.in / com/in)
// -------------------------------------------------------------
function isDisallowedIndianUrl(url) {
if (!url) return false;
var cleanUrl = url.toLowerCase();
if (/(?:\.in|\.co\.in|\.org\.in|\.net\.in)(?:[\/\?#]|$)/i.test(cleanUrl)) {
return true;
}
if (/(?:com\/in|\/en-in|\/in)(?:[\/\?#]|$)/i.test(cleanUrl)) {
return true;
}
return false;
}

function scanDisallowedUrls(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return 0;

state.disallowedUrls = [];
state.disallowedUrlIndex = 0;

try {
var hyperlinks = doc.hyperlinks;
for (var h = 0; h < hyperlinks.length; h++) {
var hl = hyperlinks[h];
var dest = hl.destination;
if (dest && dest.hasOwnProperty("destinationURL")) {
var url = dest.destinationURL;
if (isDisallowedIndianUrl(url)) {
var sourceItem = null;
if (hl.source) {
if (hl.source.hasOwnProperty("sourceText")) {
sourceItem = hl.source.sourceText;
} else if (hl.source.hasOwnProperty("sourcePageItem")) {
sourceItem = hl.source.sourcePageItem;
}
}
state.disallowedUrls.push({
url: url,
target: sourceItem,
type: "Hyperlink Object"
});
}
}
}

app.findGrepPreferences = NothingEnum.nothing;
app.findGrepPreferences.findWhat = "(?i)\\b(?:https?:\\/\\/|www\\.)[^\\s<>'\"\\)\\]]+";

var textMatches = doc.findGrep();
for (var m = 0; m < textMatches.length; m++) {
var matchText = textMatches[m].contents;
if (isDisallowedIndianUrl(matchText)) {
state.disallowedUrls.push({
url: matchText,
target: textMatches[m],
type: "Text URL"
});
}
}
} catch (e) {
appendLog("Error scanning disallowed URLs: " + e.message);
} finally {
app.findGrepPreferences = NothingEnum.nothing;
}

lblUrlCount.text = "" + state.disallowedUrls.length;
appendLog("Disallowed URLs (.in): " + state.disallowedUrls.length + " found.");
return state.disallowedUrls.length;
}

function reviewDisallowedUrl() {
if (state.disallowedUrls.length === 0) {
appendLog("No disallowed URLs to review.");
return;
}
if (state.disallowedUrlIndex >= state.disallowedUrls.length) {
state.disallowedUrlIndex = 0;
}

var item = state.disallowedUrls[state.disallowedUrlIndex];
try {
if (item.target && item.target.isValid) {
app.select(item.target, SelectionOptions.REPLACE_WITH);
zoomToSelectedTarget(item.target);
var page = getItemPage(item.target);
var pageName = page ? "Page " + page.name : "Pasteboard";
appendLog("Reviewing URL (" + (state.disallowedUrlIndex + 1) + " of " + state.disallowedUrls.length + ") on " + pageName + " [" + item.type + "]: " + item.url);
state.disallowedUrlIndex++;
} else {
appendLog("URL (" + (state.disallowedUrlIndex + 1) + " of " + state.disallowedUrls.length + "): " + item.url);
state.disallowedUrlIndex++;
if (state.disallowedUrlIndex < state.disallowedUrls.length) {
reviewDisallowedUrl();
}
}
} catch (e) {
appendLog("Error navigating to disallowed URL: " + e.message);
}
}

// -------------------------------------------------------------
// QC Check 7: Hyphenation Check & Uncheck All
// -------------------------------------------------------------
function scanHyphenation(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return 0;

state.hyphenations = [];
state.hyphenationIndex = 0;

try {
var stories = doc.stories;
for (var s = 0; s < stories.length; s++) {
var story = stories[s];
var paras = story.paragraphs;
for (var p = 0; p < paras.length; p++) {
var para = paras[p];
if (para.hyphenation === true) {
var words = para.words;
var hasBrokenWord = false;

for (var w = 0; w < words.length; w++) {
var word = words[w];
if (word.lines.length > 1) {
state.hyphenations.push({
target: word,
para: para,
desc: "Hyphenated Word: \"" + word.contents + "\""
});
hasBrokenWord = true;
}
}

if (!hasBrokenWord && para.lines.length > 1) {
state.hyphenations.push({
target: para,
para: para,
desc: "Hyphenate Enabled on Paragraph (" + para.lines.length + " lines)"
});
}
}
}
}
} catch (e) {
appendLog("Error scanning hyphenated text: " + e.message);
}

lblHyphenCount.text = "" + state.hyphenations.length;
appendLog("Hyphenation: " + state.hyphenations.length + " occurrences found.");
return state.hyphenations.length;
}

function reviewHyphenation() {
if (state.hyphenations.length === 0) {
appendLog("No hyphenation to review.");
return;
}
if (state.hyphenationIndex >= state.hyphenations.length) {
state.hyphenationIndex = 0;
}

var item = state.hyphenations[state.hyphenationIndex];
try {
if (item.target.isValid) {
app.select(item.target, SelectionOptions.REPLACE_WITH);
zoomToSelectedTarget(item.target);
var page = getItemPage(item.target);
appendLog("Reviewing Hyphenation (" + (state.hyphenationIndex + 1) + " of " + state.hyphenations.length + ") on " + (page ? "Page " + page.name : "Pasteboard") + ": " + item.desc);
state.hyphenationIndex++;
} else {
state.hyphenationIndex++;
reviewHyphenation();
}
} catch (e) {
appendLog("Error navigating to hyphenation: " + e.message);
}
}

function uncheckAllHyphenation(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return;

if (!confirm("Are you sure you want to uncheck hyphenation across all paragraphs in the document?", false, "Confirm Uncheck Hyphenation")) {
return;
}

app.doScript(function () {
var count = 0;
var prevRedraw = app.scriptPreferences.enableRedraw;
try {
app.scriptPreferences.enableRedraw = false;
var stories = doc.stories;
for (var s = 0; s < stories.length; s++) {
var paras = stories[s].paragraphs;
for (var p = 0; p < paras.length; p++) {
if (paras[p].hyphenation === true) {
paras[p].hyphenation = false;
count++;
}
}
}
appendLog("Unchecked Hyphenate on " + count + " paragraphs across the document.");
scanHyphenation(doc);
} catch (e) {
appendLog("Error unchecking hyphenation: " + e.message);
} finally {
app.scriptPreferences.enableRedraw = prevRedraw;
}
}, ScriptLanguage.JAVASCRIPT, undefined, UndoModes.ENTIRE_SCRIPT, "QC Uncheck Hyphenation");
}

// -------------------------------------------------------------
// QC Check 8: Ligatures Check & Turn Off Fix
// -------------------------------------------------------------
function scanLigatures(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return 0;

state.ligatures = [];
state.ligatureIndex = 0;

try {
var stories = doc.stories;
for (var s = 0; s < stories.length; s++) {
var ranges = stories[s].textStyleRanges;
for (var r = 0; r < ranges.length; r++) {
var range = ranges[r];
if (range.contents.replace(/[\r\n\s\t]+/g, "").length === 0) continue;

if (range.ligatures === true) {
if (/(fi|fl|ff|ffi|ffl|[fiflffffiffl])/i.test(range.contents)) {
state.ligatures.push(range);
}
}
}
}
} catch (e) {
appendLog("Error scanning ligatures: " + e.message);
}

lblLigatureCount.text = "" + state.ligatures.length;
appendLog("Ligatures Used: " + state.ligatures.length + " ranges found.");
return state.ligatures.length;
}

function reviewLigature() {
if (state.ligatures.length === 0) {
appendLog("No ligatures to review.");
return;
}
if (state.ligatureIndex >= state.ligatures.length) {
state.ligatureIndex = 0;
}

var range = state.ligatures[state.ligatureIndex];
try {
if (range.isValid) {
app.select(range, SelectionOptions.REPLACE_WITH);
zoomToSelectedTarget(range);
var page = getItemPage(range);
var sample = range.contents;
if (sample.length > 22) sample = sample.substring(0, 19) + "...";
appendLog("Reviewing Ligature (" + (state.ligatureIndex + 1) + " of " + state.ligatures.length + ") on " + (page ? "Page " + page.name : "Pasteboard") + ": \"" + sample.replace(/[\r\n]+/g, " ") + "\"");
state.ligatureIndex++;
} else {
state.ligatureIndex++;
reviewLigature();
}
} catch (e) {
appendLog("Error navigating to ligature: " + e.message);
}
}

function fixCurrentLigature() {
if (state.ligatures.length === 0) {
appendLog("No ligature found to fix.");
return;
}

var targetIdx = state.ligatureIndex > 0 ? (state.ligatureIndex - 1) : 0;
if (targetIdx >= state.ligatures.length) targetIdx = 0;

var range = state.ligatures[targetIdx];
try {
if (range.isValid) {
range.ligatures = false;
state.ligatures.splice(targetIdx, 1);
lblLigatureCount.text = "" + state.ligatures.length;
appendLog("Turned off ligatures on selected text. Remaining: " + state.ligatures.length);

if (state.ligatures.length > 0) {
if (targetIdx >= state.ligatures.length) state.ligatureIndex = 0;
else state.ligatureIndex = targetIdx;
reviewLigature();
} else {
state.ligatureIndex = 0;
appendLog("All ligatures turned off!");
}
} else {
state.ligatures.splice(targetIdx, 1);
lblLigatureCount.text = "" + state.ligatures.length;
fixCurrentLigature();
}
} catch (e) {
appendLog("Error fixing ligature: " + e.message);
}
}

function fixAllLigatures(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return;

if (!confirm("Are you sure you want to turn off ligatures across the entire document?", false, "Confirm Turn Off All Ligatures")) {
return;
}

app.doScript(function () {
var count = 0;
var prevRedraw = app.scriptPreferences.enableRedraw;
try {
app.scriptPreferences.enableRedraw = false;
var stories = doc.stories;
for (var s = 0; s < stories.length; s++) {
var ranges = stories[s].textStyleRanges;
for (var r = 0; r < ranges.length; r++) {
if (ranges[r].ligatures === true) {
ranges[r].ligatures = false;
count++;
}
}
}
appendLog("Turned off ligatures on " + count + " text ranges across document.");
scanLigatures(doc);
} catch (e) {
appendLog("Error turning off ligatures: " + e.message);
} finally {
app.scriptPreferences.enableRedraw = prevRedraw;
}
}, ScriptLanguage.JAVASCRIPT, undefined, UndoModes.ENTIRE_SCRIPT, "QC Turn Off Ligatures");
}

// -------------------------------------------------------------
// Zoom Helper (Maintains user's current zoom level like native Find/Change)
// -------------------------------------------------------------
function zoomToSelectedTarget(target) {
try {
var loc = getItemLocation(target);
var curZoom = 100;
try {
curZoom = app.activeWindow.zoomPercentage;
} catch (eZ) {}

// 1. Switch active spread if target is on a different spread
if (loc.spread && loc.spread.isValid) {
if (app.activeWindow.activeSpread !== loc.spread) {
app.activeWindow.activeSpread = loc.spread;
}
}

// 2. Set active page on that spread
if (loc.page && loc.page.isValid) {
app.activeWindow.activePage = loc.page;
}

// 3. Center viewport on the selected item while keeping current zoom level
try {
var act = null;
try { act = app.menuActions.itemByName("$ID/Fit Selection in Window"); } catch (e1) {}
if (!act || !act.isValid) {
try { act = app.menuActions.itemByName("Fit Selection in Window"); } catch (e2) {}
}
if (act && act.isValid && act.enabled) {
act.invoke();
}
} catch (eFit) {}

// 4. Restore the user's exact zoom level (e.g. 150%, 200%, etc.)
// so it never forces "fit page" or "fit frame", behaving exactly like native Find/Change!
if (curZoom && curZoom > 0) {
app.activeWindow.zoomPercentage = curZoom;
}
} catch (e) {}
}

// -------------------------------------------------------------
// Full QC Runner & Pristine Reset
// -------------------------------------------------------------
function pristineReset() {
state.doubleSpaces = [];
state.doubleSpaceIndex = 0;
state.widows = [];
state.widowIndex = 0;
state.imageFramesWithFill = [];
state.imageFrameIndex = 0;
state.blackIssues = [];
state.blackIssueIndex = 0;
state.disallowedUrls = [];
state.disallowedUrlIndex = 0;
state.hyphenations = [];
state.hyphenationIndex = 0;
state.ligatures = [];
state.ligatureIndex = 0;
state.unusedSwatches = [];
state.unusedSwatchesCount = 0;

lblUnusedCount.text = "0";
lblDoubleSpaceCount.text = "0";
lblWidowCount.text = "0";
lblImageFrameCount.text = "0";
lblBlackColorCount.text = "0";
lblUrlCount.text = "0";
lblHyphenCount.text = "0";
lblLigatureCount.text = "0";

var doc = getActiveDocument();
lblDocName.text = "Document: " + (doc ? doc.name : "None");
lblStatus.text = "Status: Ready";

try {
app.select(NothingEnum.nothing);
} catch (e) {}

reportBox.text = "QC Master Dashboard ready.\nClick 'Run Full QC' or individual [Scan] buttons to begin.\n";
}

function runFullQC() {
var doc = getActiveDocument();
if (!doc) {
alert("Please open an InDesign document before running QC.", "No Document");
return;
}

lblDocName.text = "Document: " + doc.name;
lblStatus.text = "Status: Running...";

var prevRedraw = app.scriptPreferences.enableRedraw;
try {
// Speed optimization: suppress screen redraws during multi-page audits (50+ pages)
app.scriptPreferences.enableRedraw = false;

// Step 1: Detect and add any unnamed colors to the Swatches panel
addUnnamedColors(doc);

var cSwatches = scanUnusedSwatches(doc);
var cDouble = scanDoubleSpaces(doc);
var cWidows = scanWidows(doc);
var cImgFill = scanImageFramesWithFill(doc);
var cBlack = scanBlackColorIssues(doc);
var cUrls = scanDisallowedUrls(doc);
var cHyphens = scanHyphenation(doc);
var cLigatures = scanLigatures(doc);

var total = cSwatches + cDouble + cWidows + cImgFill + cBlack + cUrls + cHyphens + cLigatures;
lblStatus.text = "Status: Complete (" + total + " issues)";

// Post summary header at the top
appendLog("=========================================");
appendLog("Full QC Complete for: " + doc.name + " (" + total + " total issues)");
appendLog("=========================================");
} catch (eQC) {
lblStatus.text = "Status: Error";
appendLog("Error during Full QC: " + eQC.message);
} finally {
app.scriptPreferences.enableRedraw = prevRedraw;
}
}

function refreshQC() {
var doc = getActiveDocument();
if (!doc) {
lblDocName.text = "Document: None";
lblStatus.text = "Status: No document open";
return;
}
lblDocName.text = "Document: " + doc.name;
lblStatus.text = "Status: Ready";
runFullQC();
}

function exportReport() {
var reportContent = reportBox.text;
if (!reportContent || reportContent.length === 0) {
alert("The report log is empty. Run QC before exporting.", "Empty Log");
return;
}

var doc = getActiveDocument();
var defaultPath = "~/Desktop";
if (doc && doc.saved) {
defaultPath = doc.filePath;
}

var fileName = (doc ? doc.name.replace(/\.indd$/i, "") : "Document") + "_QC_Report.txt";
var file = new File(defaultPath + "/" + fileName).saveDlg("Save QC Audit Report", "Text File:*.txt");
if (file) {
file.encoding = "UTF-8";
file.open("w");
file.write(reportContent);
file.close();
appendLog("Report successfully saved to: " + file.fsName);
}
}

// -------------------------------------------------------------
// UI Construction (Compact 2-Column Layout)
// -------------------------------------------------------------
if ($.global.qcmaster_win && $.global.qcmaster_win instanceof Window) {
try { $.global.qcmaster_win.close(); } catch (eW) {}
}
var win = new Window("palette", "QC Master Dashboard v2.2 | Prepress Audit & Fix Hub", undefined, { resizeable: true });
$.global.qcmaster_win = win;
win.orientation = "column";
win.alignChildren = ["fill", "top"];
win.margins = 10;
win.spacing = 6;

// --- Compact Header: Document Information & Actions ---
var pnlHeader = win.add("panel", undefined, "Document & Actions");
pnlHeader.orientation = "column";
pnlHeader.alignChildren = ["fill", "top"];
pnlHeader.margins = 8;
pnlHeader.spacing = 6;

var headerRow1 = pnlHeader.add("group");
headerRow1.orientation = "row";
headerRow1.alignChildren = ["left", "center"];
headerRow1.spacing = 16;

var initialDoc = getActiveDocument();
var docNameText = initialDoc ? initialDoc.name : "No active document";
var lblDocName = headerRow1.add("statictext", undefined, "Document: " + docNameText);
lblDocName.preferredSize.width = 340;
var lblStatus = headerRow1.add("statictext", undefined, "Status: Ready");
lblStatus.preferredSize.width = 250;

var headerRow2 = pnlHeader.add("group");
headerRow2.orientation = "row";
headerRow2.alignChildren = ["left", "center"];
headerRow2.spacing = 8;

var btnRunQC = headerRow2.add("button", undefined, "Run Full QC");
btnRunQC.preferredSize = [105, 24];

var btnReset = headerRow2.add("button", undefined, "Reset");
btnReset.preferredSize = [60, 24];

var btnRefresh = headerRow2.add("button", undefined, "Refresh");
btnRefresh.preferredSize = [70, 24];

var btnExport = headerRow2.add("button", undefined, "Export Report");
btnExport.preferredSize = [100, 24];

// --- Main 2-Column Container ---
var mainCols = win.add("group");
mainCols.orientation = "row";
mainCols.alignChildren = ["fill", "top"];
mainCols.spacing = 8;

// =============================================================
// COLUMN 1: Text & Typography
// =============================================================
var col1 = mainCols.add("panel", undefined, "Typography & Text");
col1.orientation = "column";
col1.alignChildren = ["fill", "top"];
col1.preferredSize.width = 315;
col1.margins = 8;
col1.spacing = 7;

// Row 1.1: Double Spaces
var rowDouble = col1.add("group");
rowDouble.orientation = "row";
rowDouble.alignChildren = ["left", "center"];
rowDouble.spacing = 4;
var lblDoubleTitle = rowDouble.add("statictext", undefined, "Double Space:");
lblDoubleTitle.preferredSize.width = 82;
var lblDoubleSpaceCount = rowDouble.add("statictext", undefined, "0");
lblDoubleSpaceCount.preferredSize.width = 24;
var btnScanDouble = rowDouble.add("button", undefined, "Scan");
btnScanDouble.preferredSize = [38, 21];
var btnReviewDoubleSpace = rowDouble.add("button", undefined, "Rev");
btnReviewDoubleSpace.preferredSize = [34, 21];
var btnFixCurrentDoubleSpace = rowDouble.add("button", undefined, "Fix");
btnFixCurrentDoubleSpace.preferredSize = [32, 21];
var btnFixDoubleSpaces = rowDouble.add("button", undefined, "All");
btnFixDoubleSpaces.preferredSize = [32, 21];

// Row 1.2: Widow Check (Scan | Rev | Fix | All)
var rowWidow = col1.add("group");
rowWidow.orientation = "row";
rowWidow.alignChildren = ["left", "center"];
rowWidow.spacing = 4;
var lblWidowTitle = rowWidow.add("statictext", undefined, "Widows:");
lblWidowTitle.preferredSize.width = 82;
var lblWidowCount = rowWidow.add("statictext", undefined, "0");
lblWidowCount.preferredSize.width = 24;
var btnScanWidow = rowWidow.add("button", undefined, "Scan");
btnScanWidow.preferredSize = [38, 21];
var btnReviewWidow = rowWidow.add("button", undefined, "Rev");
btnReviewWidow.preferredSize = [34, 21];
var btnFixCurrentWidow = rowWidow.add("button", undefined, "Fix");
btnFixCurrentWidow.preferredSize = [32, 21];
var btnFixAllWidow = rowWidow.add("button", undefined, "All");
btnFixAllWidow.preferredSize = [32, 21];

// Row 1.3: Hyphenated Lines & Uncheck Option
var rowHyphen = col1.add("group");
rowHyphen.orientation = "row";
rowHyphen.alignChildren = ["left", "center"];
rowHyphen.spacing = 4;
var lblHyphenTitle = rowHyphen.add("statictext", undefined, "Hyphenation:");
lblHyphenTitle.preferredSize.width = 82;
var lblHyphenCount = rowHyphen.add("statictext", undefined, "0");
lblHyphenCount.preferredSize.width = 24;
var btnScanHyphen = rowHyphen.add("button", undefined, "Scan");
btnScanHyphen.preferredSize = [38, 21];
var btnReviewHyphen = rowHyphen.add("button", undefined, "Review");
btnReviewHyphen.preferredSize = [48, 21];
var btnUncheckHyphen = rowHyphen.add("button", undefined, "Uncheck");
btnUncheckHyphen.preferredSize = [56, 21];

// Row 1.4: Ligatures Used & Fix
var rowLigature = col1.add("group");
rowLigature.orientation = "row";
rowLigature.alignChildren = ["left", "center"];
rowLigature.spacing = 4;
var lblLigatureTitle = rowLigature.add("statictext", undefined, "Ligatures:");
lblLigatureTitle.preferredSize.width = 82;
var lblLigatureCount = rowLigature.add("statictext", undefined, "0");
lblLigatureCount.preferredSize.width = 24;
var btnScanLigature = rowLigature.add("button", undefined, "Scan");
btnScanLigature.preferredSize = [38, 21];
var btnReviewLigature = rowLigature.add("button", undefined, "Rev");
btnReviewLigature.preferredSize = [34, 21];
var btnFixLigature = rowLigature.add("button", undefined, "Off");
btnFixLigature.preferredSize = [32, 21];
var btnAllLigature = rowLigature.add("button", undefined, "All");
btnAllLigature.preferredSize = [32, 21];

// =============================================================
// COLUMN 2: Colors, Assets & Links
// =============================================================
var col2 = mainCols.add("panel", undefined, "Colors, Frames & Links");
col2.orientation = "column";
col2.alignChildren = ["fill", "top"];
col2.preferredSize.width = 315;
col2.margins = 8;
col2.spacing = 7;

// Row 2.1: Unused Swatches
var rowSwatches = col2.add("group");
rowSwatches.orientation = "row";
rowSwatches.alignChildren = ["left", "center"];
rowSwatches.spacing = 4;
var lblSwatchesTitle = rowSwatches.add("statictext", undefined, "Swatches:");
lblSwatchesTitle.preferredSize.width = 82;
var lblUnusedCount = rowSwatches.add("statictext", undefined, "0");
lblUnusedCount.preferredSize.width = 24;
var btnScanSwatches = rowSwatches.add("button", undefined, "Scan");
btnScanSwatches.preferredSize = [38, 21];
var btnRemoveSwatches = rowSwatches.add("button", undefined, "Remove All");
btnRemoveSwatches.preferredSize = [80, 21];

// Row 2.2: Black Color Issues (Strict 100% Solid & 80% Tint Only; No Rich Black)
var rowBlack = col2.add("group");
rowBlack.orientation = "row";
rowBlack.alignChildren = ["left", "center"];
rowBlack.spacing = 4;
var lblBlackTitle = rowBlack.add("statictext", undefined, "Black Issues:");
lblBlackTitle.preferredSize.width = 82;
var lblBlackColorCount = rowBlack.add("statictext", undefined, "0");
lblBlackColorCount.preferredSize.width = 24;
var btnScanBlack = rowBlack.add("button", undefined, "Scan");
btnScanBlack.preferredSize = [38, 21];
var btnReviewBlack = rowBlack.add("button", undefined, "Rev");
btnReviewBlack.preferredSize = [34, 21];
var btnFixCurrentBlack = rowBlack.add("button", undefined, "Fix");
btnFixCurrentBlack.preferredSize = [32, 21];
var btnFixAllBlack = rowBlack.add("button", undefined, "All");
btnFixAllBlack.preferredSize = [32, 21];

// Row 2.3: Image Frames with Fill (Review | Clear)
var rowImgFill = col2.add("group");
rowImgFill.orientation = "row";
rowImgFill.alignChildren = ["left", "center"];
rowImgFill.spacing = 4;
var lblImgFillTitle = rowImgFill.add("statictext", undefined, "Img Fill Color:");
lblImgFillTitle.preferredSize.width = 82;
var lblImageFrameCount = rowImgFill.add("statictext", undefined, "0");
lblImageFrameCount.preferredSize.width = 24;
var btnScanImgFill = rowImgFill.add("button", undefined, "Scan");
btnScanImgFill.preferredSize = [38, 21];
var btnReviewImageFrame = rowImgFill.add("button", undefined, "Review");
btnReviewImageFrame.preferredSize = [48, 21];
var btnClearImgFill = rowImgFill.add("button", undefined, "Clear");
btnClearImgFill.preferredSize = [40, 21];

// Row 2.4: Disallowed URLs (.in / com/in)
var rowUrls = col2.add("group");
rowUrls.orientation = "row";
rowUrls.alignChildren = ["left", "center"];
rowUrls.spacing = 4;
var lblUrlTitle = rowUrls.add("statictext", undefined, "URLs (.in):");
lblUrlTitle.preferredSize.width = 82;
var lblUrlCount = rowUrls.add("statictext", undefined, "0");
lblUrlCount.preferredSize.width = 24;
var btnScanUrls = rowUrls.add("button", undefined, "Scan");
btnScanUrls.preferredSize = [38, 21];
var btnReviewUrl = rowUrls.add("button", undefined, "Review");
btnReviewUrl.preferredSize = [55, 21];

// --- Panel: QC Report Log ---
var pnlReport = win.add("panel", undefined, "QC Report");
pnlReport.alignChildren = ["fill", "fill"];
pnlReport.margins = 6;

var reportBox = pnlReport.add("edittext", undefined, "QC Master Dashboard ready.\nClick 'Run Full QC' or individual [Scan] buttons to begin.\n", {
multiline: true,
readonly: true,
scrollable: true
});
reportBox.preferredSize = [620, 85];

// --- Footer: Credits & Buttons ---
var footerGroup = win.add("group");
footerGroup.orientation = "row";
footerGroup.alignChildren = ["fill", "center"];
footerGroup.alignment = ["fill", "bottom"];

var lblCredits = footerGroup.add("statictext", undefined, "Developed by Nousheeth Iqbal");
lblCredits.preferredSize.width = 380;

var btnClearLog = footerGroup.add("button", undefined, "Clear Log");
btnClearLog.preferredSize = [75, 23];
var btnClose = footerGroup.add("button", undefined, "Close");
btnClose.preferredSize = [65, 23];

// -------------------------------------------------------------
// Event Listeners
// -------------------------------------------------------------
btnRunQC.onClick = function () { runFullQC(); };
btnReset.onClick = function () { pristineReset(); };
btnRefresh.onClick = function () { refreshQC(); };
btnExport.onClick = function () { exportReport(); };

// Individual Scan Listeners
btnScanDouble.onClick = function () { scanDoubleSpaces(); };
btnScanWidow.onClick = function () { scanWidows(); };
btnScanHyphen.onClick = function () { scanHyphenation(); };
btnScanLigature.onClick = function () { scanLigatures(); };
btnScanSwatches.onClick = function () { scanUnusedSwatches(); };
btnScanBlack.onClick = function () { scanBlackColorIssues(); };
btnScanImgFill.onClick = function () { scanImageFramesWithFill(); };
btnScanUrls.onClick = function () { scanDisallowedUrls(); };

// Action Listeners
btnReviewDoubleSpace.onClick = function () { reviewDoubleSpace(); };
btnFixCurrentDoubleSpace.onClick = function () { fixCurrentDoubleSpace(); };
btnFixDoubleSpaces.onClick = function () { fixAllDoubleSpaces(); };

btnReviewWidow.onClick = function () { reviewWidow(); };
btnFixCurrentWidow.onClick = function () { fixCurrentWidow(); };
btnFixAllWidow.onClick = function () { fixAllWidows(); };

btnReviewHyphen.onClick = function () { reviewHyphenation(); };
btnUncheckHyphen.onClick = function () { uncheckAllHyphenation(); };

btnReviewLigature.onClick = function () { reviewLigature(); };
btnFixLigature.onClick = function () { fixCurrentLigature(); };
btnAllLigature.onClick = function () { fixAllLigatures(); };

btnRemoveSwatches.onClick = function () { removeAllUnusedSwatches(); };
btnReviewBlack.onClick = function () { reviewBlackColorIssue(); };
btnFixCurrentBlack.onClick = function () { fixCurrentBlackIssue(); };
btnFixAllBlack.onClick = function () { fixAllBlackIssues(); };

btnReviewImageFrame.onClick = function () { reviewImageFrame(); };
btnClearImgFill.onClick = function () { clearCurrentImgFill(); };

btnReviewUrl.onClick = function () { reviewDisallowedUrl(); };

btnClearLog.onClick = function () { reportBox.text = ""; };
btnClose.onClick = function () {
try { win.close(); } catch (e) {}
$.global.qcmaster_win = null;
};

// Show modeless palette
win.center();
win.show();
})();

 

Translate
NousheethAuthor
Known Participant
Oct 26, 2026 7:21 PM UTC

Check the attached

Translate
NousheethAuthor
Known Participant
Oct 26, 2026 3:51 PM UTC
Asad

#target indesign
#targetengine "qcmaster_engine"

/**
* QC Master Dashboard v2.2
* Print Production Preflight & Audit Script for Adobe InDesign
* Compatible with InDesign CC 2022 through CC 2025+
* Developed by Nousheeth Iqbal
*/

(function () {
// -------------------------------------------------------------
// Global Storage for Issue Navigation
// -------------------------------------------------------------
var state = {
doc: null,
doubleSpaces: [],
doubleSpaceIndex: 0,
widows: [],
widowIndex: 0,
imageFramesWithFill: [],
imageFrameIndex: 0,
blackIssues: [],
blackIssueIndex: 0,
disallowedUrls: [],
disallowedUrlIndex: 0,
hyphenations: [],
hyphenationIndex: 0,
ligatures: [],
ligatureIndex: 0,
unusedSwatches: [],
unusedSwatchesCount: 0
};

function getActiveDocument() {
if (app.documents.length === 0) {
return null;
}
return app.activeDocument;
}

// -------------------------------------------------------------
// Helper: Logging (Newest / Most Recent Entry at the Top)
// -------------------------------------------------------------
function appendLog(txt) {
var time = new Date();
var timeStr = "[" + pad(time.getHours()) + ":" + pad(time.getMinutes()) + ":" + pad(time.getSeconds()) + "] ";
var newEntry = timeStr + txt + "\n";
var currentText = reportBox.text;

if (currentText.length > 25000) {
currentText = currentText.substring(0, 20000);
}

reportBox.text = newEntry + currentText;
}

function pad(n) {
return (n < 10 ? "0" : "") + n;
}

function getItemLocation(target) {
var page = null;
var spread = null;
if (!target) return { page: null, spread: null };

try {
// Case 1: Text target (Paragraph, Line, Word, Character, TextStyleRange, Match)
var tf = null;
try {
if (target.parentTextFrames && target.parentTextFrames.length > 0) {
tf = target.parentTextFrames[0];
}
} catch (eTf) {}

var cur = tf ? tf : target;

// Direct parentPage check
try {
if (cur.parentPage && cur.parentPage.isValid) {
page = cur.parentPage;
}
} catch (ePp) {}

// Ascend hierarchy to find Page and Spread
var node = cur;
while (node) {
try {
if (!page && node.constructor.name === "Page") {
page = node;
}
if (node.constructor.name === "Spread") {
spread = node;
break;
}
if (node.constructor.name === "Document") {
break;
}
} catch (eNode) {}
try {
node = node.parent;
} catch (ePar) {
break;
}
}

// Derive spread from page if needed
if (page && page.isValid && (!spread || !spread.isValid)) {
try {
if (page.parent && page.parent.constructor.name === "Spread") {
spread = page.parent;
}
} catch (eSp) {}
}
} catch (e) {}

return { page: page, spread: spread };
}

function getItemPage(target) {
return getItemLocation(target).page;
}

// -------------------------------------------------------------
// Helper: Automatically Add Unnamed Colors to Swatches Panel
// -------------------------------------------------------------
function addUnnamedColors(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return 0;

try {
if (app.documents.length > 0 && app.activeDocument !== doc) {
app.activeDocument = doc;
}
} catch (eDoc) {}

var beforeCount = doc.swatches.length;
try {
var act = null;
try {
act = app.menuActions.itemByName("$ID/Add Unnamed Colors");
} catch (e1) {}

if (!act || !act.isValid) {
try {
act = app.menuActions.itemByName("Add Unnamed Colors");
} catch (e2) {}
}

if (act && act.isValid && act.enabled) {
act.invoke();
}
} catch (e) {}

var afterCount = doc.swatches.length;
var added = afterCount - beforeCount;
if (added > 0) {
appendLog("Added " + added + " unnamed color(s) to Swatches panel.");
}
return added;
}

// -------------------------------------------------------------
// QC Check 1: Unused Swatches (Auto-adds unnamed colors first)
// -------------------------------------------------------------
function scanUnusedSwatches(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return 0;

// Auto-add any unnamed colors applied to text/objects into the Swatches panel
addUnnamedColors(doc);

state.unusedSwatches = [];
var swatchNames = [];

try {
var rawUnused = doc.unusedSwatches;
var rawUnusedMap = {};
for (var u = 0; u < rawUnused.length; u++) {
try {
rawUnusedMap[rawUnused[u].name] = true;
} catch (eU) {}
}

var panelSwatches = doc.swatches;
for (var i = 0; i < panelSwatches.length; i++) {
var sw = panelSwatches[i];
var swName = sw.name;

// Protect default system swatches
if (swName === "None" || swName === "Registration" || swName === "Black" || swName === "Paper" ||
swName === "[None]" || swName === "[Registration]" || swName === "[Black]" || swName === "[Paper]") {
continue;
}

if (rawUnusedMap[swName]) {
state.unusedSwatches.push(sw);
swatchNames.push(swName);
}
}
} catch (e) {
appendLog("Error scanning unused swatches: " + e.message);
}

state.unusedSwatchesCount = state.unusedSwatches.length;
lblUnusedCount.text = "" + state.unusedSwatchesCount;

if (state.unusedSwatchesCount > 0) {
appendLog("Unused Swatches in Panel (" + state.unusedSwatchesCount + "): " + swatchNames.join(", "));
} else {
appendLog("Swatches: 0 unused swatches in Swatches panel.");
}

return state.unusedSwatchesCount;
}

function removeAllUnusedSwatches(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return;

if (state.unusedSwatches.length === 0) {
scanUnusedSwatches(doc);
}
if (state.unusedSwatches.length === 0) {
appendLog("No unused swatches to remove.");
return;
}

if (!confirm("Are you sure you want to remove all " + state.unusedSwatches.length + " unused swatch(es) from the Swatches panel?", false, "Confirm Remove Unused Swatches")) {
return;
}

app.doScript(function () {
var count = 0;
try {
for (var i = state.unusedSwatches.length - 1; i >= 0; i--) {
var sw = state.unusedSwatches[i];
try {
if (sw && sw.isValid) {
sw.remove();
count++;
}
} catch (err) {}
}
appendLog("Removed " + count + " unused swatches from Swatches panel.");
scanUnusedSwatches(doc);
} catch (e) {
appendLog("Error removing swatches: " + e.message);
}
}, ScriptLanguage.JAVASCRIPT, undefined, UndoModes.ENTIRE_SCRIPT, "QC Remove Unused Swatches");
}

// -------------------------------------------------------------
// QC Check 2: Double Spaces (Scan | Review | Fix | All)
// -------------------------------------------------------------
function scanDoubleSpaces(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return 0;

state.doubleSpaces = [];
state.doubleSpaceIndex = 0;

app.findGrepPreferences = NothingEnum.nothing;
app.changeGrepPreferences = NothingEnum.nothing;
app.findGrepPreferences.findWhat = "\\x{0020}{2,}";

try {
var matches = doc.findGrep();
for (var i = 0; i < matches.length; i++) {
state.doubleSpaces.push(matches[i]);
}
} catch (e) {
appendLog("Error scanning double spaces: " + e.message);
} finally {
app.findGrepPreferences = NothingEnum.nothing;
app.changeGrepPreferences = NothingEnum.nothing;
}

lblDoubleSpaceCount.text = "" + state.doubleSpaces.length;
appendLog("Double Spaces: " + state.doubleSpaces.length + " found.");
return state.doubleSpaces.length;
}

function reviewDoubleSpace() {
if (state.doubleSpaces.length === 0) {
appendLog("No double spaces to review.");
return;
}
if (state.doubleSpaceIndex >= state.doubleSpaces.length) {
state.doubleSpaceIndex = 0;
}

var match = state.doubleSpaces[state.doubleSpaceIndex];
try {
if (match.isValid) {
app.select(match, SelectionOptions.REPLACE_WITH);
zoomToSelectedTarget(match);
var page = getItemPage(match);
appendLog("Reviewing Double Space (" + (state.doubleSpaceIndex + 1) + " of " + state.doubleSpaces.length + ") on " + (page ? "Page " + page.name : "Pasteboard"));
state.doubleSpaceIndex++;
} else {
state.doubleSpaces.splice(state.doubleSpaceIndex, 1);
lblDoubleSpaceCount.text = "" + state.doubleSpaces.length;
reviewDoubleSpace();
}
} catch (e) {
appendLog("Error navigating to double space: " + e.message);
}
}

function fixCurrentDoubleSpace() {
if (state.doubleSpaces.length === 0) {
appendLog("No double space found to fix.");
return;
}

var targetIdx = state.doubleSpaceIndex > 0 ? (state.doubleSpaceIndex - 1) : 0;
if (targetIdx >= state.doubleSpaces.length) {
targetIdx = 0;
}

var match = state.doubleSpaces[targetIdx];
try {
if (match.isValid) {
var page = getItemPage(match);
match.contents = " ";
state.doubleSpaces.splice(targetIdx, 1);
lblDoubleSpaceCount.text = "" + state.doubleSpaces.length;
appendLog("Fixed 1 double space on " + (page ? "Page " + page.name : "Pasteboard") + ". Remaining: " + state.doubleSpaces.length);

if (state.doubleSpaces.length > 0) {
if (targetIdx >= state.doubleSpaces.length) {
state.doubleSpaceIndex = 0;
} else {
state.doubleSpaceIndex = targetIdx;
}
reviewDoubleSpace();
} else {
state.doubleSpaceIndex = 0;
appendLog("All double spaces resolved!");
}
} else {
state.doubleSpaces.splice(targetIdx, 1);
lblDoubleSpaceCount.text = "" + state.doubleSpaces.length;
fixCurrentDoubleSpace();
}
} catch (e) {
appendLog("Error fixing single double space: " + e.message);
}
}

function fixAllDoubleSpaces(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return;

if (!confirm("Are you sure you want to replace all double spaces across the entire document?", false, "Confirm Fix All Double Spaces")) {
return;
}

app.doScript(function () {
app.findGrepPreferences = NothingEnum.nothing;
app.changeGrepPreferences = NothingEnum.nothing;
app.findGrepPreferences.findWhat = "\\x{0020}{2,}";
app.changeGrepPreferences.changeTo = " ";

try {
var changed = doc.changeGrep();
appendLog("Replaced " + changed.length + " double space occurrences with single space.");
scanDoubleSpaces(doc);
} catch (e) {
appendLog("Error fixing double spaces: " + e.message);
} finally {
app.findGrepPreferences = NothingEnum.nothing;
app.changeGrepPreferences = NothingEnum.nothing;
}
}, ScriptLanguage.JAVASCRIPT, undefined, UndoModes.ENTIRE_SCRIPT, "QC Fix All Double Spaces");
}

// -------------------------------------------------------------
// QC Check 3: Widow Check (Excludes 1-word/bullet paragraphs)
// -------------------------------------------------------------
function scanWidows(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return 0;

state.widows = [];
state.widowIndex = 0;

try {
var stories = doc.stories;
for (var s = 0; s < stories.length; s++) {
var story = stories[s];
var paras = story.paragraphs;
for (var p = 0; p < paras.length; p++) {
var para = paras[p];

// Multi-line paragraph check: must have at least 2 lines and at least 3 words total
// (Excludes 1-word bullets and 2-word icon descriptions like "Icon\nLabel")
if (para.lines.length >= 2 && para.words.length >= 3) {
// Skip if it is a bullet/numbered list item with < 4 words
if (para.bulletsAndNumberingListType !== ListType.NO_LIST && para.words.length < 4) {
continue;
}

var lastLine = para.lines.lastItem();
var lastLineClean = lastLine.contents.replace(/[\r\n\s\t]+$/, "");
var wordsInLastLine = lastLine.words;

// Check the line before the last line
var prevLine = para.lines.item(para.lines.length - 2);

// If the line was broken using a manual Forced Line Break (Shift + Enter / Soft Return),
// this is an intentional manual line break, NOT an automated typographic widow!
if (/[\u000A\n]/.test(prevLine.contents)) {
continue;
}

// A widow occurs if the last line has only 1 word, or is a very short stub (<= 8 chars)
if (wordsInLastLine.length === 1 || (lastLineClean.length > 0 && lastLineClean.length <= 8)) {
state.widows.push(para);
}
}
}
}
} catch (e) {
appendLog("Error scanning widows: " + e.message);
}

lblWidowCount.text = "" + state.widows.length;
appendLog("Widows: " + state.widows.length + " found.");
return state.widows.length;
}

function reviewWidow() {
if (state.widows.length === 0) {
appendLog("No widows to review.");
return;
}
if (state.widowIndex >= state.widows.length) {
state.widowIndex = 0;
}

var para = state.widows[state.widowIndex];
try {
if (para.isValid) {
var lastLine = para.lines.lastItem();
app.select(lastLine, SelectionOptions.REPLACE_WITH);
zoomToSelectedTarget(lastLine);
var page = getItemPage(para);
appendLog("Reviewing Widow (" + (state.widowIndex + 1) + " of " + state.widows.length + ") on " + (page ? "Page " + page.name : "Pasteboard") + ": \"" + lastLine.contents.replace(/[\r\n]+$/, "") + "\"");
state.widowIndex++;
} else {
state.widowIndex++;
reviewWidow();
}
} catch (e) {
appendLog("Error selecting widow: " + e.message);
}
}

function fixCurrentWidow() {
if (state.widows.length === 0) {
appendLog("No widow found to fix.");
return;
}

var targetIdx = state.widowIndex > 0 ? (state.widowIndex - 1) : 0;
if (targetIdx >= state.widows.length) {
targetIdx = 0;
}

var para = state.widows[targetIdx];
try {
if (para && para.isValid && para.words.length >= 2) {
var words = para.words;
var lastWord = words.lastItem();
var wordBeforeLast = words.item(words.length - 2);

var story = para.parentStory;
var lastCharOfPrev = wordBeforeLast.characters.lastItem();
var firstCharOfLast = lastWord.characters.firstItem();

var startIdx = lastCharOfPrev.index + 1;
var endIdx = firstCharOfLast.index - 1;

if (startIdx <= endIdx) {
var spaceRange = story.characters.itemByRange(startIdx, endIdx);
if (spaceRange.isValid) {
spaceRange.contents = SpecialCharacters.NONBREAKING_SPACE;
}
} else {
wordBeforeLast.insertionPoints.lastItem().contents = SpecialCharacters.NONBREAKING_SPACE;
}

var page = getItemPage(para);
state.widows.splice(targetIdx, 1);
lblWidowCount.text = "" + state.widows.length;
appendLog("Fixed 1 widow on " + (page ? "Page " + page.name : "Pasteboard") + ". Remaining: " + state.widows.length);

if (state.widows.length > 0) {
if (targetIdx >= state.widows.length) {
state.widowIndex = 0;
} else {
state.widowIndex = targetIdx;
}
reviewWidow();
} else {
lblStatus.text = "Status: Widows Cleared";
}
} else {
state.widows.splice(targetIdx, 1);
lblWidowCount.text = "" + state.widows.length;
if (state.widows.length > 0) reviewWidow();
}
} catch (e) {
appendLog("Error fixing widow: " + e.message);
}
}

function fixAllWidows(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return;

if (state.widows.length === 0) {
scanWidows(doc);
}
if (state.widows.length === 0) {
appendLog("No widows to fix.");
return;
}

if (!confirm("Are you sure you want to fix all " + state.widows.length + " detected widow(s) across the document?", false, "Confirm Fix All Widows")) {
return;
}

app.doScript(function () {
var fixedCount = 0;
var prevRedraw = app.scriptPreferences.enableRedraw;

try {
app.scriptPreferences.enableRedraw = false;
for (var i = 0; i < state.widows.length; i++) {
var para = state.widows[i];
try {
if (para && para.isValid && para.words.length >= 2) {
var words = para.words;
var lastWord = words.lastItem();
var wordBeforeLast = words.item(words.length - 2);

var story = para.parentStory;
var lastCharOfPrev = wordBeforeLast.characters.lastItem();
var firstCharOfLast = lastWord.characters.firstItem();

var startIdx = lastCharOfPrev.index + 1;
var endIdx = firstCharOfLast.index - 1;

if (startIdx <= endIdx) {
var spaceRange = story.characters.itemByRange(startIdx, endIdx);
if (spaceRange.isValid) {
spaceRange.contents = SpecialCharacters.NONBREAKING_SPACE;
fixedCount++;
}
} else {
wordBeforeLast.insertionPoints.lastItem().contents = SpecialCharacters.NONBREAKING_SPACE;
fixedCount++;
}
}
} catch (eOne) {}
}
} catch (eAll) {
appendLog("Error fixing widows: " + eAll.message);
} finally {
app.scriptPreferences.enableRedraw = prevRedraw;
}

appendLog("Inserted non-breaking space on " + fixedCount + " paragraph(s) to eliminate widows.");
scanWidows(doc);
}, ScriptLanguage.JAVASCRIPT, undefined, UndoModes.ENTIRE_SCRIPT, "QC Fix All Widows");
}

function fixWidows(doc) {
fixAllWidows(doc);
}

// -------------------------------------------------------------
// QC Check 4: Image Frames with Fill Color (Safe DOM check)
// -------------------------------------------------------------
function scanImageFramesWithFill(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return 0;

state.imageFramesWithFill = [];
state.imageFrameIndex = 0;

try {
var noneSwatch = doc.swatches.itemByName("None");
var seen = {};

var graphics = doc.allGraphics;
for (var i = 0; i < graphics.length; i++) {
var g = graphics[i];
var frame = g.parent;
if (frame && frame.isValid) {
var frameId = "" + frame.id;
if (!seen[frameId]) {
try {
var fc = frame.fillColor;
if (fc && fc.isValid) {
var fcName = fc.name;
if (fcName !== "None" && fcName !== "[None]" && fcName !== "" && fc !== noneSwatch) {
var fTint = -1;
try { fTint = frame.fillTint; } catch (errT) {}
if (fTint === -1 || fTint > 0) {
seen[frameId] = true;
state.imageFramesWithFill.push({
frame: frame,
fillName: fcName,
tint: fTint
});
}
}
}
} catch (errF) {}
}
}
}

var allItems = doc.allPageItems;
for (var j = 0; j < allItems.length; j++) {
var item = allItems[j];
try {
var hasG = false;
try {
if (item.allGraphics && item.allGraphics.length > 0) hasG = true;
else if (item.graphics && item.graphics.length > 0) hasG = true;
} catch (eG) {}

if (hasG) {
var itemId = "" + item.id;
if (!seen[itemId]) {
var ifc = item.fillColor;
if (ifc && ifc.isValid) {
var ifcName = ifc.name;
if (ifcName !== "None" && ifcName !== "[None]" && ifcName !== "" && ifc !== noneSwatch) {
var itint = -1;
try { itint = item.fillTint; } catch (eT) {}
if (itint === -1 || itint > 0) {
seen[itemId] = true;
state.imageFramesWithFill.push({
frame: item,
fillName: ifcName,
tint: itint
});
}
}
}
}
}
} catch (eItem) {}
}
} catch (e) {
appendLog("Error checking image frames: " + e.message);
}

lblImageFrameCount.text = "" + state.imageFramesWithFill.length;
appendLog("Image Frames with Fill Color: " + state.imageFramesWithFill.length + " found.");
return state.imageFramesWithFill.length;
}

function reviewImageFrame() {
if (state.imageFramesWithFill.length === 0) {
appendLog("No image frames with fill color found.");
return;
}
if (state.imageFrameIndex >= state.imageFramesWithFill.length) {
state.imageFrameIndex = 0;
}

var item = state.imageFramesWithFill[state.imageFrameIndex];
var frame = item.frame;
try {
if (frame && frame.isValid) {
app.select(frame, SelectionOptions.REPLACE_WITH);
zoomToSelectedTarget(frame);
var page = getItemPage(frame);
appendLog("Reviewing Image Frame (" + (state.imageFrameIndex + 1) + " of " + state.imageFramesWithFill.length + ") on " + (page ? "Page " + page.name : "Pasteboard") + " [Fill: " + item.fillName + "]");
state.imageFrameIndex++;
} else {
state.imageFrameIndex++;
reviewImageFrame();
}
} catch (e) {
appendLog("Error selecting frame: " + e.message);
}
}

function clearCurrentImgFill() {
if (state.imageFramesWithFill.length === 0) {
appendLog("No image frame fill to clear.");
return;
}

var targetIdx = state.imageFrameIndex > 0 ? (state.imageFrameIndex - 1) : 0;
if (targetIdx >= state.imageFramesWithFill.length) targetIdx = 0;

var item = state.imageFramesWithFill[targetIdx];
var frame = item.frame;
try {
if (frame && frame.isValid) {
var doc = getActiveDocument();
frame.fillColor = doc.swatches.itemByName("None");
var page = getItemPage(frame);
state.imageFramesWithFill.splice(targetIdx, 1);
lblImageFrameCount.text = "" + state.imageFramesWithFill.length;
appendLog("Cleared fill color on Image Frame (" + (page ? "Page " + page.name : "Pasteboard") + "). Remaining: " + state.imageFramesWithFill.length);

if (state.imageFramesWithFill.length > 0) {
if (targetIdx >= state.imageFramesWithFill.length) state.imageFrameIndex = 0;
else state.imageFrameIndex = targetIdx;
reviewImageFrame();
} else {
state.imageFrameIndex = 0;
appendLog("All image frame fill colors cleared!");
}
} else {
state.imageFramesWithFill.splice(targetIdx, 1);
lblImageFrameCount.text = "" + state.imageFramesWithFill.length;
clearCurrentImgFill();
}
} catch (e) {
appendLog("Error clearing frame fill: " + e.message);
}
}

// -------------------------------------------------------------
// QC Check 5: Black Color Issues (Strict Tint 80% or 100% Solid Only; No Rich Black)
// -------------------------------------------------------------
function isRichBlack(swatch) {
if (!swatch || !swatch.isValid) return false;
var name = "";
try { name = (swatch.name || "").toLowerCase(); } catch (eN) {}

if (name === "[registration]" || name === "registration") {
return false;
}

if (name.indexOf("rich black") !== -1 || name.indexOf("4 color black") !== -1 ||
name.indexOf("4-color black") !== -1 || name.indexOf("4-colour black") !== -1) {
return true;
}

try {
if (swatch.space === ColorSpace.CMYK) {
var cv = swatch.colorValue;
if (cv && cv.length >= 4) {
var c = cv[0], m = cv[1], y = cv[2], k = cv[3];
// 4-color black: all 4 CMYK channels present with high K
if (c > 0 && m > 0 && y > 0 && k >= 40) {
return true;
}
// Or high total ink with dominant K and other process inks
if (k >= 60 && (c + m + y) >= 20) {
return true;
}
}
}
} catch (eCv) {}
return false;
}

function isBlackColor(swatch) {
if (!swatch || !swatch.isValid) return false;
var name = "";
try { name = swatch.name; } catch (eN) {}
if (name === "Black" || name === "[Black]") {
return true;
}
if (isRichBlack(swatch)) {
return true;
}
try {
if (swatch.space === ColorSpace.CMYK) {
var cv = swatch.colorValue;
if (cv && cv.length >= 4) {
// Pure K (1-color black)
if (cv[0] === 0 && cv[1] === 0 && cv[2] === 0 && cv[3] > 0) {
return true;
}
if (name.toLowerCase().indexOf("black") !== -1 && cv[3] > 0) {
return true;
}
}
}
} catch (e) {}
return false;
}

function getEffectiveTint(swatch, appliedTint) {
var baseK = 100;
try {
if (swatch.space === ColorSpace.CMYK) {
var cv = swatch.colorValue;
if (cv && cv[0] === 0 && cv[1] === 0 && cv[2] === 0) {
baseK = cv[3];
}
}
} catch (e) {}
if (appliedTint === -1) {
return Math.round(baseK);
}
return Math.round((baseK * appliedTint) / 100);
}

function isApprovedBlack(swatch, appliedTint) {
if (!swatch || !swatch.isValid) return false;

// Rich Black (4-color black) is strictly NOT allowed!
if (isRichBlack(swatch)) {
return false;
}

var effK = getEffectiveTint(swatch, appliedTint);
// Only 100% Solid Black and 80% Black Tint are approved!
return (effK === 100 || effK === 80);
}

function scanBlackColorIssues(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return 0;

// Auto-add unnamed colors so unnamed tints of black get identified as swatches
addUnnamedColors(doc);

state.blackIssues = [];
state.blackIssueIndex = 0;

try {
// 1. Text scans
var stories = doc.stories;
for (var s = 0; s < stories.length; s++) {
var story = stories[s];
var ranges = story.textStyleRanges;
for (var r = 0; r < ranges.length; r++) {
var range = ranges[r];
if (range.contents.replace(/[\r\n\s\t]+/g, "").length === 0) {
continue;
}

try {
var fc = range.fillColor;
if (fc && fc.isValid && isBlackColor(fc)) {
if (!isApprovedBlack(fc, range.fillTint)) {
var isRich = isRichBlack(fc);
var effK = isRich ? "4-Color" : getEffectiveTint(fc, range.fillTint);
var sampleText = range.contents;
if (sampleText.length > 22) {
sampleText = sampleText.substring(0, 19) + "...";
}
var descText = isRich ?
("Text Fill Rich Black (4-Color Black - Not Allowed) [\"" + sampleText.replace(/[\r\n]+/g, " ") + "\"]") :
("Text Fill Black " + effK + "% (Approved: 100%/80% only) [\"" + sampleText.replace(/[\r\n]+/g, " ") + "\"]");

state.blackIssues.push({
target: range,
type: "textFill",
effK: isRich ? 100 : effK,
isRich: isRich,
desc: descText
});
}
}
} catch (eF) {}

try {
if (range.strokeWeight > 0) {
var sc = range.strokeColor;
if (sc && sc.isValid && isBlackColor(sc)) {
if (!isApprovedBlack(sc, range.strokeTint)) {
var isRichS = isRichBlack(sc);
var effStrokeK = isRichS ? "4-Color" : getEffectiveTint(sc, range.strokeTint);
var descStroke = isRichS ?
"Text Stroke Rich Black (4-Color Black - Not Allowed)" :
("Text Stroke Black " + effStrokeK + "% (Approved: 100%/80% only)");

state.blackIssues.push({
target: range,
type: "textStroke",
effK: isRichS ? 100 : effStrokeK,
isRich: isRichS,
desc: descStroke
});
}
}
}
} catch (eS) {}
}
}

// 2. PageItems scans
var pageItems = doc.allPageItems;
for (var i = 0; i < pageItems.length; i++) {
var item = pageItems[i];

try {
var ifc = item.fillColor;
if (ifc && ifc.isValid && isBlackColor(ifc)) {
var ift = -1;
try { ift = item.fillTint; } catch (eT) {}
if (!isApprovedBlack(ifc, ift)) {
var isRichIF = isRichBlack(ifc);
var effItemK = isRichIF ? "4-Color" : getEffectiveTint(ifc, ift);
var descIF = isRichIF ?
(item.constructor.name + " Fill Rich Black (4-Color Black - Not Allowed)") :
(item.constructor.name + " Fill Black " + effItemK + "% (Approved: 100%/80% only)");

state.blackIssues.push({
target: item,
type: "itemFill",
effK: isRichIF ? 100 : effItemK,
isRich: isRichIF,
desc: descIF
});
}
}
} catch (eIF) {}

try {
var isw = 0;
try { isw = item.strokeWeight; } catch (eSW) {}
if (isw > 0) {
var isc = item.strokeColor;
if (isc && isc.isValid && isBlackColor(isc)) {
var ist = -1;
try { ist = item.strokeTint; } catch (eST) {}
if (!isApprovedBlack(isc, ist)) {
var isRichIS = isRichBlack(isc);
var effItemStrokeK = isRichIS ? "4-Color" : getEffectiveTint(isc, ist);
var descIS = isRichIS ?
(item.constructor.name + " Stroke Rich Black (4-Color Black - Not Allowed)") :
(item.constructor.name + " Stroke Black " + effItemStrokeK + "% (Approved: 100%/80% only)");

state.blackIssues.push({
target: item,
type: "itemStroke",
effK: isRichIS ? 100 : effItemStrokeK,
isRich: isRichIS,
desc: descIS
});
}
}
}
} catch (eIS) {}
}
} catch (e) {
appendLog("Error scanning black color issues: " + e.message);
}

lblBlackColorCount.text = "" + state.blackIssues.length;
appendLog("Black Color Issues: " + state.blackIssues.length + " found.");
return state.blackIssues.length;
}

function reviewBlackColorIssue() {
if (state.blackIssues.length === 0) {
appendLog("No black color issues to review.");
return;
}
if (state.blackIssueIndex >= state.blackIssues.length) {
state.blackIssueIndex = 0;
}

var issue = state.blackIssues[state.blackIssueIndex];
try {
if (issue.target.isValid) {
app.select(issue.target, SelectionOptions.REPLACE_WITH);
zoomToSelectedTarget(issue.target);
var page = getItemPage(issue.target);
var pageName = page ? "Page " + page.name : "Pasteboard";
appendLog("Reviewing Black Issue (" + (state.blackIssueIndex + 1) + " of " + state.blackIssues.length + ") on " + pageName + ": " + issue.desc);
state.blackIssueIndex++;
} else {
state.blackIssueIndex++;
reviewBlackColorIssue();
}
} catch (e) {
appendLog("Error navigating to black issue: " + e.message);
}
}

function fixCurrentBlackIssue() {
if (state.blackIssues.length === 0) {
appendLog("No black color issue found to fix.");
return;
}

var targetIdx = state.blackIssueIndex > 0 ? (state.blackIssueIndex - 1) : 0;
if (targetIdx >= state.blackIssues.length) {
targetIdx = 0;
}

var issue = state.blackIssues[targetIdx];
var doc = getActiveDocument();
if (!doc) return;

var blackSwatch = doc.swatches.itemByName("[Black]");
if (!blackSwatch || !blackSwatch.isValid) {
blackSwatch = doc.swatches.itemByName("Black");
}

try {
if (issue.target && issue.target.isValid) {
var targetTint = (!issue.isRich && issue.effK >= 70 && issue.effK < 90) ? 80 : 100;
if (issue.type === "textFill") {
issue.target.fillColor = blackSwatch;
issue.target.fillTint = targetTint;
} else if (issue.type === "textStroke") {
issue.target.strokeColor = blackSwatch;
issue.target.strokeTint = targetTint;
} else if (issue.type === "itemFill") {
issue.target.fillColor = blackSwatch;
try { issue.target.fillTint = targetTint; } catch (eT1) {}
} else if (issue.type === "itemStroke") {
issue.target.strokeColor = blackSwatch;
try { issue.target.strokeTint = targetTint; } catch (eT2) {}
}

var page = getItemPage(issue.target);
var pageName = page ? "Page " + page.name : "Pasteboard";
state.blackIssues.splice(targetIdx, 1);
lblBlackColorCount.text = "" + state.blackIssues.length;
appendLog("Fixed 1 Black issue to " + targetTint + "% on " + pageName + ". Remaining: " + state.blackIssues.length);

if (state.blackIssues.length > 0) {
if (targetIdx >= state.blackIssues.length) {
state.blackIssueIndex = 0;
} else {
state.blackIssueIndex = targetIdx;
}
reviewBlackColorIssue();
} else {
lblStatus.text = "Status: Black Issues Cleared";
}
} else {
state.blackIssues.splice(targetIdx, 1);
lblBlackColorCount.text = "" + state.blackIssues.length;
if (state.blackIssues.length > 0) reviewBlackColorIssue();
}
} catch (e) {
appendLog("Error fixing black issue: " + e.message);
}
}

function fixAllBlackIssues(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return;

if (state.blackIssues.length === 0) {
scanBlackColorIssues(doc);
}
if (state.blackIssues.length === 0) {
appendLog("No black color issues to fix.");
return;
}

if (!confirm("Are you sure you want to fix all " + state.blackIssues.length + " unapproved black issue(s) across the document to approved values (100% Solid Black / 80% Tint)?", false, "Confirm Fix All Black Issues")) {
return;
}

app.doScript(function () {
var blackSwatch = doc.swatches.itemByName("[Black]");
if (!blackSwatch || !blackSwatch.isValid) {
blackSwatch = doc.swatches.itemByName("Black");
}

var fixedCount = 0;
var prevRedraw = app.scriptPreferences.enableRedraw;
try {
app.scriptPreferences.enableRedraw = false;
for (var i = 0; i < state.blackIssues.length; i++) {
var issue = state.blackIssues[i];
try {
if (issue.target && issue.target.isValid) {
var targetTint = (!issue.isRich && issue.effK >= 70 && issue.effK < 90) ? 80 : 100;
if (issue.type === "textFill") {
issue.target.fillColor = blackSwatch;
issue.target.fillTint = targetTint;
fixedCount++;
} else if (issue.type === "textStroke") {
issue.target.strokeColor = blackSwatch;
issue.target.strokeTint = targetTint;
fixedCount++;
} else if (issue.type === "itemFill") {
issue.target.fillColor = blackSwatch;
try { issue.target.fillTint = targetTint; } catch (eT1) {}
fixedCount++;
} else if (issue.type === "itemStroke") {
issue.target.strokeColor = blackSwatch;
try { issue.target.strokeTint = targetTint; } catch (eT2) {}
fixedCount++;
}
}
} catch (eOne) {}
}
appendLog("Fixed " + fixedCount + " black color issue(s) across document.");
scanBlackColorIssues(doc);
} catch (eAll) {
appendLog("Error fixing all black issues: " + eAll.message);
} finally {
app.scriptPreferences.enableRedraw = prevRedraw;
}
}, ScriptLanguage.JAVASCRIPT, undefined, UndoModes.ENTIRE_SCRIPT, "QC Fix All Black Issues");
}

// -------------------------------------------------------------
// QC Check 6: Indian / Disallowed URLs (.in / com/in)
// -------------------------------------------------------------
function isDisallowedIndianUrl(url) {
if (!url) return false;
var cleanUrl = url.toLowerCase();
if (/(?:\.in|\.co\.in|\.org\.in|\.net\.in)(?:[\/\?#]|$)/i.test(cleanUrl)) {
return true;
}
if (/(?:com\/in|\/en-in|\/in)(?:[\/\?#]|$)/i.test(cleanUrl)) {
return true;
}
return false;
}

function scanDisallowedUrls(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return 0;

state.disallowedUrls = [];
state.disallowedUrlIndex = 0;

try {
var hyperlinks = doc.hyperlinks;
for (var h = 0; h < hyperlinks.length; h++) {
var hl = hyperlinks[h];
var dest = hl.destination;
if (dest && dest.hasOwnProperty("destinationURL")) {
var url = dest.destinationURL;
if (isDisallowedIndianUrl(url)) {
var sourceItem = null;
if (hl.source) {
if (hl.source.hasOwnProperty("sourceText")) {
sourceItem = hl.source.sourceText;
} else if (hl.source.hasOwnProperty("sourcePageItem")) {
sourceItem = hl.source.sourcePageItem;
}
}
state.disallowedUrls.push({
url: url,
target: sourceItem,
type: "Hyperlink Object"
});
}
}
}

app.findGrepPreferences = NothingEnum.nothing;
app.findGrepPreferences.findWhat = "(?i)\\b(?:https?:\\/\\/|www\\.)[^\\s<>'\"\\)\\]]+";

var textMatches = doc.findGrep();
for (var m = 0; m < textMatches.length; m++) {
var matchText = textMatches[m].contents;
if (isDisallowedIndianUrl(matchText)) {
state.disallowedUrls.push({
url: matchText,
target: textMatches[m],
type: "Text URL"
});
}
}
} catch (e) {
appendLog("Error scanning disallowed URLs: " + e.message);
} finally {
app.findGrepPreferences = NothingEnum.nothing;
}

lblUrlCount.text = "" + state.disallowedUrls.length;
appendLog("Disallowed URLs (.in): " + state.disallowedUrls.length + " found.");
return state.disallowedUrls.length;
}

function reviewDisallowedUrl() {
if (state.disallowedUrls.length === 0) {
appendLog("No disallowed URLs to review.");
return;
}
if (state.disallowedUrlIndex >= state.disallowedUrls.length) {
state.disallowedUrlIndex = 0;
}

var item = state.disallowedUrls[state.disallowedUrlIndex];
try {
if (item.target && item.target.isValid) {
app.select(item.target, SelectionOptions.REPLACE_WITH);
zoomToSelectedTarget(item.target);
var page = getItemPage(item.target);
var pageName = page ? "Page " + page.name : "Pasteboard";
appendLog("Reviewing URL (" + (state.disallowedUrlIndex + 1) + " of " + state.disallowedUrls.length + ") on " + pageName + " [" + item.type + "]: " + item.url);
state.disallowedUrlIndex++;
} else {
appendLog("URL (" + (state.disallowedUrlIndex + 1) + " of " + state.disallowedUrls.length + "): " + item.url);
state.disallowedUrlIndex++;
if (state.disallowedUrlIndex < state.disallowedUrls.length) {
reviewDisallowedUrl();
}
}
} catch (e) {
appendLog("Error navigating to disallowed URL: " + e.message);
}
}

// -------------------------------------------------------------
// QC Check 7: Hyphenation Check & Uncheck All
// -------------------------------------------------------------
function scanHyphenation(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return 0;

state.hyphenations = [];
state.hyphenationIndex = 0;

try {
var stories = doc.stories;
for (var s = 0; s < stories.length; s++) {
var story = stories[s];
var paras = story.paragraphs;
for (var p = 0; p < paras.length; p++) {
var para = paras[p];
if (para.hyphenation === true) {
var words = para.words;
var hasBrokenWord = false;

for (var w = 0; w < words.length; w++) {
var word = words[w];
if (word.lines.length > 1) {
state.hyphenations.push({
target: word,
para: para,
desc: "Hyphenated Word: \"" + word.contents + "\""
});
hasBrokenWord = true;
}
}

if (!hasBrokenWord && para.lines.length > 1) {
state.hyphenations.push({
target: para,
para: para,
desc: "Hyphenate Enabled on Paragraph (" + para.lines.length + " lines)"
});
}
}
}
}
} catch (e) {
appendLog("Error scanning hyphenated text: " + e.message);
}

lblHyphenCount.text = "" + state.hyphenations.length;
appendLog("Hyphenation: " + state.hyphenations.length + " occurrences found.");
return state.hyphenations.length;
}

function reviewHyphenation() {
if (state.hyphenations.length === 0) {
appendLog("No hyphenation to review.");
return;
}
if (state.hyphenationIndex >= state.hyphenations.length) {
state.hyphenationIndex = 0;
}

var item = state.hyphenations[state.hyphenationIndex];
try {
if (item.target.isValid) {
app.select(item.target, SelectionOptions.REPLACE_WITH);
zoomToSelectedTarget(item.target);
var page = getItemPage(item.target);
appendLog("Reviewing Hyphenation (" + (state.hyphenationIndex + 1) + " of " + state.hyphenations.length + ") on " + (page ? "Page " + page.name : "Pasteboard") + ": " + item.desc);
state.hyphenationIndex++;
} else {
state.hyphenationIndex++;
reviewHyphenation();
}
} catch (e) {
appendLog("Error navigating to hyphenation: " + e.message);
}
}

function uncheckAllHyphenation(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return;

if (!confirm("Are you sure you want to uncheck hyphenation across all paragraphs in the document?", false, "Confirm Uncheck Hyphenation")) {
return;
}

app.doScript(function () {
var count = 0;
var prevRedraw = app.scriptPreferences.enableRedraw;
try {
app.scriptPreferences.enableRedraw = false;
var stories = doc.stories;
for (var s = 0; s < stories.length; s++) {
var paras = stories[s].paragraphs;
for (var p = 0; p < paras.length; p++) {
if (paras[p].hyphenation === true) {
paras[p].hyphenation = false;
count++;
}
}
}
appendLog("Unchecked Hyphenate on " + count + " paragraphs across the document.");
scanHyphenation(doc);
} catch (e) {
appendLog("Error unchecking hyphenation: " + e.message);
} finally {
app.scriptPreferences.enableRedraw = prevRedraw;
}
}, ScriptLanguage.JAVASCRIPT, undefined, UndoModes.ENTIRE_SCRIPT, "QC Uncheck Hyphenation");
}

// -------------------------------------------------------------
// QC Check 8: Ligatures Check & Turn Off Fix
// -------------------------------------------------------------
function scanLigatures(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return 0;

state.ligatures = [];
state.ligatureIndex = 0;

try {
var stories = doc.stories;
for (var s = 0; s < stories.length; s++) {
var ranges = stories[s].textStyleRanges;
for (var r = 0; r < ranges.length; r++) {
var range = ranges[r];
if (range.contents.replace(/[\r\n\s\t]+/g, "").length === 0) continue;

if (range.ligatures === true) {
if (/(fi|fl|ff|ffi|ffl|[fiflffffiffl])/i.test(range.contents)) {
state.ligatures.push(range);
}
}
}
}
} catch (e) {
appendLog("Error scanning ligatures: " + e.message);
}

lblLigatureCount.text = "" + state.ligatures.length;
appendLog("Ligatures Used: " + state.ligatures.length + " ranges found.");
return state.ligatures.length;
}

function reviewLigature() {
if (state.ligatures.length === 0) {
appendLog("No ligatures to review.");
return;
}
if (state.ligatureIndex >= state.ligatures.length) {
state.ligatureIndex = 0;
}

var range = state.ligatures[state.ligatureIndex];
try {
if (range.isValid) {
app.select(range, SelectionOptions.REPLACE_WITH);
zoomToSelectedTarget(range);
var page = getItemPage(range);
var sample = range.contents;
if (sample.length > 22) sample = sample.substring(0, 19) + "...";
appendLog("Reviewing Ligature (" + (state.ligatureIndex + 1) + " of " + state.ligatures.length + ") on " + (page ? "Page " + page.name : "Pasteboard") + ": \"" + sample.replace(/[\r\n]+/g, " ") + "\"");
state.ligatureIndex++;
} else {
state.ligatureIndex++;
reviewLigature();
}
} catch (e) {
appendLog("Error navigating to ligature: " + e.message);
}
}

function fixCurrentLigature() {
if (state.ligatures.length === 0) {
appendLog("No ligature found to fix.");
return;
}

var targetIdx = state.ligatureIndex > 0 ? (state.ligatureIndex - 1) : 0;
if (targetIdx >= state.ligatures.length) targetIdx = 0;

var range = state.ligatures[targetIdx];
try {
if (range.isValid) {
range.ligatures = false;
state.ligatures.splice(targetIdx, 1);
lblLigatureCount.text = "" + state.ligatures.length;
appendLog("Turned off ligatures on selected text. Remaining: " + state.ligatures.length);

if (state.ligatures.length > 0) {
if (targetIdx >= state.ligatures.length) state.ligatureIndex = 0;
else state.ligatureIndex = targetIdx;
reviewLigature();
} else {
state.ligatureIndex = 0;
appendLog("All ligatures turned off!");
}
} else {
state.ligatures.splice(targetIdx, 1);
lblLigatureCount.text = "" + state.ligatures.length;
fixCurrentLigature();
}
} catch (e) {
appendLog("Error fixing ligature: " + e.message);
}
}

function fixAllLigatures(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return;

if (!confirm("Are you sure you want to turn off ligatures across the entire document?", false, "Confirm Turn Off All Ligatures")) {
return;
}

app.doScript(function () {
var count = 0;
var prevRedraw = app.scriptPreferences.enableRedraw;
try {
app.scriptPreferences.enableRedraw = false;
var stories = doc.stories;
for (var s = 0; s < stories.length; s++) {
var ranges = stories[s].textStyleRanges;
for (var r = 0; r < ranges.length; r++) {
if (ranges[r].ligatures === true) {
ranges[r].ligatures = false;
count++;
}
}
}
appendLog("Turned off ligatures on " + count + " text ranges across document.");
scanLigatures(doc);
} catch (e) {
appendLog("Error turning off ligatures: " + e.message);
} finally {
app.scriptPreferences.enableRedraw = prevRedraw;
}
}, ScriptLanguage.JAVASCRIPT, undefined, UndoModes.ENTIRE_SCRIPT, "QC Turn Off Ligatures");
}

// -------------------------------------------------------------
// Zoom Helper (Maintains user's current zoom level like native Find/Change)
// -------------------------------------------------------------
function zoomToSelectedTarget(target) {
try {
var loc = getItemLocation(target);
var curZoom = 100;
try {
curZoom = app.activeWindow.zoomPercentage;
} catch (eZ) {}

// 1. Switch active spread if target is on a different spread
if (loc.spread && loc.spread.isValid) {
if (app.activeWindow.activeSpread !== loc.spread) {
app.activeWindow.activeSpread = loc.spread;
}
}

// 2. Set active page on that spread
if (loc.page && loc.page.isValid) {
app.activeWindow.activePage = loc.page;
}

// 3. Center viewport on the selected item while keeping current zoom level
try {
var act = null;
try { act = app.menuActions.itemByName("$ID/Fit Selection in Window"); } catch (e1) {}
if (!act || !act.isValid) {
try { act = app.menuActions.itemByName("Fit Selection in Window"); } catch (e2) {}
}
if (act && act.isValid && act.enabled) {
act.invoke();
}
} catch (eFit) {}

// 4. Restore the user's exact zoom level (e.g. 150%, 200%, etc.)
// so it never forces "fit page" or "fit frame", behaving exactly like native Find/Change!
if (curZoom && curZoom > 0) {
app.activeWindow.zoomPercentage = curZoom;
}
} catch (e) {}
}

// -------------------------------------------------------------
// Full QC Runner & Pristine Reset
// -------------------------------------------------------------
function pristineReset() {
state.doubleSpaces = [];
state.doubleSpaceIndex = 0;
state.widows = [];
state.widowIndex = 0;
state.imageFramesWithFill = [];
state.imageFrameIndex = 0;
state.blackIssues = [];
state.blackIssueIndex = 0;
state.disallowedUrls = [];
state.disallowedUrlIndex = 0;
state.hyphenations = [];
state.hyphenationIndex = 0;
state.ligatures = [];
state.ligatureIndex = 0;
state.unusedSwatches = [];
state.unusedSwatchesCount = 0;

lblUnusedCount.text = "0";
lblDoubleSpaceCount.text = "0";
lblWidowCount.text = "0";
lblImageFrameCount.text = "0";
lblBlackColorCount.text = "0";
lblUrlCount.text = "0";
lblHyphenCount.text = "0";
lblLigatureCount.text = "0";

var doc = getActiveDocument();
lblDocName.text = "Document: " + (doc ? doc.name : "None");
lblStatus.text = "Status: Ready";

try {
app.select(NothingEnum.nothing);
} catch (e) {}

reportBox.text = "QC Master Dashboard ready.\nClick 'Run Full QC' or individual [Scan] buttons to begin.\n";
}

function runFullQC() {
var doc = getActiveDocument();
if (!doc) {
alert("Please open an InDesign document before running QC.", "No Document");
return;
}

lblDocName.text = "Document: " + doc.name;
lblStatus.text = "Status: Running...";

var prevRedraw = app.scriptPreferences.enableRedraw;
try {
// Speed optimization: suppress screen redraws during multi-page audits (50+ pages)
app.scriptPreferences.enableRedraw = false;

// Step 1: Detect and add any unnamed colors to the Swatches panel
addUnnamedColors(doc);

var cSwatches = scanUnusedSwatches(doc);
var cDouble = scanDoubleSpaces(doc);
var cWidows = scanWidows(doc);
var cImgFill = scanImageFramesWithFill(doc);
var cBlack = scanBlackColorIssues(doc);
var cUrls = scanDisallowedUrls(doc);
var cHyphens = scanHyphenation(doc);
var cLigatures = scanLigatures(doc);

var total = cSwatches + cDouble + cWidows + cImgFill + cBlack + cUrls + cHyphens + cLigatures;
lblStatus.text = "Status: Complete (" + total + " issues)";

// Post summary header at the top
appendLog("=========================================");
appendLog("Full QC Complete for: " + doc.name + " (" + total + " total issues)");
appendLog("=========================================");
} catch (eQC) {
lblStatus.text = "Status: Error";
appendLog("Error during Full QC: " + eQC.message);
} finally {
app.scriptPreferences.enableRedraw = prevRedraw;
}
}

function refreshQC() {
var doc = getActiveDocument();
if (!doc) {
lblDocName.text = "Document: None";
lblStatus.text = "Status: No document open";
return;
}
lblDocName.text = "Document: " + doc.name;
lblStatus.text = "Status: Ready";
runFullQC();
}

function exportReport() {
var reportContent = reportBox.text;
if (!reportContent || reportContent.length === 0) {
alert("The report log is empty. Run QC before exporting.", "Empty Log");
return;
}

var doc = getActiveDocument();
var defaultPath = "~/Desktop";
if (doc && doc.saved) {
defaultPath = doc.filePath;
}

var fileName = (doc ? doc.name.replace(/\.indd$/i, "") : "Document") + "_QC_Report.txt";
var file = new File(defaultPath + "/" + fileName).saveDlg("Save QC Audit Report", "Text File:*.txt");
if (file) {
file.encoding = "UTF-8";
file.open("w");
file.write(reportContent);
file.close();
appendLog("Report successfully saved to: " + file.fsName);
}
}

// -------------------------------------------------------------
// UI Construction (Compact 2-Column Layout)
// -------------------------------------------------------------
var win = new Window("palette", "QC Master Dashboard v1.8", undefined, { resizeable: true });
win.orientation = "column";
win.alignChildren = ["fill", "top"];
win.margins = 10;
win.spacing = 6;

// --- Compact Header: Document Information & Actions ---
var pnlHeader = win.add("panel", undefined, "Document & Actions");
pnlHeader.orientation = "column";
pnlHeader.alignChildren = ["fill", "top"];
pnlHeader.margins = 8;
pnlHeader.spacing = 6;

var headerRow1 = pnlHeader.add("group");
headerRow1.orientation = "row";
headerRow1.alignChildren = ["left", "center"];
headerRow1.spacing = 16;

var initialDoc = getActiveDocument();
var docNameText = initialDoc ? initialDoc.name : "No active document";
var lblDocName = headerRow1.add("statictext", undefined, "Document: " + docNameText);
lblDocName.preferredSize.width = 340;
var lblStatus = headerRow1.add("statictext", undefined, "Status: Ready");
lblStatus.preferredSize.width = 250;

var headerRow2 = pnlHeader.add("group");
headerRow2.orientation = "row";
headerRow2.alignChildren = ["left", "center"];
headerRow2.spacing = 8;

var btnRunQC = headerRow2.add("button", undefined, "Run Full QC");
btnRunQC.preferredSize = [105, 24];

var btnReset = headerRow2.add("button", undefined, "Reset");
btnReset.preferredSize = [60, 24];

var btnRefresh = headerRow2.add("button", undefined, "Refresh");
btnRefresh.preferredSize = [70, 24];

var btnExport = headerRow2.add("button", undefined, "Export Report");
btnExport.preferredSize = [100, 24];

// --- Main 2-Column Container ---
var mainCols = win.add("group");
mainCols.orientation = "row";
mainCols.alignChildren = ["fill", "top"];
mainCols.spacing = 8;

// =============================================================
// COLUMN 1: Text & Typography
// =============================================================
var col1 = mainCols.add("panel", undefined, "Typography & Text");
col1.orientation = "column";
col1.alignChildren = ["fill", "top"];
col1.preferredSize.width = 315;
col1.margins = 8;
col1.spacing = 7;

// Row 1.1: Double Spaces
var rowDouble = col1.add("group");
rowDouble.orientation = "row";
rowDouble.alignChildren = ["left", "center"];
rowDouble.spacing = 4;
var lblDoubleTitle = rowDouble.add("statictext", undefined, "Double Space:");
lblDoubleTitle.preferredSize.width = 82;
var lblDoubleSpaceCount = rowDouble.add("statictext", undefined, "0");
lblDoubleSpaceCount.preferredSize.width = 24;
var btnScanDouble = rowDouble.add("button", undefined, "Scan");
btnScanDouble.preferredSize = [38, 21];
var btnReviewDoubleSpace = rowDouble.add("button", undefined, "Rev");
btnReviewDoubleSpace.preferredSize = [34, 21];
var btnFixCurrentDoubleSpace = rowDouble.add("button", undefined, "Fix");
btnFixCurrentDoubleSpace.preferredSize = [32, 21];
var btnFixDoubleSpaces = rowDouble.add("button", undefined, "All");
btnFixDoubleSpaces.preferredSize = [32, 21];

// Row 1.2: Widow Check (Scan | Rev | Fix | All)
var rowWidow = col1.add("group");
rowWidow.orientation = "row";
rowWidow.alignChildren = ["left", "center"];
rowWidow.spacing = 4;
var lblWidowTitle = rowWidow.add("statictext", undefined, "Widows:");
lblWidowTitle.preferredSize.width = 82;
var lblWidowCount = rowWidow.add("statictext", undefined, "0");
lblWidowCount.preferredSize.width = 24;
var btnScanWidow = rowWidow.add("button", undefined, "Scan");
btnScanWidow.preferredSize = [38, 21];
var btnReviewWidow = rowWidow.add("button", undefined, "Rev");
btnReviewWidow.preferredSize = [34, 21];
var btnFixCurrentWidow = rowWidow.add("button", undefined, "Fix");
btnFixCurrentWidow.preferredSize = [32, 21];
var btnFixAllWidow = rowWidow.add("button", undefined, "All");
btnFixAllWidow.preferredSize = [32, 21];

// Row 1.3: Hyphenated Lines & Uncheck Option
var rowHyphen = col1.add("group");
rowHyphen.orientation = "row";
rowHyphen.alignChildren = ["left", "center"];
rowHyphen.spacing = 4;
var lblHyphenTitle = rowHyphen.add("statictext", undefined, "Hyphenation:");
lblHyphenTitle.preferredSize.width = 82;
var lblHyphenCount = rowHyphen.add("statictext", undefined, "0");
lblHyphenCount.preferredSize.width = 24;
var btnScanHyphen = rowHyphen.add("button", undefined, "Scan");
btnScanHyphen.preferredSize = [38, 21];
var btnReviewHyphen = rowHyphen.add("button", undefined, "Review");
btnReviewHyphen.preferredSize = [48, 21];
var btnUncheckHyphen = rowHyphen.add("button", undefined, "Uncheck");
btnUncheckHyphen.preferredSize = [56, 21];

// Row 1.4: Ligatures Used & Fix
var rowLigature = col1.add("group");
rowLigature.orientation = "row";
rowLigature.alignChildren = ["left", "center"];
rowLigature.spacing = 4;
var lblLigatureTitle = rowLigature.add("statictext", undefined, "Ligatures:");
lblLigatureTitle.preferredSize.width = 82;
var lblLigatureCount = rowLigature.add("statictext", undefined, "0");
lblLigatureCount.preferredSize.width = 24;
var btnScanLigature = rowLigature.add("button", undefined, "Scan");
btnScanLigature.preferredSize = [38, 21];
var btnReviewLigature = rowLigature.add("button", undefined, "Rev");
btnReviewLigature.preferredSize = [34, 21];
var btnFixLigature = rowLigature.add("button", undefined, "Off");
btnFixLigature.preferredSize = [32, 21];
var btnAllLigature = rowLigature.add("button", undefined, "All");
btnAllLigature.preferredSize = [32, 21];

// =============================================================
// COLUMN 2: Colors, Assets & Links
// =============================================================
var col2 = mainCols.add("panel", undefined, "Colors, Frames & Links");
col2.orientation = "column";
col2.alignChildren = ["fill", "top"];
col2.preferredSize.width = 315;
col2.margins = 8;
col2.spacing = 7;

// Row 2.1: Unused Swatches
var rowSwatches = col2.add("group");
rowSwatches.orientation = "row";
rowSwatches.alignChildren = ["left", "center"];
rowSwatches.spacing = 4;
var lblSwatchesTitle = rowSwatches.add("statictext", undefined, "Swatches:");
lblSwatchesTitle.preferredSize.width = 82;
var lblUnusedCount = rowSwatches.add("statictext", undefined, "0");
lblUnusedCount.preferredSize.width = 24;
var btnScanSwatches = rowSwatches.add("button", undefined, "Scan");
btnScanSwatches.preferredSize = [38, 21];
var btnRemoveSwatches = rowSwatches.add("button", undefined, "Remove All");
btnRemoveSwatches.preferredSize = [80, 21];

// Row 2.2: Black Color Issues (Strict 100% Solid & 80% Tint Only; No Rich Black)
var rowBlack = col2.add("group");
rowBlack.orientation = "row";
rowBlack.alignChildren = ["left", "center"];
rowBlack.spacing = 4;
var lblBlackTitle = rowBlack.add("statictext", undefined, "Black Issues:");
lblBlackTitle.preferredSize.width = 82;
var lblBlackColorCount = rowBlack.add("statictext", undefined, "0");
lblBlackColorCount.preferredSize.width = 24;
var btnScanBlack = rowBlack.add("button", undefined, "Scan");
btnScanBlack.preferredSize = [38, 21];
var btnReviewBlack = rowBlack.add("button", undefined, "Rev");
btnReviewBlack.preferredSize = [34, 21];
var btnFixCurrentBlack = rowBlack.add("button", undefined, "Fix");
btnFixCurrentBlack.preferredSize = [32, 21];
var btnFixAllBlack = rowBlack.add("button", undefined, "All");
btnFixAllBlack.preferredSize = [32, 21];

// Row 2.3: Image Frames with Fill (Review | Clear)
var rowImgFill = col2.add("group");
rowImgFill.orientation = "row";
rowImgFill.alignChildren = ["left", "center"];
rowImgFill.spacing = 4;
var lblImgFillTitle = rowImgFill.add("statictext", undefined, "Img Fill Color:");
lblImgFillTitle.preferredSize.width = 82;
var lblImageFrameCount = rowImgFill.add("statictext", undefined, "0");
lblImageFrameCount.preferredSize.width = 24;
var btnScanImgFill = rowImgFill.add("button", undefined, "Scan");
btnScanImgFill.preferredSize = [38, 21];
var btnReviewImageFrame = rowImgFill.add("button", undefined, "Review");
btnReviewImageFrame.preferredSize = [48, 21];
var btnClearImgFill = rowImgFill.add("button", undefined, "Clear");
btnClearImgFill.preferredSize = [40, 21];

// Row 2.4: Disallowed URLs (.in / com/in)
var rowUrls = col2.add("group");
rowUrls.orientation = "row";
rowUrls.alignChildren = ["left", "center"];
rowUrls.spacing = 4;
var lblUrlTitle = rowUrls.add("statictext", undefined, "URLs (.in):");
lblUrlTitle.preferredSize.width = 82;
var lblUrlCount = rowUrls.add("statictext", undefined, "0");
lblUrlCount.preferredSize.width = 24;
var btnScanUrls = rowUrls.add("button", undefined, "Scan");
btnScanUrls.preferredSize = [38, 21];
var btnReviewUrl = rowUrls.add("button", undefined, "Review");
btnReviewUrl.preferredSize = [55, 21];

// --- Panel: QC Report Log ---
var pnlReport = win.add("panel", undefined, "QC Report");
pnlReport.alignChildren = ["fill", "fill"];
pnlReport.margins = 6;

var reportBox = pnlReport.add("edittext", undefined, "QC Master Dashboard ready.\nClick 'Run Full QC' or individual [Scan] buttons to begin.\n", {
multiline: true,
readonly: true,
scrollable: true
});
reportBox.preferredSize = [620, 85];

// --- Footer: Credits & Buttons ---
var footerGroup = win.add("group");
footerGroup.orientation = "row";
footerGroup.alignChildren = ["fill", "center"];
footerGroup.alignment = ["fill", "bottom"];

var lblCredits = footerGroup.add("statictext", undefined, "Developed by Nousheeth Iqbal");
lblCredits.preferredSize.width = 380;

var btnClearLog = footerGroup.add("button", undefined, "Clear Log");
btnClearLog.preferredSize = [75, 23];
var btnClose = footerGroup.add("button", undefined, "Close");
btnClose.preferredSize = [65, 23];

// -------------------------------------------------------------
// Event Listeners
// -------------------------------------------------------------
btnRunQC.onClick = function () { runFullQC(); };
btnReset.onClick = function () { pristineReset(); };
btnRefresh.onClick = function () { refreshQC(); };
btnExport.onClick = function () { exportReport(); };

// Individual Scan Listeners
btnScanDouble.onClick = function () { scanDoubleSpaces(); };
btnScanWidow.onClick = function () { scanWidows(); };
btnScanHyphen.onClick = function () { scanHyphenation(); };
btnScanLigature.onClick = function () { scanLigatures(); };
btnScanSwatches.onClick = function () { scanUnusedSwatches(); };
btnScanBlack.onClick = function () { scanBlackColorIssues(); };
btnScanImgFill.onClick = function () { scanImageFramesWithFill(); };
btnScanUrls.onClick = function () { scanDisallowedUrls(); };

// Action Listeners
btnReviewDoubleSpace.onClick = function () { reviewDoubleSpace(); };
btnFixCurrentDoubleSpace.onClick = function () { fixCurrentDoubleSpace(); };
btnFixDoubleSpaces.onClick = function () { fixAllDoubleSpaces(); };

btnReviewWidow.onClick = function () { reviewWidow(); };
btnFixCurrentWidow.onClick = function () { fixCurrentWidow(); };
btnFixAllWidow.onClick = function () { fixAllWidows(); };

btnReviewHyphen.onClick = function () { reviewHyphenation(); };
btnUncheckHyphen.onClick = function () { uncheckAllHyphenation(); };

btnReviewLigature.onClick = function () { reviewLigature(); };
btnFixLigature.onClick = function () { fixCurrentLigature(); };
btnAllLigature.onClick = function () { fixAllLigatures(); };

btnRemoveSwatches.onClick = function () { removeAllUnusedSwatches(); };
btnReviewBlack.onClick = function () { reviewBlackColorIssue(); };
btnFixCurrentBlack.onClick = function () { fixCurrentBlackIssue(); };
btnFixAllBlack.onClick = function () { fixAllBlackIssues(); };

btnReviewImageFrame.onClick = function () { reviewImageFrame(); };
btnClearImgFill.onClick = function () { clearCurrentImgFill(); };

btnReviewUrl.onClick = function () { reviewDisallowedUrl(); };

btnClearLog.onClick = function () { reportBox.text = ""; };
btnClose.onClick = function () { win.close(); };

// Show modeless palette
win.center();
win.show();
})();

 

Translate
NousheethAuthor
Known Participant
Oct 23, 2026 5:33 PM UTC

Please check the 

#target indesign
#targetengine "qcmaster_engine"

/**
* QC Master Dashboard v1.9
* Print Production Preflight & Audit Script for Adobe InDesign
* Compatible with InDesign CC 2022 through CC 2025+
* Developed by Nousheeth Iqbal
*/

(function () {
// -------------------------------------------------------------
// Global Storage for Issue Navigation
// -------------------------------------------------------------
var state = {
doc: null,
doubleSpaces: [],
doubleSpaceIndex: 0,
widows: [],
widowIndex: 0,
imageFramesWithFill: [],
imageFrameIndex: 0,
blackIssues: [],
blackIssueIndex: 0,
disallowedUrls: [],
disallowedUrlIndex: 0,
hyphenations: [],
hyphenationIndex: 0,
ligatures: [],
ligatureIndex: 0,
unusedSwatches: [],
unusedSwatchesCount: 0
};

function getActiveDocument() {
if (app.documents.length === 0) {
return null;
}
return app.activeDocument;
}

// -------------------------------------------------------------
// Helper: Logging (Newest / Most Recent Entry at the Top)
// -------------------------------------------------------------
function appendLog(txt) {
var time = new Date();
var timeStr = "[" + pad(time.getHours()) + ":" + pad(time.getMinutes()) + ":" + pad(time.getSeconds()) + "] ";
var newEntry = timeStr + txt + "\n";
var currentText = reportBox.text;

if (currentText.length > 25000) {
currentText = currentText.substring(0, 20000);
}

reportBox.text = newEntry + currentText;
}

function pad(n) {
return (n < 10 ? "0" : "") + n;
}

function getItemPage(target) {
try {
if (target.hasOwnProperty && target.hasOwnProperty("parentPage") && target.parentPage) {
return target.parentPage;
}
if (target.parentPage) {
return target.parentPage;
}
if (target.parentTextFrames && target.parentTextFrames.length > 0) {
return target.parentTextFrames[0].parentPage;
}
var cur = target.parent;
while (cur) {
if (cur.parentPage) {
return cur.parentPage;
}
if (cur.constructor.name === "Page") {
return cur;
}
if (cur.constructor.name === "Spread" || cur.constructor.name === "Document") {
break;
}
cur = cur.parent;
}
} catch (e) {}
return null;
}

// -------------------------------------------------------------
// Helper: Automatically Add Unnamed Colors to Swatches Panel
// -------------------------------------------------------------
function addUnnamedColors(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return 0;

try {
if (app.documents.length > 0 && app.activeDocument !== doc) {
app.activeDocument = doc;
}
} catch (eDoc) {}

var beforeCount = doc.swatches.length;
try {
var act = null;
try {
act = app.menuActions.itemByName("$ID/Add Unnamed Colors");
} catch (e1) {}

if (!act || !act.isValid) {
try {
act = app.menuActions.itemByName("Add Unnamed Colors");
} catch (e2) {}
}

if (act && act.isValid && act.enabled) {
act.invoke();
}
} catch (e) {}

var afterCount = doc.swatches.length;
var added = afterCount - beforeCount;
if (added > 0) {
appendLog("Added " + added + " unnamed color(s) to Swatches panel.");
}
return added;
}

// -------------------------------------------------------------
// QC Check 1: Unused Swatches (Auto-adds unnamed colors first)
// -------------------------------------------------------------
function scanUnusedSwatches(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return 0;

// Auto-add any unnamed colors applied to text/objects into the Swatches panel
addUnnamedColors(doc);

state.unusedSwatches = [];
var swatchNames = [];

try {
var rawUnused = doc.unusedSwatches;
var rawUnusedMap = {};
for (var u = 0; u < rawUnused.length; u++) {
try {
rawUnusedMap[rawUnused[u].name] = true;
} catch (eU) {}
}

var panelSwatches = doc.swatches;
for (var i = 0; i < panelSwatches.length; i++) {
var sw = panelSwatches[i];
var swName = sw.name;

// Protect default system swatches
if (swName === "None" || swName === "Registration" || swName === "Black" || swName === "Paper" ||
swName === "[None]" || swName === "[Registration]" || swName === "[Black]" || swName === "[Paper]") {
continue;
}

if (rawUnusedMap[swName]) {
state.unusedSwatches.push(sw);
swatchNames.push(swName);
}
}
} catch (e) {
appendLog("Error scanning unused swatches: " + e.message);
}

state.unusedSwatchesCount = state.unusedSwatches.length;
lblUnusedCount.text = "" + state.unusedSwatchesCount;

if (state.unusedSwatchesCount > 0) {
appendLog("Unused Swatches in Panel (" + state.unusedSwatchesCount + "): " + swatchNames.join(", "));
} else {
appendLog("Swatches: 0 unused swatches in Swatches panel.");
}

return state.unusedSwatchesCount;
}

function removeAllUnusedSwatches(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return;

var count = 0;
try {
for (var i = state.unusedSwatches.length - 1; i >= 0; i--) {
var sw = state.unusedSwatches[i];
try {
if (sw && sw.isValid) {
sw.remove();
count++;
}
} catch (err) {}
}
appendLog("Removed " + count + " unused swatches from Swatches panel.");
scanUnusedSwatches(doc);
} catch (e) {
appendLog("Error removing swatches: " + e.message);
}
}

// -------------------------------------------------------------
// QC Check 2: Double Spaces (Scan | Review | Fix | All)
// -------------------------------------------------------------
function scanDoubleSpaces(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return 0;

state.doubleSpaces = [];
state.doubleSpaceIndex = 0;

app.findGrepPreferences = NothingEnum.nothing;
app.changeGrepPreferences = NothingEnum.nothing;
app.findGrepPreferences.findWhat = "\\x{0020}{2,}";

try {
var matches = doc.findGrep();
for (var i = 0; i < matches.length; i++) {
state.doubleSpaces.push(matches[i]);
}
} catch (e) {
appendLog("Error scanning double spaces: " + e.message);
} finally {
app.findGrepPreferences = NothingEnum.nothing;
app.changeGrepPreferences = NothingEnum.nothing;
}

lblDoubleSpaceCount.text = "" + state.doubleSpaces.length;
appendLog("Double Spaces: " + state.doubleSpaces.length + " found.");
return state.doubleSpaces.length;
}

function reviewDoubleSpace() {
if (state.doubleSpaces.length === 0) {
appendLog("No double spaces to review.");
return;
}
if (state.doubleSpaceIndex >= state.doubleSpaces.length) {
state.doubleSpaceIndex = 0;
}

var match = state.doubleSpaces[state.doubleSpaceIndex];
try {
if (match.isValid) {
app.select(match, SelectionOptions.REPLACE_WITH);
zoomToSelectedTarget(match);
var page = getItemPage(match);
appendLog("Reviewing Double Space (" + (state.doubleSpaceIndex + 1) + " of " + state.doubleSpaces.length + ") on " + (page ? "Page " + page.name : "Pasteboard"));
state.doubleSpaceIndex++;
} else {
state.doubleSpaces.splice(state.doubleSpaceIndex, 1);
lblDoubleSpaceCount.text = "" + state.doubleSpaces.length;
reviewDoubleSpace();
}
} catch (e) {
appendLog("Error navigating to double space: " + e.message);
}
}

function fixCurrentDoubleSpace() {
if (state.doubleSpaces.length === 0) {
appendLog("No double space found to fix.");
return;
}

var targetIdx = state.doubleSpaceIndex > 0 ? (state.doubleSpaceIndex - 1) : 0;
if (targetIdx >= state.doubleSpaces.length) {
targetIdx = 0;
}

var match = state.doubleSpaces[targetIdx];
try {
if (match.isValid) {
var page = getItemPage(match);
match.contents = " ";
state.doubleSpaces.splice(targetIdx, 1);
lblDoubleSpaceCount.text = "" + state.doubleSpaces.length;
appendLog("Fixed 1 double space on " + (page ? "Page " + page.name : "Pasteboard") + ". Remaining: " + state.doubleSpaces.length);

if (state.doubleSpaces.length > 0) {
if (targetIdx >= state.doubleSpaces.length) {
state.doubleSpaceIndex = 0;
} else {
state.doubleSpaceIndex = targetIdx;
}
reviewDoubleSpace();
} else {
state.doubleSpaceIndex = 0;
appendLog("All double spaces resolved!");
}
} else {
state.doubleSpaces.splice(targetIdx, 1);
lblDoubleSpaceCount.text = "" + state.doubleSpaces.length;
fixCurrentDoubleSpace();
}
} catch (e) {
appendLog("Error fixing single double space: " + e.message);
}
}

function fixAllDoubleSpaces(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return;

app.findGrepPreferences = NothingEnum.nothing;
app.changeGrepPreferences = NothingEnum.nothing;
app.findGrepPreferences.findWhat = "\\x{0020}{2,}";
app.changeGrepPreferences.changeTo = " ";

try {
var changed = doc.changeGrep();
appendLog("Replaced " + changed.length + " double space occurrences with single space.");
scanDoubleSpaces(doc);
} catch (e) {
appendLog("Error fixing double spaces: " + e.message);
} finally {
app.findGrepPreferences = NothingEnum.nothing;
app.changeGrepPreferences = NothingEnum.nothing;
}
}

// -------------------------------------------------------------
// QC Check 3: Widow Check (Excludes 1-word/bullet paragraphs)
// -------------------------------------------------------------
function scanWidows(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return 0;

state.widows = [];
state.widowIndex = 0;

try {
var stories = doc.stories;
for (var s = 0; s < stories.length; s++) {
var paras = stories[s].paragraphs;
for (var p = 0; p < paras.length; p++) {
var para = paras[p];

if (para.lines.length >= 2 && para.words.length >= 6) {
if (para.bulletsAndNumberingListType !== ListType.NO_LIST && para.words.length < 8) {
continue;
}

var lastLine = para.lines.lastItem();
var lastLineClean = lastLine.contents.replace(/[\r\n\s\t]+$/, "");
var wordsInLastLine = lastLine.words;

var prevLine = para.lines.item(para.lines.length - 2);
if (prevLine.words.length >= 3) {
if (wordsInLastLine.length === 1 || (lastLineClean.length > 0 && lastLineClean.length <= 8)) {
state.widows.push(para);
}
}
}
}
}
} catch (e) {
appendLog("Error scanning widows: " + e.message);
}

lblWidowCount.text = "" + state.widows.length;
appendLog("Widows: " + state.widows.length + " found.");
return state.widows.length;
}

function reviewWidow() {
if (state.widows.length === 0) {
appendLog("No widows to review.");
return;
}
if (state.widowIndex >= state.widows.length) {
state.widowIndex = 0;
}

var para = state.widows[state.widowIndex];
try {
if (para.isValid) {
var lastLine = para.lines.lastItem();
app.select(lastLine, SelectionOptions.REPLACE_WITH);
zoomToSelectedTarget(lastLine);
var page = getItemPage(para);
appendLog("Reviewing Widow (" + (state.widowIndex + 1) + " of " + state.widows.length + ") on " + (page ? "Page " + page.name : "Pasteboard") + ": \"" + lastLine.contents.replace(/[\r\n]+$/, "") + "\"");
state.widowIndex++;
} else {
state.widowIndex++;
reviewWidow();
}
} catch (e) {
appendLog("Error selecting widow: " + e.message);
}
}

function fixWidows(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return;
var fixedCount = 0;

for (var i = 0; i < state.widows.length; i++) {
var para = state.widows[i];
try {
if (para.isValid && para.words.length >= 6) {
var words = para.words;
var lastWord = words.lastItem();
var wordBeforeLast = words.item(words.length - 2);

var textRange = para.characters.itemByRange(
wordBeforeLast.characters.lastItem().index + 1,
lastWord.characters.firstItem().index - 1
);

if (textRange.isValid && textRange.contents.indexOf(" ") !== -1) {
textRange.contents = SpecialCharacters.NONBREAKING_SPACE;
fixedCount++;
}
}
} catch (e) {}
}

appendLog("Inserted non-breaking space on " + fixedCount + " paragraphs to prevent widows.");
scanWidows(doc);
}

// -------------------------------------------------------------
// QC Check 4: Image Frames with Fill Color (Safe DOM check)
// -------------------------------------------------------------
function scanImageFramesWithFill(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return 0;

state.imageFramesWithFill = [];
state.imageFrameIndex = 0;

try {
var noneSwatch = doc.swatches.itemByName("None");
var seen = {};

var graphics = doc.allGraphics;
for (var i = 0; i < graphics.length; i++) {
var g = graphics[i];
var frame = g.parent;
if (frame && frame.isValid) {
var frameId = "" + frame.id;
if (!seen[frameId]) {
try {
var fc = frame.fillColor;
if (fc && fc.isValid) {
var fcName = fc.name;
if (fcName !== "None" && fcName !== "[None]" && fcName !== "" && fc !== noneSwatch) {
var fTint = -1;
try { fTint = frame.fillTint; } catch (errT) {}
if (fTint === -1 || fTint > 0) {
seen[frameId] = true;
state.imageFramesWithFill.push({
frame: frame,
fillName: fcName,
tint: fTint
});
}
}
}
} catch (errF) {}
}
}
}

var allItems = doc.allPageItems;
for (var j = 0; j < allItems.length; j++) {
var item = allItems[j];
try {
var hasG = false;
try {
if (item.allGraphics && item.allGraphics.length > 0) hasG = true;
else if (item.graphics && item.graphics.length > 0) hasG = true;
} catch (eG) {}

if (hasG) {
var itemId = "" + item.id;
if (!seen[itemId]) {
var ifc = item.fillColor;
if (ifc && ifc.isValid) {
var ifcName = ifc.name;
if (ifcName !== "None" && ifcName !== "[None]" && ifcName !== "" && ifc !== noneSwatch) {
var itint = -1;
try { itint = item.fillTint; } catch (eT) {}
if (itint === -1 || itint > 0) {
seen[itemId] = true;
state.imageFramesWithFill.push({
frame: item,
fillName: ifcName,
tint: itint
});
}
}
}
}
}
} catch (eItem) {}
}
} catch (e) {
appendLog("Error checking image frames: " + e.message);
}

lblImageFrameCount.text = "" + state.imageFramesWithFill.length;
appendLog("Image Frames with Fill Color: " + state.imageFramesWithFill.length + " found.");
return state.imageFramesWithFill.length;
}

function reviewImageFrame() {
if (state.imageFramesWithFill.length === 0) {
appendLog("No image frames with fill color found.");
return;
}
if (state.imageFrameIndex >= state.imageFramesWithFill.length) {
state.imageFrameIndex = 0;
}

var item = state.imageFramesWithFill[state.imageFrameIndex];
var frame = item.frame;
try {
if (frame && frame.isValid) {
app.select(frame, SelectionOptions.REPLACE_WITH);
zoomToSelectedTarget(frame);
var page = getItemPage(frame);
appendLog("Reviewing Image Frame (" + (state.imageFrameIndex + 1) + " of " + state.imageFramesWithFill.length + ") on " + (page ? "Page " + page.name : "Pasteboard") + " [Fill: " + item.fillName + "]");
state.imageFrameIndex++;
} else {
state.imageFrameIndex++;
reviewImageFrame();
}
} catch (e) {
appendLog("Error selecting frame: " + e.message);
}
}

function clearCurrentImgFill() {
if (state.imageFramesWithFill.length === 0) {
appendLog("No image frame fill to clear.");
return;
}

var targetIdx = state.imageFrameIndex > 0 ? (state.imageFrameIndex - 1) : 0;
if (targetIdx >= state.imageFramesWithFill.length) targetIdx = 0;

var item = state.imageFramesWithFill[targetIdx];
var frame = item.frame;
try {
if (frame && frame.isValid) {
var doc = getActiveDocument();
frame.fillColor = doc.swatches.itemByName("None");
var page = getItemPage(frame);
state.imageFramesWithFill.splice(targetIdx, 1);
lblImageFrameCount.text = "" + state.imageFramesWithFill.length;
appendLog("Cleared fill color on Image Frame (" + (page ? "Page " + page.name : "Pasteboard") + "). Remaining: " + state.imageFramesWithFill.length);

if (state.imageFramesWithFill.length > 0) {
if (targetIdx >= state.imageFramesWithFill.length) state.imageFrameIndex = 0;
else state.imageFrameIndex = targetIdx;
reviewImageFrame();
} else {
state.imageFrameIndex = 0;
appendLog("All image frame fill colors cleared!");
}
} else {
state.imageFramesWithFill.splice(targetIdx, 1);
lblImageFrameCount.text = "" + state.imageFramesWithFill.length;
clearCurrentImgFill();
}
} catch (e) {
appendLog("Error clearing frame fill: " + e.message);
}
}

// -------------------------------------------------------------
// QC Check 5: Black Color Issues (Strict Tint 80% or 100% Solid Only; No Rich Black)
// -------------------------------------------------------------
function isRichBlack(swatch) {
if (!swatch || !swatch.isValid) return false;
var name = "";
try { name = (swatch.name || "").toLowerCase(); } catch (eN) {}

if (name === "[registration]" || name === "registration") {
return false;
}

if (name.indexOf("rich black") !== -1 || name.indexOf("4 color black") !== -1 ||
name.indexOf("4-color black") !== -1 || name.indexOf("4-colour black") !== -1) {
return true;
}

try {
if (swatch.space === ColorSpace.CMYK) {
var cv = swatch.colorValue;
if (cv && cv.length >= 4) {
var c = cv[0], m = cv[1], y = cv[2], k = cv[3];
// 4-color black: all 4 CMYK channels present with high K
if (c > 0 && m > 0 && y > 0 && k >= 40) {
return true;
}
// Or high total ink with dominant K and other process inks
if (k >= 60 && (c + m + y) >= 20) {
return true;
}
}
}
} catch (eCv) {}
return false;
}

function isBlackColor(swatch) {
if (!swatch || !swatch.isValid) return false;
var name = "";
try { name = swatch.name; } catch (eN) {}
if (name === "Black" || name === "[Black]") {
return true;
}
if (isRichBlack(swatch)) {
return true;
}
try {
if (swatch.space === ColorSpace.CMYK) {
var cv = swatch.colorValue;
if (cv && cv.length >= 4) {
// Pure K (1-color black)
if (cv[0] === 0 && cv[1] === 0 && cv[2] === 0 && cv[3] > 0) {
return true;
}
if (name.toLowerCase().indexOf("black") !== -1 && cv[3] > 0) {
return true;
}
}
}
} catch (e) {}
return false;
}

function getEffectiveTint(swatch, appliedTint) {
var baseK = 100;
try {
if (swatch.space === ColorSpace.CMYK) {
var cv = swatch.colorValue;
if (cv && cv[0] === 0 && cv[1] === 0 && cv[2] === 0) {
baseK = cv[3];
}
}
} catch (e) {}
if (appliedTint === -1) {
return Math.round(baseK);
}
return Math.round((baseK * appliedTint) / 100);
}

function isApprovedBlack(swatch, appliedTint) {
if (!swatch || !swatch.isValid) return false;

// Rich Black (4-color black) is strictly NOT allowed!
if (isRichBlack(swatch)) {
return false;
}

var effK = getEffectiveTint(swatch, appliedTint);
// Only 100% Solid Black and 80% Black Tint are approved!
return (effK === 100 || effK === 80);
}

function scanBlackColorIssues(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return 0;

// Auto-add unnamed colors so unnamed tints of black get identified as swatches
addUnnamedColors(doc);

state.blackIssues = [];
state.blackIssueIndex = 0;

try {
// 1. Text scans
var stories = doc.stories;
for (var s = 0; s < stories.length; s++) {
var story = stories[s];
var ranges = story.textStyleRanges;
for (var r = 0; r < ranges.length; r++) {
var range = ranges[r];
if (range.contents.replace(/[\r\n\s\t]+/g, "").length === 0) {
continue;
}

try {
var fc = range.fillColor;
if (fc && fc.isValid && isBlackColor(fc)) {
if (!isApprovedBlack(fc, range.fillTint)) {
var isRich = isRichBlack(fc);
var effK = isRich ? "4-Color" : getEffectiveTint(fc, range.fillTint);
var sampleText = range.contents;
if (sampleText.length > 22) {
sampleText = sampleText.substring(0, 19) + "...";
}
var descText = isRich ?
("Text Fill Rich Black (4-Color Black - Not Allowed) [\"" + sampleText.replace(/[\r\n]+/g, " ") + "\"]") :
("Text Fill Black " + effK + "% (Approved: 100%/80% only) [\"" + sampleText.replace(/[\r\n]+/g, " ") + "\"]");

state.blackIssues.push({
target: range,
type: "textFill",
effK: isRich ? 100 : effK,
isRich: isRich,
desc: descText
});
}
}
} catch (eF) {}

try {
if (range.strokeWeight > 0) {
var sc = range.strokeColor;
if (sc && sc.isValid && isBlackColor(sc)) {
if (!isApprovedBlack(sc, range.strokeTint)) {
var isRichS = isRichBlack(sc);
var effStrokeK = isRichS ? "4-Color" : getEffectiveTint(sc, range.strokeTint);
var descStroke = isRichS ?
"Text Stroke Rich Black (4-Color Black - Not Allowed)" :
("Text Stroke Black " + effStrokeK + "% (Approved: 100%/80% only)");

state.blackIssues.push({
target: range,
type: "textStroke",
effK: isRichS ? 100 : effStrokeK,
isRich: isRichS,
desc: descStroke
});
}
}
}
} catch (eS) {}
}
}

// 2. PageItems scans
var pageItems = doc.allPageItems;
for (var i = 0; i < pageItems.length; i++) {
var item = pageItems[i];

try {
var ifc = item.fillColor;
if (ifc && ifc.isValid && isBlackColor(ifc)) {
var ift = -1;
try { ift = item.fillTint; } catch (eT) {}
if (!isApprovedBlack(ifc, ift)) {
var isRichIF = isRichBlack(ifc);
var effItemK = isRichIF ? "4-Color" : getEffectiveTint(ifc, ift);
var descIF = isRichIF ?
(item.constructor.name + " Fill Rich Black (4-Color Black - Not Allowed)") :
(item.constructor.name + " Fill Black " + effItemK + "% (Approved: 100%/80% only)");

state.blackIssues.push({
target: item,
type: "itemFill",
effK: isRichIF ? 100 : effItemK,
isRich: isRichIF,
desc: descIF
});
}
}
} catch (eIF) {}

try {
var isw = 0;
try { isw = item.strokeWeight; } catch (eSW) {}
if (isw > 0) {
var isc = item.strokeColor;
if (isc && isc.isValid && isBlackColor(isc)) {
var ist = -1;
try { ist = item.strokeTint; } catch (eST) {}
if (!isApprovedBlack(isc, ist)) {
var isRichIS = isRichBlack(isc);
var effItemStrokeK = isRichIS ? "4-Color" : getEffectiveTint(isc, ist);
var descIS = isRichIS ?
(item.constructor.name + " Stroke Rich Black (4-Color Black - Not Allowed)") :
(item.constructor.name + " Stroke Black " + effItemStrokeK + "% (Approved: 100%/80% only)");

state.blackIssues.push({
target: item,
type: "itemStroke",
effK: isRichIS ? 100 : effItemStrokeK,
isRich: isRichIS,
desc: descIS
});
}
}
}
} catch (eIS) {}
}
} catch (e) {
appendLog("Error scanning black color issues: " + e.message);
}

lblBlackColorCount.text = "" + state.blackIssues.length;
appendLog("Black Color Issues: " + state.blackIssues.length + " found.");
return state.blackIssues.length;
}

function reviewBlackColorIssue() {
if (state.blackIssues.length === 0) {
appendLog("No black color issues to review.");
return;
}
if (state.blackIssueIndex >= state.blackIssues.length) {
state.blackIssueIndex = 0;
}

var issue = state.blackIssues[state.blackIssueIndex];
try {
if (issue.target.isValid) {
app.select(issue.target, SelectionOptions.REPLACE_WITH);
zoomToSelectedTarget(issue.target);
var page = getItemPage(issue.target);
var pageName = page ? "Page " + page.name : "Pasteboard";
appendLog("Reviewing Black Issue (" + (state.blackIssueIndex + 1) + " of " + state.blackIssues.length + ") on " + pageName + ": " + issue.desc);
state.blackIssueIndex++;
} else {
state.blackIssueIndex++;
reviewBlackColorIssue();
}
} catch (e) {
appendLog("Error navigating to black issue: " + e.message);
}
}

function fixCurrentBlackIssue() {
if (state.blackIssues.length === 0) {
appendLog("No black color issue found to fix.");
return;
}

var targetIdx = state.blackIssueIndex > 0 ? (state.blackIssueIndex - 1) : 0;
if (targetIdx >= state.blackIssues.length) {
targetIdx = 0;
}

var issue = state.blackIssues[targetIdx];
var doc = getActiveDocument();
if (!doc) return;

var blackSwatch = doc.swatches.itemByName("[Black]");
if (!blackSwatch || !blackSwatch.isValid) {
blackSwatch = doc.swatches.itemByName("Black");
}

try {
if (issue.target && issue.target.isValid) {
var targetTint = (!issue.isRich && issue.effK >= 70 && issue.effK < 90) ? 80 : 100;
if (issue.type === "textFill") {
issue.target.fillColor = blackSwatch;
issue.target.fillTint = targetTint;
} else if (issue.type === "textStroke") {
issue.target.strokeColor = blackSwatch;
issue.target.strokeTint = targetTint;
} else if (issue.type === "itemFill") {
issue.target.fillColor = blackSwatch;
try { issue.target.fillTint = targetTint; } catch (eT1) {}
} else if (issue.type === "itemStroke") {
issue.target.strokeColor = blackSwatch;
try { issue.target.strokeTint = targetTint; } catch (eT2) {}
}

var page = getItemPage(issue.target);
var pageName = page ? "Page " + page.name : "Pasteboard";
state.blackIssues.splice(targetIdx, 1);
lblBlackColorCount.text = "" + state.blackIssues.length;
appendLog("Fixed 1 Black issue to " + targetTint + "% on " + pageName + ". Remaining: " + state.blackIssues.length);

if (state.blackIssues.length > 0) {
if (targetIdx >= state.blackIssues.length) {
state.blackIssueIndex = 0;
} else {
state.blackIssueIndex = targetIdx;
}
reviewBlackColorIssue();
} else {
lblStatus.text = "Status: Black Issues Cleared";
}
} else {
state.blackIssues.splice(targetIdx, 1);
lblBlackColorCount.text = "" + state.blackIssues.length;
if (state.blackIssues.length > 0) reviewBlackColorIssue();
}
} catch (e) {
appendLog("Error fixing black issue: " + e.message);
}
}

function fixAllBlackIssues(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return;

if (state.blackIssues.length === 0) {
scanBlackColorIssues(doc);
}
if (state.blackIssues.length === 0) {
appendLog("No black color issues to fix.");
return;
}

var blackSwatch = doc.swatches.itemByName("[Black]");
if (!blackSwatch || !blackSwatch.isValid) {
blackSwatch = doc.swatches.itemByName("Black");
}

var fixedCount = 0;
var prevRedraw = app.scriptPreferences.enableRedraw;
try {
app.scriptPreferences.enableRedraw = false;
for (var i = 0; i < state.blackIssues.length; i++) {
var issue = state.blackIssues[i];
try {
if (issue.target && issue.target.isValid) {
var targetTint = (!issue.isRich && issue.effK >= 70 && issue.effK < 90) ? 80 : 100;
if (issue.type === "textFill") {
issue.target.fillColor = blackSwatch;
issue.target.fillTint = targetTint;
fixedCount++;
} else if (issue.type === "textStroke") {
issue.target.strokeColor = blackSwatch;
issue.target.strokeTint = targetTint;
fixedCount++;
} else if (issue.type === "itemFill") {
issue.target.fillColor = blackSwatch;
try { issue.target.fillTint = targetTint; } catch (eT1) {}
fixedCount++;
} else if (issue.type === "itemStroke") {
issue.target.strokeColor = blackSwatch;
try { issue.target.strokeTint = targetTint; } catch (eT2) {}
fixedCount++;
}
}
} catch (eOne) {}
}
appendLog("Fixed " + fixedCount + " black color issue(s) across document.");
scanBlackColorIssues(doc);
} catch (eAll) {
appendLog("Error fixing all black issues: " + eAll.message);
} finally {
app.scriptPreferences.enableRedraw = prevRedraw;
}
}

// -------------------------------------------------------------
// QC Check 6: Indian / Disallowed URLs (.in / com/in)
// -------------------------------------------------------------
function isDisallowedIndianUrl(url) {
if (!url) return false;
var cleanUrl = url.toLowerCase();
if (/(?:\.in|\.co\.in|\.org\.in|\.net\.in)(?:[\/\?#]|$)/i.test(cleanUrl)) {
return true;
}
if (/(?:com\/in|\/en-in|\/in)(?:[\/\?#]|$)/i.test(cleanUrl)) {
return true;
}
return false;
}

function scanDisallowedUrls(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return 0;

state.disallowedUrls = [];
state.disallowedUrlIndex = 0;

try {
var hyperlinks = doc.hyperlinks;
for (var h = 0; h < hyperlinks.length; h++) {
var hl = hyperlinks[h];
var dest = hl.destination;
if (dest && dest.hasOwnProperty("destinationURL")) {
var url = dest.destinationURL;
if (isDisallowedIndianUrl(url)) {
var sourceItem = null;
if (hl.source) {
if (hl.source.hasOwnProperty("sourceText")) {
sourceItem = hl.source.sourceText;
} else if (hl.source.hasOwnProperty("sourcePageItem")) {
sourceItem = hl.source.sourcePageItem;
}
}
state.disallowedUrls.push({
url: url,
target: sourceItem,
type: "Hyperlink Object"
});
}
}
}

app.findGrepPreferences = NothingEnum.nothing;
app.findGrepPreferences.findWhat = "(?i)\\b(?:https?:\\/\\/|www\\.)[^\\s<>'\"\\)\\]]+";

var textMatches = doc.findGrep();
for (var m = 0; m < textMatches.length; m++) {
var matchText = textMatches[m].contents;
if (isDisallowedIndianUrl(matchText)) {
state.disallowedUrls.push({
url: matchText,
target: textMatches[m],
type: "Text URL"
});
}
}
} catch (e) {
appendLog("Error scanning disallowed URLs: " + e.message);
} finally {
app.findGrepPreferences = NothingEnum.nothing;
}

lblUrlCount.text = "" + state.disallowedUrls.length;
appendLog("Disallowed URLs (.in): " + state.disallowedUrls.length + " found.");
return state.disallowedUrls.length;
}

function reviewDisallowedUrl() {
if (state.disallowedUrls.length === 0) {
appendLog("No disallowed URLs to review.");
return;
}
if (state.disallowedUrlIndex >= state.disallowedUrls.length) {
state.disallowedUrlIndex = 0;
}

var item = state.disallowedUrls[state.disallowedUrlIndex];
try {
if (item.target && item.target.isValid) {
app.select(item.target, SelectionOptions.REPLACE_WITH);
zoomToSelectedTarget(item.target);
var page = getItemPage(item.target);
var pageName = page ? "Page " + page.name : "Pasteboard";
appendLog("Reviewing URL (" + (state.disallowedUrlIndex + 1) + " of " + state.disallowedUrls.length + ") on " + pageName + " [" + item.type + "]: " + item.url);
state.disallowedUrlIndex++;
} else {
appendLog("URL (" + (state.disallowedUrlIndex + 1) + " of " + state.disallowedUrls.length + "): " + item.url);
state.disallowedUrlIndex++;
if (state.disallowedUrlIndex < state.disallowedUrls.length) {
reviewDisallowedUrl();
}
}
} catch (e) {
appendLog("Error navigating to disallowed URL: " + e.message);
}
}

// -------------------------------------------------------------
// QC Check 7: Hyphenation Check & Uncheck All
// -------------------------------------------------------------
function scanHyphenation(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return 0;

state.hyphenations = [];
state.hyphenationIndex = 0;

try {
var stories = doc.stories;
for (var s = 0; s < stories.length; s++) {
var story = stories[s];
var paras = story.paragraphs;
for (var p = 0; p < paras.length; p++) {
var para = paras[p];
if (para.hyphenation === true) {
var words = para.words;
var hasBrokenWord = false;

for (var w = 0; w < words.length; w++) {
var word = words[w];
if (word.lines.length > 1) {
state.hyphenations.push({
target: word,
para: para,
desc: "Hyphenated Word: \"" + word.contents + "\""
});
hasBrokenWord = true;
}
}

if (!hasBrokenWord && para.lines.length > 1) {
state.hyphenations.push({
target: para,
para: para,
desc: "Hyphenate Enabled on Paragraph (" + para.lines.length + " lines)"
});
}
}
}
}
} catch (e) {
appendLog("Error scanning hyphenated text: " + e.message);
}

lblHyphenCount.text = "" + state.hyphenations.length;
appendLog("Hyphenation: " + state.hyphenations.length + " occurrences found.");
return state.hyphenations.length;
}

function reviewHyphenation() {
if (state.hyphenations.length === 0) {
appendLog("No hyphenation to review.");
return;
}
if (state.hyphenationIndex >= state.hyphenations.length) {
state.hyphenationIndex = 0;
}

var item = state.hyphenations[state.hyphenationIndex];
try {
if (item.target.isValid) {
app.select(item.target, SelectionOptions.REPLACE_WITH);
zoomToSelectedTarget(item.target);
var page = getItemPage(item.target);
appendLog("Reviewing Hyphenation (" + (state.hyphenationIndex + 1) + " of " + state.hyphenations.length + ") on " + (page ? "Page " + page.name : "Pasteboard") + ": " + item.desc);
state.hyphenationIndex++;
} else {
state.hyphenationIndex++;
reviewHyphenation();
}
} catch (e) {
appendLog("Error navigating to hyphenation: " + e.message);
}
}

function uncheckAllHyphenation(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return;

var count = 0;
var prevRedraw = app.scriptPreferences.enableRedraw;
try {
app.scriptPreferences.enableRedraw = false;
var stories = doc.stories;
for (var s = 0; s < stories.length; s++) {
var paras = stories[s].paragraphs;
for (var p = 0; p < paras.length; p++) {
if (paras[p].hyphenation === true) {
paras[p].hyphenation = false;
count++;
}
}
}
appendLog("Unchecked Hyphenate on " + count + " paragraphs across the document.");
scanHyphenation(doc);
} catch (e) {
appendLog("Error unchecking hyphenation: " + e.message);
} finally {
app.scriptPreferences.enableRedraw = prevRedraw;
}
}

// -------------------------------------------------------------
// QC Check 8: Ligatures Check & Turn Off Fix
// -------------------------------------------------------------
function scanLigatures(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return 0;

state.ligatures = [];
state.ligatureIndex = 0;

try {
var stories = doc.stories;
for (var s = 0; s < stories.length; s++) {
var ranges = stories[s].textStyleRanges;
for (var r = 0; r < ranges.length; r++) {
var range = ranges[r];
if (range.contents.replace(/[\r\n\s\t]+/g, "").length === 0) continue;

if (range.ligatures === true) {
if (/(fi|fl|ff|ffi|ffl|[fiflffffiffl])/i.test(range.contents)) {
state.ligatures.push(range);
}
}
}
}
} catch (e) {
appendLog("Error scanning ligatures: " + e.message);
}

lblLigatureCount.text = "" + state.ligatures.length;
appendLog("Ligatures Used: " + state.ligatures.length + " ranges found.");
return state.ligatures.length;
}

function reviewLigature() {
if (state.ligatures.length === 0) {
appendLog("No ligatures to review.");
return;
}
if (state.ligatureIndex >= state.ligatures.length) {
state.ligatureIndex = 0;
}

var range = state.ligatures[state.ligatureIndex];
try {
if (range.isValid) {
app.select(range, SelectionOptions.REPLACE_WITH);
zoomToSelectedTarget(range);
var page = getItemPage(range);
var sample = range.contents;
if (sample.length > 22) sample = sample.substring(0, 19) + "...";
appendLog("Reviewing Ligature (" + (state.ligatureIndex + 1) + " of " + state.ligatures.length + ") on " + (page ? "Page " + page.name : "Pasteboard") + ": \"" + sample.replace(/[\r\n]+/g, " ") + "\"");
state.ligatureIndex++;
} else {
state.ligatureIndex++;
reviewLigature();
}
} catch (e) {
appendLog("Error navigating to ligature: " + e.message);
}
}

function fixCurrentLigature() {
if (state.ligatures.length === 0) {
appendLog("No ligature found to fix.");
return;
}

var targetIdx = state.ligatureIndex > 0 ? (state.ligatureIndex - 1) : 0;
if (targetIdx >= state.ligatures.length) targetIdx = 0;

var range = state.ligatures[targetIdx];
try {
if (range.isValid) {
range.ligatures = false;
state.ligatures.splice(targetIdx, 1);
lblLigatureCount.text = "" + state.ligatures.length;
appendLog("Turned off ligatures on selected text. Remaining: " + state.ligatures.length);

if (state.ligatures.length > 0) {
if (targetIdx >= state.ligatures.length) state.ligatureIndex = 0;
else state.ligatureIndex = targetIdx;
reviewLigature();
} else {
state.ligatureIndex = 0;
appendLog("All ligatures turned off!");
}
} else {
state.ligatures.splice(targetIdx, 1);
lblLigatureCount.text = "" + state.ligatures.length;
fixCurrentLigature();
}
} catch (e) {
appendLog("Error fixing ligature: " + e.message);
}
}

function fixAllLigatures(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return;

var count = 0;
var prevRedraw = app.scriptPreferences.enableRedraw;
try {
app.scriptPreferences.enableRedraw = false;
var stories = doc.stories;
for (var s = 0; s < stories.length; s++) {
var ranges = stories[s].textStyleRanges;
for (var r = 0; r < ranges.length; r++) {
if (ranges[r].ligatures === true) {
ranges[r].ligatures = false;
count++;
}
}
}
appendLog("Turned off ligatures on " + count + " text ranges across document.");
scanLigatures(doc);
} catch (e) {
appendLog("Error turning off ligatures: " + e.message);
} finally {
app.scriptPreferences.enableRedraw = prevRedraw;
}
}

// -------------------------------------------------------------
// Zoom Helper
// -------------------------------------------------------------
function zoomToSelectedTarget(target) {
try {
var page = getItemPage(target);
if (page) {
app.activeWindow.activePage = page;
}
app.activeWindow.zoom(ZoomOptions.FIT_SPREAD);
} catch (e) {}
}

// -------------------------------------------------------------
// Full QC Runner & Pristine Reset
// -------------------------------------------------------------
function pristineReset() {
state.doubleSpaces = [];
state.doubleSpaceIndex = 0;
state.widows = [];
state.widowIndex = 0;
state.imageFramesWithFill = [];
state.imageFrameIndex = 0;
state.blackIssues = [];
state.blackIssueIndex = 0;
state.disallowedUrls = [];
state.disallowedUrlIndex = 0;
state.hyphenations = [];
state.hyphenationIndex = 0;
state.ligatures = [];
state.ligatureIndex = 0;
state.unusedSwatches = [];
state.unusedSwatchesCount = 0;

lblUnusedCount.text = "0";
lblDoubleSpaceCount.text = "0";
lblWidowCount.text = "0";
lblImageFrameCount.text = "0";
lblBlackColorCount.text = "0";
lblUrlCount.text = "0";
lblHyphenCount.text = "0";
lblLigatureCount.text = "0";

var doc = getActiveDocument();
lblDocName.text = "Document: " + (doc ? doc.name : "None");
lblStatus.text = "Status: Ready";

try {
app.select(NothingEnum.nothing);
} catch (e) {}

reportBox.text = "QC Master Dashboard ready.\nClick 'Run Full QC' or individual [Scan] buttons to begin.\n";
}

function runFullQC() {
var doc = getActiveDocument();
if (!doc) {
alert("Please open an InDesign document before running QC.", "No Document");
return;
}

lblDocName.text = "Document: " + doc.name;
lblStatus.text = "Status: Running...";

var prevRedraw = app.scriptPreferences.enableRedraw;
try {
// Speed optimization: suppress screen redraws during multi-page audits (50+ pages)
app.scriptPreferences.enableRedraw = false;

// Step 1: Detect and add any unnamed colors to the Swatches panel
addUnnamedColors(doc);

var cSwatches = scanUnusedSwatches(doc);
var cDouble = scanDoubleSpaces(doc);
var cWidows = scanWidows(doc);
var cImgFill = scanImageFramesWithFill(doc);
var cBlack = scanBlackColorIssues(doc);
var cUrls = scanDisallowedUrls(doc);
var cHyphens = scanHyphenation(doc);
var cLigatures = scanLigatures(doc);

var total = cSwatches + cDouble + cWidows + cImgFill + cBlack + cUrls + cHyphens + cLigatures;
lblStatus.text = "Status: Complete (" + total + " issues)";

// Post summary header at the top
appendLog("=========================================");
appendLog("Full QC Complete for: " + doc.name + " (" + total + " total issues)");
appendLog("=========================================");
} catch (eQC) {
lblStatus.text = "Status: Error";
appendLog("Error during Full QC: " + eQC.message);
} finally {
app.scriptPreferences.enableRedraw = prevRedraw;
}
}

function refreshQC() {
var doc = getActiveDocument();
if (!doc) {
lblDocName.text = "Document: None";
lblStatus.text = "Status: No document open";
return;
}
lblDocName.text = "Document: " + doc.name;
lblStatus.text = "Status: Ready";
runFullQC();
}

function exportReport() {
var reportContent = reportBox.text;
if (!reportContent || reportContent.length === 0) {
alert("The report log is empty. Run QC before exporting.", "Empty Log");
return;
}

var doc = getActiveDocument();
var defaultPath = "~/Desktop";
if (doc && doc.saved) {
defaultPath = doc.filePath;
}

var fileName = (doc ? doc.name.replace(/\.indd$/i, "") : "Document") + "_QC_Report.txt";
var file = new File(defaultPath + "/" + fileName).saveDlg("Save QC Audit Report", "Text File:*.txt");
if (file) {
file.encoding = "UTF-8";
file.open("w");
file.write(reportContent);
file.close();
appendLog("Report successfully saved to: " + file.fsName);
}
}

// -------------------------------------------------------------
// UI Construction (Compact 2-Column Layout)
// -------------------------------------------------------------
var win = new Window("palette", "QC Master Dashboard v1.8", undefined, { resizeable: true });
win.orientation = "column";
win.alignChildren = ["fill", "top"];
win.margins = 10;
win.spacing = 6;

// --- Compact Header: Document Information & Actions ---
var pnlHeader = win.add("panel", undefined, "Document & Actions");
pnlHeader.orientation = "column";
pnlHeader.alignChildren = ["fill", "top"];
pnlHeader.margins = 8;
pnlHeader.spacing = 6;

var headerRow1 = pnlHeader.add("group");
headerRow1.orientation = "row";
headerRow1.alignChildren = ["left", "center"];
headerRow1.spacing = 16;

var initialDoc = getActiveDocument();
var docNameText = initialDoc ? initialDoc.name : "No active document";
var lblDocName = headerRow1.add("statictext", undefined, "Document: " + docNameText);
lblDocName.preferredSize.width = 340;
var lblStatus = headerRow1.add("statictext", undefined, "Status: Ready");
lblStatus.preferredSize.width = 250;

var headerRow2 = pnlHeader.add("group");
headerRow2.orientation = "row";
headerRow2.alignChildren = ["left", "center"];
headerRow2.spacing = 8;

var btnRunQC = headerRow2.add("button", undefined, "Run Full QC");
btnRunQC.preferredSize = [105, 24];

var btnReset = headerRow2.add("button", undefined, "Reset");
btnReset.preferredSize = [60, 24];

var btnRefresh = headerRow2.add("button", undefined, "Refresh");
btnRefresh.preferredSize = [70, 24];

var btnExport = headerRow2.add("button", undefined, "Export Report");
btnExport.preferredSize = [100, 24];

// --- Main 2-Column Container ---
var mainCols = win.add("group");
mainCols.orientation = "row";
mainCols.alignChildren = ["fill", "top"];
mainCols.spacing = 8;

// =============================================================
// COLUMN 1: Text & Typography
// =============================================================
var col1 = mainCols.add("panel", undefined, "Typography & Text");
col1.orientation = "column";
col1.alignChildren = ["fill", "top"];
col1.preferredSize.width = 315;
col1.margins = 8;
col1.spacing = 7;

// Row 1.1: Double Spaces
var rowDouble = col1.add("group");
rowDouble.orientation = "row";
rowDouble.alignChildren = ["left", "center"];
rowDouble.spacing = 4;
var lblDoubleTitle = rowDouble.add("statictext", undefined, "Double Space:");
lblDoubleTitle.preferredSize.width = 82;
var lblDoubleSpaceCount = rowDouble.add("statictext", undefined, "0");
lblDoubleSpaceCount.preferredSize.width = 24;
var btnScanDouble = rowDouble.add("button", undefined, "Scan");
btnScanDouble.preferredSize = [38, 21];
var btnReviewDoubleSpace = rowDouble.add("button", undefined, "Rev");
btnReviewDoubleSpace.preferredSize = [34, 21];
var btnFixCurrentDoubleSpace = rowDouble.add("button", undefined, "Fix");
btnFixCurrentDoubleSpace.preferredSize = [32, 21];
var btnFixDoubleSpaces = rowDouble.add("button", undefined, "All");
btnFixDoubleSpaces.preferredSize = [32, 21];

// Row 1.2: Widow Check
var rowWidow = col1.add("group");
rowWidow.orientation = "row";
rowWidow.alignChildren = ["left", "center"];
rowWidow.spacing = 4;
var lblWidowTitle = rowWidow.add("statictext", undefined, "Widows:");
lblWidowTitle.preferredSize.width = 82;
var lblWidowCount = rowWidow.add("statictext", undefined, "0");
lblWidowCount.preferredSize.width = 24;
var btnScanWidow = rowWidow.add("button", undefined, "Scan");
btnScanWidow.preferredSize = [38, 21];
var btnReviewWidow = rowWidow.add("button", undefined, "Review");
btnReviewWidow.preferredSize = [48, 21];
var btnFixWidow = rowWidow.add("button", undefined, "Fix");
btnFixWidow.preferredSize = [36, 21];

// Row 1.3: Hyphenated Lines & Uncheck Option
var rowHyphen = col1.add("group");
rowHyphen.orientation = "row";
rowHyphen.alignChildren = ["left", "center"];
rowHyphen.spacing = 4;
var lblHyphenTitle = rowHyphen.add("statictext", undefined, "Hyphenation:");
lblHyphenTitle.preferredSize.width = 82;
var lblHyphenCount = rowHyphen.add("statictext", undefined, "0");
lblHyphenCount.preferredSize.width = 24;
var btnScanHyphen = rowHyphen.add("button", undefined, "Scan");
btnScanHyphen.preferredSize = [38, 21];
var btnReviewHyphen = rowHyphen.add("button", undefined, "Review");
btnReviewHyphen.preferredSize = [48, 21];
var btnUncheckHyphen = rowHyphen.add("button", undefined, "Uncheck");
btnUncheckHyphen.preferredSize = [56, 21];

// Row 1.4: Ligatures Used & Fix
var rowLigature = col1.add("group");
rowLigature.orientation = "row";
rowLigature.alignChildren = ["left", "center"];
rowLigature.spacing = 4;
var lblLigatureTitle = rowLigature.add("statictext", undefined, "Ligatures:");
lblLigatureTitle.preferredSize.width = 82;
var lblLigatureCount = rowLigature.add("statictext", undefined, "0");
lblLigatureCount.preferredSize.width = 24;
var btnScanLigature = rowLigature.add("button", undefined, "Scan");
btnScanLigature.preferredSize = [38, 21];
var btnReviewLigature = rowLigature.add("button", undefined, "Rev");
btnReviewLigature.preferredSize = [34, 21];
var btnFixLigature = rowLigature.add("button", undefined, "Off");
btnFixLigature.preferredSize = [32, 21];
var btnAllLigature = rowLigature.add("button", undefined, "All");
btnAllLigature.preferredSize = [32, 21];

// =============================================================
// COLUMN 2: Colors, Assets & Links
// =============================================================
var col2 = mainCols.add("panel", undefined, "Colors, Frames & Links");
col2.orientation = "column";
col2.alignChildren = ["fill", "top"];
col2.preferredSize.width = 315;
col2.margins = 8;
col2.spacing = 7;

// Row 2.1: Unused Swatches
var rowSwatches = col2.add("group");
rowSwatches.orientation = "row";
rowSwatches.alignChildren = ["left", "center"];
rowSwatches.spacing = 4;
var lblSwatchesTitle = rowSwatches.add("statictext", undefined, "Swatches:");
lblSwatchesTitle.preferredSize.width = 82;
var lblUnusedCount = rowSwatches.add("statictext", undefined, "0");
lblUnusedCount.preferredSize.width = 24;
var btnScanSwatches = rowSwatches.add("button", undefined, "Scan");
btnScanSwatches.preferredSize = [38, 21];
var btnRemoveSwatches = rowSwatches.add("button", undefined, "Remove All");
btnRemoveSwatches.preferredSize = [80, 21];

// Row 2.2: Black Color Issues (Strict 100% Solid & 80% Tint Only; No Rich Black)
var rowBlack = col2.add("group");
rowBlack.orientation = "row";
rowBlack.alignChildren = ["left", "center"];
rowBlack.spacing = 4;
var lblBlackTitle = rowBlack.add("statictext", undefined, "Black Issues:");
lblBlackTitle.preferredSize.width = 82;
var lblBlackColorCount = rowBlack.add("statictext", undefined, "0");
lblBlackColorCount.preferredSize.width = 24;
var btnScanBlack = rowBlack.add("button", undefined, "Scan");
btnScanBlack.preferredSize = [38, 21];
var btnReviewBlack = rowBlack.add("button", undefined, "Rev");
btnReviewBlack.preferredSize = [34, 21];
var btnFixCurrentBlack = rowBlack.add("button", undefined, "Fix");
btnFixCurrentBlack.preferredSize = [32, 21];
var btnFixAllBlack = rowBlack.add("button", undefined, "All");
btnFixAllBlack.preferredSize = [32, 21];

// Row 2.3: Image Frames with Fill (Review | Clear)
var rowImgFill = col2.add("group");
rowImgFill.orientation = "row";
rowImgFill.alignChildren = ["left", "center"];
rowImgFill.spacing = 4;
var lblImgFillTitle = rowImgFill.add("statictext", undefined, "Img Fill Color:");
lblImgFillTitle.preferredSize.width = 82;
var lblImageFrameCount = rowImgFill.add("statictext", undefined, "0");
lblImageFrameCount.preferredSize.width = 24;
var btnScanImgFill = rowImgFill.add("button", undefined, "Scan");
btnScanImgFill.preferredSize = [38, 21];
var btnReviewImageFrame = rowImgFill.add("button", undefined, "Review");
btnReviewImageFrame.preferredSize = [48, 21];
var btnClearImgFill = rowImgFill.add("button", undefined, "Clear");
btnClearImgFill.preferredSize = [40, 21];

// Row 2.4: Disallowed URLs (.in / com/in)
var rowUrls = col2.add("group");
rowUrls.orientation = "row";
rowUrls.alignChildren = ["left", "center"];
rowUrls.spacing = 4;
var lblUrlTitle = rowUrls.add("statictext", undefined, "URLs (.in):");
lblUrlTitle.preferredSize.width = 82;
var lblUrlCount = rowUrls.add("statictext", undefined, "0");
lblUrlCount.preferredSize.width = 24;
var btnScanUrls = rowUrls.add("button", undefined, "Scan");
btnScanUrls.preferredSize = [38, 21];
var btnReviewUrl = rowUrls.add("button", undefined, "Review");
btnReviewUrl.preferredSize = [55, 21];

// --- Panel: QC Report Log ---
var pnlReport = win.add("panel", undefined, "QC Report");
pnlReport.alignChildren = ["fill", "fill"];
pnlReport.margins = 6;

var reportBox = pnlReport.add("edittext", undefined, "QC Master Dashboard ready.\nClick 'Run Full QC' or individual [Scan] buttons to begin.\n", {
multiline: true,
readonly: true,
scrollable: true
});
reportBox.preferredSize = [620, 85];

// --- Footer: Credits & Buttons ---
var footerGroup = win.add("group");
footerGroup.orientation = "row";
footerGroup.alignChildren = ["fill", "center"];
footerGroup.alignment = ["fill", "bottom"];

var lblCredits = footerGroup.add("statictext", undefined, "Developed by Nousheeth Iqbal");
lblCredits.preferredSize.width = 380;

var btnClearLog = footerGroup.add("button", undefined, "Clear Log");
btnClearLog.preferredSize = [75, 23];
var btnClose = footerGroup.add("button", undefined, "Close");
btnClose.preferredSize = [65, 23];

// -------------------------------------------------------------
// Event Listeners
// -------------------------------------------------------------
btnRunQC.onClick = function () { runFullQC(); };
btnReset.onClick = function () { pristineReset(); };
btnRefresh.onClick = function () { refreshQC(); };
btnExport.onClick = function () { exportReport(); };

// Individual Scan Listeners
btnScanDouble.onClick = function () { scanDoubleSpaces(); };
btnScanWidow.onClick = function () { scanWidows(); };
btnScanHyphen.onClick = function () { scanHyphenation(); };
btnScanLigature.onClick = function () { scanLigatures(); };
btnScanSwatches.onClick = function () { scanUnusedSwatches(); };
btnScanBlack.onClick = function () { scanBlackColorIssues(); };
btnScanImgFill.onClick = function () { scanImageFramesWithFill(); };
btnScanUrls.onClick = function () { scanDisallowedUrls(); };

// Action Listeners
btnReviewDoubleSpace.onClick = function () { reviewDoubleSpace(); };
btnFixCurrentDoubleSpace.onClick = function () { fixCurrentDoubleSpace(); };
btnFixDoubleSpaces.onClick = function () { fixAllDoubleSpaces(); };

btnReviewWidow.onClick = function () { reviewWidow(); };
btnFixWidow.onClick = function () { fixWidows(); };

btnReviewHyphen.onClick = function () { reviewHyphenation(); };
btnUncheckHyphen.onClick = function () { uncheckAllHyphenation(); };

btnReviewLigature.onClick = function () { reviewLigature(); };
btnFixLigature.onClick = function () { fixCurrentLigature(); };
btnAllLigature.onClick = function () { fixAllLigatures(); };

btnRemoveSwatches.onClick = function () { removeAllUnusedSwatches(); };
btnReviewBlack.onClick = function () { reviewBlackColorIssue(); };
btnFixCurrentBlack.onClick = function () { fixCurrentBlackIssue(); };
btnFixAllBlack.onClick = function () { fixAllBlackIssues(); };

btnReviewImageFrame.onClick = function () { reviewImageFrame(); };
btnClearImgFill.onClick = function () { clearCurrentImgFill(); };

btnReviewUrl.onClick = function () { reviewDisallowedUrl(); };

btnClearLog.onClick = function () { reportBox.text = ""; };
btnClose.onClick = function () { win.close(); };

// Show modeless palette
win.center();
win.show();
})();

 

Translate
NousheethAuthor
Known Participant
Oct 30, 2026 1:26 PM UTC
#target indesign
#targetengine "qcmaster_engine"

/**
* QC Master Dashboard v2.4
* Print Production Preflight & Audit Script for Adobe InDesign
* Compatible with InDesign CC 2022 through CC 2025+
* Developed by Nousheeth Iqbal
*/

(function () {
// -------------------------------------------------------------
// Global Storage for Issue Navigation
// -------------------------------------------------------------
var state = {
doc: null,
doubleSpaces: [],
doubleSpaceIndex: 0,
widows: [],
widowIndex: 0,
imageFramesWithFill: [],
imageFrameIndex: 0,
blackIssues: [],
blackIssueIndex: 0,
disallowedUrls: [],
disallowedUrlIndex: 0,
hyphenations: [],
hyphenationIndex: 0,
ligatures: [],
ligatureIndex: 0,
unusedSwatches: [],
unusedSwatchesCount: 0
};

function getActiveDocument() {
if (app.documents.length === 0) {
return null;
}
return app.activeDocument;
}

// -------------------------------------------------------------
// Helper: Logging (Newest / Most Recent Entry at the Top)
// -------------------------------------------------------------
function appendLog(txt) {
var time = new Date();
var timeStr = "[" + pad(time.getHours()) + ":" + pad(time.getMinutes()) + ":" + pad(time.getSeconds()) + "] ";
var newEntry = timeStr + txt + "\n";
var currentText = reportBox.text;

if (currentText.length > 25000) {
currentText = currentText.substring(0, 20000);
}

reportBox.text = newEntry + currentText;
}

function pad(n) {
return (n < 10 ? "0" : "") + n;
}

function getItemLocation(target) {
var page = null;
var spread = null;
if (!target) return { page: null, spread: null };

try {
// Case 1: Text target (Paragraph, Line, Word, Character, TextStyleRange, Match)
var tf = null;
try {
if (target.parentTextFrames && target.parentTextFrames.length > 0) {
tf = target.parentTextFrames[0];
}
} catch (eTf) {}

var cur = tf ? tf : target;

// Direct parentPage check
try {
if (cur.parentPage && cur.parentPage.isValid) {
page = cur.parentPage;
}
} catch (ePp) {}

// Ascend hierarchy to find Page and Spread
var node = cur;
while (node) {
try {
if (!page && node.constructor.name === "Page") {
page = node;
}
if (node.constructor.name === "Spread") {
spread = node;
break;
}
if (node.constructor.name === "Document") {
break;
}
} catch (eNode) {}
try {
node = node.parent;
} catch (ePar) {
break;
}
}

// Derive spread from page if needed
if (page && page.isValid && (!spread || !spread.isValid)) {
try {
if (page.parent && page.parent.constructor.name === "Spread") {
spread = page.parent;
}
} catch (eSp) {}
}
} catch (e) {}

return { page: page, spread: spread };
}

function getItemPage(target) {
return getItemLocation(target).page;
}

// -------------------------------------------------------------
// Helper: Automatically Add Unnamed Colors to Swatches Panel
// -------------------------------------------------------------
function addUnnamedColors(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return 0;

try {
if (app.documents.length > 0 && app.activeDocument !== doc) {
app.activeDocument = doc;
}
} catch (eDoc) {}

var beforeCount = doc.swatches.length;
try {
var act = null;
try {
act = app.menuActions.itemByName("$ID/Add Unnamed Colors");
} catch (e1) {}

if (!act || !act.isValid) {
try {
act = app.menuActions.itemByName("Add Unnamed Colors");
} catch (e2) {}
}

if (act && act.isValid && act.enabled) {
act.invoke();
}
} catch (e) {}

var afterCount = doc.swatches.length;
var added = afterCount - beforeCount;
if (added > 0) {
appendLog("Added " + added + " unnamed color(s) to Swatches panel.");
}
return added;
}

// -------------------------------------------------------------
// QC Check 1: Unused Swatches (Auto-adds unnamed colors first)
// -------------------------------------------------------------
function scanUnusedSwatches(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return 0;

// Auto-add any unnamed colors applied to text/objects into the Swatches panel
addUnnamedColors(doc);

state.unusedSwatches = [];
var swatchNames = [];

try {
var rawUnused = doc.unusedSwatches;
var rawUnusedMap = {};
for (var u = 0; u < rawUnused.length; u++) {
try {
rawUnusedMap[rawUnused[u].name] = true;
} catch (eU) {}
}

var panelSwatches = doc.swatches;
for (var i = 0; i < panelSwatches.length; i++) {
var sw = panelSwatches[i];
var swName = sw.name;

// Protect default system swatches
if (swName === "None" || swName === "Registration" || swName === "Black" || swName === "Paper" ||
swName === "[None]" || swName === "[Registration]" || swName === "[Black]" || swName === "[Paper]") {
continue;
}

if (rawUnusedMap[swName]) {
state.unusedSwatches.push(sw);
swatchNames.push(swName);
}
}
} catch (e) {
appendLog("Error scanning unused swatches: " + e.message);
}

state.unusedSwatchesCount = state.unusedSwatches.length;
lblUnusedCount.text = "" + state.unusedSwatchesCount;

if (state.unusedSwatchesCount > 0) {
appendLog("Unused Swatches in Panel (" + state.unusedSwatchesCount + "): " + swatchNames.join(", "));
} else {
appendLog("Swatches: 0 unused swatches in Swatches panel.");
}

return state.unusedSwatchesCount;
}

function removeAllUnusedSwatches(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return;

if (state.unusedSwatches.length === 0) {
scanUnusedSwatches(doc);
}
if (state.unusedSwatches.length === 0) {
appendLog("No unused swatches to remove.");
return;
}

if (!confirm("Are you sure you want to remove all " + state.unusedSwatches.length + " unused swatch(es) from the Swatches panel?", false, "Confirm Remove Unused Swatches")) {
return;
}

app.doScript(function () {
var count = 0;
try {
for (var i = state.unusedSwatches.length - 1; i >= 0; i--) {
var sw = state.unusedSwatches[i];
try {
if (sw && sw.isValid) {
sw.remove();
count++;
}
} catch (err) {}
}
appendLog("Removed " + count + " unused swatches from Swatches panel.");
scanUnusedSwatches(doc);
} catch (e) {
appendLog("Error removing swatches: " + e.message);
}
}, ScriptLanguage.JAVASCRIPT, undefined, UndoModes.ENTIRE_SCRIPT, "QC Remove Unused Swatches");
}

// -------------------------------------------------------------
// QC Check 2: Double Spaces (Scan | Review | Fix | All)
// -------------------------------------------------------------
function scanDoubleSpaces(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return 0;

state.doubleSpaces = [];
state.doubleSpaceIndex = 0;

app.findGrepPreferences = NothingEnum.nothing;
app.changeGrepPreferences = NothingEnum.nothing;
app.findGrepPreferences.findWhat = "\\x{0020}{2,}";

try {
var matches = doc.findGrep();
for (var i = 0; i < matches.length; i++) {
state.doubleSpaces.push(matches[i]);
}
} catch (e) {
appendLog("Error scanning double spaces: " + e.message);
} finally {
app.findGrepPreferences = NothingEnum.nothing;
app.changeGrepPreferences = NothingEnum.nothing;
}

lblDoubleSpaceCount.text = "" + state.doubleSpaces.length;
appendLog("Double Spaces: " + state.doubleSpaces.length + " found.");
return state.doubleSpaces.length;
}

function reviewDoubleSpace() {
if (state.doubleSpaces.length === 0) {
appendLog("No double spaces to review.");
return;
}
if (state.doubleSpaceIndex >= state.doubleSpaces.length) {
state.doubleSpaceIndex = 0;
}

var match = state.doubleSpaces[state.doubleSpaceIndex];
try {
if (match.isValid) {
app.select(match, SelectionOptions.REPLACE_WITH);
zoomToSelectedTarget(match);
var page = getItemPage(match);
appendLog("Reviewing Double Space (" + (state.doubleSpaceIndex + 1) + " of " + state.doubleSpaces.length + ") on " + (page ? "Page " + page.name : "Pasteboard"));
state.doubleSpaceIndex++;
} else {
state.doubleSpaces.splice(state.doubleSpaceIndex, 1);
lblDoubleSpaceCount.text = "" + state.doubleSpaces.length;
reviewDoubleSpace();
}
} catch (e) {
appendLog("Error navigating to double space: " + e.message);
}
}

function fixCurrentDoubleSpace() {
if (state.doubleSpaces.length === 0) {
appendLog("No double space found to fix.");
return;
}

var targetIdx = state.doubleSpaceIndex > 0 ? (state.doubleSpaceIndex - 1) : 0;
if (targetIdx >= state.doubleSpaces.length) {
targetIdx = 0;
}

var match = state.doubleSpaces[targetIdx];
try {
if (match.isValid) {
var page = getItemPage(match);
match.contents = " ";
state.doubleSpaces.splice(targetIdx, 1);
lblDoubleSpaceCount.text = "" + state.doubleSpaces.length;
appendLog("Fixed 1 double space on " + (page ? "Page " + page.name : "Pasteboard") + ". Remaining: " + state.doubleSpaces.length);

if (state.doubleSpaces.length > 0) {
if (targetIdx >= state.doubleSpaces.length) {
state.doubleSpaceIndex = 0;
} else {
state.doubleSpaceIndex = targetIdx;
}
reviewDoubleSpace();
} else {
state.doubleSpaceIndex = 0;
appendLog("All double spaces resolved!");
}
} else {
state.doubleSpaces.splice(targetIdx, 1);
lblDoubleSpaceCount.text = "" + state.doubleSpaces.length;
fixCurrentDoubleSpace();
}
} catch (e) {
appendLog("Error fixing single double space: " + e.message);
}
}

function fixAllDoubleSpaces(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return;

if (!confirm("Are you sure you want to replace all double spaces across the entire document?", false, "Confirm Fix All Double Spaces")) {
return;
}

app.doScript(function () {
app.findGrepPreferences = NothingEnum.nothing;
app.changeGrepPreferences = NothingEnum.nothing;
app.findGrepPreferences.findWhat = "\\x{0020}{2,}";
app.changeGrepPreferences.changeTo = " ";

try {
var changed = doc.changeGrep();
appendLog("Replaced " + changed.length + " double space occurrences with single space.");
scanDoubleSpaces(doc);
} catch (e) {
appendLog("Error fixing double spaces: " + e.message);
} finally {
app.findGrepPreferences = NothingEnum.nothing;
app.changeGrepPreferences = NothingEnum.nothing;
}
}, ScriptLanguage.JAVASCRIPT, undefined, UndoModes.ENTIRE_SCRIPT, "QC Fix All Double Spaces");
}

// -------------------------------------------------------------
// QC Check 3: Widow Check (Excludes 1-word/bullet paragraphs)
// -------------------------------------------------------------
function scanWidows(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return 0;

state.widows = [];
state.widowIndex = 0;

try {
var stories = doc.stories;
for (var s = 0; s < stories.length; s++) {
var story = stories[s];
var paras = story.paragraphs;
for (var p = 0; p < paras.length; p++) {
var para = paras[p];

// Multi-line paragraph check: must have at least 2 lines and at least 3 words total
// (Excludes 1-word bullets and 2-word icon descriptions like "Icon\nLabel")
if (para.lines.length >= 2 && para.words.length >= 3) {
// Skip if it is a bullet/numbered list item with < 4 words
if (para.bulletsAndNumberingListType !== ListType.NO_LIST && para.words.length < 4) {
continue;
}

var lastLine = para.lines.lastItem();
var lastLineClean = lastLine.contents.replace(/[\r\n\s\t]+$/, "");
var wordsInLastLine = lastLine.words;

// Check the line before the last line
var prevLine = para.lines.item(para.lines.length - 2);

// If the line was broken using a manual Forced Line Break (Shift + Enter / Soft Return),
// this is an intentional manual line break, NOT an automated typographic widow!
if (/[\u000A\n]/.test(prevLine.contents)) {
continue;
}

// A widow occurs if the last line has only 1 word, or is a very short stub (<= 8 chars)
if (wordsInLastLine.length === 1 || (lastLineClean.length > 0 && lastLineClean.length <= 8)) {
state.widows.push(para);
}
}
}
}
} catch (e) {
appendLog("Error scanning widows: " + e.message);
}

lblWidowCount.text = "" + state.widows.length;
appendLog("Widows: " + state.widows.length + " found.");
return state.widows.length;
}

function reviewWidow() {
if (state.widows.length === 0) {
appendLog("No widows to review.");
return;
}
if (state.widowIndex >= state.widows.length) {
state.widowIndex = 0;
}

var para = state.widows[state.widowIndex];
try {
if (para.isValid) {
var lastLine = para.lines.lastItem();
app.select(lastLine, SelectionOptions.REPLACE_WITH);
zoomToSelectedTarget(lastLine);
var page = getItemPage(para);
appendLog("Reviewing Widow (" + (state.widowIndex + 1) + " of " + state.widows.length + ") on " + (page ? "Page " + page.name : "Pasteboard") + ": \"" + lastLine.contents.replace(/[\r\n]+$/, "") + "\"");
state.widowIndex++;
} else {
state.widowIndex++;
reviewWidow();
}
} catch (e) {
appendLog("Error selecting widow: " + e.message);
}
}

function fixCurrentWidow() {
if (state.widows.length === 0) {
appendLog("No widow found to fix.");
return;
}

var targetIdx = state.widowIndex > 0 ? (state.widowIndex - 1) : 0;
if (targetIdx >= state.widows.length) {
targetIdx = 0;
}

var para = state.widows[targetIdx];
try {
if (para && para.isValid && para.words.length >= 2) {
var words = para.words;
var lastWord = words.lastItem();
var wordBeforeLast = words.item(words.length - 2);

var story = para.parentStory;
var lastCharOfPrev = wordBeforeLast.characters.lastItem();
var firstCharOfLast = lastWord.characters.firstItem();

var startIdx = lastCharOfPrev.index + 1;
var endIdx = firstCharOfLast.index - 1;

if (startIdx <= endIdx) {
var spaceRange = story.characters.itemByRange(startIdx, endIdx);
if (spaceRange.isValid) {
spaceRange.contents = SpecialCharacters.NONBREAKING_SPACE;
}
} else {
wordBeforeLast.insertionPoints.lastItem().contents = SpecialCharacters.NONBREAKING_SPACE;
}

var page = getItemPage(para);
state.widows.splice(targetIdx, 1);
lblWidowCount.text = "" + state.widows.length;
appendLog("Fixed 1 widow on " + (page ? "Page " + page.name : "Pasteboard") + ". Remaining: " + state.widows.length);

if (state.widows.length > 0) {
if (targetIdx >= state.widows.length) {
state.widowIndex = 0;
} else {
state.widowIndex = targetIdx;
}
reviewWidow();
} else {
lblStatus.text = "Status: Widows Cleared";
}
} else {
state.widows.splice(targetIdx, 1);
lblWidowCount.text = "" + state.widows.length;
if (state.widows.length > 0) reviewWidow();
}
} catch (e) {
appendLog("Error fixing widow: " + e.message);
}
}

function fixAllWidows(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return;

if (state.widows.length === 0) {
scanWidows(doc);
}
if (state.widows.length === 0) {
appendLog("No widows to fix.");
return;
}

if (!confirm("Are you sure you want to fix all " + state.widows.length + " detected widow(s) across the document?", false, "Confirm Fix All Widows")) {
return;
}

app.doScript(function () {
var fixedCount = 0;
var prevRedraw = app.scriptPreferences.enableRedraw;

try {
app.scriptPreferences.enableRedraw = false;
for (var i = 0; i < state.widows.length; i++) {
var para = state.widows[i];
try {
if (para && para.isValid && para.words.length >= 2) {
var words = para.words;
var lastWord = words.lastItem();
var wordBeforeLast = words.item(words.length - 2);

var story = para.parentStory;
var lastCharOfPrev = wordBeforeLast.characters.lastItem();
var firstCharOfLast = lastWord.characters.firstItem();

var startIdx = lastCharOfPrev.index + 1;
var endIdx = firstCharOfLast.index - 1;

if (startIdx <= endIdx) {
var spaceRange = story.characters.itemByRange(startIdx, endIdx);
if (spaceRange.isValid) {
spaceRange.contents = SpecialCharacters.NONBREAKING_SPACE;
fixedCount++;
}
} else {
wordBeforeLast.insertionPoints.lastItem().contents = SpecialCharacters.NONBREAKING_SPACE;
fixedCount++;
}
}
} catch (eOne) {}
}
} catch (eAll) {
appendLog("Error fixing widows: " + eAll.message);
} finally {
app.scriptPreferences.enableRedraw = prevRedraw;
}

appendLog("Inserted non-breaking space on " + fixedCount + " paragraph(s) to eliminate widows.");
scanWidows(doc);
}, ScriptLanguage.JAVASCRIPT, undefined, UndoModes.ENTIRE_SCRIPT, "QC Fix All Widows");
}

function fixWidows(doc) {
fixAllWidows(doc);
}

// -------------------------------------------------------------
// QC Check 4: Image Frames with Fill Color (Safe DOM check)
// -------------------------------------------------------------
function scanImageFramesWithFill(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return 0;

state.imageFramesWithFill = [];
state.imageFrameIndex = 0;

try {
var noneSwatch = doc.swatches.itemByName("None");
var seen = {};

var graphics = doc.allGraphics;
for (var i = 0; i < graphics.length; i++) {
var g = graphics[i];
var frame = g.parent;
if (frame && frame.isValid) {
var frameId = "" + frame.id;
if (!seen[frameId]) {
try {
var fc = frame.fillColor;
if (fc && fc.isValid) {
var fcName = fc.name;
if (fcName !== "None" && fcName !== "[None]" && fcName !== "" && fc !== noneSwatch) {
var fTint = -1;
try { fTint = frame.fillTint; } catch (errT) {}
if (fTint === -1 || fTint > 0) {
seen[frameId] = true;
state.imageFramesWithFill.push({
frame: frame,
fillName: fcName,
tint: fTint
});
}
}
}
} catch (errF) {}
}
}
}

var allItems = doc.allPageItems;
for (var j = 0; j < allItems.length; j++) {
var item = allItems[j];
try {
var hasG = false;
try {
if (item.allGraphics && item.allGraphics.length > 0) hasG = true;
else if (item.graphics && item.graphics.length > 0) hasG = true;
} catch (eG) {}

if (hasG) {
var itemId = "" + item.id;
if (!seen[itemId]) {
var ifc = item.fillColor;
if (ifc && ifc.isValid) {
var ifcName = ifc.name;
if (ifcName !== "None" && ifcName !== "[None]" && ifcName !== "" && ifc !== noneSwatch) {
var itint = -1;
try { itint = item.fillTint; } catch (eT) {}
if (itint === -1 || itint > 0) {
seen[itemId] = true;
state.imageFramesWithFill.push({
frame: item,
fillName: ifcName,
tint: itint
});
}
}
}
}
}
} catch (eItem) {}
}
} catch (e) {
appendLog("Error checking image frames: " + e.message);
}

lblImageFrameCount.text = "" + state.imageFramesWithFill.length;
appendLog("Image Frames with Fill Color: " + state.imageFramesWithFill.length + " found.");
return state.imageFramesWithFill.length;
}

function reviewImageFrame() {
if (state.imageFramesWithFill.length === 0) {
appendLog("No image frames with fill color found.");
return;
}
if (state.imageFrameIndex >= state.imageFramesWithFill.length) {
state.imageFrameIndex = 0;
}

var item = state.imageFramesWithFill[state.imageFrameIndex];
var frame = item.frame;
try {
if (frame && frame.isValid) {
app.select(frame, SelectionOptions.REPLACE_WITH);
zoomToSelectedTarget(frame);
var page = getItemPage(frame);
appendLog("Reviewing Image Frame (" + (state.imageFrameIndex + 1) + " of " + state.imageFramesWithFill.length + ") on " + (page ? "Page " + page.name : "Pasteboard") + " [Fill: " + item.fillName + "]");
state.imageFrameIndex++;
} else {
state.imageFrameIndex++;
reviewImageFrame();
}
} catch (e) {
appendLog("Error selecting frame: " + e.message);
}
}

function clearCurrentImgFill() {
if (state.imageFramesWithFill.length === 0) {
appendLog("No image frame fill to clear.");
return;
}

var targetIdx = state.imageFrameIndex > 0 ? (state.imageFrameIndex - 1) : 0;
if (targetIdx >= state.imageFramesWithFill.length) targetIdx = 0;

var item = state.imageFramesWithFill[targetIdx];
var frame = item.frame;
try {
if (frame && frame.isValid) {
var doc = getActiveDocument();
frame.fillColor = doc.swatches.itemByName("None");
var page = getItemPage(frame);
state.imageFramesWithFill.splice(targetIdx, 1);
lblImageFrameCount.text = "" + state.imageFramesWithFill.length;
appendLog("Cleared fill color on Image Frame (" + (page ? "Page " + page.name : "Pasteboard") + "). Remaining: " + state.imageFramesWithFill.length);

if (state.imageFramesWithFill.length > 0) {
if (targetIdx >= state.imageFramesWithFill.length) state.imageFrameIndex = 0;
else state.imageFrameIndex = targetIdx;
reviewImageFrame();
} else {
state.imageFrameIndex = 0;
appendLog("All image frame fill colors cleared!");
}
} else {
state.imageFramesWithFill.splice(targetIdx, 1);
lblImageFrameCount.text = "" + state.imageFramesWithFill.length;
clearCurrentImgFill();
}
} catch (e) {
appendLog("Error clearing frame fill: " + e.message);
}
}

// -------------------------------------------------------------
// QC Check 5: Black Color Issues (Strict Tint 80% or 100% Solid Only; No Rich Black)
// -------------------------------------------------------------
function getCmykString(swatch) {
try {
if (swatch && swatch.isValid && swatch.space === ColorSpace.CMYK) {
var cv = swatch.colorValue;
if (cv && cv.length >= 4) {
return "C=" + Math.round(cv[0]) + " M=" + Math.round(cv[1]) + " Y=" + Math.round(cv[2]) + " K=" + Math.round(cv[3]);
}
}
} catch (e) {}
return "";
}

function isRichBlack(swatch) {
if (!swatch || !swatch.isValid) return false;
var name = "";
try { name = (swatch.name || "").toLowerCase(); } catch (eN) {}

if (name === "[none]" || name === "none" ||
name === "[paper]" || name === "paper") {
return false;
}

// Explicitly named rich blacks / 4-color blacks / registration
if (name === "[registration]" || name === "registration" ||
name.indexOf("rich black") !== -1 || name.indexOf("4 color black") !== -1 ||
name.indexOf("4-color black") !== -1 || name.indexOf("4-colour black") !== -1 ||
name.indexOf("cool black") !== -1 || name.indexOf("warm black") !== -1 ||
name.indexOf("super black") !== -1) {
return true;
}

try {
if (swatch.space === ColorSpace.CMYK) {
var cv = swatch.colorValue;
if (cv && cv.length >= 4) {
var c = cv[0], m = cv[1], y = cv[2], k = cv[3];

// Pure 1-color black (C=0, M=0, Y=0) is handled separately
if (c === 0 && m === 0 && y === 0) {
return false;
}

// 4-Color Black:
// 1) Black channel is dominant (K >= 85%) and process inks are present (C+M+Y >= 15%)
// e.g. C=91 M=86 Y=49 K=100 (from company standard), C=60 M=40 Y=40 K=100, etc.
// 2) OR K >= 98% and any process ink (C+M+Y >= 5%) is present
if ((k >= 85 && (c + m + y) >= 15) || (k >= 98 && (c + m + y) >= 5)) {
return true;
}
}
}
} catch (eCv) {}
return false;
}

function isBlackColor(swatch) {
if (!swatch || !swatch.isValid) return false;
var name = "";
try { name = swatch.name; } catch (eN) {}
if (name === "Black" || name === "[Black]") {
return true;
}
if (isRichBlack(swatch)) {
return true;
}
try {
if (swatch.space === ColorSpace.CMYK) {
var cv = swatch.colorValue;
if (cv && cv.length >= 4) {
// Pure K (1-color black: C=0, M=0, Y=0, and K>0)
if (cv[0] === 0 && cv[1] === 0 && cv[2] === 0 && cv[3] > 0) {
return true;
}
}
}
} catch (e) {}
return false;
}

function getEffectiveTint(swatch, appliedTint) {
var baseK = 100;
try {
if (swatch.space === ColorSpace.CMYK) {
var cv = swatch.colorValue;
if (cv && cv[0] === 0 && cv[1] === 0 && cv[2] === 0) {
baseK = cv[3];
}
}
} catch (e) {}
if (appliedTint === -1) {
return Math.round(baseK);
}
return Math.round((baseK * appliedTint) / 100);
}

function isApprovedBlack(swatch, appliedTint) {
if (!swatch || !swatch.isValid) return false;

// Rich Black (4-color black) is strictly NOT allowed!
if (isRichBlack(swatch)) {
return false;
}

var effK = getEffectiveTint(swatch, appliedTint);
// As per standard: ONLY 100% Solid Black and 80% Black Tint are approved!
// 70%, 75%, 85%, 90% (or any other tint) are strictly NOT acceptable.
return (effK === 100 || effK === 80);
}

function scanBlackColorIssues(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return 0;

// Auto-add unnamed colors so unnamed tints of black get identified as swatches
addUnnamedColors(doc);

state.blackIssues = [];
state.blackIssueIndex = 0;

try {
// 1. Text scans
var stories = doc.stories;
for (var s = 0; s < stories.length; s++) {
var story = stories[s];
var ranges = story.textStyleRanges;
for (var r = 0; r < ranges.length; r++) {
var range = ranges[r];
if (range.contents.replace(/[\r\n\s\t]+/g, "").length === 0) {
continue;
}

try {
var fc = range.fillColor;
if (fc && fc.isValid && isBlackColor(fc)) {
if (!isApprovedBlack(fc, range.fillTint)) {
var isRich = isRichBlack(fc);
var cmykInfo = isRich ? getCmykString(fc) : "";
var effK = isRich ? "4-Color" : getEffectiveTint(fc, range.fillTint);
var sampleText = range.contents;
if (sampleText.length > 20) {
sampleText = sampleText.substring(0, 18) + "...";
}
var descText = isRich ?
("Text Fill 4-Color Black (" + (cmykInfo ? cmykInfo + " " : "") + "- Not Allowed) [\"" + sampleText.replace(/[\r\n]+/g, " ") + "\"]") :
("Text Fill Black " + effK + "% (Approved: 100% or 80% only) [\"" + sampleText.replace(/[\r\n]+/g, " ") + "\"]");

state.blackIssues.push({
target: range,
type: "textFill",
effK: isRich ? 100 : effK,
isRich: isRich,
desc: descText
});
}
}
} catch (eF) {}

try {
if (range.strokeWeight > 0) {
var sc = range.strokeColor;
if (sc && sc.isValid && isBlackColor(sc)) {
if (!isApprovedBlack(sc, range.strokeTint)) {
var isRichS = isRichBlack(sc);
var cmykInfoS = isRichS ? getCmykString(sc) : "";
var effStrokeK = isRichS ? "4-Color" : getEffectiveTint(sc, range.strokeTint);
var descStroke = isRichS ?
("Text Stroke 4-Color Black (" + (cmykInfoS ? cmykInfoS + " " : "") + "- Not Allowed)") :
("Text Stroke Black " + effStrokeK + "% (Approved: 100% or 80% only)");

state.blackIssues.push({
target: range,
type: "textStroke",
effK: isRichS ? 100 : effStrokeK,
isRich: isRichS,
desc: descStroke
});
}
}
}
} catch (eS) {}
}
}

// 2. PageItems scans
var pageItems = doc.allPageItems;
for (var i = 0; i < pageItems.length; i++) {
var item = pageItems[i];

try {
var ifc = item.fillColor;
if (ifc && ifc.isValid && isBlackColor(ifc)) {
var ift = -1;
try { ift = item.fillTint; } catch (eT) {}
if (!isApprovedBlack(ifc, ift)) {
var isRichIF = isRichBlack(ifc);
var cmykInfoIF = isRichIF ? getCmykString(ifc) : "";
var effItemK = isRichIF ? "4-Color" : getEffectiveTint(ifc, ift);
var descIF = isRichIF ?
(item.constructor.name + " Fill 4-Color Black (" + (cmykInfoIF ? cmykInfoIF + " " : "") + "- Not Allowed)") :
(item.constructor.name + " Fill Black " + effItemK + "% (Approved: 100% or 80% only)");

state.blackIssues.push({
target: item,
type: "itemFill",
effK: isRichIF ? 100 : effItemK,
isRich: isRichIF,
desc: descIF
});
}
}
} catch (eIF) {}

try {
var isw = 0;
try { isw = item.strokeWeight; } catch (eSW) {}
if (isw > 0) {
var isc = item.strokeColor;
if (isc && isc.isValid && isBlackColor(isc)) {
var ist = -1;
try { ist = item.strokeTint; } catch (eST) {}
if (!isApprovedBlack(isc, ist)) {
var isRichIS = isRichBlack(isc);
var cmykInfoIS = isRichIS ? getCmykString(isc) : "";
var effItemStrokeK = isRichIS ? "4-Color" : getEffectiveTint(isc, ist);
var descIS = isRichIS ?
(item.constructor.name + " Stroke 4-Color Black (" + (cmykInfoIS ? cmykInfoIS + " " : "") + "- Not Allowed)") :
(item.constructor.name + " Stroke Black " + effItemStrokeK + "% (Approved: 100% or 80% only)");

state.blackIssues.push({
target: item,
type: "itemStroke",
effK: isRichIS ? 100 : effItemStrokeK,
isRich: isRichIS,
desc: descIS
});
}
}
}
} catch (eIS) {}
}
} catch (e) {
appendLog("Error scanning black color issues: " + e.message);
}

lblBlackColorCount.text = "" + state.blackIssues.length;
appendLog("Black Color Issues: " + state.blackIssues.length + " found.");
return state.blackIssues.length;
}

function reviewBlackColorIssue() {
if (state.blackIssues.length === 0) {
appendLog("No black color issues to review.");
return;
}
if (state.blackIssueIndex >= state.blackIssues.length) {
state.blackIssueIndex = 0;
}

var issue = state.blackIssues[state.blackIssueIndex];
try {
if (issue.target.isValid) {
app.select(issue.target, SelectionOptions.REPLACE_WITH);
zoomToSelectedTarget(issue.target);
var page = getItemPage(issue.target);
var pageName = page ? "Page " + page.name : "Pasteboard";
appendLog("Reviewing Black Issue (" + (state.blackIssueIndex + 1) + " of " + state.blackIssues.length + ") on " + pageName + ": " + issue.desc);
state.blackIssueIndex++;
} else {
state.blackIssueIndex++;
reviewBlackColorIssue();
}
} catch (e) {
appendLog("Error navigating to black issue: " + e.message);
}
}

function fixCurrentBlackIssue(targetTint) {
if (typeof targetTint !== "number") {
targetTint = 100;
}
if (state.blackIssues.length === 0) {
appendLog("No black color issue found to fix.");
return;
}

var targetIdx = state.blackIssueIndex > 0 ? (state.blackIssueIndex - 1) : 0;
if (targetIdx >= state.blackIssues.length) {
targetIdx = 0;
}

var issue = state.blackIssues[targetIdx];
var doc = getActiveDocument();
if (!doc) return;

var blackSwatch = doc.swatches.itemByName("[Black]");
if (!blackSwatch || !blackSwatch.isValid) {
blackSwatch = doc.swatches.itemByName("Black");
}

try {
if (issue.target && issue.target.isValid) {
if (issue.type === "textFill") {
issue.target.fillColor = blackSwatch;
issue.target.fillTint = targetTint;
} else if (issue.type === "textStroke") {
issue.target.strokeColor = blackSwatch;
issue.target.strokeTint = targetTint;
} else if (issue.type === "itemFill") {
issue.target.fillColor = blackSwatch;
try { issue.target.fillTint = targetTint; } catch (eT1) {}
} else if (issue.type === "itemStroke") {
issue.target.strokeColor = blackSwatch;
try { issue.target.strokeTint = targetTint; } catch (eT2) {}
}

var page = getItemPage(issue.target);
var pageName = page ? "Page " + page.name : "Pasteboard";
state.blackIssues.splice(targetIdx, 1);
lblBlackColorCount.text = "" + state.blackIssues.length;
appendLog("Fixed 1 Black issue to " + targetTint + "% pure Black on " + pageName + ". Remaining: " + state.blackIssues.length);

if (state.blackIssues.length > 0) {
if (targetIdx >= state.blackIssues.length) {
state.blackIssueIndex = 0;
} else {
state.blackIssueIndex = targetIdx;
}
reviewBlackColorIssue();
} else {
lblStatus.text = "Status: Black Issues Cleared";
}
} else {
state.blackIssues.splice(targetIdx, 1);
lblBlackColorCount.text = "" + state.blackIssues.length;
if (state.blackIssues.length > 0) reviewBlackColorIssue();
}
} catch (e) {
appendLog("Error fixing black issue: " + e.message);
}
}

function promptFixAllBlackChoice(count) {
var dlg = new Window("dialog", "Fix All Black Issues");
dlg.orientation = "column";
dlg.alignChildren = ["fill", "top"];
dlg.margins = 16;
dlg.spacing = 10;

var titleTxt = dlg.add("statictext", undefined, "Found " + count + " unapproved black color issue(s).");
titleTxt.graphics.font = ScriptUI.newFont(titleTxt.graphics.font.name, "Bold", 12);

var infoTxt = dlg.add("statictext", undefined, "Standard requires pure Black at either 80% Tint or 100% Solid.\nChoose which approved tint to convert ALL issues to:", { multiline: true });
infoTxt.preferredSize.width = 340;

var btnGroup = dlg.add("group");
btnGroup.orientation = "row";
btnGroup.alignment = ["center", "center"];
btnGroup.spacing = 8;

var choice = null;

var btn80 = btnGroup.add("button", undefined, "All to 80% Black");
btn80.preferredSize = [125, 26];
btn80.onClick = function () {
choice = 80;
dlg.close();
};

var btn100 = btnGroup.add("button", undefined, "All to 100% Black");
btn100.preferredSize = [135, 26];
btn100.onClick = function () {
choice = 100;
dlg.close();
};

var btnCancel = btnGroup.add("button", undefined, "Cancel", { name: "cancel" });
btnCancel.preferredSize = [70, 26];
btnCancel.onClick = function () {
choice = null;
dlg.close();
};

dlg.show();
return choice;
}

function fixAllBlackIssues(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return;

if (state.blackIssues.length === 0) {
scanBlackColorIssues(doc);
}
if (state.blackIssues.length === 0) {
appendLog("No black color issues to fix.");
return;
}

var chosenTint = promptFixAllBlackChoice(state.blackIssues.length);
if (chosenTint === null) {
appendLog("Fix All Black cancelled.");
return;
}

app.doScript(function () {
var blackSwatch = doc.swatches.itemByName("[Black]");
if (!blackSwatch || !blackSwatch.isValid) {
blackSwatch = doc.swatches.itemByName("Black");
}

var fixedCount = 0;
var prevRedraw = app.scriptPreferences.enableRedraw;
try {
app.scriptPreferences.enableRedraw = false;
for (var i = 0; i < state.blackIssues.length; i++) {
var issue = state.blackIssues[i];
try {
if (issue.target && issue.target.isValid) {
if (issue.type === "textFill") {
issue.target.fillColor = blackSwatch;
issue.target.fillTint = chosenTint;
fixedCount++;
} else if (issue.type === "textStroke") {
issue.target.strokeColor = blackSwatch;
issue.target.strokeTint = chosenTint;
fixedCount++;
} else if (issue.type === "itemFill") {
issue.target.fillColor = blackSwatch;
try { issue.target.fillTint = chosenTint; } catch (eT1) {}
fixedCount++;
} else if (issue.type === "itemStroke") {
issue.target.strokeColor = blackSwatch;
try { issue.target.strokeTint = chosenTint; } catch (eT2) {}
fixedCount++;
}
}
} catch (eOne) {}
}
appendLog("Fixed " + fixedCount + " black issue(s) across document to " + chosenTint + "% pure Black.");
scanBlackColorIssues(doc);
} catch (eAll) {
appendLog("Error fixing all black issues: " + eAll.message);
} finally {
app.scriptPreferences.enableRedraw = prevRedraw;
}
}, ScriptLanguage.JAVASCRIPT, undefined, UndoModes.ENTIRE_SCRIPT, "QC Fix All Black Issues to " + chosenTint + "%");
}

// -------------------------------------------------------------
// QC Check 6: Indian / Disallowed URLs (.in / com/in)
// -------------------------------------------------------------
function isDisallowedIndianUrl(url) {
if (!url) return false;
var cleanUrl = url.toLowerCase();
if (/(?:\.in|\.co\.in|\.org\.in|\.net\.in)(?:[\/\?#]|$)/i.test(cleanUrl)) {
return true;
}
if (/(?:com\/in|\/en-in|\/in)(?:[\/\?#]|$)/i.test(cleanUrl)) {
return true;
}
return false;
}

function scanDisallowedUrls(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return 0;

state.disallowedUrls = [];
state.disallowedUrlIndex = 0;

try {
var hyperlinks = doc.hyperlinks;
for (var h = 0; h < hyperlinks.length; h++) {
var hl = hyperlinks[h];
var dest = hl.destination;
if (dest && dest.hasOwnProperty("destinationURL")) {
var url = dest.destinationURL;
if (isDisallowedIndianUrl(url)) {
var sourceItem = null;
if (hl.source) {
if (hl.source.hasOwnProperty("sourceText")) {
sourceItem = hl.source.sourceText;
} else if (hl.source.hasOwnProperty("sourcePageItem")) {
sourceItem = hl.source.sourcePageItem;
}
}
state.disallowedUrls.push({
url: url,
target: sourceItem,
type: "Hyperlink Object"
});
}
}
}

app.findGrepPreferences = NothingEnum.nothing;
app.findGrepPreferences.findWhat = "(?i)\\b(?:https?:\\/\\/|www\\.)[^\\s<>'\"\\)\\]]+";

var textMatches = doc.findGrep();
for (var m = 0; m < textMatches.length; m++) {
var matchText = textMatches[m].contents;
if (isDisallowedIndianUrl(matchText)) {
state.disallowedUrls.push({
url: matchText,
target: textMatches[m],
type: "Text URL"
});
}
}
} catch (e) {
appendLog("Error scanning disallowed URLs: " + e.message);
} finally {
app.findGrepPreferences = NothingEnum.nothing;
}

lblUrlCount.text = "" + state.disallowedUrls.length;
appendLog("Disallowed URLs (.in): " + state.disallowedUrls.length + " found.");
return state.disallowedUrls.length;
}

function reviewDisallowedUrl() {
if (state.disallowedUrls.length === 0) {
appendLog("No disallowed URLs to review.");
return;
}
if (state.disallowedUrlIndex >= state.disallowedUrls.length) {
state.disallowedUrlIndex = 0;
}

var item = state.disallowedUrls[state.disallowedUrlIndex];
try {
if (item.target && item.target.isValid) {
app.select(item.target, SelectionOptions.REPLACE_WITH);
zoomToSelectedTarget(item.target);
var page = getItemPage(item.target);
var pageName = page ? "Page " + page.name : "Pasteboard";
appendLog("Reviewing URL (" + (state.disallowedUrlIndex + 1) + " of " + state.disallowedUrls.length + ") on " + pageName + " [" + item.type + "]: " + item.url);
state.disallowedUrlIndex++;
} else {
appendLog("URL (" + (state.disallowedUrlIndex + 1) + " of " + state.disallowedUrls.length + "): " + item.url);
state.disallowedUrlIndex++;
if (state.disallowedUrlIndex < state.disallowedUrls.length) {
reviewDisallowedUrl();
}
}
} catch (e) {
appendLog("Error navigating to disallowed URL: " + e.message);
}
}

// -------------------------------------------------------------
// QC Check 7: Hyphenation Check & Uncheck All
// -------------------------------------------------------------
function scanHyphenation(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return 0;

state.hyphenations = [];
state.hyphenationIndex = 0;

try {
var stories = doc.stories;
for (var s = 0; s < stories.length; s++) {
var story = stories[s];
var paras = story.paragraphs;
for (var p = 0; p < paras.length; p++) {
var para = paras[p];
if (para.hyphenation === true) {
var words = para.words;
var hasBrokenWord = false;

for (var w = 0; w < words.length; w++) {
var word = words[w];
if (word.lines.length > 1) {
state.hyphenations.push({
target: word,
para: para,
desc: "Hyphenated Word: \"" + word.contents + "\""
});
hasBrokenWord = true;
}
}

if (!hasBrokenWord && para.lines.length > 1) {
state.hyphenations.push({
target: para,
para: para,
desc: "Hyphenate Enabled on Paragraph (" + para.lines.length + " lines)"
});
}
}
}
}
} catch (e) {
appendLog("Error scanning hyphenated text: " + e.message);
}

lblHyphenCount.text = "" + state.hyphenations.length;
appendLog("Hyphenation: " + state.hyphenations.length + " occurrences found.");
return state.hyphenations.length;
}

function reviewHyphenation() {
if (state.hyphenations.length === 0) {
appendLog("No hyphenation to review.");
return;
}
if (state.hyphenationIndex >= state.hyphenations.length) {
state.hyphenationIndex = 0;
}

var item = state.hyphenations[state.hyphenationIndex];
try {
if (item.target.isValid) {
app.select(item.target, SelectionOptions.REPLACE_WITH);
zoomToSelectedTarget(item.target);
var page = getItemPage(item.target);
appendLog("Reviewing Hyphenation (" + (state.hyphenationIndex + 1) + " of " + state.hyphenations.length + ") on " + (page ? "Page " + page.name : "Pasteboard") + ": " + item.desc);
state.hyphenationIndex++;
} else {
state.hyphenationIndex++;
reviewHyphenation();
}
} catch (e) {
appendLog("Error navigating to hyphenation: " + e.message);
}
}

function uncheckAllHyphenation(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return;

if (!confirm("Are you sure you want to uncheck hyphenation across all paragraphs in the document?", false, "Confirm Uncheck Hyphenation")) {
return;
}

app.doScript(function () {
var count = 0;
var prevRedraw = app.scriptPreferences.enableRedraw;
try {
app.scriptPreferences.enableRedraw = false;
var stories = doc.stories;
for (var s = 0; s < stories.length; s++) {
var paras = stories[s].paragraphs;
for (var p = 0; p < paras.length; p++) {
if (paras[p].hyphenation === true) {
paras[p].hyphenation = false;
count++;
}
}
}
appendLog("Unchecked Hyphenate on " + count + " paragraphs across the document.");
scanHyphenation(doc);
} catch (e) {
appendLog("Error unchecking hyphenation: " + e.message);
} finally {
app.scriptPreferences.enableRedraw = prevRedraw;
}
}, ScriptLanguage.JAVASCRIPT, undefined, UndoModes.ENTIRE_SCRIPT, "QC Uncheck Hyphenation");
}

// -------------------------------------------------------------
// QC Check 8: Ligatures Check & Turn Off Fix
// -------------------------------------------------------------
function scanLigatures(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return 0;

state.ligatures = [];
state.ligatureIndex = 0;

try {
var stories = doc.stories;
for (var s = 0; s < stories.length; s++) {
var ranges = stories[s].textStyleRanges;
for (var r = 0; r < ranges.length; r++) {
var range = ranges[r];
if (range.contents.replace(/[\r\n\s\t]+/g, "").length === 0) continue;

if (range.ligatures === true) {
if (/(fi|fl|ff|ffi|ffl|[fiflffffiffl])/i.test(range.contents)) {
state.ligatures.push(range);
}
}
}
}
} catch (e) {
appendLog("Error scanning ligatures: " + e.message);
}

lblLigatureCount.text = "" + state.ligatures.length;
appendLog("Ligatures Used: " + state.ligatures.length + " ranges found.");
return state.ligatures.length;
}

function reviewLigature() {
if (state.ligatures.length === 0) {
appendLog("No ligatures to review.");
return;
}
if (state.ligatureIndex >= state.ligatures.length) {
state.ligatureIndex = 0;
}

var range = state.ligatures[state.ligatureIndex];
try {
if (range.isValid) {
app.select(range, SelectionOptions.REPLACE_WITH);
zoomToSelectedTarget(range);
var page = getItemPage(range);
var sample = range.contents;
if (sample.length > 22) sample = sample.substring(0, 19) + "...";
appendLog("Reviewing Ligature (" + (state.ligatureIndex + 1) + " of " + state.ligatures.length + ") on " + (page ? "Page " + page.name : "Pasteboard") + ": \"" + sample.replace(/[\r\n]+/g, " ") + "\"");
state.ligatureIndex++;
} else {
state.ligatureIndex++;
reviewLigature();
}
} catch (e) {
appendLog("Error navigating to ligature: " + e.message);
}
}

function fixCurrentLigature() {
if (state.ligatures.length === 0) {
appendLog("No ligature found to fix.");
return;
}

var targetIdx = state.ligatureIndex > 0 ? (state.ligatureIndex - 1) : 0;
if (targetIdx >= state.ligatures.length) targetIdx = 0;

var range = state.ligatures[targetIdx];
try {
if (range.isValid) {
range.ligatures = false;
state.ligatures.splice(targetIdx, 1);
lblLigatureCount.text = "" + state.ligatures.length;
appendLog("Turned off ligatures on selected text. Remaining: " + state.ligatures.length);

if (state.ligatures.length > 0) {
if (targetIdx >= state.ligatures.length) state.ligatureIndex = 0;
else state.ligatureIndex = targetIdx;
reviewLigature();
} else {
state.ligatureIndex = 0;
appendLog("All ligatures turned off!");
}
} else {
state.ligatures.splice(targetIdx, 1);
lblLigatureCount.text = "" + state.ligatures.length;
fixCurrentLigature();
}
} catch (e) {
appendLog("Error fixing ligature: " + e.message);
}
}

function fixAllLigatures(doc) {
if (!doc) doc = getActiveDocument();
if (!doc) return;

if (!confirm("Are you sure you want to turn off ligatures across the entire document?", false, "Confirm Turn Off All Ligatures")) {
return;
}

app.doScript(function () {
var count = 0;
var prevRedraw = app.scriptPreferences.enableRedraw;
try {
app.scriptPreferences.enableRedraw = false;
var stories = doc.stories;
for (var s = 0; s < stories.length; s++) {
var ranges = stories[s].textStyleRanges;
for (var r = 0; r < ranges.length; r++) {
if (ranges[r].ligatures === true) {
ranges[r].ligatures = false;
count++;
}
}
}
appendLog("Turned off ligatures on " + count + " text ranges across document.");
scanLigatures(doc);
} catch (e) {
appendLog("Error turning off ligatures: " + e.message);
} finally {
app.scriptPreferences.enableRedraw = prevRedraw;
}
}, ScriptLanguage.JAVASCRIPT, undefined, UndoModes.ENTIRE_SCRIPT, "QC Turn Off Ligatures");
}

// -------------------------------------------------------------
// Zoom Helper (Maintains user's current zoom level like native Find/Change)
// -------------------------------------------------------------
function zoomToSelectedTarget(target) {
try {
var loc = getItemLocation(target);
var curZoom = 100;
try {
curZoom = app.activeWindow.zoomPercentage;
} catch (eZ) {}

// 1. Switch active spread if target is on a different spread
if (loc.spread && loc.spread.isValid) {
if (app.activeWindow.activeSpread !== loc.spread) {
app.activeWindow.activeSpread = loc.spread;
}
}

// 2. Set active page on that spread
if (loc.page && loc.page.isValid) {
app.activeWindow.activePage = loc.page;
}

// 3. Center viewport on the selected item while keeping current zoom level
try {
var act = null;
try { act = app.menuActions.itemByName("$ID/Fit Selection in Window"); } catch (e1) {}
if (!act || !act.isValid) {
try { act = app.menuActions.itemByName("Fit Selection in Window"); } catch (e2) {}
}
if (act && act.isValid && act.enabled) {
act.invoke();
}
} catch (eFit) {}

// 4. Restore the user's exact zoom level (e.g. 150%, 200%, etc.)
// so it never forces "fit page" or "fit frame", behaving exactly like native Find/Change!
if (curZoom && curZoom > 0) {
app.activeWindow.zoomPercentage = curZoom;
}
} catch (e) {}
}

// -------------------------------------------------------------
// Full QC Runner & Pristine Reset
// -------------------------------------------------------------
function pristineReset() {
state.doubleSpaces = [];
state.doubleSpaceIndex = 0;
state.widows = [];
state.widowIndex = 0;
state.imageFramesWithFill = [];
state.imageFrameIndex = 0;
state.blackIssues = [];
state.blackIssueIndex = 0;
state.disallowedUrls = [];
state.disallowedUrlIndex = 0;
state.hyphenations = [];
state.hyphenationIndex = 0;
state.ligatures = [];
state.ligatureIndex = 0;
state.unusedSwatches = [];
state.unusedSwatchesCount = 0;

lblUnusedCount.text = "0";
lblDoubleSpaceCount.text = "0";
lblWidowCount.text = "0";
lblImageFrameCount.text = "0";
lblBlackColorCount.text = "0";
lblUrlCount.text = "0";
lblHyphenCount.text = "0";
lblLigatureCount.text = "0";

var doc = getActiveDocument();
lblDocName.text = "Document: " + (doc ? doc.name : "None");
lblStatus.text = "Status: Ready";

try {
app.select(NothingEnum.nothing);
} catch (e) {}

reportBox.text = "QC Master Dashboard ready.\nClick 'Run Full QC' or individual [Scan] buttons to begin.\n";
}

function runFullQC() {
var doc = getActiveDocument();
if (!doc) {
alert("Please open an InDesign document before running QC.", "No Document");
return;
}

lblDocName.text = "Document: " + doc.name;
lblStatus.text = "Status: Running...";

var prevRedraw = app.scriptPreferences.enableRedraw;
try {
// Speed optimization: suppress screen redraws during multi-page audits (50+ pages)
app.scriptPreferences.enableRedraw = false;

// Step 1: Detect and add any unnamed colors to the Swatches panel
addUnnamedColors(doc);

var cSwatches = scanUnusedSwatches(doc);
var cDouble = scanDoubleSpaces(doc);
var cWidows = scanWidows(doc);
var cImgFill = scanImageFramesWithFill(doc);
var cBlack = scanBlackColorIssues(doc);
var cUrls = scanDisallowedUrls(doc);
var cHyphens = scanHyphenation(doc);
var cLigatures = scanLigatures(doc);

var total = cSwatches + cDouble + cWidows + cImgFill + cBlack + cUrls + cHyphens + cLigatures;
lblStatus.text = "Status: Complete (" + total + " issues)";

// Post summary header at the top
appendLog("=========================================");
appendLog("Full QC Complete for: " + doc.name + " (" + total + " total issues)");
appendLog("=========================================");
} catch (eQC) {
lblStatus.text = "Status: Error";
appendLog("Error during Full QC: " + eQC.message);
} finally {
app.scriptPreferences.enableRedraw = prevRedraw;
}
}

function refreshQC() {
var doc = getActiveDocument();
if (!doc) {
lblDocName.text = "Document: None";
lblStatus.text = "Status: No document open";
return;
}
lblDocName.text = "Document: " + doc.name;
lblStatus.text = "Status: Ready";
runFullQC();
}

function exportReport() {
var reportContent = reportBox.text;
if (!reportContent || reportContent.length === 0) {
alert("The report log is empty. Run QC before exporting.", "Empty Log");
return;
}

var doc = getActiveDocument();
var defaultPath = "~/Desktop";
if (doc && doc.saved) {
defaultPath = doc.filePath;
}

var fileName = (doc ? doc.name.replace(/\.indd$/i, "") : "Document") + "_QC_Report.txt";
var file = new File(defaultPath + "/" + fileName).saveDlg("Save QC Audit Report", "Text File:*.txt");
if (file) {
file.encoding = "UTF-8";
file.open("w");
file.write(reportContent);
file.close();
appendLog("Report successfully saved to: " + file.fsName);
}
}

// -------------------------------------------------------------
// UI Construction (Compact 2-Column Layout)
// -------------------------------------------------------------
if ($.global.qcmaster_win && $.global.qcmaster_win instanceof Window) {
try { $.global.qcmaster_win.close(); } catch (eW) {}
}
var win = new Window("palette", "QC Master Dashboard v2.4 | Prepress Audit & Fix Hub", undefined, { resizeable: true });
$.global.qcmaster_win = win;
win.orientation = "column";
win.alignChildren = ["fill", "top"];
win.margins = 10;
win.spacing = 6;

// --- Compact Header: Document Information & Actions ---
var pnlHeader = win.add("panel", undefined, "Document & Actions");
pnlHeader.orientation = "column";
pnlHeader.alignChildren = ["fill", "top"];
pnlHeader.margins = 8;
pnlHeader.spacing = 6;

var headerRow1 = pnlHeader.add("group");
headerRow1.orientation = "row";
headerRow1.alignChildren = ["left", "center"];
headerRow1.spacing = 16;

var initialDoc = getActiveDocument();
var docNameText = initialDoc ? initialDoc.name : "No active document";
var lblDocName = headerRow1.add("statictext", undefined, "Document: " + docNameText);
lblDocName.preferredSize.width = 340;
var lblStatus = headerRow1.add("statictext", undefined, "Status: Ready");
lblStatus.preferredSize.width = 250;

var headerRow2 = pnlHeader.add("group");
headerRow2.orientation = "row";
headerRow2.alignChildren = ["left", "center"];
headerRow2.spacing = 8;

var btnRunQC = headerRow2.add("button", undefined, "Run Full QC");
btnRunQC.preferredSize = [105, 24];

var btnReset = headerRow2.add("button", undefined, "Reset");
btnReset.preferredSize = [60, 24];

var btnRefresh = headerRow2.add("button", undefined, "Refresh");
btnRefresh.preferredSize = [70, 24];

var btnExport = headerRow2.add("button", undefined, "Export Report");
btnExport.preferredSize = [100, 24];

// --- Main 2-Column Container ---
var mainCols = win.add("group");
mainCols.orientation = "row";
mainCols.alignChildren = ["fill", "top"];
mainCols.spacing = 8;

// =============================================================
// COLUMN 1: Text & Typography
// =============================================================
var col1 = mainCols.add("panel", undefined, "Typography & Text");
col1.orientation = "column";
col1.alignChildren = ["fill", "top"];
col1.preferredSize.width = 335;
col1.margins = 8;
col1.spacing = 7;

// Row 1.1: Double Spaces
var rowDouble = col1.add("group");
rowDouble.orientation = "row";
rowDouble.alignChildren = ["left", "center"];
rowDouble.spacing = 4;
var lblDoubleTitle = rowDouble.add("statictext", undefined, "Double Space:");
lblDoubleTitle.preferredSize.width = 82;
var lblDoubleSpaceCount = rowDouble.add("statictext", undefined, "0");
lblDoubleSpaceCount.preferredSize.width = 24;
var btnScanDouble = rowDouble.add("button", undefined, "Scan");
btnScanDouble.preferredSize = [38, 21];
var btnReviewDoubleSpace = rowDouble.add("button", undefined, "Find");
btnReviewDoubleSpace.preferredSize = [38, 21];
var btnFixCurrentDoubleSpace = rowDouble.add("button", undefined, "Fix");
btnFixCurrentDoubleSpace.preferredSize = [32, 21];
var btnFixDoubleSpaces = rowDouble.add("button", undefined, "All");
btnFixDoubleSpaces.preferredSize = [32, 21];

// Row 1.2: Widow Check (Scan | Find | Fix | All)
var rowWidow = col1.add("group");
rowWidow.orientation = "row";
rowWidow.alignChildren = ["left", "center"];
rowWidow.spacing = 4;
var lblWidowTitle = rowWidow.add("statictext", undefined, "Widows:");
lblWidowTitle.preferredSize.width = 82;
var lblWidowCount = rowWidow.add("statictext", undefined, "0");
lblWidowCount.preferredSize.width = 24;
var btnScanWidow = rowWidow.add("button", undefined, "Scan");
btnScanWidow.preferredSize = [38, 21];
var btnReviewWidow = rowWidow.add("button", undefined, "Find");
btnReviewWidow.preferredSize = [38, 21];
var btnFixCurrentWidow = rowWidow.add("button", undefined, "Fix");
btnFixCurrentWidow.preferredSize = [32, 21];
var btnFixAllWidow = rowWidow.add("button", undefined, "All");
btnFixAllWidow.preferredSize = [32, 21];

// Row 1.3: Hyphenated Lines & Uncheck Option
var rowHyphen = col1.add("group");
rowHyphen.orientation = "row";
rowHyphen.alignChildren = ["left", "center"];
rowHyphen.spacing = 4;
var lblHyphenTitle = rowHyphen.add("statictext", undefined, "Hyphenation:");
lblHyphenTitle.preferredSize.width = 82;
var lblHyphenCount = rowHyphen.add("statictext", undefined, "0");
lblHyphenCount.preferredSize.width = 24;
var btnScanHyphen = rowHyphen.add("button", undefined, "Scan");
btnScanHyphen.preferredSize = [38, 21];
var btnReviewHyphen = rowHyphen.add("button", undefined, "Find");
btnReviewHyphen.preferredSize = [38, 21];
var btnUncheckHyphen = rowHyphen.add("button", undefined, "Uncheck");
btnUncheckHyphen.preferredSize = [56, 21];

// Row 1.4: Ligatures Used & Fix
var rowLigature = col1.add("group");
rowLigature.orientation = "row";
rowLigature.alignChildren = ["left", "center"];
rowLigature.spacing = 4;
var lblLigatureTitle = rowLigature.add("statictext", undefined, "Ligatures:");
lblLigatureTitle.preferredSize.width = 82;
var lblLigatureCount = rowLigature.add("statictext", undefined, "0");
lblLigatureCount.preferredSize.width = 24;
var btnScanLigature = rowLigature.add("button", undefined, "Scan");
btnScanLigature.preferredSize = [38, 21];
var btnReviewLigature = rowLigature.add("button", undefined, "Find");
btnReviewLigature.preferredSize = [38, 21];
var btnFixLigature = rowLigature.add("button", undefined, "Off");
btnFixLigature.preferredSize = [32, 21];
var btnAllLigature = rowLigature.add("button", undefined, "All");
btnAllLigature.preferredSize = [32, 21];

// =============================================================
// COLUMN 2: Colors, Assets & Links
// =============================================================
var col2 = mainCols.add("panel", undefined, "Colors, Frames & Links");
col2.orientation = "column";
col2.alignChildren = ["fill", "top"];
col2.preferredSize.width = 335;
col2.margins = 8;
col2.spacing = 7;

// Row 2.1: Unused Swatches
var rowSwatches = col2.add("group");
rowSwatches.orientation = "row";
rowSwatches.alignChildren = ["left", "center"];
rowSwatches.spacing = 4;
var lblSwatchesTitle = rowSwatches.add("statictext", undefined, "Swatches:");
lblSwatchesTitle.preferredSize.width = 82;
var lblUnusedCount = rowSwatches.add("statictext", undefined, "0");
lblUnusedCount.preferredSize.width = 24;
var btnScanSwatches = rowSwatches.add("button", undefined, "Scan");
btnScanSwatches.preferredSize = [38, 21];
var btnRemoveSwatches = rowSwatches.add("button", undefined, "Remove All");
btnRemoveSwatches.preferredSize = [80, 21];

// Row 2.2: Black Color Issues (Strict 100% Solid & 80% Tint Only; No 4-Color Black)
var rowBlack = col2.add("group");
rowBlack.orientation = "row";
rowBlack.alignChildren = ["left", "center"];
rowBlack.spacing = 3;
var lblBlackTitle = rowBlack.add("statictext", undefined, "Black Issues:");
lblBlackTitle.preferredSize.width = 75;
var lblBlackColorCount = rowBlack.add("statictext", undefined, "0");
lblBlackColorCount.preferredSize.width = 20;
var btnScanBlack = rowBlack.add("button", undefined, "Scan");
btnScanBlack.preferredSize = [35, 21];
var btnReviewBlack = rowBlack.add("button", undefined, "Find");
btnReviewBlack.preferredSize = [35, 21];
var btnFix80Black = rowBlack.add("button", undefined, "FIX80");
btnFix80Black.preferredSize = [42, 21];
var btnFix100Black = rowBlack.add("button", undefined, "FIX100");
btnFix100Black.preferredSize = [46, 21];
var btnFixAllBlack = rowBlack.add("button", undefined, "All");
btnFixAllBlack.preferredSize = [28, 21];

// Row 2.3: Image Frames with Fill (Find | Clear)
var rowImgFill = col2.add("group");
rowImgFill.orientation = "row";
rowImgFill.alignChildren = ["left", "center"];
rowImgFill.spacing = 4;
var lblImgFillTitle = rowImgFill.add("statictext", undefined, "Img Fill Color:");
lblImgFillTitle.preferredSize.width = 82;
var lblImageFrameCount = rowImgFill.add("statictext", undefined, "0");
lblImageFrameCount.preferredSize.width = 24;
var btnScanImgFill = rowImgFill.add("button", undefined, "Scan");
btnScanImgFill.preferredSize = [38, 21];
var btnReviewImageFrame = rowImgFill.add("button", undefined, "Find");
btnReviewImageFrame.preferredSize = [38, 21];
var btnClearImgFill = rowImgFill.add("button", undefined, "Clear");
btnClearImgFill.preferredSize = [40, 21];

// Row 2.4: Disallowed URLs (.in / com/in)
var rowUrls = col2.add("group");
rowUrls.orientation = "row";
rowUrls.alignChildren = ["left", "center"];
rowUrls.spacing = 4;
var lblUrlTitle = rowUrls.add("statictext", undefined, "URLs (.in):");
lblUrlTitle.preferredSize.width = 82;
var lblUrlCount = rowUrls.add("statictext", undefined, "0");
lblUrlCount.preferredSize.width = 24;
var btnScanUrls = rowUrls.add("button", undefined, "Scan");
btnScanUrls.preferredSize = [38, 21];
var btnReviewUrl = rowUrls.add("button", undefined, "Find");
btnReviewUrl.preferredSize = [38, 21];

// --- Panel: QC Report Log ---
var pnlReport = win.add("panel", undefined, "QC Report");
pnlReport.alignChildren = ["fill", "fill"];
pnlReport.margins = 6;

var reportBox = pnlReport.add("edittext", undefined, "QC Master Dashboard ready.\nClick 'Run Full QC' or individual [Scan] buttons to begin.\n", {
multiline: true,
readonly: true,
scrollable: true
});
reportBox.preferredSize = [670, 85];

// --- Footer: Credits & Buttons ---
var footerGroup = win.add("group");
footerGroup.orientation = "row";
footerGroup.alignChildren = ["fill", "center"];
footerGroup.alignment = ["fill", "bottom"];

var lblCredits = footerGroup.add("statictext", undefined, "Developed by Nousheeth Iqbal");
lblCredits.preferredSize.width = 380;

var btnClearLog = footerGroup.add("button", undefined, "Clear Log");
btnClearLog.preferredSize = [75, 23];
var btnClose = footerGroup.add("button", undefined, "Close");
btnClose.preferredSize = [65, 23];

// -------------------------------------------------------------
// Event Listeners
// -------------------------------------------------------------
btnRunQC.onClick = function () { runFullQC(); };
btnReset.onClick = function () { pristineReset(); };
btnRefresh.onClick = function () { refreshQC(); };
btnExport.onClick = function () { exportReport(); };

// Individual Scan Listeners
btnScanDouble.onClick = function () { scanDoubleSpaces(); };
btnScanWidow.onClick = function () { scanWidows(); };
btnScanHyphen.onClick = function () { scanHyphenation(); };
btnScanLigature.onClick = function () { scanLigatures(); };
btnScanSwatches.onClick = function () { scanUnusedSwatches(); };
btnScanBlack.onClick = function () { scanBlackColorIssues(); };
btnScanImgFill.onClick = function () { scanImageFramesWithFill(); };
btnScanUrls.onClick = function () { scanDisallowedUrls(); };

// Action Listeners
btnReviewDoubleSpace.onClick = function () { reviewDoubleSpace(); };
btnFixCurrentDoubleSpace.onClick = function () { fixCurrentDoubleSpace(); };
btnFixDoubleSpaces.onClick = function () { fixAllDoubleSpaces(); };

btnReviewWidow.onClick = function () { reviewWidow(); };
btnFixCurrentWidow.onClick = function () { fixCurrentWidow(); };
btnFixAllWidow.onClick = function () { fixAllWidows(); };

btnReviewHyphen.onClick = function () { reviewHyphenation(); };
btnUncheckHyphen.onClick = function () { uncheckAllHyphenation(); };

btnReviewLigature.onClick = function () { reviewLigature(); };
btnFixLigature.onClick = function () { fixCurrentLigature(); };
btnAllLigature.onClick = function () { fixAllLigatures(); };

btnRemoveSwatches.onClick = function () { removeAllUnusedSwatches(); };
btnReviewBlack.onClick = function () { reviewBlackColorIssue(); };
btnFix80Black.onClick = function () { fixCurrentBlackIssue(80); };
btnFix100Black.onClick = function () { fixCurrentBlackIssue(100); };
btnFixAllBlack.onClick = function () { fixAllBlackIssues(); };

btnReviewImageFrame.onClick = function () { reviewImageFrame(); };
btnClearImgFill.onClick = function () { clearCurrentImgFill(); };

btnReviewUrl.onClick = function () { reviewDisallowedUrl(); };

btnClearLog.onClick = function () { reportBox.text = ""; };
btnClose.onClick = function () {
try { win.close(); } catch (e) {}
$.global.qcmaster_win = null;
};

// Show modeless palette
win.center();
win.show();
})();

 

Translate
leo.r
Community Expert
Community Expert
Nov 4, 2026 2:32 PM UTC

​@Nousheeth Hi, you keep posting these ultra-long reviews of your script. The forum is not intended for this and this thread becomes unmanageable and impossible to view. Should you decide to post additional script reviews, please either attach them as text files or post them on a cloud platform and link to them. 

Translate
NousheethAuthor
Known Participant
Oct 23, 2026 5:30 PM UTC

HI

 

Translate
Colin Flashman
Community Expert
Community Expert
Oct 27, 2023 12:38 PM UTC

There is a script that will do this that is available from CreativePro.com - it is called combomambo.

Description on how it works/where to download it is here: https://creativepro.com/combomambo-script-updated-indesign-cc-2014/

Translate
If the answer wasn't in my post, perhaps it might be on my blog at colecandoo!
G_Man
Inspiring
Oct 16, 2024 1:07 PM UTC

Wow - I'm SO pleased I found this. Designed in 2014 - works in 2024 - why isn't this a part of CC? Saved me a ton of work. Just create a plain text file of your lists, run the script and it imports the info for each text field individually. Brilliant. 

Translate