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

`,
-"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() {