Search code examples
swift

How can I generate a barcode using a specific barcode symbology (codabar)?


I have a string for which I want to generate a barcode using a specific barcode symbology: codabar.

I can generate a barcode, but it uses the wrong barcode symbology causing it to be read incorrectly by scanners.

func generateBarcode(from string: String) -> Image {
    let context = CIContext()
    let generator = CIFilter.code128BarcodeGenerator()
    generator.message = Data(string.utf8)

    if let outputImage = generator.outputImage,
        let cgImage = context.createCGImage(outputImage, from: outputImage.extent) {
        let uiImage = UIImage(cgImage: cgImage)
        return Image(uiImage: uiImage)
    }

    return Image(systemName: "barcode")
}

I have a barcode font and tried to convert the string using that, but that doesn't work. (there is a small chance the font I'm using is incomplete or otherwise "wrong", I did get an error loading it when trying some other stuff)

Text("A25490001104784B").font(.custom("codabar", size: 18))

How can I generate a barcode using a specific barcode symbology (codabar)?


Solution

  • Based on the answer in this question I tried to generate Codabar barcodes using the RSBarcodes_Swift library. I didn't get it working using the usage example from the README, it gave an No code generator selected. message.

    RSUnifiedCodeGenerator.shared.generateCode("2166529V", machineReadableCodeObjectType: AVMetadataObject.ObjectType.codabar.rawValue)
    

    This is because the Codabar generator is not listed in the RSUnifiedCodeGenerator.generateCode method.

    Instead of trying to use the RSCodaBarGenerator directly I decided to not use the library and instead copy and alter the code necessary to generate Codabar barcodes.

    import Foundation
    import UIKit
    import AVFoundation
    
    open class CodabarGenerator {
        private let codabarAlphabetString = "0123456789-$:/.+ABCD"
    
        // swiftlint:disable:next cyclomatic_complexity
        private func encodeCharacterString(_ characterString: String) -> String {
            switch characterString {
            case "0":
                return "1010100110"
            case "1":
                return "1010110010"
            case "2":
                return "1010010110"
            case "3":
                return "1100101010"
            case "4":
                return "1011010010"
            case "5":
                return "1101010010"
            case "6":
                return "1001010110"
            case "7":
                return "1001011010"
            case "8":
                return "1001101010"
            case "9":
                return "1101001010"
            case "ー":
                return "1010011010"
            case "$":
                return "1011001010"
            case ":":
                return "11010110110"
            case "/":
                return "11011010110"
            case ".":
                return "11011011010"
            case "+":
                return "10110110110"
            case "A":
                return "10110010010"
            case "B":
                return "10010010110"
            case "C":
                return "10100100110"
            case "D":
                return "10100110010"
            default:
                fatalError("Invalid character \(characterString) for Codabar encoding")
            }
        }
    
        func isValid(_ contents: String) -> Bool {
            if contents.count > 0
                && contents.range(of: "A") == nil
                && contents.range(of: "B") == nil
                && contents.range(of: "C") == nil
                && contents.range(of: "D") == nil {
                for character in contents {
                    let location = codabarAlphabetString.firstIndex(of: character)
                    if location == nil {
                        return false
                    }
                }
                return true
            } else {
                return false
            }
        }
    
        func initiator() -> String {
            self.encodeCharacterString("A")
        }
    
        func terminator() -> String {
            self.encodeCharacterString("B")
        }
    
        func barcode(_ contents: String) -> String {
            var barcode = ""
            for character in contents {
                barcode += self.encodeCharacterString(String(character))
            }
            return initiator() + barcode + terminator()
        }
    
        let barcodeDefaultHeight = 28
        var fillColor: UIColor = UIColor.white
        var strokeColor: UIColor = UIColor.black
    
        func drawCompleteBarcode(_ completeBarcode: String, targetSize: CGSize? = nil) -> UIImage? {
            let length: Int = completeBarcode.count
            if length <= 0 {
                return nil
            }
    
            // Values taken from CIImage generated AVMetadataObjectTypePDF417Code type image
            // Top spacing          = 1.5
            // Bottom spacing       = 2
            // Left & right spacing = 2
            let width = length + 4
            // Calculate the correct aspect ratio, so that the resulting image can be resized to the target size
            var height = barcodeDefaultHeight
            if let targetSize = targetSize {
                height = Int(targetSize.height / targetSize.width * CGFloat(width))
            }
            let size = CGSize(width: CGFloat(width), height: CGFloat(height))
            UIGraphicsBeginImageContextWithOptions(size, false, 1)
            if let context = UIGraphicsGetCurrentContext() {
                context.setShouldAntialias(false)
    
                self.fillColor.setFill()
                self.strokeColor.setStroke()
    
                context.fill(CGRect(x: 0, y: 0, width: size.width, height: size.height))
                context.setLineWidth(1)
    
                // swiftlint:disable:next identifier_name
                var i = 0
                for char in completeBarcode {
                    i += 1
                    if char == "1" {
                        // swiftlint:disable:next identifier_name
                        let x = i + (2 + 1)
                        context.move(to: CGPoint(x: CGFloat(x), y: 1.5))
                        context.addLine(to: CGPoint(x: CGFloat(x), y: size.height - 2))
                    }
                }
                context.drawPath(using: CGPathDrawingMode.fillStroke)
                let barcode = UIGraphicsGetImageFromCurrentImageContext()
                UIGraphicsEndImageContext()
    
                return barcode
            } else {
                return nil
            }
        }
    }
    

    Using the above code barcodes can be generated like this.

    func generateBarcode(contents: String) -> Image {
        let codabarGenerator = CodabarGenerator()
        let encodedBarcode = codabarGenerator.barcode(contents)
    
        if let barcodeImage = codabarGenerator.drawCompleteBarcode(encodedBarcode) {
            return Image(uiImage: barcodeImage
        } else {
            return Image(systemName: "xmark.circle")
        }
    }