[android开发]手写签名系统的设计与实现之实现pdf文件上手写签名效果(五)---完

http://www.2cto.com/kf/201312/266046.html


前几篇文章我们分别介绍了显示文件列表、解析pdf、手写画板及画笔设置的功能了,今天我们就介绍一下,最后最关键的一部分-手写签名效果。先看看效果图:

选定位置 画板上写字 预览签名效果

\ \ \

一、实现原理:

对于pdf文件上进行相关操作,本人并没找到一些比较好的方法,为了实现签名效果,尝试了很多方法也没达到预期效果,最后这种实现方法相对好些,也比较简单。其基本思想是根据对pdf文件加印章及水印来实现的,事先我们准备一张透明的png图片,当做手写画板的背景图片,写字时实际就在这张图片操作了,最后将手写的画板内容重新保存一种新的png背景透明图片,就是对pdf文件的操作了,对pdf操作要用到第三方jar包droidText0.5.jar包,通过里面的方法Image img = Image.getInstance(InPicFilePath);完成将透明图片加入到pdf文件上,最后重新生成新的pdf文件,签名就完成了。并不是直接对pdf文件进行操作,不知道其他的实现方法有哪些,也请告知一下。下面我就把自己实现具体过程介绍一下:

新建一个类,用于处理签名pdf文件HandWriteToPDF.java:

 

?
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
package com.xinhui.handwrite;
 
import java.io.FileOutputStream;
import java.io.InputStream;
import java.security.PrivateKey;
import java.security.PublicKey;
 
import android.util.Log;
 
import com.artifex.mupdf.MuPDFActivity;
import com.artifex.mupdf.MuPDFPageView;
import com.lowagie.text.Image;
import com.lowagie.text.pdf.PdfArray;
import com.lowagie.text.pdf.PdfContentByte;
import com.lowagie.text.pdf.PdfDictionary;
import com.lowagie.text.pdf.PdfName;
import com.lowagie.text.pdf.PdfObject;
import com.lowagie.text.pdf.PdfReader;
import com.lowagie.text.pdf.PdfStamper;
import com.xinhui.electronicsignature.R;
 
 
 
public class HandWriteToPDF {
     private String InPdfFilePath;
     private String outPdfFilePath;
     private String InPicFilePath;
     public static int writePageNumble; //要签名的页码
     HandWriteToPDF(){
         
     }
     public HandWriteToPDF(String InPdfFilePath,String outPdfFilePath,String InPicFilePath){
         this .InPdfFilePath = InPdfFilePath;
         this .outPdfFilePath = outPdfFilePath;
         this .InPicFilePath = InPicFilePath;
     }
     public String getInPdfFilePath() {
         return InPdfFilePath;
     }
     public void setInPdfFilePath(String inPdfFilePath) {
         InPdfFilePath = inPdfFilePath;
     }
     public String getOutPdfFilePath() {
         return outPdfFilePath;
     }
     public void setOutPdfFilePath(String outPdfFilePath) {
         this .outPdfFilePath = outPdfFilePath;
     }
     public String getInPicFilePath() {
         return InPicFilePath;
     }
     public void setInPicFilePath(String inPicFilePath) {
         InPicFilePath = inPicFilePath;
     }
     
     public void addText(){
         try {
             PdfReader reader = new PdfReader(InPdfFilePath, "PDF" .getBytes()); //选择需要印章的pdf
             FileOutputStream outStream = new FileOutputStream(outPdfFilePath);
             PdfStamper stamp;
             stamp = new PdfStamper(reader, outStream); //加完印章后的pdf
             PdfContentByte over = stamp.getOverContent(writePageNumble); //设置在第几页打印印章
             //用pdfreader获得当前页字典对象.包含了该页的一些数据.比如该页的坐标轴信
             PdfDictionary p = reader.getPageN(writePageNumble);
             //拿到mediaBox 里面放着该页pdf的大小信息. 
             PdfObject po =  p.get( new PdfName( "MediaBox" ));
             //po是一个数组对象.里面包含了该页pdf的坐标轴范围. 
             PdfArray pa = (PdfArray) po; 
             Image img = Image.getInstance(InPicFilePath); //选择图片
             img.setAlignment( 1 );
             img.scaleAbsolute( 300 , 150 ); //控制图片大小,原始比例720:360
             //调用书写pdf位置方法
             writingPosition(img ,pa.getAsNumber(pa.size()- 1 ).floatValue());
             over.addImage(img);        
             stamp.close();
         } catch (Exception e) {
             // TODO Auto-generated catch block
             e.printStackTrace();
         }
     }
     /**
      * 功能:处理要书写pdf位置
      * @param img
      */
     private void writingPosition(Image img , float pdfHigth){
         
         int pdfSizeX = MuPDFPageView.pdfSizeX;
         int pdfSizeY = MuPDFPageView.pdfSizeY;
         int pdfPatchX = MuPDFPageView.pdfPatchX;
         int pdfPatchY = MuPDFPageView.pdfPatchY;
         int pdfPatchWidth = MuPDFPageView.pdfPatchWidth;
         int pdfPatchHeight = MuPDFPageView.pdfPatchHeight;
         int y = MuPDFActivity.y+ 180 ;
         float n = pdfPatchWidth* 1 .0f;
         float m = pdfPatchHeight* 1 .0f;
         n = pdfSizeX/n;
         m = pdfSizeY/m;
         if (n == 1 .0f){
             //pdf页面没有放大时的比例
             if (MuPDFActivity.y >= 900 ){
                 img.setAbsolutePosition(MuPDFActivity.x* 5 / 6 , 0 );
             } else if (MuPDFActivity.y <= 60 ){
                 img.setAbsolutePosition(MuPDFActivity.x* 5 / 6 ,pdfHigth- 150 );
             } else {
                 img.setAbsolutePosition(MuPDFActivity.x* 5 / 6 ,pdfHigth-((MuPDFActivity.y+ 120 )* 5 / 6 ));
             }
         } else {
             //pdf页面放大时的比例,这里坐标不精确???
             n = (MuPDFActivity.x+pdfPatchX)/n;
             m = (MuPDFActivity.y+pdfPatchY)/m;
             img.setAbsolutePosition(n* 5 / 6 ,pdfHigth-((m+ 120 )* 5 / 6 ));
         }
     }
}

其中:

 

 

?
1
img.setAbsolutePosition(MuPDFActivity.x* 5 / 6 ,pdfHigth-((MuPDFActivity.y+ 120 )* 5 / 6 ));
是用来控制要在pdf文件上的签名位置,这个得说说,我并没有搞明白怎么设置这个坐标值才是最合适的,屏幕和pdf文件的坐标原点都是在左上角,setAbsolutPosition(float x,float y)的坐标原点是在屏幕的左下角,还有就是pdf实际分辨率和手机的实际分辨率又不是相同的,在我移动坐标,通过获取的手机屏幕的坐标:

 

 

?
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
/**
      * 功能:自定义一个显示截屏区域视图方法
      * */
     public void screenShot(MotionEvent e2){
         //这里实现截屏区域控制
         /*if(MuPDFActivity.screenShotView == null || !(MuPDFActivity.screenShotView.isShown())){
             MuPDFActivity.screenShotView = new MyView(MuPDFActivity.THIS);
             MuPDFActivity.THIS.addContentView(MuPDFActivity.screenShotView, new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
         }*/
         MuPDFActivity.oX = ( int ) e2.getX();
         MuPDFActivity.oY = ( int ) e2.getY();
         View screenView = new View(MuPDFActivity.THIS);
         screenView = MuPDFActivity.THIS.getWindow().getDecorView();
         screenView.setDrawingCacheEnabled( true );
         screenView.buildDrawingCache();
         Bitmap screenbitmap = screenView.getDrawingCache();
         screenWidth = screenbitmap.getWidth();
         screenHeight = screenbitmap.getHeight();
         int oX = MuPDFActivity.oX;
         int oY = MuPDFActivity.oY;
         int x = 0  ;
         int y = 0 ;
         int m = 0 ;
         int n = 0 ;
         //oX = (int) event.getX();
         //oY = (int) event.getY();
         if (oX - 180 <= 0 ){
             if (oY - 90 <= 0 ){
                 //左边界和上边界同时出界
                 x = 0 ;
                 y = 0 ;
                 m = 360 ;
                 n = 180 ;
             } else if (oY + 90 >= screenHeight){
                 //左边界和下边界同时出界
                 x = 0 ;
                 y = screenHeight - 180 ;
                 m = 360 ;
                 n = screenHeight;
             } else {
                 //只有左边界
                 x = 0 ;
                 y = oY - 90 ;
                 m = 360 ;
                 n = y + 180 ;
             }
         } else if (oX + 180 >= screenWidth){
             if (oY - 90 <= 0 ){
                 //右边界和上边界同时出界
                 x = screenWidth - 360 ;
                 y = 0 ;
                 m = screenWidth;
                 n = y + 180 ;
             } else if (oY + 90 >= screenHeight){
                 //右边界和下边界同时出界
                 
                 
             } else {
                 //只有右边界出界
                 x = screenWidth - 360 ;
                 y = oY - 90 ;
                 m = screenWidth;
                 n = y + 180 ;
             }
         } else if (oY - 90 <= 0 ){
             //只有上边界出界
             x = oX - 90 ;
             y = 0 ;
             m = x + 360 ;
             n = y + 180 ;
         } else if (oY + 90 >= screenHeight){
             //只有下边界出界
             x = oX - 180 ;
             y = screenHeight - 180 ;
             m = x + 360 ;
             n = y + 180 ;
         } else {
             //都不出界
             x = oX - 180 ;
             y = oY - 90 ;
             m = x + 360 ;
             n = y + 180 ;
         }
         //根据屏幕坐标,显示要截图的区域范围
         MuPDFActivity.x = x;
         MuPDFActivity.y = y;
         MuPDFActivity.screenShotView.setSeat(x, y, m, n);
         MuPDFActivity.screenShotView.postInvalidate();
}
在类ReaderView.java中实现动态获取坐标,本人并没有搞明白屏幕坐标和pdf文件坐标已经setAbsolutPosition()方法三者之间的关系,通过很多数据测试得出了在没有放大pdf页面的情况下关系如下:

 

 

?
1
img.setAbsolutePosition(MuPDFActivity.x* 5 / 6 ,pdfHigth-((MuPDFActivity.y+ 120 )* 5 / 6 ));
也就是屏幕坐标和setAbsolutPosition()坐标是5:6的关系,这样对于没有放大页面的情况下,坐标对应是非常准确的,但是当方法pdf页面再去取坐标时,就混乱了,这个问题目前还没有解决,知道好的方法的朋友一定记得告诉在下呀!

 

定义了三个用于保存文件路径的变量:

 

?
1
2
3
private String InPdfFilePath;
private String outPdfFilePath;
private String InPicFilePath;
类建好后,就是在MuPdfActivity类中对相关按钮功能进行完善了:

 

 

?
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
@Override
     public void onClick(View v) {
         // TODO Auto-generated method stub
         switch (v.getId()) {
         case R.id.cancel_bt: //撤销已签名pdf文件
             if (isPreviewPDF){
                 AlertDialog.Builder builder = new Builder( this );
                 builder.setTitle( "提醒:撤销后,已签名文件文件将无法恢复,是否继续?" )
                 .setPositiveButton( "继续" , new DialogInterface.OnClickListener() {
                     
                     @Override
                     public void onClick(DialogInterface arg0, int arg1) {
                         // TODO Auto-generated method stub
                         try {
                             core = openFile(InPdfFilePath);
                             File file = new File(OutPdfFilePath);
                             file.delete();
                         } catch (Exception e) {
                             // TODO: handle exception
                             Toast.makeText(MuPDFActivity. this , "无法打开该文件" , Toast.LENGTH_SHORT).show();
                         }
                         createUI(InstanceState);
                         isPreviewPDF = false ; //重新解析pdf,恢复初始值
                         ReaderView.NoTouch = true ; //重新释放对pdf手势操作
                         isScreenShotViewShow = false ; //重新解析pdf,恢复初始值
                         isWriting = false ; //
                         showButtonsDisabled = false ;
                     }
                 })
                 .setNegativeButton( "取消" , null )
                 .create()
                 .show();
             } else {
                 Toast.makeText( this , "没有要撤销的签名文件" , Toast.LENGTH_SHORT).show();
             }
             break ;
         case R.id.clear_bt: //清除画板字迹
             if (mAddPicButton.getContentDescription().equals( "取消签名" )){
                 handWritingView.clear();
             } else {
                 Toast.makeText( this , "手写板未打开" , Toast.LENGTH_SHORT).show();
             }
             break ;
         case R.id.add_pic_bt: //打开手写画板
             //记录当前签名页码
             writingPageNumble = mDocView.getDisplayedViewIndex();
             if (mAddPicButton.getContentDescription().equals( "开始签名" )){
                 if (screenShotView.isShown()){
                     screenShotView.setVisibility(View.INVISIBLE);
                     handWritingView.setVisibility(View.VISIBLE);
                     
                     mAddPicButton.setContentDescription( "取消签名" );
                     mScreenShot.setContentDescription( "锁定屏幕" );
                     isWriting = true ;
                 } else if (isPreviewPDF){
                     Toast.makeText(MuPDFActivity. this , "预览模式" , Toast.LENGTH_SHORT).show();
                 } else {
                     Toast.makeText(MuPDFActivity. this , "请先选定书写区域" , Toast.LENGTH_SHORT).show();
                 }
             } else {
                 handWritingView.setVisibility(View.GONE);
                 mAddPicButton.setContentDescription( "开始签名" );
                 isWriting = false ;
                 ReaderView.NoTouch = true ; //释放pdf手势操作
             }
             break ;
         case R.id.screenshot_ib: //打开区域选择view
             if (screenShotView == null ){
                 screenShotView = new ScreenShotView( this );
             }
             if (isPreviewPDF){
                 Toast.makeText(MuPDFActivity. this , "预览模式" , Toast.LENGTH_SHORT).show();
             } else if (!isPreviewPDF && isWriting){
                 Toast.makeText(MuPDFActivity. this , "正在签名……" , Toast.LENGTH_SHORT).show();
             } else {
                 if (!screenShotView.isShown() && !isScreenShotViewShow){
                     this .addContentView(screenShotView,
                             new LayoutParams(LayoutParams.WRAP_CONTENT,
                                     LayoutParams.WRAP_CONTENT));
                     
                     
                     screenShotView.setSeat(x, y, x+ 360 , y+ 180 );
                     screenShotView.postInvalidate();
                     isScreenShotViewShow = true ;
                 }
                 if (mScreenShot.getContentDescription().equals( "锁定屏幕" )){
                     ReaderView.NoTouch = false ;
                     mScreenShot.setContentDescription( "释放屏幕" );
                     screenShotView.setVisibility(View.VISIBLE);
                 } else {
                     ReaderView.NoTouch = true ;
                     mScreenShot.setContentDescription( "锁定屏幕" );
                     screenShotView.setVisibility(View.INVISIBLE);
                 }
             }
             break ;
         case R.id.confirm_bt: //保存签名文件
             if (mAddPicButton.getContentDescription().equals( "取消签名" )){
                 saveImageAsyncTask asyncTask = new saveImageAsyncTask( this );
                 asyncTask.execute();
                 ReaderView.NoTouch = true ;
                 handWritingView.setVisibility(View.INVISIBLE);
                 mAddPicButton.setContentDescription( "开始签名" );
                 isPreviewPDF = true ;
                 showButtonsDisabled = false ;
             } else {
                 Toast.makeText( this , "没有要保存的签名文件" , Toast.LENGTH_SHORT).show();
             }
             break ;
         default :
             break ;
}
在保存文件时,对于程序来说,是比较耗时了,这样我们就用到了异步任务来完成耗时的操作:

 

 

?
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
/**
           * 运行在UI线程中,在调用doInBackground()之前执行
           */
          @Override
         protected void onPreExecute() {
             // TODO Auto-generated method stub
              Toast.makeText(context, "正在处理……" ,Toast.LENGTH_SHORT).show(); 
         }
          
          /**
           *后台运行的方法,可以运行非UI线程,可以执行耗时的方法
           */ 
         @Override
         protected Integer doInBackground(Void... arg0) {
             // TODO Auto-generated method stub
             mSaveImage();
             return null ;
         }
         
         /**
          * 运行在ui线程中,在doInBackground()执行完毕后执行
          */
          @Override
         protected void onPostExecute(Integer result) {
             // TODO Auto-generated method stub
             //super.onPostExecute(result);
             createUI(InstanceState);
             Toast.makeText(context, "签名完成" ,Toast.LENGTH_SHORT).show();
         }
         /**
          * 在publishProgress()被调用以后执行,publishProgress()用于更新进度
          */
          @Override
         protected void onProgressUpdate(Integer... values) {
             // TODO Auto-generated method stub
             super .onProgressUpdate(values);
         }
     }
     /**
      * 功能:处理书写完毕的画板,重新生成bitmap
      */
     public void mSaveImage(){
         HandWritingView.saveImage = Bitmap.createBitmap(handWritingView.HandWriting(HandWritingView.new1Bitmap));
         HandWritingView mv = handWritingView;
         storeInSDBitmap = mv.saveImage();
         Canvas canvas = new Canvas(storeInSDBitmap);
         Paint paint = new Paint();
         canvas.drawARGB( 0 , 0 , 0 , 0 );
         canvas.isOpaque();
         paint.setAlpha( 255 ); //设置签名水印透明度
         //这个方法  第一个参数是图片原来的大小,第二个参数是 绘画该图片需显示多少。
         //也就是说你想绘画该图片的某一些地方,而不是全部图片,
         //第三个参数表示该图片绘画的位置.
         canvas.drawBitmap(storeInSDBitmap, 0 , 0 , paint);
         storeInSD(storeInSDBitmap); //保存签名过的pdf文件
         previewPDFShow();
     }
     
     /**
      * 功能:预览签名过的pdf
      */
     public  void previewPDFShow(){
         String openNewPath = OutPdfFilePath;
         
         try {
             core = openFile(openNewPath); //打开已经签名好的文件进行预览
             //截屏坐标恢复默认
             x = 200 ;
             y = 200 ;
         } catch (Exception e) {
             // TODO: handle exception
             Log.e( "info" , "------打开失败" );
         }
     }
     
     /**
      * 功能:将签好名的bitmap保存到sd卡
      * @param bitmap
      */
     public static void storeInSD(Bitmap bitmap) {
         File file = new File( "/sdcard/签名" ); //要保存的文件地址和文件名
         if (!file.exists()) {
             file.mkdir();
         }
         File imageFile = new File(file, "签名" + ".png" );
         try {
             imageFile.createNewFile();
             FileOutputStream fos = new FileOutputStream(imageFile);
             bitmap.compress(Bitmap.CompressFormat.PNG, 100 , fos);
             fos.flush();
             fos.close();
             addTextToPdf();
         } catch (FileNotFoundException e) {
             // TODO Auto-generated catch block
             e.printStackTrace();
         } catch (IOException e) {
             // TODO Auto-generated catch block
             e.printStackTrace();
         }
     }
     
     public static void addTextToPdf(){
         String  SDCardRoot = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator;
         SimpleDateFormat formatter = new SimpleDateFormat( "yyyy-MM-dd-HH-mm-ss" );      
         Date curDate = new Date(System.currentTimeMillis()); //获取当前时间      
         String currentSystemTime = formatter.format(curDate);   
         InPdfFilePath = MuPDFActivity.PATH;
         OutPdfFilePath = SDCardRoot+ "/签名/已签名文件" +currentSystemTime+ ".pdf" ;
         InPicFilePath = SDCardRoot+ "/签名/签名.png" ;
         HandWriteToPDF handWriteToPDF = new HandWriteToPDF(InPdfFilePath, OutPdfFilePath, InPicFilePath);
         handWriteToPDF.addText();
     }

由于这一排操作按钮 \ 之间存在逻辑关系:比如没有确定签名位置不能打开画板等,我们就需要判断什么时候应该打开,什么打不开并提示,这样我们就定义几个布尔类型的变量:

 

 

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
      * 判断是否为预览pdf模式
      */
     public static boolean isPreviewPDF = false ;
     /**
      * 判断是否正在书写
      */
     public static boolean isWriting = false ;
     /**
      * 判断页面按钮是否显示
      */
     private boolean showButtonsDisabled;
     /**
      * 判断截屏视图框是否显示
      */
     private static boolean isScreenShotViewShow = false ;
?
1
2
3
4
/**
      * NoTouch =false 屏蔽pdf手势操作,为true时释放pdf手势操作
      */
     public static boolean NoTouch = true ;


为了在我们进行选择位置和签名时pdf不会再监听手势操作,我们要做相应的屏蔽:

 

 

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public boolean onScale(ScaleGestureDetector detector) {
         //截屏视图不显示时,手势操作可以进行
         if (NoTouch){
             float previousScale = mScale;
             mScale = Math.min(Math.max(mScale * detector.getScaleFactor(), MIN_SCALE), MAX_SCALE);
             scalingFactor = mScale/previousScale; //缩放比例
             //Log.e("info", "--->scalingFactor="+scalingFactor);
             View v = mChildViews.get(mCurrent);
             if (v != null ) {
                 // Work out the focus point relative to the view top left
                 int viewFocusX = ( int )detector.getFocusX() - (v.getLeft() + mXScroll);
                 int viewFocusY = ( int )detector.getFocusY() - (v.getTop() + mYScroll);
                 // Scroll to maintain the focus point
                 mXScroll += viewFocusX - viewFocusX * scalingFactor;
                 mYScroll += viewFocusY - viewFocusY * scalingFactor;
                 requestLayout();
             }
         }
         return true ;
}
到此在pdf文件上签名就完成了,采用的方法也是比较局限,目前能匹配上的只限于手机的分辨率为720*1280,其他分辨手机没有做适配。由于每次是一边写文章,一边写代码,项目会存在很多不完善的,欢迎指正。在文章的最后,我会把今天的项目代码附上 下载 链接地址,需要的朋友可以下载分享。
二、总结:

 

在做对于pdf文件的签名时,查阅了很多资料,也没有看到相对较好的实例,结合自己的想法和网上的一些资料,一步一步最终实现了签名的效果,其中存在的问题也是很大,最主要的就是坐标匹配问题,放大页面就无法正常匹配了。之前想做的效果,就是直接在pdf页面进行操作,不过没有实现成功。对于手写签名,在很多领域都用到了,目前主要以收费为主,比如一些认证中心,他们在提供签证与验签时,也提供相关的签名过程。欢迎提出更好的实现方法,大家一起进步……

 

 

【android开发】手写签名 系统 的设计与实现(完整版)

 


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值