agentsclimarketplace

Pptx generation

Skill yigityildiz0/universal-ai-skill-library/skills/common/pptx-generation

531 searchable AI Agent Skills for Claude Code, OpenAI Codex, and OpenCode — EN/TR catalog, platform and risk notes, direct ZIPs, and curated bundles.

Install
npx -y skills add yigityildiz0/universal-ai-skill-library --skill pptx-generation

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

3 things to look at

  • 18 days oldThe repository was created 18 days ago. New is not bad, but a brand new repository carrying a familiar-sounding name is the shape a typosquat arrives in, and there has been no time for anyone else to find a problem with it.
  • no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
  • 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

PowerPoint presentation generation expertise for creating, editing, and designing professional slide decks programmatically. Use when building presentation.

SKILL.md

58.3 KB, as published. Nobody here has run it

PPTX Generation

Structured guidance for building systems that generate professional PowerPoint presentations programmatically. Covers library selection, slide layout design, chart integration, master slide management, template-based generation, batch processing, and quality assurance strategies for automated presentation pipelines.

When to Use This Skill

Use this skill for:

  • Building automated presentation generators from structured data
  • Creating report decks (financial summaries, analytics dashboards, project status updates)
  • Designing reusable slide templates with consistent corporate branding
  • Adding charts, tables, and data visualizations to slides programmatically
  • Populating existing PowerPoint templates with dynamic content
  • Batch-generating personalized slide decks from datasets (mail merge pattern)
  • Integrating presentation generation into CI/CD pipelines or reporting workflows
  • Converting Markdown, JSON, or database records into formatted slide decks

Trigger phrases: "pptx", "PowerPoint generation", "slide deck", "presentation builder", "python-pptx", "PptxGenJS", "slide template", "chart slides", "automated reports", "batch presentations", "slide layouts", "master slides", "branding deck", "report generator", "slide automation"

What This Skill Does

Provides presentation generation patterns including:

  • Library Selection: Decision matrix for python-pptx, PptxGenJS, Apache POI, and LibreOffice approaches
  • Slide Design: Layout patterns for title, content, two-column, section divider, and closing slides
  • Charts and Data: Bar, line, pie, scatter, and combo charts with data-driven generation
  • Master Slides: Theme management, color schemes, font families, and brand consistency
  • Templates: Loading existing .pptx templates, populating placeholders, and extending layouts
  • Batch Generation: Mail merge patterns, data-driven deck creation, and parallel processing
  • Quality Assurance: Slide count verification, content extraction, visual validation, and file size optimization

Instructions

Step 1: Library Selection

Choose a PPTX generation library based on your runtime environment, feature requirements, and integration constraints. The following decision matrix compares the four primary options.

Library Comparison Matrix:

Criteriapython-pptxPptxGenJSApache POILibreOffice (CLI)
LanguagePythonJavaScript/TypeScriptJava/KotlinAny (CLI wrapper)
LicenseMITMITApache 2.0MPL 2.0
Template supportFull (load/modify .pptx)Limited (no template loading)Full (load/modify .pptx)Full (via UNO API)
Chart supportNative (OOXML charts)Built-in chart typesNative (OOXML charts)Full (via template)
Table supportFull with merged cellsFull with stylingFull with merged cellsFull
Image supportPNG, JPEG, SVG (via EMF)PNG, JPEG, SVG, GIFPNG, JPEG, EMF, WMFAll formats
Master slidesRead and modifyCreate from scratch onlyRead and modifyRead and modify
File sizeSmall (efficient XML)SmallMedium (Java overhead)Depends on conversion
DependenciesPure PythonZero dependencies (browser/Node)JVM requiredLibreOffice installation
MaturityStable, widely usedActive, growingVery matureVery mature
Best forBackend report generationBrowser/Node slide buildersEnterprise Java stacksConverting other formats

Decision Guide:

  • Choose python-pptx when you have a Python backend, need to load and modify existing templates, require native OOXML chart support, or are building data pipeline report generators
  • Choose PptxGenJS when you need browser-side generation, are building a Node.js service, want zero-dependency simplicity, or need to generate slides from a web application
  • Choose Apache POI when you are in a Java/Kotlin ecosystem, need enterprise-grade OOXML manipulation, or must integrate with existing Java reporting infrastructure
  • Choose LibreOffice CLI when you need to convert other formats (HTML, Markdown, ODP) to PPTX, require a headless server-side converter, or need PDF export from slides

Installation:

# python-pptx (Python)
pip install python-pptx
# or with uv
uv pip install python-pptx

# PptxGenJS (Node.js)
npm install pptxgenjs
# or browser via CDN
# <script src="https://cdn.jsdelivr.net/npm/pptxgenjs/dist/pptxgenjs.bundle.js"></script>

# Apache POI (Maven)
# <dependency>
#   <groupId>org.apache.poi</groupId>
#   <artifactId>poi-ooxml</artifactId>
#   <version>5.2.5</version>
# </dependency>

# LibreOffice CLI (system package)
# apt install libreoffice-impress   # Debian/Ubuntu
# brew install --cask libreoffice   # macOS

Step 2: Python python-pptx Fundamentals

python-pptx is the most popular Python library for creating and modifying PowerPoint files. It provides full access to the OOXML presentation model including slides, layouts, placeholders, shapes, text frames, tables, charts, and images.

Core Object Model:

Presentation
  -> SlideMasters[]
       -> SlideLayouts[]
  -> Slides[]
       -> Shapes[]
            -> TextFrame -> Paragraphs[] -> Runs[]
            -> Table -> Rows[] -> Cells[]
            -> Chart -> ChartData
            -> Picture

Creating a Presentation from Scratch:

from pptx import Presentation
from pptx.util import Inches, Pt, Emu
from pptx.enum.text import PP_ALIGN, MSO_ANCHOR
from pptx.dml.color import RGBColor

def create_presentation() -> Presentation:
    """Create a new presentation with standard 16:9 dimensions."""
    prs = Presentation()
    # Set slide dimensions to 16:9 (13.333 x 7.5 inches)
    prs.slide_width = Inches(13.333)
    prs.slide_height = Inches(7.5)
    return prs

Understanding Slide Layouts:

Every presentation has a slide master that contains slide layouts. The default template provides these standard layouts:

def list_available_layouts(prs: Presentation) -> list[dict]:
    """List all available slide layouts from the slide master."""
    layouts = []
    for idx, layout in enumerate(prs.slide_masters[0].slide_layouts):
        layouts.append({
            "index": idx,
            "name": layout.name,
            "placeholders": [
                {"idx": ph.placeholder_format.idx, "name": ph.name, "type": ph.placeholder_format.type}
                for ph in layout.placeholders
            ],
        })
    return layouts

# Standard layout indices (default template):
# 0 = Title Slide (title + subtitle)
# 1 = Title and Content (title + body)
# 2 = Section Header
# 3 = Two Content (title + two body columns)
# 4 = Comparison (title + two columns with subtitles)
# 5 = Title Only
# 6 = Blank
# 7 = Content with Caption
# 8 = Picture with Caption

Adding Slides with Text:

from pptx.util import Inches, Pt
from pptx.dml.color import RGBColor
from pptx.enum.text import PP_ALIGN

def add_title_slide(
    prs: Presentation,
    title: str,
    subtitle: str,
) -> None:
    """Add a title slide (layout index 0) with title and subtitle."""
    slide_layout = prs.slide_layouts[0]
    slide = prs.slides.add_slide(slide_layout)

    slide.placeholders[0].text = title
    slide.placeholders[1].text = subtitle


def add_content_slide(
    prs: Presentation,
    title: str,
    body_text: str,
    bullet_points: list[str] | None = None,
) -> None:
    """Add a title-and-content slide (layout index 1) with formatted text."""
    slide_layout = prs.slide_layouts[1]
    slide = prs.slides.add_slide(slide_layout)

    # Set title
    slide.placeholders[0].text = title

    # Set body content
    text_frame = slide.placeholders[1].text_frame
    text_frame.clear()

    if body_text:
        paragraph = text_frame.paragraphs[0]
        paragraph.text = body_text
        paragraph.font.size = Pt(18)
        paragraph.font.color.rgb = RGBColor(0x33, 0x33, 0x33)

    if bullet_points:
        for point in bullet_points:
            paragraph = text_frame.add_paragraph()
            paragraph.text = point
            paragraph.level = 0
            paragraph.font.size = Pt(16)
            paragraph.space_after = Pt(6)

Working with Text Frames and Runs:

from pptx.util import Pt
from pptx.dml.color import RGBColor
from pptx.enum.text import PP_ALIGN

def add_formatted_text(
    prs: Presentation,
    title: str,
    content_blocks: list[dict],
) -> None:
    """Add a slide with rich text formatting using runs.

    Each content_block: {"text": str, "bold": bool, "size": int, "color": str}
    """
    slide = prs.slides.add_slide(prs.slide_layouts[5])  # Title Only layout
    slide.placeholders[0].text = title

    # Add a text box for custom-positioned content
    from pptx.util import Inches
    left = Inches(1.0)
    top = Inches(2.0)
    width = Inches(11.0)
    height = Inches(4.5)
    text_box = slide.shapes.add_textbox(left, top, width, height)
    text_frame = text_box.text_frame
    text_frame.word_wrap = True

    for idx, block in enumerate(content_blocks):
        if idx == 0:
            paragraph = text_frame.paragraphs[0]
        else:
            paragraph = text_frame.add_paragraph()

        run = paragraph.add_run()
        run.text = block["text"]
        run.font.size = Pt(block.get("size", 14))
        run.font.bold = block.get("bold", False)
        run.font.italic = block.get("italic", False)

        color_hex = block.get("color", "333333")
        run.font.color.rgb = RGBColor.from_string(color_hex)

    # Set paragraph alignment
    for paragraph in text_frame.paragraphs:
        paragraph.alignment = PP_ALIGN.LEFT

Adding Images:

from pptx.util import Inches

def add_image_slide(
    prs: Presentation,
    title: str,
    image_path: str,
    left: float = 2.0,
    top: float = 2.0,
    width: float = 9.0,
) -> None:
    """Add a slide with a positioned image.

    The height is calculated automatically to maintain aspect ratio.
    """
    slide = prs.slides.add_slide(prs.slide_layouts[5])  # Title Only
    slide.placeholders[0].text = title

    slide.shapes.add_picture(
        image_path,
        left=Inches(left),
        top=Inches(top),
        width=Inches(width),
        # height omitted: auto-calculated from aspect ratio
    )

Adding Tables:

from pptx.util import Inches, Pt
from pptx.dml.color import RGBColor
from pptx.enum.text import PP_ALIGN

def add_table_slide(
    prs: Presentation,
    title: str,
    headers: list[str],
    rows: list[list[str]],
) -> None:
    """Add a slide with a formatted data table."""
    slide = prs.slides.add_slide(prs.slide_layouts[5])  # Title Only
    slide.placeholders[0].text = title

    row_count = len(rows) + 1  # +1 for header
    col_count = len(headers)
    left = Inches(1.0)
    top = Inches(2.0)
    width = Inches(11.0)
    height = Inches(0.5 * row_count)

    table_shape = slide.shapes.add_table(
        row_count, col_count, left, top, width, height,
    )
    table = table_shape.table

    # Style header row
    for col_idx, header in enumerate(headers):
        cell = table.cell(0, col_idx)
        cell.text = header
        for paragraph in cell.text_frame.paragraphs:
            paragraph.font.bold = True
            paragraph.font.size = Pt(12)
            paragraph.font.color.rgb = RGBColor(0xFF, 0xFF, 0xFF)
            paragraph.alignment = PP_ALIGN.CENTER
        cell.fill.solid()
        cell.fill.fore_color.rgb = RGBColor(0x2E, 0x4A, 0x7A)

    # Populate data rows
    for row_idx, row_data in enumerate(rows):
        for col_idx, cell_value in enumerate(row_data):
            cell = table.cell(row_idx + 1, col_idx)
            cell.text = str(cell_value)
            for paragraph in cell.text_frame.paragraphs:
                paragraph.font.size = Pt(11)
                paragraph.alignment = PP_ALIGN.LEFT

    # Set column widths proportionally
    total_width = Inches(11.0)
    col_width = int(total_width / col_count)
    for col_idx in range(col_count):
        table.columns[col_idx].width = col_width

Saving the Presentation:

from pathlib import Path

def save_presentation(prs: Presentation, output_path: str | Path) -> Path:
    """Save presentation to disk and return the resolved path."""
    output = Path(output_path).resolve()
    output.parent.mkdir(parents=True, exist_ok=True)
    prs.save(str(output))
    return output

Step 3: JavaScript PptxGenJS

PptxGenJS is a zero-dependency JavaScript library that generates PPTX files in both browser and Node.js environments. It provides a fluent API for creating slides with text, images, charts, tables, and shapes.

Creating a Presentation (Node.js):

import PptxGenJS from "pptxgenjs";

interface SlideTheme {
  primaryColor: string;
  secondaryColor: string;
  fontFamily: string;
  titleSize: number;
  bodySize: number;
}

const DEFAULT_THEME: SlideTheme = {
  primaryColor: "2E4A7A",
  secondaryColor: "5B8DEF",
  fontFamily: "Segoe UI",
  titleSize: 28,
  bodySize: 14,
};

function createPresentation(theme: SlideTheme = DEFAULT_THEME): PptxGenJS {
  const pptx = new PptxGenJS();

  // Set presentation metadata
  pptx.author = "Automated Report Generator";
  pptx.company = "Your Company";
  pptx.subject = "Generated Presentation";

  // Set 16:9 layout
  pptx.layout = "LAYOUT_16x9";

  // Define reusable master slides
  pptx.defineSlideMaster({
    title: "TITLE_SLIDE",
    background: { color: theme.primaryColor },
    objects: [
      {
        placeholder: {
          options: {
            name: "title",
            type: "title",
            x: 1.0,
            y: 2.5,
            w: 11.0,
            h: 1.5,
            fontFace: theme.fontFamily,
            fontSize: 36,
            color: "FFFFFF",
            align: "center",
          },
          text: "",
        },
      },
      {
        placeholder: {
          options: {
            name: "subtitle",
            type: "body",
            x: 2.0,
            y: 4.2,
            w: 9.0,
            h: 1.0,
            fontFace: theme.fontFamily,
            fontSize: 18,
            color: "CCCCCC",
            align: "center",
          },
          text: "",
        },
      },
    ],
  });

  pptx.defineSlideMaster({
    title: "CONTENT_SLIDE",
    background: { color: "FFFFFF" },
    objects: [
      {
        rect: {
          x: 0,
          y: 0,
          w: "100%",
          h: 0.75,
          fill: { color: theme.primaryColor },
        },
      },
      {
        placeholder: {
          options: {
            name: "title",
            type: "title",
            x: 0.5,
            y: 0.1,
            w: 12.0,
            h: 0.55,
            fontFace: theme.fontFamily,
            fontSize: 22,
            color: "FFFFFF",
            bold: true,
          },
          text: "",
        },
      },
    ],
  });

  return pptx;
}

Adding Slides with Content:

function addTitleSlide(
  pptx: PptxGenJS,
  title: string,
  subtitle: string,
): void {
  const slide = pptx.addSlide({ masterName: "TITLE_SLIDE" });
  slide.addText(title, {
    placeholder: "title",
  });
  slide.addText(subtitle, {
    placeholder: "subtitle",
  });
}

function addContentSlide(
  pptx: PptxGenJS,
  title: string,
  bulletPoints: string[],
  theme: SlideTheme = DEFAULT_THEME,
): void {
  const slide = pptx.addSlide({ masterName: "CONTENT_SLIDE" });
  slide.addText(title, { placeholder: "title" });

  const textRows = bulletPoints.map((point) => ({
    text: point,
    options: {
      fontSize: theme.bodySize,
      fontFace: theme.fontFamily,
      color: "333333",
      bullet: { type: "bullet" as const },
      paraSpaceAfter: 6,
    },
  }));

  slide.addText(textRows, {
    x: 0.75,
    y: 1.2,
    w: 11.5,
    h: 5.5,
    valign: "top",
  });
}

Adding Tables:

interface TableConfig {
  headers: string[];
  rows: string[][];
  theme?: SlideTheme;
}

function addTableSlide(
  pptx: PptxGenJS,
  title: string,
  config: TableConfig,
): void {
  const theme = config.theme ?? DEFAULT_THEME;
  const slide = pptx.addSlide({ masterName: "CONTENT_SLIDE" });
  slide.addText(title, { placeholder: "title" });

  const headerRow: PptxGenJS.TableCell[] = config.headers.map((h) => ({
    text: h,
    options: {
      bold: true,
      color: "FFFFFF",
      fill: { color: theme.primaryColor },
      fontSize: 11,
      align: "center" as const,
    },
  }));

  const dataRows: PptxGenJS.TableCell[][] = config.rows.map((row) =>
    row.map((cell) => ({
      text: cell,
      options: {
        fontSize: 10,
        color: "333333",
        border: { type: "solid", pt: 0.5, color: "CCCCCC" },
      },
    })),
  );

  slide.addTable([headerRow, ...dataRows], {
    x: 0.5,
    y: 1.2,
    w: 12.0,
    colW: config.headers.map(() => 12.0 / config.headers.length),
    rowH: 0.4,
    autoPage: true,
    autoPageRepeatHeader: true,
  });
}

Adding Images and Shapes:

import * as fs from "node:fs";
import * as path from "node:path";

function addImageSlide(
  pptx: PptxGenJS,
  title: string,
  imagePath: string,
): void {
  const slide = pptx.addSlide({ masterName: "CONTENT_SLIDE" });
  slide.addText(title, { placeholder: "title" });

  // Read image as base64 for Node.js
  const imageBuffer = fs.readFileSync(imagePath);
  const base64 = imageBuffer.toString("base64");
  const ext = path.extname(imagePath).slice(1).toLowerCase();

  slide.addImage({
    data: `image/${ext};base64,${base64}`,
    x: 2.0,
    y: 1.5,
    w: 9.0,
    h: 5.0,
    sizing: { type: "contain", w: 9.0, h: 5.0 },
  });
}

function addShapeSlide(pptx: PptxGenJS, title: string): void {
  const slide = pptx.addSlide({ masterName: "CONTENT_SLIDE" });
  slide.addText(title, { placeholder: "title" });

  // Add a rounded rectangle with text
  slide.addShape(pptx.ShapeType.roundRect, {
    x: 3.0,
    y: 2.0,
    w: 7.0,
    h: 3.0,
    fill: { color: "E8F0FE" },
    line: { color: "2E4A7A", width: 2 },
    rectRadius: 0.2,
  });

  slide.addText("Key Insight", {
    x: 3.5,
    y: 3.0,
    w: 6.0,
    h: 1.0,
    fontSize: 24,
    color: "2E4A7A",
    align: "center",
    bold: true,
  });
}

Saving (Node.js and Browser):

// Node.js: save to file
async function saveToFile(
  pptx: PptxGenJS,
  outputPath: string,
): Promise<string> {
  await pptx.writeFile({ fileName: outputPath });
  return outputPath;
}

// Browser: trigger download
async function downloadInBrowser(
  pptx: PptxGenJS,
  fileName: string,
): Promise<void> {
  await pptx.writeFile({ fileName });
  // PptxGenJS handles the browser download automatically
}

// Get as base64 (for API responses)
async function toBase64(pptx: PptxGenJS): Promise<string> {
  const output = await pptx.write({ outputType: "base64" });
  return output as string;
}

Step 4: Slide Design Patterns

Consistent slide design requires a defined system of layout types, spacing rules, and visual hierarchy. The following patterns cover the most common slide types needed in automated presentation generation.

Slide Type Taxonomy:

Slide TypePurposeLayoutKey Elements
Title SlideOpening, section startFull background colorTitle (36pt), subtitle (18pt), logo
Content SlideBody informationHeader bar + white bodyTitle (22pt), bullets/text (14pt)
Two-ColumnComparison, dual infoHeader bar + two panelsTitle, left column, right column
Section DividerTopic transitionAccent backgroundSection title (32pt), section number
Data SlideCharts and tablesHeader bar + data areaTitle, chart/table, source note
Image SlideVisuals, screenshotsHeader bar + image areaTitle, image (contain fit), caption
Key TakeawayEmphasis, calloutAccent backgroundIcon, headline (28pt), supporting text
Closing SlideEnd, contact infoFull background colorThank you text, contact details, logo

Design System Constants (python-pptx):

from dataclasses import dataclass
from pptx.dml.color import RGBColor
from pptx.util import Inches, Pt


@dataclass(frozen=True)
class DesignSystem:
    """Centralized design constants for consistent slide generation."""

    # Colors
    primary: RGBColor = RGBColor(0x2E, 0x4A, 0x7A)
    secondary: RGBColor = RGBColor(0x5B, 0x8D, 0xEF)
    accent: RGBColor = RGBColor(0xE8, 0x6C, 0x00)
    text_dark: RGBColor = RGBColor(0x33, 0x33, 0x33)
    text_light: RGBColor = RGBColor(0xFF, 0xFF, 0xFF)
    background_light: RGBColor = RGBColor(0xF5, 0xF7, 0xFA)
    border: RGBColor = RGBColor(0xDD, 0xDD, 0xDD)

    # Typography
    font_family: str = "Segoe UI"
    title_size: Pt = Pt(28)
    heading_size: Pt = Pt(22)
    body_size: Pt = Pt(14)
    caption_size: Pt = Pt(10)

    # Spacing (16:9 slide = 13.333 x 7.5 inches)
    margin_left: Inches = Inches(0.75)
    margin_top: Inches = Inches(1.0)
    content_width: Inches = Inches(11.83)
    content_height: Inches = Inches(5.75)
    header_height: Inches = Inches(0.75)
    footer_height: Inches = Inches(0.4)


DESIGN = DesignSystem()

Two-Column Layout:

from pptx import Presentation
from pptx.util import Inches, Pt
from pptx.enum.text import PP_ALIGN

def add_two_column_slide(
    prs: Presentation,
    title: str,
    left_title: str,
    left_points: list[str],
    right_title: str,
    right_points: list[str],
) -> None:
    """Add a two-column comparison slide with independent bullet lists."""
    slide = prs.slides.add_slide(prs.slide_layouts[5])  # Title Only
    slide.placeholders[0].text = title

    column_width = Inches(5.5)
    left_x = Inches(0.75)
    right_x = Inches(7.0)
    top_y = Inches(1.5)
    height = Inches(5.0)

    for col_x, col_title, points in [
        (left_x, left_title, left_points),
        (right_x, right_title, right_points),
    ]:
        text_box = slide.shapes.add_textbox(col_x, top_y, column_width, height)
        tf = text_box.text_frame
        tf.word_wrap = True

        # Column header
        p = tf.paragraphs[0]
        p.text = col_title
        p.font.size = Pt(18)
        p.font.bold = True
        p.font.color.rgb = DESIGN.primary
        p.space_after = Pt(12)

        # Bullet points
        for point in points:
            p = tf.add_paragraph()
            p.text = point
            p.font.size = Pt(13)
            p.font.color.rgb = DESIGN.text_dark
            p.level = 0
            p.space_after = Pt(6)

Section Divider Slide:

from pptx.util import Inches, Pt
from pptx.dml.color import RGBColor
from pptx.enum.text import PP_ALIGN, MSO_ANCHOR

def add_section_divider(
    prs: Presentation,
    section_number: int,
    section_title: str,
) -> None:
    """Add a section divider slide with number and title on accent background."""
    slide = prs.slides.add_slide(prs.slide_layouts[6])  # Blank

    # Full-slide background
    background = slide.background
    fill = background.fill
    fill.solid()
    fill.fore_color.rgb = DESIGN.primary

    # Section number
    num_box = slide.shapes.add_textbox(
        Inches(1.0), Inches(2.0), Inches(11.0), Inches(1.5),
    )
    num_tf = num_box.text_frame
    num_tf.paragraphs[0].text = f"0{section_number}" if section_number < 10 else str(section_number)
    num_tf.paragraphs[0].font.size = Pt(60)
    num_tf.paragraphs[0].font.bold = True
    num_tf.paragraphs[0].font.color.rgb = DESIGN.secondary
    num_tf.paragraphs[0].alignment = PP_ALIGN.LEFT

    # Section title
    title_box = slide.shapes.add_textbox(
        Inches(1.0), Inches(3.5), Inches(11.0), Inches(1.5),
    )
    title_tf = title_box.text_frame
    title_tf.paragraphs[0].text = section_title
    title_tf.paragraphs[0].font.size = Pt(32)
    title_tf.paragraphs[0].font.color.rgb = DESIGN.text_light
    title_tf.paragraphs[0].alignment = PP_ALIGN.LEFT

Closing Slide:

def add_closing_slide(
    prs: Presentation,
    title: str = "Thank You",
    contact_info: dict | None = None,
) -> None:
    """Add a closing slide with optional contact information."""
    slide = prs.slides.add_slide(prs.slide_layouts[6])  # Blank

    background = slide.background
    fill = background.fill
    fill.solid()
    fill.fore_color.rgb = DESIGN.primary

    # Main title
    title_box = slide.shapes.add_textbox(
        Inches(1.0), Inches(2.5), Inches(11.0), Inches(1.5),
    )
    tf = title_box.text_frame
    tf.paragraphs[0].text = title
    tf.paragraphs[0].font.size = Pt(36)
    tf.paragraphs[0].font.bold = True
    tf.paragraphs[0].font.color.rgb = DESIGN.text_light
    tf.paragraphs[0].alignment = PP_ALIGN.CENTER

    # Contact details
    if contact_info:
        info_box = slide.shapes.add_textbox(
            Inches(3.0), Inches(4.5), Inches(7.0), Inches(2.0),
        )
        info_tf = info_box.text_frame
        for key, value in contact_info.items():
            p = info_tf.add_paragraph()
            run = p.add_run()
            run.text = f"{key}: {value}"
            run.font.size = Pt(14)
            run.font.color.rgb = RGBColor(0xCC, 0xCC, 0xCC)
            p.alignment = PP_ALIGN.CENTER
            p.space_after = Pt(4)

Step 5: Charts and Data Visualization

Both python-pptx and PptxGenJS support native OOXML chart generation. Charts are embedded directly in the PPTX file as editable objects that PowerPoint can re-render.

Bar Chart (python-pptx):

from pptx.chart.data import CategoryChartData
from pptx.enum.chart import XL_CHART_TYPE, XL_LEGEND_POSITION
from pptx.util import Inches, Pt
from pptx.dml.color import RGBColor

def add_bar_chart_slide(
    prs: Presentation,
    title: str,
    categories: list[str],
    series_data: dict[str, list[float]],
    chart_title: str = "",
) -> None:
    """Add a slide with a clustered bar chart.

    Args:
        prs: Target presentation.
        title: Slide title.
        categories: X-axis category labels.
        series_data: Mapping of series name to list of values.
        chart_title: Optional chart title displayed above the chart area.
    """
    slide = prs.slides.add_slide(prs.slide_layouts[5])  # Title Only
    slide.placeholders[0].text = title

    chart_data = CategoryChartData()
    chart_data.categories = categories
    for series_name, values in series_data.items():
        chart_data.add_series(series_name, values)

    chart_shape = slide.shapes.add_chart(
        XL_CHART_TYPE.COLUMN_CLUSTERED,
        Inches(1.0), Inches(1.5),
        Inches(11.0), Inches(5.5),
        chart_data,
    )
    chart = chart_shape.chart

    # Configure chart appearance
    chart.has_legend = True
    chart.legend.position = XL_LEGEND_POSITION.BOTTOM
    chart.legend.include_in_layout = False

    if chart_title:
        chart.has_title = True
        chart.chart_title.text_frame.paragraphs[0].text = chart_title
        chart.chart_title.text_frame.paragraphs[0].font.size = Pt(14)

    # Style the value axis
    value_axis = chart.value_axis
    value_axis.has_title = False
    value_axis.major_gridlines.format.line.color.rgb = RGBColor(0xDD, 0xDD, 0xDD)

    # Style the category axis
    category_axis = chart.category_axis
    category_axis.has_major_gridlines = False
    category_axis.tick_labels.font.size = Pt(10)

    # Apply colors to each series
    colors = ["2E4A7A", "5B8DEF", "E86C00", "2ECC71", "E74C3C"]
    plot = chart.plots[0]
    for idx, series in enumerate(plot.series):
        fill = series.format.fill
        fill.solid()
        fill.fore_color.rgb = RGBColor.from_string(colors[idx % len(colors)])

Line Chart (python-pptx):

from pptx.chart.data import CategoryChartData
from pptx.enum.chart import XL_CHART_TYPE

def add_line_chart_slide(
    prs: Presentation,
    title: str,
    categories: list[str],
    series_data: dict[str, list[float]],
    smooth_lines: bool = False,
) -> None:
    """Add a slide with a line chart, optionally with smoothed lines."""
    slide = prs.slides.add_slide(prs.slide_layouts[5])
    slide.placeholders[0].text = title

    chart_data = CategoryChartData()
    chart_data.categories = categories
    for series_name, values in series_data.items():
        chart_data.add_series(series_name, values)

    chart_type = (
        XL_CHART_TYPE.LINE_MARKERS_STACKED
        if not smooth_lines
        else XL_CHART_TYPE.LINE
    )

    chart_shape = slide.shapes.add_chart(
        chart_type,
        Inches(1.0), Inches(1.5),
        Inches(11.0), Inches(5.5),
        chart_data,
    )
    chart = chart_shape.chart
    chart.has_legend = True
    chart.legend.position = XL_LEGEND_POSITION.BOTTOM

    # Configure smooth lines if requested
    if smooth_lines:
        for series in chart.series:
            series.smooth = True

Pie Chart (python-pptx):

from pptx.chart.data import CategoryChartData
from pptx.enum.chart import XL_CHART_TYPE, XL_LABEL_POSITION

def add_pie_chart_slide(
    prs: Presentation,
    title: str,
    categories: list[str],
    values: list[float],
    show_percentages: bool = True,
) -> None:
    """Add a slide with a pie chart showing category distribution."""
    slide = prs.slides.add_slide(prs.slide_layouts[5])
    slide.placeholders[0].text = title

    chart_data = CategoryChartData()
    chart_data.categories = categories
    chart_data.add_series("Values", values)

    chart_shape = slide.shapes.add_chart(
        XL_CHART_TYPE.PIE,
        Inches(2.5), Inches(1.5),
        Inches(8.0), Inches(5.5),
        chart_data,
    )
    chart = chart_shape.chart
    chart.has_legend = True
    chart.legend.position = XL_LEGEND_POSITION.RIGHT

    # Configure data labels
    plot = chart.plots[0]
    data_labels = plot.data_labels
    data_labels.show_category_name = True
    data_labels.show_percentage = show_percentages
    data_labels.show_value = not show_percentages
    data_labels.font.size = Pt(10)
    data_labels.number_format = "0.0%" if show_percentages else "0"

Charts in PptxGenJS:

function addBarChartSlide(
  pptx: PptxGenJS,
  title: string,
  chartData: { name: string; labels: string[]; values: number[] }[],
): void {
  const slide = pptx.addSlide({ masterName: "CONTENT_SLIDE" });
  slide.addText(title, { placeholder: "title" });

  slide.addChart(pptx.ChartType.bar, chartData, {
    x: 0.75,
    y: 1.2,
    w: 11.5,
    h: 5.5,
    showLegend: true,
    legendPos: "b",
    showValue: false,
    catAxisOrientation: "minMax",
    valAxisOrientation: "minMax",
    chartColors: ["2E4A7A", "5B8DEF", "E86C00", "2ECC71"],
    valGridLine: { color: "DDDDDD", size: 0.5 },
  });
}

function addLineChartSlide(
  pptx: PptxGenJS,
  title: string,
  chartData: { name: string; labels: string[]; values: number[] }[],
): void {
  const slide = pptx.addSlide({ masterName: "CONTENT_SLIDE" });
  slide.addText(title, { placeholder: "title" });

  slide.addChart(pptx.ChartType.line, chartData, {
    x: 0.75,
    y: 1.2,
    w: 11.5,
    h: 5.5,
    showLegend: true,
    legendPos: "b",
    lineSmooth: true,
    lineSize: 2,
    showMarker: true,
    chartColors: ["2E4A7A", "5B8DEF", "E86C00"],
  });
}

function addPieChartSlide(
  pptx: PptxGenJS,
  title: string,
  chartData: { name: string; labels: string[]; values: number[] }[],
): void {
  const slide = pptx.addSlide({ masterName: "CONTENT_SLIDE" });
  slide.addText(title, { placeholder: "title" });

  slide.addChart(pptx.ChartType.pie, chartData, {
    x: 2.0,
    y: 1.2,
    w: 9.0,
    h: 5.5,
    showLegend: true,
    legendPos: "r",
    showPercent: true,
    showTitle: false,
    chartColors: ["2E4A7A", "5B8DEF", "E86C00", "2ECC71", "E74C3C", "9B59B6"],
  });
}

Scatter Chart (python-pptx):

from pptx.chart.data import XyChartData
from pptx.enum.chart import XL_CHART_TYPE

def add_scatter_chart_slide(
    prs: Presentation,
    title: str,
    series_data: dict[str, list[tuple[float, float]]],
) -> None:
    """Add a slide with a scatter (XY) chart.

    Args:
        series_data: Mapping of series name to list of (x, y) tuples.
    """
    slide = prs.slides.add_slide(prs.slide_layouts[5])
    slide.placeholders[0].text = title

    chart_data = XyChartData()
    for series_name, points in series_data.items():
        series = chart_data.add_series(series_name)
        for x, y in points:
            series.add_data_point(x, y)

    chart_shape = slide.shapes.add_chart(
        XL_CHART_TYPE.XY_SCATTER,
        Inches(1.0), Inches(1.5),
        Inches(11.0), Inches(5.5),
        chart_data,
    )
    chart = chart_shape.chart
    chart.has_legend = True
    chart.legend.position = XL_LEGEND_POSITION.BOTTOM

Step 6: Advanced Features

Master Slide Templates with python-pptx:

from pptx import Presentation
from pptx.util import Inches, Pt, Emu
from pptx.oxml.ns import qn
from pptx.dml.color import RGBColor
import copy


def create_branded_presentation(
    company_name: str,
    primary_color: str = "2E4A7A",
    logo_path: str | None = None,
) -> Presentation:
    """Create a presentation with custom branded master slides.

    Modifies the default slide master to apply corporate branding
    including colors, fonts, and an optional logo.
    """
    prs = Presentation()
    prs.slide_width = Inches(13.333)
    prs.slide_height = Inches(7.5)

    # Access the slide master
    slide_master = prs.slide_masters[0]

    # Set the background color of the title layout
    title_layout = prs.slide_layouts[0]
    background = title_layout.background
    fill = background.fill
    fill.solid()
    fill.fore_color.rgb = RGBColor.from_string(primary_color)

    return prs

Speaker Notes:

def add_slide_with_notes(
    prs: Presentation,
    title: str,
    content: str,
    speaker_notes: str,
) -> None:
    """Add a content slide with speaker notes for the presenter."""
    slide = prs.slides.add_slide(prs.slide_layouts[1])
    slide.placeholders[0].text = title
    slide.placeholders[1].text = content

    # Add speaker notes
    notes_slide = slide.notes_slide
    notes_text_frame = notes_slide.notes_text_frame
    notes_text_frame.text = speaker_notes

Adding Logos to Every Slide:

from pptx.util import Inches

def add_logo_to_all_slides(
    prs: Presentation,
    logo_path: str,
    width: float = 1.2,
    position: str = "bottom-right",
) -> None:
    """Add a logo image to every slide in the presentation.

    Position options: "top-left", "top-right", "bottom-left", "bottom-right".
    """
    slide_width = prs.slide_width
    slide_height = prs.slide_height
    logo_width = Inches(width)

    positions = {
        "top-left": (Inches(0.3), Inches(0.2)),
        "top-right": (slide_width - logo_width - Inches(0.3), Inches(0.2)),
        "bottom-left": (Inches(0.3), slide_height - Inches(0.8)),
        "bottom-right": (slide_width - logo_width - Inches(0.3), slide_height - Inches(0.8)),
    }
    left, top = positions.get(position, positions["bottom-right"])

    for slide in prs.slides:
        slide.shapes.add_picture(logo_path, left, top, width=logo_width)

Hyperlinks:

from pptx.util import Inches, Pt
from pptx.oxml.ns import qn
from pptx.oxml import parse_xml

def add_hyperlink_text(
    prs: Presentation,
    title: str,
    links: list[dict],
) -> None:
    """Add a slide with clickable hyperlinks.

    Each link: {"text": str, "url": str, "description": str}
    """
    slide = prs.slides.add_slide(prs.slide_layouts[5])
    slide.placeholders[0].text = title

    text_box = slide.shapes.add_textbox(
        Inches(1.0), Inches(2.0), Inches(11.0), Inches(4.5),
    )
    tf = text_box.text_frame
    tf.word_wrap = True

    for idx, link_info in enumerate(links):
        if idx == 0:
            p = tf.paragraphs[0]
        else:
            p = tf.add_paragraph()

        run = p.add_run()
        run.text = link_info["text"]
        run.font.size = Pt(16)
        run.font.color.rgb = DESIGN.secondary
        run.font.underline = True

        # Set the hyperlink via the OOXML run element
        r_element = run._r
        hlinkClick = parse_xml(
            f'<a:hlinkClick xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main"'
            f' xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships"/>'
        )
        r_props = r_element.get_or_add_rPr()
        r_props.append(hlinkClick)

        # Add the relationship
        rel = slide.part.relate_to(
            link_info["url"],
            "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink",
            is_external=True,
        )
        hlinkClick.set(qn("r:id"), rel.rId)

        if link_info.get("description"):
            desc_p = tf.add_paragraph()
            desc_p.text = f"  {link_info['description']}"
            desc_p.font.size = Pt(12)
            desc_p.font.color.rgb = DESIGN.text_dark
            desc_p.space_after = Pt(12)

Animations and Transitions (PptxGenJS):

function addAnimatedSlide(pptx: PptxGenJS, title: string): void {
  const slide = pptx.addSlide({ masterName: "CONTENT_SLIDE" });
  slide.addText(title, { placeholder: "title" });

  // Add slide transition
  slide.transition = {
    type: "fade",
    speed: 1.0,  // seconds
  };

  // Animate text elements
  const points = ["First point", "Second point", "Third point"];
  points.forEach((point, idx) => {
    slide.addText(point, {
      x: 1.0,
      y: 1.5 + idx * 0.8,
      w: 11.0,
      h: 0.6,
      fontSize: 18,
      color: "333333",
      bullet: { type: "bullet" },
    });
  });

  // Note: PptxGenJS supports slide transitions but has limited
  // shape-level animation support. For complex animations,
  // use a template-based approach (see Step 7).
}

Step 7: Template-Based Generation

Template-based generation loads an existing .pptx file (designed in PowerPoint or Google Slides) and populates it with dynamic data. This approach separates design from code and is ideal for branded report generation.

Loading and Inspecting a Template (python-pptx):

from pptx import Presentation
from pptx.util import Inches


def inspect_template(template_path: str) -> dict:
    """Inspect a template to discover its layouts and placeholders.

    Run this first when working with a new template to understand
    which placeholder indices and names are available.
    """
    prs = Presentation(template_path)
    template_info = {
        "slide_width": str(prs.slide_width),
        "slide_height": str(prs.slide_height),
        "slide_count": len(prs.slides),
        "layouts": [],
    }

    for layout_idx, layout in enumerate(prs.slide_layouts):
        layout_info = {
            "index": layout_idx,
            "name": layout.name,
            "placeholders": [],
        }
        for ph in layout.placeholders:
            layout_info["placeholders"].append({
                "idx": ph.placeholder_format.idx,
                "name": ph.name,
                "type": str(ph.placeholder_format.type),
                "left": str(ph.left),
                "top": str(ph.top),
                "width": str(ph.width),
                "height": str(ph.height),
            })
        template_info["layouts"].append(layout_info)

    return template_info


def load_template(template_path: str) -> Presentation:
    """Load an existing .pptx template for population."""
    return Presentation(template_path)

Populating Template Placeholders:

from pptx import Presentation
from pptx.util import Pt


def populate_template_slide(
    prs: Presentation,
    layout_index: int,
    placeholder_data: dict[int, str],
) -> None:
    """Add a new slide from a template layout and fill its placeholders.

    Args:
        prs: Presentation loaded from template.
        layout_index: Index of the slide layout to use.
        placeholder_data: Mapping of placeholder index to text content.
    """
    layout = prs.slide_layouts[layout_index]
    slide = prs.slides.add_slide(layout)

    for ph_idx, text in placeholder_data.items():
        if ph_idx in [ph.placeholder_format.idx for ph in slide.placeholders]:
            slide.placeholders[ph_idx].text = text


def populate_with_formatting(
    prs: Presentation,
    layout_index: int,
    placeholder_content: dict[int, list[dict]],
) -> None:
    """Populate placeholders with formatted text runs.

    Each entry in placeholder_content maps a placeholder index to a list of
    run descriptors: {"text": str, "bold": bool, "size": int, "color": str}.
    """
    layout = prs.slide_layouts[layout_index]
    slide = prs.slides.add_slide(layout)

    for ph_idx, runs in placeholder_content.items():
        if ph_idx not in [ph.placeholder_format.idx for ph in slide.placeholders]:
            continue

        text_frame = slide.placeholders[ph_idx].text_frame
        text_frame.clear()

        for run_idx, run_data in enumerate(runs):
            if run_idx == 0:
                paragraph = text_frame.paragraphs[0]
            else:
                paragraph = text_frame.add_paragraph()

            run = paragraph.add_run()
            run.text = run_data["text"]
            run.font.size = Pt(run_data.get("size", 14))
            run.font.bold = run_data.get("bold", False)
            if "color" in run_data:
                from pptx.dml.color import RGBColor
                run.font.color.rgb = RGBColor.from_string(run_data["color"])

Batch Generation from Data (Mail Merge Pattern):

from pathlib import Path
from pptx import Presentation
import json


def batch_generate_decks(
    template_path: str,
    data_records: list[dict],
    output_dir: str,
    filename_field: str = "name",
) -> list[Path]:
    """Generate one presentation per data record using a shared template.

    This implements a mail merge pattern where each record produces
    a complete deck with its own data.

    Args:
        template_path: Path to the .pptx template.
        data_records: List of dicts, each containing fields for one deck.
        output_dir: Directory to write generated files.
        filename_field: Key in each record to use for the output filename.

    Returns:
        List of paths to generated files.
    """
    output = Path(output_dir)
    output.mkdir(parents=True, exist_ok=True)
    generated_files: list[Path] = []

    for record in data_records:
        prs = Presentation(template_path)

        # Remove template example slides (keep only layouts)
        while len(prs.slides) > 0:
            rId = prs.slides._sldIdLst[0].get("r:id")
            prs.part.drop_rel(rId)
            del prs.slides._sldIdLst[0]

        # Build slides from record data
        _build_deck_from_record(prs, record)

        # Save with sanitized filename
        safe_name = "".join(
            c if c.isalnum() or c in "-_ " else "" for c in record.get(filename_field, "output")
        ).strip()
        file_path = output / f"{safe_name}.pptx"
        prs.save(str(file_path))
        generated_files.append(file_path)

    return generated_files


def _build_deck_from_record(prs: Presentation, record: dict) -> None:
    """Build slides for a single record. Customize per template structure."""
    # Title slide (layout 0)
    slide = prs.slides.add_slide(prs.slide_layouts[0])
    slide.placeholders[0].text = record.get("title", "Untitled")
    slide.placeholders[1].text = record.get("subtitle", "")

    # Content slides (layout 1)
    for section in record.get("sections", []):
        slide = prs.slides.add_slide(prs.slide_layouts[1])
        slide.placeholders[0].text = section.get("heading", "")
        slide.placeholders[1].text = section.get("body", "")

Data-Driven Generation from JSON:

from pathlib import Path
from pptx import Presentation
import json


def generate_from_json(
    json_path: str,
    template_path: str | None = None,
    output_path: str = "output.pptx",
) -> Path:
    """Generate a presentation from a JSON specification.

    JSON schema:
    {
      "metadata": {"title": str, "author": str},
      "theme": {"primary_color": str, "font": str},
      "slides": [
        {
          "type": "title|content|two_column|chart|table|image",
          "title": str,
          "content": {...type-specific fields...}
        }
      ]
    }
    """
    with open(json_path) as f:
        spec = json.load(f)

    prs = Presentation(template_path) if template_path else Presentation()
    prs.slide_width = Inches(13.333)
    prs.slide_height = Inches(7.5)

    for slide_spec in spec.get("slides", []):
        slide_type = slide_spec.get("type", "content")

        if slide_type == "title":
            add_title_slide(
                prs,
                slide_spec.get("title", ""),
                slide_spec.get("content", {}).get("subtitle", ""),
            )
        elif slide_type == "content":
            add_content_slide(
                prs,
                slide_spec.get("title", ""),
                slide_spec.get("content", {}).get("body", ""),
                slide_spec.get("content", {}).get("bullets", []),
            )
        elif slide_type == "two_column":
            content = slide_spec.get("content", {})
            add_two_column_slide(
                prs,
                slide_spec.get("title", ""),
                content.get("left_title", ""),
                content.get("left_points", []),
                content.get("right_title", ""),
                content.get("right_points", []),
            )
        elif slide_type == "chart":
            content = slide_spec.get("content", {})
            chart_type = content.get("chart_type", "bar")
            if chart_type == "bar":
                add_bar_chart_slide(
                    prs,
                    slide_spec.get("title", ""),
                    content.get("categories", []),
                    content.get("series", {}),
                )
            elif chart_type == "pie":
                add_pie_chart_slide(
                    prs,
                    slide_spec.get("title", ""),
                    content.get("categories", []),
                    content.get("values", []),
                )
        elif slide_type == "table":
            content = slide_spec.get("content", {})
            add_table_slide(
                prs,
                slide_spec.get("title", ""),
                content.get("headers", []),
                content.get("rows", []),
            )

    output = Path(output_path)
    prs.save(str(output))
    return output

Example JSON Specification:

{
  "metadata": {
    "title": "Q4 Performance Report",
    "author": "Analytics Team"
  },
  "slides": [
    {
      "type": "title",
      "title": "Q4 2025 Performance Report",
      "content": { "subtitle": "Analytics Team | January 2026" }
    },
    {
      "type": "content",
      "title": "Executive Summary",
      "content": {
        "bullets": [
          "Revenue grew 23% year-over-year to $4.2M",
          "Customer acquisition cost decreased by 15%",
          "Net promoter score improved from 42 to 58",
          "Three new enterprise clients onboarded"
        ]
      }
    },
    {
      "type": "chart",
      "title": "Revenue by Quarter",
      "content": {
        "chart_type": "bar",
        "categories": ["Q1", "Q2", "Q3", "Q4"],
        "series": {
          "2024": [2800000, 3100000, 3400000, 3600000],
          "2025": [3200000, 3500000, 3900000, 4200000]
        }
      }
    },
    {
      "type": "table",
      "title": "Regional Performance",
      "content": {
        "headers": ["Region", "Revenue", "Growth", "Clients"],
        "rows": [
          ["North America", "$2.1M", "+18%", "45"],
          ["Europe", "$1.2M", "+28%", "32"],
          ["Asia Pacific", "$0.9M", "+35%", "21"]
        ]
      }
    }
  ]
}

Step 8: Testing and Quality Assurance

Automated presentation generation requires validation at multiple levels: structural correctness, content accuracy, visual consistency, and file integrity.

Slide Count and Structure Verification:

from pptx import Presentation
from pathlib import Path


def verify_presentation_structure(
    pptx_path: str,
    expected_slide_count: int | None = None,
    expected_titles: list[str] | None = None,
) -> dict:
    """Verify the structural integrity of a generated presentation.

    Returns a dict with verification results and any issues found.
    """
    prs = Presentation(pptx_path)
    issues: list[str] = []
    results = {
        "file_path": pptx_path,
        "file_size_bytes": Path(pptx_path).stat().st_size,
        "slide_count": len(prs.slides),
        "issues": issues,
    }

    # Verify slide count
    if expected_slide_count is not None and len(prs.slides) != expected_slide_count:
        issues.append(
            f"Expected {expected_slide_count} slides, got {len(prs.slides)}"
        )

    # Verify slide titles
    actual_titles = []
    for idx, slide in enumerate(prs.slides):
        title_shape = slide.shapes.title
        title_text = title_shape.text if title_shape else ""
        actual_titles.append(title_text)

        if not title_text:
            issues.append(f"Slide {idx + 1} has no title")

    results["titles"] = actual_titles

    if expected_titles:
        for idx, expected in enumerate(expected_titles):
            if idx >= len(actual_titles):
                issues.append(f"Missing slide {idx + 1}: expected title '{expected}'")
            elif actual_titles[idx] != expected:
                issues.append(
                    f"Slide {idx + 1} title mismatch: "
                    f"expected '{expected}', got '{actual_titles[idx]}'"
                )

    results["valid"] = len(issues) == 0
    return results

Content Extraction for Assertions:

from pptx import Presentation
from pptx.enum.shapes import MSO_SHAPE_TYPE


def extract_all_text(pptx_path: str) -> list[dict]:
    """Extract all text content from every slide for validation.

    Returns a list of dicts, one per slide, with all text content.
    """
    prs = Presentation(pptx_path)
    slides_content = []

    for slide_idx, slide in enumerate(prs.slides):
        slide_data = {
            "slide_number": slide_idx + 1,
            "title": "",
            "text_blocks": [],
            "tables": [],
            "notes": "",
        }

        for shape in slide.shapes:
            if shape.has_text_frame:
                text = shape.text_frame.text.strip()
                if shape == slide.shapes.title:
                    slide_data["title"] = text
                elif text:
                    slide_data["text_blocks"].append(text)

            if shape.has_table:
                table_data = []
                for row in shape.table.rows:
                    row_data = [cell.text.strip() for cell in row.cells]
                    table_data.append(row_data)
                slide_data["tables"].append(table_data)

        # Extract speaker notes
        if slide.has_notes_slide:
            notes_frame = slide.notes_slide.notes_text_frame
            slide_data["notes"] = notes_frame.text.strip()

        slides_content.append(slide_data)

    return slides_content


def extract_images(pptx_path: str) -> list[dict]:
    """Extract image metadata from the presentation."""
    prs = Presentation(pptx_path)
    images = []

    for slide_idx, slide in enumerate(prs.slides):
        for shape in slide.shapes:
            if shape.shape_type == MSO_SHAPE_TYPE.PICTURE:
                images.append({
                    "slide_number": slide_idx + 1,
                    "name": shape.name,
                    "width": shape.width,
                    "height": shape.height,
                    "content_type": shape.image.content_type,
                    "size_bytes": len(shape.image.blob),
                })

    return images

Pytest Test Suite:

import pytest
from pathlib import Path
from pptx import Presentation

# Assume your generator module is importable
# from my_generator import generate_report_deck


@pytest.fixture
def sample_presentation(tmp_path: Path) -> Path:
    """Generate a sample presentation for testing."""
    prs = Presentation()
    prs.slide_width = Inches(13.333)
    prs.slide_height = Inches(7.5)

    # Add test slides
    add_title_slide(prs, "Test Report", "Generated by pytest")
    add_content_slide(prs, "Summary", "", ["Point 1", "Point 2"])
    add_table_slide(prs, "Data", ["Col A", "Col B"], [["1", "2"], ["3", "4"]])

    output = tmp_path / "test_output.pptx"
    prs.save(str(output))
    return output


class TestPresentationStructure:
    """Tests for presentation structural correctness."""

    def test_slide_count(self, sample_presentation: Path) -> None:
        result = verify_presentation_structure(
            str(sample_presentation), expected_slide_count=3,
        )
        assert result["valid"], f"Issues: {result['issues']}"

    def test_slide_titles_present(self, sample_presentation: Path) -> None:
        result = verify_presentation_structure(str(sample_presentation))
        assert all(title for title in result["titles"]), "Some slides missing titles"

    def test_expected_titles(self, sample_presentation: Path) -> None:
        result = verify_presentation_structure(
            str(sample_presentation),
            expected_titles=["Test Report", "Summary", "Data"],
        )
        assert result["valid"], f"Title mismatches: {result['issues']}"


class TestContentExtraction:
    """Tests for content accuracy in generated slides."""

    def test_bullet_points_present(self, sample_presentation: Path) -> None:
        content = extract_all_text(str(sample_presentation))
        summary_slide = content[1]  # Second slide
        all_text = " ".join(summary_slide["text_blocks"])
        assert "Point 1" in all_text
        assert "Point 2" in all_text

    def test_table_data_correct(self, sample_presentation: Path) -> None:
        content = extract_all_text(str(sample_presentation))
        data_slide = content[2]  # Third slide
        assert len(data_slide["tables"]) == 1
        table = data_slide["tables"][0]
        assert table[0] == ["Col A", "Col B"]  # Header row
        assert table[1] == ["1", "2"]  # First data row


class TestFileIntegrity:
    """Tests for file-level quality checks."""

    def test_file_not_empty(self, sample_presentation: Path) -> None:
        assert sample_presentation.stat().st_size > 0

    def test_file_opens_without_error(self, sample_presentation: Path) -> None:
        prs = Presentation(str(sample_presentation))
        assert prs is not None

    def test_file_size_reasonable(self, sample_presentation: Path) -> None:
        size_mb = sample_presentation.stat().st_size / (1024 * 1024)
        assert size_mb < 50, f"File too large: {size_mb:.1f} MB"

    @pytest.mark.parametrize("max_size_mb", [10, 25, 50])
    def test_file_size_thresholds(
        self, sample_presentation: Path, max_size_mb: int,
    ) -> None:
        size_mb = sample_presentation.stat().st_size / (1024 * 1024)
        assert size_mb < max_size_mb

File Size Optimization:

from pptx import Presentation
from pathlib import Path
from PIL import Image
import io


def optimize_images_in_presentation(
    input_path: str,
    output_path: str,
    max_dimension: int = 1920,
    jpeg_quality: int = 85,
) -> dict:
    """Optimize images in a presentation to reduce file size.

    Resizes images larger than max_dimension and recompresses JPEGs.
    Returns optimization statistics.
    """
    prs = Presentation(input_path)
    original_size = Path(input_path).stat().st_size
    images_optimized = 0

    for slide in prs.slides:
        for shape in slide.shapes:
            if shape.shape_type != MSO_SHAPE_TYPE.PICTURE:
                continue

            image_blob = shape.image.blob
            content_type = shape.image.content_type

            if content_type not in ("image/jpeg", "image/png"):
                continue

            img = Image.open(io.BytesIO(image_blob))
            w, h = img.size

            # Skip if already small enough
            if max(w, h) <= max_dimension:
                continue

            # Resize maintaining aspect ratio
            if w > h:
                new_w = max_dimension
                new_h = int(h * max_dimension / w)
            else:
                new_h = max_dimension
                new_w = int(w * max_dimension / h)

            img = img.resize((new_w, new_h), Image.LANCZOS)

            # Recompress
            buffer = io.BytesIO()
            if content_type == "image/jpeg":
                img.save(buffer, format="JPEG", quality=jpeg_quality, optimize=True)
            else:
                img.save(buffer, format="PNG", optimize=True)

            # Replace the image blob in the presentation
            shape.image._blob = buffer.getvalue()
            images_optimized += 1

    prs.save(output_path)
    new_size = Path(output_path).stat().st_size

    return {
        "original_size_bytes": original_size,
        "optimized_size_bytes": new_size,
        "reduction_percent": round((1 - new_size / original_size) * 100, 1),
        "images_optimized": images_optimized,
    }

Visual Validation (Convert to Images for Comparison):

import subprocess
from pathlib import Path


def export_slides_as_images(
    pptx_path: str,
    output_dir: str,
    format: str = "png",
) -> list[Path]:
    """Export each slide as an image using LibreOffice for visual validation.

    Requires LibreOffice installed and accessible via command line.
    Useful for visual regression testing in CI pipelines.
    """
    output = Path(output_dir)
    output.mkdir(parents=True, exist_ok=True)

    # Convert PPTX to individual images via LibreOffice
    result = subprocess.run(
        [
            "libreoffice",
            "--headless",
            "--convert-to", format,
            "--outdir", str(output),
            pptx_path,
        ],
        capture_output=True,
        text=True,
        timeout=120,
    )

    if result.returncode != 0:
        raise RuntimeError(
            f"LibreOffice conversion failed: {result.stderr}"
        )

    # Collect generated images
    return sorted(output.glob(f"*.{format}"))

Key Quality Assurance Principles:

  • Always verify slide count matches the expected number of data items
  • Extract and assert text content rather than relying on visual inspection
  • Test table dimensions (row count, column count) match input data
  • Verify chart data by re-reading the chart XML when possible
  • Keep file sizes under control by optimizing images before embedding
  • Use LibreOffice headless conversion for visual regression testing in CI
  • Test with both minimal and maximal data to catch overflow and layout issues
  • Validate that speaker notes are populated when expected
  • Check that hyperlinks resolve to valid URLs
  • Run generation tests with tmp_path fixtures to avoid polluting the working directory

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.