【问题标题】:How can I determine if a file is a PDF file?如何确定文件是否为 PDF 文件?
【发布时间】:2010-10-30 19:40:54
【问题描述】:

我在 Java 中使用 PdfBox 从 PDF 文件中提取文本。提供的某些输入文件无效,PDFTextStripper 在这些文件上停止。是否有一种干净的方法来检查提供的文件是否确实是有效的 PDF?

【问题讨论】:

  • 我的王国??任何链接或说明?

标签: java validation pdf text


【解决方案1】:

这是我在 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");
    }

【讨论】:

  • 谢谢,这只是帮助我弄清楚我生成的 PDF 出了什么问题 - EOL 问题仅显示在 Adob​​e Reader 中,而不是 Foxit/GoogleApps/Sumatra 中。
  • 这是在 Java 中吗?它也不会检测到加密的 PDF。由于 OP 想要提取信息,因此您也需要它。
  • 谢谢!我真的很感激这个答案与图书馆无关。它为我节省了很多时间 =)
  • 现在(在原始答案发布后差不多十年)我们有更多的 pdf 版本,所以如果你打算只是复制和粘贴上面的代码,请小心!
  • @1_bug 你伏笔!我对 1.6 格式有问题,现在,只需检查“25 50 44 46 2D”组!
【解决方案2】:

您可以找出文件(或字节数组)的 mime 类型,因此您不会愚蠢地依赖扩展名。我是用光圈的 MimeExtractor (http://aperture.sourceforge.net/) 来做的,或者前几天我看到了一个专门用于此的库 (http://sourceforge.net/projects/mime-util)

我使用光圈从各种文件中提取文本,不仅是 pdf,而且还必须针对 pdf 进行调整(光圈使用 pdfbox,但我添加了另一个库作为 pdfbox 失败时的后备)

【讨论】:

  • 哦,我忘了说现在有一个用于文本提取的 apache 项目,lucene.apache.org/tika,如果你更喜欢它而不是孔径
  • 正确阅读问题:问题不是关于使用 PDFBox,而是关于“检查提供的文件是否确实是有效的 PDF”
  • 我在问题标题中看到“使用 Apache 的 PdfBox”。如果使用 PDFBox 可以解决问题,难道不比引入额外的依赖项更好吗?
【解决方案3】:

由于您使用 PDFBox,您可以这样做:

PDDocument.load(file);

如果 PDF 损坏等,它将失败并出现异常。

如果成功,您还可以检查 PDF 是否使用 .isEncrypted() 加密

【讨论】:

  • 据我所见,这不是真的。我可以使用 PDDocument.load(stream) 加载损坏的 PDF。仅在修改权限后尝试保存 PDF 时出现错误。
  • 对应用程序流使用异常是不好的做法。
  • @BenTurner:你是对的,我支持你。不过,API 并没有给我们提供检查文件有效性的方法。
  • 这并不总是抛出异常。 stackoverflow.com/questions/20004290/…
  • PDDocument.load(file).getNumberOfPages() 怎么样?这就是我所做的,我还没有遇到过 PDFBox 可以计算页数的无效 PDF 文件。
【解决方案4】:

这里是 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()));
}

如果您提出任何失败的单元测试,请告诉我。

【讨论】:

    【解决方案5】:

    我正在使用我在此处和其他网站/帖子上找到的一些建议来确定 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();
    

    这如我所愿,为我损坏的文件引发了异常。希望这可以帮助某人!

    【讨论】:

      【解决方案6】:

      Pdf 文件以“%PDF”开头(在 TextPad 或类似工具中打开一个并查看)

      您有什么理由不能只使用 StringReader 读取文件并进行检查?

      【讨论】:

      • 我已经尝试过了,似乎 PDF 文件可以使用多种编码,并且读取的文本有时与有效且可读的 PDF 文件的 %PDF 不匹配。
      • 并非所有以 %PDF 开头的文件都是有效的 PDF 文件。
      【解决方案7】:

      你必须试试这个......

      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;
      }
      

      【讨论】:

      • 这个答案让我很困扰......是否存在不以“%PDF-”开头但仅包含它的 PDF?为什么读取整个文件的麻烦?如果我检查一个 2 GB 的 zip 文件怎么办?
      • 对于较大的文件,大小为 10+MB 且扩展名错误的文件(例如 mp3File.pdf),会花费很多时间(例如 5 秒或更多)
      【解决方案8】:

      也许我来不及回答。但是你应该看看 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/

      【讨论】:

        【解决方案9】:

        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 对。
        【解决方案10】:

        一般来说,我们可以喜欢这样,任何以 %%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;
            }
        

        【讨论】:

          【解决方案11】:

          这是一种检查%%EOF 是否存在的方法,并可选择检查空白字符。您可以传入 Filebyte[] 对象。在某些 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);
          }
          

          【讨论】:

          • ISO 32000-2 已经发布了很长一段时间。所以...“我还定制了这个来检查 PDF 版本 1.3 和 1.7” - 你也应该允许 2.0。
          • @mkl 我删除了版本检查。显示格式从x.y 更改的版本可能存在问题。更安全的检查是查看百分比符号,例如%xx.yyy%.
          【解决方案12】:

          依靠幻数对我来说并没有真正的吸引力。我最终为此使用了 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 具有文件和其他数据源的构造函数。

          【讨论】:

            【解决方案13】:

            有一个非常方便和简单的库用于测试 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"));
              }
            }
            

            【讨论】:

            • 为什么投反对票?这确实回答了这个问题。也许这个解决方案不如其他答案那么强大,但其他答案应该得到更多的支持,不是吗?
            猜你喜欢
            • 1970-01-01
            • 2021-12-17
            • 2014-12-26
            • 1970-01-01
            • 2019-09-26
            • 1970-01-01
            • 2021-02-15
            • 2012-11-05
            • 2012-03-10
            相关资源
            最近更新 更多