@muhammara/native-core 7.0.0-beta.4 → 7.0.0-rc.1

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 (43) hide show
  1. package/README.md +57 -7
  2. package/THIRD_PARTY_NOTICES.md +39 -0
  3. package/index.js +120 -1
  4. package/lib/PDFRStreamForBuffer.js +52 -7
  5. package/lib/PDFRStreamForFile.js +56 -7
  6. package/lib/PDFStreamForResponse.js +22 -5
  7. package/lib/PDFWStreamForBuffer.js +49 -10
  8. package/lib/PDFWStreamForFile.js +26 -5
  9. package/lib/Recipe.js +79 -28
  10. package/lib/content-stream.js +503 -0
  11. package/lib/css-colors.js +149 -0
  12. package/lib/font-text.js +1461 -0
  13. package/lib/glyph-list.js +1127 -0
  14. package/lib/recipe/annotation.js +464 -101
  15. package/lib/recipe/appendPage.js +2 -0
  16. package/lib/recipe/colors.js +132 -35
  17. package/lib/recipe/coordinate.js +82 -16
  18. package/lib/recipe/encrypt.js +32 -13
  19. package/lib/recipe/font.js +70 -25
  20. package/lib/recipe/graphics-state.js +3 -2
  21. package/lib/recipe/htmlToTextObjects.js +121 -24
  22. package/lib/recipe/image.js +34 -9
  23. package/lib/recipe/info.js +93 -22
  24. package/lib/recipe/insertPage.js +12 -3
  25. package/lib/recipe/overlay.js +4 -2
  26. package/lib/recipe/page-references.js +436 -0
  27. package/lib/recipe/page.js +363 -262
  28. package/lib/recipe/parameters.js +8 -1
  29. package/lib/recipe/replaceText.js +464 -30
  30. package/lib/recipe/shapes.js +418 -45
  31. package/lib/recipe/split.js +6 -2
  32. package/lib/recipe/table.js +41 -8
  33. package/lib/recipe/text.helper.js +172 -8
  34. package/lib/recipe/text.js +294 -56
  35. package/lib/recipe/utils.js +17 -6
  36. package/lib/recipe/vector-line.js +101 -39
  37. package/lib/recipe/vector-polygon.js +18 -6
  38. package/lib/recipe/vector.helper.js +148 -6
  39. package/lib/recipe/vector.js +150 -51
  40. package/lib/recipe/xObjectForm.js +73 -10
  41. package/lib/recipe-constants.js +373 -0
  42. package/muhammara.d.ts +3469 -155
  43. package/package.json +1 -1
@@ -12,6 +12,8 @@ const PAGE_CONTEXT_STATE = Object.freeze({
12
12
  /**
13
13
  * Copies nested option objects and arrays without discarding callback values.
14
14
  * @private
15
+ * @param {*} value - The options or value to copy.
16
+ * @returns {*} The copy; functions and primitives are returned as they are.
15
17
  */
16
18
  function cloneOptions(value) {
17
19
  if (Array.isArray(value)) return value.map(cloneOptions);
@@ -27,9 +29,11 @@ function cloneOptions(value) {
27
29
  * Append PDF Page with annotations.
28
30
  *
29
31
  * @param {any} pdfWriter - Hummus writer.
30
- * @param {string|any} sourcePDFPath - The path for the output pdfs or Reader stream.
31
- * @param {number} pageNumber - page number.
32
+ * @param {string|any} sourcePDFPath - The source PDF path or read stream.
33
+ * @param {number} pageNumber - The zero-based page index.
32
34
  * @private
35
+ * @returns {void}
36
+ * @throws {Error} If the source cannot be read or the page does not exist.
33
37
  */
34
38
  function appendPDFPageFromPDFWithAnnotations(
35
39
  pdfWriter,
@@ -67,8 +71,13 @@ function appendPDFPageFromPDFWithAnnotations(
67
71
  * Append PDF Pages with annotations.
68
72
  *
69
73
  * @param {any} pdfWriter - Hummus writer.
70
- * @param {string|any} sourcePDFPath - The path for the output pdfs or Reader stream.
74
+ * @param {string|any} sourcePDFPath - The source PDF path or read stream.
75
+ * @param {Object} [options] - The options.
76
+ * @param {number[][]} [options.specificRanges] - Zero-based inclusive
77
+ * [start, end] page ranges; all pages when omitted.
71
78
  * @private
79
+ * @returns {void}
80
+ * @throws {Error} If the source cannot be read or a page does not exist.
72
81
  */
73
82
  function appendPDFPagesFromPDFWithAnnotations(
74
83
  pdfWriter,
@@ -97,7 +106,8 @@ function appendPDFPagesFromPDFWithAnnotations(
97
106
 
98
107
  /**
99
108
  * Resolves a text font size from `size`, its `fontSize` alias, or a fallback,
100
- * rejecting a size that is not greater than zero. Zero and negative sizes have
109
+ * rejecting a size that is not a finite number greater than zero. Zero,
110
+ * negative, and infinite sizes have
101
111
  * no usable meaning: they draw nothing readable and measure to nonsensical font
102
112
  * metrics, so they are reported as invalid input naming the option and the
103
113
  * value. Omitting both options, or passing `null` or `undefined`, selects the
@@ -105,7 +115,8 @@ function appendPDFPagesFromPDFWithAnnotations(
105
115
  * @param {Object} [options] - Options holding `size` or `fontSize`.
106
116
  * @param {number} [fallback] - Size used when neither option is given.
107
117
  * @returns {number} The resolved font size in PDF points.
108
- * @throws {RangeError} If the given size is not greater than zero.
118
+ * @throws {RangeError} If the given size is not a finite number greater than
119
+ * zero.
109
120
  */
110
121
  function resolveFontSize(options = {}, fallback) {
111
122
  const name = options.size == null ? "fontSize" : "size";
@@ -113,7 +124,7 @@ function resolveFontSize(options = {}, fallback) {
113
124
  if (size == null) {
114
125
  return fallback;
115
126
  }
116
- if (!(size > 0)) {
127
+ if (!(size > 0) || size === Infinity) {
117
128
  throw new RangeError(
118
129
  `Text ${name} must be a number greater than zero, received ${size}`,
119
130
  );
@@ -1,11 +1,15 @@
1
+ const muhammara = require("../muhammara");
2
+ const { Colorspace } = require("../recipe-constants");
3
+
1
4
  /**
2
5
  * move the current position to target position
3
6
  * @name moveTo
4
7
  * @function
5
8
  * @memberof Recipe#
6
- * @param {number} x - The coordinate x
7
- * @param {number} y - The coordinate y
9
+ * @param {number|"center"} x - The coordinate x
10
+ * @param {number|"center"} y - The coordinate y
8
11
  * @returns {Recipe} The recipe instance.
12
+ * @throws {TypeError} If no page is active.
9
13
  */
10
14
  exports.moveTo = function moveTo(x, y) {
11
15
  const { nx, ny } = this._calibrateCoordinate(x, y);
@@ -21,53 +25,95 @@ exports.moveTo = function moveTo(x, y) {
21
25
  * @name lineTo
22
26
  * @function
23
27
  * @memberof Recipe#
24
- * @param {number} x - The coordinate x
25
- * @param {number} y - The coordinate y
28
+ * @param {number|"center"} x - The coordinate x
29
+ * @param {number|"center"} y - The coordinate y
26
30
  * @param {Object} [options] - The options
27
31
  * @returns {Recipe} The recipe instance.
32
+ * @throws {TypeError} If no page is active.
28
33
  * @param {string|number[]} [options.color] - HexColor, PercentColor or DecimalColor
29
34
  * @param {string|number[]} [options.stroke] - HexColor, PercentColor or DecimalColor
30
35
  * @param {number} [options.lineWidth] - The line width
31
36
  * @param {number} [options.opacity] - how transparent should line be, from 0: invisible to 1: opaque
32
37
  * @param {number[]} [options.dash] - The dash pattern [dashSize, gapSize] or [dashAndGapSize]
33
38
  * @param {number} [options.dashPhase] - distance into dash pattern at which to start dash (default: 0, immediately)
34
- * @param {string} [options.lineCap] - open line end style, 'butt', 'round', or 'square' (default: 'round')
35
- * @param {string} [options.lineJoin] - joined line end style, 'miter', 'round', or 'bevel' (default: 'round')
39
+ * @param {Recipe.LineCap} [options.lineCap] - open line end style, a `Recipe.LineCap` value (default: 'round')
40
+ * @param {Recipe.LineJoin} [options.lineJoin] - joined line end style, a `Recipe.LineJoin` value (default: 'round')
36
41
  * @param {number} [options.miterLimit] - limit at which 'miter' joins are forced to 'bevel' (default: 1.414)
37
- *
38
42
  */
39
43
  exports.lineTo = function lineTo(x, y, options = {}) {
40
- const fromX = this._position.x;
41
- const fromY = this._position.y;
44
+ const from = [this._position.x, this._position.y];
42
45
  const { nx, ny } = this._calibrateCoordinate(x, y);
43
- const context = this.pageContext;
46
+ this._strokePolyline([from, [nx, ny]], options);
47
+ this.moveTo(x, y);
48
+ return this;
49
+ };
50
+
51
+ /**
52
+ * Stroke one path through PDF points, so segments meet at the line join
53
+ * instead of overlapping, and a Separation line writes one form.
54
+ * @private
55
+ * @param {number[][]} points - Two or more [x, y] points in PDF coordinates.
56
+ * @param {Object} options - The line options of `lineTo()` and `line()`.
57
+ * @returns {void}
58
+ * @throws {TypeError} If no page is active.
59
+ */
60
+ exports._strokePolyline = function _strokePolyline(points, options) {
61
+ // _getPathOptions() may pause the page to write graphics states, and an
62
+ // edited page resumes into a new context, so read the context afterwards.
44
63
  const pathOptions = this._getPathOptions(options);
45
- pathOptions.type = "stroke";
64
+ const context = this.pageContext;
65
+ pathOptions.type = muhammara.DrawingPathType.STROKE;
46
66
 
47
67
  if (pathOptions.stroke !== undefined) {
48
68
  pathOptions.color = pathOptions.stroke;
49
69
  pathOptions.colorspace = pathOptions.strokeModel.colorspace;
50
70
  }
51
71
 
52
- context
53
- .q()
54
- .J(pathOptions.lineCap)
55
- .j(pathOptions.lineJoin)
56
- .d(pathOptions.dash, pathOptions.dashPhase)
57
- .M(pathOptions.miterLimit)
58
- .drawPath(fromX, fromY, nx, ny, pathOptions)
59
- .Q();
60
- this.moveTo(x, y);
61
- return this;
72
+ const colorModel = pathOptions.strokeModel || pathOptions.colorModel;
73
+ const drawLine = (ctx, originX, originY) =>
74
+ ctx
75
+ .J(pathOptions.lineCap)
76
+ .j(pathOptions.lineJoin)
77
+ .d(pathOptions.dash, pathOptions.dashPhase)
78
+ .M(pathOptions.miterLimit)
79
+ .drawPath(
80
+ ...points.flatMap(([px, py]) => [px - originX, py - originY]),
81
+ this._devicePathOptions(pathOptions),
82
+ );
83
+
84
+ if (colorModel.colorspace === Colorspace.SEPARATION) {
85
+ // A Separation color space is a resource, so draw through a form XObject
86
+ // as the other shapes do; the padding keeps caps and miter joins inside.
87
+ const padding = pathOptions.width * Math.max(1, pathOptions.miterLimit);
88
+ const xs = points.map((point) => point[0]);
89
+ const ys = points.map((point) => point[1]);
90
+ const left = Math.min(...xs) - padding;
91
+ const bottom = Math.min(...ys) - padding;
92
+ this._drawObject(
93
+ this,
94
+ left,
95
+ bottom,
96
+ Math.max(...xs) - Math.min(...xs) + padding * 2,
97
+ Math.max(...ys) - Math.min(...ys) + padding * 2,
98
+ {},
99
+ (ctx, xObject) => {
100
+ this._setSeparationColor(xObject, colorModel, true);
101
+ drawLine(ctx, left, bottom);
102
+ },
103
+ );
104
+ } else {
105
+ drawLine(context.q(), 0, 0).Q();
106
+ }
62
107
  };
63
108
 
64
109
  /**
65
- * Draw a line
110
+ * Draw a line through coordinate pairs, or from (startX, startY) to
111
+ * (endX, endY) when called as `line(startX, startY, endX, endY, options?)`.
66
112
  * @name line
67
113
  * @function
68
114
  * @memberof Recipe#
69
- * @param {number[]} coordinates - The array of coordinate [[x,y], [m,n]]
70
- * @param {Object} [options] - The options
115
+ * @param {number[][]|number} coordinates - The array of coordinate [[x,y], [m,n]], or the start x
116
+ * @param {Object} [options] - The options, or the start y in the four-number form
71
117
  * @returns {Recipe} The recipe instance.
72
118
  * @param {string|number[]} [options.color] - HexColor, PercentColor or DecimalColor
73
119
  * @param {string|number[]} [options.stroke] - HexColor, PercentColor or DecimalColor
@@ -75,20 +121,36 @@ exports.lineTo = function lineTo(x, y, options = {}) {
75
121
  * @param {number} [options.opacity] - how transparent should line be, from 0: invisible to 1: opaque
76
122
  * @param {number[]} [options.dash] - The dash pattern [dashSize, gapSize] or [dashAndGapSize]
77
123
  * @param {number} [options.dashPhase] - distance into dash pattern at which to start dash (default: 0, immediately)
78
- * @param {string} [options.lineCap] - open line end style, 'butt', 'round', or 'square' (default: 'round')
79
- * @param {string} [options.lineJoin] - joined line end style, 'miter', 'round', or 'bevel' (default: 'round')
80
- * @param {number} [options.miterLimit] - limit at which 'miter' joins are forced to 'bevel' (default: 1.414)*/
124
+ * @param {Recipe.LineCap} [options.lineCap] - open line end style, a `Recipe.LineCap` value (default: 'round')
125
+ * @param {Recipe.LineJoin} [options.lineJoin] - joined line end style, a `Recipe.LineJoin` value (default: 'round')
126
+ * @param {number} [options.miterLimit] - limit at which 'miter' joins are forced to 'bevel' (default: 1.414)
127
+ * @throws {TypeError} If no page is active.
128
+ */
81
129
  exports.line = function line(coordinates = [], options = {}) {
82
- coordinates.forEach((coordinate, index) => {
83
- if (index === 0) {
84
- this.moveTo(coordinate[0], coordinate[1]);
85
- if (this.editingPage) {
86
- // hack to force out first line when editing page
87
- this.lineTo(coordinate[0], coordinate[1], options);
88
- }
89
- } else {
90
- this.lineTo(coordinate[0], coordinate[1], options);
91
- }
92
- });
93
- return this;
130
+ // null arguments act like omitted ones.
131
+ if (coordinates === null) coordinates = [];
132
+ if (options === null) options = {};
133
+ if (typeof coordinates === "number") {
134
+ // line(startX, startY, endX, endY, options?), as in Wasm.
135
+ const [startX, startY, endX, endY, lineOptions = {}] = arguments;
136
+ coordinates = [
137
+ [startX, startY],
138
+ [endX, endY],
139
+ ];
140
+ options = lineOptions;
141
+ }
142
+ if (coordinates.length < 2) {
143
+ if (coordinates.length) this.moveTo(coordinates[0][0], coordinates[0][1]);
144
+ return this;
145
+ }
146
+ // Stroke every segment as one path, as Wasm does.
147
+ this._strokePolyline(
148
+ coordinates.map(([x, y]) => {
149
+ const { nx, ny } = this._calibrateCoordinate(x, y);
150
+ return [nx, ny];
151
+ }),
152
+ options,
153
+ );
154
+ const [lastX, lastY] = coordinates[coordinates.length - 1];
155
+ return this.moveTo(lastX, lastY);
94
156
  };
@@ -29,10 +29,20 @@
29
29
  * @param {number} [options.dashPhase] - distance into dash pattern at which to start dash (default: 0, immediately)
30
30
  * @param {number} [options.rotation] - Accept: +/- 0 through 360. Default: 0
31
31
  * @param {number[]} [options.rotationOrigin] - [originX, originY] Default: x, y
32
- * @param {string} [options.lineCap] - open line end style, 'butt', 'round', or 'square' (default: 'round')
33
- * @param {string} [options.lineJoin] - joined line end style, 'miter', 'round', or 'bevel' (default: 'round')
34
- * @param {number} [options.miterLimit] - limit at which 'miter' joins are forced to 'bevel' (default: 1.414) */
32
+ * @param {Recipe.LineCap} [options.lineCap] - open line end style, a `Recipe.LineCap` value (default: 'round')
33
+ * @param {Recipe.LineJoin} [options.lineJoin] - joined line end style, a `Recipe.LineJoin` value (default: 'round')
34
+ * @param {number} [options.miterLimit] - limit at which 'miter' joins are forced to 'bevel' (default: 1.414)
35
+ * @param {string} [options.link] - Make the polygon's bounding box open this URL.
36
+ * @throws {TypeError} If no page is active or there are no coordinates.
37
+ */
35
38
  exports.polygon = function polygon(coordinates = [], options = {}) {
39
+ // null options act like omitted options.
40
+ if (options === null) options = {};
41
+ if (!Array.isArray(coordinates) || coordinates.length === 0) {
42
+ throw new TypeError("A polygon needs at least one coordinate pair");
43
+ }
44
+ // Work on a copy so closing the path leaves the caller's array unchanged.
45
+ coordinates = coordinates.slice();
36
46
  // close polygon
37
47
  if (
38
48
  this._getDistance(coordinates[0], coordinates[coordinates.length - 1]) != 0
@@ -110,9 +120,11 @@ exports.polygon = function polygon(coordinates = [], options = {}) {
110
120
  }
111
121
 
112
122
  if (options.stroke || options.color || !options.fill) {
113
- if (pathOptions.stroke !== undefined) {
114
- colorModel = pathOptions.strokeModel;
115
- }
123
+ // The fill above may have replaced colorModel; stroke with `color`.
124
+ colorModel =
125
+ pathOptions.stroke !== undefined
126
+ ? pathOptions.strokeModel
127
+ : pathOptions.colorModel;
116
128
 
117
129
  this._drawObject(
118
130
  this,
@@ -1,6 +1,19 @@
1
1
  const { xObjectForm } = require("./xObjectForm");
2
2
  const { resolveFontSize } = require("./utils");
3
-
3
+ const { Colorspace, LineCap, LineJoin } = require("../recipe-constants");
4
+
5
+ /**
6
+ * Resolve drawing and text options into path options: font, size, colors
7
+ * and color models, line style, opacity graphics states, rotation, skew and
8
+ * dash. Clamps `options.opacity` to 0..1 in place.
9
+ * @private
10
+ * @param {Object} [options] - The drawing or text options.
11
+ * @param {number} originX - The PDF x of the default rotation origin.
12
+ * @param {number} originY - The PDF y of the default rotation origin.
13
+ * @returns {Object} The resolved path options.
14
+ * @throws {RangeError} If a given font size is not greater than zero.
15
+ * @throws {Error} If the font cannot be loaded.
16
+ */
4
17
  exports._getPathOptions = function _getPathOptions(
5
18
  options = {},
6
19
  originX,
@@ -47,8 +60,9 @@ exports._getPathOptions = function _getPathOptions(
47
60
  if (options.opacity == void 0 || isNaN(options.opacity)) {
48
61
  options.opacity = this.current.opacity ?? 1;
49
62
  } else {
50
- options.opacity =
51
- options.opacity < 0 ? 0 : options.opacity > 1 ? 1 : options.opacity;
63
+ // A numeric string such as "0.5" passes the isNaN check; write it as a
64
+ // number, as Wasm does.
65
+ options.opacity = Math.max(0, Math.min(1, Number(options.opacity)));
52
66
  }
53
67
  pathOptions.opacity = options.opacity;
54
68
  const extGStates = this._createExtGStates(options.opacity);
@@ -78,9 +92,13 @@ exports._getPathOptions = function _getPathOptions(
78
92
  pathOptions.fill = pathOptions.fillModel.color;
79
93
  }
80
94
 
95
+ // A fill or stroke replaces the default color. Resolving that default under
96
+ // colorName would write an unused Separation color space for the ink.
97
+ const paintsColor =
98
+ options.color || options.colour || (!options.fill && !options.stroke);
81
99
  pathOptions.colorModel = this._transformColor(
82
100
  options.color || options.colour,
83
- colorOpts,
101
+ paintsColor ? colorOpts : { ...colorOpts, colorName: undefined },
84
102
  );
85
103
  pathOptions.color = pathOptions.colorModel.color;
86
104
  pathOptions.colorspace = pathOptions.colorModel.colorspace;
@@ -126,6 +144,13 @@ exports._getPathOptions = function _getPathOptions(
126
144
  return pathOptions;
127
145
  };
128
146
 
147
+ /**
148
+ * The distance between two points.
149
+ * @private
150
+ * @param {number[]} coordA - The first [x, y] point.
151
+ * @param {number[]} coordB - The second [x, y] point.
152
+ * @returns {number} The distance.
153
+ */
129
154
  exports._getDistance = function _getDistance(coordA, coordB) {
130
155
  const disX = Math.abs(coordB[0] - coordA[0]);
131
156
  const disY = Math.abs(coordB[1] - coordA[1]);
@@ -136,6 +161,16 @@ exports._getDistance = function _getDistance(coordA, coordB) {
136
161
 
137
162
  exports._getTransformParams = getTransformParams;
138
163
 
164
+ /**
165
+ * The transformation matrix that rotates content around an origin.
166
+ * @private
167
+ * @param {number} inAngle - The rotation in degrees.
168
+ * @param {number} x - The origin x.
169
+ * @param {number} y - The origin y.
170
+ * @param {number} offsetX - The content x relative to the origin.
171
+ * @param {number} offsetY - The content y relative to the origin.
172
+ * @returns {number[]} The [a, b, c, d, e, f] matrix.
173
+ */
139
174
  function getTransformParams(inAngle, x, y, offsetX, offsetY) {
140
175
  const theta = toRadians(inAngle);
141
176
  const cosTheta = Math.cos(theta);
@@ -145,6 +180,17 @@ function getTransformParams(inAngle, x, y, offsetX, offsetY) {
145
180
  return [cosTheta, -sinTheta, sinTheta, cosTheta, x - nx, y - ny];
146
181
  }
147
182
 
183
+ /**
184
+ * Translate a content context to a point and apply the rotation options.
185
+ * @private
186
+ * @param {Object} context - The content context.
187
+ * @param {number} x - The PDF x.
188
+ * @param {number} y - The PDF y.
189
+ * @param {Object} options - The path options: rotation, rotationOrigin,
190
+ * useGivenCoords, originX, originY and deltaY.
191
+ * @returns {void}
192
+ * @throws {TypeError} If a Recipe rotation origin is converted without an active page.
193
+ */
148
194
  exports._setRotationContext = function _setRotationTransform(
149
195
  context,
150
196
  x,
@@ -185,6 +231,12 @@ exports._setRotationContext = function _setRotationTransform(
185
231
  }
186
232
  };
187
233
 
234
+ /**
235
+ * Whether the options carry an explicit [x, y] rotation origin.
236
+ * @private
237
+ * @param {Object} options - The path options.
238
+ * @returns {boolean} True for a two-element rotationOrigin array.
239
+ */
188
240
  function hasRotation(options) {
189
241
  return (
190
242
  options.rotationOrigin &&
@@ -193,10 +245,23 @@ function hasRotation(options) {
193
245
  );
194
246
  }
195
247
 
248
+ /**
249
+ * Convert degrees to radians, wrapping at 360.
250
+ * @private
251
+ * @param {number} angle - The angle in degrees.
252
+ * @returns {number} The angle in radians.
253
+ */
196
254
  function toRadians(angle) {
197
255
  return 2 * Math.PI * ((angle % 360) / 360);
198
256
  }
199
257
 
258
+ /**
259
+ * The transformation matrix for skew angles.
260
+ * @private
261
+ * @param {number} [skewXAngle=0] - The skew off the x axis, in degrees.
262
+ * @param {number} [skewYAngle=0] - The skew off the y axis, in degrees.
263
+ * @returns {number[]} The [a, b, c, d, e, f] matrix.
264
+ */
200
265
  function getSkewTransform(skewXAngle = 0, skewYAngle = 0) {
201
266
  const alpha = toRadians(skewXAngle);
202
267
  const beta = toRadians(skewYAngle);
@@ -206,6 +271,13 @@ function getSkewTransform(skewXAngle = 0, skewYAngle = 0) {
206
271
  return [1, tanAlpha, tanBeta, 1, 0, 0];
207
272
  }
208
273
 
274
+ /**
275
+ * Apply the skew options to a content context.
276
+ * @private
277
+ * @param {Object} context - The content context.
278
+ * @param {Object} options - The path options: skewX and skewY.
279
+ * @returns {void}
280
+ */
209
281
  exports._setSkewContext = function _setSkewTransform(context, options) {
210
282
  if (options.skewX || options.skewY) {
211
283
  const sm = getSkewTransform(options.skewX, options.skewY);
@@ -214,12 +286,68 @@ exports._setSkewContext = function _setSkewTransform(context, options) {
214
286
  }
215
287
  };
216
288
 
289
+ /**
290
+ * Apply a [scaleX, scaleY] ratio to a content context.
291
+ * @private
292
+ * @param {Object} context - The content context.
293
+ * @param {Object} options - The path options: ratio.
294
+ * @returns {void}
295
+ */
217
296
  exports._setScalingTransform = function _setScalingTransform(context, options) {
218
297
  if (options.ratio) {
219
298
  context.cm(options.ratio[0], 0, 0, options.ratio[1], 0, 0);
220
299
  }
221
300
  };
222
301
 
302
+ /**
303
+ * Options for a low-level drawing call. A separation color is not a device
304
+ * color: the caller sets it on the form XObject, so the low-level call gets
305
+ * neither its color nor its colorspace.
306
+ * @private
307
+ * @param {Object} pathOptions - The path options.
308
+ * @returns {Object} The path options, or a copy without color and colorspace
309
+ * for a separation color.
310
+ */
311
+ exports._devicePathOptions = function _devicePathOptions(pathOptions) {
312
+ if (pathOptions.colorspace !== Colorspace.SEPARATION) return pathOptions;
313
+ const { color, colorspace, ...deviceOptions } = pathOptions;
314
+ return deviceOptions;
315
+ };
316
+
317
+ /**
318
+ * Select a Separation color in a form XObject. Device colors are left to the
319
+ * low-level drawing call, so their output is unchanged.
320
+ * @private
321
+ * @param {Object} xObject - The form the color is selected in.
322
+ * @param {Object} [colorModel] - The color model from _transformColor().
323
+ * @param {boolean} stroke - Select the stroking instead of the fill color.
324
+ * @returns {void}
325
+ */
326
+ exports._setSeparationColor = function _setSeparationColor(
327
+ xObject,
328
+ colorModel,
329
+ stroke,
330
+ ) {
331
+ if (!colorModel || colorModel.colorspace !== Colorspace.SEPARATION) return;
332
+ if (stroke) xObject.stroke(colorModel);
333
+ else xObject.fill(colorModel);
334
+ };
335
+
336
+ /**
337
+ * Draw content through a form XObject, creating it with the callback unless
338
+ * `options.xObject` supplies one, then place it with rotation, skew and
339
+ * scaling.
340
+ * @private
341
+ * @param {Recipe} self - The recipe instance.
342
+ * @param {number} x - The PDF x.
343
+ * @param {number} y - The PDF y.
344
+ * @param {number} width - The form width.
345
+ * @param {number} height - The form height.
346
+ * @param {Object} options - The path options.
347
+ * @param {function(Object, xObjectForm): void} callback - Draws into a new form.
348
+ * @returns {void}
349
+ * @throws {Error} If no page content context is active.
350
+ */
223
351
  exports._drawObject = function _drawObject(
224
352
  self,
225
353
  x,
@@ -253,12 +381,19 @@ exports._drawObject = function _drawObject(
253
381
  context.doXObject(xObject).Q();
254
382
  };
255
383
 
384
+ /**
385
+ * The PDF line cap number of a `Recipe.LineCap` value.
386
+ * @private
387
+ * @param {Recipe.LineCap} [type] - The cap style; round when omitted or unknown.
388
+ * @returns {number} 0 for butt, 1 for round, 2 for square.
389
+ */
256
390
  exports._lineCap = function _lineCap(type) {
257
391
  const round = 1;
258
392
  let cap = round;
259
393
 
260
394
  if (type) {
261
- const capStyle = ["butt", "round", "square"];
395
+ // In PDF line cap order.
396
+ const capStyle = [LineCap.BUTT, LineCap.ROUND, LineCap.SQUARE];
262
397
  const capType = capStyle.indexOf(type);
263
398
  cap = capType !== -1 ? capType : round;
264
399
  }
@@ -266,12 +401,19 @@ exports._lineCap = function _lineCap(type) {
266
401
  return cap;
267
402
  };
268
403
 
404
+ /**
405
+ * The PDF line join number of a `Recipe.LineJoin` value.
406
+ * @private
407
+ * @param {Recipe.LineJoin} [type] - The join style; round when omitted or unknown.
408
+ * @returns {number} 0 for miter, 1 for round, 2 for bevel.
409
+ */
269
410
  exports._lineJoin = function _lineJoin(type) {
270
411
  const round = 1;
271
412
  let join = round;
272
413
 
273
414
  if (type) {
274
- const joinStyle = ["miter", "round", "bevel"];
415
+ // In PDF line join order.
416
+ const joinStyle = [LineJoin.MITER, LineJoin.ROUND, LineJoin.BEVEL];
275
417
  const joinType = joinStyle.indexOf(type);
276
418
  join = joinType !== -1 ? joinType : round;
277
419
  }