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

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 (44) 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 +91 -38
  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 +385 -39
  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 +156 -14
  39. package/lib/recipe/vector.js +156 -52
  40. package/lib/recipe/xObjectForm.js +73 -10
  41. package/lib/recipe-constants.js +373 -0
  42. package/lib/recipe-options.js +47 -0
  43. package/muhammara.d.ts +3501 -160
  44. package/package.json +2 -3
@@ -14,6 +14,8 @@ const utils = require("./utils");
14
14
  * @returns {Recipe} The recipe instance.
15
15
  * @throws {RangeError} If a selection is not a positive integer or a two-value
16
16
  * range in ascending order.
17
+ * @throws {Error} If pages were deleted with deletePage() on this Recipe.
18
+ * @throws {Error} If the source PDF cannot be read.
17
19
  */
18
20
  exports.appendPage = function appendPage(pdfSrc, pages = []) {
19
21
  if (this.deletedPages?.size) {
@@ -1,5 +1,24 @@
1
1
  const muhammara = require("../muhammara");
2
2
  const fs = require("fs");
3
+ const { Colorspace, ChromaCommand } = require("../recipe-constants");
4
+
5
+ /**
6
+ * Register a color under a name as an own property, so any name, including
7
+ * `__proto__`, is stored and later found by an own-property lookup.
8
+ * @private
9
+ * @param {Object} colors - The colors of one colorspace.
10
+ * @param {string} name - The color name.
11
+ * @param {string} value - The color value.
12
+ * @returns {void}
13
+ */
14
+ function setKnownColor(colors, name, value) {
15
+ Object.defineProperty(colors, name, {
16
+ value,
17
+ writable: true,
18
+ enumerable: true,
19
+ configurable: true,
20
+ });
21
+ }
3
22
 
4
23
  this.knownColors = {
5
24
  // knownColors.colorspace.colorName = value
@@ -36,7 +55,7 @@ this.knownColors = {
36
55
  * PDF color space called 'separation' may also be used. The color value is then
37
56
  * treated as the alternative color when the named 'separation' color is unavailable.
38
57
  *
39
- * If the 'name' parameter is '!load', the second parameter is the name of a JSON
58
+ * If the 'name' parameter is `Recipe.ChromaCommand.LOAD` ('!load'), the second parameter is the name of a JSON
40
59
  * formatted file containing a formatted list of defined colors associated with the
41
60
  * color spaces rgb, cmyk, gray, or separation (think PANTONE color definitions).
42
61
  * This file will be merged with existing set of known colors. The color values
@@ -52,25 +71,28 @@ this.knownColors = {
52
71
  * @name chroma
53
72
  * @function
54
73
  * @memberof Recipe#
55
- * @param {string} name - the name to be associated to given color value, or '!load'
56
- * @param {string|number[]} value - the color value (HexColor, DecimalColor, or PercentColor), or name of '!load' file
57
- * @param {string} [colorspace=''] - One of: 'rgb', 'cmyk', 'gray', 'separation'.
74
+ * @param {string} name - the name to be associated to given color value, or `Recipe.ChromaCommand.LOAD`
75
+ * @param {string|number[]} value - the color value (HexColor, DecimalColor, or PercentColor), or the path of the JSON file to load
76
+ * @param {Recipe.Colorspace|""} [colorspace=''] - One of the `Recipe.Colorspace`
77
+ * values; empty picks gray, rgb or cmyk from the value length.
58
78
  * @returns {Recipe} The recipe instance.
79
+ * @throws {Error} If the file to load cannot be read or is not valid JSON.
59
80
  * @throws {Error} If a loaded color definition has an unrecognized colorspace.
60
81
  * @throws {Error} If a color value has an invalid size.
61
- * @throws {Error} If the colorspace is unknown.
82
+ * @throws {TypeError} If the colorspace is unknown.
62
83
  */
63
84
  exports.chroma = function chroma(name, value, colorspace = "") {
64
85
  if (name) {
65
- if (name === "!load") {
86
+ if (name === ChromaCommand.LOAD) {
66
87
  let newColors = JSON.parse(fs.readFileSync(value));
67
88
  // Add new colors to existing colorspaces
68
- for (let cs in newColors) {
69
- if (this.knownColors[cs]) {
70
- Object.assign(this.knownColors[cs], newColors[cs]);
71
- } else {
89
+ for (const cs of Object.keys(newColors)) {
90
+ if (!Object.values(Colorspace).includes(cs)) {
72
91
  throw new Error(`Unrecognized colorspace: ${cs}`);
73
92
  }
93
+ for (const [colorName, color] of Object.entries(newColors[cs])) {
94
+ setKnownColor(this.knownColors[cs], colorName, color);
95
+ }
74
96
  }
75
97
  } else {
76
98
  if (Array.isArray(value)) {
@@ -92,14 +114,18 @@ exports.chroma = function chroma(name, value, colorspace = "") {
92
114
  // Determine colorspace by length of given input
93
115
  // value when colorspace not provided in call.
94
116
  if (colorspace === "") {
95
- const colorSpaces = { 2: "gray", 6: "rgb", 8: "cmyk" };
117
+ const colorSpaces = {
118
+ 2: Colorspace.GRAY,
119
+ 6: Colorspace.RGB,
120
+ 8: Colorspace.CMYK,
121
+ };
96
122
  colorspace = colorSpaces[`${value.length}`];
97
- } else if (!["rgb", "cmyk", "gray", "separation"].includes(colorspace)) {
98
- throw new Error(`Unknown colorspace: ${colorspace}.`);
123
+ } else if (!Object.values(Colorspace).includes(colorspace)) {
124
+ throw new TypeError(`Unknown colorspace: ${colorspace}`);
99
125
  }
100
126
 
101
127
  if (colorspace) {
102
- this.knownColors[colorspace][name] = value;
128
+ setKnownColor(this.knownColors[colorspace], name, value);
103
129
  }
104
130
  }
105
131
  }
@@ -107,12 +133,23 @@ exports.chroma = function chroma(name, value, colorspace = "") {
107
133
  return this;
108
134
  };
109
135
 
136
+ /**
137
+ * Write a Separation color space for a named color once per Recipe and
138
+ * return its object ID.
139
+ * @private
140
+ * @param {Recipe} self - The recipe instance that owns the cache.
141
+ * @param {string} colorName - The separation color name.
142
+ * @param {number[]} color - The alternate device color components, 0 to 1.
143
+ * @returns {number} The object ID of the Separation color space.
144
+ */
110
145
  function createColorSpaces(self, colorName, color) {
111
146
  const deviceCS = { 1: "DeviceGray", 3: "DeviceRGB", 4: "DeviceCMYK" };
112
147
  const altCS = deviceCS[`${color.length}`];
113
- this.colorSpaces = this.colorSpaces || {};
114
- this.colorSpaces[altCS] = this.colorSpaces[altCS] || {};
115
- let colorSpaceID = this.colorSpaces[altCS][colorName];
148
+ // Cache per Recipe: the IDs belong to this Recipe's PDF writer. A Map keyed
149
+ // like Wasm's keeps ink names such as "__proto__" from hitting inherited keys.
150
+ self.colorSpaces = self.colorSpaces || new Map();
151
+ const key = `${altCS}:${colorName}`;
152
+ let colorSpaceID = self.colorSpaces.get(key);
116
153
 
117
154
  if (!colorSpaceID) {
118
155
  const transformFunction = tintTransform(self, color);
@@ -128,12 +165,20 @@ function createColorSpaces(self, colorName, color) {
128
165
  .endArray(muhammara.eTokenSeparatorEndLine)
129
166
  .endIndirectObject();
130
167
  self.resumeContext();
131
- this.colorSpaces[altCS][colorName] = colorSpaceID;
168
+ self.colorSpaces.set(key, colorSpaceID);
132
169
  }
133
170
 
134
171
  return colorSpaceID;
135
172
  }
136
173
 
174
+ /**
175
+ * Write the type 2 tint transform function that maps a separation tint to
176
+ * its alternate device color.
177
+ * @private
178
+ * @param {Recipe} self - The recipe instance.
179
+ * @param {number[]} color - The alternate device color components, 0 to 1.
180
+ * @returns {number} The object ID of the function.
181
+ */
137
182
  function tintTransform(self, color) {
138
183
  const rangeCount = color.length;
139
184
  self.pauseContext();
@@ -170,6 +215,14 @@ function tintTransform(self, color) {
170
215
  return tintFuncID;
171
216
  }
172
217
 
218
+ /**
219
+ * Write the stroke and fill opacity graphics states for a value once per
220
+ * Recipe.
221
+ * @private
222
+ * @param {number} value - The opacity, from 0 to 1.
223
+ * @returns {{stroke: number, fill: number}} The object IDs of the stroking
224
+ * (CA) and non-stroking (ca) ExtGState dictionaries.
225
+ */
173
226
  exports._createExtGStates = function _createExtGStates(value) {
174
227
  this.extGStates = this.extGStates || {};
175
228
  if (this.extGStates[value]) {
@@ -181,7 +234,7 @@ exports._createExtGStates = function _createExtGStates(value) {
181
234
  const objCxt = this.writer.getObjectsContext();
182
235
  const gsId = objCxt.startNewIndirectObject();
183
236
  const dict = objCxt.startDictionary();
184
- dict.writeKey("type");
237
+ dict.writeKey("Type");
185
238
  dict.writeNameValue("ExtGState");
186
239
  dict.writeKey(key);
187
240
  objCxt.writeNumber(value);
@@ -198,16 +251,22 @@ exports._createExtGStates = function _createExtGStates(value) {
198
251
  return this.extGStates[value];
199
252
  };
200
253
 
201
- function _defaultColor(colorspace = "rgb") {
254
+ /**
255
+ * The hex color used when a color value is missing or invalid.
256
+ * @private
257
+ * @param {Recipe.Colorspace} [colorspace=Recipe.Colorspace.RGB] - The colorspace.
258
+ * @returns {string} The default color as a hex string without '#'.
259
+ */
260
+ function _defaultColor(colorspace = Colorspace.RGB) {
202
261
  let defaultColor;
203
262
  switch (colorspace) {
204
- case "cmyk":
263
+ case Colorspace.CMYK:
205
264
  defaultColor = "FF000000";
206
265
  break;
207
- case "gray":
266
+ case Colorspace.GRAY:
208
267
  defaultColor = "00";
209
268
  break;
210
- case "rgb":
269
+ case Colorspace.RGB:
211
270
  default:
212
271
  defaultColor = "1777d1";
213
272
  break;
@@ -232,7 +291,7 @@ function _defaultColor(colorspace = "rgb") {
232
291
  * @private
233
292
  * @param {Recipe} self The recipe instance.
234
293
  * @param {string} code the color encoding as HexColor
235
- * @param {string} colorspace the name of the colorspace of given color code
294
+ * @param {Recipe.Colorspace} colorspace the name of the colorspace of given color code
236
295
  * @param {string} colorName the name to be associated with given color code
237
296
  * @returns {Object} The color model.
238
297
  */
@@ -246,17 +305,17 @@ function toColorModel(self, code, colorspace, colorName) {
246
305
 
247
306
  switch (color.length) {
248
307
  default:
249
- color = hexToArray(_defaultColor("rgb"));
308
+ color = hexToArray(_defaultColor(Colorspace.RGB));
250
309
  // purposely want to fall through to 'rgb' case below.
251
310
  case 3:
252
- cmodel.colorspace = "rgb";
311
+ cmodel.colorspace = Colorspace.RGB;
253
312
  cmodel.r = color[0];
254
313
  cmodel.g = color[1];
255
314
  cmodel.b = color[2];
256
315
  break;
257
316
 
258
317
  case 4:
259
- cmodel.colorspace = "cmyk";
318
+ cmodel.colorspace = Colorspace.CMYK;
260
319
  cmodel.c = color[0];
261
320
  cmodel.m = color[1];
262
321
  cmodel.y = color[2];
@@ -264,7 +323,7 @@ function toColorModel(self, code, colorspace, colorName) {
264
323
  break;
265
324
 
266
325
  case 1:
267
- cmodel.colorspace = "gray";
326
+ cmodel.colorspace = Colorspace.GRAY;
268
327
  cmodel.gray = color[0];
269
328
  break;
270
329
  }
@@ -273,7 +332,7 @@ function toColorModel(self, code, colorspace, colorName) {
273
332
  // use the colorspace from above as the
274
333
  // alternative color transformation when
275
334
  // the named color is unavailable.
276
- if (colorspace === "separation" && colorName !== "") {
335
+ if (colorspace === Colorspace.SEPARATION && colorName !== "") {
277
336
  cmodel.colorspace = colorspace;
278
337
  cmodel.colorName = colorName;
279
338
  cmodel.colorspaceId = createColorSpaces(self, colorName, color);
@@ -300,11 +359,23 @@ function percentToHex(code) {
300
359
  * @param {string|number[]} [code=''] Color specification in the form of HexColor (string beginning with '#'),
301
360
  * DecimalColor (1, 3, or 4 element array with values between 0-255),
302
361
  * PercentColor (string, begins with '%' followed by values separated
303
- * by commas with values between 0-100)
362
+ * by commas with values between 0-100), or a color name known to
363
+ * `chroma()`
364
+ * @param {Object} [opt] - The options.
365
+ * @param {Recipe.Colorspace} [opt.colorspace] - The colorspace; when omitted it
366
+ * is picked from the value length.
367
+ * @param {boolean} [opt.wantColorModel=false] - Return a color model instead of a number.
368
+ * @param {string} [opt.colorName] - The name to record the color under.
369
+ * @returns {number|Object} The color as a number, or the color model when
370
+ * `opt.wantColorModel` is set. Invalid values fall back to the default color.
371
+ * @throws {TypeError} If `opt.colorspace` is not a `Recipe.Colorspace` value.
304
372
  */
305
373
  exports._transformColor = function _transformColor(code = "", opt = {}) {
306
374
  this.knownColors = this.knownColors || {};
307
- let colorspace = opt.colorspace || "rgb";
375
+ if (opt.colorspace && !Object.values(Colorspace).includes(opt.colorspace)) {
376
+ throw new TypeError(`Unknown colorspace: ${opt.colorspace}`);
377
+ }
378
+ let colorspace = opt.colorspace || Colorspace.RGB;
308
379
  let wantColorModel = opt.wantColorModel || false;
309
380
  let colorName = opt.colorName || "";
310
381
  let defaultColor = _defaultColor(colorspace);
@@ -317,7 +388,10 @@ exports._transformColor = function _transformColor(code = "", opt = {}) {
317
388
  } else if (code.startsWith("%")) {
318
389
  code = percentToHex(code.replace("%", ""));
319
390
  } else if (code !== "") {
320
- let color = this.knownColors[colorspace][code]; // assuming code is a color name
391
+ // A color name counts only when registered, not an inherited key such as
392
+ // "constructor".
393
+ const colors = this.knownColors[colorspace];
394
+ let color = Object.hasOwn(colors, code) ? colors[code] : undefined;
321
395
  if (!color) {
322
396
  color = "";
323
397
  code = defaultColor;
@@ -339,7 +413,10 @@ exports._transformColor = function _transformColor(code = "", opt = {}) {
339
413
  // When colorspace is not explicitly given,
340
414
  // use size of value to determine colorspace.
341
415
  if (!opt.colorspace) {
342
- colorspace = { 2: "gray", 6: "rgb", 8: "cmyk" }[`${code.length}`] || "rgb";
416
+ colorspace =
417
+ { 2: Colorspace.GRAY, 6: Colorspace.RGB, 8: Colorspace.CMYK }[
418
+ `${code.length}`
419
+ ] || Colorspace.RGB;
343
420
  defaultColor = _defaultColor(colorspace);
344
421
  }
345
422
 
@@ -347,7 +424,7 @@ exports._transformColor = function _transformColor(code = "", opt = {}) {
347
424
  // when colorspace is given and given color code does not have appropriate length, or
348
425
  // when colorspace is missing, verify allowable hex value sizes for rgb, cmyk, or gray.
349
426
  if (
350
- (["rgb", "cmyk", "gray"].includes(colorspace) &&
427
+ ([Colorspace.RGB, Colorspace.CMYK, Colorspace.GRAY].includes(colorspace) &&
351
428
  code.length != defaultColor.length) ||
352
429
  ![2, 6, 8].includes(code.toString().length)
353
430
  ) {
@@ -356,7 +433,7 @@ exports._transformColor = function _transformColor(code = "", opt = {}) {
356
433
 
357
434
  if (wantColorModel) {
358
435
  transformation = toColorModel(this, code, colorspace, colorName);
359
- if (colorName && !this.knownColors[colorspace][colorName]) {
436
+ if (colorName && !Object.hasOwn(this.knownColors[colorspace], colorName)) {
360
437
  this.chroma(colorName, code, colorspace);
361
438
  }
362
439
  } else {
@@ -366,6 +443,12 @@ exports._transformColor = function _transformColor(code = "", opt = {}) {
366
443
  return transformation;
367
444
  };
368
445
 
446
+ /**
447
+ * Convert DecimalColor components (0 to 255) to a hex string.
448
+ * @private
449
+ * @param {number[]} [color] - The color components.
450
+ * @returns {string} The hex string without '#'.
451
+ */
369
452
  function arrayToHex(color = []) {
370
453
  let code = "";
371
454
  color.forEach((item) => {
@@ -376,6 +459,13 @@ function arrayToHex(color = []) {
376
459
  return code;
377
460
  }
378
461
 
462
+ /**
463
+ * Convert a 2, 6 or 8 digit hex color to components between 0 and 1.
464
+ * @private
465
+ * @param {string} hex - The hex color, with or without '#'.
466
+ * @returns {number[]} One gray, three RGB or four CMYK components.
467
+ * @throws {TypeError} If the value is not a 2, 4, 6 or 8 digit hex string.
468
+ */
379
469
  function hexToArray(hex) {
380
470
  const result =
381
471
  /^#?([a-f\d]{2})([a-f\d]{2})?([a-f\d]{2})?([a-f\d]{2})?$/i.exec(hex);
@@ -387,6 +477,13 @@ function hexToArray(hex) {
387
477
  }, []);
388
478
  }
389
479
 
480
+ /**
481
+ * Split an RGB color number into its 0 to 255 components.
482
+ * @private
483
+ * @param {number} bigint - The color as 0xRRGGBB.
484
+ * @returns {{r: number, g: number, b: number}} The components; black for 0 or
485
+ * a missing value.
486
+ */
390
487
  exports._colorNumberToRGB = (bigint) => {
391
488
  if (!bigint) {
392
489
  return {
@@ -1,20 +1,52 @@
1
- exports._centrify = function _centrify(x, y, pageNumber) {
2
- pageNumber = pageNumber || this.pageNumber;
3
- const { width, height } = this.metadata[pageNumber];
4
- switch (x) {
5
- case "center":
6
- x = width / 2;
7
- break;
8
- }
9
- switch (y) {
10
- case "center":
11
- y = height / 2;
12
- break;
1
+ const { Coordinate } = require("../recipe-constants");
2
+
3
+ /**
4
+ * Look up the metadata of a page, or of the active page.
5
+ * @private
6
+ * @param {Recipe} recipe - The recipe instance.
7
+ * @param {number} [pageNumber] - The one-based page number; defaults to the active page.
8
+ * @returns {Object} The page metadata.
9
+ * @throws {TypeError} If there is no such page, for example before createPage().
10
+ */
11
+ function pageMetadata(recipe, pageNumber) {
12
+ const metadata = recipe.metadata[pageNumber || recipe.pageNumber];
13
+ if (!metadata) {
14
+ throw new TypeError(
15
+ "No page is active; call createPage() or editPage() first",
16
+ );
13
17
  }
18
+ return metadata;
19
+ }
20
+
21
+ /**
22
+ * Replace `Recipe.Coordinate.CENTER` coordinates with the page center.
23
+ * @private
24
+ * @param {number|"center"} x - The x coordinate.
25
+ * @param {number|"center"} y - The y coordinate.
26
+ * @param {number} [pageNumber] - The one-based page number; defaults to the active page.
27
+ * @returns {number[]} The numeric [x, y].
28
+ * @throws {TypeError} If no page is active.
29
+ */
30
+ exports._centrify = function _centrify(x, y, pageNumber) {
31
+ const { width, height } = pageMetadata(this, pageNumber);
32
+ if (x === Coordinate.CENTER) x = width / 2;
33
+ if (y === Coordinate.CENTER) y = height / 2;
14
34
 
15
35
  return [x, y];
16
36
  };
17
37
 
38
+ /**
39
+ * Convert Recipe coordinates (top-left origin, y down) to PDF coordinates
40
+ * (bottom-left origin of the media box, y up).
41
+ * @private
42
+ * @param {number|"center"} x - The Recipe x coordinate.
43
+ * @param {number|"center"} y - The Recipe y coordinate.
44
+ * @param {number} [offsetX=0] - Added to the PDF x coordinate.
45
+ * @param {number} [offsetY=0] - Added to the PDF y coordinate.
46
+ * @param {number} [pageNumber] - The one-based page number; defaults to the active page.
47
+ * @returns {{nx: number, ny: number}} The PDF coordinates.
48
+ * @throws {TypeError} If no page is active.
49
+ */
18
50
  exports._calibrateCoordinate = function _calibrateCoordinate(
19
51
  x,
20
52
  y,
@@ -23,7 +55,7 @@ exports._calibrateCoordinate = function _calibrateCoordinate(
23
55
  pageNumber,
24
56
  ) {
25
57
  pageNumber = pageNumber || this.pageNumber;
26
- const { height, mediaBox } = this.metadata[pageNumber];
58
+ const { height, mediaBox } = pageMetadata(this, pageNumber);
27
59
  const startX = mediaBox[0];
28
60
  const startY = mediaBox[1];
29
61
  [x, y] = this._centrify(x, y, pageNumber);
@@ -36,6 +68,18 @@ exports._calibrateCoordinate = function _calibrateCoordinate(
36
68
  };
37
69
  };
38
70
 
71
+ /**
72
+ * Convert Recipe coordinates to PDF coordinates for an annotation, undoing
73
+ * the page rotation so the annotation lands where it appears on screen.
74
+ * @private
75
+ * @param {number|"center"} x - The Recipe x coordinate.
76
+ * @param {number|"center"} y - The Recipe y coordinate.
77
+ * @param {number} [offsetX=0] - Added to the PDF x coordinate.
78
+ * @param {number} [offsetY=0] - Added to the PDF y coordinate.
79
+ * @param {number} pageNumber - The one-based page number.
80
+ * @returns {{nx: number, ny: number}} The PDF coordinates.
81
+ * @throws {TypeError} If the page is unknown.
82
+ */
39
83
  exports._calibrateCoordinateForAnnots = function _calibrateCoordinateForAnnots(
40
84
  x,
41
85
  y,
@@ -50,7 +94,7 @@ exports._calibrateCoordinateForAnnots = function _calibrateCoordinateForAnnots(
50
94
  offsetY,
51
95
  pageNumber,
52
96
  );
53
- const { width, height, rotate, mediaBox } = this.metadata[pageNumber];
97
+ const { width, height, rotate, mediaBox } = pageMetadata(this, pageNumber);
54
98
  const startX = mediaBox[0];
55
99
  const startY = mediaBox[1];
56
100
  let rotateOffsetX = 0,
@@ -86,6 +130,17 @@ exports._calibrateCoordinateForAnnots = function _calibrateCoordinateForAnnots(
86
130
  };
87
131
  };
88
132
 
133
+ /**
134
+ * Convert PDF coordinates back to Recipe coordinates.
135
+ * @private
136
+ * @param {number} x - The PDF x coordinate.
137
+ * @param {number} y - The PDF y coordinate.
138
+ * @param {number} [offsetX=0] - Subtracted from x.
139
+ * @param {number} [offsetY=0] - Subtracted from the Recipe y coordinate.
140
+ * @param {number} [pageNumber] - The one-based page number; defaults to the active page.
141
+ * @returns {{ox: number, oy: number}} The Recipe coordinates.
142
+ * @throws {TypeError} If no page is active.
143
+ */
89
144
  exports._reverseCoordinate = function _reverseCoordinate(
90
145
  x,
91
146
  y,
@@ -93,8 +148,7 @@ exports._reverseCoordinate = function _reverseCoordinate(
93
148
  offsetY = 0,
94
149
  pageNumber,
95
150
  ) {
96
- pageNumber = pageNumber || this.pageNumber;
97
- const { height } = this.metadata[pageNumber];
151
+ const { height } = pageMetadata(this, pageNumber);
98
152
  const ox = x - offsetX;
99
153
  const oy = height - y - offsetY;
100
154
  return {
@@ -103,6 +157,18 @@ exports._reverseCoordinate = function _reverseCoordinate(
103
157
  };
104
158
  };
105
159
 
160
+ /**
161
+ * Rotate a point around a center and translate it.
162
+ * @private
163
+ * @param {number} cx - The center x.
164
+ * @param {number} cy - The center y.
165
+ * @param {number} x - The point x.
166
+ * @param {number} y - The point y.
167
+ * @param {number} angle - The rotation in degrees, clockwise in PDF space.
168
+ * @param {number} [offsetX=0] - Added to the rotated x.
169
+ * @param {number} [offsetY=0] - Added to the rotated y.
170
+ * @returns {{nx: number, ny: number}} The rotated point.
171
+ */
106
172
  function rotateCoord(cx, cy, x, y, angle, offsetX = 0, offsetY = 0) {
107
173
  const radians = (Math.PI / 180) * angle,
108
174
  cos = Math.cos(radians),
@@ -1,5 +1,6 @@
1
1
  const muhammara = require("../muhammara");
2
2
  const fs = require("fs");
3
+ const { Permission } = require("../recipe-constants");
3
4
 
4
5
  /**
5
6
  * Encryption user access permissions
@@ -10,23 +11,25 @@ const fs = require("fs");
10
11
  * @name permission
11
12
  * @function
12
13
  * @memberof Recipe#
13
- * @param {string} [flags='print'] From the list print, modify, copy, edit, fillform, extract, assemble, and printbest.
14
- * More than one may be specified by using a comma to separate the names in the input string.
14
+ * @param {string} [flags='print'] One or more `Recipe.Permission` values
15
+ * (print, modify, copy, edit, fillform, extract, assemble, printbest),
16
+ * separated by commas, for example `[Permission.PRINT, Permission.COPY].join()`.
15
17
  * @returns {number} The numeric user protection flag.
18
+ * @throws {Error} If a name is not a `Recipe.Permission` value.
16
19
  */
17
- exports.permission = function permission(flags = "print") {
20
+ exports.permission = function permission(flags = Permission.PRINT) {
18
21
  // https://www.adobe.com/content/dam/acom/en/devnet/pdf/pdfs/PDF32000_2008.pdf
19
22
 
20
23
  const userAccessPermissions = {
21
24
  // see table on page 61 of above document
22
- print: 1 << 2, // allow printing
23
- modify: 1 << 3, // allow template creation, signing, filling form fields
24
- copy: 1 << 4, // allow content copying and copying for accessibility
25
- edit: 1 << 5, // allow commenting
26
- fillform: 1 << 8, // allow filling of form fields
27
- extract: 1 << 9, // allow content copying for accessibility
28
- assemble: 1 << 10, // unused
29
- printbest: 1 << 11, // allow high resolution printing when 'print' is allowed
25
+ [Permission.PRINT]: 1 << 2, // allow printing
26
+ [Permission.MODIFY]: 1 << 3, // allow template creation, signing, filling form fields
27
+ [Permission.COPY]: 1 << 4, // allow content copying and copying for accessibility
28
+ [Permission.EDIT]: 1 << 5, // allow commenting
29
+ [Permission.FILL_FORM]: 1 << 8, // allow filling of form fields
30
+ [Permission.EXTRACT]: 1 << 9, // allow content copying for accessibility
31
+ [Permission.ASSEMBLE]: 1 << 10, // unused
32
+ [Permission.PRINT_BEST]: 1 << 11, // allow high resolution printing when 'print' is allowed
30
33
  };
31
34
 
32
35
  const perms = flags.split(",").map((x) => {
@@ -43,6 +46,15 @@ exports.permission = function permission(flags = "print") {
43
46
  return access;
44
47
  };
45
48
 
49
+ /**
50
+ * Build writer encryption options from Recipe options. `password` and
51
+ * `ownerPassword` are aliases; when only an owner password is given the user
52
+ * password is empty and the default permission applies.
53
+ * @private
54
+ * @param {Object} options - The Recipe or encrypt() options.
55
+ * @param {boolean} [addPermissions=true] - Copy `userProtectionFlag` into the result.
56
+ * @returns {Object} The writer encryption options; empty when no password is set.
57
+ */
46
58
  exports._getEncryptOptions = function _getEncryptOptions(
47
59
  options,
48
60
  addPermissions = true,
@@ -89,8 +101,9 @@ exports._getEncryptOptions = function _getEncryptOptions(
89
101
  * @param {string} [options.password] - The permission password.
90
102
  * @param {string} [options.ownerPassword] - The password for editing.
91
103
  * @param {string} [options.userPassword] - The password for viewing & encryption.
92
- * @param {number} [options.userProtectionFlag] - The flag for the security level.
93
- * @returns {Recipe} The recipe instance.
104
+ * @param {number} [options.userProtectionFlag] - The flag for the security level, see `permission()`.
105
+ * @returns {Recipe} The recipe instance. The file is encrypted by `endPDF()`;
106
+ * Buffer sources are not encrypted.
94
107
  */
95
108
  exports.encrypt = function encrypt(options = {}) {
96
109
  this.needToEncrypt = true;
@@ -100,6 +113,12 @@ exports.encrypt = function encrypt(options = {}) {
100
113
  };
101
114
 
102
115
  // http://pdfhummus.com/post/147451287581/hummus-1058-and-pdf-writer-updates-encryption
116
+ /**
117
+ * Re-encrypt the finished output file with the encrypt() options.
118
+ * @private
119
+ * @returns {void}
120
+ * @throws {Error} If the output cannot be renamed, re-encrypted or removed.
121
+ */
103
122
  exports._encrypt = function _encrypt() {
104
123
  if (!this.encryption_) {
105
124
  return;