├── .gitattributes ├── .gitignore ├── src ├── native-func.js ├── image-loaders.js ├── ImageBitmap-wrapper.js ├── constants.js ├── options-parser.js ├── check-usability.js ├── misc.js ├── monkeypatch.js ├── image-cropper.js └── test-support.js ├── LICENSE ├── README.md └── dist └── createImageBitmap.js /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | package-lock.json 3 | .DS_Store 4 | webpack.config.js 5 | package.json 6 | -------------------------------------------------------------------------------- /src/native-func.js: -------------------------------------------------------------------------------- 1 | const native_func = globalThis.createImageBitmap; 2 | const nativeCreateImageBitmap = native_func ? 3 | (...args) => native_func.call( globalThis, ...args ) : false; 4 | 5 | export { nativeCreateImageBitmap }; 6 | -------------------------------------------------------------------------------- /src/image-loaders.js: -------------------------------------------------------------------------------- 1 | import { XLINK_NS, error_messages } from "./constants.js"; 2 | import { checkUsability } from "./check-usability.js"; 3 | function loadImage( url ) { 4 | const img = new Image(); 5 | img.src = url; 6 | return new Promise( (resolve, reject) => { 7 | img.onload = (evt) => { 8 | // might be broken 9 | checkUsability( img ); 10 | resolve( img ); 11 | }; 12 | img.onerror = (evt) => { 13 | const error = new DOMException( 14 | error_messages.INVALID_STATE_IMAGE, 15 | "InvalidStateError" 16 | ); 17 | reject( error ); 18 | }; 19 | } ); 20 | } 21 | function loadSVGImage( elem ) { 22 | const url = elem.getAttribute( "href" ) || elem.getAttributeNS( XLINK_NS, "href" ); 23 | return loadImage( url ); 24 | } 25 | 26 | export { loadImage, loadSVGImage }; 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Kaiido 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/ImageBitmap-wrapper.js: -------------------------------------------------------------------------------- 1 | import { cropImage } from "./image-cropper.js"; 2 | import { nativeCreateImageBitmap } from "./native-func.js"; 3 | 4 | const getCanvasProtoDesc = ( prop ) => 5 | Object.getOwnPropertyDescriptor( HTMLCanvasElement.prototype, prop ); 6 | 7 | // we can't use the 'class' syntax here 8 | // see https://esdiscuss.org/topic/classes-and-enumerability 9 | function ImageBitmapPolyfill() { 10 | throw new TypeError( "Illegal Constructor" ); 11 | } 12 | ImageBitmapPolyfill.prototype = Object.create( { 13 | close() { 14 | // there is no real way to "detach" a canvas's buffer 15 | // after a drawing context has been created. 16 | // the closest is to set its width and height to 0 17 | const width_setter = getCanvasProtoDesc( "width" ).set; 18 | width_setter.call( this, 0); 19 | const height_setter = getCanvasProtoDesc( "height" ).set; 20 | height_setter.call( this, 0 ); 21 | }, 22 | get width() { 23 | const canvas_getter = getCanvasProtoDesc( "width" ).get; 24 | return canvas_getter.call( this ); 25 | }, 26 | get height() { 27 | const canvas_getter = getCanvasProtoDesc( "height" ).get; 28 | return canvas_getter.call( this ); 29 | }, 30 | get [ Symbol.toStringTag ]() { 31 | return "ImageBitmap"; 32 | } 33 | } ); 34 | 35 | function customWrapper( source ) { 36 | if( !(source instanceof HTMLCanvasElement) ) { 37 | source = cropImage( source ); 38 | } 39 | Object.setPrototypeOf( source, ImageBitmapPolyfill.prototype ); 40 | return source; 41 | } 42 | 43 | function convertToImageBitmap( source ) { 44 | if( (source instanceof globalThis.ImageBitmap) ) { 45 | return source; 46 | } 47 | if( nativeCreateImageBitmap ) { 48 | return nativeCreateImageBitmap( source ); 49 | } 50 | return customWrapper( source ); 51 | } 52 | 53 | export { convertToImageBitmap, ImageBitmapPolyfill }; 54 | -------------------------------------------------------------------------------- /src/constants.js: -------------------------------------------------------------------------------- 1 | // the properties we look for 2 | // the ones that aren't real property names are prefixed with a _ 3 | // they are separated in two lists 4 | // so we can perform synchronous and async tests separately 5 | const synchronous_polyfills = [ 6 | "_imageBitmapOptions", 7 | "resizeWidth", 8 | "resizeHeight", 9 | "resizeQuality", 10 | "imageOrientation" 11 | ] 12 | const asynchronous_polyfills = [ 13 | "_Blob", 14 | "_ImageData", 15 | "_SVGBlob", 16 | "_SVGImageElement", 17 | ]; 18 | // imageBitmapOptions properties we know of, 19 | // we don't necessarily have a polyfill for all of these 20 | // we still test them anyway 21 | const known_properties = [ 22 | "resizeWidth", 23 | "resizeHeight", 24 | "resizeQuality", 25 | "imageOrientation", 26 | "premultiplyAlpha", 27 | "colorSpaceConversion" 28 | ]; 29 | // https://html.spec.whatwg.org/multipage/imagebitmap-and-animations.html#imagebitmap 30 | const enums = { 31 | ResizeQuality: [ "pixelated", "low", "medium", "high" ], 32 | ImageOrientation: [ "none", "flipY" ] 33 | }; 34 | 35 | const error_messages = { 36 | COMMON_HEADER: "Failed to execute 'createImageBitmap': ", 37 | INVALID_STATE_IMAGE: "Provided image was in an invalid state.", 38 | ARGUMENT_COUNT_1: "At least one argument is required.", 39 | ARGUMENT_COUNT_N: "%s is not a valid argument count for any overload", 40 | CROP_RECT_ZERO: "The crop rect width passed to createImageBitmap must be nonzero", 41 | ALLOCATION_FAILED: "The ImageBitmap could not be allocated.", 42 | INVALID_SOURCE: "Argument 1 could not be converted to any of: HTMLImageElement, " + 43 | "SVGImageElement, HTMLCanvasElement, HTMLVideoElement, ImageBitmap, Blob, " + 44 | "CanvasRenderingContext2D, ImageData.", 45 | ENUM: "'%v' is not a valid value for enumeration %e", 46 | ALLOCATION: "The ImageBitmap couldn't be allocated." 47 | } 48 | 49 | const canvas_sources = [ 50 | "HTMLImageElement", 51 | "SVGImageElement", 52 | "HTMLVideoElement", 53 | "HTMLCanvasElement", 54 | "OffscreenCanvas", 55 | "ImageBitmap" 56 | ]; 57 | 58 | const SVG_MIME = "image/svg+xml"; 59 | const SVG_NS = "http://www.w3.org/2000/svg"; 60 | const XLINK_NS = "http://www.w3.org/1999/xlink"; 61 | const GIF_URL = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"; 62 | 63 | export { 64 | synchronous_polyfills, 65 | asynchronous_polyfills, 66 | enums, 67 | known_properties, 68 | error_messages, 69 | canvas_sources, 70 | SVG_MIME, 71 | SVG_NS, 72 | XLINK_NS, 73 | GIF_URL 74 | }; 75 | -------------------------------------------------------------------------------- /src/options-parser.js: -------------------------------------------------------------------------------- 1 | import { convertToLong, enforceRangeLong } from "./misc.js"; 2 | import { enums, error_messages } from "./constants.js"; 3 | 4 | const common_error_header = error_messages.COMMON_HEADER; 5 | 6 | function testEnum( options, property, enum_name ) { 7 | const enum_list = enums[ enum_name ]; 8 | const value = options[ property ]; 9 | if( value !== undefined && !enum_list.includes( value ) ) { 10 | const error_msg = error_messages.ENUM 11 | .replace( "%v", value ) 12 | .replace( "%e", enum_name ); 13 | throw new TypeError( error_msg ); 14 | } 15 | } 16 | function parseOptions( source, ...args ) { 17 | 18 | let [ sx, sy, sw, sh ] = args.map( convertToLong ); 19 | let cropRect; // optional 20 | const options = (args.length === 1 ? args[ 0 ] : args[ 4 ]) || {}; 21 | 22 | const resizeWidth = ("resizeWidth" in options) && 23 | enforceRangeLong( options.resizeWidth ); 24 | const resizeHeight = ("resizeHeight" in options) && 25 | enforceRangeLong( options.resizeHeight ); 26 | 27 | testEnum( options, "resizeQuality", "ResizeQuality" ); 28 | testEnum( options, "imageOrientation", "ImageOrientation" ); 29 | 30 | const arguments_count = arguments.length; 31 | 32 | if( !arguments_count ) { 33 | throw new TypeError( common_error_header + error_messages.ARGUMENT_COUNT_1 ); 34 | } 35 | if( arguments_count > 2 && arguments_count < 5 ) { 36 | const error_count_msg = error_messages.ARGUMENT_COUNT_N.replace( "%s", arguments_count ); 37 | throw new TypeError( common_error_header + error_count_msg ); 38 | } 39 | if( arguments_count >= 5 ) { // 'sw' and 'sh' are defined 40 | if( !sw || !sh ) { // convertToLong converts invalid longs to NaN 41 | throw new RangeError( common_error_header + error_messages.CROP_RECT_ZERO ); 42 | } 43 | // invalid sx and sy are treated as 0 44 | sx = sx || 0; 45 | sy = sy || 0; 46 | cropRect = { sx, sy, sw, sh }; 47 | } 48 | if( resizeWidth === 0 || resizeHeight === 0 ) { 49 | const message = common_error_header + error_messages.ALLOCATION_FAILED; 50 | throw new DOMException( message, "InvalidStateError" ); 51 | } 52 | 53 | return { 54 | cropRect, 55 | ...options 56 | }; 57 | 58 | } 59 | function cleanArguments( missing_features, ...args ) { 60 | const number_of_arguments = args.length; 61 | if( 62 | missing_features.has( "_imageBitmapOptions" ) && 63 | [ 2, 6 ].includes( number_of_arguments ) 64 | ) { 65 | return args.slice( 0, number_of_arguments - 1 ); 66 | } 67 | return args; 68 | } 69 | 70 | export { parseOptions, cleanArguments }; 71 | -------------------------------------------------------------------------------- /src/check-usability.js: -------------------------------------------------------------------------------- 1 | import { error_messages } from "./constants.js"; 2 | import { get2dContext, buildSourceIs } from "./misc.js"; 3 | import { getSourceDimensions } from "./image-cropper.js"; 4 | 5 | function invalidStateImageError() { 6 | const message = error_messages.COMMON_HEADER + error_messages.INVALID_STATE_IMAGE; 7 | throw new DOMException( message, "InvalidStateError" ); 8 | } 9 | // 10 | // https://html.spec.whatwg.org/multipage/canvas.html#check-the-usability-of-the-image-argument 11 | // 12 | function checkUsability( source ) { 13 | 14 | const sourceIs = buildSourceIs( source ); 15 | 16 | // HTMLCanvasElement, and OffscreenCanvas are only invalid if their width or height is zero 17 | // ImageBitmap would be when detached, but detaching it will set its width and height to zero 18 | // so we can share their check 19 | if( 20 | sourceIs( "HTMLCanvasElement" ) || 21 | sourceIs( "OffscreenCanvas" ) || 22 | sourceIs( "ImageBitmap" ) 23 | ) { 24 | 25 | if( source.width === 0 || source.height === 0 ) { 26 | invalidStateImageError(); 27 | } 28 | 29 | } 30 | // there is no synchronous way to tell if an HTMLImageElement 31 | // is in a broken state, such an element could have a width and height 32 | // at the time we check it 33 | // HTMLImageElement::decode() could tell this asynchronously 34 | // but 1. we need a synchrounous method, 2. it's not available everywhere 35 | // and 3. it only concerns HTMLImageElement 36 | // SVGImageElements don't have any API letting us know (not even a width nor height) 37 | // HTMLVideoElements only have unreliable videoWidth and videoHeight 38 | // however, CanvasRenderingContext2d.createPattern is supposed to call this algorithm too 39 | // so we can (ab)use it, even though it works only in Firefox... 40 | // see https://wpt.fyi/results/html/canvas/element/fill-and-stroke-styles/2d.pattern.image.broken.html 41 | else if( 42 | sourceIs( "HTMLImageElement" ) || 43 | sourceIs( "HTMLVideoElement" ) || 44 | sourceIs( "SVGImageElement" ) 45 | ) { 46 | 47 | // we can still start by checking the size, if 0 no need to go farther 48 | const { width, height } = getSourceDimensions( source ); 49 | // look for strict equality, SVGImageElement returns NaN 50 | if( width === 0 || height === 0 ) { 51 | invalidStateImageError(); 52 | } 53 | 54 | let failed = false; 55 | try { 56 | const pat = get2dContext( 0, 0 ).createPattern( source, "no-repeat" ); 57 | failed = !pat; 58 | } 59 | catch( err ) { 60 | // Safari doesn't support drawing SVGImageElement on a canvas, 61 | // nor do they support creating a CanvasPattern from it 62 | // we thus return a "maybe" string to let the other side handle it as they wish 63 | return "maybe"; 64 | } 65 | if( failed ) { 66 | invalidStateImageError(); 67 | } 68 | 69 | } 70 | 71 | return true; 72 | 73 | } 74 | 75 | export { checkUsability, invalidStateImageError }; 76 | -------------------------------------------------------------------------------- /src/misc.js: -------------------------------------------------------------------------------- 1 | import { error_messages } from "./constants.js"; 2 | 3 | // we could be lazy an just check for length 0, 4 | // which would probably be enough in 99% of the case 5 | // but we won't call it many times anyway, 6 | // so better be precise 7 | function isDetachedBuffer( buffer ) { 8 | try { 9 | buffer.slice( 0,0 ); 10 | return false; 11 | } 12 | catch( err ) { return true; } 13 | } 14 | 15 | // https://heycam.github.io/webidl/#idl-long 16 | const long_tester = new Int32Array( 1 ); 17 | function convertToLong( value ) { 18 | if( !Number.isFinite( value ) ) { 19 | return NaN; 20 | } 21 | long_tester[ 0 ] = value; 22 | return long_tester[ 0 ]; 23 | } 24 | // https://heycam.github.io/webidl/#EnforceRange 25 | function enforceRangeLong( value ) { 26 | const as_long = convertToLong( value ); 27 | if( isNaN( as_long ) ) { 28 | throw new TypeError( "Invalid long value" ); 29 | } 30 | return as_long; 31 | } 32 | // we can be quite strict here 33 | // because to load it in a , 34 | // the type must be set to this value 35 | function blobCanBeSVG( blob ) { 36 | return blob && blob.type === "image/svg+xml"; 37 | } 38 | 39 | function get2dContext( width, height ) { 40 | const canvas = Object.assign( document.createElement( "canvas" ), { width, height } ); 41 | const ctx = canvas.getContext( "2d" ); 42 | // not yet in the specs 43 | // https://github.com/whatwg/html/issues/3323 44 | if( 45 | width && height && // 0 width and height should not throw here 46 | !contextIsCorrectlyAllocated( ctx ) 47 | ) { 48 | throw new DOMException( error_messages.ALLOCATION, "InvalidStateError" ); 49 | } 50 | return ctx; 51 | } 52 | 53 | function contextIsCorrectlyAllocated( ctx ) { 54 | // https://github.com/fserb/canvas2D/blob/master/spec/context-loss.md 55 | if( ctx.isContextLost ) { 56 | // weirdness in the API, 57 | // we need to interact with the context for the status to change 58 | ctx.translate( 0, 0 ); 59 | return !ctx.isContextLost(); 60 | } 61 | // Chrome has a isContextLost method, but it seems to only work async... 62 | let allocated = false; 63 | // Firefox does throw if the context is lost when we call getImageData 64 | try { 65 | ctx.fillRect( 0, 0, 1, 1 ); 66 | allocated = ctx.getImageData( 0, 0, 1, 1 ).data[ 3 ] !== 0; 67 | } 68 | finally { 69 | ctx.clearRect( 0, 0, 1, 1 ); 70 | return allocated; 71 | } 72 | } 73 | 74 | // ImageData constructor may not be available 75 | // fortunately we can fall back to ctx2D.createImageData 76 | function createImageData( width, height ) { 77 | try { 78 | // low finger-print if available 79 | return new ImageData( width, height ); 80 | } 81 | catch( err ) { 82 | const context = create2dContext( 0, 0 ); 83 | return context.createImageData( width, height ); 84 | } 85 | } 86 | 87 | // a helper to build a 'sourceIs' function 88 | function buildSourceIs( source ) { 89 | return ( type ) => { 90 | const constructor = globalThis[ type ]; 91 | return constructor && (source instanceof constructor); 92 | }; 93 | } 94 | 95 | export { 96 | isDetachedBuffer, 97 | convertToLong, 98 | enforceRangeLong, 99 | blobCanBeSVG, 100 | get2dContext, 101 | createImageData, 102 | buildSourceIs 103 | }; 104 | -------------------------------------------------------------------------------- /src/monkeypatch.js: -------------------------------------------------------------------------------- 1 | import { 2 | requiresPolyfill, 3 | requiresAsyncTests, 4 | tests_results, 5 | async_tests 6 | } from "./test-support.js"; 7 | import { checkUsability, invalidStateImageError } from "./check-usability.js"; 8 | import { parseOptions, cleanArguments } from "./options-parser.js"; 9 | import { nativeCreateImageBitmap } from "./native-func.js"; 10 | import { canvas_sources, error_messages } from "./constants.js"; 11 | import { loadImage, loadSVGImage } from "./image-loaders.js"; 12 | import { cropImage, cropBlobImage, cropImageData } from "./image-cropper.js"; 13 | import { convertToImageBitmap, ImageBitmapPolyfill } from "./ImageBitmap-wrapper.js"; 14 | import { isDetachedBuffer, buildSourceIs } from "./misc.js"; 15 | 16 | if( !nativeCreateImageBitmap ) { 17 | globalThis.ImageBitmap = ImageBitmapPolyfill; 18 | } 19 | 20 | // https://html.spec.whatwg.org/multipage/imagebitmap-and-animations.html#dom-createimagebitmap 21 | globalThis.createImageBitmap = async function( source, ...args ) { // length 1 22 | 23 | const options = parseOptions( ...arguments ); 24 | 25 | // with some sources (canvas & video) we need to stay synchronous until 26 | // the source's buffer gets painted 27 | // but some tests are async (e.g SVGImageElement) 28 | // so we only await for these tests when we are sure they are required 29 | const missing_features = requiresAsyncTests( source, options ) ? 30 | await async_tests : tests_results; 31 | 32 | // full compat 33 | if( missing_features.size === 0 ) { 34 | return nativeCreateImageBitmap( ...arguments ); 35 | } 36 | 37 | // polyfill is not required, but some features may not be supported 38 | if( 39 | nativeCreateImageBitmap && 40 | !requiresPolyfill( missing_features, options, source ) 41 | ) { 42 | return nativeCreateImageBitmap( ...cleanArguments( missing_features, ...arguments ) ); 43 | } 44 | 45 | const sourceIs = buildSourceIs( source ); 46 | 47 | let cropped_image; 48 | if( canvas_sources.some( sourceIs ) ) { 49 | 50 | const is_usable = checkUsability( source ); 51 | 52 | // checkUsability failed to determine if source is usable, 53 | // as of this writing, this only concerns SVGImageElement 54 | // and since we do convert SVGImageElements anyway right after 55 | // we don't have much to do now 56 | if( is_usable === "maybe" ) { 57 | // might want to do something someday. 58 | } 59 | 60 | // SVGImageElement have a very poor API to work with 61 | // (e.g no intrinsic dimension is exposed) 62 | // so we convert these to HTMLImageElement 63 | // must be called after 'checkUsability' for we correctly throw with unusable media 64 | // (that is, for UAs that do support SVGImageElement as CanvasSource) 65 | if( sourceIs( "SVGImageElement" ) ) { 66 | source = await loadSVGImage( source ); 67 | } 68 | 69 | if( sourceIs( "HTMLImageElement" ) ) { 70 | // Since we can't detect true "image has no dimensions" (see test-support.js) 71 | // we unfortunately can't fall here... 72 | // so while per specs this should work, it's just dead code... 73 | if( 74 | (!source.naturalWidth && !source.naturalHeight) && 75 | (!options.resizeWidth || !options.resizeHeight) 76 | ) { 77 | invalidStateImageError() 78 | } 79 | 80 | } 81 | cropped_image = cropImage( source, options ); 82 | 83 | } 84 | else if( sourceIs( "Blob" ) ) { 85 | cropped_image = await cropBlobImage( source, options ); 86 | } 87 | else if( sourceIs( "ImageData" ) ) { 88 | if( isDetachedBuffer( source.data.buffer ) ) { 89 | invalidStateImageError(); 90 | } 91 | cropped_image = await cropImageData( source, options ); 92 | } 93 | if( !cropped_image ) { 94 | throw new TypeError( error_messages.INVALID_SOURCE ); 95 | } 96 | 97 | return await convertToImageBitmap( cropped_image ); 98 | 99 | }; 100 | 101 | globalThis.createImageBitmap._original = nativeCreateImageBitmap; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # createImageBitmap MonkeyPatch 2 | 3 | ### A monkey-patch to the HTML's [`createImageBitmap`][1] method. 4 | 5 | Only Chromium based browsers currently support the full API, Firefox supporting it only partially and Safari not at all. 6 | This means that for cross-browser projects, we can't use it, despite its great usefulness and ease of use. 7 | 8 | This monkey-patch aims at filling as much gaps as possible between all these browsers. 9 | 10 | However, truth be told, it can not polyfill everything in this API, here is a list of what it supports in every browsers and what it doesn't: 11 | 12 | Feature | ![Chromium based][2] | ![Firefox][3] | ![Safari][4] 13 | ----------------------------------------- | -------------------- | --------------------- | ---------------- 14 | createImageBitmap + cropping | native | native | ✔ (1) 15 | HTMLImageElement as source | native | native | ✔ 16 | HTMLCanvasElement as source | native | native | ✔ 17 | OffscreenCanvas as source | native | :x: | :x: 18 | SVGImageElement as source | native | native | ✔ 19 | ImageBitmap as source | native | native | ✔ 20 | ImageData as source | native | native | ✔ 21 | Raster image in Blob as source | native | native | ✔ 22 | SVG image in Blob as source (2)| ✔ | ✔ | ✔ 23 | ImageBitmapOptions | native | ✔ | ✔ 24 | resizeWidth & resizeHeight | native | ✔ | ✔ 25 | resizeQuality: pixelated | native | ✔ | ✔ 26 | resizeQuality: low | native | native | ✔ 27 | resizeQuality: medium | native | :x: | :x: 28 | resizeQuality: high | native | :x: | :x: 29 | imageOrientation | native | ✔ | ✔ 30 | colorSpaceConversion | native | :x: | :x: 31 | premultiplyAlpha | native | :x: | :x: 32 | Available in Workers | native | native (3) | :x: 33 | Transferrable | native | native | :x: 34 | 35 | (1) Returns a modified HTMLCanvasElement instance in place of an ImageBitmap object. 36 | (2) Only for SVG images with an intrinsic width and height, as per the specs recommendation. 37 | (3) Only the native implementation is available in Workers 38 | 39 | ### How to use 40 | 41 | You can include the bundled version available in `/src/` in a <script> tag before the first use of `createImageBitmap`, 42 | 43 | ```html 44 | 45 | ``` 46 | 47 | or you can use the ESM modules available in `/src/`. 48 | 49 | ```html 50 | 51 | ``` 52 | 53 | ### Why isn't the monkey-patch available in web Workers? 54 | 55 | The monkey-patch makes most of its work on an HTMLCanvasElement 2d context, and relies in various places in the ability to have DOM Elements (for instance HTMLImageElements are used to load images from Blobs). 56 | Since Web-Workers don't have access to the DOM nor any Elements, the scope of what this monkey-patch could do currently seems too limited to be worth the pain. 57 | 58 | ### Why isn't the monkey-patch available in node-js? 59 | 60 | People at [node-canvas][2] probably are the best to implement such a thing. 61 | It should take only a few tweaks from here to get a node-canvas + jsdom version working though, so feel free to fork this repo and propose them a PR if you wish. 62 | 63 | ### Why doesn't this monkey-patch add support for options like all resize qualities, *colorSpaceConversion* or *premultiplyAlpha**? 64 | 65 | Mainly lack of time. We accept PR though ¯\_(ツ)_/¯ 66 | 67 | ### Why no ImageBitmapRenderer at least? 68 | 69 | Still lack of time, and a Safari weirdness where they do expose this interface, even though they don't have the `ImageBitmap` that goes with it, and which won't accept our "fake" version. 70 | But hopefully, this will be one of the first next additions. 71 | 72 | ### What about my old Internet Explorer? Can I use it there? 73 | 74 | Not as-is. The scripts are written using ES2021 syntax and are targeting recent browsers. 75 | You should be able to transpile it to ES5 though, and it should work even in IE9, **but it hasn't been tested there**. 76 | 77 | ### Does this mean even my shiny Chromium browser will use the less than optimal 2D context instead of a true ImageBitmap? 78 | 79 | No, the monkey-patch only patches what it detects is not supported by the current browser. It leaves the native implementation do its job when it can do it. 80 | 81 | [1]: https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/createImageBitmap 82 | [2]: https://raw.githubusercontent.com/alrra/browser-logos/main/src/chromium/chromium_48x48.png 83 | [3]: https://raw.githubusercontent.com/alrra/browser-logos/master/src/firefox/firefox_48x48.png 84 | [4]: https://raw.githubusercontent.com/alrra/browser-logos/master/src/safari/safari_48x48.png 85 | [5]: https://github.com/Automattic/node-canvas 86 | -------------------------------------------------------------------------------- /src/image-cropper.js: -------------------------------------------------------------------------------- 1 | import { nativeCreateImageBitmap } from "./native-func.js"; 2 | import { blobCanBeSVG, get2dContext, buildSourceIs } from "./misc.js"; 3 | import { loadImage } from "./image-loaders.js"; 4 | import { cleanArguments } from "./options-parser.js"; 5 | 6 | function getSourceDimensions( source ) { 7 | const sourceIs = buildSourceIs( source ); 8 | if( sourceIs( "HTMLImageElement" ) ) { 9 | return { width: source.naturalWidth, height: source.naturalHeight }; 10 | } 11 | else if( sourceIs( "HTMLVideoElement" ) ) { 12 | return { width: source.videoWidth, height: source.videoHeight }; 13 | } 14 | else if( sourceIs( "SVGImageElement" ) ) { 15 | return { width: NaN, height: NaN }; 16 | } 17 | else if( 18 | sourceIs( "HTMLCanvasElement" ) || 19 | sourceIs( "OffscreenCanvas" ) || 20 | sourceIs( "ImageBitmap" ) || 21 | sourceIs( "ImageData" ) 22 | ) { 23 | return source; 24 | } 25 | return { width: NaN, height: NaN }; 26 | } 27 | 28 | function cropImage( source, options ) { 29 | 30 | // merge passed cropRect with resize and orientation options 31 | const safe_rect = getFinalCropRect( source, options ); 32 | 33 | const { height } = getSourceDimensions( source ); 34 | const { sx, sy, sw, sh, dx, dy, dw, dh, flipY, resizeQuality } = safe_rect; 35 | const out_ctx = get2dContext( safe_rect.width, safe_rect.height ); 36 | const out_canvas = out_ctx.canvas; 37 | 38 | if( resizeQuality === "pixelated" ) { 39 | out_ctx.imageSmoothingEnabled = false; 40 | } 41 | else if( resizeQuality ) { 42 | // to do: better image quality resizers 43 | out_ctx.imageSmoothingQuality = resizeQuality; 44 | } 45 | out_ctx.drawImage( source, sx, sy, sw, sh, dx, dy, dw, dh ); 46 | 47 | if( flipY ) { 48 | out_ctx.globalCompositeOperation = "copy"; 49 | out_ctx.setTransform( 1, 0, 0, -1, 0, out_canvas.height ); 50 | out_ctx.drawImage( out_canvas, 0, 0 ); 51 | } 52 | 53 | return out_canvas; 54 | 55 | } 56 | 57 | 58 | async function cropBlobImage( blob, options ) { 59 | 60 | const url = globalThis.URL.createObjectURL( blob ); 61 | const img = await loadImage( url ); 62 | globalThis.URL.revokeObjectURL( blob ); 63 | 64 | return cropImage( img, options ); 65 | 66 | } 67 | async function cropImageData( img, options ) { 68 | 69 | const { resizeWidth, resizeHeight } = parseResizeOptions( img, options ); 70 | const { resizeQuality, imageOrientation } = options; 71 | const cropRect = options.cropRect || {}; 72 | // beware, sw and sh can be negative 73 | const sx = cropRect.sx || 0; 74 | const sy = cropRect.sy || 0; 75 | const sw = cropRect.sw || img.width; 76 | const sh = cropRect.sh || img.height; 77 | const dx = sw < 0 ? (sw * -1) - sx : -sx; 78 | const dy = sh < 0 ? (sh * -1) - sy : -sy; 79 | const dw = Math.abs( sw ); 80 | const dh = Math.abs( sh ); 81 | 82 | const a_ctx = get2dContext( dw, dh ) 83 | a_ctx.putImageData( img, dx, dy, sx, sy, sw, sh ); 84 | 85 | const flipY = imageOrientation === "flipY"; 86 | const should_redraw = resizeWidth || flipY; 87 | let out_ctx; 88 | 89 | if( resizeWidth ) { 90 | out_ctx = get2dContext( resizeWidth, resizeHeight ); 91 | if( resizeQuality === "pixelated" ) { 92 | out_ctx.imageSmoothingEnabled = false; 93 | } 94 | else { 95 | out_ctx.imageSmoothingQuality = resizeQuality; 96 | } 97 | } 98 | else { 99 | out_ctx = a_ctx; 100 | a_ctx.globalCompositeOperation = "copy"; 101 | } 102 | 103 | const a_canvas = a_ctx.canvas; 104 | const out_canvas = out_ctx.canvas; 105 | 106 | if( flipY ){ 107 | out_ctx.setTransform( 1, 0, 0, -1, 0, out_canvas.height ); 108 | } 109 | 110 | if( should_redraw ) { 111 | out_ctx.drawImage( a_canvas, 112 | 0, 0, a_canvas.width, a_canvas.height, 113 | 0, 0, out_canvas.width, out_canvas.height 114 | ); 115 | } 116 | 117 | return out_canvas; 118 | 119 | } 120 | 121 | function getFinalCropRect( source, options ) { 122 | 123 | const { width, height } = getSourceDimensions( source ); 124 | const { resizeWidth, resizeHeight } = parseResizeOptions( source, options ); 125 | 126 | const crop_rect = options.cropRect || 127 | { sx: 0, sy: 0, sw: width, sh: height }; 128 | const dest_rect = { 129 | dx: 0, 130 | dy: 0, 131 | dw: resizeWidth || Math.abs( crop_rect.sw ), // resizeXXX must be non-zero 132 | dh: resizeHeight || Math.abs( crop_rect.sh ) 133 | }; 134 | 135 | const safe_rect = getSafeRect( width, height, crop_rect, dest_rect ); 136 | safe_rect.resizeQuality = options.resizeQuality; 137 | safe_rect.flipY = options.imageOrientation === "flipY"; 138 | safe_rect.width = dest_rect.dw; 139 | safe_rect.height = dest_rect.dh; 140 | 141 | return safe_rect; 142 | 143 | } 144 | 145 | function getSafeRect( width, height, { sx, sy, sw, sh }, { dx, dy, dw, dh } ) { 146 | // Safari doesn't support cropping outside of source's boundaries through drawImage 147 | // so we need to provide a "safe" rect 148 | 149 | if( sw < 0 ) { 150 | sx += sw; 151 | sw = Math.abs( sw ); 152 | } 153 | if( sh < 0 ) { 154 | sy += sh; 155 | sh = Math.abs( sh ); 156 | } 157 | if( dw < 0 ) { 158 | dx += dw; 159 | dw = Math.abs( dw ); 160 | } 161 | if( dh < 0 ) { 162 | dy += dh; 163 | dh = Math.abs( dh ); 164 | } 165 | const x1 = Math.max( sx, 0 ); 166 | const x2 = Math.min( sx + sw, width ); 167 | const y1 = Math.max( sy, 0 ); 168 | const y2 = Math.min( sy + sh, height ); 169 | const w_ratio = dw / sw; 170 | const h_ratio = dh / sh; 171 | 172 | return { 173 | sx: x1, 174 | sy: y1, 175 | sw: x2 - x1, 176 | sh: y2 - y1, 177 | dx: sx < 0 ? dx - (sx * w_ratio) : dx, 178 | dy: sy < 0 ? dy - (sy * h_ratio) : dy, 179 | dw: (x2 - x1) * w_ratio, 180 | dh: (y2 - y1) * h_ratio 181 | }; 182 | 183 | } 184 | 185 | function parseResizeOptions( source, options ) { 186 | 187 | const { width, height } = getSourceDimensions( source ); 188 | let { resizeWidth, resizeHeight } = options; 189 | if( resizeWidth === undefined && resizeHeight !== undefined ) { 190 | const { width, height } = getSourceDimensions( source ); 191 | resizeWidth = Math.ceil( width * (resizeHeight / height) ); 192 | } 193 | if( resizeHeight === undefined && resizeWidth !== undefined ) { 194 | const { width, height } = getSourceDimensions( source ); 195 | resizeHeight = Math.ceil( height * (resizeWidth / width) ); 196 | } 197 | return { resizeWidth, resizeHeight }; 198 | 199 | } 200 | 201 | export { 202 | getSourceDimensions, 203 | cropImage, 204 | cropBlobImage, 205 | cropImageData 206 | }; 207 | -------------------------------------------------------------------------------- /src/test-support.js: -------------------------------------------------------------------------------- 1 | import { 2 | known_properties, 3 | synchronous_polyfills, 4 | asynchronous_polyfills, 5 | SVG_MIME, 6 | SVG_NS, 7 | GIF_URL 8 | } from "./constants.js"; 9 | import { checkUsability } from "./check-usability.js"; 10 | import { nativeCreateImageBitmap } from "./native-func.js"; 11 | import { createImageData, blobCanBeSVG, buildSourceIs } from "./misc.js"; 12 | 13 | // to avoid having the grabbing of the source's buffer delayed 14 | // we try to return the results of the tests synchronously when possible. 15 | // for this matter, we update a single list of results. 16 | let async_tests_are_done = false; 17 | 18 | // some tests can be performed synchronously 19 | // we initiate our Set with these directly 20 | const tests_results = performSynchronousTests(); 21 | const async_tests = performAsynchronousTests() 22 | 23 | function performSynchronousTests() { 24 | // nothing to test... 25 | if( !nativeCreateImageBitmap ) { 26 | const all_polyfills = synchronous_polyfills.concat( asynchronous_polyfills ); 27 | return new Set( all_polyfills ); 28 | } 29 | const synchronous_tests = [ testOptionsBag() ]; 30 | const supported_features = new Set( synchronous_tests.flat() ); 31 | const required_polyfills = synchronous_polyfills 32 | .filter( (type) => !supported_features.has( type ) ); 33 | 34 | return new Set( required_polyfills ); 35 | } 36 | async function performAsynchronousTests() { 37 | 38 | // nothing to test... 39 | if( !nativeCreateImageBitmap ) { 40 | async_tests_are_done = true; 41 | return tests_results; 42 | } 43 | 44 | const asynchronous_tests = [ 45 | testBitmapBlobSource(), 46 | testImageDataSource(), 47 | testSVGBlobSource(), 48 | testSVGImageElementSource() 49 | ]; 50 | 51 | const results = await Promise.all( asynchronous_tests ); 52 | const supported_features = new Set( results.flat() ); 53 | 54 | const required_polyfills = asynchronous_polyfills 55 | .filter( (type) => !supported_features.has( type ) ); 56 | 57 | required_polyfills.forEach( (prop) => tests_results.add( prop ) ); 58 | 59 | async_tests_are_done = true; 60 | 61 | return tests_results; 62 | 63 | } 64 | 65 | function testOptionsBag() { 66 | 67 | const out = []; 68 | const tester = {}; 69 | const addTestProperty = ( prop ) => { 70 | Object.defineProperty( tester, prop , { get(){ out.push( prop ) } } ); 71 | }; 72 | known_properties.forEach( addTestProperty ); 73 | 74 | nativeCreateImageBitmap( new ImageData(1,1), tester ) 75 | .then( (img) => img.close() ) 76 | .catch( () => {} ); 77 | 78 | // some implementations will throw in the Promise 79 | // if the option bag is passed 80 | // to avoid doing it ourselves when calling the native method 81 | // we need to mark its support here 82 | // however since we need this method to be synchronous 83 | // we can't wait for the Promise to reject 84 | // so we assume any implementation supporting the options bag 85 | // will support at least one of the currently known options 86 | if( out.length ) { 87 | out.push( "_imageBitmapOptions" ); 88 | } 89 | 90 | return out; 91 | 92 | } 93 | async function testBitmapBlobSource() { 94 | 95 | const out = []; 96 | const blob = await fetch( GIF_URL ) 97 | .then( resp => resp.ok && resp.blob() ); 98 | 99 | try { 100 | const img = await nativeCreateImageBitmap( blob ); 101 | if( img.width === 1 ) { 102 | out.push( "_Blob" ); 103 | } 104 | } 105 | catch( err ) { 106 | 107 | } 108 | 109 | return out; 110 | 111 | } 112 | async function testImageDataSource() { 113 | 114 | const out = []; 115 | const image = createImageData( 1, 1 ); 116 | 117 | try { 118 | const img = await nativeCreateImageBitmap( image ); 119 | if( img.width === 1 ) { 120 | out.push( "_ImageData" ); 121 | } 122 | } 123 | catch( err ) { 124 | 125 | } 126 | 127 | return out; 128 | 129 | } 130 | async function testSVGBlobSource() { 131 | 132 | const out = []; 133 | const svg_markup = ` 134 | 135 | `; 136 | const svg_blob = new Blob( [ svg_markup ], { type: SVG_MIME } ); 137 | 138 | try { 139 | const img = await nativeCreateImageBitmap( svg_blob ); 140 | if( img.width === 1 ) { 141 | out.push( '_SVGBlob' ); 142 | } 143 | } 144 | catch( err ) { 145 | 146 | } 147 | 148 | return out; 149 | 150 | } 151 | async function testSVGImageElementSource() { 152 | 153 | const out = []; 154 | if( !("SVGImageElement" in globalThis) ) { // Worker 155 | return out; 156 | } 157 | 158 | const wait = (time) => new Promise( res => setTimeout( res, time ) ); 159 | 160 | const img = document.createElementNS( SVG_NS, 'image' ); 161 | img.setAttributeNS( 'http://www.w3.org/1999/xlink', 'xlink:href', GIF_URL ); 162 | 163 | const imgIsNotAvailable = () => { 164 | try { 165 | checkUsability( img ); 166 | return false; 167 | } 168 | catch( err ) { 169 | return true; 170 | } 171 | }; 172 | 173 | let retries = 0; 174 | do { 175 | await wait( 10 ); 176 | } 177 | while( imgIsNotAvailable() && (++retries < 300) ); 178 | 179 | try { 180 | const bmp = await nativeCreateImageBitmap( img ); 181 | out.push( "_SVGImageElement" ); 182 | } 183 | catch( err ) { 184 | 185 | } 186 | 187 | return out; 188 | 189 | } 190 | 191 | function requiresPolyfill( missing_features, options, source ) { 192 | 193 | const sourceIs = buildSourceIs( source ); 194 | 195 | if( sourceIs( "Blob" ) ) { 196 | if( missing_features.has( "_Blob" ) ) { 197 | return true; 198 | } 199 | const is_svg_blob = blobCanBeSVG( source ); 200 | if( is_svg_blob && missing_features.has( "_SVGBlob" ) ) { 201 | return true; 202 | } 203 | } 204 | if( 205 | sourceIs( "ImageData" ) && 206 | missing_features.has( "_ImageData" ) 207 | ) { 208 | return true; 209 | } 210 | if( 211 | sourceIs( "SVGImageElement" ) && 212 | missing_features.has( "_SVGImageElement" ) 213 | ) { 214 | return true; 215 | } 216 | if( 217 | Object.keys( options ) 218 | .some( (key) => missing_features.has( key ) ) 219 | ) { 220 | return true; 221 | } 222 | 223 | return false; 224 | 225 | } 226 | 227 | function requiresAsyncTests( source, options ) { 228 | if( async_tests_are_done ) { 229 | return false; 230 | } 231 | // async tests are currently all for different types of source 232 | // so we can currently only check for these types 233 | const sourceIs = buildSourceIs( source ); 234 | const types_requiring_async_tests = [ 235 | "Blob", 236 | "ImageData", 237 | "SVGImageElement" 238 | ]; 239 | return types_requiring_async_tests.some( sourceIs ); 240 | } 241 | 242 | export { 243 | requiresPolyfill, 244 | requiresAsyncTests, 245 | tests_results, 246 | async_tests 247 | }; 248 | -------------------------------------------------------------------------------- /dist/createImageBitmap.js: -------------------------------------------------------------------------------- 1 | (()=>{"use strict";const t=["_imageBitmapOptions","resizeWidth","resizeHeight","resizeQuality","imageOrientation"],e=["_Blob","_ImageData","_SVGBlob","_SVGImageElement"],a=["resizeWidth","resizeHeight","resizeQuality","imageOrientation","premultiplyAlpha","colorSpaceConversion"],n={ResizeQuality:["pixelated","low","medium","high"],ImageOrientation:["none","flipY"]},i={COMMON_HEADER:"Failed to execute 'createImageBitmap': ",INVALID_STATE_IMAGE:"Provided image was in an invalid state.",ARGUMENT_COUNT_1:"At least one argument is required.",ARGUMENT_COUNT_N:"%s is not a valid argument count for any overload",CROP_RECT_ZERO:"The crop rect width passed to createImageBitmap must be nonzero",ALLOCATION_FAILED:"The ImageBitmap could not be allocated.",INVALID_SOURCE:"Argument 1 could not be converted to any of: HTMLImageElement, SVGImageElement, HTMLCanvasElement, HTMLVideoElement, ImageBitmap, Blob, CanvasRenderingContext2D, ImageData.",ENUM:"'%v' is not a valid value for enumeration %e",ALLOCATION:"The ImageBitmap couldn't be allocated."},r=["HTMLImageElement","SVGImageElement","HTMLVideoElement","HTMLCanvasElement","OffscreenCanvas","ImageBitmap"],o="http://www.w3.org/2000/svg",s="data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";function h(t){try{return t.slice(0,0),!1}catch(t){return!0}}const c=new Int32Array(1);function l(t){return Number.isFinite(t)?(c[0]=t,c[0]):NaN}function g(t){const e=l(t);if(isNaN(e))throw new TypeError("Invalid long value");return e}function m(t,e){const a=Object.assign(document.createElement("canvas"),{width:t,height:e}).getContext("2d");if(t&&e&&!function(t){if(t.isContextLost)return t.translate(0,0),!t.isContextLost();let e=!1;try{t.fillRect(0,0,1,1),e=0!==t.getImageData(0,0,1,1).data[3]}finally{return t.clearRect(0,0,1,1),e}}(a))throw new DOMException(i.ALLOCATION,"InvalidStateError");return a}function u(t){return e=>{const a=globalThis[e];return a&&t instanceof a}}const d=globalThis.createImageBitmap,w=!!d&&((...t)=>d.call(globalThis,...t));function f(t){const e=new Image;return e.src=t,new Promise(((t,a)=>{e.onload=a=>{N(e),t(e)},e.onerror=t=>{const e=new DOMException(i.INVALID_STATE_IMAGE,"InvalidStateError");a(e)}}))}function p(t){return f(t.getAttribute("href")||t.getAttributeNS("http://www.w3.org/1999/xlink","href"))}const I=i.COMMON_HEADER;function E(t,e,a){const r=n[a],o=t[e];if(void 0!==o&&!r.includes(o)){const t=i.ENUM.replace("%v",o).replace("%e",a);throw new TypeError(t)}}function A(t,...e){let a,[n,r,o,s]=e.map(l);const h=(1===e.length?e[0]:e[4])||{},c="resizeWidth"in h&&g(h.resizeWidth),m="resizeHeight"in h&&g(h.resizeHeight);E(h,"resizeQuality","ResizeQuality"),E(h,"imageOrientation","ImageOrientation");const u=arguments.length;if(!u)throw new TypeError(I+i.ARGUMENT_COUNT_1);if(u>2&&u<5){const t=i.ARGUMENT_COUNT_N.replace("%s",u);throw new TypeError(I+t)}if(u>=5){if(!o||!s)throw new RangeError(I+i.CROP_RECT_ZERO);n=n||0,r=r||0,a={sx:n,sy:r,sw:o,sh:s}}if(0===c||0===m){const t=I+i.ALLOCATION_FAILED;throw new DOMException(t,"InvalidStateError")}return{cropRect:a,...h}}function y(t,...e){const a=e.length;return t.has("_imageBitmapOptions")&&[2,6].includes(a)?e.slice(0,a-1):e}function T(t){const e=u(t);return e("HTMLImageElement")?{width:t.naturalWidth,height:t.naturalHeight}:e("HTMLVideoElement")?{width:t.videoWidth,height:t.videoHeight}:e("SVGImageElement")?{width:NaN,height:NaN}:e("HTMLCanvasElement")||e("OffscreenCanvas")||e("ImageBitmap")||e("ImageData")?t:{width:NaN,height:NaN}}function b(t,e){const a=function(t,e){const{width:a,height:n}=T(t),{resizeWidth:i,resizeHeight:r}=v(t,e),o=e.cropRect||{sx:0,sy:0,sw:a,sh:n},s={dx:0,dy:0,dw:i||Math.abs(o.sw),dh:r||Math.abs(o.sh)},h=function(t,e,{sx:a,sy:n,sw:i,sh:r},{dx:o,dy:s,dw:h,dh:c}){i<0&&(a+=i,i=Math.abs(i)),r<0&&(n+=r,r=Math.abs(r)),h<0&&(o+=h,h=Math.abs(h)),c<0&&(s+=c,c=Math.abs(c));const l=Math.max(a,0),g=Math.min(a+i,t),m=Math.max(n,0),u=Math.min(n+r,e),d=h/i,w=c/r;return{sx:l,sy:m,sw:g-l,sh:u-m,dx:a<0?o-a*d:o,dy:n<0?s-n*w:s,dw:(g-l)*d,dh:(u-m)*w}}(a,n,o,s);return h.resizeQuality=e.resizeQuality,h.flipY="flipY"===e.imageOrientation,h.width=s.dw,h.height=s.dh,h}(t,e),{height:n}=T(t),{sx:i,sy:r,sw:o,sh:s,dx:h,dy:c,dw:l,dh:g,flipY:u,resizeQuality:d}=a,w=m(a.width,a.height),f=w.canvas;return"pixelated"===d?w.imageSmoothingEnabled=!1:d&&(w.imageSmoothingQuality=d),w.drawImage(t,i,r,o,s,h,c,l,g),u&&(w.globalCompositeOperation="copy",w.setTransform(1,0,0,-1,0,f.height),w.drawImage(f,0,0)),f}async function O(t,e){const a=globalThis.URL.createObjectURL(t),n=await f(a);return globalThis.URL.revokeObjectURL(t),b(n,e)}async function M(t,e){const{resizeWidth:a,resizeHeight:n}=v(t,e),{resizeQuality:i,imageOrientation:r}=e,o=e.cropRect||{},s=o.sx||0,h=o.sy||0,c=o.sw||t.width,l=o.sh||t.height,g=c<0?-1*c-s:-s,u=l<0?-1*l-h:-h,d=m(Math.abs(c),Math.abs(l));d.putImageData(t,g,u,s,h,c,l);const w="flipY"===r,f=a||w;let p;a?(p=m(a,n),"pixelated"===i?p.imageSmoothingEnabled=!1:p.imageSmoothingQuality=i):(p=d,d.globalCompositeOperation="copy");const I=d.canvas,E=p.canvas;return w&&p.setTransform(1,0,0,-1,0,E.height),f&&p.drawImage(I,0,0,I.width,I.height,0,0,E.width,E.height),E}function v(t,e){const{width:a,height:n}=T(t);let{resizeWidth:i,resizeHeight:r}=e;if(void 0===i&&void 0!==r){const{width:e,height:a}=T(t);i=Math.ceil(e*(r/a))}if(void 0===r&&void 0!==i){const{width:e,height:a}=T(t);r=Math.ceil(a*(i/e))}return{resizeWidth:i,resizeHeight:r}}function _(){const t=i.COMMON_HEADER+i.INVALID_STATE_IMAGE;throw new DOMException(t,"InvalidStateError")}function N(t){const e=u(t);if(e("HTMLCanvasElement")||e("OffscreenCanvas")||e("ImageBitmap"))0!==t.width&&0!==t.height||_();else if(e("HTMLImageElement")||e("HTMLVideoElement")||e("SVGImageElement")){const{width:e,height:a}=T(t);0!==e&&0!==a||_();let n=!1;try{n=!m(0,0).createPattern(t,"no-repeat")}catch(t){return"maybe"}n&&_()}return!0}let L=!1;const S=function(){if(!w){const a=t.concat(e);return new Set(a)}const a=[B()],n=new Set(a.flat()),i=t.filter((t=>!n.has(t)));return new Set(i)}(),C=async function(){if(!w)return L=!0,S;const t=[x(),H(),D(),R()],a=await Promise.all(t),n=new Set(a.flat());return e.filter((t=>!n.has(t))).forEach((t=>S.add(t))),L=!0,S}();function B(){const t=[],e={};return a.forEach((a=>{Object.defineProperty(e,a,{get(){t.push(a)}})})),w(new ImageData(1,1),e).then((t=>t.close())).catch((()=>{})),t.length&&t.push("_imageBitmapOptions"),t}async function x(){const t=[],e=await fetch(s).then((t=>t.ok&&t.blob()));try{1===(await w(e)).width&&t.push("_Blob")}catch(t){}return t}async function H(){const t=[],e=function(t,e){try{return new ImageData(1,1)}catch(t){return create2dContext(0,0).createImageData(1,1)}}();try{1===(await w(e)).width&&t.push("_ImageData")}catch(t){}return t}async function D(){const t=[],e=new Blob([`\n \n `],{type:"image/svg+xml"});try{1===(await w(e)).width&&t.push("_SVGBlob")}catch(t){}return t}async function R(){const t=[];if(!("SVGImageElement"in globalThis))return t;const e=t=>new Promise((e=>setTimeout(e,t))),a=document.createElementNS(o,"image");a.setAttributeNS("http://www.w3.org/1999/xlink","xlink:href",s);const n=()=>{try{return N(a),!1}catch(t){return!0}};let i=0;do{await e(10)}while(n()&&++i<300);try{await w(a),t.push("_SVGImageElement")}catch(t){}return t}function z(t,e,a){const n=u(a);if(n("Blob")){if(t.has("_Blob"))return!0;if((i=a)&&"image/svg+xml"===i.type&&t.has("_SVGBlob"))return!0}var i;return!(!n("ImageData")||!t.has("_ImageData"))||!(!n("SVGImageElement")||!t.has("_SVGImageElement"))||!!Object.keys(e).some((e=>t.has(e)))}function V(t,e){if(L)return!1;const a=u(t);return["Blob","ImageData","SVGImageElement"].some(a)}const G=t=>Object.getOwnPropertyDescriptor(HTMLCanvasElement.prototype,t);function U(){throw new TypeError("Illegal Constructor")}function Q(t){return t instanceof globalThis.ImageBitmap?t:w?w(t):function(t){return t instanceof HTMLCanvasElement||(t=b(t)),Object.setPrototypeOf(t,U.prototype),t}(t)}U.prototype=Object.create({close(){G("width").set.call(this,0),G("height").set.call(this,0)},get width(){return G("width").get.call(this)},get height(){return G("height").get.call(this)},get[Symbol.toStringTag](){return"ImageBitmap"}}),w||(globalThis.ImageBitmap=U),globalThis.createImageBitmap=async function(t,...e){const a=A(...arguments),n=V(t)?await C:S;if(0===n.size)return w(...arguments);if(w&&!z(n,a,t))return w(...y(n,...arguments));const o=u(t);let s;if(r.some(o)?(N(t),o("SVGImageElement")&&(t=await p(t)),o("HTMLImageElement")&&(t.naturalWidth||t.naturalHeight||a.resizeWidth&&a.resizeHeight||_()),s=b(t,a)):o("Blob")?s=await O(t,a):o("ImageData")&&(h(t.data.buffer)&&_(),s=await M(t,a)),!s)throw new TypeError(i.INVALID_SOURCE);return await Q(s)},globalThis.createImageBitmap._original=w})(); --------------------------------------------------------------------------------