diff --git a/index.html b/index.html index 250b012..f570ac6 100644 --- a/index.html +++ b/index.html @@ -33,6 +33,7 @@

Bookbinder JS

+ diff --git a/package.json b/package.json index 68f9189..4529599 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bookbinder", - "version": "1.5.0", + "version": "1.6.0", "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 5e5f2b6..27589dc 100644 --- a/src/book.js +++ b/src/book.js @@ -10,7 +10,13 @@ import { PAGE_LAYOUTS, PAGE_SIZES } from './constants.js'; import { updatePageLayoutInfo } from './utils/renderUtils.js'; import JSZip from 'jszip'; import { loadConfiguration } from './utils/formUtils.js'; -import { drawFoldlines, drawCropmarks, drawSpineMarks, drawSewingMarks } from './utils/drawing.js'; +import { + drawFoldlines, + drawCropmarks, + drawSpineMark, + drawSigOrderMark, + drawSewingMarks, +} from './utils/drawing.js'; import { calculateDimensions, calculateLayout } from './utils/layout.js'; import { interleavePages, embedPagesInNewPdf } from './utils/pdf.js'; @@ -21,6 +27,8 @@ import { interleavePages, embedPagesInNewPdf } from './utils/pdf.js'; * @property {string|number} info - page # or 'b' * @property {boolean} isSigStart * @property {boolean} isSigEnd + * @property {boolean} isSigMiddle + * @property {number} signatureNum - which signature is this page in. 0 based */ /** @@ -90,12 +98,14 @@ export class Book { this.flyleafs = configuration.flyleafs; this.cropmarks = configuration.cropMarks; this.sewingMarks = { + sewingMarkLocation: configuration.sewingMarkLocation, isEnabled: configuration.sewingMarksEnabled, amount: configuration.sewingMarksAmount, marginPt: configuration.sewingMarksMarginPt, tapeWidthPt: configuration.sewingMarksTapeWidthPt, }; this.pdfEdgeMarks = configuration.pdfEdgeMarks; + this.sigOrderMarks = configuration.sigOrderMarks; this.cutmarks = configuration.cutMarks; this.format = configuration.sigFormat; if (configuration.sigFormat === 'standardsig') { @@ -229,13 +239,6 @@ export class Book { this.cropbox = newPage.getCropBox(); } - console.log( - 'The updatedDoc doc has : ', - this.managedDoc.getPages(), - ' vs --- ', - this.managedDoc.getPageCount() - ); - switch (this.format) { case 'perfect': case 'booklet': @@ -303,6 +306,8 @@ export class Book { * generate preview content AND a downloadable zip */ async createoutputfiles(isPreview) { + // set this to `true` to enable full-book previews (placeholder till it's in the UI) + const fullPreviewDevHack = false; const previewFrame = document.getElementById('pdf'); let previewPdf = null; @@ -323,14 +328,16 @@ export class Book { this.format == 'customsig' ) { // Only generate the first signature for preview - const pagesArr = isPreview ? this.rearrangedpages.slice(0, 1) : this.rearrangedpages; + const pagesArr = + isPreview && !fullPreviewDevHack ? this.rearrangedpages.slice(0, 1) : this.rearrangedpages; const signatures = [{}]; const makeSignatures = async () => { const tasks = pagesArr.map(async (pages, i) => { console.log(pages); signatures[i] = { name: `${this.filename}_signature${i}` }; - [signatures[i].front, signatures[i].back] = await this.createSignatures({ + [signatures[i].front, signatures[i].back] = await this.createSignature({ pageIndexDetails: pages, + maxSigCount: pagesArr.length, }); }); await Promise.all(tasks); @@ -350,7 +357,7 @@ export class Book { previewPdf = signatures[0].duplex; } - if (this.print_file != 'aggregated' && !isPreview) { + if (this.print_file != 'aggregated' && (!isPreview || fullPreviewDevHack)) { const saveSignatures = async () => { const tasks = signatures.map(async (sig) => { await sig.front?.save().then((pdfBytes) => { @@ -368,7 +375,7 @@ export class Book { await saveSignatures(); } - if (this.print_file != 'signatures' && !isPreview) { + if (this.print_file != 'signatures' && (!isPreview || fullPreviewDevHack)) { const saveAggregate = async () => { const aggregate = { front: !this.duplex ? await PDFDocument.create() : null, @@ -414,7 +421,9 @@ export class Book { this.zip.file(`${this.filename}_typeset.pdf`, pdfBytes); }); } + previewPdf = aggregate.duplex; }; + await saveAggregate(); } @@ -461,18 +470,20 @@ export class Book { } /** - * Part of the Classic (non-Wacky) flow. Called by [createsignatures]. + * Part of the Classic (non-Wacky) flow. Called by [createsignature]. * (conditionally) populates the destPdf and (conditionally) generates the outname PDF * * @param {Object} config - object /w the following parameters: - * @param {PageInfo[]} config.pageList : objects that contain 3 values: { isSigStart: boolean, isSigEnd: boolean, info: either the page number or 'b'} + * @param {PageInfo[]} config.pageList : see documentation at top of file * @param {boolean} config.back : is 'back' of page (boolean) * @param {boolean} config.alt : alternate pages (boolean) + * @param {number} config.maxSigCount * @return reference to the new PDF created */ async writepages(config) { const pagelist = config.pageList; const back = config.back; + const maxSigCount = config.maxSigCount; const filteredList = []; const blankIndices = []; pagelist.forEach((pageInfo, i) => { @@ -482,6 +493,7 @@ export class Book { blankIndices.push(i); } }); + const [outPDF, embeddedPages] = await embedPagesInNewPdf(this.managedDoc, filteredList); blankIndices.forEach((i) => embeddedPages.splice(i, 0, 'b')); @@ -497,7 +509,7 @@ export class Book { let side2flag = back; while (block_end <= pagelist.length) { - const sigDetails = config.pageList.slice(block_start, block_end); + const sigDetails = pagelist.slice(block_start, block_end); side2flag = this.draw_block_onto_page({ outPDF: outPDF, embeddedPages: embeddedPages, @@ -507,10 +519,12 @@ export class Book { papersize: this.papersize, positions: positions, cropmarks: this.cropmarks, + sigOrderMarks: this.sigOrderMarks, pdfEdgeMarks: this.pdfEdgeMarks, cutmarks: this.cutmarks, alt: config.alt, side2flag: side2flag, + maxSigCount: maxSigCount, sewingMarks: this.sewingMarks, }); block_start += offset; @@ -523,7 +537,8 @@ export class Book { * * @param {Object} config - object /w the following parameters: * @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 {PageInfo[]} config.sigDetails : see documentation at top of file + * @param {number} config.maxSigCount: Total number of signatures * @param {boolean} config.side2flag : is 'back' of page (boolean) * @param {[number, number]} config.papersize : paper size (as [number, number]) * @param {number} config.block_start: Starting page index @@ -546,9 +561,11 @@ export class Book { const outPDF = config.outPDF; const positions = config.positions; const foldmarks = config.cropmarks; + const sigOrderMarks = config.sigOrderMarks; const pdfEdgeMarks = config.pdfEdgeMarks; const cutmarks = config.cutmarks; const alt = config.alt; + const maxSigCount = config.maxSigCount; let side2flag = config.side2flag; const sewingMarks = config.sewingMarks; @@ -559,6 +576,7 @@ export class Book { ? drawFoldlines(side2flag, this.duplexrotate, papersize, this.per_sheet) : []; const drawLines = [...cropLines, ...foldLines]; + const drawRects = []; const drawPoints = []; block.forEach((page, i) => { @@ -577,13 +595,23 @@ export class Book { console.error('Unexpected type for page: ', page); } - if (pdfEdgeMarks && (sigDetails[i].isSigStart || sigDetails[i].isSigEnd)) { - drawLines.push(drawSpineMarks(sigDetails[i], positions[i])); + if (sigDetails[i].isSigStart) { + if (pdfEdgeMarks) { + drawLines.push(drawSpineMark(true, positions[i], 5)); + } + if (sigOrderMarks) { + drawRects.push(drawSigOrderMark(sigDetails[i], positions[i], maxSigCount, 5, 20)); + } + } else if (sigDetails[i].isSigEnd) { + if (pdfEdgeMarks) { + drawLines.push(drawSpineMark(false, positions[i], 5)); + } } const sewingMarkPoints = sewingMarks.isEnabled ? drawSewingMarks( sigDetails[i], positions[i], + sewingMarks.sewingMarkLocation, sewingMarks.amount, sewingMarks.marginPt, sewingMarks.tapeWidthPt @@ -595,6 +623,9 @@ export class Book { drawLines.forEach((line) => { currPage.drawLine(line); }); + drawRects.forEach((rect) => { + currPage.drawRectangle(rect); + }); drawPoints.forEach((point) => { currPage.drawCircle(point); @@ -608,22 +639,27 @@ export class Book { /** * PDF builder base function for Classic (non-Wacky) layouts. Called by [createoutputfiles] + * Generates a single signature (or is it just a single sheet? -- comments left long after coding) + * TODO : re-examine this logic and clean up this comment. What is going on?? * * @param {Object} config + * @param {number} config.maxSigCount * @param {PageInfo[][]} config.pageIndexDetails : a nested list of objects. */ - async createSignatures(config) { + async createSignature(config) { const pages = config.pageIndexDetails; const tasks = [ this.writepages({ pageList: pages[0], back: false, alt: false, + maxSigCount: config.maxSigCount, }), this.writepages({ pageList: pages[1], back: true, alt: false, + maxSigCount: config.maxSigCount, }), ]; const [pdfFront, pdfBack] = await Promise.all(tasks); diff --git a/src/book.test.js b/src/book.test.js index dd236d0..b5a53a8 100644 --- a/src/book.test.js +++ b/src/book.test.js @@ -45,6 +45,7 @@ describe('Book model', () => { top: 0, }, sewingMarks: { + sewingMarkLocation: 'all', amount: 3, isEnabled: false, marginPt: 72, @@ -57,6 +58,7 @@ describe('Book model', () => { source_rotation: 'none', print_file: 'both', signatureconfig: [], + sigOrderMarks: false, }; it('returns a new Book', () => { diff --git a/src/html/crop_box.html b/src/html/crop_box.html new file mode 100644 index 0000000..d99ae60 --- /dev/null +++ b/src/html/crop_box.html @@ -0,0 +1,99 @@ +
+

PDF Markup

+ (does not currently work with Wacky Small Layouts -- soon!)
+ + + + + + + + + + + + + + + + +
+ Detailed settings for sewing + +
+ Look at the image below.
+ + + + + + sewing image - use tape width of 0pt to make a single dot (overlapping marks) +
+
+
+
diff --git a/src/html/page_layout.html b/src/html/page_layout.html index 04dd6b5..dd510eb 100644 --- a/src/html/page_layout.html +++ b/src/html/page_layout.html @@ -9,34 +9,6 @@

Page Layout

Sextodecimo - sixteen pages per side of sheet -
- - - -
- -
-
Folding instructions for quarto/octavo @@ -77,49 +49,9 @@

Page Layout

- - - - -
- Detailed settings for sewing - Look at the image below. - - - pt
- - -
- - - pt
- sewing image -
-
-
-
White Space Manipulation. All values are in points, relative to original document.

1 diff --git a/src/main.js b/src/main.js index 5a1b15c..0a97b9a 100644 --- a/src/main.js +++ b/src/main.js @@ -9,6 +9,7 @@ import { handleGenerateClick, handlePreviewClick, handleResetSettingsClick, + handleSewingMarksCheckboxState, } from './utils/clickHandlers.js'; import { renderPaperSelectOptions } from './utils/renderUtils.js'; @@ -25,6 +26,7 @@ window.addEventListener('DOMContentLoaded', () => { const fileInput = document.getElementById('input_file'); const inputs = document.querySelectorAll('input, select'); const sourceRotation = document.getElementById('source_rotation'); + const sewingMarks = document.getElementById('add_sewing_marks_checkbox'); const sourceRotationExamples = Array.from( document.getElementsByClassName('source_rotation_example') ); @@ -53,4 +55,8 @@ window.addEventListener('DOMContentLoaded', () => { example.style.display = example.id === selectedValue ? 'block' : 'none'; }); }); + sewingMarks.addEventListener('change', (e) => { + const willBeEnabled = e.srcElement.checked; + handleSewingMarksCheckboxState(willBeEnabled); + }); }); diff --git a/src/models/configuration.js b/src/models/configuration.js index 0dde7b5..4006aae 100644 --- a/src/models/configuration.js +++ b/src/models/configuration.js @@ -35,6 +35,10 @@ const sourceRotation = urlSafe( z.enum(['none', '90cw', '90ccw', 'out_binding', 'in_binding']) ).default('none'); +const sewingMarkLocation = urlSafe(z.enum(['all', 'only_out', 'only_in', 'in_n_out'])).default( + 'all' +); + /** @type { keyof typeof import("../constants").PAGE_SIZES } */ const availablePaperSizes = Object.keys(PAGE_SIZES); @@ -82,6 +86,7 @@ export const schema = z.object({ cropMarks: urlSafe(coercedBoolean).default(false), cutMarks: urlSafe(coercedBoolean).default(false), pdfEdgeMarks: urlSafe(coercedBoolean).default(false), + sigOrderMarks: urlSafe(coercedBoolean).default(false), pageScaling, pagePositioning, mainForeEdgePaddingPt: urlSafe(z.coerce.number()).default(0), @@ -96,6 +101,7 @@ export const schema = z.object({ flyleafs: urlSafe(z.coerce.number()).default(1), sewingMarksEnabled: urlSafe(coercedBoolean).default(false), + sewingMarkLocation, sewingMarksMarginPt: urlSafe(z.coerce.number()).default(72), sewingMarksAmount: urlSafe(z.coerce.number()).default(3), sewingMarksTapeWidthPt: urlSafe(z.coerce.number()).default(36), diff --git a/src/signatures.js b/src/signatures.js index 4a35528..7bf27c5 100644 --- a/src/signatures.js +++ b/src/signatures.js @@ -77,12 +77,13 @@ export class Signatures { const newsigs = []; // Use the booklet class for each signature - this.signaturepagelists.forEach((pagerange) => { + this.signaturepagelists.forEach((pagerange, i) => { const pagelistdetails = this.booklet( pagerange, this.duplex, this.per_sheet, - this.duplexrotate + this.duplexrotate, + i ); newsigs.push(pagelistdetails); }); @@ -122,8 +123,9 @@ export class Signatures { * @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. + * @param {number} sig_num - signature number (0 indexed) */ - booklet(pages, duplex, per_sheet, duplexrotate) { + booklet(pages, duplex, per_sheet, duplexrotate, sig_num) { const pagelistdetails = duplex ? [[]] : [[], []]; const { front, rotate, back } = BOOKLET_LAYOUTS[per_sheet]; @@ -146,24 +148,40 @@ export class Signatures { const block = [...front_block, ...back_block]; + console.log( + `Looking front_config : block.length ${block.length} : given center ${center}, front_start ${front_start} - front_end ${front_end}, back_start ${back_start} - back_end ${back_end}, pages.length ${pages.length}` + ); front_config.forEach((pnum) => { const page = block[pnum - 1]; //page layouts are 1-indexed, not 0-index pagelistdetails[0].push({ info: page, isSigStart: front_start == 0 && pnum == 1, isSigEnd: front_start == 0 && pnum == block.length, + isSigMiddle: front_end == back_start && block.length / 2 + 1 == pnum, + signatureNum: sig_num, }); + console.log( + ` >> ${pnum} :: ${page} :: ${front_end == back_start && block.length / 2 + 1 == pnum}` + ); }); const backlist = this.duplex ? 0 : 1; + console.log( + `Looking back_config : given center ${center}, front_start ${front_start} - front_end ${front_end}, back_start ${back_start} - back_end ${back_end}, pages.length ${pages.length}` + ); back_config.forEach((pnum) => { const page = block[pnum - 1]; pagelistdetails[backlist].push({ info: page, isSigStart: front_start == 0 && pnum == 1, isSigEnd: front_start == 0 && pnum == block.length, + isSigMiddle: front_end == back_start && block.length / 2 + 1 == pnum, + signatureNum: sig_num, }); + console.log( + ` >> ${pnum} :: ${page} :: ${front_end == back_start && block.length / 2 + 1 == pnum}` + ); }); // Update all our counters diff --git a/src/utils/clickHandlers.js b/src/utils/clickHandlers.js index 6c6e3a0..5517581 100644 --- a/src/utils/clickHandlers.js +++ b/src/utils/clickHandlers.js @@ -53,3 +53,12 @@ export function handleResetSettingsClick(book) { updateAddOrRemoveCustomPaperOption(); updatePaperSelectOptionsUnits(); } + +export function handleSewingMarksCheckboxState(sewingMarksEnabled) { + const sewingMarkDetailsEl = document.getElementById('sewing_marks_details'); + if (sewingMarksEnabled) { + sewingMarkDetailsEl.setAttribute('open', 0); + } else { + sewingMarkDetailsEl.removeAttribute('open'); + } +} diff --git a/src/utils/drawing.js b/src/utils/drawing.js index afeb25c..3cd62a1 100644 --- a/src/utils/drawing.js +++ b/src/utils/drawing.js @@ -124,12 +124,20 @@ export function drawCropmarks(papersize, per_sheet) { /** * @param {@param {import("../book.js").PageInfo}} sigDetails - information about signature where marks will be printed * @param {import("../book.js").Position} position - position info object + * @param sewingMarkLocation - see ./models/configuration.js for possible values * @param {number} amount - amount of sewing crosses. * @param {number} marginPt - distance from the end of sheet of paper to kettle mark * @param {number} tapeWidthPt - distance between two points in a single sewwing cross. * @returns {Point[]} */ -export function drawSewingMarks(sigDetails, position, amount, marginPt, tapeWidthPt) { +export function drawSewingMarks( + sigDetails, + position, + sewingMarkLocation, + amount, + marginPt, + tapeWidthPt +) { // Here normalize coordinates to always think in x an y like this // | P |H| P | // | A |E| A | @@ -139,7 +147,17 @@ export function drawSewingMarks(sigDetails, position, amount, marginPt, tapeWidt // |-POSITION-| | | // Left pages have spine position on the edge :/ + console.log('try to draw'); if (position.isLeftPage) return []; + console.log(' on right'); + + if (sewingMarkLocation == 'only_out' && !sigDetails.isSigStart) return []; + console.log(' a'); + if (sewingMarkLocation == 'only_in' && !sigDetails.isSigMiddle) return []; + console.log(' b'); + if (sewingMarkLocation == 'in_n_out' && !(sigDetails.isSigStart || sigDetails.isSigMiddle)) + return []; + console.log(' c'); var arePageRotated = Math.abs(position.rotation) === 90; let totalSpineHeight = 0; @@ -188,14 +206,14 @@ export function drawSewingMarks(sigDetails, position, amount, marginPt, tapeWidt } /** - * @param {import("../book.js").PageInfo} sigDetails - page info object + * @param {boolean} draw_top_mark - true to draw mark at top of PDF, false for bottom of PDF * @param {import("../book.js").Position} position - position info object + * @param {number} w - width of the line in pts * @returns {Line} */ -export function drawSpineMarks(sigDetails, position) { - const w = 5; +export function drawSpineMark(draw_top_mark, position, w) { let startX, startY, endX, endY; - if (sigDetails.isSigStart) { + if (draw_top_mark) { [startX, startY] = position.spineMarkTop; [endX, endY] = position.spineMarkTop; } else { @@ -223,6 +241,48 @@ export function drawSpineMarks(sigDetails, position) { return drawLineArgs; } +/** + * TODO : these params should probably be pushed into a config... maybe next time/next pass + * + * @param {import("../book.js").PageInfo} sigDetails - page info object + * @param {import("../book.js").Position} position - position info object + * @param {number} maxSigCount - number of total signatures + * @param {number} w - width of the mark in pts + * @param {number} suggested_h - suggested height of the mark in pts (can be scaled down to fit all marks between PDF top/bottom) + * @returns {Line} + */ +export function drawSigOrderMark(sigDetails, position, maxSigCount, w, suggested_h) { + const top = drawSpineMark(true, position, w); + const bottom = drawSpineMark(false, position, w); + + let x = top.start.x; + let y = top.start.y; + + const dist = position.rotation == 0 ? top.start.y - bottom.start.y : top.start.x - bottom.start.x; + let h = Math.min(suggested_h, dist / maxSigCount); + const offset = h * sigDetails.signatureNum; + // console.log("Looking at signature ",sigDetails.signatureNum," of ",maxSigCount," PDF top/bottom distance ",dist," results in ",h," (",suggested_h," vs ",(dist/maxSigCount),") order mark height w/ offset ",offset," (width ",w,")"); + + if (position.rotation == 0) { + h = h * -1; + y -= offset; + } else { + const temp = h; + h = w; + w = temp * -1; + x -= offset; + } + + return { + x: x, + y: y, + width: w, + height: h, + borderWidth: 0, + color: rgb(0, 0, 0), + opacity: 0.5, + }; +} /** * @param {number} x * @param {number} ystart diff --git a/src/utils/formUtils.js b/src/utils/formUtils.js index 1766dc9..89618a6 100644 --- a/src/utils/formUtils.js +++ b/src/utils/formUtils.js @@ -22,6 +22,7 @@ const fromFormToConfiguration = (form) => paperRotation90: form.has('paper_rotation_90'), pageLayout: form.get('pagelayout'), cropMarks: form.has('cropmarks'), + sigOrderMarks: form.has('sig_order_marks'), pdfEdgeMarks: form.has('pdf_edge_marks'), cutMarks: form.has('cutmarks'), pageScaling: form.get('page_scaling'), @@ -41,7 +42,8 @@ const fromFormToConfiguration = (form) => paperSizeCustomWidth: form.get('paper_size_custom_width'), paperSizeCustomHeight: form.get('paper_size_custom_height'), - sewingMarksEnabled: form.get('add_sewing_marks_checkbox'), + sewingMarksEnabled: form.has('add_sewing_marks_checkbox'), + sewingMarkLocation: form.get('sewing_mark_locations'), sewingMarksMarginPt: form.get('sewing_marks_margin_pt'), sewingMarksAmount: form.get('sewing_marks_amount'), sewingMarksTapeWidthPt: form.get('sewing_marks_tape_width_pt'), diff --git a/src/utils/layout.js b/src/utils/layout.js index a040371..033792f 100644 --- a/src/utils/layout.js +++ b/src/utils/layout.js @@ -131,7 +131,7 @@ export function calculateDimensions(book) { top: padding_pt.top * sy, }; - // page_positioning has 2 options: centered, binding_alinged + // page_positioning has 2 options: centered, binding_aligned const positioning = page_positioning; const xForeEdgeShiftFunc = function () { diff --git a/src/utils/renderUtils.js b/src/utils/renderUtils.js index 1c8b299..101d378 100644 --- a/src/utils/renderUtils.js +++ b/src/utils/renderUtils.js @@ -3,6 +3,7 @@ // file, You can obtain one at https://mozilla.org/MPL/2.0/. import { PAGE_SIZES } from '../constants'; +import { handleSewingMarksCheckboxState } from './clickHandlers.js'; export function renderPageCount(book) { const pageCount = document.getElementById('page_count'); @@ -183,6 +184,10 @@ export function renderFormFromSettings(configuration) { document.querySelector("input[name='cropmarks']").checked = true; } + if (configuration.sigOrderMarks) { + document.querySelector("input[name='sig_order_marks']").checked = true; + } + if (configuration.pdfEdgeMarks) { document.querySelector("input[name='pdf_edge_marks']").checked = true; } @@ -199,8 +204,11 @@ export function renderFormFromSettings(configuration) { ).checked = true; // Set french link stitches settings + handleSewingMarksCheckboxState(configuration.sewingMarksEnabled); document.querySelector('input[name="add_sewing_marks_checkbox"]').checked = configuration.sewingMarksEnabled; + document.querySelector('select[name="sewing_mark_locations"]').value = + configuration.sewingMarkLocation; document.querySelector('input[name="sewing_marks_margin_pt"]').value = configuration.sewingMarksMarginPt; document.querySelector('input[name="sewing_marks_amount"]').value =