def apply_commands(rule: Dict[str, dict], shapes, data: dict): ''' Apply commands in rule to change shapes using data. :arg dict rule: a dict of shape names, and commands to apply on each. e.g. ``{"Oval 1": {"fill": "red"}, "Oval 2": {"text": "OK"}}`` :arg Shapes shapes: a slide.shapes or group.shapes object on which the rule should be applied :arg dict data: data context for the commands in the rule ''' # Apply every rule to every pattern -- as long as the rule key matches the shape name for pattern, spec in rule.items(): if pattern in rule_cmdlist: continue shape_matched = False for shape in shapes: if not fnmatchcase(shape.name, pattern): continue shape_matched = True # Clone all slides into the `clones` list BEFORE applying any command. Ensures that # commands applied to the shape don't propagate into its clones clones = [] clone_seq = iterate_on(spec.get('clone-shape', [None]), data) parent_clone = data.get('clone', None) for i, (clone_key, clone_val) in enumerate(clone_seq): if i > 0: # This copies only a shape, group or image. Not table, chart, media, equation, # or zoom. But we don't see a need for these yet. el = copy.deepcopy(shape.element) shape.element.addnext(el) shape = pptx.shapes.autoshape.Shape(el, None) clones.append(AttrDict(pos=i, key=clone_key, val=clone_val, shape=shape, parent=parent_clone)) # Run commands in the spec on all cloned shapes is_group = shape.element.tag.endswith('}grpSp') for i, clone in enumerate(clones): # Include shape-level `data:`. Add shape, clone as variables shape_data = load_data( spec.get('data', {}), _default_key='function', shape=shape, clone=clone, **{k: v for k, v in data.items() if k not in {'shape', 'clone'}}) for cmd in spec: if cmd in commands.cmdlist: commands.cmdlist[cmd](clone.shape, spec[cmd], shape_data) # Warn on unknown commands. But don't warn on groups -- they have sub-shapes elif cmd not in special_cmdlist and not is_group: app_log.warn('pptgen2: Unknown command: %s on shape: %s', cmd, pattern) # If the shape is a group, apply spec to each sub-shape if is_group: apply_commands(spec, SlideShapes(clone.shape.element, shapes), shape_data) # Warn if the pattern is neither a shape nor a command if (not shape_matched and pattern not in special_cmdlist and pattern not in commands.cmdlist): app_log.warn('pptgen2: No shape matches pattern: %s', pattern)
def table(shape, spec, data: dict): if not shape.has_table: raise ValueError( 'Cannot run table commands on shape %s that is not a table' % shape.name) table = shape.table # Set or get table first/last row/col attributes header_row = expr(spec.get('header-row', table.first_row), data) table.first_row = bool(header_row) table.last_row = bool(expr(spec.get('total-row', table.last_row), data)) table.first_col = bool( expr(spec.get('first-column', table.first_col), data)) table.last_col = bool(expr(spec.get('last-column', table.last_col), data)) # table data must be a DataFrame if specified. Else, create a DataFrame from existing text table_data = expr(spec.get('data', None), data) if table_data is not None and not isinstance(table_data, pd.DataFrame): raise ValueError('data on table %s must be a DataFrame, not %r' % (shape.name, table_data)) # Extract data from table text if no data is specified. if table_data is None: table_data = pd.DataFrame([[cell.text for cell in row.cells] for row in table.rows]) # If the PPTX table has a header row, set the first row as the DataFrame header too if table.first_row: table_data = table_data.T.set_index([0]).T # Adjust PPTX table size to data table size header_offset = 1 if table.first_row else 0 _resize(table._tbl.tr_lst, len(table_data) + header_offset) data_cols = len(table_data.columns) _resize(table._tbl.tblGrid.gridCol_lst, data_cols) for row in table.rows: _resize(row._tr.tc_lst, data_cols) # Set header row text from header-row or from data column headers if table.first_row: header_columns = table_data.columns if isinstance(header_row, (list, tuple, pd.Index, pd.Series)): header_columns = header_row[:len(table_data.columns)] for j, column in enumerate(header_columns): text(table.cell(0, j), column, {'_expr_mode': False}) # If `text` is not specified, just use the table value expr_mode = data.get('_expr_mode') spec.setdefault('text', 'cell.val' if expr_mode else {'expr': 'cell.val'}) # TODO: Handle nans # Apply table commands. (Copy data to avoid modifying original. We'll add data['cell'] later) data = dict(data) columns = table_data.columns.tolist() for key, cmdspec in spec.items(): # The command spec can be an expression, or a dict of expressions for each column. # Always convert into a {column: expression}. # But carefully, handling {value: ...} in expr mode and {expr: ...} in literal mode if (not isinstance(cmdspec, dict) or (expr_mode and 'value' in cmdspec) or (not expr_mode and 'expr' in cmdspec)): cmdspec = {column: cmdspec for column in table_data.columns} # Apply commands that run on each cell if key in table_cell_commands: for i, (index, row) in enumerate(table_data.iterrows()): for j, (column, val) in enumerate(row.iteritems()): data['cell'] = AttrDict(val=val, column=column, index=index, row=row, data=table_data, pos=AttrDict(row=i, column=j)) cell = table.cell(i + header_offset, j) if column in cmdspec: table_cell_commands[key](cell, cmdspec[column], data) for column in cmdspec: if column not in columns: app_log.warn('pptgen2: No column: %s in table: %s', column, shape.name) # Apply commands that run on each column elif key in table_col_commands: for column, colspec in cmdspec.items(): if column in columns: col_index = columns.index(column) data['cell'] = AttrDict(val=column, column=column, index=None, row=table.columns, data=table_data, pos=AttrDict(row=-1, column=col_index)) table_col_commands[key](table, col_index, colspec, data) else: app_log.warn('pptgen2: No column: %s in table: %s', column, shape.name)
def pptgen(source: Union[str, pptx.presentation.Presentation], rules: List[dict] = [], data: dict = {}, target: str = None, only: Union[int, List[int]] = None, register: Dict[str, str] = {}, unit: str = 'Inches', mode: str = 'literal', handler=None, **config) -> pptx.presentation.Presentation: ''' Process a configuration. This loads a Presentation from source, applies the (optional) configuration changes and (optionally) saves it into target. Returns the modified :arg PPTX source: string or pptx.Presentation object to transform :arg list rules: list of rules to apply to the ``source`` PPTX. Each rule :arg str target: optional path save file :arg int/list only: slide number(s) to process. 1 is the first slide. [1, 3] is slides 1 & 3 :arg dict register: new commands to register via :py:func:`register_commands`. :arg str unit: default length unit (Inches, Cm, Centipoints, etc) :arg str mode: default expression mode. Values in Python are treated as 'literals' (e.g. 'red' is the STRING red). But PPTXHandler passes the mode as `expr`. Values are treated as expressions (e.g. 'red' is the VARIABLE red). :arg handler: if PPTXHandler passes a handler, make it available to the commands as a variable :return: target PPTX ''' # TODO: source can be an expression. PPTXHandler may need to use multiple themes prs = source if isinstance(source, pptx.presentation.Presentation) else Presentation(source) # Load data with additional variables: # prs: source presentation # handler: if PPTXHandler passes a handler, allow commands to use it as a variable # _expr_mode: Allows commands.expr() to evaluate specs as literals or expressions correctly data = load_data(data, handler=handler, prs=prs, _expr_mode='expr' in mode.lower()) register_commands(register) commands.length_unit = commands.length_class(unit) slides = pick_only_slides(prs, only) # PPTX applies transforms to groups. Flatten these so that changing position works as expected for slide in slides: for shape in slide.shapes: commands.flatten_group_transforms(shape) # copy-slide: can copy any set of slides multiple times. To track of which source slide maps to # which target slide, we use `slide_map`. slide_map[target_slide_index] = source_slide_index slide_map = [index for index in range(len(slides))] # Loop through each rule (copying them to protect from modification) for rule in copy.deepcopy(rules): slides_in_rule = tuple(slide_filter(slides, rule, data)) # If no slides matched, warn the user if len(slides_in_rule) == 0: app_log.warn('pptgen2: No slide with slide-number: %s, slide-title: %s', rule.get('slide-number'), rule.get('slide-title')) continue # Copy slides after the last mapped position of the last slide in this rule max_index = max(index for index, slide in slides_in_rule) copy_pos = next(i for i, v in reversed(tuple(enumerate(slide_map))) if max_index == v) # Copy all slides into the `copies` list BEFORE applying any rules. Ensures that rules # applied to slide 1 don't propagate into 2, 3, etc. copies = [] copy_seq = iterate_on(rule.get('copy-slide', [None]), data) for i, (copy_key, copy_val) in enumerate(copy_seq): copy_row = AttrDict(pos=i, key=copy_key, val=copy_val, slides=[]) copies.append(copy_row) for index, slide in slides_in_rule: if i > 0: copy_pos += 1 slide = copy_slide(prs, slide, copy_pos) slide_map.insert(copy_pos, index) copy_row.slides.append(slide) # Apply rules on all copied slides for copy_row in copies: # Include rule-level `data:`. Add copy, slide as variables slide_data = load_data( rule.get('data', {}), _default_key='function', copy=copy_row, **data) for slide in copy_row.slides: slide_data['slide'] = slide # Rule can use 'slide' as a variable transition(slide, rule.get('transition', None), data) apply_commands(rule, slide.shapes, slide_data) if target: prs.save(target) return prs