diff --git a/package.json b/package.json index 2361d7df..cddb71da 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "license": "MIT", "dependencies": { "@popperjs/core": "^2.11.8", - "@zsviczian/excalidraw": "0.17.6-27", + "@zsviczian/excalidraw": "0.17.6-28", "chroma-js": "^2.4.2", "clsx": "^2.0.0", "@zsviczian/colormaster": "^1.2.2", diff --git a/src/shared/Dialogs/ExportDialog.ts b/src/shared/Dialogs/ExportDialog.ts index 9cadbf9d..6a63b5bc 100644 --- a/src/shared/Dialogs/ExportDialog.ts +++ b/src/shared/Dialogs/ExportDialog.ts @@ -391,7 +391,6 @@ export class ExportDialog extends Modal { }); bPDFExport.onclick = () => { this.view.exportPDF( - false, this.hasSelectedElements && this.exportSelectedOnly, this.pageSize, this.pageOrientation @@ -402,7 +401,7 @@ export class ExportDialog extends Modal { public getPaperColor(): string { switch (this.paperColor) { - case "white": return "#ffffff"; + case "white": return this.theme === "light" ? "#ffffff" : "#000000"; case "scene": return this.api.getAppState().viewBackgroundColor; case "custom": return this.customPaperColor; default: return "#ffffff"; diff --git a/src/shared/Dialogs/SuggesterInfo.ts b/src/shared/Dialogs/SuggesterInfo.ts index 9a11c6cd..f294f5f4 100644 --- a/src/shared/Dialogs/SuggesterInfo.ts +++ b/src/shared/Dialogs/SuggesterInfo.ts @@ -226,14 +226,14 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [ }, { field: "createPDF", - code: "async createPDF({SVG: SVGSVGElement[], scale?: PDFExportScale, pageProps?: PDFPageProperties}): Promise", - desc: "", - after: "Creates a PDF from the provided SVG elements with specified scaling and page properties.\n" + + code: "async createPDF({SVG: SVGSVGElement[], scale?: PDFExportScale, pageProps?: PDFPageProperties, filename: string}): Promise", + desc: "Creates a PDF from the provided SVG elements with specified scaling and page properties.\n" + "\n" + "@param {Object} params - The parameters for creating the PDF.\n" + "@param {SVGSVGElement[]} params.SVG - An array of SVG elements to be included in the PDF. If multiple SVGs are provided, each will be added to a new page.\n" + "@param {PDFExportScale} [params.scale={ fitToPage: true, zoom: 1 }] - The scaling options for the SVG elements.\n" + "@param {PDFPageProperties} [params.pageProps] - The properties for the PDF pages.\n" + + "@param {string} params.filename - The name of the PDF file to be created.\n" + "@returns {Promise} - A promise that resolves to an ArrayBuffer containing the PDF data.\n" + "\n" + "@typedef {Object} PDFExportScale\n" + @@ -244,10 +244,8 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [ "@property {{width: number, height: number}} [dimensions] - The dimensions of the PDF pages in pixels. Use getPageDimensions to get standard page sizes.\n" + "@property {string} [backgroundColor] - The background color of the PDF pages.\n" + "@property {PDFMargin} margin - The margins of the PDF pages in pixels.\n" + - "@property {PDFPageAlignment} alignment - The alignment of the SVG on the PDF pages.\n" + - "\n" + - "@example\n" + - "const pdfData = await createPDF({\n" + + "@property {PDFPageAlignment} alignment - The alignment of the SVG on the PDF pages.", + after: "({\n" + " SVG: [svgElement1, svgElement2],\n" + " scale: { fitToPage: true },\n" + " pageProps: {\n" + @@ -255,9 +253,40 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [ " backgroundColor: \"#ffffff\",\n" + " margin: { left: 20, right: 20, top: 20, bottom: 20 },\n" + " alignment: \"center\"\n" + + " filename: \"myPDF.pdf\"\n" + " }\n" + "});", }, + { + field: "createViewSVG", + code: "async createViewSVG({withBackground?: boolean, theme?: 'light' | 'dark', frameRendering?: FrameRenderingOptions, padding?: number, selectedOnly?: boolean, skipInliningFonts?: boolean, embedScene?: boolean}): Promise", + desc: "Creates an SVG representation of the current view with specified options.\n" + + "\n" + + "@param {Object} options - The options for creating the SVG.\n" + + "@param {boolean} [options.withBackground=true] - Whether to include the background in the SVG.\n" + + "@param {\"light\" | \"dark\"} [options.theme] - The theme to use for the SVG.\n" + + "@param {FrameRenderingOptions} [options.frameRendering={enabled: true, name: true, outline: true, clip: true}] - The frame rendering options.\n" + + "@param {number} [options.padding] - The padding to apply around the SVG.\n" + + "@param {boolean} [options.selectedOnly=false] - Whether to include only the selected elements in the SVG.\n" + + "@param {boolean} [options.skipInliningFonts=false] - Whether to skip inlining fonts in the SVG.\n" + + "@param {boolean} [options.embedScene=false] - Whether to embed the scene in the SVG.\n" + + "@returns {Promise} A promise that resolves to the SVG element.\n" + + "\n" + + "@typedef {Object} FrameRenderingOptions\n" + + "@property {boolean} enabled - Whether frame rendering is enabled.\n" + + "@property {boolean} name - Whether to include the name in the frame rendering.\n" + + "@property {boolean} outline - Whether to include the outline in the frame rendering.\n" + + "@property {boolean} clip - Whether to clip the frame rendering.\n", + after: "({\n" + + " withBackground: true,\n" + + " theme: 'light',\n" + + " frameRendering: { enabled: true, name: true, outline: true, clip: true },\n" + + " padding: 10,\n" + + " selectedOnly: false,\n" + + " skipInliningFonts: false,\n" + + " embedScene: false,\n" + + "});", + }, { field: "getPagePDFDimensions", code: "getPagePDFDimensions(pageSize: PageSize, orientation: PageOrientation): PageDimensions", @@ -273,12 +302,8 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [ "\n" + "@typedef {\"A0\" | \"A1\" | \"A2\" | \"A3\" | \"A4\" | \"A5\" | \"Letter\" | \"Legal\" | \"Tabloid\"} PageSize\n" + "\n" + - "@typedef {\"portrait\" | \"landscape\"} PageOrientation\n" + - "\n" + - "@example\n" + - "const dimensions = getPDFPageDimensions(\"A4\", \"portrait\");\n" + - "console.log(dimensions); // { width: 595.28, height: 841.89 }", - after: "", + "@typedef {\"portrait\" | \"landscape\"} PageOrientation", + after: "(\"A4\", \"portrait\");", }, { field: "createPNG", diff --git a/src/shared/ExcalidrawAutomate.ts b/src/shared/ExcalidrawAutomate.ts index 37cdcee3..94897ca5 100644 --- a/src/shared/ExcalidrawAutomate.ts +++ b/src/shared/ExcalidrawAutomate.ts @@ -42,6 +42,8 @@ import { wrapTextAtCharLength, arrayToMap, addAppendUpdateCustomData, + getSVG, + getWithBackground, } from "src/utils/utils"; import { getAttachmentsFolderAndFilePath, getExcalidrawViews, getLeaf, getNewOrAdjacentLeaf, isObsidianThemeDark, mergeMarkdownFiles, openLeaf } from "src/utils/obsidianUtils"; import { AppState, BinaryFileData, DataURL, ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/excalidraw/types"; @@ -84,6 +86,7 @@ import { GlobalPoint } from "@zsviczian/excalidraw/types/math/types"; import { AddImageOptions, ImageInfo, SVGColorInfo } from "src/types/excalidrawAutomateTypes"; import { _measureText, cloneElement, createPNG, createSVG, errorMessage, filterColorMap, getEmbeddedFileForImageElment, getFontFamily, getLineBox, getTemplate, isColorStringTransparent, isSVGColorInfo, mergeColorMapIntoSVGColorInfo, normalizeLinePoints, repositionElementsToCursor, svgColorInfoToColorMap, updateOrAddSVGColorInfo, verifyMinimumPluginVersion } from "src/utils/excalidrawAutomateUtils"; import { exportToPDF, getMarginValue, getPageDimensions, PageDimensions, PageOrientation, PageSize, PDFExportScale, PDFPageProperties } from "src/utils/exportUtils"; +import { FrameRenderingOptions } from "src/types/utilTypes"; extendPlugins([ HarmonyPlugin, @@ -969,6 +972,7 @@ export class ExcalidrawAutomate { * margin: { left: 20, right: 20, top: 20, bottom: 20 }, * alignment: "center", * } + * filename: "example.pdf", * }); */ async createPDF({ @@ -1002,6 +1006,68 @@ export class ExcalidrawAutomate { await exportToPDF({SVG, scale, pageProps, filename}); } + /** + * Creates an SVG representation of the current view. + * + * @param {Object} options - The options for creating the SVG. + * @param {boolean} [options.withBackground=true] - Whether to include the background in the SVG. + * @param {"light" | "dark"} [options.theme] - The theme to use for the SVG. + * @param {FrameRenderingOptions} [options.frameRendering={enabled: true, name: true, outline: true, clip: true}] - The frame rendering options. + * @param {number} [options.padding] - The padding to apply around the SVG. + * @param {boolean} [options.selectedOnly=false] - Whether to include only the selected elements in the SVG. + * @param {boolean} [options.skipInliningFonts=false] - Whether to skip inlining fonts in the SVG. + * @param {boolean} [options.embedScene=false] - Whether to embed the scene in the SVG. + * @returns {Promise} A promise that resolves to the SVG element. + */ + async createViewSVG({ + withBackground = true, + theme, + frameRendering = {enabled: true, name: true, outline: true, clip: true}, + padding, + selectedOnly = false, + skipInliningFonts = false, + embedScene = false, + } : { + withBackground?: boolean, + theme?: "light" | "dark", + frameRendering?: FrameRenderingOptions, + padding?: number, + selectedOnly?: boolean, + skipInliningFonts?: boolean, + embedScene?: boolean, + }): Promise { + if(!this.targetView || !this.targetView.file || !this.targetView._loaded) { + console.log("No view loaded"); + return; + } + const view = this.targetView; + const scene = this.targetView.getScene(selectedOnly); + + const exportSettings: ExportSettings = { + withBackground: view.getViewExportWithBackground(withBackground), + withTheme: true, + isMask: isMaskFile(this.plugin, view.file), + skipInliningFonts, + frameRendering, + }; + + return await getSVG( + { + ...scene, + ...{ + appState: { + ...scene.appState, + theme: view.getViewExportTheme(theme), + exportEmbedScene: view.getViewExportEmbedScene(embedScene), + }, + }, + }, + exportSettings, + view.getViewExportPadding(padding), + view.file, + ); + } + /** * Creates an SVG image from the ExcalidrawAutomate elements and the template provided. * @param {string} [templatePath] - The template path to use for the SVG. diff --git a/src/types/utilTypes.ts b/src/types/utilTypes.ts index 0b33aafe..e6fd93a6 100644 --- a/src/types/utilTypes.ts +++ b/src/types/utilTypes.ts @@ -17,4 +17,11 @@ export enum PreviewImageType { PNG = "PNG", SVGIMG = "SVGIMG", SVG = "SVG" +} + +export interface FrameRenderingOptions { + enabled: boolean; + name: boolean; + outline: boolean; + clip: boolean; } \ No newline at end of file diff --git a/src/utils/exportUtils.ts b/src/utils/exportUtils.ts index 0c670845..e41882ff 100644 --- a/src/utils/exportUtils.ts +++ b/src/utils/exportUtils.ts @@ -203,6 +203,7 @@ async function printPdf( } function calculateDimensions( + svg: SVGSVGElement, svgWidth: number, svgHeight: number, pageDim: PageDimensions, @@ -219,6 +220,9 @@ function calculateDimensions( }[], pages: number } { + const viewBox = svg.getAttribute('viewBox')?.split(' ').map(Number) || [0, 0, svgWidth, svgHeight]; + const [viewBoxX, viewBoxY] = viewBox; + const availableWidth = pageDim.width - margin.left - margin.right; const availableHeight = pageDim.height - margin.top - margin.bottom; @@ -262,7 +266,7 @@ function calculateDimensions( return { tiles: [{ - viewBox: `0 0 ${svgWidth} ${svgHeight}`, + viewBox: `${viewBoxX} ${viewBoxY} ${svgWidth} ${svgHeight}`, width: finalWidth, height: finalHeight, x: position.x, @@ -297,7 +301,7 @@ function calculateDimensions( ); tiles.push({ - viewBox: `${tileX} ${tileY} ${tileWidth} ${tileHeight}`, + viewBox: `${tileX + viewBoxX} ${tileY + viewBoxY} ${tileWidth} ${tileHeight}`, width: scaledTileWidth, height: scaledTileHeight, x: position.x, @@ -364,12 +368,13 @@ export async function exportToPDF({ allPagesDiv.style.width = "100%"; allPagesDiv.style.height = "fit-content"; - let j = 1; + let j = 0; for (const svg of SVG) { const svgWidth = parseFloat(svg.getAttribute('width') || '0'); const svgHeight = parseFloat(svg.getAttribute('height') || '0'); const {tiles} = calculateDimensions( + svg, svgWidth, svgHeight, pageProps.dimensions, @@ -378,7 +383,7 @@ export async function exportToPDF({ pageProps.alignment ); - let i = 1; + let i = 0; for (const tile of tiles) { const pageDiv = createDiv(); pageDiv.style.width = `${width}px`; @@ -394,7 +399,7 @@ export async function exportToPDF({ clonedSVG.style.height = `${tile.height}px`; clonedSVG.style.position = 'absolute'; clonedSVG.style.left = `${tile.x}px`; - clonedSVG.style.top = `${tile.y + (i-1)*height}px`; + clonedSVG.style.top = `${tile.y + (i+j)*height}px`; pageDiv.appendChild(clonedSVG); allPagesDiv.appendChild(pageDiv); diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 6339e561..6dd7a249 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -307,7 +307,7 @@ export async function getSVG ( : {}, }, files: scene.files, - exportPadding: exportSettings.frameRendering ? 0 : padding, + exportPadding: exportSettings.frameRendering?.enabled ? 0 : padding, exportingFrame: null, renderEmbeddables: true, skipInliningFonts: exportSettings.skipInliningFonts, @@ -365,7 +365,7 @@ export async function getPNG ( : {}, }, files: filterFiles(scene.files), - exportPadding: exportSettings.frameRendering ? 0 : padding, + exportPadding: exportSettings.frameRendering.enabled ? 0 : padding, mimeType: "image/png", getDimensions: (width: number, height: number) => ({ width: width * scale, diff --git a/src/view/ExcalidrawView.ts b/src/view/ExcalidrawView.ts index 75d9298b..b1927b7b 100644 --- a/src/view/ExcalidrawView.ts +++ b/src/view/ExcalidrawView.ts @@ -153,6 +153,7 @@ import { DropManager } from "./managers/DropManager"; import { ImageInfo } from "src/types/excalidrawAutomateTypes"; import { exportToPDF, getMarginValue, getPageDimensions, PageOrientation, PageSize } from "src/utils/exportUtils"; import { create } from "domain"; +import { FrameRenderingOptions } from "src/types/utilTypes"; const EMBEDDABLE_SEMAPHORE_TIMEOUT = 2000; const PREVENT_RELOAD_TIMEOUT = 2000; @@ -186,12 +187,7 @@ export interface ExportSettings { withBackground: boolean; withTheme: boolean; isMask: boolean; - frameRendering?: { //optional, overrides relevant appState settings for rendering the frame - enabled: boolean; - name: boolean; - outline: boolean; - clip: boolean; - }; + frameRendering?: FrameRenderingOptions; //optional, overrides relevant appState settings for rendering the frame skipInliningFonts?: boolean; } @@ -475,36 +471,75 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{ ); } + public getViewExportTheme(theme?:string):string { + if(theme) return theme; + if(!this.exportDialog) { + this.exportDialog = new ExportDialog(this.plugin, this,this.file); + } + const ed = this.exportDialog; + return ed ? ed.theme : getExportTheme(this.plugin, this.file, this.excalidrawAPI.getAppState().theme) + } + + public getViewExportEmbedScene(embedScene?:boolean):boolean { + if(!this.exportDialog) { + this.exportDialog = new ExportDialog(this.plugin, this,this.file); + } + const ed = this.exportDialog; + return typeof embedScene === "undefined" + ? (ed ? ed.embedScene : false) + : embedScene; + } + + public getViewExportPadding(padding?: number): number { + if(typeof padding !== "undefined") return padding; + if(!this.exportDialog) { + this.exportDialog = new ExportDialog(this.plugin, this,this.file); + } + const ed = this.exportDialog; + return ed ? ed.padding : getExportPadding(this.plugin, this.file) + } + + public getViewExportScale(scale?: number): number { + if(typeof scale !== "undefined") return scale; + if(!this.exportDialog) { + this.exportDialog = new ExportDialog(this.plugin, this,this.file); + } + const ed = this.exportDialog; + return ed ? ed.scale : getPNGScale(this.plugin, this.file); + } + + public getViewExportWithBackground(withBackground?:boolean) { + if(typeof withBackground !== "undefined") return withBackground; + if(!this.exportDialog) { + this.exportDialog = new ExportDialog(this.plugin, this,this.file); + } + const ed = this.exportDialog; + return ed ? !ed.transparent : getWithBackground(this.plugin, this.file) + } + public async svg(scene: any, theme?:string, embedScene?: boolean, embedFont: boolean = false): Promise { (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.svg, "ExcalidrawView.svg", scene, theme, embedScene); - const ed = this.exportDialog; const exportSettings: ExportSettings = { - withBackground: ed ? !ed.transparent : getWithBackground(this.plugin, this.file), + withBackground: this.getViewExportWithBackground(), withTheme: true, isMask: isMaskFile(this.plugin, this.file), skipInliningFonts: !embedFont, }; - if(typeof embedScene === "undefined") { - embedScene = shouldEmbedScene(this.plugin, this.file); - } - return await getSVG( { ...scene, ...{ appState: { ...scene.appState, - theme: theme ?? (ed ? ed.theme : getExportTheme(this.plugin, this.file, scene.appState.theme)), - exportEmbedScene: typeof embedScene === "undefined" - ? (ed ? ed.embedScene : false) - : embedScene, + theme: this.getViewExportTheme(theme), + exportEmbedScene: this.getViewExportEmbedScene(embedScene), }, }, }, exportSettings, - ed ? ed.padding : getExportPadding(this.plugin, this.file), + this.getViewExportPadding(), this.file, ); } @@ -567,7 +602,6 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{ } public async exportPDF( - toVault: boolean, selectedOnly?: boolean, pageSize: PageSize = "A4", orientation: PageOrientation = "portrait" @@ -605,34 +639,27 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{ public async png(scene: any, theme?:string, embedScene?: boolean): Promise { (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.png, "ExcalidrawView.png", scene, theme, embedScene); - const ed = this.exportDialog; const exportSettings: ExportSettings = { - withBackground: ed ? !ed.transparent : getWithBackground(this.plugin, this.file), + withBackground: this.getViewExportWithBackground(), withTheme: true, isMask: isMaskFile(this.plugin, this.file), }; - if(typeof embedScene === "undefined") { - embedScene = shouldEmbedScene(this.plugin, this.file); - } - return await getPNG( { ...scene, ...{ appState: { ...scene.appState, - theme: theme ?? (ed ? ed.theme : getExportTheme(this.plugin, this.file, scene.appState.theme)), - exportEmbedScene: typeof embedScene === "undefined" - ? (ed ? ed.embedScene : false) - : embedScene, + theme: this.getViewExportTheme(theme), + exportEmbedScene: this.getViewExportEmbedScene(embedScene), }, }, }, exportSettings, - ed ? ed.padding : getExportPadding(this.plugin, this.file), - ed ? ed.scale : getPNGScale(this.plugin, this.file), + this.getViewExportPadding(), + this.getViewExportScale(), ); }