Files
file-tool-wails/services/pdf_service.go
2026-06-30 12:30:49 +08:00

408 lines
10 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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")
}