pptxfill 0.2.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pptxfill/__init__.py +21 -0
- pptxfill/eq.py +66 -0
- pptxfill/loader.py +57 -0
- pptxfill/picture.py +73 -0
- pptxfill/slides.py +93 -0
- pptxfill/table.py +105 -0
- pptxfill/text.py +23 -0
- pptxfill/walker.py +48 -0
- pptxfill-0.2.5.dist-info/METADATA +34 -0
- pptxfill-0.2.5.dist-info/RECORD +11 -0
- pptxfill-0.2.5.dist-info/WHEEL +4 -0
pptxfill/__init__.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from .loader import load_template
|
|
2
|
+
from .picture import replace_image, replace_image_by_name
|
|
3
|
+
from .slides import copy_slide_content, delete_slide, insert_slide, multiply_slide
|
|
4
|
+
from .table import fill_table, fill_table_by_name
|
|
5
|
+
from .text import format_text_on_slide
|
|
6
|
+
from .walker import find_shape_by_name, walk_shapes
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"load_template",
|
|
10
|
+
"replace_image",
|
|
11
|
+
"replace_image_by_name",
|
|
12
|
+
"copy_slide_content",
|
|
13
|
+
"delete_slide",
|
|
14
|
+
"insert_slide",
|
|
15
|
+
"multiply_slide",
|
|
16
|
+
"fill_table",
|
|
17
|
+
"fill_table_by_name",
|
|
18
|
+
"format_text_on_slide",
|
|
19
|
+
"walk_shapes",
|
|
20
|
+
"find_shape_by_name",
|
|
21
|
+
]
|
pptxfill/eq.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from typing import Callable, Optional, cast
|
|
2
|
+
|
|
3
|
+
from lxml.etree import Element, QName
|
|
4
|
+
from lxml.etree import tostring as lxml_tostring
|
|
5
|
+
from pptx.oxml.action import CT_Hyperlink
|
|
6
|
+
from pptx.oxml.text import CT_TextCharacterProperties, CT_TextFont
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def optional_eq[T](v1: Optional[T], v2: Optional[T], eq: Callable[[T, T], bool]):
|
|
10
|
+
if v1 is None or v2 is None:
|
|
11
|
+
return (v1 is None) == (v2 is None)
|
|
12
|
+
|
|
13
|
+
return eq(v1, v2)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def get_fill_properties(rpr: CT_TextCharacterProperties):
|
|
17
|
+
"""rPr.eg_fillProperties doesn't cover all fill-related tags, so using a custom method"""
|
|
18
|
+
return next(
|
|
19
|
+
filter(
|
|
20
|
+
lambda el: not isinstance(el.tag, bytearray)
|
|
21
|
+
and QName(el.tag).localname
|
|
22
|
+
in {
|
|
23
|
+
"noFill",
|
|
24
|
+
"solidFill",
|
|
25
|
+
"gradFill",
|
|
26
|
+
"blipFill",
|
|
27
|
+
"pattFill",
|
|
28
|
+
"grpFill",
|
|
29
|
+
"effectLst",
|
|
30
|
+
"effectDag",
|
|
31
|
+
"highlight",
|
|
32
|
+
"uLnTx",
|
|
33
|
+
"uLn",
|
|
34
|
+
"uFillTx",
|
|
35
|
+
"uFill",
|
|
36
|
+
},
|
|
37
|
+
cast(Element, rpr),
|
|
38
|
+
),
|
|
39
|
+
None,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def elements_eq(el1: Element, el2: Element):
|
|
44
|
+
return lxml_tostring(el1) == lxml_tostring(el2)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def text_font_eq(t1: CT_TextFont, t2: CT_TextFont):
|
|
48
|
+
return t1.typeface == t2.typeface
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def hyperlink_eq(h1: CT_Hyperlink, h2: CT_Hyperlink):
|
|
52
|
+
return h1.rId == h2.rId and h1.action == h2.action
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def text_character_properties_eq(
|
|
56
|
+
rpr1: CT_TextCharacterProperties, rpr2: CT_TextCharacterProperties
|
|
57
|
+
):
|
|
58
|
+
return (
|
|
59
|
+
optional_eq(get_fill_properties(rpr1), get_fill_properties(rpr2), elements_eq)
|
|
60
|
+
and optional_eq(rpr1.latin, rpr2.latin, text_font_eq)
|
|
61
|
+
and optional_eq(rpr1.hlinkClick, rpr2.hlinkClick, hyperlink_eq)
|
|
62
|
+
and rpr1.sz == rpr2.sz
|
|
63
|
+
and rpr1.b == rpr2.b
|
|
64
|
+
and rpr1.i == rpr2.i
|
|
65
|
+
and rpr1.u == rpr2.u
|
|
66
|
+
)
|
pptxfill/loader.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from typing import IO
|
|
2
|
+
|
|
3
|
+
from lxml.etree import Element
|
|
4
|
+
from pptx import Presentation as new_presentation
|
|
5
|
+
from pptx.oxml.text import CT_RegularTextRun
|
|
6
|
+
from pptx.shapes.autoshape import Shape
|
|
7
|
+
|
|
8
|
+
from .eq import optional_eq, text_character_properties_eq
|
|
9
|
+
from .walker import walk_shapes
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def merge_spans_with_similar_style(parent: Element):
|
|
13
|
+
children = list(parent)
|
|
14
|
+
|
|
15
|
+
merged = [children[0]]
|
|
16
|
+
for i in range(1, len(children)):
|
|
17
|
+
last_part = merged[-1]
|
|
18
|
+
next_part = children[i]
|
|
19
|
+
|
|
20
|
+
if (
|
|
21
|
+
isinstance(last_part, CT_RegularTextRun)
|
|
22
|
+
and isinstance(next_part, CT_RegularTextRun)
|
|
23
|
+
and optional_eq(last_part.rPr, next_part.rPr, text_character_properties_eq)
|
|
24
|
+
):
|
|
25
|
+
last_part.text += next_part.text
|
|
26
|
+
else:
|
|
27
|
+
merged.append(next_part)
|
|
28
|
+
|
|
29
|
+
parent.clear()
|
|
30
|
+
parent.extend(merged)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def load_template(pptx: str | IO[bytes]):
|
|
34
|
+
"""Loads a presentation similarly to :func:`pptx.Presentation`,
|
|
35
|
+
|
|
36
|
+
fixing split tags such as:
|
|
37
|
+
|
|
38
|
+
.. code-block:: xml
|
|
39
|
+
|
|
40
|
+
<a:r>
|
|
41
|
+
<a:t>{uni_full</a:t>
|
|
42
|
+
</a:r>
|
|
43
|
+
<a:r>
|
|
44
|
+
<a:t>_name}</a:t>
|
|
45
|
+
</a:r>
|
|
46
|
+
|
|
47
|
+
to make future formatting easier
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
template = new_presentation(pptx)
|
|
51
|
+
|
|
52
|
+
for slide in template.slides:
|
|
53
|
+
for shape in walk_shapes(slide, Shape):
|
|
54
|
+
for paragraph in shape.text_frame.paragraphs:
|
|
55
|
+
merge_spans_with_similar_style(paragraph._element)
|
|
56
|
+
|
|
57
|
+
return template
|
pptxfill/picture.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import IO, cast
|
|
3
|
+
|
|
4
|
+
from lxml.etree import Element
|
|
5
|
+
from pptx.shapes.base import BaseShape
|
|
6
|
+
from pptx.shapes.picture import Picture
|
|
7
|
+
from pptx.slide import Slide
|
|
8
|
+
|
|
9
|
+
from .walker import find_shape_by_name
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def replace_image(
|
|
13
|
+
slide: Slide,
|
|
14
|
+
old_pic: BaseShape,
|
|
15
|
+
image: Path | str | IO[bytes],
|
|
16
|
+
full_width: bool = True,
|
|
17
|
+
full_height: bool = True,
|
|
18
|
+
) -> Picture:
|
|
19
|
+
"""Replace contents of a :class:`Picture <pptx.shapes.picture.Picture>` shape
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
slide: target slide
|
|
23
|
+
old_pic: shape to replace
|
|
24
|
+
image: new image file
|
|
25
|
+
full_width: stretch to original shape's width
|
|
26
|
+
full_height: stretch to original shape's height
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
if isinstance(image, Path):
|
|
30
|
+
image = str(image)
|
|
31
|
+
|
|
32
|
+
new_pic = slide.shapes.add_picture(
|
|
33
|
+
image,
|
|
34
|
+
left=old_pic.left,
|
|
35
|
+
top=old_pic.top,
|
|
36
|
+
width=old_pic.width if full_width else None,
|
|
37
|
+
height=old_pic.height if full_height else None,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
old_pic_element = cast(Element, old_pic._element)
|
|
41
|
+
old_pic_element.addnext(new_pic._element)
|
|
42
|
+
parent = old_pic_element.getparent()
|
|
43
|
+
if parent is not None:
|
|
44
|
+
parent.remove(old_pic_element)
|
|
45
|
+
|
|
46
|
+
new_pic.name = old_pic.name
|
|
47
|
+
return new_pic
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def replace_image_by_name(
|
|
51
|
+
slide: Slide,
|
|
52
|
+
name: str,
|
|
53
|
+
image: Path | str | IO[bytes],
|
|
54
|
+
full_width: bool = True,
|
|
55
|
+
full_height: bool = True,
|
|
56
|
+
):
|
|
57
|
+
"""Shorthand for :func:`pptxfill.find_shape_by_name` + :func:`pptxfill.replace_image`
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
slide: target slide
|
|
61
|
+
name: target shape name
|
|
62
|
+
image: new image file
|
|
63
|
+
full_width: stretch to original shape's width
|
|
64
|
+
full_height: stretch to original shape's height
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
return replace_image(
|
|
68
|
+
slide,
|
|
69
|
+
find_shape_by_name(slide, name),
|
|
70
|
+
image,
|
|
71
|
+
full_width=full_width,
|
|
72
|
+
full_height=full_height,
|
|
73
|
+
)
|
pptxfill/slides.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import io
|
|
2
|
+
from copy import deepcopy
|
|
3
|
+
|
|
4
|
+
from pptx.shapes.picture import Picture
|
|
5
|
+
from pptx.slide import Slide, SlideLayout, Slides
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def insert_slide(slides: Slides, layout: SlideLayout, position: int) -> Slide:
|
|
9
|
+
"""Insert a new slide into the slide list
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
slides: the slide list (usually :attr:`pptx.presentation.Presentation.slides`)
|
|
13
|
+
layout: layout to base the new slide off
|
|
14
|
+
position: index where the new slide will be inserted
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
the new slide
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
new_slide = slides.add_slide(layout)
|
|
21
|
+
|
|
22
|
+
slide_ids = slides._sldIdLst
|
|
23
|
+
new_id = slide_ids[-1]
|
|
24
|
+
slide_ids.remove(new_id)
|
|
25
|
+
slide_ids.insert(position, new_id)
|
|
26
|
+
|
|
27
|
+
return new_slide
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def copy_slide_content(source: Slide, target: Slide):
|
|
31
|
+
"""Copy XML tree and images to the new slide"""
|
|
32
|
+
|
|
33
|
+
for shape in target.shapes:
|
|
34
|
+
target.shapes.element.remove(shape.element)
|
|
35
|
+
|
|
36
|
+
for shape in source.shapes:
|
|
37
|
+
if isinstance(shape, Picture):
|
|
38
|
+
img = io.BytesIO(shape.image.blob)
|
|
39
|
+
new_shape = target.shapes.add_picture(
|
|
40
|
+
img,
|
|
41
|
+
left=shape.left,
|
|
42
|
+
top=shape.top,
|
|
43
|
+
width=shape.width,
|
|
44
|
+
height=shape.height,
|
|
45
|
+
)
|
|
46
|
+
new_shape.name = shape.name
|
|
47
|
+
else:
|
|
48
|
+
target.shapes._spTree.insert_element_before(
|
|
49
|
+
deepcopy(shape.element), "p:extList"
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def delete_slide(slides: Slides, index: int):
|
|
54
|
+
"""Delete slide at index
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
slides: the slide list (usually :attr:`pptx.presentation.Presentation.slides`)
|
|
58
|
+
index: target index
|
|
59
|
+
"""
|
|
60
|
+
slides._sldIdLst.remove(slides._sldIdLst[index])
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def multiply_slide(slides: Slides, source: Slide, n: int) -> list[Slide]:
|
|
64
|
+
"""Replace a single slide with N copies of itself
|
|
65
|
+
|
|
66
|
+
Useful for templating where you need a certain slide to repeat.
|
|
67
|
+
The new slides will be inserted at the original slide's index
|
|
68
|
+
and will have the original slides's content copied into them.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
slides: the slide list (usually :attr:`pptx.presentation.Presentation.slides`)
|
|
72
|
+
source: template slide
|
|
73
|
+
n: number of new slides
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
list of slides with length N
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
source_index = slides.index(source)
|
|
80
|
+
|
|
81
|
+
if n == 0:
|
|
82
|
+
delete_slide(slides, source_index)
|
|
83
|
+
return []
|
|
84
|
+
else:
|
|
85
|
+
slide_list = [source]
|
|
86
|
+
|
|
87
|
+
for _ in range(n - 1):
|
|
88
|
+
new_slide = insert_slide(slides, source.slide_layout, source_index)
|
|
89
|
+
copy_slide_content(source, new_slide)
|
|
90
|
+
slide_list.append(new_slide)
|
|
91
|
+
|
|
92
|
+
slide_list.reverse()
|
|
93
|
+
return slide_list
|
pptxfill/table.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
from copy import deepcopy
|
|
2
|
+
from itertools import islice
|
|
3
|
+
from typing import Any, cast
|
|
4
|
+
|
|
5
|
+
from lxml.etree import Element
|
|
6
|
+
from pandas import DataFrame
|
|
7
|
+
from pptx.oxml.text import CT_RegularTextRun
|
|
8
|
+
from pptx.shapes.graphfrm import GraphicFrame
|
|
9
|
+
from pptx.slide import Slide
|
|
10
|
+
from pptx.table import Table, _Cell
|
|
11
|
+
|
|
12
|
+
from .loader import merge_spans_with_similar_style
|
|
13
|
+
from .walker import find_shape_by_name
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def get_cell_template(cell: _Cell):
|
|
17
|
+
for paragraph in cell.text_frame.paragraphs:
|
|
18
|
+
merge_spans_with_similar_style(paragraph._element)
|
|
19
|
+
|
|
20
|
+
return list(cast(Element, cell.text_frame._txBody))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def set_cell_value(cell: _Cell, template: list[Element], value: Any):
|
|
24
|
+
text_body = cast(Element, cell.text_frame._txBody)
|
|
25
|
+
text_body.clear()
|
|
26
|
+
|
|
27
|
+
for template_element in template:
|
|
28
|
+
text_body.append(deepcopy(template_element))
|
|
29
|
+
|
|
30
|
+
for paragraph in cell.text_frame.paragraphs:
|
|
31
|
+
for tag in cast(Element, paragraph._element):
|
|
32
|
+
if isinstance(tag, CT_RegularTextRun):
|
|
33
|
+
tag.text = tag.text.format(value)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def fill_table(
|
|
37
|
+
table: Table,
|
|
38
|
+
data: DataFrame,
|
|
39
|
+
skip_header: bool = True,
|
|
40
|
+
rownum: bool = False,
|
|
41
|
+
):
|
|
42
|
+
"""Fill :class:`pptx.table.Table` object with data from a :class:`pandas.DataFrame`
|
|
43
|
+
|
|
44
|
+
Does not expand the table, data will be cut off where the table ends.
|
|
45
|
+
|
|
46
|
+
In the template, the first row must be filled with format strings, for example:
|
|
47
|
+
|
|
48
|
+
.. list-table::
|
|
49
|
+
|
|
50
|
+
* - Name
|
|
51
|
+
- Number
|
|
52
|
+
- Percent
|
|
53
|
+
* - {}
|
|
54
|
+
- {:.2f}
|
|
55
|
+
- {}%
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
table: table in the presentation
|
|
59
|
+
data: fill with this data
|
|
60
|
+
skip_header: consider the first row the header, begin filling table from the second row
|
|
61
|
+
rownum: fill the first column with row numbers, data begins from the second column
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
row_start = 1 if skip_header else 0
|
|
65
|
+
height = min(len(table.rows) - row_start, len(data))
|
|
66
|
+
|
|
67
|
+
template = [
|
|
68
|
+
get_cell_template(table.cell(row_start, i)) for i in range(len(table.columns))
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
if rownum:
|
|
72
|
+
for y, index in enumerate(islice(data.index, height)):
|
|
73
|
+
set_cell_value(table.cell(row_start + y, 0), template[0], str(index))
|
|
74
|
+
|
|
75
|
+
col_start = 1 if rownum else 0
|
|
76
|
+
width = min(len(table.columns) - col_start, len(data.columns))
|
|
77
|
+
|
|
78
|
+
for x, (_, column) in enumerate(islice(data.items(), width)):
|
|
79
|
+
for y, value in enumerate(islice(column, height)):
|
|
80
|
+
set_cell_value(
|
|
81
|
+
table.cell(row_start + y, col_start + x),
|
|
82
|
+
template[col_start + x],
|
|
83
|
+
value,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def fill_table_by_name(
|
|
88
|
+
slide: Slide,
|
|
89
|
+
name: str,
|
|
90
|
+
data: DataFrame,
|
|
91
|
+
skip_header: bool = True,
|
|
92
|
+
rownum: bool = False,
|
|
93
|
+
):
|
|
94
|
+
"""Shorthand for :func:`pptxfill.find_shape_by_name` + :func:`pptxfill.fill_table`
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
slide: target slide
|
|
98
|
+
name: target shape's name
|
|
99
|
+
data: fill with this data
|
|
100
|
+
skip_header: consider the first row the header, begin filling table from the second row
|
|
101
|
+
rownum: fill the first column with row numbers, data begins from the second column
|
|
102
|
+
"""
|
|
103
|
+
table = find_shape_by_name(slide, name, GraphicFrame).table
|
|
104
|
+
fill_table(table, data, skip_header=skip_header, rownum=rownum)
|
|
105
|
+
return table
|
pptxfill/text.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from typing import cast
|
|
2
|
+
|
|
3
|
+
from lxml.etree import Element
|
|
4
|
+
from pptx.oxml.text import CT_RegularTextRun
|
|
5
|
+
from pptx.shapes.autoshape import Shape
|
|
6
|
+
from pptx.slide import Slide
|
|
7
|
+
|
|
8
|
+
from .walker import walk_shapes
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def format_text_on_slide(slide: Slide, **format_args):
|
|
12
|
+
"""Walk the slides's shape tree, applying :meth:`str.format` to each text frame
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
slide: the target slide
|
|
16
|
+
*format_args: identical to :meth:`str.format` arguments
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
for shape in walk_shapes(slide, Shape):
|
|
20
|
+
for paragraph in shape.text_frame.paragraphs:
|
|
21
|
+
for tag in cast(Element, paragraph._element):
|
|
22
|
+
if isinstance(tag, CT_RegularTextRun):
|
|
23
|
+
tag.text = tag.text.format(**format_args)
|
pptxfill/walker.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from typing import Iterator
|
|
2
|
+
|
|
3
|
+
from pptx.shapes.base import BaseShape
|
|
4
|
+
from pptx.shapes.group import GroupShape
|
|
5
|
+
from pptx.shapes.shapetree import _BaseGroupShapes
|
|
6
|
+
from pptx.slide import Slide
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def walk_group_shapes[T: BaseShape](
|
|
10
|
+
group: _BaseGroupShapes, t: type[T] = BaseShape
|
|
11
|
+
) -> Iterator[T]:
|
|
12
|
+
for shape in group:
|
|
13
|
+
if isinstance(shape, t):
|
|
14
|
+
yield shape
|
|
15
|
+
|
|
16
|
+
if isinstance(shape, GroupShape):
|
|
17
|
+
yield from walk_group_shapes(shape.shapes, t)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def walk_shapes[T: BaseShape](slide: Slide, t: type[T] = BaseShape) -> Iterator[T]:
|
|
21
|
+
"""Walks the shape tree of the slide, visiting each :class:`GroupShape <pptx.shapes.group.GroupShape>` contents
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
slide: target slide
|
|
25
|
+
t: filter by shape type (if omitted, will visit all shapes)
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
yield from walk_group_shapes(slide.shapes, t)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def find_shape_by_name[T: BaseShape](
|
|
32
|
+
slide: Slide, name: str, t: type[T] = BaseShape
|
|
33
|
+
) -> T:
|
|
34
|
+
"""Walk the slide's shape tree and find shape with the given name
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
slide: target slide
|
|
38
|
+
name: target shape name
|
|
39
|
+
t: target shape's type
|
|
40
|
+
|
|
41
|
+
Raises:
|
|
42
|
+
RuntimeError: If shape wasn't found
|
|
43
|
+
"""
|
|
44
|
+
for shape in walk_shapes(slide, t):
|
|
45
|
+
if shape.name == name:
|
|
46
|
+
return shape
|
|
47
|
+
|
|
48
|
+
raise RuntimeError(f"Shape '{name}' of type {t} not found")
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: pptxfill
|
|
3
|
+
Version: 0.2.5
|
|
4
|
+
Summary: Add your description here
|
|
5
|
+
Project-URL: Repository, https://gitlab.com/csml-tools/pptxfill/
|
|
6
|
+
Project-URL: Documentation, https://csml-tools.gitlab.io/pptxfill/
|
|
7
|
+
Requires-Python: >=3.13
|
|
8
|
+
Requires-Dist: pandas>=2.3.3
|
|
9
|
+
Requires-Dist: python-pptx>=1.0.2
|
|
10
|
+
Requires-Dist: types-lxml>=2026.1.1
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# pptxfill
|
|
14
|
+
|
|
15
|
+
**pptxfill** is a templating library for PowerPoint presentations.
|
|
16
|
+
|
|
17
|
+
Unlike other similar template engines, this one does not rely on Slide Layouts,
|
|
18
|
+
instead using a more flexible system similar to ones used for Word documents,
|
|
19
|
+
where you can substitute variables in text blocks using string formatting,
|
|
20
|
+
replace arbitrary images and fill tables with data.
|
|
21
|
+
|
|
22
|
+
[Documentation](https://csml-tools.gitlab.io/pptxfill/)
|
|
23
|
+
|
|
24
|
+
## Installation:
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
uv add pptxfill
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
or
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
pip install pptxfill
|
|
34
|
+
```
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
pptxfill/__init__.py,sha256=T-OZylf9hyoqJ9WiKqSEg8ZYYbnrS141uF2spkmWgbU,607
|
|
2
|
+
pptxfill/eq.py,sha256=JlZ1f5tzKpgtrWistRY_8E5MzWLtOt8m5Jrc57CSJ-o,1927
|
|
3
|
+
pptxfill/loader.py,sha256=K7vaBHaJQ6gqtWblmgmE_TG3EgGRqgJGd079TL5KsMo,1462
|
|
4
|
+
pptxfill/picture.py,sha256=-FQMNlczPgw0fP3mvaQknuUdWG7s83ZnMpkAeEjavEA,1886
|
|
5
|
+
pptxfill/slides.py,sha256=bqLtTaQaAmwS0Y8aP2Mz7DgUqMiG77W6bFaRMwmCMoo,2643
|
|
6
|
+
pptxfill/table.py,sha256=KNrAN6t2lL7Zmvu6Q1SnIRpoItpzwOg6yG8UBSvjeGs,3248
|
|
7
|
+
pptxfill/text.py,sha256=5saIUGwm58y2HjeOTHNE0GWcPFkgPCu1Xdp1OvgfjwQ,733
|
|
8
|
+
pptxfill/walker.py,sha256=uXYo7SDXJ4rRaK2sJg0I47bp44AacpdmL9U4ownyOuc,1346
|
|
9
|
+
pptxfill-0.2.5.dist-info/METADATA,sha256=YUqmgQlaxZkC6Q3loD1S7xxS4qnfunVkP8jrhHbCSmc,883
|
|
10
|
+
pptxfill-0.2.5.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
|
|
11
|
+
pptxfill-0.2.5.dist-info/RECORD,,
|