[android开发]手写签名系统的设计与实现之实现pdf文件上手写签名效果(五)---完
前几篇文章我们分别介绍了显示文件列表、解析pdf、手写画板及画笔设置的功能了,今天我们就介绍一下,最后最关键的一部分-手写签名效果。先看看效果图:
选定位置 画板上写字 预览签名效果
一、实现原理:
对于pdf文件上进行相关操作,本人并没找到一些比较好的方法,为了实现签名效果,尝试了很多方法也没达到预期效果,最后这种实现方法相对好些,也比较简单。其基本思想是根据对pdf文件加印章及水印来实现的,事先我们准备一张透明的png图片,当做手写画板的背景图片,写字时实际就在这张图片操作了,最后将手写的画板内容重新保存一种新的png背景透明图片,就是对pdf文件的操作了,对pdf操作要用到第三方jar包droidText0.5.jar包,通过里面的方法Image img = Image.getInstance(InPicFilePath);完成将透明图片加入到pdf文件上,最后重新生成新的pdf文件,签名就完成了。并不是直接对pdf文件进行操作,不知道其他的实现方法有哪些,也请告知一下。下面我就把自己实现具体过程介绍一下:
新建一个类,用于处理签名pdf文件HandWriteToPDF.java:
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)); } }}
其中:
img.setAbsolutePosition(MuPDFActivity.x*5/6,pdfHigth-((MuPDFActivity.y+120)*5/6));是用来控制要在pdf文件上的签名位置,这个得说说,我并没有搞明白怎么设置这个坐标值才是最合适的,屏幕和pdf文件的坐标原点都是在左上角,setAbsolutPosition(float x,float y)的坐标原点是在屏幕的左下角,还有就是pdf实际分辨率和手机的实际分辨率又不是相同的,在我移动坐标,通过获取的手机屏幕的坐标:
/** * 功能:自定义一个显示截屏区域视图方法 * */ 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页面的情况下关系如下:
img.setAbsolutePosition(MuPDFActivity.x*5/6,pdfHigth-((MuPDFActivity.y+120)*5/6));也就是屏幕坐标和setAbsolutPosition()坐标是5:6的关系,这样对于没有放大页面的情况下,坐标对应是非常准确的,但是当方法pdf页面再去取坐标时,就混乱了,这个问题目前还没有解决,知道好的方法的朋友一定记得告诉在下呀!
定义了三个用于保存文件路径的变量:
private String InPdfFilePath;private String outPdfFilePath;private String InPicFilePath;类建好后,就是在MuPdfActivity类中对相关按钮功能进行完善了:
@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;}在保存文件时,对于程序来说,是比较耗时了,这样我们就用到了异步任务来完成耗时的操作:
/** * 运行在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(); }
由于这一排操作按钮之间存在逻辑关系:比如没有确定签名位置不能打开画板等,我们就需要判断什么时候应该打开,什么打不开并提示,这样我们就定义几个布尔类型的变量:
/** * 判断是否为预览pdf模式 */ public static boolean isPreviewPDF = false; /** * 判断是否正在书写 */ public static boolean isWriting = false; /** * 判断页面按钮是否显示 */ private boolean showButtonsDisabled; /** * 判断截屏视图框是否显示 */ private static boolean isScreenShotViewShow = false;
/** * NoTouch =false 屏蔽pdf手势操作,为true时释放pdf手势操作 */ public static boolean NoTouch = true;
为了在我们进行选择位置和签名时pdf不会再监听手势操作,我们要做相应的屏蔽:
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开发】手写签名系统的设计与实现(完整版)
- 01-11全球最受赞誉公司揭晓:苹果连续九年第一
- 12-09罗伯特·莫里斯:让黑客真正变黑
- 12-09谁闯入了中国网络?揭秘美国绝密黑客小组TA
- 12-09警示:iOS6 惊现“闪退”BUG
- 04-29通用智能人“通通”亮相中关村论坛
- 04-29拼多多投入45亿补贴,助力上海“五五购物节
- 04-29通义千问再开源 推出最大尺寸1100亿参数模型
- 04-29【环球视线】比亚迪交付首列出海云轨
- 04-21中国产品数字护照体系加速建设