|
|
|
|
@ -25,7 +25,7 @@ import { restore, restoreLibraryItems } from "./restore";
@@ -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<string> => { |
|
|
|
|
@ -416,37 +416,42 @@ export const getFileHandle = async (
@@ -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<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
|
|
|
|
|
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<typeof MIME_TYPES>, |
|
|
|
|
mimeType: string, |
|
|
|
|
name: string | undefined, |
|
|
|
|
) => { |
|
|
|
|
return new File([blob], name || "", { |
|
|
|
|
@ -454,40 +459,33 @@ export const createFile = (
@@ -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) { |
|
|
|
|
// to prevent double normalization (perf optim)
|
|
|
|
|
if ((file as any)[normalizedFileSymbol]) { |
|
|
|
|
return file; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
if (file?.name?.endsWith(".excalidrawlib")) { |
|
|
|
|
file = createFile( |
|
|
|
|
await blobToArrayBuffer(file), |
|
|
|
|
MIME_TYPES.excalidrawlib, |
|
|
|
|
file.name, |
|
|
|
|
); |
|
|
|
|
file = createFile(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); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
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; |
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|