diff --git a/README.md b/README.md index c1432fb..5c149bb 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,11 @@ This layer contains code to interact with printers - [TSPL](documentations/TSPL.pdf) -### Usefull units: +# Usefull units: - 1 pt = 1/72 inch -- 1 dot = 1 / dpi \ No newline at end of file +- 1 dot = 1 / dpi + +# Notes + +- If a font is not working, make sure the extension is TTF \ No newline at end of file diff --git a/package.json b/package.json index 536f34d..e573c91 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,9 @@ "repository": "https://github.com/kemkriszt/raw-thermal-print.git", "devDependencies": { "@changesets/cli": "^2.27.1", + "@types/fontkit": "^2.0.6", "@types/jest": "^29.5.11", + "@types/w3c-web-usb": "^1.0.10", "jest": "^29.7.0", "ts-jest": "^29.1.1", "ts-node": "^10.9.2", @@ -34,9 +36,9 @@ "typescript": "^5.3.3" }, "dependencies": { - "@types/w3c-web-usb": "^1.0.10", - "clean-html": "^2.0.1", + "fontkit": "^2.0.2", "image-pixels": "^2.2.2", + "node-html-parser": "^6.1.12", "usb": "^2.11.0" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f4bd8dd..e364f0d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,15 +5,15 @@ settings: excludeLinksFromLockfile: false dependencies: - '@types/w3c-web-usb': - specifier: ^1.0.10 - version: 1.0.10 - clean-html: - specifier: ^2.0.1 - version: 2.0.1 + fontkit: + specifier: ^2.0.2 + version: 2.0.2 image-pixels: specifier: ^2.2.2 version: 2.2.2 + node-html-parser: + specifier: ^6.1.12 + version: 6.1.12 usb: specifier: ^2.11.0 version: 2.11.0 @@ -22,9 +22,15 @@ devDependencies: '@changesets/cli': specifier: ^2.27.1 version: 2.27.1 + '@types/fontkit': + specifier: ^2.0.6 + version: 2.0.6 '@types/jest': specifier: ^29.5.11 version: 29.5.11 + '@types/w3c-web-usb': + specifier: ^1.0.10 + version: 1.0.10 jest: specifier: ^29.7.0 version: 29.7.0(@types/node@20.10.4)(ts-node@10.9.2) @@ -1225,6 +1231,19 @@ packages: '@sinonjs/commons': 3.0.0 dev: true + /@swc/helpers@0.4.14: + resolution: {integrity: sha512-4C7nX/dvpzB7za4Ql9K81xK3HPxCpHMgwTZVyf+9JQ6VUbn9jjZVN7/Nkdz/Ugzs2CSjqnL/UPXroiVBVHUWUw==} + dependencies: + tslib: 2.6.2 + dev: false + + /@swc/helpers@0.4.36: + resolution: {integrity: sha512-5lxnyLEYFskErRPenYItLRSge5DjrJngYKdVjRSrWfza9G6KkgHEXi0vUZiyUeMU5JfXH1YnvXZzSp8ul88o2Q==} + dependencies: + legacy-swc-helpers: /@swc/helpers@0.4.14 + tslib: 2.6.2 + dev: false + /@tsconfig/node10@1.0.9: resolution: {integrity: sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==} dev: true @@ -1270,6 +1289,12 @@ packages: '@babel/types': 7.23.5 dev: true + /@types/fontkit@2.0.6: + resolution: {integrity: sha512-GxeaJay2RidoCWRDogp2CRttkv0weLwrB7hC+Qmh0mCdW1g0aEVwlLA6kLv5ZDJ6hGZSEiGZG7r57MgAD+fWOg==} + dependencies: + '@types/node': 20.10.4 + dev: true + /@types/graceful-fs@4.1.9: resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} dependencies: @@ -1327,7 +1352,6 @@ packages: /@types/w3c-web-usb@1.0.10: resolution: {integrity: sha512-CHgUI5kTc/QLMP8hODUHhge0D4vx+9UiAwIGiT0sTy/B2XpdX1U5rJt6JSISgr6ikRT7vxV9EVAFeYZqUnl1gQ==} - dev: false /@types/yargs-parser@21.0.3: resolution: {integrity: sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==} @@ -1584,6 +1608,10 @@ packages: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} dev: true + /base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + dev: false + /bcrypt-pbkdf@1.0.2: resolution: {integrity: sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==} dependencies: @@ -1606,6 +1634,10 @@ packages: resolution: {integrity: sha512-vHdS19CnY3hwiNdkaqk93DvjVLfbEcI8mys4UjuWrlX1haDmroo8o4xCzh4wD6DGV6HxRCyauwhHRqMTfERtjw==} dev: false + /boolbase@1.0.0: + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + dev: false + /brace-expansion@1.1.11: resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} dependencies: @@ -1632,6 +1664,12 @@ packages: wcwidth: 1.0.1 dev: true + /brotli@1.3.3: + resolution: {integrity: sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==} + dependencies: + base64-js: 1.5.1 + dev: false + /browserslist@4.22.2: resolution: {integrity: sha512-0UgcrvQmBDvZHFGdYUehrCNIazki7/lUP3kkoi/r3YB2amZbFM9J43ZRkJTXBUZK4gmx56+Sqk9+Vs9mwZx9+A==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} @@ -1773,14 +1811,6 @@ packages: resolution: {integrity: sha512-kgMuFyE78OC6Dyu3Dy7vcx4uy97EIbVxJB/B0eJ3bUNAkwdNcxYzgKltnyADiYwsR7SEqkkUPsEUT//OVS6XMA==} dev: false - /clean-html@2.0.1: - resolution: {integrity: sha512-8nqCWku8DLhZfDAtb6QRoRvT2o5BOW9nh8QlmpS2ujkyEz5TBDHNIAvL4PK+mNp52ir6zm5pAJJ3XOYtBe4mqw==} - hasBin: true - dependencies: - htmlparser2: 8.0.2 - minimist: 1.2.8 - dev: false - /clip-pixels@1.0.1: resolution: {integrity: sha512-nJ22fZvCwkJfMppkOEE7GciLX08rDnVzEJ+U46kBFZtwNzH2V4tNxMWa9Tc365WspCxy1c3NtGJ5EeT4SgjmCA==} dev: false @@ -1807,6 +1837,11 @@ packages: engines: {node: '>=0.8'} dev: true + /clone@2.1.2: + resolution: {integrity: sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==} + engines: {node: '>=0.8'} + dev: false + /co@4.6.0: resolution: {integrity: sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==} engines: {iojs: '>= 1.0.0', node: '>= 0.12.0'} @@ -1919,6 +1954,21 @@ packages: which: 2.0.2 dev: true + /css-select@5.1.0: + resolution: {integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==} + dependencies: + boolbase: 1.0.0 + css-what: 6.1.0 + domhandler: 5.0.3 + domutils: 3.1.0 + nth-check: 2.1.1 + dev: false + + /css-what@6.1.0: + resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==} + engines: {node: '>= 6'} + dev: false + /csv-generate@3.4.3: resolution: {integrity: sha512-w/T+rqR0vwvHqWs/1ZyMDWtHHSJaN06klRqJXBEpDJaM/+dZkso0OKh1VcuuYvK3XM53KysVNq8Ko/epCK8wOw==} dev: true @@ -2026,6 +2076,10 @@ packages: engines: {node: '>=8'} dev: true + /dfa@1.2.0: + resolution: {integrity: sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==} + dev: false + /diff-sequences@29.6.3: resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -2370,6 +2424,20 @@ packages: resolution: {integrity: sha512-oXbJGbjDnfJRWPC7Va38EFhd+A8JWE5/hCiKcK8qjCdbLj9DTpsq6MEudwpRTH+V4qq+Jw7d3pUgQdSr3x3mTA==} dev: false + /fontkit@2.0.2: + resolution: {integrity: sha512-jc4k5Yr8iov8QfS6u8w2CnHWVmbOGtdBtOXMze5Y+QD966Rx6PEVWXSEGwXlsDlKtu1G12cJjcsybnqhSk/+LA==} + dependencies: + '@swc/helpers': 0.4.36 + brotli: 1.3.3 + clone: 2.1.2 + dfa: 1.2.0 + fast-deep-equal: 3.1.3 + restructure: 3.0.0 + tiny-inflate: 1.0.3 + unicode-properties: 1.4.1 + unicode-trie: 2.0.0 + dev: false + /for-each@0.3.3: resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==} dependencies: @@ -2619,6 +2687,11 @@ packages: function-bind: 1.1.2 dev: true + /he@1.2.0: + resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} + hasBin: true + dev: false + /hosted-git-info@2.8.9: resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} dev: true @@ -2627,15 +2700,6 @@ packages: resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} dev: true - /htmlparser2@8.0.2: - resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==} - dependencies: - domelementtype: 2.3.0 - domhandler: 5.0.3 - domutils: 3.1.0 - entities: 4.5.0 - dev: false - /http-signature@1.2.0: resolution: {integrity: sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==} engines: {node: '>=0.8', npm: '>=1.3.7'} @@ -3715,6 +3779,7 @@ packages: /minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + dev: true /minipass@7.0.4: resolution: {integrity: sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==} @@ -3751,6 +3816,13 @@ packages: hasBin: true dev: false + /node-html-parser@6.1.12: + resolution: {integrity: sha512-/bT/Ncmv+fbMGX96XG9g05vFt43m/+SYKIs9oAemQVYyVcZmDAI2Xq/SbNcpOA35eF0Zk2av3Ksf+Xk8Vt8abA==} + dependencies: + css-select: 5.1.0 + he: 1.2.0 + dev: false + /node-int64@0.4.0: resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} dev: true @@ -3780,6 +3852,12 @@ packages: path-key: 3.1.1 dev: true + /nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + dependencies: + boolbase: 1.0.0 + dev: false + /oauth-sign@0.9.0: resolution: {integrity: sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==} dev: false @@ -3877,6 +3955,10 @@ packages: engines: {node: '>=6'} dev: true + /pako@0.2.9: + resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==} + dev: false + /pako@1.0.11: resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} dev: false @@ -4190,6 +4272,10 @@ packages: supports-preserve-symlinks-flag: 1.0.0 dev: true + /restructure@3.0.0: + resolution: {integrity: sha512-Xj8/MEIhhfj9X2rmD9iJ4Gga9EFqVlpMj3vfLnV2r/Mh5jRMryNV+6lWh9GdJtDBcBSPIqzRdfBQ3wDtNFv/uw==} + dev: false + /reusify@1.0.4: resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} @@ -4598,6 +4684,10 @@ packages: any-promise: 1.3.0 dev: true + /tiny-inflate@1.0.3: + resolution: {integrity: sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==} + dev: false + /tmp@0.0.33: resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} engines: {node: '>=0.6.0'} @@ -4742,6 +4832,10 @@ packages: strip-bom: 3.0.0 dev: true + /tslib@2.6.2: + resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} + dev: false + /tsup@8.0.1(ts-node@10.9.2)(typescript@5.3.3): resolution: {integrity: sha512-hvW7gUSG96j53ZTSlT4j/KL0q1Q2l6TqGBFc6/mu/L46IoNWqLLUzLRLP1R8Q7xrJTmkDxxDoojV5uCVs1sVOg==} engines: {node: '>=18'} @@ -4896,6 +4990,20 @@ packages: resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} dev: true + /unicode-properties@1.4.1: + resolution: {integrity: sha512-CLjCCLQ6UuMxWnbIylkisbRj31qxHPAurvena/0iwSVbQ2G1VY5/HjV0IRabOEbDHlzZlRdCrD4NhB0JtU40Pg==} + dependencies: + base64-js: 1.5.1 + unicode-trie: 2.0.0 + dev: false + + /unicode-trie@2.0.0: + resolution: {integrity: sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==} + dependencies: + pako: 0.2.9 + tiny-inflate: 1.0.3 + dev: false + /universalify@0.1.2: resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} engines: {node: '>= 4.0.0'} diff --git a/src/commands/tspl/commands/basic/TSPLDisplay.ts b/src/commands/tspl/commands/basic/TSPLDisplay.ts index 9ea8c3f..c782b57 100644 --- a/src/commands/tspl/commands/basic/TSPLDisplay.ts +++ b/src/commands/tspl/commands/basic/TSPLDisplay.ts @@ -1,6 +1,6 @@ import TSPLCommand from "../../TSPLCommand"; -export type DisplayType = "CLS"|"IMAGE" +export type DisplayType = "CLS"|"IMAGE"|"OFF" /** * Displays the image buffer on the screen diff --git a/src/examples/node/index.ts b/src/examples/node/index.ts index b159127..a2731b8 100644 --- a/src/examples/node/index.ts +++ b/src/examples/node/index.ts @@ -2,29 +2,63 @@ import { Label } from "@/labels" import { Line, Text } from "@/labels/fields" import { PrinterService } from "@/printers" import fs from "fs" +import { NodeType, parse, HTMLElement } from "node-html-parser" +import fontkit from "fontkit" export default async () => { + // const rootNode = parse("Some cool text first u Other remaining u cool asd asd qwe") + // rootNode.childNodes.forEach(node => { + // if(node.nodeType == NodeType.TEXT_NODE) { + // console.log("T (", node.innerText, ")") + // } else if(node.nodeType == NodeType.ELEMENT_NODE) { + // const casted = node as HTMLElement + // console.log() + // console.log(casted.rawTagName, " ----> ") + + // casted.childNodes.forEach((element) => { + // if(element.nodeType == NodeType.TEXT_NODE) { + // console.log("T (", element.innerText, ")") + // } else if(element.nodeType == NodeType.ELEMENT_NODE) { + // const casted2 = element as HTMLElement + // console.log(casted2.rawTagName, " (", element.innerText, ")") + // } + // }) + + // console.log(casted.rawTagName, " <---- ") + // console.log() + // } + // }) + const printers = await PrinterService.getPrinters() console.log("Printers", printers) if(printers.length > 0) { const printer = printers[0] - const fontName = "super.TTF" - const testText = "Hello 4" - const fontSize = 40 + const fontName = "super" + const fontName2 = "mathova" + const testText = "Hello 4 from the other side" + const fontSize = 25 const textX = 10 const textY = 10 - const font = fs.readFileSync(__dirname+"/"+fontName).buffer + const font = fs.readFileSync(__dirname+"/"+fontName+".TTF").buffer + const font2 = fs.readFileSync(__dirname+"/"+fontName2+".TTF").buffer const label = new Label(50, 25) - const text = new Text(testText, textX, textY) - const line = new Line({x: textX, y: textY + fontSize}, {x: textX + 100, y: textY + fontSize}) - text.setFont(fontName, fontSize) label.registerFont(font, fontName) - label.add(text, line) + label.registerFont(font2, fontName2) + + const text = new Text(testText, textX, textY, true) + // const line = new Line({x: textX, y: textY + fontSize}, {x: textX , y: textY + fontSize}) + const line2 = new Line({x: textX - 5, y: textY + fontSize}, {x: textX - 5, y: textY}) + + text.setFont(fontName2, fontSize) + text.setMultiLine(150) + + + label.add(text) // line2 await printer.display(label) await printer.close() diff --git a/src/examples/node/mathova.ttf b/src/examples/node/mathova.ttf new file mode 100644 index 0000000..c418f73 Binary files /dev/null and b/src/examples/node/mathova.ttf differ diff --git a/src/helpers/StringUtils.ts b/src/helpers/StringUtils.ts index bff5405..667c621 100644 --- a/src/helpers/StringUtils.ts +++ b/src/helpers/StringUtils.ts @@ -20,4 +20,6 @@ export default class StringUtils { let decoder = new TextDecoder() return decoder.decode(bytes) } -} \ No newline at end of file +} + +export const isWhitespace = (text: string) => text.trim() === "" \ No newline at end of file diff --git a/src/helpers/UnitUtils.ts b/src/helpers/UnitUtils.ts index 3062c74..da4565f 100644 --- a/src/helpers/UnitUtils.ts +++ b/src/helpers/UnitUtils.ts @@ -16,4 +16,18 @@ export function valueWithUnit(value: number, unitSystem: UnitSystem) { export function dotToPoint(dots: number, dpi: number): number { const inch = dots / dpi return Math.round(inch * pointsPerInch) +} + +/** + * Converts the points value to dots + * 1 inch = 72 points (standard in typography) + * Formula: dots = points * dpi / pointsPerInch + * @param points + * @param dpi + * @returns + */ +export function pointsToDots(points: number, dpi: number): number { + const pointsPerInch = 72; + const dots = points * dpi / pointsPerInch; + return dots; } \ No newline at end of file diff --git a/src/labels/Label.ts b/src/labels/Label.ts index 94d3c22..6952c59 100644 --- a/src/labels/Label.ts +++ b/src/labels/Label.ts @@ -1,10 +1,12 @@ import { Command, PrinterLanguage } from "@/commands"; import Printable, { PrintConfig } from "./Printable"; import { UnitSystem } from "@/commands"; -import { LabelDirection, TSPLCommand, TSPLRawCommand } from "@/commands/tspl"; +import { LabelDirection } from "@/commands/tspl"; import LabelField from "./fields/LabelField"; import { Font } from "./types"; import CommandGenerator from "@/commands/CommandGenerator"; +import fontkit from "fontkit" +import { dotToPoint, pointsToDots } from "@/helpers/UnitUtils"; /** * Holds the content of a label and handles printing @@ -23,7 +25,7 @@ export default class Label extends Printable { * Units for width, height, gap and offset */ private readonly unitSystem: UnitSystem - private fonts: Font[] = [] + private fonts: Record = {} private dpi: number /** @@ -34,8 +36,19 @@ export default class Label extends Printable { /** * Configuration used when generating commands */ - private get printConfig(): PrintConfig { - return { dpi: this.dpi } + get printConfig(): PrintConfig { + return { + dpi: this.dpi, + textWidth: (text, font, fontSize) => { + const size = dotToPoint(fontSize, this.dpi) + const fontObject = this.fonts[font].font + + const run = fontObject.layout(text) + + const scaledWidth = size * run.advanceWidth / fontObject.unitsPerEm + return pointsToDots(scaledWidth, this.dpi) + } + } } constructor(width: number, height: number, dimensionUnit: UnitSystem = "metric", dpi: number = 203) { @@ -71,7 +84,8 @@ export default class Label extends Printable { file = await resp.arrayBuffer() } - this.fonts.push({name, data: file}) + const fontBuffer = Buffer.from(file) + this.fonts[name] = {name, data: file, font: fontkit.create(fontBuffer)} } /** @@ -129,7 +143,7 @@ export default class Label extends Printable { gapOffset: number = 0, generator: CommandGenerator) { const commands = [ - ...(this.fonts.map((font) => generator.upload(font.name, font.data) )), + ...(Object.values(this.fonts).map((font) => generator.upload(font.name+".TTF", font.data) )), generator.setUp(this.width, this.height, gap, gapOffset, direction, mirror, this.unitSystem), (await this.commandForLanguage(language, this.printConfig)), ] diff --git a/src/labels/Printable.ts b/src/labels/Printable.ts index 5185137..3a0f39c 100644 --- a/src/labels/Printable.ts +++ b/src/labels/Printable.ts @@ -3,7 +3,8 @@ import CommandGenerator from "@/commands/CommandGenerator"; import { Printer } from "@/printers"; export type PrintConfig = { - dpi: number + dpi: number, + textWidth: (text: string, font: string, fontSize: number) => number } /** diff --git a/src/labels/fields/Text.ts b/src/labels/fields/Text.ts index 1b924df..b0df90a 100644 --- a/src/labels/fields/Text.ts +++ b/src/labels/fields/Text.ts @@ -2,8 +2,22 @@ import { Command, PrinterLanguage } from "@/commands" import LabelField from "./LabelField" import { PrintConfig } from "../Printable" import { dotToPoint } from "@/helpers/UnitUtils" +import CommandGenerator from "@/commands/CommandGenerator" +import { isWhitespace } from "@/helpers/StringUtils" +import { NodeType, parse, HTMLElement, Node } from "node-html-parser" export type TextFieldType = "singleline"|"multiline" +type Context = { + language: PrinterLanguage, + generator: CommandGenerator, + config?: PrintConfig +} + +type TextDecoration = "underline"|"strike" + +const BOLD_TAG = "b" +const UNDERLINE_TAG = "u" +const STRIKE_TAG = "s" /** * Presents a piece of text on the label @@ -25,6 +39,9 @@ export default class Text extends LabelField { private fontSize: number = 10 private font: string = "default" private type: TextFieldType = "singleline" + private context: Context|undefined = undefined + private readonly lineSpacing = 1 + /** * Width of the text. * If set, the text will be clipped to this size @@ -66,7 +83,7 @@ export default class Text extends LabelField { } /** - * Set a font to use. + * Set a font to use as a base. If no formatting is set on the text with a html tag, this will be used * Note: The font name either has to be a built in font on your printer or a font * that is registered on the label using 'registerFont'. * @@ -78,8 +95,239 @@ export default class Text extends LabelField { this.fontSize = size } - commandForLanguage(language: PrinterLanguage, config?: PrintConfig): Promise { - const fontSize = dotToPoint(this.fontSize, config?.dpi ?? 203) - return this.commandGeneratorFor(language).text(this.content, this.x, this.y, this.font, fontSize) + async commandForLanguage(language: PrinterLanguage, config?: PrintConfig): Promise { + this.context = { + config, + language, + generator: this.commandGeneratorFor(language) + } + + + let command: Command + if(this.formatted) { + command = this.generateFormattedText() + } else { + command = this.generatePlainText() + } + this.context = undefined + + return command + } + + private generateFormattedText(): Command { + if(!this.context) throw "context-not-set" + + // Starter values + const rootNode = parse(this.content) + const { command } = this.generateFormattedRecursive(this.x, this.y, rootNode, this.font, this.fontSize, []) + return command + } + + /** + * Iterats the nodes in a html text and generates text commands for it + */ + private generateFormattedRecursive(initialX: number, initialY: number, rootNode: Node, font: string, fontSize: number, features: TextDecoration[]): {x: number, y: number, command: Command} { + if(rootNode.nodeType == NodeType.TEXT_NODE) { + const result = this.generatePlainTextCore(rootNode.innerText, initialX, initialY, font, fontSize, features) + return result + } else { + const elementNode = rootNode as HTMLElement + const tag = elementNode.rawTagName + + let commands: Command[] = [] + let currentX = initialX + let currentY = initialY + + const baseFont = tag == BOLD_TAG ? this.getBoldFont(font) : font + const baseFontSize = fontSize + let baseFeatures = [...features] + + if(tag == UNDERLINE_TAG) { + baseFeatures.push("underline") + } else if(tag == STRIKE_TAG) { + baseFeatures.push("strike") + } + + elementNode.childNodes.forEach(node => { + const {x,y,command} = this.generateFormattedRecursive(currentX, currentY, node, baseFont, baseFontSize, baseFeatures, ) + currentX = x + currentY = y + commands.push(command) + }) + + return { + x: currentX, + y: currentY, + command: this.context!.generator.commandGroup(commands) + } + } + } + + /** + * Generate commands for plain text + * @param config + * @returns + */ + private generatePlainText(): Command { + const {command} = this.generatePlainTextCore(this.content, this.x, this.y, this.font, this.fontSize) + return command + } + + /** + * Generate commands for plain text + * @param config + * @returns + */ + private generatePlainTextCore(content: string, initialX: number, initialY: number, font: string, fontSize: number, features: TextDecoration[] = []): {x: number, y: number, command: Command} { + if(!this.context) throw "context-not-set" + + const textWidhtFunction = (this.context.config?.textWidth ?? this.defaultTextWidth) + const fullWidth = textWidhtFunction(content, font, fontSize) + + if(this.width) { + const initialPadding = initialX - this.x + // Because we may start from further in the row, the first rows width may be smaller + let rowWidth = this.width - initialPadding + + // We may not start from the begining of the textbox so we have to offset + // by our current position + if(fullWidth <= rowWidth) { + return { + x: initialX + fullWidth, + y: initialY, + command: this.textCommand(content, initialX, initialY, font, fontSize) + } + } else { + const commands: Command[] = [] + + let x = initialX + let y = initialY + let remainingContent = content + let remainingWidth = fullWidth + let currentHeight = 0 + + let finalX = x + let finalY = y + + do { + // This will be the last row of the text. + if(remainingWidth < rowWidth) { + finalX = this.x + remainingWidth + finalY = y + commands.push(this.textCommand(remainingContent, x, y, font, fontSize)) + remainingContent = "" + } else { + // On how many rows this text would fit + let rows = remainingWidth / rowWidth + // From the second row, all rows are full width + rowWidth = this.width + // Which caracter is the last if dividing into the right number of rows + let rowEndIndex = Math.floor(remainingContent.length / rows) + let originalRowEndIndex = rowEndIndex + + // This means we have to fit a relatively short text into + // a lot of rows which can only happen if the row width is very small + // in this case, we have to go to a new line + if(rowEndIndex == 0) { + x = this.x + y += fontSize + this.lineSpacing + continue + } + + // Scenario 1: Current index is in a middle of row + // I am iron m@n + // End this row with the last words last character + + // Scneraio 2: Current index is space: + // I am iron@man + // No action, but to simplify code, we threat as scenario 1 + + // Scenario 3: Current index is right before a sapce + // I am iro@ man + // Start next row from the first latter + + // Find the end of the last word + while( + ! ( + !isWhitespace(remainingContent.charAt(rowEndIndex)) && + ( + rowEndIndex == remainingContent.length - 1 || + isWhitespace(remainingContent.charAt(rowEndIndex + 1)) + ) + ) && rowEndIndex > 0 + ) { rowEndIndex -- } + + let nextRowStartIndex = rowEndIndex + 1 + // We didn't find a space, we split the text wherever we land + if(rowEndIndex == 0) { + rowEndIndex = originalRowEndIndex + nextRowStartIndex = originalRowEndIndex + 1 + } else { + while( + isWhitespace(remainingContent.charAt(nextRowStartIndex)) && + nextRowStartIndex < remainingContent.length + ) { nextRowStartIndex ++ } + } + + const thisRow = remainingContent.substring(0, rowEndIndex + 1) + commands.push(this.textCommand(thisRow, x, y, font, fontSize)) + + // Make sure to move the cursor back to the left side of the text box + // as we may have started further into the row + x = this.x + y += fontSize + this.lineSpacing + currentHeight = y - this.y + remainingContent = remainingContent.substring(nextRowStartIndex) + remainingWidth = textWidhtFunction(remainingContent, font, fontSize) + } + } while( + // We don't have a height constraint or we are still within bounds + // and there is still content + // and we are supporting multiline + (this.height == undefined || (currentHeight + fontSize) <= this.height) && + (remainingContent != "") && + this.type == "multiline" + ) + + return { + x: finalX, + y: finalY, + command: this.context!.generator.commandGroup(commands) + } + } + } else { + // + return { + x: initialX + fullWidth, + y: initialY, + command: this.textCommand(content, initialX, initialY, font, fontSize) + } + } + } + + private textCommand(text: string, x: number, y: number, font: string, fontSize: number) { + const finalFontSize = dotToPoint(fontSize, this.context!.config?.dpi ?? 203) + const finalFont = this.getFontName(font) + return this.context!.generator.text(text, x, y, finalFont, finalFontSize) + } + + /** + * This function is used to calculate the font size if no + * print config is provided. This will asume that the font has square characters + */ + private defaultTextWidth(text: string, _f: string, fontSize: number) { + return text.length * fontSize + } + + private getBoldFont(font: string) { + if(font.includes('-bold')) { + return font + } else { + return `${font}-bold` + } + } + + private getFontName(font: string) { + return `${font}.TTF` } } \ No newline at end of file diff --git a/src/labels/types.ts b/src/labels/types.ts index 91dda00..848d318 100644 --- a/src/labels/types.ts +++ b/src/labels/types.ts @@ -1,6 +1,7 @@ +import { Font as FKFont } from "fontkit" + export type Font = { name: string, data: ArrayBufferLike - // width: number, - // height: number + font: FKFont } \ No newline at end of file