Explorar el Código

feat: resave to png/svg with metadata if you loaded your scene from a png/svg file (#3645)

Co-authored-by: dwelle <luzar.david@gmail.com>
David Laban hace 4 años
padre
commit
685abac81a
Se han modificado 7 ficheros con 108 adiciones y 15 borrados
  1. 7 1
      src/actions/actionExport.tsx
  2. 11 0
      src/components/App.tsx
  3. 10 1
      src/components/LayerUI.tsx
  4. 21 1
      src/data/blob.ts
  5. 19 10
      src/data/index.ts
  6. 2 2
      src/data/json.ts
  7. 38 0
      src/data/resave.ts

+ 7 - 1
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: {

+ 11 - 0
src/components/App.tsx

@@ -3827,6 +3827,17 @@ class App extends React.Component<AppProps, AppState> {
     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,

+ 10 - 1
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 (

+ 21 - 1
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)

+ 19 - 10
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);

+ 2 - 2
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 };
 };

+ 38 - 0
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 };
+};