mirror of
https://github.com/chartdb/chartdb.git
synced 2026-05-25 07:29:25 -05:00
add support to export high resolution for png/jpg
This commit is contained in:
committed by
Guy Ben-Aharon
parent
3a276cc647
commit
395af73ee0
@@ -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,
|
||||
]
|
||||
);
|
||||
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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}>
|
||||
|
||||
Reference in New Issue
Block a user