// Package barcode wraps the system zbarimg binary (Debian package // zbar-tools) as a best-effort barcode decoding sidecar, consistent with // archivdms's general philosophy of shelling out to small CLI tools via // os/exec instead of adding CGO/native Go dependencies (see // internal/ocr/ocr.go package comment for the same rationale applied to // tesseract/poppler-utils). // // Barcode decoding failures (binary missing, non-zero exit because no // barcode was found, timeout) are never fatal to an upload: DecodeBarcodes // returns an empty slice and a nil error in the "nothing found / binary // missing" cases, matching how OCR failures are tolerated by callers. package barcode import ( "bytes" "context" "fmt" "os/exec" "strings" "time" ) const defaultTimeout = 20 * time.Second // DecodeBarcodes runs `zbarimg --raw -q ` and returns the // decoded raw values, one per line of output. If zbarimg is not installed, // this returns an empty slice and a nil error (tolerant, no hard-fail) — // callers that want to warn about a missing binary should check // exec.LookPath("zbarimg") themselves if they need to distinguish // "not installed" from "installed, nothing found". func DecodeBarcodes(ctx context.Context, imagePath string) ([]string, error) { if _, err := exec.LookPath("zbarimg"); err != nil { // Binary not present: tolerated, same as a missing tesseract binary. return nil, nil } cctx, cancel := context.WithTimeout(ctx, defaultTimeout) defer cancel() cmd := exec.CommandContext(cctx, "zbarimg", "--raw", "-q", imagePath) var out, stderr bytes.Buffer cmd.Stdout = &out cmd.Stderr = &stderr err := cmd.Run() if err != nil { // zbarimg exits non-zero (typically exit code 4) when no barcode is // found in the image at all — that is a normal, expected outcome for // the vast majority of scanned documents, not an error condition. if exitErr, ok := err.(*exec.ExitError); ok { _ = exitErr return nil, nil } return nil, fmt.Errorf("barcode: zbarimg failed: %w (%s)", err, strings.TrimSpace(stderr.String())) } var values []string for _, line := range strings.Split(out.String(), "\n") { line = strings.TrimSpace(line) if line != "" { values = append(values, line) } } return values, nil }