新增: HTML预览导出PDF并兼容Edge环境
- chromedp 通道新增源文件入口,file:// 加载原始HTML - 相对资源在原目录天然解析,printBackground 保背景色 - 预览工具栏新增导出按钮,默认文件名取当前HTML名 - 远程文件复用站点缓存路径,本地远程同一命令 - 浏览器探测扩Chrome+Edge双查,Markdown通道重复探测归一
This commit is contained in:
+169
-15
@@ -4,12 +4,15 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"html"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/chromedp/cdproto/page"
|
||||
"github.com/chromedp/cdproto/runtime"
|
||||
"github.com/chromedp/chromedp"
|
||||
"github.com/yuin/goldmark"
|
||||
"u-desk/internal/common"
|
||||
@@ -275,22 +278,12 @@ func (api *PdfAPI) generatePDFFromHTML(htmlContent, title string, pageWidth, pag
|
||||
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
|
||||
}
|
||||
}
|
||||
// 浏览器路径统一走 findChromeExecutable,Chrome 与 Edge 任一可用即可
|
||||
browserPath, err := findChromeExecutable()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
opts = append(opts, chromedp.ExecPath(browserPath))
|
||||
|
||||
// 创建执行分配器上下文
|
||||
allocCtx, allocCancel := chromedp.NewExecAllocator(ctx, opts...)
|
||||
@@ -348,6 +341,167 @@ func (api *PdfAPI) generatePDFFromHTML(htmlContent, title string, pageWidth, pag
|
||||
return buf, nil
|
||||
}
|
||||
|
||||
// findChromeExecutable 探测本机可用的浏览器可执行文件,Chrome 优先,全部未命中再探测 Edge
|
||||
func findChromeExecutable() (string, error) {
|
||||
if common.IsWindows() {
|
||||
candidates := []string{
|
||||
`C:\Program Files\Google\Chrome\Application\chrome.exe`,
|
||||
`C:\Program Files (x86)\Google\Chrome\Application\chrome.exe`,
|
||||
filepath.Join(os.Getenv("USERPROFILE"), "AppData", "Local", "Google", "Chrome", "Application", "chrome.exe"),
|
||||
`C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe`,
|
||||
`C:\Program Files\Microsoft\Edge\Application\msedge.exe`,
|
||||
}
|
||||
for _, path := range candidates {
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
return path, nil
|
||||
}
|
||||
}
|
||||
for _, name := range []string{"chrome.exe", "msedge.exe"} {
|
||||
if p, err := exec.LookPath(name); err == nil {
|
||||
return p, nil
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for _, name := range []string{"google-chrome", "google-chrome-stable", "chromium", "chromium-browser"} {
|
||||
if p, err := exec.LookPath(name); err == nil {
|
||||
return p, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("未找到 Chrome 或 Edge 浏览器,导出 PDF 需要安装其一")
|
||||
}
|
||||
|
||||
// ExportHtmlFileToPDF 将本地HTML文件加载渲染后导出为PDF
|
||||
func (api *PdfAPI) ExportHtmlFileToPDF(srcPath string, saveName string) (*PdfExportResponse, error) {
|
||||
// 只接受实际存在的 html 文件,避免把任意路径交给 Chrome 渲染
|
||||
info, err := os.Stat(srcPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("HTML文件不存在: %s", srcPath)
|
||||
}
|
||||
if info.IsDir() {
|
||||
return nil, fmt.Errorf("目标路径是目录,不是HTML文件: %s", srcPath)
|
||||
}
|
||||
ext := strings.ToLower(filepath.Ext(srcPath))
|
||||
if ext != ".html" && ext != ".htm" {
|
||||
return nil, fmt.Errorf("仅支持 .html/.htm 文件,当前扩展名: %s", ext)
|
||||
}
|
||||
|
||||
// 保存名兜底:空名用源文件名,缺扩展补 .pdf,取 Base 防止路径片段拼进桌面目录
|
||||
if strings.TrimSpace(saveName) == "" {
|
||||
saveName = filepath.Base(srcPath)
|
||||
}
|
||||
if !strings.HasSuffix(strings.ToLower(saveName), ".pdf") {
|
||||
saveName += ".pdf"
|
||||
}
|
||||
saveName = filepath.Base(saveName)
|
||||
|
||||
pdfBuf, err := api.generatePDFFromLocalFile(srcPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
saveDir := api.getDesktopDirectory()
|
||||
if err := os.MkdirAll(saveDir, 0755); err != nil {
|
||||
return nil, fmt.Errorf("创建目录失败: %v", err)
|
||||
}
|
||||
|
||||
savePath := filepath.Join(saveDir, saveName)
|
||||
if err := os.WriteFile(savePath, pdfBuf, 0644); 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
|
||||
}
|
||||
|
||||
// generatePDFFromLocalFile 用 headless Chrome 加载本地HTML文件并打印为PDF
|
||||
func (api *PdfAPI) generatePDFFromLocalFile(srcPath string) ([]byte, error) {
|
||||
chromePath, err := findChromeExecutable()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
absPath, err := filepath.Abs(srcPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("解析文件路径失败: %v", err)
|
||||
}
|
||||
// file URL 需整体转义,含中文和空格的路径才能正确导航
|
||||
fileURL := (&url.URL{Scheme: "file", Path: filepath.ToSlash(absPath)}).String()
|
||||
|
||||
// 外部HTML常引用图片等资源,加载时间给得比Markdown通道更宽裕
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
defer cancel()
|
||||
|
||||
opts := []chromedp.ExecAllocatorOption{
|
||||
chromedp.ExecPath(chromePath),
|
||||
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),
|
||||
}
|
||||
|
||||
allocCtx, allocCancel := chromedp.NewExecAllocator(ctx, opts...)
|
||||
defer allocCancel()
|
||||
|
||||
chromeCtx, chromeCancel := chromedp.NewContext(allocCtx)
|
||||
defer chromeCancel()
|
||||
|
||||
var buf []byte
|
||||
|
||||
err = chromedp.Run(chromeCtx,
|
||||
chromedp.Navigate(fileURL),
|
||||
// 先确保新文档的 body 可查询,后续 JS 才会落在正确文档上
|
||||
chromedp.WaitReady("body"),
|
||||
// PrintToPDF 不等图片资源,必须等 load 事件完成后才打印
|
||||
chromedp.ActionFunc(func(ctx context.Context) error {
|
||||
_, exp, err := runtime.Evaluate(`new Promise(resolve => {
|
||||
if (document.readyState === 'complete') resolve(true)
|
||||
else window.addEventListener('load', () => resolve(true))
|
||||
})`).WithAwaitPromise(true).WithReturnByValue(true).Do(ctx)
|
||||
if exp != nil {
|
||||
return exp
|
||||
}
|
||||
return err
|
||||
}),
|
||||
chromedp.ActionFunc(func(ctx context.Context) error {
|
||||
printToPDF := page.PrintToPDF().
|
||||
WithPrintBackground(true).
|
||||
WithLandscape(false).
|
||||
WithMarginTop(0).
|
||||
WithMarginBottom(0).
|
||||
WithMarginLeft(0).
|
||||
WithMarginRight(0).
|
||||
WithPaperWidth(210.0/25.4). // CDP 纸张单位是英寸,A4 宽 210mm
|
||||
WithPaperHeight(297.0/25.4)
|
||||
|
||||
var printErr error
|
||||
buf, _, printErr = printToPDF.Do(ctx)
|
||||
return printErr
|
||||
}),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("chromedp执行失败: %v", err)
|
||||
}
|
||||
|
||||
if len(buf) == 0 {
|
||||
return nil, fmt.Errorf("PDF生成结果为空")
|
||||
}
|
||||
|
||||
return buf, nil
|
||||
}
|
||||
|
||||
// getDesktopDirectory 获取用户桌面目录
|
||||
func (api *PdfAPI) getDesktopDirectory() string {
|
||||
// Windows系统
|
||||
|
||||
Reference in New Issue
Block a user