mirror of
https://github.com/zsviczian/obsidian-excalidraw-plugin.git
synced 2025-08-06 05:46:28 +00:00
initial implementation
This commit is contained in:
132
package-lock.json
generated
132
package-lock.json
generated
@@ -9,6 +9,7 @@
|
||||
"version": "2.2.5",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@cantoo/pdf-lib": "^2.2.4",
|
||||
"@popperjs/core": "^2.11.8",
|
||||
"@zsviczian/colormaster": "^1.2.2",
|
||||
"@zsviczian/excalidraw": "0.17.6-26",
|
||||
@@ -41,6 +42,7 @@
|
||||
"@excalidraw/prettier-config": "^1.0.2",
|
||||
"@rollup/plugin-babel": "^6.0.3",
|
||||
"@rollup/plugin-commonjs": "^26.0.1",
|
||||
"@rollup/plugin-json": "^6.1.0",
|
||||
"@rollup/plugin-node-resolve": "^15.2.3",
|
||||
"@rollup/plugin-replace": "^5.0.2",
|
||||
"@rollup/plugin-typescript": "^12.1.2",
|
||||
@@ -1882,6 +1884,19 @@
|
||||
"resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-6.0.2.tgz",
|
||||
"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": {
|
||||
"version": "6.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.6.0.tgz",
|
||||
@@ -2346,6 +2361,22 @@
|
||||
"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": {
|
||||
"version": "0.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
|
||||
@@ -2775,6 +2806,26 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"version": "15.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.2.3.tgz",
|
||||
@@ -3796,6 +3847,18 @@
|
||||
"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": {
|
||||
"version": "1.9.3",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
|
||||
@@ -3808,8 +3871,32 @@
|
||||
"node_modules/color-name": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
|
||||
"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
|
||||
"devOptional": true
|
||||
"integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="
|
||||
},
|
||||
"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": {
|
||||
"version": "2.9.3",
|
||||
@@ -3922,6 +4009,11 @@
|
||||
"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": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-7.2.0.tgz",
|
||||
@@ -5501,6 +5593,21 @@
|
||||
"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": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||
@@ -5595,6 +5702,11 @@
|
||||
"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": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
|
||||
@@ -6745,6 +6857,14 @@
|
||||
"dev": 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": {
|
||||
"version": "2.0.14",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz",
|
||||
@@ -8317,6 +8437,14 @@
|
||||
"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": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
|
||||
|
||||
@@ -40,7 +40,8 @@
|
||||
"react-dom": "^18.2.0",
|
||||
"roughjs": "^4.5.2",
|
||||
"woff2sfnt-sfnt2woff": "^1.0.0",
|
||||
"es6-promise-pool": "2.5.0"
|
||||
"es6-promise-pool": "2.5.0",
|
||||
"@cantoo/pdf-lib": "^2.2.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"jsesc": "^3.0.2",
|
||||
@@ -59,6 +60,7 @@
|
||||
"@rollup/plugin-node-resolve": "^15.2.3",
|
||||
"@rollup/plugin-replace": "^5.0.2",
|
||||
"@rollup/plugin-typescript": "^12.1.2",
|
||||
"@rollup/plugin-json": "^6.1.0",
|
||||
"@types/chroma-js": "^2.4.0",
|
||||
"@types/js-beautify": "^1.14.0",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
|
||||
@@ -11,6 +11,7 @@ import postprocess from '@zsviczian/rollup-plugin-postprocess';
|
||||
import cssnano from 'cssnano';
|
||||
import jsesc from 'jsesc';
|
||||
import { minify } from 'uglify-js';
|
||||
import json from '@rollup/plugin-json';
|
||||
|
||||
// Load environment variables
|
||||
import dotenv from 'dotenv';
|
||||
@@ -130,6 +131,7 @@ const BASE_CONFIG = {
|
||||
|
||||
const getRollupPlugins = (tsconfig, ...plugins) => [
|
||||
typescript2(tsconfig),
|
||||
json(),
|
||||
replace({
|
||||
preventAssignment: true,
|
||||
"process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV),
|
||||
|
||||
@@ -6,6 +6,7 @@ import { ExcalidrawAutomate } from "src/shared/ExcalidrawAutomate";
|
||||
import ExcalidrawView from "src/view/ExcalidrawView";
|
||||
import ExcalidrawPlugin from "src/core/main";
|
||||
import { fragWithHTML, getExportPadding, getExportTheme, getPNGScale, getWithBackground, shouldEmbedScene } from "src/utils/utils";
|
||||
import { PageOrientation, PageSize, STANDARD_PAGE_SIZES } from "src/utils/exportUtils";
|
||||
|
||||
export class ExportDialog extends Modal {
|
||||
private ea: ExcalidrawAutomate;
|
||||
@@ -27,11 +28,17 @@ export class ExportDialog extends Modal {
|
||||
public embedScene: boolean;
|
||||
public exportSelectedOnly: boolean;
|
||||
public saveToVault: boolean;
|
||||
public pageSize: PageSize = "A4";
|
||||
public pageOrientation: PageOrientation = "portrait";
|
||||
private activeTab: "image" | "pdf" = "image";
|
||||
private contentContainer: HTMLDivElement;
|
||||
private buttonContainer: HTMLDivElement;
|
||||
|
||||
constructor(
|
||||
private plugin: ExcalidrawPlugin,
|
||||
private view: ExcalidrawView,
|
||||
private file: TFile,
|
||||
|
||||
) {
|
||||
super(plugin.app);
|
||||
this.ea = getEA(this.view);
|
||||
@@ -73,11 +80,81 @@ export class ExportDialog extends Modal {
|
||||
}
|
||||
|
||||
async createForm() {
|
||||
let scaleSetting:Setting;
|
||||
let paddingSetting: Setting;
|
||||
if(DEVICE.isDesktop) {
|
||||
// Create tab container
|
||||
const tabContainer = this.contentEl.createDiv("nav-buttons-container");
|
||||
const imageTab = tabContainer.createEl("button", {
|
||||
text: "Image",
|
||||
cls: `nav-button ${this.activeTab === "image" ? "is-active" : ""}`
|
||||
});
|
||||
|
||||
this.contentEl.createEl("h1",{text: "Image settings"});
|
||||
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."})
|
||||
|
||||
const pdfTab = tabContainer.createEl("button", {
|
||||
text: "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.buttonContainer = this.contentEl.createDiv({cls: "excalidraw-prompt-buttons-div"});
|
||||
|
||||
this.renderContent();
|
||||
}
|
||||
|
||||
private createSaveSettingsDropdown() {
|
||||
new Setting(this.contentContainer)
|
||||
.setName("Save settings?")
|
||||
.addDropdown(dropdown =>
|
||||
dropdown
|
||||
.addOption("save", "Save these settings as the preset for this image")
|
||||
.addOption("one-time", "These are one-time settings")
|
||||
.setValue(this.saveSettings ? "save" : "one-time")
|
||||
.onChange(value => {
|
||||
this.saveSettings = value === "save";
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private renderContent() {
|
||||
this.contentContainer.empty();
|
||||
this.buttonContainer.empty();
|
||||
|
||||
// Always show save settings dropdown
|
||||
this.createSaveSettingsDropdown();
|
||||
|
||||
if (this.activeTab === "image") {
|
||||
this.createImageSettings();
|
||||
this.createExportSettings();
|
||||
this.createImageButtons();
|
||||
} else {
|
||||
this.createImageSettings();
|
||||
this.createPDFSettings();
|
||||
this.createPDFButton();
|
||||
}
|
||||
}
|
||||
|
||||
private createImageSettings() {
|
||||
let scaleSetting:Setting;
|
||||
let paddingSetting: Setting;
|
||||
|
||||
this.contentContainer.createEl("h1",{text: "Image settings"});
|
||||
this.contentContainer.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."})
|
||||
|
||||
const size = ():DocumentFragment => {
|
||||
const width = Math.round(this.scale*this.boundingBox.width + this.padding*2);
|
||||
@@ -89,7 +166,7 @@ export class ExportDialog extends Modal {
|
||||
return fragWithHTML(`Current image padding is <b>${this.padding}</b>`);
|
||||
}
|
||||
|
||||
paddingSetting = new Setting(this.contentEl)
|
||||
paddingSetting = new Setting(this.contentContainer)
|
||||
.setName("Image padding")
|
||||
.setDesc(padding())
|
||||
.addSlider(slider => {
|
||||
@@ -103,8 +180,8 @@ export class ExportDialog extends Modal {
|
||||
})
|
||||
})
|
||||
|
||||
scaleSetting = new Setting(this.contentEl)
|
||||
.setName("PNG Scale")
|
||||
scaleSetting = new Setting(this.contentContainer)
|
||||
.setName("Scale")
|
||||
.setDesc(size())
|
||||
.addSlider(slider =>
|
||||
slider
|
||||
@@ -116,7 +193,7 @@ export class ExportDialog extends Modal {
|
||||
})
|
||||
)
|
||||
|
||||
new Setting(this.contentEl)
|
||||
new Setting(this.contentContainer)
|
||||
.setName("Export theme")
|
||||
.addDropdown(dropdown =>
|
||||
dropdown
|
||||
@@ -128,8 +205,8 @@ export class ExportDialog extends Modal {
|
||||
})
|
||||
)
|
||||
|
||||
new Setting(this.contentEl)
|
||||
.setName("Background color")
|
||||
new Setting(this.contentContainer)
|
||||
.setName("Use scene background color")
|
||||
.addDropdown(dropdown =>
|
||||
dropdown
|
||||
.addOption("transparent","Transparent")
|
||||
@@ -139,22 +216,24 @@ export class ExportDialog extends Modal {
|
||||
this.transparent = value === "transparent";
|
||||
})
|
||||
)
|
||||
|
||||
new Setting(this.contentEl)
|
||||
.setName("Save or one-time settings?")
|
||||
|
||||
this.selectedOnlySetting = new Setting(this.contentContainer)
|
||||
.setName("The scene or just selected elements?")
|
||||
.addDropdown(dropdown =>
|
||||
dropdown
|
||||
.addOption("save","Save these settings as the preset for this image")
|
||||
.addOption("one-time","These are one-time settings")
|
||||
.setValue(this.saveSettings?"save":"one-time")
|
||||
.addOption("all","Entire scene")
|
||||
.addOption("selected","Selected elements")
|
||||
.setValue(this.exportSelectedOnly?"selected":"all")
|
||||
.onChange(value => {
|
||||
this.saveSettings = value === "save";
|
||||
this.exportSelectedOnly = value === "selected";
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
this.contentEl.createEl("h1",{text:"Export settings"});
|
||||
private createExportSettings() {
|
||||
this.contentContainer.createEl("h1",{text:"Export settings"});
|
||||
|
||||
new Setting(this.contentEl)
|
||||
new Setting(this.contentContainer)
|
||||
.setName("Embed the Excalidraw scene in the exported file?")
|
||||
.addDropdown(dropdown =>
|
||||
dropdown
|
||||
@@ -167,7 +246,7 @@ export class ExportDialog extends Modal {
|
||||
)
|
||||
|
||||
if(DEVICE.isDesktop) {
|
||||
new Setting(this.contentEl)
|
||||
new Setting(this.contentContainer)
|
||||
.setName("Where to save the image?")
|
||||
.addDropdown(dropdown =>
|
||||
dropdown
|
||||
@@ -179,46 +258,87 @@ export class ExportDialog extends Modal {
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
this.selectedOnlySetting = new Setting(this.contentEl)
|
||||
.setName("Export entire scene or just selected elements?")
|
||||
private createPDFSettings() {
|
||||
if (!DEVICE.isDesktop) return;
|
||||
|
||||
this.contentContainer.createEl("h1", { text: "PDF settings" });
|
||||
|
||||
const pageSizeOptions: Record<string, string> = Object.keys(STANDARD_PAGE_SIZES)
|
||||
.reduce((acc, key) => ({
|
||||
...acc,
|
||||
[key]: key
|
||||
}), {});
|
||||
|
||||
new Setting(this.contentContainer)
|
||||
.setName("Page size")
|
||||
.addDropdown(dropdown =>
|
||||
dropdown
|
||||
.addOption("all","Export entire scene")
|
||||
.addOption("selected","Export selected elements")
|
||||
.setValue(this.exportSelectedOnly?"selected":"all")
|
||||
.addOptions(pageSizeOptions)
|
||||
.setValue(this.pageSize)
|
||||
.onChange(value => {
|
||||
this.exportSelectedOnly = value === "selected";
|
||||
this.pageSize = value as PageSize;
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
new Setting(this.contentContainer)
|
||||
.setName("Page orientation")
|
||||
.addDropdown(dropdown =>
|
||||
dropdown
|
||||
.addOptions({
|
||||
"portrait": "Portrait",
|
||||
"landscape": "Landscape"
|
||||
})
|
||||
.setValue(this.pageOrientation)
|
||||
.onChange(value => {
|
||||
this.pageOrientation = value as PageOrientation;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const div = this.contentEl.createDiv({cls: "excalidraw-prompt-buttons-div"});
|
||||
const bPNG = div.createEl("button", { text: "PNG to File", cls: "excalidraw-prompt-button"});
|
||||
private createImageButtons() {
|
||||
const bPNG = this.buttonContainer.createEl("button", { text: "PNG > 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();
|
||||
};
|
||||
const bSVG = div.createEl("button", { text: "SVG to File", cls: "excalidraw-prompt-button" });
|
||||
const bSVG = this.buttonContainer.createEl("button", { text: "SVG > File", cls: "excalidraw-prompt-button" });
|
||||
bSVG.onclick = () => {
|
||||
this.saveToVault
|
||||
? this.view.saveSVG(this.view.getScene(this.hasSelectedElements && this.exportSelectedOnly))
|
||||
: this.view.exportSVG(this.embedScene,this.hasSelectedElements && this.exportSelectedOnly);
|
||||
this.close();
|
||||
};
|
||||
const bExcalidraw = div.createEl("button", { text: "Excalidraw", cls: "excalidraw-prompt-button" });
|
||||
const bExcalidraw = this.buttonContainer.createEl("button", { text: "Excalidraw", cls: "excalidraw-prompt-button" });
|
||||
bExcalidraw.onclick = () => {
|
||||
this.view.exportExcalidraw(this.hasSelectedElements && this.exportSelectedOnly);
|
||||
this.close();
|
||||
};
|
||||
if(DEVICE.isDesktop) {
|
||||
const bPNGClipboard = div.createEl("button", { text: "PNG to Clipboard", cls: "excalidraw-prompt-button" });
|
||||
const bPNGClipboard = this.buttonContainer.createEl("button", { text: "PNG > Clipboard", cls: "excalidraw-prompt-button" });
|
||||
bPNGClipboard.onclick = () => {
|
||||
this.view.exportPNGToClipboard(this.embedScene, this.hasSelectedElements && this.exportSelectedOnly);
|
||||
this.close();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private createPDFButton() {
|
||||
if (!DEVICE.isDesktop) return;
|
||||
const bPDF = this.buttonContainer.createEl("button", {
|
||||
text: "PDF",
|
||||
cls: "excalidraw-prompt-button"
|
||||
});
|
||||
bPDF.onclick = () => {
|
||||
this.view.exportPDF(
|
||||
this.hasSelectedElements && this.exportSelectedOnly,
|
||||
this.pageSize,
|
||||
this.pageOrientation
|
||||
);
|
||||
this.close();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
204
src/utils/exportUtils.ts
Normal file
204
src/utils/exportUtils.ts
Normal file
@@ -0,0 +1,204 @@
|
||||
import { PDFDocument, rgb } from '@cantoo/pdf-lib';
|
||||
import { getEA } from 'src/core';
|
||||
import { svgToBase64 } from './utils';
|
||||
|
||||
interface PDFExportScale {
|
||||
fitToPage: boolean;
|
||||
zoom?: number;
|
||||
}
|
||||
|
||||
interface PDFPageProperties {
|
||||
dimensions?: {width: number; height: number};
|
||||
backgroundColor?: string;
|
||||
margin?: {
|
||||
left: number;
|
||||
right: number;
|
||||
top: number;
|
||||
bottom: number;
|
||||
};
|
||||
}
|
||||
|
||||
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 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 };
|
||||
}
|
||||
|
||||
const DEFAULT_MARGIN = 20; // 20pt margins
|
||||
|
||||
interface SVGDimensions {
|
||||
width: number;
|
||||
height: number;
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
function calculateDimensions(
|
||||
svgWidth: number,
|
||||
svgHeight: number,
|
||||
pageDim: PageDimensions,
|
||||
margin: PDFPageProperties['margin'],
|
||||
scale: PDFExportScale
|
||||
): SVGDimensions {
|
||||
const availableWidth = pageDim.width - (margin?.left || 0) - (margin?.right || 0);
|
||||
const availableHeight = pageDim.height - (margin?.top || 0) - (margin?.bottom || 0);
|
||||
|
||||
let finalWidth, finalHeight;
|
||||
if (scale.fitToPage) {
|
||||
const ratio = Math.min(availableWidth / svgWidth, availableHeight / svgHeight);
|
||||
finalWidth = svgWidth * ratio;
|
||||
finalHeight = svgHeight * ratio;
|
||||
} else {
|
||||
finalWidth = svgWidth * (scale.zoom || 1);
|
||||
finalHeight = svgHeight * (scale.zoom || 1);
|
||||
}
|
||||
|
||||
const x = (margin?.left || 0) + (availableWidth - finalWidth) / 2;
|
||||
const y = pageDim.height - (margin?.top || 0) - finalHeight;
|
||||
|
||||
return { width: finalWidth, height: finalHeight, x, y };
|
||||
}
|
||||
|
||||
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),
|
||||
});
|
||||
}
|
||||
|
||||
const svgImage = await pdfDoc.embedSvg(svg.outerHTML);
|
||||
|
||||
page.drawSvg(svgImage, {
|
||||
x: dimensions.x,
|
||||
y: dimensions.y,
|
||||
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 margin = pageProps.margin ?? {
|
||||
left: DEFAULT_MARGIN,
|
||||
right: DEFAULT_MARGIN,
|
||||
top: DEFAULT_MARGIN,
|
||||
bottom: DEFAULT_MARGIN
|
||||
};
|
||||
|
||||
const pageDim = pageProps.dimensions ?? getPageDimensions("A4", "portrait");
|
||||
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, pageDim, margin, scale);
|
||||
|
||||
if (!scale.fitToPage && (dimensions.width > pageDim.width || dimensions.height > pageDim.height)) {
|
||||
// Split oversized SVG into pages
|
||||
const maxWidth = pageDim.width - margin.left - margin.right;
|
||||
const maxHeight = pageDim.height - margin.top - margin.bottom;
|
||||
const splitSVGs = splitSVGIntoPages(svg, maxWidth, maxHeight);
|
||||
|
||||
for (const splitSvg of splitSVGs) {
|
||||
const splitDimensions = calculateDimensions(
|
||||
parseFloat(splitSvg.getAttribute('width') || '0'),
|
||||
parseFloat(splitSvg.getAttribute('height') || '0'),
|
||||
pageDim,
|
||||
margin,
|
||||
{ fitToPage: true }
|
||||
);
|
||||
await addSVGToPage(pdfDoc, splitSvg, splitDimensions, pageDim, pageProps.backgroundColor);
|
||||
}
|
||||
} else {
|
||||
await addSVGToPage(pdfDoc, svg, dimensions, pageDim, 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;
|
||||
}
|
||||
@@ -290,6 +290,17 @@ export const blobToBase64 = async (blob: Blob): Promise<string> => {
|
||||
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> => {
|
||||
if(typeof window.pdfjsLib === "undefined") await loadPdfJs();
|
||||
return await window.pdfjsLib.getDocument(EXCALIDRAW_PLUGIN.app.vault.getResourcePath(f)).promise;
|
||||
|
||||
@@ -76,6 +76,7 @@ import {
|
||||
getExcalidrawMarkdownHeaderSection,
|
||||
} from "../shared/ExcalidrawData";
|
||||
import {
|
||||
arrayBufferToBase64,
|
||||
checkAndCreateFolder,
|
||||
download,
|
||||
getDataURLFromURL,
|
||||
@@ -149,6 +150,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, getPageDimensions, PageOrientation, PageSize } from "src/utils/exportUtils";
|
||||
|
||||
const EMBEDDABLE_SEMAPHORE_TIMEOUT = 2000;
|
||||
const PREVENT_RELOAD_TIMEOUT = 2000;
|
||||
@@ -555,6 +557,52 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
|
||||
);
|
||||
}
|
||||
|
||||
public async exportPDF(
|
||||
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: { fitToPage: true },
|
||||
pageProps: {
|
||||
dimensions: getPageDimensions(pageSize, orientation),
|
||||
backgroundColor: this.exportDialog.transparent ? undefined : "#ffffff",
|
||||
margin: {
|
||||
top: 20,
|
||||
left: 20,
|
||||
right: 20,
|
||||
bottom: 20
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (!pdfArrayBuffer) {
|
||||
return;
|
||||
}
|
||||
|
||||
download(
|
||||
"data:application/pdf;base64",
|
||||
arrayBufferToBase64(pdfArrayBuffer),
|
||||
`${this.file.basename}.pdf`
|
||||
);
|
||||
}
|
||||
|
||||
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);
|
||||
const ed = this.exportDialog;
|
||||
|
||||
18
styles.css
18
styles.css
@@ -672,3 +672,21 @@ textarea.excalidraw-wysiwyg, .excalidraw input {
|
||||
background-color: var(--background-modifier-hover);
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user