From d7a9f3a2ab4622410b50afb9b948a533e5f9d69e Mon Sep 17 00:00:00 2001 From: Jim Barlow Date: Sat, 25 Jul 2015 18:05:25 -0700 Subject: [PATCH] Transfer Unicode document information from input PDF to output PDF What a pain getting Unicode right, but there it is. I cannot find anything to confirm that it is acceptable to put the PDF/A definition file at the end of the Ghostscript inputs. I did this because Ghostscript seems to copy document info from the last document on the list so reportlab's information "wins" in normal order, so it fixes that issue, and reportlab 'helpfully' fills in all of those fields even if it does not have information. It could also work to pass document information along to reportlab, and set it in each output PDF: .debug.pdf, .rendered.pdf, and .page.pdf to ensure that whatever page is last in the pipeline has the right information. Or perhaps it's possible to write a Postscript trailer that overwrites any previous docinfo with no side effects, but I can't find any information on how to do that. I don't think it's worth pursuing unless this arrangement causes some problem with PDF/A generation. On a minor note, Jhove misreads the way I have encoded the strings in producing its validation log. It reads them as UTF-16 little endian, so will tend to produce a string of Asian characters in place of the real data. --- src/ocrmypdf.py | 36 +++++++++++++++++++++++------------- src/pdfa.py | 37 +++++++++++++++++++++++++++++-------- 2 files changed, 52 insertions(+), 21 deletions(-) diff --git a/src/ocrmypdf.py b/src/ocrmypdf.py index be5a35e1..048e6e7a 100755 --- a/src/ocrmypdf.py +++ b/src/ocrmypdf.py @@ -596,17 +596,24 @@ def generate_postscript_stub( input_file, output_file, log): - try: - pdf = pypdf.PdfFileReader(input_file) - pdfmark = { - 'title': pdf.documentInfo['/Title'], - 'author': pdf.documentInfo['/Author'], - 'keywords': pdf.documentInfo['/Keywords'], - 'subject': pdf.documentInfo['/Subject'] - } - except KeyError: - pdfmark = {} + pdf = pypdf.PdfFileReader(input_file) + + def from_document_info(key): + # pdf.documentInfo.get() DOES NOT work as expected + try: + s = pdf.documentInfo[key] + return str(s) + except KeyError: + return '' + + pdfmark = { + 'title': from_document_info('/Title'), + 'author': from_document_info('/Author'), + 'keywords': from_document_info('/Keywords'), + 'subject': from_document_info('/Subject'), + } + print(pdfmark) generate_pdfa_def(output_file, pdfmark) @@ -636,10 +643,13 @@ def merge_pages( pdfinfo_lock): def input_file_order(s): - '''Sort order: Postscript PDF/A header, and then pages followed - by their debug page, if any.''' + '''Sort order: All rendered pages followed + by their debug page, if any, followed by Postscript stub. + Ghostscript documentation has the Postscript stub at the + beginning, but it works at the end and also gets document info + right that way.''' if s.endswith('.ps'): - return -1 + return 99999999 key = int(os.path.basename(s)[0:6]) * 10 if 'debug' in os.path.basename(s): key += 1 diff --git a/src/pdfa.py b/src/pdfa.py index cdd56356..2c274336 100644 --- a/src/pdfa.py +++ b/src/pdfa.py @@ -9,6 +9,7 @@ from __future__ import print_function, absolute_import, division from string import Template from subprocess import Popen, PIPE import os +import codecs # This is a template written in PostScript which is needed to create PDF/A @@ -24,10 +25,10 @@ pdfa_def_template = u"""%! /ICCProfile ($icc_profile) def -[ /Title ($pdf_title) - /Author ($pdf_author) - /Subject ($pdf_subject) - /Keywords ($pdf_keywords) +[ /Title <$title> + /Author <$author> + /Subject <$subject> + /Keywords <$keywords> /DOCINFO pdfmark % Define an ICC profile : @@ -60,14 +61,34 @@ def """ +def encode_text_string(s: str) -> str: + '''Encode text string to hex string for use in a PDF + + From PDF 32000-1:2008 a string object may be included in hexademical form + if it is enclosed in angle brackets. For general Unicode the string should + be UTF-16 (big endian) with byte order marks. A non-hexademical + presentation is possible but this is preferable since it allows the output + Postscript file to be completely ASCII. + ''' + if s == '': + return '' + utf16_bytes = s.encode('utf-16be') + ascii_hex_bytes = codecs.encode(b'\xfe\xff' + utf16_bytes, 'hex') + ascii_hex_str = ascii_hex_bytes.decode('ascii').lower() + return ascii_hex_str + + def _get_pdfa_def(icc_profile, icc_identifier, pdfmark): + pdfmark_utf16 = {k: encode_text_string(v) for k, v in pdfmark.items()} + t = Template(pdfa_def_template) result = t.substitute(icc_profile=icc_profile, icc_identifier=icc_identifier, - pdf_title=pdfmark.get('title', ''), - pdf_author=pdfmark.get('author', ''), - pdf_subject=pdfmark.get('subject', ''), - pdf_keywords=pdfmark.get('keywords', '')) + title=pdfmark_utf16.get('title', ''), + author=pdfmark_utf16.get('author', ''), + subject=pdfmark_utf16.get('subject', ''), + keywords=pdfmark_utf16.get('keywords', '')) + print(result) return result