UNPKG

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.11 MB
{"version":3,"file":"pdfparser.cjs","sources":["../lib/simpleXmlParser.js","../lib/pdfanno.js","../lib/pdfimage.js","../lib/pdfconst.js","../lib/pdfunit.js","../lib/pdfline.js","../lib/pdffill.js","../lib/pdffont.js","../lib/pdfcanvas.js","../lib/pdfjs-code.js","../lib/pkinfo.js","../lib/pdffield.js","../lib/ptixmlinject.js","../lib/pdf.js","../lib/parserstream.js","../pdfparser.js"],"sourcesContent":["// 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 { PJS } from \"./pdf.js\";\n\n//BEGIN - MQZ 9/19/2012. Helper functions to parse acroForm elements\nfunction setupRadioButton(annotation, item) {\n //let asName = '';\n //PDF Spec p.689: parent item's DV holds the item's value that is selected by default\n const po = annotation.get('Parent');\n if (po) {\n po.forEach((key, val) => {\n if (key === 'DV') {\n //asName = val.name || '';\n }\n else if (key === 'TU') {\n //radio buttons use the alternative text from the parent\n item.alternativeText = val;\n } else if( key === 'TM') {\n item.alternativeID = val;\n }\n });\n }\n\n //PDF Spec p.606: get appearance dictionary\n const ap = annotation.get('AP');\n //PDF Spec p.614 get normal appearance\n const nVal = ap.get('N');\n //PDF Spec p.689\n nVal.forEach((key, value) => {\n if (key.toLowerCase() !== \"off\") {\n //value if selected\n item.value = key; //export value\n \n item.checked = (item.fieldValue === item.value); //initial selection state\n }\n });\n\n if (!item.value)\n item.value = \"off\";\n}\n\nfunction setupPushButton(annotation, item) {\n //button label: PDF Spec p.640\n const mk = annotation.get('MK');\n if(mk) {\n item.value = mk.get('CA') || '';\n }\n\n //button action: url when mouse up: PDF Spec:p.642\n item.FL = \"\";\n const ap = annotation.get('A');\n if (ap) {\n const sp = ap.get('S');\n item.FL = ap.get(sp.name);\n }\n}\n\nfunction setupCheckBox(annotation, item) {\n //PDF Spec p.606: get appearance dictionary\n const ap = annotation.get('AP');\n //PDF Spec p.614 get normal appearance\n const nVal = ap.get('N');\n\n //PDF Spec p.689\n let i = 0;\n nVal.forEach((key, value) => {\n i++;\n if (i === 1) //initial selection state\n item.value = key;\n });\n}\n\nfunction setupDropDown(annotation, item) {\n //PDF Spec p.688\n item.value = annotation.get('Opt') || [];\n}\n\nfunction setupFieldAttributes(annotation, item) {\n //MQZ. Jan.03.2013. additional-actions dictionary\n //PDF Spec P.648. 8.5.2. Trigger Events\n const aa = annotation.get('AA');\n if (!aa) {\n return;\n }\n\n //PDF Spec p.651 get format dictionary\n let nVal = aa.get('F');\n if (!nVal) {\n nVal = aa.get('K');\n if (!nVal)\n return;\n }\n\n nVal.forEach((key, value) => {\n if (key === \"JS\") {\n processFieldAttribute(value, item);\n }\n });\n}\n\nconst AFSpecialFormat = ['zip', 'zip', 'phone', 'ssn', ''];\n// let AFNumber_Format = ['nDec', 'sepStyle', 'negStyle', 'currStyle', 'strCurrency', 'bCurrencyPrepend'];\n//– nDec is the number of places after the decimal point;\n//– sepStyle is an integer denoting whether to use a separator or not. If sepStyle=0, use commas. If sepStyle=1, do not separate.\n//– negStyle is the formatting used for negative numbers: 0 = MinusBlack, 1 = Red, 2 = ParensBlack, 3 = ParensRed\n//– currStyle is the currency style - not used\n//- strCurrency is the currency symbol\n//– bCurrencyPrepend\n// let AFDate_FormatEx = [\"m/d\", \"m/d/yy\", \"mm/dd/yy\", \"mm/yy\", \"d-mmm\", \"d-mmm-yy\", \"dd-mmm-yy\", \"yymm-dd\", \"mmm-yy\", \"mmmm-yy\", \"mmm d, yyyy\", \"mmmm d, yyyy\", \"m/d/yy h:MM tt\", \"m/d/yy HH:MM\"];\n\nfunction processFieldAttribute(jsFuncName, item) {\n if (Object.prototype.hasOwnProperty.call(item, 'TName'))\n return;\n\n if(!jsFuncName.split)\n return;\n\n const vParts = jsFuncName.split('(');\n if (vParts.length !== 2)\n return;\n\n const funcName = vParts[0];\n const funcParam = vParts[1].split(')')[0];\n\n\tswitch (funcName) {\n\t\tcase 'AFSpecial_Format':\n\t\t\titem.TName = AFSpecialFormat[Number(funcParam)];\n\t\t\tbreak;\n\t\tcase 'AFNumber_Format':\n\t\t\t// nfs = funcParam.split(',');\n\t\t\t//set the Money fields to use the Number type with no decimal places after, no commas, and bCurrencyPrepend is set as true; (o use a negative sign (fits the PDF layout and our print formatting as well).\n\t\t\t// if (nfs[0] === '0' && nfs[1] === '1' && nfs[5])\n\t\t\t// item.TName = 'money';\n\t\t\t// else\n\t\t\titem.TName = 'number';\n\t\t\tbreak;\n\t\tcase 'AFDate_FormatEx':\n\t\t\titem.TName = 'date';\n\t\t\titem.MV = funcParam.replace(/^'+|^\"+|'+$|\"+$/g, ''); //mask value\n\t\t\tbreak;\n\t\tcase 'AFSpecial_KeystrokeEx': { //special format: \"arbitrary mask\"\n\t\t\t\tconst maskValue = funcParam.replace(/^'+|^\"+|'+$|\"+$/g, ''); //mask value\n\t\t\t\tif ((!!maskValue) && maskValue.length > 0 && maskValue.length < 64) {\n\t\t\t\t\titem.TName = 'mask'; //fixed length input\n\t\t\t\t\titem.MV = maskValue;\n\t\t\t\t}\n\t\t\t}\n break;\n case 'AFPercent_Format':\n item.TName = 'percent'; //funcParam => 2, 0, will specified how many decimal places\n break;\n }\n}\n\nfunction setupSignature(annotation, item) {\n //PDF Spec p.695: field value is signature dict if signed\n const sig = annotation.get('V');\n if (!sig) return;\n\n //PDF Spec p.728: get signature information\n item.Sig = {};\n const name = sig.get('Name');\n if (name) item.Sig.Name = name;\n const time = sig.get('M');\n if (time) item.Sig.M = time;\n const location = sig.get('Location');\n if (location) item.Sig.Location = location;\n const reason = sig.get('Reason');\n if (reason) item.Sig.Reason = reason;\n const contactInfo = sig.get('ContactInfo');\n if (contactInfo) item.Sig.ContactInfo = contactInfo;\n}\n\n//END - MQZ 9/19/2012. Helper functions to parse acroForm elements\n\nexport default class PDFAnno {\n static processAnnotation(annotation, item) {\n if (item.fieldType === 'Btn') { //PDF Spec p.675\n if (item.fieldFlags & 32768) {\n setupRadioButton(annotation, item);\n }\n else if (item.fieldFlags & 65536) {\n setupPushButton(annotation, item);\n }\n else {\n setupCheckBox(annotation, item);\n }\n }\n else if (item.fieldType === 'Ch') {\n setupDropDown(annotation, item);\n }\n else if (item.fieldType === 'Tx') {\n setupFieldAttributes(annotation, item);\n }\n else if (item.fieldType === 'Sig') {\n setupSignature(annotation, item);\n }\n else {\n PJS.warn(\"Unknown fieldType: \", item);\n }\n }\n}\n","\nimport { Buffer } from \"node:buffer\";\nexport default class PDFImage {\n\t#_src = '';\n\t#_onload = null;\n\n\tset onload(val) {\n\t\tthis.#_onload = typeof val === 'function' ? val : null;\n\t}\n\n\tget onload() {\n\t\treturn this.#_onload;\n\t}\n\n\tset src(val) {\n\t\tthis.#_src = val;\n\t\tif (this.#_onload) this.#_onload();\n\t}\n\n\tget src() {\n\t\treturn this.#_src;\n\t}\n\n btoa(val) {\n\t\treturn (new Buffer.from(val, 'binary')).toString('base64'); // ascii?\n }\n\n}\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 { PJS } from \"./pdf.js\";\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 PJS.info(`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 PJS.info(`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 { PJS } from \"./pdf.js\";\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 PJS.info(`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 { PJS } from \"./pdf.js\";\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 PJS.info(\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 PJS.info(\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 PJS.info(`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 PJS.info(`${this.fontObj.type} - SymbolicFont - (${this.fontObj.name}) : ${str.charCodeAt(0)}::${str.charCodeAt(1)} => ${retVal}`);\n }\n\n return retVal;\n }\n\n #processType3Font(str) {\n // Special handling for Type3 fonts\n if (!str || str.length !== 1 || this.fontObj.type !== 'Type3') {\n return str;\n }\n\n // Debug info\n PJS.info(`Processing Type3 font: char code = ${str.charCodeAt(0)}, char = '${str}'`);\n if (this.fontObj.charProcMapping) {\n PJS.info(`charProcMapping available with ${Object.keys(this.fontObj.charProcMapping).length} entries`);\n } else {\n PJS.info(`No charProcMapping available for this Type3 font`);\n \n // If no mapping is available, try to use the character directly\n if (str && str.length === 1) {\n const code = str.charCodeAt(0);\n if (code >= 65 && code <= 90) { // A-Z\n PJS.info(`Using direct uppercase letter: ${str}`);\n return str;\n } else if (code >= 97 && code <= 122) { // a-z\n PJS.info(`Using direct lowercase letter: ${str}`);\n return str;\n } else if (code >= 48 && code <= 57) { // 0-9\n PJS.info(`Using direct digit: ${str}`);\n return str;\n }\n }\n }\n\n // Use the charProcMapping if available to map character code to glyph name\n if (this.fontObj.charProcMapping) {\n const charCode = str.charCodeAt(0);\n const glyphName = this.fontObj.charProcMapping[charCode];\n \n if (glyphName) {\n PJS.info(`Found glyph name in mapping: ${glyphName}`);\n // Map common Type3 glyph names to Unicode characters\n const glyphToUnicode = {\n 'bullet': '\\u2022',\n 'checkbox': '\\u2610',\n 'checkmark': '\\u2713',\n 'check': '\\u2713',\n 'circle': '\\u25CB',\n 'square': '\\u25A1',\n 'triangle': '\\u25B2',\n 'triangledown': '\\u25BC',\n 'triangleleft': '\\u25C0',\n 'triangleright': '\\u25B6',\n 'star': '\\u2605',\n 'diamond': '\\u25C6',\n 'heart': '\\u2665',\n 'club': '\\u2663',\n 'spade': '\\u2660',\n 'filledcircle': '\\u25CF',\n 'filledsquare': '\\u25A0',\n 'filledtriangle': '\\u25B2',\n 'filledtriangledown': '\\u25BC',\n 'filledtriangleright': '\\u25B6',\n 'filledtriangleleft': '\\u25C0',\n 'arrowleft': '\\u2190',\n 'arrowright': '\\u2192',\n 'arrowup': '\\u2191',\n 'arrowdown': '\\u2193',\n 'cross': '\\u2717'\n };\n \n // Check for direct match\n const glyphNameLower = typeof glyphName === 'string' ? glyphName.toLowerCase() : '';\n if (glyphNameLower in glyphToUnicode) {\n const unicodeChar = glyphToUnicode[/** @type {keyof typeof glyphToUnicode} */ (glyphNameLower)];\n PJS.info(`Mapped ${glyphNameLower} to Unicode ${unicodeChar}`);\n return unicodeChar;\n }\n \n // Check for letters in the glyph name (g0, g1, etc.)\n if (typeof glyphName === 'string' && glyphName.length > 1) {\n const letterMatch = glyphName.match(/[A-Za-z]/g);\n if (letterMatch && letterMatch.length === 1) {\n const letter = letterMatch[0].toUpperCase();\n PJS.info(`Extracted letter ${letter} from glyph name ${glyphName}`);\n return letter;\n }\n }\n \n // Check for partial match (glyph name contains known keyword)\n for (const key in glyphToUnicode) {\n if (glyphNameLower.indexOf(key) >= 0) {\n const unicodeChar = glyphToUnicode[/** @type {keyof typeof glyphToUnicode} */ (key)];\n PJS.info(`Partial match: ${glyphNameLower} contains ${key}, mapped to ${unicodeChar}`);\n return unicodeChar;\n }\n }\n \n // Try to match letters in the glyph name (e.g. g26 -> \"C\", g28 -> \"O\", etc.)\n // Look for letter patterns in the glyph name \n if (typeof glyphName === 'string') {\n // Try to extract letter from glyph name\n const letterMatch = glyphName.match(/[A-Za-z]/g);\n if (letterMatch && letterMatch.length === 1) {\n const letter = letterMatch[0].toUpperCase();\n PJS.info(`Extracted letter ${letter} from glyph name ${glyphName}`);\n return letter;\n }\n \n // Handle number in glyph name to suggest possible letter\n const numberMatch = glyphName.match(/\\d+/);\n if (numberMatch && numberMatch.length === 1) {\n const num = parseInt(numberMatch[0], 10);\n // Map numbers to alphabet (1=A, 2=B, etc.)\n if (num >= 1 && num <= 26) {\n const letter = String.fromCharCode(64 + num); // ASCII 'A' is 65\n PJS.info(`Mapped number ${num} in glyph name ${glyphName} to letter ${letter}`);\n return letter;\n }\n }\n }\n \n // Handle uniXXXX format glyph names\n if (typeof glyphName === 'string' && glyphName.startsWith('uni')) {\n const hex = glyphName.substring(3);\n if (/^[0-9A-F]{4,6}$/i.test(hex)) {\n PJS.info(`Mapped uni${hex} to Unicode character`);\n return String.fromCharCode(parseInt(hex, 16));\n }\n }\n }\n }\n \n // If we reach here, try direct character code mapping\n const charCode = str.charCodeAt(0);\n \n // No hard-coded directMappings, rely on charProcMapping from the font object\n PJS.info(`No direct mapping for character code ${charCode}, checking general mappings`);\n \n \n // Direct mapping for common Type3 glyph character codes\n let result = str;\n switch (charCode) {\n case 18: result = '\\u2713'; break; // Check mark\n case 19: result = '\\u2610'; break; // Ballot box\n case 20: result = '\\u2611'; break; // Ballot box with check\n case 108: result = '\\u2022'; break; // Bullet\n case 109: result = '\\u25CF'; break; // Black circle\n case 110: result = '\\u25CB'; break; // White circle\n case 111: result = '\\u25A0'; break; // Black square\n case 112: result = '\\u25A1'; break; // White square\n case 113: result = '\\u25B2'; break; // Black up-pointing triangle\n case 114: result = '\\u25BC'; break; // Black down-pointing triangle\n case 117: result = '\\u2190'; break; // Left arrow\n case 118: result = '\\u2192'; break; // Right arrow\n case 119: result = '\\u2191'; break; // Up arrow\n case 120: result = '\\u2193'; break; // Down arrow\n case 128: result = '\\u221E'; break; // Infinity\n case 129: result = '\\u2260'; break; // Not equal\n case 130: result = '\\u2264'; break; // Less than or equal\n case 131: result = '\\u2265'; break; // Greater than or equal\n }\n \n if (result !== str) {\n PJS.info(`Mapped char code ${charCode} to Unicode ${result}`);\n } else {\n PJS.info(`No mapping found for char code ${charCode}, returning original character`);\n }\n \n return result;\n }\n\n /**\n * Calculate the rotation angle from a 2D transformation matrix\n * @param {number[][]} matrix2D - The 2D transformation matrix\n * @returns {number} - The rotation angle in degrees\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 /**\n * Process text for rendering\n * @param {{x: number, y: number}} p - The position\n * @param {string} str - The text string\n * @param {number} maxWidth - Maximum width\n * @param {string} color - Color value\n * @param {number} fontSize - Font size\n * @param {{Texts: Array<any>}} targetData - Target data object\n * @param {number[][]} matrix2D - 2D transformation matrix\n */\n processText(p, str, maxWidth, color, fontSize, targetData, matrix2D) {\n // Debug the incoming text processing\n PJS.info(`Processing text: '${str}', font type: ${this.fontObj.type || 'unknown'}, char code: ${str ? str.charCodeAt(0) : 'none'}`);\n\n // Save original text for fallback\n const originalStr = str;\n \n // First try to process Type3 fonts, then fall back to symbolic fonts\n let text = this.fontObj.type === 'Type3' ? \n this.#processType3Font(str) : \n this.#processSymbolicFont(str);\n \n if (!text) {\n PJS.info('Text processing returned null or empty, falling back to original text');\n text = originalStr; // Use original text as fallback\n }\n \n PJS.info(`Processed text: '${str}' -> '${text}'`);\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 PJS.info(`${str}: rotated ${rAngle} degree.`);\n // Add RA property safely\n textRun = Object.assign({}, 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 // TT: this.fontObj.isSymbolicFont || this.fontObj.type === 'Type3' ? 1 : 0, // Add TT flag for symbolic and Type3 fonts\n };\n\n PJS.info(`Adding text to output: '${text}'`);\n targetData.Texts.push(oneText);\n }\n\n /**\n * Encode text for output - preserves UTF-8 multi-byte characters\n * NOTE: Breaking change in v3.3.0 - removed URI encoding to fix issue #385\n * Chinese/Japanese/Korean and other multi-byte characters now output as UTF-8\n * @param {string} str - The string to encode\n * @returns {string} - The encoded string with legacy character replacements\n */\n flashEncode(str) {\n if (!str) return str;\n \n let retVal = str;\n \n // Apply legacy Flash-specific character replacements\n // These handle problematic characters from old PDF encodings\n retVal = retVal.replace(/\\u0096/g, '-'); // En dash\n retVal = retVal.replace(/\\u0091/g, \"'\"); // Left single quote\n retVal = retVal.replace(/\\u0092/g, \"'\"); // Right single quote\n retVal = retVal.replace(/\\u0082/g, \"'\"); // Low single quote\n retVal = retVal.replace(/\\u0093/g, '\"'); // Left double quote\n retVal = retVal.replace(/\\u0094/g, '\"'); // Right double quote\n retVal = retVal.replace(/\\u0084/g, '\"'); // Low double quote\n retVal = retVal.replace(/\\u008B/g, '«'); // Left guillemet\n retVal = retVal.replace(/\\u009B/g, '»'); // Right guillemet\n \n return retVal;\n }\n\n clean() {\n this.fontObj = null;\n delete this.fontObj;\n }\n}\n","import { PJS } from \"./pdf.js\";\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