diff --git a/app/appearance/langs/en_US.json b/app/appearance/langs/en_US.json index 04d717955..2c28326f9 100644 --- a/app/appearance/langs/en_US.json +++ b/app/appearance/langs/en_US.json @@ -740,6 +740,8 @@ "export18": "After enabling, insert the document title as a heading 1 at the beginning", "export19": "Path to Pandoc executable", "export20": "Exporting Word .docx files requires format conversion using Pandoc", + "export21": "Export PDF footer template", + "export22": "%page is the current page number, %pages is the total page number, and supports Sprig template functions", "export23": "Export Markdown wit YFM", "export24": "After enabling, add some general metadata information at the beginning of the exported Markdown file", "blockRef": "Ref Block", diff --git a/app/appearance/langs/es_ES.json b/app/appearance/langs/es_ES.json index 93c6e94f1..47c7cd401 100644 --- a/app/appearance/langs/es_ES.json +++ b/app/appearance/langs/es_ES.json @@ -740,6 +740,8 @@ "export18": "Después de habilitar, inserte el título del documento como encabezado 1 al principio", "export19": "Ruta de acceso al ejecutable de Pandoc", "export20": "La exportación de archivos Word .docx requiere la conversión del formato mediante Pandoc", + "export21": "Exportar plantilla de pie de página PDF", + "export22": "%page es el número de página actual, %pages es el número de página total y es compatible con las funciones de plantilla de Sprig ", "export23": "Exportar descuento con YFM", "export24": "Después de habilitar, agregue información general de metadatos al comienzo del archivo Markdown exportado", "blockRef": "Bloque de referencia", diff --git a/app/appearance/langs/fr_FR.json b/app/appearance/langs/fr_FR.json index 1ace4d2c6..e2f3fd159 100644 --- a/app/appearance/langs/fr_FR.json +++ b/app/appearance/langs/fr_FR.json @@ -740,6 +740,8 @@ "export18": "Après activation, insérez le titre du document comme titre 1 au début", "export19": "Chemin vers l'exécutable Pandoc", "export20": "L'exportation de fichiers Word .docx nécessite une conversion de format à l'aide de Pandoc", + "export21": "Exporter le modèle de pied de page PDF", + "export22": "%page est le numéro de page actuel, %pages est le numéro de page total et prend en charge les fonctions de modèle Sprig ", "export23": "Exporter Markdown avec YFM", "export24": "Après l'activation, ajoutez des informations générales sur les métadonnées au début du fichier Markdown exporté", "blockRef": "Bloc Réf", diff --git a/app/appearance/langs/zh_CHT.json b/app/appearance/langs/zh_CHT.json index b2faaa450..af2ccd9e3 100644 --- a/app/appearance/langs/zh_CHT.json +++ b/app/appearance/langs/zh_CHT.json @@ -740,6 +740,8 @@ "export18": "啟用後將文檔標題以一級標題的形式插入到開頭", "export19": "Pandoc 可執行文件路徑", "export20": "導出 Word .docx 文件需要使用 Pandoc 進行格式轉換", + "export21": "導出 PDF 頁腳模板", + "export22": "%page 為當前頁碼,%pages 為總頁碼,支持 Sprig 模板函數", "export23": "導出 Markdown 添加 YFM", "export24": "啟用後在導出的 Markdown 文件開頭處添加一些較為通用的元數據信息", "blockRef": "引用塊", diff --git a/app/appearance/langs/zh_CN.json b/app/appearance/langs/zh_CN.json index 9ca3dd09e..81013ada7 100644 --- a/app/appearance/langs/zh_CN.json +++ b/app/appearance/langs/zh_CN.json @@ -740,6 +740,8 @@ "export18": "启用后将文档标题以一级标题的形式插入到开头", "export19": "Pandoc 可执行文件路径", "export20": "导出 Word .docx 文件需要使用 Pandoc 进行格式转换", + "export21": "导出 PDF 页脚模板", + "export22": "%page 为当前页码,%pages 为总页码,支持 Sprig 模板函数", "export23": "导出 Markdown 添加 YFM", "export24": "启用后在导出的 Markdown 文件开头处添加一些较为通用的元数据信息", "blockRef": "引用块", diff --git a/kernel/conf/export.go b/kernel/conf/export.go index 5b3773845..18b277e39 100644 --- a/kernel/conf/export.go +++ b/kernel/conf/export.go @@ -44,5 +44,6 @@ func NewExport() *Export { FileAnnotationRefMode: 0, PandocBin: "", MarkdownYFM: false, + PDFFooter: "%page / %pages", } } diff --git a/kernel/go.mod b/kernel/go.mod index 9aeec296d..0ae3423df 100644 --- a/kernel/go.mod +++ b/kernel/go.mod @@ -108,6 +108,7 @@ require ( github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/onsi/ginkgo/v2 v2.9.1 // indirect + github.com/pdfcpu/pdfcpu v0.4.0 // indirect github.com/pelletier/go-toml/v2 v2.0.7 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b // indirect diff --git a/kernel/go.sum b/kernel/go.sum index 77da33b9e..cd08c463b 100644 --- a/kernel/go.sum +++ b/kernel/go.sum @@ -232,6 +232,8 @@ github.com/panjf2000/ants/v2 v2.7.1 h1:qBy5lfSdbxvrR0yUnZfaEDjf0FlCw4ufsbcsxmE7r github.com/panjf2000/ants/v2 v2.7.1/go.mod h1:KIBmYG9QQX5U2qzFP/yQJaq/nSb6rahS9iEHkrCMgM8= github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= +github.com/pdfcpu/pdfcpu v0.4.0 h1:381iGNvMeLP+GFqIAqgd0LSj36AsK3JH4UTaF6D5jRc= +github.com/pdfcpu/pdfcpu v0.4.0/go.mod h1:9NDeS6hrCheauxw6YUlzgL/q6At2+PMzUKyFcfUzLLY= github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= github.com/pelletier/go-toml/v2 v2.0.7 h1:muncTPStnKRos5dpVKULv2FVd4bMOhNePj9CjgDb8Us= github.com/pelletier/go-toml/v2 v2.0.7/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek= diff --git a/kernel/model/export.go b/kernel/model/export.go index 7ae9a4023..8b4e4bc04 100644 --- a/kernel/model/export.go +++ b/kernel/model/export.go @@ -20,6 +20,7 @@ import ( "bytes" "errors" "fmt" + "github.com/Masterminds/sprig/v3" "net/http" "net/url" "os" @@ -29,6 +30,7 @@ import ( "sort" "strconv" "strings" + "text/template" "time" "unicode/utf8" @@ -711,6 +713,7 @@ func ProcessPDF(id, p string, merge, removeAssets bool) (err error) { processPDFBookmarks(pdfCtx, headings) processPDFLinkEmbedAssets(pdfCtx, assetDests, removeAssets) + processPDFFooter(pdfCtx) pdfcpu.VersionStr = "SiYuan v" + util.Ver if writeErr := api.WriteContextFile(pdfCtx, p); nil != writeErr { @@ -970,26 +973,49 @@ func processPDFLinkEmbedAssets(pdfCtx *pdfcpu.Context, assetDests []string, remo } } -func annotRect(i int, w, h, d, l float64) *pdfcpu.Rectangle { - // d..distance between annotation rectangles - // l..side length of rectangle +func processPDFFooter(pdfCtx *pdfcpu.Context) { + templateContent := strings.TrimSpace(Conf.Export.PDFFooter) + if "" == templateContent { + return + } - // max number of rectangles fitting into w - xmax := int((w - d) / (l + d)) + footerTpl, err := template.New("").Funcs(sprig.TxtFuncMap()).Parse(templateContent) + if nil != err { + logging.LogErrorf("parse pdf footer template failed: %s", err) + return + } - // max number of rectangles fitting into h - ymax := int((h - d) / (l + d)) + buf := &bytes.Buffer{} + buf.Grow(4096) + err = footerTpl.Execute(buf, nil) + if nil != err { + logging.LogErrorf("render pdf footer template failed: %s", err) + return + } + footer := buf.String() - col := float64(i % xmax) - row := float64(i / xmax % ymax) + fontName := "Times-Roman" + pos := "bc" + dx := 10 + fillCol := "#000000" + desc := fmt.Sprintf("font:%s, points:12, sc:1 abs, pos:%s, off:%d 10, fillcol:%s, rot:0", fontName, pos, dx, fillCol) + footer = strings.ReplaceAll(footer, "%pages", strconv.Itoa(pdfCtx.PageCount)) + m := map[int]*pdfcpu.Watermark{} + for i := 1; i <= pdfCtx.PageCount; i++ { + text := strings.ReplaceAll(footer, "%page", strconv.Itoa(i)) + wm, watermarkErr := api.TextWatermark(text, desc, true, false, pdfcpu.POINTS) + if nil != watermarkErr { + logging.LogErrorf("add pdf footer failed: %s", watermarkErr) + return + } - llx := d + col*(l+d) - lly := d + row*(l+d) + m[i] = wm + } - urx := llx + l - ury := lly + l - - return pdfcpu.Rect(llx, lly, urx, ury) + if watermarkErr := pdfCtx.AddWatermarksMap(m); nil != watermarkErr { + logging.LogErrorf("add pdf footer failed: %s", watermarkErr) + return + } } func ExportStdMarkdown(id string) string {