Ejemplo n.º 1
0
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)
Ejemplo n.º 2
0
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)
Ejemplo n.º 3
0
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