diff --git a/packages/tldraw/api-report.md b/packages/tldraw/api-report.md index 85f1edffb..cf5506b29 100644 --- a/packages/tldraw/api-report.md +++ b/packages/tldraw/api-report.md @@ -673,7 +673,10 @@ export class GeoShapeUtil extends BaseBoxShapeUtil { export function getEmbedInfo(inputUrl: string): TLEmbedResult; // @public -export function getResizedImageDataUrl(dataURLForImage: string, width: number, height: number): Promise; +export function getResizedImageDataUrl(dataURLForImage: string, width: number, height: number, opts?: { + type?: string | undefined; + quality?: number | undefined; +}): Promise; // @public (undocumented) function Group({ children, size, }: { diff --git a/packages/tldraw/api/api.json b/packages/tldraw/api/api.json index 4062caff9..01d727889 100644 --- a/packages/tldraw/api/api.json +++ b/packages/tldraw/api/api.json @@ -7236,7 +7236,7 @@ { "kind": "Function", "canonicalReference": "@tldraw/tldraw!getResizedImageDataUrl:function(1)", - "docComment": "/**\n * Get the size of an image from its source.\n *\n * @param dataURLForImage - The image file as a string.\n *\n * @param width - The desired width.\n *\n * @param height - The desired height.\n *\n * @public\n */\n", + "docComment": "/**\n * Get the size of an image from its source.\n *\n * @param dataURLForImage - The image file as a string.\n *\n * @param width - The desired width.\n *\n * @param height - The desired height.\n *\n * @param opts - Options for the image.\n *\n * @example\n * ```ts\n * const size = await getImageSize('https://example.com/image.jpg')\n * const dataUrl = await getResizedImageDataUrl('https://example.com/image.jpg', size.w, size.h, { type: \"image/jpeg\", quality: 0.92 })\n * ```\n *\n * @public\n */\n", "excerptTokens": [ { "kind": "Content", @@ -7262,6 +7262,14 @@ "kind": "Content", "text": "number" }, + { + "kind": "Content", + "text": ", opts?: " + }, + { + "kind": "Content", + "text": "{\n type?: string | undefined;\n quality?: number | undefined;\n}" + }, { "kind": "Content", "text": "): " @@ -7282,8 +7290,8 @@ ], "fileUrlPath": "packages/tldraw/src/lib/utils/assets.ts", "returnTypeTokenRange": { - "startIndex": 7, - "endIndex": 9 + "startIndex": 9, + "endIndex": 11 }, "releaseTag": "Public", "overloadIndex": 1, @@ -7311,6 +7319,14 @@ "endIndex": 6 }, "isOptional": false + }, + { + "parameterName": "opts", + "parameterTypeTokenRange": { + "startIndex": 7, + "endIndex": 8 + }, + "isOptional": true } ], "name": "getResizedImageDataUrl" diff --git a/packages/tldraw/package.json b/packages/tldraw/package.json index ad6cb1b95..413a5f8ac 100644 --- a/packages/tldraw/package.json +++ b/packages/tldraw/package.json @@ -55,6 +55,7 @@ "@tldraw/editor": "workspace:*", "canvas-size": "^1.2.6", "classnames": "^2.3.2", + "downscale": "^1.0.6", "hotkeys-js": "^3.11.2", "lz-string": "^1.4.4" }, @@ -68,6 +69,7 @@ "@testing-library/react": "^14.0.0", "@types/canvas-size": "^1.2.0", "@types/classnames": "^2.3.1", + "@types/downscale": "^1.0.4", "@types/lz-string": "^1.3.34", "chokidar-cli": "^3.0.0", "jest-canvas-mock": "^2.5.2", diff --git a/packages/tldraw/src/lib/defaultExternalContentHandlers.ts b/packages/tldraw/src/lib/defaultExternalContentHandlers.ts index 285e9a9c1..825d3cda4 100644 --- a/packages/tldraw/src/lib/defaultExternalContentHandlers.ts +++ b/packages/tldraw/src/lib/defaultExternalContentHandlers.ts @@ -91,10 +91,16 @@ export function registerDefaultExternalContentHandlers( if (isFinite(maxImageDimension)) { const resizedSize = containBoxSize(size, { w: maxImageDimension, h: maxImageDimension }) if (size !== resizedSize && (file.type === 'image/jpeg' || file.type === 'image/png')) { - // If we created a new size and the type is an image, rescale the image - dataUrl = await getResizedImageDataUrl(dataUrl, size.w, size.h) + size = resizedSize } - size = resizedSize + } + + // Always rescale the image + if (file.type === 'image/jpeg' || file.type === 'image/png') { + dataUrl = await getResizedImageDataUrl(dataUrl, size.w, size.h, { + type: file.type, + quality: 0.92, + }) } const assetId: TLAssetId = AssetRecordType.createId(getHashForString(dataUrl)) diff --git a/packages/tldraw/src/lib/utils/assets.ts b/packages/tldraw/src/lib/utils/assets.ts index 3c5ef99f6..41a21646c 100644 --- a/packages/tldraw/src/lib/utils/assets.ts +++ b/packages/tldraw/src/lib/utils/assets.ts @@ -1,3 +1,4 @@ +import downscale from 'downscale' import { getBrowserCanvasMaxSize } from '../shapes/shared/getBrowserCanvasMaxSize' import { isAnimated } from './is-gif-animated' @@ -39,60 +40,49 @@ export function containBoxSize( /** * Get the size of an image from its source. * + * @example + * ```ts + * const size = await getImageSize('https://example.com/image.jpg') + * const dataUrl = await getResizedImageDataUrl('https://example.com/image.jpg', size.w, size.h, { type: "image/jpeg", quality: 0.92 }) + * ``` + * * @param dataURLForImage - The image file as a string. * @param width - The desired width. * @param height - The desired height. + * @param opts - Options for the image. * @public */ export async function getResizedImageDataUrl( dataURLForImage: string, width: number, - height: number + height: number, + opts = {} as { type?: string; quality?: number } ): Promise { - return await new Promise((resolve) => { - const img = new Image() - img.onload = async () => { - // Initialize the canvas and it's size - const canvas = document.createElement('canvas') - const ctx = canvas.getContext('2d') + let desiredWidth = width * 2 + let desiredHeight = height * 2 + const { type = 'image/jpeg', quality = 0.92 } = opts - if (!ctx) return + const canvasSizes = await getBrowserCanvasMaxSize() - const canvasSizes = await getBrowserCanvasMaxSize() + const aspectRatio = width / height - let desiredWidth = width * 2 - let desiredHeight = height * 2 - const aspectRatio = img.width / img.height + if (desiredWidth > canvasSizes.maxWidth) { + desiredWidth = canvasSizes.maxWidth + desiredHeight = desiredWidth / aspectRatio + } - if (desiredWidth > canvasSizes.maxWidth) { - desiredWidth = canvasSizes.maxWidth - desiredHeight = desiredWidth / aspectRatio - } + if (desiredHeight > canvasSizes.maxHeight) { + desiredHeight = canvasSizes.maxHeight + desiredWidth = desiredHeight * aspectRatio + } - if (desiredHeight > canvasSizes.maxHeight) { - desiredHeight = canvasSizes.maxHeight - desiredWidth = desiredHeight * aspectRatio - } + if (desiredWidth * desiredHeight > canvasSizes.maxArea) { + const ratio = Math.sqrt(canvasSizes.maxArea / (desiredWidth * desiredHeight)) + desiredWidth *= ratio + desiredHeight *= ratio + } - if (desiredWidth * desiredHeight > canvasSizes.maxArea) { - const ratio = Math.sqrt(canvasSizes.maxArea / (desiredWidth * desiredHeight)) - desiredWidth *= ratio - desiredHeight *= ratio - } - - canvas.width = desiredWidth - canvas.height = desiredHeight - - // Draw image and export to a data-uri - ctx.drawImage(img, 0, 0, canvas.width, canvas.height) - const newDataURL = canvas.toDataURL() - - // Do something with the result, like overwrite original - resolve(newDataURL) - } - img.crossOrigin = 'anonymous' - img.src = dataURLForImage - }) + return await downscale(dataURLForImage, desiredWidth, desiredHeight, { imageType: type, quality }) } /** @public */ diff --git a/public-yarn.lock b/public-yarn.lock index dada964b6..2095a28a4 100644 --- a/public-yarn.lock +++ b/public-yarn.lock @@ -4608,10 +4608,12 @@ __metadata: "@tldraw/editor": "workspace:*" "@types/canvas-size": ^1.2.0 "@types/classnames": ^2.3.1 + "@types/downscale": ^1.0.4 "@types/lz-string": ^1.3.34 canvas-size: ^1.2.6 chokidar-cli: ^3.0.0 classnames: ^2.3.2 + downscale: ^1.0.6 hotkeys-js: ^3.11.2 jest-canvas-mock: ^2.5.2 jest-environment-jsdom: ^28.1.2 @@ -4875,6 +4877,13 @@ __metadata: languageName: node linkType: hard +"@types/downscale@npm:^1.0.4": + version: 1.0.4 + resolution: "@types/downscale@npm:1.0.4" + checksum: b29ca279b616e06896c01795effa5c483e243e85a137462f98eb43dec3598acab3f72b0637225a4087080e0326cf4bde89ba21d0473a9254a7661c5fd29f98d9 + languageName: node + linkType: hard + "@types/estree-jsx@npm:^0.0.1": version: 0.0.1 resolution: "@types/estree-jsx@npm:0.0.1" @@ -7927,6 +7936,13 @@ __metadata: languageName: node linkType: hard +"downscale@npm:^1.0.6": + version: 1.0.6 + resolution: "downscale@npm:1.0.6" + checksum: f69beb8fe7711964b259ef3d3502fd9f5dd0c4472aa279537dc58d69dcff8a619c77b0209db65841aab014930b97e82f92d43558acc1e569407b8fbc63fb04ee + languageName: node + linkType: hard + "duplexify@npm:^3.5.0, duplexify@npm:^3.6.0": version: 3.7.1 resolution: "duplexify@npm:3.7.1"