Replace Ghostscript DOCINFO and fix 9.25 metadata date regression
We no longer use Ghostscript to manage PDF metadata, instead omitting the DOCINFO segment from the pdfmark file we generate. Instead all of the relevant metadata code has been migrated to pikepdf, and we use that API. This should be more consistent and fixes the Ghostscript version-depedent quirks. Also removes our python-xmp-toolkit dependency, except for testing.
This commit is contained in:
+17
-26
@@ -795,7 +795,7 @@ def ocr_tesseract_textonly_pdf(
|
||||
log=log)
|
||||
|
||||
|
||||
def get_pdfmark(base_pdf, options):
|
||||
def get_docinfo(base_pdf, options):
|
||||
def from_document_info(key):
|
||||
try:
|
||||
s = base_pdf.metadata[key]
|
||||
@@ -840,21 +840,7 @@ def generate_postscript_stub(
|
||||
context):
|
||||
options = context.get_options()
|
||||
pdf = pikepdf.open(input_file)
|
||||
pdfmark = get_pdfmark(pdf, options)
|
||||
|
||||
ascii_docinfo = False
|
||||
if ghostscript.version() >= '9.24':
|
||||
ascii_docinfo = True
|
||||
try:
|
||||
for v in pdfmark.values():
|
||||
v.encode('ascii', errors='strict')
|
||||
except UnicodeEncodeError:
|
||||
log.warning(
|
||||
"Ghostscript 9.24+ does not support Unicode strings in "
|
||||
" metadata. These will be converted to ASCII if possible."
|
||||
)
|
||||
|
||||
generate_pdfa_ps(output_file, pdfmark, ascii_docinfo=ascii_docinfo)
|
||||
generate_pdfa_ps(output_file)
|
||||
|
||||
|
||||
def metadata_fixup(
|
||||
@@ -874,6 +860,8 @@ def metadata_fixup(
|
||||
ps = next(
|
||||
(ii for ii in input_files if ii.endswith('.ps')), None
|
||||
)
|
||||
metadata = pikepdf.open(metadata_file)
|
||||
docinfo = get_docinfo(metadata, options)
|
||||
|
||||
if options.output_type.startswith('pdfa'):
|
||||
input_pdfinfo = context.get_pdfinfo()
|
||||
@@ -886,18 +874,21 @@ def metadata_fixup(
|
||||
threads=options.jobs or 1,
|
||||
pdfa_part=options.output_type[-1] # is pdfa-1, pdfa-2, or pdfa-3
|
||||
)
|
||||
pdf = pikepdf.open(output_file)
|
||||
with pdf.open_metadata() as meta:
|
||||
# Note Ghostscript will populate xmp:CreateDate or /CreationDate
|
||||
meta.load_from_docinfo(docinfo, delete_missing=False)
|
||||
pdf.save(output_file)
|
||||
else:
|
||||
metadata = pikepdf.open(metadata_file)
|
||||
pdfmark = get_pdfmark(metadata, options)
|
||||
pdf = pikepdf.open(layers_file)
|
||||
pdf.metadata = pdf.make_indirect(pikepdf.Dictionary(pdfmark))
|
||||
try:
|
||||
pdf.save(output_file, compress_streams=True,
|
||||
object_stream_mode=pikepdf.ObjectStreamMode.generate)
|
||||
except AttributeError:
|
||||
# pikepdf <= 0.3.4
|
||||
pdf.save(output_file,
|
||||
stream_data_mode=pikepdf.StreamDataMode.compress)
|
||||
with pdf.open_metadata() as meta:
|
||||
meta.load_from_docinfo(docinfo, delete_missing=False)
|
||||
# If xmp:CreateDate is missing, set it to the modify date to
|
||||
# match Ghostscript, for consistency
|
||||
if 'xmp:CreateDate' not in meta:
|
||||
meta['xmp:CreateDate'] = meta.get('xmp:ModifyDate', '')
|
||||
pdf.save(output_file, compress_streams=True,
|
||||
object_stream_mode=pikepdf.ObjectStreamMode.generate)
|
||||
|
||||
|
||||
def optimize_pdf(
|
||||
|
||||
+6
-61
@@ -61,9 +61,6 @@ pdfa_def_template = u"""%!
|
||||
/ICCProfile $icc_profile
|
||||
def
|
||||
|
||||
[$docinfo
|
||||
/DOCINFO pdfmark
|
||||
|
||||
% Define an ICC profile :
|
||||
|
||||
[/_objdef {icc_PDFA} /type /stream /OBJ pdfmark
|
||||
@@ -94,6 +91,7 @@ def
|
||||
"""
|
||||
|
||||
|
||||
@deprecated
|
||||
def encode_text_string(s: str) -> str:
|
||||
"""
|
||||
Encode text string to hex string for use in a PDF
|
||||
@@ -149,35 +147,7 @@ def decode_pdf_date(*args, **kwargs):
|
||||
return _decode_date(*args, **kwargs)
|
||||
|
||||
|
||||
def _get_pdfmark_dates(pdfmark):
|
||||
"""
|
||||
Encode dates in the expected format for pdfmark Postscript
|
||||
|
||||
The best way to deal with amissing date entry is set it to null, because if
|
||||
the key is omitted Ghostscript will set it to now - we do not want to erase
|
||||
the fact that the value was unknown. Setting to an empty string breaks
|
||||
Ghostscript 9.22 as reported here:
|
||||
https://bugs.ghostscript.com/show_bug.cgi?id=699182
|
||||
"""
|
||||
|
||||
for key in ('/CreationDate', '/ModDate'):
|
||||
if key not in pdfmark:
|
||||
continue
|
||||
if pdfmark[key].strip() == '':
|
||||
yield ' {} null'.format(key)
|
||||
continue
|
||||
date_str = pdfmark[key]
|
||||
if date_str.startswith('D:'):
|
||||
date_str = date_str[2:]
|
||||
try:
|
||||
yield ' {} (D:{})'.format(
|
||||
key,
|
||||
_encode_date(_decode_date(date_str)))
|
||||
except ValueError:
|
||||
yield ' {} null'.format(key)
|
||||
|
||||
|
||||
def _get_pdfa_def(icc_profile, icc_identifier, pdfmark, ascii_docinfo=False):
|
||||
def _get_pdfa_def(icc_profile, icc_identifier, pdfmark=None, ascii_docinfo=None):
|
||||
"""
|
||||
Create a Postscript pdfmark file for Ghostscript.
|
||||
|
||||
@@ -187,43 +157,18 @@ def _get_pdfa_def(icc_profile, icc_identifier, pdfmark, ascii_docinfo=False):
|
||||
:param icc_profile: filename of the ICC profile to include in pdfmark
|
||||
:param icc_identifier: ICC identifier such as 'sRGB'
|
||||
:param pdfmark: a dictionary containing keys to include the pdfmark
|
||||
:param ascii_docinfo: if True, the docinfo block must be encoded in pure
|
||||
ASCII and may not contain UTF-16BE-BOM-hex encoded strings, as
|
||||
required for Ghostscript 9.24+
|
||||
:param ascii_docinfo: parameter is no longer meaningful
|
||||
|
||||
:returns: a string containing the entire pdfmark
|
||||
|
||||
"""
|
||||
|
||||
# Ghostscript <= 9.21 has a bug where null entries in DOCINFO might produce
|
||||
# ERROR: VMerror (-25) on closing pdfwrite device.
|
||||
# https://bugs.ghostscript.com/show_bug.cgi?id=697684
|
||||
# Work around this by only adding keys that have a nontrivial value
|
||||
docinfo_keys = ('/Title', '/Author', '/Subject', '/Creator', '/Keywords')
|
||||
|
||||
def docinfo_gen():
|
||||
if not ascii_docinfo:
|
||||
docinfo_line_template = ' {key} <{value}>'
|
||||
encode = encode_text_string
|
||||
else:
|
||||
docinfo_line_template = ' {key} ({value})'
|
||||
encode = _encode_ascii
|
||||
yield from _get_pdfmark_dates(pdfmark)
|
||||
for key in docinfo_keys:
|
||||
if key in pdfmark and pdfmark[key].strip() != '':
|
||||
line = docinfo_line_template.format(
|
||||
key=key, value=encode(pdfmark[key]))
|
||||
yield line
|
||||
docinfo = '\n'.join(docinfo_gen())
|
||||
|
||||
t = Template(pdfa_def_template)
|
||||
result = t.substitute(icc_profile=icc_profile,
|
||||
icc_identifier=icc_identifier,
|
||||
docinfo=docinfo)
|
||||
icc_identifier=icc_identifier)
|
||||
return result
|
||||
|
||||
|
||||
def generate_pdfa_ps(target_filename, pdfmark, icc='sRGB', ascii_docinfo=False):
|
||||
def generate_pdfa_ps(target_filename, pdfmark=None, icc='sRGB', ascii_docinfo=None):
|
||||
if icc == 'sRGB':
|
||||
icc_profile = SRGB_ICC_PROFILE
|
||||
else:
|
||||
@@ -240,7 +185,7 @@ def generate_pdfa_ps(target_filename, pdfmark, icc='sRGB', ascii_docinfo=False):
|
||||
hex_icc_profile = hexlify(bytes_icc_profile)
|
||||
icc_profile = '<' + hex_icc_profile.decode('ascii') + '>'
|
||||
|
||||
ps = _get_pdfa_def(icc_profile, icc, pdfmark, ascii_docinfo=ascii_docinfo)
|
||||
ps = _get_pdfa_def(icc_profile, icc, pdfmark)
|
||||
|
||||
# We should have encoded everything to pure ASCII by this point, and
|
||||
# to be safe, only allow ASCII in PostScript
|
||||
|
||||
Reference in New Issue
Block a user