feat: Add complex table cell content APIs (paragraphs, nested tables, lists, images) (#70)

* Initial plan

* Implement complex table cell content features for issue #27

- Add AddCellParagraph method to add paragraphs to table cells
- Add AddCellFormattedParagraph for formatted paragraphs in cells
- Add ClearCellParagraphs and GetCellParagraphs methods
- Add AddNestedTable method for nested tables in cells
- Add AddCellList method with support for various list types
- Add AddCellImage, AddCellImageFromFile, AddCellImageFromData methods
- Add comprehensive tests for all new features
- Add complex_table_demo example demonstrating all features

Co-authored-by: ZeroHawkeye <161401688+ZeroHawkeye@users.noreply.github.com>

* Remove accidental binary and update .gitignore

Co-authored-by: ZeroHawkeye <161401688+ZeroHawkeye@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: ZeroHawkeye <161401688+ZeroHawkeye@users.noreply.github.com>
This commit is contained in:
Copilot
2025-11-25 11:06:10 +08:00
committed by GitHub
parent e2f4402c5d
commit 90f24b746d
5 changed files with 1708 additions and 0 deletions
+3
View File
@@ -3,6 +3,9 @@ examples/output/
*/output/
output/
# 编译的示例二进制文件
complex_table_demo
# IDE和编辑器文件
.cursor/
.vscode/
+393
View File
@@ -0,0 +1,393 @@
// Package main 演示WordZero复杂表格结构功能
// 展示如何在表格单元格中添加段落、列表、嵌套表格和图片
package main
import (
"bytes"
"fmt"
"image"
"image/color"
"image/png"
"log"
"os"
"github.com/ZeroHawkeye/wordZero/pkg/document"
)
func main() {
fmt.Println("=== WordZero 复杂表格结构演示 ===")
// 确保输出目录存在
if _, err := os.Stat("examples/output"); os.IsNotExist(err) {
os.MkdirAll("examples/output", 0755)
}
// 创建新文档
doc := document.New()
// 添加文档标题
title := doc.AddParagraph("WordZero 复杂表格结构演示")
title.SetAlignment(document.AlignCenter)
doc.AddParagraph("") // 空行
// 演示1:单元格中添加多个段落
fmt.Println("1. 单元格多段落演示...")
demonstrateMultipleParagraphs(doc)
// 演示2:单元格中添加列表
fmt.Println("2. 单元格列表演示...")
demonstrateCellLists(doc)
// 演示3:单元格中添加嵌套表格
fmt.Println("3. 嵌套表格演示...")
demonstrateNestedTable(doc)
// 演示4:单元格中添加图片
fmt.Println("4. 单元格图片演示...")
demonstrateCellImages(doc)
// 演示5:综合复杂表格
fmt.Println("5. 综合复杂表格演示...")
demonstrateComplexTable(doc)
// 保存文档
outputFile := "examples/output/complex_table_demo.docx"
err := doc.Save(outputFile)
if err != nil {
log.Fatalf("保存文档失败: %v", err)
}
fmt.Printf("\n复杂表格演示文档已保存到: %s\n", outputFile)
fmt.Println("=== 演示完成 ===")
}
// demonstrateMultipleParagraphs 演示单元格中添加多个段落
func demonstrateMultipleParagraphs(doc *document.Document) {
doc.AddParagraph("1. 单元格多段落演示")
doc.AddParagraph("")
// 创建表格
config := &document.TableConfig{
Rows: 2,
Cols: 2,
Width: 8000,
}
table, err := doc.AddTable(config)
if err != nil {
log.Printf("创建表格失败: %v", err)
return
}
// 设置表头
table.SetCellText(0, 0, "多段落单元格")
table.SetCellText(0, 1, "格式化段落单元格")
// 在单元格中添加多个段落
table.SetCellText(1, 0, "这是第一段内容,介绍了表格的基本概念。")
table.AddCellParagraph(1, 0, "这是第二段内容,说明了表格的使用方法。")
table.AddCellParagraph(1, 0, "这是第三段内容,总结了表格的优势。")
// 在另一个单元格中添加格式化段落
table.SetCellText(1, 1, "普通文本介绍")
table.AddCellFormattedParagraph(1, 1, "粗体重点内容", &document.TextFormat{
Bold: true,
FontSize: 12,
})
table.AddCellFormattedParagraph(1, 1, "红色提示文字", &document.TextFormat{
FontColor: "FF0000",
Italic: true,
})
table.AddCellFormattedParagraph(1, 1, "大号蓝色标题", &document.TextFormat{
FontColor: "0000FF",
FontSize: 14,
Bold: true,
})
doc.AddParagraph("")
fmt.Println(" 多段落单元格演示完成")
}
// demonstrateCellLists 演示单元格中添加列表
func demonstrateCellLists(doc *document.Document) {
doc.AddParagraph("2. 单元格列表演示")
doc.AddParagraph("")
// 创建表格
config := &document.TableConfig{
Rows: 2,
Cols: 3,
Width: 9000,
}
table, err := doc.AddTable(config)
if err != nil {
log.Printf("创建表格失败: %v", err)
return
}
// 设置表头
table.SetCellText(0, 0, "无序列表")
table.SetCellText(0, 1, "有序列表")
table.SetCellText(0, 2, "罗马数字列表")
// 添加无序列表
bulletList := &document.CellListConfig{
Type: document.ListTypeBullet,
BulletSymbol: document.BulletTypeDot,
Items: []string{
"第一个要点",
"第二个要点",
"第三个要点",
},
}
table.ClearCellParagraphs(1, 0) // 清空默认段落
table.AddCellList(1, 0, bulletList)
// 添加有序列表
numberList := &document.CellListConfig{
Type: document.ListTypeNumber,
Items: []string{
"第一步操作",
"第二步操作",
"第三步操作",
},
}
table.ClearCellParagraphs(1, 1)
table.AddCellList(1, 1, numberList)
// 添加罗马数字列表
romanList := &document.CellListConfig{
Type: document.ListTypeUpperRoman,
Items: []string{
"主要内容",
"次要内容",
"补充内容",
},
}
table.ClearCellParagraphs(1, 2)
table.AddCellList(1, 2, romanList)
doc.AddParagraph("")
fmt.Println(" 单元格列表演示完成")
}
// demonstrateNestedTable 演示嵌套表格
func demonstrateNestedTable(doc *document.Document) {
doc.AddParagraph("3. 嵌套表格演示")
doc.AddParagraph("")
// 创建主表格
config := &document.TableConfig{
Rows: 2,
Cols: 2,
Width: 8000,
}
table, err := doc.AddTable(config)
if err != nil {
log.Printf("创建表格失败: %v", err)
return
}
// 设置表头
table.SetCellText(0, 0, "产品信息")
table.SetCellText(0, 1, "销售数据")
// 在第一个单元格添加产品信息嵌套表格
productNestedConfig := &document.TableConfig{
Rows: 3,
Cols: 2,
Width: 3500,
Data: [][]string{
{"属性", "值"},
{"名称", "智能手表"},
{"型号", "SW-2024"},
},
}
table.ClearCellParagraphs(1, 0)
table.AddCellParagraph(1, 0, "产品详细信息:")
table.AddNestedTable(1, 0, productNestedConfig)
// 在第二个单元格添加销售数据嵌套表格
salesNestedConfig := &document.TableConfig{
Rows: 4,
Cols: 2,
Width: 3500,
Data: [][]string{
{"季度", "销量"},
{"Q1", "1000"},
{"Q2", "1500"},
{"Q3", "2000"},
},
}
table.ClearCellParagraphs(1, 1)
table.AddCellParagraph(1, 1, "季度销售数据:")
table.AddNestedTable(1, 1, salesNestedConfig)
doc.AddParagraph("")
fmt.Println(" 嵌套表格演示完成")
}
// demonstrateCellImages 演示单元格中添加图片
func demonstrateCellImages(doc *document.Document) {
doc.AddParagraph("4. 单元格图片演示")
doc.AddParagraph("")
// 创建表格
config := &document.TableConfig{
Rows: 2,
Cols: 2,
Width: 8000,
}
table, err := doc.AddTable(config)
if err != nil {
log.Printf("创建表格失败: %v", err)
return
}
// 设置表头
table.SetCellText(0, 0, "产品图片")
table.SetCellText(0, 1, "产品描述")
// 在单元格中添加图片
imageData := createColorImage(150, 100, color.RGBA{100, 150, 255, 255})
table.ClearCellParagraphs(1, 0)
_, err = doc.AddCellImageFromData(table, 1, 0, imageData, 40) // 40mm宽度
if err != nil {
log.Printf("添加图片失败: %v", err)
}
// 在另一个单元格添加描述
table.SetCellText(1, 1, "产品名称:智能设备")
table.AddCellParagraph(1, 1, "规格:150mm x 100mm")
table.AddCellFormattedParagraph(1, 1, "状态:在售", &document.TextFormat{
FontColor: "00AA00",
Bold: true,
})
doc.AddParagraph("")
fmt.Println(" 单元格图片演示完成")
}
// demonstrateComplexTable 演示综合复杂表格
func demonstrateComplexTable(doc *document.Document) {
doc.AddParagraph("5. 综合复杂表格演示")
doc.AddParagraph("")
// 创建复杂表格
config := &document.TableConfig{
Rows: 4,
Cols: 3,
Width: 10000,
}
table, err := doc.AddTable(config)
if err != nil {
log.Printf("创建表格失败: %v", err)
return
}
// 第一行:表头
table.SetCellText(0, 0, "项目")
table.SetCellText(0, 1, "详细信息")
table.SetCellText(0, 2, "备注")
// 第二行:多段落内容
table.SetCellText(1, 0, "公司简介")
table.ClearCellParagraphs(1, 1)
table.AddCellParagraph(1, 1, "WordZero科技是一家专注于文档处理的技术公司。")
table.AddCellFormattedParagraph(1, 1, "成立于2024年", &document.TextFormat{Bold: true})
table.AddCellFormattedParagraph(1, 1, "总部位于北京", &document.TextFormat{Italic: true})
// 添加备注列表
noteList := &document.CellListConfig{
Type: document.ListTypeBullet,
BulletSymbol: document.BulletTypeArrow,
Items: []string{"技术驱动", "用户至上", "持续创新"},
}
table.ClearCellParagraphs(1, 2)
table.AddCellList(1, 2, noteList)
// 第三行:嵌套表格
table.SetCellText(2, 0, "产品矩阵")
nestedConfig := &document.TableConfig{
Rows: 3,
Cols: 2,
Width: 3000,
Data: [][]string{
{"产品", "版本"},
{"WordZero Core", "v1.0"},
{"WordZero Pro", "v2.0"},
},
}
table.ClearCellParagraphs(2, 1)
table.AddNestedTable(2, 1, nestedConfig)
table.SetCellText(2, 2, "更多产品开发中...")
// 第四行:图片和描述
table.SetCellText(3, 0, "团队展示")
// 添加团队图片
teamImage := createColorImage(120, 80, color.RGBA{150, 200, 150, 255})
table.ClearCellParagraphs(3, 1)
doc.AddCellImageFromData(table, 3, 1, teamImage, 35)
table.AddCellParagraph(3, 1, "专业团队")
// 添加联系方式
contactList := &document.CellListConfig{
Type: document.ListTypeNumber,
Items: []string{
"电话:400-123-4567",
"邮箱:contact@wordzero.com",
"网站:www.wordzero.com",
},
}
table.ClearCellParagraphs(3, 2)
table.AddCellList(3, 2, contactList)
doc.AddParagraph("")
fmt.Println(" 综合复杂表格演示完成")
}
// createColorImage 创建指定颜色的示例图片
func createColorImage(width, height int, bgColor color.RGBA) []byte {
img := image.NewRGBA(image.Rect(0, 0, width, height))
// 填充背景色
for y := 0; y < height; y++ {
for x := 0; x < width; x++ {
img.Set(x, y, bgColor)
}
}
// 添加边框
borderColor := color.RGBA{50, 50, 50, 255}
for x := 0; x < width; x++ {
img.Set(x, 0, borderColor)
img.Set(x, height-1, borderColor)
}
for y := 0; y < height; y++ {
img.Set(0, y, borderColor)
img.Set(width-1, y, borderColor)
}
// 添加中心十字标记
centerX, centerY := width/2, height/2
markColor := color.RGBA{0, 0, 0, 255}
for x := centerX - 10; x <= centerX+10 && x < width; x++ {
if x >= 0 {
img.Set(x, centerY, markColor)
}
}
for y := centerY - 10; y <= centerY+10 && y < height; y++ {
if y >= 0 {
img.Set(centerX, y, markColor)
}
}
buf := new(bytes.Buffer)
png.Encode(buf, img)
return buf.Bytes()
}
+162
View File
@@ -1079,6 +1079,168 @@ func (d *Document) SetImageTitle(imageInfo *ImageInfo, title string) error {
return nil
}
// AddCellImage 向表格单元格添加图片
//
// 此方法用于向表格单元格中添加图片,支持从文件路径或二进制数据添加。
// 由于图片需要在文档级别管理资源关系,所以此方法必须在Document上调用。
//
// 参数:
// - table: 目标表格
// - row: 行索引(从0开始)
// - col: 列索引(从0开始)
// - config: 单元格图片配置
//
// 返回:
// - *ImageInfo: 添加的图片信息
// - error: 如果添加失败则返回错误
//
// 示例:
//
// table, _ := doc.AddTable(&document.TableConfig{Rows: 2, Cols: 2, Width: 6000})
// imageConfig := &document.CellImageConfig{
// FilePath: "logo.png",
// Width: 50, // 50mm宽度
// KeepAspectRatio: true,
// }
// imageInfo, err := doc.AddCellImage(table, 0, 0, imageConfig)
func (d *Document) AddCellImage(table *Table, row, col int, config *CellImageConfig) (*ImageInfo, error) {
if table == nil {
return nil, fmt.Errorf("表格不能为空")
}
cell, err := table.GetCell(row, col)
if err != nil {
return nil, err
}
var imageData []byte
var format ImageFormat
var width, height int
// 从文件或数据获取图片
if config.FilePath != "" {
// 从文件读取图片
imageData, err = os.ReadFile(config.FilePath)
if err != nil {
Errorf("读取图片文件失败 %s: %v", config.FilePath, err)
return nil, fmt.Errorf("读取图片文件失败: %v", err)
}
// 检测图片格式
format, err = detectImageFormat(imageData)
if err != nil {
Errorf("检测图片格式失败 %s: %v", config.FilePath, err)
return nil, fmt.Errorf("检测图片格式失败: %v", err)
}
// 获取图片尺寸
width, height, err = getImageDimensions(imageData, format)
if err != nil {
Errorf("获取图片尺寸失败 %s: %v", config.FilePath, err)
return nil, fmt.Errorf("获取图片尺寸失败: %v", err)
}
} else if len(config.Data) > 0 {
// 使用提供的二进制数据
imageData = config.Data
if config.Format == "" {
// 检测图片格式
format, err = detectImageFormat(imageData)
if err != nil {
return nil, fmt.Errorf("检测图片格式失败: %v", err)
}
} else {
format = config.Format
}
// 获取图片尺寸
width, height, err = getImageDimensions(imageData, format)
if err != nil {
return nil, fmt.Errorf("获取图片尺寸失败: %v", err)
}
} else {
return nil, fmt.Errorf("必须提供图片文件路径或二进制数据")
}
// 创建图片配置
imageConfig := &ImageConfig{
Position: ImagePositionInline,
Alignment: AlignCenter,
AltText: config.AltText,
Title: config.Title,
}
if config.Width > 0 || config.Height > 0 {
imageConfig.Size = &ImageSize{
Width: config.Width,
Height: config.Height,
KeepAspectRatio: config.KeepAspectRatio,
}
}
// 使用Document的方法添加图片资源,但不添加到文档主体
fileName := "cell_image.png"
if config.FilePath != "" {
fileName = config.FilePath
}
imageInfo, err := d.AddImageFromDataWithoutElement(imageData, fileName, format, width, height, imageConfig)
if err != nil {
return nil, err
}
// 创建包含图片的段落并添加到单元格
paragraph := d.createImageParagraph(imageInfo)
cell.Paragraphs = append(cell.Paragraphs, *paragraph)
Infof("向表格单元格(%d,%d)添加图片成功: ID=%s", row, col, imageInfo.ID)
return imageInfo, nil
}
// AddCellImageFromFile 从文件向表格单元格添加图片(便捷方法)
//
// 此方法是AddCellImage的便捷封装,直接从文件路径添加图片。
//
// 参数:
// - table: 目标表格
// - row: 行索引(从0开始)
// - col: 列索引(从0开始)
// - filePath: 图片文件路径
// - widthMM: 图片宽度(毫米),0表示使用原始尺寸
//
// 返回:
// - *ImageInfo: 添加的图片信息
// - error: 如果添加失败则返回错误
func (d *Document) AddCellImageFromFile(table *Table, row, col int, filePath string, widthMM float64) (*ImageInfo, error) {
return d.AddCellImage(table, row, col, &CellImageConfig{
FilePath: filePath,
Width: widthMM,
KeepAspectRatio: true,
})
}
// AddCellImageFromData 从二进制数据向表格单元格添加图片(便捷方法)
//
// 此方法是AddCellImage的便捷封装,直接从二进制数据添加图片。
//
// 参数:
// - table: 目标表格
// - row: 行索引(从0开始)
// - col: 列索引(从0开始)
// - data: 图片二进制数据
// - widthMM: 图片宽度(毫米),0表示使用原始尺寸
//
// 返回:
// - *ImageInfo: 添加的图片信息
// - error: 如果添加失败则返回错误
func (d *Document) AddCellImageFromData(table *Table, row, col int, data []byte, widthMM float64) (*ImageInfo, error) {
return d.AddCellImage(table, row, col, &CellImageConfig{
Data: data,
Width: widthMM,
KeepAspectRatio: true,
})
}
// SetImageAlignment 设置图片对齐方式
//
// 此方法用于设置嵌入式图片(ImagePositionInline)的对齐方式。
+423
View File
@@ -2643,3 +2643,426 @@ func (t *Table) FindCellsByText(searchText string, exactMatch bool) ([]*CellInfo
return strings.Contains(text, searchText)
})
}
// ============== 单元格复杂内容功能 ==============
// 以下方法支持向表格单元格中添加段落、图片、列表、嵌套表格等复杂内容
// AddCellParagraph 向单元格添加段落
// 参数:
// - row: 行索引(从0开始)
// - col: 列索引(从0开始)
// - text: 段落文本内容
//
// 返回:
// - *Paragraph: 新添加的段落对象
// - error: 如果索引无效则返回错误
func (t *Table) AddCellParagraph(row, col int, text string) (*Paragraph, error) {
cell, err := t.GetCell(row, col)
if err != nil {
return nil, err
}
// 创建新段落
para := &Paragraph{
Runs: []Run{
{
Text: Text{
Content: text,
Space: "preserve",
},
},
},
}
// 添加到单元格
cell.Paragraphs = append(cell.Paragraphs, *para)
Info(fmt.Sprintf("向单元格(%d,%d)添加段落成功", row, col))
return &cell.Paragraphs[len(cell.Paragraphs)-1], nil
}
// AddCellFormattedParagraph 向单元格添加格式化段落
// 参数:
// - row: 行索引(从0开始)
// - col: 列索引(从0开始)
// - text: 段落文本内容
// - format: 文本格式配置
//
// 返回:
// - *Paragraph: 新添加的段落对象
// - error: 如果索引无效则返回错误
func (t *Table) AddCellFormattedParagraph(row, col int, text string, format *TextFormat) (*Paragraph, error) {
cell, err := t.GetCell(row, col)
if err != nil {
return nil, err
}
// 创建运行属性
runProps := &RunProperties{}
if format != nil {
if format.FontFamily != "" {
runProps.FontFamily = &FontFamily{
ASCII: format.FontFamily,
HAnsi: format.FontFamily,
EastAsia: format.FontFamily,
CS: format.FontFamily,
}
}
if format.Bold {
runProps.Bold = &Bold{}
}
if format.Italic {
runProps.Italic = &Italic{}
}
if format.FontColor != "" {
color := strings.TrimPrefix(format.FontColor, "#")
runProps.Color = &Color{Val: color}
}
if format.FontSize > 0 {
runProps.FontSize = &FontSize{Val: fmt.Sprintf("%d", format.FontSize*2)}
}
if format.Underline {
runProps.Underline = &Underline{Val: "single"}
}
if format.Strike {
runProps.Strike = &Strike{}
}
if format.Highlight != "" {
runProps.Highlight = &Highlight{Val: format.Highlight}
}
}
// 创建新段落
para := &Paragraph{
Runs: []Run{
{
Properties: runProps,
Text: Text{
Content: text,
Space: "preserve",
},
},
},
}
// 添加到单元格
cell.Paragraphs = append(cell.Paragraphs, *para)
Info(fmt.Sprintf("向单元格(%d,%d)添加格式化段落成功", row, col))
return &cell.Paragraphs[len(cell.Paragraphs)-1], nil
}
// ClearCellParagraphs 清空单元格中的所有段落,只保留一个空段落
// 参数:
// - row: 行索引(从0开始)
// - col: 列索引(从0开始)
//
// 返回:
// - error: 如果索引无效则返回错误
func (t *Table) ClearCellParagraphs(row, col int) error {
cell, err := t.GetCell(row, col)
if err != nil {
return err
}
// 清空段落,只保留一个空段落(OOXML规范要求单元格至少有一个段落)
cell.Paragraphs = []Paragraph{
{
Runs: []Run{
{
Text: Text{Content: ""},
},
},
},
}
Info(fmt.Sprintf("清空单元格(%d,%d)段落成功", row, col))
return nil
}
// GetCellParagraphs 获取单元格中的所有段落
// 参数:
// - row: 行索引(从0开始)
// - col: 列索引(从0开始)
//
// 返回:
// - []Paragraph: 单元格中的所有段落
// - error: 如果索引无效则返回错误
func (t *Table) GetCellParagraphs(row, col int) ([]Paragraph, error) {
cell, err := t.GetCell(row, col)
if err != nil {
return nil, err
}
return cell.Paragraphs, nil
}
// AddNestedTable 向单元格添加嵌套表格
// 参数:
// - row: 行索引(从0开始)
// - col: 列索引(从0开始)
// - config: 嵌套表格的配置
//
// 返回:
// - *Table: 新创建的嵌套表格对象
// - error: 如果索引无效或配置无效则返回错误
func (t *Table) AddNestedTable(row, col int, config *TableConfig) (*Table, error) {
cell, err := t.GetCell(row, col)
if err != nil {
return nil, err
}
if config.Rows <= 0 || config.Cols <= 0 {
Error("嵌套表格行数和列数必须大于0")
return nil, NewValidationError("TableConfig", "", "嵌套表格行数和列数必须大于0")
}
// 创建嵌套表格
nestedTable := &Table{
Properties: &TableProperties{
TableW: &TableWidth{
W: fmt.Sprintf("%d", config.Width),
Type: "dxa",
},
TableJc: &TableJc{
Val: "center",
},
TableLook: &TableLook{
Val: "04A0",
FirstRow: "1",
LastRow: "0",
FirstCol: "1",
LastCol: "0",
NoHBand: "0",
NoVBand: "1",
},
TableBorders: &TableBorders{
Top: &TableBorder{Val: "single", Sz: "4", Space: "0", Color: "auto"},
Left: &TableBorder{Val: "single", Sz: "4", Space: "0", Color: "auto"},
Bottom: &TableBorder{Val: "single", Sz: "4", Space: "0", Color: "auto"},
Right: &TableBorder{Val: "single", Sz: "4", Space: "0", Color: "auto"},
InsideH: &TableBorder{Val: "single", Sz: "4", Space: "0", Color: "auto"},
InsideV: &TableBorder{Val: "single", Sz: "4", Space: "0", Color: "auto"},
},
TableLayout: &TableLayoutType{
Type: "autofit",
},
TableCellMar: &TableCellMargins{
Left: &TableCellSpace{W: "108", Type: "dxa"},
Right: &TableCellSpace{W: "108", Type: "dxa"},
},
},
Grid: &TableGrid{},
Rows: make([]TableRow, 0, config.Rows),
}
// 设置列宽
colWidths := config.ColWidths
if len(colWidths) == 0 {
avgWidth := config.Width / config.Cols
colWidths = make([]int, config.Cols)
for i := range colWidths {
colWidths[i] = avgWidth
}
} else if len(colWidths) != config.Cols {
Error("嵌套表格列宽数量与列数不匹配")
return nil, NewValidationError("TableConfig.ColWidths", "", "列宽数量与列数不匹配")
}
// 创建表格网格
for _, width := range colWidths {
nestedTable.Grid.Cols = append(nestedTable.Grid.Cols, TableGridCol{
W: fmt.Sprintf("%d", width),
})
}
// 创建表格行和单元格
for i := 0; i < config.Rows; i++ {
tableRow := TableRow{
Cells: make([]TableCell, 0, config.Cols),
}
for j := 0; j < config.Cols; j++ {
tableCell := TableCell{
Properties: &TableCellProperties{
TableCellW: &TableCellW{
W: fmt.Sprintf("%d", colWidths[j]),
Type: "dxa",
},
VAlign: &VAlign{
Val: "center",
},
},
Paragraphs: []Paragraph{
{
Runs: []Run{
{
Text: Text{Content: ""},
},
},
},
},
}
// 如果有初始数据,设置单元格内容
if config.Data != nil && i < len(config.Data) && j < len(config.Data[i]) {
tableCell.Paragraphs[0].Runs[0].Text.Content = config.Data[i][j]
}
tableRow.Cells = append(tableRow.Cells, tableCell)
}
nestedTable.Rows = append(nestedTable.Rows, tableRow)
}
// 添加到单元格的嵌套表格列表
cell.Tables = append(cell.Tables, *nestedTable)
Info(fmt.Sprintf("向单元格(%d,%d)添加嵌套表格成功:%d行 x %d列", row, col, config.Rows, config.Cols))
return &cell.Tables[len(cell.Tables)-1], nil
}
// GetNestedTables 获取单元格中的所有嵌套表格
// 参数:
// - row: 行索引(从0开始)
// - col: 列索引(从0开始)
//
// 返回:
// - []Table: 单元格中的所有嵌套表格
// - error: 如果索引无效则返回错误
func (t *Table) GetNestedTables(row, col int) ([]Table, error) {
cell, err := t.GetCell(row, col)
if err != nil {
return nil, err
}
return cell.Tables, nil
}
// CellListConfig 单元格列表配置
type CellListConfig struct {
Type ListType // 列表类型
BulletSymbol BulletType // 项目符号(仅用于无序列表)
Items []string // 列表项内容
}
// AddCellList 向单元格添加列表
// 参数:
// - row: 行索引(从0开始)
// - col: 列索引(从0开始)
// - config: 列表配置
//
// 返回:
// - error: 如果索引无效则返回错误
func (t *Table) AddCellList(row, col int, config *CellListConfig) error {
cell, err := t.GetCell(row, col)
if err != nil {
return err
}
if config == nil || len(config.Items) == 0 {
return NewValidationError("CellListConfig", "", "列表配置不能为空且必须包含列表项")
}
// 根据列表类型确定前缀
for i, item := range config.Items {
var prefix string
switch config.Type {
case ListTypeBullet:
// 使用项目符号
bulletSymbol := config.BulletSymbol
if bulletSymbol == "" {
bulletSymbol = BulletTypeDot
}
prefix = string(bulletSymbol) + " "
case ListTypeNumber, ListTypeDecimal:
// 使用数字编号
prefix = fmt.Sprintf("%d. ", i+1)
case ListTypeLowerLetter:
// 使用小写字母
prefix = fmt.Sprintf("%c. ", 'a'+i)
case ListTypeUpperLetter:
// 使用大写字母
prefix = fmt.Sprintf("%c. ", 'A'+i)
case ListTypeLowerRoman:
// 使用小写罗马数字
prefix = fmt.Sprintf("%s. ", toRomanLower(i+1))
case ListTypeUpperRoman:
// 使用大写罗马数字
prefix = fmt.Sprintf("%s. ", toRomanUpper(i+1))
default:
// 默认使用项目符号
prefix = string(BulletTypeDot) + " "
}
// 创建列表项段落
para := Paragraph{
Runs: []Run{
{
Text: Text{
Content: prefix + item,
Space: "preserve",
},
},
},
}
// 添加到单元格
cell.Paragraphs = append(cell.Paragraphs, para)
}
Info(fmt.Sprintf("向单元格(%d,%d)添加列表成功:%d个列表项", row, col, len(config.Items)))
return nil
}
// toRomanLower 将数字转换为小写罗马数字
func toRomanLower(num int) string {
return strings.ToLower(toRomanUpper(num))
}
// toRomanUpper 将数字转换为大写罗马数字
func toRomanUpper(num int) string {
values := []int{1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1}
symbols := []string{"M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I"}
if num <= 0 || num > 3999 {
return fmt.Sprintf("%d", num)
}
result := ""
for i, value := range values {
for num >= value {
result += symbols[i]
num -= value
}
}
return result
}
// CellImageConfig 单元格图片配置
type CellImageConfig struct {
// 图片来源 - 文件路径
FilePath string
// 图片来源 - 二进制数据
Data []byte
// 图片格式(当使用Data时需要指定)
Format ImageFormat
// 图片宽度(毫米),0表示自动
Width float64
// 图片高度(毫米),0表示自动
Height float64
// 是否保持宽高比
KeepAspectRatio bool
// 图片替代文字
AltText string
// 图片标题
Title string
}
+727
View File
@@ -0,0 +1,727 @@
package document
import (
"bytes"
"image"
"image/color"
"image/png"
"os"
"testing"
)
// createCellTestImage 创建测试用的PNG图片数据
func createCellTestImage(width, height int) []byte {
img := image.NewRGBA(image.Rect(0, 0, width, height))
// 填充红色背景
for y := 0; y < height; y++ {
for x := 0; x < width; x++ {
img.Set(x, y, color.RGBA{255, 100, 100, 255})
}
}
buf := new(bytes.Buffer)
png.Encode(buf, img)
return buf.Bytes()
}
// TestAddCellParagraph 测试向单元格添加段落
func TestAddCellParagraph(t *testing.T) {
doc := New()
config := &TableConfig{
Rows: 2,
Cols: 2,
Width: 4000,
}
table, err := doc.CreateTable(config)
if err != nil {
t.Fatalf("创建表格失败: %v", err)
}
// 测试添加段落
para, err := table.AddCellParagraph(0, 0, "第一段内容")
if err != nil {
t.Errorf("添加段落失败: %v", err)
}
if para == nil {
t.Error("返回的段落不应为空")
}
// 添加第二个段落
para2, err := table.AddCellParagraph(0, 0, "第二段内容")
if err != nil {
t.Errorf("添加第二段落失败: %v", err)
}
if para2 == nil {
t.Error("返回的第二段落不应为空")
}
// 验证段落数量
paragraphs, err := table.GetCellParagraphs(0, 0)
if err != nil {
t.Errorf("获取段落失败: %v", err)
}
// 初始有一个空段落,加上两个新段落
if len(paragraphs) < 3 {
t.Errorf("期望至少3个段落,实际%d", len(paragraphs))
}
// 测试无效索引
_, err = table.AddCellParagraph(10, 10, "无效")
if err == nil {
t.Error("期望无效索引失败,但成功了")
}
}
// TestAddCellFormattedParagraph 测试向单元格添加格式化段落
func TestAddCellFormattedParagraph(t *testing.T) {
doc := New()
config := &TableConfig{
Rows: 2,
Cols: 2,
Width: 4000,
}
table, err := doc.CreateTable(config)
if err != nil {
t.Fatalf("创建表格失败: %v", err)
}
// 测试添加格式化段落
format := &TextFormat{
Bold: true,
Italic: true,
FontSize: 14,
FontColor: "FF0000",
FontFamily: "Arial",
Underline: true,
}
para, err := table.AddCellFormattedParagraph(0, 0, "格式化内容", format)
if err != nil {
t.Errorf("添加格式化段落失败: %v", err)
}
if para == nil {
t.Error("返回的段落不应为空")
}
// 验证格式
if len(para.Runs) == 0 {
t.Error("段落应包含至少一个Run")
}
run := para.Runs[0]
if run.Properties == nil {
t.Error("Run应有属性")
} else {
if run.Properties.Bold == nil {
t.Error("期望粗体属性")
}
if run.Properties.Italic == nil {
t.Error("期望斜体属性")
}
if run.Properties.Underline == nil {
t.Error("期望下划线属性")
}
}
}
// TestClearCellParagraphs 测试清空单元格段落
func TestClearCellParagraphs(t *testing.T) {
doc := New()
config := &TableConfig{
Rows: 2,
Cols: 2,
Width: 4000,
Data: [][]string{
{"A1", "B1"},
{"A2", "B2"},
},
}
table, err := doc.CreateTable(config)
if err != nil {
t.Fatalf("创建表格失败: %v", err)
}
// 添加多个段落
table.AddCellParagraph(0, 0, "段落1")
table.AddCellParagraph(0, 0, "段落2")
// 清空段落
err = table.ClearCellParagraphs(0, 0)
if err != nil {
t.Errorf("清空段落失败: %v", err)
}
// 验证清空后只有一个空段落
paragraphs, err := table.GetCellParagraphs(0, 0)
if err != nil {
t.Errorf("获取段落失败: %v", err)
}
if len(paragraphs) != 1 {
t.Errorf("期望清空后只有1个段落,实际%d", len(paragraphs))
}
// 测试无效索引
err = table.ClearCellParagraphs(10, 10)
if err == nil {
t.Error("期望无效索引失败,但成功了")
}
}
// TestAddNestedTable 测试向单元格添加嵌套表格
func TestAddNestedTable(t *testing.T) {
doc := New()
// 创建主表格
mainConfig := &TableConfig{
Rows: 2,
Cols: 2,
Width: 8000,
}
mainTable, err := doc.CreateTable(mainConfig)
if err != nil {
t.Fatalf("创建主表格失败: %v", err)
}
// 创建嵌套表格配置
nestedConfig := &TableConfig{
Rows: 2,
Cols: 3,
Width: 3000,
Data: [][]string{
{"嵌套1", "嵌套2", "嵌套3"},
{"数据1", "数据2", "数据3"},
},
}
// 添加嵌套表格
nestedTable, err := mainTable.AddNestedTable(0, 0, nestedConfig)
if err != nil {
t.Errorf("添加嵌套表格失败: %v", err)
}
if nestedTable == nil {
t.Error("返回的嵌套表格不应为空")
}
// 验证嵌套表格结构
if nestedTable.GetRowCount() != 2 {
t.Errorf("期望嵌套表格2行,实际%d", nestedTable.GetRowCount())
}
if nestedTable.GetColumnCount() != 3 {
t.Errorf("期望嵌套表格3列,实际%d", nestedTable.GetColumnCount())
}
// 验证嵌套表格内容
cellText, err := nestedTable.GetCellText(0, 0)
if err != nil {
t.Errorf("获取嵌套表格单元格内容失败: %v", err)
}
if cellText != "嵌套1" {
t.Errorf("期望嵌套表格内容'嵌套1',实际'%s'", cellText)
}
// 获取嵌套表格列表
nestedTables, err := mainTable.GetNestedTables(0, 0)
if err != nil {
t.Errorf("获取嵌套表格列表失败: %v", err)
}
if len(nestedTables) != 1 {
t.Errorf("期望1个嵌套表格,实际%d", len(nestedTables))
}
}
// TestAddNestedTableInvalidConfig 测试嵌套表格的无效配置
func TestAddNestedTableInvalidConfig(t *testing.T) {
doc := New()
mainConfig := &TableConfig{
Rows: 2,
Cols: 2,
Width: 4000,
}
mainTable, err := doc.CreateTable(mainConfig)
if err != nil {
t.Fatalf("创建主表格失败: %v", err)
}
// 测试无效的行列数
_, err = mainTable.AddNestedTable(0, 0, &TableConfig{Rows: 0, Cols: 2, Width: 2000})
if err == nil {
t.Error("期望行数为0时失败,但成功了")
}
_, err = mainTable.AddNestedTable(0, 0, &TableConfig{Rows: 2, Cols: 0, Width: 2000})
if err == nil {
t.Error("期望列数为0时失败,但成功了")
}
// 测试无效的单元格索引
_, err = mainTable.AddNestedTable(10, 10, &TableConfig{Rows: 2, Cols: 2, Width: 2000})
if err == nil {
t.Error("期望无效索引失败,但成功了")
}
}
// TestAddCellList 测试向单元格添加列表
func TestAddCellList(t *testing.T) {
doc := New()
config := &TableConfig{
Rows: 3,
Cols: 2,
Width: 6000,
}
table, err := doc.CreateTable(config)
if err != nil {
t.Fatalf("创建表格失败: %v", err)
}
// 测试添加无序列表
bulletListConfig := &CellListConfig{
Type: ListTypeBullet,
BulletSymbol: BulletTypeDot,
Items: []string{"项目一", "项目二", "项目三"},
}
err = table.AddCellList(0, 0, bulletListConfig)
if err != nil {
t.Errorf("添加无序列表失败: %v", err)
}
// 验证列表项数量
paragraphs, err := table.GetCellParagraphs(0, 0)
if err != nil {
t.Errorf("获取段落失败: %v", err)
}
// 初始有一个空段落,加上3个列表项
expectedCount := 1 + 3
if len(paragraphs) != expectedCount {
t.Errorf("期望%d个段落,实际%d", expectedCount, len(paragraphs))
}
// 测试添加有序列表
numberListConfig := &CellListConfig{
Type: ListTypeNumber,
Items: []string{"第一步", "第二步", "第三步"},
}
err = table.AddCellList(1, 0, numberListConfig)
if err != nil {
t.Errorf("添加有序列表失败: %v", err)
}
// 测试添加小写字母列表
letterListConfig := &CellListConfig{
Type: ListTypeLowerLetter,
Items: []string{"选项a", "选项b"},
}
err = table.AddCellList(2, 0, letterListConfig)
if err != nil {
t.Errorf("添加字母列表失败: %v", err)
}
}
// TestAddCellListInvalidConfig 测试列表的无效配置
func TestAddCellListInvalidConfig(t *testing.T) {
doc := New()
config := &TableConfig{
Rows: 2,
Cols: 2,
Width: 4000,
}
table, err := doc.CreateTable(config)
if err != nil {
t.Fatalf("创建表格失败: %v", err)
}
// 测试空配置
err = table.AddCellList(0, 0, nil)
if err == nil {
t.Error("期望空配置失败,但成功了")
}
// 测试空列表项
err = table.AddCellList(0, 0, &CellListConfig{Type: ListTypeBullet, Items: []string{}})
if err == nil {
t.Error("期望空列表项失败,但成功了")
}
// 测试无效索引
err = table.AddCellList(10, 10, &CellListConfig{Type: ListTypeBullet, Items: []string{"测试"}})
if err == nil {
t.Error("期望无效索引失败,但成功了")
}
}
// TestAddCellImage 测试向单元格添加图片
func TestAddCellImage(t *testing.T) {
doc := New()
config := &TableConfig{
Rows: 2,
Cols: 2,
Width: 6000,
}
table, err := doc.AddTable(config)
if err != nil {
t.Fatalf("创建表格失败: %v", err)
}
// 创建测试图片数据
imageData := createCellTestImage(100, 100)
// 测试从数据添加图片
imageInfo, err := doc.AddCellImageFromData(table, 0, 0, imageData, 30)
if err != nil {
t.Errorf("添加图片失败: %v", err)
}
if imageInfo == nil {
t.Error("返回的图片信息不应为空")
}
// 验证图片ID不为空
if imageInfo.ID == "" {
t.Error("图片ID不应为空")
}
// 验证关系ID不为空
if imageInfo.RelationID == "" {
t.Error("关系ID不应为空")
}
// 验证单元格段落包含图片
paragraphs, err := table.GetCellParagraphs(0, 0)
if err != nil {
t.Errorf("获取段落失败: %v", err)
}
hasImage := false
for _, para := range paragraphs {
for _, run := range para.Runs {
if run.Drawing != nil {
hasImage = true
break
}
}
}
if !hasImage {
t.Error("单元格应包含图片")
}
}
// TestAddCellImageWithConfig 测试使用配置添加图片
func TestAddCellImageWithConfig(t *testing.T) {
doc := New()
config := &TableConfig{
Rows: 2,
Cols: 2,
Width: 6000,
}
table, err := doc.AddTable(config)
if err != nil {
t.Fatalf("创建表格失败: %v", err)
}
// 创建测试图片数据
imageData := createCellTestImage(200, 150)
// 使用完整配置添加图片
imageConfig := &CellImageConfig{
Data: imageData,
Width: 50,
Height: 40,
KeepAspectRatio: false,
AltText: "测试图片",
Title: "单元格图片",
}
imageInfo, err := doc.AddCellImage(table, 0, 0, imageConfig)
if err != nil {
t.Errorf("添加图片失败: %v", err)
}
// 验证图片配置
if imageInfo.Config == nil {
t.Error("图片配置不应为空")
} else {
if imageInfo.Config.AltText != "测试图片" {
t.Errorf("期望替代文字'测试图片',实际'%s'", imageInfo.Config.AltText)
}
if imageInfo.Config.Title != "单元格图片" {
t.Errorf("期望标题'单元格图片',实际'%s'", imageInfo.Config.Title)
}
}
}
// TestAddCellImageInvalidCases 测试添加图片的无效情况
func TestAddCellImageInvalidCases(t *testing.T) {
doc := New()
config := &TableConfig{
Rows: 2,
Cols: 2,
Width: 4000,
}
table, err := doc.AddTable(config)
if err != nil {
t.Fatalf("创建表格失败: %v", err)
}
// 测试空表格
_, err = doc.AddCellImage(nil, 0, 0, &CellImageConfig{Data: createCellTestImage(100, 100)})
if err == nil {
t.Error("期望空表格失败,但成功了")
}
// 测试无效索引
_, err = doc.AddCellImage(table, 10, 10, &CellImageConfig{Data: createCellTestImage(100, 100)})
if err == nil {
t.Error("期望无效索引失败,但成功了")
}
// 测试无数据配置
_, err = doc.AddCellImage(table, 0, 0, &CellImageConfig{})
if err == nil {
t.Error("期望无数据配置失败,但成功了")
}
}
// TestComplexTableStructure 测试复杂表格结构
func TestComplexTableStructure(t *testing.T) {
doc := New()
// 创建主表格
mainConfig := &TableConfig{
Rows: 3,
Cols: 3,
Width: 9000,
}
table, err := doc.AddTable(mainConfig)
if err != nil {
t.Fatalf("创建表格失败: %v", err)
}
// 第一个单元格:添加多个段落
table.AddCellParagraph(0, 0, "第一段")
table.AddCellFormattedParagraph(0, 0, "格式化段落", &TextFormat{Bold: true})
// 第二个单元格:添加列表
listConfig := &CellListConfig{
Type: ListTypeBullet,
BulletSymbol: BulletTypeDot,
Items: []string{"列表项1", "列表项2"},
}
table.AddCellList(0, 1, listConfig)
// 第三个单元格:添加嵌套表格
nestedConfig := &TableConfig{
Rows: 2,
Cols: 2,
Width: 2500,
Data: [][]string{
{"A", "B"},
{"C", "D"},
},
}
table.AddNestedTable(0, 2, nestedConfig)
// 第四个单元格:添加图片
imageData := createCellTestImage(50, 50)
doc.AddCellImageFromData(table, 1, 0, imageData, 20)
// 验证复杂结构
paragraphs00, _ := table.GetCellParagraphs(0, 0)
if len(paragraphs00) < 3 { // 初始1个 + 添加2个
t.Errorf("单元格(0,0)应至少有3个段落,实际%d", len(paragraphs00))
}
paragraphs01, _ := table.GetCellParagraphs(0, 1)
if len(paragraphs01) < 3 { // 初始1个 + 列表2项
t.Errorf("单元格(0,1)应至少有3个段落,实际%d", len(paragraphs01))
}
nestedTables, _ := table.GetNestedTables(0, 2)
if len(nestedTables) != 1 {
t.Errorf("单元格(0,2)应有1个嵌套表格,实际%d", len(nestedTables))
}
paragraphs10, _ := table.GetCellParagraphs(1, 0)
hasImage := false
for _, para := range paragraphs10 {
for _, run := range para.Runs {
if run.Drawing != nil {
hasImage = true
break
}
}
}
if !hasImage {
t.Error("单元格(1,0)应包含图片")
}
}
// TestSaveComplexTable 测试保存复杂表格
func TestSaveComplexTable(t *testing.T) {
doc := New()
// 创建表格
config := &TableConfig{
Rows: 3,
Cols: 2,
Width: 6000,
}
table, err := doc.AddTable(config)
if err != nil {
t.Fatalf("创建表格失败: %v", err)
}
// 添加复杂内容
table.AddCellParagraph(0, 0, "复杂表格测试")
table.AddCellFormattedParagraph(0, 0, "粗体文本", &TextFormat{Bold: true})
listConfig := &CellListConfig{
Type: ListTypeNumber,
Items: []string{"第一项", "第二项"},
}
table.AddCellList(0, 1, listConfig)
nestedConfig := &TableConfig{
Rows: 2,
Cols: 2,
Width: 2000,
Data: [][]string{
{"X", "Y"},
{"Z", "W"},
},
}
table.AddNestedTable(1, 0, nestedConfig)
// 添加图片
imageData := createCellTestImage(80, 60)
doc.AddCellImageFromData(table, 1, 1, imageData, 25)
// 保存并验证
outputDir := "test_output"
if _, err := os.Stat(outputDir); os.IsNotExist(err) {
os.MkdirAll(outputDir, 0755)
}
outputFile := outputDir + "/complex_table_test.docx"
err = doc.Save(outputFile)
if err != nil {
t.Errorf("保存文档失败: %v", err)
}
// 验证文件存在
if _, err := os.Stat(outputFile); os.IsNotExist(err) {
t.Error("输出文件不存在")
}
// 清理
defer os.RemoveAll(outputDir)
}
// TestRomanNumerals 测试罗马数字转换
func TestRomanNumerals(t *testing.T) {
testCases := []struct {
num int
expected string
}{
{1, "I"},
{2, "II"},
{3, "III"},
{4, "IV"},
{5, "V"},
{9, "IX"},
{10, "X"},
{40, "XL"},
{50, "L"},
{90, "XC"},
{100, "C"},
{400, "CD"},
{500, "D"},
{900, "CM"},
{1000, "M"},
{1999, "MCMXCIX"},
{2024, "MMXXIV"},
}
for _, tc := range testCases {
result := toRomanUpper(tc.num)
if result != tc.expected {
t.Errorf("toRomanUpper(%d) = %s, 期望 %s", tc.num, result, tc.expected)
}
}
// 测试边界情况
if toRomanUpper(0) != "0" {
t.Error("0应返回字符串'0'")
}
if toRomanUpper(4000) != "4000" {
t.Error("4000应返回字符串'4000'")
}
}
// TestAddCellListAllTypes 测试所有列表类型
func TestAddCellListAllTypes(t *testing.T) {
doc := New()
config := &TableConfig{
Rows: 7,
Cols: 1,
Width: 3000,
}
table, err := doc.CreateTable(config)
if err != nil {
t.Fatalf("创建表格失败: %v", err)
}
testTypes := []struct {
listType ListType
name string
}{
{ListTypeBullet, "无序列表"},
{ListTypeNumber, "数字列表"},
{ListTypeDecimal, "十进制列表"},
{ListTypeLowerLetter, "小写字母列表"},
{ListTypeUpperLetter, "大写字母列表"},
{ListTypeLowerRoman, "小写罗马列表"},
{ListTypeUpperRoman, "大写罗马列表"},
}
for i, tc := range testTypes {
listConfig := &CellListConfig{
Type: tc.listType,
Items: []string{"项目1", "项目2", "项目3"},
}
err := table.AddCellList(i, 0, listConfig)
if err != nil {
t.Errorf("添加%s失败: %v", tc.name, err)
}
}
}