package services import ( "fmt" "image" "image/jpeg" "image/png" "os" "path/filepath" "strings" "time" "github.com/pdfcpu/pdfcpu/pkg/api" "github.com/signintech/gopdf" ) type PDFService struct{} func NewPDFService() *PDFService { return &PDFService{} } type PDFInfo struct { Pages int `json:"pages"` Title string `json:"title"` Author string `json:"author"` FilePath string `json:"filePath"` Size int64 `json:"size"` } func (s *PDFService) GetPDFInfo(filePath string) (*PDFInfo, error) { fileInfo, err := os.Stat(filePath) if err != nil { return nil, fmt.Errorf("获取文件信息失败: %v", err) } pages, err := api.PageCountFile(filePath) if err != nil { pages = 1 } return &PDFInfo{ Pages: pages, Title: filepath.Base(filePath), Author: "", FilePath: filePath, Size: fileInfo.Size(), }, nil } func safeFileOp(inputPath, outputPath string, op func(tmpPath string) error) error { tmpDir := os.TempDir() tmpFile := filepath.Join(tmpDir, "xk_pdf_"+fmt.Sprintf("%d", time.Now().UnixNano())+".pdf") defer os.Remove(tmpFile) if err := op(tmpFile); err != nil { return err } data, err := os.ReadFile(tmpFile) if err != nil { return fmt.Errorf("read temp file: %v", err) } if err := os.WriteFile(outputPath, data, 0644); err != nil { return fmt.Errorf("write output file (may be locked by another process): %v", err) } return nil } func (s *PDFService) OptimizePDF(inputPath, outputPath, level string) error { if outputPath == "" { outputPath = GetOutputPath(inputPath, "_optimized") } // 记录原始文件大小 originalInfo, err := os.Stat(inputPath) if err != nil { return fmt.Errorf("获取原文件信息失败: %v", err) } originalSize := originalInfo.Size() conf := api.LoadConfiguration() err = safeFileOp(inputPath, outputPath, func(tmpPath string) error { return api.OptimizeFile(inputPath, tmpPath, conf) }) if err != nil { return err } // 检查优化后的文件大小 optimizedInfo, err := os.Stat(outputPath) if err != nil { return fmt.Errorf("获取优化文件信息失败: %v", err) } optimizedSize := optimizedInfo.Size() // 计算减少的百分比 reduction := float64(originalSize-optimizedSize) / float64(originalSize) * 100 // 如果优化后文件变大或没有减少,删除输出文件并返回提示 if optimizedSize >= originalSize { os.Remove(outputPath) return fmt.Errorf("PDF已是最优状态,无法进一步压缩。原始大小: %s, 优化后: %s", formatSize(originalSize), formatSize(optimizedSize)) } // 记录优化效果 fmt.Printf("PDF优化: %s -> %s (%.1f%% 减少)\n", formatSize(originalSize), formatSize(optimizedSize), reduction) return nil } func (s *PDFService) SplitPDF(inputPath, outputDir string, pageRanges string) ([]string, error) { if outputDir == "" { outputDir = filepath.Dir(inputPath) } conf := api.LoadConfiguration() if pageRanges != "" { pageSelection, err := api.ParsePageSelection(pageRanges) if err != nil { return nil, fmt.Errorf("解析页码范围失败: %v", err) } pageCount, err := api.PageCountFile(inputPath) if err != nil { return nil, fmt.Errorf("获取页数失败: %v", err) } pageNrs, err := api.PagesForPageCollection(pageCount, pageSelection) if err != nil { return nil, fmt.Errorf("解析页码失败: %v", err) } baseName := GetBaseName(inputPath) err = api.SplitByPageNrFile(inputPath, outputDir, pageNrs, conf) if err != nil { return nil, fmt.Errorf("分割PDF失败: %v", err) } var results []string entries, _ := os.ReadDir(outputDir) for _, e := range entries { if !e.IsDir() && strings.HasSuffix(e.Name(), ".pdf") && strings.Contains(e.Name(), baseName) { results = append(results, filepath.Join(outputDir, e.Name())) } } return results, nil } baseName := GetBaseName(inputPath) err := api.SplitFile(inputPath, outputDir, 1, conf) if err != nil { return nil, fmt.Errorf("分割PDF失败: %v", err) } var results []string entries, _ := os.ReadDir(outputDir) for _, e := range entries { if !e.IsDir() && strings.HasSuffix(e.Name(), ".pdf") && strings.HasPrefix(e.Name(), baseName) { results = append(results, filepath.Join(outputDir, e.Name())) } } return results, nil } func (s *PDFService) MergePDFs(inputPaths []string, outputPath string) error { if outputPath == "" { outputPath = filepath.Join(filepath.Dir(inputPaths[0]), "merged.pdf") } conf := api.LoadConfiguration() return api.MergeCreateFile(inputPaths, outputPath, false, conf) } func (s *PDFService) RotatePDF(inputPath, outputPath string, rotation int) error { if outputPath == "" { outputPath = GetOutputPath(inputPath, "_rotated") } conf := api.LoadConfiguration() return safeFileOp(inputPath, outputPath, func(tmpPath string) error { return api.RotateFile(inputPath, tmpPath, rotation, nil, conf) }) } func (s *PDFService) AddWatermark(inputPath, outputPath, text string) error { if outputPath == "" { outputPath = GetOutputPath(inputPath, "_watermarked") } conf := api.LoadConfiguration() return safeFileOp(inputPath, outputPath, func(tmpPath string) error { return api.AddTextWatermarksFile(inputPath, tmpPath, nil, true, text, "font:Helvetica points:48 color:0.8,0.8,0.8 rotation:45", conf) }) } func (s *PDFService) ExtractText(inputPath string) (string, error) { // 检查是否应使用nofitz模式 if os.Getenv("XK_NO_FITZ") == "1" { return extractTextNoFitz(inputPath) } // 尝试fitz,失败则降级 text, err := extractTextWithFitz(inputPath) if err != nil { // 如果是MuPDF相关错误,切换到nofitz模式 errMsg := err.Error() if contains(errMsg, []string{"MuPDF", "DLL", "libmupdf", "not found"}) { os.Setenv("XK_NO_FITZ", "1") return extractTextNoFitz(inputPath) } return "", err } return text, nil } func (s *PDFService) PDFToImages(inputPath, outputDir, format string, dpi float64) ([]string, error) { // 检查是否应使用nofitz模式 if os.Getenv("XK_NO_FITZ") == "1" { return pdfToImagesNoFitz(inputPath, outputDir, format, dpi) } images, err := pdfToImagesWithFitz(inputPath, outputDir, format, dpi) if err != nil { // 如果是MuPDF相关错误,切换到nofitz模式 errMsg := err.Error() if contains(errMsg, []string{"MuPDF", "DLL", "libmupdf", "not found"}) { os.Setenv("XK_NO_FITZ", "1") return pdfToImagesNoFitz(inputPath, outputDir, format, dpi) } return nil, err } return images, nil } // contains 检查字符串是否包含任意一个子串 func contains(s string, substrs []string) bool { for _, substr := range substrs { if len(s) >= len(substr) { for i := 0; i <= len(s)-len(substr); i++ { if s[i:i+len(substr)] == substr { return true } } } } return false } func savePNG(img image.Image, path string) error { f, err := os.Create(path) if err != nil { return err } defer f.Close() return png.Encode(f, img) } func saveJPEG(img image.Image, path string, quality int) error { f, err := os.Create(path) if err != nil { return err } defer f.Close() return jpeg.Encode(f, img, &jpeg.Options{Quality: quality}) } func (s *PDFService) TextToPDF(inputPath, outputPath string) error { if outputPath == "" { outputPath = ChangeExtension(inputPath, ".pdf") } data, err := os.ReadFile(inputPath) if err != nil { return fmt.Errorf("读取文本文件失败: %v", err) } text := string(data) if strings.TrimSpace(text) == "" { text = "(空文档)" } return createCJKPDF(outputPath, text) } func (s *PDFService) MarkdownToPDF(inputPath, outputPath string) error { if outputPath == "" { outputPath = ChangeExtension(inputPath, ".pdf") } data, err := os.ReadFile(inputPath) if err != nil { return fmt.Errorf("读取Markdown文件失败: %v", err) } lines := strings.Split(string(data), "\n") 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", } var fontLoaded bool for _, fp := range fontPaths { if _, err := os.Stat(fp); err == nil { if err := pdf.AddTTFFont("cjk", fp); err == nil { fontLoaded = true break } } } pdf.AddPage() y := 50.0 pageHeight := 800.0 marginLeft := 50.0 for _, line := range lines { if y > pageHeight-30 { pdf.AddPage() y = 50.0 } trimmed := strings.TrimSpace(line) if strings.HasPrefix(trimmed, "# ") { if fontLoaded { pdf.SetFont("cjk", "", 20) } y += 4 } else if strings.HasPrefix(trimmed, "## ") { if fontLoaded { pdf.SetFont("cjk", "", 16) } y += 3 } else if strings.HasPrefix(trimmed, "### ") { if fontLoaded { pdf.SetFont("cjk", "", 14) } y += 2 } else { if fontLoaded { pdf.SetFont("cjk", "", 11) } } displayLine := trimmed displayLine = strings.ReplaceAll(displayLine, "**", "") displayLine = strings.ReplaceAll(displayLine, "*", "") displayLine = strings.ReplaceAll(displayLine, "`", "") displayLine = strings.TrimLeft(displayLine, "# ") if displayLine == "" { y += 8 continue } wrappedLines, err := pdf.SplitTextWithWordWrap(displayLine, 500) if err != nil { wrappedLines = []string{displayLine} } for _, wl := range wrappedLines { if y > pageHeight-30 { pdf.AddPage() y = 50.0 } pdf.SetXY(marginLeft, y) pdf.Cell(nil, wl) y += 16 } } return pdf.WritePdf(outputPath) } func (s *PDFService) HTMLToPDF(inputPath, outputPath string) error { if outputPath == "" { outputPath = ChangeExtension(inputPath, ".pdf") } data, err := os.ReadFile(inputPath) if err != nil { return fmt.Errorf("读取HTML文件失败: %v", err) } text := stripHTML(string(data)) if strings.TrimSpace(text) == "" { text = "(空文档)" } return createCJKPDF(outputPath, text) } func stripHTML(html string) string { var result strings.Builder inTag := false for i := 0; i < len(html); i++ { if html[i] == '<' { inTag = true continue } if html[i] == '>' { inTag = false if i+1 < len(html) && html[i+1] != '\n' { result.WriteByte('\n') } continue } if !inTag { result.WriteByte(html[i]) } } text := result.String() lines := strings.Split(text, "\n") var cleaned []string for _, line := range lines { line = strings.TrimSpace(line) if line != "" { cleaned = append(cleaned, line) } } return strings.Join(cleaned, "\n") }