pptx-lint 0.1.0__tar.gz
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.
- pptx_lint-0.1.0/LICENSE +21 -0
- pptx_lint-0.1.0/PKG-INFO +129 -0
- pptx_lint-0.1.0/README.md +114 -0
- pptx_lint-0.1.0/pyproject.toml +34 -0
- pptx_lint-0.1.0/setup.cfg +4 -0
- pptx_lint-0.1.0/src/pptx_lint/__init__.py +7 -0
- pptx_lint-0.1.0/src/pptx_lint/checker.py +163 -0
- pptx_lint-0.1.0/src/pptx_lint/cli.py +72 -0
- pptx_lint-0.1.0/src/pptx_lint/reporters/__init__.py +1 -0
- pptx_lint-0.1.0/src/pptx_lint/reporters/console.py +58 -0
- pptx_lint-0.1.0/src/pptx_lint/rules/__init__.py +8 -0
- pptx_lint-0.1.0/src/pptx_lint/rules/fake_table.py +65 -0
- pptx_lint-0.1.0/src/pptx_lint/rules/font_size.py +18 -0
- pptx_lint-0.1.0/src/pptx_lint/rules/overflow.py +45 -0
- pptx_lint-0.1.0/src/pptx_lint/rules/overlap.py +48 -0
- pptx_lint-0.1.0/src/pptx_lint/utils.py +44 -0
- pptx_lint-0.1.0/src/pptx_lint.egg-info/PKG-INFO +129 -0
- pptx_lint-0.1.0/src/pptx_lint.egg-info/SOURCES.txt +21 -0
- pptx_lint-0.1.0/src/pptx_lint.egg-info/dependency_links.txt +1 -0
- pptx_lint-0.1.0/src/pptx_lint.egg-info/entry_points.txt +2 -0
- pptx_lint-0.1.0/src/pptx_lint.egg-info/requires.txt +5 -0
- pptx_lint-0.1.0/src/pptx_lint.egg-info/top_level.txt +1 -0
- pptx_lint-0.1.0/tests/test_checker.py +155 -0
pptx_lint-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 pptx-lint contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
pptx_lint-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pptx-lint
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Lint / quality checker for python-pptx presentations â detects overflow, overlap, fake tables, small fonts, and empty slides
|
|
5
|
+
License: MIT
|
|
6
|
+
Keywords: pptx,powerpoint,lint,quality,python-pptx,presentation
|
|
7
|
+
Requires-Python: >=3.9
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: python-pptx>=0.6.21
|
|
11
|
+
Provides-Extra: dev
|
|
12
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
13
|
+
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
14
|
+
Dynamic: license-file
|
|
15
|
+
|
|
16
|
+
# pptx-lint
|
|
17
|
+
|
|
18
|
+
**Lint / quality checker for python-pptx presentations.**
|
|
19
|
+
|
|
20
|
+
[](https://pypi.org/project/pptx-lint/)
|
|
21
|
+
[](https://pypi.org/project/pptx-lint/)
|
|
22
|
+
[](LICENSE)
|
|
23
|
+
[](https://github.com/LeppardWang/pptx-lint/actions/workflows/test.yml)
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Why?
|
|
28
|
+
|
|
29
|
+
If you generate PowerPoint files with [python-pptx](https://python-pptx.readthedocs.io/),
|
|
30
|
+
you've probably run into these problems:
|
|
31
|
+
|
|
32
|
+
- đ´ **Overflow** â Text boxes or tables exceeding slide boundaries
|
|
33
|
+
- đĄ **Overlap** â Shapes covering each other because of hard-coded coordinates
|
|
34
|
+
- đĄ **Fake tables** â Multiple text boxes trying (and failing) to look like a table
|
|
35
|
+
- âšī¸ **Small fonts** â Text too small to read
|
|
36
|
+
- đ´ **Empty slides** â Slides without any content
|
|
37
|
+
|
|
38
|
+
**pptx-lint** automatically detects all of these, giving you a clear report
|
|
39
|
+
so you can fix them *before* presenting.
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Quick start
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install pptx-lint
|
|
47
|
+
|
|
48
|
+
# Check a single file
|
|
49
|
+
pptx-lint presentation.pptx
|
|
50
|
+
|
|
51
|
+
# Check all files in a directory
|
|
52
|
+
pptx-lint ./output/
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## Example output
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
======================================================================
|
|
61
|
+
pptx-lint report
|
|
62
|
+
File: my_presentation.pptx
|
|
63
|
+
======================================================================
|
|
64
|
+
Slides: 12 | Empty: 0
|
|
65
|
+
Errors: 3 | Warnings: 5 | Infos: 2
|
|
66
|
+
======================================================================
|
|
67
|
+
|
|
68
|
+
[Slide 4]
|
|
69
|
+
Content: Performance Metrics | Results
|
|
70
|
+
â [Overflow] Shape#3 (Performance table): right overflow (r=12.80" > 10.00")
|
|
71
|
+
Ⲡ[FakeTable] 6 text boxes form a grid (e.g.: Precision, Recall)
|
|
72
|
+
|
|
73
|
+
[Slide 7]
|
|
74
|
+
Content: System Architecture Overview
|
|
75
|
+
Ⲡ[Overlap] Shape#2 overlaps Shape#4 â overlap area: 2.30 sq.in.
|
|
76
|
+
|
|
77
|
+
[Slide 9]
|
|
78
|
+
Content: References
|
|
79
|
+
i [SmallFont] Shape#1: 'Acknowledgements' font = 7pt (< 8pt)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## Checks
|
|
85
|
+
|
|
86
|
+
| Rule | Severity | What it detects |
|
|
87
|
+
|------|----------|-----------------|
|
|
88
|
+
| **Overflow** | đ´ Error | Shapes whose `left`/`top` < 0 or `right`/`bottom` > slide dimensions |
|
|
89
|
+
| **Empty slide** | đ´ Error | Slides with zero shapes |
|
|
90
|
+
| **Overlap** | đĄ Warning | Two shapes overlapping > 50% of the smaller shape's area |
|
|
91
|
+
| **Fake table** | đĄ Warning | âĨ4 text boxes arranged in a âĨ2Ã2 grid (should use `add_table()`) |
|
|
92
|
+
| **Small font** | âšī¸ Info | Text smaller than 8 pt |
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## Use as a library
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
from pptx_lint.checker import check_pptx
|
|
100
|
+
|
|
101
|
+
results, filename = check_pptx("my_slides.pptx")
|
|
102
|
+
for slide in results:
|
|
103
|
+
for err in slide['errors']:
|
|
104
|
+
print(f"Slide {slide['slide_num']}: {err['detail']}")
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## Development
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
git clone https://github.com/LeppardWang/pptx-lint.git
|
|
113
|
+
cd pptx-lint
|
|
114
|
+
pip install -e ".[dev]"
|
|
115
|
+
pytest
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## Related
|
|
121
|
+
|
|
122
|
+
- [python-pptx](https://python-pptx.readthedocs.io/) â the library that creates `.pptx` files
|
|
123
|
+
- [python-pptx-table](https://pypi.org/project/python-pptx-table/) â helpers for Table objects
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## License
|
|
128
|
+
|
|
129
|
+
MIT
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# pptx-lint
|
|
2
|
+
|
|
3
|
+
**Lint / quality checker for python-pptx presentations.**
|
|
4
|
+
|
|
5
|
+
[](https://pypi.org/project/pptx-lint/)
|
|
6
|
+
[](https://pypi.org/project/pptx-lint/)
|
|
7
|
+
[](LICENSE)
|
|
8
|
+
[](https://github.com/LeppardWang/pptx-lint/actions/workflows/test.yml)
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Why?
|
|
13
|
+
|
|
14
|
+
If you generate PowerPoint files with [python-pptx](https://python-pptx.readthedocs.io/),
|
|
15
|
+
you've probably run into these problems:
|
|
16
|
+
|
|
17
|
+
- đ´ **Overflow** â Text boxes or tables exceeding slide boundaries
|
|
18
|
+
- đĄ **Overlap** â Shapes covering each other because of hard-coded coordinates
|
|
19
|
+
- đĄ **Fake tables** â Multiple text boxes trying (and failing) to look like a table
|
|
20
|
+
- âšī¸ **Small fonts** â Text too small to read
|
|
21
|
+
- đ´ **Empty slides** â Slides without any content
|
|
22
|
+
|
|
23
|
+
**pptx-lint** automatically detects all of these, giving you a clear report
|
|
24
|
+
so you can fix them *before* presenting.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Quick start
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install pptx-lint
|
|
32
|
+
|
|
33
|
+
# Check a single file
|
|
34
|
+
pptx-lint presentation.pptx
|
|
35
|
+
|
|
36
|
+
# Check all files in a directory
|
|
37
|
+
pptx-lint ./output/
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## Example output
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
======================================================================
|
|
46
|
+
pptx-lint report
|
|
47
|
+
File: my_presentation.pptx
|
|
48
|
+
======================================================================
|
|
49
|
+
Slides: 12 | Empty: 0
|
|
50
|
+
Errors: 3 | Warnings: 5 | Infos: 2
|
|
51
|
+
======================================================================
|
|
52
|
+
|
|
53
|
+
[Slide 4]
|
|
54
|
+
Content: Performance Metrics | Results
|
|
55
|
+
â [Overflow] Shape#3 (Performance table): right overflow (r=12.80" > 10.00")
|
|
56
|
+
Ⲡ[FakeTable] 6 text boxes form a grid (e.g.: Precision, Recall)
|
|
57
|
+
|
|
58
|
+
[Slide 7]
|
|
59
|
+
Content: System Architecture Overview
|
|
60
|
+
Ⲡ[Overlap] Shape#2 overlaps Shape#4 â overlap area: 2.30 sq.in.
|
|
61
|
+
|
|
62
|
+
[Slide 9]
|
|
63
|
+
Content: References
|
|
64
|
+
i [SmallFont] Shape#1: 'Acknowledgements' font = 7pt (< 8pt)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## Checks
|
|
70
|
+
|
|
71
|
+
| Rule | Severity | What it detects |
|
|
72
|
+
|------|----------|-----------------|
|
|
73
|
+
| **Overflow** | đ´ Error | Shapes whose `left`/`top` < 0 or `right`/`bottom` > slide dimensions |
|
|
74
|
+
| **Empty slide** | đ´ Error | Slides with zero shapes |
|
|
75
|
+
| **Overlap** | đĄ Warning | Two shapes overlapping > 50% of the smaller shape's area |
|
|
76
|
+
| **Fake table** | đĄ Warning | âĨ4 text boxes arranged in a âĨ2Ã2 grid (should use `add_table()`) |
|
|
77
|
+
| **Small font** | âšī¸ Info | Text smaller than 8 pt |
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Use as a library
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
from pptx_lint.checker import check_pptx
|
|
85
|
+
|
|
86
|
+
results, filename = check_pptx("my_slides.pptx")
|
|
87
|
+
for slide in results:
|
|
88
|
+
for err in slide['errors']:
|
|
89
|
+
print(f"Slide {slide['slide_num']}: {err['detail']}")
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## Development
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
git clone https://github.com/LeppardWang/pptx-lint.git
|
|
98
|
+
cd pptx-lint
|
|
99
|
+
pip install -e ".[dev]"
|
|
100
|
+
pytest
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## Related
|
|
106
|
+
|
|
107
|
+
- [python-pptx](https://python-pptx.readthedocs.io/) â the library that creates `.pptx` files
|
|
108
|
+
- [python-pptx-table](https://pypi.org/project/python-pptx-table/) â helpers for Table objects
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## License
|
|
113
|
+
|
|
114
|
+
MIT
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pptx-lint"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Lint / quality checker for python-pptx presentations â detects overflow, overlap, fake tables, small fonts, and empty slides"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = {text = "MIT"}
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
dependencies = [
|
|
13
|
+
"python-pptx>=0.6.21",
|
|
14
|
+
]
|
|
15
|
+
keywords = ["pptx", "powerpoint", "lint", "quality", "python-pptx", "presentation"]
|
|
16
|
+
|
|
17
|
+
[project.optional-dependencies]
|
|
18
|
+
dev = [
|
|
19
|
+
"pytest>=7.0",
|
|
20
|
+
"ruff>=0.1.0",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[project.scripts]
|
|
24
|
+
pptx-lint = "pptx_lint.cli:main"
|
|
25
|
+
|
|
26
|
+
[tool.setuptools.packages.find]
|
|
27
|
+
where = ["src"]
|
|
28
|
+
|
|
29
|
+
[tool.setuptools.package-data]
|
|
30
|
+
"*" = ["*.md"]
|
|
31
|
+
|
|
32
|
+
[tool.pytest.ini_options]
|
|
33
|
+
testpaths = ["tests"]
|
|
34
|
+
pythonpath = ["src"]
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""Core checker: orchestrate all lint rules against one or more PPTX files."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from pptx import Presentation
|
|
5
|
+
|
|
6
|
+
from .utils import emu_to_inches, emu_to_pt
|
|
7
|
+
from . import rules
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def get_shape_bounds(shape):
|
|
11
|
+
"""Return (left, top, width, height, right, bottom) in inches."""
|
|
12
|
+
try:
|
|
13
|
+
left = emu_to_inches(shape.left)
|
|
14
|
+
top = emu_to_inches(shape.top)
|
|
15
|
+
w = emu_to_inches(shape.width)
|
|
16
|
+
h = emu_to_inches(shape.height)
|
|
17
|
+
return left, top, w, h, left + w, top + h
|
|
18
|
+
except Exception:
|
|
19
|
+
return None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def get_shape_texts(shape):
|
|
23
|
+
"""Extract plain text from a shape (text-frame or table)."""
|
|
24
|
+
texts = []
|
|
25
|
+
if shape.has_text_frame:
|
|
26
|
+
for p in shape.text_frame.paragraphs:
|
|
27
|
+
t = p.text.strip()
|
|
28
|
+
if t:
|
|
29
|
+
texts.append(t)
|
|
30
|
+
if shape.has_table:
|
|
31
|
+
table = shape.table
|
|
32
|
+
for row in table.rows:
|
|
33
|
+
parts = []
|
|
34
|
+
for cell in row.cells:
|
|
35
|
+
t = cell.text.strip()
|
|
36
|
+
if t:
|
|
37
|
+
parts.append(t)
|
|
38
|
+
if parts:
|
|
39
|
+
texts.append(' | '.join(parts))
|
|
40
|
+
return texts
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def collect_shapes_info(slide):
|
|
44
|
+
"""Return list of shape info dicts for a single slide."""
|
|
45
|
+
infos = []
|
|
46
|
+
for idx, shape in enumerate(slide.shapes):
|
|
47
|
+
bounds = get_shape_bounds(shape)
|
|
48
|
+
if bounds is None:
|
|
49
|
+
continue
|
|
50
|
+
|
|
51
|
+
texts = get_shape_texts(shape)
|
|
52
|
+
|
|
53
|
+
font_info = []
|
|
54
|
+
if shape.has_text_frame:
|
|
55
|
+
for p in shape.text_frame.paragraphs:
|
|
56
|
+
if p.text.strip():
|
|
57
|
+
sz = p.font.size
|
|
58
|
+
font_info.append({
|
|
59
|
+
'text': p.text.strip(),
|
|
60
|
+
'size': emu_to_pt(sz) if sz else None,
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
infos.append({
|
|
64
|
+
'idx': idx,
|
|
65
|
+
'bounds': bounds,
|
|
66
|
+
'texts': texts,
|
|
67
|
+
'font_info': font_info,
|
|
68
|
+
'type': str(shape.shape_type),
|
|
69
|
+
})
|
|
70
|
+
return infos
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def check_slide(slide, slide_num, sw, sh):
|
|
74
|
+
"""Run all lint rules against one slide.
|
|
75
|
+
|
|
76
|
+
Returns a dict with keys:
|
|
77
|
+
slide_num, errors, warnings, infos, text_content, is_empty
|
|
78
|
+
"""
|
|
79
|
+
result = {
|
|
80
|
+
'slide_num': slide_num,
|
|
81
|
+
'total_shapes': len(slide.shapes),
|
|
82
|
+
'errors': [],
|
|
83
|
+
'warnings': [],
|
|
84
|
+
'infos': [],
|
|
85
|
+
'text_content': [],
|
|
86
|
+
'is_empty': True,
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
shapes_info = collect_shapes_info(slide)
|
|
90
|
+
for info in shapes_info:
|
|
91
|
+
result['text_content'].extend(info['texts'])
|
|
92
|
+
result['is_empty'] = len(result['text_content']) == 0
|
|
93
|
+
|
|
94
|
+
# ââ overflow (error) ââ
|
|
95
|
+
for issue in rules.overflow.check(sw, sh, shapes_info):
|
|
96
|
+
result['errors'].append({
|
|
97
|
+
'type': 'Overflow',
|
|
98
|
+
'detail': (
|
|
99
|
+
f"Shape#{issue['shape_idx']} ({issue['shape_name']}): "
|
|
100
|
+
f"{' | '.join(issue['issues'])}"
|
|
101
|
+
),
|
|
102
|
+
'bounds': (
|
|
103
|
+
f"({issue['bounds'][0]:.2f}, {issue['bounds'][1]:.2f}, "
|
|
104
|
+
f"{issue['bounds'][2]:.2f}, {issue['bounds'][3]:.2f})"
|
|
105
|
+
),
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
# ââ empty slide (error) ââ
|
|
109
|
+
if result['total_shapes'] == 0:
|
|
110
|
+
result['errors'].append({
|
|
111
|
+
'type': 'EmptySlide',
|
|
112
|
+
'detail': 'Slide has no shapes at all.',
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
# ââ overlap (warning) ââ
|
|
116
|
+
for issue in rules.overlap.check(shapes_info):
|
|
117
|
+
result['warnings'].append({
|
|
118
|
+
'type': 'Overlap',
|
|
119
|
+
'detail': (
|
|
120
|
+
f"Shape#{issue['shapes'][0]} ({issue['names'][0]}) "
|
|
121
|
+
f"overlaps Shape#{issue['shapes'][1]} ({issue['names'][1]}) "
|
|
122
|
+
f"â overlap area: {issue['overlap_area']}"
|
|
123
|
+
),
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
# ââ fake table (warning) ââ
|
|
127
|
+
for issue in rules.fake_table.check(shapes_info):
|
|
128
|
+
samples = ', '.join(issue['sample_texts'])
|
|
129
|
+
result['warnings'].append({
|
|
130
|
+
'type': 'FakeTable',
|
|
131
|
+
'detail': (
|
|
132
|
+
f"{issue['shape_count']} text boxes form a grid-like layout "
|
|
133
|
+
f"(e.g.: {samples}) â consider using ``add_table()`` instead."
|
|
134
|
+
),
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
# ââ small font (info) ââ
|
|
138
|
+
for issue in rules.font_size.check(shapes_info):
|
|
139
|
+
result['infos'].append({
|
|
140
|
+
'type': 'SmallFont',
|
|
141
|
+
'detail': (
|
|
142
|
+
f"Shape#{issue['shape_idx']}: '{issue['text']}' "
|
|
143
|
+
f"font size = {issue['font_size']:.0f}pt "
|
|
144
|
+
f"(< {rules.font_size.MIN_FONT_SIZE}pt)"
|
|
145
|
+
),
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
return result
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def check_pptx(filepath):
|
|
152
|
+
"""Run all checks on a single .pptx file.
|
|
153
|
+
|
|
154
|
+
Returns list of slide-level result dicts.
|
|
155
|
+
"""
|
|
156
|
+
prs = Presentation(filepath)
|
|
157
|
+
sw = emu_to_inches(prs.slide_width)
|
|
158
|
+
sh = emu_to_inches(prs.slide_height)
|
|
159
|
+
|
|
160
|
+
all_results = []
|
|
161
|
+
for si, slide in enumerate(prs.slides):
|
|
162
|
+
all_results.append(check_slide(slide, si + 1, sw, sh))
|
|
163
|
+
return all_results, os.path.basename(filepath)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""Command-line interface for pptx-lint."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
from . import __version__
|
|
7
|
+
from .checker import check_pptx
|
|
8
|
+
from .reporters.console import print_report
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def collect_targets(args):
|
|
12
|
+
"""Resolve CLI args to a list of .pptx file paths."""
|
|
13
|
+
files = []
|
|
14
|
+
for arg in args:
|
|
15
|
+
if os.path.isdir(arg):
|
|
16
|
+
for f in os.listdir(arg):
|
|
17
|
+
if f.endswith('.pptx'):
|
|
18
|
+
files.append(os.path.join(arg, f))
|
|
19
|
+
elif os.path.isfile(arg) and arg.endswith('.pptx'):
|
|
20
|
+
files.append(arg)
|
|
21
|
+
else:
|
|
22
|
+
print(f"[pptx-lint] Skipping: {arg} (not a .pptx file)")
|
|
23
|
+
return files
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def main():
|
|
27
|
+
if len(sys.argv) < 2:
|
|
28
|
+
print(f"pptx-lint v{__version__}")
|
|
29
|
+
print()
|
|
30
|
+
print("Usage: pptx-lint <file.pptx> [file.pptx ...]")
|
|
31
|
+
print(" pptx-lint <directory>")
|
|
32
|
+
print()
|
|
33
|
+
print("Examples:")
|
|
34
|
+
print(" pptx-lint my_presentation.pptx")
|
|
35
|
+
print(" pptx-lint ./output/")
|
|
36
|
+
sys.exit(1)
|
|
37
|
+
|
|
38
|
+
targets = collect_targets(sys.argv[1:])
|
|
39
|
+
if not targets:
|
|
40
|
+
print("[pptx-lint] No .pptx files found.")
|
|
41
|
+
sys.exit(1)
|
|
42
|
+
|
|
43
|
+
print(f"\n{'=' * 70}")
|
|
44
|
+
print(f" pptx-lint v{__version__} â {len(targets)} file(s)")
|
|
45
|
+
print(f"{'=' * 70}")
|
|
46
|
+
|
|
47
|
+
grand_total = {'errors': 0, 'warnings': 0, 'infos': 0}
|
|
48
|
+
|
|
49
|
+
for fpath in targets:
|
|
50
|
+
results, name = check_pptx(fpath)
|
|
51
|
+
total_e = sum(len(r['errors']) for r in results)
|
|
52
|
+
total_w = sum(len(r['warnings']) for r in results)
|
|
53
|
+
total_i = sum(len(r['infos']) for r in results)
|
|
54
|
+
grand_total['errors'] += total_e
|
|
55
|
+
grand_total['warnings'] += total_w
|
|
56
|
+
grand_total['infos'] += total_i
|
|
57
|
+
|
|
58
|
+
print_report(results, name)
|
|
59
|
+
|
|
60
|
+
print(f"{'=' * 70}")
|
|
61
|
+
print(f" Grand total across all files:")
|
|
62
|
+
print(f" đ´ {grand_total['errors']} errors")
|
|
63
|
+
print(f" đĄ {grand_total['warnings']} warnings")
|
|
64
|
+
print(f" âšī¸ {grand_total['infos']} infos")
|
|
65
|
+
print(f"{'=' * 70}\n")
|
|
66
|
+
|
|
67
|
+
if grand_total['errors'] > 0:
|
|
68
|
+
sys.exit(1)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
if __name__ == '__main__':
|
|
72
|
+
main()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Reporters for pptx-lint (pluggable output formats)."""
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Console (terminal) reporter for pptx-lint."""
|
|
2
|
+
|
|
3
|
+
from ..utils import red, yellow, cyan, blue, green, bold
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def print_report(all_results, filename):
|
|
7
|
+
"""Print a colourised per-file report to stdout."""
|
|
8
|
+
total_e = sum(len(r['errors']) for r in all_results)
|
|
9
|
+
total_w = sum(len(r['warnings']) for r in all_results)
|
|
10
|
+
total_i = sum(len(r['infos']) for r in all_results)
|
|
11
|
+
total_slides = len(all_results)
|
|
12
|
+
empty = sum(1 for r in all_results if r['is_empty'])
|
|
13
|
+
|
|
14
|
+
print(f"\n{'=' * 70}")
|
|
15
|
+
print(f" {bold('pptx-lint report')}")
|
|
16
|
+
print(f" File: {blue(filename)}")
|
|
17
|
+
print(f"{'=' * 70}")
|
|
18
|
+
print(f" Slides: {total_slides} | Empty: {empty}")
|
|
19
|
+
print(f" {red(f'Errors: {total_e}')} "
|
|
20
|
+
f"{yellow(f'Warnings: {total_w}')} "
|
|
21
|
+
f"{cyan(f'Infos: {total_i}')}")
|
|
22
|
+
print(f"{'=' * 70}")
|
|
23
|
+
|
|
24
|
+
for result in all_results:
|
|
25
|
+
sn = result['slide_num']
|
|
26
|
+
if not (result['errors'] or result['warnings'] or result['infos']):
|
|
27
|
+
continue
|
|
28
|
+
|
|
29
|
+
label = f'[Slide {sn}]'
|
|
30
|
+
empty_label = ' (EMPTY)' if result['is_empty'] else ''
|
|
31
|
+
print(f"\n{green(label)}{empty_label}")
|
|
32
|
+
|
|
33
|
+
if result['text_content']:
|
|
34
|
+
preview = ' | '.join(result['text_content'][:2])
|
|
35
|
+
if len(preview) > 80:
|
|
36
|
+
preview = preview[:80] + '...'
|
|
37
|
+
print(f" {blue('Content:')} {preview}")
|
|
38
|
+
|
|
39
|
+
for e in result['errors']:
|
|
40
|
+
et = e['type']
|
|
41
|
+
print(f" {red(' â [' + et + ']')} {e['detail']}")
|
|
42
|
+
|
|
43
|
+
for w in result['warnings']:
|
|
44
|
+
wt = w['type']
|
|
45
|
+
print(f" {yellow(' Ⲡ[' + wt + ']')} {w['detail']}")
|
|
46
|
+
|
|
47
|
+
for i in result['infos']:
|
|
48
|
+
it = i['type']
|
|
49
|
+
print(f" {cyan(' i [' + it + ']')} {i['detail']}")
|
|
50
|
+
|
|
51
|
+
# Summary line
|
|
52
|
+
if total_e == 0 and total_w == 0:
|
|
53
|
+
print(f"\n {green('â No issues found!')}")
|
|
54
|
+
elif total_e == 0:
|
|
55
|
+
print(f"\n {yellow('â Warnings only, no errors.')}")
|
|
56
|
+
else:
|
|
57
|
+
print(f"\n {red('â Errors detected â review above.')}")
|
|
58
|
+
print()
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""Fake-table rule: detect multiple text boxes arranged as a grid
|
|
2
|
+
that should probably be a proper ``Table`` object instead.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
EPSILON = 0.25 # inches â alignment tolerance
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def check(shapes_info):
|
|
9
|
+
"""Return list of fake-table issues (warning severity).
|
|
10
|
+
|
|
11
|
+
Detection criteria:
|
|
12
|
+
* at least 4 text-carrying shapes
|
|
13
|
+
* shapes can be grouped into âĨ2 rows (by Y-centre)
|
|
14
|
+
* at least 2 of those rows have âĨ2 shapes each
|
|
15
|
+
"""
|
|
16
|
+
text_shapes = []
|
|
17
|
+
for info in shapes_info:
|
|
18
|
+
l, t, w, h, r, b = info['bounds']
|
|
19
|
+
if w > 0.3 and h > 0.2 and info['texts'] and l >= 0 and t >= 0:
|
|
20
|
+
text_shapes.append({
|
|
21
|
+
'idx': info['idx'],
|
|
22
|
+
'left': l, 'top': t, 'w': w, 'h': h,
|
|
23
|
+
'cx': l + w / 2, 'cy': t + h / 2,
|
|
24
|
+
'texts': info['texts'],
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
if len(text_shapes) < 4:
|
|
28
|
+
return []
|
|
29
|
+
|
|
30
|
+
# Group by row (similar Y-centre)
|
|
31
|
+
rows = []
|
|
32
|
+
used = set()
|
|
33
|
+
for ts in text_shapes:
|
|
34
|
+
if ts['idx'] in used:
|
|
35
|
+
continue
|
|
36
|
+
row = [ts]
|
|
37
|
+
used.add(ts['idx'])
|
|
38
|
+
for ts2 in text_shapes:
|
|
39
|
+
if ts2['idx'] in used:
|
|
40
|
+
continue
|
|
41
|
+
if abs(ts2['cy'] - ts['cy']) < EPSILON:
|
|
42
|
+
row.append(ts2)
|
|
43
|
+
used.add(ts2['idx'])
|
|
44
|
+
rows.append(row)
|
|
45
|
+
|
|
46
|
+
if len(rows) < 2:
|
|
47
|
+
return []
|
|
48
|
+
|
|
49
|
+
multi_col = [r for r in rows if len(r) >= 2]
|
|
50
|
+
if len(multi_col) < 2:
|
|
51
|
+
return []
|
|
52
|
+
|
|
53
|
+
total = sum(len(r) for r in multi_col)
|
|
54
|
+
samples = []
|
|
55
|
+
for r in multi_col[:2]:
|
|
56
|
+
for ts in r[:2]:
|
|
57
|
+
if ts['texts']:
|
|
58
|
+
samples.append(ts['texts'][0][:30])
|
|
59
|
+
|
|
60
|
+
return [{
|
|
61
|
+
'shape_count': total,
|
|
62
|
+
'rows': len(multi_col),
|
|
63
|
+
'sample_texts': samples[:3],
|
|
64
|
+
'severity': 'WARN',
|
|
65
|
+
}]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Font-size rule: detect text smaller than a minimum threshold."""
|
|
2
|
+
|
|
3
|
+
MIN_FONT_SIZE = 8 # points
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def check(shapes_info):
|
|
7
|
+
"""Return list of font-size issues (info severity)."""
|
|
8
|
+
issues = []
|
|
9
|
+
for info in shapes_info:
|
|
10
|
+
for fi in info.get('font_info', []):
|
|
11
|
+
if fi['size'] is not None and fi['size'] < MIN_FONT_SIZE:
|
|
12
|
+
issues.append({
|
|
13
|
+
'shape_idx': info['idx'],
|
|
14
|
+
'text': fi['text'][:40],
|
|
15
|
+
'font_size': fi['size'],
|
|
16
|
+
'severity': 'INFO',
|
|
17
|
+
})
|
|
18
|
+
return issues
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Overflow rule: detect shapes that exceed slide boundaries."""
|
|
2
|
+
|
|
3
|
+
from ..utils import emu_to_inches
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def check(slide_width, slide_height, shapes_info):
|
|
7
|
+
"""Return list of overflow issues.
|
|
8
|
+
|
|
9
|
+
Each issue dict:
|
|
10
|
+
shape_idx, shape_name, bounds, issues (list of str), texts
|
|
11
|
+
"""
|
|
12
|
+
issues = []
|
|
13
|
+
sw = slide_width # already in inches
|
|
14
|
+
sh = slide_height
|
|
15
|
+
|
|
16
|
+
TOLERANCE = 0.05 # inches, small positive margin
|
|
17
|
+
|
|
18
|
+
for info in shapes_info:
|
|
19
|
+
left, top, width, height, right, bottom = info['bounds']
|
|
20
|
+
name = (info['texts'][0] if info['texts']
|
|
21
|
+
else f"Shape {info['idx']}")
|
|
22
|
+
|
|
23
|
+
overflow_msgs = []
|
|
24
|
+
if left < 0:
|
|
25
|
+
overflow_msgs.append(f"left overflow ({left:.2f}\")")
|
|
26
|
+
if top < 0:
|
|
27
|
+
overflow_msgs.append(f"top overflow ({top:.2f}\")")
|
|
28
|
+
if right > sw + TOLERANCE:
|
|
29
|
+
overflow_msgs.append(
|
|
30
|
+
f"right overflow (r={right:.2f}\" > {sw:.2f}\")")
|
|
31
|
+
if bottom > sh + TOLERANCE:
|
|
32
|
+
overflow_msgs.append(
|
|
33
|
+
f"bottom overflow (b={bottom:.2f}\" > {sh:.2f}\")")
|
|
34
|
+
|
|
35
|
+
if overflow_msgs:
|
|
36
|
+
issues.append({
|
|
37
|
+
'shape_idx': info['idx'],
|
|
38
|
+
'shape_name': name,
|
|
39
|
+
'bounds': (left, top, width, height),
|
|
40
|
+
'issues': overflow_msgs,
|
|
41
|
+
'severity': 'ERROR',
|
|
42
|
+
'texts': info['texts'],
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
return issues
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Overlap rule: detect shapes that overlap each other."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def check(shapes_info):
|
|
5
|
+
"""Return list of overlap issues (warning severity)."""
|
|
6
|
+
issues = []
|
|
7
|
+
rects = []
|
|
8
|
+
|
|
9
|
+
for info in shapes_info:
|
|
10
|
+
l, t, w, h, r, b = info['bounds']
|
|
11
|
+
if w == 0 or h == 0:
|
|
12
|
+
continue
|
|
13
|
+
rects.append((info['idx'], l, t, r, b, info['texts'], w * h))
|
|
14
|
+
|
|
15
|
+
if not rects:
|
|
16
|
+
return issues
|
|
17
|
+
|
|
18
|
+
max_area = max(a for (_, _, _, _, _, _, a) in rects)
|
|
19
|
+
|
|
20
|
+
for i in range(len(rects)):
|
|
21
|
+
for j in range(i + 1, len(rects)):
|
|
22
|
+
idx1, l1, t1, r1, b1, t1s, a1 = rects[i]
|
|
23
|
+
idx2, l2, t2, r2, b2, t2s, a2 = rects[j]
|
|
24
|
+
|
|
25
|
+
# Skip full-slide background shapes (no text, huge area)
|
|
26
|
+
if a1 > max_area * 0.8 and not t1s:
|
|
27
|
+
continue
|
|
28
|
+
if a2 > max_area * 0.8 and not t2s:
|
|
29
|
+
continue
|
|
30
|
+
|
|
31
|
+
ox = max(0.0, min(r1, r2) - max(l1, l2))
|
|
32
|
+
oy = max(0.0, min(b1, b2) - max(t1, t2))
|
|
33
|
+
|
|
34
|
+
if ox > 0 and oy > 0:
|
|
35
|
+
oa = ox * oy
|
|
36
|
+
threshold = min(a1, a2) * 0.5
|
|
37
|
+
if oa > threshold:
|
|
38
|
+
name1 = t1s[0] if t1s else f"Shape {idx1}"
|
|
39
|
+
name2 = t2s[0] if t2s else f"Shape {idx2}"
|
|
40
|
+
issues.append({
|
|
41
|
+
'shapes': (idx1, idx2),
|
|
42
|
+
'names': (name1, name2),
|
|
43
|
+
'overlap_area': f"{oa:.2f} sq.in.",
|
|
44
|
+
'severity': 'WARN',
|
|
45
|
+
'texts': (t1s, t2s),
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
return issues
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Shared utilities for pptx-lint."""
|
|
2
|
+
|
|
3
|
+
from pptx.util import Emu, Pt
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def emu_to_inches(emu):
|
|
7
|
+
"""Convert EMU (English Metric Unit) to inches."""
|
|
8
|
+
if emu is None:
|
|
9
|
+
return 0.0
|
|
10
|
+
return emu / 914400
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def emu_to_pt(emu):
|
|
14
|
+
"""Convert EMU to points (font size)."""
|
|
15
|
+
if emu is None:
|
|
16
|
+
return None
|
|
17
|
+
return emu / 12700
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def inches(x):
|
|
21
|
+
"""Alias: convert inches to EMU via python-pptx helper.""" # noqa
|
|
22
|
+
# We keep the emu_to_inches path for readability.
|
|
23
|
+
# Actual inches-to-EMU is done by pptx.util.Inches downstream.
|
|
24
|
+
return x
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# ââ Terminal colours ââââââââââââââââââââââââââââââââââââââââââ
|
|
28
|
+
|
|
29
|
+
class Colors:
|
|
30
|
+
RED = '\033[91m'
|
|
31
|
+
YELLOW = '\033[93m'
|
|
32
|
+
BLUE = '\033[94m'
|
|
33
|
+
CYAN = '\033[96m'
|
|
34
|
+
GREEN = '\033[92m'
|
|
35
|
+
RESET = '\033[0m'
|
|
36
|
+
BOLD = '\033[1m'
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def red(s): return f"{Colors.RED}{s}{Colors.RESET}"
|
|
40
|
+
def yellow(s): return f"{Colors.YELLOW}{s}{Colors.RESET}"
|
|
41
|
+
def blue(s): return f"{Colors.BLUE}{s}{Colors.RESET}"
|
|
42
|
+
def cyan(s): return f"{Colors.CYAN}{s}{Colors.RESET}"
|
|
43
|
+
def green(s): return f"{Colors.GREEN}{s}{Colors.RESET}"
|
|
44
|
+
def bold(s): return f"{Colors.BOLD}{s}{Colors.RESET}"
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pptx-lint
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Lint / quality checker for python-pptx presentations â detects overflow, overlap, fake tables, small fonts, and empty slides
|
|
5
|
+
License: MIT
|
|
6
|
+
Keywords: pptx,powerpoint,lint,quality,python-pptx,presentation
|
|
7
|
+
Requires-Python: >=3.9
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: python-pptx>=0.6.21
|
|
11
|
+
Provides-Extra: dev
|
|
12
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
13
|
+
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
14
|
+
Dynamic: license-file
|
|
15
|
+
|
|
16
|
+
# pptx-lint
|
|
17
|
+
|
|
18
|
+
**Lint / quality checker for python-pptx presentations.**
|
|
19
|
+
|
|
20
|
+
[](https://pypi.org/project/pptx-lint/)
|
|
21
|
+
[](https://pypi.org/project/pptx-lint/)
|
|
22
|
+
[](LICENSE)
|
|
23
|
+
[](https://github.com/LeppardWang/pptx-lint/actions/workflows/test.yml)
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Why?
|
|
28
|
+
|
|
29
|
+
If you generate PowerPoint files with [python-pptx](https://python-pptx.readthedocs.io/),
|
|
30
|
+
you've probably run into these problems:
|
|
31
|
+
|
|
32
|
+
- đ´ **Overflow** â Text boxes or tables exceeding slide boundaries
|
|
33
|
+
- đĄ **Overlap** â Shapes covering each other because of hard-coded coordinates
|
|
34
|
+
- đĄ **Fake tables** â Multiple text boxes trying (and failing) to look like a table
|
|
35
|
+
- âšī¸ **Small fonts** â Text too small to read
|
|
36
|
+
- đ´ **Empty slides** â Slides without any content
|
|
37
|
+
|
|
38
|
+
**pptx-lint** automatically detects all of these, giving you a clear report
|
|
39
|
+
so you can fix them *before* presenting.
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Quick start
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pip install pptx-lint
|
|
47
|
+
|
|
48
|
+
# Check a single file
|
|
49
|
+
pptx-lint presentation.pptx
|
|
50
|
+
|
|
51
|
+
# Check all files in a directory
|
|
52
|
+
pptx-lint ./output/
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## Example output
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
======================================================================
|
|
61
|
+
pptx-lint report
|
|
62
|
+
File: my_presentation.pptx
|
|
63
|
+
======================================================================
|
|
64
|
+
Slides: 12 | Empty: 0
|
|
65
|
+
Errors: 3 | Warnings: 5 | Infos: 2
|
|
66
|
+
======================================================================
|
|
67
|
+
|
|
68
|
+
[Slide 4]
|
|
69
|
+
Content: Performance Metrics | Results
|
|
70
|
+
â [Overflow] Shape#3 (Performance table): right overflow (r=12.80" > 10.00")
|
|
71
|
+
Ⲡ[FakeTable] 6 text boxes form a grid (e.g.: Precision, Recall)
|
|
72
|
+
|
|
73
|
+
[Slide 7]
|
|
74
|
+
Content: System Architecture Overview
|
|
75
|
+
Ⲡ[Overlap] Shape#2 overlaps Shape#4 â overlap area: 2.30 sq.in.
|
|
76
|
+
|
|
77
|
+
[Slide 9]
|
|
78
|
+
Content: References
|
|
79
|
+
i [SmallFont] Shape#1: 'Acknowledgements' font = 7pt (< 8pt)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## Checks
|
|
85
|
+
|
|
86
|
+
| Rule | Severity | What it detects |
|
|
87
|
+
|------|----------|-----------------|
|
|
88
|
+
| **Overflow** | đ´ Error | Shapes whose `left`/`top` < 0 or `right`/`bottom` > slide dimensions |
|
|
89
|
+
| **Empty slide** | đ´ Error | Slides with zero shapes |
|
|
90
|
+
| **Overlap** | đĄ Warning | Two shapes overlapping > 50% of the smaller shape's area |
|
|
91
|
+
| **Fake table** | đĄ Warning | âĨ4 text boxes arranged in a âĨ2Ã2 grid (should use `add_table()`) |
|
|
92
|
+
| **Small font** | âšī¸ Info | Text smaller than 8 pt |
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## Use as a library
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
from pptx_lint.checker import check_pptx
|
|
100
|
+
|
|
101
|
+
results, filename = check_pptx("my_slides.pptx")
|
|
102
|
+
for slide in results:
|
|
103
|
+
for err in slide['errors']:
|
|
104
|
+
print(f"Slide {slide['slide_num']}: {err['detail']}")
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## Development
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
git clone https://github.com/LeppardWang/pptx-lint.git
|
|
113
|
+
cd pptx-lint
|
|
114
|
+
pip install -e ".[dev]"
|
|
115
|
+
pytest
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## Related
|
|
121
|
+
|
|
122
|
+
- [python-pptx](https://python-pptx.readthedocs.io/) â the library that creates `.pptx` files
|
|
123
|
+
- [python-pptx-table](https://pypi.org/project/python-pptx-table/) â helpers for Table objects
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## License
|
|
128
|
+
|
|
129
|
+
MIT
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/pptx_lint/__init__.py
|
|
5
|
+
src/pptx_lint/checker.py
|
|
6
|
+
src/pptx_lint/cli.py
|
|
7
|
+
src/pptx_lint/utils.py
|
|
8
|
+
src/pptx_lint.egg-info/PKG-INFO
|
|
9
|
+
src/pptx_lint.egg-info/SOURCES.txt
|
|
10
|
+
src/pptx_lint.egg-info/dependency_links.txt
|
|
11
|
+
src/pptx_lint.egg-info/entry_points.txt
|
|
12
|
+
src/pptx_lint.egg-info/requires.txt
|
|
13
|
+
src/pptx_lint.egg-info/top_level.txt
|
|
14
|
+
src/pptx_lint/reporters/__init__.py
|
|
15
|
+
src/pptx_lint/reporters/console.py
|
|
16
|
+
src/pptx_lint/rules/__init__.py
|
|
17
|
+
src/pptx_lint/rules/fake_table.py
|
|
18
|
+
src/pptx_lint/rules/font_size.py
|
|
19
|
+
src/pptx_lint/rules/overflow.py
|
|
20
|
+
src/pptx_lint/rules/overlap.py
|
|
21
|
+
tests/test_checker.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pptx_lint
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""Basic tests for pptx-lint rules."""
|
|
2
|
+
|
|
3
|
+
from pptx import Presentation
|
|
4
|
+
from pptx.util import Inches
|
|
5
|
+
|
|
6
|
+
from pptx_lint.utils import emu_to_inches
|
|
7
|
+
from pptx_lint import rules
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _make_dummy_shapes_info():
|
|
11
|
+
"""Return a shapes_info list with two non-overlapping text boxes."""
|
|
12
|
+
return [
|
|
13
|
+
{
|
|
14
|
+
'idx': 0,
|
|
15
|
+
'bounds': (0.5, 0.5, 4.0, 0.5, 4.5, 1.0),
|
|
16
|
+
'texts': ['Left box'],
|
|
17
|
+
'font_info': [{'text': 'Left box', 'size': 14}],
|
|
18
|
+
'type': 'TEXT_BOX',
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
'idx': 1,
|
|
22
|
+
'bounds': (5.0, 0.5, 4.0, 0.5, 9.0, 1.0),
|
|
23
|
+
'texts': ['Right box'],
|
|
24
|
+
'font_info': [{'text': 'Right box', 'size': 14}],
|
|
25
|
+
'type': 'TEXT_BOX',
|
|
26
|
+
},
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# ââ overflow ââââââââââââââââââââââââââââââââââââââââââââââââââ
|
|
31
|
+
|
|
32
|
+
def test_overflow_no_error():
|
|
33
|
+
infos = _make_dummy_shapes_info()
|
|
34
|
+
issues = rules.overflow.check(10.0, 7.5, infos)
|
|
35
|
+
assert len(issues) == 0
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_overflow_right_edge():
|
|
39
|
+
infos = _make_dummy_shapes_info()
|
|
40
|
+
infos[0]['bounds'] = (0.5, 0.5, 10.0, 0.5, 10.5, 1.0) # 10.5 > 10.0
|
|
41
|
+
issues = rules.overflow.check(10.0, 7.5, infos)
|
|
42
|
+
assert len(issues) == 1
|
|
43
|
+
assert 'right' in issues[0]['issues'][0]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_overflow_bottom_edge():
|
|
47
|
+
infos = _make_dummy_shapes_info()
|
|
48
|
+
infos[0]['bounds'] = (0.5, 7.6, 4.0, 0.5, 4.5, 8.1) # 8.1 > 7.5
|
|
49
|
+
issues = rules.overflow.check(10.0, 7.5, infos)
|
|
50
|
+
assert len(issues) == 1
|
|
51
|
+
assert 'bottom' in issues[0]['issues'][0]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# ââ overlap âââââââââââââââââââââââââââââââââââââââââââââââââââ
|
|
55
|
+
|
|
56
|
+
def test_overlap_none():
|
|
57
|
+
infos = _make_dummy_shapes_info()
|
|
58
|
+
issues = rules.overlap.check(infos)
|
|
59
|
+
assert len(issues) == 0
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_overlap_detected():
|
|
63
|
+
# Two boxes at the exact same position
|
|
64
|
+
infos = _make_dummy_shapes_info()
|
|
65
|
+
infos.append({
|
|
66
|
+
'idx': 2,
|
|
67
|
+
'bounds': (0.5, 0.5, 4.0, 0.5, 4.5, 1.0),
|
|
68
|
+
'texts': ['Overlapping'],
|
|
69
|
+
'font_info': [],
|
|
70
|
+
'type': 'TEXT_BOX',
|
|
71
|
+
})
|
|
72
|
+
issues = rules.overlap.check(infos)
|
|
73
|
+
assert len(issues) >= 1
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# ââ fake table ââââââââââââââââââââââââââââââââââââââââââââââââ
|
|
77
|
+
|
|
78
|
+
def test_fake_table_none_for_list():
|
|
79
|
+
"""A vertical list (same column, many rows) is NOT a fake table."""
|
|
80
|
+
infos = []
|
|
81
|
+
for i in range(6):
|
|
82
|
+
infos.append({
|
|
83
|
+
'idx': i,
|
|
84
|
+
'bounds': (0.5, 1.0 + i * 0.5, 4.0, 0.4, 4.5, 1.4 + i * 0.5),
|
|
85
|
+
'texts': [f'Item {i}'],
|
|
86
|
+
'font_info': [],
|
|
87
|
+
'type': 'TEXT_BOX',
|
|
88
|
+
})
|
|
89
|
+
issues = rules.fake_table.check(infos)
|
|
90
|
+
assert len(issues) == 0
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def test_fake_table_detected_for_grid():
|
|
94
|
+
"""A 2Ã2 grid IS a fake table."""
|
|
95
|
+
infos = []
|
|
96
|
+
for row in range(2):
|
|
97
|
+
for col in range(2):
|
|
98
|
+
infos.append({
|
|
99
|
+
'idx': row * 2 + col,
|
|
100
|
+
'bounds': (
|
|
101
|
+
0.5 + col * 4.5,
|
|
102
|
+
1.0 + row * 0.6,
|
|
103
|
+
4.0,
|
|
104
|
+
0.5,
|
|
105
|
+
4.5 + col * 4.5,
|
|
106
|
+
1.5 + row * 0.6,
|
|
107
|
+
),
|
|
108
|
+
'texts': [f'R{row}C{col}'],
|
|
109
|
+
'font_info': [],
|
|
110
|
+
'type': 'TEXT_BOX',
|
|
111
|
+
})
|
|
112
|
+
issues = rules.fake_table.check(infos)
|
|
113
|
+
assert len(issues) == 1
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
# ââ font size âââââââââââââââââââââââââââââââââââââââââââââââââ
|
|
117
|
+
|
|
118
|
+
def test_small_font_detected():
|
|
119
|
+
infos = _make_dummy_shapes_info()
|
|
120
|
+
infos[0]['font_info'] = [{'text': 'tiny', 'size': 6}] # 6pt < 8pt
|
|
121
|
+
issues = rules.font_size.check(infos)
|
|
122
|
+
assert len(issues) == 1
|
|
123
|
+
assert issues[0]['font_size'] == 6
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
# ââ end-to-end via checker ââââââââââââââââââââââââââââââââââââ
|
|
127
|
+
|
|
128
|
+
def _create_minimal_pptx(tmp_path):
|
|
129
|
+
"""Create a minimal .pptx for integration testing."""
|
|
130
|
+
from pptx import Presentation
|
|
131
|
+
prs = Presentation()
|
|
132
|
+
prs.slide_width = Inches(10)
|
|
133
|
+
prs.slide_height = Inches(7.5)
|
|
134
|
+
slide = prs.slides.add_slide(prs.slide_layouts[6])
|
|
135
|
+
from pptx.util import Pt
|
|
136
|
+
from pptx.enum.text import PP_ALIGN
|
|
137
|
+
txBox = slide.shapes.add_textbox(Inches(0.5), Inches(0.5),
|
|
138
|
+
Inches(9.0), Inches(0.5))
|
|
139
|
+
tf = txBox.text_frame
|
|
140
|
+
p = tf.paragraphs[0]
|
|
141
|
+
p.text = "Hello pptx-lint"
|
|
142
|
+
p.font.size = Pt(14)
|
|
143
|
+
path = tmp_path / "test.pptx"
|
|
144
|
+
prs.save(str(path))
|
|
145
|
+
return str(path)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def test_end_to_end(tmp_path):
|
|
149
|
+
path = _create_minimal_pptx(tmp_path)
|
|
150
|
+
from pptx_lint.checker import check_pptx
|
|
151
|
+
results, name = check_pptx(path)
|
|
152
|
+
assert len(results) == 1
|
|
153
|
+
assert 'Hello' in results[0]['text_content'][0]
|
|
154
|
+
# No errors expected for this well-formed slide
|
|
155
|
+
assert len(results[0]['errors']) == 0
|