<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Azeem Ullah</title>
    <description>The latest articles on DEV Community by Azeem Ullah (@azeem_ullah).</description>
    <link>https://dev.to/azeem_ullah</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2517951%2Ff95da6a1-c619-48ad-b259-41ef7d64098d.png</url>
      <title>DEV Community: Azeem Ullah</title>
      <link>https://dev.to/azeem_ullah</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/azeem_ullah"/>
    <language>en</language>
    <item>
      <title>Printing a receipt with a logo and Georgian text from a .NET Windows service, without System.Drawing</title>
      <dc:creator>Azeem Ullah</dc:creator>
      <pubDate>Tue, 20 Oct 2026 14:40:31 +0000</pubDate>
      <link>https://dev.to/azeem_ullah/printing-a-receipt-with-a-logo-and-georgian-text-from-a-net-windows-service-without-systemdrawing-3b5h</link>
      <guid>https://dev.to/azeem_ullah/printing-a-receipt-with-a-logo-and-georgian-text-from-a-net-windows-service-without-systemdrawing-3b5h</guid>
      <description>&lt;p&gt;A question on Stack Overflow this year describes a setup I see a lot: &lt;a href="https://stackoverflow.com/questions/79927490" rel="noopener noreferrer"&gt;an ASP.NET Core backend for kiosks&lt;/a&gt;, each kiosk a Windows PC with Chrome and a USB-only thermal printer (a NIPPON PRIMEX NP-3511). Sharing the printer over the network to the server kept dropping, so the author moved printing into a small service on each kiosk. Then printing itself broke: the receipts were built with &lt;code&gt;System.Drawing&lt;/code&gt; and &lt;code&gt;PrintDocument&lt;/code&gt;, which Microsoft doesn't support in Windows services, and SkiaSharp didn't seem to have a way to print at all.&lt;/p&gt;

&lt;p&gt;The receipts need a logo, formatted text, and Georgian.&lt;/p&gt;

&lt;p&gt;I don't have an NP-3511. I do have a different 80 mm USB receipt printer, .NET 9 and the NP-3511's specification sheet, so I built the part that can be checked without the printer. Here is what I found, and what I'd do.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stop printing through a graphics API
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;PrintDocument&lt;/code&gt; asks Windows to draw your page through the printer driver with GDI+. That is the bit Microsoft's &lt;a href="https://learn.microsoft.com/en-us/dotnet/api/system.drawing" rel="noopener noreferrer"&gt;System.Drawing remarks&lt;/a&gt; warn about: the GDI+ types such as &lt;code&gt;Bitmap&lt;/code&gt; and &lt;code&gt;Font&lt;/code&gt; are not supported in Windows services or ASP.NET apps.&lt;/p&gt;

&lt;p&gt;A receipt printer doesn't need a page drawn for it, though. It takes a stream of bytes: commands, text, and bitmap rows, one bit per dot. So the work splits into three steps, and none of them touches GDI+:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;draw the receipt into an in-memory bitmap (SkiaSharp is fine for this, you just don't print with it),&lt;/li&gt;
&lt;li&gt;turn the bitmap into the printer's raster command,&lt;/li&gt;
&lt;li&gt;hand those bytes to the Windows spooler with the &lt;code&gt;RAW&lt;/code&gt; datatype, so the driver passes them through unchanged.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Georgian means printing an image
&lt;/h2&gt;

&lt;p&gt;The NP-3511's spec sheet lists its character tables: Japanese, code page 858, and Windows code pages 1250 to 1254 (Central European, Cyrillic, Western, Greek, Turkish). None of them contains Georgian. Sending Georgian as text can't work, whatever encoding you pick.&lt;/p&gt;

&lt;p&gt;So the whole receipt goes out as a picture. Draw the text with a font that has Georgian letters. &lt;code&gt;Sylfaen&lt;/code&gt; ships with Windows; if you don't want to depend on that, bundle &lt;a href="https://fonts.google.com/noto/specimen/Noto+Sans+Georgian" rel="noopener noreferrer"&gt;Noto Sans Georgian&lt;/a&gt; with the service. In my test SkiaSharp drew Mkhedruli and the lari sign (₾) correctly with plain &lt;code&gt;DrawText&lt;/code&gt;, without any extra shaping library.&lt;/p&gt;

&lt;h2&gt;
  
  
  The printer-specific part: NP-3511 doesn't use GS v 0
&lt;/h2&gt;

&lt;p&gt;Most "print an image on a receipt printer" code you'll find sends Epson's raster command &lt;code&gt;GS v 0&lt;/code&gt; (&lt;code&gt;1D 76 30&lt;/code&gt;). The NP-3511 command table in the &lt;a href="https://www.ricelake.com/media/k1rhkdc5/np-2511_3511_product_specifications.pdf" rel="noopener noreferrer"&gt;NP-2511/3511 specification&lt;/a&gt; (hosted by Rice Lake, a distributor) has no &lt;code&gt;GS v 0&lt;/code&gt;. It has its own raster command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ESC b n1 n2 n3 data      1B 62 n1 n2 n3 ...
n1 = bytes per row (1..72 on the 3-inch model)
height = n2 + 256 * n3 dot rows
bit 1 = black
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;72 bytes is 576 dots, so that's the width to draw at. The same table uses &lt;code&gt;1D 76 00&lt;/code&gt; (&lt;code&gt;GS v NUL&lt;/code&gt;) to switch on automatic status reporting, so an Epson &lt;code&gt;1D 76 30&lt;/code&gt; header lands on a different command. The cut is &lt;code&gt;ESC i&lt;/code&gt; (full) or &lt;code&gt;ESC m&lt;/code&gt; (partial), not Epson's &lt;code&gt;GS V&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That document says "Ver. 0.07" and calls itself provisional, so check it against the manual for your firmware. If you do have one of these printers, the first test is a tiny &lt;code&gt;ESC b&lt;/code&gt; image, before anything else.&lt;/p&gt;

&lt;h2&gt;
  
  
  The code
&lt;/h2&gt;

&lt;p&gt;Tested on .NET 9 with SkiaSharp 4.153.1 (&lt;code&gt;dotnet add package SkiaSharp&lt;/code&gt;). This is a console app so it's easy to try; the three classes drop into a Worker Service unchanged.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Runtime.InteropServices&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;SkiaSharp&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;WidthDots&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;576&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// NP-3511: ESC b allows at most 72 bytes = 576 dots per row&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;typeface&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SKTypeface&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;@"C:\Windows\Fonts\sylfaen.ttf"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// has Georgian&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;receipt&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Receipt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;WidthDots&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;typeface&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;logoPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ყავის სახლი"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;40f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"თბილისი, რუსთაველის გამზირი 12"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;22f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ესპრესო x2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"7.00 ₾"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;24f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ხაჭაპური"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"12.50 ₾"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;24f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"სულ"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"19.50 ₾"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;28f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"გმადლობთ!"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;24f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;]);&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bytesPerRow&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Raster&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Pack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;receipt&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;np3511&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[..&lt;/span&gt; &lt;span class="n"&gt;Raster&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EscB&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bytesPerRow&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="m"&gt;0x1B&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0x64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0x1B&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0x69&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt; &lt;span class="c1"&gt;// feed 4 lines, full cut (ESC i)&lt;/span&gt;
&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;escpos&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;0x1B&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0x40&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;..&lt;/span&gt; &lt;span class="n"&gt;Raster&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GsV0&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bytesPerRow&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="m"&gt;0x1B&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0x64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0x1D&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0x56&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="n"&gt;RawPrinter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"NP-3511"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;np3511&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// the queue name as it appears in Windows&lt;/span&gt;

&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Receipt&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;SKBitmap&lt;/span&gt; &lt;span class="nf"&gt;Render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SKTypeface&lt;/span&gt; &lt;span class="n"&gt;tf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;logoPath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;left&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;right&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;bold&lt;/span&gt;&lt;span class="p"&gt;)[]&lt;/span&gt; &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;bmp&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;SKBitmap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;SKCanvas&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bmp&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Clear&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SKColors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;White&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;paint&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;SKPaint&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;Color&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SKColors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Black&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;IsAntialias&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
        &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;margin&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;logoPath&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;logo&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SKImage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromEncodedData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;logoPath&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;logo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Height&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;width&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;margin&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;/&lt;/span&gt; &lt;span class="n"&gt;logo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Width&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="kt"&gt;float&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;logo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Width&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="p"&gt;/&lt;/span&gt; &lt;span class="n"&gt;logo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Height&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;DrawImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;logo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SKRect&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;width&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;/&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;SKSamplingOptions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SKFilterMode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Linear&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
            &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;left&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;right&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bold&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;font&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;SKFont&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;Embolden&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bold&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Edging&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SKFontEdging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Alias&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
            &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="m"&gt;1.3f&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;right&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;DrawText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;left&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;width&lt;/span&gt; &lt;span class="p"&gt;/&lt;/span&gt; &lt;span class="m"&gt;2f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SKTextAlign&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Center&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;font&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;paint&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;else&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;DrawText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;left&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;margin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SKTextAlign&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Left&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;font&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;paint&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;DrawText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;right&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;width&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;margin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SKTextAlign&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Right&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;font&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;paint&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;cropped&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;SKBitmap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="m"&gt;12&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="n"&gt;bmp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ExtractSubset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cropped&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SKRectI&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="m"&gt;12&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;cropped&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Copy&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Raster&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// One bit per dot, rows top to bottom, 1 = black.&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;bytesPerRow&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;Pack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SKBitmap&lt;/span&gt; &lt;span class="n"&gt;bmp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;bpr&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bmp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Width&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="m"&gt;7&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;/&lt;/span&gt; &lt;span class="m"&gt;8&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;bpr&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;bmp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Height&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;bmp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Height&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;++)&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;bmp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Width&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;++)&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bmp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetPixel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Alpha&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;127&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Red&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="m"&gt;0.299&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Green&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="m"&gt;0.587&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Blue&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="m"&gt;0.114&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="m"&gt;128&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;bpr&lt;/span&gt; &lt;span class="p"&gt;+&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;/&lt;/span&gt; &lt;span class="m"&gt;8&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;|=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="m"&gt;0x80&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="p"&gt;%&lt;/span&gt; &lt;span class="m"&gt;8&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bpr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bmp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Height&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// NIPPON PRIMEX NP-2511/3511: ESC b n1 n2 n3 data (n1 = bytes per row, height = n2 + 256*n3).&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="nf"&gt;EscB&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;bpr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;Bands&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bpr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;0x1B&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0x62&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;bpr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&lt;/span&gt; &lt;span class="m"&gt;0xFF&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)]);&lt;/span&gt;

    &lt;span class="c1"&gt;// Epson-style ESC/POS: GS v 0 m xL xH yL yH data.&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="nf"&gt;GsV0&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;bpr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;Bands&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bpr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;0x1D&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0x76&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0x30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;bpr&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&lt;/span&gt; &lt;span class="m"&gt;0xFF&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;bpr&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;8&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&lt;/span&gt; &lt;span class="m"&gt;0xFF&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)]);&lt;/span&gt;

    &lt;span class="c1"&gt;// Send tall receipts in bands so the printer's receive buffer never has to hold the whole image.&lt;/span&gt;
    &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="nf"&gt;Bands&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;bpr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Func&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;]&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;header&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;band&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;256&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="p"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;band&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;band&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;height&lt;/span&gt; &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddRange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
            &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddRange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AsSpan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;bpr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="p"&gt;*&lt;/span&gt; &lt;span class="n"&gt;bpr&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;ToArray&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[..&lt;/span&gt; &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RawPrinter&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;StructLayout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LayoutKind&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sequential&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CharSet&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;CharSet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Unicode&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DOC_INFO_1&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;pDocName&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;pOutputFile&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;pDatatype&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;DllImport&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"winspool.drv"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CharSet&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;CharSet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Unicode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SetLastError&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;extern&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="nf"&gt;OpenPrinter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="n"&gt;IntPtr&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;IntPtr&lt;/span&gt; &lt;span class="n"&gt;defaults&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;DllImport&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"winspool.drv"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SetLastError&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;extern&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="nf"&gt;ClosePrinter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IntPtr&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;DllImport&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"winspool.drv"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CharSet&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;CharSet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Unicode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SetLastError&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;extern&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;StartDocPrinter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IntPtr&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;level&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DOC_INFO_1&lt;/span&gt; &lt;span class="n"&gt;di&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;DllImport&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"winspool.drv"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SetLastError&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;extern&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="nf"&gt;EndDocPrinter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IntPtr&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;DllImport&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"winspool.drv"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SetLastError&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;extern&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="nf"&gt;StartPagePrinter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IntPtr&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;DllImport&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"winspool.drv"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SetLastError&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;extern&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="nf"&gt;EndPagePrinter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IntPtr&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;DllImport&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"winspool.drv"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SetLastError&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;extern&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="nf"&gt;WritePrinter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IntPtr&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;written&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;printerName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;byte&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="nf"&gt;OpenPrinter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;printerName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;IntPtr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Zero&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;System&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ComponentModel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Win32Exception&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="c1"&gt;// "RAW" tells the spooler to pass the bytes through without the driver rendering anything.&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;StartDocPrinter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;DOC_INFO_1&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;pDocName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Receipt"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pDatatype&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"RAW"&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;System&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ComponentModel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Win32Exception&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
            &lt;span class="nf"&gt;StartPagePrinter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;WritePrinter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;written&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nf"&gt;EndPagePrinter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="nf"&gt;EndDocPrinter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(!&lt;/span&gt;&lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="p"&gt;||&lt;/span&gt; &lt;span class="n"&gt;written&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;System&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ComponentModel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Win32Exception&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nf"&gt;ClosePrinter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;GetPixel&lt;/code&gt; in a loop is slow for big images, but a receipt is a few hundred rows; it runs in milliseconds and keeps the code obvious.&lt;/p&gt;

&lt;p&gt;Two details worth knowing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Logos.&lt;/strong&gt; The printer has no grey. My first test logo had a dark green background, and after the threshold it came out as a solid black rectangle with white text. Use a black-on-white version of the logo, or dither it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bands.&lt;/strong&gt; I send the image in strips of 256 rows. The spec allows one &lt;code&gt;ESC b&lt;/code&gt; block up to 65,535 rows tall. Strips just keep each command small; I haven't measured whether it changes anything on this printer.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Windows service or not?
&lt;/h2&gt;

&lt;p&gt;A service is a reasonable home for this: it runs at boot with nobody signed in, which is what a kiosk wants. The open question is how a receipt gets to it. There are two options.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The kiosk page calls the service on &lt;code&gt;http://127.0.0.1&lt;/code&gt;.&lt;/strong&gt; This is simple, but Chrome 142 and later shows a permission prompt before a public website may talk to a program on the same machine. Until someone clicks Allow, the requests just hang. On a kiosk nobody is there to click, so pre-allow your site with the Chrome policy &lt;code&gt;LocalNetworkAccessAllowedForUrls&lt;/code&gt;. I measured how this looks to the page &lt;a href="https://dev.to/azeem_ullah/agent-not-running-when-it-is-running-chromes-local-network-prompt-and-localhost-helpers-4nll"&gt;in an earlier post&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The service connects out to your backend and receives jobs.&lt;/strong&gt; That can be a WebSocket or SignalR connection, or a poll every second or two. The browser is no longer involved, a page reload can't lose a receipt, and the server already knows what it wants printed. Give each job an ID and have the service report "printed" or "failed" back, so a reconnect doesn't print the same receipt twice.&lt;/p&gt;

&lt;p&gt;Since the backend already builds the receipts, I would go with the second.&lt;/p&gt;

&lt;p&gt;Two smaller service details:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The service's account has to see the printer queue by its exact name. &lt;code&gt;OpenPrinter&lt;/code&gt; fails with error 1801 (&lt;code&gt;ERROR_INVALID_PRINTER_NAME&lt;/code&gt;) if it doesn't, so log &lt;code&gt;Win32Exception.NativeErrorCode&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The spec sheet lists drivers only for Windows 2000, XP and CE 5.0. If nothing newer exists, the &lt;code&gt;RAW&lt;/code&gt; path doesn't need the driver to understand anything. It only needs a queue on the right USB port. Windows' "Generic / Text Only" driver is the first thing I'd try. I haven't tried it with this printer.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I actually tested
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tested:&lt;/strong&gt; the code above builds and runs on .NET 9. The Georgian receipt renders correctly. I decoded the generated &lt;code&gt;ESC b&lt;/code&gt; bytes back into an image and got the receipt back pixel for pixel, with a 4-line feed and &lt;code&gt;ESC i&lt;/code&gt; at the end. The same program queued the &lt;code&gt;GS v 0&lt;/code&gt; version on my own printer (a BIXOLON SRP-352plusIII) through &lt;code&gt;RawPrinter.Send&lt;/code&gt;. &lt;code&gt;OpenPrinter&lt;/code&gt;, &lt;code&gt;StartDocPrinter&lt;/code&gt; and &lt;code&gt;WritePrinter&lt;/code&gt; all succeeded.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not tested:&lt;/strong&gt; any of this on an NP-3511, and running it inside an actual Windows service rather than a console app. I also have no paper result yet: my printer was switched off when the job went in.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you have an NP-3511 and try the &lt;code&gt;ESC b&lt;/code&gt; path, I'd like to hear what came out.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Disclosure:&lt;/em&gt; I sell &lt;a href="https://mianazeemdaula.github.io/thermalink/" rel="noopener noreferrer"&gt;Thermalink&lt;/a&gt;, a local print agent for web apps that does this bitmap rendering for you. When I first published this post it only sent Epson's &lt;code&gt;GS v 0&lt;/code&gt;, so it could not drive an NP-3511. Version 1.0.2, added the same day, has a &lt;code&gt;"commands": "nippon"&lt;/code&gt; printer setting that sends &lt;code&gt;ESC b&lt;/code&gt; and &lt;code&gt;ESC i&lt;/code&gt; / &lt;code&gt;ESC m&lt;/code&gt; as described above. It's written from the same spec sheet and covered by automated tests, but it hasn't been tried on a real NP-3511 yet. Everything in this post is plain .NET and doesn't need it.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>csharp</category>
      <category>printing</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>I tested a CSV cleaner on data it should leave alone. It failed 4 of 23 checks.</title>
      <dc:creator>Azeem Ullah</dc:creator>
      <pubDate>Sat, 17 Oct 2026 12:05:16 +0000</pubDate>
      <link>https://dev.to/azeem_ullah/i-tested-a-csv-cleaner-on-data-it-should-leave-alone-it-failed-4-of-23-checks-5a37</link>
      <guid>https://dev.to/azeem_ullah/i-tested-a-csv-cleaner-on-data-it-should-leave-alone-it-failed-4-of-23-checks-5a37</guid>
      <description>&lt;p&gt;A CSV cleaner landed on my desk this week: an MIT-licensed, browser-only tool that repairs broken quotes, fixes garbled characters and masks emails and phone numbers. It came with 50 tests, and all 50 passed.&lt;/p&gt;

&lt;p&gt;Every one of those tests fed it broken data and checked that it got fixed. None of them fed it good data and checked that it stayed the same. So I wrote 23 of those.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why I cared
&lt;/h3&gt;

&lt;p&gt;The broken-quote problem is real and common. Shopify's own community forum has several threads titled some version of "Illegal quoting in line 2", usually caused by an inch mark or a quote inside a product title or description: &lt;a href="https://community.shopify.com/t/illegal-quoting-in-line-2-and-missing-headers-title/10418" rel="noopener noreferrer"&gt;https://community.shopify.com/t/illegal-quoting-in-line-2-and-missing-headers-title/10418&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A tool that fixes that is useful. A tool that fixes that and quietly changes other cells is worse than no tool, because nobody checks a 5,000-row file by eye.&lt;/p&gt;

&lt;h3&gt;
  
  
  What failed
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;1. Two identical rows became one.&lt;/strong&gt; Duplicate removal was switched on by default. That sounds tidy until you remember that two identical order lines (same order, same item, same quantity) are real data.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csvs"&gt;&lt;code&gt;&lt;span class="k"&gt;order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;item&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;qty&lt;/span&gt;
&lt;span class="mf"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;Pen&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;1&lt;/span&gt;
&lt;span class="mf"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;Pen&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output: one row. The fix was to make it opt-in.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. "SÃO PAULO" became "SÁO PAULO".&lt;/strong&gt; The mojibake fixer was a table of find-and-replace pairs: &lt;code&gt;Ã©&lt;/code&gt; to &lt;code&gt;é&lt;/code&gt;, &lt;code&gt;â€™&lt;/code&gt; to &lt;code&gt;'&lt;/code&gt;, and so on. One entry was just &lt;code&gt;Ã&lt;/code&gt; to &lt;code&gt;Á&lt;/code&gt; (the real pair is &lt;code&gt;Ã&lt;/code&gt; plus an invisible control character, which had probably been lost), so every &lt;code&gt;Ã&lt;/code&gt; in correct Portuguese text got changed.&lt;/p&gt;

&lt;p&gt;The fix was to stop guessing with a table. Mojibake is UTF-8 bytes read as Windows-1252, so you can reverse it exactly: take a suspicious run of characters, turn each one back into its Windows-1252 byte, and decode those bytes as UTF-8 with a strict decoder. If they are not valid UTF-8, the text was never mojibake, and you leave it alone.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;decoder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;TextDecoder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf-8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;fatal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;fixRun&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;run&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;bytes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;toCp1252Bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;run&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;      &lt;span class="c1"&gt;// null if a character has no Windows-1252 byte&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;run&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;decoder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;run&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;                  &lt;span class="c1"&gt;// not valid UTF-8: it was real text&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;3. Curly apostrophes came back straight.&lt;/strong&gt; The same table mapped &lt;code&gt;â€™&lt;/code&gt; to a plain &lt;code&gt;'&lt;/code&gt;. The original character was &lt;code&gt;’&lt;/code&gt;. The round trip above gives back exactly what was there.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. An ISBN got masked as a phone number.&lt;/strong&gt; With masking on, &lt;code&gt;9780306406157&lt;/code&gt; became &lt;code&gt;(***) ***-6157&lt;/code&gt;. The phone pattern matched any long run of digits. Order numbers and account numbers had the same risk. Now it only masks numbers written like phone numbers (a &lt;code&gt;+&lt;/code&gt;, brackets, or groups separated by spaces or dashes), and card masking checks the card checksum.&lt;/p&gt;

&lt;h3&gt;
  
  
  The other thing the tests didn't cover
&lt;/h3&gt;

&lt;p&gt;The tool had six landing pages, each with a little demo box. They didn't use the engine at all. Each one had its own short script, and its line-split regex was written as &lt;code&gt;/\\r?\\n/&lt;/code&gt; with doubled backslashes, so it matched a literal backslash followed by &lt;code&gt;n&lt;/code&gt; and never split anything. The tests only covered the engine, so nobody noticed. All six now use the same engine as the main page.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I took from it
&lt;/h3&gt;

&lt;p&gt;For any tool that changes data, half the tests should be inputs it must not touch: valid escaped quotes, quoted commas, line breaks inside fields, correct accented text, real duplicate rows, numbers that only look like phone numbers. Those are the bugs users never report, because they never notice.&lt;/p&gt;

&lt;p&gt;The fixed version is here, free, and it runs in your browser: &lt;a href="https://mianazeemdaula.github.io/csv-shield/" rel="noopener noreferrer"&gt;https://mianazeemdaula.github.io/csv-shield/&lt;/a&gt;&lt;br&gt;
Source: &lt;a href="https://github.com/mianazeemdaula/csv-shield" rel="noopener noreferrer"&gt;https://github.com/mianazeemdaula/csv-shield&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Tested in Node 22 and headless Chrome 155. Not tested in Firefox, Safari or on large files yet.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>testing</category>
      <category>opensource</category>
    </item>
    <item>
      <title>WebUSB says "Access denied" to your receipt printer on Windows. Here is who has it.</title>
      <dc:creator>Azeem Ullah</dc:creator>
      <pubDate>Sun, 11 Oct 2026 11:42:35 +0000</pubDate>
      <link>https://dev.to/azeem_ullah/webusb-says-access-denied-to-your-receipt-printer-on-windows-here-is-who-has-it-5hl2</link>
      <guid>https://dev.to/azeem_ullah/webusb-says-access-denied-to-your-receipt-printer-on-windows-here-is-who-has-it-5hl2</guid>
      <description>&lt;p&gt;If you have tried to print to a USB receipt or label printer straight from the browser on Windows, you have probably met this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DOMException: Failed to execute 'open' on 'USBDevice': Access denied.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It is not a permission you forgot to grant. The user picked the device in Chrome's chooser, and it still fails.&lt;/p&gt;

&lt;p&gt;People keep getting stuck on this. A developer with a Zebra ZD621 &lt;a href="https://developer.zebra.com/content/domexception-failed-execute-open-usbdevice-access-denied" rel="noopener noreferrer"&gt;posted it on Zebra's own forum&lt;/a&gt;: fine on macOS, "Access denied" on Windows 10 and 11, reinstalling the driver did nothing, and nobody from Zebra answered. Back in 2021 someone raised the same thing with a label printer on &lt;a href="https://github.com/WICG/webusb/issues/199" rel="noopener noreferrer"&gt;the WebUSB spec repo&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;So I looked at my own machine to see what is actually holding the printer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who has the printer
&lt;/h2&gt;

&lt;p&gt;My receipt printer is a BIXOLON SRP-352plusIII on USB. This asks Windows which driver service owns its USB device:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;Get-PnpDevice&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Where-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FriendlyName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-match&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'BIXOLON'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="n"&gt;Select-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;FriendlyName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;@{&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'Service'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{(&lt;/span&gt;&lt;span class="n"&gt;Get-PnpDeviceProperty&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-InstanceId&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;InstanceId&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;-KeyName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;DEVPKEY_Device_Service&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ErrorAction&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;SilentlyContinue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;}},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nx"&gt;InstanceId&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Format&lt;/span&gt;&lt;span class="err"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;List&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The line that matters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Class        : USB
FriendlyName : BIXOLON  SRP-352plusIII
Service      : usbprint
InstanceId   : USB\VID_1504&amp;amp;PID_003F\00000001
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;usbprint&lt;/code&gt; is &lt;code&gt;usbprint.sys&lt;/code&gt;, Microsoft's USB printing driver. It is what gives you the &lt;code&gt;USB004&lt;/code&gt; port and lets the Windows spooler talk to the printer. It also owns the printer's USB interface, and Windows gives an interface to one driver at a time.&lt;/p&gt;

&lt;p&gt;Chrome's WebUSB on Windows can only open an interface that is bound to WinUSB. So &lt;code&gt;open()&lt;/code&gt; is refused. That is the whole error.&lt;/p&gt;

&lt;p&gt;The WebUSB editor said as much on that 2021 issue: "I don't think there is a good solution for this problem, particularly on Windows."&lt;/p&gt;

&lt;h2&gt;
  
  
  What you can do about it
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Swap the driver with Zadig.&lt;/strong&gt; This is the fix people in both threads ended up with: replace &lt;code&gt;usbprint&lt;/code&gt; with WinUSB for that device, and WebUSB can open it. The cost is that the printer stops being a Windows printer. Nothing else can print to it until you put the old driver back, and every customer machine needs the same manual step. The Zebra poster rejected it for exactly that reason. I have not done this to my own printer, because I still need it to print normally.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Use a different door.&lt;/strong&gt; If the printer has a serial or virtual COM mode, Web Serial may work where WebUSB does not. Network printers are no help here: a web page cannot open a raw TCP socket to port 9100.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Leave the driver alone and go through the spooler.&lt;/strong&gt; The spooler will accept raw bytes for a print queue and pass them to the printer untouched. A web page cannot call the spooler, so this needs a small program on the PC that listens on localhost and hands the bytes over. QZ Tray works this way. So does the one I wrote, &lt;a href="https://7207381545467.gumroad.com/l/xwyogp" rel="noopener noreferrer"&gt;Thermalink&lt;/a&gt;, which is how the BIXOLON above prints from my web app with its normal driver still installed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which one
&lt;/h2&gt;

&lt;p&gt;If you control the machine and the printer is used by one web app only, Zadig is free and it works. Write down how to undo it.&lt;/p&gt;

&lt;p&gt;If the printer also has to work for anything else on that PC, or you are shipping to customers who will not run a driver tool, option 3 is the one that survives.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I did not test
&lt;/h2&gt;

&lt;p&gt;I did not reproduce the &lt;code&gt;open()&lt;/code&gt; failure on my own printer; the error text and the Zadig result come from the two threads linked above. What I checked myself is the driver binding shown here, and that raw printing through the spooler works on this printer. Everything in this post is about Windows. Both threads say macOS works.&lt;/p&gt;

&lt;p&gt;Thermalink has a site now, with the docs and a free &lt;a href="https://mianazeemdaula.github.io/thermalink/cheatsheet/" rel="noopener noreferrer"&gt;ESC/POS, ZPL and TSPL cheat sheet&lt;/a&gt;: &lt;a href="https://mianazeemdaula.github.io/thermalink/" rel="noopener noreferrer"&gt;https://mianazeemdaula.github.io/thermalink/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>usb</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>My form wrote a sentence that wasn't true, and the user would have signed it</title>
      <dc:creator>Azeem Ullah</dc:creator>
      <pubDate>Sun, 11 Oct 2026 11:28:55 +0000</pubDate>
      <link>https://dev.to/azeem_ullah/my-form-wrote-a-sentence-that-wasnt-true-and-the-user-would-have-signed-it-3a6b</link>
      <guid>https://dev.to/azeem_ullah/my-form-wrote-a-sentence-that-wasnt-true-and-the-user-would-have-signed-it-3a6b</guid>
      <description>&lt;p&gt;I built a small page that writes appeal letters for UK private parking tickets. You answer a few questions, it produces a letter.&lt;/p&gt;

&lt;p&gt;I tested it the way I test most forms. Fill in good data, check the output looks right. Every combination passed: three parts of the UK, keeper or driver, five grounds. No script errors.&lt;/p&gt;

&lt;p&gt;Then I did the thing I should have done first. I picked a ground and typed in data that did not support it.&lt;/p&gt;

&lt;h3&gt;
  
  
  The ground
&lt;/h3&gt;

&lt;p&gt;One of the five grounds is about a deadline. In England and Wales, if a private parking company posts you a ticket and nothing was left on the windscreen, the notice has to be delivered to the registered keeper within 14 days. It's in Schedule 4 of the Protection of Freedoms Act 2012, paragraph 9. People ask about it on forums all the time. Here is one thread where the notice took more than 30 days: &lt;a href="https://www.ftla.uk/private-parking-tickets/late-notice-to-keeper-30-days-worth-appealing/" rel="noopener noreferrer"&gt;https://www.ftla.uk/private-parking-tickets/late-notice-to-keeper-30-days-worth-appealing/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So the form asks for the date of parking and the date the notice arrived.&lt;/p&gt;

&lt;h3&gt;
  
  
  The bug
&lt;/h3&gt;

&lt;p&gt;I entered a parking date and a received date 7 days apart. Under the field, the helper text did the right thing:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You received it 7 days after the parking date. This ground needs more than 14 days, so it may not apply.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And the letter, a few centimetres to the right, said this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Your notice is dated 6 October 2026 and I received it on 6 October 2026, which is outside that period.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That sentence is false, and the page wrote it. The person using it is stressed, skims the preview, prints, signs.&lt;/p&gt;

&lt;p&gt;The cause was boring. The helper text and the letter were built by two different functions. One compared the dates. The other just filled a template.&lt;/p&gt;

&lt;h3&gt;
  
  
  The fix
&lt;/h3&gt;

&lt;p&gt;I did not make the letter clever. A letter that silently rewrites its own argument is hard to trust. The page now puts a warning above the letter, next to the count of unfilled fields:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;$&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;parkDate&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;$&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;receivedDate&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;radio&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ground&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;keeper&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nf"&gt;$&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;jurisdiction&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;EW&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
    &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;pd&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;rd&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nf"&gt;daysBetween&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;pd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;lt;strong&amp;gt;Check your dates.&amp;lt;/strong&amp;gt; This letter says the notice &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;arrived more than 14 days after parking, but the dates you entered are &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;within 14 days. Correct the dates or choose a different ground.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The second one
&lt;/h3&gt;

&lt;p&gt;While I was in there I found the letter's date came from this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;toISOString()&lt;/code&gt; is UTC. The UK is an hour ahead of UTC in summer. Between midnight and 1 am the letter would be dated yesterday. Not a disaster, but it is a dated legal letter, and the fix is three lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;localIso&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getFullYear&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getMonth&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;padStart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getDate&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nf"&gt;padStart&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  What I took from it
&lt;/h3&gt;

&lt;p&gt;If a form generates text that a person will sign, test each option with inputs that contradict it. "Does it render" is not the test. "Can it say something untrue" is.&lt;/p&gt;

&lt;p&gt;The page is here if you want to poke at it: &lt;a href="https://mianazeemdaula.github.io/parking-appeal-letter/" rel="noopener noreferrer"&gt;https://mianazeemdaula.github.io/parking-appeal-letter/&lt;/a&gt;&lt;br&gt;
Source: &lt;a href="https://github.com/mianazeemdaula/parking-appeal-letter" rel="noopener noreferrer"&gt;https://github.com/mianazeemdaula/parking-appeal-letter&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I'm a developer, not a lawyer, and no solicitor has reviewed the wording. It was tested in headless Chrome only, not on real phones, Safari or Firefox.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>testing</category>
      <category>showdev</category>
    </item>
    <item>
      <title>The receipt CSS everyone copies, @page { size: 80mm auto }, does nothing in Chrome</title>
      <dc:creator>Azeem Ullah</dc:creator>
      <pubDate>Sat, 10 Oct 2026 11:21:19 +0000</pubDate>
      <link>https://dev.to/azeem_ullah/the-receipt-css-everyone-copies-page-size-80mm-auto-does-nothing-in-chrome-19o0</link>
      <guid>https://dev.to/azeem_ullah/the-receipt-css-everyone-copies-page-size-80mm-auto-does-nothing-in-chrome-19o0</guid>
      <description>&lt;p&gt;If you print receipts with &lt;code&gt;window.print()&lt;/code&gt;, you have probably pasted this into your stylesheet at some point:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="k"&gt;@page&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;80mm&lt;/span&gt; &lt;span class="nb"&gt;auto&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;margin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's everywhere. One example: on the WordPress.org support forum, a WooCommerce invoice plugin's support team &lt;a href="https://wordpress.org/support/?p=18861607" rel="noopener noreferrer"&gt;gave exactly this rule&lt;/a&gt; to a user setting up an 80 mm thermal printer. It reads well. 80 mm wide, as tall as the content. That's what a receipt is.&lt;/p&gt;

&lt;p&gt;I wanted to know what Chrome does with it, so I measured.&lt;/p&gt;

&lt;h2&gt;
  
  
  The test
&lt;/h2&gt;

&lt;p&gt;One small receipt: a shop name, two lines and a total. I printed it to PDF with headless Chrome 155 on Windows, changing only the &lt;code&gt;@page&lt;/code&gt; rule, then read the page size out of each PDF.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;
&lt;code&gt;@page&lt;/code&gt; rule&lt;/th&gt;
&lt;th&gt;Page Chrome produced&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;215.9 × 279.4 mm (Letter)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;size: 80mm auto&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;215.9 × 279.4 mm (Letter)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;size: 80mm&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;80 × 80 mm&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;size: 80mm 297mm&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;80 × 297 mm&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;height measured in JS&lt;/td&gt;
&lt;td&gt;80 × 27 mm&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Row two is the same as row one. The rule did nothing.&lt;/p&gt;

&lt;p&gt;I didn't trust that, so I asked the browser directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;CSSStyleSheet&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replaceSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@page { size: 80mm auto; margin: 0 }&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cssRules&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getPropertyValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;size&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// ""&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Empty string. Chrome's parser drops the declaration. &lt;code&gt;auto&lt;/code&gt; is valid on its own, and &lt;code&gt;80mm 297mm&lt;/code&gt; is valid, but a length followed by &lt;code&gt;auto&lt;/code&gt; isn't something &lt;code&gt;size&lt;/code&gt; accepts. So the browser falls back to the default page, and your receipt sits in the corner of a Letter or A4 sheet. I think that explains the two classic complaints, a tiny shrunken receipt and a long tail of blank paper, though which one you get will depend on the driver.&lt;/p&gt;

&lt;p&gt;Row three caught me out too. &lt;code&gt;size: 80mm&lt;/code&gt; with one value doesn't mean "80 wide, any height". It means a square.&lt;/p&gt;

&lt;h2&gt;
  
  
  What works: measure, then tell Chrome
&lt;/h2&gt;

&lt;p&gt;CSS has no way to say "page as tall as the content". But you know how tall the receipt is, because it's already laid out on screen. Measure it and write the number in.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;printReceipt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getBoundingClientRect&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;25.4&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;96&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// CSS px to mm&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;style&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;receipt-page&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;style&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;style&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;receipt-page&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;head&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appendChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`@page { size: 80mm &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ceil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;mm&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;mm; margin: 0 }`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's row five in the table: an 80 × 27 mm page for a 25 mm receipt, with nothing left over.&lt;/p&gt;

&lt;p&gt;It matters more as the receipt gets longer. I ran the same thing with long orders:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Receipt&lt;/th&gt;
&lt;th&gt;&lt;code&gt;size: 80mm 297mm&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;measured height&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;120 lines&lt;/td&gt;
&lt;td&gt;2 pages&lt;/td&gt;
&lt;td&gt;1 page, 532 mm&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;600 lines&lt;/td&gt;
&lt;td&gt;9 pages&lt;/td&gt;
&lt;td&gt;1 page, 2588 mm&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A fixed 297 mm page cuts a long receipt into pieces. On a printer with an auto-cutter that can mean an actual cut through the middle of an order. With the measured height it stays one page, even at two and a half metres.&lt;/p&gt;

&lt;p&gt;Two things to watch with the measuring:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Measure after fonts and images have loaded. A logo that arrives late makes the receipt taller than the page you just declared.&lt;/li&gt;
&lt;li&gt;Measure the element at the width it will print at. If your print stylesheet changes the width, the text wraps differently and the height is wrong.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Your 80 mm printer is 72 mm wide
&lt;/h2&gt;

&lt;p&gt;The other complaint I keep seeing is "the right side of my receipt is cut off". Payment and POS vendors have whole help pages about it, and they usually tell staff to play with margins and scale in the print dialog.&lt;/p&gt;

&lt;p&gt;I looked at what the drivers on my own machine report. I have a BIXOLON SRP-352plusIII and a generic "POS-80" driver installed. This is what Windows says their paper sizes are:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Paper name in the driver&lt;/th&gt;
&lt;th&gt;Actual size&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;80 x 297 mm&lt;/td&gt;
&lt;td&gt;71.9 × 296.9 mm&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;80 x 3276 mm&lt;/td&gt;
&lt;td&gt;71.9 × 3275.8 mm&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Thermal Paper (80 x 210)&lt;/td&gt;
&lt;td&gt;72.1 × 210.1 mm&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every "80" is 72. The roll is 80 mm wide, but the print head only covers about 72 mm of it, and that's the size the driver hands to the browser.&lt;/p&gt;

&lt;p&gt;So if your receipt container is &lt;code&gt;width: 80mm&lt;/code&gt;, it's 8 mm wider than the page. Something has to give, and the right edge is usually what goes. Make the container 72 mm so it fits the page the driver actually offers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.receipt&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;72mm&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;58 mm printers have the same gap. Check what your driver really reports before you pick a number. In PowerShell:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;Add-Type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-AssemblyName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;System.Drawing&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$p&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;New-Object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;System.Drawing.Printing.PrinterSettings&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PrinterName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'YOUR PRINTER NAME'&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PaperSizes&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'{0}: {1:N1} x {2:N1} mm'&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-f&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PaperName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Width&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.254&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Height&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.254&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What I didn't test
&lt;/h2&gt;

&lt;p&gt;All the page sizes above come from Chrome's PDF output, which is the only place I can measure a page exactly. Since publishing I've printed one measured receipt from Chrome to my BIXOLON and it came out fine, but I haven't put a ruler on paper for each variant in the table. A real print goes through the driver, and I can't promise every driver accepts a custom page height the same way. If yours ignores it, selecting the long roll size in the driver (the 3276 mm one above) is the next thing I'd try, but I haven't measured that.&lt;/p&gt;

&lt;p&gt;I also only tested Chrome. Firefox and Safari have their own history with &lt;code&gt;@page size&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you'd rather not write this yourself
&lt;/h2&gt;

&lt;p&gt;I packaged the stylesheet, the measuring script and 8 receipt templates (each for 80 mm and 58 mm rolls) as the &lt;a href="https://7207381545467.gumroad.com/l/receipt-print-kit" rel="noopener noreferrer"&gt;Receipt Print Kit&lt;/a&gt;. It's $9, or $5 for the first 20 buyers through &lt;a href="https://7207381545467.gumroad.com/l/receipt-print-kit/first20" rel="noopener noreferrer"&gt;this link&lt;/a&gt; (code FIRST20). &lt;em&gt;Disclosure: I built it.&lt;/em&gt; Everything in this post works without it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where &lt;code&gt;window.print()&lt;/code&gt; stops
&lt;/h2&gt;

&lt;p&gt;Even with the page size right, the browser still shows a dialog, and it can't cut the paper or open the cash drawer. Those need raw commands sent to the printer. I wrote about that route &lt;a href="https://dev.to/azeem_ullah/printing-receipts-silently-from-a-web-app-what-actually-works-in-2026-cm5"&gt;here&lt;/a&gt;, and I sell a small agent that does it called &lt;a href="https://7207381545467.gumroad.com/l/xwyogp" rel="noopener noreferrer"&gt;Thermalink&lt;/a&gt;. &lt;em&gt;Disclosure: I built it.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;For a lot of shops, though, a correct page size and a 72 mm container are all that was missing. If you try the measuring trick on a real printer and it behaves differently from my table, tell me the model and what came out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Free:&lt;/strong&gt; the measuring function on its own, MIT licensed: &lt;a href="https://github.com/mianazeemdaula/receipt-fit" rel="noopener noreferrer"&gt;receipt-fit on GitHub&lt;/a&gt;. And if you end up sending raw bytes instead, here's the &lt;a href="https://mianazeemdaula.github.io/thermalink/cheatsheet/" rel="noopener noreferrer"&gt;ESC/POS, ZPL and TSPL cheat sheet&lt;/a&gt; I use.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>css</category>
      <category>javascript</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>"Agent not running" when it is running: Chrome's local network prompt and localhost helpers</title>
      <dc:creator>Azeem Ullah</dc:creator>
      <pubDate>Sat, 10 Oct 2026 11:14:14 +0000</pubDate>
      <link>https://dev.to/azeem_ullah/agent-not-running-when-it-is-running-chromes-local-network-prompt-and-localhost-helpers-4nll</link>
      <guid>https://dev.to/azeem_ullah/agent-not-running-when-it-is-running-chromes-local-network-prompt-and-localhost-helpers-4nll</guid>
      <description>&lt;p&gt;This week I found a bug in my own code by reading another company's support page.&lt;/p&gt;

&lt;p&gt;Dynamsoft sell a document scanning SDK. Like a lot of hardware-in-the-browser tools, it works through a small service installed on the user's PC, and the web page talks to it on &lt;code&gt;127.0.0.1&lt;/code&gt;. Their &lt;a href="https://www.dynamsoft.com/web-twain/docs/faq/chromium-142-local-network-access-issue.html" rel="noopener noreferrer"&gt;FAQ&lt;/a&gt; lists this symptom for Chromium 142 and later: the page keeps asking the user to download and install the service, even though it is already installed and running.&lt;/p&gt;

&lt;p&gt;I build the same kind of thing for receipt printers, so I read that and thought: mine probably does this too. It did.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changed in the browser
&lt;/h2&gt;

&lt;p&gt;Since Chrome 142 (28 October 2025), a public website can't just &lt;code&gt;fetch()&lt;/code&gt; something on the user's machine or LAN. Chrome shows a permission prompt first. The Chrome team's &lt;a href="https://developer.chrome.com/blog/local-network-access" rel="noopener noreferrer"&gt;post about it&lt;/a&gt; calls this Local Network Access.&lt;/p&gt;

&lt;p&gt;A few things have moved since then. These come from Chrome's own &lt;a href="https://github.com/GoogleChrome/modern-web-guidance-src/issues/1127" rel="noopener noreferrer"&gt;guidance tracker&lt;/a&gt; and the Dynamsoft page:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Chrome 145&lt;/strong&gt; split the one permission into two. &lt;code&gt;loopback-network&lt;/code&gt; is for programs on the same computer, and in site settings it shows as "Apps on device". &lt;code&gt;local-network&lt;/code&gt; is for other devices on the LAN. The old name &lt;code&gt;local-network-access&lt;/code&gt; still works as an alias.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chrome 147&lt;/strong&gt; put WebSocket and WebTransport connections behind the same prompt. If your helper speaks WebSocket and you thought you'd escaped, you haven't.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chrome 156&lt;/strong&gt; is planned to remove &lt;code&gt;LocalNetworkAccessRestrictionsTemporaryOptOut&lt;/code&gt;, the enterprise policy some IT teams used to switch the whole thing off. If a customer told you "we fixed it with a policy", ask them which one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Firefox 153&lt;/strong&gt; now does the same on desktop, with its own prompt.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So this is no longer a Chrome quirk you can wait out.&lt;/p&gt;

&lt;h2&gt;
  
  
  What your page sees
&lt;/h2&gt;

&lt;p&gt;This is the part I couldn't find written down, so I tested it. Chrome 155 on Windows, my agent listening on &lt;code&gt;127.0.0.1:17777&lt;/code&gt;, and a page on a public &lt;code&gt;https://&lt;/code&gt; origin that had never asked before.&lt;/p&gt;

&lt;p&gt;First, the permission state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;loopback-network&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;local-network&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;local-network-access&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;permissions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;})).&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;// loopback-network prompt&lt;/span&gt;
&lt;span class="c1"&gt;// local-network prompt&lt;/span&gt;
&lt;span class="c1"&gt;// local-network-access prompt&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All three names are accepted, and all say &lt;code&gt;prompt&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Then the request, with a 3 second timeout on an &lt;code&gt;AbortController&lt;/code&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Target&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;127.0.0.1:17777&lt;/code&gt; (agent running)&lt;/td&gt;
&lt;td&gt;hangs, then my own timeout aborts it after 3 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;127.0.0.1:17778&lt;/code&gt; (nothing listening)&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;TypeError: Failed to fetch&lt;/code&gt; after about 10 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two things I didn't expect.&lt;/p&gt;

&lt;p&gt;Chrome only asks when something is actually there. A dead port fails straight away, with no prompt.&lt;/p&gt;

&lt;p&gt;And while the prompt is open, the request doesn't fail. It just waits. There's no error and no event. The promise sits there until the user clicks something.&lt;/p&gt;

&lt;p&gt;Now look at how nearly every "is the helper installed?" check is written, mine included:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="nf"&gt;isRunning&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_req&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;GET&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/v1/health&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1500&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 1.5 s timeout&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;!!&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The prompt appears. The cashier is looking at the till, not at the address bar. 1.5 seconds pass, the check gives up, and the app says "Printer helper is not running, please install it". The helper is running. The browser is waiting for a click nobody knows they need to make.&lt;/p&gt;

&lt;p&gt;If the user clicks Block instead, it gets worse. From what I've read, Chrome doesn't ask that site again, and every request fails from then on. Dynamsoft's page shows the console message for that case: &lt;code&gt;Permission was denied for this request to access the unknown address space.&lt;/code&gt; Your JavaScript can't read that text. It gets the same &lt;code&gt;TypeError&lt;/code&gt; as a missing agent.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: ask the browser before you blame the agent
&lt;/h2&gt;

&lt;p&gt;You can't trigger the prompt on demand and you can't clear a block from script. What you can do is tell the three situations apart and show the right message.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;agentStatus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://127.0.0.1:17777/v1/health&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ctrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;AbortController&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;timer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;ctrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abort&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="mi"&gt;1500&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ctrl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;signal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;targetAddressSpace&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;loopback&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;running&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;not_running&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;perm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;lnaState&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;perm&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;denied&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;permission_denied&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="c1"&gt;// an unanswered prompt leaves the request hanging until our own abort&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;perm&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;prompt&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;AbortError&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;permission_prompt&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;not_running&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;finally&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;clearTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;lnaState&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;permissions&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unsupported&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;loopback-network&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;local-network-access&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="c1"&gt;// 145+, then 142-144&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;navigator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;permissions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;})).&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unsupported&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Safari, older browsers&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then say something useful for each one:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;permission_prompt&lt;/code&gt;: "Click Allow in the prompt next to the address bar, then print again."&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;permission_denied&lt;/code&gt;: "Your browser is blocking the printer helper. Click the icon to the left of the address bar, open Site settings, set &lt;strong&gt;Apps on device&lt;/strong&gt; to Allow, and reload." (Chrome 142 to 144 call it "Local network access".)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;not_running&lt;/code&gt;: only now do you show the install link.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code&gt;AbortError&lt;/code&gt; check matters. A page whose permission is still &lt;code&gt;prompt&lt;/code&gt; but whose agent really is missing gets the fast &lt;code&gt;TypeError&lt;/code&gt;, and that should still say "not running".&lt;/p&gt;

&lt;h2&gt;
  
  
  Other things that bit, or will
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Iframes.&lt;/strong&gt; If your app is embedded cross-origin, the outer page has to hand the permission down: &lt;code&gt;&amp;lt;iframe allow="loopback-network; local-network-access" ...&amp;gt;&lt;/code&gt;. Same-origin frames are fine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HTTPS only.&lt;/strong&gt; The Chrome post says the prompt can only be requested from a secure context. A public page on plain &lt;code&gt;http://&lt;/code&gt; has no way to get the permission.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dev machines hide it.&lt;/strong&gt; A page you open from &lt;code&gt;http://localhost:3000&lt;/code&gt; isn't a public site, so you never see the prompt while developing. It first shows up in production, on someone else's computer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Managed fleets.&lt;/strong&gt; &lt;code&gt;LocalNetworkAccessAllowedForUrls&lt;/code&gt; pre-allows your origin so staff never see the prompt. That's the policy to ask IT for, not the opt-out that's going away.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A correction to my earlier post.&lt;/strong&gt; I wrote that you need to answer the preflight with &lt;code&gt;Access-Control-Allow-Private-Network: true&lt;/code&gt;. That header belongs to the older Private Network Access design, which this permission replaced. I still send it and it does no harm, but it isn't what lets you through any more.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I haven't tested
&lt;/h2&gt;

&lt;p&gt;The "blocked" branch. I ran it against a mocked permission state, and the behaviour I describe for it comes from Dynamsoft's documentation, not from me clicking Block on a real machine. I also haven't run any of this on Firefox 153 or on a Mac. If you see something different there, tell me and I'll update the post.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this landed in my own product
&lt;/h2&gt;

&lt;p&gt;I make &lt;a href="https://7207381545467.gumroad.com/l/xwyogp" rel="noopener noreferrer"&gt;Thermalink&lt;/a&gt;, a paid local print agent and JS SDK for receipt and label printers. &lt;em&gt;Disclosure: I built it.&lt;/em&gt; Version 1.0.1 has this fix: &lt;code&gt;tl.status()&lt;/code&gt; returns those four states, and print calls throw &lt;code&gt;permission_prompt&lt;/code&gt; or &lt;code&gt;permission_denied&lt;/code&gt; instead of a misleading &lt;code&gt;not_running&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;You don't need it to use anything above. The function is about twenty lines, and it works for any localhost helper: printing, scanning, card readers, scales.&lt;/p&gt;

&lt;p&gt;If you maintain one and you've seen different timing from what's in my table, I'd like to compare notes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;More:&lt;/strong&gt; Thermalink's public &lt;a href="https://mianazeemdaula.github.io/thermalink/docs/troubleshooting/" rel="noopener noreferrer"&gt;troubleshooting page&lt;/a&gt; covers this prompt and the other ways a local print agent looks dead when it isn't, and there's a free &lt;a href="https://mianazeemdaula.github.io/thermalink/cheatsheet/" rel="noopener noreferrer"&gt;ESC/POS, ZPL and TSPL cheat sheet&lt;/a&gt; on the same site.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>browser</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Printing shipping and product labels from a web app (ZPL and TSPL, no PDF)</title>
      <dc:creator>Azeem Ullah</dc:creator>
      <pubDate>Fri, 09 Oct 2026 09:41:36 +0000</pubDate>
      <link>https://dev.to/azeem_ullah/printing-shipping-and-product-labels-from-a-web-app-zpl-and-tspl-no-pdf-4mmc</link>
      <guid>https://dev.to/azeem_ullah/printing-shipping-and-product-labels-from-a-web-app-zpl-and-tspl-no-pdf-4mmc</guid>
      <description>&lt;p&gt;If your web app ships parcels, prices products or tracks stock, sooner or later someone asks: &lt;em&gt;"Can it print the label straight to the Zebra?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The usual first attempt is to generate a PDF and call &lt;code&gt;window.print()&lt;/code&gt;. That works badly. The label comes out scaled, shifted or split across two labels, and every PC needs its own driver settings. Label printers have their own languages, and sending those directly is simpler and much more reliable.&lt;/p&gt;

&lt;p&gt;This post covers the two languages you'll meet most (ZPL and TSPL), the units and sizes that trip people up, and how to get the bytes from a browser to the printer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two languages cover most printers
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Language&lt;/th&gt;
&lt;th&gt;Printers&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ZPL&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Zebra, and most printers sold as "Zebra-compatible"&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;TSPL&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;TSC, 4BARCODE, many Xprinter and generic 4" label printers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;EPL&lt;/td&gt;
&lt;td&gt;older Zebra/Eltron models (avoid for new work)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Both are plain text. You build a string, send it to the printer as-is (raw), and the printer draws the label itself. No driver rendering, no PDF, no scaling.&lt;/p&gt;

&lt;h2&gt;
  
  
  Units: everything is in dots
&lt;/h2&gt;

&lt;p&gt;This is where most first labels go wrong. Coordinates are in &lt;strong&gt;printer dots&lt;/strong&gt;, not millimetres or pixels:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;203 dpi&lt;/strong&gt; printers (the common ones): 8 dots per mm&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;300 dpi&lt;/strong&gt; printers: about 11.8 dots per mm&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So a 4 × 6 inch shipping label is 812 × 1218 dots at 203 dpi, and 1200 × 1800 at 300 dpi. If your label prints at two-thirds size, you wrote it for 203 dpi and sent it to a 300 dpi printer (or the other way round). Keep the dpi in your printer settings and multiply.&lt;/p&gt;

&lt;h2&gt;
  
  
  ZPL: a shipping label
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;^XA
^PW812
^LL1218
^CI28
^CF0,40
^FO40,40^FDShip to: Jane Doe^FS
^CF0,30
^FO40,90^FD221B Baker Street, London^FS
^FO40,150^BY3^BCN,120,Y,N,N^FD1Z999AA10123456784^FS
^FO40,320^BQN,2,6^FDQA,https://example.com/track/1Z999AA10123456784^FS
^XZ
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What the commands do:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;^XA&lt;/code&gt; … &lt;code&gt;^XZ&lt;/code&gt;: start and end of a label&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;^PW812&lt;/code&gt; / &lt;code&gt;^LL1218&lt;/code&gt;: print width and label length in dots (4 × 6" at 203 dpi)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;^CI28&lt;/code&gt;: treat field data as UTF-8, so &lt;code&gt;é&lt;/code&gt;, &lt;code&gt;ü&lt;/code&gt; and &lt;code&gt;£&lt;/code&gt; work with the built-in scalable font&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;^CF0,40&lt;/code&gt;: default font 0 (scalable), 40 dots high&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;^FOx,y&lt;/code&gt;: field origin (top-left of the next element)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;^FD…^FS&lt;/code&gt;: field data, then field separator&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;^BY3^BCN,120,Y,N,N&lt;/code&gt;: Code 128 barcode, module width 3, 120 dots high, human-readable text underneath&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;^BQN,2,6&lt;/code&gt; + &lt;code&gt;^FDQA,…&lt;/code&gt;: QR code, magnification 6. The &lt;code&gt;QA,&lt;/code&gt; prefix means error-correction level Q and automatic input mode. It isn't part of your data.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Tip:&lt;/strong&gt; paste ZPL into the free &lt;a href="https://labelary.com/viewer.html" rel="noopener noreferrer"&gt;Labelary viewer&lt;/a&gt; to see the label before you print a single one. It saves a lot of paper.&lt;/p&gt;

&lt;h2&gt;
  
  
  TSPL: a product label
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SIZE 50 mm, 30 mm
GAP 2 mm, 0 mm
DIRECTION 1
CLS
TEXT 20,20,"3",0,1,1,"Organic Honey 500g"
TEXT 20,60,"2",0,1,1,"Price: 9.99"
BARCODE 20,100,"128",70,1,0,2,2,"SKU-000123"
PRINT 1,1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;SIZE&lt;/code&gt; / &lt;code&gt;GAP&lt;/code&gt;: label size and the gap between labels. TSPL lets you use mm here, but &lt;code&gt;TEXT&lt;/code&gt; and &lt;code&gt;BARCODE&lt;/code&gt; positions are still in dots.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;CLS&lt;/code&gt;: clear the image buffer. Forget it and you'll print the previous label on top of the new one.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;TEXT x,y,"font",rotation,x-mul,y-mul,"content"&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;BARCODE x,y,"128",height,readable,rotation,narrow,wide,"content"&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;QRCODE x,y,M,4,A,0,"https://…"&lt;/code&gt; adds a QR code (ECC level M, cell width 4).&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;PRINT 1,1&lt;/code&gt;: print one copy of one set&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;End each line with &lt;code&gt;\r\n&lt;/code&gt;. Many printers accept &lt;code&gt;\n&lt;/code&gt;, but some older firmware doesn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common gotchas
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Wrong gap/label length = blank labels or a label skipped every time.&lt;/strong&gt; Calibrate the media sensor once. On Zebra, hold the feed button or send &lt;code&gt;~JC&lt;/code&gt;. On TSC, use the printer's utility or the &lt;code&gt;AUTODETECT&lt;/code&gt; command. Then make sure &lt;code&gt;^LL&lt;/code&gt; / &lt;code&gt;SIZE&lt;/code&gt; match the real stock.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Built-in fonts are limited.&lt;/strong&gt; ZPL's font 0 plus &lt;code&gt;^CI28&lt;/code&gt; handles most Latin text. TSPL's bitmap fonts are often ASCII-only. For Arabic, Cyrillic or CJK, either install a font on the printer or &lt;strong&gt;draw the text as an image&lt;/strong&gt; in the browser (a &lt;code&gt;&amp;lt;canvas&amp;gt;&lt;/code&gt;), then send it with ZPL &lt;code&gt;^GFA&lt;/code&gt; or TSPL &lt;code&gt;BITMAP&lt;/code&gt;. Treat the image as 1-bit and threshold it, because label printers don't do greys.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't send ZPL through a "normal" print job.&lt;/strong&gt; On Windows, a job submitted through the driver with datatype &lt;code&gt;RAW&lt;/code&gt; (&lt;code&gt;OpenPrinter&lt;/code&gt; → &lt;code&gt;StartDocPrinter&lt;/code&gt; with &lt;code&gt;"RAW"&lt;/code&gt; → &lt;code&gt;WritePrinter&lt;/code&gt;) passes your ZPL through untouched. Printing a text file through the driver instead prints the &lt;em&gt;ZPL source code&lt;/em&gt; as text on the label. On macOS/Linux, &lt;code&gt;lp -d &amp;lt;printer&amp;gt; -o raw label.zpl&lt;/code&gt; does the right thing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Network label printers listen on TCP port 9100.&lt;/strong&gt; &lt;code&gt;nc 192.168.1.60 9100 &amp;lt; label.zpl&lt;/code&gt; is the fastest way to test one. Browsers can't open raw TCP sockets, though, which brings us to…&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Getting the bytes from the browser to the printer
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Option 1: the vendor's agent
&lt;/h3&gt;

&lt;p&gt;Zebra ships a free local agent called &lt;strong&gt;Browser Print&lt;/strong&gt;, with a JS library. If every printer you'll ever support is a Zebra, use it. It doesn't help with TSC and the many generic TSPL printers, and you'll need a separate solution for receipt printers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option 2: WebUSB
&lt;/h3&gt;

&lt;p&gt;Chromium-only, needs a user gesture to pair, and on Windows the installed driver usually holds the USB interface, so the browser can't claim it. It's fine for a kiosk where you control the machine, and painful anywhere else.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option 3: your own small local agent
&lt;/h3&gt;

&lt;p&gt;A program on the PC listens on &lt;code&gt;127.0.0.1&lt;/code&gt;, your page &lt;code&gt;POST&lt;/code&gt;s the ZPL/TSPL string to it, and it writes the bytes to the OS print queue as RAW (or to TCP 9100 for network printers). It's about a hundred lines of code, plus the parts people skip:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;answer Chrome's Local Network Access preflight with &lt;code&gt;Access-Control-Allow-Private-Network: true&lt;/code&gt;. If you pass &lt;code&gt;targetAddressSpace&lt;/code&gt; to &lt;code&gt;fetch&lt;/code&gt;, use &lt;code&gt;'loopback'&lt;/code&gt; for 127.0.0.1. In Chrome 155, &lt;code&gt;'local'&lt;/code&gt; fails.&lt;/li&gt;
&lt;li&gt;bind to &lt;code&gt;127.0.0.1&lt;/code&gt; only, check the &lt;code&gt;Origin&lt;/code&gt; header against an allowlist, and reject non-loopback &lt;code&gt;Host&lt;/code&gt; headers (DNS rebinding)&lt;/li&gt;
&lt;li&gt;lock per printer so two tabs don't interleave two labels&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I wrote up the receipt-printer side of this (ESC/POS, printer widths, raster images) in &lt;a href="https://dev.to/azeem_ullah/printing-receipts-silently-from-a-web-app-what-actually-works-in-2026-cm5"&gt;a previous post&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you'd rather not build the agent
&lt;/h2&gt;

&lt;p&gt;I packaged that agent as &lt;strong&gt;Thermalink&lt;/strong&gt;: a single ~7 MB binary for Windows/macOS/Linux plus a JS SDK. For label printers it's a pure raw passthrough: you write the ZPL/TSPL, and it gets the bytes to the right printer (USB via the OS queue, or network via 9100). The same agent also does receipt printers, where it renders receipts as images.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Thermalink&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;tl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;printRaw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;zpl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;printer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;labels&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt; &lt;span class="c1"&gt;// 'labels' = alias for the Zebra/TSC queue or tcp://ip:9100&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To be upfront: I've tested it on a real BIXOLON receipt printer, and the raw label path is covered by automated tests, but I haven't tested it on a physical label printer yet. If you try it on one, I'd like to hear how it went. It's a paid one-time download with full source: &lt;a href="https://7207381545467.gumroad.com/l/xwyogp" rel="noopener noreferrer"&gt;https://7207381545467.gumroad.com/l/xwyogp&lt;/a&gt;. &lt;em&gt;Disclosure: I built it.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Everything above works without it too. If you're stuck on a specific label printer, drop the model and your ZPL/TSPL in the comments and I'll take a look.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Free reference:&lt;/strong&gt; the ZPL, TSPL and ESC/POS commands I use, on one page: &lt;a href="https://mianazeemdaula.github.io/thermalink/cheatsheet/" rel="noopener noreferrer"&gt;printer command cheat sheet&lt;/a&gt;. Thermalink's setup docs are public too: &lt;a href="https://mianazeemdaula.github.io/thermalink/" rel="noopener noreferrer"&gt;mianazeemdaula.github.io/thermalink&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>tutorial</category>
      <category>ecommerce</category>
    </item>
    <item>
      <title>Printing receipts silently from a web app: what actually works in 2026</title>
      <dc:creator>Azeem Ullah</dc:creator>
      <pubDate>Fri, 09 Oct 2026 08:03:46 +0000</pubDate>
      <link>https://dev.to/azeem_ullah/printing-receipts-silently-from-a-web-app-what-actually-works-in-2026-cm5</link>
      <guid>https://dev.to/azeem_ullah/printing-receipts-silently-from-a-web-app-what-actually-works-in-2026-cm5</guid>
      <description>&lt;p&gt;If you've built a POS, restaurant or warehouse web app, you've hit the receipt-printer wall: the browser wants to show a print dialog, the cashier wants the receipt &lt;em&gt;now&lt;/em&gt;, and the printer wants raw bytes.&lt;/p&gt;

&lt;p&gt;Here's what I learned getting a web page to print to a thermal receipt printer with no dialog, including two gotchas that cost me real time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option 1: &lt;code&gt;window.print()&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Works everywhere, but:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;it always shows a dialog (outside kiosk mode)&lt;/li&gt;
&lt;li&gt;it adds page margins and scaling you don't control&lt;/li&gt;
&lt;li&gt;it can't &lt;strong&gt;cut the paper&lt;/strong&gt; or &lt;strong&gt;open the cash drawer&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Fine for the occasional print, painful at a busy counter.&lt;/p&gt;

&lt;p&gt;If you do stay with &lt;code&gt;window.print()&lt;/code&gt;, the page size is the part that usually goes wrong: tiny receipts on a big page, or a long tail of blank paper. I measured what Chrome does and wrote up the fix in &lt;a href="https://dev.to/azeem_ullah/the-receipt-css-everyone-copies-page-size-80mm-auto-does-nothing-in-chrome-19o0"&gt;a separate post&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option 2: WebUSB / Web Serial
&lt;/h2&gt;

&lt;p&gt;No install, but it's Chromium-only, needs a user gesture to pair the device, and on Windows it fights the installed printer driver for the USB interface. It's OK for kiosks you fully control.&lt;/p&gt;

&lt;h2&gt;
  
  
  Option 3: a local agent (what most POS apps end up doing)
&lt;/h2&gt;

&lt;p&gt;A small program on the shop PC listens on &lt;code&gt;127.0.0.1&lt;/code&gt;, and your page calls it with &lt;code&gt;fetch()&lt;/code&gt;. It talks to the printer through the OS print queue or a network socket. Three things to get right:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Chrome's Local Network Access rules
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;Updated 10 Oct 2026. I got this part wrong the first time.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I originally wrote that the thing to get right here was answering the preflight with &lt;code&gt;Access-Control-Allow-Private-Network: true&lt;/code&gt;. That header comes from the older Private Network Access design. What actually stands in your way now is a permission prompt: since Chrome 142 the user has to click Allow before a public site can reach &lt;code&gt;127.0.0.1&lt;/code&gt;, and Firefox 153 does the same.&lt;/p&gt;

&lt;p&gt;While that prompt is open, your request doesn't fail. It hangs. A typical "is the agent running?" check times out and tells the user to install something that is already running. I tested this and wrote up the fix in &lt;a href="https://dev.to/azeem_ullah/agent-not-running-when-it-is-running-chromes-local-network-prompt-and-localhost-helpers-4nll"&gt;a separate post&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;One detail from the original still holds. If you pass the newer &lt;code&gt;targetAddressSpace&lt;/code&gt; fetch option, it has to match the real address space. In Chrome 155, &lt;code&gt;targetAddressSpace: 'local'&lt;/code&gt; for a loopback address makes the fetch &lt;strong&gt;fail outright&lt;/strong&gt;. &lt;code&gt;'loopback'&lt;/code&gt; (or no option) works:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://127.0.0.1:17777/v1/health&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;targetAddressSpace&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;loopback&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Security
&lt;/h3&gt;

&lt;p&gt;Anything listening on localhost can be called by &lt;em&gt;any&lt;/em&gt; website the user opens. At minimum:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;bind to &lt;code&gt;127.0.0.1&lt;/code&gt; only, never &lt;code&gt;0.0.0.0&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;check the &lt;code&gt;Origin&lt;/code&gt; header against an allowlist (browsers set it, and pages can't fake it)&lt;/li&gt;
&lt;li&gt;reject requests whose &lt;code&gt;Host&lt;/code&gt; header isn't a loopback name, which blocks DNS-rebinding attacks&lt;/li&gt;
&lt;li&gt;require an API key for non-browser callers (no &lt;code&gt;Origin&lt;/code&gt; header)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Printer width is never what the spec sheet says
&lt;/h3&gt;

&lt;p&gt;This one surprised me. "80 mm" printers differ in DPI (180 vs 203), some have a 58 mm mode switch, and they all keep their own margins. My BIXOLON SRP-352plusIII is &lt;strong&gt;576 dots&lt;/strong&gt; wide on paper (the spec sheet), but in practice only &lt;strong&gt;568 dots&lt;/strong&gt; print. Text at the very right edge got clipped.&lt;/p&gt;

&lt;p&gt;If you build receipts out of fixed 48-character text lines, they'll wrap or clip differently on the next printer you meet. The approach that fixed it for me: &lt;strong&gt;render the receipt as a bitmap at the printer's measured width&lt;/strong&gt; (real TTF fonts, measured columns, QR codes, barcodes) and send it as an ESC/POS raster image. Then it looks the same on every brand.&lt;/p&gt;

&lt;p&gt;How to measure: print a ruler image 640 dots wide with a tick every 8 dots and a number every 32 dots, each number ending at its tick. The last fully readable number (+8 per extra tick) is the usable width.&lt;/p&gt;

&lt;h2&gt;
  
  
  ESC/POS cheat sheet
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What&lt;/th&gt;
&lt;th&gt;Bytes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Initialize&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1B 40&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Partial cut&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1D 56 01&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kick cash drawer (pin 2)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1B 70 00 19 FA&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Raster image&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;1D 76 30 00 xL xH yL yH&lt;/code&gt; + 1 bit per dot, MSB first&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Tips:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Send raster images in bands of about 128 rows. Cheap printers overflow their buffer on one huge image.&lt;/li&gt;
&lt;li&gt;On Windows, send bytes through the spooler with datatype &lt;code&gt;RAW&lt;/code&gt; (&lt;code&gt;OpenPrinter&lt;/code&gt; → &lt;code&gt;StartDocPrinter&lt;/code&gt; with &lt;code&gt;"RAW"&lt;/code&gt; → &lt;code&gt;WritePrinter&lt;/code&gt;). Then any installed driver works, and it won't re-render your bytes.&lt;/li&gt;
&lt;li&gt;On macOS/Linux, &lt;code&gt;lp -d &amp;lt;printer&amp;gt; -o raw&lt;/code&gt; does the same through CUPS.&lt;/li&gt;
&lt;li&gt;Network printers almost always accept raw bytes on TCP port &lt;strong&gt;9100&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  If you'd rather not build it
&lt;/h2&gt;

&lt;p&gt;I packaged all of the above as &lt;strong&gt;Thermalink&lt;/strong&gt;: a ~7 MB agent for Windows/macOS/Linux plus a small JS SDK. It has image mode (and a text mode for slow links), a &lt;code&gt;calibrate&lt;/code&gt; command that prints that ruler, QR/barcodes/logos, cash drawer, label printers (ZPL/TSPL), and full source:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Thermalink&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;tl&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;Thermalink&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;receipt&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;title&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;BLUE DOOR CAFE&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;row&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Flat white x2&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;8.00&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;row&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;TOTAL&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;8.00&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;bold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;qr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://example.com/r/A1B2C3&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cut&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's a paid, one-time download: &lt;a href="https://7207381545467.gumroad.com/l/xwyogp" rel="noopener noreferrer"&gt;https://7207381545467.gumroad.com/l/xwyogp&lt;/a&gt;. &lt;em&gt;Disclosure: I built it.&lt;/em&gt; Everything in this article works without it too. Happy to answer ESC/POS questions in the comments.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Free reference:&lt;/strong&gt; the ESC/POS, ZPL and TSPL commands I use, on one page: &lt;a href="https://mianazeemdaula.github.io/thermalink/cheatsheet/" rel="noopener noreferrer"&gt;printer command cheat sheet&lt;/a&gt;. Thermalink's setup docs are public too: &lt;a href="https://mianazeemdaula.github.io/thermalink/" rel="noopener noreferrer"&gt;mianazeemdaula.github.io/thermalink&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>tutorial</category>
      <category>pos</category>
    </item>
  </channel>
</rss>
