Dienstag, 23. August 2016

Native Barcode Scanner Integration

1) Introduction

There are lots of solutions, showing how to implement a barcode scanner into your FireMonkey app.

There are different problems with all of these solutions:
  • not platform independent
  • platform dependent API handling
  • using external libraries
  • missing code formats (f.e. no QR-Code support)
  • cost a lot of money
  • confusing or difficult licences
  • expect another app to be installed (BarcodeScanner)
  • [...]
Especially the last problem is quite uncharming, because we can't demand to install another app to use our app.
On Android the Java ZXing library is used, which is not installed by default on the customers device. So the Google Barcode Scanner app need to be installed from PlayStore.

2) Solution

A million thanks to the work of Spelt! He migrated the Java ZXing open-source API to Delphi!
And the best thing about it: It works!


The reason, why I post about external code is, because in my opinion, he didn't got enough credit for his effort.
In these times, where everywhere QR-Codes are used, a barcode scanner is fundamental!

  • It's free!
  • native code for Windows/Android/iOS/OSX
  • Simple and fast
  • Multiple barcodes: QR-Code, Code-128, Code-93, ITF


3) Integration

We want build a mini application with a single form displaying the camera, framed by a darkened area and scanning line.

The form is scanning on the fly when starting the application. 
If a code was recognized, we'd like to stop scanning and showing up the content in a message dialog.

I will not explain how to build a FireMonkey form. Therefor take a look at tons of tutorials all around the internet.
[...]

constructor TMyBarcodeReader.Create();
var lAppEventSvc : IFMXApplicationEventService;
begin
  inherited;

  fCamera := TCameraComponent.Create(nil);
  fCamera.Kind      := FMX.Media.TCameraKind.ckFrontCamera;
  fCamera.FlashMode := FMX.Media.TFlashMode.fmFlashOff;
  fCamera.Quality   := FMX.Media.TVideoCaptureQuality.MediumQuality;
  fCamera.OnSampleBufferReady := doOnCameraSampleBufferReady;

  fFrameTake := 0;

  if TPlatformServices.Current.SupportsPlatformService(IFMXApplicationEventService, IInterface(lAppEventSvc)) then
    lAppEventSvc.SetApplicationEventHandler(AppEvent);

  fScanManager := TScanManager.Create(TBarcodeFormat.Auto, nil);
end;

procedure TMyBarcodeReader.start();
begin
  fResultReceived := false;

  fCamera.Active := false;
  fCamera.Kind   := FMX.Media.TCameraKind.BackCamera;
  fCamera.Active := true;
end;

procedure TMyBarcodeReader.stop();
begin
  fCamera.Active := false;
end;

procedure TMyBarcodeReader.doOnCameraSampleBufferReady(Sender: TObject; const ATime: TMediaTime);
begin
  TThread.Synchronize(TThread.CurrentThread, getImage);
end;

procedure TMyBarcodeReader.getImage();
var lScanBitmap : TBitmap;
    lReadResult : TReadResult;
    lImgObj     : TObject;
    lOutBitmap  : TBitmap;
begin
  // get the TImage where we want to display the camera image
  if assigned(fOnDisplayImage) then fOnDisplayImage(lImgObj)
                               else lImgObj := nil;

  if (not assigned(lImgObj)) or not (lImgObj is TBitmap) then exit;
  lOutBitmap := TBitmap(lImgObj);

  fCamera.SampleBufferToBitmap(lOutBitmap, true);

  if fScanInProgress then
    exit;

  lScanBitmap := TBitmap.Create();
  lOutBitmap.Assign(lScanBitmap);
  TTask.Run(
    procedure
    begin
      try
        if not fResultReceived then
        begin
          fScanInProgress := true;
          lReadResult     := fScanManager.Scan(lScanBitmap);
          fScanInProgress := false;
        end;
      except
        on E: Exception do
        begin
          fScanInProgress := false;
          TThread.Synchronize(nil,
            procedure
            begin
              //
            end);

          if assigned(lScanBitmap) then
            freeAndNil(lScanBitmap);

          exit;
        end;

      end;

      TThread.Synchronize(nil,
        procedure
        begin
          if assigned(lReadResult) then
          begin
            fResultReceived := true;
            if assigned(fOnResult) then
              fOnResult(lReadResult.Text);

            stop();
          end;

          if assigned(lScanBitmap) then
            freeAndNil(lScanBitmap);

          freeAndNil(lReadResult);
        end);
    end);

end;

// Make sure the camera is released if you're going away
function TMyBarcodeReader.appEvent(pAppEvent : TApplicationEvent; pContext : TObject) : Boolean;
begin
  case pAppEvent of
    TApplicationEvent.WillBecomeInactive :
      fCamera.Active := false;
    TApplicationEvent.EnteredBackground :
      fCamera.Active := false;
    TApplicationEvent.WillTerminate :
      fCamera.Active := false;
  end;
end;

[...]
This is just an excerpt on how to implement the API. Spelt also got a demo, where you can have a look at.

Hint:

Simply build a Form with an TImage in the back. Add 4 black rectangles with transparency as a darkened frame and a red line, symbolizing the ancient scanning line.
Then include the TMyBarcodeReader component.

Inside TMyBarcodeReader.Create() we create the logical TCameraComponent and the ScanManager. Here the TMyBarcodeReader.doOnCameraSampleBufferReady() event is applied to the TCameraComponent. 
So, every time we get a new camera image, we enter this event.


constructor TMyBarcodeReader.Create();
var lAppEventSvc : IFMXApplicationEventService;
begin
  inherited;

  fCamera := TCameraComponent.Create(nil);
  fCamera.Kind      := FMX.Media.TCameraKind.ckFrontCamera;
  fCamera.FlashMode := FMX.Media.TFlashMode.fmFlashOff;
  fCamera.Quality   := FMX.Media.TVideoCaptureQuality.MediumQuality;
  fCamera.OnSampleBufferReady := doOnCameraSampleBufferReady;

  fFrameTake := 0;

  if TPlatformServices.Current.SupportsPlatformService(IFMXApplicationEventService, IInterface(lAppEventSvc)) then
    lAppEventSvc.SetApplicationEventHandler(AppEvent);

  fScanManager := TScanManager.Create(TBarcodeFormat.Auto, nil);
end;

With TMyBarcodeReader.start() we startup the scanning process, by activating the back camera.


procedure TMyBarcodeReader.start();
begin
  fResultReceived := false;

  fCamera.Active := false;
  fCamera.Kind   := FMX.Media.TCameraKind.BackCamera;
  fCamera.Active := true;
end;

When the camera service provides the current camera image, we automatically enter the TMyBarcodeReader.doOnCameraSampleBufferReady() event, where we synchronize our "getImage" method.

TMyBarcodeReader.getImage() is the main handling routine. At first we want to get the destination image by the "OnDisplayImage" event, to know where to display the
camera image.
  // get the TImage where we want to display the camera image
  if assigned(fOnDisplayImage) then fOnDisplayImage(lImgObj)
                               else lImgObj := nil;

  if (not assigned(lImgObj)) or not (lImgObj is TBitmap) then exit;
  lOutBitmap := TBitmap(lImgObj);
Then we're requesting the TCameraComponent image itself directly onto our form image.
fCamera.SampleBufferToBitmap(lOutBitmap, true);
In the next step we're about to run a TTask where we want to scan the latest camera buffered image.
if not fResultReceived then
begin
  fScanInProgress := true;
  lReadResult     := fScanManager.Scan(lScanBitmap);
  fScanInProgress := false;
end;
If we found a code, we'll stop the scanning process by deactivating camera and entering the "OnResult" event.
if assigned(lReadResult) then
begin
  fResultReceived := true;
  if assigned(fOnResult) then
    fOnResult(lReadResult.Text);

  stop();
end;
The last method you see, is the platform service event. When the app status changes, for example, if it's becoming inactive, we do not need to scan anymore. So we're deactivating the camera component.

function TMyBarcodeReader.appEvent(pAppEvent : TApplicationEvent; pContext : TObject) : Boolean;
begin
  case pAppEvent of
    TApplicationEvent.WillBecomeInactive :
      fCamera.Active := false;
    TApplicationEvent.EnteredBackground :
      fCamera.Active := false;
    TApplicationEvent.WillTerminate :
      fCamera.Active := false;
  end;
end;

4) Conclusion

As always I'd like to thank you for reading my blog. Let me know if there are some bugs, mistakes or problems with this article or component.



(*) These links are being provided as a convenience and for informational purposes only; they do not constitute an endorsement or an approval by this blog of any of the products, services or opinions of the corporation or organization or individual. This blog bears no responsibility for the accuracy, legality or content of the external site or for that of subsequent links. Contact the external site for answers to questions regarding its content.