mirror of
https://github.com/zsviczian/obsidian-excalidraw-plugin.git
synced 2025-08-06 05:46:28 +00:00
Compare commits
47 Commits
2.7.5
...
2.8.0-beta
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1744668fbd | ||
|
|
8e3e2ffb25 | ||
|
|
f5475bfde6 | ||
|
|
27fa270b42 | ||
|
|
15ece75b5d | ||
|
|
a796621f93 | ||
|
|
3c943c6685 | ||
|
|
4209774b4e | ||
|
|
b18637f7d0 | ||
|
|
af8a848d14 | ||
|
|
01e392158d | ||
|
|
fc47b7aa0d | ||
|
|
a0e0627a49 | ||
|
|
efcb0c0580 | ||
|
|
23d7105fb1 | ||
|
|
5d9565bd7c | ||
|
|
59785523ae | ||
|
|
2a21ed5fc7 | ||
|
|
3d3ce73fa1 | ||
|
|
c35bd385fe | ||
|
|
a790b04547 | ||
|
|
5171978c37 | ||
|
|
ea4a0c91e8 | ||
|
|
34af6dd447 | ||
|
|
ed2e700946 | ||
|
|
7eb23ab5e1 | ||
|
|
7cccf1d4e2 | ||
|
|
2a5545964c | ||
|
|
4ce22883cc | ||
|
|
272804afc8 | ||
|
|
dc0b50f717 | ||
|
|
a0eb625b8a | ||
|
|
524dc54d03 | ||
|
|
918718be90 | ||
|
|
78ee784be1 | ||
|
|
7e0e016bf9 | ||
|
|
4f875a03a0 | ||
|
|
63c56e0e98 | ||
|
|
46477208be | ||
|
|
3194c014c7 | ||
|
|
25ccb9dc43 | ||
|
|
fa46f8c39d | ||
|
|
8ffe5c3942 | ||
|
|
88f256cd8f | ||
|
|
1562600cd3 | ||
|
|
d759abbc47 | ||
|
|
90533138e5 |
9
.github/ISSUE_TEMPLATE/How-to.yml
vendored
9
.github/ISSUE_TEMPLATE/How-to.yml
vendored
@@ -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:
|
||||
|
||||
11
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
11
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -1,5 +1,5 @@
|
||||
name: Bug report
|
||||
description: If something is clearly broken, it’s 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, it’s 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
.gitignore
vendored
1
.gitignore
vendored
@@ -4,7 +4,6 @@
|
||||
|
||||
# npm
|
||||
node_modules
|
||||
package-lock.json
|
||||
|
||||
# build
|
||||
main.js
|
||||
|
||||
1340
MathjaxToSVG/package-lock.json
generated
Normal file
1340
MathjaxToSVG/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
MathjaxToSVG/tsconfig.json
Normal file
26
MathjaxToSVG/tsconfig.json
Normal 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",
|
||||
]
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
The project runs with `node 18`.
|
||||
|
||||
After running `npm -i` you'll need to make two manual changes:
|
||||
|
||||
1026
docs/API/ExcalidrawAutomate.d.ts
vendored
1026
docs/API/ExcalidrawAutomate.d.ts
vendored
File diff suppressed because it is too large
Load Diff
@@ -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();
|
||||
@@ -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();
|
||||
|
||||
@@ -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,7 +1,7 @@
|
||||
{
|
||||
"id": "obsidian-excalidraw-plugin",
|
||||
"name": "Excalidraw",
|
||||
"version": "2.7.4",
|
||||
"version": "2.8.0-beta-4",
|
||||
"minAppVersion": "1.1.6",
|
||||
"description": "An Obsidian plugin to edit and view Excalidraw drawings",
|
||||
"author": "Zsolt Viczian",
|
||||
|
||||
@@ -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
package-lock.json
generated
Normal file
9391
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
14
package.json
14
package.json
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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),
|
||||
@@ -145,6 +147,7 @@ const BUILD_CONFIG = {
|
||||
entryFileNames: 'main.js',
|
||||
format: 'cjs',
|
||||
exports: 'default',
|
||||
inlineDynamicImports: true, // Add this line only
|
||||
},
|
||||
plugins: getRollupPlugins(
|
||||
{
|
||||
|
||||
@@ -105,6 +105,41 @@
|
||||
*/
|
||||
//ea.onFileCreateHook = (data) => {};
|
||||
|
||||
/**
|
||||
* If set, this callback is triggered when a image is being saved in Excalidraw.
|
||||
* You can use this callback to customize the naming and path of pasted images to avoid
|
||||
* default names like "Pasted image 123147170.png" being saved in the attachments folder,
|
||||
* and instead use more meaningful names based on the Excalidraw file or other criteria,
|
||||
* plus save the image in a different folder.
|
||||
*
|
||||
* If the function returns null or undefined, the normal Excalidraw operation will continue
|
||||
* with the excalidraw generated name and default path.
|
||||
* If a filepath is returned, that will be used. Include the full Vault filepath and filename
|
||||
* with the file extension.
|
||||
* The currentImageName is the name of the image generated by excalidraw or provided during paste.
|
||||
*
|
||||
* @param data - An object containing the following properties:
|
||||
* @property {string} [currentImageName] - Default name for the image.
|
||||
* @property {string} drawingFilePath - The file path of the Excalidraw file where the image is being used.
|
||||
*
|
||||
* @returns {string} - The new filepath for the image including full vault path and extension.
|
||||
*
|
||||
* Example usage:
|
||||
* ```
|
||||
* onImageFilePathHook: (data) => {
|
||||
* const { currentImageName, drawingFilePath } = data;
|
||||
* const ext = currentImageName.split('.').pop();
|
||||
* // Generate a new filepath based on the drawing file name and other criteria
|
||||
* return `${drawingFileName} - ${currentImageName || 'image'}.${ext}`;
|
||||
* }
|
||||
* ```
|
||||
* onImageFilePathHook: (data: {
|
||||
* currentImageName: string; // Excalidraw generated name of the image, or the name received from the file system.
|
||||
* drawingFilePath: string; // The full filepath of the Excalidraw file where the image is being used.
|
||||
* }) => string = null;
|
||||
*/
|
||||
//ea.onImageFilePathHook = (data) => {};
|
||||
|
||||
/**
|
||||
* If set, this callback is triggered whenever the active canvas color changes
|
||||
* onCanvasColorChangeHook: (
|
||||
|
||||
@@ -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;
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -39,7 +39,8 @@ import {
|
||||
setRootElementSize,
|
||||
} from "../constants/constants";
|
||||
import { ExcalidrawSettings, DEFAULT_SETTINGS, ExcalidrawSettingTab } from "./settings";
|
||||
import { initExcalidrawAutomate, ExcalidrawAutomate } from "../shared/ExcalidrawAutomate";
|
||||
import { ExcalidrawAutomate } from "../shared/ExcalidrawAutomate";
|
||||
import { initExcalidrawAutomate } from "src/utils/excalidrawAutomateUtils";
|
||||
import { around, dedupe } from "monkey-around";
|
||||
import { t } from "../lang/helpers";
|
||||
import {
|
||||
@@ -91,6 +92,15 @@ import { EventManager } from "./managers/EventManager";
|
||||
declare const PLUGIN_VERSION:string;
|
||||
declare const INITIAL_TIMESTAMP: number;
|
||||
|
||||
type FileMasterInfo = {
|
||||
isHyperLink: boolean;
|
||||
isLocalLink: boolean;
|
||||
path: string;
|
||||
hasSVGwithBitmap: boolean;
|
||||
blockrefData: string,
|
||||
colorMapJSON?: string
|
||||
}
|
||||
|
||||
export default class ExcalidrawPlugin extends Plugin {
|
||||
private fileManager: PluginFileManager;
|
||||
private observerManager: ObserverManager;
|
||||
@@ -113,7 +123,7 @@ export default class ExcalidrawPlugin extends Plugin {
|
||||
public opencount: number = 0;
|
||||
public ea: ExcalidrawAutomate;
|
||||
//A master list of fileIds to facilitate copy / paste
|
||||
public filesMaster: Map<FileId, { isHyperLink: boolean; isLocalLink: boolean; path: string; hasSVGwithBitmap: boolean; blockrefData: string, colorMapJSON?: string}> =
|
||||
public filesMaster: Map<FileId, FileMasterInfo> =
|
||||
null; //fileId, path
|
||||
public equationsMaster: Map<FileId, string> = null; //fileId, formula
|
||||
public mermaidsMaster: Map<FileId, string> = null; //fileId, mermaidText
|
||||
|
||||
@@ -29,11 +29,8 @@ import { InsertCommandDialog } from "../../shared/Dialogs/InsertCommandDialog";
|
||||
import { InsertImageDialog } from "../../shared/Dialogs/InsertImageDialog";
|
||||
import { ImportSVGDialog } from "../../shared/Dialogs/ImportSVGDialog";
|
||||
import { InsertMDDialog } from "../../shared/Dialogs/InsertMDDialog";
|
||||
import {
|
||||
ExcalidrawAutomate,
|
||||
insertLaTeXToView,
|
||||
search,
|
||||
} from "../../shared/ExcalidrawAutomate";
|
||||
import { ExcalidrawAutomate } from "../../shared/ExcalidrawAutomate";
|
||||
import { insertLaTeXToView, search } from "src/utils/excalidrawAutomateUtils";
|
||||
import { templatePromt } from "../../shared/Dialogs/Prompt";
|
||||
import { t } from "../../lang/helpers";
|
||||
import {
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
} from "obsidian";
|
||||
import { DEVICE, RERENDER_EVENT } from "../../constants/constants";
|
||||
import { EmbeddedFilesLoader } from "../../shared/EmbeddedFileLoader";
|
||||
import { createPNG, createSVG } from "../../shared/ExcalidrawAutomate";
|
||||
import { createPNG, createSVG } from "../../utils/excalidrawAutomateUtils";
|
||||
import { ExportSettings } from "../../view/ExcalidrawView";
|
||||
import ExcalidrawPlugin from "../main";
|
||||
import {getIMGFilename,} from "../../utils/fileUtils";
|
||||
@@ -801,15 +801,16 @@ const tmpObsidianWYSIWYG = async (
|
||||
if(Boolean(ctx.frontmatter)) {
|
||||
el.empty();
|
||||
} else {
|
||||
const warningEl = el.querySelector("div>h3[data-heading^='Unable to find section #^");
|
||||
//Obsidian changed this at some point from h3 to h5 and also the text...
|
||||
const warningEl = el.querySelector("div>*[data-heading^='Unable to find ");
|
||||
if(warningEl) {
|
||||
const ref = warningEl.getAttr("data-heading").match(/Unable to find section (#\^(?:group=|area=|frame=|clippedframe=)[^ ]*)/)?.[1];
|
||||
const dataHeading = warningEl.getAttr("data-heading");
|
||||
const ref = warningEl.getAttr("data-heading").match(/Unable to find[^^]+(\^(?:group=|area=|frame=|clippedframe=)[^ ”]+)/)?.[1];
|
||||
if(ref) {
|
||||
attr.fname = file.path + ref;
|
||||
attr.fname = file.path + "#" +ref;
|
||||
areaPreview = true;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
if(!isFrontmatterDiv && !areaPreview) {
|
||||
if(el.parentElement === containerEl) containerEl.removeChild(el);
|
||||
|
||||
@@ -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,
|
||||
@@ -2486,6 +2509,9 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
|
||||
this.requestReloadDrawings = true;
|
||||
this.plugin.settings.experimentalEnableFourthFont = value;
|
||||
this.applySettingsUpdate();
|
||||
if(value) {
|
||||
this.plugin.initializeFonts();
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -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",
|
||||
};
|
||||
|
||||
@@ -386,13 +386,14 @@ FILENAME_HEAD: "文件名",
|
||||
"此设置不会影响您在 Excalidraw 模式下的绘图显示,或者在将绘图嵌入 Markdown 文档时,或在渲染悬停预览时。<br><ul>" +
|
||||
"<li>请参阅下面‘嵌入和导出’部分的 <a href='#"+TAG_PDFEXPORT+"'>PDF 导出</a> 相关设置。</li></ul><br>" +
|
||||
"您必须关闭当前的 Excalidraw/Markdown 文件并重新打开,以使此更改生效。",
|
||||
SHOW_DRAWING_OR_MD_IN_EXPORTPDF_NAME: "在将 Excalidraw 文件导出为 PDF 时将文件渲染为图像",
|
||||
SHOW_DRAWING_OR_MD_IN_EXPORTPDF_NAME : "在 Obsidian 中导出为 PDF 格式时将 Excalidraw 渲染为图像" ,
|
||||
SHOW_DRAWING_OR_MD_IN_EXPORTPDF_DESC:
|
||||
"处于 Markdown 视图模式时,此设置控制 Excalidraw 在使用 Obsidian 的 <b>导出为 PDF</b> 功能时,将 Excalidraw 文件导出为 PDF 的行为。<br>" +
|
||||
"<ul><li>当 <b>启用</b> 时,PDF 将仅显示 Excalidraw 绘图;</li>" +
|
||||
"<li>当 <b>禁用</b> 时,PDF 将显示文档的 Markdown 部分(背景笔记)。</li></ul>" +
|
||||
"请参阅上面‘外观和行为’部分的 <<a href='#"+TAG_MDREADINGMODE+"'>>Markdown 阅读模式</a> 相关设置。" +
|
||||
"⚠️ 注意,您必须关闭当前的 Excalidraw/Markdown 文件并重新打开,以使此更改生效。⚠️",
|
||||
"此设置控制在使用 Obsidian 内置的<b>导出为 PDF</b>功能,如何将 Excalidraw 文件导出为 PDF。<br>" +
|
||||
"<ul><li><b>启用:</b>PDF 将包含图像格式的 Excalidraw 绘图。</li>" +
|
||||
"<li><b>禁用:</b>PDF 将包含作为文本的 Markdown 内容。</li></ul>" +
|
||||
"注意:此设置不会影响 Excalidraw 本身的 PDF 导出功能。<br>" +
|
||||
"请参阅上方“外观和行为”部分中与<a href='#" + TAG_MDREADINGMODE + "'>Markdown 阅读模式</a>相关的其他设置。<br>" +
|
||||
"⚠️ 您必须关闭并重新打开 Excalidraw/Markdown 文件,设置更改才会生效。⚠️",
|
||||
HOTKEY_OVERRIDE_HEAD: "热键覆盖",
|
||||
HOTKEY_OVERRIDE_DESC: `一些 Excalidraw 的热键,例如 ${labelCTRL()}+Enter 用于编辑文本,或 ${labelCTRL()}+K 用于创建元素链接。` +
|
||||
"与 Obsidian 的热键设置发生冲突。您在下面添加的热键组合将在使用 Excalidraw 时覆盖 Obsidian 的热键设置," +
|
||||
@@ -661,6 +662,7 @@ FILENAME_HEAD: "文件名",
|
||||
EXPORT_EMBED_SCENE_DESC:
|
||||
"在导出的图像中嵌入 Excalidraw 场景。可以通过在文件级别添加 <code>excalidraw-export-embed-scene: true/false</code> frontmatter 元数据键来覆盖此设置。" +
|
||||
"此设置仅在您下次(重新)打开绘图时生效。",
|
||||
PDF_EXPORT_SETTINGS : "PDF 导出设置",
|
||||
EXPORT_HEAD: "导出设置",
|
||||
EXPORT_SYNC_NAME:
|
||||
"保持 SVG/PNG 文件名与绘图文件同步",
|
||||
@@ -1006,4 +1008,86 @@ FILENAME_HEAD: "文件名",
|
||||
LINK_CLICK_POPOUT : "在弹出窗口中打开" ,
|
||||
LINK_CLICK_NEW_TAB : "在新标签页中打开" ,
|
||||
LINK_CLICK_MD_PROPS : "显示 Markdown 图片属性对话框(仅在嵌入 Markdown 文档为图片时适用)" ,
|
||||
|
||||
// 导出对话框
|
||||
// 对话框和标签页
|
||||
EXPORTDIALOG_TITLE : "导出图形",
|
||||
EXPORTDIALOG_TAB_IMAGE : "图像",
|
||||
EXPORTDIALOG_TAB_PDF : "PDF",
|
||||
// 设置持久化
|
||||
EXPORTDIALOG_SAVE_SETTINGS : "将图像设置保存到文件 doc.properties 吗?",
|
||||
EXPORTDIALOG_SAVE_SETTINGS_SAVE : "保存为预设",
|
||||
EXPORTDIALOG_SAVE_SETTINGS_ONETIME : "仅本次使用",
|
||||
// 图像设置
|
||||
EXPORTDIALOG_IMAGE_SETTINGS : "图像",
|
||||
EXPORTDIALOG_IMAGE_DESC : "PNG 支持透明。外部文件可以包含 Excalidraw 场景数据。",
|
||||
EXPORTDIALOG_PADDING : "边距",
|
||||
EXPORTDIALOG_SCALE : "缩放",
|
||||
EXPORTDIALOG_CURRENT_PADDING : "当前边距:",
|
||||
EXPORTDIALOG_SIZE_DESC : "缩放会影响输出大小",
|
||||
EXPORTDIALOG_SCALE_VALUE : "缩放:",
|
||||
EXPORTDIALOG_IMAGE_SIZE : "大小:",
|
||||
// 主题和背景
|
||||
EXPORTDIALOG_EXPORT_THEME : "主题",
|
||||
EXPORTDIALOG_THEME_LIGHT : "浅色",
|
||||
EXPORTDIALOG_THEME_DARK : "深色",
|
||||
EXPORTDIALOG_BACKGROUND : "背景",
|
||||
EXPORTDIALOG_BACKGROUND_TRANSPARENT : "透明",
|
||||
EXPORTDIALOG_BACKGROUND_USE_COLOR : "使用场景颜色",
|
||||
// 选择
|
||||
EXPORTDIALOG_SELECTED_ELEMENTS : "导出",
|
||||
EXPORTDIALOG_SELECTED_ALL : "整个场景",
|
||||
EXPORTDIALOG_SELECTED_SELECTED : "仅选中部分",
|
||||
// 导出选项
|
||||
EXPORTDIALOG_EMBED_SCENE : "包含场景数据吗?",
|
||||
EXPORTDIALOG_EMBED_YES : "是",
|
||||
EXPORTDIALOG_EMBED_NO : "否",
|
||||
// PDF 设置
|
||||
EXPORTDIALOG_PDF_SETTINGS : "PDF",
|
||||
EXPORTDIALOG_PAGE_SIZE : "页面大小",
|
||||
EXPORTDIALOG_PAGE_ORIENTATION : "方向",
|
||||
EXPORTDIALOG_ORIENTATION_PORTRAIT : "纵向",
|
||||
EXPORTDIALOG_ORIENTATION_LANDSCAPE : "横向",
|
||||
EXPORTDIALOG_PDF_DPI : "图像质量 [DPI]",
|
||||
EXPORTDIALOG_PDF_FIT_TO_PAGE : "页面适配",
|
||||
EXPORTDIALOG_PDF_FIT_OPTION : "适配页面",
|
||||
EXPORTDIALOG_PDF_FIT_2_OPTION : "适配至 2 页" ,
|
||||
EXPORTDIALOG_PDF_FIT_4_OPTION : "适配至 4 页" ,
|
||||
EXPORTDIALOG_PDF_FIT_6_OPTION : "适配至 6 页" ,
|
||||
EXPORTDIALOG_PDF_FIT_8_OPTION : "适配至 8 页" ,
|
||||
EXPORTDIALOG_PDF_FIT_12_OPTION : "适配至 12 页" ,
|
||||
EXPORTDIALOG_PDF_FIT_16_OPTION : "适配至 16 页" ,
|
||||
EXPORTDIALOG_PDF_SCALE_OPTION : "使用图像缩放(可能跨多页)",
|
||||
EXPORTDIALOG_PDF_PAPER_COLOR : "纸张颜色",
|
||||
EXPORTDIALOG_PDF_PAPER_WHITE : "白色",
|
||||
EXPORTDIALOG_PDF_PAPER_SCENE : "使用场景颜色",
|
||||
EXPORTDIALOG_PDF_PAPER_CUSTOM : "自定义颜色",
|
||||
EXPORTDIALOG_PDF_ALIGNMENT : "页面位置",
|
||||
EXPORTDIALOG_PDF_ALIGN_CENTER : "居中",
|
||||
EXPORTDIALOG_PDF_ALIGN_TOP_LEFT : "左上角",
|
||||
EXPORTDIALOG_PDF_ALIGN_TOP_CENTER : "顶部居中",
|
||||
EXPORTDIALOG_PDF_ALIGN_TOP_RIGHT : "右上角",
|
||||
EXPORTDIALOG_PDF_ALIGN_BOTTOM_LEFT : "左下角",
|
||||
EXPORTDIALOG_PDF_ALIGN_BOTTOM_CENTER : "底部居中",
|
||||
EXPORTDIALOG_PDF_ALIGN_BOTTOM_RIGHT : "右下角",
|
||||
EXPORTDIALOG_PDF_MARGIN : "边距",
|
||||
EXPORTDIALOG_PDF_MARGIN_NONE : "无",
|
||||
EXPORTDIALOG_PDF_MARGIN_TINY : "小",
|
||||
EXPORTDIALOG_PDF_MARGIN_NORMAL : "正常",
|
||||
EXPORTDIALOG_SAVE_PDF_SETTINGS : "保存 PDF 设置",
|
||||
EXPORTDIALOG_SAVE_CONFIRMATION : "PDF 配置已保存为插件默认设置",
|
||||
// 按钮
|
||||
EXPORTDIALOG_PNGTOFILE : "导出 PNG 文件",
|
||||
EXPORTDIALOG_SVGTOFILE : "导出 SVG 文件",
|
||||
EXPORTDIALOG_PNGTOVAULT : "PNG 保存到 Vault",
|
||||
EXPORTDIALOG_SVGTOVAULT : "SVG 保存到 Vault",
|
||||
EXPORTDIALOG_EXCALIDRAW : "Excalidraw",
|
||||
EXPORTDIALOG_PNGTOCLIPBOARD : "PNG 复制到剪贴板",
|
||||
EXPORTDIALOG_SVGTOCLIPBOARD : "SVG 复制到剪贴板",
|
||||
EXPORTDIALOG_PDF : "导出 PDF 文件",
|
||||
EXPORTDIALOG_PDFTOVAULT : "PDF 保存到 Vault",
|
||||
|
||||
EXPORTDIALOG_PDF_PROGRESS_NOTICE : "正在导出页面" ,
|
||||
EXPORTDIALOG_PDF_PROGRESS_IMAGE : "的图像" ,
|
||||
EXPORTDIALOG_PDF_PROGRESS_DONE : "导出完成" ,
|
||||
};
|
||||
@@ -4,11 +4,36 @@ import { Mutable } from "@zsviczian/excalidraw/types/excalidraw/utility-types";
|
||||
import { Notice } from "obsidian";
|
||||
|
||||
import { getEA } from "src/core";
|
||||
import { ExcalidrawAutomate, cloneElement } from "src/shared/ExcalidrawAutomate";
|
||||
import { ExcalidrawAutomate } from "src/shared/ExcalidrawAutomate";
|
||||
import { cloneElement } from "src/utils/excalidrawAutomateUtils";
|
||||
import { ExportSettings } from "src/view/ExcalidrawView";
|
||||
import { nanoid } from "src/constants/constants";
|
||||
import { svgToBase64 } from "../utils/utils";
|
||||
|
||||
/**
|
||||
* Creates a masked image from an Excalidraw scene.
|
||||
*
|
||||
* The scene must contain:
|
||||
* - One element.type="frame" element that defines the crop area
|
||||
* - One or more element.type="image" elements
|
||||
* - Zero or more non-image shape elements (rectangles, ellipses etc) that define the mask
|
||||
*
|
||||
* The class splits the scene into two parts:
|
||||
* 1. Images (managed in imageEA)
|
||||
* 2. Mask shapes (managed in maskEA)
|
||||
*
|
||||
* A transparent rectangle matching the combined bounding box is added to both
|
||||
* imageEA and maskEA to ensure consistent sizing between image and mask.
|
||||
*
|
||||
* For performance, if there is only one image, it is not rotated, and
|
||||
* its size matches the bounding box,
|
||||
* the image data is used directly from cache rather than regenerating.
|
||||
*
|
||||
* @example
|
||||
* const cropper = new CropImage(elements, files);
|
||||
* const pngBlob = await cropper.getCroppedPNG();
|
||||
* cropper.destroy();
|
||||
*/
|
||||
export class CropImage {
|
||||
private imageEA: ExcalidrawAutomate;
|
||||
private maskEA: ExcalidrawAutomate;
|
||||
@@ -106,10 +131,15 @@ export class CropImage {
|
||||
withTheme: false,
|
||||
isMask: false,
|
||||
}
|
||||
const isRotated = this.imageEA.getElements().some(el=>el.type === "image" && el.angle !== 0);
|
||||
const images = Object.values(this.imageEA.imagesDict);
|
||||
if(!isRotated && (images.length === 1)) {
|
||||
return images[0].dataURL;
|
||||
const images = this.imageEA.getElements().filter(el=>el.type === "image" && el.isDeleted === false);
|
||||
const isRotated = images.some(el=>el.angle !== 0);
|
||||
const imageDataURLs = Object.values(this.imageEA.imagesDict);
|
||||
if(!isRotated && images.length === 1 && imageDataURLs.length === 1) {
|
||||
const { width, height } = this.bbox;
|
||||
if(images[0].width === width && images[0].height === height) {
|
||||
//get image from the cache if mask is not bigger than the image, and if there is a single image element
|
||||
return imageDataURLs[0].dataURL;
|
||||
}
|
||||
}
|
||||
return await this.imageEA.createPNGBase64(null,1,exportSettings,null,null,0);
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
164
src/shared/Dialogs/PDFExportSettingsComponent.ts
Normal file
164
src/shared/Dialogs/PDFExportSettingsComponent.ts
Normal file
@@ -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();
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { App, FuzzySuggestModal, Notice, TFile } from "obsidian";
|
||||
import { App, FuzzySuggestModal, Notice } from "obsidian";
|
||||
import { t } from "../../lang/helpers";
|
||||
import ExcalidrawView from "src/view/ExcalidrawView";
|
||||
import { getEA } from "src/core";
|
||||
|
||||
@@ -157,6 +157,15 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
|
||||
desc: "Set ea.style.roundness. 0: is the legacy value, 3: is the current default value, null is sharp",
|
||||
after: "",
|
||||
},
|
||||
{
|
||||
field: "addAppendUpdateCustomData",
|
||||
code: "addAppendUpdateCustomData(id: string, newData: Partial<Record<string, unknown>>)",
|
||||
desc: "Add, modify keys in element customData and preserve existing keys.\n" +
|
||||
"Creates customData={} if it does not exist.\n" +
|
||||
"Takes the element ID for an element in the elementsDict and the new data to add or modify.\n" +
|
||||
"To delete keys set key value in newData to undefined. so {keyToBeDeleted:undefined} will be deleted.",
|
||||
after: "",
|
||||
},
|
||||
{
|
||||
field: "addToGroup",
|
||||
code: "addToGroup(objectIds: []): string;",
|
||||
@@ -215,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>;",
|
||||
@@ -491,6 +558,21 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
|
||||
desc: "If set Excalidraw will call this function onDrop events.\nA return of true will stop the default onDrop processing in Excalidraw.\n\ndraggable is the Obsidian draggable object\nfiles is the array of dropped files\nexcalidrawFile is the file receiving the drop event\nview is the excalidraw view receiving the drop.\npointerPosition is the pointer position on canvas at the time of drop.",
|
||||
after: "",
|
||||
},
|
||||
{
|
||||
field: "onImageFilePathHook",
|
||||
code: `onImageFilePathHook: (data: {currentImageName: string; drawingFilePath: string;}): string;`,
|
||||
desc: "If set, this callback is triggered when an image is being saved in Excalidraw.\n"
|
||||
+ "You can use this callback to customize the naming and path of pasted images to avoid\n"
|
||||
+ 'default names like "Pasted image 123147170.png" being saved in the attachments folder,\n'
|
||||
+ "and instead use more meaningful names based on the Excalidraw file or other criteria,\n"
|
||||
+ "plus save the image in a different folder.\n\n"
|
||||
+ "If the function returns null or undefined, the normal Excalidraw operation will continue\n"
|
||||
+ "with the excalidraw generated name and default path.\n"
|
||||
+ "If a filepath is returned, that will be used. Include the full Vault filepath and filename\n"
|
||||
+ "with the file extension.\n"
|
||||
+ "The currentImageName is the name of the image generated by excalidraw or provided during paste.",
|
||||
after: "",
|
||||
},
|
||||
{
|
||||
field: "mostRecentMarkdownSVG",
|
||||
code: "mostRecentMarkdownSVG: SVGSVGElement;",
|
||||
|
||||
@@ -13,13 +13,13 @@ import {
|
||||
FRONTMATTER_KEYS,
|
||||
getCSSFontDefinition,
|
||||
} from "../constants/constants";
|
||||
import { createSVG } from "./ExcalidrawAutomate";
|
||||
import { createSVG } from "src/utils/excalidrawAutomateUtils";
|
||||
import { ExcalidrawData, getTransclusion } from "./ExcalidrawData";
|
||||
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,
|
||||
@@ -73,7 +73,8 @@ type ImgData = {
|
||||
dataURL: DataURL;
|
||||
created: number;
|
||||
hasSVGwithBitmap: boolean;
|
||||
size: { height: number; width: number };
|
||||
size: Size;
|
||||
pdfPageViewProps?: PDFPageViewProps;
|
||||
};
|
||||
|
||||
export declare type MimeType = ValueOf<typeof IMAGE_MIME_TYPES> | "application/octet-stream";
|
||||
@@ -82,8 +83,17 @@ export type FileData = BinaryFileData & {
|
||||
size: Size;
|
||||
hasSVGwithBitmap: boolean;
|
||||
shouldScale: boolean; //true if image should maintain its area, false if image should display at 100% its size
|
||||
pdfPageViewProps?: PDFPageViewProps;
|
||||
};
|
||||
|
||||
export type PDFPageViewProps = {
|
||||
left: number;
|
||||
bottom: number;
|
||||
right: number;
|
||||
top: number;
|
||||
rotate?: number; //may be undefined in legacy files
|
||||
}
|
||||
|
||||
export type Size = {
|
||||
height: number;
|
||||
width: number;
|
||||
@@ -177,6 +187,7 @@ export class EmbeddedFile {
|
||||
public isLocalLink: boolean = false;
|
||||
public hyperlink:DataURL;
|
||||
public colorMap: ColorMap | null = null;
|
||||
public pdfPageViewProps: PDFPageViewProps;
|
||||
|
||||
constructor(plugin: ExcalidrawPlugin, hostPath: string, imgPath: string, colorMapJSON?: string) {
|
||||
this.plugin = plugin;
|
||||
@@ -252,12 +263,14 @@ export class EmbeddedFile {
|
||||
return this.mtime !== this.file.stat.mtime;
|
||||
}
|
||||
|
||||
public setImage(
|
||||
imgBase64: string,
|
||||
mimeType: MimeType,
|
||||
size: Size,
|
||||
isDark: boolean,
|
||||
isSVGwithBitmap: boolean,
|
||||
public setImage({ imgBase64, mimeType, size, isDark, isSVGwithBitmap, pdfPageViewProps } : {
|
||||
imgBase64: string;
|
||||
mimeType: MimeType;
|
||||
size: Size;
|
||||
isDark: boolean;
|
||||
isSVGwithBitmap: boolean;
|
||||
pdfPageViewProps?: PDFPageViewProps;
|
||||
}
|
||||
) {
|
||||
if (!this.file && !this.isHyperLink && !this.isLocalLink) {
|
||||
return;
|
||||
@@ -266,6 +279,7 @@ export class EmbeddedFile {
|
||||
this.imgInverted = this.img = "";
|
||||
}
|
||||
this.mtime = this.isHyperLink || this.isLocalLink ? 0 : this.file.stat.mtime;
|
||||
this.pdfPageViewProps = pdfPageViewProps;
|
||||
this.size = size;
|
||||
this.mimeType = mimeType;
|
||||
switch (isDark && isSVGwithBitmap) {
|
||||
@@ -345,6 +359,7 @@ export class EmbeddedFilesLoader {
|
||||
created: number;
|
||||
hasSVGwithBitmap: boolean;
|
||||
size: { height: number; width: number };
|
||||
pdfPageViewProps?: PDFPageViewProps;
|
||||
}> {
|
||||
const result = await this._getObsidianImage(inFile, depth);
|
||||
this.emptyPDFDocsMap();
|
||||
@@ -552,9 +567,9 @@ export class EmbeddedFilesLoader {
|
||||
|
||||
const excalidrawSVG = isExcalidrawFile ? dURL : null;
|
||||
|
||||
const [pdfDataURL, pdfSize] = isPDF
|
||||
const [pdfDataURL, pdfSize, pdfPageViewProps] = isPDF
|
||||
? await this.pdfToDataURL(file,linkParts)
|
||||
: [null, null];
|
||||
: [null, null, null];
|
||||
|
||||
let mimeType: MimeType = isPDF
|
||||
? "image/png"
|
||||
@@ -600,6 +615,7 @@ export class EmbeddedFilesLoader {
|
||||
created: isHyperLink || isLocalLink ? 0 : file.stat.mtime,
|
||||
hasSVGwithBitmap,
|
||||
size,
|
||||
pdfPageViewProps,
|
||||
};
|
||||
} catch(e) {
|
||||
return null;
|
||||
@@ -634,7 +650,7 @@ export class EmbeddedFilesLoader {
|
||||
files.push([]);
|
||||
let batch = 0;
|
||||
|
||||
function* loadIterator():Generator<Promise<void>> {
|
||||
function* loadIterator(this: EmbeddedFilesLoader):Generator<Promise<void>> {
|
||||
while (!(entry = entries.next()).done) {
|
||||
if(fileIDWhiteList && !fileIDWhiteList.has(entry.value[0])) continue;
|
||||
const embeddedFile: EmbeddedFile = entry.value[1];
|
||||
@@ -654,20 +670,22 @@ export class EmbeddedFilesLoader {
|
||||
created: data.created,
|
||||
size: data.size,
|
||||
hasSVGwithBitmap: data.hasSVGwithBitmap,
|
||||
shouldScale: embeddedFile.shouldScale()
|
||||
shouldScale: embeddedFile.shouldScale(),
|
||||
pdfPageViewProps: data.pdfPageViewProps,
|
||||
};
|
||||
files[batch].push(fileData);
|
||||
}
|
||||
} else if (embeddedFile.isSVGwithBitmap && (depth !== 0 || isThemeChange)) {
|
||||
//this will reload the image in light/dark mode when switching themes
|
||||
const fileData = {
|
||||
const fileData: FileData = {
|
||||
mimeType: embeddedFile.mimeType,
|
||||
id: id,
|
||||
dataURL: embeddedFile.getImage(this.isDark) as DataURL,
|
||||
created: embeddedFile.mtime,
|
||||
size: embeddedFile.size,
|
||||
hasSVGwithBitmap: embeddedFile.isSVGwithBitmap,
|
||||
shouldScale: embeddedFile.shouldScale()
|
||||
shouldScale: embeddedFile.shouldScale(),
|
||||
pdfPageViewProps: embeddedFile.pdfPageViewProps,
|
||||
};
|
||||
files[batch].push(fileData);
|
||||
}
|
||||
@@ -803,7 +821,7 @@ export class EmbeddedFilesLoader {
|
||||
private async pdfToDataURL(
|
||||
file: TFile,
|
||||
linkParts: LinkParts,
|
||||
): Promise<[DataURL,{width:number, height:number}]> {
|
||||
): Promise<[DataURL,Size, PDFPageViewProps]> {
|
||||
try {
|
||||
let width = 0, height = 0;
|
||||
const pdfDoc = this.pdfDocsMap.get(file.path) ?? await getPDFDoc(file);
|
||||
@@ -814,6 +832,7 @@ export class EmbeddedFilesLoader {
|
||||
const scale = this.plugin.settings.pdfScale;
|
||||
const cropRect = linkParts.ref.split("rect=")[1]?.split(",").map(x=>parseInt(x));
|
||||
const validRect = cropRect && cropRect.length === 4 && cropRect.every(x=>!isNaN(x));
|
||||
let viewProps: PDFPageViewProps;
|
||||
|
||||
// Render the page
|
||||
const renderPage = async (num:number) => {
|
||||
@@ -824,8 +843,8 @@ export class EmbeddedFilesLoader {
|
||||
const page = await pdfDoc.getPage(num);
|
||||
// Set scale
|
||||
const viewport = page.getViewport({ scale });
|
||||
height = canvas.height = viewport.height;
|
||||
width = canvas.width = viewport.width;
|
||||
height = canvas.height = Math.round(viewport.height);
|
||||
width = canvas.width = Math.round(viewport.width);
|
||||
|
||||
const renderCtx = {
|
||||
canvasContext: ctx,
|
||||
@@ -846,20 +865,60 @@ export class EmbeddedFilesLoader {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if(validRect) {
|
||||
const [left, bottom, _, top] = page.view;
|
||||
|
||||
const pageHeight = top - bottom;
|
||||
width = (cropRect[2] - cropRect[0]) * scale;
|
||||
height = (cropRect[3] - cropRect[1]) * scale;
|
||||
const [left, bottom, right, top] = page.view;
|
||||
viewProps = {left, bottom, right, top};
|
||||
viewProps.rotate = page.rotate;
|
||||
|
||||
const crop = validRect ? {
|
||||
left: (cropRect[0] - left) * scale,
|
||||
top: (bottom + pageHeight - cropRect[3]) * scale,
|
||||
width,
|
||||
height,
|
||||
} : undefined;
|
||||
if(crop) {
|
||||
if(validRect) {
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -868,19 +927,19 @@ export class EmbeddedFilesLoader {
|
||||
|
||||
const canvas = await renderPage(pageNum);
|
||||
if(canvas) {
|
||||
const result: [DataURL,{width:number, height:number}] = [`data:image/png;base64,${await new Promise((resolve, reject) => {
|
||||
const result: [DataURL,Size, PDFPageViewProps] = [`data:image/png;base64,${await new Promise((resolve, reject) => {
|
||||
canvas.toBlob(async (blob) => {
|
||||
const dataURL = await blobToBase64(blob);
|
||||
resolve(dataURL);
|
||||
});
|
||||
})}` as DataURL, {width, height}];
|
||||
})}` as DataURL, {width, height}, viewProps];
|
||||
canvas.width = 0; //free memory iOS bug
|
||||
canvas.height = 0;
|
||||
return result;
|
||||
}
|
||||
} catch(e) {
|
||||
console.log(e);
|
||||
return [null,null];
|
||||
return [null, null, null];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -20,7 +20,7 @@ import {
|
||||
loadSceneFonts,
|
||||
} from "../constants/constants";
|
||||
import ExcalidrawPlugin from "../core/main";
|
||||
import { TextMode } from "../view/ExcalidrawView";
|
||||
import ExcalidrawView, { TextMode } from "../view/ExcalidrawView";
|
||||
import {
|
||||
addAppendUpdateCustomData,
|
||||
compress,
|
||||
@@ -52,10 +52,11 @@ import { getMermaidImageElements, getMermaidText, shouldRenderMermaid } from "..
|
||||
import { DEBUGGING, debug } from "../utils/debugHelper";
|
||||
import { Mutable } from "@zsviczian/excalidraw/types/excalidraw/utility-types";
|
||||
import { updateElementIdsInScene } from "../utils/excalidrawSceneUtils";
|
||||
import { getNewUniqueFilepath } from "../utils/fileUtils";
|
||||
import { checkAndCreateFolder, getNewUniqueFilepath, splitFolderAndFilename } from "../utils/fileUtils";
|
||||
import { t } from "../lang/helpers";
|
||||
import { displayFontMessage } from "../utils/excalidrawViewUtils";
|
||||
import { getPDFRect } from "../utils/PDFUtils";
|
||||
import { create } from "domain";
|
||||
|
||||
type SceneDataWithFiles = SceneData & { files: BinaryFiles };
|
||||
|
||||
@@ -480,7 +481,7 @@ export class ExcalidrawData {
|
||||
selectedElementIds: {[key:string]:boolean} = {}; //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/609
|
||||
|
||||
constructor(
|
||||
private plugin: ExcalidrawPlugin,
|
||||
private plugin: ExcalidrawPlugin, private view?: ExcalidrawView,
|
||||
) {
|
||||
this.app = this.plugin.app;
|
||||
this.files = new Map<FileId, EmbeddedFile>();
|
||||
@@ -649,7 +650,6 @@ export class ExcalidrawData {
|
||||
containers.forEach((container: any) => {
|
||||
if(ellipseAndRhombusContainerWrapping && !container.customData?.legacyTextWrap) {
|
||||
addAppendUpdateCustomData(container, {legacyTextWrap: true});
|
||||
//container.customData = {...container.customData, legacyTextWrap: true};
|
||||
}
|
||||
const filteredBoundElements = container.boundElements.filter(
|
||||
(boundEl: any) => elements.some((el: any) => el.id === boundEl.id),
|
||||
@@ -1547,13 +1547,24 @@ export class ExcalidrawData {
|
||||
}
|
||||
}
|
||||
|
||||
const x = await getAttachmentsFolderAndFilePath(this.app, this.file.path, fname);
|
||||
const filepath = getNewUniqueFilepath(this.app.vault,fname,x.folder);
|
||||
let hookFilepath:string;
|
||||
const ea = this.view?.getHookServer();
|
||||
if(ea?.onImageFilePathHook) {
|
||||
hookFilepath = ea.onImageFilePathHook({
|
||||
currentImageName: fname,
|
||||
drawingFilePath: this.view?.file?.path,
|
||||
})
|
||||
}
|
||||
|
||||
/*
|
||||
const filepath = (
|
||||
await getAttachmentsFolderAndFilePath(this.app, this.file.path, fname)
|
||||
).filepath;*/
|
||||
let filepath:string;
|
||||
if(hookFilepath) {
|
||||
const {folderpath, filename} = splitFolderAndFilename(hookFilepath);
|
||||
await checkAndCreateFolder(folderpath);
|
||||
filepath = getNewUniqueFilepath(this.app.vault,filename,folderpath);
|
||||
} else {
|
||||
const x = await getAttachmentsFolderAndFilePath(this.app, this.file.path, fname);
|
||||
filepath = getNewUniqueFilepath(this.app.vault,fname,x.folder);
|
||||
}
|
||||
|
||||
const arrayBuffer = await getBinaryFileFromDataURL(dataURL);
|
||||
if(!arrayBuffer) return null;
|
||||
@@ -1569,13 +1580,13 @@ export class ExcalidrawData {
|
||||
filepath,
|
||||
);
|
||||
|
||||
embeddedFile.setImage(
|
||||
dataURL,
|
||||
embeddedFile.setImage({
|
||||
imgBase64: dataURL,
|
||||
mimeType,
|
||||
{ height: 0, width: 0 },
|
||||
scene.appState?.theme === "dark",
|
||||
mimeType === "image/svg+xml", //this treat all SVGs as if they had embedded images REF:addIMAGE
|
||||
);
|
||||
size: { height: 0, width: 0 },
|
||||
isDark: scene.appState?.theme === "dark",
|
||||
isSVGwithBitmap: mimeType === "image/svg+xml", //this treat all SVGs as if they had embedded images REF:addIMAGE
|
||||
});
|
||||
this.setFile(key as FileId, embeddedFile);
|
||||
return file;
|
||||
}
|
||||
@@ -1593,7 +1604,9 @@ export class ExcalidrawData {
|
||||
const pageRef = ef.linkParts.original.split("#")?.[1];
|
||||
if(!pageRef || !pageRef.startsWith("page=") || pageRef.includes("rect")) return;
|
||||
const restOfLink = el.link ? el.link.match(/&rect=\d*,\d*,\d*,\d*(.*)/)?.[1] : "";
|
||||
const link = ef.linkParts.original + getPDFRect(el.crop, pdfScale) + (restOfLink ? restOfLink : "]]");
|
||||
const link = ef.linkParts.original +
|
||||
getPDFRect({elCrop: el.crop, scale: pdfScale, customData: el.customData}) +
|
||||
(restOfLink ? restOfLink : "]]");
|
||||
el.link = `[[${link}`;
|
||||
this.elementLinks.set(el.id, el.link);
|
||||
dirty = true;
|
||||
@@ -1992,7 +2005,7 @@ export class ExcalidrawData {
|
||||
isLocalLink: data.isLocalLink,
|
||||
path: data.hyperlink,
|
||||
blockrefData: null,
|
||||
hasSVGwithBitmap: data.isSVGwithBitmap
|
||||
hasSVGwithBitmap: data.isSVGwithBitmap,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { DataURL } from "@zsviczian/excalidraw/types/excalidraw/types";
|
||||
import { TFile } from "obsidian";
|
||||
import { FileId } from "src/core";
|
||||
import { ColorMap, MimeType } from "src/shared/EmbeddedFileLoader";
|
||||
import { ColorMap, MimeType, PDFPageViewProps, Size } from "src/shared/EmbeddedFileLoader";
|
||||
|
||||
export type SVGColorInfo = Map<string, {
|
||||
mappedTo: string;
|
||||
@@ -19,8 +19,9 @@ export type ImageInfo = {
|
||||
file?:string | TFile,
|
||||
hasSVGwithBitmap: boolean,
|
||||
latex?: string,
|
||||
size?: {height: number, width: number},
|
||||
size?: Size,
|
||||
colorMap?: ColorMap,
|
||||
pdfPageViewProps?: PDFPageViewProps,
|
||||
}
|
||||
|
||||
export interface AddImageOptions {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Notice, RequestUrlResponse, requestUrl } from "obsidian";
|
||||
import { Notice, RequestUrlResponse } from "obsidian";
|
||||
import ExcalidrawPlugin from "src/core/main";
|
||||
|
||||
type MessageContent =
|
||||
|
||||
@@ -1,37 +1,128 @@
|
||||
//for future use, not used currently
|
||||
|
||||
import { ImageCrop } from "@zsviczian/excalidraw/types/excalidraw/element/types";
|
||||
import { PDFPageViewProps } from "src/shared/EmbeddedFileLoader";
|
||||
|
||||
export function getPDFCropRect (props: {
|
||||
scale: number,
|
||||
link: string,
|
||||
naturalHeight: number,
|
||||
naturalWidth: number,
|
||||
pdfPageViewProps: PDFPageViewProps,
|
||||
}) : ImageCrop | null {
|
||||
const rectVal = props.link.match(/&rect=(\d*),(\d*),(\d*),(\d*)/);
|
||||
if (!rectVal || rectVal.length !== 5) {
|
||||
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 * props.scale,
|
||||
y: (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,
|
||||
}
|
||||
}
|
||||
|
||||
export function getPDFRect(elCrop: ImageCrop, scale: number): string {
|
||||
const R0 = elCrop.x / scale;
|
||||
const R2 = elCrop.width / scale + R0;
|
||||
const R3 = (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)}`;
|
||||
}
|
||||
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 };
|
||||
|
||||
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)}`;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import { ColorMaster } from "@zsviczian/colormaster";
|
||||
import { ExcalidrawAutomate } from "src/shared/ExcalidrawAutomate";
|
||||
import ExcalidrawView from "src/view/ExcalidrawView";
|
||||
import { DynamicStyle } from "src/types/types";
|
||||
import { cloneElement } from "src/shared/ExcalidrawAutomate";
|
||||
import { cloneElement } from "./excalidrawAutomateUtils";
|
||||
import { ExcalidrawFrameElement } from "@zsviczian/excalidraw/types/excalidraw/element/types";
|
||||
import { addAppendUpdateCustomData } from "./utils";
|
||||
import { mutateElement } from "src/constants/constants";
|
||||
|
||||
@@ -1,8 +1,49 @@
|
||||
import { ExcalidrawElement } from "@zsviczian/excalidraw/types/excalidraw/element/types";
|
||||
import { ExcalidrawAutomate } from "src/shared/ExcalidrawAutomate";
|
||||
import { errorlog } from "./utils";
|
||||
import { ColorMap } from "src/shared/EmbeddedFileLoader";
|
||||
import { DataURL } from "@zsviczian/excalidraw/types/excalidraw/types";
|
||||
import ExcalidrawPlugin from "src/core/main";
|
||||
import {
|
||||
ExcalidrawElement,
|
||||
ExcalidrawImageElement,
|
||||
FileId,
|
||||
} from "@zsviczian/excalidraw/types/excalidraw/element/types";
|
||||
import { normalizePath, TFile } from "obsidian";
|
||||
|
||||
import ExcalidrawView, { ExportSettings, getTextMode } from "src/view/ExcalidrawView";
|
||||
import {
|
||||
GITHUB_RELEASES,
|
||||
getCommonBoundingBox,
|
||||
restore,
|
||||
REG_LINKINDEX_INVALIDCHARS,
|
||||
THEME_FILTER,
|
||||
EXCALIDRAW_PLUGIN,
|
||||
getFontFamilyString,
|
||||
getLineHeight,
|
||||
measureText,
|
||||
} from "src/constants/constants";
|
||||
import {
|
||||
//debug,
|
||||
errorlog,
|
||||
getEmbeddedFilenameParts,
|
||||
getLinkParts,
|
||||
getPNG,
|
||||
getSVG,
|
||||
isVersionNewerThanOther,
|
||||
scaleLoadedImage,
|
||||
} from "src/utils/utils";
|
||||
import { GenericInputPrompt, NewFileActions } from "src/shared/Dialogs/Prompt";
|
||||
import { t } from "src/lang/helpers";
|
||||
import { Mutable } from "@zsviczian/excalidraw/types/excalidraw/utility-types";
|
||||
import {
|
||||
postOpenAI as _postOpenAI,
|
||||
extractCodeBlocks as _extractCodeBlocks,
|
||||
} from "../utils/AIUtils";
|
||||
import { ColorMap, EmbeddedFilesLoader, FileData } from "src/shared/EmbeddedFileLoader";
|
||||
import { SVGColorInfo } from "src/types/excalidrawAutomateTypes";
|
||||
import { ExcalidrawData, getExcalidrawMarkdownHeaderSection, REGEX_LINK } from "src/shared/ExcalidrawData";
|
||||
import { getFrameBasedOnFrameNameOrId } from "./excalidrawViewUtils";
|
||||
import { ScriptEngine } from "src/shared/Scripts";
|
||||
|
||||
declare const PLUGIN_VERSION:string;
|
||||
|
||||
export function isSVGColorInfo(obj: ColorMap | SVGColorInfo): boolean {
|
||||
return (
|
||||
@@ -112,3 +153,668 @@ export function isColorStringTransparent(color: string): boolean {
|
||||
|
||||
return rgbaHslaTransparentRegex.test(color) || hexTransparentRegex.test(color);
|
||||
}
|
||||
|
||||
export function initExcalidrawAutomate(
|
||||
plugin: ExcalidrawPlugin,
|
||||
): ExcalidrawAutomate {
|
||||
const ea = new ExcalidrawAutomate(plugin);
|
||||
//@ts-ignore
|
||||
window.ExcalidrawAutomate = ea;
|
||||
return ea;
|
||||
}
|
||||
|
||||
export function normalizeLinePoints(
|
||||
points: [x: number, y: number][],
|
||||
//box: { x: number; y: number; w: number; h: number },
|
||||
): number[][] {
|
||||
const p = [];
|
||||
const [x, y] = points[0];
|
||||
for (let i = 0; i < points.length; i++) {
|
||||
p.push([points[i][0] - x, points[i][1] - y]);
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
export function getLineBox(
|
||||
points: [x: number, y: number][]
|
||||
):{x:number, y:number, w: number, h:number} {
|
||||
const [x1, y1, x2, y2] = estimateLineBound(points);
|
||||
return {
|
||||
x: x1,
|
||||
y: y1,
|
||||
w: x2 - x1, //Math.abs(points[points.length-1][0]-points[0][0]),
|
||||
h: y2 - y1, //Math.abs(points[points.length-1][1]-points[0][1])
|
||||
};
|
||||
}
|
||||
|
||||
export function getFontFamily(id: number):string {
|
||||
return getFontFamilyString({fontFamily:id})
|
||||
}
|
||||
|
||||
export function _measureText(
|
||||
newText: string,
|
||||
fontSize: number,
|
||||
fontFamily: number,
|
||||
lineHeight: number,
|
||||
): {w: number, h:number} {
|
||||
//following odd error with mindmap on iPad while synchornizing with desktop.
|
||||
if (!fontSize) {
|
||||
fontSize = 20;
|
||||
}
|
||||
if (!fontFamily) {
|
||||
fontFamily = 1;
|
||||
lineHeight = getLineHeight(fontFamily);
|
||||
}
|
||||
const metrics = measureText(
|
||||
newText,
|
||||
`${fontSize.toString()}px ${getFontFamily(fontFamily)}` as any,
|
||||
lineHeight
|
||||
);
|
||||
return { w: metrics.width, h: metrics.height };
|
||||
}
|
||||
|
||||
export async function getTemplate(
|
||||
plugin: ExcalidrawPlugin,
|
||||
fileWithPath: string,
|
||||
loadFiles: boolean = false,
|
||||
loader: EmbeddedFilesLoader,
|
||||
depth: number,
|
||||
convertMarkdownLinksToObsidianURLs: boolean = false,
|
||||
): Promise<{
|
||||
elements: any;
|
||||
appState: any;
|
||||
frontmatter: string;
|
||||
files: any;
|
||||
hasSVGwithBitmap: boolean;
|
||||
plaintext: string; //markdown data above Excalidraw data and below YAML frontmatter
|
||||
}> {
|
||||
const app = plugin.app;
|
||||
const vault = app.vault;
|
||||
const filenameParts = getEmbeddedFilenameParts(fileWithPath);
|
||||
const templatePath = normalizePath(filenameParts.filepath);
|
||||
const file = app.metadataCache.getFirstLinkpathDest(templatePath, "");
|
||||
let hasSVGwithBitmap = false;
|
||||
if (file && file instanceof TFile) {
|
||||
const data = (await vault.read(file))
|
||||
.replaceAll("\r\n", "\n")
|
||||
.replaceAll("\r", "\n");
|
||||
const excalidrawData: ExcalidrawData = new ExcalidrawData(plugin);
|
||||
|
||||
if (file.extension === "excalidraw") {
|
||||
await excalidrawData.loadLegacyData(data, file);
|
||||
return {
|
||||
elements: convertMarkdownLinksToObsidianURLs
|
||||
? updateElementLinksToObsidianLinks({
|
||||
elements: excalidrawData.scene.elements,
|
||||
hostFile: file,
|
||||
}) : excalidrawData.scene.elements,
|
||||
appState: excalidrawData.scene.appState,
|
||||
frontmatter: "",
|
||||
files: excalidrawData.scene.files,
|
||||
hasSVGwithBitmap,
|
||||
plaintext: "",
|
||||
};
|
||||
}
|
||||
|
||||
const textMode = getTextMode(data);
|
||||
await excalidrawData.loadData(
|
||||
data,
|
||||
file,
|
||||
textMode,
|
||||
);
|
||||
|
||||
let trimLocation = data.search(/^##? Text Elements$/m);
|
||||
if (trimLocation == -1) {
|
||||
trimLocation = data.search(/##? Drawing\n/);
|
||||
}
|
||||
|
||||
let scene = excalidrawData.scene;
|
||||
|
||||
let groupElements:ExcalidrawElement[] = scene.elements;
|
||||
if(filenameParts.hasGroupref) {
|
||||
const el = filenameParts.hasSectionref
|
||||
? getTextElementsMatchingQuery(scene.elements,["# "+filenameParts.sectionref],true)
|
||||
: scene.elements.filter((el: ExcalidrawElement)=>el.id===filenameParts.blockref);
|
||||
if(el.length > 0) {
|
||||
groupElements = plugin.ea.getElementsInTheSameGroupWithElement(el[0],scene.elements,true)
|
||||
}
|
||||
}
|
||||
if(filenameParts.hasFrameref || filenameParts.hasClippedFrameref) {
|
||||
const el = getFrameBasedOnFrameNameOrId(filenameParts.blockref,scene.elements);
|
||||
|
||||
if(el) {
|
||||
groupElements = plugin.ea.getElementsInFrame(el,scene.elements, filenameParts.hasClippedFrameref);
|
||||
}
|
||||
}
|
||||
|
||||
if(filenameParts.hasTaskbone) {
|
||||
groupElements = groupElements.filter( el =>
|
||||
el.type==="freedraw" ||
|
||||
( el.type==="image" &&
|
||||
!plugin.isExcalidrawFile(excalidrawData.getFile(el.fileId)?.file)
|
||||
));
|
||||
}
|
||||
|
||||
let fileIDWhiteList:Set<FileId>;
|
||||
|
||||
if(groupElements.length < scene.elements.length) {
|
||||
fileIDWhiteList = new Set<FileId>();
|
||||
groupElements.filter(el=>el.type==="image").forEach((el:ExcalidrawImageElement)=>fileIDWhiteList.add(el.fileId));
|
||||
}
|
||||
|
||||
if (loadFiles) {
|
||||
//debug({where:"getTemplate",template:file.name,loader:loader.uid});
|
||||
await loader.loadSceneFiles({
|
||||
excalidrawData,
|
||||
addFiles: (fileArray: FileData[]) => {
|
||||
//, isDark: boolean) => {
|
||||
if (!fileArray || fileArray.length === 0) {
|
||||
return;
|
||||
}
|
||||
for (const f of fileArray) {
|
||||
if (f.hasSVGwithBitmap) {
|
||||
hasSVGwithBitmap = true;
|
||||
}
|
||||
excalidrawData.scene.files[f.id] = {
|
||||
mimeType: f.mimeType,
|
||||
id: f.id,
|
||||
dataURL: f.dataURL,
|
||||
created: f.created,
|
||||
};
|
||||
}
|
||||
scene = scaleLoadedImage(excalidrawData.scene, fileArray).scene;
|
||||
},
|
||||
depth,
|
||||
fileIDWhiteList
|
||||
});
|
||||
}
|
||||
|
||||
excalidrawData.destroy();
|
||||
const filehead = getExcalidrawMarkdownHeaderSection(data); // data.substring(0, trimLocation);
|
||||
let files:any = {};
|
||||
const sceneFilesSize = Object.values(scene.files).length;
|
||||
if (sceneFilesSize > 0) {
|
||||
if(fileIDWhiteList && (sceneFilesSize > fileIDWhiteList.size)) {
|
||||
Object.values(scene.files).filter((f: any) => fileIDWhiteList.has(f.id)).forEach((f: any) => {
|
||||
files[f.id] = f;
|
||||
});
|
||||
} else {
|
||||
files = scene.files;
|
||||
}
|
||||
}
|
||||
|
||||
const frontmatter = filehead.match(/^---\n.*\n---\n/ms)?.[0] ?? filehead;
|
||||
return {
|
||||
elements: convertMarkdownLinksToObsidianURLs
|
||||
? updateElementLinksToObsidianLinks({
|
||||
elements: groupElements,
|
||||
hostFile: file,
|
||||
}) : groupElements,
|
||||
appState: scene.appState,
|
||||
frontmatter,
|
||||
plaintext: frontmatter !== filehead
|
||||
? (filehead.split(/^---\n.*\n---\n/ms)?.[1] ?? "")
|
||||
: "",
|
||||
files,
|
||||
hasSVGwithBitmap,
|
||||
};
|
||||
}
|
||||
return {
|
||||
elements: [],
|
||||
appState: {},
|
||||
frontmatter: null,
|
||||
files: [],
|
||||
hasSVGwithBitmap,
|
||||
plaintext: "",
|
||||
};
|
||||
}
|
||||
|
||||
export const generatePlaceholderDataURL = (width: number, height: number): DataURL => {
|
||||
const svgString = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}"><rect width="100%" height="100%" fill="#E7E7E7" /><text x="${width / 2}" y="${height / 2}" dominant-baseline="middle" text-anchor="middle" font-family="Arial" font-size="${Math.min(width, height) / 5}" fill="#888">Placeholder</text></svg>`;
|
||||
return `data:image/svg+xml;base64,${btoa(svgString)}` as DataURL;
|
||||
};
|
||||
|
||||
export async function createPNG(
|
||||
templatePath: string = undefined,
|
||||
scale: number = 1,
|
||||
exportSettings: ExportSettings,
|
||||
loader: EmbeddedFilesLoader,
|
||||
forceTheme: string = undefined,
|
||||
canvasTheme: string = undefined,
|
||||
canvasBackgroundColor: string = undefined,
|
||||
automateElements: ExcalidrawElement[] = [],
|
||||
plugin: ExcalidrawPlugin,
|
||||
depth: number,
|
||||
padding?: number,
|
||||
imagesDict?: any,
|
||||
): Promise<Blob> {
|
||||
if (!loader) {
|
||||
loader = new EmbeddedFilesLoader(plugin);
|
||||
}
|
||||
padding = padding ?? plugin.settings.exportPaddingSVG;
|
||||
const template = templatePath
|
||||
? await getTemplate(plugin, templatePath, true, loader, depth)
|
||||
: null;
|
||||
let elements = template?.elements ?? [];
|
||||
elements = elements.concat(automateElements);
|
||||
const files = imagesDict ?? {};
|
||||
if(template?.files) {
|
||||
Object.values(template.files).forEach((f:any)=>{
|
||||
if(!f.dataURL.startsWith("http")) {
|
||||
files[f.id]=f;
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
return await getPNG(
|
||||
{
|
||||
type: "excalidraw",
|
||||
version: 2,
|
||||
source: GITHUB_RELEASES+PLUGIN_VERSION,
|
||||
elements,
|
||||
appState: {
|
||||
theme: forceTheme ?? template?.appState?.theme ?? canvasTheme,
|
||||
viewBackgroundColor:
|
||||
template?.appState?.viewBackgroundColor ?? canvasBackgroundColor,
|
||||
...template?.appState?.frameRendering ? {frameRendering: template.appState.frameRendering} : {},
|
||||
},
|
||||
files,
|
||||
},
|
||||
{
|
||||
withBackground:
|
||||
exportSettings?.withBackground ?? plugin.settings.exportWithBackground,
|
||||
withTheme: exportSettings?.withTheme ?? plugin.settings.exportWithTheme,
|
||||
isMask: exportSettings?.isMask ?? false,
|
||||
},
|
||||
padding,
|
||||
scale,
|
||||
);
|
||||
}
|
||||
|
||||
export const updateElementLinksToObsidianLinks = ({elements, hostFile}:{
|
||||
elements: ExcalidrawElement[];
|
||||
hostFile: TFile;
|
||||
}): ExcalidrawElement[] => {
|
||||
return elements.map((el)=>{
|
||||
if(el.link && el.link.startsWith("[")) {
|
||||
const partsArray = REGEX_LINK.getResList(el.link)[0];
|
||||
if(!partsArray?.value) return el;
|
||||
let linkText = REGEX_LINK.getLink(partsArray);
|
||||
if (linkText.search("#") > -1) {
|
||||
const linkParts = getLinkParts(linkText, hostFile);
|
||||
linkText = linkParts.path;
|
||||
}
|
||||
if (linkText.match(REG_LINKINDEX_INVALIDCHARS)) {
|
||||
return el;
|
||||
}
|
||||
const file = EXCALIDRAW_PLUGIN.app.metadataCache.getFirstLinkpathDest(
|
||||
linkText,
|
||||
hostFile.path,
|
||||
);
|
||||
if(!file) {
|
||||
return el;
|
||||
}
|
||||
let link = EXCALIDRAW_PLUGIN.app.getObsidianUrl(file);
|
||||
if(window.ExcalidrawAutomate?.onUpdateElementLinkForExportHook) {
|
||||
link = window.ExcalidrawAutomate.onUpdateElementLinkForExportHook({
|
||||
originalLink: el.link,
|
||||
obsidianLink: link,
|
||||
linkedFile: file,
|
||||
hostFile: hostFile
|
||||
});
|
||||
}
|
||||
const newElement: Mutable<ExcalidrawElement> = cloneElement(el);
|
||||
newElement.link = link;
|
||||
return newElement;
|
||||
}
|
||||
return el;
|
||||
})
|
||||
}
|
||||
|
||||
function addFilterToForeignObjects(svg:SVGSVGElement):void {
|
||||
const foreignObjects = svg.querySelectorAll("foreignObject");
|
||||
foreignObjects.forEach((foreignObject) => {
|
||||
foreignObject.setAttribute("filter", THEME_FILTER);
|
||||
});
|
||||
}
|
||||
|
||||
export async function createSVG(
|
||||
templatePath: string = undefined,
|
||||
embedFont: boolean = false,
|
||||
exportSettings: ExportSettings,
|
||||
loader: EmbeddedFilesLoader,
|
||||
forceTheme: string = undefined,
|
||||
canvasTheme: string = undefined,
|
||||
canvasBackgroundColor: string = undefined,
|
||||
automateElements: ExcalidrawElement[] = [],
|
||||
plugin: ExcalidrawPlugin,
|
||||
depth: number,
|
||||
padding?: number,
|
||||
imagesDict?: any,
|
||||
convertMarkdownLinksToObsidianURLs: boolean = false,
|
||||
): Promise<SVGSVGElement> {
|
||||
if (!loader) {
|
||||
loader = new EmbeddedFilesLoader(plugin);
|
||||
}
|
||||
if(typeof exportSettings.skipInliningFonts === "undefined") {
|
||||
exportSettings.skipInliningFonts = !embedFont;
|
||||
}
|
||||
const template = templatePath
|
||||
? await getTemplate(plugin, templatePath, true, loader, depth, convertMarkdownLinksToObsidianURLs)
|
||||
: null;
|
||||
let elements = template?.elements ?? [];
|
||||
elements = elements.concat(automateElements);
|
||||
padding = padding ?? plugin.settings.exportPaddingSVG;
|
||||
const files = imagesDict ?? {};
|
||||
if(template?.files) {
|
||||
Object.values(template.files).forEach((f:any)=>{
|
||||
files[f.id]=f;
|
||||
});
|
||||
}
|
||||
|
||||
const theme = forceTheme ?? template?.appState?.theme ?? canvasTheme;
|
||||
const withTheme = exportSettings?.withTheme ?? plugin.settings.exportWithTheme;
|
||||
|
||||
const filenameParts = getEmbeddedFilenameParts(templatePath);
|
||||
const svg = await getSVG(
|
||||
{
|
||||
//createAndOpenDrawing
|
||||
type: "excalidraw",
|
||||
version: 2,
|
||||
source: GITHUB_RELEASES+PLUGIN_VERSION,
|
||||
elements,
|
||||
appState: {
|
||||
theme,
|
||||
viewBackgroundColor:
|
||||
template?.appState?.viewBackgroundColor ?? canvasBackgroundColor,
|
||||
...template?.appState?.frameRendering ? {frameRendering: template.appState.frameRendering} : {},
|
||||
},
|
||||
files,
|
||||
},
|
||||
{
|
||||
withBackground:
|
||||
exportSettings?.withBackground ?? plugin.settings.exportWithBackground,
|
||||
withTheme,
|
||||
isMask: exportSettings?.isMask ?? false,
|
||||
...filenameParts?.hasClippedFrameref
|
||||
? {frameRendering: {enabled: true, name: false, outline: false, clip: true}}
|
||||
: {},
|
||||
},
|
||||
padding,
|
||||
null,
|
||||
);
|
||||
|
||||
if (withTheme && theme === "dark") addFilterToForeignObjects(svg);
|
||||
|
||||
if(
|
||||
!(filenameParts.hasGroupref || filenameParts.hasFrameref || filenameParts.hasClippedFrameref) &&
|
||||
(filenameParts.hasBlockref || filenameParts.hasSectionref)
|
||||
) {
|
||||
let el = filenameParts.hasSectionref
|
||||
? getTextElementsMatchingQuery(elements,["# "+filenameParts.sectionref],true)
|
||||
: elements.filter((el: ExcalidrawElement)=>el.id===filenameParts.blockref);
|
||||
if(el.length>0) {
|
||||
const containerId = el[0].containerId;
|
||||
if(containerId) {
|
||||
el = el.concat(elements.filter((el: ExcalidrawElement)=>el.id === containerId));
|
||||
}
|
||||
const elBB = plugin.ea.getBoundingBox(el);
|
||||
const drawingBB = plugin.ea.getBoundingBox(elements);
|
||||
svg.viewBox.baseVal.x = elBB.topX - drawingBB.topX;
|
||||
svg.viewBox.baseVal.y = elBB.topY - drawingBB.topY;
|
||||
const width = elBB.width + 2*padding;
|
||||
svg.viewBox.baseVal.width = width;
|
||||
const height = elBB.height + 2*padding;
|
||||
svg.viewBox.baseVal.height = height;
|
||||
svg.setAttribute("width", `${width}`);
|
||||
svg.setAttribute("height", `${height}`);
|
||||
}
|
||||
}
|
||||
if (template?.hasSVGwithBitmap) {
|
||||
svg.setAttribute("hasbitmap", "true");
|
||||
}
|
||||
return svg;
|
||||
}
|
||||
|
||||
function estimateLineBound(points: any): [number, number, number, number] {
|
||||
let minX = Infinity;
|
||||
let minY = Infinity;
|
||||
let maxX = -Infinity;
|
||||
let maxY = -Infinity;
|
||||
|
||||
for (const [x, y] of points) {
|
||||
minX = Math.min(minX, x);
|
||||
minY = Math.min(minY, y);
|
||||
maxX = Math.max(maxX, x);
|
||||
maxY = Math.max(maxY, y);
|
||||
}
|
||||
|
||||
return [minX, minY, maxX, maxY];
|
||||
}
|
||||
|
||||
export function estimateBounds(
|
||||
elements: ExcalidrawElement[],
|
||||
): [number, number, number, number] {
|
||||
const bb = getCommonBoundingBox(elements);
|
||||
return [bb.minX, bb.minY, bb.maxX, bb.maxY];
|
||||
}
|
||||
|
||||
export function repositionElementsToCursor(
|
||||
elements: ExcalidrawElement[],
|
||||
newPosition: { x: number; y: number },
|
||||
center: boolean = false,
|
||||
): ExcalidrawElement[] {
|
||||
const [x1, y1, x2, y2] = estimateBounds(elements);
|
||||
let [offsetX, offsetY] = [0, 0];
|
||||
if (center) {
|
||||
[offsetX, offsetY] = [
|
||||
newPosition.x - (x1 + x2) / 2,
|
||||
newPosition.y - (y1 + y2) / 2,
|
||||
];
|
||||
} else {
|
||||
[offsetX, offsetY] = [newPosition.x - x1, newPosition.y - y1];
|
||||
}
|
||||
|
||||
elements.forEach((element: any) => {
|
||||
//using any so I can write read-only propery x & y
|
||||
element.x = element.x + offsetX;
|
||||
element.y = element.y + offsetY;
|
||||
});
|
||||
|
||||
return restore({elements}, null, null).elements;
|
||||
}
|
||||
|
||||
export const insertLaTeXToView = (view: ExcalidrawView) => {
|
||||
const app = view.plugin.app;
|
||||
const ea = view.plugin.ea;
|
||||
GenericInputPrompt.Prompt(
|
||||
view,
|
||||
view.plugin,
|
||||
app,
|
||||
t("ENTER_LATEX"),
|
||||
"\\color{red}\\oint_S {E_n dA = \\frac{1}{{\\varepsilon _0 }}} Q_{inside}",
|
||||
view.plugin.settings.latexBoilerplate,
|
||||
undefined,
|
||||
3
|
||||
).then(async (formula: string) => {
|
||||
if (!formula) {
|
||||
return;
|
||||
}
|
||||
ea.reset();
|
||||
await ea.addLaTex(0, 0, formula);
|
||||
ea.setView(view);
|
||||
ea.addElementsToView(true, false, true);
|
||||
});
|
||||
};
|
||||
|
||||
export const search = async (view: ExcalidrawView) => {
|
||||
const ea = view.plugin.ea;
|
||||
ea.reset();
|
||||
ea.setView(view);
|
||||
const elements = ea.getViewElements().filter((el) => el.type === "text" || el.type === "frame" || el.link || el.type === "image");
|
||||
if (elements.length === 0) {
|
||||
return;
|
||||
}
|
||||
let text = await ScriptEngine.inputPrompt(
|
||||
view,
|
||||
view.plugin,
|
||||
view.plugin.app,
|
||||
"Search for",
|
||||
"use quotation marks for exact match",
|
||||
"",
|
||||
);
|
||||
if (!text) {
|
||||
return;
|
||||
}
|
||||
const res = text.matchAll(/"(.*?)"/g);
|
||||
let query: string[] = [];
|
||||
let parts;
|
||||
while (!(parts = res.next()).done) {
|
||||
query.push(parts.value[1]);
|
||||
}
|
||||
text = text.replaceAll(/"(.*?)"/g, "");
|
||||
query = query.concat(text.split(" ").filter((s:string) => s.length !== 0));
|
||||
|
||||
ea.targetView.selectElementsMatchingQuery(elements, query);
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @param elements
|
||||
* @param query
|
||||
* @param exactMatch - when searching for section header exactMatch should be set to true
|
||||
* @returns the elements matching the query
|
||||
*/
|
||||
export const getTextElementsMatchingQuery = (
|
||||
elements: ExcalidrawElement[],
|
||||
query: string[],
|
||||
exactMatch: boolean = false, //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/530
|
||||
): ExcalidrawElement[] => {
|
||||
if (!elements || elements.length === 0 || !query || query.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return elements.filter((el: any) =>
|
||||
el.type === "text" &&
|
||||
query.some((q) => {
|
||||
if (exactMatch) {
|
||||
const text = el.rawText.toLowerCase().split("\n")[0].trim();
|
||||
const m = text.match(/^#*(# .*)/);
|
||||
if (!m || m.length !== 2) {
|
||||
return false;
|
||||
}
|
||||
return m[1] === q.toLowerCase();
|
||||
}
|
||||
const text = el.rawText.toLowerCase().replaceAll("\n", " ").trim();
|
||||
return text.match(q.toLowerCase()); //to distinguish between "# frame" and "# frame 1" https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/530
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param elements
|
||||
* @param query
|
||||
* @param exactMatch - when searching for section header exactMatch should be set to true
|
||||
* @returns the elements matching the query
|
||||
*/
|
||||
export const getFrameElementsMatchingQuery = (
|
||||
elements: ExcalidrawElement[],
|
||||
query: string[],
|
||||
exactMatch: boolean = false, //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/530
|
||||
): ExcalidrawElement[] => {
|
||||
if (!elements || elements.length === 0 || !query || query.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return elements.filter((el: any) =>
|
||||
el.type === "frame" &&
|
||||
query.some((q) => {
|
||||
if (exactMatch) {
|
||||
const text = el.name?.toLowerCase().split("\n")[0].trim() ?? "";
|
||||
const m = text.match(/^#*(# .*)/);
|
||||
if (!m || m.length !== 2) {
|
||||
return false;
|
||||
}
|
||||
return m[1] === q.toLowerCase();
|
||||
}
|
||||
const text = el.name
|
||||
? el.name.toLowerCase().replaceAll("\n", " ").trim()
|
||||
: "";
|
||||
|
||||
return text.match(q.toLowerCase()); //to distinguish between "# frame" and "# frame 1" https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/530
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param elements
|
||||
* @param query
|
||||
* @param exactMatch - when searching for section header exactMatch should be set to true
|
||||
* @returns the elements matching the query
|
||||
*/
|
||||
export const getElementsWithLinkMatchingQuery = (
|
||||
elements: ExcalidrawElement[],
|
||||
query: string[],
|
||||
exactMatch: boolean = false, //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/530
|
||||
): ExcalidrawElement[] => {
|
||||
if (!elements || elements.length === 0 || !query || query.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return elements.filter((el: any) =>
|
||||
el.link &&
|
||||
query.some((q) => {
|
||||
const text = el.link.toLowerCase().trim();
|
||||
return exactMatch
|
||||
? (text === q.toLowerCase())
|
||||
: text.match(q.toLowerCase());
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param elements
|
||||
* @param query
|
||||
* @param exactMatch - when searching for section header exactMatch should be set to true
|
||||
* @returns the elements matching the query
|
||||
*/
|
||||
export const getImagesMatchingQuery = (
|
||||
elements: ExcalidrawElement[],
|
||||
query: string[],
|
||||
excalidrawData: ExcalidrawData,
|
||||
exactMatch: boolean = false, //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/530
|
||||
): ExcalidrawElement[] => {
|
||||
if (!elements || elements.length === 0 || !query || query.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return elements.filter((el: ExcalidrawElement) =>
|
||||
el.type === "image" &&
|
||||
query.some((q) => {
|
||||
const filename = excalidrawData.getFile(el.fileId)?.file?.basename.toLowerCase().trim();
|
||||
const equation = excalidrawData.getEquation(el.fileId)?.latex?.toLocaleLowerCase().trim();
|
||||
const text = filename ?? equation;
|
||||
if(!text) return false;
|
||||
return exactMatch
|
||||
? (text === q.toLowerCase())
|
||||
: text.match(q.toLowerCase());
|
||||
}));
|
||||
}
|
||||
|
||||
export const cloneElement = (el: ExcalidrawElement):any => {
|
||||
const newEl = JSON.parse(JSON.stringify(el));
|
||||
newEl.version = el.version + 1;
|
||||
newEl.updated = Date.now();
|
||||
newEl.versionNonce = Math.floor(Math.random() * 1000000000);
|
||||
return newEl;
|
||||
}
|
||||
|
||||
export const verifyMinimumPluginVersion = (requiredVersion: string): boolean => {
|
||||
return PLUGIN_VERSION === requiredVersion || isVersionNewerThanOther(PLUGIN_VERSION,requiredVersion);
|
||||
}
|
||||
|
||||
export const getBoundTextElementId = (container: ExcalidrawElement | null) => {
|
||||
return container?.boundElements?.length
|
||||
? container?.boundElements?.find((ele) => ele.type === "text")?.id || null
|
||||
: null;
|
||||
};
|
||||
424
src/utils/exportUtils.ts
Normal file
424
src/utils/exportUtils.ts
Normal file
@@ -0,0 +1,424 @@
|
||||
import { Notice } from 'obsidian';
|
||||
import { getEA } from 'src/core';
|
||||
import { t } from 'src/lang/helpers';
|
||||
|
||||
// Define proper types for PDFLib
|
||||
type PDFLibType = typeof import('@cantoo/pdf-lib');
|
||||
let pdfLibPromise: Promise<PDFLibType> | null = null;
|
||||
|
||||
async function getPDFLib(): Promise<PDFLibType> {
|
||||
if (!pdfLibPromise) {
|
||||
pdfLibPromise = import('@cantoo/pdf-lib');
|
||||
}
|
||||
return pdfLibPromise;
|
||||
}
|
||||
|
||||
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: Awaited<ReturnType<typeof import('@cantoo/pdf-lib').PDFDocument.create>>,
|
||||
svg: SVGSVGElement,
|
||||
dimensions: SVGDimensions,
|
||||
pageDim: PageDimensions,
|
||||
pageProps: PDFPageProperties
|
||||
) {
|
||||
const { rgb } = await getPDFLib();
|
||||
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 { PDFDocument } = await getPDFLib();
|
||||
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);
|
||||
//@ts-ignore
|
||||
let noticeContainerEl = notice.containerEl ?? notice.noticeEl;
|
||||
|
||||
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) {
|
||||
if(noticeContainerEl.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);
|
||||
//@ts-ignore
|
||||
noticeContainerEl = notice.containerEl ?? notice.noticeEl;
|
||||
}
|
||||
await addSVGToPage(pdfDoc, svg, dim, pageProps.dimensions, pageProps);
|
||||
}
|
||||
j++;
|
||||
}
|
||||
|
||||
//@ts-ignore
|
||||
if(noticeContainerEl.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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import { ExcalidrawElement, ExcalidrawImageElement, ExcalidrawTextElement } from
|
||||
import { REGEX_LINK, REG_LINKINDEX_HYPERLINK } from "../shared/ExcalidrawData";
|
||||
import ExcalidrawView, { TextMode } from "src/view/ExcalidrawView";
|
||||
import { rotatedDimensions } from "./utils";
|
||||
import { getBoundTextElementId } from "src/shared/ExcalidrawAutomate";
|
||||
import { getBoundTextElementId } from "src/utils/excalidrawAutomateUtils";
|
||||
|
||||
export const getElementsAtPointer = (
|
||||
pointer: any,
|
||||
|
||||
@@ -18,20 +18,21 @@ import {
|
||||
getContainerElement,
|
||||
} from "../constants/constants";
|
||||
import ExcalidrawPlugin from "../core/main";
|
||||
import { ExcalidrawElement, ExcalidrawTextElement, ImageCrop } from "@zsviczian/excalidraw/types/excalidraw/element/types";
|
||||
import { ExcalidrawElement, ExcalidrawImageElement, ExcalidrawTextElement, ImageCrop } from "@zsviczian/excalidraw/types/excalidraw/element/types";
|
||||
import { ExportSettings } from "../view/ExcalidrawView";
|
||||
import { getDataURLFromURL, getIMGFilename, getMimeType, getURLImageExtension } from "./fileUtils";
|
||||
import { generateEmbeddableLink } from "./customEmbeddableUtils";
|
||||
import { FILENAMEPARTS } from "../types/utilTypes";
|
||||
import { Mutable } from "@zsviczian/excalidraw/types/excalidraw/utility-types";
|
||||
import { cleanBlockRef, cleanSectionHeading, getFileCSSClasses } from "./obsidianUtils";
|
||||
import { updateElementLinksToObsidianLinks } from "src/shared/ExcalidrawAutomate";
|
||||
import { updateElementLinksToObsidianLinks } from "./excalidrawAutomateUtils";
|
||||
import { CropImage } from "../shared/CropImage";
|
||||
import opentype from 'opentype.js';
|
||||
import { runCompressionWorker } from "src/shared/Workers/compression-worker";
|
||||
import Pool from "es6-promise-pool";
|
||||
import { FileData } from "../shared/EmbeddedFileLoader";
|
||||
import { t } from "src/lang/helpers";
|
||||
import ExcalidrawScene from "src/shared/svgToExcalidraw/elements/ExcalidrawScene";
|
||||
|
||||
declare const PLUGIN_VERSION:string;
|
||||
declare var LZString: any;
|
||||
@@ -415,11 +416,17 @@ export async function getImageSize (
|
||||
});
|
||||
};
|
||||
|
||||
export function addAppendUpdateCustomData (el: Mutable<ExcalidrawElement>, newData: any): ExcalidrawElement {
|
||||
export function addAppendUpdateCustomData (
|
||||
el: Mutable<ExcalidrawElement>,
|
||||
newData: Partial<Record<string, unknown>>
|
||||
): ExcalidrawElement {
|
||||
if(!newData) return el;
|
||||
if(!el.customData) el.customData = {};
|
||||
for (const key in newData) {
|
||||
if(typeof newData[key] === "undefined") continue;
|
||||
if(typeof newData[key] === "undefined") {
|
||||
delete el.customData[key];
|
||||
continue;
|
||||
}
|
||||
el.customData[key] = newData[key];
|
||||
}
|
||||
return el;
|
||||
@@ -447,7 +454,7 @@ export function scaleLoadedImage (
|
||||
|
||||
scene.elements
|
||||
.filter((e: any) => e.type === "image" && e.fileId === img.id)
|
||||
.forEach((el: any) => {
|
||||
.forEach((el: Mutable<ExcalidrawImageElement>) => {
|
||||
const [elWidth, elHeight] = [el.width, el.height];
|
||||
const maintainArea = img.shouldScale; //true if image should maintain its area, false if image should display at 100% its size
|
||||
const elCrop: ImageCrop = el.crop;
|
||||
|
||||
@@ -57,16 +57,16 @@ import {
|
||||
getContainerElement,
|
||||
} from "../constants/constants";
|
||||
import ExcalidrawPlugin from "../core/main";
|
||||
import { ExcalidrawAutomate } from "../shared/ExcalidrawAutomate";
|
||||
import {
|
||||
repositionElementsToCursor,
|
||||
ExcalidrawAutomate,
|
||||
getTextElementsMatchingQuery,
|
||||
cloneElement,
|
||||
getFrameElementsMatchingQuery,
|
||||
getElementsWithLinkMatchingQuery,
|
||||
getImagesMatchingQuery,
|
||||
getBoundTextElementId
|
||||
} from "../shared/ExcalidrawAutomate";
|
||||
} from "../utils/excalidrawAutomateUtils";
|
||||
import { t } from "../lang/helpers";
|
||||
import {
|
||||
ExcalidrawData,
|
||||
@@ -76,7 +76,9 @@ import {
|
||||
getExcalidrawMarkdownHeaderSection,
|
||||
} from "../shared/ExcalidrawData";
|
||||
import {
|
||||
arrayBufferToBase64,
|
||||
checkAndCreateFolder,
|
||||
createOrOverwriteFile,
|
||||
download,
|
||||
getDataURLFromURL,
|
||||
getIMGFilename,
|
||||
@@ -105,8 +107,9 @@ import {
|
||||
shouldEmbedScene,
|
||||
_getContainerElement,
|
||||
arrayToMap,
|
||||
addAppendUpdateCustomData,
|
||||
} from "../utils/utils";
|
||||
import { cleanBlockRef, cleanSectionHeading, closeLeafView, getAttachmentsFolderAndFilePath, getLeaf, getParentOfClass, obsidianPDFQuoteWithRef, openLeaf, setExcalidrawView } from "../utils/obsidianUtils";
|
||||
import { cleanBlockRef, cleanSectionHeading, closeLeafView, getActivePDFPageNumberFromPDFView, getAttachmentsFolderAndFilePath, getLeaf, getParentOfClass, obsidianPDFQuoteWithRef, openLeaf, setExcalidrawView } from "../utils/obsidianUtils";
|
||||
import { splitFolderAndFilename } from "../utils/fileUtils";
|
||||
import { ConfirmationPrompt, GenericInputPrompt, NewFileActions, Prompt, linkPrompt } from "../shared/Dialogs/Prompt";
|
||||
import { ClipboardData } from "@zsviczian/excalidraw/types/excalidraw/clipboard";
|
||||
@@ -147,6 +150,9 @@ import { IS_WORKER_SUPPORTED } from "../shared/Workers/compression-worker";
|
||||
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;
|
||||
@@ -223,18 +229,26 @@ export const addFiles = async (
|
||||
.filter((f:FileData) => view.excalidrawData.getFile(f.id)?.file?.extension === "pdf")
|
||||
.forEach((f:FileData) => {
|
||||
s.scene.elements
|
||||
.filter((el:ExcalidrawElement)=>el.type === "image" && el.fileId === f.id && el.crop && el.crop.naturalWidth !== f.size.width)
|
||||
.filter((el:ExcalidrawElement)=>el.type === "image" && el.fileId === f.id && (
|
||||
(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,
|
||||
};
|
||||
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});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -250,13 +264,14 @@ export const addFiles = async (
|
||||
if (view.excalidrawData.hasFile(f.id)) {
|
||||
const embeddedFile = view.excalidrawData.getFile(f.id);
|
||||
|
||||
embeddedFile.setImage(
|
||||
f.dataURL,
|
||||
f.mimeType,
|
||||
f.size,
|
||||
embeddedFile.setImage({
|
||||
imgBase64: f.dataURL,
|
||||
mimeType: f.mimeType,
|
||||
size: f.size,
|
||||
isDark,
|
||||
f.hasSVGwithBitmap,
|
||||
);
|
||||
isSVGwithBitmap: f.hasSVGwithBitmap,
|
||||
pdfPageViewProps: f.pdfPageViewProps,
|
||||
});
|
||||
}
|
||||
if (view.excalidrawData.hasEquation(f.id)) {
|
||||
const latex = view.excalidrawData.getEquation(f.id).latex;
|
||||
@@ -357,7 +372,7 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
|
||||
constructor(leaf: WorkspaceLeaf, plugin: ExcalidrawPlugin) {
|
||||
super(leaf);
|
||||
this._plugin = plugin;
|
||||
this.excalidrawData = new ExcalidrawData(plugin);
|
||||
this.excalidrawData = new ExcalidrawData(plugin, this);
|
||||
this.canvasNodeFactory = new CanvasNodeFactory(this);
|
||||
this.setHookServer();
|
||||
this.dropManager = new DropManager(this);
|
||||
@@ -504,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) {
|
||||
@@ -544,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;
|
||||
@@ -587,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) {
|
||||
@@ -1087,7 +1167,7 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
|
||||
|
||||
isFullscreen(): boolean {
|
||||
//(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.isFullscreen, "ExcalidrawView.isFullscreen");
|
||||
return Boolean(document.body.querySelector(".excalidraw-hidden"));
|
||||
return Boolean(this.ownerDocument.body.querySelector(".excalidraw-hidden"));
|
||||
}
|
||||
|
||||
exitFullscreen() {
|
||||
@@ -1498,6 +1578,9 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
|
||||
this.packages = this.plugin.getPackage(this.ownerWindow);
|
||||
|
||||
if(DEVICE.isDesktop && !apiMissing) {
|
||||
if(this.ownerWindow !== window) {
|
||||
this.plugin.initializeFonts();
|
||||
}
|
||||
this.destroyers.push(
|
||||
//this.containerEl.onWindowMigrated(this.leaf.rebuildView.bind(this))
|
||||
this.containerEl.onWindowMigrated(async() => {
|
||||
@@ -3322,19 +3405,31 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
|
||||
const isPointerOutsideVisibleArea = top.x>this.currentPosition.x || bottom.x<this.currentPosition.x || top.y>this.currentPosition.y || bottom.y<this.currentPosition.y;
|
||||
|
||||
const id = ea.addText(this.currentPosition.x, this.currentPosition.y, text);
|
||||
await this.addElements(ea.getElements(), isPointerOutsideVisibleArea, save, undefined, true);
|
||||
await this.addElements({
|
||||
newElements: ea.getElements(),
|
||||
repositionToCursor: isPointerOutsideVisibleArea,
|
||||
save: save,
|
||||
newElementsOnTop: true
|
||||
});
|
||||
ea.destroy();
|
||||
return id;
|
||||
};
|
||||
|
||||
public async addElements(
|
||||
newElements: ExcalidrawElement[],
|
||||
repositionToCursor: boolean = false,
|
||||
save: boolean = false,
|
||||
images: any,
|
||||
newElementsOnTop: boolean = false,
|
||||
shouldRestoreElements: boolean = false,
|
||||
): Promise<boolean> {
|
||||
public async addElements({
|
||||
newElements,
|
||||
repositionToCursor = false,
|
||||
save = false,
|
||||
images,
|
||||
newElementsOnTop = false,
|
||||
shouldRestoreElements = false,
|
||||
}: {
|
||||
newElements: ExcalidrawElement[];
|
||||
repositionToCursor?: boolean;
|
||||
save?: boolean;
|
||||
images?: {[key: FileId]: ImageInfo};
|
||||
newElementsOnTop?: boolean;
|
||||
shouldRestoreElements?: boolean;
|
||||
}): Promise<boolean> {
|
||||
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.addElements, "ExcalidrawView.addElements", newElements, repositionToCursor, save, images, newElementsOnTop, shouldRestoreElements);
|
||||
const api = this.excalidrawAPI as ExcalidrawImperativeAPI;
|
||||
if (!api) {
|
||||
@@ -3391,40 +3486,38 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
|
||||
? el.concat(newElements.filter((e) => !removeList.includes(e.id)))
|
||||
: newElements.filter((e) => !removeList.includes(e.id)).concat(el);
|
||||
|
||||
this.updateScene(
|
||||
{
|
||||
elements,
|
||||
storeAction: "capture",
|
||||
},
|
||||
shouldRestoreElements,
|
||||
);
|
||||
|
||||
if (images && Object.keys(images).length >0) {
|
||||
const files: BinaryFileData[] = [];
|
||||
Object.keys(images).forEach((k) => {
|
||||
const files: BinaryFileData[] = [];
|
||||
if (images && Object.keys(images).length >0) {
|
||||
Object.keys(images).forEach((k: FileId) => {
|
||||
files.push({
|
||||
mimeType: images[k].mimeType,
|
||||
id: images[k].id,
|
||||
dataURL: images[k].dataURL,
|
||||
created: images[k].created,
|
||||
});
|
||||
if (images[k].file || images[k].isHyperLink || images[k].isLocalLink) {
|
||||
if (images[k].file || images[k].isHyperLink) { //|| images[k].isLocalLink but isLocalLink was never passed
|
||||
const embeddedFile = new EmbeddedFile(
|
||||
this.plugin,
|
||||
this.file.path,
|
||||
images[k].isHyperLink && !images[k].isLocalLink
|
||||
images[k].isHyperLink //&& !images[k].isLocalLink local link is never passed to addElements
|
||||
? images[k].hyperlink
|
||||
: images[k].file,
|
||||
: (typeof images[k].file === "string" ? images[k].file : images[k].file.path),
|
||||
);
|
||||
const st: AppState = api.getAppState();
|
||||
embeddedFile.setImage(
|
||||
images[k].dataURL,
|
||||
images[k].mimeType,
|
||||
images[k].size,
|
||||
st.theme === "dark",
|
||||
images[k].hasSVGwithBitmap,
|
||||
);
|
||||
embeddedFile.setImage({
|
||||
imgBase64: images[k].dataURL,
|
||||
mimeType: images[k].mimeType,
|
||||
size: images[k].size,
|
||||
isDark: st.theme === "dark",
|
||||
isSVGwithBitmap: images[k].hasSVGwithBitmap,
|
||||
pdfPageViewProps: images[k].pdfPageViewProps,
|
||||
});
|
||||
this.excalidrawData.setFile(images[k].id, embeddedFile);
|
||||
if(images[k].pdfPageViewProps) {
|
||||
elements.filter((e) => e.type === "image" && e.fileId === images[k].id).forEach((e) => {
|
||||
addAppendUpdateCustomData(e, {pdfPageViewProps: images[k].pdfPageViewProps});
|
||||
});
|
||||
}
|
||||
}
|
||||
if (images[k].latex) {
|
||||
this.excalidrawData.setEquation(images[k].id, {
|
||||
@@ -3433,8 +3526,20 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.updateScene(
|
||||
{
|
||||
elements,
|
||||
storeAction: "capture",
|
||||
},
|
||||
shouldRestoreElements,
|
||||
);
|
||||
|
||||
if(files.length > 0) {
|
||||
api.addFiles(files);
|
||||
}
|
||||
|
||||
api.updateContainerSize(api.getSceneElements().filter(el => newIds.includes(el.id)).filter(isContainer));
|
||||
if (save) {
|
||||
await this.save(false); //preventReload=false will ensure that markdown links are paresed and displayed correctly
|
||||
@@ -3993,7 +4098,9 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{
|
||||
link,
|
||||
naturalHeight: fd.size.height,
|
||||
naturalWidth: fd.size.width,
|
||||
pdfPageViewProps: fd.pdfPageViewProps,
|
||||
});
|
||||
addAppendUpdateCustomData(el, {pdfPageViewProps: fd.pdfPageViewProps});
|
||||
if(el.crop) {
|
||||
el.width = el.crop.width/this.plugin.settings.pdfScale;
|
||||
el.height = el.crop.height/this.plugin.settings.pdfScale;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -4,7 +4,7 @@ import * as React from "react";
|
||||
import { ActionButton } from "./ActionButton";
|
||||
import { ICONS, saveIcon, stringToSVG } from "../../../constants/actionIcons";
|
||||
import { DEVICE, SCRIPT_INSTALL_FOLDER } from "../../../constants/constants";
|
||||
import { insertLaTeXToView, search } from "../../../shared/ExcalidrawAutomate";
|
||||
import { insertLaTeXToView, search } from "../../../utils/excalidrawAutomateUtils";
|
||||
import ExcalidrawView, { TextMode } from "../../ExcalidrawView";
|
||||
import { t } from "../../../lang/helpers";
|
||||
import { ReleaseNotes } from "../../../shared/Dialogs/ReleaseNotes";
|
||||
|
||||
48
styles.css
48
styles.css
@@ -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;
|
||||
@@ -104,6 +116,7 @@ li[data-testid] {
|
||||
border: 0 !important;
|
||||
box-shadow: 0 !important;
|
||||
background-color: transparent !important;
|
||||
overflow-y: auto !important;
|
||||
}
|
||||
|
||||
.excalidraw .popover {
|
||||
@@ -324,6 +337,16 @@ label.color-input-container > input {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.excalidraw .App-toolbar-content .dropdown-menu {
|
||||
max-height: 70vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.excalidraw .panelColumn {
|
||||
max-height: 70vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.excalidraw .panelColumn .buttonList {
|
||||
max-width: 13rem;
|
||||
}
|
||||
@@ -361,9 +384,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 +695,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;
|
||||
}
|
||||
BIN
test-data/PDFs/page-rotated-180.pdf
Normal file
BIN
test-data/PDFs/page-rotated-180.pdf
Normal file
Binary file not shown.
BIN
test-data/PDFs/page-rotated-270.pdf
Normal file
BIN
test-data/PDFs/page-rotated-270.pdf
Normal file
Binary file not shown.
BIN
test-data/PDFs/page-rotated-90.pdf
Normal file
BIN
test-data/PDFs/page-rotated-90.pdf
Normal file
Binary file not shown.
BIN
test-data/PDFs/page-trimmed-rotated-180.pdf
Normal file
BIN
test-data/PDFs/page-trimmed-rotated-180.pdf
Normal file
Binary file not shown.
BIN
test-data/PDFs/page-trimmed-rotated-270.pdf
Normal file
BIN
test-data/PDFs/page-trimmed-rotated-270.pdf
Normal file
Binary file not shown.
BIN
test-data/PDFs/page-trimmed-rotated-90.pdf
Normal file
BIN
test-data/PDFs/page-trimmed-rotated-90.pdf
Normal file
Binary file not shown.
BIN
test-data/PDFs/page-trimmed.pdf
Normal file
BIN
test-data/PDFs/page-trimmed.pdf
Normal file
Binary file not shown.
BIN
test-data/PDFs/page.pdf
Normal file
BIN
test-data/PDFs/page.pdf
Normal file
Binary file not shown.
Reference in New Issue
Block a user