Metadata-Version: 2.4
Name: pptxizza
Version: 0.1.8
Summary: A streamlined, LLM-friendly pip package for creating and editing pptx files from templates.
Author: Kameron
Classifier: Programming Language :: Python :: 3
Classifier: Operating System :: OS Independent
Requires-Python: >=3.8
Description-Content-Type: text/markdown
Requires-Dist: lxml>=4.9.0
Requires-Dist: openpyxl>=3.1.0
Provides-Extra: dev
Requires-Dist: pytest>=7.0.0; extra == "dev"

# pptxizza

A streamlined, LLM-friendly pip package for creating and editing `.pptx` files dynamically. `pptxizza` uses direct XML manipulation under the hood (via `lxml`) to fill text, replace images, and update charts and tables within existing PowerPoint templates quickly and efficiently, featuring a highly-granular Object-Oriented API for direct shape manipulation!

## Why pptxizza? (vs `python-pptx`)

This project is heavily inspired by [`python-pptx`](https://github.com/scanny/python-pptx), which is a fantastic and comprehensive library for working with PowerPoint files. However, `pptxizza` was created primarily for one very important reason: **to add native SVG support haha!**

Native PowerPoint doesn't typically embed pure SVGs comfortably via older XML specifications, and `python-pptx` lacks native support for them. `pptxizza` solves this by utilizing modern Microsoft Open XML extension nodes to automatically parse, embed, and inject SVGs cleanly into your slides.

## Features

- **Object-Oriented Integrity**: Native interaction with shapes (`Shape`, `Rect`, `Circle`, `TextBox`) and charts (`BarChart`, `PieChart`, etc.).
- **Programmatic Shape Insertion**: Generate and structure exact geometry auto-shapes or vector pictures dynamically from code.
- **Text Replacement**: Replace `{{mustache}}` template variables anywhere on a slide, or target specific named shapes.
- **Image Replacement**: Easily swap out placeholder images with `.png`, `.jpg`, or `.svg` files while preserving position and styling.
- **Chart & Table Data Binding**: Update the underlying data of native PowerPoint charts (`BarChart`, `PieChart`, `LineChart`) directly via their class objects.

## Installation

As this package is under development, you can use it locally by ensuring the package is in your `sys.path` or installing it in editable mode:

```bash
pip install -e .
```

Dependencies:
- `lxml`

## Quick Start (Templating)

The most robust way to use `pptxizza` is to create a template (`template.pptx`) with some named placeholders or `{{mustache_keys}}`, then parse it:

```python
from pptxizza import Presentation

def main():
    pres = Presentation("template.pptx")
    slide = pres.slides[0]

    # Fill the slide with dynamic content mapped to shape names
    slide.fill({
        "{{title}}": "Quarterly Business Review",  # Global mustache text replacement
        "SubtitleShape": "Q3 2026 Results",         # Named shape text replacement
        "LogoPlaceholder": "company_logo.svg"       # Named picture shape replacement (injects SVG!)
    })

    pres.save("output.pptx")

if __name__ == "__main__":
    main()
```

## Creating a Presentation from Scratch

If you just need a blank presentation without an existing template, you can initialize `Presentation` with no arguments.

```python
from pptxizza import Presentation

def main():
    # Calling Presentation() without an argument loads a default blank template
    pres = Presentation()
    
    # You can now add slides or insert shapes programmatically
    # ...
    
    pres.save("new_presentation.pptx")

if __name__ == "__main__":
    main()
```

## The Object-Oriented API (`pptxizza.shapes`)

`pptxizza` maps OpenXML structures directly into typed Python objects, allowing for programmatic instantiation and highly specific template discovery.

### 1. Generating & Inserting Shapes Programmatically

Instead of relying solely on existing templates, you can import granular geometric components from `pptxizza.shapes` to build slides from scratch!

```python
from pptxizza.shapes import Shape, Rect, Circle, Picture, Table, TextBox
from pptxizza import Inches

slide = pres.slides[0]

# Create a primitive Rect shape with text
my_rect = Rect(text="Action Item", x=Inches(1), y=Inches(1), cx=Inches(3), cy=Inches(1))

# Create an Oval/Circle
my_circle = Circle(text="1", x=Inches(5), y=Inches(1), cx=Inches(1), cy=Inches(1))

# Create a Table (3 rows, 3 columns)
my_table = Table(rows=3, cols=3, x=Inches(1), y=Inches(3), cx=Inches(6), cy=Inches(1.5))

# Create a TextBox (supports either a string or a rich Text object!)
t = Text()
t.add_run("Dynamic ", bold=True)
t.add_run("Note", color=RGBColor(255, 0, 0))
my_textbox = TextBox(text=t, x=Inches(1), y=Inches(2.5), cx=Inches(2), cy=Inches(0.5))

# Create an SVG Vector graphic dynamically
my_ufo = Picture(image_path="ufo.svg", x=Inches(3), y=Inches(5), cx=Inches(2), cy=Inches(2))

# Insert the objects natively. Background relation mapping is handled automatically!
slide.insert_shape(my_rect)
slide.insert_shape(my_circle)
slide.insert_shape(my_table)
slide.insert_shape(my_textbox)
slide.insert_shape(my_ufo)
```

_Note_: `x`, `y`, `cx`, and `cy` are tracked in English Metric Units (EMUs). All shapes also expose `x_inches`, `y_inches`, `width_inches`, and `height_inches` for easier visual math.

### 2. Inspecting and Manipulating Dynamic Shapes

`pptxizza` intelligently probes into the ZIP packages under the hood to detect exactly what's sitting on a template slide. When examining `slide.shapes`, generic graphic frames are dynamically resolved into explicit subclasses like `BarChart`, `PieChart`, or `LineChart`.

```python
for shape in slide.shapes:
    print(f"Discovered: {type(shape).__name__} named '{shape.shape_name}'")
    
    # You can access common underlying properties easily!
    shape.x_inches += 1.5  # shift everything right by 1.5 inches
```

### 3. Updating Chart Data via Shape Objects

When `slide.shapes` returns a chart class (e.g. `BarChart`, `PieChart`), you can work with chart data in a pandas-like way without adding `pandas` as a dependency. The most ergonomic form is the `split` orientation: category labels in `index`, series names in `columns`, and row-major values in `data`.

```python
from pptxizza.shapes import BarChart, Chart

for shape in slide.shapes:
    if isinstance(shape, BarChart) and shape.shape_name == "SalesChart":
        existing = shape.get_data(orient="split")
        print(existing)
        # {
        #     "index": ["Jan", "Feb", "Mar"],
        #     "columns": ["Series 1", "Europe"],
        #     "data": [
        #         [10, 40],
        #         [20, 50],
        #         [30, 60],
        #     ],
        # }

        # Overwrite the chart completely using a pandas-like split structure.
        # This can shrink or grow the number of series compared to the template.
        shape.set_data(
            {
                "index": ["Q1", "Q2", "Q3"],
                "columns": ["Revenue", "Europe", "APAC"],
                "data": [
                    [150, 120, 100],
                    [200, 180, 140],
                    [250, 210, 175],
                ],
            }
        )

        # A shorthand row mapping is also supported when you want category names
        # with one value per current series in each row.
        shape.set_data(
            {
                "Category 1": [1, 2, 3],
                "Category 2": [4, 5, 6],
                "Category 3": [7, 8, 9],
            }
        )

        # Patch selected series only, matched by index or existing series name.
        shape.replace_data(
            {
                0: [155, 205, 255],
                "Europe": [125, 185, 215],
            },
            ["Q1", "Q2", "Q3"],
        )
        
    if isinstance(shape, Chart):
        # Chart title support
        if not shape.has_title:
            shape.set_title("Quarterly Report")
        else:
            shape.title_text = "Updated Title"
```

Notes:
- `.get_data(orient="split")` returns a pandas-like mapping with `index`, `columns`, and `data`.
- `.get_data(orient="series")` returns the verbose structure with `categories` and `series` if you prefer that orientation.
- `.set_data()` accepts:
  - the verbose `{"categories": ..., "series": ...}` structure
  - the pandas-like `split` structure
  - a DataFrame-like object exposing `index`, `columns`, and `values.tolist()` or `to_numpy().tolist()`
  - the shorthand row mapping `{category_name: [series values...]}`
- `.set_data()` can shrink or grow the number of series compared to the template chart.
- `.replace_data()` updates only the series you specify, matched by zero-based index or existing series name.
- `.replace_chart_data()` is a compatibility alias for `.replace_data()`.
- Embedded Excel chart workbooks are updated alongside the chart XML, so saved presentations reflect the new data in PowerPoint.
- If you provide unmatched series names, `pptxizza` raises a `ValueError` listing the available keys instead of silently doing nothing.

### 4. Formatting Chart Series

For common chart formatting tasks, you can work with `chart.series` or look up a single series by index or name.

```python
from pptxizza.shapes import Chart

for shape in slide.shapes:
    if isinstance(shape, Chart):
        revenue = shape.get_series("Revenue")
        revenue.set_color("#FF6600")
        revenue.set_data_labels(
            show_value=True,
            show_category_name=True,
            position="outEnd",
            separator=" | ",
        )
        revenue.set_trendline(
            trendline_type="linear",
            display_equation=True,
            display_r_squared=True,
        )

        # Access by zero-based index as well
        shape.get_series(1).set_color((0, 120, 215))

        # Remove formatting when needed
        revenue.clear_data_labels()
        revenue.clear_trendline()
```

Available series helpers:
- `.set_color(color)`
- `.set_data_labels(...)`
- `.clear_data_labels()`
- `.set_trendline(...)`
- `.clear_trendline()`

### 5. Manipulating Tables

`Table` objects allow for granular cell access and automatic data binding.

```python
from pptxizza.shapes import Table

for shape in slide.shapes:
    if isinstance(shape, Table):
        # Access dimensions
        print(f"Table is {shape.row_count}x{shape.column_count}")
        
        # Populate from a list of dicts (auto-headers)
        data = [
            {"ID": 1, "Name": "Alice", "Status": "Active"},
            {"ID": 2, "Name": "Bob", "Status": "Pending"}
        ]
        shape.set_data(data)
        
        # Or populate from a matrix (list of lists)
        # shape.set_data([["H1", "H2"], ["V1", "V2"]])
        
        # Ensure the table is big enough
        shape.ensure_size(rows=5, cols=3)
        
        # Access cells by (row, col)
        shape.cell(0, 0).text = "Updated Header"
        
        # Convert back to matrix
        matrix = shape.to_matrix()
```

### 6. Styling and Rich Text

Most shapes support direct styling of fill and font properties. You can also use the `Text` and `TextRun` objects for rich text formatting with comprehensive control over horizontal and vertical alignment, character formatting, and transparency.

#### Text Alignment and Positioning

```python
from pptxizza import Text, Pt

# Horizontal Alignment
text = Text(alignment="center")  # or "left", "right", "justify"
text.add_run("Centered text")

# Vertical Alignment (within text frame)
text = Text(vertical_alignment="middle")  # or "top", "bottom"
text.add_run("Vertically centered text")

# Combined
text = Text(alignment="center", vertical_alignment="middle")
text.add_run("Centered both ways")
```

#### Rich Text with Character Formatting

```python
from pptxizza import RGBColor, HexColor, ThemeColor, Pt, Text

# Multiple formatted runs in one paragraph
t = Text()
t.add_run("Normal text, ")
t.add_run("BOLD AND RED", bold=True, color=HexColor("FF0000"))
t.add_run(" and ", italic=True)
t.add_run("LARGE BLUE", size=Pt(40), color=ThemeColor("accent1"))
t.add_run(" with ", underline=True)
t.add_run("STRIKETHROUGH", strikethrough=True)

shape.text = t
```

#### Advanced Character Formatting

```python
# Superscript and subscript
text = Text()
text.add_run("H")
text.add_run("2", subscript=True)  # H₂O for water
text.add_run("O")

text = Text()
text.add_run("E=mc")
text.add_run("2", superscript=True)  # E=mc²

# Transparency/Opacity (0.0 = fully transparent, 1.0 = fully opaque)
text = Text()
text.add_run("Fully opaque", transparency=1.0, color=RGBColor(255, 0, 0))
text.add_run(" | Semi-transparent ", transparency=0.5, color=RGBColor(0, 255, 0))
text.add_run("Very transparent", transparency=0.2, color=RGBColor(0, 0, 255))

# Letter spacing
text = Text()
text.add_run("Spaced out text", letter_spacing=Pt(5))

# Custom font
text = Text()
text.add_run("Helvetica font", font_name="Helvetica")
```

#### Available TextRun Properties

When creating a `TextRun` directly or via `text.add_run()`:

| Property | Type | Description |
|----------|------|-------------|
| `text` | str | The actual text content |
| `bold` | bool | Make text bold |
| `italic` | bool | Make text italic |
| `underline` | bool | Underline the text |
| `strikethrough` | bool | Strike through the text |
| `superscript` | bool | Raise text above baseline |
| `subscript` | bool | Lower text below baseline |
| `color` | Color/str/tuple | Text color (Color object, hex string, or RGB tuple) |
| `font_name` | str | Font typeface (e.g., "Arial", "Helvetica") |
| `size` | Length | Font size (e.g., `Pt(12)`) |
| `transparency` | float | Opacity from 0.0 (transparent) to 1.0 (opaque) |
| `letter_spacing` | Length | Space between characters |

#### Available Text Paragraph Properties

When creating a `Text` object:

| Property | Type | Values | Description |
|----------|------|--------|-------------|
| `alignment` | str | "left", "center", "right", "justify" | Horizontal text alignment |
| `vertical_alignment` | str | "top", "middle", "bottom" | Vertical text alignment within frame |

#### Editing Existing Shape Runs with `text_object`

If you need direct run-level editing on an existing shape, use `shape.text_object`.
It returns a mutable `Text` object (instead of a plain string), so you can inspect and update runs, then assign it back.

```python
from pptxizza import TextRun

shape = slide.shapes[0]

# Read existing shape content as a structured Text object
t = shape.text_object

# Modify an existing run
if t.runs:
    t.runs[0].text = "Updated "
    t.runs[0].bold = True

# Add a new run
t.runs.append(TextRun("content", italic=True))

# Write back to the shape
shape.text_object = t

# `shape.text` still works and returns the concatenated plain text
print(shape.text)  # Updated content
```

```python
# Solid Fill
shape.fill.solid(HexColor("#4287f5"))

# Font Styling
shape.font.size = Pt(24)
shape.font.bold = True
shape.font.color = RGBColor(255, 255, 255)
```

## Advanced Native Image Support (SVG)

Native PowerPoint doesn't typically embed pure SVGs comfortably via older XML specifications. `pptxizza` utilizes modern Microsoft Open XML extension nodes natively mapping `a:svgBlip` relations to automatically parse, embed, and inject SVGs interchangeably with PNGs and JPGs!

```python
# Replace any shape (placeholder) with an image directly
slide.find_text("{{image_here}}")[0].replace_with_picture("logo.svg")
```

## Spatial Shape Queries

`pptxizza` lets you query shapes by their physical position on the slide, which is useful when you need to locate shapes relative to a known coordinate or another shape (e.g. finding the label closest to a data icon).

### `slide.find_shapes_by_distance(x, y, ...)`

Returns all shapes sorted by Euclidean distance from a point `(x, y)` in EMUs, closest first.

```python
from pptxizza import Inches

# All shapes sorted by distance from the top-left corner
results = slide.find_shapes_by_distance(0, 0)

# Limit to the 3 closest shapes
results = slide.find_shapes_by_distance(Inches(3), Inches(2), limit=3)

# Filter by type
from pptxizza.shapes import Picture
results = slide.find_shapes_by_distance(Inches(3), Inches(2), shape_type=Picture)

# Or by a case-insensitive class name string
results = slide.find_shapes_by_distance(Inches(3), Inches(2), shape_type="textbox")
```

You can also constrain the search to shapes in a specific direction relative to the point using the `direction` parameter:

| Value | Condition |
|-------|-----------|
| `"right"` | shape's left edge is strictly to the right of `x` |
| `"left"` | shape's right edge is strictly to the left of `x` |
| `"below"` | shape's top edge is strictly below `y` |
| `"above"` | shape's bottom edge is strictly above `y` |

```python
# Find the closest shape that is to the right of x=3in
results = slide.find_shapes_by_distance(Inches(3), Inches(2), direction="right")

# Combine with type filter and limit
results = slide.find_shapes_by_distance(Inches(3), Inches(2), shape_type="rect", direction="below", limit=2)
```

### `slide.find_closest_shape_to(shape, ...)`

A convenience wrapper around `find_shapes_by_distance` that uses the center of an existing shape as the search origin. The reference shape itself is always excluded from results.

```python
# Find the 2 shapes closest to a known picture
anchor = slide.find_shapes(shape_type=Picture)[0]
nearby = slide.find_closest_shape_to(anchor, limit=2)

# Find the nearest TextBox to the right of an anchor shape
label = slide.find_closest_shape_to(anchor, shape_type="textbox", direction="right", limit=1)
```

All parameters (`shape_type`, `limit`, `direction`) behave identically to `find_shapes_by_distance`. The `limit` is applied after the reference shape is excluded, so `limit=1` always returns the single closest *other* shape.
