fixup file helpers (#3130)

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

<!--  Please select a 'Scope' label ️ -->

- [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

<!--  Please select a 'Type' label ️ -->

- [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
This commit is contained in:
alex 2024-03-12 16:51:29 +00:00 committed by GitHub
parent adebb680e5
commit 0a48aea7bb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 171 additions and 93 deletions

View file

@ -1,6 +1,7 @@
import { import {
AssetRecordType, AssetRecordType,
Editor, Editor,
FileHelpers,
MediaHelpers, MediaHelpers,
TLAsset, TLAsset,
TLAssetId, TLAssetId,
@ -96,7 +97,7 @@ export function registerDefaultExternalContentHandlers(
typeName: 'asset', typeName: 'asset',
props: { props: {
name, name,
src: await MediaHelpers.blobToDataUrl(file), src: await FileHelpers.blobToDataUrl(file),
w: size.w, w: size.w,
h: size.h, h: size.h,
mimeType: file.type, mimeType: file.type,

View file

@ -20,7 +20,7 @@ import { usePrefersReducedMotion } from '../shared/usePrefersReducedMotion'
async function getDataURIFromURL(url: string): Promise<string> { async function getDataURIFromURL(url: string): Promise<string> {
const response = await fetch(url) const response = await fetch(url)
const blob = await response.blob() const blob = await response.blob()
return FileHelpers.fileToBase64(blob) return FileHelpers.blobToDataUrl(blob)
} }
/** @public */ /** @public */

View file

@ -23,12 +23,12 @@ export function getFontDefForExport(fontStyle: TLDefaultFontStyle): SvgExportDef
const font = findFont(fontStyle) const font = findFont(fontStyle)
if (!font) return null if (!font) return null
const url = (font as any).$$_url const url: string = (font as any).$$_url
const fontFaceRule = (font as any).$$_fontface const fontFaceRule: string = (font as any).$$_fontface
if (!url || !fontFaceRule) return null if (!url || !fontFaceRule) return null
const fontFile = await (await fetch(url)).blob() 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 newFontFaceRule = fontFaceRule.replace(url, base64FontFile)
const style = document.createElementNS('http://www.w3.org/2000/svg', 'style') const style = document.createElementNS('http://www.w3.org/2000/svg', 'style')

View file

@ -250,27 +250,30 @@ const handlePasteFromClipboardApi = async (
if (item.types.includes('text/html')) { if (item.types.includes('text/html')) {
things.push({ things.push({
type: 'html', type: 'html',
source: new Promise<string>((r) => source: (async () => {
item.getType('text/html').then((blob) => FileHelpers.fileToBase64(blob).then(r)) const blob = await item.getType('text/html')
), return await FileHelpers.blobToText(blob)
})(),
}) })
} }
if (item.types.includes('text/uri-list')) { if (item.types.includes('text/uri-list')) {
things.push({ things.push({
type: 'url', type: 'url',
source: new Promise<string>((r) => source: (async () => {
item.getType('text/uri-list').then((blob) => FileHelpers.fileToBase64(blob).then(r)) const blob = await item.getType('text/uri-list')
), return await FileHelpers.blobToText(blob)
})(),
}) })
} }
if (item.types.includes('text/plain')) { if (item.types.includes('text/plain')) {
things.push({ things.push({
type: 'text', type: 'text',
source: new Promise<string>((r) => source: (async () => {
item.getType('text/plain').then((blob) => FileHelpers.fileToBase64(blob).then(r)) const blob = await item.getType('text/plain')
), return await FileHelpers.blobToText(blob)
})(),
}) })
} }
} }

View file

@ -106,7 +106,7 @@ export async function getSvgAsString(svg: SVGElement) {
if (src) { if (src) {
if (!src.startsWith('data:')) { if (!src.startsWith('data:')) {
const blob = await (await fetch(src)).blob() const blob = await (await fetch(src)).blob()
const base64 = await FileHelpers.fileToBase64(blob) const base64 = await FileHelpers.blobToDataUrl(blob)
img.setAttribute('xlink:href', base64) img.setAttribute('xlink:href', base64)
} }
} }

View file

@ -168,7 +168,7 @@ export async function serializeTldrawJson(store: TLStore): Promise<string> {
let assetSrcToSave let assetSrcToSave
try { try {
// try to save the asset as a base64 string // try to save the asset as a base64 string
assetSrcToSave = await FileHelpers.fileToBase64( assetSrcToSave = await FileHelpers.blobToDataUrl(
await (await fetch(record.props.src)).blob() await (await fetch(record.props.src)).blob()
) )
} catch { } catch {

View file

@ -62,9 +62,10 @@ export type Expand<T> = T extends infer O ? {
// @public // @public
export class FileHelpers { export class FileHelpers {
// @internal (undocumented) static blobToDataUrl(file: Blob): Promise<string>;
static base64ToFile(dataURL: string): Promise<ArrayBuffer>; static blobToText(file: Blob): Promise<string>;
static fileToBase64(file: Blob): Promise<string>; // (undocumented)
static dataUrlToArrayBuffer(dataURL: string): Promise<ArrayBuffer>;
} }
// @internal // @internal
@ -177,7 +178,6 @@ export function mapObjectMapValues<Key extends string, ValueBefore, ValueAfter>(
// @public // @public
export class MediaHelpers { export class MediaHelpers {
static blobToDataUrl(blob: Blob): Promise<string>;
static getImageSize(blob: Blob): Promise<{ static getImageSize(blob: Blob): Promise<{
w: number; w: number;
h: number; h: number;

View file

@ -519,12 +519,12 @@
"members": [ "members": [
{ {
"kind": "Method", "kind": "Method",
"canonicalReference": "@tldraw/utils!FileHelpers.fileToBase64:member(1)", "canonicalReference": "@tldraw/utils!FileHelpers.blobToDataUrl: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", "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": [ "excerptTokens": [
{ {
"kind": "Content", "kind": "Content",
"text": "static fileToBase64(file: " "text": "static blobToDataUrl(file: "
}, },
{ {
"kind": "Reference", "kind": "Reference",
@ -569,7 +569,123 @@
], ],
"isOptional": false, "isOptional": false,
"isAbstract": 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": "<string>"
},
{
"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": [] "implementsTokenRanges": []
@ -1844,60 +1960,6 @@
"name": "MediaHelpers", "name": "MediaHelpers",
"preserveMemberOrder": false, "preserveMemberOrder": false,
"members": [ "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": "<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", "kind": "Method",
"canonicalReference": "@tldraw/utils!MediaHelpers.getImageSize:member(1)", "canonicalReference": "@tldraw/utils!MediaHelpers.getImageSize:member(1)",

View file

@ -6,29 +6,27 @@
export class FileHelpers { export class FileHelpers {
/** /**
* @param dataURL - The file as a string. * @param dataURL - The file as a string.
* @internal
* *
* from https://stackoverflow.com/a/53817185 * from https://stackoverflow.com/a/53817185
*/ */
static async base64ToFile(dataURL: string) { static async dataUrlToArrayBuffer(dataURL: string) {
return fetch(dataURL).then(function (result) { return fetch(dataURL).then(function (result) {
return result.arrayBuffer() return result.arrayBuffer()
}) })
} }
/** /**
* Convert a file to base64. * Convert a file to a base64 encoded data url.
* *
* @example * @example
* *
* ```ts * ```ts
* const A = fileToBase64('./test.png') * const A = FileHelpers.toDataUrl(myImageFile)
* ``` * ```
* *
* @param value - The file as a blob. * @param value - The file as a blob.
* @public
*/ */
static async fileToBase64(file: Blob): Promise<string> { static async blobToDataUrl(file: Blob): Promise<string> {
return await new Promise((resolve, reject) => { return await new Promise((resolve, reject) => {
if (file) { if (file) {
const reader = new FileReader() 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<string> {
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)
}
})
}
} }

View file

@ -1,4 +1,3 @@
import { FileHelpers } from './file'
import { PngHelpers } from './png' 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 * Get the size of a video blob
* *