mirror of https://gitee.com/godoos/godoos.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
412 lines
9.5 KiB
412 lines
9.5 KiB
package office
|
|
|
|
import (
|
|
"archive/zip"
|
|
"bufio"
|
|
"bytes"
|
|
"encoding/xml"
|
|
"fmt"
|
|
"io"
|
|
"io/fs"
|
|
"os"
|
|
"strings"
|
|
)
|
|
|
|
// Contains functions to work with data from a zip file
|
|
type ZipData interface {
|
|
files() []*zip.File
|
|
close() error
|
|
}
|
|
|
|
// Type for in memory zip files
|
|
type ZipInMemory struct {
|
|
data *zip.Reader
|
|
}
|
|
|
|
func (d ZipInMemory) files() []*zip.File {
|
|
return d.data.File
|
|
}
|
|
|
|
// Since there is nothing to close for in memory, just nil the data and return nil
|
|
func (d ZipInMemory) close() error {
|
|
return nil
|
|
}
|
|
|
|
// Type for zip files read from disk
|
|
type ZipFile struct {
|
|
data *zip.ReadCloser
|
|
}
|
|
|
|
func (d ZipFile) files() []*zip.File {
|
|
return d.data.File
|
|
}
|
|
|
|
func (d ZipFile) close() error {
|
|
return d.data.Close()
|
|
}
|
|
|
|
type ReplaceDocx struct {
|
|
zipReader ZipData
|
|
content string
|
|
links string
|
|
headers map[string]string
|
|
footers map[string]string
|
|
images map[string]string
|
|
}
|
|
|
|
func (r *ReplaceDocx) Editable() *Docx {
|
|
return &Docx{
|
|
files: r.zipReader.files(),
|
|
content: r.content,
|
|
links: r.links,
|
|
headers: r.headers,
|
|
footers: r.footers,
|
|
images: r.images,
|
|
}
|
|
}
|
|
|
|
func (r *ReplaceDocx) Close() error {
|
|
return r.zipReader.close()
|
|
}
|
|
|
|
type Docx struct {
|
|
files []*zip.File
|
|
content string
|
|
links string
|
|
headers map[string]string
|
|
footers map[string]string
|
|
images map[string]string
|
|
}
|
|
|
|
func (d *Docx) GetContent() string {
|
|
return d.content
|
|
}
|
|
|
|
func (d *Docx) SetContent(content string) {
|
|
d.content = content
|
|
}
|
|
|
|
func (d *Docx) ReplaceRaw(oldString string, newString string, num int) {
|
|
d.content = strings.Replace(d.content, oldString, newString, num)
|
|
}
|
|
|
|
func (d *Docx) Replace(oldString string, newString string, num int) (err error) {
|
|
oldString, err = encode(oldString)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
newString, err = encode(newString)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
d.content = strings.Replace(d.content, oldString, newString, num)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (d *Docx) ReplaceLink(oldString string, newString string, num int) (err error) {
|
|
oldString, err = encode(oldString)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
newString, err = encode(newString)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
d.links = strings.Replace(d.links, oldString, newString, num)
|
|
|
|
return nil
|
|
}
|
|
|
|
func (d *Docx) ReplaceHeader(oldString string, newString string) (err error) {
|
|
return replaceHeaderFooter(d.headers, oldString, newString)
|
|
}
|
|
|
|
func (d *Docx) ReplaceFooter(oldString string, newString string) (err error) {
|
|
return replaceHeaderFooter(d.footers, oldString, newString)
|
|
}
|
|
|
|
func (d *Docx) WriteToFile(path string) (err error) {
|
|
var target *os.File
|
|
target, err = os.Create(path)
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer target.Close()
|
|
err = d.Write(target)
|
|
return
|
|
}
|
|
|
|
func (d *Docx) Write(ioWriter io.Writer) (err error) {
|
|
w := zip.NewWriter(ioWriter)
|
|
for _, file := range d.files {
|
|
var writer io.Writer
|
|
var readCloser io.ReadCloser
|
|
|
|
writer, err = w.Create(file.Name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
readCloser, err = file.Open()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if file.Name == "word/document.xml" {
|
|
writer.Write([]byte(d.content))
|
|
} else if file.Name == "word/_rels/document.xml.rels" {
|
|
writer.Write([]byte(d.links))
|
|
} else if strings.Contains(file.Name, "header") && d.headers[file.Name] != "" {
|
|
writer.Write([]byte(d.headers[file.Name]))
|
|
} else if strings.Contains(file.Name, "footer") && d.footers[file.Name] != "" {
|
|
writer.Write([]byte(d.footers[file.Name]))
|
|
} else if strings.HasPrefix(file.Name, "word/media/") && d.images[file.Name] != "" {
|
|
newImage, err := os.Open(d.images[file.Name])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
writer.Write(streamToByte(newImage))
|
|
newImage.Close()
|
|
} else {
|
|
writer.Write(streamToByte(readCloser))
|
|
}
|
|
}
|
|
w.Close()
|
|
return
|
|
}
|
|
|
|
func replaceHeaderFooter(headerFooter map[string]string, oldString string, newString string) (err error) {
|
|
oldString, err = encode(oldString)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
newString, err = encode(newString)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for k := range headerFooter {
|
|
headerFooter[k] = strings.Replace(headerFooter[k], oldString, newString, -1)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ReadDocxFromFS opens a docx file from the file system
|
|
func ReadDocxFromFS(file string, fs fs.FS) (*ReplaceDocx, error) {
|
|
f, err := fs.Open(file)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
buff := bytes.NewBuffer([]byte{})
|
|
size, err := io.Copy(buff, f)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
reader := bytes.NewReader(buff.Bytes())
|
|
return ReadDocxFromMemory(reader, size)
|
|
}
|
|
|
|
func ReadDocxFromMemory(data io.ReaderAt, size int64) (*ReplaceDocx, error) {
|
|
reader, err := zip.NewReader(data, size)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
zipData := ZipInMemory{data: reader}
|
|
return ReadDocx(zipData)
|
|
}
|
|
|
|
func ReadDocxFile(path string) (*ReplaceDocx, error) {
|
|
reader, err := zip.OpenReader(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
zipData := ZipFile{data: reader}
|
|
return ReadDocx(zipData)
|
|
}
|
|
|
|
func ReadDocx(reader ZipData) (*ReplaceDocx, error) {
|
|
content, err := readText(reader.files())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
links, err := readLinks(reader.files())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
headers, footers, _ := readHeaderFooter(reader.files())
|
|
images, _ := retrieveImageFilenames(reader.files())
|
|
return &ReplaceDocx{zipReader: reader, content: content, links: links, headers: headers, footers: footers, images: images}, nil
|
|
}
|
|
|
|
func retrieveImageFilenames(files []*zip.File) (map[string]string, error) {
|
|
images := make(map[string]string)
|
|
for _, f := range files {
|
|
if strings.HasPrefix(f.Name, "word/media/") {
|
|
images[f.Name] = ""
|
|
}
|
|
}
|
|
return images, nil
|
|
}
|
|
|
|
func readHeaderFooter(files []*zip.File) (headerText map[string]string, footerText map[string]string, err error) {
|
|
|
|
h, f, err := retrieveHeaderFooterDoc(files)
|
|
|
|
if err != nil {
|
|
return map[string]string{}, map[string]string{}, err
|
|
}
|
|
|
|
headerText, err = buildHeaderFooter(h)
|
|
if err != nil {
|
|
return map[string]string{}, map[string]string{}, err
|
|
}
|
|
|
|
footerText, err = buildHeaderFooter(f)
|
|
if err != nil {
|
|
return map[string]string{}, map[string]string{}, err
|
|
}
|
|
|
|
return headerText, footerText, err
|
|
}
|
|
|
|
func buildHeaderFooter(headerFooter []*zip.File) (map[string]string, error) {
|
|
|
|
headerFooterText := make(map[string]string)
|
|
for _, element := range headerFooter {
|
|
documentReader, err := element.Open()
|
|
if err != nil {
|
|
return map[string]string{}, err
|
|
}
|
|
|
|
text, err := wordDocToString(documentReader)
|
|
if err != nil {
|
|
return map[string]string{}, err
|
|
}
|
|
|
|
headerFooterText[element.Name] = text
|
|
}
|
|
|
|
return headerFooterText, nil
|
|
}
|
|
|
|
func readText(files []*zip.File) (text string, err error) {
|
|
var documentFile *zip.File
|
|
documentFile, err = retrieveWordDoc(files)
|
|
if err != nil {
|
|
return text, err
|
|
}
|
|
var documentReader io.ReadCloser
|
|
documentReader, err = documentFile.Open()
|
|
if err != nil {
|
|
return text, err
|
|
}
|
|
|
|
text, err = wordDocToString(documentReader)
|
|
return
|
|
}
|
|
|
|
func readLinks(files []*zip.File) (text string, err error) {
|
|
var documentFile *zip.File
|
|
documentFile, err = retrieveLinkDoc(files)
|
|
if err != nil {
|
|
return text, err
|
|
}
|
|
var documentReader io.ReadCloser
|
|
documentReader, err = documentFile.Open()
|
|
if err != nil {
|
|
return text, err
|
|
}
|
|
|
|
text, err = wordDocToString(documentReader)
|
|
return
|
|
}
|
|
|
|
func wordDocToString(reader io.Reader) (string, error) {
|
|
b, err := io.ReadAll(reader)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return string(b), nil
|
|
}
|
|
|
|
func retrieveWordDoc(files []*zip.File) (file *zip.File, err error) {
|
|
for _, f := range files {
|
|
if f.Name == "word/document.xml" {
|
|
file = f
|
|
}
|
|
}
|
|
if file == nil {
|
|
err = fmt.Errorf("document.xml file not found")
|
|
}
|
|
return
|
|
}
|
|
|
|
func retrieveLinkDoc(files []*zip.File) (file *zip.File, err error) {
|
|
for _, f := range files {
|
|
if f.Name == "word/_rels/document.xml.rels" {
|
|
file = f
|
|
}
|
|
}
|
|
if file == nil {
|
|
err = fmt.Errorf("document.xml.rels file not found")
|
|
}
|
|
return
|
|
}
|
|
|
|
func retrieveHeaderFooterDoc(files []*zip.File) (headers []*zip.File, footers []*zip.File, err error) {
|
|
for _, f := range files {
|
|
|
|
if strings.Contains(f.Name, "header") {
|
|
headers = append(headers, f)
|
|
}
|
|
if strings.Contains(f.Name, "footer") {
|
|
footers = append(footers, f)
|
|
}
|
|
}
|
|
if len(headers) == 0 && len(footers) == 0 {
|
|
err = fmt.Errorf("headers[1-3].xml file not found and footers[1-3].xml file not found")
|
|
}
|
|
return
|
|
}
|
|
|
|
func streamToByte(stream io.Reader) []byte {
|
|
buf := new(bytes.Buffer)
|
|
buf.ReadFrom(stream)
|
|
return buf.Bytes()
|
|
}
|
|
|
|
// To get Word to recognize a tab character, we have to first close off the previous
|
|
// text element. This means if there are multiple consecutive tabs, there are empty <w:t></w:t>
|
|
// in between but it still seems to work correctly in the output document, certainly better
|
|
// than other combinations I tried.
|
|
const TAB = "</w:t><w:tab/><w:t>"
|
|
const NEWLINE = "<w:br/>"
|
|
|
|
func encode(s string) (string, error) {
|
|
var b bytes.Buffer
|
|
enc := xml.NewEncoder(bufio.NewWriter(&b))
|
|
if err := enc.Encode(s); err != nil {
|
|
return s, err
|
|
}
|
|
output := strings.Replace(b.String(), "<string>", "", 1) // remove string tag
|
|
output = strings.Replace(output, "</string>", "", 1)
|
|
output = strings.Replace(output, "
", NEWLINE, -1) // \r\n (Windows newline)
|
|
output = strings.Replace(output, "
", NEWLINE, -1) // \r (earlier Mac newline)
|
|
output = strings.Replace(output, "
", NEWLINE, -1) // \n (unix/linux/OS X newline)
|
|
output = strings.Replace(output, "	", TAB, -1) // \t (tab)
|
|
return output, nil
|
|
}
|
|
|
|
func (d *Docx) ReplaceImage(oldImage string, newImage string) (err error) {
|
|
if _, ok := d.images[oldImage]; ok {
|
|
d.images[oldImage] = newImage
|
|
return nil
|
|
}
|
|
return fmt.Errorf("old image: %q, file not found", oldImage)
|
|
}
|
|
|
|
func (d *Docx) ImagesLen() int {
|
|
return len(d.images)
|
|
}
|
|
|