如何用 Java 对 PDF 文件进行电子签章(二)生成一个图片签章

参考:

  1. https://blog.csdn.net/javasun608/article/details/79307845
  2. https://blog.csdn.net/zdavb/article/details/50956475

一、圆形图片

如何用 Java 对 PDF 文件进行电子签章(二)生成一个图片签章_第1张图片

Graphics2D

  根据API上的说法是,在使用Graphics2D类库的时候,这是进行操作的主要类,类似于提供了一种context。Graphics2D为抽象类,继承自Graphics类,所以在操作前,需要先获取一种可以操作的对象,然后再创建Graphics2D对象。
  Graphics2D可以支持三种操作:图形操作(画各种图形,填充)、写文字、变换(比如旋转、切割等)

// 创建了bufferedImage对象,可以在最后保存为图片
BufferedImage bi = new BufferedImage(canvasWidth, canvasHeight, BufferedImage.TYPE_INT_ARGB);
// 创建Graphics2D对象
Graphics2D g2d = bi.createGraphics();

填充

  填充图形时,首先要设置画笔,然后调用g2d对象的fill函数,如

// 设置画笔颜色
g2d.setPaint(Color.WHITE);
//填充区域,将区域背景设置为白色
g2d.fillRect(0, 0, canvasWidth, canvasWidth);

  除了fillRect外,还有很多其他填充方法,还有fill(Shape s),因此基本可以满足大多数需求。

画圆

  再画圆的时候,首先要生成一个Shape对象,然后调用g2d.draw(shape)即可,因此,这里面如何生成Shape对象就很重要了。
  可以看一下Shape类结构,shape是一个接口类,实现了众多实现类,比如Arc(弧度)、贝塞尔曲线、椭圆、线等等。这里我们要使用Arc,既然可以生成弧度,那么当然就可以生成圆。

       g2d.setPaint(Color.red);//设置画笔
       g2d.setStroke(new BasicStroke(5));//设置画笔的粗度
       // 构造圆形,Arc2D为抽象类,Arc2D.double和Arc2D.float为它的具体实现类。
       Shape circle = new Arc2D.Double(0,0,circleRadius*2,circleRadius*2,0,360,Arc2D.OPEN);
       g2d.draw(circle);//OK,现在已经有了一个圆

画线

  我们需要的是距离圆心上下等距的两条线。所以,这里我们要知道距离有多远,或者说线段的顶点与圆心的连线,如下图所示,这个夹角的大小,我称为lineArc(注意必须是弧度),根据这个角,我们就得到了上线段的两个顶点(radius-radiussin(α),radius-radiuscos(α)),(radius+radiussin(α),radius-radiuscos(α)),同理下线段的两个顶点也知道了。再然后调用drawLine即可,如果需要更改颜色,或者更改画笔,随时都可以通过set来更改。
如何用 Java 对 PDF 文件进行电子签章(二)生成一个图片签章_第2张图片

 double halfHeight = circleRadius * (Math.cos(lineArc));
 double halfWidth = circleRadius * (Math.sin(lineArc));
 g2d.drawLine((int)(circleRadius-halfWidth),(int)(circleRadius-halfHeight),
              (int)(circleRadius+halfWidth),(int)(circleRadius-halfHeight));//
 g2d.drawLine((int)(circleRadius-halfWidth),(int)(circleRadius+halfHeight),
			  (int)(circleRadius+halfWidth),(int)(circleRadius+halfHeight)

写字

  写字看起来最简单,直接调用g2d.drawString()即可了,是的,是这样,但是又不仅仅如此,因为我们常常对这些字的格式有很高的要求。这得让我们仔细思考两个问题。

  从哪里开始绘制?也就是说固定那个坐标,固定字的左上角?左下角?甚至中间?
  字的宽度是多少?有人说不是由font-size决定吗?的确由它决定,但是它的宽度又不等于font-size。
  这两个问题的答案是至关重要的,因为如果不搞清楚,我们没办法绘制。
  g2d有一个方法是getFontRenderContext,通过它我们可以获取到字体渲染的上下文环境。然后定义一个Font对象,然后在该对象上调用 f.getStringBounds()来得到一个Rectangle2D的对象。而这个对象是一个长方形的对象类。Briliant,从字面意思我们也可以知道getStringBounds可以获取到文字的外轮廓所构成的矩形。
  这个矩阵包含如下几个方法:getX()、getY():分别获取矩形左上角的坐标。getHeight()、getWidth():分别获取矩形的高度、宽度。get CenterX()、getCenterY()表示获取矩形的中心点坐标。
OK,我们来回答上述两个问题
  第一:从哪里开始绘制,我不知道,这个很尴尬,我做了如下实验,这个感觉就好像是以(0,-30)开始绘制一个宽度为40的矩形一样。
  第二:字的宽度,我们可以通过getHeight()来得到字的高度,通过getWidth()来获取字符串的宽度。

       int fontSize = 30;
       Font f = new Font("宋体", Font.PLAIN, fontSize);
       FontRenderContext context = g2d.getFontRenderContext();
       Rectangle2D bounds = f.getStringBounds(center, context);

       System.out.println("X:"+bounds.getX());//X:0.0
       System.out.println("Y:"+bounds.getY());//Y:-30.1611328125
       System.out.println("height:"+bounds.getHeight());//height:40.9716796875
       System.out.println("center y:"+bounds.getCenterY());//center y:-9.67529296875

  那我们如何在我们希望的位置进行绘制呢?很简单,平移。只要将矩形的中心放到我们想要的位置就可以了。因此,我们可以

g2d.drawString(center, (float) (circleRadius - bounds.getCenterX()), 
				(float) (circleRadius - bounds.getCenterY()));

  上面就是将文字的中心点平移到(circleRadius,circleRadius)处。

弧形文字

  印章上可能最复杂的部分就是在于顶部的弧形文字,android的canvas上是利用一条曲线,然后用一个API可以在这条path上写字。但是java应用并没有这样的库,所以需要自己来处理。这里主要指对文字要进行旋转。
旋转是这样实现的:

Font f = new Font(...);
// AffineTransform就是表示线性变换,
f.deriveFont(AffineTransform transform);
// 表示正向旋转α度,这里是通过矩阵变换实现的,详细信息请看API
AffineTransform transform = AffineTransform .getRotateInstance(α);

  弧形文字知道怎么构建了,下一个问题是如何确定第一个文字的位置?
  这里要分两种情况,当文字个数为奇数时,中间的文字正好在圆心的上方,而当文字个数为偶数时,中间两个文字都正好沿着中间线对称排布。

 if(countOfMsg % 2 == 1){//奇数
      firstAngle = (countOfMsg-1)*radianPerInterval/2.0 + Math.PI/2;
  }else{//偶数
      firstAngle = (countOfMsg/2.0-1)*radianPerInterval + radianPerInterval/2.0 +Math.PI/2;
}

其中radianPerInterval是指字和字之間的夾角。

  那夾角是怎麼計算出來的呢?這就是下一個問題了。

  我們知道文字所對應矩形的長度,也知道文字的個數,這樣就可以知道相鄰文字之間的間距了
  上面說的間距,我们可以通过2arcsin(length/(2radius))来计算角度,至于为什么,你画一画就知道了。
下面就可以贴出程序了。

package com.dacas;

import java.awt.*;
import java.awt.font.FontRenderContext;
import java.awt.geom.AffineTransform;
import java.awt.geom.Arc2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;

/**
 * Created by dave on 2016/3/22.
 */
public class GraphicsUtil {

   public static BufferedImage getSeal(String head,String center,String foot,int canvasWidth,int canvasHeight,double lineArc){
       BufferedImage bi = new BufferedImage(canvasWidth, canvasHeight, BufferedImage.TYPE_INT_ARGB);
       Graphics2D g2d = bi.createGraphics();
       //设置画笔
       g2d.setPaint(Color.WHITE);
       g2d.fillRect(0, 0, canvasWidth, canvasWidth);

       int circleRadius = Math.min(canvasWidth,canvasHeight)/2;
       /***********draw circle*************/
       g2d.setPaint(Color.red);
       g2d.setStroke(new BasicStroke(5));//设置画笔的粗度
       Shape circle = new Arc2D.Double(0,0,circleRadius*2,circleRadius*2,0,360,Arc2D.OPEN);
       g2d.draw(circle);
       /************************************/

       /***************draw line*******************/
       double halfHeight = circleRadius * (Math.cos(lineArc));
       double halfWidth = circleRadius * (Math.sin(lineArc));

       g2d.drawLine((int)(circleRadius-halfWidth),(int)(circleRadius-halfHeight),(int)(circleRadius+halfWidth),(int)(circleRadius-halfHeight));//
       g2d.drawLine((int)(circleRadius-halfWidth),(int)(circleRadius+halfHeight),(int)(circleRadius+halfWidth),(int)(circleRadius+halfHeight));//
       /***********************END********************/

       /*****************draw string******************/
       int fontSize = 30;
       Font f = new Font("宋体", Font.PLAIN, fontSize);
       FontRenderContext context = g2d.getFontRenderContext();
       Rectangle2D bounds = f.getStringBounds(center, context);

       System.out.println("X:"+bounds.getX());
       System.out.println("Y:"+bounds.getY());
       System.out.println("height:"+bounds.getHeight());
       System.out.println("center y:"+bounds.getCenterY());
       g2d.setFont(f);
       g2d.drawString(center, (float) (circleRadius - bounds.getCenterX()), (float) (circleRadius - bounds.getCenterY()));

       /********************END*********************/

       /*****************draw foot*******************/
       fontSize = 50;
       f = new Font("黑体",Font.BOLD,fontSize);
       context = g2d.getFontRenderContext();
       bounds = f.getStringBounds(foot,context);
       g2d.setFont(f);
       g2d.drawString(foot, (float) (circleRadius - bounds.getCenterX()), (float) (circleRadius*1.5 - bounds.getCenterY()));

       /***************draw string head**************/
       fontSize = 30;
       f = new Font("宋体",Font.PLAIN,fontSize);
       context = g2d.getFontRenderContext();
       bounds = f.getStringBounds(head,context);

       double msgWidth = bounds.getWidth();
       int countOfMsg = head.length();
       double interval = msgWidth/(countOfMsg-1);//计算间距


       double newRadius = circleRadius + bounds.getY()-5;//bounds.getY()是负数,这样可以将弧形文字固定在圆内了。-5目的是离圆环稍远一点
       double radianPerInterval = 2 * Math.asin(interval / (2 * newRadius));//每个间距对应的角度

       //第一个元素的角度
       double firstAngle;
       if(countOfMsg % 2 == 1){//奇数
           firstAngle = (countOfMsg-1)*radianPerInterval/2.0 + Math.PI/2+0.08;
       }else{//偶数
           firstAngle = (countOfMsg/2.0-1)*radianPerInterval + radianPerInterval/2.0 +Math.PI/2+0.08;
       }

       for(int i = 0;i<countOfMsg;i++){
           double aa = firstAngle - i*radianPerInterval;
           double ax = newRadius * Math.sin(Math.PI/2 - aa);//小小的trick,将【0,pi】区间变换到[pi/2,-pi/2]区间
           double ay = newRadius * Math.cos(aa-Math.PI/2);//同上类似,这样处理就不必再考虑正负的问题了
           AffineTransform transform = AffineTransform .getRotateInstance(Math.PI/2 - aa);// ,x0 + ax, y0 + ay);
           Font f2 = f.deriveFont(transform);
           g2d.setFont(f2);
           g2d.drawString(head.substring(i,i+1), (float) (circleRadius+ax),  (float) (circleRadius - ay));
       }

       g2d.dispose();//销毁资源
       return bi;
   }
}

  看看上面代码,在firstAngle里面多了个0.08,似乎毫无道理,如果没有0.08,那么我们可以预想从数学上 应该是一个完美的印章,但是结果发现并不是非常完美,头部的弧形曲线不是特别对称。

分析一下原因,就可以想明白了

  很多计算其实是有误差的,比如arcsin、以及强制将double转成float时都会丧失精确度,另外,本身做浮点运算就有误差。
  上面一个很重要的点是 计算夹角,而这个夹角的计算并不完全正确,我们这里计算newRadiu是指文字的外轮廓所构成的圆,但是文字之间的长度是通过width/(count-1)计算出来的,这个细细推敲一下,会发现这条线并不在圆上,但是在哪,我们并不清楚。
  处于上述原因,我们需要一个校正ε,经过自己测试,我选定了0.08。

main.java

package com.dacas;

/**
 * Created by dave on 2016/3/22.
 */
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import javax.imageio.ImageIO;

public class main
{
    public static void main(String[] args)
    {
        int canvasWidth = 400;
        int canvasHeight = 400;
        double lineArc = 80*(Math.PI/180);//角度转弧度
        String savepath = "seal.png";
        SimpleDateFormat format = new SimpleDateFormat("yyyy'年'MM'月'dd'日'");
        String head = "中国科学院信息工程研究所";
        String foot = "受理专用章";
        String center = format.format(new Date());
        BufferedImage image = GraphicsUtil.getSeal(head, center, foot, canvasWidth, canvasHeight, lineArc);

        try
        {
            ImageIO.write(image, "PNG", new File(savepath));
        }
        catch (IOException e)
        {
            e.printStackTrace();
        }
    }
}

二、矩形图片

生成一个如下图的签章图片:
如何用 Java 对 PDF 文件进行电子签章(二)生成一个图片签章_第3张图片

  1. 相关代码
    import java.awt.Color;
    import java.awt.Font;
    import java.awt.FontMetrics;
    import java.awt.Graphics2D;
    import java.awt.RenderingHints;
    import java.awt.image.BufferedImage;
    import java.io.FileOutputStream;
    import java.io.IOException;
    import sun.font.FontDesignMetrics;
 
    import com.sun.image.codec.jpeg.JPEGCodec;
    import com.sun.image.codec.jpeg.JPEGEncodeParam;
    import com.sun.image.codec.jpeg.JPEGImageEncoder;
 
    public class SignImage {
 
    /**
     * @param doctorName
     *            String 医生名字
     * @param hospitalName
     *            String 医生名称
     * @param date
     *            String 签名日期
     *            图片高度
     * @param jpgname
     *            String jpg图片名
     * @return
     */
    public static boolean createSignTextImg(
            String doctorName, //
            String hospitalName, //
            String date, 
            String jpgname) {
        int width = 255;//图片的宽度
        int height = 100;//图片的高度
        FileOutputStream out = null;
        //背景色
        Color bgcolor = Color.WHITE;
        //字色
        Color fontcolor = Color.RED;
        Font doctorNameFont = new Font(null, Font.BOLD, 20);
        Font othorTextFont = new Font(null, Font.BOLD, 18);
        try { // 宽度 高度
            BufferedImage bimage = new BufferedImage(width, height,  BufferedImage.TYPE_INT_RGB);
            Graphics2D g = bimage.createGraphics();
            g.setColor(bgcolor); // 背景色
            g.fillRect(0, 0, width, height); // 画一个矩形
            g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,  RenderingHints.VALUE_ANTIALIAS_ON); // 去除锯齿(当设置的字体过大的时候,会出现锯齿)
 
            g.setColor(Color.RED);
            g.fillRect(0, 0, 8, height);
            g.fillRect(0, 0, width, 8);
            g.fillRect(0, height - 8, width, height);
            g.fillRect(width - 8, 0, width, height);
 
            g.setColor(fontcolor); // 字的颜色
            g.setFont(doctorNameFont); // 字体字形字号
            FontMetrics fm = FontDesignMetrics.getMetrics(doctorNameFont);
            int font1_Hight = fm.getHeight();
            int strWidth = fm.stringWidth(doctorName);
            int y = 35;
            int x = (width - strWidth) / 2;
            g.drawString(doctorName, x, y); // 在指定坐标处 添加文字
 
            g.setFont(othorTextFont); // 字体字形字号
 
            fm = FontDesignMetrics.getMetrics(othorTextFont);
            int font2_Hight = fm.getHeight();
            strWidth = fm.stringWidth(hospitalName);
            x = (width - strWidth) / 2;
            g.drawString(hospitalName, x, y + font1_Hight); // 在指定坐标处添加文字
 
            strWidth = fm.stringWidth(date);
            x = (width - strWidth) / 2;
            g.drawString(date, x, y + font1_Hight + font2_Hight); // 在指定坐标处添加文字
 
            g.dispose();
            out = new FileOutputStream(jpgname); // 指定输出文件
            JPEGImageEncoder encoder = JPEGCodec.createJPEGEncoder(out);
            JPEGEncodeParam param = encoder.getDefaultJPEGEncodeParam(bimage);
            param.setQuality(50f, true);
            encoder.encode(bimage, param); // 存盘
            out.flush();
            return true;
        } catch (Exception e) {
            return false;
        }finally{
            if(out!=null){
                try {
                    out.close();
                } catch (IOException e) {
                }
            }
        }
    }
    public static void main(String[] args) {
        createSignTextImg("华佗", "在线医院", "2018.01.01",   "sign.jpg");
    }
}

你可能感兴趣的:(pdf)