mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
feat: make file handling more robust (#5057)
This commit is contained in:
parent
0d70690ec8
commit
d2e687ed0a
11 changed files with 328 additions and 152 deletions
210
src/data/blob.ts
210
src/data/blob.ts
|
@ -8,7 +8,7 @@ import { t } from "../i18n";
|
|||
import { calculateScrollCenter } from "../scene";
|
||||
import { AppState, DataURL, LibraryItem } from "../types";
|
||||
import { bytesToHexString } from "../utils";
|
||||
import { FileSystemHandle } from "./filesystem";
|
||||
import { FileSystemHandle, nativeFileSystemSupported } from "./filesystem";
|
||||
import { isValidExcalidrawData, isValidLibrary } from "./json";
|
||||
import { restore, restoreLibraryItems } from "./restore";
|
||||
import { ImportedLibraryData } from "./types";
|
||||
|
@ -123,40 +123,72 @@ export const isSupportedImageFile = (
|
|||
);
|
||||
};
|
||||
|
||||
export const loadSceneOrLibraryFromBlob = async (
|
||||
blob: Blob | File,
|
||||
/** @see restore.localAppState */
|
||||
localAppState: AppState | null,
|
||||
localElements: readonly ExcalidrawElement[] | null,
|
||||
/** FileSystemHandle. Defaults to `blob.handle` if defined, otherwise null. */
|
||||
fileHandle?: FileSystemHandle | null,
|
||||
) => {
|
||||
const contents = await parseFileContents(blob);
|
||||
try {
|
||||
const data = JSON.parse(contents);
|
||||
if (isValidExcalidrawData(data)) {
|
||||
return {
|
||||
type: MIME_TYPES.excalidraw,
|
||||
data: restore(
|
||||
{
|
||||
elements: clearElementsForExport(data.elements || []),
|
||||
appState: {
|
||||
theme: localAppState?.theme,
|
||||
fileHandle: fileHandle || blob.handle || null,
|
||||
...cleanAppStateForExport(data.appState || {}),
|
||||
...(localAppState
|
||||
? calculateScrollCenter(
|
||||
data.elements || [],
|
||||
localAppState,
|
||||
null,
|
||||
)
|
||||
: {}),
|
||||
},
|
||||
files: data.files,
|
||||
},
|
||||
localAppState,
|
||||
localElements,
|
||||
),
|
||||
};
|
||||
} else if (isValidLibrary(data)) {
|
||||
return {
|
||||
type: MIME_TYPES.excalidrawlib,
|
||||
data,
|
||||
};
|
||||
}
|
||||
throw new Error(t("alerts.couldNotLoadInvalidFile"));
|
||||
} catch (error: any) {
|
||||
console.error(error.message);
|
||||
throw new Error(t("alerts.couldNotLoadInvalidFile"));
|
||||
}
|
||||
};
|
||||
|
||||
export const loadFromBlob = async (
|
||||
blob: Blob,
|
||||
/** @see restore.localAppState */
|
||||
localAppState: AppState | null,
|
||||
localElements: readonly ExcalidrawElement[] | null,
|
||||
/** FileSystemHandle. Defaults to `blob.handle` if defined, otherwise null. */
|
||||
fileHandle?: FileSystemHandle | null,
|
||||
) => {
|
||||
const contents = await parseFileContents(blob);
|
||||
try {
|
||||
const data = JSON.parse(contents);
|
||||
if (!isValidExcalidrawData(data)) {
|
||||
throw new Error(t("alerts.couldNotLoadInvalidFile"));
|
||||
}
|
||||
const result = restore(
|
||||
{
|
||||
elements: clearElementsForExport(data.elements || []),
|
||||
appState: {
|
||||
theme: localAppState?.theme,
|
||||
fileHandle: blob.handle || null,
|
||||
...cleanAppStateForExport(data.appState || {}),
|
||||
...(localAppState
|
||||
? calculateScrollCenter(data.elements || [], localAppState, null)
|
||||
: {}),
|
||||
},
|
||||
files: data.files,
|
||||
},
|
||||
localAppState,
|
||||
localElements,
|
||||
);
|
||||
|
||||
return result;
|
||||
} catch (error: any) {
|
||||
console.error(error.message);
|
||||
const ret = await loadSceneOrLibraryFromBlob(
|
||||
blob,
|
||||
localAppState,
|
||||
localElements,
|
||||
fileHandle,
|
||||
);
|
||||
if (ret.type !== MIME_TYPES.excalidraw) {
|
||||
throw new Error(t("alerts.couldNotLoadInvalidFile"));
|
||||
}
|
||||
return ret.data;
|
||||
};
|
||||
|
||||
export const loadLibraryFromBlob = async (
|
||||
|
@ -200,7 +232,7 @@ export const generateIdFromFile = async (file: File): Promise<FileId> => {
|
|||
try {
|
||||
const hashBuffer = await window.crypto.subtle.digest(
|
||||
"SHA-1",
|
||||
await file.arrayBuffer(),
|
||||
await blobToArrayBuffer(file),
|
||||
);
|
||||
return bytesToHexString(new Uint8Array(hashBuffer)) as FileId;
|
||||
} catch (error: any) {
|
||||
|
@ -289,3 +321,125 @@ export const SVGStringToFile = (SVGString: string, filename: string = "") => {
|
|||
type: MIME_TYPES.svg,
|
||||
}) as File & { type: typeof MIME_TYPES.svg };
|
||||
};
|
||||
|
||||
export const getFileFromEvent = async (
|
||||
event: React.DragEvent<HTMLDivElement>,
|
||||
) => {
|
||||
const file = event.dataTransfer.files.item(0);
|
||||
const fileHandle = await getFileHandle(event);
|
||||
|
||||
return { file: file ? await normalizeFile(file) : null, fileHandle };
|
||||
};
|
||||
|
||||
export const getFileHandle = async (
|
||||
event: React.DragEvent<HTMLDivElement>,
|
||||
): Promise<FileSystemHandle | null> => {
|
||||
if (nativeFileSystemSupported) {
|
||||
try {
|
||||
const item = event.dataTransfer.items[0];
|
||||
const handle: FileSystemHandle | null =
|
||||
(await (item as any).getAsFileSystemHandle()) || null;
|
||||
|
||||
return handle;
|
||||
} catch (error: any) {
|
||||
console.warn(error.name, error.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* attemps to detect if a buffer is a valid image by checking its leading bytes
|
||||
*/
|
||||
const getActualMimeTypeFromImage = (buffer: ArrayBuffer) => {
|
||||
let mimeType: ValueOf<Pick<typeof MIME_TYPES, "png" | "jpg" | "gif">> | null =
|
||||
null;
|
||||
|
||||
const first8Bytes = `${[...new Uint8Array(buffer).slice(0, 8)].join(" ")} `;
|
||||
|
||||
// uint8 leading bytes
|
||||
const headerBytes = {
|
||||
// https://en.wikipedia.org/wiki/Portable_Network_Graphics#File_header
|
||||
png: "137 80 78 71 13 10 26 10 ",
|
||||
// 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 ",
|
||||
// https://en.wikipedia.org/wiki/GIF#Example_GIF_file
|
||||
gif: "71 73 70 56 57 97 ",
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
return mimeType;
|
||||
};
|
||||
|
||||
export const createFile = (
|
||||
blob: File | Blob | ArrayBuffer,
|
||||
mimeType: ValueOf<typeof MIME_TYPES>,
|
||||
name: string | undefined,
|
||||
) => {
|
||||
return new File([blob], name || "", {
|
||||
type: mimeType,
|
||||
});
|
||||
};
|
||||
|
||||
/** attemps 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);
|
||||
}
|
||||
}
|
||||
// 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);
|
||||
if (mimeType && mimeType !== file.type) {
|
||||
file = createFile(buffer, mimeType, file.name);
|
||||
}
|
||||
}
|
||||
|
||||
return file;
|
||||
};
|
||||
|
||||
export const blobToArrayBuffer = (blob: Blob): Promise<ArrayBuffer> => {
|
||||
if ("arrayBuffer" in blob) {
|
||||
return blob.arrayBuffer();
|
||||
}
|
||||
// Safari
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (event) => {
|
||||
if (!event.target?.result) {
|
||||
return reject(new Error("Couldn't convert blob to ArrayBuffer"));
|
||||
}
|
||||
resolve(event.target.result as ArrayBuffer);
|
||||
};
|
||||
reader.readAsArrayBuffer(blob);
|
||||
});
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue