python-pptx2 3.1.1__py3-none-any.whl → 3.2.0__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.
- pptx2/__init__.py +9 -3
- pptx2/design/blocks.py +162 -3
- pptx2/lint.py +4 -3
- pptx2/skill/SKILL.md +19 -5
- pptx2/skill/references/slide-design.md +550 -495
- {python_pptx2-3.1.1.dist-info → python_pptx2-3.2.0.dist-info}/METADATA +1 -1
- {python_pptx2-3.1.1.dist-info → python_pptx2-3.2.0.dist-info}/RECORD +11 -11
- {python_pptx2-3.1.1.dist-info → python_pptx2-3.2.0.dist-info}/WHEEL +0 -0
- {python_pptx2-3.1.1.dist-info → python_pptx2-3.2.0.dist-info}/entry_points.txt +0 -0
- {python_pptx2-3.1.1.dist-info → python_pptx2-3.2.0.dist-info}/licenses/LICENSE +0 -0
- {python_pptx2-3.1.1.dist-info → python_pptx2-3.2.0.dist-info}/top_level.txt +0 -0
pptx2/__init__.py
CHANGED
|
@@ -8,15 +8,16 @@ from typing import TYPE_CHECKING
|
|
|
8
8
|
import pptx2.exc as exceptions
|
|
9
9
|
from pptx2.api import Presentation
|
|
10
10
|
from pptx2.audit import AuditReport, audit
|
|
11
|
-
from pptx2.geometry import BBox
|
|
12
11
|
from pptx2.design.blocks import (
|
|
12
|
+
CODE_THEMES,
|
|
13
13
|
Card,
|
|
14
14
|
FittedPicture,
|
|
15
15
|
add_bullets,
|
|
16
16
|
add_card,
|
|
17
|
+
add_code,
|
|
17
18
|
add_picture_fit,
|
|
19
|
+
code_shape_name,
|
|
18
20
|
)
|
|
19
|
-
from pptx2.design.palettes import PALETTES, Palette, palette
|
|
20
21
|
from pptx2.design.components import (
|
|
21
22
|
ArticleCard,
|
|
22
23
|
Gauge,
|
|
@@ -38,6 +39,8 @@ from pptx2.design.figures import (
|
|
|
38
39
|
add_plotly_figure,
|
|
39
40
|
add_svg_figure,
|
|
40
41
|
)
|
|
42
|
+
from pptx2.design.palettes import PALETTES, Palette, palette
|
|
43
|
+
from pptx2.geometry import BBox
|
|
41
44
|
from pptx2.math import MathBackendUnavailable
|
|
42
45
|
from pptx2.opc.constants import CONTENT_TYPE as CT
|
|
43
46
|
from pptx2.opc.package import PartFactory
|
|
@@ -65,7 +68,7 @@ from pptx2.parts.slide import (
|
|
|
65
68
|
if TYPE_CHECKING:
|
|
66
69
|
from pptx2.opc.package import Part
|
|
67
70
|
|
|
68
|
-
__version__ = "3.
|
|
71
|
+
__version__ = "3.2.0"
|
|
69
72
|
|
|
70
73
|
sys.modules["pptx2.exceptions"] = exceptions
|
|
71
74
|
del sys
|
|
@@ -103,6 +106,9 @@ __all__ = [
|
|
|
103
106
|
"add_card",
|
|
104
107
|
"add_bullets",
|
|
105
108
|
"add_picture_fit",
|
|
109
|
+
"add_code",
|
|
110
|
+
"code_shape_name",
|
|
111
|
+
"CODE_THEMES",
|
|
106
112
|
"Card",
|
|
107
113
|
"FittedPicture",
|
|
108
114
|
# Curated colour sets.
|
pptx2/design/blocks.py
CHANGED
|
@@ -14,6 +14,11 @@ driven by plain hex colours and points so a script needs no token setup:
|
|
|
14
14
|
* :func:`add_picture_fit` — a picture placed *inside* a box, either
|
|
15
15
|
letter-boxed (``mode="contain"``) or cropped to fill (``mode="cover"``),
|
|
16
16
|
centred, with an optional caption underneath.
|
|
17
|
+
* :func:`add_code` — a source listing: monospace, one paragraph per line,
|
|
18
|
+
no wrapping, on a rounded editor-coloured surface. The shape is *named*
|
|
19
|
+
``fika:code:<language>:<theme>:<0|1>`` so an editor that has a native
|
|
20
|
+
code block (Fika) restores a syntax-highlighted element from it instead
|
|
21
|
+
of a plain text box; PowerPoint just sees a tidy monospace box.
|
|
17
22
|
|
|
18
23
|
Every block tags the shapes it stacks with ``lint_group`` so the linter
|
|
19
24
|
treats a card and the text on it as one deliberate cluster, and every
|
|
@@ -36,11 +41,14 @@ if TYPE_CHECKING:
|
|
|
36
41
|
from pptx2.slide import Slide
|
|
37
42
|
|
|
38
43
|
__all__ = (
|
|
44
|
+
"CODE_THEMES",
|
|
39
45
|
"Card",
|
|
40
46
|
"FittedPicture",
|
|
41
47
|
"add_card",
|
|
42
48
|
"add_bullets",
|
|
49
|
+
"add_code",
|
|
43
50
|
"add_picture_fit",
|
|
51
|
+
"code_shape_name",
|
|
44
52
|
)
|
|
45
53
|
|
|
46
54
|
|
|
@@ -99,7 +107,7 @@ def add_bullets(
|
|
|
99
107
|
align: str = "left",
|
|
100
108
|
anchor: str = "top",
|
|
101
109
|
margin_pt: float = 0.0,
|
|
102
|
-
min_size_pt: float =
|
|
110
|
+
min_size_pt: float = 18.0,
|
|
103
111
|
) -> "Shape":
|
|
104
112
|
"""Add a bulleted (or numbered) list that fits its box.
|
|
105
113
|
|
|
@@ -199,7 +207,7 @@ def add_card(
|
|
|
199
207
|
align: str = "left",
|
|
200
208
|
anchor: str = "top",
|
|
201
209
|
title_gap_pt: float = 6.0,
|
|
202
|
-
body_min_size_pt: float =
|
|
210
|
+
body_min_size_pt: float = 16.0,
|
|
203
211
|
numbered: bool = False,
|
|
204
212
|
) -> Card:
|
|
205
213
|
"""Add a card: one surface, padded title and body, nothing else.
|
|
@@ -260,7 +268,7 @@ def add_card(
|
|
|
260
268
|
run.font.size = Pt(title_size_pt)
|
|
261
269
|
run.font.bold = True
|
|
262
270
|
run.font.color.rgb = coerce_color(title_color)
|
|
263
|
-
_fit(tf, font=font, max_pt=title_size_pt, min_pt=max(
|
|
271
|
+
_fit(tf, font=font, max_pt=title_size_pt, min_pt=max(18.0, title_size_pt * 0.7), bold=True)
|
|
264
272
|
cursor_top += title_h + int(Pt(title_gap_pt))
|
|
265
273
|
remaining = int(inner.bottom) - cursor_top
|
|
266
274
|
|
|
@@ -431,3 +439,154 @@ def add_picture_fit(
|
|
|
431
439
|
|
|
432
440
|
_tag([picture, caption_box], f"picture@{int(bb.left)},{int(bb.top)}")
|
|
433
441
|
return FittedPicture(picture=picture, caption_box=caption_box, frame=frame)
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
# ----------------------------------------------------------------------------- code
|
|
445
|
+
|
|
446
|
+
CODE_SHAPE_NAME_PREFIX = "fika:code"
|
|
447
|
+
|
|
448
|
+
#: Editor themes a listing can be painted in: ``theme -> (background, foreground)``.
|
|
449
|
+
#: The ids are Shiki's, so the tag round-trips into the editor's highlighter;
|
|
450
|
+
#: PowerPoint only sees the two flat colours.
|
|
451
|
+
CODE_THEMES: dict[str, tuple[str, str]] = {
|
|
452
|
+
"github-dark": ("#24292E", "#E1E4E8"),
|
|
453
|
+
"github-light": ("#FFFFFF", "#24292E"),
|
|
454
|
+
"one-dark-pro": ("#282C34", "#ABB2BF"),
|
|
455
|
+
"one-light": ("#FAFAFA", "#383A42"),
|
|
456
|
+
"dracula": ("#282A36", "#F8F8F2"),
|
|
457
|
+
"monokai": ("#272822", "#F8F8F2"),
|
|
458
|
+
"nord": ("#2E3440", "#D8DEE9"),
|
|
459
|
+
"tokyo-night": ("#1A1B26", "#A9B1D6"),
|
|
460
|
+
"night-owl": ("#011627", "#D6DEEB"),
|
|
461
|
+
"catppuccin-mocha": ("#1E1E2E", "#CDD6F4"),
|
|
462
|
+
"catppuccin-latte": ("#EFF1F5", "#4C4F69"),
|
|
463
|
+
"vitesse-dark": ("#121212", "#DBD7CA"),
|
|
464
|
+
"vitesse-light": ("#FFFFFF", "#393A34"),
|
|
465
|
+
"min-dark": ("#1F1F1F", "#B392F0"),
|
|
466
|
+
"min-light": ("#FFFFFF", "#24292E"),
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
_CODE_LANGUAGE_ALIASES = {
|
|
470
|
+
"js": "javascript",
|
|
471
|
+
"ts": "typescript",
|
|
472
|
+
"py": "python",
|
|
473
|
+
"sh": "bash",
|
|
474
|
+
"shell": "bash",
|
|
475
|
+
"zsh": "bash",
|
|
476
|
+
"yml": "yaml",
|
|
477
|
+
"md": "markdown",
|
|
478
|
+
"c++": "cpp",
|
|
479
|
+
"c#": "csharp",
|
|
480
|
+
"cs": "csharp",
|
|
481
|
+
"text": "plaintext",
|
|
482
|
+
"txt": "plaintext",
|
|
483
|
+
"plain": "plaintext",
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
def _code_language(language: str) -> str:
|
|
488
|
+
key = (language or "plaintext").strip().lower()
|
|
489
|
+
return _CODE_LANGUAGE_ALIASES.get(key, key) or "plaintext"
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
def code_shape_name(language: str, theme: str = "github-dark", line_numbers: bool = False) -> str:
|
|
493
|
+
"""The shape name that marks a text box as a code block for the editor.
|
|
494
|
+
|
|
495
|
+
``fika:code:<language>:<theme>:<0|1>`` — the same tag Fika writes when it
|
|
496
|
+
exports one of its native code elements, so a deck authored here and a
|
|
497
|
+
deck exported from the editor import identically.
|
|
498
|
+
"""
|
|
499
|
+
lang = _code_language(language)
|
|
500
|
+
if ":" in lang or ":" in theme:
|
|
501
|
+
raise ValueError("language and theme must not contain ':'")
|
|
502
|
+
return f"{CODE_SHAPE_NAME_PREFIX}:{lang}:{theme}:{1 if line_numbers else 0}"
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
def add_code(
|
|
506
|
+
slide: "Slide",
|
|
507
|
+
*bbox_or_positional,
|
|
508
|
+
code: str,
|
|
509
|
+
language: str = "plaintext",
|
|
510
|
+
theme: str = "github-dark",
|
|
511
|
+
size_pt: float = 16.0,
|
|
512
|
+
line_numbers: bool = False,
|
|
513
|
+
font: str = "Consolas",
|
|
514
|
+
pad_pt: float = 12.0,
|
|
515
|
+
radius_pt: float = 10.0,
|
|
516
|
+
line_spacing: float = 1.2,
|
|
517
|
+
tab_width: int = 4,
|
|
518
|
+
min_size_pt: float = 12.0,
|
|
519
|
+
) -> "Shape":
|
|
520
|
+
"""Add a source-code listing that the editor recognises as a code block.
|
|
521
|
+
|
|
522
|
+
``code`` is written verbatim — one paragraph per line, leading spaces and
|
|
523
|
+
blank lines kept, tabs expanded to ``tab_width`` spaces, never wrapped —
|
|
524
|
+
in a monospace ``font`` on a rounded surface coloured after ``theme``
|
|
525
|
+
(a key of :data:`CODE_THEMES`; unknown names fall back to a dark
|
|
526
|
+
surface). ``language`` is a Shiki id or common alias (``py``, ``cs``,
|
|
527
|
+
``c#``, ``ts`` …). When the listing is taller than the box the type
|
|
528
|
+
shrinks, never below ``min_size_pt`` — beyond that, split the listing
|
|
529
|
+
across slides instead of squeezing it.
|
|
530
|
+
|
|
531
|
+
The shape is named with :func:`code_shape_name`; an editor with native
|
|
532
|
+
code blocks (Fika) turns it into a highlighted, editable code element
|
|
533
|
+
on import. PowerPoint shows a plain monospace box in the theme colours.
|
|
534
|
+
|
|
535
|
+
Returns the :class:`Shape`.
|
|
536
|
+
"""
|
|
537
|
+
from pptx2._color import coerce_color
|
|
538
|
+
from pptx2.enum.text import MSO_ANCHOR, MSO_AUTO_SIZE, PP_ALIGN
|
|
539
|
+
|
|
540
|
+
bb = _as_bbox(bbox_or_positional)
|
|
541
|
+
lines = code.replace("\r\n", "\n").replace("\r", "\n").expandtabs(tab_width).split("\n")
|
|
542
|
+
while lines and not lines[-1].strip():
|
|
543
|
+
lines.pop()
|
|
544
|
+
while lines and not lines[0].strip():
|
|
545
|
+
lines.pop(0)
|
|
546
|
+
if not lines:
|
|
547
|
+
raise ValueError("code must not be empty")
|
|
548
|
+
|
|
549
|
+
bg_hex, fg_hex = CODE_THEMES.get(theme, CODE_THEMES["github-dark"])
|
|
550
|
+
bg, fg = coerce_color(bg_hex), coerce_color(fg_hex)
|
|
551
|
+
|
|
552
|
+
box = slide.shapes.add_shape(MSO_SHAPE.ROUNDED_RECTANGLE, *bb)
|
|
553
|
+
box.name = code_shape_name(language, theme, line_numbers)
|
|
554
|
+
box.fill.solid()
|
|
555
|
+
box.fill.fore_color.rgb = bg
|
|
556
|
+
box.line.fill.background()
|
|
557
|
+
box.shadow.clear()
|
|
558
|
+
short_edge = min(int(bb.width), int(bb.height))
|
|
559
|
+
box.corner_radius = Emu(min(int(Pt(radius_pt)), short_edge // 2))
|
|
560
|
+
|
|
561
|
+
tf = box.text_frame
|
|
562
|
+
tf.word_wrap = False
|
|
563
|
+
tf.vertical_anchor = MSO_ANCHOR.TOP
|
|
564
|
+
pad = Pt(pad_pt)
|
|
565
|
+
tf.margin_left = tf.margin_right = tf.margin_top = tf.margin_bottom = pad
|
|
566
|
+
|
|
567
|
+
width = len(str(len(lines)))
|
|
568
|
+
for i, line in enumerate(lines):
|
|
569
|
+
para = tf.paragraphs[0] if i == 0 else tf.add_paragraph()
|
|
570
|
+
para.alignment = PP_ALIGN.LEFT
|
|
571
|
+
para.line_spacing = float(line_spacing)
|
|
572
|
+
text = f"{i + 1:>{width}} {line}" if line_numbers else line
|
|
573
|
+
run = para.add_run()
|
|
574
|
+
run.text = text if text else " "
|
|
575
|
+
run.font.name = font
|
|
576
|
+
run.font.size = Pt(size_pt)
|
|
577
|
+
run.font.color.rgb = fg
|
|
578
|
+
|
|
579
|
+
# Fit by height only: a listing must never wrap, so shrink the type until
|
|
580
|
+
# every line has room, then let PowerPoint clip the rest rather than reflow.
|
|
581
|
+
avail_h = int(bb.height) - 2 * int(pad)
|
|
582
|
+
per_line = size_pt * 1.2 * line_spacing
|
|
583
|
+
needed_h = int(Pt(per_line * len(lines)))
|
|
584
|
+
if needed_h > avail_h > 0:
|
|
585
|
+
fitted = max(min_size_pt, size_pt * avail_h / needed_h)
|
|
586
|
+
for para in tf.paragraphs:
|
|
587
|
+
for run in para.runs:
|
|
588
|
+
run.font.size = Pt(fitted)
|
|
589
|
+
tf.auto_size = MSO_AUTO_SIZE.NONE
|
|
590
|
+
|
|
591
|
+
_tag([box], f"code@{int(bb.left)},{int(bb.top)}")
|
|
592
|
+
return box
|
pptx2/lint.py
CHANGED
|
@@ -293,7 +293,7 @@ class MinFontSize(LintIssue):
|
|
|
293
293
|
"""A text run uses a font size below the configured legibility threshold."""
|
|
294
294
|
|
|
295
295
|
pt: float = 0.0
|
|
296
|
-
threshold_pt: float =
|
|
296
|
+
threshold_pt: float = 12.0
|
|
297
297
|
|
|
298
298
|
def __init__(self, shape: BaseShape, pt: float, threshold_pt: float):
|
|
299
299
|
super().__init__(
|
|
@@ -1819,10 +1819,11 @@ def _check_collisions(
|
|
|
1819
1819
|
|
|
1820
1820
|
|
|
1821
1821
|
# ---------------------------------------------------------------------------
|
|
1822
|
-
# Min font size — flag any run below the legibility threshold (default
|
|
1822
|
+
# Min font size — flag any run below the legibility threshold (default 12pt:
|
|
1823
|
+
# projected slides are read from the back of a room, not from a desk).
|
|
1823
1824
|
# ---------------------------------------------------------------------------
|
|
1824
1825
|
|
|
1825
|
-
_DEFAULT_MIN_FONT_PT =
|
|
1826
|
+
_DEFAULT_MIN_FONT_PT = 12.0
|
|
1826
1827
|
_PT_TO_EMU = 12700
|
|
1827
1828
|
|
|
1828
1829
|
|
pptx2/skill/SKILL.md
CHANGED
|
@@ -38,14 +38,22 @@ helpers that implement it:
|
|
|
38
38
|
- one **palette** (`PALETTES["slate"]` → `P.paper / P.ink / P.accent /
|
|
39
39
|
P.surface / P.muted`; `P.dark()` for title and section slides),
|
|
40
40
|
- one **grid** (`TITLE` band + `BODY` region; `BODY.columns(n, gap=…)`),
|
|
41
|
-
- a **type scale** (title 32, body
|
|
41
|
+
- a **type scale** read from the back row (title 32–36, body 22–26, card
|
|
42
|
+
body 18–20, caption 14 — nothing under 14 pt),
|
|
42
43
|
- **surfaces done well** — `add_card` is one tinted rounded rectangle with
|
|
43
44
|
padded text inside, nothing attached to it,
|
|
44
45
|
- real **bullets** with hanging indents (`add_bullets`), pictures that
|
|
45
|
-
keep their aspect (`add_picture_fit`),
|
|
46
|
+
keep their aspect (`add_picture_fit`), **code listings** the editor
|
|
47
|
+
recognises as code blocks (`add_code`), palette-driven diagrams and
|
|
46
48
|
tables,
|
|
47
|
-
- a catalog of **slide archetypes** (title, section, statement,
|
|
48
|
-
picture, three cards, process, comparison, quote,
|
|
49
|
+
- a catalog of **slide archetypes** (title, section, statement, big
|
|
50
|
+
number, bullets + picture, three cards, process, comparison, quote,
|
|
51
|
+
full-bleed photo, question, summary) — planned as a sequence before
|
|
52
|
+
slide 2, consecutive slides never sharing one, dense alternating with
|
|
53
|
+
light,
|
|
54
|
+
- a **visual per slide** chosen for its content: a photo for a real
|
|
55
|
+
thing, a chart for numbers, a diagram for a process, a table for a
|
|
56
|
+
comparison, typeset math for formulas,
|
|
49
57
|
- and a **visual check**: `prs.render_contact_sheet("preview.png")` — one
|
|
50
58
|
PNG of every slide, looked at once before shipping.
|
|
51
59
|
|
|
@@ -61,7 +69,7 @@ working set:
|
|
|
61
69
|
|
|
62
70
|
```python
|
|
63
71
|
from pptx2 import Presentation, BBox, audit, PALETTES
|
|
64
|
-
from pptx2 import add_card, add_bullets, add_picture_fit
|
|
72
|
+
from pptx2 import add_card, add_bullets, add_picture_fit, add_code
|
|
65
73
|
from pptx2.diagrams import horizontal_pipeline, hub_and_spoke, cycle
|
|
66
74
|
from pptx2.enum.shapes import MSO_SHAPE
|
|
67
75
|
from pptx2.util import Inches, Pt
|
|
@@ -104,6 +112,12 @@ add_bullets(slide, left, items=["First point", "Second point"],
|
|
|
104
112
|
add_picture_fit(slide, "photo.jpg", right, mode="contain", # or "cover" to fill + crop
|
|
105
113
|
caption="Source: Wikimedia", caption_color=P.muted)
|
|
106
114
|
|
|
115
|
+
# --- source code: monospace listing, never a plain text box ---
|
|
116
|
+
add_code(slide, right, code='for i in range(3):\n print(i)', language="python",
|
|
117
|
+
size_pt=18) # theme="github-dark" | "github-light" | …; line_numbers=True
|
|
118
|
+
# the shape is named fika:code:<lang>:<theme>:<0|1> — editors with a native
|
|
119
|
+
# code block (Fika) turn it into a highlighted, editable element on import
|
|
120
|
+
|
|
107
121
|
# --- native equation from LaTeX (pip install "python-pptx2[math]") ---
|
|
108
122
|
slide.shapes.add_equation(bb, latex=r"\frac{a}{b}", size_pt=28)
|
|
109
123
|
para = slide.shapes.add_text(bb, text="Euler: ").text_frame.paragraphs[0]
|