|
@@ -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> => {
|
|
@@ -416,37 +416,42 @@ 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) => {
|
|
|
|
|
- let mimeType: ValueOf<Pick<typeof MIME_TYPES, "png" | "jpg" | "gif">> | null =
|
|
|
|
|
- null;
|
|
|
|
|
|
|
+const getActualMimeTypeFromImage = async (file: Blob | File) => {
|
|
|
|
|
+ let mimeType: ValueOf<
|
|
|
|
|
+ Pick<typeof MIME_TYPES, "png" | "jpg" | "gif" | "webp">
|
|
|
|
|
+ > | 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
|
|
// 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\b/,
|
|
|
// https://en.wikipedia.org/wiki/JPEG#Syntax_and_structure
|
|
// https://en.wikipedia.org/wiki/JPEG#Syntax_and_structure
|
|
|
// jpg is a bit wonky. Checking the first three bytes should be enough,
|
|
// jpg is a bit wonky. Checking the first three bytes should be enough,
|
|
|
// but may yield false positives. (https://stackoverflow.com/a/23360709/927631)
|
|
// 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
|
|
// 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 = (
|
|
export const createFile = (
|
|
|
blob: File | Blob | ArrayBuffer,
|
|
blob: File | Blob | ArrayBuffer,
|
|
|
- mimeType: ValueOf<typeof MIME_TYPES>,
|
|
|
|
|
|
|
+ mimeType: string,
|
|
|
name: string | undefined,
|
|
name: string | undefined,
|
|
|
) => {
|
|
) => {
|
|
|
return new File([blob], name || "", {
|
|
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
|
|
/** 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) {
|
|
|
|
|
- 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
|
|
// 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) {
|
|
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;
|
|
|
};
|
|
};
|
|
|
|
|
|