Files

408 lines
10 KiB
Go
Raw Permalink Normal View History

2026-06-11 15:46:19 +08:00
package services
import (
"fmt"
2026-06-23 15:33:22 +08:00
"image"
"image/jpeg"
"image/png"
2026-06-11 15:46:19 +08:00
"os"
"path/filepath"
2026-06-17 08:05:20 +08:00
"strings"
2026-06-23 15:33:22 +08:00
"time"
"github.com/pdfcpu/pdfcpu/pkg/api"
"github.com/signintech/gopdf"
2026-06-11 15:46:19 +08:00
)
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)
}
2026-06-23 15:33:22 +08:00
pages, err := api.PageCountFile(filePath)
if err != nil {
pages = 1
2026-06-17 08:05:20 +08:00
}
2026-06-11 15:46:19 +08:00
return &PDFInfo{
2026-06-17 08:05:20 +08:00
Pages: pages,
Title: filepath.Base(filePath),
Author: "",
2026-06-11 15:46:19 +08:00
FilePath: filePath,
Size: fileInfo.Size(),
}, nil
}
2026-06-23 15:33:22 +08:00
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)
2026-06-11 15:46:19 +08:00
2026-06-23 15:33:22 +08:00
if err := op(tmpFile); err != nil {
return err
2026-06-11 15:46:19 +08:00
}
2026-06-23 15:33:22 +08:00
data, err := os.ReadFile(tmpFile)
2026-06-11 15:46:19 +08:00
if err != nil {
2026-06-23 15:33:22 +08:00
return fmt.Errorf("read temp file: %v", err)
2026-06-11 15:46:19 +08:00
}
2026-06-23 15:33:22 +08:00
if err := os.WriteFile(outputPath, data, 0644); err != nil {
return fmt.Errorf("write output file (may be locked by another process): %v", err)
}
2026-06-11 15:46:19 +08:00
return nil
}
2026-06-23 15:33:22 +08:00
func (s *PDFService) OptimizePDF(inputPath, outputPath, level string) error {
if outputPath == "" {
outputPath = GetOutputPath(inputPath, "_optimized")
}
2026-06-30 12:30:49 +08:00
// 记录原始文件大小
originalInfo, err := os.Stat(inputPath)
if err != nil {
return fmt.Errorf("获取原文件信息失败: %v", err)
}
originalSize := originalInfo.Size()
2026-06-23 15:33:22 +08:00
conf := api.LoadConfiguration()
2026-06-30 12:30:49 +08:00
err = safeFileOp(inputPath, outputPath, func(tmpPath string) error {
2026-06-23 15:33:22 +08:00
return api.OptimizeFile(inputPath, tmpPath, conf)
})
2026-06-30 12:30:49 +08:00
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
2026-06-23 15:33:22 +08:00
}
func (s *PDFService) SplitPDF(inputPath, outputDir string, pageRanges string) ([]string, error) {
2026-06-11 15:46:19 +08:00
if outputDir == "" {
outputDir = filepath.Dir(inputPath)
}
2026-06-23 15:33:22 +08:00
conf := api.LoadConfiguration()
2026-06-11 15:46:19 +08:00
2026-06-23 15:33:22 +08:00
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
2026-06-17 08:05:20 +08:00
}
2026-06-23 15:33:22 +08:00
baseName := GetBaseName(inputPath)
err := api.SplitFile(inputPath, outputDir, 1, conf)
2026-06-11 15:46:19 +08:00
if err != nil {
2026-06-23 15:33:22 +08:00
return nil, fmt.Errorf("分割PDF失败: %v", err)
2026-06-11 15:46:19 +08:00
}
2026-06-23 15:33:22 +08:00
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
2026-06-11 15:46:19 +08:00
}
func (s *PDFService) MergePDFs(inputPaths []string, outputPath string) error {
if outputPath == "" {
outputPath = filepath.Join(filepath.Dir(inputPaths[0]), "merged.pdf")
}
2026-06-23 15:33:22 +08:00
conf := api.LoadConfiguration()
return api.MergeCreateFile(inputPaths, outputPath, false, conf)
}
2026-06-11 15:46:19 +08:00
2026-06-23 15:33:22 +08:00
func (s *PDFService) RotatePDF(inputPath, outputPath string, rotation int) error {
if outputPath == "" {
outputPath = GetOutputPath(inputPath, "_rotated")
2026-06-11 15:46:19 +08:00
}
2026-06-23 15:33:22 +08:00
conf := api.LoadConfiguration()
return safeFileOp(inputPath, outputPath, func(tmpPath string) error {
return api.RotateFile(inputPath, tmpPath, rotation, nil, conf)
})
}
2026-06-11 15:46:19 +08:00
2026-06-23 15:33:22 +08:00
func (s *PDFService) AddWatermark(inputPath, outputPath, text string) error {
if outputPath == "" {
outputPath = GetOutputPath(inputPath, "_watermarked")
2026-06-11 15:46:19 +08:00
}
2026-06-23 15:33:22 +08:00
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)
})
}
2026-06-11 15:46:19 +08:00
2026-06-23 15:33:22 +08:00
func (s *PDFService) ExtractText(inputPath string) (string, error) {
2026-06-30 12:30:49 +08:00
// 检查是否应使用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
2026-06-11 15:46:19 +08:00
}
2026-06-23 15:33:22 +08:00
func (s *PDFService) PDFToImages(inputPath, outputDir, format string, dpi float64) ([]string, error) {
2026-06-30 12:30:49 +08:00
// 检查是否应使用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
2026-06-23 15:33:22 +08:00
}
2026-06-11 15:46:19 +08:00
2026-06-23 15:33:22 +08:00
func savePNG(img image.Image, path string) error {
f, err := os.Create(path)
2026-06-11 15:46:19 +08:00
if err != nil {
2026-06-23 15:33:22 +08:00
return err
2026-06-11 15:46:19 +08:00
}
2026-06-23 15:33:22 +08:00
defer f.Close()
return png.Encode(f, img)
}
2026-06-11 15:46:19 +08:00
2026-06-23 15:33:22 +08:00
func saveJPEG(img image.Image, path string, quality int) error {
f, err := os.Create(path)
2026-06-11 15:46:19 +08:00
if err != nil {
2026-06-23 15:33:22 +08:00
return err
2026-06-11 15:46:19 +08:00
}
2026-06-23 15:33:22 +08:00
defer f.Close()
return jpeg.Encode(f, img, &jpeg.Options{Quality: quality})
2026-06-11 15:46:19 +08:00
}
2026-06-23 15:33:22 +08:00
func (s *PDFService) TextToPDF(inputPath, outputPath string) error {
2026-06-11 15:46:19 +08:00
if outputPath == "" {
2026-06-23 15:33:22 +08:00
outputPath = ChangeExtension(inputPath, ".pdf")
2026-06-11 15:46:19 +08:00
}
2026-06-23 15:33:22 +08:00
data, err := os.ReadFile(inputPath)
2026-06-11 15:46:19 +08:00
if err != nil {
2026-06-23 15:33:22 +08:00
return fmt.Errorf("读取文本文件失败: %v", err)
}
text := string(data)
if strings.TrimSpace(text) == "" {
text = "(空文档)"
2026-06-11 15:46:19 +08:00
}
2026-06-23 15:33:22 +08:00
return createCJKPDF(outputPath, text)
}
2026-06-11 15:46:19 +08:00
2026-06-23 15:33:22 +08:00
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)
}
2026-06-11 15:46:19 +08:00
2026-06-23 15:33:22 +08:00
func (s *PDFService) HTMLToPDF(inputPath, outputPath string) error {
if outputPath == "" {
outputPath = ChangeExtension(inputPath, ".pdf")
}
data, err := os.ReadFile(inputPath)
2026-06-11 15:46:19 +08:00
if err != nil {
2026-06-23 15:33:22 +08:00
return fmt.Errorf("读取HTML文件失败: %v", err)
2026-06-11 15:46:19 +08:00
}
2026-06-23 15:33:22 +08:00
text := stripHTML(string(data))
if strings.TrimSpace(text) == "" {
text = "(空文档)"
}
return createCJKPDF(outputPath, text)
}
2026-06-11 15:46:19 +08:00
2026-06-23 15:33:22 +08:00
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")
2026-06-11 15:46:19 +08:00
}