diff --git a/apipara.go b/apipara.go index 4627443..aa6cabb 100644 --- a/apipara.go +++ b/apipara.go @@ -1,15 +1,32 @@ package docxlib +import "unsafe" + // AddParagraph adds a new paragraph func (f *Docx) AddParagraph() *Paragraph { f.Document.Body.mu.Lock() defer f.Document.Body.mu.Unlock() - f.Document.Body.Paragraphs = append(f.Document.Body.Paragraphs, Paragraph{ + f.Document.Body.Items = append(f.Document.Body.Items, Paragraph{ Children: make([]interface{}, 0, 64), file: f, }) - return &f.Document.Body.Paragraphs[len(f.Document.Body.Paragraphs)-1] + p := f.Document.Body.Items[len(f.Document.Body.Items)-1] + + return *(**Paragraph)(unsafe.Add(unsafe.Pointer(&p), unsafe.Sizeof(uintptr(0)))) +} + +// AddParagraph adds a new paragraph +func (c *WTableCell) AddParagraph() *Paragraph { + c.mu.Lock() + defer c.mu.Unlock() + + c.Paragraphs = append(c.Paragraphs, Paragraph{ + Children: make([]interface{}, 0, 64), + file: c.file, + }) + + return &c.Paragraphs[len(c.Paragraphs)-1] } // Justification allows to set para's horizonal alignment diff --git a/apitable.go b/apitable.go new file mode 100644 index 0000000..4db86f4 --- /dev/null +++ b/apitable.go @@ -0,0 +1,117 @@ +package docxlib + +import "unsafe" + +//nolint:revive,stylecheck +const ( + TABLE_STYLE = "a3" +) + +// AddTable add a new table to body by col*row +// +// unit: twips (1/20 point) +func (f *Docx) AddTable(row int, col int) *WTable { + trs := make([]*WTableRow, row) + for i := 0; i < row; i++ { + cells := make([]*WTableCell, col) + for i := range cells { + cells[i] = &WTableCell{ + TableCellProperties: &WTableCellProperties{ + TableCellWidth: &WTableCellWidth{Type: "auto"}, + }, + file: f, + } + } + trs[i] = &WTableRow{ + TableRowProperties: &WTableRowProperties{}, + TableCells: cells, + } + } + f.Document.Body.mu.Lock() + defer f.Document.Body.mu.Unlock() + f.Document.Body.Items = append(f.Document.Body.Items, WTable{ + TableProperties: &WTableProperties{ + Style: &WTableStyle{Val: TABLE_STYLE}, + Width: &WTableWidth{Type: "auto"}, + TableBorders: &WTableBorders{ + Top: &WTableBorder{Val: "single", Size: "4", Space: "0", Color: "000000"}, + Left: &WTableBorder{Val: "single", Size: "4", Space: "0", Color: "000000"}, + Bottom: &WTableBorder{Val: "single", Size: "4", Space: "0", Color: "000000"}, + Right: &WTableBorder{Val: "single", Size: "4", Space: "0", Color: "000000"}, + InsideH: &WTableBorder{Val: "single", Size: "4", Space: "0", Color: "000000"}, + InsideV: &WTableBorder{Val: "single", Size: "4", Space: "0", Color: "000000"}, + }, + Look: &WTableLook{ + Val: "0000", + }, + }, + TableGrid: &WTableGrid{}, + TableRows: trs, + }) + + t := f.Document.Body.Items[len(f.Document.Body.Items)-1] + + return *(**WTable)(unsafe.Add(unsafe.Pointer(&t), unsafe.Sizeof(uintptr(0)))) +} + +// AddTableTwips add a new table to body by height and width +// +// unit: twips (1/20 point) +func (f *Docx) AddTableTwips(colWidths []int64, rowHeights []int64) *WTable { + grids := make([]*WGridCol, len(colWidths)) + trs := make([]*WTableRow, len(rowHeights)) + for i, w := range colWidths { + if w > 0 { + grids[i] = &WGridCol{ + W: w, + } + } + } + for i, h := range rowHeights { + cells := make([]*WTableCell, len(colWidths)) + for i := range cells { + cells[i] = &WTableCell{ + TableCellProperties: &WTableCellProperties{ + TableCellWidth: &WTableCellWidth{Type: "auto"}, + }, + file: f, + } + } + trs[i] = &WTableRow{ + TableRowProperties: &WTableRowProperties{}, + TableCells: cells, + } + if h > 0 { + trs[i].TableRowProperties.TableRowHeight = &WTableRowHeight{ + Val: h, + } + } + } + f.Document.Body.mu.Lock() + defer f.Document.Body.mu.Unlock() + f.Document.Body.Items = append(f.Document.Body.Items, WTable{ + TableProperties: &WTableProperties{ + Style: &WTableStyle{Val: TABLE_STYLE}, + Width: &WTableWidth{Type: "auto"}, + TableBorders: &WTableBorders{ + Top: &WTableBorder{Val: "single", Size: "4", Space: "0", Color: "000000"}, + Left: &WTableBorder{Val: "single", Size: "4", Space: "0", Color: "000000"}, + Bottom: &WTableBorder{Val: "single", Size: "4", Space: "0", Color: "000000"}, + Right: &WTableBorder{Val: "single", Size: "4", Space: "0", Color: "000000"}, + InsideH: &WTableBorder{Val: "single", Size: "4", Space: "0", Color: "000000"}, + InsideV: &WTableBorder{Val: "single", Size: "4", Space: "0", Color: "000000"}, + }, + Look: &WTableLook{ + Val: "0000", + }, + }, + TableGrid: &WTableGrid{ + GridCols: grids, + }, + TableRows: trs, + }) + + t := f.Document.Body.Items[len(f.Document.Body.Items)-1] + + return *(**WTable)(unsafe.Add(unsafe.Pointer(&t), unsafe.Sizeof(uintptr(0)))) +} diff --git a/cmd/main/main.go b/cmd/main/main.go index 6f584a1..81b9576 100644 --- a/cmd/main/main.go +++ b/cmd/main/main.go @@ -9,77 +9,86 @@ import ( "github.com/fumiama/docxlib" ) -var fileLocation *string - -func init() { - fileLocation = flag.String("file", "new-file.docx", "file location") - flag.Parse() -} func main() { - fmt.Printf("Preparing new document to write at %s\n", *fileLocation) + fileLocation := flag.String("f", "new-file.docx", "file location") + analyzeOnly := flag.Bool("a", false, "analyze file only") + verbose := flag.Bool("v", false, "verbose mode") + unm := flag.Bool("u", false, "lease unmarshalled file") + flag.Parse() + var w *docxlib.Docx + if !*analyzeOnly { + fmt.Printf("Preparing new document to write at %s\n", *fileLocation) - w := docxlib.NewA4() - // add new paragraph - para1 := w.AddParagraph().Justification("distribute") - r, err := para1.AddAnchorDrawingFrom("testdata/fumiama.JPG") - if err != nil { - panic(err) - } - r.Drawing.Anchor.Size(r.Drawing.Anchor.Extent.CX/4, r.Drawing.Anchor.Extent.CY/4) - r.Drawing.Anchor.BehindDoc = 1 - r.Drawing.Anchor.PositionH.PosOffset = r.Drawing.Anchor.Extent.CX - r.Drawing.Anchor.Graphic.GraphicData.Pic.BlipFill.Blip.AlphaModFix = &docxlib.AAlphaModFix{Amount: 50000} - // add text - para1.AddText("test") - para1.AddText("test font size").Size("44") - para1.AddText("test color").Color("808080") + w = docxlib.NewA4() + // add new paragraph + para1 := w.AddParagraph().Justification("distribute") + r, err := para1.AddAnchorDrawingFrom("testdata/fumiama.JPG") + if err != nil { + panic(err) + } + r.Drawing.Anchor.Size(r.Drawing.Anchor.Extent.CX/4, r.Drawing.Anchor.Extent.CY/4) + r.Drawing.Anchor.BehindDoc = 1 + r.Drawing.Anchor.PositionH.PosOffset = r.Drawing.Anchor.Extent.CX + r.Drawing.Anchor.Graphic.GraphicData.Pic.BlipFill.Blip.AlphaModFix = &docxlib.AAlphaModFix{Amount: 50000} + // add text + para1.AddText("test") + para1.AddText("test font size").Size("44") + para1.AddText("test color").Color("808080") - para2 := w.AddParagraph().Justification("end") - para2.AddText("test font size and color").Size("44").Color("ff0000") + para2 := w.AddParagraph().Justification("end") + para2.AddText("test font size and color").Size("44").Color("ff0000") - nextPara := w.AddParagraph() - nextPara.AddLink("google", `http://google.com`) + nextPara := w.AddParagraph() + nextPara.AddLink("google", `http://google.com`) - para3 := w.AddParagraph().Justification("center") - // add text - para3.AddText("一行2个 inline").Size("44") + para3 := w.AddParagraph().Justification("center") + // add text + para3.AddText("一行2个 inline").Size("44") - para4 := w.AddParagraph().Justification("center") - r, err = para4.AddInlineDrawingFrom("testdata/fumiama.JPG") - if err != nil { - panic(err) - } - r.Drawing.Inline.Size(r.Drawing.Inline.Extent.CX*4/5, r.Drawing.Inline.Extent.CY*4/5) - para4.AddTab().AddTab() - r, err = para4.AddInlineDrawingFrom("testdata/fumiama2x.webp") - if err != nil { - panic(err) - } - r.Drawing.Inline.Size(r.Drawing.Inline.Extent.CX*4/5, r.Drawing.Inline.Extent.CY*4/5) + para4 := w.AddParagraph().Justification("center") + r, err = para4.AddInlineDrawingFrom("testdata/fumiama.JPG") + if err != nil { + panic(err) + } + r.Drawing.Inline.Size(r.Drawing.Inline.Extent.CX*4/5, r.Drawing.Inline.Extent.CY*4/5) + para4.AddTab().AddTab() + r, err = para4.AddInlineDrawingFrom("testdata/fumiama2x.webp") + if err != nil { + panic(err) + } + r.Drawing.Inline.Size(r.Drawing.Inline.Extent.CX*4/5, r.Drawing.Inline.Extent.CY*4/5) - para5 := w.AddParagraph().Justification("center") - // add text - para5.AddText("一行1个 横向 inline").Size("44") + para5 := w.AddParagraph().Justification("center") + // add text + para5.AddText("一行1个 横向 inline").Size("44") - para6 := w.AddParagraph() - _, err = para6.AddInlineDrawingFrom("testdata/fumiamayoko.png") - if err != nil { - panic(err) + para6 := w.AddParagraph() + _, err = para6.AddInlineDrawingFrom("testdata/fumiamayoko.png") + if err != nil { + panic(err) + } + + tbl1 := w.AddTable(3, 2) + for x, r := range tbl1.TableRows { + for y, c := range r.TableCells { + c.AddParagraph().Justification("center").AddText(fmt.Sprintf("(%d, %d)", x, y)) + } + } + f, err := os.Create(*fileLocation) + if err != nil { + panic(err) + } + _, err = w.WriteTo(f) + if err != nil { + panic(err) + } + err = f.Close() + if err != nil { + panic(err) + } + fmt.Println("Document writen. \nNow trying to read it") } - f, err := os.Create(*fileLocation) - if err != nil { - panic(err) - } - _, err = w.WriteTo(f) - if err != nil { - panic(err) - } - err = f.Close() - if err != nil { - panic(err) - } - fmt.Println("Document writen. \nNow trying to read it") // Now let's try to read the file readFile, err := os.Open(*fileLocation) if err != nil { @@ -94,46 +103,87 @@ func main() { if err != nil { panic(err) } - for _, para := range doc.Document.Body.Paragraphs { - fmt.Println("New paragraph") - for _, child := range para.Children { - switch o := child.(type) { - case *docxlib.Run: - if o.Text != nil { - fmt.Printf("\tWe've found a new run with the text ->%s\n", o.Text.Text) - } - if o.Drawing != nil { - if o.Drawing.Inline != nil { - fmt.Printf("\tWe've found a new run with the inline drawing ->%s\n", o.Drawing.Inline.DocPr.Name) - } - if o.Drawing.Anchor != nil { - fmt.Printf("\tWe've found a new run with the anchor drawing ->%s\n", o.Drawing.Anchor.DocPr.Name) + if *verbose { + for _, it := range doc.Document.Body.Items { + switch para := it.(type) { + case docxlib.Paragraph: + fmt.Println("New paragraph") + for _, child := range para.Children { + switch o := child.(type) { + case *docxlib.Run: + if o.Text != nil { + fmt.Printf("\tWe've found a new run with the text ->%s\n", o.Text.Text) + } + if o.Drawing != nil { + if o.Drawing.Inline != nil { + fmt.Printf("\tWe've found a new run with the inline drawing ->%s\n", o.Drawing.Inline.DocPr.Name) + } + if o.Drawing.Anchor != nil { + fmt.Printf("\tWe've found a new run with the anchor drawing ->%s\n", o.Drawing.Anchor.DocPr.Name) + } + } + case *docxlib.Hyperlink: + id := o.ID + text := o.Run.InstrText + link, err := doc.ReferTarget(id) + if err != nil { + fmt.Printf("\tWe found a link with id %s and text %s without target\n", id, text) + } else { + fmt.Printf("\tWe've found a new hyperlink with ref %s and the text %s\n", link, text) + } } } - case *docxlib.Hyperlink: - id := o.ID - text := o.Run.InstrText - link, err := doc.ReferTarget(id) - if err != nil { - fmt.Printf("\tWe found a link with id %s and text %s without target\n", id, text) - } else { - fmt.Printf("\tWe've found a new hyperlink with ref %s and the text %s\n", link, text) + fmt.Print("End of paragraph\n\n") + case docxlib.WTable: + fmt.Println("New table") + for x, r := range para.TableRows { + fmt.Printf("[%d] ", x) + for y, c := range r.TableCells { + fmt.Printf("<%d> %s\t", y, c.Paragraphs[0].Children[0].(*docxlib.Run).Text.Text) + } + fmt.Print("\n") } + fmt.Print("End of table\n\n") } + } - fmt.Print("End of paragraph\n\n") + } - f, err = os.Create("unmarshal_" + *fileLocation) - if err != nil { - panic(err) + if *unm { + f, err := os.Create("unmarshal_" + *fileLocation) + if err != nil { + panic(err) + } + _, err = doc.WriteTo(f) + if err != nil { + panic(err) + } + err = f.Close() + if err != nil { + panic(err) + } } - _, err = doc.WriteTo(f) - if err != nil { - panic(err) - } - err = f.Close() - if err != nil { - panic(err) + fmt.Println("Plain text:") + for _, it := range doc.Document.Body.Items { + switch para := it.(type) { + case docxlib.Paragraph: + fmt.Println(para.String()) + case docxlib.WTable: + fmt.Println("------------------------------") + for x, r := range para.TableRows { + fmt.Printf("[%d] ", x) + for y, c := range r.TableCells { + if len(c.Paragraphs) > 0 && len(c.Paragraphs[0].Children) > 0 { + fmt.Printf("<%d> %s\t", y, c.Paragraphs[0].Children[0].(*docxlib.Run).Text.Text) + } else { + fmt.Print("\t") + } + } + fmt.Print("\n") + } + fmt.Println("------------------------------") + } + } fmt.Println("End of main") } diff --git a/empty.go b/empty.go index 0b2f6ff..afb2734 100644 --- a/empty.go +++ b/empty.go @@ -16,7 +16,7 @@ func newEmptyA4File() *Docx { XMLWP: XMLNS_WP, // XMLWP14: XMLNS_WP14, Body: Body{ - Paragraphs: make([]Paragraph, 0, 64), + Items: make([]interface{}, 0, 64), }, }, docRelation: Relationships{ diff --git a/go.mod b/go.mod index 0e1e5c6..96957da 100644 --- a/go.mod +++ b/go.mod @@ -1,5 +1,5 @@ module github.com/fumiama/docxlib -go 1.16 +go 1.18 require github.com/fumiama/imgsz v0.0.2 diff --git a/structdoc.go b/structdoc.go index 0cd0fe9..3e0aed4 100644 --- a/structdoc.go +++ b/structdoc.go @@ -28,8 +28,8 @@ func getAtt(atts []xml.Attr, name string) string { // Body type Body struct { - mu sync.Mutex - Paragraphs []Paragraph `xml:"w:p,omitempty"` + mu sync.Mutex + Items []interface{} file *Docx } @@ -46,23 +46,32 @@ func (b *Body) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { } if tt, ok := t.(xml.StartElement); ok { - if tt.Name.Local == "p" { + switch tt.Name.Local { + case "p": var value Paragraph err = d.DecodeElement(&value, &tt) if err != nil && !strings.HasPrefix(err.Error(), "expected") { return err } - if len(value.Children) > 0 { - value.file = b.file - b.mu.Lock() - b.Paragraphs = append(b.Paragraphs, value) - b.mu.Unlock() + value.file = b.file + b.mu.Lock() + b.Items = append(b.Items, value) + b.mu.Unlock() + case "tbl": + var value WTable + err = d.DecodeElement(&value, &tt) + if err != nil && !strings.HasPrefix(err.Error(), "expected") { + return err + } + value.file = b.file + b.mu.Lock() + b.Items = append(b.Items, value) + b.mu.Unlock() + default: + err = d.Skip() // skip unsupported tags + if err != nil { + return err } - continue - } - err = d.Skip() // skip unsupported tags - if err != nil { - return err } } } diff --git a/structdoc_test.go b/structdoc_test.go index 68f8ee7..4c718c5 100644 --- a/structdoc_test.go +++ b/structdoc_test.go @@ -26,10 +26,11 @@ func TestUnmarshalPlainStructure(t *testing.T) { if err != nil { t.Fatal(err) } - if len(doc.Body.Paragraphs) != tc.numParagraphs { - t.Fatalf("We expected %d paragraphs, we got %d", tc.numParagraphs, len(doc.Body.Paragraphs)) + if len(doc.Body.Items) != tc.numParagraphs { + t.Fatalf("We expected %d paragraphs, we got %d", tc.numParagraphs, len(doc.Body.Items)) } - for i, p := range doc.Body.Paragraphs { + for i, it := range doc.Body.Items { + p := it.(Paragraph) if len(p.Children) == 0 { t.Fatalf("We were not able to parse paragraph %d", i) } diff --git a/structdrawing_test.go b/structdrawing_test.go index bf9d305..b641713 100644 --- a/structdrawing_test.go +++ b/structdrawing_test.go @@ -29,7 +29,7 @@ func TestDrawingStructure(t *testing.T) { para3 := w.AddParagraph() para3.AddInlineDrawingFrom("testdata/fumiamayoko.png") - f, err := os.Create("TestMarshalInlineDrawingStructure.xml") + f, err := os.Create("TestMarshalDrawingStructure.xml") if err != nil { t.Fatal(err) } @@ -47,7 +47,7 @@ func TestDrawingStructure(t *testing.T) { if err != nil { t.Fatal(err) } - f1, err := os.Create("TestUnmarshalInlineDrawingStructure.xml") + f1, err := os.Create("TestUnmarshalDrawingStructure.xml") if err != nil { t.Fatal(err) } diff --git a/structpara.go b/structpara.go index 22efc4b..ff1228c 100644 --- a/structpara.go +++ b/structpara.go @@ -1,6 +1,8 @@ package docxlib import ( + "crypto/md5" + "encoding/hex" "encoding/xml" "io" "strings" @@ -40,7 +42,7 @@ func (p *ParagraphProperties) UnmarshalXML(d *xml.Decoder, start xml.StartElemen // Paragraph type Paragraph struct { - // XMLName xml.Name `xml:"w:p,omitempty"` + XMLName xml.Name `xml:"w:p,omitempty"` RsidR string `xml:"w:rsidR,attr,omitempty"` RsidRPr string `xml:"w:rsidRPr,attr,omitempty"` @@ -61,8 +63,9 @@ func (p *Paragraph) String() string { id := o.ID text := o.Run.InstrText link, err := p.file.ReferTarget(id) + sb.WriteString("[") sb.WriteString(text) - sb.WriteByte('(') + sb.WriteString("](") if err != nil { sb.WriteString(id) } else { @@ -70,9 +73,36 @@ func (p *Paragraph) String() string { } sb.WriteByte(')') case *Run: - sb.WriteString("run") //TODO: implement + switch { + case o.Text != nil: + sb.WriteString(o.Text.Text) + case o.Drawing != nil: + if o.Drawing.Inline != nil { + sb.WriteString("![inline image: ") + tgt, err := p.file.ReferTarget(o.Drawing.Inline.Graphic.GraphicData.Pic.BlipFill.Blip.Embed) + if err != nil { + sb.WriteString(err.Error()) + } else { + h := md5.Sum(p.file.Media(tgt[6:]).Data) + sb.WriteString(hex.EncodeToString(h[:])) + } + sb.WriteString("]()") + continue + } + if o.Drawing.Anchor != nil { + sb.WriteString("![anchor image: ") + tgt, err := p.file.ReferTarget(o.Drawing.Anchor.Graphic.GraphicData.Pic.BlipFill.Blip.Embed) + if err != nil { + sb.WriteString(err.Error()) + } else { + h := md5.Sum(p.file.Media(tgt[6:]).Data) + sb.WriteString(hex.EncodeToString(h[:])) + } + sb.WriteString("]()") + } + } case *RunProperties: - sb.WriteString("prop") //TODO: implement + sb.WriteString("") //TODO: implement default: continue } diff --git a/structtable.go b/structtable.go index abeb83f..8060370 100644 --- a/structtable.go +++ b/structtable.go @@ -14,14 +14,61 @@ type WTable struct { TableProperties *WTableProperties TableGrid *WTableGrid TableRows []*WTableRow + + file *Docx +} + +// UnmarshalXML implements the xml.Unmarshaler interface. +func (t *WTable) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { + for { + token, err := d.Token() + if err == io.EOF { + break + } + if err != nil { + return err + } + if tt, ok := token.(xml.StartElement); ok { + switch tt.Name.Local { + case "tr": + var value WTableRow + err = d.DecodeElement(&value, &tt) + if err != nil && !strings.HasPrefix(err.Error(), "expected") { + return err + } + value.file = t.file + t.TableRows = append(t.TableRows, &value) + case "tblPr": + t.TableProperties = new(WTableProperties) + err = d.DecodeElement(t.TableProperties, &tt) + if err != nil && !strings.HasPrefix(err.Error(), "expected") { + return err + } + case "tblGrid": + t.TableGrid = new(WTableGrid) + err = d.DecodeElement(t.TableGrid, &tt) + if err != nil && !strings.HasPrefix(err.Error(), "expected") { + return err + } + default: + err = d.Skip() // skip unsupported tags + if err != nil { + return err + } + continue + } + } + } + return nil } // WTableProperties is an element that represents the properties of a table in Word document. type WTableProperties struct { - XMLName xml.Name `xml:"w:tblPr,omitempty"` - Style *WTableStyle - Width *WTableWidth - Look *WTableLook + XMLName xml.Name `xml:"w:tblPr,omitempty"` + Style *WTableStyle + Width *WTableWidth + TableBorders *WTableBorders `xml:"w:tblBorders"` + Look *WTableLook } // UnmarshalXML implements the xml.Unmarshaler interface. @@ -54,6 +101,12 @@ func (t *WTableProperties) UnmarshalXML(d *xml.Decoder, start xml.StartElement) if err != nil && !strings.HasPrefix(err.Error(), "expected") { return err } + case "tblBorders": + t.TableBorders = new(WTableBorders) + err = d.DecodeElement(t.TableBorders, &tt) + if err != nil && !strings.HasPrefix(err.Error(), "expected") { + return err + } default: err = d.Skip() // skip unsupported tags if err != nil { @@ -246,6 +299,8 @@ type WTableRow struct { RsidTr string `xml:"w:rsidTr,attr"` TableRowProperties *WTableRowProperties TableCells []*WTableCell + + file *Docx } // UnmarshalXML ... @@ -284,6 +339,7 @@ func (w *WTableRow) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { if err != nil && !strings.HasPrefix(err.Error(), "expected") { return err } + value.file = w.file w.TableCells = append(w.TableCells, &value) default: err = d.Skip() // skip unsupported tags @@ -299,8 +355,8 @@ func (w *WTableRow) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { // WTableRowProperties represents the properties of a row within a table. type WTableRowProperties struct { - XMLName xml.Name `xml:"w:trPr,omitempty"` - TrHeight *WTrHeight + XMLName xml.Name `xml:"w:trPr,omitempty"` + TableRowHeight *WTableRowHeight } // UnmarshalXML ... @@ -317,17 +373,17 @@ func (t *WTableRowProperties) UnmarshalXML(d *xml.Decoder, start xml.StartElemen if elem, ok := tok.(xml.StartElement); ok { switch elem.Name.Local { case "trHeight": - th := new(WTrHeight) + th := new(WTableRowHeight) for _, attr := range elem.Attr { if attr.Name.Local == "val" { - th.Val, err = strconv.Atoi(attr.Value) + th.Val, err = strconv.ParseInt(attr.Value, 10, 64) if err != nil { return err } break } } - t.TrHeight = th + t.TableRowHeight = th err = d.Skip() if err != nil { return err @@ -343,19 +399,19 @@ func (t *WTableRowProperties) UnmarshalXML(d *xml.Decoder, start xml.StartElemen return nil } -// WTrHeight represents the height of a row within a table. -type WTrHeight struct { +// WTableRowHeight represents the height of a row within a table. +type WTableRowHeight struct { XMLName xml.Name `xml:"w:trHeight,omitempty"` - Val int `xml:"w:val,attr"` + Val int64 `xml:"w:val,attr"` } // WTableCell represents a cell within a table. type WTableCell struct { mu sync.Mutex - XMLName xml.Name `xml:"w:tc,omitempty"` - TcPr *WTcPr - Paragraphs []Paragraph `xml:"w:p,omitempty"` + XMLName xml.Name `xml:"w:tc,omitempty"` + TableCellProperties *WTableCellProperties + Paragraphs []Paragraph `xml:"w:p,omitempty"` file *Docx } @@ -386,12 +442,12 @@ func (r *WTableCell) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error r.mu.Unlock() } case "tcPr": - var value WTcPr + var value WTableCellProperties err = d.DecodeElement(&value, &tt) if err != nil && !strings.HasPrefix(err.Error(), "expected") { return err } - r.TcPr = &value + r.TableCellProperties = &value default: err = d.Skip() // skip unsupported tags if err != nil { @@ -404,16 +460,17 @@ func (r *WTableCell) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error return nil } -// WTcPr represents the properties of a table cell. -type WTcPr struct { - XMLName xml.Name `xml:"w:tcPr,omitempty"` - TableCellWidth *WTableCellWidth `xml:"w:tcW,omitempty"` - GridSpan *WGridSpan `xml:"w:gridSpan,omitempty"` - VAlign *WVerticalAlignment `xml:"w:vAlign,omitempty"` +// WTableCellProperties represents the properties of a table cell. +type WTableCellProperties struct { + XMLName xml.Name `xml:"w:tcPr,omitempty"` + TableCellWidth *WTableCellWidth + GridSpan *WGridSpan + VAlign *WVerticalAlignment + TableBorders *WTableBorders `xml:"w:tcBorders"` } // UnmarshalXML ... -func (r *WTcPr) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { +func (r *WTableCellProperties) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { for { t, err := d.Token() if err == io.EOF { @@ -441,6 +498,12 @@ func (r *WTcPr) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { case "vAlign": r.VAlign = new(WVerticalAlignment) r.VAlign.Val = getAtt(tt.Attr, "val") + case "tcBorders": + r.TableBorders = new(WTableBorders) + err = d.DecodeElement(r.TableBorders, &tt) + if err != nil && !strings.HasPrefix(err.Error(), "expected") { + return err + } default: err = d.Skip() // skip unsupported tags if err != nil { @@ -454,12 +517,104 @@ func (r *WTcPr) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { } // WTableCellWidth represents the width of a table cell. +// +// 在w:tcW元素中,type属性可以有以下几种取值: +// +// "auto":表示表格列宽度由文本或表格布局决定。 +// "dxa":表示表格列宽度使用磅为单位。 +// +// 不同的取值对应着不同的宽度计量单位和宽度定义方式。 type WTableCellWidth struct { XMLName xml.Name `xml:"w:tcW,omitempty"` W int64 `xml:"w,attr"` Type string `xml:"type,attr"` } +// WTableBorders is a structure representing the borders of a Word table. +type WTableBorders struct { + Top *WTableBorder `xml:"w:top,omitempty"` + Left *WTableBorder `xml:"w:left,omitempty"` + Bottom *WTableBorder `xml:"w:bottom,omitempty"` + Right *WTableBorder `xml:"w:right,omitempty"` + InsideH *WTableBorder `xml:"w:insideH,omitempty"` + InsideV *WTableBorder `xml:"w:insideV,omitempty"` +} + +// UnmarshalXML ... +func (w *WTableBorders) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { + for { + t, err := d.Token() + if err == io.EOF { + break + } + if err != nil { + return err + } + + if tt, ok := t.(xml.StartElement); ok { + switch tt.Name.Local { + case "top": + value := new(WTableBorder) + err = d.DecodeElement(value, &tt) + if err != nil && !strings.HasPrefix(err.Error(), "expected") { + return err + } + w.Top = value + case "left": + value := new(WTableBorder) + err = d.DecodeElement(value, &tt) + if err != nil && !strings.HasPrefix(err.Error(), "expected") { + return err + } + w.Left = value + case "bottom": + value := new(WTableBorder) + err = d.DecodeElement(value, &tt) + if err != nil && !strings.HasPrefix(err.Error(), "expected") { + return err + } + w.Bottom = value + case "right": + value := new(WTableBorder) + err = d.DecodeElement(value, &tt) + if err != nil && !strings.HasPrefix(err.Error(), "expected") { + return err + } + w.Right = value + case "insideH": + value := new(WTableBorder) + err = d.DecodeElement(value, &tt) + if err != nil && !strings.HasPrefix(err.Error(), "expected") { + return err + } + w.InsideH = value + case "insideV": + value := new(WTableBorder) + err = d.DecodeElement(value, &tt) + if err != nil && !strings.HasPrefix(err.Error(), "expected") { + return err + } + w.InsideV = value + default: + err = d.Skip() // skip unsupported tags + if err != nil { + return err + } + continue + } + } + } + return nil +} + +// WTableBorder is a structure representing a single border of a Word table. +type WTableBorder struct { + Val string `xml:"val,attr"` + Size string `xml:"sz,attr"` + Space string `xml:"space,attr"` + Color string `xml:"color,attr"` +} + // WGridSpan represents the number of grid columns this cell should span. type WGridSpan struct { XMLName xml.Name `xml:"w:gridSpan,omitempty"` diff --git a/structtable_test.go b/structtable_test.go new file mode 100644 index 0000000..18f9864 --- /dev/null +++ b/structtable_test.go @@ -0,0 +1,79 @@ +package docxlib + +import ( + "encoding/xml" + "hash/crc64" + "io" + "os" + "testing" +) + +func TestTableStructure(t *testing.T) { + w := NewA4() + // add new paragraph + para1 := w.AddParagraph() + // add text + para1.AddText("table") + tab1 := w.AddTable(4, 3) + para2 := tab1.TableRows[3].TableCells[2].AddParagraph() + r, err := para2.AddAnchorDrawingFrom("testdata/fumiama.JPG") + if err != nil { + t.Fatal(err) + } + r.Drawing.Anchor.Graphic.GraphicData.Pic.BlipFill.Blip.AlphaModFix = &AAlphaModFix{Amount: 50000} + r.Drawing.Anchor.Graphic.GraphicData.Pic.NonVisualPicProperties.CNvPicPr.Locks = &APicLocks{NoChangeAspect: 1} + r.Drawing.Anchor.Graphic.GraphicData.Pic.SpPr.Xfrm.Rot = 50000 + para3 := tab1.TableRows[0].TableCells[0].AddParagraph() + para3.AddText("first cell") + + f, err := os.Create("TestMarshalTableStructure.xml") + if err != nil { + t.Fatal(err) + } + defer f.Close() + _, err = marshaller{data: &w.Document}.WriteTo(f) + if err != nil { + t.Fatal(err) + } + _, err = f.Seek(0, io.SeekStart) + if err != nil { + t.Fatal(err) + } + w = NewA4() + err = xml.NewDecoder(f).Decode(&w.Document) + if err != nil { + t.Fatal(err) + } + f1, err := os.Create("TestUnmarshalTableStructure.xml") + if err != nil { + t.Fatal(err) + } + defer f1.Close() + _, err = marshaller{data: &w.Document}.WriteTo(f1) + if err != nil { + t.Fatal(err) + } + _, err = f.Seek(0, io.SeekStart) + if err != nil { + t.Fatal(err) + } + _, err = f1.Seek(0, io.SeekStart) + if err != nil { + t.Fatal(err) + } + h := crc64.New(crc64.MakeTable(crc64.ECMA)) + _, err = io.Copy(h, f) + if err != nil { + t.Fatal(err) + } + crc1 := h.Sum64() + h.Reset() + _, err = io.Copy(h, f1) + if err != nil { + t.Fatal(err) + } + crc2 := h.Sum64() + if crc1 != crc2 { + t.Fail() + } +} diff --git a/unpack.go b/unpack.go index fe01c1c..5dcdcae 100644 --- a/unpack.go +++ b/unpack.go @@ -61,6 +61,8 @@ func (f *Docx) parseDocument(file *zip.File) error { // f.Document.XMLWP14 = XMLNS_WP14 f.Document.XMLName.Space = XMLNS_W f.Document.XMLName.Local = "document" + + f.Document.Body.file = f err = xml.NewDecoder(zf).Decode(&f.Document) if err != nil { return err