Compare commits

...

36 Commits

Author SHA1 Message Date
zsviczian 4209774b4e 2.8.0-beta-2 2025-01-19 15:56:39 +01:00
zsviczian b18637f7d0 pdf export fitToPage fix number of pages
CodeQL / Analyze (javascript) (push) Waiting to run
2025-01-19 08:35:01 +01:00
zsviczian 01e392158d fix png, jpg conversion and move from symbol to inline
CodeQL / Analyze (javascript) (push) Waiting to run
2025-01-18 22:42:49 +01:00
zsviczian fc47b7aa0d 2.8.0-beta-1, 0.17.6-27 2025-01-18 19:12:57 +01:00
zsviczian a0e0627a49 Merge pull request #2219 from zsviczian/PDF-export
Pdf export
2025-01-18 18:59:50 +01:00
zsviczian efcb0c0580 added ExcalidrawAutomate getPagePDFDimensions and createPDF 2025-01-18 18:56:21 +01:00
zsviczian 23d7105fb1 export dialog buttons 2025-01-18 18:16:49 +01:00
zsviczian 5d9565bd7c PDF export settings 2025-01-18 17:01:40 +01:00
zsviczian 59785523ae carved out PDFExportSettingsComponent 2025-01-18 15:46:27 +01:00
zsviczian 2a21ed5fc7 vertical positioning fixed 2025-01-18 15:13:14 +01:00
zsviczian 3d3ce73fa1 export to vault, added pdf settings 2025-01-18 13:20:00 +01:00
zsviczian c35bd385fe extracted strings to language file 2025-01-18 12:18:56 +01:00
zsviczian a790b04547 initial implementation 2025-01-18 11:37:58 +01:00
zsviczian 5171978c37 Merge pull request #2217 from zsviczian/PDF-fix
CodeQL / Analyze (javascript) (push) Has been cancelled
Pdf crop fix
2025-01-17 06:46:59 +01:00
zsviczian ea4a0c91e8 added PDF samples for testing 2025-01-17 06:45:30 +01:00
zsviczian 34af6dd447 getPDFCropRect, getPDFRect improved 90,180,270 calc, still issue with page offsets 2025-01-15 23:22:35 +01:00
zsviczian ed2e700946 Merge pull request #2215 from zsviczian/local-graph-embed-sync
CodeQL / Analyze (javascript) (push) Has been cancelled
Set local graph when embeddable is activated/deactivated #2200
2025-01-15 09:38:13 +01:00
zsviczian 7eb23ab5e1 Set local graph when embeddable is activated/deactivated #2200 2025-01-15 08:22:38 +00:00
zsviczian 7cccf1d4e2 2.7.6-beta-1
CodeQL / Analyze (javascript) (push) Waiting to run
2025-01-14 22:57:14 +01:00
zsviczian 2a5545964c update package-lock.json, update packages, style.css to support new arrow picker 2025-01-14 22:15:28 +01:00
zsviczian 4ce22883cc Merge pull request #2214 from zsviczian/allow-new-image-formats
CodeQL / Analyze (javascript) (push) Waiting to run
fix: allow jfif and avif
2025-01-14 16:01:01 +01:00
zsviczian 272804afc8 allow jfif and avif 2025-01-14 14:59:47 +00:00
zsviczian dc0b50f717 Update How-to.yml
CodeQL / Analyze (javascript) (push) Has been cancelled
2025-01-09 08:52:41 +01:00
zsviczian a0eb625b8a Update How-to.yml 2025-01-09 08:52:11 +01:00
zsviczian 524dc54d03 Update bug_report.yml 2025-01-09 08:50:35 +01:00
zsviczian 918718be90 Update bug_report.yml 2025-01-09 08:49:59 +01:00
zsviczian 78ee784be1 Update bug_report.yml 2025-01-09 08:48:58 +01:00
zsviczian 7e0e016bf9 Update bug_report.yml 2025-01-09 08:48:18 +01:00
zsviczian 4f875a03a0 2.7.5
CodeQL / Analyze (javascript) (push) Has been cancelled
2025-01-05 23:09:16 +01:00
zsviczian 63c56e0e98 similar elements allows selection of containers
CodeQL / Analyze (javascript) (push) Waiting to run
2025-01-05 12:49:11 +01:00
zsviczian 46477208be split ellipse and concatenate line now works with rotated lines
CodeQL / Analyze (javascript) (push) Waiting to run
2025-01-05 11:38:45 +01:00
zsviczian 3194c014c7 updated concatenate lines 2025-01-05 10:33:31 +01:00
zsviczian 25ccb9dc43 updated EA lib in docs 2025-01-05 07:03:04 +01:00
zsviczian fa46f8c39d Merge pull request #1880 from karaolidis/package-lock
CodeQL / Analyze (javascript) (push) Waiting to run
Allow automated & deterministic builds
2025-01-04 22:18:09 +01:00
zsviczian 8ffe5c3942 2.7.5-beta-1 2025-01-04 21:26:49 +01:00
Nikolaos Karaolidis d759abbc47 Allow commiting package-lock.json
Signed-off-by: Nikolaos Karaolidis <nick@karaolidis.com>
2025-01-04 18:38:56 +02:00
43 changed files with 13634 additions and 936 deletions
+9
View File
@@ -12,6 +12,8 @@ body:
Before submitting a support request, please:
1. **Review the [documentation](https://github.com/zsviczian/obsidian-excalidraw-plugin/wiki)** your question may already be answered.
2. **[Search issues](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues) (including closed ones)** to see if your question has already been addressed.
3. **[Watch the Feature Walkthrough Video](https://youtu.be/P_Q6avJGoWI)**: As it infact answers 90% of the typical questions I receive
4. **[Consult NotebookLM with your question](https://excalidraw-obsidian.online/WIKI/09+Video+Transcripts/Videos/Turn+any+YouTube+Channel+into+your+AI+Mentor+-+Obsidian+is+the+ultimate+automation+workbench+for+PKM)**
- type: markdown
attributes:
@@ -31,6 +33,13 @@ body:
options:
- label: Yes, I have reviewed the documentation and searched for related issues.
- type: textarea
id: notebook_lm
attributes:
label: "Your NotebookLM query"
description: "See point 4) above. Paste the question and answer you received from NotebookLM. This serves partly as proof, partly to help me see where the model is incorrect"
placeholder: "Copy/Paste your question and the resulting answer you got from NotebookLM"
- type: textarea
id: support_question
attributes:
+10 -1
View File
@@ -1,5 +1,5 @@
name: Bug report
description: If something is clearly broken, its a bug. Everything else is a feature or support request. Most reported “bugs” are actually how-to questions or feature requests.
description: If something is clearly broken, its a bug. **Everything else** is a feature or support request. Most reported “bugs” are actually how-to questions or feature requests.
title: "BUG: "
body:
- type: markdown
@@ -12,6 +12,8 @@ body:
Before creating a bug report, please:
1. **Review recent [release notes](https://github.com/zsviczian/obsidian-excalidraw-plugin/releases)** maybe there is already an answer.
2. **[Search issues](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues) (including closed ones)** to see if there is anything similar.
3. **[Watch the Feature Walkthrough Video](https://youtu.be/P_Q6avJGoWI)**: As it infact answers 90% of the typical questions I receive
4. **[Consult NotebookLM with your question](https://excalidraw-obsidian.online/WIKI/09+Video+Transcripts/Videos/Turn+any+YouTube+Channel+into+your+AI+Mentor+-+Obsidian+is+the+ultimate+automation+workbench+for+PKM)**
- type: markdown
attributes:
@@ -46,6 +48,13 @@ body:
description: "Run `Command Palette/Show Debug info` in Obsidian and paste the result here."
placeholder: "Paste your Obsidian debug info here..."
- type: textarea
id: notebook_lm
attributes:
label: "Your NotebookLM query"
description: "See point 4) above. Paste the question and answer you received from NotebookLM. This serves partly as proof, partly to help me see where the model is incorrect"
placeholder: "Copy/Paste your question and the resulting answer you got from NotebookLM"
- type: textarea
id: bug_description
attributes:
-1
View File
@@ -4,7 +4,6 @@
# npm
node_modules
package-lock.json
# build
main.js
+1 -1
View File
@@ -1 +1 @@
18
18
+1340
View File
File diff suppressed because it is too large Load Diff
+5 -4
View File
@@ -13,11 +13,12 @@
"devDependencies": {
"@rollup/plugin-commonjs": "^26.0.1",
"@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-typescript": "^11.1.6",
"@rollup/plugin-typescript": "^12.1.2",
"cross-env": "^7.0.3",
"obsidian": "1.5.7-1",
"rollup": "^2.70.1",
"typescript": "^5.2.2",
"rollup-plugin-terser": "^7.0.2"
"rollup-plugin-terser": "^7.0.2",
"tslib": "^2.8.1",
"typescript": "^5.7.3"
}
}
}
+3 -3
View File
@@ -16,10 +16,10 @@ export default {
},
plugins: [
typescript({
tsconfig: '../tsconfig.json',
tsconfig: 'tsconfig.json',
}),
commonjs(),
nodeResolve({
nodeResolve({
browser: true,
preferBuiltins: false
}),
@@ -32,4 +32,4 @@ export default {
}
})
].filter(Boolean)
};
};
+26
View File
@@ -0,0 +1,26 @@
{
"compilerOptions": {
"baseUrl": ".",
"sourceMap": false,
"module": "es2020",
"target": "es2022", //min es2017 because script engine requires for async execution and min es2018 for named capture groups
"allowJs": false,
"noImplicitAny": true,
"moduleResolution": "node",
"esModuleInterop": true,
"importHelpers": true,
"resolveJsonModule": true,
"lib": [
"dom",
"scripthost",
"es2022",
"DOM.Iterable"
],
"jsx": "react",
},
"include": [
"**/*.ts",
"**/*.tsx", "src/shared/Dialogs/OpenDrawing.ts",
"src/types/types.d.ts",
]
}
-4
View File
@@ -1,4 +0,0 @@
The project runs with `node 18`.
After running `npm -i` you'll need to make two manual changes:
+686 -340
View File
File diff suppressed because it is too large Load Diff
+69 -13
View File
@@ -8,20 +8,76 @@ if(lines.length !== 2) {
return;
}
// https://math.stackexchange.com/questions/2204520/how-do-i-rotate-a-line-segment-in-a-specific-point-on-the-line
const rotate = (point, element) => {
const [x1, y1] = point;
const x2 = element.x + element.width/2;
const y2 = element.y - element.height/2;
const angle = element.angle;
return [
(x1 - x2) * Math.cos(angle) - (y1 - y2) * Math.sin(angle) + x2,
(x1 - x2) * Math.sin(angle) + (y1 - y2) * Math.cos(angle) + y2,
];
//Same line but with angle=0
function getNormalizedLine(originalElement) {
if(originalElement.angle === 0) return originalElement;
// Get absolute coordinates for all points first
const pointRotateRads = (point, center, angle) => {
const [x, y] = point;
const [cx, cy] = center;
return [
(x - cx) * Math.cos(angle) - (y - cy) * Math.sin(angle) + cx,
(x - cx) * Math.sin(angle) + (y - cy) * Math.cos(angle) + cy
];
};
// Get element absolute coordinates (matching Excalidraw's approach)
const getElementAbsoluteCoords = (element) => {
const points = element.points;
let minX = Infinity;
let minY = Infinity;
let maxX = -Infinity;
let maxY = -Infinity;
for (const [x, y] of points) {
const absX = x + element.x;
const absY = y + element.y;
minX = Math.min(minX, absX);
minY = Math.min(minY, absY);
maxX = Math.max(maxX, absX);
maxY = Math.max(maxY, absY);
}
return [minX, minY, maxX, maxY];
};
// Calculate center point based on absolute coordinates
const [x1, y1, x2, y2] = getElementAbsoluteCoords(originalElement);
const centerX = (x1 + x2) / 2;
const centerY = (y1 + y2) / 2;
// Calculate absolute coordinates of all points
const absolutePoints = originalElement.points.map(([x, y]) => [
x + originalElement.x,
y + originalElement.y
]);
// Rotate all points around the center
const rotatedPoints = absolutePoints.map(point =>
pointRotateRads(point, [centerX, centerY], originalElement.angle)
);
// Convert back to relative coordinates
const newPoints = rotatedPoints.map(([x, y]) => [
x - rotatedPoints[0][0],
y - rotatedPoints[0][1]
]);
const newLineId = ea.addLine(newPoints);
// Set the position of the new line to the first rotated point
const newLine = ea.getElement(newLineId);
newLine.x = rotatedPoints[0][0];
newLine.y = rotatedPoints[0][1];
newLine.angle = 0;
delete ea.elementsDict[newLine.id];
return newLine;
}
const points = lines.map(
el=>el.points.map(p=>rotate([p[0]+el.x, p[1]+el.y],el))
const points = lines.map(getNormalizedLine).map(
el=>el.points.map(p=>[p[0]+el.x, p[1]+el.y])
);
const last = (p) => p[p.length-1];
@@ -99,4 +155,4 @@ switch (lineTypes) {
}
ea.addElementsToView();
await ea.addElementsToView();
+32 -7
View File
@@ -7,16 +7,41 @@ This script enables the selection of elements based on matching properties. Sele
```js */
let config = window.ExcalidrawSelectConfig;
config = Boolean(config) && (Date.now() - config.timestamp < 60000) ? config : null;
const isValidConfig = config && (Date.now() - config.timestamp < 60000);
config = isValidConfig ? config : null;
let elements = ea.getViewSelectedElements();
if(!config && (elements.length !==1)) {
new Notice("Select a single element");
return;
} else {
if(elements.length === 0) {
elements = ea.getViewElements();
if(!config) {
async function shouldAbort() {
if(elements.length === 1) return false;
if(elements.length !== 2) return true;
//maybe container?
const textEl = elements.find(el=>el.type==="text");
if(!textEl || !textEl.containerId) return true;
const containerEl = elements.find(el=>el.id === textEl.containerId);
if(!containerEl) return true;
const id = await utils.suggester(
elements.map(el=>el.type),
elements.map(el=>el.id),
"Select container component"
);
if(!id) return true;
elements = elements.filter(el=>el.id === id);
return false;
}
if(await shouldAbort()) {
new Notice("Select a single element");
return;
}
}
if(Boolean(config) && elements.length === 0) {
elements = ea.getViewElements();
}
const {angle, backgroundColor, fillStyle, fontFamily, fontSize, height, width, opacity, roughness, roundness, strokeColor, strokeStyle, strokeWidth, type, startArrowhead, endArrowhead, fileId} = ea.getViewSelectedElement();
+68
View File
@@ -18,6 +18,7 @@ if (!ellipse) return;
let lines = elements.filter(el => el.type == "line" || el.type == "arrow");
if (lines.length == 0) lines = ea.getViewElements().filter(el => el.type == "line" || el.type == "arrow");
lines = lines.map(getNormalizedLine);
const subLines = getSubLines(lines);
const angles = subLines.flatMap(line => {
@@ -206,3 +207,70 @@ function isBetween(num, min, max) {
function clamp(number, min, max) {
return Math.max(min, Math.min(number, max));
}
//Same line but with angle=0
function getNormalizedLine(originalElement) {
if(originalElement.angle === 0) return originalElement;
// Get absolute coordinates for all points first
const pointRotateRads = (point, center, angle) => {
const [x, y] = point;
const [cx, cy] = center;
return [
(x - cx) * Math.cos(angle) - (y - cy) * Math.sin(angle) + cx,
(x - cx) * Math.sin(angle) + (y - cy) * Math.cos(angle) + cy
];
};
// Get element absolute coordinates (matching Excalidraw's approach)
const getElementAbsoluteCoords = (element) => {
const points = element.points;
let minX = Infinity;
let minY = Infinity;
let maxX = -Infinity;
let maxY = -Infinity;
for (const [x, y] of points) {
const absX = x + element.x;
const absY = y + element.y;
minX = Math.min(minX, absX);
minY = Math.min(minY, absY);
maxX = Math.max(maxX, absX);
maxY = Math.max(maxY, absY);
}
return [minX, minY, maxX, maxY];
};
// Calculate center point based on absolute coordinates
const [x1, y1, x2, y2] = getElementAbsoluteCoords(originalElement);
const centerX = (x1 + x2) / 2;
const centerY = (y1 + y2) / 2;
// Calculate absolute coordinates of all points
const absolutePoints = originalElement.points.map(([x, y]) => [
x + originalElement.x,
y + originalElement.y
]);
// Rotate all points around the center
const rotatedPoints = absolutePoints.map(point =>
pointRotateRads(point, [centerX, centerY], originalElement.angle)
);
// Convert back to relative coordinates
const newPoints = rotatedPoints.map(([x, y]) => [
x - rotatedPoints[0][0],
y - rotatedPoints[0][1]
]);
const newLineId = ea.addLine(newPoints);
// Set the position of the new line to the first rotated point
const newLine = ea.getElement(newLineId);
newLine.x = rotatedPoints[0][0];
newLine.y = rotatedPoints[0][1];
newLine.angle = 0;
delete ea.elementsDict[newLine.id];
return newLine;
}
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -1,7 +1,7 @@
{
"id": "obsidian-excalidraw-plugin",
"name": "Excalidraw",
"version": "2.7.4",
"version": "2.8.0-beta-2",
"minAppVersion": "1.1.6",
"description": "An Obsidian plugin to edit and view Excalidraw drawings",
"author": "Zsolt Viczian",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"id": "obsidian-excalidraw-plugin",
"name": "Excalidraw",
"version": "2.7.4",
"version": "2.7.5",
"minAppVersion": "1.1.6",
"description": "An Obsidian plugin to edit and view Excalidraw drawings",
"author": "Zsolt Viczian",
+9391
View File
File diff suppressed because it is too large Load Diff
+8 -6
View File
@@ -24,7 +24,7 @@
"license": "MIT",
"dependencies": {
"@popperjs/core": "^2.11.8",
"@zsviczian/excalidraw": "0.17.6-24",
"@zsviczian/excalidraw": "0.17.6-27",
"chroma-js": "^2.4.2",
"clsx": "^2.0.0",
"@zsviczian/colormaster": "^1.2.2",
@@ -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",
@@ -58,7 +59,8 @@
"@rollup/plugin-commonjs": "^26.0.1",
"@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-replace": "^5.0.2",
"@rollup/plugin-typescript": "^11.1.6",
"@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",
@@ -79,10 +81,10 @@
"rollup-plugin-copy": "^3.5.0",
"@zsviczian/rollup-plugin-postprocess": "^1.0.3",
"rollup-plugin-terser": "^7.0.2",
"rollup-plugin-typescript2": "^0.34.1",
"tslib": "^2.6.1",
"rollup-plugin-typescript2": "^0.36.0",
"tslib": "^2.8.1",
"ttypescript": "^1.5.15",
"typescript": "^5.2.2",
"typescript": "^5.7.3",
"fs-extra": "^11.2.0",
"uglify-js": "^3.19.3"
},
+2
View File
@@ -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),
+1 -1
View File
@@ -198,7 +198,7 @@ export const REG_BLOCK_REF_CLEAN = /[!"#$%&()*+,.:;<=>?@^`{|}~\/\[\]\\\r\n]/g;
// /[!"#$%&()*+,.:;<=>?@^`{|}~\/\[\]\\]/g;
// https://discord.com/channels/686053708261228577/989603365606531104/1000128926619816048
// /\+|\/|~|=|%|\(|\)|{|}|,|&|\.|\$|!|\?|;|\[|]|\^|#|\*|<|>|&|@|\||\\|"|:|\s/g;
export const IMAGE_TYPES = ["jpeg", "jpg", "png", "gif", "svg", "webp", "bmp", "ico", "jtif", "tif"];
export const IMAGE_TYPES = ["jpeg", "jpg", "png", "gif", "svg", "webp", "bmp", "ico", "jtif", "tif", "jfif", "avif"];
export const ANIMATED_IMAGE_TYPES = ["gif", "webp", "apng", "svg"];
export const EXPORT_TYPES = ["svg", "dark.svg", "light.svg", "png", "dark.png", "light.png"];
export const MAX_IMAGE_SIZE = 500;
+26 -3
View File
@@ -42,6 +42,7 @@ import { TAG_AUTOEXPORT, TAG_MDREADINGMODE, TAG_PDFEXPORT } from "src/constants/
import { HotkeyEditor } from "src/shared/Dialogs/HotkeyEditor";
import { getExcalidrawViews } from "src/utils/obsidianUtils";
import { createSliderWithText } from "src/utils/sliderUtils";
import { PDFExportSettingsComponent, PDFExportSettings } from "src/shared/Dialogs/PDFExportSettingsComponent";
export interface ExcalidrawSettings {
folder: string;
@@ -218,6 +219,7 @@ export interface ExcalidrawSettings {
rank: Rank;
modifierKeyOverrides: {modifiers: Modifier[], key: string}[];
showSplashscreen: boolean;
pdfSettings: PDFExportSettings;
}
declare const PLUGIN_VERSION:string;
@@ -497,6 +499,16 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
{modifiers: ["Mod"], key:"G"},
],
showSplashscreen: true,
pdfSettings: {
pageSize: "A4",
pageOrientation: "portrait",
fitToPage: 1,
paperColor: "white",
customPaperColor: "#ffffff",
alignment: "center",
margin: "normal",
exportDPI: 300,
},
};
export class ExcalidrawSettingTab extends PluginSettingTab {
@@ -2118,6 +2130,20 @@ 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.createEl("summary", {
text: t("EXPORT_HEAD"),
@@ -2262,9 +2288,6 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
el.innerHTML = t("MD_EMBED_CUSTOMDATA_HEAD_DESC");
});
new EmbeddalbeMDFileCustomDataSettingsComponent(
detailsEl,
this.plugin.settings.embeddableMarkdownDefaults,
+90 -6
View File
@@ -386,13 +386,14 @@ 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>" +
"<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.",
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_NAME: "Render Excalidraw as Image in Obsidian PDF Export",
SHOW_DRAWING_OR_MD_IN_EXPORTPDF_DESC:
"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>When <b>enabled</b> the PDF will show the Excalidraw drawing only;</li>" +
"<li>When <b>disabled</b> the PDF will show the markdown side of the document.</li></ul>" +
"See the other related setting for <a href='#"+TAG_MDREADINGMODE+"'>Markdown Reading Mode</a> under 'Appearnace and Behavior' further above.<br>" +
"⚠️ Note, you must close the active excalidraw/markdown file and reopen for this change to take effect. ⚠️",
"This setting controls how Excalidraw files are exported to PDF using Obsidian's built-in <b>Export to PDF</b> feature.<br>" +
"<ul><li><b>Enabled:</b> The PDF will include the Excalidraw drawing as an image.</li>" +
"<li><b>Disabled:</b> The PDF will include the markdown content as text.</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 'Appearance and Behavior' further above.<br>" +
"⚠️ You must close and reopen the Excalidraw/markdown file for changes to take effect. ⚠️",
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 ` +
"conflict with Obsidian hotkey settings. The hotkey combinations you add below will override Obsidian's hotkey settings while useing Excalidraw, thus " +
@@ -661,6 +662,7 @@ FILENAME_HEAD: "Filename",
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. " +
"The setting only takes effect the next time you (re)open drawings.",
PDF_EXPORT_SETTINGS: "PDF Export Settings",
EXPORT_HEAD: "Auto-export Settings",
EXPORT_SYNC_NAME:
"Keep the .SVG and/or .PNG filenames in sync with the drawing file",
@@ -1006,4 +1008,86 @@ FILENAME_HEAD: "Filename",
LINK_CLICK_POPOUT: "Open in a popout window",
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)",
//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_DPI: "Image quality [DPI]",
EXPORTDIALOG_PDF_FIT_TO_PAGE: "Page Fitting",
EXPORTDIALOG_PDF_FIT_OPTION: "Fit to page",
EXPORTDIALOG_PDF_FIT_2_OPTION: "Fit to 2-pages",
EXPORTDIALOG_PDF_FIT_4_OPTION: "Fit to 4-pages",
EXPORTDIALOG_PDF_FIT_6_OPTION: "Fit to 6-pages",
EXPORTDIALOG_PDF_FIT_8_OPTION: "Fit to 8-pages",
EXPORTDIALOG_PDF_FIT_12_OPTION: "Fit to 12-pages",
EXPORTDIALOG_PDF_FIT_16_OPTION: "Fit to 16-pages",
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",
EXPORTDIALOG_PDF_PROGRESS_NOTICE: "Exporting page",
EXPORTDIALOG_PDF_PROGRESS_IMAGE: "of image",
EXPORTDIALOG_PDF_PROGRESS_DONE: "Export complete",
};
+292 -86
View File
@@ -1,11 +1,16 @@
import { ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/excalidraw/types";
import { Modal, Setting, TFile } from "obsidian";
import { Modal, Notice, Setting, TFile, ButtonComponent } from "obsidian";
import { getEA } from "src/core";
import { DEVICE } from "src/constants/constants";
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, 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 {
private ea: ExcalidrawAutomate;
@@ -27,6 +32,18 @@ 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 buttonContainerRow1: HTMLDivElement;
private buttonContainerRow2: HTMLDivElement;
public fitToPage: number = 1;
public paperColor: "white" | "scene" | "custom" = "white";
public customPaperColor: string = "#ffffff";
public alignment: PDFPageAlignment = "center";
public margin: PDFPageMarginString = "normal";
public exportDPI: number = 300;
constructor(
private plugin: ExcalidrawPlugin,
@@ -44,6 +61,16 @@ export class ExportDialog extends Modal {
this.exportSelectedOnly = false;
this.saveToVault = true;
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.exportDPI = plugin.settings.pdfSettings.exportDPI ?? 300;
this.saveSettings = false;
}
@@ -62,7 +89,7 @@ export class ExportDialog extends Modal {
onOpen(): void {
this.containerEl.classList.add("excalidraw-release");
this.titleEl.setText(`Export Image`);
this.titleEl.setText(t("EXPORTDIALOG_TITLE"));
this.hasSelectedElements = this.view.getViewSelectedElements().length > 0;
//@ts-ignore
this.selectedOnlySetting.setVisibility(this.hasSelectedElements);
@@ -73,28 +100,100 @@ 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: t("EXPORTDIALOG_TAB_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: 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 paddingSetting: Setting;
this.contentContainer.createEl("h1",{text: t("EXPORTDIALOG_IMAGE_SETTINGS")});
this.contentContainer.createEl("p",{text: t("EXPORTDIALOG_IMAGE_DESC")})
this.createSaveSettingsDropdown();
const size = ():DocumentFragment => {
const width = Math.round(this.scale*this.boundingBox.width + this.padding*2);
const height = Math.round(this.scale*this.boundingBox.height + this.padding*2);
return fragWithHTML(`The lager the scale, the larger the image.<br>Scale: <b>${this.scale}</b><br>Image size: <b>${width}x${height}</b>`);
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>`);
}
const padding = ():DocumentFragment => {
return fragWithHTML(`Current image padding is <b>${this.padding}</b>`);
return fragWithHTML(`${t("EXPORTDIALOG_CURRENT_PADDING")} <b>${this.padding}</b>`);
}
paddingSetting = new Setting(this.contentEl)
.setName("Image padding")
paddingSetting = new Setting(this.contentContainer)
.setName(t("EXPORTDIALOG_PADDING"))
.setDesc(padding())
.addSlider(slider => {
slider
.setLimits(0,50,1)
.setLimits(0,100,1)
.setValue(this.padding)
.onChange(value => {
this.padding = value;
@@ -103,12 +202,12 @@ export class ExportDialog extends Modal {
})
})
scaleSetting = new Setting(this.contentEl)
.setName("PNG Scale")
scaleSetting = new Setting(this.contentContainer)
.setName(t("EXPORTDIALOG_SCALE"))
.setDesc(size())
.addSlider(slider =>
slider
.setLimits(0.5,5,0.5)
.setLimits(0.2,7,0.1)
.setValue(this.scale)
.onChange(value => {
this.scale = value;
@@ -116,109 +215,216 @@ export class ExportDialog extends Modal {
})
)
new Setting(this.contentEl)
.setName("Export theme")
new Setting(this.contentContainer)
.setName(t("EXPORTDIALOG_EXPORT_THEME"))
.addDropdown(dropdown =>
dropdown
.addOption("light","Light")
.addOption("dark","Dark")
.addOption("light", t("EXPORTDIALOG_THEME_LIGHT"))
.addOption("dark", t("EXPORTDIALOG_THEME_DARK"))
.setValue(this.theme)
.onChange(value => {
this.theme = value;
})
)
new Setting(this.contentEl)
.setName("Background color")
new Setting(this.contentContainer)
.setName(t("EXPORTDIALOG_BACKGROUND"))
.addDropdown(dropdown =>
dropdown
.addOption("transparent","Transparent")
.addOption("with-color","Use scene background color")
.addOption("transparent", t("EXPORTDIALOG_BACKGROUND_TRANSPARENT"))
.addOption("with-color", t("EXPORTDIALOG_BACKGROUND_USE_COLOR"))
.setValue(this.transparent?"transparent":"with-color")
.onChange(value => {
this.transparent = value === "transparent";
})
)
new Setting(this.contentEl)
.setName("Save or one-time settings?")
this.selectedOnlySetting = new Setting(this.contentContainer)
.setName(t("EXPORTDIALOG_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", t("EXPORTDIALOG_SELECTED_ALL"))
.addOption("selected", t("EXPORTDIALOG_SELECTED_SELECTED"))
.setValue(this.exportSelectedOnly?"selected":"all")
.onChange(value => {
this.saveSettings = value === "save";
this.exportSelectedOnly = value === "selected";
})
)
);
}
this.contentEl.createEl("h1",{text:"Export settings"});
new Setting(this.contentEl)
.setName("Embed the Excalidraw scene in the exported file?")
private createExportSettings() {
new Setting(this.contentContainer)
.setName(t("EXPORTDIALOG_EMBED_SCENE"))
.addDropdown(dropdown =>
dropdown
.addOption("embed","Embed scene")
.addOption("no-embed","Do not embed scene")
.addOption("embed",t("EXPORTDIALOG_EMBED_YES"))
.addOption("no-embed",t("EXPORTDIALOG_EMBED_NO"))
.setValue(this.embedScene?"embed":"no-embed")
.onChange(value => {
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,
exportDPI: this.exportDPI,
};
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;
this.exportDPI = pdfSettings.exportDPI ?? 300;
}
).render();
}
private createImageButtons() {
if(DEVICE.isDesktop) {
new Setting(this.contentEl)
.setName("Where to save the image?")
.addDropdown(dropdown =>
dropdown
.addOption("vault","Save image to your Vault")
.addOption("outside","Export image outside your Vault")
.setValue(this.saveToVault?"vault":"outside")
.onChange(value => {
this.saveToVault = value === "vault";
})
)
}
this.selectedOnlySetting = new Setting(this.contentEl)
.setName("Export entire scene or just selected elements?")
.addDropdown(dropdown =>
dropdown
.addOption("all","Export entire scene")
.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();
};
const bSVG = div.createEl("button", { text: "SVG to 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" });
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" });
bPNGClipboard.onclick = () => {
this.view.exportPNGToClipboard(this.embedScene, this.hasSelectedElements && this.exportSelectedOnly);
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) {
const bExcalidraw = this.buttonContainerRow2.createEl("button", {
text: t("EXPORTDIALOG_EXCALIDRAW"),
cls: "excalidraw-export-button"
});
bExcalidraw.onclick = () => {
this.view.exportExcalidraw();
this.close();
};
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", {
text: t("EXPORTDIALOG_SVGTOVAULT"),
cls: "excalidraw-export-button"
});
bSVGVault.onclick = () => {
this.view.saveSVG(this.view.getScene(this.hasSelectedElements && this.exportSelectedOnly));
this.close();
};
const bSVGClipboard = this.buttonContainerRow2.createEl("button", {
text: t("EXPORTDIALOG_SVGTOCLIPBOARD"),
cls: "excalidraw-export-button"
});
bSVGClipboard.onclick = async () => {
const svg = await this.view.getSVG(this.embedScene, this.hasSelectedElements && this.exportSelectedOnly);
exportSVGToClipboard(svg);
this.close();
};
}
private createPDFButton() {
const bSavePDFSettings = this.buttonContainerRow1.createEl("button",
{ text: t("EXPORTDIALOG_SAVE_PDF_SETTINGS"), cls: "excalidraw-export-button" }
);
bSavePDFSettings.onclick = async () => {
//in case sync loaded a new version of settings in the mean time
await this.plugin.loadSettings();
this.plugin.settings.pdfSettings = {
pageSize: this.pageSize,
pageOrientation: this.pageOrientation,
fitToPage: this.fitToPage,
paperColor: this.paperColor,
customPaperColor: this.customPaperColor,
alignment: this.alignment,
margin: this.margin,
exportDPI: this.exportDPI,
};
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";
}
}
}
+26
View File
@@ -17,6 +17,32 @@ I develop this plugin as a hobby, spending my free time doing this. If you find
<div class="ex-coffee-div"><a href="https://ko-fi.com/zsolt"><img src="https://storage.ko-fi.com/cdn/kofi6.png?v=6" border="0" alt="Buy Me a Coffee at ko-fi.com" height=45></a></div>
`,
"2.7.5":`
## Fixed
- PDF export scenario described in [#2184](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/2184)
- Elbow arrows do not work within frames [#2187](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/2187)
- Embedding images into Excalidraw with areaRef links did not work as expected due to conflicting SVG viewbox and width and height values
- Can't exit full-screen mode in popout windows using the Command Palette toggle action [#2188](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/2188)
- If the image mask extended beyond the image in "Mask and Crop" image mode, the mask got misaligned from the image.
- PDF image embedding fixes that impacted some PDF files (not all):
- When cropping the PDF page in the scene (by double-clicking the image to crop), the size and position of the PDF cutout drifted.
- Using PDF++ there was a small offset in the position of the cutout in PDF++ and the image in Excalidraw.
- Updated a number of scripts including Split Ellipse, Select Similar Elements, and Concatenate Lines
## New in ExcalidrawAutomate
${String.fromCharCode(96,96,96)}
/**
* Add, modify, or delete keys in element.customData and preserve existing keys.
* Creates customData={} if it does not exist.
* Takes the element id for an element in ea.elementsDict and the newData to add or modify.
* To delete keys set key value in newData to undefined. So {keyToBeDeleted:undefined} will be deleted.
* @param id
* @param newData
* @returns undefined if element does not exist in elementsDict, returns the modified element otherwise.
*/
public addAppendUpdateCustomData(id:string, newData: Partial<Record<string, unknown>>);
${String.fromCharCode(96,96,96)}
`,
"2.7.4":`
## Fixed
- Regression from 2.7.3 where image fileId got overwritten in some cases
@@ -0,0 +1,164 @@
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: number;
paperColor: "white" | "scene" | "custom";
customPaperColor: string;
alignment: PDFPageAlignment;
margin: PDFPageMarginString;
exportDPI: number;
}
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_DPI"))
.addDropdown(dropdown =>
dropdown
.addOptions({
"150": "150",
"300": "300",
"600": "600",
"1200": "1200"
})
.setValue(`${this.settings.exportDPI}`)
.onChange(value => {
this.settings.exportDPI = parseInt(value);
this.update();
})
);
new Setting(this.contentEl)
.setName(t("EXPORTDIALOG_PDF_FIT_TO_PAGE"))
.addDropdown(dropdown =>
dropdown
.addOptions({
"scale": t("EXPORTDIALOG_PDF_SCALE_OPTION"),
"fit": t("EXPORTDIALOG_PDF_FIT_OPTION"),
"fit-2": t("EXPORTDIALOG_PDF_FIT_2_OPTION"),
"fit-4": t("EXPORTDIALOG_PDF_FIT_4_OPTION"),
"fit-6": t("EXPORTDIALOG_PDF_FIT_6_OPTION"),
"fit-8": t("EXPORTDIALOG_PDF_FIT_8_OPTION"),
"fit-12": t("EXPORTDIALOG_PDF_FIT_12_OPTION"),
"fit-16": t("EXPORTDIALOG_PDF_FIT_16_OPTION")
})
.setValue(this.settings.fitToPage === 1 ? "fit" :
(typeof this.settings.fitToPage === "number" ? `fit-${this.settings.fitToPage}` : "scale"))
.onChange(value => {
this.settings.fitToPage = value === "scale" ? 0 :
(value === "fit" ? 1 : parseInt(value.split("-")[1]));
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();
})
);
}
}
+58
View File
@@ -224,6 +224,64 @@ 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.",
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" +
"@property {number} exportDPI - The DPI of the exported PDF (150/300/600/1200).\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" +
" exportDPI: 300\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",
code: "async createPNG(templatePath?: string, scale?: number, exportSettings?: ExportSettings, loader?: EmbeddedFilesLoader, theme?: string,padding?: number): Promise<any>;",
+51 -11
View File
@@ -19,7 +19,7 @@ import { ExportSettings } from "../view/ExcalidrawView";
import { t } from "../lang/helpers";
import { tex2dataURL } from "./LaTeX";
import ExcalidrawPlugin from "../core/main";
import { blobToBase64, getDataURLFromURL, getMimeType, getPDFDoc, getURLImageExtension, hasExcalidrawEmbeddedImagesTreeChanged, readLocalFileBinary } from "../utils/fileUtils";
import { blobToBase64, getDataURLFromURL, getMimeType, getPDFDoc, getURLImageExtension, readLocalFileBinary } from "../utils/fileUtils";
import {
errorlog,
getDataURL,
@@ -91,6 +91,7 @@ export type PDFPageViewProps = {
bottom: number;
right: number;
top: number;
rotate?: number; //may be undefined in legacy files
}
export type Size = {
@@ -866,19 +867,58 @@ export class EmbeddedFilesLoader {
}
const [left, bottom, right, top] = page.view;
viewProps = {left, bottom, right, top};
viewProps.rotate = page.rotate;
if(validRect) {
const pageHeight = top - bottom;
width = (cropRect[2] - cropRect[0]) * scale;
height = (cropRect[3] - cropRect[1]) * scale;
const crop = validRect ? {
left: (cropRect[0] - left) * scale,
top: (bottom + pageHeight - cropRect[3]) * scale,
width,
height,
} : undefined;
if(crop) {
const pageHeight = top - bottom;
const pageWidth = right - left;
if(!page.rotate || page.rotate === 0) {
width = (cropRect[2] - cropRect[0]) * scale;
height = (cropRect[3] - cropRect[1]) * scale;
const crop = {
left: (cropRect[0] - left) * scale,
top: (bottom + pageHeight - cropRect[3]) * scale,
width,
height,
};
return cropCanvas(canvas, crop);
}
if(page.rotate === 90) {
width = (cropRect[3] - cropRect[1]) * scale;
height = (cropRect[2] - cropRect[0]) * scale;
const crop = {
left: cropRect[1] * scale,
top: (pageHeight - cropRect[2]) * scale,
width,
height,
};
return cropCanvas(canvas, crop);
}
if(page.rotate === 180) {
width = (cropRect[2] - cropRect[0]) * scale;
height = (cropRect[3] - cropRect[1]) * scale;
const crop = {
left: (pageWidth - cropRect[2]) * scale,
top: cropRect[1] * scale,
width,
height,
};
return cropCanvas(canvas, crop);
}
if(page.rotate === 270) {
width = (cropRect[3] - cropRect[1]) * scale;
height = (cropRect[2] - cropRect[0]) * scale;
const crop = {
left: (pageWidth - cropRect[3]) * scale,
top: cropRect[0] * scale,
width,
height,
};
return cropCanvas(canvas, crop);
}
}
File diff suppressed because it is too large Load Diff
+92 -10
View File
@@ -15,17 +15,76 @@ export function getPDFCropRect (props: {
return null;
}
const rotate = props.pdfPageViewProps.rotate ?? 0;
const { left, bottom } = props.pdfPageViewProps;
const R0 = parseInt(rectVal[1]);
const R1 = parseInt(rectVal[2]);
const R2 = parseInt(rectVal[3]);
const R3 = parseInt(rectVal[4]);
if(rotate === 90) {
const _top = R0;
const _left = R1;
const _bottom = R2;
const _right = R3;
const x = _left * props.scale;
const y = _top * props.scale;
return {
x,
y,
width: _right*props.scale - x,
height: _bottom*props.scale - y,
naturalWidth: props.naturalWidth,
naturalHeight: props.naturalHeight,
}
}
if(rotate === 180) {
const _right = R0;
const _top = R1;
const _left = R2;
const _bottom = R3;
const y = _top * props.scale;
const x = props.naturalWidth - _left * props.scale;
return {
x,
y,
width: props.naturalWidth - x - _right * props.scale,
height: _bottom * props.scale - y,
naturalWidth: props.naturalWidth,
naturalHeight: props.naturalHeight,
}
}
if(rotate === 270) {
const _bottom = R0;
const _right = R1;
const _top = R2;
const _left = R3;
const x = props.naturalWidth - _left * props.scale;
const y = props.naturalHeight - _top * props.scale;
return {
x,
y,
width: props.naturalWidth - x - _right * props.scale,
height: props.naturalHeight - y - _bottom * props.scale,
naturalWidth: props.naturalWidth,
naturalHeight: props.naturalHeight,
}
}
// default to 0° rotation
const _left = R0;
const _bottom = R1;
const _right = R2;
const _top = R3;
return {
x: (R0 - left) * props.scale,
y: (bottom + props.naturalHeight/props.scale - R3) * props.scale,
width: (R2 - R0) * props.scale,
height: (R3 - R1) * props.scale,
x: (_left - left) * props.scale,
y: props.naturalHeight - (_top - bottom) * props.scale,
width: (_right - _left) * props.scale,
height: (_top - _bottom) * props.scale,
naturalWidth: props.naturalWidth,
naturalHeight: props.naturalHeight,
}
@@ -34,13 +93,36 @@ export function getPDFCropRect (props: {
export function getPDFRect({elCrop, scale, customData}:{
elCrop: ImageCrop, scale: number, customData: Record<string, unknown>
}): string {
const rotate = (customData.pdfPageViewProps as PDFPageViewProps)?.rotate ?? 0;
const { left, bottom } = (customData && customData.pdfPageViewProps)
? customData.pdfPageViewProps as PDFPageViewProps
: { left: 0, bottom: 0 };
const R0 = elCrop.x / scale + left;
const R2 = elCrop.width / scale + R0;
const R3 = bottom + (elCrop.naturalHeight - elCrop.y) / scale;
const R1 = R3 - elCrop.height / scale;
return `&rect=${Math.round(R0)},${Math.round(R1)},${Math.round(R2)},${Math.round(R3)}`;
}
if(rotate === 90) {
const _top = (elCrop.y) / scale;
const _left = (elCrop.x) / scale;
const _bottom = (elCrop.height + elCrop.y) / scale;
const _right = (elCrop.width + elCrop.x) / scale;
return `&rect=${Math.round(_top)},${Math.round(_left)},${Math.round(_bottom)},${Math.round(_right)}`;
}
if(rotate === 180) {
const _right = (elCrop.naturalWidth-elCrop.x-elCrop.width) / scale;
const _top = (elCrop.y) / scale;
const _left = (elCrop.naturalWidth - elCrop.x) / scale;
const _bottom = (elCrop.height + elCrop.y) / scale;
return `&rect=${Math.round(_right)},${Math.round(_top)},${Math.round(_left)},${Math.round(_bottom)}`;
}
if(rotate === 270) {
const _bottom = (elCrop.naturalHeight - elCrop.height-elCrop.y) / scale;
const _right = (elCrop.naturalWidth - elCrop.width - elCrop.x) / scale;
const _top = (elCrop.naturalHeight - elCrop.y) / scale;
const _left = (elCrop.naturalWidth - elCrop.x) / scale;
return `&rect=${Math.round(_bottom)},${Math.round(_right)},${Math.round(_top)},${Math.round(_left)}`;
}
const _left = elCrop.x / scale + left;
const _right = elCrop.width / scale + _left;
const _top = bottom + (elCrop.naturalHeight - elCrop.y) / scale;
const _bottom = _top - elCrop.height / scale;
return `&rect=${Math.round(_left)},${Math.round(_bottom)},${Math.round(_right)},${Math.round(_top)}`;
}
+12 -1
View File
@@ -1,7 +1,7 @@
import { NonDeletedExcalidrawElement } from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { DEVICE, REG_LINKINDEX_INVALIDCHARS } from "src/constants/constants";
import { getParentOfClass } from "./obsidianUtils";
import { TFile, WorkspaceLeaf } from "obsidian";
import { App, TFile, WorkspaceLeaf } from "obsidian";
import { getLinkParts } from "./utils";
import ExcalidrawView from "src/view/ExcalidrawView";
@@ -55,4 +55,15 @@ export const generateEmbeddableLink = (src: string, theme: "light" | "dark"):str
}
}*/
return src;
}
export function setFileToLocalGraph(app: App, file: TFile) {
let lgv;
app.workspace.iterateAllLeaves((l) => {
if (l.view?.getViewType() === "localgraph") lgv = l.view;
});
if (lgv) {
//@ts-ignore
lgv.loadFile(file);
}
}
+411
View File
@@ -0,0 +1,411 @@
import { PDFDocument, rgb } from '@cantoo/pdf-lib';
import exp from 'constants';
import { Notice } from 'obsidian';
import { getEA } from 'src/core';
import { t } from 'src/lang/helpers';
const PDF_DPI = 72;
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: number; // 0 means use zoom, >1 means fit to that many pages exactly
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;
exportDPI: number;
}
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,
): {x: number, y: number} {
const availableWidth = pageWidth - margin.left - margin.right;
const availableHeight = pageHeight - margin.top - margin.bottom;
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;
}
return {x, y};
}
function getNumberOfPages(
width: number,
height: number,
availableWidth: number,
availableHeight: number
): number {
const cols = Math.ceil(width / availableWidth);
const rows = Math.ceil(height / availableHeight);
return cols * rows;
}
function calculateDimensions(
svgWidth: number,
svgHeight: number,
pageDim: PageDimensions,
margin: PDFPageProperties['margin'],
scale: PDFExportScale,
alignment: PDFPageAlignment,
exportDPI: number,
): SVGDimensions[] {
const svg_to_pdf_scale = PDF_DPI / exportDPI;
const pdfWidth = svgWidth * svg_to_pdf_scale;
const pdfHeight = svgHeight * svg_to_pdf_scale;
const availableWidth = pageDim.width - margin.left - margin.right;
const availableHeight = pageDim.height - margin.top - margin.bottom;
// If fitToPage is specified, find optimal zoom using binary search
if (scale.fitToPage > 0) {
let low = 0;
let high = 100; // Start with a reasonable upper bound
let bestZoom = 1;
const tolerance = 0.000001;
while (high - low > tolerance) {
const mid = (low + high) / 2;
const scaledWidth = pdfWidth * mid;
const scaledHeight = pdfHeight * mid;
const pages = getNumberOfPages(scaledWidth, scaledHeight, availableWidth, availableHeight);
if (pages > scale.fitToPage) {
high = mid;
} else {
bestZoom = mid;
low = mid;
}
}
// Apply a small reduction to prevent floating-point issues
scale.zoom = Math.round(bestZoom * 0.99999 * 1000000) / 1000000;
}
// Now handle as regular scale mode
const finalWidth = Math.round(pdfWidth * (scale.zoom || 1) * 1000) / 1000;
const finalHeight = Math.round(pdfHeight * (scale.zoom || 1) * 1000) / 1000;
// Round the available dimensions as well for consistent comparison
const roundedAvailableWidth = Math.round(availableWidth * 1000) / 1000;
const roundedAvailableHeight = Math.round(availableHeight * 1000) / 1000;
if (finalWidth <= roundedAvailableWidth && finalHeight <= roundedAvailableHeight) {
// Content fits on one page
const position = calculatePosition(
finalWidth,
finalHeight,
pageDim.width,
pageDim.height,
margin,
alignment,
);
return [{
width: finalWidth,
height: finalHeight,
x: position.x,
y: position.y
}];
} else {
// Content needs to be tiled across multiple pages
const dimensions: SVGDimensions[] = [];
// Calculate exact number of needed columns and rows
const cols = Math.ceil(finalWidth / roundedAvailableWidth);
const rows = Math.ceil(finalHeight / roundedAvailableHeight);
for (let row = 0; row < rows; row++) {
for (let col = 0; col < cols; col++) {
// Calculate remaining width and height for this tile
const remainingWidth = finalWidth - col * roundedAvailableWidth;
const remainingHeight = finalHeight - row * roundedAvailableHeight;
// Only create tile if there's actual content to show
if (remainingWidth > 0 && remainingHeight > 0) {
const tileWidth = Math.min(roundedAvailableWidth, remainingWidth);
const tileHeight = Math.min(roundedAvailableHeight, remainingHeight);
dimensions.push({
width: tileWidth,
height: tileHeight,
x: margin.left,
y: margin.top,
sourceX: (col * roundedAvailableWidth) / ((scale.zoom || 1) * svg_to_pdf_scale),
sourceY: (row * roundedAvailableHeight) / ((scale.zoom || 1) * svg_to_pdf_scale),
sourceWidth: tileWidth / ((scale.zoom || 1) * svg_to_pdf_scale),
sourceHeight: tileHeight / ((scale.zoom || 1) * svg_to_pdf_scale)
});
}
}
}
return dimensions;
}
}
async function addSVGToPage(
pdfDoc: PDFDocument,
svg: SVGSVGElement,
dimensions: SVGDimensions,
pageDim: PageDimensions,
pageProps: PDFPageProperties
) {
const page = pdfDoc.addPage([pageDim.width, pageDim.height]);
if (pageProps.backgroundColor && pageProps.backgroundColor !== '#ffffff') {
const { r, g, b } = hexToRGB(pageProps.backgroundColor);
page.drawRectangle({
x: 0,
y: 0,
width: pageDim.width,
height: pageDim.height,
color: rgb(r/255, g/255, b/255),
});
}
// Render SVG to canvas with specified DPI
const canvas = await renderSVGToCanvas(svg, dimensions, pageProps.exportDPI);
// Convert canvas to PNG
const pngData = canvas.toDataURL('image/png');
// Embed the PNG in the PDF
const image = await pdfDoc.embedPng(pngData);
// Adjust y-coordinate to account for PDF coordinate system
const adjustedY = pageDim.height - dimensions.y - dimensions.height;
// Draw the image
page.drawImage(image, {
x: dimensions.x,
y: adjustedY,
width: dimensions.width,
height: dimensions.height,
});
return page;
}
async function renderSVGToCanvas(
svg: SVGSVGElement,
dimensions: SVGDimensions,
exportDPI: number = 300,
): Promise<HTMLCanvasElement> {
const canvas = document.createElement('canvas');
const scale = exportDPI / PDF_DPI;
canvas.width = dimensions.width * scale;
canvas.height = dimensions.height * scale;
const ctx = canvas.getContext('2d');
if (!ctx) throw new Error('Failed to get canvas context');
let svgToRender = svg;
if (dimensions.sourceX !== undefined) {
svgToRender = svg.cloneNode(true) as SVGSVGElement;
const viewBox = `${dimensions.sourceX} ${dimensions.sourceY} ${dimensions.sourceWidth} ${dimensions.sourceHeight}`;
svgToRender.setAttribute('viewBox', viewBox);
svgToRender.setAttribute('width', String(dimensions.sourceWidth));
svgToRender.setAttribute('height', String(dimensions.sourceHeight));
}
const svgBlob = new Blob([svgToRender.outerHTML], { type: 'image/svg+xml;charset=utf-8' });
const blobUrl = URL.createObjectURL(svgBlob);
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => {
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
URL.revokeObjectURL(blobUrl);
resolve(canvas);
};
img.onerror = () => {
URL.revokeObjectURL(blobUrl);
reject(new Error('Failed to load SVG'));
};
img.src = blobUrl;
});
}
export async function exportToPDF({
SVG,
scale = { fitToPage: 1, zoom: 1 },
pageProps,
}: {
SVG: SVGSVGElement[];
scale: PDFExportScale;
pageProps: PDFPageProperties;
}): Promise<ArrayBuffer> {
const pdfDoc = await PDFDocument.create();
const msg = t('EXPORTDIALOG_PDF_PROGRESS_NOTICE');
const imgmsg = t('EXPORTDIALOG_PDF_PROGRESS_IMAGE');
let notice = new Notice(msg, 0);
let j=1;
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,
pageProps.exportDPI
);
let i=1;
for (const dim of dimensions) {
//@ts-ignore
if(notice.containerEl.parentElement) {
notice.setMessage(`${msg} ${i++}/${dimensions.length}${SVG.length>1?` ${imgmsg} ${j}`:""}`);
} else {
notice = new Notice(`${msg} ${i++}/${dimensions.length}${SVG.length>1?` ${imgmsg} ${j}`:""}`, 0);
}
await addSVGToPage(pdfDoc, svg, dim, pageProps.dimensions, pageProps);
}
j++;
}
//@ts-ignore
if(notice.containerEl.parentElement) {
notice.setMessage(t('EXPORTDIALOG_PDF_PROGRESS_DONE'));
setTimeout(() => notice.hide(), 4000);
} else {
new Notice(t('EXPORTDIALOG_PDF_PROGRESS_DONE'));
}
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);
}
}
+30
View File
@@ -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;
@@ -482,3 +493,22 @@ export const hasExcalidrawEmbeddedImagesTreeChanged = (sourceFile: TFile, mtime:
const fileList = getExcalidrawEmbeddedFilesFiletree(sourceFile, plugin);
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);
}
}
+100 -26
View File
@@ -76,7 +76,9 @@ import {
getExcalidrawMarkdownHeaderSection,
} from "../shared/ExcalidrawData";
import {
arrayBufferToBase64,
checkAndCreateFolder,
createOrOverwriteFile,
download,
getDataURLFromURL,
getIMGFilename,
@@ -149,6 +151,8 @@ import { getPDFCropRect } from "../utils/PDFUtils";
import { Position, ViewSemaphores } from "../types/excalidrawViewTypes";
import { DropManager } from "./managers/DropManager";
import { ImageInfo } from "src/types/excalidrawAutomateTypes";
import { exportToPDF, getMarginValue, getPageDimensions, PageOrientation, PageSize } from "src/utils/exportUtils";
import { create } from "domain";
const EMBEDDABLE_SEMAPHORE_TIMEOUT = 2000;
const PREVENT_RELOAD_TIMEOUT = 2000;
@@ -226,20 +230,25 @@ export const addFiles = async (
.forEach((f:FileData) => {
s.scene.elements
.filter((el:ExcalidrawElement)=>el.type === "image" && el.fileId === f.id && (
(el.crop && el.crop.naturalWidth !== f.size.width) || !el.customData?.pdfPageViewProps
(el.crop && el.crop?.naturalWidth !== f.size.width) || !el.customData?.pdfPageViewProps
))
.forEach((el:Mutable<ExcalidrawImageElement>) => {
s.dirty = true;
const scale = f.size.width / el.crop.naturalWidth;
el.crop = {
x: el.crop.x * scale,
y: el.crop.y * scale,
width: el.crop.width * scale,
height: el.crop.height * scale,
naturalWidth: f.size.width,
naturalHeight: f.size.height,
};
addAppendUpdateCustomData(el, { pdfPageViewProps: f.pdfPageViewProps});
if(el.crop) {
s.dirty = true;
const scale = f.size.width / el.crop.naturalWidth;
el.crop = {
x: el.crop.x * scale,
y: el.crop.y * scale,
width: el.crop.width * scale,
height: el.crop.height * scale,
naturalWidth: f.size.width,
naturalHeight: f.size.height,
};
}
if(!el.customData?.pdfPageViewProps) {
s.dirty = true;
addAppendUpdateCustomData(el, { pdfPageViewProps: f.pdfPageViewProps});
}
});
});
@@ -510,19 +519,13 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
}
const exportImage = async (filepath:string, theme?:string) => {
const file = this.app.vault.getAbstractFileByPath(normalizePath(filepath));
const svg = await this.svg(scene,theme, embedScene, true);
if (!svg) {
return;
}
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/2026
const svgString = svg.outerHTML;
if (file && file instanceof TFile) {
await this.app.vault.modify(file, svgString);
} else {
await this.app.vault.create(filepath, svgString);
}
await createOrOverwriteFile(this.app, filepath, svgString);
}
if(this.plugin.settings.autoExportLightAndDark) {
@@ -550,6 +553,83 @@ 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: {
zoom: this.exportDialog.scale,
fitToPage: this.exportDialog.fitToPage
},
pageProps: {
dimensions: getPageDimensions(pageSize, orientation),
backgroundColor: this.exportDialog.getPaperColor(),
margin: getMarginValue(this.exportDialog.margin),
alignment: this.exportDialog.alignment,
exportDPI: this.exportDialog.exportDPI,
}
});
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> {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.png, "ExcalidrawView.png", scene, theme, embedScene);
const ed = this.exportDialog;
@@ -593,17 +673,11 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
}
const exportImage = async (filepath:string, theme?:string) => {
const file = this.app.vault.getAbstractFileByPath(normalizePath(filepath));
const png = await this.png(scene, theme, embedScene);
if (!png) {
return;
}
if (file && file instanceof TFile) {
await this.app.vault.modifyBinary(file, await png.arrayBuffer());
} else {
await this.app.vault.createBinary(filepath, await png.arrayBuffer());
}
await createOrOverwriteFile(this.app, filepath, await png.arrayBuffer());
}
if(this.plugin.settings.autoExportLightAndDark) {
+14 -1
View File
@@ -6,7 +6,7 @@ import { ConstructableWorkspaceSplit, getContainerForDocument, isObsidianThemeDa
import { DEVICE, EXTENDED_EVENT_TYPES, KEYBOARD_EVENT_TYPES } from "src/constants/constants";
import { ExcalidrawImperativeAPI, UIAppState } from "@zsviczian/excalidraw/types/excalidraw/types";
import { ObsidianCanvasNode } from "src/view/managers/CanvasNodeFactory";
import { processLinkText, patchMobileView } from "src/utils/customEmbeddableUtils";
import { processLinkText, patchMobileView, setFileToLocalGraph } from "src/utils/customEmbeddableUtils";
import { EmbeddableMDCustomProps } from "src/shared/Dialogs/EmbeddableSettings";
declare module "obsidian" {
@@ -154,6 +154,15 @@ function RenderObsidianView(
}; //cleanup on unmount
}, [isActiveRef.current, containerRef.current]);
//set local graph to view when deactivating embeddables
React.useEffect(() => {
if(file === view.file) {
return;
}
if(!isActiveRef.current) {
setFileToLocalGraph(view.app, view.file);
}
}, [isActiveRef.current]);
//--------------------------------------------------------------------------------
//Mount the workspace leaf or the canvas node depending on subpath
@@ -408,6 +417,10 @@ function RenderObsidianView(
return;
}
if(file !== view.file) {
setFileToLocalGraph(view.app, file);
}
if(leafRef.current.leaf?.view?.getViewType() === "markdown") {
//Handle markdown leaf
//@ts-ignore
+37
View File
@@ -80,6 +80,11 @@ button.ToolIcon_type_button[title="Export"] {
width: 9em;
}
.excalidraw-export-button {
width: 9em;
margin-left: 10px;
}
.excalidraw-prompt-buttons-div {
display: flex;
flex-direction: row;
@@ -87,6 +92,13 @@ button.ToolIcon_type_button[title="Export"] {
justify-content: space-evenly;
}
.excalidraw-export-buttons-div {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
justify-content: right;
}
li[data-testid] {
border: 0 !important;
margin: 0 !important;
@@ -361,9 +373,16 @@ div.excalidraw-draginfo {
}
.excalidraw [data-radix-popper-content-wrapper] {
/*Overrides position:fixed in popover*/
position: absolute !important;
}
.excalidraw .Island .App-mobile-menu,
.excalidraw .Island.App-menu__left {
/*Arrow Picker Popover Overflow*/
overflow: visible;
}
.excalidraw__embeddable-container .view-header {
display: none !important;
}
@@ -665,3 +684,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;
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.