diff --git a/packages/excalidraw/clipboard.ts b/packages/excalidraw/clipboard.ts index 007a02161b..ae532a6c27 100644 --- a/packages/excalidraw/clipboard.ts +++ b/packages/excalidraw/clipboard.ts @@ -470,13 +470,14 @@ export const parseDataTransferEvent = async ( Array.from(items || []).map( async (item): Promise => { if (item.kind === "file") { - const file = item.getAsFile(); + let file = item.getAsFile(); if (file) { const fileHandle = await getFileHandle(item); + file = await normalizeFile(file); return { type: file.type, kind: "file", - file: await normalizeFile(file), + file, fileHandle, }; } diff --git a/packages/excalidraw/data/blob.ts b/packages/excalidraw/data/blob.ts index 2b6829a938..e8a5401a7a 100644 --- a/packages/excalidraw/data/blob.ts +++ b/packages/excalidraw/data/blob.ts @@ -25,7 +25,7 @@ import { restore, restoreLibraryItems } from "./restore"; import type { AppState, DataURL, LibraryItem } from "../types"; -import type { FileSystemHandle } from "./filesystem"; +import type { FileSystemHandle } from "browser-fs-access"; import type { ImportedLibraryData } from "./types"; const parseFileContents = async (blob: Blob | File): Promise => { @@ -416,37 +416,42 @@ export const getFileHandle = async ( /** * attempts to detect if a buffer is a valid image by checking its leading bytes */ -const getActualMimeTypeFromImage = (buffer: ArrayBuffer) => { - let mimeType: ValueOf> | null = - null; +const getActualMimeTypeFromImage = async (file: Blob | File) => { + let mimeType: ValueOf< + Pick + > | null = null; - const first8Bytes = `${[...new Uint8Array(buffer).slice(0, 8)].join(" ")} `; + const leadingBytes = [ + ...new Uint8Array(await blobToArrayBuffer(file.slice(0, 15))), + ].join(" "); // uint8 leading bytes - const headerBytes = { + const bytes = { // https://en.wikipedia.org/wiki/Portable_Network_Graphics#File_header - png: "137 80 78 71 13 10 26 10 ", + png: /^137 80 78 71 13 10 26 10\b/, // https://en.wikipedia.org/wiki/JPEG#Syntax_and_structure // jpg is a bit wonky. Checking the first three bytes should be enough, // but may yield false positives. (https://stackoverflow.com/a/23360709/927631) - jpg: "255 216 255 ", + jpg: /^255 216 255\b/, // https://en.wikipedia.org/wiki/GIF#Example_GIF_file - gif: "71 73 70 56 57 97 ", + gif: /^71 73 70 56 57 97\b/, + // 4 bytes for RIFF + 4 bytes for chunk size + WEBP identifier + webp: /^82 73 70 70 \d+ \d+ \d+ \d+ 87 69 66 80 86 80 56\b/, }; - if (first8Bytes === headerBytes.png) { - mimeType = MIME_TYPES.png; - } else if (first8Bytes.startsWith(headerBytes.jpg)) { - mimeType = MIME_TYPES.jpg; - } else if (first8Bytes.startsWith(headerBytes.gif)) { - mimeType = MIME_TYPES.gif; + for (const type of Object.keys(bytes) as (keyof typeof bytes)[]) { + if (leadingBytes.match(bytes[type])) { + mimeType = MIME_TYPES[type]; + break; + } } - return mimeType; + + return mimeType || file.type || null; }; export const createFile = ( blob: File | Blob | ArrayBuffer, - mimeType: ValueOf, + mimeType: string, name: string | undefined, ) => { return new File([blob], name || "", { @@ -454,40 +459,33 @@ export const createFile = ( }); }; +const normalizedFileSymbol = Symbol("fileNormalized"); + /** attempts to detect correct mimeType if none is set, or if an image * has an incorrect extension. * Note: doesn't handle missing .excalidraw/.excalidrawlib extension */ export const normalizeFile = async (file: File) => { - if (!file.type) { - if (file?.name?.endsWith(".excalidrawlib")) { - file = createFile( - await blobToArrayBuffer(file), - MIME_TYPES.excalidrawlib, - file.name, - ); - } else if (file?.name?.endsWith(".excalidraw")) { - file = createFile( - await blobToArrayBuffer(file), - MIME_TYPES.excalidraw, - file.name, - ); - } else { - const buffer = await blobToArrayBuffer(file); - const mimeType = getActualMimeTypeFromImage(buffer); - if (mimeType) { - file = createFile(buffer, mimeType, file.name); - } - } + // to prevent double normalization (perf optim) + if ((file as any)[normalizedFileSymbol]) { + return file; + } + + if (file?.name?.endsWith(".excalidrawlib")) { + file = createFile(file, MIME_TYPES.excalidrawlib, file.name); + } else if (file?.name?.endsWith(".excalidraw")) { + file = createFile(file, MIME_TYPES.excalidraw, file.name); + } else if (!file.type || file.type?.startsWith("image/")) { // when the file is an image, make sure the extension corresponds to the - // actual mimeType (this is an edge case, but happens sometime) - } else if (isSupportedImageFile(file)) { - const buffer = await blobToArrayBuffer(file); - const mimeType = getActualMimeTypeFromImage(buffer); + // actual mimeType (this is an edge case, but happens - especially + // with AI generated images) + const mimeType = await getActualMimeTypeFromImage(file); if (mimeType && mimeType !== file.type) { - file = createFile(buffer, mimeType, file.name); + file = createFile(file, mimeType, file.name); } } + (file as any)[normalizedFileSymbol] = true; + return file; }; diff --git a/packages/excalidraw/data/filesystem.ts b/packages/excalidraw/data/filesystem.ts index 0f4ae745f9..44474a6f61 100644 --- a/packages/excalidraw/data/filesystem.ts +++ b/packages/excalidraw/data/filesystem.ts @@ -8,13 +8,15 @@ import { EVENT, MIME_TYPES, debounce } from "@excalidraw/common"; import { AbortError } from "../errors"; +import { normalizeFile } from "./blob"; + import type { FileSystemHandle } from "browser-fs-access"; type FILE_EXTENSION = Exclude; const INPUT_CHANGE_INTERVAL_MS = 500; -export const fileOpen = (opts: { +export const fileOpen = async (opts: { extensions?: FILE_EXTENSION[]; description: string; multiple?: M; @@ -35,7 +37,7 @@ export const fileOpen = (opts: { return acc.concat(`.${ext}`); }, [] as string[]); - return _fileOpen({ + const files = await _fileOpen({ description: opts.description, extensions, mimeTypes, @@ -74,7 +76,14 @@ export const fileOpen = (opts: { } }; }, - }) as Promise; + }); + + if (Array.isArray(files)) { + return (await Promise.all( + files.map((file) => normalizeFile(file)), + )) as RetType; + } + return (await normalizeFile(files)) as RetType; }; export const fileSave = ( diff --git a/packages/excalidraw/data/json.ts b/packages/excalidraw/data/json.ts index 52cbf99581..b8fb0f62cc 100644 --- a/packages/excalidraw/data/json.ts +++ b/packages/excalidraw/data/json.ts @@ -15,7 +15,7 @@ import type { ExcalidrawElement } from "@excalidraw/element/types"; import { cleanAppStateForExport, clearAppStateForDatabase } from "../appState"; -import { isImageFileHandle, loadFromBlob, normalizeFile } from "./blob"; +import { isImageFileHandle, loadFromBlob } from "./blob"; import { fileOpen, fileSave } from "./filesystem"; import type { AppState, BinaryFiles, LibraryItems } from "../types"; @@ -108,12 +108,7 @@ export const loadFromJSON = async ( // gets resolved. Else, iOS users cannot open `.excalidraw` files. // extensions: ["json", "excalidraw", "png", "svg"], }); - return loadFromBlob( - await normalizeFile(file), - localAppState, - localElements, - file.handle, - ); + return loadFromBlob(file, localAppState, localElements, file.handle); }; export const isValidExcalidrawData = (data?: {