Java实现pdf文件压缩(aspose-pdf实现压缩、itextpdf去除aspose-pdf版权水印)

Java实现pdf文件压缩

时间换空间,实现pdf文件无损压缩。

1、依赖准备

市面上操作pdf文件的组件有spire.pdf.free、itextpdf、openpdf、pdfbox等,它们各有千秋。我们主要完成的场景为压缩,减少文件大小去优化存储、传输等。

在这里选取的组件为aspose-pdfitextpdf,原因是spire.pdf.free压缩代码比较直观和简单但是只能免费压缩前10页,itextpdf压缩代码较为复杂开发难度大适合去水印,而openpdfpdfbox也有开发难度较大的问题。

1、aspose-pdf依赖

可能比较冷门,阿里云maven仓库等没有对应的依赖,无法通过gav坐标添加!因此我们需要到中央仓库下载jar包!

地址为https://mvnrepository.com/artifact/com.aspose/aspose-pdf

建议选择低版本,高版本难以去除版权水印,如这里选择21.11版本的

在这里插入图片描述
将jar引入工程

这里可以参考这篇文章

https://blog.youkuaiyun.com/m0_46357847/article/details/140749772

如果是gradle工程,可参考下图

在这里插入图片描述

2、itextpdf依赖

这里主要用于去除aspose-pdf的版权水印,直接添加即可。

<!-- https://mvnrepository.com/artifact/com.itextpdf/itextpdf -->
<dependency>
    <groupId>com.itextpdf</groupId>
    <artifactId>itextpdf</artifactId>
    <version>5.5.13</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.itextpdf/itext-asian -->
<dependency>
    <groupId>com.itextpdf</groupId>
    <artifactId>itext-asian</artifactId>
    <version>5.2.0</version>
</dependency>

2、压缩代码实现

PdfCompression.java压缩逻辑与去除水印的逻辑都在这个类上。

/**
 * TODO
 *
 * @Description
 * @Author laizhenghua
 * @Date 2025/2/25 11:28
 **/
public class PdfCompression {
    private final static Logger log = LoggerFactory.getLogger(PdfCompression.class);
    /**
     * 水印字体常量
     */
    private static final String WATERMARK_TEXT = "Evaluation Only. Created with Aspose.PDF. Copyright 2002-2021 Aspose Pty Ltd.";
    /**
     * 压缩比0-100可选 越低压缩比越大
     */
    private int imageQuality = 40;

    public PdfCompression() {

    }

    public PdfCompression(int imageQuality) {
        this.imageQuality = imageQuality;
    }

    public void start(String fileName, String src, String dest) {
        InputStream inputStream = null;
        OutputStream outputStream = null;
        try {
            File srcFile = new File(src);
            inputStream = new FileInputStream(srcFile);

            File destFile = new File(dest);
            if (!destFile.exists()) {
                destFile.createNewFile();
            }
            outputStream = new FileOutputStream(destFile);
            start(fileName, inputStream, outputStream);
        } catch (IOException ex) {
            log.error(ex.getMessage());
            ex.printStackTrace();
        } finally {
            IoUtil.close(inputStream);
            IoUtil.close(outputStream);
        }
    }

    public void start(String fileName, InputStream inputStream, OutputStream outputStream) {
        long startTime = System.currentTimeMillis();
        int sourceSize = 0;
        long compressionSize = 0;
        OutputStream tempOutputStream = null;
        InputStream tempInputStream = null;
        try {
            // 创建临时文件
            // File tempFile = PathUtil.getDistTempFile(fileName);
            // 使用 hutool 工具类创建临时文件
            File tempFile = FileUtil.createTempFile("temp", ".pdf", new File("src/main/resources/static/"), true);
            tempOutputStream = new FileOutputStream(tempFile);

            Locale locale = new Locale("zh", "cn");
            Locale.setDefault(locale);

            // 记录原始大小单位为MB
            sourceSize = inputStream.available() / (1024 * 1024);
            // 读取pdf文档
            Document document = new Document(inputStream);
            // 设置压缩属性
            OptimizationOptions options = new OptimizationOptions();
            // 删除PDF不必要的对象
            options.setRemoveUnusedObjects(true);
            // 链接重复流
            options.setLinkDuplcateStreams(false);
            // 删除未使用的流
            options.setRemoveUnusedStreams(false);
            // 删除不必要的字体
            options.setUnembedFonts(true);
            // 压缩PDF中的图片
            options.getImageCompressionOptions().setCompressImages(true);
            // 图片压缩比 0-100可选 越低压缩比越大
            options.getImageCompressionOptions().setImageQuality(imageQuality);
            document.optimizeResources(options);
            // 优化web的PDF文档
            document.optimize();
            // 先输出到临时文件方便后续去除水印
            document.save(tempOutputStream);
            // 关闭文档-此时 aspose-pdf 使命已达
            document.close();
            // tempOutputStream.flush();

            // 重新记录压缩后的大小
            compressionSize = tempFile.length() / (1024 * 1024);

            // 使用 itext-pdf 去除水印
            // ================== 去除水印 ==================
            List<MatchItem> matchItemList = new ArrayList<>();

            // itext-pdf reader
            tempInputStream = new FileInputStream(tempFile);
            PdfReader reader = new PdfReader(tempInputStream);

            PdfReaderContentParser parser = new PdfReaderContentParser(reader);
            // pdf页数
            int pageSize = reader.getNumberOfPages();

            for (int pageNum = 1; pageNum <= pageSize; pageNum++) {
                Rectangle rectangle = reader.getPageSize(pageNum);
                // 匹配监听
                KeyWordPositionListener listener = new KeyWordPositionListener();
                listener.setKeyword(WATERMARK_TEXT);
                listener.setPageNumber(pageNum);
                listener.setCurPageSize(rectangle);
                parser.processContent(pageNum, listener);

                // 先判断本页中是否存在关键词
                List<MatchItem> allItems = listener.getAllItems();

                StringBuilder sbTemp = new StringBuilder();

                // 将一页中所有的块内容连接起来组成一个字符串
                for (MatchItem item : allItems) {
                    sbTemp.append(item.getContent());
                }
                List<MatchItem> matches = listener.getMatches();

                // 第一种情况:关键词与块内容完全匹配的项直接返回
                if (!sbTemp.toString().contains(WATERMARK_TEXT) || matches.size() > 0) {
                    matchItemList.addAll(matches);
                    continue;
                }
                // 第二种情况:多个块内容拼成一个关键词,则一个一个来匹配,组装成一个关键词
                sbTemp = new StringBuilder();
                List<MatchItem> tempItems = new ArrayList<>();
                for (MatchItem item : allItems) {
                    if (WATERMARK_TEXT.contains(item.getContent())) {
                        tempItems.add(item);
                        sbTemp.append(item.getContent());
                        // 如果暂存的字符串和关键词 不再匹配时
                        if (!WATERMARK_TEXT.contains(sbTemp.toString())) {
                            sbTemp = new StringBuilder(item.getContent());
                            tempItems.clear();
                            tempItems.add(item);
                        }
                        // 暂存的字符串正好匹配到关键词时
                        if (sbTemp.toString().equalsIgnoreCase(WATERMARK_TEXT)) {
                            // 得到匹配的项
                            matches.add(tempItems.get(0));
                            // 清空暂存的字符串
                            sbTemp = new StringBuilder();
                            // 清空暂存的LIST
                            tempItems.clear();
                            // 继续查找
                        }
                    } else {
                        // 如果找不到则清空
                        sbTemp = new StringBuilder();
                        tempItems.clear();
                    }
                }
                matchItemList.addAll(matches);
            }

            PdfStamper stamper = new PdfStamper(reader, outputStream);
            PdfContentByte canvas = null;

            Map<Integer, List<MatchItem>> mapItem = new HashMap<>();
            List<MatchItem> itemList = null;

            for (MatchItem item : matchItemList) {
                Integer pageNum = item.getPageNum();
                if (mapItem.containsKey(pageNum)) {
                    itemList = mapItem.get(pageNum);
                    itemList.add(item);
                } else {
                    itemList = new ArrayList<>();
                    itemList.add(item);
                    mapItem.put(pageNum, itemList);
                }
            }

            // 遍历每一页去修改
            for (Integer page : mapItem.keySet()) {
                List<MatchItem> items = mapItem.get(page);
                // 遍历每一页中的匹配项
                for (MatchItem item : items) {
                    canvas = stamper.getOverContent(page);
                    float x = item.getX();
                    float y = item.getY();
                    float fontWidth = item.getFontWidth();
                    canvas.saveState();
                    canvas.setColorFill(BaseColor.WHITE);
                    canvas.rectangle(x, y, fontWidth * WATERMARK_TEXT.length(), fontWidth + 2);
                    canvas.fill();
                    canvas.restoreState();
                    // 开始写入文本
                    canvas.beginText();
                    BaseFont bf = BaseFont.createFont("STSong-Light", "UniGB-UCS2-H", BaseFont.EMBEDDED);
                    Font font = new Font(bf, fontWidth, Font.BOLD);
                    // 设置字体和大小
                    canvas.setFontAndSize(font.getBaseFont(), fontWidth);
                    // 设置字体的输出位置
                    canvas.setTextMatrix(x, y + fontWidth / 10 + 0.5f);
                    // 要输出的text
                    canvas.showText("");
                    canvas.endText();
                }
            }
            stamper.close();
            reader.close();
            // 使用 hutool 工具类删除临时文件
            FileUtil.del(tempFile);
        } catch (Exception ex) {
            ex.printStackTrace();
        } finally {
            IoUtil.close(tempOutputStream);
            IoUtil.close(tempInputStream);
        }

        long endTime = System.currentTimeMillis();

        long duration = endTime - startTime;

        log.info("[{}] 压缩成功:[{}MB -> {}MB] 耗时为 {}s", fileName, sourceSize, compressionSize, duration / 1000);
    }
}

其他新增的辅助类:

MatchItem.java

/**
 * TODO
 *
 * @Description
 * @Author laizhenghua
 * @Date 2025/2/25 09:48
 **/
public class MatchItem {
    // 页数
    private Integer pageNum;
    // x坐标
    private Float x;
    // y坐标
    private Float y;
    // 页宽
    private Float pageWidth;
    // 页高
    private Float pageHeight;
    // 匹配字符
    private String content;
    // 字体宽
    private float fontWidth;
    // 字体高
    private float fontHeight = 12;

    public Integer getPageNum() {
        return pageNum;
    }

    public void setPageNum(Integer pageNum) {
        this.pageNum = pageNum;
    }

    public Float getX() {
        return x;
    }

    public void setX(Float x) {
        this.x = x;
    }

    public Float getY() {
        return y;
    }

    public void setY(Float y) {
        this.y = y;
    }

    public Float getPageWidth() {
        return pageWidth;
    }

    public void setPageWidth(Float pageWidth) {
        this.pageWidth = pageWidth;
    }

    public Float getPageHeight() {
        return pageHeight;
    }

    public void setPageHeight(Float pageHeight) {
        this.pageHeight = pageHeight;
    }

    public String getContent() {
        return content;
    }

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

    public float getFontWidth() {
        return fontWidth;
    }

    public void setFontWidth(float fontWidth) {
        this.fontWidth = fontWidth;
    }

    public float getFontHeight() {
        return fontHeight;
    }

    public void setFontHeight(float fontHeight) {
        this.fontHeight = fontHeight;
    }
}

KeyWordPositionListener.java

/**
 * TODO
 *
 * @Description 用来匹配pdf的关键词-监听类
 * @Author laizhenghua
 * @Date 2025/2/25 10:01
 **/
public class KeyWordPositionListener implements RenderListener {
    // 存放匹配上的字符信息
    private final List<MatchItem> matches = new ArrayList<>();
    // 存放所有的字符信息
    private List<MatchItem> allItems = new ArrayList<>();

    private Rectangle curPageSize;
    /**
     * 匹配的关键字
     */
    private String keyword;
    /**
     * 匹配的当前页
     */
    private Integer pageNumber;

    @Override
    public void beginTextBlock() {
        // do nothing
    }

    @Override
    public void renderText(TextRenderInfo renderInfo) {
        // 获取字符
        String content = renderInfo.getText();
        Rectangle2D.Float textRectangle = renderInfo.getDescentLine().getBoundingRectange();

        MatchItem item = new MatchItem();
        item.setContent(content);
        item.setPageNum(pageNumber);
        item.setFontHeight(textRectangle.height == 0 ? 12 : textRectangle.height); // 默认12
        item.setFontWidth(textRectangle.width);
        item.setPageHeight(curPageSize.getHeight());
        item.setPageWidth(curPageSize.getWidth());
        item.setX((float) textRectangle.getX());
        item.setY((float) textRectangle.getY());

        // 若keyword是单个字符,匹配上的情况
        if (content.equalsIgnoreCase(keyword)) {
            matches.add(item);
        }
        // 保存所有的项
        allItems.add(item);
    }

    @Override
    public void endTextBlock() {
        // do nothing
    }

    @Override
    public void renderImage(ImageRenderInfo renderInfo) {
        //do nothing
    }

    /**
     * 设置需要匹配的当前页
     *
     * @param pageNumber
     */
    public void setPageNumber(Integer pageNumber) {
        this.pageNumber = pageNumber;
    }

    /**
     * 设置需要匹配的关键字,忽略大小写
     *
     * @param keyword
     */
    public void setKeyword(String keyword) {
        this.keyword = keyword;
    }

    /**
     * 返回匹配的结果列表
     *
     * @return
     */
    public List<MatchItem> getMatches() {
        return matches;
    }

    public  void setCurPageSize(Rectangle rect) {
        this.curPageSize = rect;
    }

    public List<MatchItem> getAllItems() {
        return allItems;
    }

    public void setAllItems(List<MatchItem> allItems) {
        this.allItems = allItems;
    }
}

3、测试

详见以下代码

@Test
public void test4() {
    ClassPathResource resource = new ClassPathResource("/static/dist.pdf");
    InputStream inputStream = null;
    OutputStream outputStream = null;
    try {
        File file = new File("src/main/resources/static/output.pdf");
        if (!file.exists()) {
            file.createNewFile();
        }
        inputStream = resource.getInputStream();
        outputStream = new FileOutputStream(file);
        // 创建压缩类
        PdfCompression pdfCompression = new PdfCompression();
        // 调用start()方法开始压缩
        pdfCompression.start(resource.getFilename(), inputStream, outputStream);
    } catch (IOException ex) {
        ex.printStackTrace();
    } finally {
        IoUtil.close(inputStream);
        IoUtil.close(outputStream);
    }
}

执行代码后输出日志

在这里插入图片描述

再来看压缩效果

在这里插入图片描述

4、去水印优化

我们发现使用itext-pdf去除水印,还是涉及一次IO(影响压缩效率),并且代码比较复杂不太友好。

其实这里可以不用这么复杂,我们aspose-pdf工具版本可以选低一点,就可以使用网上的许可证,避免还需要使用itext-pdf去除水印,例如

license.xml

<License>
    <Data>
        <Products>
            <Product>Aspose.Total for Java</Product>
            <Product>Aspose.Words for Java</Product>
        </Products>
        <EditionType>Enterprise</EditionType>
        <SubscriptionExpiry>20991231</SubscriptionExpiry>
        <LicenseExpiry>20991231</LicenseExpiry>
        <SerialNumber>8bfe198c-7f0c-4ef8-8ff0-acc3237bf0d7</SerialNumber>
    </Data>
    <Signature>
        sNLLKGMUdF0r8O1kKilWAGdgfs2BvJb/2Xp8p5iuDVfZXmhppo+d0Ran1P9TKdjV4ABwAgKXxJ3jcQTqE/2IRfqwnPf8itN8aFZlV3TJPYeD3yWE7IT55Gz6EijUpC7aKeoohTb4w2fpox58wWoF3SNp6sK6jDfiAUGEHYJ9pjU=
    </Signature>
</License>

改进后的代码

public void startByLicense(InputStream inputStream, OutputStream outputStream) {
    long startTime = System.currentTimeMillis();
     int sourceSize = 0;
     long compressionSize = 0;
     try {
         Locale locale = new Locale("zh", "cn");
         Locale.setDefault(locale);
         // 记录原始大小单位为MB
         // sourceSize = inputStream.available() / (1024 * 1024);
         InputStream is = new FileInputStream("src/main/resources/static/license.xml");
         License license = new License();
         license.setLicense(is);
         is.close();

         // 读取pdf文档
         Document document = new Document(inputStream);
         // 设置压缩属性
         OptimizationOptions options = new OptimizationOptions();
         // 删除PDF不必要的对象
         options.setRemoveUnusedObjects(true);
         // 链接重复流
         options.setLinkDuplcateStreams(false);
         // 删除未使用的流
         options.setRemoveUnusedStreams(false);
         // 删除不必要的字体
         options.setUnembedFonts(true);
         // 压缩PDF中的图片
         options.getImageCompressionOptions().setCompressImages(true);
         // 图片压缩比 0-100可选 越低压缩比越大
         options.getImageCompressionOptions().setImageQuality(imageQuality);
         document.optimizeResources(options);
         // 优化web的PDF文档
         document.optimize();
         // 先输出到临时文件方便后续去除水印
         document.save(outputStream);
         // 关闭文档-此时 aspose-pdf 使命已达
         document.close();
         // 重新记录压缩后的大小
         // compressionSize = tempFile.length() / (1024 * 1024);
     } catch (Exception ex) {
         ex.printStackTrace();
     }

     long endTime = System.currentTimeMillis();

     long duration = endTime - startTime;

     // log.info("[{}] 压缩成功:[{}MB -> {}MB] 耗时为 {}s", fileName, sourceSize, compressionSize, duration / 1000);
 }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

laizhenghua

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

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

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

打赏作者

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

抵扣说明:

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

余额充值