Skip to main content

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.

PyPI version Python 3.8+ License: MIT

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 line
  • dotted - Dotted line
  • dash-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:

  1. 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.

  2. Table size increases - A 3x3 logical table becomes a 5x5 actual table. This may affect performance with very large tables.

  3. Cell merging is complex - Merging cells across spacers requires careful handling of the underlying table structure.

  4. Copy/paste behavior - When users copy cells, they might accidentally include spacer cells.

  5. 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pptx_extended-1.0.0.tar.gz (38.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pptx_extended-1.0.0-py3-none-any.whl (29.9 kB view details)

Uploaded Python 3

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

Hashes for pptx_extended-1.0.0.tar.gz
Algorithm Hash digest
SHA256 05487f18b8227d4530e0b953c55591eb3649c5e15dc9fa5d668e21e6bc5ed077
MD5 0aa4c23428939cace317bec845dd74d6
BLAKE2b-256 3690b70c5bfef78129654a8d546ec567e75dab74b4e5ae2ba7449d6035a23122

See more details on using hashes here.

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

Hashes for pptx_extended-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 453277892db563fd9f4dd1f88b42f374468334a3f9271eab21ac484db6c68cd2
MD5 a70005a6c1dd08f9b6367f8d514a9519
BLAKE2b-256 f8ccd0f863a6b1b366e5ffc25017b435490b845b9f46d9dbeee02312202febf6

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page