Complete Unified API refactoring (v1.3.0)
This commit is contained in:
parent
3d74c78d8b
commit
baf0b3835d
@ -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`)。
|
||||
|
||||
95
README.md
95
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` 直接访问。
|
||||
|
||||
57
docx.go
57
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
|
||||
}
|
||||
|
||||
149
excel.go
149
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
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
48
office.go
Normal file
48
office.go
Normal 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
86
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)
|
||||
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 != "" {
|
||||
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
55
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
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user