package api import ( "context" "fmt" "html" "os" "path/filepath" "strings" "time" "github.com/chromedp/cdproto/page" "github.com/chromedp/chromedp" "github.com/yuin/goldmark" "u-desk/internal/common" ) // PdfExportRequest PDF导出请求结构体 type PdfExportRequest struct { Content string `json:"content"` // Markdown/HTML内容 Title string `json:"title"` // PDF标题 FileName string `json:"fileName"` // 文件名(不含扩展名) FontSize int `json:"fontSize"` // 字体大小 PageWidth int `json:"pageWidth"` // 页面宽度(mm) PageHeight int `json:"pageHeight"` // 页面高度(mm) } // PdfExportResponse PDF导出响应结构体 type PdfExportResponse struct { Success bool `json:"success"` Message string `json:"message"` Path string `json:"path"` // PDF文件保存路径 Size int64 `json:"size"` // 文件大小(字节) } // PdfAPI PDF导出API type PdfAPI struct { // 可以在这里添加依赖,如文件系统服务等 } // NewPdfAPI 创建PDF导出API func NewPdfAPI() (*PdfAPI, error) { return &PdfAPI{}, nil } // ExportMarkdownToPDF 将Markdown内容导出为PDF - 使用chromedp实现 func (api *PdfAPI) ExportMarkdownToPDF(req PdfExportRequest) (*PdfExportResponse, error) { // 验证参数 if strings.TrimSpace(req.Content) == "" { return nil, fmt.Errorf("内容不能为空") } if strings.TrimSpace(req.FileName) == "" { req.FileName = "document_" + time.Now().Format("20060102_150405") } if req.FontSize <= 0 { req.FontSize = 12 } // 设置默认页面尺寸(A4) if req.PageWidth <= 0 { req.PageWidth = 210 } if req.PageHeight <= 0 { req.PageHeight = 297 } // 将Markdown转换为HTML htmlContent := api.markdownToHTML(req.Content, req.Title, req.FontSize) // 使用chromedp生成PDF pdfBuffer, err := api.generatePDFFromHTML(htmlContent, req.Title, req.PageWidth, req.PageHeight) if err != nil { return nil, fmt.Errorf("生成PDF失败: %v", err) } // 生成文件名 if !strings.HasSuffix(strings.ToLower(req.FileName), ".pdf") { req.FileName += ".pdf" } // 获取用户桌面目录作为默认保存位置 saveDir := api.getDesktopDirectory() // 确保目录存在 if err := os.MkdirAll(saveDir, 0755); err != nil { return nil, fmt.Errorf("创建目录失败: %v", err) } // 完整保存路径 savePath := filepath.Join(saveDir, filepath.Base(req.FileName)) // 保存PDF文件 err = os.WriteFile(savePath, pdfBuffer, 0644) if err != nil { return nil, fmt.Errorf("保存PDF文件失败: %v", err) } // 获取文件信息 fileInfo, err := os.Stat(savePath) if err != nil { return nil, fmt.Errorf("获取文件信息失败: %v", err) } // 返回成功响应 return &PdfExportResponse{ Success: true, Message: "PDF生成成功", Path: savePath, Size: fileInfo.Size(), }, nil } // markdownToHTML 将Markdown转换为HTML func (api *PdfAPI) markdownToHTML(markdownContent string, title string, fontSize int) string { // 基础HTML模板 htmlTemplate := `
%s
%s ` // 标题处理 docTitle := "" if title != "" { docTitle = html.EscapeString(title) } else { docTitle = "文档" } // Markdown转HTML(使用goldmark) var htmlContent string var htmlBuf strings.Builder if err := goldmark.Convert([]byte(markdownContent), &htmlBuf); err != nil { htmlContent = "

Markdown 解析失败

" } else { htmlContent = htmlBuf.String() } // 生成完整的HTML fullHTML := fmt.Sprintf(htmlTemplate, fontSize, docTitle, htmlContent) return fullHTML } // generatePDFFromHTML 使用chromedp从HTML生成PDF func (api *PdfAPI) generatePDFFromHTML(htmlContent, title string, pageWidth, pageHeight int) ([]byte, error) { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() // 配置chromedp选项 opts := []chromedp.ExecAllocatorOption{ chromedp.Flag("headless", true), chromedp.Flag("disable-gpu", true), chromedp.Flag("no-sandbox", true), chromedp.Flag("disable-dev-shm-usage", true), chromedp.Flag("disable-software-rasterizer", true), chromedp.Flag("disable-extensions", true), chromedp.Flag("disable-notifications", true), } // 在Windows上设置Chrome路径 if common.IsWindows() { // 常见的Windows Chrome路径 chromePaths := []string{ "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe", "C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe", "C:\\Users\\" + os.Getenv("USERNAME") + "\\AppData\\Local\\Google\\Chrome\\Application\\chrome.exe", } for _, path := range chromePaths { if _, err := os.Stat(path); err == nil { opts = append(opts, chromedp.ExecPath(path)) break } } } // 创建执行分配器上下文 allocCtx, allocCancel := chromedp.NewExecAllocator(ctx, opts...) defer allocCancel() // 创建chromedp上下文 chromeCtx, chromeCancel := chromedp.NewContext(allocCtx) defer chromeCancel() // 创建一个临时的目录用于PDF生成 tempDir, err := os.MkdirTemp("", "pdf_gen") if err != nil { return nil, fmt.Errorf("创建临时目录失败: %v", err) } defer os.RemoveAll(tempDir) // 将HTML写入临时文件 htmlFile := filepath.Join(tempDir, "document.html") if err := os.WriteFile(htmlFile, []byte(htmlContent), 0644); err != nil { return nil, fmt.Errorf("写入HTML文件失败: %v", err) } var buf []byte // 使用 file URL 加载本地HTML文件 err = chromedp.Run(chromeCtx, // 导航到HTML文件 chromedp.Navigate("file://"+htmlFile), // 等待页面加载完成 chromedp.WaitReady("body"), // 打印到PDF chromedp.ActionFunc(func(ctx context.Context) error { // 设置页面打印参数 printToPDF := page.PrintToPDF(). WithPrintBackground(true). WithLandscape(false). WithMarginTop(0). WithMarginBottom(0). WithMarginLeft(0). WithMarginRight(0). WithPaperWidth(float64(pageWidth) / 25.4). // mm to inches WithPaperHeight(float64(pageHeight) / 25.4) // mm to inches // 执行打印并获取PDF数据 var err error buf, _, err = printToPDF.Do(ctx) return err }), ) if err != nil { return nil, fmt.Errorf("chromedp执行失败: %v", err) } return buf, nil } // getDesktopDirectory 获取用户桌面目录 func (api *PdfAPI) getDesktopDirectory() string { // Windows系统 if common.IsWindows() { home := os.Getenv("USERPROFILE") if home != "" { return filepath.Join(home, "Desktop") } } // Linux/Mac系统 home := os.Getenv("HOME") if home != "" { return filepath.Join(home, "Desktop") } // 备用:当前目录 return "." } // SelectDirectory 选择保存目录(简化版,实际应该使用Wails runtime) func (api *PdfAPI) SelectDirectory() (string, error) { // 简化版:直接返回桌面目录 desktop := api.getDesktopDirectory() if desktop == "." { return "", fmt.Errorf("无法确定默认目录") } return desktop, nil }