diff options
Diffstat (limited to 'subprojects/frontend/src')
12 files changed, 130 insertions, 17 deletions
diff --git a/subprojects/frontend/src/RootStore.ts b/subprojects/frontend/src/RootStore.ts index 27bff0de..8a17d2de 100644 --- a/subprojects/frontend/src/RootStore.ts +++ b/subprojects/frontend/src/RootStore.ts | |||
@@ -9,7 +9,7 @@ import { makeAutoObservable, runInAction } from 'mobx'; | |||
9 | 9 | ||
10 | import PWAStore from './PWAStore'; | 10 | import PWAStore from './PWAStore'; |
11 | import type EditorStore from './editor/EditorStore'; | 11 | import type EditorStore from './editor/EditorStore'; |
12 | import ExportSettingsScotre from './graph/ExportSettingsStore'; | 12 | import ExportSettingsScotre from './graph/export/ExportSettingsStore'; |
13 | import Compressor from './persistence/Compressor'; | 13 | import Compressor from './persistence/Compressor'; |
14 | import ThemeStore from './theme/ThemeStore'; | 14 | import ThemeStore from './theme/ThemeStore'; |
15 | 15 | ||
diff --git a/subprojects/frontend/src/graph/GraphArea.tsx b/subprojects/frontend/src/graph/GraphArea.tsx index 1416a259..b5d93aef 100644 --- a/subprojects/frontend/src/graph/GraphArea.tsx +++ b/subprojects/frontend/src/graph/GraphArea.tsx | |||
@@ -11,10 +11,10 @@ import { useState } from 'react'; | |||
11 | import { useResizeDetector } from 'react-resize-detector'; | 11 | import { useResizeDetector } from 'react-resize-detector'; |
12 | 12 | ||
13 | import DotGraphVisualizer from './DotGraphVisualizer'; | 13 | import DotGraphVisualizer from './DotGraphVisualizer'; |
14 | import ExportPanel from './ExportPanel'; | ||
15 | import type GraphStore from './GraphStore'; | 14 | import type GraphStore from './GraphStore'; |
16 | import VisibilityPanel from './VisibilityPanel'; | 15 | import VisibilityPanel from './VisibilityPanel'; |
17 | import ZoomCanvas from './ZoomCanvas'; | 16 | import ZoomCanvas from './ZoomCanvas'; |
17 | import ExportPanel from './export/ExportPanel'; | ||
18 | 18 | ||
19 | function GraphArea({ graph }: { graph: GraphStore }): JSX.Element { | 19 | function GraphArea({ graph }: { graph: GraphStore }): JSX.Element { |
20 | const { breakpoints } = useTheme(); | 20 | const { breakpoints } = useTheme(); |
diff --git a/subprojects/frontend/src/graph/ExportPanel.tsx b/subprojects/frontend/src/graph/export/ExportPanel.tsx index 2621deff..c93fa837 100644 --- a/subprojects/frontend/src/graph/ExportPanel.tsx +++ b/subprojects/frontend/src/graph/export/ExportPanel.tsx | |||
@@ -8,6 +8,7 @@ import ChevronRightIcon from '@mui/icons-material/ChevronRight'; | |||
8 | import ContentCopyIcon from '@mui/icons-material/ContentCopy'; | 8 | import ContentCopyIcon from '@mui/icons-material/ContentCopy'; |
9 | import DarkModeIcon from '@mui/icons-material/DarkMode'; | 9 | import DarkModeIcon from '@mui/icons-material/DarkMode'; |
10 | import ImageIcon from '@mui/icons-material/Image'; | 10 | import ImageIcon from '@mui/icons-material/Image'; |
11 | import InsertDriveFileOutlinedIcon from '@mui/icons-material/InsertDriveFileOutlined'; | ||
11 | import LightModeIcon from '@mui/icons-material/LightMode'; | 12 | import LightModeIcon from '@mui/icons-material/LightMode'; |
12 | import SaveAltIcon from '@mui/icons-material/SaveAlt'; | 13 | import SaveAltIcon from '@mui/icons-material/SaveAlt'; |
13 | import ShapeLineIcon from '@mui/icons-material/ShapeLine'; | 14 | import ShapeLineIcon from '@mui/icons-material/ShapeLine'; |
@@ -24,11 +25,11 @@ import { styled } from '@mui/material/styles'; | |||
24 | import { observer } from 'mobx-react-lite'; | 25 | import { observer } from 'mobx-react-lite'; |
25 | import { useCallback } from 'react'; | 26 | import { useCallback } from 'react'; |
26 | 27 | ||
27 | import { useRootStore } from '../RootStoreProvider'; | 28 | import { useRootStore } from '../../RootStoreProvider'; |
28 | import getLogger from '../utils/getLogger'; | 29 | import getLogger from '../../utils/getLogger'; |
30 | import type GraphStore from '../GraphStore'; | ||
31 | import SlideInPanel from '../SlideInPanel'; | ||
29 | 32 | ||
30 | import type GraphStore from './GraphStore'; | ||
31 | import SlideInPanel from './SlideInPanel'; | ||
32 | import exportDiagram from './exportDiagram'; | 33 | import exportDiagram from './exportDiagram'; |
33 | 34 | ||
34 | const log = getLogger('graph.ExportPanel'); | 35 | const log = getLogger('graph.ExportPanel'); |
@@ -138,6 +139,13 @@ function ExportPanel({ | |||
138 | <ShapeLineIcon fontSize="small" /> SVG | 139 | <ShapeLineIcon fontSize="small" /> SVG |
139 | </ToggleButton> | 140 | </ToggleButton> |
140 | <ToggleButton | 141 | <ToggleButton |
142 | value="pdf" | ||
143 | selected={exportSettingsStore.format === 'pdf'} | ||
144 | onClick={() => exportSettingsStore.setFormat('pdf')} | ||
145 | > | ||
146 | <InsertDriveFileOutlinedIcon fontSize="small" /> PDF | ||
147 | </ToggleButton> | ||
148 | <ToggleButton | ||
141 | value="png" | 149 | value="png" |
142 | selected={exportSettingsStore.format === 'png'} | 150 | selected={exportSettingsStore.format === 'png'} |
143 | onClick={() => exportSettingsStore.setFormat('png')} | 151 | onClick={() => exportSettingsStore.setFormat('png')} |
@@ -170,7 +178,7 @@ function ExportPanel({ | |||
170 | } | 178 | } |
171 | label="Transparent background" | 179 | label="Transparent background" |
172 | /> | 180 | /> |
173 | {exportSettingsStore.format === 'svg' && ( | 181 | {exportSettingsStore.canEmbedFonts && ( |
174 | <FormControlLabel | 182 | <FormControlLabel |
175 | control={ | 183 | control={ |
176 | <Switch | 184 | <Switch |
@@ -182,13 +190,17 @@ function ExportPanel({ | |||
182 | <Stack direction="column"> | 190 | <Stack direction="column"> |
183 | <Typography>Embed fonts</Typography> | 191 | <Typography>Embed fonts</Typography> |
184 | <Typography variant="caption"> | 192 | <Typography variant="caption"> |
185 | +75 kB, only supported in browsers | 193 | {exportSettingsStore.format === 'pdf' ? ( |
194 | <>+20 kB fully embedded</> | ||
195 | ) : ( | ||
196 | <>+75 kB, only supported in browsers</> | ||
197 | )} | ||
186 | </Typography> | 198 | </Typography> |
187 | </Stack> | 199 | </Stack> |
188 | } | 200 | } |
189 | /> | 201 | /> |
190 | )} | 202 | )} |
191 | {exportSettingsStore.format === 'png' && ( | 203 | {exportSettingsStore.canScale && ( |
192 | <Box mx={4} mt={1} mb={2}> | 204 | <Box mx={4} mt={1} mb={2}> |
193 | <Slider | 205 | <Slider |
194 | aria-label="Image scale" | 206 | aria-label="Image scale" |
diff --git a/subprojects/frontend/src/graph/ExportSettingsStore.tsx b/subprojects/frontend/src/graph/export/ExportSettingsStore.tsx index 8ee91b73..53a161ab 100644 --- a/subprojects/frontend/src/graph/ExportSettingsStore.tsx +++ b/subprojects/frontend/src/graph/export/ExportSettingsStore.tsx | |||
@@ -6,7 +6,7 @@ | |||
6 | 6 | ||
7 | import { makeAutoObservable } from 'mobx'; | 7 | import { makeAutoObservable } from 'mobx'; |
8 | 8 | ||
9 | export type ExportFormat = 'svg' | 'png'; | 9 | export type ExportFormat = 'svg' | 'pdf' | 'png'; |
10 | export type ExportTheme = 'light' | 'dark'; | 10 | export type ExportTheme = 'light' | 'dark'; |
11 | 11 | ||
12 | export default class ExportSettingsStore { | 12 | export default class ExportSettingsStore { |
@@ -16,7 +16,9 @@ export default class ExportSettingsStore { | |||
16 | 16 | ||
17 | transparent = true; | 17 | transparent = true; |
18 | 18 | ||
19 | embedFonts = false; | 19 | embedSVGFonts = false; |
20 | |||
21 | embedPDFFonts = true; | ||
20 | 22 | ||
21 | scale = 100; | 23 | scale = 100; |
22 | 24 | ||
@@ -43,4 +45,23 @@ export default class ExportSettingsStore { | |||
43 | setScale(scale: number): void { | 45 | setScale(scale: number): void { |
44 | this.scale = scale; | 46 | this.scale = scale; |
45 | } | 47 | } |
48 | |||
49 | get embedFonts(): boolean { | ||
50 | return this.format === 'pdf' ? this.embedPDFFonts : this.embedSVGFonts; | ||
51 | } | ||
52 | |||
53 | private set embedFonts(embedFonts: boolean) { | ||
54 | if (this.format === 'pdf') { | ||
55 | this.embedPDFFonts = embedFonts; | ||
56 | } | ||
57 | this.embedSVGFonts = embedFonts; | ||
58 | } | ||
59 | |||
60 | get canEmbedFonts(): boolean { | ||
61 | return this.format === 'svg' || this.format === 'pdf'; | ||
62 | } | ||
63 | |||
64 | get canScale(): boolean { | ||
65 | return this.format === 'png'; | ||
66 | } | ||
46 | } | 67 | } |
diff --git a/subprojects/frontend/src/graph/exportDiagram.tsx b/subprojects/frontend/src/graph/export/exportDiagram.tsx index 3ba278f9..685b6ace 100644 --- a/subprojects/frontend/src/graph/exportDiagram.tsx +++ b/subprojects/frontend/src/graph/export/exportDiagram.tsx | |||
@@ -17,13 +17,13 @@ import labelSVG from '@material-icons/svg/svg/label/baseline.svg?raw'; | |||
17 | import labelOutlinedSVG from '@material-icons/svg/svg/label/outline.svg?raw'; | 17 | import labelOutlinedSVG from '@material-icons/svg/svg/label/outline.svg?raw'; |
18 | import type { Theme } from '@mui/material/styles'; | 18 | import type { Theme } from '@mui/material/styles'; |
19 | 19 | ||
20 | import { darkTheme, lightTheme } from '../theme/ThemeProvider'; | 20 | import { darkTheme, lightTheme } from '../../theme/ThemeProvider'; |
21 | import { copyBlob, saveBlob } from '../utils/fileIO'; | 21 | import { copyBlob, saveBlob } from '../../utils/fileIO'; |
22 | import type GraphStore from '../GraphStore'; | ||
23 | import { createGraphTheme } from '../GraphTheme'; | ||
24 | import { SVG_NS } from '../postProcessSVG'; | ||
22 | 25 | ||
23 | import type ExportSettingsStore from './ExportSettingsStore'; | 26 | import type ExportSettingsStore from './ExportSettingsStore'; |
24 | import type GraphStore from './GraphStore'; | ||
25 | import { createGraphTheme } from './GraphTheme'; | ||
26 | import { SVG_NS } from './postProcessSVG'; | ||
27 | 27 | ||
28 | const PROLOG = '<?xml version="1.0" encoding="UTF-8" standalone="no"?>'; | 28 | const PROLOG = '<?xml version="1.0" encoding="UTF-8" standalone="no"?>'; |
29 | const PNG_CONTENT_TYPE = 'image/png'; | 29 | const PNG_CONTENT_TYPE = 'image/png'; |
@@ -283,6 +283,20 @@ async function serializePNG( | |||
283 | }); | 283 | }); |
284 | } | 284 | } |
285 | 285 | ||
286 | let serializePDFCached: | ||
287 | | ((svg: SVGSVGElement, embedFonts: boolean) => Promise<Blob>) | ||
288 | | undefined; | ||
289 | |||
290 | async function serializePDF( | ||
291 | svg: SVGSVGElement, | ||
292 | settings: ExportSettingsStore, | ||
293 | ): Promise<Blob> { | ||
294 | if (serializePDFCached === undefined) { | ||
295 | serializePDFCached = (await import('./serializePDF')).default; | ||
296 | } | ||
297 | return serializePDFCached(svg, settings.embedFonts); | ||
298 | } | ||
299 | |||
286 | export default async function exportDiagram( | 300 | export default async function exportDiagram( |
287 | svgContainer: HTMLElement | undefined, | 301 | svgContainer: HTMLElement | undefined, |
288 | graph: GraphStore, | 302 | graph: GraphStore, |
@@ -319,11 +333,16 @@ export default async function exportDiagram( | |||
319 | // If we are creating a PNG, font file size doesn't matter, | 333 | // If we are creating a PNG, font file size doesn't matter, |
320 | // and we can reuse fonts the browser has already downloaded. | 334 | // and we can reuse fonts the browser has already downloaded. |
321 | fontsCSS = await fetchVariableFontCSS(); | 335 | fontsCSS = await fetchVariableFontCSS(); |
322 | } else if (settings.embedFonts) { | 336 | } else if (settings.format === 'svg' && settings.embedFonts) { |
323 | fontsCSS = await fetchFontCSS(); | 337 | fontsCSS = await fetchFontCSS(); |
324 | } | 338 | } |
325 | appendStyles(svgDocument, copyOfSVG, theme, colorNodes, fontsCSS); | 339 | appendStyles(svgDocument, copyOfSVG, theme, colorNodes, fontsCSS); |
326 | 340 | ||
341 | if (settings.format === 'pdf') { | ||
342 | const pdf = await serializePDF(copyOfSVG, settings); | ||
343 | await saveBlob(pdf, 'graph.pdf', 'application/pdf', EXPORT_ID); | ||
344 | return; | ||
345 | } | ||
327 | const serializedSVG = serializeSVG(svgDocument); | 346 | const serializedSVG = serializeSVG(svgDocument); |
328 | if (settings.format === 'png') { | 347 | if (settings.format === 'png') { |
329 | const png = await serializePNG(serializedSVG, svg, settings, theme); | 348 | const png = await serializePNG(serializedSVG, svg, settings, theme); |
diff --git a/subprojects/frontend/src/graph/export/open-sans-latin-bold.ttf b/subprojects/frontend/src/graph/export/open-sans-latin-bold.ttf new file mode 100644 index 00000000..7472d192 --- /dev/null +++ b/subprojects/frontend/src/graph/export/open-sans-latin-bold.ttf | |||
Binary files differ | |||
diff --git a/subprojects/frontend/src/graph/export/open-sans-latin-bold.ttf.license b/subprojects/frontend/src/graph/export/open-sans-latin-bold.ttf.license new file mode 100644 index 00000000..442f1821 --- /dev/null +++ b/subprojects/frontend/src/graph/export/open-sans-latin-bold.ttf.license | |||
@@ -0,0 +1,8 @@ | |||
1 | Copyright 2020 The Open Sans Project Authors (https://github.com/googlefonts/opensans) | ||
2 | |||
3 | SPDX-License-Identifier: OFL-1.1 | ||
4 | |||
5 | This file was derived from Open Sans v3.3 | ||
6 | (https://github.com/googlefonts/opensans/blob/bd7e37632246368c60fdcbd374dbf9bad11969b6/fonts/ttf/OpenSans-Bold.ttf) | ||
7 | using the Font Squirrel Web Font Generator (https://www.fontsquirrel.com/tools/webfont-generator) | ||
8 | with the Basic Subsetting setting to reduce the file size by retaining latin characters only. | ||
diff --git a/subprojects/frontend/src/graph/export/open-sans-latin-italic.ttf b/subprojects/frontend/src/graph/export/open-sans-latin-italic.ttf new file mode 100644 index 00000000..cecb0ce1 --- /dev/null +++ b/subprojects/frontend/src/graph/export/open-sans-latin-italic.ttf | |||
Binary files differ | |||
diff --git a/subprojects/frontend/src/graph/export/open-sans-latin-italic.ttf.license b/subprojects/frontend/src/graph/export/open-sans-latin-italic.ttf.license new file mode 100644 index 00000000..0657164a --- /dev/null +++ b/subprojects/frontend/src/graph/export/open-sans-latin-italic.ttf.license | |||
@@ -0,0 +1,8 @@ | |||
1 | Copyright 2020 The Open Sans Project Authors (https://github.com/googlefonts/opensans) | ||
2 | |||
3 | SPDX-License-Identifier: OFL-1.1 | ||
4 | |||
5 | This file was derived from Open Sans v3.3 | ||
6 | (https://github.com/googlefonts/opensans/blob/bd7e37632246368c60fdcbd374dbf9bad11969b6/fonts/ttf/OpenSans-Italic.ttf) | ||
7 | using the Font Squirrel Web Font Generator (https://www.fontsquirrel.com/tools/webfont-generator) | ||
8 | with the Basic Subsetting setting to reduce the file size by retaining latin characters only. | ||
diff --git a/subprojects/frontend/src/graph/export/open-sans-latin-regular.ttf b/subprojects/frontend/src/graph/export/open-sans-latin-regular.ttf new file mode 100644 index 00000000..46c0f716 --- /dev/null +++ b/subprojects/frontend/src/graph/export/open-sans-latin-regular.ttf | |||
Binary files differ | |||
diff --git a/subprojects/frontend/src/graph/export/open-sans-latin-regular.ttf.license b/subprojects/frontend/src/graph/export/open-sans-latin-regular.ttf.license new file mode 100644 index 00000000..8bc20e51 --- /dev/null +++ b/subprojects/frontend/src/graph/export/open-sans-latin-regular.ttf.license | |||
@@ -0,0 +1,8 @@ | |||
1 | Copyright 2020 The Open Sans Project Authors (https://github.com/googlefonts/opensans) | ||
2 | |||
3 | SPDX-License-Identifier: OFL-1.1 | ||
4 | |||
5 | This file was derived from Open Sans v3.3 | ||
6 | (https://github.com/googlefonts/opensans/blob/bd7e37632246368c60fdcbd374dbf9bad11969b6/fonts/ttf/OpenSans-Regular.ttf) | ||
7 | using the Font Squirrel Web Font Generator (https://www.fontsquirrel.com/tools/webfont-generator) | ||
8 | with the Basic Subsetting setting to reduce the file size by retaining latin characters only. | ||
diff --git a/subprojects/frontend/src/graph/export/serializePDF.ts b/subprojects/frontend/src/graph/export/serializePDF.ts new file mode 100644 index 00000000..75d1a4f4 --- /dev/null +++ b/subprojects/frontend/src/graph/export/serializePDF.ts | |||
@@ -0,0 +1,37 @@ | |||
1 | /* | ||
2 | * SPDX-FileCopyrightText: 2024 The Refinery Authors <https://refinery.tools/> | ||
3 | * | ||
4 | * SPDX-License-Identifier: EPL-2.0 | ||
5 | */ | ||
6 | |||
7 | import { jsPDF } from 'jspdf'; | ||
8 | import { svg2pdf } from 'svg2pdf.js'; | ||
9 | |||
10 | import boldFontURL from './open-sans-latin-bold.ttf?url'; | ||
11 | import italicFontURL from './open-sans-latin-italic.ttf?url'; | ||
12 | import normalFontURL from './open-sans-latin-regular.ttf?url'; | ||
13 | |||
14 | export default async function serializePDF( | ||
15 | svg: SVGSVGElement, | ||
16 | embedFonts: boolean, | ||
17 | ): Promise<Blob> { | ||
18 | const width = svg.width.baseVal.value; | ||
19 | const height = svg.height.baseVal.value; | ||
20 | // eslint-disable-next-line new-cap -- jsPDF uses a lowercase constructor. | ||
21 | const document = new jsPDF({ | ||
22 | orientation: width > height ? 'l' : 'p', | ||
23 | unit: 'px', | ||
24 | format: [width, height], | ||
25 | compress: true, | ||
26 | }); | ||
27 | if (embedFonts) { | ||
28 | document.addFont(normalFontURL, 'Open Sans', 'normal', 400); | ||
29 | document.addFont(italicFontURL, 'Open Sans', 'italic', 400); | ||
30 | document.addFont(boldFontURL, 'Open Sans', 'normal', 700); | ||
31 | } | ||
32 | const result = await svg2pdf(svg, document, { | ||
33 | width, | ||
34 | height, | ||
35 | }); | ||
36 | return result.output('blob'); | ||
37 | } | ||