Registration forms, sign-up sheets, and questionnaires go out as blank PDFs and come back needing to be filled in by hand, one file at a time. The forms themselves change too — a missing text box, an extra checkbox nobody uses anymore — and every change means opening desktop software like Acrobat. That is endurable for one or two files; in bulk it is nothing but manual clicking.

This article shows how to add, fill, and delete PDF form fields with Spire.PDF for JavaScript. It is built on WebAssembly and loads, modifies, and saves PDF documents directly in the browser. The whole process runs locally and reads and writes files through a virtual file system (VFS), with no backend involved.

This article covers three core features:

For installation and project configuration, see Integrate Spire.PDF for JavaScript into a React Project. The examples below assume Spire.PDF is installed and the WebAssembly module is initialized.


Add Form Fields

Spire.PDF for JavaScript provides a complete set of form field classes covering text boxes, check boxes, radio buttons, combo boxes, list boxes, buttons, and signature fields. They all work the same way: create an instance on the page, position it with Bounds, and hand it to doc.Form.Fields.Add(). When you load an existing document, set doc.AllowCreateForm to true first.

Class Description
PdfTextBoxField Text box field
PdfCheckBoxField Check box field
PdfRadioButtonListField Radio button field
PdfComboBoxField Combo box field
PdfListBoxField List box field
PdfButtonField Button field
PdfSignatureField Signature field

Bounds, BorderWidth, BorderStyle, Required, ReadOnly, Visible, and ToolTip are shared by every field type.

function App() {
  const addFormFields = async () => {
    // Get the Spire.PDF WASM module
    const pdfModule = window.wasmModule?.spirepdf;

    // Check that the module is ready
    if (!pdfModule) {
      alert('Spire.PDF is not ready yet');
      return;
    }

    // Load the PDF file into the VFS
    const inputFileName = 'BlankRegistrationForm.pdf';
    await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}/data/`);

    // Create a PdfDocument object and load the PDF
    let doc = new pdfModule.PdfDocument();
    doc.LoadFromFile(inputFileName);

    // Form creation must be enabled explicitly for an existing document
    doc.AllowCreateForm = true;

    let page = doc.Pages.get_Item(0);
    let uiFont = new pdfModule.PdfFont({
      fontFamily: pdfModule.PdfFontFamily.Helvetica,
      size: 10
    });
    const box = (x, y, width, height) => new pdfModule.RectangleF({ x, y, width, height });
    const border = 0.75;

    // 1. Text box: name
    let nameBox = new pdfModule.PdfTextBoxField(page, 'name');
    nameBox.Bounds = box(178, 168, 210, 20);
    nameBox.BorderWidth = border;
    nameBox.BorderStyle = pdfModule.PdfBorderStyle.Solid;
    nameBox.Font = uiFont;
    doc.Form.Fields.Add(nameBox);

    // 2. Text box: email
    let emailBox = new pdfModule.PdfTextBoxField(page, 'email');
    emailBox.Bounds = box(178, 206, 210, 20);
    emailBox.BorderWidth = border;
    emailBox.BorderStyle = pdfModule.PdfBorderStyle.Solid;
    emailBox.Font = uiFont;
    doc.Form.Fields.Add(emailBox);

    // 3. Combo box: department
    let departmentBox = new pdfModule.PdfComboBoxField(page, 'department');
    departmentBox.Bounds = box(178, 244, 210, 20);
    departmentBox.BorderWidth = border;
    departmentBox.Font = uiFont;
    ['Engineering', 'Marketing', 'Sales', 'Support'].forEach(function (item) {
      departmentBox.Items.Add(new pdfModule.PdfListFieldItem({ text: item, value: item.toLowerCase() }));
    });
    doc.Form.Fields.Add(departmentBox);

    // 4. Radio buttons: gender, each option is a PdfRadioButtonListItem
    let genderBox = new pdfModule.PdfRadioButtonListField(page, 'gender');
    ['male', 'female'].forEach(function (value, index) {
      let item = new pdfModule.PdfRadioButtonListItem();
      item.Bounds = box(185.5 + index * 110, 285.5, 13, 13);
      item.BorderWidth = border;
      item.Value = value;
      genderBox.Items.Add(item);
    });
    doc.Form.Fields.Add(genderBox);

    // 5. List box: education
    let educationBox = new pdfModule.PdfListBoxField(page, 'education');
    educationBox.Bounds = box(178, 320, 210, 52);
    educationBox.BorderWidth = border;
    educationBox.Font = uiFont;
    ['Bachelor', 'Master', 'Doctor'].forEach(function (item) {
      educationBox.Items.Add(new pdfModule.PdfListFieldItem({ text: item, value: item.toLowerCase() }));
    });
    doc.Form.Fields.Add(educationBox);

    // 6. Check box: agree to terms
    let agreeBox = new pdfModule.PdfCheckBoxField(page, 'agree_terms');
    agreeBox.Bounds = box(178, 392, 15, 15);
    agreeBox.BorderWidth = border;
    agreeBox.Style = pdfModule.PdfCheckBoxStyle.Check;
    agreeBox.Required = true;
    doc.Form.Fields.Add(agreeBox);

    // 7. Signature field: reserve a place to sign
    let signatureBox = new pdfModule.PdfSignatureField(page, 'signature');
    signatureBox.Bounds = box(178, 424, 210, 40);
    doc.Form.Fields.Add(signatureBox);

    // 8. Button: submit
    let submitButton = new pdfModule.PdfButtonField(page, 'submit');
    submitButton.Bounds = box(72, 478, 90, 26);
    submitButton.Text = 'Submit';
    submitButton.HighlightMode = pdfModule.PdfHighlightMode.Push;
    doc.Form.Fields.Add(submitButton);

    const outputFileName = 'AddFormFields.pdf';
    doc.SaveToFile(outputFileName);
    doc.Close();

    // Read the generated file from the VFS and trigger the download
    const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
    const blob = new Blob([fileArray], { type: 'application/pdf' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = outputFileName;
    a.click();
    URL.revokeObjectURL(url);
  };

  return (
    <div style={{ textAlign: 'center', height: '300px' }}>
      <h1>Add Form Fields</h1>
      <button onClick={addFormFields}>
        Add fields
      </button>
    </div>
  );
}

export default App;

The blank registration form now carries a text box, check box, radio buttons, combo box, list box, button, and signature field:

The blank registration form now carries a text box, check box, radio buttons, combo box, list box, button, and signature field


Fill Form Fields

Filling an existing form means working from the widget side: PdfFormWidget wraps the document's form, FieldsWidget hands out the field instances one by one, and after checking the type you cast to the matching subclass to write a value — Text for text boxes, Checked for check boxes, SelectedIndex for combo boxes. Fields are told apart by Name, so a single pass fills the whole form.

function App() {
  const fillFormFields = async () => {
    // Get the Spire.PDF WASM module
    const pdfModule = window.wasmModule?.spirepdf;

    // Check that the module is ready
    if (!pdfModule) {
      alert('Spire.PDF is not ready yet');
      return;
    }

    // Load the PDF file to be filled into the VFS
    const inputFileName = 'EmployeeRegistrationForm.pdf';
    await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}/data/`);

    // Create a PdfDocument object and load the PDF
    let doc = new pdfModule.PdfDocument();
    doc.LoadFromFile(inputFileName);

    // Get the widget collection from the form
    let formWidget = new pdfModule.PdfFormWidget(doc.Form.H);

    for (let i = 0; i < formWidget.FieldsWidget.Count; i++) {
      let field = formWidget.FieldsWidget.get_Item({ index: i });

      // Text box: assign Text directly
      if (field instanceof pdfModule.PdfTextBoxFieldWidget) {
        switch (field.Name) {
          case 'name':
            field.Text = 'Jane Doe';
            break;
          case 'email':
            field.Text = 'jane.doe@example.com';
            break;
        }
      }

      // Combo box: SelectedIndex takes an array of indices
      if (field instanceof pdfModule.PdfComboBoxWidgetFieldWidget) {
        if (field.Name === 'department') {
          field.SelectedIndex = [1];
        }
      }

      // Check box: set Checked to true to tick it
      if (field instanceof pdfModule.PdfCheckBoxWidgetFieldWidget) {
        if (field.Name === 'agree_terms') {
          field.Checked = true;
        }
      }
    }

    const outputFileName = 'FillFormFields.pdf';
    doc.SaveToFile(outputFileName);
    doc.Close();

    // Read the generated file from the VFS and trigger the download
    const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
    const blob = new Blob([fileArray], { type: 'application/pdf' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = outputFileName;
    a.click();
    URL.revokeObjectURL(url);
  };

  return (
    <div style={{ textAlign: 'center', height: '300px' }}>
      <h1>Fill Form Fields</h1>
      <button onClick={fillFormFields}>
        Fill form
      </button>
    </div>
  );
}

export default App;

The text box, combo box, and check box have each been given their values:

The text box, combo box, and check box have each been given their values


Delete Form Fields

Deleting also starts from FieldsWidget: locate the target instance by Name, then call Remove() to take it out of the field collection. Locating by field name is safer than by index — a document whose layout has changed will not lose the wrong field.

function App() {
  const deleteFormField = async () => {
    // Get the Spire.PDF WASM module
    const pdfModule = window.wasmModule?.spirepdf;

    // Check that the module is ready
    if (!pdfModule) {
      alert('Spire.PDF is not ready yet');
      return;
    }

    // Load the PDF file into the VFS
    const inputFileName = 'EmployeeRegistrationForm.pdf';
    await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}/data/`);

    // Create a PdfDocument object and load the PDF
    let doc = new pdfModule.PdfDocument();
    doc.LoadFromFile(inputFileName);

    let form = doc.Form;
    if (form != null) {
      let formWidget = new pdfModule.PdfFormWidget(form.H);

      // Locate the target field by name and remove it
      for (let i = 0; i < formWidget.FieldsWidget.Count; i++) {
        let field = formWidget.FieldsWidget.get_Item({ index: i });
        if (field.Name === 'name') {
          formWidget.FieldsWidget.Remove(field);
          break;
        }
      }
    }

    const outputFileName = 'DeleteFormField.pdf';
    doc.SaveToFile(outputFileName);
    doc.Close();

    // Read the generated file from the VFS and trigger the download
    const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
    const blob = new Blob([fileArray], { type: 'application/pdf' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = outputFileName;
    a.click();
    URL.revokeObjectURL(url);
  };

  return (
    <div style={{ textAlign: 'center', height: '300px' }}>
      <h1>Delete Form Fields</h1>
      <button onClick={deleteFormField}>
        Delete field
      </button>
    </div>
  );
}

export default App;

The Name text box has been removed from the form:

The Name text box has been removed from the form


FAQ

New form fields cannot be clicked in the reader

Cause: AllowCreateForm defaults to false. When you open an existing document with LoadFromFile, Spire.PDF keeps the document's original form structure and does not allow fields to be appended, so doc.Form.Fields.Add() throws no error but the saved document has no new fields.

Fix: Turn the property on after loading the document and before adding fields:

doc.LoadFromFile(inputFileName);
doc.AllowCreateForm = true;

Fields are still empty after filling

Cause: The field name in the case branch is not character-for-character identical to the real name in the document. PDF field names are case-sensitive and keep leading and trailing spaces, so a name such as company_name (with a trailing space) never matches company_name written in code. Another common mistake is assigning values to objects taken from doc.Form.Fields — those fields have no widget properties such as Text.

Fix: Print every field name before filling and copy from the output. Assignments must land on the *FieldWidget instances that PdfFormWidget returns:

let formWidget = new pdfModule.PdfFormWidget(doc.Form.H);
for (let i = 0; i < formWidget.FieldsWidget.Count; i++) {
  console.log(formWidget.FieldsWidget.get_Item({ index: i }).Name);
}

Deleting one field makes the fields after it disappear too

Cause: Remove() changes FieldsWidget.Count immediately, and every field after the removed element shifts up by one position. If you iterate forward and remove inside the loop, the next iteration has already skipped past the element that moved into that slot.

Fix: break after a single removal; to delete several, collect the targets by name and process them one by one, or iterate backwards from the end:

// Iterate backwards and remove every field whose name starts with temp_
for (let i = formWidget.FieldsWidget.Count - 1; i >= 0; i--) {
  let field = formWidget.FieldsWidget.get_Item({ index: i });
  if (field.Name.startsWith('temp_')) {
    formWidget.FieldsWidget.Remove(field);
  }
}

Get a Free License

If you want to remove the evaluation message from the result documents, or to get rid of the feature limitations, contact sales for a temporary license valid for 30 days.

Archiving is usually the next step after a form is filled in. PDF form fields are exactly where that falls apart: the file looks complete, but the controls are still live, so the recipient can edit an amount, a date or a signature box, and some viewers re-validate the form as it opens. Turning that file into something nobody can change means pressing the controls and their values into the page content.

This article uses Spire.PDF for JavaScript to flatten PDF form fields. It runs on WebAssembly, loading, modifying and saving documents in the browser, and reads and writes files through a virtual file system (VFS) with no backend involved.

This article covers two core features:

For installation and project configuration, see Integrate Spire.PDF for JavaScript into a React Project. The examples below assume Spire.PDF is installed and the WebAssembly module has been initialized.


Flatten the Whole Form

Spire.PDF for JavaScript provides the PdfForm.IsFlatten property to flatten an entire form in one pass. After it is set to true, every field in the document is converted into static page content together with its current value, and the saved PDF has no interactive controls left. The text stays in the text layer, so it can still be selected, copied and searched.

function App() {
  const flattenWholeForm = async () => {
    // Get the Spire.PDF WASM module
    const pdfModule = window.wasmModule?.spirepdf;

    // Check that the module is ready
    if (!pdfModule) {
      alert('Spire.PDF is not ready yet');
      return;
    }

    // Load the PDF file to be processed into the VFS
    const inputFileName = 'EmployeeRegistrationForm.pdf';
    await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}/data/`);

    // Create a PdfDocument object and load the PDF document
    let doc = new pdfModule.PdfDocument();
    doc.LoadFromFile(inputFileName);

    // Flatten the entire form in one pass
    doc.Form.IsFlatten = true;

    const outputFileName = 'FlattenWholeForm.pdf';
    doc.SaveToFile(outputFileName);
    doc.Close();

    // Read the generated file from the VFS and trigger the download
    const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
    const blob = new Blob([fileArray], { type: 'application/pdf' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = outputFileName;
    a.click();
    URL.revokeObjectURL(url);
  };

  return (
    <div style={{ textAlign: 'center', height: '300px' }}>
      <h1>Flatten the whole form</h1>
      <button onClick={flattenWholeForm}>
        Flatten now
      </button>
    </div>
  );
}

export default App;

Every input box disappears and the values remain on the page as plain text:

Every input box disappears and the values remain on the page as plain text


Flatten a Selected Field

Spire.PDF for JavaScript also provides the PdfField.Flatten property for field-level flattening. Take the field instance by Name from the FieldsWidget collection of a PdfFormWidget; only the field you pick is fixed and the rest stay editable — freezing an email address that has already been verified, for example, while leaving the date column for the recipient to fill in.

function App() {
  const flattenSelectedField = async () => {
    // Get the Spire.PDF WASM module
    const pdfModule = window.wasmModule?.spirepdf;

    // Check that the module is ready
    if (!pdfModule) {
      alert('Spire.PDF is not ready yet');
      return;
    }

    // Load the PDF file to be processed into the VFS
    const inputFileName = 'EmployeeRegistrationForm.pdf';
    await window.spire.FetchFileToVFS(inputFileName, "", `${process.env.PUBLIC_URL}/data/`);

    // Create a PdfDocument object and load the PDF document
    let doc = new pdfModule.PdfDocument();
    doc.LoadFromFile(inputFileName);

    // Get the widget collection from the form
    let formWidget = new pdfModule.PdfFormWidget(doc.Form.H);

    // Pick the target field by name and flatten only that one
    for (let i = 0; i < formWidget.FieldsWidget.Count; i++) {
      let field = formWidget.FieldsWidget.get_Item({ index: i });
      if (field.Name === 'email') {
        field.Flatten = true;
      }
    }

    const outputFileName = 'FlattenSelectedField.pdf';
    doc.SaveToFile(outputFileName);
    doc.Close();

    // Read the generated file from the VFS and trigger the download
    const fileArray = window.dotnetRuntime.Module.FS.readFile(outputFileName);
    const blob = new Blob([fileArray], { type: 'application/pdf' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = outputFileName;
    a.click();
    URL.revokeObjectURL(url);
  };

  return (
    <div style={{ textAlign: 'center', height: '300px' }}>
      <h1>Flatten a selected field</h1>
      <button onClick={flattenSelectedField}>
        Flatten now
      </button>
    </div>
  );
}

export default App;

Only the email input box disappears; the remaining fields are still editable controls:

Only the email input box disappears; the remaining fields are still editable controls


Frequently Asked Questions

The fields are still clickable in the viewer after setting IsFlatten

Reason: The code mixes up two field collections. For an AcroForm produced by another tool, doc.Form.Fields frequently reads back no fields at all — in this article's sample document Count reads 0, and calling get_Item() on it throws ArgumentOutOfRange_IndexMustBeLess. Even when it does return entries, those PdfField objects have no control properties such as Text or Checked, so changing them leaves the page untouched.

Solution: Use doc.Form.IsFlatten alone to flatten the whole form; it does not depend on any collection. As soon as you work field by field — selecting by name, writing a value, flattening one field — go through PdfFormWidget:

let formWidget = new pdfModule.PdfFormWidget(doc.Form.H);
for (let i = 0; i < formWidget.FieldsWidget.Count; i++) {
  console.log(formWidget.FieldsWidget.get_Item({ index: i }).Name);
}

How do I tell whether a PDF has already been flattened

Reason: PdfForm.IsFlatten is a write instruction, not a state flag. Reload the flattened output and doc.Form.IsFlatten still reads false — in testing the document already had 0 fields at that point.

Solution: Check the field count instead; a FieldsWidget.Count of 0 means no interactive controls are left:

let formWidget = new pdfModule.PdfFormWidget(doc.Form.H);
const hasFormFields = formWidget.FieldsWidget.Count > 0;

Only the selected field was flattened, but its value was not baked in

Reason: Name is compared character by character, so it is case-sensitive and keeps leading and trailing spaces. Write company_name when the document actually has company_name (with a trailing space) and the loop never matches — and it raises no error, it just saves the file unchanged.

Solution: Print every field name first and copy from that output. Both the comparison and the assignment have to run on the *FieldWidget instance that FieldsWidget returns:

for (let i = 0; i < formWidget.FieldsWidget.Count; i++) {
  console.log(formWidget.FieldsWidget.get_Item({ index: i }).Name);
}

Get a Free License

If you want to remove the evaluation message from the result document or lift the feature limits, contact our sales team for a temporary license valid for 30 days.

When you build a presentation, the part that really costs time is rarely the layout — it is deciding what to say. A slide only has room for the essentials: a few short lines, a set of numbers, one chart. The words that actually need to be spoken live in the author's head. The night before the talk, the author reads the whole deck again, slide by slide, trying to remember what each page was meant to say, and then writes it up.

It gets worse when a team is involved. For a product launch, one person writes the content, another builds the slides, and a third delivers the talk. Whoever steps on stage inherits a deck of bullet points with no idea which numbers matter most — so they improvise. Speaker notes exist precisely for this: they never appear on the projector, only in Presenter View, as the author's message to the speaker. In practice, almost nobody has the patience to fill them in page by page.

The PowerPoint AI capability in Spire.Agent.Office lets you describe in plain language what the notes should say and what tone the script should take. The AI agent reads the title and bullet points of every slide and then handles two jobs: writing speaker notes for each slide, and expanding the whole deck into a Word script that can be read aloud directly.

Compared with the Traditional SDK API

Traditional Spire.Office for .NET API Spire.Agent.Office
Driving model Loop over slides, pull text out of each shape, assemble the copy yourself, then write it back to the notes page — every step is code Describe the writing requirements in natural language and let the AI plan the execution path
Code size 150-300 lines of C# (slide iteration, shape traversal, text extraction, notes-page writing, font and paragraph settings) About 10 lines of calling code plus one natural-language instruction
Content generation You must wire up a model yourself or hand-write templates, and it is hard to keep each slide in context The AI understands each slide's points and writes copy that reads coherently page to page
Layout handling Writing notes easily damages the original layout and placeholder structure The AI preserves the original layout, fonts and colours, appending only to the notes pane
Changing requirements Adjust tone, length or script format → edit code → rebuild → redeploy Edit the wording of the instruction and it takes effect immediately

This article shows how to use Spire.Agent.Office to generate speaker notes and a script for a PowerPoint deck. Two cases cover the two common patterns — adding notes to an existing deck and turning a report into a Word script:

For product installation and SpireToken configuration, please refer to Integrating Spire.Agent.Office in a .NET Project. The examples below assume Spire.Agent.Office is installed and SpireToken is configured.


Case 1: Generate per-slide speaker notes for a deck

The most common situation is a deck that is already final: layout and content stay exactly as they are, and the only thing missing is the prompt for whoever has to speak. A projected slide is built to be read at a glance, so it carries a title and a few bullet points — where the emphasis falls, what is behind each number, and how to move from one page to the next all stay in the author's head. Speaker notes are what carry that across. They are the author's handover to the presenter: what this page is really getting at, what to stress, and how to lead naturally into the next one. This product launch deck has 8 slides, each with a title and a few bullet points, and an empty notes pane from start to finish. The presenter needs one short paragraph per slide — not an essay, but enough to present the deck rather than read it out. Writing those notes by hand is expensive because every slide needs its own thought. Read the page, rebuild the sentence, and calibrate length — too short and it is no help, too long and it turns into reading from a script. Eight slides is tolerable; a fifty-page annual review rarely gets the treatment it deserves.

The example below uses the Spire.Agent.Office agent to read every slide and write speaker notes into the notes pane, ready to be read aloud:

using Spire.Agent.Office.AI;
using Spire.Agent.Office.Extensions;
using Spire.Presentation;

// PowerPoint processing configuration
string inputPath = @"E:\product-launch.pptx";  // Deck to add speaker notes to
string savePath = @"E:\product_result.pptx";  // Result document path

string key = "**************************";  // SpireToken key
string instruction = "Generate speaker notes for each slide of the input PPT file and write them into the notes pane of that slide: " +
"1. Read each slide's title and bullet points and understand what the page is meant to convey; " +
"2. Write one paragraph of speaker notes per slide, in the presenter's voice, ready to be read aloud; " +
"3. Each note should make clear what this page emphasises and lead naturally into the next slide; keep the notes varied in depth — explain the important content in detail and keep transition slides brief; " +
"4. Keep the original slide layout, fonts, colours and page order completely unchanged, and do not add or remove any slides; " +
"Finally, save the output as a PowerPoint file";

// Call the PowerPoint processing function
AIResult result = ExecuteDemoPpt(instruction, inputPath, savePath, key);

// Run the PowerPoint AI processing
static AIResult ExecuteDemoPpt(string instruction, string inputPath, string savePath, string key)
{
    // Create the AIOptions configuration object
    AIOptions options = new AIOptions();
    options.SpireToken = key;  // Set the SpireToken key

    // Process the PowerPoint document with a Presentation object
    using (Presentation ppt = new Presentation())
    {
        // Load the PowerPoint document from file
        if (!string.IsNullOrEmpty(inputPath) && File.Exists(inputPath))
        {
            ppt.LoadFromFile(inputPath);
        }
        // Create the AI document processor
        AIDocumentProcessor processor = ppt.AI(options);

        // Execute the AI instruction
        return processor.ExecuteInstruction(ppt, instruction, savePath);
    }
}

The original deck with an empty notes pane The original deck with an empty notes pane The deck after speaker notes were written slide by slide The deck after speaker notes were written slide by slide

The generated deck looks identical to the original on the projector; the notes appear only in Presenter View. The speaker opens Presenter View and sees the prompt for each slide below it, ready to deliver a complete product launch in order.


Case 2: Turn a business review into a ready-to-read Word script

The notes pane is good for prompting, but not for reading aloud. Notes are short fragments in a small font at the edge of the screen; looking down at them on stage hurts delivery and invites missed lines. When the occasion is more formal — reporting quarterly results to management, for instance, where every figure has to be explained — the speaker needs a complete, spoken-word script. The medium for that script deserves a word of its own. A presentation is made to be looked at; a script is made to be read. A script needs control over font size, line spacing and sectioning, and it is usually printed and carried into the room. Packing several hundred words of body text onto each slide reads badly — and projecting it hands the audience every line. So this case takes a PPT as input and produces a Word script document as output.

Handling that format change is exactly what Spire.Agent.Office does: the deck is still loaded with Presentation, the AI reads each slide, and it writes the script as a Word file following the instruction. So this case has the AI read the figures page by page and then write the script as sections in the original order: each slide of the original becomes one section of the script document, keeping the original title, with the body holding the full script text.

The example below uses the Spire.Agent.Office agent to interpret the figures page by page and write the script, producing a Word document:

using Spire.Agent.Office.AI;
using Spire.Agent.Office.Extensions;
using Spire.Presentation;

// PowerPoint processing configuration
string inputPath = @"E:\quarterly-review.pptx";  // Input presentation path
string savePath = null;  // Result document path (null here, the output folder below is used instead)
string OutDir = @"E:\output-script";  // Output directory
string key = "**************************";  // SpireToken key
string instruction =
    "Read the input PPT file and write a complete presentation script for the speaker, outputting it as a Word script document: " +
    "1. Read the figures and bullet points on each slide and understand the core conclusion of that page; " +
    "2. Write a complete, conversational script per slide that can be read aloud directly; keep it varied in depth, quote the specific figures on that page and explain what they mean for the business; " +
    "3. Lay the script out as sections in the original slide order, each headed 'Slide N + the original title', with the full script for that page as the body; " +
    "4. Format the body for reading aloud: a clear typeface, no smaller than 14 pt, 1.5 line spacing, comfortable paragraph spacing;";

// Call the PowerPoint processing function
AIResult result = ExecuteDemoPpt(instruction, inputPath, savePath, key, OutDir);

// Run the PowerPoint AI processing
static AIResult ExecuteDemoPpt(string instruction, string inputPath, string savePath, string key, string output)
{
    // Create the AIOptions configuration object
    AIOptions options = new AIOptions();
    options.WorkDir = output;  // Set the working directory to the output directory
    options.SpireToken = key;  // Set the SpireToken key

    // Process the PowerPoint document with a Presentation object
    using (Presentation ppt = new Presentation())
    {
        // Load the PowerPoint document from file
        if (!string.IsNullOrEmpty(inputPath) && File.Exists(inputPath))
        {
            ppt.LoadFromFile(inputPath);
        }
        // Create the AI document processor
        AIDocumentProcessor processor = ppt.AI(options);

        // Execute the AI instruction
        return processor.ExecuteInstruction(ppt, instruction, savePath);
    }
}

The generated Word script document The generated Word script document


FAQ

Can I use my own model or a private deployment?

Note: By default the AI requests go to Spire's model service. AIOptions also exposes Model, BaseUrl and ApiKey for the model name, the service address and the access key — set these three to route the requests to your own or a third-party inference service.

AIOptions options = new AIOptions();
options.BaseUrl = "baseUrl";
options.Model = "modelName";
options.SpireToken = "*************";
options.ApiKey = "*************";

A long deck is slow to process — and may time out

Cause: Notes and scripts are generated page by page, so the AI reads and writes the document repeatedly and sends the existing content back to the model each time. The more slides there are, and the denser the text on each, the faster token consumption grows and the longer the whole run takes — the default timeout is often not enough.

Solution: Set TimeoutMs explicitly on the AIOptions instance (the unit is milliseconds) so a long deck has enough time. For a 50-slide PPT file, TimeoutMs can be set to 10 minutes (600000).


Getting a SpireToken Key

Configure it in code:

AIOptions options = new AIOptions();
options.SpireToken = key;
page 16