Skip to content

A Barcode and QR Contract Unlike OCR: Preserve Values, Formats, and Regions

A dark 3D workbench where OCR, object detection, and QR analysis split into separate result contracts before a final policy decision for a visitor pass
Several analyzers can read one image without producing the same kind of result. Preserve OCR text, detection facts, and barcode payloads at their own boundaries before application policy interprets them.

A visitor-pass image can contain a person’s name and company, a face, and an access QR code. OCR and a QR decoder may both return a text field, but the fields do not carry the same meaning.

OCR asks, “How much human-readable text did we obtain from this area?” Barcode and QR extraction asks, “Which payload and standard format did this symbol decode to?” OCR therefore cares about reading quality and page structure, while barcode extraction needs the value, symbology, provider, and optionally the location of the symbol.

This article follows the visitor-pass scenario in the bluetape4k-image Spring Boot Image Intelligence API. The important boundary is not a particular decoder. BarcodeReader returns provider-neutral facts, and VisitorPassPolicy interprets those facts as business rules.

The integrated example sends the same ImmutableImage to OCR, detection, and barcode paths. What they share is the input and request lifetime, not the meaning of their results.

PathRepresentative resultMeaning of “nothing found”Role of confidence
OCRtext, pageCountNo readable characters were foundMay describe text or region quality, depending on the provider
Object detectionlabel, category, confidence, detectorNo target object was foundA detector-reported score for policy to interpret
Barcode or QRtext, format, provider, optional regionNo code was decodedOften null because many decoders do not expose a stable score

Do not concatenate a barcode result as if it were an OCR string, or turn “no code found” into “the image is invalid.” Preserve each analysis result as its own contract, then let the application decide whether a visitor pass is allowed or needs review.

The provider-neutral API starts with a small BarcodeReader functional interface.

fun interface BarcodeReader {
fun readBarcodes(
image: ImmutableImage,
options: BarcodeOptions,
): List<BarcodeResult>
}

Whether the implementation calls a blocking decoder, a native runtime, or a remote service, callers receive BarcodeResult values. A coroutine path can use suspendExtractBarcodes with a selected dispatcher. If an application replaces ZXing later, its controller and policy do not need to depend on ZXing types.

Extraction constraints live in BarcodeOptions.

val qrOptions = BarcodeOptions(
formats = setOf(BarcodeFormat.QR_CODE),
tryHarder = true,
includeRawBytes = false,
)
val results = image.extractBarcodes(reader, qrOptions)

An empty formats set requests every format supported by the provider. tryHarder is a decoder hint, and includeRawBytes is an opt-in for retaining raw payload bytes only when they are needed. minimumConfidence filters results that have a score, but it keeps results whose confidence is null; many barcode libraries do not expose a stable score.

BarcodeResult keeps the facts callers need without requiring them to parse provider-specific strings again.

BarcodeResult(
text = "visitor:PASS-001",
format = BarcodeFormat.QR_CODE,
provider = BarcodeProviderIdentity(
name = "ZXing",
version = "3.5.4",
backend = "zxing-core",
metadata = mapOf("decoder" to "MultiFormatReader"),
),
region = BarcodeRegion(
points = listOf(
BarcodePoint(412.0, 188.0),
BarcodePoint(628.0, 188.0),
BarcodePoint(628.0, 404.0),
),
coordinateSpace = BarcodeCoordinateSpace.PIXEL,
),
)
FieldContract
textA non-blank decoded payload such as visitor:PASS-001.
formatA provider-neutral symbology such as QR_CODE, CODE_128, or EAN_13.
providerDiagnostic name, version, backend, and string metadata; it does not expose a provider object.
regionOptional finder/result points and an optional bounding box, together with PIXEL or NORMALIZED coordinates.
confidence, qualityOptional values populated only when the decoder provides them. Their absence is not a failure.
rawBytesAn optional payload retained only when requested and available.
rawBackendFormat, metadataBackend format and string metadata that a normalized mapping might otherwise lose.

The points in a BarcodeRegion do not have to form a closed polygon. A provider can return result points without a bounding box; a box is added only when a positive width and height can be calculated from at least two points. Normalized coordinates must stay within 0.0..1.0; pixel coordinates cannot be negative. These checks make the location safe for a later renderer or policy to interpret. They are data constraints, not rendering instructions.

The public API choice must be kept separate from the library model. The current integrated example exposes only these three fields in BarcodeResponse:

internal data class BarcodeResponse(
val text: String,
val format: BarcodeFormat,
val provider: String,
)

The underlying BarcodeResult can contain region, confidence, and rawBytes, but the current HTTP response is deliberately narrowed to text, format, and provider. Exposing a region in a public DTO is a separate API decision that must consider display needs, privacy, payload size, and version compatibility. This article says that the library model preserves a region; it does not claim that the current example returns one.

ZXing is an open-source barcode image-processing library implemented in Java that decodes multiple 1D and 2D formats. The images-barcode-zxing provider used here maps ZXing text, formats, result points, bounding boxes, raw bytes, and metadata into the provider-neutral BarcodeResult. ZXing decodes the payload; it does not decide whether a visitor pass is ALLOW or REJECT.

The integrated example’s barcode provider is a thin adapter that receives BarcodeReader and BarcodeOptions.

internal class ZxingBarcodeAnalysisProvider(
private val reader: BarcodeReader,
private val options: BarcodeOptions = BarcodeOptions(),
private val dispatcher: CoroutineDispatcher,
) : BarcodeAnalysisProvider {
override val id: String = "zxing"
override suspend fun analyze(image: ImmutableImage): List<BarcodeResult> =
image.suspendExtractBarcodes(reader, options, dispatcher)
}

The concrete ZxingBarcodeReader uses the ZXing dependency only inside the images-barcode-zxing module. A format from ZXing’s MultiFormatReader is mapped to BarcodeFormat, result points become a pixel BarcodeRegion, and the provider identity records ZXing, its version, zxing-core, and MultiFormatReader as string metadata. The core API and Spring service never need to know about ZXing’s Result or exception types.

The adapter also fixes the meaning of failures:

  • A ZXing NotFoundException means that no code was found, so the reader returns an empty list.
  • Checksum, format, reader, and runtime decode errors become BarcodeException(DECODE_FAILED).
  • If none of the requested formats is understood by ZXing, the reader returns UNSUPPORTED_FORMAT.
  • Malformed image bytes can be normalized as MALFORMED_INPUT.

“There was no barcode” and “the decoder could not run or decode” must not become the same exception or the same empty list.

Empty, Unavailable, and Failed are different responses

Section titled “Empty, Unavailable, and Failed are different responses”

The integrated example wraps analysis in these states:

StateMeaningBarcode example
CompletedThe reader ran and returned one or more BarcodeResult valuesA QR_CODE with visitor:PASS-001
EmptyThe reader ran but found no codeThe pass has no QR, or it is too small to decode
UnavailableThe provider is not configured or cannot be usedNo zxing bean or profile is available
FailedThe reader ran but hit an input, decode, or runtime errorDECODE_FAILED, UNSUPPORTED_FORMAT
A flow diagram showing BarcodeReader crossing into the images-barcode-zxing adapter, ZXing becoming normalized BarcodeResult facts, four distinct analysis states, and VisitorPassPolicy choosing ALLOW, REJECT, or MANUAL_REVIEW
BarcodeReader returns a provider-neutral contract and the adapter keeps ZXing types inside its boundary. Policy interprets facts and states as actions; the application owns HTTP, storage, and rendering side effects.

The current public response can be summarized like this:

{
"status": "COMPLETED",
"provider": "zxing",
"items": [
{
"text": "visitor:PASS-001",
"format": "QR_CODE",
"provider": "ZXing"
}
],
"reasonCode": null
}

When no code is found, the response is status: "EMPTY" with items: []. When the provider is missing it is UNAVAILABLE with a reasonCode; when decoding fails it is FAILED with a reasonCode. If every case is serialized as items: [], policy can no longer distinguish “there was no QR” from “the QR could not be checked.”

This is the same state-separation principle used for OCR in Part 3, but the business meaning of Empty depends on the analysis type. An empty OCR result may mean no readable text, while an empty visitor-pass barcode result may mean that a required access identifier is absent. A shared state wrapper does not make the final reason or action universal.

Decoding returns facts, not allow or reject

Section titled “Decoding returns facts, not allow or reject”

Visitor-pass policy interprets part of the decoded value as a business rule.

private fun BarcodeResult.isVisitorQr(): Boolean =
format == BarcodeFormat.QR_CODE && text.startsWith("visitor:")
val barcodes = results.barcode.completedValue().orEmpty()
if (results.barcode is AnalysisResult.Completed && barcodes.any { !it.isVisitorQr() }) {
return decision(VisitorPassAction.REJECT, "INVALID_VISITOR_QR")
}
if (results.barcode is AnalysisResult.Failed || results.barcode is AnalysisResult.Unavailable) {
return VisitorPassDecision(VisitorPassAction.MANUAL_REVIEW, listOf("BARCODE_DEGRADED"))
}

Decoding visitor:PASS-001 does not immediately produce ALLOW. The example policy first checks for sensitive regions and rejects a completed barcode that is not a visitor QR. If OCR, detection, or barcode is Failed or Unavailable, it chooses MANUAL_REVIEW instead of auto-approving. Only one face, one visitor QR, and non-blank OCR text together satisfy the final ALLOW conditions.

BarcodeReader does not return REJECT or MANUAL_REVIEW. It preserves payload, format, provider, and region, then normalizes execution state. HTTP status, original-image quarantine, QR expiry, the manual-review queue, and audit history belong to the application. Keeping those responsibilities separate lets decoder replacement and visitor-pass policy changes be tested independently.

The current source and example support tests for these contracts:

  • a no-code image returns emptyList() instead of an exception;
  • requested format hints map to BarcodeFormat;
  • decode and unsupported-format errors normalize to BarcodeException reasons;
  • provider identity and pixel regions survive in the result;
  • Empty, Unavailable, and Failed remain distinct in the public response and policy reasons;
  • VisitorPassPolicy does not auto-approve an invalid QR, a degraded lane, or a QR count mismatch.

The example does not promise the following:

Not guaranteedApplication decision
A confidence or quality score from every providerThreshold, recapture, and review rules when scores are absent
Multi-barcode results from every imageA decoder capability, result-order, and duplicate policy for multiple codes
A region in the current public DTOCoordinate exposure, privacy, payload size, and API versioning
Production quality for ZXingProvider versions, timeouts, process/native isolation, and observability

The current ZXing path focuses on the single-result MultiFormatReader path. If an image must yield several codes, design a separate multi-barcode capability and test contract. “Supports barcode” alone does not promise every symbology, orientation, damage level, or duplicate-result behavior.

Part 5 explains why OCR and barcode or QR paths need different result and empty-result contracts even when they start from the same image.

Part 6 will narrow parallel execution, cancellation propagation, and partial-failure aggregation into one response contract. Part 7 will connect input qualification, provider selection, and visitor-pass policy across the complete example.

This article is not a production barcode product or a promise of every decoder capability. It is a starting point for extending provider-neutral results and explicit states into application contracts.

Comments

Leave a note or reaction with your GitHub account.