mirror of
https://github.com/unidoc/unioffice.git
synced 2025-04-25 13:48:53 +08:00
spreadsheet: add comment support
This adds comment support for sheets. Excel requires a VML drawing with the comment box shape for each comment to display the comment. LibreOffice displays comments fine with or without the shape, and creates the shape for its own comments. For the sake of compatibility, we create comment shapes as well. I know of no other use for the legacy VML support other than comment boxes...
This commit is contained in:
parent
2b50dca676
commit
3bc4675cf6
BIN
_examples/spreadsheet/comments/comments.xlsx
Normal file
BIN
_examples/spreadsheet/comments/comments.xlsx
Normal file
Binary file not shown.
23
_examples/spreadsheet/comments/main.go
Normal file
23
_examples/spreadsheet/comments/main.go
Normal file
@ -0,0 +1,23 @@
|
||||
// Copyright 2017 Baliance. All rights reserved.
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"baliance.com/gooxml/spreadsheet"
|
||||
)
|
||||
|
||||
func main() {
|
||||
ss := spreadsheet.New()
|
||||
sheet := ss.AddSheet()
|
||||
|
||||
sheet.Cell("A1").SetString("Hello World!")
|
||||
sheet.Comments().AddCommentWithStyle("A1", "Gopher", "This looks interesting.")
|
||||
sheet.Comments().AddCommentWithStyle("C10", "Gopher", "This is a different comment.")
|
||||
|
||||
if err := ss.Validate(); err != nil {
|
||||
log.Fatalf("error validating sheet: %s", err)
|
||||
}
|
||||
|
||||
ss.SaveToFile("comments.xlsx")
|
||||
}
|
@ -69,8 +69,7 @@ func TestRawEncode(t *testing.T) {
|
||||
end := strings.LastIndex(xmlStr, "</w:hdrShapeDefaults>")
|
||||
|
||||
gotRaw := xmlStr[beg+20 : end]
|
||||
|
||||
exp := "<o:shapedefaults v:ext=\"edit\" spidmax=\"2049\" xmlns:o=\"urn:schemas-microsoft-com:office:office\" xmlns:v=\"urn:schemas-microsoft-com:vml\"><o:idmap v:ext=\"edit\" data=\"1\"/></o:shapedefaults>"
|
||||
exp := "<o:shapedefaults xmlns=\"urn:schemas-microsoft-com:office:office\" xmlns:o=\"urn:schemas-microsoft-com:office:office\" xmlns:r=\"http://schemas.openxmlformats.org/officeDocument/2006/relationships\" xmlns:s=\"http://schemas.openxmlformats.org/officeDocument/2006/sharedTypes\" xmlns:v=\"urn:schemas-microsoft-com:vml\" xmlns:xml=\"http://www.w3.org/XML/1998/namespace\" spidmax=\"2049\" ext=\"edit\"/>"
|
||||
if gotRaw != exp {
|
||||
t.Errorf("expected\n%q\ngot\n%q\n", exp, gotRaw)
|
||||
}
|
||||
|
@ -329,9 +329,11 @@ func Read(r io.ReaderAt, size int64) (*Document, error) {
|
||||
decMap := zippkg.DecodeMap{}
|
||||
decMap.SetOnNewRelationshipFunc(doc.onNewRelationship)
|
||||
// we should discover all contents by starting with these two files
|
||||
decMap.AddTarget(gooxml.ContentTypesFilename, doc.ContentTypes.X())
|
||||
decMap.AddTarget(gooxml.BaseRelsFilename, doc.Rels.X())
|
||||
decMap.Decode(files)
|
||||
decMap.AddTarget(zippkg.Target{Path: gooxml.ContentTypesFilename, Ifc: doc.ContentTypes.X()})
|
||||
decMap.AddTarget(zippkg.Target{Path: gooxml.BaseRelsFilename, Ifc: doc.Rels.X()})
|
||||
if err := decMap.Decode(files); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, f := range files {
|
||||
if f == nil {
|
||||
@ -467,23 +469,23 @@ func (d *Document) FormFields() []FormField {
|
||||
return ret
|
||||
}
|
||||
|
||||
func (doc *Document) onNewRelationship(decMap *zippkg.DecodeMap, target, typ string, files []*zip.File, rel *relationships.Relationship) error {
|
||||
func (doc *Document) onNewRelationship(decMap *zippkg.DecodeMap, target, typ string, files []*zip.File, rel *relationships.Relationship, src zippkg.Target) error {
|
||||
dt := gooxml.DocTypeDocument
|
||||
|
||||
switch typ {
|
||||
case gooxml.OfficeDocumentType:
|
||||
doc.x = wml.NewDocument()
|
||||
decMap.AddTarget(target, doc.x)
|
||||
decMap.AddTarget(zippkg.Target{Path: target, Ifc: doc.x})
|
||||
// look for the document relationships file as well
|
||||
decMap.AddTarget(zippkg.RelationsPathFor(target), doc.docRels.X())
|
||||
decMap.AddTarget(zippkg.Target{Path: zippkg.RelationsPathFor(target), Ifc: doc.docRels.X()})
|
||||
rel.TargetAttr = gooxml.RelativeFilename(dt, typ, 0)
|
||||
|
||||
case gooxml.CorePropertiesType:
|
||||
decMap.AddTarget(target, doc.CoreProperties.X())
|
||||
decMap.AddTarget(zippkg.Target{Path: target, Ifc: doc.CoreProperties.X()})
|
||||
rel.TargetAttr = gooxml.RelativeFilename(dt, typ, 0)
|
||||
|
||||
case gooxml.ExtendedPropertiesType:
|
||||
decMap.AddTarget(target, doc.AppProperties.X())
|
||||
decMap.AddTarget(zippkg.Target{Path: target, Ifc: doc.AppProperties.X()})
|
||||
rel.TargetAttr = gooxml.RelativeFilename(dt, typ, 0)
|
||||
|
||||
case gooxml.ThumbnailType:
|
||||
@ -507,55 +509,55 @@ func (doc *Document) onNewRelationship(decMap *zippkg.DecodeMap, target, typ str
|
||||
}
|
||||
|
||||
case gooxml.SettingsType:
|
||||
decMap.AddTarget(target, doc.Settings.X())
|
||||
decMap.AddTarget(zippkg.Target{Path: target, Ifc: doc.Settings.X()})
|
||||
rel.TargetAttr = gooxml.RelativeFilename(dt, typ, 0)
|
||||
|
||||
case gooxml.NumberingType:
|
||||
doc.Numbering = NewNumbering()
|
||||
decMap.AddTarget(target, doc.Numbering.X())
|
||||
decMap.AddTarget(zippkg.Target{Path: target, Ifc: doc.Numbering.X()})
|
||||
rel.TargetAttr = gooxml.RelativeFilename(dt, typ, 0)
|
||||
|
||||
case gooxml.StylesType:
|
||||
doc.Styles.Clear()
|
||||
decMap.AddTarget(target, doc.Styles.X())
|
||||
decMap.AddTarget(zippkg.Target{Path: target, Ifc: doc.Styles.X()})
|
||||
rel.TargetAttr = gooxml.RelativeFilename(dt, typ, 0)
|
||||
|
||||
case gooxml.HeaderType:
|
||||
hdr := wml.NewHdr()
|
||||
decMap.AddTarget(zippkg.Target{Path: target, Ifc: hdr, Index: uint32(len(doc.headers))})
|
||||
doc.headers = append(doc.headers, hdr)
|
||||
decMap.AddTarget(target, hdr)
|
||||
rel.TargetAttr = gooxml.RelativeFilename(dt, typ, len(doc.headers))
|
||||
|
||||
case gooxml.FooterType:
|
||||
ftr := wml.NewFtr()
|
||||
decMap.AddTarget(zippkg.Target{Path: target, Ifc: ftr, Index: uint32(len(doc.footers))})
|
||||
doc.footers = append(doc.footers, ftr)
|
||||
decMap.AddTarget(target, ftr)
|
||||
rel.TargetAttr = gooxml.RelativeFilename(dt, typ, len(doc.footers))
|
||||
|
||||
case gooxml.ThemeType:
|
||||
thm := dml.NewTheme()
|
||||
decMap.AddTarget(zippkg.Target{Path: target, Ifc: thm, Index: uint32(len(doc.themes))})
|
||||
doc.themes = append(doc.themes, thm)
|
||||
decMap.AddTarget(target, thm)
|
||||
rel.TargetAttr = gooxml.RelativeFilename(dt, typ, len(doc.themes))
|
||||
|
||||
case gooxml.WebSettingsType:
|
||||
doc.webSettings = wml.NewWebSettings()
|
||||
decMap.AddTarget(target, doc.webSettings)
|
||||
decMap.AddTarget(zippkg.Target{Path: target, Ifc: doc.webSettings})
|
||||
rel.TargetAttr = gooxml.RelativeFilename(dt, typ, 0)
|
||||
|
||||
case gooxml.FontTableType:
|
||||
doc.fontTable = wml.NewFonts()
|
||||
decMap.AddTarget(target, doc.fontTable)
|
||||
decMap.AddTarget(zippkg.Target{Path: target, Ifc: doc.fontTable})
|
||||
rel.TargetAttr = gooxml.RelativeFilename(dt, typ, 0)
|
||||
|
||||
case gooxml.EndNotesType:
|
||||
doc.endNotes = wml.NewEndnotes()
|
||||
decMap.AddTarget(target, doc.endNotes)
|
||||
decMap.AddTarget(zippkg.Target{Path: target, Ifc: doc.endNotes})
|
||||
rel.TargetAttr = gooxml.RelativeFilename(dt, typ, 0)
|
||||
|
||||
case gooxml.FootNotesType:
|
||||
doc.footNotes = wml.NewFootnotes()
|
||||
decMap.AddTarget(target, doc.footNotes)
|
||||
decMap.AddTarget(zippkg.Target{Path: target, Ifc: doc.footNotes})
|
||||
rel.TargetAttr = gooxml.RelativeFilename(dt, typ, 0)
|
||||
|
||||
case gooxml.ImageType:
|
||||
|
24
filenames.go
24
filenames.go
@ -47,6 +47,11 @@ func RelativeFilename(dt DocType, typ string, index int) string {
|
||||
return fmt.Sprintf("../charts/chart%d.xml", index)
|
||||
case DrawingType, DrawingContentType:
|
||||
return fmt.Sprintf("../drawings/drawing%d.xml", index)
|
||||
case CommentsType, CommentsContentType:
|
||||
return fmt.Sprintf("../comments%d.xml", index)
|
||||
|
||||
case VMLDrawingType, VMLDrawingContentType:
|
||||
return fmt.Sprintf("../drawings/vmlDrawing%d.vml", index)
|
||||
|
||||
case ThemeType, ThemeContentType:
|
||||
return fmt.Sprintf("theme/theme%d.xml", index)
|
||||
@ -128,6 +133,8 @@ func AbsoluteFilename(dt DocType, typ string, index int) string {
|
||||
return "xl/styles.xml"
|
||||
case DocTypeDocument:
|
||||
return "word/styles.xml"
|
||||
default:
|
||||
log.Printf("unsupported type %s pair and %v", typ, dt)
|
||||
}
|
||||
|
||||
case ChartType, ChartContentType:
|
||||
@ -145,6 +152,23 @@ func AbsoluteFilename(dt DocType, typ string, index int) string {
|
||||
default:
|
||||
log.Printf("unsupported type %s pair and %v", typ, dt)
|
||||
}
|
||||
|
||||
case CommentsType, CommentsContentType:
|
||||
switch dt {
|
||||
case DocTypeSpreadsheet:
|
||||
return fmt.Sprintf("xl/comments%d.xml", index)
|
||||
default:
|
||||
log.Printf("unsupported type %s pair and %v", typ, dt)
|
||||
}
|
||||
|
||||
case VMLDrawingType, VMLDrawingContentType:
|
||||
switch dt {
|
||||
case DocTypeSpreadsheet:
|
||||
return fmt.Sprintf("xl/drawings/vmlDrawing%d.vml", index)
|
||||
default:
|
||||
log.Fatalf("unsupported type %s pair and %v", typ, dt)
|
||||
}
|
||||
|
||||
// SML
|
||||
case WorksheetType, WorksheetContentType:
|
||||
return fmt.Sprintf("xl/worksheets/sheet%d.xml", index)
|
||||
|
@ -9,6 +9,12 @@ package gooxml
|
||||
|
||||
import "fmt"
|
||||
|
||||
// Float32 returns a copy of v as a pointer.
|
||||
func Float32(v float32) *float32 {
|
||||
x := v
|
||||
return &x
|
||||
}
|
||||
|
||||
// Float64 returns a copy of v as a pointer.
|
||||
func Float64(v float64) *float64 {
|
||||
x := v
|
||||
|
32
schemas.go
32
schemas.go
@ -10,20 +10,20 @@ package gooxml
|
||||
// Consts for content types used throughout the package
|
||||
const (
|
||||
// Common
|
||||
OfficeDocumentType = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument"
|
||||
StylesType = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles"
|
||||
ThemeType = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme"
|
||||
ThemeContentType = "application/vnd.openxmlformats-officedocument.theme+xml"
|
||||
SettingsType = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/settings"
|
||||
ImageType = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image"
|
||||
CommentsType = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments"
|
||||
ThumbnailType = "http://schemas.openxmlformats.org/package/2006/relationships/metadata/thumbnail"
|
||||
DrawingType = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing"
|
||||
DrawingContentType = "application/vnd.openxmlformats-officedocument.drawing+xml"
|
||||
ChartType = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/chart"
|
||||
ChartContentType = "application/vnd.openxmlformats-officedocument.drawingml.chart+xml"
|
||||
HyperLinkType = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink"
|
||||
|
||||
OfficeDocumentType = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument"
|
||||
StylesType = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles"
|
||||
ThemeType = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme"
|
||||
ThemeContentType = "application/vnd.openxmlformats-officedocument.theme+xml"
|
||||
SettingsType = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/settings"
|
||||
ImageType = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image"
|
||||
CommentsType = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments"
|
||||
CommentsContentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml"
|
||||
ThumbnailType = "http://schemas.openxmlformats.org/package/2006/relationships/metadata/thumbnail"
|
||||
DrawingType = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing"
|
||||
DrawingContentType = "application/vnd.openxmlformats-officedocument.drawing+xml"
|
||||
ChartType = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/chart"
|
||||
ChartContentType = "application/vnd.openxmlformats-officedocument.drawingml.chart+xml"
|
||||
HyperLinkType = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink"
|
||||
ExtendedPropertiesType = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties"
|
||||
CorePropertiesType = "http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties"
|
||||
|
||||
@ -50,4 +50,8 @@ const (
|
||||
SlideMasterContentType = "application/vnd.openxmlformats-officedocument.presentationml.slideMaster+xml"
|
||||
SlideLayoutType = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout"
|
||||
SlideLayoutContentType = "application/vnd.openxmlformats-officedocument.presentationml.slideLayout+xml"
|
||||
|
||||
// VML
|
||||
VMLDrawingType = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/vmlDrawing"
|
||||
VMLDrawingContentType = "application/vnd.openxmlformats-officedocument.vmlDrawing"
|
||||
)
|
||||
|
50
spreadsheet/comment.go
Normal file
50
spreadsheet/comment.go
Normal file
@ -0,0 +1,50 @@
|
||||
// Copyright 2017 Baliance. All rights reserved.
|
||||
//
|
||||
// Use of this source code is governed by the terms of the Affero GNU General
|
||||
// Public License version 3.0 as published by the Free Software Foundation and
|
||||
// appearing in the file LICENSE included in the packaging of this file. A
|
||||
// commercial license can be purchased by contacting sales@baliance.com.
|
||||
|
||||
package spreadsheet
|
||||
|
||||
import sml "baliance.com/gooxml/schema/schemas.openxmlformats.org/spreadsheetml"
|
||||
|
||||
// Comment is a single comment within a sheet.
|
||||
type Comment struct {
|
||||
w *Workbook
|
||||
x *sml.CT_Comment
|
||||
cmts *sml.Comments
|
||||
}
|
||||
|
||||
// X returns the inner wrapped XML type.
|
||||
func (c Comment) X() *sml.CT_Comment {
|
||||
return c.x
|
||||
}
|
||||
|
||||
// CellReference returns the cell reference within a sheet that a comment refers
|
||||
// to (e.g. "A1")
|
||||
func (c Comment) CellReference() string {
|
||||
return c.x.RefAttr
|
||||
}
|
||||
|
||||
// SetCellReference sets the cell reference within a sheet that a comment refers
|
||||
// to (e.g. "A1")
|
||||
func (c Comment) SetCellReference(cellRef string) {
|
||||
c.x.RefAttr = cellRef
|
||||
}
|
||||
|
||||
// Author returns the author of the comment
|
||||
func (c Comment) Author() string {
|
||||
if c.x.AuthorIdAttr < uint32(len(c.cmts.Authors.Author)) {
|
||||
return c.cmts.Authors.Author[c.x.AuthorIdAttr]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// SetAuthor sets the author of the comment. If the comment body contains the
|
||||
// author's name (as is the case with Excel and Comments.AddCommentWithStyle, it
|
||||
// will not be changed). This method only changes the metadata author of the
|
||||
// comment.
|
||||
func (c Comment) SetAuthor(author string) {
|
||||
c.x.AuthorIdAttr = Comments{c.w, c.cmts}.getOrCreateAuthor(author)
|
||||
}
|
85
spreadsheet/comments.go
Normal file
85
spreadsheet/comments.go
Normal file
@ -0,0 +1,85 @@
|
||||
// Copyright 2017 Baliance. All rights reserved.
|
||||
//
|
||||
// Use of this source code is governed by the terms of the Affero GNU General
|
||||
// Public License version 3.0 as published by the Free Software Foundation and
|
||||
// appearing in the file LICENSE included in the packaging of this file. A
|
||||
// commercial license can be purchased by contacting sales@baliance.com.
|
||||
|
||||
package spreadsheet
|
||||
|
||||
import (
|
||||
"baliance.com/gooxml/color"
|
||||
sml "baliance.com/gooxml/schema/schemas.openxmlformats.org/spreadsheetml"
|
||||
"baliance.com/gooxml/vmldrawing"
|
||||
)
|
||||
|
||||
// Comments is the container for comments for a single sheet.
|
||||
type Comments struct {
|
||||
w *Workbook
|
||||
x *sml.Comments
|
||||
}
|
||||
|
||||
// MakeComments constructs a new Comments wrapper.
|
||||
func MakeComments(w *Workbook, x *sml.Comments) Comments {
|
||||
return Comments{w, x}
|
||||
}
|
||||
|
||||
// X returns the inner wrapped XML type.
|
||||
func (c Comments) X() *sml.Comments {
|
||||
return c.x
|
||||
}
|
||||
|
||||
// Comments returns the list of comments for this sheet
|
||||
func (c Comments) Comments() []Comment {
|
||||
ret := []Comment{}
|
||||
for _, cmt := range c.x.CommentList.Comment {
|
||||
ret = append(ret, Comment{c.w, cmt, c.x})
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (c Comments) getOrCreateAuthor(author string) uint32 {
|
||||
for i, knownAuthor := range c.x.Authors.Author {
|
||||
if knownAuthor == author {
|
||||
return uint32(i)
|
||||
}
|
||||
}
|
||||
|
||||
// didn't find the author, so add a new one
|
||||
authIdx := uint32(len(c.x.Authors.Author))
|
||||
c.x.Authors.Author = append(c.x.Authors.Author, author)
|
||||
return authIdx
|
||||
}
|
||||
|
||||
// AddComment adds a new comment and returns a RichText which will contain the
|
||||
// styled comment text.
|
||||
func (c Comments) AddComment(cellRef string, author string) RichText {
|
||||
|
||||
cmt := sml.NewCT_Comment()
|
||||
c.x.CommentList.Comment = append(c.x.CommentList.Comment, cmt)
|
||||
cmt.RefAttr = cellRef
|
||||
cmt.AuthorIdAttr = c.getOrCreateAuthor(author)
|
||||
cmt.Text = sml.NewCT_Rst()
|
||||
return RichText{cmt.Text}
|
||||
}
|
||||
|
||||
// AddCommentWithStyle adds a new comment styled in a default way
|
||||
func (c Comments) AddCommentWithStyle(cellRef string, author string, comment string) {
|
||||
rt := c.AddComment(cellRef, author)
|
||||
run := rt.AddRun()
|
||||
run.SetBold(true)
|
||||
run.SetSize(10)
|
||||
run.SetColor(color.Black)
|
||||
run.SetFont("Calibri")
|
||||
run.SetText(author + ":")
|
||||
|
||||
run = rt.AddRun()
|
||||
run.SetSize(10)
|
||||
run.SetFont("Calibri")
|
||||
run.SetColor(color.Black)
|
||||
run.SetText("\r\n" + comment + "\r\n")
|
||||
|
||||
col, rowIdx, _ := ParseCellReference(cellRef)
|
||||
colIdx := ColumnToIndex(col)
|
||||
c.w.vmlDrawings[0].Shape = append(c.w.vmlDrawings[0].Shape, vmldrawing.NewCommentShape(int64(colIdx), int64(rowIdx-1)))
|
||||
}
|
79
spreadsheet/comments_test.go
Normal file
79
spreadsheet/comments_test.go
Normal file
@ -0,0 +1,79 @@
|
||||
// Copyright 2017 Baliance. All rights reserved.
|
||||
//
|
||||
// Use of this source code is governed by the terms of the Affero GNU General
|
||||
// Public License version 3.0 as published by the Free Software Foundation and
|
||||
// appearing in the file LICENSE included in the packaging of this file. A
|
||||
// commercial license can be purchased by contacting sales@baliance.com.
|
||||
|
||||
package spreadsheet_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"baliance.com/gooxml/spreadsheet"
|
||||
)
|
||||
|
||||
func TestComments(t *testing.T) {
|
||||
wb := spreadsheet.New()
|
||||
sheet := wb.AddSheet()
|
||||
c := sheet.Comments()
|
||||
|
||||
expRef := "A1"
|
||||
expAuth := "John Doe"
|
||||
c.AddCommentWithStyle(expRef, expAuth, "This is my comment")
|
||||
if c.X().Authors == nil {
|
||||
t.Fatalf("author should be non-nil")
|
||||
}
|
||||
if len(c.X().Authors.Author) != 1 {
|
||||
t.Errorf("expected one author, got %v", c.X().Authors.Author)
|
||||
}
|
||||
|
||||
auth := c.X().Authors.Author[0]
|
||||
if auth != expAuth {
|
||||
t.Errorf("expected author = %s, got %s", expAuth, auth)
|
||||
}
|
||||
if c.X().CommentList == nil {
|
||||
t.Fatalf("commentlist should be non-nil")
|
||||
}
|
||||
if len(c.X().CommentList.Comment) != 1 {
|
||||
t.Errorf("expected one comment, got %v", c.X().CommentList.Comment)
|
||||
}
|
||||
cmt := c.X().CommentList.Comment[0]
|
||||
if cmt.AuthorIdAttr != 0 {
|
||||
t.Errorf("expected author ID = 0, got %d", cmt.AuthorIdAttr)
|
||||
}
|
||||
if cmt.RefAttr != expRef {
|
||||
t.Errorf("expected ref = %s, got %s", expRef, cmt.RefAttr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommentsReusesAuthorIDs(t *testing.T) {
|
||||
wb := spreadsheet.New()
|
||||
sheet := wb.AddSheet()
|
||||
c := sheet.Comments()
|
||||
|
||||
c.AddCommentWithStyle("A1", "foo", "This is my comment")
|
||||
if c.X().Authors == nil {
|
||||
t.Fatalf("author should be non-nil")
|
||||
}
|
||||
if len(c.X().Authors.Author) != 1 {
|
||||
t.Errorf("expected one author, got %v", c.X().Authors.Author)
|
||||
}
|
||||
|
||||
c.AddCommentWithStyle("B1", "foo", "This is another comment")
|
||||
if c.X().Authors == nil {
|
||||
t.Fatalf("author should be non-nil")
|
||||
}
|
||||
if len(c.X().Authors.Author) != 1 {
|
||||
t.Errorf("expected one author, got %v", c.X().Authors.Author)
|
||||
}
|
||||
|
||||
c.AddCommentWithStyle("C1", "bar", "This is the last comment")
|
||||
if c.X().Authors == nil {
|
||||
t.Fatalf("author should be non-nil")
|
||||
}
|
||||
if len(c.X().Authors.Author) != 2 {
|
||||
t.Errorf("expected two authors, got %v", c.X().Authors.Author)
|
||||
}
|
||||
|
||||
}
|
@ -30,8 +30,9 @@ func New() *Workbook {
|
||||
wb.wbRels.AddRelationship(gooxml.RelativeFilename(gooxml.DocTypeSpreadsheet, gooxml.StylesType, 0), gooxml.StylesType)
|
||||
|
||||
wb.ContentTypes = common.NewContentTypes()
|
||||
wb.ContentTypes.AddDefault("vml", gooxml.VMLDrawingContentType)
|
||||
wb.ContentTypes.AddOverride(gooxml.AbsoluteFilename(gooxml.DocTypeSpreadsheet, gooxml.OfficeDocumentType, 0), "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml")
|
||||
wb.ContentTypes.AddOverride(gooxml.AbsoluteFilename(gooxml.DocTypeSpreadsheet, gooxml.StylesType, 0), "application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml")
|
||||
wb.ContentTypes.AddOverride(gooxml.AbsoluteFilename(gooxml.DocTypeSpreadsheet, gooxml.StylesType, 0), gooxml.SMLStyleSheetContentType)
|
||||
|
||||
wb.SharedStrings = NewSharedStrings()
|
||||
wb.ContentTypes.AddOverride(gooxml.AbsoluteFilename(gooxml.DocTypeSpreadsheet, gooxml.SharedStingsType, 0), gooxml.SharedStringsContentType)
|
||||
|
@ -37,9 +37,11 @@ func Read(r io.ReaderAt, size int64) (*Workbook, error) {
|
||||
decMap := zippkg.DecodeMap{}
|
||||
decMap.SetOnNewRelationshipFunc(wb.onNewRelationship)
|
||||
// we should discover all contents by starting with these two files
|
||||
decMap.AddTarget(gooxml.ContentTypesFilename, wb.ContentTypes.X())
|
||||
decMap.AddTarget(gooxml.BaseRelsFilename, wb.Rels.X())
|
||||
decMap.Decode(files)
|
||||
decMap.AddTarget(zippkg.Target{Path: gooxml.ContentTypesFilename, Ifc: wb.ContentTypes.X()})
|
||||
decMap.AddTarget(zippkg.Target{Path: gooxml.BaseRelsFilename, Ifc: wb.Rels.X()})
|
||||
if err := decMap.Decode(files); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// etra files are things we don't handle yet, or files that happened to have
|
||||
// been in the zip before. We just round-trip them.
|
||||
|
@ -16,6 +16,7 @@ import (
|
||||
"baliance.com/gooxml"
|
||||
"baliance.com/gooxml/common"
|
||||
sml "baliance.com/gooxml/schema/schemas.openxmlformats.org/spreadsheetml"
|
||||
"baliance.com/gooxml/vmldrawing"
|
||||
)
|
||||
|
||||
// Sheet is a single sheet within a workbook.
|
||||
@ -374,3 +375,29 @@ func (s Sheet) Column(idx uint32) Column {
|
||||
colSet.Col = append(colSet.Col, col)
|
||||
return Column{col}
|
||||
}
|
||||
|
||||
// Comments returns the comments for a sheet.
|
||||
func (s Sheet) Comments() Comments {
|
||||
for i, wks := range s.w.xws {
|
||||
if wks == s.x {
|
||||
if s.w.comments[i] == nil {
|
||||
s.w.comments[i] = sml.NewComments()
|
||||
s.w.xwsRels[i].AddAutoRelationship(gooxml.DocTypeSpreadsheet, i+1, gooxml.CommentsType)
|
||||
s.w.ContentTypes.AddOverride(gooxml.AbsoluteFilename(gooxml.DocTypeSpreadsheet, gooxml.CommentsType, i+1), gooxml.CommentsContentType)
|
||||
}
|
||||
if len(s.w.vmlDrawings) == 0 {
|
||||
s.w.vmlDrawings = append(s.w.vmlDrawings, vmldrawing.NewCommentDrawing())
|
||||
vmlID := s.w.xwsRels[i].AddAutoRelationship(gooxml.DocTypeSpreadsheet, 1, gooxml.VMLDrawingType)
|
||||
if s.x.LegacyDrawing == nil {
|
||||
s.x.LegacyDrawing = sml.NewCT_LegacyDrawing()
|
||||
}
|
||||
s.x.LegacyDrawing.IdAttr = vmlID.ID()
|
||||
}
|
||||
return Comments{s.w, s.w.comments[i]}
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("attempted to access comments for non-existent sheet")
|
||||
// should never occur
|
||||
return Comments{}
|
||||
}
|
||||
|
BIN
spreadsheet/testdata/comments.xlsx
vendored
Normal file
BIN
spreadsheet/testdata/comments.xlsx
vendored
Normal file
Binary file not shown.
BIN
spreadsheet/testdata/simple-1.xlsx
vendored
BIN
spreadsheet/testdata/simple-1.xlsx
vendored
Binary file not shown.
BIN
spreadsheet/testdata/simple-2.xlsx
vendored
BIN
spreadsheet/testdata/simple-2.xlsx
vendored
Binary file not shown.
@ -19,6 +19,7 @@ import (
|
||||
|
||||
"baliance.com/gooxml"
|
||||
"baliance.com/gooxml/common"
|
||||
"baliance.com/gooxml/vmldrawing"
|
||||
"baliance.com/gooxml/zippkg"
|
||||
|
||||
dml "baliance.com/gooxml/schema/schemas.openxmlformats.org/drawingml"
|
||||
@ -36,12 +37,14 @@ type Workbook struct {
|
||||
StyleSheet StyleSheet
|
||||
SharedStrings SharedStrings
|
||||
|
||||
comments []*sml.Comments
|
||||
xws []*sml.Worksheet
|
||||
xwsRels []common.Relationships
|
||||
wbRels common.Relationships
|
||||
themes []*dml.Theme
|
||||
drawings []*sd.WsDr
|
||||
drawingRels []common.Relationships
|
||||
vmlDrawings []*vmldrawing.Container
|
||||
charts []*crt.ChartSpace
|
||||
}
|
||||
|
||||
@ -71,9 +74,12 @@ func (wb *Workbook) AddSheet() Sheet {
|
||||
ws.Dimension.RefAttr = "A1"
|
||||
wb.xws = append(wb.xws, ws)
|
||||
wsRel := common.NewRelationships()
|
||||
|
||||
wb.xwsRels = append(wb.xwsRels, wsRel)
|
||||
ws.SheetData = sml.NewCT_SheetData()
|
||||
|
||||
wb.comments = append(wb.comments, nil)
|
||||
|
||||
dt := gooxml.DocTypeSpreadsheet
|
||||
// update the references
|
||||
rid := wb.wbRels.AddAutoRelationship(dt, len(wb.x.Sheets.Sheet), gooxml.WorksheetType)
|
||||
@ -176,9 +182,20 @@ func (wb *Workbook) Save(w io.Writer) error {
|
||||
zippkg.MarshalXML(z, zippkg.RelationsPathFor(fn), wb.drawingRels[i].X())
|
||||
}
|
||||
}
|
||||
for i, drawing := range wb.vmlDrawings {
|
||||
zippkg.MarshalXML(z, gooxml.AbsoluteFilename(dt, gooxml.VMLDrawingType, i+1), drawing)
|
||||
// never seen relationships for a VML drawing yet
|
||||
}
|
||||
|
||||
if err := zippkg.MarshalXML(z, gooxml.ContentTypesFilename, wb.ContentTypes.X()); err != nil {
|
||||
return err
|
||||
}
|
||||
for i, cmt := range wb.comments {
|
||||
if cmt == nil {
|
||||
continue
|
||||
}
|
||||
zippkg.MarshalXML(z, gooxml.AbsoluteFilename(dt, gooxml.CommentsType, i+1), cmt)
|
||||
}
|
||||
|
||||
if err := wb.WriteExtraFiles(z); err != nil {
|
||||
return err
|
||||
@ -237,52 +254,58 @@ func (wb Workbook) SheetCount() int {
|
||||
return len(wb.xws)
|
||||
}
|
||||
|
||||
func (wb *Workbook) onNewRelationship(decMap *zippkg.DecodeMap, target, typ string, files []*zip.File, rel *relationships.Relationship) error {
|
||||
func (wb *Workbook) onNewRelationship(decMap *zippkg.DecodeMap, target, typ string, files []*zip.File, rel *relationships.Relationship, src zippkg.Target) error {
|
||||
dt := gooxml.DocTypeSpreadsheet
|
||||
|
||||
switch typ {
|
||||
case gooxml.OfficeDocumentType:
|
||||
wb.x = sml.NewWorkbook()
|
||||
decMap.AddTarget(target, wb.x)
|
||||
decMap.AddTarget(zippkg.Target{Path: target, Ifc: wb.x})
|
||||
// look for the workbook relationships file as well
|
||||
wb.wbRels = common.NewRelationships()
|
||||
decMap.AddTarget(zippkg.RelationsPathFor(target), wb.wbRels.X())
|
||||
decMap.AddTarget(zippkg.Target{Path: zippkg.RelationsPathFor(target), Ifc: wb.wbRels.X()})
|
||||
rel.TargetAttr = gooxml.RelativeFilename(dt, typ, 0)
|
||||
|
||||
case gooxml.CorePropertiesType:
|
||||
decMap.AddTarget(target, wb.CoreProperties.X())
|
||||
decMap.AddTarget(zippkg.Target{Path: target, Ifc: wb.CoreProperties.X()})
|
||||
rel.TargetAttr = gooxml.RelativeFilename(dt, typ, 0)
|
||||
|
||||
case gooxml.ExtendedPropertiesType:
|
||||
decMap.AddTarget(target, wb.AppProperties.X())
|
||||
decMap.AddTarget(zippkg.Target{Path: target, Ifc: wb.AppProperties.X()})
|
||||
rel.TargetAttr = gooxml.RelativeFilename(dt, typ, 0)
|
||||
|
||||
case gooxml.WorksheetType:
|
||||
ws := sml.NewWorksheet()
|
||||
idx := uint32(len(wb.xws))
|
||||
wb.xws = append(wb.xws, ws)
|
||||
decMap.AddTarget(target, ws)
|
||||
decMap.AddTarget(zippkg.Target{Path: target, Ifc: ws, Index: idx})
|
||||
// look for worksheet rels
|
||||
wksRel := common.NewRelationships()
|
||||
decMap.AddTarget(zippkg.RelationsPathFor(target), wksRel.X())
|
||||
decMap.AddTarget(zippkg.Target{Path: zippkg.RelationsPathFor(target), Ifc: wksRel.X(), Index: idx})
|
||||
wb.xwsRels = append(wb.xwsRels, wksRel)
|
||||
|
||||
// add a comments placeholder that will be replaced if we see a comments
|
||||
// relationship for the current sheet
|
||||
wb.comments = append(wb.comments, nil)
|
||||
|
||||
// fix the relationship target so it points to where we'll save
|
||||
// the worksheet
|
||||
rel.TargetAttr = gooxml.RelativeFilename(dt, typ, len(wb.xws))
|
||||
|
||||
case gooxml.StylesType:
|
||||
wb.StyleSheet = NewStyleSheet(wb)
|
||||
decMap.AddTarget(target, wb.StyleSheet.X())
|
||||
decMap.AddTarget(zippkg.Target{Path: target, Ifc: wb.StyleSheet.X()})
|
||||
rel.TargetAttr = gooxml.RelativeFilename(dt, typ, 0)
|
||||
|
||||
case gooxml.ThemeType:
|
||||
thm := dml.NewTheme()
|
||||
wb.themes = append(wb.themes, thm)
|
||||
decMap.AddTarget(target, thm)
|
||||
decMap.AddTarget(zippkg.Target{Path: target, Ifc: thm})
|
||||
rel.TargetAttr = gooxml.RelativeFilename(dt, typ, len(wb.themes))
|
||||
|
||||
case gooxml.SharedStingsType:
|
||||
wb.SharedStrings = NewSharedStrings()
|
||||
decMap.AddTarget(target, wb.SharedStrings.X())
|
||||
decMap.AddTarget(zippkg.Target{Path: target, Ifc: wb.SharedStrings.X()})
|
||||
rel.TargetAttr = gooxml.RelativeFilename(dt, typ, 0)
|
||||
|
||||
case gooxml.ThumbnailType:
|
||||
@ -307,17 +330,30 @@ func (wb *Workbook) onNewRelationship(decMap *zippkg.DecodeMap, target, typ stri
|
||||
|
||||
case gooxml.DrawingType:
|
||||
drawing := sd.NewWsDr()
|
||||
decMap.AddTarget(target, drawing)
|
||||
idx := uint32(len(wb.drawings))
|
||||
decMap.AddTarget(zippkg.Target{Path: target, Ifc: drawing, Index: idx})
|
||||
wb.drawings = append(wb.drawings, drawing)
|
||||
|
||||
drel := common.NewRelationships()
|
||||
decMap.AddTarget(zippkg.RelationsPathFor(target), drel.X())
|
||||
decMap.AddTarget(zippkg.Target{Path: zippkg.RelationsPathFor(target), Ifc: drel.X(), Index: idx})
|
||||
wb.drawingRels = append(wb.drawingRels, drel)
|
||||
rel.TargetAttr = gooxml.RelativeFilename(dt, typ, len(wb.drawings))
|
||||
|
||||
case gooxml.VMLDrawingType:
|
||||
vd := vmldrawing.NewContainer()
|
||||
idx := uint32(len(wb.vmlDrawings))
|
||||
decMap.AddTarget(zippkg.Target{Path: target, Ifc: vd, Index: idx})
|
||||
wb.vmlDrawings = append(wb.vmlDrawings, vd)
|
||||
|
||||
case gooxml.CommentsType:
|
||||
wb.comments[src.Index] = sml.NewComments()
|
||||
decMap.AddTarget(zippkg.Target{Path: target, Ifc: wb.comments[src.Index], Index: src.Index})
|
||||
rel.TargetAttr = gooxml.RelativeFilename(dt, typ, len(wb.comments))
|
||||
|
||||
case gooxml.ChartType:
|
||||
chart := crt.NewChartSpace()
|
||||
decMap.AddTarget(target, chart)
|
||||
idx := uint32(len(wb.charts))
|
||||
decMap.AddTarget(zippkg.Target{Path: target, Ifc: chart, Index: idx})
|
||||
wb.charts = append(wb.charts, chart)
|
||||
rel.TargetAttr = gooxml.RelativeFilename(dt, typ, len(wb.charts))
|
||||
|
||||
|
@ -208,6 +208,7 @@ func TestAddDefinedName(t *testing.T) {
|
||||
t.Errorf("expected content = %s, got %s", ref, dn.Content())
|
||||
}
|
||||
}
|
||||
|
||||
func ExampleWorkbook_AddDefinedName() {
|
||||
wb := spreadsheet.New()
|
||||
sheet := wb.AddSheet()
|
||||
@ -216,3 +217,19 @@ func ExampleWorkbook_AddDefinedName() {
|
||||
fmt.Printf("%s refers to %s", productNames.Name(), productNames.Content())
|
||||
// Output: ProductNames refers to 'Sheet 1'!$A$2:$A$6
|
||||
}
|
||||
|
||||
func TestOpenComments(t *testing.T) {
|
||||
wb, err := spreadsheet.Open("./testdata/comments.xlsx")
|
||||
if err != nil {
|
||||
t.Fatalf("error opening workbook: %s", err)
|
||||
}
|
||||
|
||||
sheet := wb.Sheets()[0]
|
||||
if len(sheet.Comments().Comments()) != 1 {
|
||||
t.Fatalf("sheet should have returned 1 existing comments")
|
||||
}
|
||||
cmt := sheet.Comments().Comments()[0]
|
||||
if cmt.Author() != "John Doe" {
|
||||
t.Errorf("error reading comment author")
|
||||
}
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ import (
|
||||
"bytes"
|
||||
"encoding/xml"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
@ -139,6 +140,8 @@ func compareFiles(exp, got *zip.File) func(t *testing.T) {
|
||||
gotAll, _ := ioutil.ReadAll(gf)
|
||||
if !bytes.Equal(expAll, gotAll) {
|
||||
dumpXmlDiff(t, expAll, gotAll)
|
||||
fmt.Println(string(expAll))
|
||||
fmt.Println(string(gotAll))
|
||||
t.Errorf("mismatched contents %d vs %d", len(expAll), len(gotAll))
|
||||
}
|
||||
|
||||
@ -181,19 +184,19 @@ func dumpXmlDiff(t *testing.T, exp, got []byte) {
|
||||
xmlIndentFile(expF)
|
||||
xmlIndentFile(gotF)
|
||||
|
||||
a := exec.Command("diff", "-u", expF, gotF)
|
||||
outp, err := a.StdoutPipe()
|
||||
diff := exec.Command("diff", "-u", expF, gotF)
|
||||
outp, err := diff.StdoutPipe()
|
||||
if err != nil {
|
||||
t.Fatalf("error running xmlindent: %s", err)
|
||||
}
|
||||
defer outp.Close()
|
||||
errp, err := a.StderrPipe()
|
||||
errp, err := diff.StderrPipe()
|
||||
if err != nil {
|
||||
t.Fatalf("error running xmlindent: %s", err)
|
||||
}
|
||||
defer errp.Close()
|
||||
|
||||
if err := a.Start(); err != nil {
|
||||
if err := diff.Start(); err != nil {
|
||||
t.Fatalf("error string xmlindent: %s", err)
|
||||
}
|
||||
scanner := bufio.NewScanner(outp)
|
||||
@ -201,7 +204,7 @@ func dumpXmlDiff(t *testing.T, exp, got []byte) {
|
||||
log.Println(scanner.Text())
|
||||
}
|
||||
|
||||
if err := a.Wait(); err != nil {
|
||||
if err := diff.Wait(); err != nil {
|
||||
errOutput, _ := ioutil.ReadAll(errp)
|
||||
t.Fatalf("error waiting on xmlindent: %s [%s]", string(errOutput), err)
|
||||
}
|
||||
|
96
vmldrawing/commentdrawing.go
Normal file
96
vmldrawing/commentdrawing.go
Normal file
@ -0,0 +1,96 @@
|
||||
// Copyright 2017 Baliance. All rights reserved.
|
||||
//
|
||||
// Use of this source code is governed by the terms of the Affero GNU General
|
||||
// Public License version 3.0 as published by the Free Software Foundation and
|
||||
// appearing in the file LICENSE included in the packaging of this file. A
|
||||
// commercial license can be purchased by contacting sales@baliance.com.
|
||||
|
||||
package vmldrawing
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"baliance.com/gooxml"
|
||||
"baliance.com/gooxml/schema/urn/schemas_microsoft_com/office/excel"
|
||||
|
||||
st "baliance.com/gooxml/schema/schemas.openxmlformats.org/officeDocument/2006/sharedTypes"
|
||||
vml "baliance.com/gooxml/schema/urn/schemas_microsoft_com/vml"
|
||||
)
|
||||
|
||||
// NewCommentDrawing constructs a new comment drawing.
|
||||
func NewCommentDrawing() *Container {
|
||||
c := NewContainer()
|
||||
c.Layout = vml.NewOfcShapelayout()
|
||||
c.Layout.ExtAttr = vml.ST_ExtEdit
|
||||
c.Layout.Idmap = vml.NewOfcCT_IdMap()
|
||||
c.Layout.Idmap.DataAttr = gooxml.String("1")
|
||||
c.Layout.Idmap.ExtAttr = vml.ST_ExtEdit
|
||||
|
||||
c.ShapeType = vml.NewShapetype()
|
||||
c.ShapeType.IdAttr = gooxml.String("_x0000_t202")
|
||||
c.ShapeType.CoordsizeAttr = gooxml.String("21600,21600")
|
||||
c.ShapeType.SptAttr = gooxml.Float32(202)
|
||||
c.ShapeType.PathAttr = gooxml.String("m0,0l0,21600,21600,21600,21600,0xe")
|
||||
|
||||
se := vml.NewEG_ShapeElements()
|
||||
c.ShapeType.EG_ShapeElements = append(c.ShapeType.EG_ShapeElements, se)
|
||||
se.Path = vml.NewPath()
|
||||
se.Path.GradientshapeokAttr = st.ST_TrueFalseT
|
||||
se.Path.ConnecttypeAttr = vml.OfcST_ConnectTypeRect
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
// NewCommentShape creates a new comment shape for a given cell index. The
|
||||
// indices here are zero based.
|
||||
func NewCommentShape(col, row int64) *vml.Shape {
|
||||
shape := vml.NewShape()
|
||||
shape.IdAttr = gooxml.String(fmt.Sprintf("cs_%d_%d", col, row))
|
||||
shape.TypeAttr = gooxml.String("#_x0000_t202")
|
||||
// visibility of the comment box is controlled by this visibility style
|
||||
shape.StyleAttr = gooxml.String("position:absolute;margin-left:80pt;margin-top:2pt;width:104pt;height:76pt;z-index:1;visibility:hidden")
|
||||
shape.FillcolorAttr = gooxml.String("#fbf6d6")
|
||||
shape.StrokecolorAttr = gooxml.String("#edeaa1")
|
||||
//shape.InsetmodeAttr = vml.OfcST_InsetModeAuto
|
||||
|
||||
fill := vml.NewEG_ShapeElements()
|
||||
fill.Fill = vml.NewFill()
|
||||
fill.Fill.Color2Attr = gooxml.String("#fbfe82")
|
||||
fill.Fill.AngleAttr = gooxml.Float64(-180)
|
||||
fill.Fill.TypeAttr = vml.ST_FillTypeGradient
|
||||
fill.Fill.Fill = vml.NewOfcFill()
|
||||
fill.Fill.Fill.ExtAttr = vml.ST_ExtView
|
||||
fill.Fill.Fill.TypeAttr = vml.OfcST_FillTypeGradientUnscaled
|
||||
|
||||
shape.EG_ShapeElements = append(shape.EG_ShapeElements, fill)
|
||||
|
||||
shadow := vml.NewEG_ShapeElements()
|
||||
shadow.Shadow = vml.NewShadow()
|
||||
shadow.Shadow.OnAttr = st.ST_TrueFalseT
|
||||
shadow.Shadow.ObscuredAttr = st.ST_TrueFalseT
|
||||
shape.EG_ShapeElements = append(shape.EG_ShapeElements, shadow)
|
||||
|
||||
fpath := vml.NewEG_ShapeElements()
|
||||
fpath.Path = vml.NewPath()
|
||||
fpath.Path.ConnecttypeAttr = vml.OfcST_ConnectTypeNone
|
||||
shape.EG_ShapeElements = append(shape.EG_ShapeElements, fpath)
|
||||
|
||||
tb := vml.NewEG_ShapeElements()
|
||||
tb.Textbox = vml.NewTextbox()
|
||||
tb.Textbox.StyleAttr = gooxml.String("mso-direction-alt:auto")
|
||||
// TODO: add div?
|
||||
shape.EG_ShapeElements = append(shape.EG_ShapeElements, tb)
|
||||
|
||||
cd := vml.NewEG_ShapeElements()
|
||||
cd.ClientData = excel.NewClientData()
|
||||
cd.ClientData.ObjectTypeAttr = excel.ST_ObjectTypeNote
|
||||
cd.ClientData.MoveWithCells = st.ST_TrueFalseBlankT
|
||||
cd.ClientData.SizeWithCells = st.ST_TrueFalseBlankT
|
||||
cd.ClientData.Anchor = gooxml.String("1, 15, 0, 2, 2, 54, 5, 3")
|
||||
cd.ClientData.AutoFill = st.ST_TrueFalseBlankFalse
|
||||
cd.ClientData.Row = gooxml.Int64(row)
|
||||
cd.ClientData.Column = gooxml.Int64(col)
|
||||
shape.EG_ShapeElements = append(shape.EG_ShapeElements, cd)
|
||||
|
||||
return shape
|
||||
}
|
80
vmldrawing/container.go
Normal file
80
vmldrawing/container.go
Normal file
@ -0,0 +1,80 @@
|
||||
// Copyright 2017 Baliance. All rights reserved.
|
||||
//
|
||||
// Use of this source code is governed by the terms of the Affero GNU General
|
||||
// Public License version 3.0 as published by the Free Software Foundation and
|
||||
// appearing in the file LICENSE included in the packaging of this file. A
|
||||
// commercial license can be purchased by contacting sales@baliance.com.
|
||||
|
||||
package vmldrawing
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
|
||||
"baliance.com/gooxml/schema/urn/schemas_microsoft_com/vml"
|
||||
)
|
||||
|
||||
type Container struct {
|
||||
Layout *vml.OfcShapelayout
|
||||
ShapeType *vml.Shapetype
|
||||
Shape []*vml.Shape
|
||||
}
|
||||
|
||||
func NewContainer() *Container {
|
||||
return &Container{}
|
||||
}
|
||||
|
||||
func (c *Container) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||||
start.Attr = append(start.Attr, xml.Attr{Name: xml.Name{Local: "xmlns:v"}, Value: "urn:schemas-microsoft-com:vml"})
|
||||
start.Attr = append(start.Attr, xml.Attr{Name: xml.Name{Local: "xmlns:o"}, Value: "urn:schemas-microsoft-com:office:office"})
|
||||
start.Attr = append(start.Attr, xml.Attr{Name: xml.Name{Local: "xmlns:x"}, Value: "urn:schemas-microsoft-com:office:excel"})
|
||||
start.Name.Local = "xml"
|
||||
e.EncodeToken(start)
|
||||
if c.Layout != nil {
|
||||
se := xml.StartElement{Name: xml.Name{Local: "o:shapelayout"}}
|
||||
e.EncodeElement(c.Layout, se)
|
||||
}
|
||||
if c.ShapeType != nil {
|
||||
se := xml.StartElement{Name: xml.Name{Local: "v:shapetype"}}
|
||||
e.EncodeElement(c.ShapeType, se)
|
||||
}
|
||||
for _, s := range c.Shape {
|
||||
se := xml.StartElement{Name: xml.Name{Local: "v:shape"}}
|
||||
e.EncodeElement(s, se)
|
||||
}
|
||||
return e.EncodeToken(xml.EndElement{Name: start.Name})
|
||||
}
|
||||
|
||||
func (c *Container) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||
c.Shape = nil
|
||||
outer:
|
||||
for {
|
||||
tok, err := d.Token()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switch el := tok.(type) {
|
||||
case xml.StartElement:
|
||||
switch el.Name.Local {
|
||||
case "shapelayout":
|
||||
c.Layout = vml.NewOfcShapelayout()
|
||||
if err := d.DecodeElement(c.Layout, &el); err != nil {
|
||||
return err
|
||||
}
|
||||
case "shapetype":
|
||||
c.ShapeType = vml.NewShapetype()
|
||||
if err := d.DecodeElement(c.ShapeType, &el); err != nil {
|
||||
return err
|
||||
}
|
||||
case "shape":
|
||||
shp := vml.NewShape()
|
||||
if err := d.DecodeElement(shp, &el); err != nil {
|
||||
return err
|
||||
}
|
||||
c.Shape = append(c.Shape, shp)
|
||||
}
|
||||
case xml.EndElement:
|
||||
break outer
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
@ -26,32 +26,38 @@ import (
|
||||
// target doesn't match where gooxml will write the file (e.g. read in
|
||||
// 'xl/worksheets/MyWorksheet.xml' and we'll write out
|
||||
// 'xl/worksheets/sheet1.xml')
|
||||
type OnNewRelationshipFunc func(decMap *DecodeMap, target, typ string, files []*zip.File, rel *relationships.Relationship) error
|
||||
type OnNewRelationshipFunc func(decMap *DecodeMap, target, typ string, files []*zip.File, rel *relationships.Relationship, src Target) error
|
||||
|
||||
// DecodeMap is used to walk a tree of relationships, decoding files and passing
|
||||
// control back to the document.
|
||||
type DecodeMap struct {
|
||||
pathsToIfcs map[string]interface{}
|
||||
pathsToIfcs map[string]Target
|
||||
basePaths map[*relationships.Relationships]string
|
||||
rels []*relationships.Relationships
|
||||
decFunc OnNewRelationshipFunc
|
||||
rels []Target
|
||||
decodeFunc OnNewRelationshipFunc
|
||||
}
|
||||
|
||||
// SetOnNewRelationshipFunc sets the function to be called when a new
|
||||
// relationship has been discovered.
|
||||
func (d *DecodeMap) SetOnNewRelationshipFunc(fn OnNewRelationshipFunc) {
|
||||
d.decFunc = fn
|
||||
d.decodeFunc = fn
|
||||
}
|
||||
|
||||
type Target struct {
|
||||
Path string
|
||||
Ifc interface{}
|
||||
Index uint32
|
||||
}
|
||||
|
||||
// AddTarget allows documents to register decode targets. Path is a path that
|
||||
// will be found in the zip file and ifc is an XML element that the file will be
|
||||
// unmarshaled to.
|
||||
func (d *DecodeMap) AddTarget(path string, ifc interface{}) {
|
||||
func (d *DecodeMap) AddTarget(tgt Target) {
|
||||
if d.pathsToIfcs == nil {
|
||||
d.pathsToIfcs = make(map[string]interface{})
|
||||
d.pathsToIfcs = make(map[string]Target)
|
||||
d.basePaths = make(map[*relationships.Relationships]string)
|
||||
}
|
||||
d.pathsToIfcs[filepath.Clean(path)] = ifc
|
||||
d.pathsToIfcs[filepath.Clean(tgt.Path)] = tgt
|
||||
}
|
||||
|
||||
// Decode loops decoding targets registered with AddTarget and calling th
|
||||
@ -62,11 +68,12 @@ func (d *DecodeMap) Decode(files []*zip.File) error {
|
||||
// if we've loaded any relationships files, notify the document so it
|
||||
// can create elements to receive the decoded version
|
||||
for len(d.rels) > 0 {
|
||||
relFile := d.rels[len(d.rels)-1]
|
||||
relSource := d.rels[len(d.rels)-1]
|
||||
d.rels = d.rels[0 : len(d.rels)-1]
|
||||
for _, r := range relFile.Relationship {
|
||||
bp, _ := d.basePaths[relFile]
|
||||
d.decFunc(d, bp+r.TargetAttr, r.TypeAttr, files, r)
|
||||
relRaw := relSource.Ifc.(*relationships.Relationships)
|
||||
for _, r := range relRaw.Relationship {
|
||||
bp, _ := d.basePaths[relRaw]
|
||||
d.decodeFunc(d, bp+r.TargetAttr, r.TypeAttr, files, r, relSource)
|
||||
}
|
||||
}
|
||||
|
||||
@ -74,19 +81,20 @@ func (d *DecodeMap) Decode(files []*zip.File) error {
|
||||
if f == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// if there is a registered target for the file
|
||||
if dest, ok := d.pathsToIfcs[f.Name]; ok {
|
||||
delete(d.pathsToIfcs, f.Name)
|
||||
// decode to the target and mark the file as nil so we'll skip
|
||||
// it later
|
||||
if err := Decode(f, dest); err != nil {
|
||||
if err := Decode(f, dest.Ifc); err != nil {
|
||||
return err
|
||||
}
|
||||
files[i] = nil
|
||||
|
||||
// we decoded a relationships file, so we need to traverse it
|
||||
if drel, ok := dest.(*relationships.Relationships); ok {
|
||||
d.rels = append(d.rels, drel)
|
||||
if drel, ok := dest.Ifc.(*relationships.Relationships); ok {
|
||||
d.rels = append(d.rels, dest)
|
||||
// find the path that any files mentioned in the
|
||||
// relationships file are relative to
|
||||
basePath, _ := filepath.Split(filepath.Clean(f.Name + "/../"))
|
||||
|
Loading…
x
Reference in New Issue
Block a user