File size: 4,082 Bytes
2409829
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
// Rasterize the string of an SVG document at a given width and height and return the canvas it was drawn onto during the rasterization process
export async function rasterizeSVGCanvas(svg: string, width: number, height: number, backgroundColor?: string): Promise<HTMLCanvasElement> {
	// A canvas to render our SVG to in order to get a raster image
	const canvas = document.createElement("canvas");
	canvas.width = width;
	canvas.height = height;
	const context = canvas.getContext("2d", { willReadFrequently: true });
	if (!context) throw new Error("Can't create 2D context from canvas during SVG rasterization");

	// Apply a background fill color if one is given
	if (backgroundColor) {
		context.fillStyle = backgroundColor;
		context.fillRect(0, 0, width, height);
	}

	// Create a blob URL for our SVG
	const svgBlob = new Blob([svg], { type: "image/svg+xml;charset=utf-8" });
	const url = URL.createObjectURL(svgBlob);

	// Load the Image from the URL and wait until it's done
	const image = new Image();
	image.src = url;
	await new Promise<void>((resolve) => {
		image.onload = () => resolve();
	});

	// Draw our SVG to the canvas
	context?.drawImage(image, 0, 0, width, height);

	// Clean up the SVG blob URL (once the URL is revoked, the SVG blob data itself is garbage collected after `svgBlob` goes out of scope)
	URL.revokeObjectURL(url);

	return canvas;
}

// Rasterize the string of an SVG document at a given width and height and turn it into the blob data of an image file matching the given MIME type
export async function rasterizeSVG(svg: string, width: number, height: number, mime: string, backgroundColor?: string): Promise<Blob> {
	if (!width || !height) throw new Error("Width and height must be nonzero when given to rasterizeSVG()");

	const canvas = await rasterizeSVGCanvas(svg, width, height, backgroundColor);

	// Convert the canvas to an image of the correct MIME type
	const blob = await new Promise<Blob | undefined>((resolve) => {
		canvas.toBlob((blob) => {
			resolve(blob || undefined);
		}, mime);
	});

	if (!blob) throw new Error("Converting canvas to blob data failed in rasterizeSVG()");

	return blob;
}

/// Convert an image source (e.g. PNG document) into pixel data, a width, and a height
export async function extractPixelData(imageData: ImageBitmapSource): Promise<ImageData> {
	const canvasContext = await imageToCanvasContext(imageData);
	const width = canvasContext.canvas.width;
	const height = canvasContext.canvas.height;

	return canvasContext.getImageData(0, 0, width, height);
}

export async function imageToCanvasContext(imageData: ImageBitmapSource): Promise<CanvasRenderingContext2D> {
	// Special handling to rasterize an SVG file
	let svgImageData;
	if (imageData instanceof File && imageData.type === "image/svg+xml") {
		const svgSource = await imageData.text();
		const svgElement = new DOMParser().parseFromString(svgSource, "image/svg+xml").querySelector("svg");
		if (!svgElement) throw new Error("Error reading SVG file");

		let bounds = svgElement.viewBox.baseVal;

		// If the bounds are zero (which will happen if the `viewBox` is not provided), set bounds to the artwork's bounding box
		if (bounds.width === 0 || bounds.height === 0) {
			// It's necessary to measure while the element is in the DOM, otherwise the dimensions are zero
			const toRemove = document.body.insertAdjacentElement("beforeend", svgElement);
			bounds = svgElement.getBBox();
			toRemove?.remove();
		}

		svgImageData = await rasterizeSVGCanvas(svgSource, bounds.width, bounds.height);
	}

	// Decode the image file binary data
	const image = await createImageBitmap(svgImageData || imageData);

	let { width, height } = image;
	width = Math.floor(width);
	height = Math.floor(height);

	// Render image to canvas
	const canvas = document.createElement("canvas");
	canvas.width = width;
	canvas.height = height;

	const context = canvas.getContext("2d");
	if (!context) throw new Error("Could not create canvas context");
	context.drawImage(image, 0, 0, image.width, image.height, 0, 0, width, height);

	return context;
}