Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BC-6431 - Fix tldraw export to image functionality #53

Merged
merged 13 commits into from
Feb 12, 2024
21 changes: 7 additions & 14 deletions src/components/Editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,20 +15,13 @@ function Editor({
darkModeHandler: (isDarkMode: boolean) => void;
focusModeHandler: (isFocusMode: boolean) => void;
}) {
const { onSaveProjectAs, onSaveProject, onOpenMedia } = useFileSystem();
const {
onMount,
onOpen,
onAssetCreate,
onAssetDelete,
onPatch,
onExport,
...events
} = useMultiplayerState({
roomId,
setIsDarkMode: darkModeHandler,
setIsFocusMode: focusModeHandler,
});
const { onOpenMedia, onOpenProject } = useFileSystem();
const { onMount, onSave, onAssetCreate, onPatch, onExport, ...events } =
useMultiplayerState({
roomId,
setIsDarkMode: darkModeHandler,
setIsFocusMode: focusModeHandler,
});
const containerRef = useRef<HTMLDivElement | null>(null);
useTldrawUiSanitizer(containerRef);
const { isDarkMode } = useTldrawSettings();
Expand Down
61 changes: 33 additions & 28 deletions src/hooks/useMultiplayerState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@ import lodash from "lodash";
import {
TDAsset,
TDBinding,
TDDocument,
TDExport,
TDFile,
TDShape,
TDUser,
TldrawApp,
Expand All @@ -26,6 +24,12 @@ import {
} from "../stores/setup";
import { STORAGE_SETTINGS_KEY } from "../utils/userSettings";
import { UserPresence } from "../types/UserPresence";
import {
importAssetsToS3,
openFromFileSystem,
} from "../utils/boardImportUtils";
import { saveToFileSystem } from "../utils/boardExportUtils";
import { uploadFileToStorage } from "../utils/fileUpload";
import { getImageBlob } from "../utils/tldrawImageExportUtils";

declare const window: Window & { app: TldrawApp };
Expand Down Expand Up @@ -62,7 +66,7 @@ export function useMultiplayerState({
app.fileSystemHandle = handle;
}
} catch (error) {
console.error("Error while exporting project");
console.error("Error while exporting project", error);
toast.error("An error occurred while exporting project");
}
app.setIsLoading(false);
Expand All @@ -84,7 +88,7 @@ export function useMultiplayerState({
app.fileSystemHandle = handle;
}
} catch (error) {
console.error("Error while exporting project");
console.error("Error while exporting project", error);
toast.error("An error occurred while exporting project");
}
app.setIsLoading(false);
Expand Down Expand Up @@ -153,7 +157,7 @@ export function useMultiplayerState({
if (file.size > envs!.TLDRAW__ASSETS_MAX_SIZE) {
toast.info(
`Asset is too big - max. ${
envs!.TLDRAW__ASSETS_MAX_SIZE / 1000000
envs!.TLDRAW__ASSETS_MAX_SIZE / 1048576
davwas marked this conversation as resolved.
Show resolved Hide resolved
}MB`,
);
return false;
Expand Down Expand Up @@ -202,20 +206,7 @@ export function useMultiplayerState({
setIsFocusMode(app.settings.isFocusMode);
}
},
[setIsDarkMode],
);

const onMount = useCallback(
(app: TldrawApp) => {
app.loadRoom(roomId);
app.document.name = `room-${roomId}`;
// Turn off the app's own undo / redo stack
app.pause();
// Put the state into the window, for debugging
window.app = app;
setApp(app);
},
[roomId],
[setIsDarkMode, setIsFocusMode],
);

const onUndo = useCallback(() => {
Expand Down Expand Up @@ -254,14 +245,27 @@ export function useMultiplayerState({

const onExport = useCallback(
async (app: TldrawApp, info: TDExport) => {
const blob = await getImageBlob(app, info.type);
if (!blob) return;

const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = `${roomId}_export.${info.type}`;
link.click();
app.setIsLoading(true);
undoManager.stopCapturing();
syncAssets(app);
try {
const blob = await getImageBlob(app, info.type);

if (!blob) {
app.setIsLoading(false);
return;
}

const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = `${roomId}_export.${info.type}`;
link.click();
} catch (error) {
console.error("Error while exporting project as image", error);
davwas marked this conversation as resolved.
Show resolved Hide resolved
toast.error("An error occurred while exporting project as image");
}
app.setIsLoading(false);
},
[roomId],
);
Expand Down Expand Up @@ -355,7 +359,8 @@ export function useMultiplayerState({
onUndo,
onRedo,
onMount,
onOpen,
onSave,
onSaveAs,
onExport,
onChangePage,
onChangePresence,
Expand Down
2 changes: 1 addition & 1 deletion src/utils/imageExportUtils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* Custom function for exporting images stored in external storage correctly
* Custom function for exporting images stored in external storage correctly.
**/

export const serializeImage = (id: string): string => {
Expand Down
54 changes: 53 additions & 1 deletion src/utils/tldrawImageExportUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ const getImageBlob = async (
return blob;
}

const imageBlob = await TLDR.getImageForSvg(svg, format, {
const imageBlob = await getImageForSvg(svg, format, {
scale: 2,
quality: 1,
});
Expand Down Expand Up @@ -284,4 +284,56 @@ const includeTldrawFonts = async (
}
};

const getImageForSvg = async (
svg: SVGElement,
type: Exclude<TDExportType, TDExportType.JSON> = TDExportType.PNG,
opts = {} as Partial<{
scale: number;
quality: number;
}>,
) => {
const { scale = 2, quality = 1 } = opts;

const svgString = TLDR.getSvgString(svg, scale);
if (!svgString) return;

const canvas = await new Promise<HTMLCanvasElement>((resolve, reject) => {
const image = new Image();

image.crossOrigin = "anonymous";

const base64SVG = window.btoa(unescape(encodeURIComponent(svgString)));

const dataUrl = `data:image/svg+xml;base64,${base64SVG}`;

image.onload = () => {
const canvas = document.createElement("canvas") as HTMLCanvasElement;
const context = canvas.getContext("2d")!;

const imageWidth = image.width;
const imageHeight = image.height;

canvas.width = imageWidth;
canvas.height = imageHeight;
context.drawImage(image, 0, 0, imageWidth, imageHeight);

URL.revokeObjectURL(dataUrl);

resolve(canvas);
};

image.onerror = () => {
reject("Could not convert that SVG to an image.");
};

image.src = dataUrl;
});

const blob = await new Promise<Blob>((resolve) =>
canvas.toBlob((blob) => resolve(blob!), "image/" + type, quality),
);

return blob;
};

export { getImageBlob };
Loading