From 12575d594a0fa9468a892feb52473a0cd588d5e7 Mon Sep 17 00:00:00 2001 From: "James R. Barlow" Date: Wed, 3 Aug 2016 01:26:16 -0700 Subject: [PATCH] Improve PDF/A validity checking at end --- ocrmypdf/__init__.py | 2 +- ocrmypdf/main.py | 30 ++++++++++++++++++------------ ocrmypdf/pdfa.py | 37 +++++++++++++++++++++++++++++++++++++ 3 files changed, 56 insertions(+), 13 deletions(-) diff --git a/ocrmypdf/__init__.py b/ocrmypdf/__init__.py index 515709b1..31cbe937 100644 --- a/ocrmypdf/__init__.py +++ b/ocrmypdf/__init__.py @@ -8,7 +8,7 @@ class ExitCode(IntEnum): bad_args = 1 input_file = 2 missing_dependency = 3 - invalid_output_pdfa = 4 + invalid_output_pdf = 4 file_access_error = 5 already_done_ocr = 6 child_process_error = 7 diff --git a/ocrmypdf/main.py b/ocrmypdf/main.py index cb035a9e..c2d83e74 100755 --- a/ocrmypdf/main.py +++ b/ocrmypdf/main.py @@ -27,7 +27,7 @@ import ruffus.proxy_logger as proxy_logger from .hocrtransform import HocrTransform from .pageinfo import pdf_get_all_pageinfo -from .pdfa import generate_pdfa_def +from .pdfa import generate_pdfa_def, file_claims_pdfa from . import ghostscript from . import tesseract from . import qpdf @@ -127,8 +127,12 @@ parser.add_argument( '--image-dpi', metavar='DPI', type=int, help="for input image instead of PDF, use this DPI instead of file's") parser.add_argument( - '--output-type', choices=['pdfa', 'pdf'], default='pdf', - help="choose output type") + '--output-type', choices=['pdfa', 'pdf'], default='pdfa', + help="Choose output type. 'pdfa' creates a PDF/A-2b compliant file for " + "long term archiving (default, recommended) but may not suitable " + "for users who want their file altered as little as possible. 'pdfa' " + "also has problems with full Unicode text. 'pdf' attempts to " + "preserve file contents as much as possible.") metadata = parser.add_argument_group( "Metadata options", @@ -1220,12 +1224,6 @@ def copy_final( shutil.copy(input_file, output_file) -def validate_pdfa( - input_file, - log): - return qpdf.check(input_file, log) - - def available_cpu_count(): try: return multiprocessing.cpu_count() @@ -1360,9 +1358,17 @@ def run_pipeline(): _log.error(e) return ExitCode.other_error - if not validate_pdfa(options.output_file, _log): - _log.warning('Output file: The generated PDF/A file is INVALID') - return ExitCode.invalid_output_pdfa + if options.output_type == 'pdfa': + pdfa_info = file_claims_pdfa(options.output_file) + if pdfa_info['pass']: + _log.info(pdfa_info['message']) + else: + _log.warning(pdfa_info['message']) + return ExitCode.invalid_output_pdf + + if not qpdf.check(options.output_file, _log): + _log.warning('Output file: The generated PDF is INVALID') + return ExitCode.invalid_output_pdf with _pdfinfo_lock: _log.debug(_pdfinfo) diff --git a/ocrmypdf/pdfa.py b/ocrmypdf/pdfa.py index 5779e106..7985a5ba 100644 --- a/ocrmypdf/pdfa.py +++ b/ocrmypdf/pdfa.py @@ -7,6 +7,7 @@ from __future__ import print_function, absolute_import, division from string import Template import codecs import pkg_resources +import PyPDF2 as pypdf ICC_PROFILE_RELPATH = 'data/sRGB.icc' @@ -108,3 +109,39 @@ def generate_pdfa_def(target_filename, pdfmark, icc='sRGB'): # answer), insist on ascii with open(target_filename, 'w', encoding='ascii') as f: f.write(ps) + + +def file_claims_pdfa(filename): + """Determines if the file claims to be PDF/A compliant + + Checking if a file is a truly compliant PDF/A is a massive undertaking + that no open source tool does properly. Some commercial tools are + generally reliable (Acrobat). + + This checks if the XMP metadata contains a PDF/A marker. + """ + + pdf = pypdf.PdfFileReader(filename) + xmp = pdf.getXmpMetadata() + + pdfa_nodes = xmp.getNodesInNamespace( + aboutUri='', + namespace='http://www.aiim.org/pdfa/ns/id/') + + pdfa_dict = {attr.localName: attr.value for attr in pdfa_nodes} + pdfa_dict['pass'] = False + if pdfa_dict: + part_conformance = pdfa_dict['part'] + pdfa_dict['conformance'] + valid_part_conforms = {'1A', '1B', '2A', '2B', '2U', '3A', '3B', '3U'} + + message = 'File claims to be PDF/A-{}'.format( + part_conformance) + + if part_conformance in valid_part_conforms: + pdfa_dict['pass'] = True + pdfa_dict['message'] = message + else: + pdfa_dict['message'] = 'File is a regular PDF' + + return pdfa_dict +