diff --git a/pptx/oxml/__init__.py b/pptx/oxml/__init__.py index 4c1d1b699..f02f7b7e2 100644 --- a/pptx/oxml/__init__.py +++ b/pptx/oxml/__init__.py @@ -119,6 +119,7 @@ def register_element_cls(nsptagname, cls): register_element_cls('a:hslClr', CT_HslColor) register_element_cls('a:lumMod', CT_Percentage) register_element_cls('a:lumOff', CT_Percentage) +register_element_cls('a:alpha', CT_Percentage) register_element_cls('a:prstClr', CT_PresetColor) register_element_cls('a:schemeClr', CT_SchemeColor) register_element_cls('a:scrgbClr', CT_ScRgbColor) @@ -173,6 +174,8 @@ def register_element_cls(nsptagname, cls): register_element_cls('p:sldLayoutIdLst', CT_SlideLayoutIdList) register_element_cls('p:sldMaster', CT_SlideMaster) +from .parts.notes_slide import CT_NotesSlide +register_element_cls('p:notes', CT_NotesSlide) from .shapes.autoshape import ( CT_GeomGuide, CT_GeomGuideList, CT_NonVisualDrawingShapeProps, @@ -209,6 +212,20 @@ def register_element_cls(nsptagname, cls): register_element_cls('p:nvGrpSpPr', CT_GroupShapeNonVisual) register_element_cls('p:spTree', CT_GroupShape) +from .shapes.cust_geom import ( + CT_CustomGeometry2D, CT_Path2DList, CT_Path2D, CT_Path2DMoveTo, + CT_Path2DLineTo, CT_Path2DCubicBezierTo, CT_Path2DClose, CT_AdjPoint2D, + CT_Path2DArcTo +) +register_element_cls('a:custGeom', CT_CustomGeometry2D) +register_element_cls('a:pathLst', CT_Path2DList) +register_element_cls('a:path', CT_Path2D) +register_element_cls('a:moveTo', CT_Path2DMoveTo) +register_element_cls('a:lnTo', CT_Path2DLineTo) +register_element_cls('a:cubicBezTo', CT_Path2DCubicBezierTo) +register_element_cls('a:arcTo', CT_Path2DArcTo) +register_element_cls('a:close', CT_Path2DClose) +register_element_cls('a:pt', CT_AdjPoint2D) from .shapes.picture import CT_Picture, CT_PictureNonVisual register_element_cls('p:blipFill', CT_BlipFillProperties) @@ -218,19 +235,22 @@ def register_element_cls(nsptagname, cls): from .shapes.shared import ( CT_ApplicationNonVisualDrawingProps, CT_LineProperties, - CT_NonVisualDrawingProps, CT_Placeholder, CT_Point2D, CT_PositiveSize2D, + CT_PresetLineDashProperties, CT_NonVisualDrawingProps, + CT_Placeholder, CT_Point2D, CT_PositiveSize2D, CT_ShapeProperties, CT_Transform2D ) -register_element_cls('a:ext', CT_PositiveSize2D) -register_element_cls('a:ln', CT_LineProperties) -register_element_cls('a:off', CT_Point2D) -register_element_cls('a:xfrm', CT_Transform2D) -register_element_cls('c:spPr', CT_ShapeProperties) -register_element_cls('p:cNvPr', CT_NonVisualDrawingProps) -register_element_cls('p:nvPr', CT_ApplicationNonVisualDrawingProps) -register_element_cls('p:ph', CT_Placeholder) -register_element_cls('p:spPr', CT_ShapeProperties) -register_element_cls('p:xfrm', CT_Transform2D) +register_element_cls('a:ext', CT_PositiveSize2D) +register_element_cls('a:ln', CT_LineProperties) +register_element_cls('a:prstDash', CT_PresetLineDashProperties) +register_element_cls('a:off', CT_Point2D) +register_element_cls('a:xfrm', CT_Transform2D) +register_element_cls('c:spPr', CT_ShapeProperties) +register_element_cls('p:cNvPr', CT_NonVisualDrawingProps) +register_element_cls('p:nvPr', CT_ApplicationNonVisualDrawingProps) +register_element_cls('p:ph', CT_Placeholder) +register_element_cls('p:spPr', CT_ShapeProperties) +register_element_cls('p:xfrm', CT_Transform2D) + from .shapes.table import ( diff --git a/pptx/oxml/dml/color.py b/pptx/oxml/dml/color.py index df9c99c68..c0085a84a 100644 --- a/pptx/oxml/dml/color.py +++ b/pptx/oxml/dml/color.py @@ -17,6 +17,7 @@ class _BaseColorElement(BaseOxmlElement): """ lumMod = ZeroOrOne('a:lumMod') lumOff = ZeroOrOne('a:lumOff') + alpha = ZeroOrOne('a:alpha') def add_lumMod(self, value): """ @@ -43,6 +44,21 @@ def clear_lum(self): self._remove_lumOff() return self + def add_alpha(self, value): + """ + Return a newly added child element. + """ + alpha = self._add_alpha() + alpha.val = value + return alpha + + def clear_alpha(self): + """ + Return self after removing any child elements. + """ + self._remove_alpha() + return self + class CT_HslColor(_BaseColorElement): """ diff --git a/pptx/oxml/parts/notes_slide.py b/pptx/oxml/parts/notes_slide.py new file mode 100644 index 000000000..6c5403fc4 --- /dev/null +++ b/pptx/oxml/parts/notes_slide.py @@ -0,0 +1,56 @@ +# encoding: utf-8 + +""" +lxml custom element classes for slide master-related XML elements. +""" + +from __future__ import absolute_import + +from .. import parse_xml +from ..ns import nsdecls +from ..xmlchemy import ( + BaseOxmlElement, OneAndOnlyOne, ZeroOrOne +) + +class CT_NotesSlide(BaseOxmlElement): + """ + ```` element, root of a slide part + """ + _tag_seq = ('cSld', 'clrMapOvr', ) + cSld = OneAndOnlyOne('p:cSld') + clrMapOvr = ZeroOrOne('p:clrMapOvr', successors=_tag_seq[2:]) + del _tag_seq + + @classmethod + def new(cls): + """ + Return a new ```` element configured as a base slide shape. + """ + return parse_xml(cls._sld_xml()) + + @property + def spTree(self): + """ + Return required `p:cSld/p:spTree` grandchild. + """ + return self.cSld.spTree + + @staticmethod + def _sld_xml(): + return ( + '\n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + '' % nsdecls('a', 'p', 'r') + ) diff --git a/pptx/oxml/shapes/autoshape.py b/pptx/oxml/shapes/autoshape.py index ce229b9d3..5960e8e2b 100644 --- a/pptx/oxml/shapes/autoshape.py +++ b/pptx/oxml/shapes/autoshape.py @@ -167,6 +167,17 @@ def new_textbox_sp(id_, name, left, top, width, height): sp = parse_xml(xml) return sp + @staticmethod + def new_custom_geometry_sp(id_, name, left, top, width, height): + """ + Return a new ```` element tree configured as a custom + geometry shape. + """ + tmpl = CT_Shape._custgeom_sp_tmpl() + xml = tmpl % (id_, name, left, top, width, height) + sp = parse_xml(xml) + return sp + @property def prst(self): """ @@ -277,6 +288,53 @@ def _textbox_sp_tmpl(): (nsdecls('a', 'p'), '%d', '%s', '%d', '%d', '%d', '%d') ) + @staticmethod + def _custgeom_sp_tmpl(): + return ( + '\n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + '' % + (nsdecls('a', 'p'), '%d', '%s', '%d', '%d', '%d', '%d') + ) class CT_ShapeNonVisual(BaseShapeElement): """ diff --git a/pptx/oxml/shapes/cust_geom.py b/pptx/oxml/shapes/cust_geom.py new file mode 100644 index 000000000..b22a96320 --- /dev/null +++ b/pptx/oxml/shapes/cust_geom.py @@ -0,0 +1,183 @@ + +from pptx.oxml import parse_xml +from pptx.oxml.xmlchemy import ( + BaseOxmlElement, OneAndOnlyOne, ZeroOrMore, ZeroOrOne, + OneOrMore, OptionalAttribute, RequiredAttribute +) +from pptx.oxml.simpletypes import ( + ST_PositiveCoordinate, ST_AdjCoordinate, ST_AdjAngle +) +from pptx.oxml.ns import nsdecls + +class CT_CustomGeometry2D(BaseOxmlElement): + """ + + """ + pathLst = OneAndOnlyOne('a:pathLst') + + +class CT_Path2DList(BaseOxmlElement): + """ + + """ + path = ZeroOrMore('a:path') + + +class CT_Path2D(BaseOxmlElement): + """ + + """ + moveTo = ZeroOrMore('a:moveTo') + lnTo = ZeroOrMore('a:lnTo') + cubicBezTo = ZeroOrMore('a:cubicBezTo') + arcTo = ZeroOrMore('a:arcTo') + close = ZeroOrMore('a:close') + + w = RequiredAttribute('w', ST_PositiveCoordinate) + h = RequiredAttribute('h', ST_PositiveCoordinate) + + def add_moveTo(self, x, y): + """ + Create a moveTo element to the provided x and y + points, specified in pptx.Length units. + """ + mt = self._add_moveTo() + mt.pt.x = x + mt.pt.y = y + return mt + + def add_lnTo(self, x, y): + """ + Create a lineTo element to the provided x and y + points, specified in pptx.Length units. + """ + lt = self._add_lnTo() + lt.pt.x = x + lt.pt.y = y + return lt + + def add_cubicBezTo(self, x1, y1, x2, y2, x, y): + """ + Create a cubicBezTo element to provided points. + """ + cbt = self._add_cubicBezTo() + + pt = cbt._add_pt() + pt.x = x1 + pt.y = y1 + + pt = cbt._add_pt() + pt.x = x2 + pt.y = y2 + + pt = cbt._add_pt() + pt.x = x + pt.y = y + return cbt + + def add_arcTo(self, hR, wR, stAng, swAng): + """ + Create a arcTo element to provided info. + """ + at = self._add_arcTo() + + at.hR = hR + at.wR = wR + at.stAng = stAng + at.swAng = swAng + return at + + def _new_moveTo(self): + return CT_Path2DMoveTo.new() + + def _new_lnTo(self): + return CT_Path2DLineTo.new() + + +class CT_Path2DMoveTo(BaseOxmlElement): + """ + + """ + pt = OneAndOnlyOne('a:pt') + + @classmethod + def new(cls): + xml = cls._tmpl() + moveTo = parse_xml(xml) + return moveTo + + @classmethod + def _tmpl(self): + return ( + '\n' + ' \n' + '' + ) % (nsdecls('a'), '%d', '%d') + + +class CT_Path2DLineTo(BaseOxmlElement): + """ + + """ + pt = OneAndOnlyOne('a:pt') + + @classmethod + def new(cls): + xml = cls._tmpl() + return parse_xml(xml) + + @classmethod + def _tmpl(self): + return ( + '\n' + ' \n' + '' + ) % (nsdecls('a'), '%d', '%d') + + +class CT_Path2DCubicBezierTo(BaseOxmlElement): + """ + + """ + pt = OneOrMore('a:pt') + + @classmethod + def new(cls): + xml = cls._tmpl() + return parse_xml(xml) + + @classmethod + def _tmpl(self): + return ( + '\n' + ' \n' + ' \n' + ' \n' + '' + ) % (nsdecls('a'), '%d', '%d', '%d', '%d', '%d', '%d') + + +class CT_Path2DClose(BaseOxmlElement): + """ + + """ + pass + + +class CT_AdjPoint2D(BaseOxmlElement): + """ + + """ + x = RequiredAttribute('x', ST_PositiveCoordinate) + y = RequiredAttribute('y', ST_PositiveCoordinate) + + +class CT_Path2DArcTo(BaseOxmlElement): + """ + + """ + wR = RequiredAttribute('wR', ST_AdjCoordinate) + hR = RequiredAttribute('hR', ST_AdjCoordinate) + stAng = RequiredAttribute('stAng', ST_AdjAngle) + swAng = RequiredAttribute('swAng', ST_AdjAngle) + diff --git a/pptx/oxml/shapes/groupshape.py b/pptx/oxml/shapes/groupshape.py index 5f90de645..2725fd795 100644 --- a/pptx/oxml/shapes/groupshape.py +++ b/pptx/oxml/shapes/groupshape.py @@ -6,9 +6,10 @@ from __future__ import absolute_import +from .. import parse_xml from .autoshape import CT_Shape from .graphfrm import CT_GraphicalObjectFrame -from ..ns import qn +from ..ns import qn, nsdecls from .picture import CT_Picture from .shared import BaseShapeElement from ..xmlchemy import BaseOxmlElement, OneAndOnlyOne, ZeroOrOne @@ -76,6 +77,26 @@ def add_textbox(self, id_, name, x, y, cx, cy): self.insert_element_before(sp, 'p:extLst') return sp + def add_custom_geometry(self, id_, name, left, top, width, height): + """ + Append a newly-created custom geometry shape with the specified + position and size. + """ + sp = CT_Shape.new_custom_geometry_sp(id_, name, left, top, width, height) + self.insert_element_before(sp, 'p:extLst') + return sp + + def add_groupshape(self, id_, name, x, y, cx, cy, + ch_x, ch_y, ch_cx, ch_cy): + """ + Append a newly-created ```` shape having the specified + position and size. + """ + sp = CT_GroupShape.new_groupshape_sp(id_, name, x, y, cx, cy, + ch_x, ch_y, ch_cx, ch_cy) + self.insert_element_before(sp, 'p:extLst') + return sp + def get_or_add_xfrm(self): """ Return the ```` grandchild element, newly-added if not @@ -107,6 +128,39 @@ def xfrm(self): """ return self.grpSpPr.xfrm + @staticmethod + def new_groupshape_sp(id_, name, left, top, width, height, child_left, + child_top, child_width, child_height): + """ + + """ + tmpl = CT_GroupShape._groupshape_tmpl() + xml = tmpl % (id_, name, left, top, width, height, child_left, + child_top, child_width, child_height) + sp = parse_xml(xml) + return sp + + @staticmethod + def _groupshape_tmpl(): + return ( + '\n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + ' \n' + '\n' % + (nsdecls('a', 'p'), '%d', '%s', '%d', '%d', '%d', '%d', '%d', + '%d', '%d', '%d') + ) class CT_GroupShapeNonVisual(BaseShapeElement): """ diff --git a/pptx/oxml/shapes/shared.py b/pptx/oxml/shapes/shared.py index 690b93a64..d6eed7eb9 100644 --- a/pptx/oxml/shapes/shared.py +++ b/pptx/oxml/shapes/shared.py @@ -10,7 +10,8 @@ from ..ns import qn from ..simpletypes import ( ST_Angle, ST_Coordinate, ST_Direction, ST_DrawingElementId, ST_LineWidth, - ST_PlaceholderSize, ST_PositiveCoordinate, XsdString, XsdUnsignedInt + ST_PlaceholderSize, ST_PositiveCoordinate, XsdString, XsdUnsignedInt, + ST_PresetLineDashVal ) from ...util import Emu from ..xmlchemy import ( @@ -212,6 +213,8 @@ class CT_LineProperties(BaseOxmlElement): 'a:headEnd', 'a:tailEnd', 'a:extLst' ) ) + eg_lineDashProperties = ZeroOrOne('a:prstDash', successors=()) + w = OptionalAttribute('w', ST_LineWidth, default=Emu(0)) @property @@ -221,6 +224,12 @@ def eg_fillProperties(self): """ return self.eg_lineFillProperties +class CT_PresetLineDashProperties(BaseOxmlElement): + """ + Custom element class for element + """ + val = OptionalAttribute('val', ST_PresetLineDashVal, + default=ST_PresetLineDashVal.SOLID) class CT_NonVisualDrawingProps(BaseOxmlElement): """ diff --git a/pptx/oxml/simpletypes.py b/pptx/oxml/simpletypes.py index 511f15343..fceabb75b 100644 --- a/pptx/oxml/simpletypes.py +++ b/pptx/oxml/simpletypes.py @@ -11,6 +11,7 @@ from ..exc import InvalidXmlError from ..util import Centipoints, Emu +import re class BaseSimpleType(object): @@ -241,6 +242,12 @@ def validate(cls, value): cls.validate_int_in_range(value, 0, 65535) +class ST_GeomGuideName(XsdToken): + """ + specifies a geometry guide name + """ + + class ST_Angle(XsdInt): """ Valid values for `rot` attribute on `` element. 60000ths of @@ -269,6 +276,27 @@ def validate(cls, value): BaseFloatType.validate(value) +class ST_AdjAngle(BaseSimpleType): + """ + xsd:union of ST_Angle and ST_GeomGuideName + """ + @classmethod + def convert_from_xml(cls, str_value): + try: + int(str_value) + return ST_Angle.convert_from_xml(str_value) + except ValueError: + return ST_GeomGuideName.convert_from_xml(str_value) + + @classmethod + def convert_to_xml(cls, value): + return ST_Angle.convert_to_xml(value) + + @classmethod + def validate(cls, value): + return ST_Angle.validate(value) + + class ST_AxisUnit(XsdDouble): """ Valid values for val attribute on c:majorUnit and others. @@ -318,6 +346,43 @@ def validate(cls, value): ST_CoordinateUnqualified.validate(value) +class ST_AdjCoordinate(BaseSimpleType): + """ + xsd:union of ST_Coordinate, ST_GeomGuideName + """ + def _is_universal_measurement(str_value): + """ + Check if str_value mateches universal measure type + """ + p = re.compile("^\s*-?[0-9]+(\.[0-9]+)?(mm|cm|in|pt|pc|pi)\s*$") + return p.match(str_value) + + def _is_emu(str_value): + """ + Check if str_value is a number + """ + try: + int(str_value) + return True + except ValueError: + return False + + @classmethod + def convert_from_xml(cls, str_value): + if _is_universal_measurement(str_value) or _is_emu(str_value): + return ST_Coordinate.convert_from_xml(str_value) + else: + return ST_GeomGuideName.convert_from_xml(str_value) + + @classmethod + def convert_to_xml(cls, value): + return ST_Coordinate.convert_to_xml(value) + + @classmethod + def validate(cls, value): + ST_Coordinate.validate(value) + + class ST_Coordinate32(BaseSimpleType): """ xsd:union of ST_Coordinate32Unqualified, ST_UniversalMeasure @@ -695,3 +760,22 @@ def convert_from_xml(cls, str_value): }[units_part] emu_value = Emu(int(round(quantity * multiplier))) return emu_value + + +class ST_PresetLineDashVal(XsdToken): + """ + """ + SOLID = 'slide' + SYSDASH = 'sysDash' + SYSDOT = 'sysDot' + SYSDASHDOT = 'sysDashDot' + SYSDASHDOTDOT = 'sysDashDotDot' + DOT = 'dot' + DASH = 'dash' + LGDASH = 'lgDash' + DASHDOT = 'dashDot' + LGDASHDOT = 'lgDashDot' + LGDASHDOTDOT = 'lgDashDotDot' + + _members = (SOLID, SYSDASH, SYSDASHDOT, SYSDASHDOTDOT, + DOT, DASH, LGDASH, DASHDOT, LGDASHDOT, LGDASHDOTDOT) diff --git a/pptx/parts/slide.py b/pptx/parts/slide.py index 752b8dc82..736265a90 100644 --- a/pptx/parts/slide.py +++ b/pptx/parts/slide.py @@ -12,6 +12,7 @@ from ..opc.constants import CONTENT_TYPE as CT, RELATIONSHIP_TYPE as RT from ..opc.package import XmlPart from ..oxml.parts.slide import CT_Slide +from ..oxml.parts.notes_slide import CT_NotesSlide from ..shapes.shapetree import SlideShapeFactory, SlideShapeTree from ..shared import ParentedElementProxy from ..util import lazyproperty @@ -83,6 +84,18 @@ def add_chart_part(self, chart_type, chart_data): rId = self.relate_to(chart_part, RT.CHART) return rId + @lazyproperty + def notes_slide(self): + """ + + """ + part_fmt = '/ppt/notesSlides/notesSlide%d.xml' + partname = self._package.next_partname(part_fmt) + notes_slide = NotesSlide.new(self, partname, self._package) + rId = self.relate_to(notes_slide, RT.NOTES_SLIDE) + notes_slide.relate_to(self, RT.SLIDE) + return notes_slide + @lazyproperty def placeholders(self): """ @@ -119,6 +132,37 @@ def slidelayout(self): return self.slide_layout +class NotesSlide(BaseSlide): + """ + Notes Slide part. Corresponds to package files + ppt/notesSlides/notesSlide[1-9][0-9]*.xml. + """ + @classmethod + def new(cls, slide, partname, package): + """ + Return a new slide based on *slide* and having *partname*, + created from scratch. + """ + element = CT_NotesSlide.new() + notes_slide = cls(partname, CT.PML_NOTES_SLIDE, element, package) + return notes_slide + + @property + def slide(self): + """ + |SlideLayout| object this slide inherits appearance from. + """ + return self.part_related_by(RT.SLIDE) + + @lazyproperty + def shapes(self): + """ + Instance of |SlideShapeTree| containing sequence of shape objects + appearing on this slide. + """ + return SlideShapeTree(self) + + class _SlidePlaceholders(ParentedElementProxy): """ Collection of placeholder shapes on a slide. Supports iteration, diff --git a/pptx/shapes/factory.py b/pptx/shapes/factory.py index 86ba957d3..0fce56c78 100644 --- a/pptx/shapes/factory.py +++ b/pptx/shapes/factory.py @@ -9,6 +9,7 @@ ) from .autoshape import Shape +from .groupshape import GroupShape from .base import BaseShape from ..enum.shapes import PP_PLACEHOLDER from .graphfrm import GraphicFrame @@ -27,6 +28,8 @@ def BaseShapeFactory(shape_elm, parent): tag_name = shape_elm.tag if tag_name == qn('p:sp'): return Shape(shape_elm, parent) + if tag_name == qn('p:grpSp'): + return GroupShape(shape_elm, parent) if tag_name == qn('p:pic'): return Picture(shape_elm, parent) if tag_name == qn('p:graphicFrame'): diff --git a/pptx/shapes/groupshape.py b/pptx/shapes/groupshape.py new file mode 100644 index 000000000..23e5b1e89 --- /dev/null +++ b/pptx/shapes/groupshape.py @@ -0,0 +1,392 @@ +# encoding: utf-8 + +""" +The group shape, the structure that holds a set of sub-shapes. +""" + +from __future__ import ( + absolute_import, division, print_function, unicode_literals +) + +from .base import BaseShape +from .autoshape import AutoShapeType +from ..enum.shapes import PP_PLACEHOLDER +#from .factory import BaseShapeFactory +from ..oxml.shapes.graphfrm import CT_GraphicalObjectFrame +from ..oxml.simpletypes import ST_Direction + + +class BaseGroupShape(BaseShape): + """ + Base class for a shape collection. + """ + + def __init__(self, grpSp, parent): + super(BaseGroupShape, self).__init__(grpSp, parent) + self._grpSp = grpSp + + def __getitem__(self, idx): + """ + Return shape at *idx* in sequence, e.g. ``shapes[2]``. + """ + shape_elms = list(self._iter_member_elms()) + try: + shape_elm = shape_elms[idx] + except IndexError: + raise IndexError('shape index out of range') + return self._shape_factory(shape_elm) + + def __iter__(self): + """ + Generate a reference to each shape in the collection, in sequence. + """ + for shape_elm in self._iter_member_elms(): + yield self._shape_factory(shape_elm) + + def __len__(self): + """ + Return count of shapes in this shape tree. A group shape contributes + 1 to the total, without regard to the number of shapes contained in + the group. + """ + shape_elms = list(self._iter_member_elms()) + return len(shape_elms) + + @staticmethod + def _is_member_elm(shape_elm): + """ + Return true if *shape_elm* represents a member of this collection, + False otherwise. + """ + return True + + def _iter_member_elms(self): + """ + Generate each child of the ```` element that corresponds to + a shape, in the sequence they appear in the XML. + """ + grp = self._sp + for shape_elm in grp.iter_shape_elms(): + if self._is_member_elm(shape_elm): + yield shape_elm + + @property + def _next_shape_id(self): + """ + Next available positive integer drawing object id in shape tree, + starting from 1 and making use of any gaps in numbering. In practice, + the minimum id is 2 because the spTree element is always assigned + id="1". + """ + id_str_lst = self._sp.xpath('//@id') + used_ids = [int(id_str) for id_str in id_str_lst if id_str.isdigit()] + for n in range(1, len(used_ids)+2): + if n not in used_ids: + return n + + def _shape_factory(self, shape_elm): + """ + Return an instance of the appropriate shape proxy class for + *shape_elm*. + """ + from .factory import BaseShapeFactory + return BaseShapeFactory(shape_elm, self) + + @property + def _sp(self): + """ + Return the pointer to the current element. In a regular GroupShape, + this will be self._grpSp. In a SlideShapeTree, this will be + self._spTree + """ + return self._grpSp + +class GroupShape(BaseGroupShape): + """ + Grouping of shapes within a SlideShapeTree. The first shape in the sequence + is the backmost in z-order and the last shape is topmost. Supports indexed + access, len(), index(), and iteration. + """ + def add_chart(self, chart_type, x, y, cx, cy, chart_data): + """ + Add a new chart of *chart_type* to the slide, positioned at (*x*, + *y*), having size (*cx*, *cy*), and depicting *chart_data*. + *chart_type* is one of the :ref:`XlChartType` enumeration values. + *chart_data* is a |ChartData| object populated with the categories + and series values for the chart. Note that a |GraphicFrame| shape + object is returned, not the |Chart| object contained in that graphic + frame shape. The chart object may be accessed using the :attr:`chart` + property of the returned |GraphicFrame| object. + """ + rId = self.part.add_chart_part(chart_type, chart_data) + graphic_frame = self._add_chart_graphic_frame(rId, x, y, cx, cy) + return graphic_frame + + def add_picture(self, image_file, left, top, width=None, height=None): + """ + Add picture shape displaying image in *image_file*, where + *image_file* can be either a path to a file (a string) or a file-like + object. + """ + from .shapetree import BaseShapeTree + + parent = self._parent + while not isinstance(parent, BaseShapeTree): + parent = parent._parent + + # FIXME: How do i find the parent slide object? + image_part, rId = parent._slide.get_or_add_image_part(image_file) + pic = self._add_pic_from_image_part( + image_part, rId, left, top, width, height + ) + picture = self._shape_factory(pic) + return picture + + def add_shape(self, autoshape_type_id, left, top, width, height): + """ + Add auto shape of type specified by *autoshape_type_id* (like + ``MSO_SHAPE.RECTANGLE``) and of specified size at specified position. + """ + autoshape_type = AutoShapeType(autoshape_type_id) + sp = self._add_sp_from_autoshape_type( + autoshape_type, left, top, width, height + ) + shape = self._shape_factory(sp) + return shape + + def add_table(self, rows, cols, left, top, width, height): + """ + Add a |GraphicFrame| object containing a table with the specified + number of *rows* and *cols* and the specified position and size. + *width* is evenly distributed between the columns of the new table. + Likewise, *height* is evenly distributed between the rows. Note that + the ``.table`` property on the returned |GraphicFrame| shape must be + used to access the enclosed |Table| object. + """ + graphicFrame = self._add_graphicFrame_containing_table( + rows, cols, left, top, width, height + ) + graphic_frame = self._shape_factory(graphicFrame) + return graphic_frame + + def add_textbox(self, left, top, width, height): + """ + Add text box shape of specified size at specified position on slide. + """ + sp = self._add_textbox_sp(left, top, width, height) + textbox = self._shape_factory(sp) + return textbox + + def add_custom_geometry(self, left, top, width, height): + """ + Add a custom geometry shpane of specified size at specified position + on slide. + """ + sp = self._add_cust_geom_sp(left, top, width, height) + cust_geom = self._shape_factory(sp) + return cust_geom + + def add_groupshape(self, left, top, width, height, child_left=0, child_top=0, + child_width=0, child_height=0): + """ + Add groupshape of specified size at specified position on slide. + """ + child_width = child_width or width + child_height = child_height or height + + sp = self._add_groupshape_sp(left, top, width, height, child_left, + child_top, child_width, child_height) + groupshape = self._shape_factory(sp) + return groupshape + + def clone_layout_placeholders(self, slide_layout): + """ + Add placeholder shapes based on those in *slide_layout*. Z-order of + placeholders is preserved. Latent placeholders (date, slide number, + and footer) are not cloned. + """ + for placeholder in slide_layout.iter_cloneable_placeholders(): + self._clone_layout_placeholder(placeholder) + + def index(self, shape): + """ + Return the index of *shape* in this sequence, raising |ValueError| if + *shape* is not in the collection. + """ + shape_elm = shape.element + for idx, elm in enumerate(self._grpSp.iter_shape_elms()): + if elm is shape_elm: + return idx + raise ValueError('shape not in collection') + + @property + def placeholders(self): + """ + Instance of |_SlidePlaceholders| containing sequence of placeholder + shapes in this slide. + """ + return self._sp.placeholders + + @property + def title(self): + """ + The title placeholder shape on the slide or |None| if the slide has + no title placeholder. + """ + for elm in self._grpSp.iter_shape_elms(): + if elm.ph_idx == 0: + return self._shape_factory(elm) + return None + + def _add_chart_graphicFrame(self, rId, x, y, cx, cy): + """ + Add a new ```` element to this shape tree having the + specified position and size and referring to the chart part + identified by *rId*. + """ + shape_id = self._next_shape_id + name = 'Chart %d' % (shape_id-1) + graphicFrame = CT_GraphicalObjectFrame.new_chart_graphicFrame( + shape_id, name, rId, x, y, cx, cy + ) + self._grpSp.append(graphicFrame) + return graphicFrame + + def _add_chart_graphic_frame(self, rId, x, y, cx, cy): + """ + Return a |GraphicFrame| object having the specified position and size + and referring to the chart part identified by *rId*. + """ + graphicFrame = self._add_chart_graphicFrame(rId, x, y, cx, cy) + graphic_frame = self._shape_factory(graphicFrame) + return graphic_frame + + def _add_graphicFrame_containing_table(self, rows, cols, x, y, cx, cy): + """ + Return a newly added ```` element containing a table + as specified by the parameters. + """ + id_ = self._next_shape_id + name = 'Table %d' % (id_-1) + graphicFrame = self._grpSp.add_table( + id_, name, rows, cols, x, y, cx, cy + ) + return graphicFrame + + def _add_pic_from_image_part(self, image_part, rId, x, y, cx, cy): + """ + Return a newly added ```` element specifying a picture shape + displaying *image_part* with size and position specified by *x*, *y*, + *cx*, and *cy*. The element is appended to the shape tree, causing it + to be displayed first in z-order on the slide. + """ + id = self._next_shape_id + name = 'Picture %d' % (id-1) + desc = image_part.desc + scaled_cx, scaled_cy = image_part.scale(cx, cy) + + pic = self._grpSp.add_pic( + id, name, desc, rId, x, y, scaled_cx, scaled_cy + ) + + return pic + + def _add_sp_from_autoshape_type(self, autoshape_type, x, y, cx, cy): + """ + Return a newly-added ```` element for a shape of + *autoshape_type* at position (x, y) and of size (cx, cy). + """ + id_ = self._next_shape_id + name = '%s %d' % (autoshape_type.basename, id_-1) + sp = self._grpSp.add_autoshape( + id_, name, autoshape_type.prst, x, y, cx, cy + ) + return sp + + def _add_textbox_sp(self, x, y, cx, cy): + """ + Return a newly-added textbox ```` element at position (x, y) + and of size (cx, cy). + """ + id_ = self._next_shape_id + name = 'TextBox %d' % (id_-1) + sp = self._grpSp.add_textbox(id_, name, x, y, cx, cy) + return sp + + def _add_cust_geom_sp(self, x, y, cx, cy): + """ + Return a newly-added custom geometry ```` element + """ + id_ = self._next_shape_id + name = 'CustGeom %s' % id_ + sp = self._grpSp.add_custom_geometry(id_, name, x, y, cx, cy) + return sp + + def _add_groupshape_sp(self, x, y, cx, cy, ch_x, ch_y, ch_cx, ch_cy): + """ + + """ + id_ = self._next_shape_id + name = 'GroupShape %d' % (id_-1) + sp = self._grpSp.add_groupshape(id_, name, x, y, cx, cy, ch_x, ch_y, + ch_cx, ch_cy) + return sp + + def _clone_layout_placeholder(self, layout_placeholder): + """ + Add a new placeholder shape based on the slide layout placeholder + *layout_ph*. + """ + id_ = self._next_shape_id + ph_type = layout_placeholder.ph_type + orient = layout_placeholder.orient + name = self._next_ph_name(ph_type, id_, orient) + sz = layout_placeholder.sz + idx = layout_placeholder.idx + + self._grpSp.add_placeholder(id_, name, ph_type, orient, sz, idx) + + def _next_ph_name(self, ph_type, id, orient): + """ + Next unique placeholder name for placeholder shape of type *ph_type*, + with id number *id* and orientation *orient*. Usually will be standard + placeholder root name suffixed with id-1, e.g. + _next_ph_name(ST_PlaceholderType.TBL, 4, 'horz') ==> + 'Table Placeholder 3'. The number is incremented as necessary to make + the name unique within the collection. If *orient* is ``'vert'``, the + placeholder name is prefixed with ``'Vertical '``. + """ + basename = { + # BODY is named 'Notes Placeholder' in a notes master + PP_PLACEHOLDER.BODY: 'Text Placeholder', + PP_PLACEHOLDER.CHART: 'Chart Placeholder', + PP_PLACEHOLDER.BITMAP: 'ClipArt Placeholder', + PP_PLACEHOLDER.CENTER_TITLE: 'Title', + PP_PLACEHOLDER.ORG_CHART: 'SmartArt Placeholder', + PP_PLACEHOLDER.DATE: 'Date Placeholder', + PP_PLACEHOLDER.FOOTER: 'Footer Placeholder', + PP_PLACEHOLDER.HEADER: 'Header Placeholder', + PP_PLACEHOLDER.MEDIA_CLIP: 'Media Placeholder', + PP_PLACEHOLDER.OBJECT: 'Content Placeholder', + PP_PLACEHOLDER.PICTURE: 'Picture Placeholder', + # PP_PLACEHOLDER.SLIDE_IMAGE: 'Slide Image Placeholder', + PP_PLACEHOLDER.SLIDE_NUMBER: 'Slide Number Placeholder', + PP_PLACEHOLDER.SUBTITLE: 'Subtitle', + PP_PLACEHOLDER.TABLE: 'Table Placeholder', + PP_PLACEHOLDER.TITLE: 'Title', + }[ph_type] + + # prefix rootname with 'Vertical ' if orient is 'vert' + if orient == ST_Direction.VERT: + basename = 'Vertical %s' % basename + + # increment numpart as necessary to make name unique + numpart = id - 1 + names = self._grpSp.xpath('//p:cNvPr/@name') + while True: + name = '%s %d' % (basename, numpart) + if name not in names: + break + numpart += 1 + + return name diff --git a/pptx/shapes/shapetree.py b/pptx/shapes/shapetree.py index 84e2e22b1..d0117f9e7 100644 --- a/pptx/shapes/shapetree.py +++ b/pptx/shapes/shapetree.py @@ -105,6 +105,9 @@ def _spTree(self): """ return self._slide.spTree + def _sp(self): + return self._spTree + class BasePlaceholders(BaseShapeTree): """ @@ -188,6 +191,28 @@ def add_textbox(self, left, top, width, height): textbox = self._shape_factory(sp) return textbox + def add_custom_geometry(self, left, top, width, height): + """ + Add a custom geometry shpane of specified size at specified position + on slide. + """ + sp = self._add_cust_geom_sp(left, top, width, height) + cust_geom = self._shape_factory(sp) + return cust_geom + + def add_groupshape(self, left, top, width, height, child_left=0, + child_top=0, child_width=0, child_height=0): + """ + Add groupshape of specified size at specified position on slide. + """ + child_width = child_width or width + child_height = child_height or height + + sp = self._add_groupshape_sp(left, top, width, height, child_left, + child_top, child_width, child_height) + groupshape = self._shape_factory(sp) + return groupshape + def clone_layout_placeholders(self, slide_layout): """ Add placeholder shapes based on those in *slide_layout*. Z-order of @@ -302,6 +327,25 @@ def _add_textbox_sp(self, x, y, cx, cy): sp = self._spTree.add_textbox(id_, name, x, y, cx, cy) return sp + def _add_cust_geom_sp(self, x, y, cx, cy): + """ + Return a newly-added custom geometry ```` element + """ + id_ = self._next_shape_id + name = 'CustGeom %s' % id_ + sp = self._spTree.add_custom_geometry(id_, name, x, y, cx, cy) + return sp + + def _add_groupshape_sp(self, x, y, cx, cy, ch_x, ch_y, ch_cx, ch_cy): + """ + + """ + id_ = self._next_shape_id + name = 'GroupShape %d' % (id_-1) + sp = self._spTree.add_groupshape(id_, name, x, y, cx, cy, ch_x, ch_y, + ch_cx, ch_cy) + return sp + def _clone_layout_placeholder(self, layout_placeholder): """ Add a new placeholder shape based on the slide layout placeholder diff --git a/pptx/text/fonts.py b/pptx/text/fonts.py index 9c67352a7..390d4940a 100644 --- a/pptx/text/fonts.py +++ b/pptx/text/fonts.py @@ -54,6 +54,8 @@ def _font_directories(cls): return cls._os_x_font_directories() if sys.platform.startswith('win32'): return cls._windows_font_directories() + if sys.platform.startswith('linux'): + return cls._linux_font_directories() raise OSError('unsupported operating system') @classmethod @@ -99,7 +101,26 @@ def _windows_font_directories(cls): likely to be located. """ return [r'C:\Windows\Fonts'] + """ + Return a sequence of directory paths on Windows in which fonts are + likely to be located. + """ + @classmethod + def _linux_font_directories(cls): + """ + Return a sequence of directory paths on Windows in which fonts are + likely to be located. + """ + linux_font_dirs = [ + '/usr/share/fonts', + ] + home = os.environ.get('HOME') + if home is not None: + linux_font_dirs.extend([ + os.path.join(home, '.fonts') + ]) + return linux_font_dirs class _Font(object): """ diff --git a/tests/oxml/test_dml.py b/tests/oxml/test_dml.py index 78abb1837..f3dcc050f 100644 --- a/tests/oxml/test_dml.py +++ b/tests/oxml/test_dml.py @@ -12,7 +12,7 @@ from pptx.oxml.dml.color import CT_Percentage, CT_SchemeColor, CT_SRgbColor from pptx.oxml.ns import qn -from .unitdata.dml import a_lumMod, a_lumOff, a_schemeClr, an_srgbClr +from .unitdata.dml import a_lumMod, a_lumOff, a_alpha, a_schemeClr, an_srgbClr class Describe_BaseColorElement(object): @@ -27,6 +27,11 @@ def it_can_get_the_lumOff_child_element_if_there_is_one( assert schemeClr.lumOff is None assert schemeClr_with_lumOff.lumOff is lumOff + def it_can_get_the_alpha_child_element_if_there_is_one( + self, schemeClr, schemeClr_with_alpha, alpha): + assert schemeClr.alpha is None + assert schemeClr_with_alpha.alpha is alpha + def it_can_remove_existing_lumMod_and_lumOff_child_elements( self, schemeClr_with_lumMod, schemeClr_with_lumOff, schemeClr_xml): @@ -35,6 +40,11 @@ def it_can_remove_existing_lumMod_and_lumOff_child_elements( assert schemeClr_with_lumMod.xml == schemeClr_xml assert schemeClr_with_lumOff.xml == schemeClr_xml + def it_can_remove_existing_alpha_child_elements( + self, schemeClr_with_alpha, schemeClr_xml): + schemeClr_with_alpha.clear_alpha() + assert schemeClr_with_alpha.xml == schemeClr_xml + def it_can_add_a_lumMod_child_element( self, schemeClr, schemeClr_with_lumMod_xml): lumMod = schemeClr.add_lumMod(0.75) @@ -47,6 +57,13 @@ def it_can_add_a_lumOff_child_element( assert schemeClr.xml == schemeClr_with_lumOff_xml assert schemeClr.find(qn('a:lumOff')) == lumOff + def it_can_add_a_alpha_child_element( + self, schemeClr, schemeClr_with_alpha_xml): + alpha = schemeClr.add_alpha(0.4) + assert schemeClr.xml == schemeClr_with_alpha_xml + assert schemeClr.find(qn('a:alpha')) == alpha + + # fixtures --------------------------------------------- @pytest.fixture @@ -57,6 +74,10 @@ def lumMod(self): def lumOff(self): return a_lumOff().with_nsdecls().element + @pytest.fixture + def alpha(self): + return a_alpha().with_nsdecls().element + @pytest.fixture def schemeClr(self): return a_schemeClr().with_nsdecls().element @@ -76,17 +97,28 @@ def schemeClr_with_lumMod_xml(self): lumMod_bldr = a_lumMod().with_val(75000) return a_schemeClr().with_nsdecls().with_child(lumMod_bldr).xml() + @pytest.fixture + def schemeClr_with_lumOff(self, lumOff): + schemeClr = a_schemeClr().with_nsdecls().element + schemeClr.append(lumOff) + return schemeClr + @pytest.fixture def schemeClr_with_lumOff_xml(self): lumOff_bldr = a_lumOff().with_val(40000) return a_schemeClr().with_nsdecls().with_child(lumOff_bldr).xml() @pytest.fixture - def schemeClr_with_lumOff(self, lumOff): + def schemeClr_with_alpha(self, alpha): schemeClr = a_schemeClr().with_nsdecls().element - schemeClr.append(lumOff) + schemeClr.append(alpha) return schemeClr + @pytest.fixture + def schemeClr_with_alpha_xml(self): + alpha_bldr = a_alpha().with_val(40000) + return a_schemeClr().with_nsdecls().with_child(alpha_bldr).xml() + class DescribeCT_Percentage(object): diff --git a/tests/oxml/unitdata/dml.py b/tests/oxml/unitdata/dml.py index 7e6c44900..88a8ee0f7 100644 --- a/tests/oxml/unitdata/dml.py +++ b/tests/oxml/unitdata/dml.py @@ -122,6 +122,10 @@ def a_lumOff(): return CT_PercentageBuilder('a:lumOff') +def a_alpha(): + return CT_PercentageBuilder('a:alpha') + + def a_noFill(): return CT_NoFillPropertiesBuilder() diff --git a/tests/text/test_fonts.py b/tests/text/test_fonts.py index 95cdc7a68..07fe3a91d 100644 --- a/tests/text/test_fonts.py +++ b/tests/text/test_fonts.py @@ -54,6 +54,11 @@ def it_knows_windows_font_dirs_to_help_find(self, win_dirs_fixture): font_dirs = FontFiles._windows_font_directories() assert font_dirs == expected_dirs + def it_knows_linuxdows_font_dirs_to_help_find(self, linux_dirs_fixture): + expected_dirs = linux_dirs_fixture + font_dirs = FontFiles._linux_font_directories() + assert font_dirs == expected_dirs + def it_iterates_over_fonts_in_dir_to_help_find(self, iter_fixture): directory, _Font_, expected_calls, expected_paths = iter_fixture paths = list(FontFiles._iter_font_files_in(directory)) @@ -77,14 +82,17 @@ def find_fixture(self, request, _installed_fonts_): @pytest.fixture(params=[ ('darwin', ['a', 'b']), ('win32', ['c', 'd']), + ('linux2', ['e', 'f']), ]) def font_dirs_fixture( self, request, _os_x_font_directories_, - _windows_font_directories_): + _windows_font_directories_, + _linux_font_directories_): platform, expected_dirs = request.param dirs_meth_mock = { 'darwin': _os_x_font_directories_, 'win32': _windows_font_directories_, + 'linux2': _linux_font_directories_, }[platform] sys_ = var_mock(request, 'pptx.text.fonts.sys') sys_.platform = platform @@ -133,6 +141,17 @@ def osx_dirs_fixture(self, request): def win_dirs_fixture(self, request): return [r'C:\Windows\Fonts'] + @pytest.fixture + def linux_dirs_fixture(self, request): + import os + os_ = var_mock(request, 'pptx.text.fonts.os') + os_.path = os.path + os_.environ = {'HOME': '/home/fbar'} + return [ + '/usr/share/fonts', + '/home/fbar/.fonts', + ] + # fixture components ----------------------------------- @pytest.fixture @@ -167,6 +186,10 @@ def _os_x_font_directories_(self, request): def _windows_font_directories_(self, request): return method_mock(request, FontFiles, '_windows_font_directories') + @pytest.fixture + def _linux_font_directories_(self, request): + return method_mock(request, FontFiles, '_linux_font_directories') + class Describe_Font(object):