@pdfme/pdf-lib 1.18.1 → 1.18.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/cjs/api/PDFDocument.js +195 -225
  2. package/cjs/api/PDFDocument.js.map +1 -1
  3. package/cjs/api/PDFEmbeddedFile.js +30 -33
  4. package/cjs/api/PDFEmbeddedFile.js.map +1 -1
  5. package/cjs/api/PDFEmbeddedPage.js +5 -7
  6. package/cjs/api/PDFEmbeddedPage.js.map +1 -1
  7. package/cjs/api/PDFFont.js +6 -8
  8. package/cjs/api/PDFFont.js.map +1 -1
  9. package/cjs/api/PDFImage.js +14 -16
  10. package/cjs/api/PDFImage.js.map +1 -1
  11. package/cjs/api/PDFJavaScript.js +19 -22
  12. package/cjs/api/PDFJavaScript.js.map +1 -1
  13. package/cjs/api/PDFPage.d.ts.map +1 -1
  14. package/cjs/api/PDFPage.js +26 -16
  15. package/cjs/api/PDFPage.js.map +1 -1
  16. package/cjs/api/PDFPageOptions.d.ts +14 -8
  17. package/cjs/api/PDFPageOptions.d.ts.map +1 -1
  18. package/cjs/api/PDFPageOptions.js.map +1 -1
  19. package/cjs/api/form/PDFField.js +1 -1
  20. package/cjs/api/form/PDFField.js.map +1 -1
  21. package/cjs/api/form/PDFForm.js +1 -1
  22. package/cjs/api/form/PDFForm.js.map +1 -1
  23. package/cjs/api/form/appearances.js +56 -16
  24. package/cjs/api/form/appearances.js.map +1 -1
  25. package/cjs/api/operations.d.ts +15 -0
  26. package/cjs/api/operations.d.ts.map +1 -1
  27. package/cjs/api/operations.js +22 -0
  28. package/cjs/api/operations.js.map +1 -1
  29. package/cjs/api/svg.d.ts +7 -1
  30. package/cjs/api/svg.d.ts.map +1 -1
  31. package/cjs/api/svg.js +332 -1016
  32. package/cjs/api/svg.js.map +1 -1
  33. package/cjs/core/PDFContext.js +11 -2
  34. package/cjs/core/PDFContext.js.map +1 -1
  35. package/cjs/core/embedders/CustomFontEmbedder.js +62 -74
  36. package/cjs/core/embedders/CustomFontEmbedder.js.map +1 -1
  37. package/cjs/core/embedders/CustomFontSubsetEmbedder.js +3 -5
  38. package/cjs/core/embedders/CustomFontSubsetEmbedder.js.map +1 -1
  39. package/cjs/core/embedders/FileEmbedder.js +30 -32
  40. package/cjs/core/embedders/FileEmbedder.js.map +1 -1
  41. package/cjs/core/embedders/JavaScriptEmbedder.js +12 -14
  42. package/cjs/core/embedders/JavaScriptEmbedder.js.map +1 -1
  43. package/cjs/core/embedders/JpegEmbedder.js +54 -59
  44. package/cjs/core/embedders/JpegEmbedder.js.map +1 -1
  45. package/cjs/core/embedders/PDFPageEmbedder.js +22 -26
  46. package/cjs/core/embedders/PDFPageEmbedder.js.map +1 -1
  47. package/cjs/core/embedders/PngEmbedder.js +20 -25
  48. package/cjs/core/embedders/PngEmbedder.js.map +1 -1
  49. package/cjs/core/parser/PDFObjectStreamParser.js +15 -17
  50. package/cjs/core/parser/PDFObjectStreamParser.js.map +1 -1
  51. package/cjs/core/parser/PDFParser.js +66 -74
  52. package/cjs/core/parser/PDFParser.js.map +1 -1
  53. package/cjs/core/writers/PDFStreamWriter.js +53 -55
  54. package/cjs/core/writers/PDFStreamWriter.js.map +1 -1
  55. package/cjs/core/writers/PDFWriter.js +62 -66
  56. package/cjs/core/writers/PDFWriter.js.map +1 -1
  57. package/cjs/types/index.d.ts +4 -4
  58. package/cjs/types/index.d.ts.map +1 -1
  59. package/dist/pdf-lib.esm.js +1123 -2589
  60. package/dist/pdf-lib.esm.js.map +1 -1
  61. package/dist/pdf-lib.esm.min.js +1 -15
  62. package/dist/pdf-lib.esm.min.js.map +1 -1
  63. package/dist/pdf-lib.js +1123 -2589
  64. package/dist/pdf-lib.js.map +1 -1
  65. package/dist/pdf-lib.min.js +1 -15
  66. package/dist/pdf-lib.min.js.map +1 -1
  67. package/es/api/PDFDocument.js +195 -226
  68. package/es/api/PDFDocument.js.map +1 -1
  69. package/es/api/PDFEmbeddedFile.js +30 -33
  70. package/es/api/PDFEmbeddedFile.js.map +1 -1
  71. package/es/api/PDFEmbeddedPage.js +5 -8
  72. package/es/api/PDFEmbeddedPage.js.map +1 -1
  73. package/es/api/PDFFont.js +6 -9
  74. package/es/api/PDFFont.js.map +1 -1
  75. package/es/api/PDFImage.js +14 -17
  76. package/es/api/PDFImage.js.map +1 -1
  77. package/es/api/PDFJavaScript.js +19 -22
  78. package/es/api/PDFJavaScript.js.map +1 -1
  79. package/es/api/PDFPage.d.ts.map +1 -1
  80. package/es/api/PDFPage.js +26 -17
  81. package/es/api/PDFPage.js.map +1 -1
  82. package/es/api/PDFPageOptions.d.ts +14 -8
  83. package/es/api/PDFPageOptions.d.ts.map +1 -1
  84. package/es/api/PDFPageOptions.js.map +1 -1
  85. package/es/api/form/PDFField.js +1 -1
  86. package/es/api/form/PDFField.js.map +1 -1
  87. package/es/api/form/PDFForm.js +1 -1
  88. package/es/api/form/PDFForm.js.map +1 -1
  89. package/es/api/form/appearances.js +56 -16
  90. package/es/api/form/appearances.js.map +1 -1
  91. package/es/api/operations.d.ts +15 -0
  92. package/es/api/operations.d.ts.map +1 -1
  93. package/es/api/operations.js +23 -1
  94. package/es/api/operations.js.map +1 -1
  95. package/es/api/svg.d.ts +7 -1
  96. package/es/api/svg.d.ts.map +1 -1
  97. package/es/api/svg.js +333 -1017
  98. package/es/api/svg.js.map +1 -1
  99. package/es/core/PDFContext.js +11 -2
  100. package/es/core/PDFContext.js.map +1 -1
  101. package/es/core/embedders/CustomFontEmbedder.js +62 -75
  102. package/es/core/embedders/CustomFontEmbedder.js.map +1 -1
  103. package/es/core/embedders/CustomFontSubsetEmbedder.js +3 -6
  104. package/es/core/embedders/CustomFontSubsetEmbedder.js.map +1 -1
  105. package/es/core/embedders/FileEmbedder.js +30 -33
  106. package/es/core/embedders/FileEmbedder.js.map +1 -1
  107. package/es/core/embedders/JavaScriptEmbedder.js +12 -15
  108. package/es/core/embedders/JavaScriptEmbedder.js.map +1 -1
  109. package/es/core/embedders/JpegEmbedder.js +54 -59
  110. package/es/core/embedders/JpegEmbedder.js.map +1 -1
  111. package/es/core/embedders/PDFPageEmbedder.js +22 -27
  112. package/es/core/embedders/PDFPageEmbedder.js.map +1 -1
  113. package/es/core/embedders/PngEmbedder.js +20 -25
  114. package/es/core/embedders/PngEmbedder.js.map +1 -1
  115. package/es/core/parser/PDFObjectStreamParser.js +15 -18
  116. package/es/core/parser/PDFObjectStreamParser.js.map +1 -1
  117. package/es/core/parser/PDFParser.js +66 -75
  118. package/es/core/parser/PDFParser.js.map +1 -1
  119. package/es/core/writers/PDFStreamWriter.js +53 -56
  120. package/es/core/writers/PDFStreamWriter.js.map +1 -1
  121. package/es/core/writers/PDFWriter.js +62 -67
  122. package/es/core/writers/PDFWriter.js.map +1 -1
  123. package/es/types/index.d.ts +4 -4
  124. package/es/types/index.d.ts.map +1 -1
  125. package/package.json +1 -1
  126. package/src/api/PDFPage.ts +12 -0
  127. package/src/api/PDFPageOptions.ts +14 -8
  128. package/src/api/operations.ts +45 -3
  129. package/src/api/svg.ts +305 -1086
  130. package/src/types/index.ts +6 -1
  131. package/ts3.4/cjs/api/PDFPageOptions.d.ts +14 -8
  132. package/ts3.4/cjs/api/operations.d.ts +15 -0
  133. package/ts3.4/cjs/api/svg.d.ts +7 -1
  134. package/ts3.4/cjs/types/index.d.ts +4 -4
  135. package/ts3.4/es/api/PDFPageOptions.d.ts +14 -8
  136. package/ts3.4/es/api/operations.d.ts +15 -0
  137. package/ts3.4/es/api/svg.d.ts +7 -1
  138. package/ts3.4/es/types/index.d.ts +4 -4
package/src/api/svg.ts CHANGED
@@ -9,17 +9,13 @@ import { Color, colorString } from './colors';
9
9
  import {
10
10
  Degrees,
11
11
  degreesToRadians,
12
- RotationTypes,
13
- degrees,
14
- radiansToDegrees,
15
12
  } from './rotations';
16
13
  import PDFFont from './PDFFont';
17
14
  import PDFPage from './PDFPage';
18
15
  import { PDFPageDrawSVGElementOptions } from './PDFPageOptions';
19
16
  import { LineCapStyle, LineJoinStyle, FillRule } from './operators';
20
- import { Rectangle, Point, Segment, Ellipse } from '../utils/elements';
21
- import { getIntersections } from '../utils/intersections';
22
- import { distanceCoords, isEqual, distance, rotate, angle, minus } from '../utils/maths';
17
+ import { TransformationMatrix, identityMatrix } from '../types/matrix'
18
+ import { Coordinates, Space } from '../types';
23
19
 
24
20
  interface Position {
25
21
  x: number;
@@ -33,11 +29,6 @@ interface Size {
33
29
 
34
30
  type Box = Position & Size;
35
31
 
36
- interface SVGSizeConverter {
37
- point: (x: number, y: number) => Position;
38
- size: (w: number, h: number) => Size;
39
- }
40
-
41
32
  type SVGStyle = Record<string, string>;
42
33
 
43
34
  type InheritedAttributes = {
@@ -85,34 +76,89 @@ type SVGAttributes = {
85
76
  points?: string;
86
77
  };
87
78
 
79
+ type TransformAttributes = {
80
+ matrix: TransformationMatrix
81
+ clipSpaces: Space[]
82
+ }
83
+
88
84
  export type SVGElement = HTMLElement & {
89
- svgAttributes: InheritedAttributes & SVGAttributes;
85
+ svgAttributes: InheritedAttributes & SVGAttributes & TransformAttributes;
90
86
  };
91
87
 
92
88
  interface SVGElementToDrawMap {
93
89
  [cmd: string]: (a: SVGElement) => Promise<void>;
94
90
  }
95
- /**
96
- * take an array of T and turn it into an 2D-array where each sub array has n elements
97
- * ex: [1,2,3,4] -> [[1,2], [3, 4]]
98
- * @param arr the array of elements
99
- * @param n the size of each sub array
100
- */
101
- const groupBy = <T>(arr: T[], n: number) => {
102
- if (arr.length <= n) return [arr];
103
- return arr?.reduce((acc, curr, i) => {
104
- const index = Math.floor(i / n);
105
- if (i % n) {
106
- acc[index].push(curr);
107
- } else {
108
- acc.push([curr]);
91
+
92
+ const combineMatrix = (
93
+ [a, b, c, d, e, f]: TransformationMatrix,
94
+ [a2, b2, c2, d2, e2, f2]: TransformationMatrix
95
+ ): TransformationMatrix => [
96
+ a * a2 + c * b2,
97
+ b * a2 + d * b2,
98
+ a * c2 + c * d2,
99
+ b * c2 + d * d2,
100
+ a * e2 + c * f2 + e,
101
+ b * e2 + d * f2 + f,
102
+ ]
103
+
104
+ const applyTransformation = ([a, b, c, d, e, f]: TransformationMatrix, { x, y }: Coordinates): Coordinates => ({
105
+ x: a * x + c * y + e,
106
+ y: b * x + d * y + f
107
+ })
108
+
109
+
110
+ type TransformationName = 'scale' | 'scaleX' | 'scaleY' | 'translate' | 'translateX' | 'translateY' | 'rotate' | 'skewX' | 'skewY' | 'matrix'
111
+ const transformationToMatrix = (name: TransformationName, args: number[]) : TransformationMatrix => {
112
+ switch (name) {
113
+ case 'scale':
114
+ case 'scaleX':
115
+ case 'scaleY': {
116
+ // [sx 0 0 sy 0 0]
117
+ const [sx, sy = sx] = args
118
+ return [name === 'scaleY' ? 1 : sx, 0, 0, name === 'scaleX' ? 1 : sy, 0, 0]
109
119
  }
110
- return acc;
111
- }, [] as T[][]);
112
- };
120
+ case 'translate':
121
+ case 'translateX':
122
+ case 'translateY': {
123
+ // [1 0 0 1 tx ty]
124
+ const [tx, ty = tx] = args
125
+ // -ty is necessary because the pdf's y axis is inverted
126
+ return [1, 0, 0, 1, name === 'translateY' ? 0 : tx, name === 'translateX' ? 0 : -ty]
127
+ }
128
+ case 'rotate': {
129
+ // [cos(a) sin(a) -sin(a) cos(a) 0 0]
130
+ const [a, x = 0, y = 0] = args
131
+ const t1 = transformationToMatrix('translate', [x, y])
132
+ const t2 = transformationToMatrix('translate', [-x, -y])
133
+ // -args[0] -> the '-' operator is necessary because the pdf rotation system is inverted
134
+ const aRadians = degreesToRadians(-a)
135
+ const r: TransformationMatrix = [Math.cos(aRadians), Math.sin(aRadians), -Math.sin(aRadians), Math.cos(aRadians), 0, 0]
136
+ // rotation around a point is the combination of: translate * rotate * (-translate)
137
+ return combineMatrix(combineMatrix(t1, r), t2)
138
+ }
139
+ case 'skewY':
140
+ case 'skewX': {
141
+ // [1 tan(a) 0 1 0 0]
142
+ // [1 0 tan(a) 1 0 0]
143
+ // -args[0] -> the '-' operator is necessary because the pdf rotation system is inverted
144
+ const a = degreesToRadians(-args[0])
145
+ const skew = Math.tan(a)
146
+ const skewX = name === 'skewX' ? skew : 0
147
+ const skewY = name === 'skewY' ? skew : 0
148
+ return [1, skewY, skewX, 1, 0, 0]
149
+ }
150
+ case 'matrix': {
151
+ const [a, b, c, d, e, f] = args
152
+ const r = transformationToMatrix('scale', [1, -1])
153
+ const m: TransformationMatrix = [a, b, c, d, e, f]
154
+ return combineMatrix(combineMatrix(r, m), r)
155
+ }
156
+ default:
157
+ return identityMatrix
158
+ }
159
+ }
113
160
 
114
- const isCoordinateInsideTheRect = (dot: Point, rect: Rectangle) =>
115
- isEqual(0, distance(dot, rect.orthoProjection(dot)));
161
+ const combineTransformation = (matrix: TransformationMatrix, name: TransformationName, args: number[]) => combineMatrix(matrix, transformationToMatrix(name, args))
116
162
 
117
163
  const StrokeLineCapMap: Record<string, LineCapStyle> = {
118
164
  butt: LineCapStyle.Butt,
@@ -131,538 +177,6 @@ const StrokeLineJoinMap: Record<string, LineJoinStyle> = {
131
177
  round: LineJoinStyle.Round,
132
178
  };
133
179
 
134
- const getInnerSegment = (start: Point, end: Point, rect: Rectangle) => {
135
- const isStartInside = isCoordinateInsideTheRect(start, rect);
136
- const isEndInside = isCoordinateInsideTheRect(end, rect);
137
- let resultLineStart = start;
138
- let resultLineEnd = end;
139
- // it means that the segment is already inside the rect
140
- if (isEndInside && isStartInside) return new Segment(start, end);
141
-
142
- const line = new Segment(start, end);
143
- const intersection = getIntersections([rect, line]);
144
-
145
- // if there's no intersection it means that the line doesn't intersects the svgRect and isn't visible
146
- if (intersection.length === 0) return;
147
-
148
- if (!isStartInside) {
149
- // replace the line start point by the nearest intersection
150
- const nearestPoint = intersection.sort(
151
- (p1, p2) => distanceCoords(start, p1) - distanceCoords(start, p2),
152
- )[0];
153
- resultLineStart = new Point(nearestPoint);
154
- }
155
-
156
- if (!isEndInside) {
157
- // replace the line start point by the nearest intersection
158
- const nearestPoint = intersection.sort(
159
- (p1, p2) => distanceCoords(end, p1) - distanceCoords(end, p2),
160
- )[0];
161
- resultLineEnd = new Point(nearestPoint);
162
- }
163
-
164
- return new Segment(resultLineStart, resultLineEnd);
165
- };
166
-
167
- const cropSvgElement = (
168
- svgRect: Rectangle,
169
- element: SVGElement,
170
- ): SVGElement => {
171
- switch (element.tagName) {
172
- case 'text': {
173
- const fontSize = element.svgAttributes.fontSize || 12;
174
- // TODO: compute the right font boundaries to know which characters should be drawn
175
- // this is an workaround to draw text that are just a little outside the viewbox boundaries
176
- const start = new Point({
177
- x: element.svgAttributes.x || 0,
178
- y: element.svgAttributes.y || 0,
179
- });
180
- const paddingRect = new Rectangle(
181
- new Point({
182
- x: svgRect.start.x - fontSize,
183
- y: svgRect.start.y + fontSize,
184
- }),
185
- new Point({ x: svgRect.end.x + fontSize, y: svgRect.end.y - fontSize }),
186
- );
187
- if (!isCoordinateInsideTheRect(start, paddingRect)) {
188
- element.set_content('');
189
- }
190
- break;
191
- }
192
- case 'line': {
193
- const start = new Point({
194
- x: element.svgAttributes.x1!,
195
- y: element.svgAttributes.y1!,
196
- });
197
-
198
- const end = new Point({
199
- x: element.svgAttributes.x2!,
200
- y: element.svgAttributes.y2!,
201
- });
202
- const line = getInnerSegment(start, end, svgRect);
203
- element.svgAttributes.x1 = line ? line.A.x : 0;
204
- element.svgAttributes.x2 = line ? line.B.x : 0;
205
- element.svgAttributes.y1 = line ? line.A.y : 0;
206
- element.svgAttributes.y2 = line ? line.B.y : 0;
207
- break;
208
- }
209
- case 'path': {
210
- // the path origin coordinate
211
- const basePoint = new Point({
212
- x: element.svgAttributes.x || 0,
213
- y: element.svgAttributes.y || 0,
214
- });
215
- const normalizePoint = (p: Point) =>
216
- new Point({ x: p.x - basePoint.x, y: p.y - basePoint.y });
217
- /**
218
- *
219
- * @param origin is the origin of the current drawing in the page coordinate system
220
- * @param command the path instruction
221
- * @param params the instruction params
222
- * @returns the point where the next instruction starts and the new instruction text
223
- */
224
- const handlePath = (origin: Point, command: string, params: number[]) => {
225
- switch (command) {
226
- case 'm':
227
- case 'M': {
228
- const isLocalInstruction = command === command.toLocaleLowerCase();
229
- const nextPoint = new Point({
230
- x: (isLocalInstruction ? origin.x : basePoint.x) + params[0],
231
- y: (isLocalInstruction ? origin.y : basePoint.y) + params[1],
232
- });
233
- return {
234
- point: nextPoint,
235
- command: `${command}${params[0]},${params[1]}`,
236
- };
237
- }
238
- case 'v':
239
- case 'V':
240
- case 'h':
241
- case 'H':
242
- case 'l':
243
- case 'L': {
244
- const isLocalInstruction = ['l', 'v', 'h'].includes(command);
245
- const getNextPoint = () => {
246
- switch (command.toLocaleLowerCase()) {
247
- case 'l':
248
- return new Point({
249
- x:
250
- (isLocalInstruction ? origin.x : basePoint.x) + params[0],
251
- y:
252
- (isLocalInstruction ? origin.y : basePoint.y) + params[1],
253
- });
254
- case 'v':
255
- return new Point({
256
- x: origin.x,
257
- y:
258
- (isLocalInstruction ? origin.y : basePoint.y) + params[0],
259
- });
260
- case 'h':
261
- return new Point({
262
- x:
263
- (isLocalInstruction ? origin.x : basePoint.x) + params[0],
264
- y: origin.y,
265
- });
266
- default:
267
- return new Point({
268
- x: 0,
269
- y: 0,
270
- });
271
- }
272
- };
273
- const nextPoint = getNextPoint();
274
- const normalizedNext = normalizePoint(nextPoint);
275
-
276
- let endPoint = new Point({ x: nextPoint.x, y: nextPoint.y });
277
- let startPoint = new Point({ x: origin.x, y: origin.y });
278
- const result = getInnerSegment(startPoint, endPoint, svgRect);
279
- if (!result) {
280
- return {
281
- point: nextPoint,
282
- command: `M${normalizedNext.x},${normalizedNext.y}`,
283
- };
284
- }
285
- // if the point wasn't moved it means that it's inside the rect
286
- const isStartInside = result.A.isEqual(startPoint);
287
- const isEndInside = result.B.isEqual(endPoint);
288
-
289
- // the intersection points are referencing the pdf coordinates, it's necessary to convert these points to the path's origin point
290
- endPoint = normalizePoint(new Point(result.B.toCoords()));
291
- startPoint = normalizePoint(new Point(result.A.toCoords()));
292
- const startInstruction = isStartInside
293
- ? ''
294
- : `M${startPoint.x},${startPoint.y}`;
295
- const endInstruction = isEndInside
296
- ? ''
297
- : `M${normalizedNext.x},${normalizedNext.y}`
298
- return {
299
- point: nextPoint,
300
- command: `${startInstruction} L${endPoint.x},${endPoint.y} ${endInstruction} `,
301
- };
302
- }
303
- case 'a':
304
- case 'A': {
305
- const isLocalInstruction = command === 'a';
306
- const [, , , , , x, y] = params;
307
- const nextPoint = new Point({
308
- x: (isLocalInstruction ? origin.x : basePoint.x) + x,
309
- y: (isLocalInstruction ? origin.y : basePoint.y) + y,
310
- });
311
- // TODO: implement the code to fit the Elliptical Arc Curve instructions into the viewbox
312
- return {
313
- point: nextPoint,
314
- command: `${command} ${params.map((p) => `${p}`).join()}`,
315
- };
316
- }
317
- case 'c':
318
- case 'C': {
319
- const isLocalInstruction = command === 'c';
320
-
321
- let x = 0;
322
- let y = 0;
323
-
324
- for (
325
- let pendingParams = params;
326
- pendingParams.length > 0;
327
- pendingParams = pendingParams.slice(6)
328
- ) {
329
- const [, , , , pendingX, pendingY] = pendingParams;
330
- if (isLocalInstruction) {
331
- x += pendingX;
332
- y += pendingY;
333
- } else {
334
- x = pendingX;
335
- y = pendingY;
336
- }
337
- }
338
-
339
- const nextPoint = new Point({
340
- x: (isLocalInstruction ? origin.x : basePoint.x) + x,
341
- y: (isLocalInstruction ? origin.y : basePoint.y) + y,
342
- });
343
- // TODO: implement the code to fit the Cubic Bézier Curve instructions into the viewbox
344
- return {
345
- point: nextPoint,
346
- command: `${command} ${params.map((p) => `${p}`).join()}`,
347
- };
348
- }
349
- case 's':
350
- case 'S':
351
- const isLocalInstruction = command === 's';
352
-
353
- let x = 0;
354
- let y = 0;
355
-
356
- for (
357
- let pendingParams = params;
358
- pendingParams.length > 0;
359
- pendingParams = pendingParams.slice(4)
360
- ) {
361
- const [, , pendingX, pendingY] = pendingParams;
362
- x += pendingX;
363
- y += pendingY;
364
- }
365
-
366
- const nextPoint = new Point({
367
- x: (isLocalInstruction ? origin.x : basePoint.x) + x,
368
- y: (isLocalInstruction ? origin.y : basePoint.y) + y,
369
- });
370
-
371
- return {
372
- point: nextPoint,
373
- command: `${command} ${params.map((p) => `${p}`).join()}`,
374
- };
375
- case 'q':
376
- case 'Q': {
377
- const isLocalInstruction = command === 'q';
378
- const [, , x, y] = params;
379
- const nextPoint = new Point({
380
- x: (isLocalInstruction ? origin.x : basePoint.x) + x,
381
- y: (isLocalInstruction ? origin.y : basePoint.y) + y,
382
- });
383
- // TODO: implement the code to fit the Quadratic Bézier Curve instructions into the viewbox
384
- return {
385
- point: nextPoint,
386
- command: `${command} ${params.map((p) => `${p}`).join()}`,
387
- };
388
- }
389
- // TODO: Handle the remaining svg instructions: t,q
390
- default:
391
- return {
392
- point: origin,
393
- command: `${command} ${params.map((p) => `${p}`).join()}`,
394
- };
395
- }
396
- };
397
-
398
- const commands = element.svgAttributes.d?.match(
399
- /(v|h|a|l|t|m|q|c|s|z)([0-9,e\s.-]*)/gi,
400
- );
401
- let currentPoint = new Point({ x: basePoint.x, y: basePoint.y });
402
- const newPath = commands
403
- ?.map((command) => {
404
- const letter = command.match(/[a-z]/i)?.[0];
405
- const params = command
406
- .match(
407
- /(-?[0-9]+\.[0-9]+(e[+-]?[0-9]+)?)|(-?\.[0-9]+(e[+-]?[0-9]+)?)|(-?[0-9]+)/gi,
408
- )
409
- ?.filter((m) => m !== '')
410
- .map((v) => parseFloat(v));
411
- if (letter && params) {
412
- const result = handlePath(currentPoint, letter, params);
413
- if (result) {
414
- currentPoint = result.point;
415
- return result.command;
416
- }
417
- }
418
- return command;
419
- })
420
- .join(' ');
421
- element.svgAttributes.d = newPath;
422
- break;
423
- }
424
- case 'ellipse':
425
- case 'circle': {
426
- if (
427
- element.svgAttributes.cx === undefined ||
428
- element.svgAttributes.cy === undefined ||
429
- element.svgAttributes.rx === undefined ||
430
- element.svgAttributes.ry === undefined
431
- ) {
432
- break;
433
- }
434
- const { cx = 0, cy = 0, rx = 0, ry = 0 } = element.svgAttributes;
435
- const center = new Point({
436
- x: cx,
437
- y: cy,
438
- });
439
- const rotation = element.svgAttributes.rotation?.angle || 0;
440
- // these points are relative to the ellipse's center
441
- const a = new Point(rotate({ x: -rx, y: 0 }, degreesToRadians(rotation)));
442
- const b = new Point(rotate({ x: rx, y: 0 }, degreesToRadians(rotation)));
443
- const c = new Point(rotate({ x: 0, y: ry }, degreesToRadians(rotation)));
444
- // these points are relative to the real coordinate system
445
- const A = center.plus(a);
446
- const B = center.plus(b);
447
- const C = center.plus(c);
448
- const ellipse = new Ellipse(A, B, C);
449
- const intersections = getIntersections([svgRect, ellipse]);
450
- const isCenterInsideRect = isCoordinateInsideTheRect(center, svgRect);
451
- /**
452
- * if there are less than 2 intersection, there are two possibilities:
453
- * - the ellipse is outside the viewbox and therefore isn't visible
454
- * - the ellipse is inside the viewbox and don't need to be cropped
455
- */
456
- if (intersections.length < 2) {
457
- !isCenterInsideRect && element.setAttribute('rx', '0');
458
- !isCenterInsideRect && (element.svgAttributes.rx = 0);
459
- !isCenterInsideRect && element.setAttribute('ry', '0');
460
- !isCenterInsideRect && (element.svgAttributes.ry = 0);
461
- break;
462
- }
463
-
464
- // viewbox rectangle coordinates
465
- const P1 = new Point(svgRect.getCoords());
466
- const P3 = new Point(svgRect.getEnd());
467
- const P2 = new Point({ x: P3.x, y: P1.y });
468
- const P4 = new Point({ x: P1.x, y: P3.y });
469
- const top = new Segment(P1, P2);
470
- const right = new Segment(P2, P3);
471
- const bottom = new Segment(P3, P4);
472
- const left = new Segment(P4, P1);
473
- // Warning: keep the order of the segments, it's important when building the path that will represent the ellipse
474
- const rectSegments = [top, right, bottom, left];
475
-
476
- const isPointInsideEllipse = (P: Point) =>
477
- (P.x - cx) ** 2 / rx ** 2 + (P.y - cy) ** 2 / ry ** 2 <= 1;
478
- // check if the rect boundaries are inside the circle
479
- const isRectInsideEllipse =
480
- isPointInsideEllipse(P1) &&
481
- isPointInsideEllipse(P2) &&
482
- isPointInsideEllipse(P3) &&
483
- isPointInsideEllipse(P4);
484
-
485
- // the segments that are intersecting the circle. And, therefore, are lines that are cropping the drawing
486
- const circleSegments = isRectInsideEllipse
487
- ? rectSegments
488
- : rectSegments.map((segment, i) => {
489
- const [p1, p2] = getIntersections([segment, ellipse])
490
- // it's important to sort the segment's point because it impacts the angle of the arc, the points are sorted on clockwise direction
491
- .sort((p1, p2) => {
492
- // top
493
- if (i === 0) {
494
- return p1.x - p2.x;
495
- // right
496
- } else if (i === 1) {
497
- return p2.y - p1.y;
498
- // bottom
499
- } else if (i === 2) {
500
- return p2.x - p1.x;
501
- // left
502
- } else {
503
- return p1.y - p2.y;
504
- }
505
- });
506
-
507
- if (p1 && p2) {
508
- return new Segment(new Point(p1), new Point(p2));
509
- // if the other point isn't inside the circle it means that the circle isn't cropped by the segment
510
- } else if (
511
- p1 &&
512
- (isPointInsideEllipse(segment.A) ||
513
- isPointInsideEllipse(segment.B))
514
- ) {
515
- const intersectionPoint = new Point(p1);
516
- const innerPoint = isPointInsideEllipse(segment.A)
517
- ? segment.A
518
- : segment.B;
519
- // ensures that the segment is always following the clockwise direction
520
- const start = innerPoint.isEqual(segment.A)
521
- ? innerPoint
522
- : intersectionPoint;
523
- const end = innerPoint.isEqual(segment.A)
524
- ? intersectionPoint
525
- : innerPoint;
526
- return new Segment(start, end);
527
- // if there's no intersection and the segment's points are inside the Ellipse it means that the segment should be drawn as part of the ellipse
528
- } else if (
529
- !(p1 && p2) &&
530
- isPointInsideEllipse(segment.A) &&
531
- isPointInsideEllipse(segment.B)
532
- ) {
533
- return segment;
534
- }
535
- return;
536
- });
537
-
538
- const inverseAngle = (angle: number) => (360 - angle) % 360;
539
- const pointsAngle = (
540
- p1: Point,
541
- p2: Point,
542
- direction: 'clockwise' | 'counter-clockwise' = 'clockwise',
543
- ) => {
544
- const startAngle = radiansToDegrees(
545
- Math.atan2(p1.y - center.y, p1.x - center.x),
546
- );
547
- const endAngle = radiansToDegrees(
548
- Math.atan2(p2.y - center.y, p2.x - center.x),
549
- );
550
- const arcAngle = (endAngle + (360 - startAngle)) % 360;
551
- return direction === 'clockwise' ? arcAngle : inverseAngle(arcAngle);
552
- };
553
-
554
- /**
555
- * - draw a line for each segment
556
- * - if two segments aren't connected draw an arc connecting them
557
- */
558
- let startPoint: Point | undefined;
559
- // the point where the pen is located
560
- let currentPoint: Point | undefined;
561
- let lastSegment: Segment | undefined;
562
- let path = circleSegments.reduce((path, segment) => {
563
- if (!segment) return path;
564
- if (!startPoint) {
565
- startPoint = segment.A;
566
- path = `M ${segment.A.x},${segment.A.y}`;
567
- }
568
- // if the current segment isn't connected to the last one, connect both with an arc
569
- if (lastSegment && !lastSegment.B.isEqual(segment.A)) {
570
- const arcAngle = pointsAngle(segment.A, lastSegment.B);
571
- // angles greater than 180 degrees are marked as large-arc-flag = 1
572
- path += `A ${rx},${ry} ${rotation} ${arcAngle > 180 ? 1 : 0},0 ${
573
- segment.A.x
574
- }, ${segment.A.y}`;
575
- }
576
- path += ` L ${segment.B.x},${segment.B.y}`;
577
- currentPoint = segment.B;
578
- lastSegment = segment;
579
- return path;
580
- }, '');
581
-
582
- // if the path isn't closed, close it by drawing an arc
583
- if (startPoint && currentPoint && !startPoint.isEqual(currentPoint)) {
584
- const arcAngle = pointsAngle(
585
- currentPoint,
586
- startPoint,
587
- 'counter-clockwise',
588
- );
589
- // angles greater than 180 degrees are marked as large-arc-flag = 1
590
- path += `A ${rx},${ry} ${rotation} ${arcAngle > 180 ? 1 : 0},0 ${startPoint.x
591
- }, ${startPoint.y}`;
592
- }
593
-
594
- // create a new element that will represent the cropped ellipse
595
- const newElement = parseHtml(`<path d="${path}" fill="red"/>`).firstChild;
596
- const svgAttributes: SVGAttributes = {
597
- ...element.svgAttributes,
598
- // the x and y values are 0 because all the path coordinates are global
599
- x: 0,
600
- y: 0,
601
- // the path coordinates are already rotated
602
- rotate: undefined,
603
- d: path,
604
- };
605
- Object.assign(newElement, { svgAttributes });
606
- return newElement as SVGElement;
607
- }
608
- case 'rect': {
609
- const {
610
- x = 0,
611
- y = 0,
612
- width = 0,
613
- height = 0,
614
- rotate: rawRotation,
615
- } = element.svgAttributes;
616
- const rotation = rawRotation?.angle || 0;
617
- if (!(width && height)) return element;
618
- // bottomLeft point
619
- const origin = new Point({ x, y });
620
-
621
- const rotateAroundOrigin = (p: Point) =>
622
- new Point(rotate(normalize(p), degreesToRadians(rotation))).plus(
623
- origin,
624
- );
625
- const normalize = (p: Point) => p.plus({ x: -origin.x, y: -origin.y });
626
-
627
- const topLeft = rotateAroundOrigin(origin.plus({ x: 0, y: -height }));
628
- const topRight = rotateAroundOrigin(
629
- origin.plus({ x: width, y: -height }),
630
- );
631
- const bottomRight = rotateAroundOrigin(origin.plus({ x: width, y: 0 }));
632
-
633
- const pointToString = (p: Point) => [p.x, p.y].join();
634
-
635
- const d = `M${pointToString(topLeft)} L${pointToString(
636
- topRight,
637
- )} L${pointToString(bottomRight)} L${pointToString(
638
- origin,
639
- )} L${pointToString(topLeft)}`;
640
- const el = parseHtml(`<path d="${d}"/>`).firstChild;
641
-
642
- const newAttributes = {
643
- ...element.svgAttributes,
644
- d,
645
- x: 0,
646
- y: 0,
647
- };
648
- // @ts-ignore
649
- delete newAttributes.width;
650
- // @ts-ignore
651
- delete newAttributes.height;
652
- delete newAttributes.rotate;
653
- delete newAttributes.rotation;
654
- Object.assign(el, {
655
- svgAttributes: newAttributes,
656
- });
657
- return cropSvgElement(svgRect, el as unknown as SVGElement);
658
- }
659
- // TODO: implement the crop for the following elements
660
- case 'image':
661
- default:
662
- return element;
663
- }
664
- return element;
665
- };
666
180
  // TODO: Improve type system to require the correct props for each tagName.
667
181
  /** methods to draw SVGElements onto a PDFPage */
668
182
  const runnersToPage = (
@@ -670,7 +184,7 @@ const runnersToPage = (
670
184
  options: PDFPageDrawSVGElementOptions,
671
185
  ): SVGElementToDrawMap => ({
672
186
  async text(element) {
673
- const anchor = element.svgAttributes.textAnchor;
187
+ const anchor = element.svgAttributes.textAnchor;
674
188
  const dominantBaseline = element.svgAttributes.dominantBaseline
675
189
  const text = element.text.trim().replace(/\s/g, ' ');
676
190
  const fontSize = element.svgAttributes.fontSize || 12;
@@ -698,7 +212,7 @@ const runnersToPage = (
698
212
 
699
213
  const font =
700
214
  options.fonts && getBestFont(element.svgAttributes, options.fonts);
701
- const textWidth = (font || page.getFont()[0]).widthOfTextAtSize(
215
+ const textWidth = (font || page.getFont()[0]).widthOfTextAtSize(
702
216
  text,
703
217
  fontSize,
704
218
  );
@@ -715,53 +229,52 @@ const runnersToPage = (
715
229
  : dominantBaseline === 'middle'
716
230
  ? textHeight / 2
717
231
  : 0
718
- const point = new Point({
719
- x: (element.svgAttributes.x || 0) - offsetX,
720
- y: (element.svgAttributes.y || 0) - offsetY,
721
- });
722
- // TODO: compute the right font boundaries to know which characters should be drawed
723
- // this is an workaround to draw text that are just a little outside the viewbox boundaries
232
+
724
233
  page.drawText(text, {
725
- x: point.x,
726
- y: point.y,
234
+ x: -offsetX,
235
+ y: -offsetY,
727
236
  font,
237
+ // TODO: the font size should be correctly scaled too
728
238
  size: fontSize,
729
239
  color: element.svgAttributes.fill,
730
240
  opacity: element.svgAttributes.fillOpacity,
731
- rotate: element.svgAttributes.rotate,
241
+ matrix: element.svgAttributes.matrix,
242
+ clipSpaces: element.svgAttributes.clipSpaces,
732
243
  });
733
244
  },
734
245
  async line(element) {
735
246
  page.drawLine({
736
247
  start: {
737
- x: element.svgAttributes.x1!,
738
- y: element.svgAttributes.y1!,
248
+ x: element.svgAttributes.x1 || 0,
249
+ y: -element.svgAttributes.y1! || 0,
739
250
  },
740
251
  end: {
741
- x: element.svgAttributes.x2!,
742
- y: element.svgAttributes.y2!,
252
+ x: element.svgAttributes.x2! || 0,
253
+ y: -element.svgAttributes.y2! || 0,
743
254
  },
744
255
  thickness: element.svgAttributes.strokeWidth,
745
256
  color: element.svgAttributes.stroke,
746
257
  opacity: element.svgAttributes.strokeOpacity,
747
258
  lineCap: element.svgAttributes.strokeLineCap,
259
+ matrix: element.svgAttributes.matrix,
260
+ clipSpaces: element.svgAttributes.clipSpaces,
748
261
  });
749
262
  },
750
263
  async path(element) {
751
264
  if (!element.svgAttributes.d) return
752
265
  // See https://jsbin.com/kawifomupa/edit?html,output and
753
266
  page.drawSvgPath(element.svgAttributes.d, {
754
- x: element.svgAttributes.x || 0,
755
- y: element.svgAttributes.y || 0,
267
+ x: 0,
268
+ y: 0,
756
269
  borderColor: element.svgAttributes.stroke,
757
270
  borderWidth: element.svgAttributes.strokeWidth,
758
271
  borderOpacity: element.svgAttributes.strokeOpacity,
759
272
  borderLineCap: element.svgAttributes.strokeLineCap,
760
273
  color: element.svgAttributes.fill,
761
274
  opacity: element.svgAttributes.fillOpacity,
762
- scale: element.svgAttributes.scale,
763
- rotate: element.svgAttributes.rotate,
764
275
  fillRule: element.svgAttributes.fillRule,
276
+ matrix: element.svgAttributes.matrix,
277
+ clipSpaces: element.svgAttributes.clipSpaces,
765
278
  });
766
279
  },
767
280
  async image(element) {
@@ -780,21 +293,20 @@ const runnersToPage = (
780
293
  element.svgAttributes.preserveAspectRatio,
781
294
  );
782
295
  page.drawImage(img, {
783
- x: (element.svgAttributes.x || 0) + x,
784
- y: (element.svgAttributes.y || 0) - y - height,
296
+ x,
297
+ y: -y - height,
785
298
  width,
786
299
  height,
787
300
  opacity: element.svgAttributes.fillOpacity,
788
- xSkew: element.svgAttributes.skewX,
789
- ySkew: element.svgAttributes.skewY,
790
- rotate: element.svgAttributes.rotate,
301
+ matrix: element.svgAttributes.matrix,
302
+ clipSpaces: element.svgAttributes.clipSpaces,
791
303
  });
792
304
  },
793
305
  async rect(element) {
794
306
  if (!element.svgAttributes.fill && !element.svgAttributes.stroke) return;
795
307
  page.drawRectangle({
796
- x: element.svgAttributes.x,
797
- y: element.svgAttributes.y || 0,
308
+ x: 0,
309
+ y: 0,
798
310
  width: element.svgAttributes.width,
799
311
  height: element.svgAttributes.height * -1,
800
312
  borderColor: element.svgAttributes.stroke,
@@ -803,15 +315,14 @@ const runnersToPage = (
803
315
  borderLineCap: element.svgAttributes.strokeLineCap,
804
316
  color: element.svgAttributes.fill,
805
317
  opacity: element.svgAttributes.fillOpacity,
806
- xSkew: element.svgAttributes.skewX,
807
- ySkew: element.svgAttributes.skewY,
808
- rotate: element.svgAttributes.rotate,
318
+ matrix: element.svgAttributes.matrix,
319
+ clipSpaces: element.svgAttributes.clipSpaces,
809
320
  });
810
321
  },
811
322
  async ellipse(element) {
812
323
  page.drawEllipse({
813
- x: element.svgAttributes.cx,
814
- y: element.svgAttributes.cy,
324
+ x: element.svgAttributes.cx || 0,
325
+ y: -(element.svgAttributes.cy || 0),
815
326
  xScale: element.svgAttributes.rx,
816
327
  yScale: element.svgAttributes.ry,
817
328
  borderColor: element.svgAttributes.stroke,
@@ -820,7 +331,8 @@ const runnersToPage = (
820
331
  borderLineCap: element.svgAttributes.strokeLineCap,
821
332
  color: element.svgAttributes.fill,
822
333
  opacity: element.svgAttributes.fillOpacity,
823
- rotate: element.svgAttributes.rotate,
334
+ matrix: element.svgAttributes.matrix,
335
+ clipSpaces: element.svgAttributes.clipSpaces,
824
336
  });
825
337
  },
826
338
  async circle(element) {
@@ -828,86 +340,6 @@ const runnersToPage = (
828
340
  },
829
341
  });
830
342
 
831
- const transform = (
832
- converter: SVGSizeConverter,
833
- name: string,
834
- args: number[],
835
- ): SVGSizeConverter => {
836
- switch (name) {
837
- case 'scaleX':
838
- return transform(converter, 'scale', [args[0], 0]);
839
- case 'scaleY':
840
- return transform(converter, 'scale', [0, args[0]]);
841
- case 'scale':
842
- const [xScale, yScale = xScale] = args;
843
- return {
844
- point: (x: number, y: number) =>
845
- converter.point(x * xScale, y * yScale),
846
- size: (w: number, h: number) => converter.size(w * xScale, h * yScale),
847
- };
848
- case 'translateX':
849
- return transform(converter, 'translate', [args[0], 0]);
850
- case 'translateY':
851
- return transform(converter, 'translate', [0, args[0]]);
852
- case 'translate':
853
- const [dx, dy = dx] = args;
854
- return {
855
- point: (x: number, y: number) => converter.point(x + dx, y + dy),
856
- size: converter.size,
857
- };
858
- case 'rotate': {
859
- if (args.length > 1) {
860
- const [a, x, y = x] = args;
861
- let tempResult = transform(converter, 'translate', [x, y]);
862
- tempResult = transform(tempResult, 'rotate', [a]);
863
- return transform(tempResult, 'translate', [-x, -y]);
864
- } else {
865
- const [a] = args;
866
- const angle = degreesToRadians(a);
867
- return {
868
- point: (x, y) =>
869
- converter.point(
870
- x * Math.cos(angle) - y * Math.sin(angle),
871
- y * Math.cos(angle) + x * Math.sin(angle),
872
- ),
873
- size: converter.size,
874
- };
875
- }
876
- }
877
- case 'matrix': {
878
- const [scaleX, skewY, skewX, scaleY, translateX, translateY] = args;
879
- return {
880
- point: (x: number, y: number) =>
881
- converter.point(
882
- x * scaleX + y * skewX + translateX,
883
- x * skewY + y * scaleY + translateY,
884
- ),
885
- size: (w: number, h: number) => converter.size(w * scaleX, h * scaleY),
886
- };
887
- }
888
- case 'skewX': {
889
- const angle = degreesToRadians(args[0]);
890
- return {
891
- point: (x: number, y: number) =>
892
- converter.point((1 + x) * Math.tan(angle), y),
893
- size: converter.size,
894
- };
895
- }
896
- case 'skewY': {
897
- const angle = degreesToRadians(args[0]);
898
- return {
899
- point: (x: number, y: number) =>
900
- converter.point(x, (1 + y) * Math.tan(angle)),
901
- size: converter.size,
902
- };
903
- }
904
- default: {
905
- console.log('transformation unsupported:', name);
906
- return converter;
907
- }
908
- }
909
- };
910
-
911
343
  const styleOrAttribute = (
912
344
  attributes: Attributes,
913
345
  style: SVGStyle,
@@ -946,15 +378,15 @@ const parseColor = (
946
378
 
947
379
  type ParsedAttributes = {
948
380
  inherited: InheritedAttributes;
949
- converter: SVGSizeConverter;
950
381
  tagName: string;
951
382
  svgAttributes: SVGAttributes;
383
+ matrix: TransformationMatrix;
952
384
  };
953
385
 
954
386
  const parseAttributes = (
955
387
  element: HTMLElement,
956
388
  inherited: InheritedAttributes,
957
- converter: SVGSizeConverter,
389
+ matrix: TransformationMatrix,
958
390
  ): ParsedAttributes => {
959
391
  const attributes = element.attributes;
960
392
  const style = parseStyles(attributes.style);
@@ -1038,8 +470,6 @@ const parseAttributes = (
1038
470
  preserveAspectRatio: attributes.preserveAspectRatio,
1039
471
  };
1040
472
 
1041
- let newConverter = converter;
1042
-
1043
473
  let transformList = attributes.transform || '';
1044
474
  // Handle transformations set as direct attributes
1045
475
  [
@@ -1058,26 +488,12 @@ const parseAttributes = (
1058
488
  transformList = attributes[name] + ' ' + transformList;
1059
489
  }
1060
490
  });
1061
- // skewX, skewY, rotate and scale are handled by the pdf-lib
1062
- (['skewX', 'skewY', 'rotate'] as const).forEach((name) => {
1063
- if (attributes[name]) {
1064
- const d = attributes[name].match(/-?(\d+\.?|\.)\d*/)?.[0];
1065
- if (d !== undefined) {
1066
- svgAttributes[name] = {
1067
- angle: parseInt(d, 10),
1068
- type: RotationTypes.Degrees,
1069
- };
1070
- }
1071
- }
1072
- });
1073
- if (attributes.scale) {
1074
- const d = attributes.scale.match(/-?(\d+\.?|\.)\d*/)?.[0];
1075
- if (d !== undefined) svgAttributes.scale = parseInt(d, 10);
1076
- }
491
+
1077
492
  // Convert x/y as if it was a translation
1078
493
  if (x || y) {
1079
494
  transformList = transformList + `translate(${x || 0} ${y || 0}) `;
1080
495
  }
496
+ let newMatrix = matrix
1081
497
  // Apply the transformations
1082
498
  if (transformList) {
1083
499
  const regexTransform = /(\w+)\((.+?)\)/g;
@@ -1088,335 +504,42 @@ const parseAttributes = (
1088
504
  .split(/\s*,\s*|\s+/)
1089
505
  .filter((value) => value.length > 0)
1090
506
  .map((value) => parseFloat(value));
1091
- const currentConverter = newConverter
1092
- newConverter = transform(newConverter, name, args);
1093
- const xAxisVector = minus(currentConverter.point(0, 0), currentConverter.point(1, 0))
1094
- const xAxisVectorPostTransform = minus(newConverter.point(0, 0), newConverter.point(1, 0))
1095
- // matrix transform may also represent rotations: https://www.w3.org/TR/SVGTiny12/coords.html
1096
- if (name === 'rotate' || name === 'matrix') {
1097
- // transformations over x and y axis might change the page coord direction
1098
- const { width: xDirection, height: yDirection } = currentConverter.size(
1099
- 1,
1100
- 1,
1101
- );
1102
- const rotationAdded = name === 'rotate' ? args[0] : radiansToDegrees(angle(xAxisVectorPostTransform, xAxisVector))
1103
- // the page Y coord is inverted so the angle rotation is inverted too
1104
- const pageYDirection = -1;
1105
- newInherited.rotation = degrees(
1106
- pageYDirection * rotationAdded * Math.sign(xDirection * yDirection) +
1107
- (inherited.rotation?.angle || 0),
1108
- );
1109
- svgAttributes.rotate = newInherited.rotation;
1110
- }
507
+ newMatrix = combineTransformation(newMatrix, name as TransformationName, args)
1111
508
  parsed = regexTransform.exec(transformList);
1112
509
  }
1113
510
  }
1114
511
 
1115
- // x and y were already transformed into a translation. The new reference point is now 0,0
1116
- const { x: newX, y: newY } = newConverter.point(0, 0);
1117
- svgAttributes.x = newX;
1118
- svgAttributes.y = newY;
512
+ svgAttributes.x = x;
513
+ svgAttributes.y = y;
1119
514
 
1120
515
  if (attributes.cx || attributes.cy) {
1121
- const { x: newCX, y: newCY } = newConverter.point(cx || 0, cy || 0);
1122
- svgAttributes.cx = newCX;
1123
- svgAttributes.cy = newCY;
516
+ svgAttributes.cx = cx;
517
+ svgAttributes.cy = cy;
1124
518
  }
1125
519
  if (attributes.rx || attributes.ry || attributes.r) {
1126
- const { width: newRX, height: newRY } = newConverter.size(rx || 0, ry || 0);
1127
- svgAttributes.rx = newRX;
1128
- svgAttributes.ry = newRY;
520
+ svgAttributes.rx = rx;
521
+ svgAttributes.ry = ry;
1129
522
  }
1130
523
  if (attributes.x1 || attributes.y1) {
1131
- const { x: newX1, y: newY1 } = newConverter.point(x1 || 0, y1 || 0);
1132
- svgAttributes.x1 = newX1;
1133
- svgAttributes.y1 = newY1;
524
+ svgAttributes.x1 = x1;
525
+ svgAttributes.y1 = y1;
1134
526
  }
1135
527
  if (attributes.x2 || attributes.y2) {
1136
- const { x: newX2, y: newY2 } = newConverter.point(x2 || 0, y2 || 0);
1137
- svgAttributes.x2 = newX2;
1138
- svgAttributes.y2 = newY2;
528
+ svgAttributes.x2 = x2;
529
+ svgAttributes.y2 = y2;
1139
530
  }
1140
531
  if (attributes.width || attributes.height) {
1141
- const size = newConverter.size(
1142
- width ?? inherited.width,
1143
- height ?? inherited.height,
1144
- );
1145
- svgAttributes.width = size.width;
1146
- svgAttributes.height = size.height;
532
+ svgAttributes.width = width ?? inherited.width
533
+ svgAttributes.height = height ?? inherited.height
1147
534
  }
1148
535
 
1149
- // We convert all the points from the path
1150
536
  if (attributes.d) {
1151
- const { x: xOrigin, y: yOrigin } = newConverter.point(0, 0);
1152
- // these are the x and y coordinates relative to the svg space, therefore these values weren't parsed by any converter. Each instruction will left the cursor on new position
1153
- let currentX = 0;
1154
- let currentY = 0;
1155
- svgAttributes.d = attributes.d?.replace(
1156
- /(l|m|s|t|q|c|z|a|v|h)([0-9,e\s.-]*)/gi,
1157
- (command) => {
1158
- const letter = command.match(/[a-z]/i)?.[0];
1159
- if (letter?.toLocaleLowerCase() === 'z') return letter;
1160
- // const params = command.match(/([0-9e.-]+)/ig)?.filter(m => m !== '')//.map(v => parseFloat(v))
1161
- const params = command
1162
- .match(
1163
- /(-?[0-9]+\.[0-9]+(e[+-]?[0-9]+)?)|(-?\.[0-9]+(e[+-]?[0-9]+)?)|(-?[0-9]+)/gi,
1164
- )
1165
- ?.filter((m) => m !== ''); // .map(v => parseFloat(v))
1166
- if (!params) return letter || '';
1167
- switch (letter?.toLocaleLowerCase()) {
1168
- case 'm':
1169
- case 'l': {
1170
- const groupedParams = groupBy<string>(params, 2);
1171
- return groupedParams
1172
- .map((pair, pairIndex) => {
1173
- const [x, y] = pair;
1174
- const xReal = parseFloatValue(x, inherited.width) || 0;
1175
- const yReal = parseFloatValue(y, innerHeight) || 0;
1176
- if (letter === letter.toLowerCase()) {
1177
- currentX += xReal;
1178
- currentY += yReal;
1179
- } else {
1180
- currentX = xReal;
1181
- currentY = yReal;
1182
- }
1183
- const point = newConverter.point(currentX, currentY);
1184
- return (
1185
- (pairIndex > 0 || letter.toUpperCase() === 'L' ? 'L' : 'M') +
1186
- [point.x - xOrigin, point.y - yOrigin].join(',')
1187
- );
1188
- })
1189
- .join(' ');
1190
- }
1191
- case 'v': {
1192
- return params
1193
- .map((value) => {
1194
- const coord = parseFloatValue(value) || 0;
1195
- if (letter === letter.toLowerCase()) {
1196
- currentY += coord;
1197
- } else {
1198
- currentY = coord;
1199
- }
1200
- const point = newConverter.point(currentX, currentY);
1201
- // we can't use 'v' as the final command because rotations might require a different command after the path parsing
1202
- // for instance, a 90 degree rotation would turn a 'v' into an 'h' command
1203
- return `L${point.x - xOrigin} ${point.y - yOrigin}`;
1204
- })
1205
- .join(' ');
1206
- }
1207
- case 'h': {
1208
- return params
1209
- .map((value) => {
1210
- const coord = parseFloatValue(value) || 0;
1211
- if (letter === letter.toLowerCase()) {
1212
- currentX += coord;
1213
- } else {
1214
- currentX = coord;
1215
- }
1216
- const point = newConverter.point(currentX, currentY);
1217
- // we can't use 'h' as the final command because rotations might require a different command after the path parsing
1218
- // for instance, a 90 degree rotation would turn a 'h' into an 'v' command
1219
- return `L${point.x - xOrigin} ${point.y - yOrigin}`;
1220
- })
1221
- .join(' ');
1222
- }
1223
- case 'a': {
1224
- const groupedParams = groupBy<string>(params, 7);
1225
- return groupedParams
1226
- .map((p) => {
1227
- const [
1228
- rxPixel,
1229
- ryPixel,
1230
- xAxisRotation = '0',
1231
- largeArc = '0',
1232
- sweepFlag = '0',
1233
- xPixel,
1234
- yPixel,
1235
- ] = p;
1236
- const realRx = parseFloatValue(rxPixel, inherited.width) || 0;
1237
- const realRy = parseFloatValue(ryPixel, inherited.height) || 0;
1238
- const realX = parseFloatValue(xPixel, inherited.width) || 0;
1239
- const realY = parseFloatValue(yPixel, inherited.height) || 0;
1240
- const { width: newRx, height: newRy } = newConverter.size(
1241
- realRx,
1242
- realRy,
1243
- );
1244
- let point;
1245
- if (letter === letter.toLowerCase()) {
1246
- currentX += realX;
1247
- currentY += realY;
1248
- } else {
1249
- currentX = realX;
1250
- currentY = realY;
1251
- }
1252
- point = newConverter.point(currentX, currentY);
1253
- // transformations over x and y axis might change the page coord direction
1254
- const { width: xDirection, height: yDirection } = newConverter.size(
1255
- 1,
1256
- 1,
1257
- );
1258
- // -1 is the default direction
1259
- const pageYDirection = -1 * Math.sign(xDirection * yDirection);
1260
- const oppositeSweepFlag = sweepFlag === '0' ? '1' : '0'
1261
- return [
1262
- letter.toUpperCase(),
1263
- newRx,
1264
- newRy,
1265
- xAxisRotation,
1266
- largeArc,
1267
- pageYDirection === -1 ? oppositeSweepFlag : sweepFlag,
1268
- point.x - xOrigin,
1269
- point.y - yOrigin,
1270
- ].join(' ');
1271
- })
1272
- .join(' ');
1273
- }
1274
- case 'c': {
1275
- const groupedParams = groupBy<string>(params, 6);
1276
- const result = groupedParams!
1277
- .map(([c1X, c1Y, c2X, c2Y, xString, yString]) => [
1278
- parseFloatValue(c1X, inherited.width) || 0,
1279
- parseFloatValue(c1Y, inherited.height) || 0,
1280
- parseFloatValue(c2X, inherited.width) || 0,
1281
- parseFloatValue(c2Y, inherited.height) || 0,
1282
- parseFloatValue(xString, inherited.width) || 0,
1283
- parseFloatValue(yString, inherited.height) || 0,
1284
- ])
1285
- .map(([c1X, c1Y, c2X, c2Y, xReal, yReal]) => {
1286
- let controlPoint1X;
1287
- let controlPoint1Y;
1288
- let controlPoint2X;
1289
- let controlPoint2Y;
1290
- if (letter === letter!.toLowerCase()) {
1291
- controlPoint1X = currentX + c1X;
1292
- controlPoint1Y = currentY + c1Y;
1293
- controlPoint2X = currentX + c2X;
1294
- controlPoint2Y = currentY + c2Y;
1295
- currentX += xReal;
1296
- currentY += yReal;
1297
- } else {
1298
- controlPoint1X = c1X;
1299
- controlPoint1Y = c1Y;
1300
- controlPoint2X = c2X;
1301
- controlPoint2Y = c2Y;
1302
- currentX = xReal;
1303
- currentY = yReal;
1304
- }
1305
- const controlPoint1 = newConverter.point(
1306
- controlPoint1X,
1307
- controlPoint1Y,
1308
- );
1309
- const controlPoint2 = newConverter.point(
1310
- controlPoint2X,
1311
- controlPoint2Y,
1312
- );
1313
- const point = newConverter.point(currentX, currentY);
1314
- return [
1315
- controlPoint1.x - xOrigin,
1316
- controlPoint1.y - yOrigin,
1317
- controlPoint2.x - xOrigin,
1318
- controlPoint2.y - yOrigin,
1319
- point.x - xOrigin,
1320
- point.y - yOrigin,
1321
- ].join(',');
1322
- })
1323
- .join(' ');
1324
- return letter?.toUpperCase() + '' + result;
1325
- }
1326
- case 's': {
1327
- const groupedParams = groupBy<string>(params, 4);
1328
- const result = groupedParams!
1329
- // the control point 1 is omitted because it's the reflection of c2
1330
- .map(([c2X, c2Y, xString, yString]) => [
1331
- parseFloatValue(c2X, inherited.width) || 0,
1332
- parseFloatValue(c2Y, inherited.height) || 0,
1333
- parseFloatValue(xString, inherited.width) || 0,
1334
- parseFloatValue(yString, inherited.height) || 0,
1335
- ])
1336
- .map(([c2X, c2Y, xReal, yReal]) => {
1337
- let controlPoint2X;
1338
- let controlPoint2Y;
1339
- if (letter === letter!.toLowerCase()) {
1340
- controlPoint2X = currentX + c2X;
1341
- controlPoint2Y = currentY + c2Y;
1342
- currentX += xReal;
1343
- currentY += yReal;
1344
- } else {
1345
- controlPoint2X = c2X;
1346
- controlPoint2Y = c2Y;
1347
- currentX = xReal;
1348
- currentY = yReal;
1349
- }
1350
- const controlPoint2 = newConverter.point(
1351
- controlPoint2X,
1352
- controlPoint2Y,
1353
- );
1354
- const point = newConverter.point(currentX, currentY);
1355
- return [
1356
- controlPoint2.x - xOrigin,
1357
- controlPoint2.y - yOrigin,
1358
- point.x - xOrigin,
1359
- point.y - yOrigin,
1360
- ].join(',');
1361
- })
1362
- .join(' ');
1363
- return letter?.toUpperCase() + '' + result;
1364
- }
1365
- default: {
1366
- const groupedParams = groupBy<string>(params, 2);
1367
- const result = groupedParams!
1368
- .map(([xString, yString]) => [
1369
- parseFloatValue(xString, inherited.width) || 0,
1370
- parseFloatValue(yString, inherited.height) || 0,
1371
- ])
1372
- .map(([xReal, yReal]) => {
1373
- if (letter === letter!.toLowerCase()) {
1374
- currentX += xReal;
1375
- currentY += yReal;
1376
- } else {
1377
- currentX = xReal;
1378
- currentY = yReal;
1379
- }
1380
- const point = newConverter.point(currentX, currentY);
1381
- return [point.x - xOrigin, point.y - yOrigin].join(',');
1382
- })
1383
- .join(' ');
1384
- return letter?.toUpperCase() + '' + result;
1385
- }
1386
- }
1387
- },
1388
- );
537
+ newMatrix = combineTransformation(newMatrix, 'scale', [1, -1])
538
+ svgAttributes.d = attributes.d
1389
539
  }
1390
- if (attributes.viewBox) {
1391
- const viewBox = parseViewBox(attributes.viewBox)!;
1392
- const size = {
1393
- width: width || inherited.viewBox.width,
1394
- height: height || inherited.viewBox.height,
1395
- };
1396
540
 
1397
- const localConverter = getConverterWithAspectRatio(
1398
- size,
1399
- viewBox,
1400
- attributes.preserveAspectRatio,
1401
- );
1402
- const oldConverter = newConverter;
1403
- newConverter = {
1404
- point: (px: number, py: number) => {
1405
- const { x: localX, y: localY } = localConverter.point(px, py);
1406
- return oldConverter.point(localX, localY);
1407
- },
1408
- size: (w: number, h: number) => {
1409
- const { width: localWidth, height: localHeight } = localConverter.size(
1410
- w,
1411
- h,
1412
- );
1413
- return oldConverter.size(localWidth, localHeight);
1414
- },
1415
- };
1416
- }
1417
- // apply the converter only when there's a local fontSize instruction
1418
541
  if (fontSizeRaw && newInherited.fontSize) {
1419
- newInherited.fontSize = newConverter.size(1, newInherited.fontSize).height;
542
+ newInherited.fontSize = newInherited.fontSize
1420
543
  }
1421
544
  if (newInherited.fontFamily) {
1422
545
  // Handle complex fontFamily like `"Linux Libertine O", serif`
@@ -1425,62 +548,14 @@ const parseAttributes = (
1425
548
  }
1426
549
 
1427
550
  if (newInherited.strokeWidth) {
1428
- const result = newConverter.size(
1429
- newInherited.strokeWidth,
1430
- newInherited.strokeWidth,
1431
- );
1432
- svgAttributes.strokeWidth = Math.max(
1433
- Math.min(Math.abs(result.width), Math.abs(result.height)),
1434
- 1,
1435
- );
551
+ svgAttributes.strokeWidth = newInherited.strokeWidth
1436
552
  }
1437
553
 
1438
554
  return {
1439
555
  inherited: newInherited,
1440
556
  svgAttributes,
1441
- converter: newConverter,
1442
557
  tagName: element.tagName,
1443
- };
1444
- };
1445
-
1446
- const getConverter = (box: Size, viewBox: Box): SVGSizeConverter => {
1447
- const { width, height } = box;
1448
- const { x: xMin, y: yMin, width: viewWidth, height: viewHeight } = viewBox;
1449
- const converter = {
1450
- point: (xReal: number, yReal: number) => ({
1451
- x: ((xReal - xMin) / viewWidth) * (width || 0),
1452
- y: ((yReal - yMin) / viewHeight) * (height || 0),
1453
- }),
1454
- size: (wReal: number, hReal: number) => ({
1455
- width: (wReal / viewWidth) * (width || 0),
1456
- height: (hReal / viewHeight) * (height || 0),
1457
- }),
1458
- };
1459
- return converter;
1460
- };
1461
-
1462
- const getConverterWithAspectRatio = (
1463
- size: Size,
1464
- viewBox: Box,
1465
- preserveAspectRatio?: string,
1466
- ) => {
1467
- // explanation about how the svg attributes applies transformations to the child elements
1468
- // https://www.w3.org/TR/SVG/coords.html#ComputingAViewportsTransform
1469
- const { x, y, width, height } = getFittingRectangle(
1470
- viewBox.width,
1471
- viewBox.height,
1472
- size.width,
1473
- size.height,
1474
- preserveAspectRatio,
1475
- );
1476
- const ratioConverter = getConverter({ width, height }, viewBox);
1477
- // We translate the drawing in the page when the aspect ratio is different, according to the preserveAspectRatio instructions.
1478
- return {
1479
- point: (xReal: number, yReal: number) => {
1480
- const P = ratioConverter.point(xReal, yReal);
1481
- return { x: P.x + x, y: P.y + y };
1482
- },
1483
- size: ratioConverter.size,
558
+ matrix: newMatrix,
1484
559
  };
1485
560
  };
1486
561
 
@@ -1528,10 +603,85 @@ const getFittingRectangle = (
1528
603
  return { x, y, width, height };
1529
604
  };
1530
605
 
606
+ const getAspectRatioTransformation = (
607
+ matrix: TransformationMatrix,
608
+ originalWidth: number,
609
+ originalHeight: number,
610
+ targetWidth: number,
611
+ targetHeight: number,
612
+ preserveAspectRatio?: string,
613
+ ): {
614
+ clipBox: TransformationMatrix
615
+ content: TransformationMatrix
616
+ } => {
617
+ const scaleX = targetWidth / originalWidth
618
+ const scaleY = targetHeight / originalHeight
619
+ const boxScale = combineTransformation(
620
+ matrix,
621
+ 'scale',
622
+ [
623
+ scaleX,
624
+ scaleY
625
+ ]
626
+ )
627
+ if (preserveAspectRatio === 'none') {
628
+ return {
629
+ clipBox: boxScale,
630
+ content: boxScale
631
+ }
632
+ }
633
+ // TODO: the following code works for the 'meet' param but not for the 'slice'
634
+ const scale =
635
+ targetWidth > targetHeight ? scaleY : scaleX
636
+ const dx = targetWidth - (originalWidth * scale)
637
+ const dy = targetHeight - (originalHeight * scale)
638
+ const [x, y] = (() => {
639
+ switch (preserveAspectRatio) {
640
+ case 'xMinYMin':
641
+ return [0, 0];
642
+ case 'xMidYMin':
643
+ return [dx / 2, 0];
644
+ case 'xMaxYMin':
645
+ return [dx, dy / 2];
646
+ case 'xMinYMid':
647
+ return [0, dy];
648
+ case 'xMaxYMid':
649
+ return [dx, dy / 2];
650
+ case 'xMinYMax':
651
+ return [0, dy];
652
+ case 'xMidYMax':
653
+ return [dx / 2, dy];
654
+ case 'xMaxYMax':
655
+ return [dx, dy];
656
+ case 'xMidYMid':
657
+ default:
658
+ return [dx / 2, dy / 2];
659
+ }
660
+ })();
661
+
662
+ const contentTransform = combineTransformation(
663
+ combineTransformation(
664
+ matrix,
665
+ 'translate',
666
+ [x, y]
667
+ ),
668
+ 'scale',
669
+ [scale]
670
+ )
671
+
672
+ return {
673
+ clipBox: boxScale,
674
+ content: contentTransform
675
+ }
676
+
677
+ };
678
+
679
+
1531
680
  const parseHTMLNode = (
1532
681
  node: Node,
1533
682
  inherited: InheritedAttributes,
1534
- converter: SVGSizeConverter,
683
+ matrix: TransformationMatrix,
684
+ clipSpaces: Space[],
1535
685
  ): SVGElement[] => {
1536
686
  if (node.nodeType === NodeType.COMMENT_NODE) return [];
1537
687
  else if (node.nodeType === NodeType.TEXT_NODE) return [];
@@ -1539,24 +689,28 @@ const parseHTMLNode = (
1539
689
  return parseGroupNode(
1540
690
  node as HTMLElement & { tagName: 'g' },
1541
691
  inherited,
1542
- converter,
692
+ matrix,
693
+ clipSpaces,
1543
694
  );
1544
695
  } else if (node.tagName === 'svg') {
1545
696
  return parseSvgNode(
1546
697
  node as HTMLElement & { tagName: 'svg' },
1547
698
  inherited,
1548
- converter,
699
+ matrix,
700
+ clipSpaces,
1549
701
  );
1550
702
  } else {
1551
703
  if (node.tagName === 'polygon') {
1552
704
  node.tagName = 'path';
1553
- node.attributes.d = `M${node.attributes.points}Z`;
705
+ node.attributes.d = `M${node.attributes.points}Z`;
1554
706
  delete node.attributes.points;
1555
707
  }
1556
- const attributes = parseAttributes(node, inherited, converter);
708
+ const attributes = parseAttributes(node, inherited, matrix);
1557
709
  const svgAttributes = {
1558
710
  ...attributes.inherited,
1559
711
  ...attributes.svgAttributes,
712
+ matrix: attributes.matrix,
713
+ clipSpaces
1560
714
  };
1561
715
  Object.assign(node, { svgAttributes });
1562
716
  return [node as SVGElement];
@@ -1566,35 +720,74 @@ const parseHTMLNode = (
1566
720
  const parseSvgNode = (
1567
721
  node: HTMLElement & { tagName: 'svg' },
1568
722
  inherited: InheritedAttributes,
1569
- converter: SVGSizeConverter,
723
+ matrix: TransformationMatrix,
724
+ clipSpaces: Space[],
1570
725
  ): SVGElement[] => {
1571
726
  // if the width/height aren't set, the svg will have the same dimension as the current drawing space
1572
727
  node.attributes.width ??
1573
728
  node.setAttribute('width', inherited.viewBox.width + '');
1574
729
  node.attributes.height ??
1575
730
  node.setAttribute('height', inherited.viewBox.height + '');
1576
- const attributes = parseAttributes(node, inherited, converter);
731
+ const attributes = parseAttributes(node, inherited, matrix);
1577
732
  const result: SVGElement[] = [];
1578
733
  const viewBox = node.attributes.viewBox
1579
734
  ? parseViewBox(node.attributes.viewBox)!
1580
735
  : node.attributes.width && node.attributes.height
1581
736
  ? parseViewBox(`0 0 ${node.attributes.width} ${node.attributes.height}`)!
1582
737
  : inherited.viewBox;
1583
- const svgRect = new Rectangle(
1584
- new Point(attributes.converter.point(viewBox.x, viewBox.y)),
1585
- new Point(
1586
- attributes.converter.point(
1587
- viewBox.x + viewBox.width,
1588
- viewBox.y + viewBox.height,
1589
- ),
1590
- ),
1591
- );
738
+ const x = parseFloat(node.attributes.x) || 0
739
+ const y = parseFloat(node.attributes.y) || 0
740
+
741
+ let newMatrix = combineTransformation(matrix, 'translate', [x, y])
742
+
743
+ const { clipBox: clipBoxTransform, content: contentTransform } =
744
+ getAspectRatioTransformation(
745
+ newMatrix,
746
+ viewBox.width,
747
+ viewBox.height,
748
+ parseFloat(node.attributes.width),
749
+ parseFloat(node.attributes.height),
750
+ node.attributes.preserveAspectRatio || 'xMidYMid'
751
+ )
752
+
753
+ const topLeft = applyTransformation(clipBoxTransform, {
754
+ x: 0,
755
+ y: 0,
756
+ })
757
+
758
+ const topRight = applyTransformation(clipBoxTransform, {
759
+ x: viewBox.width,
760
+ y: 0,
761
+ })
762
+
763
+ const bottomRight = applyTransformation(clipBoxTransform, {
764
+ x: viewBox.width,
765
+ y: -viewBox.height,
766
+ })
767
+
768
+ const bottomLeft = applyTransformation(clipBoxTransform, {
769
+ x: 0,
770
+ y: -viewBox.height,
771
+ })
772
+
773
+ const baseClipSpace: Space = {
774
+ topLeft,
775
+ topRight,
776
+ bottomRight,
777
+ bottomLeft
778
+ }
779
+
780
+ // TODO: maybe this is the correct transformation
781
+ // newMatrix = combineTransformation(newMatrix, 'translate', [-baseClipSpace.xMin, -baseClipSpace.yMin])
782
+ newMatrix = combineTransformation(contentTransform, 'translate', [-viewBox.x, -viewBox.y])
783
+
1592
784
  node.childNodes.forEach((child) => {
1593
785
  const parsedNodes = parseHTMLNode(
1594
786
  child,
1595
787
  { ...attributes.inherited, viewBox },
1596
- attributes.converter,
1597
- ).map((el) => cropSvgElement(svgRect, el));
788
+ newMatrix,
789
+ [...clipSpaces, baseClipSpace],
790
+ )
1598
791
  result.push(...parsedNodes);
1599
792
  });
1600
793
  return result;
@@ -1603,13 +796,14 @@ const parseSvgNode = (
1603
796
  const parseGroupNode = (
1604
797
  node: HTMLElement & { tagName: 'g' },
1605
798
  inherited: InheritedAttributes,
1606
- converter: SVGSizeConverter,
799
+ matrix: TransformationMatrix,
800
+ clipSpaces: Space[],
1607
801
  ): SVGElement[] => {
1608
- const attributes = parseAttributes(node, inherited, converter);
802
+ const attributes = parseAttributes(node, inherited, matrix);
1609
803
  const result: SVGElement[] = [];
1610
804
  node.childNodes.forEach((child) => {
1611
805
  result.push(
1612
- ...parseHTMLNode(child, attributes.inherited, attributes.converter),
806
+ ...parseHTMLNode(child, attributes.inherited, attributes.matrix, clipSpaces),
1613
807
  );
1614
808
  });
1615
809
  return result;
@@ -1640,15 +834,13 @@ const parseViewBox = (viewBox?: string): Box | undefined => {
1640
834
 
1641
835
  const parse = (
1642
836
  svg: string,
1643
- { width, height, x, y, fontSize }: PDFPageDrawSVGElementOptions,
837
+ { width, height, fontSize }: PDFPageDrawSVGElementOptions,
1644
838
  size: Size,
1645
- converter: SVGSizeConverter,
839
+ matrix: TransformationMatrix
1646
840
  ): SVGElement[] => {
1647
841
  const htmlElement = parseHtml(svg).firstChild as HTMLElement;
1648
842
  if (width) htmlElement.setAttribute('width', width + '');
1649
843
  if (height) htmlElement.setAttribute('height', height + '');
1650
- if (x !== undefined) htmlElement.setAttribute('x', x + '');
1651
- if (y !== undefined) htmlElement.setAttribute('y', size.height - y + '');
1652
844
  if (fontSize) htmlElement.setAttribute('font-size', fontSize + '');
1653
845
  // TODO: what should be the default viewBox?
1654
846
  return parseHTMLNode(
@@ -1657,7 +849,8 @@ const parse = (
1657
849
  ...size,
1658
850
  viewBox: parseViewBox(htmlElement.attributes.viewBox || '0 0 1 1')!,
1659
851
  },
1660
- converter,
852
+ matrix,
853
+ []
1661
854
  );
1662
855
  };
1663
856
 
@@ -1702,17 +895,43 @@ export const drawSvg = async (
1702
895
  );
1703
896
  }
1704
897
 
1705
- // The y axis of the page is reverted
1706
- const defaultConverter = {
1707
- point: (xP: number, yP: number) => ({ x: xP, y: size.height - yP }),
1708
- size: (w: number, h: number) => ({ width: w, height: h }),
1709
- };
898
+ const baseTransformation: TransformationMatrix = [1, 0, 0, 1, options.x || 0, options.y || 0]
1710
899
 
1711
900
  const runners = runnersToPage(page, options);
1712
- const elements = parse(firstChild.outerHTML, options, size, defaultConverter);
901
+ const elements = parse(firstChild.outerHTML, options, size, baseTransformation);
1713
902
 
1714
903
  await elements.reduce(async (prev, elt) => {
1715
904
  await prev;
905
+ // uncomment these lines to draw the clipSpaces
906
+ // elt.svgAttributes.clipSpaces.forEach(space => {
907
+ // page.drawLine({
908
+ // start: space.topLeft,
909
+ // end: space.topRight,
910
+ // color: parseColor('#000000')?.rgb,
911
+ // thickness: 1
912
+ // })
913
+
914
+ // page.drawLine({
915
+ // start: space.topRight,
916
+ // end: space.bottomRight,
917
+ // color: parseColor('#000000')?.rgb,
918
+ // thickness: 1
919
+ // })
920
+
921
+ // page.drawLine({
922
+ // start: space.bottomRight,
923
+ // end: space.bottomLeft,
924
+ // color: parseColor('#000000')?.rgb,
925
+ // thickness: 1
926
+ // })
927
+
928
+ // page.drawLine({
929
+ // start: space.bottomLeft,
930
+ // end: space.topLeft,
931
+ // color: parseColor('#000000')?.rgb,
932
+ // thickness: 1
933
+ // })
934
+ // })
1716
935
  return runners[elt.tagName]?.(elt);
1717
936
  }, Promise.resolve());
1718
937
  };