mirror of
https://github.com/gotenberg/gotenberg.git
synced 2026-08-17 04:32:15 +01:00
feat: add qpdf engine (#380)
Co-authored-by: Andrei Regiani <andrei.cpp@gmail.com>
This commit is contained in:
@@ -131,6 +131,8 @@ RUN \
|
|||||||
# See https://github.com/gotenberg/gotenberg/pull/273.
|
# See https://github.com/gotenberg/gotenberg/pull/273.
|
||||||
curl -o /usr/bin/pdftk-all.jar "https://gitlab.com/pdftk-java/pdftk/-/jobs/$PDFTK_VERSION/artifacts/raw/build/libs/pdftk-all.jar" &&\
|
curl -o /usr/bin/pdftk-all.jar "https://gitlab.com/pdftk-java/pdftk/-/jobs/$PDFTK_VERSION/artifacts/raw/build/libs/pdftk-all.jar" &&\
|
||||||
chmod a+x /usr/bin/pdftk-all.jar &&\
|
chmod a+x /usr/bin/pdftk-all.jar &&\
|
||||||
|
# Download QPDF.
|
||||||
|
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends qpdf &&\
|
||||||
# See https://github.com/nextcloud/docker/issues/380.
|
# See https://github.com/nextcloud/docker/issues/380.
|
||||||
mkdir -p /usr/share/man/man1mkdir -p /usr/share/man/man1 &&\
|
mkdir -p /usr/share/man/man1mkdir -p /usr/share/man/man1 &&\
|
||||||
# Cleanup.
|
# Cleanup.
|
||||||
@@ -142,7 +144,8 @@ RUN \
|
|||||||
chromium --version &&\
|
chromium --version &&\
|
||||||
libreoffice --version &&\
|
libreoffice --version &&\
|
||||||
unoconv --version &&\
|
unoconv --version &&\
|
||||||
pdftk --version
|
pdftk --version &&\
|
||||||
|
qpdf --version
|
||||||
|
|
||||||
# Copy the Gotenberg binary from the builder stage.
|
# Copy the Gotenberg binary from the builder stage.
|
||||||
COPY --from=builder /home/gotenberg /usr/bin/
|
COPY --from=builder /home/gotenberg /usr/bin/
|
||||||
@@ -152,6 +155,7 @@ ENV GC_EXCLUDE_SUBSTR "hsperfdata_root,hsperfdata_gotenberg"
|
|||||||
ENV CHROMIUM_BIN_PATH /usr/bin/chromium
|
ENV CHROMIUM_BIN_PATH /usr/bin/chromium
|
||||||
ENV UNOCONV_BIN_PATH /usr/bin/unoconv
|
ENV UNOCONV_BIN_PATH /usr/bin/unoconv
|
||||||
ENV PDFTK_BIN_PATH /usr/bin/pdftk
|
ENV PDFTK_BIN_PATH /usr/bin/pdftk
|
||||||
|
ENV QPDF_BIN_PATH /usr/bin/qpdf
|
||||||
|
|
||||||
USER gotenberg
|
USER gotenberg
|
||||||
WORKDIR /home/gotenberg
|
WORKDIR /home/gotenberg
|
||||||
|
|||||||
3
pkg/modules/qpdf/doc.go
Normal file
3
pkg/modules/qpdf/doc.go
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
// Package qpdf provides a module which abstracts the CLI tool QPDF and
|
||||||
|
// implements the gotenberg.PDFEngine interface.
|
||||||
|
package qpdf
|
||||||
117
pkg/modules/qpdf/qpdf.go
Normal file
117
pkg/modules/qpdf/qpdf.go
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
package qpdf
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
gotenberg.MustRegisterModule(QPDF{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// QPDF abstracts the CLI tool QPDF and implements the gotenberg.QPDF
|
||||||
|
// interface.
|
||||||
|
type QPDF struct {
|
||||||
|
binPath string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Descriptor returns a QPDF's module descriptor.
|
||||||
|
func (QPDF) Descriptor() gotenberg.ModuleDescriptor {
|
||||||
|
return gotenberg.ModuleDescriptor{
|
||||||
|
ID: "qpdf",
|
||||||
|
New: func() gotenberg.Module { return new(QPDF) },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Provision sets the modules properties. It returns an error if the
|
||||||
|
// environment variable QPDF_BIN_PATH is not set.
|
||||||
|
func (engine *QPDF) Provision(_ *gotenberg.Context) error {
|
||||||
|
binPath, ok := os.LookupEnv("QPDF_BIN_PATH")
|
||||||
|
if !ok {
|
||||||
|
return errors.New("QPDF_BIN_PATH environment variable is not set")
|
||||||
|
}
|
||||||
|
|
||||||
|
engine.binPath = binPath
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate validates the module properties.
|
||||||
|
func (engine QPDF) Validate() error {
|
||||||
|
_, err := os.Stat(engine.binPath)
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return fmt.Errorf("QPDF binary path does not exist: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Metrics returns the metrics.
|
||||||
|
func (engine QPDF) Metrics() ([]gotenberg.Metric, error) {
|
||||||
|
return []gotenberg.Metric{
|
||||||
|
{
|
||||||
|
Name: "qpdf_active_instances_count",
|
||||||
|
Description: "Current number of active QPDF instances.",
|
||||||
|
Read: func() float64 {
|
||||||
|
activeInstancesCountMu.RLock()
|
||||||
|
defer activeInstancesCountMu.RUnlock()
|
||||||
|
|
||||||
|
return activeInstancesCount
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge merges the given PDFs into a unique PDF.
|
||||||
|
func (engine QPDF) Merge(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error {
|
||||||
|
var args []string
|
||||||
|
args = append(args, "--empty")
|
||||||
|
args = append(args, "--pages")
|
||||||
|
args = append(args, inputPaths...)
|
||||||
|
args = append(args, "--", outputPath)
|
||||||
|
|
||||||
|
cmd, err := gotenberg.CommandContext(ctx, logger, engine.binPath, args...)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("create command: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
activeInstancesCountMu.Lock()
|
||||||
|
activeInstancesCount += 1
|
||||||
|
activeInstancesCountMu.Unlock()
|
||||||
|
|
||||||
|
err = cmd.Exec()
|
||||||
|
|
||||||
|
activeInstancesCountMu.Lock()
|
||||||
|
activeInstancesCount -= 1
|
||||||
|
activeInstancesCountMu.Unlock()
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("merge PDFs with QPDF: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert is not available for this PDF engine.
|
||||||
|
func (engine QPDF) Convert(_ context.Context, _ *zap.Logger, format, _, _ string) error {
|
||||||
|
return fmt.Errorf("convert PDF to '%s' with QPDF: %w", format, gotenberg.ErrPDFEngineMethodNotAvailable)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
activeInstancesCount float64
|
||||||
|
activeInstancesCountMu sync.RWMutex
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ gotenberg.Module = (*QPDF)(nil)
|
||||||
|
_ gotenberg.Provisioner = (*QPDF)(nil)
|
||||||
|
_ gotenberg.Validator = (*QPDF)(nil)
|
||||||
|
_ gotenberg.MetricsProvider = (*QPDF)(nil)
|
||||||
|
_ gotenberg.PDFEngine = (*QPDF)(nil)
|
||||||
|
)
|
||||||
152
pkg/modules/qpdf/qpdf_test.go
Normal file
152
pkg/modules/qpdf/qpdf_test.go
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
package qpdf
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/gotenberg/gotenberg/v7/pkg/gotenberg"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestQPDF_Descriptor(t *testing.T) {
|
||||||
|
descriptor := QPDF{}.Descriptor()
|
||||||
|
|
||||||
|
actual := reflect.TypeOf(descriptor.New())
|
||||||
|
expect := reflect.TypeOf(new(QPDF))
|
||||||
|
|
||||||
|
if actual != expect {
|
||||||
|
t.Errorf("expected '%s' but got '%s'", expect, actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestQPDF_Provision(t *testing.T) {
|
||||||
|
mod := new(QPDF)
|
||||||
|
ctx := gotenberg.NewContext(gotenberg.ParsedFlags{}, nil)
|
||||||
|
|
||||||
|
err := mod.Provision(ctx)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestQPDF_Validate(t *testing.T) {
|
||||||
|
for i, tc := range []struct {
|
||||||
|
binPath string
|
||||||
|
expectErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
expectErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
binPath: "/foo",
|
||||||
|
expectErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
binPath: os.Getenv("QPDF_BIN_PATH"),
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
mod := new(QPDF)
|
||||||
|
mod.binPath = tc.binPath
|
||||||
|
err := mod.Validate()
|
||||||
|
|
||||||
|
if tc.expectErr && err == nil {
|
||||||
|
t.Errorf("test %d: expected error but got: %v", i, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !tc.expectErr && err != nil {
|
||||||
|
t.Errorf("test %d: expected no error but got: %v", i, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestQPDF_Metrics(t *testing.T) {
|
||||||
|
metrics, err := new(QPDF).Metrics()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("expected no error but got: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(metrics) != 1 {
|
||||||
|
t.Errorf("expected %d metrics, but got %d", 1, len(metrics))
|
||||||
|
}
|
||||||
|
|
||||||
|
actual := metrics[0].Read()
|
||||||
|
if actual != 0 {
|
||||||
|
t.Errorf("expected %d QPDF instances, but got %f", 0, actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestQPDF_Merge(t *testing.T) {
|
||||||
|
for i, tc := range []struct {
|
||||||
|
ctx context.Context
|
||||||
|
inputPaths []string
|
||||||
|
expectErr bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
ctx: context.TODO(),
|
||||||
|
inputPaths: []string{
|
||||||
|
"/tests/test/testdata/pdfengines/sample1.pdf",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ctx: context.TODO(),
|
||||||
|
inputPaths: []string{
|
||||||
|
"/tests/test/testdata/pdfengines/sample1.pdf",
|
||||||
|
"/tests/test/testdata/pdfengines/sample2.pdf",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ctx: nil,
|
||||||
|
expectErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ctx: context.TODO(),
|
||||||
|
inputPaths: []string{
|
||||||
|
"foo",
|
||||||
|
},
|
||||||
|
expectErr: true,
|
||||||
|
},
|
||||||
|
} {
|
||||||
|
func() {
|
||||||
|
mod := new(QPDF)
|
||||||
|
|
||||||
|
err := mod.Provision(nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("test %d: expected error but got: %v", i, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
outputDir, err := gotenberg.MkdirAll()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("test %d: expected error but got: %v", i, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
err := os.RemoveAll(outputDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("test %d: expected no error but got: %v", i, err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
err = mod.Merge(tc.ctx, zap.NewNop(), tc.inputPaths, outputDir+"/foo.pdf")
|
||||||
|
|
||||||
|
if tc.expectErr && err == nil {
|
||||||
|
t.Errorf("test %d: expected error but got: %v", i, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !tc.expectErr && err != nil {
|
||||||
|
t.Errorf("test %d: expected no error but got: %v", i, err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestQPDF_Convert(t *testing.T) {
|
||||||
|
mod := new(QPDF)
|
||||||
|
err := mod.Convert(context.TODO(), zap.NewNop(), "", "", "")
|
||||||
|
|
||||||
|
if !errors.Is(err, gotenberg.ErrPDFEngineMethodNotAvailable) {
|
||||||
|
t.Errorf("expected error %v, but got: %v", gotenberg.ErrPDFEngineMethodNotAvailable, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,5 +13,6 @@ import (
|
|||||||
_ "github.com/gotenberg/gotenberg/v7/pkg/modules/pdfengines"
|
_ "github.com/gotenberg/gotenberg/v7/pkg/modules/pdfengines"
|
||||||
_ "github.com/gotenberg/gotenberg/v7/pkg/modules/pdftk"
|
_ "github.com/gotenberg/gotenberg/v7/pkg/modules/pdftk"
|
||||||
_ "github.com/gotenberg/gotenberg/v7/pkg/modules/prometheus"
|
_ "github.com/gotenberg/gotenberg/v7/pkg/modules/prometheus"
|
||||||
|
_ "github.com/gotenberg/gotenberg/v7/pkg/modules/qpdf"
|
||||||
_ "github.com/gotenberg/gotenberg/v7/pkg/modules/webhook"
|
_ "github.com/gotenberg/gotenberg/v7/pkg/modules/webhook"
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user