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
JavaScript
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);