pdf2json
Version:
PDF file parser that converts PDF binaries to JSON and text, powered by porting a fork of PDF.JS to Node.js
1 lines • 3.08 MB
Source Map (JSON)
{"version":3,"file":"pdfparser.cjs","sources":["../lib/pkinfo.js","../lib/pdfconst.js","../lib/pdfunit.js","../lib/pdffield.js","../lib/pdffont.js","../lib/simpleXmlParser.js","../lib/ptixmlinject.js","../lib/pdfline.js","../lib/pdffill.js","../lib/pdfcanvas.js","../lib/pdfanno.js","../lib/pdfimage.js","../lib/pdfjs-code.js","../lib/pdf.js","../lib/parserstream.js","../pdfparser.js"],"sourcesContent":["// import path from \"path\";\n// import { fileURLToPath } from \"url\";\n// import fs from \"fs\";\nimport * as pkInfo from \"../package.json\";\n\n// const __filename = fileURLToPath(import.meta.url);\n// const __dirname = path.dirname(__filename);\n// const pkInfo = JSON.parse(fs.readFileSync(`${__dirname}/../package.json`, \"utf8\"));\nconst _PARSER_SIG = `${pkInfo.name}@${pkInfo.version} [${pkInfo.homepage}]`;\n\nexport { pkInfo, _PARSER_SIG };\n","export const kColors = [\n '#000000',\t\t// 0\n '#ffffff',\t\t// 1\n '#4c4c4c',\t\t// 2\n '#808080',\t\t// 3\n '#999999',\t\t// 4\n '#c0c0c0',\t\t// 5\n '#cccccc',\t\t// 6\n '#e5e5e5',\t\t// 7\n '#f2f2f2',\t\t// 8\n '#008000',\t\t// 9\n '#00ff00',\t\t// 10\n '#bfffa0',\t\t// 11\n '#ffd629',\t\t// 12\n '#ff99cc',\t\t// 13\n '#004080',\t\t// 14\n '#9fc0e1',\t\t// 15\n '#5580ff',\t\t// 16\n '#a9c9fa',\t\t// 17\n '#ff0080',\t\t// 18\n '#800080',\t\t// 19\n '#ffbfff',\t\t// 20\n '#e45b21',\t\t// 21\n '#ffbfaa',\t\t// 22\n '#008080',\t\t// 23\n '#ff0000',\t\t// 24\n '#fdc59f',\t\t// 25\n '#808000',\t\t// 26\n '#bfbf00',\t\t// 27\n '#824100',\t\t// 28\n '#007256',\t\t// 29\n '#008000',\t\t// 30\n '#000080',\t\t// Last + 1\n '#008080',\t\t// Last + 2\n '#800080',\t\t// Last + 3\n '#ff0000',\t\t// Last + 4\n '#0000ff',\t\t// Last + 5\n '#008000'\t\t// Last + 6\n];\n\nexport const kFontFaces = [\n \"quicktype,arial,helvetica,sans-serif\",\t\t\t\t\t\t\t// 00 - QuickType - sans-serif variable font\n \"quicktype condensed,arial narrow,arial,helvetica,sans-serif\",\t// 01 - QuickType Condensed - thin sans-serif variable font\n \"quicktypepi,quicktypeiipi\",\t\t\t\t\t\t\t\t\t// 02 - QuickType Pi\n \"quicktype mono,courier new,courier,monospace\",\t\t\t\t\t// 03 - QuickType Mono - san-serif fixed font\n \"ocr-a,courier new,courier,monospace\",\t\t\t\t\t\t\t// 04 - OCR-A - OCR readable san-serif fixed font\n \"ocr b mt,courier new,courier,monospace\"\t\t\t\t\t\t// 05 - OCR-B MT - OCR readable san-serif fixed font\n ];\n\n export const kFontStyles = [\n // Face\t\tSize\tBold\tItalic\t\tStyleID(Comment)\n // -----\t----\t----\t-----\t\t-----------------\n [0,\t\t6,\t\t0,\t\t0],\t\t\t//00\n [0,\t\t8,\t\t0,\t\t0],\t\t\t//01\n [0,\t\t10,\t\t0,\t\t0],\t\t\t//02\n [0,\t\t12,\t\t0,\t\t0],\t\t\t//03\n [0,\t\t14,\t\t0,\t\t0],\t\t\t//04\n [0,\t\t18,\t\t0,\t\t0],\t\t\t//05\n [0,\t\t6,\t\t1,\t\t0],\t\t\t//06\n [0,\t\t8,\t\t1,\t\t0],\t\t\t//07\n [0,\t\t10,\t\t1,\t\t0],\t\t\t//08\n [0,\t\t12,\t\t1,\t\t0],\t\t\t//09\n [0,\t\t14,\t\t1,\t\t0],\t\t\t//10\n [0,\t\t18,\t\t1,\t\t0],\t\t\t//11\n [0,\t\t6,\t\t0,\t\t1],\t\t\t//12\n [0,\t\t8,\t\t0,\t\t1],\t\t\t//13\n [0,\t\t10,\t\t0,\t\t1],\t\t\t//14\n [0,\t\t12,\t\t0,\t\t1],\t\t\t//15\n [0,\t\t14,\t\t0,\t\t1],\t\t\t//16\n [0,\t\t18,\t\t0,\t\t1],\t\t\t//17\n [0,\t\t6,\t\t1,\t\t1],\t\t\t//18\n [0,\t\t8,\t\t1,\t\t1],\t\t\t//19\n [0,\t\t10,\t\t1,\t\t1],\t\t\t//20\n [0,\t\t12,\t\t1,\t\t1],\t\t\t//21\n [0,\t\t14,\t\t1,\t\t1],\t\t\t//22\n [0,\t\t18,\t\t1,\t\t1],\t\t\t//23\n [1,\t\t6,\t\t0,\t\t0],\t\t\t//24\n [1,\t\t8,\t\t0,\t\t0],\t\t\t//25\n [1,\t\t10,\t\t0,\t\t0],\t\t\t//26\n [1,\t\t12,\t\t0,\t\t0],\t\t\t//27\n [1,\t\t14,\t\t0,\t\t0],\t\t\t//28\n [1,\t\t18,\t\t0,\t\t0],\t\t\t//29\n [1,\t\t6,\t\t1,\t\t0],\t\t\t//30\n [1,\t\t8,\t\t1,\t\t0],\t\t\t//31\n [1,\t\t10,\t\t1,\t\t0],\t\t\t//32\n [1,\t\t12,\t\t1,\t\t0],\t\t\t//33\n [1,\t\t14,\t\t1,\t\t0],\t\t\t//34\n [1,\t\t18,\t\t1,\t\t0],\t\t\t//35\n [1,\t\t6,\t\t0,\t\t1],\t\t\t//36\n [1,\t\t8,\t\t0,\t\t1],\t\t\t//37\n [1,\t\t10,\t\t0,\t\t1],\t\t\t//38\n [1,\t\t12,\t\t0,\t\t1],\t\t\t//39\n [1,\t\t14,\t\t0,\t\t1],\t\t\t//40\n [1,\t\t18,\t\t0,\t\t1],\t\t\t//41\n [2,\t\t8,\t\t0,\t\t0],\t\t\t//42\n [2,\t\t10,\t\t0,\t\t0],\t\t\t//43\n [2,\t\t12,\t\t0,\t\t0],\t\t\t//44\n [2,\t\t14,\t\t0,\t\t0],\t\t\t//45\n [2,\t\t18,\t\t0,\t\t0],\t\t\t//46\n [3,\t\t8,\t\t0,\t\t0],\t\t\t//47\n [3,\t\t10,\t\t0,\t\t0],\t\t\t//48\n [3,\t\t12,\t\t0,\t\t0],\t\t\t//49\n [4,\t\t12,\t\t0,\t\t0],\t\t\t//50\n [0,\t\t9,\t\t0,\t\t0],\t\t\t//51\n [0,\t\t9,\t\t1,\t\t0],\t\t\t//52\n [0,\t\t9,\t\t0,\t\t1],\t\t\t//53\n [0,\t\t9,\t\t1,\t\t1],\t\t\t//54\n [1,\t\t9,\t\t0,\t\t0],\t\t\t//55\n [1,\t\t9,\t\t1,\t\t0],\t\t\t//56\n [1,\t\t9,\t\t1,\t\t1],\t\t\t//57\n [4,\t\t10,\t\t0,\t\t0],\t\t\t//58\n [5,\t\t10,\t\t0,\t\t0],\t\t\t//59\n [5,\t\t12,\t\t0,\t\t0]\t\t\t//60\n];","import { kColors } from \"./pdfconst.js\";\n\nconst dpi = 96.0;\nconst gridXPerInch = 4.0;\nconst gridYPerInch = 4.0;\n\nconst _pixelXPerGrid = dpi/gridXPerInch;\nconst _pixelYPerGrid = dpi/gridYPerInch;\nconst _pixelPerPoint = dpi/72;\n\nexport default class PDFUnit {\n static toFixedFloat(fNum) {\n return parseFloat(fNum.toFixed(3));\n }\n\n static colorCount() {\n return kColors.length;\n }\n\n static toPixelX(formX) {\n return Math.round(formX * _pixelXPerGrid);\n }\n\n static toPixelY(formY) {\n return Math.round(formY * _pixelYPerGrid);\n }\n\n static pointToPixel(point) {// Point unit (1/72 an inch) to pixel units\n return point * _pixelPerPoint;\n }\n\n static getColorByIndex(clrId) {\n return kColors[clrId];\n }\n\n static toFormPoint(viewportX, viewportY) {\n return [(viewportX / _pixelXPerGrid), (viewportY / _pixelYPerGrid)];\n }\n\n static toFormX(viewportX) {\n return PDFUnit.toFixedFloat(viewportX / _pixelXPerGrid);\n }\n\n static toFormY(viewportY) {\n return PDFUnit.toFixedFloat(viewportY / _pixelYPerGrid);\n }\n\n static findColorIndex(color) {\n if (color.length === 4)\n color += \"000\";\n //MQZ. 07/29/2013: if color is not in dictionary, just return -1. The caller (pdffont, pdffill) will set the actual color\n return kColors.indexOf(color);\n }\n\n static dateToIso8601(date) {\n // PDF spec p.160\n if (date.slice(0, 2) === 'D:') { // D: prefix is optional\n date = date.slice(2);\n }\n let tz = 'Z';\n const idx = date.search(/[Z+-]/); // timezone is optional\n if (idx >= 0) {\n tz = date.slice(idx);\n if (tz !== 'Z') { // timezone format OHH'mm'\n tz = `${tz.slice(0, 3)}:${tz.slice(4, 6)}`;\n }\n date = date.slice(0, idx);\n }\n const yr = date.slice(0, 4); // everything after year is optional\n const mth = date.slice(4, 6) || '01';\n const day = date.slice(6, 8) || '01';\n const hr = date.slice(8, 10) || '00';\n const min = date.slice(10, 12) || '00';\n const sec = date.slice(12, 14) || '00';\n return `${yr}-${mth}-${day}T${hr}:${min}:${sec}${tz}`;\n }\n}\n","import nodeUtil from \"node:util\";\nimport PDFUnit from \"./pdfunit.js\";\n\nconst kFBANotOverridable = 0x00000400; // indicates the field is read only by the user\nconst kFBARequired = 0x00000010; // indicates the field is required\nconst kMinHeight = 20;\n\nexport default class PDFField {\n static tabIndex = 0;\n\n static isWidgetSupported(field) {\n let retVal = false;\n\n switch(field.fieldType) {\n case 'Tx': retVal = true; break; //text input\n case 'Btn':\n if (field.fieldFlags & 32768) {\n field.fieldType = 'Rd'; //radio button\n }\n else if (field.fieldFlags & 65536) {\n field.fieldType = 'Btn'; //push button\n }\n else {\n field.fieldType = 'Cb'; //checkbox\n }\n retVal = true;\n break;\n case 'Ch': retVal = true; break; //drop down\n case 'Sig': retVal = true; break; //signature\n default:\n nodeUtil.p2jwarn(`Unsupported: field.fieldType of ${field.fieldType}`);\n break;\n }\n\n return retVal;\n }\n\n static isFormElement(field) {\n let retVal = false;\n switch(field.subtype) {\n case 'Widget': retVal = PDFField.isWidgetSupported(field); break;\n default:\n nodeUtil.p2jwarn(`Unsupported: field.type of ${field.subtype}`);\n break;\n }\n return retVal;\n }\n\n // constructor\n constructor(field, viewport, Fields, Boxsets) {\n this.field = field;\n this.viewport = viewport;\n this.Fields = Fields;\n this.Boxsets = Boxsets;\n }\n\n // Normalize rectangle rect=[x1, y1, x2, y2] so that (x1,y1) < (x2,y2)\n // For coordinate systems whose origin lies in the bottom-left, this\n // means normalization to (BL,TR) ordering. For systems with origin in the\n // top-left, this means (TL,BR) ordering.\n static #normalizeRect(rect) {\n const r = rect.slice(0); // clone rect\n if (rect[0] > rect[2]) {\n r[0] = rect[2];\n r[2] = rect[0];\n }\n if (rect[1] > rect[3]) {\n r[1] = rect[3];\n r[3] = rect[1];\n }\n return r;\n }\n\n #getFieldPosition(field) {\n const viewPort = this.viewport;\n const fieldRect = viewPort.convertToViewportRectangle(field.rect);\n const rect = PDFField.#normalizeRect(fieldRect);\n\n let height = rect[3] - rect[1];\n if (field.fieldType === 'Tx') {\n if (height > kMinHeight + 2) {\n rect[1] += 2;\n height -= 2;\n }\n }\n else if (field.fieldType !== 'Ch') { //checkbox, radio button, and link button\n rect[1] -= 3;\n }\n\n height = (height >= kMinHeight) ? height : kMinHeight;\n\n return {\n x: PDFUnit.toFormX(rect[0]),\n y: PDFUnit.toFormY(rect[1]),\n w: PDFUnit.toFormX(rect[2] - rect[0]),\n h: PDFUnit.toFormY(height)\n };\n }\n\n #getFieldBaseData(field) {\n let attributeMask = 0;\n //PDF Spec p.676 TABLE 8.70 Field flags common to all field types\n if (field.fieldFlags & 0x00000001) {\n attributeMask |= kFBANotOverridable;\n }\n if (field.fieldFlags & 0x00000002) {\n attributeMask |= kFBARequired;\n }\n\n const anData = {\n id: { Id: field.fullName, EN: 0 },\n TI: field.TI,\n AM: attributeMask\n };\n //PDF Spec p.675: add TU (AlternativeText) fields to provide accessibility info\n if (field.alternativeText && field.alternativeText.length > 1) {\n anData.TU = field.alternativeText;\n }\n\n if (field.alternativeID && field.alternativeID.length > 1) {\n anData.TM = field.alternativeID;\n }\n\n return Object.assign(anData, this.#getFieldPosition(field));\n }\n\n #addAlpha(field) {\n const anData = Object.assign({\n style: 48,\n T: {\n Name: field.TName || \"alpha\",\n TypeInfo: {}\n }\n }, this.#getFieldBaseData(field));\n\n if (field.MV) { //field attributes: arbitrary mask value\n anData.MV = field.MV;\n }\n if (field.fieldValue) {\n anData.V = field.fieldValue; //read-only field value, like \"self-prepared\"\n }\n\n this.Fields.push(anData);\n }\n\n #addCheckBox(box) {\n const anData = Object.assign({\n style: 48,\n T: {\n Name: \"box\",\n TypeInfo: {}\n }\n }, this.#getFieldBaseData(box));\n if(box.fieldValue) {\n anData.checked = box.fieldValue !== 'Off';\n }\n\n this.Boxsets.push({ boxes:[anData] });\n }\n\n #addRadioButton(box) {\n const anData = Object.assign({\n style: 48,\n T: {\n Name: \"box\",\n TypeInfo: {}\n }\n }, this.#getFieldBaseData(box));\n\n anData.id.Id = box.value;\n if ('checked' in box) {\n anData.checked = box.checked;\n }\n\n const rdGroup = this.Boxsets.filter(boxset => ('id' in boxset) && ('Id' in boxset.id) && (boxset.id.Id === box.fullName))[0];\n if ((!!rdGroup) && ('boxes' in rdGroup)) {\n rdGroup.boxes.push(anData);\n }\n else {\n this.Boxsets.push({ boxes:[anData], id: { Id: box.fullName, EN: 0 } });\n }\n }\n\n #addLinkButton(field) {\n const anData = Object.assign({\n style: 48,\n T: {\n Name: \"link\"\n },\n FL: {\n form: { Id: field.FL }\n }\n }, this.#getFieldBaseData(field));\n\n this.Fields.push(anData);\n }\n\n #addSelect(field) {\n const anData = Object.assign({\n style: 48,\n T: {\n Name: \"alpha\",\n TypeInfo: {}\n }\n }, this.#getFieldBaseData(field));\n\n anData.w -= 0.5; //adjust combobox width\n anData.PL = { V: [], D: [] };\n field.value.forEach( (ele, idx) => {\n if (Array.isArray(ele)) {\n anData.PL.D.push(ele[0]);\n anData.PL.V.push(ele[1]);\n } else {\n anData.PL.D.push(ele);\n anData.PL.V.push(ele);\n }\n });\n\n\t\t// add field value to the object\n\t\tif (field.fieldValue) {\n\t\t\tanData.V = field.fieldValue;\n\t\t}\n this.Fields.push(anData);\n }\n\n #addSignature(field) {\n const anData = Object.assign({\n style: 48,\n T: {\n Name: \"signature\",\n TypeInfo: {}\n }\n }, this.#getFieldBaseData(field));\n\n if (field.Sig) {\n anData.Sig = {};\n if (field.Sig.Name) anData.Sig.Name = field.Sig.Name;\n if (field.Sig.M) anData.Sig.M = PDFUnit.dateToIso8601(field.Sig.M);\n if (field.Sig.Location) anData.Sig.Location = field.Sig.Location;\n if (field.Sig.Reason) anData.Sig.Reason = field.Sig.Reason;\n if (field.Sig.ContactInfo) anData.Sig.ContactInfo = field.Sig.ContactInfo;\n }\n\n this.Fields.push(anData);\n }\n\n // public instance methods\n processField() {\n this.field.TI = PDFField.tabIndex++;\n\n switch(this.field.fieldType) {\n case 'Tx': this.#addAlpha(this.field); break;\n case 'Cb': this.#addCheckBox(this.field); break;\n case 'Rd': this.#addRadioButton(this.field);break;\n case 'Btn':this.#addLinkButton(this.field); break;\n case 'Ch': this.#addSelect(this.field); break;\n case 'Sig': this.#addSignature(this.field); break;\n }\n\n this.clean();\n }\n\n clean() {\n delete this.field;\n delete this.viewport;\n delete this.Fields;\n delete this.Boxsets;\n }\n\n //static public method to generate fieldsType object based on parser result\n static getAllFieldsTypes(data) {\n const isFieldReadOnly = field => (field.AM & kFBANotOverridable) ? true : false;\n\n\t\tconst getFieldBase = field => ({\n\t\t\tid: field.id.Id,\n\t\t\ttype: field.T.Name,\n\t\t\tcalc: isFieldReadOnly(field),\n\t\t\tvalue: field.V || \"\"\n\t\t});\n\n const retVal = [];\n data.Pages.forEach( page => {\n page.Boxsets.forEach( boxsets => {\n if (boxsets.boxes.length > 1) { //radio button\n boxsets.boxes.forEach( box => {\n retVal.push({ id: boxsets.id.Id, type: \"radio\", calc: isFieldReadOnly(box), value: box.id.Id });\n });\n }\n else { //checkbox\n retVal.push(getFieldBase(boxsets.boxes[0]));\n }\n });\n\n page.Fields.forEach(field => retVal.push(getFieldBase(field)));\n\n });\n return retVal;\n }\n \n //static public method to generate data output for all field types\n static getAllFieldData(data) {\n const isFieldReadOnly = field => (field.AM & kFBANotOverridable) ? true : false;\n\n function getFieldValue(field) {\n switch(field.T.Name) {\n case 'box':\n return field.checked === undefined ? false : field.checked;\n case 'alpha':\n case 'date':\n default:\n return field.V;\n }\n }\n\t\tconst getFieldBase = field => ({\n\t\t\tid: field.id.Id,\n\t\t\ttype: field.T.Name,\n\t\t\tcalc: isFieldReadOnly(field),\n\t\t\tvalue: getFieldValue(field)\n\t\t});\n\n const retVal = [];\n data.Pages.forEach( page => {\n page.Boxsets.forEach( boxsets => {\n if (boxsets.boxes.length > 1) { //radio button\n boxsets.boxes.forEach( box => {\n if(box.checked)\n retVal.push({ id: boxsets.id.Id, type: \"radio\", calc: isFieldReadOnly(box), value: box.id.Id });\n });\n }\n else { //checkbox\n retVal.push(getFieldBase(boxsets.boxes[0]));\n }\n });\n\n page.Fields.forEach(field => retVal.push(getFieldBase(field)));\n\n });\n return retVal;\n }\n}\n","import nodeUtil from 'node:util';\nimport PDFUnit from './pdfunit.js';\nimport { kFontFaces, kFontStyles } from './pdfconst.js';\n\nconst _boldSubNames = ['bd', 'bold', 'demi', 'black', 'medi'];\nconst _stdFonts = [\n 'arial',\n 'helvetica',\n 'sans-serif ',\n 'courier ',\n 'monospace ',\n 'ocr ',\n];\nconst DISTANCE_DELTA = 0.1;\n\nexport default class PDFFont {\n #initTypeName() {\n let typeName = this.fontObj.name || this.fontObj.fallbackName;\n if (!typeName) {\n typeName = kFontFaces[0]; //default font family name\n }\n typeName = typeName.toLowerCase();\n return typeName;\n }\n\n #initSubType() {\n let subType = this.typeName;\n let bold = false;\n\n const nameArray = this.typeName.split('+');\n if (Array.isArray(nameArray) && nameArray.length > 1) {\n subType = nameArray[1].split('-');\n if (Array.isArray(subType) && subType.length > 1) {\n const subName = subType[1].toLowerCase();\n bold = _boldSubNames.indexOf(subName) >= 0;\n subType = subType[0];\n }\n }\n return { subType, bold };\n }\n\n #initSymbol() {\n const isSymbol =\n this.typeName.indexOf('symbol') > 0 ||\n kFontFaces[2].indexOf(this.subType) >= 0;\n if (this.fontObj.isSymbolicFont) {\n const mFonts = _stdFonts.filter(\n (oneName) => this.typeName.indexOf(oneName) >= 0\n );\n\n if (mFonts.length > 0) {\n this.fontObj.isSymbolicFont = false; //lots of Arial-based font is detected as symbol in VA forms (301, 76-c, etc.) reset the flag for now\n nodeUtil.p2jinfo(\n `Reset: isSymbolicFont (false) for ${this.fontObj.name}`\n );\n }\n } else {\n if (isSymbol) {\n this.fontObj.isSymbolicFont = true; //text pdf: va_ind_760c\n nodeUtil.p2jinfo(\n `Reset: isSymbolicFont (true) for ${this.fontObj.name}`\n );\n }\n }\n return isSymbol;\n }\n\n #initSpaceWidth() {\n let { spaceWidth } = this.fontObj;\n if (!spaceWidth) {\n var spaceId = Array.isArray(this.fontObj.toFontChar)\n ? this.fontObj.toFontChar.indexOf(32)\n : -1;\n spaceWidth =\n spaceId >= 0 && Array.isArray(this.fontObj.widths)\n ? this.fontObj.widths[spaceId]\n : 250;\n }\n spaceWidth = PDFUnit.toFormX(spaceWidth) / 32;\n return spaceWidth;\n }\n\n // constructor\n constructor(fontObj) {\n this.fontObj = fontObj;\n\n this.typeName = this.#initTypeName();\n\n const { subType, bold } = this.#initSubType();\n this.subType = subType;\n this.bold = bold;\n\n this.isSymbol = this.#initSymbol();\n this.spaceWidth = this.#initSpaceWidth();\n\n this.fontSize = 1;\n this.faceIdx = 0;\n this.italic = false;\n this.fontStyleId = -1;\n }\n\n /** sort text blocks by y then x */\n static compareBlockPos(t1, t2) {\n if (t1.y < t2.y - DISTANCE_DELTA) {\n return -1;\n }\n if (Math.abs(t1.y - t2.y) <= DISTANCE_DELTA) {\n if (t1.x < t2.x - DISTANCE_DELTA) {\n return -1;\n }\n if (Math.abs(t1.x - t2.x) <= DISTANCE_DELTA) {\n return 0;\n }\n }\n return 1;\n }\n\n static haveSameStyle(t1, t2) {\n let retVal = t1.R[0].S === t2.R[0].S;\n if (retVal && t1.R[0].S < 0) {\n for (let i = 0; i < t1.R[0].TS.length; i++) {\n if (t1.R[0].TS[i] !== t2.R[0].TS[i]) {\n retVal = false;\n break;\n }\n }\n }\n if (retVal) {\n // make sure both block are not rotated\n retVal =\n typeof t1.R[0].RA === 'undefined' &&\n typeof t2.R[0].RA === 'undefined';\n }\n\n return retVal;\n }\n\n static getSpaceThreshHold(t1) {\n return (PDFFont.getFontSize(t1) / 12) * t1.sw;\n }\n\n static areAdjacentBlocks(t1, t2) {\n const isInSameLine = Math.abs(t1.y - t2.y) <= DISTANCE_DELTA;\n const isDistanceSmallerThanASpace =\n t2.x - t1.x - t1.w < PDFFont.getSpaceThreshHold(t1);\n\n return isInSameLine && isDistanceSmallerThanASpace;\n }\n\n static getFontSize(textBlock) {\n const sId = textBlock.R[0].S;\n return sId < 0 ? textBlock.R[0].TS[1] : kFontStyles[sId][1];\n }\n\n static areDuplicateBlocks(t1, t2) {\n return (\n t1.x === t2.x &&\n t1.y === t2.y &&\n t1.R[0].T === t2.R[0].T &&\n PDFFont.haveSameStyle(t1, t2)\n );\n }\n\n // private\n #setFaceIndex() {\n const { fontObj } = this;\n\n this.bold = fontObj.bold;\n if (!this.bold) {\n this.bold =\n this.typeName.indexOf('bold') >= 0 ||\n this.typeName.indexOf('black') >= 0;\n }\n this.italic = fontObj.italic; // fix https://github.com/modesty/pdf2json/issues/42\n // Extended the fix for https://github.com/modesty/pdf2json/issues/42\n if (!this.italic) {\n this.italic =\n this.typeName.indexOf('italic') >= 0 ||\n this.typeName.indexOf('oblique') >= 0;\n }\n // Added detection of hybrid dual bolditalic fonts\n if (\n (!this.bold || !this.italic) &&\n this.typeName.indexOf('boldobl') >= 0\n ) {\n this.bold = true;\n this.italic = true;\n }\n\n const typeName = this.subType;\n if (fontObj.isSerifFont) {\n if (kFontFaces[1].indexOf(typeName) >= 0) this.faceIdx = 1;\n } else if (kFontFaces[2].indexOf(this.subType) >= 0) {\n this.faceIdx = 2;\n } else if (fontObj.isMonospace) {\n this.faceIdx = 3;\n\n if (kFontFaces[4].indexOf(typeName) >= 0) this.faceIdx = 4;\n else if (kFontFaces[5].indexOf(typeName) >= 0) this.faceIdx = 5;\n } else if (fontObj.isSymbolicFont) {\n this.faceIdx = 2;\n }\n\n if (this.faceIdx === 0) {\n if (this.typeName.indexOf('narrow') > 0) this.faceIdx = 1;\n }\n\n // nodeUtil.p2jinfo\"typeName = \" + typeName + \" => faceIdx = \" + this.faceIdx);\n }\n\n #getFontStyleIndex(fontSize) {\n this.#setFaceIndex();\n\n //MQZ Feb.28.2013. Adjust bold text fontsize to work around word spacing issue\n this.fontSize = this.bold && fontSize > 12 ? fontSize + 1 : fontSize;\n\n const fsa = [\n this.faceIdx,\n this.fontSize,\n this.bold ? 1 : 0,\n this.italic ? 1 : 0,\n ];\n let retVal = -1;\n\n kFontStyles.forEach((element, index, list) => {\n if (retVal === -1) {\n if (\n element[0] === fsa[0] &&\n element[1] === fsa[1] &&\n element[2] === fsa[2] &&\n element[3] === fsa[3]\n ) {\n retVal = index;\n }\n }\n });\n\n return retVal;\n }\n\n #processSymbolicFont(str) {\n let retVal = str;\n\n if (!str || str.length !== 1) return retVal;\n\n if (!this.fontObj.isSymbolicFont || !this.isSymbol) {\n if (retVal === 'C' || retVal === 'G') {\n //prevent symbolic encoding from the client\n retVal = ` ${retVal} `; //sample: va_ind_760c\n }\n return retVal;\n }\n\n switch (str.charCodeAt(0)) {\n case 20:\n retVal = '\\u2713';\n break; //check mark\n case 70:\n retVal = this.fontObj.type === 'CIDFontType0' ? '\\u26A0' : '\\u007D';\n break; //exclaimation in triangle OR right curly bracket\n case 71:\n retVal = '\\u25b6';\n break; //right triangle\n case 97:\n retVal = '\\u25b6';\n break; //right triangle\n case 99:\n retVal = this.isSymbol ? '\\u2022' : '\\u25b2';\n break; //up triangle. set to Bullet Dot for VA SchSCR\n case 100:\n retVal = '\\u25bc';\n break; //down triangle\n case 103:\n retVal = '\\u27A8';\n break; //right arrow. sample: va_ind_760pff and pmt\n case 106:\n retVal = '';\n break; //VA 301: string j character by the checkbox, hide it for now\n case 114:\n retVal = '\\u2022';\n break; //Bullet dot\n case 115:\n retVal = '\\u25b2';\n break; //up triangle\n case 116:\n retVal = '\\u2022';\n break; //Bullet dot\n case 118:\n retVal = '\\u2022';\n break; //Bullet dot\n default:\n nodeUtil.p2jinfo(\n\t\t\t\t`${this.fontObj.type} - SymbolicFont - (${this.fontObj.name}) : ${str.charCodeAt(0)}::${str.charCodeAt(1)} => ${retVal}`\n\t\t\t);\n }\n\n return retVal;\n }\n\n #textRotationAngle(matrix2D) {\n let retVal = 0;\n if (matrix2D[0][0] === 0 && matrix2D[1][1] === 0) {\n if (matrix2D[0][1] !== 0 && matrix2D[1][0] !== 0) {\n if (matrix2D[0][1] / matrix2D[1][0] + 1 < 0.0001) retVal = 90;\n }\n } else if (matrix2D[0][0] !== 0 && matrix2D[1][1] !== 0) {\n const r1 = Math.atan(-matrix2D[0][1] / matrix2D[0][0]);\n const r2 = Math.atan(matrix2D[1][0] / matrix2D[1][1]);\n if (Math.abs(r1) > 0.0001 && r1 - r2 < 0.0001) {\n retVal = (r1 * 180) / Math.PI;\n }\n }\n return retVal;\n }\n\n // public instance methods\n processText(p, str, maxWidth, color, fontSize, targetData, matrix2D) {\n const text = this.#processSymbolicFont(str);\n if (!text) {\n return;\n }\n this.fontStyleId = this.#getFontStyleIndex(fontSize);\n\n // when this.fontStyleId === -1, it means the text style doesn't match any entry in the dictionary\n // adding TS to better describe text style [fontFaceId, fontSize, 1/0 for bold, 1/0 for italic];\n const TS = [\n this.faceIdx,\n this.fontSize,\n this.bold ? 1 : 0,\n this.italic ? 1 : 0,\n ];\n\n const clrId = PDFUnit.findColorIndex(color);\n const colorObj =\n clrId >= 0 && clrId < PDFUnit.colorCount()\n ? { clr: clrId }\n : { oc: color };\n\n let textRun = {\n T: this.flashEncode(text),\n S: this.fontStyleId,\n TS,\n };\n const rAngle = this.#textRotationAngle(matrix2D);\n if (rAngle !== 0) {\n nodeUtil.p2jinfo(`${str}: rotated ${rAngle} degree.`);\n textRun = { ...textRun, RA: rAngle };\n }\n\n const oneText = {\n x: PDFUnit.toFormX(p.x) - 0.25,\n y: PDFUnit.toFormY(p.y) - 0.75,\n w: PDFUnit.toFixedFloat(maxWidth),\n ...colorObj, //MQZ.07/29/2013: when color is not in color dictionary, set the original color (oc)\n sw: this.spaceWidth, //font space width, use to merge adjacent text blocks\n A: 'left',\n R: [textRun],\n };\n\n targetData.Texts.push(oneText);\n }\n\n flashEncode(str) {\n let retVal = encodeURIComponent(str);\n retVal = retVal.replace('%C2%96', '-');\n retVal = retVal.replace('%C2%91', '%27');\n retVal = retVal.replace('%C2%92', '%27');\n retVal = retVal.replace('%C2%82', '%27');\n retVal = retVal.replace('%C2%93', '%22');\n retVal = retVal.replace('%C2%94', '%22');\n retVal = retVal.replace('%C2%84', '%22');\n retVal = retVal.replace('%C2%8B', '%C2%AB');\n retVal = retVal.replace('%C2%9B', '%C2%BB');\n\n return retVal;\n }\n\n clean() {\n this.fontObj = null;\n delete this.fontObj;\n }\n}\n","// A simple XML parser to replace @xmldom/xmldom dependency\n// This implements just enough functionality to support the existing code\n\n/**\n * A simple XML Element implementation\n * @class\n */\nclass Element {\n /**\n * Create a new Element\n * @param {string} nodeName - The name of the node/tag\n */\n constructor(nodeName) {\n /** @type {string} */\n this.nodeName = nodeName;\n /** @type {Array<Element>} */\n this.childNodes = [];\n /** @type {Object.<string, string>} */\n this.attributes = {};\n /** @type {string} */\n this.textContent = \"\";\n }\n\n /**\n * Get attribute value by name\n * @param {string} name - The attribute name\n * @returns {string|null} The attribute value or null\n */\n getAttribute(name) {\n return this.attributes[name] || null;\n }\n\n /**\n * Get elements by tag name\n * @param {string} tagName - The tag name to search for\n * @returns {Array<Element>} The matching elements\n */\n getElementsByTagName(tagName) {\n /** @type {Array<Element>} */\n let results = [];\n\n // Check if this element matches\n if (this.nodeName === tagName) {\n results.push(this);\n }\n\n // Check child elements recursively\n for (const child of this.childNodes) {\n if (child instanceof Element) {\n if (tagName === \"*\" || child.nodeName === tagName) {\n results.push(child);\n }\n\n // Add matching descendants\n const childMatches = child.getElementsByTagName(tagName);\n results = results.concat(childMatches);\n }\n }\n\n return results;\n }\n}\n\n/**\n * A simple XML Document implementation\n * @class\n */\nclass Document {\n constructor() {\n /** @type {Element|null} */\n this.documentElement = null;\n }\n}\n\n/**\n * A minimal DOMParser implementation that supports the basic features needed\n * @class\n */\nclass SimpleDOMParser {\n /**\n * Parse XML string into a Document\n * @param {string} xmlString - The XML string to parse\n * @returns {Document} The parsed document\n */\n parseFromString(xmlString) {\n const doc = new Document();\n\n // Remove XML declaration if present\n xmlString = xmlString.replace(/<\\?xml[^?]*\\?>/, \"\").trim();\n\n // Parse the document\n doc.documentElement = this.parseElement(xmlString);\n\n return doc;\n }\n\n /**\n * Parse an XML element\n * @param {string} xmlString - The XML string to parse\n * @returns {Element|null} The parsed element or null\n */\n parseElement(xmlString) {\n // Regular expressions for parsing XML\n const startTagRegex = /<([^\\s/>]+)([^>]*)>/;\n const attributeRegex = /([^\\s=]+)=(?:\"([^\"]*)\"|'([^']*)')/g;\n\n // Find the start tag\n const startMatch = xmlString.match(startTagRegex);\n if (!startMatch) {\n return null;\n }\n\n const tagName = startMatch[1];\n const attributeString = startMatch[2];\n\n // Create the element\n const element = new Element(tagName);\n\n // Parse attributes\n let attributeMatch;\n while ((attributeMatch = attributeRegex.exec(attributeString)) !== null) {\n const attrName = attributeMatch[1];\n const attrValue = attributeMatch[2] || attributeMatch[3]; // Use whichever capture group matched\n element.attributes[attrName] = attrValue;\n }\n\n // Find the content between start and end tags\n const startTagEnd = startMatch[0].length;\n const endTagSearch = new RegExp(`</${tagName}>`);\n const endMatch = xmlString.slice(startTagEnd).search(endTagSearch);\n\n if (endMatch === -1) {\n // Self-closing or malformed tag\n return element;\n }\n\n const contentString = xmlString.slice(startTagEnd, startTagEnd + endMatch);\n\n // Parse child elements\n let remainingContent = contentString.trim();\n while (remainingContent.length > 0) {\n // Check if there's a child element\n if (remainingContent.startsWith(\"<\") && !remainingContent.startsWith(\"</\")) {\n // Find the next child element\n const childStartMatch = remainingContent.match(startTagRegex);\n if (childStartMatch) {\n const childTagName = childStartMatch[1];\n const childEndTagSearch = new RegExp(`</${childTagName}>`);\n const childEndIndex = remainingContent.search(childEndTagSearch);\n\n if (childEndIndex !== -1) {\n // Extract the complete child element string (including its end tag)\n const childEndTagLength = childTagName.length + 3; // \"</tag>\"\n const childXmlString = remainingContent.slice(0, childEndIndex + childEndTagLength);\n\n // Parse the child element and add it to parent\n const childElement = this.parseElement(childXmlString);\n if (childElement) {\n element.childNodes.push(childElement);\n }\n\n // Remove the processed child from remaining content\n remainingContent = remainingContent.slice(childXmlString.length).trim();\n continue;\n }\n }\n }\n\n // Handle text content\n const nextTagIndex = remainingContent.indexOf(\"<\");\n if (nextTagIndex === -1) {\n // The rest is all text\n element.textContent += remainingContent.trim();\n break;\n } else if (nextTagIndex > 0) {\n // There's some text before the next tag\n element.textContent += remainingContent.slice(0, nextTagIndex).trim();\n remainingContent = remainingContent.slice(nextTagIndex).trim();\n } else {\n // Can't parse further, just break\n break;\n }\n }\n\n return element;\n }\n}\n\n// Export DOMParser as a class\nexport { SimpleDOMParser as DOMParser };\n","import fs from \"node:fs\";\nimport { DOMParser } from \"./simpleXmlParser.js\";\n\n/**\n * XML Parser for PTI format\n * @class\n */\nexport default class PTIXmlParser {\n /** @type {string|null} */\n xmlData = null;\n\t/** @type {Array<any>} */\n\tptiPageArray = [];\n\n\t/**\n\t * Create a new PTIXmlParser\n\t */\n\tconstructor() {\n this.xmlData = null;\n this.ptiPageArray = [];\n }\n\n\t/**\n\t * Parse an XML file\n\t * @param {string} filePath - The path to the XML file\n\t * @param {Function} callback - The callback function\n\t */\n\tparseXml(filePath, callback) {\n\t\tfs.readFile(filePath, 'utf8', (err, data) => {\n\t\t\tif (err) {\n callback(err);\n\t\t\t}\n\t\t\telse {\n\t\t\t\t/** @type {string} */\n\t\t\t\tthis.xmlData = data;\n\n\t\t\t\tvar parser = new DOMParser();\n\t\t\t\tvar dom = parser.parseFromString(this.xmlData);\n\t\t\t\tvar root = dom.documentElement;\n\n\t\t\t\tvar xmlFields = root ? root.getElementsByTagName(\"field\") : [];\n\t\t\t\tvar fields = [];\n\n\t\t\t\tfor (var i = 0; i < xmlFields.length; i++) {\n\t\t\t\t\tvar id = xmlFields[i].getAttribute('id');\n\t\t\t\t\tvar xPos = xmlFields[i].getAttribute('x');\n\t\t\t\t\tvar yPos = xmlFields[i].getAttribute('y');\n\t\t\t\t\tvar width = xmlFields[i].getAttribute('width');\n\t\t\t\t\tvar height = xmlFields[i].getAttribute('height');\n\t\t\t\t\tvar type = xmlFields[i].getAttribute('xsi:type');\n\t\t\t\t\tvar page = xmlFields[i].getAttribute('page');\n\t\t\t\t\tvar fontName = xmlFields[i].getAttribute('fontName');\n\t\t\t\t\tvar fontSize = xmlFields[i].getAttribute('fontSize');\n\n\t\t\t\t\t/** @type {Record<string, any>} */\n\t\t\t\t\tvar item = {};\n\n\t\t\t\t\tvar rectLeft = parseInt(xPos || '0') - 21; //was 23.5\n\t\t\t\t\tvar rectTop = parseInt(yPos || '0') - 20;//was 23\n\t\t\t\t\tvar rectRight = parseInt(String(rectLeft)) + parseInt(width || '0') - 4;\n\t\t\t\t\tvar rectBottom = parseInt(String(rectTop)) + parseInt(height || '0') - 4;\n\n\t\t\t\t\titem.fieldType = \"Tx\";\n\t\t\t\t\tif (type === \"Boolean\") {\n\t\t\t\t\t\titem.fieldType=\"Btn\";\n\t\t\t\t\t}\n\t\t\t\t\telse if (type === \"SSN\" || type === \"Phone\" || type === \"zip\") {\n\t\t\t\t\t\titem.TName = type ? type.toLowerCase() : '';\n\t\t\t\t\t}\n\t\t\t\t\titem.alternativeText = \"\";\n\t\t\t\t\titem.fullName = id || '';\n\t\t\t\t\titem.fontSize = fontSize || '';\n\t\t\t\t\titem.fontName = fontName || '';\n\t\t\t\t\titem.subtype = \"Widget\";\n\n\t\t\t\t\titem.rect = [rectLeft, rectTop, rectRight, rectBottom];\n\n\t\t\t\t\tfields.push(item);\n\n\t\t\t\t\tif (page) {\n\t\t\t\t\t\tthis.ptiPageArray[parseInt(page)] = fields;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t}\n\t\t\tcallback();\n\t\t});\n\t}\n\n\t/**\n\t * Get fields for a specific page\n\t * @param {number} pageNum - The page number\n\t * @returns {Array<any>|undefined} The fields for the page\n\t */\n\tgetFields(pageNum) {\n\t\treturn this.ptiPageArray[pageNum];\n\t}\n}\n","import nodeUtil from \"node:util\";\nimport PDFUnit from \"./pdfunit.js\";\n\nexport default class PDFLine {\n constructor(x1, y1, x2, y2, lineWidth, color, dashed) {\n this.x1 = x1;\n this.y1 = y1;\n this.x2 = x2;\n this.y2 = y2;\n this.lineWidth = lineWidth || 1.0;\n this.color = color;\n this.dashed = dashed;\n }\n\n #setStartPoint(oneLine, x, y) {\n oneLine.x = PDFUnit.toFormX(x);\n oneLine.y = PDFUnit.toFormY(y);\n }\n\n processLine(targetData) {\n const xDelta = Math.abs(this.x2 - this.x1);\n const yDelta = Math.abs(this.y2 - this.y1);\n const minDelta = this.lineWidth;\n\n let oneLine = { x:0, y:0, w: PDFUnit.toFixedFloat(this.lineWidth), l:0 };\n\n //MQZ Aug.28.2013, adding color support, using color dictionary and default to black\n const clrId = PDFUnit.findColorIndex(this.color);\n const colorObj = (clrId > 0 && clrId < PDFUnit.colorCount()) ? { clr: clrId } : { oc: this.color };\n oneLine = { ...oneLine, ...colorObj };\n\n //MQZ Aug.29 dashed line support\n if (this.dashed) {\n oneLine = { ...oneLine, dsh: 1 };\n }\n\n if ((yDelta < this.lineWidth) && (xDelta > minDelta)) { //HLine\n if (this.lineWidth < 4 && (xDelta / this.lineWidth < 4)) {\n nodeUtil.p2jinfo(`Skipped: short thick HLine: lineWidth = ${this.lineWidth}, xDelta = ${xDelta}`);\n return; //skip short thick lines, like PA SPP lines behinds checkbox\n }\n\n oneLine.l = PDFUnit.toFormX(xDelta);\n if (this.x1 > this.x2)\n this.#setStartPoint(oneLine, this.x2, this.y2);\n else\n this.#setStartPoint(oneLine, this.x1, this.y1);\n targetData.HLines.push(oneLine);\n }\n else if ((xDelta < this.lineWidth) && (yDelta > minDelta)) {//VLine\n if (this.lineWidth < 4 && (yDelta / this.lineWidth < 4)) {\n nodeUtil.p2jinfo(`Skipped: short thick VLine: lineWidth = ${this.lineWidth}, yDelta = ${yDelta}`);\n return; //skip short think lines, like PA SPP lines behinds checkbox\n }\n\n oneLine.l = PDFUnit.toFormY(yDelta);\n if (this.y1 > this.y2)\n this.#setStartPoint(oneLine, this.x2, this.y2);\n else\n this.#setStartPoint(oneLine, this.x1, this.y1);\n targetData.VLines.push(oneLine);\n }\n }\n}\n","import nodeUtil from \"node:util\";\nimport PDFUnit from \"./pdfunit.js\";\n\nexport default class PDFFill{\n // constructor\n constructor(x, y, width, height, color) {\n this.x = x;\n this.y = y;\n this.width = width;\n this.height = height;\n this.color = color;\n }\n\n processFill(targetData) {\n //MQZ.07/29/2013: when color is not in color dictionary, set the original color (oc)\n const clrId = PDFUnit.findColorIndex(this.color);\n const colorObj = (clrId > 0 && clrId < PDFUnit.colorCount()) ? { clr: clrId } : { oc: this.color };\n\n const oneFill = { x:PDFUnit.toFormX(this.x),\n y:PDFUnit.toFormY(this.y),\n w:PDFUnit.toFormX(this.width),\n h:PDFUnit.toFormY(this.height),\n ...colorObj };\n\n\n if (oneFill.w < 2 && oneFill.h < 2) {\n nodeUtil.p2jinfo(`Skipped: tiny fill: ${oneFill.w} x ${oneFill.h}`);\n return; //skip short thick lines, like PA SPP lines behinds checkbox\n }\n\n targetData.Fills.push(oneFill);\n }\n}\n","import nodeUtil from \"node:util\";\nimport PDFLine from \"./pdfline.js\";\nimport PDFFill from \"./pdffill.js\";\nimport PDFFont from \"./pdffont.js\";\n\n// alias some functions to make (compiled) code shorter\n// eslint-disable-next-line @typescript-eslint/no-unused-vars\nconst { round: mr, sin: ms, cos: mc, abs, sqrt } = Math;\n\n// precompute \"00\" to \"FF\"\nconst dec2hex = [];\nfor (let i = 0; i < 16; i++) {\n\tfor (let j = 0; j < 16; j++) {\n\t\tdec2hex[i * 16 + j] = i.toString(16) + j.toString(16);\n\t}\n}\n\nfunction createMatrixIdentity() {\n\treturn [\n\t\t[1, 0, 0],\n\t\t[0, 1, 0],\n\t\t[0, 0, 1],\n\t];\n}\n\nfunction matrixMultiply(m1, m2) {\n\tconst result = createMatrixIdentity();\n\n\tfor (let x = 0; x < 3; x++) {\n\t\tfor (let y = 0; y < 3; y++) {\n\t\t\tlet sum = 0;\n\n\t\t\tfor (let z = 0; z < 3; z++) {\n\t\t\t\tsum += m1[x][z] * m2[z][y];\n\t\t\t}\n\n\t\t\tresult[x][y] = sum;\n\t\t}\n\t}\n\treturn result;\n}\n\nfunction copyState(o1, o2) {\n\to2.fillStyle = o1.fillStyle;\n\to2.lineCap = o1.lineCap;\n\to2.lineJoin = o1.lineJoin;\n\to2.lineWidth = o1.lineWidth;\n\to2.miterLimit = o1.miterLimit;\n\to2.shadowBlur = o1.shadowBlur;\n\to2.shadowColor = o1.shadowColor;\n\to2.shadowOffsetX = o1.shadowOffsetX;\n\to2.shadowOffsetY = o1.shadowOffsetY;\n\to2.strokeStyle = o1.strokeStyle;\n\to2.globalAlpha = o1.globalAlpha;\n\to2.arcScaleX_ = o1.arcScaleX_;\n\to2.arcScaleY_ = o1.arcScaleY_;\n\to2.lineScale_ = o1.lineScale_;\n\to2.dashArray = o1.dashArray;\n}\n\nfunction processStyle(styleString) {\n\tlet str,\n\t\talpha = 1;\n\n\tstyleString = String(styleString);\n\tif (styleString.substring(0, 3) === \"rgb\") {\n\t\tconst start = styleString.indexOf(\"(\", 3);\n\t\tconst end = styleString.indexOf(\")\", start + 1);\n\t\tconst guts = styleString.substring(start + 1, end).split(\",\");\n\n\t\tstr = \"#\";\n\t\tfor (let i = 0; i < 3; i++) {\n\t\t\tstr += dec2hex[Number(guts[i])];\n\t\t}\n\n\t\tif (guts.length === 4 && styleString.substring(3, 4) === \"a\") {\n\t\t\talpha = guts[3];\n\t\t}\n\t} else {\n\t\tstr = styleString;\n\t}\n\n\treturn { color: str, alpha };\n}\n\n// eslint-disable-next-line @typescript-eslint/no-unused-vars\nfunction processLineCap(lineCap) {\n\tswitch (lineCap) {\n\t\tcase \"butt\":\n\t\t\treturn \"flat\";\n\t\tcase \"round\":\n\t\t\treturn \"round\";\n\t\tcase \"square\":\n\t\tdefault:\n\t\t\treturn \"square\";\n\t}\n}\n\n// Helper function that takes the already fixed cordinates.\nfunction bezierCurveToHelper(self, cp1, cp2, p) {\n\tself.currentPath_.push({\n\t\ttype: \"bezierCurveTo\",\n\t\tcp1x: cp1.x,\n\t\tcp1y: cp1.y,\n\t\tcp2x: cp2.x,\n\t\tcp2y: cp2.y,\n\t\tx: p.x,\n\t\ty: p.y,\n\t});\n\tself.currentX_ = p.x;\n\tself.currentY_ = p.y;\n}\n\nfunction matrixIsFinite(m) {\n\tfor (let j = 0; j < 3; j++) {\n\t\tfor (let k = 0; k < 2; k++) {\n\t\t\tif (!isFinite(m[j][k]) || isNaN(m[j][k])) {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t}\n\t}\n\treturn true;\n}\n\nfunction setM(ctx, m, updateLineScale) {\n\tif (!matrixIsFinite(m)) {\n\t\treturn;\n\t}\n\tctx.m_ = m;\n\n\tif (updateLineScale) {\n\t\t// Get the line scale.\n\t\t// Determinant of this.m_ means how much the area is enlarged by the\n\t\t// transformation. So its square root can be used as a scale factor\n\t\t// for width.\n\t\tconst det = m[0][0] * m[1][1] - m[0][1] * m[1][0];\n\t\tctx.lineScale_ = sqrt(abs(det));\n\t}\n}\n\n// eslint-disable-next-line @typescript-eslint/naming-convention\nclass CanvasPattern_ {\n\tconstructor() {}\n}\n\n// Gradient / Pattern Stubs\n// eslint-disable-next-line @typescript-eslint/naming-convention\nclass CanvasGradient_ {\n\tconstructor(aType) {\n\t\tthis.type_ = aType;\n\t\tthis.x0_ = 0;\n\t\tthis.y0_ = 0;\n\t\tthis.r0_ = 0;\n\t\tthis.x1_ = 0;\n\t\tthis.y1_ = 0;\n\t\tthis.r1_ = 0;\n\t\tthis.colors_ = [];\n\t}\n\taddColorStop(aOffset, aColor) {\n\t\taColor = processStyle(aColor);\n\t\tthis.colors_.push({\n\t\t\toffset: aOffset,\n\t\t\tcolor: aColor.color,\n\t\t\talpha: aColor.alpha,\n\t\t});\n\t}\n}\n\n/**\n * This class implements CanvasRenderingContext2D interface as described by\n * the WHATWG.\n * @param {HTMLElement} surfaceElement The element that the 2D context should\n * be associated with\n */\n// eslint-disable-next-line @typescript-eslint/naming-convention\nexport default class CanvasRenderingContext2D_ {\n\tconstructor(canvasTarget, scaledWidth, scaledHeight) {\n\t\tthis.m_ = createMatrixIdentity();\n\n\t\tthis.mStack_ = [];\n\t\tthis.aStack_ = [];\n\t\tthis.currentPath_ = [];\n\n\t\t// Canvas context properties\n\t\tthis.strokeStyle = \"#000\";\n\t\tthis.fillStyle = \"#000\";\n\n\t\tthis.lineWidth = 1;\n\t\tthis.lineJoin = \"miter\";\n\t\tthis.lineCap = \"butt\";\n\t\tthis.dashArray = [];\n\t\tthis.miterLimit = 1;\n\t\tthis.globalAlpha = 1;\n\n\t\tif (!(\"HLines\" in canvasTarget) || !Array.isArray(canvasTarget.HLines))\n\t\t\tcanvasTarget.HLines = [];\n\t\tif (!(\"VLines\" in canvasTarget) || !Array.isArray(canvasTarget.VLines))\n\t\t\tcanvasTarget.VLines = [];\n\t\tif (!(\"Fills\" in canvasTarget) || !Array.isArray(canvasTarget.Fills))\n\t\t\tcanvasTarget.Fills = [];\n\t\tif (!(\"Texts\" in canvasTarget) || !Array.isArray(canvasTarget.Texts))\n\t\t\tcanvasTarget.Texts = [];\n\n\t\tthis.canvas = canvasTarget;\n\n\t\tthis.width = scaledWidth;\n\t\tthis.height = scaledHeight;\n\n\t\tthis.arcScaleX_ = 1;\n\t\tthis.arcScaleY_ = 1;\n\t\tthis.lineScale_ = 1;\n\n\t\tthis.currentFont = null;\n\t}\n\n\t//private helper methods\n\t#drawPDFLine(p1, p2, lineWidth, color) {\n\t\tconst dashedLine = Array.isArray(this.dashArray) && this.dashArray.length > 1;\n\t\tconst pL = new PDFLine(p1.x, p1.y, p2.x, p2.y, lineWidth, color, dashedLine);\n\t\tpL.processLine(this.canvas);\n\t}\n\n\t#drawPDFFill(cp, min, max, color) {\n\t\tconst width = max.x - min.x;\n\t\tconst height = max.y - min.y;\n\t\tconst pF = new PDFFill(cp.x, cp.y, width, height, color);\n\t\tpF.processFill(this.canvas);\n\t}\n\n\t#needRemoveRect(x, y, w, h) {\n\t\tconst retVal = Math.abs(w - Math.abs(h)) < 1 && w < 13;\n\t\tif (retVal) {\n\t\t\tnodeUtil.p2jinfo(`Skipped: tiny rect: w=${w}, h=${h}`);\n\t\t}\n\t\treturn retVal;\n\t}\n\n\tgetContext(ctxType) {\n\t\treturn ctxType === \"2d\" ? this : null;\n\t}\n\n\tsetLineDash(lineDash) {\n\t\tthis.dashArray = lineDash;\n\t}\n\n\tgetLineDash() {\n\t\treturn this.dashArray;\n\t}\n\n\tfillText(text, x, y, maxWidth, fontSize) {\n\t\tif (!text || (!text.length === 1 && text.trim().length < 1)) return;\n\t\tconst p = this.getCoords_(x, y);\n\n\t\tconst a = processStyle(this.fillStyle || this.strokeStyle);\n\t\tconst color = a ? a.color : \"#000000\";\n\n\t\tthis.currentFont.processText(\n\t\t\tp,\n\t\t\ttext,\n\t\t\tmaxWidth,\n\t\t\tcolor,\n\t\t\tfontSize,\n\t\t\tthis.canvas,\n\t\t\tthis.m_\n\t\t);\n\t}\n\n\tstrokeText(text, x, y, maxWidth) {\n\t\t//MQZ. 10/23/2012, yeah, no hollow text for now\n\t\tthis.fillText(text, x, y, maxWidth);\n\t}\n\n\tmeasureText(text) {\n\t\tnodeUtil.p2jwarn(\"to be implemented: contextPrototype.measureText - \", text);\n\t\tconst chars = text.length || 1;\n\t\treturn { width: chars * (this.currentFont.spaceWidth || 5) };\n\t}\n\n\tsetFont(fontObj) {\n\t\tif (!!this.currentFont && typeof this.currentFont.clean === \"function\") {\n\t\t\tthis.currentFont.clean();\n\t\t\tthis.currentFont = null;\n\t\t}\n\n\t\tthis.currentFont = new PDFFont(fontObj);\n\t}\n\n\tclearRect() {\n\t\tnodeUtil.p2jwarn(\"to be