From 050dd1f5a8d3c4141ac99cb917f84bdb5f2e449a Mon Sep 17 00:00:00 2001 From: f-hansen <53111364+f-hansen@users.noreply.github.com> Date: Tue, 20 Jun 2023 04:07:23 -0400 Subject: [PATCH] Allow title, subject, author, and keywords to be unset with an empty string argument (#1117) Co-authored-by: Frederick D. Hansen --- src/ocrmypdf/_pipeline.py | 37 +++++++++--- tests/resources/meta.pdf | 115 ++++++++++++++++++++++++++++++++++++++ tests/test_metadata.py | 44 +++++++++++++++ 3 files changed, 187 insertions(+), 9 deletions(-) create mode 100644 tests/resources/meta.pdf diff --git a/src/ocrmypdf/_pipeline.py b/src/ocrmypdf/_pipeline.py index 559aab89..b25b9fad 100644 --- a/src/ocrmypdf/_pipeline.py +++ b/src/ocrmypdf/_pipeline.py @@ -852,24 +852,43 @@ def metadata_fixup(working_file: Path, context: PdfContext) -> Path: with pikepdf.open(context.origin) as original, pikepdf.open(working_file) as pdf: docinfo = get_docinfo(original, context) - with pdf.open_metadata() as meta: - meta.load_from_docinfo(docinfo, delete_missing=False, raise_failure=False) + with pdf.open_metadata() as meta_pdf: + meta_pdf.load_from_docinfo(docinfo, delete_missing=False, raise_failure=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', '') + # ensure consistency with Ghostscript. + if 'xmp:CreateDate' not in meta_pdf: + meta_pdf['xmp:CreateDate'] = meta_pdf.get('xmp:ModifyDate', '') with original.open_metadata( set_pikepdf_as_editor=False, update_docinfo=False, strict=False ) as meta_original: - if meta.get('dc:title') == 'Untitled': + if meta_pdf.get('dc:title') == 'Untitled': # Ghostscript likes to set title to Untitled if omitted from input. # Reverse this, because PDF/A TechNote 0003:Metadata in PDF/A-1 # and the XMP Spec do not make this recommendation. if 'dc:title' not in meta_original: - del meta['dc:title'] - missing = set(meta_original.keys()) - set(meta.keys()) - report_on_metadata(missing) + del meta_pdf['dc:title'] + # If the user explicitly specified an empty string for any of the + # following, they should be unset and not reported as missing in + # the output pdf. Note that some metadata fields use differing names + # between PDF-A and PDF. + for meta in [meta_pdf, meta_original]: + if options.title == '' and 'dc:title' in meta: + del meta['dc:title'] # PDF-A and PDF + if options.author == '': + if 'dc:creator' in meta: + del meta['dc:creator'] # PDF-A (Not xmp:CreatorTool) + if 'pdf:Author' in meta: + del meta['pdf:Author'] # PDF + if options.subject == '': + if 'dc:description' in meta: + del meta['dc:description'] # PDF-A + if 'dc:subject' in meta: + del meta['dc:subject'] # PDF + if options.keywords == '' and 'pdf:Keywords' in meta: + del meta['pdf:Keywords'] # PDF-A and PDF + meta_missing = set(meta_original.keys()) - set(meta_pdf.keys()) + report_on_metadata(meta_missing) optimizing = context.plugin_manager.hook.is_optimization_enabled( context=context diff --git a/tests/resources/meta.pdf b/tests/resources/meta.pdf new file mode 100644 index 00000000..db3bdffe --- /dev/null +++ b/tests/resources/meta.pdf @@ -0,0 +1,115 @@ +%PDF-1.3 +%¿÷¢þ +1 0 obj +<< /Pages 2 0 R /Type /Catalog >> +endobj +2 0 obj +<< /Count 1 /Kids [ 3 0 R ] /Type /Pages >> +endobj +3 0 obj +<< /MediaBox [ 0 0 720 720 ] /Parent 2 0 R /Resources << >> /Type /Page >> +endobj +xref +0 4 +0000000000 65535 f +0000000015 00000 n +0000000064 00000 n +0000000123 00000 n +trailer << /Root 1 0 R /Size 4 /ID [<52bba3c78160d0c6e851b59110e5d076><52bba3c78160d0c6e851b59110e5d076>] >> +startxref +213 +%%EOF +%BeginExifToolUpdate +1 0 obj +<< +/Pages 2 0 R +/Type /Catalog +/Metadata 5 0 R +>> +endobj +4 0 obj +<< +/Author (yXaryipxyRk9dVjWjSSaVaNCKeLRgEVzPRMp) +/Keywords (s9EeALwUg7urA7fnnhm5EtUyC54sW2WPUzqh) +/Subject (t49vimctvnuH7ZeAjAkv52ACvWFjcnm5MPJr) +/Title (NFY5f7Ft2DWMkxLhXwxvFf7eWR2KeK3vEDcd) +>> +endobj +5 0 obj +<< +/Type /Metadata +/Subtype /XML +/Length 3232 +>> +stream + + + + + + + + t49vimctvnuH7ZeAjAkv52ACvWFjcnm5MPJr + + + + + NFY5f7Ft2DWMkxLhXwxvFf7eWR2KeK3vEDcd + + + + + + yXaryipxyRk9dVjWjSSaVaNCKeLRgEVzPRMp + s9EeALwUg7urA7fnnhm5EtUyC54sW2WPUzqh + + + + + + + + + + + + + + + + + + + + + + + + + + + + +endstream +endobj +xref +0 2 +0000000000 65535 f +0000000452 00000 n +4 2 +0000000517 00000 n +0000000728 00000 n +trailer +<< +/Root 1 0 R +/Size 6 +/ID [ <52bba3c78160d0c6e851b59110e5d076> <54bba3c78160d0c6e851b59110e5d076> ] +/Info 4 0 R +/Prev 213 +>> +%EndExifToolUpdate 431 +startxref +4043 +%%EOF diff --git a/tests/test_metadata.py b/tests/test_metadata.py index 786e3cf5..3ef661bf 100644 --- a/tests/test_metadata.py +++ b/tests/test_metadata.py @@ -76,6 +76,50 @@ def test_override_metadata(output_type, resources, outpdf): assert pdfa_info['output'] == output_type +@pytest.mark.parametrize('output_type', ['pdfa', 'pdf', 'pdfa-1', 'pdfa-2', 'pdfa-3']) +@pytest.mark.parametrize('field', ['title', 'author', 'subject', 'keywords']) +def test_unset_metadata(output_type, field, resources, outpdf): + input_file = resources / 'meta.pdf' + + # magic strings contained in the input pdf metadata + meta = { + 'title': b'NFY5f7Ft2DWMkxLhXwxvFf7eWR2KeK3vEDcd', + 'author': b'yXaryipxyRk9dVjWjSSaVaNCKeLRgEVzPRMp', + 'subject': b't49vimctvnuH7ZeAjAkv52ACvWFjcnm5MPJr', + 'keywords': b's9EeALwUg7urA7fnnhm5EtUyC54sW2WPUzqh'} + + p = run_ocrmypdf( + input_file, + outpdf, + f'--{field}', + '', + '--output-type', + output_type, + '--plugin', + 'tests/plugins/tesseract_noop.py', + ) + + assert p.returncode == ExitCode.ok, p.stderr + + # We mainly want to ensure that when '' is passed, the corresponding + # metadata is unset in the output pdf. Since metedata is not compressed, + # the best way to gaurentee the metadata of interest didn't carry + # forward is to just check to ensure the corresponding magic string + # isn't contained anywhere in the output pdf. We'll also check to ensure + # it's in the input pdf and that any values not unset are still in the + # output pdf. + with open(input_file, 'rb') as before, open(outpdf, 'rb') as after: + before_data = before.read() + after_data = after.read() + + for k, v in meta.items(): + assert v in before_data + if k == field: + assert v not in after_data + else: + assert v in after_data + + def test_high_unicode(resources, no_outpdf): # Ghostscript doesn't support high Unicode, so neither do we, to be # safe