/////////////////////////////////////////////////////////////////////////////// // Copyright (C) 2002-2026, Open Design Alliance (the "Alliance"). // All rights reserved. // // This software and its documentation and related materials are owned by // the Alliance. The software may only be incorporated into application // programs owned by members of the Alliance, subject to a signed // Membership Agreement and Supplemental Software License Agreement with the // Alliance. The structure and organization of this software are the valuable // trade secrets of the Alliance and its suppliers. The software is also // protected by copyright law and international treaty provisions. Application // programs incorporating this software must include the following statement // with their copyright notices: // // This application incorporates Open Design Alliance software pursuant to a // license agreement with Open Design Alliance. // Open Design Alliance Copyright (C) 2002-2026 by Open Design Alliance. // All rights reserved. // // By use of this software, its documentation or related materials, you // acknowledge and accept the above terms. /////////////////////////////////////////////////////////////////////////////// import Konva from "konva"; import { IMarkupImage, IMarkupImageParams } from "../../primitives/IMarkupImage"; import { IWorldTransform } from "../../IWorldTransform"; import { WorldTransform } from "../../WorldTransform"; export class KonvaImage implements IMarkupImage { private _ref: Konva.Image; private _canvasImage: HTMLImageElement; private _ratio = 1; private _worldTransformer: IWorldTransform; private readonly EPSILON = 10e-6; private readonly BASE64_HEADER_START = "data:image/"; private readonly BASE64_NOT_FOUND = "data:image/jpeg;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAADsAAAA7AF5KHG9AAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAAmhJREFUWIXtlr9rVEEQxz+H8RQUJIdeIopYm0vkCg0GBBtbG1NF7Kxt7dR/IGIw/uhTaBNLERURg2kCEUyCYCPi70b0InjGS57FzOZN3r19d+9HJIVfWO52dma/s7Mz8xa2KAaBCWAR+AkECWOmSOIdwC1gtQOpHc+NfQ8wClQ8+1d0vcdH/lQ3bSIRGAZ2pTjAqNovANXIWlXlAXA2zvi2Ln4AjqYgtagYEutENSLvjRoOImFv5iB32Ae8UrLXwFBk3h9ndF0VJnKSO9gTu3yKu5Z1LKnS8YIcABgw5Ks692JZFXcXRJ46Aq6kikCnHNi/mQ50WwVtfaIoBzL3gRk2drSscJ2wrc4VvUoe2wn/41/iBfoVLRnBGnDSY3AAKacy8AmYR+o7K1zCl6wgrgpOAc/MuhvfgMuk+1JGHQgSBcAloKXy78AjYBppJk5/noTulseBMZ23iD/piHFkEdgTQzKk+5wHjmHC3cmBg0BD5xcSTrFXyQPgIWFtDwMvab+2N8DpbhyY1v/3E8gdDgNfVX9SCVZ0/gW4B0wB71S2BpxLcuCM/jaQSHSDEeAX4VMuAG4gTzyHbcAVXXO6GxxwIX+vvxe7JHcYQ07nHqklj96UIW/YhSWzMKcep8VVtf8B1Dw6h4DfhB+sdbgn2R+gnoEc5NR3dZ+3QJ9H74HqXLPCGlJyTfI9y3YCs0owq3OLOpKkLeBI1HhSDT/mdKIPiUCARMTlQx34TMLjtww8IczmO8AJ/N/2JNSQXAiQ671JePePge0+wzJSQq4FFzlaenIvucUAkiQLhC/mLGNZ9xgn5s63BP4CCk0QDtm4BhoAAAAASUVORK5CYII="; constructor(params: IMarkupImageParams, ref = null, worldTransformer = new WorldTransform()) { this._worldTransformer = worldTransformer; if (ref) { if (!ref.src || !ref.src.startsWith(this.BASE64_HEADER_START)) ref.src = this.BASE64_NOT_FOUND; if (ref.height() <= this.EPSILON) ref.height(32); if (ref.width() <= this.EPSILON) ref.width(32); this._ref = ref; this._canvasImage = ref.image(); this._ratio = this._ref.height() <= this.EPSILON || this._ref.width() <= this.EPSILON ? 1 : this._ref.height() / this._ref.width(); const wcsStart = this._ref.getAttr("wcsStart"); const wcsEnd = this._ref.getAttr("wcsEnd"); if (!wcsStart) { this._ref.setAttr("wcsStart", this._worldTransformer.screenToWorld({ x: ref.x(), y: ref.y() })); } if (!wcsEnd) { const rightBottomPoint = { x: ref.x() + ref.width(), y: ref.y() + ref.height() }; this._ref.setAttr( "wcsEnd", this._worldTransformer.screenToWorld({ x: rightBottomPoint.x, y: rightBottomPoint.y }) ); } return; } if (!params) params = {}; if (!params.position) params.position = { x: 0, y: 0 }; if (!params.src || !params.src.startsWith(this.BASE64_HEADER_START)) params.src = this.BASE64_NOT_FOUND; if (params.position2) { params.width = params.position2.x - params.position.x; params.height = params.position2.y - params.position.y; } this._canvasImage = new Image(); this._canvasImage.onload = () => { this._ref.image(this._canvasImage); if (this._ref.height() <= this.EPSILON) this._ref.height(this._canvasImage.height); if (this._ref.width() <= this.EPSILON) this._ref.width(this._canvasImage.width); this._ratio = this._ref.height() <= this.EPSILON || this._ref.width() <= this.EPSILON ? 1 : this._ref.height() / this._ref.width(); // need to rescale only if input width and height are 0 - we do not loading Viewpoint with existing params if ( (params.width <= this.EPSILON || params.height <= this.EPSILON) && (params.maxWidth >= this.EPSILON || params.maxWidth >= this.EPSILON) ) { const heightOutOfCanvas = params.maxHeight - this._canvasImage.height; const widthOutOfCanvas = params.maxWidth - this._canvasImage.width; if (heightOutOfCanvas <= this.EPSILON || widthOutOfCanvas <= this.EPSILON) { if (widthOutOfCanvas <= this.EPSILON && widthOutOfCanvas < heightOutOfCanvas / this._ratio) { this._ref.height(params.maxWidth * this._ratio); this._ref.width(params.maxWidth); } else { this._ref.width(params.maxHeight / this._ratio); this._ref.height(params.maxHeight); } } } const wcsEnd = this._worldTransformer.screenToWorld({ x: params.position.x + this._ref.width(), y: params.position.y + this._ref.height(), }); this._ref.setAttr("wcsEnd", wcsEnd); }; this._canvasImage.onerror = () => { this._canvasImage.onerror = function () {}; this._canvasImage.src = this.BASE64_NOT_FOUND; }; this._canvasImage.src = params.src; this._ref = new Konva.Image({ x: params.position.x, y: params.position.y, image: this._canvasImage, width: params.width ?? 0, height: params.height ?? 0, draggable: true, }); this._ref.setAttr("wcsStart", this._worldTransformer.screenToWorld({ x: params.position.x, y: params.position.y })); if (params.position2) { this._ref.setAttr( "wcsEnd", this._worldTransformer.screenToWorld({ x: params.position2.x, y: params.position2.y }) ); } this._ref.on("transform", (e) => { const attrs = e.target.attrs; if (attrs.rotation !== this._ref.rotation()) this._ref.rotation(attrs.rotation); const scaleByX = Math.abs(attrs.scaleX - 1) > 10e-6; const scaleByY = Math.abs(attrs.scaleY - 1) > 10e-6; let newWidth = this._ref.width(); if (scaleByX) newWidth *= attrs.scaleX; let newHeight = this._ref.height(); if (scaleByY) newHeight *= attrs.scaleY; if (e.evt.ctrlKey || e.evt.shiftKey) { if (scaleByX) { this._ref.width(newWidth); this._ref.height(newWidth * this._ratio); } else { this._ref.width(newHeight / this._ratio); this._ref.height(newHeight); } } else { if (scaleByX) { this._ref.width(newWidth); } if (scaleByY) { this._ref.height(newHeight); } } this._ref.scale({ x: 1, y: 1 }); }); this._ref.on("transformend", () => { const absoluteTransform = this._ref.getStage().getAbsoluteTransform(); const position = absoluteTransform.point({ x: this._ref.x(), y: this._ref.y() }); this._ref.setAttr("wcsStart", this._worldTransformer.screenToWorld(position)); this._ref.setAttr( "wcsEnd", this._worldTransformer.screenToWorld({ x: position.x + this._ref.width(), y: position.y + this._ref.height() }) ); }); this._ref.on("dragend", () => { const absoluteTransform = this._ref.getStage().getAbsoluteTransform(); const position = absoluteTransform.point({ x: this._ref.x(), y: this._ref.y() }); this._ref.setAttr("wcsStart", this._worldTransformer.screenToWorld(position)); this._ref.setAttr( "wcsEnd", this._worldTransformer.screenToWorld({ x: position.x + this._ref.width(), y: position.y + this._ref.height() }) ); }); this._ref.id(this._ref._id.toString()); } getSrc(): string { return this._canvasImage.src; } setSrc(src: any): void { this._canvasImage.src = src; } getWidth(): number { return this._ref.width(); } setWidth(w: number): void { this._ref.width(w); this._ref.height(w * this._ratio); const rightLowerPoint = { x: this._ref.x() + w, y: this._ref.y() + this._ref.height() }; const wcsRightLowerPoint = this._worldTransformer.screenToWorld(rightLowerPoint); this._ref.setAttr("wcsEnd", wcsRightLowerPoint); } getHeight(): number { return this._ref.height(); } setHeight(h: number): void { this._ref.height(h); this._ref.width(h / this._ratio); const rightLowerPoint = { x: this._ref.x() + this._ref.width(), y: this._ref.y() + h }; const wcsRightLowerPoint = this._worldTransformer.screenToWorld(rightLowerPoint); this._ref.setAttr("wcsEnd", wcsRightLowerPoint); } ref() { return this._ref; } id(): string { return this._ref.id(); } enableMouseEditing(value: boolean): void { this._ref.draggable(value); } type(): string { return "Image"; } getRotation(): number { return this._ref.rotation(); } setRotation(degrees: number): void { this._ref.rotation(degrees); } getZIndex(): number { return this._ref.zIndex(); } setZIndex(zIndex: number): void { this._ref.zIndex(zIndex); } delete(): void { this._ref.destroy(); this._ref = null; } getPosition(): { x: number; y: number } { return this._ref.getPosition(); } setPosition(x: number, y: number): void { this._ref.setPosition({ x, y }); this._ref.setAttr("wcsStart", this._worldTransformer.screenToWorld({ x, y })); const rightLowerPoint = { x: x + this._ref.width(), y: y + this._ref.y() }; const wcsRightLowerPoint = this._worldTransformer.screenToWorld(rightLowerPoint); this._ref.setAttr("wcsEnd", wcsRightLowerPoint); } updateScreenCoordinates(): void { const screenPositionStart = this._worldTransformer.worldToScreen(this._ref.getAttr("wcsStart")); const screenPositionEnd = this._worldTransformer.worldToScreen(this._ref.getAttr("wcsEnd")); let invert = this._ref.getStage().getAbsoluteTransform().copy(); invert = invert.invert(); const positionStart = invert.point(screenPositionStart); const positionEnd = invert.point(screenPositionEnd); this._ref.position({ x: positionStart.x, y: positionStart.y }); this._ref.width(Math.abs(positionEnd.x - positionStart.x)); this._ref.height(Math.abs(positionEnd.y - positionStart.y)); } }