Extended utilities for python-pptx: 16.7M highlight colors, independent cell borders, shadows, glow effects, and more
Project description
pptx-extended
Extended utilities for python-pptx: custom highlights, consistent borders, shape effects, and more.
The Problem
python-pptx is a great library, but it has limitations that have frustrated developers for over 10 years:
- Highlights: Only 15 preset colors available (no custom RGB)
- Table Borders: Adjacent cells share edges, causing border conflicts when different styles are needed
- No Shape Effects: No shadows, glow, reflection, or soft edges
- No Gradients: Creating color progressions requires manual calculation
The Solution
pptx-extended uses XML insertion to unlock PowerPoint's full capabilities:
from pptx_extended import (
add_highlight,
set_cell_border,
create_gapped_table, # EXPERIMENTAL: for independent borders
generate_gradient,
add_shadow,
add_glow,
add_reflection
)
# Use ANY of 16.7 million RGB colors as highlight!
add_highlight(run, '#FF5733') # Not limited to 15 presets!
# Set borders on individual cells (basic)
set_cell_border(cell, '#0000FF', border_width_pt=2.0)
# For INDEPENDENT borders on adjacent cells, use GappedTable (EXPERIMENTAL)
gapped = create_gapped_table(slide, rows=3, cols=3, ...)
gapped.set_cell_border(0, 0, '#FF0000') # Red - won't conflict with neighbor!
gapped.set_cell_border(0, 1, '#0000FF') # Blue - independent border!
gapped.donate_borders()
# Generate gradient colors for visual effects
colors = generate_gradient('#FF0000', '#0000FF', 5)
# Add professional shape effects
add_shadow(shape, distance_pt=5) # Auto-calculated blur!
add_glow(shape, color='#FFD700', radius_pt=10)
add_reflection(shape, start_opacity=50)
Installation
pip install pptx-extended
Features
1. Custom Highlight Colors
python-pptx limits you to 15 preset highlight colors. With pptx-extended, use any RGB color:
from pptx import Presentation
from pptx.util import Inches
from pptx_extended import add_highlight
prs = Presentation()
slide = prs.slides.add_slide(prs.slide_layouts[5])
# Add a text box
textbox = slide.shapes.add_textbox(Inches(1), Inches(1), Inches(6), Inches(1))
p = textbox.text_frame.paragraphs[0]
run = p.add_run()
run.text = "This text has a custom highlight color!"
# Apply ANY color as highlight
add_highlight(run, '#FF5733') # Orange
add_highlight(run, '#00FF00') # Green
add_highlight(run, '#BE2BBB') # Purple
prs.save('custom_highlights.pptx')
2. Table Cell Borders (Basic)
Set borders on individual table cells with consistent styling on all 4 sides:
from pptx import Presentation
from pptx.util import Inches
from pptx_extended import set_cell_border, clear_table_style
prs = Presentation()
slide = prs.slides.add_slide(prs.slide_layouts[5])
# Create a table
table_shape = slide.shapes.add_table(3, 3, Inches(1), Inches(1), Inches(6), Inches(3))
table = table_shape.table
# Clear default styling (important!)
clear_table_style(table)
# Apply borders - works great for uniform styling
set_cell_border(table.cell(0, 0), '#0000FF', border_width_pt=2.0)
set_cell_border(table.cell(1, 1), '#FF0000', border_width_pt=1.5, dash_style='dashed')
set_cell_border(table.cell(2, 2), '#00FF00', border_width_pt=1.0, dash_style='dotted')
prs.save('borders.pptx')
Supported Border Styles:
solid- Continuous line (default)dashed- Dashed linedotted- Dotted linedash-dot- Alternating dash-dot pattern
IMPORTANT LIMITATION: This function works well when cells don't share edges with different colors. If you need different border colors on adjacent cells (e.g., cell A has a red right border, cell B has a blue left border), the borders will conflict because PowerPoint tables share edges.
For independent cell borders, use GappedTable instead.
3. Shape Effects (NEW!)
Add professional effects to any shape - shadows, glow, reflection, and soft edges:
Drop Shadows
from pptx import Presentation
from pptx.util import Inches
from pptx_extended import add_shadow
prs = Presentation()
slide = prs.slides.add_slide(prs.slide_layouts[5])
shape = slide.shapes.add_shape(1, Inches(2), Inches(2), Inches(3), Inches(1.5))
# Basic shadow (auto-calculates blur based on distance)
add_shadow(shape, distance_pt=5)
# Customized shadow
add_shadow(
shape,
color='#000000', # Shadow color
transparency=50, # 0-100 (50 = half transparent)
blur_radius_pt=8, # Manual blur (omit for auto-diffuse)
distance_pt=6, # Distance from shape
angle=135, # Direction in degrees (0-360)
shadow_type='outer' # 'outer' or 'inner'
)
prs.save('shadows.pptx')
Auto-Diffuse Feature: When you omit blur_radius_pt, the library automatically calculates a realistic blur based on distance:
- Formula:
blur = 2.0 + (distance × 0.7) - Closer shadows are sharper, distant shadows are softer (like real physics!)
Shadow Angles
Control shadow direction with 0-360 degree angles:
0° (top)
↑
315° ← ● → 45°
↓
180° (bottom)
# Shadows at different angles
add_shadow(shape, angle=0) # Shadow above
add_shadow(shape, angle=45) # Shadow to bottom-right
add_shadow(shape, angle=90) # Shadow to the right
add_shadow(shape, angle=180) # Shadow below
add_shadow(shape, angle=270) # Shadow to the left
Glow Effects
from pptx_extended import add_glow
# Add golden glow
add_glow(
shape,
color='#FFD700', # Glow color
radius_pt=10, # Glow size
transparency=30 # 0-100
)
Reflection Effects
from pptx_extended import add_reflection
# Add mirror reflection
add_reflection(
shape,
blur_radius_pt=1, # Reflection blur
start_opacity=50, # Opacity at shape edge (0-100)
end_opacity=0, # Opacity at reflection end
distance_pt=0, # Gap between shape and reflection
direction=90, # Reflection direction in degrees
fade_direction=90, # Fade direction
scale_x=100, # Horizontal scale %
scale_y=-100 # Vertical scale % (negative = flip)
)
Soft Edges
from pptx_extended import add_soft_edge
# Add feathered/blurred edges
add_soft_edge(shape, radius_pt=5)
Removing Effects
from pptx_extended import (
remove_shadow,
remove_glow,
remove_reflection,
remove_soft_edge,
remove_all_effects
)
# Remove individual effects
remove_shadow(shape)
remove_glow(shape)
remove_reflection(shape)
remove_soft_edge(shape)
# Or remove everything at once
remove_all_effects(shape)
4. GappedTable - Independent Cell Borders (EXPERIMENTAL)
WARNING: EXPERIMENTAL FEATURE
This feature is experimental and may not work perfectly in all scenarios. It uses a workaround to achieve independent cell borders, which PowerPoint doesn't natively support. Use with caution in production environments and always test your output files thoroughly.
The Problem: Border Conflicts in PowerPoint Tables
If you've ever tried to create a table where adjacent cells have different border colors, you've likely encountered this frustration: the borders conflict.
What you want: What you get:
┌─────────┬─────────┐ ┌─────────┬─────────┐
│ RED │ BLUE │ │ RED │ BLUE │
│ border │ border │ │ border │ border │
└─────────┴─────────┘ └─────────┴─────────┘
↑ ↑ ↑
Both colors visible One color "wins" - the other is lost!
Why does this happen?
PowerPoint tables share physical edges between adjacent cells. When you set cell (0,0)'s right border to RED and cell (0,1)'s left border to BLUE, they're fighting over the same edge. PowerPoint's renderer has to pick one, and the results are inconsistent.
Our Investigation
We thoroughly explored the OOXML (Office Open XML) specification to find a proper solution:
| What We Checked | Result |
|---|---|
tblPr (Table Properties) |
No independentBorders attribute exists |
tcPr (Cell Properties) |
No borderOwnership or borderPriority attribute |
Table Styles (tcBdr) |
Only supports uniform internal borders |
| WordprocessingML | Has cellSpacing but PowerPoint uses DrawingML |
| Extension mechanisms | Can't add custom border behavior |
Conclusion: Border inheritance is hardcoded in PowerPoint's rendering engine. There's no XML property to make cells have independent borders.
The Solution: Spacer Cells
Since we can't change PowerPoint's behavior, we work with it by inserting thin, invisible "spacer" cells between content cells:
Standard Table (3x3): GappedTable (3x3 logical, 5x5 actual):
┌───────┬───────┬───────┐ ┌───────┬─┬───────┬─┬───────┐
│ (0,0) │ (0,1) │ (0,2) │ │ (0,0) │S│ (0,1) │S│ (0,2) │
├───────┼───────┼───────┤ ├───────┼─┼───────┼─┼───────┤
│ (1,0) │ (1,1) │ (1,2) │ → │ S │S│ S │S│ S │ ← spacer row
├───────┼───────┼───────┤ ├───────┼─┼───────┼─┼───────┤
│ (2,0) │ (2,1) │ (2,2) │ │ (1,0) │S│ (1,1) │S│ (1,2) │
└───────┴───────┴───────┘ ├───────┼─┼───────┼─┼───────┤
│ S │S│ S │S│ S │ ← spacer row
Cells share edges ├───────┼─┼───────┼─┼───────┤
(borders conflict) │ (2,0) │S│ (2,1) │S│ (2,2) │
└───────┴─┴───────┴─┴───────┘
↑
Spacer columns (S)
Each content cell now has its OWN edges!
The key insight: Spacer cells "donate" their borders to neighboring content cells. PowerPoint's border ownership model means:
- Cells own their RIGHT and BOTTOM borders
- Cells inherit their LEFT (from left neighbor) and TOP (from top neighbor)
So spacers donate their RIGHT border to the content cell on the right, and their BOTTOM border to the content cell below.
Experimental Warnings
Please be aware of these limitations:
-
Spacer cells are visible in edit mode - When you click on the table in PowerPoint, you'll see the spacer cells. They're nearly invisible (0.1pt wide) but they exist.
-
Table size increases - A 3x3 logical table becomes a 5x5 actual table. This may affect performance with very large tables.
-
Cell merging is complex - Merging cells across spacers requires careful handling of the underlying table structure.
-
Copy/paste behavior - When users copy cells, they might accidentally include spacer cells.
-
Not tested on all PowerPoint versions - We've tested on recent versions, but older versions may behave differently.
Quick Start
from pptx import Presentation
from pptx.util import Inches
from pptx_extended import create_gapped_table
prs = Presentation()
slide = prs.slides.add_slide(prs.slide_layouts[6])
# Create a 3x3 table with independent borders
gapped = create_gapped_table(
slide, rows=3, cols=3,
left=Inches(1), top=Inches(1),
width=Inches(6), height=Inches(3)
)
# Access cells by logical index (spacers are hidden!)
gapped[0, 0].text = "Red border"
gapped[0, 1].text = "Blue border"
# Set different borders on adjacent cells - NO CONFLICT!
gapped.set_cell_border(0, 0, '#FF0000', 3.0) # Red
gapped.set_cell_border(0, 1, '#0000FF', 3.0) # Blue
# IMPORTANT: Call donate_borders() after setting individual borders
gapped.donate_borders()
# Or use set_content_borders() for uniform borders (auto-donates)
# gapped.set_content_borders('#000000', 2.0)
prs.save('gapped_table.pptx')
GappedTable API Reference
# ─────────────────────────────────────────────────────────────
# Creation
# ─────────────────────────────────────────────────────────────
gapped = create_gapped_table(
slide, # pptx Slide object
rows=3, cols=3, # Logical dimensions (content cells)
left=Inches(1), # Position
top=Inches(1),
width=Inches(6), # Total size
height=Inches(3),
spacer_width_pt=0.1, # Spacer thickness (default: 0.1pt)
gap_rows=True, # Insert spacer rows (default: True)
gap_cols=True # Insert spacer columns (default: True)
)
# ─────────────────────────────────────────────────────────────
# Cell Access (uses LOGICAL indices - spacers are hidden)
# ─────────────────────────────────────────────────────────────
gapped[0, 0] # Indexing syntax
gapped.cell(0, 0) # Method syntax
gapped.actual_cell(r, c) # Raw access (includes spacers)
# ─────────────────────────────────────────────────────────────
# Row/Column Access
# ─────────────────────────────────────────────────────────────
for cell in gapped.row(0): # Iterate over row
cell.text = "Row 0"
for cell in gapped.column(0): # Iterate over column
cell.text = "Col 0"
# ─────────────────────────────────────────────────────────────
# Iteration
# ─────────────────────────────────────────────────────────────
for row, col, cell in gapped.iter_content_cells(): # Content only
pass
for row, col, cell in gapped.iter_spacer_cells(): # Spacers only
pass
# ─────────────────────────────────────────────────────────────
# Border Methods (THE KEY FEATURE!)
# ─────────────────────────────────────────────────────────────
# Option 1: Set individual cell borders
gapped.set_cell_border(row, col, '#FF0000', 3.0)
gapped.set_cell_border(row, col, '#0000FF', 3.0, 'dashed')
gapped.donate_borders() # REQUIRED after set_cell_border()!
# Option 2: Set uniform borders on all cells (auto-donates)
gapped.set_content_borders('#000000', 2.0)
gapped.set_content_borders('#FF0000', 1.5, 'dotted')
# ─────────────────────────────────────────────────────────────
# Bulk Operations
# ─────────────────────────────────────────────────────────────
gapped.apply_to_content(func) # Apply function to all content cells
gapped.apply_to_spacers(func) # Apply function to all spacer cells
gapped.apply_to_all(func) # Apply to ALL cells
# ─────────────────────────────────────────────────────────────
# Properties
# ─────────────────────────────────────────────────────────────
gapped.shape # (rows, cols) tuple of content dimensions
gapped.content_rows # Number of logical content rows
gapped.content_cols # Number of logical content columns
gapped.actual_rows # Total rows including spacers
gapped.actual_cols # Total columns including spacers
gapped.table # Underlying pptx Table object
# ─────────────────────────────────────────────────────────────
# Index Conversion
# ─────────────────────────────────────────────────────────────
actual_r, actual_c = gapped.logical_to_actual(row, col)
is_spacer = gapped.is_spacer(actual_row, actual_col)
When to Use GappedTable
| Scenario | Recommendation |
|---|---|
| Different border colors on adjacent cells | Use GappedTable |
| All cells have the same border style | Use standard set_cell_border() |
| Borders only on outer edges | Use standard set_cell_border() |
| Performance-critical large tables | Use standard tables |
| Tables that users will edit heavily | Consider standard tables |
Demo Script
See examples/gapped_table_demo.py for a comprehensive demonstration of all GappedTable features.
5. Color Gradients
Create smooth color transitions for headers, progress bars, or visual effects:
from pptx_extended import generate_gradient, interpolate_color, set_cell_border
# Generate 5 colors from red to blue
colors = generate_gradient('#FF0000', '#0000FF', 5)
# Returns: ['#FF0000', '#BF003F', '#7F007F', '#3F00BF', '#0000FF']
# Or interpolate a single color
mid_color = interpolate_color('#FF0000', '#0000FF', 0.5)
# Returns: '#7F007F' (purple - midpoint between red and blue)
# Apply gradient to table header
for i, cell in enumerate(table.rows[0].cells):
set_cell_border(cell, colors[i], border_width_pt=2.0)
6. Dimension Utilities
Calculate text heights for dynamic layouts:
from pptx_extended import calculate_text_height, pt_to_emu, emu_to_inches
# Calculate height needed for wrapped text
height = calculate_text_height(
"This is a long text that will wrap to multiple lines",
available_width_pt=200,
font_size_pt=12
)
# Convert between units
emu_value = pt_to_emu(12) # Points to EMUs
inches = emu_to_inches(914400) # EMUs to inches
API Reference
Highlights
| Function | Description |
|---|---|
add_highlight(run, hex_color) |
Add custom RGB highlight to text run |
remove_highlight(run) |
Remove highlight from text run |
Borders
| Function | Description |
|---|---|
set_cell_border(cell, color, width, style, sides) |
Set consistent borders |
remove_cell_borders(cell) |
Remove all borders from cell |
clear_table_style(table) |
Clear default table styling |
GappedTable (EXPERIMENTAL)
| Function/Class | Description |
|---|---|
create_gapped_table(slide, rows, cols, ...) |
Create table with independent borders |
GappedTable |
Wrapper class managing spacer cells |
GappedRow |
Helper for row iteration |
GappedColumn |
Helper for column iteration |
gapped[r, c] |
Access content cell by logical index |
gapped.set_cell_border(r, c, color, width) |
Set border on specific cell |
gapped.donate_borders() |
Configure spacers (REQUIRED after set_cell_border) |
gapped.set_content_borders(color, width) |
Set uniform borders (auto-donates) |
gapped.iter_content_cells() |
Iterate over content cells |
gapped.apply_to_content(func) |
Apply function to all content |
Shape Effects
| Function | Description |
|---|---|
add_shadow(shape, ...) |
Add drop shadow (outer or inner) |
add_glow(shape, color, radius_pt, ...) |
Add glow effect |
add_reflection(shape, ...) |
Add mirror reflection |
add_soft_edge(shape, radius_pt) |
Add feathered edges |
remove_shadow(shape) |
Remove shadow effect |
remove_glow(shape) |
Remove glow effect |
remove_reflection(shape) |
Remove reflection effect |
remove_soft_edge(shape) |
Remove soft edge effect |
remove_all_effects(shape) |
Remove all effects from shape |
Colors
| Function | Description |
|---|---|
hex_to_rgb(hex_color) |
Convert hex to RGB tuple |
rgb_to_hex(r, g, b) |
Convert RGB to hex string |
hex_to_RGBColor(hex_color) |
Convert to python-pptx RGBColor |
interpolate_color(start, end, t) |
Blend between two colors |
generate_gradient(start, end, steps) |
Create gradient color list |
Dimensions
| Function | Description |
|---|---|
pt_to_emu(pt) |
Points to EMUs |
emu_to_pt(emu) |
EMUs to points |
inches_to_emu(inches) |
Inches to EMUs |
emu_to_inches(emu) |
EMUs to inches |
calculate_text_height(text, width, ...) |
Calculate wrapped text height |
calculate_wrapped_lines(text, width, ...) |
Estimate line count |
How It Works
PowerPoint files (.pptx) are ZIP archives containing XML. We inject DrawingML elements that python-pptx doesn't expose.
Want to understand the details? Read our friendly guide: How pptx-extended Works
It covers:
- Why PowerPoint files are just ZIP + XML
- How we bypass python-pptx's 15-color highlight limit
- The shared border problem and our spacer cell solution
- How shape effects work under the hood
Requirements
- Python 3.8+
- python-pptx >= 0.6.21
- lxml >= 4.9.0
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
License
MIT License - see LICENSE file for details.
Author
Sahil Bhatt - Prediction & Insights Team, Bristol-Myers Squibb
Developed while building an internal application. The XML insertion techniques were discovered through research into the OOXML/DrawingML specification when existing libraries couldn't meet project requirements.
Special Thanks
- David Liu — for supporting the creative freedom and the constant encouragement to think outside the box. This project wouldn't exist without him.
- Albert Wang — for providing the management backing and executive support that made the public release possible.
- Ravi Patel — for stepping up and supporting the legal approval process through to the finish line.
- Kshitij Palria — for setting up BMS's public-facing GitHub organization and guiding us through the infrastructure setup.
- The BMS Legal team — for their thorough review and guidance in making this publication possible.
Acknowledgments
- python-pptx - The foundation this library extends
- ECMA-376 - Office Open XML specification
- The developers who opened GitHub issues requesting these features - your frustration inspired this solution!
Public Disclosure
This repository has been reviewed and approved by the BMS Legal and IT teams prior to public release (Public Disclosure Reference: PDISC-2625).
If this library helps you, consider giving it a star!
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pptx_extended-1.0.0.tar.gz.
File metadata
- Download URL: pptx_extended-1.0.0.tar.gz
- Upload date:
- Size: 38.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.10.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
05487f18b8227d4530e0b953c55591eb3649c5e15dc9fa5d668e21e6bc5ed077
|
|
| MD5 |
0aa4c23428939cace317bec845dd74d6
|
|
| BLAKE2b-256 |
3690b70c5bfef78129654a8d546ec567e75dab74b4e5ae2ba7449d6035a23122
|
File details
Details for the file pptx_extended-1.0.0-py3-none-any.whl.
File metadata
- Download URL: pptx_extended-1.0.0-py3-none-any.whl
- Upload date:
- Size: 29.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.10.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
453277892db563fd9f4dd1f88b42f374468334a3f9271eab21ac484db6c68cd2
|
|
| MD5 |
a70005a6c1dd08f9b6367f8d514a9519
|
|
| BLAKE2b-256 |
f8ccd0f863a6b1b366e5ffc25017b435490b845b9f46d9dbeee02312202febf6
|