mirror of
https://github.com/gotenberg/gotenberg.git
synced 2026-08-16 04:12:16 +01:00
feat(pdfengines): inject Factur-X/ZUGFeRD XMP metadata
This commit is contained in:
@@ -28,6 +28,7 @@ type multiPdfEngines struct {
|
||||
watermarkEngines []gotenberg.PdfEngine
|
||||
stampEngines []gotenberg.PdfEngine
|
||||
rotateEngines []gotenberg.PdfEngine
|
||||
facturXEngines []gotenberg.PdfEngine
|
||||
}
|
||||
|
||||
func newMultiPdfEngines(
|
||||
@@ -44,7 +45,8 @@ func newMultiPdfEngines(
|
||||
writeBookmarksEngines,
|
||||
watermarkEngines,
|
||||
stampEngines,
|
||||
rotateEngines []gotenberg.PdfEngine,
|
||||
rotateEngines,
|
||||
facturXEngines []gotenberg.PdfEngine,
|
||||
) *multiPdfEngines {
|
||||
return &multiPdfEngines{
|
||||
mergeEngines: mergeEngines,
|
||||
@@ -61,6 +63,7 @@ func newMultiPdfEngines(
|
||||
watermarkEngines: watermarkEngines,
|
||||
stampEngines: stampEngines,
|
||||
rotateEngines: rotateEngines,
|
||||
facturXEngines: facturXEngines,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -320,6 +323,17 @@ func (multi *multiPdfEngines) EmbedFilesMetadata(ctx context.Context, logger *sl
|
||||
)
|
||||
}
|
||||
|
||||
// InjectFacturXXMP injects Factur-X/ZUGFeRD XMP metadata using the first
|
||||
// available engine that supports it.
|
||||
func (multi *multiPdfEngines) InjectFacturXXMP(ctx context.Context, logger *slog.Logger, facturX gotenberg.FacturX, inputPath string) error {
|
||||
return runWithFallbackVoid(ctx, "pdfengines.InjectFacturXXMP", multi.facturXEngines,
|
||||
func(ctx context.Context, engine gotenberg.PdfEngine) error {
|
||||
return engine.InjectFacturXXMP(ctx, logger, facturX, inputPath)
|
||||
},
|
||||
func(err error) error { return fmt.Errorf("inject Factur-X XMP with multi PDF engines: %w", err) },
|
||||
)
|
||||
}
|
||||
|
||||
// Interface guards.
|
||||
var (
|
||||
_ gotenberg.PdfEngine = (*multiPdfEngines)(nil)
|
||||
|
||||
@@ -42,6 +42,7 @@ type PdfEngines struct {
|
||||
watermarkNames []string
|
||||
stampNames []string
|
||||
rotateNames []string
|
||||
facturXNames []string
|
||||
engines []gotenberg.PdfEngine
|
||||
disableRoutes bool
|
||||
}
|
||||
@@ -66,6 +67,7 @@ func (mod *PdfEngines) Descriptor() gotenberg.ModuleDescriptor {
|
||||
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.
|
||||
@@ -99,6 +101,7 @@ func (mod *PdfEngines) Provision(ctx *gotenberg.Context) error {
|
||||
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))
|
||||
@@ -195,6 +198,11 @@ func (mod *PdfEngines) Provision(ctx *gotenberg.Context) error {
|
||||
mod.rotateNames = rotateNames
|
||||
}
|
||||
|
||||
mod.facturXNames = defaultNames
|
||||
if len(facturXNames) > 0 {
|
||||
mod.facturXNames = facturXNames
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -250,6 +258,7 @@ func (mod *PdfEngines) Validate() error {
|
||||
findNonExistingEngines(mod.watermarkNames)
|
||||
findNonExistingEngines(mod.stampNames)
|
||||
findNonExistingEngines(mod.rotateNames)
|
||||
findNonExistingEngines(mod.facturXNames)
|
||||
|
||||
if len(nonExistingEngines) == 0 {
|
||||
return nil
|
||||
@@ -276,6 +285,7 @@ func (mod *PdfEngines) SystemMessages() []string {
|
||||
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, " ")),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -310,6 +320,7 @@ func (mod *PdfEngines) PdfEngine() (gotenberg.PdfEngine, error) {
|
||||
engines(mod.watermarkNames),
|
||||
engines(mod.stampNames),
|
||||
engines(mod.rotateNames),
|
||||
engines(mod.facturXNames),
|
||||
), nil
|
||||
}
|
||||
|
||||
@@ -340,6 +351,7 @@ func (mod *PdfEngines) Routes() ([]api.Route, error) {
|
||||
watermarkRoute(engine),
|
||||
stampRoute(engine),
|
||||
rotateRoute(engine),
|
||||
facturXRoute(engine),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -467,6 +467,31 @@ func EmbedFilesMetadataStub(ctx *api.Context, engine gotenberg.PdfEngine, metada
|
||||
return nil
|
||||
}
|
||||
|
||||
// FormDataPdfFacturX extracts Factur-X parameters from form data.
|
||||
// The "facturx" field is a JSON string with the four fx properties.
|
||||
func FormDataPdfFacturX(form *api.FormData, mandatory bool) gotenberg.FacturX {
|
||||
var facturX gotenberg.FacturX
|
||||
form.FacturX(&facturX, mandatory)
|
||||
return facturX
|
||||
}
|
||||
|
||||
// InjectFacturXXMPStub injects Factur-X XMP metadata into PDF files. If the
|
||||
// Factur-X data is not set, it does nothing.
|
||||
func InjectFacturXXMPStub(ctx *api.Context, engine gotenberg.PdfEngine, facturX gotenberg.FacturX, inputPaths []string) error {
|
||||
if facturX.ConformanceLevel == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, inputPath := range inputPaths {
|
||||
err := engine.InjectFacturXXMP(ctx, ctx.Log(), facturX, inputPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("inject Factur-X XMP into PDF '%s': %w", inputPath, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// FormDataPdfEncrypt extracts encryption parameters from form data.
|
||||
func FormDataPdfEncrypt(form *api.FormData) (userPassword, ownerPassword string) {
|
||||
form.String("userPassword", &userPassword, "")
|
||||
@@ -707,6 +732,7 @@ func mergeRoute(engine gotenberg.PdfEngine) api.Route {
|
||||
stampFile := FormDataPdfStampFile(form)
|
||||
angle, rotatePages := FormDataPdfRotate(form, false)
|
||||
embedsMetadata := FormDataPdfEmbedsMetadata(form)
|
||||
facturX := FormDataPdfFacturX(form, false)
|
||||
|
||||
var inputPaths []string
|
||||
var flatten bool
|
||||
@@ -830,6 +856,11 @@ func mergeRoute(engine gotenberg.PdfEngine) api.Route {
|
||||
return fmt.Errorf("set embeds metadata: %w", err)
|
||||
}
|
||||
|
||||
err = InjectFacturXXMPStub(ctx, engine, facturX, outputPaths)
|
||||
if err != nil {
|
||||
return fmt.Errorf("inject Factur-X XMP: %w", err)
|
||||
}
|
||||
|
||||
err = EncryptPdfStub(ctx, engine, userPassword, ownerPassword, outputPaths)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encrypt PDFs: %w", err)
|
||||
@@ -866,6 +897,7 @@ func splitRoute(engine gotenberg.PdfEngine) api.Route {
|
||||
stampFile := FormDataPdfStampFile(form)
|
||||
angle, rotatePages := FormDataPdfRotate(form, false)
|
||||
embedsMetadata := FormDataPdfEmbedsMetadata(form)
|
||||
facturX := FormDataPdfFacturX(form, false)
|
||||
|
||||
var inputPaths []string
|
||||
var flatten bool
|
||||
@@ -940,6 +972,11 @@ func splitRoute(engine gotenberg.PdfEngine) api.Route {
|
||||
return fmt.Errorf("set embeds metadata: %w", err)
|
||||
}
|
||||
|
||||
err = InjectFacturXXMPStub(ctx, engine, facturX, convertOutputPaths)
|
||||
if err != nil {
|
||||
return fmt.Errorf("inject Factur-X XMP: %w", err)
|
||||
}
|
||||
|
||||
err = EncryptPdfStub(ctx, engine, userPassword, ownerPassword, convertOutputPaths)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encrypt PDFs: %w", err)
|
||||
@@ -1265,6 +1302,7 @@ func embedRoute(engine gotenberg.PdfEngine) api.Route {
|
||||
form := ctx.FormData()
|
||||
embedPaths := FormDataPdfEmbeds(form)
|
||||
embedsMetadata := FormDataPdfEmbedsMetadata(form)
|
||||
facturX := FormDataPdfFacturX(form, false)
|
||||
|
||||
var inputPaths []string
|
||||
err := form.
|
||||
@@ -1283,6 +1321,11 @@ func embedRoute(engine gotenberg.PdfEngine) api.Route {
|
||||
return fmt.Errorf("set embeds metadata: %w", err)
|
||||
}
|
||||
|
||||
err = InjectFacturXXMPStub(ctx, engine, facturX, inputPaths)
|
||||
if err != nil {
|
||||
return fmt.Errorf("inject Factur-X XMP: %w", err)
|
||||
}
|
||||
|
||||
err = ctx.AddOutputPaths(inputPaths...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("add output paths: %w", err)
|
||||
@@ -1413,3 +1456,39 @@ func rotateRoute(engine gotenberg.PdfEngine) api.Route {
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// facturXRoute returns an [api.Route] which injects Factur-X/ZUGFeRD XMP
|
||||
// metadata into PDF/A-3 files.
|
||||
func facturXRoute(engine gotenberg.PdfEngine) api.Route {
|
||||
return api.Route{
|
||||
Method: http.MethodPost,
|
||||
Path: "/forms/pdfengines/factur-x",
|
||||
IsMultipart: true,
|
||||
Handler: func(c echo.Context) error {
|
||||
ctx := c.Get("context").(*api.Context)
|
||||
|
||||
form := ctx.FormData()
|
||||
facturX := FormDataPdfFacturX(form, true)
|
||||
|
||||
var inputPaths []string
|
||||
err := form.
|
||||
MandatoryPaths([]string{".pdf"}, &inputPaths).
|
||||
Validate()
|
||||
if err != nil {
|
||||
return fmt.Errorf("validate form data: %w", err)
|
||||
}
|
||||
|
||||
err = InjectFacturXXMPStub(ctx, engine, facturX, inputPaths)
|
||||
if err != nil {
|
||||
return fmt.Errorf("inject Factur-X XMP into PDFs: %w", err)
|
||||
}
|
||||
|
||||
err = ctx.AddOutputPaths(inputPaths...)
|
||||
if err != nil {
|
||||
return fmt.Errorf("add output paths: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user