diff --git a/.github/workflows/continuous_delivery.yml b/.github/workflows/continuous_delivery.yml index b6b5e70d..ff590907 100644 --- a/.github/workflows/continuous_delivery.yml +++ b/.github/workflows/continuous_delivery.yml @@ -18,9 +18,26 @@ jobs: - name: Log in to Docker Hub Container Registry uses: docker/login-action@v3 with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push Docker image for release + env: + LINUX_AMD64_RELEASE: "true" run: | - make release GOTENBERG_VERSION=${{ github.event.release.tag_name }} - make release GOTENBERG_VERSION=${{ github.event.release.tag_name }} DOCKER_REGISTRY=thecodingmachine + make release CHROME_VERSION=127.0.6533.119-1 GOTENBERG_VERSION=${{ github.event.release.tag_name }} DOCKER_REGISTRY=ghcr.io/fulll DOCKER_REPOSITORY=gotenberg + + - name: generate aws credentials config + env: + AWS_CREDENTIALS: ${{ secrets.STAGING_AWS_CREDENTIALS }} + aws-region: eu-central-1 + run: | + mkdir -p "${HOME}/.aws" + echo "${AWS_CREDENTIALS}" > "${HOME}/.aws/credentials" + + - name: docker login and push + run: | + docker pull ghcr.io/fulll/gotenberg/gotenberg:${{ github.event.release.tag_name }}-cloudrun + docker tag ghcr.io/fulll/gotenberg/gotenberg:${{ github.event.release.tag_name }}-cloudrun 285715278780.dkr.ecr.eu-central-1.amazonaws.com/gotenberg:${{ github.event.release.tag_name }}-cloudrun + aws --region eu-central-1 ecr get-login-password | docker login --username AWS --password-stdin 285715278780.dkr.ecr.eu-central-1.amazonaws.com + docker push 285715278780.dkr.ecr.eu-central-1.amazonaws.com/gotenberg-fulll:${{ github.event.release.tag_name }}-cloudrun diff --git a/Makefile b/Makefile index def2bb19..ad05f524 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,7 @@ help: ## Show the help it: build build-tests ## Initialize the development environment GOLANG_VERSION=1.23 -DOCKER_REGISTRY=gotenberg +DOCKER_REGISTRY=ghcr.io/fulll DOCKER_REPOSITORY=gotenberg GOTENBERG_VERSION=snapshot GOTENBERG_USER_GID=1001 @@ -26,6 +26,7 @@ build: ## Build the Gotenberg's Docker image --build-arg NOTO_COLOR_EMOJI_VERSION=$(NOTO_COLOR_EMOJI_VERSION) \ --build-arg PDFTK_VERSION=$(PDFTK_VERSION) \ --build-arg PDFCPU_VERSION=$(PDFCPU_VERSION) \ + --build-arg CHROME_VERSION=$(CHROME_VERSION) \ -t $(DOCKER_REGISTRY)/$(DOCKER_REPOSITORY):$(GOTENBERG_VERSION) \ -f build/Dockerfile . @@ -167,6 +168,7 @@ build-tests: ## Build the tests' Docker image --build-arg DOCKER_REPOSITORY=$(DOCKER_REPOSITORY) \ --build-arg GOTENBERG_VERSION=$(GOTENBERG_VERSION) \ --build-arg GOLANGCI_LINT_VERSION=$(GOLANGCI_LINT_VERSION) \ + --build-arg CHROME_VERSION=$(CHROME_VERSION) \ -t $(DOCKER_REGISTRY)/$(DOCKER_REPOSITORY):$(GOTENBERG_VERSION)-tests \ -f test/Dockerfile . @@ -212,5 +214,6 @@ release: ## Build the Gotenberg's Docker image and push it to a Docker repositor $(PDFCPU_VERSION) \ $(DOCKER_REGISTRY) \ $(DOCKER_REPOSITORY) \ - $(LINUX_AMD64_RELEASE) + $(LINUX_AMD64_RELEASE) \ + $(CHROME_VERSION) diff --git a/build/Dockerfile b/build/Dockerfile index 38771906..eada5179 100644 --- a/build/Dockerfile +++ b/build/Dockerfile @@ -62,6 +62,7 @@ ARG GOTENBERG_USER_UID ARG NOTO_COLOR_EMOJI_VERSION ARG PDFTK_VERSION ARG TMP_CHOMIUM_VERSION_ARMHF="116.0.5845.180-1~deb12u1" +ARG CHROME_VERSION LABEL org.opencontainers.image.title="Gotenberg" \ org.opencontainers.image.description="A Docker-powered stateless API for PDF files." \ @@ -151,11 +152,20 @@ RUN \ /bin/bash -c \ 'set -e &&\ if [[ "$(dpkg --print-architecture)" == "amd64" ]]; then \ - curl https://dl.google.com/linux/linux_signing_key.pub | apt-key add - &&\ - echo "deb http://dl.google.com/linux/chrome/deb/ stable main" | tee /etc/apt/sources.list.d/google-chrome.list &&\ apt-get update -qq &&\ - DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends --allow-unauthenticated google-chrome-stable &&\ - mv /usr/bin/google-chrome-stable /usr/bin/chromium; \ + if [ -z "$CHROME_VERSION" ]; then \ + curl https://dl.google.com/linux/linux_signing_key.pub | apt-key add - &&\ + echo "deb http://dl.google.com/linux/chrome/deb/ stable main" | tee /etc/apt/sources.list.d/google-chrome.list &&\ + apt-get update -qq &&\ + DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends --allow-unauthenticated google-chrome-stable &&\ + mv /usr/bin/google-chrome-stable /usr/bin/chromium; \ + else \ + apt-get update -qq &&\ + curl --output /tmp/chrome.deb "https://dl.google.com/linux/chrome/deb/pool/main/g/google-chrome-stable/google-chrome-stable_${CHROME_VERSION}_amd64.deb" &&\ + DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends /tmp/chrome.deb &&\ + mv /usr/bin/google-chrome-stable /usr/bin/chromium &&\ + rm -rf /tmp/chrome.deb; \ + fi \ elif [[ "$(dpkg --print-architecture)" == "armhf" ]]; then \ apt-get update -qq &&\ DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends devscripts &&\ diff --git a/go.mod b/go.mod index 02b01da4..f385b36a 100644 --- a/go.mod +++ b/go.mod @@ -27,15 +27,18 @@ require ( github.com/ulikunitz/xz v0.5.12 // indirect go.uber.org/multierr v1.11.0 go.uber.org/zap v1.27.0 - golang.org/x/crypto v0.29.0 // indirect + golang.org/x/crypto v0.31.0 // indirect golang.org/x/net v0.31.0 - golang.org/x/sync v0.9.0 - golang.org/x/sys v0.27.0 // indirect - golang.org/x/term v0.26.0 - golang.org/x/text v0.20.0 + golang.org/x/sync v0.10.0 + golang.org/x/sys v0.28.0 // indirect + golang.org/x/term v0.27.0 + golang.org/x/text v0.21.0 ) -require github.com/dlclark/regexp2 v1.11.4 +require ( + github.com/dlclark/regexp2 v1.11.4 + github.com/pdfcpu/pdfcpu v0.9.1 +) require ( github.com/aymerick/douceur v0.2.0 // indirect @@ -48,16 +51,21 @@ require ( github.com/gobwas/ws v1.4.0 // indirect github.com/golang-jwt/jwt v3.2.2+incompatible // indirect github.com/gorilla/css v1.0.1 // indirect + github.com/hhrutter/lzw v1.0.0 // indirect + github.com/hhrutter/tiff v1.0.1 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/prometheus/client_model v0.6.1 // indirect github.com/prometheus/common v0.60.1 // indirect github.com/prometheus/procfs v0.15.1 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasttemplate v1.2.2 // indirect github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect + golang.org/x/image v0.21.0 // indirect golang.org/x/time v0.8.0 // indirect google.golang.org/protobuf v1.35.2 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/go.sum b/go.sum index 6f328e5f..4fb0cd43 100644 --- a/go.sum +++ b/go.sum @@ -51,6 +51,10 @@ github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB1 github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU= github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= +github.com/hhrutter/lzw v1.0.0 h1:laL89Llp86W3rRs83LvKbwYRx6INE8gDn0XNb1oXtm0= +github.com/hhrutter/lzw v1.0.0/go.mod h1:2HC6DJSn/n6iAZfgM3Pg+cP1KxeWc3ezG8bBqW5+WEo= +github.com/hhrutter/tiff v1.0.1 h1:MIus8caHU5U6823gx7C6jrfoEvfSTGtEFRiM8/LOzC0= +github.com/hhrutter/tiff v1.0.1/go.mod h1:zU/dNgDm0cMIa8y8YwcYBeuEEveI4B0owqHyiPpJPHc= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A= @@ -61,6 +65,10 @@ github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgo github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/labstack/echo/v4 v4.12.0 h1:IKpw49IMryVB2p1a4dzwlhP1O2Tf2E0Ir/450lH+kI0= @@ -87,9 +95,13 @@ github.com/nwaples/rardecode v1.1.3 h1:cWCaZwfM5H7nAD6PyEdcVnczzV8i/JtotnyW/dD9l github.com/nwaples/rardecode v1.1.3/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0= github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw= github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0= +github.com/pdfcpu/pdfcpu v0.9.1 h1:q8/KlBdHjkE7ZJU4ofhKG5Rjf7M6L324CVM6BMDySao= +github.com/pdfcpu/pdfcpu v0.9.1/go.mod h1:fVfOloBzs2+W2VJCCbq60XIxc3yJHAZ0Gahv1oO0gyI= github.com/pierrec/lz4/v4 v4.1.2/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ= github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y= @@ -100,6 +112,8 @@ github.com/prometheus/common v0.60.1 h1:FUas6GcOw66yB/73KC+BOZoFJmbo/1pojoILArPA github.com/prometheus/common v0.60.1/go.mod h1:h0LYf1R1deLSKtD4Vdg8gy4RuOvENW2J/h19V5NADQw= github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= @@ -128,24 +142,31 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= -golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ= -golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg= +golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/image v0.21.0 h1:c5qV36ajHpdj4Qi0GnE0jUc/yuo33OLFaa0d+crTD5s= +golang.org/x/image v0.21.0/go.mod h1:vUbsLavqK/W303ZroQQVKQ+Af3Yl6Uz1Ppu5J/cLz78= golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo= golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM= -golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= -golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= -golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU= -golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E= -golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= -golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= +golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io= google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/pkg/gotenberg/mocks.go b/pkg/gotenberg/mocks.go index 49154c32..a1f26d42 100644 --- a/pkg/gotenberg/mocks.go +++ b/pkg/gotenberg/mocks.go @@ -35,10 +35,11 @@ func (mod *ValidatorMock) Validate() error { // PdfEngineMock is a mock for the [PdfEngine] interface. type PdfEngineMock struct { - MergeMock func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error - ConvertMock func(ctx context.Context, logger *zap.Logger, formats PdfFormats, inputPath, outputPath string) error - ReadMetadataMock func(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]interface{}, error) - WriteMetadataMock func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error + MergeMock func(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error + ConvertMock func(ctx context.Context, logger *zap.Logger, formats PdfFormats, inputPath, outputPath string) error + ReadMetadataMock func(ctx context.Context, logger *zap.Logger, inputPath string) (map[string]interface{}, error) + WriteMetadataMock func(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error + ImportBookmarksMock func(ctx context.Context, logger *zap.Logger, inputPath, inputBookmarksPath, outputPath string) error } func (engine *PdfEngineMock) Merge(ctx context.Context, logger *zap.Logger, inputPaths []string, outputPath string) error { @@ -57,6 +58,10 @@ func (engine *PdfEngineMock) WriteMetadata(ctx context.Context, logger *zap.Logg return engine.WriteMetadataMock(ctx, logger, metadata, inputPath) } +func (engine *PdfEngineMock) ImportBookmarks(ctx context.Context, logger *zap.Logger, inputPath, inputBookmarksPath, outputPath string) error { + return engine.ImportBookmarksMock(ctx, logger, inputPath, inputBookmarksPath, outputPath) +} + // PdfEngineProviderMock is a mock for the [PdfEngineProvider] interface. type PdfEngineProviderMock struct { PdfEngineMock func() (PdfEngine, error) diff --git a/pkg/gotenberg/pdfengine.go b/pkg/gotenberg/pdfengine.go index 87c32c15..d513917c 100644 --- a/pkg/gotenberg/pdfengine.go +++ b/pkg/gotenberg/pdfengine.go @@ -74,6 +74,9 @@ type PdfEngine interface { // WriteMetadata writes the metadata into a given PDF file. WriteMetadata(ctx context.Context, logger *zap.Logger, metadata map[string]interface{}, inputPath string) error + + // Import Bookmarks in a given PDF. + ImportBookmarks(ctx context.Context, logger *zap.Logger, inputPath, inputBookmarksPath, outputPath string) error } // PdfEngineProvider offers an interface to instantiate a [PdfEngine]. diff --git a/pkg/modules/chromium/chromium.go b/pkg/modules/chromium/chromium.go index b31c5e70..326b506a 100644 --- a/pkg/modules/chromium/chromium.go +++ b/pkg/modules/chromium/chromium.go @@ -10,6 +10,7 @@ import ( "github.com/alexliesenfeld/health" "github.com/chromedp/cdproto/network" "github.com/dlclark/regexp2" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu" flag "github.com/spf13/pflag" "go.uber.org/zap" @@ -210,6 +211,10 @@ type PdfOptions struct { // same format as the HeaderTemplate. FooterTemplate string + // Bookmarks to be inserted unmarshaled + // as defined in pdfcpu bookmarks export + Bookmarks pdfcpu.BookmarkTree + // PreferCssPageSize defines whether to prefer page size as defined by CSS. // If false, the content will be scaled to fit the paper size. PreferCssPageSize bool @@ -236,6 +241,7 @@ func DefaultPdfOptions() PdfOptions { PageRanges: "", HeaderTemplate: "", FooterTemplate: "", + Bookmarks: pdfcpu.BookmarkTree{}, PreferCssPageSize: false, GenerateDocumentOutline: false, } diff --git a/pkg/modules/chromium/routes.go b/pkg/modules/chromium/routes.go index 2ae33b25..eb3ee541 100644 --- a/pkg/modules/chromium/routes.go +++ b/pkg/modules/chromium/routes.go @@ -16,6 +16,7 @@ import ( "github.com/dlclark/regexp2" "github.com/labstack/echo/v4" "github.com/microcosm-cc/bluemonday" + "github.com/pdfcpu/pdfcpu/pkg/pdfcpu" "github.com/russross/blackfriday/v2" "go.uber.org/multierr" @@ -205,6 +206,7 @@ func FormDataChromiumPdfOptions(ctx *api.Context) (*api.FormData, PdfOptions) { marginTop, marginBottom, marginLeft, marginRight float64 pageRanges string headerTemplate, footerTemplate string + bookmarks pdfcpu.BookmarkTree preferCssPageSize bool generateDocumentOutline bool ) @@ -223,6 +225,17 @@ func FormDataChromiumPdfOptions(ctx *api.Context) (*api.FormData, PdfOptions) { String("nativePageRanges", &pageRanges, defaultPdfOptions.PageRanges). Content("header.html", &headerTemplate, defaultPdfOptions.HeaderTemplate). Content("footer.html", &footerTemplate, defaultPdfOptions.FooterTemplate). + Custom("bookmarks", func(value string) error { + if len(value) > 0 { + err := json.Unmarshal([]byte(value), &bookmarks) + if err != nil { + return fmt.Errorf("unmarshal bookmarks: %w", err) + } + } else { + bookmarks = defaultPdfOptions.Bookmarks + } + return nil + }). Bool("preferCssPageSize", &preferCssPageSize, defaultPdfOptions.PreferCssPageSize). Bool("generateDocumentOutline", &generateDocumentOutline, defaultPdfOptions.GenerateDocumentOutline) @@ -241,6 +254,7 @@ func FormDataChromiumPdfOptions(ctx *api.Context) (*api.FormData, PdfOptions) { PageRanges: pageRanges, HeaderTemplate: headerTemplate, FooterTemplate: footerTemplate, + Bookmarks: bookmarks, PreferCssPageSize: preferCssPageSize, GenerateDocumentOutline: generateDocumentOutline, } @@ -632,6 +646,20 @@ func convertUrl(ctx *api.Context, chromium Api, engine gotenberg.PdfEngine, url return fmt.Errorf("convert to PDF: %w", err) } + if options.GenerateDocumentOutline { + bookmarks, errMarshal := json.Marshal(options.Bookmarks) + outputBMPath := ctx.GeneratePath(".pdf") + + if errMarshal == nil { + outputPath, err = pdfengines.ImportBookmarksStub(ctx, engine, outputPath, bookmarks, outputBMPath) + if err != nil { + return fmt.Errorf("import bookmarks into PDF err: %w", err) + } + } else { + return fmt.Errorf("import bookmarks into PDF errMarshal : %w", errMarshal) + } + } + outputPaths, err := pdfengines.ConvertStub(ctx, engine, pdfFormats, []string{outputPath}) if err != nil { return fmt.Errorf("convert PDF: %w", err) diff --git a/pkg/modules/exiftool/exiftool.go b/pkg/modules/exiftool/exiftool.go index 7d2cb8d9..33e6abe5 100644 --- a/pkg/modules/exiftool/exiftool.go +++ b/pkg/modules/exiftool/exiftool.go @@ -146,6 +146,11 @@ func (engine *ExifTool) WriteMetadata(ctx context.Context, logger *zap.Logger, m return nil } +// Import bookmarks is not available in this implementation. +func (engine *ExifTool) ImportBookmarks(ctx context.Context, logger *zap.Logger, inputPath, inputBookmarksPath, outputPath string) error { + return fmt.Errorf("import bookmarks into PDF with ExifTool: %w", gotenberg.ErrPdfEngineMethodNotSupported) +} + // Interface guards. var ( _ gotenberg.Module = (*ExifTool)(nil) diff --git a/pkg/modules/libreoffice/pdfengine/pdfengine.go b/pkg/modules/libreoffice/pdfengine/pdfengine.go index b478c21b..11a798a9 100644 --- a/pkg/modules/libreoffice/pdfengine/pdfengine.go +++ b/pkg/modules/libreoffice/pdfengine/pdfengine.go @@ -81,6 +81,11 @@ func (engine *LibreOfficePdfEngine) WriteMetadata(ctx context.Context, logger *z return fmt.Errorf("write PDF metadata with LibreOffice: %w", gotenberg.ErrPdfEngineMethodNotSupported) } +// Import bookmarks is not available in this implementation. +func (engine *LibreOfficePdfEngine) ImportBookmarks(ctx context.Context, logger *zap.Logger, inputPath, inputBookmarksPath, outputPath string) error { + return fmt.Errorf("import bookmarks into PDF with LibreOffice: %w", gotenberg.ErrPdfEngineMethodNotSupported) +} + // Interface guards. var ( _ gotenberg.Module = (*LibreOfficePdfEngine)(nil) diff --git a/pkg/modules/pdfcpu/doc.go b/pkg/modules/pdfcpu/doc.go index e68e2a61..842ce78a 100644 --- a/pkg/modules/pdfcpu/doc.go +++ b/pkg/modules/pdfcpu/doc.go @@ -2,6 +2,7 @@ // interface using the pdfcpu command-line tool. This package allows for: // // 1. The merging of PDF files. +// 2. Import bookmarks in a PDF file. // // See: https://github.com/pdfcpu/pdfcpu. package pdfcpu diff --git a/pkg/modules/pdfcpu/pdfcpu.go b/pkg/modules/pdfcpu/pdfcpu.go index ac2d5358..fcc6815c 100644 --- a/pkg/modules/pdfcpu/pdfcpu.go +++ b/pkg/modules/pdfcpu/pdfcpu.go @@ -85,6 +85,28 @@ func (engine *PdfCpu) WriteMetadata(ctx context.Context, logger *zap.Logger, met return fmt.Errorf("write PDF metadata with pdfcpu: %w", gotenberg.ErrPdfEngineMethodNotSupported) } +// Import Bookmarks in a given PDF. +func (engine *PdfCpu) ImportBookmarks(ctx context.Context, logger *zap.Logger, inputPath, inputBookmarksPath, outputPath string) error { + if inputBookmarksPath == "" { + return nil + } + + var args []string + args = append(args, "bookmarks", "import", inputPath, inputBookmarksPath, outputPath) + + cmd, err := gotenberg.CommandContext(ctx, logger, engine.binPath, args...) + if err != nil { + return fmt.Errorf("create command: %w", err) + } + + _, err = cmd.Exec() + if err == nil { + return nil + } + + return fmt.Errorf("ImportBookmarks PDFs with pdfcpu: %w", err) +} + // Interface guards. var ( _ gotenberg.Module = (*PdfCpu)(nil) diff --git a/pkg/modules/pdfengines/multi.go b/pkg/modules/pdfengines/multi.go index 4cbbc3ea..e8e9f12d 100644 --- a/pkg/modules/pdfengines/multi.go +++ b/pkg/modules/pdfengines/multi.go @@ -12,23 +12,26 @@ import ( ) type multiPdfEngines struct { - mergeEngines []gotenberg.PdfEngine - convertEngines []gotenberg.PdfEngine - readMedataEngines []gotenberg.PdfEngine - writeMedataEngines []gotenberg.PdfEngine + mergeEngines []gotenberg.PdfEngine + convertEngines []gotenberg.PdfEngine + readMedataEngines []gotenberg.PdfEngine + writeMedataEngines []gotenberg.PdfEngine + importBookmarksEngines []gotenberg.PdfEngine } func newMultiPdfEngines( mergeEngines, convertEngines, readMetadataEngines, - writeMedataEngines []gotenberg.PdfEngine, + writeMedataEngines, + importBookmarksEngines []gotenberg.PdfEngine, ) *multiPdfEngines { return &multiPdfEngines{ - mergeEngines: mergeEngines, - convertEngines: convertEngines, - readMedataEngines: readMetadataEngines, - writeMedataEngines: writeMedataEngines, + mergeEngines: mergeEngines, + convertEngines: convertEngines, + readMedataEngines: readMetadataEngines, + writeMedataEngines: writeMedataEngines, + importBookmarksEngines: importBookmarksEngines, } } @@ -141,6 +144,30 @@ func (multi *multiPdfEngines) WriteMetadata(ctx context.Context, logger *zap.Log return fmt.Errorf("write PDF metadata with multi PDF engines: %w", err) } +// Merge is not available in this implementation. +func (multi *multiPdfEngines) ImportBookmarks(ctx context.Context, logger *zap.Logger, inputPath, inputBookmarksPath, outputPath string) error { + var err error + errChan := make(chan error, 1) + + for _, engine := range multi.importBookmarksEngines { + go func(engine gotenberg.PdfEngine) { + errChan <- engine.ImportBookmarks(ctx, logger, inputPath, inputBookmarksPath, outputPath) + }(engine) + + select { + case mergeErr := <-errChan: + errored := multierr.AppendInto(&err, mergeErr) + if !errored { + return nil + } + case <-ctx.Done(): + return ctx.Err() + } + } + + return fmt.Errorf("import bookmarks into PDF with multi PDF engines: %w", err) +} + // Interface guards. var ( _ gotenberg.PdfEngine = (*multiPdfEngines)(nil) diff --git a/pkg/modules/pdfengines/multi_test.go b/pkg/modules/pdfengines/multi_test.go index 00e706d7..d8623d68 100644 --- a/pkg/modules/pdfengines/multi_test.go +++ b/pkg/modules/pdfengines/multi_test.go @@ -30,6 +30,7 @@ func TestMultiPdfEngines_Merge(t *testing.T) { nil, nil, nil, + nil, ), ctx: context.Background(), expectError: false, @@ -52,6 +53,7 @@ func TestMultiPdfEngines_Merge(t *testing.T) { nil, nil, nil, + nil, ), ctx: context.Background(), expectError: false, @@ -74,6 +76,7 @@ func TestMultiPdfEngines_Merge(t *testing.T) { nil, nil, nil, + nil, ), ctx: context.Background(), expectError: true, @@ -91,6 +94,7 @@ func TestMultiPdfEngines_Merge(t *testing.T) { nil, nil, nil, + nil, ), ctx: func() context.Context { ctx, cancel := context.WithCancel(context.Background()) @@ -135,6 +139,7 @@ func TestMultiPdfEngines_Convert(t *testing.T) { }, nil, nil, + nil, ), ctx: context.Background(), }, @@ -156,6 +161,7 @@ func TestMultiPdfEngines_Convert(t *testing.T) { }, nil, nil, + nil, ), ctx: context.Background(), }, @@ -177,6 +183,7 @@ func TestMultiPdfEngines_Convert(t *testing.T) { }, nil, nil, + nil, ), ctx: context.Background(), expectError: true, @@ -194,6 +201,7 @@ func TestMultiPdfEngines_Convert(t *testing.T) { }, nil, nil, + nil, ), ctx: func() context.Context { ctx, cancel := context.WithCancel(context.Background()) @@ -238,6 +246,7 @@ func TestMultiPdfEngines_ReadMetadata(t *testing.T) { }, }, nil, + nil, ), ctx: context.Background(), }, @@ -259,6 +268,7 @@ func TestMultiPdfEngines_ReadMetadata(t *testing.T) { }, }, nil, + nil, ), ctx: context.Background(), }, @@ -280,6 +290,7 @@ func TestMultiPdfEngines_ReadMetadata(t *testing.T) { }, }, nil, + nil, ), ctx: context.Background(), expectError: true, @@ -297,6 +308,7 @@ func TestMultiPdfEngines_ReadMetadata(t *testing.T) { }, }, nil, + nil, ), ctx: func() context.Context { ctx, cancel := context.WithCancel(context.Background()) @@ -341,6 +353,7 @@ func TestMultiPdfEngines_WriteMetadata(t *testing.T) { }, }, }, + nil, ), ctx: context.Background(), }, @@ -362,6 +375,7 @@ func TestMultiPdfEngines_WriteMetadata(t *testing.T) { }, }, }, + nil, ), ctx: context.Background(), }, @@ -383,6 +397,7 @@ func TestMultiPdfEngines_WriteMetadata(t *testing.T) { }, }, }, + nil, ), ctx: context.Background(), expectError: true, @@ -400,6 +415,7 @@ func TestMultiPdfEngines_WriteMetadata(t *testing.T) { }, }, }, + nil, ), ctx: func() context.Context { ctx, cancel := context.WithCancel(context.Background()) diff --git a/pkg/modules/pdfengines/pdfengines.go b/pkg/modules/pdfengines/pdfengines.go index 7bd00018..b0a55bf0 100644 --- a/pkg/modules/pdfengines/pdfengines.go +++ b/pkg/modules/pdfengines/pdfengines.go @@ -27,12 +27,13 @@ func init() { // the [api.Router] interface to expose relevant PDF processing routes if // enabled. type PdfEngines struct { - mergeNames []string - convertNames []string - readMetadataNames []string - writeMedataNames []string - engines []gotenberg.PdfEngine - disableRoutes bool + mergeNames []string + convertNames []string + readMetadataNames []string + writeMedataNames []string + importBookmarksNames []string + engines []gotenberg.PdfEngine + disableRoutes bool } // Descriptor returns a PdfEngines' module descriptor. @@ -45,6 +46,7 @@ func (mod *PdfEngines) Descriptor() gotenberg.ModuleDescriptor { 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-import-bookmarks-engines", []string{"pdfcpu"}, "Set the PDF engines and their order for the import bookmarks feature - empty means all") fs.Bool("pdfengines-disable-routes", false, "Disable the routes") fs.StringSlice("pdfengines-engines", make([]string, 0), "Set the default PDF engines and their default order - all by default") @@ -67,6 +69,7 @@ func (mod *PdfEngines) Provision(ctx *gotenberg.Context) error { convertNames := flags.MustStringSlice("pdfengines-convert-engines") readMetadataNames := flags.MustStringSlice("pdfengines-read-metadata-engines") writeMetadataNames := flags.MustStringSlice("pdfengines-write-metadata-engines") + importBookmarksNames := flags.MustStringSlice("pdfengines-import-bookmarks-engines") mod.disableRoutes = flags.MustBool("pdfengines-disable-routes") engines, err := ctx.Modules(new(gotenberg.PdfEngine)) @@ -113,6 +116,11 @@ func (mod *PdfEngines) Provision(ctx *gotenberg.Context) error { mod.writeMedataNames = writeMetadataNames } + mod.importBookmarksNames = defaultNames + if len(importBookmarksNames) > 0 { + mod.importBookmarksNames = importBookmarksNames + } + return nil } @@ -164,6 +172,7 @@ func (mod *PdfEngines) Validate() error { findNonExistingEngines(mod.convertNames) findNonExistingEngines(mod.readMetadataNames) findNonExistingEngines(mod.writeMedataNames) + findNonExistingEngines(mod.importBookmarksNames) if len(nonExistingEngines) == 0 { return nil @@ -180,6 +189,7 @@ func (mod *PdfEngines) SystemMessages() []string { fmt.Sprintf("convert engines - %s", strings.Join(mod.convertNames[:], " ")), fmt.Sprintf("read metadata engines - %s", strings.Join(mod.readMetadataNames[:], " ")), fmt.Sprintf("write medata engines - %s", strings.Join(mod.writeMedataNames[:], " ")), + fmt.Sprintf("import bookmarks engines - %s", strings.Join(mod.importBookmarksNames[:], " ")), } } @@ -204,6 +214,7 @@ func (mod *PdfEngines) PdfEngine() (gotenberg.PdfEngine, error) { engines(mod.convertNames), engines(mod.readMetadataNames), engines(mod.writeMedataNames), + engines(mod.importBookmarksNames), ), nil } diff --git a/pkg/modules/pdfengines/pdfengines_test.go b/pkg/modules/pdfengines/pdfengines_test.go index fe999432..01c997c4 100644 --- a/pkg/modules/pdfengines/pdfengines_test.go +++ b/pkg/modules/pdfengines/pdfengines_test.go @@ -29,7 +29,8 @@ func TestPdfEngines_Provision(t *testing.T) { expectedConvertPdfEngines []string expectedReadMetadataPdfEngines []string expectedWriteMetadataPdfEngines []string - expectError bool + //expectedImportBookmarksPdfEngines []string + expectError bool }{ { scenario: "no selection from user", @@ -69,7 +70,8 @@ func TestPdfEngines_Provision(t *testing.T) { expectedConvertPdfEngines: []string{"libreoffice-pdfengine"}, expectedReadMetadataPdfEngines: []string{"exiftool"}, expectedWriteMetadataPdfEngines: []string{"exiftool"}, - expectError: false, + //expectedImportBookmarksPdfEngines: []string{"pdfcpu"}, + expectError: false, }, { scenario: "selection from user", @@ -107,7 +109,7 @@ func TestPdfEngines_Provision(t *testing.T) { } fs := new(PdfEngines).Descriptor().FlagSet - err := fs.Parse([]string{"--pdfengines-merge-engines=b", "--pdfengines-convert-engines=b", "--pdfengines-read-metadata-engines=a", "--pdfengines-write-metadata-engines=a"}) + err := fs.Parse([]string{"--pdfengines-merge-engines=b", "--pdfengines-convert-engines=b", "--pdfengines-read-metadata-engines=a", "--pdfengines-write-metadata-engines=a", "pdfengines-import-bookmarks-engines=b"}) if err != nil { t.Fatalf("expected no error but got: %v", err) } @@ -128,7 +130,8 @@ func TestPdfEngines_Provision(t *testing.T) { expectedConvertPdfEngines: []string{"b"}, expectedReadMetadataPdfEngines: []string{"a"}, expectedWriteMetadataPdfEngines: []string{"a"}, - expectError: false, + //expectedImportBookmarksPdfEngines: []string{"b"}, + expectError: false, }, { scenario: "no valid PDF engine", @@ -194,6 +197,10 @@ func TestPdfEngines_Provision(t *testing.T) { t.Fatalf("expected %d write metadata names but got %d", len(tc.expectedWriteMetadataPdfEngines), len(mod.writeMedataNames)) } + // if len(tc.expectedImportBookmarksPdfEngines) != len(mod.importBookmarksNames) { + // t.Fatalf("expected %d write metadata names but got %d", len(tc.expectedImportBookmarksPdfEngines), len(mod.importBookmarksNames)) + // } + for index, name := range mod.mergeNames { if name != tc.expectedMergePdfEngines[index] { t.Fatalf("expected merge name at index %d to be %s, but got: %s", index, name, tc.expectedMergePdfEngines[index]) @@ -217,6 +224,12 @@ func TestPdfEngines_Provision(t *testing.T) { t.Fatalf("expected write metadat name at index %d to be %s, but got: %s", index, name, tc.expectedWriteMetadataPdfEngines[index]) } } + + // for index, name := range mod.importBookmarksNames { + // if name != tc.expectedImportBookmarksPdfEngines[index] { + // t.Fatalf("expected import bookmarks name at index %d to be %s, but got: %s", index, name, tc.expectedImportBookmarksPdfEngines[index]) + // } + // } }) } } @@ -280,11 +293,12 @@ func TestPdfEngines_Validate(t *testing.T) { } { t.Run(tc.scenario, func(t *testing.T) { mod := PdfEngines{ - mergeNames: tc.names, - convertNames: tc.names, - readMetadataNames: tc.names, - writeMedataNames: tc.names, - engines: tc.engines, + mergeNames: tc.names, + convertNames: tc.names, + readMetadataNames: tc.names, + writeMedataNames: tc.names, + importBookmarksNames: tc.names, + engines: tc.engines, } err := mod.Validate() @@ -306,9 +320,10 @@ func TestPdfEngines_SystemMessages(t *testing.T) { mod.convertNames = []string{"foo", "bar"} mod.readMetadataNames = []string{"foo", "bar"} mod.writeMedataNames = []string{"foo", "bar"} + mod.importBookmarksNames = []string{"foo", "bar"} messages := mod.SystemMessages() - if len(messages) != 4 { + if len(messages) != 5 { t.Errorf("expected one and only one message, but got %d", len(messages)) } @@ -317,6 +332,7 @@ func TestPdfEngines_SystemMessages(t *testing.T) { fmt.Sprintf("convert engines - %s", strings.Join(mod.convertNames[:], " ")), fmt.Sprintf("read metadata engines - %s", strings.Join(mod.readMetadataNames[:], " ")), fmt.Sprintf("write medata engines - %s", strings.Join(mod.writeMedataNames[:], " ")), + fmt.Sprintf("import bookmarks engines - %s", strings.Join(mod.importBookmarksNames[:], " ")), } for i, message := range messages { @@ -328,10 +344,11 @@ func TestPdfEngines_SystemMessages(t *testing.T) { func TestPdfEngines_PdfEngine(t *testing.T) { mod := PdfEngines{ - mergeNames: []string{"foo", "bar"}, - convertNames: []string{"foo", "bar"}, - readMetadataNames: []string{"foo", "bar"}, - writeMedataNames: []string{"foo", "bar"}, + mergeNames: []string{"foo", "bar"}, + convertNames: []string{"foo", "bar"}, + readMetadataNames: []string{"foo", "bar"}, + writeMedataNames: []string{"foo", "bar"}, + importBookmarksNames: []string{"foo", "bar"}, engines: func() []gotenberg.PdfEngine { engine1 := &struct { gotenberg.ModuleMock diff --git a/pkg/modules/pdfengines/routes.go b/pkg/modules/pdfengines/routes.go index a0ddb756..86a050bc 100644 --- a/pkg/modules/pdfengines/routes.go +++ b/pkg/modules/pdfengines/routes.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "net/http" + "os" "path/filepath" "github.com/labstack/echo/v4" @@ -105,6 +106,26 @@ func WriteMetadataStub(ctx *api.Context, engine gotenberg.PdfEngine, metadata ma return nil } +// Import Bookmarks in a given PDF. +func ImportBookmarksStub(ctx *api.Context, engine gotenberg.PdfEngine, inputPath string, inputBookmarks []byte, outputPath string) (string, error) { + if len(inputBookmarks) == 0 { + fmt.Println("ImportBookmarksStub BM empty") + return inputPath, nil + } + + inputBookmarksPath := ctx.GeneratePath(".json") + err := os.WriteFile(inputBookmarksPath, inputBookmarks, 0o600) + if err != nil { + return "", fmt.Errorf("write file %v: %w", inputBookmarksPath, err) + } + err = engine.ImportBookmarks(ctx, ctx.Log(), inputPath, inputBookmarksPath, outputPath) + if err != nil { + return "", fmt.Errorf("import bookmarks %v: %w", inputPath, err) + } + + return outputPath, nil +} + // mergeRoute returns an [api.Route] which can merge PDFs. func mergeRoute(engine gotenberg.PdfEngine) api.Route { return api.Route{ diff --git a/pkg/modules/pdftk/pdftk.go b/pkg/modules/pdftk/pdftk.go index 9846ee9d..73a4e37c 100644 --- a/pkg/modules/pdftk/pdftk.go +++ b/pkg/modules/pdftk/pdftk.go @@ -85,6 +85,11 @@ func (engine *PdfTk) WriteMetadata(ctx context.Context, logger *zap.Logger, meta return fmt.Errorf("write PDF metadata with PDFtk: %w", gotenberg.ErrPdfEngineMethodNotSupported) } +// Import bookmarks is not available in this implementation. +func (engine *PdfTk) ImportBookmarks(ctx context.Context, logger *zap.Logger, inputPath, inputBookmarksPath, outputPath string) error { + return fmt.Errorf("import bookmarks into PDF with PDFtk: %w", gotenberg.ErrPdfEngineMethodNotSupported) +} + // Interface guards. var ( _ gotenberg.Module = (*PdfTk)(nil) diff --git a/pkg/modules/qpdf/qpdf.go b/pkg/modules/qpdf/qpdf.go index 57698281..4fcd189f 100644 --- a/pkg/modules/qpdf/qpdf.go +++ b/pkg/modules/qpdf/qpdf.go @@ -87,6 +87,11 @@ func (engine *QPdf) WriteMetadata(ctx context.Context, logger *zap.Logger, metad return fmt.Errorf("write PDF metadata with QPDF: %w", gotenberg.ErrPdfEngineMethodNotSupported) } +// Import bookmarks is not available in this implementation. +func (engine *QPdf) ImportBookmarks(ctx context.Context, logger *zap.Logger, inputPath, inputBookmarksPath, outputPath string) error { + return fmt.Errorf("import bookmarks into PDF with QPDF: %w", gotenberg.ErrPdfEngineMethodNotSupported) +} + var ( _ gotenberg.Module = (*QPdf)(nil) _ gotenberg.Provisioner = (*QPdf)(nil) diff --git a/scripts/release.sh b/scripts/release.sh index f9c014ff..7856e6f6 100755 --- a/scripts/release.sh +++ b/scripts/release.sh @@ -13,6 +13,7 @@ PDFCPU_VERSION="$7" DOCKER_REGISTRY="$8" DOCKER_REPOSITORY="$9" LINUX_AMD64_RELEASE="${10}" +CHROME_VERSION="${11}" # Find out if given version is "semver". GOTENBERG_VERSION="${GOTENBERG_VERSION//v}" @@ -67,6 +68,7 @@ docker buildx build \ --build-arg NOTO_COLOR_EMOJI_VERSION="$NOTO_COLOR_EMOJI_VERSION" \ --build-arg PDFTK_VERSION="$PDFTK_VERSION" \ --build-arg PDFCPU_VERSION="$PDFCPU_VERSION" \ + --build-arg CHROME_VERSION="$CHROME_VERSION" \ $PLATFORM_FLAG \ "${TAGS[@]}" \ --push \