Files
file-tool-wails/services/pdf_service.go
2026-06-23 15:33:22 +08:00

325 lines
8.1 KiB
Go

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")
}
conf := api.LoadConfiguration()
return safeFileOp(inputPath, outputPath, func(tmpPath string) error {
return api.OptimizeFile(inputPath, tmpPath, conf)
})
}
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) {
return extractTextWithFitz(inputPath)
}
func (s *PDFService) PDFToImages(inputPath, outputDir, format string, dpi float64) ([]string, error) {
return pdfToImagesWithFitz(inputPath, outputDir, format, dpi)
}
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")
}