前几篇文章我们分别介绍了显示文件列表、解析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页面进行操作,不过没有实现成功。对于手写签名,在很多领域都用到了,目前主要以收费为主,比如一些认证中心,他们在提供签证与验签时,也提供相关的签名过程。欢迎提出更好的实现方法,大家一起进步……
2016年10月21日备注说明:
各位博友您好,很抱歉,由于本人目前已经从开发工作主要转向互联网创业,很久没有更新博客内容,一些博友的提问也未能及时回复,敬请见谅!
为保持和各位博友的互动,如遇到什么问题,可以到我们团队里面找我或者我们团队,有什么能帮助大家的,我们相互沟通: