diff --git a/package.json b/package.json index 408557a..aaf7140 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bookbinder", - "version": "1.3.4", + "version": "1.3.5", "description": "An app to rearrange PDF pages for printing for bookbinding", "type": "module", "scripts": { diff --git a/src/book.js b/src/book.js index f9c5d96..417dfe1 100644 --- a/src/book.js +++ b/src/book.js @@ -2,7 +2,7 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. -import { PDFDocument, degrees } from 'pdf-lib'; +import { PDFDocument, PDFEmbeddedPage, degrees } from 'pdf-lib'; import { saveAs } from 'file-saver'; import { Signatures } from './signatures.js'; import { WackyImposition } from './wacky_imposition.js'; @@ -11,6 +11,7 @@ import { updatePageLayoutInfo } from './utils/renderUtils.js'; import JSZip from 'jszip'; import { loadConfiguration } from './utils/formUtils.js'; import { drawFoldlines, drawCropmarks, drawSpineMarks } from './utils/drawing.js'; +import { calculateDimensions, calculateLayout } from './utils/layout.js'; // Some JSDoc typedefs we use multiple places /** @@ -29,6 +30,9 @@ import { drawFoldlines, drawCropmarks, drawSpineMarks } from './utils/drawing.js * @property {number} sy - y scale factor (where 1.0 is 100%) * @property {number} x - x position * @property {number} y - y position + * @property {number[]} [spineMarkTop]: spineMarkTop, + * @property {number[]} [spineMarkBottom]: spineMarkBottom, + * @property {boolean} [isLeftPage]: isLeftPage, */ export class Book { @@ -110,7 +114,6 @@ export class Book { this.input = await file.arrayBuffer(); //fs.readFileSync(filepath); this.currentdoc = await PDFDocument.load(this.input); //TODO: handle pw-protected PDFs - /** @type {number} */ const pages = this.currentdoc.getPages(); this.cropbox = null; @@ -242,7 +245,6 @@ export class Book { case 'customsig': this.book = new Signatures( this.orderedpages, - this.duplex, this.sigsize, this.per_sheet, this.duplexrotate @@ -272,16 +274,17 @@ export class Book { } console.log('Created pages for : ', this.book); - const dim = this.calculate_dimensions(); + const dimensions = calculateDimensions(this); + const positions = calculateLayout(this); updatePageLayoutInfo({ - dimensions: dim, + dimensions, book: this.book, perSheet: this.per_sheet, papersize: this.papersize, cropbox: this.cropbox, managedDoc: this.managedDoc, - positions: this.calculatelayout(), + positions, }); } @@ -299,7 +302,11 @@ export class Book { // create a directory named after the input pdf and fill it with // the signatures this.zip = new JSZip(); - var origFileName = this.inputpdf.replace(/\s|,|\.pdf/, ''); + var origFileName = this.inputpdf; + origFileName = origFileName + .replace(/[-\s,_]+/g, '_') + .replace(/_*\.pdf/g, '') + .toLowerCase(); this.filename = origFileName; if ( @@ -308,23 +315,79 @@ export class Book { this.format == 'standardsig' || this.format == 'customsig' ) { - // const generateAggregate = this.print_file != 'signatures'; - // const generateSignatures = this.print_file != 'aggregated'; + const generateAggregate = this.print_file != 'signatures'; + const generateSignatures = this.print_file != 'aggregated'; + let aggregatePdf0, aggregatePdf1; + if (generateAggregate) { + aggregatePdf0 = await PDFDocument.create(); + aggregatePdf1 = this.duplex ? null : await PDFDocument.create(); + } const forLoop = async () => { + for (let i = 0; i < this.rearrangedpages.length; i++) { const signature = this.rearrangedpages[i]; - await this.createsignatures({ + console.log(signature); + const sigName = `${this.filename}_signature${i}`; + const [sigFront, sigBack] = await this.createSignatures({ pageIndexDetails: signature, - index: i, - isDuplex: this.duplex, fileList: this.filelist, }); + + if (this.duplex) { + // collate + const sig = this.collatePages(sigFront, sigBack) + + if (generateSignatures) { + await sig.save().then((pdfBytes) => { + this.zip.file(`signatures/${sigName}_duplex`, pdfBytes); + }); + } + + if (generateAggregate) { + const copiedPages = await aggregatePdf0.embedPages(sig, sig.getPageIndices()); + copiedPages.forEach((page) => aggregatePdf0.addPage(page)); + } + + } else { + if (generateSignatures) { + await sigFront.save().then((pdfBytes) => { + this.zip.file(`signatures/${sigName}_side1`, pdfBytes); + }); + await sigBack.save().then((pdfBytes) => { + this.zip.file(`signatures/${sigName}_side2`, pdfBytes); + }); + } + + if (generateAggregate) { + const copiedPagesFront = await aggregatePdf0.embedPages(sigFront, sigFront.getPageIndices()); + copiedPagesFront.forEach((page) => aggregatePdf0.addPage(page)); + + const copiedPagesBack = await aggregatePdf1.embedPages(sigBack, sigBack.getPageIndices()); + copiedPagesBack.forEach((page) => aggregatePdf1.addPage(page)); + } + + } + } }; await forLoop(); + if (aggregatePdf1 != null) { + await aggregatePdf1.save().then((pdfBytes) => { + if (!isPreview) this.zip.file(`${this.filename}_typeset_side2.pdf`, pdfBytes); + }); + } + if (aggregatePdf0 != null) { + await aggregatePdf0.save().then((pdfBytes) => { + if (!isPreview) + this.zip.file( + this.duplex ? `${this.filename}_typeset.pdf` : `${this.filename}_typeset_side1.pdf`, + pdfBytes + ); + }); + } var rotationMetaInfo = - (this.paper_rotation_90 ? '_paperRotated' : '') + + (this.paper_rotation_90 ? 'paper_rotated' : '') + (this.source_rotation == 'none' ? '' : `_${this.source_rotation}`); this.filename = `${origFileName}${rotationMetaInfo}`; } else if (this.format == 'a9_3_3_4') { @@ -368,10 +431,10 @@ export class Book { /** * Generates a new PDF & embeds the prescribed pages of the source PDF into it * @param sourcePdf - * @param {number[]} pageNumbers - an array of page numbers. Ex: [1,5,6,7,8,'b',10] or null to embed all pages from source + * @param {(string|number)[]} [pageNumbers] - an array of page numbers. Ex: [1,5,6,7,8,'b',10] or null to embed all pages from source * NOTE: re-construction behavior kicks in if there's 'b's in the list * - * @return [newPdf with pages embedded, embedded page array] + * @return {Promise<[PDFDocument, PDFEmbeddedPage[]]>} PDF with pages embedded, embedded page array */ async embedPagesInNewPdf(sourcePdf, pageNumbers) { const newPdf = await PDFDocument.create(); @@ -397,6 +460,11 @@ export class Book { return [newPdf, embeddedPages]; } + async addPdf(destPdf, sourcePdf) { + await mergedPdf.copyPages(pdfA, pdfA.getPageIndices()); + copiedPagesA.forEach((page) => mergedPdf.addPage(page)) + } + /** * Part of the Classic (non-Wacky) flow. Called by [createsignatures]. * (conditionally) populates the destPdf and (conditionally) generates the outname PDF @@ -430,9 +498,9 @@ export class Book { const offset = this.per_sheet / 2; let block_end = offset; - const alt_folio = this.per_sheet == 4 && back; + // const alt_folio = this.per_sheet == 4 && back; - const positions = this.calculatelayout(alt_folio); + const positions = calculateLayout(this); let side2flag = back; @@ -455,9 +523,8 @@ export class Book { block_start += offset; block_end += offset; } - await outPDF.save().then((pdfBytes) => { - this.zip.file(config.outname, pdfBytes); - }); + + return outPDF; } /** * @@ -465,7 +532,7 @@ export class Book { * @param {string|null} config.outname : name of pdf added to ongoing zip file. Ex: 'signature1duplex.pdf' (or null if no signature file needed) * @param {PageInfo[]} config.sigDetails : objects that contain 3 values: { isSigStart: boolean, isSigEnd: boolean, info: either the page number or 'b'} * @param {boolean} config.side2flag : is 'back' of page (boolean) - * @param {number[]} config.papersize : paper size (as [number, number]) + * @param {[number, number]} config.papersize : paper size (as [number, number]) * @param {number} config.block_start: Starting page index * @param {number} config.block_end: Ending page index * @param {boolean} config.alt : alternate pages (boolean) @@ -473,8 +540,8 @@ export class Book { * @param {boolean} config.cropmarks: whether to print cropmarks * @param {boolean} config.pdfEdgeMarks: whether to print PDF edge marks * @param {Position[]} config.positions: list of page positions - * @param config.outPDF : PDF to write to, in addition to PDF created w/ `outname` (or null) - * @param config.embeddedPages : pages already embedded in the `destPdf` to assemble in addition (or null) + * @param {PDFDocument} [config.outPDF]: PDF to write to, in addition to PDF created w/ `outname` (or null) + * @param {(PDFEmbeddedPage|string)[]} [config.embeddedPages] : pages already embedded in the `destPdf` to assemble in addition (or null) */ draw_block_onto_page(config) { @@ -501,7 +568,7 @@ export class Book { block.forEach((page, i) => { if (page == 'b' || page === undefined) { // blank page, move on. - } else { + } else if (page instanceof PDFEmbeddedPage) { const { y, x, sx, sy, rotation } = positions[i]; currPage.drawPage(page, { y, @@ -510,6 +577,8 @@ export class Book { yScale: sy, rotate: degrees(rotation), }); + } else { + console.error('Unexpected type for page: ', page); } if (pdfEdgeMarks && (sigDetails[i].isSigStart || sigDetails[i].isSigEnd)) { @@ -527,232 +596,28 @@ export class Book { return side2flag; } - /** - * Looks at [this.cropbox] and [this.padding_pt] and [this.papersize] and [this.page_layout] and [this.page_scaling] - * in order to calculate the information needed to render a PDF page within a layout cell. It provides several functions - * in the return object that calculate the positioning and scaling needed when provided the rotation information. - * - * When calculating 'x' and 'y' values, those are relative to a laid out PDF page, not necessarily paper sheet x & y - * - * @return the object: { - * layoutCell: 2 dimensional array of the largest possible space the PDF page could take within the layout (and not overflow) - * rawPdfSize: 2 dimensional array of dimensions for the PDF (pre scaled) - * pdfSize: 2 dimensional array of dimensions for the PDF page + margins (pre scaled) - * pdfScale: 2 dimensional array of scaling factors for the raw PDF so it fits in layoutCell (w/ margins) - * padding: object containing the already scaled padding. Keys are: fore_edge, binding, top, bottom - * xForeEdgeShiftFunc: requires the page rotation, in degrees. In pts, already scaled. - * xBindingShiftFunc: requires the page rotation, in degrees. In pts, already scaled. - * xPdfWidthFunc: requires the page rotation, in degrees. In pts, already scaled. - * yPdfHeightFunc: requires the page rotation, in degrees. In pts, already scaled. - * yTopShiftFunc: requires the page rotation, in degrees. In pts, already scaled. - * yBottomShiftFunc: requires the page rotation, in degrees. In pts, already scaled. - * } - */ - calculate_dimensions() { - const onlyPos = function (v) { - return v > 0 ? v : 0; - }; - // const onlyNeg = function (v) { - // return v < 0 ? v : 0; - // }; - // PDF + margins (positive) - const pagex = - this.cropbox.width + onlyPos(this.padding_pt.binding) + onlyPos(this.padding_pt.fore_edge); - const pagey = - this.cropbox.height + onlyPos(this.padding_pt.top) + onlyPos(this.padding_pt.bottom); - - const layout = this.page_layout; - - // Calculate the size of each page box on the sheet - let finalx = this.papersize[0] / layout.cols; - let finaly = this.papersize[1] / layout.rows; - - // if pages are rotated a quarter-turn in this layout, we need to swap the width and height measurements - if (layout.landscape) { - const temp = finalx; - finalx = finaly; - finaly = temp; - } - - let sx = 1; - let sy = 1; - - // The page_scaling options are: 'lockratio', 'stretch', 'centered' - if (this.page_scaling == 'lockratio') { - const scale = Math.min(finalx / pagex, finaly / pagey); - sx = scale; - sy = scale; - } else if (this.page_scaling == 'stretch') { - sx = finalx / pagex; - sy = finaly / pagey; - } // else = centered retains 1 x 1 - - const padding = { - fore_edge: this.padding_pt.fore_edge * sx, - binding: this.padding_pt.binding * sx, - bottom: this.padding_pt.bottom * sy, - top: this.padding_pt.top * sy, - }; - - // page_positioning has 2 options: centered, binding_alinged - const positioning = this.page_positioning; - - const xForeEdgeShiftFunc = function () { - // amount to inset by, relative to fore edge, on left side of book - const xgap = finalx - pagex * sx; - return padding.fore_edge + (positioning == 'centered' ? xgap / 2 : xgap); - }; - const xBindingShiftFunc = function () { - // amount to inset by, relative to binding, on right side of book - const xgap = finalx - pagex * sx; - return padding.binding + (positioning == 'centered' ? xgap / 2 : 0); - }; - const yTopShiftFunc = function () { - const ygap = finaly - pagey * sy; - return padding.top + ygap / 2; - }; - const yBottomShiftFunc = function () { - const ygap = finaly - pagey * sy; - return padding.bottom + ygap / 2; - }; - const xPdfWidthFunc = function () { - return pagex * sx - padding.fore_edge - padding.binding; - }; - const yPdfHeightFunc = function () { - return pagey * sy - padding.top - padding.bottom; - }; - return { - layout: layout, - rawPdfSize: [this.cropbox.width, this.cropbox.height], - pdfScale: [sx, sy], - pdfSize: [pagex, pagey], - layoutCell: [finalx, finaly], - padding: padding, - - xForeEdgeShiftFunc: xForeEdgeShiftFunc, - xBindingShiftFunc: xBindingShiftFunc, - xPdfWidthFunc: xPdfWidthFunc, - yPdfHeightFunc: yPdfHeightFunc, - yTopShiftFunc: yTopShiftFunc, - yBottomShiftFunc: yBottomShiftFunc, - - positioning: positioning, - }; - } - - /** - * When considering page size, don't forget to take into account - * this.padding_pt's ['top','bottom','binding','fore_edge'] values - * - * @return {Position[]} - */ - calculatelayout() { - // vampire - const l = this.calculate_dimensions(); - const cellWidth = l.layoutCell[0]; - const cellHeight = l.layoutCell[1]; - const positions = []; - - l.layout.rotations.forEach((row, i) => { - row.forEach((col, j) => { - const xForeEdgeShift = l.xForeEdgeShiftFunc(); - const xBindingShift = l.xBindingShiftFunc(); - const yTopShift = l.yTopShiftFunc(); - const yBottomShift = l.yBottomShiftFunc(); - - let isLeftPage = j % 2 == 0; //page on 'left' side of open book - let x = j * cellWidth + (isLeftPage ? xForeEdgeShift : xBindingShift); - let y = i * cellHeight + yBottomShift; - let spineMarkTop = [j * cellWidth, (i + 1) * cellHeight - yTopShift]; - let spineMarkBottom = [(j + 1) * cellWidth, i * cellHeight + yBottomShift]; - - if (col == -180) { - // upside-down page - isLeftPage = j % 2 == 1; //page on 'left' (right side on screen) - y = (i + 1) * cellHeight - yBottomShift; - x = (j + 1) * cellWidth - (isLeftPage ? xForeEdgeShift : xBindingShift); - spineMarkTop = [(j + 1) * cellWidth, (i + 1) * cellHeight]; - spineMarkBottom = [(j + 1) * cellWidth, i * cellHeight]; - } else if (col == 90) { - // 'top' of page is on left, right side of screen - isLeftPage = i % 2 == 0; // page is on 'left' (top side of screen) - x = (1 + j) * cellHeight - yBottomShift; - y = i * cellWidth + (isLeftPage ? xBindingShift : xForeEdgeShift); - spineMarkTop = [(1 + j) * cellHeight, i * cellWidth]; - spineMarkBottom = [j * cellHeight, i * cellWidth]; - } else if (col == -90) { - // 'top' of page is on the right, left sight of screen - isLeftPage = i % 2 == 1; // page is on 'left' (bottom side of screen) - x = j * cellHeight + yBottomShift; - y = (1 + i) * cellWidth - (isLeftPage ? xForeEdgeShift : xBindingShift); - spineMarkTop = [(j + 1) * cellHeight - yTopShift, (isLeftPage ? i : i + 1) * cellWidth]; - spineMarkBottom = [j * cellHeight + yBottomShift, (isLeftPage ? i : i + 1) * cellWidth]; - } - - console.log( - `>> (${i},${j})[${col}] : [${x},${y}] :: [xForeEdgeShift: ${xForeEdgeShift}][xBindingShift: ${xBindingShift}]` - ); - positions.push({ - rotation: col, - sx: l.pdfScale[0], - sy: l.pdfScale[1], - x: x, - y: y, - spineMarkTop: spineMarkTop, - spineMarkBottom: spineMarkBottom, - isLeftPage: isLeftPage, - }); - }); - }); - console.log('And in the end of it all, (calculatelayout) we get: ', positions); - return positions; - } - /** * PDF builder base function for Classic (non-Wacky) layouts. Called by [createoutputfiles] * * @param {Object} config * @param {PageInfo[][]|PageInfo[]} config.pageIndexDetails : a nested list of objects. - * @param config.aggregatePdfs : list of destination PDF(s_ for aggregated content ( [0] for duplex & front, [1] for backs -- value is null if no aggregate printing enabled) * @param config.embeddedPages : list of lists of embedded pages from source document ( [0] for duplex & front, [1] for backs -- value is null if no aggregate printing enabled) - * @param {id} config.id : string dentifier for signature file name (null if no signature files to be generated) - * @param {boolean} config.isDuplex : boolean + * @param {string} config.id : string dentifier for signature file name (null if no signature files to be generated) * @param {string[]} config.fileList : list of filenames for sig filename to be added to (modifies list) */ - async createsignatures(config) { + async createSignatures(config) { const pages = config.pageIndexDetails; - // duplex printers print both sides of the sheet, - if (config.isDuplex) { - const outduplex = `signature${config.index}duplex.pdf`; - await this.writepages({ - outname: outduplex, - pageList: pages[0], - back: false, - alt: true, - }); - config.fileList[config.index] = outduplex; - } else { - // for non-duplex printers we have two files, print the first, flip - // the sheets over, then print the second - const outname1 = `signature${config.index}side1.pdf`; - const outname2 = `signature${config.index}side2.pdf`; - - await this.writepages({ - outname: outname1, + const pdfFront = await this.writepages({ pageList: pages[0], back: false, alt: false, }); - await this.writepages({ - outname: outname2, + const pdfBack = await this.writepages({ pageList: pages[1], back: true, alt: false, }); - config.fileList[config.index * 2] = outname1; - config.fileList[config.index * 2 + 1] = outname2; - } - console.log('After creating signatures, our filelist looks like: ', this.filelist); + return [pdfFront, pdfBack]; } bundleSettings() { @@ -769,7 +634,7 @@ export class Book { this.bundleSettings(); return this.zip.generateAsync({ type: 'blob' }).then((blob) => { console.log(' calling saveAs on ', this.filename); - saveAs(blob, `${this.filename}.zip`); + saveAs(blob, `${this.filename}_bookbinder.zip`); }); } @@ -783,7 +648,6 @@ export class Book { /** * @callback LineMaker - * @param {number} x - ... */ /** diff --git a/src/signatures.js b/src/signatures.js index 8190def..4a35528 100644 --- a/src/signatures.js +++ b/src/signatures.js @@ -9,14 +9,13 @@ export class Signatures { /** * Create a signature. * @param {number[]} pages - List of pages in a book. - * @param {boolean} duplex - Whether both front and back sides go in the same file or not. * @param {number} per_sheet - number of pages per sheet (front and back combined) * @param {boolean} duplexrotate - whether to rotate alternating sheets or not. */ - constructor(pages, duplex, sigsize, per_sheet, duplexrotate) { + constructor(pages, sigsize, per_sheet, duplexrotate) { this.sigsize = sigsize; - this.duplex = duplex; + this.duplex = false; this.inputpagelist = pages; this.per_sheet = per_sheet || 4; // pages per sheet - default is 4. this.duplexrotate = duplexrotate || false; diff --git a/src/utils/drawing.js b/src/utils/drawing.js index c2f2670..59af030 100644 --- a/src/utils/drawing.js +++ b/src/utils/drawing.js @@ -113,9 +113,9 @@ export function drawCropmarks(papersize, per_sheet) { } /** - * @param {PageInfo} sigDetails - page info object - * @param {Position} position - position info object - * @returns {Line[]} + * @param {import("../book.js").PageInfo} sigDetails - page info object + * @param {import("../book.js").Position} position - position info object + * @returns {Line} */ export function drawSpineMarks(sigDetails, position) { const w = 5; diff --git a/src/utils/layout.js b/src/utils/layout.js new file mode 100644 index 0000000..a040371 --- /dev/null +++ b/src/utils/layout.js @@ -0,0 +1,178 @@ +/** + * When considering page size, don't forget to take into account + * this.padding_pt's ['top','bottom','binding','fore_edge'] values + * + * @return {import("../book.js").Position[]} + */ +export function calculateLayout(book) { + const l = calculateDimensions(book); + const { + layoutCell, + xForeEdgeShiftFunc, + xBindingShiftFunc, + yTopShiftFunc, + yBottomShiftFunc, + pdfScale, + } = l; + const [cellWidth, cellHeight] = layoutCell; + const positions = []; + + l.layout.rotations.forEach((row, i) => { + row.forEach((col, j) => { + const xForeEdgeShift = xForeEdgeShiftFunc(); + const xBindingShift = xBindingShiftFunc(); + const yTopShift = yTopShiftFunc(); + const yBottomShift = yBottomShiftFunc(); + + let isLeftPage = j % 2 == 0; //page on 'left' side of open book + let x = j * cellWidth + (isLeftPage ? xForeEdgeShift : xBindingShift); + let y = i * cellHeight + yBottomShift; + let spineMarkTop = [j * cellWidth, (i + 1) * cellHeight - yTopShift]; + let spineMarkBottom = [(j + 1) * cellWidth, i * cellHeight + yBottomShift]; + + if (col == -180) { + // upside-down page + isLeftPage = j % 2 == 1; //page on 'left' (right side on screen) + y = (i + 1) * cellHeight - yBottomShift; + x = (j + 1) * cellWidth - (isLeftPage ? xForeEdgeShift : xBindingShift); + spineMarkTop = [(j + 1) * cellWidth, (i + 1) * cellHeight]; + spineMarkBottom = [(j + 1) * cellWidth, i * cellHeight]; + } else if (col == 90) { + // 'top' of page is on left, right side of screen + isLeftPage = i % 2 == 0; // page is on 'left' (top side of screen) + x = (1 + j) * cellHeight - yBottomShift; + y = i * cellWidth + (isLeftPage ? xBindingShift : xForeEdgeShift); + spineMarkTop = [(1 + j) * cellHeight, i * cellWidth]; + spineMarkBottom = [j * cellHeight, i * cellWidth]; + } else if (col == -90) { + // 'top' of page is on the right, left sight of screen + isLeftPage = i % 2 == 1; // page is on 'left' (bottom side of screen) + x = j * cellHeight + yBottomShift; + y = (1 + i) * cellWidth - (isLeftPage ? xForeEdgeShift : xBindingShift); + spineMarkTop = [(j + 1) * cellHeight - yTopShift, (isLeftPage ? i : i + 1) * cellWidth]; + spineMarkBottom = [j * cellHeight + yBottomShift, (isLeftPage ? i : i + 1) * cellWidth]; + } + + console.log( + `>> (${i},${j})[${col}] : [${x},${y}] :: [xForeEdgeShift: ${xForeEdgeShift}][xBindingShift: ${xBindingShift}]` + ); + positions.push({ + rotation: col, + sx: pdfScale[0], + sy: pdfScale[1], + x: x, + y: y, + spineMarkTop: spineMarkTop, + spineMarkBottom: spineMarkBottom, + isLeftPage: isLeftPage, + }); + }); + }); + console.log('And in the end of it all, (calculatelayout) we get: ', positions); + return positions; +} + +/** + * Looks at [this.cropbox] and [this.padding_pt] and [this.papersize] and [this.page_layout] and [this.page_scaling] + * in order to calculate the information needed to render a PDF page within a layout cell. It provides several functions + * in the return object that calculate the positioning and scaling needed when provided the rotation information. + * + * When calculating 'x' and 'y' values, those are relative to a laid out PDF page, not necessarily paper sheet x & y + * + * @return the object: { + * layoutCell: 2 dimensional array of the largest possible space the PDF page could take within the layout (and not overflow) + * rawPdfSize: 2 dimensional array of dimensions for the PDF (pre scaled) + * pdfSize: 2 dimensional array of dimensions for the PDF page + margins (pre scaled) + * pdfScale: 2 dimensional array of scaling factors for the raw PDF so it fits in layoutCell (w/ margins) + * padding: object containing the already scaled padding. Keys are: fore_edge, binding, top, bottom + * xForeEdgeShiftFunc: requires the page rotation, in degrees. In pts, already scaled. + * xBindingShiftFunc: requires the page rotation, in degrees. In pts, already scaled. + * xPdfWidthFunc: requires the page rotation, in degrees. In pts, already scaled. + * yPdfHeightFunc: requires the page rotation, in degrees. In pts, already scaled. + * yTopShiftFunc: requires the page rotation, in degrees. In pts, already scaled. + * yBottomShiftFunc: requires the page rotation, in degrees. In pts, already scaled. + * } + */ +export function calculateDimensions(book) { + const { cropbox, padding_pt, papersize, page_layout, page_positioning, page_scaling } = book; + + const { width, height } = cropbox; + const pageX = width + Math.max(padding_pt.binding, 0) + Math.max(padding_pt.fore_edge, 0); + const pageY = height + Math.max(padding_pt.top, 0) + Math.max(padding_pt.bottom, 0); + + // Calculate the size of each page box on the sheet + let finalX = papersize[0] / page_layout.cols; + let finalY = papersize[1] / page_layout.rows; + + // if pages are rotated a quarter-turn in this layout, we need to swap the width and height measurements + if (page_layout.landscape) { + const temp = finalX; + finalX = finalY; + finalY = temp; + } + + let sx = 1; + let sy = 1; + + // The page_scaling options are: 'lockratio', 'stretch', 'centered' + if (page_scaling == 'lockratio') { + const scale = Math.min(finalX / pageX, finalY / pageY); + sx = scale; + sy = scale; + } else if (page_scaling == 'stretch') { + sx = finalX / pageX; + sy = finalY / pageY; + } // else = centered retains 1 x 1 + + const padding = { + fore_edge: padding_pt.fore_edge * sx, + binding: padding_pt.binding * sx, + bottom: padding_pt.bottom * sy, + top: padding_pt.top * sy, + }; + + // page_positioning has 2 options: centered, binding_alinged + const positioning = page_positioning; + + const xForeEdgeShiftFunc = function () { + // amount to inset by, relative to fore edge, on left side of book + const xgap = finalX - pageX * sx; + return padding.fore_edge + (positioning == 'centered' ? xgap / 2 : xgap); + }; + const xBindingShiftFunc = function () { + // amount to inset by, relative to binding, on right side of book + const xgap = finalX - pageX * sx; + return padding.binding + (positioning == 'centered' ? xgap / 2 : 0); + }; + const yTopShiftFunc = function () { + const ygap = finalY - pageY * sy; + return padding.top + ygap / 2; + }; + const yBottomShiftFunc = function () { + const ygap = finalY - pageY * sy; + return padding.bottom + ygap / 2; + }; + const xPdfWidthFunc = function () { + return pageX * sx - padding.fore_edge - padding.binding; + }; + const yPdfHeightFunc = function () { + return pageY * sy - padding.top - padding.bottom; + }; + return { + layout: page_layout, + rawPdfSize: [width, height], + pdfScale: [sx, sy], + pdfSize: [pageX, pageY], + layoutCell: [finalX, finalY], + padding: padding, + + xForeEdgeShiftFunc: xForeEdgeShiftFunc, + xBindingShiftFunc: xBindingShiftFunc, + xPdfWidthFunc: xPdfWidthFunc, + yPdfHeightFunc: yPdfHeightFunc, + yTopShiftFunc: yTopShiftFunc, + yBottomShiftFunc: yBottomShiftFunc, + + positioning: positioning, + }; +}