add support to export high resolution for png/jpg

This commit is contained in:
johnnyfish
2024-09-25 15:16:43 +03:00
committed by Guy Ben-Aharon
parent 3a276cc647
commit 395af73ee0
5 changed files with 123 additions and 41 deletions
@@ -3,7 +3,7 @@ import { emptyFn } from '@/lib/utils';
export type ImageType = 'png' | 'jpeg' | 'svg';
export interface ExportImageContext {
exportImage: (type: ImageType) => Promise<void>;
exportImage: (type: ImageType, scale: number) => Promise<void>;
}
export const exportImageContext = createContext<ExportImageContext>({
@@ -2,31 +2,22 @@ import React, { useCallback, useMemo } from 'react';
import type { ExportImageContext, ImageType } from './export-image-context';
import { exportImageContext } from './export-image-context';
import { toJpeg, toPng, toSvg } from 'html-to-image';
import {
getNodesBounds,
getViewportForBounds,
useReactFlow,
} from '@xyflow/react';
import { useReactFlow } from '@xyflow/react';
import { useChartDB } from '@/hooks/use-chartdb';
import { useFullScreenLoader } from '@/hooks/use-full-screen-spinner';
const imageWidth = 1024;
const imageHeight = 768;
export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({
children,
}) => {
const { hideLoader, showLoader } = useFullScreenLoader();
const { getNodes, setNodes } = useReactFlow();
const { setNodes, getViewport } = useReactFlow();
const { diagramName } = useChartDB();
const downloadImage = useCallback(
(dataUrl: string, type: ImageType) => {
const a = document.createElement('a');
a.setAttribute('download', `${diagramName}.${type}`);
a.setAttribute('href', dataUrl);
a.click();
},
[diagramName]
@@ -45,7 +36,7 @@ export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({
);
const exportImage: ExportImageContext['exportImage'] = useCallback(
async (type) => {
async (type, scale = 1) => {
showLoader({
animated: false,
});
@@ -54,15 +45,16 @@ export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({
nodes.map((node) => ({ ...node, selected: false }))
);
const nodesBounds = getNodesBounds(getNodes());
const viewport = getViewportForBounds(
nodesBounds,
imageWidth,
imageHeight,
0.01,
2,
0.02
);
const viewport = getViewport();
const reactFlowBounds = document
.querySelector('.react-flow')
?.getBoundingClientRect();
if (!reactFlowBounds) {
console.error('Could not find React Flow container');
hideLoader();
return;
}
const imageCreateFn = imageCreatorMap[type];
@@ -75,14 +67,15 @@ export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({
...(type === 'jpeg' || type === 'png'
? { backgroundColor: '#ffffff' }
: {}),
width: imageWidth,
height: imageHeight,
width: reactFlowBounds.width,
height: reactFlowBounds.height,
style: {
width: `${imageWidth}px`,
height: `${imageHeight}px`,
width: `${reactFlowBounds.width}px`,
height: `${reactFlowBounds.height}px`,
transform: `translate(${viewport.x}px, ${viewport.y}px) scale(${viewport.zoom})`,
},
quality: 1,
pixelRatio: scale,
}
);
@@ -92,11 +85,11 @@ export const ExportImageProvider: React.FC<React.PropsWithChildren> = ({
},
[
downloadImage,
getNodes,
getViewport,
hideLoader,
imageCreatorMap,
setNodes,
showLoader,
hideLoader,
]
);
+12
View File
@@ -309,6 +309,18 @@ export const en = {
edit_table: 'Edit Table',
delete_table: 'Delete Table',
},
export_high_res: {
title: 'Export High Resolution Image',
description: 'Choose the scale factor for export:',
select_scale: 'Select scale',
scale_1x: '1x Regular',
scale_2x: '2x (Recommended)',
scale_3x: '3x',
scale_4x: '4x',
export_png: 'Export High-Res PNG',
export_jpg: 'Export High-Res JPG',
},
},
};
+37 -2
View File
@@ -99,7 +99,6 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
const nodeTypes = useMemo(() => ({ table: TableNode }), []);
const edgeTypes = useMemo(() => ({ 'table-edge': TableEdge }), []);
const [isInitialLoadingNodes, setIsInitialLoadingNodes] = useState(true);
const [nodes, setNodes, onNodesChange] = useNodesState<TableNodeType>(
initialTables.map((table) => tableToTableNode(table, filteredSchemas))
);
@@ -273,6 +272,26 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
const onNodesChangeHandler: OnNodesChange<TableNodeType> = useCallback(
(changes) => {
changes.forEach((change) => {
if (
change.type === 'dimensions' &&
'dimensions' in change &&
change.dimensions
) {
setNodes((nds) =>
nds.map((node) =>
node.id === change.id
? {
...node,
width: change.dimensions.width,
height: change.dimensions.height,
}
: node
)
);
}
});
const positionChanges: NodePositionChange[] = changes.filter(
(change) => change.type === 'position' && !change.dragging
) as NodePositionChange[];
@@ -328,9 +347,25 @@ export const Canvas: React.FC<CanvasProps> = ({ initialTables }) => {
);
}
changes.forEach((change) => {
if (
change.type === 'dimensions' &&
'dimensions' in change &&
change.dimensions
) {
setNodes((nds) =>
nds.map((node) =>
node.id === change.id
? { ...node, ...change.dimensions }
: node
)
);
}
});
return onNodesChange(changes);
},
[onNodesChange, updateTablesState]
[onNodesChange, updateTablesState, setNodes]
);
const isLoadingDOM =
+53 -11
View File
@@ -116,16 +116,8 @@ export const TopNavbar: React.FC<TopNavbarProps> = () => {
setEditMode(true);
};
const exportPNG = useCallback(() => {
exportImage('png');
}, [exportImage]);
const exportSVG = useCallback(() => {
exportImage('svg');
}, [exportImage]);
const exportJPG = useCallback(() => {
exportImage('jpeg');
exportImage('svg', 1);
}, [exportImage]);
const openChartDBIO = useCallback(() => {
@@ -302,6 +294,48 @@ export const TopNavbar: React.FC<TopNavbarProps> = () => {
[i18n]
);
const handleHighResExport = useCallback(
(format: 'png' | 'jpg') => {
showAlert({
title: t('export_high_res.title'),
content: (
<div className="flex flex-col gap-4">
<p>{t('export_high_res.description')}</p>
<Select
onValueChange={(value) =>
exportImage(format, Number(value))
}
>
<SelectTrigger>
<SelectValue
placeholder={t(
'export_high_res.select_scale'
)}
/>
</SelectTrigger>
<SelectContent>
<SelectItem value="1">
{t('export_high_res.scale_1x')}
</SelectItem>
<SelectItem value="2">
{t('export_high_res.scale_2x')}
</SelectItem>
<SelectItem value="3">
{t('export_high_res.scale_3x')}
</SelectItem>
<SelectItem value="4">
{t('export_high_res.scale_4x')}
</SelectItem>
</SelectContent>
</Select>
</div>
),
closeLabel: 'Cancel',
});
},
[exportImage, showAlert, t]
);
return (
<nav className="flex h-20 flex-col justify-between border-b px-3 md:h-12 md:flex-row md:items-center md:px-4">
<div className="flex flex-1 justify-between gap-x-3 md:justify-normal">
@@ -502,10 +536,18 @@ export const TopNavbar: React.FC<TopNavbarProps> = () => {
{t('menu.file.export_as')}
</MenubarSubTrigger>
<MenubarSubContent>
<MenubarItem onClick={exportPNG}>
<MenubarItem
onClick={() =>
handleHighResExport('png')
}
>
PNG
</MenubarItem>
<MenubarItem onClick={exportJPG}>
<MenubarItem
onClick={() =>
handleHighResExport('jpg')
}
>
JPG
</MenubarItem>
<MenubarItem onClick={exportSVG}>