Skip to content

Commit

Permalink
Image mask offset, PDF drift and offset, addAppendUpdateCustomData on…
Browse files Browse the repository at this point in the history
… EA, some type cleanup
  • Loading branch information
zsviczian committed Jan 4, 2025
1 parent 9053313 commit 1562600
Show file tree
Hide file tree
Showing 10 changed files with 222 additions and 90 deletions.
11 changes: 10 additions & 1 deletion src/core/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,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;
Expand All @@ -113,7 +122,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
Expand Down
37 changes: 33 additions & 4 deletions src/shared/CropImage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,30 @@ 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;
Expand Down Expand Up @@ -106,10 +130,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);
}
Expand Down
9 changes: 9 additions & 0 deletions src/shared/Dialogs/SuggesterInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;",
Expand Down
61 changes: 40 additions & 21 deletions src/shared/EmbeddedFileLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -82,8 +83,16 @@ 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;
}

export type Size = {
height: number;
width: number;
Expand Down Expand Up @@ -177,6 +186,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;
Expand Down Expand Up @@ -252,12 +262,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;
Expand All @@ -266,6 +278,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) {
Expand Down Expand Up @@ -345,6 +358,7 @@ export class EmbeddedFilesLoader {
created: number;
hasSVGwithBitmap: boolean;
size: { height: number; width: number };
pdfPageViewProps?: PDFPageViewProps;
}> {
const result = await this._getObsidianImage(inFile, depth);
this.emptyPDFDocsMap();
Expand Down Expand Up @@ -552,9 +566,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"
Expand Down Expand Up @@ -600,6 +614,7 @@ export class EmbeddedFilesLoader {
created: isHyperLink || isLocalLink ? 0 : file.stat.mtime,
hasSVGwithBitmap,
size,
pdfPageViewProps,
};
} catch(e) {
return null;
Expand Down Expand Up @@ -634,7 +649,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];
Expand All @@ -654,20 +669,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);
}
Expand Down Expand Up @@ -803,7 +820,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);
Expand All @@ -814,6 +831,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) => {
Expand All @@ -824,8 +842,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,
Expand All @@ -846,9 +864,10 @@ export class EmbeddedFilesLoader {
continue;
}
}
const [left, bottom, right, top] = page.view;
viewProps = {left, bottom, right, top};

if(validRect) {
const [left, bottom, _, top] = page.view;

const pageHeight = top - bottom;
width = (cropRect[2] - cropRect[0]) * scale;
height = (cropRect[3] - cropRect[1]) * scale;
Expand All @@ -868,19 +887,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];
}
}

Expand Down
28 changes: 23 additions & 5 deletions src/shared/ExcalidrawAutomate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ import {
scaleLoadedImage,
wrapTextAtCharLength,
arrayToMap,
addAppendUpdateCustomData,
} from "src/utils/utils";
import { getAttachmentsFolderAndFilePath, getExcalidrawViews, getLeaf, getNewOrAdjacentLeaf, isObsidianThemeDark, mergeMarkdownFiles, openLeaf } from "src/utils/obsidianUtils";
import { AppState, BinaryFileData, DataURL, ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/excalidraw/types";
Expand Down Expand Up @@ -97,7 +98,6 @@ import { ExcalidrawLib } from "../types/excalidrawLib";
import { GlobalPoint } from "@zsviczian/excalidraw/types/math/types";
import { AddImageOptions, ImageInfo, SVGColorInfo } from "src/types/excalidrawAutomateTypes";
import { errorMessage, filterColorMap, getEmbeddedFileForImageElment, isColorStringTransparent, isSVGColorInfo, mergeColorMapIntoSVGColorInfo, svgColorInfoToColorMap, updateOrAddSVGColorInfo } from "src/utils/excalidrawAutomateUtils";
import { Color } from "chroma-js";

extendPlugins([
HarmonyPlugin,
Expand Down Expand Up @@ -141,6 +141,23 @@ export class ExcalidrawAutomate {
this.plugin.printStarupBreakdown();
}

/**
* Add, modify keys in element customData and preserve existing keys.
* Creates customData={} if it does not exist.
* Takes the element ID for an element in the elementsDict and the new data 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>>) {
const el = this.elementsDict[id];
if (!el) {
return;
}
return addAppendUpdateCustomData(el,newData);
}

public help(target: Function | string) {
if (!target) {
log("Usage: ea.help(ea.functionName) or ea.help('propertyName') or ea.help('utils.functionName') - notice property name and utils function name is in quotes");
Expand Down Expand Up @@ -1596,6 +1613,7 @@ export class ExcalidrawAutomate {
width: image.size.width,
},
colorMap,
pdfPageViewProps: image.pdfPageViewProps,
};
if (scale && (Math.max(image.size.width, image.size.height) > MAX_IMAGE_SIZE)) {
const scale =
Expand Down Expand Up @@ -2392,14 +2410,14 @@ export class ExcalidrawAutomate {
return false;
}
const elements = this.getElements();
return await this.targetView.addElements(
elements,
return await this.targetView.addElements({
newElements: elements,
repositionToCursor,
save,
this.imagesDict,
images: this.imagesDict,
newElementsOnTop,
shouldRestoreElements,
);
});
};

/**
Expand Down
19 changes: 10 additions & 9 deletions src/shared/ExcalidrawData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -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;
}
Expand All @@ -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;
Expand Down Expand Up @@ -1992,7 +1993,7 @@ export class ExcalidrawData {
isLocalLink: data.isLocalLink,
path: data.hyperlink,
blockrefData: null,
hasSVGwithBitmap: data.isSVGwithBitmap
hasSVGwithBitmap: data.isSVGwithBitmap,
});
return;
}
Expand Down
Loading

0 comments on commit 1562600

Please sign in to comment.