From dec2909db0d48e599db06f96cffaa5d34dd90c76 Mon Sep 17 00:00:00 2001 From: zsviczian Date: Fri, 1 Nov 2024 07:44:58 +0100 Subject: [PATCH] 2.6.2-beta-2, 0.17.6-10 PDFCropping --- manifest-beta.json | 2 +- package.json | 2 +- src/EmbeddedFileLoader.ts | 58 ---------------------------- src/ExcalidrawAutomate.ts | 4 ++ src/ExcalidrawData.ts | 21 +++++++++++ src/ExcalidrawView.ts | 37 +++++++++++++++++- src/constants/constants.ts | 1 + src/dialogs/ExcalidrawLoading.ts | 16 +++++--- src/dialogs/Prompt.ts | 65 ++++++++++++++++++++++++++------ src/lang/locale/en.ts | 4 ++ src/main.ts | 50 +++++++++++++++++++++++- src/utils/ExcalidrawViewUtils.ts | 3 +- src/utils/PDFUtils.ts | 30 ++++++++++----- src/utils/Utils.ts | 4 -- 14 files changed, 203 insertions(+), 94 deletions(-) diff --git a/manifest-beta.json b/manifest-beta.json index 37e280d..8a6bab8 100644 --- a/manifest-beta.json +++ b/manifest-beta.json @@ -1,7 +1,7 @@ { "id": "obsidian-excalidraw-plugin", "name": "Excalidraw", - "version": "2.6.3-beta-1", + "version": "2.6.3-beta-2", "minAppVersion": "1.1.6", "description": "An Obsidian plugin to edit and view Excalidraw drawings", "author": "Zsolt Viczian", diff --git a/package.json b/package.json index a1338ad..b7b298f 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "license": "MIT", "dependencies": { "@popperjs/core": "^2.11.8", - "@zsviczian/excalidraw": "0.17.6-9", + "@zsviczian/excalidraw": "0.17.6-10", "chroma-js": "^2.4.2", "clsx": "^2.0.0", "@zsviczian/colormaster": "^1.2.2", diff --git a/src/EmbeddedFileLoader.ts b/src/EmbeddedFileLoader.ts index 629a969..27b5eb2 100644 --- a/src/EmbeddedFileLoader.ts +++ b/src/EmbeddedFileLoader.ts @@ -1108,66 +1108,8 @@ export class EmbeddedFilesLoader { const getSVGData = async (app: App, file: TFile, colorMap: ColorMap | null): Promise => { const svgString = replaceSVGColors(await app.vault.read(file), colorMap) as string; return svgToBase64(svgString) as DataURL; -/* - try { - const container = document.createElement('div'); - container.innerHTML = svgString; - - const svgElement = container.querySelector('svg'); - - if (!svgElement) { - throw new Error('Invalid SVG content'); // Ensure there's an SVG element - } - - // Check for width and height attributes - const hasWidth = svgElement.hasAttribute('width'); - const hasHeight = svgElement.hasAttribute('height'); - - // If width or height is missing, calculate based on viewBox - if (!hasWidth || !hasHeight) { - const viewBox = svgElement.getAttribute('viewBox'); - - if (viewBox) { - const [ , , viewBoxWidth, viewBoxHeight] = viewBox.split(/\s+/).map(Number); - - // Set width and height based on viewBox if they are missing - if (!hasWidth) { - svgElement.setAttribute('width', `${viewBoxWidth}px`); - } - if (!hasHeight) { - svgElement.setAttribute('height', `${viewBoxHeight}px`); - } - } - } - - // Get the updated SVG string from outerHTML - const updatedSVGString = svgElement.outerHTML; - - // Convert the updated SVG string to a base64 Data URL - return svgToBase64(updatedSVGString) as DataURL; - } catch (error) { - errorlog({ where: "EmbeddedFileLoader.getSVGData", error }); - return svgToBase64(svgString) as DataURL; - }*/ }; -/*export const generateIdFromFile = async (file: ArrayBuffer): Promise => { - let id: FileId; - try { - const hashBuffer = await window.crypto.subtle.digest("SHA-1", file); - id = - // convert buffer to byte array - Array.from(new Uint8Array(hashBuffer)) - // convert to hex string - .map((byte) => byte.toString(16).padStart(2, "0")) - .join("") as FileId; - } catch (error) { - errorlog({ where: "EmbeddedFileLoader.generateIdFromFile", error }); - id = fileid() as FileId; - } - return id; -};*/ - export const generateIdFromFile = async (file: ArrayBuffer, key?: string): Promise => { let id: FileId; try { diff --git a/src/ExcalidrawAutomate.ts b/src/ExcalidrawAutomate.ts index ea04027..5ac0176 100644 --- a/src/ExcalidrawAutomate.ts +++ b/src/ExcalidrawAutomate.ts @@ -1547,6 +1547,10 @@ export class ExcalidrawAutomate { : imageFile.path + (scale || !anchor ? "":"|100%"), hasSVGwithBitmap: image.hasSVGwithBitmap, latex: null, + size: { //must have the natural size here (e.g. for PDF cropping) + height: image.size.height, + width: image.size.width, + }, }; if (scale && (Math.max(image.size.width, image.size.height) > MAX_IMAGE_SIZE)) { const scale = diff --git a/src/ExcalidrawData.ts b/src/ExcalidrawData.ts index 056803b..a3c7db4 100644 --- a/src/ExcalidrawData.ts +++ b/src/ExcalidrawData.ts @@ -55,6 +55,7 @@ import { updateElementIdsInScene } from "./utils/ExcalidrawSceneUtils"; import { getNewUniqueFilepath } from "./utils/FileUtils"; import { t } from "./lang/helpers"; import { displayFontMessage } from "./utils/ExcalidrawViewUtils"; +import { getPDFRect } from "./utils/PDFUtils"; type SceneDataWithFiles = SceneData & { files: BinaryFiles }; @@ -1579,6 +1580,25 @@ export class ExcalidrawData { return file; } + private syncCroppedPDFs() { + let dirty = false; + const scene = this.scene as SceneDataWithFiles; + const pdfScale = this.plugin.settings.pdfScale; + scene.elements + .filter(el=>el.type === "image" && el.crop && !el.isDeleted) + .forEach((el: Mutable)=>{ + const ef = this.getFile(el.fileId); + if(ef.file.extension !== "pdf") return; + 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 : "]]"); + el.link = `[[${link}`; + this.elementLinks.set(el.id, el.link); + dirty = true; + }); + } + /** * deletes fileIds from Excalidraw data for files no longer in the scene * @returns @@ -1699,6 +1719,7 @@ export class ExcalidrawData { this.updateElementLinksFromScene(); result = result || + this.syncCroppedPDFs() || this.setLinkPrefix() || this.setUrlPrefix() || this.setShowLinkBrackets() || diff --git a/src/ExcalidrawView.ts b/src/ExcalidrawView.ts index 9ecd79f..6c4c5c6 100644 --- a/src/ExcalidrawView.ts +++ b/src/ExcalidrawView.ts @@ -148,6 +148,7 @@ import { Packages } from "./types/types"; import React from "react"; import { diagramToHTML } from "./utils/matic"; import { IS_WORKER_SUPPORTED } from "./workers/compression-worker"; +import { getPDFCropRect } from "./utils/PDFUtils"; const EMBEDDABLE_SEMAPHORE_TIMEOUT = 2000; const PREVENT_RELOAD_TIMEOUT = 2000; @@ -218,6 +219,27 @@ export const addFiles = async ( if (isDark === undefined) { isDark = s.scene.appState.theme; } + // update element.crop naturalWidth and naturalHeight in case scale of PDF loading has changed + // update crop.x crop.y, crop.width, crop.height according to the new scale + files + .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) + .forEach((el:Mutable) => { + 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 (s.dirty) { //debug({where:"ExcalidrawView.addFiles",file:view.file.name,dataTheme:view.excalidrawData.scene.appState.theme,before:"updateScene",state:scene.appState}) view.updateScene({ @@ -4036,7 +4058,20 @@ export default class ExcalidrawView extends TextFileView implements HoverParent{ } else { if(link.match(/^[^#]*#page=\d*(&\w*=[^&]+){0,}&rect=\d*,\d*,\d*,\d*/g)) { const ea = getEA(this) as ExcalidrawAutomate; - await ea.addImage(this.currentPosition.x, this.currentPosition.y,link); + const imgID = await ea.addImage(this.currentPosition.x, this.currentPosition.y,link.split("&rect=")[0]); + const el = ea.getElement(imgID) as Mutable; + const fd = ea.imagesDict[el.fileId] as FileData; + el.crop = getPDFCropRect({ + scale: this.plugin.settings.pdfScale, + link, + naturalHeight: fd.size.height, + naturalWidth: fd.size.width, + }); + if(el.crop) { + el.width = el.crop.width/this.plugin.settings.pdfScale; + el.height = el.crop.height/this.plugin.settings.pdfScale; + } + el.link = `[[${link}]]`; ea.addElementsToView(false,false).then(()=>ea.destroy()); } else { const modal = new UniversalInsertFileModal(this.plugin, this); diff --git a/src/constants/constants.ts b/src/constants/constants.ts index 8fe9130..59ac165 100644 --- a/src/constants/constants.ts +++ b/src/constants/constants.ts @@ -222,6 +222,7 @@ export const FRONTMATTER_KEYS:{[key:string]: {name: string, type: string, depric export const EMBEDDABLE_THEME_FRONTMATTER_VALUES = ["light", "dark", "auto", "dafault"]; export const VIEW_TYPE_EXCALIDRAW = "excalidraw"; +export const VIEW_TYPE_EXCALIDRAW_LOADING = "excalidraw-loading"; export const ICON_NAME = "excalidraw-icon"; export const MAX_COLORS = 5; export const COLOR_FREQ = 6; diff --git a/src/dialogs/ExcalidrawLoading.ts b/src/dialogs/ExcalidrawLoading.ts index fb39eba..62e5b62 100644 --- a/src/dialogs/ExcalidrawLoading.ts +++ b/src/dialogs/ExcalidrawLoading.ts @@ -1,11 +1,16 @@ -import { FileView, TextFileView, View, WorkspaceLeaf } from "obsidian"; +import { App, FileView, WorkspaceLeaf } from "obsidian"; +import { VIEW_TYPE_EXCALIDRAW_LOADING } from "src/constants/constants"; import ExcalidrawPlugin from "src/main"; import { setExcalidrawView } from "src/utils/ObsidianUtils"; -export default class ExcalidrawLoading extends FileView { +export function switchToExcalidraw(app: App) { + const leaves = app.workspace.getLeavesOfType(VIEW_TYPE_EXCALIDRAW_LOADING).filter(l=>l.view instanceof ExcalidrawLoading); + leaves.forEach(l=>(l.view as ExcalidrawLoading).switchToeExcalidraw()); +} + +export class ExcalidrawLoading extends FileView { constructor(leaf: WorkspaceLeaf, private plugin: ExcalidrawPlugin) { super(leaf); - this.switchToeExcalidraw(); this.displayLoadingText(); } @@ -14,13 +19,12 @@ export default class ExcalidrawLoading extends FileView { this.displayLoadingText(); } - private async switchToeExcalidraw() { - await this.plugin.awaitInit(); + public switchToeExcalidraw() { setExcalidrawView(this.leaf); } getViewType(): string { - return "excalidra-loading"; + return VIEW_TYPE_EXCALIDRAW_LOADING; } getDisplayText() { diff --git a/src/dialogs/Prompt.ts b/src/dialogs/Prompt.ts index cde99c8..088fea3 100644 --- a/src/dialogs/Prompt.ts +++ b/src/dialogs/Prompt.ts @@ -713,27 +713,70 @@ export class ConfirmationPrompt extends Modal { } } -export async function linkPrompt ( - linkText:string, +export async function linkPrompt( + linkText: string, app: App, view?: ExcalidrawView, - message: string = "Select link to open", -):Promise<[file:TFile, linkText:string, subpath: string]> { - const linksArray = REGEX_LINK.getResList(linkText); - const tagsArray = REGEX_TAGS.getResList(linkText.replaceAll(/([^\s])#/g,"$1 ")); + message: string = t("SELECT_LINK_TO_OPEN"), +): Promise<[file: TFile, linkText: string, subpath: string]> { + const linksArray = REGEX_LINK.getResList(linkText).filter(x => Boolean(x.value)); + const links = linksArray.map(x => REGEX_LINK.getLink(x)); + + // Create a map to track duplicates by base link (without rect reference) + const linkMap = new Map(); + links.forEach((link, i) => { + const linkBase = link.split("&rect=")[0]; + if (!linkMap.has(linkBase)) linkMap.set(linkBase, []); + linkMap.get(linkBase).push(i); + }); + + // Determine indices to keep + const indicesToKeep = new Set(); + linkMap.forEach(indices => { + if (indices.length === 1) { + // Only one link, keep it + indicesToKeep.add(indices[0]); + } else { + // Multiple links: prefer the one with rect reference, if available + const rectIndex = indices.find(i => links[i].includes("&rect=")); + if (rectIndex !== undefined) { + indicesToKeep.add(rectIndex); + } else { + // No rect reference in duplicates, add the first one + indicesToKeep.add(indices[0]); + } + } + }); + + // Final validation to ensure each duplicate group has at least one entry + linkMap.forEach(indices => { + const hasKeptEntry = indices.some(i => indicesToKeep.has(i)); + if (!hasKeptEntry) { + // Add the first index if none were kept + indicesToKeep.add(indices[0]); + } + }); + + // Filter linksArray, links, itemsDisplay, and items based on indicesToKeep + const filteredLinksArray = linksArray.filter((_, i) => indicesToKeep.has(i)); + const tagsArray = REGEX_TAGS.getResList(linkText.replaceAll(/([^\s])#/g, "$1 ")).filter(x => Boolean(x.value)); + let subpath: string = null; let file: TFile = null; - let parts = linksArray[0] ?? tagsArray[0]; + let parts = filteredLinksArray[0] ?? tagsArray[0]; + + // Generate filtered itemsDisplay and items arrays const itemsDisplay = [ - ...linksArray.filter(p=> Boolean(p.value)).map(p => { + ...filteredLinksArray.map(p => { const alias = REGEX_LINK.getAliasOrLink(p); return alias === "100%" ? REGEX_LINK.getLink(p) : alias; }), - ...tagsArray.filter(x=> Boolean(x.value)).map(x => REGEX_TAGS.getTag(x)), + ...tagsArray.map(x => REGEX_TAGS.getTag(x)), ]; + const items = [ - ...linksArray.filter(p=>Boolean(p.value)), - ...tagsArray.filter(x=> Boolean(x.value)), + ...filteredLinksArray, + ...tagsArray, ]; if (items.length>1) { diff --git a/src/lang/locale/en.ts b/src/lang/locale/en.ts index 6d207f1..b0ebeea 100644 --- a/src/lang/locale/en.ts +++ b/src/lang/locale/en.ts @@ -76,6 +76,7 @@ export default { IMPORT_SVG_CONTEXTMENU: "Convert SVG to strokes - with limitations", INSERT_MD: "Insert markdown file from vault", INSERT_PDF: "Insert PDF file from vault", + INSERT_LAST_ACTIVE_PDF_PAGE_AS_IMAGE: "Insert last active PDF page as image", UNIVERSAL_ADD_FILE: "Insert ANY file", INSERT_CARD: "Add back-of-note card", CONVERT_CARD_TO_FILE: "Move back-of-note card to File", @@ -101,6 +102,9 @@ export default { FONTS_LOADED: "Excalidraw: CJK Fonts loaded", FONTS_LOAD_ERROR: "Excalidraw: Could not find CJK Fonts in the assets folder\n", + //Prompt.ts + SELECT_LINK_TO_OPEN: "Select a link to open", + //ExcalidrawView.ts NO_SEARCH_RESULT: "Didn't find a matching element in the drawing", FORCE_SAVE_ABORTED: "Force Save aborted because saving is in progress", diff --git a/src/main.ts b/src/main.ts index 3162ad2..23b9b8e 100644 --- a/src/main.ts +++ b/src/main.ts @@ -20,6 +20,7 @@ import { Editor, MarkdownFileInfo, loadMermaid, + View, } from "obsidian"; import { BLANK_DRAWING, @@ -143,7 +144,8 @@ import { RankMessage } from "./dialogs/RankMessage"; import { initCompressionWorker, terminateCompressionWorker } from "./workers/compression-worker"; import { WeakArray } from "./utils/WeakArray"; import { getCJKDataURLs } from "./utils/CJKLoader"; -import ExcalidrawLoading from "./dialogs/ExcalidrawLoading"; +import { ExcalidrawLoading, switchToExcalidraw } from "./dialogs/ExcalidrawLoading"; +import { insertImageToView } from "./utils/ExcalidrawViewUtils"; declare let EXCALIDRAW_PACKAGE:string; declare let REACT_PACKAGES:string; @@ -432,6 +434,7 @@ export default class ExcalidrawPlugin extends Plugin { this.taskbone = new Taskbone(this); this.isReady = true; + switchToExcalidraw(this.app); this.app.workspace.onLayoutReady(() => { (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.onload,"ExcalidrawPlugin.onload > app.workspace.onLayoutReady"); @@ -2447,6 +2450,27 @@ export default class ExcalidrawPlugin extends Plugin { }, }); + this.addCommand({ + id: "insert-pdf", + name: t("INSERT_LAST_ACTIVE_PDF_PAGE_AS_IMAGE"), + checkCallback: (checking: boolean) => { + const view = this.app.workspace.getActiveViewOfType(ExcalidrawView); + if(!Boolean(view)) return false; + const PDFLink = this.getLastActivePDFPageLink(view.file); + if(!PDFLink) return false; + if(checking) return true; + const ea = getEA(view); + insertImageToView( + ea, + view.currentPosition, + PDFLink, + undefined, + undefined, + true, + ); + }, + }); + this.addCommand({ id: "universal-add-file", name: t("UNIVERSAL_ADD_FILE"), @@ -2814,9 +2838,33 @@ export default class ExcalidrawPlugin extends Plugin { }); } + private lastPDFLeafID: string = null; + + public getLastActivePDFPageLink(requestorFile: TFile): string { + if(!this.lastPDFLeafID) return; + const leaf = this.app.workspace.getLeafById(this.lastPDFLeafID); + //@ts-ignore + if(!leaf || !leaf.view || leaf.view.getViewType() !== "pdf") return; + const view:any = leaf.view; + const file = view.file; + const page = view.viewer.child.pdfViewer.page; + if(!file || !page) return; + return this.app.metadataCache.fileToLinktext( + file, + requestorFile?.path, + false, + ) + `#page=${page}`; + } + public async activeLeafChangeEventHandler (leaf: WorkspaceLeaf) { (process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.activeLeafChangeEventHandler,`ExcalidrawPlugin.activeLeafChangeEventHandler`, leaf); //https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/723 + + if (leaf.view && leaf.view.getViewType() === "pdf") { + //@ts-ignore + this.lastPDFLeafID = leaf.id; + } + if(this.leafChangeTimeout) { window.clearTimeout(this.leafChangeTimeout); } diff --git a/src/utils/ExcalidrawViewUtils.ts b/src/utils/ExcalidrawViewUtils.ts index 967b27f..c9fa602 100644 --- a/src/utils/ExcalidrawViewUtils.ts +++ b/src/utils/ExcalidrawViewUtils.ts @@ -19,6 +19,7 @@ export async function insertImageToView( file: TFile | string, scale?: boolean, shouldInsertToView: boolean = true, + repositionToCursor: boolean = false, ):Promise { if(shouldInsertToView) {ea.clear();} ea.style.strokeColor = "transparent"; @@ -31,7 +32,7 @@ export async function insertImageToView( file, scale, ); - if(shouldInsertToView) {await ea.addElementsToView(false, true, true);} + if(shouldInsertToView) {await ea.addElementsToView(repositionToCursor, true, true);} return id; } diff --git a/src/utils/PDFUtils.ts b/src/utils/PDFUtils.ts index e8d6e91..6e5901c 100644 --- a/src/utils/PDFUtils.ts +++ b/src/utils/PDFUtils.ts @@ -1,27 +1,37 @@ //for future use, not used currently import { ImageCrop } from "@zsviczian/excalidraw/types/excalidraw/element/types"; -import { LinkParts } from "./Utils"; export function getPDFCropRect (props: { scale: number, - linkParts: LinkParts, + link: string, naturalHeight: number, naturalWidth: number, }) : ImageCrop | null { - const cropRect = props.linkParts.ref.split("rect=")[1]?.split(",").map(x=>parseInt(x)); - const validRect = cropRect && cropRect.length === 4 && cropRect.every(x=>!isNaN(x)); - - if(!validRect) { + const rectVal = props.link.match(/&rect=(\d*),(\d*),(\d*),(\d*)/); + if (!rectVal || rectVal.length !== 5) { return null; } + const R0 = parseInt(rectVal[1]); + const R1 = parseInt(rectVal[2]); + const R2 = parseInt(rectVal[3]); + const R3 = parseInt(rectVal[4]); + return { - x: cropRect[0] * props.scale, - y: (props.naturalHeight/props.scale - cropRect[3]) * props.scale, - width: (cropRect[2] - cropRect[0]) * props.scale, - height: (cropRect[3] - cropRect[1]) * props.scale, + x: R0 * props.scale, + y: (props.naturalHeight/props.scale - R3) * props.scale, + width: (R2 - R0) * props.scale, + height: (R3 - R1) * 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)}`; } \ No newline at end of file diff --git a/src/utils/Utils.ts b/src/utils/Utils.ts index f136996..3991fc7 100644 --- a/src/utils/Utils.ts +++ b/src/utils/Utils.ts @@ -477,8 +477,6 @@ export function scaleLoadedImage ( const ratioX = elCrop.x / (elCrop.naturalWidth - elCrop.x - elCrop.width); const gapX = imgWidth - elCrop.width; el.crop.x = ratioX * gapX / (1 + ratioX); -// const ratioA = elCrop.x / (elCrop.naturalWidth - elCrop.x); -// el.crop.x = ratioA * imgWidth / (1 + ratioA); if(el.crop.x + elCrop.width > imgWidth) { el.crop.x = (imgWidth - elCrop.width) / 2; } @@ -492,8 +490,6 @@ export function scaleLoadedImage ( const ratioY = elCrop.y / (elCrop.naturalHeight - elCrop.y - elCrop.height); const gapY = imgHeight - elCrop.height; el.crop.y = ratioY * gapY / (1 + ratioY); -// const ratioB = elCrop.y / (elCrop.naturalHeight - elCrop.y); -// el.crop.y = ratioB * imgHeight / (1 + ratioB); if(el.crop.y + elCrop.height > imgHeight) { el.crop.y = (imgHeight - elCrop.height)/2; }