项目结构
在这里插入图片描述

1.引包

<dependencies>
        <!--        itextpdf-->
        <dependency>
            <groupId>com.itextpdf</groupId>
            <artifactId>itextpdf</artifactId>
            <version>5.5.10</version>
        </dependency>
        <dependency>
            <groupId>com.itextpdf</groupId>
            <artifactId>itext-asian</artifactId>
            <version>5.2.0</version>
        </dependency>
        <dependency>
            <groupId>com.itextpdf.tool</groupId>
            <artifactId>xmlworker</artifactId>
            <version>5.5.10</version>
        </dependency>

        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.7.22</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.freemarker/freemarker -->
        <dependency>
            <groupId>org.freemarker</groupId>
            <artifactId>freemarker</artifactId>
            <version>2.3.19</version>
        </dependency>


        <!-- https://mvnrepository.com/artifact/org.xhtmlrenderer/flying-saucer-pdf -->
        <dependency>
            <groupId>org.xhtmlrenderer</groupId>
            <artifactId>flying-saucer-pdf</artifactId>
            <version>9.1.5</version>
        </dependency>

        <!-- https://mvnrepository.com/artifact/org.xhtmlrenderer/flying-saucer-pdf-itext5 -->
        <dependency>
            <groupId>org.xhtmlrenderer</groupId>
            <artifactId>flying-saucer-pdf-itext5</artifactId>
            <version>9.1.5</version>
        </dependency>
    </dependencies>

2.使用freemarker生成html文件

 /**
     * 获取模板内容   : PDFHelper.getTemplateContent("071515133867.ftl", null);
     * @param templateName      模板文件名
     * @param param          模板参数
     * @return
     * @throws Exception
     */
    public static String getTemplateContent(String templateName, Object param) {
        Configuration configuration = new Configuration();
        Writer out = null;
        try {
            configuration.setClassForTemplateLoading(PDFHelper.class, "/templates");
            // 设置编码格式
            configuration.setEncoding(Locale.CHINA, ENCODING);
            out = new StringWriter();
            Template template = configuration.getTemplate(templateName);
            template.process(param, out);
            out.flush();
            return out.toString();
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            if (out != null) {
                try {
                    out.close();
                } catch (IOException e) {
                }
            }
        }
    }

3.html转pdf

/**
     * HTML 转 PDF
     * @param content html内容
     * @return PDF字节数组
     */
    public static byte[] html2Pdf(String content, Image... imageList) {
        ByteArrayOutputStream outputStream = null;
        try {
            Document document = new Document();
            outputStream = new ByteArrayOutputStream();
            PdfWriter writer = PdfWriter.getInstance(document, outputStream);
            document.open();
            XMLWorkerHelper.getInstance().parseXHtml(writer,
                    document,
                    new ByteArrayInputStream(content.getBytes()),
                    XMLWorkerHelper.class.getResourceAsStream("/default.css"),
                    Charset.forName(ENCODING),
                    new CustomXMLWorkerFontProvider());
            if (imageList != null && imageList.length > 0) {
                for (Image image : imageList) {
                    document.add(image);
                }
            }
            document.close();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        return outputStream.toByteArray();
    }

到此生成完毕。但是特殊情况需要添加印章之类的,因为博主开始是在html中使用img 标签和background发现样式都不生效。所以改用直接通过itextpdf的API添加


<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
    <meta http-equiv="Content-Style-Type" content="text/css"/>
    <meta name="generator" content="Aspose.Words for .NET 15.1.0.0"/>
    <title></title></head>
<body>
<div>
    <p style="margin:0pt; orphans:0; text-align:justify; widows:0"><span
            style="color:#00b050; font-family:Consolas; font-size:10.5pt; font-style:normal; text-transform:none">ACT_RE_*: 'RE’代表repository。带有这个前缀的表包含“静态”信息,例如流程定义与流程资源(图片、规则等)。</span>
    </p>
    <p style="margin:0pt; orphans:0; text-align:justify; widows:0"><span
            style="font-family:Consolas; font-size:10.5pt; font-style:normal; text-transform:none">ACT_RE_DEPLOYMENT 流程部署表</span>
    </p>
    <p style="margin:0pt; orphans:0; text-align:justify; widows:0"><span
            style="font-family:Consolas; font-size:10.5pt; font-style:normal; text-transform:none">ACT_RE_PROCDEF 流程定义信息表</span>
    </p>
    <p style="margin:0pt; orphans:0; text-align:justify; widows:0"><span
            style="font-family:Consolas; font-size:10.5pt; font-style:normal; text-transform:none">ACT_RE_MODEL 模型信息表(此表已经舍弃,更换为ACT_DE_MODEL)</span>
    </p>
    <p style="margin:0pt; orphans:0; text-align:justify; widows:0"><span
            style="font-family:Consolas; font-size:10.5pt; font-style:normal; text-transform:none">&#xa0;</span></p>
    <p style="margin:0pt; orphans:0; text-align:justify; widows:0"><span
            style="color:#00b050; font-family:Consolas; font-size:10.5pt; font-style:normal; text-transform:none">ACT_RU_*: 'RU’代表runtime。这些表存储运行时信息,例如流程实例(process instance)、用户任务(user task)、变量(variable)、作业(job)等。Flowable只在流程实例运行中保存运行时数据,并在流程实例结束时删除记录。这样保证运行时表小和快。</span>
    </p>
    <p style="margin:0pt; orphans:0; text-align:justify; widows:0"><span
            style="font-family:Consolas; font-size:10.5pt; font-style:normal; text-transform:none">ACT_RU_EXECUTION 流程实例与分支执行表</span>
    </p>
    <p style="margin:0pt; orphans:0; text-align:justify; widows:0"><span
            style="font-family:Consolas; font-size:10.5pt; font-style:normal; text-transform:none">ACT_RU_TASK 用户任务表</span>
    </p>
    <p style="margin:0pt; orphans:0; text-align:justify; widows:0"><span
            style="font-family:Consolas; font-size:10.5pt; font-style:normal; text-transform:none">ACT_RU_VARIABLE 变量信息</span>
    </p>
    <p style="margin:0pt; orphans:0; text-align:justify; widows:0"><span
            style="font-family:Consolas; font-size:10.5pt; font-style:normal; text-transform:none">ACT_RU_IDENTITYLINK 参与者相关信息表</span>
    </p>
    <p style="margin:0pt; orphans:0; text-align:justify; widows:0"><span
            style="font-family:Consolas; font-size:10.5pt; font-style:normal; text-transform:none">ACT_RU_EVENT_SUBSCR 事件订阅表</span>
    </p>
    <p style="margin:0pt; orphans:0; text-align:justify; widows:0"><span
            style="font-family:Consolas; font-size:10.5pt; font-style:normal; text-transform:none">ACT_RU_JOB 作业表</span>
    </p>
    <p style="margin:0pt; orphans:0; text-align:justify; widows:0"><span
            style="font-family:Consolas; font-size:10.5pt; font-style:normal; text-transform:none">ACT_RU_TIMER_JOB 定时器表</span>
    </p>
    <p style="margin:0pt; orphans:0; text-align:justify; widows:0"><span
            style="font-family:Consolas; font-size:10.5pt; font-style:normal; text-transform:none">ACT_RU_SUSPENDED_JOB 暂停作业表</span>
    </p>
    <p style="margin:0pt; orphans:0; text-align:justify; widows:0"><span
            style="font-family:Consolas; font-size:10.5pt; font-style:normal; text-transform:none">ACT_RU_DEADLETTER_JOB 死信表</span>
    </p>
    <p style="margin:0pt; orphans:0; text-align:justify; widows:0"><span
            style="font-family:Consolas; font-size:10.5pt; font-style:normal; text-transform:none">ACT_RU_HISTORY_JOB 历史作业表</span>
    </p>
    <p style="margin:0pt; orphans:0; text-align:justify; widows:0"><span
            style="font-family:Consolas; font-size:10.5pt; font-style:normal; text-transform:none">&#xa0;</span></p>
    <p style="margin:0pt; orphans:0; text-align:justify; widows:0"><span
            style="color:#00b050; font-family:Consolas; font-size:10.5pt; font-style:normal; text-transform:none">ACT_HI_*: 'HI’代表history。这些表存储历史数据,例如已完成的流程实例、变量、任务等。</span>
    </p>
    <p style="margin:0pt; orphans:0; text-align:justify; widows:0"><span
            style="font-family:Consolas; font-size:10.5pt; font-style:normal; text-transform:none">ACT_HI_PROCINST 历史流程实例表</span>
    </p>
    <p style="margin:0pt; orphans:0; text-align:justify; widows:0"><span
            style="font-family:Consolas; font-size:10.5pt; font-style:normal; text-transform:none">ACT_HI_ACTINST 历史节点信息表</span>
    </p>
    <p style="margin:0pt; orphans:0; text-align:justify; widows:0"><span
            style="font-family:Consolas; font-size:10.5pt; font-style:normal; text-transform:none">ACT_HI_TASKINST 历史任务表</span>
    </p>
    <p style="margin:0pt; orphans:0; text-align:justify; widows:0"><span
            style="font-family:Consolas; font-size:10.5pt; font-style:normal; text-transform:none">ACT_HI_VARINST 历史变量</span>
    </p>
    <p style="margin:0pt; orphans:0; text-align:justify; widows:0"><span
            style="font-family:Consolas; font-size:10.5pt; font-style:normal; text-transform:none">ACT_HI_IDENTITYLINK 历史参与者表</span>
    </p>
    <p style="margin:0pt; orphans:0; text-align:justify; widows:0"><span
            style="font-family:Consolas; font-size:10.5pt; font-style:normal; text-transform:none">ACT_HI_DETAIL 历史的流程运行中的细节信息</span>
    </p>
    <p style="margin:0pt; orphans:0; text-align:justify; widows:0"><span
            style="font-family:Consolas; font-size:10.5pt; font-style:normal; text-transform:none">ACT_HI_ATTACHMENT 附件表</span>
    </p>
    <p style="margin:0pt; orphans:0; text-align:justify; widows:0"><span
            style="font-family:Consolas; font-size:10.5pt; font-style:normal; text-transform:none">ACT_HI_COMMENT 评论表</span>
    </p>
    <p style="margin:0pt; orphans:0; text-align:justify; widows:0"><span
            style="font-family:Consolas; font-size:10.5pt; font-style:normal; text-transform:none">&#xa0;</span></p>
    <p style="margin:0pt; orphans:0; text-align:justify; widows:0"><span
            style="color:#00b050; font-family:Consolas; font-size:10.5pt; font-style:normal; text-transform:none">ACT_GE_*: 通用数据。在多处使用。</span>
    </p>
    <p style="margin:0pt; orphans:0; text-align:justify; widows:0"><span
                style="font-family:Consolas; font-size:10.5pt; font-style:normal; text-transform:none">ACT_RU_JOB 作业表</span>
    </p>
    <p style="margin:0pt; orphans:0; text-align:justify; widows:0"><span
            style="font-family:Consolas; font-size:10.5pt; font-style:normal; text-transform:none">ACT_GE_PROPERTY 属性表(保存流程引擎的kv键值属性)</span>
    </p>
    <p style="margin:0pt; orphans:0; text-align:justify; widows:0"><span
            style="font-family:Consolas; font-size:10.5pt; font-style:normal; text-transform:none">ACT_GE_BTYEARRAY 资源表(存储流程定义相关的资源)</span>
    </p>
    <p style="margin:0pt; orphans:0; text-align:justify; widows:0"><span style="font-family:Calibri; font-size:10.5pt">&#xa0;</span>
    </p></div>
</body>
</html>

其中ACT_RU_JOB关键字出现两次,我想要在标记它出现的地方打印章,这里需要使用工具类找出PDF中出现关键字的位置


import cn.hutool.core.io.IoUtil;
import com.itextpdf.awt.geom.Rectangle2D;
import com.itextpdf.text.pdf.PdfDictionary;
import com.itextpdf.text.pdf.PdfName;
import com.itextpdf.text.pdf.PdfReader;
import com.itextpdf.text.pdf.parser.*;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;


public class ItextPDFUtil {


    public static final float defaultHeight = 16;
    public static final float fixHeight = 10;

    /**
     * findKeywordPostions
     * 返回查找到关键字的首个文字的左上角坐标值
     *
     * @param keyword
     * @return List<float [ ]> : float[0]:pageNum float[1]:x float[2]:y
     * @throws IOException
     */
    public static List<float[]> findKeywordPostions(InputStream pdfInputStream, String keyword) throws IOException {
        byte[] pdfData = IoUtil.readBytes(pdfInputStream);
        pdfInputStream.close();

        List<float[]> result = new ArrayList<>();
        List<PdfPageContentPositions> pdfPageContentPositions = getPdfContentPostionsList(pdfData);


        for (PdfPageContentPositions pdfPageContentPosition : pdfPageContentPositions) {
            List<float[]> charPositions = findPositions(keyword, pdfPageContentPosition);
            if (charPositions == null || charPositions.size() < 1) {
                continue;
            }
            result.addAll(charPositions);
        }
        return result;
    }


    /**
     * findKeywordPostions
     *
     * @param pdfData 通过IO流 PDF文件转化的byte数组
     * @param keyword 关键字
     * @return List<float [ ]> : float[0]:pageNum float[1]:x float[2]:y
     * @throws IOException
     */
    public static List<float[]> findKeywordPostions(byte[] pdfData, String keyword) throws IOException {
        List<float[]> result = new ArrayList<float[]>();
        List<PdfPageContentPositions> pdfPageContentPositions = getPdfContentPostionsList(pdfData);


        for (PdfPageContentPositions pdfPageContentPosition : pdfPageContentPositions) {
            List<float[]> charPositions = findPositions(keyword, pdfPageContentPosition);
            if (charPositions == null || charPositions.size() < 1) {
                continue;
            }
            result.addAll(charPositions);
        }
        return result;
    }


    private static List<PdfPageContentPositions> getPdfContentPostionsList(byte[] pdfData) throws IOException {
        PdfReader reader = new PdfReader(pdfData);

        List<PdfPageContentPositions> result = new ArrayList<PdfPageContentPositions>();

        int pages = reader.getNumberOfPages();
        for (int pageNum = 1; pageNum <= pages; pageNum++) {
            float width = reader.getPageSize(pageNum).getWidth();
            float height = reader.getPageSize(pageNum).getHeight();


            PdfRenderListener pdfRenderListener = new PdfRenderListener(pageNum, width, height);


            //解析pdf,定位位置
            PdfContentStreamProcessor processor = new PdfContentStreamProcessor(pdfRenderListener);
            PdfDictionary pageDic = reader.getPageN(pageNum);
            PdfDictionary resourcesDic = pageDic.getAsDict(PdfName.RESOURCES);
            try {
                processor.processContent(ContentByteUtils.getContentBytesForPage(reader, pageNum), resourcesDic);
            } catch (IOException e) {
                reader.close();
                throw e;
            }


            String content = pdfRenderListener.getContent();
            List<CharPosition> charPositions = pdfRenderListener.getcharPositions();


            List<float[]> positionsList = new ArrayList<float[]>();
            for (CharPosition charPosition : charPositions) {
                float[] positions = new float[]{charPosition.getPageNum(), charPosition.getX(), charPosition.getY(), charPosition.getWidth(), charPosition.getHeight()};
                positionsList.add(positions);
            }


            PdfPageContentPositions pdfPageContentPositions = new PdfPageContentPositions();
            pdfPageContentPositions.setContent(content);
            pdfPageContentPositions.setPostions(positionsList);


            result.add(pdfPageContentPositions);
        }
        reader.close();
        return result;
    }


    private static List<float[]> findPositions(String keyword, PdfPageContentPositions pdfPageContentPositions) {


        List<float[]> result = new ArrayList<float[]>();


        String content = pdfPageContentPositions.getContent();
        List<float[]> charPositions = pdfPageContentPositions.getPositions();


        for (int pos = 0; pos < content.length(); ) {
            int positionIndex = content.indexOf(keyword, pos);
            if (positionIndex == -1) {
                break;
            }
            float[] postions = charPositions.get(positionIndex);
            //此处较为关键通过第一个关键字计算出整个关键字的宽度
            for (int i = 1; i < keyword.length(); i++) {
                float[] postionsNew = charPositions.get(positionIndex + i);
                postions[3] = postions[3] + postionsNew[3];
            }
            result.add(postions);
            pos = positionIndex + 1;
        }
        return result;
    }


    private static class PdfPageContentPositions {
        private String content;
        private List<float[]> positions;


        public String getContent() {
            return content;
        }


        public void setContent(String content) {
            this.content = content;
        }


        public List<float[]> getPositions() {
            return positions;
        }


        public void setPostions(List<float[]> positions) {
            this.positions = positions;
        }
    }


    private static class PdfRenderListener implements RenderListener {
        private int pageNum;
        private float pageWidth;
        private float pageHeight;
        private StringBuilder contentBuilder = new StringBuilder();
        private List<CharPosition> charPositions = new ArrayList<CharPosition>();


        public PdfRenderListener(int pageNum, float pageWidth, float pageHeight) {
            this.pageNum = pageNum;
            this.pageWidth = pageWidth;
            this.pageHeight = pageHeight;
        }


        @Override
        public void beginTextBlock() {
        }


        @Override
        public void renderText(TextRenderInfo renderInfo) {
            List<TextRenderInfo> characterRenderInfos = renderInfo.getCharacterRenderInfos();
            for (TextRenderInfo textRenderInfo : characterRenderInfos) {
                String word = textRenderInfo.getText();
                if (word.length() > 1) {
                    word = word.substring(word.length() - 1, word.length());
                }
                Rectangle2D.Float rectangle = textRenderInfo.getAscentLine().getBoundingRectange();

                float x = (float) rectangle.getX();
                float y = (float) rectangle.getY();
//                float x = (float)rectangle.getCenterX();
//                float y = (float)rectangle.getCenterY();
//                double x = rectangle.getMinX();
//                double y = rectangle.getMaxY();


                //这两个是关键字在所在页面的XY轴的百分比
//                float xPercent = Math.round(x / pageWidth * 10000) / 10000f;
//                float yPercent = Math.round((1 - y / pageHeight) * 10000) / 10000f;


//                CharPosition charPosition = new CharPosition(pageNum, xPercent, yPercent);
                CharPosition charPosition = new CharPosition(pageNum, x, y - fixHeight, (float) rectangle.getWidth(), (float) (rectangle.getHeight() == 0 ? defaultHeight : rectangle.getHeight()));
                charPositions.add(charPosition);
                contentBuilder.append(word);
            }
        }

        @Override
        public void endTextBlock() {
        }

        @Override
        public void renderImage(ImageRenderInfo renderInfo) {
        }


        public String getContent() {
            return contentBuilder.toString();
        }


        public List<CharPosition> getcharPositions() {
            return charPositions;
        }
    }


    private static class CharPosition {
        private int pageNum = 0;
        private float x = 0;
        private float y = 0;
        private float width;
        private float height;


        public CharPosition(int pageNum, float x, float y, float width, float height) {
            this.pageNum = pageNum;
            this.x = x;
            this.y = y;
            this.width = width;
            this.height = height;
        }


        public int getPageNum() {
            return pageNum;
        }


        public float getX() {
            return x;
        }


        public float getY() {
            return y;
        }

        public float getWidth() {
            return width;
        }

        public float getHeight() {
            return height;
        }

        @Override
        public String toString() {
            return "CharPosition{" +
                    "pageNum=" + pageNum +
                    ", x=" + x +
                    ", y=" + y +
                    ", width=" + width +
                    ", height=" + height +
                    '}';
        }
    }


}

使用main方法执行


import com.itextpdf.text.DocumentException;
import com.itextpdf.text.Image;
import com.itextpdf.text.pdf.*;

import java.io.FileOutputStream;
import java.io.IOException;
import java.util.List;

public class Test {

    public static void main(String[] args) throws IOException, DocumentException {
        String html = PDFHelper.getTemplateContent("071515133867.ftl", null);
        byte[] bytes = PDFHelper.html2Pdf(html);
        //查找位置
        List<float[]> postions = ItextPDFUtil.findKeywordPostions(bytes, "ACT_RU_JOB");

        // 第一次找到的位置
        float[] postion = postions.get(0);
        System.out.println("x:" + postion[1] + " y:" + postion[2]);

        //Modify file using PdfReader
        //Read file using PdfReader
        PdfReader pdfReader = new PdfReader(bytes);
        PdfStamper pdfStamper = new PdfStamper(pdfReader, new FileOutputStream("D:/test.pdf"));

        Image image = Image.getInstance("E:\\workspace\\java_pdf\\src\\main\\resources\\images\\69f70ad3-1cd7-4793-8c89-99dea0311a1b.001.png");
        //Fixed Positioning
//        固定图片大小
        image.scaleAbsolute(100, 100);
        //Scale to new height and new width of image
        // 偏移量
        image.setAbsolutePosition(postion[1], postion[2]);

        System.out.println("pages-1:" + pdfReader.getNumberOfPages());

        PdfContentByte content = pdfStamper.getUnderContent((int) postion[0]);
        content.addImage(image);

        // 第二次找到的位置
        postion = postions.get(1);
        //Fixed Positioning
//        image.scaleToFit(100, 50);
        image.scaleAbsolute(100, 100);
        //Scale to new height and new width of image
        image.setAbsolutePosition(postion[1], postion[2]);

        System.out.println("pages-2:" + pdfReader.getNumberOfPages());
        content = pdfStamper.getUnderContent((int) postion[0]);
        content.addImage(image);

//        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
//        byte[] pdf = content.toPdf(PdfWriter.getInstance(content.getPdfDocument(), outputStream));
        pdfStamper.close();
    }
}

最后看一下效果
在这里插入图片描述

Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐