【发布时间】:2010-10-30 19:40:54
【问题描述】:
我在 Java 中使用 PdfBox 从 PDF 文件中提取文本。提供的某些输入文件无效,PDFTextStripper 在这些文件上停止。是否有一种干净的方法来检查提供的文件是否确实是有效的 PDF?
【问题讨论】:
-
我的王国??任何链接或说明?
标签: java validation pdf text
我在 Java 中使用 PdfBox 从 PDF 文件中提取文本。提供的某些输入文件无效,PDFTextStripper 在这些文件上停止。是否有一种干净的方法来检查提供的文件是否确实是有效的 PDF?
【问题讨论】:
标签: java validation pdf text
这是我在 NUnit 测试中使用的,它必须针对使用 Crystal Reports 生成的多个 PDF 版本进行验证:
public static void CheckIsPDF(byte[] data)
{
Assert.IsNotNull(data);
Assert.Greater(data.Length,4);
// header
Assert.AreEqual(data[0],0x25); // %
Assert.AreEqual(data[1],0x50); // P
Assert.AreEqual(data[2],0x44); // D
Assert.AreEqual(data[3],0x46); // F
Assert.AreEqual(data[4],0x2D); // -
if(data[5]==0x31 && data[6]==0x2E && data[7]==0x33) // version is 1.3 ?
{
// file terminator
Assert.AreEqual(data[data.Length-7],0x25); // %
Assert.AreEqual(data[data.Length-6],0x25); // %
Assert.AreEqual(data[data.Length-5],0x45); // E
Assert.AreEqual(data[data.Length-4],0x4F); // O
Assert.AreEqual(data[data.Length-3],0x46); // F
Assert.AreEqual(data[data.Length-2],0x20); // SPACE
Assert.AreEqual(data[data.Length-1],0x0A); // EOL
return;
}
if(data[5]==0x31 && data[6]==0x2E && data[7]==0x34) // version is 1.4 ?
{
// file terminator
Assert.AreEqual(data[data.Length-6],0x25); // %
Assert.AreEqual(data[data.Length-5],0x25); // %
Assert.AreEqual(data[data.Length-4],0x45); // E
Assert.AreEqual(data[data.Length-3],0x4F); // O
Assert.AreEqual(data[data.Length-2],0x46); // F
Assert.AreEqual(data[data.Length-1],0x0A); // EOL
return;
}
Assert.Fail("Unsupported file format");
}
【讨论】:
您可以找出文件(或字节数组)的 mime 类型,因此您不会愚蠢地依赖扩展名。我是用光圈的 MimeExtractor (http://aperture.sourceforge.net/) 来做的,或者前几天我看到了一个专门用于此的库 (http://sourceforge.net/projects/mime-util)
我使用光圈从各种文件中提取文本,不仅是 pdf,而且还必须针对 pdf 进行调整(光圈使用 pdfbox,但我添加了另一个库作为 pdfbox 失败时的后备)
【讨论】:
由于您使用 PDFBox,您可以这样做:
PDDocument.load(file);
如果 PDF 损坏等,它将失败并出现异常。
如果成功,您还可以检查 PDF 是否使用 .isEncrypted() 加密
【讨论】:
这里是 NinjaCross 代码的改编 Java 版本。
/**
* Test if the data in the given byte array represents a PDF file.
*/
public static boolean is_pdf(byte[] data) {
if (data != null && data.length > 4 &&
data[0] == 0x25 && // %
data[1] == 0x50 && // P
data[2] == 0x44 && // D
data[3] == 0x46 && // F
data[4] == 0x2D) { // -
// version 1.3 file terminator
if (data[5] == 0x31 && data[6] == 0x2E && data[7] == 0x33 &&
data[data.length - 7] == 0x25 && // %
data[data.length - 6] == 0x25 && // %
data[data.length - 5] == 0x45 && // E
data[data.length - 4] == 0x4F && // O
data[data.length - 3] == 0x46 && // F
data[data.length - 2] == 0x20 && // SPACE
data[data.length - 1] == 0x0A) { // EOL
return true;
}
// version 1.3 file terminator
if (data[5] == 0x31 && data[6] == 0x2E && data[7] == 0x34 &&
data[data.length - 6] == 0x25 && // %
data[data.length - 5] == 0x25 && // %
data[data.length - 4] == 0x45 && // E
data[data.length - 3] == 0x4F && // O
data[data.length - 2] == 0x46 && // F
data[data.length - 1] == 0x0A) { // EOL
return true;
}
}
return false;
}
还有一些简单的单元测试:
@Test
public void test_valid_pdf_1_3_data_is_pdf() {
assertTrue(is_pdf("%PDF-1.3 CONTENT %%EOF \n".getBytes()));
}
@Test
public void test_valid_pdf_1_4_data_is_pdf() {
assertTrue(is_pdf("%PDF-1.4 CONTENT %%EOF\n".getBytes()));
}
@Test
public void test_invalid_data_is_not_pdf() {
assertFalse(is_pdf("Hello World".getBytes()));
}
如果您提出任何失败的单元测试,请告诉我。
【讨论】:
我正在使用我在此处和其他网站/帖子上找到的一些建议来确定 pdf 是否有效。我故意损坏了一个 pdf 文件,不幸的是,许多解决方案都没有检测到该文件已损坏。
最终,在修改了 API 中的不同方法后,我尝试了这个:
PDDocument.load(file).getPage(0).getContents().toString();
这并没有抛出异常,但它确实输出了这个:
WARN [COSParser:1154] The end of the stream doesn't point to the correct offset, using workaround to read the stream, stream start position: 171, length: 1145844, expected end position: 1146015
就个人而言,我希望在文件损坏时引发异常,以便我自己处理它,但似乎我正在实现的 API 已经以自己的方式处理它们。
为了解决这个问题,我决定尝试使用提供暖语句的类 (COSParser) 来解析文件。我发现有一个子类,叫PDFParser,它继承了一个叫“setLenient”的方法,这就是关键(https://pdfbox.apache.org/docs/2.0.4/javadocs/org/apache/pdfbox/pdfparser/COSParser.html)。
然后我实现了以下内容:
RandomAccessFile accessFile = new RandomAccessFile(file, "r");
PDFParser parser = new PDFParser(accessFile);
parser.setLenient(false);
parser.parse();
这如我所愿,为我损坏的文件引发了异常。希望这可以帮助某人!
【讨论】:
Pdf 文件以“%PDF”开头(在 TextPad 或类似工具中打开一个并查看)
您有什么理由不能只使用 StringReader 读取文件并进行检查?
【讨论】:
你必须试试这个......
public boolean isPDF(File file){
file = new File("Demo.pdf");
Scanner input = new Scanner(new FileReader(file));
while (input.hasNextLine()) {
final String checkline = input.nextLine();
if(checkline.contains("%PDF-")) {
// a match!
return true;
}
}
return false;
}
【讨论】:
也许我来不及回答。但是你应该看看 Tika。它在内部使用 PDFBox Parser 来解析 PDF
你只需要导入 tika-app-latest*.jar
public String parseToStringExample() throws IOException, SAXException, TikaException
{
Tika tika = new Tika();
try (InputStream stream = ParsingExample.class.getResourceAsStream("test.pdf")) {
return tika.parseToString(stream); // This should return you the pdf's text
}
}
这将是一个更清洁的解决方案。您可以参考这里了解更多关于 Tika 用法的详细信息:https://tika.apache.org/1.12/api/
【讨论】:
Roger Keays 的答案是错误的!因为并非 1.3 版中的所有 PDF 文件都被 EOL 终止。 以下答案适用于所有未损坏的 pdf 文件:
public static boolean is_pdf(byte[] data) {
if (data != null && data.length > 4
&& data[0] == 0x25 && // %
data[1] == 0x50 && // P
data[2] == 0x44 && // D
data[3] == 0x46 && // F
data[4] == 0x2D) { // -
// version 1.3 file terminator
if (//data[5] == 0x31 && data[6] == 0x2E && data[7] == 0x33 &&
data[data.length - 7] == 0x25 && // %
data[data.length - 6] == 0x25 && // %
data[data.length - 5] == 0x45 && // E
data[data.length - 4] == 0x4F && // O
data[data.length - 3] == 0x46 && // F
data[data.length - 2] == 0x20 // SPACE
//&& data[data.length - 1] == 0x0A// EOL
) {
return true;
}
// version 1.3 file terminator
if (//data[5] == 0x31 && data[6] == 0x2E && data[7] == 0x34 &&
data[data.length - 6] == 0x25 && // %
data[data.length - 5] == 0x25 && // %
data[data.length - 4] == 0x45 && // E
data[data.length - 3] == 0x4F && // O
data[data.length - 2] == 0x46 // F
//&& data[data.length - 1] == 0x0A // EOL
) {
return true;
}
}
return false;
}
【讨论】:
%%EOF 必须是 PDF 最后一行的唯一内容。因此,严格来说,%%EOF 后面有空格的文件是无效的。其后只能有一个行分隔符,即单个 CR、单个 LF 或 CR LF 对。
一般来说,我们可以喜欢这样,任何以 %%EOF 结尾的 pdf 版本,所以我们可以像下面这样检查。
public static boolean is_pdf(byte[] data) {
String s = new String(data);
String d = s.substring(data.length - 7, data.length - 1);
if (data != null && data.length > 4 &&
data[0] == 0x25 && // %
data[1] == 0x50 && // P
data[2] == 0x44 && // D
data[3] == 0x46 && // F
data[4] == 0x2D) { // -
if(d.contains("%%EOF")){
return true;
}
}
return false;
}
【讨论】:
这是一种检查%%EOF 是否存在的方法,并可选择检查空白字符。您可以传入 File 或 byte[] 对象。在某些 PDF 版本中,对空白字符的限制较少。
public boolean isPdf(byte[] data) {
if (data == null || data.length < 5) return false;
// %PDF-
if (data[0] == 0x25 && data[1] == 0x50 && data[2] == 0x44 && data[3] == 0x46 && data[4] == 0x2D) {
int offset = data.length - 8, count = 0; // check last 8 bytes for %%EOF with optional white-space
boolean hasSpace = false, hasCr = false, hasLf = false;
while (offset < data.length) {
if (count == 0 && data[offset] == 0x25) count++; // %
if (count == 1 && data[offset] == 0x25) count++; // %
if (count == 2 && data[offset] == 0x45) count++; // E
if (count == 3 && data[offset] == 0x4F) count++; // O
if (count == 4 && data[offset] == 0x46) count++; // F
// Optional flags for meta info
if (count == 5 && data[offset] == 0x20) hasSpace = true; // SPACE
if (count == 5 && data[offset] == 0x0D) hasCr = true; // CR
if (count == 5 && data[offset] == 0x0A) hasLf = true; // LF / EOL
offset++;
}
if (count == 5) {
String version = data.length > 13 ? String.format("%s%s%s", (char) data[5], (char) data[6], (char) data[7]) : "?";
System.out.printf("Version : %s | Space : %b | CR : %b | LF : %b%n", version, hasSpace, hasCr, hasLf);
return true;
}
}
return false;
}
public boolean isPdf(File file) throws IOException {
return isPdf(file, false);
}
// With version: 16 bytes, without version: 13 bytes.
public boolean isPdf(File file, boolean includeVersion) throws IOException {
if (file == null) return false;
int offsetStart = includeVersion ? 8 : 5, offsetEnd = 8;
byte[] bytes = new byte[offsetStart + offsetEnd];
InputStream is = new FileInputStream(file);
try {
is.read(bytes, 0, offsetStart); // %PDF-
is.skip(file.length() - bytes.length); // Skip bytes
is.read(bytes, offsetStart, offsetEnd); // %%EOF,SP?,CR?,LF?
} finally {
is.close();
}
return isPdf(bytes);
}
【讨论】:
x.y 更改的版本可能存在问题。更安全的检查是查看百分比符号,例如%xx.yyy%.
依靠幻数对我来说并没有真正的吸引力。我最终为此使用了 Apache 的预检库:
编译组:'org.apache.pdfbox',名称:'preflight',版本: '2.0.19'
private boolean isPdf(InputStream fileInputStream) {
try {
PreflightParser preflightParser = new PreflightParser(new ByteArrayDataSource(fileInputStream));
preflightParser.parse();
return true;
} catch (Exception e) {
return false;
}
}
PreflightParser 具有文件和其他数据源的构造函数。
【讨论】:
有一个非常方便和简单的库用于测试 PDF 内容: https://github.com/codeborne/pdf-test
API 非常简单:
import com.codeborne.pdftest.PDF;
import static com.codeborne.pdftest.PDF.*;
import static org.junit.Assert.assertThat;
public class PDFContainsTextTest {
@Test
public void canAssertThatPdfContainsText() {
PDF pdf = new PDF(new File("src/test/resources/50quickideas.pdf"));
assertThat(pdf, containsText("50 Quick Ideas to Improve your User Stories"));
}
}
【讨论】: