Skip to content

Commit 2b993e6

Browse files
authored
fix(docx): make image generation safe for MS Word (#753)
* fix(docx): make image generation safe for MS Word * chore: make createImage function for all ImageRuns * chore: clean * chore: clean * chore: clean * chore: validate images first
1 parent fd9442d commit 2b993e6

4 files changed

Lines changed: 103 additions & 135 deletions

File tree

utils/docx/bl/writeLocationTable.tsx

Lines changed: 30 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { cantonalMappings } from "@geops/tree-lib";
2-
import { BorderStyle, ImageRun, Paragraph, Table, TextRun } from "docx";
2+
import { BorderStyle, Paragraph, Table, TextRun } from "docx";
33

44
import Site from "@/components/ForestTypeModal/ForestTypeDescription/bl/Site";
55
import getImageHtml from "@/utils/getImageHtml";
@@ -10,27 +10,44 @@ import {
1010
getTilleringTreeTypes,
1111
soilIconTranslator,
1212
} from "../../../components/ForestTypeModal/ForestTypeDescription/bl/utils";
13-
import { getLocationTableRow, jsxToBlob, PAGE_WIDTH_DXA } from "../exportUtils";
13+
import {
14+
createPng,
15+
getLocationTableRow,
16+
jsxToBlob,
17+
PAGE_WIDTH_DXA,
18+
validateImage,
19+
} from "../exportUtils";
1420
import { writeDataTable } from "../writeDataTable";
1521

1622
import type { BlForestType } from "@geops/tree-lib/types";
1723

18-
// const { getImageHtml, getMapping, getReliefImageUrl } = utils();
19-
2024
const vegetationMapping = cantonalMappings?.bl?.vegetation;
2125

2226
const writeLocationTable = async (
2327
data: BlForestType,
2428
t: (key: string) => string,
2529
) => {
2630
const sitePng = await jsxToBlob(<Site data={data.expoandaspect} />);
27-
const imagePath = getReliefImageUrl(data.code, "bl");
28-
const imageHtml = imagePath && (await getImageHtml(imagePath));
29-
const imageBlob =
30-
imagePath &&
31-
(await fetch(imagePath)
32-
.then((response) => response.blob())
33-
.then((blob) => blob.arrayBuffer()));
31+
32+
const terrainImagePath = getReliefImageUrl(data.code, "bl");
33+
const terrainImageHtml = terrainImagePath
34+
? await getImageHtml(terrainImagePath)
35+
: null;
36+
const terrainImageBlob = terrainImagePath
37+
? await fetch(terrainImagePath)
38+
.then((response) => response.blob())
39+
.then((blob) => blob.arrayBuffer())
40+
: null;
41+
const terrainImage = validateImage(terrainImageBlob, terrainImageHtml)
42+
? createPng(
43+
terrainImageBlob,
44+
terrainImageHtml?.width ?? 0,
45+
terrainImageHtml?.height ?? 0,
46+
)
47+
: undefined;
48+
49+
const slopeAndExpoImage = createPng(sitePng, 120, 120);
50+
3451
const transitions = getValidForestTypes<BlForestType>(data.transitions, "bl");
3552
const borderConfig = {
3653
color: "e0e1e2",
@@ -110,34 +127,12 @@ const writeLocationTable = async (
110127
getLocationTableRow("Geologie", data.geology),
111128
getLocationTableRow(t("forestType.terrain"), [
112129
new Paragraph({
113-
children: [
114-
imageBlob
115-
? // @ts-expect-error Don't need fallback
116-
new ImageRun({
117-
data: imageBlob,
118-
transformation: {
119-
height: (imageHtml as HTMLImageElement)?.height,
120-
width: (imageHtml as HTMLImageElement)?.width,
121-
},
122-
})
123-
: new TextRun("-"),
124-
],
130+
children: terrainImage ? [terrainImage] : [new TextRun("-")],
125131
}),
126132
]),
127133
getLocationTableRow("Hangneigung & Exposition", [
128134
new Paragraph({
129-
children: [
130-
sitePng
131-
? // @ts-expect-error Don't need fallback
132-
new ImageRun({
133-
data: sitePng,
134-
transformation: {
135-
height: 120,
136-
width: 120,
137-
},
138-
})
139-
: new TextRun("-"),
140-
],
135+
children: slopeAndExpoImage ? [slopeAndExpoImage] : [new TextRun("-")],
141136
}),
142137
]),
143138
getLocationTableRow("Vegetation", data.vegetation),

utils/docx/exportUtils.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {
22
BorderStyle,
33
ExternalHyperlink,
44
HeadingLevel,
5+
ImageRun,
56
Paragraph,
67
ShadingType,
78
Table,
@@ -110,7 +111,7 @@ export const svgStringToBlob = async (string: string) => {
110111
fetch(uri)
111112
.then((res) => res.blob())
112113
.then((blob) => blob.arrayBuffer()),
113-
)) as Buffer;
114+
)) as ArrayBuffer;
114115
};
115116

116117
export const svgUriToBlob = async (dataUri: string) =>
@@ -121,6 +122,29 @@ export const svgUriToBlob = async (dataUri: string) =>
121122
export const jsxToBlob = (jsx: React.ReactNode) =>
122123
isSvg(renderToString(jsx)) ? svgStringToBlob(renderToString(jsx)) : null;
123124

125+
export const validateImage = (
126+
blob: ArrayBuffer | null,
127+
html: HTMLImageElement | null,
128+
) => {
129+
return !!blob && !!html;
130+
};
131+
132+
export const createPng = (
133+
buffer: ArrayBuffer | null | undefined,
134+
width = 25,
135+
height = 25,
136+
) =>
137+
buffer
138+
? new ImageRun({
139+
data: buffer,
140+
transformation: {
141+
height,
142+
width,
143+
},
144+
type: "png",
145+
})
146+
: undefined;
147+
124148
// Docx table helpers
125149
export const getPermalink = (text: string) =>
126150
new Paragraph({

utils/docx/lu/writeLocationTable.tsx

Lines changed: 37 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { cantonalMappings } from "@geops/tree-lib";
2-
import { ImageRun, Paragraph, Table, TextRun } from "docx";
2+
import { Paragraph, Table, TextRun } from "docx";
33

44
import getTilleringTreeTypes from "@/components/ForestTypeModal/ForestTypeDescription/lu/getTilleringTreeTypes";
55
import getImageHtml from "@/utils/getImageHtml";
@@ -8,7 +8,13 @@ import getReliefImageUrl from "@/utils/getReliefImageUrl";
88
import Site from "../../../components/ForestTypeModal/ForestTypeDescription/lu/Site";
99
import Tillering from "../../../components/ForestTypeModal/ForestTypeDescription/lu/Tillering";
1010
import TilleringSingle from "../../../components/ForestTypeModal/ForestTypeDescription/lu/TilleringSingle";
11-
import { getLocationTableRow, jsxToBlob, PAGE_WIDTH_DXA } from "../exportUtils";
11+
import {
12+
createPng,
13+
getLocationTableRow,
14+
jsxToBlob,
15+
PAGE_WIDTH_DXA,
16+
validateImage,
17+
} from "../exportUtils";
1218
import { writeDataTable } from "../writeDataTable";
1319

1420
import type { LuForestType } from "@geops/tree-lib/types";
@@ -23,49 +29,44 @@ const writeLocationTable = async (
2329
const tilleringHardwoodPng = await jsxToBlob(
2430
<TilleringSingle data={data.tilleringhardwood} />,
2531
);
32+
const tilleringHardwoodImage = createPng(tilleringHardwoodPng, 225, 30);
2633
const tilleringPng = await jsxToBlob(<Tillering data={data.tillering} />);
34+
const tilleringImage = createPng(
35+
tilleringPng,
36+
225,
37+
getTilleringTreeTypes(data.tillering).length * 19 + 30,
38+
);
39+
40+
const terrainImagePath = getReliefImageUrl(data.code, "lu", true);
41+
const terrainImageHtml = terrainImagePath
42+
? await getImageHtml(terrainImagePath)
43+
: null;
44+
const terrainImageBlob = terrainImagePath
45+
? await fetch(terrainImagePath)
46+
.then((response) => response.blob())
47+
.then((blob) => blob.arrayBuffer())
48+
: null;
49+
const imageWidth = terrainImageHtml ? terrainImageHtml.width / 3 : 0;
50+
const imageHeight = terrainImageHtml ? terrainImageHtml.height / 3 : 0;
51+
const terrainImage = validateImage(terrainImageBlob, terrainImageHtml)
52+
? createPng(terrainImageBlob, imageWidth, imageHeight)
53+
: undefined;
54+
2755
const sitePng = await jsxToBlob(<Site data={data.expoandaspect} />);
28-
const imagePath = getReliefImageUrl(data.code, "lu", true);
29-
const imageHtml = imagePath && (await getImageHtml(imagePath));
30-
const imageBlob =
31-
imagePath &&
32-
(await fetch(imagePath)
33-
.then((response) => response.blob())
34-
.then((blob) => blob.arrayBuffer()));
56+
const siteImage = createPng(sitePng, 120, 120);
3557

3658
const rows = [
3759
getLocationTableRow(t("lu.forestType.tilleringHardwood"), [
3860
new Paragraph({
39-
children: [
40-
tilleringHardwoodPng
41-
? // @ts-expect-error Don't need fallback
42-
new ImageRun({
43-
data: tilleringHardwoodPng,
44-
transformation: {
45-
height: 30,
46-
width: 225,
47-
},
48-
})
49-
: new TextRun("-"),
50-
],
61+
children: tilleringHardwoodImage
62+
? [tilleringHardwoodImage]
63+
: [new TextRun("-")],
5164
}),
5265
]),
5366
getLocationTableRow(t("lu.forestType.tillering"), [
5467
new Paragraph(""),
5568
new Paragraph({
56-
children: [
57-
tilleringPng
58-
? // @ts-expect-error Don't need fallback
59-
new ImageRun({
60-
data: tilleringPng,
61-
transformation: {
62-
height:
63-
getTilleringTreeTypes(data.tillering).length * 19 + 30,
64-
width: 225,
65-
},
66-
})
67-
: new TextRun("-"),
68-
],
69+
children: tilleringImage ? [tilleringImage] : [new TextRun("-")],
6970
}),
7071
new Paragraph(""),
7172
]),
@@ -99,18 +100,7 @@ const writeLocationTable = async (
99100
),
100101
getLocationTableRow(t("forestType.terrain"), [
101102
new Paragraph({
102-
children: [
103-
imageBlob
104-
? // @ts-expect-error Don't need fallback
105-
new ImageRun({
106-
data: imageBlob,
107-
transformation: {
108-
height: (imageHtml as HTMLImageElement).height / 3,
109-
width: (imageHtml as HTMLImageElement).width / 3,
110-
},
111-
})
112-
: new TextRun("-"),
113-
],
103+
children: terrainImage ? [terrainImage] : [new TextRun("-")],
114104
}),
115105
]),
116106
getLocationTableRow(
@@ -119,18 +109,7 @@ const writeLocationTable = async (
119109
)}`,
120110
[
121111
new Paragraph({
122-
children: [
123-
sitePng
124-
? // @ts-expect-error Don't need fallback
125-
new ImageRun({
126-
data: sitePng,
127-
transformation: {
128-
height: 120,
129-
width: 120,
130-
},
131-
})
132-
: new TextRun("-"),
133-
],
112+
children: siteImage ? [siteImage] : [new TextRun("-")],
134113
}),
135114
],
136115
),

utils/docx/writeRecommendationTable.tsx

Lines changed: 11 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { AlignmentType, ImageRun, Paragraph, Table, TableRow } from "docx";
1+
import { AlignmentType, Paragraph, Table, TableRow } from "docx";
22

33
import AttentionIcon from "@/components/icons/RecommendationAttention";
44
import NegativeIcon from "@/components/icons/RecommendationNegative";
@@ -10,6 +10,7 @@ import getSortedTreeTypes from "../getSortedTreeTypes";
1010

1111
import {
1212
cellIconPadding,
13+
createPng,
1314
getRecommendationTableCell,
1415
jsxToBlob,
1516
PAGE_WIDTH_DXA,
@@ -61,23 +62,19 @@ export const writeRecommendationTable = async (
6162
const neutralIcon = await jsxToBlob(<NeutralIcon color="#000000" />);
6263
const attentionIcon = await jsxToBlob(<AttentionIcon color="#000000" />);
6364

65+
const positiveRun = createPng(positiveIcon);
66+
const negativeRun = createPng(negativeIcon);
67+
const neutralRun = createPng(neutralIcon);
68+
const attentionRun = createPng(attentionIcon);
69+
6470
const rows = [
6571
new TableRow({
6672
children: [
6773
getRecommendationTableCell(
6874
[
6975
new Paragraph({
7076
alignment: AlignmentType.CENTER,
71-
children: [
72-
// @ts-expect-error Don't need fallback
73-
new ImageRun({
74-
data: positiveIcon ?? new ArrayBuffer(0),
75-
transformation: {
76-
height: 25,
77-
width: 25,
78-
},
79-
}),
80-
],
77+
children: positiveRun ? [positiveRun] : [],
8178
}),
8279
],
8380
cellIconPadding,
@@ -106,16 +103,7 @@ export const writeRecommendationTable = async (
106103
[
107104
new Paragraph({
108105
alignment: AlignmentType.CENTER,
109-
children: [
110-
// @ts-expect-error Don't need fallback
111-
new ImageRun({
112-
data: neutralIcon ?? new ArrayBuffer(0),
113-
transformation: {
114-
height: 25,
115-
width: 25,
116-
},
117-
}),
118-
],
106+
children: neutralRun ? [neutralRun] : [],
119107
}),
120108
],
121109
cellIconPadding,
@@ -144,16 +132,7 @@ export const writeRecommendationTable = async (
144132
[
145133
new Paragraph({
146134
alignment: AlignmentType.CENTER,
147-
children: [
148-
// @ts-expect-error Don't need fallback
149-
new ImageRun({
150-
data: negativeIcon ?? new ArrayBuffer(0),
151-
transformation: {
152-
height: 25,
153-
width: 25,
154-
},
155-
}),
156-
],
135+
children: negativeRun ? [negativeRun] : [],
157136
}),
158137
],
159138
cellIconPadding,
@@ -178,16 +157,7 @@ export const writeRecommendationTable = async (
178157
[
179158
new Paragraph({
180159
alignment: AlignmentType.CENTER,
181-
children: [
182-
// @ts-expect-error Don't need fallback
183-
new ImageRun({
184-
data: attentionIcon ?? new ArrayBuffer(0),
185-
transformation: {
186-
height: 25,
187-
width: 25,
188-
},
189-
}),
190-
],
160+
children: attentionRun ? [attentionRun] : [],
191161
}),
192162
],
193163
cellIconPadding,

0 commit comments

Comments
 (0)