Metadata-Version: 2.4
Name: pptx-extended
Version: 1.0.0
Summary: Extended utilities for python-pptx: 16.7M highlight colors, independent cell borders, shadows, glow effects, and more
Author: Sahil Bhatt
Maintainer: Sahil Bhatt
License: MIT
Project-URL: Homepage, https://github.com/bms-pub-rbit/pptx-extended
Project-URL: Documentation, https://github.com/bms-pub-rbit/pptx-extended#readme
Project-URL: Repository, https://github.com/bms-pub-rbit/pptx-extended.git
Project-URL: Issues, https://github.com/bms-pub-rbit/pptx-extended/issues
Project-URL: Changelog, https://github.com/bms-pub-rbit/pptx-extended/blob/main/CHANGELOG.md
Keywords: powerpoint,pptx,python-pptx,presentation,highlight,table,border,shadow,glow,effects,xml,office,openxml,ooxml
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Office/Business :: Office Suites
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: python-pptx>=0.6.21
Requires-Dist: lxml>=4.9.0
Provides-Extra: dev
Requires-Dist: pytest>=7.0.0; extra == "dev"
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
Requires-Dist: black>=23.0.0; extra == "dev"
Requires-Dist: isort>=5.12.0; extra == "dev"
Requires-Dist: mypy>=1.0.0; extra == "dev"
Dynamic: license-file

# pptx-extended

**Extended utilities for python-pptx: custom highlights, consistent borders, shape effects, and more.**

[![PyPI version](https://badge.fury.io/py/pptx-extended.svg)](https://badge.fury.io/py/pptx-extended)
[![Python 3.8+](https://img.shields.io/badge/python-3.8+-blue.svg)](https://www.python.org/downloads/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/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:

```python
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

```bash
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**:

```python
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:

```python
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](#4-gappedtable---independent-cell-borders-experimental) instead.**

### 3. Shape Effects (NEW!)

Add professional effects to any shape - shadows, glow, reflection, and soft edges:

#### Drop Shadows

```python
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)
```

```python
# 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

```python
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

```python
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

```python
from pptx_extended import add_soft_edge

# Add feathered/blurred edges
add_soft_edge(shape, radius_pt=5)
```

#### Removing Effects

```python
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

```python
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

```python
# ─────────────────────────────────────────────────────────────
# 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:

```python
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:

```python
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](docs/how_it_works.md)**

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](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](https://python-pptx.readthedocs.io/) - The foundation this library extends
- [ECMA-376](https://www.ecma-international.org/publications-and-standards/standards/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!**
