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 @@

Step 3: Extract Cards

Comma separated list of cards or ranges of all included cards -
+
+ Use this if the backs are otherwise upside down
@@ -260,10 +261,11 @@

Step 4: Generate PDF

- Direction of the fold line + Direction of the fold line, "Auto" will optimize for cards per page
@@ -300,6 +302,12 @@

Step 4: Generate PDF

Distance from the fold line to the edge of the card
+ +
+ + + Allowing multiple rows will possibly allow more cards per page +
diff --git a/tests/files/test-pdf.foldable.horizontal.pdf b/tests/files/test-pdf.foldable.horizontal.pdf new file mode 100644 index 0000000..e18e042 Binary files /dev/null and b/tests/files/test-pdf.foldable.horizontal.pdf differ diff --git a/tests/files/test-pdf.foldable.no-rows.pdf b/tests/files/test-pdf.foldable.no-rows.pdf new file mode 100644 index 0000000..5c2ffa9 Binary files /dev/null and b/tests/files/test-pdf.foldable.no-rows.pdf differ diff --git a/tests/files/test-pdf.foldable.pdf b/tests/files/test-pdf.foldable.pdf index 83cadb0..9d6b9b3 100644 Binary files a/tests/files/test-pdf.foldable.pdf and b/tests/files/test-pdf.foldable.pdf differ diff --git a/tests/files/test-pdf.foldable.vertical.pdf b/tests/files/test-pdf.foldable.vertical.pdf new file mode 100644 index 0000000..e947e12 Binary files /dev/null and b/tests/files/test-pdf.foldable.vertical.pdf differ diff --git a/tests/fixtures.js b/tests/fixtures.js index 3afbafd..89613d1 100644 --- a/tests/fixtures.js +++ b/tests/fixtures.js @@ -5,7 +5,10 @@ const testPdf = { preset: {"grid-count-x":"3","grid-count-y":"3","grid-width":"40","grid-height":"40","grid-start-x":"0","grid-start-y":"0","grid-margin-x":"0","grid-margin-y":"0","grid-cut-margin":"0","grid-step-size":"0.1","cards-backs":"fileall","cards-rotate-backs":false,"_key":"test-pdf","_name":"Test PDF"}, path: "./files/test-pdf.pdf", filename: "test-pdf.pdf", - outputPath : "./files/test-pdf.foldable.pdf", + outputPathDefault: "./files/test-pdf.foldable.pdf", + outputPathVerticalOnly: "./files/test-pdf.foldable.vertical.pdf", + outputPathHorizontalOnly: "./files/test-pdf.foldable.horizontal.pdf", + outputPathNoRows: "./files/test-pdf.foldable.no-rows.pdf", outputFilename: "test-pdf.foldable.pdf" } diff --git a/tests/specs/generate-pdf.spec.js b/tests/specs/generate-pdf.spec.js index bed6f71..c859d92 100644 --- a/tests/specs/generate-pdf.spec.js +++ b/tests/specs/generate-pdf.spec.js @@ -1,6 +1,7 @@ const { test, expect } = require('../fixtures'); const ComparePdf = require("compare-pdf"); -const fs = require("fs"); + +test.describe.configure({ mode: 'serial' }); const pdfCompare = async (actual, expected) => { return await new ComparePdf() @@ -9,6 +10,24 @@ const pdfCompare = async (actual, expected) => { .compare(); }; +const checkPdf = async (download, expected, testInfo) => { + if (testInfo.project.name === "chromium") { + // compare PDFs, but only in Chrome for now due to different rendering in Firefox + let result; + try { + result = await pdfCompare(await download.path(), expected); + } catch (error) { + result.message = error; + } + + const actualPath = testInfo.outputPath("test-pdf.foldable.pdf"); + await download.saveAs(actualPath); + + expect(result.message).toBeUndefined(); + expect(result.status).toBe("passed") + } +}; + test.beforeEach(async ({page, testPdf}) => { await page.goto(`./?${testPdf.query}`); @@ -24,7 +43,7 @@ test.beforeEach(async ({page, testPdf}) => { await expect(page.locator("#generate")).toBeEnabled(); }); -test("Simple PDF generation", async ({page, testPdf}, testInfo) => { +test("PDF generation: defaults", async ({page, testPdf}, testInfo) => { // generate PDF await page.locator("#generate").click(); @@ -41,18 +60,82 @@ test("Simple PDF generation", async ({page, testPdf}, testInfo) => { // verify the PDF is downloaded expect(download.url()).toContain("blob:"); expect(download.suggestedFilename()).toMatch(/.pdf$/i); + await checkPdf(download, testPdf.outputPathDefault, testInfo); + + // cleanup + await download.delete(); +}); - if (testInfo.project === "chromium") { - // compare PDFs, but only in Chrome for now due to different rendering in Firefox - let result; - try { - result = await pdfCompare(await download.path(), testPdf.outputPath); - } catch (error) { - result = error; - } - expect(result.message).toBeUndefined(); - expect(result.status).toBe("passed") - } +test("PDF generation: vertical enforced", async ({page, testPdf}, testInfo) => { + await page.locator("#foldLine").selectOption("vertical"); + + // generate PDF + await page.locator("#generate").click(); + + // verify the PDF is generated + await expect(await page.locator("#download-button").getAttribute("href")).toContain("blob:"); + await expect(await page.locator("#output iframe").getAttribute("src")).toContain("blob:"); + + // download PDF + const downloadPromise = page.waitForEvent('download'); + await expect(await page.locator("#download-button").getAttribute("download")).toBe(testPdf.outputFilename); + await page.locator("#download-button").click(); + const download = await downloadPromise; + + // verify the PDF is downloaded + expect(download.url()).toContain("blob:"); + expect(download.suggestedFilename()).toMatch(/.pdf$/i); + await checkPdf(download, testPdf.outputPathVerticalOnly, testInfo); + + // cleanup + await download.delete(); +}); + +test("PDF generation: horizontal enforced", async ({page, testPdf}, testInfo) => { + await page.locator("#foldLine").selectOption("horizontal"); + + // generate PDF + await page.locator("#generate").click(); + + // verify the PDF is generated + await expect(await page.locator("#download-button").getAttribute("href")).toContain("blob:"); + await expect(await page.locator("#output iframe").getAttribute("src")).toContain("blob:"); + + // download PDF + const downloadPromise = page.waitForEvent('download'); + await expect(await page.locator("#download-button").getAttribute("download")).toBe(testPdf.outputFilename); + await page.locator("#download-button").click(); + const download = await downloadPromise; + + // verify the PDF is downloaded + expect(download.url()).toContain("blob:"); + expect(download.suggestedFilename()).toMatch(/.pdf$/i); + await checkPdf(download, testPdf.outputPathHorizontalOnly, testInfo); + + // cleanup + await download.delete(); +}); + +test("PDF generation: no rows", async ({page, testPdf}, testInfo) => { + await page.locator("#allowMultipleRows").uncheck(); + + // generate PDF + await page.locator("#generate").click(); + + // verify the PDF is generated + await expect(await page.locator("#download-button").getAttribute("href")).toContain("blob:"); + await expect(await page.locator("#output iframe").getAttribute("src")).toContain("blob:"); + + // download PDF + const downloadPromise = page.waitForEvent('download'); + await expect(await page.locator("#download-button").getAttribute("download")).toBe(testPdf.outputFilename); + await page.locator("#download-button").click(); + const download = await downloadPromise; + + // verify the PDF is downloaded + expect(download.url()).toContain("blob:"); + expect(download.suggestedFilename()).toMatch(/.pdf$/i); + await checkPdf(download, testPdf.outputPathNoRows, testInfo); // cleanup await download.delete();