diff options
| -rw-r--r-- | NEWS | 4 | ||||
| -rw-r--r-- | mammoth/docx/__init__.py | 7 | ||||
| -rw-r--r-- | mammoth/docx/body_xml.py | 45 | ||||
| -rw-r--r-- | mammoth/docx/content_types_xml.py | 43 | ||||
| -rw-r--r-- | mammoth/docx/files.py | 25 | ||||
| -rw-r--r-- | tests/docx/body_xml_tests.py | 41 | ||||
| -rw-r--r-- | tests/docx/content_types_xml_tests.py | 44 | ||||
| -rw-r--r-- | tests/docx/files_tests.py | 25 | ||||
| -rw-r--r-- | tests/mammoth_tests.py | 8 | ||||
| -rw-r--r-- | tests/test-data/external-picture.docx | bin | 0 -> 4120 bytes | |||
| -rw-r--r-- | tests/test-data/tiny-picture.png | bin | 0 -> 110 bytes |
11 files changed, 225 insertions, 17 deletions
@@ -1,3 +1,7 @@ +# 0.3.20 + +* Add support for linked images. + # 0.3.19 * Fix: cannot extract raw text from elements without children diff --git a/mammoth/docx/__init__.py b/mammoth/docx/__init__.py index ec8d363..1ca8cb0 100644 --- a/mammoth/docx/__init__.py +++ b/mammoth/docx/__init__.py @@ -1,5 +1,6 @@ import zipfile import contextlib +import os from .. import results, lists from .xmlparser import parse_xml @@ -9,6 +10,7 @@ from .relationships_xml import read_relationships_xml_element, Relationships from .numbering_xml import read_numbering_xml_element, Numbering from .styles_xml import read_styles_xml_element from .notes_xml import create_footnotes_reader, create_endnotes_reader +from .files import Files from . import body_xml @@ -26,7 +28,7 @@ _namespaces = [ def read(fileobj): zip_file = zipfile.ZipFile(fileobj) - body_readers = _body_readers(zip_file) + body_readers = _body_readers(getattr(fileobj, "name"), zip_file) return _read_notes(zip_file, body_readers).bind(lambda notes: _read_document(zip_file, body_readers, notes)) @@ -55,7 +57,7 @@ def _read_document(zip_file, body_readers, notes): ) -def _body_readers(zip_file): +def _body_readers(document_path, zip_file): with _open_entry(zip_file, "[Content_Types].xml") as content_types_fileobj: content_types = read_content_types_xml_element(_parse_docx_xml(content_types_fileobj)) @@ -77,6 +79,7 @@ def _body_readers(zip_file): relationships=relationships, styles=styles, docx_file=zip_file, + files=Files(os.path.dirname(document_path)), ) return for_name diff --git a/mammoth/docx/body_xml.py b/mammoth/docx/body_xml.py index cb42f05..54f9dd3 100644 --- a/mammoth/docx/body_xml.py +++ b/mammoth/docx/body_xml.py @@ -10,7 +10,8 @@ def reader(numbering=None, content_types=None, relationships=None, styles=None, - docx_file=None): + docx_file=None, + files=None): read, read_all = _create_reader( numbering=numbering, @@ -18,6 +19,7 @@ def reader(numbering=None, relationships=relationships, styles=styles, docx_file=docx_file, + files=files, ) return _BodyReader(read, read_all) @@ -37,7 +39,7 @@ class _BodyReader(object): return results.Result(result.value, result.messages) -def _create_reader(numbering, content_types, relationships, styles, docx_file): +def _create_reader(numbering, content_types, relationships, styles, docx_file, files): _handlers = {} _ignored_elements = set([ "w:bookmarkStart", @@ -221,17 +223,9 @@ def _create_reader(numbering, content_types, relationships, styles, docx_file): return _combine_results(map(lambda blip: _read_blip(blip, alt_text), blips)) def _read_blip(element, alt_text): - relationship_id = element.attributes["r:embed"] - image_path = "word/" + relationships[relationship_id].target.lstrip("/") + image_path, open_image = _read_blip_image(element) content_type = content_types.find_content_type(image_path) - def open_image(): - image_file = docx_file.open(image_path) - if hasattr(image_file, "__exit__"): - return image_file - else: - return contextlib.closing(image_file) - image = documents.image(alt_text=alt_text, content_type=content_type, open=open_image) if content_type in ["image/png", "image/gif", "image/jpeg", "image/svg+xml", "image/tiff"]: @@ -241,6 +235,35 @@ def _create_reader(numbering, content_types, relationships, styles, docx_file): return _element_result_with_messages(image, messages) + def _read_blip_image(element): + embed_relationship_id = element.attributes.get("r:embed") + link_relationship_id = element.attributes.get("r:link") + if embed_relationship_id is not None: + return _read_embedded_blip_image(embed_relationship_id) + elif link_relationship_id is not None: + return _read_linked_blip_image(link_relationship_id) + + def _read_embedded_blip_image(relationship_id): + image_path = "word/" + relationships[relationship_id].target.lstrip("/") + + def open_image(): + image_file = docx_file.open(image_path) + if hasattr(image_file, "__exit__"): + return image_file + else: + return contextlib.closing(image_file) + + return image_path, open_image + + + def _read_linked_blip_image(relationship_id): + image_path = relationships[relationship_id].target + + def open_image(): + return files.open(image_path) + + return image_path, open_image + def note_reference_reader(note_type): @handler("w:{0}Reference".format(note_type)) def note_reference(element): diff --git a/mammoth/docx/content_types_xml.py b/mammoth/docx/content_types_xml.py index a2eb14c..f0406b7 100644 --- a/mammoth/docx/content_types_xml.py +++ b/mammoth/docx/content_types_xml.py @@ -7,7 +7,10 @@ def read_content_types_xml_element(element): _read_override, element.find_children("content-types:Override") )) - return ContentTypes(extension_defaults, overrides) + return _ContentTypes([ + _XmlContentTypes(extension_defaults, overrides), + _FallbackContentTypes(), + ]) def _read_default(element): @@ -22,7 +25,37 @@ def _read_override(element): return part_name.lstrip("/"), content_type -class ContentTypes(object): +class _ContentTypes(object): + def __init__(self, finders): + self._finders = finders + + def find_content_type(self, path): + for finder in self._finders: + content_type = finder.find_content_type(path) + if content_type is not None: + return content_type + + +class _FallbackContentTypes(object): + _image_content_types = { + "png": "png", + "gif": "gif", + "jpeg": "jpeg", + "jpg": "jpeg", + "tif": "tiff", + "tiff": "tiff", + "bmp": "bmp", + } + + def find_content_type(self, path): + extension = _get_extension(path).lower() + if extension in self._image_content_types: + return "image/" + self._image_content_types[extension] + else: + return None + + +class _XmlContentTypes(object): def __init__(self, extension_defaults, overrides): self._extension_defaults = extension_defaults self._overrides = overrides @@ -31,5 +64,9 @@ class ContentTypes(object): if path in self._overrides: return self._overrides[path] else: - extension = path.rpartition(".")[2] + extension = _get_extension(path) return self._extension_defaults.get(extension) + + +def _get_extension(path): + return path.rpartition(".")[2] diff --git a/mammoth/docx/files.py b/mammoth/docx/files.py new file mode 100644 index 0000000..d919de9 --- /dev/null +++ b/mammoth/docx/files.py @@ -0,0 +1,25 @@ +import os +import contextlib +try: + from urllib2 import urlopen +except ImportError: + from urllib.request import urlopen +try: + from urllib.parse import urlparse +except ImportError: + from urlparse import urlparse + + +class Files(object): + def __init__(self, base): + self._base = base + + def open(self, uri): + if _is_absolute(uri): + return contextlib.closing(urlopen(uri)) + else: + return open(os.path.join(self._base, uri), "rb") + + +def _is_absolute(url): + return urlparse(url).scheme != "" diff --git a/tests/docx/body_xml_tests.py b/tests/docx/body_xml_tests.py index 91be80f..d3b552b 100644 --- a/tests/docx/body_xml_tests.py +++ b/tests/docx/body_xml_tests.py @@ -364,6 +364,39 @@ class ReadXmlElementTests(object): assert_equal("image/x-emf", result.value[0].content_type) expected_warning = results.warning("Image of type image/x-emf is unlikely to display in web browsers") assert_equal([expected_warning], result.messages) + + + @istest + @funk.with_context + def can_read_linked_pictures(self, context): + drawing_element = _create_inline_image( + blip=_linked_blip("rId5"), + description="It's a hat", + ) + + image_bytes = b"Not an image at all!" + + relationships = Relationships({ + "rId5": Relationship(target="file:///media/hat.png") + }) + + files = context.mock() + funk.allows(files).open("file:///media/hat.png").returns(io.BytesIO(image_bytes)) + + content_types = context.mock() + funk.allows(content_types).find_content_type("file:///media/hat.png").returns("image/png") + + image = _read_and_get_document_xml_element( + drawing_element, + content_types=content_types, + relationships=relationships, + files=files, + )[0] + assert_equal(documents.Image, type(image)) + assert_equal("It's a hat", image.alt_text) + assert_equal("image/png", image.content_type) + with image.open() as image_file: + assert_equal(image_bytes, image_file.read()) @istest def footnote_reference_has_id_read(self): @@ -494,4 +527,10 @@ def _create_image_elements(description, blip): ] def _embedded_blip(relationship_id): - return xml_element("a:blip", {"r:embed": relationship_id}) + return _blip({"r:embed": relationship_id}) + +def _linked_blip(relationship_id): + return _blip({"r:link": relationship_id}) + +def _blip(attributes): + return xml_element("a:blip", attributes) diff --git a/tests/docx/content_types_xml_tests.py b/tests/docx/content_types_xml_tests.py index 9df8569..8b104d9 100644 --- a/tests/docx/content_types_xml_tests.py +++ b/tests/docx/content_types_xml_tests.py @@ -36,3 +36,47 @@ def content_type_is_based_on_override_if_present(): "image/hat", content_types.find_content_type("word/media/hat.png"), ) + + +@istest +def fallback_content_types_have_common_image_types(): + element = xml_element("content-types:Types", {}, []) + content_types = read_content_types_xml_element(element) + assert_equal( + "image/png", + content_types.find_content_type("word/media/hat.png"), + ) + assert_equal( + "image/gif", + content_types.find_content_type("word/media/hat.gif"), + ) + assert_equal( + "image/jpeg", + content_types.find_content_type("word/media/hat.jpg"), + ) + assert_equal( + "image/jpeg", + content_types.find_content_type("word/media/hat.jpeg"), + ) + assert_equal( + "image/bmp", + content_types.find_content_type("word/media/hat.bmp"), + ) + assert_equal( + "image/tiff", + content_types.find_content_type("word/media/hat.tif"), + ) + assert_equal( + "image/tiff", + content_types.find_content_type("word/media/hat.tiff"), + ) + + +@istest +def fallback_content_types_are_case_insensitive(): + element = xml_element("content-types:Types", {}, []) + content_types = read_content_types_xml_element(element) + assert_equal( + "image/png", + content_types.find_content_type("word/media/hat.PnG"), + ) diff --git a/tests/docx/files_tests.py b/tests/docx/files_tests.py new file mode 100644 index 0000000..c1e15dc --- /dev/null +++ b/tests/docx/files_tests.py @@ -0,0 +1,25 @@ +from nose.tools import istest, assert_equal + +from mammoth.docx.files import Files +from ..testing import test_path + + +@istest +def can_open_files_with_file_uri(): + path = test_path("tiny-picture.png") + files = Files(None) + with files.open("file:///" + path) as image_file: + contents = image_file.read() + assert_equal(bytes, type(contents)) + with open(path, "rb") as source_file: + assert_equal(source_file.read(), contents) + + +@istest +def can_open_files_with_relative_uri(): + files = Files(test_path("")) + with files.open("tiny-picture.png") as image_file: + contents = image_file.read() + assert_equal(bytes, type(contents)) + with open(test_path("tiny-picture.png"), "rb") as source_file: + assert_equal(source_file.read(), contents) diff --git a/tests/mammoth_tests.py b/tests/mammoth_tests.py index 2c7262c..88aa445 100644 --- a/tests/mammoth_tests.py +++ b/tests/mammoth_tests.py @@ -47,6 +47,14 @@ def inline_images_are_included_in_output(): result = mammoth.convert_to_html(fileobj=fileobj) assert_equal("""<p><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAIAAAACUFjqAAAAAXNSR0IArs4c6QAAAAlwSFlzAAAOvgAADr4B6kKxwAAAABNJREFUKFNj/M+ADzDhlWUYqdIAQSwBE8U+X40AAAAASUVORK5CYII=" /></p>""", result.value) assert_equal([], result.messages) + + +@istest +def images_stored_outside_of_document_are_included_in_output(): + with open(test_path("external-picture.docx"), "rb") as fileobj: + result = mammoth.convert_to_html(fileobj=fileobj) + assert_equal("""<p><img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAIAAAACUFjqAAAAAXNSR0IArs4c6QAAAAlwSFlzAAAOvgAADr4B6kKxwAAAABNJREFUKFNj/M+ADzDhlWUYqdIAQSwBE8U+X40AAAAASUVORK5CYII=" /></p>""", result.value) + assert_equal([], result.messages) @istest diff --git a/tests/test-data/external-picture.docx b/tests/test-data/external-picture.docx Binary files differnew file mode 100644 index 0000000..3475626 --- /dev/null +++ b/tests/test-data/external-picture.docx diff --git a/tests/test-data/tiny-picture.png b/tests/test-data/tiny-picture.png Binary files differnew file mode 100644 index 0000000..74cb4ba --- /dev/null +++ b/tests/test-data/tiny-picture.png |
