Skip to content

Instantly share code, notes, and snippets.

@deqyra
Created January 10, 2024 11:49
Show Gist options
  • Save deqyra/6a52332a6523bc279b8eb5d01231e4b8 to your computer and use it in GitHub Desktop.
Save deqyra/6a52332a6523bc279b8eb5d01231e4b8 to your computer and use it in GitHub Desktop.
InDesign script: Tile PDF pages for booklet printing
/**
* Import a PDF file and tile its pages onto an InDesign document.
* The document is duplex-print-ready, and the printed pages can be
* folded in half into a single booklet or several book signatures.
*
* How to install:
* - Launch InDesign
* - Open the Scripts window (Ctrl+Alt+F11)
* - Right-click the "User" folder and select "Reveal in explorer"
* - Place this script inside the folder
* - Go back to InDesign and enjoy!
*
* @author François Brachais
* @version 1.0
* @license CC 4.0 BY-NC-SA: Creative Commons license with attribution, non-commercial and share-alike terms
* @see https://creativecommons.org/licenses/by-nc-sa/4.0/deed.en
*/
main();
function main() {
app.scriptPreferences.userInteractionLevel = UserInteractionLevels.interactWithAll;
if (app.extractLabel("PDFTiling_ShowInfoDialog") != "false") {
var ok = ShowScriptInfo();
if (!ok) {
exit();
}
}
if (app.documents.length == 0) {
ShowErrorAndExit("No active document. Please create a document first.");
}
var doc = app.activeDocument;
if (!IsDocumentBleedUniform(doc)) {
ShowErrorAndExit("Non-uniform bleed is not supported. Please make sure the bleed is the same for all four sides of your document.");
}
if (!IsDocumentMarginUniform(doc)) {
ShowErrorAndExit("Non-uniform margins are not supported. Please make sure the margins are the same for all four sides of your document.");
}
var unit = doc.viewPreferences.horizontalMeasurementUnits;
app.scriptPreferences.measurementUnit = unit;
var pdfFile = File.openDialog("Choose a PDF file");
if ((pdfFile != "") && (pdfFile != null)) {
var pdfContentBoundingBox = InputPDFContentBoundingBox(unit);
var lastPage = doc.pages.add(LocationOptions.atEnd);
TilePDF(doc, lastPage, pdfFile, pdfContentBoundingBox);
}
}
function TilePDF(doc, startPage, pdfFileName, pdfContentBoundingBox) {
var pdfFile = File(pdfFileName);
var pageCount = GetPDFPageCount(pdfFile);
var pageDims = GetPageDimensions(doc);
var bleed = doc.documentPreferences.documentBleedTopOffset;
var margin = doc.marginPreferences.top;
var contentTargetW = pageDims.w - (2 * margin);
var contentTargetH = pageDims.h - (2 * margin);
var contentXRatio = (contentTargetW / pdfContentBoundingBox.w);
var contentYRatio = (contentTargetH / pdfContentBoundingBox.h);
var scalingFactor = Math.min(contentXRatio, contentYRatio);
var scaledPdfContentMargin = {
"xOdd": pdfContentBoundingBox.xOdd * scalingFactor,
"xEven": pdfContentBoundingBox.xEven * scalingFactor,
"y": pdfContentBoundingBox.y * scalingFactor,
}
var pageSetup = {
"xLeft": -bleed,
"xMid": pageDims.w / 2,
"xRight": pageDims.w + bleed,
"yTop": -bleed,
"yBottom": pageDims.h + bleed
}
var pos = {
"left": {
"x": margin - scaledPdfContentMargin.xEven + bleed,
"y": margin - scaledPdfContentMargin.y + bleed
},
"right": {
"x": pageSetup.xMid + margin - scaledPdfContentMargin.xOdd + bleed,
"y": margin - scaledPdfContentMargin.y + bleed
},
}
app.pdfPlacePreferences.pdfCrop = PDFCrop.cropMedia;
var currentPage = startPage;
var isRecto = false;
for (var i = 0; i < (pageCount / 2); i++) {
var rects = PlaceRectangles(currentPage, pageSetup);
// page indices for the inner face of a double page -- first page left, second page right
var leftIndex = i + 1;
var rightIndex = pageCount - i;
if (!isRecto) { // outer face of a double page
// flip pages around
var tmp = leftIndex;
leftIndex = rightIndex;
rightIndex = tmp;
}
PlacePageInRectangle(rects.left, pdfFile, leftIndex, pos.left, scalingFactor);
PlacePageInRectangle(rects.right, pdfFile, rightIndex, pos.right, scalingFactor);
isRecto = !isRecto;
currentPage = doc.pages.add(LocationOptions.after, currentPage);
}
currentPage.remove();
}
function PlaceRectangles(currentPage, pageSetup) {
var leftRect = currentPage.rectangles.add({
"geometricBounds": [pageSetup.yTop, pageSetup.xLeft, pageSetup.yBottom, pageSetup.xMid],
"fillColor": "None",
"strokeColor": "None",
});
var rightRect = currentPage.rectangles.add({
"geometricBounds": [pageSetup.yTop, pageSetup.xMid, pageSetup.yBottom, pageSetup.xRight],
"fillColor": "None",
"strokeColor": "None",
});
return {
"left": leftRect,
"right": rightRect
};
}
function PlacePageInRectangle(rectangle, file, pageIndex, position, scale) {
app.pdfPlacePreferences.pageNumber = pageIndex;
var page = rectangle.place(file, {
"horizontalScale": scale * 100,
"verticalScale": scale * 100,
}
)[0];
page.move([position.x, position.y]);
}
function GetPDFPageCount(file) {
file.open("r");
next_line = file.readln();
while (next_line.indexOf("/N ") < 0) {
next_line = file.readln();
}
file.close();
var pageCount = next_line.match(/\/N (\d+)\/T/)[1];
return Number(pageCount);
}
function IsDocumentBleedUniform(doc) {
if (doc.documentPreferences.documentBleedUniformSize) {
return true;
}
var b = {
"top": doc.documentPreferences.documentBleedTopOffset,
"left": doc.documentPreferences.documentBleedInsideOrLeftOffset,
"right": doc.documentPreferences.documentBleedOutsideOrRightOffset,
"bottom": doc.documentPreferences.documentBleedBottomOffset
};
return (b.top == b.left)
&& (b.top == b.right)
&& (b.top == b.bottom);
}
function IsDocumentMarginUniform(doc) {
var m = {
"top": doc.marginPreferences.top,
"left": doc.marginPreferences.left,
"right": doc.marginPreferences.right,
"bottom": doc.marginPreferences.bottom
};
return (m.top == m.left)
&& (m.top == m.right)
&& (m.top == m.bottom);
}
function GetPageDimensions(doc) {
return {
"w": doc.documentPreferences.pageWidth,
"h": doc.documentPreferences.pageHeight,
};
}
function InputPDFContentBoundingBox(unit) {
var d = app.dialogs.add({name:"PDF layout info", canCancel:false});
var widthEdit;
var heightEdit;
var xOffsetOddEdit;
var xOffsetEvenEdit;
var yOffsetEdit;
var widthValue = ReadLabelAsNumber(app, "PDFTiling_ContentBoundingBox_Width", 0.0);
var heightValue = ReadLabelAsNumber(app, "PDFTiling_ContentBoundingBox_Height", 0.0);
var xOffsetOddValue = ReadLabelAsNumber(app, "PDFTiling_ContentBoundingBox_XOffsetOdd", 0.0);
var xOffsetEvenValue = ReadLabelAsNumber(app, "PDFTiling_ContentBoundingBox_XOffsetEven", 0.0);
var yOffsetValue = ReadLabelAsNumber(app, "PDFTiling_ContentBoundingBox_YOffset", 0.0);
with (d.dialogColumns.add()) {
with (dialogRows.add()) {
staticTexts.add({staticLabel: "Enter the coordinates for the smallest bounding box which fits all of the content in the selected PDF file.", staticAlignment: StaticAlignmentOptions.leftAlign});
}
with (dialogRows.add()) {
with (dialogColumns.add()) {
staticTexts.add({staticLabel: "Width:", minWidth: 200, staticAlignment: StaticAlignmentOptions.rightAlign});
}
with (dialogColumns.add()) {
widthEdit = measurementEditboxes.add({editValue: widthValue, editUnits: unit, minimumValue: 0, maximumValue: 1000});
}
with (dialogColumns.add()) {
staticTexts.add({staticLabel: "Height:", minWidth: 200, staticAlignment: StaticAlignmentOptions.rightAlign});
}
with (dialogColumns.add()) {
heightEdit = measurementEditboxes.add({editValue: heightValue, editUnits: unit, minimumValue: 0, maximumValue: 1000});
}
}
with (dialogRows.add()) {
with (dialogColumns.add()) {
staticTexts.add({staticLabel: "X offset (odd pages):", minWidth: 200, staticAlignment: StaticAlignmentOptions.rightAlign});
}
with (dialogColumns.add()) {
xOffsetOddEdit = measurementEditboxes.add({editValue: xOffsetOddValue, editUnits: unit, minimumValue: 0, maximumValue: 1000});
}
with (dialogColumns.add()) {
staticTexts.add({staticLabel: "Y offset:", minWidth: 200, staticAlignment: StaticAlignmentOptions.rightAlign});
}
with (dialogColumns.add()) {
yOffsetEdit = measurementEditboxes.add({editValue: yOffsetValue, editUnits: unit, minimumValue: 0, maximumValue: 1000});
}
}
with (dialogRows.add()) {
with (dialogColumns.add()) {
staticTexts.add({staticLabel: "X offset (even pages):", minWidth: 200, staticAlignment: StaticAlignmentOptions.rightAlign});
}
with (dialogColumns.add()) {
xOffsetEvenEdit = measurementEditboxes.add({editValue: xOffsetEvenValue, editUnits: unit, minimumValue: 0, maximumValue: 1000});
}
}
}
d.show();
widthValue = widthEdit.editValue;
heightValue = heightEdit.editValue;
xOffsetOddValue = xOffsetOddEdit.editValue;
xOffsetEvenValue = xOffsetEvenEdit.editValue;
yOffsetValue = yOffsetEdit.editValue;
d.destroy();
app.insertLabel("PDFTiling_ContentBoundingBox_Width", widthValue.toString());
app.insertLabel("PDFTiling_ContentBoundingBox_Height", heightValue.toString());
app.insertLabel("PDFTiling_ContentBoundingBox_XOffsetOdd", xOffsetOddValue.toString());
app.insertLabel("PDFTiling_ContentBoundingBox_XOffsetEven", xOffsetEvenValue.toString());
app.insertLabel("PDFTiling_ContentBoundingBox_YOffset", yOffsetValue.toString());
widthValue = UnitValue(widthValue + "pt").as(unit.toString());
heightValue = UnitValue(heightValue + "pt").as(unit.toString());
xOffsetOddValue = UnitValue(xOffsetOddValue + "pt").as(unit.toString());
xOffsetEvenValue = UnitValue(xOffsetEvenValue + "pt").as(unit.toString());
yOffsetValue = UnitValue(yOffsetValue + "pt").as(unit.toString());
return {
"w": widthValue,
"h": heightValue,
"xOdd": xOffsetOddValue,
"xEven": xOffsetEvenValue,
"y": yOffsetValue,
}
}
function ReadLabelAsNumber(object, labelName, defaultValue) {
var value = object.extractLabel(labelName);
if (value == null || value == "") {
value = defaultValue;
} else {
value = Number(value);
}
return value;
}
function ShowScriptInfo() {
var infoDialog = app.dialogs.add({name: "About this script"});
var textBits = [
"This script will:",
"- let you select a PDF file",
"- ask for information about the content layout in the selected PDF",
"- scale the PDF pages to fit the content into your document margins",
"- place 2 PDF pages per single document page, one per each horizontal half",
"",
"The PDF pages are tiled in such a way that you can duplex-print the result and then fold all of it in half to get a booklet.",
"",
"Details:",
"- non-uniform bleed is not supported",
"- non-uniform margins are not supported",
"- your document margin settings will be also be used to create a double margin between two tiled pages",
"- pages are inserted after the last page of your document",
"",
"Click OK to continue, or Cancel to abort"
];
with (infoDialog.dialogColumns.add()) {
for (var i = 0; i < textBits.length; i++) {
with (dialogRows.add()) {
staticTexts.add({staticLabel: textBits[i], staticAlignment: StaticAlignmentOptions.leftAlign});
}
}
with (dialogRows.add()) {
dontShowAgainCheckbox = checkboxControls.add({staticLabel: "Do not show this dialog again"});
}
}
var clickedOk = infoDialog.show();
app.insertLabel("PDFTiling_ShowInfoDialog", dontShowAgainCheckbox.checkedState ? "false" : "true");
infoDialog.destroy();
return clickedOk;
}
function ShowErrorAndExit(text) {
var errorDialog = app.dialogs.add({name: "An error occurred", canCancel: false});
with (errorDialog.dialogColumns.add()) {
with (dialogRows.add()) {
staticTexts.add({staticLabel: text});
}
}
errorDialog.show();
errorDialog.destroy();
exit();
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment