diff --git a/manifest-beta.json b/manifest-beta.json index 1e7f610..cef8625 100644 --- a/manifest-beta.json +++ b/manifest-beta.json @@ -1,7 +1,7 @@ { "id": "obsidian-excalidraw-plugin", "name": "Excalidraw", - "version": "2.10.2-beta-1", + "version": "2.11.0-beta-1", "minAppVersion": "1.1.6", "description": "An Obsidian plugin to edit and view Excalidraw drawings", "author": "Zsolt Viczian", diff --git a/package.json b/package.json index 4ad072e..6e69838 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "license": "MIT", "dependencies": { "@popperjs/core": "^2.11.8", - "@zsviczian/excalidraw": "0.18.0-9", + "@zsviczian/excalidraw": "0.18.0-12", "chroma-js": "^2.4.2", "clsx": "^2.0.0", "@zsviczian/colormaster": "^1.2.2", diff --git a/src/lang/locale/en.ts b/src/lang/locale/en.ts index 9d4070a..840c91c 100644 --- a/src/lang/locale/en.ts +++ b/src/lang/locale/en.ts @@ -1100,6 +1100,15 @@ FILENAME_HEAD: "Filename", EXPORTDIALOG_PDF_PROGRESS_DONE: "Export complete", EXPORTDIALOG_PDF_PROGRESS_ERROR: "Error exporting PDF, check developer console for details", + // Screenshot tab + EXPORTDIALOG_TAB_SCREENSHOT: "Screenshot", + EXPORTDIALOG_SCREENSHOT_DESC: "Screenshots will include embeddables such as markdown pages, YouTube, websites, etc. They are only available on desktop, cannot be automatically exported, and only support PNG format.", + SCREENSHOT_DESKTOP_ONLY: "Screenshot feature is only available on desktop", + SCREENSHOT_FILE_SUCCESS: "Screenshot saved to vault", + SCREENSHOT_CLIPBOARD_SUCCESS: "Screenshot copied to clipboard", + SCREENSHOT_CLIPBOARD_ERROR: "Failed to copy screenshot to clipboard: ", + SCREENSHOT_ERROR: "Error capturing screenshot - see console log", + //exportUtils.ts PDF_EXPORT_DESKTOP_ONLY: "PDF export is only available on desktop", }; diff --git a/src/shared/Dialogs/ExportDialog.ts b/src/shared/Dialogs/ExportDialog.ts index 52a6d7b..778cef5 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, PDFPageAlignment, PDFPageMarginString, exportSVGToClipboard } from "src/utils/exportUtils"; +import { PageOrientation, PageSize, PDFPageAlignment, PDFPageMarginString, exportSVGToClipboard, exportPNG, exportPNGToClipboard } from "src/utils/exportUtils"; import { t } from "src/lang/helpers"; import { PDFExportSettings, PDFExportSettingsComponent } from "./PDFExportSettingsComponent"; +import { captureScreenshot } from "src/utils/screenshot"; +import { createOrOverwriteFile, getIMGFilename } from "src/utils/fileUtils"; @@ -34,7 +36,7 @@ export class ExportDialog extends Modal { public saveToVault: boolean; public pageSize: PageSize = "A4"; public pageOrientation: PageOrientation = "portrait"; - private activeTab: "image" | "pdf" = "image"; + private activeTab: "image" | "pdf" | "screenshot" = "image"; private contentContainer: HTMLDivElement; private buttonContainerRow1: HTMLDivElement; private buttonContainerRow2: HTMLDivElement; @@ -126,11 +128,17 @@ export class ExportDialog extends Modal { cls: `nav-button ${this.activeTab === "pdf" ? "is-active" : ""}` }); + const screenshotTab = tabContainer.createEl("button", { + text: t("EXPORTDIALOG_TAB_SCREENSHOT"), + cls: `nav-button ${this.activeTab === "screenshot" ? "is-active" : ""}` + }); + // Tab click handlers imageTab.onclick = () => { this.activeTab = "image"; imageTab.addClass("is-active"); pdfTab.removeClass("is-active"); + screenshotTab.removeClass("is-active"); this.renderContent(); }; @@ -138,8 +146,17 @@ export class ExportDialog extends Modal { this.activeTab = "pdf"; pdfTab.addClass("is-active"); imageTab.removeClass("is-active"); + screenshotTab.removeClass("is-active"); this.renderContent(); }; + + screenshotTab.onclick = () => { + this.activeTab = "screenshot"; + screenshotTab.addClass("is-active"); + imageTab.removeClass("is-active"); + pdfTab.removeClass("is-active"); + this.renderContent(); + } } // Create content container @@ -170,14 +187,23 @@ export class ExportDialog extends Modal { this.buttonContainerRow1.empty(); this.buttonContainerRow2.empty(); - if (this.activeTab === "image") { - this.createImageSettings(); - this.createExportSettings(); - this.createImageButtons(); - } else { - this.createImageSettings(); - this.createPDFSettings(); - this.createPDFButton(); + this.createHeader(); + switch (this.activeTab) { + case "pdf": + this.createImageSettings(); + this.createPDFSettings(); + this.createPDFButton(); + break; + case "screenshot": + this.createImageSettings(true); + this.createImageButtons(true); + break; + case "image": + default: + this.createImageSettings(false); + this.createExportSettings(); + this.createImageButtons(); + break; } } @@ -186,12 +212,28 @@ export class ExportDialog extends Modal { const height = Math.round(this.scale*this.boundingBox.height + this.padding*2); return fragWithHTML(`${t("EXPORTDIALOG_SIZE_DESC")}
${t("EXPORTDIALOG_SCALE_VALUE")} ${this.scale}
${t("EXPORTDIALOG_IMAGE_SIZE")} ${width}x${height}`); } - - private createImageSettings() { - let paddingSetting: Setting; - this.contentContainer.createEl("h1",{text: t("EXPORTDIALOG_IMAGE_SETTINGS")}); - this.contentContainer.createEl("p",{text: t("EXPORTDIALOG_IMAGE_DESC")}) + private createHeader() { + switch (this.activeTab) { + case "pdf": + this.contentContainer.createEl("h1",{text: t("EXPORTDIALOG_PDF_SETTINGS")}); + //this.contentContainer.createEl("p",{text: t("EXPORTDIALOG_PDF_DESC")}); + break; + case "screenshot": + this.contentContainer.createEl("h1",{text: t("EXPORTDIALOG_TAB_SCREENSHOT")}); + this.contentContainer.createEl("p",{text: t("EXPORTDIALOG_SCREENSHOT_DESC")}) + break; + case "image": + default: + this.contentContainer.createEl("h1",{text: t("EXPORTDIALOG_IMAGE_SETTINGS")}); + this.contentContainer.createEl("p",{text: t("EXPORTDIALOG_IMAGE_DESC")}) + break; + } + + } + + private createImageSettings(isScreenshot: boolean = false) { + let paddingSetting: Setting; this.createSaveSettingsDropdown(); @@ -238,17 +280,19 @@ export class ExportDialog extends Modal { }) ) - new Setting(this.contentContainer) - .setName(t("EXPORTDIALOG_BACKGROUND")) - .addDropdown(dropdown => - dropdown - .addOption("transparent", t("EXPORTDIALOG_BACKGROUND_TRANSPARENT")) - .addOption("with-color", t("EXPORTDIALOG_BACKGROUND_USE_COLOR")) - .setValue(this.transparent?"transparent":"with-color") - .onChange(value => { - this.transparent = value === "transparent"; - }) - ) + if(!isScreenshot) { + new Setting(this.contentContainer) + .setName(t("EXPORTDIALOG_BACKGROUND")) + .addDropdown(dropdown => + dropdown + .addOption("transparent", t("EXPORTDIALOG_BACKGROUND_TRANSPARENT")) + .addOption("with-color", t("EXPORTDIALOG_BACKGROUND_USE_COLOR")) + .setValue(this.transparent?"transparent":"with-color") + .onChange(value => { + this.transparent = value === "transparent"; + }) + ) + } this.selectedOnlySetting = new Setting(this.contentContainer) .setName(t("EXPORTDIALOG_SELECTED_ELEMENTS")) @@ -262,6 +306,8 @@ export class ExportDialog extends Modal { this.updateBoundingBox(); }) ); + //@ts-ignore + this.selectedOnlySetting.setVisibility(this.hasSelectedElements); } private createExportSettings() { @@ -308,14 +354,29 @@ export class ExportDialog extends Modal { ).render(); } - private createImageButtons() { + private createImageButtons(isScreenshot: boolean = false) { if(DEVICE.isDesktop) { const bPNG = this.buttonContainerRow1.createEl("button", { text: t("EXPORTDIALOG_PNGTOFILE"), cls: "excalidraw-export-button" }); bPNG.onclick = () => { - this.view.exportPNG(this.embedScene, this.hasSelectedElements && this.exportSelectedOnly); + if(isScreenshot) { + //allow dialot to close before taking screenshot + setTimeout(async () => { + const png = await captureScreenshot(this.view, { + zoom: this.scale, + margin: this.padding, + selectedOnly: this.exportSelectedOnly, + theme: this.theme + }); + if(png) { + exportPNG(png, this.view.file.basename); + } + }); + } else { + this.view.exportPNG(this.embedScene, this.hasSelectedElements && this.exportSelectedOnly); + } this.close(); }; } @@ -325,7 +386,22 @@ export class ExportDialog extends Modal { cls: "excalidraw-export-button" }); bPNGVault.onclick = () => { - this.view.savePNG(this.view.getScene(this.hasSelectedElements && this.exportSelectedOnly)); + if(isScreenshot) { + //allow dialot to close before taking screenshot + setTimeout(async () => { + const png = await captureScreenshot(this.view, { + zoom: this.scale, + margin: this.padding, + selectedOnly: this.exportSelectedOnly, + theme: this.theme + }); + if(png) { + createOrOverwriteFile(this.app, getIMGFilename(this.view.file.path,"png"), png); + } + }); + } else { + this.view.savePNG(this.view.getScene(this.hasSelectedElements && this.exportSelectedOnly)); + } this.close(); }; @@ -334,10 +410,27 @@ export class ExportDialog extends Modal { cls: "excalidraw-export-button" }); bPNGClipboard.onclick = async () => { - this.view.exportPNGToClipboard(this.embedScene, this.hasSelectedElements && this.exportSelectedOnly); + if(isScreenshot) { + //allow dialot to close before taking screenshot + setTimeout(async () => { + const png = await captureScreenshot(this.view, { + zoom: this.scale, + margin: this.padding, + selectedOnly: this.exportSelectedOnly, + theme: this.theme + }); + if(png) { + exportPNGToClipboard(png); + } + }); + } else { + this.view.exportPNGToClipboard(this.embedScene, this.hasSelectedElements && this.exportSelectedOnly); + } this.close(); }; + if(isScreenshot) return; + if(DEVICE.isDesktop) { const bExcalidraw = this.buttonContainerRow2.createEl("button", { text: t("EXPORTDIALOG_EXCALIDRAW"), diff --git a/src/shared/Dialogs/Messages.ts b/src/shared/Dialogs/Messages.ts index 5eca86c..b916373 100644 --- a/src/shared/Dialogs/Messages.ts +++ b/src/shared/Dialogs/Messages.ts @@ -17,11 +17,9 @@ I build this plugin in my free time, as a labor of love. Curious about the philo
Buy Me a Coffee at ko-fi.com
`, -"2.10.2": ` -## Fixed by Excalidraw.com -- Alt-duplicate now preserves the original element. Previously, using Alt to duplicate would swap the original with the new element, leading to unexpected behavior and several downstream issues. [#9403](https://github.com/excalidraw/excalidraw/pull/9403) - +"2.11.0": ` ## New +- New "Screenshot" option in the Export Image dialog. This allows you to take a screenshot of the current view, including embedded web pages, youtube videos, and markdown documents. Screenshot is only possible in PNG. - Expose parameter in plugin settings to disable AI functionality [#2325](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/2325) - Enable double-click text editing option in Excalidraw appearance and behavior (based on request on Discord) - Added two new PDF export sizes: "Match image", "HD Screen". @@ -29,8 +27,13 @@ I build this plugin in my free time, as a labor of love. Curious about the philo ## Fixed in the plugin - Scaling multiple embeddables at once did not work. [#2276](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/2276) - When creating multiple back-of-the-note the second card is not created correctly if autosave has not yet happened. -- Drawing reloads while editing the back-of-the-note card in certain cases causing editing to be interrupted. -- Moved Excalidraw filetype indicator ✏️ to after filename where other filetype tags are displayed. You can turn filetype indicator on/off in plugin settings under Miscellaneous. +- Drawing reloads while editing the back-of-the-note card in certain cases causes editing to be interrupted. +- Moved Excalidraw filetype indicator ✏️ to after filename where other filetype tags are displayed. You can turn the filetype indicator on/off in plugin settings under Miscellaneous. + +## Fixed by Excalidraw.com +- Alt-duplicate now preserves the original element. Previously, using Alt to duplicate would swap the original with the new element, leading to unexpected behavior and several downstream issues. [#9403](https://github.com/excalidraw/excalidraw/pull/9403) +- When dragging the arrow endpoint, update the binding only on the dragged side [#9367](https://github.com/excalidraw/excalidraw/pull/9367) +- Laser pointer trail disappearing on pointerup [#9413](https://github.com/excalidraw/excalidraw/pull/9413) [#9427](https://github.com/excalidraw/excalidraw/pull/9427) `, "2.10.1": ` diff --git a/src/utils/exportUtils.ts b/src/utils/exportUtils.ts index f421f00..1833b23 100644 --- a/src/utils/exportUtils.ts +++ b/src/utils/exportUtils.ts @@ -1,6 +1,8 @@ import { Notice } from 'obsidian'; import { DEVICE } from 'src/constants/constants'; import { t } from 'src/lang/helpers'; +import { download } from './fileUtils'; +import { svgToBase64 } from './utils'; const DPI = 96; @@ -512,4 +514,30 @@ export async function exportSVGToClipboard(svg: SVGSVGElement) { } catch (error) { console.error("Failed to copy SVG to clipboard: ", error); } -} \ No newline at end of file +} + +export async function exportPNGToClipboard(png: Blob) { + await navigator.clipboard.write([ + new window.ClipboardItem({ + "image/png": png, + }), + ]); +} + +export function exportPNG(png: Blob, filename: string) { + const reader = new FileReader(); + reader.readAsDataURL(png); + reader.onloadend = () => { + const base64data = reader.result; + download(null, base64data, `${filename}.png`); + }; +} + +export function exportSVG(svg: SVGSVGElement, filename: string) { + download( + null, + svgToBase64(svg.outerHTML), + `${filename}.svg`, + ); +} + diff --git a/src/utils/fileUtils.ts b/src/utils/fileUtils.ts index 3038298..8d1d1bc 100644 --- a/src/utils/fileUtils.ts +++ b/src/utils/fileUtils.ts @@ -404,8 +404,8 @@ export const getAliasWithSize = (alias: string, size: string): string => { } export const getCropFileNameAndFolder = async (plugin: ExcalidrawPlugin, hostPath: string, baseNewFileName: string):Promise<{folderpath: string, filename: string}> => { - let prefix = plugin.settings.cropPrefix; - if(!prefix || prefix.trim() === "") prefix = CROPPED_PREFIX; + let prefix = plugin.settings.cropPrefix || ""; + if(prefix.trim() === "") prefix = CROPPED_PREFIX; const filename = prefix + baseNewFileName + ".md"; if(!plugin.settings.cropFolder || plugin.settings.cropFolder.trim() === "") { const folderpath = (await getAttachmentsFolderAndFilePath(plugin.app, hostPath, filename)).folder; @@ -417,8 +417,8 @@ export const getCropFileNameAndFolder = async (plugin: ExcalidrawPlugin, hostPat } export const getAnnotationFileNameAndFolder = async (plugin: ExcalidrawPlugin, hostPath: string, baseNewFileName: string):Promise<{folderpath: string, filename: string}> => { - let prefix = plugin.settings.annotatePrefix; - if(!prefix || prefix.trim() === "") prefix = ANNOTATED_PREFIX; + let prefix = plugin.settings.annotatePrefix || ""; + if(prefix.trim() === "") prefix = ANNOTATED_PREFIX; const filename = prefix + baseNewFileName + ".md"; if(!plugin.settings.annotateFolder || plugin.settings.annotateFolder.trim() === "") { const folderpath = (await getAttachmentsFolderAndFilePath(plugin.app, hostPath, filename)).folder; @@ -494,8 +494,11 @@ export const hasExcalidrawEmbeddedImagesTreeChanged = (sourceFile: TFile, mtime: return fileList.some(f=>f.stat.mtime > mtime); } -export async function createOrOverwriteFile(app: App, path: string, content: string | ArrayBuffer): Promise { +export async function createOrOverwriteFile(app: App, path: string, content: string | ArrayBuffer | Blob): Promise { const file = app.vault.getAbstractFileByPath(normalizePath(path)); + if (content instanceof Blob) { + content = await content.arrayBuffer(); + } if(content instanceof ArrayBuffer) { if(file && file instanceof TFile) { await app.vault.modifyBinary(file, content); diff --git a/src/utils/screenshot.ts b/src/utils/screenshot.ts new file mode 100644 index 0000000..626afc5 --- /dev/null +++ b/src/utils/screenshot.ts @@ -0,0 +1,273 @@ +import { ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/excalidraw/types"; +import { request } from "http"; +import { Notice } from "obsidian"; +import { DEVICE } from "src/constants/constants"; +import { getEA } from "src/core"; +import { t } from "src/lang/helpers"; +import { ExcalidrawAutomate } from "src/shared/ExcalidrawAutomate"; +import ExcalidrawView from "src/view/ExcalidrawView"; + +export interface ScreenshotOptions { + zoom: number; + margin: number; + selectedOnly: boolean; + theme: string; +} + +export async function captureScreenshot(view: ExcalidrawView, options: ScreenshotOptions): Promise { + if (!DEVICE.isDesktop) { + new Notice(t("SCREENSHOT_DESKTOP_ONLY")); + return null; + } + + (view.excalidrawAPI as ExcalidrawImperativeAPI).setForceRenderAllEmbeddables(true); + + const remote = window.require("electron").remote; + const scene = view.getScene(); + const viewSelectedElements = view.getViewSelectedElements(); + const selectedIDs = new Set(options.selectedOnly ? viewSelectedElements.map(el => el.id) : []); + const savedOpacity: { id: string; opacity: number }[] = []; + const ea = getEA(view) as ExcalidrawAutomate; + + // Save the current browser zoom level + const webContents = remote.getCurrentWebContents(); + const originalZoomFactor = webContents.getZoomFactor(); + + // Set browser zoom to 100% + webContents.setZoomFactor(1.0); + await sleep(100); // Give the browser time to apply zoom + const devicePixelRatio = window.devicePixelRatio || 1; + + if (options.selectedOnly) { + ea.copyViewElementsToEAforEditing(ea.getViewElements().filter(el => !selectedIDs.has(el.id))); + ea.getElements().forEach(el => { + savedOpacity.push({ + id: el.id, + opacity: el.opacity + }); + el.opacity = 0; + }); + if (savedOpacity.length > 0) { + await ea.addElementsToView(false, false, false, false); + } + } + + let boundingBox = ea.getBoundingBox(options.selectedOnly ? viewSelectedElements : scene.elements); + boundingBox = { + topX: Math.ceil(boundingBox.topX), + topY: Math.ceil(boundingBox.topY), + width: Math.ceil(boundingBox.width), + height: Math.ceil(boundingBox.height) + } + + const margin = options.margin; + const availableWidth = Math.floor(view.excalidrawAPI.getAppState().width); + const availableHeight = Math.floor(view.excalidrawAPI.getAppState().height); + + // Apply zoom to the total dimensions + const totalWidth = Math.ceil(boundingBox.width * options.zoom + margin * 2); + const totalHeight = Math.ceil(boundingBox.height * options.zoom + margin * 2); + + // Calculate number of tiles + const cols = Math.ceil(totalWidth / availableWidth); + const rows = Math.ceil(totalHeight / availableHeight); + + // Use exact tile sizes to avoid rounding issues + const tileWidth = Math.ceil(totalWidth / cols); + const tileHeight = Math.ceil(totalHeight / rows); + + // Adjust totalWidth and totalHeight to be multiples of tileWidth and tileHeight + const adjustedTotalWidth = tileWidth * cols; + const adjustedTotalHeight = tileHeight * rows; + + // Save and set state + const saveState = () => { + const { + scrollX, + scrollY, + zoom, + viewModeEnabled, + linkOpacity, + theme, + } = view.excalidrawAPI.getAppState(); + return { + scrollX, + scrollY, + zoom, + viewModeEnabled, + linkOpacity, + theme, + }; + } + + const restoreState = (st: any) => { + view.updateScene({ + appState: { + ...st + } + }); + } + + const savedState = saveState(); + + // Switch to view mode for layerUIWrapper to be rendered so it can be hidden + view.updateScene({ + appState: { + viewModeEnabled: true, + linkOpacity: 0, + theme: options.theme, + }, + }); + + await sleep(50); + + // Hide UI elements (must be after changing to view mode) + const container = view.excalidrawWrapperRef.current; + const layerUIWrapper = container.querySelector(".layer-ui__wrapper"); + const layerUIWrapperOriginalDisplay = layerUIWrapper.style.display; + layerUIWrapper.style.display = "none"; + + const originalStyle = { + width: container.style.width, + height: container.style.height, + left: container.style.left, + top: container.style.top, + position: container.style.position, + overflow: container.style.overflow, + }; + + try { + + container.style.width = tileWidth + "px"; + container.style.height = tileHeight + "px"; + container.style.overflow = "visible"; + + // Set canvas size and zoom value for capture + view.updateScene({ + appState: { + zoom: { + value: options.zoom + }, + width: tileWidth, + height: tileHeight + }, + }); + + await sleep(50); // wait for frame to render + + // Prepare to collect tile images as data URLs + const rect = container.getBoundingClientRect(); + const tiles = []; + + for (let row = 0; row < rows; row++) { + for (let col = 0; col < cols; col++) { + // Calculate scroll position for this tile (adjusted for zoom) + const scrollX = boundingBox.topX - margin / options.zoom + (col * tileWidth) / options.zoom; + const scrollY = boundingBox.topY - margin / options.zoom + (row * tileHeight) / options.zoom; + + view.updateScene({ + appState: { + scrollX: -scrollX, + scrollY: -scrollY, + zoom: { + value: options.zoom + }, + width: tileWidth, + height: tileHeight + }, + }); + + await sleep(250); + + // Calculate the exact width/height for this tile + const captureWidth = col === cols - 1 ? adjustedTotalWidth - tileWidth * (cols - 1) : tileWidth; + const captureHeight = row === rows - 1 ? adjustedTotalHeight - tileHeight * (rows - 1) : tileHeight; + + const image = await remote.getCurrentWebContents().capturePage({ + x: rect.left * devicePixelRatio, + y: rect.top * devicePixelRatio, + width: captureWidth * devicePixelRatio, + height: captureHeight * devicePixelRatio, + }); + + tiles.push({ + url: "data:image/png;base64," + image.toPNG().toString("base64"), + width: captureWidth, + height: captureHeight, + col: col, + row: row + }); + } + } + + // Restore original styles + Object.assign(container.style, originalStyle); + + // Stitch tiles together using a browser canvas + const canvas = document.createElement("canvas"); + canvas.width = adjustedTotalWidth * devicePixelRatio; + canvas.height = adjustedTotalHeight * devicePixelRatio; + canvas.style.width = `${adjustedTotalWidth}px`; + canvas.style.height = `${adjustedTotalHeight}px`; + + const ctx = canvas.getContext("2d"); + ctx.scale(1, 1); + ctx.imageSmoothingEnabled = true; + ctx.imageSmoothingQuality = 'high'; + + let y = 0; + for (let row = 0; row < rows; row++) { + let x = 0; + for (let col = 0; col < cols; col++) { + const tile = tiles[row * cols + col]; + const img = new window.Image(); + img.src = tile.url; + await new Promise(res => { + img.onload = res; + }); + ctx.drawImage(img, x, y); + x += tile.width * devicePixelRatio; + } + y += tiles[row * cols].height * devicePixelRatio; // Use the height of the first tile in the row + } + + // Return the blob for the caller to handle + return new Promise((resolve) => { + canvas.toBlob((blob) => { + resolve(blob); + }, "image/png"); + }); + + } catch (e) { + console.error(e); + new Notice(t("SCREENSHOT_ERROR")); + return null; + } finally { + // Restore browser zoom to its original value + webContents.setZoomFactor(originalZoomFactor); + + // Restore UI elements + try { + const container = view.excalidrawWrapperRef.current; + const layerUIWrapper = container.querySelector(".layer-ui__wrapper"); + if (layerUIWrapper) { + layerUIWrapper.style.display = layerUIWrapperOriginalDisplay; + } + + // Restore original state + restoreState(savedState); + + // Restore opacity for selected elements if necessary + if (options.selectedOnly && savedOpacity.length > 0) { + ea.clear(); + ea.copyViewElementsToEAforEditing(ea.getViewElements().filter(el => !selectedIDs.has(el.id))); + savedOpacity.forEach(x => { + ea.getElement(x.id).opacity = x.opacity; + }); + await ea.addElementsToView(false, false, false, false); + } + } catch (e) { + console.error("Error in cleanup:", e); + } + } +} diff --git a/src/view/ExcalidrawView.ts b/src/view/ExcalidrawView.ts index 32f33f8..1bcfa7a 100644 --- a/src/view/ExcalidrawView.ts +++ b/src/view/ExcalidrawView.ts @@ -151,7 +151,7 @@ import { getPDFCropRect } from "../utils/PDFUtils"; import { Position, ViewSemaphores } from "../types/excalidrawViewTypes"; import { DropManager } from "./managers/DropManager"; import { ImageInfo } from "src/types/excalidrawAutomateTypes"; -import { exportToPDF, getMarginValue, getPageDimensions, PageOrientation, PageSize } from "src/utils/exportUtils"; +import { exportPNG, exportPNGToClipboard, exportSVG, exportToPDF, getMarginValue, getPageDimensions, PageOrientation, PageSize } from "src/utils/exportUtils"; import { FrameRenderingOptions } from "src/types/utilTypes"; import { CaptureUpdateAction } from "src/constants/constants"; @@ -581,11 +581,7 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{ if (!svg) { return; } - download( - null, - svgToBase64(svg.outerHTML), - `${this.file.basename}.svg`, - ); + exportSVG(svg, this.file.basename); } public async getSVG(embedScene?: boolean, selectedOnly?: boolean):Promise { @@ -685,7 +681,7 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{ if (!png) { return; } - await createOrOverwriteFile(this.app, filepath, await png.arrayBuffer()); + await createOrOverwriteFile(this.app, filepath, png); } if(this.plugin.settings.autoExportLightAndDark) { @@ -714,11 +710,7 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{ // // not await so that we can detect whether the thrown error likely relates // to a lack of support for the Promise ClipboardItem constructor - await navigator.clipboard.write([ - new window.ClipboardItem({ - "image/png": png, - }), - ]); + await exportPNGToClipboard(png); } public async exportPNG(embedScene?:boolean, selectedOnly?: boolean):Promise { @@ -731,12 +723,7 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{ if (!png) { return; } - const reader = new FileReader(); - reader.readAsDataURL(png); - reader.onloadend = () => { - const base64data = reader.result; - download(null, base64data, `${this.file.basename}.png`); - }; + exportPNG(png, this.file.basename); } public setPreventReload() {