From 0a48aea7bb042ceaebf692e04cbdd0c97074d709 Mon Sep 17 00:00:00 2001 From: alex Date: Tue, 12 Mar 2024 16:51:29 +0000 Subject: [PATCH] fixup file helpers (#3130) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We had a couple regressions in #3110: first a missing `await` was causing fonts not to get properly embedded in exports. second, some `readAsText` calls were replaced with `readAsDataURL` calls. ### Change Type - [x] `sdk` — Changes the tldraw SDK - [ ] `dotcom` — Changes the tldraw.com web app - [ ] `docs` — Changes to the documentation, examples, or templates. - [ ] `vs code` — Changes to the vscode plugin - [ ] `internal` — Does not affect user-facing stuff - [x] `bugfix` — Bug fix - [ ] `feature` — New feature - [ ] `improvement` — Improving existing features - [ ] `chore` — Updating dependencies, other boring stuff - [ ] `galaxy brain` — Architectural changes - [ ] `tests` — Changes to any test code - [ ] `tools` — Changes to infrastructure, CI, internal scripts, debugging tools, etc. - [ ] `dunno` — I don't know --- .../src/lib/defaultExternalContentHandlers.ts | 3 +- .../src/lib/shapes/image/ImageShapeUtil.tsx | 2 +- .../lib/shapes/shared/defaultStyleDefs.tsx | 6 +- .../src/lib/ui/hooks/useClipboardEvents.ts | 21 ++- .../tldraw/src/lib/utils/export/export.ts | 2 +- packages/tldraw/src/lib/utils/tldr/file.ts | 2 +- packages/utils/api-report.md | 8 +- packages/utils/api/api.json | 178 ++++++++++++------ packages/utils/src/lib/file.ts | 33 +++- packages/utils/src/lib/media.ts | 9 - 10 files changed, 171 insertions(+), 93 deletions(-) diff --git a/packages/tldraw/src/lib/defaultExternalContentHandlers.ts b/packages/tldraw/src/lib/defaultExternalContentHandlers.ts index 8fbb634fc..3d68610aa 100644 --- a/packages/tldraw/src/lib/defaultExternalContentHandlers.ts +++ b/packages/tldraw/src/lib/defaultExternalContentHandlers.ts @@ -1,6 +1,7 @@ import { AssetRecordType, Editor, + FileHelpers, MediaHelpers, TLAsset, TLAssetId, @@ -96,7 +97,7 @@ export function registerDefaultExternalContentHandlers( typeName: 'asset', props: { name, - src: await MediaHelpers.blobToDataUrl(file), + src: await FileHelpers.blobToDataUrl(file), w: size.w, h: size.h, mimeType: file.type, diff --git a/packages/tldraw/src/lib/shapes/image/ImageShapeUtil.tsx b/packages/tldraw/src/lib/shapes/image/ImageShapeUtil.tsx index c521124e3..18003f450 100644 --- a/packages/tldraw/src/lib/shapes/image/ImageShapeUtil.tsx +++ b/packages/tldraw/src/lib/shapes/image/ImageShapeUtil.tsx @@ -20,7 +20,7 @@ import { usePrefersReducedMotion } from '../shared/usePrefersReducedMotion' async function getDataURIFromURL(url: string): Promise { const response = await fetch(url) const blob = await response.blob() - return FileHelpers.fileToBase64(blob) + return FileHelpers.blobToDataUrl(blob) } /** @public */ diff --git a/packages/tldraw/src/lib/shapes/shared/defaultStyleDefs.tsx b/packages/tldraw/src/lib/shapes/shared/defaultStyleDefs.tsx index 8f114af66..3f8e2f831 100644 --- a/packages/tldraw/src/lib/shapes/shared/defaultStyleDefs.tsx +++ b/packages/tldraw/src/lib/shapes/shared/defaultStyleDefs.tsx @@ -23,12 +23,12 @@ export function getFontDefForExport(fontStyle: TLDefaultFontStyle): SvgExportDef const font = findFont(fontStyle) if (!font) return null - const url = (font as any).$$_url - const fontFaceRule = (font as any).$$_fontface + const url: string = (font as any).$$_url + const fontFaceRule: string = (font as any).$$_fontface if (!url || !fontFaceRule) return null const fontFile = await (await fetch(url)).blob() - const base64FontFile = FileHelpers.fileToBase64(fontFile) + const base64FontFile = await FileHelpers.blobToDataUrl(fontFile) const newFontFaceRule = fontFaceRule.replace(url, base64FontFile) const style = document.createElementNS('http://www.w3.org/2000/svg', 'style') diff --git a/packages/tldraw/src/lib/ui/hooks/useClipboardEvents.ts b/packages/tldraw/src/lib/ui/hooks/useClipboardEvents.ts index 7b51510be..f57dbd81f 100644 --- a/packages/tldraw/src/lib/ui/hooks/useClipboardEvents.ts +++ b/packages/tldraw/src/lib/ui/hooks/useClipboardEvents.ts @@ -250,27 +250,30 @@ const handlePasteFromClipboardApi = async ( if (item.types.includes('text/html')) { things.push({ type: 'html', - source: new Promise((r) => - item.getType('text/html').then((blob) => FileHelpers.fileToBase64(blob).then(r)) - ), + source: (async () => { + const blob = await item.getType('text/html') + return await FileHelpers.blobToText(blob) + })(), }) } if (item.types.includes('text/uri-list')) { things.push({ type: 'url', - source: new Promise((r) => - item.getType('text/uri-list').then((blob) => FileHelpers.fileToBase64(blob).then(r)) - ), + source: (async () => { + const blob = await item.getType('text/uri-list') + return await FileHelpers.blobToText(blob) + })(), }) } if (item.types.includes('text/plain')) { things.push({ type: 'text', - source: new Promise((r) => - item.getType('text/plain').then((blob) => FileHelpers.fileToBase64(blob).then(r)) - ), + source: (async () => { + const blob = await item.getType('text/plain') + return await FileHelpers.blobToText(blob) + })(), }) } } diff --git a/packages/tldraw/src/lib/utils/export/export.ts b/packages/tldraw/src/lib/utils/export/export.ts index c16d580ad..263f478d0 100644 --- a/packages/tldraw/src/lib/utils/export/export.ts +++ b/packages/tldraw/src/lib/utils/export/export.ts @@ -106,7 +106,7 @@ export async function getSvgAsString(svg: SVGElement) { if (src) { if (!src.startsWith('data:')) { const blob = await (await fetch(src)).blob() - const base64 = await FileHelpers.fileToBase64(blob) + const base64 = await FileHelpers.blobToDataUrl(blob) img.setAttribute('xlink:href', base64) } } diff --git a/packages/tldraw/src/lib/utils/tldr/file.ts b/packages/tldraw/src/lib/utils/tldr/file.ts index 5632c7379..e7c933316 100644 --- a/packages/tldraw/src/lib/utils/tldr/file.ts +++ b/packages/tldraw/src/lib/utils/tldr/file.ts @@ -168,7 +168,7 @@ export async function serializeTldrawJson(store: TLStore): Promise { let assetSrcToSave try { // try to save the asset as a base64 string - assetSrcToSave = await FileHelpers.fileToBase64( + assetSrcToSave = await FileHelpers.blobToDataUrl( await (await fetch(record.props.src)).blob() ) } catch { diff --git a/packages/utils/api-report.md b/packages/utils/api-report.md index ab0a93439..ae37fa6c1 100644 --- a/packages/utils/api-report.md +++ b/packages/utils/api-report.md @@ -62,9 +62,10 @@ export type Expand = T extends infer O ? { // @public export class FileHelpers { - // @internal (undocumented) - static base64ToFile(dataURL: string): Promise; - static fileToBase64(file: Blob): Promise; + static blobToDataUrl(file: Blob): Promise; + static blobToText(file: Blob): Promise; + // (undocumented) + static dataUrlToArrayBuffer(dataURL: string): Promise; } // @internal @@ -177,7 +178,6 @@ export function mapObjectMapValues( // @public export class MediaHelpers { - static blobToDataUrl(blob: Blob): Promise; static getImageSize(blob: Blob): Promise<{ w: number; h: number; diff --git a/packages/utils/api/api.json b/packages/utils/api/api.json index bbb72575d..1f3e89e29 100644 --- a/packages/utils/api/api.json +++ b/packages/utils/api/api.json @@ -519,12 +519,12 @@ "members": [ { "kind": "Method", - "canonicalReference": "@tldraw/utils!FileHelpers.fileToBase64:member(1)", - "docComment": "/**\n * Convert a file to base64.\n *\n * @param value - The file as a blob.\n *\n * @example\n * ```ts\n * const A = fileToBase64('./test.png')\n * ```\n *\n * @public\n */\n", + "canonicalReference": "@tldraw/utils!FileHelpers.blobToDataUrl:member(1)", + "docComment": "/**\n * Convert a file to a base64 encoded data url.\n *\n * @param value - The file as a blob.\n *\n * @example\n * ```ts\n * const A = FileHelpers.toDataUrl(myImageFile)\n * ```\n *\n */\n", "excerptTokens": [ { "kind": "Content", - "text": "static fileToBase64(file: " + "text": "static blobToDataUrl(file: " }, { "kind": "Reference", @@ -569,7 +569,123 @@ ], "isOptional": false, "isAbstract": false, - "name": "fileToBase64" + "name": "blobToDataUrl" + }, + { + "kind": "Method", + "canonicalReference": "@tldraw/utils!FileHelpers.blobToText:member(1)", + "docComment": "/**\n * Convert a file to a unicode text string.\n *\n * @param value - The file as a blob.\n *\n * @example\n * ```ts\n * const A = FileHelpers.fileToDataUrl(myTextFile)\n * ```\n *\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "static blobToText(file: " + }, + { + "kind": "Reference", + "text": "Blob", + "canonicalReference": "!Blob:interface" + }, + { + "kind": "Content", + "text": "): " + }, + { + "kind": "Reference", + "text": "Promise", + "canonicalReference": "!Promise:interface" + }, + { + "kind": "Content", + "text": "" + }, + { + "kind": "Content", + "text": ";" + } + ], + "isStatic": true, + "returnTypeTokenRange": { + "startIndex": 3, + "endIndex": 5 + }, + "releaseTag": "Public", + "isProtected": false, + "overloadIndex": 1, + "parameters": [ + { + "parameterName": "file", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isOptional": false + } + ], + "isOptional": false, + "isAbstract": false, + "name": "blobToText" + }, + { + "kind": "Method", + "canonicalReference": "@tldraw/utils!FileHelpers.dataUrlToArrayBuffer:member(1)", + "docComment": "/**\n * @param dataURL - The file as a string.\n *\n * from https://stackoverflow.com/a/53817185\n */\n", + "excerptTokens": [ + { + "kind": "Content", + "text": "static dataUrlToArrayBuffer(dataURL: " + }, + { + "kind": "Content", + "text": "string" + }, + { + "kind": "Content", + "text": "): " + }, + { + "kind": "Reference", + "text": "Promise", + "canonicalReference": "!Promise:interface" + }, + { + "kind": "Content", + "text": "<" + }, + { + "kind": "Reference", + "text": "ArrayBuffer", + "canonicalReference": "!ArrayBuffer:interface" + }, + { + "kind": "Content", + "text": ">" + }, + { + "kind": "Content", + "text": ";" + } + ], + "isStatic": true, + "returnTypeTokenRange": { + "startIndex": 3, + "endIndex": 7 + }, + "releaseTag": "Public", + "isProtected": false, + "overloadIndex": 1, + "parameters": [ + { + "parameterName": "dataURL", + "parameterTypeTokenRange": { + "startIndex": 1, + "endIndex": 2 + }, + "isOptional": false + } + ], + "isOptional": false, + "isAbstract": false, + "name": "dataUrlToArrayBuffer" } ], "implementsTokenRanges": [] @@ -1844,60 +1960,6 @@ "name": "MediaHelpers", "preserveMemberOrder": false, "members": [ - { - "kind": "Method", - "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 blobToDataUrl(blob: " - }, - { - "kind": "Reference", - "text": "Blob", - "canonicalReference": "!Blob:interface" - }, - { - "kind": "Content", - "text": "): " - }, - { - "kind": "Reference", - "text": "Promise", - "canonicalReference": "!Promise:interface" - }, - { - "kind": "Content", - "text": "" - }, - { - "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)", diff --git a/packages/utils/src/lib/file.ts b/packages/utils/src/lib/file.ts index 56d33acde..3bc32fec2 100644 --- a/packages/utils/src/lib/file.ts +++ b/packages/utils/src/lib/file.ts @@ -6,29 +6,27 @@ export class FileHelpers { /** * @param dataURL - The file as a string. - * @internal * * from https://stackoverflow.com/a/53817185 */ - static async base64ToFile(dataURL: string) { + static async dataUrlToArrayBuffer(dataURL: string) { return fetch(dataURL).then(function (result) { return result.arrayBuffer() }) } /** - * Convert a file to base64. + * Convert a file to a base64 encoded data url. * * @example * * ```ts - * const A = fileToBase64('./test.png') + * const A = FileHelpers.toDataUrl(myImageFile) * ``` * * @param value - The file as a blob. - * @public */ - static async fileToBase64(file: Blob): Promise { + static async blobToDataUrl(file: Blob): Promise { return await new Promise((resolve, reject) => { if (file) { const reader = new FileReader() @@ -39,4 +37,27 @@ export class FileHelpers { } }) } + + /** + * Convert a file to a unicode text string. + * + * @example + * + * ```ts + * const A = FileHelpers.fileToDataUrl(myTextFile) + * ``` + * + * @param value - The file as a blob. + */ + static async blobToText(file: Blob): Promise { + return await new Promise((resolve, reject) => { + if (file) { + const reader = new FileReader() + reader.onload = () => resolve(reader.result as string) + reader.onerror = (error) => reject(error) + reader.onabort = (error) => reject(error) + reader.readAsText(file) + } + }) + } } diff --git a/packages/utils/src/lib/media.ts b/packages/utils/src/lib/media.ts index 0f27c50f0..50cf01ad9 100644 --- a/packages/utils/src/lib/media.ts +++ b/packages/utils/src/lib/media.ts @@ -1,4 +1,3 @@ -import { FileHelpers } from './file' import { PngHelpers } from './png' /** @@ -41,14 +40,6 @@ export class MediaHelpers { }) } - /** - * Read a blob into a data url - * @public - */ - static blobToDataUrl(blob: Blob) { - return FileHelpers.fileToBase64(blob) - } - /** * Get the size of a video blob *