Skip to content

从零创建 PPTX 演示文稿

PptxWriter 在内存中全新构建一个 PowerPoint .pptx 文件,并写入磁盘或字节缓冲区 — 无需模板、无需安装 PowerPoint、无需 COM 自动化。当你需要对幻灯片、标题、正文文本和图片进行精细控制时,这是最合适的工具,而非通过与格式无关的 IR 进行映射。

该 Writer 与提取 PPTX 的 Rust 核心相同,在 806 个真实 PowerPoint 文件上的平均耗时 0.7ms,通过率 98.4%,并对外暴露 Python、Rust、Go、C# 和 JavaScript(原生 Node)接口。

关于单位:所有位置和尺寸均以 EMU(English Metric Units)表示。914400 EMU = 1 英寸360000 EMU ≈ 1 厘米。默认画布为 12192000 × 6858000 EMU — 标准 16:9 宽屏(33.87cm × 19.05cm)。

如何在 Python 中从零创建 PPTX 文件?

构建一个 PptxWriter,调用 add_slide() 获取从 0 开始的幻灯片索引,然后用该索引设置标题并添加正文文本。最后调用 save()

Rust

use office_oxide::pptx::write::PptxWriter;

fn main() -> office_oxide::Result<()> {
    let mut pres = PptxWriter::new();

    let slide = pres.add_slide_get_index();        // -> 0 (the slide index)
    pres.slide_set_title(slide, "Quarterly Results");
    pres.slide_add_text(slide, "Revenue grew 18% quarter over quarter.");
    pres.slide_add_text(slide, "All regions exceeded target.");

    let slide2 = pres.add_slide_get_index();       // -> 1
    pres.slide_set_title(slide2, "Next Steps");
    pres.slide_add_text(slide2, "Expand EU sales team.");

    pres.save("results.pptx")?;
    Ok(())
}

Python

from office_oxide import PptxWriter

pres = PptxWriter()

slide = pres.add_slide()                       # -> 0 (the slide index)
pres.set_slide_title(slide, "Quarterly Results")
pres.add_slide_text(slide, "Revenue grew 18% quarter over quarter.")
pres.add_slide_text(slide, "All regions exceeded target.")

slide2 = pres.add_slide()                       # -> 1
pres.set_slide_title(slide2, "Next Steps")
pres.add_slide_text(slide2, "Expand EU sales team.")

pres.save("results.pptx")

JavaScript

import { PptxWriter } from 'office-oxide';

const pres = new PptxWriter();

const slide = pres.addSlide(); // -> 0 (the slide index)
pres.setSlideTitle(slide, 'Quarterly Results');
pres.addSlideText(slide, 'Revenue grew 18% quarter over quarter.');
pres.addSlideText(slide, 'All regions exceeded target.');

const slide2 = pres.addSlide(); // -> 1
pres.setSlideTitle(slide2, 'Next Steps');
pres.addSlideText(slide2, 'Expand EU sales team.');

pres.save('results.pptx');
pres.close(); // release the native handle

Go

package main

import officeoxide "github.com/yfedoseev/office_oxide/go"

func main() {
	pres := officeoxide.NewPptxWriter()
	defer pres.Close()

	slide := pres.AddSlide() // -> 0 (the slide index)
	pres.SetSlideTitle(slide, "Quarterly Results")
	pres.AddSlideText(slide, "Revenue grew 18% quarter over quarter.")
	pres.AddSlideText(slide, "All regions exceeded target.")

	slide2 := pres.AddSlide() // -> 1
	pres.SetSlideTitle(slide2, "Next Steps")
	pres.AddSlideText(slide2, "Expand EU sales team.")

	if err := pres.Save("results.pptx"); err != nil {
		panic(err)
	}
}

C#

using OfficeOxide;

using var pres = new PptxWriter();

uint slide = pres.AddSlide(); // -> 0 (the slide index)
pres.SetSlideTitle(slide, "Quarterly Results");
pres.AddSlideText(slide, "Revenue grew 18% quarter over quarter.");
pres.AddSlideText(slide, "All regions exceeded target.");

uint slide2 = pres.AddSlide(); // -> 1
pres.SetSlideTitle(slide2, "Next Steps");
pres.AddSlideText(slide2, "Expand EU sales team.");

pres.Save("results.pptx");

C

#include "office_oxide.h"

OfficePptxWriterHandle *p = office_pptx_writer_new();

uint32_t slide = office_pptx_writer_add_slide(p);    /* -> 0 (the slide index) */
office_pptx_slide_set_title(p, slide, "Quarterly Results");
office_pptx_slide_add_text(p, slide, "Revenue grew 18% quarter over quarter.");
office_pptx_slide_add_text(p, slide, "All regions exceeded target.");

uint32_t slide2 = office_pptx_writer_add_slide(p);   /* -> 1 */
office_pptx_slide_set_title(p, slide2, "Next Steps");
office_pptx_slide_add_text(p, slide2, "Expand EU sales team.");

int err = 0;
office_pptx_writer_save(p, "results.pptx", &err);
office_pptx_writer_free(p);

如果要将字节流传递给 Web 响应或对象存储,而非写入磁盘,请使用 to_bytes()

Rust

use std::io::Cursor;

let mut buf = Cursor::new(Vec::new());
pres.write_to(&mut buf)?;       // a complete .pptx ZIP
let data = buf.into_inner();    // -> Vec<u8>

Python

data = pres.to_bytes()         # -> bytes (a complete .pptx ZIP)
with open("results.pptx", "wb") as f:
    f.write(data)

JavaScript

const data = pres.toBytes();   // -> Buffer (a complete .pptx ZIP)
writeFileSync('results.pptx', data);

Go

data, err := pres.ToBytes()    // -> []byte (a complete .pptx ZIP)
if err != nil {
	panic(err)
}
_ = os.WriteFile("results.pptx", data, 0o644)

C#

byte[] data = pres.ToBytes();  // a complete .pptx ZIP
File.WriteAllBytes("results.pptx", data);

C

size_t out_len = 0;
int err = 0;
uint8_t *buf = office_pptx_writer_to_bytes(p, &out_len, &err);  /* a complete .pptx ZIP */
if (buf) { /* stream buf[0..out_len] */ office_oxide_free_bytes(buf, out_len); }

方法签名(Python)

方法 签名
构造函数 PptxWriter()
画布尺寸 set_presentation_size(cx: int, cy: int) -> None
添加幻灯片 add_slide() -> int
设置标题 set_slide_title(slide: int, title: str) -> None
添加正文文本 add_slide_text(slide: int, text: str) -> None
添加图片 add_slide_image(slide: int, data: bytes, format: str, x: int, y: int, cx: int, cy: int) -> None
保存 save(path) -> None
导出 to_bytes() -> bytes

add_slide 返回新幻灯片从 0 开始的索引 — 将其作为 slide 参数传递给所有其他调用。每次调用 add_slide_text 都会向幻灯片的正文占位符追加一个段落。

如何设置幻灯片尺寸并添加图片?

在添加幻灯片之前调用 set_presentation_size(cx, cy) 来修改画布 — 例如切换到 4:3 比例(9144000 × 6858000)。add_slide_image 将原始图片字节嵌入到绝对位置;format"png""jpeg"/"jpg""gif"

Rust

use office_oxide::pptx::write::PptxWriter;
use office_oxide::ir::ImageFormat;

fn main() -> office_oxide::Result<()> {
    let mut pres = PptxWriter::new();
    pres.set_presentation_size(9_144_000, 6_858_000); // 4:3, in EMU

    let logo = std::fs::read("logo.png")?;
    pres.add_slide()
        .set_title("Logo")
        // Place the image 1 inch from the top-left, sized 3in × 2in
        .add_image(logo, ImageFormat::Png, 914_400, 914_400, 2_743_200, 1_828_800);

    pres.save("deck.pptx")?;
    Ok(())
}

Python

from office_oxide import PptxWriter

pres = PptxWriter()
pres.set_presentation_size(9_144_000, 6_858_000)   # 4:3, in EMU

slide = pres.add_slide()
pres.set_slide_title(slide, "Logo")

with open("logo.png", "rb") as f:
    logo = f.read()

# Place the image 1 inch from the top-left, sized 3in × 2in
pres.add_slide_image(slide, logo, "png",
                     x=914_400, y=914_400,
                     cx=2_743_200, cy=1_828_800)

pres.save("deck.pptx")

JavaScript

import { readFileSync } from 'node:fs';
import { PptxWriter } from 'office-oxide';

const pres = new PptxWriter();
pres.setPresentationSize(9144000, 6858000); // 4:3, in EMU

const slide = pres.addSlide();
pres.setSlideTitle(slide, 'Logo');

const logo = readFileSync('logo.png');

// Place the image 1 inch from the top-left, sized 3in × 2in
pres.addSlideImage(slide, logo, 'png', 914400, 914400, 2743200, 1828800);

pres.save('deck.pptx');
pres.close();

Go

package main

import (
	"os"

	officeoxide "github.com/yfedoseev/office_oxide/go"
)

func main() {
	pres := officeoxide.NewPptxWriter()
	defer pres.Close()
	pres.SetPresentationSize(9144000, 6858000) // 4:3, in EMU

	slide := pres.AddSlide()
	pres.SetSlideTitle(slide, "Logo")

	logo, _ := os.ReadFile("logo.png")

	// Place the image 1 inch from the top-left, sized 3in × 2in
	pres.AddSlideImage(slide, logo, "png", 914400, 914400, 2743200, 1828800)

	if err := pres.Save("deck.pptx"); err != nil {
		panic(err)
	}
}

C#

using OfficeOxide;

using var pres = new PptxWriter();
pres.SetPresentationSize(9144000, 6858000); // 4:3, in EMU

uint slide = pres.AddSlide();
pres.SetSlideTitle(slide, "Logo");

byte[] logo = File.ReadAllBytes("logo.png");

// Place the image 1 inch from the top-left, sized 3in × 2in
pres.AddSlideImage(slide, logo, "png", 914400, 914400, 2743200, 1828800);

pres.Save("deck.pptx");

C

#include "office_oxide.h"

OfficePptxWriterHandle *p = office_pptx_writer_new();
office_pptx_writer_set_presentation_size(p, 9144000, 6858000); /* 4:3, in EMU */

uint32_t slide = office_pptx_writer_add_slide(p);
office_pptx_slide_set_title(p, slide, "Logo");

/* read logo.png into img_data / img_len yourself, then: */
/* Place the image 1 inch from the top-left, sized 3in × 2in */
office_pptx_slide_add_image(p, slide, img_data, img_len, "png",
                            914400, 914400, 2743200, 1828800);

int err = 0;
office_pptx_writer_save(p, "deck.pptx", &err);
office_pptx_writer_free(p);

完整的创建与导出流程(按语言分类)

以下示例构建两张幻灯片、嵌入一张图片、保存到磁盘并导出为字节流 — 每种语言绑定的操作完全相同。

Rust

use office_oxide::pptx::write::{PptxWriter, Run};
use office_oxide::ir::ImageFormat;

fn main() -> office_oxide::Result<()> {
    let mut pres = PptxWriter::new();

    // Slide 1: title, body text, a bullet list, and a styled paragraph
    pres.add_slide()
        .set_title("Quarterly Results")
        .add_text("Revenue grew 18% quarter over quarter.")
        .add_bullet_list(&["North America: +22%", "EU: +14%", "APAC: +9%"])
        .add_rich_text(&[
            Run::new("Highlight: ").bold(),
            Run::new("record EU quarter").italic().color("0070C0"),
        ]);

    // Slide 2: a title plus a positioned image
    let logo = std::fs::read("logo.png")?;
    pres.add_slide()
        .set_title("Logo")
        .add_image(logo, ImageFormat::Png, 914_400, 914_400, 2_743_200, 1_828_800);

    pres.save("results.pptx")?;
    Ok(())
}

Go

package main

import (
	"os"

	officeoxide "github.com/yfedoseev/office_oxide/go"
)

func main() {
	pres := officeoxide.NewPptxWriter()
	defer pres.Close()

	slide := pres.AddSlide() // uint32, 0-based index
	pres.SetSlideTitle(slide, "Quarterly Results")
	pres.AddSlideText(slide, "Revenue grew 18% quarter over quarter.")
	pres.AddSlideText(slide, "All regions exceeded target.")

	// Embed an image. format is "png", "jpeg"/"jpg", or "gif".
	// x, y, cx, cy are in EMU (914400 = 1 inch).
	logo, _ := os.ReadFile("logo.png")
	pres.AddSlideImage(slide, logo, "png", 914400, 914400, 2743200, 1828800)

	if err := pres.Save("results.pptx"); err != nil {
		panic(err)
	}

	// Or export to bytes:
	data, err := pres.ToBytes()
	if err != nil {
		panic(err)
	}
	_ = os.WriteFile("results-copy.pptx", data, 0o644)
}

C#

using OfficeOxide;

using var pres = new PptxWriter();

uint slide = pres.AddSlide(); // 0-based index
pres.SetSlideTitle(slide, "Quarterly Results");
pres.AddSlideText(slide, "Revenue grew 18% quarter over quarter.");
pres.AddSlideText(slide, "All regions exceeded target.");

// Embed an image. format is "png", "jpeg"/"jpg", or "gif".
// x, y, cx, cy are in EMU (914400 = 1 inch).
byte[] logo = File.ReadAllBytes("logo.png");
pres.AddSlideImage(slide, logo, "png", 914400, 914400, 2743200, 1828800);

pres.Save("results.pptx");

// Or export to a byte[]:
byte[] data = pres.ToBytes();
File.WriteAllBytes("results-copy.pptx", data);

JavaScript

import { readFileSync, writeFileSync } from 'node:fs';
import { PptxWriter } from 'office-oxide';

const pres = new PptxWriter();

const slide = pres.addSlide(); // 0-based index
pres.setSlideTitle(slide, 'Quarterly Results');
pres.addSlideText(slide, 'Revenue grew 18% quarter over quarter.');
pres.addSlideText(slide, 'All regions exceeded target.');

// Embed an image. format is 'png', 'jpeg'/'jpg', or 'gif'.
// x, y, cx, cy are in EMU (914400 = 1 inch).
const logo = readFileSync('logo.png');
pres.addSlideImage(slide, logo, 'png', 914400, 914400, 2743200, 1828800);

pres.save('results.pptx');

// Or export to a Buffer:
const data = pres.toBytes();
writeFileSync('results-copy.pptx', data);

pres.close(); // release the native handle

C

#include "office_oxide.h"

OfficePptxWriterHandle *p = office_pptx_writer_new();

uint32_t slide = office_pptx_writer_add_slide(p); /* 0-based index */
office_pptx_slide_set_title(p, slide, "Quarterly Results");
office_pptx_slide_add_text(p, slide, "Revenue grew 18% quarter over quarter.");
office_pptx_slide_add_text(p, slide, "All regions exceeded target.");

/* Embed an image: raw png/jpeg/gif bytes; x,y,cx,cy in EMU (914400 = 1 inch). */
/* read logo.png into img_data / img_len yourself, then: */
office_pptx_slide_add_image(p, slide, img_data, img_len, "png",
                            914400, 914400, 2743200, 1828800);

int err = 0;
office_pptx_writer_save(p, "results.pptx", &err);

/* Or export to bytes: */
size_t out_len = 0;
uint8_t *buf = office_pptx_writer_to_bytes(p, &out_len, &err);
if (buf) { /* stream buf[0..out_len] */ office_oxide_free_bytes(buf, out_len); }
office_pptx_writer_free(p);

各语言绑定签名

Rust crate 提供了更丰富的流式构建器。add_slide() 返回一个借用的 &mut SlideData 句柄,其方法可以链式调用,因此整张幻灯片可以在一个表达式中完成配置。除纯文本外,还支持样式化 Run、项目符号列表和自由浮动文本框。

PptxWriter 上的关键 Rust 签名:

pub fn new() -> Self
pub fn set_presentation_size(&mut self, cx: u64, cy: u64) -> &mut Self
pub fn add_slide(&mut self) -> &mut SlideData
pub fn save(&self, path: impl AsRef<Path>) -> Result<()>
pub fn write_to<W: Write + Seek>(&self, writer: W) -> Result<()>

add_slide 返回的 &mut SlideData 上的方法:

pub fn set_title(&mut self, title: &str) -> &mut Self
pub fn add_text(&mut self, text: &str) -> &mut Self
pub fn add_rich_text(&mut self, runs: &[Run]) -> &mut Self
pub fn add_bullet_list(&mut self, items: &[&str]) -> &mut Self
pub fn add_text_box(&mut self, text: &str, x: i64, y: i64, cx: i64, cy: i64) -> &mut Self
pub fn add_image(&mut self, data: Vec<u8>, format: ImageFormat, x: i64, y: i64, cx: u64, cy: u64) -> &mut Self

Run 是一个构建器:Run::new(text) 后可链式调用 .bold().italic().underline().strikethrough().color("FF0000")(6位十六进制,无 #)、.font_size(18.0).font("Calibri")。内存输出请将 write_to 与任意 Write + Seek 目标(如 Cursor<Vec<u8>>)配合使用。当你偏好其他绑定使用的扁平风格时,也可使用基于索引的镜像 API(add_slide_get_indexslide_set_titleslide_add_textslide_add_image)。

Go 签名:NewPptxWriter() *PptxWriterSetPresentationSize(cx, cy uint64)AddSlide() uint32SetSlideTitle(slide uint32, title string)AddSlideText(slide uint32, text string)AddSlideImage(slide uint32, data []byte, format string, x, y int64, cx, cy uint64)Save(path string) errorToBytes() ([]byte, error)Close()。始终使用 defer pres.Close() 释放原生句柄。

C# 签名:PptxWriter()SetPresentationSize(ulong cx, ulong cy)AddSlide() -> uintSetSlideTitle(uint slide, string title)AddSlideText(uint slide, string text)AddSlideImage(uint slide, byte[] data, string format, long x, long y, ulong cx, ulong cy)Save(string path)ToBytes() -> byte[]PptxWriter 实现了 IDisposable,请用 using 包裹。

JavaScript 签名:new PptxWriter()setPresentationSize(cx, cy)addSlide()setSlideTitle(slide, title)addSlideText(slide, text)addSlideImage(slide, data, format, x, y, cx, cy)save(path)toBytes()close()format 参数为 'png' | 'jpeg' | 'jpg' | 'gif'dataUint8ArrayBuffer

C 签名:office_pptx_writer_new()office_pptx_writer_set_presentation_size(p, cx, cy)office_pptx_writer_add_slide(p) -> uint32_toffice_pptx_slide_set_title(p, slide, title)office_pptx_slide_add_text(p, slide, text)office_pptx_slide_add_image(p, slide, data, len, format, x, y, cx, cy)office_pptx_writer_save(p, path, &err)office_pptx_writer_to_bytes(p, &out_len, &err) -> uint8_t*office_pptx_writer_free(p)。返回的字节缓冲区须用 office_oxide_free_bytes(ptr, len) 释放。

样式化文本段、项目符号与文本框(Rust)

扁平绑定(Python/Go/C#/JS)提供标题、正文段落和图片 — 这是 AI 生成幻灯片的基础构建块。若需更丰富的单页布局,Rust 的 SlideData 构建器额外支持项目符号列表、样式化 Run 和绝对定位文本框:

use office_oxide::pptx::write::{PptxWriter, Run};

fn main() -> office_oxide::Result<()> {
    let mut pres = PptxWriter::new();

    pres.add_slide()
        .set_title("Free-floating text boxes")
        .add_text("The body placeholder holds regular paragraphs.")
        // A text box at 1in × 4in, sized 6in × 0.75in (EMU)
        .add_text_box("Pinned note", 914_400, 3_657_600, 5_486_400, 685_800)
        // A bullet list rendered in the body placeholder
        .add_bullet_list(&["Fast", "Safe", "Dependency-free"]);

    pres.write_to(std::io::Cursor::new(Vec::new()))?; // in-memory
    Ok(())
}

常见问题

幻灯片是通过句柄还是索引来访问的? 两种方式都支持。在 Rust 中,add_slide() 返回一个 &mut SlideData 句柄,其方法(set_titleadd_text 等)可以流式链式调用。在 Python、Go、C# 和 JavaScript 中,add_slide() 返回幻灯片从 0 开始的索引,将其传递给 set_slide_titleadd_slide_textadd_slide_image

位置和尺寸使用什么单位? EMU(English Metric Units)。914400 EMU = 1 英寸360000 EMU ≈ 1 厘米。默认画布为 12192000 × 6858000(16:9 宽屏)。在添加幻灯片前调用 set_presentation_size(cx, cy) 进行修改 — 例如 4:3 为 9144000 × 6858000

需要安装 PowerPoint 或任何 Microsoft 运行时吗? 不需要。PptxWriter 完全用 Rust 生成合规的 OOXML .pptx ZIP。没有 COM 自动化、没有 JVM、没有系统依赖 — 与在 806 个真实文件上平均 0.7ms、通过率 98.4% 读取 PPTX 的引擎完全相同。

如何获取字节流而非写入磁盘? 调用 to_bytes()(Python/C#/JS)或 ToBytes()(Go),返回完整的 .pptx 字节缓冲区。在 Rust 中,使用 write_to(writer) 配合任意 Write + Seek 目标(如 Cursor<Vec<u8>>)。这非常适合 HTTP 响应和对象存储上传。

可以嵌入哪些图片格式? PNG、JPEG("jpeg""jpg")和 GIF。将原始图片字节、format 字符串以及 EMU 单位的位置/尺寸传递给 add_slide_image(Rust 中为接受 ImageFormat 枚举的 add_image)。

参阅