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:
Vendored
+6
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 (�).
|
||||
# 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,
|
||||
|
||||
@@ -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.
@@ -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'�', xmp_start, xmp_end) == -1, "found escaped nul"
|
||||
assert mm.find(b'\x00', xmp_start, xmp_end) == -1
|
||||
|
||||
Reference in New Issue
Block a user