mirror of
https://github.com/gotenberg/gotenberg.git
synced 2026-07-02 00:17:40 +08:00
367 lines
13 KiB
Go
367 lines
13 KiB
Go
package pdfengines
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"slices"
|
|
"strings"
|
|
|
|
flag "github.com/spf13/pflag"
|
|
|
|
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
|
"github.com/gotenberg/gotenberg/v8/pkg/modules/api"
|
|
)
|
|
|
|
func init() {
|
|
gotenberg.MustRegisterModule(new(PdfEngines))
|
|
}
|
|
|
|
// PdfEngines acts as an aggregator and manager for multiple PDF engine
|
|
// modules. It enables the selection and ordering of PDF engines based on user
|
|
// preferences passed via command-line flags. The [PdfEngines] module also
|
|
// implements the [gotenberg.PdfEngine] interface, providing a unified approach
|
|
// to PDF processing across the various engines it manages.
|
|
//
|
|
// When processing PDFs, [PdfEngines] will attempt to use the engines in the
|
|
// order they were defined. If the primary engine encounters an error,
|
|
// [PdfEngines] can fall back to the next available engine. It also implements
|
|
// the [api.Router] interface to expose relevant PDF processing routes if
|
|
// enabled.
|
|
type PdfEngines struct {
|
|
mergeNames []string
|
|
splitNames []string
|
|
flattenNames []string
|
|
convertNames []string
|
|
readMetadataNames []string
|
|
writeMetadataNames []string
|
|
encryptNames []string
|
|
embedNames []string
|
|
embedMetadataNames []string
|
|
readBookmarksNames []string
|
|
writeBookmarksNames []string
|
|
watermarkNames []string
|
|
stampNames []string
|
|
rotateNames []string
|
|
facturXNames []string
|
|
engines []gotenberg.PdfEngine
|
|
disableRoutes bool
|
|
}
|
|
|
|
// Descriptor returns a PdfEngines' module descriptor.
|
|
func (mod *PdfEngines) Descriptor() gotenberg.ModuleDescriptor {
|
|
return gotenberg.ModuleDescriptor{
|
|
ID: "pdfengines",
|
|
FlagSet: func() *flag.FlagSet {
|
|
fs := flag.NewFlagSet("pdfengines", flag.ExitOnError)
|
|
fs.StringSlice("pdfengines-merge-engines", []string{"qpdf", "pdfcpu", "pdftk"}, "Set the PDF engines and their order for the merge feature - empty means all")
|
|
fs.StringSlice("pdfengines-split-engines", []string{"pdfcpu", "qpdf", "pdftk"}, "Set the PDF engines and their order for the split feature - empty means all")
|
|
fs.StringSlice("pdfengines-flatten-engines", []string{"qpdf"}, "Set the PDF engines and their order for the flatten feature - empty means all")
|
|
fs.StringSlice("pdfengines-convert-engines", []string{"libreoffice-pdfengine"}, "Set the PDF engines and their order for the convert feature - empty means all")
|
|
fs.StringSlice("pdfengines-read-metadata-engines", []string{"exiftool"}, "Set the PDF engines and their order for the read metadata feature - empty means all")
|
|
fs.StringSlice("pdfengines-write-metadata-engines", []string{"exiftool"}, "Set the PDF engines and their order for the write metadata feature - empty means all")
|
|
fs.StringSlice("pdfengines-encrypt-engines", []string{"qpdf", "pdftk", "pdfcpu"}, "Set the PDF engines and their order for the password protection feature - empty means all")
|
|
fs.StringSlice("pdfengines-embed-engines", []string{"pdfcpu"}, "Set the PDF engines and their order for the file embedding feature - empty means all")
|
|
fs.StringSlice("pdfengines-embed-metadata-engines", []string{"qpdf"}, "Set the PDF engines and their order for the embed metadata feature - empty means all")
|
|
fs.StringSlice("pdfengines-read-bookmarks-engines", []string{"pdfcpu"}, "Set the PDF engines and their order for the read bookmarks feature - empty means all")
|
|
fs.StringSlice("pdfengines-write-bookmarks-engines", []string{"pdfcpu"}, "Set the PDF engines and their order for the write bookmarks feature - empty means all")
|
|
fs.StringSlice("pdfengines-watermark-engines", []string{"pdfcpu", "pdftk"}, "Set the PDF engines and their order for the watermark feature - empty means all")
|
|
fs.StringSlice("pdfengines-stamp-engines", []string{"pdfcpu", "pdftk"}, "Set the PDF engines and their order for the stamp feature - empty means all")
|
|
fs.StringSlice("pdfengines-rotate-engines", []string{"pdfcpu", "pdftk"}, "Set the PDF engines and their order for the rotate feature - empty means all")
|
|
fs.StringSlice("pdfengines-factur-x-engines", []string{"qpdf"}, "Set the PDF engines and their order for the Factur-X XMP feature - empty means all")
|
|
fs.Bool("pdfengines-disable-routes", false, "Disable the routes")
|
|
|
|
// Deprecated flags.
|
|
fs.StringSlice("pdfengines-engines", make([]string, 0), "Set the default PDF engines and their default order - all by default")
|
|
err := fs.MarkDeprecated("pdfengines-engines", "use other flags for a more granular selection of PDF engines per method")
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
return fs
|
|
}(),
|
|
New: func() gotenberg.Module { return new(PdfEngines) },
|
|
}
|
|
}
|
|
|
|
// Provision gets either all [gotenberg.PdfEngine] modules or the modules
|
|
// selected by the user thanks to the "engines" flag.
|
|
func (mod *PdfEngines) Provision(ctx *gotenberg.Context) error {
|
|
flags := ctx.ParsedFlags()
|
|
mergeNames := flags.MustStringSlice("pdfengines-merge-engines")
|
|
splitNames := flags.MustStringSlice("pdfengines-split-engines")
|
|
flattenNames := flags.MustStringSlice("pdfengines-flatten-engines")
|
|
convertNames := flags.MustStringSlice("pdfengines-convert-engines")
|
|
readMetadataNames := flags.MustStringSlice("pdfengines-read-metadata-engines")
|
|
writeMetadataNames := flags.MustStringSlice("pdfengines-write-metadata-engines")
|
|
encryptNames := flags.MustStringSlice("pdfengines-encrypt-engines")
|
|
embedNames := flags.MustStringSlice("pdfengines-embed-engines")
|
|
embedMetadataNames := flags.MustStringSlice("pdfengines-embed-metadata-engines")
|
|
readBookmarksNames := flags.MustStringSlice("pdfengines-read-bookmarks-engines")
|
|
writeBookmarksNames := flags.MustStringSlice("pdfengines-write-bookmarks-engines")
|
|
watermarkNames := flags.MustStringSlice("pdfengines-watermark-engines")
|
|
stampNames := flags.MustStringSlice("pdfengines-stamp-engines")
|
|
rotateNames := flags.MustStringSlice("pdfengines-rotate-engines")
|
|
facturXNames := flags.MustStringSlice("pdfengines-factur-x-engines")
|
|
mod.disableRoutes = flags.MustBool("pdfengines-disable-routes")
|
|
|
|
engines, err := ctx.Modules(new(gotenberg.PdfEngine))
|
|
if err != nil {
|
|
return fmt.Errorf("get PDF engines: %w", err)
|
|
}
|
|
|
|
mod.engines = make([]gotenberg.PdfEngine, len(engines))
|
|
|
|
for i, engine := range engines {
|
|
mod.engines[i] = engine.(gotenberg.PdfEngine)
|
|
}
|
|
|
|
defaultNames := make([]string, len(mod.engines))
|
|
for i, engine := range mod.engines {
|
|
defaultNames[i] = engine.(gotenberg.Module).Descriptor().ID
|
|
}
|
|
|
|
// Example in the case of deprecated module name.
|
|
// for i, name := range defaultNames {
|
|
// if name == "unoconv-pdfengine" || name == "uno-pdfengine" {
|
|
// logger.Warn(fmt.Sprintf("%s is deprecated; prefer libreoffice-pdfengine instead", name))
|
|
// mod.defaultNames[i] = "libreoffice-pdfengine"
|
|
// }
|
|
// }
|
|
|
|
mod.mergeNames = defaultNames
|
|
if len(mergeNames) > 0 {
|
|
mod.mergeNames = mergeNames
|
|
}
|
|
|
|
mod.splitNames = defaultNames
|
|
if len(splitNames) > 0 {
|
|
mod.splitNames = splitNames
|
|
}
|
|
|
|
mod.flattenNames = defaultNames
|
|
if len(flattenNames) > 0 {
|
|
mod.flattenNames = flattenNames
|
|
}
|
|
|
|
mod.convertNames = defaultNames
|
|
if len(convertNames) > 0 {
|
|
mod.convertNames = convertNames
|
|
}
|
|
|
|
mod.readMetadataNames = defaultNames
|
|
if len(readMetadataNames) > 0 {
|
|
mod.readMetadataNames = readMetadataNames
|
|
}
|
|
|
|
mod.writeMetadataNames = defaultNames
|
|
if len(writeMetadataNames) > 0 {
|
|
mod.writeMetadataNames = writeMetadataNames
|
|
}
|
|
|
|
mod.encryptNames = defaultNames
|
|
if len(encryptNames) > 0 {
|
|
mod.encryptNames = encryptNames
|
|
}
|
|
|
|
mod.embedNames = defaultNames
|
|
if len(embedNames) > 0 {
|
|
mod.embedNames = embedNames
|
|
}
|
|
|
|
mod.embedMetadataNames = defaultNames
|
|
if len(embedMetadataNames) > 0 {
|
|
mod.embedMetadataNames = embedMetadataNames
|
|
}
|
|
|
|
mod.readBookmarksNames = defaultNames
|
|
if len(readBookmarksNames) > 0 {
|
|
mod.readBookmarksNames = readBookmarksNames
|
|
}
|
|
|
|
mod.writeBookmarksNames = defaultNames
|
|
if len(writeBookmarksNames) > 0 {
|
|
mod.writeBookmarksNames = writeBookmarksNames
|
|
}
|
|
|
|
mod.watermarkNames = defaultNames
|
|
if len(watermarkNames) > 0 {
|
|
mod.watermarkNames = watermarkNames
|
|
}
|
|
|
|
mod.stampNames = defaultNames
|
|
if len(stampNames) > 0 {
|
|
mod.stampNames = stampNames
|
|
}
|
|
|
|
mod.rotateNames = defaultNames
|
|
if len(rotateNames) > 0 {
|
|
mod.rotateNames = rotateNames
|
|
}
|
|
|
|
mod.facturXNames = defaultNames
|
|
if len(facturXNames) > 0 {
|
|
mod.facturXNames = facturXNames
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Validate validates there is at least one [gotenberg.PdfEngine] module
|
|
// available. It also validates that selected [gotenberg.PdfEngine] modules
|
|
// actually exist.
|
|
func (mod *PdfEngines) Validate() error {
|
|
if len(mod.engines) == 0 {
|
|
return errors.New("no PDF engine is available; enable at least one engine module (e.g. qpdf, pdfcpu, pdftk, libreoffice-pdfengine, exiftool)")
|
|
}
|
|
|
|
availableEngines := make([]string, len(mod.engines))
|
|
|
|
for i, engine := range mod.engines {
|
|
availableEngines[i] = engine.(gotenberg.Module).Descriptor().ID
|
|
}
|
|
|
|
nonExistingEngines := make([]string, 0)
|
|
findNonExistingEngines := func(names []string) {
|
|
for _, name := range names {
|
|
engineExists := false
|
|
|
|
for _, engine := range mod.engines {
|
|
if name == engine.(gotenberg.Module).Descriptor().ID {
|
|
engineExists = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if engineExists {
|
|
continue
|
|
}
|
|
|
|
alreadyInSlice := slices.Contains(nonExistingEngines, name)
|
|
|
|
if !alreadyInSlice {
|
|
nonExistingEngines = append(nonExistingEngines, name)
|
|
}
|
|
}
|
|
}
|
|
|
|
findNonExistingEngines(mod.mergeNames)
|
|
findNonExistingEngines(mod.splitNames)
|
|
findNonExistingEngines(mod.flattenNames)
|
|
findNonExistingEngines(mod.convertNames)
|
|
findNonExistingEngines(mod.readMetadataNames)
|
|
findNonExistingEngines(mod.writeMetadataNames)
|
|
findNonExistingEngines(mod.encryptNames)
|
|
findNonExistingEngines(mod.embedNames)
|
|
findNonExistingEngines(mod.embedMetadataNames)
|
|
findNonExistingEngines(mod.readBookmarksNames)
|
|
findNonExistingEngines(mod.writeBookmarksNames)
|
|
findNonExistingEngines(mod.watermarkNames)
|
|
findNonExistingEngines(mod.stampNames)
|
|
findNonExistingEngines(mod.rotateNames)
|
|
findNonExistingEngines(mod.facturXNames)
|
|
|
|
if len(nonExistingEngines) == 0 {
|
|
return nil
|
|
}
|
|
|
|
return fmt.Errorf("non-existing PDF engine(s): %s - available PDF engine(s): %s", nonExistingEngines, availableEngines)
|
|
}
|
|
|
|
// SystemMessages returns one message with the selected [gotenberg.PdfEngine]
|
|
// modules.
|
|
func (mod *PdfEngines) SystemMessages() []string {
|
|
return []string{
|
|
fmt.Sprintf("merge engines - %s", strings.Join(mod.mergeNames, " ")),
|
|
fmt.Sprintf("split engines - %s", strings.Join(mod.splitNames, " ")),
|
|
fmt.Sprintf("flatten engines - %s", strings.Join(mod.flattenNames, " ")),
|
|
fmt.Sprintf("convert engines - %s", strings.Join(mod.convertNames, " ")),
|
|
fmt.Sprintf("read metadata engines - %s", strings.Join(mod.readMetadataNames, " ")),
|
|
fmt.Sprintf("write metadata engines - %s", strings.Join(mod.writeMetadataNames, " ")),
|
|
fmt.Sprintf("encrypt engines - %s", strings.Join(mod.encryptNames, " ")),
|
|
fmt.Sprintf("embed engines - %s", strings.Join(mod.embedNames, " ")),
|
|
fmt.Sprintf("embed metadata engines - %s", strings.Join(mod.embedMetadataNames, " ")),
|
|
fmt.Sprintf("read bookmarks engines - %s", strings.Join(mod.readBookmarksNames, " ")),
|
|
fmt.Sprintf("write bookmarks engines - %s", strings.Join(mod.writeBookmarksNames, " ")),
|
|
fmt.Sprintf("watermark engines - %s", strings.Join(mod.watermarkNames, " ")),
|
|
fmt.Sprintf("stamp engines - %s", strings.Join(mod.stampNames, " ")),
|
|
fmt.Sprintf("rotate engines - %s", strings.Join(mod.rotateNames, " ")),
|
|
fmt.Sprintf("Factur-X engines - %s", strings.Join(mod.facturXNames, " ")),
|
|
}
|
|
}
|
|
|
|
// PdfEngine returns a [gotenberg.PdfEngine].
|
|
func (mod *PdfEngines) PdfEngine() (gotenberg.PdfEngine, error) {
|
|
engines := func(names []string) []gotenberg.PdfEngine {
|
|
list := make([]gotenberg.PdfEngine, len(names))
|
|
for i, name := range names {
|
|
for _, engine := range mod.engines {
|
|
if name == engine.(gotenberg.Module).Descriptor().ID {
|
|
list[i] = engine
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
return list
|
|
}
|
|
|
|
return newMultiPdfEngines(
|
|
engines(mod.mergeNames),
|
|
engines(mod.splitNames),
|
|
engines(mod.flattenNames),
|
|
engines(mod.convertNames),
|
|
engines(mod.readMetadataNames),
|
|
engines(mod.writeMetadataNames),
|
|
engines(mod.encryptNames),
|
|
engines(mod.embedNames),
|
|
engines(mod.embedMetadataNames),
|
|
engines(mod.readBookmarksNames),
|
|
engines(mod.writeBookmarksNames),
|
|
engines(mod.watermarkNames),
|
|
engines(mod.stampNames),
|
|
engines(mod.rotateNames),
|
|
engines(mod.facturXNames),
|
|
), nil
|
|
}
|
|
|
|
// Routes returns the HTTP routes.
|
|
func (mod *PdfEngines) Routes() ([]api.Route, error) {
|
|
if mod.disableRoutes {
|
|
return nil, nil
|
|
}
|
|
|
|
engine, err := mod.PdfEngine()
|
|
if err != nil {
|
|
// Should not happen, unless our provider implementation
|
|
// changes in the future.
|
|
return nil, fmt.Errorf("get pdf mod: %w", err)
|
|
}
|
|
|
|
return []api.Route{
|
|
mergeRoute(engine),
|
|
splitRoute(engine),
|
|
flattenRoute(engine),
|
|
convertRoute(engine),
|
|
readMetadataRoute(engine),
|
|
writeMetadataRoute(engine),
|
|
readBookmarksRoute(engine),
|
|
writeBookmarksRoute(engine),
|
|
encryptRoute(engine),
|
|
embedRoute(engine),
|
|
watermarkRoute(engine),
|
|
stampRoute(engine),
|
|
rotateRoute(engine),
|
|
facturXRoute(engine),
|
|
}, nil
|
|
}
|
|
|
|
// Interface guards.
|
|
var (
|
|
_ gotenberg.Module = (*PdfEngines)(nil)
|
|
_ gotenberg.Provisioner = (*PdfEngines)(nil)
|
|
_ gotenberg.Validator = (*PdfEngines)(nil)
|
|
_ gotenberg.SystemLogger = (*PdfEngines)(nil)
|
|
_ gotenberg.PdfEngineProvider = (*PdfEngines)(nil)
|
|
_ api.Router = (*PdfEngines)(nil)
|
|
)
|