Skip to content

Commit

Permalink
pdf export fitToPage fix number of pages
Browse files Browse the repository at this point in the history
  • Loading branch information
zsviczian committed Jan 19, 2025
1 parent 01e3921 commit b18637f
Show file tree
Hide file tree
Showing 7 changed files with 101 additions and 67 deletions.
2 changes: 1 addition & 1 deletion src/core/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -502,7 +502,7 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
pdfSettings: {
pageSize: "A4",
pageOrientation: "portrait",
fitToPage: true,
fitToPage: 1,
paperColor: "white",
customPaperColor: "#ffffff",
alignment: "center",
Expand Down
6 changes: 6 additions & 0 deletions src/lang/locale/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1050,6 +1050,12 @@ FILENAME_HEAD: "Filename",
EXPORTDIALOG_ORIENTATION_LANDSCAPE: "Landscape",
EXPORTDIALOG_PDF_FIT_TO_PAGE: "Page Fitting",
EXPORTDIALOG_PDF_FIT_OPTION: "Fit to page",
EXPORTDIALOG_PDF_FIT_2_OPTION: "Fit to 2-pages",
EXPORTDIALOG_PDF_FIT_4_OPTION: "Fit to 4-pages",
EXPORTDIALOG_PDF_FIT_6_OPTION: "Fit to 6-pages",
EXPORTDIALOG_PDF_FIT_8_OPTION: "Fit to 8-pages",
EXPORTDIALOG_PDF_FIT_12_OPTION: "Fit to 12-pages",
EXPORTDIALOG_PDF_FIT_16_OPTION: "Fit to 16-pages",
EXPORTDIALOG_PDF_SCALE_OPTION: "Use image scale (may span multiple pages)",
EXPORTDIALOG_PDF_PAPER_COLOR: "Paper Color",
EXPORTDIALOG_PDF_PAPER_WHITE: "White",
Expand Down
2 changes: 1 addition & 1 deletion src/shared/Dialogs/ExportDialog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export class ExportDialog extends Modal {
private contentContainer: HTMLDivElement;
private buttonContainerRow1: HTMLDivElement;
private buttonContainerRow2: HTMLDivElement;
public fitToPage: boolean = true;
public fitToPage: number = 1;
public paperColor: "white" | "scene" | "custom" = "white";
public customPaperColor: string = "#ffffff";
public alignment: PDFPageAlignment = "center";
Expand Down
16 changes: 12 additions & 4 deletions src/shared/Dialogs/PDFExportSettingsComponent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { t } from "src/lang/helpers";
export interface PDFExportSettings {
pageSize: PageSize;
pageOrientation: PageOrientation;
fitToPage: boolean;
fitToPage: number;
paperColor: "white" | "scene" | "custom";
customPaperColor: string;
alignment: PDFPageAlignment;
Expand Down Expand Up @@ -60,12 +60,20 @@ export class PDFExportSettingsComponent {
.addDropdown(dropdown =>
dropdown
.addOptions({
"scale": t("EXPORTDIALOG_PDF_SCALE_OPTION"),
"fit": t("EXPORTDIALOG_PDF_FIT_OPTION"),
"scale": t("EXPORTDIALOG_PDF_SCALE_OPTION")
"fit-2": t("EXPORTDIALOG_PDF_FIT_2_OPTION"),
"fit-4": t("EXPORTDIALOG_PDF_FIT_4_OPTION"),
"fit-6": t("EXPORTDIALOG_PDF_FIT_6_OPTION"),
"fit-8": t("EXPORTDIALOG_PDF_FIT_8_OPTION"),
"fit-12": t("EXPORTDIALOG_PDF_FIT_12_OPTION"),
"fit-16": t("EXPORTDIALOG_PDF_FIT_16_OPTION")
})
.setValue(this.settings.fitToPage ? "fit" : "scale")
.setValue(this.settings.fitToPage === 1 ? "fit" :
(typeof this.settings.fitToPage === "number" ? `fit-${this.settings.fitToPage}` : "scale"))
.onChange(value => {
this.settings.fitToPage = value === "fit";
this.settings.fitToPage = value === "scale" ? 0 :
(value === "fit" ? 1 : parseInt(value.split("-")[1]));
this.update();
})
);
Expand Down
6 changes: 3 additions & 3 deletions src/shared/ExcalidrawAutomate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -955,14 +955,14 @@ export class ExcalidrawAutomate {
*
* @param {Object} params - The parameters for creating the PDF.
* @param {SVGSVGElement[]} params.SVG - An array of SVG elements to be included in the PDF.
* @param {PDFExportScale} [params.scale={ fitToPage: true, zoom: 1 }] - The scaling options for the SVG elements.
* @param {PDFExportScale} [params.scale={ fitToPage: 1, zoom: 1 }] - The scaling options for the SVG elements.
* @param {PDFPageProperties} [params.pageProps] - The properties for the PDF pages.
* @returns {Promise<ArrayBuffer>} - A promise that resolves to an ArrayBuffer containing the PDF data.
*
* @example
* const pdfData = await createToPDF({
* SVG: [svgElement1, svgElement2],
* scale: { fitToPage: true },
* scale: { fitToPage: 1 },
* pageProps: {
* dimensions: { width: 595.28, height: 841.89 },
* backgroundColor: "#ffffff",
Expand All @@ -973,7 +973,7 @@ export class ExcalidrawAutomate {
*/
async createPDF({
SVG,
scale = { fitToPage: true, zoom: 1 },
scale = { fitToPage: 1, zoom: 1 },
pageProps,
}: {
SVG: SVGSVGElement[];
Expand Down
129 changes: 75 additions & 54 deletions src/utils/exportUtils.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import { PDFDocument, rgb } from '@cantoo/pdf-lib';
import { getEA } from 'src/core';

const SVG_DPI = 300;
const PDF_DPI = 72;
const SVG_TO_PDF_SCALE = PDF_DPI / SVG_DPI;

export type PDFPageAlignment = "center" | "top-left" | "top-center" | "top-right" | "bottom-left" | "bottom-center" | "bottom-right";
export type PDFPageMarginString = "none" | "tiny" | "normal";

export interface PDFExportScale {
fitToPage: boolean;
fitToPage: number; // 0 means use zoom, >1 means fit to that many pages exactly
zoom?: number;
}

Expand Down Expand Up @@ -106,6 +109,17 @@ function calculatePosition(
return {x, y};
}

function getNumberOfPages(
width: number,
height: number,
availableWidth: number,
availableHeight: number
): number {
const cols = Math.ceil(width / availableWidth);
const rows = Math.ceil(height / availableHeight);
return cols * rows;
}

function calculateDimensions(
svgWidth: number,
svgHeight: number,
Expand All @@ -114,17 +128,46 @@ function calculateDimensions(
scale: PDFExportScale,
alignment: PDFPageAlignment
): SVGDimensions[] {
const pdfWidth = svgWidth * SVG_TO_PDF_SCALE;
const pdfHeight = svgHeight * SVG_TO_PDF_SCALE;
const availableWidth = pageDim.width - margin.left - margin.right;
const availableHeight = pageDim.height - margin.top - margin.bottom;

let finalWidth: number;
let finalHeight: number;
// If fitToPage is specified, find optimal zoom using binary search
if (scale.fitToPage > 0) {
let low = 0;
let high = 100; // Start with a reasonable upper bound
let bestZoom = 1;
const tolerance = 0.000001;

while (high - low > tolerance) {
const mid = (low + high) / 2;
const scaledWidth = pdfWidth * mid;
const scaledHeight = pdfHeight * mid;
const pages = getNumberOfPages(scaledWidth, scaledHeight, availableWidth, availableHeight);

if (pages > scale.fitToPage) {
high = mid;
} else {
bestZoom = mid;
low = mid;
}
}

if (scale.fitToPage) {
const ratio = Math.min(availableWidth / svgWidth, availableHeight / svgHeight);
finalWidth = svgWidth * ratio;
finalHeight = svgHeight * ratio;

// Apply a small reduction to prevent floating-point issues
scale.zoom = Math.round(bestZoom * 0.99999 * 1000000) / 1000000;
}

// Now handle as regular scale mode
const finalWidth = Math.round(pdfWidth * (scale.zoom || 1) * 1000) / 1000;
const finalHeight = Math.round(pdfHeight * (scale.zoom || 1) * 1000) / 1000;

// Round the available dimensions as well for consistent comparison
const roundedAvailableWidth = Math.round(availableWidth * 1000) / 1000;
const roundedAvailableHeight = Math.round(availableHeight * 1000) / 1000;

if (finalWidth <= roundedAvailableWidth && finalHeight <= roundedAvailableHeight) {
// Content fits on one page
const position = calculatePosition(
finalWidth,
finalHeight,
Expand All @@ -133,64 +176,45 @@ function calculateDimensions(
margin,
alignment,
);

return [{
width: finalWidth,
height: finalHeight,
x: position.x,
y: position.y
}];
} else {
// Scale mode - may need multiple pages
finalWidth = svgWidth * (scale.zoom || 1);
finalHeight = svgHeight * (scale.zoom || 1);

if (finalWidth <= availableWidth && finalHeight <= availableHeight) {
// Content fits on one page
const position = calculatePosition(
finalWidth,
finalHeight,
pageDim.width,
pageDim.height,
margin,
alignment,
);

return [{
width: finalWidth,
height: finalHeight,
x: position.x,
y: position.y
}];
} else {
// Content needs to be tiled across multiple pages
const dimensions: SVGDimensions[] = [];
const cols = Math.ceil(finalWidth / availableWidth);
const rows = Math.ceil(finalHeight / availableHeight);

for (let row = 0; row < rows; row++) {
for (let col = 0; col < cols; col++) {
const tileWidth = Math.min(availableWidth, finalWidth - col * availableWidth);
const tileHeight = Math.min(availableHeight, finalHeight - row * availableHeight);

// Calculate y coordinate following the same logic as single-page rendering
// We start from the bottom margin and work our way up
//const y = margin.bottom + row * availableHeight;
// Content needs to be tiled across multiple pages
const dimensions: SVGDimensions[] = [];
// Calculate exact number of needed columns and rows
const cols = Math.ceil(finalWidth / roundedAvailableWidth);
const rows = Math.ceil(finalHeight / roundedAvailableHeight);

for (let row = 0; row < rows; row++) {
for (let col = 0; col < cols; col++) {
// Calculate remaining width and height for this tile
const remainingWidth = finalWidth - col * roundedAvailableWidth;
const remainingHeight = finalHeight - row * roundedAvailableHeight;

// Only create tile if there's actual content to show
if (remainingWidth > 0 && remainingHeight > 0) {
const tileWidth = Math.min(roundedAvailableWidth, remainingWidth);
const tileHeight = Math.min(roundedAvailableHeight, remainingHeight);

dimensions.push({
width: tileWidth,
height: tileHeight,
x: margin.left,
y: margin.top,
sourceX: col * availableWidth / (scale.zoom || 1),
sourceY: row * availableHeight / (scale.zoom || 1),
sourceWidth: tileWidth / (scale.zoom || 1),
sourceHeight: tileHeight / (scale.zoom || 1)
sourceX: (col * roundedAvailableWidth) / ((scale.zoom || 1) * SVG_TO_PDF_SCALE),
sourceY: (row * roundedAvailableHeight) / ((scale.zoom || 1) * SVG_TO_PDF_SCALE),
sourceWidth: tileWidth / ((scale.zoom || 1) * SVG_TO_PDF_SCALE),
sourceHeight: tileHeight / ((scale.zoom || 1) * SVG_TO_PDF_SCALE)
});
}
}
return dimensions;
}
return dimensions;
}
}

Expand Down Expand Up @@ -292,18 +316,15 @@ async function preprocessSVGForPDFLib(svg: SVGSVGElement): Promise<SVGSVGElement
use.parentNode.replaceChild(newImage, use);
}
}
symbol.remove();
}

// Remove defs and symbols
const defs = clone.querySelector('defs');
if (defs) defs.remove();

return clone;
}

export async function exportToPDF({
SVG,
scale = { fitToPage: true, zoom: 1 },
scale = { fitToPage: 1, zoom: 1 },
pageProps,
}: {
SVG: SVGSVGElement[];
Expand Down
7 changes: 3 additions & 4 deletions src/view/ExcalidrawView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -589,10 +589,9 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{

const pdfArrayBuffer = await exportToPDF({
SVG: [svg],
scale: {
...this.exportDialog.fitToPage
? { fitToPage: true }
: { zoom: this.exportDialog.scale, fitToPage: false },
scale: {
zoom: this.exportDialog.scale,
fitToPage: this.exportDialog.fitToPage
},
pageProps: {
dimensions: getPageDimensions(pageSize, orientation),
Expand Down

0 comments on commit b18637f

Please sign in to comment.