Improve exporting PDF files.

Changed strategy from rendering the SVG to a canvas and then exporting
the canvas as an image, to directly exporting the SVG as an image.
This results in much smaller PDF files.
This commit is contained in:
Przemek Więch
2025-12-31 00:44:30 +01:00
parent 19b1ff3713
commit ad7eadb4fd
2 changed files with 40 additions and 17 deletions

View File

@@ -1,5 +1,9 @@
# Changelog # Changelog
## 2026-02-21
- Improved saving PDF files. Decreased file size and increased chart size that can be saved as PDF.
## 2026-02-13 ## 2026-02-13
- Show header information of the gedcom file on the side panel (by FrankBuchholz) - Show header information of the gedcom file on the side panel (by FrankBuchholz)

View File

@@ -1,6 +1,6 @@
import {max, min} from 'd3-array'; import { max, min } from 'd3-array';
import {interpolateNumber} from 'd3-interpolate'; import { interpolateNumber } from 'd3-interpolate';
import {select, Selection} from 'd3-selection'; import { select, Selection } from 'd3-selection';
import 'd3-transition'; import 'd3-transition';
import { import {
D3ZoomEvent, D3ZoomEvent,
@@ -9,9 +9,9 @@ import {
ZoomedElementBaseType, ZoomedElementBaseType,
zoomTransform, zoomTransform,
} from 'd3-zoom'; } from 'd3-zoom';
import {saveAs} from 'file-saver'; import { saveAs } from 'file-saver';
import {useEffect, useRef} from 'react'; import { useEffect, useRef } from 'react';
import {IntlShape, useIntl} from 'react-intl'; import { IntlShape, useIntl } from 'react-intl';
import { import {
ChartHandle, ChartHandle,
ChartInfo, ChartInfo,
@@ -25,9 +25,9 @@ import {
RelativesChart, RelativesChart,
ChartColors as TopolaChartColors, ChartColors as TopolaChartColors,
} from 'topola'; } from 'topola';
import {ChartColors, Ids, Sex} from './sidepanel/config/config'; import { ChartColors, Ids, Sex } from './sidepanel/config/config';
import {Media} from './util/media'; import { Media } from './util/media';
import {usePrevious} from './util/previous-hook'; import { usePrevious } from './util/previous-hook';
/** How much to zoom when using the +/- buttons. */ /** How much to zoom when using the +/- buttons. */
const ZOOM_FACTOR = 1.3; const ZOOM_FACTOR = 1.3;
@@ -154,12 +154,29 @@ function getStrippedSvg() {
return svg; return svg;
} }
function getSvgDimensions() {
const svg = document.getElementById('chartSvg')!;
return { width: Number(svg.getAttribute('width')), height: Number(svg.getAttribute('height')) };
}
function getSvgContents() { function getSvgContents() {
return new XMLSerializer().serializeToString(getStrippedSvg()); return new XMLSerializer().serializeToString(getStrippedSvg());
} }
async function getSvgContentsWithInlinedImages() { async function getSvgContentsWithInlinedImages() {
const svg = getStrippedSvg(); const svg = getStrippedSvg();
// Set white background because the default background of the SVG
// is transparent, which causes issues when printing or exporting to PDF.
const svgNs = 'http://www.w3.org/2000/svg';
const rect = document.createElementNS(svgNs, 'rect');
rect.setAttribute('x', '0');
rect.setAttribute('y', '0');
rect.setAttribute('width', '100%');
rect.setAttribute('height', '100%');
rect.setAttribute('fill', 'white');
svg.prepend(rect);
await inlineImages(svg); await inlineImages(svg);
return new XMLSerializer().serializeToString(svg); return new XMLSerializer().serializeToString(svg);
} }
@@ -186,13 +203,13 @@ export function printChart() {
export async function downloadSvg() { export async function downloadSvg() {
const contents = await getSvgContentsWithInlinedImages(); const contents = await getSvgContentsWithInlinedImages();
const blob = new Blob([contents], {type: 'image/svg+xml'}); const blob = new Blob([contents], { type: 'image/svg+xml' });
saveAs(blob, 'topola.svg'); saveAs(blob, 'topola.svg');
} }
async function drawOnCanvas(): Promise<HTMLCanvasElement> { async function drawOnCanvas(): Promise<HTMLCanvasElement> {
const contents = await getSvgContentsWithInlinedImages(); const contents = await getSvgContentsWithInlinedImages();
const blob = new Blob([contents], {type: 'image/svg+xml'}); const blob = new Blob([contents], { type: 'image/svg+xml' });
return drawImageOnCanvas(await loadImage(blob)); return drawImageOnCanvas(await loadImage(blob));
} }
@@ -204,14 +221,16 @@ export async function downloadPng() {
export async function downloadPdf() { export async function downloadPdf() {
// Lazy load jspdf. // Lazy load jspdf.
const {default: jspdf} = await import('jspdf'); const { default: jspdf } = await import('jspdf');
const canvas = await drawOnCanvas();
const {width, height} = getSvgDimensions();
const doc = new jspdf({ const doc = new jspdf({
orientation: canvas.width > canvas.height ? 'l' : 'p', orientation: width > height ? 'l' : 'p',
unit: 'pt', unit: 'pt',
format: [canvas.width, canvas.height], format: [width, height],
}); });
doc.addImage(canvas, 'PNG', 0, 0, canvas.width, canvas.height, 'NONE'); const contents = await getSvgContentsWithInlinedImages();
await doc.addSvgAsImage(contents, 0, 0, width, height);
doc.save('topola.pdf'); doc.save('topola.pdf');
} }
@@ -323,7 +342,7 @@ class ChartWrapper {
renderChart( renderChart(
props: ChartProps, props: ChartProps,
intl: IntlShape, intl: IntlShape,
args: {initialRender: boolean; resetPosition: boolean} = { args: { initialRender: boolean; resetPosition: boolean } = {
initialRender: false, initialRender: false,
resetPosition: false, resetPosition: false,
}, },