@muhammara/native-core 7.0.0-beta.1 → 7.0.0-beta.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.
@@ -81,6 +81,8 @@ exports._setParameters = function _setParameters() {
81
81
  };
82
82
 
83
83
  this._margin = Object.assign({}, this.default.pageMargin);
84
+ this.contextState = "idle";
85
+ this.modifiedSourcePages = new Set();
84
86
 
85
87
  // Object.assign(this, options);
86
88
  };
@@ -10,14 +10,15 @@ function escapePDFLiteralString(value) {
10
10
  * @memberof Recipe#
11
11
  * @param {string} text Text to replace.
12
12
  * @param {string} replacement Replacement text.
13
- * @param {number} [pageNumber=1] One-based page number.
13
+ * @param {number} pageNumber One-based page number.
14
14
  */
15
15
  exports.replaceText = function replaceText(text, replacement, pageNumber) {
16
- pageNumber = pageNumber || 1;
17
-
18
16
  if (typeof text !== "string" || typeof replacement !== "string") {
19
17
  throw new TypeError("replaceText expects text and replacement strings");
20
18
  }
19
+ if (!Number.isInteger(pageNumber) || pageNumber < 1) {
20
+ throw new TypeError("replaceText expects a positive integer page number");
21
+ }
21
22
 
22
23
  var pageIndex = pageNumber - 1;
23
24
  var page = this.pdfReader.parsePage(pageIndex).getDictionary();
@@ -60,5 +61,6 @@ exports.replaceText = function replaceText(text, replacement, pageNumber) {
60
61
  replacementStream.getWriteStream().write(Array.from(Buffer.from(replaced)));
61
62
  objectsContext.endPDFStream(replacementStream).endIndirectObject();
62
63
  this.writer.replaceObject(pageIndex, contentsObjectId, replacementObjectId);
64
+ this.modifiedSourcePages.add(pageNumber);
63
65
  return this;
64
66
  };
@@ -155,7 +155,10 @@ exports.n_gon = function n_gon(cx, cy, radius, sides = 3, options = {}) {
155
155
 
156
156
  const ngon = _n_gon(sides, cx, cy, radius, options);
157
157
 
158
- this.polygon(ngon, options);
158
+ const drawOptions = Object.assign({}, options, { link: undefined });
159
+ this.polygon(ngon, drawOptions);
160
+ if (options.link)
161
+ this.link(options.link, cx - radius, cy - radius, radius * 2, radius * 2);
159
162
 
160
163
  if (options.rotationVertice) {
161
164
  delete options["rotationOrigin"]; // cleanup n-gon generated point
@@ -220,6 +223,7 @@ exports.star = function star(cx, cy, radius, points = 5, options = {}) {
220
223
  }
221
224
 
222
225
  const starOptions = Object.assign({}, options);
226
+ delete starOptions.link;
223
227
 
224
228
  if (odd(points)) {
225
229
  starPath = _oddStar(_n_gon(points, cx, cy, radius, options));
@@ -265,6 +269,9 @@ exports.star = function star(cx, cy, radius, points = 5, options = {}) {
265
269
 
266
270
  this.polygon(starPath, starOptions);
267
271
 
272
+ if (options.link)
273
+ this.link(options.link, cx - radius, cy - radius, radius * 2, radius * 2);
274
+
268
275
  if (options.debug) {
269
276
  this.circle(cx, cy, radius, { width: 1, stroke: "#00ff00" });
270
277
  this.circle(cx, cy, 2, { fill: "#ff0000" });
@@ -375,6 +382,7 @@ exports.triangle = function triangle(x, y, traits, options = {}) {
375
382
  let traitID = options.traitID || options.traitsID || "sss";
376
383
  let position = options.position ? options.position.toLowerCase() : "default";
377
384
  let triopts = Object.assign({}, options);
385
+ delete triopts.link;
378
386
 
379
387
  if (traits.length !== 3) {
380
388
  throw new Error(
@@ -433,6 +441,19 @@ exports.triangle = function triangle(x, y, traits, options = {}) {
433
441
  }
434
442
 
435
443
  this.polygon(trigon, triopts);
444
+ if (options.link) {
445
+ const xs = trigon.map((point) => point[0]);
446
+ const ys = trigon.map((point) => point[1]);
447
+ const left = Math.min(...xs);
448
+ const top = Math.min(...ys);
449
+ this.link(
450
+ options.link,
451
+ left,
452
+ top,
453
+ Math.max(...xs) - left,
454
+ Math.max(...ys) - top,
455
+ );
456
+ }
436
457
 
437
458
  if (options.debug) {
438
459
  let angle = triopts.rotation || 0;
@@ -1,5 +1,5 @@
1
1
  const charSpacing = function charSpacing(text, charSpace) {
2
- let txt = text.trim();
2
+ let txt = text.replace(/^(?:(?!\u00a0)\s)+|(?:(?!\u00a0)\s)+$/g, "");
3
3
  return txt.length ? (txt.length - 1) * charSpace : 0;
4
4
  };
5
5
 
@@ -2,6 +2,7 @@ const LineBreaker = require("linebreak");
2
2
  const { Word, Line, Column } = require("./text.helper");
3
3
  const { htmlToTextObjects } = require("./htmlToTextObjects");
4
4
  const { Color, xObjectForm } = require("./xObjectForm");
5
+ const { linkPdf } = require("./annotation");
5
6
  const muhammara = require("../muhammara");
6
7
  const { UsedFont } = require("../muhammara");
7
8
 
@@ -258,6 +259,8 @@ exports.text = function text(text = "", x, y, options = {}) {
258
259
  return this;
259
260
  }
260
261
  options = _initOptions(this, x, y, options);
262
+ const linkX = this.x;
263
+ const linkY = this.y;
261
264
 
262
265
  const targetAnnotations = options;
263
266
  const originCoord = this._calibrateCoordinate(
@@ -273,6 +276,7 @@ exports.text = function text(text = "", x, y, options = {}) {
273
276
  originCoord.ny,
274
277
  );
275
278
  pathOptions.html = options.html;
279
+ pathOptions.link = options.link;
276
280
  pathOptions.hilite = options.hilite;
277
281
 
278
282
  // save text state for continued text?
@@ -294,6 +298,7 @@ exports.text = function text(text = "", x, y, options = {}) {
294
298
  textBox,
295
299
  pathOptions,
296
300
  );
301
+ const linkAnnotations = [];
297
302
 
298
303
  if (!textBox.width) {
299
304
  textBox.width = toWriteTextObjects[0].lineWidth;
@@ -316,7 +321,7 @@ exports.text = function text(text = "", x, y, options = {}) {
316
321
  toWriteTextObjects = clippedText.textObjects;
317
322
  textBox.textHeight = getTextBoxHeight(toWriteTextObjects);
318
323
 
319
- if (clippedText.remainder.length) {
324
+ if (clippedText.clipped) {
320
325
  clipResult = {
321
326
  remainder: clippedText.remainder,
322
327
  linesWritten: clippedText.linesWritten,
@@ -641,6 +646,15 @@ exports.text = function text(text = "", x, y, options = {}) {
641
646
  const x = next_x || getStartX(content.startX, content);
642
647
  const y = currentY;
643
648
  next_x = writeText(context, x, y, content);
649
+ if (content.writeOptions.link) {
650
+ linkAnnotations.push({
651
+ url: content.writeOptions.link,
652
+ left: x,
653
+ bottom: y,
654
+ width: next_x ? next_x - x : content.lineWidth,
655
+ height: content.lineHeight,
656
+ });
657
+ }
644
658
  });
645
659
  // The line offset from the last line in the
646
660
  // group determines Y positioning for next line.
@@ -739,6 +753,15 @@ exports.text = function text(text = "", x, y, options = {}) {
739
753
  const x = next_x || getStartX(content.startX, content);
740
754
  const y = currentY;
741
755
  next_x = writeText(context, x, y, content);
756
+ if (content.writeOptions.link) {
757
+ linkAnnotations.push({
758
+ url: content.writeOptions.link,
759
+ left: x,
760
+ bottom: y,
761
+ width: next_x ? next_x - x : content.lineWidth,
762
+ height: content.lineHeight,
763
+ });
764
+ }
742
765
  }
743
766
 
744
767
  // Flush any left over text objects.
@@ -754,6 +777,16 @@ exports.text = function text(text = "", x, y, options = {}) {
754
777
  }
755
778
  }
756
779
 
780
+ linkAnnotations.forEach((annotation) => {
781
+ linkPdf(
782
+ this,
783
+ annotation.url,
784
+ annotation.left,
785
+ annotation.bottom,
786
+ annotation.width,
787
+ annotation.height,
788
+ );
789
+ });
757
790
  return this;
758
791
  };
759
792
 
@@ -798,6 +831,13 @@ exports._layoutText = function _layoutText(textObjects, textBox, pathOptions) {
798
831
 
799
832
  let firstLineHeight;
800
833
  let toWriteTextObjects = [];
834
+ /** Reports whether a layout node contains content other than the break sentinel. */
835
+ const hasRenderableContent = (textObject) =>
836
+ (textObject.value !== undefined &&
837
+ textObject.value !== null &&
838
+ textObject.value !== "" &&
839
+ textObject.value !== "[@@DONOT_RENDER_THIS@@]") ||
840
+ textObject.childs?.some(hasRenderableContent);
801
841
 
802
842
  const writeValue = (textObject) => {
803
843
  textObject.lineID = textObject.lineID || Date.now() * Math.random();
@@ -832,6 +872,7 @@ exports._layoutText = function _layoutText(textObjects, textBox, pathOptions) {
832
872
  textObject.layer++;
833
873
 
834
874
  textObject.currentIndex = 0;
875
+ let prependValue = textObject.prependValue;
835
876
 
836
877
  textObject.childs.forEach((child) => {
837
878
  if (textObject.tag == "ul") {
@@ -852,11 +893,17 @@ exports._layoutText = function _layoutText(textObjects, textBox, pathOptions) {
852
893
  child.layer = textObject.layer - 1;
853
894
  }
854
895
  }
855
- if (textObject.prependValue) {
856
- child.prependValue = !["ol", "ul"].includes(textObject.tag)
857
- ? textObject.prependValue
858
- : child.prependValue;
859
- textObject.indent = 2 * textObject.layer;
896
+ if (
897
+ prependValue &&
898
+ !["ol", "ul"].includes(child.tag) &&
899
+ hasRenderableContent(child)
900
+ ) {
901
+ child.prependValue = prependValue;
902
+ prependValue = null;
903
+ textObject.indent =
904
+ textObject.tag == "li"
905
+ ? 2 * textObject.layer
906
+ : textObject.indent || 2 * textObject.layer;
860
907
  }
861
908
  if (textObject.indent) {
862
909
  child.indent = child.indent || textObject.indent;
@@ -866,6 +913,7 @@ exports._layoutText = function _layoutText(textObjects, textBox, pathOptions) {
866
913
  child.sizeRatios = [...textObject.sizeRatios, child.sizeRatio];
867
914
  }
868
915
  child.styles = Object.assign(child.styles, textObject.styles);
916
+ child.link = child.link || textObject.link;
869
917
 
870
918
  child.isBold = textObject.isBold ? textObject.isBold : child.isBold;
871
919
  child.isItalic = textObject.isItalic
@@ -887,7 +935,55 @@ exports._layoutText = function _layoutText(textObjects, textBox, pathOptions) {
887
935
  writeValue(textObject);
888
936
  });
889
937
 
890
- return { toWriteTextObjects: toWriteTextObjects, textHeight: totalHeight };
938
+ const normalizedTextObjects = [];
939
+ let pendingBreaks = [];
940
+ const replacementLineIDs = new Map();
941
+ /** Converts pending break sentinels into line state without rendering their text. */
942
+ const appendPendingBreaks = (nextTextObject) => {
943
+ const previous = normalizedTextObjects[normalizedTextObjects.length - 1];
944
+ if (previous) previous.lineComplete = true;
945
+ const blankBreaks =
946
+ previous && nextTextObject ? pendingBreaks.slice(1) : pendingBreaks;
947
+ blankBreaks.forEach((breakObject, index) => {
948
+ normalizedTextObjects.push({
949
+ ...breakObject,
950
+ lineID: previous ? pendingBreaks[index].lineID : breakObject.lineID,
951
+ text: "",
952
+ lineComplete: true,
953
+ lineWidth: 0,
954
+ textWidth: 0,
955
+ });
956
+ });
957
+ if (nextTextObject && previous) {
958
+ replacementLineIDs.set(
959
+ nextTextObject.lineID,
960
+ pendingBreaks[pendingBreaks.length - 1].lineID,
961
+ );
962
+ }
963
+ pendingBreaks = [];
964
+ };
965
+ toWriteTextObjects.forEach((textObject, index, objects) => {
966
+ const sentinel = textObject.text.trim() == "[@@DONOT_RENDER_THIS@@]";
967
+ const beforeSentinel =
968
+ textObject.text.trim() == "" &&
969
+ objects[index + 1]?.text.trim() == "[@@DONOT_RENDER_THIS@@]";
970
+ if (beforeSentinel) return;
971
+ if (sentinel) {
972
+ pendingBreaks.push(textObject);
973
+ return;
974
+ }
975
+ if (pendingBreaks.length) appendPendingBreaks(textObject);
976
+ textObject.lineID =
977
+ replacementLineIDs.get(textObject.lineID) || textObject.lineID;
978
+ normalizedTextObjects.push(textObject);
979
+ });
980
+ if (pendingBreaks.length) appendPendingBreaks();
981
+ toWriteTextObjects = normalizedTextObjects;
982
+
983
+ return {
984
+ toWriteTextObjects: toWriteTextObjects,
985
+ textHeight: getTextBoxHeight(toWriteTextObjects) || totalHeight,
986
+ };
891
987
  };
892
988
 
893
989
  function getTextBoxHeight(textObjs) {
@@ -949,6 +1045,7 @@ function clipTextToBox(textObjs, availableHeight) {
949
1045
  return {
950
1046
  textObjects: visibleLines.flat(),
951
1047
  linesWritten,
1048
+ clipped: remainderLines.length > 0,
952
1049
  // Preserve line boundaries so a remainder can be written into another box.
953
1050
  remainder: remainderLines
954
1051
  .map((currentLine) => currentLine.map((textObj) => textObj.text).join(""))
@@ -1195,6 +1292,7 @@ function makeTextObjects(self, textObject = {}, pathOptions, textBox = {}) {
1195
1292
  ? textBox.textAlign.split(" ")
1196
1293
  : [];
1197
1294
  const writeOptions = Object.assign({}, pathOptions, {
1295
+ link: textObject.link || pathOptions.link,
1198
1296
  color: textObject.styles.color,
1199
1297
  opacity: parseFloat(textObject.styles.opacity || pathOptions.opacity || 1),
1200
1298
  underline: textObject.underline || pathOptions.underline,
@@ -1216,7 +1314,7 @@ function makeTextObjects(self, textObject = {}, pathOptions, textBox = {}) {
1216
1314
  const indent = textObject.indent || 0;
1217
1315
 
1218
1316
  const lineMaxWidth = textBox.width
1219
- ? textBox.width - textBox.paddingLeft - textBox.paddingRight - indent
1317
+ ? textBox.width - textBox.paddingLeft - textBox.paddingRight
1220
1318
  : null;
1221
1319
  let remainderWidth = lineMaxWidth;
1222
1320
  let newLine;
@@ -122,6 +122,7 @@ exports.polygon = function polygon(coordinates = [], options = {}) {
122
122
  height,
123
123
  pathOptions,
124
124
  (ctx, xObject) => {
125
+ ctx.gs(xObject.getGsName(pathOptions.strokeGsId));
125
126
  ctx.w(pathOptions.width);
126
127
  setPathOptions(ctx);
127
128
  xObject.stroke(colorModel);
@@ -131,5 +132,15 @@ exports.polygon = function polygon(coordinates = [], options = {}) {
131
132
  );
132
133
  }
133
134
 
135
+ if (options.link) {
136
+ this.link(
137
+ options.link,
138
+ boundBox[0] - margin,
139
+ boundBox[1] - margin,
140
+ width,
141
+ height,
142
+ );
143
+ }
144
+
134
145
  return this;
135
146
  };
@@ -30,9 +30,21 @@ exports._getPathOptions = function _getPathOptions(
30
30
  width: 2,
31
31
  align: options.align,
32
32
  };
33
+ const lineStyle = this.current.lineStyle || {};
34
+
35
+ if (lineStyle.width !== undefined) pathOptions.width = lineStyle.width;
36
+ if (lineStyle.cap !== undefined) pathOptions.lineCap = lineStyle.cap;
37
+ if (lineStyle.join !== undefined) pathOptions.lineJoin = lineStyle.join;
38
+ if (lineStyle.miterLimit !== undefined)
39
+ pathOptions.miterLimit = lineStyle.miterLimit;
40
+ if (lineStyle.dash !== undefined) pathOptions.dash = lineStyle.dash;
41
+ if (lineStyle.dashPhase !== undefined)
42
+ pathOptions.dashPhase = lineStyle.dashPhase;
43
+ if (pathOptions.dash === undefined) pathOptions.dash = [];
44
+ if (pathOptions.dashPhase === undefined) pathOptions.dashPhase = 0;
33
45
 
34
46
  if (options.opacity == void 0 || isNaN(options.opacity)) {
35
- options.opacity = 1;
47
+ options.opacity = this.current.opacity ?? 1;
36
48
  } else {
37
49
  options.opacity =
38
50
  options.opacity < 0 ? 0 : options.opacity > 1 ? 1 : options.opacity;
@@ -95,8 +107,8 @@ exports._getPathOptions = function _getPathOptions(
95
107
  }
96
108
 
97
109
  // Page 127 of PDF 1.7 specification
98
- pathOptions.dash = Array.isArray(options.dash) ? options.dash : [];
99
- pathOptions.dashPhase = !isNaN(options.dashPhase) ? options.dashPhase : 0;
110
+ if (Array.isArray(options.dash)) pathOptions.dash = options.dash;
111
+ if (!isNaN(options.dashPhase)) pathOptions.dashPhase = options.dashPhase;
100
112
  if (pathOptions.dash[0] == 0 && pathOptions.dash[1] == 0) {
101
113
  pathOptions.dash = []; // no dash, solid unbroken line
102
114
  pathOptions.dashPhase = 0;
@@ -12,6 +12,8 @@
12
12
  // DecimalColor component values range from 0 to 255.
13
13
  // PercentColor component values range from 1 to 100.
14
14
 
15
+ const { linkPdf } = require("./annotation");
16
+
15
17
  /**
16
18
  * Draw a circle
17
19
  * @name circle
@@ -74,7 +76,8 @@ exports.circle = function circle(x, y, radius, options = {}) {
74
76
  diameter,
75
77
  diameter,
76
78
  pathOptions,
77
- (ctx) => {
79
+ (ctx, xObject) => {
80
+ ctx.gs(xObject.getGsName(pathOptions.strokeGsId));
78
81
  ctx
79
82
  .d(pathOptions.dash, pathOptions.dashPhase)
80
83
  .drawCircle(
@@ -88,6 +91,8 @@ exports.circle = function circle(x, y, radius, options = {}) {
88
91
  },
89
92
  );
90
93
  }
94
+ if (options.link)
95
+ this.link(options.link, x - radius, y - radius, diameter, diameter);
91
96
  return this;
92
97
  };
93
98
 
@@ -173,6 +178,7 @@ exports.rectangle = function rectangle(x, y, width, height, options = {}) {
173
178
  height,
174
179
  pathOptions,
175
180
  (ctx, xObject) => {
181
+ ctx.gs(xObject.getGsName(pathOptions.strokeGsId));
176
182
  // ... requires adjusting the internal drawing to accomodate line thickness.
177
183
  const margin = pathOptions.width;
178
184
  xObject.stroke(colorModel);
@@ -203,6 +209,11 @@ exports.rectangle = function rectangle(x, y, width, height, options = {}) {
203
209
  },
204
210
  );
205
211
  }
212
+ if (options.link) {
213
+ if (options.useGivenCoords)
214
+ linkPdf(this, options.link, x, y, width, height);
215
+ else this.link(options.link, x, y, width, height);
216
+ }
206
217
 
207
218
  return this;
208
219
  };
@@ -362,6 +373,7 @@ exports.ellipse = function ellipse(cx, cy, rx, ry, options = {}) {
362
373
  height,
363
374
  pathOptions,
364
375
  (ctx, xObject) => {
376
+ ctx.gs(xObject.getGsName(pathOptions.strokeGsId));
365
377
  const margin = pathOptions.width / 2;
366
378
  xObject.stroke(colorModel);
367
379
  ctx.w(pathOptions.width).d(pathOptions.dash, pathOptions.dashPhase);
@@ -378,6 +390,7 @@ exports.ellipse = function ellipse(cx, cy, rx, ry, options = {}) {
378
390
  },
379
391
  );
380
392
  }
393
+ if (options.link) this.link(options.link, cx - rx, cy - ry, width, height);
381
394
  return this;
382
395
  };
383
396
 
@@ -518,6 +531,7 @@ exports.arc = function arc(
518
531
  diameter,
519
532
  pathOptions,
520
533
  (ctx, xObject) => {
534
+ ctx.gs(xObject.getGsName(pathOptions.strokeGsId));
521
535
  const margin = pathOptions.width / 2;
522
536
  xObject.stroke(colorModel);
523
537
  ctx.w(pathOptions.width).d(pathOptions.dash, pathOptions.dashPhase);
@@ -532,34 +546,118 @@ exports.arc = function arc(
532
546
  );
533
547
  }
534
548
 
549
+ if (options.link)
550
+ this.link(options.link, x - radius, y - radius, diameter, diameter);
551
+ return this;
552
+ };
553
+
554
+ /**
555
+ * Draw a closed sector of a circle.
556
+ * @name pie
557
+ * @function
558
+ * @memberof Recipe#
559
+ * @param {number} x - the x coordinate of the pie center point
560
+ * @param {number} y - the y coordinate of the pie center point
561
+ * @param {number} radius - the distance from the center point to the arc
562
+ * @param {number} [startAngle=0] - the start of the arc in degree units
563
+ * @param {number} [endAngle=360] - the end of the arc in degree units
564
+ * @param {Object} [options] - The path options.
565
+ * @returns {Recipe} The recipe instance.
566
+ */
567
+ exports.pie = function pie(x, y, radius, startAngle, endAngle, options = {}) {
568
+ return this.arc(x, y, radius, startAngle, endAngle, {
569
+ ...options,
570
+ sector: true,
571
+ });
572
+ };
573
+
574
+ /**
575
+ * Set the line style for the current page content context.
576
+ * @name lineStyle
577
+ * @function
578
+ * @memberof Recipe#
579
+ * @param {Recipe.LineStyleOptions} [options] - The line style options.
580
+ * @param {number} [options.width] - The line width.
581
+ * @param {number} [options.lineWidth] - Alias for width.
582
+ * @param {number} [options.cap] - The PDF line cap style.
583
+ * @param {number} [options.join] - The PDF line join style.
584
+ * @param {number} [options.miterLimit] - The miter limit.
585
+ * @param {number[]} [options.dash] - The dash pattern.
586
+ * @param {number} [options.dashPhase] - The dash pattern phase.
587
+ * @returns {Recipe} The recipe instance.
588
+ */
589
+ exports.lineStyle = function lineStyle(options = {}) {
590
+ this.current = this.current || {};
591
+ this.current.lineStyle = this.current.lineStyle || {};
592
+
593
+ if (options.width !== undefined || options.lineWidth !== undefined) {
594
+ const width = options.width ?? options.lineWidth;
595
+ this.current.lineStyle.width = width;
596
+ this.pageContext.w(width);
597
+ }
598
+ if (options.cap !== undefined) {
599
+ this.current.lineStyle.cap = options.cap;
600
+ this.pageContext.J(options.cap);
601
+ }
602
+ if (options.join !== undefined) {
603
+ this.current.lineStyle.join = options.join;
604
+ this.pageContext.j(options.join);
605
+ }
606
+ if (options.miterLimit !== undefined) {
607
+ this.current.lineStyle.miterLimit = options.miterLimit;
608
+ this.pageContext.M(options.miterLimit);
609
+ }
610
+ if (options.dash !== undefined || options.dashPhase !== undefined) {
611
+ const dash = options.dash ?? this.current.lineStyle.dash ?? [];
612
+ const dashPhase =
613
+ options.dashPhase ?? this.current.lineStyle.dashPhase ?? 0;
614
+ this.current.lineStyle.dash = dash;
615
+ this.current.lineStyle.dashPhase = dashPhase;
616
+ }
617
+ return this._setLineStyle(options);
618
+ };
619
+
620
+ exports._setLineStyle = function _setLineStyle(options = {}) {
621
+ if (options.width !== undefined || options.lineWidth !== undefined)
622
+ this.pageContext.w(options.width ?? options.lineWidth);
623
+ if (options.cap !== undefined) this.pageContext.J(options.cap);
624
+ if (options.join !== undefined) this.pageContext.j(options.join);
625
+ if (options.miterLimit !== undefined) this.pageContext.M(options.miterLimit);
626
+ if (options.dash !== undefined || options.dashPhase !== undefined)
627
+ this.pageContext.d(
628
+ options.dash ?? this.current.lineStyle.dash ?? [],
629
+ options.dashPhase ?? this.current.lineStyle.dashPhase ?? 0,
630
+ );
535
631
  return this;
536
632
  };
537
633
 
538
634
  /**
539
635
  * Set the line width.
540
636
  *
541
- * This compatibility method currently has no effect.
542
637
  * @name lineWidth
543
638
  * @function
544
639
  * @memberof Recipe#
545
- * @param {number} width - The requested line width.
640
+ * @param {number} width - The line width.
546
641
  * @returns {Recipe} The recipe instance.
547
642
  */
548
- exports.lineWidth = function lineWidth() {
549
- return this;
643
+ exports.lineWidth = function lineWidth(width) {
644
+ return this.lineStyle({ width });
550
645
  };
551
646
 
552
647
  /**
553
- * Set the fill opacity.
648
+ * Set fill and stroke opacity.
554
649
  *
555
- * This compatibility method currently has no effect.
556
- * @name fillOpacity
650
+ * @name opacity
557
651
  * @function
558
652
  * @memberof Recipe#
559
- * @param {number} opacity - The requested fill opacity.
653
+ * @param {number} value - The requested opacity from 0 (transparent) to 1 (opaque).
560
654
  * @returns {Recipe} The recipe instance.
561
655
  */
562
- exports.fillOpacity = function fillOpacity() {
656
+ exports.opacity = function opacity(value) {
657
+ if (!Number.isFinite(value) || value < 0 || value > 1) {
658
+ throw new RangeError("Opacity must be a finite number between 0 and 1");
659
+ }
660
+ this.current.opacity = value;
563
661
  return this;
564
662
  };
565
663
 
@@ -0,0 +1,10 @@
1
+ // Metadata fields accepted by Recipe constructor options and info().
2
+ var recipeInfoKeys = Object.freeze({
3
+ author: "author",
4
+ title: "title",
5
+ subject: "subject",
6
+ keywords: "keywords",
7
+ });
8
+ var standardInfoKeys = Object.freeze(Object.values(recipeInfoKeys));
9
+
10
+ module.exports = { recipeInfoKeys, standardInfoKeys };