Compare commits

...

39 Commits

Author SHA1 Message Date
zsviczian
efcb0c0580 added ExcalidrawAutomate getPagePDFDimensions and createPDF 2025-01-18 18:56:21 +01:00
zsviczian
23d7105fb1 export dialog buttons 2025-01-18 18:16:49 +01:00
zsviczian
5d9565bd7c PDF export settings 2025-01-18 17:01:40 +01:00
zsviczian
59785523ae carved out PDFExportSettingsComponent 2025-01-18 15:46:27 +01:00
zsviczian
2a21ed5fc7 vertical positioning fixed 2025-01-18 15:13:14 +01:00
zsviczian
3d3ce73fa1 export to vault, added pdf settings 2025-01-18 13:20:00 +01:00
zsviczian
c35bd385fe extracted strings to language file 2025-01-18 12:18:56 +01:00
zsviczian
a790b04547 initial implementation 2025-01-18 11:37:58 +01:00
zsviczian
5171978c37 Merge pull request #2217 from zsviczian/PDF-fix
Some checks failed
CodeQL / Analyze (javascript) (push) Has been cancelled
Pdf crop fix
2025-01-17 06:46:59 +01:00
zsviczian
ea4a0c91e8 added PDF samples for testing 2025-01-17 06:45:30 +01:00
zsviczian
34af6dd447 getPDFCropRect, getPDFRect improved 90,180,270 calc, still issue with page offsets 2025-01-15 23:22:35 +01:00
zsviczian
ed2e700946 Merge pull request #2215 from zsviczian/local-graph-embed-sync
Some checks failed
CodeQL / Analyze (javascript) (push) Has been cancelled
Set local graph when embeddable is activated/deactivated #2200
2025-01-15 09:38:13 +01:00
zsviczian
7eb23ab5e1 Set local graph when embeddable is activated/deactivated #2200 2025-01-15 08:22:38 +00:00
zsviczian
7cccf1d4e2 2.7.6-beta-1
Some checks are pending
CodeQL / Analyze (javascript) (push) Waiting to run
2025-01-14 22:57:14 +01:00
zsviczian
2a5545964c update package-lock.json, update packages, style.css to support new arrow picker 2025-01-14 22:15:28 +01:00
zsviczian
4ce22883cc Merge pull request #2214 from zsviczian/allow-new-image-formats
Some checks are pending
CodeQL / Analyze (javascript) (push) Waiting to run
fix: allow jfif and avif
2025-01-14 16:01:01 +01:00
zsviczian
272804afc8 allow jfif and avif 2025-01-14 14:59:47 +00:00
zsviczian
dc0b50f717 Update How-to.yml
Some checks failed
CodeQL / Analyze (javascript) (push) Has been cancelled
2025-01-09 08:52:41 +01:00
zsviczian
a0eb625b8a Update How-to.yml 2025-01-09 08:52:11 +01:00
zsviczian
524dc54d03 Update bug_report.yml 2025-01-09 08:50:35 +01:00
zsviczian
918718be90 Update bug_report.yml 2025-01-09 08:49:59 +01:00
zsviczian
78ee784be1 Update bug_report.yml 2025-01-09 08:48:58 +01:00
zsviczian
7e0e016bf9 Update bug_report.yml 2025-01-09 08:48:18 +01:00
zsviczian
4f875a03a0 2.7.5
Some checks failed
CodeQL / Analyze (javascript) (push) Has been cancelled
2025-01-05 23:09:16 +01:00
zsviczian
63c56e0e98 similar elements allows selection of containers
Some checks are pending
CodeQL / Analyze (javascript) (push) Waiting to run
2025-01-05 12:49:11 +01:00
zsviczian
46477208be split ellipse and concatenate line now works with rotated lines
Some checks are pending
CodeQL / Analyze (javascript) (push) Waiting to run
2025-01-05 11:38:45 +01:00
zsviczian
3194c014c7 updated concatenate lines 2025-01-05 10:33:31 +01:00
zsviczian
25ccb9dc43 updated EA lib in docs 2025-01-05 07:03:04 +01:00
zsviczian
fa46f8c39d Merge pull request #1880 from karaolidis/package-lock
Some checks are pending
CodeQL / Analyze (javascript) (push) Waiting to run
Allow automated & deterministic builds
2025-01-04 22:18:09 +01:00
zsviczian
8ffe5c3942 2.7.5-beta-1 2025-01-04 21:26:49 +01:00
zsviczian
88f256cd8f Added comments to EA, moved non-class function to EAUtils 2025-01-04 20:23:14 +01:00
zsviczian
1562600cd3 Image mask offset, PDF drift and offset, addAppendUpdateCustomData on EA, some type cleanup
Some checks are pending
CodeQL / Analyze (javascript) (push) Waiting to run
2025-01-04 19:06:43 +01:00
Nikolaos Karaolidis
d759abbc47 Allow commiting package-lock.json
Signed-off-by: Nikolaos Karaolidis <nick@karaolidis.com>
2025-01-04 18:38:56 +02:00
zsviczian
90533138e5 fixed embedding images into Excalidraw with areaRef links did not work as expected due to conflicting width and height values
Some checks failed
CodeQL / Analyze (javascript) (push) Has been cancelled
2024-12-31 08:37:46 +01:00
zsviczian
80d8f0e5b6 image occlusion, full-year calendar 2024-12-29 10:52:54 +01:00
zsviczian
9829fab97c Merge pull request #2185 from simonperet/full-year-calendar-script
Add full year calendar generator script
2024-12-29 10:48:30 +01:00
Simon
a33c8b6eab Add a full year calendar generator script 2024-12-29 09:21:44 +01:00
zsviczian
f0921856c1 fix 2184 2024-12-29 07:15:47 +01:00
zsviczian
31e06ac0e0 updated add connector point and set stroke width 2024-12-28 22:19:47 +01:00
64 changed files with 20544 additions and 1694 deletions

View File

@@ -12,6 +12,8 @@ body:
Before submitting a support request, please:
1. **Review the [documentation](https://github.com/zsviczian/obsidian-excalidraw-plugin/wiki)** your question may already be answered.
2. **[Search issues](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues) (including closed ones)** to see if your question has already been addressed.
3. **[Watch the Feature Walkthrough Video](https://youtu.be/P_Q6avJGoWI)**: As it infact answers 90% of the typical questions I receive
4. **[Consult NotebookLM with your question](https://excalidraw-obsidian.online/WIKI/09+Video+Transcripts/Videos/Turn+any+YouTube+Channel+into+your+AI+Mentor+-+Obsidian+is+the+ultimate+automation+workbench+for+PKM)**
- type: markdown
attributes:
@@ -31,6 +33,13 @@ body:
options:
- label: Yes, I have reviewed the documentation and searched for related issues.
- type: textarea
id: notebook_lm
attributes:
label: "Your NotebookLM query"
description: "See point 4) above. Paste the question and answer you received from NotebookLM. This serves partly as proof, partly to help me see where the model is incorrect"
placeholder: "Copy/Paste your question and the resulting answer you got from NotebookLM"
- type: textarea
id: support_question
attributes:

View File

@@ -1,5 +1,5 @@
name: Bug report
description: If something is clearly broken, its a bug. Everything else is a feature or support request. Most reported “bugs” are actually how-to questions or feature requests.
description: If something is clearly broken, its a bug. **Everything else** is a feature or support request. Most reported “bugs” are actually how-to questions or feature requests.
title: "BUG: "
body:
- type: markdown
@@ -12,6 +12,8 @@ body:
Before creating a bug report, please:
1. **Review recent [release notes](https://github.com/zsviczian/obsidian-excalidraw-plugin/releases)** maybe there is already an answer.
2. **[Search issues](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues) (including closed ones)** to see if there is anything similar.
3. **[Watch the Feature Walkthrough Video](https://youtu.be/P_Q6avJGoWI)**: As it infact answers 90% of the typical questions I receive
4. **[Consult NotebookLM with your question](https://excalidraw-obsidian.online/WIKI/09+Video+Transcripts/Videos/Turn+any+YouTube+Channel+into+your+AI+Mentor+-+Obsidian+is+the+ultimate+automation+workbench+for+PKM)**
- type: markdown
attributes:
@@ -46,6 +48,13 @@ body:
description: "Run `Command Palette/Show Debug info` in Obsidian and paste the result here."
placeholder: "Paste your Obsidian debug info here..."
- type: textarea
id: notebook_lm
attributes:
label: "Your NotebookLM query"
description: "See point 4) above. Paste the question and answer you received from NotebookLM. This serves partly as proof, partly to help me see where the model is incorrect"
placeholder: "Copy/Paste your question and the resulting answer you got from NotebookLM"
- type: textarea
id: bug_description
attributes:

1
.gitignore vendored
View File

@@ -4,7 +4,6 @@
# npm
node_modules
package-lock.json
# build
main.js

2
.nvmrc
View File

@@ -1 +1 @@
18
18

1340
MathjaxToSVG/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -13,11 +13,12 @@
"devDependencies": {
"@rollup/plugin-commonjs": "^26.0.1",
"@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-typescript": "^11.1.6",
"@rollup/plugin-typescript": "^12.1.2",
"cross-env": "^7.0.3",
"obsidian": "1.5.7-1",
"rollup": "^2.70.1",
"typescript": "^5.2.2",
"rollup-plugin-terser": "^7.0.2"
"rollup-plugin-terser": "^7.0.2",
"tslib": "^2.8.1",
"typescript": "^5.7.3"
}
}
}

View File

@@ -16,10 +16,10 @@ export default {
},
plugins: [
typescript({
tsconfig: '../tsconfig.json',
tsconfig: 'tsconfig.json',
}),
commonjs(),
nodeResolve({
nodeResolve({
browser: true,
preferBuiltins: false
}),
@@ -32,4 +32,4 @@ export default {
}
})
].filter(Boolean)
};
};

View File

@@ -0,0 +1,26 @@
{
"compilerOptions": {
"baseUrl": ".",
"sourceMap": false,
"module": "es2020",
"target": "es2022", //min es2017 because script engine requires for async execution and min es2018 for named capture groups
"allowJs": false,
"noImplicitAny": true,
"moduleResolution": "node",
"esModuleInterop": true,
"importHelpers": true,
"resolveJsonModule": true,
"lib": [
"dom",
"scripthost",
"es2022",
"DOM.Iterable"
],
"jsx": "react",
},
"include": [
"**/*.ts",
"**/*.tsx", "src/shared/Dialogs/OpenDrawing.ts",
"src/types/types.d.ts",
]
}

View File

@@ -1,4 +0,0 @@
The project runs with `node 18`.
After running `npm -i` you'll need to make two manual changes:

File diff suppressed because it is too large Load Diff

5782
docs/Release-notes.md Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -22,4 +22,4 @@ elements.forEach((el)=>{
);
ea.addToGroup([el.id,ellipseId]);
});
ea.addElementsToView(false,false);
await ea.addElementsToView(false,false,true);

View File

@@ -8,20 +8,76 @@ if(lines.length !== 2) {
return;
}
// https://math.stackexchange.com/questions/2204520/how-do-i-rotate-a-line-segment-in-a-specific-point-on-the-line
const rotate = (point, element) => {
const [x1, y1] = point;
const x2 = element.x + element.width/2;
const y2 = element.y - element.height/2;
const angle = element.angle;
return [
(x1 - x2) * Math.cos(angle) - (y1 - y2) * Math.sin(angle) + x2,
(x1 - x2) * Math.sin(angle) + (y1 - y2) * Math.cos(angle) + y2,
];
//Same line but with angle=0
function getNormalizedLine(originalElement) {
if(originalElement.angle === 0) return originalElement;
// Get absolute coordinates for all points first
const pointRotateRads = (point, center, angle) => {
const [x, y] = point;
const [cx, cy] = center;
return [
(x - cx) * Math.cos(angle) - (y - cy) * Math.sin(angle) + cx,
(x - cx) * Math.sin(angle) + (y - cy) * Math.cos(angle) + cy
];
};
// Get element absolute coordinates (matching Excalidraw's approach)
const getElementAbsoluteCoords = (element) => {
const points = element.points;
let minX = Infinity;
let minY = Infinity;
let maxX = -Infinity;
let maxY = -Infinity;
for (const [x, y] of points) {
const absX = x + element.x;
const absY = y + element.y;
minX = Math.min(minX, absX);
minY = Math.min(minY, absY);
maxX = Math.max(maxX, absX);
maxY = Math.max(maxY, absY);
}
return [minX, minY, maxX, maxY];
};
// Calculate center point based on absolute coordinates
const [x1, y1, x2, y2] = getElementAbsoluteCoords(originalElement);
const centerX = (x1 + x2) / 2;
const centerY = (y1 + y2) / 2;
// Calculate absolute coordinates of all points
const absolutePoints = originalElement.points.map(([x, y]) => [
x + originalElement.x,
y + originalElement.y
]);
// Rotate all points around the center
const rotatedPoints = absolutePoints.map(point =>
pointRotateRads(point, [centerX, centerY], originalElement.angle)
);
// Convert back to relative coordinates
const newPoints = rotatedPoints.map(([x, y]) => [
x - rotatedPoints[0][0],
y - rotatedPoints[0][1]
]);
const newLineId = ea.addLine(newPoints);
// Set the position of the new line to the first rotated point
const newLine = ea.getElement(newLineId);
newLine.x = rotatedPoints[0][0];
newLine.y = rotatedPoints[0][1];
newLine.angle = 0;
delete ea.elementsDict[newLine.id];
return newLine;
}
const points = lines.map(
el=>el.points.map(p=>rotate([p[0]+el.x, p[1]+el.y],el))
const points = lines.map(getNormalizedLine).map(
el=>el.points.map(p=>[p[0]+el.x, p[1]+el.y])
);
const last = (p) => p[p.length-1];
@@ -99,4 +155,4 @@ switch (lineTypes) {
}
ea.addElementsToView();
await ea.addElementsToView();

View File

@@ -0,0 +1,157 @@
/*
This script generates a complete calendar for a specified year, visually distinguishing weekends from weekdays through color coding.
![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-full-year-calendar-exemple.excalidraw.png)
## Customizable Colors
You can personalize the calendars appearance by defining your own colors:
1. Create two rectangles in your design.
2. Select both rectangles before running the script:
• The **fill and stroke colors of the first rectangle** will be applied to weekdays.
• The **fill and stroke colors of the second rectangle** will be used for weekends.
If no rectangle are selected, the default color schema will be used (white and purple).
![](https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-full-year-calendar-customize.excalidraw.png)
```javascript
*/
ea.reset();
// -------------------------------------
// Constants initiation
// -------------------------------------
const RECT_WIDTH = 300; // day width
const RECT_HEIGHT = 45; // day height
const START_X = 0; // X start position
const START_Y = 0; // PY start position
const MONTH_SPACING = 30; // space between months
const DAY_SPACING = 0; // space between days
const DAY_NAME_SPACING = 45; // space between day number and day letters
const DAY_NAME_AND_NUMBER_X_MARGIN = 5;
const MONTH_NAME_SPACING = -40;
const YEAR_X = (RECT_WIDTH + MONTH_SPACING) * 6 - 150;
const YEAR_Y = -200;
let COLOR_WEEKEND = "#c3abf3";
let COLOR_WEEKDAY = "#ffffff";
const COLOR_DAY_STROKE = "none";
let STROKE_DAY = 4;
let FILLSTYLE_DAY = "solid";
const FONT_SIZE_MONTH = 60;
const FONT_SIZE_DAY = 30;
const FONT_SIZE_YEAR = 100;
const LINE_STROKE_SIZE = 4;
let LINE_STROKE_COLOR_WEEKDAY = "black";
let LINE_STROKE_COLOR_WEEKEND = "black";
const SATURDAY = 6;
const SUNDAY = 0;
const JANUARY = 0;
const FIRST_DAY_OF_THE_MONTH = 1;
const DAY_NAME_AND_NUMBER_Y_MARGIN = (RECT_HEIGHT - FONT_SIZE_DAY) / 2;
// -------------------------------------
// ask for requested Year
// Default value is the current year
let requestedYear = parseFloat(new Date().getFullYear());
requestedYear = parseFloat(await utils.inputPrompt("Year ?", requestedYear, requestedYear));
if(isNaN(requestedYear)) {
new Notice("Invalid number");
return;
}
// -------------------------------------
// Use selected element for the calendar style
// -------------------------------------
let elements = ea.getViewSelectedElements();
if (elements.length>=1){
COLOR_WEEKDAY = elements[0].backgroundColor;
FILLSTYLE_DAY = elements[0].fillStyle;
STROKE_DAY = elements[0].strokeWidth;
LINE_STROKE_COLOR_WEEKDAY = elements[0].strokeColor;
}
if (elements.length>=2){
COLOR_WEEKEND = elements[1].backgroundColor;
LINE_STROKE_COLOR_WEEKEND = elements[1].strokeColor;
}
// get the first day of the current year (01/01)
var firstDayOfYear = new Date(requestedYear, JANUARY, FIRST_DAY_OF_THE_MONTH);
var currentDay = firstDayOfYear
// write year number
let calendarYear = firstDayOfYear.getFullYear();
ea.style.fontSize = FONT_SIZE_YEAR;
ea.addText(START_X + YEAR_X, START_Y + YEAR_Y, String(calendarYear));
// while we do not reach the end of the year iterate on all the day of the current year
do {
var curentDayOfTheMonth = currentDay.getDate();
var currentMonth = currentDay.getMonth();
var isWeekend = currentDay.getDay() == SATURDAY || currentDay.getDay() == SUNDAY;
// set background color if it's a weekend or weekday
ea.style.backgroundColor = isWeekend ? COLOR_WEEKEND : COLOR_WEEKDAY ;
ea.style.strokeColor = COLOR_DAY_STROKE;
ea.style.fillStyle = FILLSTYLE_DAY;
ea.style.strokeWidth = STROKE_DAY;
let x = START_X + currentMonth * (RECT_WIDTH + MONTH_SPACING);
let y = START_Y + curentDayOfTheMonth * (RECT_HEIGHT + DAY_SPACING);
// only one time per month
if(curentDayOfTheMonth == FIRST_DAY_OF_THE_MONTH) {
// add month name
ea.style.fontSize = FONT_SIZE_MONTH;
ea.addText(x + DAY_NAME_AND_NUMBER_X_MARGIN, START_Y+MONTH_NAME_SPACING, currentDay.toLocaleString('default', { month: 'long' }));
}
// Add day rectangle
ea.style.fontSize = FONT_SIZE_DAY;
ea.addRect(x, y, RECT_WIDTH, RECT_HEIGHT);
// set stroke color based on weekday
ea.style.strokeColor = isWeekend ? LINE_STROKE_COLOR_WEEKEND : LINE_STROKE_COLOR_WEEKDAY;
// add line between days
//ea.style.strokeColor = LINE_STROKE_COLOR_WEEKDAY;
ea.style.strokeWidth = LINE_STROKE_SIZE;
ea.addLine([[x,y],[x+RECT_WIDTH, y]]);
// add day number
ea.addText(x + DAY_NAME_AND_NUMBER_X_MARGIN, y + DAY_NAME_AND_NUMBER_Y_MARGIN, String(curentDayOfTheMonth));
// add day name
ea.addText(x + DAY_NAME_AND_NUMBER_X_MARGIN + DAY_NAME_SPACING, y + DAY_NAME_AND_NUMBER_Y_MARGIN, String(currentDay.toLocaleString('default', { weekday: 'narrow' })));
// go to the next day
currentDay.setDate(currentDay.getDate() + 1);
} while (!(currentDay.getMonth() == JANUARY && currentDay.getDate() == FIRST_DAY_OF_THE_MONTH)) // stop if we reach the 01/01 of the next year
await ea.addElementsToView(false, false, true);

View File

@@ -0,0 +1,10 @@
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 50.097575068473816 56.100231877975375" width="50.097575068473816" height="56.100231877975375" class="excalidraw-svg">
<!-- svg-source:excalidraw -->
<defs>
<style class="style-fonts">
@font-face { font-family: Nunito; src: url(data:font/woff2;base64,d09GMgABAAAAAAN4AA8AAAAABswAAAMeAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGhYbgQwcLgZgP1NUQVREAEQRCAqCMIIHCwoAATYCJAMQBCAFhCQHIBuUBcguChxjano4VFxM2vQ1QySkcTXuPsXzT/vRzn0z64qIV7yJJ/EmGSp5Q0EalEiqlgiVyv/L7uv1Qpy/pAfsqfk9Cx44ly4UHRVC2VW+YH51qZj/wAD1eZu/7e/zfzNtkm0LNEs8kV4W4FwgUb7BEox+1s0hcpt7B9o7UD9zWyypoLCgyLAoF3TIGFjCt/9zgmlAiWgimHRUDsvrVQ0d4PV6RpPA690oUcCLFQz/C/KW1hSwQxBdGRfjymFWuCiNpQ7DZwDDscLwyUUTpYlomhJRStJMOi7BBt/PBqzAjvfKyhfW1JZFD4AbvUueQVDCsDDk3yTfBN7BFQ3t838A/UISdgP6BED+1nvsZim+dJYbtD/zgeUIAj7ZZBGCWGbFzydiAggCGUFfAAoNyywFy6ycBsbpAlxRrmEgQGPIthJUmvOuGydAT4H3YPkMSmTbaOxSp3F7M1DceuB47Z4/v7F+08G4H9CV65T/cP2u2BPnHPNo1Njby9l9lqCzm7uyMRu86fNiz8HY2fFxdzcm+/lrt24Fx+vVjuOWhqzPHlHBaqZuP3PXoc7G406+6sZfeO7ufn05DaoJRR5e5HzVrTsP79AzLra5Dm2pRJjYVsGw41e6W67j9sQLnUPzqc0Fimnx1xZHPf0V1+aX3CiUZM329mTN07WNyR3+eb++hXAzh+fUMLjgc9WH8Z3yyaPxrSryLvf0qvAGgKB9DTasXLeHXQv+jTfLj/DT99Yu4E/arcz/tgqsErobMKpAeLRyF0C2LRAeQ88XaXVqd92A/IrQCaF7wtobVgIAumDKFlhKtwe+w49BxmkfyDLrB9lcN1numByxaYAYdVJSCoFpewlG+CpsjU9+k4kD7sNkoxSNaBN4OlktYpSEN64bjcfiEE10Ch6BVZpGaEY1oGqiArsLTWOeiiko6ZJkSZEm3HxNmjWpzFMZbutn6SSjtL1mKvApcDlMNUNDv0uTIlUGSgcOjVL8TAsNJqCNIyildAQH05hRYnAIQmWWJ1kyFl8W6cYkGYfJCxXDbbqExsAUhFky5ZIfyxKLDU8LAgAA); }
</style>
</defs>
<g stroke-linecap="round"><g transform="translate(17.077535418245503 32.086027259866626) rotate(0 -6.0775204356467825 0)"><path d="M0 0 C-2.03 0, -10.13 0, -12.16 0 M0 0 C-2.03 0, -10.13 0, -12.16 0" stroke="#1e1e1e" stroke-width="4" fill="none"></path></g></g><mask></mask><g stroke-linecap="round"><g transform="translate(16.865850031647774 39.07185193521212) rotate(0 -6.077520435646779 0)"><path d="M0 0 C-2.03 0, -10.13 0, -12.16 0 M0 0 C-2.03 0, -10.13 0, -12.16 0" stroke="#1e1e1e" stroke-width="4" fill="none"></path></g></g><mask></mask><g stroke-linecap="round"><g transform="translate(16.946763254178506 46.743160072731996) rotate(0 -6.0775204356467825 0)"><path d="M0 0 C-2.03 0, -10.13 0, -12.16 0 M0 0 C-2.03 0, -10.13 0, -12.16 0" stroke="#1e1e1e" stroke-width="4" fill="none"></path></g></g><mask></mask><g stroke-linecap="round"><g transform="translate(16.680839244467208 53.831041680294504) rotate(0 -6.0775204356467825 0)"><path d="M0 0 C-2.03 0, -10.13 0, -12.16 0 M0 0 C-2.03 0, -10.13 0, -12.16 0" stroke="#1e1e1e" stroke-width="4" fill="none"></path></g></g><mask></mask><g stroke-linecap="round"><g transform="translate(47.41441860670051 32.80666516147113) rotate(0 -6.077520435646779 0)"><path d="M0 0 C-2.03 0, -10.13 0, -12.16 0 M0 0 C-2.03 0, -10.13 0, -12.16 0" stroke="#1e1e1e" stroke-width="4" fill="none"></path></g></g><mask></mask><g stroke-linecap="round"><g transform="translate(47.20273322010279 39.79248983681666) rotate(0 -6.077520435646779 0)"><path d="M0 0 C-2.03 0, -10.13 0, -12.16 0 M0 0 C-2.03 0, -10.13 0, -12.16 0" stroke="#1e1e1e" stroke-width="4" fill="none"></path></g></g><mask></mask><g transform="translate(0 0) rotate(0 25.048787534236908 17.010119812063635)"><text x="0" y="25.30097820935094" font-family="Nunito, Segoe UI Emoji" font-size="25.200177499353526px" fill="#1e1e1e" text-anchor="start" style="white-space: pre;" direction="ltr" dominant-baseline="alphabetic">CAL</text></g><g stroke-linecap="round"><g transform="translate(26.079917947816256 27.03803518092093) rotate(0 0 14.531098348527223)"><path d="M0 0 C0 4.84, 0 24.22, 0 29.06 M0 0 C0 4.84, 0 24.22, 0 29.06" stroke="#1e1e1e" stroke-width="4" fill="none"></path></g></g><mask></mask><g stroke-linecap="round"><g transform="translate(47.199941306131535 47.15190785557627) rotate(0 -6.077520435646779 0)"><path d="M0 0 C-2.03 0, -10.13 0, -12.16 0 M0 0 C-2.03 0, -10.13 0, -12.16 0" stroke="#1e1e1e" stroke-width="4" fill="none"></path></g></g><mask></mask></svg>

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

@@ -7,16 +7,41 @@ This script enables the selection of elements based on matching properties. Sele
```js */
let config = window.ExcalidrawSelectConfig;
config = Boolean(config) && (Date.now() - config.timestamp < 60000) ? config : null;
const isValidConfig = config && (Date.now() - config.timestamp < 60000);
config = isValidConfig ? config : null;
let elements = ea.getViewSelectedElements();
if(!config && (elements.length !==1)) {
new Notice("Select a single element");
return;
} else {
if(elements.length === 0) {
elements = ea.getViewElements();
if(!config) {
async function shouldAbort() {
if(elements.length === 1) return false;
if(elements.length !== 2) return true;
//maybe container?
const textEl = elements.find(el=>el.type==="text");
if(!textEl || !textEl.containerId) return true;
const containerEl = elements.find(el=>el.id === textEl.containerId);
if(!containerEl) return true;
const id = await utils.suggester(
elements.map(el=>el.type),
elements.map(el=>el.id),
"Select container component"
);
if(!id) return true;
elements = elements.filter(el=>el.id === id);
return false;
}
if(await shouldAbort()) {
new Notice("Select a single element");
return;
}
}
if(Boolean(config) && elements.length === 0) {
elements = ea.getViewElements();
}
const {angle, backgroundColor, fillStyle, fontFamily, fontSize, height, width, opacity, roughness, roundness, strokeColor, strokeStyle, strokeWidth, type, startArrowhead, endArrowhead, fileId} = ea.getViewSelectedElement();

View File

@@ -17,4 +17,5 @@ if(isNaN(width)) {
const elements=ea.getViewSelectedElements();
ea.copyViewElementsToEAforEditing(elements);
ea.getElements().forEach((el)=>el.strokeWidth=width);
ea.addElementsToView(false,false);
await ea.addElementsToView(false,false);
ea.viewUpdateScene({appState: {currentItemStrokeWidth: width}});

View File

@@ -18,6 +18,7 @@ if (!ellipse) return;
let lines = elements.filter(el => el.type == "line" || el.type == "arrow");
if (lines.length == 0) lines = ea.getViewElements().filter(el => el.type == "line" || el.type == "arrow");
lines = lines.map(getNormalizedLine);
const subLines = getSubLines(lines);
const angles = subLines.flatMap(line => {
@@ -206,3 +207,70 @@ function isBetween(num, min, max) {
function clamp(number, min, max) {
return Math.max(min, Math.min(number, max));
}
//Same line but with angle=0
function getNormalizedLine(originalElement) {
if(originalElement.angle === 0) return originalElement;
// Get absolute coordinates for all points first
const pointRotateRads = (point, center, angle) => {
const [x, y] = point;
const [cx, cy] = center;
return [
(x - cx) * Math.cos(angle) - (y - cy) * Math.sin(angle) + cx,
(x - cx) * Math.sin(angle) + (y - cy) * Math.cos(angle) + cy
];
};
// Get element absolute coordinates (matching Excalidraw's approach)
const getElementAbsoluteCoords = (element) => {
const points = element.points;
let minX = Infinity;
let minY = Infinity;
let maxX = -Infinity;
let maxY = -Infinity;
for (const [x, y] of points) {
const absX = x + element.x;
const absY = y + element.y;
minX = Math.min(minX, absX);
minY = Math.min(minY, absY);
maxX = Math.max(maxX, absX);
maxY = Math.max(maxY, absY);
}
return [minX, minY, maxX, maxY];
};
// Calculate center point based on absolute coordinates
const [x1, y1, x2, y2] = getElementAbsoluteCoords(originalElement);
const centerX = (x1 + x2) / 2;
const centerY = (y1 + y2) / 2;
// Calculate absolute coordinates of all points
const absolutePoints = originalElement.points.map(([x, y]) => [
x + originalElement.x,
y + originalElement.y
]);
// Rotate all points around the center
const rotatedPoints = absolutePoints.map(point =>
pointRotateRads(point, [centerX, centerY], originalElement.angle)
);
// Convert back to relative coordinates
const newPoints = rotatedPoints.map(([x, y]) => [
x - rotatedPoints[0][0],
y - rotatedPoints[0][1]
]);
const newLineId = ea.addLine(newPoints);
// Set the position of the new line to the first rotated point
const newLine = ea.getElement(newLineId);
newLine.x = rotatedPoints[0][0];
newLine.y = rotatedPoints[0][1];
newLine.angle = 0;
delete ea.elementsDict[newLine.id];
return newLine;
}

File diff suppressed because one or more lines are too long

View File

@@ -148,6 +148,7 @@ I would love to include your contribution in the script library. If you have a s
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Add%20Next%20Step%20in%20Process.svg"/></div>|[[#Add Next Step in Process]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Convert%20freedraw%20to%20line.svg"/></div>|[[#Convert freedraw to line]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Deconstruct%20selected%20elements%20into%20new%20drawing.svg"/></div>|[[#Deconstruct selected elements into new drawing]]|
|<div><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Full-Year%20Calendar%20Generator.svg"/></div>|[[#Full-Year Calendar Generator]]|
## Masking and cropping
**Keywords**: Crop, Mask, Transform images
@@ -394,6 +395,12 @@ https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea
```
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/zsviczian'>@zsviczian</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Excalidraw%20Writing%20Machine.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">Creates a hierarchical Markdown document out of a visual layout of an article that can be fed to Templater and converted into an article using AI for Templater.<br>Watch this video to understand how the script is intended to work:<br><iframe width="400" height="225" src="https://www.youtube.com/embed/zvRpCOZAUSs" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe><br>You can download the sample Obsidian Templater file from <a href="https://gist.github.com/zsviczian/bf49d4b2d401f5749aaf8c2fa8a513d9">here</a>. You can download the demo PDF document showcased in the video from <a href="https://zsviczian.github.io/DemoArticle-AtomicHabits.pdf">here</a>.</td></tr></table>
## Full-Year Calendar Generator
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/Full-Year%20Calendar%20Generator.md
```
<table><tr valign='top'><td class="label">Author</td><td class="data"><a href='https://github.com/simonperet'>@simonperet</a></td></tr><tr valign='top'><td class="label">Source</td><td class="data"><a href='https://github.com/zsviczian/obsidian-excalidraw-plugin/blob/master/ea-scripts/Full-Year%20Calendar%20Generator.md'>File on GitHub</a></td></tr><tr valign='top'><td class="label">Description</td><td class="data">Generates a complete calendar for a specified year.<br><img src='https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/scripts-full-year-calendar-exemple.excalidraw.png'></td></tr></table>
## GPT Draw-a-UI
```excalidraw-script-install
https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/ea-scripts/GPT-Draw-a-UI.md

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 KiB

View File

@@ -1,7 +1,7 @@
{
"id": "obsidian-excalidraw-plugin",
"name": "Excalidraw",
"version": "2.7.4",
"version": "2.7.6-beta-1",
"minAppVersion": "1.1.6",
"description": "An Obsidian plugin to edit and view Excalidraw drawings",
"author": "Zsolt Viczian",

View File

@@ -1,7 +1,7 @@
{
"id": "obsidian-excalidraw-plugin",
"name": "Excalidraw",
"version": "2.7.4",
"version": "2.7.5",
"minAppVersion": "1.1.6",
"description": "An Obsidian plugin to edit and view Excalidraw drawings",
"author": "Zsolt Viczian",

9418
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -24,7 +24,7 @@
"license": "MIT",
"dependencies": {
"@popperjs/core": "^2.11.8",
"@zsviczian/excalidraw": "0.17.6-24",
"@zsviczian/excalidraw": "0.17.6-26",
"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"
},

View File

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

View File

@@ -198,7 +198,7 @@ export const REG_BLOCK_REF_CLEAN = /[!"#$%&()*+,.:;<=>?@^`{|}~\/\[\]\\\r\n]/g;
// /[!"#$%&()*+,.:;<=>?@^`{|}~\/\[\]\\]/g;
// https://discord.com/channels/686053708261228577/989603365606531104/1000128926619816048
// /\+|\/|~|=|%|\(|\)|{|}|,|&|\.|\$|!|\?|;|\[|]|\^|#|\*|<|>|&|@|\||\\|"|:|\s/g;
export const IMAGE_TYPES = ["jpeg", "jpg", "png", "gif", "svg", "webp", "bmp", "ico", "jtif", "tif"];
export const IMAGE_TYPES = ["jpeg", "jpg", "png", "gif", "svg", "webp", "bmp", "ico", "jtif", "tif", "jfif", "avif"];
export const ANIMATED_IMAGE_TYPES = ["gif", "webp", "apng", "svg"];
export const EXPORT_TYPES = ["svg", "dark.svg", "light.svg", "png", "dark.png", "light.png"];
export const MAX_IMAGE_SIZE = 500;

View File

@@ -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

View File

@@ -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 {

View File

@@ -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";
@@ -893,7 +893,12 @@ export const markdownPostProcessor = async (
el.firstElementChild?.hasClass("frontmatter") ||
el.firstElementChild?.hasClass("block-language-yaml");
if(isPrinting && isFrontmatter) {
return;
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/2184
const file = app.vault.getAbstractFileByPath(ctx.sourcePath);
const isDrawing = file && file instanceof TFile && plugin.isExcalidrawFile(file);
if(isDrawing) {
return;
}
}
//@ts-ignore

View File

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

View File

@@ -386,13 +386,14 @@ FILENAME_HEAD: "Filename",
"This setting will not affect the display of the drawing when you are in Excalidraw mode or when you embed the drawing into a markdown document or when rendering hover preview.<br><ul>" +
"<li>See other related setting for <a href='#"+TAG_PDFEXPORT+"'>PDF Export</a> under 'Embedding and Exporting' further below.</li></ul><br>" +
"You must close the active excalidraw/markdown file and reopen it for this change to take effect.",
SHOW_DRAWING_OR_MD_IN_EXPORTPDF_NAME: "Render the file as an image when exporting an Excalidraw file to PDF",
SHOW_DRAWING_OR_MD_IN_EXPORTPDF_NAME: "Render Excalidraw as Image in Obsidian PDF Export",
SHOW_DRAWING_OR_MD_IN_EXPORTPDF_DESC:
"This setting controls the behavior of Excalidraw when exporting an Excalidraw file to PDF in markdown view mode using Obsidian's <b>Export to PDF</b> feature.<br>" +
"<ul><li>When <b>enabled</b> the PDF will show the Excalidraw drawing only;</li>" +
"<li>When <b>disabled</b> the PDF will show the markdown side of the document.</li></ul>" +
"See the other related setting for <a href='#"+TAG_MDREADINGMODE+"'>Markdown Reading Mode</a> under 'Appearnace and Behavior' further above.<br>" +
"⚠️ Note, you must close the active excalidraw/markdown file and reopen for this change to take effect. ⚠️",
"This setting controls how Excalidraw files are exported to PDF using Obsidian's built-in <b>Export to PDF</b> feature.<br>" +
"<ul><li><b>Enabled:</b> The PDF will include the Excalidraw drawing as an image.</li>" +
"<li><b>Disabled:</b> The PDF will include the markdown content as text.</li></ul>" +
"Note: This setting does not affect the PDF export feature within Excalidraw itself.<br>" +
"See the other related setting for <a href='#"+TAG_MDREADINGMODE+"'>Markdown Reading Mode</a> under 'Appearance and Behavior' further above.<br>" +
"⚠️ You must close and reopen the Excalidraw/markdown file for changes to take effect. ⚠️",
HOTKEY_OVERRIDE_HEAD: "Hotkey overrides",
HOTKEY_OVERRIDE_DESC: `Some of the Excalidraw hotkeys such as <code>${labelCTRL()}+Enter</code> to edit text or <code>${labelCTRL()}+K</code> to create an element link ` +
"conflict with Obsidian hotkey settings. The hotkey combinations you add below will override Obsidian's hotkey settings while useing Excalidraw, thus " +
@@ -661,6 +662,7 @@ FILENAME_HEAD: "Filename",
EXPORT_EMBED_SCENE_DESC:
"Embed Excalidraw scene in exported image. Can be overridden at a file level by adding the <code>excalidraw-export-embed-scene: true/false<code> frontmatter key. " +
"The setting only takes effect the next time you (re)open drawings.",
PDF_EXPORT_SETTINGS: "PDF Export Settings",
EXPORT_HEAD: "Auto-export Settings",
EXPORT_SYNC_NAME:
"Keep the .SVG and/or .PNG filenames in sync with the drawing file",
@@ -1006,4 +1008,75 @@ 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_FIT_TO_PAGE: "Page Fitting",
EXPORTDIALOG_PDF_FIT_OPTION: "Fit to page",
EXPORTDIALOG_PDF_SCALE_OPTION: "Use image scale (may span multiple pages)",
EXPORTDIALOG_PDF_PAPER_COLOR: "Paper Color",
EXPORTDIALOG_PDF_PAPER_WHITE: "White",
EXPORTDIALOG_PDF_PAPER_SCENE: "Use scene color",
EXPORTDIALOG_PDF_PAPER_CUSTOM: "Custom color",
EXPORTDIALOG_PDF_ALIGNMENT: "Position on Page",
EXPORTDIALOG_PDF_ALIGN_CENTER: "Center",
EXPORTDIALOG_PDF_ALIGN_TOP_LEFT: "Top Left",
EXPORTDIALOG_PDF_ALIGN_TOP_CENTER: "Top Center",
EXPORTDIALOG_PDF_ALIGN_TOP_RIGHT: "Top Right",
EXPORTDIALOG_PDF_ALIGN_BOTTOM_LEFT: "Bottom Left",
EXPORTDIALOG_PDF_ALIGN_BOTTOM_CENTER: "Bottom Center",
EXPORTDIALOG_PDF_ALIGN_BOTTOM_RIGHT: "Bottom Right",
EXPORTDIALOG_PDF_MARGIN: "Margin",
EXPORTDIALOG_PDF_MARGIN_NONE: "None",
EXPORTDIALOG_PDF_MARGIN_TINY: "Small",
EXPORTDIALOG_PDF_MARGIN_NORMAL: "Normal",
EXPORTDIALOG_SAVE_PDF_SETTINGS: "Save PDF settings",
EXPORTDIALOG_SAVE_CONFIRMATION: "PDF config saved to plugin settings as default",
// Buttons
EXPORTDIALOG_PNGTOFILE : "Export PNG",
EXPORTDIALOG_SVGTOFILE : "Export SVG",
EXPORTDIALOG_PNGTOVAULT : "PNG to Vault",
EXPORTDIALOG_SVGTOVAULT : "SVG to Vault",
EXPORTDIALOG_EXCALIDRAW: "Excalidraw",
EXPORTDIALOG_PNGTOCLIPBOARD : "PNG to Clipboard",
EXPORTDIALOG_SVGTOCLIPBOARD : "SVG to Clipboard",
EXPORTDIALOG_PDF: "Export PDF",
EXPORTDIALOG_PDFTOVAULT: "PDF to Vault",
};

View File

@@ -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);
}

View File

@@ -1,11 +1,16 @@
import { ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/excalidraw/types";
import { Modal, Setting, TFile } from "obsidian";
import { Modal, Notice, Setting, TFile, ButtonComponent } from "obsidian";
import { getEA } from "src/core";
import { DEVICE } from "src/constants/constants";
import { ExcalidrawAutomate } from "src/shared/ExcalidrawAutomate";
import ExcalidrawView from "src/view/ExcalidrawView";
import ExcalidrawPlugin from "src/core/main";
import { fragWithHTML, getExportPadding, getExportTheme, getPNGScale, getWithBackground, shouldEmbedScene } from "src/utils/utils";
import { PageOrientation, PageSize, PDFMargin, PDFPageAlignment, PDFPageMarginString, STANDARD_PAGE_SIZES, exportSVGToClipboard } from "src/utils/exportUtils";
import { t } from "src/lang/helpers";
import { PDFExportSettings, PDFExportSettingsComponent } from "./PDFExportSettingsComponent";
export class ExportDialog extends Modal {
private ea: ExcalidrawAutomate;
@@ -27,6 +32,17 @@ export class ExportDialog extends Modal {
public embedScene: boolean;
public exportSelectedOnly: boolean;
public saveToVault: boolean;
public pageSize: PageSize = "A4";
public pageOrientation: PageOrientation = "portrait";
private activeTab: "image" | "pdf" = "image";
private contentContainer: HTMLDivElement;
private buttonContainerRow1: HTMLDivElement;
private buttonContainerRow2: HTMLDivElement;
public fitToPage: boolean = true;
public paperColor: "white" | "scene" | "custom" = "white";
public customPaperColor: string = "#ffffff";
public alignment: PDFPageAlignment = "center";
public margin: PDFPageMarginString = "normal";
constructor(
private plugin: ExcalidrawPlugin,
@@ -44,6 +60,15 @@ 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.saveSettings = false;
}
@@ -62,7 +87,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 +98,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 +200,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 +213,213 @@ 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
};
new PDFExportSettingsComponent(
this.contentContainer,
pdfSettings,
() => {
this.pageSize = pdfSettings.pageSize;
this.pageOrientation = pdfSettings.pageOrientation;
this.fitToPage = pdfSettings.fitToPage;
this.paperColor = pdfSettings.paperColor;
this.customPaperColor = pdfSettings.customPaperColor;
this.alignment = pdfSettings.alignment;
this.margin = pdfSettings.margin;
}
).render();
}
private createImageButtons() {
if(DEVICE.isDesktop) {
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
};
await this.plugin.saveSettings();
new Notice(t("EXPORTDIALOG_SAVE_CONFIRMATION"));
};
const bPDFVault = this.buttonContainerRow1.createEl("button", {
text: t("EXPORTDIALOG_PDFTOVAULT"),
cls: "excalidraw-export-button"
});
bPDFVault.onclick = () => {
this.view.exportPDF(
true,
this.hasSelectedElements && this.exportSelectedOnly,
this.pageSize,
this.pageOrientation
);
this.close();
};
if (!DEVICE.isDesktop) return;
const bPDFExport = this.buttonContainerRow1.createEl("button", {
text: t("EXPORTDIALOG_PDF"),
cls: "excalidraw-export-button"
});
bPDFExport.onclick = () => {
this.view.exportPDF(
false,
this.hasSelectedElements && this.exportSelectedOnly,
this.pageSize,
this.pageOrientation
);
this.close();
};
}
public getPaperColor(): string {
switch (this.paperColor) {
case "white": return "#ffffff";
case "scene": return this.api.getAppState().viewBackgroundColor;
case "custom": return this.customPaperColor;
default: return "#ffffff";
}
}
}

View File

@@ -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

View File

@@ -0,0 +1,138 @@
import { Setting } from "obsidian";
import { PageOrientation, PageSize, PDFPageAlignment, PDFPageMarginString, STANDARD_PAGE_SIZES } from "src/utils/exportUtils";
import { t } from "src/lang/helpers";
export interface PDFExportSettings {
pageSize: PageSize;
pageOrientation: PageOrientation;
fitToPage: boolean;
paperColor: "white" | "scene" | "custom";
customPaperColor: string;
alignment: PDFPageAlignment;
margin: PDFPageMarginString;
}
export class PDFExportSettingsComponent {
constructor(
private contentEl: HTMLElement,
private settings: PDFExportSettings,
private update?: Function,
) {
if (!update) this.update = () => {};
}
render() {
const pageSizeOptions: Record<string, string> = Object.keys(STANDARD_PAGE_SIZES)
.reduce((acc, key) => ({
...acc,
[key]: key
}), {});
new Setting(this.contentEl)
.setName(t("EXPORTDIALOG_PAGE_SIZE"))
.addDropdown(dropdown =>
dropdown
.addOptions(pageSizeOptions)
.setValue(this.settings.pageSize)
.onChange(value => {
this.settings.pageSize = value as PageSize;
this.update();
})
);
new Setting(this.contentEl)
.setName(t("EXPORTDIALOG_PAGE_ORIENTATION"))
.addDropdown(dropdown =>
dropdown
.addOptions({
"portrait": t("EXPORTDIALOG_ORIENTATION_PORTRAIT"),
"landscape": t("EXPORTDIALOG_ORIENTATION_LANDSCAPE")
})
.setValue(this.settings.pageOrientation)
.onChange(value => {
this.settings.pageOrientation = value as PageOrientation;
this.update();
})
);
new Setting(this.contentEl)
.setName(t("EXPORTDIALOG_PDF_FIT_TO_PAGE"))
.addDropdown(dropdown =>
dropdown
.addOptions({
"fit": t("EXPORTDIALOG_PDF_FIT_OPTION"),
"scale": t("EXPORTDIALOG_PDF_SCALE_OPTION")
})
.setValue(this.settings.fitToPage ? "fit" : "scale")
.onChange(value => {
this.settings.fitToPage = value === "fit";
this.update();
})
);
new Setting(this.contentEl)
.setName(t("EXPORTDIALOG_PDF_MARGIN"))
.addDropdown(dropdown =>
dropdown
.addOptions({
"none": t("EXPORTDIALOG_PDF_MARGIN_NONE"),
"tiny": t("EXPORTDIALOG_PDF_MARGIN_TINY"),
"normal": t("EXPORTDIALOG_PDF_MARGIN_NORMAL")
})
.setValue(this.settings.margin)
.onChange(value => {
this.settings.margin = value as PDFPageMarginString;
this.update();
})
);
const paperColorSetting = new Setting(this.contentEl)
.setName(t("EXPORTDIALOG_PDF_PAPER_COLOR"))
.addDropdown(dropdown =>
dropdown
.addOptions({
"white": t("EXPORTDIALOG_PDF_PAPER_WHITE"),
"scene": t("EXPORTDIALOG_PDF_PAPER_SCENE"),
"custom": t("EXPORTDIALOG_PDF_PAPER_CUSTOM")
})
.setValue(this.settings.paperColor)
.onChange(value => {
this.settings.paperColor = value as typeof this.settings.paperColor;
colorInput.style.display = (value === "custom") ? "block" : "none";
this.update();
})
);
const colorInput = paperColorSetting.controlEl.createEl("input", {
type: "color",
value: this.settings.customPaperColor
});
colorInput.style.width = "50px";
colorInput.style.marginLeft = "10px";
colorInput.style.display = this.settings.paperColor === "custom" ? "block" : "none";
colorInput.addEventListener("change", (e) => {
this.settings.customPaperColor = (e.target as HTMLInputElement).value;
this.update();
});
new Setting(this.contentEl)
.setName(t("EXPORTDIALOG_PDF_ALIGNMENT"))
.addDropdown(dropdown =>
dropdown
.addOptions({
"center": t("EXPORTDIALOG_PDF_ALIGN_CENTER"),
"top-left": t("EXPORTDIALOG_PDF_ALIGN_TOP_LEFT"),
"top-center": t("EXPORTDIALOG_PDF_ALIGN_TOP_CENTER"),
"top-right": t("EXPORTDIALOG_PDF_ALIGN_TOP_RIGHT"),
"bottom-left": t("EXPORTDIALOG_PDF_ALIGN_BOTTOM_LEFT"),
"bottom-center": t("EXPORTDIALOG_PDF_ALIGN_BOTTOM_CENTER"),
"bottom-right": t("EXPORTDIALOG_PDF_ALIGN_BOTTOM_RIGHT")
})
.setValue(this.settings.alignment)
.onChange(value => {
this.settings.alignment = value as PDFPageAlignment;
this.update();
})
);
}
}

View File

@@ -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";

View File

@@ -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,62 @@ 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" +
"\n" +
"@example\n" +
"const pdfData = await createPDF({\n" +
" SVG: [svgElement1, svgElement2],\n" +
" scale: { fitToPage: true },\n" +
" pageProps: {\n" +
" dimensions: { width: 595.28, height: 841.89 },\n" +
" backgroundColor: \"#ffffff\",\n" +
" margin: { left: 20, right: 20, top: 20, bottom: 20 },\n" +
" alignment: \"center\"\n" +
" }\n" +
"});",
},
{
field: "getPagePDFDimensions",
code: "getPagePDFDimensions(pageSize: PageSize, orientation: PageOrientation): PageDimensions",
desc: "Returns the dimensions of a standard page size in points (pt).\n" +
"\n" +
"@param {PageSize} pageSize - The standard page size. Possible values are \"A0\", \"A1\", \"A2\", \"A3\", \"A4\", \"A5\", \"Letter\", \"Legal\", \"Tabloid\".\n" +
"@param {PageOrientation} orientation - The orientation of the page. Possible values are \"portrait\" and \"landscape\".\n" +
"@returns {PageDimensions} - An object containing the width and height of the page in points (pt).\n" +
"\n" +
"@typedef {Object} PageDimensions\n" +
"@property {number} width - The width of the page in points (pt).\n" +
"@property {number} height - The height of the page in points (pt).\n" +
"\n" +
"@typedef {\"A0\" | \"A1\" | \"A2\" | \"A3\" | \"A4\" | \"A5\" | \"Letter\" | \"Legal\" | \"Tabloid\"} PageSize\n" +
"\n" +
"@typedef {\"portrait\" | \"landscape\"} PageOrientation\n" +
"\n" +
"@example\n" +
"const dimensions = getPDFPageDimensions(\"A4\", \"portrait\");\n" +
"console.log(dimensions); // { width: 595.28, height: 841.89 }",
after: "",
},
{
field: "createPNG",
code: "async createPNG(templatePath?: string, scale?: number, exportSettings?: ExportSettings, loader?: EmbeddedFilesLoader, theme?: string,padding?: number): Promise<any>;",

View File

@@ -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

View File

@@ -649,7 +649,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),
@@ -1569,13 +1568,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 +1592,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 +1993,7 @@ export class ExcalidrawData {
isLocalLink: data.isLocalLink,
path: data.hyperlink,
blockrefData: null,
hasSVGwithBitmap: data.isSVGwithBitmap
hasSVGwithBitmap: data.isSVGwithBitmap,
});
return;
}

View File

@@ -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 {

View File

@@ -1,4 +1,4 @@
import { Notice, RequestUrlResponse, requestUrl } from "obsidian";
import { Notice, RequestUrlResponse } from "obsidian";
import ExcalidrawPlugin from "src/core/main";
type MessageContent =

View File

@@ -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)}`;
}

View File

@@ -1,7 +1,7 @@
import { NonDeletedExcalidrawElement } from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { DEVICE, REG_LINKINDEX_INVALIDCHARS } from "src/constants/constants";
import { getParentOfClass } from "./obsidianUtils";
import { TFile, WorkspaceLeaf } from "obsidian";
import { App, TFile, WorkspaceLeaf } from "obsidian";
import { getLinkParts } from "./utils";
import ExcalidrawView from "src/view/ExcalidrawView";
@@ -55,4 +55,15 @@ export const generateEmbeddableLink = (src: string, theme: "light" | "dark"):str
}
}*/
return src;
}
export function setFileToLocalGraph(app: App, file: TFile) {
let lgv;
app.workspace.iterateAllLeaves((l) => {
if (l.view?.getViewType() === "localgraph") lgv = l.view;
});
if (lgv) {
//@ts-ignore
lgv.loadFile(file);
}
}

View 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";

View File

@@ -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;
};

375
src/utils/exportUtils.ts Normal file
View File

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

View File

@@ -290,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);
}
}

View File

@@ -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,

View File

@@ -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;

View File

@@ -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;
@@ -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: {
...this.exportDialog.fitToPage
? { fitToPage: true }
: { zoom: this.exportDialog.scale, fitToPage: false },
},
pageProps: {
dimensions: getPageDimensions(pageSize, orientation),
backgroundColor: this.exportDialog.getPaperColor(),
margin: getMarginValue(this.exportDialog.margin),
alignment: this.exportDialog.alignment,
}
});
if (!pdfArrayBuffer) {
return;
}
if(toVault) {
const filepath = getIMGFilename(this.file.path, "pdf");
const file = await createOrOverwriteFile(this.app, filepath, pdfArrayBuffer);
let leaf: WorkspaceLeaf;
this.app.workspace.getLeavesOfType("pdf").forEach((l) => {
//@ts-ignore
if(l.view?.file === file) {
leaf = l;
}
});
if(leaf) {
this.app.workspace.revealLeaf(leaf);
} else {
this.app.workspace.getLeaf("split").openFile(file);
}
} else {
download(
"data:application/pdf;base64",
arrayBufferToBase64(pdfArrayBuffer),
`${this.file.basename}.pdf`
);
}
}
public async png(scene: any, theme?:string, embedScene?: boolean): Promise<Blob> {
(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() {
@@ -3322,19 +3402,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 +3483,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 +3523,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 +4095,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;

View File

@@ -6,7 +6,7 @@ import { ConstructableWorkspaceSplit, getContainerForDocument, isObsidianThemeDa
import { DEVICE, EXTENDED_EVENT_TYPES, KEYBOARD_EVENT_TYPES } from "src/constants/constants";
import { ExcalidrawImperativeAPI, UIAppState } from "@zsviczian/excalidraw/types/excalidraw/types";
import { ObsidianCanvasNode } from "src/view/managers/CanvasNodeFactory";
import { processLinkText, patchMobileView } from "src/utils/customEmbeddableUtils";
import { processLinkText, patchMobileView, setFileToLocalGraph } from "src/utils/customEmbeddableUtils";
import { EmbeddableMDCustomProps } from "src/shared/Dialogs/EmbeddableSettings";
declare module "obsidian" {
@@ -154,6 +154,15 @@ function RenderObsidianView(
}; //cleanup on unmount
}, [isActiveRef.current, containerRef.current]);
//set local graph to view when deactivating embeddables
React.useEffect(() => {
if(file === view.file) {
return;
}
if(!isActiveRef.current) {
setFileToLocalGraph(view.app, view.file);
}
}, [isActiveRef.current]);
//--------------------------------------------------------------------------------
//Mount the workspace leaf or the canvas node depending on subpath
@@ -408,6 +417,10 @@ function RenderObsidianView(
return;
}
if(file !== view.file) {
setFileToLocalGraph(view.app, file);
}
if(leafRef.current.leaf?.view?.getViewType() === "markdown") {
//Handle markdown leaf
//@ts-ignore

View File

@@ -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";

View File

@@ -80,6 +80,11 @@ button.ToolIcon_type_button[title="Export"] {
width: 9em;
}
.excalidraw-export-button {
width: 9em;
margin-left: 10px;
}
.excalidraw-prompt-buttons-div {
display: flex;
flex-direction: row;
@@ -87,6 +92,13 @@ button.ToolIcon_type_button[title="Export"] {
justify-content: space-evenly;
}
.excalidraw-export-buttons-div {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
justify-content: right;
}
li[data-testid] {
border: 0 !important;
margin: 0 !important;
@@ -361,9 +373,16 @@ div.excalidraw-draginfo {
}
.excalidraw [data-radix-popper-content-wrapper] {
/*Overrides position:fixed in popover*/
position: absolute !important;
}
.excalidraw .Island .App-mobile-menu,
.excalidraw .Island.App-menu__left {
/*Arrow Picker Popover Overflow*/
overflow: visible;
}
.excalidraw__embeddable-container .view-header {
display: none !important;
}
@@ -665,3 +684,21 @@ textarea.excalidraw-wysiwyg, .excalidraw input {
background-color: var(--background-modifier-hover);
color: var(--text-accent);
}
.excalidraw-release .nav-buttons-container {
display: flex;
margin-bottom: 20px;
border-bottom: 2px solid var(--background-modifier-border);
}
.excalidraw-release .nav-button {
padding: 8px 16px;
border: none;
background: transparent;
cursor: pointer;
}
.excalidraw-release .nav-button.is-active {
border-bottom: 2px solid var(--interactive-accent);
margin-bottom: -2px;
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
test-data/PDFs/page.pdf Normal file

Binary file not shown.