Files
gotenberg/pkg/modules/pdfengines/multi.go
T
2026-03-27 16:28:45 +01:00

600 lines
16 KiB
Go

package pdfengines
import (
"context"
"fmt"
"log/slog"
"sync"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/trace"
"go.uber.org/multierr"
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
)
type multiPdfEngines struct {
mergeEngines []gotenberg.PdfEngine
splitEngines []gotenberg.PdfEngine
flattenEngines []gotenberg.PdfEngine
convertEngines []gotenberg.PdfEngine
readMetadataEngines []gotenberg.PdfEngine
writeMetadataEngines []gotenberg.PdfEngine
passwordEngines []gotenberg.PdfEngine
embedEngines []gotenberg.PdfEngine
readBookmarksEngines []gotenberg.PdfEngine
writeBookmarksEngines []gotenberg.PdfEngine
watermarkEngines []gotenberg.PdfEngine
stampEngines []gotenberg.PdfEngine
rotateEngines []gotenberg.PdfEngine
}
func newMultiPdfEngines(
mergeEngines,
splitEngines,
flattenEngines,
convertEngines,
readMetadataEngines,
writeMetadataEngines,
passwordEngines,
embedEngines,
readBookmarksEngines,
writeBookmarksEngines,
watermarkEngines,
stampEngines,
rotateEngines []gotenberg.PdfEngine,
) *multiPdfEngines {
return &multiPdfEngines{
mergeEngines: mergeEngines,
splitEngines: splitEngines,
flattenEngines: flattenEngines,
convertEngines: convertEngines,
readMetadataEngines: readMetadataEngines,
writeMetadataEngines: writeMetadataEngines,
passwordEngines: passwordEngines,
embedEngines: embedEngines,
readBookmarksEngines: readBookmarksEngines,
writeBookmarksEngines: writeBookmarksEngines,
watermarkEngines: watermarkEngines,
stampEngines: stampEngines,
rotateEngines: rotateEngines,
}
}
// Merge combines multiple PDF files into a single document using the first
// available engine that supports PDF merging.
//
//nolint:dupl
func (multi *multiPdfEngines) Merge(ctx context.Context, logger *slog.Logger, inputPaths []string, outputPath string) error {
tracer := gotenberg.Tracer()
ctx, span := tracer.Start(ctx, "pdfengines.Merge", trace.WithSpanKind(trace.SpanKindInternal))
defer span.End()
var err error
errChan := make(chan error, 1)
for _, engine := range multi.mergeEngines {
go func(engine gotenberg.PdfEngine) {
errChan <- engine.Merge(ctx, logger, inputPaths, outputPath)
}(engine)
select {
case mergeErr := <-errChan:
errored := multierr.AppendInto(&err, mergeErr)
if !errored {
span.SetStatus(codes.Ok, "")
return nil
}
case <-ctx.Done():
return ctx.Err()
}
}
err = fmt.Errorf("merge PDFs with multi PDF engines: %w", err)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return err
}
type splitResult struct {
outputPaths []string
err error
}
// Split divides the PDF into separate pages using the first available engine
// that supports PDF splitting.
func (multi *multiPdfEngines) Split(ctx context.Context, logger *slog.Logger, mode gotenberg.SplitMode, inputPath, outputDirPath string) ([]string, error) {
tracer := gotenberg.Tracer()
ctx, span := tracer.Start(ctx, "pdfengines.Split", trace.WithSpanKind(trace.SpanKindInternal))
defer span.End()
var err error
var mu sync.Mutex // to safely append errors.
for _, engine := range multi.splitEngines {
resultChan := make(chan splitResult, 1)
go func(engine gotenberg.PdfEngine) {
outputPaths, err := engine.Split(ctx, logger, mode, inputPath, outputDirPath)
resultChan <- splitResult{outputPaths: outputPaths, err: err}
}(engine)
select {
case result := <-resultChan:
if result.err != nil {
mu.Lock()
err = multierr.Append(err, result.err)
mu.Unlock()
} else {
span.SetStatus(codes.Ok, "")
return result.outputPaths, nil
}
case <-ctx.Done():
return nil, ctx.Err()
}
}
err = fmt.Errorf("split PDF with multi PDF engines: %w", err)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return nil, err
}
// Flatten merges existing annotation appearances with page content using the
// first available engine that supports flattening.
func (multi *multiPdfEngines) Flatten(ctx context.Context, logger *slog.Logger, inputPath string) error {
tracer := gotenberg.Tracer()
ctx, span := tracer.Start(ctx, "pdfengines.Flatten", trace.WithSpanKind(trace.SpanKindInternal))
defer span.End()
var err error
errChan := make(chan error, 1)
for _, engine := range multi.flattenEngines {
go func(engine gotenberg.PdfEngine) {
errChan <- engine.Flatten(ctx, logger, inputPath)
}(engine)
select {
case mergeErr := <-errChan:
errored := multierr.AppendInto(&err, mergeErr)
if !errored {
span.SetStatus(codes.Ok, "")
return nil
}
case <-ctx.Done():
return ctx.Err()
}
}
err = fmt.Errorf("flatten PDF with multi PDF engines: %w", err)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return err
}
// Convert transforms the given PDF to a specific PDF format using the first
// available engine that supports PDF conversion.
func (multi *multiPdfEngines) Convert(ctx context.Context, logger *slog.Logger, formats gotenberg.PdfFormats, inputPath, outputPath string) error {
tracer := gotenberg.Tracer()
ctx, span := tracer.Start(ctx, "pdfengines.Convert", trace.WithSpanKind(trace.SpanKindInternal))
defer span.End()
var err error
errChan := make(chan error, 1)
for _, engine := range multi.convertEngines {
go func(engine gotenberg.PdfEngine) {
errChan <- engine.Convert(ctx, logger, formats, inputPath, outputPath)
}(engine)
select {
case mergeErr := <-errChan:
errored := multierr.AppendInto(&err, mergeErr)
if !errored {
span.SetStatus(codes.Ok, "")
return nil
}
case <-ctx.Done():
return ctx.Err()
}
}
err = fmt.Errorf("convert PDF to '%+v' with multi PDF engines: %w", formats, err)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return err
}
type readMetadataResult struct {
metadata map[string]any
err error
}
// ReadMetadata extracts metadata from a PDF file using the first available
// engine that supports metadata reading.
//
//nolint:dupl
func (multi *multiPdfEngines) ReadMetadata(ctx context.Context, logger *slog.Logger, inputPath string) (map[string]any, error) {
tracer := gotenberg.Tracer()
ctx, span := tracer.Start(ctx, "pdfengines.ReadMetadata", trace.WithSpanKind(trace.SpanKindInternal))
defer span.End()
var err error
var mu sync.Mutex // to safely append errors.
for _, engine := range multi.readMetadataEngines {
resultChan := make(chan readMetadataResult, 1)
go func(engine gotenberg.PdfEngine) {
metadata, err := engine.ReadMetadata(ctx, logger, inputPath)
resultChan <- readMetadataResult{metadata: metadata, err: err}
}(engine)
select {
case result := <-resultChan:
if result.err != nil {
mu.Lock()
err = multierr.Append(err, result.err)
mu.Unlock()
} else {
span.SetStatus(codes.Ok, "")
return result.metadata, nil
}
case <-ctx.Done():
return nil, ctx.Err()
}
}
err = fmt.Errorf("read PDF metadata with multi PDF engines: %w", err)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return nil, err
}
// WriteMetadata embeds metadata into a PDF file using the first available
// engine that supports metadata writing.
func (multi *multiPdfEngines) WriteMetadata(ctx context.Context, logger *slog.Logger, metadata map[string]any, inputPath string) error {
tracer := gotenberg.Tracer()
ctx, span := tracer.Start(ctx, "pdfengines.WriteMetadata", trace.WithSpanKind(trace.SpanKindInternal))
defer span.End()
var err error
errChan := make(chan error, 1)
for _, engine := range multi.writeMetadataEngines {
go func(engine gotenberg.PdfEngine) {
errChan <- engine.WriteMetadata(ctx, logger, metadata, inputPath)
}(engine)
select {
case writeMetadataErr := <-errChan:
errored := multierr.AppendInto(&err, writeMetadataErr)
if !errored {
span.SetStatus(codes.Ok, "")
return nil
}
case <-ctx.Done():
return ctx.Err()
}
}
err = fmt.Errorf("write PDF metadata with multi PDF engines: %w", err)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return err
}
type pageCountResult struct {
pageCount int
err error
}
// PageCount returns the number of pages in a PDF file using the first available
// engine that supports metadata reading.
func (multi *multiPdfEngines) PageCount(ctx context.Context, logger *slog.Logger, inputPath string) (int, error) {
tracer := gotenberg.Tracer()
ctx, span := tracer.Start(ctx, "pdfengines.PageCount", trace.WithSpanKind(trace.SpanKindInternal))
defer span.End()
var err error
var mu sync.Mutex // to safely append errors.
for _, engine := range multi.readMetadataEngines {
resultChan := make(chan pageCountResult, 1)
go func(engine gotenberg.PdfEngine) {
pageCount, err := engine.PageCount(ctx, logger, inputPath)
resultChan <- pageCountResult{pageCount: pageCount, err: err}
}(engine)
select {
case result := <-resultChan:
if result.err != nil {
mu.Lock()
err = multierr.Append(err, result.err)
mu.Unlock()
} else {
span.SetStatus(codes.Ok, "")
return result.pageCount, nil
}
case <-ctx.Done():
return 0, ctx.Err()
}
}
err = fmt.Errorf("page count with multi PDF engines: %w", err)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return 0, err
}
type readBookmarksResult struct {
bookmarks []gotenberg.Bookmark
err error
}
// ReadBookmarks reads bookmarks from a PDF file using the first available
// engine that supports bookmarks reading.
//
//nolint:dupl
func (multi *multiPdfEngines) ReadBookmarks(ctx context.Context, logger *slog.Logger, inputPath string) ([]gotenberg.Bookmark, error) {
tracer := gotenberg.Tracer()
ctx, span := tracer.Start(ctx, "pdfengines.ReadBookmarks", trace.WithSpanKind(trace.SpanKindInternal))
defer span.End()
var err error
var mu sync.Mutex // to safely append errors.
for _, engine := range multi.readBookmarksEngines {
resultChan := make(chan readBookmarksResult, 1)
go func(engine gotenberg.PdfEngine) {
bookmarks, err := engine.ReadBookmarks(ctx, logger, inputPath)
resultChan <- readBookmarksResult{bookmarks: bookmarks, err: err}
}(engine)
select {
case result := <-resultChan:
if result.err != nil {
mu.Lock()
err = multierr.Append(err, result.err)
mu.Unlock()
} else {
span.SetStatus(codes.Ok, "")
return result.bookmarks, nil
}
case <-ctx.Done():
return nil, ctx.Err()
}
}
err = fmt.Errorf("read PDF bookmarks with multi PDF engines: %w", err)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return nil, err
}
// WriteBookmarks adds a document outline (bookmarks) to a PDF file using the
// first available engine that supports bookmarks writing.
func (multi *multiPdfEngines) WriteBookmarks(ctx context.Context, logger *slog.Logger, inputPath string, bookmarks []gotenberg.Bookmark) error {
tracer := gotenberg.Tracer()
ctx, span := tracer.Start(ctx, "pdfengines.WriteBookmarks", trace.WithSpanKind(trace.SpanKindInternal))
defer span.End()
var err error
errChan := make(chan error, 1)
for _, engine := range multi.writeBookmarksEngines {
go func(engine gotenberg.PdfEngine) {
errChan <- engine.WriteBookmarks(ctx, logger, inputPath, bookmarks)
}(engine)
select {
case writeBookmarksErr := <-errChan:
errored := multierr.AppendInto(&err, writeBookmarksErr)
if !errored {
span.SetStatus(codes.Ok, "")
return nil
}
case <-ctx.Done():
return ctx.Err()
}
}
err = fmt.Errorf("write PDF bookmarks with multi PDF engines: %w", err)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return err
}
// Encrypt adds password protection to a PDF file using the first available
// engine that supports password protection.
func (multi *multiPdfEngines) Encrypt(ctx context.Context, logger *slog.Logger, inputPath, userPassword, ownerPassword string) error {
tracer := gotenberg.Tracer()
ctx, span := tracer.Start(ctx, "pdfengines.Encrypt", trace.WithSpanKind(trace.SpanKindInternal))
defer span.End()
var err error
errChan := make(chan error, 1)
for _, engine := range multi.passwordEngines {
go func(engine gotenberg.PdfEngine) {
errChan <- engine.Encrypt(ctx, logger, inputPath, userPassword, ownerPassword)
}(engine)
select {
case protectErr := <-errChan:
errored := multierr.AppendInto(&err, protectErr)
if !errored {
span.SetStatus(codes.Ok, "")
return nil
}
case <-ctx.Done():
return ctx.Err()
}
}
err = fmt.Errorf("encrypt PDF using multi PDF engines: %w", err)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return err
}
// EmbedFiles embeds files into a PDF using the first available
// engine that supports file embedding.
//
//nolint:dupl
func (multi *multiPdfEngines) EmbedFiles(ctx context.Context, logger *slog.Logger, filePaths []string, inputPath string) error {
tracer := gotenberg.Tracer()
ctx, span := tracer.Start(ctx, "pdfengines.EmbedFiles", trace.WithSpanKind(trace.SpanKindInternal))
defer span.End()
var err error
errChan := make(chan error, 1)
for _, engine := range multi.embedEngines {
go func(engine gotenberg.PdfEngine) {
errChan <- engine.EmbedFiles(ctx, logger, filePaths, inputPath)
}(engine)
select {
case embedErr := <-errChan:
errored := multierr.AppendInto(&err, embedErr)
if !errored {
span.SetStatus(codes.Ok, "")
return nil
}
case <-ctx.Done():
return ctx.Err()
}
}
err = fmt.Errorf("embed files into PDF using multi PDF engines: %w", err)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return err
}
// Watermark applies a watermark (behind page content) to a PDF file using the
// first available engine that supports watermarking.
//
//nolint:dupl
func (multi *multiPdfEngines) Watermark(ctx context.Context, logger *slog.Logger, inputPath string, stamp gotenberg.Stamp) error {
tracer := gotenberg.Tracer()
ctx, span := tracer.Start(ctx, "pdfengines.Watermark", trace.WithSpanKind(trace.SpanKindInternal))
defer span.End()
var err error
errChan := make(chan error, 1)
for _, engine := range multi.watermarkEngines {
go func(engine gotenberg.PdfEngine) {
errChan <- engine.Watermark(ctx, logger, inputPath, stamp)
}(engine)
select {
case watermarkErr := <-errChan:
errored := multierr.AppendInto(&err, watermarkErr)
if !errored {
span.SetStatus(codes.Ok, "")
return nil
}
case <-ctx.Done():
return ctx.Err()
}
}
err = fmt.Errorf("watermark PDF with multi PDF engines: %w", err)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return err
}
// Stamp applies a stamp (on top of page content) to a PDF file using the
// first available engine that supports stamping.
//
//nolint:dupl
func (multi *multiPdfEngines) Stamp(ctx context.Context, logger *slog.Logger, inputPath string, stamp gotenberg.Stamp) error {
tracer := gotenberg.Tracer()
ctx, span := tracer.Start(ctx, "pdfengines.Stamp", trace.WithSpanKind(trace.SpanKindInternal))
defer span.End()
var err error
errChan := make(chan error, 1)
for _, engine := range multi.stampEngines {
go func(engine gotenberg.PdfEngine) {
errChan <- engine.Stamp(ctx, logger, inputPath, stamp)
}(engine)
select {
case stampErr := <-errChan:
errored := multierr.AppendInto(&err, stampErr)
if !errored {
span.SetStatus(codes.Ok, "")
return nil
}
case <-ctx.Done():
return ctx.Err()
}
}
err = fmt.Errorf("stamp PDF with multi PDF engines: %w", err)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return err
}
// Rotate rotates pages of a PDF file using the first available engine that
// supports rotation.
func (multi *multiPdfEngines) Rotate(ctx context.Context, logger *slog.Logger, inputPath string, angle int, pages string) error {
tracer := gotenberg.Tracer()
ctx, span := tracer.Start(ctx, "pdfengines.Rotate", trace.WithSpanKind(trace.SpanKindInternal))
defer span.End()
var err error
errChan := make(chan error, 1)
for _, engine := range multi.rotateEngines {
go func(engine gotenberg.PdfEngine) {
errChan <- engine.Rotate(ctx, logger, inputPath, angle, pages)
}(engine)
select {
case rotateErr := <-errChan:
errored := multierr.AppendInto(&err, rotateErr)
if !errored {
span.SetStatus(codes.Ok, "")
return nil
}
case <-ctx.Done():
return ctx.Err()
}
}
err = fmt.Errorf("rotate PDF with multi PDF engines: %w", err)
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return err
}
// Interface guards.
var (
_ gotenberg.PdfEngine = (*multiPdfEngines)(nil)
)