From 685abac81a3c488da49a951a91e77a019ddfbfe5 Mon Sep 17 00:00:00 2001 From: David Laban Date: Thu, 15 Jul 2021 09:54:26 -0400 Subject: [PATCH] feat: resave to png/svg with metadata if you loaded your scene from a png/svg file (#3645) Co-authored-by: dwelle --- src/actions/actionExport.tsx | 8 +++++++- src/components/App.tsx | 11 +++++++++++ src/components/LayerUI.tsx | 11 ++++++++++- src/data/blob.ts | 22 ++++++++++++++++++++- src/data/index.ts | 29 +++++++++++++++++---------- src/data/json.ts | 4 ++-- src/data/resave.ts | 38 ++++++++++++++++++++++++++++++++++++ 7 files changed, 108 insertions(+), 15 deletions(-) create mode 100644 src/data/resave.ts diff --git a/src/actions/actionExport.tsx b/src/actions/actionExport.tsx index 05d9bc7d..e3208615 100644 --- a/src/actions/actionExport.tsx +++ b/src/actions/actionExport.tsx @@ -7,6 +7,7 @@ import "../components/ToolIcon.scss"; import { Tooltip } from "../components/Tooltip"; import { DarkModeToggle, Appearence } from "../components/DarkModeToggle"; import { loadFromJSON, saveAsJSON } from "../data"; +import { resaveAsImageWithScene } from "../data/resave"; import { t } from "../i18n"; import { useIsMobile } from "../components/App"; import { KEYS } from "../keys"; @@ -18,6 +19,7 @@ import { DEFAULT_EXPORT_PADDING, EXPORT_SCALES } from "../constants"; import { getSelectedElements, isSomeElementSelected } from "../scene"; import { getNonDeletedElements } from "../element"; import { ActiveFile } from "../components/ActiveFile"; +import { isImageFileHandle } from "../data/blob"; export const actionChangeProjectName = register({ name: "changeProjectName", @@ -128,8 +130,12 @@ export const actionSaveToActiveFile = register({ name: "saveToActiveFile", perform: async (elements, appState, value) => { const fileHandleExists = !!appState.fileHandle; + try { - const { fileHandle } = await saveAsJSON(elements, appState); + const { fileHandle } = isImageFileHandle(appState.fileHandle) + ? await resaveAsImageWithScene(elements, appState) + : await saveAsJSON(elements, appState); + return { commitToHistory: false, appState: { diff --git a/src/components/App.tsx b/src/components/App.tsx index 105489da..5b077f89 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -3827,6 +3827,17 @@ class App extends React.Component { try { const file = event.dataTransfer.files[0]; if (file?.type === "image/png" || file?.type === "image/svg+xml") { + if (fsSupported) { + try { + // This will only work as of Chrome 86, + // but can be safely ignored on older releases. + const item = event.dataTransfer.items[0]; + (file as any).handle = await (item as any).getAsFileSystemHandle(); + } catch (error) { + console.warn(error.name, error.message); + } + } + const { elements, appState } = await loadFromBlob( file, this.state, diff --git a/src/components/LayerUI.tsx b/src/components/LayerUI.tsx index e64ee575..f42c1bc1 100644 --- a/src/components/LayerUI.tsx +++ b/src/components/LayerUI.tsx @@ -48,6 +48,7 @@ import { UserList } from "./UserList"; import Library from "../data/library"; import { JSONExportDialog } from "./JSONExportDialog"; import { LibraryButton } from "./LibraryButton"; +import { isImageFileHandle } from "../data/blob"; interface LayerUIProps { actionManager: ActionManager; @@ -407,7 +408,7 @@ const LayerUI = ({ const createExporter = (type: ExportType): ExportCB => async ( exportedElements, ) => { - await exportCanvas(type, exportedElements, appState, { + const fileHandle = await exportCanvas(type, exportedElements, appState, { exportBackground: appState.exportBackground, name: appState.name, viewBackgroundColor: appState.viewBackgroundColor, @@ -417,6 +418,14 @@ const LayerUI = ({ console.error(error); setAppState({ errorMessage: error.message }); }); + + if ( + appState.exportEmbedScene && + fileHandle && + isImageFileHandle(fileHandle) + ) { + setAppState({ fileHandle }); + } }; return ( diff --git a/src/data/blob.ts b/src/data/blob.ts index 88bae342..67755ac4 100644 --- a/src/data/blob.ts +++ b/src/data/blob.ts @@ -1,3 +1,4 @@ +import { FileSystemHandle } from "browser-fs-access"; import { cleanAppStateForExport } from "../appState"; import { EXPORT_DATA_TYPES } from "../constants"; import { clearElementsForExport } from "../element"; @@ -80,6 +81,25 @@ export const getMimeType = (blob: Blob | string): string => { return ""; }; +export const getFileHandleType = (handle: FileSystemHandle | null) => { + if (!handle) { + return null; + } + + return handle.name.match(/\.(json|excalidraw|png|svg)$/)?.[1] || null; +}; + +export const isImageFileHandleType = ( + type: string | null, +): type is "png" | "svg" => { + return type === "png" || type === "svg"; +}; + +export const isImageFileHandle = (handle: FileSystemHandle | null) => { + const type = getFileHandleType(handle); + return type === "png" || type === "svg"; +}; + export const loadFromBlob = async ( blob: Blob, /** @see restore.localAppState */ @@ -97,7 +117,7 @@ export const loadFromBlob = async ( elements: clearElementsForExport(data.elements || []), appState: { theme: localAppState?.theme, - fileHandle: (!blob.type.startsWith("image/") && blob.handle) || null, + fileHandle: blob.handle || null, ...cleanAppStateForExport(data.appState || {}), ...(localAppState ? calculateScrollCenter(data.elements || [], localAppState, null) diff --git a/src/data/index.ts b/src/data/index.ts index c1b50d8d..fc988986 100644 --- a/src/data/index.ts +++ b/src/data/index.ts @@ -1,4 +1,4 @@ -import { fileSave } from "browser-fs-access"; +import { fileSave, FileSystemHandle } from "browser-fs-access"; import { copyBlobToClipboardAsPng, copyTextToSystemClipboard, @@ -24,11 +24,13 @@ export const exportCanvas = async ( exportPadding = DEFAULT_EXPORT_PADDING, viewBackgroundColor, name, + fileHandle = null, }: { exportBackground: boolean; exportPadding?: number; viewBackgroundColor: string; name: string; + fileHandle?: FileSystemHandle | null; }, ) => { if (elements.length === 0) { @@ -44,11 +46,14 @@ export const exportCanvas = async ( exportEmbedScene: appState.exportEmbedScene && type === "svg", }); if (type === "svg") { - await fileSave(new Blob([tempSvg.outerHTML], { type: "image/svg+xml" }), { - fileName: `${name}.svg`, - extensions: [".svg"], - }); - return; + return await fileSave( + new Blob([tempSvg.outerHTML], { type: "image/svg+xml" }), + { + fileName: `${name}.svg`, + extensions: [".svg"], + }, + fileHandle, + ); } else if (type === "clipboard-svg") { await copyTextToSystemClipboard(tempSvg.outerHTML); return; @@ -76,10 +81,14 @@ export const exportCanvas = async ( }); } - await fileSave(blob, { - fileName, - extensions: [".png"], - }); + return await fileSave( + blob, + { + fileName, + extensions: [".png"], + }, + fileHandle, + ); } else if (type === "clipboard") { try { await copyBlobToClipboardAsPng(blob); diff --git a/src/data/json.ts b/src/data/json.ts index 2b9e91c0..ce130cd6 100644 --- a/src/data/json.ts +++ b/src/data/json.ts @@ -4,7 +4,7 @@ import { EXPORT_DATA_TYPES, EXPORT_SOURCE, MIME_TYPES } from "../constants"; import { clearElementsForExport } from "../element"; import { ExcalidrawElement } from "../element/types"; import { AppState } from "../types"; -import { loadFromBlob } from "./blob"; +import { isImageFileHandle, loadFromBlob } from "./blob"; import { ExportedDataState, @@ -44,7 +44,7 @@ export const saveAsJSON = async ( description: "Excalidraw file", extensions: [".excalidraw"], }, - appState.fileHandle, + isImageFileHandle(appState.fileHandle) ? null : appState.fileHandle, ); return { fileHandle }; }; diff --git a/src/data/resave.ts b/src/data/resave.ts new file mode 100644 index 00000000..2f8f0bf9 --- /dev/null +++ b/src/data/resave.ts @@ -0,0 +1,38 @@ +import { ExcalidrawElement } from "../element/types"; +import { AppState } from "../types"; +import { exportCanvas } from "."; +import { getNonDeletedElements } from "../element"; +import { getFileHandleType, isImageFileHandleType } from "./blob"; + +export const resaveAsImageWithScene = async ( + elements: readonly ExcalidrawElement[], + appState: AppState, +) => { + const { exportBackground, viewBackgroundColor, name, fileHandle } = appState; + + const fileHandleType = getFileHandleType(fileHandle); + + if (!fileHandle || !isImageFileHandleType(fileHandleType)) { + throw new Error( + "fileHandle should exist and should be of type svg or png when resaving", + ); + } + appState = { + ...appState, + exportEmbedScene: true, + }; + + await exportCanvas( + fileHandleType, + getNonDeletedElements(elements), + appState, + { + exportBackground, + viewBackgroundColor, + name, + fileHandle, + }, + ); + + return { fileHandle }; +};