From ae15f89f73cffaebd20ba74b9c20ad641a1ea5e4 Mon Sep 17 00:00:00 2001 From: "Benjamin Frantzen (bfr)" Date: Wed, 10 Jun 2020 17:03:10 +0000 Subject: [PATCH] [FIX] tools.pdf: do not crash when PDF has no attachments In the PDF, `DictionaryObject` can be wrapped in `IndirectObject`. For nested dictionaries, using `get` on a `DictionaryObject` will unwrap the result if it's an `IndirectObject`, but not `__getitem__` so when trying to futher call `get` on the result will cause an error: we need to unwrap the object first. Instead, we patch `DictionaryObject.get()` for it to unwrap the object in case it's an `IndirectObject`. This is a follow-up commit of fece5ab1bf2fef043b131f1bd0886f0841116103 closes odoo/odoo#53269 X-original-commit: 189a0b28ec7fdb7472f936450cfd7b4bfd6912ed Signed-off-by: Olivier Dony (odo) Signed-off-by: bfr-o --- odoo/addons/base/tests/__init__.py | 1 + odoo/addons/base/tests/minimal.pdf | 44 ++++++++++++++++++ odoo/addons/base/tests/test_pdf.py | 75 ++++++++++++++++++++++++++++++ odoo/tools/pdf.py | 17 ++++++- 4 files changed, 135 insertions(+), 2 deletions(-) create mode 100644 odoo/addons/base/tests/minimal.pdf create mode 100644 odoo/addons/base/tests/test_pdf.py diff --git a/odoo/addons/base/tests/__init__.py b/odoo/addons/base/tests/__init__.py index b08e17ca0ae..af317988c81 100644 --- a/odoo/addons/base/tests/__init__.py +++ b/odoo/addons/base/tests/__init__.py @@ -45,3 +45,4 @@ from . import test_tests_tags from . import test_base_document_layout from . import test_form_create from . import test_cloc +from . import test_pdf diff --git a/odoo/addons/base/tests/minimal.pdf b/odoo/addons/base/tests/minimal.pdf new file mode 100644 index 00000000000..2a73de072da --- /dev/null +++ b/odoo/addons/base/tests/minimal.pdf @@ -0,0 +1,44 @@ +%PDF-1.3 +%µ¶ + +1 0 obj +<> +endobj + +2 0 obj +<> +endobj + +3 0 obj +<>>>>>/Contents 4 0 R>> +endobj + +4 0 obj +<> +stream +BT +/F1 100 Tf +10 400 Td +(!Odoo Test!) Tj +ET +endstream +endobj + +5 0 obj +<> +endobj + +xref +0 6 +0000000000 00001 f +0000000016 00000 n +0000000074 00000 n +0000000126 00000 n +0000000254 00000 n +0000000346 00000 n + +trailer +<> +startxref +376 +%%EOF diff --git a/odoo/addons/base/tests/test_pdf.py b/odoo/addons/base/tests/test_pdf.py new file mode 100644 index 00000000000..6954ca4a689 --- /dev/null +++ b/odoo/addons/base/tests/test_pdf.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo.tests.common import TransactionCase +from odoo.tools import pdf +from odoo.modules.module import get_module_resource +import io + + +class TestPdf(TransactionCase): + """ Tests on pdf. """ + + def setUp(self): + super().setUp() + file_path = get_module_resource('base', 'tests', 'minimal.pdf') + self.file = open(file_path, 'rb').read() + self.minimal_reader_buffer = io.BytesIO(self.file) + self.minimal_pdf_reader = pdf.OdooPdfFileReader(self.minimal_reader_buffer) + + def test_odoo_pdf_file_reader(self): + attachments = list(self.minimal_pdf_reader.getAttachments()) + self.assertEqual(len(attachments), 0) + + pdf_writer = pdf.PdfFileWriter() + pdf_writer.cloneReaderDocumentRoot(self.minimal_pdf_reader) + pdf_writer.addAttachment('test_attachment.txt', b'My awesome attachment') + + attachments = list(self.minimal_pdf_reader.getAttachments()) + self.assertEqual(len(attachments), 1) + + def test_odoo_pdf_file_writer(self): + attachments = list(self.minimal_pdf_reader.getAttachments()) + self.assertEqual(len(attachments), 0) + + pdf_writer = pdf.OdooPdfFileWriter() + pdf_writer.cloneReaderDocumentRoot(self.minimal_pdf_reader) + + pdf_writer.addAttachment('test_attachment.txt', b'My awesome attachment') + attachments = list(self.minimal_pdf_reader.getAttachments()) + self.assertEqual(len(attachments), 1) + + pdf_writer.addAttachment('another_attachment.txt', b'My awesome OTHER attachment') + attachments = list(self.minimal_pdf_reader.getAttachments()) + self.assertEqual(len(attachments), 2) + + def test_merge_pdf(self): + self.assertEqual(self.minimal_pdf_reader.getNumPages(), 1) + page = self.minimal_pdf_reader.getPage(0) + + merged_pdf = pdf.merge_pdf([self.file, self.file]) + merged_reader_buffer = io.BytesIO(merged_pdf) + merged_pdf_reader = pdf.OdooPdfFileReader(merged_reader_buffer) + self.assertEqual(merged_pdf_reader.getNumPages(), 2) + merged_reader_buffer.close() + + def test_branded_file_writer(self): + # It's not easy to create a PDF with PyPDF2, so instead we copy minimal.pdf with our custom pdf writer + pdf_writer = pdf.PdfFileWriter() # BrandedFileWriter + pdf_writer.cloneReaderDocumentRoot(self.minimal_pdf_reader) + writer_buffer = io.BytesIO() + pdf_writer.write(writer_buffer) + branded_content = writer_buffer.getvalue() + writer_buffer.close() + + # Read the metadata of the newly created pdf. + reader_buffer = io.BytesIO(branded_content) + pdf_reader = pdf.PdfFileReader(reader_buffer) + pdf_info = pdf_reader.getDocumentInfo() + self.assertEqual(pdf_info['/Producer'], 'Odoo') + self.assertEqual(pdf_info['/Creator'], 'Odoo') + reader_buffer.close() + + def tearDown(self): + super().tearDown() + self.minimal_reader_buffer.close() diff --git a/odoo/tools/pdf.py b/odoo/tools/pdf.py index 8e317fa2bde..dadae1df522 100644 --- a/odoo/tools/pdf.py +++ b/odoo/tools/pdf.py @@ -12,6 +12,17 @@ import hashlib DEFAULT_PDF_DATETIME_FORMAT = "D:%Y%m%d%H%M%S+00'00'" +# make sure values are unwrapped by calling the specialized __getitem__ +def _unwrapping_get(self, key, default=None): + try: + return self[key] + except KeyError: + return default + + +DictionaryObject.get = _unwrapping_get + + class BrandedFileWriter(PdfFileWriter): def __init__(self): super().__init__() @@ -25,7 +36,8 @@ PdfFileWriter = BrandedFileWriter def merge_pdf(pdf_data): - ''' Merge a collection of PDF documents in one + ''' Merge a collection of PDF documents in one. + Note that the attachments are not merged. :param list pdf_data: a list of PDF datastrings :return: a unique merged PDF datastring ''' @@ -40,7 +52,8 @@ def merge_pdf(pdf_data): def rotate_pdf(pdf): - ''' Rotate clockwise PDF (90°) + ''' Rotate clockwise PDF (90°) into a new PDF. + Note that the attachments are not copied. :param pdf: a PDF to rotate :return: a PDF rotated '''