From cc1341c3cf40e27215e2c99aa28e21264699658b Mon Sep 17 00:00:00 2001 From: Julien Neuhart Date: Thu, 13 Aug 2026 20:39:11 +0200 Subject: [PATCH] chore(deps): update pdfcpu to v0.15.0 (#1628) --- Makefile | 2 +- build/Dockerfile | 2 +- pkg/modules/exiftool/exiftool.go | 27 ++++++++++----- pkg/modules/pdfengines/pdfengines.go | 2 +- pkg/modules/qpdf/qpdf.go | 50 +++++++++++++++++++++++++--- 5 files changed, 67 insertions(+), 16 deletions(-) diff --git a/Makefile b/Makefile index b4f2fed5..96675256 100644 --- a/Makefile +++ b/Makefile @@ -91,7 +91,7 @@ PDFENGINES_WATERMARK_ENGINES=pdfcpu,pdftk PDFENGINES_STAMP_ENGINES=pdfcpu,pdftk PDFENGINES_ENCRYPT_ENGINES=qpdf,pdfcpu,pdftk PDFENGINES_ROTATE_ENGINES=pdfcpu,pdftk -PDFENGINES_EMBED_ENGINES=qpdf,pdfcpu +PDFENGINES_EMBED_ENGINES=pdfcpu,qpdf PDFENGINES_EMBED_METADATA_ENGINES=qpdf PDFENGINES_FACTUR_X_ENGINES=qpdf PROMETHEUS_NAMESPACE=gotenberg diff --git a/build/Dockerfile b/build/Dockerfile index 5bf7a20f..2bb6a945 100644 --- a/build/Dockerfile +++ b/build/Dockerfile @@ -11,7 +11,7 @@ ARG GOLANG_VERSION=1.26.5 FROM golang:$GOLANG_VERSION AS pdfcpu-binary-stage # See https://github.com/pdfcpu/pdfcpu/releases. -ARG PDFCPU_VERSION=v0.13.0 +ARG PDFCPU_VERSION=v0.15.0 ENV CGO_ENABLED=0 # Define the working directory outside of $GOPATH (we're using go modules). diff --git a/pkg/modules/exiftool/exiftool.go b/pkg/modules/exiftool/exiftool.go index 5e82350d..72b156a8 100644 --- a/pkg/modules/exiftool/exiftool.go +++ b/pkg/modules/exiftool/exiftool.go @@ -115,18 +115,29 @@ func buildExifToolWriteArgs(metadata map[string]any) ([]string, error) { return nil, fmt.Errorf("write PDF metadata with ExifTool: invalid metadata key %q: %w", key, gotenberg.ErrPdfEngineMetadataValueNotSupported) } + tag := key + if key == "Trapped" { + // ExifTool writes the document info /Trapped entry as a malformed + // name-in-a-string, e.g. "(/Unknown)". pdfcpu's stricter validation + // (as of v0.15) rejects it, which breaks later pdfcpu operations on + // the file such as embedding. Writing Trapped to XMP keeps the value + // readable without the invalid document info entry. + // See https://github.com/gotenberg/gotenberg/issues/1628. + tag = "XMP-pdf:Trapped" + } + switch val := value.(type) { case string: if err := validateMetadataValue(key, val); err != nil { return nil, err } - args = append(args, fmt.Sprintf("-%s=%s", key, val)) + args = append(args, fmt.Sprintf("-%s=%s", tag, val)) case []string: for _, s := range val { if err := validateMetadataValue(key, s); err != nil { return nil, err } - args = append(args, fmt.Sprintf("-%s=%s", key, s)) + args = append(args, fmt.Sprintf("-%s=%s", tag, s)) } case []any: // See https://github.com/gotenberg/gotenberg/issues/1048. @@ -138,18 +149,18 @@ func buildExifToolWriteArgs(metadata map[string]any) ([]string, error) { if err := validateMetadataValue(key, s); err != nil { return nil, err } - args = append(args, fmt.Sprintf("-%s=%s", key, s)) + args = append(args, fmt.Sprintf("-%s=%s", tag, s)) } case bool: - args = append(args, fmt.Sprintf("-%s=%t", key, val)) + args = append(args, fmt.Sprintf("-%s=%t", tag, val)) case int: - args = append(args, fmt.Sprintf("-%s=%d", key, val)) + args = append(args, fmt.Sprintf("-%s=%d", tag, val)) case int64: - args = append(args, fmt.Sprintf("-%s=%d", key, val)) + args = append(args, fmt.Sprintf("-%s=%d", tag, val)) case float32: - args = append(args, fmt.Sprintf("-%s=%g", key, val)) + args = append(args, fmt.Sprintf("-%s=%g", tag, val)) case float64: - args = append(args, fmt.Sprintf("-%s=%g", key, val)) + args = append(args, fmt.Sprintf("-%s=%g", tag, val)) default: return nil, fmt.Errorf("write PDF metadata with ExifTool: unsupported type %T for key %q: %w", value, key, gotenberg.ErrPdfEngineMetadataValueNotSupported) } diff --git a/pkg/modules/pdfengines/pdfengines.go b/pkg/modules/pdfengines/pdfengines.go index af455ad5..a015a4b9 100644 --- a/pkg/modules/pdfengines/pdfengines.go +++ b/pkg/modules/pdfengines/pdfengines.go @@ -62,7 +62,7 @@ func (mod *PdfEngines) Descriptor() gotenberg.ModuleDescriptor { 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-engines", []string{"pdfcpu", "qpdf"}, "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") diff --git a/pkg/modules/qpdf/qpdf.go b/pkg/modules/qpdf/qpdf.go index 398f4e82..0bed780d 100644 --- a/pkg/modules/qpdf/qpdf.go +++ b/pkg/modules/qpdf/qpdf.go @@ -419,17 +419,57 @@ func (engine *QPdf) Encrypt(ctx context.Context, logger *slog.Logger, inputPath } // EmbedFiles is not available in this implementation. +// EmbedFiles embeds the given files into a PDF as attachments using qpdf's +// --add-attachment. It is a fallback for pdfcpu: qpdf reads PDFs whose document +// info pdfcpu's stricter validation rejects (for example a non-conformant +// /Trapped value written by ExifTool). See +// https://github.com/gotenberg/gotenberg/issues/1628. func (engine *QPdf) EmbedFiles(ctx context.Context, logger *slog.Logger, filePaths []string, inputPath string) error { - _, span := gotenberg.Tracer().Start(ctx, "qpdf.EmbedFiles", + ctx, span := gotenberg.Tracer().Start(ctx, "qpdf.EmbedFiles", trace.WithSpanKind(trace.SpanKindClient), trace.WithAttributes(engine.spanAttrs()...), ) defer span.End() - err := fmt.Errorf("embed files with QPDF: %w", gotenberg.ErrPdfEngineMethodNotSupported) - span.RecordError(err) - span.SetStatus(codes.Error, err.Error()) - return err + if len(filePaths) == 0 { + span.SetStatus(codes.Ok, "") + return nil + } + + fail := func(err error) error { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + return err + } + + // Each --add-attachment group must be terminated by "--", so qpdf attaches + // one file per invocation. Chain them, writing to a temporary file and + // moving it back over the input each time. + outputPath := inputPath + "~embed" + for _, filePath := range filePaths { + args := make([]string, 0, 6+len(engine.globalArgs)) + args = append(args, engine.globalArgs...) + args = append(args, "--add-attachment", filePath, "--key="+filepath.Base(filePath)) + args = append(args, "--", inputPath, outputPath) + + cmd, err := gotenberg.CommandContext(ctx, logger, engine.binPath, args...) + if err != nil { + return fail(fmt.Errorf("create command: %w", err)) + } + + _, err = cmd.Exec() + if err != nil { + return fail(fmt.Errorf("embed files with QPDF: %w", err)) + } + + err = os.Rename(outputPath, inputPath) + if err != nil { + return fail(fmt.Errorf("embed files with QPDF: replace input: %w", err)) + } + } + + span.SetStatus(codes.Ok, "") + return nil } // EmbedFilesMetadata sets metadata on already-embedded files in a PDF using