Prevent Ghostscript from generating invalid XMP metadata

If DocumentInfo contains NULs Ghostscript will generate XMP with
NULs which is not allowed. Repair DocumentInfo before Ghostscript sees it.
This commit is contained in:
James R. Barlow
2019-01-04 13:20:41 -08:00
parent 089ece2715
commit f34b3015b2
7 changed files with 71 additions and 2 deletions
+6
View File
@@ -95,6 +95,12 @@ Files: tests/resources/vector.pdf
Copyright: (C) 2018 Catscratch
License: Expat
Files: test/resources/enron1.pdf
Copyright: EnronData.org
License: CC-BY-3.0
See: https://enrondata.readthedocs.io/en/latest/data/edo-enron-email-pst-dataset/
Comment: Unprocessed.
Files: src/ocrmypdf/data/sRGB.icc
Copyright: Kai-Uwe Behrmann <www.behrmann.name>
Marti Maria <www.littlecms.com>
+1 -1
View File
@@ -5,7 +5,7 @@ chardet == 3.0.4
cffi == 1.11.5
img2pdf == 0.3.1
pdfminer.six == 20181108
pikepdf == 0.10.2
pikepdf == 1.0.1
Pillow >= 5.0.0, != 5.1.0 ; sys_platform == "darwin"
pycparser == 2.19
python-xmp-toolkit == 2.0.1
+1 -1
View File
@@ -251,7 +251,7 @@ setup(
'cffi >= 1.9.1', # must be a setup and install requirement
'img2pdf >= 0.3.0, < 0.4', # pure Python, so track HEAD closely
'pdfminer.six == 20181108 ; sys_platform != "darwin"',
'pikepdf >= 0.10.2, < 0.11.0',
'pikepdf >= 1.0.1, < 2',
'Pillow >= 4.0.0, != 5.1.0 ; sys_platform == "darwin"',
# Pillow < 4 has BytesIO/TIFF bug w/img2pdf 0.2.3
# block 5.1.0, broken wheels
+17
View File
@@ -810,6 +810,23 @@ def convert_to_pdfa(input_files_groups, output_file, log, context):
layers_file = next(
(ii for ii in input_files if ii.endswith('layers.rendered.pdf')), None
)
# If the DocumentInfo record contains NUL characters, Ghostscript will
# produce XMP metadata which contains invalid XML entities (&#0;).
# NULs in DocumentInfo seem to be common since older Acrobats included them.
# pikepdf can deal with this, but we make the world a better place by
# stamping them out as soon as possible.
pdf_layers_file = pikepdf.open(layers_file)
if pdf_layers_file.docinfo:
modified = False
for k, v in pdf_layers_file.docinfo.items():
if b'\x00' in bytes(v):
pdf_layers_file.docinfo[k] = bytes(v).replace(b'\x00', b'')
modified = True
if modified:
pdf_layers_file.save(layers_file)
del pdf_layers_file
ps = next((ii for ii in input_files if ii.endswith('.ps')), None)
ghostscript.generate_pdfa(
pdf_version=input_pdfinfo.min_version,
+3
View File
@@ -37,6 +37,9 @@ In some cases they were converted from one image format to another without other
* - baiona.png
- `Wikimedia: Baionako udalerri mugakideak`_
- Creative Commons BY-SA 4.0
* - enron1.pdf
- EnronData.org
- Creative Commons BY 3.0
Files generated for this project
Binary file not shown.
+43
View File
@@ -18,6 +18,8 @@
import datetime
from datetime import timezone
import logging
import mmap
from os import fspath
from pathlib import Path
from shutil import copyfile
@@ -26,6 +28,7 @@ from unittest.mock import MagicMock, patch
import pytest
import pikepdf
from ocrmypdf._jobcontext import JobContext
from ocrmypdf.exceptions import ExitCode
from ocrmypdf.pdfa import SRGB_ICC_PROFILE, file_claims_pdfa, generate_pdfa_ps
from pikepdf.models.metadata import decode_pdf_date
@@ -319,3 +322,43 @@ def test_metadata_fixup_warning(resources, outdir):
context=context,
)
log.warning.assert_called_once()
def test_prevent_gs_invalid_xml(resources, outdir):
from ocrmypdf.__main__ import parser
from ocrmypdf._pipeline import convert_to_pdfa
from ocrmypdf.pdfa import generate_pdfa_ps
from ocrmypdf.pdfinfo import PdfInfo
generate_pdfa_ps(outdir / 'pdfa.ps')
input_files = [
str(outdir / 'layers.rendered.pdf'),
str(outdir / 'pdfa.ps'),
]
copyfile(resources / 'enron1.pdf', outdir / 'layers.rendered.pdf')
log = logging.getLogger()
context = JobContext()
options = parser.parse_args(args=[
'-j', '1', '--output-type', 'pdfa-2', 'a.pdf', 'b.pdf']
)
context.options = options
context.pdfinfo = PdfInfo(resources / 'enron1.pdf')
convert_to_pdfa(
input_files_groups=input_files,
output_file=outdir / 'pdfa.pdf',
log=log,
context=context
)
with open(outdir / 'pdfa.pdf', 'rb') as f:
with mmap.mmap(f.fileno(), 0, flags=mmap.MAP_PRIVATE, prot=mmap.PROT_READ) as mm:
# Since the XML may be invalid, we scan instead of actually feeding it
# to a parser.
XMP_MAGIC = b'W5M0MpCehiHzreSzNTczkc9d'
xmp_start = mm.find(XMP_MAGIC)
xmp_end = mm.rfind(b'<?xpacket end', xmp_start)
assert 0 < xmp_start < xmp_end
assert mm.find(b'&#0;', xmp_start, xmp_end) == -1, "found escaped nul"
assert mm.find(b'\x00', xmp_start, xmp_end) == -1