那些年为了开发二维码扫描-ZBar

什么是ZBar

ZBar是一个开源库,用于扫描、读取二维码和条形码。支持的二维码包括:EAN/UPC,QR等

如果你是一个iPhone应用开发人员,做到二维码模块的时候,是不是会考虑ZBar开源项目来助你一臂之力呢?可是我这里说的是Android平台的开发,我为什么提到ZBar项目呢,难道我要用ZBar在Android平台扫描二维码吗?对的,没有错!这将会是一个极其不错的选择。为什么这么说呢,不是很多Android开发都是用ZXing来解析二维码的么?好吧,ZXing是我下一篇文章要写的,这里先抛砖引玉说一点点。我将ZXing和ZBar做一个比较,说说它们的优缺点,便于大家的取舍。

  • ZXing项目的示例程序对于摄像头的控制写的非常全面,ZBar的没有
  • ZBar基于C语言编写,解码效率高于ZXing项目
  • ZBar是日本人写的,对于中文解析会乱码这个肯定有人遇到过的,ZXing不会乱码
  • 扫描框的绘制,ZXing的扫描框绘制是自定义View的,截取区域不好控制(至少我没控制好),ZBar的可以自定义,只要你会计算截取区域

这里需要着重说一下第四点,我也是沿着解决这个第四点和第二点的问题才思考了这么多东西的。好烦躁自己的这种强迫症啊

下载ZBar项目

编写ZBar示例程序

ZBar示例程序主要通过ZBar解码截取区域计算,源码编译和Android示例项目三方面来展开

着重介绍一下扫描截取界面的计算

  • pt:预览图中二维码图片的左上顶点坐标,也就是手机中相机预览中看到的待扫描二维码的位置
  • qrheight:预览图中二维码图片的高度
  • qrwidth:预览图中二维码图片的宽度
  • pheight:预览图的高度,也即camera的分辨率高度
  • pwidth:预览图的宽度,也即camera的分辨率宽度
  • st:布局文件中扫描框的左上顶点坐标
  • sheight:布局文件中扫描框的高度
  • swidth:布局文件中扫描框的宽度
  • cheight:布局文件中相机预览控件的高度
  • cwidth:布局文件中相机预览控件的宽度

其中存在这样一个等比例公式

1
2
3
4
ptx / pwidth = stx / cwidth ;
pty / pheight = sty / cheight ;
qrwidth / pwidth = swidth / cwidth ;
qrheight / pheight = sheight / cheight ;

即:

1
2
3
4
ptx = stx * pwidth / cwidth ;
pty = sty * pheight / cheight ;
qrwidth = swidth * pwidth / cwidth ;
qrheight = sheight * pheight / cheight ;

以上ptx,pty,qrwidth,qrheight四个参数也就是ZBar中解码是需要crop时传入的四个参数,如此便知道了截取区域应该如何计算了。这样扫描的灵活性都大大增强了

ZBar中文乱码的解决

ZBar扫描含有中文的二维码图片时,结果是乱码的,所以需要修改c文件重新编译打包so文件才行。

需要修改的文件是zbar/qrcode/qrdextxt.c文件

1
2
3
4
5
6
/*This is the encoding the standard says is the default.*/
latin1_cd=iconv_open("UTF-8","ISO8859-1");
/*But this one is often used, as well.*/
sjis_cd=iconv_open("UTF-8","SJIS");
/*This is a trivial conversion just to check validity without extra code.*/
utf8_cd=iconv_open("UTF-8","UTF-8");

修改为:

1
2
3
4
5
6
/*This is the encoding the standard says is the default.*/
latin1_cd=iconv_open("UTF-8","GB18030");
/*But this one is often used, as well.*/
sjis_cd=iconv_open("UTF-8","UTF-8");
/*This is a trivial conversion just to check validity without extra code.*/
utf8_cd=iconv_open("UTF-8","UTF-8");

重新编译zbar生成so文件

这个真的需要一定的NDK开发经验了,我个人只是了解一点点NDK的知识,所以在网上找到了一个大神的博客一步一步做下来才算是编译完成了。

其中NDK开发环境搭建可以参考: http://www.eoeandroid.com/forum.php?mod=viewthread&tid=272919
ZBar项目编译可以参考: http://magiclen.org/zbar/

ZBar编译项目结构

ZBar编译后生成的so文件

编写Android示例程序

安卓示例程序主要是通过提取ZXing项目中的扫描界面处理代码和ZBar解码模块相结合的方式来实现的

安卓工程结构

布局界面代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<RelativeLayout
android:id="@+id/capture_container"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<FrameLayout
android:id="@+id/capture_preview"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<ImageView
android:id="@+id/capture_mask_top"
android:layout_width="match_parent"
android:layout_height="120dp"
android:layout_alignParentTop="true"
android:background="@drawable/shadow" />
<RelativeLayout
android:id="@+id/capture_crop_view"
android:layout_width="200dp"
android:layout_height="200dp"
android:layout_centerHorizontal="true"
android:layout_below="@id/capture_mask_top"
android:background="@drawable/qr_code_bg" >
<ImageView
android:id="@+id/capture_scan_line"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_marginBottom="5dp"
android:layout_marginTop="5dp"
android:src="@drawable/scan_line" />
</RelativeLayout>
<ImageView
android:id="@+id/capture_mask_bottom"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_below="@id/capture_crop_view"
android:background="@drawable/shadow" />
<ImageView
android:id="@+id/capture_mask_left"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_above="@id/capture_mask_bottom"
android:layout_alignParentLeft="true"
android:layout_below="@id/capture_mask_top"
android:layout_toLeftOf="@id/capture_crop_view"
android:background="@drawable/shadow" />
<ImageView
android:id="@+id/capture_mask_right"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_above="@id/capture_mask_bottom"
android:layout_alignParentRight="true"
android:layout_below="@id/capture_mask_top"
android:layout_toRightOf="@id/capture_crop_view"
android:background="@drawable/shadow" />
</RelativeLayout>
<Button
android:id="@+id/capture_restart_scan"
android:layout_width="match_parent"
android:layout_height="40dp"
android:layout_alignParentBottom="true"
android:background="#66ffcc00"
android:gravity="center"
android:text="restart scan"
android:textColor="@android:color/white"
android:textSize="14sp" />
<TextView
android:id="@+id/capture_scan_result"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_above="@id/capture_restart_scan"
android:layout_marginBottom="10dp"
android:gravity="center"
android:text="Scanning..."
android:textColor="@android:color/white"
android:textSize="14sp" />
</RelativeLayout>

扫描Activity关键代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
import java.io.IOException;
import java.lang.reflect.Field;
import net.sourceforge.zbar.Config;
import net.sourceforge.zbar.Image;
import net.sourceforge.zbar.ImageScanner;
import net.sourceforge.zbar.Symbol;
import net.sourceforge.zbar.SymbolSet;
import android.app.Activity;
import android.content.pm.ActivityInfo;
import android.graphics.Rect;
import android.hardware.Camera;
import android.hardware.Camera.AutoFocusCallback;
import android.hardware.Camera.PreviewCallback;
import android.hardware.Camera.Size;
import android.os.Bundle;
import android.os.Handler;
import android.text.TextUtils;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.animation.Animation;
import android.view.animation.TranslateAnimation;
import android.widget.Button;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import android.widget.TextView;
public class CaptureActivity extends Activity {
private Camera mCamera;
private CameraPreview mPreview;
private Handler autoFocusHandler;
private CameraManager mCameraManager;
private TextView scanResult;
private FrameLayout scanPreview;
private Button scanRestart;
private RelativeLayout scanContainer;
private RelativeLayout scanCropView;
private ImageView scanLine;
private Rect mCropRect = null;
private boolean barcodeScanned = false;
private boolean previewing = true;
private ImageScanner mImageScanner = null;
static {
System.loadLibrary("iconv");
}
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_capture);
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
findViewById();
addEvents();
initViews();
}
private void findViewById() {
scanPreview = (FrameLayout) findViewById(R.id.capture_preview);
scanResult = (TextView) findViewById(R.id.capture_scan_result);
scanRestart = (Button) findViewById(R.id.capture_restart_scan);
scanContainer = (RelativeLayout) findViewById(R.id.capture_container);
scanCropView = (RelativeLayout) findViewById(R.id.capture_crop_view);
scanLine = (ImageView) findViewById(R.id.capture_scan_line);
}
private void addEvents() {
scanRestart.setOnClickListener(new OnClickListener() {
public void onClick(View v) {
if (barcodeScanned) {
barcodeScanned = false;
scanResult.setText("Scanning...");
mCamera.setPreviewCallback(previewCb);
mCamera.startPreview();
previewing = true;
mCamera.autoFocus(autoFocusCB);
}
}
});
}
private void initViews() {
mImageScanner = new ImageScanner();
mImageScanner.setConfig(0, Config.X_DENSITY, 3);
mImageScanner.setConfig(0, Config.Y_DENSITY, 3);
autoFocusHandler = new Handler();
mCameraManager = new CameraManager(this);
try {
mCameraManager.openDriver();
} catch (IOException e) {
e.printStackTrace();
}
mCamera = mCameraManager.getCamera();
mPreview = new CameraPreview(this, mCamera, previewCb, autoFocusCB);
scanPreview.addView(mPreview);
TranslateAnimation animation = new TranslateAnimation(
Animation.RELATIVE_TO_PARENT, 0.0f,
Animation.RELATIVE_TO_PARENT, 0.0f,
Animation.RELATIVE_TO_PARENT, 0.0f,
Animation.RELATIVE_TO_PARENT, 0.85f);
animation.setDuration(3000);
animation.setRepeatCount(-1);
animation.setRepeatMode(Animation.REVERSE);
scanLine.startAnimation(animation);
}
public void onPause() {
super.onPause();
releaseCamera();
}
private void releaseCamera() {
if (mCamera != null) {
previewing = false;
mCamera.setPreviewCallback(null);
mCamera.release();
mCamera = null;
}
}
private Runnable doAutoFocus = new Runnable() {
public void run() {
if (previewing)
mCamera.autoFocus(autoFocusCB);
}
};
PreviewCallback previewCb = new PreviewCallback() {
public void onPreviewFrame(byte[] data, Camera camera) {
Size size = camera.getParameters().getPreviewSize();
// 这里需要将获取的data翻转一下,因为相机默认拿的的横屏的数据
byte[] rotatedData = new byte[data.length];
for (int y = 0; y < size.height; y++) {
for (int x = 0; x < size.width; x++)
rotatedData[x * size.height + size.height - y - 1] = data[x
+ y * size.width];
}
// 宽高也要调整
int tmp = size.width;
size.width = size.height;
size.height = tmp;
initCrop();
Image barcode = new Image(size.width, size.height, "Y800");
barcode.setData(rotatedData);
barcode.setCrop(mCropRect.left, mCropRect.top, mCropRect.width(),
mCropRect.height());
int result = mImageScanner.scanImage(barcode);
String resultStr = null;
if (result != 0) {
SymbolSet syms = mImageScanner.getResults();
for (Symbol sym : syms) {
resultStr = sym.getData();
}
}
if (!TextUtils.isEmpty(resultStr)) {
previewing = false;
mCamera.setPreviewCallback(null);
mCamera.stopPreview();
scanResult.setText("barcode result " + resultStr);
barcodeScanned = true;
}
}
};
// Mimic continuous auto-focusing
AutoFocusCallback autoFocusCB = new AutoFocusCallback() {
public void onAutoFocus(boolean success, Camera camera) {
autoFocusHandler.postDelayed(doAutoFocus, 1000);
}
};
/**
* 初始化截取的矩形区域
*/
private void initCrop() {
int cameraWidth = mCameraManager.getCameraResolution().y;
int cameraHeight = mCameraManager.getCameraResolution().x;
/** 获取布局中扫描框的位置信息 */
int[] location = new int[2];
scanCropView.getLocationInWindow(location);
int cropLeft = location[0];
int cropTop = location[1] - getStatusBarHeight();
int cropWidth = scanCropView.getWidth();
int cropHeight = scanCropView.getHeight();
/** 获取布局容器的宽高 */
int containerWidth = scanContainer.getWidth();
int containerHeight = scanContainer.getHeight();
/** 计算最终截取的矩形的左上角顶点x坐标 */
int x = cropLeft * cameraWidth / containerWidth;
/** 计算最终截取的矩形的左上角顶点y坐标 */
int y = cropTop * cameraHeight / containerHeight;
/** 计算最终截取的矩形的宽度 */
int width = cropWidth * cameraWidth / containerWidth;
/** 计算最终截取的矩形的高度 */
int height = cropHeight * cameraHeight / containerHeight;
/** 生成最终的截取的矩形 */
mCropRect = new Rect(x, y, width + x, height + y);
}
private int getStatusBarHeight() {
try {
Class<?> c = Class.forName("com.android.internal.R$dimen");
Object obj = c.newInstance();
Field field = c.getField("status_bar_height");
int x = Integer.parseInt(field.get(obj).toString());
return getResources().getDimensionPixelSize(x);
} catch (Exception e) {
e.printStackTrace();
}
return 0;
}
}

运行效果图

项目地址: 猛戳跳转