summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--NEWS4
-rw-r--r--mammoth/docx/__init__.py7
-rw-r--r--mammoth/docx/body_xml.py45
-rw-r--r--mammoth/docx/content_types_xml.py43
-rw-r--r--mammoth/docx/files.py25
-rw-r--r--tests/docx/body_xml_tests.py41
-rw-r--r--tests/docx/content_types_xml_tests.py44
-rw-r--r--tests/docx/files_tests.py25
-rw-r--r--tests/mammoth_tests.py8
-rw-r--r--tests/test-data/external-picture.docxbin0 -> 4120 bytes
-rw-r--r--tests/test-data/tiny-picture.pngbin0 -> 110 bytes
11 files changed, 225 insertions, 17 deletions
diff --git a/NEWS b/NEWS
index 98f27c0..ea5e806 100644
--- a/NEWS
+++ b/NEWS
@@ -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
new file mode 100644
index 0000000..3475626
--- /dev/null
+++ b/tests/test-data/external-picture.docx
Binary files differ
diff --git a/tests/test-data/tiny-picture.png b/tests/test-data/tiny-picture.png
new file mode 100644
index 0000000..74cb4ba
--- /dev/null
+++ b/tests/test-data/tiny-picture.png
Binary files differ