faster image processing in default asset handler (#2441)

![Kapture 2024-01-10 at 13 42
06](https://github.com/tldraw/tldraw/assets/1489520/616bcda7-c05b-46f1-b985-3a36bb5c9476)
(gif is with 6x CPU throttling to make the effect more visible)

This is the first of a few diffs I'm working on to make dropping images
onto the canvas feel a lot faster.

There are three main changes here:
1. We operate on `Blob`s and `File`s rather than data urls. This saves a
fair bit on converting to/from base64 all the time. I've updated our
`MediaHelper` APIs to encourage the same in consumers.
2. We only check the max canvas size (slow) if images are above a
certain dimension that we consider "safe" (8k x 8k)
3. Switching from the `downscale` npm library to canvas native
downscaling. that library claims to give better results than the
browser, but hasn't been updated in ~7 years. in modern browsers, we can
opt-in to native high-quality image smoothing to achieve similar results
much faster than with an algorithm implemented in pure JS.

I want to follow this up with a system to show image placeholders whilst
we're waiting for long-running operations like resizing etc but i'm
going to split that out into its own diff as it'll involve some fairly
complex changes to the history management API.

### Change Type

- [x] `major` — Breaking change

[^1]: publishes a `patch` release, for devDependencies use `internal`
[^2]: will not publish a new version

### Test Plan

1. Tested manually, unit tests & end-to-end tests pass
This commit is contained in:
alex 2024-01-10 14:41:18 +00:00 committed by GitHub
parent 7902dc65c3
commit 3c1aee492a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 674 additions and 403 deletions

View file

@ -654,6 +654,52 @@
],
"name": "getFirstFromIterable"
},
{
"kind": "Function",
"canonicalReference": "@tldraw/utils!getHashForBuffer:function(1)",
"docComment": "/**\n * Hash an ArrayBuffer using the FNV-1a algorithm.\n *\n * @public\n */\n",
"excerptTokens": [
{
"kind": "Content",
"text": "export declare function getHashForBuffer(buffer: "
},
{
"kind": "Reference",
"text": "ArrayBuffer",
"canonicalReference": "!ArrayBuffer:interface"
},
{
"kind": "Content",
"text": "): "
},
{
"kind": "Content",
"text": "string"
},
{
"kind": "Content",
"text": ";"
}
],
"fileUrlPath": "packages/utils/src/lib/hash.ts",
"returnTypeTokenRange": {
"startIndex": 3,
"endIndex": 4
},
"releaseTag": "Public",
"overloadIndex": 1,
"parameters": [
{
"parameterName": "buffer",
"parameterTypeTokenRange": {
"startIndex": 1,
"endIndex": 2
},
"isOptional": false
}
],
"name": "getHashForBuffer"
},
{
"kind": "Function",
"canonicalReference": "@tldraw/utils!getHashForObject:function(1)",
@ -1246,16 +1292,71 @@
"members": [
{
"kind": "Method",
"canonicalReference": "@tldraw/utils!MediaHelpers.getImageSizeFromSrc:member(1)",
"docComment": "/**\n * Get the size of an image from its source.\n *\n * @param dataURL - The file as a string.\n *\n * @public\n */\n",
"canonicalReference": "@tldraw/utils!MediaHelpers.blobToDataUrl:member(1)",
"docComment": "/**\n * Read a blob into a data url\n *\n * @public\n */\n",
"excerptTokens": [
{
"kind": "Content",
"text": "static getImageSizeFromSrc(dataURL: "
"text": "static blobToDataUrl(blob: "
},
{
"kind": "Reference",
"text": "Blob",
"canonicalReference": "!Blob:interface"
},
{
"kind": "Content",
"text": "string"
"text": "): "
},
{
"kind": "Reference",
"text": "Promise",
"canonicalReference": "!Promise:interface"
},
{
"kind": "Content",
"text": "<string>"
},
{
"kind": "Content",
"text": ";"
}
],
"isStatic": true,
"returnTypeTokenRange": {
"startIndex": 3,
"endIndex": 5
},
"releaseTag": "Public",
"isProtected": false,
"overloadIndex": 1,
"parameters": [
{
"parameterName": "blob",
"parameterTypeTokenRange": {
"startIndex": 1,
"endIndex": 2
},
"isOptional": false
}
],
"isOptional": false,
"isAbstract": false,
"name": "blobToDataUrl"
},
{
"kind": "Method",
"canonicalReference": "@tldraw/utils!MediaHelpers.getImageSize:member(1)",
"docComment": "/**\n * Get the size of an image blob\n *\n * @param dataURL - A Blob containing the image.\n *\n * @public\n */\n",
"excerptTokens": [
{
"kind": "Content",
"text": "static getImageSize(blob: "
},
{
"kind": "Reference",
"text": "Blob",
"canonicalReference": "!Blob:interface"
},
{
"kind": "Content",
@ -1285,7 +1386,7 @@
"overloadIndex": 1,
"parameters": [
{
"parameterName": "dataURL",
"parameterName": "blob",
"parameterTypeTokenRange": {
"startIndex": 1,
"endIndex": 2
@ -1295,20 +1396,21 @@
],
"isOptional": false,
"isAbstract": false,
"name": "getImageSizeFromSrc"
"name": "getImageSize"
},
{
"kind": "Method",
"canonicalReference": "@tldraw/utils!MediaHelpers.getVideoSizeFromSrc:member(1)",
"docComment": "/**\n * Get the size of a video from its source.\n *\n * @param src - The source of the video.\n *\n * @public\n */\n",
"canonicalReference": "@tldraw/utils!MediaHelpers.getVideoSize:member(1)",
"docComment": "/**\n * Get the size of a video blob\n *\n * @param src - A SharedBlob containing the video\n *\n * @public\n */\n",
"excerptTokens": [
{
"kind": "Content",
"text": "static getVideoSizeFromSrc(src: "
"text": "static getVideoSize(blob: "
},
{
"kind": "Content",
"text": "string"
"kind": "Reference",
"text": "Blob",
"canonicalReference": "!Blob:interface"
},
{
"kind": "Content",
@ -1336,6 +1438,68 @@
"releaseTag": "Public",
"isProtected": false,
"overloadIndex": 1,
"parameters": [
{
"parameterName": "blob",
"parameterTypeTokenRange": {
"startIndex": 1,
"endIndex": 2
},
"isOptional": false
}
],
"isOptional": false,
"isAbstract": false,
"name": "getVideoSize"
},
{
"kind": "Method",
"canonicalReference": "@tldraw/utils!MediaHelpers.loadImage:member(1)",
"docComment": "/**\n * Load an image from a url.\n *\n * @public\n */\n",
"excerptTokens": [
{
"kind": "Content",
"text": "static loadImage(src: "
},
{
"kind": "Content",
"text": "string"
},
{
"kind": "Content",
"text": "): "
},
{
"kind": "Reference",
"text": "Promise",
"canonicalReference": "!Promise:interface"
},
{
"kind": "Content",
"text": "<"
},
{
"kind": "Reference",
"text": "HTMLImageElement",
"canonicalReference": "!HTMLImageElement:interface"
},
{
"kind": "Content",
"text": ">"
},
{
"kind": "Content",
"text": ";"
}
],
"isStatic": true,
"returnTypeTokenRange": {
"startIndex": 3,
"endIndex": 7
},
"releaseTag": "Public",
"isProtected": false,
"overloadIndex": 1,
"parameters": [
{
"parameterName": "src",
@ -1348,7 +1512,161 @@
],
"isOptional": false,
"isAbstract": false,
"name": "getVideoSizeFromSrc"
"name": "loadImage"
},
{
"kind": "Method",
"canonicalReference": "@tldraw/utils!MediaHelpers.loadVideo:member(1)",
"docComment": "/**\n * Load a video from a url.\n *\n * @public\n */\n",
"excerptTokens": [
{
"kind": "Content",
"text": "static loadVideo(src: "
},
{
"kind": "Content",
"text": "string"
},
{
"kind": "Content",
"text": "): "
},
{
"kind": "Reference",
"text": "Promise",
"canonicalReference": "!Promise:interface"
},
{
"kind": "Content",
"text": "<"
},
{
"kind": "Reference",
"text": "HTMLVideoElement",
"canonicalReference": "!HTMLVideoElement:interface"
},
{
"kind": "Content",
"text": ">"
},
{
"kind": "Content",
"text": ";"
}
],
"isStatic": true,
"returnTypeTokenRange": {
"startIndex": 3,
"endIndex": 7
},
"releaseTag": "Public",
"isProtected": false,
"overloadIndex": 1,
"parameters": [
{
"parameterName": "src",
"parameterTypeTokenRange": {
"startIndex": 1,
"endIndex": 2
},
"isOptional": false
}
],
"isOptional": false,
"isAbstract": false,
"name": "loadVideo"
},
{
"kind": "Method",
"canonicalReference": "@tldraw/utils!MediaHelpers.usingObjectURL:member(1)",
"docComment": "",
"excerptTokens": [
{
"kind": "Content",
"text": "static usingObjectURL<T>(blob: "
},
{
"kind": "Reference",
"text": "Blob",
"canonicalReference": "!Blob:interface"
},
{
"kind": "Content",
"text": ", fn: "
},
{
"kind": "Content",
"text": "(url: string) => "
},
{
"kind": "Reference",
"text": "Promise",
"canonicalReference": "!Promise:interface"
},
{
"kind": "Content",
"text": "<T>"
},
{
"kind": "Content",
"text": "): "
},
{
"kind": "Reference",
"text": "Promise",
"canonicalReference": "!Promise:interface"
},
{
"kind": "Content",
"text": "<T>"
},
{
"kind": "Content",
"text": ";"
}
],
"typeParameters": [
{
"typeParameterName": "T",
"constraintTokenRange": {
"startIndex": 0,
"endIndex": 0
},
"defaultTypeTokenRange": {
"startIndex": 0,
"endIndex": 0
}
}
],
"isStatic": true,
"returnTypeTokenRange": {
"startIndex": 7,
"endIndex": 9
},
"releaseTag": "Public",
"isProtected": false,
"overloadIndex": 1,
"parameters": [
{
"parameterName": "blob",
"parameterTypeTokenRange": {
"startIndex": 1,
"endIndex": 2
},
"isOptional": false
},
{
"parameterName": "fn",
"parameterTypeTokenRange": {
"startIndex": 3,
"endIndex": 6
},
"isOptional": false
}
],
"isOptional": false,
"isAbstract": false,
"name": "usingObjectURL"
}
],
"implementsTokenRanges": []