From 45dbff64017eb3f7c218a9aac000cb15e035bf80 Mon Sep 17 00:00:00 2001 From: "James R. Barlow" Date: Mon, 26 Mar 2018 02:23:19 -0700 Subject: [PATCH] Fix table of contents not preserved in PDF/A --- src/ocrmypdf/pdfinfo.py | 9 +++++++-- src/ocrmypdf/pipeline.py | 5 ++++- tests/resources/README.rst | 10 +++++----- tests/resources/toc.pdf | Bin 0 -> 9698 bytes tests/test_metadata.py | 22 +++++++++++++++++++++- 5 files changed, 37 insertions(+), 9 deletions(-) create mode 100644 tests/resources/toc.pdf diff --git a/src/ocrmypdf/pdfinfo.py b/src/ocrmypdf/pdfinfo.py index 08931d2a..8f5ff932 100644 --- a/src/ocrmypdf/pdfinfo.py +++ b/src/ocrmypdf/pdfinfo.py @@ -29,7 +29,7 @@ from enum import Enum from contextlib import contextmanager import PyPDF2 as pypdf -from fitz import Document +import fitz from .helpers import universal_open @@ -555,7 +555,7 @@ def borrow_stream(stream): def _page_has_text(infile, pageno): - doc = Document(infile) + doc = fitz.Document(infile) text = doc.getPageText(pageno) if text.strip() != '': return True @@ -692,6 +692,7 @@ class PdfInfo: def __init__(self, infile): self._infile = infile self._pages = _pdf_get_all_pageinfo(infile) + self._toc = fitz.Document(infile).getToC() @property def pages(self): @@ -712,6 +713,10 @@ class PdfInfo: raise NotImplementedError("can't get filename from stream") return self._infile + @property + def table_of_contents(self): + return self._toc + def __getitem__(self, item): return self._pages[item] diff --git a/src/ocrmypdf/pipeline.py b/src/ocrmypdf/pipeline.py index 32b742a9..9ebb0b3a 100644 --- a/src/ocrmypdf/pipeline.py +++ b/src/ocrmypdf/pipeline.py @@ -963,11 +963,14 @@ def merge_pages_ghostscript( ghostscript.generate_pdfa( pdf_version=input_pdfinfo.min_version, pdf_pages=pdf_pages, - output_file=output_file, + output_file=output_file + '_toc.pdf', compression=options.pdfa_image_compression, log=log, threads=options.jobs or 1, pdfa_part=('1' if options.output_type == 'pdfa-1' else '2')) + doc = fitz.Document(output_file + '_toc.pdf') + doc.setToC(input_pdfinfo.table_of_contents) + doc.save(output_file) def merge_pages_qpdf( diff --git a/tests/resources/README.rst b/tests/resources/README.rst index c3956acd..769f10d3 100644 --- a/tests/resources/README.rst +++ b/tests/resources/README.rst @@ -128,6 +128,7 @@ Assemblies These test resources are assemblies or derivatives from other previously mentioned files, released under the same license terms as their input files. +- baiona_gray.png (from baiona.png) - cardinal.pdf (four cardinal directions, baked-in rotated copies of LinnSequencer.jpg) - ccitt.pdf (LinnSequencer.jpg, converted to CCITT encoding) - encrypted_algo4.pdf (congress.jpg, encrypted with algorithm 4 - not supported by PyPDF2) @@ -135,12 +136,11 @@ These test resources are assemblies or derivatives from other previously mention - jbig2.pdf (congress.jpg, converted to JBIG2 encoding) - multipage.pdf (from several other files) - palette.pdf (congress.jpg, converted to a 256-color palette) -- rotated_skew.pdf (a /Rotate'd and skewed document from LinnSequencer.jpg) -- skew.pdf (from LinnSequencer.jpg, skew simulated by adjusting the transformation matrix) -- skew-encrypted.pdf (skew.pdf with encryption - access supported by PyPDF2, password is "password") -- baiona_gray.png (from baiona.png) - poster.pdf (from LinnSequencer.jpg) - +- rotated_skew.pdf (a /Rotate'd and skewed document from LinnSequencer.jpg) +- skew-encrypted.pdf (skew.pdf with encryption - access supported by PyPDF2, password is "password") +- skew.pdf (from LinnSequencer.jpg, skew simulated by adjusting the transformation matrix) +- toc.pdf (from formxobject.pdf, trivial.pdf) .. _`Wikimedia: LinnSequencer`: https://upload.wikimedia.org/wikipedia/en/b/b7/LinnSequencer_hardware_MIDI_sequencer_brochure_page_2_300dpi.jpg diff --git a/tests/resources/toc.pdf b/tests/resources/toc.pdf new file mode 100644 index 0000000000000000000000000000000000000000..6f710c780823eb14d242c308e07a128ae4323f8a GIT binary patch literal 9698 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 XtlHV}f?NCGd|+W|V8#ocYUKq0dhfUt literal 0 HcmV?d00001 diff --git a/tests/test_metadata.py b/tests/test_metadata.py index fdb301c3..02b632b7 100644 --- a/tests/test_metadata.py +++ b/tests/test_metadata.py @@ -18,6 +18,7 @@ import pytest import PyPDF2 as pypdf +import fitz from ocrmypdf.pdfa import file_claims_pdfa from ocrmypdf.exceptions import ExitCode @@ -93,4 +94,23 @@ def test_high_unicode(spoof_tesseract_noop, resources, no_outpdf): '--output-type', 'pdfa', env=spoof_tesseract_noop) - assert p.returncode == ExitCode.bad_args, err \ No newline at end of file + assert p.returncode == ExitCode.bad_args, err + + +@pytest.mark.parametrize('ocr_option', ['--skip-text', '--force-ocr']) +@pytest.mark.parametrize('output_type', ['pdf', 'pdfa']) +def test_bookmarks_preserved(spoof_tesseract_noop, output_type, ocr_option, + resources, outpdf): + input_file = resources / 'toc.pdf' + before_toc = fitz.Document(str(input_file)).getToC() + + check_ocrmypdf( + input_file, outpdf, + ocr_option, + '--output-type', output_type, + env=spoof_tesseract_noop) + + after_toc = fitz.Document(str(outpdf)).getToC() + print(before_toc) + print(after_toc) + assert before_toc == after_toc