mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-09-26 10:49:57 +02:00
feat: better file normalization
This commit is contained in:
@@ -470,13 +470,14 @@ export const parseDataTransferEvent = async (
|
|||||||
Array.from(items || []).map(
|
Array.from(items || []).map(
|
||||||
async (item): Promise<ParsedDataTransferItem | null> => {
|
async (item): Promise<ParsedDataTransferItem | null> => {
|
||||||
if (item.kind === "file") {
|
if (item.kind === "file") {
|
||||||
const file = item.getAsFile();
|
let file = item.getAsFile();
|
||||||
if (file) {
|
if (file) {
|
||||||
const fileHandle = await getFileHandle(item);
|
const fileHandle = await getFileHandle(item);
|
||||||
|
file = await normalizeFile(file);
|
||||||
return {
|
return {
|
||||||
type: file.type,
|
type: file.type,
|
||||||
kind: "file",
|
kind: "file",
|
||||||
file: await normalizeFile(file),
|
file,
|
||||||
fileHandle,
|
fileHandle,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@@ -25,7 +25,7 @@ import { restore, restoreLibraryItems } from "./restore";
|
|||||||
|
|
||||||
import type { AppState, DataURL, LibraryItem } from "../types";
|
import type { AppState, DataURL, LibraryItem } from "../types";
|
||||||
|
|
||||||
import type { FileSystemHandle } from "./filesystem";
|
import type { FileSystemHandle } from "browser-fs-access";
|
||||||
import type { ImportedLibraryData } from "./types";
|
import type { ImportedLibraryData } from "./types";
|
||||||
|
|
||||||
const parseFileContents = async (blob: Blob | File): Promise<string> => {
|
const parseFileContents = async (blob: Blob | File): Promise<string> => {
|
||||||
@@ -417,13 +417,14 @@ export const getFileHandle = async (
|
|||||||
* attempts to detect if a buffer is a valid image by checking its leading bytes
|
* attempts to detect if a buffer is a valid image by checking its leading bytes
|
||||||
*/
|
*/
|
||||||
const getActualMimeTypeFromImage = (buffer: ArrayBuffer) => {
|
const getActualMimeTypeFromImage = (buffer: ArrayBuffer) => {
|
||||||
let mimeType: ValueOf<Pick<typeof MIME_TYPES, "png" | "jpg" | "gif">> | null =
|
let mimeType: ValueOf<
|
||||||
null;
|
Pick<typeof MIME_TYPES, "png" | "jpg" | "gif" | "webp">
|
||||||
|
> | null = null;
|
||||||
|
|
||||||
const first8Bytes = `${[...new Uint8Array(buffer).slice(0, 8)].join(" ")} `;
|
const leadingBytes = `${[...new Uint8Array(buffer).slice(0, 15)].join(" ")} `;
|
||||||
|
|
||||||
// uint8 leading bytes
|
// uint8 leading bytes
|
||||||
const headerBytes = {
|
const bytes = {
|
||||||
// https://en.wikipedia.org/wiki/Portable_Network_Graphics#File_header
|
// 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 ",
|
||||||
// https://en.wikipedia.org/wiki/JPEG#Syntax_and_structure
|
// https://en.wikipedia.org/wiki/JPEG#Syntax_and_structure
|
||||||
@@ -432,14 +433,18 @@ const getActualMimeTypeFromImage = (buffer: ArrayBuffer) => {
|
|||||||
jpg: "255 216 255 ",
|
jpg: "255 216 255 ",
|
||||||
// https://en.wikipedia.org/wiki/GIF#Example_GIF_file
|
// https://en.wikipedia.org/wiki/GIF#Example_GIF_file
|
||||||
gif: "71 73 70 56 57 97 ",
|
gif: "71 73 70 56 57 97 ",
|
||||||
|
// 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/,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (first8Bytes === headerBytes.png) {
|
if (leadingBytes === bytes.png) {
|
||||||
mimeType = MIME_TYPES.png;
|
mimeType = MIME_TYPES.png;
|
||||||
} else if (first8Bytes.startsWith(headerBytes.jpg)) {
|
} else if (leadingBytes.startsWith(bytes.jpg)) {
|
||||||
mimeType = MIME_TYPES.jpg;
|
mimeType = MIME_TYPES.jpg;
|
||||||
} else if (first8Bytes.startsWith(headerBytes.gif)) {
|
} else if (leadingBytes.startsWith(bytes.gif)) {
|
||||||
mimeType = MIME_TYPES.gif;
|
mimeType = MIME_TYPES.gif;
|
||||||
|
} else if (bytes.webp.test(leadingBytes)) {
|
||||||
|
mimeType = MIME_TYPES.webp;
|
||||||
}
|
}
|
||||||
return mimeType;
|
return mimeType;
|
||||||
};
|
};
|
||||||
@@ -454,40 +459,33 @@ export const createFile = (
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const normalizedFileSymbol = Symbol("fileNormalized");
|
||||||
|
|
||||||
/** attempts to detect correct mimeType if none is set, or if an image
|
/** attempts to detect correct mimeType if none is set, or if an image
|
||||||
* has an incorrect extension.
|
* has an incorrect extension.
|
||||||
* Note: doesn't handle missing .excalidraw/.excalidrawlib extension */
|
* Note: doesn't handle missing .excalidraw/.excalidrawlib extension */
|
||||||
export const normalizeFile = async (file: File) => {
|
export const normalizeFile = async (file: File) => {
|
||||||
if (!file.type) {
|
// to prevent double normalization (perf optim)
|
||||||
|
if ((file as any)[normalizedFileSymbol]) {
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
|
||||||
if (file?.name?.endsWith(".excalidrawlib")) {
|
if (file?.name?.endsWith(".excalidrawlib")) {
|
||||||
file = createFile(
|
file = createFile(file, MIME_TYPES.excalidrawlib, file.name);
|
||||||
await blobToArrayBuffer(file),
|
|
||||||
MIME_TYPES.excalidrawlib,
|
|
||||||
file.name,
|
|
||||||
);
|
|
||||||
} else if (file?.name?.endsWith(".excalidraw")) {
|
} else if (file?.name?.endsWith(".excalidraw")) {
|
||||||
file = createFile(
|
file = createFile(file, MIME_TYPES.excalidraw, file.name);
|
||||||
await blobToArrayBuffer(file),
|
} else if (!file.type || file.type?.startsWith("image/")) {
|
||||||
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
|
// when the file is an image, make sure the extension corresponds to the
|
||||||
// actual mimeType (this is an edge case, but happens sometime)
|
// actual mimeType (this is an edge case, but happens - especially
|
||||||
} else if (isSupportedImageFile(file)) {
|
// with AI generated images)
|
||||||
const buffer = await blobToArrayBuffer(file);
|
const mimeType = getActualMimeTypeFromImage(await blobToArrayBuffer(file));
|
||||||
const mimeType = getActualMimeTypeFromImage(buffer);
|
|
||||||
if (mimeType && mimeType !== file.type) {
|
if (mimeType && mimeType !== file.type) {
|
||||||
file = createFile(buffer, mimeType, file.name);
|
file = createFile(file, mimeType, file.name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
(file as any)[normalizedFileSymbol] = true;
|
||||||
|
|
||||||
return file;
|
return file;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -8,13 +8,15 @@ import { EVENT, MIME_TYPES, debounce } from "@excalidraw/common";
|
|||||||
|
|
||||||
import { AbortError } from "../errors";
|
import { AbortError } from "../errors";
|
||||||
|
|
||||||
|
import { normalizeFile } from "./blob";
|
||||||
|
|
||||||
import type { FileSystemHandle } from "browser-fs-access";
|
import type { FileSystemHandle } from "browser-fs-access";
|
||||||
|
|
||||||
type FILE_EXTENSION = Exclude<keyof typeof MIME_TYPES, "binary">;
|
type FILE_EXTENSION = Exclude<keyof typeof MIME_TYPES, "binary">;
|
||||||
|
|
||||||
const INPUT_CHANGE_INTERVAL_MS = 500;
|
const INPUT_CHANGE_INTERVAL_MS = 500;
|
||||||
|
|
||||||
export const fileOpen = <M extends boolean | undefined = false>(opts: {
|
export const fileOpen = async <M extends boolean | undefined = false>(opts: {
|
||||||
extensions?: FILE_EXTENSION[];
|
extensions?: FILE_EXTENSION[];
|
||||||
description: string;
|
description: string;
|
||||||
multiple?: M;
|
multiple?: M;
|
||||||
@@ -35,7 +37,7 @@ export const fileOpen = <M extends boolean | undefined = false>(opts: {
|
|||||||
return acc.concat(`.${ext}`);
|
return acc.concat(`.${ext}`);
|
||||||
}, [] as string[]);
|
}, [] as string[]);
|
||||||
|
|
||||||
return _fileOpen({
|
const files = await _fileOpen({
|
||||||
description: opts.description,
|
description: opts.description,
|
||||||
extensions,
|
extensions,
|
||||||
mimeTypes,
|
mimeTypes,
|
||||||
@@ -74,7 +76,14 @@ export const fileOpen = <M extends boolean | undefined = false>(opts: {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
}) as Promise<RetType>;
|
});
|
||||||
|
|
||||||
|
if (Array.isArray(files)) {
|
||||||
|
return (await Promise.all(
|
||||||
|
files.map((file) => normalizeFile(file)),
|
||||||
|
)) as RetType;
|
||||||
|
}
|
||||||
|
return (await normalizeFile(files)) as RetType;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const fileSave = (
|
export const fileSave = (
|
||||||
|
@@ -108,12 +108,7 @@ export const loadFromJSON = async (
|
|||||||
// gets resolved. Else, iOS users cannot open `.excalidraw` files.
|
// gets resolved. Else, iOS users cannot open `.excalidraw` files.
|
||||||
// extensions: ["json", "excalidraw", "png", "svg"],
|
// extensions: ["json", "excalidraw", "png", "svg"],
|
||||||
});
|
});
|
||||||
return loadFromBlob(
|
return loadFromBlob(file, localAppState, localElements, file.handle);
|
||||||
await normalizeFile(file),
|
|
||||||
localAppState,
|
|
||||||
localElements,
|
|
||||||
file.handle,
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isValidExcalidrawData = (data?: {
|
export const isValidExcalidrawData = (data?: {
|
||||||
|
Reference in New Issue
Block a user