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 readBookmarksNames []string writeBookmarksNames []string watermarkNames []string stampNames []string rotateNames []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-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.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") 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") 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.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 } 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") } 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.readBookmarksNames) findNonExistingEngines(mod.writeBookmarksNames) findNonExistingEngines(mod.watermarkNames) findNonExistingEngines(mod.stampNames) findNonExistingEngines(mod.rotateNames) 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("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, " ")), } } // 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.readBookmarksNames), engines(mod.writeBookmarksNames), engines(mod.watermarkNames), engines(mod.stampNames), engines(mod.rotateNames), ), 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), }, 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) )