This commit is contained in:
zsviczian
2024-05-05 19:13:30 +02:00
parent efce44f0a7
commit aa501c2843
20 changed files with 302 additions and 94 deletions

View File

@@ -3,6 +3,7 @@
The Obsidian-Excalidraw plugin integrates [Excalidraw](https://excalidraw.com/), a feature rich sketching tool, into Obsidian. You can store and edit Excalidraw files in your vault, you can embed drawings into your documents, and you can link to documents and other drawings to/and from Excalidraw. For a showcase of Excalidraw features, please read my blog post [here](https://www.zsolt.blog/2021/03/showcasing-excalidraw.html) and/or watch the videos below.
## Video Walkthrough
<a href="https://youtu.be/P_Q6avJGoWI" target="_blank"><img src=https://github.com/zsviczian/obsidian-excalidraw-plugin/assets/14358394/da34bb33-7610-45e6-b36f-cb7a02a9141b" width="300"/></a>
<a href="https://youtu.be/o0exK-xFP3k" target="_blank"><img src="https://user-images.githubusercontent.com/14358394/156931370-aa4d88de-c4a8-46cc-aeb2-dc09aa0bea39.jpg" width="300"/></a>
<a href="https://youtu.be/QKnQgSjJVuc" target="_blank"><img src="https://raw.githubusercontent.com/zsviczian/obsidian-excalidraw-plugin/master/images/thumbnail-getting-started.jpg" width="300"/></a>

View File

@@ -21,7 +21,7 @@ The script will convert your drawing into a slideshow presentation.
```javascript
*/
if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("1.9.23")) {
if(!ea.verifyMinimumPluginVersion || !ea.verifyMinimumPluginVersion("2.1.7")) {
new Notice("This script requires a newer version of Excalidraw. Please install the latest version.");
return;
}
@@ -190,7 +190,7 @@ let preventFullscreenExit = true;
const gotoFullscreen = async () => {
if(isFullscreen) return;
preventFullscreenExit = true;
if(app.isMobile) {
if(ea.DEVICE.isMobile) {
ea.viewToggleFullScreen();
} else {
await contentEl.webkitRequestFullscreen();
@@ -206,8 +206,8 @@ const gotoFullscreen = async () => {
const exitFullscreen = async () => {
if(!isFullscreen) return;
preventFullscreenExit = true;
if(!app.isMobile && ownerDocument?.fullscreenElement) await ownerDocument.exitFullscreen();
if(app.isMobile) ea.viewToggleFullScreen();
if(!ea.DEVICE.isMobile && ownerDocument?.fullscreenElement) await ownerDocument.exitFullscreen();
if(ea.DEVICE.isMobile) ea.viewToggleFullScreen();
if(toggleFullscreenButton) toggleFullscreenButton.innerHTML = SVG_MAXIMIZE;
await waitForExcalidrawResize();
resetControlPanelElPosition();
@@ -649,7 +649,7 @@ const initializeEventListners = () => {
controlPanelEl.removeEventListener('mouseenter', onMouseEnter, false);
controlPanelEl.removeEventListener('mouseleave', onMouseLeave, false);
controlPanelEl.parentElement?.removeChild(controlPanelEl);
if(!app.isMobile) {
if(!ea.DEVICE.isMobile) {
contentEl.removeEventListener('webkitfullscreenchange', fullscreenListener);
contentEl.removeEventListener('fullscreenchange', fullscreenListener);
}
@@ -664,7 +664,7 @@ const initializeEventListners = () => {
return true;
};
if(!app.isMobile) {
if(!ea.DEVICE.isMobile) {
contentEl.addEventListener('webkitfullscreenchange', fullscreenListener);
contentEl.addEventListener('fullscreenchange', fullscreenListener);
}
@@ -727,7 +727,7 @@ const exitPresentation = async (openForEdit = false) => {
//Resets pointer offsets. Ugly solution.
//During testing offsets were wrong after presentation, but don't know why.
//This should solve it even if they are wrong.
hostView.refresh();
hostView.refreshCanvasOffset();
excalidrawAPI.setActiveTool({type: "selection"});
})
}

View File

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

View File

@@ -19,7 +19,7 @@
"license": "MIT",
"dependencies": {
"@popperjs/core": "^2.11.8",
"@zsviczian/excalidraw": "0.17.1-obsidian-20",
"@zsviczian/excalidraw": "0.17.1-obsidian-21",
"chroma-js": "^2.4.2",
"clsx": "^2.0.0",
"colormaster": "^1.2.1",

View File

@@ -1,7 +1,7 @@
//https://stackoverflow.com/questions/2068344/how-do-i-get-a-youtube-video-thumbnail-from-the-youtube-api
//https://img.youtube.com/vi/uZz5MgzWXiM/maxresdefault.jpg
import { ExcalidrawElement, ExcalidrawImageElement, FileId } from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { ExcalidrawElement, FileId } from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { BinaryFileData, DataURL } from "@zsviczian/excalidraw/types/excalidraw/types";
import { App, MarkdownRenderer, Notice, TFile } from "obsidian";
import {
@@ -23,7 +23,7 @@ import { ExportSettings } from "./ExcalidrawView";
import { t } from "./lang/helpers";
import { tex2dataURL } from "./LaTeX";
import ExcalidrawPlugin from "./main";
import { blobToBase64, getDataURLFromURL, getMimeType, getPDFDoc, getURLImageExtension, readLocalFileBinary } from "./utils/FileUtils";
import { blobToBase64, getDataURLFromURL, getMimeType, getPDFDoc, getURLImageExtension, hasExcalidrawEmbeddedImagesTreeChanged, readLocalFileBinary } from "./utils/FileUtils";
import {
errorlog,
getDataURL,
@@ -42,6 +42,8 @@ import {
import { ValueOf } from "./types";
import { getMermaidImageElements, getMermaidText, shouldRenderMermaid } from "./utils/MermaidUtils";
import { mermaidToExcalidraw } from "src/constants/constants";
import { ImageKey, imageCache } from "./utils/ImageCache";
import { PreviewImageType } from "./utils/UtilTypes";
//An ugly workaround for the following situation.
//File A is a markdown file that has an embedded Excalidraw file B
@@ -164,7 +166,7 @@ export class EmbeddedFile {
constructor(plugin: ExcalidrawPlugin, hostPath: string, imgPath: string, colorMapJSON?: string) {
this.plugin = plugin;
this.resetImage(hostPath, imgPath);
if(this.file && (this.plugin.ea.isExcalidrawFile(this.file) || this.file.extension.toLowerCase() === "svg")) {
if(this.file && (this.plugin.isExcalidrawFile(this.file) || this.file.extension.toLowerCase() === "svg")) {
try {
this.colorMap = colorMapJSON ? JSON.parse(colorMapJSON.toLocaleLowerCase()) : null;
} catch (error) {
@@ -358,22 +360,46 @@ export class EmbeddedFilesLoader {
withTheme: !!forceTheme,
isMask,
};
const svg = replaceSVGColors(
await createSVG(
file?.path,
true,
exportSettings,
this,
forceTheme,
null,
null,
elements,
this.plugin,
depth+1,
getExportPadding(this.plugin, file),
),
inFile instanceof EmbeddedFile ? inFile.colorMap : null
) as SVGSVGElement;
const shouldUseCache = file && imageCache.isReady();
const cacheKey:ImageKey = {
filepath: file.path,
blockref: null,
sectionref: null,
isDark,
previewImageType: PreviewImageType.SVG,
scale: 1,
isTransparent: !exportSettings.withBackground,
hasBlockref: false,
hasGroupref: false,
hasTaskbone: false,
hasArearef: false,
hasFrameref: false,
hasSectionref: false,
linkpartReference: null,
linkpartAlias: null,
}
const maybeSVG = await imageCache.getImageFromCache(cacheKey);
const svg = (maybeSVG && (maybeSVG instanceof SVGSVGElement))
? maybeSVG
: replaceSVGColors(
await createSVG(
file?.path,
true,
exportSettings,
this,
forceTheme,
null,
null,
elements,
this.plugin,
depth+1,
getExportPadding(this.plugin, file),
),
inFile instanceof EmbeddedFile ? inFile.colorMap : null
) as SVGSVGElement;
//https://stackoverflow.com/questions/51154171/remove-css-filter-on-child-elements
const imageList = svg.querySelectorAll(
@@ -382,7 +408,8 @@ export class EmbeddedFilesLoader {
if (imageList.length > 0) {
hasSVGwithBitmap = true;
}
if (hasSVGwithBitmap && isDark) {
if (hasSVGwithBitmap && isDark && !Boolean(maybeSVG)) {
imageList.forEach((i) => {
const id = i.parentElement?.id;
svg.querySelectorAll(`use[href='#${id}']`).forEach((u) => {
@@ -393,6 +420,9 @@ export class EmbeddedFilesLoader {
if (!hasSVGwithBitmap && svg.getAttribute("hasbitmap")) {
hasSVGwithBitmap = true;
}
if(shouldUseCache && !Boolean(maybeSVG)) {
imageCache.addImageToCache(cacheKey,"", svg);
}
const dURL = svgToBase64(svg.outerHTML) as DataURL;
return {dataURL: dURL as DataURL, hasSVGwithBitmap};
};
@@ -526,7 +556,8 @@ export class EmbeddedFilesLoader {
public async loadSceneFiles(
excalidrawData: ExcalidrawData,
addFiles: (files: FileData[], isDark: boolean, final?: boolean) => void,
depth:number
depth:number,
isThemeChange:boolean = false,
) {
if(depth > 7) {
@@ -563,7 +594,8 @@ export class EmbeddedFilesLoader {
}
//files.push(fileData);
}
} /*else if (embeddedFile.isSVGwithBitmap) {
} else if (embeddedFile.isSVGwithBitmap && (depth !== 0 || isThemeChange)) {
//this will reload the image in light/dark mode when switching themes
const fileData = {
mimeType: embeddedFile.mimeType,
id: entry.value[0],
@@ -580,7 +612,7 @@ export class EmbeddedFilesLoader {
catch(e) {
errorlog({ where: "EmbeddedFileLoader.loadSceneFiles", error: e });
}
}*/
}
}
let equation;

View File

@@ -38,7 +38,7 @@ import {
MD_TEXTELEMENTS,
MD_DRAWING,
} from "src/constants/constants";
import { blobToBase64, checkAndCreateFolder, getDrawingFilename, getListOfTemplateFiles, getNewUniqueFilepath, } from "src/utils/FileUtils";
import { blobToBase64, checkAndCreateFolder, getDrawingFilename, getExcalidrawEmbeddedFilesFiletree, getListOfTemplateFiles, getNewUniqueFilepath, hasExcalidrawEmbeddedImagesTreeChanged, } from "src/utils/FileUtils";
import {
arrayToMap,
//debug,
@@ -252,6 +252,22 @@ export class ExcalidrawAutomate {
return getListOfTemplateFiles(this.plugin);
}
/**
* Retruns the embedded images in the scene recursively. If excalidrawFile is not provided,
* the function will use ea.targetView.file
* @param excalidrawFile
* @returns TFile[] of all nested images and Excalidraw drawings recursively
*/
public getEmbeddedImagesFiletree(excalidrawFile?: TFile): TFile[] {
if(!excalidrawFile && this.targetView && this.targetView.file) {
excalidrawFile = this.targetView.file;
}
if(!excalidrawFile) {
return [];
}
return getExcalidrawEmbeddedFilesFiletree(excalidrawFile, this.plugin);
}
public async getAttachmentFilepath(filename: string): Promise<string> {
if (!this.targetView || !this.targetView?.file) {
errorMessage("targetView not set", "getAttachmentFolderAndFilePath()");

View File

@@ -343,6 +343,7 @@ export default class ExcalidrawView extends TextFileView {
private hoverPreviewTarget: EventTarget = null;
private viewModeEnabled:boolean = false;
private lastMouseEvent: any = null;
private editingTextElementId: string = null; //storing to handle on-screen keyboard hide events
id: string = (this.leaf as any).id;
@@ -1960,7 +1961,7 @@ export default class ExcalidrawView extends TextFileView {
public activeLoader: EmbeddedFilesLoader = null;
private nextLoader: EmbeddedFilesLoader = null;
public async loadSceneFiles() {
public async loadSceneFiles(isThemeChange: boolean = false) {
if (!this.excalidrawAPI) {
return;
}
@@ -1996,7 +1997,7 @@ export default class ExcalidrawView extends TextFileView {
return false;
})
}
},0
},0,isThemeChange,
);
};
if (!this.activeLoader) {
@@ -2956,6 +2957,12 @@ export default class ExcalidrawView extends TextFileView {
api.refresh();
};
// depricated. kept for backward compatibility. e.g. used by the Slideshow plugin
// 2024.05.03
public refresh() {
this.refreshCanvasOffset();
}
private clearHoverPreview() {
if (this.hoverPreviewTarget) {
const event = new MouseEvent("click", {
@@ -3446,7 +3453,7 @@ export default class ExcalidrawView extends TextFileView {
private async onThemeChange (newTheme: string) {
//debug({where:"ExcalidrawView.onThemeChange",file:this.file.name,before:"this.loadSceneFiles",newTheme});
this.excalidrawData.scene.appState.theme = newTheme;
this.loadSceneFiles();
this.loadSceneFiles(true);
this.toolsPanelRef?.current?.setTheme(newTheme);
//Timeout is to allow appState to update
setTimeout(()=>setDynamicStyle(this.plugin.ea,this,this.previousBackgroundColor,this.plugin.settings.dynamicStyling));
@@ -3867,7 +3874,7 @@ export default class ExcalidrawView extends TextFileView {
}
// 1. Set the isEditingText flag to true to prevent autoresize on mobile
// 1500ms is an empirical number, the onscreen keyboard usually disappears in 1-2 seconds
// 1500ms is an empirical number, the on-screen keyboard usually disappears in 1-2 seconds
this.semaphores.isEditingText = true;
if(this.isEditingTextResetTimer) {
clearTimeout(this.isEditingTextResetTimer);
@@ -4697,7 +4704,7 @@ export default class ExcalidrawView extends TextFileView {
const height = this.contentEl.clientHeight;
if(width === 0 || height === 0) return;
//this is an aweful hack to prevent the keyboard pushing the canvas out of view.
//this is an aweful hack to prevent the on-screen keyboard pushing the canvas out of view.
//The issue is that contrary to Excalidraw.com where the page is simply pushed up, in
//Obsidian the leaf has a fixed top. As a consequence the top of excalidrawWrapperDiv does not get pushed out of view
//but shirnks. But the text area is positioned relative to excalidrawWrapperDiv and consequently does not fit, which
@@ -4708,8 +4715,12 @@ export default class ExcalidrawView extends TextFileView {
//I found that adding and removing this style solves the issue.
//...again, just aweful, but works.
const st = this.excalidrawAPI.getAppState();
const isKeyboardOutEvent = st.editingElement?.type === "text";
const isKeyboardBackEvent = this.semaphores.isEditingText && !isKeyboardOutEvent;
//isEventOnSameElement attempts to solve https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1729
//the issue is that when the user hides the keyboard with the keyboard hide button and not tapping on the screen, then editingElement is not null
const isEventOnSameElement = this.editingTextElementId === st.editingElement?.id;
const isKeyboardOutEvent:Boolean = st.editingElement && st.editingElement.type === "text" && !isEventOnSameElement;
const isKeyboardBackEvent:Boolean = (this.semaphores.isEditingText || isEventOnSameElement) && !isKeyboardOutEvent;
this.editingTextElementId = isKeyboardOutEvent ? st.editingElement.id : null;
if(isKeyboardOutEvent) {
const self = this;
const appToolHeight = (self.contentEl.querySelector(".Island.App-toolbar") as HTMLElement)?.clientHeight ?? 0;

View File

@@ -86,7 +86,7 @@ const _getPNG = async ({imgAttributes,filenameParts,theme,cacheReady,img,file,ex
? 2
: 1;
const cacheKey = {...filenameParts, isDark: theme==="dark", previewImageType: PreviewImageType.PNG, scale};
const cacheKey = {...filenameParts, isDark: theme==="dark", previewImageType: PreviewImageType.PNG, scale, isTransparent: !exportSettings.withBackground};
if(cacheReady) {
const src = await imageCache.getImageFromCache(cacheKey);
@@ -163,7 +163,7 @@ const _getSVGIMG = async ({filenameParts,theme,cacheReady,img,file,exportSetting
exportSettings: ExportSettings,
loader: EmbeddedFilesLoader,
}):Promise<HTMLImageElement> => {
const cacheKey = {...filenameParts, isDark: theme==="dark", previewImageType: PreviewImageType.SVGIMG, scale:1};
const cacheKey = {...filenameParts, isDark: theme==="dark", previewImageType: PreviewImageType.SVGIMG, scale:1, isTransparent: !exportSettings.withBackground};
if(cacheReady) {
const src = await imageCache.getImageFromCache(cacheKey);
if(src && typeof src === "string") {
@@ -220,13 +220,13 @@ const _getSVGNative = async ({filenameParts,theme,cacheReady,containerElement,fi
exportSettings: ExportSettings,
loader: EmbeddedFilesLoader,
}):Promise<HTMLDivElement> => {
const cacheKey = {...filenameParts, isDark: theme==="dark", previewImageType: PreviewImageType.SVG, scale:1};
const cacheKey = {...filenameParts, isDark: theme==="dark", previewImageType: PreviewImageType.SVG, scale:1, isTransparent: !exportSettings.withBackground};
let maybeSVG;
if(cacheReady) {
maybeSVG = await imageCache.getImageFromCache(cacheKey);
}
let svg = maybeSVG && (maybeSVG instanceof SVGSVGElement)
let svg = (maybeSVG && (maybeSVG instanceof SVGSVGElement))
? maybeSVG
: convertSVGStringToElement((await createSVG(
filenameParts.hasGroupref || filenameParts.hasBlockref || filenameParts.hasSectionref || filenameParts.hasFrameref

View File

@@ -14,7 +14,7 @@ export const MD_JSON_START = "```json\n";
export const MD_JSON_END = "```";
export const MD_DRAWING = "# Drawing";
export const MD_ELEMENTLINKS = "# Element Links";
export const MD_EMBEDFILES = "# Embed files";
export const MD_EMBEDFILES = "# Embedded files";
export const MD_EX_SECTIONS = [MD_TEXTELEMENTS, MD_DRAWING, MD_ELEMENTLINKS, MD_EMBEDFILES];
export const ERROR_IFRAME_CONVERSION_CANCELED = "iframe conversion canceled";

View File

@@ -6,7 +6,7 @@ If you'd like to learn more, please subscribe to my YouTube channel: [Visual PKM
Thank you & Enjoy!
<div class="excalidraw-videoWrapper"><div>
<iframe src="https://www.youtube.com/embed/o0exK-xFP3k" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
<iframe src="https://www.youtube.com/embed/P_Q6avJGoWI" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div></div>
`;
@@ -16,6 +16,41 @@ export const RELEASE_NOTES: { [k: string]: string } = {
I develop this plugin as a hobby, spending my free time doing this. If you find it valuable, then please say THANK YOU or...
<div class="ex-coffee-div"><a href="https://ko-fi.com/zsolt"><img src="https://cdn.ko-fi.com/cdn/kofi3.png?v=3" height=45></a></div>
`,
"2.1.7:":`
<div class="excalidraw-videoWrapper"><div>
<iframe src="https://www.youtube.com/embed/P_Q6avJGoWI" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div></div>
## Updates from Excalidraw.com
- Improved undo management.
- Improved handle to scale images from the side.
- Changed arrow binding behavior.
- Many other minor fixes and improvements.
## New
- Introduced image caching for nested (embedded) Excalidraw drawings on the scene. This enhancement should lead to improved scene loading times, especially when dealing with numerous embedded Excalidraw drawings.
- Added new OCR Command Palette actions. Users can now re-run OCR and run OCR for selected elements.
## Fixed
- Fixed an issue where cropping an embeddable PDF frame in the Excalidraw Scene caused distortion based on the embeddable element's aspect ratio. ([#1756](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1756))
- Removed the listing of ${String.fromCharCode(96)}# Embedded files${String.fromCharCode(96)} section when adding a "Back of the note card". ([#1754](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1754))
- Resolved the issue where closing the on-screen keyboard with the keyboard hide button of your phone, instead of tapping somewhere else on the Excalidraw scene, did not resize the scene correctly. ([#1729](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1729))
- Fixed the problem where pasting a text element as text into markdown incorrectly pasted the text to the end of the MD note, with line breaks as rendered on screen in Excalidraw. Also addressed the issue where pasting an image element as an image resulted in it being pasted to the end of the document. ([#1749](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1749))
- Corrected the color inversion of embedded images when changing the theme from light to dark, then back from dark to light, and again from light to dark on the third change.
- Addressed the problem where cropping an image while unlocking and rotating it in the cropper did not reflect the rotation. Note that rotating the image in Cropper required switching to markdown view mode, changing the "locked": true property to false, then switching back to Excalidraw mode. This issue likely impacted only a very few power users. ([#1745](https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1745))
## New in ExcalidrawAutomate
${String.fromCharCode(96,96,96)}ts
/**
* Retruns the embedded images in the scene recursively. If excalidrawFile is not provided,
* the function will use ea.targetView.file
* @param excalidrawFile
* @returns TFile[] of all nested images and Excalidraw drawings recursively
*/
public getEmbeddedImagesFiletree(excalidrawFile?: TFile): TFile[];
${String.fromCharCode(96,96,96)}
`,
"2.1.6":`
## Two minor fixes

View File

@@ -571,6 +571,13 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
"If no template is set, it returns null.",
after: "",
},
{
field: "getEmbeddedImagesFiletree",
code: "getEmbeddedImagesFiletree(excalidrawFile?: TFile): TFile[]",
desc: "Retruns the embedded images in the scene recursively. If excalidrawFile is not provided, " +
"the function will use ea.targetView.file",
after: "",
},
{
field: "getAttachmentFilepath",
code: "async getAttachmentFilepath(filename: string): Promise<string>",

View File

@@ -74,7 +74,9 @@ export default {
`Insert LaTeX formula (e.g. \\binom{n}{k} = \\frac{n!}{k!(n-k)!}). ${labelALT()}+CLICK to watch a help video.`,
ENTER_LATEX: "Enter a valid LaTeX expression",
READ_RELEASE_NOTES: "Read latest release notes",
RUN_OCR: "OCR: Grab text from freedraw scribble and pictures to clipboard",
RUN_OCR: "OCR full drawing: Grab text from freedraw + images to clipboard and doc.props",
RERUN_OCR: "OCR full drawing re-run: Grab text from freedraw + images to clipboard and doc.props",
RUN_OCR_ELEMENTS: "OCR selected elements: Grab text from freedraw + images to clipboard",
TRAY_MODE: "Toggle property-panel tray-mode",
SEARCH: "Search for text in drawing",
CROP_IMAGE: "Crop and mask image",

View File

@@ -105,7 +105,7 @@ import {
decompress,
getImageSize,
} from "./utils/Utils";
import { extractSVGPNGFileName, getActivePDFPageNumberFromPDFView, getAttachmentsFolderAndFilePath, getNewOrAdjacentLeaf, getParentOfClass, isObsidianThemeDark, mergeMarkdownFiles, openLeaf } from "./utils/ObsidianUtils";
import { editorInsertText, extractSVGPNGFileName, getActivePDFPageNumberFromPDFView, getAttachmentsFolderAndFilePath, getNewOrAdjacentLeaf, getParentOfClass, isObsidianThemeDark, mergeMarkdownFiles, openLeaf } from "./utils/ObsidianUtils";
import { ExcalidrawElement, ExcalidrawEmbeddableElement, ExcalidrawImageElement, ExcalidrawTextElement, FileId } from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { ScriptEngine } from "./Scripts";
import {
@@ -1182,7 +1182,51 @@ export default class ExcalidrawPlugin extends Plugin {
new Notice("Taskbone OCR is not enabled. Please go to plugins settings to enable it.",4000);
return true;
}
this.taskbone.getTextForView(view, false);
this.taskbone.getTextForView(view, {forceReScan: false});
return true;
}
return false;
},
});
this.addCommand({
id: "rerun-ocr",
name: t("RERUN_OCR"),
checkCallback: (checking: boolean) => {
const view = this.app.workspace.getActiveViewOfType(ExcalidrawView);
if (checking) {
return (
Boolean(view)
);
}
if (view) {
if(!this.settings.taskboneEnabled) {
new Notice("Taskbone OCR is not enabled. Please go to plugins settings to enable it.",4000);
return true;
}
this.taskbone.getTextForView(view, {forceReScan: true});
return true;
}
return false;
},
});
this.addCommand({
id: "run-ocr-selectedelements",
name: t("RUN_OCR_ELEMENTS"),
checkCallback: (checking: boolean) => {
const view = this.app.workspace.getActiveViewOfType(ExcalidrawView);
if (checking) {
return (
Boolean(view)
);
}
if (view) {
if(!this.settings.taskboneEnabled) {
new Notice("Taskbone OCR is not enabled. Please go to plugins settings to enable it.",4000);
return true;
}
this.taskbone.getTextForView(view, {forceReScan: false, selectedElementsOnly: true, addToFrontmatter: false});
return true;
}
return false;
@@ -2419,16 +2463,16 @@ export default class ExcalidrawPlugin extends Plugin {
if(sourceFile && imageFile && imageFile instanceof TFile) {
path = self.app.metadataCache.fileToLinktext(imageFile,sourceFile.path);
}
editor.insertText(getLink(self, {path}));
editorInsertText(editor, getLink(self, {path}));
}
return;
}
if (element.type === "text") {
editor.insertText(element.text);
editorInsertText(editor, element.rawText);
return;
}
if (element.link) {
editor.insertText(`${element.link}`);
editorInsertText(editor, `${element.link}`);
return;
}
} catch (e) {

View File

@@ -381,7 +381,7 @@ export class ToolsPanel extends React.Component<PanelProps, PanelState> {
new Notice("Taskbone OCR is not enabled. Please go to plugins settings to enable it.",4000);
return;
}
this.props.view.plugin.taskbone.getTextForView(this.props.view, isWinCTRLorMacCMD(e));
this.props.view.plugin.taskbone.getTextForView(this.props.view, {forceReScan: isWinCTRLorMacCMD(e)});
}}
icon={ICONS.ocr}
view={this.props.view}

View File

@@ -1,12 +1,13 @@
import { createPNG, ExcalidrawAutomate } from "../ExcalidrawAutomate";
import { ExcalidrawAutomate, createPNG } from "../ExcalidrawAutomate";
import {Notice, requestUrl} from "obsidian"
import ExcalidrawPlugin from "../main"
import {log} from "../utils/Utils"
import ExcalidrawView, { ExportSettings } from "../ExcalidrawView"
import FrontmatterEditor from "src/utils/Frontmatter";
import { ExcalidrawElement, ExcalidrawImageElement } from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { ExcalidrawElement } from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { EmbeddedFilesLoader } from "src/EmbeddedFileLoader";
import { blobToBase64 } from "src/utils/FileUtils";
import { getEA } from "src";
const TASKBONE_URL = "https://api.taskbone.com/"; //"https://excalidraw-preview.onrender.com/";
const TASKBONE_OCR_FN = "execute?id=60f394af-85f6-40bc-9613-5d26dc283cbb";
@@ -39,23 +40,9 @@ export default class Taskbone {
return apiKey;
}
public async getTextForView(view: ExcalidrawView, forceReScan: boolean) {
await view.forceSave(true);
const viewElements = view.excalidrawAPI.getSceneElements().filter((el:ExcalidrawElement) =>
el.type==="freedraw" ||
( el.type==="image" &&
!this.plugin.isExcalidrawFile(view.excalidrawData.getFile(el.fileId)?.file)
));
if(viewElements.length === 0) {
new Notice ("Aborting OCR because there are no image or freedraw elements on the canvas.",4000);
return;
}
const fe = new FrontmatterEditor(view.data);
if(fe.hasKey("taskbone-ocr") && !forceReScan) {
new Notice ("The drawing has already been processed, you will find the result in the frontmatter in markdown view mode. If you ran the command from the Obsidian Panel in Excalidraw then you can CTRL(CMD)+click the command to force the rescaning.",4000)
return;
}
const bb = this.plugin.ea.getBoundingBox(viewElements);
public async getTextForElements(elements: ExcalidrawElement[], ea: ExcalidrawAutomate): Promise<string> {
ea.copyViewElementsToEAforEditing(elements, true);
const bb = ea.getBoundingBox(elements);
const size = (bb.width*bb.height);
const minRatio = Math.sqrt(360000/size);
const maxRatio = Math.sqrt(size/16000000);
@@ -79,26 +66,52 @@ export default class Taskbone {
};
const img =
await createPNG(
view.file.path + "#^taskbone",
await ea.createPNG(
null,
scale,
exportSettings,
loader,
"light",
null,
null,
[],
this.plugin,
0
);
)
return await this.getTextForImage(img);
}
const text = await this.getTextForImage(img);
public async getTextForView(view: ExcalidrawView, {
forceReScan,
selectedElementsOnly = false,
addToFrontmatter = true,
}: {
forceReScan: boolean,
selectedElementsOnly?: boolean,
addToFrontmatter?: boolean,
}) {
await view.forceSave(true);
const ea = getEA(view) as ExcalidrawAutomate;
const viewElements = (selectedElementsOnly ? ea.getViewSelectedElements() : ea.getViewElements())
.filter((el:ExcalidrawElement) =>
el.type==="freedraw" ||
( el.type==="image" &&
!this.plugin.isExcalidrawFile(view.excalidrawData.getFile(el.fileId)?.file)
));
if(viewElements.length === 0) {
new Notice ("Aborting OCR because there are no image or freedraw elements on the canvas.",4000);
return;
}
const fe = new FrontmatterEditor(view.data);
if(addToFrontmatter && fe.hasKey("taskbone-ocr") && !forceReScan) {
new Notice ("The drawing has already been processed, you will find the result in the frontmatter in markdown view mode. If you ran the command from the Obsidian Panel in Excalidraw then you can CTRL(CMD)+click the command to force the rescaning.",4000)
return;
}
const text = await this.getTextForElements(viewElements, ea);
if(text) {
fe.setKey("taskbone-ocr",text);
view.data = fe.data;
view.save(false);
if(addToFrontmatter) {
fe.setKey("taskbone-ocr",text);
view.data = fe.data;
view.save(false);
}
window.navigator.clipboard.writeText(text);
new Notice("I placed the recognized in the drawing's frontmatter and onto the system clipboard.");
new Notice(`I placed the recognized text onto the system clipboard${addToFrontmatter?" and to document properties":""}.`);
}
}

4
src/types.d.ts vendored
View File

@@ -58,9 +58,9 @@ declare module "obsidian" {
},
basePath: string;
}
interface Editor {
/*interface Editor {
insertText(data: string): void;
}
}*/
interface MetadataCache {
getBacklinksForFile(file: TFile): any;
getLinks(): { [id: string]: Array<{ link: string; displayText: string; original: string; position: any }> };

View File

@@ -69,7 +69,7 @@ export const carveOutPDF = async (sourceEA: ExcalidrawAutomate, embeddableEl: Ex
const targetEA = getEA(sourceEA.targetView) as ExcalidrawAutomate;
const {height, width} = embeddableEl;
let {height, width} = embeddableEl;
if(!height || !width || height === 0 || width === 0) return;
@@ -77,8 +77,6 @@ export const carveOutPDF = async (sourceEA: ExcalidrawAutomate, embeddableEl: Ex
const newImage = targetEA.getElement(imageId) as Mutable<ExcalidrawImageElement>;
newImage.x = 0;
newImage.y = 0;
newImage.width = width;
newImage.height = height;
const angle = embeddableEl.angle;
const fname = pdfFile.basename;

View File

@@ -440,3 +440,38 @@ export const fileShouldDefaultAsExcalidraw = (path:string, app:App):boolean => {
cache.frontmatter[FRONTMATTER_KEYS["plugin"].name] &&
!Boolean(cache.frontmatter[FRONTMATTER_KEYS["open-as-markdown"].name]);
}
export const getExcalidrawEmbeddedFilesFiletree = (sourceFile: TFile, plugin: ExcalidrawPlugin):TFile[] => {
if(!sourceFile || !plugin.isExcalidrawFile(sourceFile)) {
return [];
}
const fileList = new Set<TFile>();
const app = plugin.app;
const addAttachedImages = (f:TFile) => Object
.keys(app.metadataCache.resolvedLinks[f.path])
.forEach(path => {
const file = app.vault.getAbstractFileByPath(path);
if (!file || !(file instanceof TFile)) return;
const isExcalidraw = plugin.isExcalidrawFile(file);
if (
(file.extension === "md" && !isExcalidraw) ||
fileList.has(file) //avoid infinite loops
) {
return;
}
fileList.add(file);
if (isExcalidraw) {
addAttachedImages(file);
}
});
addAttachedImages(sourceFile);
return Array.from(fileList);
}
export const hasExcalidrawEmbeddedImagesTreeChanged = (sourceFile: TFile, mtime:number, plugin: ExcalidrawPlugin):boolean => {
const fileList = getExcalidrawEmbeddedFilesFiletree(sourceFile, plugin);
return fileList.some(f=>f.stat.mtime > mtime);
}

View File

@@ -2,6 +2,8 @@ import { App, Notice, TFile } from "obsidian";
import ExcalidrawPlugin from "src/main";
import { convertSVGStringToElement } from "./Utils";
import { FILENAMEPARTS, PreviewImageType } from "./UtilTypes";
import { has } from "src/svgToExcalidraw/attributes";
import { hasExcalidrawEmbeddedImagesTreeChanged } from "./FileUtils";
//@ts-ignore
const DB_NAME = "Excalidraw " + app.appId;
@@ -19,6 +21,7 @@ export type ImageKey = {
isDark: boolean;
previewImageType: PreviewImageType;
scale: number;
isTransparent: boolean;
} & FILENAMEPARTS;
const getKey = (key: ImageKey): string =>
@@ -29,7 +32,7 @@ const getKey = (key: ImageKey): string =>
: key.previewImageType === PreviewImageType.PNG
? 0
: 2
}#${key.scale}`; //key.isSVG ? 1 : 0
}#${key.scale}${key.isTransparent?"#t":""}`; //key.isSVG ? 1 : 0
class ImageCache {
private dbName: string;
@@ -291,7 +294,10 @@ class ImageCache {
const file = this.app.vault.getAbstractFileByPath(key_.filepath.split("#")[0]);
if (!file || !(file instanceof TFile)) return undefined;
if (cachedData && cachedData.mtime === file.stat.mtime) {
if (cachedData && cachedData.mtime >= file.stat.mtime) {
if(hasExcalidrawEmbeddedImagesTreeChanged(file, cachedData.mtime, this.plugin)) {
return undefined;
}
if(cachedData.svg) {
return convertSVGStringToElement(cachedData.svg);
}
@@ -332,7 +338,7 @@ class ImageCache {
} else {
blob = image as Blob;
}
const data: FileCacheData = { mtime: file.stat.mtime, blob, svg};
const data: FileCacheData = { mtime: Date.now(), blob, svg};
const transaction = this.db.transaction(this.cacheStoreName, "readwrite");
const store = transaction.objectStore(this.cacheStoreName);
const key = getKey(key_);

View File

@@ -1,5 +1,6 @@
import {
App,
Editor,
FrontMatterCache,
normalizePath, OpenViewState, parseFrontMatterEntry, TFile, View, Workspace, WorkspaceLeaf, WorkspaceSplit
} from "obsidian";
@@ -342,4 +343,11 @@ export const mergeMarkdownFiles = (template: string, target: string): string =>
const mergedMarkdown = `---\n${mergedFrontmatterYaml}---\n${targetContent}\n\n${templateContent.trim()}\n`;
return mergedMarkdown;
};
};
export const editorInsertText = (editor: Editor, text: string)=> {
const cursor = editor.getCursor();
const line = editor.getLine(cursor.line);
const updatedLine = line.slice(0, cursor.ch) + text + line.slice(cursor.ch);
editor.setLine(cursor.line, updatedLine);
}