pdfbox
1.引入maven依赖
<!-- https://mvnrepository.com/artifact/org.apache.pdfbox/pdfbox -->
<dependency>
<groupId>org.apache.pdfbox</groupId>
<artifactId>pdfbox</artifactId>
<version>3.0.3</version>
</dependency>
2.各个方法解析
加载pdf文档,获取基本信息
// 这个必须要使用try-with-resources包围
PDDocument doc = Loader.loadPDF(new File(pdfPath))
// 获取pdf第一页
PDPage page = doc.getPage(0);
// 获取pdf页的各个属性
PDRectangle mediaBox = page.getMediaBox();
// 获取pdf页面的宽和高,单位是 点(points),在印刷和排版领域,“点” 是一种常用的长度单位1 英寸等于 72 点,也就是说 1 毫米等于 2.8346457 点
float width = mediaBox.getWidth();
float height = mediaBox.getHeight();
// 将点转化成毫米,保留1位小数(一般pdf里面都是直接保留到毫米)
BigDecimal width_mm = new BigDecimal(width).divide(new BigDecimal("2.8346457"), 1, RoundingMode.HALF_UP);
BigDecimal height_mm = new BigDecimal(height).divide(new BigDecimal("2.8346457"), 1, RoundingMode.HALF_UP);
将pdf图片页面渲染成图片
//
PDFRenderer renderer = new PDFRenderer(doc);
// 这里第一个参数是读取pdf里面的第几个图片,第2个参数是渲染图片的DPI,一般人眼看300就完全够用,600一般是用于高精度印刷,900-1200主要用于高进度扫描等,DPI越大,渲染时间越长,图片精度越高,图片越大。第3个参数是图片类型,默认ImageType.RGB,还有ImageType.ARGB和ImageType.GRAY
BufferedImage pageImage = renderer.renderImageWithDPI(0, 600);
BufferedImage pageImage = renderer.renderImageWithDPI(pageIndex, dpi, ImageType.RGB);
1. ImageType.RGB
- 含义:RGB(Red, Green, Blue)是一种加色模型,它通过组合红、绿、蓝三种基本颜色的不同强度来表示各种颜色。在
ImageType.RGB
模式下,每个像素由三个字节(24 位)表示,分别对应红、绿、蓝三个颜色通道,每个通道的取值范围是 0 - 255。 - 特点
- 可以表示丰富的颜色,大约能表示 1677 万种不同的颜色(224),适用于大多数彩色图像的显示和处理。
- 颜色信息较为准确和细腻,能很好地还原 PDF 文档中的彩色内容。
- 适用场景:当 PDF 文档包含大量彩色图像、图表或文字时,使用
ImageType.RGB
可以获得高质量的渲染效果。
2. ImageType.ARGB
- 含义:ARGB 是在 RGB 的基础上增加了一个 Alpha 通道(透明度通道)。每个像素由四个字节(32 位)表示,分别对应 Alpha、红、绿、蓝四个通道,Alpha 通道的取值范围也是 0 - 255,0 表示完全透明,255 表示完全不透明。
- 特点
- 除了能表示丰富的颜色外,还可以处理图像的透明度,使得图像可以与背景进行融合。
- 由于增加了 Alpha 通道,文件大小相对 RGB 模式会更大一些。
- 适用场景:当需要处理带有透明效果的 PDF 元素(如透明的图像、水印等)时,使用
ImageType.ARGB
可以保留这些透明度信息。
3. ImageType.GRAY
- 含义:GRAY 模式表示灰度图像,每个像素只用一个字节(8 位)表示,取值范围是 0 - 255,0 表示黑色,255 表示白色,中间的值表示不同程度的灰色。
- 特点
- 只包含亮度信息,不包含颜色信息,因此文件大小相对较小。
- 处理速度较快,因为只需要处理一个通道的数据。
- 适用场景:当 PDF 文档是黑白文档,或者只需要关注图像的亮度信息而不需要颜色信息时,使用
ImageType.GRAY
可以提高处理效率并减少存储空间。
创建PDF图像对象
// 创建一个输出pdf
PDDocument outputDocument = new PDDocument()
// 使用LosslessFactory创建pdf,精度样式损失最少
PDImageXObject pdImage1 = LosslessFactory.createFromImage(outputDocument, pageImage)
// 使用JPEGFactory创建pdf,支持对图片有损压缩
PDImageXObject pdImage2 = JPEGFactory.createFromImage(outputDocument, pageImage, 0.8f);
// 直接读取图片创建pdf,pdf质量和图片质量关联
PDImageXObject pdImage3 = PDImageXObject.createFromFile("path/to/image.jpg", outputDocument);
1. LosslessFactory.createFromImage
方法
PDImageXObject pdImage1 = LosslessFactory.createFromImage(outputDocument, pageImage);
- 原理:此方法会把
BufferedImage
对象转换为PDImageXObject
对象,并且在转换过程中尽可能减少图像精度和样式的损失。它运用无损压缩算法来保存图像数据,从而保证图像质量。 - 适用场景:适用于对图像质量要求极高的场景,像处理包含精细图形、高质量照片等的 PDF 文档。
- 优点:能最大程度保留图像的原始质量,不会出现明显的压缩失真。
- 缺点:由于采用无损压缩,生成的 PDF 文件可能会相对较大,尤其是对于大尺寸或高分辨率的图像。
2. JPEGFactory.createFromImage
方法
PDImageXObject pdImage2 = JPEGFactory.createFromImage(outputDocument, pageImage, 0.8f);
- 原理:该方法会把
BufferedImage
对象转换为PDImageXObject
对象,并且使用 JPEG 压缩算法对图像进行压缩。你可以通过传入一个质量参数(范围是 0.0f - 1.0f)来控制压缩质量,1.0f 表示最高质量,0.0f 表示最低质量。 - 适用场景:适用于对图像质量要求不是特别高,但对文件大小有严格限制的场景,比如需要通过网络传输的 PDF 文档。
- 优点:可以显著减小 PDF 文件的大小,特别是对于色彩丰富的照片类图像,压缩效果明显。
- 缺点:JPEG 是一种有损压缩算法,可能会导致图像质量有所下降,尤其是在压缩质量较低时,图像可能会出现明显的失真。
3. PDImageXObject.createFromFile
方法
PDImageXObject pdImage3 = PDImageXObject.createFromFile("path/to/image.jpg", outputDocument);
- 原理:此方法直接从文件系统中读取图像文件,并将其转换为
PDImageXObject
对象。它会根据图像文件的格式自动选择合适的处理方式。 - 适用场景:适用于已经有现成的图像文件,并且希望直接将其添加到 PDF 文档中的场景。
- 优点:使用简单,无需先将图像加载为
BufferedImage
对象,可直接从文件创建PDImageXObject
。 - 缺点:如果图像文件本身质量不佳或格式不兼容,可能会影响最终 PDF 文档中的图像显示效果。而且,该方法不支持在创建过程中对图像进行额外的处理,如压缩质量调整等。
方法对比总结
方法 | 压缩方式 | 图像质量 | 文件大小 | 适用场景 |
---|---|---|---|---|
LosslessFactory.createFromImage | 无损压缩 | 高 | 大 | 对图像质量要求极高,不考虑文件大小 |
JPEGFactory.createFromImage | 有损压缩(JPEG) | 可调整 | 小 | 对文件大小有严格限制,图像质量要求不是特别高 |
PDImageXObject.createFromFile | 无(取决于原始文件) | 取决于原始文件 | 取决于原始文件 | 已有现 |
创建pdf页面,绘制页面内容并保存
// 创建页面(这里两个参数都是float类型,并且单位是点,如果单位是毫米,需要乘2.8346457)
PDRectangle pageSize = new PDRectangle(pdfWidthPoints, pdfHeightPoints);
// 将页面添加到pdf里面去
PDPage page = new PDPage(pageSize);
// outputDocument的类型是PDDocument,就相当于整个pdf文件,我们通过这个对象可以实现获取创建pdf。
outputDocument.addPage(page);
PDPageContentStream contentStream = new PDPageContentStream(outputDocument, newPage1);
// 将第一个页面绘制到新页面的左侧,这个的第一个参数是要绘制的图片,第二,三个参数是绘制的开始点坐标,第四,五个参数是图片的宽和高。如果这个宽和高和原图片的不一致,就会导致图片被拉伸和压缩。
contentStream.drawImage(pdImage1, 0, 0, width, height);
// 将第二个页面绘制到新页面的右侧
contentStream.drawImage(pdImage2, width, 0, width, height);
// 保存新的 PDF 文件,传入保存的路径(包含文件夹路径和最后的pdf名称)
outputDocument.save(outputPath);
绘制图片时旋转
// 构建旋转矩阵并应用变换 a(水平缩放因子)、b(水平错切因子)、c(垂直错切因子)、d(垂直缩放因子)、e(水平平移量)、f(垂直平移量)
Matrix rotationMatrix = new Matrix(-1, 0, 0, -1, width, height);
contentStream.transform(rotationMatrix);
contentStream.drawImage(pdImage2, 0, 0, width, height);
在仿射变换矩阵 Matrix(a, b, c, d, e, f)
中,对于围绕原点的旋转操作,我们可以使用三角函数公式来计算参数 a
、b
、c
和 d
:
参数 e
和 f
用于平移操作,通常在旋转时,如果要将旋转中心移到图像中心,需要根据图像的宽度 width
和高度 height
进行适当的平移。这个可能比较复杂,你可以通过其他方法实现,比如直接将图片旋转后保存再通过**PDImageXObject pdImage3 = PDImageXObject.createFromFile(“path/to/image.jpg”, outputDocument);**方法重新读取。
旋转 45°为例
double angle = Math.toRadians(45);
float a = (float) Math.cos(angle);
float b = (float) -Math.sin(angle);
float c = (float) Math.sin(angle);
float d = (float) Math.cos(angle);
float e = (float) (width / 2 * (1 - a) + height / 2 * b);
float f = (float) (height / 2 * (1 - d) - width / 2 * c);
Matrix rotationMatrix = new Matrix(a, b, c, d, e, f);
自定义旋转方法
/**
* 在指定的中心点位置绘制图像,并按指定角度旋转
*
* @param contentStream 内容流对象
* @param image 要绘制的图像
* @param centerX 图像中心点X坐标
* @param centerY 图像中心点Y坐标
* @param width 图像宽度
* @param height 图像高度
* @param rotationAngle 旋转角度(度数)
* @throws IOException 如果绘制过程中发生I/O错误
*/
private static void drawImageAtCenter(PDPageContentStream contentStream, PDImageXObject image,
float centerX, float centerY, float width, float height, float rotationAngle) throws IOException {
// 保存当前图形状态
contentStream.saveGraphicsState();
// 平移到中心点位置
Matrix translateMatrix = Matrix.getTranslateInstance(centerX, centerY);
contentStream.transform(translateMatrix);
// 旋转指定角度(将角度转换为弧度)
float radians = (float) Math.toRadians(rotationAngle);
Matrix rotationMatrix = new Matrix(
(float) Math.cos(radians), (float) Math.sin(radians),
-(float) Math.sin(radians), (float) Math.cos(radians),
0, 0);
contentStream.transform(rotationMatrix);
// 将图像绘制在中心点,需要将图像移回原点
contentStream.drawImage(image, -width / 2, -height / 2, width, height);
// 恢复图形状态
contentStream.restoreGraphicsState();
}
3.完整示例
下面示例实现了将一个pdf的每页按照两种不同方式渲染成图片,并将两种图片拼接到一起成为一个pdf页面,还是用自定义旋转方法实现将第2个图片旋转。
import org.apache.pdfbox.Loader;
import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.pdmodel.PDPage;
import org.apache.pdfbox.pdmodel.PDPageContentStream;
import org.apache.pdfbox.pdmodel.PDPageTree;
import org.apache.pdfbox.pdmodel.common.PDRectangle;
import org.apache.pdfbox.pdmodel.graphics.image.JPEGFactory;
import org.apache.pdfbox.pdmodel.graphics.image.LosslessFactory;
import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject;
import org.apache.pdfbox.rendering.ImageType;
import org.apache.pdfbox.rendering.PDFRenderer;
import org.apache.pdfbox.util.Matrix;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.math.BigDecimal;
import java.math.RoundingMode;
public class PdfDpiReader {
private static final int DPI = 100;
/**
* 在指定的中心点位置绘制图像,并按指定角度旋转
*
* @param contentStream 内容流对象
* @param image 要绘制的图像
* @param centerX 图像中心点X坐标
* @param centerY 图像中心点Y坐标
* @param width 图像宽度
* @param height 图像高度
* @param rotationAngle 旋转角度(度数)
* @throws IOException 如果绘制过程中发生I/O错误
*/
private static void drawImageAtCenter(PDPageContentStream contentStream, PDImageXObject image,
float centerX, float centerY, float width, float height, float rotationAngle) throws IOException {
// 保存当前图形状态
contentStream.saveGraphicsState();
// 平移到中心点位置
Matrix translateMatrix = Matrix.getTranslateInstance(centerX, centerY);
contentStream.transform(translateMatrix);
// 旋转指定角度(将角度转换为弧度)
float radians = (float) Math.toRadians(rotationAngle);
Matrix rotationMatrix = new Matrix(
(float) Math.cos(radians), (float) Math.sin(radians),
-(float) Math.sin(radians), (float) Math.cos(radians),
0, 0);
contentStream.transform(rotationMatrix);
// 将图像绘制在中心点,需要将图像移回原点
contentStream.drawImage(image, -width / 2, -height / 2, width, height);
// 恢复图形状态
contentStream.restoreGraphicsState();
}
public static void mergeFirstPages(String pdfPath, String outputPath) throws IOException {
try (PDDocument doc = Loader.loadPDF(new File(pdfPath)); PDDocument outputDocument = new PDDocument()) {
PDFRenderer renderer = new PDFRenderer(doc);
PDPageTree pages = doc.getPages();
int numberOfPages = doc.getNumberOfPages();
for (int i = 0; i < numberOfPages; i++) {
PDPage page = doc.getPage(i);
PDRectangle mediaBox = page.getMediaBox();
float width = mediaBox.getWidth();
float height = mediaBox.getHeight();
System.out.println("第" + i + "页的尺寸信息:" + "宽度:" + width + " 点 高度:" + height + " 点");
BigDecimal width_mm = new BigDecimal(width).divide(new BigDecimal("2.8346457"), 1, RoundingMode.HALF_UP);
BigDecimal height_mm = new BigDecimal(height).divide(new BigDecimal("2.8346457"), 1, RoundingMode.HALF_UP);
System.out.println("第" + i + "页的尺寸信息:" + "宽度:" + width_mm + " 毫米 高度:" + height_mm + " 毫米");
BufferedImage rgb_image = renderer.renderImageWithDPI(i, DPI, ImageType.RGB);
System.out.println("第" + i + "页使用 " + DPI + " DPI 渲染后的图像宽度:" + rgb_image.getWidth() + " 像素,高度:" + rgb_image.getHeight() + " 像素");
PDImageXObject pdImage1 = LosslessFactory.createFromImage(outputDocument, rgb_image);
PDImageXObject pdImage2 = JPEGFactory.createFromImage(outputDocument, rgb_image, 0.1f);
PDRectangle pageSize = new PDRectangle(width * 2, height);
PDPage pdPage = new PDPage(pageSize);
outputDocument.addPage(pdPage);
try (PDPageContentStream contentStream = new PDPageContentStream(outputDocument, pdPage)) {
// 使用中心点坐标绘制第一张图片(不旋转)
drawImageAtCenter(contentStream, pdImage1, width / 2, height / 2, width, height, 0f);
// 使用中心点坐标绘制第二张图片(旋转指定角度)
drawImageAtCenter(contentStream, pdImage2, width * 1.5f, height / 2, width, height, 90f);
}
}
outputDocument.save(outputPath);
}
}
public static void main(String[] args) {
try {
String pdfPath = "D:\\work\\demo\\pdf\\test.pdf";
String outputPath = "D:\\work\\demo\\pdf\\merged.pdf";
mergeFirstPages(pdfPath, outputPath);
System.out.println("PDF 合并完成,保存路径: " + outputPath);
} catch (IOException e) {
e.printStackTrace();
}
}
}
4.直接读取pdf中的图片
上面展示的方法是将pdf的每一页,转化成一个图片,这个渲染效率比较低,下面将展示直接从pdf里面读取图片。
PDPage page = 按照上面获取的PDPage;
PDResources resources = page.getResources();
Iterable<COSName> xObjectNames = resources.getXObjectNames();
PDResources resources = page.getResources();
Iterable<COSName> xObjectNames = resources.getXObjectNames();
// 下面会获取这一页pdf的所有图片,现在这个有一个很严重的bug,我也不知道怎么回事,就是明明我的pdf一页只有一个图片,
// 它会把这个图片拆分成很多不会图片,完整图片也包含在里面,所以需要去找是哪一张,可以根据色彩空间,获取的图片大小来筛选出,
// 但也有时候不包含完整的图片,就需要通过图片渲染来
List<PDImageXObject> images = new ArrayList<>();
for (COSName name : xObjectNames) {
PDXObject pdxObject = resources.getXObject(name);
if (pdxObject instanceof PDImageXObject) {
images.add((PDImageXObject) pdxObject);
}
}
// 你可以通过自定义方法筛选出正确的图片,这里我就选择了第一张图片获取到BufferedImage。
// 如果根据筛选出来为null,那么你还可以按照上面的图片渲染方式获取到图片。这样可以大大缩短渲染时间。
BufferedImage bufferedImage = images.get(0).getImage();