-
Notifications
You must be signed in to change notification settings - Fork 208
/
ImageUtil.ts
357 lines (316 loc) · 15.2 KB
/
ImageUtil.ts
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
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
* @module Rendering
*/
import { Point2d } from "@itwin/core-geometry";
import { ImageBuffer, ImageBufferFormat, ImageSource, ImageSourceFormat } from "@itwin/core-common";
import { ViewRect } from "./ViewRect";
interface Rgba {
r: number;
g: number;
b: number;
a: number;
}
const scratchRgba = { r: 0, g: 0, b: 0, a: 0 };
function rgbaFromAlpha(rgba: Rgba, src: Uint8Array, idx: number): number {
rgba.r = rgba.g = rgba.b = rgba.a = src[idx];
return idx + 1;
}
function rgbaFromRgb(rgba: Rgba, src: Uint8Array, idx: number): number {
rgba.r = src[idx + 0];
rgba.g = src[idx + 1];
rgba.b = src[idx + 2];
rgba.a = 255;
return idx + 3;
}
function rgbaFromRgba(rgba: Rgba, src: Uint8Array, idx: number): number {
rgbaFromRgb(rgba, src, idx);
rgba.a = src[idx + 3];
return idx + 4;
}
/** Resize a canvas to a desired size. The final size will be targetSize plus barSize. The original canvas is left untouched and a new, resized canvas with potential side bars is returned.
* @param canvasIn the source [HTMLCanvasElement](https://developer.mozilla.org/docs/Web/API/HTMLCanvasElement) to resize.
* @param targetSize the desired new size for the canvas image.
* @param barSize total size of side bars to add to the image in width and height; defaults to (0, 0). For example, if you specify (2, 0), a 1 pixel side bar will be added to the left and right sides of the resized image. If an odd dimension is specified, the left or upper side of the image will be one pixel larger than the opposite side. For example, if you specify (1, 0), a 1 pixel side bar will be added to the left side of the image and a 0 pixel side bar will be added to the right side of the image.
* @param barStyle CSS style string to apply to any side bars; defaults to "#C0C0C0", which is silver.
* @returns an [HTMLCanvasElement](https://developer.mozilla.org/docs/Web/API/HTMLCanvasElement) object containing the resized image and any requested side bars.
* @public
* @extensions
*/
export function canvasToResizedCanvasWithBars(canvasIn: HTMLCanvasElement, targetSize: Point2d, barSize = new Point2d(0, 0), barStyle = "#C0C0C0"): HTMLCanvasElement {
const canvasOut = document.createElement("canvas");
canvasOut.width = targetSize.x + barSize.x;
canvasOut.height = targetSize.y + barSize.y;
let adjustImageX = barSize.x / 2;
let adjustImageY = barSize.y / 2;
if (1 === barSize.x % 2) {
adjustImageX += 0.5;
}
if (1 === barSize.y % 2) {
adjustImageY += 0.5;
}
const context = canvasOut.getContext("2d")!;
context.fillStyle = barStyle;
context.fillRect(0, 0, canvasOut.width, canvasOut.height);
context.drawImage(canvasIn, adjustImageX, adjustImageY, targetSize.x, targetSize.y);
return canvasOut;
}
/** Create a canvas element with the same dimensions and contents as an image buffer.
* @param buffer the source [[ImageBuffer]] object from which the [HTMLCanvasElement](https://developer.mozilla.org/docs/Web/API/HTMLCanvasElement) object will be constructed.
* @param preserveAlpha If false, the alpha channel will be set to 255 (fully opaque). This is recommended when converting an already-blended image (e.g., one obtained from [[Viewport.readImageBuffer]]).
* @returns an [HTMLCanvasElement](https://developer.mozilla.org/docs/Web/API/HTMLCanvasElement) object containing the contents of the source image buffer, or undefined if the conversion fails.
* @public
* @extensions
*/
export function imageBufferToCanvas(buffer: ImageBuffer, preserveAlpha: boolean = true): HTMLCanvasElement | undefined {
const canvas = document.createElement("canvas");
if (null === canvas)
return undefined;
canvas.width = buffer.width;
canvas.height = buffer.height;
const context = canvas.getContext("2d");
if (null === context)
return undefined;
const imageData = context.createImageData(buffer.width, buffer.height);
const extractRgba = ImageBufferFormat.Alpha === buffer.format ? rgbaFromAlpha : (ImageBufferFormat.Rgb === buffer.format ? rgbaFromRgb : rgbaFromRgba);
const bufferData = buffer.data;
let i = 0;
let j = 0;
const rgba = scratchRgba;
while (i < bufferData.length) {
i = extractRgba(rgba, bufferData, i);
imageData.data[j + 0] = rgba.r;
imageData.data[j + 1] = rgba.g;
imageData.data[j + 2] = rgba.b;
imageData.data[j + 3] = preserveAlpha ? rgba.a : 0xff;
j += 4;
}
context.putImageData(imageData, 0, 0);
return canvas;
}
/** Create an ImageBuffer in the specified format with the same dimensions and contents as a canvas.
* @param canvas the source [HTMLCanvasElement](https://developer.mozilla.org/docs/Web/API/HTMLCanvasElement) object from which the [[ImageBuffer]] object will be constructed.
* @param format the desired format of the created ImageBuffer; defaults to [[ImageBufferFormat.Rgba]].
* @returns an [[ImageBuffer]] object containing the contents of the source canvas, or undefined if the conversion fails.
* @public
* @extensions
*/
export function canvasToImageBuffer(canvas: HTMLCanvasElement, format = ImageBufferFormat.Rgba): ImageBuffer | undefined {
const context = canvas.getContext("2d");
if (null === context)
return undefined;
const imageData = context.getImageData(0, 0, canvas.width, canvas.height);
let imageBufferData: Uint8Array | undefined;
if (ImageBufferFormat.Rgba === format) {
imageBufferData = new Uint8Array(imageData.data.length);
} else if (ImageBufferFormat.Rgb === format) {
imageBufferData = new Uint8Array((imageData.data.length / 4) * 3);
} else if (ImageBufferFormat.Alpha === format) {
imageBufferData = new Uint8Array(imageData.data.length / 4);
}
if (undefined === imageBufferData)
return undefined;
let i = 0;
let j = 0;
while (i < imageData.data.length) {
if (ImageBufferFormat.Rgba === format) {
imageBufferData[j + 0] = imageData.data[i + 0];
imageBufferData[j + 1] = imageData.data[i + 1];
imageBufferData[j + 2] = imageData.data[i + 2];
imageBufferData[j + 3] = imageData.data[i + 3];
j += 4;
} else if (ImageBufferFormat.Rgb === format) {
imageBufferData[j + 0] = imageData.data[i + 0];
imageBufferData[j + 1] = imageData.data[i + 1];
imageBufferData[j + 2] = imageData.data[i + 2];
j += 3;
} else if (ImageBufferFormat.Alpha === format) {
imageBufferData[j] = imageData.data[i + 3];
j++;
}
i += 4;
}
return ImageBuffer.create(imageBufferData, format, canvas.width);
}
/** Get a string describing the mime type associated with an ImageSource format.
* @public
* @extensions
*/
export function getImageSourceMimeType(format: ImageSourceFormat): string {
switch (format) {
case ImageSourceFormat.Jpeg:
return "image/jpeg";
case ImageSourceFormat.Png:
return "image/png";
case ImageSourceFormat.Svg:
return "image/svg+xml;charset=utf-8";
}
return "";
}
/** Get the ImageSourceFormat corresponding to the mime type string, or undefined if the string does not identify a supported ImageSourceFormat.
* @public
* @extensions
*/
export function getImageSourceFormatForMimeType(mimeType: string): ImageSourceFormat | undefined {
switch (mimeType) {
case "image/jpeg": return ImageSourceFormat.Jpeg;
// not standard, but people accidentally use it anyway.
case "image/jpg": return ImageSourceFormat.Jpeg;
case "image/png": return ImageSourceFormat.Png;
case "image/svg+xml;charset=utf-8": return ImageSourceFormat.Svg;
default: return undefined;
}
}
/** Extract an html Image element from a binary jpeg or png.
* @param source The ImageSource containing the binary jpeg or png data.
* @returns a Promise which resolves to an HTMLImageElement containing the uncompressed bitmap image in RGBA format.
* @public
* @extensions
*/
export async function imageElementFromImageSource(source: ImageSource): Promise<HTMLImageElement> {
const blob = new Blob([source.data], { type: getImageSourceMimeType(source.format) });
return imageElementFromUrl(URL.createObjectURL(blob));
}
/** Extract a bitmap from a binary jpeg or png.
* @param source The ImageSource containing the binary jpeg or png data.
* @returns a Promise which resolves to an ImageBitmap containing the uncompressed bitmap image in RGBA format.
* @public
*/
export async function imageBitmapFromImageSource(source: ImageSource): Promise<ImageBitmap> {
const blob = new Blob([source.data], { type: getImageSourceMimeType(source.format) });
return createImageBitmap(blob, {
premultiplyAlpha: "none",
colorSpaceConversion: "none",
});
}
/** Create an html Image element from a URL.
* @param url The URL pointing to the image data.
* @param skipCrossOriginCheck Set this to true to allow an image from a different origin than the web app to load. Default is false.
* @returns A Promise resolving to an HTMLImageElement when the image data has been loaded from the URL.
* @see tryImageElementFromUrl.
* @public
* @extensions
*/
export async function imageElementFromUrl(url: string, skipCrossOriginCheck = false): Promise<HTMLImageElement> {
// We must set crossorigin property so that images loaded from same origin can be used with texImage2d.
// We must do that outside of the promise constructor or it won't work, for reasons.
const image = new Image();
if (!skipCrossOriginCheck) {
image.crossOrigin = "anonymous";
}
return new Promise((resolve: (image: HTMLImageElement) => void, reject) => {
image.onload = () => resolve(image);
// The "error" produced by Image is not an Error. It looks like an Event, but isn't one.
image.onerror = () => reject(new Error("Failed to create image from url"));
image.src = url;
});
}
/** Try to create an html Image element from a URL.
* @param url The URL pointing to the image data.
* @param skipCrossOriginCheck Set this to true to allow an image from a different origin than the web app to load. Default is false.
* @returns A Promise resolving to an HTMLImageElement when the image data has been loaded from the URL, or to `undefined` if an exception occurred.
* @see imageElementFromUrl
* @public
*/
export async function tryImageElementFromUrl(url: string, skipCrossOriginCheck = false): Promise<HTMLImageElement | undefined> {
try {
return await imageElementFromUrl(url, skipCrossOriginCheck);
} catch {
return undefined;
}
}
/**
* Extract the dimensions of the jpeg or png data encoded in an ImageSource.
* @param source The ImageSource containing the binary jpeg or png data.
* @returns a Promise resolving to a Point2d of which x corresponds to the integer width of the uncompressed bitmap and y to the height.
* @public
* @extensions
*/
export async function extractImageSourceDimensions(source: ImageSource): Promise<Point2d> {
const image = await imageElementFromImageSource(source);
return new Point2d(image.naturalWidth, image.naturalHeight);
}
/**
* Produces a data url in "image/png" format from the contents of an ImageBuffer.
* @param buffer The ImageBuffer, of any format.
* @param preserveAlpha If false, the alpha channel will be set to 255 (fully opaque). This is recommended when converting an already-blended image (e.g., one obtained from [[Viewport.readImageBuffer]]).
* @returns a data url as a string suitable for setting as the `src` property of an HTMLImageElement, or undefined if the url could not be created.
* @public
* @extensions
*/
export function imageBufferToPngDataUrl(buffer: ImageBuffer, preserveAlpha = true): string | undefined {
// The default format (and the only format required to be supported) for toDataUrl() is "image/png".
const canvas = imageBufferToCanvas(buffer, preserveAlpha);
return undefined !== canvas ? canvas.toDataURL() : undefined;
}
/**
* Converts the contents of an ImageBuffer to PNG format.
* @param buffer The ImageBuffer, of any format.
* @param preserveAlpha If false, the alpha channel will be set to 255 (fully opaque). This is recommended when converting an already-blended image (e.g., one obtained from [[Viewport.readImageBuffer]]).
* @returns a base64-encoded string representing the image as a PNG, or undefined if the conversion failed.
* @public
* @extensions
*/
export function imageBufferToBase64EncodedPng(buffer: ImageBuffer, preserveAlpha = true): string | undefined {
const urlPrefix = "data:image/png;base64,";
const url = imageBufferToPngDataUrl(buffer, preserveAlpha);
if (undefined === url || !url.startsWith(urlPrefix))
return undefined;
return url.substring(urlPrefix.length);
}
/** Open an image specified as a data URL in a new window or tab. Works around differences between browsers and Electron.
* @param url The base64-encoded image URL.
* @param title An optional title to apply to the new window.
* @beta
*/
export function openImageDataUrlInNewWindow(url: string, title?: string): void {
const win = window.open();
if (null === win)
return;
const div = win.document.createElement("div");
div.innerHTML = `<img src='${url}'/>`;
win.document.body.append(div);
if (undefined !== title)
win.document.title = title;
}
/** Determine the maximum [[ViewRect]] that can be fitted and centered in specified ViewRect given a required aspect ratio.
* @param viewRect The rectangle in which the returned rectangle is to be centered and fitted.
* @param aspectRatio Ratio of width to height.
* @returns A ViewRect centered in the input rectangle.
* @public
*/
export function getCenteredViewRect(viewRect: ViewRect, aspectRatio = 1.4): ViewRect {
// Determine scale that ensures ability to return an image with the prescribed aspectRatio
const scale = Math.min(viewRect.width / aspectRatio, viewRect.height);
const finalWidth = scale * aspectRatio;
const finalHeight = scale;
const left = (viewRect.width - finalWidth) / 2.0;
const right = left + finalWidth;
const top = (viewRect.height - finalHeight) / 2.0;
const bottom = top + finalHeight;
return new ViewRect(left, top, right, bottom);
}
/** Produce a jpeg compressed to no more than specified bytes and of no less than specified quality.
* @param canvas Canvas containing the image to be compressed.
* @param maxBytes Maximum size of output jpeg in bytes.
* @param minCompressionQuality The minimum acceptable image quality as a number between 0 (lowest quality) and 1 (highest quality).
* @returns A [data URL](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs) for the image, or `undefined` if the compression and size constraints could not be met.
* @public
* @extensions
*/
export function getCompressedJpegFromCanvas(canvas: HTMLCanvasElement, maxBytes = 60000, minCompressionQuality = 0.1): string | undefined {
const decrements = 0.1; // Decrements of quality
const bytesPerCharacter = 2; // Assume 16-bit per character
let quality = 1.0; // JPEG Compression quality
while (quality > minCompressionQuality) {
const data = canvas.toDataURL("image/jpeg", quality);
// If we are less than 60 Kb, we are good
if (data.length * bytesPerCharacter < maxBytes)
return data;
quality -= decrements;
}
return undefined;
}