From 69d0d54ddf79bf586c72afd9e003c56d66850b31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois-L=C3=A9onard=20Gilbert?= <83510612+gilbertfl@users.noreply.github.com> Date: Sun, 20 Jul 2025 10:28:35 -0400 Subject: [PATCH 01/16] Update README.md Update readme so all references point to v3.1. Add references to the prebuilt container on Docker Hub and change the quickstart to explain how to run from there. Move the build instructions to a new section, and add a description of the debugging facilities. --- README.md | 68 ++++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 57 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 6179e6e..93f4c71 100644 --- a/README.md +++ b/README.md @@ -17,28 +17,74 @@ A print cannot last longer than 10 seconds. This timeout could be changed at so This project requires: - A Docker installation (kubernetes should work, but is untested.) -To install v3.0 from source: +### Use the prebuilt container +A prebuilt container of this project is avaliable on [Docker Hub](https://hub.docker.com/repository/docker/gilbertfl/escpos-netprinter). + +To run the prebuilt container: ```bash -wget --show-progress https://github.com/gilbertfl/escpos-netprinter/archive/refs/tags/3.0.zip -unzip 3.0.zip -cd escpos-netprinter-3.0 -docker build -t escpos-netprinter:3.0 . +docker run -d \ + -p 515:515/tcp \ + -p 80:80/tcp \ + -p 9100:9100/tcp \ + --mount source=receiptVolume,target=/home/escpos-emu/web \ + gilbertfl/escpos-netprinter:3.1 +``` +### Once started +Once started, the container will accept prints by JetDirect on the default port(9100) and by lpd on the default port(515). You can access all received receipts with the web application at port 80. + +Version 3.1 is capable of dealing with all status requests from POS systems as described in the Epson APG. + +## Working on the code + +### Building from source + +To install v3.1 from source: + +```bash +wget --show-progress https://github.com/gilbertfl/escpos-netprinter/archive/refs/tags/3.1.zip +unzip 3.1.zip +cd escpos-netprinter-3.1 +docker build -t escpos-netprinter:3.1 . ``` To run the resulting container: ```bash -docker run -d --rm --name escpos_netprinter -p 515:515/tcp -p 80:80/tcp -p 9100:9100/tcp escpos-netprinter:3.0 +docker run -d \ + -p 515:515/tcp \ + -p 80:80/tcp \ + -p 9100:9100/tcp \ + --mount source=receiptVolume,target=/home/escpos-emu/web \ + escpos-netprinter:3.1 ``` -It should now accept prints by JetDirect on the default port(9100) and by lpd on the default port(515), and you can visualize it with the web application at port 80. -For debugging, you can add port 631 to access the CUPS interface. The CUPS administrative username is `cupsadmin` and the password is `123456`. -As of version 2.0, this has been tested to work with a regular POS program without adapting it. +### Debugging +Two interfaces have been made avaliable to help debugging. + +There is also a general environment flag that make the Docker logs more verbose: set ```ESCPOS_DEBUG=True``` (case-sensitive) +```bash +docker run -d \ + -p 515:515/tcp \ + -p 80:80/tcp \ + -p 9100:9100/tcp \ + --mount source=receiptVolume,target=/home/escpos-emu/web \ + --env ESCPOS_DEBUG=True \ + escpos-netprinter:3.1 +``` -Version 3.0 is now dealing with all status requests from POS systems as described in the Epson APG. +If you have problems with the CUPS interface, you can add port 631 to access the CUPS administrator interface. The CUPS administrative username is `cupsadmin` and the password is `123456`; you can change that in the dockerfile or at runtime inside the administrator interface. +```bash +docker run -d \ + -p 515:515/tcp \ + -p 80:80/tcp \ + -p 9100:9100/tcp \ + -p 631:631/tcp + --mount source=receiptVolume,target=/home/escpos-emu/web \ + escpos-netprinter:3.1 +``` ## Known issues -While version 3.0 is no longer a beta version, it has known defects: +While version 3.1 is no longer a beta version, it has known defects: - It still uses the Flask development server, so it is unsafe for public networks. - While it works with simple drivers, for example the one for the MUNBYN ITPP047 printers, the [Epson utilities](https://download.epson-biz.com/modules/pos/) refuse to speak to it. From a0b65b6e0dae020c7625baacc50e5006b61a66dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois-L=C3=A9onard=20Gilbert?= <83510612+gilbertfl@users.noreply.github.com> Date: Sun, 20 Jul 2025 10:33:42 -0400 Subject: [PATCH 02/16] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 93f4c71..d0dff3f 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,8 @@ docker run -d \ ### Once started Once started, the container will accept prints by JetDirect on the default port(9100) and by lpd on the default port(515). You can access all received receipts with the web application at port 80. +The receipts are kept on a docker volume, so they will be kept if the container is restarted. To make the prints temporary, simply remove the `--mount` line from the run command. + Version 3.1 is capable of dealing with all status requests from POS systems as described in the Epson APG. ## Working on the code From 5f43b660198576551d863877969146814ab98269 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois-L=C3=A9onard=20Gilbert?= <83510612+gilbertfl@users.noreply.github.com> Date: Sun, 20 Jul 2025 10:41:45 -0400 Subject: [PATCH 03/16] Update README.md --- README.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index d0dff3f..155a9bc 100644 --- a/README.md +++ b/README.md @@ -41,13 +41,13 @@ Version 3.1 is capable of dealing with all status requests from POS systems as d ### Building from source -To install v3.1 from source: +To install v3.1.1 from source: ```bash -wget --show-progress https://github.com/gilbertfl/escpos-netprinter/archive/refs/tags/3.1.zip -unzip 3.1.zip -cd escpos-netprinter-3.1 -docker build -t escpos-netprinter:3.1 . +wget --show-progress https://github.com/gilbertfl/escpos-netprinter/archive/refs/tags/3.1.1.zip +unzip 3.1.1.zip +cd escpos-netprinter-3.1.1 +docker build -t escpos-netprinter:3.1.1 . ``` To run the resulting container: @@ -57,7 +57,7 @@ docker run -d \ -p 80:80/tcp \ -p 9100:9100/tcp \ --mount source=receiptVolume,target=/home/escpos-emu/web \ - escpos-netprinter:3.1 + escpos-netprinter:3.1.1 ``` ### Debugging @@ -71,7 +71,7 @@ docker run -d \ -p 9100:9100/tcp \ --mount source=receiptVolume,target=/home/escpos-emu/web \ --env ESCPOS_DEBUG=True \ - escpos-netprinter:3.1 + escpos-netprinter:3.1.1 ``` If you have problems with the CUPS interface, you can add port 631 to access the CUPS administrator interface. The CUPS administrative username is `cupsadmin` and the password is `123456`; you can change that in the dockerfile or at runtime inside the administrator interface. @@ -82,11 +82,11 @@ docker run -d \ -p 9100:9100/tcp \ -p 631:631/tcp --mount source=receiptVolume,target=/home/escpos-emu/web \ - escpos-netprinter:3.1 + escpos-netprinter:3.1.1 ``` ## Known issues -While version 3.1 is no longer a beta version, it has known defects: +While version 3.1.1 is no longer a beta version, it has known defects: - It still uses the Flask development server, so it is unsafe for public networks. - While it works with simple drivers, for example the one for the MUNBYN ITPP047 printers, the [Epson utilities](https://download.epson-biz.com/modules/pos/) refuse to speak to it. From 3b69f213a9e6b34ffd0986f3d05ebcc8192f5609 Mon Sep 17 00:00:00 2001 From: Francois-Leonard Gilbert Date: Sun, 20 Jul 2025 11:28:05 -0400 Subject: [PATCH 04/16] Stupid bug: when a CUPS print comes in, it truncates the log. --- escpos-netprinter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/escpos-netprinter.py b/escpos-netprinter.py index 1d76ab2..52dc815 100644 --- a/escpos-netprinter.py +++ b/escpos-netprinter.py @@ -1570,7 +1570,7 @@ def publish_receipt_from_CUPS(): #Load the log file from /var/spool/cups/tmp/ and append it in web/tmp/esc2html_log logfile_filename = os.environ['LOG_FILENAME'] # print(logfile_filename) - log = open(PurePath('web','tmp', 'esc2html_log'), mode='wt') + log = open(PurePath('web','tmp', 'esc2html_log'), mode='a') source_log = open(source_dir.joinpath(logfile_filename), mode='rt') log.write(f"CUPS print received at {datetime.now(tz=ZoneInfo('Canada/Eastern')).strftime('%Y%b%d %X.%f %Z')}\n") log.write(source_log.read()) From 9b9a3bf72366b101e93ce1e20013d72b19f7ef68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois-L=C3=A9onard=20Gilbert?= <83510612+gilbertfl@users.noreply.github.com> Date: Sun, 20 Jul 2025 11:32:31 -0400 Subject: [PATCH 05/16] Update README.md Add more details for usage. --- README.md | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 155a9bc..c50e4f5 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ docker run -d \ ### Debugging Two interfaces have been made avaliable to help debugging. -There is also a general environment flag that make the Docker logs more verbose: set ```ESCPOS_DEBUG=True``` (case-sensitive) +There is also a general environment flag that make the Docker logs verbose: set ```ESCPOS_DEBUG=True``` (case-sensitive) ```bash docker run -d \ -p 515:515/tcp \ @@ -73,6 +73,11 @@ docker run -d \ --env ESCPOS_DEBUG=True \ escpos-netprinter:3.1.1 ``` +Setting this variable will generate logs from 3 different sources: +- The CUPS printer driver +- Jetdirect requests +- The ESC/POS to HTML conversion itself +- Accesses to the web interface If you have problems with the CUPS interface, you can add port 631 to access the CUPS administrator interface. The CUPS administrative username is `cupsadmin` and the password is `123456`; you can change that in the dockerfile or at runtime inside the administrator interface. ```bash @@ -85,6 +90,24 @@ docker run -d \ escpos-netprinter:3.1.1 ``` +### Runtime Directory Structure + +The following directories inside the container are useful: +- `/home/escpos-emu/web/`: Stores all the printed receipts and other control info +- `/home/escpos-emu/web/receipts`: Stores the HTML receipts +- `/home/escpos-emu/web/tmp`: Stores temporary files during processing (for debugging only) +- `/home/escpos-emu/web/receipt_list.csv`: Created at runtime, this file contains the list of the printed receipts with the file location. + +## Configuration Options + +The following environment variables can be configured: +| Variable | Default | Description | +|----------|---------|-------------| +| ESCPOS_DEBUG | false | Enable debug mode for detailed logs | +| PRINTER_PORT | 9100 | JetDirect port for printer communication | +| FLASK_RUN_DEBUG | false | Enable Flask debug mode | +| FLASK_RUN_PORT | 80 | Sets the listening port for the web interface | + ## Known issues While version 3.1.1 is no longer a beta version, it has known defects: - It still uses the Flask development server, so it is unsafe for public networks. From 028804401183b04277e4b3c8c544f124b8f4d806 Mon Sep 17 00:00:00 2001 From: erikn69 Date: Thu, 2 Oct 2025 17:40:53 -0500 Subject: [PATCH 06/16] Print barcodes --- composer.json | 3 +- esc2html.php | 38 ++++++++++++++++++++++++++ src/Parser/Command/BarcodeAData.php | 4 +++ src/Parser/Command/BarcodeBData.php | 4 +++ src/Parser/Command/PrintBarcodeCmd.php | 10 +++++++ src/resources/esc2html.css | 16 +++++++++++ 6 files changed, 74 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 3c22314..7bb88e4 100644 --- a/composer.json +++ b/composer.json @@ -4,7 +4,8 @@ "squizlabs/php_codesniffer" : "^2.8", "mike42/escpos-php": "^1.5", "chillerlan/php-qrcode": "^5.0.3", - "zbateson/mb-wrapper": "2.0.1" + "zbateson/mb-wrapper": "2.0.1", + "picqer/php-barcode-generator": "^3.2" }, "autoload" : { "psr-4" : { diff --git a/esc2html.php b/esc2html.php index 1879f8d..dd360c1 100644 --- a/esc2html.php +++ b/esc2html.php @@ -165,6 +165,44 @@ break; } } + } else if ($cmd -> isAvailableAs('PrintBarcodeCmd')) { + $types = [ + 0 => 'TypeUpcA', + 65 => 'TypeUpcA', + 1 => 'TypeUpcE', + 66 => 'TypeUpcE', + 2 => 'TypeEan13', + 67 => 'TypeEan13', + 3 => 'TypeEan8', + 68 => 'TypeEan8', + 4 => 'TypeCode39', + 69 => 'TypeCode39', + 7 => 'TypeITF14', + 70 => 'TypeITF14', + 6 => 'TypeCodabar', + 71 => 'TypeCodabar', + 72 => 'TypeCode93', + 73 => 'TypeCode128', + 74 => 'TypeCode128', + ]; + $type = $types[$cmd->getType()] ?? null; + $data = $cmd -> subCommand()->getData(); + $lineHtml = ""; // flush buffer + + if ($type){ + $renderer = new \Picqer\Barcode\Renderers\PngRenderer(); + $renderer->setBackgroundColor([255, 255, 255]); + $renderer->useGd(); + $type = '\\Picqer\\Barcode\\Types\\' . $type; + $barcode = (new $type)->getBarcode($data); + $imgSrc = base64_encode($renderer->render($barcode, $barcode->getWidth(), 40)); # TODO: Check width/height on cmd? + $outp[] = "
\"{$data}\"
"; + } else { + # TODO: maybe implement by printing the info? + //$classes = getBlockClasses($formatting); + //$classesStr = implode(" ", $classes); + //$outp[] = wrapInline("
", "
", "BARCODE NOT SUPPORTED - {$cmd->getType()}[{$data}]"); + } } } diff --git a/src/Parser/Command/BarcodeAData.php b/src/Parser/Command/BarcodeAData.php index a01355b..5e6332a 100644 --- a/src/Parser/Command/BarcodeAData.php +++ b/src/Parser/Command/BarcodeAData.php @@ -19,4 +19,8 @@ public function addChar($char) $this -> data .= $char; } } + + public function getData() { + return $this -> data; + } } diff --git a/src/Parser/Command/BarcodeBData.php b/src/Parser/Command/BarcodeBData.php index f21ad18..c5a38f9 100644 --- a/src/Parser/Command/BarcodeBData.php +++ b/src/Parser/Command/BarcodeBData.php @@ -20,4 +20,8 @@ public function addChar($char) } return false; } + + public function getData() { + return $this -> data; + } } diff --git a/src/Parser/Command/PrintBarcodeCmd.php b/src/Parser/Command/PrintBarcodeCmd.php index a72e19a..80b983e 100644 --- a/src/Parser/Command/PrintBarcodeCmd.php +++ b/src/Parser/Command/PrintBarcodeCmd.php @@ -24,4 +24,14 @@ public function addChar($char) } return $this -> subCommand -> addChar($char); } + + public function subCommand() + { + // TODO rename and take getSubCommand() name. + return $this -> subCommand; + } + + public function getType() { + return $this -> m; + } } diff --git a/src/resources/esc2html.css b/src/resources/esc2html.css index 31253cf..ec7c8b2 100644 --- a/src/resources/esc2html.css +++ b/src/resources/esc2html.css @@ -73,6 +73,22 @@ body { font-size: 75% } +.esc-line-command{ + text-align: center; + font-weight: bold; + background: linear-gradient(180deg, + rgba(0,0,0,0) calc(50% - 1px), + rgba(192,192,192,1) calc(50%), + rgba(0,0,0,0) calc(50% + 1px) + ); +} + +.esc-line-command .command{ + background-color:white; + padding-left: 10px; + padding-right: 10px; +} + span { display: inline-block; } From 904aaf17a9544808087adbbfbd57dd4d87f3779b Mon Sep 17 00:00:00 2001 From: Erik Niebla Date: Fri, 3 Oct 2025 17:21:07 +0100 Subject: [PATCH 07/16] Barcode demo binary, fixes --- barcodes.bin | 21 ++++++++++++ esc2html.php | 44 ++++++++++++++++--------- src/Parser/Command/CommandOneArg.php | 2 +- src/Parser/Command/CommandThreeArgs.php | 15 +++++++++ src/Parser/Command/CommandTwoArgs.php | 10 ++++++ src/resources/esc2html.css | 5 ++- 6 files changed, 78 insertions(+), 19 deletions(-) create mode 100644 barcodes.bin diff --git a/barcodes.bin b/barcodes.bin new file mode 100644 index 0000000..6342322 --- /dev/null +++ b/barcodes.bin @@ -0,0 +1,21 @@ +@ +.ta!8Code128: +Hh-kI012z !., +.ta!8Code93: +Hh-kH012.ABZ +.ta!8Codabar: +Hh-kG A012$+-./:A +.ta!8Code39: +Hh-kEABCZ012 +.ta!8Ean8: +Hh-kD0123456 +.ta!8Ean13: +Hh-kC 012345678901 +.ta!8UpcE: +Hh-kB1234567 +.ta!8UpcA: +Hh-kA 036000291452 +.ta!8ITF: +Hh-kF +0123456789 +d \ No newline at end of file diff --git a/esc2html.php b/esc2html.php index dd360c1..a9ccc25 100644 --- a/esc2html.php +++ b/esc2html.php @@ -18,7 +18,7 @@ exit(1); } else { - if ($argv[1]=='--debug'){ + if ($argv[1]=='--debug'){ $debugMode = true; if (!isset($argv[2])) { print("Usage: php " . $argv[0] . " [--debug] filename ". $argc-1 . " arguments received\n"); @@ -46,7 +46,7 @@ if ( !$fp ) { error_log("File ". $targetFilename . "not found."); exit(1); -} +} $parser = new Parser(); $parser -> addFile($fp); @@ -60,6 +60,9 @@ $imgNo = 0; $skipLineBreak = false; $code2dStorage = new Code2DStateStorage(); +$barcodeHeight = null; +$barcodeWidth = null; +$barcodeHri = null; foreach ($commands as $cmd) { if ($debugMode) error_log("". get_class($cmd) ."", 0); //Output the command class in the debug console @@ -121,7 +124,7 @@ error_log("Data size:". $sub->getDataSize() ."",0); error_log("Data: " . $sub->get_data() ."",0); } - if($sub->isAvailableAs('QRCodeSubCommand')){ + if($sub->isAvailableAs('QRCodeSubCommand')){ switch ($sub->get_fn()) { case 65: //set model $code2dStorage->setQRModel($sub->get_data()); @@ -158,13 +161,19 @@ $qrcodeData = $code2dStorage->getQRCodeData(); $outp[] = "
\"$qrcodeData\"
"; } - + break; case 82: //Transmit size information of symbol storage data. # TODO: maybe implement by printing the info? break; } } + }if ($cmd -> isAvailableAs('SetBarcodeHeightCmd')) { + $barcodeHeight = $cmd -> getArg(); + } else if ($cmd -> isAvailableAs('SetBarcodeWidthCmd')) { + $barcodeWidth = $cmd -> getArg(); + } else if ($cmd -> isAvailableAs('SelectHriPrintPosCmd')) { + $barcodeHri = $cmd -> getArg(); } else if ($cmd -> isAvailableAs('PrintBarcodeCmd')) { $types = [ 0 => 'TypeUpcA', @@ -177,32 +186,37 @@ 68 => 'TypeEan8', 4 => 'TypeCode39', 69 => 'TypeCode39', - 7 => 'TypeITF14', - 70 => 'TypeITF14', 6 => 'TypeCodabar', 71 => 'TypeCodabar', 72 => 'TypeCode93', 73 => 'TypeCode128', - 74 => 'TypeCode128', ]; $type = $types[$cmd->getType()] ?? null; $data = $cmd -> subCommand()->getData(); - $lineHtml = ""; // flush buffer - + $classes = getBlockClasses($formatting); + $classesStr = implode(" ", $classes); if ($type){ $renderer = new \Picqer\Barcode\Renderers\PngRenderer(); $renderer->setBackgroundColor([255, 255, 255]); $renderer->useGd(); $type = '\\Picqer\\Barcode\\Types\\' . $type; $barcode = (new $type)->getBarcode($data); - $imgSrc = base64_encode($renderer->render($barcode, $barcode->getWidth(), 40)); # TODO: Check width/height on cmd? - $outp[] = "
\"{$data}\"
"; + $imgSrc = base64_encode($renderer->render($barcode, $barcodeWidth ?? $barcode->getWidth(), $barcodeHeight ?? 40)); + $lineHtml = "\"{$data}\""; + } else { - # TODO: maybe implement by printing the info? - //$classes = getBlockClasses($formatting); - //$classesStr = implode(" ", $classes); - //$outp[] = wrapInline("
", "
", "BARCODE NOT SUPPORTED - {$cmd->getType()}[{$data}]"); + $classesStr .= ' esc-line-command'; + $lineHtml = "BARCODE {$cmd->getType()} (NO PREVIEW)" . (in_array($barcodeHri, [1, 2, 3]) ? '' : " [$data]") . ""; + } + if (in_array($barcodeHri, [1, 3])) { + $lineHtml = "
$data
" . $lineHtml; } + if (in_array($barcodeHri, [2, 3])) { + $lineHtml = $lineHtml . "
$data
"; + } + $outp[] = wrapInline("
", "
", wrapInline("", "", $lineHtml)); + $lineHtml = ""; // flush buffer + $barcodeWidth = $barcodeHeight = $barcodeHri = null; } } diff --git a/src/Parser/Command/CommandOneArg.php b/src/Parser/Command/CommandOneArg.php index d854067..f3f1bb2 100644 --- a/src/Parser/Command/CommandOneArg.php +++ b/src/Parser/Command/CommandOneArg.php @@ -17,7 +17,7 @@ public function addChar($char) } } - protected function getArg() + public function getArg() { return $this -> arg; } diff --git a/src/Parser/Command/CommandThreeArgs.php b/src/Parser/Command/CommandThreeArgs.php index c50d7ed..85ba730 100644 --- a/src/Parser/Command/CommandThreeArgs.php +++ b/src/Parser/Command/CommandThreeArgs.php @@ -23,4 +23,19 @@ public function addChar($char) } return false; } + + public function getArg1() + { + return $this -> arg1; + } + + public function getArg2() + { + return $this -> arg2; + } + + public function getArg3() + { + return $this -> arg3; + } } diff --git a/src/Parser/Command/CommandTwoArgs.php b/src/Parser/Command/CommandTwoArgs.php index 1949cb1..6016e31 100644 --- a/src/Parser/Command/CommandTwoArgs.php +++ b/src/Parser/Command/CommandTwoArgs.php @@ -19,4 +19,14 @@ public function addChar($char) } return false; } + + public function getArg1() + { + return $this -> arg1; + } + + public function getArg2() + { + return $this -> arg2; + } } diff --git a/src/resources/esc2html.css b/src/resources/esc2html.css index ec7c8b2..9931881 100644 --- a/src/resources/esc2html.css +++ b/src/resources/esc2html.css @@ -84,9 +84,8 @@ body { } .esc-line-command .command{ - background-color:white; - padding-left: 10px; - padding-right: 10px; + background-color: white; + padding: 1px 10px 1px 10px; } span { From 33ac1985aca0f957119d07ebd10fbde34b24ee7f Mon Sep 17 00:00:00 2001 From: erikn69 Date: Sat, 4 Oct 2025 10:02:29 -0500 Subject: [PATCH 08/16] Avoid double encoding problem when text is already UTF8 --- src/Parser/Command/TextCmd.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Parser/Command/TextCmd.php b/src/Parser/Command/TextCmd.php index 4fbdd90..dc70381 100644 --- a/src/Parser/Command/TextCmd.php +++ b/src/Parser/Command/TextCmd.php @@ -33,7 +33,10 @@ public function addChar($char) public function getText(InlineFormatting $context = new InlineFormatting): array|bool|string { $text = ""; - if($context->charCodeTable == "auto"){ + if(mb_detect_encoding((string) $this->str, 'UTF-8', true)){ + $text = $this->str; + } + else if($context->charCodeTable == "auto"){ # This charset is unknown to MbWrapper, so we try mbstring's "auto" as a last resort. $text = mb_convert_encoding(string: $this->str, to_encoding: "UTF-8", from_encoding: "auto"); } From 06969b6e7ad2e7ba68e80e62749036693310b372 Mon Sep 17 00:00:00 2001 From: erikn69 Date: Tue, 7 Oct 2025 09:35:10 -0500 Subject: [PATCH 09/16] Use Imagick if available --- esc2html.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/esc2html.php b/esc2html.php index a9ccc25..aac0a84 100644 --- a/esc2html.php +++ b/esc2html.php @@ -198,7 +198,11 @@ if ($type){ $renderer = new \Picqer\Barcode\Renderers\PngRenderer(); $renderer->setBackgroundColor([255, 255, 255]); - $renderer->useGd(); + if (!class_exists(\Imagick::class)) { + $renderer->useGd(); + } else { + $renderer->useImagick(); + } $type = '\\Picqer\\Barcode\\Types\\' . $type; $barcode = (new $type)->getBarcode($data); $imgSrc = base64_encode($renderer->render($barcode, $barcodeWidth ?? $barcode->getWidth(), $barcodeHeight ?? 40)); From cd4acc9a13a276584b5bde6fa6367a2f508a7a32 Mon Sep 17 00:00:00 2001 From: Francois-Leonard Gilbert Date: Thu, 6 Nov 2025 22:59:41 -0500 Subject: [PATCH 10/16] Internationalization of the UI Fixes gilbertfl/escpos-netprinter#14 --- .dockerignore | 6 +- README.md | 12 +++ babel.cfg | 2 + babel_config.py | 4 + compile_babel_translations.sh | 4 + dockerfile | 6 ++ escpos-netprinter.py | 52 +++++++++++-- extract_babel_messages.sh | 4 + init_babel_locale.sh | 11 +++ templates/accueil.html.j2 | 20 +++-- templates/{footer.html => footer.html.j2} | 3 +- templates/lang_select.html.j2 | 12 +++ templates/receiptList.html.j2 | 8 +- translations/en/LC_MESSAGES/messages.mo | Bin 0 -> 1268 bytes translations/en/LC_MESSAGES/messages.po | 85 ++++++++++++++++++++++ translations/fr/LC_MESSAGES/messages.mo | Bin 0 -> 1318 bytes translations/fr/LC_MESSAGES/messages.po | 85 ++++++++++++++++++++++ translations/messages.pot | 84 +++++++++++++++++++++ update_babel_messages.sh | 5 ++ 19 files changed, 381 insertions(+), 22 deletions(-) create mode 100644 babel.cfg create mode 100644 babel_config.py create mode 100755 compile_babel_translations.sh create mode 100755 extract_babel_messages.sh create mode 100755 init_babel_locale.sh rename templates/{footer.html => footer.html.j2} (57%) create mode 100644 templates/lang_select.html.j2 create mode 100644 translations/en/LC_MESSAGES/messages.mo create mode 100644 translations/en/LC_MESSAGES/messages.po create mode 100644 translations/fr/LC_MESSAGES/messages.mo create mode 100644 translations/fr/LC_MESSAGES/messages.po create mode 100644 translations/messages.pot create mode 100755 update_babel_messages.sh diff --git a/.dockerignore b/.dockerignore index eaf6ab3..9b4bae3 100644 --- a/.dockerignore +++ b/.dockerignore @@ -31,4 +31,8 @@ __pycache__ # Remove test scripts tests/** -tests \ No newline at end of file +tests + +# Remove pybabel config and translation scripts from the runtime container +babel.cfg +*_babel_*.sh \ No newline at end of file diff --git a/README.md b/README.md index c50e4f5..c2e347a 100644 --- a/README.md +++ b/README.md @@ -98,6 +98,18 @@ The following directories inside the container are useful: - `/home/escpos-emu/web/tmp`: Stores temporary files during processing (for debugging only) - `/home/escpos-emu/web/receipt_list.csv`: Created at runtime, this file contains the list of the printed receipts with the file location. +## Translations + +This project supports internationalization using [Flask-Babel](https://python-babel.github.io/flask-babel). All translation files are in the `/translations` subdirectory, including the `messages.pot` file. + +You can create a new locale using the following scripts: +- `init_babel_locale.sh`: to create a new locale file NOTE: you will need to add the locale in `babel_config.py` manually after this. +- `compile_babel_translations.sh`: to compile all locales and make them available to Flask. This should be launched after every change to the translations, not immediately after creating a locale. + +Other helpful scripts for developers : +- `update_babel_messages.sh`: If you add more translatable text in the UI or code, use this script to make Babel extract and add them to the locale files +- `extract_babel_messages.sh`: If you ever want to generate the `messages.pot` file without updating any locale, this is the script. + ## Configuration Options The following environment variables can be configured: diff --git a/babel.cfg b/babel.cfg new file mode 100644 index 0000000..a76950c --- /dev/null +++ b/babel.cfg @@ -0,0 +1,2 @@ +[python: ./*.py] +[jinja2: templates/**.html.j2] \ No newline at end of file diff --git a/babel_config.py b/babel_config.py new file mode 100644 index 0000000..1c029ac --- /dev/null +++ b/babel_config.py @@ -0,0 +1,4 @@ +# This is the flask-babel runtime config file + +class Config: + LANGUAGES = ['fr', 'en'] \ No newline at end of file diff --git a/compile_babel_translations.sh b/compile_babel_translations.sh new file mode 100755 index 0000000..1ea05da --- /dev/null +++ b/compile_babel_translations.sh @@ -0,0 +1,4 @@ +#!/bin/bash +# This script compiles all locales and makes them usable by Python. + +pybabel compile -d translations \ No newline at end of file diff --git a/dockerfile b/dockerfile index 3ae81d7..204bb85 100644 --- a/dockerfile +++ b/dockerfile @@ -12,6 +12,7 @@ RUN install-php-extensions mbstring @composer imagick #Install Flask RUN apt-get update RUN apt-get install -y python3-flask +RUN apt-get install -y python3-flask-babel RUN apt-get install -y python3-lxml #Install CUPS @@ -54,6 +55,11 @@ ENV FLASK_RUN_DEBUG=false # To activate the netprinter debug mode, set at True (case-sensitive) ENV ESCPOS_DEBUG=false +#Localization support environment variables +ENV ESCPOS_TIMEZONE="America/Montreal" + + +# Expose all necessary ports EXPOSE ${PRINTER_PORT} EXPOSE ${FLASK_RUN_PORT} #Expose the lpd port diff --git a/escpos-netprinter.py b/escpos-netprinter.py index 52dc815..1ca5ca6 100644 --- a/escpos-netprinter.py +++ b/escpos-netprinter.py @@ -1,5 +1,5 @@ import os -from flask import Flask, redirect, render_template, request, url_for +from flask import Flask, redirect, render_template, request, session, url_for from os import getenv from io import BufferedWriter import csv @@ -9,11 +9,12 @@ from lxml import html, etree from datetime import datetime from zoneinfo import ZoneInfo +from flask_babel import Babel +import babel_config import threading import socketserver - #Network ESC/pos printer server class ESCPOSServer(socketserver.TCPServer): @@ -1409,7 +1410,7 @@ def print_toHTML(self, binfile:BufferedWriter, bin_filename:PurePath): # append the error output to the log file with open(PurePath('web','tmp', 'esc2html_log'), mode='at') as log: log.write(f"Error while converting a JetDirect print: {err.returncode}") - log.write(datetime.now(tz=ZoneInfo("Canada/Eastern")).strftime('%Y%b%d %X.%f %Z')) + log.write(datetime.now(tz=ZoneInfo(os.environ['ESCPOS_TIMEZONE'])).strftime('%Y%b%d %X.%f %Z')) log.write(err.stderr) log.close() @@ -1421,13 +1422,13 @@ def print_toHTML(self, binfile:BufferedWriter, bin_filename:PurePath): print (f"Receipt decoded", flush=True) with open(PurePath('web','tmp', 'esc2html_log'), mode='at') as log: log.write("Successful JetDirect print\n") - log.write(datetime.now(tz=ZoneInfo("Canada/Eastern")).strftime('%Y%b%d %X.%f %Z\n\n')) + log.write(datetime.now(tz=ZoneInfo(os.environ['ESCPOS_TIMEZONE'])).strftime('%Y%b%d %X.%f %Z\n\n')) log.write(recu.stderr) log.close() #print(recu.stdout, flush=True) #Ajouter un titre au reçu - heureRecept = datetime.now(tz=ZoneInfo("Canada/Eastern")) + heureRecept = datetime.now(tz=ZoneInfo(os.environ['ESCPOS_TIMEZONE'])) recuConvert = self.add_html_title(heureRecept, recu.stdout) #print(etree.tostring(theHead), flush=True) @@ -1483,8 +1484,39 @@ def add_receipt_to_directory(new_filename: str, self=None) -> None: writer.writerow([next_fileID, new_filename]) +## Printer UI + +# Flask startup app = Flask(__name__) +# Babel setup + +def get_locale() -> str | None: + lang = request.cookies.get('lang') + if lang in babel_config.Config.LANGUAGES : + return lang + else : + return request.accept_languages.best_match(babel_config.Config.LANGUAGES) + +babel = Babel(app, locale_selector=get_locale) + +@app.context_processor +def inject_conf_var(): + return dict(AVAILABLE_LANGUAGES=babel_config.Config.LANGUAGES, + CURRENT_LANGUAGE=get_locale()) + +@app.route('/language=') +def set_language(language=None): + resp = redirect(url_for('accueil')) + # Check if the desired translation is available. If not, get the best match to what the browser desires. + if language in babel_config.Config.LANGUAGES: + resp.set_cookie('lang', language) + else: + resp.set_cookie('lang', request.accept_languages.best_match(babel_config.Config.LANGUAGES) ) # Ignore the request and guess the preferred language of the browser. + return resp + + +# Flask routes for the UI @app.route("/") def accueil(): return render_template('accueil.html.j2', host = request.host.split(':')[0], @@ -1534,14 +1566,14 @@ def show_receipt(fileID:int): with open(PurePath('web', 'receipts', filename), mode='rt') as receipt: receipt_html = receipt.read() # Read the file content receipt_html = receipt_html.replace('', '
') - receipt_html = receipt_html.replace('', '
' + render_template('footer.html') + '') # Append the footer + receipt_html = receipt_html.replace('', '' + render_template('footer.html.j2') + '') # Append the footer return receipt_html @app.route("/newReceipt") def publish_receipt_from_CUPS(): """ Get the receipt from the CUPS temp directory and publish it in the web/receipts directory and add the corresponding log to our permanent logfile""" - heureRecept = datetime.now(tz=ZoneInfo("Canada/Eastern")) + heureRecept = datetime.now(tz=ZoneInfo(os.environ['ESCPOS_TIMEZONE'])) #TODO: make this configurable from the dockerfile. #NOTE: on set dans cups-files.conf le répertoire TempDir: #Extraire le répertoire temporaire de CUPS de cups-files.conf source_dir=PurePath('/var', 'spool', 'cups', 'tmp') @@ -1572,7 +1604,7 @@ def publish_receipt_from_CUPS(): # print(logfile_filename) log = open(PurePath('web','tmp', 'esc2html_log'), mode='a') source_log = open(source_dir.joinpath(logfile_filename), mode='rt') - log.write(f"CUPS print received at {datetime.now(tz=ZoneInfo('Canada/Eastern')).strftime('%Y%b%d %X.%f %Z')}\n") + log.write(f"CUPS print received at {datetime.now(tz=ZoneInfo(os.environ['ESCPOS_TIMEZONE'])).strftime('%Y%b%d %X.%f %Z')}\n") log.write(source_log.read()) log.close() source_log.close() @@ -1580,6 +1612,10 @@ def publish_receipt_from_CUPS(): #send an http acknowledgement return "OK" +## End printer UI + + + def launchPrintServer(printServ:ESCPOSServer): #Recevoir des connexions, une à la fois, pour l'éternité. Émule le protocle HP JetDirect diff --git a/extract_babel_messages.sh b/extract_babel_messages.sh new file mode 100755 index 0000000..4bef0bf --- /dev/null +++ b/extract_babel_messages.sh @@ -0,0 +1,4 @@ +#!/bin/bash +# This script creates messages.pot from the project files + +pybabel extract -F babel.cfg --copyright-holder="Francois-Leonard Gilbert" --project="escpos-netprinter" --msgid-bugs-address="github.com/gilbertfl/escpos-netprinter/issues" --version="3.2" -o translations/messages.pot . \ No newline at end of file diff --git a/init_babel_locale.sh b/init_babel_locale.sh new file mode 100755 index 0000000..4527a3d --- /dev/null +++ b/init_babel_locale.sh @@ -0,0 +1,11 @@ +#!/bin/bash +# This script creates a new locale for translation + +# Argument validation check +if [ "$#" -ne 1 ]; then + echo "This initializes a new locale for Flask-Babel" + echo "Usage: $0 " + exit 1 +fi + +pybabel init -i translations/messages.pot -d translations -l ${1} \ No newline at end of file diff --git a/templates/accueil.html.j2 b/templates/accueil.html.j2 index 4b85020..78973c3 100644 --- a/templates/accueil.html.j2 +++ b/templates/accueil.html.j2 @@ -1,22 +1,26 @@ - Imprimante virtuelle + {{ _("Imprimante virtuelle")}} +
-

État de l'imprimante:

+{% include 'lang_select.html.j2'%} + +

{{ _("État de l'imprimante:")}}

    -
  • En ligne
  • -
  • Adresse de cette imprimante: {{host}}
  • -
  • Ports d'impression: {{jetDirectPort}} (Jetdirect), 515 (lpd)
  • +
  • {{ _("En ligne")}}
  • +
  • {{ _("Adresse de cette imprimante:")}} {{host}}
  • +
  • {{ _("Ports d'impression:")}} {{jetDirectPort}} (Jetdirect), 515 (lpd)
  • {%if debug == 'True'%} -
  • Mode débogage activé
  • +
  • {{ _("Mode débogage activé")}}
  • {%endif%}
- Consulter la liste des reçus imprimés + {{ _("Consulter la liste des reçus imprimés")}}
-{% include 'footer.html' %} + +{% include 'footer.html.j2' %} \ No newline at end of file diff --git a/templates/footer.html b/templates/footer.html.j2 similarity index 57% rename from templates/footer.html rename to templates/footer.html.j2 index 7cdb201..249ea30 100644 --- a/templates/footer.html +++ b/templates/footer.html.j2 @@ -2,5 +2,6 @@ \ No newline at end of file diff --git a/templates/lang_select.html.j2 b/templates/lang_select.html.j2 new file mode 100644 index 0000000..7d994d7 --- /dev/null +++ b/templates/lang_select.html.j2 @@ -0,0 +1,12 @@ + + +
+{{ _("Changer de langue:")}} +{% for language in AVAILABLE_LANGUAGES %} + {% if CURRENT_LANGUAGE == language %} + {{ language }} + {% else %} + {{ language }} + {% endif %} +{% endfor %} +
\ No newline at end of file diff --git a/templates/receiptList.html.j2 b/templates/receiptList.html.j2 index 075c09a..3706df6 100644 --- a/templates/receiptList.html.j2 +++ b/templates/receiptList.html.j2 @@ -1,22 +1,22 @@ - Tous les reçus imprimés + {{ _("Tous les reçus imprimés")}}
{%if receiptlist|length > 0 %} -

{{receiptlist|length}} recu{%if receiptlist|length > 1%}s{%endif%} disponible{%if receiptlist|length > 1%}s{%endif%}

+

{{receiptlist|length}} {%if receiptlist|length > 1%}{{_("recus")}}{%else%}{{_("recu")}}{%endif%} {%if receiptlist|length > 1%}{{_("disponibles")}}{%else%}{{_("disponible")}}{%endif%}

{% else %} -

Aucun reçu trouvé!

+

{{ _("Aucun reçu trouvé!")}}

{% endif %} {# a comment #}
-{% include 'footer.html' %} +{% include 'footer.html.j2' %} \ No newline at end of file diff --git a/translations/en/LC_MESSAGES/messages.mo b/translations/en/LC_MESSAGES/messages.mo new file mode 100644 index 0000000000000000000000000000000000000000..621574b3ebfdd755a7775adc76aa6bf2e6fa7fff GIT binary patch literal 1268 zcmZvbO^+Kj7{^`I78($O1&Tma@}5dXowwaptJ!YVbT>s2NjFN;UN|{3&(0b-_Q>|E zXukmJ2k5OQgoIW-_g*PCq~3Z&oZvff zTQuH!uAgJJeg^&xeg*yuHo%Sd z82b>s0-gu2ffv9g7~{6V7x4X|-aiMYnCBw+1^6}i3D^c>-X3@n{1N;ToP$xvV=(Ie z1^gI%R@=|POR!(o`~!^VeGSI*{0T_3a1v*1WH)1 zOiRNZEq^b0aJD>Io^G=JV`*}7F+yWi!f)RiU+RF5T5_#C(q!aUfxgbTJoT(^u$voV zgLM&pqFg9RYhuGHu&UAba9+IodaPnzky)YEeAhgHvt!B3| zp#qY`$mc2(cgx(1nN4`E!f`oorgqWFm7bFeN4oXD1TE!#Nxm^YoQZu$GT=DkK%(=! z+wSg&PDiwF@b*@+z4Kig4;;mWPE^$M4!=&awY~BiN*~0`N#nH))+PMjgW-_h?~VHW zR)5qR4Da*~4krES^mbzyuMizhIGME}W+WH!3%@lS-qEUQU4FYU)}@nL+_P@s6K;yi z^n2Z1zFO{mYxw3K?_Aq$j0U6rS?4;e`@`T@^5 zzQru2q>8}hiBwu*A~xBSH12pc#IXbHu)v8|mz%8v;yANIqZP)sKizMQ52ve{mAdwI zY*Ht$d=F?|=6n(7x0>20cC9K7i^m^JzX@I!@@y^btyL{MPHU9!*NHsl7Lxx1HbG^a literal 0 HcmV?d00001 diff --git a/translations/en/LC_MESSAGES/messages.po b/translations/en/LC_MESSAGES/messages.po new file mode 100644 index 0000000..3cd80e6 --- /dev/null +++ b/translations/en/LC_MESSAGES/messages.po @@ -0,0 +1,85 @@ +# English translations for escpos-netprinter. +# Copyright (C) 2025 Francois-Leonard Gilbert +# This file is distributed under the same license as the escpos-netprinter +# project. +# FIRST AUTHOR , 2025. +# +msgid "" +msgstr "" +"Project-Id-Version: escpos-netprinter 3.2\n" +"Report-Msgid-Bugs-To: github.com/gilbertfl/escpos-netprinter/issues\n" +"POT-Creation-Date: 2025-11-07 03:45+0000\n" +"PO-Revision-Date: 2025-11-07 01:34+0000\n" +"Last-Translator: FULL NAME \n" +"Language: en\n" +"Language-Team: en \n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.17.0\n" + +#: templates/accueil.html.j2:4 +msgid "Imprimante virtuelle" +msgstr "ESC/POS virtual printer" + +#: templates/accueil.html.j2:10 +msgid "État de l'imprimante:" +msgstr "Printer state:" + +#: templates/accueil.html.j2:12 +msgid "En ligne" +msgstr "Online" + +#: templates/accueil.html.j2:13 +msgid "Adresse de cette imprimante:" +msgstr "Printer IP address:" + +#: templates/accueil.html.j2:14 +msgid "Ports d'impression:" +msgstr "Printer ports:" + +#: templates/accueil.html.j2:16 +msgid "Mode débogage activé" +msgstr "Debug mode activated" + +#: templates/accueil.html.j2:20 +msgid "Consulter la liste des reçus imprimés" +msgstr "Open printed receipt list" + +#: templates/footer.html.j2:5 +msgid "Accueil" +msgstr "Main" + +#: templates/footer.html.j2:5 +msgid "Liste reçus" +msgstr "Receipt list" + +#: templates/lang_select.html.j2:4 +msgid "Changer de langue:" +msgstr "Select language:" + +#: templates/receiptList.html.j2:4 +msgid "Tous les reçus imprimés" +msgstr "All printed receipts" + +#: templates/receiptList.html.j2:9 +msgid "recus" +msgstr "receipts" + +#: templates/receiptList.html.j2:9 +msgid "recu" +msgstr "receipt" + +#: templates/receiptList.html.j2:9 +msgid "disponibles" +msgstr "available" + +#: templates/receiptList.html.j2:9 +msgid "disponible" +msgstr "available" + +#: templates/receiptList.html.j2:16 +msgid "Aucun reçu trouvé!" +msgstr "No receipt available!" + diff --git a/translations/fr/LC_MESSAGES/messages.mo b/translations/fr/LC_MESSAGES/messages.mo new file mode 100644 index 0000000000000000000000000000000000000000..6ae5365c0f8d13db72ef24dff4af1f06d9a7d23b GIT binary patch literal 1318 zcmd6lO=}ZD7{^DcsH-4W5d?+tAhqb^tt~Cv7n-K21QQ#Q=zX(0*^W$R!n_pg!Mh*8 zlQ$8xc=fK3vnTJKyn64^|D?4Q><8$uyT5s6o|peTJ3ptV-V%)Kc<K74|pq7Wf^!0saDSg9l&)PM;^_3V0Ws0%ySs zU;zwq^WYcwJ^_P{;1qOR0)vkGV9*hRK}QP=Iy@NsUV}l$J22$?1O^>n!Jy*@7F{%30D{A=oqo>Yj zzdPQ)M`}BX%DD*<5;G6KnpW0JheerCDJ;@t+#Uwnqm1KyOX>={x#Fa8T!iil<2;v= zljbCFQq7WR$FztQg2lo&GqW$ zW~bimu0_r83(?`24$QH^dOR716}r-Fu1itSCSQx%(wjuGEp3K2rfM`X?P_J29!^&u zD7r?=v&&Jd(W;*`u3RWZxFL=!h}s*Wl;ba5aU>I=mT9_^7|Y$NcLTO`;upjZxM6jb u>P)CSrb~U{qQ_iugASqBdokTe`drdVp}dF^XYfkXdbd_=Z+Fkq)&2vQ=6$^Y literal 0 HcmV?d00001 diff --git a/translations/fr/LC_MESSAGES/messages.po b/translations/fr/LC_MESSAGES/messages.po new file mode 100644 index 0000000..01d05cc --- /dev/null +++ b/translations/fr/LC_MESSAGES/messages.po @@ -0,0 +1,85 @@ +# French translations for escpos-netprinter. +# Copyright (C) 2025 Francois-Leonard Gilbert +# This file is distributed under the same license as the escpos-netprinter +# project. +# FIRST AUTHOR , 2025. +# +msgid "" +msgstr "" +"Project-Id-Version: escpos-netprinter 3.2\n" +"Report-Msgid-Bugs-To: github.com/gilbertfl/escpos-netprinter/issues\n" +"POT-Creation-Date: 2025-11-07 03:45+0000\n" +"PO-Revision-Date: 2025-11-07 01:27+0000\n" +"Last-Translator: FULL NAME \n" +"Language: fr\n" +"Language-Team: fr \n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.17.0\n" + +#: templates/accueil.html.j2:4 +msgid "Imprimante virtuelle" +msgstr "Imprimante virtuelle ESC/POS" + +#: templates/accueil.html.j2:10 +msgid "État de l'imprimante:" +msgstr "État de l'imprimante:" + +#: templates/accueil.html.j2:12 +msgid "En ligne" +msgstr "En ligne" + +#: templates/accueil.html.j2:13 +msgid "Adresse de cette imprimante:" +msgstr "Adresse de cette imprimante:" + +#: templates/accueil.html.j2:14 +msgid "Ports d'impression:" +msgstr "Ports d'impression:" + +#: templates/accueil.html.j2:16 +msgid "Mode débogage activé" +msgstr "Mode débogage activé" + +#: templates/accueil.html.j2:20 +msgid "Consulter la liste des reçus imprimés" +msgstr "Consulter la liste des reçus imprimés" + +#: templates/footer.html.j2:5 +msgid "Accueil" +msgstr "Accueil" + +#: templates/footer.html.j2:5 +msgid "Liste reçus" +msgstr "Liste reçus" + +#: templates/lang_select.html.j2:4 +msgid "Changer de langue:" +msgstr "Changer de langue:" + +#: templates/receiptList.html.j2:4 +msgid "Tous les reçus imprimés" +msgstr "Tous les reçus imprimés" + +#: templates/receiptList.html.j2:9 +msgid "recus" +msgstr "recus" + +#: templates/receiptList.html.j2:9 +msgid "recu" +msgstr "recu" + +#: templates/receiptList.html.j2:9 +msgid "disponibles" +msgstr "disponibles" + +#: templates/receiptList.html.j2:9 +msgid "disponible" +msgstr "disponible" + +#: templates/receiptList.html.j2:16 +msgid "Aucun reçu trouvé!" +msgstr "Aucun reçu trouvé!" + diff --git a/translations/messages.pot b/translations/messages.pot new file mode 100644 index 0000000..2f33b89 --- /dev/null +++ b/translations/messages.pot @@ -0,0 +1,84 @@ +# Translations template for escpos-netprinter. +# Copyright (C) 2025 Francois-Leonard Gilbert +# This file is distributed under the same license as the escpos-netprinter +# project. +# FIRST AUTHOR , 2025. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: escpos-netprinter 3.2\n" +"Report-Msgid-Bugs-To: github.com/gilbertfl/escpos-netprinter/issues\n" +"POT-Creation-Date: 2025-11-07 03:45+0000\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.17.0\n" + +#: templates/accueil.html.j2:4 +msgid "Imprimante virtuelle" +msgstr "" + +#: templates/accueil.html.j2:10 +msgid "État de l'imprimante:" +msgstr "" + +#: templates/accueil.html.j2:12 +msgid "En ligne" +msgstr "" + +#: templates/accueil.html.j2:13 +msgid "Adresse de cette imprimante:" +msgstr "" + +#: templates/accueil.html.j2:14 +msgid "Ports d'impression:" +msgstr "" + +#: templates/accueil.html.j2:16 +msgid "Mode débogage activé" +msgstr "" + +#: templates/accueil.html.j2:20 +msgid "Consulter la liste des reçus imprimés" +msgstr "" + +#: templates/footer.html.j2:5 +msgid "Accueil" +msgstr "" + +#: templates/footer.html.j2:5 +msgid "Liste reçus" +msgstr "" + +#: templates/lang_select.html.j2:4 +msgid "Changer de langue:" +msgstr "" + +#: templates/receiptList.html.j2:4 +msgid "Tous les reçus imprimés" +msgstr "" + +#: templates/receiptList.html.j2:9 +msgid "recus" +msgstr "" + +#: templates/receiptList.html.j2:9 +msgid "recu" +msgstr "" + +#: templates/receiptList.html.j2:9 +msgid "disponibles" +msgstr "" + +#: templates/receiptList.html.j2:9 +msgid "disponible" +msgstr "" + +#: templates/receiptList.html.j2:16 +msgid "Aucun reçu trouvé!" +msgstr "" + diff --git a/update_babel_messages.sh b/update_babel_messages.sh new file mode 100755 index 0000000..b819939 --- /dev/null +++ b/update_babel_messages.sh @@ -0,0 +1,5 @@ +#!/bin/bash +# This script creates messages.pot from the project files, then updates the existing translations with any new texts + +pybabel extract -F babel.cfg --copyright-holder="Francois-Leonard Gilbert" --project="escpos-netprinter" --msgid-bugs-address="github.com/gilbertfl/escpos-netprinter/issues" --version="3.2" -o translations/messages.pot . +pybabel update -i translations/messages.pot -d translations \ No newline at end of file From a76cfdb39f5f2de2e95dadb4bbaecab2a56ba707 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois-L=C3=A9onard=20Gilbert?= <83510612+gilbertfl@users.noreply.github.com> Date: Thu, 6 Nov 2025 23:16:27 -0500 Subject: [PATCH 11/16] Update README.md Add the ESCPOS_TIMEZONE environment variable. --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index c2e347a..f89ee90 100644 --- a/README.md +++ b/README.md @@ -119,6 +119,7 @@ The following environment variables can be configured: | PRINTER_PORT | 9100 | JetDirect port for printer communication | | FLASK_RUN_DEBUG | false | Enable Flask debug mode | | FLASK_RUN_PORT | 80 | Sets the listening port for the web interface | +| ESCPOS_TIMEZONE | "America/Montreal" | Sets the timezone for all datetime formatting | ## Known issues While version 3.1.1 is no longer a beta version, it has known defects: From f9b36f7674949a9c0be9444b89c6510069a29de7 Mon Sep 17 00:00:00 2001 From: erikn69 Date: Fri, 7 Nov 2025 10:07:57 -0500 Subject: [PATCH 12/16] Handle valid barcode types explicitely --- esc2html.php | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/esc2html.php b/esc2html.php index aac0a84..7631d4a 100644 --- a/esc2html.php +++ b/esc2html.php @@ -175,7 +175,7 @@ } else if ($cmd -> isAvailableAs('SelectHriPrintPosCmd')) { $barcodeHri = $cmd -> getArg(); } else if ($cmd -> isAvailableAs('PrintBarcodeCmd')) { - $types = [ + $types = [ // download4.epson.biz/sec_pubs/pos/reference_en/escpos/gs_lk.html 0 => 'TypeUpcA', 65 => 'TypeUpcA', 1 => 'TypeUpcE', @@ -190,12 +190,20 @@ 71 => 'TypeCodabar', 72 => 'TypeCode93', 73 => 'TypeCode128', + 5 => 'NoTypePreview', + 70 => 'NoTypePreview', + 74 => 'NoTypePreview', + 75 => 'NoTypePreview', + 76 => 'NoTypePreview', + 77 => 'NoTypePreview', + 78 => 'NoTypePreview', + 79 => 'NoTypePreview', ]; $type = $types[$cmd->getType()] ?? null; $data = $cmd -> subCommand()->getData(); $classes = getBlockClasses($formatting); $classesStr = implode(" ", $classes); - if ($type){ + if ($type && $type !== 'NoTypePreview'){ $renderer = new \Picqer\Barcode\Renderers\PngRenderer(); $renderer->setBackgroundColor([255, 255, 255]); if (!class_exists(\Imagick::class)) { @@ -207,10 +215,13 @@ $barcode = (new $type)->getBarcode($data); $imgSrc = base64_encode($renderer->render($barcode, $barcodeWidth ?? $barcode->getWidth(), $barcodeHeight ?? 40)); $lineHtml = "\"{$data}\""; - - } else { + } else if ($type) { // NoTypePreview $classesStr .= ' esc-line-command'; $lineHtml = "BARCODE {$cmd->getType()} (NO PREVIEW)" . (in_array($barcodeHri, [1, 2, 3]) ? '' : " [$data]") . ""; + } else { // Missing type + $lineHtml = ""; // flush buffer + $barcodeWidth = $barcodeHeight = $barcodeHri = null; + continue; } if (in_array($barcodeHri, [1, 3])) { $lineHtml = "
$data
" . $lineHtml; From 1102a24ec91c2d56cea707a160a667e8d94e6bf6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois-L=C3=A9onard=20Gilbert?= <83510612+gilbertfl@users.noreply.github.com> Date: Fri, 7 Nov 2025 20:30:23 -0500 Subject: [PATCH 13/16] Update esc2html.php Cleanly separate known barcode types that can and cannot be rendered, and unknown or missing type. --- esc2html.php | 36 +++++++++++++++++++++--------------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/esc2html.php b/esc2html.php index 7631d4a..a25bc20 100644 --- a/esc2html.php +++ b/esc2html.php @@ -203,23 +203,29 @@ $data = $cmd -> subCommand()->getData(); $classes = getBlockClasses($formatting); $classesStr = implode(" ", $classes); - if ($type && $type !== 'NoTypePreview'){ - $renderer = new \Picqer\Barcode\Renderers\PngRenderer(); - $renderer->setBackgroundColor([255, 255, 255]); - if (!class_exists(\Imagick::class)) { - $renderer->useGd(); - } else { - $renderer->useImagick(); + if ($type){ //A valid barcode system type has been specified + if($type !== 'NoTypePreview'){ + $renderer = new \Picqer\Barcode\Renderers\PngRenderer(); + $renderer->setBackgroundColor([255, 255, 255]); + if (!class_exists(\Imagick::class)) { + $renderer->useGd(); + } + else { + $renderer->useImagick(); + } + $type = '\\Picqer\\Barcode\\Types\\' . $type; + $barcode = (new $type)->getBarcode($data); + $imgSrc = base64_encode($renderer->render($barcode, $barcodeWidth ?? $barcode->getWidth(), $barcodeHeight ?? 40)); + $lineHtml = "\"{$data}\""; + } + else { // A valid type that cannot be rendered by the library. + $classesStr .= ' esc-line-command'; + $lineHtml = "BARCODE {$cmd->getType()} (NO PREVIEW AVAILABLE)" . (in_array($barcodeHri, [1, 2, 3]) ? '' : " [$data]") . ""; } - $type = '\\Picqer\\Barcode\\Types\\' . $type; - $barcode = (new $type)->getBarcode($data); - $imgSrc = base64_encode($renderer->render($barcode, $barcodeWidth ?? $barcode->getWidth(), $barcodeHeight ?? 40)); - $lineHtml = "\"{$data}\""; - } else if ($type) { // NoTypePreview + } + else { // Missing or invalid barcode system type $classesStr .= ' esc-line-command'; - $lineHtml = "BARCODE {$cmd->getType()} (NO PREVIEW)" . (in_array($barcodeHri, [1, 2, 3]) ? '' : " [$data]") . ""; - } else { // Missing type - $lineHtml = ""; // flush buffer + $lineHtml = "BARCODE {$cmd->getType()} (UNKNOWN TYPE)" . (in_array($barcodeHri, [1, 2, 3]) ? '' : " [$data]") . ""; $barcodeWidth = $barcodeHeight = $barcodeHri = null; continue; } From da976da002380f7dbf51a65dd0751e483d1b581f Mon Sep 17 00:00:00 2001 From: Francois-Leonard Gilbert Date: Fri, 7 Nov 2025 21:41:19 -0500 Subject: [PATCH 14/16] Implementation of PR comments --- esc2html.php | 8 ++--- src/Parser/Command/CommandFiveArgs.php | 30 +++++++++++++++---- src/Parser/Command/CommandOneArg.php | 4 +-- src/Parser/Command/CommandThreeArgs.php | 12 ++++---- src/Parser/Command/CommandTwoArgs.php | 8 ++--- src/Parser/Command/Printout.php | 2 +- ...rintPosCmd.php => SelectBarCodeHriCmd.php} | 6 ++-- src/Parser/Command/SetBarcodeHeightCmd.php | 4 ++- src/Parser/Command/SetBarcodeWidthCmd.php | 4 ++- 9 files changed, 52 insertions(+), 26 deletions(-) rename src/Parser/Command/{SelectHriPrintPosCmd.php => SelectBarCodeHriCmd.php} (51%) diff --git a/esc2html.php b/esc2html.php index a25bc20..3b9c9c4 100644 --- a/esc2html.php +++ b/esc2html.php @@ -169,11 +169,11 @@ } } }if ($cmd -> isAvailableAs('SetBarcodeHeightCmd')) { - $barcodeHeight = $cmd -> getArg(); + $barcodeHeight = $cmd -> getHeight(); } else if ($cmd -> isAvailableAs('SetBarcodeWidthCmd')) { - $barcodeWidth = $cmd -> getArg(); - } else if ($cmd -> isAvailableAs('SelectHriPrintPosCmd')) { - $barcodeHri = $cmd -> getArg(); + $barcodeWidth = $cmd -> getWidth(); + } else if ($cmd -> isAvailableAs('SelectBarCodeHriCmd')) { + $barcodeHri = $cmd -> getHRI(); } else if ($cmd -> isAvailableAs('PrintBarcodeCmd')) { $types = [ // download4.epson.biz/sec_pubs/pos/reference_en/escpos/gs_lk.html 0 => 'TypeUpcA', diff --git a/src/Parser/Command/CommandFiveArgs.php b/src/Parser/Command/CommandFiveArgs.php index 8b0c54c..c45c8f4 100644 --- a/src/Parser/Command/CommandFiveArgs.php +++ b/src/Parser/Command/CommandFiveArgs.php @@ -5,11 +5,11 @@ class CommandFiveArgs extends EscposCommand { - private $arg1 = null; - private $arg2 = null; - private $arg3 = null; - private $arg4 = null; - private $arg5 = null; + private ?int $arg1 = null; + private ?int $arg2 = null; + private ?int $arg3 = null; + private ?int $arg4 = null; + private ?int $arg5 = null; public function addChar($char) { @@ -31,4 +31,24 @@ public function addChar($char) } return false; } + + protected function getArg1():?int{ + return $this->arg1; + } + + protected function getArg2():?int{ + return $this->arg2; + } + + protected function getArg3():?int{ + return $this->arg3; + } + + protected function getArg4():?int{ + return $this->arg4; + } + + protected function getArg5():?int{ + return $this->arg5; + } } diff --git a/src/Parser/Command/CommandOneArg.php b/src/Parser/Command/CommandOneArg.php index f3f1bb2..5683830 100644 --- a/src/Parser/Command/CommandOneArg.php +++ b/src/Parser/Command/CommandOneArg.php @@ -5,7 +5,7 @@ class CommandOneArg extends EscposCommand { - private $arg = null; + private ?int $arg = null; public function addChar($char) { @@ -17,7 +17,7 @@ public function addChar($char) } } - public function getArg() + protected function getArg(): ?int { return $this -> arg; } diff --git a/src/Parser/Command/CommandThreeArgs.php b/src/Parser/Command/CommandThreeArgs.php index 85ba730..47f22f2 100644 --- a/src/Parser/Command/CommandThreeArgs.php +++ b/src/Parser/Command/CommandThreeArgs.php @@ -5,9 +5,9 @@ class CommandThreeArgs extends EscposCommand { - private $arg1 = null; - private $arg2 = null; - private $arg3 = null; + private ?int $arg1 = null; + private ?int $arg2 = null; + private ?int $arg3 = null; public function addChar($char) { @@ -24,17 +24,17 @@ public function addChar($char) return false; } - public function getArg1() + protected function getArg1(): ?int { return $this -> arg1; } - public function getArg2() + protected function getArg2(): ?int { return $this -> arg2; } - public function getArg3() + protected function getArg3(): ?int { return $this -> arg3; } diff --git a/src/Parser/Command/CommandTwoArgs.php b/src/Parser/Command/CommandTwoArgs.php index 6016e31..8534cb7 100644 --- a/src/Parser/Command/CommandTwoArgs.php +++ b/src/Parser/Command/CommandTwoArgs.php @@ -5,8 +5,8 @@ class CommandTwoArgs extends EscposCommand { - private $arg1 = null; - private $arg2 = null; + private ?int $arg1 = null; + private ?int $arg2 = null; public function addChar($char) { @@ -20,12 +20,12 @@ public function addChar($char) return false; } - public function getArg1() + protected function getArg1(): ?int { return $this -> arg1; } - public function getArg2() + protected function getArg2(): ?int { return $this -> arg2; } diff --git a/src/Parser/Command/Printout.php b/src/Parser/Command/Printout.php index 030fe43..f656f00 100755 --- a/src/Parser/Command/Printout.php +++ b/src/Parser/Command/Printout.php @@ -80,7 +80,7 @@ class Printout extends Command 'I' => 'TransmitPrinterID', 'h' => 'SetBarcodeHeightCmd', 'w' => 'SetBarcodeWidthCmd', - 'H' => 'SelectHriPrintPosCmd', + 'H' => 'SelectBarCodeHriCmd', 'k' => 'PrintBarcodeCmd', 'v' => array( '0' => 'PrintRasterBitImageCmd' diff --git a/src/Parser/Command/SelectHriPrintPosCmd.php b/src/Parser/Command/SelectBarCodeHriCmd.php similarity index 51% rename from src/Parser/Command/SelectHriPrintPosCmd.php rename to src/Parser/Command/SelectBarCodeHriCmd.php index a4cadd8..3bbcc96 100644 --- a/src/Parser/Command/SelectHriPrintPosCmd.php +++ b/src/Parser/Command/SelectBarCodeHriCmd.php @@ -3,7 +3,9 @@ use ReceiptPrintHq\EscposTools\Parser\Command\CommandOneArg; -class SelectHriPrintPosCmd extends CommandOneArg +class SelectBarCodeHriCmd extends CommandOneArg { - + public function getHRI():?int{ + return $this->getArg(); + } } diff --git a/src/Parser/Command/SetBarcodeHeightCmd.php b/src/Parser/Command/SetBarcodeHeightCmd.php index 35d455a..b7cfb94 100644 --- a/src/Parser/Command/SetBarcodeHeightCmd.php +++ b/src/Parser/Command/SetBarcodeHeightCmd.php @@ -5,5 +5,7 @@ class SetBarcodeHeightCmd extends CommandOneArg { - + public function getHeight(): ?int{ + return $this->getArg(); + } } diff --git a/src/Parser/Command/SetBarcodeWidthCmd.php b/src/Parser/Command/SetBarcodeWidthCmd.php index a1c63cd..05bc479 100755 --- a/src/Parser/Command/SetBarcodeWidthCmd.php +++ b/src/Parser/Command/SetBarcodeWidthCmd.php @@ -5,5 +5,7 @@ class SetBarcodeWidthCmd extends CommandOneArg { - + public function getWidth():?int{ + return $this->getArg(); + } } From de0ce11b834d693e6e45fcc5f03f6ccaf55d5fd1 Mon Sep 17 00:00:00 2001 From: Francois-Leonard Gilbert Date: Tue, 25 Nov 2025 08:31:42 -0500 Subject: [PATCH 15/16] Update dockerignore to remove more fluff from the container. Update readme.md to make the 3.2 release. --- .dockerignore | 5 ++++- README.md | 28 ++++++++++++++-------------- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/.dockerignore b/.dockerignore index 9b4bae3..d69f7c6 100644 --- a/.dockerignore +++ b/.dockerignore @@ -29,9 +29,12 @@ web/tmp/** __pycache__/** __pycache__ -# Remove test scripts +# Remove test scripts and test files tests/** tests +barcodes.bin +receipt-with-logo.bin +receipt-with-qrcode.bin # Remove pybabel config and translation scripts from the runtime container babel.cfg diff --git a/README.md b/README.md index f89ee90..218f465 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ The printer emulates a 80mm roll of paper. ## Limits This docker image is not to be exposed on a public network (see [known issues](#known-issues)) -A print cannot last longer than 10 seconds. This timeout could be changed at some point, or made configurable. +A print cannot last longer than 10 seconds. This timeout could be changed in the code, or made configurable at some later point. ## Quick start @@ -28,26 +28,26 @@ docker run -d \ -p 80:80/tcp \ -p 9100:9100/tcp \ --mount source=receiptVolume,target=/home/escpos-emu/web \ - gilbertfl/escpos-netprinter:3.1 + gilbertfl/escpos-netprinter:3.2 ``` ### Once started Once started, the container will accept prints by JetDirect on the default port(9100) and by lpd on the default port(515). You can access all received receipts with the web application at port 80. The receipts are kept on a docker volume, so they will be kept if the container is restarted. To make the prints temporary, simply remove the `--mount` line from the run command. -Version 3.1 is capable of dealing with all status requests from POS systems as described in the Epson APG. +Version 3.2 is capable of dealing with all status requests from POS systems as described in the Epson APG. ## Working on the code ### Building from source -To install v3.1.1 from source: +To install v3.2 from source: ```bash -wget --show-progress https://github.com/gilbertfl/escpos-netprinter/archive/refs/tags/3.1.1.zip -unzip 3.1.1.zip -cd escpos-netprinter-3.1.1 -docker build -t escpos-netprinter:3.1.1 . +wget --show-progress https://github.com/gilbertfl/escpos-netprinter/archive/refs/tags/3.2.zip +unzip 3.2.zip +cd escpos-netprinter-3.2 +docker build -t escpos-netprinter:3.2 . ``` To run the resulting container: @@ -57,7 +57,7 @@ docker run -d \ -p 80:80/tcp \ -p 9100:9100/tcp \ --mount source=receiptVolume,target=/home/escpos-emu/web \ - escpos-netprinter:3.1.1 + escpos-netprinter:3.2 ``` ### Debugging @@ -71,13 +71,13 @@ docker run -d \ -p 9100:9100/tcp \ --mount source=receiptVolume,target=/home/escpos-emu/web \ --env ESCPOS_DEBUG=True \ - escpos-netprinter:3.1.1 + escpos-netprinter:3.2 ``` Setting this variable will generate logs from 3 different sources: - The CUPS printer driver - Jetdirect requests - The ESC/POS to HTML conversion itself -- Accesses to the web interface +- Browsing the web interface If you have problems with the CUPS interface, you can add port 631 to access the CUPS administrator interface. The CUPS administrative username is `cupsadmin` and the password is `123456`; you can change that in the dockerfile or at runtime inside the administrator interface. ```bash @@ -87,13 +87,13 @@ docker run -d \ -p 9100:9100/tcp \ -p 631:631/tcp --mount source=receiptVolume,target=/home/escpos-emu/web \ - escpos-netprinter:3.1.1 + escpos-netprinter:3.2 ``` ### Runtime Directory Structure The following directories inside the container are useful: -- `/home/escpos-emu/web/`: Stores all the printed receipts and other control info +- `/home/escpos-emu/web/`: Stores all the printed receipts and printer state (including logs) - `/home/escpos-emu/web/receipts`: Stores the HTML receipts - `/home/escpos-emu/web/tmp`: Stores temporary files during processing (for debugging only) - `/home/escpos-emu/web/receipt_list.csv`: Created at runtime, this file contains the list of the printed receipts with the file location. @@ -122,7 +122,7 @@ The following environment variables can be configured: | ESCPOS_TIMEZONE | "America/Montreal" | Sets the timezone for all datetime formatting | ## Known issues -While version 3.1.1 is no longer a beta version, it has known defects: +While version 3.2 is ready for production, it has known defects: - It still uses the Flask development server, so it is unsafe for public networks. - While it works with simple drivers, for example the one for the MUNBYN ITPP047 printers, the [Epson utilities](https://download.epson-biz.com/modules/pos/) refuse to speak to it. From 4df4a66f62e679e1e28bcbce98936ad99280a26d Mon Sep 17 00:00:00 2001 From: Francois-Leonard Gilbert Date: Tue, 25 Nov 2025 10:19:00 -0500 Subject: [PATCH 16/16] Restore the addition of documentation for esc2html --- doc/esc2html.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/doc/esc2html.md b/doc/esc2html.md index 3dd09aa..6390eab 100644 --- a/doc/esc2html.md +++ b/doc/esc2html.md @@ -14,12 +14,21 @@ This utility is included with escpos-tools. See the ## Usage +Basic usage: ``` php esc2html FILE ``` +Optional debugging: +``` +php esc2html --debug FILE +``` +In the debug mode, the debugging messages are sent to the debug console while the output is the same as in basic mode. + + ## Example +### Basic example ``` $ php esc2html.php receipt-with-logo.bin > receipt-with-logo.html ``` @@ -38,6 +47,15 @@ $ cat receipt-with-logo.bin > /dev/usb/lp0 The input file used as an example here was generated by [escpos-php](https://github.com/mike42/escpos-php), and is available [here](https://raw.githubusercontent.com/receipt-print-hq/escpos-tools/master/receipt-with-logo.bin). +### Saving debugging output separately from the real output + +``` +$ php esc2html.php receipt-with-qrcode.bin 1>test.html 2>log.txt +``` + +The HTML representation of the receipt is saved to test.html, and the debugging output to log.txt. + + ## Further conversions This utility will create a formatted HTML file. This can be converted accurately to PDF