diff --git a/src/lang/locale/en.ts b/src/lang/locale/en.ts index 02d44e5..a765c34 100644 --- a/src/lang/locale/en.ts +++ b/src/lang/locale/en.ts @@ -1050,11 +1050,30 @@ FILENAME_HEAD: "Filename", EXPORTDIALOG_PAGE_ORIENTATION: "Orientation", EXPORTDIALOG_ORIENTATION_PORTRAIT: "Portrait", EXPORTDIALOG_ORIENTATION_LANDSCAPE: "Landscape", + EXPORTDIALOG_PDF_FIT_TO_PAGE: "Page Fitting", + EXPORTDIALOG_PDF_FIT_OPTION: "Fit to page", + EXPORTDIALOG_PDF_SCALE_OPTION: "Use image scale (may span multiple pages)", + EXPORTDIALOG_PDF_PAPER_COLOR: "Paper Color", + EXPORTDIALOG_PDF_PAPER_WHITE: "White", + EXPORTDIALOG_PDF_PAPER_SCENE: "Use scene color", + EXPORTDIALOG_PDF_PAPER_CUSTOM: "Custom color", + EXPORTDIALOG_PDF_ALIGNMENT: "Position on Page", + EXPORTDIALOG_PDF_ALIGN_CENTER: "Center", + EXPORTDIALOG_PDF_ALIGN_TOP_LEFT: "Top Left", + EXPORTDIALOG_PDF_ALIGN_TOP_CENTER: "Top Center", + EXPORTDIALOG_PDF_ALIGN_TOP_RIGHT: "Top Right", + EXPORTDIALOG_PDF_ALIGN_BOTTOM_LEFT: "Bottom Left", + EXPORTDIALOG_PDF_ALIGN_BOTTOM_CENTER: "Bottom Center", + EXPORTDIALOG_PDF_ALIGN_BOTTOM_RIGHT: "Bottom Right", + EXPORTDIALOG_PDF_MARGIN: "Margin", + EXPORTDIALOG_PDF_MARGIN_NONE: "None", + EXPORTDIALOG_PDF_MARGIN_TINY: "Small", + EXPORTDIALOG_PDF_MARGIN_NORMAL: "Normal", // Buttons EXPORTDIALOG_PNGTOFILE : "PNT to File", EXPORTDIALOG_SVGTOFILE : "SVG to File", EXPORTDIALOG_EXCALIDRAW: "Excalidraw", EXPORTDIALOG_PNGTOCLIPBOARD : "PNG to Clipboard", - EXPORTDIALOG_PDF: "Export PDF", - EXPORTDIALOG_PDFTOVAULT: "Save PDF to Vault", + EXPORTDIALOG_PDF: "Export", + EXPORTDIALOG_PDFTOVAULT: "Save to Vault", }; diff --git a/src/shared/Dialogs/ExportDialog.ts b/src/shared/Dialogs/ExportDialog.ts index bee237e..771760e 100644 --- a/src/shared/Dialogs/ExportDialog.ts +++ b/src/shared/Dialogs/ExportDialog.ts @@ -6,9 +6,11 @@ import { ExcalidrawAutomate } from "src/shared/ExcalidrawAutomate"; import ExcalidrawView from "src/view/ExcalidrawView"; import ExcalidrawPlugin from "src/core/main"; import { fragWithHTML, getExportPadding, getExportTheme, getPNGScale, getWithBackground, shouldEmbedScene } from "src/utils/utils"; -import { PageOrientation, PageSize, STANDARD_PAGE_SIZES } from "src/utils/exportUtils"; +import { PageOrientation, PageSize, PDFMargin, PDFPageAlignment, PDFPageMarginString, STANDARD_PAGE_SIZES } from "src/utils/exportUtils"; import { t } from "src/lang/helpers"; + + export class ExportDialog extends Modal { private ea: ExcalidrawAutomate; private api: ExcalidrawImperativeAPI; @@ -34,6 +36,11 @@ export class ExportDialog extends Modal { private activeTab: "image" | "pdf" = "image"; private contentContainer: HTMLDivElement; private buttonContainer: HTMLDivElement; + public fitToPage: boolean = true; + public paperColor: "white" | "scene" | "custom" = "white"; + public customPaperColor: string = "#ffffff"; + public alignment: PDFPageAlignment = "center"; + public margin: PDFPageMarginString = "normal"; constructor( private plugin: ExcalidrawPlugin, @@ -296,6 +303,82 @@ export class ExportDialog extends Modal { this.pageOrientation = value as PageOrientation; }) ); + + new Setting(this.contentContainer) + .setName(t("EXPORTDIALOG_PDF_FIT_TO_PAGE")) + .addDropdown(dropdown => + dropdown + .addOptions({ + "fit": t("EXPORTDIALOG_PDF_FIT_OPTION"), + "scale": t("EXPORTDIALOG_PDF_SCALE_OPTION") + }) + .setValue(this.fitToPage ? "fit" : "scale") + .onChange(value => { + this.fitToPage = value === "fit"; + }) + ); + + new Setting(this.contentContainer) + .setName(t("EXPORTDIALOG_PDF_MARGIN")) + .addDropdown(dropdown => + dropdown + .addOptions({ + "none": t("EXPORTDIALOG_PDF_MARGIN_NONE"), + "tiny": t("EXPORTDIALOG_PDF_MARGIN_TINY"), + "normal": t("EXPORTDIALOG_PDF_MARGIN_NORMAL") + }) + .setValue(this.margin) + .onChange(value => { + this.margin = value as typeof this.margin; + }) + ); + + + const paperColorSetting = new Setting(this.contentContainer) + .setName(t("EXPORTDIALOG_PDF_PAPER_COLOR")) + .addDropdown(dropdown => + dropdown + .addOptions({ + "white": t("EXPORTDIALOG_PDF_PAPER_WHITE"), + "scene": t("EXPORTDIALOG_PDF_PAPER_SCENE"), + "custom": t("EXPORTDIALOG_PDF_PAPER_CUSTOM") + }) + .setValue(this.paperColor) + .onChange(value => { + this.paperColor = value as typeof this.paperColor; + colorInput.style.display = (value === "custom") ? "block" : "none"; + }) + ); + + const colorInput = paperColorSetting.controlEl.createEl("input", { + type: "color", + value: this.customPaperColor + }); + colorInput.style.width = "50px"; + colorInput.style.marginLeft = "10px"; + colorInput.style.display = this.paperColor === "custom" ? "block" : "none"; + colorInput.addEventListener("change", (e) => { + this.customPaperColor = (e.target as HTMLInputElement).value; + }); + + new Setting(this.contentContainer) + .setName(t("EXPORTDIALOG_PDF_ALIGNMENT")) + .addDropdown(dropdown => + dropdown + .addOptions({ + "center": t("EXPORTDIALOG_PDF_ALIGN_CENTER"), + "top-left": t("EXPORTDIALOG_PDF_ALIGN_TOP_LEFT"), + "top-center": t("EXPORTDIALOG_PDF_ALIGN_TOP_CENTER"), + "top-right": t("EXPORTDIALOG_PDF_ALIGN_TOP_RIGHT"), + "bottom-left": t("EXPORTDIALOG_PDF_ALIGN_BOTTOM_LEFT"), + "bottom-center": t("EXPORTDIALOG_PDF_ALIGN_BOTTOM_CENTER"), + "bottom-right": t("EXPORTDIALOG_PDF_ALIGN_BOTTOM_RIGHT") + }) + .setValue(this.alignment) + .onChange(value => { + this.alignment = value as typeof this.alignment; + }) + ); } private createImageButtons() { @@ -328,13 +411,28 @@ export class ExportDialog extends Modal { } private createPDFButton() { + const bPDFVault = this.buttonContainer.createEl("button", { + text: t("EXPORTDIALOG_PDFTOVAULT"), + cls: "excalidraw-prompt-button" + }); + bPDFVault.onclick = () => { + this.view.exportPDF( + true, + this.hasSelectedElements && this.exportSelectedOnly, + this.pageSize, + this.pageOrientation + ); + this.close(); + }; + if (!DEVICE.isDesktop) return; - const bPDF = this.buttonContainer.createEl("button", { + const bPDFExport = this.buttonContainer.createEl("button", { text: t("EXPORTDIALOG_PDF"), cls: "excalidraw-prompt-button" }); - bPDF.onclick = () => { + bPDFExport.onclick = () => { this.view.exportPDF( + false, this.hasSelectedElements && this.exportSelectedOnly, this.pageSize, this.pageOrientation @@ -342,4 +440,13 @@ export class ExportDialog extends Modal { this.close(); }; } + + public getPaperColor(): string { + switch (this.paperColor) { + case "white": return "#ffffff"; + case "scene": return this.api.getAppState().viewBackgroundColor; + case "custom": return this.customPaperColor; + default: return "#ffffff"; + } + } } diff --git a/src/utils/exportUtils.ts b/src/utils/exportUtils.ts index 0e37fc0..8eb244f 100644 --- a/src/utils/exportUtils.ts +++ b/src/utils/exportUtils.ts @@ -1,21 +1,27 @@ import { PDFDocument, rgb } from '@cantoo/pdf-lib'; import { getEA } from 'src/core'; -import { svgToBase64 } from './utils'; + + +export type PDFPageAlignment = "center" | "top-left" | "top-center" | "top-right" | "bottom-left" | "bottom-center" | "bottom-right"; +export type PDFPageMarginString = "none" | "tiny" | "normal"; interface PDFExportScale { fitToPage: boolean; zoom?: number; } +export interface PDFMargin { + left: number; + right: number; + top: number; + bottom: number; +} + interface PDFPageProperties { dimensions?: {width: number; height: number}; backgroundColor?: string; - margin?: { - left: number; - right: number; - top: number; - bottom: number; - }; + margin: PDFMargin; + alignment: PDFPageAlignment; } interface PageDimensions { @@ -40,6 +46,15 @@ export const STANDARD_PAGE_SIZES = { export type PageSize = keyof typeof STANDARD_PAGE_SIZES; +export function getMarginValue(margin:PDFPageMarginString): PDFMargin { + switch(margin) { + case "none": return { left: 0, right: 0, top: 0, bottom: 0 }; + case "tiny": return { left: 5, right: 5, top: 5, bottom: 5 }; + case "normal": return { left: 20, right: 20, top: 20, bottom: 20 }; + default: return { left: 20, right: 20, top: 20, bottom: 20 }; + } +} + export function getPageDimensions(pageSize: PageSize, orientation: PageOrientation): PageDimensions { const dimensions = STANDARD_PAGE_SIZES[pageSize]; return orientation === "portrait" @@ -47,8 +62,6 @@ export function getPageDimensions(pageSize: PageSize, orientation: PageOrientati : { width: dimensions.height, height: dimensions.width }; } -const DEFAULT_MARGIN = 20; // 20pt margins - interface SVGDimensions { width: number; height: number; @@ -117,20 +130,15 @@ async function addSVGToPage( export async function exportToPDF({ SVG, scale = { fitToPage: true, zoom: 1 }, - pageProps = {} + pageProps, }: { SVG: SVGSVGElement[]; - scale?: PDFExportScale; - pageProps?: PDFPageProperties; + scale: PDFExportScale; + pageProps: PDFPageProperties; }): Promise { - const margin = pageProps.margin ?? { - left: DEFAULT_MARGIN, - right: DEFAULT_MARGIN, - top: DEFAULT_MARGIN, - bottom: DEFAULT_MARGIN - }; - - const pageDim = pageProps.dimensions ?? getPageDimensions("A4", "portrait"); + const margin = pageProps.margin; + const pageDim = pageProps.dimensions; + const pdfDoc = await PDFDocument.create(); for (const svg of SVG) { diff --git a/src/utils/fileUtils.ts b/src/utils/fileUtils.ts index d81d5b3..3038298 100644 --- a/src/utils/fileUtils.ts +++ b/src/utils/fileUtils.ts @@ -493,3 +493,22 @@ export const hasExcalidrawEmbeddedImagesTreeChanged = (sourceFile: TFile, mtime: const fileList = getExcalidrawEmbeddedFilesFiletree(sourceFile, plugin); return fileList.some(f=>f.stat.mtime > mtime); } + +export async function createOrOverwriteFile(app: App, path: string, content: string | ArrayBuffer): Promise { + const file = app.vault.getAbstractFileByPath(normalizePath(path)); + if(content instanceof ArrayBuffer) { + if(file && file instanceof TFile) { + await app.vault.modifyBinary(file, content); + return file; + } else { + return await app.vault.createBinary(path, content); + } + } + + if (file && file instanceof TFile) { + await app.vault.modify(file, content); + return file; + } else { + return await app.vault.create(path, content); + } +} \ No newline at end of file diff --git a/src/view/ExcalidrawView.ts b/src/view/ExcalidrawView.ts index 368cebd..c68a2ac 100644 --- a/src/view/ExcalidrawView.ts +++ b/src/view/ExcalidrawView.ts @@ -78,6 +78,7 @@ import { import { arrayBufferToBase64, checkAndCreateFolder, + createOrOverwriteFile, download, getDataURLFromURL, getIMGFilename, @@ -150,7 +151,8 @@ import { getPDFCropRect } from "../utils/PDFUtils"; import { Position, ViewSemaphores } from "../types/excalidrawViewTypes"; import { DropManager } from "./managers/DropManager"; import { ImageInfo } from "src/types/excalidrawAutomateTypes"; -import { exportToPDF, getPageDimensions, PageOrientation, PageSize } from "src/utils/exportUtils"; +import { exportToPDF, getMarginValue, getPageDimensions, PageOrientation, PageSize } from "src/utils/exportUtils"; +import { create } from "domain"; const EMBEDDABLE_SEMAPHORE_TIMEOUT = 2000; const PREVENT_RELOAD_TIMEOUT = 2000; @@ -517,19 +519,13 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{ } const exportImage = async (filepath:string, theme?:string) => { - const file = this.app.vault.getAbstractFileByPath(normalizePath(filepath)); - const svg = await this.svg(scene,theme, embedScene, true); if (!svg) { return; } //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/2026 const svgString = svg.outerHTML; - if (file && file instanceof TFile) { - await this.app.vault.modify(file, svgString); - } else { - await this.app.vault.create(filepath, svgString); - } + await createOrOverwriteFile(this.app, filepath, svgString); } if(this.plugin.settings.autoExportLightAndDark) { @@ -558,6 +554,7 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{ } public async exportPDF( + toVault: boolean, selectedOnly?: boolean, pageSize: PageSize = "A4", orientation: PageOrientation = "portrait" @@ -579,16 +576,16 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{ const pdfArrayBuffer = await exportToPDF({ SVG: [svg], - scale: { fitToPage: true }, + scale: { + ...this.exportDialog.fitToPage + ? { fitToPage: true } + : { zoom: this.exportDialog.scale, fitToPage: false }, + }, pageProps: { dimensions: getPageDimensions(pageSize, orientation), - backgroundColor: this.exportDialog.transparent ? undefined : "#ffffff", - margin: { - top: 20, - left: 20, - right: 20, - bottom: 20 - } + backgroundColor: this.exportDialog.getPaperColor(), + margin: getMarginValue(this.exportDialog.margin), + alignment: this.exportDialog.alignment, } }); @@ -596,11 +593,17 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{ return; } - download( - "data:application/pdf;base64", - arrayBufferToBase64(pdfArrayBuffer), - `${this.file.basename}.pdf` - ); + if(toVault) { + const filepath = getIMGFilename(this.file.path, "pdf"); + const file = await createOrOverwriteFile(this.app, filepath, pdfArrayBuffer); + this.app.workspace.getLeaf("split").openFile(file); + } else { + download( + "data:application/pdf;base64", + arrayBufferToBase64(pdfArrayBuffer), + `${this.file.basename}.pdf` + ); + } } public async png(scene: any, theme?:string, embedScene?: boolean): Promise { @@ -646,17 +649,11 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{ } const exportImage = async (filepath:string, theme?:string) => { - const file = this.app.vault.getAbstractFileByPath(normalizePath(filepath)); - const png = await this.png(scene, theme, embedScene); if (!png) { return; } - if (file && file instanceof TFile) { - await this.app.vault.modifyBinary(file, await png.arrayBuffer()); - } else { - await this.app.vault.createBinary(filepath, await png.arrayBuffer()); - } + await createOrOverwriteFile(this.app, filepath, await png.arrayBuffer()); } if(this.plugin.settings.autoExportLightAndDark) {