408 lines
10 KiB
Go
408 lines
10 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")
|
||
}
|
||
|
||
// 记录原始文件大小
|
||
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")
|
||
}
|