Complete Unified API refactoring (v1.3.0)

This commit is contained in:
Star 2026-05-12 13:05:16 +08:00
parent 3d74c78d8b
commit baf0b3835d
8 changed files with 327 additions and 224 deletions

View File

@ -1,5 +1,12 @@
# CHANGELOG # 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) ## v1.2.0 (2026-05-12)
- 新增 Excel 与 JSON 的双向转换支持 (`ToJSON`, `FromJSON`)。 - 新增 Excel 与 JSON 的双向转换支持 (`ToJSON`, `FromJSON`)。

View File

@ -1,57 +1,76 @@
# office # office
极简、高效的 Go Office 文档处理库,符合 `@go` 设计哲学。支持将 Office 文档与 JSON/Markdown 等通用格式无缝转换,作为 AI 时代的数据桥梁 极简、高效的 Go Office 文档处理桥梁,符合 `@go` 设计哲学
## 特性 ## 核心设计
- **统一 API**: 提供极简的 `Open`, `Save`, `ToJSON`, `ToMarkdown` 等操作。 `office` 包将所有复杂的文档格式Excel, Word, PPT, PDF抽象为统一的 `Document` 接口。你只需要关心 **数据载体** (JSON/Markdown),而不需要关心底层实现。
- **载体转换**:
- **Excel <-> JSON**: 自动将表格映射为对象数组,支持双向转换。
- **Docx/Pptx/PDF -> Markdown**: 提取并转化为 AI 友好的 Markdown 格式。
- **纯 Go 实现**: 无 CGo 依赖,极致的跨平台与部署性能。
- **Excel 增强**: 自动处理工作表对齐,支持动态列扩展。
## 快速开始 - **结构化数据 (Excel)** <-> **JSON**
- **半结构化内容 (Word/PPT/PDF)** -> **Markdown**
### 安装 ## 统一 API
```bash
go get apigo.cc/go/office
```
### Excel <-> JSON (结构化数据载体)
```go ```go
// Excel 转 JSON import "apigo.cc/go/office"
xls, _ := office.Open("data.xlsx")
jsonStr, _ := xls.ToJSON("Sheet1", "A1", "")
// JSON 转 Excel // 1. 打开文档 (自动识别类型)
newXls := office.New() doc, _ := office.Open("data.xlsx")
newXls.FromJSON("Sheet1", jsonStr, "A1", "")
newXls.Save("restored.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 ```go
// Word 转 Markdown // 一行代码将 PDF 转为 AI 易读的 Markdown
doc, _ := office.OpenDocx("contract.docx") md, _ := office.Open("report.pdf").ToMarkdown()
md, _ := doc.ToMarkdown()
// PDF 转 Markdown // 一行代码将 Excel 转为结构化 JSON
pdf, _ := office.OpenPDF("report.pdf") json, _ := office.Open("sales.xlsx").ToJSON()
md, _ := pdf.ToMarkdown() ```
### 极简读写 (Excel)
```go
xls, _ := office.OpenExcel("config.xlsx")
data, _ := xls.GetData("Sheet1", "A1", "")
// ... 修改 data
xls.SetData("Sheet1", data, "A1", "")
xls.Save()
``` ```
## API 参考 ## API 参考
### Excel (JSON 转换) ### 顶级函数
- `ToJSON(sheetName string, start, end string) (string, error)` - `Open(filename string, password ...string) (Document, error)`
- `FromJSON(sheetName string, jsonStr string, start, end string) error` - `Create(ext string) (Document, error)`
- `GetData(sheetName string, start, end string) ([]map[string]any, error)`
- `SetData(sheetName string, data []map[string]any, start, end string) error`
### Word/PPT/PDF (Markdown 提取) ### Document 接口
- `ToMarkdown() (string, error)`: 将文档内容提取为 Markdown 格式字符串。 - `ToJSON() string`
- `Text() (string, error)`: 提取纯文本。 - `ToMarkdown() string`
- `Save(filename ...string) error`
### 类型特定 (导出)
- `Excel`: 支持 `SetData`, `GetData`, `Sheets` 等。
- `Docx`, `Pptx`, `PDF`: 支持 `Content``Metadata` 直接访问。

57
docx.go
View File

@ -4,13 +4,16 @@ import (
"io" "io"
"os" "os"
"apigo.cc/go/cast"
"apigo.cc/go/file" "apigo.cc/go/file"
"github.com/young2j/oxmltotext/docxtotext" "github.com/young2j/oxmltotext/docxtotext"
) )
// Docx 封装了 Word 文档的读取操作 // Docx 封装了 Word 文档的读取与识别
type Docx struct { type Docx struct {
filename string filename string
Content string
Metadata map[string]any
} }
// OpenDocx 打开一个 Word 文档 (.docx)。 // OpenDocx 打开一个 Word 文档 (.docx)。
@ -18,18 +21,43 @@ func OpenDocx(filename string) (*Docx, error) {
if !file.Exists(filename) { if !file.Exists(filename) {
return nil, os.ErrNotExist 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 提取文档中的所有文本。 // ToJSON 返回包含元数据和内容的 JSON 字符串。
func (d *Docx) Text() (string, error) { func (d *Docx) ToJSON() string {
dp, err := docxtotext.Open(d.filename) res, _ := cast.ToJSON(map[string]any{
if err != nil { "metadata": d.Metadata,
return "", err "content": d.Content,
} })
defer dp.Close() 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 文本。 // ReadText 从 io.Reader 中读取并提取 Word 文本。
@ -42,12 +70,3 @@ func (d *Docx) ReadText(r io.ReaderAt, size int64) (string, error) {
return dp.ExtractTexts() return dp.ExtractTexts()
} }
// ToMarkdown 将 Word 文档内容转换为 Markdown 格式。
func (d *Docx) ToMarkdown() (string, error) {
text, err := d.Text()
if err != nil {
return "", err
}
return text, nil
}

149
excel.go
View File

@ -5,29 +5,29 @@ import (
"fmt" "fmt"
"regexp" "regexp"
"strconv" "strconv"
"strings"
"apigo.cc/go/cast" "apigo.cc/go/cast"
"apigo.cc/go/file" "apigo.cc/go/file"
"github.com/xuri/excelize/v2" "github.com/xuri/excelize/v2"
) )
// Excel 封装了 Excel 文件的核心操作,提供极简且高效的 API // Excel 封装了 Excel 文件的核心操作
type Excel struct { type Excel struct {
filename string filename string
password string password string
excel *excelize.File excel *excelize.File
} }
// New 创建一个新的 Excel 对象。 // NewExcel 创建一个新的 Excel 对象。
func New() *Excel { func NewExcel() *Excel {
return &Excel{ return &Excel{
excel: excelize.NewFile(), excel: excelize.NewFile(),
} }
} }
// Open 打开一个现有的 Excel 文件。 // OpenExcel 打开一个现有的 Excel 文件。
// 如果文件不存在,将创建一个新的 Excel 对象。 func OpenExcel(filename string, password ...string) (*Excel, error) {
func Open(filename string, password ...string) (*Excel, error) {
pwd := "" pwd := ""
if len(password) > 0 { if len(password) > 0 {
pwd = password[0] pwd = password[0]
@ -52,7 +52,6 @@ func Open(filename string, password ...string) (*Excel, error) {
} }
// Save 保存 Excel 文件。 // Save 保存 Excel 文件。
// 如果提供了 filename将另存为该文件名。
func (xls *Excel) Save(filename ...string) error { func (xls *Excel) Save(filename ...string) error {
if len(filename) > 0 && filename[0] != "" { if len(filename) > 0 && filename[0] != "" {
xls.filename = 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}) return xls.excel.SaveAs(xls.filename, excelize.Options{Password: xls.password})
} }
// Bytes 将 Excel 内容写入字节切片 // ToJSON 将第一个工作表的数据转换为 JSON 字符串
func (xls *Excel) Bytes() ([]byte, error) { func (xls *Excel) ToJSON() string {
buf, err := xls.excel.WriteToBuffer() sheets := xls.Sheets()
if err != nil { if len(sheets) == 0 {
return nil, err 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 设置指定单元格范围的值。 // Set 设置指定单元格范围的值。
// table: 二维数组,代表行和列。
// start: 起始单元格 ID如 "A1"),默认为 "A1"。
// end: 结束单元格 ID如 "C3"),用于限制写入范围。
func (xls *Excel) Set(sheetName string, table [][]any, start, end string) error { func (xls *Excel) Set(sheetName string, table [][]any, start, end string) error {
sheet := xls.getOrCreateSheet(sheetName) sheet := xls.getOrCreateSheet(sheetName)
startX, startY := ParseCellID(start) startX, startY := ParseCellID(start)
@ -127,7 +158,6 @@ func (xls *Excel) Get(sheetName string, start, end string) ([][]any, error) {
break break
} }
// 尝试根据单元格类型转换数据
cellID := MakeCellID(x, y) cellID := MakeCellID(x, y)
cellType, _ := xls.excel.GetCellType(sheet, cellID) cellType, _ := xls.excel.GetCellType(sheet, cellID)
@ -140,9 +170,6 @@ func (xls *Excel) Get(sheetName string, start, end string) ([][]any, error) {
} }
case excelize.CellTypeBool: case excelize.CellTypeBool:
rowData = append(rowData, cast.To[bool](v)) rowData = append(rowData, cast.To[bool](v))
case excelize.CellTypeDate:
// TODO: 更好的日期处理
rowData = append(rowData, v)
default: default:
rowData = append(rowData, v) rowData = append(rowData, v)
} }
@ -152,53 +179,6 @@ func (xls *Excel) Get(sheetName string, start, end string) ([][]any, error) {
return result, nil 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。 // SetData 将对象列表写入 Excel。
func (xls *Excel) SetData(sheetName string, data []map[string]any, start, end string) error { func (xls *Excel) SetData(sheetName string, data []map[string]any, start, end string) error {
table, err := xls.Get(sheetName, start, end) 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 idx = fieldNum
fieldIndex[k] = idx fieldIndex[k] = idx
fieldNum++ fieldNum++
// 扩展所有行以匹配新的列数
for r := range table { for r := range table {
for len(table[r]) < fieldNum { for len(table[r]) < fieldNum {
table[r] = append(table[r], "") table[r] = append(table[r], "")
@ -276,22 +255,9 @@ func (xls *Excel) GetData(sheetName string, start, end string) ([]map[string]any
return data, nil return data, nil
} }
// ToJSON 将指定工作表的数据转换为 JSON 字符串。 // Sheets 返回所有工作表名称。
func (xls *Excel) ToJSON(sheetName string, start, end string) (string, error) { func (xls *Excel) Sheets() []string {
data, err := xls.GetData(sheetName, start, end) return xls.excel.GetSheetList()
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)
} }
// 辅助方法:获取或创建工作表 // 辅助方法:获取或创建工作表
@ -299,12 +265,10 @@ func (xls *Excel) getOrCreateSheet(name string) string {
if name == "" { if name == "" {
name = "Sheet1" name = "Sheet1"
} }
// 如果是数字索引
if matched, _ := regexp.MatchString(`^\d+$`, name); matched { if matched, _ := regexp.MatchString(`^\d+$`, name); matched {
idx := cast.To[int](name) idx := cast.To[int](name)
return xls.excel.GetSheetName(idx) return xls.excel.GetSheetName(idx)
} }
idx, _ := xls.excel.GetSheetIndex(name) idx, _ := xls.excel.GetSheetIndex(name)
if idx == -1 { if idx == -1 {
xls.excel.NewSheet(name) xls.excel.NewSheet(name)
@ -312,12 +276,12 @@ func (xls *Excel) getOrCreateSheet(name string) string {
return name return name
} }
// MakeCellID 根据行列索引生成单元格 ID(如 0, 0 -> "A1" // MakeCellID 生成单元格 ID。
func MakeCellID(col, row int) string { func MakeCellID(col, row int) string {
return MakeColID(col) + strconv.Itoa(row+1) return MakeColID(col) + strconv.Itoa(row+1)
} }
// MakeColID 根据列索引生成列 ID(如 0 -> "A", 26 -> "AA" // MakeColID 生成列 ID。
func MakeColID(col int) string { func MakeColID(col int) string {
colName := "" colName := ""
for col >= 0 { for col >= 0 {
@ -327,13 +291,11 @@ func MakeColID(col int) string {
return colName return colName
} }
// ParseCellID 解析单元格 ID(如 "A1" -> 0, 0 // ParseCellID 解析单元格 ID
func ParseCellID(cell string) (col, row int) { func ParseCellID(cell string) (col, row int) {
if cell == "" { if cell == "" {
return 0, 0 return 0, 0
} }
// 找到第一个数字的位置
numIdx := -1 numIdx := -1
for i, r := range cell { for i, r := range cell {
if r >= '0' && r <= '9' { if r >= '0' && r <= '9' {
@ -341,15 +303,12 @@ func ParseCellID(cell string) (col, row int) {
break break
} }
} }
if numIdx == -1 { if numIdx == -1 {
// 只有字母,当做列处理
return parseCol(cell), 0 return parseCol(cell), 0
} }
col = parseCol(cell[:numIdx]) col = parseCol(cell[:numIdx])
row, _ = strconv.Atoi(cell[numIdx:]) row, _ = strconv.Atoi(cell[numIdx:])
row-- // 转为 0 索引 row--
if row < 0 { if row < 0 {
row = 0 row = 0
} }

View File

@ -1,6 +1,7 @@
package office package office
import ( import (
"strings"
"testing" "testing"
"apigo.cc/go/file" "apigo.cc/go/file"
@ -10,7 +11,7 @@ func TestExcel_Basic(t *testing.T) {
filename := "test_basic.xlsx" filename := "test_basic.xlsx"
defer file.Remove(filename) defer file.Remove(filename)
xls := New() xls := NewExcel()
table := [][]any{ table := [][]any{
{"Name", "Age", "City"}, {"Name", "Age", "City"},
{"Alice", 25, "New York"}, {"Alice", 25, "New York"},
@ -27,10 +28,11 @@ func TestExcel_Basic(t *testing.T) {
t.Fatalf("Save failed: %v", err) t.Fatalf("Save failed: %v", err)
} }
xls2, err := Open(filename) doc2, err := Open(filename)
if err != nil { if err != nil {
t.Fatalf("Open failed: %v", err) t.Fatalf("Open failed: %v", err)
} }
xls2 := doc2.(*Excel)
data, err := xls2.Get("Sheet1", "A1", "C3") data, err := xls2.Get("Sheet1", "A1", "C3")
if err != nil { if err != nil {
@ -50,7 +52,7 @@ func TestExcel_Data(t *testing.T) {
filename := "test_data.xlsx" filename := "test_data.xlsx"
defer file.Remove(filename) defer file.Remove(filename)
xls := New() xls := NewExcel()
data := []map[string]any{ data := []map[string]any{
{"ID": 1, "Value": "A"}, {"ID": 1, "Value": "A"},
{"ID": 2, "Value": "B"}, {"ID": 2, "Value": "B"},
@ -66,10 +68,11 @@ func TestExcel_Data(t *testing.T) {
t.Fatalf("Save failed: %v", err) t.Fatalf("Save failed: %v", err)
} }
xls2, err := Open(filename) doc2, err := Open(filename)
if err != nil { if err != nil {
t.Fatalf("Open failed: %v", err) t.Fatalf("Open failed: %v", err)
} }
xls2 := doc2.(*Excel)
readData, err := xls2.GetData("Data", "A1", "") readData, err := xls2.GetData("Data", "A1", "")
if err != nil { 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) { func TestIDGeneration(t *testing.T) {
tests := []struct { tests := []struct {
col, row int col, row int

48
office.go Normal file
View File

@ -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)
}
}

86
pdf.go
View File

@ -6,13 +6,16 @@ import (
"os" "os"
"strings" "strings"
"apigo.cc/go/cast"
"apigo.cc/go/file" "apigo.cc/go/file"
"github.com/dslipak/pdf" "github.com/dslipak/pdf"
) )
// PDF 封装了 PDF 文档的读取操作 // PDF 封装了 PDF 文档的读取与识别
type PDF struct { type PDF struct {
filename string filename string
Content string
Metadata map[string]any
} }
// OpenPDF 打开一个 PDF 文档。 // OpenPDF 打开一个 PDF 文档。
@ -20,57 +23,54 @@ func OpenPDF(filename string) (*PDF, error) {
if !file.Exists(filename) { if !file.Exists(filename) {
return nil, os.ErrNotExist return nil, os.ErrNotExist
} }
return &PDF{filename: filename}, nil p := &PDF{
} filename: filename,
Metadata: make(map[string]any),
// Text 提取 PDF 中的所有文本。
func (p *PDF) Text() (string, error) {
f, err := pdf.Open(p.filename)
if err != nil {
return "", err
} }
var b bytes.Buffer f, err := pdf.Open(filename)
t, err := f.GetPlainText() if err == nil {
if err != nil { p.Metadata["pages"] = f.NumPage()
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() trailer := f.Trailer()
infoDict := trailer.Key("Info") infoDict := trailer.Key("Info")
if !infoDict.IsNull() { if !infoDict.IsNull() {
for _, field := range infoDict.Keys() { for _, field := range infoDict.Keys() {
val := infoDict.Key(field).Text() val := infoDict.Key(field).Text()
if val != "" { if val != "" {
info[strings.ToLower(field)] = val p.Metadata[strings.ToLower(field)] = val
} }
} }
} }
info["pages"] = f.NumPage()
return info var b bytes.Buffer
if t, err := f.GetPlainText(); err == nil {
io.Copy(&b, t)
p.Content = b.String()
}
}
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)
} }

55
pptx.go
View File

@ -4,13 +4,16 @@ import (
"io" "io"
"os" "os"
"apigo.cc/go/cast"
"apigo.cc/go/file" "apigo.cc/go/file"
"github.com/young2j/oxmltotext/pptxtotext" "github.com/young2j/oxmltotext/pptxtotext"
) )
// Pptx 封装了 PowerPoint 文档的读取操作 // Pptx 封装了 PowerPoint 文档的读取与识别
type Pptx struct { type Pptx struct {
filename string filename string
Content string
Metadata map[string]any
} }
// OpenPptx 打开一个 PowerPoint 文档 (.pptx)。 // OpenPptx 打开一个 PowerPoint 文档 (.pptx)。
@ -18,18 +21,41 @@ func OpenPptx(filename string) (*Pptx, error) {
if !file.Exists(filename) { if !file.Exists(filename) {
return nil, os.ErrNotExist 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 提取文档中的所有文本。 // ToJSON 返回结构化 JSON。
func (p *Pptx) Text() (string, error) { func (p *Pptx) ToJSON() string {
pp, err := pptxtotext.Open(p.filename) res, _ := cast.ToJSON(map[string]any{
if err != nil { "metadata": p.Metadata,
return "", err "content": p.Content,
} })
defer pp.Close() 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 文本。 // ReadText 从 io.Reader 中读取并提取 PPT 文本。
@ -42,12 +68,3 @@ func (p *Pptx) ReadText(r io.ReaderAt, size int64) (string, error) {
return pp.ExtractTexts() return pp.ExtractTexts()
} }
// ToMarkdown 将 PowerPoint 文档内容转换为 Markdown 格式。
func (p *Pptx) ToMarkdown() (string, error) {
text, err := p.Text()
if err != nil {
return "", err
}
return text, nil
}