├── .env ├── .gitignore ├── .vscode └── launch.json ├── LICENSE ├── README.md ├── docs └── zpl_printer_configure.png ├── index.js ├── package.json └── ssl ├── server.cert └── server.key /.env: -------------------------------------------------------------------------------- 1 | # set to 'false', if you do not have the chrome extension 2 | CHROME_EXTENSION_ENABLED=true 3 | CHROME_EXTENSION_PORT=9102 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | .nvmrc 4 | /yarn.lock 5 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "pwa-node", 9 | "request": "launch", 10 | "name": "Launch Program", 11 | "skipFiles": [ 12 | "/**" 13 | ], 14 | "program": "${workspaceFolder}\\index.js" 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # zebra-emulator 2 | Paper saver that emulates the service provided by `BrowserPrint` application (Zebra ZPL). 3 | 4 | --- 5 | The `BrowserPrint` service application sends the ZPL to the Zebra Printer from the browser and we are intercepting those calls and logging them and optionally sending to a browser extension to view in real time (without using a physical printer or paper). 6 | 7 | - This project logs what would be sent to the printer 8 | - You can upload to an online viewer such as [labelary](http://labelary.com/viewer.html) or better yet, if you install the [Zpl Printer Chrome browser extension](https://chrome.google.com/webstore/detail/zpl-printer/phoidlklenidapnijkabnfdgmadlcmjo?hl=en-US) (ChromeOS only) or [ZplPrinter Emulator](https://github.com/MrL0co/ZplPrinter/releases) (Standalone Electron for Windows/Linux/Mac). Once installed and turned on you will see the label(s) rendered in real time. 9 | 10 | If you are using the ZplPrinter emulator, ensure to set the port to `9102` or match in your `.env` file. 11 | ![Configuring the port](https://raw.githubusercontent.com/brianzinn/zebra-emulator/main/docs/zpl_printer_configure.png) 12 | 13 | > `HTTP` service on 9100 14 | 15 | > `HTTPS` service on 9101 (with provided SSL cert) 16 | 17 | To build the SSL keys and cert files, use command: 18 | ``` 19 | $ openssl req -nodes -new -x509 -days 7300 -keyout server.key -out server.cert 20 | ``` 21 | 22 | If you are printing a lot at once it may overflow your terminal/prompt. You can redirect to a file for easier searching/viewing: 23 | ```bash 24 | $ yarn start > out.log 25 | ``` 26 | 27 | **NOTE: Makes use of ES Modules which require Node v14 or greater. 28 | 29 | If you are working on a website - check out the browserprint-es module: 30 | [github.com/brianzinn/browserprint-es](https://github.com/brianzinn/browserprint-es) 31 | -------------------------------------------------------------------------------- /docs/zpl_printer_configure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/brianzinn/zebra-emulator/a8008d10bcee00f7506263ec6ad4948fad0a9199/docs/zpl_printer_configure.png -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import express from 'express' 2 | import cors from 'cors' 3 | import fetch from 'node-fetch' 4 | import https from 'https' 5 | import fs from 'fs' 6 | import * as dotenv from 'dotenv'; 7 | 8 | const app = express(); 9 | dotenv.config(); 10 | app.use(cors()); 11 | app.use(express.json({ type: '*/*' })); 12 | 13 | const DEVICES_RESPONSE = { 14 | "deviceType": "printer", 15 | "uid": "Zebra ZP 500 (ZPL)", 16 | "provider": "com.zebra.ds.webdriver.desktop.provider.DefaultDeviceProvider", 17 | "name": "Zebra ZP 500 (ZPL)", 18 | "connection": "driver", 19 | "version": 3, 20 | "manufacturer": "Zebra Technologies" 21 | }; 22 | 23 | app.get('/default', (req, res) => { 24 | console.log('GET request for /default recieved') 25 | res.status(200).json(DEVICES_RESPONSE) 26 | }) 27 | 28 | app.post('/convert', (req, res) => { 29 | // TODO: set content-type to "text/plain"? 30 | // this is for PDF. the other ones would work. you should not need it. see: 31 | // https://developer.zebra.com/forum/25874 32 | res.send('The format conversion attempted requires a licensing key and none was provided'); 33 | }) 34 | 35 | app.get('/config', (req, res) => { 36 | res.json({ 37 | "application": { 38 | "supportedConversions": { 39 | "jpg": ["cpcl", "zpl", "kpl"], 40 | "tif": ["cpcl", "zpl", "kpl"], 41 | "pdf": ["cpcl", "zpl", "kpl"], 42 | "bmp": ["cpcl", "zpl", "kpl"], 43 | "pcx": ["cpcl", "zpl", "kpl"], 44 | "gif": ["cpcl", "zpl", "kpl"], 45 | "png": ["cpcl", "zpl", "kpl"], 46 | "jpeg": ["cpcl", "zpl", "kpl"] 47 | }, 48 | "version": "1.3.2.489", 49 | "apiLevel": 5, 50 | "buildNumber": 489, 51 | "platform": "windows" 52 | } 53 | }) 54 | }) 55 | 56 | app.get('/available', (req, res) => { 57 | console.log('GET request for /available recieved') 58 | res.json({ 59 | "deviceList": [ 60 | { 61 | "uid": "Zebra ZP 500 (ZPL)", 62 | "name": "Zebra ZP 500 (ZPL)" 63 | } 64 | ] 65 | }) 66 | }) 67 | 68 | // From browser-print ES 69 | // this._paperOut = this.isFlagSet(5); 70 | // this._paused = this.isFlagSet(7); 71 | // this._headOpen = this.isFlagSet(43); 72 | // this._ribbonOut = this.isFlagSet(45); 73 | const STATUS_REQUEST = '~HQES' 74 | const STATUS_RESPONSE = `${String.fromCharCode([2])} 75 | 76 | PRINTER STATUS 77 | ERRORS: 0 00000000 00000000 78 | WARNINGS: 0 00000000 00000000 79 | ${String.fromCharCode([3])}`; 80 | 81 | const FIXED_RESPONSES_MAP = { 82 | [STATUS_REQUEST]: STATUS_RESPONSE 83 | } 84 | 85 | const preparedResponses = []; 86 | 87 | app.post('/read', (req, res) => { 88 | // if(req.headers['content-type'] === 'text/plain') { 89 | // if(req.body in FIXED_RESPONSES_MAP) { 90 | // res.status(200).send(FIXED_RESPONSES_MAP[req.body]) 91 | // return; 92 | // } else { 93 | // console.log(`unexpected text/plain: '${req.body}'`); 94 | // } 95 | // } 96 | if (preparedResponses.length !== 0) { 97 | console.log('Returning prepared response'); 98 | res.status(200).send(preparedResponses.pop()); 99 | } else { 100 | res.status(200).send(); 101 | } 102 | }) 103 | 104 | app.post('/write', (req, res) => { 105 | console.log('POST request for /write recieved') 106 | 107 | const body = req.body; 108 | 109 | console.log('data:', body.data ? body.data : '"data" missing req.body'); 110 | console.log('---------------------------------------\n'); 111 | 112 | if (body && body.data) { 113 | if (body.data in FIXED_RESPONSES_MAP) { 114 | // TODO: should we not call the chrome extension here? 115 | console.log(`Response prepared for: ${body.data}`); 116 | preparedResponses.push(FIXED_RESPONSES_MAP[body.data]); 117 | } else if (process.env.CHROME_EXTENSION_ENABLED === String(true)) { 118 | const port = process.env.CHROME_EXTENSION_PORT ?? '9102'; 119 | // const payload = typeof req.body.data === 'string' ? req.body.data : Buffer.from(req.body.data); 120 | fetch(`http://localhost:${port}`, { method: 'POST', body: '{"mode":"print","epl":"' + Buffer.from(body.data) + '"}' }) 121 | .catch((e) => { 122 | // do nothing, keep the console clean 123 | }); 124 | } 125 | } 126 | res.json({}) 127 | }) 128 | 129 | const SERVER_PORT = 9100; 130 | const SERVER_PORT_SSL = 9101; 131 | const EXTENSION_PORT = process.env.CHROME_EXTENSION_PORT ?? '9101'; 132 | 133 | //SSL key and cert 134 | var option = { 135 | key: fs.readFileSync('ssl/server.key'), 136 | cert: fs.readFileSync('ssl/server.cert') 137 | }; 138 | 139 | 140 | https.createServer(option, app).listen(SERVER_PORT_SSL, () => { 141 | console.log(`\nBrowser Print Fake Server running on https://localhost:${SERVER_PORT}\n\n`) 142 | console.log('******************************************************************************') 143 | if (process.env.CHROME_EXTENSION_ENABLED === String(true)) { 144 | console.log( 145 | ' TIP: If you want to preview the label you can install the Zpl Printer\n extension from the chrome web store and set it up to listen on port ' + EXTENSION_PORT 146 | ) 147 | } else { 148 | console.log('Chrome extension support not enabled.'); 149 | } 150 | console.log('******************************************************************************\n') 151 | }); 152 | 153 | 154 | app.listen(SERVER_PORT, '127.0.0.1', () => { 155 | console.log(`\nBrowser Print Fake Server running on http://localhost:${SERVER_PORT}\n\n`) 156 | console.log('******************************************************************************') 157 | if (process.env.CHROME_EXTENSION_ENABLED === String(true)) { 158 | console.log( 159 | ' TIP: If you want to preview the label you can install the Zpl Printer\n extension from the chrome web store and set it up to listen on port ' + EXTENSION_PORT 160 | ) 161 | } else { 162 | console.log('Chrome extension support not enabled.'); 163 | } 164 | console.log('******************************************************************************\n') 165 | }); 166 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zebra-browser-print-emulator", 3 | "version": "1.0.0", 4 | "private": "true", 5 | "description": "Zebra BrowserPrinter emulator", 6 | "main": "index.js", 7 | "type": "module", 8 | "scripts": { 9 | "start": "node .", 10 | "startdev": "nodemon index.js" 11 | }, 12 | "engines": { 13 | "node": ">=14.0.0" 14 | }, 15 | "author": "Brian Zinn", 16 | "dependencies": { 17 | "cors": "^2.8.5", 18 | "dotenv": "^16.0.0", 19 | "express": "^4.17.1", 20 | "node-fetch": "^3.2.0", 21 | "nodemon": "^2.0.15" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ssl/server.cert: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDazCCAlOgAwIBAgIUIVR6qlAEUWh2CpDB6WRItvUk8sYwDQYJKoZIhvcNAQEL 3 | BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM 4 | GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMjAyMjIxNDIxNDRaFw00MjAy 5 | MTcxNDIxNDRaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw 6 | HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEB 7 | AQUAA4IBDwAwggEKAoIBAQDqiYgMiGVOFsRxfILLKoK6t8sqWpJl4PMeUYk7Y2dl 8 | 0kyehlNErrNOPthHw8P1fUZkNxOufvz3Nbob91ajgzNTNRNmUerJ8xEj4SKltJml 9 | sxTIuK3dR3zpZK82sZVn9WtRcCo5WDRpbSESjTGgqGxlN4autaoTmvRFTlG8dtGE 10 | AXVSQZCoZ4WHDB5A8D4U371/zFPAfpdLC7hOprooNAfgbAx4F96Rhj0jFwDlJTsK 11 | Y37ZLdolo1IsS19+CrEb3UJK51j5QE+1ApSP856bpPyT/ZcXoAKPU8L3DVdcdAVC 12 | z7wUviHZb8hISfjZDAYH/0RAlX3IPp0tKAguqywzL8nlAgMBAAGjUzBRMB0GA1Ud 13 | DgQWBBRXxl/0ZFFroO8zZdZicb1lUIwYyzAfBgNVHSMEGDAWgBRXxl/0ZFFroO8z 14 | ZdZicb1lUIwYyzAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQCT 15 | +7Si0M2D3mRme9t536Dr1nefxh/K+rbxmzz428+85nHqkUgzsSp6eD+n5qn45Uzh 16 | u0CpqlZEpLsnu1IgDdoaug5DnWsxMiSqx+I8suxmw15IH1AE/lJ8uPrjJSgqyHX/ 17 | GLxg+re+Z6+an1VlyUl9OzWms2AZF76bGNN5nJXjQQxY7VebL0UiwlQI3IR2ELFZ 18 | ud/K635pKGX1aQAM8f6zoJuJ/XUO8dA+sM5oTFfAWDhwKLg7+OCmE4wxmeLaEHFF 19 | HeE87ZMnfW9jmjmPVMdvxdSsaK9r7R8ATv8QkpMR9Xh7z2tVdAPI+3vnChkQiZsW 20 | rSsVZQ4o95mYE1QcLi/V 21 | -----END CERTIFICATE----- 22 | -------------------------------------------------------------------------------- /ssl/server.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDqiYgMiGVOFsRx 3 | fILLKoK6t8sqWpJl4PMeUYk7Y2dl0kyehlNErrNOPthHw8P1fUZkNxOufvz3Nbob 4 | 91ajgzNTNRNmUerJ8xEj4SKltJmlsxTIuK3dR3zpZK82sZVn9WtRcCo5WDRpbSES 5 | jTGgqGxlN4autaoTmvRFTlG8dtGEAXVSQZCoZ4WHDB5A8D4U371/zFPAfpdLC7hO 6 | prooNAfgbAx4F96Rhj0jFwDlJTsKY37ZLdolo1IsS19+CrEb3UJK51j5QE+1ApSP 7 | 856bpPyT/ZcXoAKPU8L3DVdcdAVCz7wUviHZb8hISfjZDAYH/0RAlX3IPp0tKAgu 8 | qywzL8nlAgMBAAECggEAKHFg0XBvr6CDbWJgF3KUKmhpEzudBQ5ePfsnY+NHQCqc 9 | ZbWaBDzv1G1H1FAwqVNZ4QwSS8PAEUzlc88Z1IZaQl3jo4A6sWpFqV84H0TePdF8 10 | owQdjwu1wXVINXE35z5NrDRgQ5UvLILR8iYAychhMK77fYL7+fd9aboQpCQFCvL0 11 | 6M7WI++sTjIotQFdz+gVgeD5oB3tG53gcSbZm31NSMocNVrjLcp9I3NKxwS606Lw 12 | zTjpseHgC8prLJjWcw1DI7Bxb/9+rmltqDeLov2BuSVaBcOAY6la2bQDKLszeSJK 13 | Myc7L466u5qIMOC0vH935Y7SAySume0Kuk6K97B/gQKBgQD4z4ew5ZlWu8P80vEM 14 | BrCSMIArCTloM5+dCI3mHm3UnIvH91LsS7jVZL3z/l1rIZ8yAWQuIm31jAD9Baga 15 | f6IH6ozzMxGC+DPfTq3qC/V2fGJANV8M81NZya4AqLgCuh9JrLx0wS7vlsza2CU+ 16 | F82cYpMAxroCu9tHUePH5bLGBQKBgQDxUGt5Qwwn0eiVQAKHliBeE16nruBOP+Fy 17 | cP6xPFZkDq2dp3VPOYJnOVILpgby/X/bHzIxsRQzHGrwBxNBxx6oj53PQi+lKkN7 18 | Yot08J0udMa9C6yIzdIv+jmxV40/8DIPG9Z9+0g/U22U/V62yRFd0H4L+A5WpsTG 19 | rSI/CJFaYQKBgGPokYgK6gPV6eD/oyM9HHHXX9/oGFErSZB4+uHInjmdktN9EKMp 20 | 1NJ7XXs4TLLUEWS3Ak80LAtJaKwxNUtOrPWYzXhqrUF2AP5OTa+Jlcs6LwmxZLWC 21 | j5miTmidVyZhLmcbIaGQYRR77U6DNJE2Za3CN108bzzhyi9dRqXiKmMxAoGAZDdC 22 | uvRmSu84ySrxIf/t50PiDn9otfZ/mThl/ZMSSbFy1sNhy4oVpuq+MQo9XpJ6k7Sr 23 | 8skYm3VUz+QQUkPwbo5eOmHBSbV4mvg89E6s6Esk8gO0HohjnvsrFIRIq+ID2BR2 24 | ipFd2ra6DqL+v6hXJv/cKPpWchQGKoBidY0IWGECgYAQB3proIm+GHOO3V70RysV 25 | JAbt5d3rmSyBt+yImFpKPqmNsUwVy7+nIKsi7+1otRoi8s7YotXGrnp5Qw8l446X 26 | 17haBBq6ej+G/d+elsXvt5SPMWeTpJqGq3cjxdOoNj5evdPKUhjNXFcEVagaooY4 27 | dMTafKYqzIa6+IPhQSc2ew== 28 | -----END PRIVATE KEY----- 29 | --------------------------------------------------------------------------------