Table of Contents

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

  1. 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.
  2. 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

Share