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>
This commit is contained in:
David Laban 2021-07-15 09:54:26 -04:00 committed by GitHub
parent 9581c45522
commit 685abac81a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 108 additions and 15 deletions

View File

@ -7,6 +7,7 @@ import "../components/ToolIcon.scss";
import { Tooltip } from "../components/Tooltip"; import { Tooltip } from "../components/Tooltip";
import { DarkModeToggle, Appearence } from "../components/DarkModeToggle"; import { DarkModeToggle, Appearence } from "../components/DarkModeToggle";
import { loadFromJSON, saveAsJSON } from "../data"; import { loadFromJSON, saveAsJSON } from "../data";
import { resaveAsImageWithScene } from "../data/resave";
import { t } from "../i18n"; import { t } from "../i18n";
import { useIsMobile } from "../components/App"; import { useIsMobile } from "../components/App";
import { KEYS } from "../keys"; import { KEYS } from "../keys";
@ -18,6 +19,7 @@ import { DEFAULT_EXPORT_PADDING, EXPORT_SCALES } from "../constants";
import { getSelectedElements, isSomeElementSelected } from "../scene"; import { getSelectedElements, isSomeElementSelected } from "../scene";
import { getNonDeletedElements } from "../element"; import { getNonDeletedElements } from "../element";
import { ActiveFile } from "../components/ActiveFile"; import { ActiveFile } from "../components/ActiveFile";
import { isImageFileHandle } from "../data/blob";
export const actionChangeProjectName = register({ export const actionChangeProjectName = register({
name: "changeProjectName", name: "changeProjectName",
@ -128,8 +130,12 @@ export const actionSaveToActiveFile = register({
name: "saveToActiveFile", name: "saveToActiveFile",
perform: async (elements, appState, value) => { perform: async (elements, appState, value) => {
const fileHandleExists = !!appState.fileHandle; const fileHandleExists = !!appState.fileHandle;
try { try {
const { fileHandle } = await saveAsJSON(elements, appState); const { fileHandle } = isImageFileHandle(appState.fileHandle)
? await resaveAsImageWithScene(elements, appState)
: await saveAsJSON(elements, appState);
return { return {
commitToHistory: false, commitToHistory: false,
appState: { appState: {

View File

@ -3827,6 +3827,17 @@ class App extends React.Component<AppProps, AppState> {
try { try {
const file = event.dataTransfer.files[0]; const file = event.dataTransfer.files[0];
if (file?.type === "image/png" || file?.type === "image/svg+xml") { 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( const { elements, appState } = await loadFromBlob(
file, file,
this.state, this.state,

View File

@ -48,6 +48,7 @@ import { UserList } from "./UserList";
import Library from "../data/library"; import Library from "../data/library";
import { JSONExportDialog } from "./JSONExportDialog"; import { JSONExportDialog } from "./JSONExportDialog";
import { LibraryButton } from "./LibraryButton"; import { LibraryButton } from "./LibraryButton";
import { isImageFileHandle } from "../data/blob";
interface LayerUIProps { interface LayerUIProps {
actionManager: ActionManager; actionManager: ActionManager;
@ -407,7 +408,7 @@ const LayerUI = ({
const createExporter = (type: ExportType): ExportCB => async ( const createExporter = (type: ExportType): ExportCB => async (
exportedElements, exportedElements,
) => { ) => {
await exportCanvas(type, exportedElements, appState, { const fileHandle = await exportCanvas(type, exportedElements, appState, {
exportBackground: appState.exportBackground, exportBackground: appState.exportBackground,
name: appState.name, name: appState.name,
viewBackgroundColor: appState.viewBackgroundColor, viewBackgroundColor: appState.viewBackgroundColor,
@ -417,6 +418,14 @@ const LayerUI = ({
console.error(error); console.error(error);
setAppState({ errorMessage: error.message }); setAppState({ errorMessage: error.message });
}); });
if (
appState.exportEmbedScene &&
fileHandle &&
isImageFileHandle(fileHandle)
) {
setAppState({ fileHandle });
}
}; };
return ( return (

View File

@ -1,3 +1,4 @@
import { FileSystemHandle } from "browser-fs-access";
import { cleanAppStateForExport } from "../appState"; import { cleanAppStateForExport } from "../appState";
import { EXPORT_DATA_TYPES } from "../constants"; import { EXPORT_DATA_TYPES } from "../constants";
import { clearElementsForExport } from "../element"; import { clearElementsForExport } from "../element";
@ -80,6 +81,25 @@ export const getMimeType = (blob: Blob | string): string => {
return ""; 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 ( export const loadFromBlob = async (
blob: Blob, blob: Blob,
/** @see restore.localAppState */ /** @see restore.localAppState */
@ -97,7 +117,7 @@ export const loadFromBlob = async (
elements: clearElementsForExport(data.elements || []), elements: clearElementsForExport(data.elements || []),
appState: { appState: {
theme: localAppState?.theme, theme: localAppState?.theme,
fileHandle: (!blob.type.startsWith("image/") && blob.handle) || null, fileHandle: blob.handle || null,
...cleanAppStateForExport(data.appState || {}), ...cleanAppStateForExport(data.appState || {}),
...(localAppState ...(localAppState
? calculateScrollCenter(data.elements || [], localAppState, null) ? calculateScrollCenter(data.elements || [], localAppState, null)

View File

@ -1,4 +1,4 @@
import { fileSave } from "browser-fs-access"; import { fileSave, FileSystemHandle } from "browser-fs-access";
import { import {
copyBlobToClipboardAsPng, copyBlobToClipboardAsPng,
copyTextToSystemClipboard, copyTextToSystemClipboard,
@ -24,11 +24,13 @@ export const exportCanvas = async (
exportPadding = DEFAULT_EXPORT_PADDING, exportPadding = DEFAULT_EXPORT_PADDING,
viewBackgroundColor, viewBackgroundColor,
name, name,
fileHandle = null,
}: { }: {
exportBackground: boolean; exportBackground: boolean;
exportPadding?: number; exportPadding?: number;
viewBackgroundColor: string; viewBackgroundColor: string;
name: string; name: string;
fileHandle?: FileSystemHandle | null;
}, },
) => { ) => {
if (elements.length === 0) { if (elements.length === 0) {
@ -44,11 +46,14 @@ export const exportCanvas = async (
exportEmbedScene: appState.exportEmbedScene && type === "svg", exportEmbedScene: appState.exportEmbedScene && type === "svg",
}); });
if (type === "svg") { if (type === "svg") {
await fileSave(new Blob([tempSvg.outerHTML], { type: "image/svg+xml" }), { return await fileSave(
new Blob([tempSvg.outerHTML], { type: "image/svg+xml" }),
{
fileName: `${name}.svg`, fileName: `${name}.svg`,
extensions: [".svg"], extensions: [".svg"],
}); },
return; fileHandle,
);
} else if (type === "clipboard-svg") { } else if (type === "clipboard-svg") {
await copyTextToSystemClipboard(tempSvg.outerHTML); await copyTextToSystemClipboard(tempSvg.outerHTML);
return; return;
@ -76,10 +81,14 @@ export const exportCanvas = async (
}); });
} }
await fileSave(blob, { return await fileSave(
blob,
{
fileName, fileName,
extensions: [".png"], extensions: [".png"],
}); },
fileHandle,
);
} else if (type === "clipboard") { } else if (type === "clipboard") {
try { try {
await copyBlobToClipboardAsPng(blob); await copyBlobToClipboardAsPng(blob);

View File

@ -4,7 +4,7 @@ import { EXPORT_DATA_TYPES, EXPORT_SOURCE, MIME_TYPES } from "../constants";
import { clearElementsForExport } from "../element"; import { clearElementsForExport } from "../element";
import { ExcalidrawElement } from "../element/types"; import { ExcalidrawElement } from "../element/types";
import { AppState } from "../types"; import { AppState } from "../types";
import { loadFromBlob } from "./blob"; import { isImageFileHandle, loadFromBlob } from "./blob";
import { import {
ExportedDataState, ExportedDataState,
@ -44,7 +44,7 @@ export const saveAsJSON = async (
description: "Excalidraw file", description: "Excalidraw file",
extensions: [".excalidraw"], extensions: [".excalidraw"],
}, },
appState.fileHandle, isImageFileHandle(appState.fileHandle) ? null : appState.fileHandle,
); );
return { fileHandle }; return { fileHandle };
}; };

38
src/data/resave.ts Normal file
View File

@ -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 };
};