Compare commits

..

1 Commits

Author SHA1 Message Date
zsviczian
f5475bfde6 imagepath hook 2025-01-20 22:40:27 +01:00
16 changed files with 198 additions and 1313 deletions

132
package-lock.json generated
View File

@@ -9,7 +9,6 @@
"version": "2.2.5", "version": "2.2.5",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@cantoo/pdf-lib": "^2.2.4",
"@popperjs/core": "^2.11.8", "@popperjs/core": "^2.11.8",
"@zsviczian/colormaster": "^1.2.2", "@zsviczian/colormaster": "^1.2.2",
"@zsviczian/excalidraw": "0.17.6-26", "@zsviczian/excalidraw": "0.17.6-26",
@@ -42,7 +41,6 @@
"@excalidraw/prettier-config": "^1.0.2", "@excalidraw/prettier-config": "^1.0.2",
"@rollup/plugin-babel": "^6.0.3", "@rollup/plugin-babel": "^6.0.3",
"@rollup/plugin-commonjs": "^26.0.1", "@rollup/plugin-commonjs": "^26.0.1",
"@rollup/plugin-json": "^6.1.0",
"@rollup/plugin-node-resolve": "^15.2.3", "@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-replace": "^5.0.2", "@rollup/plugin-replace": "^5.0.2",
"@rollup/plugin-typescript": "^12.1.2", "@rollup/plugin-typescript": "^12.1.2",
@@ -1884,19 +1882,6 @@
"resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-6.0.2.tgz", "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-6.0.2.tgz",
"integrity": "sha512-Tbsj02wXCbqGmzdnXNk0SOF19ChhRU70BsroIi4Pm6Ehp56in6vch94mfbdQ17DozxkL3BAVjbZ4Qc1a0HFRAg==" "integrity": "sha512-Tbsj02wXCbqGmzdnXNk0SOF19ChhRU70BsroIi4Pm6Ehp56in6vch94mfbdQ17DozxkL3BAVjbZ4Qc1a0HFRAg=="
}, },
"node_modules/@cantoo/pdf-lib": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/@cantoo/pdf-lib/-/pdf-lib-2.2.4.tgz",
"integrity": "sha512-69bECXjl0QVMrBTEsPZDPFmhoyLGwXKiJI5JWPgBnAx0kS5+7v76rNCeYMTbDmZaPgvdIViMvTaz2HVCe8u1fw==",
"dependencies": {
"@pdf-lib/standard-fonts": "^1.0.0",
"@pdf-lib/upng": "^1.0.1",
"color": "^4.2.3",
"crypto-js": "^4.2.0",
"node-html-better-parser": "^1.4.0",
"pako": "^1.0.11"
}
},
"node_modules/@codemirror/commands": { "node_modules/@codemirror/commands": {
"version": "6.6.0", "version": "6.6.0",
"resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.6.0.tgz", "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.6.0.tgz",
@@ -2361,22 +2346,6 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/@pdf-lib/standard-fonts": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@pdf-lib/standard-fonts/-/standard-fonts-1.0.0.tgz",
"integrity": "sha512-hU30BK9IUN/su0Mn9VdlVKsWBS6GyhVfqjwl1FjZN4TxP6cCw0jP2w7V3Hf5uX7M0AZJ16vey9yE0ny7Sa59ZA==",
"dependencies": {
"pako": "^1.0.6"
}
},
"node_modules/@pdf-lib/upng": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@pdf-lib/upng/-/upng-1.0.1.tgz",
"integrity": "sha512-dQK2FUMQtowVP00mtIksrlZhdFXQZPC+taih1q4CvPZ5vqdxR/LKBaFg0oAfzd1GlHZXXSPdQfzQnt+ViGvEIQ==",
"dependencies": {
"pako": "^1.0.10"
}
},
"node_modules/@pkgjs/parseargs": { "node_modules/@pkgjs/parseargs": {
"version": "0.11.0", "version": "0.11.0",
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
@@ -2806,26 +2775,6 @@
} }
} }
}, },
"node_modules/@rollup/plugin-json": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-6.1.0.tgz",
"integrity": "sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==",
"dev": true,
"dependencies": {
"@rollup/pluginutils": "^5.1.0"
},
"engines": {
"node": ">=14.0.0"
},
"peerDependencies": {
"rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
},
"peerDependenciesMeta": {
"rollup": {
"optional": true
}
}
},
"node_modules/@rollup/plugin-node-resolve": { "node_modules/@rollup/plugin-node-resolve": {
"version": "15.2.3", "version": "15.2.3",
"resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.2.3.tgz", "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.2.3.tgz",
@@ -3847,18 +3796,6 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/color": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
"integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==",
"dependencies": {
"color-convert": "^2.0.1",
"color-string": "^1.9.0"
},
"engines": {
"node": ">=12.5.0"
}
},
"node_modules/color-convert": { "node_modules/color-convert": {
"version": "1.9.3", "version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
@@ -3871,32 +3808,8 @@
"node_modules/color-name": { "node_modules/color-name": {
"version": "1.1.3", "version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
}, "devOptional": true
"node_modules/color-string": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
"integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
"dependencies": {
"color-name": "^1.0.0",
"simple-swizzle": "^0.2.2"
}
},
"node_modules/color/node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/color/node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
}, },
"node_modules/colord": { "node_modules/colord": {
"version": "2.9.3", "version": "2.9.3",
@@ -4009,11 +3922,6 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/crypto-js": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz",
"integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q=="
},
"node_modules/css-declaration-sorter": { "node_modules/css-declaration-sorter": {
"version": "7.2.0", "version": "7.2.0",
"resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-7.2.0.tgz", "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-7.2.0.tgz",
@@ -5593,21 +5501,6 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/html-entities": {
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.5.2.tgz",
"integrity": "sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/mdevils"
},
{
"type": "patreon",
"url": "https://patreon.com/mdevils"
}
]
},
"node_modules/iconv-lite": { "node_modules/iconv-lite": {
"version": "0.6.3", "version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
@@ -5702,11 +5595,6 @@
"loose-envify": "^1.0.0" "loose-envify": "^1.0.0"
} }
}, },
"node_modules/is-arrayish": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
"integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="
},
"node_modules/is-binary-path": { "node_modules/is-binary-path": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
@@ -6857,14 +6745,6 @@
"dev": true, "dev": true,
"peer": true "peer": true
}, },
"node_modules/node-html-better-parser": {
"version": "1.4.7",
"resolved": "https://registry.npmjs.org/node-html-better-parser/-/node-html-better-parser-1.4.7.tgz",
"integrity": "sha512-cJcBhlrn432XtbzPmzxxMLsHeSUnE5qFQtVbVlHXvUGt2ccqc0/wBsB43CFv39BIkLkP5rrPUN5Hg52CnheH+A==",
"dependencies": {
"html-entities": "^2.3.2"
}
},
"node_modules/node-releases": { "node_modules/node-releases": {
"version": "2.0.14", "version": "2.0.14",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz",
@@ -8437,14 +8317,6 @@
"url": "https://github.com/sponsors/isaacs" "url": "https://github.com/sponsors/isaacs"
} }
}, },
"node_modules/simple-swizzle": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
"integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==",
"dependencies": {
"is-arrayish": "^0.3.1"
}
},
"node_modules/slash": { "node_modules/slash": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",

View File

@@ -40,8 +40,7 @@
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"roughjs": "^4.5.2", "roughjs": "^4.5.2",
"woff2sfnt-sfnt2woff": "^1.0.0", "woff2sfnt-sfnt2woff": "^1.0.0",
"es6-promise-pool": "2.5.0", "es6-promise-pool": "2.5.0"
"@cantoo/pdf-lib": "^2.2.4"
}, },
"devDependencies": { "devDependencies": {
"jsesc": "^3.0.2", "jsesc": "^3.0.2",
@@ -60,7 +59,6 @@
"@rollup/plugin-node-resolve": "^15.2.3", "@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-replace": "^5.0.2", "@rollup/plugin-replace": "^5.0.2",
"@rollup/plugin-typescript": "^12.1.2", "@rollup/plugin-typescript": "^12.1.2",
"@rollup/plugin-json": "^6.1.0",
"@types/chroma-js": "^2.4.0", "@types/chroma-js": "^2.4.0",
"@types/js-beautify": "^1.14.0", "@types/js-beautify": "^1.14.0",
"@types/js-yaml": "^4.0.9", "@types/js-yaml": "^4.0.9",

View File

@@ -11,7 +11,6 @@ import postprocess from '@zsviczian/rollup-plugin-postprocess';
import cssnano from 'cssnano'; import cssnano from 'cssnano';
import jsesc from 'jsesc'; import jsesc from 'jsesc';
import { minify } from 'uglify-js'; import { minify } from 'uglify-js';
import json from '@rollup/plugin-json';
// Load environment variables // Load environment variables
import dotenv from 'dotenv'; import dotenv from 'dotenv';
@@ -131,7 +130,6 @@ const BASE_CONFIG = {
const getRollupPlugins = (tsconfig, ...plugins) => [ const getRollupPlugins = (tsconfig, ...plugins) => [
typescript2(tsconfig), typescript2(tsconfig),
json(),
replace({ replace({
preventAssignment: true, preventAssignment: true,
"process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV), "process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV),

View File

@@ -105,6 +105,41 @@
*/ */
//ea.onFileCreateHook = (data) => {}; //ea.onFileCreateHook = (data) => {};
/**
* If set, this callback is triggered when a image is being saved in Excalidraw.
* You can use this callback to customize the naming and path of pasted images to avoid
* default names like "Pasted image 123147170.png" being saved in the attachments folder,
* and instead use more meaningful names based on the Excalidraw file or other criteria,
* plus save the image in a different folder.
*
* If the function returns null or undefined, the normal Excalidraw operation will continue
* with the excalidraw generated name and default path.
* If a filepath is returned, that will be used. Include the full Vault filepath and filename
* with the file extension.
* The currentImageName is the name of the image generated by excalidraw or provided during paste.
*
* @param data - An object containing the following properties:
* @property {string} [currentImageName] - Default name for the image.
* @property {string} drawingFilePath - The file path of the Excalidraw file where the image is being used.
*
* @returns {string} - The new filepath for the image including full vault path and extension.
*
* Example usage:
* ```
* onImageFilePathHook: (data) => {
* const { currentImageName, drawingFilePath } = data;
* const ext = currentImageName.split('.').pop();
* // Generate a new filepath based on the drawing file name and other criteria
* return `${drawingFileName} - ${currentImageName || 'image'}.${ext}`;
* }
* ```
* onImageFilePathHook: (data: {
* currentImageName: string; // Excalidraw generated name of the image, or the name received from the file system.
* drawingFilePath: string; // The full filepath of the Excalidraw file where the image is being used.
* }) => string = null;
*/
//ea.onImageFileNameHook = (data) => {};
/** /**
* If set, this callback is triggered whenever the active canvas color changes * If set, this callback is triggered whenever the active canvas color changes
* onCanvasColorChangeHook: ( * onCanvasColorChangeHook: (

File diff suppressed because one or more lines are too long

View File

@@ -42,7 +42,6 @@ import { TAG_AUTOEXPORT, TAG_MDREADINGMODE, TAG_PDFEXPORT } from "src/constants/
import { HotkeyEditor } from "src/shared/Dialogs/HotkeyEditor"; import { HotkeyEditor } from "src/shared/Dialogs/HotkeyEditor";
import { getExcalidrawViews } from "src/utils/obsidianUtils"; import { getExcalidrawViews } from "src/utils/obsidianUtils";
import { createSliderWithText } from "src/utils/sliderUtils"; import { createSliderWithText } from "src/utils/sliderUtils";
import { PDFExportSettingsComponent, PDFExportSettings } from "src/shared/Dialogs/PDFExportSettingsComponent";
export interface ExcalidrawSettings { export interface ExcalidrawSettings {
folder: string; folder: string;
@@ -219,7 +218,6 @@ export interface ExcalidrawSettings {
rank: Rank; rank: Rank;
modifierKeyOverrides: {modifiers: Modifier[], key: string}[]; modifierKeyOverrides: {modifiers: Modifier[], key: string}[];
showSplashscreen: boolean; showSplashscreen: boolean;
pdfSettings: PDFExportSettings;
} }
declare const PLUGIN_VERSION:string; declare const PLUGIN_VERSION:string;
@@ -499,15 +497,6 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
{modifiers: ["Mod"], key:"G"}, {modifiers: ["Mod"], key:"G"},
], ],
showSplashscreen: true, showSplashscreen: true,
pdfSettings: {
pageSize: "A4",
pageOrientation: "portrait",
fitToPage: true,
paperColor: "white",
customPaperColor: "#ffffff",
alignment: "center",
margin: "normal"
},
}; };
export class ExcalidrawSettingTab extends PluginSettingTab { export class ExcalidrawSettingTab extends PluginSettingTab {
@@ -2129,20 +2118,6 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
}), }),
); );
detailsEl = exportDetailsEl.createEl("details");
detailsEl.createEl("summary", {
text: t("PDF_EXPORT_SETTINGS"),
cls: "excalidraw-setting-h4",
});
new PDFExportSettingsComponent(
detailsEl,
this.plugin.settings.pdfSettings,
() => {
this.applySettingsUpdate();
}
).render();
detailsEl = exportDetailsEl.createEl("details"); detailsEl = exportDetailsEl.createEl("details");
detailsEl.createEl("summary", { detailsEl.createEl("summary", {
text: t("EXPORT_HEAD"), text: t("EXPORT_HEAD"),
@@ -2287,6 +2262,9 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
el.innerHTML = t("MD_EMBED_CUSTOMDATA_HEAD_DESC"); el.innerHTML = t("MD_EMBED_CUSTOMDATA_HEAD_DESC");
}); });
new EmbeddalbeMDFileCustomDataSettingsComponent( new EmbeddalbeMDFileCustomDataSettingsComponent(
detailsEl, detailsEl,
this.plugin.settings.embeddableMarkdownDefaults, this.plugin.settings.embeddableMarkdownDefaults,

View File

@@ -386,14 +386,13 @@ FILENAME_HEAD: "Filename",
"This setting will not affect the display of the drawing when you are in Excalidraw mode or when you embed the drawing into a markdown document or when rendering hover preview.<br><ul>" + "This setting will not affect the display of the drawing when you are in Excalidraw mode or when you embed the drawing into a markdown document or when rendering hover preview.<br><ul>" +
"<li>See other related setting for <a href='#"+TAG_PDFEXPORT+"'>PDF Export</a> under 'Embedding and Exporting' further below.</li></ul><br>" + "<li>See other related setting for <a href='#"+TAG_PDFEXPORT+"'>PDF Export</a> under 'Embedding and Exporting' further below.</li></ul><br>" +
"You must close the active excalidraw/markdown file and reopen it for this change to take effect.", "You must close the active excalidraw/markdown file and reopen it for this change to take effect.",
SHOW_DRAWING_OR_MD_IN_EXPORTPDF_NAME: "Render Excalidraw as Image in Obsidian PDF Export", SHOW_DRAWING_OR_MD_IN_EXPORTPDF_NAME: "Render the file as an image when exporting an Excalidraw file to PDF",
SHOW_DRAWING_OR_MD_IN_EXPORTPDF_DESC: SHOW_DRAWING_OR_MD_IN_EXPORTPDF_DESC:
"This setting controls how Excalidraw files are exported to PDF using Obsidian's built-in <b>Export to PDF</b> feature.<br>" + "This setting controls the behavior of Excalidraw when exporting an Excalidraw file to PDF in markdown view mode using Obsidian's <b>Export to PDF</b> feature.<br>" +
"<ul><li><b>Enabled:</b> The PDF will include the Excalidraw drawing as an image.</li>" + "<ul><li>When <b>enabled</b> the PDF will show the Excalidraw drawing only;</li>" +
"<li><b>Disabled:</b> The PDF will include the markdown content as text.</li></ul>" + "<li>When <b>disabled</b> the PDF will show the markdown side of the document.</li></ul>" +
"Note: This setting does not affect the PDF export feature within Excalidraw itself.<br>" + "See the other related setting for <a href='#"+TAG_MDREADINGMODE+"'>Markdown Reading Mode</a> under 'Appearnace and Behavior' further above.<br>" +
"See the other related setting for <a href='#"+TAG_MDREADINGMODE+"'>Markdown Reading Mode</a> under 'Appearance and Behavior' further above.<br>" + "⚠️ Note, you must close the active excalidraw/markdown file and reopen for this change to take effect. ⚠️",
"⚠️ You must close and reopen the Excalidraw/markdown file for changes to take effect. ⚠️",
HOTKEY_OVERRIDE_HEAD: "Hotkey overrides", HOTKEY_OVERRIDE_HEAD: "Hotkey overrides",
HOTKEY_OVERRIDE_DESC: `Some of the Excalidraw hotkeys such as <code>${labelCTRL()}+Enter</code> to edit text or <code>${labelCTRL()}+K</code> to create an element link ` + HOTKEY_OVERRIDE_DESC: `Some of the Excalidraw hotkeys such as <code>${labelCTRL()}+Enter</code> to edit text or <code>${labelCTRL()}+K</code> to create an element link ` +
"conflict with Obsidian hotkey settings. The hotkey combinations you add below will override Obsidian's hotkey settings while useing Excalidraw, thus " + "conflict with Obsidian hotkey settings. The hotkey combinations you add below will override Obsidian's hotkey settings while useing Excalidraw, thus " +
@@ -662,7 +661,6 @@ FILENAME_HEAD: "Filename",
EXPORT_EMBED_SCENE_DESC: EXPORT_EMBED_SCENE_DESC:
"Embed Excalidraw scene in exported image. Can be overridden at a file level by adding the <code>excalidraw-export-embed-scene: true/false<code> frontmatter key. " + "Embed Excalidraw scene in exported image. Can be overridden at a file level by adding the <code>excalidraw-export-embed-scene: true/false<code> frontmatter key. " +
"The setting only takes effect the next time you (re)open drawings.", "The setting only takes effect the next time you (re)open drawings.",
PDF_EXPORT_SETTINGS: "PDF Export Settings",
EXPORT_HEAD: "Auto-export Settings", EXPORT_HEAD: "Auto-export Settings",
EXPORT_SYNC_NAME: EXPORT_SYNC_NAME:
"Keep the .SVG and/or .PNG filenames in sync with the drawing file", "Keep the .SVG and/or .PNG filenames in sync with the drawing file",
@@ -1008,75 +1006,4 @@ FILENAME_HEAD: "Filename",
LINK_CLICK_POPOUT: "Open in a popout window", LINK_CLICK_POPOUT: "Open in a popout window",
LINK_CLICK_NEW_TAB: "Open in a new tab", LINK_CLICK_NEW_TAB: "Open in a new tab",
LINK_CLICK_MD_PROPS: "Show the Markdown image-properties dialog (only relevant if you have embedded a markdown document as an image)", LINK_CLICK_MD_PROPS: "Show the Markdown image-properties dialog (only relevant if you have embedded a markdown document as an image)",
//ExportDialog
// Dialog and tabs
EXPORTDIALOG_TITLE: "Export Drawing",
EXPORTDIALOG_TAB_IMAGE: "Image",
EXPORTDIALOG_TAB_PDF: "PDF",
// Settings persistence
EXPORTDIALOG_SAVE_SETTINGS: "Save image settings to file doc.properties?",
EXPORTDIALOG_SAVE_SETTINGS_SAVE: "Save as preset",
EXPORTDIALOG_SAVE_SETTINGS_ONETIME: "One-time use",
// Image settings
EXPORTDIALOG_IMAGE_SETTINGS: "Image",
EXPORTDIALOG_IMAGE_DESC: "PNG supports transparency. External files can include Excalidraw scene data.",
EXPORTDIALOG_PADDING: "Padding",
EXPORTDIALOG_SCALE: "Scale",
EXPORTDIALOG_CURRENT_PADDING: "Current padding:",
EXPORTDIALOG_SIZE_DESC: "Scale affects output size",
EXPORTDIALOG_SCALE_VALUE: "Scale:",
EXPORTDIALOG_IMAGE_SIZE: "Size:",
// Theme and background
EXPORTDIALOG_EXPORT_THEME: "Theme",
EXPORTDIALOG_THEME_LIGHT: "Light",
EXPORTDIALOG_THEME_DARK: "Dark",
EXPORTDIALOG_BACKGROUND: "Background",
EXPORTDIALOG_BACKGROUND_TRANSPARENT: "Transparent",
EXPORTDIALOG_BACKGROUND_USE_COLOR: "Use scene color",
// Selection
EXPORTDIALOG_SELECTED_ELEMENTS: "Export",
EXPORTDIALOG_SELECTED_ALL: "Entire scene",
EXPORTDIALOG_SELECTED_SELECTED: "Selection only",
// Export options
EXPORTDIALOG_EMBED_SCENE: "Include scene data?",
EXPORTDIALOG_EMBED_YES: "Yes",
EXPORTDIALOG_EMBED_NO: "No",
// PDF settings
EXPORTDIALOG_PDF_SETTINGS: "PDF",
EXPORTDIALOG_PAGE_SIZE: "Size",
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",
EXPORTDIALOG_SAVE_PDF_SETTINGS: "Save PDF settings",
EXPORTDIALOG_SAVE_CONFIRMATION: "PDF config saved to plugin settings as default",
// Buttons
EXPORTDIALOG_PNGTOFILE : "Export PNG",
EXPORTDIALOG_SVGTOFILE : "Export SVG",
EXPORTDIALOG_PNGTOVAULT : "PNG to Vault",
EXPORTDIALOG_SVGTOVAULT : "SVG to Vault",
EXPORTDIALOG_EXCALIDRAW: "Excalidraw",
EXPORTDIALOG_PNGTOCLIPBOARD : "PNG to Clipboard",
EXPORTDIALOG_SVGTOCLIPBOARD : "SVG to Clipboard",
EXPORTDIALOG_PDF: "Export PDF",
EXPORTDIALOG_PDFTOVAULT: "PDF to Vault",
}; };

View File

@@ -1,16 +1,11 @@
import { ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/excalidraw/types"; import { ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/excalidraw/types";
import { Modal, Notice, Setting, TFile, ButtonComponent } from "obsidian"; import { Modal, Setting, TFile } from "obsidian";
import { getEA } from "src/core"; import { getEA } from "src/core";
import { DEVICE } from "src/constants/constants"; import { DEVICE } from "src/constants/constants";
import { ExcalidrawAutomate } from "src/shared/ExcalidrawAutomate"; import { ExcalidrawAutomate } from "src/shared/ExcalidrawAutomate";
import ExcalidrawView from "src/view/ExcalidrawView"; import ExcalidrawView from "src/view/ExcalidrawView";
import ExcalidrawPlugin from "src/core/main"; import ExcalidrawPlugin from "src/core/main";
import { fragWithHTML, getExportPadding, getExportTheme, getPNGScale, getWithBackground, shouldEmbedScene } from "src/utils/utils"; import { fragWithHTML, getExportPadding, getExportTheme, getPNGScale, getWithBackground, shouldEmbedScene } from "src/utils/utils";
import { PageOrientation, PageSize, PDFMargin, PDFPageAlignment, PDFPageMarginString, STANDARD_PAGE_SIZES, exportSVGToClipboard } from "src/utils/exportUtils";
import { t } from "src/lang/helpers";
import { PDFExportSettings, PDFExportSettingsComponent } from "./PDFExportSettingsComponent";
export class ExportDialog extends Modal { export class ExportDialog extends Modal {
private ea: ExcalidrawAutomate; private ea: ExcalidrawAutomate;
@@ -32,17 +27,6 @@ export class ExportDialog extends Modal {
public embedScene: boolean; public embedScene: boolean;
public exportSelectedOnly: boolean; public exportSelectedOnly: boolean;
public saveToVault: boolean; public saveToVault: boolean;
public pageSize: PageSize = "A4";
public pageOrientation: PageOrientation = "portrait";
private activeTab: "image" | "pdf" = "image";
private contentContainer: HTMLDivElement;
private buttonContainerRow1: HTMLDivElement;
private buttonContainerRow2: HTMLDivElement;
public fitToPage: boolean = true;
public paperColor: "white" | "scene" | "custom" = "white";
public customPaperColor: string = "#ffffff";
public alignment: PDFPageAlignment = "center";
public margin: PDFPageMarginString = "normal";
constructor( constructor(
private plugin: ExcalidrawPlugin, private plugin: ExcalidrawPlugin,
@@ -60,15 +44,6 @@ export class ExportDialog extends Modal {
this.exportSelectedOnly = false; this.exportSelectedOnly = false;
this.saveToVault = true; this.saveToVault = true;
this.transparent = !getWithBackground(this.plugin, this.file); this.transparent = !getWithBackground(this.plugin, this.file);
this.pageSize = plugin.settings.pdfSettings.pageSize;
this.pageOrientation = plugin.settings.pdfSettings.pageOrientation;
this.fitToPage = plugin.settings.pdfSettings.fitToPage;
this.paperColor = plugin.settings.pdfSettings.paperColor;
this.customPaperColor = plugin.settings.pdfSettings.customPaperColor;
this.alignment = plugin.settings.pdfSettings.alignment;
this.margin = plugin.settings.pdfSettings.margin;
this.saveSettings = false; this.saveSettings = false;
} }
@@ -87,7 +62,7 @@ export class ExportDialog extends Modal {
onOpen(): void { onOpen(): void {
this.containerEl.classList.add("excalidraw-release"); this.containerEl.classList.add("excalidraw-release");
this.titleEl.setText(t("EXPORTDIALOG_TITLE")); this.titleEl.setText(`Export Image`);
this.hasSelectedElements = this.view.getViewSelectedElements().length > 0; this.hasSelectedElements = this.view.getViewSelectedElements().length > 0;
//@ts-ignore //@ts-ignore
this.selectedOnlySetting.setVisibility(this.hasSelectedElements); this.selectedOnlySetting.setVisibility(this.hasSelectedElements);
@@ -98,100 +73,28 @@ export class ExportDialog extends Modal {
} }
async createForm() { async createForm() {
if(DEVICE.isDesktop) {
// Create tab container
const tabContainer = this.contentEl.createDiv("nav-buttons-container");
const imageTab = tabContainer.createEl("button", {
text: t("EXPORTDIALOG_TAB_IMAGE"),
cls: `nav-button ${this.activeTab === "image" ? "is-active" : ""}`
});
const pdfTab = tabContainer.createEl("button", {
text: t("EXPORTDIALOG_TAB_PDF"),
cls: `nav-button ${this.activeTab === "pdf" ? "is-active" : ""}`
});
// Tab click handlers
imageTab.onclick = () => {
this.activeTab = "image";
imageTab.addClass("is-active");
pdfTab.removeClass("is-active");
this.renderContent();
};
pdfTab.onclick = () => {
this.activeTab = "pdf";
pdfTab.addClass("is-active");
imageTab.removeClass("is-active");
this.renderContent();
};
}
// Create content container
this.contentContainer = this.contentEl.createDiv();
this.buttonContainerRow1 = this.contentEl.createDiv({cls: "excalidraw-export-buttons-div"});
this.buttonContainerRow2 = this.contentEl.createDiv({cls: "excalidraw-export-buttons-div"});
this.buttonContainerRow2.style.marginTop = "10px";
this.renderContent();
}
private createSaveSettingsDropdown() {
new Setting(this.contentContainer)
.setName(t("EXPORTDIALOG_SAVE_SETTINGS"))
.addDropdown(dropdown =>
dropdown
.addOption("save", t("EXPORTDIALOG_SAVE_SETTINGS_SAVE"))
.addOption("one-time", t("EXPORTDIALOG_SAVE_SETTINGS_ONETIME"))
.setValue(this.saveSettings ? "save" : "one-time")
.onChange(value => {
this.saveSettings = value === "save";
})
);
}
private renderContent() {
this.contentContainer.empty();
this.buttonContainerRow1.empty();
this.buttonContainerRow2.empty();
if (this.activeTab === "image") {
this.createImageSettings();
this.createExportSettings();
this.createImageButtons();
} else {
this.createImageSettings();
this.createPDFSettings();
this.createPDFButton();
}
}
private createImageSettings() {
let scaleSetting:Setting; let scaleSetting:Setting;
let paddingSetting: Setting; let paddingSetting: Setting;
this.contentContainer.createEl("h1",{text: t("EXPORTDIALOG_IMAGE_SETTINGS")}); this.contentEl.createEl("h1",{text: "Image settings"});
this.contentContainer.createEl("p",{text: t("EXPORTDIALOG_IMAGE_DESC")}) this.contentEl.createEl("p",{text: "Transparency only affects PNGs. Excalidraw files can only be exported outside the Vault. PNGs copied to clipboard may not include the scene."})
this.createSaveSettingsDropdown();
const size = ():DocumentFragment => { const size = ():DocumentFragment => {
const width = Math.round(this.scale*this.boundingBox.width + this.padding*2); const width = Math.round(this.scale*this.boundingBox.width + this.padding*2);
const height = Math.round(this.scale*this.boundingBox.height + this.padding*2); const height = Math.round(this.scale*this.boundingBox.height + this.padding*2);
return fragWithHTML(`${t("EXPORTDIALOG_SIZE_DESC")}<br>${t("EXPORTDIALOG_SCALE_VALUE")} <b>${this.scale}</b><br>${t("EXPORTDIALOG_IMAGE_SIZE")} <b>${width}x${height}</b>`); return fragWithHTML(`The lager the scale, the larger the image.<br>Scale: <b>${this.scale}</b><br>Image size: <b>${width}x${height}</b>`);
} }
const padding = ():DocumentFragment => { const padding = ():DocumentFragment => {
return fragWithHTML(`${t("EXPORTDIALOG_CURRENT_PADDING")} <b>${this.padding}</b>`); return fragWithHTML(`Current image padding is <b>${this.padding}</b>`);
} }
paddingSetting = new Setting(this.contentContainer) paddingSetting = new Setting(this.contentEl)
.setName(t("EXPORTDIALOG_PADDING")) .setName("Image padding")
.setDesc(padding()) .setDesc(padding())
.addSlider(slider => { .addSlider(slider => {
slider slider
.setLimits(0,100,1) .setLimits(0,50,1)
.setValue(this.padding) .setValue(this.padding)
.onChange(value => { .onChange(value => {
this.padding = value; this.padding = value;
@@ -200,12 +103,12 @@ export class ExportDialog extends Modal {
}) })
}) })
scaleSetting = new Setting(this.contentContainer) scaleSetting = new Setting(this.contentEl)
.setName(t("EXPORTDIALOG_SCALE")) .setName("PNG Scale")
.setDesc(size()) .setDesc(size())
.addSlider(slider => .addSlider(slider =>
slider slider
.setLimits(0.2,7,0.1) .setLimits(0.5,5,0.5)
.setValue(this.scale) .setValue(this.scale)
.onChange(value => { .onChange(value => {
this.scale = value; this.scale = value;
@@ -213,213 +116,109 @@ export class ExportDialog extends Modal {
}) })
) )
new Setting(this.contentContainer) new Setting(this.contentEl)
.setName(t("EXPORTDIALOG_EXPORT_THEME")) .setName("Export theme")
.addDropdown(dropdown => .addDropdown(dropdown =>
dropdown dropdown
.addOption("light", t("EXPORTDIALOG_THEME_LIGHT")) .addOption("light","Light")
.addOption("dark", t("EXPORTDIALOG_THEME_DARK")) .addOption("dark","Dark")
.setValue(this.theme) .setValue(this.theme)
.onChange(value => { .onChange(value => {
this.theme = value; this.theme = value;
}) })
) )
new Setting(this.contentContainer) new Setting(this.contentEl)
.setName(t("EXPORTDIALOG_BACKGROUND")) .setName("Background color")
.addDropdown(dropdown => .addDropdown(dropdown =>
dropdown dropdown
.addOption("transparent", t("EXPORTDIALOG_BACKGROUND_TRANSPARENT")) .addOption("transparent","Transparent")
.addOption("with-color", t("EXPORTDIALOG_BACKGROUND_USE_COLOR")) .addOption("with-color","Use scene background color")
.setValue(this.transparent?"transparent":"with-color") .setValue(this.transparent?"transparent":"with-color")
.onChange(value => { .onChange(value => {
this.transparent = value === "transparent"; this.transparent = value === "transparent";
}) })
) )
this.selectedOnlySetting = new Setting(this.contentContainer) new Setting(this.contentEl)
.setName(t("EXPORTDIALOG_SELECTED_ELEMENTS")) .setName("Save or one-time settings?")
.addDropdown(dropdown => .addDropdown(dropdown =>
dropdown dropdown
.addOption("all", t("EXPORTDIALOG_SELECTED_ALL")) .addOption("save","Save these settings as the preset for this image")
.addOption("selected", t("EXPORTDIALOG_SELECTED_SELECTED")) .addOption("one-time","These are one-time settings")
.setValue(this.exportSelectedOnly?"selected":"all") .setValue(this.saveSettings?"save":"one-time")
.onChange(value => { .onChange(value => {
this.exportSelectedOnly = value === "selected"; this.saveSettings = value === "save";
}) })
); )
}
private createExportSettings() { this.contentEl.createEl("h1",{text:"Export settings"});
new Setting(this.contentContainer)
.setName(t("EXPORTDIALOG_EMBED_SCENE")) new Setting(this.contentEl)
.setName("Embed the Excalidraw scene in the exported file?")
.addDropdown(dropdown => .addDropdown(dropdown =>
dropdown dropdown
.addOption("embed",t("EXPORTDIALOG_EMBED_YES")) .addOption("embed","Embed scene")
.addOption("no-embed",t("EXPORTDIALOG_EMBED_NO")) .addOption("no-embed","Do not embed scene")
.setValue(this.embedScene?"embed":"no-embed") .setValue(this.embedScene?"embed":"no-embed")
.onChange(value => { .onChange(value => {
this.embedScene = value === "embed"; this.embedScene = value === "embed";
}) })
) )
}
private createPDFSettings() {
if (!DEVICE.isDesktop) return;
this.contentContainer.createEl("h1", { text: t("EXPORTDIALOG_PDF_SETTINGS") });
const pdfSettings: PDFExportSettings = {
pageSize: this.pageSize,
pageOrientation: this.pageOrientation,
fitToPage: this.fitToPage,
paperColor: this.paperColor,
customPaperColor: this.customPaperColor,
alignment: this.alignment,
margin: this.margin
};
new PDFExportSettingsComponent(
this.contentContainer,
pdfSettings,
() => {
this.pageSize = pdfSettings.pageSize;
this.pageOrientation = pdfSettings.pageOrientation;
this.fitToPage = pdfSettings.fitToPage;
this.paperColor = pdfSettings.paperColor;
this.customPaperColor = pdfSettings.customPaperColor;
this.alignment = pdfSettings.alignment;
this.margin = pdfSettings.margin;
}
).render();
}
private createImageButtons() {
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);
this.close();
};
}
const bPNGVault = this.buttonContainerRow1.createEl("button", {
text: t("EXPORTDIALOG_PNGTOVAULT"),
cls: "excalidraw-export-button"
});
bPNGVault.onclick = () => {
this.view.savePNG(this.view.getScene(this.hasSelectedElements && this.exportSelectedOnly));
this.close();
};
const bPNGClipboard = this.buttonContainerRow1.createEl("button", {
text: t("EXPORTDIALOG_PNGTOCLIPBOARD"),
cls: "excalidraw-export-button"
});
bPNGClipboard.onclick = async () => {
this.view.exportPNGToClipboard(this.embedScene, this.hasSelectedElements && this.exportSelectedOnly);
this.close();
};
if(DEVICE.isDesktop) { if(DEVICE.isDesktop) {
const bExcalidraw = this.buttonContainerRow2.createEl("button", { new Setting(this.contentEl)
text: t("EXPORTDIALOG_EXCALIDRAW"), .setName("Where to save the image?")
cls: "excalidraw-export-button" .addDropdown(dropdown =>
}); dropdown
bExcalidraw.onclick = () => { .addOption("vault","Save image to your Vault")
this.view.exportExcalidraw(); .addOption("outside","Export image outside your Vault")
this.close(); .setValue(this.saveToVault?"vault":"outside")
}; .onChange(value => {
this.saveToVault = value === "vault";
const bSVG = this.buttonContainerRow2.createEl("button", { })
text: t("EXPORTDIALOG_SVGTOFILE"), )
cls: "excalidraw-export-button"
});
bSVG.onclick = () => {
this.view.exportSVG(this.embedScene, this.hasSelectedElements && this.exportSelectedOnly);
this.close();
};
} }
const bSVGVault = this.buttonContainerRow2.createEl("button", { this.selectedOnlySetting = new Setting(this.contentEl)
text: t("EXPORTDIALOG_SVGTOVAULT"), .setName("Export entire scene or just selected elements?")
cls: "excalidraw-export-button" .addDropdown(dropdown =>
}); dropdown
bSVGVault.onclick = () => { .addOption("all","Export entire scene")
this.view.saveSVG(this.view.getScene(this.hasSelectedElements && this.exportSelectedOnly)); .addOption("selected","Export selected elements")
.setValue(this.exportSelectedOnly?"selected":"all")
.onChange(value => {
this.exportSelectedOnly = value === "selected";
})
)
const div = this.contentEl.createDiv({cls: "excalidraw-prompt-buttons-div"});
const bPNG = div.createEl("button", { text: "PNG to File", cls: "excalidraw-prompt-button"});
bPNG.onclick = () => {
this.saveToVault
? this.view.savePNG(this.view.getScene(this.hasSelectedElements && this.exportSelectedOnly))
: this.view.exportPNG(this.embedScene,this.hasSelectedElements && this.exportSelectedOnly);
this.close(); this.close();
}; };
const bSVG = div.createEl("button", { text: "SVG to File", cls: "excalidraw-prompt-button" });
const bSVGClipboard = this.buttonContainerRow2.createEl("button", { bSVG.onclick = () => {
text: t("EXPORTDIALOG_SVGTOCLIPBOARD"), this.saveToVault
cls: "excalidraw-export-button" ? this.view.saveSVG(this.view.getScene(this.hasSelectedElements && this.exportSelectedOnly))
}); : this.view.exportSVG(this.embedScene,this.hasSelectedElements && this.exportSelectedOnly);
bSVGClipboard.onclick = async () => {
const svg = await this.view.getSVG(this.embedScene, this.hasSelectedElements && this.exportSelectedOnly);
exportSVGToClipboard(svg);
this.close(); this.close();
}; };
} const bExcalidraw = div.createEl("button", { text: "Excalidraw", cls: "excalidraw-prompt-button" });
bExcalidraw.onclick = () => {
private createPDFButton() { this.view.exportExcalidraw(this.hasSelectedElements && this.exportSelectedOnly);
const bSavePDFSettings = this.buttonContainerRow1.createEl("button", this.close();
{ text: t("EXPORTDIALOG_SAVE_PDF_SETTINGS"), cls: "excalidraw-export-button" } };
); if(DEVICE.isDesktop) {
bSavePDFSettings.onclick = async () => { const bPNGClipboard = div.createEl("button", { text: "PNG to Clipboard", cls: "excalidraw-prompt-button" });
//in case sync loaded a new version of settings in the mean time bPNGClipboard.onclick = () => {
await this.plugin.loadSettings(); this.view.exportPNGToClipboard(this.embedScene, this.hasSelectedElements && this.exportSelectedOnly);
this.plugin.settings.pdfSettings = { this.close();
pageSize: this.pageSize,
pageOrientation: this.pageOrientation,
fitToPage: this.fitToPage,
paperColor: this.paperColor,
customPaperColor: this.customPaperColor,
alignment: this.alignment,
margin: this.margin
}; };
await this.plugin.saveSettings();
new Notice(t("EXPORTDIALOG_SAVE_CONFIRMATION"));
};
const bPDFVault = this.buttonContainerRow1.createEl("button", {
text: t("EXPORTDIALOG_PDFTOVAULT"),
cls: "excalidraw-export-button"
});
bPDFVault.onclick = () => {
this.view.exportPDF(
true,
this.hasSelectedElements && this.exportSelectedOnly,
this.pageSize,
this.pageOrientation
);
this.close();
};
if (!DEVICE.isDesktop) return;
const bPDFExport = this.buttonContainerRow1.createEl("button", {
text: t("EXPORTDIALOG_PDF"),
cls: "excalidraw-export-button"
});
bPDFExport.onclick = () => {
this.view.exportPDF(
false,
this.hasSelectedElements && this.exportSelectedOnly,
this.pageSize,
this.pageOrientation
);
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";
} }
} }
} }

View File

@@ -1,138 +0,0 @@
import { Setting } from "obsidian";
import { PageOrientation, PageSize, PDFPageAlignment, PDFPageMarginString, STANDARD_PAGE_SIZES } from "src/utils/exportUtils";
import { t } from "src/lang/helpers";
export interface PDFExportSettings {
pageSize: PageSize;
pageOrientation: PageOrientation;
fitToPage: boolean;
paperColor: "white" | "scene" | "custom";
customPaperColor: string;
alignment: PDFPageAlignment;
margin: PDFPageMarginString;
}
export class PDFExportSettingsComponent {
constructor(
private contentEl: HTMLElement,
private settings: PDFExportSettings,
private update?: Function,
) {
if (!update) this.update = () => {};
}
render() {
const pageSizeOptions: Record<string, string> = Object.keys(STANDARD_PAGE_SIZES)
.reduce((acc, key) => ({
...acc,
[key]: key
}), {});
new Setting(this.contentEl)
.setName(t("EXPORTDIALOG_PAGE_SIZE"))
.addDropdown(dropdown =>
dropdown
.addOptions(pageSizeOptions)
.setValue(this.settings.pageSize)
.onChange(value => {
this.settings.pageSize = value as PageSize;
this.update();
})
);
new Setting(this.contentEl)
.setName(t("EXPORTDIALOG_PAGE_ORIENTATION"))
.addDropdown(dropdown =>
dropdown
.addOptions({
"portrait": t("EXPORTDIALOG_ORIENTATION_PORTRAIT"),
"landscape": t("EXPORTDIALOG_ORIENTATION_LANDSCAPE")
})
.setValue(this.settings.pageOrientation)
.onChange(value => {
this.settings.pageOrientation = value as PageOrientation;
this.update();
})
);
new Setting(this.contentEl)
.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.settings.fitToPage ? "fit" : "scale")
.onChange(value => {
this.settings.fitToPage = value === "fit";
this.update();
})
);
new Setting(this.contentEl)
.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.settings.margin)
.onChange(value => {
this.settings.margin = value as PDFPageMarginString;
this.update();
})
);
const paperColorSetting = new Setting(this.contentEl)
.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.settings.paperColor)
.onChange(value => {
this.settings.paperColor = value as typeof this.settings.paperColor;
colorInput.style.display = (value === "custom") ? "block" : "none";
this.update();
})
);
const colorInput = paperColorSetting.controlEl.createEl("input", {
type: "color",
value: this.settings.customPaperColor
});
colorInput.style.width = "50px";
colorInput.style.marginLeft = "10px";
colorInput.style.display = this.settings.paperColor === "custom" ? "block" : "none";
colorInput.addEventListener("change", (e) => {
this.settings.customPaperColor = (e.target as HTMLInputElement).value;
this.update();
});
new Setting(this.contentEl)
.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.settings.alignment)
.onChange(value => {
this.settings.alignment = value as PDFPageAlignment;
this.update();
})
);
}
}

View File

@@ -224,62 +224,6 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
desc: "Use ExcalidrawAutomate.getExportSettings(boolean,boolean) to create an ExportSettings object.\nUse ExcalidrawAutomate.getEmbeddedFilesLoader(boolean?) to create an EmbeddedFilesLoader object.", desc: "Use ExcalidrawAutomate.getExportSettings(boolean,boolean) to create an ExportSettings object.\nUse ExcalidrawAutomate.getEmbeddedFilesLoader(boolean?) to create an EmbeddedFilesLoader object.",
after: "", after: "",
}, },
{
field: "createPDF",
code: "async createPDF({SVG: SVGSVGElement[], scale?: PDFExportScale, pageProps?: PDFPageProperties}): Promise<ArrayBuffer>",
desc: "",
after: "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" +
"@returns {Promise<ArrayBuffer>} - A promise that resolves to an ArrayBuffer containing the PDF data.\n" +
"\n" +
"@typedef {Object} PDFExportScale\n" +
"@property {boolean} fitToPage - Whether to fit the SVG to the page.\n" +
"@property {number} [zoom=1] - The zoom level for the SVG. Used only if fitToPage is false. If the SVG does not fit the page, it will be tiled over multiple pages.\n" +
"\n" +
"@typedef {Object} PDFPageProperties\n" +
"@property {{width: number, height: number}} [dimensions] - The dimensions of the PDF pages. 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.\n" +
"@property {PDFPageAlignment} alignment - The alignment of the SVG on the PDF pages.\n" +
"\n" +
"@example\n" +
"const pdfData = await createPDF({\n" +
" SVG: [svgElement1, svgElement2],\n" +
" scale: { fitToPage: true },\n" +
" pageProps: {\n" +
" dimensions: { width: 595.28, height: 841.89 },\n" +
" backgroundColor: \"#ffffff\",\n" +
" margin: { left: 20, right: 20, top: 20, bottom: 20 },\n" +
" alignment: \"center\"\n" +
" }\n" +
"});",
},
{
field: "getPagePDFDimensions",
code: "getPagePDFDimensions(pageSize: PageSize, orientation: PageOrientation): PageDimensions",
desc: "Returns the dimensions of a standard page size in points (pt).\n" +
"\n" +
"@param {PageSize} pageSize - The standard page size. Possible values are \"A0\", \"A1\", \"A2\", \"A3\", \"A4\", \"A5\", \"Letter\", \"Legal\", \"Tabloid\".\n" +
"@param {PageOrientation} orientation - The orientation of the page. Possible values are \"portrait\" and \"landscape\".\n" +
"@returns {PageDimensions} - An object containing the width and height of the page in points (pt).\n" +
"\n" +
"@typedef {Object} PageDimensions\n" +
"@property {number} width - The width of the page in points (pt).\n" +
"@property {number} height - The height of the page in points (pt).\n" +
"\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: "",
},
{ {
field: "createPNG", field: "createPNG",
code: "async createPNG(templatePath?: string, scale?: number, exportSettings?: ExportSettings, loader?: EmbeddedFilesLoader, theme?: string,padding?: number): Promise<any>;", code: "async createPNG(templatePath?: string, scale?: number, exportSettings?: ExportSettings, loader?: EmbeddedFilesLoader, theme?: string,padding?: number): Promise<any>;",

View File

@@ -83,7 +83,6 @@ import { ExcalidrawLib } from "../types/excalidrawLib";
import { GlobalPoint } from "@zsviczian/excalidraw/types/math/types"; import { GlobalPoint } from "@zsviczian/excalidraw/types/math/types";
import { AddImageOptions, ImageInfo, SVGColorInfo } from "src/types/excalidrawAutomateTypes"; 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 { _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";
extendPlugins([ extendPlugins([
HarmonyPlugin, HarmonyPlugin,
@@ -931,75 +930,6 @@ export class ExcalidrawAutomate {
} }
}; };
/**
* Returns the dimensions of a standard page size in points (pt).
*
* @param {PageSize} pageSize - The standard page size. Possible values are "A0", "A1", "A2", "A3", "A4", "A5", "Letter", "Legal", "Tabloid".
* @param {PageOrientation} orientation - The orientation of the page. Possible values are "portrait" and "landscape".
* @returns {PageDimensions} - An object containing the width and height of the page in points (pt).
*
* @typedef {Object} PageDimensions
* @property {number} width - The width of the page in points (pt).
* @property {number} height - The height of the page in points (pt).
*
* @example
* const dimensions = getPageDimensions("A4", "portrait");
* console.log(dimensions); // { width: 595.28, height: 841.89 }
*/
getPagePDFDimensions(pageSize: PageSize, orientation: PageOrientation): PageDimensions {
return getPageDimensions(pageSize, orientation);
}
/**
* Creates a PDF from the provided SVG elements with specified scaling and page properties.
*
* @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 {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 },
* pageProps: {
* dimensions: { width: 595.28, height: 841.89 },
* backgroundColor: "#ffffff",
* margin: { left: 20, right: 20, top: 20, bottom: 20 },
* alignment: "center"
* }
* });
*/
async createPDF({
SVG,
scale = { fitToPage: true, zoom: 1 },
pageProps,
}: {
SVG: SVGSVGElement[];
scale?: PDFExportScale;
pageProps?: PDFPageProperties;
}): Promise<ArrayBuffer> {
if(!pageProps) {
pageProps = {
alignment: this.plugin.settings.pdfSettings.alignment,
margin: getMarginValue(this.plugin.settings.pdfSettings.margin),
};
}
if(!pageProps.dimensions) {
pageProps.dimensions = getPageDimensions(
this.plugin.settings.pdfSettings.pageSize,
this.plugin.settings.pdfSettings.pageOrientation
)
}
if(!pageProps.backgroundColor) {
pageProps.backgroundColor = "#ffffff";
}
return await exportToPDF({SVG, scale, pageProps});
}
/** /**
* Creates an SVG image from the ExcalidrawAutomate elements and the template provided. * Creates an SVG image from the ExcalidrawAutomate elements and the template provided.
* @param {string} [templatePath] - The template path to use for the SVG. * @param {string} [templatePath] - The template path to use for the SVG.
@@ -2740,6 +2670,40 @@ export class ExcalidrawAutomate {
pointerPosition: { x: number; y: number }; //the pointer position on canvas pointerPosition: { x: number; y: number }; //the pointer position on canvas
}) => boolean = null; }) => boolean = null;
/**
* If set, this callback is triggered when a image is being saved in Excalidraw.
* You can use this callback to customize the naming and path of pasted images to avoid
* default names like "Pasted image 123147170.png" being saved in the attachments folder,
* and instead use more meaningful names based on the Excalidraw file or other criteria,
* plus save the image in a different folder.
*
* If the function returns null or undefined, the normal Excalidraw operation will continue
* with the excalidraw generated name and default path.
* If a filepath is returned, that will be used. Include the full Vault filepath and filename
* with the file extension.
* The currentImageName is the name of the image generated by excalidraw or provided during paste.
*
* @param data - An object containing the following properties:
* @property {string} [currentImageName] - Default name for the image.
* @property {string} drawingFilePath - The file path of the Excalidraw file where the image is being used.
*
* @returns {string} - The new filepath for the image including full vault path and extension.
*
* Example usage:
* ```
* onImageFilePathHook: (data) => {
* const { currentImageName, drawingFilePath } = data;
* // Generate a new filepath based on the drawing file name and other criteria
* const ext = currentImageName.split('.').pop();
* return `${drawingFileName} - ${currentImageName || 'image'}.${ext}`;
* }
* ```
*/
onImageFilePathHook: (data: {
currentImageName: string; // Excalidraw generated name of the image, or the name received from the file system.
drawingFilePath: string; // The full filepath of the Excalidraw file where the image is being used.
}) => string = null;
/** /**
* if set, this callback is triggered, when an Excalidraw file is opened * if set, this callback is triggered, when an Excalidraw file is opened
* You can use this callback in case you want to do something additional when the file is opened. * You can use this callback in case you want to do something additional when the file is opened.

View File

@@ -20,7 +20,7 @@ import {
loadSceneFonts, loadSceneFonts,
} from "../constants/constants"; } from "../constants/constants";
import ExcalidrawPlugin from "../core/main"; import ExcalidrawPlugin from "../core/main";
import { TextMode } from "../view/ExcalidrawView"; import ExcalidrawView, { TextMode } from "../view/ExcalidrawView";
import { import {
addAppendUpdateCustomData, addAppendUpdateCustomData,
compress, compress,
@@ -52,7 +52,7 @@ import { getMermaidImageElements, getMermaidText, shouldRenderMermaid } from "..
import { DEBUGGING, debug } from "../utils/debugHelper"; import { DEBUGGING, debug } from "../utils/debugHelper";
import { Mutable } from "@zsviczian/excalidraw/types/excalidraw/utility-types"; import { Mutable } from "@zsviczian/excalidraw/types/excalidraw/utility-types";
import { updateElementIdsInScene } from "../utils/excalidrawSceneUtils"; import { updateElementIdsInScene } from "../utils/excalidrawSceneUtils";
import { getNewUniqueFilepath } from "../utils/fileUtils"; import { getNewUniqueFilepath, splitFolderAndFilename } from "../utils/fileUtils";
import { t } from "../lang/helpers"; import { t } from "../lang/helpers";
import { displayFontMessage } from "../utils/excalidrawViewUtils"; import { displayFontMessage } from "../utils/excalidrawViewUtils";
import { getPDFRect } from "../utils/PDFUtils"; import { getPDFRect } from "../utils/PDFUtils";
@@ -480,7 +480,7 @@ export class ExcalidrawData {
selectedElementIds: {[key:string]:boolean} = {}; //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/609 selectedElementIds: {[key:string]:boolean} = {}; //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/609
constructor( constructor(
private plugin: ExcalidrawPlugin, private plugin: ExcalidrawPlugin, private view?: ExcalidrawView,
) { ) {
this.app = this.plugin.app; this.app = this.plugin.app;
this.files = new Map<FileId, EmbeddedFile>(); this.files = new Map<FileId, EmbeddedFile>();
@@ -1546,13 +1546,23 @@ export class ExcalidrawData {
} }
} }
const x = await getAttachmentsFolderAndFilePath(this.app, this.file.path, fname); let hookFilepath:string;
const filepath = getNewUniqueFilepath(this.app.vault,fname,x.folder); const ea = this.view?.getHookServer();
if(ea?.onImageFilePathHook) {
hookFilepath = ea.onImageFilePathHook({
currentImageName: fname,
drawingFilePath: this.view?.file?.path,
})
}
/* let filepath:string;
const filepath = ( if(hookFilepath) {
await getAttachmentsFolderAndFilePath(this.app, this.file.path, fname) const {folderpath, filename} = splitFolderAndFilename(hookFilepath);
).filepath;*/ filepath = getNewUniqueFilepath(this.app.vault,filename,folderpath);
} else {
const x = await getAttachmentsFolderAndFilePath(this.app, this.file.path, fname);
filepath = getNewUniqueFilepath(this.app.vault,fname,x.folder);
}
const arrayBuffer = await getBinaryFileFromDataURL(dataURL); const arrayBuffer = await getBinaryFileFromDataURL(dataURL);
if(!arrayBuffer) return null; if(!arrayBuffer) return null;

View File

@@ -1,375 +0,0 @@
import { PDFDocument, rgb } from '@cantoo/pdf-lib';
import { getEA } from 'src/core';
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;
zoom?: number;
}
export interface PDFMargin {
left: number;
right: number;
top: number;
bottom: number;
}
export interface PDFPageProperties {
dimensions?: {width: number; height: number};
backgroundColor?: string;
margin: PDFMargin;
alignment: PDFPageAlignment;
}
export interface PageDimensions {
width: number;
height: number;
}
export type PageOrientation = "portrait" | "landscape";
// All dimensions in points (pt)
export const STANDARD_PAGE_SIZES = {
"A0": { width: 2383.94, height: 3370.39 },
"A1": { width: 1683.78, height: 2383.94 },
"A2": { width: 1190.55, height: 1683.78 },
"A3": { width: 841.89, height: 1190.55 },
"A4": { width: 595.28, height: 841.89 },
"A5": { width: 419.53, height: 595.28 },
"Letter": { width: 612, height: 792 },
"Legal": { width: 612, height: 1008 },
"Tabloid": { width: 792, height: 1224 },
} as const;
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: 25, right: 25, top: 25, bottom: 25 };
default: return { left: 25, right: 25, top: 25, bottom: 25 };
}
}
export function getPageDimensions(pageSize: PageSize, orientation: PageOrientation): PageDimensions {
const dimensions = STANDARD_PAGE_SIZES[pageSize];
return orientation === "portrait"
? { width: dimensions.width, height: dimensions.height }
: { width: dimensions.height, height: dimensions.width };
}
interface SVGDimensions {
width: number;
height: number;
x: number;
y: number;
sourceX?: number;
sourceY?: number;
sourceWidth?: number;
sourceHeight?: number;
}
function calculatePosition(
svgWidth: number,
svgHeight: number,
pageWidth: number,
pageHeight: number,
margin: PDFMargin,
alignment: PDFPageAlignment,
scale: PDFExportScale
): {x: number, y: number} {
const availableWidth = pageWidth - margin.left - margin.right;
const availableHeight = pageHeight - margin.top - margin.bottom;
console.log(JSON.stringify({
message: 'PDF Position Debug',
input: {
svgWidth,
svgHeight,
pageWidth,
pageHeight,
margin,
alignment,
scale
},
calculated: {
availableWidth,
availableHeight
}
}));
let x = margin.left;
let y = margin.bottom;
// Handle horizontal alignment
if (alignment.includes('center')) {
x = margin.left + (availableWidth - svgWidth) / 2;
} else if (alignment.includes('right')) {
x = margin.left + availableWidth - svgWidth;
}
// Handle vertical alignment
if (alignment.startsWith('center')) {
y = margin.bottom + (availableHeight - svgHeight) / 2;
} else if (alignment.startsWith('top')) {
y = margin.bottom;
} else if (alignment.startsWith('bottom')) {
y = pageHeight - margin.top - svgHeight;
}
console.log(JSON.stringify({
message: 'PDF Position Intermediate',
x,
y,
alignment,
availableHeight,
marginTop: margin.top,
marginBottom: margin.bottom,
svgHeight,
pageHeight
}));
console.log(JSON.stringify({
message: 'PDF Position Result',
x,
y,
finalPosition: {
bottom: y,
top: y + svgHeight,
left: x,
right: x + svgWidth
}
}));
return {x, y};
}
function calculateDimensions(
svgWidth: number,
svgHeight: number,
pageDim: PageDimensions,
margin: PDFPageProperties['margin'],
scale: PDFExportScale,
alignment: PDFPageAlignment
): SVGDimensions[] {
const availableWidth = pageDim.width - margin.left - margin.right;
const availableHeight = pageDim.height - margin.top - margin.bottom;
let finalWidth: number;
let finalHeight: number;
if (scale.fitToPage) {
const ratio = Math.min(availableWidth / svgWidth, availableHeight / svgHeight);
finalWidth = svgWidth * ratio;
finalHeight = svgHeight * ratio;
const position = calculatePosition(
finalWidth,
finalHeight,
pageDim.width,
pageDim.height,
margin,
alignment,
scale
);
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,
scale
);
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;
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)
});
}
}
return dimensions;
}
}
}
async function addSVGToPage(
pdfDoc: PDFDocument,
svg: SVGSVGElement,
dimensions: SVGDimensions,
pageDim: PageDimensions,
backgroundColor?: string
) {
const page = pdfDoc.addPage([pageDim.width, pageDim.height]);
if (backgroundColor && backgroundColor !== '#ffffff') {
const { r, g, b } = hexToRGB(backgroundColor);
page.drawRectangle({
x: 0,
y: 0,
width: pageDim.width,
height: pageDim.height,
color: rgb(r/255, g/255, b/255),
});
}
// Clone and modify SVG for tiling if needed
let svgToEmbed = svg;
if (dimensions.sourceX !== undefined) {
svgToEmbed = svg.cloneNode(true) as SVGSVGElement;
const viewBox = `${dimensions.sourceX} ${dimensions.sourceY} ${dimensions.sourceWidth} ${dimensions.sourceHeight}`;
svgToEmbed.setAttribute('viewBox', viewBox);
svgToEmbed.setAttribute('width', String(dimensions.sourceWidth));
svgToEmbed.setAttribute('height', String(dimensions.sourceHeight));
}
const svgImage = await pdfDoc.embedSvg(svgToEmbed.outerHTML);
console.log(JSON.stringify({message: "addSVGToPage", dimensions, html: svgToEmbed.outerHTML}));
// Adjust y-coordinate to account for PDF coordinate system
const adjustedY = pageDim.height - dimensions.y;
page.drawSvg(svgImage, {
x: dimensions.x,
y: adjustedY,
width: dimensions.width,
height: dimensions.height,
});
console.log(JSON.stringify({
message: 'PDF Draw SVG',
x: dimensions.x,
y: adjustedY,
width: dimensions.width,
height: dimensions.height
}));
return page;
}
export async function exportToPDF({
SVG,
scale = { fitToPage: true, zoom: 1 },
pageProps,
}: {
SVG: SVGSVGElement[];
scale: PDFExportScale;
pageProps: PDFPageProperties;
}): Promise<ArrayBuffer> {
const pdfDoc = await PDFDocument.create();
for (const svg of SVG) {
const svgWidth = parseFloat(svg.getAttribute('width') || '0');
const svgHeight = parseFloat(svg.getAttribute('height') || '0');
const dimensions = calculateDimensions(
svgWidth,
svgHeight,
pageProps.dimensions,
pageProps.margin,
scale,
pageProps.alignment
);
for (const dim of dimensions) {
await addSVGToPage(pdfDoc, svg, dim, pageProps.dimensions, pageProps.backgroundColor);
}
}
return pdfDoc.save();
}
function hexToRGB(hex: string): { r: number; g: number; b: number } {
const ea = getEA();
const color = ea.getCM(hex);
if (color) {
return { r: color.red, g: color.green, b: color.blue };
}
return {r: 255, g: 255, b: 255};
}
// Helper function to split SVG into pages if needed
function splitSVGIntoPages(
svg: SVGSVGElement,
maxWidth: number,
maxHeight: number
): SVGSVGElement[] {
const width = parseFloat(svg.getAttribute('width') || '0');
const height = parseFloat(svg.getAttribute('height') || '0');
if (width <= maxWidth && height <= maxHeight) {
return [svg];
}
const pages: SVGSVGElement[] = [];
const cols = Math.ceil(width / maxWidth);
const rows = Math.ceil(height / maxHeight);
for (let row = 0; row < rows; row++) {
for (let col = 0; col < cols; col++) {
const viewBox = `${col * maxWidth} ${row * maxHeight} ${maxWidth} ${maxHeight}`;
const clonedSvg = svg.cloneNode(true) as SVGSVGElement;
clonedSvg.setAttribute('viewBox', viewBox);
clonedSvg.setAttribute('width', String(maxWidth));
clonedSvg.setAttribute('height', String(maxHeight));
pages.push(clonedSvg);
}
}
return pages;
}
export async function exportSVGToClipboard(svg: SVGSVGElement) {
try {
const svgString = svg.outerHTML;
await navigator.clipboard.writeText(svgString);
} catch (error) {
console.error("Failed to copy SVG to clipboard: ", error);
}
}

View File

@@ -290,17 +290,6 @@ export const blobToBase64 = async (blob: Blob): Promise<string> => {
return btoa(binary); return btoa(binary);
} }
export const arrayBufferToBase64 = (arrayBuffer: ArrayBuffer): string => {
const bytes = new Uint8Array(arrayBuffer);
let binary = '';
for (let i = 0; i < bytes.length; i++) {
binary += String.fromCharCode(bytes[i]);
}
return btoa(binary);
};
export const getPDFDoc = async (f: TFile): Promise<any> => { export const getPDFDoc = async (f: TFile): Promise<any> => {
if(typeof window.pdfjsLib === "undefined") await loadPdfJs(); if(typeof window.pdfjsLib === "undefined") await loadPdfJs();
return await window.pdfjsLib.getDocument(EXCALIDRAW_PLUGIN.app.vault.getResourcePath(f)).promise; return await window.pdfjsLib.getDocument(EXCALIDRAW_PLUGIN.app.vault.getResourcePath(f)).promise;
@@ -493,22 +482,3 @@ export const hasExcalidrawEmbeddedImagesTreeChanged = (sourceFile: TFile, mtime:
const fileList = getExcalidrawEmbeddedFilesFiletree(sourceFile, plugin); const fileList = getExcalidrawEmbeddedFilesFiletree(sourceFile, plugin);
return fileList.some(f=>f.stat.mtime > mtime); return fileList.some(f=>f.stat.mtime > mtime);
} }
export async function createOrOverwriteFile(app: App, path: string, content: string | ArrayBuffer): Promise<TFile> {
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);
}
}

View File

@@ -76,9 +76,7 @@ import {
getExcalidrawMarkdownHeaderSection, getExcalidrawMarkdownHeaderSection,
} from "../shared/ExcalidrawData"; } from "../shared/ExcalidrawData";
import { import {
arrayBufferToBase64,
checkAndCreateFolder, checkAndCreateFolder,
createOrOverwriteFile,
download, download,
getDataURLFromURL, getDataURLFromURL,
getIMGFilename, getIMGFilename,
@@ -151,8 +149,6 @@ import { getPDFCropRect } from "../utils/PDFUtils";
import { Position, ViewSemaphores } from "../types/excalidrawViewTypes"; import { Position, ViewSemaphores } from "../types/excalidrawViewTypes";
import { DropManager } from "./managers/DropManager"; import { DropManager } from "./managers/DropManager";
import { ImageInfo } from "src/types/excalidrawAutomateTypes"; import { ImageInfo } from "src/types/excalidrawAutomateTypes";
import { exportToPDF, getMarginValue, getPageDimensions, PageOrientation, PageSize } from "src/utils/exportUtils";
import { create } from "domain";
const EMBEDDABLE_SEMAPHORE_TIMEOUT = 2000; const EMBEDDABLE_SEMAPHORE_TIMEOUT = 2000;
const PREVENT_RELOAD_TIMEOUT = 2000; const PREVENT_RELOAD_TIMEOUT = 2000;
@@ -372,7 +368,7 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
constructor(leaf: WorkspaceLeaf, plugin: ExcalidrawPlugin) { constructor(leaf: WorkspaceLeaf, plugin: ExcalidrawPlugin) {
super(leaf); super(leaf);
this._plugin = plugin; this._plugin = plugin;
this.excalidrawData = new ExcalidrawData(plugin); this.excalidrawData = new ExcalidrawData(plugin, this);
this.canvasNodeFactory = new CanvasNodeFactory(this); this.canvasNodeFactory = new CanvasNodeFactory(this);
this.setHookServer(); this.setHookServer();
this.dropManager = new DropManager(this); this.dropManager = new DropManager(this);
@@ -519,13 +515,19 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
} }
const exportImage = async (filepath:string, theme?:string) => { const exportImage = async (filepath:string, theme?:string) => {
const file = this.app.vault.getAbstractFileByPath(normalizePath(filepath));
const svg = await this.svg(scene,theme, embedScene, true); const svg = await this.svg(scene,theme, embedScene, true);
if (!svg) { if (!svg) {
return; return;
} }
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/2026 //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/2026
const svgString = svg.outerHTML; const svgString = svg.outerHTML;
await createOrOverwriteFile(this.app, filepath, svgString); if (file && file instanceof TFile) {
await this.app.vault.modify(file, svgString);
} else {
await this.app.vault.create(filepath, svgString);
}
} }
if(this.plugin.settings.autoExportLightAndDark) { if(this.plugin.settings.autoExportLightAndDark) {
@@ -553,83 +555,6 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
); );
} }
public async getSVG(embedScene?: boolean, selectedOnly?: boolean):Promise<SVGSVGElement> {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.getSVG, "ExcalidrawView.getSVG", embedScene, selectedOnly);
if (!this.excalidrawAPI || !this.file) {
return;
}
const svg = await this.svg(this.getScene(selectedOnly),undefined,embedScene, true);
if (!svg) {
return;
}
return svg;
}
public async exportPDF(
toVault: boolean,
selectedOnly?: boolean,
pageSize: PageSize = "A4",
orientation: PageOrientation = "portrait"
): Promise<void> {
if (!this.excalidrawAPI || !this.file) {
return;
}
const svg = await this.svg(
this.getScene(selectedOnly),
undefined,
false,
true
);
if (!svg) {
return;
}
const pdfArrayBuffer = await exportToPDF({
SVG: [svg],
scale: {
...this.exportDialog.fitToPage
? { fitToPage: true }
: { zoom: this.exportDialog.scale, fitToPage: false },
},
pageProps: {
dimensions: getPageDimensions(pageSize, orientation),
backgroundColor: this.exportDialog.getPaperColor(),
margin: getMarginValue(this.exportDialog.margin),
alignment: this.exportDialog.alignment,
}
});
if (!pdfArrayBuffer) {
return;
}
if(toVault) {
const filepath = getIMGFilename(this.file.path, "pdf");
const file = await createOrOverwriteFile(this.app, filepath, pdfArrayBuffer);
let leaf: WorkspaceLeaf;
this.app.workspace.getLeavesOfType("pdf").forEach((l) => {
//@ts-ignore
if(l.view?.file === file) {
leaf = l;
}
});
if(leaf) {
this.app.workspace.revealLeaf(leaf);
} else {
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<Blob> { public async png(scene: any, theme?:string, embedScene?: boolean): Promise<Blob> {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.png, "ExcalidrawView.png", scene, theme, embedScene); (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.png, "ExcalidrawView.png", scene, theme, embedScene);
const ed = this.exportDialog; const ed = this.exportDialog;
@@ -673,11 +598,17 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
} }
const exportImage = async (filepath:string, theme?:string) => { const exportImage = async (filepath:string, theme?:string) => {
const file = this.app.vault.getAbstractFileByPath(normalizePath(filepath));
const png = await this.png(scene, theme, embedScene); const png = await this.png(scene, theme, embedScene);
if (!png) { if (!png) {
return; return;
} }
await createOrOverwriteFile(this.app, filepath, await png.arrayBuffer()); if (file && file instanceof TFile) {
await this.app.vault.modifyBinary(file, await png.arrayBuffer());
} else {
await this.app.vault.createBinary(filepath, await png.arrayBuffer());
}
} }
if(this.plugin.settings.autoExportLightAndDark) { if(this.plugin.settings.autoExportLightAndDark) {

View File

@@ -80,11 +80,6 @@ button.ToolIcon_type_button[title="Export"] {
width: 9em; width: 9em;
} }
.excalidraw-export-button {
width: 9em;
margin-left: 10px;
}
.excalidraw-prompt-buttons-div { .excalidraw-prompt-buttons-div {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
@@ -92,13 +87,6 @@ button.ToolIcon_type_button[title="Export"] {
justify-content: space-evenly; justify-content: space-evenly;
} }
.excalidraw-export-buttons-div {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
justify-content: right;
}
li[data-testid] { li[data-testid] {
border: 0 !important; border: 0 !important;
margin: 0 !important; margin: 0 !important;
@@ -684,21 +672,3 @@ textarea.excalidraw-wysiwyg, .excalidraw input {
background-color: var(--background-modifier-hover); background-color: var(--background-modifier-hover);
color: var(--text-accent); color: var(--text-accent);
} }
.excalidraw-release .nav-buttons-container {
display: flex;
margin-bottom: 20px;
border-bottom: 2px solid var(--background-modifier-border);
}
.excalidraw-release .nav-button {
padding: 8px 16px;
border: none;
background: transparent;
cursor: pointer;
}
.excalidraw-release .nav-button.is-active {
border-bottom: 2px solid var(--interactive-accent);
margin-bottom: -2px;
}