When Font Replacement Should Happen at the Definition Level in InDesign
Replacing Helvetica with Arial inside document composite fonts
InDesign font replacement is not always a text-level task.
Recently, I had a small but practical multilingual DTP problem: a Chinese InDesign document used composite fonts, and Helvetica was assigned inside those composite font definitions. The production requirement was simple:
Replace Helvetica with Arial.
At first glance, this sounds like an ordinary font replacement task. But in InDesign, the important question is not only which font should be replaced.
The more important question is:
Which layer owns the font decision?
InDesign documents can contain font information in several places: direct text formatting, paragraph styles, character styles, and composite font definitions. If the change is made at the wrong layer, the document may look correct temporarily but become harder to maintain later.
This is why I created a small script that replaces Helvetica with Arial only inside document-level composite fonts.
Why I Usually Avoid Direct Font Replacement
In many InDesign production workflows, I do not treat font replacement as a direct text-level operation.
If a font is controlled by a paragraph style or a character style, the safer approach is usually to update the style definition itself. This keeps the document structure clean.
Directly replacing the font in the text may fix the appearance, but it can create local overrides.
That causes two practical problems.
First, if the override is cleared later, the text may return to the old font defined in the style.
Second, when new content is added, the same local override may need to be copied or recreated manually.
In other words, direct font replacement can solve the visible problem while leaving the structural problem unresolved.
For maintainable InDesign files, font changes should usually happen at the style level when the font is controlled by a paragraph style or a character style.
If the font is controlled by a paragraph style, update the paragraph style.
If the font is controlled by a character style, update the character style.
But composite fonts add another layer.
Why Composite Fonts Are Different
Composite fonts are commonly used in CJK typography workflows.
Depending on the InDesign version or language edition, composite font features may not be exposed in the same way through the user interface. In other words, some readers may not see the same composite font controls in their own InDesign environment.
In CJK workflows, a paragraph style may use a composite font. Inside that composite font, different character classes can be assigned different fonts. For example, Chinese characters may use a CJK font, while Latin letters, numbers, and symbols may use a separate Latin font.
That means the visible text may not directly tell the whole story.
The paragraph style may simply point to a composite font. The actual Latin font used inside that composite font may be Helvetica.
In that case, changing the paragraph style to Arial would not be the right interpretation of the problem.
The issue is not that the paragraph style itself should become Arial.
The issue is that Helvetica should no longer be used inside the composite font definition.
So the replacement target is not ordinary text.
It is not a paragraph style.
It is not a character style.
It is the applied font inside the document’s composite font entries.
Two Possible Approaches
There were two reasonable ways to handle this.
| Approach | When it makes sense |
|---|---|
| Create a new composite font | When the old and new composite fonts need to coexist |
| Modify the existing composite font | When the old font should no longer be used in that composite font |
The first approach is to create a new composite font and then update paragraph styles and character styles so that they use the new composite font.
This is a good approach when both composite fonts need to remain available. For example, if some pages or styles should continue using the old composite font while others should use the new one, creating a separate composite font keeps the distinction clear.
The second approach is to modify the existing composite font definition.
This is appropriate when the old font should no longer be used at all in that composite font. In this case, the style references can stay as they are. The definition they point to is updated instead.
For this case, Helvetica was no longer supposed to be used.
So I chose the second approach.
Why I Chose to Modify the Existing Composite Font
The requirement was not to create a new font design system.
It was to remove Helvetica from the existing document-level composite font setup and replace it with Arial.
Creating a new composite font would have worked, but it would also have introduced another definition into the document. Then I would need to update paragraph styles and character styles that referenced the old composite font.
That approach is useful when both definitions must coexist.
But here, the old Helvetica-based composite font was not needed anymore.
So the better choice was to update the existing composite font definition.
This kept the style structure intact. Paragraph styles and character styles could continue pointing to the same composite font. Only the internal font assignment changed.
The important decision was not Helvetica versus Arial.
It was whether the existing composite font definition should be updated or replaced with a new one.
The Script Scope: Document Composite Fonts Only
The script is intentionally narrow.
It does not try to replace every instance of Helvetica in the document.
It only scans composite fonts stored in the active document and changes Helvetica entries inside those document composite font definitions.
At the top of the script, the intended scope is documented clearly:
/*Replaces Helvetica with Arial in document composite fonts.Scope:- Processes only app.activeDocument.compositeFonts.- Does not directly modify app.compositeFonts.- Does not modify paragraph styles, character styles, normal text, or find/change settings.*/(function () {main();}());function main() {var doc;var fallbackArialFont;var log;var compositeFonts;var i;var cf;var shouldSkipIndexZero;var summaryMessage;if (app.documents.length === 0) {alert("No InDesign document is open.");return;}if (!confirm("This script will replace Helvetica used in composite fonts in the current document with Arial. Continue?")) {return;}fallbackArialFont = getArialFontByStyle("Regular");if (fallbackArialFont === null) {alert("Cannot continue because Arial is not available.");return;}doc = app.activeDocument;log = {compositeFontsScanned: 0,entriesScanned: 0,entriesChanged: 0,styleMappedEntries: 0,fallbackEntries: 0,styleSetWarnings: 0,errors: 0,lines: []};try {compositeFonts = doc.compositeFonts;} catch (e) {alert("Could not retrieve the document composite fonts.\r" + e);return;}shouldSkipIndexZero = canSkipFirstCompositeFont(compositeFonts);for (i = 0; i < compositeFonts.length; i++) {if (i === 0 && shouldSkipIndexZero) {log.lines.push("Skipped composite font at index 0: " + safeGetCompositeFontName(compositeFonts[0]));continue;}try {cf = compositeFonts[i];processCompositeFont(cf, fallbackArialFont, log);} catch (e2) {log.errors++;log.lines.push("[Error] Composite font index " + i + ": " + e2);}}summaryMessage = buildSummaryMessage(log);alert(summaryMessage);}function getArialFontByStyle(styleName) {var font;var normalizedStyle;var candidates;var i;normalizedStyle = normalizeArialStyleName(styleName);candidates = getArialFontNameCandidates(normalizedStyle);for (i = 0; i < candidates.length; i++) {font = getValidFontByName(candidates[i]);if (font !== null) {return font;}}return null;}function getArialFontNameCandidates(styleName) {var candidates;candidates = [];if (styleName === "Bold Italic") {candidates.push("Arial\tBold Italic");candidates.push("Arial Bold Italic");candidates.push("Arial-BoldItalicMT");candidates.push("Arial-BoldItalic");} else if (styleName === "Bold") {candidates.push("Arial\tBold");candidates.push("Arial Bold");candidates.push("Arial-BoldMT");candidates.push("Arial-Bold");} else if (styleName === "Italic") {candidates.push("Arial\tItalic");candidates.push("Arial Italic");candidates.push("Arial-ItalicMT");candidates.push("Arial-Italic");} else {candidates.push("Arial\tRegular");candidates.push("Arial");candidates.push("ArialMT");candidates.push("Arial-Regular");}return candidates;}function getValidFontByName(fontName) {var font;try {font = app.fonts.itemByName(fontName);if (font !== null && font.isValid) {return font;}} catch (e) {}return null;}function canSkipFirstCompositeFont(compositeFonts) {var firstName;if (!compositeFonts || compositeFonts.length < 1) {return false;}try {if (!compositeFonts[0].isValid) {return true;}} catch (e) {return true;}firstName = safeGetCompositeFontName(compositeFonts[0]);if (firstName === "") {return false;}if (firstName.indexOf("\u306A\u3057") >= 0 ||firstName.toLowerCase().indexOf("none") >= 0 ||firstName.toLowerCase().indexOf("no composite") >= 0) {return true;}return false;}function processCompositeFont(compositeFont, fallbackArialFont, log) {var entries;var compositeFontName;var i;var entry;compositeFontName = safeGetCompositeFontName(compositeFont);log.compositeFontsScanned++;try {entries = compositeFont.compositeFontEntries;} catch (e) {log.errors++;log.lines.push("[Error] Could not read entries for composite font \"" + compositeFontName + "\": " + e);return;}for (i = 0; i < entries.length; i++) {try {entry = entries[i];processCompositeFontEntry(compositeFontName, entry, fallbackArialFont, log);} catch (e2) {log.errors++;log.lines.push("[Error] Composite font \"" + compositeFontName + "\", entry index " + i + ": " + e2);}}}function processCompositeFontEntry(compositeFontName, entry, fallbackArialFont, log) {var entryName;var oldFont;var oldFontName;var rawStyle;var inferredStyle;var mappedTargetStyle;var arialFont;var fallbackUsed;var styleMapped;var newFontName;var newStyleName;log.entriesScanned++;entryName = safeGetEntryName(entry);try {oldFont = entry.appliedFont;} catch (e) {log.errors++;log.lines.push("[Error] Could not read appliedFont. Composite font: \"" + compositeFontName + "\", entry: \"" + entryName + "\": " + e);return;}oldFontName = getFontDisplayName(oldFont);if (!isHelveticaFont(oldFont)) {return;}rawStyle = getRawFontStyleName(oldFont, entry);inferredStyle = inferFontStyleName(oldFont, entry);mappedTargetStyle = inferredStyle;arialFont = getArialFontByStyle(mappedTargetStyle);fallbackUsed = false;if (arialFont === null) {arialFont = fallbackArialFont;fallbackUsed = true;}styleMapped = isMeaningfullyDifferentStyle(rawStyle, mappedTargetStyle);try {entry.appliedFont = arialFont;} catch (e2) {log.errors++;log.lines.push("[Error] Could not change appliedFont. Composite font: \"" + compositeFontName + "\", entry: \"" + entryName + "\", before: \"" + oldFontName + "\": " + e2);return;}try {newStyleName = getFontStyleForEntry(arialFont, mappedTargetStyle);if (newStyleName !== "") {entry.fontStyle = newStyleName;}} catch (e3) {log.styleSetWarnings++;log.lines.push("[Warning] appliedFont was changed, but fontStyle could not be set. Composite font: \"" + compositeFontName + "\", entry: \"" + entryName + "\": " + e3);}newFontName = getFontDisplayName(arialFont);log.entriesChanged++;if (styleMapped) {log.styleMappedEntries++;}if (fallbackUsed) {log.fallbackEntries++;}log.lines.push("[Changed] Composite font: \"" + compositeFontName + "\", entry: \"" + entryName + "\", inferred style: \"" + inferredStyle + "\", before: \"" + oldFontName + "\", after: \"" + newFontName + "\"" + (fallbackUsed ? " [Fell back to Arial Regular]" : ""));if (styleMapped) {log.lines.push("[Style mapped] Original style \"" + rawStyle + "\" was mapped to \"" + mappedTargetStyle + "\".");}}function isHelveticaFont(fontValue) {var family;var name;var fullName;var text;if (fontValue === null || fontValue === undefined) {return false;}if (typeof fontValue === "string") {return containsHelvetica(fontValue);}family = safeGetProperty(fontValue, "fontFamily");if (family !== "" && containsHelveticaFamily(family)) {return true;}name = safeGetProperty(fontValue, "name");if (name !== "" && containsHelvetica(name)) {return true;}fullName = safeGetProperty(fontValue, "fullName");if (fullName !== "" && containsHelvetica(fullName)) {return true;}text = "";try {text = String(fontValue);} catch (e) {text = "";}return containsHelvetica(text);}function inferFontStyleName(fontValue, entry) {var parts;var i;var normalized;parts = [];addIfNotEmpty(parts, safeGetProperty(fontValue, "fontStyleName"));addIfNotEmpty(parts, safeGetProperty(fontValue, "styleName"));addIfNotEmpty(parts, safeGetProperty(entry, "fontStyle"));addIfNotEmpty(parts, safeGetProperty(fontValue, "name"));addIfNotEmpty(parts, safeGetProperty(fontValue, "fullName"));if (typeof fontValue === "string") {addIfNotEmpty(parts, fontValue);}for (i = 0; i < parts.length; i++) {normalized = normalizeArialStyleName(parts[i]);if (normalized !== "Regular") {return normalized;}}return "Regular";}function getRawFontStyleName(fontValue, entry) {var style;style = safeGetProperty(fontValue, "fontStyleName");if (style !== "") {return style;}style = safeGetProperty(fontValue, "styleName");if (style !== "") {return style;}style = safeGetProperty(entry, "fontStyle");if (style !== "") {return style;}return "";}function isMeaningfullyDifferentStyle(rawStyle, mappedStyle) {var raw;var mapped;raw = normalizeStyleForComparison(rawStyle);mapped = normalizeStyleForComparison(mappedStyle);return raw !== "" && mapped !== "" && raw !== mapped;}function normalizeStyleForComparison(styleName) {var text;text = normalizeText(styleName);if (text === "") {return "";}text = removeFontFamilyWords(text);text = text.replace(/[^a-z0-9]+/g, "");// Ignore family-name residue so plain Helvetica Neue is not reported as a style mapping.if (text === "neue" || text === "helveticaneue" || text === "helvetica" || text === "arial") {return "";}if (text === "roman" || text === "plain" || text === "normal" || text === "book") {return "regular";}return text;}function normalizeArialStyleName(styleName) {var text;var compactText;var isBold;var isItalic;text = normalizeText(styleName);if (text === "") {return "Regular";}text = removeFontFamilyWords(text);compactText = text.replace(/[^a-z0-9]/g, "");isBold = containsStyleWord(text, "bold") ||compactText.indexOf("bold") >= 0 ||containsStyleWord(text, "black") ||compactText.indexOf("black") >= 0 ||containsStyleWord(text, "heavy") ||compactText.indexOf("heavy") >= 0 ||containsStyleWord(text, "demi") ||compactText.indexOf("demi") >= 0 ||containsStyleWord(text, "semi bold") ||containsStyleWord(text, "semibold") ||compactText.indexOf("semibold") >= 0;isItalic = containsStyleWord(text, "italic") ||compactText.indexOf("italic") >= 0 ||containsStyleWord(text, "oblique") ||compactText.indexOf("oblique") >= 0 ||containsStyleWord(text, "slanted") ||compactText.indexOf("slanted") >= 0;if (isBold && isItalic) {return "Bold Italic";}if (isBold) {return "Bold";}if (isItalic) {return "Italic";}return "Regular";}function removeFontFamilyWords(value) {var text;text = value;text = text.replace(/helvetica neue/g, " ");text = text.replace(/helveticaneue/g, " ");text = text.replace(/helvetica/g, " ");text = text.replace(/arial/g, " ");text = text.replace(/\s+/g, " ");text = trimString(text);return text;}function containsStyleWord(value, word) {var text;var normalizedWord;text = " " + value + " ";normalizedWord = " " + normalizeText(word) + " ";if (text.indexOf(normalizedWord) >= 0) {return true;}if (word === "semibold" && value.indexOf("semi bold") >= 0) {return true;}return false;}function getFontStyleForEntry(fontValue, inferredStyle) {var style;style = safeGetProperty(fontValue, "fontStyleName");if (style !== "") {return style;}style = safeGetProperty(fontValue, "styleName");if (style !== "") {return style;}return inferredStyle;}function addIfNotEmpty(list, value) {if (value !== null && value !== undefined && String(value) !== "") {list.push(String(value));}}function containsHelveticaFamily(value) {var text;text = normalizeText(value);if (text === "") {return false;}return text === "helvetica" || text.indexOf("helvetica") === 0;}function containsHelvetica(value) {var text;text = normalizeText(value);if (text === "") {return false;}return /(^|[^a-z0-9])helvetica($|[^a-z0-9]|neue)/.test(text);}function normalizeText(value) {var text;if (value === null || value === undefined) {return "";}try {text = String(value);} catch (e) {return "";}text = text.toLowerCase();text = text.replace(/\r/g, " ");text = text.replace(/\n/g, " ");text = text.replace(/\t/g, " ");text = text.replace(/_/g, " ");text = text.replace(/\s+/g, " ");text = trimString(text);return text;}function getFontDisplayName(fontValue) {var family;var style;var fullName;var name;var result;if (fontValue === null || fontValue === undefined) {return "(None)";}if (typeof fontValue === "string") {return fontValue;}family = safeGetProperty(fontValue, "fontFamily");style = safeGetProperty(fontValue, "fontStyleName");fullName = safeGetProperty(fontValue, "fullName");name = safeGetProperty(fontValue, "name");if (family !== "") {result = family;if (style !== "") {result += "\t" + style;}return result;}if (fullName !== "") {return fullName;}if (name !== "") {return name;}try {return String(fontValue);} catch (e) {return "(Unknown font)";}}function safeGetCompositeFontName(compositeFont) {var name;name = safeGetProperty(compositeFont, "name");if (name !== "") {return name;}return "(Unnamed composite font)";}function safeGetEntryName(entry) {var name;var characterRange;name = safeGetProperty(entry, "name");if (name !== "") {return name;}characterRange = safeGetProperty(entry, "characterRange");if (characterRange !== "") {return characterRange;}return "(Unnamed entry)";}function safeGetProperty(obj, propertyName) {var value;if (obj === null || obj === undefined) {return "";}try {value = obj[propertyName];if (value === null || value === undefined) {return "";}return String(value);} catch (e) {return "";}}function trimString(value) {return value.replace(/^\s+/, "").replace(/\s+$/, "");}function buildSummaryMessage(log) {var message;var maxLines;var i;message = "Finished replacing Helvetica with Arial in composite fonts.\r\r";message += "Composite fonts scanned: " + log.compositeFontsScanned + "\r";message += "Composite font entries scanned: " + log.entriesScanned + "\r";message += "Entries changed: " + log.entriesChanged + "\r";message += "Style-mapped entries: " + log.styleMappedEntries + "\r";message += "Fallback entries: " + log.fallbackEntries + "\r";message += "Style set warnings: " + log.styleSetWarnings + "\r";message += "Errors: " + log.errors;if (log.lines.length > 0) {message += "\r\rLog:";maxLines = 20;for (i = 0; i < log.lines.length && i < maxLines; i++) {message += "\r" + log.lines[i];}if (log.lines.length > maxLines) {message += "\r... " + (log.lines.length - maxLines) + " more log entries.";}}return message;}
This is the core design principle of the script.
The script is not a general font replacement tool.
It is a targeted cleanup script for a specific composite font problem.
Limitations
This script is intentionally limited.
It does not modify ordinary text formatting.
It does not modify paragraph styles.
It does not modify character styles.
It does not create a new composite font.
It does not directly modify application-level composite fonts.
It does not check fonts embedded in exported PDFs.
It does not manage a full font policy for a project.
For this specific workflow, the goal was not to build a general font management system. The goal was to correct one document-level composite font rule safely and repeatably.
What the Script Does
The script follows a simple process:
- Check whether an InDesign document is open.
- Ask for confirmation before making changes.
- Check whether Arial Regular is available.
- Get the composite fonts from the active document.
- Scan each composite font entry.
- Detect Helvetica or Helvetica Neue.
- Infer the font style, such as Regular, Bold, Italic, or Bold Italic.
- Replace the Helvetica entry with the matching Arial font when available.
- Fall back to Arial Regular if a matching Arial style cannot be found.
- Show a summary that separates changed entries, style mappings, fallback replacements, style-setting warnings, and errors.
This is a small script, but the logic reflects a production rule:
Only replace Helvetica when it appears inside document composite font entries.
The summary separates several types of results:
| Summary item | Meaning |
|---|---|
| Composite fonts scanned | Number of document composite fonts checked |
| Composite font entries scanned | Number of composite font entries checked |
| Entries changed | Number of entries where Helvetica was replaced |
| Style-mapped entries | Number of entries where the original style was mapped to a simpler Arial style |
| Fallback entries | Number of entries where a matching Arial style was not found and Arial Regular was used |
| Style set warnings | Number of entries where the font was changed but the font style setting could not be applied |
| Errors | Number of entries or composite fonts that could not be processed |
The distinction between style mapping and fallback is important.
For example, Helvetica Neue Medium may be mapped to Arial Regular. In that case, the script records a style mapping so that the change is visible in the log.
Fallback is different. It means the script tried to find a matching Arial style, but could not find one, so it used Arial Regular instead.
This makes the result easier to review after execution.
Key Code Points
The script starts by getting the composite fonts from the active document:
compositeFonts = doc.compositeFonts;
This is the key scope decision.
The script is not scanning all text in the document. It is not searching paragraph styles. It is not searching character styles.
It is targeting document composite font definitions.
Then it reads the entries inside each composite font:
entries = compositeFont.compositeFontEntries;
Each entry may have an applied font. The script reads that font:
oldFont = entry.appliedFont;
Then it checks whether the font is Helvetica:
if (!isHelveticaFont(oldFont)) {
return;
}
This prevents the script from touching unrelated fonts.
If the entry uses Helvetica, the script selects an Arial font and applies it to the composite font entry:
entry.appliedFont = arialFont;
This is the actual replacement.
Again, the target is not the text itself.
The target is the font assigned inside the composite font definition.
That distinction is the main point of the script.
Demo: Updating the Composite Font Definition
For the demo, I created a fictional Chinese InDesign document.
The demo file contains sample Chinese text mixed with Latin letters and numbers. The paragraph style uses a composite font. Inside that composite font, the Latin entry uses Helvetica.
Before running the script, the composite font definition contains Helvetica.
After running the script, the same composite font definition uses Arial.
The paragraph style still points to the same composite font.
The normal text structure remains unchanged.
The script also shows a summary dialog after execution. This makes it possible to confirm not only how many entries were changed, but also whether any style mappings, fallback replacements, style-setting warnings, or errors occurred.
This is the behavior I wanted.
The script does not create a new document structure. It updates the existing composite font definition.
Conclusion
The lesson from this script is not that every font replacement needs automation.
The lesson is that font replacement should happen at the layer that owns the font decision.
If the font is controlled by a paragraph style, update the paragraph style.
If it is controlled by a character style, update the character style.
If it is controlled inside a composite font, update the composite font definition.
In multilingual DTP, this distinction matters.
A script should not simply change as much as possible. A good script should change only the layer it is supposed to change.
For this case, Helvetica did not need to be replaced everywhere.
It needed to be replaced inside document composite font definitions.
That is why a small, targeted script was the right solution.