文档扫描与图像处理

B4X跨平台扫码应用

B4X是一套快速应用开发(RAD)工具,可用于为主流的移动和桌面平台开发应用。

它包含四个产品:B4A、B4i、B4J和B4R,分别用于Android、iOS、Java(JavaFX桌面、服务器和树莓派)和Arduino开发。

除了多平台支持外,B4X还具有以下特点:

  • 易于使用的编程语言
  • 功能齐全的专用IDE
  • 活跃而友好的社区

B4X可以创建Android和iOS原生应用。B4X编程语言是一种Visual Basic的现代版本。它会被编译成Android的Java和iOS的Objective-C代码。因为这个特点,将Java/Objective-C的类库集成到B4X项目中是很容易的事情。B4X代码可以跨平台共享。不过我们仍然需要编写平台专属的部分代码,比如控制摄像头和选择文件。

Dynamsoft Barcode Reader (DBR)是一种用C++编写的条码SDK,提供JavaScript、Java和Objective-C的封装。我们可以很容易地将其封装为一个B4X库,便于用B4X开发Android、iOS和桌面端的条码读取软件。

在本文中,我们将完成B4X库的封装,并创建Android、iOS和桌面平台上的演示应用。

创建一个新项目

我们首先建立一个新的B4J项目来创建和测试这个扫码库。

  1. 在B4J中创建一个新的B4XPages项目。

    新项目

  2. 打开UI设计器。添加一个选择图像按钮,一个解码按钮,一个面板用于显示图像,一个标签用于显示结果。

    设计器

    之后,我们单击生成成员以在代码中声明这些控件并添加事件子程序。

    生成成员

  3. 实现图像选择器。用B4XCanvas在面板上绘制所选图像。

    Private Sub btnLoadImage_Click
        Dim bm As B4XBitmap
        Dim fc As FileChooser
        fc.Initialize
        Dim path As String=fc.ShowOpen(B4XPages.GetNativeParent(Me))    
        If File.Exists(path,"") Then
            bm=xui.LoadBitmap(path,"")
            cvs.ClearRect(cvs.TargetRect)
            drawBitmap(bm)
            Panel1.Tag=bm
        End If
    End Sub
    

现在,我们要实现解码部分。

封装Dynamsoft Barcode SDK

让我们先完成封装。

添加依赖项

  1. 官网下载Dynamsoft Barcode Reader的jar文件。
  2. 配置额外类库文件夹的路径。

    库的路径

  3. 将jar放在额外类库文件夹中。
  4. Main中添加以下代码行导入jar:

    #AdditionalJar: dynamsoft-barcodereader-8.2
    

封装第三方库的方法

封装库主要有两种方法。

一种是直接用Java编写(详细说明)。使用Objective-C创建iOS库会稍微困难一点。

另一种是使用JavaObject,基于Java反射特性直接调用Java API。B4i中和JavaObject对应的是NativeObject

它可以与内联Java代码一起使用。Java代码可以直接包含在B4X的源代码中,如下所示。

#If JAVA
public String FirstMethod() {
   return "Hello World!";
}
#End If

然后可以使用JavaObject调用它。

Dim JO as JavaObject=Me
Dim s As String = JO.RunMethod("FirstMethod", Null)
Log(s) 'will print Hello World!

这里,我们选择使用JavaObject来进行封装。

实现封装

创建一个类并将其命名为DBR。具体代码如下。

Sub Class_Globals
    Private reader As JavaObject
End Sub

'Initializes the object. You can add parameters to this method if needed.
Public Sub Initialize
    reader.InitializeNewInstance("com.dynamsoft.dbr.BarcodeReader",Null)            
End Sub

public Sub initLicenseFromKey(license As String)
    'request your license here: https://www.dynamsoft.com/customer/license/trialLicense?ver=latest
    reader.RunMethod("initLicense",Array(license))
End Sub

private Sub ConvertToTextResults(results() As Object) As List
    Dim list1 As List
    list1.Initialize
    For Each result As Object In results
        Dim tr As TextResult
        tr.Initialize(result) 'convert the TextResult Java object to a B4X object
        list1.Add(tr)
    Next
    Return list1
End Sub

Sub decodeImage(bitmap As B4XBitmap) As List    
    Dim results() As Object
    Dim SwingFXUtils As JavaObject
    SwingFXUtils.InitializeStatic("javafx.embed.swing.SwingFXUtils")
    Dim bufferedImage As Object=SwingFXUtils.RunMethod("fromFXImage",Array(bitmap,Null)) ' convert JavaFX image to bufferedImage
    results=reader.RunMethod("decodeBufferedImage",Array(bufferedImage,""))
    Return ConvertToTextResults(results)
End Sub

创建一个TextResult类来表示解码结果。这里我们只解析文本和定位结果。

Sub Class_Globals
    Private mTextResult As JavaObject
    Private mText As String
    Private mResultPoints(4) As Point2D
    Type Point2D(x As Int,y As Int)
End Sub

'Initializes the object. You can add parameters to this method if needed.
Public Sub Initialize(result As Object)
    mTextResult=result    
    Parse
End Sub

Private Sub Parse
    mText=mTextResult.GetField("barcodeText")
    Dim points() As Object=mTextResult.GetFieldJO("localizationResult").GetField("resultPoints")
    For i=0 To 3
        Dim point As JavaObject=points(i)    
        Dim p As Point2D
        p.Initialize
        p.x=point.GetField("x")
        p.y=point.GetField("y")
        mResultPoints(i)=p
    Next
End Sub

Public Sub getObject As Object
    Return mTextResult
End Sub

Public Sub getText As String
    Return mText
End Sub

Public Sub getResultPoints As Point2D()
    Return mResultPoints
End Sub

封装后的使用

现在,我们可以用做好的封装来实现解码部分。

  1. 初始化一个DBR的实例,命名为reader。

    Sub Class_Globals
        Private reader As DBR
    End Sub
    
    Public Sub Initialize
        reader.Initialize
        reader.initLicenseFromKey("<license key>")
    End Sub
    
  2. 解码图像并显示结果。

    	Private Sub btnDecode_Click        
    		If (Panel1.Tag Is B4XBitmap)=False  Then
    			xui.MsgboxAsync("Please load an image first","")
    			Return
    		End If
    		Dim bm As B4XBitmap=Panel1.Tag
    		Dim results As List=reader.decodeImage(bm)
    		Dim sb As StringBuilder
    		sb.Initialize
    		Dim color As Int=xui.Color_Red
    		Dim stroke As Int=2
    		For Each result As TextResult In results
    			sb.Append("Text: ").Append(result.Text).Append(CRLF)
    			For i=0 To 2
    				Dim x1 As Int=result.ResultPoints(i).x*xPercent
    				Dim y1 As Int=result.ResultPoints(i).y*yPercent
    				Dim x2 As Int=result.ResultPoints(i+1).x*xPercent
    				Dim y2 As Int=result.ResultPoints(i+1).y*yPercent
    				cvs.DrawLine(x1,y1,x2,y2,color,stroke)
    			Next
    			cvs.DrawLine(result.ResultPoints(3).x*xPercent, _
    						 result.ResultPoints(3).y*yPercent, _
    						 result.ResultPoints(0).x*xPercent, _
    						 result.ResultPoints(0).y*yPercent,color,stroke)
    		Next    
    		cvs.Invalidate
    		lblResult.Text=sb.ToString
    	End Sub
    

下图是最后实现的应用:

条码读取器B4J

跨平台的实现

我们已经完成了应用的B4J版本。接下来我们稍作修改,使其能够在Android和iOS上工作。

共享类和布局

在建立B4XPages项目时,会创建包含平台特定的文件的三个文件夹和一个共享资产文件的Shared Files文件夹。共享的类文件存储在根目录中。

打开之前建立的项目的文件夹。我们可以看到这样的文件夹结构:

│  B4XMainPage.bas
│
├─B4A
│  │  BarcodeReader.b4a
│  │  BarcodeReader.b4a.meta
│  │  Starter.bas
│  │
│  └─Files
│          mainpage.bal
│
├─B4i
│  │  BarcodeReader.b4i
│  │  BarcodeReader.b4i.meta
│  │
│  └─Files
│      │  mainpage.bil
│      │
│      └─Special
├─B4J
│  │  BarcodeReader.b4j
│  │  BarcodeReader.b4j.meta
│  │  DBR.bas
│  │  TextResult.bas
│  │
│  └─Files
│          MainPage.bjl
│
└─Shared Files

现在我们想让我们刚建立的文件在三个平台的项目中共享。

  1. DBR.basTextResult.bas移到根目录。打开B4A、B4J和B4i项目,使用相对路径导入这两个文件。

    链接模块

  2. 在B4J中,打开设计器。选择并复制所有控件。用设计器打开B4i和B4A的布局文件并粘贴。

    粘贴布局

添加特定于平台的代码

特定于平台的代码可以使用#if语句存在于一个源文件中。让我们具体来看一下。

B4A代码

  1. 使用aar。

    从Dynamsoft下载aar文件,将其放入额外类库文件夹,并在Main中添加以下代码行:

    #AdditionalJar: DynamsoftBarcodeReaderAndroid.aar
    
  2. 图像选择器是特定于平台的。我们在if b4a语句中添加代码,使用ContentChooser来拾取图像。

    Private Sub btnLoadImage_Click
        Dim bm As B4XBitmap
    
        #if b4a
        Dim cc As ContentChooser
        cc.Initialize("CC")
        cc.Show("image/*", "Choose image")
        Wait For CC_Result (Success As Boolean, Dir As String, FileName As String)
        If Success Then
            bm=LoadBitmap(Dir,FileName)
        Else
            ToastMessageShow("No image selected", True)
        End If    
        #End If
    
        #if b4j
        Dim fc As FileChooser
        fc.Initialize
        Dim path As String=fc.ShowOpen(B4XPages.GetNativeParent(Me))    
        If File.Exists(path,"") Then
            bm=xui.LoadBitmap(path,"")
        End If        
        #End If    
    
        If bm.IsInitialized And bm<>Null Then         
            cvs.ClearRect(cvs.TargetRect)
            drawBitmap(bm)
            Panel1.Tag=bm
        End If
    End Sub
    
  3. Dynamsoft Barcode Reader移动版可以通过连接到许可证跟踪服务器(LTS)进行为期7天的公共试用。

    将以下Java代码添加到DBR.bas中:

    #If b4a
    
    #if java
    import com.dynamsoft.dbr.BarcodeReader;
    import com.dynamsoft.dbr.BarcodeReaderException;
    import com.dynamsoft.dbr.DMLTSConnectionParameters;
    import com.dynamsoft.dbr.DBRLTSLicenseVerificationListener;
    public static void initLicenseFromLTS(BarcodeReader dbr,String organizationID){
        DMLTSConnectionParameters parameters = new DMLTSConnectionParameters();
        parameters.organizationID = organizationID;
        dbr.initLicenseFromLTS(parameters, new DBRLTSLicenseVerificationListener() {
            @Override
            public void LTSLicenseVerificationCallback(boolean isSuccess, Exception error) {
                if (!isSuccess) {
                    error.printStackTrace();
                }
            }
        });
    }
    #End If
    
    #End If
    

    添加下列Sub以从LTS初始化许可证。

    public Sub initLicenseFromLTS(organizationID As String)
        Dim JO as JavaObject=Me
        JO.RunMethod("initLicenseFromLTS",Array(reader,organizationID))    
    End Sub    
    

    B4XMainPage.bas中,如果是移动平台,则使用initLicenseFromLTS

    Public Sub Initialize
        reader.Initialize        
        #if b4j
        reader.initLicenseFromKey("<license key>")
        #else
        reader.initLicenseFromLTS("200001")    
        #End If    
    End Sub
    
  4. 在B4J中,图像需要转换为BufferedImage,而Android上的位图可以直接使用。

    Sub decodeImage(bitmap As B4XBitmap) As List            
        Dim results() As Object        
    
        #If b4j
    
        Dim SwingFXUtils As JavaObject
        SwingFXUtils.InitializeStatic("javafx.embed.swing.SwingFXUtils")
        Dim bufferedImage As Object=SwingFXUtils.RunMethod("fromFXImage",Array(bitmap,Null))
        results=reader.RunMethod("decodeBufferedImage",Array(bufferedImage,""))
    
        #else if b4a
    
        results=reader.RunMethod("decodeBufferedImage",Array(bitmap,""))    
    
        #End If    
    
        Return ConvertToTextResults(results)    
    End Sub
    

完成了。我们现在可以在Android上运行这个应用了。

Android

B4i代码

  1. 使用Framework。

    从Dynamsoft网站下载Dynamsoft Barcode Reader的iOS框架,将其放入本地Mac构建器的库文件夹中,并在Main中添加以下代码行:

    #AdditionalLib: DynamsoftBarcodeReader.framework.3
    #AdditionalLib: libc++.tbd
    
  2. 使用Camera类选择图像。

    #if b4i
    Dim cam As Camera
    cam.Initialize("camera",B4XPages.GetNativeParent(Me))
    cam.SelectFromSavedPhotos(Sender, cam.TYPE_IMAGE)    
    Wait For camera_Complete (Success As Boolean, Image As Bitmap, VideoPath As String)
    If Success Then        
        Dim NO as NativeObject=Me
        bm=NO.RunMethod("normalizedImage:",Array(Image))            
    End If
    #End If
    

    iOS中拍摄的图像需要进行处理,否则图像可能会被旋转。

    以下Objective-C代码用于执行此操作:

    #if b4i
    #if objc
    //https://stackoverflow.com/questions/8915630/ios-uiimageview-how-to-handle-uiimage-image-orientation
    - (UIImage *)normalizedImage: (UIImage*) image {
        if (image.imageOrientation == UIImageOrientationUp) return image;
        UIGraphicsBeginImageContextWithOptions(image.size, NO, image.scale);
        [image drawInRect:(CGRect){0, 0, image.size}];
        UIImage *normalizedImage = UIGraphicsGetImageFromCurrentImageContext();
        UIGraphicsEndImageContext();
        return normalizedImage;
    }
    #End If
    #End If
    
    
  3. 使用NativeObject和JavaObject存在不同。我们使用内嵌Objective-C代码直接调用DBR的api,相关的B4X Sub需要稍加修改。

    #if b4i
    #If ObjC
    #import <DynamsoftBarcodeReader/DynamsoftBarcodeReader.h>
    - (DynamsoftBarcodeReader*) initializeDBR: (NSString*) license {
        DynamsoftBarcodeReader *dbr;
        dbr = [[DynamsoftBarcodeReader alloc] initWithLicense:license];
        NSLog(dbr.getVersion);
        return dbr;
    }
    
    - (DynamsoftBarcodeReader*) initializeDBRFromLTS: (NSString*) organizationID {
    
        DynamsoftBarcodeReader *dbr;
        iDMLTSConnectionParameters* lts = [[iDMLTSConnectionParameters alloc] init];
        lts.organizationID = organizationID;    
        dbr = [[DynamsoftBarcodeReader alloc] initLicenseFromLTS:lts verificationDelegate:self];
        return dbr;
     }
    
    - (NSArray<iTextResult*>*) decodeImage: (UIImage*) image {
        NSError __autoreleasing * _Nullable error;
        DynamsoftBarcodeReader* dbr=self->__reader.object;
        NSArray<iTextResult*>* result = [dbr decodeImage:image withTemplate:@"" error:&error];    
        NSLog(@"%lu",(unsigned long)result.count);
        return result;
    }
    
    #end if
    #End If
    

    整个decodeImage方法:

    Sub decodeImage(bitmap As B4XBitmap) As List            
        #if b4i    
    
        Dim results As List=asNO(Me).RunMethod("decodeImage:",Array(bitmap))
        Return ConvertToTextResults2(results)        
    
        #Else    
    
        Dim results() As Object        
    
        #If b4j
    
        Dim SwingFXUtils As JavaObject
        SwingFXUtils.InitializeStatic("javafx.embed.swing.SwingFXUtils")
        Dim bufferedImage As Object=SwingFXUtils.RunMethod("fromFXImage",Array(bitmap,Null))
        results=reader.RunMethod("decodeBufferedImage",Array(bufferedImage,""))
    
        #Else If b4a
    
        results=reader.RunMethod("decodeBufferedImage",Array(bitmap,""))    
    
        #End If    
    
        Return ConvertToTextResults(results)
    
        #End if        
    End Sub
    

现在,我们可以在iOS上运行条码阅读器了。

iOS

结论

使用B4X可以很容易地创建跨平台条码阅读器。代码和UI可以跨平台共享。不过我们还是需要了解一些原生开发。

源代码

https://github.com/xulihang/BarcodeReader-B4X

该库已经在B4X论坛上发布。

更多

以后文章里会介绍开发的细节。