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:
parent
9581c45522
commit
685abac81a
@ -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: {
|
||||||
|
@ -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,
|
||||||
|
@ -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 (
|
||||||
|
@ -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)
|
||||||
|
@ -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(
|
||||||
fileName: `${name}.svg`,
|
new Blob([tempSvg.outerHTML], { type: "image/svg+xml" }),
|
||||||
extensions: [".svg"],
|
{
|
||||||
});
|
fileName: `${name}.svg`,
|
||||||
return;
|
extensions: [".svg"],
|
||||||
|
},
|
||||||
|
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(
|
||||||
fileName,
|
blob,
|
||||||
extensions: [".png"],
|
{
|
||||||
});
|
fileName,
|
||||||
|
extensions: [".png"],
|
||||||
|
},
|
||||||
|
fileHandle,
|
||||||
|
);
|
||||||
} else if (type === "clipboard") {
|
} else if (type === "clipboard") {
|
||||||
try {
|
try {
|
||||||
await copyBlobToClipboardAsPng(blob);
|
await copyBlobToClipboardAsPng(blob);
|
||||||
|
@ -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
38
src/data/resave.ts
Normal 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 };
|
||||||
|
};
|
Loading…
x
Reference in New Issue
Block a user