Barcodes
PdfPinata draws four kinds of code as vector graphics, so they stay sharp at any zoom and in print.
Three are linear barcodes: Code 3 of 9 (Code 39), interleaved 2 of 5, and OMR marks for mail-sorting
machines. The fourth is the ECC200 Data Matrix, a two-dimensional code. They are in the core
PdfPinata package, in the PdfPinata.Drawing.BarCodes namespace, and need no extra package.
A code is an object, not a single call. You create it with its text, its size and the direction it
runs in, then draw it at a point. Linear codes are drawn with XGraphics.DrawBarCode and the Data
Matrix with XGraphics.DrawMatrixCode.
Code 3 of 9
Code 3 of 9 takes the digits, the capital letters A to Z, the space and the characters
- . $ / + %. It adds its own start and stop characters, so do not add asterisks yourself.
// A bar code is an object rather than a call: it carries the text, the size it should
// occupy and the direction it runs in, and DrawBarCode paints it at a point. That point is
// the code's Anchor, which is its top left corner until it is told otherwise.
var code39 = new Code3of9Standard("PDFPINATA-2026", new XSize(230, 50))
{
TextLocation = TextLocation.Below
};
gfx1.DrawBarCode(code39, XBrushes.Black, codeText, new XPoint(50, 95));
The XSize is the box the code fills. The bars are scaled to fill its full width, so a longer text
gives thinner bars in the same box. The point passed to DrawBarCode is the top-left corner of the
box, unless you change the Anchor.
DrawBarCode has overloads with and without a brush and a font. The font is for the text under or
over the bars. If you give none, the code uses your font resolver's default font at one sixth of the
code's height.
Interleaved 2 of 5
Interleaved 2 of 5 takes digits only, and an even number of them, because each group of five bars carries two digits. It is denser than Code 3 of 9.
// Interleaved 2 of 5 packs two digits into every five bars, so it is denser than Code 39
// and takes digits only - and an even number of them, because of the interleaving.
var code25 = new Code2of5Interleaved("20260816", new XSize(200, 50))
{
TextLocation = TextLocation.Below
};
gfx1.DrawBarCode(code25, XBrushes.Black, codeText, new XPoint(320, 95));
If your number has an odd count of digits, add a leading zero.
Wide-to-narrow ratio
Both linear codes are made of wide and narrow bars. WideNarrowRatio sets how much wider a wide bar
is. It must be from 2 to 3, and the default is 2.6. A larger ratio is easier for a scanner to read and
needs more width for the same text.
var scaled = new Code3of9Standard("RATIO", new XSize(140, 42))
{
TextLocation = TextLocation.None,
WideNarrowRatio = ratio
};
gfx1.DrawBarCode(scaled, XBrushes.Black, codeText, new XPoint(left, 240));
Human-readable text
TextLocation puts the code's text above or below the bars, or leaves it out:
// Where the human-readable text goes, and whether it takes room from the bars or sits over
// them. The two "embedded" locations put it inside the code's own box.
left = 50;
foreach (var location in new[]
{
TextLocation.None, TextLocation.Above, TextLocation.Below,
TextLocation.AboveEmbedded, TextLocation.BelowEmbedded
})
{
var located = new Code3of9Standard("TEXT", new XSize(85, 55))
{
TextLocation = location
};
gfx1.DrawBarCode(located, XBrushes.Black, codeText, new XPoint(left, 350));
gfx1.DrawString(location.ToString(), note, XBrushes.DimGray, new XPoint(left, 425));
left += 100;
}
Above and Below draw the text outside the box. AboveEmbedded and BelowEmbedded draw it inside
the box. With any setting but None, the bars take four fifths of the box's height.
Direction and anchor
CodeDirection turns the code about the point you draw it at. The four values are LeftToRight
(the default), RightToLeft, TopToBottom and BottomToTop. You do not need to set a transform
yourself.
// Each of the four is given the same box and the same point. What differs is which way the
// bars run away from that point, which is why the point is marked on every one.
// The label goes on the side of the point the code does not occupy, which differs per
// direction - that being the whole of what this panel is about.
(CodeDirection Direction, double X, double Y, double LabelY)[] directions =
{
(CodeDirection.LeftToRight, 90, 150, -8),
(CodeDirection.RightToLeft, 400, 150, 14),
(CodeDirection.TopToBottom, 150, 250, -8),
(CodeDirection.BottomToTop, 420, 400, 14)
};
foreach (var each in directions)
{
var turned = new Code3of9Standard("TURN", new XSize(110, 34), each.Direction)
{
TextLocation = TextLocation.None
};
gfx2.DrawBarCode(turned, XBrushes.Black, codeText, new XPoint(each.X, each.Y));
gfx2.DrawEllipse(XBrushes.Firebrick, each.X - 2.5, each.Y - 2.5, 5, 5);
gfx2.DrawString(each.Direction.ToString(), note, XBrushes.Firebrick,
new XPoint(each.X + 6, each.Y + each.LabelY));
}
Anchor says which part of the code's box lands on that point. There are nine AnchorType values,
from TopLeft (the default) to BottomRight. MiddleCenter centres the code on the point.
var anchored = new Code3of9Standard("ABC", new XSize(80, 30))
{
TextLocation = TextLocation.None,
Anchor = anchors[index]
};
gfx2.DrawBarCode(anchored, XBrushes.Black, codeText, at);
OMR marks
OMR marks tell a mail-sorting machine how to handle a sheet. They are not read as characters. The text you give is parsed as a whole number, and the code draws one mark for each bit of it that is set, lowest bit first, after a synchronisation mark.
// OMR is not a bar code in the reading sense. Its "text" is parsed as a number and the
// marks drawn are that number's bits, which a sorting machine counts rather than decodes.
// The low bit is forced on by the renderer, so 1382 and 1383 draw the same marks.
left = 50;
foreach (var value in new[] { 1, 5, 1382 })
{
var omr = new CodeOmr(value.ToString(), new XSize(150, 40), CodeDirection.LeftToRight)
{
SynchronizeCode = true
};
gfx1.DrawBarCode(omr, XBrushes.Black, new XPoint(left, 480));
gfx1.DrawString($"OMR for {value}", note, XBrushes.DimGray, new XPoint(left, 535));
left += 165;
}
MakerDistance sets the space between marks, in points (12 by default, one sixth of an inch).
MakerThickness sets the thickness of each mark (1 by default).
Data Matrix
CodeDataMatrix draws an ECC200 Data Matrix. You give the text, the symbol size in modules (rows and
columns), and the size to draw it at:
// The symbol size is given in modules, and the encoder needs one large enough for the data
// plus its error correction. ECC200 fixes the legal sizes; one that is not on the list, or
// one too small for the text, is refused rather than silently truncated.
(string Code, int Size, string Note)[] matrices =
{
("PDFPINATA", 16, "16 x 16 modules"),
("PDFPINATA-2026-08-16", 22, "22 x 22, the same plus a date"),
("https://github.com/PinataLabs/PdfPinata", 32, "32 x 32, a whole URL")
};
left = 50;
foreach (var matrix in matrices)
{
var square = new CodeDataMatrix(matrix.Code, matrix.Size, matrix.Size,
new XSize(120, 120));
gfx3.DrawMatrixCode(square, XBrushes.Black, new XPoint(left, 110));
Caption(gfx3, left, 250, matrix.Note, "");
left += 165;
}
ECC200 allows only certain sizes. The square sizes run from 10 × 10 to 144 × 144 modules. The
rectangular sizes are 8 × 18, 8 × 32, 12 × 26, 12 × 36, 16 × 36 and 16 × 48. A size that is not on the
list, or too small for the text, throws an InvalidOperationException when you draw the symbol.
PdfPinata does not truncate the text or pick a larger symbol for you.
A rectangular symbol suits a label that is wide but short. Give it a box with the same proportions as the symbol, or the modules come out stretched:
// A symbol does not have to be square. ECC200 defines rectangular sizes too, which suit a
// label with width to spare and no height - a cable marker, a shelf edge.
(int Rows, int Columns, string Note)[] shapes =
{
(18, 18, "18 x 18, square"),
(8, 32, "8 x 32, rectangular"),
(12, 36, "12 x 36, rectangular")
};
left = 50;
foreach (var shape in shapes)
{
// The drawn size is exactly what it is asked for, so a rectangular symbol given a
// square box comes out with rectangular modules. Matching the box to the symbol's own
// proportions is the caller's job.
var height = 120.0 * shape.Rows / shape.Columns;
var oblong = new CodeDataMatrix("PDFPINATA", shape.Rows, shape.Columns,
new XSize(120, height));
gfx3.DrawMatrixCode(oblong, XBrushes.Black, new XPoint(left, 300));
Caption(gfx3, left, 440, shape.Note, "");
left += 165;
}
If you give no size, the symbol is drawn at 2 points per module.
Quiet zone
A scanner needs a blank margin, the quiet zone, around a symbol to find its edges. QuietZone sets it
in modules. The margin is drawn inside the size you give, in white, so a wider quiet zone makes the
symbol itself smaller:
// The quiet zone is the blank margin a reader needs to find the symbol's edges. It is
// counted in modules and drawn inside the size given, so a wider one shrinks the symbol
// rather than growing the code. The grey box is the size asked for.
left = 50;
foreach (var quiet in new[] { 0, 2, 5 })
{
var bordered = new CodeDataMatrix("QUIET", "", 16, 16, quiet,
new XSize(120, 120));
gfx3.DrawRectangle(new XPen(XColors.Firebrick, 0.5), left, 480, 120, 120);
gfx3.DrawMatrixCode(bordered, XBrushes.Black, new XPoint(left, 480));
Caption(gfx3, left, 620, $"QuietZone = {quiet}", "");
left += 165;
}
Things to know
- Invalid text throws. Code 3 of 9 and interleaved 2 of 5 throw an
ArgumentExceptionwhen you create the code, or set itsText, with characters they cannot encode. For interleaved 2 of 5, an odd number of digits is also invalid. - Linear codes need a size. A linear code created without an
XSizethrows when you draw it. - Leave your own quiet zone round linear codes. Code 3 of 9 and interleaved 2 of 5 fill their box edge to edge. Leave blank space on each side when you place them.
- No check digits. Neither linear code adds a check digit. If your scanner expects one, calculate it and add it to the text.
- Data Matrix uses ASCII encodation only.
DataMatrixEncodingalso namesC40,Text,X12,EDIFACTandBase256, but they throwNotImplementedException. ASCII can carry any text a Data Matrix can hold. It is less compact for long runs of one kind of character. - Data Matrix carries bytes, not Unicode. A character above U+00FF throws an
InvalidOperationExceptionwhen you draw the symbol. - A Data Matrix is never taller than it is wide. If you give more rows than columns, the two numbers are swapped.
- OMR marks always start with a synchronisation mark, and the lowest bit is always set. The
SynchronizeCodeproperty has no effect, and 1382 draws the same marks as 1383. Text that is not a number is read as zero. BarCode.FromTypecannot make a Data Matrix. It throws forCodeType.DataMatrix. Create aCodeDataMatrixand draw it withDrawMatrixCode.
See it in action
The Barcodes demo draws every code on three pages, with each ratio, text location, direction, anchor, symbol size and quiet zone side by side.
The full Barcodes demo
var document = new PdfDocument();
document.Info.Title = "Barcodes";
var heading = new XFont("Liberation Sans", 16, XFontStyle.Bold);
var label = new XFont("Liberation Sans", 9, XFontStyle.Bold);
var note = new XFont("Liberation Sans", 7.5);
var codeText = new XFont("Liberation Sans", 8);
void Caption(XGraphics gfx, double x, double y, string title, string detail)
{
gfx.DrawString(title, label, XBrushes.Black, new XPoint(x, y));
if (detail.Length > 0)
gfx.DrawString(detail, note, XBrushes.DimGray, new XPoint(x, y + 11));
}
// ----- page 1: the linear codes -----
var page1 = document.AddPage();
var gfx1 = XGraphics.FromPdfPage(page1);
gfx1.DrawString("Linear codes", heading, XBrushes.Black, new XPoint(50, 60));
// A bar code is an object rather than a call: it carries the text, the size it should
// occupy and the direction it runs in, and DrawBarCode paints it at a point. That point is
// the code's Anchor, which is its top left corner until it is told otherwise.
var code39 = new Code3of9Standard("PDFPINATA-2026", new XSize(230, 50))
{
TextLocation = TextLocation.Below
};
gfx1.DrawBarCode(code39, XBrushes.Black, codeText, new XPoint(50, 95));
Caption(gfx1, 50, 180, "Code 3 of 9 (Code 39)",
"0-9, A-Z and - . $ / + % space. Anything else throws, by name.");
// Interleaved 2 of 5 packs two digits into every five bars, so it is denser than Code 39
// and takes digits only - and an even number of them, because of the interleaving.
var code25 = new Code2of5Interleaved("20260816", new XSize(200, 50))
{
TextLocation = TextLocation.Below
};
gfx1.DrawBarCode(code25, XBrushes.Black, codeText, new XPoint(320, 95));
Caption(gfx1, 320, 180, "Interleaved 2 of 5",
"Digits, evenly many - two are carried per five bars.");
// The ratio of a wide bar to a narrow one. The standard allows anything from 2 to 3; the
// wider the ratio the easier a scanner finds it and the more paper it takes. The default
// here is 2.6, which is neither of the two numbers the standard actually names.
double left = 50;
foreach (var ratio in new[] { 2.0, 2.6, 3.0 })
{
var scaled = new Code3of9Standard("RATIO", new XSize(140, 42))
{
TextLocation = TextLocation.None,
WideNarrowRatio = ratio
};
gfx1.DrawBarCode(scaled, XBrushes.Black, codeText, new XPoint(left, 240));
#pragma warning disable S1244 // Exact on purpose: compared with the literal the value was taken from.
// ReSharper disable once CompareOfFloatsByEqualityOperator
gfx1.DrawString($"WideNarrowRatio {ratio:0.0}" + (ratio == 2.6 ? " (default)" : ""),
note, XBrushes.DimGray, new XPoint(left, 296));
#pragma warning restore S1244
left += 165;
}
Caption(gfx1, 50, 225, "The same five characters in the same box", "");
// Where the human-readable text goes, and whether it takes room from the bars or sits over
// them. The two "embedded" locations put it inside the code's own box.
left = 50;
foreach (var location in new[]
{
TextLocation.None, TextLocation.Above, TextLocation.Below,
TextLocation.AboveEmbedded, TextLocation.BelowEmbedded
})
{
var located = new Code3of9Standard("TEXT", new XSize(85, 55))
{
TextLocation = location
};
gfx1.DrawBarCode(located, XBrushes.Black, codeText, new XPoint(left, 350));
gfx1.DrawString(location.ToString(), note, XBrushes.DimGray, new XPoint(left, 425));
left += 100;
}
Caption(gfx1, 50, 335, "TextLocation", "");
// OMR is not a bar code in the reading sense. Its "text" is parsed as a number and the
// marks drawn are that number's bits, which a sorting machine counts rather than decodes.
// The low bit is forced on by the renderer, so 1382 and 1383 draw the same marks.
left = 50;
foreach (var value in new[] { 1, 5, 1382 })
{
var omr = new CodeOmr(value.ToString(), new XSize(150, 40), CodeDirection.LeftToRight)
{
SynchronizeCode = true
};
gfx1.DrawBarCode(omr, XBrushes.Black, new XPoint(left, 480));
gfx1.DrawString($"OMR for {value}", note, XBrushes.DimGray, new XPoint(left, 535));
left += 165;
}
Caption(gfx1, 50, 465, "OMR marks",
"The bits of a number, low bit first, behind one synchronisation mark.");
gfx1.DrawString("What each code accepts", label, XBrushes.Black, new XPoint(50, 590));
(string Code, string Accepts)[] rules =
{
("Code 3 of 9", "0-9, A-Z and - . $ / + % * space. Anything else throws ArgumentException."),
("Interleaved 2 of 5", "Digits, evenly many. Anything else throws ArgumentException."),
("OMR", "A number. Text that will not parse becomes zero, and the low bit is forced on."),
("Data matrix", "Any text, in ASCII encodation, within the symbol size asked for.")
};
double y = 610;
foreach (var rule in rules)
{
gfx1.DrawString(rule.Code, note, XBrushes.Black, new XPoint(50, y));
gfx1.DrawString(rule.Accepts, note, XBrushes.DimGray, new XPoint(160, y));
y += 14;
}
// ----- page 2: where a code lands and which way it runs -----
var page2 = document.AddPage();
var gfx2 = XGraphics.FromPdfPage(page2);
gfx2.DrawString("Placing a code", heading, XBrushes.Black, new XPoint(50, 60));
Caption(gfx2, 50, 90, "CodeDirection",
"The code turns about the point it is drawn at - the red dot - so it can run up a page "
+ "without the caller touching the transform.");
// Each of the four is given the same box and the same point. What differs is which way the
// bars run away from that point, which is why the point is marked on every one.
// The label goes on the side of the point the code does not occupy, which differs per
// direction - that being the whole of what this panel is about.
(CodeDirection Direction, double X, double Y, double LabelY)[] directions =
{
(CodeDirection.LeftToRight, 90, 150, -8),
(CodeDirection.RightToLeft, 400, 150, 14),
(CodeDirection.TopToBottom, 150, 250, -8),
(CodeDirection.BottomToTop, 420, 400, 14)
};
foreach (var each in directions)
{
var turned = new Code3of9Standard("TURN", new XSize(110, 34), each.Direction)
{
TextLocation = TextLocation.None
};
gfx2.DrawBarCode(turned, XBrushes.Black, codeText, new XPoint(each.X, each.Y));
gfx2.DrawEllipse(XBrushes.Firebrick, each.X - 2.5, each.Y - 2.5, 5, 5);
gfx2.DrawString(each.Direction.ToString(), note, XBrushes.Firebrick,
new XPoint(each.X + 6, each.Y + each.LabelY));
}
Caption(gfx2, 50, 450, "AnchorType",
"Which part of the code lands on the point given. The default is TopLeft.");
AnchorType[] anchors =
{
AnchorType.TopLeft, AnchorType.TopCenter, AnchorType.TopRight,
AnchorType.MiddleLeft, AnchorType.MiddleCenter, AnchorType.MiddleRight,
AnchorType.BottomLeft, AnchorType.BottomCenter, AnchorType.BottomRight
};
for (var index = 0; index < anchors.Length; index++)
{
// ReSharper disable once PossibleLossOfFraction
var at = new XPoint(140 + index % 3 * 170, 520 + index / 3 * 100);
var anchored = new Code3of9Standard("ABC", new XSize(80, 30))
{
TextLocation = TextLocation.None,
Anchor = anchors[index]
};
gfx2.DrawBarCode(anchored, XBrushes.Black, codeText, at);
// Drawn after the code so the point is not buried under the bars. The label clears the
// full height of the code below the point, whichever way the anchor put it.
gfx2.DrawEllipse(XBrushes.Firebrick, at.X - 2.5, at.Y - 2.5, 5, 5);
gfx2.DrawString(anchors[index].ToString(), note, XBrushes.DimGray,
new XRect(at.X - 85, at.Y + 40, 170, 10), XStringFormats.TopCenter);
}
// ----- page 3: the data matrix -----
var page3 = document.AddPage();
var gfx3 = XGraphics.FromPdfPage(page3);
gfx3.DrawString("ECC200 data matrix", heading, XBrushes.Black, new XPoint(50, 60));
// DrawString does not wrap - it draws one line and runs off the page if the line is too
// long for it. Anything that has to fit a measure goes through XTextFormatter instead.
var prose = new XTextFormatter(gfx3);
prose.DrawString(
"A data matrix is a MatrixCode rather than a BarCode - a different base class, and "
+ "DrawMatrixCode rather than DrawBarCode. BarCode.FromType says so if asked for one.",
note, XBrushes.DimGray, new XRect(50, 74, 495, 30));
// The symbol size is given in modules, and the encoder needs one large enough for the data
// plus its error correction. ECC200 fixes the legal sizes; one that is not on the list, or
// one too small for the text, is refused rather than silently truncated.
(string Code, int Size, string Note)[] matrices =
{
("PDFPINATA", 16, "16 x 16 modules"),
("PDFPINATA-2026-08-16", 22, "22 x 22, the same plus a date"),
("https://github.com/PinataLabs/PdfPinata", 32, "32 x 32, a whole URL")
};
left = 50;
foreach (var matrix in matrices)
{
var square = new CodeDataMatrix(matrix.Code, matrix.Size, matrix.Size,
new XSize(120, 120));
gfx3.DrawMatrixCode(square, XBrushes.Black, new XPoint(left, 110));
Caption(gfx3, left, 250, matrix.Note, "");
left += 165;
}
// A symbol does not have to be square. ECC200 defines rectangular sizes too, which suit a
// label with width to spare and no height - a cable marker, a shelf edge.
(int Rows, int Columns, string Note)[] shapes =
{
(18, 18, "18 x 18, square"),
(8, 32, "8 x 32, rectangular"),
(12, 36, "12 x 36, rectangular")
};
left = 50;
foreach (var shape in shapes)
{
// The drawn size is exactly what it is asked for, so a rectangular symbol given a
// square box comes out with rectangular modules. Matching the box to the symbol's own
// proportions is the caller's job.
var height = 120.0 * shape.Rows / shape.Columns;
var oblong = new CodeDataMatrix("PDFPINATA", shape.Rows, shape.Columns,
new XSize(120, height));
gfx3.DrawMatrixCode(oblong, XBrushes.Black, new XPoint(left, 300));
Caption(gfx3, left, 440, shape.Note, "");
left += 165;
}
// The quiet zone is the blank margin a reader needs to find the symbol's edges. It is
// counted in modules and drawn inside the size given, so a wider one shrinks the symbol
// rather than growing the code. The grey box is the size asked for.
left = 50;
foreach (var quiet in new[] { 0, 2, 5 })
{
var bordered = new CodeDataMatrix("QUIET", "", 16, 16, quiet,
new XSize(120, 120));
gfx3.DrawRectangle(new XPen(XColors.Firebrick, 0.5), left, 480, 120, 120);
gfx3.DrawMatrixCode(bordered, XBrushes.Black, new XPoint(left, 480));
Caption(gfx3, left, 620, $"QuietZone = {quiet}", "");
left += 165;
}
gfx3.DrawString("Encodation: ASCII only", label, XBrushes.Black, new XPoint(50, 660));
prose.DrawString(
"DataMatrixEncoding names C40, Text, X12, Edifact and Base256 beside it, and every one "
+ "of them throws NotImplementedException rather than encoding wrongly. ASCII carries "
+ "anything a data matrix can hold; it is only less dense over a long run of one case.",
note, XBrushes.DimGray, new XRect(50, 668, 495, 40));