From b8d115b30c7fd8828c17d57c70cdee5a934b4d10 Mon Sep 17 00:00:00 2001 From: Jervis Whitley Date: Fri, 28 Apr 2017 16:31:52 +1000 Subject: [PATCH 01/14] Add a rudimentary anchor type shape --- .../features/shapes/shapes-anchor.rst | 232 ++++++++++++++++++ docx/enum/shape.py | 16 ++ docx/oxml/__init__.py | 3 +- docx/oxml/shape.py | 23 ++ docx/parts/document.py | 11 +- docx/shape.py | 29 ++- tests/oxml/unitdata/dml.py | 10 + tests/test_shape.py | 68 ++++- 8 files changed, 387 insertions(+), 5 deletions(-) create mode 100644 docs/dev/analysis/features/shapes/shapes-anchor.rst diff --git a/docs/dev/analysis/features/shapes/shapes-anchor.rst b/docs/dev/analysis/features/shapes/shapes-anchor.rst new file mode 100644 index 000000000..e08753482 --- /dev/null +++ b/docs/dev/analysis/features/shapes/shapes-anchor.rst @@ -0,0 +1,232 @@ +Anchor shape +============ + +Word allows a graphical object to be placed into a document as a floating +object. A floating shape appears as a ```` element as a child of +a ```` element and has a ```` child. + + +Candidate protocol -- anchor shape access +----------------------------------------- + +The following interactive session illustrates the protocol for accessing an +anchor shape:: + + >>> shapes = document.body.anchor_shapes + >>> shape = shapes[0] + >>> assert shape.type == MSO_SHAPE_TYPE.PICTURE + + +Resources +--------- + +* `Document Members (Word) on MSDN`_ +* `Shape Members (Word) on MSDN`_ + +.. _Document Members (Word) on MSDN: + http://msdn.microsoft.com/en-us/library/office/ff840898.aspx + +.. _Shape Members (Word) on MSDN: + http://msdn.microsoft.com/en-us/library/office/ff195191.aspx + + +MS API +------ + +The Shapes and InlineShapes properties on Document hold references to things +like pictures in the MS API. + +* Height and Width +* Borders +* Shadow +* Hyperlink +* PictureFormat (providing brightness, color, crop, transparency, contrast) +* ScaleHeight and ScaleWidth +* HasChart +* HasSmartArt +* Type (Chart, LockedCanvas, Picture, SmartArt, etc.) + + +Spec references +--------------- + +* 17.3.3.9 drawing (DrawingML Object) +* 20.4.2.3 anchor (Anchor DrawingML Object) +* 20.4.2.7 extent (Drawing Object Size) + + +Minimal XML +----------- + +.. highlight:: xml + +This XML represents my best guess of the minimal inline shape container that +Word will load:: + + + + + + + right + + + center + + + + + + + + + + + + + + + + +Specimen XML +------------ + +.. highlight:: xml + +A ``CT_Drawing`` (````) element can appear in a run, as a peer of, +for example, a ```` element. This element contains a DrawingML object. +WordprocessingML drawings are discussed in section 20.4 of the ISO/IEC spec. + +This XML represents an inline shape inserted inline on a paragraph by itself. +The particulars of the graphical object itself are redacted:: + + + + + + + + + + + + + + + + + + + + + + + + + + + +Schema definitions +------------------ + +.. highlight:: xml + +:: + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/docx/enum/shape.py b/docx/enum/shape.py index f1d6ffd8c..2f0224d3f 100644 --- a/docx/enum/shape.py +++ b/docx/enum/shape.py @@ -18,4 +18,20 @@ class WD_INLINE_SHAPE_TYPE(object): SMART_ART = 15 NOT_IMPLEMENTED = -6 + WD_INLINE_SHAPE = WD_INLINE_SHAPE_TYPE + + +class WD_ANCHOR_SHAPE_TYPE(object): + """ + Corresponds to WdInlineShapeType enumeration + http://msdn.microsoft.com/en-us/library/office/ff192587.aspx + """ + CHART = 12 + LINKED_PICTURE = 4 + PICTURE = 3 + SMART_ART = 15 + NOT_IMPLEMENTED = -6 + + +WD_ANCHOR_SHAPE = WD_ANCHOR_SHAPE_TYPE diff --git a/docx/oxml/__init__.py b/docx/oxml/__init__.py index 528b1eac7..cd422734b 100644 --- a/docx/oxml/__init__.py +++ b/docx/oxml/__init__.py @@ -93,7 +93,7 @@ def OxmlElement(nsptag_str, attrs=None, nsdecls=None): register_element_cls('w:type', CT_SectType) from .shape import ( - CT_Blip, CT_BlipFillProperties, CT_GraphicalObject, + CT_Anchor, CT_Blip, CT_BlipFillProperties, CT_GraphicalObject, CT_GraphicalObjectData, CT_Inline, CT_NonVisualDrawingProps, CT_Picture, CT_PictureNonVisual, CT_Point2D, CT_PositiveSize2D, CT_ShapeProperties, CT_Transform2D @@ -112,6 +112,7 @@ def OxmlElement(nsptag_str, attrs=None, nsdecls=None): register_element_cls('wp:docPr', CT_NonVisualDrawingProps) register_element_cls('wp:extent', CT_PositiveSize2D) register_element_cls('wp:inline', CT_Inline) +register_element_cls('wp:anchor', CT_Anchor) from .styles import CT_LatentStyles, CT_LsdException, CT_Style, CT_Styles register_element_cls('w:basedOn', CT_String) diff --git a/docx/oxml/shape.py b/docx/oxml/shape.py index 77ca7db8a..6e0a570b4 100644 --- a/docx/oxml/shape.py +++ b/docx/oxml/shape.py @@ -102,6 +102,29 @@ def _inline_xml(cls): ) +class CT_Anchor(CT_Inline): + """ + ```` element, container for a floating shape. + """ + wrapSquare = ZeroOrOne('wp:wrapSquare') + + @classmethod + def _inline_xml(cls): + return ( + '\n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + '' % nsdecls('wp', 'a', 'pic', 'r') + ) + + class CT_NonVisualDrawingProps(BaseOxmlElement): """ Used for ```` element, and perhaps others. Specifies the id and diff --git a/docx/parts/document.py b/docx/parts/document.py index 7a23e9a5e..ad9e0e042 100644 --- a/docx/parts/document.py +++ b/docx/parts/document.py @@ -12,7 +12,7 @@ from .numbering import NumberingPart from ..opc.constants import RELATIONSHIP_TYPE as RT from ..opc.part import XmlPart -from ..oxml.shape import CT_Inline +from ..oxml.shape import CT_Anchor, CT_Inline from ..shape import InlineShapes from ..shared import lazyproperty from .settings import SettingsPart @@ -89,10 +89,17 @@ def new_pic_inline(self, image_descriptor, width, height): specified by *image_descriptor* and scaled based on the values of *width* and *height*. """ + return self.new_pic(image_descriptor, width, height, inline=True) + + def new_pic(self, image_descriptor, width, height, inline=True): rId, image = self.get_or_add_image(image_descriptor) cx, cy = image.scaled_dimensions(width, height) shape_id, filename = self.next_id, image.filename - return CT_Inline.new_pic_inline(shape_id, rId, filename, cx, cy) + if inline: + ShapeType = CT_Inline + else: + ShapeType = CT_Anchor + return ShapeType.new_pic_inline(shape_id, rId, filename, cx, cy) @property def next_id(self): diff --git a/docx/shape.py b/docx/shape.py index e4f885d73..fbf2183cb 100644 --- a/docx/shape.py +++ b/docx/shape.py @@ -9,7 +9,7 @@ absolute_import, division, print_function, unicode_literals ) -from .enum.shape import WD_INLINE_SHAPE +from .enum.shape import WD_INLINE_SHAPE, WD_ANCHOR_SHAPE from .oxml.ns import nsmap from .shared import Parented @@ -101,3 +101,30 @@ def width(self): def width(self, cx): self._inline.extent.cx = cx self._inline.graphic.graphicData.pic.spPr.cx = cx + + +class AnchorShape(InlineShape): + """ + Proxy for an ```` element, representing the container for a + positioned graphical element. + """ + + @property + def type(self): + """ + The type of this anchored shape as a member of + ``docx.enum.shape.WD_INLINE_SHAPE``, e.g. ``LINKED_PICTURE``. + Read-only. + """ + graphicData = self._inline.graphic.graphicData + uri = graphicData.uri + if uri == nsmap['pic']: + blip = graphicData.pic.blipFill.blip + if blip.link is not None: + return WD_ANCHOR_SHAPE.LINKED_PICTURE + return WD_ANCHOR_SHAPE.PICTURE + if uri == nsmap['c']: + return WD_ANCHOR_SHAPE.CHART + if uri == nsmap['dgm']: + return WD_ANCHOR_SHAPE.SMART_ART + return WD_ANCHOR_SHAPE.NOT_IMPLEMENTED diff --git a/tests/oxml/unitdata/dml.py b/tests/oxml/unitdata/dml.py index 84518f8b7..e10dbb323 100644 --- a/tests/oxml/unitdata/dml.py +++ b/tests/oxml/unitdata/dml.py @@ -7,6 +7,12 @@ from ...unitdata import BaseBuilder +class CT_AnchorBuilder(BaseBuilder): + __tag__ = 'wp:anchor' + __nspfxs__ = ('wp',) + __attrs__ = ('distT', 'distB', 'distL', 'distR') + + class CT_BlipBuilder(BaseBuilder): __tag__ = 'a:blip' __nspfxs__ = ('a',) @@ -195,6 +201,10 @@ def an_inline(): return CT_InlineBuilder() +def an_anchor(): + return CT_AnchorBuilder() + + def an_nvPicPr(): return CT_PictureNonVisualBuilder() diff --git a/tests/test_shape.py b/tests/test_shape.py index 105d2fa40..53cf37aa2 100644 --- a/tests/test_shape.py +++ b/tests/test_shape.py @@ -10,11 +10,12 @@ from docx.enum.shape import WD_INLINE_SHAPE from docx.oxml.ns import nsmap -from docx.shape import InlineShape, InlineShapes +from docx.shape import AnchorShape, InlineShape, InlineShapes from docx.shared import Length from .oxml.unitdata.dml import ( a_blip, a_blipFill, a_graphic, a_graphicData, a_pic, an_inline, + an_anchor, ) from .unitutil.cxml import element, xml from .unitutil.mock import loose_mock @@ -78,6 +79,71 @@ def inline_shapes_with_parent_(self, request): return inline_shapes, parent_ +class DescribeAncorShape(object): + + def it_knows_what_type_of_shape_it_is(self, shape_type_fixture): + shape, shape_type = shape_type_fixture + assert shape.type == shape_type + + @pytest.fixture(params=[ + 'embed pic', 'link pic', 'link+embed pic', 'chart', 'smart art', + 'not implemented' + ]) + def shape_type_fixture(self, request): + if request.param == 'embed pic': + inline = self._with_picture(embed=True) + shape_type = WD_INLINE_SHAPE.PICTURE + + elif request.param == 'link pic': + inline = self._with_picture(link=True) + shape_type = WD_INLINE_SHAPE.LINKED_PICTURE + + elif request.param == 'link+embed pic': + inline = self._with_picture(embed=True, link=True) + shape_type = WD_INLINE_SHAPE.LINKED_PICTURE + + elif request.param == 'chart': + inline = self._with_uri(nsmap['c']) + shape_type = WD_INLINE_SHAPE.CHART + + elif request.param == 'smart art': + inline = self._with_uri(nsmap['dgm']) + shape_type = WD_INLINE_SHAPE.SMART_ART + + elif request.param == 'not implemented': + inline = self._with_uri('foobar') + shape_type = WD_INLINE_SHAPE.NOT_IMPLEMENTED + + return AnchorShape(inline), shape_type + + def _with_picture(self, embed=False, link=False): + picture_ns = nsmap['pic'] + + blip_bldr = a_blip() + if embed: + blip_bldr.with_embed('rId1') + if link: + blip_bldr.with_link('rId2') + + image = ( + an_anchor().with_nsdecls('wp', 'r').with_child( + a_graphic().with_nsdecls().with_child( + a_graphicData().with_uri(picture_ns).with_child( + a_pic().with_nsdecls().with_child( + a_blipFill().with_child( + blip_bldr))))) + ).element + return image + + def _with_uri(self, uri): + inline = ( + an_anchor().with_nsdecls('wp').with_child( + a_graphic().with_nsdecls().with_child( + a_graphicData().with_uri(uri))) + ).element + return inline + + class DescribeInlineShape(object): def it_knows_what_type_of_shape_it_is(self, shape_type_fixture): From 42323a4d4ff68f54fe9fe7f0845e55a819d04426 Mon Sep 17 00:00:00 2001 From: Jervis Whitley Date: Fri, 28 Apr 2017 16:32:15 +1000 Subject: [PATCH 02/14] Expand .gitignore --- .gitignore | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.gitignore b/.gitignore index de25a6f76..2f010f907 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,10 @@ _scratch/ Session.vim /.tox/ +*.swp +*.cache +.Python +bin +include +lib +pip-selfcheck.json From d81aaf178b6f68b7ba0a26e46729d0efa22df698 Mon Sep 17 00:00:00 2001 From: Jervis Whitley Date: Fri, 28 Apr 2017 19:15:51 +1000 Subject: [PATCH 03/14] Add float image Currently hardcoded to position right and vertical middle. --- docx/document.py | 7 +++++-- docx/oxml/shape.py | 26 ++++++++++++++++++++------ docx/parts/document.py | 4 ++-- docx/text/run.py | 21 ++++++++++++++++----- 4 files changed, 43 insertions(+), 15 deletions(-) diff --git a/docx/document.py b/docx/document.py index ba94a7990..228b58750 100644 --- a/docx/document.py +++ b/docx/document.py @@ -62,7 +62,8 @@ def add_paragraph(self, text='', style=None): """ return self._body.add_paragraph(text, style) - def add_picture(self, image_path_or_stream, width=None, height=None): + def add_picture( + self, image_path_or_stream, width=None, height=None, inline=True): """ Return a new picture shape added in its own paragraph at the end of the document. The picture contains the image at @@ -76,7 +77,9 @@ def add_picture(self, image_path_or_stream, width=None, height=None): is often the case. """ run = self.add_paragraph().add_run() - return run.add_picture(image_path_or_stream, width, height) + return run.add_picture( + image_path_or_stream, width, height, inline=inline + ) def add_section(self, start_type=WD_SECTION.NEW_PAGE): """ diff --git a/docx/oxml/shape.py b/docx/oxml/shape.py index 6e0a570b4..384862c13 100644 --- a/docx/oxml/shape.py +++ b/docx/oxml/shape.py @@ -106,22 +106,34 @@ class CT_Anchor(CT_Inline): """ ```` element, container for a floating shape. """ + simplePos = OneAndOnlyOne('wp:simplePos') + positionH = OneAndOnlyOne('wp:positionH') + positionV = OneAndOnlyOne('wp:positionV') + effectExtent = OneAndOnlyOne('wp:effectExtent') wrapSquare = ZeroOrOne('wp:wrapSquare') @classmethod def _inline_xml(cls): return ( - '\n' + '\n' + ' \n' + ' \n' + ' right\n' + ' \n' + ' \n' + ' center\n' + ' \n' ' \n' - ' \n' + ' \n' + ' \n' ' \n' ' \n' - ' \n' + ' \n' ' \n' - ' \n' + ' \n' ' \n' ' \n' - '' % nsdecls('wp', 'a', 'pic', 'r') + '' % (nsdecls('wp'), nsdecls('a'), nsdecls('a')) ) @@ -183,7 +195,9 @@ def _pic_xml(cls): ' \n' ' \n' ' \n' - ' \n' + ' \n' + ' \n' + ' \n' ' \n' '' % nsdecls('pic', 'a', 'r') ) diff --git a/docx/parts/document.py b/docx/parts/document.py index ad9e0e042..c7ece23a5 100644 --- a/docx/parts/document.py +++ b/docx/parts/document.py @@ -83,13 +83,13 @@ def inline_shapes(self): """ return InlineShapes(self._element.body, self) - def new_pic_inline(self, image_descriptor, width, height): + def new_pic_inline(self, image_descriptor, width, height, inline=True): """ Return a newly-created `w:inline` element containing the image specified by *image_descriptor* and scaled based on the values of *width* and *height*. """ - return self.new_pic(image_descriptor, width, height, inline=True) + return self.new_pic(image_descriptor, width, height, inline=inline) def new_pic(self, image_descriptor, width, height, inline=True): rId, image = self.get_or_add_image(image_descriptor) diff --git a/docx/text/run.py b/docx/text/run.py index 97d6da7db..929e6efcf 100644 --- a/docx/text/run.py +++ b/docx/text/run.py @@ -9,7 +9,7 @@ from ..enum.style import WD_STYLE_TYPE from ..enum.text import WD_BREAK from .font import Font -from ..shape import InlineShape +from ..shape import AnchorShape, InlineShape from ..shared import Parented @@ -46,7 +46,8 @@ def add_break(self, break_type=WD_BREAK.LINE): if clear is not None: br.clear = clear - def add_picture(self, image_path_or_stream, width=None, height=None): + def add_picture( + self, image_path_or_stream, width=None, height=None, inline=True): """ Return an |InlineShape| instance containing the image identified by *image_path_or_stream*, added to the end of this run. @@ -58,10 +59,20 @@ def add_picture(self, image_path_or_stream, width=None, height=None): native size of the picture is calculated using the dots-per-inch (dpi) value specified in the image file, defaulting to 72 dpi if no value is specified, as is often the case. + *inline* boolean true if the picture is inline with text, + false if floated. """ - inline = self.part.new_pic_inline(image_path_or_stream, width, height) - self._r.add_drawing(inline) - return InlineShape(inline) + image = self.part.new_pic_inline( + image_path_or_stream, width, height, inline=inline + ) + self._r.add_drawing(image) + + if inline: + ShapeType = InlineShape + else: + ShapeType = AnchorShape + + return ShapeType(image) def add_tab(self): """ From 45b9cf9bada08b0a36ede5ddd23d7c50ede28005 Mon Sep 17 00:00:00 2001 From: Jervis Whitley Date: Mon, 1 May 2017 11:59:57 +1000 Subject: [PATCH 04/14] Style --- docx/oxml/shape.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docx/oxml/shape.py b/docx/oxml/shape.py index 384862c13..7db63e0cc 100644 --- a/docx/oxml/shape.py +++ b/docx/oxml/shape.py @@ -115,7 +115,9 @@ class CT_Anchor(CT_Inline): @classmethod def _inline_xml(cls): return ( - '\n' + '\n' ' \n' ' \n' ' right\n' From f7b8a57a796aee2376a5586412b684757986fb00 Mon Sep 17 00:00:00 2001 From: Jervis Whitley Date: Mon, 1 May 2017 12:50:56 +1000 Subject: [PATCH 05/14] Implement position and wrapText for floating images Position can be `None` - for inline or a tuple ('right', 'center') for (positionH, positionV) positioning. valid positionH: ('left', 'center', 'right') valid positionV: ('top': 'center', 'bottom') wrap is currently supporting only `wrapSquare` such that its value is the `wrapText` value. Supported options are: 'left', 'right' and 'bothSides' --- docx/oxml/shape.py | 36 +++++++++++++++++++++++++++++++++--- docx/parts/document.py | 21 ++++++++++++++++----- docx/text/run.py | 10 ++++++---- 3 files changed, 55 insertions(+), 12 deletions(-) diff --git a/docx/oxml/shape.py b/docx/oxml/shape.py index 7db63e0cc..0c8c11af5 100644 --- a/docx/oxml/shape.py +++ b/docx/oxml/shape.py @@ -58,7 +58,7 @@ class CT_Inline(BaseOxmlElement): graphic = OneAndOnlyOne('a:graphic') @classmethod - def new(cls, cx, cy, shape_id, pic): + def new(cls, cx, cy, shape_id, pic, position=None, wrap=None): """ Return a new ```` element populated with the values passed as parameters. @@ -75,17 +75,25 @@ def new(cls, cx, cy, shape_id, pic): return inline @classmethod - def new_pic_inline(cls, shape_id, rId, filename, cx, cy): + def new_pic_inline( + cls, shape_id, rId, filename, cx, cy, position=None, wrap=None): """ Return a new `wp:inline` element containing the `pic:pic` element specified by the argument values. """ pic_id = 0 # Word doesn't seem to use this, but does not omit it pic = CT_Picture.new(pic_id, filename, rId, cx, cy) - inline = cls.new(cx, cy, shape_id, pic) + inline = cls.new(cx, cy, shape_id, pic, position, wrap) inline.graphic.graphicData._insert_pic(pic) return inline + @classmethod + def new_pic( + cls, shape_id, rId, filename, cx, cy, position=None, wrap=None): + return cls.new_pic_inline( + shape_id, rId, filename, cx, cy, position, wrap + ) + @classmethod def _inline_xml(cls): return ( @@ -112,6 +120,28 @@ class CT_Anchor(CT_Inline): effectExtent = OneAndOnlyOne('wp:effectExtent') wrapSquare = ZeroOrOne('wp:wrapSquare') + @classmethod + def new(cls, cx, cy, shape_id, pic, position, wrap=None): + """ + Return a new ```` element populated with the values passed + as parameters. + """ + anchor = parse_xml(cls._inline_xml()) + anchor.extent.cx = cx + anchor.extent.cy = cy + anchor.docPr.id = shape_id + anchor.docPr.name = 'Picture %d' % shape_id + anchor.graphic.graphicData.uri = ( + 'http://schemas.openxmlformats.org/drawingml/2006/picture' + ) + anchor.graphic.graphicData._insert_pic(pic) + positionH, positionV = position + anchor.positionH.getchildren()[0].text = positionH + anchor.positionV.getchildren()[0].text = positionV + if wrap is not None: + anchor.wrapSquare.set('wrapText', wrap) + return anchor + @classmethod def _inline_xml(cls): return ( diff --git a/docx/parts/document.py b/docx/parts/document.py index c7ece23a5..5028b30b4 100644 --- a/docx/parts/document.py +++ b/docx/parts/document.py @@ -83,23 +83,34 @@ def inline_shapes(self): """ return InlineShapes(self._element.body, self) - def new_pic_inline(self, image_descriptor, width, height, inline=True): + def new_pic_inline(self, image_descriptor, width, height): """ Return a newly-created `w:inline` element containing the image specified by *image_descriptor* and scaled based on the values of *width* and *height*. """ - return self.new_pic(image_descriptor, width, height, inline=inline) + return self.new_pic(image_descriptor, width, height) - def new_pic(self, image_descriptor, width, height, inline=True): + def new_pic( + self, image_descriptor, width, height, position=None, wrap=None): + """ + Return a new `w:inline` or `w:anchor` element containing the image + specified by *image_descriptor* and scaled based on the values of + *width* and *height*. *position* is a tuple specifying the positionH + and positionV, setting this will float the image in `w:anchor`. *wrap* + can specify settings for the wrap, the default is: + ``wrapSquare wrapText='bothSides' + """ rId, image = self.get_or_add_image(image_descriptor) cx, cy = image.scaled_dimensions(width, height) shape_id, filename = self.next_id, image.filename - if inline: + if position is None: ShapeType = CT_Inline else: ShapeType = CT_Anchor - return ShapeType.new_pic_inline(shape_id, rId, filename, cx, cy) + return ShapeType.new_pic( + shape_id, rId, filename, cx, cy, position, wrap + ) @property def next_id(self): diff --git a/docx/text/run.py b/docx/text/run.py index 929e6efcf..7dfbb9adb 100644 --- a/docx/text/run.py +++ b/docx/text/run.py @@ -47,7 +47,9 @@ def add_break(self, break_type=WD_BREAK.LINE): br.clear = clear def add_picture( - self, image_path_or_stream, width=None, height=None, inline=True): + self, image_path_or_stream, width=None, height=None, + position=None, wrap=None + ): """ Return an |InlineShape| instance containing the image identified by *image_path_or_stream*, added to the end of this run. @@ -62,12 +64,12 @@ def add_picture( *inline* boolean true if the picture is inline with text, false if floated. """ - image = self.part.new_pic_inline( - image_path_or_stream, width, height, inline=inline + image = self.part.new_pic( + image_path_or_stream, width, height, position, wrap ) self._r.add_drawing(image) - if inline: + if position is None: ShapeType = InlineShape else: ShapeType = AnchorShape From 7e2a8540d9a14c0de72a673a0193ff55ce350b09 Mon Sep 17 00:00:00 2001 From: Jervis Whitley Date: Mon, 1 May 2017 15:04:55 +1000 Subject: [PATCH 06/14] Missing args --- docx/document.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docx/document.py b/docx/document.py index 228b58750..319873154 100644 --- a/docx/document.py +++ b/docx/document.py @@ -63,7 +63,9 @@ def add_paragraph(self, text='', style=None): return self._body.add_paragraph(text, style) def add_picture( - self, image_path_or_stream, width=None, height=None, inline=True): + self, image_path_or_stream, width=None, height=None, + position=None, wrap=None + ): """ Return a new picture shape added in its own paragraph at the end of the document. The picture contains the image at @@ -78,7 +80,7 @@ def add_picture( """ run = self.add_paragraph().add_run() return run.add_picture( - image_path_or_stream, width, height, inline=inline + image_path_or_stream, width, height, position, wrap ) def add_section(self, start_type=WD_SECTION.NEW_PAGE): From a162814803eb7fee1afabaa0384cd4935512f686 Mon Sep 17 00:00:00 2001 From: Jervis Whitley Date: Mon, 1 May 2017 17:59:04 +1000 Subject: [PATCH 07/14] Add margin --- docx/document.py | 4 ++-- docx/oxml/shape.py | 22 ++++++++++++++++------ docx/parts/document.py | 5 +++-- docx/text/run.py | 4 ++-- 4 files changed, 23 insertions(+), 12 deletions(-) diff --git a/docx/document.py b/docx/document.py index 319873154..51f4a87bb 100644 --- a/docx/document.py +++ b/docx/document.py @@ -64,7 +64,7 @@ def add_paragraph(self, text='', style=None): def add_picture( self, image_path_or_stream, width=None, height=None, - position=None, wrap=None + position=None, margin=None, wrap=None ): """ Return a new picture shape added in its own paragraph at the end of @@ -80,7 +80,7 @@ def add_picture( """ run = self.add_paragraph().add_run() return run.add_picture( - image_path_or_stream, width, height, position, wrap + image_path_or_stream, width, height, position, margin, wrap ) def add_section(self, start_type=WD_SECTION.NEW_PAGE): diff --git a/docx/oxml/shape.py b/docx/oxml/shape.py index 0c8c11af5..e23fbc441 100644 --- a/docx/oxml/shape.py +++ b/docx/oxml/shape.py @@ -58,7 +58,7 @@ class CT_Inline(BaseOxmlElement): graphic = OneAndOnlyOne('a:graphic') @classmethod - def new(cls, cx, cy, shape_id, pic, position=None, wrap=None): + def new(cls, cx, cy, shape_id, pic, position=None, margin=None, wrap=None): """ Return a new ```` element populated with the values passed as parameters. @@ -76,22 +76,24 @@ def new(cls, cx, cy, shape_id, pic, position=None, wrap=None): @classmethod def new_pic_inline( - cls, shape_id, rId, filename, cx, cy, position=None, wrap=None): + cls, shape_id, rId, filename, cx, cy, + position=None, margin=None, wrap=None): """ Return a new `wp:inline` element containing the `pic:pic` element specified by the argument values. """ pic_id = 0 # Word doesn't seem to use this, but does not omit it pic = CT_Picture.new(pic_id, filename, rId, cx, cy) - inline = cls.new(cx, cy, shape_id, pic, position, wrap) + inline = cls.new(cx, cy, shape_id, pic, position, margin, wrap) inline.graphic.graphicData._insert_pic(pic) return inline @classmethod def new_pic( - cls, shape_id, rId, filename, cx, cy, position=None, wrap=None): + cls, shape_id, rId, filename, cx, cy, + position=None, margin=None, wrap=None): return cls.new_pic_inline( - shape_id, rId, filename, cx, cy, position, wrap + shape_id, rId, filename, cx, cy, position, margin, wrap ) @classmethod @@ -121,7 +123,7 @@ class CT_Anchor(CT_Inline): wrapSquare = ZeroOrOne('wp:wrapSquare') @classmethod - def new(cls, cx, cy, shape_id, pic, position, wrap=None): + def new(cls, cx, cy, shape_id, pic, position, margin=None, wrap=None): """ Return a new ```` element populated with the values passed as parameters. @@ -138,8 +140,16 @@ def new(cls, cx, cy, shape_id, pic, position, wrap=None): positionH, positionV = position anchor.positionH.getchildren()[0].text = positionH anchor.positionV.getchildren()[0].text = positionV + + if margin is not None: + anchor.distT = margin.get('top', 0) + anchor.distR = margin.get('right', 0) + anchor.distB = margin.get('bottom', 0) + anchor.distL = margin.get('left', 0) + if wrap is not None: anchor.wrapSquare.set('wrapText', wrap) + return anchor @classmethod diff --git a/docx/parts/document.py b/docx/parts/document.py index 5028b30b4..fea181af9 100644 --- a/docx/parts/document.py +++ b/docx/parts/document.py @@ -92,7 +92,8 @@ def new_pic_inline(self, image_descriptor, width, height): return self.new_pic(image_descriptor, width, height) def new_pic( - self, image_descriptor, width, height, position=None, wrap=None): + self, image_descriptor, width, height, + position=None, margin=None, wrap=None): """ Return a new `w:inline` or `w:anchor` element containing the image specified by *image_descriptor* and scaled based on the values of @@ -109,7 +110,7 @@ def new_pic( else: ShapeType = CT_Anchor return ShapeType.new_pic( - shape_id, rId, filename, cx, cy, position, wrap + shape_id, rId, filename, cx, cy, position, margin, wrap ) @property diff --git a/docx/text/run.py b/docx/text/run.py index 7dfbb9adb..0fd4df0e4 100644 --- a/docx/text/run.py +++ b/docx/text/run.py @@ -48,7 +48,7 @@ def add_break(self, break_type=WD_BREAK.LINE): def add_picture( self, image_path_or_stream, width=None, height=None, - position=None, wrap=None + position=None, margin=None, wrap=None ): """ Return an |InlineShape| instance containing the image identified by @@ -65,7 +65,7 @@ def add_picture( false if floated. """ image = self.part.new_pic( - image_path_or_stream, width, height, position, wrap + image_path_or_stream, width, height, position, margin, wrap ) self._r.add_drawing(image) From af65204cb2d2d96863418099032447cff85582a1 Mon Sep 17 00:00:00 2001 From: Jervis Whitley Date: Mon, 1 May 2017 18:33:18 +1000 Subject: [PATCH 08/14] Change alignment to absolute on paragraph --- docx/oxml/shape.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docx/oxml/shape.py b/docx/oxml/shape.py index e23fbc441..1ed50c4b2 100644 --- a/docx/oxml/shape.py +++ b/docx/oxml/shape.py @@ -142,10 +142,10 @@ def new(cls, cx, cy, shape_id, pic, position, margin=None, wrap=None): anchor.positionV.getchildren()[0].text = positionV if margin is not None: - anchor.distT = margin.get('top', 0) - anchor.distR = margin.get('right', 0) - anchor.distB = margin.get('bottom', 0) - anchor.distL = margin.get('left', 0) + anchor.set('distT', margin.get('top', 0)) + anchor.set('distR', margin.get('right', 0)) + anchor.set('distB', margin.get('bottom', 0)) + anchor.set('distL', margin.get('left', 0)) if wrap is not None: anchor.wrapSquare.set('wrapText', wrap) @@ -162,8 +162,8 @@ def _inline_xml(cls): ' \n' ' right\n' ' \n' - ' \n' - ' center\n' + ' \n' + ' 3810\n' ' \n' ' \n' ' \n' From ddd652b90fd89fffee0ac5f03e65ec2895744cd8 Mon Sep 17 00:00:00 2001 From: Jervis Whitley Date: Mon, 1 May 2017 18:36:32 +1000 Subject: [PATCH 09/14] unicode --- docx/oxml/shape.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docx/oxml/shape.py b/docx/oxml/shape.py index 1ed50c4b2..54cab8bf9 100644 --- a/docx/oxml/shape.py +++ b/docx/oxml/shape.py @@ -138,14 +138,14 @@ def new(cls, cx, cy, shape_id, pic, position, margin=None, wrap=None): ) anchor.graphic.graphicData._insert_pic(pic) positionH, positionV = position - anchor.positionH.getchildren()[0].text = positionH - anchor.positionV.getchildren()[0].text = positionV + anchor.positionH.getchildren()[0].text = unicode(positionH) + anchor.positionV.getchildren()[0].text = unicode(positionV) if margin is not None: - anchor.set('distT', margin.get('top', 0)) - anchor.set('distR', margin.get('right', 0)) - anchor.set('distB', margin.get('bottom', 0)) - anchor.set('distL', margin.get('left', 0)) + anchor.set('distT', unicode(margin.get('top', 0))) + anchor.set('distR', unicode(margin.get('right', 0))) + anchor.set('distB', unicode(margin.get('bottom', 0))) + anchor.set('distL', unicode(margin.get('left', 0))) if wrap is not None: anchor.wrapSquare.set('wrapText', wrap) From 6bff61938e14d94da4dd2c01b0716f1b78571640 Mon Sep 17 00:00:00 2001 From: Jervis Whitley Date: Tue, 2 May 2017 10:47:55 +1000 Subject: [PATCH 10/14] Margins should be ints not floats --- docx/oxml/shape.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docx/oxml/shape.py b/docx/oxml/shape.py index 54cab8bf9..c867ed994 100644 --- a/docx/oxml/shape.py +++ b/docx/oxml/shape.py @@ -142,10 +142,10 @@ def new(cls, cx, cy, shape_id, pic, position, margin=None, wrap=None): anchor.positionV.getchildren()[0].text = unicode(positionV) if margin is not None: - anchor.set('distT', unicode(margin.get('top', 0))) - anchor.set('distR', unicode(margin.get('right', 0))) - anchor.set('distB', unicode(margin.get('bottom', 0))) - anchor.set('distL', unicode(margin.get('left', 0))) + anchor.set('distT', u"%d" % margin.get('top', 0)) + anchor.set('distR', u"%d" % margin.get('right', 0)) + anchor.set('distB', u"%d" % margin.get('bottom', 0)) + anchor.set('distL', u"%d" % margin.get('left', 0)) if wrap is not None: anchor.wrapSquare.set('wrapText', wrap) From 45c0cbf3a8142e1367aa45fd524ccd085013a78f Mon Sep 17 00:00:00 2001 From: Jervis Whitley Date: Tue, 2 May 2017 14:22:34 +1000 Subject: [PATCH 11/14] Dont allowOverlap and vertical position from line --- docx/oxml/shape.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docx/oxml/shape.py b/docx/oxml/shape.py index c867ed994..31ad120b1 100644 --- a/docx/oxml/shape.py +++ b/docx/oxml/shape.py @@ -157,13 +157,13 @@ def _inline_xml(cls): return ( '\n' + ' layoutInCell="1" allowOverlap="0" %s>\n' ' \n' ' \n' ' right\n' ' \n' - ' \n' - ' 3810\n' + ' \n' + ' 0\n' ' \n' ' \n' ' \n' From 275ff9cb42787382fd492cbf82fef32a2623a159 Mon Sep 17 00:00:00 2001 From: Jervis Whitley Date: Wed, 3 May 2017 14:56:33 +1000 Subject: [PATCH 12/14] Wrap options based on enum Allows wrapSquare bothSides, wrapTopAndBottom --- docx/enum/shape.py | 19 +++++++++++++++++++ docx/oxml/__init__.py | 34 ++++++++++++++++++---------------- docx/oxml/shape.py | 39 +++++++++++++++++++++++++++++++++++---- 3 files changed, 72 insertions(+), 20 deletions(-) diff --git a/docx/enum/shape.py b/docx/enum/shape.py index 2f0224d3f..03c6019b9 100644 --- a/docx/enum/shape.py +++ b/docx/enum/shape.py @@ -6,6 +6,8 @@ from __future__ import absolute_import, print_function, unicode_literals +from .base import Enumeration, EnumMember + class WD_INLINE_SHAPE_TYPE(object): """ @@ -35,3 +37,20 @@ class WD_ANCHOR_SHAPE_TYPE(object): WD_ANCHOR_SHAPE = WD_ANCHOR_SHAPE_TYPE + + +class WRAP_SHAPE_TYPE(Enumeration): + + __ms_name__ = '' + __members__ = ( + + EnumMember( + 'wrapSquareBothSides', 'bothSides', + 'A square wrapped shape with text wrapping on both sides' + ), + + EnumMember( + 'wrapTopAndBottom', '', + 'A square on its own line cleared on left and right' + ) + ) diff --git a/docx/oxml/__init__.py b/docx/oxml/__init__.py index cd422734b..e35c4822d 100644 --- a/docx/oxml/__init__.py +++ b/docx/oxml/__init__.py @@ -96,23 +96,25 @@ def OxmlElement(nsptag_str, attrs=None, nsdecls=None): CT_Anchor, CT_Blip, CT_BlipFillProperties, CT_GraphicalObject, CT_GraphicalObjectData, CT_Inline, CT_NonVisualDrawingProps, CT_Picture, CT_PictureNonVisual, CT_Point2D, CT_PositiveSize2D, CT_ShapeProperties, - CT_Transform2D + CT_Transform2D, CT_WrapSquare, CT_WrapTopAndBottom ) -register_element_cls('a:blip', CT_Blip) -register_element_cls('a:ext', CT_PositiveSize2D) -register_element_cls('a:graphic', CT_GraphicalObject) -register_element_cls('a:graphicData', CT_GraphicalObjectData) -register_element_cls('a:off', CT_Point2D) -register_element_cls('a:xfrm', CT_Transform2D) -register_element_cls('pic:blipFill', CT_BlipFillProperties) -register_element_cls('pic:cNvPr', CT_NonVisualDrawingProps) -register_element_cls('pic:nvPicPr', CT_PictureNonVisual) -register_element_cls('pic:pic', CT_Picture) -register_element_cls('pic:spPr', CT_ShapeProperties) -register_element_cls('wp:docPr', CT_NonVisualDrawingProps) -register_element_cls('wp:extent', CT_PositiveSize2D) -register_element_cls('wp:inline', CT_Inline) -register_element_cls('wp:anchor', CT_Anchor) +register_element_cls('a:blip', CT_Blip) +register_element_cls('a:ext', CT_PositiveSize2D) +register_element_cls('a:graphic', CT_GraphicalObject) +register_element_cls('a:graphicData', CT_GraphicalObjectData) +register_element_cls('a:off', CT_Point2D) +register_element_cls('a:xfrm', CT_Transform2D) +register_element_cls('pic:blipFill', CT_BlipFillProperties) +register_element_cls('pic:cNvPr', CT_NonVisualDrawingProps) +register_element_cls('pic:nvPicPr', CT_PictureNonVisual) +register_element_cls('pic:pic', CT_Picture) +register_element_cls('pic:spPr', CT_ShapeProperties) +register_element_cls('wp:docPr', CT_NonVisualDrawingProps) +register_element_cls('wp:extent', CT_PositiveSize2D) +register_element_cls('wp:inline', CT_Inline) +register_element_cls('wp:anchor', CT_Anchor) +register_element_cls('wp:wrapSquare', CT_WrapSquare) +register_element_cls('wp:wrapTopAndBottom', CT_WrapTopAndBottom) from .styles import CT_LatentStyles, CT_LsdException, CT_Style, CT_Styles register_element_cls('w:basedOn', CT_String) diff --git a/docx/oxml/shape.py b/docx/oxml/shape.py index 31ad120b1..be59a8625 100644 --- a/docx/oxml/shape.py +++ b/docx/oxml/shape.py @@ -8,12 +8,13 @@ from .ns import nsdecls from .simpletypes import ( ST_Coordinate, ST_DrawingElementId, ST_PositiveCoordinate, - ST_RelationshipId, XsdString, XsdToken + ST_RelationshipId, XsdString, XsdStringEnumeration, XsdToken ) from .xmlchemy import ( BaseOxmlElement, OneAndOnlyOne, OptionalAttribute, RequiredAttribute, ZeroOrOne ) +from ..enum.shape import WRAP_SHAPE_TYPE class CT_Blip(BaseOxmlElement): @@ -34,6 +35,29 @@ class CT_BlipFillProperties(BaseOxmlElement): )) +class ST_WrapText(XsdStringEnumeration): + """ + Valid values for `wrapText/@val`. + """ + BOTHSIDES = 'bothSides' + + _members = (BOTHSIDES,) + + +class CT_WrapSquare(BaseOxmlElement): + """ + ```` element for wrapping text + around a shape + """ + wrapText = RequiredAttribute('wrapText', ST_WrapText) + + +class CT_WrapTopAndBottom(BaseOxmlElement): + """ + ```` element for setting image on its own. + """ + + class CT_GraphicalObject(BaseOxmlElement): """ ```` element, container for a DrawingML object @@ -121,6 +145,7 @@ class CT_Anchor(CT_Inline): positionV = OneAndOnlyOne('wp:positionV') effectExtent = OneAndOnlyOne('wp:effectExtent') wrapSquare = ZeroOrOne('wp:wrapSquare') + wrapTopAndBottom = ZeroOrOne('wp:wrapTopAndBottom') @classmethod def new(cls, cx, cy, shape_id, pic, position, margin=None, wrap=None): @@ -147,8 +172,15 @@ def new(cls, cx, cy, shape_id, pic, position, margin=None, wrap=None): anchor.set('distB', u"%d" % margin.get('bottom', 0)) anchor.set('distL', u"%d" % margin.get('left', 0)) - if wrap is not None: - anchor.wrapSquare.set('wrapText', wrap) + wrap_el = None + if wrap == WRAP_SHAPE_TYPE.wrapTopAndBottom: + wrap_el = anchor.get_or_add_wrapTopAndBottom() + elif wrap == WRAP_SHAPE_TYPE.wrapSquareBothSides: + wrap_el = anchor.get_or_add_wrapSquare() + wrap_el.wrapText = ST_WrapText.BOTHSIDES + + if wrap_el is not None: + anchor.insert_element_before(wrap_el, 'wp:effectExtent') return anchor @@ -167,7 +199,6 @@ def _inline_xml(cls): ' \n' ' \n' ' \n' - ' \n' ' \n' ' \n' ' \n' From 09b3097ee399bdf966d8ffad3b4b0a2121a7c90b Mon Sep 17 00:00:00 2001 From: Jervis Whitley Date: Wed, 3 May 2017 16:16:04 +1000 Subject: [PATCH 13/14] Support cleared images Allow setting positionH to None, which would correspond to neither left or right float, but put directly where the character was. clears text to both top and bottom so the image sits on its own on the line --- docx/oxml/__init__.py | 5 +++-- docx/oxml/shape.py | 30 ++++++++++++++++++++++++++++-- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/docx/oxml/__init__.py b/docx/oxml/__init__.py index e35c4822d..d0f9c7541 100644 --- a/docx/oxml/__init__.py +++ b/docx/oxml/__init__.py @@ -95,8 +95,8 @@ def OxmlElement(nsptag_str, attrs=None, nsdecls=None): from .shape import ( CT_Anchor, CT_Blip, CT_BlipFillProperties, CT_GraphicalObject, CT_GraphicalObjectData, CT_Inline, CT_NonVisualDrawingProps, CT_Picture, - CT_PictureNonVisual, CT_Point2D, CT_PositiveSize2D, CT_ShapeProperties, - CT_Transform2D, CT_WrapSquare, CT_WrapTopAndBottom + CT_PictureNonVisual, CT_Point2D, CT_PosH, CT_PositiveSize2D, + CT_ShapeProperties, CT_Transform2D, CT_WrapSquare, CT_WrapTopAndBottom ) register_element_cls('a:blip', CT_Blip) register_element_cls('a:ext', CT_PositiveSize2D) @@ -113,6 +113,7 @@ def OxmlElement(nsptag_str, attrs=None, nsdecls=None): register_element_cls('wp:extent', CT_PositiveSize2D) register_element_cls('wp:inline', CT_Inline) register_element_cls('wp:anchor', CT_Anchor) +register_element_cls('wp:positionH', CT_PosH) register_element_cls('wp:wrapSquare', CT_WrapSquare) register_element_cls('wp:wrapTopAndBottom', CT_WrapTopAndBottom) diff --git a/docx/oxml/shape.py b/docx/oxml/shape.py index be59a8625..0f246d7f0 100644 --- a/docx/oxml/shape.py +++ b/docx/oxml/shape.py @@ -44,6 +44,14 @@ class ST_WrapText(XsdStringEnumeration): _members = (BOTHSIDES,) +class ST_RelFromH(XsdStringEnumeration): + """ + Valid values for `relativeFrom/@val` in CT_PosH. + """ + MARGIN = 'margin' + CHARACTER = 'character' + + class CT_WrapSquare(BaseOxmlElement): """ ```` element for wrapping text @@ -73,6 +81,16 @@ class CT_GraphicalObjectData(BaseOxmlElement): uri = RequiredAttribute('uri', XsdToken) +class CT_PosH(BaseOxmlElement): + """ + ```` for setting how shapes are + horizontally positioned. + """ + relativeFrom = RequiredAttribute('relativeFrom', ST_RelFromH) + align = ZeroOrOne('wp:align') + posOffset = ZeroOrOne('wp:posOffset') + + class CT_Inline(BaseOxmlElement): """ ```` element, container for an inline shape. @@ -163,7 +181,16 @@ def new(cls, cx, cy, shape_id, pic, position, margin=None, wrap=None): ) anchor.graphic.graphicData._insert_pic(pic) positionH, positionV = position - anchor.positionH.getchildren()[0].text = unicode(positionH) + + if positionH is None: + anchor.positionH.set('relativeFrom', 'character') + pos = anchor.positionH._add_posOffset() + pos.text = "0" + else: + anchor.positionH.set('relativeFrom', 'margin') + align = anchor.positionH._add_align() + align.text = unicode(positionH) + anchor.positionV.getchildren()[0].text = unicode(positionV) if margin is not None: @@ -192,7 +219,6 @@ def _inline_xml(cls): ' layoutInCell="1" allowOverlap="0" %s>\n' ' \n' ' \n' - ' right\n' ' \n' ' \n' ' 0\n' From 046fddc6e135888552ec3db451525e6cdf3c54b8 Mon Sep 17 00:00:00 2001 From: Jervis Whitley Date: Wed, 18 Oct 2017 14:30:03 +1100 Subject: [PATCH 14/14] remove unicode --- docx/oxml/shape.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docx/oxml/shape.py b/docx/oxml/shape.py index 0f246d7f0..9fad76903 100644 --- a/docx/oxml/shape.py +++ b/docx/oxml/shape.py @@ -189,9 +189,9 @@ def new(cls, cx, cy, shape_id, pic, position, margin=None, wrap=None): else: anchor.positionH.set('relativeFrom', 'margin') align = anchor.positionH._add_align() - align.text = unicode(positionH) + align.text = positionH - anchor.positionV.getchildren()[0].text = unicode(positionV) + anchor.positionV.getchildren()[0].text = positionV if margin is not None: anchor.set('distT', u"%d" % margin.get('top', 0))