UNPKG

js-draw

Version:

Draw pictures using a pen, touchscreen, or mouse! JS-draw is a drawing library for JavaScript and TypeScript.

157 lines (156 loc) 5.44 kB
import { Rect2, Mat33 } from '@js-draw/math'; import { assertIsNumber, assertIsNumberArray } from '../util/assertions.mjs'; import AbstractComponent from './AbstractComponent.mjs'; import waitForImageLoaded from '../util/waitForImageLoaded.mjs'; /** * Represents a raster image. * * **Example: Adding images**: * [[include:doc-pages/inline-examples/adding-an-image-and-data-urls.md]] */ export default class ImageComponent extends AbstractComponent { constructor(image) { super('image-component'); this.image = { ...image, label: image.label ?? image.image.getAttribute('alt') ?? image.image.getAttribute('aria-label') ?? undefined, }; const isHTMLImageElem = (elem) => { return elem.getAttribute('src') !== undefined; }; if (isHTMLImageElem(image.image) && !image.image.complete) { image.image.onload = () => this.recomputeBBox(); } this.recomputeBBox(); } getImageRect() { return new Rect2(0, 0, this.image.image.width, this.image.image.height); } recomputeBBox() { this.contentBBox = this.getImageRect(); this.contentBBox = this.contentBBox.transformedBoundingBox(this.image.transform); } /** * Load from an image. Waits for the image to load if incomplete. * * The image, `elem`, must not [taint](https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_enabled_image#security_and_tainted_canvases) * an HTMLCanvasElement when rendered. */ static async fromImage(elem, transform) { await waitForImageLoaded(elem); let width, height; if (typeof elem.width === 'number' && typeof elem.height === 'number' && elem.width !== 0 && elem.height !== 0) { width = elem.width; height = elem.height; } else { width = elem.clientWidth; height = elem.clientHeight; } let image; let url = elem.src ?? ''; if (!url.startsWith('data:image/')) { // Convert to a data URL: const canvas = document.createElement('canvas'); canvas.width = width; canvas.height = height; const ctx = canvas.getContext('2d'); ctx.drawImage(elem, 0, 0, canvas.width, canvas.height); url = canvas.toDataURL(); image = canvas; } else { image = new Image(); image.src = url; image.width = width; image.height = height; } image.setAttribute('alt', elem.getAttribute('alt') ?? ''); image.setAttribute('aria-label', elem.getAttribute('aria-label') ?? ''); return new ImageComponent({ image, base64Url: url, transform: transform, }); } render(canvas, _visibleRect) { canvas.startObject(this.contentBBox); canvas.drawImage(this.image); canvas.endObject(this.getLoadSaveData()); } // A *very* rough estimate of how long it takes to render this component getProportionalRenderingTime() { // Estimate: Equivalent to a stroke with 10 segments. return 10; } intersects(lineSegment) { const rect = this.getImageRect(); const edges = rect.getEdges().map((edge) => edge.transformedBy(this.image.transform)); for (const edge of edges) { if (edge.intersects(lineSegment)) { return true; } } return false; } applyTransformation(affineTransfm) { this.image.transform = affineTransfm.rightMul(this.image.transform); this.recomputeBBox(); } description(localizationTable) { return this.image.label ? localizationTable.imageNode(this.image.label) : localizationTable.unlabeledImageNode; } getAltText() { return this.image.label; } // The base64 image URL of this image. getURL() { return this.image.base64Url; } getTransformation() { return this.image.transform; } createClone() { return new ImageComponent({ ...this.image, }); } serializeToJSON() { return { src: this.image.base64Url, label: this.image.label, // Store the width and height for bounding box computations while the image is loading. width: this.image.image.width, height: this.image.image.height, transform: this.image.transform.toArray(), }; } static deserializeFromJSON(data) { if (!(typeof data.src === 'string')) { throw new Error(`${data} has invalid format! Expected src property.`); } assertIsNumberArray(data.transform); assertIsNumber(data.width); assertIsNumber(data.height); const image = new Image(); image.src = data.src; image.width = data.width; image.height = data.height; const transform = new Mat33(...data.transform); return new ImageComponent({ image: image, base64Url: data.src, label: data.label, transform, }); } } AbstractComponent.registerComponent('image-component', ImageComponent.deserializeFromJSON);