Compare commits

...

3 Commits

Author SHA1 Message Date
zsviczian
5cbd98e543 Merge pull request #2092 from dmscode/master
Update zh-cn.ts  to dec2909
2024-11-02 07:45:49 +01:00
dmscode
e2d5966ca3 Update zh-cn.ts to dec2909 2024-11-01 18:37:48 +08:00
zsviczian
dec2909db0 2.6.2-beta-2, 0.17.6-10 PDFCropping 2024-11-01 07:44:58 +01:00
15 changed files with 207 additions and 94 deletions

View File

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

View File

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

View File

@@ -1108,66 +1108,8 @@ export class EmbeddedFilesLoader {
const getSVGData = async (app: App, file: TFile, colorMap: ColorMap | null): Promise<DataURL> => {
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<FileId> => {
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<FileId> => {
let id: FileId;
try {

View File

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

View File

@@ -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<ExcalidrawImageElement>)=>{
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() ||

View File

@@ -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<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 (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<ExcalidrawImageElement>;
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);

View File

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

View File

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

View File

@@ -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<string, number[]>();
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<number>();
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) {

View File

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

View File

@@ -76,6 +76,7 @@ export default {
IMPORT_SVG_CONTEXTMENU: "转换 SVG 到线条 - 有限制",
INSERT_MD: "插入 Markdown 文档(以图像形式嵌入)到当前绘图中",
INSERT_PDF: "插入 PDF 文档(以图像形式嵌入)到当前绘图中",
INSERT_LAST_ACTIVE_PDF_PAGE_AS_IMAGE: "将最后激活的 PDF 页面插入为图片",
UNIVERSAL_ADD_FILE: "插入任意文件(以交互形式嵌入,或者以图像形式嵌入)到当前绘图中",
INSERT_CARD: "插入“背景笔记”卡片",
CONVERT_CARD_TO_FILE: "将“背景笔记”卡片保存到文件",
@@ -101,6 +102,9 @@ export default {
FONTS_LOADED : "Excalidraw: CJK 字体已加载" ,
FONTS_LOAD_ERROR : "Excalidraw: 在资源文件夹下找不到 CJK 字体\n" ,
//Prompt.ts
SELECT_LINK_TO_OPEN: "选择要打开的链接",
//ExcalidrawView.ts
NO_SEARCH_RESULT: "在绘图中未找到匹配的元素",
FORCE_SAVE_ABORTED: "自动保存被中止,因为文件正在保存中",

View File

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

View File

@@ -19,6 +19,7 @@ export async function insertImageToView(
file: TFile | string,
scale?: boolean,
shouldInsertToView: boolean = true,
repositionToCursor: boolean = false,
):Promise<string> {
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;
}

View File

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

View File

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