@muhammara/native-core 7.0.0-beta.3 → 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 +86 -30
  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 +482 -105
  15. package/lib/recipe/appendPage.js +8 -1
  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 +154 -21
  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 +379 -275
  28. package/lib/recipe/parameters.js +11 -2
  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 +260 -173
  33. package/lib/recipe/text.helper.js +176 -10
  34. package/lib/recipe/text.js +420 -115
  35. package/lib/recipe/utils.js +66 -3
  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 +150 -13
  39. package/lib/recipe/vector.js +150 -53
  40. package/lib/recipe/xObjectForm.js +73 -10
  41. package/lib/recipe-constants.js +373 -0
  42. package/muhammara.d.ts +3487 -152
  43. package/package.json +1 -1
@@ -1,23 +1,44 @@
1
1
  const fs = require("fs");
2
2
  const path = require("path");
3
+ const { FontStyle } = require("../recipe-constants");
4
+
5
+ // Keys of the per-style font files stored for each family in this.fonts.
6
+ const FontSlot = Object.freeze({
7
+ REGULAR: "r",
8
+ BOLD: "b",
9
+ ITALIC: "i",
10
+ BOLD_ITALIC: "bi",
11
+ });
12
+
3
13
  /**
4
14
  * Register a custom font
5
15
  * @name registerFont
6
16
  * @function
7
17
  * @memberof Recipe#
8
- * @param {string} [fontName=''] - The font name used in text.
18
+ * @param {string} [fontName=''] - The font name used in text, matched case-insensitively.
9
19
  * @param {string} [fontSrcPath=''] - The path to the font file.
10
- * @param {string} [type='regular'] - The font type, one of 'bold', 'bold-italic', 'italic'
20
+ * @param {Recipe.FontStyle} [type='regular'] - The style this file provides,
21
+ * one of the `Recipe.FontStyle` values or its short form r, b, i or bi.
22
+ * Any other value registers the regular style.
11
23
  * @returns {Recipe} The recipe instance.
12
24
  */
13
25
  exports.registerFont = function registerFont(
14
26
  fontName = "",
15
27
  fontSrcPath = "",
16
- type = "regular",
28
+ type = FontStyle.REGULAR,
17
29
  ) {
18
30
  return this._registerFont(fontName, fontSrcPath, type);
19
31
  };
20
32
 
33
+ /**
34
+ * Register every .ttf, .ttc and .otf file in the given directories, deriving
35
+ * the family and style from the file name (for example "Roboto-BoldItalic").
36
+ * Missing directories are skipped.
37
+ * @private
38
+ * @param {string|string[]} fontSrcPath - The font directory or directories.
39
+ * @returns {void}
40
+ * @throws {Error} If an existing directory cannot be read.
41
+ */
21
42
  exports._loadFonts = function _loadFonts(fontSrcPath) {
22
43
  const fontTypes = [".ttf", ".ttc", ".otf"];
23
44
  const fontPaths =
@@ -33,19 +54,19 @@ exports._loadFonts = function _loadFonts(fontSrcPath) {
33
54
  .forEach((file) => {
34
55
  let fontName = path.basename(file, path.extname(file)).toLowerCase();
35
56
  // simple heuristics to make sure library fonts behave as expected
36
- const hasBold = fontName.indexOf("bold") !== -1;
37
- const hasItalic = fontName.indexOf("italic") !== -1;
38
- let type = "r";
57
+ const hasBold = fontName.indexOf(FontStyle.BOLD) !== -1;
58
+ const hasItalic = fontName.indexOf(FontStyle.ITALIC) !== -1;
59
+ let type = FontSlot.REGULAR;
39
60
  if (hasBold && hasItalic) {
40
61
  fontName = fontName.replace(/-*bold/, "");
41
62
  fontName = fontName.replace(/-*italic/, "");
42
- type = "bi";
63
+ type = FontSlot.BOLD_ITALIC;
43
64
  } else if (hasBold) {
44
65
  fontName = fontName.replace(/-*bold/, "");
45
- type = "b";
66
+ type = FontSlot.BOLD;
46
67
  } else if (hasItalic) {
47
68
  fontName = fontName.replace(/-*italic/, "");
48
- type = "i";
69
+ type = FontSlot.ITALIC;
49
70
  }
50
71
  return this._registerFont(fontName, path.join(fpath, file), type);
51
72
  });
@@ -53,10 +74,19 @@ exports._loadFonts = function _loadFonts(fontSrcPath) {
53
74
  }
54
75
  };
55
76
 
77
+ /**
78
+ * Record a font file for a family and style.
79
+ * @private
80
+ * @param {string} fontName - The family name, stored lower-cased.
81
+ * @param {string} fontSrcPath - The font file path.
82
+ * @param {Recipe.FontStyle|string} [type='regular'] - A `Recipe.FontStyle`
83
+ * value or its short form; anything else is stored as regular.
84
+ * @returns {Recipe} The recipe instance.
85
+ */
56
86
  exports._registerFont = function _registerFont(
57
87
  fontName,
58
88
  fontSrcPath,
59
- type = "regular",
89
+ type = FontStyle.REGULAR,
60
90
  ) {
61
91
  this.fonts = this.fonts || {};
62
92
  let family = fontName.toLowerCase();
@@ -64,19 +94,19 @@ exports._registerFont = function _registerFont(
64
94
 
65
95
  switch (type) {
66
96
  default:
67
- font.r = fontSrcPath;
97
+ font[FontSlot.REGULAR] = fontSrcPath;
68
98
  break;
69
- case "bold":
70
- case "b":
71
- font.b = fontSrcPath;
99
+ case FontStyle.BOLD:
100
+ case FontSlot.BOLD:
101
+ font[FontSlot.BOLD] = fontSrcPath;
72
102
  break;
73
- case "italic":
74
- case "i":
75
- font.i = fontSrcPath;
103
+ case FontStyle.ITALIC:
104
+ case FontSlot.ITALIC:
105
+ font[FontSlot.ITALIC] = fontSrcPath;
76
106
  break;
77
- case "bold-italic":
78
- case "bi":
79
- font.bi = fontSrcPath;
107
+ case FontStyle.BOLD_ITALIC:
108
+ case FontSlot.BOLD_ITALIC:
109
+ font[FontSlot.BOLD_ITALIC] = fontSrcPath;
80
110
  break;
81
111
  }
82
112
  this.fonts[family] = font;
@@ -84,18 +114,26 @@ exports._registerFont = function _registerFont(
84
114
  return this;
85
115
  };
86
116
 
117
+ /**
118
+ * Pick the registered font file for the family and bold/italic options,
119
+ * falling back to Helvetica when the family, style or file is unavailable.
120
+ * @private
121
+ * @param {Recipe} self - The recipe instance.
122
+ * @param {Object} [options] - Text options: font, and bold/isBold, italic/isItalic.
123
+ * @returns {string} The font file path.
124
+ */
87
125
  function _getFontFile(self, options = {}) {
88
126
  let fontFile;
89
127
  // Need to choose appropriate file based on bold/italic considerations
90
128
  // Note, if this is not done explicitly, the font dimensions will be incorrect.
91
129
  let type =
92
130
  (options.bold || options.isBold) && (options.italic || options.isItalic)
93
- ? "bi"
131
+ ? FontSlot.BOLD_ITALIC
94
132
  : options.italic || options.isItalic
95
- ? "i"
133
+ ? FontSlot.ITALIC
96
134
  : options.bold || options.isBold
97
- ? "b"
98
- : "r";
135
+ ? FontSlot.BOLD
136
+ : FontSlot.REGULAR;
99
137
 
100
138
  if (options.font) {
101
139
  const fontFamily = self.fonts[options.font.toLowerCase()];
@@ -110,12 +148,19 @@ function _getFontFile(self, options = {}) {
110
148
  }
111
149
 
112
150
  if (!fontFile) {
113
- fontFile = self.fonts["helvetica"]["r"]; // use default font when otherwise unavailable.
151
+ fontFile = self.fonts["helvetica"][FontSlot.REGULAR]; // use default font when otherwise unavailable.
114
152
  }
115
153
 
116
154
  return fontFile;
117
155
  }
118
156
 
157
+ /**
158
+ * Load the writer font for text options once per font file.
159
+ * @private
160
+ * @param {Object} [options] - Text options: font, and bold/isBold, italic/isItalic.
161
+ * @returns {Object} The writer's used font.
162
+ * @throws {Error} If the font file cannot be loaded.
163
+ */
119
164
  exports._getFont = function _getFont(options) {
120
165
  this.current = this.current || {};
121
166
 
@@ -4,9 +4,10 @@
4
4
  * @function
5
5
  * @memberof Recipe#
6
6
  * @param {number} degrees - Clockwise rotation in degrees.
7
- * @param {number} [x=0] - Rotation origin x coordinate.
8
- * @param {number} [y=0] - Rotation origin y coordinate.
7
+ * @param {number|"center"} [x=0] - Rotation origin x coordinate.
8
+ * @param {number|"center"} [y=0] - Rotation origin y coordinate.
9
9
  * @returns {Recipe} The recipe instance.
10
+ * @throws {TypeError} If no page is active.
10
11
  */
11
12
  exports.rotateContent = function rotateContent(degrees, x = 0, y = 0) {
12
13
  const radians = (degrees * Math.PI) / 180;
@@ -1,16 +1,54 @@
1
1
  const DOMParser = require("@xmldom/xmldom").DOMParser;
2
2
 
3
+ // HTML attribute and inline style names the parser reads.
4
+ const HtmlAttribute = Object.freeze({
5
+ STYLE: "style",
6
+ HREF: "href",
7
+ });
8
+ const CssProperty = Object.freeze({
9
+ COLOR: "color",
10
+ OPACITY: "opacity",
11
+ });
12
+
13
+ // CSS color functions whose components the parser reads.
14
+ const CssColorFunction = Object.freeze({
15
+ RGB: "rgb",
16
+ });
17
+
18
+ // Lower-case names of the HTML elements the parser handles.
19
+ const HtmlTag = Object.freeze({
20
+ HTML: "html",
21
+ P: "p",
22
+ LI: "li",
23
+ UL: "ul",
24
+ OL: "ol",
25
+ H1: "h1",
26
+ H2: "h2",
27
+ H3: "h3",
28
+ SMALL: "small",
29
+ BR: "br",
30
+ B: "b",
31
+ STRONG: "strong",
32
+ I: "i",
33
+ EM: "em",
34
+ U: "u",
35
+ DEL: "del",
36
+ A: "a",
37
+ });
38
+
3
39
  /**
4
40
  * Convert HTML into Recipe text layout objects.
5
41
  * @name htmlToTextObjects
6
42
  * @function
7
43
  * @memberof Recipe#
8
- * @param {string} htmlCodes - The HTML source.
44
+ * @param {string} htmlCodes - The HTML source. Tag names are matched case-insensitively.
9
45
  * @param {Object} [options] - Text options used to initialize the objects.
10
- * @returns {Object[]} The parsed text layout objects.
46
+ * @param {string} [options.font] - The font of every object.
47
+ * @param {number} [options.size] - The base font size of every object.
48
+ * @returns {Object[]} The parsed text layout objects: one per child node,
49
+ * each with its value, tag, style flags, link, font size ratio and childs.
11
50
  */
12
51
  exports.htmlToTextObjects = function (htmlCodes, options = {}) {
13
- htmlCodes = htmlCodes.replace(/<br\/?>/g, "<p>[@@DONOT_RENDER_THIS@@]</p>");
14
52
  const nodes = new DOMParser().parseFromString(
15
53
  `<html>${htmlCodes}</html>`,
16
54
  "text/html",
@@ -19,13 +57,19 @@ exports.htmlToTextObjects = function (htmlCodes, options = {}) {
19
57
  return textObjects;
20
58
  };
21
59
 
60
+ /**
61
+ * The font size multiplier of an element.
62
+ * @private
63
+ * @param {string} [tagName=''] - The element name, matched case-insensitively.
64
+ * @returns {number} The multiplier; 1 for elements without one.
65
+ */
22
66
  function getFontSizeRatio(tagName = "") {
23
67
  const fontSizeRatio = {
24
- p: 1, // 14px
25
- h1: 2.57, // 36px
26
- h2: 2.14, // 30px
27
- h3: 1.71, // 24px
28
- small: 0.7,
68
+ [HtmlTag.P]: 1, // 14px
69
+ [HtmlTag.H1]: 2.57, // 36px
70
+ [HtmlTag.H2]: 2.14, // 30px
71
+ [HtmlTag.H3]: 1.71, // 24px
72
+ [HtmlTag.SMALL]: 0.7,
29
73
  // h4: 1.12,
30
74
  // h5: 0.83,
31
75
  // h6: 0.75
@@ -34,22 +78,87 @@ function getFontSizeRatio(tagName = "") {
34
78
  return matched ? matched : 1;
35
79
  }
36
80
 
81
+ /**
82
+ * Whether an element starts its content on a new line.
83
+ * @private
84
+ * @param {string} [tagName=''] - The element name, matched case-insensitively.
85
+ * @returns {boolean} True for p, li and h1 to h3.
86
+ */
37
87
  function needsLineBreaker(tagName = "") {
38
- const lineBreakers = ["p", "li", "h1", "h2", "h3"];
39
- return lineBreakers.includes(tagName);
88
+ const lineBreakers = [
89
+ HtmlTag.P,
90
+ HtmlTag.LI,
91
+ HtmlTag.H1,
92
+ HtmlTag.H2,
93
+ HtmlTag.H3,
94
+ ];
95
+ return lineBreakers.includes(String(tagName).toLowerCase());
96
+ }
97
+
98
+ /**
99
+ * Reports whether text in `node` begins a visual line: after a line break or
100
+ * block element, or at the start of a block. Only such text drops its
101
+ * leading whitespace; text that follows inline content keeps one space.
102
+ * @private
103
+ * @param {Object} node - The DOM text node.
104
+ * @returns {boolean} True when the text begins a visual line.
105
+ */
106
+ function startsLine(node) {
107
+ const previous = node.previousSibling;
108
+ if (previous) {
109
+ const tag = (previous.tagName || "").toLowerCase();
110
+ return (
111
+ tag === HtmlTag.BR ||
112
+ needsLineBreaker(tag) ||
113
+ [HtmlTag.UL, HtmlTag.OL].includes(tag)
114
+ );
115
+ }
116
+ const parent = node.parentNode;
117
+ const parentTag = (
118
+ parent && parent.tagName ? parent.tagName : ""
119
+ ).toLowerCase();
120
+ if (
121
+ !parent ||
122
+ !parentTag ||
123
+ parentTag === HtmlTag.HTML ||
124
+ needsLineBreaker(parentTag)
125
+ ) {
126
+ return true;
127
+ }
128
+ return startsLine(parent);
40
129
  }
41
130
 
131
+ /**
132
+ * Whether an element makes its text bold.
133
+ * @private
134
+ * @param {string} [tagName=''] - The element name, matched case-insensitively.
135
+ * @returns {boolean} True for b and strong.
136
+ */
42
137
  function isBoldTag(tagName = "") {
43
- const boldTags = ["b", "strong"];
44
- return boldTags.includes(tagName);
138
+ const boldTags = [HtmlTag.B, HtmlTag.STRONG];
139
+ return boldTags.includes(String(tagName).toLowerCase());
45
140
  }
46
141
 
142
+ /**
143
+ * Whether an element makes its text italic.
144
+ * @private
145
+ * @param {string} [tagName=''] - The element name, matched case-insensitively.
146
+ * @returns {boolean} True for i and em.
147
+ */
47
148
  function isItalicTag(tagName = "") {
48
- const italicTags = ["i", "em"];
49
- return italicTags.includes(tagName);
149
+ const italicTags = [HtmlTag.I, HtmlTag.EM];
150
+ return italicTags.includes(String(tagName).toLowerCase());
50
151
  }
51
152
 
153
+ /**
154
+ * Convert a DOM node and its children into a text layout object.
155
+ * @private
156
+ * @param {Object} node - The DOM node.
157
+ * @param {Object} options - The htmlToTextObjects() options.
158
+ * @returns {Object} The text layout object with its parsed childs.
159
+ */
52
160
  function parseNode(node, options) {
161
+ const tag = (node.tagName || "").toLowerCase();
53
162
  const attributes = [];
54
163
  const styles = {};
55
164
  for (let i in node.attributes) {
@@ -58,22 +167,22 @@ function parseNode(node, options) {
58
167
  name: node.attributes[i].nodeName,
59
168
  value: node.attributes[i].nodeValue,
60
169
  });
61
- if (node.attributes[i].nodeName == "style") {
170
+ if (node.attributes[i].nodeName === HtmlAttribute.STYLE) {
62
171
  const styleValues = node.attributes[i].nodeValue.split(";");
63
172
  styleValues.forEach((element) => {
64
173
  if (element && element != "") {
65
174
  element = element.split(":");
66
175
  const key = element[0];
67
176
  let value = element[1].replace(/ /g, "");
68
- if (key == "color") {
69
- if (value.search("rgb") > -1) {
177
+ if (key === CssProperty.COLOR) {
178
+ if (value.search(CssColorFunction.RGB) > -1) {
70
179
  value = value
71
180
  .replace(/rgba?\(/, "")
72
181
  .replace(/\)/, "")
73
182
  .split(",")
74
183
  .map((item) => parseFloat(item));
75
184
  if (value.length > 3) {
76
- styles["opacity"] = value.pop();
185
+ styles[CssProperty.OPACITY] = value.pop();
77
186
  }
78
187
  }
79
188
  }
@@ -84,6 +193,18 @@ function parseNode(node, options) {
84
193
  }
85
194
  }
86
195
  let value = node.data ? node.data.replace(/^\s*/gm, "") : null;
196
+ if (value !== null && /^\s/.test(node.data) && !startsLine(node)) {
197
+ value = " " + value;
198
+ }
199
+ // Whitespace before a line break would only pad the end of the line.
200
+ const next = node.nextSibling;
201
+ if (
202
+ value !== null &&
203
+ next &&
204
+ (next.tagName || "").toLowerCase() === HtmlTag.BR
205
+ ) {
206
+ value = value.replace(/\s+$/, "");
207
+ }
87
208
  if (value && value.charCodeAt(0) == 8203) {
88
209
  // zero width space
89
210
  value = value.substring(1);
@@ -94,15 +215,24 @@ function parseNode(node, options) {
94
215
  font: options.font,
95
216
  isBold: isBoldTag(node.tagName),
96
217
  isItalic: isItalicTag(node.tagName),
97
- underline: node.tagName == "u",
98
- strikeOut: node.tagName == "del",
218
+ underline: tag === HtmlTag.U,
219
+ strikeOut: tag === HtmlTag.DEL,
99
220
  attributes,
100
221
  styles,
101
222
  needsLineBreaker: needsLineBreaker(node.tagName),
223
+ // An explicit line break; text layout ends the current line here.
224
+ lineBreak: tag === HtmlTag.BR,
102
225
  size: options.size,
103
226
  sizeRatio: getFontSizeRatio(node.tagName),
104
227
  sizeRatios: [getFontSizeRatio(node.tagName)],
105
- link: node.tagName == "a" ? node.attributes[0].value : null,
228
+ link:
229
+ tag === HtmlTag.A
230
+ ? (
231
+ attributes.find(
232
+ (attribute) => attribute.name === HtmlAttribute.HREF,
233
+ ) || {}
234
+ ).value || null
235
+ : null,
106
236
  childs: [],
107
237
  };
108
238
  for (let num in node.childNodes) {
@@ -117,3 +247,6 @@ function parseNode(node, options) {
117
247
  });
118
248
  return parsedData;
119
249
  }
250
+
251
+ // Shared with text layout; non-enumerable so it is not a Recipe method.
252
+ Object.defineProperty(exports, "HtmlTag", { value: HtmlTag });
@@ -1,11 +1,14 @@
1
+ const muhammara = require("../muhammara");
2
+ const { HorizontalAlign, VerticalAlign } = require("../recipe-constants");
3
+
1
4
  /**
2
5
  * Place images to pdf
3
6
  * @name image
4
7
  * @function
5
8
  * @memberof Recipe#
6
- * @param {string} imgSrc - The path for the image. [JPEG, PNG, TIFF]
7
- * @param {number} x - The coordinate x
8
- * @param {number} y - The coordinate y
9
+ * @param {string} imgSrc - The path for the image. [JPEG, PNG, TIFF, PDF]
10
+ * @param {number|"center"} x - The coordinate x of the top-left corner
11
+ * @param {number|"center"} y - The coordinate y of the top-left corner
9
12
  * @param {Object} [options] - The options
10
13
  * @returns {Recipe} The recipe instance.
11
14
  * @param {number} [options.width] - The new width
@@ -13,7 +16,14 @@
13
16
  * @param {number} [options.scale] - Scale the image from the original width and height.
14
17
  * @param {boolean} [options.keepAspectRatio=true] - Keep the aspect ratio.
15
18
  * @param {number} [options.opacity] - The opacity.
16
- * @param {string} [options.align] - 'center center'...
19
+ * @param {string} [options.align] - A `Recipe.HorizontalAlign` value, optionally
20
+ * followed by a space and a `Recipe.VerticalAlign` value, for example
21
+ * "center center". Horizontal center moves the image left by half its width
22
+ * and right moves it right by half; vertical center moves it up by half its
23
+ * height and bottom moves it down by half from its top-left placement.
24
+ * @param {string} [options.link] - Make the image open this URL.
25
+ * @throws {TypeError} If no page is active.
26
+ * @throws {Error} If the image cannot be read.
17
27
  */
18
28
  exports.image = function image(imgSrc, x, y, options = {}) {
19
29
  const { width, height, offsetX, offsetY } = this._getImgOffset(
@@ -22,7 +32,7 @@ exports.image = function image(imgSrc, x, y, options = {}) {
22
32
  );
23
33
  const imgOptions = {
24
34
  transformation: {
25
- fit: "always",
35
+ fit: muhammara.ImageFit.ALWAYS,
26
36
  // proportional: true,
27
37
  width,
28
38
  height,
@@ -65,11 +75,26 @@ exports.image = function image(imgSrc, x, y, options = {}) {
65
75
  return this;
66
76
  };
67
77
 
78
+ /**
79
+ * Compute the drawn size of an image and the offset its alignment applies.
80
+ * Sets `options.keepAspectRatio` to true when it is not given.
81
+ * @private
82
+ * @param {string} [imgSrc=''] - The image path.
83
+ * @param {Object} [options] - The image() options.
84
+ * @returns {{width: number, height: number, offsetX: number, offsetY: number}}
85
+ * The drawn size and the PDF offset from the placement point.
86
+ * @throws {Error} If the image cannot be read.
87
+ */
68
88
  exports._getImgOffset = function _getImgOffset(imgSrc = "", options = {}) {
69
89
  // set default to true
70
90
  options.keepAspectRatio =
71
91
  options.keepAspectRatio == void 0 ? true : options.keepAspectRatio;
72
92
  const dimensions = this.writer.getImageDimensions(imgSrc);
93
+ // An unreadable image measures 0 by 0. Drawing it would leave an image
94
+ // object that is never written, so endPDF() could not finish the PDF.
95
+ if (!(dimensions.width > 0 && dimensions.height > 0)) {
96
+ throw new Error(`Unknown image: ${imgSrc}`);
97
+ }
73
98
  const ratio = dimensions.width / dimensions.height;
74
99
 
75
100
  let width = dimensions.width;
@@ -105,10 +130,10 @@ exports._getImgOffset = function _getImgOffset(imgSrc = "", options = {}) {
105
130
  const alignments = options.align.split(" ");
106
131
  if (alignments[0]) {
107
132
  switch (alignments[0]) {
108
- case "center":
133
+ case HorizontalAlign.CENTER:
109
134
  offsetX = (-1 * width) / 2;
110
135
  break;
111
- case "right":
136
+ case HorizontalAlign.RIGHT:
112
137
  offsetX = width / 2;
113
138
  break;
114
139
  default:
@@ -116,10 +141,10 @@ exports._getImgOffset = function _getImgOffset(imgSrc = "", options = {}) {
116
141
  }
117
142
  if (alignments[1]) {
118
143
  switch (alignments[1]) {
119
- case "center":
144
+ case VerticalAlign.CENTER:
120
145
  offsetY = (-1 * height) / 2;
121
146
  break;
122
- case "bottom":
147
+ case VerticalAlign.BOTTOM:
123
148
  offsetY = height / 2;
124
149
  break;
125
150
  default: