🐛(frontend) fix svg not rendering export dox

The svg was not rendering in the dox export.
We overwrite the default mapping to convert the
svg to png before rendering.
The images could be out of the page as well,
we fixed this issue by adding a maxWidth to the image.
This commit is contained in:
Anthony LC
2025-03-13 13:21:28 +01:00
committed by Anthony LC
parent 0405e6a3f6
commit 7007d56c38
5 changed files with 159 additions and 1 deletions

View File

@@ -19,6 +19,7 @@ and this project adheres to
## Fixed
- 🐛(frontend) SVG export #706
- 🐛(frontend) remove scroll listener table content #688
- 🔒️(back) restrict access to favorite_list endpoint #690
- 🐛(backend) refactor to fix filtering on children

View File

@@ -98,6 +98,7 @@ test.describe('Doc Export', () => {
test('it exports the doc to docx', async ({ page, browserName }) => {
const [randomDoc] = await createDoc(page, 'doc-editor', browserName, 1);
const fileChooserPromise = page.waitForEvent('filechooser');
const downloadPromise = page.waitForEvent('download', (download) => {
return download.suggestedFilename().includes(`${randomDoc}.docx`);
});
@@ -107,6 +108,18 @@ test.describe('Doc Export', () => {
await page.locator('.ProseMirror.bn-editor').click();
await page.locator('.ProseMirror.bn-editor').fill('Hello World');
await page.keyboard.press('Enter');
await page.locator('.bn-block-outer').last().fill('/');
await page.getByText('Resizable image with caption').click();
await page.getByText('Upload image').click();
const fileChooser = await fileChooserPromise;
await fileChooser.setFiles(path.join(__dirname, 'assets/test.svg'));
const image = page.getByRole('img', { name: 'test.svg' });
await expect(image).toBeVisible();
await page
.getByRole('button', {
name: 'download',

View File

@@ -0,0 +1,141 @@
import {
COLORS_DEFAULT,
DefaultProps,
UnreachableCaseError,
} from '@blocknote/core';
import {
IParagraphOptions,
ImageRun,
Paragraph,
ShadingType,
TextRun,
} from 'docx';
import { DocsExporterDocx } from '../types';
import { convertSvgToPng } from '../utils';
const MAX_WIDTH = 600;
export const blockMappingImageDocx: DocsExporterDocx['mappings']['blockMapping']['image'] =
async (block, exporter) => {
const blob = await exporter.resolveFile(block.props.url);
let pngConverted: string | undefined;
let dimensions: { width: number; height: number } | undefined;
if (blob.type.includes('svg')) {
const svgText = await blob.text();
pngConverted = await convertSvgToPng(svgText);
const img = new Image();
img.src = pngConverted;
await new Promise((resolve) => {
img.onload = () => {
dimensions = { width: img.width, height: img.height };
resolve(null);
};
});
} else {
dimensions = await getImageDimensions(blob);
}
if (!dimensions) {
return [];
}
const { width, height } = dimensions;
let previewWidth = block.props.previewWidth;
if (previewWidth > MAX_WIDTH) {
previewWidth = MAX_WIDTH;
}
return [
new Paragraph({
...blockPropsToStyles(block.props, exporter.options.colors),
children: [
new ImageRun({
data: pngConverted
? await (await fetch(pngConverted)).arrayBuffer()
: await blob.arrayBuffer(),
type: pngConverted ? 'png' : 'gif',
altText: block.props.caption
? {
description: block.props.caption,
name: block.props.caption,
title: block.props.caption,
}
: undefined,
transformation: {
width: previewWidth,
height: (previewWidth / width) * height,
},
}),
],
}),
...caption(block.props, exporter as DocsExporterDocx),
];
};
async function getImageDimensions(blob: Blob) {
if (typeof window !== 'undefined') {
const bmp = await createImageBitmap(blob);
const { width, height } = bmp;
bmp.close();
return { width, height };
}
}
function blockPropsToStyles(
props: Partial<DefaultProps>,
colors: typeof COLORS_DEFAULT,
): IParagraphOptions {
return {
shading:
props.backgroundColor === 'default' || !props.backgroundColor
? undefined
: {
type: ShadingType.SOLID,
color:
colors[
props.backgroundColor as keyof typeof colors
].background.slice(1),
},
run:
props.textColor === 'default' || !props.textColor
? undefined
: {
color: colors[props.textColor as keyof typeof colors].text.slice(1),
},
alignment:
!props.textAlignment || props.textAlignment === 'left'
? undefined
: props.textAlignment === 'center'
? 'center'
: props.textAlignment === 'right'
? 'right'
: props.textAlignment === 'justify'
? 'distribute'
: (() => {
throw new UnreachableCaseError(props.textAlignment);
})(),
};
}
function caption(
props: Partial<DefaultProps & { caption: string }>,
exporter: DocsExporterDocx,
) {
if (!props.caption) {
return [];
}
return [
new Paragraph({
...blockPropsToStyles(props, exporter.options.colors),
children: [
new TextRun({
text: props.caption,
}),
],
style: 'Caption',
}),
];
}

View File

@@ -1,6 +1,7 @@
export * from './dividerDocx';
export * from './dividerPDF';
export * from './headingPDF';
export * from './imageDocx';
export * from './imagePDF';
export * from './paragraphPDF';
export * from './quoteDocx';

View File

@@ -2,8 +2,9 @@ import { docxDefaultSchemaMappings } from '@blocknote/xl-docx-exporter';
import {
blockMappingDividerDocx,
blockMappingImageDocx,
blockMappingQuoteDocx,
} from './blocks-mapping/';
} from './blocks-mapping';
import { DocsExporterDocx } from './types';
export const docxDocsSchemaMappings: DocsExporterDocx['mappings'] = {
@@ -12,5 +13,6 @@ export const docxDocsSchemaMappings: DocsExporterDocx['mappings'] = {
...docxDefaultSchemaMappings.blockMapping,
divider: blockMappingDividerDocx,
quote: blockMappingQuoteDocx,
image: blockMappingImageDocx,
},
};