Compare commits

...

6 Commits

Author SHA1 Message Date
zsviczian
d529a04f48 2.4.0-beta-6, pdf++ crop support, double tap eraser disable 2024-08-14 20:02:41 +02:00
zsviczian
8786c5aa99 2.4.0-beta-5 2024-08-14 10:07:55 +02:00
zsviczian
013279ab60 2.2.4-beta-4, markdown post processor, PDF frames, selectFrameElements 2024-08-13 22:53:03 +02:00
zsviczian
06193b6d49 2.4.0-beta-3 2024-08-11 09:22:13 +02:00
zsviczian
ac6f4af5d6 Merge pull request #1928 from mProjectsCode/patch-1
Fix `authorUrl` in manifest
2024-08-11 08:42:11 +02:00
Moritz Jung
0f9dafb01d Fix authorUrl in manifest 2024-08-09 22:16:07 +02:00
15 changed files with 413 additions and 142 deletions

View File

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

View File

@@ -5,8 +5,8 @@
"minAppVersion": "1.1.6",
"description": "An Obsidian plugin to edit and view Excalidraw drawings",
"author": "Zsolt Viczian",
"authorUrl": "https://zsolt.blog",
"authorUrl": "https://www.zsolt.blog",
"fundingUrl": "https://ko-fi.com/zsolt",
"helpUrl": "https://github.com/zsviczian/obsidian-excalidraw-plugin#readme",
"isDesktopOnly": false
}
}

View File

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

View File

@@ -15,7 +15,7 @@ import cssnano from 'cssnano';
import dotenv from 'dotenv';
dotenv.config();
const DIST_FOLDER = 'dist';
const DIST_FOLDER = 'dist';
const isProd = (process.env.NODE_ENV === "production");
const isLib = (process.env.NODE_ENV === "lib");
console.log(`Running: ${process.env.NODE_ENV}`);

View File

@@ -35,6 +35,7 @@ import {
svgToBase64,
isMaskFile,
getEmbeddedFilenameParts,
cropCanvas,
} from "./utils/Utils";
import { ValueOf } from "./types/types";
import { getMermaidImageElements, getMermaidText, shouldRenderMermaid } from "./utils/MermaidUtils";
@@ -746,6 +747,8 @@ export class EmbeddedFilesLoader {
}
const pageNum = isNaN(linkParts.page) ? 1 : (linkParts.page??1);
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));
// Render the page
const renderPage = async (num:number) => {
@@ -766,6 +769,23 @@ export class EmbeddedFilesLoader {
};
await page.render(renderCtx).promise;
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 crop = validRect ? {
left: (cropRect[0] - left) * scale,
top: (bottom + pageHeight - cropRect[3]) * scale,
width,
height,
} : undefined;
if(crop) {
return cropCanvas(canvas, crop);
}
}
return canvas;
};

View File

@@ -13,6 +13,7 @@ import {
ExcalidrawFrameElement,
ExcalidrawTextContainer,
} from "@zsviczian/excalidraw/types/excalidraw/element/types";
import { MimeType } from "./EmbeddedFileLoader";
import { Editor, normalizePath, Notice, OpenViewState, RequestUrlResponse, TFile, TFolder, WorkspaceLeaf } from "obsidian";
import * as obsidian_module from "obsidian";
import ExcalidrawView, { ExportSettings, TextMode, getTextMode } from "src/ExcalidrawView";
@@ -1026,6 +1027,22 @@ export class ExcalidrawAutomate {
return id;
};
/**
* Add elements to frame
* @param frameId
* @param elementIDs to add
* @returns void
*/
addElementsToFrame(frameId: string, elementIDs: string[]):void {
if(!this.getElement(frameId)) return;
elementIDs.forEach(elID => {
const el = this.getElement(elID);
if(el) {
el.frameId = frameId;
}
});
}
/**
*
* @param topX
@@ -1571,6 +1588,26 @@ export class ExcalidrawAutomate {
return id;
};
/**
* returns the base64 dataURL of the LaTeX equation rendered as an SVG
* @param tex The LaTeX equation as string
* @param scale of the image, default value is 4
* @returns
*/
//https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/1930
async tex2dataURL(
tex: string,
scale: number = 4 // Default scale value, adjust as needed
): Promise<{
mimeType: MimeType;
fileId: FileId;
dataURL: DataURL;
created: number;
size: { height: number; width: number };
}> {
return await tex2dataURL(tex,scale);
};
/**
*
* @param objectA
@@ -1896,15 +1933,16 @@ export class ExcalidrawAutomate {
/**
*
* @param includeFrameChildren
* @returns
*/
getViewSelectedElements(): any[] {
getViewSelectedElements(includeFrameChildren:boolean = true): any[] {
//@ts-ignore
if (!this.targetView || !this.targetView?._loaded) {
errorMessage("targetView not set", "getViewSelectedElements()");
return [];
}
return this.targetView.getViewSelectedElements();
return this.targetView.getViewSelectedElements(includeFrameChildren);
};
/**
@@ -2396,24 +2434,44 @@ export class ExcalidrawAutomate {
* @param elements - typically all the non-deleted elements in the scene
* @returns
*/
getElementsInTheSameGroupWithElement(element: ExcalidrawElement, elements: ExcalidrawElement[]): ExcalidrawElement[] {
getElementsInTheSameGroupWithElement(
element: ExcalidrawElement,
elements: ExcalidrawElement[],
includeFrameElements: boolean = false,
): ExcalidrawElement[] {
if(!element || !elements) return [];
const container = (element.type === "text" && element.containerId)
? elements.filter(el=>el.id === element.containerId)
: [];
if(element.groupIds.length === 0) {
if(includeFrameElements && element.type === "frame") {
return this.getElementsInFrame(element,elements,true);
}
if(container.length === 1) return [element,container[0]];
return [element];
}
if(container.length === 1) {
return elements.filter(el=>
el.groupIds.some(id=>element.groupIds.includes(id)) ||
el === container[0]
);
const conditionFN = container.length === 1
? (el: ExcalidrawElement) => el.groupIds.some(id=>element.groupIds.includes(id)) || el === container[0]
: (el: ExcalidrawElement) => el.groupIds.some(id=>element.groupIds.includes(id));
if(!includeFrameElements) {
return elements.filter(el=>conditionFN(el));
} else {
//I use the set and the filter at the end to preserve scene layer seqeuence
//adding frames could potentially mess up the sequence otherwise
const elementIDs = new Set<string>();
elements
.filter(el=>conditionFN(el))
.forEach(el=>{
if(el.type === "frame") {
this.getElementsInFrame(el,elements,true).forEach(el=>elementIDs.add(el.id))
} else {
elementIDs.add(el.id);
}
});
return elements.filter(el=>elementIDs.has(el.id));
}
return elements.filter(el=>el.groupIds.some(id=>element.groupIds.includes(id)));
}
/**
@@ -2738,7 +2796,7 @@ export async function initExcalidrawAutomate(
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++) {
@@ -2747,7 +2805,9 @@ function normalizeLinePoints(
return p;
}
function getLineBox(points: [x: number, y: number][]) {
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,
@@ -2757,11 +2817,11 @@ function getLineBox(points: [x: number, y: number][]) {
};
}
function getFontFamily(id: number) {
getFontFamilyString({fontFamily:id})
function getFontFamily(id: number):string {
return getFontFamilyString({fontFamily:id})
}
export async function initFonts() {
export async function initFonts():Promise<void> {
await excalidrawLib.registerFontsInCSS();
const fonts = excalidrawLib.getFontFamilies();
for(let i=0;i<fonts.length;i++) {
@@ -2774,7 +2834,7 @@ export function _measureText(
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;
@@ -2873,7 +2933,7 @@ async function getTemplate(
? 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)
groupElements = plugin.ea.getElementsInTheSameGroupWithElement(el[0],scene.elements,true)
}
}
if(filenameParts.hasFrameref || filenameParts.hasClippedFrameref) {
@@ -2932,7 +2992,7 @@ export async function createPNG(
depth: number,
padding?: number,
imagesDict?: any,
) {
): Promise<Blob> {
if (!loader) {
loader = new EmbeddedFilesLoader(plugin);
}
@@ -3016,7 +3076,7 @@ export const updateElementLinksToObsidianLinks = ({elements, hostFile}:{
})
}
function addFilterToForeignObjects(svg:SVGSVGElement) {
function addFilterToForeignObjects(svg:SVGSVGElement):void {
const foreignObjects = svg.querySelectorAll("foreignObject");
foreignObjects.forEach((foreignObject) => {
foreignObject.setAttribute("filter", THEME_FILTER);
@@ -3165,7 +3225,7 @@ export function repositionElementsToCursor(
return restore({elements}, null, null).elements;
}
function errorMessage(message: string, source: string) {
function errorMessage(message: string, source: string):void {
switch (message) {
case "targetView not set":
errorlog({

View File

@@ -3766,8 +3766,14 @@ export default class ExcalidrawView extends TextFileView {
ea.selectElementsInView([await insertEmbeddableToView (ea, this.currentPosition, file, link)]);
ea.destroy();
} else {
const modal = new UniversalInsertFileModal(this.plugin, this);
modal.open(file, this.currentPosition);
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);
ea.addElementsToView(false,false).then(()=>ea.destroy());
} else {
const modal = new UniversalInsertFileModal(this.plugin, this);
modal.open(file, this.currentPosition);
}
}
this.setDirty(9);
})) {
@@ -5681,9 +5687,14 @@ export default class ExcalidrawView extends TextFileView {
return api.getSceneElements();
}
public getViewSelectedElements(): ExcalidrawElement[] {
/**
*
* @param deepSelect: if set to true, child elements of the selected frame will also be selected
* @returns
*/
public getViewSelectedElements(includFrameChildren: boolean = true): ExcalidrawElement[] {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.getViewSelectedElements, "ExcalidrawView.getViewSelectedElements");
const api = this.excalidrawAPI;
const api = this.excalidrawAPI as ExcalidrawImperativeAPI;
if (!api) {
return [];
}
@@ -5695,6 +5706,9 @@ export default class ExcalidrawView extends TextFileView {
if (!selectedElementsKeys) {
return [];
}
const elementIDs = new Set<string>();
const elements: ExcalidrawElement[] = api
.getSceneElements()
.filter((e: any) => selectedElementsKeys.includes(e.id));
@@ -5712,15 +5726,27 @@ export default class ExcalidrawView extends TextFileView {
.map((be) => be.id)[0],
);
const elementIDs = elements
.map((el) => el.id)
.concat(containerBoundTextElmenetsReferencedInElements);
if(includFrameChildren && elements.some(el=>el.type === "frame")) {
elements.filter(el=>el.type === "frame").forEach(frameEl => {
api.getSceneElements()
.filter(el=>el.frameId === frameEl.id)
.forEach(el=>elementIDs.add(el.id))
})
}
elements.forEach(el=>elementIDs.add(el.id));
containerBoundTextElmenetsReferencedInElements.forEach(id=>elementIDs.add(id));
return api
.getSceneElements()
.filter((el: ExcalidrawElement) => elementIDs.contains(el.id));
.filter((el: ExcalidrawElement) => elementIDs.has(el.id));
}
/**
*
* @param prefix - defines the default button.
* @returns
*/
public async copyLinkToSelectedElementToClipboard(prefix:string) {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(this.copyLinkToSelectedElementToClipboard, "ExcalidrawView.copyLinkToSelectedElementToClipboard", prefix);
const elements = this.getViewSelectedElements();
@@ -5747,58 +5773,59 @@ export default class ExcalidrawView extends TextFileView {
: this.plugin.ea.getLargestElement(elements).id;
}
const isFrame = elements.some(el=>el.id === elementId && el.type==="frame");
const frames = elements.filter(el=>el.type==="frame");
const hasFrame = frames.length === 1;
const hasGroup = elements.some(el=>el.groupIds && el.groupIds.length>0);
let button = {
area: {caption: "Area", action:()=>{prefix="area="; return;}},
link: {caption: "Link", action:()=>{prefix="";return}},
group: {caption: "Group", action:()=>{prefix="group="; return;}},
frame: {caption: "Frame", action:()=>{prefix="frame="; elementId = frames[0].id; return;}},
clippedframe: {caption: "Clipped Frame", action:()=>{prefix="clippedframe="; ; elementId = frames[0].id; return;}},
}
let buttons = [];
if(isFrame) {
switch(prefix) {
case "clippedframe=":
buttons = [
{caption: "Clipped Frame", action:()=>{prefix="clippedframe="; return;}},
{caption: "Frame", action:()=>{prefix="frame="; return;}},
{caption: "Link", action:()=>{prefix="";return}},
];
break;
case "area=":
case "group=":
case "frame=":
buttons = [
{caption: "Frame", action:()=>{prefix="frame="; return;}},
{caption: "Clipped Frame", action:()=>{prefix="clippedframe="; return;}},
{caption: "Link", action:()=>{prefix="";return}},
];
break;
default:
buttons = [
{caption: "Link", action:()=>{prefix="";return}},
{caption: "Frame", action:()=>{prefix="frame="; return;}},
{caption: "Clipped Frame", action:()=>{prefix="clippedframe="; return;}},
]
}
} else {
switch(prefix) {
case "area=":
buttons = [
{caption: "Area", action:()=>{prefix="area="; return;}},
{caption: "Link", action:()=>{prefix="";return}},
{caption: "Group", action:()=>{prefix="group="; return;}},
];
break;
case "group=":
buttons = [
{caption: "Group", action:()=>{prefix="group="; return;}},
{caption: "Link", action:()=>{prefix="";return}},
{caption: "Area", action:()=>{prefix="area="; return;}},
];
break;
default:
buttons = [
{caption: "Link", action:()=>{prefix="";return}},
{caption: "Area", action:()=>{prefix="area="; return;}},
{caption: "Group", action:()=>{prefix="group="; return;}},
]
}
switch(prefix) {
case "area=":
buttons = [
button.area,
button.link,
...hasGroup ? [button.group] : [],
...hasFrame ? [button.frame, button.clippedframe] : [],
];
break;
case "group=":
buttons = [
...hasGroup ? [button.group] : [],
button.link,
button.area,
...hasFrame ? [button.frame, button.clippedframe] : [],
];
break;
case "frame=":
buttons = [
...hasFrame ? [button.frame, button.clippedframe] : [],
...hasGroup ? [button.group] : [],
button.link,
button.area,
];
break;
case "clippedframe=":
buttons = [
...hasFrame ? [button.clippedframe, button.frame] : [],
...hasGroup ? [button.group] : [],
button.link,
button.area,
];
break;
default:
buttons = [
{caption: "Link", action:()=>{prefix="";return}},
{caption: "Area", action:()=>{prefix="area="; return;}},
{caption: "Group", action:()=>{prefix="group="; return;}},
...hasFrame ? [button.frame, button.clippedframe] : [],
]
}
const alias = await ScriptEngine.inputPrompt(

View File

@@ -1,4 +1,5 @@
import {
App,
MarkdownPostProcessorContext,
MetadataCache,
PaneType,
@@ -25,7 +26,7 @@ import { getParentOfClass, isObsidianThemeDark, getFileCSSClasses } from "./util
import { linkClickModifierType } from "./utils/ModifierkeyHelper";
import { ImageKey, imageCache } from "./utils/ImageCache";
import { FILENAMEPARTS, PreviewImageType } from "./utils/UtilTypes";
import { CustomMutationObserver, DEBUGGING } from "./utils/DebugHelper";
import { CustomMutationObserver, debug, DEBUGGING } from "./utils/DebugHelper";
import { getExcalidrawFileForwardLinks } from "./utils/ExcalidrawViewUtils";
import { linkPrompt } from "./dialogs/Prompt";
@@ -38,8 +39,11 @@ interface imgElementAttributes {
}
let plugin: ExcalidrawPlugin;
let app: App;
let vault: Vault;
let metadataCache: MetadataCache;
const DEBUGGING_MPP = false;
const getDefaultWidth = (plugin: ExcalidrawPlugin): string => {
const width = parseInt(plugin.settings.width);
@@ -60,8 +64,9 @@ const getDefaultHeight = (plugin: ExcalidrawPlugin): string => {
export const initializeMarkdownPostProcessor = (p: ExcalidrawPlugin) => {
plugin = p;
vault = p.app.vault;
metadataCache = p.app.metadataCache;
app = plugin.app;
vault = app.vault;
metadataCache = app.metadataCache;
};
const _getPNG = async ({imgAttributes,filenameParts,theme,cacheReady,img,file,exportSettings,loader}:{
@@ -74,6 +79,7 @@ const _getPNG = async ({imgAttributes,filenameParts,theme,cacheReady,img,file,ex
exportSettings: ExportSettings,
loader: EmbeddedFilesLoader,
}):Promise<HTMLImageElement> => {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(_getPNG, `MarkdownPostProcessor.ts > _getPNG`);
const width = parseInt(imgAttributes.fwidth);
const scale = width >= 2400
? 5
@@ -140,6 +146,7 @@ const setStyle = ({element,imgAttributes,onCanvas}:{
onCanvas: boolean,
}
) => {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(setStyle, `MarkdownPostProcessor.ts > setStyle`);
let style = "";
if(imgAttributes.fwidth) {
style = `max-width:${imgAttributes.fwidth}${imgAttributes.fwidth.match(/\d$/) ? "px":""}; `; //width:100%;`; //removed !important https://github.com/zsviczian/obsidian-excalidraw-plugin/issues/886
@@ -171,6 +178,7 @@ const _getSVGIMG = async ({filenameParts,theme,cacheReady,img,file,exportSetting
exportSettings: ExportSettings,
loader: EmbeddedFilesLoader,
}):Promise<HTMLImageElement> => {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(_getSVGIMG, `MarkdownPostProcessor.ts > _getSVGIMG`);
exportSettings.skipInliningFonts = false;
const cacheKey = {
...filenameParts,
@@ -238,6 +246,7 @@ const _getSVGNative = async ({filenameParts,theme,cacheReady,containerElement,fi
exportSettings: ExportSettings,
loader: EmbeddedFilesLoader,
}):Promise<HTMLDivElement> => {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(_getSVGNative, `MarkdownPostProcessor.ts > _getSVGNative`);
exportSettings.skipInliningFonts = false;
const cacheKey = {
...filenameParts,
@@ -300,6 +309,7 @@ const getIMG = async (
imgAttributes: imgElementAttributes,
onCanvas: boolean = false,
): Promise<HTMLImageElement | HTMLDivElement> => {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(getIMG, `MarkdownPostProcessor.ts > getIMG`, imgAttributes);
let file = imgAttributes.file;
if (!imgAttributes.file) {
const f = vault.getAbstractFileByPath(imgAttributes.fname?.split("#")[0]);
@@ -347,22 +357,23 @@ const getIMG = async (
case PreviewImageType.PNG: {
const img = createEl("img");
setStyle({element:img,imgAttributes,onCanvas});
return _getPNG({imgAttributes,filenameParts,theme,cacheReady,img,file,exportSettings,loader});
return await _getPNG({imgAttributes,filenameParts,theme,cacheReady,img,file,exportSettings,loader});
}
case PreviewImageType.SVGIMG: {
const img = createEl("img");
setStyle({element:img,imgAttributes,onCanvas});
return _getSVGIMG({filenameParts,theme,cacheReady,img,file,exportSettings,loader});
return await _getSVGIMG({filenameParts,theme,cacheReady,img,file,exportSettings,loader});
}
case PreviewImageType.SVG: {
const img = createEl("div");
setStyle({element:img,imgAttributes,onCanvas});
return _getSVGNative({filenameParts,theme,cacheReady,containerElement: img,file,exportSettings,loader});
return await _getSVGNative({filenameParts,theme,cacheReady,containerElement: img,file,exportSettings,loader});
}
}
};
const addSVGToImgSrc = (img: HTMLImageElement, svg: SVGSVGElement, cacheReady: boolean, cacheKey: ImageKey):HTMLImageElement => {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(addSVGToImgSrc, `MarkdownPostProcessor.ts > addSVGToImgSrc`);
const svgString = new XMLSerializer().serializeToString(svg);
const blob = new Blob([svgString], { type: 'image/svg+xml' });
const blobUrl = URL.createObjectURL(blob);
@@ -375,6 +386,7 @@ const createImgElement = async (
attr: imgElementAttributes,
onCanvas: boolean = false,
) :Promise<HTMLElement> => {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(createImgElement, `MarkdownPostProcessor.ts > createImgElement`);
const imgOrDiv = await getIMG(attr,onCanvas);
if(!imgOrDiv) {
return null;
@@ -502,6 +514,7 @@ const createImageDiv = async (
attr: imgElementAttributes,
onCanvas: boolean = false
): Promise<HTMLDivElement> => {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(createImageDiv, `MarkdownPostProcessor.ts > createImageDiv`);
const img = await createImgElement(attr, onCanvas);
return createDiv(attr.style.join(" "), (el) => el.append(img));
};
@@ -510,6 +523,7 @@ const processReadingMode = async (
embeddedItems: NodeListOf<Element> | [HTMLElement],
ctx: MarkdownPostProcessorContext,
) => {
(process.env.NODE_ENV === 'development') && DEBUGGING_MPP && debug(processReadingMode, `MarkdownPostProcessor.ts > processReadingMode`);
//We are processing a non-excalidraw file in reading mode
//Embedded files will be displayed in an .internal-embed container
@@ -541,6 +555,7 @@ const processReadingMode = async (
};
const processInternalEmbed = async (internalEmbedEl: Element, file: TFile ):Promise<HTMLDivElement> => {
(process.env.NODE_ENV === 'development') && DEBUGGING_MPP && debug(processInternalEmbed, `MarkdownPostProcessor.ts > processInternalEmbed`, internalEmbedEl);
const attr: imgElementAttributes = {
fname: "",
fheight: "",
@@ -577,6 +592,7 @@ const processAltText = (
alt:string,
attr: imgElementAttributes
) => {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(processAltText, `MarkdownPostProcessor.ts > processAltText`);
if (alt && !alt.startsWith(fname)) {
//2:width, 3:height, 4:style 12 3 4
const parts = alt.match(/[^\|\d]*\|?((\d*%?)x?(\d*%?))?\|?(.*)/);
@@ -596,6 +612,7 @@ const processAltText = (
}
const isTextOnlyEmbed = (internalEmbedEl: Element):boolean => {
(process.env.NODE_ENV === 'development') && DEBUGGING && debug(isTextOnlyEmbed, `MarkdownPostProcessor.ts > isTextOnlyEmbed`);
const src = internalEmbedEl.getAttribute("src");
if(!src) return true; //technically this does not mean this is a text only embed, but still should abort further processing
const fnameParts = getEmbeddedFilenameParts(src);
@@ -606,7 +623,11 @@ const isTextOnlyEmbed = (internalEmbedEl: Element):boolean => {
const tmpObsidianWYSIWYG = async (
el: HTMLElement,
ctx: MarkdownPostProcessorContext,
isPrinting: boolean,
isMarkdownReadingMode: boolean,
isHoverPopover: boolean,
) => {
(process.env.NODE_ENV === 'development') && DEBUGGING_MPP && debug(tmpObsidianWYSIWYG, `MarkdownPostProcessor.ts > tmpObsidianWYSIWYG`);
const file = app.vault.getAbstractFileByPath(ctx.sourcePath);
if(!(file instanceof TFile)) return;
if(!plugin.isExcalidrawFile(file)) return;
@@ -624,11 +645,11 @@ const tmpObsidianWYSIWYG = async (
//@ts-ignore
const containerEl = ctx.containerEl;
if(!plugin.settings.renderImageInMarkdownReadingMode && containerEl.parentElement?.parentElement?.hasClass("markdown-reading-view")) {
if(!plugin.settings.renderImageInMarkdownReadingMode && isMarkdownReadingMode) { // containerEl.parentElement?.parentElement?.hasClass("markdown-reading-view")) {
return;
}
if(!plugin.settings.renderImageInMarkdownToPDF && containerEl.parentElement?.hasClass("print")) {
if(!plugin.settings.renderImageInMarkdownToPDF && isPrinting) { //containerEl.parentElement?.hasClass("print")) {
return;
}
@@ -656,14 +677,14 @@ const tmpObsidianWYSIWYG = async (
if(!plugin.settings.renderImageInHoverPreviewForMDNotes) {
const isHoverPopover = internalEmbedDiv.parentElement?.hasClass("hover-popover");
//const isHoverPopover = internalEmbedDiv.parentElement?.hasClass("hover-popover");
const shouldOpenMD = Boolean(ctx.frontmatter?.["excalidraw-open-md"]);
if(isHoverPopover && shouldOpenMD) {
return;
}
}
const isPrinting = Boolean(internalEmbedDiv.hasClass("print"));
//const isPrinting = Boolean(internalEmbedDiv.hasClass("print"));
const attr: imgElementAttributes = {
fname: ctx.sourcePath,
@@ -675,7 +696,7 @@ const tmpObsidianWYSIWYG = async (
attr.file = file;
const markdownEmbed = internalEmbedDiv.hasClass("markdown-embed");
const markdownReadingView = internalEmbedDiv.hasClass("markdown-reading-view") || isPrinting;
const markdownReadingView = isPrinting || isMarkdownReadingMode; //internalEmbedDiv.hasClass("markdown-reading-view")
if (!internalEmbedDiv.hasClass("internal-embed") && (markdownEmbed || markdownReadingView)) {
if(isPrinting) {
internalEmbedDiv = containerEl;
@@ -762,6 +783,7 @@ const tmpObsidianWYSIWYG = async (
});
};
const docIDs = new Set<string>();
/**
*
* @param el
@@ -771,12 +793,35 @@ export const markdownPostProcessor = async (
el: HTMLElement,
ctx: MarkdownPostProcessorContext,
) => {
const isPrinting = Boolean(document.body.querySelectorAll("body > .print").length>0);
if(isPrinting && el.hasClass("mod-frontmatter")) {
return;
}
//@ts-ignore
const containerEl = ctx.containerEl;
(process.env.NODE_ENV === 'development') && DEBUGGING_MPP && debug(markdownPostProcessor, `MarkdownPostProcessor.ts > markdownPostProcessor`, ctx, el);
//check to see if we are rendering in editing mode or live preview
//if yes, then there should be no .internal-embed containers
//if yes, then there should be no .internal-embed containers
const isMarkdownReadingMode = Boolean(containerEl && getParentOfClass(containerEl, "markdown-reading-view"));
const isHoverPopover = Boolean(containerEl && getParentOfClass(containerEl, "hover-popover"));
const isPreview = isPrinting || isMarkdownReadingMode ||
(isHoverPopover && Boolean(ctx?.frontmatter?.["excalidraw-open-md"]) && !plugin.settings.renderImageInHoverPreviewForMDNotes);
const embeddedItems = el.querySelectorAll(".internal-embed");
if (embeddedItems.length === 0) {
tmpObsidianWYSIWYG(el, ctx);
if (!isPreview && embeddedItems.length === 0) {
if(el.hasClass("mod-frontmatter")) {
docIDs.add(ctx.docId);
} else {
if(docIDs.has(ctx.docId)) {
if(!el.hasChildNodes()) {
docIDs.delete(ctx.docId);
}
return;
}
}
await tmpObsidianWYSIWYG(el, ctx, isPrinting, isMarkdownReadingMode, isHoverPopover);
return;
}
@@ -785,8 +830,7 @@ export const markdownPostProcessor = async (
//transcluded text element or some other transcluded content inside the Excalidraw file
//in reading mode these elements should be hidden
const excalidrawFile = Boolean(ctx.frontmatter?.hasOwnProperty("excalidraw-plugin"));
const isPrinting = Boolean(document.body.querySelectorAll("body > .print"));
if (excalidrawFile && !isPrinting) {
if (!isPreview && excalidrawFile) {
el.style.display = "none";
return;
}

View File

@@ -1,4 +1,4 @@
import { ButtonComponent, TFile } from "obsidian";
import { ButtonComponent, TFile, ToggleComponent } from "obsidian";
import ExcalidrawView from "../ExcalidrawView";
import ExcalidrawPlugin from "../main";
import { getPDFDoc } from "src/utils/FileUtils";
@@ -7,9 +7,11 @@ import { FileSuggestionModal } from "./FolderSuggester";
import { getEA } from "src";
import { ExcalidrawAutomate } from "src/ExcalidrawAutomate";
import { ExcalidrawImperativeAPI } from "@zsviczian/excalidraw/types/excalidraw/types";
import { t } from "src/lang/helpers";
export class InsertPDFModal extends Modal {
private borderBox: boolean = true;
private frame: boolean = false;
private gapSize:number = 20;
private groupPages: boolean = false;
private direction: "down" | "right" = "right";
@@ -48,6 +50,7 @@ export class InsertPDFModal extends Modal {
if(this.dirty) {
this.plugin.settings.pdfImportScale = this.importScale;
this.plugin.settings.pdfBorderBox = this.borderBox;
this.plugin.settings.pdfFrame = this.frame;
this.plugin.settings.pdfGapSize = this.gapSize;
this.plugin.settings.pdfGroupPages = this.groupPages;
this.plugin.settings.pdfNumColumns = this.numColumns;
@@ -120,6 +123,7 @@ export class InsertPDFModal extends Modal {
async createForm() {
await this.plugin.loadSettings();
this.borderBox = this.plugin.settings.pdfBorderBox;
this.frame = this.plugin.settings.pdfFrame;
this.gapSize = this.plugin.settings.pdfGapSize;
this.groupPages = this.plugin.settings.pdfGroupPages;
this.numColumns = this.plugin.settings.pdfNumColumns;
@@ -138,13 +142,13 @@ export class InsertPDFModal extends Modal {
const importButtonMessages = () => {
if(!this.pdfDoc) {
importMessage.innerText = "Please select a PDF file";
importMessage.innerText = t("IPM_SELECT_PDF");
importButton.buttonEl.style.display="none";
return;
}
if(this.pagesToImport.length === 0) {
importButton.buttonEl.style.display="none";
importMessage.innerText = "Please select pages to import";
importMessage.innerText = t("IPM_SELECT_PAGES_TO_IMPORT");
return
}
if(Math.max(...this.pagesToImport) <= this.pdfDoc.numPages) {
@@ -161,7 +165,7 @@ export class InsertPDFModal extends Modal {
const numPagesMessages = () => {
if(numPages === 0) {
numPagesMessage.innerText = "Please select a PDF file";
numPagesMessage.innerText = t("IPM_SELECT_PDF");
return;
}
numPagesMessage.innerHTML = `There are <b>${numPages}</b> pages in the selected document.`;
@@ -211,7 +215,7 @@ export class InsertPDFModal extends Modal {
numPagesMessage = ce.createEl("p", {text: ""});
numPagesMessages();
new Setting(ce)
.setName("Pages to import")
.setName(t("IPM_PAGES_TO_IMPORT_NAME"))
.setDesc("e.g.: 1,3-5,7,9-10")
.addText(text => {
pageRangesTextComponent = text;
@@ -222,18 +226,52 @@ export class InsertPDFModal extends Modal {
})
importPagesMessage = ce.createEl("p", {text: ""});
new Setting(ce)
.setName("Add border box")
.addToggle(toggle => toggle
.setValue(this.borderBox)
.onChange((value) => {
this.borderBox = value;
this.dirty = true;
}))
let bbToggle: ToggleComponent;
let fToggle: ToggleComponent;
let laiToggle: ToggleComponent;
this.frame = this.borderBox ? false : this.frame;
new Setting(ce)
.setName("Group pages")
.setDesc("This will group all pages into a single group. This is recommended if you are locking the pages after import, because the group will be easier to unlock later rather than unlocking one by one.")
.setName(t("IPM_ADD_BORDER_BOX_NAME"))
.addToggle(toggle => {
bbToggle = toggle;
toggle
.setValue(this.borderBox)
.onChange((value) => {
this.borderBox = value;
if(value) {
this.frame = false;
fToggle.setValue(false);
}
this.dirty = true;
})
})
new Setting(ce)
.setName(t("IPM_ADD_FRAME_NAME"))
.setDesc(t("IPM_ADD_FRAME_DESC"))
.addToggle(toggle => {
fToggle = toggle;
toggle
.setValue(this.frame)
.onChange((value) => {
this.frame = value;
if(value) {
this.borderBox = false;
bbToggle.setValue(false);
if(!this.lockAfterImport) {
this.lockAfterImport = true;
laiToggle.setValue(true);
}
}
this.dirty = true;
})
})
new Setting(ce)
.setName(t("IPM_GROUP_PAGES_NAME"))
.setDesc(t("IPM_GROUP_PAGES_DESC"))
.addToggle(toggle => toggle
.setValue(this.groupPages)
.onChange((value) => {
@@ -244,12 +282,15 @@ export class InsertPDFModal extends Modal {
new Setting(ce)
.setName("Lock pages on canvas after import")
.addToggle(toggle => toggle
.setValue(this.lockAfterImport)
.onChange((value) => {
this.lockAfterImport = value
this.dirty = true;
}))
.addToggle(toggle => {
laiToggle = toggle;
toggle
.setValue(this.lockAfterImport)
.onChange((value) => {
this.lockAfterImport = value
this.dirty = true;
})
})
let numColumnsSetting: Setting;
let numRowsSetting: Setting;
@@ -391,6 +432,12 @@ export class InsertPDFModal extends Modal {
if(this.lockAfterImport) imgEl.locked = true;
ea.addToGroup([boxID,imageID]);
if(this.frame) {
const frameID = ea.addFrame(topX, topY,imgWidth,imgHeight,`${page}`);
ea.addElementsToFrame(frameID, [boxID,imageID]);
ea.getElement(frameID).link = this.pdfFile.path + `#page=${page}`;
}
switch(this.direction) {
case "right":
@@ -404,7 +451,9 @@ export class InsertPDFModal extends Modal {
}
}
if(this.groupPages) {
const ids = ea.getElements().map(el => el.id);
const ids = ea.getElements()
.filter(el=>!this.frame || (el.type === "frame"))
.map(el => el.id);
ea.addToGroup(ids);
}
await ea.addElementsToView(true,true,false);

View File

@@ -233,6 +233,18 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
desc: null,
after: "",
},
{
field: "addElementsToFrame",
code: "addElementsToFrame(frameId: string, elementIDs: string[]):void;",
desc: null,
after: "",
},
{
field: "addFrame",
code: "addFrame(topX: number, topY: number, width: number, height: number, name?: string): string;",
desc: null,
after: "",
},
{
field: "addRect",
code: "addRect(topX: number, topY: number, width: number, height: number, id?:string): string;",
@@ -311,6 +323,12 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
desc: "This is an async function, you need to avait the results. Adds a LaTex element to the drawing. The tex string is the LaTex code. The function returns the id of the created element.",
after: "",
},
{
field: "tex2dataURL",
code: "async tex2dataURL(tex: string, scale: number = 4): Promise<{mimeType: MimeType;fileId: FileId;dataURL: DataURL;created: number;size: { height: number; width: number };}> ",
desc: "returns the base64 dataURL of the LaTeX equation rendered as an SVG. tex is the LaTeX equation string",
after: "",
},
{
field: "connectObjects",
code: "connectObjects(objectA: string, connectionA: ConnectionPoint, objectB: string, connectionB: ConnectionPoint, formatting?: {numberOfPoints?: number; startArrowHead?: string; endArrowHead?: string; padding?: number;},): string;",
@@ -387,8 +405,8 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
},
{
field: "getViewSelectedElements",
code: "getViewSelectedElements(): ExcalidrawElement[];",
desc: null,
code: "getViewSelectedElements(includeFrameChildren: boolean = true): ExcalidrawElement[];",
desc: "If a frame is selected this function will return the frame and all its elements unless includeFrameChildren is set to false",
after: "",
},
{
@@ -489,8 +507,9 @@ export const EXCALIDRAW_AUTOMATE_INFO: SuggesterInfo[] = [
},
{
field: "getElementsInTheSameGroupWithElement",
code: "getElementsInTheSameGroupWithElement(element: ExcalidrawElement, elements: ExcalidrawElement[]): ExcalidrawElement[];",
desc: "Gets all the elements from elements[] that share one or more groupIds with element.",
code: "getElementsInTheSameGroupWithElement(element: ExcalidrawElement, elements: ExcalidrawElement[], includeFrameElements: boolean = false): ExcalidrawElement[];",
desc: "Gets all the elements from elements[] that share one or more groupIds with element.<br>" +
"If includeFrameElements is true, then if the frame is part of the group all the elements that are in the frame will also be included in the result set",
after: ""
},
{

View File

@@ -322,24 +322,29 @@ FILENAME_HEAD: "Filename",
DEFAULT_PEN_MODE_NAME: "Pen mode",
DEFAULT_PEN_MODE_DESC:
"Should pen mode be automatically enabled when opening Excalidraw?",
DISABLE_DOUBLE_TAP_ERASER_NAME: "Enable double-tap eraser in pen mode",
SHOW_PEN_MODE_FREEDRAW_CROSSHAIR_NAME: "Show (+) crosshair in pen mode",
SHOW_PEN_MODE_FREEDRAW_CROSSHAIR_DESC:
"Show crosshair in pen mode when using the freedraw tool. <b><u>Toggle ON:</u></b> SHOW <b><u>Toggle OFF:</u></b> HIDE<br>"+
"The effect depends on the device. Crosshair is typically visible on drawing tablets, MS Surface, but not on iOS.",
SHOW_DRAWING_OR_MD_IN_HOVER_PREVIEW_NAME: "Render image in hover preview for MD files",
SHOW_DRAWING_OR_MD_IN_HOVER_PREVIEW_NAME: "Render Excalidraw file as an image in hover preview...",
SHOW_DRAWING_OR_MD_IN_HOVER_PREVIEW_DESC:
"This setting effects files that have the <b>excalidraw-open-md: true</b> frontmatter key.",
SHOW_DRAWING_OR_MD_IN_READING_MODE_NAME: "Render image when in markdown reading mode",
"...even if the file has the <b>excalidraw-open-md: true</b> frontmatter key.<br>" +
"When this setting is off and the file is set to open in md by default, the hover preview will show the " +
"markdown side of the document.",
SHOW_DRAWING_OR_MD_IN_READING_MODE_NAME: "Render as image when in markdown reading mode of an Excalidraw file",
SHOW_DRAWING_OR_MD_IN_READING_MODE_DESC:
"Must close the active excalidraw/markdown file and reopen it for this change to take effect.<br>When you are in markdown reading mode (aka. reading the back side of the drawing), should the Excalidraw drawing be rendered as an image? " +
"This setting will not affect the display of the drawing when you are in Excalidraw mode, when you embed the drawing into a markdown document or when rendering hover preview.<br><ul>" +
"<li>See other related setting for <b>PDF Export</b> under 'Embedding and Exporting' further below.</li>" +
"<li>Be sure to check out the <b>Fade Out setting</b> in the 'Miscellaneous fetures' section.</li></ul>",
SHOW_DRAWING_OR_MD_IN_EXPORTPDF_NAME: "Render image when EXPORT TO PDF in markdown mode",
"When you are in markdown reading mode (aka. reading the back side of the drawing) should the Excalidraw drawing be rendered as an image? " +
"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 <b>PDF Export</b> 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_DESC:
"Must close the active excalidraw/markdown file and reopen for this change to take effect.<br>When you are printing the markdown side of the note to PDF (aka. the back side of the drawing), should the Excalidraw drawing be rendered as an image?<br><ul>" +
"<li>See other related setting for <b>Markdown Reading Mode</b> under 'Appearnace and Behavior' further above.</li>" +
"<li>Be sure to check out the <b>Fade Out setting</b> in the 'Miscellaneous fetures' section.</li></ul>",
"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 <b>Markdown Reading Mode</b> under 'Appearnace and Behavior' further above.<br>" +
"⚠️ Note, you must close the active excalidraw/markdown file and reopen for this change to take effect. ⚠️",
THEME_HEAD: "Theme and styling",
ZOOM_HEAD: "Zoom",
DEFAULT_PINCHZOOM_NAME: "Allow pinch zoom in pen mode",
@@ -835,4 +840,16 @@ FILENAME_HEAD: "Filename",
FRAME_SETTIGNS_NAME: "Display Frame Name",
FRAME_SETTINGS_OUTLINE: "Display Frame Outline",
FRAME_SETTINGS_CLIP: "Enable Frame Clipping",
//InsertPDFModal.ts
IPM_PAGES_TO_IMPORT_NAME: "Pages to import",
IPM_SELECT_PAGES_TO_IMPORT: "Please select pages to import",
IPM_ADD_BORDER_BOX_NAME: "Add border box",
IPM_ADD_FRAME_NAME: "Add page to frame",
IPM_ADD_FRAME_DESC: "For easier handling I recommend to lock the page inside the frame. " +
"If, however, you do lock the page inside the frame then the only way to unlock it is to right-click the frame, select remove elements from frame, then unlock the page.",
IPM_GROUP_PAGES_NAME: "Group pages",
IPM_GROUP_PAGES_DESC: "This will group all pages into a single group. This is recommended if you are locking the pages after import, because the group will be easier to unlock later rather than unlocking one by one.",
IPM_SELECT_PDF: "Please select a PDF file",
};

View File

@@ -78,6 +78,7 @@ export interface ExcalidrawSettings {
matchThemeTrigger: boolean;
defaultMode: string;
defaultPenMode: "never" | "mobile" | "always";
penModeDoubleTapEraser: boolean;
penModeCrosshairVisible: boolean;
renderImageInMarkdownReadingMode: boolean,
renderImageInHoverPreviewForMDNotes: boolean,
@@ -168,6 +169,7 @@ export interface ExcalidrawSettings {
numberOfCustomPens: number;
pdfScale: number;
pdfBorderBox: boolean;
pdfFrame: boolean;
pdfGapSize: number;
pdfGroupPages: boolean;
pdfLockAfterImport: boolean;
@@ -243,6 +245,7 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
matchThemeTrigger: false,
defaultMode: "normal",
defaultPenMode: "never",
penModeDoubleTapEraser: true,
penModeCrosshairVisible: true,
renderImageInMarkdownReadingMode: false,
renderImageInHoverPreviewForMDNotes: false,
@@ -339,6 +342,7 @@ export const DEFAULT_SETTINGS: ExcalidrawSettings = {
numberOfCustomPens: 0,
pdfScale: 4,
pdfBorderBox: true,
pdfFrame: false,
pdfGapSize: 20,
pdfGroupPages: false,
pdfLockAfterImport: true,
@@ -1024,6 +1028,17 @@ export class ExcalidrawSettingTab extends PluginSettingTab {
}),
);
new Setting(detailsEl)
.setName(t("DISABLE_DOUBLE_TAP_ERASER_NAME"))
.addToggle((toggle) =>
toggle
.setValue(this.plugin.settings.penModeDoubleTapEraser)
.onChange(async (value) => {
this.plugin.settings.penModeDoubleTapEraser = value;
this.applySettingsUpdate();
}),
);
new Setting(detailsEl)
.setName(t("SHOW_PEN_MODE_FREEDRAW_CROSSHAIR_NAME"))
.setDesc(fragWithHTML(t("SHOW_PEN_MODE_FREEDRAW_CROSSHAIR_DESC")))

View File

@@ -10,7 +10,8 @@ export let DEBUGGING = false;
export const log = console.log.bind(window.console);
export const debug = (fn: Function, fnName: string, ...messages: unknown[]) => {
console.log(fnName,fn,...messages);
//console.log(fnName,fn,...messages);
console.log(fnName, ...messages);
};
export class CustomMutationObserver {

View File

@@ -253,16 +253,16 @@ class ImageCache {
});
}
private async getObjectStore(mode: IDBTransactionMode, storeName: string): Promise<IDBObjectStore> {
private getObjectStore(mode: IDBTransactionMode, storeName: string): IDBObjectStore {
const transaction = this.db!.transaction(storeName, mode);
return transaction.objectStore(storeName);
}
private async getCacheData(key: string): Promise<FileCacheData | null> {
const store = await this.getObjectStore("readonly", this.cacheStoreName);
const store = this.getObjectStore("readonly", this.cacheStoreName);
const request = store.get(key);
return new Promise<FileCacheData | null>((resolve, reject) => {
return await new Promise<FileCacheData | null>((resolve, reject) => {
request.onsuccess = () => {
const result = request.result as FileCacheData;
resolve(result || null);
@@ -275,7 +275,7 @@ class ImageCache {
}
private async getBackupData(key: BackupKey): Promise<BackupData | null> {
const store = await this.getObjectStore("readonly", this.backupStoreName);
const store = this.getObjectStore("readonly", this.backupStoreName);
const request = store.get(key);
return new Promise<BackupData | null>((resolve, reject) => {
@@ -308,7 +308,9 @@ class ImageCache {
? await this.getCacheData(key)
: await Promise.race([
this.getCacheData(key),
new Promise<undefined>((_,reject) => setTimeout(() => reject(undefined), 100))
new Promise<undefined>((_,reject) => setTimeout(() => {
reject(undefined);
}, 100))
]);
this.fullyInitialized = true;
if(!cachedData) return undefined;

View File

@@ -922,3 +922,20 @@ export async function getFontMetrics(fontUrl: string, name: string): Promise<Fon
return null;
}
}
// Thanks https://stackoverflow.com/a/54555834
export function cropCanvas(
srcCanvas: HTMLCanvasElement,
crop: { left: number, top: number, width: number, height: number },
output: { width: number, height: number } = { width: crop.width, height: crop.height })
{
const dstCanvas = createEl('canvas');
dstCanvas.width = output.width;
dstCanvas.height = output.height;
dstCanvas.getContext('2d')!.drawImage(
srcCanvas,
crop.left, crop.top, crop.width, crop.height,
0, 0, output.width, output.height
);
return dstCanvas;
}