Created
January 10, 2024 11:49
-
-
Save deqyra/6a52332a6523bc279b8eb5d01231e4b8 to your computer and use it in GitHub Desktop.
InDesign script: Tile PDF pages for booklet printing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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