diff --git a/docs/Recipe.html b/docs/Recipe.html index ded2a59..ff11220 100644 --- a/docs/Recipe.html +++ b/docs/Recipe.html @@ -1437,11 +1437,11 @@

(static) chrom This file will be merged with existing set of known colors. The color values must be specified as hex values.

For example, -{ -'rgb': {'purple':'ff00ff', 'red':'#ff0000'}, -'cmyk': {'cyan':'ff000000', 'magenta':'%0,100,0,0'}, -'gray': {'grey':'#33'} -}

+ { + 'rgb': {'purple':'ff00ff', 'red':'#ff0000'}, + 'cmyk': {'cyan':'ff000000', 'magenta':'%0,100,0,0'}, + 'gray': {'grey':'#33'} + }

@@ -3002,7 +3002,7 @@

(static) elli
Source:
@@ -4914,7 +4914,7 @@

(static) lineSource:
@@ -5182,6 +5182,39 @@
Properties
+ + + opacity + + + + + +number + + + + + + + + + <optional>
+ + + + + + + + + + +

how transparent should line be, from 0: invisible to 1: opaque

+ + + + dash @@ -5210,7 +5243,139 @@
Properties
-

The dash style [number, number]

+

The dash pattern [dashSize, gapSize] or [dashAndGapSize]

+ + + + + + + dashPhase + + + + + +number + + + + + + + + + <optional>
+ + + + + + + + + + +

distance into dash pattern at which to start dash (default: 0, immediately)

+ + + + + + + lineCap + + + + + +string + + + + + + + + + <optional>
+ + + + + + + + + + +

open line end style, 'butt', 'round', or 'square' (default: 'round')

+ + + + + + + lineJoin + + + + + +string + + + + + + + + + <optional>
+ + + + + + + + + + +

joined line end style, 'miter', 'round', or 'bevel' (default: 'round')

+ + + + + + + miterLimit + + + + + +number + + + + + + + + + <optional>
+ + + + + + + + + + +

limit at which 'miter' joins are forced to 'bevel' (default: 1.414)

@@ -5524,15 +5689,78 @@
Properties
- fill + lineWidth -string -| +number + + + + + + + + + <optional>
+ + + + + + + + + + +

The line width

+ + + + + + + opacity + + + + + +number + + + + + + + + + <optional>
+ + + + + + + + + +

how transparent should line be, from 0: invisible to 1: opaque

+ + + + + + + dash + + + + + Array.<number> @@ -5553,14 +5781,14 @@
Properties
-

HexColor, PercentColor or DecimalColor

+

The dash pattern [dashSize, gapSize] or [dashAndGapSize]

- lineWidth + dashPhase @@ -5586,20 +5814,20 @@
Properties
-

The line width

+

distance into dash pattern at which to start dash (default: 0, immediately)

- opacity + lineCap -number +string @@ -5619,20 +5847,20 @@
Properties
-

The opacity

+

open line end style, 'butt', 'round', or 'square' (default: 'round')

- dash + lineJoin -Array.<number> +string @@ -5652,7 +5880,40 @@
Properties
-

The dash style [number, number]

+

joined line end style, 'miter', 'round', or 'bevel' (default: 'round')

+ + + + + + + miterLimit + + + + + +number + + + + + + + + + <optional>
+ + + + + + + + + + +

limit at which 'miter' joins are forced to 'bevel' (default: 1.414)

@@ -6540,12 +6801,136 @@
Parameters:
- coordinates + coordinates + + + + + +Array.<number> + + + + + + + + + + + + + + + + + +

The array of coordinate [[x,y], [m,n]]

+ + + + + + + options + + + + + +Object + + + + + + + + + <optional>
+ + + + + + + + + + +

The options

+
Properties
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + + +
NameTypeAttributesDescription
color + + +string +| + +Array.<number> + + + + + + <optional>
+ + + + + +

HexColor, PercentColor or DecimalColor

stroke +string +| + Array.<number> @@ -6555,6 +6940,8 @@
Parameters:
+ <optional>
+ @@ -6564,20 +6951,23 @@
Parameters:
-

The array of coordinate [[x,y], [m,n]]

HexColor, PercentColor or DecimalColor

optionsfill -Object +string +| + +Array.<number> @@ -6597,44 +6987,53 @@
Parameters:
-

The options

-
Properties
+

HexColor, PercentColor or DecimalColor

lineWidth + +number - - - - - - - + + - - - + + - - + + + + + + + + - - + + - + + - + + - + + - + + - + + - + + - + + @@ -8043,6 +8436,8 @@
Properties
+ + @@ -8081,6 +8476,10 @@
Properties
+ + @@ -8114,6 +8513,12 @@
Properties
+ + @@ -8147,8 +8552,14 @@
Properties
+ + - + @@ -8180,8 +8591,14 @@
Properties
+ + - + @@ -8213,6 +8630,12 @@
Properties
+ + @@ -8246,8 +8669,14 @@
Properties
+ + - + @@ -8279,6 +8708,12 @@
Properties
+ + @@ -8315,6 +8750,10 @@
Properties
+ + @@ -8351,6 +8790,10 @@
Properties
+ + @@ -8387,6 +8830,10 @@
Properties
+ + @@ -8420,6 +8867,10 @@
Properties
+ + + @@ -8474,6 +8927,12 @@
Properties
+ + @@ -8507,6 +8966,10 @@
Properties
+ + @@ -8540,6 +9003,12 @@
Properties
+ + @@ -8576,6 +9045,12 @@
Properties
+ + @@ -8609,12 +9084,57 @@
Properties
+ + + + + + + + + + + + + + + + + + + + + + @@ -8642,8 +9162,15 @@
Properties
+ + - + @@ -8675,6 +9202,10 @@
Properties
+ + + @@ -8729,6 +9262,12 @@
Properties
+ + @@ -8765,6 +9304,10 @@
Properties
+ + @@ -8798,6 +9341,12 @@
Properties
+ + @@ -8834,6 +9383,10 @@
Properties
+ + @@ -8867,6 +9420,12 @@
Properties
+ + @@ -9062,7 +9621,7 @@
Parameters:

- Documentation generated by JSDoc 3.6.3 on Wed Oct 02 2019 12:00:26 GMT-0700 (Pacific Daylight Time) using the docdash theme. + Documentation generated by JSDoc 3.5.5 on Fri Nov 01 2019 20:41:12 GMT-0400 (Eastern Daylight Time) using the docdash theme.
diff --git a/docs/Recipe.js.html b/docs/Recipe.js.html index dcb496d..dd52fa6 100644 --- a/docs/Recipe.js.html +++ b/docs/Recipe.js.html @@ -37,277 +37,277 @@

Recipe.js

-
const hummus = require('hummus');
-const path = require('path');
-const fs = require('fs');
-const streams = require('memory-streams');
-/**
- * @name Recipe
- * @desc Create a pdfDoc
- * @namespace
- * @constructor
- * @param {string} src - The file path or Buffer of the src file.
- * @param {string} output - The path of the output file.
- * @param {Object} [options] - The options for pdfDoc
- * @param {number} [options.version] - The pdf version
- * @param {string} [options.author] - The author
- * @param {string} [options.title] - The title
- * @param {string} [options.subject] - The subject
- * @param {string} [options.colorspace] - The default colorspace: rgb, cmyk, gray
- * @param {string[]} [options.keywords] - The array of keywords
- * @param {string} [options.password] - permission password
- * @param {string} [options.userPassword] - this 'view' password also enables encryption
- * @param {string} [options.ownerPassword] - this allows owner to 'edit' file
- * @param {string} [options.userProtectionFlag] - encryption security level (see permissions)
- * @param {string|string[]} [options.fontSrcPath] - directory location(s) of additional fonts
- */
-class Recipe {
-    constructor(src, output, options = {}) {
-        this.src = src;
-        // detect the src is Buffer or not
-        this.isBufferSrc = this.src instanceof Buffer;
-        this.isNewPDF = (!this.isBufferSrc && src.toLowerCase() === 'new');
-        this.encryptOptions = this._getEncryptOptions(options, this.isNewPDF);
-        this.options = Object.assign({}, options, this.encryptOptions);
-
-        if (this.isBufferSrc) {
-            this.outStream = new streams.WritableStream();
-            this.output = output;
-        } else {
-            this.output = output || src;
-            if (this.src) {
-                this.filename = path.basename(this.src);
-            }
-        }
-        this.hummus = hummus;
-        this.logFile = 'hummus-error.log';
-
-        this.textMarkupAnnotations = [
-            'Highlight', 'Underline', 'StrikeOut', 'Squiggly'
-        ];
-
-        this.annotationsToWrite = [];
-        this.annotations = [];
-        this.vectorsToWrite = [];
-
-        this.xObjects = [];
-
-        this.needToEncrypt = false;
-
-        this.needToInsertPages = false;
-
-        this._setParameters(options);
-        this._loadFonts(path.join(__dirname, '../fonts'));
-        if (options.fontSrcPath) {
-            this._loadFonts(options.fontSrcPath);
-        }
-        this._createWriter();
-    }
-
-    _createWriter() {
-        if (this.isNewPDF) {
-            this.writer = hummus.createWriter(this.output,
-                Object.assign( {}, this.encryptOptions, {
-                    version: this._getVersion(this.options.version)
-                })
-            );
-        } else {
-            this.read();
-            try {
-                if (this.isBufferSrc) {
-                    this.writer = hummus.createWriterToModify(
-                        new hummus.PDFRStreamForBuffer(this.src),
-                        new hummus.PDFStreamForResponse(this.outStream),
-                        Object.assign( {}, this.encryptOptions, {
-                            log: this.logFile
-                        })
-                    );
-                } else {
-                    this.writer = hummus.createWriterToModify(this.src,
-                        Object.assign( {}, this.encryptOptions, {
-                            modifiedFilePath: this.output,
-                            log: this.logFile
-                        })
-                    );
-                }
-            } catch (err) {
-                throw new Error(err);
-            }
-        }
-
-        this.info(this.options);
-    }
-
-    _getVersion(version) {
-        const supportedVersions = [
-            1.0, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7
-        ];
-        if (!supportedVersions.includes(version)) {
-            version = 1.7;
-        }
-        version = hummus[`ePDFVersion${version * 10}`];
-
-        return version;
-    }
-
-    get position() {
-        const {
-            ox,
-            oy
-        } = this._reverseCoordinate(this._position.x, this._position.y);
-        return {
-            x: ox,
-            y: oy
-        };
-    }
-
-    read(inSrc) {
-        const isForExternal = (inSrc) ? true : false;
-        try {
-            let src = (isForExternal) ? inSrc : this.src;
-            if (this.isBufferSrc) {
-                src = new hummus.PDFRStreamForBuffer(this.src);
-            }
-            const pdfReader = hummus.createReader(src, this.encryptOptions);
-            const pages = pdfReader.getPagesCount();
-            if (pages == 0) {
-                // broken or modify password protected
-                throw 'HummusJS: Unable to read/edit PDF file (pages=0)';
-            }
-            const metadata = {
-                pages
-            };
-            for (var i = 0; i < pages; i++) {
-                const info = pdfReader.parsePage(i);
-                const dimensions = info.getMediaBox();
-                const rotate = info.getRotate();
-
-                let layout,
-                    width,
-                    height,
-                    pageSize;
-                let side1 = Math.abs(dimensions[2] - dimensions[0]);
-                let side2 = Math.abs(dimensions[3] - dimensions[1]);
-                if (side1 > side2 && rotate % 180 === 0) {
-                    layout = 'landscape';
-                } else
-                if (side1 < side2 && rotate % 180 !== 0) {
-                    layout = 'landscape';
-                } else {
-                    layout = 'portrait';
-                }
-
-                if (layout === 'landscape') {
-                    width = (side1 > side2) ? side1 : side2;
-                    height = (side1 > side2) ? side2 : side1;
-                } else {
-                    width = (side1 > side2) ? side2 : side1;
-                    height = (side1 > side2) ? side1 : side2;
-                }
-
-                pageSize = [width, height].sort((a, b) => {
-                    return (a > b) ? 1 : -1;
-                });
-
-                const page = {
-                    pageNumber: i + 1,
-                    mediaBox: dimensions,
-                    layout,
-                    rotate,
-                    width,
-                    height,
-                    size: pageSize,
-                    // usually 0
-                    offsetX: dimensions[0],
-                    offsetY: dimensions[1]
-                };
-                metadata[page.pageNumber] = page;
-            }
-            if (!isForExternal) {
-                this.pdfReader = pdfReader;
-                this.metadata = metadata;
-            }
-            return metadata;
-        } catch (err) {
-            throw new Error(err);
-        }
-    }
-
-    /**
-     * End the pdfDoc
-     * @function
-     * @memberof Recipe
-     * @param {function} callback - The callback function.
-     */
-    endPDF(callback) {
-        this._writeInfo();
-        this.writer.end();
-        // This is a temporary work around for copying context will overwrite the current one
-        // write annotations at the end.
-        if (
-            (this.annotations && this.annotations.length > 0) ||
-            (this.annotationsToWrite && this.annotationsToWrite.length > 0)
-        ) {
-            if (this.isBufferSrc) {
-                const oldStream = this.outStream;
-                this.outStream = new streams.WritableStream();
-
-                this.writer = hummus.createWriterToModify(
-                    new hummus.PDFRStreamForBuffer(oldStream.toBuffer()),
-                    new hummus.PDFStreamForResponse(this.outStream),
-                    Object.assign( {}, this.encryptOptions, {
-                        log: this.logFile
-                    })
-                );
-            } else {
-                this.writer = hummus.createWriterToModify(this.output,
-                    Object.assign( {}, this.encryptOptions, {
-                        modifiedFilePath: this.output,
-                        log: this.logFile
-                    })
-                );
-            }
-
-            this._writeAnnotations();
-            this._writeInfo();
-            this.writer.end();
-        }
-        if (this.needToInsertPages) {
-            if (this.isBufferSrc) {
-                // eslint-disable-next-line no-console
-                console.log('Feature: Inserting Pages is not supported in Buffer Mode yet.');
-            } else {
-                this._insertPages();
-            }
-        }
-        if (this.needToEncrypt) {
-            if (this.isBufferSrc) {
-                // eslint-disable-next-line no-console
-                console.log('Feature: Encryption is not supported in Buffer Mode yet.');
-            } else {
-                this._encrypt();
-            }
-        }
-
-        if (this.isBufferSrc && this.output) {
-            fs.writeFileSync(this.output, this.outStream.toBuffer());
-        }
-
-        if (callback) {
-            if (this.isBufferSrc) {
-                if (this.output) {
-                    return callback(this.output);
-                } else {
-                    return callback(this.outStream.toBuffer());
-                }
-            } else {
-                return callback();
-            }
-        }
-    }
-}
-
-module.exports = Recipe;
+            
const hummus = require('hummus');
+const path = require('path');
+const fs = require('fs');
+const streams = require('memory-streams');
+/**
+ * @name Recipe
+ * @desc Create a pdfDoc
+ * @namespace
+ * @constructor
+ * @param {string} src - The file path or Buffer of the src file.
+ * @param {string} output - The path of the output file.
+ * @param {Object} [options] - The options for pdfDoc
+ * @param {number} [options.version] - The pdf version
+ * @param {string} [options.author] - The author
+ * @param {string} [options.title] - The title
+ * @param {string} [options.subject] - The subject
+ * @param {string} [options.colorspace] - The default colorspace: rgb, cmyk, gray
+ * @param {string[]} [options.keywords] - The array of keywords
+ * @param {string} [options.password] - permission password
+ * @param {string} [options.userPassword] - this 'view' password also enables encryption
+ * @param {string} [options.ownerPassword] - this allows owner to 'edit' file
+ * @param {string} [options.userProtectionFlag] - encryption security level (see permissions)
+ * @param {string|string[]} [options.fontSrcPath] - directory location(s) of additional fonts
+ */
+class Recipe {
+    constructor(src, output, options = {}) {
+        this.src = src;
+        // detect the src is Buffer or not
+        this.isBufferSrc = this.src instanceof Buffer;
+        this.isNewPDF = (!this.isBufferSrc && src.toLowerCase() === 'new');
+        this.encryptOptions = this._getEncryptOptions(options, this.isNewPDF);
+        this.options = Object.assign({}, options, this.encryptOptions);
+
+        if (this.isBufferSrc) {
+            this.outStream = new streams.WritableStream();
+            this.output = output;
+        } else {
+            this.output = output || src;
+            if (this.src) {
+                this.filename = path.basename(this.src);
+            }
+        }
+        this.hummus = hummus;
+        this.logFile = 'hummus-error.log';
+
+        this.textMarkupAnnotations = [
+            'Highlight', 'Underline', 'StrikeOut', 'Squiggly'
+        ];
+
+        this.annotationsToWrite = [];
+        this.annotations = [];
+        this.vectorsToWrite = [];
+
+        this.xObjects = [];
+
+        this.needToEncrypt = false;
+
+        this.needToInsertPages = false;
+
+        this._setParameters(options);
+        this._loadFonts(path.join(__dirname, '../fonts'));
+        if (options.fontSrcPath) {
+            this._loadFonts(options.fontSrcPath);
+        }
+        this._createWriter();
+    }
+
+    _createWriter() {
+        if (this.isNewPDF) {
+            this.writer = hummus.createWriter(this.output,
+                Object.assign( {}, this.encryptOptions, {
+                    version: this._getVersion(this.options.version)
+                })
+            );
+        } else {
+            this.read();
+            try {
+                if (this.isBufferSrc) {
+                    this.writer = hummus.createWriterToModify(
+                        new hummus.PDFRStreamForBuffer(this.src),
+                        new hummus.PDFStreamForResponse(this.outStream),
+                        Object.assign( {}, this.encryptOptions, {
+                            log: this.logFile
+                        })
+                    );
+                } else {
+                    this.writer = hummus.createWriterToModify(this.src,
+                        Object.assign( {}, this.encryptOptions, {
+                            modifiedFilePath: this.output,
+                            log: this.logFile
+                        })
+                    );
+                }
+            } catch (err) {
+                throw new Error(err);
+            }
+        }
+
+        this.info(this.options);
+    }
+
+    _getVersion(version) {
+        const supportedVersions = [
+            1.0, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7
+        ];
+        if (!supportedVersions.includes(version)) {
+            version = 1.7;
+        }
+        version = hummus[`ePDFVersion${version * 10}`];
+
+        return version;
+    }
+
+    get position() {
+        const {
+            ox,
+            oy
+        } = this._reverseCoordinate(this._position.x, this._position.y);
+        return {
+            x: ox,
+            y: oy
+        };
+    }
+
+    read(inSrc) {
+        const isForExternal = (inSrc) ? true : false;
+        try {
+            let src = (isForExternal) ? inSrc : this.src;
+            if (this.isBufferSrc) {
+                src = new hummus.PDFRStreamForBuffer(this.src);
+            }
+            const pdfReader = hummus.createReader(src, this.encryptOptions);
+            const pages = pdfReader.getPagesCount();
+            if (pages == 0) {
+                // broken or modify password protected
+                throw 'HummusJS: Unable to read/edit PDF file (pages=0)';
+            }
+            const metadata = {
+                pages
+            };
+            for (var i = 0; i < pages; i++) {
+                const info = pdfReader.parsePage(i);
+                const dimensions = info.getMediaBox();
+                const rotate = info.getRotate();
+
+                let layout,
+                    width,
+                    height,
+                    pageSize;
+                let side1 = Math.abs(dimensions[2] - dimensions[0]);
+                let side2 = Math.abs(dimensions[3] - dimensions[1]);
+                if (side1 > side2 && rotate % 180 === 0) {
+                    layout = 'landscape';
+                } else
+                if (side1 < side2 && rotate % 180 !== 0) {
+                    layout = 'landscape';
+                } else {
+                    layout = 'portrait';
+                }
+
+                if (layout === 'landscape') {
+                    width = (side1 > side2) ? side1 : side2;
+                    height = (side1 > side2) ? side2 : side1;
+                } else {
+                    width = (side1 > side2) ? side2 : side1;
+                    height = (side1 > side2) ? side1 : side2;
+                }
+
+                pageSize = [width, height].sort((a, b) => {
+                    return (a > b) ? 1 : -1;
+                });
+
+                const page = {
+                    pageNumber: i + 1,
+                    mediaBox: dimensions,
+                    layout,
+                    rotate,
+                    width,
+                    height,
+                    size: pageSize,
+                    // usually 0
+                    offsetX: dimensions[0],
+                    offsetY: dimensions[1]
+                };
+                metadata[page.pageNumber] = page;
+            }
+            if (!isForExternal) {
+                this.pdfReader = pdfReader;
+                this.metadata = metadata;
+            }
+            return metadata;
+        } catch (err) {
+            throw new Error(err);
+        }
+    }
+
+    /**
+     * End the pdfDoc
+     * @function
+     * @memberof Recipe
+     * @param {function} callback - The callback function.
+     */
+    endPDF(callback) {
+        this._writeInfo();
+        this.writer.end();
+        // This is a temporary work around for copying context will overwrite the current one
+        // write annotations at the end.
+        if (
+            (this.annotations && this.annotations.length > 0) ||
+            (this.annotationsToWrite && this.annotationsToWrite.length > 0)
+        ) {
+            if (this.isBufferSrc) {
+                const oldStream = this.outStream;
+                this.outStream = new streams.WritableStream();
+
+                this.writer = hummus.createWriterToModify(
+                    new hummus.PDFRStreamForBuffer(oldStream.toBuffer()),
+                    new hummus.PDFStreamForResponse(this.outStream),
+                    Object.assign( {}, this.encryptOptions, {
+                        log: this.logFile
+                    })
+                );
+            } else {
+                this.writer = hummus.createWriterToModify(this.output,
+                    Object.assign( {}, this.encryptOptions, {
+                        modifiedFilePath: this.output,
+                        log: this.logFile
+                    })
+                );
+            }
+
+            this._writeAnnotations();
+            this._writeInfo();
+            this.writer.end();
+        }
+        if (this.needToInsertPages) {
+            if (this.isBufferSrc) {
+                // eslint-disable-next-line no-console
+                console.log('Feature: Inserting Pages is not supported in Buffer Mode yet.');
+            } else {
+                this._insertPages();
+            }
+        }
+        if (this.needToEncrypt) {
+            if (this.isBufferSrc) {
+                // eslint-disable-next-line no-console
+                console.log('Feature: Encryption is not supported in Buffer Mode yet.');
+            } else {
+                this._encrypt();
+            }
+        }
+
+        if (this.isBufferSrc && this.output) {
+            fs.writeFileSync(this.output, this.outStream.toBuffer());
+        }
+
+        if (callback) {
+            if (this.isBufferSrc) {
+                if (this.output) {
+                    return callback(this.output);
+                } else {
+                    return callback(this.outStream.toBuffer());
+                }
+            } else {
+                return callback();
+            }
+        }
+    }
+}
+
+module.exports = Recipe;
 
@@ -320,7 +320,7 @@

Recipe.js


- Documentation generated by JSDoc 3.6.3 on Wed Oct 02 2019 12:00:26 GMT-0700 (Pacific Daylight Time) using the docdash theme. + Documentation generated by JSDoc 3.5.5 on Fri Nov 01 2019 20:41:12 GMT-0400 (Eastern Daylight Time) using the docdash theme.
diff --git a/docs/annotation.js.html b/docs/annotation.js.html index 624c43d..34d8348 100644 --- a/docs/annotation.js.html +++ b/docs/annotation.js.html @@ -37,347 +37,347 @@

annotation.js

-
/**
- * Create a comment annotation
- * @name comment
- * @function
- * @memberof Recipe
- * @param {string} text - The text content
- * @param {number} x - The coordinate x
- * @param {number} y - The coordinate y
- * @param {Object} [options] - The options
- * @param {string} [options.title] - The title.
- * @param {string} [options.date] - The date.
- * @param {boolean} [options.open=false] - Open the annotation by default?
- * @param {boolean} [options.richText] - Display with rich text format, text will be transformed automatically, or you may pass in your own rich text starts with "<?xml..."
- * @param {'invisible'|'hidden'|'print'|'nozoom'|'norotate'|'noview'|'readonly'|'locked'|'togglenoview'} [options.flag] - The flag property
- */
-exports.comment = function comment(text = '', x, y, options = {}) {
-    this.annotationsToWrite.push({
-        subtype: 'Text',
-        pageNumber: this.pageNumber,
-        args: { text, x, y, options: Object.assign({ icon: 'Comment' }, options) }
-    });
-    return this;
-};
-
-/**
- * Create an annotation
- * @name annot
- * @function
- * @memberof Recipe
- * @todo support for rich texst RC
- * @todo support for opacity CA
- * @param {number} x - The coordinate x
- * @param {number} y - The coordinate y
- * @param {string} subtype - The markup annotation type 'Text'|'FreeText'|'Line'|'Square'|'Circle'|'Polygon'|'PolyLine'|'Highlight'|'Underline'|'Squiggly'|'StrikeOut'|'Stamp'|'Caret'|'Ink'|'FileAttachment'|'Sound'
- * @param {Object} [options] - The options
- * @param {string} [options.title] - The title.
- * @param {boolean} [options.open=false] - Open the annotation by default?
- * @param {'invisible'|'hidden'|'print'|'nozoom'|'norotate'|'noview'|'readonly'|'locked'|'togglenoview'} [options.flag] - The flag property
- * @param {'Comment'|'Key'|'Note'|'Help'|'NewParagraph'|'Paragraph'|'Insert'} [options.icon] - The icon of annotation.
- * @param {number} [options.width] - Width
- * @param {number} [options.height] - Height
- */
-exports.annot = function annot(x, y, subtype, options = { text: '', width: 0, height: 0 }) {
-    const { text, width, height } = options;
-    this.annotationsToWrite.push({
-        subtype,
-        args: { text, x, y, width, height, options },
-        pageNumber: this.pageNumber
-    });
-    return this;
-};
-
-// TODO: allow non-markup annots to be associated with markup annotations
-// Link, Popup, Movie, Widget, Screen, PrinterMark, TrapNet, Watermark, 3D
-exports._attachNonMarkupAnnot = function _attachNonMarkupAnnot() {
-
-};
-
-exports._annot = function _annot(subtype, args = {}, pageNumber) {
-    const { x, y, width, height, text, options } = args;
-    this._startDictionary(pageNumber);
-    const { rotate } = this.metadata[pageNumber];
-    let { nx, ny } = this._calibrateCoordinateForAnnots(x, y, 0, 0, pageNumber);
-
-    let nWidth = width;
-    let nHeight = height;
-
-    if (!options.followOriginalPageRotation) {
-        switch (rotate) {
-            case 90:
-                nWidth = height;
-                nHeight = width;
-                nx = nx - nWidth;
-                break;
-            case 180:
-                nx = nx - nWidth;
-                ny = ny - nHeight;
-                break;
-            case 270:
-                nWidth = height;
-                nHeight = width;
-                ny = ny - nHeight;
-                break;
-            default:
-        }
-    }
-
-    const params = Object.assign({
-        title: '',
-        subject: '',
-        date: new Date(),
-        open: false,
-        flag: '' // 'readonly'
-    }, options);
-
-    const ex = (nWidth) ? nWidth : 0;
-    const ey = (nHeight) ? nHeight : 0;
-    const position = [nx, ny, nx + ex, ny + ey];
-
-    this.dictionaryContext
-        .writeKey('Type')
-        .writeNameValue('Annot')
-        .writeKey('Subtype')
-        .writeNameValue(subtype)
-        .writeKey('L')
-        .writeBooleanValue(true)
-        .writeKey('Rect')
-        .writeRectangleValue(position)
-        .writeKey('Subj')
-        .writeLiteralStringValue(this.writer.createPDFTextString(params.subject).toBytesArray())
-        .writeKey('T')
-        .writeLiteralStringValue(this.writer.createPDFTextString(params.title).toBytesArray())
-        .writeKey('M')
-        .writeLiteralStringValue(this.writer.createPDFDate(params.date).toString())
-        .writeKey('Open')
-        .writeBooleanValue(params.open)
-        .writeKey('F')
-        .writeNumberValue(getFlagBitNumberByName(params.flag));
-
-    /**
-     * Rich Text Strings
-     * 12.7.3.4
-     */
-    if (text && options.richText) {
-        const richText = (text.substring(0, 4) !== '<?xml') ? contentToRC(text) : text;
-        const richTextContent = this.writer.createPDFTextString(richText).toBytesArray();
-        this.dictionaryContext
-            .writeKey('RC')
-            .writeLiteralStringValue(richTextContent);
-    } else
-    if (text) {
-        const textContent = this.writer.createPDFTextString(text).toBytesArray();
-        this.dictionaryContext
-            .writeKey('Contents')
-            .writeLiteralStringValue(textContent);
-    }
-
-    let { border, color } = options;
-
-    if (this._getTextMarkupAnnotationSubtype(subtype)) {
-        this.dictionaryContext.writeKey('QuadPoints');
-        const { _textHeight } = options;
-        const annotHeight = height;
-        const bx = nx;
-        const by = ny + ((_textHeight) ? 0 : -annotHeight);
-        const coordinates = [
-            [bx, by + annotHeight],
-            [bx + nWidth, by + annotHeight],
-            [bx, by],
-            [bx + nWidth, by],
-        ];
-        this.objectsContext.startArray();
-        coordinates.forEach(coord => {
-            coord.forEach(point => {
-                this.objectsContext.writeNumber(Math.round(point));
-            });
-        });
-        this.objectsContext
-            .endArray()
-            .endLine();
-
-        border = border || 0;
-        if (!color) {
-            switch (subtype) {
-                case 'Highlight':
-                    color = [255, 255, 0];
-                    break;
-                case 'StrikeOut':
-                    color = [255, 0, 0];
-                    break;
-                case 'Underline':
-                    color = [0, 255, 0];
-                    break;
-                case 'Squiggly':
-                    color = [0, 255, 0];
-                    break;
-                default:
-                    color = [0, 0, 0];
-                    break;
-            }
-        }
-    }
-
-    if (border != void(0)) {
-        this.dictionaryContext.writeKey('Border');
-        this.objectsContext
-            .startArray()
-            .writeNumber(0)
-            .writeNumber(0)
-            .writeNumber(border)
-            .endArray()
-            .endLine();
-    }
-
-    if (color) {
-        const rgb = this._colorNumberToRGB(this._transformColor(color));
-        this.dictionaryContext.writeKey('C');
-        this.objectsContext
-            .startArray()
-            .writeNumber(rgb.r / 255)
-            .writeNumber(rgb.g / 255)
-            .writeNumber(rgb.b / 255)
-            .endArray()
-            .endLine();
-    }
-
-    /* Display Icon */
-    if (params.icon) {
-        this.dictionaryContext
-            .writeKey('Name')
-            .writeNameValue(params.icon);
-    }
-    this._endDictionary(pageNumber);
-};
-
-exports._writeAnnotations = function _writeAnnotations() {
-    this.annotationsToWrite.forEach((annot) => {
-        this._annot(annot.subtype, annot.args, annot.pageNumber);
-    });
-    this.annotations.forEach((pageAnnots, index) => {
-        this._writeAnnotation(index);
-    });
-};
-
-exports._writeAnnotation = function _writeAnnotation(pageIndex) {
-    const pdfWriter = this.writer;
-    const copyingContext = pdfWriter.createPDFCopyingContextForModifiedFile();
-    const pageID = copyingContext.getSourceDocumentParser().getPageObjectID(pageIndex);
-    const pageObject = copyingContext.getSourceDocumentParser().parsePage(pageIndex).getDictionary().toJSObject();
-    const objectsContext = pdfWriter.getObjectsContext();
-
-    objectsContext.startModifiedIndirectObject(pageID);
-    const modifiedPageObject = pdfWriter.getObjectsContext().startDictionary();
-    Object.getOwnPropertyNames(pageObject).forEach((element) => {
-        const ignore = ['Annots'];
-        if (!ignore.includes(element)) {
-            modifiedPageObject.writeKey(element);
-            copyingContext.copyDirectObjectAsIs(pageObject[element]);
-        }
-    });
-
-    modifiedPageObject.writeKey('Annots');
-    objectsContext.startArray();
-    if (pageObject['Annots'] && pageObject['Annots'].toJSArray) {
-        pageObject['Annots'].toJSArray().forEach((annot) => {
-            objectsContext.writeIndirectObjectReference(annot.getObjectID());
-        });
-    }
-    this.annotations[pageIndex].forEach((item) => {
-        objectsContext.writeIndirectObjectReference(item);
-    });
-
-    objectsContext
-        .endArray()
-        .endLine()
-        .endDictionary(modifiedPageObject)
-        .endIndirectObject();
-};
-
-exports._startDictionary = function _startDictionary() {
-    this.objectsContext = this.writer.getObjectsContext();
-    this.dictionaryObject = this.objectsContext.startNewIndirectObject();
-    this.dictionaryContext = this.objectsContext.startDictionary();
-};
-
-exports._endDictionary = function _endDictionary(pageNumber) {
-    this.objectsContext
-        .endDictionary(this.dictionaryContext)
-        .endIndirectObject();
-    const pageIndex = pageNumber - 1;
-    this.annotations[pageIndex] = this.annotations[pageIndex] || [];
-    this.annotations[pageIndex].push(this.dictionaryObject);
-};
-
-exports._getTextMarkupAnnotationSubtype = function _getTextMarkupAnnotationSubtype(subtype = '') {
-    const matchedSubtype = this.textMarkupAnnotations.find(item => {
-        return item.toLowerCase() == subtype.toLowerCase();
-    });
-    return matchedSubtype;
-};
-
-/**
- * Get Flag Bit by Name
- * @description 12.5.3 Annotation Flags
- * @param {string} name
- */
-function getFlagBitNumberByName(name) {
-    switch (name.toLowerCase()) {
-        case 'invisible':
-            return 1;
-        case 'hidden':
-            return 2;
-        case 'print':
-            return 4;
-        case 'nozoom':
-            return 8;
-        case 'norotate':
-            return 16;
-        case 'noview':
-            return 32;
-        case 'readonly':
-            return 64;
-        case 'locked':
-            return 128;
-        case 'togglenoview':
-            return 256;
-            // 1.7+
-            // case 'lockedcontents':
-            //     return 512;
-        default:
-            return 0;
-    }
-}
-
-/**
- * Text Strings to Rich Text Strings
- * @todo Fix display issue for ol/ul in richText
- * @param {string} content
- * @description Support XHTML Elements:  '<p>' | '<span>' | '<b>' | '<i>'
- * @description Support CSS2 Style: 'text-align' | 'vertical-align' | 'font-size' | 'font-style' | 'font-weight' | 'font-family' | 'font' | 'color' | 'text-decoration' | 'font-stretch'
- */
-function contentToRC(content) {
-    content = content.replace('&nbsp;', ' ');
-    content = content.replace(/\r?\n|\r|\t/g, '');
-    let richText =
-        '<?xml version="1.0"?>' +
-        '<body ' +
-        'xmlns="http://www.w3.org/1999/xhtml"' +
-        // 'xmlns:xga=\"http://www.xfa.org/schema/xfa-data/1.0/\" ' +
-        // 'xfa:contentType=\"text/html\" ' +
-        // 'xfa:APIVersion=\"Acrobat:8.0.0\" ' +
-        // 'xfa:spec=\"2.4\" ' +
-        '>' +
-        content +
-        '</body>';
-    richText = richText
-        .replace(/<li>/g, '<p> • ')
-        .replace(/<(\/)li>/g, '</p>')
-        .replace(/<(\/)p>/g, '</p><br/>');
-    return richText;
-}
+            
/**
+ * Create a comment annotation
+ * @name comment
+ * @function
+ * @memberof Recipe
+ * @param {string} text - The text content
+ * @param {number} x - The coordinate x
+ * @param {number} y - The coordinate y
+ * @param {Object} [options] - The options
+ * @param {string} [options.title] - The title.
+ * @param {string} [options.date] - The date.
+ * @param {boolean} [options.open=false] - Open the annotation by default?
+ * @param {boolean} [options.richText] - Display with rich text format, text will be transformed automatically, or you may pass in your own rich text starts with "<?xml..."
+ * @param {'invisible'|'hidden'|'print'|'nozoom'|'norotate'|'noview'|'readonly'|'locked'|'togglenoview'} [options.flag] - The flag property
+ */
+exports.comment = function comment(text = '', x, y, options = {}) {
+    this.annotationsToWrite.push({
+        subtype: 'Text',
+        pageNumber: this.pageNumber,
+        args: { text, x, y, options: Object.assign({ icon: 'Comment' }, options) }
+    });
+    return this;
+};
+
+/**
+ * Create an annotation
+ * @name annot
+ * @function
+ * @memberof Recipe
+ * @todo support for rich texst RC
+ * @todo support for opacity CA
+ * @param {number} x - The coordinate x
+ * @param {number} y - The coordinate y
+ * @param {string} subtype - The markup annotation type 'Text'|'FreeText'|'Line'|'Square'|'Circle'|'Polygon'|'PolyLine'|'Highlight'|'Underline'|'Squiggly'|'StrikeOut'|'Stamp'|'Caret'|'Ink'|'FileAttachment'|'Sound'
+ * @param {Object} [options] - The options
+ * @param {string} [options.title] - The title.
+ * @param {boolean} [options.open=false] - Open the annotation by default?
+ * @param {'invisible'|'hidden'|'print'|'nozoom'|'norotate'|'noview'|'readonly'|'locked'|'togglenoview'} [options.flag] - The flag property
+ * @param {'Comment'|'Key'|'Note'|'Help'|'NewParagraph'|'Paragraph'|'Insert'} [options.icon] - The icon of annotation.
+ * @param {number} [options.width] - Width
+ * @param {number} [options.height] - Height
+ */
+exports.annot = function annot(x, y, subtype, options = { text: '', width: 0, height: 0 }) {
+    const { text, width, height } = options;
+    this.annotationsToWrite.push({
+        subtype,
+        args: { text, x, y, width, height, options },
+        pageNumber: this.pageNumber
+    });
+    return this;
+};
+
+// TODO: allow non-markup annots to be associated with markup annotations
+// Link, Popup, Movie, Widget, Screen, PrinterMark, TrapNet, Watermark, 3D
+exports._attachNonMarkupAnnot = function _attachNonMarkupAnnot() {
+
+};
+
+exports._annot = function _annot(subtype, args = {}, pageNumber) {
+    const { x, y, width, height, text, options } = args;
+    this._startDictionary(pageNumber);
+    const { rotate } = this.metadata[pageNumber];
+    let { nx, ny } = this._calibrateCoordinateForAnnots(x, y, 0, 0, pageNumber);
+
+    let nWidth = width;
+    let nHeight = height;
+
+    if (!options.followOriginalPageRotation) {
+        switch (rotate) {
+            case 90:
+                nWidth = height;
+                nHeight = width;
+                nx = nx - nWidth;
+                break;
+            case 180:
+                nx = nx - nWidth;
+                ny = ny - nHeight;
+                break;
+            case 270:
+                nWidth = height;
+                nHeight = width;
+                ny = ny - nHeight;
+                break;
+            default:
+        }
+    }
+
+    const params = Object.assign({
+        title: '',
+        subject: '',
+        date: new Date(),
+        open: false,
+        flag: '' // 'readonly'
+    }, options);
+
+    const ex = (nWidth) ? nWidth : 0;
+    const ey = (nHeight) ? nHeight : 0;
+    const position = [nx, ny, nx + ex, ny + ey];
+
+    this.dictionaryContext
+        .writeKey('Type')
+        .writeNameValue('Annot')
+        .writeKey('Subtype')
+        .writeNameValue(subtype)
+        .writeKey('L')
+        .writeBooleanValue(true)
+        .writeKey('Rect')
+        .writeRectangleValue(position)
+        .writeKey('Subj')
+        .writeLiteralStringValue(this.writer.createPDFTextString(params.subject).toBytesArray())
+        .writeKey('T')
+        .writeLiteralStringValue(this.writer.createPDFTextString(params.title).toBytesArray())
+        .writeKey('M')
+        .writeLiteralStringValue(this.writer.createPDFDate(params.date).toString())
+        .writeKey('Open')
+        .writeBooleanValue(params.open)
+        .writeKey('F')
+        .writeNumberValue(getFlagBitNumberByName(params.flag));
+
+    /**
+     * Rich Text Strings
+     * 12.7.3.4
+     */
+    if (text && options.richText) {
+        const richText = (text.substring(0, 4) !== '<?xml') ? contentToRC(text) : text;
+        const richTextContent = this.writer.createPDFTextString(richText).toBytesArray();
+        this.dictionaryContext
+            .writeKey('RC')
+            .writeLiteralStringValue(richTextContent);
+    } else
+    if (text) {
+        const textContent = this.writer.createPDFTextString(text).toBytesArray();
+        this.dictionaryContext
+            .writeKey('Contents')
+            .writeLiteralStringValue(textContent);
+    }
+
+    let { border, color } = options;
+
+    if (this._getTextMarkupAnnotationSubtype(subtype)) {
+        this.dictionaryContext.writeKey('QuadPoints');
+        const { _textHeight } = options;
+        const annotHeight = height;
+        const bx = nx;
+        const by = ny + ((_textHeight) ? 0 : -annotHeight);
+        const coordinates = [
+            [bx, by + annotHeight],
+            [bx + nWidth, by + annotHeight],
+            [bx, by],
+            [bx + nWidth, by],
+        ];
+        this.objectsContext.startArray();
+        coordinates.forEach(coord => {
+            coord.forEach(point => {
+                this.objectsContext.writeNumber(Math.round(point));
+            });
+        });
+        this.objectsContext
+            .endArray()
+            .endLine();
+
+        border = border || 0;
+        if (!color) {
+            switch (subtype) {
+                case 'Highlight':
+                    color = [255, 255, 0];
+                    break;
+                case 'StrikeOut':
+                    color = [255, 0, 0];
+                    break;
+                case 'Underline':
+                    color = [0, 255, 0];
+                    break;
+                case 'Squiggly':
+                    color = [0, 255, 0];
+                    break;
+                default:
+                    color = [0, 0, 0];
+                    break;
+            }
+        }
+    }
+
+    if (border != void(0)) {
+        this.dictionaryContext.writeKey('Border');
+        this.objectsContext
+            .startArray()
+            .writeNumber(0)
+            .writeNumber(0)
+            .writeNumber(border)
+            .endArray()
+            .endLine();
+    }
+
+    if (color) {
+        const rgb = this._colorNumberToRGB(this._transformColor(color));
+        this.dictionaryContext.writeKey('C');
+        this.objectsContext
+            .startArray()
+            .writeNumber(rgb.r / 255)
+            .writeNumber(rgb.g / 255)
+            .writeNumber(rgb.b / 255)
+            .endArray()
+            .endLine();
+    }
+
+    /* Display Icon */
+    if (params.icon) {
+        this.dictionaryContext
+            .writeKey('Name')
+            .writeNameValue(params.icon);
+    }
+    this._endDictionary(pageNumber);
+};
+
+exports._writeAnnotations = function _writeAnnotations() {
+    this.annotationsToWrite.forEach((annot) => {
+        this._annot(annot.subtype, annot.args, annot.pageNumber);
+    });
+    this.annotations.forEach((pageAnnots, index) => {
+        this._writeAnnotation(index);
+    });
+};
+
+exports._writeAnnotation = function _writeAnnotation(pageIndex) {
+    const pdfWriter = this.writer;
+    const copyingContext = pdfWriter.createPDFCopyingContextForModifiedFile();
+    const pageID = copyingContext.getSourceDocumentParser().getPageObjectID(pageIndex);
+    const pageObject = copyingContext.getSourceDocumentParser().parsePage(pageIndex).getDictionary().toJSObject();
+    const objectsContext = pdfWriter.getObjectsContext();
+
+    objectsContext.startModifiedIndirectObject(pageID);
+    const modifiedPageObject = pdfWriter.getObjectsContext().startDictionary();
+    Object.getOwnPropertyNames(pageObject).forEach((element) => {
+        const ignore = ['Annots'];
+        if (!ignore.includes(element)) {
+            modifiedPageObject.writeKey(element);
+            copyingContext.copyDirectObjectAsIs(pageObject[element]);
+        }
+    });
+
+    modifiedPageObject.writeKey('Annots');
+    objectsContext.startArray();
+    if (pageObject['Annots'] && pageObject['Annots'].toJSArray) {
+        pageObject['Annots'].toJSArray().forEach((annot) => {
+            objectsContext.writeIndirectObjectReference(annot.getObjectID());
+        });
+    }
+    this.annotations[pageIndex].forEach((item) => {
+        objectsContext.writeIndirectObjectReference(item);
+    });
+
+    objectsContext
+        .endArray()
+        .endLine()
+        .endDictionary(modifiedPageObject)
+        .endIndirectObject();
+};
+
+exports._startDictionary = function _startDictionary() {
+    this.objectsContext = this.writer.getObjectsContext();
+    this.dictionaryObject = this.objectsContext.startNewIndirectObject();
+    this.dictionaryContext = this.objectsContext.startDictionary();
+};
+
+exports._endDictionary = function _endDictionary(pageNumber) {
+    this.objectsContext
+        .endDictionary(this.dictionaryContext)
+        .endIndirectObject();
+    const pageIndex = pageNumber - 1;
+    this.annotations[pageIndex] = this.annotations[pageIndex] || [];
+    this.annotations[pageIndex].push(this.dictionaryObject);
+};
+
+exports._getTextMarkupAnnotationSubtype = function _getTextMarkupAnnotationSubtype(subtype = '') {
+    const matchedSubtype = this.textMarkupAnnotations.find(item => {
+        return item.toLowerCase() == subtype.toLowerCase();
+    });
+    return matchedSubtype;
+};
+
+/**
+ * Get Flag Bit by Name
+ * @description 12.5.3 Annotation Flags
+ * @param {string} name
+ */
+function getFlagBitNumberByName(name) {
+    switch (name.toLowerCase()) {
+        case 'invisible':
+            return 1;
+        case 'hidden':
+            return 2;
+        case 'print':
+            return 4;
+        case 'nozoom':
+            return 8;
+        case 'norotate':
+            return 16;
+        case 'noview':
+            return 32;
+        case 'readonly':
+            return 64;
+        case 'locked':
+            return 128;
+        case 'togglenoview':
+            return 256;
+            // 1.7+
+            // case 'lockedcontents':
+            //     return 512;
+        default:
+            return 0;
+    }
+}
+
+/**
+ * Text Strings to Rich Text Strings
+ * @todo Fix display issue for ol/ul in richText
+ * @param {string} content
+ * @description Support XHTML Elements:  '<p>' | '<span>' | '<b>' | '<i>'
+ * @description Support CSS2 Style: 'text-align' | 'vertical-align' | 'font-size' | 'font-style' | 'font-weight' | 'font-family' | 'font' | 'color' | 'text-decoration' | 'font-stretch'
+ */
+function contentToRC(content) {
+    content = content.replace('&nbsp;', ' ');
+    content = content.replace(/\r?\n|\r|\t/g, '');
+    let richText =
+        '<?xml version="1.0"?>' +
+        '<body ' +
+        'xmlns="http://www.w3.org/1999/xhtml"' +
+        // 'xmlns:xga=\"http://www.xfa.org/schema/xfa-data/1.0/\" ' +
+        // 'xfa:contentType=\"text/html\" ' +
+        // 'xfa:APIVersion=\"Acrobat:8.0.0\" ' +
+        // 'xfa:spec=\"2.4\" ' +
+        '>' +
+        content +
+        '</body>';
+    richText = richText
+        .replace(/<li>/g, '<p> • ')
+        .replace(/<(\/)li>/g, '</p>')
+        .replace(/<(\/)p>/g, '</p><br/>');
+    return richText;
+}
 
@@ -390,7 +390,7 @@

annotation.js


- Documentation generated by JSDoc 3.6.3 on Wed Oct 02 2019 12:00:26 GMT-0700 (Pacific Daylight Time) using the docdash theme. + Documentation generated by JSDoc 3.5.5 on Fri Nov 01 2019 20:41:12 GMT-0400 (Eastern Daylight Time) using the docdash theme.
diff --git a/docs/appendPage.js.html b/docs/appendPage.js.html index 4573d22..bf69b32 100644 --- a/docs/appendPage.js.html +++ b/docs/appendPage.js.html @@ -37,51 +37,51 @@

appendPage.js

-
const hummus = require('hummus');
-const hummusUtils = require('./utils');
-
-/**
- * Append pages from the other pdf to the current pdf
- * @name appendPage
- * @function
- * @memberof Recipe
- * @param {string} pdfSrc - The path for the other pdf.
- * @param {number|number[]} pages - The page number or the array of page numbers to be appended.
- */
-exports.appendPage = function appendPage(pdfSrc, pages = []) {
-    if (!Array.isArray(pages) && !isNaN(pages)) {
-        pages = [pages];
-    }
-    const pdfReader = hummus.createReader(pdfSrc);
-    const pageCount = pdfReader.getPagesCount();
-    // prevent unmatched pagenumber
-    const transformPageNumber = (pageNum) => {
-        pageNum = (pageNum > pageCount) ? pageCount : pageNum;
-        pageNum = (pageNum < 1) ? 1 : pageNum;
-        return (pageNum - 1);
-    };
-    pages = pages.map((element) => {
-        if (Array.isArray(element)) {
-            return [
-                transformPageNumber(element[0]),
-                transformPageNumber(element[1])
-            ];
-        } else {
-            return [
-                transformPageNumber(element),
-                transformPageNumber(element)
-            ];
-        }
-    });
-    if (pages.length > 0) {
-        hummusUtils.appendPDFPagesFromPDFWithAnnotations(this.writer, pdfSrc, {
-            specificRanges: pages
-        });
-    } else {
-        hummusUtils.appendPDFPagesFromPDFWithAnnotations(this.writer, pdfSrc);
-    }
-    return this;
-};
+            
const hummus = require('hummus');
+const hummusUtils = require('./utils');
+
+/**
+ * Append pages from the other pdf to the current pdf
+ * @name appendPage
+ * @function
+ * @memberof Recipe
+ * @param {string} pdfSrc - The path for the other pdf.
+ * @param {number|number[]} pages - The page number or the array of page numbers to be appended.
+ */
+exports.appendPage = function appendPage(pdfSrc, pages = []) {
+    if (!Array.isArray(pages) && !isNaN(pages)) {
+        pages = [pages];
+    }
+    const pdfReader = hummus.createReader(pdfSrc);
+    const pageCount = pdfReader.getPagesCount();
+    // prevent unmatched pagenumber
+    const transformPageNumber = (pageNum) => {
+        pageNum = (pageNum > pageCount) ? pageCount : pageNum;
+        pageNum = (pageNum < 1) ? 1 : pageNum;
+        return (pageNum - 1);
+    };
+    pages = pages.map((element) => {
+        if (Array.isArray(element)) {
+            return [
+                transformPageNumber(element[0]),
+                transformPageNumber(element[1])
+            ];
+        } else {
+            return [
+                transformPageNumber(element),
+                transformPageNumber(element)
+            ];
+        }
+    });
+    if (pages.length > 0) {
+        hummusUtils.appendPDFPagesFromPDFWithAnnotations(this.writer, pdfSrc, {
+            specificRanges: pages
+        });
+    } else {
+        hummusUtils.appendPDFPagesFromPDFWithAnnotations(this.writer, pdfSrc);
+    }
+    return this;
+};
 
@@ -94,7 +94,7 @@

appendPage.js


- Documentation generated by JSDoc 3.6.3 on Wed Oct 02 2019 12:00:26 GMT-0700 (Pacific Daylight Time) using the docdash theme. + Documentation generated by JSDoc 3.5.5 on Fri Nov 01 2019 20:41:12 GMT-0400 (Eastern Daylight Time) using the docdash theme.
diff --git a/docs/colors.js.html b/docs/colors.js.html index e84eb22..2297d3a 100644 --- a/docs/colors.js.html +++ b/docs/colors.js.html @@ -37,402 +37,402 @@

colors.js

-
const hummus = require('hummus');
-const fs = require('fs');
-
-this.knownColors = {  // knownColors.colorspace.colorName = value
-    rgb: {
-        red    : 'ff0000',
-        green  : '00ff00',
-        blue   : '0000ff',
-    },
-    cmyk: {
-        cyan   : 'ff000000',
-        magenta: '00ff0000',
-        yellow : '0000ff00',
-        black  : '000000ff'
-    },
-    gray: {
-        white  : 'ff',
-        black  : '00'
-    },
-    separation : {              // meant for printing, so using cmyk values initially
-        cyan   : 'ff000000',
-        magenta: '00ff0000',
-        yellow : '0000ff00',
-        black  : '000000ff',
-        nans   : '%,35,6,0'   // a great PDF collaborator!
-    }
-};
-
-/**
- * Associate color values to names
- *
- * The colorspace parameter is optional. When it is missing, the colorspace
- * is automatically determined by the given color value. Note that the special
- * PDF color space called 'separation' may also be used. The color value is then
- * treated as the alternative color when the named 'separation' color is unavailable.
- *
- * If the 'name' parameter is '!load', the second parameter is the name of a JSON
- * formatted file containing a formatted list of defined colors associated with the
- * color spaces rgb, cmyk, gray, or separation (think PANTONE color definitions).
- * This file will be merged with existing set of known colors. The color values
- * must be specified as hex values.
- *
- * For example,
- *   {
- *      'rgb':  {'purple':'ff00ff', 'red':'#ff0000'},
- *      'cmyk': {'cyan':'ff000000', 'magenta':'%0,100,0,0'},
- *      'gray': {'grey':'#33'}
- *   }
- *
- * @name chroma
- * @function
- * @memberof Recipe
- * @param {string} name - the name to be associated to given color value, or '!load'
- * @param {string|number[]} value - the color value (HexColor, DecimalColor, or PercentColor), or name of '!load' file
- * @param {string} colorspace - one of the followning: 'rgb', 'cmyk', 'gray', 'separation';
- */
-exports.chroma = function chroma(name, value, colorspace='') {
-    if (name) {
-        if (name === '!load') {
-            let newColors = JSON.parse(fs.readFileSync(value));
-            // Add new colors to existing colorspaces
-            for (let cs in newColors) {
-                if (this.knownColors[cs]) {
-                    Object.assign(this.knownColors[cs], newColors[cs]);
-                } else {
-                    throw new Error(`Unrecognized colorspace: ${cs}`);
-                }
-            }
-        } else {
-            if (Array.isArray(value)) {
-                value = arrayToHex(value);
-            } else if (value.startsWith('%')) {
-                value = percentToHex(value.replace('%', ''));
-            } else {
-                value = value.replace('#', '');
-            }
-
-            // Only deal with valid hex codes from the
-            // device colorspaces gray, rgb, and cmyk.
-            if (![2,6,8].includes(value.toString().length)) {
-                throw new Error('Color value has incorrect size for gray, rgb, or cmyk colorspaces');
-            }
-
-            // Determine colorspace by length of given input
-            // value when colorspace not provided in call.
-            if (colorspace === '') {
-                const colorSpaces = {2:'gray', 6:'rgb', 8:'cmyk'};
-                colorspace = colorSpaces[`${value.length}`];
-            } else if (!['rgb','cmyk','gray','separation'].includes(colorspace)) {
-                throw new Error(`Unknown colorspace: ${colorspace}.`);
-            }
-
-            if (colorspace) {
-                this.knownColors[colorspace][name] = value;
-            }
-        }
-    }
-
-    return this;
-};
-
-function createColorSpaces(self, colorName, color) {
-    const deviceCS =  {1:'DeviceGray', 3:'DeviceRGB', 4:'DeviceCMYK'};
-    const altCS = deviceCS[`${color.length}`];
-    this.colorSpaces = this.colorSpaces || {};
-    this.colorSpaces[altCS] = this.colorSpaces[altCS] || {};
-    let colorSpaceID = this.colorSpaces[altCS][colorName];
-
-    if (!colorSpaceID) {
-        const transformFunction = tintTransform(self, color);
-        self.pauseContext();
-        const objCxt = self.writer.getObjectsContext();
-        colorSpaceID = objCxt.startNewIndirectObject();
-        objCxt
-            .startArray()
-            .writeName('Separation')
-            .writeName(colorName)
-            .writeName(altCS)
-            .writeIndirectObjectReference(transformFunction)
-            .endArray(hummus.eTokenSeparatorEndLine)
-            .endIndirectObject();
-        self.resumeContext();
-        this.colorSpaces[altCS][colorName] = colorSpaceID;
-    }
-
-    return colorSpaceID;
-}
-
-function tintTransform(self, color) {
-    const rangeCount = color.length;
-    self.pauseContext();
-    const objCxt = self.writer.getObjectsContext();
-    const tintFuncID = objCxt.startNewIndirectObject();
-    const dict = objCxt.startDictionary();
-    dict
-        .writeKey('FunctionType')
-        .writeNumberValue(2)
-        .writeKey('Domain');
-    objCxt
-        .startArray()
-        .writeNumber(0.0)
-        .writeNumber(1.0)
-        .endArray();
-    dict.writeKey('Range');
-    objCxt.startArray();
-    for (let index = 0; index < rangeCount; index++) {
-        objCxt.writeNumber(0.0).writeNumber(1.0);
-    }
-    objCxt.endArray();
-    dict.writeKey('N');
-    dict.writeNumberValue(1);
-    dict.writeKey('C0');
-    objCxt.startArray();
-    for (let index = 0; index < rangeCount; index++) {
-        objCxt.writeNumber(0.0);
-    }
-    objCxt.endArray();
-
-    dict.writeKey('C1');
-    objCxt.startArray();
-    for (let index = 0; index < rangeCount; index++) {
-        objCxt.writeNumber(color[index]);
-    }
-    objCxt.endArray();
-    objCxt.endDictionary(dict);
-    objCxt.endIndirectObject();
-    self.resumeContext();
-
-    return tintFuncID;
-}
-
-exports._createExtGStates = function _createExtGStates(value) {
-    this.extGStates = this.extGStates || {};
-    if (this.extGStates[value]) {
-        return this.extGStates[value];
-    }
-
-    const write = (key, value) => {
-        this.pauseContext();
-        const objCxt = this.writer.getObjectsContext();
-        const gsId = objCxt.startNewIndirectObject();
-        const dict = objCxt.startDictionary();
-        dict.writeKey('type');
-        dict.writeNameValue('ExtGState');
-        dict.writeKey(key);
-        objCxt.writeNumber(value);
-        objCxt.endLine();
-        objCxt.endDictionary(dict);
-        objCxt.endIndirectObject(); // new here [seh]
-        this.resumeContext();
-        return gsId;
-    };
-    this.extGStates[value] = {
-        stroke: write('CA', value),
-        fill:   write('ca', value)
-    };
-    return this.extGStates[value];
-};
-
-function _defaultColor(colorspace='rgb') {
-    let defaultColor;
-    switch (colorspace) {
-        case 'cmyk':
-            defaultColor = 'FF000000';
-            break;
-        case 'gray':
-            defaultColor = '00';
-            break;
-        case 'rgb':
-        default:
-            defaultColor = '1777d1';
-            break;
-    }
-
-    return defaultColor;
-}
-
-/**
- * Convert given color code int color model object
- *
- * ColorModel consists of: {
- *   color: number,
- *   colorspace: string {'rgb', 'cmyk', 'gray'},
- *   (colorspace == 'rgb')  r, g, b
- *   (colorspace == 'cmyk') c, m, y, k
- *   (colorspace == 'gray') gray
- * }
- *
- * where r,g,b,c,m,y,k,gray are all numbers between 0 and 1
- *
- * @param {string} code the color encoding as HexColor
- * @param {string} colorspace the name of the colorspace of given color code
- * @param {string} colorName the name to be associated with given color code
- * @returns {any} the color model
- */
-function toColorModel(self, code, colorspace, colorName) {
-    const cmodel = {};
-    let color = hexToArray(code);
-
-    cmodel.color = parseInt(code, 16);
-
-    // The initial decider of color space is length of given 'code'.
-
-    switch (color.length) {
-        default:
-            color = hexToArray(_defaultColor('rgb'));
-            // purposely want to fall through to 'rgb' case below.
-        case 3:
-            cmodel.colorspace = 'rgb';
-            cmodel.r = color[0];
-            cmodel.g = color[1];
-            cmodel.b = color[2];
-            break;
-
-        case 4:
-            cmodel.colorspace = 'cmyk';
-            cmodel.c = color[0];
-            cmodel.m = color[1];
-            cmodel.y = color[2];
-            cmodel.k = color[3];
-            break;
-
-        case 1:
-            cmodel.colorspace = 'gray';
-            cmodel.gray = color[0];
-            break;
-    }
-
-    // When creating a separation color space,
-    // use the colorspace from above as the
-    // alternative color transformation when
-    // the named color is unavailable.
-    if (colorspace === 'separation' && colorName !== '') {
-        cmodel.colorspace = colorspace;
-        cmodel.colorName = colorName;
-        cmodel.colorspaceId = createColorSpaces(self, colorName, color);
-    }
-
-    return cmodel;
-}
-
-/**
- * Convert percentage string into hex string (x / 100 * 255)
- *
- * @param {string} code numbers separated by commas with values ranging between 0-100.
- * @returns {string} massaged hexadecimal string that can be used as input to hexToArray.
- */
-function percentToHex(code) {
-    return arrayToHex(code.split(',').map( x => Math.round(x * 2.55)) );
-}
-
-/**
- * Transform color code into numeric value or colorModel
- *
- * @param code color specification in form of HexColor (string, begins with '#'),
- *             DecimalColor (1, 3, or 4 element array with values between 0-255),
- *             PercentColor (string, begins with '%' followed by values separated
- *             by commas with values between 0-100)
- */
-exports._transformColor = function _transformColor(code = '', opt = {}) {
-    this.knownColors   = this.knownColors || {};
-    let colorspace     = opt.colorspace || 'rgb';
-    let wantColorModel = opt.wantColorModel || false;
-    let colorName      = opt.colorName || '';
-    let defaultColor   = _defaultColor(colorspace);
-    let transformation;
-
-    if (Array.isArray(code)) {
-        code = arrayToHex(code);
-    } else if (code.startsWith('#')){
-        code = code.replace('#', '');
-    } else if (code.startsWith('%')) {
-        code = percentToHex(code.replace('%', ''));
-    } else if (code !== '') {
-        let color = this.knownColors[colorspace][code];  // assuming code is a color name
-        if (!color) {
-            color = '';
-            code = defaultColor;
-        } else {
-            colorName = code;
-
-            // The following handles known colors in hex form,
-            // with or without initial '#' and percent form.
-            if (color.startsWith('#')) {
-                code = color.replace('#','');
-            } else if (color.startsWith('%')) {
-                code = percentToHex(color.replace('%', ''));
-            } else {
-                code = color;
-            }
-        }
-    }
-
-    // When colorspace is not explicitly given,
-    // use size of value to determine colorspace.
-    if (!opt.colorspace) {
-        colorspace = {2:'gray', 6:'rgb', 8:'cmyk'}[`${code.length}`] || 'rgb';
-        defaultColor = _defaultColor(colorspace);
-    }
-
-    // Suppply default color:
-    //  when colorspace is given and given color code does not have appropriate length, or
-    //  when colorspace is missing, verify allowable hex value sizes for rgb, cmyk, or gray.
-    if ( ['rgb','cmyk','gray'].includes(colorspace) && code.length != defaultColor.length ||
-         ! [2,6,8].includes(code.toString().length)) {
-        code = defaultColor;
-    }
-
-    if (wantColorModel) {
-        transformation = toColorModel(this, code, colorspace, colorName);
-        if ( colorName && !this.knownColors[colorspace][colorName]) {
-            this.chroma(colorName, code, colorspace);
-        }
-    } else {
-        transformation = parseInt(`0x${code.toUpperCase()}`, 16);
-    }
-
-    return transformation;
-};
-
-function arrayToHex(color = []) {
-    let code = '';
-    color.forEach((item) => {
-        let hex = item.toString(16);
-        hex = (hex.length == 1) ? '0' + hex : hex;
-        code += hex;
-    });
-    return code;
-}
-
-function hexToArray(hex) {
-    const result = /^#?([a-f\d]{2})([a-f\d]{2})?([a-f\d]{2})?([a-f\d]{2})?$/i.exec(hex);
-    return result
-        .reduce((array, item, index) => {
-            if (index >= 1 && index <= (hex.length / 2)) {
-                array.push(parseInt(item, 16) / 255);
-            }
-            return array;
-        }, []);
-}
-
-exports._colorNumberToRGB = (bigint) => {
-    if (!bigint) {
-        return {
-            r: 0,
-            g: 0,
-            b: 0
-        };
-    } else {
-        return {
-            r: (bigint >> 16) & 255,
-            g: (bigint >>  8) & 255,
-            b: bigint & 255
-        };
-    }
-};
+            
const hummus = require('hummus');
+const fs = require('fs');
+
+this.knownColors = {  // knownColors.colorspace.colorName = value
+    rgb: {
+        red    : 'ff0000',
+        green  : '00ff00',
+        blue   : '0000ff',
+    },
+    cmyk: {
+        cyan   : 'ff000000',
+        magenta: '00ff0000',
+        yellow : '0000ff00',
+        black  : '000000ff'
+    },
+    gray: {
+        white  : 'ff',
+        black  : '00'
+    },
+    separation : {              // meant for printing, so using cmyk values initially
+        cyan   : 'ff000000',
+        magenta: '00ff0000',
+        yellow : '0000ff00',
+        black  : '000000ff',
+        nans   : '%,35,6,0'   // a great PDF collaborator!
+    }
+};
+
+/**
+ * Associate color values to names
+ *
+ * The colorspace parameter is optional. When it is missing, the colorspace
+ * is automatically determined by the given color value. Note that the special
+ * PDF color space called 'separation' may also be used. The color value is then
+ * treated as the alternative color when the named 'separation' color is unavailable.
+ *
+ * If the 'name' parameter is '!load', the second parameter is the name of a JSON
+ * formatted file containing a formatted list of defined colors associated with the
+ * color spaces rgb, cmyk, gray, or separation (think PANTONE color definitions).
+ * This file will be merged with existing set of known colors. The color values
+ * must be specified as hex values.
+ *
+ * For example,
+ *   {
+ *      'rgb':  {'purple':'ff00ff', 'red':'#ff0000'},
+ *      'cmyk': {'cyan':'ff000000', 'magenta':'%0,100,0,0'},
+ *      'gray': {'grey':'#33'}
+ *   }
+ *
+ * @name chroma
+ * @function
+ * @memberof Recipe
+ * @param {string} name - the name to be associated to given color value, or '!load'
+ * @param {string|number[]} value - the color value (HexColor, DecimalColor, or PercentColor), or name of '!load' file
+ * @param {string} colorspace - one of the followning: 'rgb', 'cmyk', 'gray', 'separation';
+ */
+exports.chroma = function chroma(name, value, colorspace='') {
+    if (name) {
+        if (name === '!load') {
+            let newColors = JSON.parse(fs.readFileSync(value));
+            // Add new colors to existing colorspaces
+            for (let cs in newColors) {
+                if (this.knownColors[cs]) {
+                    Object.assign(this.knownColors[cs], newColors[cs]);
+                } else {
+                    throw new Error(`Unrecognized colorspace: ${cs}`);
+                }
+            }
+        } else {
+            if (Array.isArray(value)) {
+                value = arrayToHex(value);
+            } else if (value.startsWith('%')) {
+                value = percentToHex(value.replace('%', ''));
+            } else {
+                value = value.replace('#', '');
+            }
+
+            // Only deal with valid hex codes from the
+            // device colorspaces gray, rgb, and cmyk.
+            if (![2,6,8].includes(value.toString().length)) {
+                throw new Error('Color value has incorrect size for gray, rgb, or cmyk colorspaces');
+            }
+
+            // Determine colorspace by length of given input
+            // value when colorspace not provided in call.
+            if (colorspace === '') {
+                const colorSpaces = {2:'gray', 6:'rgb', 8:'cmyk'};
+                colorspace = colorSpaces[`${value.length}`];
+            } else if (!['rgb','cmyk','gray','separation'].includes(colorspace)) {
+                throw new Error(`Unknown colorspace: ${colorspace}.`);
+            }
+
+            if (colorspace) {
+                this.knownColors[colorspace][name] = value;
+            }
+        }
+    }
+
+    return this;
+};
+
+function createColorSpaces(self, colorName, color) {
+    const deviceCS =  {1:'DeviceGray', 3:'DeviceRGB', 4:'DeviceCMYK'};
+    const altCS = deviceCS[`${color.length}`];
+    this.colorSpaces = this.colorSpaces || {};
+    this.colorSpaces[altCS] = this.colorSpaces[altCS] || {};
+    let colorSpaceID = this.colorSpaces[altCS][colorName];
+
+    if (!colorSpaceID) {
+        const transformFunction = tintTransform(self, color);
+        self.pauseContext();
+        const objCxt = self.writer.getObjectsContext();
+        colorSpaceID = objCxt.startNewIndirectObject();
+        objCxt
+            .startArray()
+            .writeName('Separation')
+            .writeName(colorName)
+            .writeName(altCS)
+            .writeIndirectObjectReference(transformFunction)
+            .endArray(hummus.eTokenSeparatorEndLine)
+            .endIndirectObject();
+        self.resumeContext();
+        this.colorSpaces[altCS][colorName] = colorSpaceID;
+    }
+
+    return colorSpaceID;
+}
+
+function tintTransform(self, color) {
+    const rangeCount = color.length;
+    self.pauseContext();
+    const objCxt = self.writer.getObjectsContext();
+    const tintFuncID = objCxt.startNewIndirectObject();
+    const dict = objCxt.startDictionary();
+    dict
+        .writeKey('FunctionType')
+        .writeNumberValue(2)
+        .writeKey('Domain');
+    objCxt
+        .startArray()
+        .writeNumber(0.0)
+        .writeNumber(1.0)
+        .endArray();
+    dict.writeKey('Range');
+    objCxt.startArray();
+    for (let index = 0; index < rangeCount; index++) {
+        objCxt.writeNumber(0.0).writeNumber(1.0);
+    }
+    objCxt.endArray();
+    dict.writeKey('N');
+    dict.writeNumberValue(1);
+    dict.writeKey('C0');
+    objCxt.startArray();
+    for (let index = 0; index < rangeCount; index++) {
+        objCxt.writeNumber(0.0);
+    }
+    objCxt.endArray();
+
+    dict.writeKey('C1');
+    objCxt.startArray();
+    for (let index = 0; index < rangeCount; index++) {
+        objCxt.writeNumber(color[index]);
+    }
+    objCxt.endArray();
+    objCxt.endDictionary(dict);
+    objCxt.endIndirectObject();
+    self.resumeContext();
+
+    return tintFuncID;
+}
+
+exports._createExtGStates = function _createExtGStates(value) {
+    this.extGStates = this.extGStates || {};
+    if (this.extGStates[value]) {
+        return this.extGStates[value];
+    }
+
+    const write = (key, value) => {
+        this.pauseContext();
+        const objCxt = this.writer.getObjectsContext();
+        const gsId = objCxt.startNewIndirectObject();
+        const dict = objCxt.startDictionary();
+        dict.writeKey('type');
+        dict.writeNameValue('ExtGState');
+        dict.writeKey(key);
+        objCxt.writeNumber(value);
+        objCxt.endLine();
+        objCxt.endDictionary(dict);
+        objCxt.endIndirectObject(); // new here [seh]
+        this.resumeContext();
+        return gsId;
+    };
+    this.extGStates[value] = {
+        stroke: write('CA', value),
+        fill:   write('ca', value)
+    };
+    return this.extGStates[value];
+};
+
+function _defaultColor(colorspace='rgb') {
+    let defaultColor;
+    switch (colorspace) {
+        case 'cmyk':
+            defaultColor = 'FF000000';
+            break;
+        case 'gray':
+            defaultColor = '00';
+            break;
+        case 'rgb':
+        default:
+            defaultColor = '1777d1';
+            break;
+    }
+
+    return defaultColor;
+}
+
+/**
+ * Convert given color code int color model object
+ *
+ * ColorModel consists of: {
+ *   color: number,
+ *   colorspace: string {'rgb', 'cmyk', 'gray'},
+ *   (colorspace == 'rgb')  r, g, b
+ *   (colorspace == 'cmyk') c, m, y, k
+ *   (colorspace == 'gray') gray
+ * }
+ *
+ * where r,g,b,c,m,y,k,gray are all numbers between 0 and 1
+ *
+ * @param {string} code the color encoding as HexColor
+ * @param {string} colorspace the name of the colorspace of given color code
+ * @param {string} colorName the name to be associated with given color code
+ * @returns {any} the color model
+ */
+function toColorModel(self, code, colorspace, colorName) {
+    const cmodel = {};
+    let color = hexToArray(code);
+
+    cmodel.color = parseInt(code, 16);
+
+    // The initial decider of color space is length of given 'code'.
+
+    switch (color.length) {
+        default:
+            color = hexToArray(_defaultColor('rgb'));
+            // purposely want to fall through to 'rgb' case below.
+        case 3:
+            cmodel.colorspace = 'rgb';
+            cmodel.r = color[0];
+            cmodel.g = color[1];
+            cmodel.b = color[2];
+            break;
+
+        case 4:
+            cmodel.colorspace = 'cmyk';
+            cmodel.c = color[0];
+            cmodel.m = color[1];
+            cmodel.y = color[2];
+            cmodel.k = color[3];
+            break;
+
+        case 1:
+            cmodel.colorspace = 'gray';
+            cmodel.gray = color[0];
+            break;
+    }
+
+    // When creating a separation color space,
+    // use the colorspace from above as the
+    // alternative color transformation when
+    // the named color is unavailable.
+    if (colorspace === 'separation' && colorName !== '') {
+        cmodel.colorspace = colorspace;
+        cmodel.colorName = colorName;
+        cmodel.colorspaceId = createColorSpaces(self, colorName, color);
+    }
+
+    return cmodel;
+}
+
+/**
+ * Convert percentage string into hex string (x / 100 * 255)
+ *
+ * @param {string} code numbers separated by commas with values ranging between 0-100.
+ * @returns {string} massaged hexadecimal string that can be used as input to hexToArray.
+ */
+function percentToHex(code) {
+    return arrayToHex(code.split(',').map( x => Math.round(x * 2.55)) );
+}
+
+/**
+ * Transform color code into numeric value or colorModel
+ *
+ * @param code color specification in form of HexColor (string, begins with '#'),
+ *             DecimalColor (1, 3, or 4 element array with values between 0-255),
+ *             PercentColor (string, begins with '%' followed by values separated
+ *             by commas with values between 0-100)
+ */
+exports._transformColor = function _transformColor(code = '', opt = {}) {
+    this.knownColors   = this.knownColors || {};
+    let colorspace     = opt.colorspace || 'rgb';
+    let wantColorModel = opt.wantColorModel || false;
+    let colorName      = opt.colorName || '';
+    let defaultColor   = _defaultColor(colorspace);
+    let transformation;
+
+    if (Array.isArray(code)) {
+        code = arrayToHex(code);
+    } else if (code.startsWith('#')){
+        code = code.replace('#', '');
+    } else if (code.startsWith('%')) {
+        code = percentToHex(code.replace('%', ''));
+    } else if (code !== '') {
+        let color = this.knownColors[colorspace][code];  // assuming code is a color name
+        if (!color) {
+            color = '';
+            code = defaultColor;
+        } else {
+            colorName = code;
+
+            // The following handles known colors in hex form,
+            // with or without initial '#' and percent form.
+            if (color.startsWith('#')) {
+                code = color.replace('#','');
+            } else if (color.startsWith('%')) {
+                code = percentToHex(color.replace('%', ''));
+            } else {
+                code = color;
+            }
+        }
+    }
+
+    // When colorspace is not explicitly given,
+    // use size of value to determine colorspace.
+    if (!opt.colorspace) {
+        colorspace = {2:'gray', 6:'rgb', 8:'cmyk'}[`${code.length}`] || 'rgb';
+        defaultColor = _defaultColor(colorspace);
+    }
+
+    // Suppply default color:
+    //  when colorspace is given and given color code does not have appropriate length, or
+    //  when colorspace is missing, verify allowable hex value sizes for rgb, cmyk, or gray.
+    if ( ['rgb','cmyk','gray'].includes(colorspace) && code.length != defaultColor.length ||
+         ! [2,6,8].includes(code.toString().length)) {
+        code = defaultColor;
+    }
+
+    if (wantColorModel) {
+        transformation = toColorModel(this, code, colorspace, colorName);
+        if ( colorName && !this.knownColors[colorspace][colorName]) {
+            this.chroma(colorName, code, colorspace);
+        }
+    } else {
+        transformation = parseInt(`0x${code.toUpperCase()}`, 16);
+    }
+
+    return transformation;
+};
+
+function arrayToHex(color = []) {
+    let code = '';
+    color.forEach((item) => {
+        let hex = item.toString(16);
+        hex = (hex.length == 1) ? '0' + hex : hex;
+        code += hex;
+    });
+    return code;
+}
+
+function hexToArray(hex) {
+    const result = /^#?([a-f\d]{2})([a-f\d]{2})?([a-f\d]{2})?([a-f\d]{2})?$/i.exec(hex);
+    return result
+        .reduce((array, item, index) => {
+            if (index >= 1 && index <= (hex.length / 2)) {
+                array.push(parseInt(item, 16) / 255);
+            }
+            return array;
+        }, []);
+}
+
+exports._colorNumberToRGB = (bigint) => {
+    if (!bigint) {
+        return {
+            r: 0,
+            g: 0,
+            b: 0
+        };
+    } else {
+        return {
+            r: (bigint >> 16) & 255,
+            g: (bigint >>  8) & 255,
+            b: bigint & 255
+        };
+    }
+};
 
@@ -445,7 +445,7 @@

colors.js


- Documentation generated by JSDoc 3.6.3 on Wed Oct 02 2019 12:00:26 GMT-0700 (Pacific Daylight Time) using the docdash theme. + Documentation generated by JSDoc 3.5.5 on Fri Nov 01 2019 20:41:12 GMT-0400 (Eastern Daylight Time) using the docdash theme.
diff --git a/docs/encrypt.js.html b/docs/encrypt.js.html index 1cf79ad..6e3a6ad 100644 --- a/docs/encrypt.js.html +++ b/docs/encrypt.js.html @@ -37,112 +37,112 @@

encrypt.js

-
const hummus = require('hummus');
-const fs = require('fs');
-
-
-/**
- * Encryption user access permissions
- *
- * This function supplies the numeric value for the encrypt function's 'userProtectionFlag'
- * option. When no argument is given, the default 'print' value is used.
- *
- * @name permission
- * @function
- * @memberof Recipe
- * @param {string} flags from the list print, modify, copy, edit, fillform, extract, assemble, and printbest
- * More than one may be specified by using a comma to separate the names in the input string.
- */
-exports.permission = function permission(flags='print') {
-
-    // https://www.adobe.com/content/dam/acom/en/devnet/pdf/pdfs/PDF32000_2008.pdf
-
-    const userAccessPermissions = {  // see table on page 61 of above document
-        print    : 1<<2,   // allow printing
-        modify   : 1<<3,   // allow template creation, signing, filling form fields
-        copy     : 1<<4,   // allow content copying and copying for accessibility
-        edit     : 1<<5,   // allow commenting
-        fillform : 1<<8,   // allow filling of form fields
-        extract  : 1<<9,   // allow content copying for accessibility
-        assemble : 1<<10,  // unused
-        printbest: 1<<11   // allow high resolution printing when 'print' is allowed
-    };
-
-    const perms = flags.split(',').map((x)=>{return x.trim();});
-    let access = 0;
-    perms.forEach((perm) => {
-        if (!userAccessPermissions[perm]) {
-            throw new Error(`Unknown user access permission (${perm})`);
-        }
-        access += userAccessPermissions[perm];
-    });
-
-    return access;
-};
-
-exports._getEncryptOptions = function _getEncryptOptions(options, addPermissions=true) {
-    const encryptOptions = {};
-
-    const password = options.password || options.ownerPassword;
-    if (password) {
-        encryptOptions.password = password;
-        encryptOptions.ownerPassword = password;
-    }
-
-    if (options.userPassword) {
-        encryptOptions.userPassword = options.userPassword;
-        if (!encryptOptions.password) {
-            encryptOptions.password = options.userPassword;
-        }
-    }
-
-    if (addPermissions) {
-        if (options.userProtectionFlag) {
-            encryptOptions.userProtectionFlag = options.userProtectionFlag;
-        }
-    }
-
-    // Only attach encryption mechanism when attributes
-    // have been explicitly given in the incoming options.
-    if (Object.keys(encryptOptions) > 0 && !encryptOptions.userPassword) {
-        encryptOptions.userPassword = '';
-        if (!encryptOptions.userProtectionFlag) {
-            encryptOptions.userProtectionFlag = this.permission();
-        }
-    }
-
-    return encryptOptions;
-};
-
-/**
- * Encrypt the pdf
- * @name encrypt
- * @function
- * @memberof Recipe
- * @param {Object} options - The options
- * @param {string} [options.password] - The permission password.
- * @param {string} [options.ownerPassword] - The password for editing.
- * @param {string} [options.userPassword] - The password for viewing & encryption.
- * @param {number} [options.userProtectionFlag] - The flag for the security level.
- */
-exports.encrypt = function encrypt(options = {}) {
-    this.needToEncrypt = true;
-    this.encryption_ = this._getEncryptOptions(options);
-
-    return this;
-};
-
-// http://pdfhummus.com/post/147451287581/hummus-1058-and-pdf-writer-updates-encryption
-exports._encrypt = function _encrypt() {
-    if (!this.encryption_) {
-        return;
-    }
-
-    const tmp = this.output + '.tmp.pdf';
-    fs.renameSync(this.output, tmp);
-    hummus.recrypt(tmp, this.output, this.encryption_);
-    fs.unlinkSync(tmp);
-};
+            
const hummus = require('hummus');
+const fs = require('fs');
+
+
+/**
+ * Encryption user access permissions
+ *
+ * This function supplies the numeric value for the encrypt function's 'userProtectionFlag'
+ * option. When no argument is given, the default 'print' value is used.
+ *
+ * @name permission
+ * @function
+ * @memberof Recipe
+ * @param {string} flags from the list print, modify, copy, edit, fillform, extract, assemble, and printbest
+ * More than one may be specified by using a comma to separate the names in the input string.
+ */
+exports.permission = function permission(flags='print') {
+
+    // https://www.adobe.com/content/dam/acom/en/devnet/pdf/pdfs/PDF32000_2008.pdf
+
+    const userAccessPermissions = {  // see table on page 61 of above document
+        print    : 1<<2,   // allow printing
+        modify   : 1<<3,   // allow template creation, signing, filling form fields
+        copy     : 1<<4,   // allow content copying and copying for accessibility
+        edit     : 1<<5,   // allow commenting
+        fillform : 1<<8,   // allow filling of form fields
+        extract  : 1<<9,   // allow content copying for accessibility
+        assemble : 1<<10,  // unused
+        printbest: 1<<11   // allow high resolution printing when 'print' is allowed
+    };
+
+    const perms = flags.split(',').map((x)=>{return x.trim();});
+    let access = 0;
+    perms.forEach((perm) => {
+        if (!userAccessPermissions[perm]) {
+            throw new Error(`Unknown user access permission (${perm})`);
+        }
+        access += userAccessPermissions[perm];
+    });
+
+    return access;
+};
+
+exports._getEncryptOptions = function _getEncryptOptions(options, addPermissions=true) {
+    const encryptOptions = {};
+
+    const password = options.password || options.ownerPassword;
+    if (password) {
+        encryptOptions.password = password;
+        encryptOptions.ownerPassword = password;
+    }
+
+    if (options.userPassword) {
+        encryptOptions.userPassword = options.userPassword;
+        if (!encryptOptions.password) {
+            encryptOptions.password = options.userPassword;
+        }
+    }
+
+    if (addPermissions) {
+        if (options.userProtectionFlag) {
+            encryptOptions.userProtectionFlag = options.userProtectionFlag;
+        }
+    }
+
+    // Only attach encryption mechanism when attributes
+    // have been explicitly given in the incoming options.
+    if (Object.keys(encryptOptions) > 0 && !encryptOptions.userPassword) {
+        encryptOptions.userPassword = '';
+        if (!encryptOptions.userProtectionFlag) {
+            encryptOptions.userProtectionFlag = this.permission();
+        }
+    }
+
+    return encryptOptions;
+};
+
+/**
+ * Encrypt the pdf
+ * @name encrypt
+ * @function
+ * @memberof Recipe
+ * @param {Object} options - The options
+ * @param {string} [options.password] - The permission password.
+ * @param {string} [options.ownerPassword] - The password for editing.
+ * @param {string} [options.userPassword] - The password for viewing & encryption.
+ * @param {number} [options.userProtectionFlag] - The flag for the security level.
+ */
+exports.encrypt = function encrypt(options = {}) {
+    this.needToEncrypt = true;
+    this.encryption_ = this._getEncryptOptions(options);
+
+    return this;
+};
+
+// http://pdfhummus.com/post/147451287581/hummus-1058-and-pdf-writer-updates-encryption
+exports._encrypt = function _encrypt() {
+    if (!this.encryption_) {
+        return;
+    }
+
+    const tmp = this.output + '.tmp.pdf';
+    fs.renameSync(this.output, tmp);
+    hummus.recrypt(tmp, this.output, this.encryption_);
+    fs.unlinkSync(tmp);
+};
 
@@ -155,7 +155,7 @@

encrypt.js


- Documentation generated by JSDoc 3.6.3 on Wed Oct 02 2019 12:00:26 GMT-0700 (Pacific Daylight Time) using the docdash theme. + Documentation generated by JSDoc 3.5.5 on Fri Nov 01 2019 20:41:12 GMT-0400 (Eastern Daylight Time) using the docdash theme.
diff --git a/docs/font.js.html b/docs/font.js.html index 8ce9ece..b7c1d65 100644 --- a/docs/font.js.html +++ b/docs/font.js.html @@ -37,42 +37,42 @@

font.js

-
const fs = require('fs');
-const path = require('path');
-/**
- * Register a custom font
- * @name registerFont
- * @function
- * @memberof Recipe
- * @param {string} fontName - The font name will be used in text
- * @param {string} fontSrcPath - The path to the font file.
- */
-exports.registerFont = function registerFont(fontName = '', fontSrcPath = '') {
-    return this._registerFont(fontName, fontSrcPath);
-};
-
-exports._loadFonts = function _loadFonts(fontSrcPath) {
-    const fontTypes = ['.ttf', '.ttc', '.otf'];
-    const fontPaths = (typeof fontSrcPath === 'string') ? [fontSrcPath] : fontSrcPath;
-
-    for (let fpath of fontPaths) {
-        fs
-            .readdirSync(fpath)
-            .filter((file) => {
-                return fontTypes.includes(path.extname(file).toLowerCase());
-            })
-            .forEach((file) => {
-                const fontName = path.basename(file, path.extname(file));
-                return this._registerFont(fontName, path.join(fpath, file));
-            });
-    }
-};
-
-exports._registerFont = function _registerFont(fontName, fontSrcPath) {
-    this.fonts = this.fonts || {};
-    // check fontSrcPath
-    this.fonts[fontName.toLowerCase()] = fontSrcPath;
-};
+            
const fs = require('fs');
+const path = require('path');
+/**
+ * Register a custom font
+ * @name registerFont
+ * @function
+ * @memberof Recipe
+ * @param {string} fontName - The font name will be used in text
+ * @param {string} fontSrcPath - The path to the font file.
+ */
+exports.registerFont = function registerFont(fontName = '', fontSrcPath = '') {
+    return this._registerFont(fontName, fontSrcPath);
+};
+
+exports._loadFonts = function _loadFonts(fontSrcPath) {
+    const fontTypes = ['.ttf', '.ttc', '.otf'];
+    const fontPaths = (typeof fontSrcPath === 'string') ? [fontSrcPath] : fontSrcPath;
+
+    for (let fpath of fontPaths) {
+        fs
+            .readdirSync(fpath)
+            .filter((file) => {
+                return fontTypes.includes(path.extname(file).toLowerCase());
+            })
+            .forEach((file) => {
+                const fontName = path.basename(file, path.extname(file));
+                return this._registerFont(fontName, path.join(fpath, file));
+            });
+    }
+};
+
+exports._registerFont = function _registerFont(fontName, fontSrcPath) {
+    this.fonts = this.fonts || {};
+    // check fontSrcPath
+    this.fonts[fontName.toLowerCase()] = fontSrcPath;
+};
 
@@ -85,7 +85,7 @@

font.js


- Documentation generated by JSDoc 3.6.3 on Wed Oct 02 2019 12:00:26 GMT-0700 (Pacific Daylight Time) using the docdash theme. + Documentation generated by JSDoc 3.5.5 on Fri Nov 01 2019 20:41:12 GMT-0400 (Eastern Daylight Time) using the docdash theme.
diff --git a/docs/global.html b/docs/global.html index d9d2a40..cfea0ee 100644 --- a/docs/global.html +++ b/docs/global.html @@ -127,7 +127,7 @@

_get
Source:
@@ -304,9 +304,9 @@

Parameters:
+ DecimalColor (1, 3, or 4 element array with values between 0-255), + PercentColor (string, begins with '%' followed by values separated + by commas with values between 0-100)

@@ -1302,11 +1302,11 @@

toColorMo

Convert given color code int color model object

ColorModel consists of: { -color: number, -colorspace: string {'rgb', 'cmyk', 'gray'}, -(colorspace == 'rgb') r, g, b -(colorspace == 'cmyk') c, m, y, k -(colorspace == 'gray') gray + color: number, + colorspace: string {'rgb', 'cmyk', 'gray'}, + (colorspace == 'rgb') r, g, b + (colorspace == 'cmyk') c, m, y, k + (colorspace == 'gray') gray }

where r,g,b,c,m,y,k,gray are all numbers between 0 and 1

@@ -1470,7 +1470,7 @@

Returns:

- Documentation generated by JSDoc 3.6.3 on Wed Oct 02 2019 12:00:26 GMT-0700 (Pacific Daylight Time) using the docdash theme. + Documentation generated by JSDoc 3.5.5 on Fri Nov 01 2019 20:41:12 GMT-0400 (Eastern Daylight Time) using the docdash theme.
diff --git a/docs/image.js.html b/docs/image.js.html index 0179257..ff5db5d 100644 --- a/docs/image.js.html +++ b/docs/image.js.html @@ -37,131 +37,131 @@

image.js

-
/**
- * Place images to pdf
- * @name image
- * @function
- * @memberof Recipe
- * @param {string} imgSrc - The path for the image. [JPEG, PNG, TIFF]
- * @param {number} x - The coordinate x
- * @param {number} y - The coordinate y
- * @param {Object} [options] - The options
- * @param {number} [options.width] - The new width
- * @param {number} [options.height] - The new height
- * @param {number} [options.scale] - Scale the image from the original width and height.
- * @param {boolean} [options.keepAspectRatio=true] - Keep the aspect ratio.
- * @param {number} [options.opacity] - The opacity.
- * @param {string} [options.align] - 'center center'...
- */
-exports.image = function image(imgSrc, x, y, options = {}) {
-    const { width, height, offsetX, offsetY } = this._getImgOffset(imgSrc, options);
-    const imgOptions = {
-        transformation: {
-            fit: 'always',
-            // proportional: true,
-            width,
-            height
-        }
-    };
-    const { nx, ny } = this._calibrateCoordinate(x, y, offsetX, offsetY);
-
-    const _options = this._getPathOptions(options, nx, ny);
-    const gsId = _options.fillGsId;
-
-    // See if this image has been seen already, so as to not duplicate it.
-    let xObject = this.xObjects.find((element) => {
-        return element.get('name') == imgSrc;
-    });
-
-    if (xObject) {
-        _options.xObject = xObject;
-        _options.ratio = [width / xObject.get('width'), height / xObject.get('height')];
-    }
-
-    this._drawObject(this, nx, ny, width, height, _options, (ctx, xObject) => {
-
-        // Only new images visit here
-        xObject.set('type', 'image');
-        xObject.set('name', imgSrc);
-        xObject.set('width', width);
-        xObject.set('height', height);
-
-        this.xObjects.push(xObject);
-
-        ctx
-            .gs(xObject.getGsName(gsId))
-            .drawImage(0, 0, imgSrc, imgOptions);
-    });
-
-    return this;
-};
-
-exports._getImgOffset = function _getImgOffset(imgSrc = '', options = {}) {
-    // set default to true
-    options.keepAspectRatio = (options.keepAspectRatio == void 0) ?
-        true : options.keepAspectRatio;
-    const dimensions = this.writer.getImageDimensions(imgSrc);
-    const ratio = dimensions.width / dimensions.height;
-
-    let width = dimensions.width;
-    let height = dimensions.height;
-    if (options.scale) {
-        width = width * options.scale;
-        height = height * options.scale;
-    } else
-    if (options.width && !options.height) {
-        width = options.width;
-        height = options.width / ratio;
-    } else
-    if (!options.width && options.height) {
-        width = options.height * ratio;
-        height = options.height;
-    } else
-    if (options.width && options.height) {
-        if (!options.keepAspectRatio) {
-            width = options.width;
-            height = options.height;
-        } else {
-            // fit to the smaller
-            if (options.width / ratio <= options.height) {
-                width = options.width;
-                height = options.width / ratio;
-            } else {
-                width = options.height * ratio;
-                height = options.height;
-            }
-        }
-    }
-    let offsetX = 0;
-    let offsetY = -height;
-
-    if (options.align) {
-        const alignments = options.align.split(' ');
-        if (alignments[0]) {
-            switch (alignments[0]) {
-                case 'center':
-                    offsetX = -1 * width / 2;
-                    break;
-                case 'right':
-                    offsetX = width / 2;
-                    break;
-                default:
-            }
-        }
-        if (alignments[1]) {
-            switch (alignments[1]) {
-                case 'center':
-                    offsetY = -1 * height / 2;
-                    break;
-                case 'bottom':
-                    offsetY = height / 2;
-                    break;
-                default:
-            }
-        }
-    }
-    return { width, height, offsetX, offsetY };
-};
+            
/**
+ * Place images to pdf
+ * @name image
+ * @function
+ * @memberof Recipe
+ * @param {string} imgSrc - The path for the image. [JPEG, PNG, TIFF]
+ * @param {number} x - The coordinate x
+ * @param {number} y - The coordinate y
+ * @param {Object} [options] - The options
+ * @param {number} [options.width] - The new width
+ * @param {number} [options.height] - The new height
+ * @param {number} [options.scale] - Scale the image from the original width and height.
+ * @param {boolean} [options.keepAspectRatio=true] - Keep the aspect ratio.
+ * @param {number} [options.opacity] - The opacity.
+ * @param {string} [options.align] - 'center center'...
+ */
+exports.image = function image(imgSrc, x, y, options = {}) {
+    const { width, height, offsetX, offsetY } = this._getImgOffset(imgSrc, options);
+    const imgOptions = {
+        transformation: {
+            fit: 'always',
+            // proportional: true,
+            width,
+            height
+        }
+    };
+    const { nx, ny } = this._calibrateCoordinate(x, y, offsetX, offsetY);
+
+    const _options = this._getPathOptions(options, nx, ny);
+    const gsId = _options.fillGsId;
+
+    // See if this image has been seen already, so as to not duplicate it.
+    let xObject = this.xObjects.find((element) => {
+        return element.get('name') == imgSrc;
+    });
+
+    if (xObject) {
+        _options.xObject = xObject;
+        _options.ratio = [width / xObject.get('width'), height / xObject.get('height')];
+    }
+
+    this._drawObject(this, nx, ny, width, height, _options, (ctx, xObject) => {
+
+        // Only new images visit here
+        xObject.set('type', 'image');
+        xObject.set('name', imgSrc);
+        xObject.set('width', width);
+        xObject.set('height', height);
+
+        this.xObjects.push(xObject);
+
+        ctx
+            .gs(xObject.getGsName(gsId))
+            .drawImage(0, 0, imgSrc, imgOptions);
+    });
+
+    return this;
+};
+
+exports._getImgOffset = function _getImgOffset(imgSrc = '', options = {}) {
+    // set default to true
+    options.keepAspectRatio = (options.keepAspectRatio == void 0) ?
+        true : options.keepAspectRatio;
+    const dimensions = this.writer.getImageDimensions(imgSrc);
+    const ratio = dimensions.width / dimensions.height;
+
+    let width = dimensions.width;
+    let height = dimensions.height;
+    if (options.scale) {
+        width = width * options.scale;
+        height = height * options.scale;
+    } else
+    if (options.width && !options.height) {
+        width = options.width;
+        height = options.width / ratio;
+    } else
+    if (!options.width && options.height) {
+        width = options.height * ratio;
+        height = options.height;
+    } else
+    if (options.width && options.height) {
+        if (!options.keepAspectRatio) {
+            width = options.width;
+            height = options.height;
+        } else {
+            // fit to the smaller
+            if (options.width / ratio <= options.height) {
+                width = options.width;
+                height = options.width / ratio;
+            } else {
+                width = options.height * ratio;
+                height = options.height;
+            }
+        }
+    }
+    let offsetX = 0;
+    let offsetY = -height;
+
+    if (options.align) {
+        const alignments = options.align.split(' ');
+        if (alignments[0]) {
+            switch (alignments[0]) {
+                case 'center':
+                    offsetX = -1 * width / 2;
+                    break;
+                case 'right':
+                    offsetX = width / 2;
+                    break;
+                default:
+            }
+        }
+        if (alignments[1]) {
+            switch (alignments[1]) {
+                case 'center':
+                    offsetY = -1 * height / 2;
+                    break;
+                case 'bottom':
+                    offsetY = height / 2;
+                    break;
+                default:
+            }
+        }
+    }
+    return { width, height, offsetX, offsetY };
+};
 
@@ -174,7 +174,7 @@

image.js


- Documentation generated by JSDoc 3.6.3 on Wed Oct 02 2019 12:00:26 GMT-0700 (Pacific Daylight Time) using the docdash theme. + Documentation generated by JSDoc 3.5.5 on Fri Nov 01 2019 20:41:12 GMT-0400 (Eastern Daylight Time) using the docdash theme.
diff --git a/docs/index.html b/docs/index.html index 56f003a..48ab3e3 100644 --- a/docs/index.html +++ b/docs/index.html @@ -55,7 +55,7 @@

Home

Classes

  • diff --git a/docs/info.js.html b/docs/info.js.html index 9899000..b98ef9e 100644 --- a/docs/info.js.html +++ b/docs/info.js.html @@ -37,331 +37,331 @@

    info.js

    -
    const fs = require('fs');
    -const hummus = require('hummus');
    -/**
    - * @name info
    - * @desc Add new PDF information, or retrieve existing PDF information.
    - * @memberof Recipe
    - * @function
    - * @param {Object} [options] - The options (when missing obtains existing PDF information)
    - * @param {number} [options.version] - The pdf version
    - * @param {string} [options.author] - The author
    - * @param {string} [options.title] - The title
    - * @param {string} [options.subject] - The subject
    - * @param {string[]} [options.keywords] - The array of keywords
    - */
    -exports.info = function info(options) {
    -    let result;
    -
    -    if (! options) {
    -        result = this._readInfo();
    -        
    -    } else {
    -        this.toWriteInfo_ = this.toWriteInfo_ || {};
    -        Object.assign(this.toWriteInfo_, options);
    -        result = this;
    -    }
    -
    -    return result;
    -};
    -
    -exports._readInfo = function _readInfo() {
    -    if (!this.isNewPDF && !this.infoDictionary) {
    -        const copyFrom = this.isBufferSrc ? new hummus.PDFRStreamForBuffer(this.src) : this.src;
    -        const copyCtx  = this.writer.createPDFCopyingContext(copyFrom);
    -        const infoDict = copyCtx.getSourceDocumentParser().queryDictionaryObject(
    -            copyCtx.getSourceDocumentParser().getTrailer(), 'Info'
    -        );
    -    
    -        const oldInfo = (infoDict && infoDict.toJSObject) ? infoDict.toJSObject() : null;
    -    
    -        if (oldInfo) {
    -            this.infoDictionary = {}
    -            Object.getOwnPropertyNames(oldInfo).forEach((key) => {
    -                if (!oldInfo[key]) {
    -                    return;
    -                }
    -                const oldInforSrc = this._parseObjectByType(oldInfo[key]);
    -                if (!oldInforSrc) {
    -                    return;
    -                }
    -                switch (key) {
    -                    case 'Trapped':
    -                        if (oldInforSrc && oldInforSrc.value) {
    -                            this.infoDictionary.trapped = oldInforSrc.value;
    -                        }
    -                        break;
    -                    case 'CreationDate':
    -                        if (oldInforSrc && oldInforSrc.value) {
    -                            this.infoDictionary.creationDate = oldInforSrc.value;
    -                        }
    -                        break;
    -                    case 'ModDate':
    -                        if (oldInforSrc && oldInforSrc.value) {
    -                            this.infoDictionary.modDate = oldInforSrc.value;
    -                        }
    -                        break;
    -                    case 'Creator':
    -                        if (oldInforSrc && oldInforSrc.toText) {
    -                            this.infoDictionary.creator = oldInforSrc.toText();
    -                        }
    -                        break;
    -                    case 'Producer':
    -                        if (oldInforSrc && oldInforSrc.toText) {
    -                            this.infoDictionary.producer = oldInforSrc.toText();
    -                        }
    -                        break;
    -                    default:
    -                        if (oldInforSrc && oldInforSrc.toText) {
    -                            this.infoDictionary[key.toLowerCase()] = oldInforSrc.toText();
    -                        }
    -                }
    -            });
    -        }
    -    }
    -
    -    return this.infoDictionary;
    -}
    -
    -exports._writeInfo = function _writeInfo() {
    -    const options = this.toWriteInfo_ || {};
    -    const oldInfo = this._readInfo();
    -    /*
    -        #41, #48
    -        This issue is due to the unhandled process exit from HummusJS.
    -        I have to disable this part before it gets fixed in HummusJS.
    -    */
    -    
    -    const infoDictionary = this.writer.getDocumentContext().getInfoDictionary();
    -    const fields = [{
    -        key: 'author',
    -        type: 'string'
    -    }, {
    -        key: 'title',
    -        type: 'string'
    -    }, {
    -        key: 'subject',
    -        type: 'string'
    -    }, {
    -        key: 'keywords',
    -        type: 'array'
    -    }];
    -    // const ignores = [
    -    //     'CreationDate', 'Creator', 'ModDate', 'Producer'
    -    // ];
    -
    -    if (oldInfo) {
    -        Object.getOwnPropertyNames(oldInfo).forEach((key) => {
    -            if (!oldInfo[key]) {
    -                return;
    -            }
    -
    -            switch (key) {
    -                case 'trapped':
    -                    infoDictionary.trapped = oldInfo.trapped;
    -                    break;
    -                case 'creationDate':
    -                    infoDictionary.setCreationDate(oldInfo.creationDate);
    -                    break;
    -                case 'modDate':
    -                    infoDictionary.addAdditionalInfoEntry('source-ModDate', oldInfo.modDate);
    -                    break;
    -                case 'creator':
    -                    infoDictionary.addAdditionalInfoEntry('source-Creator', oldInfo.creator);
    -                    break;
    -                case 'producer':
    -                    infoDictionary.addAdditionalInfoEntry('source-Producer', oldInfo.producer);
    -                    break;
    -                default:
    -                    infoDictionary[key] = oldInfo[key];
    -            }
    -        });
    -    }
    -
    -    if (this.isNewPDF) {
    -        infoDictionary.setCreationDate(new Date());
    -    }
    -    infoDictionary.setModDate(new Date());
    -    infoDictionary.producer = 'PDFHummus (https://github.com/galkahana/HummusJS)';
    -    infoDictionary.creator = 'Hummus-Recipe (https://github.com/chunyenHuang/hummusRecipe)';
    -
    -    fields.forEach((item) => {
    -        let value = options[item.key];
    -        if (!value) {
    -            return;
    -        } else {
    -            switch (item.type) {
    -                case 'string':
    -                    value = value.toString();
    -                    break;
    -                case 'date':
    -                    value = new Date(value);
    -                    break;
    -                case 'array':
    -                    value = (Array.isArray(value)) ? value : [value];
    -                    break;
    -                default:
    -            }
    -        }
    -        if (item.func) {
    -            infoDictionary[item.func](value);
    -        } else {
    -            infoDictionary[item.key] = value;
    -        }
    -    });
    -    return this;
    -};
    -
    -/**
    - * @name custom
    - * @desc Add custom information to pdf
    - * @memberof Recipe
    - * @function
    - * @param {string} [key] - The key
    - * @param {string} [value] - The value
    - */
    -exports.custom = function custom(key, value) {
    -    const infoDictionary = this.writer.getDocumentContext().getInfoDictionary();
    -    infoDictionary.addAdditionalInfoEntry(key.toString(), value.toString());
    -    return this;
    -};
    -
    -exports.structure = function structure(output) {
    -    // PDF file format http://lotabout.me/orgwiki/pdf.html
    -    // const outputFileType = path.extname(output);
    -    const outputFile = fs.openSync(output, 'w');
    -    const hummus = this.hummus;
    -    const pdfReader = this.pdfReader;
    -
    -    const tabWidth = '  ';
    -    const structures = [
    -        'Info',
    -        'Root', // catalog
    -        'Size',
    -        'Prev',
    -        'ID',
    -        // 'Encrypt',
    -        // 'XRefStm'
    -    ];
    -
    -    const write = (item) => {
    -        const mIteratedObjectIDs = {};
    -        let mTabLevel = 0;
    -
    -        const addTabs = () => {
    -            let output = '';
    -            for (let i = 0; i < mTabLevel; ++i) {
    -                output += tabWidth;
    -            }
    -            return output;
    -        };
    -
    -        const logToFile = (inString) => {
    -            fs.writeSync(outputFile, addTabs() + inString + '\r\n');
    -        };
    -
    -        const iterateObjectTypes = (inObject) => {
    -            const type = inObject.getType();
    -            const label = hummus.getTypeLabel(type);
    -            let output = '';
    -            let objectID, jsArray, aDictionary, keys;
    -
    -            switch (type) {
    -                case hummus.ePDFObjectIndirectObjectReference:
    -                    ++mTabLevel;
    -                    objectID = inObject.toPDFIndirectObjectReference().getObjectID();
    -                    output += `Indirect object reference (${objectID}): `;
    -                    logToFile(output);
    -                    if (!Object.prototype.hasOwnProperty.call(mIteratedObjectIDs, objectID)) {
    -                        mIteratedObjectIDs[objectID] = true;
    -                        iterateObjectTypes(pdfReader.parseNewObject(objectID));
    -                    }
    -                    for (var i = 0; i < mTabLevel; ++i) {
    -                        output += ' ';
    -                    }
    -                    --mTabLevel;
    -                    return;
    -                case hummus.ePDFObjectArray:
    -                    jsArray = inObject.toPDFArray().toJSArray();
    -                    output += `- ${label} [${jsArray.length}]`;
    -                    logToFile(output);
    -                    ++mTabLevel;
    -                    jsArray.forEach((element) => {
    -                        iterateObjectTypes(element);
    -                    });
    -                    --mTabLevel;
    -                    break;
    -                case hummus.ePDFObjectDictionary:
    -                    aDictionary = inObject.toPDFDictionary().toJSObject();
    -                    keys = Object.getOwnPropertyNames(aDictionary).join(', ');
    -                    output += `- ${label} {${keys}}`;
    -                    logToFile(output);
    -                    ++mTabLevel;
    -                    Object.getOwnPropertyNames(aDictionary).forEach((element) => {
    -                        logToFile(element + ' *');
    -                        iterateObjectTypes(aDictionary[element]);
    -                    });
    -                    --mTabLevel;
    -                    break;
    -                case hummus.ePDFObjectStream:
    -                    output += 'Stream . iterating stream dictionary:';
    -                    logToFile(output);
    -                    iterateObjectTypes(inObject.toPDFStream().getDictionary());
    -                    break;
    -                default:
    -                    output += `${tabWidth}${label}: ${inObject}`;
    -                    logToFile(output);
    -            }
    -        };
    -
    -        const itemTrailer = pdfReader.queryDictionaryObject(pdfReader.getTrailer(), item);
    -        logToFile(item);
    -        iterateObjectTypes(itemTrailer);
    -    };
    -
    -    structures.forEach((item) => {
    -        write(item);
    -    });
    -
    -    fs.closeSync(outputFile);
    -    return this;
    -};
    -
    -exports._parseObjectByType = function _parseObjectByType(inObject) {
    -    if (!inObject) {
    -        return;
    -    }
    -    const hummus = this.hummus;
    -    const pdfReader = this.pdfReader;
    -    const type = inObject.getType();
    -    const label = hummus.getTypeLabel(type);
    -    const saveToObject = this.pdfStructure || {};
    -    let objectID, parsed, dictionaryObject, dictionary;
    -    switch (type) {
    -        case hummus.ePDFObjectIndirectObjectReference:
    -            objectID = inObject.toPDFIndirectObjectReference().getObjectID();
    -            parsed = pdfReader.parseNewObject(objectID);
    -            return this._parseObjectByType(parsed);
    -        case hummus.ePDFObjectArray:
    -            inObject.toPDFArray().toJSArray().forEach((element) => {
    -                this._parseObjectByType(element);
    -            });
    -            break;
    -        case hummus.ePDFObjectDictionary:
    -            dictionaryObject = inObject.toPDFDictionary().toJSObject();
    -            Object.getOwnPropertyNames(dictionaryObject).forEach((element) => {
    -                this._parseObjectByType(dictionaryObject[element]);
    -            });
    -            break;
    -        case hummus.ePDFObjectStream:
    -            dictionary = inObject.toPDFStream().getDictionary();
    -            return this._parseObjectByType(dictionary);
    -        default:
    -            saveToObject[`${label}-${Date.now()*Math.random()}`] = inObject;
    -            return inObject;
    -    }
    -};
    +            
    const fs = require('fs');
    +const hummus = require('hummus');
    +/**
    + * @name info
    + * @desc Add new PDF information, or retrieve existing PDF information.
    + * @memberof Recipe
    + * @function
    + * @param {Object} [options] - The options (when missing obtains existing PDF information)
    + * @param {number} [options.version] - The pdf version
    + * @param {string} [options.author] - The author
    + * @param {string} [options.title] - The title
    + * @param {string} [options.subject] - The subject
    + * @param {string[]} [options.keywords] - The array of keywords
    + */
    +exports.info = function info(options) {
    +    let result;
    +
    +    if (! options) {
    +        result = this._readInfo();
    +        
    +    } else {
    +        this.toWriteInfo_ = this.toWriteInfo_ || {};
    +        Object.assign(this.toWriteInfo_, options);
    +        result = this;
    +    }
    +
    +    return result;
    +};
    +
    +exports._readInfo = function _readInfo() {
    +    if (!this.isNewPDF && !this.infoDictionary) {
    +        const copyFrom = this.isBufferSrc ? new hummus.PDFRStreamForBuffer(this.src) : this.src;
    +        const copyCtx  = this.writer.createPDFCopyingContext(copyFrom);
    +        const infoDict = copyCtx.getSourceDocumentParser().queryDictionaryObject(
    +            copyCtx.getSourceDocumentParser().getTrailer(), 'Info'
    +        );
    +    
    +        const oldInfo = (infoDict && infoDict.toJSObject) ? infoDict.toJSObject() : null;
    +    
    +        if (oldInfo) {
    +            this.infoDictionary = {}
    +            Object.getOwnPropertyNames(oldInfo).forEach((key) => {
    +                if (!oldInfo[key]) {
    +                    return;
    +                }
    +                const oldInforSrc = this._parseObjectByType(oldInfo[key]);
    +                if (!oldInforSrc) {
    +                    return;
    +                }
    +                switch (key) {
    +                    case 'Trapped':
    +                        if (oldInforSrc && oldInforSrc.value) {
    +                            this.infoDictionary.trapped = oldInforSrc.value;
    +                        }
    +                        break;
    +                    case 'CreationDate':
    +                        if (oldInforSrc && oldInforSrc.value) {
    +                            this.infoDictionary.creationDate = oldInforSrc.value;
    +                        }
    +                        break;
    +                    case 'ModDate':
    +                        if (oldInforSrc && oldInforSrc.value) {
    +                            this.infoDictionary.modDate = oldInforSrc.value;
    +                        }
    +                        break;
    +                    case 'Creator':
    +                        if (oldInforSrc && oldInforSrc.toText) {
    +                            this.infoDictionary.creator = oldInforSrc.toText();
    +                        }
    +                        break;
    +                    case 'Producer':
    +                        if (oldInforSrc && oldInforSrc.toText) {
    +                            this.infoDictionary.producer = oldInforSrc.toText();
    +                        }
    +                        break;
    +                    default:
    +                        if (oldInforSrc && oldInforSrc.toText) {
    +                            this.infoDictionary[key.toLowerCase()] = oldInforSrc.toText();
    +                        }
    +                }
    +            });
    +        }
    +    }
    +
    +    return this.infoDictionary;
    +}
    +
    +exports._writeInfo = function _writeInfo() {
    +    const options = this.toWriteInfo_ || {};
    +    const oldInfo = this._readInfo();
    +    /*
    +        #41, #48
    +        This issue is due to the unhandled process exit from HummusJS.
    +        I have to disable this part before it gets fixed in HummusJS.
    +    */
    +    
    +    const infoDictionary = this.writer.getDocumentContext().getInfoDictionary();
    +    const fields = [{
    +        key: 'author',
    +        type: 'string'
    +    }, {
    +        key: 'title',
    +        type: 'string'
    +    }, {
    +        key: 'subject',
    +        type: 'string'
    +    }, {
    +        key: 'keywords',
    +        type: 'array'
    +    }];
    +    // const ignores = [
    +    //     'CreationDate', 'Creator', 'ModDate', 'Producer'
    +    // ];
    +
    +    if (oldInfo) {
    +        Object.getOwnPropertyNames(oldInfo).forEach((key) => {
    +            if (!oldInfo[key]) {
    +                return;
    +            }
    +
    +            switch (key) {
    +                case 'trapped':
    +                    infoDictionary.trapped = oldInfo.trapped;
    +                    break;
    +                case 'creationDate':
    +                    infoDictionary.setCreationDate(oldInfo.creationDate);
    +                    break;
    +                case 'modDate':
    +                    infoDictionary.addAdditionalInfoEntry('source-ModDate', oldInfo.modDate);
    +                    break;
    +                case 'creator':
    +                    infoDictionary.addAdditionalInfoEntry('source-Creator', oldInfo.creator);
    +                    break;
    +                case 'producer':
    +                    infoDictionary.addAdditionalInfoEntry('source-Producer', oldInfo.producer);
    +                    break;
    +                default:
    +                    infoDictionary[key] = oldInfo[key];
    +            }
    +        });
    +    }
    +
    +    if (this.isNewPDF) {
    +        infoDictionary.setCreationDate(new Date());
    +    }
    +    infoDictionary.setModDate(new Date());
    +    infoDictionary.producer = 'PDFHummus (https://github.com/galkahana/HummusJS)';
    +    infoDictionary.creator = 'Hummus-Recipe (https://github.com/chunyenHuang/hummusRecipe)';
    +
    +    fields.forEach((item) => {
    +        let value = options[item.key];
    +        if (!value) {
    +            return;
    +        } else {
    +            switch (item.type) {
    +                case 'string':
    +                    value = value.toString();
    +                    break;
    +                case 'date':
    +                    value = new Date(value);
    +                    break;
    +                case 'array':
    +                    value = (Array.isArray(value)) ? value : [value];
    +                    break;
    +                default:
    +            }
    +        }
    +        if (item.func) {
    +            infoDictionary[item.func](value);
    +        } else {
    +            infoDictionary[item.key] = value;
    +        }
    +    });
    +    return this;
    +};
    +
    +/**
    + * @name custom
    + * @desc Add custom information to pdf
    + * @memberof Recipe
    + * @function
    + * @param {string} [key] - The key
    + * @param {string} [value] - The value
    + */
    +exports.custom = function custom(key, value) {
    +    const infoDictionary = this.writer.getDocumentContext().getInfoDictionary();
    +    infoDictionary.addAdditionalInfoEntry(key.toString(), value.toString());
    +    return this;
    +};
    +
    +exports.structure = function structure(output) {
    +    // PDF file format http://lotabout.me/orgwiki/pdf.html
    +    // const outputFileType = path.extname(output);
    +    const outputFile = fs.openSync(output, 'w');
    +    const hummus = this.hummus;
    +    const pdfReader = this.pdfReader;
    +
    +    const tabWidth = '  ';
    +    const structures = [
    +        'Info',
    +        'Root', // catalog
    +        'Size',
    +        'Prev',
    +        'ID',
    +        // 'Encrypt',
    +        // 'XRefStm'
    +    ];
    +
    +    const write = (item) => {
    +        const mIteratedObjectIDs = {};
    +        let mTabLevel = 0;
    +
    +        const addTabs = () => {
    +            let output = '';
    +            for (let i = 0; i < mTabLevel; ++i) {
    +                output += tabWidth;
    +            }
    +            return output;
    +        };
    +
    +        const logToFile = (inString) => {
    +            fs.writeSync(outputFile, addTabs() + inString + '\r\n');
    +        };
    +
    +        const iterateObjectTypes = (inObject) => {
    +            const type = inObject.getType();
    +            const label = hummus.getTypeLabel(type);
    +            let output = '';
    +            let objectID, jsArray, aDictionary, keys;
    +
    +            switch (type) {
    +                case hummus.ePDFObjectIndirectObjectReference:
    +                    ++mTabLevel;
    +                    objectID = inObject.toPDFIndirectObjectReference().getObjectID();
    +                    output += `Indirect object reference (${objectID}): `;
    +                    logToFile(output);
    +                    if (!Object.prototype.hasOwnProperty.call(mIteratedObjectIDs, objectID)) {
    +                        mIteratedObjectIDs[objectID] = true;
    +                        iterateObjectTypes(pdfReader.parseNewObject(objectID));
    +                    }
    +                    for (var i = 0; i < mTabLevel; ++i) {
    +                        output += ' ';
    +                    }
    +                    --mTabLevel;
    +                    return;
    +                case hummus.ePDFObjectArray:
    +                    jsArray = inObject.toPDFArray().toJSArray();
    +                    output += `- ${label} [${jsArray.length}]`;
    +                    logToFile(output);
    +                    ++mTabLevel;
    +                    jsArray.forEach((element) => {
    +                        iterateObjectTypes(element);
    +                    });
    +                    --mTabLevel;
    +                    break;
    +                case hummus.ePDFObjectDictionary:
    +                    aDictionary = inObject.toPDFDictionary().toJSObject();
    +                    keys = Object.getOwnPropertyNames(aDictionary).join(', ');
    +                    output += `- ${label} {${keys}}`;
    +                    logToFile(output);
    +                    ++mTabLevel;
    +                    Object.getOwnPropertyNames(aDictionary).forEach((element) => {
    +                        logToFile(element + ' *');
    +                        iterateObjectTypes(aDictionary[element]);
    +                    });
    +                    --mTabLevel;
    +                    break;
    +                case hummus.ePDFObjectStream:
    +                    output += 'Stream . iterating stream dictionary:';
    +                    logToFile(output);
    +                    iterateObjectTypes(inObject.toPDFStream().getDictionary());
    +                    break;
    +                default:
    +                    output += `${tabWidth}${label}: ${inObject}`;
    +                    logToFile(output);
    +            }
    +        };
    +
    +        const itemTrailer = pdfReader.queryDictionaryObject(pdfReader.getTrailer(), item);
    +        logToFile(item);
    +        iterateObjectTypes(itemTrailer);
    +    };
    +
    +    structures.forEach((item) => {
    +        write(item);
    +    });
    +
    +    fs.closeSync(outputFile);
    +    return this;
    +};
    +
    +exports._parseObjectByType = function _parseObjectByType(inObject) {
    +    if (!inObject) {
    +        return;
    +    }
    +    const hummus = this.hummus;
    +    const pdfReader = this.pdfReader;
    +    const type = inObject.getType();
    +    const label = hummus.getTypeLabel(type);
    +    const saveToObject = this.pdfStructure || {};
    +    let objectID, parsed, dictionaryObject, dictionary;
    +    switch (type) {
    +        case hummus.ePDFObjectIndirectObjectReference:
    +            objectID = inObject.toPDFIndirectObjectReference().getObjectID();
    +            parsed = pdfReader.parseNewObject(objectID);
    +            return this._parseObjectByType(parsed);
    +        case hummus.ePDFObjectArray:
    +            inObject.toPDFArray().toJSArray().forEach((element) => {
    +                this._parseObjectByType(element);
    +            });
    +            break;
    +        case hummus.ePDFObjectDictionary:
    +            dictionaryObject = inObject.toPDFDictionary().toJSObject();
    +            Object.getOwnPropertyNames(dictionaryObject).forEach((element) => {
    +                this._parseObjectByType(dictionaryObject[element]);
    +            });
    +            break;
    +        case hummus.ePDFObjectStream:
    +            dictionary = inObject.toPDFStream().getDictionary();
    +            return this._parseObjectByType(dictionary);
    +        default:
    +            saveToObject[`${label}-${Date.now()*Math.random()}`] = inObject;
    +            return inObject;
    +    }
    +};
     
    @@ -374,7 +374,7 @@

    info.js


    - Documentation generated by JSDoc 3.6.3 on Wed Oct 02 2019 12:00:26 GMT-0700 (Pacific Daylight Time) using the docdash theme. + Documentation generated by JSDoc 3.5.5 on Fri Nov 01 2019 20:41:12 GMT-0400 (Eastern Daylight Time) using the docdash theme.
    diff --git a/docs/insertPage.js.html b/docs/insertPage.js.html index 4e59ada..66663d3 100644 --- a/docs/insertPage.js.html +++ b/docs/insertPage.js.html @@ -37,83 +37,83 @@

    insertPage.js

    -
    const hummus = require('hummus');
    -const fs = require('fs');
    -const hummusUtils = require('./utils');
    -/**
    - * Insert a page from the other pdf
    - * @name insertPage
    - * @function
    - * @memberof Recipe
    - * @param {number} afterPageNumber - The page number for insertion.
    - * @param {string} pdfSrc - The path for the other pdf
    - * @param {number} srcPageNumber - The page number to be insterted from the other pdf.
    - */
    -exports.insertPage = function insertPage(afterPageNumber, pdfSrc, srcPageNumber) {
    -    if (isNaN(afterPageNumber)) {
    -        throw new Error('The afterPageNumber is inValid.');
    -    }
    -    this.needToInsertPages = true;
    -    if (pdfSrc && srcPageNumber) {
    -        this.insertInformation = this.insertInformation || {};
    -        this.insertInformation[afterPageNumber] = this.insertInformation[afterPageNumber] || [];
    -        this.insertInformation[afterPageNumber].push({
    -            afterPageNumber,
    -            pdfSrc,
    -            srcPageNumber
    -        });
    -    }
    -    return this;
    -};
    -
    -exports._insertPages = function _insertPages() {
    -    if (!this.insertInformation) {
    -        throw new Error('No insertInformation');
    -    }
    -    const pagesForInsert = [0, ...Object.keys(this.metadata)
    -        .filter(item => !isNaN(item))
    -        .map(item => parseInt(item))
    -    ];
    -    const unlinkList = [];
    -    const tmp = this.output + '.tmp.pdf';
    -    unlinkList.push(tmp);
    -    fs.renameSync(this.output, tmp);
    -
    -    const pdfWriter = hummus.createWriter(this.output);
    -    let lastInsertedOriginal = 0;
    -    pagesForInsert.forEach((pageNumber) => {
    -        const toAppendPage = pageNumber - 1;
    -        if (toAppendPage >= 0) {
    -            const specificRanges = [
    -                [lastInsertedOriginal, toAppendPage]
    -            ];
    -            hummusUtils.appendPDFPagesFromPDFWithAnnotations(pdfWriter, tmp, {
    -                specificRanges
    -            });
    -        }
    -        lastInsertedOriginal = pageNumber;
    -
    -        const toInserts = this.insertInformation[pageNumber];
    -        if (toInserts) {
    -            toInserts.forEach((info) => {
    -                const specificRanges = [
    -                    [info.srcPageNumber - 1, info.srcPageNumber - 1]
    -                ];
    -                hummusUtils.appendPDFPagesFromPDFWithAnnotations(pdfWriter, info.pdfSrc, { specificRanges });
    -            });
    -        }
    -    });
    -    pdfWriter.end();
    -
    -    unlinkList.forEach((item) => {
    -        setTimeout(() => {
    -            if (fs.existsSync(item)) {
    -                fs.unlinkSync(item);
    -            }
    -        });
    -    });
    -    return this;
    -};
    +            
    const hummus = require('hummus');
    +const fs = require('fs');
    +const hummusUtils = require('./utils');
    +/**
    + * Insert a page from the other pdf
    + * @name insertPage
    + * @function
    + * @memberof Recipe
    + * @param {number} afterPageNumber - The page number for insertion.
    + * @param {string} pdfSrc - The path for the other pdf
    + * @param {number} srcPageNumber - The page number to be insterted from the other pdf.
    + */
    +exports.insertPage = function insertPage(afterPageNumber, pdfSrc, srcPageNumber) {
    +    if (isNaN(afterPageNumber)) {
    +        throw new Error('The afterPageNumber is inValid.');
    +    }
    +    this.needToInsertPages = true;
    +    if (pdfSrc && srcPageNumber) {
    +        this.insertInformation = this.insertInformation || {};
    +        this.insertInformation[afterPageNumber] = this.insertInformation[afterPageNumber] || [];
    +        this.insertInformation[afterPageNumber].push({
    +            afterPageNumber,
    +            pdfSrc,
    +            srcPageNumber
    +        });
    +    }
    +    return this;
    +};
    +
    +exports._insertPages = function _insertPages() {
    +    if (!this.insertInformation) {
    +        throw new Error('No insertInformation');
    +    }
    +    const pagesForInsert = [0, ...Object.keys(this.metadata)
    +        .filter(item => !isNaN(item))
    +        .map(item => parseInt(item))
    +    ];
    +    const unlinkList = [];
    +    const tmp = this.output + '.tmp.pdf';
    +    unlinkList.push(tmp);
    +    fs.renameSync(this.output, tmp);
    +
    +    const pdfWriter = hummus.createWriter(this.output);
    +    let lastInsertedOriginal = 0;
    +    pagesForInsert.forEach((pageNumber) => {
    +        const toAppendPage = pageNumber - 1;
    +        if (toAppendPage >= 0) {
    +            const specificRanges = [
    +                [lastInsertedOriginal, toAppendPage]
    +            ];
    +            hummusUtils.appendPDFPagesFromPDFWithAnnotations(pdfWriter, tmp, {
    +                specificRanges
    +            });
    +        }
    +        lastInsertedOriginal = pageNumber;
    +
    +        const toInserts = this.insertInformation[pageNumber];
    +        if (toInserts) {
    +            toInserts.forEach((info) => {
    +                const specificRanges = [
    +                    [info.srcPageNumber - 1, info.srcPageNumber - 1]
    +                ];
    +                hummusUtils.appendPDFPagesFromPDFWithAnnotations(pdfWriter, info.pdfSrc, { specificRanges });
    +            });
    +        }
    +    });
    +    pdfWriter.end();
    +
    +    unlinkList.forEach((item) => {
    +        setTimeout(() => {
    +            if (fs.existsSync(item)) {
    +                fs.unlinkSync(item);
    +            }
    +        });
    +    });
    +    return this;
    +};
     
    @@ -126,7 +126,7 @@

    insertPage.js


    - Documentation generated by JSDoc 3.6.3 on Wed Oct 02 2019 12:00:26 GMT-0700 (Pacific Daylight Time) using the docdash theme. + Documentation generated by JSDoc 3.5.5 on Fri Nov 01 2019 20:41:12 GMT-0400 (Eastern Daylight Time) using the docdash theme.
    diff --git a/docs/overlay.js.html b/docs/overlay.js.html index 81cdbb0..1027488 100644 --- a/docs/overlay.js.html +++ b/docs/overlay.js.html @@ -37,78 +37,78 @@

    overlay.js

    -
    const xObjectForm = require('./xObjectForm');
    -
    -/**
    - * Overlay a pdf to the current pdf
    - * @name overlay
    - * @function
    - * @memberof Recipe
    - * @param {string} pdfSrc - The path for the overlay pdf
    - * @param {number} x - The coordinate x
    - * @param {number} y - The coordinate y
    - * @param {number} [options.scale] - Scale the overlay pdf, default is 1
    - * @param {boolean} [options.keepAspectRatio] - To keep the aspect ratio when scaling, default is true
    - * @param {boolean} [options.fitWidth] - To set the width to 100% (use with keepAspectRatio=true)
    - * @param {boolean} [options.fitHeight] - To set the height to 100% (use with keepAspectRatio=true)
    - */
    -exports.overlay = function overlay(pdfSrc, x = 0, y = 0, options = {}) {
    -    // allow to have only 2 arguments input
    -    if (arguments.length == 2) {
    -        options = x || {};
    -        x = 0;
    -        y = 0;
    -    }
    -    // const pathOptions = this._getPathOptions(options);
    -    // const gsId = this._getPathOptions(options).fillGsId;
    -
    -    const { keepAspectRatio, fitWidth, fitHeight } = options;
    -    const scale = options.scale || 1;
    -
    -    const { width: pageWidth, height: pageHeight } = this.metadata[this.pageNumber];
    -
    -    const inMetadata = this.read(pdfSrc);
    -    const page = options.page && options.page <= inMetadata.pages ? options.page : 1;
    -    // Overlays page based on page number given in options, Useful for overlaying template PDF documents
    -    const { width, height } = inMetadata[page];
    -
    -    this.pauseContext();
    -    const xObject = new xObjectForm(this.writer, width, height);
    -    xObject.getContentContext()
    -        .q()
    -        .drawImage(0, 0, pdfSrc, { index: page-1 })
    -        .Q();
    -    xObject.end();
    -    this.resumeContext();
    -
    -    const context = this.pageContext;
    -    let scaleX = 1 * scale;
    -    let scaleY = 1 * scale;
    -
    -    if (fitWidth) {
    -        scaleX = pageWidth / width;
    -        if (keepAspectRatio) {
    -            scaleY = scaleX;
    -        }
    -    }
    -    if (fitHeight) {
    -        scaleY = pageHeight / height;
    -        if (keepAspectRatio) {
    -            scaleX = scaleY;
    -        }
    -    }
    -
    -    const posX = x;
    -    const posY = pageHeight - height * scaleY - y;
    -
    -    context
    -        .q()
    -        .cm(scaleX, 0, 0, scaleY, posX, posY)
    -        .doXObject(xObject)
    -        .Q();
    -
    -    return this;
    -};
    +            
    const xObjectForm = require('./xObjectForm');
    +
    +/**
    + * Overlay a pdf to the current pdf
    + * @name overlay
    + * @function
    + * @memberof Recipe
    + * @param {string} pdfSrc - The path for the overlay pdf
    + * @param {number} x - The coordinate x
    + * @param {number} y - The coordinate y
    + * @param {number} [options.scale] - Scale the overlay pdf, default is 1
    + * @param {boolean} [options.keepAspectRatio] - To keep the aspect ratio when scaling, default is true
    + * @param {boolean} [options.fitWidth] - To set the width to 100% (use with keepAspectRatio=true)
    + * @param {boolean} [options.fitHeight] - To set the height to 100% (use with keepAspectRatio=true)
    + */
    +exports.overlay = function overlay(pdfSrc, x = 0, y = 0, options = {}) {
    +    // allow to have only 2 arguments input
    +    if (arguments.length == 2) {
    +        options = x || {};
    +        x = 0;
    +        y = 0;
    +    }
    +    // const pathOptions = this._getPathOptions(options);
    +    // const gsId = this._getPathOptions(options).fillGsId;
    +
    +    const { keepAspectRatio, fitWidth, fitHeight } = options;
    +    const scale = options.scale || 1;
    +
    +    const { width: pageWidth, height: pageHeight } = this.metadata[this.pageNumber];
    +
    +    const inMetadata = this.read(pdfSrc);
    +    const page = options.page && options.page <= inMetadata.pages ? options.page : 1;
    +    // Overlays page based on page number given in options, Useful for overlaying template PDF documents
    +    const { width, height } = inMetadata[page];
    +
    +    this.pauseContext();
    +    const xObject = new xObjectForm(this.writer, width, height);
    +    xObject.getContentContext()
    +        .q()
    +        .drawImage(0, 0, pdfSrc, { index: page-1 })
    +        .Q();
    +    xObject.end();
    +    this.resumeContext();
    +
    +    const context = this.pageContext;
    +    let scaleX = 1 * scale;
    +    let scaleY = 1 * scale;
    +
    +    if (fitWidth) {
    +        scaleX = pageWidth / width;
    +        if (keepAspectRatio) {
    +            scaleY = scaleX;
    +        }
    +    }
    +    if (fitHeight) {
    +        scaleY = pageHeight / height;
    +        if (keepAspectRatio) {
    +            scaleX = scaleY;
    +        }
    +    }
    +
    +    const posX = x;
    +    const posY = pageHeight - height * scaleY - y;
    +
    +    context
    +        .q()
    +        .cm(scaleX, 0, 0, scaleY, posX, posY)
    +        .doXObject(xObject)
    +        .Q();
    +
    +    return this;
    +};
     
    @@ -121,7 +121,7 @@

    overlay.js


    - Documentation generated by JSDoc 3.6.3 on Wed Oct 02 2019 12:00:26 GMT-0700 (Pacific Daylight Time) using the docdash theme. + Documentation generated by JSDoc 3.5.5 on Fri Nov 01 2019 20:41:12 GMT-0400 (Eastern Daylight Time) using the docdash theme.
    diff --git a/docs/page.js.html b/docs/page.js.html index 7fb9f3d..2bfabd4 100644 --- a/docs/page.js.html +++ b/docs/page.js.html @@ -37,205 +37,205 @@

    page.js

    -
    const hummus = require('hummus');
    -
    -/**
    - * Create a new page
    - * @name createPage
    - * @function
    - * @memberof Recipe
    - * @param {number} pageWidth - The page width.
    - * @param {number} pageHeight - The page height.
    - */
    -exports.createPage = function createPage(pageWidth, pageHeight) {
    -    if (!pageWidth && !pageHeight) {
    -        pageWidth = pageWidth || this.default.pageWidth;
    -        pageHeight = pageHeight || this.default.pageHeight;
    -    } else
    -    if (pageWidth && !isNaN(pageWidth) && pageHeight && !isNaN(pageHeight)) {
    -        pageWidth = pageWidth || this.default.pageWidth;
    -        pageHeight = pageHeight || this.default.pageHeight;
    -    } else
    -    if (pageWidth && typeof (pageWidth) == 'string') {
    -        // const type = pageWidth;
    -        const rotate = pageHeight;
    -        let pageType = this.default.paperSizeTypes[pageWidth];
    -
    -        if (pageType) {
    -            pageWidth = pageType.pageWidth;
    -            pageHeight = pageType.pageHeight;
    -        } else {
    -            // use default
    -            pageWidth = this.default.pageWidth;
    -            pageHeight = this.default.pageHeight;
    -        }
    -        if (rotate && !isNaN(rotate)) {
    -            if (rotate % 180 != 0) {
    -                let temp = pageHeight;
    -                pageHeight = pageWidth;
    -                pageWidth = temp;
    -            }
    -        }
    -    }
    -    // from 0
    -    this.metadata.pageCount += 1;
    -    const pageNumber = this.metadata.pageCount;
    -    const dimensions = [0, 0, pageWidth, pageHeight];
    -    const layout = (pageWidth > pageHeight) ? 'landscape' : 'portrait';
    -    this.metadata[pageNumber] = {
    -        pageNumber,
    -        mediaBox: dimensions,
    -        layout,
    -        rotate: 0,
    -        width: pageWidth,
    -        height: pageHeight
    -    };
    -
    -    const page = this.writer.createPage();
    -    page.mediaBox = [0, 0, pageWidth, pageHeight];
    -
    -    this.page = page;
    -    this.pageNumber = pageNumber;
    -    this.pageContext = this.writer.startPageContentContext(this.page);
    -    this.editingPage = false;
    -
    -    this.moveTo(0, 0);
    -    return this;
    -};
    -
    -/**
    - * Finish a page
    - * @name endPage
    - * @function
    - * @memberof Recipe
    - */
    -exports.endPage = function endPage() {
    -    if (!this.page) {
    -        return this;
    -    }
    -
    -    if (this.page.endContext) {
    -        this.page.endContext();
    -        this.page.writePage();
    -    } else {
    -        this.writer.writePage(this.page);
    -    }
    -    // this.page = null;
    -    // this.pageContext = null;
    -    // this.pageNumber = 0;
    -
    -    return this;
    -};
    -
    -/**
    - * Start editing a page
    - * @name editPage
    - * @function
    - * @memberof Recipe
    - * @param {number} pageNumber - The page number to be edited.
    - */
    -exports.editPage = function editPage(pageNumber) {
    -    const pdfWriter = this.writer;
    -    const pageIndex = pageNumber - 1;
    -    const pageModifier = new hummus.PDFPageModifier(pdfWriter, pageIndex, true);
    -    this.page = pageModifier;
    -    this.pageNumber = pageNumber;
    -    this.pageContext = pageModifier.startContext().getContext();
    -    this.editingPage = true;
    -
    -    this._resumePageRotation(pageNumber);
    -
    -    if (this.debug) {
    -        const context = this.pageContext;
    -        const {
    -            width,
    -            height,
    -            mediaBox
    -        } = this.metadata[pageNumber];
    -        const startX = mediaBox[0];
    -        const startY = mediaBox[1];
    -        const textOptions = {
    -            font: this.writer.getFontForFile(this.fonts['helvetica-bold']),
    -            size: 50,
    -            colorspace: 'gray',
    -            color: 0x00
    -        };
    -        context.writeText(`[${startX}, ${startY}] is HERE`, startX, startY, textOptions);
    -        context.writeText(`[${startX}, width/2] is HERE`, startX, width / 2, textOptions);
    -        context.writeText(`[${startX}, height/2] is HERE`, startX, height / 2, textOptions);
    -        context.writeText(`[width/2, ${startY}] is HERE`, width / 2, startY, textOptions);
    -        context.writeText(`[height/2, ${startY}] is HERE`, height / 2, startY, textOptions);
    -    }
    -    return this;
    -};
    -
    -exports._resumePageRotation = function _resumePageRotation(pageNumber, context) {
    -    pageNumber = pageNumber || this.pageNumber;
    -    const {
    -        // layout,
    -        rotate,
    -        width,
    -        height,
    -        mediaBox
    -    } = this.metadata[pageNumber];
    -    context = context || this.pageContext;
    -    const startX = mediaBox[0];
    -    const startY = mediaBox[1];
    -
    -    switch (rotate) {
    -        case 90:
    -            context.cm(0, 1, -1, 0, height - startX, startY);
    -            break;
    -        case 180:
    -            context.cm(-1, 0, 0, -1, width, height);
    -            break;
    -        case 270:
    -            context.cm(0, -1, 1, 0, startX, width - startY);
    -            break;
    -
    -        default:
    -    }
    -    return this;
    -};
    -
    -/**
    - * Get page information
    - * @name pageInfo
    - * @function
    - * @memberof Recipe
    - * @param {number} pageNumber - The page number.
    - */
    -exports.pageInfo = function pageInfo(pageNumber) {
    -    const pageInfo = this.metadata[pageNumber];
    -    return {
    -        width: pageInfo.width,
    -        height: pageInfo.height,
    -        rotate: pageInfo.rotate,
    -        pageNumber
    -    };
    -};
    -
    -exports.pauseContext = function pauseContext() {
    -    if (this.page && this.page.endContext) {
    -        this.page.endContext();
    -        // this.writer.pausePageContentContext(this.pageContext);
    -    } else
    -    if (this.pageContext) {
    -        this.writer.pausePageContentContext(this.pageContext);
    -    }
    -};
    -
    -exports.resumeContext = function resumeContext() {
    -    if (!this.isNewPDF && this.page) {
    -        this.pageContext = this.page.startContext().getContext();
    -        this._resumePageRotation();
    -    }
    -};
    -
    -exports.getPageInfo = function getPageInfo() {
    -    const info = this.writer.getDocumentContext().getInfoDictionary();
    -    return info;
    -};
    +            
    const hummus = require('hummus');
    +
    +/**
    + * Create a new page
    + * @name createPage
    + * @function
    + * @memberof Recipe
    + * @param {number} pageWidth - The page width.
    + * @param {number} pageHeight - The page height.
    + */
    +exports.createPage = function createPage(pageWidth, pageHeight) {
    +    if (!pageWidth && !pageHeight) {
    +        pageWidth = pageWidth || this.default.pageWidth;
    +        pageHeight = pageHeight || this.default.pageHeight;
    +    } else
    +    if (pageWidth && !isNaN(pageWidth) && pageHeight && !isNaN(pageHeight)) {
    +        pageWidth = pageWidth || this.default.pageWidth;
    +        pageHeight = pageHeight || this.default.pageHeight;
    +    } else
    +    if (pageWidth && typeof (pageWidth) == 'string') {
    +        // const type = pageWidth;
    +        const rotate = pageHeight;
    +        let pageType = this.default.paperSizeTypes[pageWidth];
    +
    +        if (pageType) {
    +            pageWidth = pageType.pageWidth;
    +            pageHeight = pageType.pageHeight;
    +        } else {
    +            // use default
    +            pageWidth = this.default.pageWidth;
    +            pageHeight = this.default.pageHeight;
    +        }
    +        if (rotate && !isNaN(rotate)) {
    +            if (rotate % 180 != 0) {
    +                let temp = pageHeight;
    +                pageHeight = pageWidth;
    +                pageWidth = temp;
    +            }
    +        }
    +    }
    +    // from 0
    +    this.metadata.pageCount += 1;
    +    const pageNumber = this.metadata.pageCount;
    +    const dimensions = [0, 0, pageWidth, pageHeight];
    +    const layout = (pageWidth > pageHeight) ? 'landscape' : 'portrait';
    +    this.metadata[pageNumber] = {
    +        pageNumber,
    +        mediaBox: dimensions,
    +        layout,
    +        rotate: 0,
    +        width: pageWidth,
    +        height: pageHeight
    +    };
    +
    +    const page = this.writer.createPage();
    +    page.mediaBox = [0, 0, pageWidth, pageHeight];
    +
    +    this.page = page;
    +    this.pageNumber = pageNumber;
    +    this.pageContext = this.writer.startPageContentContext(this.page);
    +    this.editingPage = false;
    +
    +    this.moveTo(0, 0);
    +    return this;
    +};
    +
    +/**
    + * Finish a page
    + * @name endPage
    + * @function
    + * @memberof Recipe
    + */
    +exports.endPage = function endPage() {
    +    if (!this.page) {
    +        return this;
    +    }
    +
    +    if (this.page.endContext) {
    +        this.page.endContext();
    +        this.page.writePage();
    +    } else {
    +        this.writer.writePage(this.page);
    +    }
    +    // this.page = null;
    +    // this.pageContext = null;
    +    // this.pageNumber = 0;
    +
    +    return this;
    +};
    +
    +/**
    + * Start editing a page
    + * @name editPage
    + * @function
    + * @memberof Recipe
    + * @param {number} pageNumber - The page number to be edited.
    + */
    +exports.editPage = function editPage(pageNumber) {
    +    const pdfWriter = this.writer;
    +    const pageIndex = pageNumber - 1;
    +    const pageModifier = new hummus.PDFPageModifier(pdfWriter, pageIndex, true);
    +    this.page = pageModifier;
    +    this.pageNumber = pageNumber;
    +    this.pageContext = pageModifier.startContext().getContext();
    +    this.editingPage = true;
    +
    +    this._resumePageRotation(pageNumber);
    +
    +    if (this.debug) {
    +        const context = this.pageContext;
    +        const {
    +            width,
    +            height,
    +            mediaBox
    +        } = this.metadata[pageNumber];
    +        const startX = mediaBox[0];
    +        const startY = mediaBox[1];
    +        const textOptions = {
    +            font: this.writer.getFontForFile(this.fonts['helvetica-bold']),
    +            size: 50,
    +            colorspace: 'gray',
    +            color: 0x00
    +        };
    +        context.writeText(`[${startX}, ${startY}] is HERE`, startX, startY, textOptions);
    +        context.writeText(`[${startX}, width/2] is HERE`, startX, width / 2, textOptions);
    +        context.writeText(`[${startX}, height/2] is HERE`, startX, height / 2, textOptions);
    +        context.writeText(`[width/2, ${startY}] is HERE`, width / 2, startY, textOptions);
    +        context.writeText(`[height/2, ${startY}] is HERE`, height / 2, startY, textOptions);
    +    }
    +    return this;
    +};
    +
    +exports._resumePageRotation = function _resumePageRotation(pageNumber, context) {
    +    pageNumber = pageNumber || this.pageNumber;
    +    const {
    +        // layout,
    +        rotate,
    +        width,
    +        height,
    +        mediaBox
    +    } = this.metadata[pageNumber];
    +    context = context || this.pageContext;
    +    const startX = mediaBox[0];
    +    const startY = mediaBox[1];
    +
    +    switch (rotate) {
    +        case 90:
    +            context.cm(0, 1, -1, 0, height - startX, startY);
    +            break;
    +        case 180:
    +            context.cm(-1, 0, 0, -1, width, height);
    +            break;
    +        case 270:
    +            context.cm(0, -1, 1, 0, startX, width - startY);
    +            break;
    +
    +        default:
    +    }
    +    return this;
    +};
    +
    +/**
    + * Get page information
    + * @name pageInfo
    + * @function
    + * @memberof Recipe
    + * @param {number} pageNumber - The page number.
    + */
    +exports.pageInfo = function pageInfo(pageNumber) {
    +    const pageInfo = this.metadata[pageNumber];
    +    return {
    +        width: pageInfo.width,
    +        height: pageInfo.height,
    +        rotate: pageInfo.rotate,
    +        pageNumber
    +    };
    +};
    +
    +exports.pauseContext = function pauseContext() {
    +    if (this.page && this.page.endContext) {
    +        this.page.endContext();
    +        // this.writer.pausePageContentContext(this.pageContext);
    +    } else
    +    if (this.pageContext) {
    +        this.writer.pausePageContentContext(this.pageContext);
    +    }
    +};
    +
    +exports.resumeContext = function resumeContext() {
    +    if (!this.isNewPDF && this.page) {
    +        this.pageContext = this.page.startContext().getContext();
    +        this._resumePageRotation();
    +    }
    +};
    +
    +exports.getPageInfo = function getPageInfo() {
    +    const info = this.writer.getDocumentContext().getInfoDictionary();
    +    return info;
    +};
     
    @@ -248,7 +248,7 @@

    page.js


    - Documentation generated by JSDoc 3.6.3 on Wed Oct 02 2019 12:00:26 GMT-0700 (Pacific Daylight Time) using the docdash theme. + Documentation generated by JSDoc 3.5.5 on Fri Nov 01 2019 20:41:12 GMT-0400 (Eastern Daylight Time) using the docdash theme.
    diff --git a/docs/split.js.html b/docs/split.js.html index 5c20fe9..6e56fdd 100644 --- a/docs/split.js.html +++ b/docs/split.js.html @@ -37,28 +37,28 @@

    split.js

    -
    const hummus = require('hummus');
    -const path = require('path');
    -const hummusUtils = require('./utils');
    -
    -/**
    - * Split the pdf
    - * @name split
    - * @function
    - * @memberof Recipe
    - * @param {string} outputDir - The path for the output pdfs.
    - * @param {string} prefix - `${prefix}-${i+1}.pdf`.
    - */
    -exports.split = function split(outputDir = '', prefix) {
    -    prefix = prefix || this.filename;
    -    for (let i = 0; i < this.metadata.pages; i++) {
    -        const newPdf = path.join(outputDir, `${prefix}-${i+1}.pdf`);
    -        const pdfWriter = hummus.createWriter(newPdf);
    -        hummusUtils.appendPDFPageFromPDFWithAnnotations(pdfWriter, this.pdfReader, i);
    -        pdfWriter.end();
    -    }
    -    return this;
    -};
    +            
    const hummus = require('hummus');
    +const path = require('path');
    +const hummusUtils = require('./utils');
    +
    +/**
    + * Split the pdf
    + * @name split
    + * @function
    + * @memberof Recipe
    + * @param {string} outputDir - The path for the output pdfs.
    + * @param {string} prefix - `${prefix}-${i+1}.pdf`.
    + */
    +exports.split = function split(outputDir = '', prefix) {
    +    prefix = prefix || this.filename;
    +    for (let i = 0; i < this.metadata.pages; i++) {
    +        const newPdf = path.join(outputDir, `${prefix}-${i+1}.pdf`);
    +        const pdfWriter = hummus.createWriter(newPdf);
    +        hummusUtils.appendPDFPageFromPDFWithAnnotations(pdfWriter, this.pdfReader, i);
    +        pdfWriter.end();
    +    }
    +    return this;
    +};
     
    @@ -71,7 +71,7 @@

    split.js


    - Documentation generated by JSDoc 3.6.3 on Wed Oct 02 2019 12:00:26 GMT-0700 (Pacific Daylight Time) using the docdash theme. + Documentation generated by JSDoc 3.5.5 on Fri Nov 01 2019 20:41:12 GMT-0400 (Eastern Daylight Time) using the docdash theme.
    diff --git a/docs/text.helper.js.html b/docs/text.helper.js.html index 97fdadb..f93cdb2 100644 --- a/docs/text.helper.js.html +++ b/docs/text.helper.js.html @@ -37,177 +37,144 @@

    text.helper.js

    -
    exports.Word = class Word {
    -    constructor(word, pathOptions) {
    -        this._value = word;
    -        this._pathOptions = pathOptions;
    -    }
    -
    -    get value() {
    -        return this._value;
    -    }
    -
    -    get dimensions() {
    -        if (this._dimensions) {
    -            return this._dimensions;
    -        }
    -        this._dimensions = this._pathOptions.font.calculateTextDimensions(
    -            this._value, this._pathOptions.size
    -        );
    -        return this._dimensions;
    -    }
    -};
    -
    -exports.Line = class Line {
    -    constructor(width, height, size, pathOptions) {
    -        this._width = width || 999999999;
    -        this._height = height;
    -        this._pathOptions = pathOptions;
    -        this.size = size || pathOptions.size;
    -        this.wordObjects = [];
    -    }
    -
    -    addWord(wordObject) {
    -        this.wordObjects.push(wordObject);
    -    }
    -
    -    canFit(wordObject) {
    -        const tempValue = this.value + wordObject.value;
    -        const toWidth = this._pathOptions.font.calculateTextDimensions(
    -            tempValue, this.size
    -        ).width;
    -        return (toWidth <= this.width);
    -    }
    -
    -    replaceLastWord(wordObject) {
    -        this.wordObjects.pop();
    -        this.addWord(wordObject);
    -    }
    -
    -    get spaceWidth() {
    -        return this._pathOptions.font.calculateTextDimensions(
    -            'o', this.size
    -        ).width;
    -    }
    -
    -    get value() {
    -        const value = this.wordObjects.reduce((string, word) => {
    -            string += word.value;
    -            return string;
    -        }, '');
    -        return value;
    -    }
    -
    -    get currentWidth() {
    -        return this._pathOptions.font.calculateTextDimensions(
    -            this.value, this.size
    -        ).xMax;
    -    }
    -
    -    get textWidth() {
    -        return this.wordObjects.reduce((width, word) => {
    -            width += word.dimensions.width;
    -            return width;
    -        }, 0);
    -    }
    -
    -    get width() {
    -        return this._width;
    -    }
    -
    -    // dynamic adjust height based on word height?
    -    get height() {
    -        if (this._height) {
    -            return this._height;
    -        }
    -        const toHeight = this._pathOptions.font.calculateTextDimensions(
    -            this.value, this.size
    -        ).height; // ymax
    -        return toHeight + 20;
    -    }
    -};
    -
    -exports._getTextOffset = function _getTextOffset(text = '', options = {}) {
    -    if (!options.size || !options.font) {
    -        return;
    -    }
    -    let offsetX = 0;
    -    let offsetY = 0;
    -    const textDimensions = options.font.calculateTextDimensions(text, options.size);
    -    if (options.align) {
    -        const alignments = options.align.split(' ');
    -        if (alignments[0]) {
    -            switch (alignments[0]) {
    -                case 'center':
    -                    offsetX = -1 * textDimensions.width / 2;
    -                    break;
    -                case 'right':
    -                    offsetX = textDimensions.width / 2;
    -                    break;
    -                default:
    -            }
    -        }
    -        if (alignments[1]) {
    -            switch (alignments[1]) {
    -                case 'center':
    -                    offsetY = -1 * textDimensions.yMax / 2;
    -                    break;
    -                case 'bottom':
    -                    offsetY = textDimensions.yMax / 2;
    -                    break;
    -                default:
    -            }
    -        }
    -    }
    -    return {
    -        offsetX,
    -        offsetY
    -    };
    -};
    -
    -/**
    - * @todo handle page margin and padding
    - */
    -exports._getTextBoxOffset = function _getTextBoxOffset(textBox, options = {}) {
    -    let offsetX = 0;
    -    let offsetY = -textBox.firstLineHeight;
    -    const {
    -        width,
    -        height
    -    } = textBox;
    -    if (options.align) {
    -        const alignments = options.align.split(' ');
    -        if (alignments[0]) {
    -            switch (alignments[0]) {
    -                case 'center':
    -                    offsetX = -1 * width / 2;
    -                    break;
    -                case 'right':
    -                    offsetX = -width;
    -                    break;
    -                default:
    -            }
    -        }
    -        if (alignments[1]) {
    -            switch (alignments[1]) {
    -                case 'center':
    -                    offsetY = (textBox.isSimpleText) ?
    -                        -textBox.firstLineHeight / 2 :
    -                        height / 2 + offsetY;
    -                    break;
    -                case 'bottom':
    -                    offsetY = height + offsetY;
    -                    break;
    -                default:
    -            }
    -        }
    -    }
    -
    -    return {
    -        offsetX,
    -        offsetY
    -    };
    -};
    +            
    exports.Word = class Word {
    +    constructor(word, pathOptions) {
    +        this._value = word;
    +        this._pathOptions = pathOptions;
    +        this._text = (word === ' ') ? 'o' : word;  // allows space to get an actual dimension
    +    }
    +
    +    get value() {
    +        return this._value;
    +    }
    +
    +    get dimensions() {
    +        if (this._dimensions) {
    +            return this._dimensions;
    +        }
    +        this._dimensions = this._pathOptions.font.calculateTextDimensions(
    +            this._text, this._pathOptions.size
    +        );
    +        return this._dimensions;
    +    }
    +};
    +
    +exports.Line = class Line {
    +    constructor(width, height, size, pathOptions) {
    +        this._width = width || 999999999;
    +        this._height = height;
    +        this._pathOptions = pathOptions;
    +        this.size = size || pathOptions.size;
    +        this.wordObjects = [];
    +    }
    +
    +    addWord(wordObject) {
    +        this.wordObjects.push(wordObject);
    +    }
    +
    +    canFit(wordObject) {
    +        const tempValue = this.value + wordObject.value;
    +        const toWidth = this._pathOptions.font.calculateTextDimensions(
    +            tempValue, this.size
    +        ).xMax;
    +        return (toWidth <= this.width);
    +    }
    +
    +    replaceLastWord(wordObject) {
    +        this.wordObjects.pop();
    +        this.addWord(wordObject);
    +    }
    +
    +    get words() {
    +        return this.wordObjects;
    +    }
    +
    +    get spaceWidth() {
    +        return this._pathOptions.font.calculateTextDimensions(
    +            'o', this.size
    +        ).width;
    +    }
    +
    +    get value() {
    +        const value = this.wordObjects.reduce((string, word) => {
    +            string += word.value;
    +            return string;
    +        }, '');
    +        return value;
    +    }
    +
    +    get currentWidth() {
    +        return this._pathOptions.font.calculateTextDimensions(
    +            this.value, this.size
    +        ).xMax;
    +    }
    +
    +    get textWidth() {
    +        return this.wordObjects.reduce((width, word) => {
    +            width += word.dimensions.xMax;
    +            return width;
    +        }, 0);
    +    }
    +
    +    get width() {
    +        return this._width;
    +    }
    +
    +    // dynamic adjust height based on word height?
    +    get height() {
    +        if (this._height) {
    +            return this._height;
    +        }
    +        const toHeight = this._pathOptions.font.calculateTextDimensions(
    +            this.value, this.size
    +        ).height; // ymax
    +        return toHeight + 20;
    +    }
    +};
    +
    +/**
    + * @todo handle page margin and padding
    + */
    +exports._getTextBoxOffset = function _getTextBoxOffset(textBox, options = {}) {
    +    let offsetX = 0;
    +    let offsetY = -textBox.firstLineHeight;
    +    const {
    +        width,
    +        height
    +    } = textBox;
    +    if (options.align) {
    +        const alignments = options.align.split(' ');
    +        if (alignments[0]) {
    +            switch (alignments[0]) {
    +                case 'center':
    +                    offsetX = -1 * width / 2;
    +                    break;
    +                case 'right':
    +                    offsetX = -width;
    +                    break;
    +                default:
    +            }
    +        }
    +        if (alignments[1]) {
    +            switch (alignments[1]) {
    +                case 'center':
    +                    offsetY = (textBox.isSimpleText) ?
    +                        -textBox.firstLineHeight / 2 :
    +                        height / 2 + offsetY;
    +                    break;
    +                case 'bottom':
    +                    offsetY = height + offsetY;
    +                    break;
    +                default:
    +            }
    +        }
    +    }
    +
    +    return {
    +        offsetX,
    +        offsetY
    +    };
    +};
     
    @@ -220,7 +187,7 @@

    text.helper.js


    - Documentation generated by JSDoc 3.6.3 on Wed Oct 02 2019 12:00:26 GMT-0700 (Pacific Daylight Time) using the docdash theme. + Documentation generated by JSDoc 3.5.5 on Fri Nov 01 2019 20:41:12 GMT-0400 (Eastern Daylight Time) using the docdash theme.
    diff --git a/docs/text.js.html b/docs/text.js.html index 946c0b4..33bcc3e 100644 --- a/docs/text.js.html +++ b/docs/text.js.html @@ -37,463 +37,627 @@

    text.js

    -
    const LineBreaker = require('linebreak');
    -const {
    -    Word,
    -    Line
    -} = require('./text.helper');
    -const {
    -    htmlToTextObjects
    -} = require('./htmlToTextObjects');
    -const xObjectForm = require('./xObjectForm');
    -
    -//  Table indicating how to specify coloration of elements
    -//  -------------------------------------------------------------------
    -// |Color | HexColor   | DecimalColor                   | PercentColor |
    -// |Space | (string)   | (array)                        | (string)     |
    -// |------+------------+--------------------------------+--------------|
    -// | Gray | #GG        | [gray]                         | %G           |
    -// |  RGB | #rrggbb    | [red, green, blue]             | %G           |
    -// | CMYK | #ccmmyykk  | [cyan, magenta, yellow, black] | %c,m,y,k     |
    -//  -------------------------------------------------------------------
    -//
    -//   HexColor component values (two hex digits) range from 00 to FF.
    -//   DecimalColor component values range from 0 to 255.
    -//   PercentColor component values range from 1 to 100.
    -
    -/**
    - * Write text elements
    - * @name text
    - * @function
    - * @todo support break words
    - * @memberof Recipe
    - * @param {string} text - The text content
    - * @param {number} x - The coordinate x
    - * @param {number} y - The coordinate y
    - * @param {Object} [options] - The options
    - * @param {string|number[]} [options.color] - Text color (HexColor, PercentColor or DecimalColor)
    - * @param {number} [options.opacity] - opacity
    - * @param {number} [options.rotation] - Accept: +/- 0 through 360. Default: 0
    - * @param {number[]} [options.rotationOrigin] - [originX, originY] Default: the text coordinate
    - * @param {string} [options.font] - The font. 'Arial', 'Helvetica'...
    - * @param {number} [options.size] - The line width
    - * @param {string} [options.align] - The alignment. 'center center'...
    - * @param {Object|Boolean} [options.highlight] - Text markup annotation.
    - * @param {Object|Boolean} [options.underline] - Text markup annotation.
    - * @param {Object|Boolean} [options.strikeOut] - Text markup annotation.
    - * @param {Object} [options.textBox] - Text Box to fit in.
    - * @param {number} [options.textBox.width] - Text Box width
    - * @param {number} [options.textBox.height] - Text Box fixed height
    - * @param {number} [options.textBox.minHeight] - Text Box minimum height
    - * @param {number|number[]} [options.textBox.padding] - Text Box padding, [top, right, bottom, left]
    - * @param {number} [options.textBox.lineHeight] - Text Box line height
    - * @param {string} [options.textBox.textAlign] - Text alignment inside text box
    - * @param {Object} [options.textBox.style] - Text Box styles
    - * @param {number} [options.textBox.style.lineWidth] - Text Box border width
    - * @param {string|number[]} [options.textBox.style.stroke] - Text Box border color  (HexColor, PercentColor or DecimalColor)
    - * @param {number[]} [options.textBox.style.dash] - Text Box border border dash style [number, number]
    - * @param {string|number[]} [options.textBox.style.fill] - Text Box border background color (HexColor, PercentColor or DecimalColor)
    - * @param {number} [options.textBox.style.opacity] - Text Box border background opacity
    - */
    -exports.text = function text(text = '', x, y, options = {}) {
    -    if (!this.pageContext) {
    -        return this;
    -    }
    -    const targetAnnotations = options;
    -    const originCoord = this._calibrateCoordinate(x, y, 0, 0, this.pageNumber);
    -    const pathOptions = this._getPathOptions(options, originCoord.nx, originCoord.ny);
    -    pathOptions.html = options.html;
    -    // const { width: pageWidth, height: pageHeight } = this.metadata[this.pageNumber];
    -
    -    const textObjects = (options.html) ? htmlToTextObjects(text) : [{
    -        value: text,
    -        tag: null,
    -        isBold: options.bold,
    -        isItalic: options.italic,
    -        attributes: [],
    -        styles: {},
    -        needsLineBreaker: false,
    -        size: pathOptions.size,
    -        sizeRatio: 1,
    -        sizeRatios: [1],
    -        link: null,
    -        childs: []
    -    }];
    -    const textBox = (!options.textBox) ? {
    -        // use page width with padding and margin
    -        isSimpleText: true,
    -        width: null,
    -        lineHeight: 0,
    -        padding: 0,
    -        minHeight: 0
    -    } : {
    -        width: options.textBox.width || 100,
    -        lineHeight: options.textBox.lineHeight,
    -        height: options.textBox.height,
    -        padding: (options.textBox.padding || 0),
    -        minHeight: options.textBox.minHeight || 0,
    -        style: options.textBox.style,
    -        textAlign: options.textBox.textAlign
    -    };
    -
    -    textBox.padding = (Array.isArray(textBox.padding)) ? textBox.padding : [textBox.padding];
    -    Object.assign(textBox, {
    -        paddingTop: textBox.padding[0],
    -        paddingRight: textBox.padding[1] || textBox.padding[0],
    -        paddingBottom: textBox.padding[2] || textBox.padding[0],
    -        paddingLeft: textBox.padding[2] || textBox.padding[1] || textBox.padding[0],
    -    });
    -    let firstLineHeight;
    -    // let lastLineHeight;
    -
    -    let totalHeight = 0;
    -    let toWriteTextObjects = [];
    -
    -    // const layerIndex = {};
    -
    -    const writeValue = (textObject) => {
    -        textObject.lineID = textObject.lineID || Date.now() * Math.random();
    -        textObject.lineID = (textObject.needsLineBreaker) ? Date.now() * Math.random() : textObject.lineID;
    -        if (textObject.value) {
    -            textObject.styles.color = (textObject.styles.color) ?
    -                this._transformColor(textObject.styles.color) : pathOptions.color;
    -            const {
    -                toWriteTextObjects: newToWriteObjects,
    -                paragraphHeight
    -            } = getToWriteTextObjects(textObject, pathOptions, textBox);
    -            toWriteTextObjects = [...toWriteTextObjects, ...newToWriteObjects];
    -            if (!firstLineHeight) {
    -                firstLineHeight = toWriteTextObjects[0].lineHeight;
    -            }
    -            totalHeight += paragraphHeight; // textObject.size;
    -        }
    -        if (textObject.tag && textObject.childs.length) {
    -            // console.log(textObject);
    -            if (!textObject.size) {
    -                textObject.size = pathOptions.size * textObject.sizeRatio;
    -                textObject.sizeRatios = [textObject.sizeRatio];
    -            }
    -            textObject.layer = textObject.layer || 0;
    -            textObject.layer++;
    -
    -            textObject.currentIndex = 0;
    -
    -            textObject.childs.forEach((child) => {
    -                if (textObject.tag == 'ul') {
    -                    child.prependValue = '* ';
    -                    child.layer = textObject.layer + 1;
    -                    // child.indent = 4 * child.layer;
    -                }
    -                if (textObject.tag == 'ol') {
    -                    if (child.tag != 'ol') {
    -                        textObject.currentIndex++;
    -                        child.prependValue = `${(textObject.currentIndex).toString()}. `;
    -                    }
    -                    child.layer = textObject.layer + 1;
    -                    // child.indent = 4 * child.layer;
    -                }
    -                if (textObject.tag == 'li') {
    -                    if (child.tag == 'ol' || child.tag == 'ul') {
    -                        child.layer = textObject.layer - 1;
    -                    }
    -                }
    -                if (textObject.prependValue) {
    -                    child.prependValue = (!['ol', 'ul'].includes(textObject.tag)) ?
    -                        textObject.prependValue : child.prependValue;
    -                    textObject.indent = 2 * textObject.layer;
    -                }
    -                if (textObject.indent) {
    -                    child.indent = child.indent || textObject.indent;
    -                }
    -                if (textObject.size) {
    -                    child.size = textObject.size * child.sizeRatio;
    -                    child.sizeRatios = [...textObject.sizeRatios, child.sizeRatio];
    -                }
    -                child.styles = Object.assign(child.styles, textObject.styles);
    -
    -                child.isBold = (textObject.isBold) ? textObject.isBold : child.isBold;
    -                child.isItalic = (textObject.isItalic) ? textObject.isItalic : child.isItalic;
    -                child.underline = (textObject.underline) ? textObject.underline : child.underline;
    -
    -                child.lineID = textObject.lineID;
    -                writeValue(child);
    -            });
    -        }
    -    };
    -    textObjects.forEach((textObject) => {
    -        writeValue(textObject);
    -    });
    -
    -    const calculatedHeight = totalHeight + textBox.paddingTop + textBox.paddingBottom;
    -
    -    if (!textBox.height) {
    -        textBox.height = calculatedHeight;
    -        textBox.height = (textBox.minHeight && textBox.minHeight >= textBox.height) ? textBox.minHeight : textBox.height;
    -    }
    -    if (!textBox.width) {
    -        textBox.width = toWriteTextObjects[0].lineWidth;
    -    }
    -    textBox.firstLineHeight = firstLineHeight;
    -    textBox.lastLineHeight = toWriteTextObjects[toWriteTextObjects.length - 1].lineHeight;
    -
    -    const {
    -        offsetX,
    -        offsetY
    -    } = this._getTextBoxOffset(textBox, pathOptions);
    -    const {
    -        nx,
    -        ny
    -    } = this._calibrateCoordinate(x, y, offsetX, offsetY);
    -    if (textBox.style) {
    -        textBox.height = textBox.firstLineHeight * toWriteTextObjects.length
    -                       + textBox.paddingTop + textBox.paddingBottom;
    -        this.rectangle(
    -            nx,
    -            ny - textBox.height + textBox.baselineHeight + textBox.paddingTop + textBox.paddingBottom,
    -            textBox.width, textBox.height, Object.assign(textBox.style, {
    -                useGivenCoords: true,
    -                rotation: pathOptions.rotation,
    -                rotationOrigin: [pathOptions.originX,pathOptions.originY],
    -            })
    -        );
    -    }
    -    const context = this.pageContext;
    -    // const space = 8;
    -    let currentY = ny + textBox.paddingBottom;
    -    let currentLineID;
    -    let currentLineWidth = 0;
    -    let toWriteContents = [];
    -
    -    toWriteTextObjects.forEach((toWriteTextObject, index) => {
    -        const {
    -            text,
    -            lineHeight,
    -            lineWidth,
    -            lineID,
    -            spaceWidth
    -        } = toWriteTextObject;
    -
    -        currentLineID = currentLineID || lineID;
    -        const isContinued = (currentLineID == lineID) ? true : false;
    -
    -        const getStartX = (startX, currentWriteOptions) => {
    -            let x = startX;
    -            let offsetX = 0;
    -            if (currentWriteOptions.align == 'center') {
    -                offsetX = (textBox.width - currentLineWidth) / 2;
    -            } else
    -            if (currentWriteOptions.align == 'right') {
    -                offsetX = (textBox.width - textBox.paddingRight - currentLineWidth);
    -            } else {
    -                offsetX = textBox.paddingLeft;
    -            }
    -            return x + offsetX;
    -        };
    -
    -        const writeText = (context, x, y, wto) => {
    -            const options = wto.writeOptions;
    -            const {lineHeight, lineWidth, text} = wto;
    -
    -            // write directly to page when not dealing with opacity, rotation and special colorspace.
    -            if ( options.opacity  === 1 && options.colorspace !== 'separation' &&
    -                (options.rotation === 0 || options.rotation === undefined) ) {
    -                context.writeText(text, x, y, options);
    -            } else {
    -                // ... otherwise use xObjectForm when rotation or opacity exists
    -                this.pauseContext();
    -                // https://github.com/galkahana/HummusJS/wiki/Use-the-pdf-drawing-operators
    -                const xObject = new xObjectForm(this.writer, lineWidth * 1.5, lineHeight * 2);
    -                const xObjectCtx = xObject.getContentContext();
    -
    -                xObjectCtx
    -                    .q()
    -                    .gs(xObject.getGsName(options.fillGsId))
    -                    .BT();
    -                xObject.fill(options.colorModel);
    -                xObjectCtx
    -                    .Tf(options.font, options.size)
    -                    .Tm(1, 0, 0, 1, 1, lineHeight)
    -                    .Tj(text)
    -                    .ET()
    -                    .Q();
    -                xObject.end();
    -                this.resumeContext();
    -                this.pageContext.q();
    -
    -                options.deltaY = lineHeight;
    -                this._setRotationContext(this.pageContext, x, y, options);
    -
    -                this.pageContext
    -                    .doXObject(xObject)
    -                    .Q();
    -            }
    -
    -            const { textHeight } = options;
    -
    -            for (let key in targetAnnotations) {
    -                const subtype = this._getTextMarkupAnnotationSubtype(key);
    -                if (subtype) {
    -                    const markupOption = (typeof (targetAnnotations[key]) != 'object') ? {} : targetAnnotations[key];
    -                    Object.assign(markupOption, {
    -                        height: textHeight * 1.4,
    -                        width: currentLineWidth,
    -                        text: markupOption.text || '',
    -                        _textHeight: textHeight
    -                    });
    -                    const {
    -                        ox,
    -                        oy
    -                    } = this._reverseCoordinate(x, y - textHeight * 0.2);
    -                    this.annot(ox, oy, subtype, markupOption);
    -                }
    -            }
    -        };
    -
    -        if (!isContinued) {
    -            toWriteContents.forEach((content) => {
    -                const x = getStartX(content.startX, content.wto.writeOptions);
    -                const y = currentY;
    -                writeText(context, x, y, content.wto);
    -            });
    -            toWriteContents = [];
    -            currentLineID = lineID;
    -            currentLineWidth = 0;
    -            currentY -= lineHeight;
    -        }
    -
    -        const startX = nx + currentLineWidth;
    -        if (text != '[@@DONOT_RENDER_THIS@@]') {
    -            toWriteContents.push({
    -                startX,
    -                wto: toWriteTextObject
    -            });
    -        }
    -
    -        currentLineWidth = currentLineWidth + lineWidth + spaceWidth;
    -
    -        // Processing last line?
    -        if (index == toWriteTextObjects.length - 1) {
    -            toWriteContents.forEach((content) => {
    -                const x = getStartX(content.startX, content.wto.writeOptions);
    -                const y = currentY;
    -                writeText(context, x, y, content.wto);
    -            });
    -        }
    -    });
    -
    -    return this;
    -};
    -
    -function getToWriteTextObjects(textObject = {}, pathOptions, textBox = {}) {
    -    const toWriteTextObjects = [];
    -    let text = ((textObject.prependValue) ? textObject.prependValue : '') +
    -        textObject.value +
    -        ((textObject.appendValue) ? textObject.appendValue : '');
    -
    -    const size = textObject.size || pathOptions.size;
    -    // Use the same string to get the same height for each string with the same font.
    -    // Need lowercase 'gjpqy' so descenders are included in text height.
    -    const textDimensions = pathOptions.font.calculateTextDimensions(
    -        'ABCDEFGHIJKLMNOPQRSTUVWXYZgjpqy', size
    -    );
    -    const textHeight = textDimensions.height;
    -
    -    pathOptions.textHeight = textHeight;
    -    textBox.lineHeight = textBox.lineHeight || textHeight;
    -    textBox.baselineHeight = textDimensions.yMax;
    -
    -    const breaker = new LineBreaker(text);
    -    const lines = [];
    -    const indent = (textObject.indent || 0);
    -    // const indentWidth = indent * size;
    -    const lineMaxWidth = (textBox.width) ? textBox.width - textBox.paddingLeft - textBox.paddingRight - indent : null;
    -    let newLine = new Line(lineMaxWidth, textBox.lineHeight, size, pathOptions);
    -    let last = 0;
    -    let bk = breaker.nextBreak();
    -    let previousWord;
    -
    -    for (let i = 0; i < indent; i++) {
    -        newLine.addWord(new Word(' '));
    -    }
    -
    -    while (bk) {
    -        let nextWord = text.slice(last, bk.position);
    -        /**
    -         * Author: silverma (Marc Silverman)
    -         * #29 Is it possible to add multi-line text?
    -         * https://github.com/chunyenHuang/hummusRecipe/issues/29
    -         */
    -        if (bk.required) {
    -            nextWord = nextWord.trim();
    -        }
    -
    -        const word = new Word(nextWord, pathOptions);
    -        if (newLine.canFit(word)) {
    -            newLine.addWord(word);
    -        } else {
    -            // remove any trailing space on previous word so right justification works appropriately
    -            if ( previousWord ) {
    -                newLine.replaceLastWord(new Word(previousWord.value.trim(), pathOptions));
    -            }
    -            lines.push(newLine);
    -            newLine = new Line(lineMaxWidth, textBox.lineHeight, size, pathOptions);
    -            for (let i = 0; i < indent; i++) {
    -                newLine.addWord(new Word(' '));
    -            }
    -            if (textObject.prependValue) {
    -                const space = Array(textObject.prependValue.length + 1).fill(' ').join('');
    -                newLine.addWord(new Word(space));
    -            }
    -            newLine.addWord(word);
    -        }
    -
    -        /**
    -         * Author: silverma (Marc Silverman)
    -         * #29 Is it possible to add multi-line text?
    -         * https://github.com/chunyenHuang/hummusRecipe/issues/29
    -         */
    -        if (bk.required) {
    -            lines.push(newLine);
    -            newLine = new Line(lineMaxWidth, textBox.lineHeight, size, pathOptions);
    -        }
    -        previousWord = word;
    -        last = bk.position;
    -        bk = breaker.nextBreak();
    -    }
    -    lines.push(newLine);
    -
    -    const lineHeightMargin = 0;
    -    let lineHeight = (textHeight + lineHeightMargin >= textBox.lineHeight) ? textHeight + lineHeightMargin : textBox.lineHeight;
    -    const writeOptions = Object.assign({}, pathOptions, {
    -        color: textObject.styles.color,
    -        opacity: parseFloat(textObject.styles.opacity || pathOptions.opacity || 1),
    -        underline: textObject.underline || pathOptions.underline,
    -        size: textObject.size,
    -        align: textBox.textAlign,
    -        font: (textObject.isBold && textObject.isItalic) ? pathOptions.fonts.boldItalic : (textObject.isItalic) ? pathOptions.fonts.italic : (textObject.isBold) ? pathOptions.fonts.bold : pathOptions.font
    -    });
    -    let paragraphHeight = 0;
    -    lines.forEach((line, index) => {
    -        lineHeight = (lineHeight >= line.height) ? lineHeight : line.height;
    -
    -        // Space width only gets applied for HTML elements.
    -        // Otherwise, normal text alignments (center,right) are shifted left by a space.
    -        toWriteTextObjects.push({
    -            text: line.value,
    -            lineID: (index == 0) ? textObject.lineID : Date.now() * Math.random(),
    -            // @todo: handle line height to mimic html tag
    -            lineHeight,
    -            lineWidth: line.currentWidth,
    -            spaceWidth: (pathOptions.html) ? line.spaceWidth : 0,
    -            writeOptions
    -        });
    -        paragraphHeight += lineHeight;
    -    });
    -    return {
    -        toWriteTextObjects,
    -        paragraphHeight
    -    };
    -}
    +            
    const LineBreaker = require('linebreak');
    +const {
    +    Word,
    +    Line
    +} = require('./text.helper');
    +const {
    +    htmlToTextObjects
    +} = require('./htmlToTextObjects');
    +const xObjectForm = require('./xObjectForm');
    +
    +//  Table indicating how to specify coloration of elements
    +//  -------------------------------------------------------------------
    +// |Color | HexColor   | DecimalColor                   | PercentColor |
    +// |Space | (string)   | (array)                        | (string)     |
    +// |------+------------+--------------------------------+--------------|
    +// | Gray | #GG        | [gray]                         | %G           |
    +// |  RGB | #rrggbb    | [red, green, blue]             | %G           |
    +// | CMYK | #ccmmyykk  | [cyan, magenta, yellow, black] | %c,m,y,k     |
    +//  -------------------------------------------------------------------
    +//
    +//   HexColor component values (two hex digits) range from 00 to FF.
    +//   DecimalColor component values range from 0 to 255.
    +//   PercentColor component values range from 1 to 100.
    +
    +/**
    + * Write text elements
    + * @name text
    + * @function
    + * @todo support break words
    + * @memberof Recipe
    + * @param {string} text - The text content
    + * @param {number} x - The coordinate x
    + * @param {number} y - The coordinate y
    + * @param {Object} [options] - The options
    + * @param {string|number[]} [options.color] - Text color (HexColor, PercentColor or DecimalColor)
    + * @param {number} [options.opacity=1] - opacity
    + * @param {number} [options.rotation=0] - Accept: +/- 0 through 360.
    + * @param {number[]} [options.rotationOrigin=[x,y]] - [originX, originY]
    + * @param {string} [options.font=Helvetica] - The font. 'Arial', 'Helvetica'...
    + * @param {number} [options.size=14] - The font size
    + * @param {string} [options.align='left top'] - The alignment. 'center center'...
    + * @param {Object|Boolean} [options.highlight] - Text markup annotation.
    + * @param {Object|Boolean} [options.underline] - Text markup annotation.
    + * @param {Object|Boolean} [options.strikeOut] - Text markup annotation.
    + * @param {Object} [options.textBox] - Text Box to fit in.
    + * @param {number} [options.textBox.width=100] - Text Box width
    + * @param {number} [options.textBox.height] - Text Box fixed height
    + * @param {number} [options.textBox.minHeight=0] - Text Box minimum height
    + * @param {number|number[]} [options.textBox.padding=0] - Text Box padding, [top, right, bottom, left]
    + * @param {number} [options.textBox.lineHeight=0] - Text Box line height
    + * @param {string|Boolean} [options.textBox.wrap='auto'] - Text wrapping mechanism, may be true, false, 
    + * 'auto', 'clip', 'trim', 'ellipsis'. All the option values that are not equivalent to 'auto' dictate
    + *  how the text which does not fit on a line is to be truncated. True is equivalent to 'auto'. False is equivalent to 'ellipsis'.
    + * @param {string} [options.textBox.textAlign='left top'] - Alignment inside text box, specified as 'horizontal vertical',
    + * where horizontal is one of: 'left', 'center', 'right', 'justify' and veritical is one of: 'top', 'center', 'bottom'.
    + * @param {Object} [options.textBox.style] - Text Box styles
    + * @param {number} [options.textBox.style.lineWidth=2] - Text Box border width
    + * @param {string|number[]} [options.textBox.style.stroke] - Text Box border color  (HexColor, PercentColor or DecimalColor)
    + * @param {number[]} [options.textBox.style.dash=[]] - Text Box border border dash style [number, number]
    + * @param {string|number[]} [options.textBox.style.fill] - Text Box border background color (HexColor, PercentColor or DecimalColor)
    + * @param {number} [options.textBox.style.opacity=1] - Text Box border background opacity
    + */
    +exports.text = function text(text = '', x, y, options = {}) {
    +    if (!this.pageContext) {
    +        return this;
    +    }
    +    const targetAnnotations = options;
    +    const originCoord = this._calibrateCoordinate(x, y, 0, 0, this.pageNumber);
    +    const pathOptions = this._getPathOptions(options, originCoord.nx, originCoord.ny);
    +    pathOptions.html = options.html;
    +    // const { width: pageWidth, height: pageHeight } = this.metadata[this.pageNumber];
    +
    +    const textObjects = (options.html) ? htmlToTextObjects(text) : [{
    +        value: text,
    +        tag: null,
    +        isBold: options.bold,
    +        isItalic: options.italic,
    +        attributes: [],
    +        styles: {},
    +        needsLineBreaker: false,
    +        size: pathOptions.size,
    +        sizeRatio: 1,
    +        sizeRatios: [1],
    +        link: null,
    +        childs: []
    +    }];
    +    const textBox = (!options.textBox) ? {
    +        // use page width with padding and margin
    +        isSimpleText: true,
    +        width: null,
    +        lineHeight: 0,
    +        padding: 0,
    +        minHeight: 0,
    +        wrap: 'auto'
    +    } : {
    +        width: options.textBox.width || 100,
    +        lineHeight: options.textBox.lineHeight,
    +        height: options.textBox.height,
    +        padding: (options.textBox.padding || 0),
    +        minHeight: options.textBox.minHeight || 0,
    +        style: options.textBox.style,
    +        textAlign: options.textBox.textAlign,
    +        wrap: (options.textBox.wrap !== undefined) ? options.textBox.wrap : 'auto'
    +    };
    +
    +    // allow user to treat wrap as boolean
    +    if (textBox.wrap === true) {
    +        textBox.wrap = 'auto';
    +    } else if (textBox.wrap === false) {
    +        textBox.wrap = 'ellipsis'
    +    }
    +
    +    // Allows user to enter a single number which will be used for all text box sides,
    +    // or an array of values [top, right, bottom, left] with any combination of missing sides.
    +    // Default value for a missing side is the value of the text box's opposite side (see below).
    +    //
    +    //               padding[0]
    +    //   padding[3]              padding[1]
    +    //               padding[2]
    +
    +    textBox.padding = (Array.isArray(textBox.padding)) ? textBox.padding : [textBox.padding];
    +
    +    Object.assign(textBox, {
    +        paddingTop:     textBox.padding[0],
    +        paddingRight:  (textBox.padding[1] !== undefined) ? textBox.padding[1] : textBox.padding[0],
    +        paddingBottom: (textBox.padding[2] !== undefined) ? textBox.padding[2] : textBox.padding[0],
    +        paddingLeft:   (textBox.padding[3] !== undefined) ? textBox.padding[3] :
    +                       (textBox.padding[1] !== undefined) ? textBox.padding[1] : textBox.padding[0]
    +    });
    +
    +    let firstLineHeight;
    +    let totalHeight = 0;
    +    let toWriteTextObjects = [];
    +
    +    const writeValue = (textObject) => {
    +        textObject.lineID = textObject.lineID || Date.now() * Math.random();
    +        textObject.lineID = (textObject.needsLineBreaker) ? Date.now() * Math.random() : textObject.lineID;
    +        if (textObject.value) {
    +            textObject.styles.color = (textObject.styles.color) ?
    +                this._transformColor(textObject.styles.color) : pathOptions.color;
    +            const {
    +                toWriteTextObjects: newToWriteObjects,
    +                paragraphHeight
    +            } = getToWriteTextObjects(textObject, pathOptions, textBox);
    +            toWriteTextObjects = [...toWriteTextObjects, ...newToWriteObjects];
    +            if (!firstLineHeight) {
    +                firstLineHeight = toWriteTextObjects[0].lineHeight;
    +            }
    +            totalHeight += paragraphHeight; // textObject.size;
    +        }
    +        if (textObject.tag && textObject.childs.length) {
    +            // console.log(textObject);
    +            if (!textObject.size) {
    +                textObject.size = pathOptions.size * textObject.sizeRatio;
    +                textObject.sizeRatios = [textObject.sizeRatio];
    +            }
    +            textObject.layer = textObject.layer || 0;
    +            textObject.layer++;
    +
    +            textObject.currentIndex = 0;
    +
    +            textObject.childs.forEach((child) => {
    +                if (textObject.tag == 'ul') {
    +                    child.prependValue = '* ';
    +                    child.layer = textObject.layer + 1;
    +                    // child.indent = 4 * child.layer;
    +                }
    +                if (textObject.tag == 'ol') {
    +                    if (child.tag != 'ol') {
    +                        textObject.currentIndex++;
    +                        child.prependValue = `${(textObject.currentIndex).toString()}. `;
    +                    }
    +                    child.layer = textObject.layer + 1;
    +                    // child.indent = 4 * child.layer;
    +                }
    +                if (textObject.tag == 'li') {
    +                    if (child.tag == 'ol' || child.tag == 'ul') {
    +                        child.layer = textObject.layer - 1;
    +                    }
    +                }
    +                if (textObject.prependValue) {
    +                    child.prependValue = (!['ol', 'ul'].includes(textObject.tag)) ?
    +                        textObject.prependValue : child.prependValue;
    +                    textObject.indent = 2 * textObject.layer;
    +                }
    +                if (textObject.indent) {
    +                    child.indent = child.indent || textObject.indent;
    +                }
    +                if (textObject.size) {
    +                    child.size = textObject.size * child.sizeRatio;
    +                    child.sizeRatios = [...textObject.sizeRatios, child.sizeRatio];
    +                }
    +                child.styles = Object.assign(child.styles, textObject.styles);
    +
    +                child.isBold = (textObject.isBold) ? textObject.isBold : child.isBold;
    +                child.isItalic = (textObject.isItalic) ? textObject.isItalic : child.isItalic;
    +                child.underline = (textObject.underline) ? textObject.underline : child.underline;
    +
    +                child.lineID = textObject.lineID;
    +                writeValue(child);
    +            });
    +        }
    +    };
    +    textObjects.forEach((textObject) => {
    +        writeValue(textObject);
    +    });
    +
    +    if (!textBox.width) {
    +        textBox.width = toWriteTextObjects[0].lineWidth;
    +    }
    +    textBox.firstLineHeight = firstLineHeight;
    +    textBox.lastLineHeight  = pathOptions.textHeight;
    +
    +    const {
    +        offsetX,
    +        offsetY
    +    } = this._getTextBoxOffset(textBox, pathOptions);
    +    const {
    +        nx,
    +        ny
    +    } = this._calibrateCoordinate(x, y, offsetX, offsetY);
    +
    +    let textYpos = ny;
    +
    +    if (textBox.style) {
    +        const textBoxWidth  = textBox.width + textBox.paddingLeft + textBox.paddingRight;
    +        const actualTextHeight = textBox.firstLineHeight * (toWriteTextObjects.length-1) + textBox.lastLineHeight
    +
    +        if (!textBox.height) {
    +            textBox.height = actualTextHeight + textBox.paddingTop + textBox.paddingBottom;
    +            
    +            if (textBox.minHeight && textBox.minHeight > textBox.height) {
    +                textBox.height = textBox.minHeight;
    +            }
    +        }
    +
    +        this.rectangle(
    +            nx - textBox.paddingLeft, 
    +            ny - textBox.height + textBox.firstLineHeight + textBox.paddingTop,
    +            textBoxWidth, textBox.height, Object.assign(textBox.style, {
    +                useGivenCoords: true,
    +                rotation: pathOptions.rotation,
    +                rotationOrigin: [pathOptions.originX,pathOptions.originY],
    +            })
    +        );
    +         
    +        // Determine vertical starting position of text within textBox
    +        switch (toWriteTextObjects[0].writeOptions.alignVertical) {
    +            case 'center':
    +                textYpos -= (textBox.height - actualTextHeight) / 2;
    +                break;
    +            case 'bottom':
    +                textYpos -= (textBox.height - actualTextHeight);
    +                break;
    +        }
    +    }
    +    
    +    const context = this.pageContext;
    +    // const space = 8;
    +    let currentY = textYpos;
    +    let currentLineID;
    +    let currentLineWidth = 0;
    +    let toWriteContents = [];
    +
    +    toWriteTextObjects.forEach((toWriteTextObject, index) => {
    +        const {
    +            text,
    +            lineHeight,
    +            lineWidth,
    +            lineID,
    +            spaceWidth
    +        } = toWriteTextObject;
    +
    +        currentLineID = currentLineID || lineID;
    +        const isContinued = (currentLineID == lineID) ? true : false;
    +
    +        const getStartX = (startX, currentWriteOptions) => {
    +            let offsetX = textBox.paddingLeft;
    +
    +            switch (currentWriteOptions.alignHorizontal) {
    +                case 'center':
    +                    offsetX = (textBox.width - currentLineWidth) / 2;
    +                    break;
    +                case 'right':
    +                    offsetX = (textBox.width - textBox.paddingRight - currentLineWidth);
    +                    break;
    +            }
    +            
    +            return startX + offsetX;
    +        };
    +
    +        const writeText = (context, x, y, wto) => {
    +            const options = wto.writeOptions;
    +            const {lineHeight, text, baseline, wordsInLine, textWidth} = wto;
    +
    +            // write directly to page when not dealing with opacity, rotation and special colorspace.
    +            if (options.opacity  === 1 && options.colorspace !== 'separation' &&
    +               (options.rotation === 0 || options.rotation === undefined)) {
    +                
    +                // Note that the last line of a text box ignores justification.
    +                if (options.alignHorizontal !== 'justify' || wto.lastLine) {
    +                    if (textBox.wrap !== 'auto') {  // This applies a clipping region around the text
    +                        this.pageContext.q();
    +                        this.pageContext
    +                            .m(nx,y+lineHeight)
    +                            .l(nx+textBox.width,y+lineHeight)
    +                            .l(nx+textBox.width,y)
    +                            .l(nx,y)
    +                            .h().W().n();
    +                    }
    +                    
    +                    context.writeText(text, x, y+baseline, options);
    +
    +                    if (textBox.wrap !== 'auto') {
    +                        this.pageContext.Q();
    +                    }
    +                } else {
    +                    justify(x, wordsInLine, textBox, textWidth, (word, xx) => {
    +                        context.writeText(word, xx, y+baseline, options);
    +                    });
    +                }            
    +            } else {
    +                this.pauseContext();
    +
    +                // https://github.com/galkahana/HummusJS/wiki/Use-the-pdf-drawing-operators
    +                const xObject = new xObjectForm(this.writer, textBox.width, lineHeight);
    +                const xObjectCtx = xObject.getContentContext();
    +                // Build form object
    +                xObjectCtx
    +                    .q()
    +                    .gs(xObject.getGsName(options.fillGsId))  // set graphic state (here opacity)
    +                    .BT();                                    // begin text context
    +                xObjectCtx.Tf(options.font, options.size);    // set font
    +                xObject.fill(options.colorModel);             // set color
    +
    +                if (options.alignHorizontal !== 'justify' || wto.lastLine) {
    +                    xObjectCtx
    +                        .Tm(1, 0, 0, 1, 0, baseline)          // set position in object
    +                        .Tj(text);                            // write text
    +                } else {
    +                    justify(0, wordsInLine, textBox, textWidth, (word, xx) => {
    +                        xObjectCtx
    +                        .Tm(1, 0, 0, 1, xx, baseline)         // set position in object
    +                        .Tj(word);                            // write word
    +                    });
    +                }
    +
    +                xObjectCtx
    +                    .ET()                                     // end text context
    +                    .Q();
    +                xObject.end();
    +
    +                this.resumeContext();
    +
    +                this.pageContext.q();
    +                this._setRotationContext(this.pageContext, x, y, options);
    +                this.pageContext
    +                    .doXObject(xObject)
    +                    .Q();
    +            }
    +
    +            const { textHeight } = options;
    +
    +            for (let key in targetAnnotations) {
    +                const subtype = this._getTextMarkupAnnotationSubtype(key);
    +                if (subtype) {
    +                    const markupOption = (typeof (targetAnnotations[key]) != 'object') ? {} : targetAnnotations[key];
    +                    Object.assign(markupOption, {
    +                        height: textHeight * 1.4,
    +                        width: currentLineWidth,
    +                        text: markupOption.text || '',
    +                        _textHeight: textHeight
    +                    });
    +                    const {
    +                        ox,
    +                        oy
    +                    } = this._reverseCoordinate(x, y - textHeight * 0.2);
    +                    this.annot(ox, oy, subtype, markupOption);
    +                }
    +            }
    +        };
    +
    +        if (!isContinued) {
    +            toWriteContents.forEach((content) => {
    +                const x = getStartX(content.startX, content.wto.writeOptions);
    +                const y = currentY;
    +                writeText(context, x, y, content.wto);
    +            });
    +            toWriteContents = [];
    +            currentLineID = lineID;
    +            currentLineWidth = 0;
    +            currentY -= lineHeight;
    +        }
    +
    +        const startX = nx + currentLineWidth;
    +        if (text != '[@@DONOT_RENDER_THIS@@]') {
    +            toWriteContents.push({
    +                startX,
    +                wto: toWriteTextObject
    +            });
    +        }
    +
    +        currentLineWidth = currentLineWidth + lineWidth + spaceWidth;
    +
    +        // Processing last line?
    +        if (index == toWriteTextObjects.length - 1) {
    +            toWriteContents.forEach((content) => {
    +                const x = getStartX(content.startX, content.wto.writeOptions);
    +                const y = currentY;
    +                writeText(context, x, y, content.wto);
    +            });
    +        }
    +    });
    +
    +    return this;
    +};
    +
    +function justify (x, wordsInLine, textBox, textWidth, callback) {
    +    // For some reason, textWidth is smaller than lineWidth. My suspicions lie in the fact
    +    // that spacing computations appear different depending on where the space is located.
    +    // What is noted though that if lineWidth is used in the calculations for text 
    +    // justitification, the text goes passed the right boundary. Due to the vagary in space
    +    // computation, the final wrinkle to make sure the last word in the line smacks up against
    +    // the right side boundary is to perform a special computation on the last word positioning
    +    // relative to that right side bounds.
    +    const spaceCount = wordsInLine.length-1;
    +    const spaceBetweenWords = ( wordsInLine.length > 1) ? (textBox.width - textWidth) / (spaceCount) : 0;
    +    const lineStart = x;
    +
    +    for (let index = 0; index < wordsInLine.length; index++) {
    +        const word = wordsInLine[index];
    +        
    +        callback(word.value, x)
    +        // Ready to compute last word spacing?
    +        if (index+1 === spaceCount) {
    +            x = lineStart + textBox.width - textBox.paddingRight - wordsInLine[index+1].dimensions.xMax;
    +        } else {
    +            x += word.dimensions.width + spaceBetweenWords;
    +        }
    +    }
    +}
    +
    +function nextWord (text, brk, previousPosition, pathOptions) {
    +    let nextWord = text.slice(previousPosition, brk.position);
    +    
    +    if (brk.required) {  // effectively saw a '\n' in text.
    +        nextWord = nextWord.trim();
    +    }
    +
    +    return new Word(nextWord, pathOptions);
    +}
    +
    +function elideNonFittingText(textBox, line, word, pathOptions) {
    +    if (textBox.wrap === 'clip') {
    +        line.addWord(word);
    +    } else if (textBox.wrap === 'ellipsis') {
    +        // This is more complicated than the other no-wrap options.
    +        // It makes an initial attempt to take the word that was
    +        // too big and make it shrink in size until it and the 
    +        // ellipsis character fit. If that doesn't work, one more
    +        // stab is taken by trying to shrink the previous word
    +        // that fit on the line.
    +        const ellipsis = '…';
    +        let usingPreviousWord = false;
    +        let tooBig = new Word(word.value.slice(0,-2)+ellipsis, pathOptions);
    +
    +        while ( !line.canFit(tooBig)) {
    +            
    +            if (tooBig.value.length > 1) {
    +                tooBig = new Word(tooBig.value.slice(0,-2)+ellipsis, pathOptions);
    +
    +            // Try last word that fit in box?
    +            } else if (!usingPreviousWord) {
    +                tooBig = new Word(line.words.pop().value.slice(0,-2) + ellipsis, pathOptions);
    +                usingPreviousWord = true;
    +
    +            } else {
    +                break;  // give up, only get 2 shots at this.
    +            }
    +        }
    +
    +        line.addWord(tooBig);
    +    }
    +}
    +
    +function getToWriteTextObjects(textObject = {}, pathOptions, textBox = {}) {
    +    const toWriteTextObjects = [];
    +    let text = ((textObject.prependValue) ? textObject.prependValue : '') +
    +        textObject.value +
    +        ((textObject.appendValue) ? textObject.appendValue : '');
    +
    +    const size = textObject.size || pathOptions.size;
    +    // Use the same string to get the same height for each string with the same font.
    +    // Need lowercase 'gjpqy' so descenders are included in text height.
    +    // Need special characters '|}' because they have ascenders that go beyond upper case letters.
    +    const textDimensions = pathOptions.font.calculateTextDimensions(
    +        'ABCDEFGHIJKLMNOPQRSTUVWXYZgjpqy|}', size
    +    );
    +    const textHeight = textDimensions.height;
    +
    +    pathOptions.textHeight = textHeight;
    +    textBox.lineHeight = textBox.lineHeight || textHeight;
    +    textBox.baselineHeight = textDimensions.yMax;
    +
    +    const breaker = new LineBreaker(text);
    +    const lines = [];
    +    const indent = (textObject.indent || 0);
    +    // const indentWidth = indent * size;
    +    const lineMaxWidth = (textBox.width) ? textBox.width - textBox.paddingLeft - textBox.paddingRight - indent : null;
    +    let newLine = new Line(lineMaxWidth, textBox.lineHeight, size, pathOptions);
    +    let last = 0;
    +    let bk = breaker.nextBreak();
    +    let previousWord;
    +    let flushLine = false;
    +
    +    for (let i = 0; i < indent; i++) {
    +        newLine.addWord(new Word(' ', pathOptions));
    +    }
    +
    +    while (bk) {
    +        let word = nextWord(text, bk, last, pathOptions);
    +
    +        if (newLine.canFit(word)) {
    +            newLine.addWord(word);
    +        } else {
    +            // remove any trailing space on previous word so right justification works appropriately
    +            if ( previousWord && textBox.wrap === 'auto') {
    +                newLine.replaceLastWord(new Word(previousWord.value.trim(), pathOptions));
    +            }
    +            lines.push(newLine);
    +
    +            // now deal with text line wrap (what happens to text that doesn't fit in line)
    +            if (textBox.wrap !== 'auto') {
    +                flushLine = true;
    +                elideNonFittingText(textBox, newLine, word, pathOptions);
    +
    +            } else {
    +                // this is the auto wrap section
    +                newLine = new Line(lineMaxWidth, textBox.lineHeight, size, pathOptions);
    +                for (let i = 0; i < indent; i++) {
    +                    newLine.addWord(new Word(' ', pathOptions));
    +                }
    +                if (textObject.prependValue) {
    +                    const space = Array(textObject.prependValue.length + 1).fill(' ').join('');
    +                    newLine.addWord(new Word(space, pathOptions));
    +                }
    +                newLine.addWord(word);
    +            }
    +        }
    +
    +        if (flushLine) {
    +            while (bk) {
    +                if (bk.required) {
    +                    flushLine = false;
    +                    newLine = new Line(lineMaxWidth, textBox.lineHeight, size, pathOptions);
    +                    word = null;
    +                    break;
    +                }
    +                bk = breaker.nextBreak();
    +            }
    +        } else {
    +            /**
    +             * Author: silverma (Marc Silverman)
    +             * #29 Is it possible to add multi-line text?
    +             * https://github.com/chunyenHuang/hummusRecipe/issues/29
    +             */
    +            if (bk.required) {
    +                lines.push(newLine);
    +                newLine = new Line(lineMaxWidth, textBox.lineHeight, size, pathOptions);
    +            }
    +        }
    +       
    +        if (bk) {
    +            previousWord = word;
    +            last = bk.position;
    +            bk = breaker.nextBreak();
    +        }
    +    }
    +
    +    if (!flushLine) {
    +        lines.push(newLine);
    +    }
    +
    +    const lineHeightMargin = 0;
    +    let lineHeight = (textHeight + lineHeightMargin >= textBox.lineHeight) ? textHeight + lineHeightMargin : textBox.lineHeight;
    +    let [alignHorizontal, alignVertical] = (textBox.textAlign) ? textBox.textAlign.split(' ') : [];
    +    const writeOptions = Object.assign({}, pathOptions, {
    +        color: textObject.styles.color,
    +        opacity: parseFloat(textObject.styles.opacity || pathOptions.opacity || 1),
    +        underline: textObject.underline || pathOptions.underline,
    +        size: textObject.size,
    +        alignHorizontal: alignHorizontal,
    +        alignVertical: alignVertical,
    +        font: pathOptions.font
    +    });
    +    let paragraphHeight = 0;
    +    lines.forEach((line, index) => {
    +        lineHeight = (lineHeight >= line.height) ? lineHeight : line.height;
    +
    +        // Space width only gets applied for HTML elements.
    +        // Otherwise, normal text alignments (center,right) are shifted left by a space.
    +        toWriteTextObjects.push({
    +            text: line.value,
    +            lineID: (index == 0) ? textObject.lineID : Date.now() * Math.random(),
    +            // @todo: handle line height to mimic html tag
    +            lineHeight,
    +            baseline: lineHeight - textBox.baselineHeight,
    +            lineWidth: line.currentWidth,
    +            textWidth: line.textWidth,  // for justification
    +            spaceWidth: (pathOptions.html) ? line.spaceWidth : 0,
    +            wordsInLine: line.words,
    +            lastLine: (index === lines.length-1),
    +            writeOptions
    +        });
    +        paragraphHeight += lineHeight;
    +    });
    +    return {
    +        toWriteTextObjects,
    +        paragraphHeight
    +    };
    +}
     
    @@ -506,7 +670,7 @@

    text.js


    - Documentation generated by JSDoc 3.6.3 on Wed Oct 02 2019 12:00:26 GMT-0700 (Pacific Daylight Time) using the docdash theme. + Documentation generated by JSDoc 3.5.5 on Tue Nov 05 2019 10:28:09 GMT-0500 (Eastern Standard Time) using the docdash theme.
    diff --git a/docs/utils.js.html b/docs/utils.js.html index 188a863..4f490fc 100644 --- a/docs/utils.js.html +++ b/docs/utils.js.html @@ -37,65 +37,65 @@

    utils.js

    -
    const ANNOTATION_PREFIX = 'Annots';
    -
    -/**
    - * Append PDF Page with annotations.
    - *
    - * @param {any} pdfWriter - Hummus writer.
    - * @param {string|any} sourcePDFPath - The path for the output pdfs or Reader stream.
    - * @param {number} pageNumber - page number.
    - * @param {any} [options={}] - appendPDFPageFromPDF options
    - */
    -function appendPDFPageFromPDFWithAnnotations(pdfWriter, sourcePDFPath, pageNumber) {
    -    const cpyCxt = pdfWriter.createPDFCopyingContext(sourcePDFPath);
    -    const cpyCxtParser = cpyCxt.getSourceDocumentParser();
    -    const pageDictionary = cpyCxtParser.parsePageDictionary(pageNumber);
    -
    -    if (!pageDictionary.exists(ANNOTATION_PREFIX)) {
    -        cpyCxt.appendPDFPageFromPDF(pageNumber);
    -    } else {
    -        let reffedObjects;
    -        pdfWriter.getEvents().once('OnPageWrite', params => {
    -            params.pageDictionaryContext.writeKey(ANNOTATION_PREFIX);
    -            reffedObjects = cpyCxt.copyDirectObjectWithDeepCopy(pageDictionary.queryObject(ANNOTATION_PREFIX));
    -        });
    -
    -        cpyCxt.appendPDFPageFromPDF(pageNumber);
    -
    -        if (reffedObjects && reffedObjects.length > 0) {
    -            cpyCxt.copyNewObjectsForDirectObject(reffedObjects);
    -        }
    -    }
    -}
    -
    -/**
    - * Append PDF Pages with annotations.
    - *
    - * @param {any} pdfWriter - Hummus writer.
    - * @param {string|any} sourcePDFPath - The path for the output pdfs or Reader stream.
    - * @param {any} [options={}] - appendPDFPagesFromPDF options
    - */
    -function appendPDFPagesFromPDFWithAnnotations(pdfWriter, sourcePDFPath, options = {}) {
    -    const cpyCxt = pdfWriter.createPDFCopyingContext(sourcePDFPath);
    -    const cpyCxtParser = cpyCxt.getSourceDocumentParser();
    -
    -    if (options.specificRanges && options.specificRanges.length) {
    -        for (const [start, end] of options.specificRanges) {
    -            for (let i = start; i <= end; ++i) {
    -                appendPDFPageFromPDFWithAnnotations(pdfWriter, sourcePDFPath, i);
    -            }
    -        }
    -    } else {
    -        for (let i = 0; i < cpyCxtParser.getPagesCount(); ++i) {
    -            appendPDFPageFromPDFWithAnnotations(pdfWriter, sourcePDFPath, i);
    -        }
    -    }
    -}
    -
    -exports.ANNOTATION_PREFIX = ANNOTATION_PREFIX;
    -exports.appendPDFPageFromPDFWithAnnotations = appendPDFPageFromPDFWithAnnotations;
    -exports.appendPDFPagesFromPDFWithAnnotations = appendPDFPagesFromPDFWithAnnotations;
    +            
    const ANNOTATION_PREFIX = 'Annots';
    +
    +/**
    + * Append PDF Page with annotations.
    + *
    + * @param {any} pdfWriter - Hummus writer.
    + * @param {string|any} sourcePDFPath - The path for the output pdfs or Reader stream.
    + * @param {number} pageNumber - page number.
    + * @param {any} [options={}] - appendPDFPageFromPDF options
    + */
    +function appendPDFPageFromPDFWithAnnotations(pdfWriter, sourcePDFPath, pageNumber) {
    +    const cpyCxt = pdfWriter.createPDFCopyingContext(sourcePDFPath);
    +    const cpyCxtParser = cpyCxt.getSourceDocumentParser();
    +    const pageDictionary = cpyCxtParser.parsePageDictionary(pageNumber);
    +
    +    if (!pageDictionary.exists(ANNOTATION_PREFIX)) {
    +        cpyCxt.appendPDFPageFromPDF(pageNumber);
    +    } else {
    +        let reffedObjects;
    +        pdfWriter.getEvents().once('OnPageWrite', params => {
    +            params.pageDictionaryContext.writeKey(ANNOTATION_PREFIX);
    +            reffedObjects = cpyCxt.copyDirectObjectWithDeepCopy(pageDictionary.queryObject(ANNOTATION_PREFIX));
    +        });
    +
    +        cpyCxt.appendPDFPageFromPDF(pageNumber);
    +
    +        if (reffedObjects && reffedObjects.length > 0) {
    +            cpyCxt.copyNewObjectsForDirectObject(reffedObjects);
    +        }
    +    }
    +}
    +
    +/**
    + * Append PDF Pages with annotations.
    + *
    + * @param {any} pdfWriter - Hummus writer.
    + * @param {string|any} sourcePDFPath - The path for the output pdfs or Reader stream.
    + * @param {any} [options={}] - appendPDFPagesFromPDF options
    + */
    +function appendPDFPagesFromPDFWithAnnotations(pdfWriter, sourcePDFPath, options = {}) {
    +    const cpyCxt = pdfWriter.createPDFCopyingContext(sourcePDFPath);
    +    const cpyCxtParser = cpyCxt.getSourceDocumentParser();
    +
    +    if (options.specificRanges && options.specificRanges.length) {
    +        for (const [start, end] of options.specificRanges) {
    +            for (let i = start; i <= end; ++i) {
    +                appendPDFPageFromPDFWithAnnotations(pdfWriter, sourcePDFPath, i);
    +            }
    +        }
    +    } else {
    +        for (let i = 0; i < cpyCxtParser.getPagesCount(); ++i) {
    +            appendPDFPageFromPDFWithAnnotations(pdfWriter, sourcePDFPath, i);
    +        }
    +    }
    +}
    +
    +exports.ANNOTATION_PREFIX = ANNOTATION_PREFIX;
    +exports.appendPDFPageFromPDFWithAnnotations = appendPDFPageFromPDFWithAnnotations;
    +exports.appendPDFPagesFromPDFWithAnnotations = appendPDFPagesFromPDFWithAnnotations;
     
    @@ -108,7 +108,7 @@

    utils.js


    - Documentation generated by JSDoc 3.6.3 on Wed Oct 02 2019 12:00:26 GMT-0700 (Pacific Daylight Time) using the docdash theme. + Documentation generated by JSDoc 3.5.5 on Fri Nov 01 2019 20:41:12 GMT-0400 (Eastern Daylight Time) using the docdash theme.
    diff --git a/docs/vector-line.js.html b/docs/vector-line.js.html index 4f984c8..5c1ff9a 100644 --- a/docs/vector-line.js.html +++ b/docs/vector-line.js.html @@ -37,87 +37,96 @@

    vector-line.js

    -
    /**
    - * move the current position to target position
    - * @name moveTo
    - * @function
    - * @memberof Recipe
    - * @param {number} x - The coordinate x
    - * @param {number} y - The coordinate y
    - */
    -exports.moveTo = function moveTo(x, y) {
    -    const { nx, ny } = this._calibrateCoordinate(x, y);
    -    this._position = {
    -        x: nx,
    -        y: ny
    -    };
    -    return this;
    -};
    -
    -/**
    - * Draw a line from current position
    - * @name lineTo
    - * @function
    - * @memberof Recipe
    - * @param {number} x - The coordinate x
    - * @param {number} y - The coordinate y
    - * @param {Object} [options] - The options
    - * @param {string|number[]} [options.color] - HexColor, PercentColor or DecimalColor
    - * @param {string|number[]} [options.stroke] - HexColor, PercentColor or DecimalColor
    - * @param {string|number[]} [options.fill] - HexColor, PercentColor or DecimalColor
    - * @param {number} [options.lineWidth] - The line width
    - * @param {number} [options.opacity] - The opacity
    - * @param {number[]} [options.dash] - The dash style [number, number]
    - */
    -exports.lineTo = function lineTo(x, y, options = {}) {
    -    const fromX = this._position.x;
    -    const fromY = this._position.y;
    -    const { nx, ny } = this._calibrateCoordinate(x, y);
    -    const context = this.pageContext;
    -    const pathOptions = this._getPathOptions(options);
    -    pathOptions.type = 'stroke';
    -
    -    if (pathOptions.stroke !== undefined) {
    -        pathOptions.color = pathOptions.stroke;
    -        pathOptions.colorspace = pathOptions.strokeModel.colorspace;
    -    }
    -
    -    context
    -        .q()
    -        .J(1)
    -        .j(1)
    -        .d(pathOptions.dash, pathOptions.dashPhase)
    -        .drawPath(fromX, fromY, nx, ny, pathOptions)
    -        .Q();
    -    this.moveTo(x, y);
    -    return this;
    -};
    -
    -/**
    - * Draw a line
    - * @name line
    - * @function
    - * @memberof Recipe
    - * @param {number[]} coordinates - The array of coordinate [[x,y], [m,n]]
    - * @param {Object} [options] - The options
    - * @param {string|number[]} [options.color] - HexColor, PercentColor or DecimalColor
    - * @param {string|number[]} [options.stroke] - HexColor, PercentColor or DecimalColor
    - * @param {number} [options.lineWidth] - The line width
    - * @param {number[]} [options.dash] - The dash style [number, number]
    - */
    -exports.line = function line(coordinates = [], options = {}) {
    -    coordinates.forEach((coordinate, index) => {
    -        if (index === 0) {
    -            this.moveTo(coordinate[0], coordinate[1]);
    -            if (this.editingPage) {  // hack to force out first line when editing page
    -                this.lineTo(coordinate[0], coordinate[1], options);
    -            }
    -        } else {
    -            this.lineTo(coordinate[0], coordinate[1], options);
    -        }
    -    });
    -    return this;
    -};
    +            
    /**
    + * move the current position to target position
    + * @name moveTo
    + * @function
    + * @memberof Recipe
    + * @param {number} x - The coordinate x
    + * @param {number} y - The coordinate y
    + */
    +exports.moveTo = function moveTo(x, y) {
    +    const { nx, ny } = this._calibrateCoordinate(x, y);
    +    this._position = {
    +        x: nx,
    +        y: ny
    +    };
    +    return this;
    +};
    +
    +/**
    + * Draw a line from current position
    + * @name lineTo
    + * @function
    + * @memberof Recipe
    + * @param {number} x - The coordinate x
    + * @param {number} y - The coordinate y
    + * @param {Object} [options] - The options
    + * @param {string|number[]} [options.color] - HexColor, PercentColor or DecimalColor
    + * @param {string|number[]} [options.stroke] - HexColor, PercentColor or DecimalColor
    + * @param {number} [options.lineWidth] - The line width
    + * @param {number} [options.opacity] - how transparent should line be, from 0: invisible to 1: opaque
    + * @param {number[]} [options.dash] - The dash pattern [dashSize, gapSize] or [dashAndGapSize]
    + * @param {number} [options.dashPhase] - distance into dash pattern at which to start dash (default: 0, immediately)
    + * @param {string} [options.lineCap] -  open line end style, 'butt', 'round', or 'square' (default: 'round')
    + * @param {string} [options.lineJoin] - joined line end style, 'miter', 'round', or 'bevel' (default: 'round')
    + * @param {number} [options.miterLimit] - limit at which 'miter' joins are forced to 'bevel' (default: 1.414)
    + * 
    + */
    +exports.lineTo = function lineTo(x, y, options = {}) {
    +    const fromX = this._position.x;
    +    const fromY = this._position.y;
    +    const { nx, ny } = this._calibrateCoordinate(x, y);
    +    const context = this.pageContext;
    +    const pathOptions = this._getPathOptions(options);
    +    pathOptions.type = 'stroke';
    +
    +    if (pathOptions.stroke !== undefined) {
    +        pathOptions.color = pathOptions.stroke;
    +        pathOptions.colorspace = pathOptions.strokeModel.colorspace;
    +    }
    +
    +    context
    +        .q()
    +        .J(pathOptions.lineCap)
    +        .j(pathOptions.lineJoin)
    +        .d(pathOptions.dash, pathOptions.dashPhase)
    +        .M(pathOptions.miterLimit)
    +        .drawPath(fromX, fromY, nx, ny, pathOptions)
    +        .Q();
    +    this.moveTo(x, y);
    +    return this;
    +};
    +
    +/**
    + * Draw a line
    + * @name line
    + * @function
    + * @memberof Recipe
    + * @param {number[]} coordinates - The array of coordinate [[x,y], [m,n]]
    + * @param {Object} [options] - The options
    + * @param {string|number[]} [options.color] - HexColor, PercentColor or DecimalColor
    + * @param {string|number[]} [options.stroke] - HexColor, PercentColor or DecimalColor
    + * @param {number} [options.lineWidth] - The line width
    + * @param {number} [options.opacity] - how transparent should line be, from 0: invisible to 1: opaque
    + * @param {number[]} [options.dash] - The dash pattern [dashSize, gapSize] or [dashAndGapSize]
    + * @param {number} [options.dashPhase] - distance into dash pattern at which to start dash (default: 0, immediately)
    + * @param {string} [options.lineCap] -  open line end style, 'butt', 'round', or 'square' (default: 'round')
    + * @param {string} [options.lineJoin] - joined line end style, 'miter', 'round', or 'bevel' (default: 'round')
    + * @param {number} [options.miterLimit] - limit at which 'miter' joins are forced to 'bevel' (default: 1.414)*/
    +exports.line = function line(coordinates = [], options = {}) {
    +    coordinates.forEach((coordinate, index) => {
    +        if (index === 0) {
    +            this.moveTo(coordinate[0], coordinate[1]);
    +            if (this.editingPage) {  // hack to force out first line when editing page
    +                this.lineTo(coordinate[0], coordinate[1], options);
    +            }
    +        } else {
    +            this.lineTo(coordinate[0], coordinate[1], options);
    +        }
    +    });
    +    return this;
    +};
     
    @@ -130,7 +139,7 @@

    vector-line.js


    - Documentation generated by JSDoc 3.6.3 on Wed Oct 02 2019 12:00:26 GMT-0700 (Pacific Daylight Time) using the docdash theme. + Documentation generated by JSDoc 3.5.5 on Fri Nov 01 2019 20:41:12 GMT-0400 (Eastern Daylight Time) using the docdash theme.
    diff --git a/docs/vector-polygon.js.html b/docs/vector-polygon.js.html index dd4a365..56315f9 100644 --- a/docs/vector-polygon.js.html +++ b/docs/vector-polygon.js.html @@ -37,116 +37,119 @@

    vector-polygon.js

    -
    //  Table indicating how to specify coloration of elements
    -//  -------------------------------------------------------------------
    -// |Color | HexColor   | DecimalColor                   | PercentColor |
    -// |Space | (string)   | (array)                        | (string)     |
    -// |------+------------+--------------------------------+--------------|
    -// | Gray | #GG        | [gray]                         | %G           |
    -// |  RGB | #rrggbb    | [red, green, blue]             | %r,g,b       |
    -// | CMYK | #ccmmyykk  | [cyan, magenta, yellow, black] | %c,m,y,k     |
    -//  -------------------------------------------------------------------
    -//
    -//   HexColor component values (two hex digits) range from 00 to FF.
    -//   DecimalColor component values range from 0 to 255.
    -//   PercentColor component values range from 1 to 100.
    -
    -/**
    - * Draw a polygon
    - * @name polygon
    - * @function
    - * @memberof Recipe
    - * @param {number[]} coordinates - The array of coordinate [[x,y], [m,n]]
    - * @param {Object} [options] - The options
    - * @param {string|number[]} [options.color] - HexColor, PercentColor or DecimalColor
    - * @param {string|number[]} [options.stroke] - HexColor, PercentColor or DecimalColor
    - * @param {string|number[]} [options.fill] - HexColor, PercentColor or DecimalColor
    - * @param {number} [options.lineWidth] - The line width
    - * @param {number} [options.opacity] - The opacity
    - * @param {number[]} [options.dash] - The dash style [number, number]
    - * @param {number} [options.rotation] - Accept: +/- 0 through 360. Default: 0
    - * @param {number[]} [options.rotationOrigin] - [originX, originY] Default: x, y
    - */
    -exports.polygon = function polygon(coordinates = [], options = {}) {
    -    // close polygon
    -    if (this._getDistance(coordinates[0], coordinates[coordinates.length - 1]) != 0) {
    -        coordinates.push(coordinates[0]);
    -    }
    -    let boundBox = [ /*minX, minY, maxX, maxY*/ ];
    -    coordinates.forEach((coord, index) => {
    -        if (index === 0) {
    -            boundBox = [coord[0], coord[1], coord[0], coord[1]];
    -        }
    -        boundBox[0] = (boundBox[0] > coord[0]) ? coord[0] : boundBox[0];
    -        boundBox[1] = (boundBox[1] > coord[1]) ? coord[1] : boundBox[1];
    -        boundBox[2] = (boundBox[2] < coord[0]) ? coord[0] : boundBox[2];
    -        boundBox[3] = (boundBox[3] < coord[1]) ? coord[1] : boundBox[3];
    -    });
    -
    -    const pathOptions = this._getPathOptions(options);
    -    let colorModel = pathOptions.colorModel;
    -    const margin = pathOptions.width * 2;
    -    const width  = Math.abs(boundBox[2] - boundBox[0]) + margin * 2;
    -    const height = Math.abs(boundBox[3] - boundBox[1]) + margin * 2;
    -    const startX = boundBox[0] - margin;
    -    const startY = boundBox[1] + height - margin;
    -    const { nx, ny } = this._calibrateCoordinate(startX, startY);
    -
    -    const drawPolygon = (context, coordinates) => {
    -        coordinates.forEach((coord, index) => {
    -            let nx = coord[0];
    -            let ny = coord[1];
    -            if (index === 0) {
    -                context.m(nx - startX,  startY -ny );
    -            } else {
    -                context.l(nx - startX,  startY -ny);
    -            }
    -        });
    -    };
    -
    -    const setPathOptions = (context) => {
    -        context
    -            .J(1)
    -            .j(1)
    -            .d(pathOptions.dash, pathOptions.dashPhase)
    -            .M(1.414);
    -    };
    -
    -    pathOptions.originX = nx + width/2;  // default rotation point
    -    pathOptions.originY = ny + height/2;
    -
    -    if (options.fill) {
    -
    -        if (pathOptions.fill !== undefined) {
    -            colorModel = pathOptions.fillModel;
    -        }
    -
    -        this._drawObject(this, nx, ny, width, height, pathOptions, (ctx, xObject) => {
    -            ctx.gs(xObject.getGsName(pathOptions.fillGsId)),
    -            setPathOptions(ctx);
    -            xObject.fill(colorModel);
    -            drawPolygon(ctx, coordinates);
    -            ctx.f();
    -        });
    -    }
    -
    -    if (options.stroke || options.color || !options.fill) {
    -
    -        if (pathOptions.stroke !== undefined) {
    -            colorModel = pathOptions.strokeModel;
    -        }
    -
    -        this._drawObject(this, nx, ny, width, height, pathOptions, (ctx, xObject) => {
    -            ctx.w(pathOptions.width);
    -            setPathOptions(ctx);
    -            xObject.stroke(colorModel);
    -            drawPolygon(ctx, coordinates);
    -            ctx.s();
    -        });
    -    }
    -
    -    return this;
    -};
    +            
    //  Table indicating how to specify coloration of elements
    +//  -------------------------------------------------------------------
    +// |Color | HexColor   | DecimalColor                   | PercentColor |
    +// |Space | (string)   | (array)                        | (string)     |
    +// |------+------------+--------------------------------+--------------|
    +// | Gray | #GG        | [gray]                         | %G           |
    +// |  RGB | #rrggbb    | [red, green, blue]             | %r,g,b       |
    +// | CMYK | #ccmmyykk  | [cyan, magenta, yellow, black] | %c,m,y,k     |
    +//  -------------------------------------------------------------------
    +//
    +//   HexColor component values (two hex digits) range from 00 to FF.
    +//   DecimalColor component values range from 0 to 255.
    +//   PercentColor component values range from 1 to 100.
    +
    +/**
    + * Draw a polygon
    + * @name polygon
    + * @function
    + * @memberof Recipe
    + * @param {number[]} coordinates - The array of coordinate [[x,y], [m,n]]
    + * @param {Object} [options] - The options
    + * @param {string|number[]} [options.color] - HexColor, PercentColor or DecimalColor
    + * @param {string|number[]} [options.stroke] - HexColor, PercentColor or DecimalColor
    + * @param {string|number[]} [options.fill] - HexColor, PercentColor or DecimalColor
    + * @param {number} [options.lineWidth] - The line width
    + * @param {number} [options.opacity] - The opacity
    + * @param {number[]} [options.dash] - The dash pattern [dashSize, gapSize] or [dashAndGapSize]
    + * @param {number} [options.dashPhase] - distance into dash pattern at which to start dash (default: 0, immediately)
    + * @param {number} [options.rotation] - Accept: +/- 0 through 360. Default: 0
    + * @param {number[]} [options.rotationOrigin] - [originX, originY] Default: x, y
    + * @param {string} [options.lineCap] -  open line end style, 'butt', 'round', or 'square' (default: 'round')
    + * @param {string} [options.lineJoin] - joined line end style, 'miter', 'round', or 'bevel' (default: 'round')
    + * @param {number} [options.miterLimit] - limit at which 'miter' joins are forced to 'bevel' (default: 1.414) */
    +exports.polygon = function polygon(coordinates = [], options = {}) {
    +    // close polygon
    +    if (this._getDistance(coordinates[0], coordinates[coordinates.length - 1]) != 0) {
    +        coordinates.push(coordinates[0]);
    +    }
    +    let boundBox = [ /*minX, minY, maxX, maxY*/ ];
    +    coordinates.forEach((coord, index) => {
    +        if (index === 0) {
    +            boundBox = [coord[0], coord[1], coord[0], coord[1]];
    +        }
    +        boundBox[0] = (boundBox[0] > coord[0]) ? coord[0] : boundBox[0];
    +        boundBox[1] = (boundBox[1] > coord[1]) ? coord[1] : boundBox[1];
    +        boundBox[2] = (boundBox[2] < coord[0]) ? coord[0] : boundBox[2];
    +        boundBox[3] = (boundBox[3] < coord[1]) ? coord[1] : boundBox[3];
    +    });
    +
    +    const pathOptions = this._getPathOptions(options);
    +    let colorModel = pathOptions.colorModel;
    +    const margin = pathOptions.width * 2;
    +    const width  = Math.abs(boundBox[2] - boundBox[0]) + margin * 2;
    +    const height = Math.abs(boundBox[3] - boundBox[1]) + margin * 2;
    +    const startX = boundBox[0] - margin;
    +    const startY = boundBox[1] + height - margin;
    +    const { nx, ny } = this._calibrateCoordinate(startX, startY);
    +
    +    const drawPolygon = (context, coordinates) => {
    +        coordinates.forEach((coord, index) => {
    +            let nx = coord[0];
    +            let ny = coord[1];
    +            if (index === 0) {
    +                context.m(nx - startX,  startY -ny );
    +            } else {
    +                context.l(nx - startX,  startY -ny);
    +            }
    +        });
    +    };
    +
    +    const setPathOptions = (context) => {
    +        context
    +            .J(pathOptions.lineCap)
    +            .j(pathOptions.lineJoin)
    +            .d(pathOptions.dash, pathOptions.dashPhase)
    +            .M(pathOptions.miterLimit);
    +    };
    +
    +    pathOptions.originX = nx + width/2;  // default rotation point
    +    pathOptions.originY = ny + height/2;
    +
    +    if (options.fill) {
    +
    +        if (pathOptions.fill !== undefined) {
    +            colorModel = pathOptions.fillModel;
    +        }
    +
    +        this._drawObject(this, nx, ny, width, height, pathOptions, (ctx, xObject) => {
    +            ctx.gs(xObject.getGsName(pathOptions.fillGsId)),
    +            setPathOptions(ctx);
    +            xObject.fill(colorModel);
    +            drawPolygon(ctx, coordinates);
    +            ctx.f();
    +        });
    +    }
    +
    +    if (options.stroke || options.color || !options.fill) {
    +
    +        if (pathOptions.stroke !== undefined) {
    +            colorModel = pathOptions.strokeModel;
    +        }
    +
    +        this._drawObject(this, nx, ny, width, height, pathOptions, (ctx, xObject) => {
    +            ctx.w(pathOptions.width);
    +            setPathOptions(ctx);
    +            xObject.stroke(colorModel);
    +            drawPolygon(ctx, coordinates);
    +            ctx.s();
    +        });
    +    }
    +
    +    return this;
    +};
     
    @@ -159,7 +162,7 @@

    vector-polygon.js


    - Documentation generated by JSDoc 3.6.3 on Wed Oct 02 2019 12:00:26 GMT-0700 (Pacific Daylight Time) using the docdash theme. + Documentation generated by JSDoc 3.5.5 on Fri Nov 01 2019 20:41:12 GMT-0400 (Eastern Daylight Time) using the docdash theme.
    diff --git a/docs/vector.js.html b/docs/vector.js.html index 43f6a90..45c971c 100644 --- a/docs/vector.js.html +++ b/docs/vector.js.html @@ -37,312 +37,313 @@

    vector.js

    -
    //  Table indicating how to specify coloration of elements
    -//  -------------------------------------------------------------------
    -// |Color | HexColor   | DecimalColor                   | PercentColor |
    -// |Space | (string)   | (array)                        | (string)     |
    -// |------+------------+--------------------------------+--------------|
    -// | Gray | #GG        | [gray]                         | %G           |
    -// |  RGB | #rrggbb    | [red, green, blue]             | %r,g,b       |
    -// | CMYK | #ccmmyykk  | [cyan, magenta, yellow, black] | %c,m,y,k     |
    -//  -------------------------------------------------------------------
    -//
    -//   HexColor component values (two hex digits) range from 00 to FF.
    -//   DecimalColor component values range from 0 to 255.
    -//   PercentColor component values range from 1 to 100.
    -
    -/**
    - * Draw a circle
    - * @name circle
    - * @function
    - * @memberof Recipe
    - * @param {number} x - The coordinate x
    - * @param {number} y - The coordinate y
    - * @param {number} radius - The radius
    - * @param {Object} [options] - The options
    - * @param {string|number[]} [options.color] - HexColor, PercentColor or DecimalColor
    - * @param {string|number[]} [options.stroke] - HexColor, PercentColor or DecimalColor
    - * @param {string|number[]}[ options.fill] - HexColor, PercentColor or DecimalColor
    - * @param {number} [options.lineWidth] - The line width
    - * @param {number} [options.opacity] - The opacity
    - * @param {number[]} [options.dash] - The dash style [number, number]
    - */
    -exports.circle = function circle(x, y, radius, options = {}) {
    -    const {
    -        nx,
    -        ny
    -    } = this._calibrateCoordinate(x, y);
    -    const diameter = radius * 2;
    -
    -    if (options.fill) {
    -        const pathOptions = this._getPathOptions(options, nx, ny);
    -        pathOptions.type = 'fill';
    -
    -        if (pathOptions.fill !== undefined) {
    -            pathOptions.color = pathOptions.fill;
    -            pathOptions.colorspace = pathOptions.fillModel.colorspace;
    -        }
    -
    -        this._drawObject(this, nx - radius, ny - radius, diameter, diameter, pathOptions, (ctx, xObject) => {
    -            ctx
    -                .gs(xObject.getGsName(pathOptions.fillGsId))
    -                .drawCircle(radius, radius, radius, pathOptions);
    -        });
    -    }
    -    if (options.stroke || options.color || !options.fill) {
    -        const pathOptions = this._getPathOptions(options);
    -        pathOptions.type = 'stroke';
    -
    -        if (pathOptions.stroke !== undefined) {
    -            pathOptions.color = pathOptions.stroke;
    -            pathOptions.colorspace = pathOptions.strokeModel.colorspace;
    -        }
    -
    -        // To honor the given width and height of the enclosing square ...
    -
    -        this._drawObject(this, nx - radius, ny - radius, diameter, diameter, pathOptions, (ctx) => {
    -            ctx
    -                .d(pathOptions.dash, pathOptions.dashPhase)
    -                .drawCircle(radius, radius, radius - pathOptions.width / 2, pathOptions);
    -
    -            // ... requires adjusting the internal drawing to accomodate line thickness.
    -        });
    -    }
    -    return this;
    -};
    -
    -/**
    - * Draw a rectangle
    - * @name rectangle
    - * @function
    - * @memberof Recipe
    - * @param {number} x - The coordinate x
    - * @param {number} y - The coordinate y
    - * @param {number} width - The width
    - * @param {number} height - The height
    - * @param {Object} [options] - The options
    - * @param {string|number[]} [options.color] - HexColor, PercentColor or DecimalColor
    - * @param {string|number[]} [options.stroke] - HexColor, PercentColor or DecimalColor
    - * @param {string|number[]} [options.fill] - HexColor, PercentColor or DecimalColor
    - * @param {number} [options.lineWidth] - The line width
    - * @param {number} [options.opacity] - The opacity
    - * @param {number[]} [options.dash] - The dash style [number, number]
    - * @param {number} [options.rotation] - Accept: +/- 0 through 360. Default: 0
    - * @param {number[]} [options.rotationOrigin] - [originX, originY] Default: x, y
    - */
    -exports.rectangle = function rectangle(x, y, width, height, options = {}) {
    -    const { nx, ny } = (options.useGivenCoords) ? { nx: x, ny: y } : this._calibrateCoordinate(x, y, 0, -height);
    -
    -    const pathOptions = this._getPathOptions(options, nx, ny);
    -    let colorModel = pathOptions.colorModel;
    -
    -    if (options.fill) {
    -        pathOptions.type = 'fill';
    -
    -        if (pathOptions.fill !== undefined) {
    -            pathOptions.color = pathOptions.fill;
    -            pathOptions.colorspace = pathOptions.fillModel.colorspace;
    -            colorModel = pathOptions.fillModel;
    -        }
    -
    -        this._drawObject(this, nx, ny, width, height, pathOptions, (ctx, xObject) => {
    -            ctx.gs(xObject.getGsName(pathOptions.fillGsId));
    -
    -            if (options.borderRadius) {
    -                xObject.fill(colorModel);
    -                drawRoundedRectangle(ctx, 0, 0, width, height, options.borderRadius);
    -                ctx.f();
    -            } else {
    -                ctx.drawRectangle(0, 0, width, height, pathOptions);
    -            }
    -        });
    -    }
    -
    -    if (options.stroke || options.color || !options.fill) {
    -        pathOptions.type = 'stroke';
    -
    -        if (pathOptions.stroke !== undefined) {
    -            pathOptions.color = pathOptions.stroke;
    -            pathOptions.colorspace = pathOptions.strokeModel.colorspace;
    -            colorModel = pathOptions.strokeModel;
    -        }
    -
    -        // To honor the given width and height of the rectangle ...
    -
    -        this._drawObject(this, nx, ny, width, height, pathOptions, (ctx, xObject) => {
    -
    -            // ... requires adjusting the internal drawing to accomodate line thickness.
    -            const margin = pathOptions.width;
    -
    -            if (options.borderRadius) {
    -                xObject.stroke(colorModel);
    -                ctx
    -                    .w(pathOptions.width)
    -                    .d(pathOptions.dash, pathOptions.dashPhase);
    -
    -                drawRoundedRectangle(ctx, margin / 2, margin / 2, width - margin, height - margin, options.borderRadius);
    -                ctx.S();
    -            } else {
    -                ctx
    -                    .d(pathOptions.dash, pathOptions.dashPhase)
    -                    .drawRectangle(margin / 2, margin / 2, width - margin, height - margin, pathOptions);
    -            }
    -        });
    -    }
    -
    -    return this;
    -};
    -
    -function drawRoundedRectangle(ctx, left, bottom, width, height, radii) {
    -    let radius = [];
    -
    -    // populate radius array accordingly.
    -    // Missing element value comes from opposite corner.
    -    if (typeof radii === 'number') {
    -        radius = new Array(4).fill(radii);
    -    } else if (Array.isArray(radii)) {
    -        switch (radii.length) {
    -            case 1:
    -                radius = new Array(4).fill(radii[0]);
    -                break;
    -            case 2:
    -                radius = radii.slice(0);
    -                radius[2] = radii[0];
    -                radius[3] = radii[1];
    -                break;
    -            case 3:
    -                radius = radii.slice(0);
    -                radius[3] = radii[1];
    -                break;
    -            case 4:
    -                radius = radii;
    -                break;
    -        }
    -    }
    -    const K = 0.551784;
    -    const right = left + width;
    -    const top = bottom + height;
    -    ctx
    -        .m(left, top - radius[0]) // top-left
    -        .c(left, top - radius[0] * (1 - K), left + radius[0] * (1 - K), top, left + radius[0], top)
    -
    -        .l(right - radius[1], top) // top-right
    -        .c(right - radius[1] * (1 - K), top, right, top - radius[1] * (1 - K), right, top - radius[1])
    -
    -        .l(right, bottom + radius[2]) // bottom-right
    -        .c(right, bottom + radius[2] * (1 - K), right - radius[2] * (1 - K), bottom, right - radius[2], bottom)
    -
    -        .l(left + radius[3], bottom) // bottom-left
    -        .c(left + radius[3] * (1 - K), bottom, left, bottom + radius[3] * (1 - K), left, bottom + radius[3])
    -
    -        .l(left, top - radius[0]); // back to top-left
    -}
    -
    -/**
    - * Draw an ellipse
    - * @name ellipse
    - * @function
    - * @memberof Recipe
    - * @param {number} cx x-coordinate of center point of ellipse
    - * @param {number} cy y-coordinate of center point of ellipse
    - * @param {number} rx radius length from the center point along x-axis
    - * @param {number} ry radius length from the center point along y-axis
    - * @param {Object} options
    - * @param {string|number[]} [options.color] - HexColor, PercentColor or DecimalColor
    - * @param {string|number[]} [options.stroke] - HexColor, PercentColor or DecimalColor
    - * @param {string|number[]}[ options.fill] - HexColor, PercentColor or DecimalColor
    - * @param {number} [options.lineWidth] - The line width
    - * @param {number} [options.opacity] - The opacity
    - * @param {number[]} [options.dash] - The dash style [number, number]
    - * @param {number} [options.rotation] - Accept: +/- 0 through 360. Default: 0
    - * @param {number[]} [options.rotationOrigin] - [originX, originY] Default: x, y
    - */
    -exports.ellipse = function ellipse(cx, cy, rx, ry, options = {}) {
    -    const {
    -        nx,
    -        ny
    -    } = this._calibrateCoordinate(cx, cy);
    -
    -    const pathOptions = this._getPathOptions(options, nx, ny);
    -    let colorModel = pathOptions.colorModel;
    -
    -    const width = rx * 2;
    -    const height = ry * 2;
    -
    -    const drawEllipse = (ctx, x, y, w, h) => {
    -        const magic = 0.551784; // from https://www.tinaja.com/glib/ellipse4.pdf
    -        const ox = rx * magic; // control point offset horizontal
    -        const oy = ry * magic; // control point offset horizontal
    -        const xe = x + w; // x-end, opposite corner from origin
    -        const ye = y + h; // y-end, opposite corner from origin
    -        const xm = rx; // x-middle of enclosing rectangle
    -        const ym = ry; // y-middle of enclosing rectangle
    -
    -        ctx
    -            .m(x, ym)
    -            .c(x, ym - oy, xm - ox, y, xm, y)
    -            .c(xm + ox, y, xe, ym - oy, xe, ym)
    -            .c(xe, ym + oy, xm + ox, ye, xm, ye)
    -            .c(xm - ox, ye, x, ym + oy, x, ym);
    -    };
    -
    -    if (options.fill) {
    -
    -        if (pathOptions.fill !== undefined) {
    -            colorModel = pathOptions.fillModel;
    -        }
    -
    -
    -        this._drawObject(this, nx - rx, ny - ry, width, height, pathOptions, (ctx, xObject) => {
    -            ctx.gs(xObject.getGsName(pathOptions.fillGsId));
    -            xObject.fill(colorModel);
    -            drawEllipse(ctx, 0, 0, width, height);
    -            ctx.f();
    -        });
    -    }
    -
    -    if (options.stroke || options.color || !options.fill) {
    -
    -        if (pathOptions.stroke !== undefined) {
    -            colorModel = pathOptions.strokeModel;
    -        }
    -
    -        // To honor the given width and height of the enclosing rectangle ...
    -
    -        this._drawObject(this, nx - rx, ny - ry, width, height, pathOptions, (ctx, xObject) => {
    -            const margin = pathOptions.width / 2;
    -            xObject.stroke(colorModel);
    -            ctx
    -                .w(pathOptions.width)
    -                .d(pathOptions.dash, pathOptions.dashPhase);
    -
    -            // ... requires adjusting the internal drawing to accomodate line thickness.
    -            drawEllipse(ctx, margin, margin, width - pathOptions.width, height - pathOptions.width);
    -            ctx.S();
    -        });
    -    }
    -    return this;
    -};
    -
    -exports.lineWidth = function lineWidth() {
    -    return this;
    -};
    -
    -exports.fillOpacity = function fillOpacity() {
    -    return this;
    -};
    -
    -exports.fill = function fill() {
    -    return this;
    -};
    -
    -exports.stroke = function stroke() {
    -    return this;
    -};
    -
    -exports.fillAndStroke = function fillAndStroke() {
    -    return this;
    -};
    +            
    //  Table indicating how to specify coloration of elements
    +//  -------------------------------------------------------------------
    +// |Color | HexColor   | DecimalColor                   | PercentColor |
    +// |Space | (string)   | (array)                        | (string)     |
    +// |------+------------+--------------------------------+--------------|
    +// | Gray | #GG        | [gray]                         | %G           |
    +// |  RGB | #rrggbb    | [red, green, blue]             | %r,g,b       |
    +// | CMYK | #ccmmyykk  | [cyan, magenta, yellow, black] | %c,m,y,k     |
    +//  -------------------------------------------------------------------
    +//
    +//   HexColor component values (two hex digits) range from 00 to FF.
    +//   DecimalColor component values range from 0 to 255.
    +//   PercentColor component values range from 1 to 100.
    +
    +/**
    + * Draw a circle
    + * @name circle
    + * @function
    + * @memberof Recipe
    + * @param {number} x - The coordinate x
    + * @param {number} y - The coordinate y
    + * @param {number} radius - The radius
    + * @param {Object} [options] - The options
    + * @param {string|number[]} [options.color] - HexColor, PercentColor or DecimalColor
    + * @param {string|number[]} [options.stroke] - HexColor, PercentColor or DecimalColor
    + * @param {string|number[]}[ options.fill] - HexColor, PercentColor or DecimalColor
    + * @param {number} [options.lineWidth] - The line width
    + * @param {number} [options.opacity] - The opacity
    + * @param {number[]} [options.dash] - The dash style [number, number]
    + */
    +exports.circle = function circle(x, y, radius, options = {}) {
    +    const {
    +        nx,
    +        ny
    +    } = this._calibrateCoordinate(x, y);
    +    const diameter = radius * 2;
    +
    +    if (options.fill) {
    +        const pathOptions = this._getPathOptions(options, nx, ny);
    +        pathOptions.type = 'fill';
    +
    +        if (pathOptions.fill !== undefined) {
    +            pathOptions.color = pathOptions.fill;
    +            pathOptions.colorspace = pathOptions.fillModel.colorspace;
    +        }
    +
    +        this._drawObject(this, nx - radius, ny - radius, diameter, diameter, pathOptions, (ctx, xObject) => {
    +            ctx
    +                .gs(xObject.getGsName(pathOptions.fillGsId))
    +                .drawCircle(radius, radius, radius, pathOptions);
    +        });
    +    }
    +    if (options.stroke || options.color || !options.fill) {
    +        const pathOptions = this._getPathOptions(options);
    +        pathOptions.type = 'stroke';
    +
    +        if (pathOptions.stroke !== undefined) {
    +            pathOptions.color = pathOptions.stroke;
    +            pathOptions.colorspace = pathOptions.strokeModel.colorspace;
    +        }
    +
    +        // To honor the given width and height of the enclosing square ...
    +
    +        this._drawObject(this, nx - radius, ny - radius, diameter, diameter, pathOptions, (ctx) => {
    +            ctx
    +                .d(pathOptions.dash, pathOptions.dashPhase)
    +                .drawCircle(radius, radius, radius - pathOptions.width / 2, pathOptions);
    +
    +            // ... requires adjusting the internal drawing to accomodate line thickness.
    +        });
    +    }
    +    return this;
    +};
    +
    +/**
    + * Draw a rectangle
    + * @name rectangle
    + * @function
    + * @memberof Recipe
    + * @param {number} x - The coordinate x
    + * @param {number} y - The coordinate y
    + * @param {number} width - The width
    + * @param {number} height - The height
    + * @param {Object} [options] - The options
    + * @param {string|number[]} [options.color] - HexColor, PercentColor or DecimalColor
    + * @param {string|number[]} [options.stroke] - HexColor, PercentColor or DecimalColor
    + * @param {string|number[]} [options.fill] - HexColor, PercentColor or DecimalColor
    + * @param {number} [options.lineWidth] - The line width
    + * @param {number} [options.opacity] - The opacity
    + * @param {number[]} [options.dash] - The dash style [number, number]
    + * @param {number} [options.rotation] - Accept: +/- 0 through 360. Default: 0
    + * @param {number[]} [options.rotationOrigin] - [originX, originY] Default: x, y
    + */
    +exports.rectangle = function rectangle(x, y, width, height, options = {}) {
    +    const { nx, ny } = (options.useGivenCoords) ? { nx: x, ny: y } : this._calibrateCoordinate(x, y, 0, -height);
    +
    +    const pathOptions = this._getPathOptions(options, nx, ny);
    +    let colorModel = pathOptions.colorModel;
    +    pathOptions.useGivenCoords = options.useGivenCoords;
    +
    +    if (options.fill) {
    +        pathOptions.type = 'fill';
    +
    +        if (pathOptions.fill !== undefined) {
    +            pathOptions.color = pathOptions.fill;
    +            pathOptions.colorspace = pathOptions.fillModel.colorspace;
    +            colorModel = pathOptions.fillModel;
    +        }
    +
    +        this._drawObject(this, nx, ny, width, height, pathOptions, (ctx, xObject) => {
    +            ctx.gs(xObject.getGsName(pathOptions.fillGsId));
    +
    +            if (options.borderRadius) {
    +                xObject.fill(colorModel);
    +                drawRoundedRectangle(ctx, 0, 0, width, height, options.borderRadius);
    +                ctx.f();
    +            } else {
    +                ctx.drawRectangle(0, 0, width, height, pathOptions);
    +            }
    +        });
    +    }
    +
    +    if (options.stroke || options.color || !options.fill) {
    +        pathOptions.type = 'stroke';
    +
    +        if (pathOptions.stroke !== undefined) {
    +            pathOptions.color = pathOptions.stroke;
    +            pathOptions.colorspace = pathOptions.strokeModel.colorspace;
    +            colorModel = pathOptions.strokeModel;
    +        }
    +
    +        // To honor the given width and height of the rectangle ...
    +
    +        this._drawObject(this, nx, ny, width, height, pathOptions, (ctx, xObject) => {
    +
    +            // ... requires adjusting the internal drawing to accomodate line thickness.
    +            const margin = pathOptions.width;
    +
    +            if (options.borderRadius) {
    +                xObject.stroke(colorModel);
    +                ctx
    +                    .w(pathOptions.width)
    +                    .d(pathOptions.dash, pathOptions.dashPhase);
    +
    +                drawRoundedRectangle(ctx, margin / 2, margin / 2, width - margin, height - margin, options.borderRadius);
    +                ctx.S();
    +            } else {
    +                ctx
    +                    .d(pathOptions.dash, pathOptions.dashPhase)
    +                    .drawRectangle(margin / 2, margin / 2, width - margin, height - margin, pathOptions);
    +            }
    +        });
    +    }
    +
    +    return this;
    +};
    +
    +function drawRoundedRectangle(ctx, left, bottom, width, height, radii) {
    +    let radius = [];
    +
    +    // populate radius array accordingly.
    +    // Missing element value comes from opposite corner.
    +    if (typeof radii === 'number') {
    +        radius = new Array(4).fill(radii);
    +    } else if (Array.isArray(radii)) {
    +        switch (radii.length) {
    +            case 1:
    +                radius = new Array(4).fill(radii[0]);
    +                break;
    +            case 2:
    +                radius = radii.slice(0);
    +                radius[2] = radii[0];
    +                radius[3] = radii[1];
    +                break;
    +            case 3:
    +                radius = radii.slice(0);
    +                radius[3] = radii[1];
    +                break;
    +            case 4:
    +                radius = radii;
    +                break;
    +        }
    +    }
    +    const K = 0.551784;
    +    const right = left + width;
    +    const top = bottom + height;
    +    ctx
    +        .m(left, top - radius[0]) // top-left
    +        .c(left, top - radius[0] * (1 - K), left + radius[0] * (1 - K), top, left + radius[0], top)
    +
    +        .l(right - radius[1], top) // top-right
    +        .c(right - radius[1] * (1 - K), top, right, top - radius[1] * (1 - K), right, top - radius[1])
    +
    +        .l(right, bottom + radius[2]) // bottom-right
    +        .c(right, bottom + radius[2] * (1 - K), right - radius[2] * (1 - K), bottom, right - radius[2], bottom)
    +
    +        .l(left + radius[3], bottom) // bottom-left
    +        .c(left + radius[3] * (1 - K), bottom, left, bottom + radius[3] * (1 - K), left, bottom + radius[3])
    +
    +        .l(left, top - radius[0]); // back to top-left
    +}
    +
    +/**
    + * Draw an ellipse
    + * @name ellipse
    + * @function
    + * @memberof Recipe
    + * @param {number} cx x-coordinate of center point of ellipse
    + * @param {number} cy y-coordinate of center point of ellipse
    + * @param {number} rx radius length from the center point along x-axis
    + * @param {number} ry radius length from the center point along y-axis
    + * @param {Object} options
    + * @param {string|number[]} [options.color] - HexColor, PercentColor or DecimalColor
    + * @param {string|number[]} [options.stroke] - HexColor, PercentColor or DecimalColor
    + * @param {string|number[]}[ options.fill] - HexColor, PercentColor or DecimalColor
    + * @param {number} [options.lineWidth] - The line width
    + * @param {number} [options.opacity] - The opacity
    + * @param {number[]} [options.dash] - The dash style [number, number]
    + * @param {number} [options.rotation] - Accept: +/- 0 through 360. Default: 0
    + * @param {number[]} [options.rotationOrigin] - [originX, originY] Default: x, y
    + */
    +exports.ellipse = function ellipse(cx, cy, rx, ry, options = {}) {
    +    const {
    +        nx,
    +        ny
    +    } = this._calibrateCoordinate(cx, cy);
    +
    +    const pathOptions = this._getPathOptions(options, nx, ny);
    +    let colorModel = pathOptions.colorModel;
    +
    +    const width = rx * 2;
    +    const height = ry * 2;
    +
    +    const drawEllipse = (ctx, x, y, w, h) => {
    +        const magic = 0.551784; // from https://www.tinaja.com/glib/ellipse4.pdf
    +        const ox = rx * magic; // control point offset horizontal
    +        const oy = ry * magic; // control point offset horizontal
    +        const xe = x + w; // x-end, opposite corner from origin
    +        const ye = y + h; // y-end, opposite corner from origin
    +        const xm = rx; // x-middle of enclosing rectangle
    +        const ym = ry; // y-middle of enclosing rectangle
    +
    +        ctx
    +            .m(x, ym)
    +            .c(x, ym - oy, xm - ox, y, xm, y)
    +            .c(xm + ox, y, xe, ym - oy, xe, ym)
    +            .c(xe, ym + oy, xm + ox, ye, xm, ye)
    +            .c(xm - ox, ye, x, ym + oy, x, ym);
    +    };
    +
    +    if (options.fill) {
    +
    +        if (pathOptions.fill !== undefined) {
    +            colorModel = pathOptions.fillModel;
    +        }
    +
    +
    +        this._drawObject(this, nx - rx, ny - ry, width, height, pathOptions, (ctx, xObject) => {
    +            ctx.gs(xObject.getGsName(pathOptions.fillGsId));
    +            xObject.fill(colorModel);
    +            drawEllipse(ctx, 0, 0, width, height);
    +            ctx.f();
    +        });
    +    }
    +
    +    if (options.stroke || options.color || !options.fill) {
    +
    +        if (pathOptions.stroke !== undefined) {
    +            colorModel = pathOptions.strokeModel;
    +        }
    +
    +        // To honor the given width and height of the enclosing rectangle ...
    +
    +        this._drawObject(this, nx - rx, ny - ry, width, height, pathOptions, (ctx, xObject) => {
    +            const margin = pathOptions.width / 2;
    +            xObject.stroke(colorModel);
    +            ctx
    +                .w(pathOptions.width)
    +                .d(pathOptions.dash, pathOptions.dashPhase);
    +
    +            // ... requires adjusting the internal drawing to accomodate line thickness.
    +            drawEllipse(ctx, margin, margin, width - pathOptions.width, height - pathOptions.width);
    +            ctx.S();
    +        });
    +    }
    +    return this;
    +};
    +
    +exports.lineWidth = function lineWidth() {
    +    return this;
    +};
    +
    +exports.fillOpacity = function fillOpacity() {
    +    return this;
    +};
    +
    +exports.fill = function fill() {
    +    return this;
    +};
    +
    +exports.stroke = function stroke() {
    +    return this;
    +};
    +
    +exports.fillAndStroke = function fillAndStroke() {
    +    return this;
    +};
     
    @@ -355,7 +356,7 @@

    vector.js


    - Documentation generated by JSDoc 3.6.3 on Wed Oct 02 2019 12:00:26 GMT-0700 (Pacific Daylight Time) using the docdash theme. + Documentation generated by JSDoc 3.5.5 on Fri Nov 01 2019 20:41:12 GMT-0400 (Eastern Daylight Time) using the docdash theme.
    diff --git a/lib/text.helper.js b/lib/text.helper.js index 516f52b..eea39b5 100644 --- a/lib/text.helper.js +++ b/lib/text.helper.js @@ -2,6 +2,7 @@ exports.Word = class Word { constructor(word, pathOptions) { this._value = word; this._pathOptions = pathOptions; + this._text = (word === ' ') ? 'o' : word; // allows space to get an actual dimension } get value() { @@ -13,7 +14,7 @@ exports.Word = class Word { return this._dimensions; } this._dimensions = this._pathOptions.font.calculateTextDimensions( - this._value, this._pathOptions.size + this._text, this._pathOptions.size ); return this._dimensions; } @@ -36,7 +37,7 @@ exports.Line = class Line { const tempValue = this.value + wordObject.value; const toWidth = this._pathOptions.font.calculateTextDimensions( tempValue, this.size - ).width; + ).xMax; return (toWidth <= this.width); } @@ -45,6 +46,10 @@ exports.Line = class Line { this.addWord(wordObject); } + get words() { + return this.wordObjects; + } + get spaceWidth() { return this._pathOptions.font.calculateTextDimensions( 'o', this.size @@ -67,7 +72,7 @@ exports.Line = class Line { get textWidth() { return this.wordObjects.reduce((width, word) => { - width += word.dimensions.width; + width += word.dimensions.xMax; return width; }, 0); } @@ -88,44 +93,6 @@ exports.Line = class Line { } }; -exports._getTextOffset = function _getTextOffset(text = '', options = {}) { - if (!options.size || !options.font) { - return; - } - let offsetX = 0; - let offsetY = 0; - const textDimensions = options.font.calculateTextDimensions(text, options.size); - if (options.align) { - const alignments = options.align.split(' '); - if (alignments[0]) { - switch (alignments[0]) { - case 'center': - offsetX = -1 * textDimensions.width / 2; - break; - case 'right': - offsetX = textDimensions.width / 2; - break; - default: - } - } - if (alignments[1]) { - switch (alignments[1]) { - case 'center': - offsetY = -1 * textDimensions.yMax / 2; - break; - case 'bottom': - offsetY = textDimensions.yMax / 2; - break; - default: - } - } - } - return { - offsetX, - offsetY - }; -}; - /** * @todo handle page margin and padding */ diff --git a/lib/text.js b/lib/text.js index 6d0ddb6..6498cef 100644 --- a/lib/text.js +++ b/lib/text.js @@ -33,28 +33,32 @@ const xObjectForm = require('./xObjectForm'); * @param {number} y - The coordinate y * @param {Object} [options] - The options * @param {string|number[]} [options.color] - Text color (HexColor, PercentColor or DecimalColor) - * @param {number} [options.opacity] - opacity - * @param {number} [options.rotation] - Accept: +/- 0 through 360. Default: 0 - * @param {number[]} [options.rotationOrigin] - [originX, originY] Default: the text coordinate - * @param {string} [options.font] - The font. 'Arial', 'Helvetica'... - * @param {number} [options.size] - The line width - * @param {string} [options.align] - The alignment. 'center center'... + * @param {number} [options.opacity=1] - opacity + * @param {number} [options.rotation=0] - Accept: +/- 0 through 360. + * @param {number[]} [options.rotationOrigin=[x,y]] - [originX, originY] + * @param {string} [options.font=Helvetica] - The font. 'Arial', 'Helvetica'... + * @param {number} [options.size=14] - The font size + * @param {string} [options.align='left top'] - The alignment. 'center center'... * @param {Object|Boolean} [options.highlight] - Text markup annotation. * @param {Object|Boolean} [options.underline] - Text markup annotation. * @param {Object|Boolean} [options.strikeOut] - Text markup annotation. * @param {Object} [options.textBox] - Text Box to fit in. - * @param {number} [options.textBox.width] - Text Box width + * @param {number} [options.textBox.width=100] - Text Box width * @param {number} [options.textBox.height] - Text Box fixed height - * @param {number} [options.textBox.minHeight] - Text Box minimum height - * @param {number|number[]} [options.textBox.padding] - Text Box padding, [top, right, bottom, left] - * @param {number} [options.textBox.lineHeight] - Text Box line height - * @param {string} [options.textBox.textAlign] - Text alignment inside text box + * @param {number} [options.textBox.minHeight=0] - Text Box minimum height + * @param {number|number[]} [options.textBox.padding=0] - Text Box padding, [top, right, bottom, left] + * @param {number} [options.textBox.lineHeight=0] - Text Box line height + * @param {string|Boolean} [options.textBox.wrap='auto'] - Text wrapping mechanism, may be true, false, + * 'auto', 'clip', 'trim', 'ellipsis'. All the option values that are not equivalent to 'auto' dictate + * how the text which does not fit on a line is to be truncated. True is equivalent to 'auto'. False is equivalent to 'ellipsis'. + * @param {string} [options.textBox.textAlign='left top'] - Alignment inside text box, specified as 'horizontal vertical', + * where horizontal is one of: 'left', 'center', 'right', 'justify' and veritical is one of: 'top', 'center', 'bottom'. * @param {Object} [options.textBox.style] - Text Box styles - * @param {number} [options.textBox.style.lineWidth] - Text Box border width + * @param {number} [options.textBox.style.lineWidth=2] - Text Box border width * @param {string|number[]} [options.textBox.style.stroke] - Text Box border color (HexColor, PercentColor or DecimalColor) - * @param {number[]} [options.textBox.style.dash] - Text Box border border dash style [number, number] + * @param {number[]} [options.textBox.style.dash=[]] - Text Box border border dash style [number, number] * @param {string|number[]} [options.textBox.style.fill] - Text Box border background color (HexColor, PercentColor or DecimalColor) - * @param {number} [options.textBox.style.opacity] - Text Box border background opacity + * @param {number} [options.textBox.style.opacity=1] - Text Box border background opacity */ exports.text = function text(text = '', x, y, options = {}) { if (!this.pageContext) { @@ -86,7 +90,8 @@ exports.text = function text(text = '', x, y, options = {}) { width: null, lineHeight: 0, padding: 0, - minHeight: 0 + minHeight: 0, + wrap: 'auto' } : { width: options.textBox.width || 100, lineHeight: options.textBox.lineHeight, @@ -94,9 +99,17 @@ exports.text = function text(text = '', x, y, options = {}) { padding: (options.textBox.padding || 0), minHeight: options.textBox.minHeight || 0, style: options.textBox.style, - textAlign: options.textBox.textAlign + textAlign: options.textBox.textAlign, + wrap: (options.textBox.wrap !== undefined) ? options.textBox.wrap : 'auto' }; + // allow user to treat wrap as boolean + if (textBox.wrap === true) { + textBox.wrap = 'auto'; + } else if (textBox.wrap === false) { + textBox.wrap = 'ellipsis' + } + // Allows user to enter a single number which will be used for all text box sides, // or an array of values [top, right, bottom, left] with any combination of missing sides. // Default value for a missing side is the value of the text box's opposite side (see below). @@ -116,13 +129,9 @@ exports.text = function text(text = '', x, y, options = {}) { }); let firstLineHeight; - // let lastLineHeight; - let totalHeight = 0; let toWriteTextObjects = []; - // const layerIndex = {}; - const writeValue = (textObject) => { textObject.lineID = textObject.lineID || Date.now() * Math.random(); textObject.lineID = (textObject.needsLineBreaker) ? Date.now() * Math.random() : textObject.lineID; @@ -211,12 +220,14 @@ exports.text = function text(text = '', x, y, options = {}) { ny } = this._calibrateCoordinate(x, y, offsetX, offsetY); + let textYpos = ny; + if (textBox.style) { const textBoxWidth = textBox.width + textBox.paddingLeft + textBox.paddingRight; + const actualTextHeight = textBox.firstLineHeight * (toWriteTextObjects.length-1) + textBox.lastLineHeight if (!textBox.height) { - textBox.height = textBox.firstLineHeight * (toWriteTextObjects.length-1) - + textBox.paddingTop + textBox.paddingBottom + textBox.lastLineHeight; + textBox.height = actualTextHeight + textBox.paddingTop + textBox.paddingBottom; if (textBox.minHeight && textBox.minHeight > textBox.height) { textBox.height = textBox.minHeight; @@ -232,10 +243,21 @@ exports.text = function text(text = '', x, y, options = {}) { rotationOrigin: [pathOptions.originX,pathOptions.originY], }) ); + + // Determine vertical starting position of text within textBox + switch (toWriteTextObjects[0].writeOptions.alignVertical) { + case 'center': + textYpos -= (textBox.height - actualTextHeight) / 2; + break; + case 'bottom': + textYpos -= (textBox.height - actualTextHeight); + break; + } } + const context = this.pageContext; // const space = 8; - let currentY = ny; + let currentY = textYpos; let currentLineID; let currentLineWidth = 0; let toWriteContents = []; @@ -253,43 +275,77 @@ exports.text = function text(text = '', x, y, options = {}) { const isContinued = (currentLineID == lineID) ? true : false; const getStartX = (startX, currentWriteOptions) => { - let x = startX; - let offsetX = 0; - if (currentWriteOptions.align == 'center') { - offsetX = (textBox.width - currentLineWidth) / 2; - } else - if (currentWriteOptions.align == 'right') { - offsetX = (textBox.width - textBox.paddingRight - currentLineWidth); - } else { - offsetX = textBox.paddingLeft; + let offsetX = textBox.paddingLeft; + + switch (currentWriteOptions.alignHorizontal) { + case 'center': + offsetX = (textBox.width - currentLineWidth) / 2; + break; + case 'right': + offsetX = (textBox.width - textBox.paddingRight - currentLineWidth); + break; } - return x + offsetX; + + return startX + offsetX; }; const writeText = (context, x, y, wto) => { const options = wto.writeOptions; - const {lineHeight, lineWidth, text, baseline} = wto; + const {lineHeight, text, baseline, wordsInLine, textWidth} = wto; // write directly to page when not dealing with opacity, rotation and special colorspace. if (options.opacity === 1 && options.colorspace !== 'separation' && (options.rotation === 0 || options.rotation === undefined)) { - context.writeText(text, x, y+baseline, options); + + // Note that the last line of a text box ignores justification. + if (options.alignHorizontal !== 'justify' || wto.lastLine) { + if (textBox.wrap !== 'auto') { // This applies a clipping region around the text + this.pageContext.q(); + this.pageContext + .m(nx,y+lineHeight) + .l(nx+textBox.width,y+lineHeight) + .l(nx+textBox.width,y) + .l(nx,y) + .h().W().n(); + } + + context.writeText(text, x, y+baseline, options); + + if (textBox.wrap !== 'auto') { + this.pageContext.Q(); + } + } else { + justify(x, wordsInLine, textBox, textWidth, (word, xx) => { + context.writeText(word, xx, y+baseline, options); + }); + } } else { this.pauseContext(); // https://github.com/galkahana/HummusJS/wiki/Use-the-pdf-drawing-operators - const xObject = new xObjectForm(this.writer, lineWidth * 1.5, lineHeight); + const xObject = new xObjectForm(this.writer, textBox.width, lineHeight); const xObjectCtx = xObject.getContentContext(); // Build form object xObjectCtx .q() .gs(xObject.getGsName(options.fillGsId)) // set graphic state (here opacity) .BT(); // begin text context + xObjectCtx.Tf(options.font, options.size); // set font xObject.fill(options.colorModel); // set color + + if (options.alignHorizontal !== 'justify' || wto.lastLine) { + xObjectCtx + .Tm(1, 0, 0, 1, 0, baseline) // set position in object + .Tj(text); // write text + } else { + justify(0, wordsInLine, textBox, textWidth, (word, xx) => { + xObjectCtx + .Tm(1, 0, 0, 1, xx, baseline) // set position in object + .Tj(word); // write word + }); + } + xObjectCtx - .Tf(options.font, options.size) // set font - .Tm(1, 0, 0, 1, 0, baseline) // set position in object - .Tj(text) // write text .ET() // end text context .Q(); xObject.end(); @@ -359,6 +415,74 @@ exports.text = function text(text = '', x, y, options = {}) { return this; }; +function justify (x, wordsInLine, textBox, textWidth, callback) { + // For some reason, textWidth is smaller than lineWidth. My suspicions lie in the fact + // that spacing computations appear different depending on where the space is located. + // What is noted though that if lineWidth is used in the calculations for text + // justitification, the text goes passed the right boundary. Due to the vagary in space + // computation, the final wrinkle to make sure the last word in the line smacks up against + // the right side boundary is to perform a special computation on the last word positioning + // relative to that right side bounds. + const spaceCount = wordsInLine.length-1; + const spaceBetweenWords = ( wordsInLine.length > 1) ? (textBox.width - textWidth) / (spaceCount) : 0; + const lineStart = x; + + for (let index = 0; index < wordsInLine.length; index++) { + const word = wordsInLine[index]; + + callback(word.value, x) + // Ready to compute last word spacing? + if (index+1 === spaceCount) { + x = lineStart + textBox.width - textBox.paddingRight - wordsInLine[index+1].dimensions.xMax; + } else { + x += word.dimensions.width + spaceBetweenWords; + } + } +} + +function nextWord (text, brk, previousPosition, pathOptions) { + let nextWord = text.slice(previousPosition, brk.position); + + if (brk.required) { // effectively saw a '\n' in text. + nextWord = nextWord.trim(); + } + + return new Word(nextWord, pathOptions); +} + +function elideNonFittingText(textBox, line, word, pathOptions) { + if (textBox.wrap === 'clip') { + line.addWord(word); + } else if (textBox.wrap === 'ellipsis') { + // This is more complicated than the other no-wrap options. + // It makes an initial attempt to take the word that was + // too big and make it shrink in size until it and the + // ellipsis character fit. If that doesn't work, one more + // attempt is taken by trying to shrink the previous word + // that fit on the line. + const ellipsis = '…'; + let usingPreviousWord = false; + let tooBig = new Word(word.value.slice(0,-2)+ellipsis, pathOptions); + + while ( !line.canFit(tooBig)) { + + if (tooBig.value.length > 1) { + tooBig = new Word(tooBig.value.slice(0,-2)+ellipsis, pathOptions); + + // Try last word that fit in box? + } else if (!usingPreviousWord) { + tooBig = new Word(line.words.pop().value.slice(0,-1) + ellipsis, pathOptions); + usingPreviousWord = true; + + } else { + break; // give up, only get 2 shots at this. + } + } + + line.addWord(tooBig); + } +} + function getToWriteTextObjects(textObject = {}, pathOptions, textBox = {}) { const toWriteTextObjects = []; let text = ((textObject.prependValue) ? textObject.prependValue : '') + @@ -387,66 +511,87 @@ function getToWriteTextObjects(textObject = {}, pathOptions, textBox = {}) { let last = 0; let bk = breaker.nextBreak(); let previousWord; + let flushLine = false; for (let i = 0; i < indent; i++) { - newLine.addWord(new Word(' ')); + newLine.addWord(new Word(' ', pathOptions)); } while (bk) { - let nextWord = text.slice(last, bk.position); - /** - * Author: silverma (Marc Silverman) - * #29 Is it possible to add multi-line text? - * https://github.com/chunyenHuang/hummusRecipe/issues/29 - */ - if (bk.required) { - nextWord = nextWord.trim(); - } + let word = nextWord(text, bk, last, pathOptions); - const word = new Word(nextWord, pathOptions); if (newLine.canFit(word)) { newLine.addWord(word); } else { // remove any trailing space on previous word so right justification works appropriately - if ( previousWord ) { + if ( previousWord && textBox.wrap === 'auto') { newLine.replaceLastWord(new Word(previousWord.value.trim(), pathOptions)); } lines.push(newLine); - newLine = new Line(lineMaxWidth, textBox.lineHeight, size, pathOptions); - for (let i = 0; i < indent; i++) { - newLine.addWord(new Word(' ')); - } - if (textObject.prependValue) { - const space = Array(textObject.prependValue.length + 1).fill(' ').join(''); - newLine.addWord(new Word(space)); + + // now deal with text line wrap (what happens to text that doesn't fit in line) + if (textBox.wrap !== 'auto') { + flushLine = true; + elideNonFittingText(textBox, newLine, word, pathOptions); + + } else { + // this is the auto wrap section + newLine = new Line(lineMaxWidth, textBox.lineHeight, size, pathOptions); + for (let i = 0; i < indent; i++) { + newLine.addWord(new Word(' ', pathOptions)); + } + if (textObject.prependValue) { + const space = Array(textObject.prependValue.length + 1).fill(' ').join(''); + newLine.addWord(new Word(space, pathOptions)); + } + newLine.addWord(word); } - newLine.addWord(word); } - /** - * Author: silverma (Marc Silverman) - * #29 Is it possible to add multi-line text? - * https://github.com/chunyenHuang/hummusRecipe/issues/29 - */ - if (bk.required) { - lines.push(newLine); - newLine = new Line(lineMaxWidth, textBox.lineHeight, size, pathOptions); + if (flushLine) { + while (bk) { + if (bk.required) { + flushLine = false; + newLine = new Line(lineMaxWidth, textBox.lineHeight, size, pathOptions); + word = null; + break; + } + bk = breaker.nextBreak(); + } + } else { + /** + * Author: silverma (Marc Silverman) + * #29 Is it possible to add multi-line text? + * https://github.com/chunyenHuang/hummusRecipe/issues/29 + */ + if (bk.required) { + lines.push(newLine); + newLine = new Line(lineMaxWidth, textBox.lineHeight, size, pathOptions); + } } - previousWord = word; - last = bk.position; - bk = breaker.nextBreak(); + + if (bk) { + previousWord = word; + last = bk.position; + bk = breaker.nextBreak(); + } + } + + if (!flushLine) { + lines.push(newLine); } - lines.push(newLine); const lineHeightMargin = 0; let lineHeight = (textHeight + lineHeightMargin >= textBox.lineHeight) ? textHeight + lineHeightMargin : textBox.lineHeight; + let [alignHorizontal, alignVertical] = (textBox.textAlign) ? textBox.textAlign.split(' ') : []; const writeOptions = Object.assign({}, pathOptions, { color: textObject.styles.color, opacity: parseFloat(textObject.styles.opacity || pathOptions.opacity || 1), underline: textObject.underline || pathOptions.underline, size: textObject.size, - align: textBox.textAlign, - font: (textObject.isBold && textObject.isItalic) ? pathOptions.fonts.boldItalic : (textObject.isItalic) ? pathOptions.fonts.italic : (textObject.isBold) ? pathOptions.fonts.bold : pathOptions.font + alignHorizontal: alignHorizontal, + alignVertical: alignVertical, + font: pathOptions.font }); let paragraphHeight = 0; lines.forEach((line, index) => { @@ -461,7 +606,10 @@ function getToWriteTextObjects(textObject = {}, pathOptions, textBox = {}) { lineHeight, baseline: lineHeight - textBox.baselineHeight, lineWidth: line.currentWidth, + textWidth: line.textWidth, // for justification spaceWidth: (pathOptions.html) ? line.spaceWidth : 0, + wordsInLine: line.words, + lastLine: (index === lines.length-1), writeOptions }); paragraphHeight += lineHeight; diff --git a/lib/vector-line.js b/lib/vector-line.js index d321b0d..0abb171 100644 --- a/lib/vector-line.js +++ b/lib/vector-line.js @@ -25,10 +25,14 @@ exports.moveTo = function moveTo(x, y) { * @param {Object} [options] - The options * @param {string|number[]} [options.color] - HexColor, PercentColor or DecimalColor * @param {string|number[]} [options.stroke] - HexColor, PercentColor or DecimalColor - * @param {string|number[]} [options.fill] - HexColor, PercentColor or DecimalColor * @param {number} [options.lineWidth] - The line width - * @param {number} [options.opacity] - The opacity - * @param {number[]} [options.dash] - The dash style [number, number] + * @param {number} [options.opacity] - how transparent should line be, from 0: invisible to 1: opaque + * @param {number[]} [options.dash] - The dash pattern [dashSize, gapSize] or [dashAndGapSize] + * @param {number} [options.dashPhase] - distance into dash pattern at which to start dash (default: 0, immediately) + * @param {string} [options.lineCap] - open line end style, 'butt', 'round', or 'square' (default: 'round') + * @param {string} [options.lineJoin] - joined line end style, 'miter', 'round', or 'bevel' (default: 'round') + * @param {number} [options.miterLimit] - limit at which 'miter' joins are forced to 'bevel' (default: 1.414) + * */ exports.lineTo = function lineTo(x, y, options = {}) { const fromX = this._position.x; @@ -45,9 +49,10 @@ exports.lineTo = function lineTo(x, y, options = {}) { context .q() - .J(1) - .j(1) + .J(pathOptions.lineCap) + .j(pathOptions.lineJoin) .d(pathOptions.dash, pathOptions.dashPhase) + .M(pathOptions.miterLimit) .drawPath(fromX, fromY, nx, ny, pathOptions) .Q(); this.moveTo(x, y); @@ -64,8 +69,12 @@ exports.lineTo = function lineTo(x, y, options = {}) { * @param {string|number[]} [options.color] - HexColor, PercentColor or DecimalColor * @param {string|number[]} [options.stroke] - HexColor, PercentColor or DecimalColor * @param {number} [options.lineWidth] - The line width - * @param {number[]} [options.dash] - The dash style [number, number] - */ + * @param {number} [options.opacity] - how transparent should line be, from 0: invisible to 1: opaque + * @param {number[]} [options.dash] - The dash pattern [dashSize, gapSize] or [dashAndGapSize] + * @param {number} [options.dashPhase] - distance into dash pattern at which to start dash (default: 0, immediately) + * @param {string} [options.lineCap] - open line end style, 'butt', 'round', or 'square' (default: 'round') + * @param {string} [options.lineJoin] - joined line end style, 'miter', 'round', or 'bevel' (default: 'round') + * @param {number} [options.miterLimit] - limit at which 'miter' joins are forced to 'bevel' (default: 1.414)*/ exports.line = function line(coordinates = [], options = {}) { coordinates.forEach((coordinate, index) => { if (index === 0) { diff --git a/lib/vector-polygon.js b/lib/vector-polygon.js index ce77401..c7b66b4 100644 --- a/lib/vector-polygon.js +++ b/lib/vector-polygon.js @@ -24,10 +24,13 @@ * @param {string|number[]} [options.fill] - HexColor, PercentColor or DecimalColor * @param {number} [options.lineWidth] - The line width * @param {number} [options.opacity] - The opacity - * @param {number[]} [options.dash] - The dash style [number, number] + * @param {number[]} [options.dash] - The dash pattern [dashSize, gapSize] or [dashAndGapSize] + * @param {number} [options.dashPhase] - distance into dash pattern at which to start dash (default: 0, immediately) * @param {number} [options.rotation] - Accept: +/- 0 through 360. Default: 0 * @param {number[]} [options.rotationOrigin] - [originX, originY] Default: x, y - */ + * @param {string} [options.lineCap] - open line end style, 'butt', 'round', or 'square' (default: 'round') + * @param {string} [options.lineJoin] - joined line end style, 'miter', 'round', or 'bevel' (default: 'round') + * @param {number} [options.miterLimit] - limit at which 'miter' joins are forced to 'bevel' (default: 1.414) */ exports.polygon = function polygon(coordinates = [], options = {}) { // close polygon if (this._getDistance(coordinates[0], coordinates[coordinates.length - 1]) != 0) { @@ -67,10 +70,10 @@ exports.polygon = function polygon(coordinates = [], options = {}) { const setPathOptions = (context) => { context - .J(1) - .j(1) + .J(pathOptions.lineCap) + .j(pathOptions.lineJoin) .d(pathOptions.dash, pathOptions.dashPhase) - .M(1.414); + .M(pathOptions.miterLimit); }; pathOptions.originX = nx + width/2; // default rotation point diff --git a/lib/vector.helper.js b/lib/vector.helper.js index fbf5ebc..4eb8a16 100644 --- a/lib/vector.helper.js +++ b/lib/vector.helper.js @@ -21,15 +21,26 @@ exports._getPathOptions = function _getPathOptions(options = {}, originX, origin colorspace, colorName, colorArray: [], + lineCap: this._lineCap(), + lineJoin: this._lineJoin(), + miterLimit: 1.414, width: 2, align: options.align }; + if (options.font) { const matchedFont = this.fonts[options.font.toLowerCase()]; if (matchedFont) { pathOptions.font = this.writer.getFontForFile(matchedFont); } + } else { // default font used, now have to make final decision based on bold/italic considerations. + // Note, if this is not done explicitly, the font dimensions will be incorrect. + pathOptions.font = + (options.bold && options.italic) ? pathOptions.fonts.boldItalic : + (options.italic) ? pathOptions.fonts.italic : + (options.bold) ? pathOptions.fonts.bold : pathOptions.font; } + if (options.opacity == void(0) || isNaN(options.opacity)) { options.opacity = 1; } else { @@ -86,13 +97,28 @@ exports._getPathOptions = function _getPathOptions(options = {}, originX, origin pathOptions.skewY = options.skewY; } - // Page 127 + // Page 127 of PDF 1.7 specification pathOptions.dash = (Array.isArray(options.dash)) ? options.dash : []; pathOptions.dashPhase = (!isNaN(options.dashPhase)) ? options.dashPhase : 0; if (pathOptions.dash[0] == 0 && pathOptions.dash[1] == 0) { - pathOptions.dash = []; + pathOptions.dash = []; // no dash, solid unbroken line pathOptions.dashPhase = 0; } + + // Page 125-126 of PDF 1.7 specification + if (options.lineJoin !== void(0)) { + pathOptions.lineJoin = this._lineJoin(options.lineJoin); + } + if (options.lineCap !== void(0)) { + pathOptions.lineCap = this._lineCap(options.lineCap); + } + + if (options.miterLimit !== void(0)) { + if (!isNaN(options.miterLimit)) { + pathOptions.miterLimit = options.miterLimit; + } + } + return pathOptions; }; @@ -203,3 +229,29 @@ exports._drawObject = function _drawObject(self, x, y, width, height, options, c .doXObject(xObject) .Q(); }; + +exports._lineCap = function _lineCap (type) { + const round = 1; + let cap = round; + + if (type) { + const capStyle = ['butt', 'round', 'square']; + const capType = capStyle.indexOf(type); + cap = (capType !== -1) ? capType : round; + } + + return cap; +} + +exports._lineJoin = function _lineJoin (type) { + const round = 1; + let join = round; + + if (type) { + const joinStyle = ['miter', 'round', 'bevel']; + const joinType = joinStyle.indexOf(type); + join = (joinType !== -1) ? joinType : round; + } + + return join; +} diff --git a/tests/text-centering.js b/tests/text-centering.js index c1a3d0b..9588425 100644 --- a/tests/text-centering.js +++ b/tests/text-centering.js @@ -66,7 +66,7 @@ describe('Text - Centering', () => { .text('A\nAAA\nOOO\nVVV\n{0}', 30, 260, { color: '#000000', font: 'Arial', - size: 30, + size: 20, textBox: { width: 300, textAlign: 'center', @@ -75,26 +75,28 @@ describe('Text - Centering', () => { fill:'#ffff00' } }, - }) + }); - .text('<-- Note, using padding of\n 2 puts dots inside fill.', 340, 475) - .text('<-- Text with line height applied\n (1.16% font size).', 340, 525) - .text('A\nAAA\nOOO\nVVV\n|', 30, 425, { + let fs = 20; + recipe + .text('<-- Note, using padding of 2\n puts marker dots inside fill.', 340, 455) + .text('<-- Text with line height applied\n (1.16% font size).', 340, 490) + .text('A\nAAA\nOOO\nVVV\n|', 30, 400, { color: '#000000', font: 'Arial', - size: 30, + size: fs, textBox: { width: 300, textAlign: 'center', padding: 2, - lineHeight: 1.16*30, // percentage of font size + lineHeight: 1.16*fs, // percentage of font size style: { fill:'default' } }, }) - .circle(30,425,2,{stroke:'red'}) - .circle(330,425,2,{stroke:'red'}) + .circle(30,400,2,{stroke:'red'}) + .circle(330,400,2,{stroke:'red'}) .line([[180,25],[180,425]],{stroke:'#ff00ff',lineWidth:.5}); @@ -145,7 +147,7 @@ describe('Text - Centering', () => { } } }) - .line([[x-5,y],[x+w+5,y]], {lineWidth: .5, stroke: 'red'}) + .line([[x-5,y],[x+w+5,y]], {lineWidth: .5, stroke: 'red', lineCap: 'butt'}) .circle(x,y,2,{stroke:'red'}); y = 400; @@ -164,7 +166,7 @@ describe('Text - Centering', () => { } } }) - .line([[x-5,y+h],[x+w+5,y+h]], {lineWidth: .5, stroke: "#ff0000"}) + .line([[x-5,y+h],[x+w+5,y+h]], {lineWidth: .5, stroke: "red", lineCap: 'butt'}) .circle(x,y,2,{stroke:'red'}); x = 500; @@ -184,7 +186,7 @@ describe('Text - Centering', () => { } } }) - .line([[x+w,y-5],[x+w,y+h+5]], {lineWidth: .5, stroke: 'red'}) + .line([[x+w,y-5],[x+w,y+h+5]], {lineWidth: .5, stroke: 'red', lineCap: 'butt'}) .circle(x,y,2,{stroke:'red'}); x = 400; @@ -203,14 +205,61 @@ describe('Text - Centering', () => { } } }) - .line([[x,y-5],[x,y+h+5]], {lineWidth: .5, stroke: 'red'}) + .line([[x,y-5],[x,y+h+5]], {lineWidth: .5, stroke: 'red', lineCap: 'butt'}) .circle(x,y,2,{stroke:'red'}); + recipe + .text('Using horizontal "justify" setting with default "top" vertical setting, no height.', 40, 535, { + color: '#000000', + textBox: { + width: 130, + textAlign: 'justify', + style: { + stroke: '#ffff00', + lineWidth:.5, + } + } + }) + + .text('... now with "justify center" setting and specific height', 180, 535, { + textBox: { + width: 130, + height: 70, + textAlign: 'justify center', + style: { + stroke: '#ff0000', + lineWidth:.5, + } + } + }) + + .text('... finally, using "justify bottom" setting with specific height', 320, 535, { + color: 'blue', + textBox: { + width: 140, + height: 70, + textAlign: 'justify bottom', + style: { + stroke: '#ff00ff', + lineWidth:.5, + } + } + }) + + .text('Oh, wait a minute. How about a justification view without any box drawn. It gives you the sense of an old fashioned newspaper column.', 470, 535, { + color: '#000000', + size: 10, + textBox: { + width: 120, + height: 70, + textAlign: 'justify', + } + }); h = 70; recipe .circle(40,625,2,{stroke:'red'}) - .text(`text box without\nminHeight\n setting`, 40, 625, { + .text(`text box without\nheight or\nminHeight\n setting\nauto fits text`, 40, 625, { color: '#000000', textBox: { width: 100, @@ -223,19 +272,105 @@ describe('Text - Centering', () => { }) .circle(150,625,2,{stroke:'red'}) .circle(150,625+h,2,{stroke:'red'}) - .text(`minHeight: ${h}`, 150, 625, { + .text(`minHeight: ${h}\nBox will not collapse when not enough text to fill it. It will auto grow when text exceeds minHeight.`, 150, 625, { color: '#000000', + size: 9, textBox: { width: 100, minHeight: h, - textAlign: 'center', style: { stroke: '#000000', lineWidth:.5, } } }) - + .text('text at\ncenter center', 265, 625,{ + color: '#000000', + textBox: { + width: 100, + minHeight: h, + textAlign: 'center center', + style: { + stroke: '#000000', + lineWidth:.5, + } + } + }) + .text('text at\ncenter bottom', 380, 625,{ + color: '#000000', + textBox: { + width: 100, + minHeight: h, + textAlign: 'center bottom', + style: { + stroke: '#000000', + lineWidth:.5, + } + } + }) + .text('text at\nright bottom', 500, 625,{ + color: '#000000', + textBox: { + width: 100, + minHeight: h, + textAlign: 'right bottom', + style: { + stroke: '#000000', + lineWidth:.5, + } + } + }) + .text('This is what happens when you use "height" and have too much text', 40, 725,{ + color: '#000000', + textBox: { + width: 125, + height: 40, + // textAlign: 'right bottom', + // textAlign: 'justify', + style: { + stroke: '#000000', + lineWidth:.5, + } + } + }) + .text('Using height and supplying textAlign: center center', 180, 725,{ + color: '#000000', + textBox: { + width: 120, + height: 40, + textAlign: 'center center', + style: { + stroke: '#000000', + lineWidth:.5, + } + } + }) + .text('Using "height" and supplying textAlign: right bottom', 310, 725,{ + color: '#000000', + textBox: { + width: 120, + height: 40, + textAlign: 'right bottom', + style: { + stroke: '#000000', + lineWidth:.5, + } + } + }) + .text('Showing text with\nellipsis when wrap\ndisabled on the last line', 440, 725, { + color: '#000000', + size: 10, + textBox: { + width: 100, + height: 40, + wrap: 'ellipsis', + style: { + stroke: '#000000', + lineWidth:.5, + } + } + }) + recipe.endPage(); recipe.endPDF(done); }); diff --git a/tests/vector.js b/tests/vector.js index eb3d8cb..7777dfd 100644 --- a/tests/vector.js +++ b/tests/vector.js @@ -64,12 +64,12 @@ describe('Vector', () => { stroke: [255, 0, 255], dash: [5, 5] }) - .circle('center', 100, 60, { + .circle('center', 150, 60, { stroke: '#3b7721', fill: '#0e0e0e', opacity: 0.4 }) - .circle('center', 100, 30, { + .circle('center', 150, 30, { stroke: '#0032FF', dash: [5, 5] }) @@ -148,7 +148,29 @@ describe('Vector', () => { ], { lineWidth: 10, dash: [0, 0] - }) + }); + let x = 175; + let y = 11; + let h = 20; + recipe + .text('lineCaps:', 100, 5+h) + .line([[x,y], [x+20,y]], {lineWidth: 10, lineCap:'butt'}) + .text('butt', x+30, 5, {color:'red'}) + .line([[x,y+20], [x+20,y+20]], {lineWidth: 10, lineCap:'round'}) // default + .text('round',x+30, 5+h, {color:'green'}) + .line([[x,y+40], [x+20,y+40]], {lineWidth: 10, lineCap:'square'}) + .text('square', x+30, 5+2*h, {color:'red'}); + + x=300 + y=10 + recipe + .polygon([[x,y+10], [x+20,y+30], [x, y+50]], {lineWidth: 8, lineJoin:'miter', miterLimit:3}) + .text('miter', x-3,70,{rotation:-45, color:'red' }) + .polygon([[x+40,y+10],[x+60,y+30], [x+40, y+50]], {lineWidth: 8, lineJoin:'round'}) // default + .text('round', x+37,70,{rotation:-45, color:'green' }) + .polygon([[x+80,y+10],[x+100,y+30], [x+80, y+50]], {lineWidth: 8, lineJoin: 'bevel'}) + .text('bevel', x+77,70,{rotation:-45, color:'red' }) + .text(':lineJoins', 415, 33) .endPage() .endPDF(done); });
NameTypeAttributes + + <optional>
+ - + -
Description

The line width

coloropacity -string -| - -Array.<number> +number @@ -6654,22 +7053,19 @@
Properties
-

HexColor, PercentColor or DecimalColor

The opacity

strokedash -string -| - Array.<number> @@ -6690,23 +7086,20 @@
Properties
-

HexColor, PercentColor or DecimalColor

The dash pattern [dashSize, gapSize] or [dashAndGapSize]

filldashPhase -string -| - -Array.<number> +number @@ -6726,14 +7119,14 @@
Properties
-

HexColor, PercentColor or DecimalColor

distance into dash pattern at which to start dash (default: 0, immediately)

lineWidthrotation @@ -6759,20 +7152,20 @@
Properties
-

The line width

Accept: +/- 0 through 360. Default: 0

opacityrotationOrigin -number +Array.<number> @@ -6792,20 +7185,20 @@
Properties
-

The opacity

[originX, originY] Default: x, y

dashlineCap -Array.<number> +string @@ -6825,20 +7218,20 @@
Properties
-

The dash style [number, number]

open line end style, 'butt', 'round', or 'square' (default: 'round')

rotationlineJoin -number +string @@ -6858,20 +7251,20 @@
Properties
-

Accept: +/- 0 through 360. Default: 0

joined line end style, 'miter', 'round', or 'bevel' (default: 'round')

rotationOriginmiterLimit -Array.<number> +number @@ -6891,7 +7284,7 @@
Properties
-

[originX, originY] Default: x, y

limit at which 'miter' joins are forced to 'bevel' (default: 1.414)

DefaultDescription
+ +

Text color (HexColor, PercentColor or DecimalColor)

+ + 1 + +

opacity

+ + 0 + +

Accept: +/- 0 through 360. Default: 0

Accept: +/- 0 through 360.

+ + [x,y] + +

[originX, originY] Default: the text coordinate

[originX, originY]

+ + Helvetica + +

The font. 'Arial', 'Helvetica'...

+ + 14 + +

The line width

The font size

+ + 'left top' + +

The alignment. 'center center'...

+ +

Text markup annotation.

+ +

Text markup annotation.

+ +

Text markup annotation.

+ +

Text Box to fit in.

Properties
@@ -8439,6 +8890,8 @@
Properties
+
DefaultDescription
+ + 100 + +

Text Box width

+ +

Text Box fixed height

+ + 0 + +

Text Box minimum height

+ + 0 + +

Text Box padding, [top, right, bottom, left]

+ + 0 + +

Text Box line height

wrap + + +string + + + + + + <optional>
+ + + + + +
+ + 'auto' + +

Text wrapping mechanism, 'auto', 'clip', 'trim'.

textAlign + + 'left top' + +

Text alignment inside text box

Alignment inside text box, specified as 'horizontal vertical', +where horizontal is one of: 'left', 'center', 'right', 'justify' and veritical is one of: 'top', 'center', 'bottom'.

+ +

Text Box styles

Properties
@@ -8694,6 +9225,8 @@
Properties
+
DefaultDescription
+ + 2 + +

Text Box border width

+ +

Text Box border color (HexColor, PercentColor or DecimalColor)

+ + [] + +

Text Box border border dash style [number, number]

+ +

Text Box border background color (HexColor, PercentColor or DecimalColor)

+ + 1 + +

Text Box border background opacity

color specification in form of HexColor (string, begins with '#'), -DecimalColor (1, 3, or 4 element array with values between 0-255), -PercentColor (string, begins with '%' followed by values separated -by commas with values between 0-100)