diff --git a/docx/oxml/__init__.py b/docx/oxml/__init__.py index 093c1b45b..f2b4de40a 100644 --- a/docx/oxml/__init__.py +++ b/docx/oxml/__init__.py @@ -219,6 +219,17 @@ def OxmlElement(nsptag_str, attrs=None, nsdecls=None): register_element_cls('w:vertAlign', CT_VerticalAlignRun) register_element_cls('w:webHidden', CT_OnOff) +from .text.border import ( # noqa + CT_BorderSide, + CT_PBdr, +) +register_element_cls('w:top', CT_BorderSide) +register_element_cls('w:left', CT_BorderSide) +register_element_cls('w:bottom', CT_BorderSide) +register_element_cls('w:right', CT_BorderSide) +register_element_cls('w:pBdr', CT_PBdr) + + from .text.paragraph import CT_P # noqa register_element_cls('w:p', CT_P) diff --git a/docx/oxml/simpletypes.py b/docx/oxml/simpletypes.py index 400a23700..e3fdf8e9b 100644 --- a/docx/oxml/simpletypes.py +++ b/docx/oxml/simpletypes.py @@ -11,7 +11,7 @@ ) from ..exceptions import InvalidXmlError -from ..shared import Emu, Pt, RGBColor, Twips +from ..shared import Length, Emu, Pt, RGBColor, Twips, EighthsOfPoint class BaseSimpleType(object): @@ -288,6 +288,22 @@ def convert_to_xml(cls, value): half_points = int(emu.pt * 2) return str(half_points) +class ST_PtsMeasure(XsdUnsignedLong): + """ + Point measure. + """ + @classmethod + def convert_from_xml(cls, str_value): + if 'm' in str_value or 'n' in str_value or 'p' in str_value: + return ST_UniversalMeasure.convert_from_xml(str_value) + return Pt(int(str_value)) + + @classmethod + def convert_to_xml(cls, value): + emu = Emu(value) + points = int(emu.pt) + return str(points) + class ST_Merge(XsdStringEnumeration): """ @@ -383,6 +399,20 @@ def convert_to_xml(cls, value): twips = emu.twips return str(twips) +class ST_EighthsOfPointMeasure(XsdUnsignedLong): + + @classmethod + def convert_from_xml(cls, str_value): + if 'i' in str_value or 'm' in str_value or 'p' in str_value: + return ST_UniversalMeasure.convert_from_xml(str_value) + return EighthsOfPoint(int(str_value)) + + @classmethod + def convert_to_xml(cls, value): + emu = Emu(value) + eop = emu.eighths_of_point + return str(eop) + class ST_UniversalMeasure(BaseSimpleType): @@ -391,8 +421,12 @@ def convert_from_xml(cls, str_value): float_part, units_part = str_value[:-2], str_value[-2:] quantity = float(float_part) multiplier = { - 'mm': 36000, 'cm': 360000, 'in': 914400, 'pt': 12700, - 'pc': 152400, 'pi': 152400 + 'mm': Length._EMUS_PER_MM, + 'cm': Length._EMUS_PER_CM, + 'in': 914400, + 'pt': Length._EMUS_PER_PT, + 'pc': 152400, + 'pi': 152400 }[units_part] emu_value = Emu(int(round(quantity * multiplier))) return emu_value @@ -407,3 +441,209 @@ class ST_VerticalAlignRun(XsdStringEnumeration): SUBSCRIPT = 'subscript' _members = (BASELINE, SUPERSCRIPT, SUBSCRIPT) + +class ST_BorderValue(XsdStringEnumeration): + """ + Valid values for val attribute of children of ```` elements. + These values come from the .NET OpenXML documentation at + https://docs.microsoft.com/en-us/dotnet/api/documentformat.openxml.wordprocessing.bordervalues?view=openxml-2.8.1 + """ + APPLES = "apples" + ARCHEDSCALLOPS = "archedScallops" + BABYPACIFIER = "babyPacifier" + BABYRATTLE = "babyRattle" + BALLOONS3COLORS = "balloons3Colors" + BALLOONSHOTAIR = "balloonsHotAir" + BASICBLACKDASHES = "basicBlackDashes" + BASICBLACKDOTS = "basicBlackDots" + BASICBLACKSQUARES = "basicBlackSquares" + BASICTHINLINES = "basicThinLines" + BASICWHITEDASHES = "basicWhiteDashes" + BASICWHITEDOTS = "basicWhiteDots" + BASICWHITESQUARES = "basicWhiteSquares" + BASICWIDEINLINE = "basicWideInline" + BASICWIDEMIDLINE = "basicWideMidline" + BASICWIDEOUTLINE = "basicWideOutline" + BATS = "bats" + BIRDS = "birds" + BIRDSFLIGHT = "birdsFlight" + CABINS = "cabins" + CAKESLICE = "cakeSlice" + CANDYCORN = "candyCorn" + CELTICKNOTWORK = "celticKnotwork" + CERTIFICATEBANNER = "certificateBanner" + CHAINLINK = "chainLink" + CHAMPAGNEBOTTLE = "champagneBottle" + CHECKEDBARBLACK = "checkedBarBlack" + CHECKEDBARCOLOR = "checkedBarColor" + CHECKERED = "checkered" + CHRISTMASTREE = "christmasTree" + CIRCLESLINES = "circlesLines" + CIRCLESRECTANGLES = "circlesRectangles" + CLASSICALWAVE = "classicalWave" + CLOCKS = "clocks" + COMPASS = "compass" + CONFETTI = "confetti" + CONFETTIGRAYS = "confettiGrays" + CONFETTIOUTLINE = "confettiOutline" + CONFETTISTREAMERS = "confettiStreamers" + CONFETTIWHITE = "confettiWhite" + CORNERTRIANGLES = "cornerTriangles" + COUPONCUTOUTDASHES = "couponCutoutDashes" + COUPONCUTOUTDOTS = "couponCutoutDots" + CRAZYMAZE = "crazyMaze" + CREATURESBUTTERFLY = "creaturesButterfly" + CREATURESFISH = "creaturesFish" + CREATURESINSECTS = "creaturesInsects" + CREATURESLADYBUG = "creaturesLadyBug" + CROSSSTITCH = "crossStitch" + CUP = "cup" + DASHDOTSTROKED = "dashDotStroked" + DASHED = "dashed" + DASHSMALLGAP = "dashSmallGap" + DECOARCH = "decoArch" + DECOARCHCOLOR = "decoArchColor" + DECOBLOCKS = "decoBlocks" + DIAMONDSGRAY = "diamondsGray" + DOTDASH = "dotDash" + DOTDOTDASH = "dotDotDash" + DOTTED = "dotted" + DOUBLE = "double" + DOUBLED = "doubleD" + DOUBLEDIAMONDS = "doubleDiamonds" + DOUBLEWAVE = "doubleWave" + EARTH1 = "earth1" + EARTH2 = "earth2" + ECLIPSINGSQUARES1 = "eclipsingSquares1" + ECLIPSINGSQUARES2 = "eclipsingSquares2" + EGGSBLACK = "eggsBlack" + FANS = "fans" + FILM = "film" + FIRECRACKERS = "firecrackers" + FLOWERSBLOCKPRINT = "flowersBlockPrint" + FLOWERSDAISIES = "flowersDaisies" + FLOWERSMODERN1 = "flowersModern1" + FLOWERSMODERN2 = "flowersModern2" + FLOWERSPANSY = "flowersPansy" + FLOWERSREDROSE = "flowersRedRose" + FLOWERSROSES = "flowersRoses" + FLOWERSTEACUP = "flowersTeacup" + FLOWERSTINY = "flowersTiny" + GEMS = "gems" + GINGERBREADMAN = "gingerbreadMan" + GRADIENT = "gradient" + HANDMADE1 = "handmade1" + HANDMADE2 = "handmade2" + HEARTBALLOON = "heartBalloon" + HEARTGRAY = "heartGray" + HEARTS = "hearts" + HEEBIEJEEBIES = "heebieJeebies" + HOLLY = "holly" + HOUSEFUNKY = "houseFunky" + HYPNOTIC = "hypnotic" + ICECREAMCONES = "iceCreamCones" + INSET = "inset" + LIGHTBULB = "lightBulb" + LIGHTNING1 = "lightning1" + LIGHTNING2 = "lightning2" + MAPLELEAF = "mapleLeaf" + MAPLEMUFFINS = "mapleMuffins" + MAPPINS = "mapPins" + MARQUEE = "marquee" + MARQUEETOOTHED = "marqueeToothed" + MOONS = "moons" + MOSAIC = "mosaic" + MUSICNOTES = "musicNotes" + NIL = "nil" + NONE = "none" + NORTHWEST = "northwest" + OUTSET = "outset" + OVALS = "ovals" + PACKAGES = "packages" + PALMSBLACK = "palmsBlack" + PALMSCOLOR = "palmsColor" + PAPERCLIPS = "paperClips" + PAPYRUS = "papyrus" + PARTYFAVOR = "partyFavor" + PARTYGLASS = "partyGlass" + PENCILS = "pencils" + PEOPLE = "people" + PEOPLEHATS = "peopleHats" + PEOPLEWAVING = "peopleWaving" + POINSETTIAS = "poinsettias" + POSTAGESTAMP = "postageStamp" + PUMPKIN1 = "pumpkin1" + PUSHPINNOTE1 = "pushPinNote1" + PUSHPINNOTE2 = "pushPinNote2" + PYRAMIDS = "pyramids" + PYRAMIDSABOVE = "pyramidsAbove" + QUADRANTS = "quadrants" + RINGS = "rings" + SAFARI = "safari" + SAWTOOTH = "sawtooth" + SAWTOOTHGRAY = "sawtoothGray" + SCAREDCAT = "scaredCat" + SEATTLE = "seattle" + SHADOWEDSQUARES = "shadowedSquares" + SHAPES1 = "shapes1" + SHAPES2 = "shapes2" + SHARKSTEETH = "sharksTeeth" + SHOREBIRDTRACKS = "shorebirdTracks" + SINGLE = "single" + SKYROCKET = "skyrocket" + SNOWFLAKEFANCY = "snowflakeFancy" + SNOWFLAKES = "snowflakes" + SOMBRERO = "sombrero" + SOUTHWEST = "southwest" + STARS = "stars" + STARS3D = "stars3d" + STARSBLACK = "starsBlack" + STARSSHADOWED = "starsShadowed" + STARSTOP = "starsTop" + SUN = "sun" + SWIRLIGIG = "swirligig" + THICK = "thick" + THICKTHINLARGEGAP = "thickThinLargeGap" + THICKTHINMEDIUMGAP = "thickThinMediumGap" + THICKTHINSMALLGAP = "thickThinSmallGap" + THINTHICKLARGEGAP = "thinThickLargeGap" + THINTHICKMEDIUMGAP = "thinThickMediumGap" + THINTHICKSMALLGAP = "thinThickSmallGap" + THINTHICKTHINLARGEGAP = "thinThickThinLargeGap" + THINTHICKTHINMEDIUMGAP = "thinThickThinMediumGap" + THINTHICKTHINSMALLGAP = "thinThickThinSmallGap" + THREEDEMBOSS = "threeDEmboss" + THREEDENGRAVE = "threeDEngrave" + TORNPAPER = "tornPaper" + TORNPAPERBLACK = "tornPaperBlack" + TREES = "trees" + TRIANGLE1 = "triangle1" + TRIANGLE2 = "triangle2" + TRIANGLECIRCLE1 = "triangleCircle1" + TRIANGLECIRCLE2 = "triangleCircle2" + TRIANGLEPARTY = "triangleParty" + TRIANGLES = "triangles" + TRIBAL1 = "tribal1" + TRIBAL2 = "tribal2" + TRIBAL3 = "tribal3" + TRIBAL4 = "tribal4" + TRIBAL5 = "tribal5" + TRIBAL6 = "tribal6" + TRIPLE = "triple" + TWISTEDLINES1 = "twistedLines1" + TWISTEDLINES2 = "twistedLines2" + VINE = "vine" + WAVE = "wave" + WAVELINE = "waveline" + WEAVINGANGLES = "weavingAngles" + WEAVINGBRAID = "weavingBraid" + WEAVINGRIBBON = "weavingRibbon" + WEAVINGSTRIPS = "weavingStrips" + WHITEFLOWERS = "whiteFlowers" + WOODWORK = "woodwork" + XILLUSIONS = "xIllusions" + ZANYTRIANGLES = "zanyTriangles" + ZIGZAG = "zigZag" + ZIGZAGSTITCH = "zigZagStitch" + + _members = (APPLES, ARCHEDSCALLOPS, BABYPACIFIER, BABYRATTLE, BALLOONS3COLORS, BALLOONSHOTAIR, BASICBLACKDASHES, BASICBLACKDOTS, BASICBLACKSQUARES, BASICTHINLINES, BASICWHITEDASHES, BASICWHITEDOTS, BASICWHITESQUARES, BASICWIDEINLINE, BASICWIDEMIDLINE, BASICWIDEOUTLINE, BATS, BIRDS, BIRDSFLIGHT, CABINS, CAKESLICE, CANDYCORN, CELTICKNOTWORK, CERTIFICATEBANNER, CHAINLINK, CHAMPAGNEBOTTLE, CHECKEDBARBLACK, CHECKEDBARCOLOR, CHECKERED, CHRISTMASTREE, CIRCLESLINES, CIRCLESRECTANGLES, CLASSICALWAVE, CLOCKS, COMPASS, CONFETTI, CONFETTIGRAYS, CONFETTIOUTLINE, CONFETTISTREAMERS, CONFETTIWHITE, CORNERTRIANGLES, COUPONCUTOUTDASHES, COUPONCUTOUTDOTS, CRAZYMAZE, CREATURESBUTTERFLY, CREATURESFISH, CREATURESINSECTS, CREATURESLADYBUG, CROSSSTITCH, CUP, DASHDOTSTROKED, DASHED, DASHSMALLGAP, DECOARCH, DECOARCHCOLOR, DECOBLOCKS, DIAMONDSGRAY, DOTDASH, DOTDOTDASH, DOTTED, DOUBLE, DOUBLED, DOUBLEDIAMONDS, DOUBLEWAVE, EARTH1, EARTH2, ECLIPSINGSQUARES1, ECLIPSINGSQUARES2, EGGSBLACK, FANS, FILM, FIRECRACKERS, FLOWERSBLOCKPRINT, FLOWERSDAISIES, FLOWERSMODERN1, FLOWERSMODERN2, FLOWERSPANSY, FLOWERSREDROSE, FLOWERSROSES, FLOWERSTEACUP, FLOWERSTINY, GEMS, GINGERBREADMAN, GRADIENT, HANDMADE1, HANDMADE2, HEARTBALLOON, HEARTGRAY, HEARTS, HEEBIEJEEBIES, HOLLY, HOUSEFUNKY, HYPNOTIC, ICECREAMCONES, INSET, LIGHTBULB, LIGHTNING1, LIGHTNING2, MAPLELEAF, MAPLEMUFFINS, MAPPINS, MARQUEE, MARQUEETOOTHED, MOONS, MOSAIC, MUSICNOTES, NIL, NONE, NORTHWEST, OUTSET, OVALS, PACKAGES, PALMSBLACK, PALMSCOLOR, PAPERCLIPS, PAPYRUS, PARTYFAVOR, PARTYGLASS, PENCILS, PEOPLE, PEOPLEHATS, PEOPLEWAVING, POINSETTIAS, POSTAGESTAMP, PUMPKIN1, PUSHPINNOTE1, PUSHPINNOTE2, PYRAMIDS, PYRAMIDSABOVE, QUADRANTS, RINGS, SAFARI, SAWTOOTH, SAWTOOTHGRAY, SCAREDCAT, SEATTLE, SHADOWEDSQUARES, SHAPES1, SHAPES2, SHARKSTEETH, SHOREBIRDTRACKS, SINGLE, SKYROCKET, SNOWFLAKEFANCY, SNOWFLAKES, SOMBRERO, SOUTHWEST, STARS, STARS3D, STARSBLACK, STARSSHADOWED, STARSTOP, SUN, SWIRLIGIG, THICK, THICKTHINLARGEGAP, THICKTHINMEDIUMGAP, THICKTHINSMALLGAP, THINTHICKLARGEGAP, THINTHICKMEDIUMGAP, THINTHICKSMALLGAP, THINTHICKTHINLARGEGAP, THINTHICKTHINMEDIUMGAP, THINTHICKTHINSMALLGAP, THREEDEMBOSS, THREEDENGRAVE, TORNPAPER, TORNPAPERBLACK, TREES, TRIANGLE1, TRIANGLE2, TRIANGLECIRCLE1, TRIANGLECIRCLE2, TRIANGLEPARTY, TRIANGLES, TRIBAL1, TRIBAL2, TRIBAL3, TRIBAL4, TRIBAL5, TRIBAL6, TRIPLE, TWISTEDLINES1, TWISTEDLINES2, VINE, WAVE, WAVELINE, WEAVINGANGLES, WEAVINGBRAID, WEAVINGRIBBON, WEAVINGSTRIPS, WHITEFLOWERS, WOODWORK, XILLUSIONS, ZANYTRIANGLES, ZIGZAG, ZIGZAGSTITCH) diff --git a/docx/oxml/text/border.py b/docx/oxml/text/border.py new file mode 100644 index 000000000..815378872 --- /dev/null +++ b/docx/oxml/text/border.py @@ -0,0 +1,29 @@ +from ..xmlchemy import ( + BaseOxmlElement, OneOrMore, OptionalAttribute, RequiredAttribute, + ZeroOrOne +) +from docx.oxml.simpletypes import (ST_BorderValue, ST_EighthsOfPointMeasure, ST_PtsMeasure, + ST_HexColor, XsdBoolean) + +class CT_BorderSide(BaseOxmlElement): + """ + Class representing children of ```` elements. + """ + val = RequiredAttribute("w:val",ST_BorderValue) + sz = RequiredAttribute("w:sz",ST_EighthsOfPointMeasure) + space = RequiredAttribute("w:space",ST_PtsMeasure) + color = RequiredAttribute("w:color",ST_HexColor) + shadow = OptionalAttribute("w:shadow",XsdBoolean) + + +class CT_PBdr(BaseOxmlElement): + """ + ```` element, containing paragraph border information. + """ + _tag_seq = ('w:top', 'w:left', 'w:bottom', 'w:right') + top = ZeroOrOne('w:top', successors=_tag_seq[1:]) + left = ZeroOrOne('w:left', successors=_tag_seq[2:]) + bottom = ZeroOrOne('w:bottom', successors=_tag_seq[3:]) + right = ZeroOrOne('w:right', successors=_tag_seq[4:]) + del _tag_seq + diff --git a/docx/oxml/text/parfmt.py b/docx/oxml/text/parfmt.py index 466b11b1b..4ef0dc7ea 100644 --- a/docx/oxml/text/parfmt.py +++ b/docx/oxml/text/parfmt.py @@ -32,6 +32,10 @@ class CT_Jc(BaseOxmlElement): val = RequiredAttribute('w:val', WD_ALIGN_PARAGRAPH) + + + + class CT_PPr(BaseOxmlElement): """ ```` element, containing the properties for a paragraph. @@ -53,6 +57,7 @@ class CT_PPr(BaseOxmlElement): pageBreakBefore = ZeroOrOne('w:pageBreakBefore', successors=_tag_seq[4:]) widowControl = ZeroOrOne('w:widowControl', successors=_tag_seq[6:]) numPr = ZeroOrOne('w:numPr', successors=_tag_seq[7:]) + pBdr = ZeroOrOne('w:pBdr', successors=_tag_seq[9:]) tabs = ZeroOrOne('w:tabs', successors=_tag_seq[11:]) spacing = ZeroOrOne('w:spacing', successors=_tag_seq[22:]) ind = ZeroOrOne('w:ind', successors=_tag_seq[23:]) @@ -193,6 +198,21 @@ def pageBreakBefore_val(self, value): else: self.get_or_add_pageBreakBefore().val = value + + # @property + # def border_top(self): + # #return self.border + # border = self.border + # if border is None: + # return None + # return border.top + + # @border.setter + # def border_top(self, value): + # if value is None and self.border is None: + # return + # self.get_or_add_spacing().top = value + @property def spacing_after(self): """ diff --git a/docx/shared.py b/docx/shared.py index 919964325..17ed80fad 100644 --- a/docx/shared.py +++ b/docx/shared.py @@ -19,6 +19,7 @@ class Length(int): _EMUS_PER_MM = 36000 _EMUS_PER_PT = 12700 _EMUS_PER_TWIP = 635 + _EMUS_PER_EIGHTH_OF_POINT = _EMUS_PER_PT/8 def __new__(cls, emu): return int.__new__(cls, emu) @@ -65,6 +66,13 @@ def twips(self): """ return int(round(self / float(self._EMUS_PER_TWIP))) + @property + def eighths_of_point(self): + """ + The equivalent length expressed in eighths of a point. + """ + return int(round(self / float(self._EMUS_PER_EIGHTH_OF_POINT))) + class Inches(Length): """ @@ -123,6 +131,13 @@ def __new__(cls, twips): emu = int(twips * Length._EMUS_PER_TWIP) return Length.__new__(cls, emu) +class EighthsOfPoint(Length): + """ + Convenience constructor for length in eighths of a point. + """ + def __new__(cls, eighths): + emu = int(eighths*Length._EMUS_PER_EIGHTH_OF_POINT) + return Length.__new__(cls, emu) class RGBColor(tuple): """ diff --git a/docx/text/border.py b/docx/text/border.py new file mode 100644 index 000000000..54cee180f --- /dev/null +++ b/docx/text/border.py @@ -0,0 +1,251 @@ +from ..shared import ElementProxy, lazyproperty + +class BorderSide(ElementProxy): + + def __init__(self, element, side, parent=None): + super(BorderSide, self).__init__(element, parent) + self._side = side + + def _get_side_element_from_pBdr(self, pBdr): + return getattr(pBdr, self._side) + + def _get_or_add_side_element_from_pBdr(self, pBdr): + return getattr(pBdr, "get_or_add_%s" % self._side)() + + def _get_side_element(self): + pPr = self._element.pPr + if pPr is None: + return None + pBdr = pPr.pBdr + if pBdr is None: + return None + side = self._get_side_element_from_pBdr(pBdr) + return side + + def _get_or_add_side_element(self): + pPr = self._element.get_or_add_pPr() + if pPr is None: + return None + pBdr = pPr.get_or_add_pBdr() + if pBdr is None: + return None + side = self._get_or_add_side_element_from_pBdr(pBdr) + return side + + @property + def side(self): + """ + Returns a string representing the side to whichthis |BorderSide| + object belongs. + """ + return self._side + + @property + def val(self): + """ + An |ST_BorderValue| object representing the style of the border. + """ + side = self._get_side_element() + if side is None: + return None + return side.val + + @val.setter + def val(self,val): + side = self._get_or_add_side_element() + side.val = val + + @property + def sz(self): + """ + An |ST_EighthsOfPointMeasure| object representing the size of the + border in eighths of a point. + """ + side = self._get_side_element() + if side is None: + return None + return side.sz + + @sz.setter + def sz(self,sz): + side = self._get_or_add_side_element() + side.sz = sz + + @property + def space(self): + """ + An |ST_PtsMeasure| object representing the spacing offset for the + border, in Points. + """ + side = self._get_side_element() + if side is None: + return None + return side.space + + @space.setter + def space(self,space): + side = self._get_or_add_side_element() + side.space = space + + @property + def color(self): + """ + An |ST_HexColor| object representing the color of the border. + """ + side = self._get_side_element() + if side is None: + return None + return side.color + + @color.setter + def color(self,color): + side = self._get_or_add_side_element() + side.color = color + + @property + def shadow(self): + """ + An |XsdBoolean| object representing specifying whether a shadow is + displayed on the border. + """ + side = self._get_side_element() + if side is None: + return None + return side.shadow + + @shadow.setter + def shadow(self,shadow): + side = self._get_or_add_side_element() + side.shadow = shadow + + +class ParagraphBorder(ElementProxy): + __slots__ = ('_top','_left','_bottom','_right',) + + @property + def top(self): + """ + The |BorderSide| object representing the top border of the + paragraph. + """ + try: + return self._top + except AttributeError: + pPr = self._element.pPr + if pPr is None: + return None + pBdr = self._element.pPr.pBdr + if pBdr is None: + return None + top = pBdr.top + if top is None: + return None + self._top = BorderSide(self._element, "top") + return self._top + + @top.setter + def top(self, side): + if self.top is None: + self._top = BorderSide(self._element, "top") + top = self.top + top.val = side.val + top.sz = side.sz + top.space = side.space + top.color = side.color + top.shadow = side.shadow + + @property + def left(self): + """ + The |BorderSide| object representing the left border of the + paragraph. + """ + try: + return self._left + except AttributeError: + pPr = self._element.pPr + if pPr is None: + return None + pBdr = self._element.pPr.pBdr + if pBdr is None: + return None + left = pBdr.left + if left is None: + return None + self._left = BorderSide(self._element, "left") + return self._left + + @left.setter + def left(self, side): + if self.left is None: + self._left = BorderSide(self._element, "left") + left = self.left + left.val = side.val + left.sz = side.sz + left.space = side.space + left.color = side.color + left.shadow = side.shadow + + @property + def bottom(self): + """ + The |BorderSide| object representing the bottom border of the + paragraph. + """ + try: + return self._bottom + except AttributeError: + pPr = self._element.pPr + if pPr is None: + return None + pBdr = self._element.pPr.pBdr + if pBdr is None: + return None + bottom = pBdr.bottom + if bottom is None: + return None + self._bottom = BorderSide(self._element, "bottom") + return self._bottom + + @bottom.setter + def bottom(self, side): + if self.bottom is None: + self._bottom = BorderSide(self._element, "bottom") + bottom = self.bottom + bottom.val = side.val + bottom.sz = side.sz + bottom.space = side.space + bottom.color = side.color + bottom.shadow = side.shadow + + @property + def right(self): + """ + The |BorderSide| object representing the right border of the + paragraph. + """ + try: + return self._right + except AttributeError: + pPr = self._element.pPr + if pPr is None: + return None + pBdr = self._element.pPr.pBdr + if pBdr is None: + return None + right = pBdr.right + if right is None: + return None + self._right = BorderSide(self._element, "right") + return self._right + + @right.setter + def right(self, side): + if self.right is None: + self._right = BorderSide(self._element, "right") + right = self.right + right.val = side.val + right.sz = side.sz + right.space = side.space + right.color = side.color + right.shadow = side.shadow diff --git a/docx/text/parfmt.py b/docx/text/parfmt.py index 37206729c..b63425273 100644 --- a/docx/text/parfmt.py +++ b/docx/text/parfmt.py @@ -11,6 +11,7 @@ from ..enum.text import WD_LINE_SPACING from ..shared import ElementProxy, Emu, lazyproperty, Length, Pt, Twips from .tabstops import TabStops +from .border import ParagraphBorder class ParagraphFormat(ElementProxy): @@ -20,7 +21,7 @@ class ParagraphFormat(ElementProxy): control. """ - __slots__ = ('_tab_stops',) + __slots__ = ('_tab_stops','_border',) @property def alignment(self): @@ -39,6 +40,33 @@ def alignment(self, value): pPr = self._element.get_or_add_pPr() pPr.jc_val = value + @property + def border(self): + """ + |ParagraphBorder| object representing the borders of the paragraph. + """ + try: + return self._border + except AttributeError: + pPr = self._element.pPr + if pPr is None: + return None + pBdr = self._element.pPr.pBdr + if pBdr is None: + return None + self._border = ParagraphBorder(self._element) + return self._border + + @border.setter + def border(self, value): + if self.border is None: + self._border = ParagraphBorder(self._element) + border = self.border + border.top = value.top + border.left = value.left + border.bottom = value.bottom + border.right = value.right + @property def first_line_indent(self): """