Code readingHow-to

Generate a DataMatrix code as PNG bytes or pixel data in .NET

Generate a DataMatrix code in .NET 7 as PNG bytes or a raw pixel buffer with ZXing.Net, without System.Drawing, and check the output still decodes.

LumaSpec Vision9 min read Share on LinkedIn

On this page 10 sections
  1. 01What you need
  2. 02Choose the output: pixel buffer, PNG bytes or SVG
  3. 03Generate the module matrix, then expand it to pixels
  4. 04Wrap the buffer in PNG bytes
  5. 05The shortcut: BarcodeWriterPixelData
  6. 06Worked example: the 300 × 300 migration call, sized end to end
  7. 07Read the bytes back before you trust them
  8. 08Common mistakes and the checks that catch them
  9. 09Where this leaves you
  10. 10Questions

When a DataMatrix generator built on System.Drawing meets a .NET 7 migration, it stops: the drawing stack is Windows-only from .NET 6 on, and your old generator drew through it. The fix is to skip raster drawing entirely. ZXing.Net's DataMatrixWriter hands you the symbol as a module matrix, and a few lines turn that matrix into the two things engineers actually ask for: a raw pixel buffer, or PNG bytes wrapped in a base64 string. This guide builds both paths end to end, sizes them for a 300 × 300 image, and shows the checks that catch a flipped or blank output before it reaches your line.

What you need

  • The ZXing.Net package from NuGet. Its core encoder is pure .NET: no GDI+ and no platform-specific build.
  • .NET 7 or later, on Windows, Linux or in a container.
  • The payload string that goes into the symbol.
  • The consumer's expectation: raw pixels or an encoded file. The byte math differs by more than a factor of a hundred, so settle it first.

The old path failed twice over. System.Drawing.Common draws only on Windows from .NET 6 on. And its indexed pixel formats — Format8bppIndexed is the one that bites in barcode work — make a plain buffer copy awkward, because you first have to walk palette-indexed bytes. The matrix path below never creates an indexed image, so there is nothing to convert.

Choose the output: pixel buffer, PNG bytes or SVG

Three shapes cover every request to hand over a DataMatrix as bytes. Decide by consumer, not by habit.

Output What you hold A 300 × 300 image costs Best for
8-bit gray buffer width, height, one byte per pixel 90,000 bytes, or 120,000 base64 characters decoders fed raw pixels, print heads, display code
PNG bytes an encoded file you can store or send anywhere 300-800 bytes, roughly 1 kB as base64 APIs, databases, message queues, raster decoders
SVG markup text you UTF-8-encode yourself varies with the renderer; compactness is not its point print masters that must scale without softening

Send PNG bytes whenever the consumer accepts a file: an API body, a database field, a message queue. Hold the raw buffer when the other end indexes pixel values directly: a decoder working on in-memory arrays, a print head, display code. Pick SVG text for label masters that must scale cleanly. Base64 is not a fourth option; it is the transport wrapper over any of the three.

Generate the module matrix, then expand it to pixels

  1. Reference ZXing.Net in the project that used to call DataMatrix.Net.
  2. Create the writer: new ZXing.DataMatrixWriter().
  3. Ask for the symbol at the size you want. The call writer.encode(content, BarcodeFormat.DATA_MATRIX, 300, 300) returns a module matrix that reports its width and height and answers true for every black module, false for every white one. DataMatrixWriter, BarcodeFormat.DATA_MATRIX and this encode overload are the library's own API; the width and height fields and the [x, y] module look-up are its public surface in current builds.
  4. Walk the matrix row by row into one byte per pixel: 0 for black, 255 for white.
  5. Call Convert.ToBase64String on the buffer when the caller wants a string.
using ZXing;

var content = "LUMA-4471-B";                        // the payload that goes into the symbol
var writer  = new DataMatrixWriter();
var matrix  = writer.encode(content, BarcodeFormat.DATA_MATRIX, 300, 300);

var gray = new byte[matrix.Width * matrix.Height];  // 8-bit grayscale: 300 * 300 * 1 = 90,000 bytes
for (int y = 0; y < matrix.Height; y++)
    for (int x = 0; x < matrix.Width; x++)
        gray[y * matrix.Width + x] = (byte)(matrix[x, y] ? 0 : 255);

string base64 = Convert.ToBase64String(gray);       // 90,000 bytes in, 120,000 characters out

The matrix comes back at the requested 300 × 300. The library scales the symbol up in whole-module steps and centers it, so the pixel pitch follows the symbol size your payload produces. The same integer-scaling behavior, worked through for QR, is in the QR code quiet zone and integer module scaling guide. If you also build 1D test targets, the EAN-13 bitmap guide follows the same buffer discipline.

Wrap the buffer in PNG bytes

A PNG is your buffer plus a header, per-row filter bytes and compressed streams, so any encoder that accepts a byte buffer with width and height works.

  1. Pick a pure .NET PNG writer from NuGet, or an imaging library such as SkiaSharp asked for PNG output. Nothing that requires a System.Drawing Image.
  2. Hand it the gray buffer, the width and height, and the 8-bit grayscale format.
  3. Take the returned byte array. That array is the PNG file, byte for byte.
  4. Call Convert.ToBase64String on it when the caller wants a string.
// pngEncoder and gray8 stand in for the names your chosen encoder uses:
// each takes buffer, width, height and format, and returns bytes
byte[] png    = pngEncoder.Encode(gray, 300, 300, gray8);
string base64 = Convert.ToBase64String(png);

The size collapse is the payoff. A DataMatrix row is long runs of a single byte value, and PNG compresses each row before storing it, so the 90,000-byte buffer typically lands between 300 and 800 bytes of PNG. If the receiving decoder rejects those bytes, the usual cause is a mismatch between file bytes and raw pixels — the same mismatch the QR code image data type guide untangles.

The shortcut: BarcodeWriterPixelData

ZXing.Net ships a class that merges encode and render in one call: ZXing.BarcodeWriterPixelData.

  1. Construct ZXing.BarcodeWriterPixelData.
  2. Set its format to BarcodeFormat.DATA_MATRIX, and put 300 and 300 into its options' width and height; add a margin of four modules to keep a quiet zone inside the image.
  3. Call its Write method with the content string.
  4. Read the returned pixel object: it carries the byte buffer with its width, height and pixel format. One byte per pixel means grayscale output, three means RGB, four means 32-bit.
  5. Call Convert.ToBase64String on the buffer when the string is the goal.

The class runs the scaling loop from the previous section for you. Keep the raw-matrix path in reserve anyway: it lets you invert, crop or threshold pixels before anything encodes them, and it shows exactly where the bytes come from.

Worked example: the 300 × 300 migration call, sized end to end

Inputs, and where each comes from:

  • Payload LUMA-4471-B, a part serial in the shape these generators usually carry.
  • Requested output 300 × 300 pixels — the size your pre-migration two-liner already used.
  • Buffer format 8-bit grayscale, one byte per pixel — the buffer this guide builds.
  • base64 rule: four characters per three bytes, no line breaks.
  • Symbol: a short alphanumeric payload yields a small square DataMatrix; take 24 × 24 modules with a 4-module margin, 32 modules across.

The rules in words: the buffer costs width times height times bytes per pixel, and base64 turns each group of three bytes into four characters.

buffer bytes = width px × height px × bytes per pixel base64 characters = 4 × ceil(buffer bytes / 3)

The arithmetic:

  • Buffer: 300 px × 300 px × 1 byte = 90,000 bytes.
  • base64: 90,000 / 3 = 30,000 groups; 30,000 × 4 = 120,000 characters, with no '=' padding because 90,000 divides by three exactly.
  • Module pitch: 300 px / 32 modules ≈ 9.4 px per module.
  • PNG: the same 90,000 bytes compressed, typically 300-800 bytes; base64 of that is 400-1,100 characters, about 1 kB of text.

Decision: send the PNG. About 1 kB of text moves instead of 120,000 characters — better than a hundred times smaller on the wire. Keep the 90,000-byte raw buffer only when the consumer indexes pixels directly: a decoder fed in-memory arrays, a print head, or a display. The threshold reads: if the receiver accepts a file format, PNG wins; raw only when it must have pixels.

Read the bytes back before you trust them

Generation code hides its bugs well: a flipped or inverted buffer still looks like a perfect symbol. Two checks catch everything.

First, decode it. Feed the PNG through the production pipeline — the same one described in camera barcode decoding: from image to validated data — and expect the payload on the first frame. A freshly generated symbol that fails to decode points at your buffer math, not at the content.

Second, look at the finder pattern. A DataMatrix prints a solid L along its left and bottom edges and an alternating dashed border on top and right. An L along the top means the rows are in bottom-up order: flip the row loop. An all-white or all-black image means the polarity or the bytes-per-pixel scaling is off.

For a line-side target, put the PNG on a screen or print it at 300 dpi or better, and read it with real hardware — any of these fixed code readers:

Fixed code readersHikrobot MV-ID3013PM-12M-WBN 12mm focal length 1.3 MP Compact Industrial Code ReaderMV-ID3013PM-12M-WBN
Interface
Fast Ethernet
Resolution
Up to 3 MP
$1,979.00View part
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
$920.00View part
Fixed code readersCompact fixed industrial barcode reader, 1.2 MP, USB 2.0, 60-120 mm reading distance (LS-CRD-0047)LS-CRD-0047
Resolution
Up to 3 MP
Shutter
Global
$559.00View part

The MV-ID3013PM-12M-WBN pairs a 12 mm focal length with a 1.3 MP sensor on Fast Ethernet. The LS-CRD-0028 reads 2560 × 2048 frames at 30 fps and up to 60 codes per second through an 8 mm lens. The LS-CRD-0047 reads at 60-120 mm over USB 2.0, which suits a bench fixture for print tests. Wire the reader to a part-present trigger instead of free-run; the GPIO trigger guide covers the pin and the setting. Reading distance versus module size follows the same geometry as any 1D symbol, worked out in the Code 128 module size guide.

Common mistakes and the checks that catch them

  • All-white or all-black output: the polarity or the bytes-per-pixel scaling is wrong. gray[0] must read 255 (top-left margin) and the buffer length must equal width × height.
  • Perfect on screen, dead on the line: the symbol fell to one or two pixels per module after scaling. Re-request a larger image rather than letting the consumer shrink it.
  • Decoder rejects the bytes: it expects a file's bytes, not a raw buffer, or the reverse. Match the consumer before you touch the generator.
  • Two '=' at the end of the base64 string: correct padding for an odd byte count, not corruption.
  • Reaching for System.Drawing just for the encoder: it draws only on Windows from .NET 6 on, so the container build breaks. Keep the generator pure .NET.

Where this leaves you

Pick the output by consumer, generate the matrix, and decode your own bytes back once before the first production run. When the check moves from the bench to the line, the camera finder narrows readers by interface, resolution and mount, and cameras that decode in-device are listed under smart cameras. More on the reading side sits on the code-reading topic page.

Build the whole station

The configurator matches a camera, lens, light, light controller and cables to each other, step by step, and lists only parts that fit what you already picked.

Start with the camera

If the reading side needs a second opinion, the contact page is the direct route to us.

Questions

Why did DataMatrix generation break after migrating to .NET 7?

System.Drawing.Common, the renderer most older generators drew through, runs only on Windows from .NET 6 on and throws on Linux or in containers. A .NET 7 build that targets cross-platform deployment, or that no longer references the package, loses generation with it. ZXing.Net's DataMatrixWriter computes the module matrix in pure .NET and never touches GDI+, so the same generation code runs on Windows, Linux and containers.

How large is the base64 string in other pixel formats?

Scale the arithmetic by bytes per pixel. A 300 × 300 image is 90,000 bytes as 8-bit gray and 120,000 base64 characters. As 24-bit RGB the buffer is 270,000 bytes, which base64 inflates to 360,000 characters; as 32-bit RGBA it is 360,000 bytes and 480,000 characters. Odd pixel counts add '=' padding: a 90,601-byte buffer encodes to 120,804 characters ending in two '='.

Can I render the symbol as SVG instead of a raster image?

Yes. The library ships ZXing.BarcodeWriterSvg, which returns the symbol as SVG markup text instead of pixels. UTF-8-encode that text and you have a byte array and a base64 string with no image encoder involved. The markup scales to any print size, which suits label masters; use PNG or the raw buffer when the receiving decoder only ingests raster input.

How many pixels per module should the generated image have?

Treat 3 pixels per module as the floor for a target shown on a screen, and allow more for a printed label read under line lighting. A 24 × 24 symbol with a 4-module margin spans 32 modules, so a 300 × 300 request gives about 9.4 pixels per module. If your payload produces a 44 × 44 symbol, re-request 500 × 500 to stay near the same pitch: 500 / 52 ≈ 9.6 pixels per module.