Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions openspec/changes/che-pptx-geometry-tools/.openspec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
schema: spec-driven
created: 2026-05-02
created_by: che cheng <kiki830621@gmail.com>
created_with: claude
62 changes: 62 additions & 0 deletions openspec/changes/che-pptx-geometry-tools/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
## Context

`pptx-swift` already models EMU-based positions and sizes for shapes and pictures, and `che-pptx-mcp` exposes lower-level tools such as `insert_image`, `move_shape`, and `resize_shape`. In real deck authoring, agents and users reason in centimetres and bounding boxes, not EMUs. The first #90 slice therefore targets metric geometry and picture placement while leaving text, templates, notes, export, scripts, and references for later issues.

The existing MCP server is session-first for mutating operations. This change keeps that model: geometry tools mutate an opened `doc_id`, mark the session dirty, and rely on `save_presentation` for persistence.

## Goals / Non-Goals

**Goals:**

- Add deterministic cm-to-EMU and EMU-to-cm helpers in `pptx-swift`.
- Add library helpers that update existing shape/picture geometry in centimetres.
- Add image fit math for `contain`, `cover`, and `stretch` inside a target cm box.
- Add MCP tools for placeholder geometry, picture placement, and native-aspect placement.
- Verify saved PPTX output through model readback and math-level tests.

**Non-Goals:**

- Add rich text/rich bullet tools.
- Add slide master copying, template deletion, or layout cloning.
- Add presenter notes export, PDF export, or PNG rendering.
- Add Swift scripts or `references/python-pptx`.
- Verify visual rendering in Keynote, PowerPoint, or LibreOffice.

## Decisions

### Add metric geometry primitives to pptx-swift

`pptx-swift` owns geometry primitives because the conversion constants and fit algorithms are presentation-model concerns, not MCP transport concerns. Add a small public surface such as `MetricGeometry`, `GeometryFitMode`, `ImagePlacement`, and conversion helpers that use 360000 EMU per centimetre. Rounding SHALL be deterministic, with half-up rounding for centimetre-to-EMU conversions.

Alternative considered: perform cm conversion only inside `che-pptx-mcp`. Rejected because scripts and future non-MCP callers need the same calculations, and duplicating conversion constants invites drift.

### Keep MCP geometry tools session-only

The new tools mutate an opened presentation using `doc_id`. They do not accept `source_path` direct mode because direct mode is read-only in the current server contract. Each successful tool call marks the presentation dirty and returns the final EMU and cm geometry so callers can log exact placement.

Alternative considered: add direct-mode write tools with `source_path` and `output_path`. Rejected because that would introduce a second persistence model outside the existing open/save lifecycle.

### Resolve placeholders by slide index and placeholder identity

`set_placeholder_geometry` targets a slide by `slide_index` and identifies a placeholder by either shape id or placeholder type plus optional occurrence index. If both shape id and placeholder type are omitted, the tool returns a validation error. If placeholder type resolves to multiple shapes without an occurrence index, the tool returns an ambiguity error.

Alternative considered: target placeholders only by numeric shape id. Rejected because template-driven deck authoring usually starts from semantic placeholder roles such as title, body, and subtitle.

### Centralize image fit rectangle math

`place_picture_at` and `fit_picture_to_native_aspect` both call the same fit helper. `stretch` uses the target box exactly. `contain` preserves native aspect and fits entirely inside the target box. `cover` preserves native aspect and fills the target box, allowing crop metadata or an explicitly oversized picture rectangle depending on existing writer support. The initial implementation records whichever representation the writer can faithfully persist and documents unsupported crop output as an explicit error if necessary.

Alternative considered: implement `contain` first and silently map `cover` to `contain`. Rejected because silent remapping would mislead agents building pixel-sensitive medical or conference decks.

### Read image dimensions through native macOS image metadata

Use a macOS-native image metadata path such as CoreGraphics ImageIO to read width and height from PNG and JPEG files for `fit_picture_to_native_aspect`. The package already targets macOS, so this avoids adding a new third-party dependency. Unsupported image formats return a typed validation error before the presentation is mutated.

Alternative considered: require callers to pass `native_width` and `native_height`. Rejected because it pushes a common, deterministic operation onto every caller and recreates the Python-script burden #90 is trying to remove.

## Risks / Trade-offs

- [Risk] Existing writer support for crop metadata is incomplete. → Mitigation: `cover` either emits persisted crop data with readback tests or returns an unsupported-fit error; it never silently behaves like `contain`.
- [Risk] Image metadata APIs differ across file formats. → Mitigation: Phase 1 supports PNG and JPEG and returns a typed unsupported-format error for other formats.
- [Risk] Placeholder lookup by type can be ambiguous. → Mitigation: require occurrence index or shape id when more than one placeholder of the requested type exists on a slide.
- [Risk] Geometry math regressions are easy to miss visually. → Mitigation: add exact conversion tests, fit rectangle examples, and saved-readback tests for final EMU values.
48 changes: 48 additions & 0 deletions openspec/changes/che-pptx-geometry-tools/proposal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
## Why

Issue #90 identifies a real PPTX authoring bottleneck: geometry and image-placement edits currently require low-level EMU math and repeated Python rebuild/render loops. The first coherent slice is cm-based geometry and picture-placement tooling, because it directly targets the highest-friction workflow while keeping rich text, template copying, notes, export, scripts, and references out of this PR-sized change.

## What Changes

- Add library-level helpers in `pptx-swift` for metric geometry:
- Convert centimetres to EMU and EMU to centimetres with deterministic rounding.
- Set an existing shape or picture frame's position and size using centimetres.
- Compute contain, cover, and stretch placement rectangles for images inside a target box.
- Extend `che-pptx-mcp` with session-mode editing tools:
- `set_placeholder_geometry` for cm-based placement of existing placeholder shapes.
- `place_picture_at` for inserting a picture into a cm-based box with `contain`, `cover`, or `stretch` fit.
- `fit_picture_to_native_aspect` for inserting a picture scaled to native aspect within a maximum cm box and aligned left, center, or right.
- Add focused tests that verify EMU conversion, fit rectangle math, MCP schema, and saved PPTX readback.
- Document the new tools as the first #90 sub-issue cluster and leave later clusters for separate changes.

## Non-Goals

- No rich bullet or run-style tooling in this change.
- No slide master/template-copying work in this change.
- No notes, PDF export, or slide PNG rendering in this change.
- No Swift script examples or `references/python-pptx` clone in this change.
- No visual rendering guarantee from Keynote or PowerPoint; this change verifies geometry through OOXML readback and deterministic math.

## Capabilities

### New Capabilities

(none)

### Modified Capabilities

- `pptx-slide-write`: Add cm-based geometry mutation and deterministic image fit rectangle behaviour for shapes and pictures.
- `pptx-mcp-server`: Expose the new geometry/image placement helpers as MCP tools with clear input schema and session-mode persistence.

## Impact

- Affected specs: pptx-slide-write, pptx-mcp-server
- Affected code:
- Modified: packages/pptx-swift/Sources/PPTXSwift/Models/Shape.swift
- Modified: packages/pptx-swift/Sources/PPTXSwift/Models/Picture.swift
- Modified: packages/pptx-swift/Sources/PPTXSwift/Models/Slide.swift
- Modified: packages/pptx-swift/Tests/PPTXSwiftTests/PPTXSwiftTests.swift
- Modified: mcp/che-pptx-mcp/Sources/ChePPTXMCP/Server.swift
- New: mcp/che-pptx-mcp/Tests/ChePPTXMCPTests/
- Modified: README.md
- Related issue: #90
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
## ADDED Requirements

### Requirement: Placeholder geometry MCP tool

The MCP server SHALL expose a session-mode tool named `set_placeholder_geometry`. The tool SHALL require `doc_id`, `slide_index`, `left_cm`, `top_cm`, `width_cm`, and `height_cm`. The tool SHALL identify the target placeholder by either `shape_id` or `placeholder_type` plus optional `occurrence_index`. A successful call SHALL mutate the open presentation, mark it dirty, and return the final geometry in both centimetres and EMU.

#### Scenario: Set placeholder geometry by shape id

- **WHEN** the caller invokes `set_placeholder_geometry` with `doc_id: "deck"`, `slide_index: 0`, `shape_id: 5`, left 1.0 cm, top 2.0 cm, width 10.0 cm, and height 4.0 cm
- **THEN** the shape with id 5 is updated to the corresponding EMU geometry
- **AND** the open presentation is marked dirty
- **AND** the response includes the final cm and EMU geometry

#### Scenario: Ambiguous placeholder type fails

- **WHEN** a slide contains two body placeholders and the caller invokes `set_placeholder_geometry` with `placeholder_type: "body"` and no `occurrence_index`
- **THEN** the tool returns an ambiguity error and does not mutate the presentation

### Requirement: Place picture at MCP tool

The MCP server SHALL expose a session-mode tool named `place_picture_at`. The tool SHALL require `doc_id`, `slide_index`, `image_path`, `left_cm`, `top_cm`, `width_cm`, `height_cm`, and `fit`. The `fit` value SHALL be one of `contain`, `cover`, or `stretch`. A successful call SHALL insert the image as a picture, mark the presentation dirty, and return the created picture id plus final geometry.

#### Scenario: Insert contained picture through MCP

- **WHEN** the caller invokes `place_picture_at` with fit `contain` for an 800x600 image inside a 10 cm by 10 cm box
- **THEN** the tool inserts a picture whose persisted geometry preserves 4:3 aspect ratio
- **AND** the response includes the created picture id and final geometry

#### Scenario: Invalid fit value fails

- **WHEN** the caller invokes `place_picture_at` with fit `tile`
- **THEN** the tool returns a validation error and does not mutate the presentation

### Requirement: Fit picture to native aspect MCP tool

The MCP server SHALL expose a session-mode tool named `fit_picture_to_native_aspect`. The tool SHALL require `doc_id`, `slide_index`, `image_path`, `anchor`, `max_width_cm`, and `max_height_cm`. The `anchor` value SHALL be one of `left`, `center`, or `right`. The tool SHALL read native image dimensions, calculate a contained rectangle inside the maximum box, align it horizontally according to `anchor`, insert the picture, mark the presentation dirty, and return the created picture id plus final geometry.

#### Scenario: Fit native aspect centered

- **WHEN** the caller invokes `fit_picture_to_native_aspect` with a 1600x900 image, anchor `center`, max width 12 cm, and max height 8 cm
- **THEN** the inserted picture preserves 16:9 aspect ratio inside the maximum box
- **AND** the picture is horizontally centered within the maximum box

#### Scenario: Unsupported native image format fails

- **WHEN** the image metadata reader cannot determine width and height for `image_path`
- **THEN** the tool returns an unsupported image format error and does not mutate the presentation

### Requirement: Geometry tool persistence through save

Geometry and picture placement changes made through the new MCP tools SHALL persist after `save_presentation` and re-open through `pptx-swift` readback.

#### Scenario: Save and read back geometry update

- **WHEN** the caller opens a presentation, calls `set_placeholder_geometry`, saves it to `out.pptx`, and reads `out.pptx`
- **THEN** the updated placeholder geometry in the readback model matches the requested centimetre values converted to EMU

#### Scenario: Save and read back inserted picture

- **WHEN** the caller opens a presentation, calls `place_picture_at`, saves it to `out.pptx`, and reads `out.pptx`
- **THEN** the readback model contains the inserted picture with the expected final geometry
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
## ADDED Requirements

### Requirement: Metric geometry conversion

The system SHALL provide deterministic conversion helpers between centimetres and EMU for presentation geometry. The conversion SHALL use 360000 EMU per centimetre. Centimetre-to-EMU conversion SHALL round half-up to the nearest integer EMU. EMU-to-centimetre conversion SHALL return a Double value without truncating precision.

#### Scenario: Convert centimetres to EMU

- **WHEN** the caller converts 2.54 cm to EMU
- **THEN** the result is 914400 EMU

##### Example: conversion table

| Centimetres | Expected EMU |
| ----------- | ------------ |
| 0.0 | 0 |
| 1.0 | 360000 |
| 2.54 | 914400 |
| 10.0 | 3600000 |

#### Scenario: Convert EMU to centimetres

- **WHEN** the caller converts 914400 EMU to centimetres
- **THEN** the result is 2.54 cm within 0.0001 cm tolerance

### Requirement: Set shape geometry in centimetres

The system SHALL allow callers to set an existing shape or picture geometry using centimetre values for left, top, width, and height. The stored model SHALL update the underlying EMU position and size fields. Negative left, top, width, or height values SHALL fail validation before mutating the slide.

#### Scenario: Update shape geometry from centimetres

- **WHEN** a shape is updated to left 1.0 cm, top 2.0 cm, width 5.0 cm, and height 3.0 cm
- **THEN** the shape position is x 360000 EMU and y 720000 EMU
- **AND** the shape size is width 1800000 EMU and height 1080000 EMU

#### Scenario: Reject negative size

- **WHEN** the caller sets width to -1.0 cm
- **THEN** the system returns a validation error and leaves the shape geometry unchanged

### Requirement: Image fit rectangle calculation

The system SHALL calculate image placement rectangles for `contain`, `cover`, and `stretch` fit modes inside a target box. `contain` SHALL preserve image aspect ratio and fit entirely inside the target box. `cover` SHALL preserve image aspect ratio and fill the target box. `stretch` SHALL use the target box exactly and ignore native aspect ratio.

#### Scenario: Contain wide image in square box

- **WHEN** a 1600x900 image is placed with `contain` inside a 10 cm by 10 cm box
- **THEN** the calculated rectangle is 10 cm wide and 5.625 cm high
- **AND** the rectangle is vertically centered when center alignment is requested

#### Scenario: Cover wide image in square box

- **WHEN** a 1600x900 image is placed with `cover` inside a 10 cm by 10 cm box
- **THEN** the calculated rectangle fills at least 10 cm width and 10 cm height while preserving 16:9 aspect ratio

#### Scenario: Stretch image to target box

- **WHEN** a 1600x900 image is placed with `stretch` inside a 10 cm by 4 cm box
- **THEN** the calculated rectangle is exactly 10 cm wide and 4 cm high

### Requirement: Native image dimension reading

The system SHALL read native pixel dimensions for PNG and JPEG files used by picture placement helpers. Unsupported or unreadable image files SHALL fail validation before mutating the presentation.

#### Scenario: Read PNG dimensions

- **WHEN** the caller provides a PNG image file with native size 800x600 pixels
- **THEN** the system reports width 800 and height 600 for fit calculations

#### Scenario: Unsupported image format fails

- **WHEN** the caller provides an unsupported image format for native-aspect placement
- **THEN** the system returns an unsupported image format error before changing the slide

##### Example: SVG input is rejected

- **GIVEN** the caller provides `diagram.svg` to the native image dimension reader
- **WHEN** the reader attempts to determine pixel dimensions for picture placement
- **THEN** the system returns an unsupported image format error
- **AND** the slide model is unchanged

### Requirement: Insert picture with fit mode

The system SHALL insert a picture into a target centimetre box using the requested fit mode. The persisted picture geometry SHALL match the fit rectangle converted to EMU. If the writer cannot persist a requested cover/crop representation faithfully, the system SHALL return an unsupported fit error before mutating the presentation.

#### Scenario: Insert contained picture

- **WHEN** the caller inserts an 800x600 image into a 10 cm by 10 cm target box with `contain`
- **THEN** the persisted picture size is 10 cm by 7.5 cm converted to EMU

#### Scenario: Cover fails when unsupported

- **WHEN** the caller requests `cover` and the writer lacks faithful crop or oversized-picture persistence for the requested output
- **THEN** the system returns an unsupported fit error and leaves the slide unchanged
19 changes: 19 additions & 0 deletions openspec/changes/che-pptx-geometry-tools/tasks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
## 1. pptx-swift Geometry Foundation

- [ ] 1.1 Implement Metric geometry conversion and tests in packages/pptx-swift, following Add metric geometry primitives to pptx-swift with 360000 EMU per centimetre and half-up rounding.
- [ ] 1.2 Implement Set shape geometry in centimetres for Shape and Picture models, including negative-value validation and unchanged-model assertions on failure.
- [ ] 1.3 Implement Image fit rectangle calculation with contain, cover, and stretch examples, following Centralize image fit rectangle math.
- [ ] 1.4 Implement Native image dimension reading for PNG and JPEG through macOS image metadata, following Read image dimensions through native macOS image metadata.
- [ ] 1.5 Implement Insert picture with fit mode so persisted picture geometry matches the calculated fit rectangle or returns an unsupported-fit error before mutation.

## 2. che-pptx-mcp Tool Surface

- [ ] 2.1 Add Placeholder geometry MCP tool schema and handler for `set_placeholder_geometry`, following Keep MCP geometry tools session-only and Resolve placeholders by slide index and placeholder identity.
- [ ] 2.2 Add Place picture at MCP tool schema and handler for `place_picture_at`, validating fit values and returning created picture id plus final geometry.
- [ ] 2.3 Add Fit picture to native aspect MCP tool schema and handler for `fit_picture_to_native_aspect`, validating anchor values and using native image dimensions.
- [ ] 2.4 Add Geometry tool persistence through save tests that open, mutate, save, and read back PPTX geometry for placeholder and picture placement.

## 3. Documentation and Scope Guard

- [ ] 3.1 Update README.md or MCP tool documentation with cm-based examples for all three new tools and explicitly defer rich text, template copying, notes, export, scripts, and python-pptx references to later #90 slices.
- [ ] 3.2 Run package-level tests for packages/pptx-swift and mcp/che-pptx-mcp, plus Spectra validation for che-pptx-geometry-tools.
Loading