Code readingTroubleshooting

QR code decoding in .NET: feed pixel data, not bitmaps

Why your .NET Framework QR decode fails on .NET 6, which image library binding to add, and how to feed a 12 MP camera frame to the decoder as raw bytes.

LumaSpec Vision8 min read Share on LinkedIn

On this page 9 sections
  1. 01Why the same code fails on .NET 6
  2. 02Likely causes, in order of probability
  3. 03Fix 1: add the Windows Compatibility binding
  4. 04Fix 2: decode from your image library's own type
  5. 05Fix 3: feed the pixel buffer directly
  6. 06Worked example: a 4000 × 3000 frame as 12,000,000 bytes
  7. 07When a PC decoder is the wrong tool
  8. 08Checks before you ship
  9. 09Questions

A QR decoder that ran for years on .NET 4.5 stops working the day the project moves to .NET 6: BarcodeReader no longer accepts the Bitmap you loaded with Image.FromFile. Nothing is wrong with your codes, your camera or the decode engine. The Bitmap class left the base runtime, and the decoding library moved every Bitmap handler into separate binding packages. Here you get the failing pattern, the likely causes in order of probability, three fixes, a worked example that feeds a 12 MP camera frame to the decoder as raw bytes, and the point at which a fixed code reader does the job with no image code at all.

Why the same code fails on .NET 6

On .NET Framework 4.5 this pattern ran for years: a reader instance, a Bitmap loaded from disk, one Decode call.

IBarcodeReader reader = new BarcodeReader();
var barcodeBitmap = (Bitmap)Image.FromFile("wwwroot/qrr/" + Path.GetFileName(file));
var result = reader.Decode(barcodeBitmap);

The failure is not in your codes or in the decoding engine. The .NET Framework shipped System.Drawing.Bitmap as part of the runtime, up to and including version 4.8, and the library's default BarcodeReader and BarcodeWriter classes were written against that type. Modern .NET — Core, 5, 6 and later — has no Bitmap class in the base framework, so the base package for those targets contains no reader or writer that accepts a Bitmap. All the image glue moved into separate binding packages. From .NET 6 there is a second catch: System.Drawing.Common, the package that carries Bitmap, is supported on Windows only.

Likely causes, in order of probability

1. No binding package. The core package you installed targets .NET 6 and decodes fine; the Bitmap glue is missing. Check the project's package list for entries starting with ZXing.Net.Bindings. If there are none, this is your cause.

2. No Bitmap type at all. On a clean .NET 6 project, System.Drawing does not resolve until you add System.Drawing.Common — and that package is Windows-only from .NET 6. A project that compiles on the Windows development box can still fail on a Linux build agent.

3. It compiles, but Decode returns null. The plumbing is right and the pixels are not: palette or indexed pixel formats, 16-bit depths and CMYK files are the usual offenders. Decode a known-good PNG first. If the file decodes and line frames do not, check the pixel format the frames actually carry — the converted-image pixel format guide shows that check — then move to the optics causes in the last section.

Fix 1: add the Windows Compatibility binding

If the application stays on Windows, this is the smallest change. Add the package the library publishes for exactly this case: ZXing.Net.Bindings.Windows.Compatibility. Its reader accepts a Bitmap again, so the old three lines come back without edits to the decode call.

You pay for it in deployment scope: System.Drawing on .NET 6 runs on Windows only, so this binding is wrong for Linux containers and cross-platform services. Choose it for Windows desktop tools and Windows services. The moment Linux enters the picture, choose Fix 2 or Fix 3.

Fix 2: decode from your image library's own type

Cross-platform applications should load pixels with a managed image library and add the binding built for it. The packages follow one pattern — ZXing.Net.Bindings followed by the library name; the ImageSharp binding and the SkiaSharp binding are the common picks. Load the file with your library, hand its image type to the binding's reader, and decode.

// illustrative names -- match them to the binding you added
var image  = ImageLibrary.Load(path);  // your image library's load call
var reader = new BindingReader();      // from the binding package you added
var result = reader.Decode(image);     // the library's image type, not Bitmap

Same decode engine, no Windows dependency, and file loading is yours to control — useful when frames arrive from streams rather than files.

Fix 3: feed the pixel buffer directly

The decoder never wanted an image object in the first place. Its first processing step computes a luminance value for every pixel, binarizes that map, and searches it for the finder patterns — the finder-pattern guide shows what it looks for. Anything that delivers brightness per pixel works, including the frame buffer your camera SDK already hands you.

For mono frames the buffer is the luminance map: 8 bits per pixel, one byte per pixel, no conversion at all. For color frames, pass RGB bytes and let the luminance source convert, or convert once yourself. This path needs no binding package, no file reading and no Bitmap — and it is the natural one for frames from area scan cameras on a line.

// illustrative names -- your camera SDK's buffer call differs
int width  = 4000;                         // px
int height = 3000;                         // px
byte[] pixels = new byte[width * height];  // Mono8: 1 byte per pixel
// 1. fill pixels from the SDK's frame buffer
// 2. build the decoder's luminance source from pixels, width, height
//    and the 8-bit format
// 3. call Decode and use the returned string

Match the shutter to the motion. Rolling shutter suits stationary, presented parts; a belt that keeps moving during exposure calls for global shutter, and the 2.5 MP GigE MV-CU020-90GC is a typical global-shutter pick for conveyor code reading.

Area scan camerasHikrobot MV-CU020-90GC 2.5 MP Area Scan Camera, GigE, GMAX4002, ColorMV-CU020-90GC
Sensor format
1/1.7"
Lens mount
C-mount
Interface
GigE
Resolution
Up to 3 MP
$309.00View partBuild a system with it

The buffer-decoding guide shows how to keep the pixel buffer intact between acquisition and decode, so a retry costs no second image.

Worked example: a 4000 × 3000 frame as 12,000,000 bytes

Inputs. Frame size 4000 × 3000 px — the printed resolution of the LS-CMR-0166, a 12 MP USB3 camera. Pixel format Mono8, 1 byte per pixel, as delivered by a mono camera; an RGB frame triples every byte count below. Buffer: one byte array filled by the SDK at acquire time.

Area scan cameras12 MP USB3 area scan camera, IMX226, 1/1.7" rolling shutter, 30 fps, 4000 × 3000 resolution (LS-CMR-0166)LS-CMR-0166
Sensor format
1/1.7"
Interface
USB3
Resolution
12 MP
Shutter
Rolling
$560.00View partBuild a system with it

Rule in words. The luminance source needs exactly width × height × bytes-per-pixel bytes, with rows tightly packed.

frame bytes = width × height × bytes per pixel Mono8: 4000 × 3000 × 1 B = 12,000,000 B RGB8: 4000 × 3000 × 3 B = 36,000,000 B

Arithmetic. 4000 × 3000 = 12,000,000 pixels, so the Mono8 buffer is 12,000,000 bytes — 12 MB, or 11.4 MiB in binary units. At 30 fps the mono stream moves 12,000,000 × 30 = 360,000,000 B/s, about 360 MB/s. Row stride: 4000 bytes per mono row is already a multiple of 4, so tight packing carries no padding — which hands you a hard test.

Result and decision. The buffer length must be exactly 12,000,000 bytes. If the SDK returns 12,288,000, the rows are padded to 4096 bytes (4096 × 3000 = 12,288,000): copy row by row or slice each row before decoding, because a padded buffer fed as tight data smears every row and destroys the finder patterns. Feed 12,000,000 bytes with width 4000, height 3000 and Mono8; Decode returns the payload string. A null result with a correct-length buffer is not a plumbing problem — go to the optics checks below.

When a PC decoder is the wrong tool

If the station's whole job is read the code and pass the string, let the device decode. A fixed code reader or smart camera runs the decode on-board and sends text over Ethernet or serial — your .NET code never sees a pixel. The LS-CRD-0028 reads up to 60 codes/s with its 8 mm lens built in, so the station wiring is one Ethernet cable and a trigger.

Keep PC-side decoding when the result feeds custom logic: MES transactions, routing decisions, image archiving, or when the frames come from cameras you already own. The camera-to-validated-data walkthrough covers that pipeline end to end.

Fixed code readers2560 x 2048 fixed code reader, 30 fps, 60 codes/s, 8 mm lens (LS-CRD-0028)LS-CRD-0028
Interface
Fast Ethernet
Resolution
5 MP
Sensor format
1/1.7"
Shutter
Global
$920.00View part

Checks before you ship

Run this list before the station goes on the line:

  • Binding present. The package list shows a ZXing.Net.Bindings.* entry matching your image library, or Windows.Compatibility on a Windows-only application. Without it the Bitmap overloads never appear.
  • File first, frames second. A saved test QR decodes before you point at the camera. That separates decode bugs from acquisition bugs in one step.
  • Buffer length asserted. Width × height × bytes per pixel — 12,000,000 for the worked example — checked before every Decode call.
  • Format declared correctly. RGB bytes declared as 8-bit gray decode nothing. The luminance source's format argument must match the actual buffer.
  • Pixels per module. Aim for 3 or more sensor pixels per QR module; below 2, sampling can no longer separate black modules from white. Huge modules waste time instead — the downscaling guide gives the cutoffs.
  • Blur and glare handled. Motion blur and specular hits break binarization even with perfect plumbing; the preprocessing guide covers both, and a tight ROI keeps the decode fast.

To pick the camera or reader for the station, browse the camera finder, start at the camera configurator, or send us the layout through the contact page. More decode-side guides sit under code reading.

Questions

Does ZXing.Net support .NET 6?

Yes. The core decoding engine targets modern .NET; only the Bitmap handling moved out of the base package into the ZXing.Net.Bindings.* packages. On a Windows-only application add the Windows.Compatibility binding, on a cross-platform application add the binding for your image library, or skip both and build the luminance source from your pixel buffer directly.

Can I keep using System.Drawing.Bitmap after upgrading to .NET 6?

On Windows, yes: add System.Drawing.Common plus the Windows.Compatibility binding and the old Bitmap-based decode compiles again. From .NET 6 System.Drawing.Common is supported on Windows only, so any plan that involves Linux containers or cross-platform builds should use the pixel-buffer route instead of Bitmap.

Why does Decode return null when the project compiles?

The types are right and the pixels are not. Decode a saved, known-good QR image first. If that works, check the buffer length against width × height × bytes per pixel, confirm the declared pixel format matches the actual buffer, then look at optics: at least 3 pixels per QR module, sharp focus, and no glare across the code.

How many bytes does one camera frame occupy?

Width × height × bytes per pixel. A 4000 × 3000 Mono8 frame is 12,000,000 bytes (12 MB); the same frame in RGB8 is 36,000,000 bytes (36 MB). Assert the buffer length before every decode call — a mismatch means padded rows or a different pixel format, and neither decodes correctly.