From d19032acadff99283ccc7d7cb9b59a587fb0b7a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=BA=90=E6=96=87=E9=9B=A8?= <41315874+fumiama@users.noreply.github.com> Date: Mon, 27 Feb 2023 16:12:56 +0800 Subject: [PATCH] add AddShape api --- apidrawing.go | 16 +++++----- apishape.go | 73 ++++++++++++++++++++++++++++++++++++++++++ cmd/main/main.go | 12 +++++++ docx.go | 8 +++-- empty.go | 1 + id.go | 10 ++++++ image.go | 8 ++++- structdrawing.go | 20 ++++++------ structshape.go | 7 +++-- structshape_test.go | 77 +++++++++++++++++++++++++++++++++++++++++++++ unpack.go | 2 ++ 11 files changed, 211 insertions(+), 23 deletions(-) create mode 100644 apishape.go create mode 100644 id.go create mode 100644 structshape_test.go diff --git a/apidrawing.go b/apidrawing.go index aca3abd..e20614e 100644 --- a/apidrawing.go +++ b/apidrawing.go @@ -35,9 +35,9 @@ func (p *Paragraph) AddInlineDrawing(pic []byte) (*Run, error) { if err != nil { return nil, err } - idn := int(atomic.AddUintptr(&p.file.imageID, 1)) - id := strconv.Itoa(idn) - rid := p.file.addImage(Media{Name: "image" + id + "." + format, Data: pic}) + idn := int(atomic.AddUintptr(&p.file.docID, 1)) + id := strconv.Itoa(int(p.file.IncreaseID("图片"))) + rid := p.file.addImage(format, pic) w, h := int64(sz.Width), int64(sz.Height) if float64(w)/float64(h) > 1.2 { h = A4_EMU_MAX_WIDTH * h / w @@ -61,7 +61,7 @@ func (p *Paragraph) AddInlineDrawing(pic []byte) (*Run, error) { Name: "图片 " + id, }, CNvGraphicFramePr: &WPCNvGraphicFramePr{ - Locks: &AGraphicFrameLocks{ + Locks: AGraphicFrameLocks{ NoChangeAspect: 1, }, }, @@ -136,9 +136,9 @@ func (p *Paragraph) AddAnchorDrawing(pic []byte) (*Run, error) { if err != nil { return nil, err } - idn := int(atomic.AddUintptr(&p.file.imageID, 1)) - id := strconv.Itoa(idn) - rid := p.file.addImage(Media{Name: "image" + id + "." + format, Data: pic}) + idn := int(atomic.AddUintptr(&p.file.docID, 1)) + id := strconv.Itoa(int(p.file.IncreaseID("图片"))) + rid := p.file.addImage(format, pic) w, h := int64(sz.Width), int64(sz.Height) if float64(w)/float64(h) > 1.2 { h = A4_EMU_MAX_WIDTH * h / w @@ -171,7 +171,7 @@ func (p *Paragraph) AddAnchorDrawing(pic []byte) (*Run, error) { Name: "图片 " + id, }, CNvGraphicFramePr: &WPCNvGraphicFramePr{ - Locks: &AGraphicFrameLocks{ + Locks: AGraphicFrameLocks{ NoChangeAspect: 1, }, }, diff --git a/apishape.go b/apishape.go new file mode 100644 index 0000000..9507111 --- /dev/null +++ b/apishape.go @@ -0,0 +1,73 @@ +package docx + +import ( + "strconv" + "sync/atomic" +) + +// AddShape adds wsp named drawing to paragraph +func (p *Paragraph) AddShape(w, h int64, name, bwMode, prst string, elems []interface{}) (*Run, error) { + idn := int(atomic.AddUintptr(&p.file.docID, 1)) + id := strconv.Itoa(int(p.file.IncreaseID(name))) + d := &Drawing{ + Anchor: &WPAnchor{ + LayoutInCell: 1, + AllowOverlap: 1, + + SimplePosXY: &WPSimplePos{}, + PositionH: &WPPositionH{ + RelativeFrom: "column", + }, + PositionV: &WPPositionV{ + RelativeFrom: "paragraph", + }, + + Extent: &WPExtent{ + CX: w, + CY: h, + }, + EffectExtent: &WPEffectExtent{}, + WrapNone: &struct{}{}, + DocPr: &WPDocPr{ + ID: idn, + Name: name + " " + id, + }, + CNvGraphicFramePr: &WPCNvGraphicFramePr{}, + Graphic: &AGraphic{ + XMLA: XMLNS_DRAWINGML_MAIN, + GraphicData: &AGraphicData{ + URI: XMLNS_WPS, + Shape: &WPSWordprocessingShape{ + CNvCnPr: &WPSCNvCnPr{ + ConnShapeLocks: &struct{}{}, + }, + SpPr: &WPSSpPr{ + BWMode: bwMode, + + Xfrm: AXfrm{ + Ext: AExt{ + CX: w, + CY: h, + }, + }, + PrstGeom: APrstGeom{ + Prst: prst, + }, + NoFill: &struct{}{}, + Elems: elems, + }, + BodyPr: &WPSBodyPr{}, + }, + }, + }, + }, + } + c := make([]interface{}, 1, 64) + c[0] = d + run := &Run{ + RunProperties: &RunProperties{}, + Children: c, + } + p.Children = append(p.Children, run) + return run, nil +} diff --git a/cmd/main/main.go b/cmd/main/main.go index 503c4b6..89a04ce 100644 --- a/cmd/main/main.go +++ b/cmd/main/main.go @@ -123,6 +123,18 @@ func main() { } tbl2.TableRows[0].TableCells[0].Shade("clear", "auto", "E7E6E6") + p := w.AddParagraph().Justification("center") + p.AddText("测试 AutoShape w:ln").Size("44") + p.AddShape(808355, 238760, "AutoShape", "auto", "straightConnector1", []interface{}{ + &docx.ALine{ + W: 9525, + SolidFill: &docx.ASolidFill{SrgbClr: &docx.ASrgbClr{Val: "000000"}}, + Round: &struct{}{}, + HeadEnd: &struct{}{}, + TailEnd: &struct{}{}, + }, + }) + f, err := os.Create(*fileLocation) if err != nil { panic(err) diff --git a/docx.go b/docx.go index bd49a45..3e7dbe2 100644 --- a/docx.go +++ b/docx.go @@ -27,6 +27,7 @@ import ( "bytes" "io" "io/fs" + "sync" ) // Docx is the structure that allow to access the internal represntation @@ -39,8 +40,11 @@ type Docx struct { media []Media mediaNameIdx map[string]int - rID uintptr - imageID uintptr + rID uintptr + imageID uintptr + docID uintptr + slowIDs map[string]uintptr + slowIDsMu sync.Mutex template string tmplfs fs.FS diff --git a/empty.go b/empty.go index 7322a7e..340f679 100644 --- a/empty.go +++ b/empty.go @@ -74,6 +74,7 @@ func newEmptyA4File() *Docx { media: make([]Media, 0, 64), mediaNameIdx: make(map[string]int, 64), rID: 5, + slowIDs: make(map[string]uintptr, 64), template: "a4", tmpfslst: []string{ "_rels/.rels", diff --git a/id.go b/id.go new file mode 100644 index 0000000..2721561 --- /dev/null +++ b/id.go @@ -0,0 +1,10 @@ +package docx + +func (f *Docx) IncreaseID(name string) (n uintptr) { + f.slowIDsMu.Lock() + n, _ = f.slowIDs[name] //nolint: go-staticcheck + n++ + f.slowIDs[name] = n + f.slowIDsMu.Unlock() + return +} diff --git a/image.go b/image.go index a58ab11..93183c2 100644 --- a/image.go +++ b/image.go @@ -20,8 +20,14 @@ package docx +import ( + "strconv" + "sync/atomic" +) + // addImage add image to docx and return its rId -func (f *Docx) addImage(m Media) string { +func (f *Docx) addImage(format string, data []byte) string { + m := Media{Name: "image" + strconv.Itoa(int(atomic.AddUintptr(&f.imageID, 1))) + "." + format, Data: data} f.addMedia(m) return f.addImageRelation(m) } diff --git a/structdrawing.go b/structdrawing.go index b476189..87151d4 100644 --- a/structdrawing.go +++ b/structdrawing.go @@ -312,7 +312,7 @@ func (r *WPDocPr) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { // WPCNvGraphicFramePr represents the non-visual properties of a graphic frame. type WPCNvGraphicFramePr struct { XMLName xml.Name `xml:"wp:cNvGraphicFramePr,omitempty"` - Locks *AGraphicFrameLocks + Locks AGraphicFrameLocks } // UnmarshalXML ... @@ -329,20 +329,14 @@ func (w *WPCNvGraphicFramePr) UnmarshalXML(d *xml.Decoder, start xml.StartElemen if tt, ok := t.(xml.StartElement); ok { switch tt.Name.Local { case "graphicFrameLocks": - var value AGraphicFrameLocks - err = d.DecodeElement(&value, &tt) - if err != nil && !strings.HasPrefix(err.Error(), "expected") { - return err - } v := getAtt(tt.Attr, "noChangeAspect") if v == "" { continue } - value.NoChangeAspect, err = strconv.Atoi(v) + w.Locks.NoChangeAspect, err = strconv.Atoi(v) if err != nil { return err } - w.Locks = &value default: err = d.Skip() // skip unsupported tags if err != nil { @@ -358,7 +352,7 @@ func (w *WPCNvGraphicFramePr) UnmarshalXML(d *xml.Decoder, start xml.StartElemen // AGraphicFrameLocks represents the locks applied to a graphic frame. type AGraphicFrameLocks struct { XMLName xml.Name `xml:"http://schemas.openxmlformats.org/drawingml/2006/main graphicFrameLocks,omitempty"` - NoChangeAspect int `xml:"noChangeAspect,attr"` + NoChangeAspect int `xml:"noChangeAspect,attr,omitempty"` } // AGraphic represents a graphic in a Word document. @@ -414,6 +408,7 @@ type AGraphicData struct { XMLName xml.Name `xml:"a:graphicData,omitempty"` URI string `xml:"uri,attr"` Pic *PICPic + Shape *WPSWordprocessingShape } // UnmarshalXML ... @@ -437,6 +432,13 @@ func (a *AGraphicData) UnmarshalXML(d *xml.Decoder, start xml.StartElement) erro } value.XMLPIC = getAtt(tt.Attr, "pic") a.Pic = &value + case "wsp": + var value WPSWordprocessingShape + err = d.DecodeElement(&value, &tt) + if err != nil && !strings.HasPrefix(err.Error(), "expected") { + return err + } + a.Shape = &value default: err = d.Skip() // skip unsupported tags if err != nil { diff --git a/structshape.go b/structshape.go index 5695ca2..b6770ec 100644 --- a/structshape.go +++ b/structshape.go @@ -98,7 +98,7 @@ type WPSSpPr struct { Xfrm AXfrm PrstGeom APrstGeom NoFill *struct{} `xml:"a:noFill,omitempty"` - Ln *ALine + Elems []interface{} } // UnmarshalXML ... @@ -132,11 +132,12 @@ func (w *WPSSpPr) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error { case "noFill": w.NoFill = &struct{}{} case "ln": - w.Ln = &ALine{} - err = d.DecodeElement(&w.Ln, &tt) + var ln ALine + err = d.DecodeElement(&ln, &tt) if err != nil && !strings.HasPrefix(err.Error(), "expected") { return err } + w.Elems = append(w.Elems, &ln) default: err = d.Skip() // skip unsupported tags if err != nil { diff --git a/structshape_test.go b/structshape_test.go new file mode 100644 index 0000000..1b9a8bf --- /dev/null +++ b/structshape_test.go @@ -0,0 +1,77 @@ +package docx + +import ( + "encoding/xml" + "hash/crc64" + "io" + "os" + "testing" +) + +func TestShapeStructure(t *testing.T) { + w := NewA4() + // add new paragraph + para1 := w.AddParagraph() + // add text + para1.AddText("test shape") + para1.AddShape(808355, 238760, "AutoShape", "auto", "straightConnector1", []interface{}{ + &ALine{ + W: 9525, + SolidFill: &ASolidFill{SrgbClr: &ASrgbClr{Val: "000000"}}, + Round: &struct{}{}, + HeadEnd: &struct{}{}, + TailEnd: &struct{}{}, + }, + }) + + f, err := os.Create("TestMarshalShapeStructure.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("TestUnmarshalShapeStructure.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 dd954f3..5e70e81 100644 --- a/unpack.go +++ b/unpack.go @@ -39,6 +39,7 @@ import ( func unpack(zipReader *zip.Reader) (docx *Docx, err error) { docx = new(Docx) docx.mediaNameIdx = make(map[string]int, 64) + docx.slowIDs = make(map[string]uintptr, 64) docx.tmplfs = zipReader docx.tmpfslst = make([]string, 0, 64) for _, f := range zipReader.File { @@ -88,6 +89,7 @@ func (f *Docx) parseDocument(file *zip.File) error { f.Document.XMLName.Local = "document" f.Document.Body.file = f + //TODO: find last docID err = xml.NewDecoder(zf).Decode(&f.Document) return err }