{"_id":"@12-apps/thermal-printing","_rev":"7-12078909276e78c4a2e4c85b5f2211ab","name":"@12-apps/thermal-printing","dist-tags":{"latest":"1.3.3"},"versions":{"1.0.0":{"name":"@12-apps/thermal-printing","version":"1.0.0","license":"MIT","_id":"@12-apps/thermal-printing@1.0.0","maintainers":[{"name":"tigredonorte","email":"tigredonorte3@gmail.com"}],"homepage":"https://github.com/12-apps/shared-packages#readme","bugs":{"url":"https://github.com/12-apps/shared-packages/issues"},"dist":{"shasum":"5400a8846c06844a05cd5e71e792154783548a41","tarball":"https://registry.npmjs.org/@12-apps/thermal-printing/-/thermal-printing-1.0.0.tgz","fileCount":8,"integrity":"sha512-SXYl3WGkOClD1aYE60z045s+FQg3LUA5cJNSIc2nFfi3tT0cDLt3rxDxGSCZzt6reurwZLoo6W6d0+XA80w3xQ==","signatures":[{"sig":"MEQCICtuS3sqeWYB59ND4eNsHGGhZH93/bu6A/xOA7M4lOe8AiB6wMn4J52X0PtP66tfTDnNDJttruggMDwGi+xXynnCyA==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":37436},"type":"module","engines":{"node":">=22.0.0"},"exports":{".":"./src/index.ts","./net":"./src/net/index.ts","./html":"./src/html/index.ts","./escpos":"./src/escpos/index.ts","./routing":"./src/routing/index.ts","./package.json":"./package.json"},"gitHead":"725d0baee1ac51487fb347be79442c231cc1d858","scripts":{"lint":"eslint src --max-warnings 0","test":"node ../../scripts/vitest-with-teardown.mjs run","clean":"rm -rf node_modules coverage","typecheck":"tsc --noEmit","test:watch":"vitest watch","check-types":"tsc --noEmit"},"_npmUser":{"name":"tigredonorte","email":"tigredonorte3@gmail.com"},"repository":{"url":"git+https://github.com/12-apps/shared-packages.git","type":"git","directory":"packages/thermal-printing"},"_npmVersion":"12.0.2","description":"Receipt printing for the two ways a thermal printer is attached, behind ONE fixed-width document. Isomorphic line model and wrapping (./), the ESC/POS encoder with its CP850 code page (./escpos), the same lines as a print-ready HTML document for a printer","directories":{},"sideEffects":false,"_nodeVersion":"24.19.0","publishConfig":{"access":"public","registry":"https://registry.npmjs.org"},"_hasShrinkwrap":false,"devDependencies":{"eslint":"^9.39.1","vitest":"^3.2.4","typescript":"^5.9.2","@types/node":"^22.10.2","@12-apps/eslint-config":"^1.22.0","@12-apps/typescript-config":"^1.21.0","eslint-plugin-test-flakiness":"^1.4.0"},"_npmOperationalInternal":{"tmp":"tmp/thermal-printing_1.0.0_1787843737114_0.0905920911670488","host":"s3://npm-registry-packages-npm-production"}},"1.1.0":{"name":"@12-apps/thermal-printing","version":"1.1.0","license":"MIT","_id":"@12-apps/thermal-printing@1.1.0","maintainers":[{"name":"tigredonorte","email":"tigredonorte3@gmail.com"}],"homepage":"https://github.com/12-apps/shared-packages#readme","bugs":{"url":"https://github.com/12-apps/shared-packages/issues"},"dist":{"shasum":"ba905662a39b1ee8c6f23d33ea90e4ff0475522d","tarball":"https://registry.npmjs.org/@12-apps/thermal-printing/-/thermal-printing-1.1.0.tgz","fileCount":12,"integrity":"sha512-yCgztTFdrTiUTdHJ5ilZkpl6skcikDXei/rXxP0ouWpUmDkn7i6g+9+QLkFpxPystehAREo1KxmcUSI7RUWaGA==","signatures":[{"sig":"MEQCIFOOLYSfhQx7hHoCYzLqY6gAvGnyrWs4cPywdwKfnftqAiB55s8j3kSImi9eIOyWPv8/DUFrsD6elFBWW4BZPPc0NQ==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"sig":"MEQCICJVuvsGmxgLpMuTqXbDcjsoedbdTZP5ljJeByjLpHFWAiADDp60pXKtrI1A6g5Z45C7fhv3+tPGNJ7g2fnp1fJBcg==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@12-apps%2fthermal-printing@1.1.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":74541},"type":"module","engines":{"node":">=22.0.0"},"exports":{".":"./src/index.ts","./net":"./src/net/index.ts","./html":"./src/html/index.ts","./escpos":"./src/escpos/index.ts","./routing":"./src/routing/index.ts","./discovery":"./src/discovery/index.ts","./package.json":"./package.json","./discovery/cli":"./src/discovery/cli.ts","./discovery/bridge":"./src/discovery/bridge.ts"},"gitHead":"6d7119381320da5b08b79364bbc24aa7c00bd86b","scripts":{"lint":"eslint src --max-warnings 0","test":"node ../../scripts/vitest-with-teardown.mjs run","clean":"rm -rf node_modules coverage","typecheck":"tsc --noEmit","test:watch":"vitest watch","check-types":"tsc --noEmit"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:1a696aa8-9ecd-4ed0-9608-e8dd60aa27cb"}},"repository":{"url":"git+https://github.com/12-apps/shared-packages.git","type":"git","directory":"packages/thermal-printing"},"_npmVersion":"12.0.2","description":"Receipt printing for the two ways a thermal printer is attached, behind ONE fixed-width document. Isomorphic line model and wrapping (./), the ESC/POS encoder with its CP850 code page (./escpos), the same lines as a print-ready HTML document for a printer","directories":{},"sideEffects":false,"_nodeVersion":"24.20.0","publishConfig":{"access":"public","registry":"https://registry.npmjs.org"},"_hasShrinkwrap":false,"devDependencies":{"eslint":"^9.39.1","vitest":"^3.2.4","typescript":"^5.9.2","@types/node":"^22.10.2","@12-apps/eslint-config":"^1.22.0","@12-apps/typescript-config":"^1.21.0","eslint-plugin-test-flakiness":"^1.4.0"},"_npmOperationalInternal":{"tmp":"tmp/thermal-printing_1.1.0_1789745966598_0.6445109566760823","host":"s3://npm-registry-packages-npm-production"}},"1.2.0":{"name":"@12-apps/thermal-printing","version":"1.2.0","license":"MIT","_id":"@12-apps/thermal-printing@1.2.0","maintainers":[{"name":"tigredonorte","email":"tigredonorte3@gmail.com"}],"homepage":"https://github.com/12-apps/shared-packages#readme","bugs":{"url":"https://github.com/12-apps/shared-packages/issues"},"dist":{"shasum":"c34c10890dcbc925404041a573ebb228888903a3","tarball":"https://registry.npmjs.org/@12-apps/thermal-printing/-/thermal-printing-1.2.0.tgz","fileCount":13,"integrity":"sha512-1pWs/OtIgIxkwa+T7A2cdhgm8Talcljx9/mvuO7lPpuKzOdec8i/i3ulwCYU8+hz7cEbWE4HA3XAuDfMqzPExQ==","signatures":[{"sig":"MEQCICSo80uBOE6J5ihuEMvSWzSSGS1Xnv9lQn986TbC/fJ/AiAFn7V/FgyKe+J9jkbKcmUXvMK+bSgLXz4uWZD0NvHmQA==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"sig":"MEQCIEGAGN0MJUBRRPh2tgGh6Cy/SEVStqyNVzrTytfezzSiAiBO5TFU0me3qOdau3BD5hxZqYTNhtaizSXxIjldXN07QA==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@12-apps%2fthermal-printing@1.2.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":79460},"type":"module","engines":{"node":">=22.0.0"},"exports":{".":"./src/index.ts","./net":"./src/net/index.ts","./html":"./src/html/index.ts","./escpos":"./src/escpos/index.ts","./routing":"./src/routing/index.ts","./electron":"./src/electron/index.ts","./discovery":"./src/discovery/index.ts","./package.json":"./package.json","./discovery/cli":"./src/discovery/cli.ts","./discovery/bridge":"./src/discovery/bridge.ts"},"gitHead":"2be3672fe68f0c69a5135b11b6b943da08705519","scripts":{"lint":"eslint src --max-warnings 0","test":"node ../../scripts/vitest-with-teardown.mjs run","clean":"rm -rf node_modules coverage","typecheck":"tsc --noEmit","test:watch":"vitest watch","check-types":"tsc --noEmit"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:1a696aa8-9ecd-4ed0-9608-e8dd60aa27cb"}},"repository":{"url":"git+https://github.com/12-apps/shared-packages.git","type":"git","directory":"packages/thermal-printing"},"_npmVersion":"12.1.0","description":"Receipt printing for the two ways a thermal printer is attached, behind ONE fixed-width document. Isomorphic line model and wrapping (./), the ESC/POS encoder with its CP850 code page (./escpos), the same lines as a print-ready HTML document for a printer","directories":{},"sideEffects":false,"_nodeVersion":"24.21.0","publishConfig":{"access":"public","registry":"https://registry.npmjs.org"},"_hasShrinkwrap":false,"devDependencies":{"eslint":"^9.39.1","vitest":"^3.2.4","electron":"^33.2.0","typescript":"^5.9.2","@types/node":"^22.10.2","@12-apps/eslint-config":"^1.22.0","@12-apps/typescript-config":"^1.21.0","eslint-plugin-test-flakiness":"^1.4.0"},"peerDependencies":{"electron":">=30.0.0"},"peerDependenciesMeta":{"electron":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/thermal-printing_1.2.0_1790382235822_0.19521707953259027","host":"s3://npm-registry-packages-npm-production"}},"1.3.0":{"name":"@12-apps/thermal-printing","version":"1.3.0","license":"MIT","_id":"@12-apps/thermal-printing@1.3.0","maintainers":[{"name":"tigredonorte","email":"tigredonorte3@gmail.com"}],"homepage":"https://github.com/12-apps/shared-packages#readme","bugs":{"url":"https://github.com/12-apps/shared-packages/issues"},"dist":{"shasum":"397072c40914829d2db530d169519694d007c080","tarball":"https://registry.npmjs.org/@12-apps/thermal-printing/-/thermal-printing-1.3.0.tgz","fileCount":26,"integrity":"sha512-LiiL7/MM44P3GkzJIG77A5CPo8/U9X8+sWfPVBkLfCOpoc+wD6PmCKUudchSwHmNJF+mZkfdZC3pzKNtUHWiXQ==","signatures":[{"sig":"MEUCIQDisJwPKBXDIS0TnIAigTZe8XaU8PNgOw+7tIZLNLVyNAIgegX3GKleZ15fyWvu0J+EY5EjdMPtS9kNhtmtZAvBQGw=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"sig":"MEQCIC9R52JmAiTPe5Uo8xgFE/maRpBzWAZKdt/bo0JqFhR4AiAmddRL1KGtkLVmI8dtOpX/GqQdMINspeplQJFqbeGKiQ==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@12-apps%2fthermal-printing@1.3.0","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":168475},"type":"module","engines":{"node":">=22.0.0"},"exports":{".":"./src/index.ts","./net":"./src/net/index.ts","./html":"./src/html/index.ts","./escpos":"./src/escpos/index.ts","./raster":"./src/raster/index.ts","./routing":"./src/routing/index.ts","./electron":"./src/electron/index.ts","./discovery":"./src/discovery/index.ts","./package.json":"./package.json","./discovery/cli":"./src/discovery/cli.ts","./discovery/bridge":"./src/discovery/bridge.ts"},"gitHead":"edeb54a4118d81fd05403e6d0b7d864bcc40de68","scripts":{"lint":"eslint src --max-warnings 0","test":"node ../../scripts/vitest-with-teardown.mjs run","clean":"rm -rf node_modules coverage","typecheck":"tsc --noEmit","test:watch":"vitest watch","check-types":"tsc --noEmit"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:1a696aa8-9ecd-4ed0-9608-e8dd60aa27cb"}},"repository":{"url":"git+https://github.com/12-apps/shared-packages.git","type":"git","directory":"packages/thermal-printing"},"_npmVersion":"12.1.0","description":"Receipt printing for the two ways a thermal printer is attached, behind ONE fixed-width document. Isomorphic line model and wrapping (./), four type sizes, inverted bands, boxes, label/amount rows and raster images, the ESC/POS encoder with its CP850 code","directories":{},"sideEffects":false,"_nodeVersion":"24.21.0","publishConfig":{"access":"public","registry":"https://registry.npmjs.org"},"_hasShrinkwrap":false,"devDependencies":{"eslint":"^9.39.1","vitest":"^3.2.4","electron":"^33.2.0","typescript":"^5.9.2","@types/node":"^22.10.2","@12-apps/eslint-config":"^1.22.0","@12-apps/typescript-config":"^1.21.0","eslint-plugin-test-flakiness":"^1.4.0"},"peerDependencies":{"electron":">=30.0.0"},"peerDependenciesMeta":{"electron":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/thermal-printing_1.3.0_1790477409736_0.986767973322658","host":"s3://npm-registry-packages-npm-production"}},"1.3.1":{"name":"@12-apps/thermal-printing","version":"1.3.1","license":"MIT","_id":"@12-apps/thermal-printing@1.3.1","maintainers":[{"name":"tigredonorte","email":"tigredonorte3@gmail.com"}],"homepage":"https://github.com/12-apps/shared-packages#readme","bugs":{"url":"https://github.com/12-apps/shared-packages/issues"},"dist":{"shasum":"f49e1f2ec3a6a090c425b887bb38dac730a1a631","tarball":"https://registry.npmjs.org/@12-apps/thermal-printing/-/thermal-printing-1.3.1.tgz","fileCount":26,"integrity":"sha512-Utsj0zDhZz4qvkkjos71jxZOXHrQs42GEg60d720d8Lk9y/6rbW+EsZ9KXK7OinJgfNAzBu7Ipv0zCFlMpMWVg==","signatures":[{"sig":"MEUCIDCIfbp+/wzgNPF8AVSEIqdRSDvHt3H+Md8+GpNNpbXmAiEAocjhl8U7EpYYsOkjuRuPsqp2V8uxMDN1sRXnNqc2d1E=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"sig":"MEYCIQDVYNiMh9NpmEEXLTfwfjqpxOnADOT6PzKTVrWSV3UVBwIhAJtuJmlaew9zOnx9XqVixtc4og7vRIQ0pxDOneJcNgFW","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@12-apps%2fthermal-printing@1.3.1","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":171476},"type":"module","engines":{"node":">=22.0.0"},"exports":{".":"./src/index.ts","./net":"./src/net/index.ts","./html":"./src/html/index.ts","./escpos":"./src/escpos/index.ts","./raster":"./src/raster/index.ts","./routing":"./src/routing/index.ts","./electron":"./src/electron/index.ts","./discovery":"./src/discovery/index.ts","./package.json":"./package.json","./discovery/cli":"./src/discovery/cli.ts","./discovery/bridge":"./src/discovery/bridge.ts"},"gitHead":"a94e45521061ea7b44c3567aa12dc965353eb042","scripts":{"lint":"eslint src --max-warnings 0","test":"node ../../scripts/vitest-with-teardown.mjs run","clean":"rm -rf node_modules coverage","typecheck":"tsc --noEmit","test:watch":"vitest watch","check-types":"tsc --noEmit"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:1a696aa8-9ecd-4ed0-9608-e8dd60aa27cb"}},"repository":{"url":"git+https://github.com/12-apps/shared-packages.git","type":"git","directory":"packages/thermal-printing"},"_npmVersion":"12.1.0","description":"Receipt printing for the two ways a thermal printer is attached, behind ONE fixed-width document. Isomorphic line model and wrapping (./), four type sizes, inverted bands, boxes, label/amount rows and raster images, the ESC/POS encoder with its CP850 code","directories":{},"sideEffects":false,"_nodeVersion":"24.21.0","publishConfig":{"access":"public","registry":"https://registry.npmjs.org"},"_hasShrinkwrap":false,"devDependencies":{"eslint":"^9.39.1","vitest":"^3.2.4","electron":"^33.2.0","typescript":"^5.9.2","@types/node":"^22.10.2","@12-apps/eslint-config":"^1.22.0","@12-apps/typescript-config":"^1.21.0","eslint-plugin-test-flakiness":"^1.4.0"},"peerDependencies":{"electron":">=30.0.0"},"peerDependenciesMeta":{"electron":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/thermal-printing_1.3.1_1790489885918_0.6041671863980143","host":"s3://npm-registry-packages-npm-production"}},"1.3.2":{"name":"@12-apps/thermal-printing","version":"1.3.2","license":"MIT","_id":"@12-apps/thermal-printing@1.3.2","maintainers":[{"name":"tigredonorte","email":"tigredonorte3@gmail.com"}],"homepage":"https://github.com/12-apps/shared-packages#readme","bugs":{"url":"https://github.com/12-apps/shared-packages/issues"},"dist":{"shasum":"0c2914a1eb9d46eab8f9118f72a45c49f2e771b3","tarball":"https://registry.npmjs.org/@12-apps/thermal-printing/-/thermal-printing-1.3.2.tgz","fileCount":26,"integrity":"sha512-7jzBgbNGFccKNJgOrQqlfJ93ZkOfQuk3sBEQERNPhVgtGb4g/Hl/UAWYxdSGwrQz3zXgfCxR0TwWJaFDxdiw8g==","signatures":[{"sig":"MEUCIQCyXj1q410W/wuzAP/ZEuaaV3X5sA3HAVXOuAnlp60QiAIgeP/JBCOexaST4EQBw7Jc5eUq2R3qCU3StDFkAhiAx7g=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"sig":"MEQCIExc4Np2TtLdHtc1fHpfeBALzZ2GL7spHsyl1Kq4oKIiAiBpxyP5Mgx7Kv33G2ziLukx4hkDHzqkJVioBlp4fvuXFw==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@12-apps%2fthermal-printing@1.3.2","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":174198},"type":"module","engines":{"node":">=22.0.0"},"exports":{".":"./src/index.ts","./net":"./src/net/index.ts","./html":"./src/html/index.ts","./escpos":"./src/escpos/index.ts","./raster":"./src/raster/index.ts","./routing":"./src/routing/index.ts","./electron":"./src/electron/index.ts","./discovery":"./src/discovery/index.ts","./package.json":"./package.json","./discovery/cli":"./src/discovery/cli.ts","./discovery/bridge":"./src/discovery/bridge.ts"},"gitHead":"2f5e197b821267308ce1eefe46a989fad8008cbc","scripts":{"lint":"eslint src --max-warnings 0","test":"node ../../scripts/vitest-with-teardown.mjs run","clean":"rm -rf node_modules coverage","typecheck":"tsc --noEmit","test:watch":"vitest watch","check-types":"tsc --noEmit"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:1a696aa8-9ecd-4ed0-9608-e8dd60aa27cb"}},"repository":{"url":"git+https://github.com/12-apps/shared-packages.git","type":"git","directory":"packages/thermal-printing"},"_npmVersion":"12.1.0","description":"Receipt printing for the two ways a thermal printer is attached, behind ONE fixed-width document. Isomorphic line model and wrapping (./), four type sizes, inverted bands, boxes, label/amount rows and raster images, the ESC/POS encoder with its CP850 code","directories":{},"sideEffects":false,"_nodeVersion":"24.21.0","publishConfig":{"access":"public","registry":"https://registry.npmjs.org"},"_hasShrinkwrap":false,"devDependencies":{"eslint":"^9.39.1","vitest":"^3.2.4","electron":"^33.2.0","typescript":"^5.9.2","@types/node":"^22.10.2","@12-apps/eslint-config":"^1.22.0","@12-apps/typescript-config":"^1.21.0","eslint-plugin-test-flakiness":"^1.4.0"},"peerDependencies":{"electron":">=30.0.0"},"peerDependenciesMeta":{"electron":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/thermal-printing_1.3.2_1790618455296_0.05267523801179186","host":"s3://npm-registry-packages-npm-production"}},"1.3.3":{"_id":"@12-apps/thermal-printing@1.3.3","bugs":{"url":"https://github.com/12-apps/shared-packages/issues"},"dist":{"shasum":"4f2df694e1d5cef9794c050ce1937e3c64a5d6ad","tarball":"https://registry.npmjs.org/@12-apps/thermal-printing/-/thermal-printing-1.3.3.tgz","fileCount":28,"integrity":"sha512-KAHXy9L6SO81X7ekiC31fWigIrqFjEuePqI96XQ+4VItsd/v8xSFVWPLFVcccslBeaTxIBnZr0OPF6Avc3cnTg==","signatures":[{"sig":"MEYCIQCt/JvSxtCLeaeyCxBBhrW1326Bi6aRkJPa+S+LRNrE+AIhAPkmh42MV/D2JsDlGOngdYHggAuKfAoRLjmASpynXMo6","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQCjMwcBxHlFpObp1TXrK+mKOo1A2pJisHQbN1bJOO1TSgIgB71039rLREQKFWBqJXl/kidtpjMVn86a6Yn/LNu6xUc="}],"attestations":{"url":"https://registry.npmjs.org/-/npm/v1/attestations/@12-apps%2fthermal-printing@1.3.3","provenance":{"predicateType":"https://slsa.dev/provenance/v1"}},"unpackedSize":180144},"name":"@12-apps/thermal-printing","type":"module","engines":{"node":">=22.0.0"},"exports":{".":"./src/index.ts","./net":"./src/net/index.ts","./html":"./src/html/index.ts","./escpos":"./src/escpos/index.ts","./raster":"./src/raster/index.ts","./routing":"./src/routing/index.ts","./electron":"./src/electron/index.ts","./discovery":"./src/discovery/index.ts","./package.json":"./package.json","./discovery/cli":"./src/discovery/cli.ts","./discovery/bridge":"./src/discovery/bridge.ts"},"gitHead":"13bd2a1d8b782d2eec5d9540a9737456ee500cc5","license":"MIT","scripts":{"lint":"eslint src --max-warnings 0","test":"node ../../scripts/vitest-with-teardown.mjs run","clean":"rm -rf node_modules coverage","typecheck":"tsc --noEmit","test:watch":"vitest watch","check-types":"tsc --noEmit"},"version":"1.3.3","_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"1a696aa8-9ecd-4ed0-9608-e8dd60aa27cb"}},"homepage":"https://github.com/12-apps/shared-packages#readme","repository":{"url":"git+https://github.com/12-apps/shared-packages.git","type":"git","directory":"packages/thermal-printing"},"_npmVersion":"12.1.0","description":"Receipt printing for the two ways a thermal printer is attached, behind ONE fixed-width document. Isomorphic line model and wrapping (./), four type sizes, inverted bands, boxes, label/amount rows and raster images, the ESC/POS encoder with its CP850 code","directories":{},"maintainers":[{"name":"tigredonorte","email":"tigredonorte3@gmail.com"}],"sideEffects":false,"_nodeVersion":"24.21.0","publishConfig":{"access":"public","registry":"https://registry.npmjs.org"},"_hasShrinkwrap":false,"devDependencies":{"eslint":"^9.39.1","vitest":"^3.2.4","electron":"^33.2.0","typescript":"^5.9.2","@types/node":"^22.10.2","@12-apps/eslint-config":"^1.22.0","@12-apps/typescript-config":"^1.21.0","eslint-plugin-test-flakiness":"^1.4.0"},"peerDependencies":{"electron":">=30.0.0"},"peerDependenciesMeta":{"electron":{"optional":true}},"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/thermal-printing_1.3.3_1790633103791_0.3441720885184181"}}},"time":{"created":"2026-08-27T15:15:36.906Z","modified":"2026-09-28T22:05:04.436Z","1.0.0":"2026-08-27T15:15:37.257Z","1.1.0":"2026-09-18T15:39:26.696Z","1.2.0":"2026-09-26T00:23:55.919Z","1.3.0":"2026-09-27T02:50:09.835Z","1.3.1":"2026-09-27T06:18:06.046Z","1.3.2":"2026-09-28T18:00:55.398Z","1.3.3":"2026-09-28T22:05:03.892Z"},"bugs":{"url":"https://github.com/12-apps/shared-packages/issues"},"license":"MIT","homepage":"https://github.com/12-apps/shared-packages#readme","repository":{"url":"git+https://github.com/12-apps/shared-packages.git","type":"git","directory":"packages/thermal-printing"},"description":"Receipt printing for the two ways a thermal printer is attached, behind ONE fixed-width document. Isomorphic line model and wrapping (./), four type sizes, inverted bands, boxes, label/amount rows and raster images, the ESC/POS encoder with its CP850 code","maintainers":[{"name":"tigredonorte","email":"tigredonorte3@gmail.com"}],"readme":"# @12-apps/thermal-printing\n\nReceipt printing for the two ways a thermal printer is attached, behind **one**\nfixed-width document.\n\n```bash\npnpm add @12-apps/thermal-printing\n```\n\n## The problem it solves\n\nA receipt printer reaches a host one of two ways, and the host does not get to\nchoose which one the shop bought:\n\n| | **Network** (Wi-Fi / Ethernet) | **Local** (USB cable) |\n|---|---|---|\n| how it is reached | raw ESC/POS over a TCP socket, `:9100` by convention | the browser on the machine it is plugged into, printing through the OS |\n| who drives it | the server — `./net` | a tab — `./html` |\n| with nothing open | still prints | stops, silently: a USB device has no address |\n| what a success means | the bytes left the process | the document reached the operating system |\n\nThe naive shape is two code paths, and the cost shows up the first time a store\nswaps one kind of printer for the other: the ticket changes, because a second\npath is a second layout. So the split here is **one document, two encoders**.\n\n```\n       host builds            this package encodes\n   ┌──────────────────┐      ┌─────────────────────┐\n   │  TicketLine[]    │─────▶│ ./escpos → bytes    │──▶ socket (./net)\n   │  (line, field,   │      │ ./html   → document │──▶ a tab prints it\n   │   rule, wrap)    │      └─────────────────────┘\n   └──────────────────┘\n```\n\nThe layout is authoritative for both. `./html` renders the same lines in a\nmonospace column of exactly the same width rather than laying out a `<table>` —\nthat is the divergence the package exists to refuse.\n\n## What it does not do\n\n**It carries no copy.** Not one sentence, in any language. A failed send returns\na `reason` code and the `host:port` it was aimed at; the host turns that into\nwords, in the language of whoever is reading the screen. Two hosts can say \"the\nprinter at 10.0.0.9 did not answer\" and \"the kitchen printer is offline\" from\nthe same event.\n\n**It does not compose your ticket.** What a ticket says, in what order, is the\nhost's product decision — and it is where the host's own vocabulary, currency\nand time zone live. This package gives you the arithmetic underneath it: column\ncounts, wrapping with a hanging indent, dividers, `Label: value` fields that\ndisappear when there is no value.\n\n**It owns no models and no routes.** There is no Prisma partial and nothing to\nmount.\n\n## Surfaces\n\n| Export | What it is |\n|---|---|\n| `@12-apps/thermal-printing` | The line model — `TicketLine`, `columnsFor`, `wrap`, `line`, `centered`, `rule`, `field`, `PAPER_WIDTHS_MM`, and the [reference-sheet vocabulary](#the-vocabulary-four-sizes-bands-boxes-rows-pictures): `textLines`, `band`, `box`, `row`, `image`, `LINE_SIZES`, `printableDotsFor`, `textWidth`. Pure, isomorphic. |\n| `…/escpos` | `encodeTicket(lines)` → `Uint8Array`. Initialise, CP850, align, emphasise, font and magnification, reverse, line spacing, `GS v 0` raster, feed, partial cut. |\n| `…/html` | `renderTicketHtml(lines, paperWidthMm, lang?)` → a standalone document exactly the roll's printable width (72 mm of 80, 48 mm of 58), pure black on white, with `@page { margin: 0 }`. `rasterToDataUri(raster)` → the 1-bit picture as a `data:` URI, for a preview. |\n| `…/raster` | `rasterizeSvg(svg, { width })` → RGBA at the printer's dot size; `toMonochrome(rgba, options?)` → a printable 1-bit `RasterImage`. Pure, isomorphic, no canvas and no native module. |\n| `…/net` | `sendToNetworkPrinter(host, port, bytes, options?)`. Node-only (`node:net`), never throws, structured failures. |\n| `…/routing` | `printerRoute(printers)` / `printerFor(route, destinationId)` — per-destination with a default, generic over your own printer rows. |\n| `…/discovery` | `scanForPrinters(options?)` / `probePrinter(host, port?)` — find the address nobody wrote down. Node-only. |\n| `…/discovery/bridge` | `startDiscoveryBridge({ allowedOrigins })` — a loopback server an HTTPS page may call to run that scan. Node-only. |\n| `…/electron` | `printHtml({ html, deviceName, paperWidthMm?, session? })` — an offscreen window, a silent print to a named device (or the default), destroyed after. The page is always named and is the roll: `paperWidthMm` wide (omitted: the narrowest roll whose printable width holds the rendered body) and as tall as the ticket, placed at the printable area. Without it Electron asks the driver for A4 (FUT-3009). A refusal rejects with `PrintRefusedError` (`reason: string \\| null`, never a sentence). `electron` is an optional peer. |\n| `…/discovery/cli` | `runFinder({ allowedOrigins })` / `main(argv, env)` — the helper a merchant downloads, as a function to bundle. Node-only. |\n\n`./net` and `./discovery` sit behind their own subpaths so that importing the\nencoders into a browser bundle never drags `node:net` in, and `./electron`\nbehind its own so that nothing else ever imports `electron`.\n\n## The vocabulary: four sizes, bands, boxes, rows, pictures\n\nA receipt is more than one size of text. The builders below cover what a\ndesigned ticket uses, and every one of them produces **plain text plus a flag**,\nso the two encoders print the same characters in the same places: a row is\npadded here, a box is drawn here, a band is centred here.\n\n```ts\nimport { band, box, image, row, rule, textLines, columnsFor } from '@12-apps/thermal-printing';\n\nconst w = printer.paperWidthMm; // 58 or 80\nconst lines = [\n  image(logo),                                                     // a RasterImage, centred\n  ...textLines(copy.headline, w, { size: 'xlarge', bold: true, align: 'center' }),\n  ...band(copy.paidBanner, w),                                     // white on black, edge to edge\n  ...row(item.label, money(item.totalCents), w, { indent: 3 }),    // amount flush right\n  rule(columnsFor(w)),\n  ...row(copy.total, money(totalCents), w, { size: 'large', bold: true }),\n  ...box(copy.bringChange, w),                                     // ┌──┐ │ … │ └──┘\n  ...textLines(copy.footer, w, { size: 'small', align: 'center' }),\n];\n```\n\nThe size-aware builders take the **paper width**, not a column count: with four\nsizes there are four counts per roll, and the builder knows which one its size\nneeds. The legacy `line`, `centered`, `rule` and `field` are unchanged, and a\nticket that uses only them encodes to exactly the bytes it did before.\n\n### The four sizes, and how they map onto ESC/POS\n\nESC/POS has no point sizes — it has two built-in fonts and integer\nmagnification. Each size is one font at one multiplier, all four reachable with\n`ESC !` alone (the part of the command set every printer in this class has):\n\n| size | reference step | font | magnification | cell (dots) | glyph height | columns 58 mm | columns 80 mm |\n|---|---|---|---|---|---|---|---|\n| `small` | P — details | B | 1 × 1 | 9 × 17 | ≈ 6 pt | 42 | 64 |\n| `medium` | M — text | A | 1 × 1 | 12 × 24 | ≈ 8.5 pt | 32 | 48 |\n| `large` | G — highlights (`TOTAL`) | B | 2 × 2 | 18 × 34 | ≈ 12 pt | 21 | 32 |\n| `xlarge` | GG — headline | A | 2 × 2 | 24 × 48 | ≈ 17 pt | 16 | 24 |\n\n`medium` and `large` match 8.5 pt and 12 pt almost exactly at 8 dots/mm. `small`\nis Font B, the only step below Font A. `xlarge` is the biggest step reachable\nwithout `GS !`; the next one (3 × 3) would leave 16 columns on 80 mm and 10 on\n58 mm, which cannot hold a typical headline on the narrow roll.\n\n**Why these sizes use double width when `double` never does.** The legacy\n`double` emphasis stays double *height* only, because a line wrapped at 48\ncolumns and printed twice as wide silently rewraps at 24. That danger is a\ncolumn count disagreeing with a magnification. A sized line carries its size,\n`columnsFor(paperWidthMm, size)` answers for it, and the builders wrap at that\ncount — so width and height are magnified together, and a glyph that is only\ntaller (which reads as condensed) is avoided.\n\nThe HTML document reproduces the column counts rather than the point sizes: the\nbody is the printable width in millimetres (72 mm on 80 mm, 48 mm on 58 mm) with\nits type sized so `columnsFor(paperWidthMm)` characters fill it — one column is\n1.5 mm, the printer's own Font A cell — and a size with N columns is set at\n(body columns / N) em — `small` 0.75em, `large` 1.5em, `xlarge` 2em on 80 mm. A\nline wraps at the same place on screen as on paper, and nothing reaches the\nmargin the head cannot print. Text is pure black, stroked about one dot thicker\nso a regular-weight stem prints solid rather than as dithered grey; a picture is\ndrawn at one image pixel per dot (`width / 8` mm), unsmoothed.\n\n`LINE_SIZE_METRICS` and `FONT_CELL_DOTS` export the table, for a host that wants\nto draw its own preview.\n\n### Bands and boxes\n\n- **`band`** prints reverse (`GS B 1`), padded to the full column count so the\n  black runs from one edge of the roll to the other — a reverse-printed space is\n  a black cell. In HTML it is `background:#000;color:#fff` with\n  `print-color-adjust:exact`, without which a browser drops the background when\n  printing and the band comes out white on white.\n- **`box`** is drawn with the box-drawing characters CP850 carries (`┌─┐│└┘`),\n  since ESC/POS has no rectangle command; it prints on every printer and renders\n  identically in the browser.\n- Both close the line spacing (`ESC 3 n`, n = the cell height) so a multi-line\n  band is one block and the verticals of a box join; `ESC 2` restores it after.\n- Both default to **bold** — they are there to be noticed; pass `bold: false`\n  to opt out.\n\n> **Hardware assumption to verify on paper:** `ESC 3 n` is read as `n` dots,\n> which holds for the 203-dpi class this package targets (Bematech MP4200,\n> Epson TM-T20 and alike, where the line-spacing motion unit is one dot). A\n> printer whose vertical motion unit is different (set by `GS P`, or a\n> 180/300-dpi head) would space framed lines tighter or looser than the cell;\n> check a band and a box on the actual model before relying on them.\n\n### Rows\n\n`row(label, amount, paperWidthMm, { size?, bold?, indent? })` puts the amount\nflush right on the **first** line and wraps the label beside it (under an\noptional hanging indent). An amount so long the label would get under a third\nof the line moves to a line of its own below the label, still flush right.\nThe amount is never split or truncated: one wider than the whole line prints\nwhole on its own line and runs past the edge (the printer wraps it), because a\ncut amount is a wrong amount.\n\n**Widths are code points of NFC text** (`textWidth`), not UTF-16 units, in\nevery builder: an emoji counts one column and a decomposed `é` is composed\nfirst. That is exactly what the ESC/POS encoder writes — one CP850 byte per\ncode point, `?` for an unmappable one — so alignment holds on paper. A browser\nmay draw an emoji wider than one monospace cell in the HTML preview.\n\n### Pictures\n\nA `RasterImage` is `{ width, height, data }` in printer **dots** (8 per mm):\nrows top to bottom, `Math.ceil(width / 8)` bytes each, MSB leftmost, 1 = black —\nexactly `GS v 0`'s format, so the encoder copies it, in strips of at most 255\nrows. Rasterise at `printableDotsFor(paperWidthMm)` for a full-width logo (576\ndots on 80 mm, 384 on 58 mm) or smaller for a mark; neither encoder rescales.\nIn HTML the same bits become a 1-bit BMP `data:` URI (no canvas, no deflate, the\nsame code in Node and in a browser), sized at 12 dots per character so it takes\nthe width on screen it takes on paper, with `image-rendering: pixelated`.\n\n### Getting a picture down to one bit (`./raster`)\n\nA thermal head has one ink. **A 50 % threshold is the wrong default**: brand\ncolours are mostly light — an orange like `#ED7A1F` has a luma of 0.57 — so a\nlogo in one prints only its darkest details, or nothing.\n\n```ts\nimport { rasterizeSvg, toMonochrome } from '@12-apps/thermal-printing/raster';\nimport { image, printableDotsFor } from '@12-apps/thermal-printing';\n\n// A vector mark, drawn at the size it will print (12 mm = 96 dots):\nconst mark = toMonochrome(rasterizeSvg(markSvg, { width: 96 }));\n// A bitmap logo the host decoded to RGBA at the dot width (canvas, sharp, …):\nconst logo = toMonochrome({ width, height, data: rgba });\n\nlines.unshift(image(logo));\n```\n\n**The policy** (`mode: 'auto'`, the default):\n\n- **Flat artwork** — a few solid colours, which is what a logo or a mark is — is\n  **thresholded at luma 0.75**. What an owner sees: an orange, red, blue or\n  green logo prints as a crisp black silhouette with its white details knocked\n  out; a pale tint (light yellow, pastel, light grey — luma 0.75 and up)\n  disappears. Show them the preview (`rasterToDataUri`) before paper does.\n- **A photograph** — many colours — is **dithered** (Atkinson; Floyd–Steinberg\n  on request), so tone becomes dot density. An orange area comes out as a fine\n  pattern of about 40 % black, not solid black.\n- A transparent pixel is paper. The result's `method` says which branch ran.\n\n`mode: 'threshold' | 'dither'`, `threshold` and `kernel` override the choice.\n\n**Why the SVG rasteriser is a small one.** The full-fidelity options are native\nmodules (`resvg-js`, `sharp`: a binary per platform and nothing in a browser) or\na WebAssembly build (megabytes, and an async init) — in a package that has no\ndependencies at all. What a one-colour printer reproduces is flat fills, and a\nmark drawn for one plate is flat fills by construction, so `rasterizeSvg` draws\nexactly that, anti-aliased: `path` (every command), `rect` (rounded), `circle`,\n`ellipse`, `polygon`, `polyline`, `g`, `transform`, `fill`, `fill-rule`,\n`fill-opacity`, `opacity`, `viewBox` and `preserveAspectRatio` (meet or none).\nWhatever it does not draw — strokes, gradients, text, `<image>`, `<use>`,\nclipping, masks, filters, `<style>` — it **names** in `unsupported`, so a host\ncan refuse the file or rasterise it itself and come in at `toMonochrome`.\nDecoding PNG/JPEG is deliberately left to the host, which already has a codec.\n\n**An uploaded SVG is untrusted input**, so every cost it can ask for is capped,\nand a document over any cap is **refused** — a 0 × 0 result with the reason in\n`unsupported` — never thrown on and never allocated:\n\n| limit | value | reason in `unsupported` |\n|---|---|---|\n| source length, `MAX_SVG_LENGTH` | 256 000 characters | `too large` |\n| output, `MAX_RASTER_WIDTH` × `MAX_RASTER_HEIGHT` | 576 × 2048 dots (checked before allocating, including a height derived from the viewBox) | `output too large` |\n| shapes, `MAX_SHAPES` | 4096 | `too many shapes` |\n| flattened vertices, `MAX_PATH_POINTS` | 250 000 | `too many path points` |\n| scan-conversion work, `MAX_FILL_WORK` | 50 000 000 units, **estimated from the edges before any row is filled** (edge sub-rows × log₂ edges, plus 5 per bounding-box pixel — an upper bound on the real work) | `too complex to fill` |\n\nThe document is read by a hand-written linear scanner (no backtracking regular\nexpressions, so no crafted comment, attribute or number can make parsing\npolynomial), and each shape is composited only across the pixels it covers on\neach row. `toMonochrome` checks its input too: a buffer that is not exactly\n`width × height × 4` bytes, or dimensions past 576 × 2048, throw a `RangeError`\nbefore anything is allocated — those dimensions are the host's own choice after\ndecoding, so a mismatch is a caller bug rather than something an upload reaches.\n\n## Finding a printer nobody wrote the address of down\n\nA settings screen asking a shop owner for an IP address is asking a fair\nquestion of a network engineer and an unfair one of somebody selling lunch: the\naddress was handed out by a router they have never logged into, to a device with\nno screen. `./discovery` sweeps the local network for devices listening on the\nprinting port and grades what it finds.\n\n**An open port proves nothing about the device.** A print spooler, a second\nserver or an unrelated appliance may all answer on 9100. So the probe asks a\nreal ESC/POS question once connected — `DLE EOT 1`, real-time status, which\nprints nothing and is safe to send to a non-printer — and grades a device that\nanswers `confirmed` against one that merely opened the port, `candidate`.\nSilence is never taken as evidence AGAINST a printer, because a good deal of\nthis hardware does not implement status at all. Nothing picks for the operator:\nevery candidate is returned, and the test page is still the only proof.\n\n**A browser cannot do any of this**, which is what `./discovery/bridge` is for.\nAn HTTPS page may not fetch `http://192.168.0.50` — mixed content — and even\nover plaintext Chrome's Private Network Access rules demand a preflight that a\nprinter on :9100 could never answer. Loopback is the one exemption: `127.0.0.1`\nis *potentially trustworthy*, so a helper running on the merchant's own machine\ncan hold the scan and hand the answer to the tab.\n\nThat server is reachable by every page the merchant has open, so it binds\nloopback only, answers **only the origins it was started with**, and exits on a\nTTL. The origin allowlist is the real control — browsers set `Origin`\nthemselves and a page cannot forge it — and the TTL bounds the window it has to\nbe wrong in. Without both, this is a port scanner any website could aim at a\nshop's network. `runFinder` therefore **refuses to start with no origin**:\nthere is no safe default, so there is no default.\n\n### Packaging the helper\n\n`./discovery/cli` is a function, not a binary, because this package publishes\nTypeScript source and a host's distribution is its own. A branded build bundles\n`main()` with esbuild and wraps it for each platform, baking its own origin in\nthrough `PRINTER_FINDER_ORIGINS` so a merchant double-clicks an icon rather than\ntyping a flag:\n\n```bash\nesbuild --bundle --platform=node --format=cjs --outfile=finder.cjs \\\n  --banner:js='process.env.PRINTER_FINDER_ORIGINS ||= \"https://admin.example.com\"' \\\n  your-entry.cjs\n```\n\n**A binary needs signing to be usable by the people it is for.** An unsigned\n`.exe` meets Windows SmartScreen's \"unrecognized app\" wall and an unnotarized\n`.app` is refused outright by Gatekeeper — for a shop owner, either is where\nthis feature ends. That is a certificate and a release pipeline, not code.\n\n## The three things that are easy to get wrong\n\n**The code page.** A printer does not speak UTF-8 — it prints the glyph at each\nbyte. Sending an accented word as UTF-8 puts two characters where one belongs,\non every printer, looking exactly like a font problem. `./escpos` selects CP850\nand maps the Latin-script characters a ticket carries; anything outside the\ntable loses its diacritic before it falls back to `?`, because a word somebody\ncan still read beats a `?` mid-word.\n\n**\"Sent\" is not \"printed\".** There is no protocol above a raw `:9100` socket: no\nhandshake, no acknowledgement, no status frame. An empty roll, a jam, and a\nperfect ticket are the same success — and `window.print()` returns when the\ndialog is dismissed, which is no better. A host that reports \"printed\" is making\na promise the transport never made. Give the operator a reprint instead.\n\n**A destination that owns a printer is answered by it or by nothing.** Switching\none room's printer off must stop that room's tickets, not quietly reroute them\nsomewhere else — a ticket coming out in the wrong room is one nobody in that\nroom ever sees. `./routing` keeps inactive printers in the map precisely so that\n\"no printer here\" and \"the printer here is off\" stay different answers.\n\n## Adoption\n\nSee [ADOPTING.md](./ADOPTING.md).\n","readmeFilename":"README.md"}