Skip to main content
dublove
Legend
May 31, 2026
Question

How can I eliminate the line spacing discrepancies caused by different line counts when “adding or removing a line”?

  • May 31, 2026
  • 3 replies
  • 40 views

Select the range from the first line to the “Only by” line (blue), run the script, and the gap between the bottom text and the bottom of the text box will be essentially the same as before.

However, when you select the range from the first line to the “help efforts” line (orange) and run the script, the gap between the bottom text and the bottom of the text box becomes noticeably larger.
Where does this discrepancy come from, and how can it be avoided?

 

Even if you change `selLinesNum` to `selLinesNum-1`, the same problem will occur.

 

The goal is to add (or remove) one row at a time.
Does the formula look wrong?

var d = app.activeDocument;
var item = d.selection[0];
var zp = app.activeDocument.zeroPoint;
app.activeDocument.zeroPoint = [0, 0];
var selLinesNum = item.lines.length;
// Get the text frame height
var tsf = item.parentTextFrames;
var sb = tsf[0].visibleBounds;
var boxH = sb[2] - sb[1];


// Get current font size and line spacing
var fontSize = item.pointSize;
var ole = item.leading;


// Desired number of lines for the text frame
var selLinesNum;
var newLeading = ole * (fontSize / (selLinesNum));
//var newLeading = ole * (fontSize / (selLinesNum-1));
item.leading = newLeading;

 

    3 replies

    Community Expert
    May 31, 2026

    This works as a rough leading recalculation, but it doesn’t actually check whether one more or one fewer line fits, and it doesn’t preserve the bottom gap. InDesign composition is a bit awkward here because changing leading affects threaded text flow, so the script needs to test leading values and recompose until the selected frame gains or loses the requested number of visible lines.

     

    Here I’ve been working on these since your first request, it’s as close as I can get

    Decrease

    (function () {
    var SCRIPT_NAME = "Decrease Line Spacing 1 Line";
    var DIRECTION = "decrease";
    var STEPS = 1;
    var SCOPE = "story";
    var DEFAULT_MIN_LEADING = 1;
    var DEFAULT_MAX_LEADING = 500;
    var SEARCH_INCREMENT = 0.05;
    var MAX_CHANGE_POINTS = 4;
    var MAX_CHANGE_RATIO = 0.25;

    if (app.documents.length === 0) {
    alert("Open a document first.", SCRIPT_NAME);
    return;
    }

    if (app.selection.length === 0) {
    alert("Select a text frame, or place the cursor in text first.", SCRIPT_NAME);
    return;
    }

    var frame = getSelectedTextFrame();
    if (!frame) {
    alert("Select a text frame, or place the cursor in text first.", SCRIPT_NAME);
    return;
    }

    app.doScript(
    function () {
    adjustLeadingToNearestLine(frame);
    },
    ScriptLanguage.JAVASCRIPT,
    undefined,
    UndoModes.ENTIRE_SCRIPT,
    SCRIPT_NAME
    );

    function adjustLeadingToNearestLine(textFrame) {
    var lines = textFrame.lines;
    var lineCount = lines.length;
    var targetLineCount = lineCount + STEPS;
    var originalBottomGap = getBottomGap(textFrame);
    var currentLeading;
    var result;
    var newLeading;

    if (lineCount < 2) {
    alert("The selected frame needs at least two composed lines.", SCRIPT_NAME);
    return;
    }

    currentLeading = getCurrentLeading(textFrame);

    if (!isFinite(currentLeading) || currentLeading <= 0) {
    alert("Could not read a numeric leading value. Try selecting text that uses fixed leading instead of Auto.", SCRIPT_NAME);
    return;
    }

    result = findNearestLeading(textFrame, currentLeading, targetLineCount, DIRECTION, SCOPE);

    if (!result) {
    alert(
    "No " + STEPS + "-line change was found within a nearby leading range.\n\n" +
    "Current leading: " + round2(currentLeading) + " pt\n" +
    "Search limit: +/-" + round2(getSearchLimit(currentLeading)) + " pt",
    SCRIPT_NAME
    );
    return;
    }

    newLeading = result.leading;
    applyLeading(textFrame, SCOPE, newLeading);
    app.activeDocument.recompose();

    if (isFinite(originalBottomGap)) {
    restoreBottomGap(textFrame, originalBottomGap);
    app.activeDocument.recompose();
    }
    }

    function findNearestLeading(textFrame, currentLeading, targetLineCount, direction, scope) {
    var limit = getSearchLimit(currentLeading);
    var maxSteps = Math.ceil(limit / SEARCH_INCREMENT);
    var originalLeadings = captureLeadings(textFrame, scope);
    var i;
    var trialLeading;
    var trialLineCount;

    if (!originalLeadings) {
    return null;
    }

    try {
    for (i = 1; i <= maxSteps; i++) {
    trialLeading = currentLeading - i * SEARCH_INCREMENT;

    if (trialLeading < DEFAULT_MIN_LEADING || trialLeading > DEFAULT_MAX_LEADING) {
    break;
    }

    applyLeading(textFrame, scope, trialLeading);
    app.activeDocument.recompose();
    trialLineCount = textFrame.lines.length;

    if (trialLineCount >= targetLineCount) {
    return {
    leading: trialLeading,
    lineCount: trialLineCount
    };
    }
    }
    } finally {
    restoreLeadings(originalLeadings);
    app.activeDocument.recompose();
    }

    return null;
    }

    function getSearchLimit(currentLeading) {
    return Math.max(MAX_CHANGE_POINTS, currentLeading * MAX_CHANGE_RATIO);
    }

    function getSelectedTextFrame() {
    var sel = app.selection[0];
    var name = "";

    try {
    name = sel.constructor.name;
    if (name === "TextFrame") {
    return sel;
    }
    } catch (e) {}

    try {
    if (sel.parentTextFrames && sel.parentTextFrames.length > 0) {
    return sel.parentTextFrames[0];
    }
    } catch (e2) {}

    try {
    name = sel.parent.constructor.name;
    if (sel.parent && name === "TextFrame") {
    return sel.parent;
    }
    } catch (e3) {}

    return null;
    }

    function getCurrentLeading(textFrame) {
    var ranges;
    var values = [];
    var i;
    var leading;
    var pointSize;
    var autoLeading;

    try {
    ranges = textFrame.textStyleRanges;
    for (i = 0; i < ranges.length; i++) {
    leading = ranges[i].leading;

    if (String(leading) === String(Leading.AUTO)) {
    pointSize = Number(ranges[i].pointSize);
    autoLeading = Number(ranges[i].autoLeading);
    if (isFinite(pointSize) && isFinite(autoLeading)) {
    values.push(pointSize * autoLeading / 100);
    }
    } else if (isFinite(Number(leading))) {
    values.push(Number(leading));
    }
    }

    if (values.length === 0) {
    return NaN;
    }

    values.sort(function (a, b) {
    return a - b;
    });

    return values[Math.floor(values.length / 2)];
    } catch (e) {
    return NaN;
    }
    }

    function applyLeading(textFrame, scope, leading) {
    if (scope === "frame") {
    textFrame.texts[0].leading = leading;
    return;
    }

    textFrame.parentStory.texts[0].leading = leading;
    }

    function captureLeadings(textFrame, scope) {
    var text;
    var ranges;
    var captured = [];
    var i;

    try {
    text = scope === "frame" ? textFrame.texts[0] : textFrame.parentStory.texts[0];
    ranges = text.textStyleRanges;

    for (i = 0; i < ranges.length; i++) {
    captured.push({
    range: ranges[i],
    leading: ranges[i].leading
    });
    }

    return captured;
    } catch (e) {
    return null;
    }
    }

    function restoreLeadings(captured) {
    var i;

    for (i = 0; i < captured.length; i++) {
    try {
    captured[i].range.leading = captured[i].leading;
    } catch (e) {}
    }
    }

    function getBottomGap(textFrame) {
    var bounds;
    var lines;

    try {
    lines = textFrame.lines;
    if (lines.length === 0) {
    return NaN;
    }

    bounds = textFrame.geometricBounds;
    return Number(bounds[2]) - Number(lines[lines.length - 1].baseline);
    } catch (e) {
    return NaN;
    }
    }

    function restoreBottomGap(textFrame, bottomGap) {
    var bounds;
    var lines;

    try {
    lines = textFrame.lines;
    if (lines.length === 0) {
    return;
    }

    bounds = textFrame.geometricBounds;
    bounds[2] = Number(lines[lines.length - 1].baseline) + bottomGap;
    textFrame.geometricBounds = bounds;
    } catch (e) {}
    }

    function round2(value) {
    return Math.round(value * 100) / 100;
    }
    }());




    Increase

    (function () {
    var SCRIPT_NAME = "Increase Line Spacing 1 Line";
    var DIRECTION = "increase";
    var STEPS = 1;
    var SCOPE = "story";
    var DEFAULT_MIN_LEADING = 1;
    var DEFAULT_MAX_LEADING = 500;
    var SEARCH_INCREMENT = 0.05;
    var MAX_CHANGE_POINTS = 4;
    var MAX_CHANGE_RATIO = 0.25;

    if (app.documents.length === 0) {
    alert("Open a document first.", SCRIPT_NAME);
    return;
    }

    if (app.selection.length === 0) {
    alert("Select a text frame, or place the cursor in text first.", SCRIPT_NAME);
    return;
    }

    var frame = getSelectedTextFrame();
    if (!frame) {
    alert("Select a text frame, or place the cursor in text first.", SCRIPT_NAME);
    return;
    }

    app.doScript(
    function () {
    adjustLeadingToNearestLine(frame);
    },
    ScriptLanguage.JAVASCRIPT,
    undefined,
    UndoModes.ENTIRE_SCRIPT,
    SCRIPT_NAME
    );

    function adjustLeadingToNearestLine(textFrame) {
    var lines = textFrame.lines;
    var lineCount = lines.length;
    var targetLineCount = lineCount - STEPS;
    var originalBottomGap = getBottomGap(textFrame);
    var currentLeading;
    var result;
    var newLeading;

    if (lineCount < 2) {
    alert("The selected frame needs at least two composed lines.", SCRIPT_NAME);
    return;
    }

    if (targetLineCount < 2) {
    alert("There are not enough composed lines in this frame to increase by " + STEPS + " line(s).", SCRIPT_NAME);
    return;
    }

    currentLeading = getCurrentLeading(textFrame);

    if (!isFinite(currentLeading) || currentLeading <= 0) {
    alert("Could not read a numeric leading value. Try selecting text that uses fixed leading instead of Auto.", SCRIPT_NAME);
    return;
    }

    result = findNearestLeading(textFrame, currentLeading, targetLineCount, DIRECTION, SCOPE);

    if (!result) {
    alert(
    "No " + STEPS + "-line change was found within a nearby leading range.\n\n" +
    "Current leading: " + round2(currentLeading) + " pt\n" +
    "Search limit: +/-" + round2(getSearchLimit(currentLeading)) + " pt",
    SCRIPT_NAME
    );
    return;
    }

    newLeading = result.leading;
    applyLeading(textFrame, SCOPE, newLeading);
    app.activeDocument.recompose();

    if (isFinite(originalBottomGap)) {
    restoreBottomGap(textFrame, originalBottomGap);
    app.activeDocument.recompose();
    }
    }

    function findNearestLeading(textFrame, currentLeading, targetLineCount, direction, scope) {
    var limit = getSearchLimit(currentLeading);
    var maxSteps = Math.ceil(limit / SEARCH_INCREMENT);
    var originalLeadings = captureLeadings(textFrame, scope);
    var i;
    var trialLeading;
    var trialLineCount;

    if (!originalLeadings) {
    return null;
    }

    try {
    for (i = 1; i <= maxSteps; i++) {
    trialLeading = currentLeading + i * SEARCH_INCREMENT;

    if (trialLeading < DEFAULT_MIN_LEADING || trialLeading > DEFAULT_MAX_LEADING) {
    break;
    }

    applyLeading(textFrame, scope, trialLeading);
    app.activeDocument.recompose();
    trialLineCount = textFrame.lines.length;

    if (trialLineCount <= targetLineCount) {
    return {
    leading: trialLeading,
    lineCount: trialLineCount
    };
    }
    }
    } finally {
    restoreLeadings(originalLeadings);
    app.activeDocument.recompose();
    }

    return null;
    }

    function getSearchLimit(currentLeading) {
    return Math.max(MAX_CHANGE_POINTS, currentLeading * MAX_CHANGE_RATIO);
    }

    function getSelectedTextFrame() {
    var sel = app.selection[0];
    var name = "";

    try {
    name = sel.constructor.name;
    if (name === "TextFrame") {
    return sel;
    }
    } catch (e) {}

    try {
    if (sel.parentTextFrames && sel.parentTextFrames.length > 0) {
    return sel.parentTextFrames[0];
    }
    } catch (e2) {}

    try {
    name = sel.parent.constructor.name;
    if (sel.parent && name === "TextFrame") {
    return sel.parent;
    }
    } catch (e3) {}

    return null;
    }

    function getCurrentLeading(textFrame) {
    var ranges;
    var values = [];
    var i;
    var leading;
    var pointSize;
    var autoLeading;

    try {
    ranges = textFrame.textStyleRanges;
    for (i = 0; i < ranges.length; i++) {
    leading = ranges[i].leading;

    if (String(leading) === String(Leading.AUTO)) {
    pointSize = Number(ranges[i].pointSize);
    autoLeading = Number(ranges[i].autoLeading);
    if (isFinite(pointSize) && isFinite(autoLeading)) {
    values.push(pointSize * autoLeading / 100);
    }
    } else if (isFinite(Number(leading))) {
    values.push(Number(leading));
    }
    }

    if (values.length === 0) {
    return NaN;
    }

    values.sort(function (a, b) {
    return a - b;
    });

    return values[Math.floor(values.length / 2)];
    } catch (e) {
    return NaN;
    }
    }

    function applyLeading(textFrame, scope, leading) {
    if (scope === "frame") {
    textFrame.texts[0].leading = leading;
    return;
    }

    textFrame.parentStory.texts[0].leading = leading;
    }

    function captureLeadings(textFrame, scope) {
    var text;
    var ranges;
    var captured = [];
    var i;

    try {
    text = scope === "frame" ? textFrame.texts[0] : textFrame.parentStory.texts[0];
    ranges = text.textStyleRanges;

    for (i = 0; i < ranges.length; i++) {
    captured.push({
    range: ranges[i],
    leading: ranges[i].leading
    });
    }

    return captured;
    } catch (e) {
    return null;
    }
    }

    function restoreLeadings(captured) {
    var i;

    for (i = 0; i < captured.length; i++) {
    try {
    captured[i].range.leading = captured[i].leading;
    } catch (e) {}
    }
    }

    function getBottomGap(textFrame) {
    var bounds;
    var lines;

    try {
    lines = textFrame.lines;
    if (lines.length === 0) {
    return NaN;
    }

    bounds = textFrame.geometricBounds;
    return Number(bounds[2]) - Number(lines[lines.length - 1].baseline);
    } catch (e) {
    return NaN;
    }
    }

    function restoreBottomGap(textFrame, bottomGap) {
    var bounds;
    var lines;

    try {
    lines = textFrame.lines;
    if (lines.length === 0) {
    return;
    }

    bounds = textFrame.geometricBounds;
    bounds[2] = Number(lines[lines.length - 1].baseline) + bottomGap;
    textFrame.geometricBounds = bounds;
    } catch (e) {}
    }

    function round2(value) {
    return Math.round(value * 100) / 100;
    }
    }());

     

    Both do the same thing roughly one brings leading up the other brings it down. 

     

    Adjustments

    At the top of each you can edit these values:

     

    var STEPS = 1; var SCOPE = "story"; var SEARCH_INCREMENT = 0.05; var MAX_CHANGE_POINTS = 4; var MAX_CHANGE_RATIO = 0.25;

     

    Most useful tweaks:

     

    var STEPS = 2;

    Changes by two visible lines instead of one.

     

    var SCOPE = "frame";

    Applies leading only to the selected frame’s text, not the whole threaded story. I kept "story" for your example.

     

    var SEARCH_INCREMENT = 0.1;

    Searches in rougher/faster steps. 0.05 is more accurate.

     

    var MAX_CHANGE_POINTS = 8;

    Allows a larger leading jump if one line cannot be gained/lost nearby.

     

    If you want a stronger or weaker adjustment, change STEPS at the top of the script. That’s the friendliest customisation point.

    dublove
    dubloveAuthor
    Legend
    May 31, 2026

    Hi ​@Eugene Tyson 

    Thank you very much.

    You're amazing.
    I didn't realize it was this complicated...
    I thought finding a reliable formula would be enough to get it all done—guess I was being naive again.

    But it seems to be affecting the right margin (line spacing in the text box); the lines that weren't selected shouldn't be affected.

    Community Expert
    May 31, 2026

    But it seems to be affecting the right margin (line spacing in the text box); the lines that weren't selected shouldn't be affected.

     

    I’m not sure what you mean. Why shouldn’t text flowing be affected? What right margin? You only want it on selected lines? 

    I’m not following what you’re asking - can you give a demo video or screenshots, clearly showing before and after? 

    Community Expert
    May 31, 2026

    I closed the other thread to replies since you opened a new topic along the same lines.

     

    rob day
    Community Expert
    Community Expert
    May 31, 2026

    Where does this discrepancy come from, and how can it be avoided?

     

    var newLeading = ole * (fontSize / (selLinesNum));

     

    The font’s point size has no relationship to leading. Here I am doubling the point size and the text’s 14pt leading (the distance from baseline to baseline) doesn’t change:

     

     

     

     

    dublove
    dubloveAuthor
    Legend
    May 31, 2026

    Hi ​@rob day 

    That's not entirely correct.
    Because the text box has a fixed height.
    The font size and the number of lines determine the value of the leading.

     

    My goal is:
    To determine the most appropriate leading value when the number of lines varies but the text box height remains constant.
    For example: the leading values for 9, 10, and 11 lines.

     

    Here, don’t set the first-line baseline.
    Since I don’t set the first-line baseline on every page, I might use “grid layout.”