Split Scanned Batches by Barcode
Mailrooms and scanning stations have marked documents with barcodes for decades: a separator sheet slipped between two documents, a cover sheet that names the file, a code stamped on every page of a claim. When a batch carries those marks, splitting it is a matter of reading them, not of interpreting the content. LM-Kit.NET's BarcodeSplitting class (namespace LMKit.Extraction.Barcodes) reads every page of a PDF, a multi-page TIFF or an image, finds the barcodes that delimit documents, and writes each document to its own file. No model is loaded: the same batch always splits the same way, and a page costs milliseconds.
For batches without marks, Automatically Split Multi-Document PDFs with AI Vision finds the boundaries from the content instead.
Why Barcode Splitting Matters
- Existing scanning workflows. Operators already print separator sheets and cover sheets. Barcode splitting turns those batches into individual documents with zero ambiguity and no GPU.
- Named, routed output. The barcode carries a value (a case number, a customer id) that names the output file and routes the document to the right folder or pipeline.
Supported symbologies: QR Code, Micro QR, rMQR, Data Matrix, PDF417, Aztec, MaxiCode, Code 128, Code 39, Code 93, Codabar, ITF, EAN/UPC, DataBar and Telepen.
Prerequisites
| Requirement | Minimum |
|---|---|
| .NET SDK | 8.0+ |
| GPU / VRAM | None (deterministic image analysis) |
Step 1: Create the Project
dotnet new console -n BarcodeSplitter
cd BarcodeSplitter
dotnet add package LM-Kit.NET
Step 2: Split a Batch on Separator Sheets
The default mode, BarcodeSplitMode.SeparatorPage, treats every page carrying a barcode as a sheet standing between two documents, and removes it from the output.
using LMKit.Extraction.Barcodes;
var splitter = new BarcodeSplitting(new BarcodeSplittingOptions
{
Mode = BarcodeSplitMode.SeparatorPage,
RemoveBlankPageAfterSeparator = true, // duplex scans: drop the blank back of each sheet
FileNameTemplate = "{source}_{index:000}"
});
BarcodeSplittingResult result = splitter.SplitToFiles("batch_scan.pdf", "output");
Console.WriteLine($"{result.DocumentCount} document(s) from {result.PageCount} page(s) in {result.Elapsed.TotalMilliseconds:F0} ms");
foreach (BarcodeSegment segment in result.Segments)
{
Console.WriteLine($" {segment.Label}: pages {segment.StartPage}-{segment.EndPage} -> {segment.OutputPath}");
}
Console.WriteLine($"Separator pages: {string.Join(", ", result.SeparatorPages)}");
A PDF source keeps its pages as they are in each output (text, fonts and vectors intact, never rasterized). Set OutputFormat = BarcodeSplitOutputFormat.Tiff to write multi-page TIFFs, or SameAsSource to keep a TIFF batch in TIFF.
What becomes of the separator sheet is SeparatorHandling: Remove (default), KeepAsFirstPage or KeepAsLastPage.
Step 3: Choose What a Barcode Means
BarcodeSplitMode |
The barcode... | Typical batch |
|---|---|---|
SeparatorPage (default) |
stands on a sheet between two documents | separator sheets printed once and reused |
FirstPageOfDocument |
marks a document's first page, which stays in it | cover sheets, stamped first pages |
LastPageOfDocument |
marks a document's last page | a closing slip after each document |
ValueChange |
carries a value; a new document starts when it changes | every page stamped with its file number |
GroupByValue |
carries a value; all pages with that value form one document | a mixed stack, pages out of order |
Pages no barcode accounts for (before the first one, or after the last one in LastPageOfDocument mode) follow LeadingPages: KeepAsDocument (default), Discard, or MergeIntoNearestDocument.
var options = new BarcodeSplittingOptions
{
Mode = BarcodeSplitMode.GroupByValue,
LeadingPages = LeadingPagesHandling.Discard,
RemoveBlankPages = true
};
With GroupByValue or removed blank pages, a document's pages are not always consecutive: BarcodeSegment.PageIndexes lists them exactly (0-based) and IsContiguous tells whether they form one range.
Step 4: Count Only the Right Barcodes
A batch carries more codes than the ones that split it: an invoice's EAN, a parcel label, a payment slip. A BarcodeMatchRule keeps them out. A barcode counts when it passes every rule that is set.
using LMKit.Graphics.Geometry;
var options = new BarcodeSplittingOptions
{
Match = new BarcodeMatchRule
{
Formats = new[] { BarcodeFormat.QRCode }, // only QR codes (also faster)
Pattern = @"^SEP-", // whose text starts with SEP-
SearchArea = Rectangle.FromSize(0.6, 0, 0.4, 0.25) // in the top right corner of the page
}
};
| Property | Purpose |
|---|---|
Formats |
Symbologies that count; groups such as BarcodeFormat.AllLinear and AllMatrix are accepted |
Values / ValueComparison |
Exact values that count ("SEPARATOR", "NEXT-DOC") |
Pattern |
A regular expression the text must match |
SearchArea |
The part of the page searched, as fractions of its width and height; only that part is scanned |
MinimumSymbolSize |
Rejects symbols smaller than this, in raster pixels along their longer side |
Predicate |
Any custom test on a BarcodeDetection and its page index |
Barcodes that are found but rejected are still reported in each page's IgnoredBarcodes, so a rule is tuned on a real batch instead of guessed:
foreach (BarcodePage page in result.Pages)
{
foreach (BarcodeDetection ignored in page.IgnoredBarcodes)
Console.WriteLine($"Page {page.PageNumber}: ignored {ignored}");
}
Step 5: Name Documents After Their Barcode
The barcode that delimits a document gives it a value: the key of ValueChange and GroupByValue, and its file name through {value} in FileNameTemplate. ValuePattern extracts it from a longer text with a named group value (or set ValueSelector to compute it in code):
var splitter = new BarcodeSplitting(new BarcodeSplittingOptions
{
Mode = BarcodeSplitMode.FirstPageOfDocument,
ValuePattern = @"CASE-(?<value>\d+)",
FileNameTemplate = "case_{value}",
FileConflict = FileConflictBehavior.MakeUnique
});
BarcodeSplittingResult result = splitter.SplitToFiles("claims.tif", "cases");
Template tokens: {source}, {index} ({index:000} pads it), {value}, {start}, {end}, {pages} and {format}. A split never overwrites a file it wrote itself.
Step 6: Keep Documents in Memory or Stream Them
SplitToFiles is one of four outputs, each available for an Attachment, a path, bytes or a stream, synchronously or with Async:
| Method | Output |
|---|---|
Split |
Finds the documents only; nothing is written |
SplitToAttachments |
Each document as an Attachment (result.Documents), ready for OCR, classification or extraction |
SplitToFiles |
Each document written to a folder (result.Files), with optional IProgress<PdfSplitterProgressEventArgs> |
SplitToStreams |
Each document written to a stream you open per BarcodeSegment |
BarcodeSplittingResult result = await splitter.SplitToAttachmentsAsync("batch_scan.pdf");
foreach (var document in result.Documents)
{
// Hand each document to the next stage of the pipeline.
}
result.ToDocumentSplittingResult() converts the result to the DocumentSplittingResult the AI splitter returns, so the pipelines built on one accept the other.
Step 7: Watch and Cancel
PageScanned fires after each page is scanned and PartWritten after each document is written; both can stop the split through Cancel.
splitter.PageScanned += (_, e) =>
Console.Write($"\rScanned {e.ScannedCount}/{e.TotalToScan} ");
splitter.PartWritten += (_, e) =>
Console.WriteLine($"\nWrote {e.Segment.OutputName} ({e.PartIndex + 1}/{e.TotalParts})");
On large batches, PagesToScan (a 1-based range such as "1-10, 15") limits the pages scanned, RenderDpi (default 200) can rise to 300 for small or dense codes, and MaxDegreeOfParallelism sets how many pages are decoded at once.
Read Barcodes Without Splitting
BarcodeReader returns every barcode on an image or on a document's pages, with its format, text, raw bytes, position and orientation. Use it to route a document by its code, to inspect a batch before choosing a split rule, or to read labels, tickets and payment slips.
var reader = new BarcodeReader(new BarcodeReaderOptions
{
Formats = new[] { BarcodeFormat.AllMatrix }
});
foreach (BarcodePage page in reader.ReadPages("batch_scan.pdf", pageRange: "1-5"))
{
foreach (BarcodeDetection barcode in page.Barcodes)
Console.WriteLine($"Page {page.PageNumber}: {barcode.FormatName} '{barcode.Text}' at {barcode.Bounds}");
}
PdfSplitter Shortcuts
For a one-liner, PdfSplitter (namespace LMKit.Document.Pdf) exposes the same engine:
using LMKit.Data;
using LMKit.Document.Pdf;
var source = new Attachment("batch_scan.pdf");
List<int> separatorPages = PdfSplitter.DetectBarcodePages(source);
List<Attachment> documents = PdfSplitter.SplitOnBarcodes(source);
Common Issues
| Problem | Cause | Fix |
|---|---|---|
| The whole batch comes back as one document | No barcode passes the match rule | Inspect IgnoredBarcodes, then loosen Formats, Pattern or SearchArea |
| Extra documents split on invoice or parcel codes | Every valid barcode counts by default | Restrict Formats and add a Pattern or SearchArea |
| Small or dense codes are missed | Pages rendered too coarsely | Raise RenderDpi to 300 |
| A blank page opens each document | Duplex scan of single-sided separator sheets | Set RemoveBlankPageAfterSeparator = true |
A page appears in FailedPages |
That page could not be read | It stays content and never splits the batch; check the source page |
LM-Kit One and Agents
LM-Kit One serves the same engine over HTTP (POST /lmkit/v1/barcode-splitting and POST /lmkit/v1/barcode-reading) and to agents through the MCP tools document_split_by_barcodes and document_read_barcodes.
Next Steps
- Automatically Split Multi-Document PDFs with AI Vision: split batches that carry no barcodes, from their content.
- Build a Classification and Extraction Pipeline: classify each split document and extract structured data from it.
- Process Scanned Documents with OCR: make each split document searchable.
- Barcode Document Splitting Demo: a console application that splits a batch and reports every page.