diff --git a/CHANGELOG.md b/CHANGELOG.md index 71e6678..8aafbce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Added a share button to the presets area that will copy the current settings as shareable URL to the clipboard. - Turned selected cards into a setting, like the page selection. That way they can also be shared. +- The PDF generation will now - unless configured otherwise - try to make the best of the available space and generate multiple rows of cards. It will also detect whether a horizontal or vertical fold line is best to get as many as possible cards on one page. This should make the generated PDFs more efficient in terms of paper usage, especially in case of tiny cards - or tiles. ### 🐛 Bug fixes diff --git a/src/assets/cardfoldr.css b/src/assets/cardfoldr.css index dfbc6de..ca5d93d 100644 --- a/src/assets/cardfoldr.css +++ b/src/assets/cardfoldr.css @@ -170,6 +170,23 @@ input[type="number"]:invalid { color: red; } +.pure-form-aligned label { + margin-left: .2em; +} + +.pure-form-aligned .pure-control-group .spacer { + width: 10em; + margin: 0 1em 0 0; + display: inline-block; +} + +.pure-form-aligned .pure-control-group label:not(:first-child) { + text-align: left; + margin: .5em 0 .2em .2em; + width: inherit; + vertical-align: inherit; +} + /* pdf rendering */ #pages, diff --git a/src/assets/cardfoldr.js b/src/assets/cardfoldr.js index df5a80f..7f95a7b 100644 --- a/src/assets/cardfoldr.js +++ b/src/assets/cardfoldr.js @@ -607,10 +607,11 @@ const generatePdf = async () => { const foldingMargin = parseFloat(document.getElementById('foldingMargin').value); const printerMargin = parseFloat(document.getElementById('printerMargin').value); const cutterOffset = parseFloat(document.getElementById('cutterOffset').value); - const foldLine = document.getElementById('foldLine').value; + const foldLinePreference = document.getElementById('foldLine').value; const foldLineEdge = document.getElementById('foldLineEdge').value; const downloadFilename = document.getElementById('downloadFilename').value; + const allowMultipleRows = document.getElementById('allowMultipleRows').checked; const generateLog = document.getElementById('generate-output'); generateLog.textContent = "Collecting cards..."; @@ -701,7 +702,8 @@ const generatePdf = async () => { cutterOffset, pageSize, - foldLine, + foldLinePreference, + allowMultipleRows, title, } } diff --git a/src/assets/worker.js b/src/assets/worker.js index 89fde11..56194ca 100644 --- a/src/assets/worker.js +++ b/src/assets/worker.js @@ -16,7 +16,8 @@ if (typeof importScripts === "function") { } const insertMark = (page, x, y, options) => { - const length = withDefault(options.length, 2); + const lengthX = withDefault(options.lengthX, 2); + const lengthY = withDefault(options.lengthY, 2); const margin = withDefault(options.margin, 1); const color = withDefault(options.color, PDFLib.grayscale(0)); const background = withDefault(options.background, PDFLib.grayscale(1)); @@ -50,19 +51,19 @@ if (typeof importScripts === "function") { switch (c) { case "n": start = { x: x, y: y + margin * mmFactor }; - end = { x: x, y: y + (margin + length) * mmFactor }; + end = { x: x, y: y + (margin + lengthY) * mmFactor }; break; case "e": start = { x: x + margin * mmFactor, y: y }; - end = { x: x + (margin + length) * mmFactor, y: y }; + end = { x: x + (margin + lengthX) * mmFactor, y: y }; break; case "s": start = { x: x, y: y - margin * mmFactor }; - end = { x: x, y: y - (margin + length) * mmFactor }; + end = { x: x, y: y - (margin + lengthY) * mmFactor }; break; case "w": start = { x: x - margin * mmFactor, y: y }; - end = { x: x - (margin + length) * mmFactor, y: y }; + end = { x: x - (margin + lengthX) * mmFactor, y: y }; break; } @@ -74,7 +75,7 @@ if (typeof importScripts === "function") { } } - const drawMarkup = (page, orientation, rotate, pageWidth, pageHeight, cardWidth, cardHeight, totalWidth, totalHeight, cardMargin, foldingMargin, cutMargin, printerMargin, cutterOffset, cardPerPage) => { + const drawMarkup = (page, orientation, rotate, pageWidth, pageHeight, cardWidth, cardHeight, totalWidth, totalHeight, cardMargin, foldingMargin, cutMargin, printerMargin, cutterOffset, cardColumnsPerPage, cardRowsPerPage) => { if (!page) return; const mmFactor = 72 / 25.4; @@ -89,7 +90,15 @@ if (typeof importScripts === "function") { const unitWidthDoc = rotate ? cardHeightDoc : cardWidthDoc; const unitHeightDoc = rotate ? cardWidthDoc : cardHeightDoc; - + + const tickOptions = { + lengthX: 2, + lengthY: 2, + margin: 1, + cutterOffset: cutterOffsetDoc + } + const minTickLength = Math.min(2, cardMargin + cutMargin - 1); + if (orientation === "vertical") { // fold line page.drawLine({ @@ -101,38 +110,43 @@ if (typeof importScripts === "function") { }) // cut ticks - const tickOptions = { - cutterOffset: cutterOffsetDoc - } - const markX1 = pageWidth / 2 - foldingMarginDoc - unitWidthDoc + cutMarginDoc; - const markX2 = pageWidth / 2 - foldingMarginDoc - cutMarginDoc; - const markX3 = pageWidth / 2 + foldingMarginDoc + cutMarginDoc; - const markX4 = pageWidth / 2 + foldingMarginDoc + unitWidthDoc - cutMarginDoc; - - for (let i = 0; i < cardPerPage; i++) { - const partsLeft = (i === 0 || cardMargin && cutMargin) ? "nw" : "w"; - const partsRight = (i === 0 || cardMargin && cutMargin) ? "ne" : "e"; - const markY = (pageHeight + totalHeight) / 2 - i * (unitHeightDoc + cardMarginDoc) - cutMarginDoc; - - insertMark(page, markX1, markY, { parts: partsLeft, ...tickOptions }); - insertMark(page, markX2, markY, { parts: partsRight, ...tickOptions }); - insertMark(page, markX3, markY, { parts: partsLeft, ...tickOptions }); - insertMark(page, markX4, markY, { parts: partsRight, ...tickOptions }); - if ((cardMargin > 0 || cutMargin > 0) && i < cardPerPage - 1) { - const markY2 = markY - unitHeightDoc + 2 * cutMarginDoc; - insertMark(page, markX1, markY2, { parts: cardMargin && cutMargin ? "sw" : "w", ...tickOptions}); - insertMark(page, markX2, markY2, { parts: cardMargin && cutMargin ? "se" : "e", ...tickOptions}); - insertMark(page, markX3, markY2, { parts: cardMargin && cutMargin ? "sw" : "w", ...tickOptions}); - insertMark(page, markX4, markY2, { parts: cardMargin && cutMargin ? "se" : "e", ...tickOptions}); + for (let x = 0; x < cardRowsPerPage; x++) { + const markX1 = pageWidth / 2 - foldingMarginDoc - unitWidthDoc + cutMarginDoc - x * (unitWidthDoc + cardMarginDoc); + const markX2 = pageWidth / 2 - foldingMarginDoc - cutMarginDoc - x * (unitWidthDoc + cardMarginDoc); + const markX3 = pageWidth / 2 + foldingMarginDoc + cutMarginDoc + x * (unitWidthDoc + cardMarginDoc); + const markX4 = pageWidth / 2 + foldingMarginDoc + unitWidthDoc - cutMarginDoc + x * (unitWidthDoc + cardMarginDoc); + + for (let y = 0; y < cardColumnsPerPage; y++) { + const partsLeft = (y === 0 || cardMargin && cutMargin) ? "nw" : "w"; + const partsRight = (y === 0 || cardMargin && cutMargin) ? "ne" : "e"; + + const tickLengthOptions1 = { lengthX: x === cardRowsPerPage - 1 ? 2 : minTickLength, lengthY: y === 0 ? 2 : minTickLength }; + const tickLengthOptions2 = { lengthX: x === 0 ? 2 : minTickLength, lengthY: y === 0 ? 2 : minTickLength }; + const tickLengthOptions3 = { lengthX: x === 0 ? 2 : minTickLength, lengthY: y === 0 ? 2 : minTickLength }; + const tickLengthOptions4 = { lengthX: x === cardRowsPerPage - 1 ? 2 : minTickLength, lengthY: y === 0 ? 2 : minTickLength }; + + const markY = (pageHeight + totalHeight) / 2 - y * (unitHeightDoc + cardMarginDoc) - cutMarginDoc; + + insertMark(page, markX1, markY, { ...tickOptions, ...tickLengthOptions1, parts: partsLeft }); + insertMark(page, markX2, markY, { ...tickOptions, ...tickLengthOptions2, parts: partsRight }); + insertMark(page, markX3, markY, { ...tickOptions, ...tickLengthOptions3, parts: partsLeft }); + insertMark(page, markX4, markY, { ...tickOptions, ...tickLengthOptions4, parts: partsRight }); + if ((cardMargin > 0 || cutMargin > 0) && y < cardColumnsPerPage - 1) { + const markY2 = markY - unitHeightDoc + 2 * cutMarginDoc; + insertMark(page, markX1, markY2, { ...tickOptions, ...tickLengthOptions1, parts: cardMargin && cutMargin ? "sw" : "w", lengthY: y === cardColumnsPerPage - 1 ? 2 : minTickLength }); + insertMark(page, markX2, markY2, { ...tickOptions, ...tickLengthOptions2, parts: cardMargin && cutMargin ? "se" : "e", lengthY: y === cardColumnsPerPage - 1 ? 2 : minTickLength }); + insertMark(page, markX3, markY2, { ...tickOptions, ...tickLengthOptions3, parts: cardMargin && cutMargin ? "sw" : "w", lengthY: y === cardColumnsPerPage - 1 ? 2 : minTickLength }); + insertMark(page, markX4, markY2, { ...tickOptions, ...tickLengthOptions4, parts: cardMargin && cutMargin ? "se" : "e", lengthY: y === cardColumnsPerPage - 1 ? 2 : minTickLength }); + } } + + const finalMarkY = (pageHeight + totalHeight) / 2 - cardColumnsPerPage * (unitHeightDoc + cardMarginDoc) + cardMarginDoc + cutMarginDoc; + insertMark(page, markX1, finalMarkY, { ...tickOptions, parts: "sw", lengthX: x === cardRowsPerPage - 1 ? 2 : minTickLength }); + insertMark(page, markX2, finalMarkY, { ...tickOptions, parts: "se", lengthX: x === 0 ? 2 : minTickLength }); + insertMark(page, markX3, finalMarkY, { ...tickOptions, parts: "sw", lengthX: x === 0 ? 2 : minTickLength}); + insertMark(page, markX4, finalMarkY, { ...tickOptions, parts: "se", lengthX: x === cardRowsPerPage - 1 ? 2 : minTickLength }); } - - const finalMarkY = (pageHeight + totalHeight) / 2 - cardPerPage * (unitHeightDoc + cardMarginDoc) + cardMarginDoc + cutMarginDoc; - insertMark(page, markX1, finalMarkY, { parts: "sw", ...tickOptions}); - insertMark(page, markX2, finalMarkY, { parts: "se", ...tickOptions}); - insertMark(page, markX3, finalMarkY, { parts: "sw", ...tickOptions}); - insertMark(page, markX4, finalMarkY, { parts: "se", ...tickOptions}); } else { // fold line page.drawLine({ @@ -144,50 +158,56 @@ if (typeof importScripts === "function") { }) // cut ticks - const markY1 = pageHeight / 2 + foldingMarginDoc + unitHeightDoc - cutMarginDoc; - const markY2 = pageHeight / 2 + foldingMarginDoc + cutMarginDoc; - const markY3 = pageHeight / 2 - foldingMarginDoc - cutMarginDoc; - const markY4 = pageHeight / 2 - foldingMarginDoc - unitHeightDoc + cutMarginDoc; - - for (let i = 0; i < cardPerPage; i++) { - const partsUp = (i === 0 || cardMargin && cutMargin) ? "nw" : "n"; - const partsDown = (i === 0 || cardMargin && cutMargin) ? "sw" : "s"; - const markX = (pageWidth - totalWidth) / 2 + i * (unitWidthDoc + cardMarginDoc) + cutMarginDoc; - - insertMark(page, markX, markY1, { parts: partsUp }); - insertMark(page, markX, markY2, { parts: partsDown }); - insertMark(page, markX, markY3, { parts: partsUp }); - insertMark(page, markX, markY4, { parts: partsDown }); - if ((cardMargin > 0 || cutMargin > 0) && i < cardPerPage - 1) { - const markX2 = markX + unitWidthDoc - 2 * cutMarginDoc; - insertMark(page, markX2, markY1, { parts: cardMargin && cutMargin ? "ne" : "n"}); - insertMark(page, markX2, markY2, { parts: cardMargin && cutMargin ? "se" : "s"}); - insertMark(page, markX2, markY3, { parts: cardMargin && cutMargin ? "ne" : "n"}); - insertMark(page, markX2, markY4, { parts: cardMargin && cutMargin ? "se" : "s"}); + for (let y = 0; y < cardRowsPerPage; y++) { + const markY1 = pageHeight / 2 + foldingMarginDoc + unitHeightDoc - cutMarginDoc + y * (unitHeightDoc + cardMarginDoc); + const markY2 = pageHeight / 2 + foldingMarginDoc + cutMarginDoc + y * (unitHeightDoc + cardMarginDoc); + const markY3 = pageHeight / 2 - foldingMarginDoc - cutMarginDoc - y * (unitHeightDoc + cardMarginDoc); + const markY4 = pageHeight / 2 - foldingMarginDoc - unitHeightDoc + cutMarginDoc - y * (unitHeightDoc + cardMarginDoc); + + for (let x = 0; x < cardColumnsPerPage; x++) { + const partsUp = (x === 0 || cardMargin && cutMargin) ? "nw" : "n"; + const partsDown = (x === 0 || cardMargin && cutMargin) ? "sw" : "s"; + + const tickLengthOptions1 = { lengthX: x === 0 ? 2 : minTickLength, lengthY: y === cardRowsPerPage - 1 ? 2 : minTickLength }; + const tickLengthOptions2 = { lengthX: x === 0 ? 2 : minTickLength, lengthY: y === 0 ? 2 : minTickLength }; + const tickLengthOptions3 = { lengthX: x === 0 ? 2 : minTickLength, lengthY: y === 0 ? 2 : minTickLength }; + const tickLengthOptions4 = { lengthX: x === 0 ? 2 : minTickLength, lengthY: y === cardRowsPerPage - 1 ? 2 : minTickLength }; + + const markX = (pageWidth - totalWidth) / 2 + x * (unitWidthDoc + cardMarginDoc) + cutMarginDoc; + + insertMark(page, markX, markY1, { ...tickOptions, ...tickLengthOptions1, parts: partsUp }); + insertMark(page, markX, markY2, { ...tickOptions, ...tickLengthOptions2, parts: partsDown }); + insertMark(page, markX, markY3, { ...tickOptions, ...tickLengthOptions3, parts: partsUp }); + insertMark(page, markX, markY4, { ...tickOptions, ...tickLengthOptions4, parts: partsDown }); + if ((cardMargin > 0 || cutMargin > 0) && x < cardColumnsPerPage - 1) { + const markX2 = markX + unitWidthDoc - 2 * cutMarginDoc; + insertMark(page, markX2, markY1, { ...tickOptions, ...tickLengthOptions1, parts: cardMargin && cutMargin ? "ne" : "n", lengthX: x === cardColumnsPerPage - 1 ? 2 : minTickLength }); + insertMark(page, markX2, markY2, { ...tickOptions, ...tickLengthOptions2, parts: cardMargin && cutMargin ? "se" : "s", lengthX: x === cardColumnsPerPage - 1 ? 2 : minTickLength }); + insertMark(page, markX2, markY3, { ...tickOptions, ...tickLengthOptions3, parts: cardMargin && cutMargin ? "ne" : "n", lengthX: x === cardColumnsPerPage - 1 ? 2 : minTickLength }); + insertMark(page, markX2, markY4, { ...tickOptions, ...tickLengthOptions4, parts: cardMargin && cutMargin ? "se" : "s", lengthX: x === cardColumnsPerPage - 1 ? 2 : minTickLength }); + } } + + const finalMarkX = (pageWidth - totalWidth) / 2 + cardColumnsPerPage * (unitWidthDoc + cardMarginDoc) - cardMarginDoc - cutMarginDoc; + insertMark(page, finalMarkX, markY1, { ...tickOptions, parts: "ne", lengthY: y === cardRowsPerPage - 1 ? 2 : minTickLength}); + insertMark(page, finalMarkX, markY2, { ...tickOptions, parts: "se", lengthY: y === 0 ? 2 : minTickLength }); + insertMark(page, finalMarkX, markY3, { ...tickOptions, parts: "ne", lengthY: y === 0 ? 2 : minTickLength }); + insertMark(page, finalMarkX, markY4, { ...tickOptions, parts: "se", lengthY: y === cardRowsPerPage - 1 ? 2 : minTickLength}); } - - const finalMarkX = (pageWidth - totalWidth) / 2 + cardPerPage * (unitWidthDoc + cardMarginDoc) - cardMarginDoc - cutMarginDoc; - insertMark(page, finalMarkX, markY1, { parts: "ne"}); - insertMark(page, finalMarkX, markY2, { parts: "se"}); - insertMark(page, finalMarkX, markY3, { parts: "ne"}); - insertMark(page, finalMarkX, markY4, { parts: "se"}); } } - - const generatedPdf = async (cards, options) => { + + const findOptimalLayout = (options) => { const cardWidth = options.cardWidth; const cardHeight = options.cardHeight; const cardMargin = withDefault(options.cardMargin, 2); - const cutMargin = withDefault(options.cutMargin, 0); const foldingMargin = withDefault(options.foldingMargin, 5); const printerMargin = withDefault(options.printerMargin, 5); - const cutterOffset = withDefault(options.cutterOffset, 0); + const foldLinePreference = withDefault(options.foldLinePreference, "auto"); + const allowMultipleRows = withDefault(options.allowMultipleRows, true); const pageSize = validated(options.pageSize, x => PDFLib.PageSizes[x] !== undefined, "A4"); - const foldLine = validated(options.foldLine, x => ["vertical", "horizontal"].includes(x), "vertical"); - const title = withDefault(options.title, "CardFoldr PDF"); const pageFormat = PDFLib.PageSizes[pageSize]; @@ -199,74 +219,149 @@ if (typeof importScripts === "function") { const [cardWidthDoc, cardHeightDoc] = [cardWidth * mmFactor, cardHeight * mmFactor]; const cardMarginDoc = cardMargin * mmFactor; const foldingMarginDoc = foldingMargin * mmFactor; + + const maxCoverage = (spaceX, spaceY, cardWidth, cardHeight, cardMargin) => { + const cardsX = Math.floor(spaceX / (cardWidth + cardMargin)); + const cardsY = Math.floor(spaceY / (cardHeight + cardMargin)); + console.log("Max coverage", spaceX, spaceY, cardWidth, cardHeight, cardMargin, cardsX, cardsY); + return [cardsX, cardsY]; + } - const usableHalf = foldLine === "vertical" ? (usableWidth / 2 - foldingMarginDoc) : (usableHeight / 2 - foldingMarginDoc); + const optimum = {}; + const foldlines = (foldLinePreference == "vertical" || foldLinePreference === "horizontal") ? [foldLinePreference] : ["vertical", "horizontal"]; + for (const foldLine of foldlines) { + const spaceX = foldLine === "horizontal" ? usableWidth : (usableWidth / 2 - foldingMarginDoc); + const spaceY = foldLine === "vertical" ? usableHeight : (usableHeight / 2 - foldingMarginDoc); + + let cardColumnsPerPage, cardRowsPerPage, rotate, totalHeight, totalWidth; + + if (foldLine === "vertical") { + if (cardWidthDoc < spaceX && cardHeightDoc < spaceX) { + // card fits on half of the page in both orientations, lets figure out how many cards we can fit + const [cardsXWidth, cardsYWidth] = maxCoverage(spaceX, spaceY, cardWidthDoc, cardHeightDoc, cardMarginDoc); + const [cardsXHeight, cardsYHeight] = maxCoverage(spaceX, spaceY, cardHeightDoc, cardWidthDoc, cardMarginDoc); - let maxCardsPerPage, rotate, totalHeight, totalWidth; - if (foldLine === "vertical") { - if (cardWidthDoc < usableHalf && cardHeightDoc < usableHalf) { - // card fits on half of the page in both orientations, lets figure out how many cards we can fit - const cardsPerPageWidth = Math.floor(usableHeight / (cardWidthDoc + cardMarginDoc)); - const cardsPerPageHeight = Math.floor(usableHeight / (cardHeightDoc + cardMarginDoc)); - - if (cardsPerPageWidth < cardsPerPageHeight) { - maxCardsPerPage = cardsPerPageHeight; + const cardsPerPageWidth = (allowMultipleRows ? cardsXWidth : 1) * cardsYWidth; + const cardsPerPageHeight = (allowMultipleRows ? cardsXHeight : 1) * cardsYHeight; + + if (cardsPerPageWidth < cardsPerPageHeight) { + cardColumnsPerPage = cardsYHeight; + cardRowsPerPage = allowMultipleRows ? cardsXHeight : 1; + rotate = true; + } else { + cardColumnsPerPage = cardsYWidth; + cardRowsPerPage = allowMultipleRows ? cardsXWidth : 1; + rotate = false; + } + } else if (cardWidthDoc < spaceX) { + // card fits on half of the page in width, but not height + const [cardsX, cardsY] = maxCoverage(spaceX, spaceY, cardWidthDoc, cardHeightDoc, cardMarginDoc); + cardColumnsPerPage = cardsY; + cardRowsPerPage = allowMultipleRows ? cardsX : 1; rotate = false; - } else { - maxCardsPerPage = cardsPerPageWidth; + } else if (cardHeightDoc < spaceX) { + // card fits on half of the page in height, but not width + const [cardsX, cardsY] = maxCoverage(spaceX, spaceY, cardHeightDoc, cardWidthDoc, cardMarginDoc); + cardColumnsPerPage = cardsY; + cardRowsPerPage = allowMultipleRows ? cardsX : 1; rotate = true; + } else { + continue; } - } else if (cardWidthDoc < usableHalf) { - // card fits on half of the page in width, but not height - maxCardsPerPage = Math.floor(usableHeight / cardHeightDoc); - rotate = false; - } else if (cardHeightDoc < usableHalf) { - // card fits on half of the page in height, but not width - maxCardsPerPage = Math.floor(usableWidth / cardWidthDoc); - rotate = true; + + const unitWidth = rotate ? cardHeightDoc : cardWidthDoc; + const unitHeight = rotate ? cardWidthDoc : cardHeightDoc; + totalHeight = cardColumnsPerPage * unitHeight + (cardColumnsPerPage - 1) * cardMarginDoc; + totalWidth = 2 * cardRowsPerPage * unitWidth + (cardRowsPerPage - 1) * cardMarginDoc + 2 * foldingMarginDoc; } else { - // card does not fit on half of the page in either orientation - postMessage({ error: "Cards are too large to fit on half of the page in either orientation" }); - return; - } - - const unitWidth = rotate ? cardHeightDoc : cardWidthDoc; - const unitHeight = rotate ? cardWidthDoc : cardHeightDoc; - totalHeight = maxCardsPerPage * unitHeight + (maxCardsPerPage - 1) * cardMargin * mmFactor; - totalWidth = 2 * unitWidth + cardMargin * mmFactor; - } else { - if (cardWidth < usableHalf && cardHeight < usableHalf) { - // card fits on half of the page in both orientations, lets figure out how many cards we can fit - const cardsPerPageWidth = Math.floor(usableWidth / (cardWidthDoc + cardMarginDoc)); - const cardsPerPageHeight = Math.floor(usableWidth / (cardHeightDoc + cardMarginDoc)); - - if (cardsPerPageWidth > cardsPerPageHeight) { - maxCardsPerPage = cardsPerPageWidth; + if (cardWidth < spaceY && cardHeight < spaceY) { + // card fits on half of the page in both orientations, lets figure out how many cards we can fit + const [cardsXWidth, cardsYWidth] = maxCoverage(spaceX, spaceY, cardWidthDoc, cardHeightDoc, cardMarginDoc); + const [cardsXHeight, cardsYHeight] = maxCoverage(spaceX, spaceY, cardHeightDoc, cardWidthDoc, cardMarginDoc); + + const cardsPerPageWidth = cardsXWidth * (allowMultipleRows ? cardsYWidth : 1); + const cardsPerPageHeight = cardsXHeight * (allowMultipleRows ? cardsYHeight : 1); + + if (cardsPerPageWidth > cardsPerPageHeight) { + cardColumnsPerPage = cardsXWidth; + cardRowsPerPage = allowMultipleRows ? cardsYWidth : 1; + rotate = false; // heads-up, inverted logic! + } else { + cardColumnsPerPage = cardsXHeight; + cardRowsPerPage = allowMultipleRows ? cardsYHeight : 1; + rotate = true; // heads-up, inverted logic! + } + } else if (cardWidthDoc < spaceY) { + // card fits on half of the page in width, but not height + const [cardsX, cardsY] = maxCoverage(spaceX, spaceY, cardWidthDoc, cardHeightDoc, cardMarginDoc); + cardColumnsPerPage = cardsX; + cardRowsPerPage = allowMultipleRows ? cardsY : 1; + rotate = false; // heads-up, inverted logic! + } else if (cardHeightDoc < spaceY) { + // card fits on half of the page in height, but not width + const [cardsX, cardsY] = maxCoverage(spaceX, spaceY, cardHeightDoc, cardWidthDoc, cardMarginDoc); + cardColumnsPerPage = cardsX; + cardRowsPerPage = allowMultipleRows ? cardsY : 1; rotate = true; // heads-up, inverted logic! } else { - maxCardsPerPage = cardsPerPageHeight; - rotate = false; // heads-up, inverted logic! + continue; } - } else if (cardWidth < usableHalf) { - // card fits on half of the page in width, but not height - maxCardsPerPage = Math.floor(usableWidth / cardWidthDoc); - rotate = true; // heads-up, inverted logic! - } else if (cardHeight < usableHalf) { - // card fits on half of the page in height, but not width - maxCardsPerPage = Math.floor(usableWidth / cardHeightDoc); - rotate = false; // heads-up, inverted logic! - } else { - // card does not fit on half of the page in either orientation - postMessage({ error: "Cards are too large to fit on half of the page in either orientation" }); - return; + + const unitWidth = rotate ? cardHeightDoc : cardWidthDoc; + const unitHeight = rotate ? cardWidthDoc : cardHeightDoc; + totalWidth = cardColumnsPerPage * unitWidth + (cardColumnsPerPage - 1) * cardMarginDoc; + totalHeight = 2 * cardRowsPerPage * unitHeight + (cardRowsPerPage - 1) * cardMarginDoc + 2 * foldingMarginDoc; } - - const unitWidth = rotate ? cardHeightDoc : cardWidthDoc; - const unitHeight = rotate ? cardWidthDoc : cardHeightDoc; - totalWidth = maxCardsPerPage * unitWidth + (maxCardsPerPage - 1) * cardMargin * mmFactor; - totalHeight = 2 * unitHeight + cardMargin * mmFactor; + + if (cardColumnsPerPage * cardRowsPerPage > (optimum.cardsPerPage || 0)) { + optimum.cardsPerPage = cardColumnsPerPage * cardRowsPerPage; + optimum.cardColumnsPerPage = cardColumnsPerPage; + optimum.cardRowsPerPage = cardRowsPerPage; + optimum.rotate = rotate; + optimum.totalHeight = totalHeight; + optimum.totalWidth = totalWidth; + optimum.orientation = foldLine; + } + } + + if (!optimum.cardsPerPage) { + // card does not fit on half of the page in either orientation + postMessage({ error: "Cards are too large to fit on half of the page in either orientation" }); + return false; } + + console.log("Optimum layout", optimum); + return optimum; + }; + const generatedPdf = async (cards, options) => { + const cardWidth = options.cardWidth; + const cardHeight = options.cardHeight; + + const cardMargin = withDefault(options.cardMargin, 2); + const cutMargin = withDefault(options.cutMargin, 0); + const foldingMargin = withDefault(options.foldingMargin, 5); + const printerMargin = withDefault(options.printerMargin, 5); + const cutterOffset = withDefault(options.cutterOffset, 0); + + const pageSize = validated(options.pageSize, x => PDFLib.PageSizes[x] !== undefined, "A4"); + const title = withDefault(options.title, "CardFoldr PDF"); + + const pageFormat = PDFLib.PageSizes[pageSize]; + + const mmFactor = 72 / 25.4; + + const [pageWidth, pageHeight] = pageFormat; + const [cardWidthDoc, cardHeightDoc] = [cardWidth * mmFactor, cardHeight * mmFactor]; + const cardMarginDoc = cardMargin * mmFactor; + const foldingMarginDoc = foldingMargin * mmFactor; + + const layoutSettings = findOptimalLayout(options); + if (!layoutSettings) { + return; + } + const { cardsPerPage, cardColumnsPerPage, cardRowsPerPage, rotate, totalWidth, totalHeight, orientation } = layoutSettings; + reportProgress(0, cards.length); const url = "https://foosel.github.io/cardfoldr"; @@ -299,49 +394,50 @@ if (typeof importScripts === "function") { const frontImage = await lookupCard(card.front); const backImage = await lookupCard(card.back); - if (page == null || count % maxCardsPerPage === 0) { - drawMarkup(page, foldLine, rotate, pageWidth, pageHeight, cardWidth, cardHeight, totalWidth, totalHeight, cardMargin, foldingMargin, cutMargin, printerMargin, cutterOffset, maxCardsPerPage); + if (page == null || count % cardsPerPage === 0) { + drawMarkup(page, orientation, rotate, pageWidth, pageHeight, cardWidth, cardHeight, totalWidth, totalHeight, cardMargin, foldingMargin, cutMargin, printerMargin, cutterOffset, cardColumnsPerPage, cardRowsPerPage); pages++; page = pdfDoc.addPage(pageFormat); } let xFront, yFront, xBack, yBack, angleFront, angleBack; - if (foldLine === "vertical") { + const row = Math.floor((count % cardsPerPage) / cardColumnsPerPage); + if (orientation === "vertical") { if (rotate) { angleFront = PDFLib.degrees(90); - xFront = pageWidth / 2 - foldingMarginDoc; - yFront = (pageHeight + totalHeight) / 2 - cardWidthDoc - (count % maxCardsPerPage) * (cardWidthDoc + cardMarginDoc); + xFront = pageWidth / 2 - foldingMarginDoc - row * (cardHeightDoc + cardMarginDoc); + yFront = (pageHeight + totalHeight) / 2 - cardWidthDoc - (count % cardColumnsPerPage) * (cardWidthDoc + cardMarginDoc); angleBack = PDFLib.degrees(-90); - xBack = pageWidth / 2 + foldingMarginDoc; - yBack = (pageHeight + totalHeight) / 2 - (count % maxCardsPerPage) * (cardWidthDoc + cardMarginDoc); + xBack = pageWidth / 2 + foldingMarginDoc + row * (cardHeightDoc + cardMarginDoc); + yBack = (pageHeight + totalHeight) / 2 - (count % cardColumnsPerPage) * (cardWidthDoc + cardMarginDoc); } else { angleFront = PDFLib.degrees(0); - xFront = pageWidth / 2 - foldingMarginDoc - cardWidthDoc; - yFront = (pageHeight + totalHeight) / 2 - cardHeightDoc - (count % maxCardsPerPage) * (cardHeightDoc + cardMarginDoc); + xFront = pageWidth / 2 - foldingMarginDoc - cardWidthDoc - row * (cardWidthDoc + cardMarginDoc); + yFront = (pageHeight + totalHeight) / 2 - cardHeightDoc - (count % cardColumnsPerPage) * (cardHeightDoc + cardMarginDoc); angleBack = PDFLib.degrees(0); - xBack = pageWidth / 2 + foldingMarginDoc; + xBack = pageWidth / 2 + foldingMarginDoc + row * (cardWidthDoc + cardMarginDoc); yBack = yFront; } } else { if (!rotate) { // heads-up, inverted logic! angleFront = PDFLib.degrees(0); - yFront = pageHeight / 2 + foldingMarginDoc; - xFront = (pageWidth - totalWidth) / 2 + (count % maxCardsPerPage) * (cardWidthDoc + cardMarginDoc); + yFront = pageHeight / 2 + foldingMarginDoc + row * (cardHeightDoc + cardMarginDoc); + xFront = (pageWidth - totalWidth) / 2 + (count % cardColumnsPerPage) * (cardWidthDoc + cardMarginDoc); angleBack = PDFLib.degrees(180); - yBack = pageHeight / 2 - foldingMarginDoc; + yBack = pageHeight / 2 - foldingMarginDoc - row * (cardHeightDoc + cardMarginDoc); xBack = xFront + cardWidthDoc; } else { angleFront = PDFLib.degrees(90); - yFront = pageHeight / 2 + foldingMarginDoc; - xFront = (pageWidth - totalWidth) / 2 + cardHeightDoc + (count % maxCardsPerPage) * (cardHeightDoc + cardMarginDoc); + yFront = pageHeight / 2 + foldingMarginDoc + row * (cardWidthDoc + cardMarginDoc); + xFront = (pageWidth - totalWidth) / 2 + cardHeightDoc + (count % cardColumnsPerPage) * (cardHeightDoc + cardMarginDoc); angleBack = PDFLib.degrees(90); - yBack = pageHeight / 2 - foldingMarginDoc - cardWidthDoc; + yBack = pageHeight / 2 - foldingMarginDoc - cardWidthDoc - row * (cardWidthDoc + cardMarginDoc); xBack = xFront } } @@ -366,7 +462,7 @@ if (typeof importScripts === "function") { reportProgress(count, cards.length); } - drawMarkup(page, foldLine, rotate, pageWidth, pageHeight, cardWidth, cardHeight, totalWidth, totalHeight, cardMargin, foldingMargin, cutMargin, printerMargin, cutterOffset, maxCardsPerPage); + drawMarkup(page, orientation, rotate, pageWidth, pageHeight, cardWidth, cardHeight, totalWidth, totalHeight, cardMargin, foldingMargin, cutMargin, printerMargin, cutterOffset, cardColumnsPerPage, cardRowsPerPage); reportSaving(); const pdfBytes = await pdfDoc.save({ useObjectStreams: true }); diff --git a/src/index.html b/src/index.html index 1a5c25b..9c556db 100644 --- a/src/index.html +++ b/src/index.html @@ -215,7 +215,8 @@