diff --git a/CHANGELOG.md b/CHANGELOG.md index a9060f3..02cd7c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # CHANGELOG +## v1.3.0 (2026-05-12) + +- 核心架构重构:引入统一的 `Document` 接口,支持 **Open/Create/Save** 的极简 API 范式。 +- 自动识别:`office.Open(path)` 可根据后缀名自动选择解析器。 +- 标准化载体:所有文档类型均支持 `ToJSON()` 和 `ToMarkdown()`,完美对齐 AI 工作流。 +- 零摩擦设计:Excel 导出自动处理 Sheet 和列扩展;PDF/Word/PPT 提取自动结构化。 + ## v1.2.0 (2026-05-12) - 新增 Excel 与 JSON 的双向转换支持 (`ToJSON`, `FromJSON`)。 diff --git a/README.md b/README.md index fb802e8..acbc768 100644 --- a/README.md +++ b/README.md @@ -1,57 +1,76 @@ # office -极简、高效的 Go Office 文档处理库,符合 `@go` 设计哲学。支持将 Office 文档与 JSON/Markdown 等通用格式无缝转换,作为 AI 时代的数据桥梁。 +极简、高效的 Go Office 文档处理桥梁,符合 `@go` 设计哲学。 -## 特性 +## 核心设计 -- **统一 API**: 提供极简的 `Open`, `Save`, `ToJSON`, `ToMarkdown` 等操作。 -- **载体转换**: - - **Excel <-> JSON**: 自动将表格映射为对象数组,支持双向转换。 - - **Docx/Pptx/PDF -> Markdown**: 提取并转化为 AI 友好的 Markdown 格式。 -- **纯 Go 实现**: 无 CGo 依赖,极致的跨平台与部署性能。 -- **Excel 增强**: 自动处理工作表对齐,支持动态列扩展。 +`office` 包将所有复杂的文档格式(Excel, Word, PPT, PDF)抽象为统一的 `Document` 接口。你只需要关心 **数据载体** (JSON/Markdown),而不需要关心底层实现。 -## 快速开始 +- **结构化数据 (Excel)** <-> **JSON** +- **半结构化内容 (Word/PPT/PDF)** -> **Markdown** -### 安装 - -```bash -go get apigo.cc/go/office -``` - -### Excel <-> JSON (结构化数据载体) +## 统一 API ```go -// Excel 转 JSON -xls, _ := office.Open("data.xlsx") -jsonStr, _ := xls.ToJSON("Sheet1", "A1", "") +import "apigo.cc/go/office" -// JSON 转 Excel -newXls := office.New() -newXls.FromJSON("Sheet1", jsonStr, "A1", "") -newXls.Save("restored.xlsx") +// 1. 打开文档 (自动识别类型) +doc, _ := office.Open("data.xlsx") + +// 2. 转换为通用载体 +jsonStr := doc.ToJSON() // 适合 Excel +mdStr := doc.ToMarkdown() // 适合 Word/PPT/PDF + +// 3. 修改并保存 (Excel 支持数据回写) +// (对于 Excel,你可以强制断言获取更多方法) +if xls, ok := doc.(*office.Excel); ok { + xls.SetData("Sheet1", newData, "A1", "") +} +doc.Save("updated.xlsx") ``` -### Docx/Pptx/PDF -> Markdown (内容载体) +## 支持格式 + +| 格式 | 后缀 | `ToJSON` 表现 | `ToMarkdown` 表现 | `Save` 行为 | +| :--- | :--- | :--- | :--- | :--- | +| **Excel** | `.xlsx` | 返回第一个 Sheet 的对象数组 | 返回第一个 Sheet 的 MD 表格 | 保存为 Excel 文件 | +| **Word** | `.docx` | 返回 `{"metadata":..., "content":...}` | 返回提取的全文文本 | 保存为纯文本文件 | +| **PPT** | `.pptx` | 返回 `{"metadata":..., "content":...}` | 返回提取的全文文本 | 保存为纯文本文件 | +| **PDF** | `.pdf` | 返回 `{"metadata":..., "content":...}` | 返回提取的全文文本 | 保存为纯文本文件 | + +## 快速示例 + +### AI 友好型转换 ```go -// Word 转 Markdown -doc, _ := office.OpenDocx("contract.docx") -md, _ := doc.ToMarkdown() +// 一行代码将 PDF 转为 AI 易读的 Markdown +md, _ := office.Open("report.pdf").ToMarkdown() -// PDF 转 Markdown -pdf, _ := office.OpenPDF("report.pdf") -md, _ := pdf.ToMarkdown() +// 一行代码将 Excel 转为结构化 JSON +json, _ := office.Open("sales.xlsx").ToJSON() +``` + +### 极简读写 (Excel) + +```go +xls, _ := office.OpenExcel("config.xlsx") +data, _ := xls.GetData("Sheet1", "A1", "") +// ... 修改 data +xls.SetData("Sheet1", data, "A1", "") +xls.Save() ``` ## API 参考 -### Excel (JSON 转换) -- `ToJSON(sheetName string, start, end string) (string, error)` -- `FromJSON(sheetName string, jsonStr string, start, end string) error` -- `GetData(sheetName string, start, end string) ([]map[string]any, error)` -- `SetData(sheetName string, data []map[string]any, start, end string) error` +### 顶级函数 +- `Open(filename string, password ...string) (Document, error)` +- `Create(ext string) (Document, error)` -### Word/PPT/PDF (Markdown 提取) -- `ToMarkdown() (string, error)`: 将文档内容提取为 Markdown 格式字符串。 -- `Text() (string, error)`: 提取纯文本。 +### Document 接口 +- `ToJSON() string` +- `ToMarkdown() string` +- `Save(filename ...string) error` + +### 类型特定 (导出) +- `Excel`: 支持 `SetData`, `GetData`, `Sheets` 等。 +- `Docx`, `Pptx`, `PDF`: 支持 `Content` 和 `Metadata` 直接访问。 diff --git a/docx.go b/docx.go index 2db25c7..d797bdb 100644 --- a/docx.go +++ b/docx.go @@ -4,13 +4,16 @@ import ( "io" "os" + "apigo.cc/go/cast" "apigo.cc/go/file" "github.com/young2j/oxmltotext/docxtotext" ) -// Docx 封装了 Word 文档的读取操作。 +// Docx 封装了 Word 文档的读取与识别。 type Docx struct { filename string + Content string + Metadata map[string]any } // OpenDocx 打开一个 Word 文档 (.docx)。 @@ -18,18 +21,43 @@ func OpenDocx(filename string) (*Docx, error) { if !file.Exists(filename) { return nil, os.ErrNotExist } - return &Docx{filename: filename}, nil + d := &Docx{ + filename: filename, + Metadata: make(map[string]any), + } + + dp, err := docxtotext.Open(filename) + if err == nil { + defer dp.Close() + d.Content, _ = dp.ExtractTexts() + } + + return d, nil } -// Text 提取文档中的所有文本。 -func (d *Docx) Text() (string, error) { - dp, err := docxtotext.Open(d.filename) - if err != nil { - return "", err - } - defer dp.Close() +// ToJSON 返回包含元数据和内容的 JSON 字符串。 +func (d *Docx) ToJSON() string { + res, _ := cast.ToJSON(map[string]any{ + "metadata": d.Metadata, + "content": d.Content, + }) + return res +} - return dp.ExtractTexts() +// ToMarkdown 返回 Markdown 格式的内容。 +func (d *Docx) ToMarkdown() string { + return d.Content +} + +// Save 保存文档。目前主要支持保存提取后的文本。 +func (d *Docx) Save(filename ...string) error { + path := d.filename + if len(filename) > 0 && filename[0] != "" { + path = filename[0] + } + // 如果是原格式保存,目前仅作为文本保存或保持原样 + // 复杂的 Docx 写入暂未集成 + return file.Write(path, d.Content) } // ReadText 从 io.Reader 中读取并提取 Word 文本。 @@ -42,12 +70,3 @@ func (d *Docx) ReadText(r io.ReaderAt, size int64) (string, error) { return dp.ExtractTexts() } - -// ToMarkdown 将 Word 文档内容转换为 Markdown 格式。 -func (d *Docx) ToMarkdown() (string, error) { - text, err := d.Text() - if err != nil { - return "", err - } - return text, nil -} diff --git a/excel.go b/excel.go index a0d0035..b823f98 100644 --- a/excel.go +++ b/excel.go @@ -5,29 +5,29 @@ import ( "fmt" "regexp" "strconv" + "strings" "apigo.cc/go/cast" "apigo.cc/go/file" "github.com/xuri/excelize/v2" ) -// Excel 封装了 Excel 文件的核心操作,提供极简且高效的 API。 +// Excel 封装了 Excel 文件的核心操作。 type Excel struct { filename string password string excel *excelize.File } -// New 创建一个新的 Excel 对象。 -func New() *Excel { +// NewExcel 创建一个新的 Excel 对象。 +func NewExcel() *Excel { return &Excel{ excel: excelize.NewFile(), } } -// Open 打开一个现有的 Excel 文件。 -// 如果文件不存在,将创建一个新的 Excel 对象。 -func Open(filename string, password ...string) (*Excel, error) { +// OpenExcel 打开一个现有的 Excel 文件。 +func OpenExcel(filename string, password ...string) (*Excel, error) { pwd := "" if len(password) > 0 { pwd = password[0] @@ -52,7 +52,6 @@ func Open(filename string, password ...string) (*Excel, error) { } // Save 保存 Excel 文件。 -// 如果提供了 filename,将另存为该文件名。 func (xls *Excel) Save(filename ...string) error { if len(filename) > 0 && filename[0] != "" { xls.filename = filename[0] @@ -63,19 +62,51 @@ func (xls *Excel) Save(filename ...string) error { return xls.excel.SaveAs(xls.filename, excelize.Options{Password: xls.password}) } -// Bytes 将 Excel 内容写入字节切片。 -func (xls *Excel) Bytes() ([]byte, error) { - buf, err := xls.excel.WriteToBuffer() - if err != nil { - return nil, err +// ToJSON 将第一个工作表的数据转换为 JSON 字符串。 +func (xls *Excel) ToJSON() string { + sheets := xls.Sheets() + if len(sheets) == 0 { + return "[]" } - return buf.Bytes(), nil + data, _ := xls.GetData(sheets[0], "A1", "") + res, _ := cast.ToJSON(data) + return res } +// ToMarkdown 将第一个工作表的数据转换为 Markdown 表格。 +func (xls *Excel) ToMarkdown() string { + sheets := xls.Sheets() + if len(sheets) == 0 { + return "" + } + rows, _ := xls.Get(sheets[0], "A1", "") + if len(rows) == 0 { + return "" + } + + var sb strings.Builder + for i, row := range rows { + sb.WriteString("| ") + for _, col := range row { + sb.WriteString(cast.To[string](col)) + sb.WriteString(" | ") + } + sb.WriteString("\n") + if i == 0 { + // 分隔线 + sb.WriteString("|") + for range row { + sb.WriteString(" --- |") + } + sb.WriteString("\n") + } + } + return sb.String() +} + +// --- 基础操作方法 --- + // Set 设置指定单元格范围的值。 -// table: 二维数组,代表行和列。 -// start: 起始单元格 ID(如 "A1"),默认为 "A1"。 -// end: 结束单元格 ID(如 "C3"),用于限制写入范围。 func (xls *Excel) Set(sheetName string, table [][]any, start, end string) error { sheet := xls.getOrCreateSheet(sheetName) startX, startY := ParseCellID(start) @@ -127,7 +158,6 @@ func (xls *Excel) Get(sheetName string, start, end string) ([][]any, error) { break } - // 尝试根据单元格类型转换数据 cellID := MakeCellID(x, y) cellType, _ := xls.excel.GetCellType(sheet, cellID) @@ -140,9 +170,6 @@ func (xls *Excel) Get(sheetName string, start, end string) ([][]any, error) { } case excelize.CellTypeBool: rowData = append(rowData, cast.To[bool](v)) - case excelize.CellTypeDate: - // TODO: 更好的日期处理 - rowData = append(rowData, v) default: rowData = append(rowData, v) } @@ -152,53 +179,6 @@ func (xls *Excel) Get(sheetName string, start, end string) ([][]any, error) { return result, nil } -// RemoveSheet 删除工作表。 -func (xls *Excel) RemoveSheet(sheetName string) error { - return xls.excel.DeleteSheet(sheetName) -} - -// Sheets 返回所有工作表名称。 -func (xls *Excel) Sheets() []string { - return xls.excel.GetSheetList() -} - -// SetColWidth 设置列宽。 -func (xls *Excel) SetColWidth(sheetName string, startCol, endCol string, width float64) error { - return xls.excel.SetColWidth(xls.getOrCreateSheet(sheetName), startCol, endCol, width) -} - -// SetColWidths 批量设置列宽。 -func (xls *Excel) SetColWidths(sheetName string, widths []float64) error { - sheet := xls.getOrCreateSheet(sheetName) - for i, w := range widths { - col := MakeColID(i) - if err := xls.excel.SetColWidth(sheet, col, col, w); err != nil { - return err - } - } - return nil -} - -// SetCellStyle 设置单元格样式。 -func (xls *Excel) SetCellStyle(sheetName string, start, end string, styleID int) error { - return xls.excel.SetCellStyle(xls.getOrCreateSheet(sheetName), start, end, styleID) -} - -// MakeStyle 创建样式并返回样式 ID。 -func (xls *Excel) MakeStyle(style *excelize.Style) (int, error) { - return xls.excel.NewStyle(style) -} - -// SetPanes 设置冻结窗格。 -func (xls *Excel) SetPanes(sheetName string, panes *excelize.Panes) error { - return xls.excel.SetPanes(xls.getOrCreateSheet(sheetName), panes) -} - -// SetAutoFilter 设置自动筛选。 -func (xls *Excel) SetAutoFilter(sheetName string, start, end string, options []excelize.AutoFilterOptions) error { - return xls.excel.AutoFilter(xls.getOrCreateSheet(sheetName), start+":"+end, options) -} - // SetData 将对象列表写入 Excel。 func (xls *Excel) SetData(sheetName string, data []map[string]any, start, end string) error { table, err := xls.Get(sheetName, start, end) @@ -227,7 +207,6 @@ func (xls *Excel) SetData(sheetName string, data []map[string]any, start, end st idx = fieldNum fieldIndex[k] = idx fieldNum++ - // 扩展所有行以匹配新的列数 for r := range table { for len(table[r]) < fieldNum { table[r] = append(table[r], "") @@ -276,22 +255,9 @@ func (xls *Excel) GetData(sheetName string, start, end string) ([]map[string]any return data, nil } -// ToJSON 将指定工作表的数据转换为 JSON 字符串。 -func (xls *Excel) ToJSON(sheetName string, start, end string) (string, error) { - data, err := xls.GetData(sheetName, start, end) - if err != nil { - return "", err - } - return cast.ToJSON(data) -} - -// FromJSON 从 JSON 字符串加载数据到指定工作表。 -func (xls *Excel) FromJSON(sheetName string, jsonStr string, start, end string) error { - data, err := cast.FromJSON[[]map[string]any](jsonStr) - if err != nil { - return err - } - return xls.SetData(sheetName, data, start, end) +// Sheets 返回所有工作表名称。 +func (xls *Excel) Sheets() []string { + return xls.excel.GetSheetList() } // 辅助方法:获取或创建工作表 @@ -299,12 +265,10 @@ func (xls *Excel) getOrCreateSheet(name string) string { if name == "" { name = "Sheet1" } - // 如果是数字索引 if matched, _ := regexp.MatchString(`^\d+$`, name); matched { idx := cast.To[int](name) return xls.excel.GetSheetName(idx) } - idx, _ := xls.excel.GetSheetIndex(name) if idx == -1 { xls.excel.NewSheet(name) @@ -312,12 +276,12 @@ func (xls *Excel) getOrCreateSheet(name string) string { return name } -// MakeCellID 根据行列索引生成单元格 ID(如 0, 0 -> "A1")。 +// MakeCellID 生成单元格 ID。 func MakeCellID(col, row int) string { return MakeColID(col) + strconv.Itoa(row+1) } -// MakeColID 根据列索引生成列 ID(如 0 -> "A", 26 -> "AA")。 +// MakeColID 生成列 ID。 func MakeColID(col int) string { colName := "" for col >= 0 { @@ -327,13 +291,11 @@ func MakeColID(col int) string { return colName } -// ParseCellID 解析单元格 ID(如 "A1" -> 0, 0)。 +// ParseCellID 解析单元格 ID。 func ParseCellID(cell string) (col, row int) { if cell == "" { return 0, 0 } - - // 找到第一个数字的位置 numIdx := -1 for i, r := range cell { if r >= '0' && r <= '9' { @@ -341,15 +303,12 @@ func ParseCellID(cell string) (col, row int) { break } } - if numIdx == -1 { - // 只有字母,当做列处理 return parseCol(cell), 0 } - col = parseCol(cell[:numIdx]) row, _ = strconv.Atoi(cell[numIdx:]) - row-- // 转为 0 索引 + row-- if row < 0 { row = 0 } diff --git a/excel_test.go b/excel_test.go index 20ff8a6..f29d72f 100644 --- a/excel_test.go +++ b/excel_test.go @@ -1,6 +1,7 @@ package office import ( + "strings" "testing" "apigo.cc/go/file" @@ -10,7 +11,7 @@ func TestExcel_Basic(t *testing.T) { filename := "test_basic.xlsx" defer file.Remove(filename) - xls := New() + xls := NewExcel() table := [][]any{ {"Name", "Age", "City"}, {"Alice", 25, "New York"}, @@ -27,10 +28,11 @@ func TestExcel_Basic(t *testing.T) { t.Fatalf("Save failed: %v", err) } - xls2, err := Open(filename) + doc2, err := Open(filename) if err != nil { t.Fatalf("Open failed: %v", err) } + xls2 := doc2.(*Excel) data, err := xls2.Get("Sheet1", "A1", "C3") if err != nil { @@ -50,7 +52,7 @@ func TestExcel_Data(t *testing.T) { filename := "test_data.xlsx" defer file.Remove(filename) - xls := New() + xls := NewExcel() data := []map[string]any{ {"ID": 1, "Value": "A"}, {"ID": 2, "Value": "B"}, @@ -66,10 +68,11 @@ func TestExcel_Data(t *testing.T) { t.Fatalf("Save failed: %v", err) } - xls2, err := Open(filename) + doc2, err := Open(filename) if err != nil { t.Fatalf("Open failed: %v", err) } + xls2 := doc2.(*Excel) readData, err := xls2.GetData("Data", "A1", "") if err != nil { @@ -85,6 +88,37 @@ func TestExcel_Data(t *testing.T) { } } +func TestUnifiedAPI(t *testing.T) { + // Test Excel with Unified API + filename := "test_unified.xlsx" + defer file.Remove(filename) + + doc, err := Open(filename) + if err != nil { + t.Fatalf("Open failed: %v", err) + } + + if xls, ok := doc.(*Excel); ok { + xls.SetData("Sheet1", []map[string]any{{"Name": "Unified", "Value": 100}}, "A1", "") + } + + err = doc.Save() + if err != nil { + t.Fatalf("Save failed: %v", err) + } + + // Verify JSON/Markdown + jsonStr := doc.ToJSON() + if !strings.Contains(jsonStr, "Unified") { + t.Errorf("ToJSON failed: %s", jsonStr) + } + + mdStr := doc.ToMarkdown() + if !strings.Contains(mdStr, "Unified") { + t.Errorf("ToMarkdown failed: %s", mdStr) + } +} + func TestIDGeneration(t *testing.T) { tests := []struct { col, row int diff --git a/office.go b/office.go new file mode 100644 index 0000000..04a4480 --- /dev/null +++ b/office.go @@ -0,0 +1,48 @@ +package office + +import ( + "fmt" + "path/filepath" + "strings" +) + +// Document 定义了所有办公文档的统一行为。 +type Document interface { + // ToJSON 将文档转换为结构化 JSON 字符串。 + ToJSON() string + // ToMarkdown 将文档内容转换为 Markdown 格式。 + ToMarkdown() string + // Save 保存文档到文件系统。 + Save(filename ...string) error +} + +// Open 根据文件后缀名自动打开文档并返回标准接口。 +func Open(filename string, password ...string) (Document, error) { + ext := strings.ToLower(filepath.Ext(filename)) + switch ext { + case ".xlsx", ".xlsm": + return OpenExcel(filename, password...) + case ".docx": + return OpenDocx(filename) + case ".pptx": + return OpenPptx(filename) + case ".pdf": + return OpenPDF(filename) + default: + return nil, fmt.Errorf("unsupported file type: %s", ext) + } +} + +// Create 创建指定类型的空白文档。 +func Create(ext string) (Document, error) { + ext = strings.ToLower(strings.TrimPrefix(ext, ".")) + switch ext { + case "xlsx": + return NewExcel(), nil + case "docx": + // TODO: 支持创建 Word + return nil, fmt.Errorf("creating docx not supported yet") + default: + return nil, fmt.Errorf("unsupported creation type: %s", ext) + } +} diff --git a/pdf.go b/pdf.go index 6b3e984..8b8df8c 100644 --- a/pdf.go +++ b/pdf.go @@ -6,13 +6,16 @@ import ( "os" "strings" + "apigo.cc/go/cast" "apigo.cc/go/file" "github.com/dslipak/pdf" ) -// PDF 封装了 PDF 文档的读取操作。 +// PDF 封装了 PDF 文档的读取与识别。 type PDF struct { filename string + Content string + Metadata map[string]any } // OpenPDF 打开一个 PDF 文档。 @@ -20,57 +23,54 @@ func OpenPDF(filename string) (*PDF, error) { if !file.Exists(filename) { return nil, os.ErrNotExist } - return &PDF{filename: filename}, nil -} - -// Text 提取 PDF 中的所有文本。 -func (p *PDF) Text() (string, error) { - f, err := pdf.Open(p.filename) - if err != nil { - return "", err + p := &PDF{ + filename: filename, + Metadata: make(map[string]any), } - var b bytes.Buffer - t, err := f.GetPlainText() - if err != nil { - return "", err - } - - _, err = io.Copy(&b, t) - if err != nil { - return "", err - } - - return b.String(), nil -} - -// ToMarkdown 将 PDF 内容转换为 Markdown 格式。 -func (p *PDF) ToMarkdown() (string, error) { - text, err := p.Text() - if err != nil { - return "", err - } - return text, nil -} - -// Info 获取 PDF 的元数据。 -func (p *PDF) Info() map[string]any { - f, err := pdf.Open(p.filename) - if err != nil { - return nil - } - - info := make(map[string]any) - trailer := f.Trailer() - infoDict := trailer.Key("Info") - if !infoDict.IsNull() { - for _, field := range infoDict.Keys() { - val := infoDict.Key(field).Text() - if val != "" { - info[strings.ToLower(field)] = val + f, err := pdf.Open(filename) + if err == nil { + p.Metadata["pages"] = f.NumPage() + trailer := f.Trailer() + infoDict := trailer.Key("Info") + if !infoDict.IsNull() { + for _, field := range infoDict.Keys() { + val := infoDict.Key(field).Text() + if val != "" { + p.Metadata[strings.ToLower(field)] = val + } } } + + var b bytes.Buffer + if t, err := f.GetPlainText(); err == nil { + io.Copy(&b, t) + p.Content = b.String() + } } - info["pages"] = f.NumPage() - return info + + return p, nil +} + +// ToJSON 返回结构化 JSON。 +func (p *PDF) ToJSON() string { + res, _ := cast.ToJSON(map[string]any{ + "metadata": p.Metadata, + "content": p.Content, + }) + return res +} + +// ToMarkdown 返回 Markdown。 +func (p *PDF) ToMarkdown() string { + return p.Content +} + +// Save 保存(目前保存为提取后的文本)。 +func (p *PDF) Save(filename ...string) error { + path := p.filename + if len(filename) > 0 && filename[0] != "" { + path = filename[0] + } + return file.Write(path, p.Content) } diff --git a/pptx.go b/pptx.go index 6769a9c..c219247 100644 --- a/pptx.go +++ b/pptx.go @@ -4,13 +4,16 @@ import ( "io" "os" + "apigo.cc/go/cast" "apigo.cc/go/file" "github.com/young2j/oxmltotext/pptxtotext" ) -// Pptx 封装了 PowerPoint 文档的读取操作。 +// Pptx 封装了 PowerPoint 文档的读取与识别。 type Pptx struct { filename string + Content string + Metadata map[string]any } // OpenPptx 打开一个 PowerPoint 文档 (.pptx)。 @@ -18,18 +21,41 @@ func OpenPptx(filename string) (*Pptx, error) { if !file.Exists(filename) { return nil, os.ErrNotExist } - return &Pptx{filename: filename}, nil + p := &Pptx{ + filename: filename, + Metadata: make(map[string]any), + } + + pp, err := pptxtotext.Open(filename) + if err == nil { + defer pp.Close() + p.Content, _ = pp.ExtractTexts() + } + + return p, nil } -// Text 提取文档中的所有文本。 -func (p *Pptx) Text() (string, error) { - pp, err := pptxtotext.Open(p.filename) - if err != nil { - return "", err - } - defer pp.Close() +// ToJSON 返回结构化 JSON。 +func (p *Pptx) ToJSON() string { + res, _ := cast.ToJSON(map[string]any{ + "metadata": p.Metadata, + "content": p.Content, + }) + return res +} - return pp.ExtractTexts() +// ToMarkdown 返回 Markdown。 +func (p *Pptx) ToMarkdown() string { + return p.Content +} + +// Save 保存文档(目前保存为提取后的文本)。 +func (p *Pptx) Save(filename ...string) error { + path := p.filename + if len(filename) > 0 && filename[0] != "" { + path = filename[0] + } + return file.Write(path, p.Content) } // ReadText 从 io.Reader 中读取并提取 PPT 文本。 @@ -42,12 +68,3 @@ func (p *Pptx) ReadText(r io.ReaderAt, size int64) (string, error) { return pp.ExtractTexts() } - -// ToMarkdown 将 PowerPoint 文档内容转换为 Markdown 格式。 -func (p *Pptx) ToMarkdown() (string, error) { - text, err := p.Text() - if err != nil { - return "", err - } - return text, nil -}