package services import ( "archive/zip" "fmt" "os" "path/filepath" "strings" "github.com/signintech/gopdf" ) type WordService struct{} func NewWordService() *WordService { return &WordService{} } type WordInfo struct { Title string `json:"title"` Author string `json:"author"` Pages int `json:"pages"` Words int `json:"words"` FilePath string `json:"filePath"` Size int64 `json:"size"` } func (s *WordService) GetWordInfo(filePath string) (*WordInfo, error) { fileInfo, err := os.Stat(filePath) if err != nil { return nil, fmt.Errorf("获取文件信息失败: %v", err) } text, _ := s.ExtractText(filePath) words := len(strings.Fields(text)) return &WordInfo{ Title: filepath.Base(filePath), Author: "", Pages: 1, Words: words, FilePath: filePath, Size: fileInfo.Size(), }, nil } func (s *WordService) ConvertToPDF(inputPath, outputPath string) error { if outputPath == "" { outputPath = ChangeExtension(inputPath, ".pdf") } text, err := s.ExtractText(inputPath) if err != nil { return fmt.Errorf("提取Word文本失败: %v", err) } if strings.TrimSpace(text) == "" { text = "(空文档)" } return createCJKPDF(outputPath, text) } func (s *WordService) ExtractText(inputPath string) (string, error) { r, err := zip.OpenReader(inputPath) if err != nil { return extractPlainText(inputPath) } defer r.Close() var docBody []byte for _, f := range r.File { if f.Name == "word/document.xml" { rc, err := f.Open() if err != nil { continue } docBody, err = readAll(rc) rc.Close() if err != nil { continue } break } } if docBody == nil { return extractPlainText(inputPath) } text := extractXMLText(string(docBody)) return text, nil } func readAll(r interface{ Read([]byte) (int, error) }) ([]byte, error) { buf := make([]byte, 0, 4096) tmp := make([]byte, 1024) for { n, err := r.Read(tmp) buf = append(buf, tmp[:n]...) if err != nil { break } } return buf, nil } func extractPlainText(filePath string) (string, error) { data, err := os.ReadFile(filePath) if err != nil { return "", err } return string(data), nil } func extractXMLText(xml string) string { var result strings.Builder inTag := false for i := 0; i < len(xml); i++ { if xml[i] == '<' { inTag = true if i+1 < len(xml) && xml[i+1] == '/' { if result.Len() > 0 { ch := result.String() if !strings.HasSuffix(ch, "\n") && !strings.HasSuffix(ch, " ") { result.WriteString(" ") } } } continue } if xml[i] == '>' { inTag = false continue } if !inTag { result.WriteByte(xml[i]) } } text := result.String() text = strings.ReplaceAll(text, " ", "\n") text = strings.ReplaceAll(text, "&", "&") text = strings.ReplaceAll(text, "<", "<") text = strings.ReplaceAll(text, ">", ">") text = strings.ReplaceAll(text, """, "\"") text = strings.ReplaceAll(text, "'", "'") text = strings.ReplaceAll(text, "\n ", "\n") text = strings.TrimSpace(text) var lines []string for _, line := range strings.Split(text, "\n") { line = strings.TrimSpace(line) if line != "" { lines = append(lines, line) } } return strings.Join(lines, "\n") } func createCJKPDF(outputPath, text string) error { pdf := gopdf.GoPdf{} pdf.Start(gopdf.Config{PageSize: *gopdf.PageSizeA4}) fontPaths := []string{ "C:\\Windows\\Fonts\\msyh.ttc", "C:\\Windows\\Fonts\\simhei.ttf", "C:\\Windows\\Fonts\\simsun.ttc", "C:\\Windows\\Fonts\\msyhbd.ttc", "/usr/share/fonts/truetype/noto/NotoSansCJK-Regular.ttc", "/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc", } var fontLoaded bool for _, fp := range fontPaths { if _, err := os.Stat(fp); err == nil { err = pdf.AddTTFFont("cjk", fp) if err == nil { fontLoaded = true break } } } pdf.AddPage() lines := strings.Split(text, "\n") y := 50.0 pageHeight := 800.0 lineHeight := 16.0 marginLeft := 50.0 for _, line := range lines { if y > pageHeight-30 { pdf.AddPage() y = 50.0 } if fontLoaded { pdf.SetFont("cjk", "", 11) } else { pdf.SetFont("helvetica", "", 11) } wrappedLines, err := pdf.SplitTextWithWordWrap(line, 500) if err != nil { wrappedLines = []string{line} } for _, wl := range wrappedLines { if y > pageHeight-30 { pdf.AddPage() y = 50.0 } pdf.SetXY(marginLeft, y) pdf.Cell(nil, wl) y += lineHeight } } return pdf.WritePdf(outputPath) }