How to Use IronPPT with GitHub Copilot

Curtis Chau
Curtis Chau
Updated: 2026年10月19日

A real-estate agency sends buyers' agents a deck of its open homes every Friday. Each property listing receives one slide with up to three photos, and the photos arrive from phones and cameras in different sizes, shapes and resolutions. A .NET service in the agency's repository builds the deck, so any assistant that changes that service must place every photo inside its frame without distorting it.

GitHub Copilot reaches the repository through chat and agent mode in Visual Studio and VS Code, through Copilot CLI in a terminal, and through a cloud agent on GitHub that turns an assigned issue into a pull request. IronPPT creates and edits PowerPoint files in .NET without Microsoft Office. AddImage places a picture on a slide, and the Image class sizes, replaces and removes it. Instruction files committed to the repository give each of those surfaces the IronPPT rules.

This guide covers directing Copilot to build software that places photos with IronPPT. Taking the photos and designing the slide template are separate tasks that happen outside the repository. The manage image guide describes the IronPPT calls that the listing deck depends on.

Quickstart
NuGetNuGetでインストール

PM > Install-Package IronPPT

Install IronPPT by running the command above in the NuGet Package Manager Console, or search for the package in the NuGet Package Manager.

Open a terminal at the repository root, start Copilot CLI and submit the prompt. Copilot CLI attaches a file with @, while Copilot Chat in Visual Studio uses # references.

Fill in @src/Listings.Deck/ListingDeck.cs so the agency can build its weekly open-homes deck.
The repo already pins IronPPT 2026.10.1 in Directory.Packages.props.

Here is what the deck code needs:
- One public static method, Build(string listingsPath, string photosDir, string outPath).
- Listings come from @data/listings.json.
- Each listing's photos are in photos/<listing id>/, under the file names the JSON gives.
- The cover and every listing slide follow @docs/listing-layout.md.

Then do these in order:
1. Add a build command with an --out <path> option to @src/Listings.Cli/Program.cs.
2. Build the solution.
3. From the repo root, run the CLI with build --out out/listings.pptx.

Tell me how many slides the deck has and how large the file is.
Text

The prompt names no box or coordinate, because docs/listing-layout.md defines the photo boxes in points and the order of the text lines.

Set IRONPPT_LICENSE_KEY in the environment where Copilot runs. IronPPT does not read that name by itself. In our test, License.IsLicensed remained false when only that variable was set. IronPPT did read variables named IronPPT.LicenseKey and IRONPPT_LICENSEKEY on its own, but the sample repository copies the conventional name into License.LicenseKey. The license key guide lists the other supported locations.



How do you set IronPPT conventions for Copilot?

Place the project-wide rules in .github/copilot-instructions.md. GitHub's support reference lists that file for Copilot Chat in Visual Studio, VS Code and the JetBrains IDEs, and for the cloud agent and Copilot code review. Copilot CLI reads it from the same location. Visual Studio loads it while its custom instructions option is selected, and that option is on by default.

This repo builds the weekly open-homes deck with IronPPT. These rules apply to all of it:
- Use IronPPT, the NuGet package and namespace. Don't add another presentation or Open XML package.
- Edit the IronPPT entry in Directory.Packages.props only when the task is an IronPPT upgrade.
- Give other packages their versions in Directory.Packages.props as well.
- The code reads IRONPPT_LICENSE_KEY itself, because IronPPT doesn't look at that name.
- Copy the key into License.LicenseKey before the code saves a deck.
- Never print the key, and never list environment variables.
- docs/listing-layout.md is the contract.
- If the layout doc looks wrong, say so in your reply and leave the file as it is.
- The IronPPT docs index for agents is https://ironsoftware.com/ppt/csharp/llms.txt

Tests follow two more rules:
- A test saves a deck, reopens it and checks its pictures and text.
- Never compare the bytes of two decks.
Markdown

IronPPT names each picture with a random number, such as Picture 86115, so two builds from the same listings differed byte for byte. The tests compare pictures and text for that reason.

How do you give the deck code its own rules?

A file such as .github/instructions/deck.instructions.md holds rules for the parts of the tree that its applyTo patterns name, here the source and test projects. According to GitHub's documentation, Copilot CLI includes the file only when it works on a file that matches one of those patterns. In the test run, Copilot opened this file before any other file in the first prompt and in the issue's separate session.

---
applyTo: "src/**,tests/**"
---

These rules cover IronPPT pictures and slides in the deck code and its tests:
- Every new PresentationDocument already has a slide. Make the cover from that slide.
- Don't add another slide in front of the cover.
- Give every picture an explicit Width, Height and Position, in points.
- Without them, IronPPT sizes a photo from its pixels and DPI.
- At 72 dpi, a 4032 x 3024 photo comes out 56 x 42 inches.
- The default slide is 10 x 7.5 inches.
- Setting Width leaves Height as it was, and the reverse.
- Work out both from the photo's pixel size to keep its aspect ratio.
- On a deck loaded from a file, call Replace on document.Images, not on a slide's Images list.
- Replace resets the picture to the new photo's own size.
- So set Width, Height and Position again after each Replace.
- Width, Height and Position read from a reopened deck are 12,700 times the value in points.
- AddImage with an AnyBitmap stores the picture as an uncompressed BMP.
- To store a resized photo as a JPEG, encode it first and pass the stream.
Markdown

Each rule in that file comes from a probe of IronPPT 2026.10.1.

How large does IronPPT make a listing photo?

AddImage without a size gives the picture a frame of the photo's pixel count divided by its DPI, in inches, and writes no position for it. The default slide is 10 by 7.5 inches, which is 720 by 540 points.

  • A 4032 by 3024 phone photo saved at 72 dpi received a frame of 56 by 42 inches.
  • The same 2048 by 1536 photo saved at 300, 72 and 96 dpi received frames 6.8, 28.4 and 21.3 inches wide. A PNG with no DPI was sized as 96 dpi.
  • A JPEG with a 1 by 1 density and no unit in its header was sized as 1 dpi, so a 4032-pixel photo became 4032 inches wide.
  • Width, Height and Position take points. Setting Width to 400 alone left Height unchanged, so the frame lost the photo's aspect ratio.

The fit in docs/listing-layout.md therefore starts from the photo's pixel size. The scale factor is the smaller of the box width divided by the photo width and the box height divided by the photo height, and the scaled frame is centered in the box.

How do you swap a photo in a saved deck?

Swap the hero photo of L-1044 in out/listings.pptx for photos/reshoot/L-1044-front.jpg.
The photographer reshot the front, and the new photo is portrait.
Change the saved deck itself. Don't rebuild it from the JSON.

Add the swap to the CLI as a swap command with these options:
- --deck <path> names the deck to change.
- --listing <id> and --photo <n> pick the picture, counting photos from 1.
- --file <path> is the new photo.

The new photo goes in the hero box and follows the fit rules in docs/listing-layout.md.
Leave every other picture and all the text as they are.

Run the command, reopen the deck and show me the hero's position and size in points.
Text

Replace gave three different results in the probes.

  • On a deck built in memory, Replace swapped the photo and reset the frame to the new photo's own size, with no position.
  • On a loaded deck, Replace called through a slide's Images list did not change the requested picture in any probe. A slide with one picture kept the old photo and stored the new photo in the file unused. On a slide with two pictures, the call on the first picture swapped the second.
  • Through document.Images, Replace on a loaded deck swapped the right picture. Setting Width, Height and Position after the call gave the frame the right size and place.

The manage image guide's replace sample calls Replace through Slides[0].Images[0] on a loaded deck, which is the route that left the old photo in place.

Output

The test run saved the slide for 220 Mill Road before and after the swap.

Listing slide for 220 Mill Road before the swap, with a landscape hero photo and one thumbnail

Slide 4 as the first prompt saved it, with each picture drawn at its stored position and size. Text is not drawn, and the photos are generated test images.

Listing slide for 220 Mill Road after the swap, with the portrait reshoot centered in the hero box

Slide 4 after the swap command, drawn the same way.

How do you test the deck?

Use Shift+Tab to cycle Copilot CLI to plan mode, then submit this prompt.

Plan an xUnit project at tests/Listings.Deck.Tests, and put it in the solution.
Show me the plan first, and hold the code until I say go.

Each test builds a deck from data/listings.json in a temp folder and checks one of these:
- The deck has a cover plus one slide per listing, in the order of the JSON.
- Each listing slide holds one picture per photo, in the box the layout doc gives that photo.
- Every picture keeps its photo's aspect ratio within 0.5% and stays inside its box.
- The address, the price and the bed and bath line are on the right slide.
- The swap command changes the chosen picture and no other.

Once I say go, write the tests and keep running dotnet test until they pass.
Then tell me how many tests passed.
Text

A test of the boxes has to read sizes that IronPPT reports wrongly. Width, Height and Position read from a reopened deck return 12,700 times their value in points, which is the value in EMUs, the unit the PPTX file stores. A 480-point frame read back as 6,096,000. The tests from the run divided the read-back values by 12,700 before comparing them with the layout.

Why does marking a listing sold leave photos behind?

In this scenario, a teammate's SoldUpdate.cs removes a sold listing's photos in a loop.

Find out why marking a listing sold leaves photos behind, then fix it.
@src/Listings.Deck/SoldUpdate.cs edits a saved deck.
It should strip the listing's photos from its slide and add a SOLD line.

Work through these steps before you touch SoldUpdate.cs:
1. Write a test that builds a deck, marks L-1042 sold and counts the pictures left on its slide.
2. Read how https://ironsoftware.com/ppt/csharp/how-to/manage-image/ removes all images.
3. Tell me what Slide.Images returns in IronPPT that explains the leftover photos.

The fix has to meet these conditions:
- It changes SoldUpdate.cs and no other source file.
- The new test expects no pictures on the slide.
- dotnet test passes.

Last, mark L-1042 sold in a copy of out/listings.pptx.
Tell me the copy's size before and after.
Text

Slide.Images builds a new list on every access. A loop that reads slide.Images again on each pass, with an index that counts up, skips a picture after every removal. On a slide with four pictures it removed two, and on a listing slide with three it removed two and left one. A loop that counts down, as the manage image guide's remove-all sample does, removed every picture, and so did a foreach over one copy of the list.

Removal also leaves the photo's bytes in the file. Removing four pictures from a 13,766,065-byte deck saved a file of 13,765,502 bytes.

What makes an issue the cloud agent can act on?

When an issue is assigned to Copilot, GitHub passes the agent the issue's title, its description, the comments that exist at that moment and any instructions given at assignment. Comments added later do not reach the agent, so the issue body must contain the whole request.

Title: Shrink the weekly deck so it can be emailed

Files: src/Listings.Deck/ListingDeck.cs and tests/Listings.Deck.Tests
Versions: IronPPT 2026.10.1 on net9.0

Expected: out/listings.pptx is under 4 MB, and the deck code works like this:
- IronPPT gets a scaled copy of any photo wider or taller than 1600 pixels.
- The copy is 1600 pixels on its long side and is stored as a JPEG at quality 85.
- Every picture keeps the box, position and size it has now.

Actual: IronPPT stores each photo file as it is, so the deck is over 15 MB.

Constraints: the change stays inside these limits:
- Keep the Build signature.
- Follow .github/instructions/deck.instructions.md.
- Add no packages.
- Leave the files in photos/ untouched.

Acceptance: the issue is done when all of this holds:
- The existing tests pass.
- A new test builds the deck and opens each photo stored under ppt/media in the saved file.
- That test fails if any stored photo is over the 1600-pixel limit.

Summary: give the deck's size before and after the change.
Markdown

IronPPT copies each photo file into the deck byte for byte, and a photo added three times is stored three times. A scaled photo passed to AddImage as an AnyBitmap was stored as an uncompressed BMP of 7,680,122 bytes for 1600 by 1200 pixels. The same pixels encoded as a JPEG at quality 85 and passed as a stream were stored as 337,813 bytes.

Why do the cloud agent's tests fail on an IronPPT project?

The cloud agent runs on Ubuntu Linux unless the repository chooses otherwise. IronPPT targets .NET Standard 2.0, and none of the packages it restores contains a native file, so the setup steps install nothing beyond the .NET SDK.

The runner also has no license key. Without one, IronPPT built decks in memory and read saved decks, but Save threw a licensing exception and wrote no file. Each test that saves a deck therefore fails on the runner.

GitHub runs the copilot-setup-steps job in .github/workflows/copilot-setup-steps.yml before the agent starts, and only when the file is on the default branch. When a setup step fails, Copilot skips the steps after it.

Add a workflow at .github/workflows/copilot-setup-steps.yml to prepare the cloud agent's machine.
GitHub reads only a job named copilot-setup-steps from that file.

Set the job up this way:
- It runs on ubuntu-latest with contents: read as its one permission.
- It triggers on workflow_dispatch and on any pull request that edits this file.

Give it these steps, in order:
1. Check out the repo.
2. Set up .NET 9.
3. Run dotnet restore.
4. Run dotnet build -c Release.
5. Check for the license key.

IRONPPT_LICENSE_KEY is an Agents secret.
GitHub's docs say copilot-setup-steps.yml can use Agents secrets.

The key check handles a missing key in two ways:
- A pull_request or workflow_dispatch run gets a warning, and the step still succeeds.
- Any other run fails, and the error names the missing secret.

Write the file and print the YAML for me.
Then tell me how the key check reads the key, and cite the GitHub page you followed.
Text

Put the key in Agents secrets: according to GitHub, the cloud agent has no access to Actions, Codespaces or Dependabot secrets. In the repository's Settings, open Secrets and variables in the Security section, select Agents and add IRONPPT_LICENSE_KEY on the Secrets tab. GitHub's documentation gives no example of a setup step that reads an Agents secret.

What should you check before merging?

Run the review as a separate copilot -p command, outside the session that wrote the code.

/review this branch against main.

Compare the code with these three sources and list each spot where they disagree:
- the rules in .github/copilot-instructions.md
- the rules in .github/instructions/deck.instructions.md
- IronPPT.xml, which ships inside the IronPPT package in the NuGet cache

Also check for two patterns that no rule file covers:
- a loop that removes pictures while its index counts up
- a line that prints the message of a licensing exception

Tell me what you find, and say so if nothing turns up. Don't change any file.
Text

What did the prompts produce in a real run?

The seven prompts ran unattended in Copilot CLI against a listing repository built for this guide. A separate check read every figure below from the saved decks and the repository.

  • Before the run, ListingDeck was an empty class and the CLI printed a usage line and exited with code 1.
  • After the first prompt, the deck held a cover and four listing slides, 15,150,897 bytes in all.
  • All 10 pictures were inside their boxes with no measurable error in aspect ratio, size or centering.
  • The swap replaced the 220 Mill Road hero through document.Images.
  • The portrait reshoot measured 243 by 324 points, centered in the hero box.
  • None of the other nine pictures or the slide text changed in the swap.
  • In plan mode, Copilot wrote a plan, changed no file and waited for the approval message.
  • The fix reversed the loop in SoldUpdate.cs.
  • Marking 18 Alder Court sold then left no picture on its slide.
  • Copilot attributed the leftover photos to entries shifting inside a list and did not mention that each read of Slide.Images builds a fresh list.
  • Three removed photos, 6,469,676 bytes in all, remained in the sold copy.
  • The issue's session scaled nine photos to 1600 pixels on the long side with SixLabors.ImageSharp, which IronPPT already brings in, and added no package.
  • The rebuilt deck measured 1,198,644 bytes.
  • Every picture kept its size and position.
  • With the key set, all seven tests passed. Without it, all seven failed with the licensing exception.
  • The review session compared the branch with main, reported no disagreement and changed no file.
  • Some steps ran differently from the page. The issue body went to a new Copilot CLI session, because the test repository has no GitHub remote. The setup workflow was written but not run, for the same reason.

Cursor also applies committed rule files to matching paths, and Claude Code also holds its plan for approval before it edits code. The shared run record lists the Copilot CLI release, the model Auto selected, the duration and the test machine.


Troubleshooting


Questions?

If you have any questions, reach out to support@ironsoftware.com

Curtis Chauは、カールトン大学でコンピュータサイエンスの学士号を取得し、Node.js、TypeScript、JavaScript、およびReactに精通したフロントエンド開発を専門としています。直感的で美しいユーザーインターフェースを作成することに情熱を持ち、Curtisは現代のフレームワークを用いた開発や、構造の良い視覚的に魅力的なマニュアルの作成を楽しんでいます。

詳しく読む

準備はできましたか?

Nuget Downloads 6,246バージョン:2026.10リリースされたばかり