<?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: Hisa Araki</title>
    <description>The latest articles on DEV Community by Hisa Araki (@maskedridersystem).</description>
    <link>https://dev.to/maskedridersystem</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%2F3870770%2F75af346f-4c2f-4ed0-a24c-a6609ae7a0d3.png</url>
      <title>DEV Community: Hisa Araki</title>
      <link>https://dev.to/maskedridersystem</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/maskedridersystem"/>
    <language>en</language>
    <item>
      <title>GS1 Sunrise 2027 Might Not Fit Your Thermal Printer</title>
      <dc:creator>Hisa Araki</dc:creator>
      <pubDate>Thu, 05 Nov 2026 10:42:19 +0000</pubDate>
      <link>https://dev.to/maskedridersystem/gs1-sunrise-2027-might-not-fit-your-thermal-printer-5cec</link>
      <guid>https://dev.to/maskedridersystem/gs1-sunrise-2027-might-not-fit-your-thermal-printer-5cec</guid>
      <description>&lt;p&gt;GS1 Sunrise 2027 is the industry's push to get retail POS systems scanning 2D barcodes (GS1 QR Code / GS1 DataMatrix) by the end of 2027. It's not a mandate, but the upside is real: a single 2D symbol can carry a GTIN plus attribute data like lot number and expiry date, instead of a bare 1D barcode that can't.&lt;/p&gt;

&lt;p&gt;During the transition, though, the 2D symbol has to be &lt;strong&gt;dual-marked&lt;/strong&gt; alongside the existing 1D barcode (EAN-13/UPC). And if your printer is a common 203 dpi thermal printer, that requirement runs into a wall faster than you'd expect.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 203 dpi printer can't quite hit the minimum 2D size
&lt;/h2&gt;

&lt;p&gt;GS1's General Specifications set the minimum X-dimension for a 2D symbol at retail POS at &lt;strong&gt;0.396 mm&lt;/strong&gt;. A 203 dpi printer can only print in whole dots (1 dot = 0.125 mm):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dots&lt;/th&gt;
&lt;th&gt;Size&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;3 dots&lt;/td&gt;
&lt;td&gt;0.375 mm&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4 dots&lt;/td&gt;
&lt;td&gt;0.500 mm&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;3 dots (0.375 mm) falls &lt;em&gt;just&lt;/em&gt; short of the 0.396 mm minimum, so &lt;strong&gt;4 dots becomes the effective minimum&lt;/strong&gt;. Meanwhile the 1D barcode (EAN-13) is locked to 3 dots (0.375 mm, 113.7% magnification) — it can't be scaled down further without going out of spec.&lt;/p&gt;

&lt;p&gt;That small shortfall has an outsized effect on label size.&lt;/p&gt;

&lt;h2&gt;
  
  
  Side by side, it doesn't fit on a 50 mm label
&lt;/h2&gt;

&lt;p&gt;GS1's placement guideline also has a "50 mm rule": the 2D symbol has to sit within 50 mm of the center of the linear barcode. That part's achievable at 4 dots. The real problem is label &lt;em&gt;area&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;At 203 dpi / 4 dots:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;EAN-13 + GS1 QR (V4): side by side, &lt;strong&gt;62.9 × 29.5 mm&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;EAN-13 + GS1 DataMatrix (20×20): side by side, &lt;strong&gt;53.4 × 29.5 mm&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Common 50 mm-wide retail labels simply can't fit either combination side by side — even the smallest DataMatrix (GTIN only, 16×16) comes out to 51.4 mm. Stack them vertically instead, and the width problem goes away, but now you need 40–50 mm of height, which eats almost all the space left for product name, price, and expiry text.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verification testing hasn't covered 203 dpi
&lt;/h2&gt;

&lt;p&gt;GS1 Japan published read-performance verification results in October 2025 — but the printers tested were 300/305/360 dpi only. The printer resolution that's actually dominant on the ground in Japan was never tested.&lt;/p&gt;

&lt;h2&gt;
  
  
  We wrote up the math as a technical note
&lt;/h2&gt;

&lt;p&gt;We dug into this quantitatively and published a technical note:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.barbridge.org/notes/sunrise2027-en/" rel="noopener noreferrer"&gt;English version&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.barbridge.org/notes/sunrise2027-ja/" rel="noopener noreferrer"&gt;日本語 (Japanese)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/acrossreport/barbridge/tree/main/docs/technical-notes" rel="noopener noreferrer"&gt;Markdown on GitHub&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Short version of the conclusions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;203 dpi + GS1 DataMatrix is the realistic option&lt;/strong&gt; for the Japanese market: side-by-side placement is physically impossible, but a stacked layout satisfies both the 50 mm rule and a 50 mm-wide label — on labels of 50×40 mm or smaller, though, the codes alone nearly fill the label.&lt;/li&gt;
&lt;li&gt;Whether &lt;strong&gt;rectangular DataMatrix&lt;/strong&gt; (e.g. 12×36) is an acceptable retail-POS format is the deciding factor for small labels.&lt;/li&gt;
&lt;li&gt;Moving to 300 dpi resolves the compliance issue, but side-by-side placement with QR is still difficult even then.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We've rolled seven open questions from this (how the 203 dpi minimum should be treated, whether 203 dpi gets included in verification testing, how the 50 mm rule is formally measured, etc.) into inquiries to both GS1 Japan and GS1 Global Office.&lt;/p&gt;

&lt;h2&gt;
  
  
  We're also building a library for this
&lt;/h2&gt;

&lt;p&gt;In parallel, we're building &lt;strong&gt;BarBridge&lt;/strong&gt;, a Rust core that generates GS1-compliant 1D/2D barcode pairs, automatically computing the largest compliant print size that still fits a given footprint — callable from Python, C/C++, C#, VB.NET, Java, PHP, and Node.js over a C ABI. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.barbridge.org/" rel="noopener noreferrer"&gt;barbridge.org&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/acrossreport/barbridge" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're dealing with 203 dpi label design, or have run into the same constraint, I'd like to hear about it — comments and corrections welcome.&lt;/p&gt;

</description>
      <category>barcode</category>
      <category>gs1</category>
      <category>rust</category>
    </item>
    <item>
      <title>BarBridge: a Rust core for GS1 Sunrise 2027 barcode pairs, callable from 7 languages over a C ABI</title>
      <dc:creator>Hisa Araki</dc:creator>
      <pubDate>Sun, 01 Nov 2026 14:00:41 +0000</pubDate>
      <link>https://dev.to/maskedridersystem/barbridge-a-rust-core-for-gs1-sunrise-2027-barcode-pairs-callable-from-7-languages-over-a-c-abi-242c</link>
      <guid>https://dev.to/maskedridersystem/barbridge-a-rust-core-for-gs1-sunrise-2027-barcode-pairs-callable-from-7-languages-over-a-c-abi-242c</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;I built and published &lt;strong&gt;&lt;a href="https://github.com/acrossreport/barbridge" rel="noopener noreferrer"&gt;BarBridge&lt;/a&gt;&lt;/strong&gt;, a language-independent Rust core that generates GS1-compliant 1D+2D barcode pairs for the upcoming &lt;strong&gt;GS1 Sunrise 2027&lt;/strong&gt; transition. The core exposes a minimal &lt;strong&gt;C ABI&lt;/strong&gt;, and today I finished publishing ready-to-run samples in 7 languages across 6 platform targets (Windows/macOS/Linux × x64/ARM64), plus a dual MIT/Apache-2.0 license.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/acrossreport/barbridge" rel="noopener noreferrer"&gt;https://github.com/acrossreport/barbridge&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem: GS1 Sunrise 2027
&lt;/h2&gt;

&lt;p&gt;In 2027, GS1 is moving retail POS barcode practice away from 1D-only symbols (EAN/UPC, etc.) toward pairing them with 2D symbols (GS1 QR Code / GS1 DataMatrix). The 2D symbol can carry attribute data — lot number, expiration date — alongside the GTIN, which is great for traceability. But during the transition, retailers and manufacturers need to print &lt;strong&gt;both&lt;/strong&gt; symbol types together, correctly sized and laid out.&lt;/p&gt;

&lt;p&gt;That means, for every label:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Keep the barcode's X-dimension (module width) inside GS1's allowed range&lt;/li&gt;
&lt;li&gt;Compute the largest GS1-compliant size that still fits a target footprint (e.g. 50mm × 50mm)&lt;/li&gt;
&lt;li&gt;Arrange the 1D and 2D symbols together (side-by-side or stacked) for the same GTIN&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Reimplementing that math per printer vendor or per language is exactly the kind of thing that quietly drifts out of spec over time. So I pulled it into a single core.&lt;/p&gt;

&lt;h2&gt;
  
  
  From an ACR feature to a standalone library
&lt;/h2&gt;

&lt;p&gt;BarBridge didn't start as its own project. The first version of this GS1 pairing logic was built as an optional feature inside &lt;strong&gt;ACR&lt;/strong&gt;, our receipt/label rendering engine, specifically to get ahead of GS1 Sunrise 2027 support there. But the moment I got it working, it was obvious the same logic — GS1 size optimization, 1D+2D layout, pairing — had nothing ACR-specific about it. Any POS, ERP, or label-printing tool facing the same 2027 transition would need the exact same math. So instead of keeping it locked inside ACR, I pulled it out and rebuilt it as &lt;strong&gt;BarBridge&lt;/strong&gt;, a standalone Rust core with its own C ABI, so it could be adopted independently of whatever rendering stack a given tool already uses.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture: one Rust core, one C ABI boundary
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;request_json (string)
      │
      ▼
barbridge_render_png()   ← Rust core: GS1 size optimization + layout + rendering
      │
      ▼
PNG bytes (caller-owned buffer)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exposed surface is intentionally small:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Function&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;barbridge_render_png(request_json, &amp;amp;out_data, &amp;amp;out_len)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Renders a GS1 1D+2D pair (or a single symbol) to PNG bytes in memory&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;barbridge_bytes_free(data, len)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Frees the returned buffer (always call it)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;barbridge_last_error()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Human-readable reason for the last failure&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Request example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"(01)04901234567894(10)LOT42(17)271231"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mode"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pair"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"matrix"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"data_matrix"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"arrangement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"auto"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"max_width_mm"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"max_height_mm"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"dpi"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;600&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;p&gt;&lt;code&gt;data&lt;/code&gt; accepts either a GS1 AI element string (&lt;code&gt;(01)GTIN(10)LOT…&lt;/code&gt;) or a bare GTIN-8/12/13/14. &lt;code&gt;mode&lt;/code&gt; is &lt;code&gt;pair&lt;/code&gt; (default), &lt;code&gt;linear&lt;/code&gt; (1D only), or &lt;code&gt;matrix&lt;/code&gt; (2D only).&lt;/p&gt;

&lt;p&gt;Everything happens &lt;strong&gt;in memory&lt;/strong&gt; — no temp files, no subprocess, no external CLI tool. The caller's process owns the output buffer, so there's no process-spawn overhead when embedding this in a server or batch pipeline. A WebAssembly build for the browser is planned too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Today's work: samples for 7 languages × 6 platforms
&lt;/h2&gt;

&lt;p&gt;The bulk of today's push was finishing a consistent sample set so anyone can see exactly how to call the library from their stack of choice, without reading the core source:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;barbridge/
├── barbridge.h             # C ABI header (C/C++, cgo, ...)
├── windows-x64/
├── windows-arm64/
├── linux-x64/
├── linux-arm64/
├── macos-arm64/
└── macos-x64/
    ├── native/              barbridge.dll / libbarbridge.so / libbarbridge.dylib
    ├── common/request.json  # shared sample GS1 data
    ├── c/
    ├── python/              ctypes, standard library only
    ├── csharp/              P/Invoke, `dotnet run`
    ├── vb/                  P/Invoke, `dotnet run`
    ├── java/                JNA, `mvn package`
    ├── php/                 built-in FFI extension (PHP 7.4+)
    └── nodejs/              koffi (prebuilt binary, no build toolchain)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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;Binding&lt;/th&gt;
&lt;th&gt;Run&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Python&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;ctypes&lt;/code&gt; (stdlib only)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;python render_sample.py&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C / C++&lt;/td&gt;
&lt;td&gt;link against &lt;code&gt;barbridge.dll.lib&lt;/code&gt; / &lt;code&gt;.so&lt;/code&gt; / &lt;code&gt;.dylib&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;see header comment in &lt;code&gt;render_sample.c&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C#&lt;/td&gt;
&lt;td&gt;P/Invoke&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dotnet run&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VB.NET&lt;/td&gt;
&lt;td&gt;P/Invoke&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dotnet run&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Java&lt;/td&gt;
&lt;td&gt;JNA&lt;/td&gt;
&lt;td&gt;&lt;code&gt;mvn package &amp;amp;&amp;amp; java -jar target/barbridge-sample-jar-with-dependencies.jar&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PHP&lt;/td&gt;
&lt;td&gt;built-in &lt;code&gt;FFI&lt;/code&gt; extension&lt;/td&gt;
&lt;td&gt;&lt;code&gt;php render_sample.php&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Node.js&lt;/td&gt;
&lt;td&gt;&lt;a href="https://koffi.dev/" rel="noopener noreferrer"&gt;&lt;code&gt;koffi&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;npm install &amp;amp;&amp;amp; node render_sample.js&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every sample follows the same four steps: read &lt;code&gt;../common/request.json&lt;/code&gt;, call &lt;code&gt;barbridge_render_png&lt;/code&gt;, write &lt;code&gt;output.png&lt;/code&gt;, free the buffer. Seeing the FFI boilerplate side-by-side across ctypes/P-Invoke/JNA/koffi/PHP-FFI was honestly useful just for my own sanity-checking of the C ABI design — if one binding felt awkward, that was a signal the header needed adjusting, not the sample.&lt;/p&gt;

&lt;p&gt;If you keep the library elsewhere, &lt;code&gt;BARBRIDGE_NATIVE_LIB&lt;/code&gt; lets you point at it instead of relying on the relative path.&lt;/p&gt;

&lt;p&gt;I also added a dual &lt;strong&gt;MIT OR Apache-2.0&lt;/strong&gt; license and cleaned up a stale "macOS build planned" note that had been sitting in both README files since the initial concept push.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;GS1-128 and GS1 DataBar support for logistics/case labels&lt;/li&gt;
&lt;li&gt;A WebAssembly sample for the browser&lt;/li&gt;
&lt;li&gt;Deciding how much of the core implementation itself to open up&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/acrossreport/barbridge" rel="noopener noreferrer"&gt;https://github.com/acrossreport/barbridge&lt;/a&gt; — issues and feedback welcome, especially from anyone already dealing with Sunrise 2027 compliance on the printing side.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>opensource</category>
      <category>barcode</category>
      <category>ffi</category>
    </item>
    <item>
      <title>Migrating Off ActiveReports to the Web and Linux While Keeping Your Existing Assets</title>
      <dc:creator>Hisa Araki</dc:creator>
      <pubDate>Mon, 19 Oct 2026 20:07:07 +0000</pubDate>
      <link>https://dev.to/maskedridersystem/migrating-off-activereports-to-the-web-and-linux-while-keeping-your-existing-assets-4j4e</link>
      <guid>https://dev.to/maskedridersystem/migrating-off-activereports-to-the-web-and-linux-while-keeping-your-existing-assets-4j4e</guid>
      <description>&lt;h2&gt;
  
  
  Background: three pain points of ActiveReports estates
&lt;/h2&gt;

&lt;p&gt;Plenty of .NET Framework-era business systems still rely on ActiveReports (GrapeCity / MESCIUS) for report output. After years of running in production, these setups tend to accumulate the same three frustrations:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;License renewal costs keep growing&lt;/strong&gt; — server and runtime license renewal fees no longer feel proportionate to the value the reporting feature alone provides&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Locked into Windows / .NET Framework&lt;/strong&gt; — ActiveReports itself, and the web app hosting it, assume .NET Framework, blocking a move to Linux containers or a cloud-native setup&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dozens or hundreds of designer assets (report layouts) already exist&lt;/strong&gt; — rebuilding them all from scratch isn't realistic; "keep the existing assets" is a hard requirement&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;When all three show up together, neither "full replacement" nor "do nothing" is an easy choice, and projects tend to stall.&lt;/p&gt;

&lt;h2&gt;
  
  
  The migration approach: aim for "semantic compatibility," not "replacement"
&lt;/h2&gt;

&lt;p&gt;Reading ActiveReports layout files (.rdlx etc.) with full binary compatibility isn't realistic — guaranteeing exact format fidelity is too costly.&lt;/p&gt;

&lt;p&gt;Instead, ACR (Across Report), which I develop, takes the approach of &lt;strong&gt;reproducing ActiveReports' concepts and behavior at the JSON template level&lt;/strong&gt;. Concretely, this means baking in the following compatibility at the implementation level:&lt;/p&gt;

&lt;h3&gt;
  
  
  Section structure compatibility
&lt;/h3&gt;

&lt;p&gt;ACR inherits ActiveReports' &lt;code&gt;Detail&lt;/code&gt; / &lt;code&gt;PageHeader&lt;/code&gt; / &lt;code&gt;PageFooter&lt;/code&gt; section concepts directly, including per-section controls like &lt;code&gt;NewPage AFTER&lt;/code&gt; (force a page break after a section outputs), reproduced with the same meaning.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// acr_core/src/layout/engine.rs&lt;/span&gt;
&lt;span class="c1"&gt;// ② ActiveReports-compatible: NewPage AFTER&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;sec&lt;/span&gt;&lt;span class="py"&gt;.new_page_after&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="o"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Date format and font specification compatibility
&lt;/h3&gt;

&lt;p&gt;Format tokens from &lt;code&gt;.NET&lt;/code&gt;, like &lt;code&gt;yyyy/MM/dd&lt;/code&gt; and &lt;code&gt;HH:mm:ss&lt;/code&gt;, are interpreted as-is, so existing template format strings can effectively be ported unchanged. Font resolution also follows ActiveReports' own priority order between the &lt;code&gt;Font&lt;/code&gt; / &lt;code&gt;FontName&lt;/code&gt; fields and a CSS-style &lt;code&gt;Style.font-family&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Barcode type compatibility
&lt;/h3&gt;

&lt;p&gt;ActiveReports' own barcode type codes (e.g., the legacy code &lt;code&gt;18&lt;/code&gt;) are mapped onto current types like &lt;code&gt;Code128B&lt;/code&gt;, absorbing the difference so data-side changes stay minimal.&lt;/p&gt;

&lt;p&gt;None of this aims for pixel-perfect, zero-deviation reproduction. The goal is &lt;strong&gt;letting you carry over existing report definitions, data, and operational know-how without a rewrite&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Linux becomes possible
&lt;/h2&gt;

&lt;p&gt;ACR is a native renderer implemented in Rust + Google Skia, with no dependency on .NET Framework or the Windows API.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The rendering engine itself (&lt;code&gt;acr_core&lt;/code&gt; / &lt;code&gt;acr_renderer_skia&lt;/code&gt;) builds as the same binary structure on Windows, macOS, and Linux&lt;/li&gt;
&lt;li&gt;On Linux, installing Japanese fonts (VL Gothic, etc.) is as simple as &lt;code&gt;apt install fonts-vlgothic&lt;/code&gt;, producing output identical to Windows&lt;/li&gt;
&lt;li&gt;Existing .NET / C# assets can call in via FFI (&lt;code&gt;axr_layout.dll&lt;/code&gt; / &lt;code&gt;libaxr_layout.so&lt;/code&gt;), so you don't need to rewrite the calling code all at once
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="c1"&gt;// FFI interface (shared between axr_layout.dll and libaxr_layout.so)&lt;/span&gt;
&lt;span class="kt"&gt;char&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nf"&gt;render_html&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;char&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;template_json&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;char&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;data_json&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;free_string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;char&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;ptr&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In other words, this isn't "throw away the C# app" — it's "swap out just the report-rendering layer for a lightweight Rust/Skia engine, while the calling side keeps using it via FFI, unchanged." Since the same FFI interface works on both Windows and Linux, once you eventually move the main web app into a Linux container, the report portion can be validated and switched over ahead of time, independently.&lt;/p&gt;

&lt;h2&gt;
  
  
  A typical migration path
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Inventory&lt;/strong&gt;: enumerate existing ActiveReports templates. Check section structure, barcode types, and any special formatting (installment pricing, ruby annotations, vertical text, etc.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Convert to JSON templates&lt;/strong&gt;: starting with the highest-priority reports, convert ActiveReports definitions into ACR's JSON template format (usually a fairly mechanical section-by-section, field-by-field conversion)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Validate with parallel operation&lt;/strong&gt;: run the same data through both the existing ActiveReports output and the new ACR output, then compare visually via the WYSIWYG preview and/or automated diffing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Swap the calling side&lt;/strong&gt;: on the C# side, this is just a switch to the FFI call — the web frontend and business logic stay untouched&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Move to Linux&lt;/strong&gt;: once a report has been validated, move its rendering piece into a Linux container&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Migrating off ActiveReports doesn't have to be an all-or-nothing choice between a full rewrite and doing nothing&lt;/li&gt;
&lt;li&gt;By moving to an engine that semantically inherits ActiveReports' &lt;em&gt;behavior&lt;/em&gt; — section structure, date formats, barcode types — you can migrate while keeping your template assets intact&lt;/li&gt;
&lt;li&gt;Because only the rendering layer needs to be swapped via FFI, moving from Windows to Linux can happen incrementally too&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;Note / consultation&lt;/strong&gt;: I offer consulting on migrating existing report systems (e.g., moving off ActiveReports) using ACR (Across Report), as well as reducing server costs by moving off Puppeteer-based setups.&lt;br&gt;
If you're dealing with rising license renewal costs, or want to port your system cheaply to Linux / cloud environments, feel free to reach out via &lt;a href="https://acrossreport.com" rel="noopener noreferrer"&gt;Across Systems contact&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>linux</category>
      <category>rust</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Building a Blazing-Fast Report PDF Environment on Docker/Linux, Callable from C# and Node.js</title>
      <dc:creator>Hisa Araki</dc:creator>
      <pubDate>Mon, 19 Oct 2026 20:06:42 +0000</pubDate>
      <link>https://dev.to/maskedridersystem/building-a-blazing-fast-report-pdf-environment-on-dockerlinux-callable-from-c-and-nodejs-450</link>
      <guid>https://dev.to/maskedridersystem/building-a-blazing-fast-report-pdf-environment-on-dockerlinux-callable-from-c-and-nodejs-450</guid>
      <description>&lt;h2&gt;
  
  
  The goal
&lt;/h2&gt;

&lt;p&gt;Carving out just the report-PDF-generation component into a Linux container, callable from existing C# or Node.js applications, is an increasingly common need — usually driven by wanting to cut Windows server licensing/ops costs, or to autoscale on Kubernetes.&lt;/p&gt;

&lt;p&gt;This article walks through that setup using ACR (Across Report), a Rust-based report engine I develop: running a native renderer in a Docker + Linux environment and calling it from existing C# / Node.js code with minimal changes. The key idea is to &lt;strong&gt;ship neither a browser engine nor a .NET runtime in the container — just a single native binary&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Overall architecture
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────┐
│   C# app  /  Node.js app │
└───────────┬─────────────┘
            │ FFI (axr_layout.so)  /  N-API (acr_node_native.so)
┌───────────▼─────────────┐
│    Docker container (Linux) │
│  ├ acr_core              │ ← JSON template → RenderCommand
│  ├ acr_renderer_skia     │ ← RenderCommand → PDF/PNG (Skia)
│  └ fonts-vlgothic        │ ← Japanese fonts
└─────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What you bundle into the container depends slightly on the calling language:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;From C#&lt;/strong&gt;: call &lt;code&gt;libaxr_layout.so&lt;/code&gt; via P/Invoke (the same FFI interface as Windows' &lt;code&gt;axr_layout.dll&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;From Node.js&lt;/strong&gt;: &lt;code&gt;require&lt;/code&gt; the &lt;code&gt;acr_node_native&lt;/code&gt; native addon (built with napi-rs) directly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Either path ultimately generates the PDF/PNG through the same underlying Rust binary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build details that matter
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Building the Rust core
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cargo build &lt;span class="nt"&gt;--release&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This builds every crate, including &lt;code&gt;acr_core&lt;/code&gt; / &lt;code&gt;acr_renderer_skia&lt;/code&gt;, for Linux. None of the Windows-ARM64-specific workarounds apply here — &lt;code&gt;skia-safe&lt;/code&gt;'s prebuilt binaries just work on Linux x86_64.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Calling from Node.js (napi-rs)
&lt;/h3&gt;

&lt;p&gt;The Node.js-facing native addon lives in the &lt;code&gt;acr_node_native&lt;/code&gt; crate. It's a &lt;code&gt;cdylib&lt;/code&gt; built with N-API (&lt;code&gt;napi&lt;/code&gt; / &lt;code&gt;napi-derive&lt;/code&gt;), exposing functions you can call directly from JS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// index.d.ts (excerpt)&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;renderPdf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;templateJson&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;dataJson&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;outputPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;renderPreview&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;templateJson&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;dataJson&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;PageImage&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To build it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;acr_node_native
npm &lt;span class="nb"&gt;install
&lt;/span&gt;npx napi build &lt;span class="nt"&gt;--platform&lt;/span&gt; &lt;span class="nt"&gt;--release&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your workspace customizes &lt;code&gt;target-dir&lt;/code&gt; (e.g., via &lt;code&gt;.cargo/config.toml&lt;/code&gt;), &lt;code&gt;napi build&lt;/code&gt;'s post-processing step can fail to locate the &lt;code&gt;.node&lt;/code&gt; file. In that case, fall back to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cargo build &lt;span class="nt"&gt;--release&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; acr_node_native
&lt;span class="c"&gt;# then manually copy/rename the built .so to index.&amp;lt;platform-triple&amp;gt;.node / index.node&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Baking this manual copy step directly into your &lt;code&gt;Dockerfile&lt;/code&gt;'s multi-stage build avoids this trap.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Example Dockerfile (Node.js consumer)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# ---- build stage ----&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;rust:1.78-bookworm&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;builder&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;cargo build &lt;span class="nt"&gt;--release&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; acr_node_native

&lt;span class="c"&gt;# ---- runtime stage ----&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; node:20-bookworm-slim&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;apt-get update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt-get &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; &lt;span class="nt"&gt;--no-install-recommends&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;        fonts-vlgothic &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; /var/lib/apt/lists/&lt;span class="k"&gt;*&lt;/span&gt;

&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder /app/target/release/libacr_node_native.so ./index.node&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; acr_node_native/index.js acr_node_native/index.d.ts ./&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; acr_node_native/package.json ./&lt;/span&gt;

&lt;span class="c"&gt;# COPY your app code here, and call it via require('./index.node')&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things matter here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Separate the build stage from the runtime stage&lt;/strong&gt; so the final image doesn't carry the Rust toolchain, keeping it lean&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Only install Japanese fonts in the runtime stage&lt;/strong&gt; (&lt;code&gt;apt install fonts-vlgothic&lt;/code&gt;) — the build stage doesn't need them&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. Calling from C
&lt;/h3&gt;

&lt;p&gt;If your C# app is already on .NET (Core) and cross-platform-ready, referencing &lt;code&gt;libaxr_layout.so&lt;/code&gt; via P/Invoke just works on Linux, with the exact same calling code as on Windows.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="kt"&gt;char&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nf"&gt;render_html&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;char&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;template_json&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;char&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;data_json&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;free_string&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;char&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;ptr&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you're still on .NET Framework, migrating to .NET (Core) / .NET 8+ is a prerequisite — but since the report-rendering call interface is identical to the Windows version, you can "Linux-ify and validate the report engine first, and tackle the rest of the app's .NET migration separately, later."&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this setup ends up lighter
&lt;/h2&gt;

&lt;p&gt;Compared to Chrome-based PDF generation (Puppeteer, etc.), this architecture wins on a few fronts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No need to ship a full Chromium install (hundreds of MB) in the runtime image&lt;/li&gt;
&lt;li&gt;No "renderer process" concept means none of the process-management/memory-monitoring complexity that scales with page count&lt;/li&gt;
&lt;li&gt;A single self-contained Rust binary means faster container startup and shorter cold starts under autoscaling&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Carving out just report-PDF generation into a Linux container, callable natively from C# or Node.js, is achievable via a multi-stage build plus bundling Japanese fonts only in the runtime stage&lt;/li&gt;
&lt;li&gt;With two call paths available — a napi-rs addon for Node.js, and a shared &lt;code&gt;.so&lt;/code&gt;/&lt;code&gt;.dll&lt;/code&gt; FFI interface for C# — the same engine can serve callers regardless of language&lt;/li&gt;
&lt;li&gt;Not depending on a browser engine gives you an edge on both image size and startup speed&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;Note / consultation&lt;/strong&gt;: I offer consulting on migrating existing report systems (e.g., moving off ActiveReports) using ACR (Across Report), as well as reducing server costs by moving off Puppeteer-based setups.&lt;br&gt;
If you're dealing with rising license renewal costs, or want to port your system cheaply to Linux / cloud environments, feel free to reach out via &lt;a href="https://acrossreport.com" rel="noopener noreferrer"&gt;Across Systems contact&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>docker</category>
      <category>node</category>
      <category>csharp</category>
      <category>rust</category>
    </item>
    <item>
      <title>Fixing Out-of-Memory Errors in Puppeteer PDF Generation</title>
      <dc:creator>Hisa Araki</dc:creator>
      <pubDate>Mon, 19 Oct 2026 20:06:02 +0000</pubDate>
      <link>https://dev.to/maskedridersystem/fixing-out-of-memory-errors-in-puppeteer-pdf-generation-28ni</link>
      <guid>https://dev.to/maskedridersystem/fixing-out-of-memory-errors-in-puppeteer-pdf-generation-28ni</guid>
      <description>&lt;h2&gt;
  
  
  What this is about
&lt;/h2&gt;

&lt;p&gt;Many teams generate invoices, delivery slips, and other business documents as PDFs from a web system using Puppeteer (Headless Chrome). It's an easy way to lay out documents in HTML/CSS, but once you scale up, you tend to hit a wall:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Memory balloons and the process gets OOM-killed once page counts reach the hundreds or thousands&lt;/li&gt;
&lt;li&gt;Docker container memory limits (512MB–1GB) get hit almost immediately&lt;/li&gt;
&lt;li&gt;Per-page generation time stays reasonable, but total time degrades worse than linearly&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This article first breaks down the structural reasons Puppeteer runs into OOM, then introduces an alternative: skipping the browser engine entirely. I'll walk through a real example from ACR (Across Report), a report engine I develop in Rust.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Puppeteer runs out of memory
&lt;/h2&gt;

&lt;p&gt;When generating PDFs for large numbers of pages with Puppeteer / Headless Chrome, memory bloat usually comes down to three causes.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The renderer process heap isn't designed for reuse
&lt;/h3&gt;

&lt;p&gt;A Chrome renderer process is fundamentally designed around "open a page, then close it." If you keep calling &lt;code&gt;page.pdf()&lt;/code&gt; repeatedly on the same &lt;code&gt;page&lt;/code&gt; object for hundreds of report pages in a row, the V8 heap and DOM tree caches accumulate without being released. Get the timing of &lt;code&gt;page.close()&lt;/code&gt; / &lt;code&gt;browser.close()&lt;/code&gt; wrong, and zombie Chrome processes linger around your Node.js process too.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Fonts and images get reloaded per page
&lt;/h3&gt;

&lt;p&gt;In HTML/CSS-based rendering, a Japanese font specified via &lt;code&gt;@font-face&lt;/code&gt; (especially one that isn't subsetted) can get re-parsed on every page transition. If the same font gets re-expanded for every single report page, both memory usage and rendering time degrade proportionally to page count.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. The &lt;code&gt;page.pdf()&lt;/code&gt; output buffer accumulates in-process
&lt;/h3&gt;

&lt;p&gt;When combining many pages into a single PDF, if the implementation holds each page's PDF byte sequence in memory before concatenating, the final PDF size translates directly into the Node.js process's memory footprint.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mitigations (if you stick with Puppeteer)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Restart the &lt;code&gt;browser&lt;/code&gt; every N pages and treat the process as disposable&lt;/li&gt;
&lt;li&gt;Reference OS-installed fonts via &lt;code&gt;font-family&lt;/code&gt; directly instead of embedding Base64 fonts&lt;/li&gt;
&lt;li&gt;Isolate generation into worker processes so an OOM doesn't take down the main process (tune &lt;code&gt;--max-old-space-size&lt;/code&gt; alongside this)&lt;/li&gt;
&lt;li&gt;Give Docker memory limits enough headroom for Chrome's peak memory (typically 3–5x normal usage)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are workarounds, though — they don't address the root cause, which is repurposing a browser engine for report rendering in the first place. If your report volume and page counts are only going to grow, it's worth considering a different architecture.&lt;/p&gt;

&lt;h2&gt;
  
  
  An alternative: a report renderer that skips the browser
&lt;/h2&gt;

&lt;p&gt;ACR (Across Report), which I develop, converts a JSON template into an intermediate rendering model (&lt;code&gt;RenderCommand&lt;/code&gt;) instead of HTML/CSS, and renders PDF/PNG directly via Google Skia. Written in Rust, it bypasses Chrome entirely, which means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;There's no concept of a "renderer process," so the V8-heap-accumulation problem simply doesn't exist&lt;/li&gt;
&lt;li&gt;Fonts are loaded once at process startup and cached; everything after that is a reference&lt;/li&gt;
&lt;li&gt;The generated PDF is written page-by-page as a stream, so final file size doesn't directly map to process memory&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That said, "not using a browser" doesn't automatically mean "safe" — the same class of bug can creep in depending on the implementation. Here's a real case I ran into.&lt;/p&gt;

&lt;h2&gt;
  
  
  Case study: rebuilding a font "per page" caused the same kind of bloat
&lt;/h2&gt;

&lt;p&gt;ACR's watermark drawing routine, &lt;code&gt;draw_watermark()&lt;/code&gt;, originally resolved a fresh &lt;code&gt;Typeface&lt;/code&gt; via &lt;code&gt;FontMgr::new()&lt;/code&gt; on every page. This is structurally the exact same problem as Puppeteer's font re-parsing issue.&lt;/p&gt;

&lt;p&gt;The result: the PDF output library (Skia's &lt;code&gt;SkPDF&lt;/code&gt;) ended up embedding a separate font subset per page instead of sharing one. On a 1,394-page sample report, this produced:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Before fix&lt;/th&gt;
&lt;th&gt;After fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PDF size&lt;/td&gt;
&lt;td&gt;~300MB&lt;/td&gt;
&lt;td&gt;~10.3MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Generation time&lt;/td&gt;
&lt;td&gt;~28s&lt;/td&gt;
&lt;td&gt;~2.5s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The fix was simple: build the font once outside the page loop, and pass a reference into &lt;code&gt;draw_watermark()&lt;/code&gt; instead of rebuilding it (applying the same pattern already used for &lt;code&gt;font_cache&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;What this case illustrates is a general principle: in any code generating large numbers of report pages, be suspicious of "whatever we're doing for every single page." Whether in Puppeteer or ACR, most OOM and bloat issues trace back to this pattern — initialization that should happen once, but is instead repeated inside the loop.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Heavy or crashing PDF generation for large page counts with Puppeteer is, to some degree, a structural consequence of how Chrome's renderer process works&lt;/li&gt;
&lt;li&gt;Workarounds (browser restarts, worker isolation, memory limit tuning) can get you by, but only up to a point&lt;/li&gt;
&lt;li&gt;If you want to fix it at the root, a native renderer that bypasses the browser engine is one option&lt;/li&gt;
&lt;li&gt;Even native implementations can fall into the same trap of redundant per-page initialization, though, so the profiling mindset doesn't change&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;Note / consultation&lt;/strong&gt;: I offer consulting on migrating existing report systems (e.g., moving off ActiveReports) using ACR (Across Report), as well as reducing server costs by moving off Puppeteer-based setups.&lt;br&gt;
If you're dealing with rising license renewal costs, or want to port your system cheaply to Linux / cloud environments, feel free to reach out via &lt;a href="https://acrossreport.com" rel="noopener noreferrer"&gt;Across Systems contact&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>puppeteer</category>
      <category>pdf</category>
      <category>docker</category>
      <category>performance</category>
    </item>
    <item>
      <title>Rendering PDF/PNG reports in Node.js without Chromium or a subprocess (napi-rs)</title>
      <dc:creator>Hisa Araki</dc:creator>
      <pubDate>Sat, 17 Oct 2026 09:45:16 +0000</pubDate>
      <link>https://dev.to/maskedridersystem/rendering-pdfpng-reports-in-nodejs-without-chromium-or-a-subprocess-napi-rs-b44</link>
      <guid>https://dev.to/maskedridersystem/rendering-pdfpng-reports-in-nodejs-without-chromium-or-a-subprocess-napi-rs-b44</guid>
      <description>&lt;p&gt;Most Node.js apps that need to generate PDF reports — invoices, receipts, delivery&lt;br&gt;
slips — end up picking one of two approaches:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Launch &lt;strong&gt;headless Chromium&lt;/strong&gt; (Puppeteer) and print an HTML page to PDF.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shell out to a separate CLI tool&lt;/strong&gt; and parse its output.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Both work. Both also come with overhead that has nothing to do with your actual&lt;br&gt;
report: Chromium's startup cost and memory footprint, or a child process's spawn&lt;br&gt;
latency and fragile stdout/stderr error handling.&lt;/p&gt;

&lt;p&gt;We just released &lt;a href="https://www.npmjs.com/package/@acrossreport/acr-node" rel="noopener noreferrer"&gt;&lt;code&gt;@acrossreport/acr-node&lt;/code&gt;&lt;/a&gt;,&lt;br&gt;
a native Node.js/Electron binding that renders PDF/PNG reports &lt;strong&gt;in-process&lt;/strong&gt;, with no&lt;br&gt;
Chromium and no subprocess involved.&lt;/p&gt;
&lt;h2&gt;
  
  
  What it's built on
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://acrossreport.com" rel="noopener noreferrer"&gt;ACR (Across Report)&lt;/a&gt; is a JSON-template-based report&lt;br&gt;
rendering engine written in Rust on top of Google Skia. It already powered a CLI tool,&lt;br&gt;
a VS Code extension, and a desktop designer app — the same renderer, same pixel output,&lt;br&gt;
across all of them. &lt;code&gt;acr-node&lt;/code&gt; is a thin &lt;a href="https://napi.rs/" rel="noopener noreferrer"&gt;napi-rs&lt;/a&gt; binding that exposes&lt;br&gt;
that same Rust core directly to Node.js as a native addon.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Template (JSON) → layout engine → Skia renderer → PDF / PNG
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Same template produces byte-identical pixels on Windows/macOS/Linux (true WYSIWYG).&lt;/li&gt;
&lt;li&gt;Preview and print output match exactly — no Chromium print-to-PDF layout drift.&lt;/li&gt;
&lt;li&gt;A section-based layout model (PageHeader/GroupHeader/Detail/GroupFooter/PageFooter)
familiar to anyone who's used traditional report designers (Crystal Reports,
ActiveReports, etc.).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why napi-rs over a CLI wrapper
&lt;/h2&gt;

&lt;p&gt;We considered two designs:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A — shell out to the existing CLI tool.&lt;/strong&gt; Simplest to build, but:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Process-spawn overhead adds up fast on a server generating many reports.&lt;/li&gt;
&lt;li&gt;Error handling over stdout/stderr is brittle.&lt;/li&gt;
&lt;li&gt;Binary placement and permissions differ per OS.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;B — native addon via napi-rs.&lt;/strong&gt; Calls the existing Rust core (&lt;code&gt;acr_core&lt;/code&gt; /&lt;br&gt;
&lt;code&gt;acr_renderer_skia&lt;/code&gt;) directly through FFI, no process boundary, and napi-rs has solid&lt;br&gt;
prior art for both Node.js and Electron with a mature prebuilt-binary distribution&lt;br&gt;
story. Since the Rust engine already existed, there was no good reason to introduce a&lt;br&gt;
process boundary — we went with B.&lt;/p&gt;

&lt;p&gt;We'd already built a similar napi-rs binding for a VS Code extension&lt;br&gt;
(&lt;code&gt;acr_vscode_native&lt;/code&gt;), so this followed an established pattern rather than starting&lt;br&gt;
from zero.&lt;/p&gt;
&lt;h2&gt;
  
  
  Implementation notes
&lt;/h2&gt;

&lt;p&gt;The binding itself doesn't touch the existing Rust rendering logic at all — it's a thin&lt;br&gt;
&lt;code&gt;#[napi]&lt;/code&gt;-annotated wrapper around functions that already existed. Two details worth&lt;br&gt;
calling out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Panic handling.&lt;/strong&gt; If Rust code panics across the napi-rs boundary, it takes the
whole Node.js process down with it. A malformed template JSON is a user error, not a
reason to crash the host app, so every exported function is wrapped in
&lt;code&gt;catch_unwind&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Licensing.&lt;/strong&gt; Unlicensed rendering adds a watermark to the output (same policy as
the CLI/Designer/Viewer tools). The activation cache file uses the same format across
all ACR products, so if a license is already activated on the machine by any of them,
&lt;code&gt;acr-node&lt;/code&gt; picks it up automatically — no duplicate activation needed.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Cross-platform builds
&lt;/h2&gt;

&lt;p&gt;napi-rs follows an &lt;code&gt;index.&amp;lt;platform-triple&amp;gt;.node&lt;/code&gt; naming convention and generates a&lt;br&gt;
loader (&lt;code&gt;index.js&lt;/code&gt;) that &lt;code&gt;require&lt;/code&gt;s the right binary for the current OS/arch:&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;switch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;win32&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;switch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;arch&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;x64&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nx"&gt;nativeBinding&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./index.win32-x64-msvc.node&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;arm64&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nx"&gt;nativeBinding&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./index.win32-arm64-msvc.node&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;linux&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;// glibc/musl, x64/arm64 ...&lt;/span&gt;
  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;darwin&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;// x64/arm64 ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We built and bundled binaries for six platform/arch combinations on real hardware&lt;br&gt;
(Windows x64, Windows ARM64, Intel Mac, Apple Silicon Mac, Ubuntu x64, Ubuntu ARM64 on&lt;br&gt;
WSL2):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Platform&lt;/th&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Windows x64&lt;/td&gt;
&lt;td&gt;Available&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Windows ARM64&lt;/td&gt;
&lt;td&gt;Available&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Linux x64 (glibc)&lt;/td&gt;
&lt;td&gt;Available&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Linux ARM64 (glibc)&lt;/td&gt;
&lt;td&gt;Available&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;macOS (Apple Silicon)&lt;/td&gt;
&lt;td&gt;Available&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;macOS (Intel)&lt;/td&gt;
&lt;td&gt;Available&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Cross-building in CI isn't set up yet — this round was built machine-by-machine with&lt;br&gt;
&lt;code&gt;cargo build --release&lt;/code&gt;. Automating that is next on the list.&lt;/p&gt;

&lt;h2&gt;
  
  
  Usage
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @acrossreport/acr-node
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;renderPdf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;renderPreview&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;renderPngZip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;listDataFields&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;activate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;licenseStatus&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@acrossreport/acr-node&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;readFileSync&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;node:fs&lt;/span&gt;&lt;span class="dl"&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;templateJson&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;invoice.template.json&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="s2"&gt;utf8&lt;/span&gt;&lt;span class="dl"&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;dataJson&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;invoice.data.json&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="s2"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// PDF output (72dpi). Returns the page count.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pageCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;renderPdf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;templateJson&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;dataJson&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;invoice.pdf&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// PNG-per-page array for on-screen preview (96dpi)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;renderPreview&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;templateJson&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;dataJson&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// pages[0].png is a Buffer&lt;/span&gt;

&lt;span class="c1"&gt;// Activate a license key (blocking HTTP request — call from a worker in Electron)&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;activate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;you@example.com&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="s2"&gt;ACR-XXXX-XXXX-XXXX-XXXX&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;p&gt;Same API whether you're calling it from a bare Node server (Express, Next.js API&lt;br&gt;
routes) or an Electron main process.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;PDF/PNG report rendering in Node.js/Electron, wired directly into a Rust/Skia engine
via napi-rs — no Chromium, no subprocess.&lt;/li&gt;
&lt;li&gt;The existing Rust rendering logic (&lt;code&gt;acr_core&lt;/code&gt; / &lt;code&gt;acr_renderer_skia&lt;/code&gt;) is untouched;
only a thin binding layer was added.&lt;/li&gt;
&lt;li&gt;Licensing is shared with the existing CLI/Designer/Viewer tools, so one activation
per machine covers all of them.&lt;/li&gt;
&lt;li&gt;Prebuilt binaries for six platforms ship in the package — no build toolchain required
on the consumer side.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/acrossreport/acr-node" rel="noopener noreferrer"&gt;https://github.com/acrossreport/acr-node&lt;/a&gt;&lt;br&gt;
npm: &lt;a href="https://www.npmjs.com/package/@acrossreport/acr-node" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/@acrossreport/acr-node&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Happy to answer questions about the napi-rs setup, the panic-safety boundary, or the&lt;br&gt;
Skia rendering pipeline in the comments.&lt;/p&gt;

</description>
      <category>node</category>
      <category>rust</category>
      <category>electron</category>
      <category>webdev</category>
    </item>
    <item>
      <title>ACR is designed as a printer-independent report rendering service, not just a library.</title>
      <dc:creator>Hisa Araki</dc:creator>
      <pubDate>Sun, 12 Apr 2026 14:32:27 +0000</pubDate>
      <link>https://dev.to/maskedridersystem/acr-is-designed-as-a-printer-independent-report-rendering-service-not-just-a-library-11d3</link>
      <guid>https://dev.to/maskedridersystem/acr-is-designed-as-a-printer-independent-report-rendering-service-not-just-a-library-11d3</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;I have added an HTTP interface to the report engine ACR (Across Report Renderer).&lt;/p&gt;

&lt;p&gt;By making ACR available over HTTP, it can be used as a rendering service independent from the application.&lt;/p&gt;

&lt;p&gt;Previously, report systems had to be implemented separately depending on the development environment (Desktop, Web), OS, and runtime environment.&lt;br&gt;
With ACR, you can now generate PDF / PNG / HTML simply by sending a template and data.&lt;/p&gt;
&lt;h2&gt;
  
  
  What you can do
&lt;/h2&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;• Generate reports via HTTP
• Language-independent (HTTP-based)
• Can be called from Node.js / C# / Python / Java
• Output formats: PDF / PNG / HTML (PNG embedded in HTML)
• Test directly from a browser (Web UI included)
• Runs on Windows / Linux / macOS
• Can run as a service (daemon) on all supported OS
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;⸻&lt;/p&gt;
&lt;h2&gt;
  
  
  Start
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./acr_http
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  You can also specify port and template directory:
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./acr_http &lt;span class="nt"&gt;--port&lt;/span&gt; 9000 &lt;span class="nt"&gt;--templates&lt;/span&gt; /opt/acr/templates
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  Web UI
&lt;/h2&gt;

&lt;p&gt;Open the following in your browser:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;http://localhost:8765/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkzj3fshdztga60zp9ijo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkzj3fshdztga60zp9ijo.png" alt=" " width="800" height="825"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  curl（PDF）
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:8765/render/pdf &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "template": "invoice",
    "data": {
      "items": [
        { "name": "Apple", "price": 100 },
        { "name": "Orange", "price": 200 }
      ]
    }
  }'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt; out.pdf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  curl（PNG）
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:8765/render/png &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{ "template": "invoice", "data": { "items": [] } }'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt; out.png
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  curl (HTML)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:8765/render/html &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{ "template": "invoice", "data": { "items": [] } }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Running as a service
&lt;/h3&gt;

&lt;h2&gt;
  
  
  Windows
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;acr_http.exe &lt;span class="nt"&gt;--install&lt;/span&gt;
sc start AcrHttpServer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Linux (systemd)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemctl &lt;span class="nb"&gt;enable &lt;/span&gt;acr_http
systemctl start  acr_http
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  macOS
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;launchctl load ~/Library/LaunchAgents/com.acrossreport.acr_http.plist
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;• Rust-based engine
• Rendering with Skia
• Runs as an HTTP server
• Template and data are separated
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;⸻&lt;/p&gt;

&lt;p&gt;Summary&lt;/p&gt;

&lt;p&gt;The HTTP version is already released for evaluation.&lt;br&gt;
    • Command-line renderer: acr-cli&lt;br&gt;
    • Report designer: acr-designer&lt;br&gt;
    • RPX converter for ActiveReports: rpx2json&lt;br&gt;
⸻&lt;/p&gt;

&lt;h2&gt;
  
  
  Roadmap
&lt;/h2&gt;

&lt;p&gt;The following features are planned:&lt;br&gt;
• WebAssembly (WASM) support for browser execution&lt;br&gt;
• Receipt printer support (ESC/POS)&lt;br&gt;
• Support for printer command languages (ZPL / SBPL / TPCL)&lt;/p&gt;

&lt;p&gt;This will extend ACR beyond server-side usage to embedded devices, edge environments, and browsers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Download
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/acrossreport" rel="noopener noreferrer"&gt;https://github.com/acrossreport&lt;/a&gt;&lt;/p&gt;

</description>
      <category>rust</category>
      <category>csharp</category>
      <category>avalonia</category>
      <category>printer</category>
    </item>
    <item>
      <title>I built a pixel-perfect, printer-independent report designer with Avalonia UI</title>
      <dc:creator>Hisa Araki</dc:creator>
      <pubDate>Sun, 12 Apr 2026 06:09:25 +0000</pubDate>
      <link>https://dev.to/maskedridersystem/i-built-a-pixel-perfect-printer-independent-report-designer-with-avalonia-ui-4dke</link>
      <guid>https://dev.to/maskedridersystem/i-built-a-pixel-perfect-printer-independent-report-designer-with-avalonia-ui-4dke</guid>
      <description>&lt;p&gt;I’ve released ACR Designer, a WYSIWYG report designer for the ACR (Across Report Renderer) engine.&lt;/p&gt;

&lt;p&gt;It focuses on pixel-perfect layout and printer-independent rendering.&lt;/p&gt;

&lt;p&gt;Key features:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;WYSIWYG report designer&lt;/li&gt;
&lt;li&gt;Pixel-perfect layout (dot-level accuracy)&lt;/li&gt;
&lt;li&gt;Printer-independent rendering&lt;/li&gt;
&lt;li&gt;Built with C# and Avalonia UI&lt;/li&gt;
&lt;li&gt;Runs natively on Windows and macOS&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;GitHub:&lt;br&gt;
&lt;a href="https://github.com/acrossreport/acr-designer" rel="noopener noreferrer"&gt;https://github.com/acrossreport/acr-designer&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is my first time building a full cross-platform desktop application with Avalonia, and I was impressed by how smoothly it works across platforms.&lt;/p&gt;

&lt;p&gt;Feedback is welcome.&lt;/p&gt;




&lt;h2&gt;
  
  
  Personal Note
&lt;/h2&gt;

&lt;p&gt;I’ve also released the macOS version of ACR Designer.&lt;/p&gt;

&lt;p&gt;At 66, this project has been an incredibly valuable learning experience for me.&lt;/p&gt;

&lt;p&gt;I truly appreciate all the support, and I’ll keep improving it whenever I can.&lt;/p&gt;

</description>
      <category>csharp</category>
      <category>dotnet</category>
      <category>rust</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I built a pixel-perfect, printer-independent report designer with Avalonia UI</title>
      <dc:creator>Hisa Araki</dc:creator>
      <pubDate>Sun, 12 Apr 2026 04:17:00 +0000</pubDate>
      <link>https://dev.to/maskedridersystem/i-built-a-pixel-perfect-printer-independent-report-designer-with-avalonia-ui-5ddl</link>
      <guid>https://dev.to/maskedridersystem/i-built-a-pixel-perfect-printer-independent-report-designer-with-avalonia-ui-5ddl</guid>
      <description>&lt;p&gt;I’ve released ACR Designer, a WYSIWYG report designer for the ACR (Across Report Renderer) engine.&lt;/p&gt;

&lt;p&gt;It focuses on pixel-perfect layout and printer-independent rendering.&lt;/p&gt;

&lt;p&gt;Key features:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;WYSIWYG report designer&lt;/li&gt;
&lt;li&gt;Pixel-perfect layout (dot-level accuracy)&lt;/li&gt;
&lt;li&gt;Printer-independent rendering&lt;/li&gt;
&lt;li&gt;Built with C# and Avalonia UI&lt;/li&gt;
&lt;li&gt;Runs natively on Windows and macOS&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;GitHub:&lt;br&gt;
&lt;a href="https://github.com/acrossreport/acr-designer" rel="noopener noreferrer"&gt;https://github.com/acrossreport/acr-designer&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is my first time building a full cross-platform desktop application with Avalonia, and I was impressed by how smoothly it works across platforms.&lt;/p&gt;

&lt;p&gt;Feedback is welcome.&lt;/p&gt;

</description>
      <category>csharp</category>
      <category>dotnet</category>
      <category>showdev</category>
      <category>ui</category>
    </item>
    <item>
      <title>Why I built a pixel-perfect report engine with Rust and Skia</title>
      <dc:creator>Hisa Araki</dc:creator>
      <pubDate>Fri, 10 Apr 2026 02:23:19 +0000</pubDate>
      <link>https://dev.to/maskedridersystem/why-i-built-a-pixel-perfect-report-engine-with-rust-and-skia-4g76</link>
      <guid>https://dev.to/maskedridersystem/why-i-built-a-pixel-perfect-report-engine-with-rust-and-skia-4g76</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;For many years, I have been working on business systems where report generation is a critical component.&lt;/p&gt;

&lt;p&gt;However, I repeatedly encountered the same problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Layout differences depending on printer drivers&lt;/li&gt;
&lt;li&gt;OS-specific rendering inconsistencies&lt;/li&gt;
&lt;li&gt;Difficulty achieving true WYSIWYG output&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These issues are not new, but they are still not fully solved.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;In traditional report systems, rendering often depends on external components such as printer drivers or OS-level graphics APIs.&lt;/p&gt;

&lt;p&gt;This leads to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Slight layout shifts&lt;/li&gt;
&lt;li&gt;Inconsistent text measurement&lt;/li&gt;
&lt;li&gt;Different output between environments&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For business reports, even small differences can be unacceptable.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Approach
&lt;/h2&gt;

&lt;p&gt;To solve this, I designed a report engine with the following principles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fully deterministic layout&lt;/li&gt;
&lt;li&gt;Pixel-perfect rendering&lt;/li&gt;
&lt;li&gt;No dependency on printer drivers&lt;/li&gt;
&lt;li&gt;Cross-platform consistency&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The engine directly controls rendering using Skia.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Rust
&lt;/h2&gt;

&lt;p&gt;I chose Rust for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Memory safety&lt;/li&gt;
&lt;li&gt;Predictable performance&lt;/li&gt;
&lt;li&gt;Strong control over low-level behavior&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is important for a rendering engine where consistency is critical.&lt;/p&gt;

&lt;h2&gt;
  
  
  Technical Highlights
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Layout engine with pre-calculated positioning&lt;/li&gt;
&lt;li&gt;Pagination control&lt;/li&gt;
&lt;li&gt;Consistent font measurement&lt;/li&gt;
&lt;li&gt;Rendering abstraction over Skia&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Result
&lt;/h2&gt;

&lt;p&gt;The engine produces identical output (PDF / PNG) across environments.&lt;/p&gt;

&lt;h2&gt;
  
  
  Repository
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/acrossreport" rel="noopener noreferrer"&gt;https://github.com/acrossreport&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;If you are interested in rendering engines, Skia, or cross-platform layout systems, I would appreciate your feedback.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>skia</category>
      <category>opensource</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
