PDF电子签名A4实现

1 篇文章 0 订阅


一、前言

  • 需要进行电子签名的html文件内容,一般为富文本编辑器对应代码。
  • 用户电子签名完成后的签名图片访问地址,例如:http://*****/hlwft/2_1659677726496.jpg。
  • 当前仅实现了PageSize.A4格式的底部签名和左/中/右对齐。
  • 签名图片访问支持http/https两种形式,http信任所有的证书与主机的客户端。

二、工程依赖(maven工程)

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <version>2.3.12.RELEASE</version>
</dependency>
<!-- Lombok -->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <scope>provided</scope>
</dependency>
<!--pdf文件生成-->
<dependency>
    <groupId>com.itextpdf</groupId>
    <artifactId>html2pdf</artifactId>
    <version>4.0.4</version>
</dependency>
<dependency>
    <groupId>com.itextpdf</groupId>
    <artifactId>font-asian</artifactId>
    <version>7.2.4</version>
</dependency>
<dependency>
    <groupId>commons-fileupload</groupId>
    <artifactId>commons-fileupload</artifactId>
    <version>1.4</version>
</dependency>
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.12.0</version>
</dependency>
<!--http工具-->
<dependency>
    <groupId>com.squareup.okhttp3</groupId>
    <artifactId>okhttp</artifactId>
    <version>4.9.3</version>
</dependency>
<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpcore</artifactId>
    <version>4.4.16</version>
</dependency>

三、工程代码示例(java)

  • 图片配置类
public final class ImgLayout {
 
    private ImgLayout() {
    }
 
    /**
     * 图片的宽度
     * {@value}
     */
    public static final int IMAGE_WIDTH = 40;
    /**
     * 图片的高度
     * {@value}
     */
    public static final int IMAGE_HEIGHT = 40;
 
    /**
     * 边界间距
     */
    public static final int BORDER_INTERVAL = 8;
}
  • 签名配置类
import com.itextpdf.layout.properties.TextAlignment;
 
public final class SignStyle {
 
    private SignStyle() {
    }
 
    /**
     * 是否每页签名:默认最后一页签名
     * <ul>
     * <li>{@code Boolean.FALSE}:最后一页签名</li>
     * <li>{@code Boolean.TRUE}:每一页都生成签名</li>
     * </ul>
     */
    public static final boolean SIGN_EACH_PAGE = Boolean.FALSE;
 
    /**
     * 对齐方式:默认居中对齐
     * <strong>
     * 仅支持三种类型{@link TextAlignment.LEFT}/{@link TextAlignment.CENTER}/{@link TextAlignment.RIGHT}
     * </strong>
     */
    public static final TextAlignment SIGN_ALIGN = TextAlignment.CENTER;
 
}
  • pdf文件样式配置类
public final class PdfStyle {
 
    private PdfStyle() {
    }
 
    /**
     * PDF页码字号
     * {@value}
     */
    public static final int PAGE_FONT_SIZE = 14;
 
    /**
     * 字体文件目录
     * <h4>windows/linux系统字体文件目录一般如下</h4>
     * ul>
     *   <li>windows字体文件目录:C:/Windows/Fonts</li>
     *   <li>linux字体文件目录:/usr/share/fonts</li>
     * </ul>
     */
    public static final String FONT_PATH = "C:/Windows/Fonts";
 
}
  • UnsafeOkHttp工具类
import com.itextpdf.commons.utils.FileUtil;
import com.itextpdf.io.util.StreamUtil;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
import org.apache.http.util.TextUtils;
import org.springframework.util.CollectionUtils;
 
import javax.net.ssl.*;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.Map;
 
public class UnsafeOkHttp {
 
    /**
     * 信任所有的证书与主机的客户端
     * {@value}
     */
    public static OkHttpClient UNSAFE_OKHTTP_CLIENT;
 
    static {
        try {
            final TrustManager[] trustAllCerts = new TrustManager[]{
 
                    new X509TrustManager() {
 
                        @Override
                        public void checkClientTrusted(java.security.cert.X509Certificate[] chain, String authType) {
 
                        }
 
                        @Override
                        public void checkServerTrusted(java.security.cert.X509Certificate[] chain, String authType) {
                        }
 
                        @Override
                        public java.security.cert.X509Certificate[] getAcceptedIssuers() {
                            return new java.security.cert.X509Certificate[]{};
                        }
                    }
            };
 
            SSLContext sslContext = SSLContext.getInstance("TLS");
            sslContext.init(null, trustAllCerts, new java.security.SecureRandom());
            SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
            OkHttpClient.Builder builder = new OkHttpClient.Builder();
            builder.sslSocketFactory(sslSocketFactory, (X509TrustManager) trustAllCerts[0]);
            builder.hostnameVerifier(new HostnameVerifier() {
                // 可以将不需要忽略的域名放入数组,也可为空(忽略所有证书)
                String[] arr = new String[]{};
 
                @Override
                public boolean verify(String hostname, SSLSession session) {
                    if (TextUtils.isEmpty(hostname)) {
                        return false;
                    }
                    return !Arrays.asList(arr).contains(hostname);
                }
            });
 
            UNSAFE_OKHTTP_CLIENT = builder.build();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
 
    /**
     * 发起HTTPGET请求
     *
     * @param url
     * @param headerMap
     * @return
     * @throws Exception
     */
    public static Response unsafeGet(String url, Map<String, String> headerMap) throws IOException {
        Request.Builder builder = new Request.Builder().url(url);
        if (!CollectionUtils.isEmpty(headerMap)) {
            headerMap.forEach((name, value) -> builder.addHeader(name, value));
        }
        Request request = builder.build();
        return UNSAFE_OKHTTP_CLIENT.newCall(request).execute();
    }
 
    /**
     * 发起HTTPGET请求,并将返回的内容写入到文件中
     *
     * @param url
     * @param headerMap
     * @param target
     * @return
     * @throws Exception
     */
    public static void unsafeGetToFile(String url, Map<String, String> headerMap, File target) throws IOException {
        Request.Builder builder = new Request.Builder().url(url);
        if (!CollectionUtils.isEmpty(headerMap)) {
            headerMap.forEach((name, value) -> builder.addHeader(name, value));
        }
        Response response = UNSAFE_OKHTTP_CLIENT.newCall(builder.build()).execute();
        try (FileOutputStream fileOutputStream = new FileOutputStream(target)) {
            StreamUtil.transferBytes(response.body().byteStream(), fileOutputStream);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
 
    /**
     * 发起HTTPGET请求,并将返回临时文件
     *
     * @param url
     * @param headerMap
     * @return 返回临时文件,使用后需手动删除
     * @throws Exception
     */
    public static File unsafeGetToFile(String url, Map<String, String> headerMap) throws IOException {
        Request.Builder builder = new Request.Builder().url(url);
        if (!CollectionUtils.isEmpty(headerMap)) {
            headerMap.forEach((name, value) -> builder.addHeader(name, value));
        }
        ResponseBody body = UNSAFE_OKHTTP_CLIENT.newCall(builder.build()).execute().body();
        String fileSuffix = body.contentType().subtype();
        File target = FileUtil.createTempFile(String.join(".", "target", fileSuffix));
        try (FileOutputStream fileOutputStream = new FileOutputStream(target)) {
            StreamUtil.transferBytes(body.byteStream(), fileOutputStream);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return target;
    }
 
}
  • pdf签名工具类
import com.itextpdf.commons.utils.FileUtil;
import com.itextpdf.html2pdf.ConverterProperties;
import com.itextpdf.html2pdf.HtmlConverter;
import com.itextpdf.io.image.ImageDataFactory;
import com.itextpdf.io.image.ImageType;
import com.itextpdf.io.util.StreamUtil;
import com.itextpdf.kernel.events.Event;
import com.itextpdf.kernel.events.IEventHandler;
import com.itextpdf.kernel.events.PdfDocumentEvent;
import com.itextpdf.kernel.geom.PageSize;
import com.itextpdf.kernel.geom.Rectangle;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.kernel.pdf.PdfPage;
import com.itextpdf.kernel.pdf.PdfReader;
import com.itextpdf.kernel.pdf.PdfWriter;
import com.itextpdf.kernel.pdf.canvas.PdfCanvas;
import com.itextpdf.layout.Canvas;
import com.itextpdf.layout.element.Image;
import com.itextpdf.layout.element.Paragraph;
import com.itextpdf.layout.font.FontProvider;
import com.itextpdf.layout.properties.TextAlignment;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import okhttp3.Response;
import okhttp3.ResponseBody;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileItemFactory;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.lang3.StringUtils;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.commons.CommonsMultipartFile;
 
import java.io.*;
import java.net.URL;
import java.util.List;
 
import static com.example.demo.pdf.ImgLayout.*;
import static com.example.demo.pdf.PdfStyle.PAGE_FONT_SIZE;
import static com.example.demo.pdf.SignStyle.SIGN_EACH_PAGE;
 
/**
 * pdf签名工具
 * <h3>注意事项</h3>
 * <ul>
 * <li>签名仅支持底部签名</li>
 * <li>签名对齐仅支持三种类型{@link TextAlignment.LEFT}/{@link TextAlignment.CENTER}/{@link TextAlignment.RIGHT}</li>
 * <li>仅支持{@code PageSize.A4}格式</li>
 * <li>签名图片参照配置类{@linkplain ImgLayout},pdf文件参照配置类{@linkplain PdfStyle},签名参照配置类{@linkplain SignStyle}</li>
 * <li>签名图片访问支持http/https两种形式,http信任所有的证书与主机的客户端,如果需要配置证书需要重新实现{@linkplain UnsafeOkHttp}</li>
 * </ul>
 *
 */
public final class PdfUtil {
 
    private PdfUtil() {
    }
 
    /**
     * 将html写入到文件中
     *
     * @param html
     * @param file
     */
    public static final void html2Pdf(String html, File file) {
        //1.配置html转换字体参数
        ConverterProperties properties = new ConverterProperties();
        FontProvider fontProvider = new FontProvider();
        fontProvider.addDirectory(PdfStyle.FONT_PATH);
        properties.setFontProvider(fontProvider);
        //2.转换html2pdf
        try (FileOutputStream fileOutputStream = new FileOutputStream(file)) {
            HtmlConverter.convertToPdf(html, fileOutputStream, properties);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
 
    /**
     * pdf图片签名
     *
     * @param source
     * @param images
     * @param signPdfName
     * @return {@literal 签名整合后的pdf文件临时文件}
     * @throws IOException
     */
    public static final File sign(File source, List<Image> images, String signPdfName) throws IOException {
        File signPdf = FileUtil.createTempFile(signPdfName);
        try (FileOutputStream signStream = new FileOutputStream(signPdf);
             FileInputStream sourceStream = new FileInputStream(source)) {
            PdfDocument signDocument = new PdfDocument(new PdfWriter(signStream));
            PdfReader sourceReader = new PdfReader(sourceStream);
            PdfDocument sourceDocument = new PdfDocument(sourceReader);
            signDocument.addEventHandler(PdfDocumentEvent.END_PAGE,
                    new PdfPageTailMarker());
            signDocument.addEventHandler(PdfDocumentEvent.INSERT_PAGE,
                    new PdfSignImgMarker(images,
                            sourceDocument.getNumberOfPages()));
            for (int i = 1, numberOfPage = sourceDocument.getNumberOfPages(); i <= numberOfPage; i++) {
                PdfPage sourcePage = sourceDocument.getPage(i);
                //复制每页内容添加到新的文件中
                signDocument.addPage(sourcePage.copyTo(signDocument));
            }
            signDocument.close();
            sourceDocument.close();
            sourceReader.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return signPdf;
    }
 
    /**
     * 根据图片访问路径创建PDF签名图片对象
     * <ul>
     *     <li>itext支持的标准图片文件格式参见{@linkplain ImageType}</li>
     *     <li>本方法单独针对.jpg格式图片按.jpeg格式处理</li>
     * </ul>
     *
     * @param imgUrl
     * @return
     * @throws IOException
     */
    public static Image createImage(String imgUrl) throws IOException {
        if (StringUtils.containsIgnoreCase(imgUrl, "https")) {
            //1.如果是https请求,则获取文件内容后返回
            Response response = UnsafeOkHttp.unsafeGet(imgUrl, null);
            ResponseBody body = response.body();
            //1.1 如果因为协议问题,响应体格式未自动转换为jpeg,则开启如下代码单独处理
//            if (StringUtils.equalsIgnoreCase(body.contentType().subtype(), "jpg")) {
//                ImageData jpgImage = new JpgImageData(body.bytes());
//                JpgImageHandler.processImage(jpgImage);
//                return new Image(jpgImage);
//            }
            return new Image(ImageDataFactory.create(body.bytes()));
        } else {
            //2.如果是http请求直接返回
            return new Image(ImageDataFactory.create(new URL(imgUrl)));
        }
    }
 
    /**
     * 将源数据转换为分片文件
     *
     * @param source
     * @return
     */
    public static final MultipartFile transformFile2MultipartFile(File source) {
        FileItemFactory factory = new DiskFileItemFactory(16, null);
        FileItem item = factory.createItem("textField", "text/plain", true, source.getName());
        try (FileInputStream sourceStream = new FileInputStream(source);
             OutputStream targetStream = item.getOutputStream()) {
            StreamUtil.transferBytes(sourceStream, targetStream);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return new CommonsMultipartFile(item);
    }
 
    /**
     * pdf页脚处理。pdf配置参照{@linkplain PdfStyle}
     */
    private static class PdfPageTailMarker implements IEventHandler {
 
        @Override
        public void handleEvent(Event event) {
            PdfDocumentEvent docEvent = (PdfDocumentEvent) event;
            PdfDocument pdf = docEvent.getDocument();
            PdfPage page = docEvent.getPage();
            Rectangle pageSize = page.getPageSize();
            Canvas canvas = new Canvas(
                    new PdfCanvas(page.getLastContentStream(), page.getResources(), pdf),
                    pageSize);
            Paragraph p = new Paragraph(String.valueOf(pdf.getPageNumber(page)))
                    .setFontSize(PAGE_FONT_SIZE);
            // 默认底部中间位置
            canvas.showTextAligned(p,
                    (pageSize.getLeft() + pageSize.getRight()) / 2,
                    pageSize.getBottom() + BORDER_INTERVAL,
                    TextAlignment.CENTER);
            canvas.close();
        }
    }
 
    /**
     * pdf签名图片设置
     * <strong>
     * 当pdf文档签名过多时,可能存在页码遮挡、超出边界问题。签名配置参照{@linkplain SignStyle}
     * </strong>
     */
    @AllArgsConstructor
    private static class PdfSignImgMarker implements IEventHandler {
        /**
         * 签名图片
         */
        private List<Image> images;
        /**
         * 总页数
         */
        private int pages;
 
        @Override
        public void handleEvent(Event event) {
            PdfDocumentEvent docEvent = (PdfDocumentEvent) event;
            PdfDocument pdf = docEvent.getDocument();
            PdfPage page = docEvent.getPage();
            Rectangle pageSize = page.getPageSize();
            PdfCanvas pdfCanvas = new PdfCanvas(page.getLastContentStream(), page.getResources(), pdf);
            Canvas canvas = new Canvas(pdfCanvas, pageSize);
            if (SIGN_EACH_PAGE) {
                this.addSignImg(canvas, pdf.getNumberOfPages(), pageSize.getBottom() + BORDER_INTERVAL);
            } else {
                if (pages == pdf.getNumberOfPages()) {
                    this.addSignImg(canvas, pdf.getNumberOfPages(), pageSize.getBottom() + BORDER_INTERVAL);
                }
            }
            canvas.close();
        }
 
        /**
         * 添加居中对齐的签名图片
         * <strong>
         * 以页码为中心,先左后右交替向两边扩散。图片配置参数参照{@linkplain ImgLayout}。仅支持{@code PageSize.A4}对齐。
         * </strong>
         *
         * @param canvas
         * @param numberOfPages
         * @param bottom
         */
        private void addCenterSignImg(Canvas canvas, int numberOfPages, float bottom) {
            float center = PageSize.A4.getRight() / 2;
            for (int i = 0,
                 leftStart = (int) (center - BORDER_INTERVAL),
                 rightStart = (int) (center + BORDER_INTERVAL),
                 imageSize = images.size(); i < imageSize; i++) {
                Image image = images.get(i);
                if (i % 2 == 0) {
                    //1.偶数向左扩散
                    image.setFixedPosition(numberOfPages, leftStart -= IMAGE_WIDTH, bottom);
                } else {
                    //2.奇数向右扩散
                    image.setFixedPosition(numberOfPages, rightStart, bottom);
                    rightStart += IMAGE_WIDTH;
                }
                image.scaleAbsolute(IMAGE_WIDTH, IMAGE_HEIGHT);// 自定义大小
                canvas.add(images.get(i));
            }
        }
 
        /**
         * 添加左对齐的签名图片
         * <strong>
         * 除去默认边距后,从左向右排列签名。图片配置参数参照{@linkplain ImgLayout}。仅支持{@code PageSize.A4}对齐。
         * </strong>
         *
         * @param canvas
         * @param numberOfPages
         * @param bottom
         */
        private void addLeftSignImg(Canvas canvas, int numberOfPages, float bottom) {
            for (int i = 0, start = BORDER_INTERVAL, imageSize = images.size(); i < imageSize; i++) {
                Image image = images.get(i);
                image.setFixedPosition(numberOfPages, start, bottom);
                image.scaleAbsolute(IMAGE_WIDTH, IMAGE_HEIGHT);// 自定义大小
                canvas.add(images.get(i));
                start += IMAGE_WIDTH;
            }
        }
 
        /**
         * 添加右对齐的签名图片
         * <strong>
         * 除去默认边距后,从右向左排列签名。图片配置参数参照{@linkplain ImgLayout}。仅支持{@code PageSize.A4}对齐。
         * </strong>
         *
         * @param canvas
         * @param numberOfPages
         * @param bottom
         */
        private void addRightSignImg(Canvas canvas, int numberOfPages, float bottom) {
            float width = PageSize.A4.getRight();
            for (int i = 0, start = (int) (width - BORDER_INTERVAL), imageSize = images.size(); i < imageSize; i++) {
                Image image = images.get(i);
                image.setFixedPosition(numberOfPages, start -= IMAGE_WIDTH, bottom);
                image.scaleAbsolute(IMAGE_WIDTH, IMAGE_HEIGHT);// 自定义大小
                canvas.add(images.get(i));
            }
        }
 
        /**
         * 添加签名图片
         *
         * @param canvas
         * @param numberOfPages
         * @param bottom
         * @throws IllegalArgumentException
         */
        private void addSignImg(Canvas canvas, int numberOfPages, float bottom) {
            switch (SignStyle.SIGN_ALIGN) {
                case LEFT:
                    addLeftSignImg(canvas, numberOfPages, bottom);
                    break;
                case RIGHT:
                    addRightSignImg(canvas, numberOfPages, bottom);
                    break;
                case CENTER:
                    addCenterSignImg(canvas, numberOfPages, bottom);
                    break;
                default:
                    throw new IllegalArgumentException("仅支持 居中、向左、向右对齐参数");
            }
        }
    }
 
}
  • pdf签名测试类
import com.example.demo.Demo1Application;
import com.itextpdf.commons.utils.FileUtil;
import com.itextpdf.layout.element.Image;
import org.assertj.core.util.Lists;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
 
import java.io.File;
import java.util.List;
 
import static com.example.demo.pdf.PdfUtil.*;
 
/**
 * pdf sign test
 */
@RunWith(SpringRunner.class)
@SpringBootTest(classes = Demo1Application.class)
public class PdfSignTest {
 
    /**
     * 测试源
     */
    public static final String SOURCE_HTML = "<p style=\"text-align: center;\"><span style=\"color: black; font-size: 22px; font-family: 宋体;\"><strong>XXX法院</strong></span></p><p style=\"text-align: center;\"><span style=\"color: black; font-size: 22px; font-family: 宋体;\"><strong>法庭审理笔录</strong></span></p><p style=\"text-align: right;\"><span style=\"color: black; font-size: 16px; font-family: 仿宋;\">(2022)陕01100100号</span></p><p style=\"text-align: left;\"><span style=\"color: black; font-size: 16px; font-family: 仿宋;\">案由:清算组成员责任纠纷</span></p><p style=\"text-align: left;\"><span style=\"color: black; font-size: 16px; font-family: 仿宋;\">开庭时间:2023-02-02 23:00:00</span></p><p style=\"text-align: left;\"><span style=\"color: black; font-size: 16px; font-family: 仿宋;\">开庭地点:远程调解</span></p><p style=\"text-align: left;\"><span style=\"color: black; font-size: 16px; font-family: 仿宋;\">审判员:法官2</span></p><p style=\"text-align: left;\"><span style=\"color: black; font-size: 16px; font-family: 仿宋;\">书记员:陈书记</span></p><p style=\"text-align: left;\"><span style=\"color: black; font-size: 16px; font-family: 仿宋;\">记录如下:</span></p>";
    /**
     * 签名图片源
     */
    public static final String SIGN_IMG_URL_HTTP = "http://10.10.20.172:9000/hlwft/2_1659677726496.jpg";
    public static final String SIGN_IMG_URL_HTTPS = "https://hlwft.tongtech.com:20011/m_file/互联网+诉讼_1671435392069.jpg";
 
    @Test
    public void testPdfSignHttp() {
        try {
            //1.转换html->pdf
            File source = new File("test_source_http.pdf");
            if (!source.exists()){
                source = FileUtil.createTempFile("test_source_http.pdf");
            }
            html2Pdf(SOURCE_HTML, source);
            //2.pdf签名
            List<Image> images = Lists.newArrayList(createImage(SIGN_IMG_URL_HTTP));
 
            File signPdf = sign(source,
                    images,
                    "test_target_http.pdf");
            if (source.exists()) {
                System.out.println(source.getAbsolutePath());
            }
            if (signPdf.exists()) {
                System.out.println(signPdf.getAbsolutePath());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
 
    @Test
    public void testPdfSignHttps() {
        try {
            //1.转换html->pdf
            File source = new File("test_source_https.pdf");
            if (!source.exists()){
                source = FileUtil.createTempFile("test_source_https.pdf");
            }
            html2Pdf(SOURCE_HTML, source);
            //2.pdf签名
            List<Image> images = Lists.newArrayList(createImage(SIGN_IMG_URL_HTTPS));
 
            File signPdf = sign(source,
                    images,
                    "test_target_https.pdf");
            if (source.exists()) {
                System.out.println(source.getAbsolutePath());
            }
            if (signPdf.exists()) {
                System.out.println(signPdf.getAbsolutePath());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
 
}

四、测试结果日志和签名截图

  • 测试日志
D:\jdk8u281\bin\java.exe -ea -Didea.test.cyclic.buffer.size=1048576 "-javaagent:D:\IntelliJ IDEA 2020.3.2\lib\idea_rt.jar=61236:D:\IntelliJ IDEA 2020.3.2\bin" -Dfile.encoding=UTF-8 -classpath "D:\IntelliJ IDEA 2020.3.2\lib\idea_rt.jar;D:\IntelliJ IDEA 2020.3.2\plugins\junit\lib\junit5-rt.jar;D:\IntelliJ IDEA 2020.3.2\plugins\junit\lib\junit-rt.jar;D:\jdk8u281\jre\lib\charsets.jar;D:\jdk8u281\jre\lib\deploy.jar;D:\jdk8u281\jre\lib\ext\access-bridge-64.jar;D:\jdk8u281\jre\lib\ext\cldrdata.jar;D:\jdk8u281\jre\lib\ext\dnsns.jar;D:\jdk8u281\jre\lib\ext\jaccess.jar;D:\jdk8u281\jre\lib\ext\jfxrt.jar;D:\jdk8u281\jre\lib\ext\localedata.jar;D:\jdk8u281\jre\lib\ext\nashorn.jar;D:\jdk8u281\jre\lib\ext\sunec.jar;D:\jdk8u281\jre\lib\ext\sunjce_provider.jar;D:\jdk8u281\jre\lib\ext\sunmscapi.jar;D:\jdk8u281\jre\lib\ext\sunpkcs11.jar;D:\jdk8u281\jre\lib\ext\zipfs.jar;D:\jdk8u281\jre\lib\javaws.jar;D:\jdk8u281\jre\lib\jce.jar;D:\jdk8u281\jre\lib\jfr.jar;D:\jdk8u281\jre\lib\jfxswt.jar;D:\jdk8u281\jre\lib\jsse.jar;D:\jdk8u281\jre\lib\management-agent.jar;D:\jdk8u281\jre\lib\plugin.jar;D:\jdk8u281\jre\lib\resources.jar;D:\jdk8u281\jre\lib\rt.jar;E:\jeecg-code\demo1\target\test-classes;E:\jeecg-code\demo1\target\classes;D:\mvn_repository\org\springframework\boot\spring-boot-starter-web\2.3.12.RELEASE\spring-boot-starter-web-2.3.12.RELEASE.jar;D:\mvn_repository\org\springframework\boot\spring-boot-starter\2.3.12.RELEASE\spring-boot-starter-2.3.12.RELEASE.jar;D:\mvn_repository\org\springframework\boot\spring-boot\2.3.12.RELEASE\spring-boot-2.3.12.RELEASE.jar;D:\mvn_repository\org\springframework\boot\spring-boot-autoconfigure\2.3.12.RELEASE\spring-boot-autoconfigure-2.3.12.RELEASE.jar;D:\mvn_repository\org\springframework\boot\spring-boot-starter-logging\2.3.12.RELEASE\spring-boot-starter-logging-2.3.12.RELEASE.jar;D:\mvn_repository\ch\qos\logback\logback-classic\1.2.3\logback-classic-1.2.3.jar;D:\mvn_repository\ch\qos\logback\logback-core\1.2.3\logback-core-1.2.3.jar;D:\mvn_repository\org\apache\logging\log4j\log4j-to-slf4j\2.13.3\log4j-to-slf4j-2.13.3.jar;D:\mvn_repository\org\apache\logging\log4j\log4j-api\2.13.3\log4j-api-2.13.3.jar;D:\mvn_repository\org\slf4j\jul-to-slf4j\1.7.30\jul-to-slf4j-1.7.30.jar;D:\mvn_repository\jakarta\annotation\jakarta.annotation-api\1.3.5\jakarta.annotation-api-1.3.5.jar;D:\mvn_repository\org\yaml\snakeyaml\1.26\snakeyaml-1.26.jar;D:\mvn_repository\org\springframework\boot\spring-boot-starter-json\2.3.12.RELEASE\spring-boot-starter-json-2.3.12.RELEASE.jar;D:\mvn_repository\com\fasterxml\jackson\core\jackson-databind\2.11.4\jackson-databind-2.11.4.jar;D:\mvn_repository\com\fasterxml\jackson\core\jackson-annotations\2.11.4\jackson-annotations-2.11.4.jar;D:\mvn_repository\com\fasterxml\jackson\core\jackson-core\2.11.4\jackson-core-2.11.4.jar;D:\mvn_repository\com\fasterxml\jackson\datatype\jackson-datatype-jdk8\2.11.4\jackson-datatype-jdk8-2.11.4.jar;D:\mvn_repository\com\fasterxml\jackson\datatype\jackson-datatype-jsr310\2.11.4\jackson-datatype-jsr310-2.11.4.jar;D:\mvn_repository\com\fasterxml\jackson\module\jackson-module-parameter-names\2.11.4\jackson-module-parameter-names-2.11.4.jar;D:\mvn_repository\org\springframework\boot\spring-boot-starter-tomcat\2.3.12.RELEASE\spring-boot-starter-tomcat-2.3.12.RELEASE.jar;D:\mvn_repository\org\apache\tomcat\embed\tomcat-embed-core\9.0.46\tomcat-embed-core-9.0.46.jar;D:\mvn_repository\org\glassfish\jakarta.el\3.0.3\jakarta.el-3.0.3.jar;D:\mvn_repository\org\apache\tomcat\embed\tomcat-embed-websocket\9.0.46\tomcat-embed-websocket-9.0.46.jar;D:\mvn_repository\org\springframework\spring-web\5.2.15.RELEASE\spring-web-5.2.15.RELEASE.jar;D:\mvn_repository\org\springframework\spring-beans\5.2.15.RELEASE\spring-beans-5.2.15.RELEASE.jar;D:\mvn_repository\org\springframework\spring-webmvc\5.2.15.RELEASE\spring-webmvc-5.2.15.RELEASE.jar;D:\mvn_repository\org\springframework\spring-aop\5.2.15.RELEASE\spring-aop-5.2.15.RELEASE.jar;D:\mvn_repository\org\springframework\spring-context\5.2.15.RELEASE\spring-context-5.2.15.RELEASE.jar;D:\mvn_repository\org\springframework\spring-expression\5.2.15.RELEASE\spring-expression-5.2.15.RELEASE.jar;D:\mvn_repository\org\springframework\boot\spring-boot-starter-test\2.3.12.RELEASE\spring-boot-starter-test-2.3.12.RELEASE.jar;D:\mvn_repository\org\springframework\boot\spring-boot-test\2.3.12.RELEASE\spring-boot-test-2.3.12.RELEASE.jar;D:\mvn_repository\org\springframework\boot\spring-boot-test-autoconfigure\2.3.12.RELEASE\spring-boot-test-autoconfigure-2.3.12.RELEASE.jar;D:\mvn_repository\com\jayway\jsonpath\json-path\2.4.0\json-path-2.4.0.jar;D:\mvn_repository\net\minidev\json-smart\2.3.1\json-smart-2.3.1.jar;D:\mvn_repository\net\minidev\accessors-smart\2.3.1\accessors-smart-2.3.1.jar;D:\mvn_repository\org\ow2\asm\asm\5.0.4\asm-5.0.4.jar;D:\mvn_repository\jakarta\xml\bind\jakarta.xml.bind-api\2.3.3\jakarta.xml.bind-api-2.3.3.jar;D:\mvn_repository\jakarta\activation\jakarta.activation-api\1.2.2\jakarta.activation-api-1.2.2.jar;D:\mvn_repository\org\assertj\assertj-core\3.16.1\assertj-core-3.16.1.jar;D:\mvn_repository\org\hamcrest\hamcrest\2.2\hamcrest-2.2.jar;D:\mvn_repository\org\junit\jupiter\junit-jupiter\5.6.3\junit-jupiter-5.6.3.jar;D:\mvn_repository\org\junit\jupiter\junit-jupiter-api\5.6.3\junit-jupiter-api-5.6.3.jar;D:\mvn_repository\org\opentest4j\opentest4j\1.2.0\opentest4j-1.2.0.jar;D:\mvn_repository\org\junit\platform\junit-platform-commons\1.6.3\junit-platform-commons-1.6.3.jar;D:\mvn_repository\org\junit\jupiter\junit-jupiter-params\5.6.3\junit-jupiter-params-5.6.3.jar;D:\mvn_repository\org\junit\jupiter\junit-jupiter-engine\5.6.3\junit-jupiter-engine-5.6.3.jar;D:\mvn_repository\org\junit\vintage\junit-vintage-engine\5.6.3\junit-vintage-engine-5.6.3.jar;D:\mvn_repository\org\apiguardian\apiguardian-api\1.1.0\apiguardian-api-1.1.0.jar;D:\mvn_repository\org\junit\platform\junit-platform-engine\1.6.3\junit-platform-engine-1.6.3.jar;D:\mvn_repository\junit\junit\4.13.2\junit-4.13.2.jar;D:\mvn_repository\org\mockito\mockito-core\3.3.3\mockito-core-3.3.3.jar;D:\mvn_repository\net\bytebuddy\byte-buddy\1.10.22\byte-buddy-1.10.22.jar;D:\mvn_repository\net\bytebuddy\byte-buddy-agent\1.10.22\byte-buddy-agent-1.10.22.jar;D:\mvn_repository\org\objenesis\objenesis\2.6\objenesis-2.6.jar;D:\mvn_repository\org\mockito\mockito-junit-jupiter\3.3.3\mockito-junit-jupiter-3.3.3.jar;D:\mvn_repository\org\skyscreamer\jsonassert\1.5.0\jsonassert-1.5.0.jar;D:\mvn_repository\com\vaadin\external\google\android-json\0.0.20131108.vaadin1\android-json-0.0.20131108.vaadin1.jar;D:\mvn_repository\org\springframework\spring-core\5.2.15.RELEASE\spring-core-5.2.15.RELEASE.jar;D:\mvn_repository\org\springframework\spring-jcl\5.2.15.RELEASE\spring-jcl-5.2.15.RELEASE.jar;D:\mvn_repository\org\springframework\spring-test\5.2.15.RELEASE\spring-test-5.2.15.RELEASE.jar;D:\mvn_repository\org\xmlunit\xmlunit-core\2.7.0\xmlunit-core-2.7.0.jar;D:\mvn_repository\org\projectlombok\lombok\1.18.20\lombok-1.18.20.jar;D:\mvn_repository\com\tencentcloudapi\tencentcloud-sdk-java\3.1.722\tencentcloud-sdk-java-3.1.722.jar;D:\mvn_repository\commons-logging\commons-logging\1.2\commons-logging-1.2.jar;D:\mvn_repository\com\squareup\okio\okio\3.2.0\okio-3.2.0.jar;D:\mvn_repository\com\squareup\okio\okio-jvm\3.2.0\okio-jvm-3.2.0.jar;D:\mvn_repository\org\jetbrains\kotlin\kotlin-stdlib-jdk8\1.3.72\kotlin-stdlib-jdk8-1.3.72.jar;D:\mvn_repository\org\jetbrains\kotlin\kotlin-stdlib-jdk7\1.3.72\kotlin-stdlib-jdk7-1.3.72.jar;D:\mvn_repository\com\google\code\gson\gson\2.8.7\gson-2.8.7.jar;D:\mvn_repository\javax\xml\bind\jaxb-api\2.3.1\jaxb-api-2.3.1.jar;D:\mvn_repository\javax\activation\javax.activation-api\1.2.0\javax.activation-api-1.2.0.jar;D:\mvn_repository\com\squareup\okhttp3\logging-interceptor\3.14.9\logging-interceptor-3.14.9.jar;D:\mvn_repository\org\ini4j\ini4j\0.5.4\ini4j-0.5.4.jar;D:\mvn_repository\com\itextpdf\html2pdf\4.0.4\html2pdf-4.0.4.jar;D:\mvn_repository\com\itextpdf\forms\7.2.4\forms-7.2.4.jar;D:\mvn_repository\com\itextpdf\kernel\7.2.4\kernel-7.2.4.jar;D:\mvn_repository\com\itextpdf\io\7.2.4\io-7.2.4.jar;D:\mvn_repository\com\itextpdf\commons\7.2.4\commons-7.2.4.jar;D:\mvn_repository\org\bouncycastle\bcpkix-jdk15on\1.70\bcpkix-jdk15on-1.70.jar;D:\mvn_repository\org\bouncycastle\bcutil-jdk15on\1.70\bcutil-jdk15on-1.70.jar;D:\mvn_repository\org\bouncycastle\bcprov-jdk15on\1.70\bcprov-jdk15on-1.70.jar;D:\mvn_repository\com\itextpdf\layout\7.2.4\layout-7.2.4.jar;D:\mvn_repository\com\itextpdf\svg\7.2.4\svg-7.2.4.jar;D:\mvn_repository\com\itextpdf\styled-xml-parser\7.2.4\styled-xml-parser-7.2.4.jar;D:\mvn_repository\org\slf4j\slf4j-api\1.7.30\slf4j-api-1.7.30.jar;D:\mvn_repository\com\itextpdf\font-asian\7.2.4\font-asian-7.2.4.jar;D:\mvn_repository\commons-fileupload\commons-fileupload\1.4\commons-fileupload-1.4.jar;D:\mvn_repository\commons-io\commons-io\2.2\commons-io-2.2.jar;D:\mvn_repository\org\apache\commons\commons-lang3\3.12.0\commons-lang3-3.12.0.jar;D:\mvn_repository\com\squareup\okhttp3\okhttp\4.9.3\okhttp-4.9.3.jar;D:\mvn_repository\org\jetbrains\kotlin\kotlin-stdlib\1.3.72\kotlin-stdlib-1.3.72.jar;D:\mvn_repository\org\jetbrains\kotlin\kotlin-stdlib-common\1.3.72\kotlin-stdlib-common-1.3.72.jar;D:\mvn_repository\org\jetbrains\annotations\13.0\annotations-13.0.jar;D:\mvn_repository\org\apache\httpcomponents\httpcore\4.4.16\httpcore-4.4.16.jar" com.intellij.rt.junit.JUnitStarter -ideVersion5 -junit4 com.example.demo.pdf.PdfSignTest
17:30:21.726 [main] DEBUG org.springframework.test.context.junit4.SpringJUnit4ClassRunner - SpringJUnit4ClassRunner constructor called with [class com.example.demo.pdf.PdfSignTest]
17:30:21.735 [main] DEBUG org.springframework.test.context.BootstrapUtils - Instantiating CacheAwareContextLoaderDelegate from class [org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate]
17:30:21.745 [main] DEBUG org.springframework.test.context.BootstrapUtils - Instantiating BootstrapContext using constructor [public org.springframework.test.context.support.DefaultBootstrapContext(java.lang.Class,org.springframework.test.context.CacheAwareContextLoaderDelegate)]
17:30:21.795 [main] DEBUG org.springframework.test.context.BootstrapUtils - Instantiating TestContextBootstrapper for test class [com.example.demo.pdf.PdfSignTest] from class [org.springframework.boot.test.context.SpringBootTestContextBootstrapper]
17:30:21.817 [main] INFO org.springframework.boot.test.context.SpringBootTestContextBootstrapper - Neither @ContextConfiguration nor @ContextHierarchy found for test class [com.example.demo.pdf.PdfSignTest], using SpringBootContextLoader
17:30:21.823 [main] DEBUG org.springframework.test.context.support.AbstractContextLoader - Did not detect default resource location for test class [com.example.demo.pdf.PdfSignTest]: class path resource [com/example/demo/pdf/PdfSignTest-context.xml] does not exist
17:30:21.824 [main] DEBUG org.springframework.test.context.support.AbstractContextLoader - Did not detect default resource location for test class [com.example.demo.pdf.PdfSignTest]: class path resource [com/example/demo/pdf/PdfSignTestContext.groovy] does not exist
17:30:21.824 [main] INFO org.springframework.test.context.support.AbstractContextLoader - Could not detect default resource locations for test class [com.example.demo.pdf.PdfSignTest]: no resource found for suffixes {-context.xml, Context.groovy}.
17:30:21.885 [main] DEBUG org.springframework.test.context.support.ActiveProfilesUtils - Could not find an 'annotation declaring class' for annotation type [org.springframework.test.context.ActiveProfiles] and class [com.example.demo.pdf.PdfSignTest]
17:30:22.049 [main] DEBUG org.springframework.boot.test.context.SpringBootTestContextBootstrapper - @TestExecutionListeners is not present for class [com.example.demo.pdf.PdfSignTest]: using defaults.
17:30:22.049 [main] INFO org.springframework.boot.test.context.SpringBootTestContextBootstrapper - Loaded default TestExecutionListener class names from location [META-INF/spring.factories]: [org.springframework.boot.test.mock.mockito.MockitoTestExecutionListener, org.springframework.boot.test.mock.mockito.ResetMocksTestExecutionListener, org.springframework.boot.test.autoconfigure.restdocs.RestDocsTestExecutionListener, org.springframework.boot.test.autoconfigure.web.client.MockRestServiceServerResetTestExecutionListener, org.springframework.boot.test.autoconfigure.web.servlet.MockMvcPrintOnlyOnFailureTestExecutionListener, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverTestExecutionListener, org.springframework.boot.test.autoconfigure.webservices.client.MockWebServiceServerTestExecutionListener, org.springframework.test.context.web.ServletTestExecutionListener, org.springframework.test.context.support.DirtiesContextBeforeModesTestExecutionListener, org.springframework.test.context.support.DependencyInjectionTestExecutionListener, org.springframework.test.context.support.DirtiesContextTestExecutionListener, org.springframework.test.context.transaction.TransactionalTestExecutionListener, org.springframework.test.context.jdbc.SqlScriptsTestExecutionListener, org.springframework.test.context.event.EventPublishingTestExecutionListener]
17:30:22.062 [main] DEBUG org.springframework.boot.test.context.SpringBootTestContextBootstrapper - Skipping candidate TestExecutionListener [org.springframework.test.context.transaction.TransactionalTestExecutionListener] due to a missing dependency. Specify custom listener classes or make the default listener classes and their required dependencies available. Offending class: [org/springframework/transaction/interceptor/TransactionAttributeSource]
17:30:22.062 [main] DEBUG org.springframework.boot.test.context.SpringBootTestContextBootstrapper - Skipping candidate TestExecutionListener [org.springframework.test.context.jdbc.SqlScriptsTestExecutionListener] due to a missing dependency. Specify custom listener classes or make the default listener classes and their required dependencies available. Offending class: [org/springframework/transaction/interceptor/TransactionAttribute]
17:30:22.062 [main] INFO org.springframework.boot.test.context.SpringBootTestContextBootstrapper - Using TestExecutionListeners: [org.springframework.test.context.web.ServletTestExecutionListener@479d31f3, org.springframework.test.context.support.DirtiesContextBeforeModesTestExecutionListener@40ef3420, org.springframework.boot.test.mock.mockito.MockitoTestExecutionListener@498d318c, org.springframework.boot.test.autoconfigure.SpringBootDependencyInjectionTestExecutionListener@6e171cd7, org.springframework.test.context.support.DirtiesContextTestExecutionListener@402bba4f, org.springframework.test.context.event.EventPublishingTestExecutionListener@795cd85e, org.springframework.boot.test.mock.mockito.ResetMocksTestExecutionListener@59fd97a8, org.springframework.boot.test.autoconfigure.restdocs.RestDocsTestExecutionListener@f5ac9e4, org.springframework.boot.test.autoconfigure.web.client.MockRestServiceServerResetTestExecutionListener@123ef382, org.springframework.boot.test.autoconfigure.web.servlet.MockMvcPrintOnlyOnFailureTestExecutionListener@dbf57b3, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverTestExecutionListener@384ad17b, org.springframework.boot.test.autoconfigure.webservices.client.MockWebServiceServerTestExecutionListener@61862a7f]
17:30:22.064 [main] DEBUG org.springframework.test.annotation.ProfileValueUtils - Retrieved @ProfileValueSourceConfiguration [null] for test class [com.example.demo.pdf.PdfSignTest]
17:30:22.065 [main] DEBUG org.springframework.test.annotation.ProfileValueUtils - Retrieved ProfileValueSource type [class org.springframework.test.annotation.SystemProfileValueSource] for class [com.example.demo.pdf.PdfSignTest]
17:30:22.067 [main] DEBUG org.springframework.test.annotation.ProfileValueUtils - Retrieved @ProfileValueSourceConfiguration [null] for test class [com.example.demo.pdf.PdfSignTest]
17:30:22.067 [main] DEBUG org.springframework.test.annotation.ProfileValueUtils - Retrieved ProfileValueSource type [class org.springframework.test.annotation.SystemProfileValueSource] for class [com.example.demo.pdf.PdfSignTest]
17:30:22.067 [main] DEBUG org.springframework.test.annotation.ProfileValueUtils - Retrieved @ProfileValueSourceConfiguration [null] for test class [com.example.demo.pdf.PdfSignTest]
17:30:22.067 [main] DEBUG org.springframework.test.annotation.ProfileValueUtils - Retrieved ProfileValueSource type [class org.springframework.test.annotation.SystemProfileValueSource] for class [com.example.demo.pdf.PdfSignTest]
17:30:22.079 [main] DEBUG org.springframework.test.annotation.ProfileValueUtils - Retrieved @ProfileValueSourceConfiguration [null] for test class [com.example.demo.pdf.PdfSignTest]
17:30:22.079 [main] DEBUG org.springframework.test.annotation.ProfileValueUtils - Retrieved ProfileValueSource type [class org.springframework.test.annotation.SystemProfileValueSource] for class [com.example.demo.pdf.PdfSignTest]
17:30:22.080 [main] DEBUG org.springframework.test.annotation.ProfileValueUtils - Retrieved @ProfileValueSourceConfiguration [null] for test class [com.example.demo.pdf.PdfSignTest]
17:30:22.080 [main] DEBUG org.springframework.test.annotation.ProfileValueUtils - Retrieved ProfileValueSource type [class org.springframework.test.annotation.SystemProfileValueSource] for class [com.example.demo.pdf.PdfSignTest]
17:30:22.081 [main] DEBUG org.springframework.test.annotation.ProfileValueUtils - Retrieved @ProfileValueSourceConfiguration [null] for test class [com.example.demo.pdf.PdfSignTest]
17:30:22.081 [main] DEBUG org.springframework.test.annotation.ProfileValueUtils - Retrieved ProfileValueSource type [class org.springframework.test.annotation.SystemProfileValueSource] for class [com.example.demo.pdf.PdfSignTest]
17:30:22.087 [main] DEBUG org.springframework.test.context.support.AbstractDirtiesContextTestExecutionListener - Before test class: context [DefaultTestContext@5158b42f testClass = PdfSignTest, testInstance = [null], testMethod = [null], testException = [null], mergedContextConfiguration = [WebMergedContextConfiguration@595b007d testClass = PdfSignTest, locations = '{}', classes = '{class com.example.demo.Demo1Application}', contextInitializerClasses = '[]', activeProfiles = '{}', propertySourceLocations = '{}', propertySourceProperties = '{org.springframework.boot.test.context.SpringBootTestContextBootstrapper=true}', contextCustomizers = set[org.springframework.boot.test.context.filter.ExcludeFilterContextCustomizer@6043cd28, org.springframework.boot.test.json.DuplicateJsonObjectContextCustomizerFactory$DuplicateJsonObjectContextCustomizer@5bfbf16f, org.springframework.boot.test.mock.mockito.MockitoContextCustomizer@0, org.springframework.boot.test.web.client.TestRestTemplateContextCustomizer@1b26f7b2, org.springframework.boot.test.autoconfigure.properties.PropertyMappingContextCustomizer@0, org.springframework.boot.test.autoconfigure.web.servlet.WebDriverContextCustomizerFactory$Customizer@13eb8acf, org.springframework.boot.test.context.SpringBootTestArgs@1, org.springframework.boot.test.context.SpringBootTestWebEnvironment@3d99d22e], resourceBasePath = 'src/main/webapp', contextLoader = 'org.springframework.boot.test.context.SpringBootContextLoader', parent = [null]], attributes = map['org.springframework.test.context.web.ServletTestExecutionListener.activateListener' -> true]], class annotated with @DirtiesContext [false] with mode [null].
17:30:22.089 [main] DEBUG org.springframework.test.annotation.ProfileValueUtils - Retrieved @ProfileValueSourceConfiguration [null] for test class [com.example.demo.pdf.PdfSignTest]
17:30:22.090 [main] DEBUG org.springframework.test.annotation.ProfileValueUtils - Retrieved ProfileValueSource type [class org.springframework.test.annotation.SystemProfileValueSource] for class [com.example.demo.pdf.PdfSignTest]
17:30:22.122 [main] DEBUG org.springframework.test.context.support.TestPropertySourceUtils - Adding inlined properties to environment: {spring.jmx.enabled=false, org.springframework.boot.test.context.SpringBootTestContextBootstrapper=true}

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::       (v2.3.12.RELEASE)

2023-03-29 17:30:22.526  INFO 2356 --- [           main] com.example.demo.pdf.PdfSignTest         : Starting PdfSignTest on DESKTOP-2F8ODD2 with PID 2356 (started by dell in E:\jeecg-code\demo1)
2023-03-29 17:30:22.527  INFO 2356 --- [           main] com.example.demo.pdf.PdfSignTest         : No active profile set, falling back to default profiles: default
2023-03-29 17:30:22.791  WARN 2356 --- [kground-preinit] o.s.h.c.j.Jackson2ObjectMapperBuilder    : For Jackson Kotlin classes support please add "com.fasterxml.jackson.module:jackson-module-kotlin" to the classpath
2023-03-29 17:30:30.568  INFO 2356 --- [           main] o.s.s.concurrent.ThreadPoolTaskExecutor  : Initializing ExecutorService 'applicationTaskExecutor'
2023-03-29 17:30:30.697  INFO 2356 --- [           main] o.s.b.a.w.s.WelcomePageHandlerMapping    : Adding welcome page: class path resource [static/index.html]
2023-03-29 17:30:30.898  INFO 2356 --- [           main] com.example.demo.pdf.PdfSignTest         : Started PdfSignTest in 8.763 seconds (JVM running for 9.721)
E:\jeecg-code\demo1\test_source_http.pdf
E:\jeecg-code\demo1\test_target_http.pdf
E:\jeecg-code\demo1\test_source_https.pdf
E:\jeecg-code\demo1\test_target_https.pdf
2023-03-29 17:30:32.700  INFO 2356 --- [extShutdownHook] o.s.s.concurrent.ThreadPoolTaskExecutor  : Shutting down ExecutorService 'applicationTaskExecutor'

Process finished with exit code 0
  • 签名截图
    http网络图
    https网络图

五、其他说明

当https访问网络签名图片格式为jpg时,如果因为协议问题,响应体格式未自动转换为jpeg,在PdfUtil.createImage(String)开启注释代码的情况下,需要引入如下jpg图片解析相关作业类。

  • jpg图片数据类
import com.itextpdf.io.image.ImageData;
import com.itextpdf.io.image.ImageType;
 
import java.net.URL;
 
 
public class JpgImageData extends ImageData {
 
    protected JpgImageData(URL url) {
        super(url, ImageType.JPEG);
    }
 
    protected JpgImageData(byte[] bytes) {
        super(bytes, ImageType.JPEG);
    }
}
  • jpg图片数据处理类
import com.itextpdf.commons.utils.MessageFormatUtil;
import com.itextpdf.io.colors.IccProfile;
import com.itextpdf.io.exceptions.IOException;
import com.itextpdf.io.image.ImageData;
import com.itextpdf.io.image.ImageType;
import com.itextpdf.io.logs.IoLogMessageConstant;
import com.itextpdf.io.util.StreamUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
 
import java.io.InputStream;
 
 
public final class JpgImageHandler {
    private static final Logger LOGGER = LoggerFactory.getLogger(JpgImageHandler.class);
 
    /**
     * This is a type of marker.
     */
    private static final int NOT_A_MARKER = -1;
 
    /**
     * This is a type of marker.
     */
    private static final int VALID_MARKER = 0;
 
    /**
     * Acceptable Jpeg markers.
     */
    private static final int[] VALID_MARKERS = {0xC0, 0xC1, 0xC2};
 
    /**
     * This is a type of marker.
     */
    private static final int UNSUPPORTED_MARKER = 1;
 
    /**
     * Unsupported Jpeg markers.
     */
    private static final int[] UNSUPPORTED_MARKERS = {0xC3, 0xC5, 0xC6, 0xC7, 0xC8, 0xC9, 0xCA, 0xCB, 0xCD, 0xCE, 0xCF};
 
    /**
     * This is a type of marker.
     */
    private static final int NOPARAM_MARKER = 2;
 
    /**
     * Jpeg markers without additional parameters.
     */
    private static final int[] NOPARAM_MARKERS = {0xD0, 0xD1, 0xD2, 0xD3, 0xD4, 0xD5, 0xD6, 0xD7, 0xD8, 0x01};
 
    /**
     * Marker value
     */
    private static final int M_APP0 = 0xE0;
    /**
     * Marker value
     */
    private static final int M_APP2 = 0xE2;
    /**
     * Marker value
     */
    private static final int M_APPE = 0xEE;
    /**
     * Marker value for Photoshop IRB
     */
    private static final int M_APPD = 0xED;
 
    /**
     * sequence that is used in all Jpeg files
     */
    private static final byte[] JFIF_ID = {0x4A, 0x46, 0x49, 0x46, 0x00};
 
    /**
     * sequence preceding Photoshop resolution data
     */
    private static final byte[] PS_8BIM_RESO = {0x38, 0x42, 0x49, 0x4d, 0x03, (byte) 0xed};
 
    /**
     * Process the passed Image data as a JPEG image.
     * Image is loaded and all image attributes are initialized and/or updated.
     *
     * @param image the image to process as a JPEG image
     */
    public static void processImage(ImageData image) {
        if (image.getOriginalType() != ImageType.JPEG) {
            throw new IllegalArgumentException("JPEG image expected");
        }
        InputStream jpegStream = null;
        try {
            String errorID = "Byte array";
            jpegStream = new java.io.ByteArrayInputStream(image.getData());
            processParameters(jpegStream, errorID, image);
        } catch (java.io.IOException e) {
            throw new IOException(IOException.JpegImageException, e);
        } finally {
            if (jpegStream != null) {
                try {
                    jpegStream.close();
                } catch (java.io.IOException ignore) {
                }
            }
        }
        updateAttributes(image);
    }
 
    static void attemptToSetIccProfileToImage(byte[][] icc, ImageData image) {
        if (icc != null) {
            int total = 0;
            for (final byte[] value : icc) {
                if (value == null) {
                    return;
                }
                total += value.length - 14;
            }
            byte[] ficc = new byte[total];
            total = 0;
            for (final byte[] bytes : icc) {
                System.arraycopy(bytes, 14, ficc, total, bytes.length - 14);
                total += bytes.length - 14;
            }
            try {
                image.setProfile(IccProfile.getInstance(ficc, image.getColorEncodingComponentsNumber()));
            } catch (Exception e) {
                LOGGER.error(MessageFormatUtil.format(
                        IoLogMessageConstant.DURING_CONSTRUCTION_OF_ICC_PROFILE_ERROR_OCCURRED,
                        e.getClass().getSimpleName(), e.getMessage()));
            }
        }
    }
 
    private static void updateAttributes(ImageData image) {
        image.setFilter("DCTDecode");
        if (image.getColorTransform() == 0) {
            image.getDecodeParms().put("ColorTransform", 0);
        }
        int colorComponents = image.getColorEncodingComponentsNumber();
        if (colorComponents != 1 && colorComponents != 3 && image.isInverted()) {
            image.setDecode(new float[]{1, 0, 1, 0, 1, 0, 1, 0});
        }
    }
 
    /**
     * This method checks if the image is a valid JPEG and processes some parameters.
     *
     * @throws IOException
     * @throws java.io.IOException
     */
    private static void processParameters(InputStream jpegStream, String errorID, ImageData image) throws java.io.IOException {
        byte[][] icc = null;
        if (jpegStream.read() != 0xFF || jpegStream.read() != 0xD8) {
            throw new IOException(IOException._1IsNotAValidJpegFile).setMessageParams(errorID);
        }
        boolean firstPass = true;
        int len;
        while (true) {
            int v = jpegStream.read();
            if (v < 0) {
                throw new IOException(IOException.PrematureEofWhileReadingJpeg);
            }
            if (v == 0xFF) {
                int marker = jpegStream.read();
                if (firstPass && marker == M_APP0) {
                    firstPass = false;
                    len = getShort(jpegStream);
                    if (len < 16) {
                        StreamUtil.skip(jpegStream, len - 2);
                        continue;
                    }
                    byte[] bcomp = new byte[JFIF_ID.length];
                    int r = jpegStream.read(bcomp);
                    if (r != bcomp.length) {
                        throw new IOException(IOException._1CorruptedJfifMarker).setMessageParams(errorID);
                    }
                    boolean found = true;
                    for (int k = 0; k < bcomp.length; ++k) {
                        if (bcomp[k] != JFIF_ID[k]) {
                            found = false;
                            break;
                        }
                    }
                    if (!found) {
                        StreamUtil.skip(jpegStream, len - 2 - bcomp.length);
                        continue;
                    }
                    StreamUtil.skip(jpegStream, 2);
                    int units = jpegStream.read();
                    int dx = getShort(jpegStream);
                    int dy = getShort(jpegStream);
                    if (units == 1) {
                        image.setDpi(dx, dy);
                    } else if (units == 2) {
                        image.setDpi((int) (dx * 2.54f + 0.5f), (int) (dy * 2.54f + 0.5f));
                    }
                    StreamUtil.skip(jpegStream, len - 2 - bcomp.length - 7);
                    continue;
                }
                if (marker == M_APPE) {
                    len = getShort(jpegStream) - 2;
                    byte[] byteappe = new byte[len];
                    for (int k = 0; k < len; ++k) {
                        byteappe[k] = (byte) jpegStream.read();
                    }
                    if (byteappe.length >= 12) {
                        String appe = new String(byteappe, 0, 5, "ISO-8859-1");
                        if (appe.equals("Adobe")) {
                            image.setInverted(true);
                        }
                    }
                    continue;
                }
                if (marker == M_APP2) {
                    len = getShort(jpegStream) - 2;
                    byte[] byteapp2 = new byte[len];
                    for (int k = 0; k < len; ++k) {
                        byteapp2[k] = (byte) jpegStream.read();
                    }
                    if (byteapp2.length >= 14) {
                        String app2 = new String(byteapp2, 0, 11, "ISO-8859-1");
                        if (app2.equals("ICC_PROFILE")) {
                            int order = byteapp2[12] & 0xff;
                            int count = byteapp2[13] & 0xff;
                            // some jpeg producers don't know how to count to 1
                            if (order < 1) {
                                order = 1;
                            }
                            if (count < 1) {
                                count = 1;
                            }
                            if (icc == null) {
                                icc = new byte[count][];
                            }
                            icc[order - 1] = byteapp2;
                        }
                    }
                    continue;
                }
                if (marker == M_APPD) {
                    len = getShort(jpegStream) - 2;
                    byte[] byteappd = new byte[len];
                    for (int k = 0; k < len; k++) {
                        byteappd[k] = (byte) jpegStream.read();
                    }
                    // search for '8BIM Resolution' marker
                    int k;
                    for (k = 0; k < len - PS_8BIM_RESO.length; k++) {
                        boolean found = true;
                        for (int j = 0; j < PS_8BIM_RESO.length; j++) {
                            if (byteappd[k + j] != PS_8BIM_RESO[j]) {
                                found = false;
                                break;
                            }
                        }
                        if (found) {
                            break;
                        }
                    }
 
                    k += PS_8BIM_RESO.length;
                    if (k < len - PS_8BIM_RESO.length) {
                        // "PASCAL String" for name, i.e. string prefix with length byte
                        // padded to be even length; 2 null bytes if empty
                        byte namelength = byteappd[k];
                        // add length byte
                        namelength++;
                        // add padding
                        if (namelength % 2 == 1) {
                            namelength++;
                        }
                        // just skip name
                        k += namelength;
                        // size of the resolution data
                        int resosize = (byteappd[k] << 24) + (byteappd[k + 1] << 16) + (byteappd[k + 2] << 8) + byteappd[k + 3];
                        // should be 16
                        if (resosize != 16) {
                            // fail silently, for now
                            //System.err.println("DEBUG: unsupported resolution IRB size");
                            continue;
                        }
                        k += 4;
                        int dx = (byteappd[k] << 8) + (byteappd[k + 1] & 0xff);
                        k += 2;
                        // skip 2 unknown bytes
                        k += 2;
                        int unitsx = (byteappd[k] << 8) + (byteappd[k + 1] & 0xff);
                        k += 2;
                        // skip 2 unknown bytes
                        k += 2;
                        int dy = (byteappd[k] << 8) + (byteappd[k + 1] & 0xff);
                        k += 2;
                        // skip 2 unknown bytes
                        k += 2;
                        int unitsy = (byteappd[k] << 8) + (byteappd[k + 1] & 0xff);
 
                        if (unitsx == 1 || unitsx == 2) {
                            dx = (unitsx == 2 ? (int) (dx * 2.54f + 0.5f) : dx);
                            // make sure this is consistent with JFIF data
                            if (image.getDpiX() != 0 && image.getDpiX() != dx) {
                                LOGGER.debug(MessageFormatUtil.format("Inconsistent metadata (dpiX: {0} vs {1})", image.getDpiX(), dx));
                            } else {
                                image.setDpi(dx, image.getDpiY());
                            }
                        }
                        if (unitsy == 1 || unitsy == 2) {
                            dy = (unitsy == 2 ? (int) (dy * 2.54f + 0.5f) : dy);
                            // make sure this is consistent with JFIF data
                            if (image.getDpiY() != 0 && image.getDpiY() != dy) {
                                LOGGER.debug(MessageFormatUtil.format("Inconsistent metadata (dpiY: {0} vs {1})", image.getDpiY(), dy));
                            } else {
                                image.setDpi(image.getDpiX(), dx);
                            }
                        }
                    }
                    continue;
                }
                firstPass = false;
                int markertype = marker(marker);
                if (markertype == VALID_MARKER) {
                    StreamUtil.skip(jpegStream, 2);
                    if (jpegStream.read() != 0x08) {
                        throw new IOException(IOException._1MustHave8BitsPerComponent).setMessageParams(errorID);
                    }
                    image.setHeight(getShort(jpegStream));
                    image.setWidth(getShort(jpegStream));
                    image.setColorEncodingComponentsNumber(jpegStream.read());
                    image.setBpc(8);
                    break;
                } else if (markertype == UNSUPPORTED_MARKER) {
                    throw new IOException(IOException._1UnsupportedJpegMarker2).setMessageParams(errorID, Integer.toString(marker));
                } else if (markertype != NOPARAM_MARKER) {
                    StreamUtil.skip(jpegStream, getShort(jpegStream) - 2);
                }
            }
        }
 
        attemptToSetIccProfileToImage(icc, image);
    }
 
    /**
     * Reads a short from the <CODE>InputStream</CODE>.
     *
     * @param jpegStream the <CODE>InputStream</CODE>
     * @return an int
     * @throws java.io.IOException
     */
    private static int getShort(InputStream jpegStream) throws java.io.IOException {
        return (jpegStream.read() << 8) + jpegStream.read();
    }
 
    /**
     * Returns a type of marker.
     *
     * @param marker an int
     * @return a type: <VAR>VALID_MARKER</VAR>, <VAR>UNSUPPORTED_MARKER</VAR> or <VAR>NOPARAM_MARKER</VAR>
     */
    private static int marker(int marker) {
        for (int i = 0; i < VALID_MARKERS.length; i++) {
            if (marker == VALID_MARKERS[i]) {
                return VALID_MARKER;
            }
        }
        for (int i = 0; i < NOPARAM_MARKERS.length; i++) {
            if (marker == NOPARAM_MARKERS[i]) {
                return NOPARAM_MARKER;
            }
        }
        for (int i = 0; i < UNSUPPORTED_MARKERS.length; i++) {
            if (marker == UNSUPPORTED_MARKERS[i]) {
                return UNSUPPORTED_MARKER;
            }
        }
        return NOT_A_MARKER;
    }
 
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

mister-big

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值