Compare commits

...

47 Commits
2.0.0 ... 4.1.0

Author SHA1 Message Date
Julien Neuhart
571c3e04c4 adding form field web fonts timeout 2019-02-01 09:40:16 +01:00
Sergei Opletaev
f9fb97a572 Fix for #44 promise wait for fonts (#45)
`runtime.ExceptionDetails: Uncaught exception at 4:47: SyntaxError: Unexpected token %`
2019-02-01 09:21:02 +01:00
Julien Neuhart
e6484bb2d5 Trigger GitHub documentation 2019-01-28 15:56:13 +01:00
Julien Neuhart
19f51fdc1f fixing PHP documentation 2019-01-28 15:36:59 +01:00
Julien Neuhart
25f7ba3ee6 4.0.0 (#42)
* adding URL conversions

* updating gotenberg version in documentation

* pkg: input as variadic string (#39)

* pkg: input as variadic string

* pkg: fix link to github doc

* update doc about v4 client

* make doc

* fixing missing variadic inputs in doc

* adding more fonts

* updating PHP documentation
2019-01-28 15:18:12 +01:00
Julien Neuhart
a3cd1ff4e1 Merge pull request #41 from thecodingmachine/office-paperSize
removing paper size for Office conversion
2019-01-25 15:57:25 +01:00
Julien Neuhart
f62c717f79 fixing typo in docs 2019-01-25 15:50:15 +01:00
Julien Neuhart
3ed99133d4 removing paper size for Office conversion 2019-01-25 15:48:40 +01:00
Julien Neuhart
ab25cc331d fixes #35 2019-01-25 15:02:18 +01:00
Julien Neuhart
2648992a9b adding moar favicons 2019-01-03 16:02:02 +01:00
Julien Neuhart
7bcc3d081e adding liveness section in documentation 2019-01-03 15:56:53 +01:00
Julien Neuhart
affd1c45ca Merge pull request #32 from Peltoche/master
Add a /ping endpoint
2019-01-03 15:37:18 +01:00
Peltoche
8c7458812d Add a /ping endpoint
This endpoint can be used to check the service liveness. It can be used
for the kubernetes liveness and readiness probs.
2019-01-03 15:21:57 +01:00
Julien Neuhart
053908c772 Merge pull request #31 from thecodingmachine/merge-alt
adding PDFtk for merging PDF files < 1.7
2019-01-03 11:10:07 +01:00
Julien Neuhart
d623736f9f rollbacking to debian 9.5 as the 9.6 image has issue with git installation 2019-01-03 11:01:02 +01:00
Julien Neuhart
a3e416ff68 adding PDFtk for merging PDF files < 1.7 2019-01-03 10:06:56 +01:00
Julien Neuhart
8bf207b9f7 improving contributing doc 2018-12-14 16:13:52 +01:00
Julien Neuhart
e1520d5e7e Merge branch 'master' of https://github.com/thecodingmachine/gotenberg 2018-12-14 15:59:59 +01:00
Julien Neuhart
4f89c9f7c6 updating logo and documentation 2018-12-14 15:59:45 +01:00
Ben Davies
d34fb92211 docs: cURL write to a file instead of stdout (#25) 2018-12-14 09:28:18 +01:00
Julien Neuhart
3f7b006d16 fixing docs generation: was adding Dockerfile too.. 2018-12-14 09:15:51 +01:00
Julien Neuhart
a15cf33691 Dockerfiles: removing useless labels. Docs: now generating with Docker. Travis: publishing done with make 2018-12-14 09:13:04 +01:00
Julien Neuhart
4c1699df6c webhook: resulting file was deleted because of defer outside of goroutine 2018-12-13 18:15:28 +01:00
Julien Neuhart
404535c512 fixing/simplifying godoc 2018-12-13 17:06:29 +01:00
Julien Neuhart
2cedcecc38 adding concurrent testing 2018-12-13 14:46:59 +01:00
Julien Neuhart
ba7b02d8d4 fixing various typos in documentation 2018-12-12 17:44:27 +01:00
Julien Neuhart
6aa3c16646 fixing golang client README 2018-12-12 16:44:32 +01:00
Julien Neuhart
5a03e17e00 fixing introduction in documentation 2018-12-12 16:37:36 +01:00
Julien Neuhart
417d145b43 fixing documentation typos 2018-12-12 16:36:21 +01:00
Julien Neuhart
e5636a2afa adding .txt and .rtf files support (#20) 2018-12-12 16:34:34 +01:00
Julien Neuhart
5291d2d129 disabling pdfcpu logs (#22) 2018-12-12 16:24:54 +01:00
Julien Neuhart
c6f6a87499 now using latest version of unoconv, allowing to specify paper size and orientation + updating go client (#24) 2018-12-12 16:16:35 +01:00
Julien Neuhart
1ef21c6ed6 RPC error (#23) 2018-12-12 12:20:07 +01:00
Julien Neuhart
6ee1932cbf improving API tests: now verifying response content type and status code 2018-12-11 14:16:43 +01:00
Julien Neuhart
83cf43a314 fixing cURL commands in documentation 2018-12-11 14:09:04 +01:00
Julien Neuhart
52c14a79a1 fixing GoDoc + some test function names 2018-12-10 20:21:31 +01:00
Julien Neuhart
152db0551d fixing GoDoc links in badges 2018-12-10 20:11:20 +01:00
Julien Neuhart
3a2467ccfe improving documentation 2018-12-10 20:08:45 +01:00
Julien Neuhart
5a28f3597c fixing typos in documentation 2018-12-10 19:59:45 +01:00
Julien Neuhart
9ce179f91b adding PHP examples in documentation 2018-12-10 19:12:54 +01:00
Julien Neuhart
7aeb072cc3 fixing issue with merge endpoint: was not returning file 2018-12-10 18:42:23 +01:00
Julien Neuhart
8a652761a3 fixing Go Report Card issues (rollback gocycly) 2018-12-10 17:03:23 +01:00
Julien Neuhart
be667575d5 fixing Go Report Card issues #2 2018-12-10 17:02:48 +01:00
Julien Neuhart
59ca8a44a0 fixing Go Report Card issues 2018-12-10 16:57:08 +01:00
Julien Neuhart
c9f0d3bfd8 fixes publishing Docker images in CI 2018-12-10 16:36:34 +01:00
Julien Neuhart
06e7a1fda1 fixing website 2018-12-10 16:13:27 +01:00
Julien Neuhart
495203c112 v3.0.0 (#18) 2018-12-10 16:09:19 +01:00
505 changed files with 7810 additions and 174163 deletions

View File

@@ -1,64 +0,0 @@
FROM {{ if eq "CI" .Orbit.Image }}golang:1.10-stretch{{ else }}debian:stretch-slim{{ end }}
LABEL authors="Julien Neuhart <j.neuhart@thecodingmachine.com>"
# |--------------------------------------------------------------------------
# | Libraries
# |--------------------------------------------------------------------------
# |
# | Installs all required libraries.
# |
RUN echo "deb http://httpredir.debian.org/debian/ stretch main contrib non-free" > /etc/apt/sources.list &&\
apt-get update &&\
apt-get install -y xvfb wkhtmltopdf pdftk unoconv ttf-mscorefonts-installer &&\
ln -s /usr/bin/xvfb-run /usr/local/bin/xvfb-run &&\
ln -s /usr/bin/wkhtmltopdf /usr/local/bin/wkhtmltopdf &&\
ln -s /usr/bin/pdftk /usr/local/bin/pdftk &&\
ln -s /usr/bin/unoconv /usr/local/bin/unoconv
RUN apt-get install -y curl gnupg &&\
curl -sL https://deb.nodesource.com/setup_8.x | bash - &&\
apt-get update &&\
apt-get install -y nodejs
RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - &&\
echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list &&\
apt-get update &&\
apt-get install -y yarn bzip2 &&\
yarn global add markdown-pdf --prefix /usr/local
# |--------------------------------------------------------------------------
# | Gotenberg
# |--------------------------------------------------------------------------
# |
# | All Gotenberg related stuff.
# |
{{- if eq "CI" .Orbit.Image }}
WORKDIR /go/src/github.com/thecodingmachine/gotenberg
# Installs lint dependencies.
RUN go get -u gopkg.in/alecthomas/gometalinter.v2 &&\
gometalinter.v2 --install
# Copies our Go source.
COPY . .
# Installs project dependencies.
RUN go get -d -v ./...
ENTRYPOINT [".ci/docker-entrypoint.sh"]
{{- else }}
COPY .ci/gotenberg /usr/bin/gotenberg
RUN ln -s /usr/bin/gotenberg /usr/local/bin/gotenberg
COPY .ci/gotenberg.yml /gotenberg/gotenberg.yml
WORKDIR /gotenberg
EXPOSE 3000
CMD ["gotenberg"]
{{- end }}

View File

@@ -1,33 +0,0 @@
#!/bin/bash
set -xe
# Statically checking Go source for errors and warnings.
gometalinter.v2 --disable-all -E vet -E gofmt -E misspell -E ineffassign -E goimports -E deadcode -E gocyclo --vendor ./...;
# Running tests according to current Gotenberg version.
if [[ "$VERSION" == "snapshot" ]]; then
for d in $(go list ./... | grep -v vendor); do
go test -race -cover -covermode=atomic $d;
done
else
echo "" > .ci/coverage.txt;
for d in $(go list ./... | grep -v vendor); do
go test -race -coverprofile=profile.out -covermode=atomic $d;
if [ -f profile.out ]; then
cat profile.out >> .ci/coverage.txt;
rm profile.out;
fi
done
fi
# Builds the Linux binary.
if [ -f .ci/gotenberg ]; then
rm .ci/gotenberg
fi
env GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -ldflags "-X main.version=${VERSION}" && mv gotenberg .ci/;
# Bye!
exit 0;

View File

@@ -1,60 +0,0 @@
# The port the application will listen to.
port: 3000
logs:
# Accepted values, in order of severity: DEBUG, INFO, WARN, ERROR, FATAL, PANIC.
# Messages at and above the selected level will be logged.
level: "INFO"
# Accepted values: text, json.
# When a TTY is not attached, the output will be in the defined format.
formatter: "text"
# You don't like a library which is used for a conversion? You want to handle a new file type?
# You may provide here your own implementation!
commands:
# Some libraries like unoconv cannot perform concurrent conversions. That's why the API does only one conversion at a time.
# If your current implementation uses libraries which are able to perform concurrent conversions, you may
# change this value to false.
lock: true
# Unlike others commands' templates, you have access to FilesPaths instead of FilePath: it gathers all PDF files which should be merged.
merge:
template: "pdftk {{ range $filePath := .FilesPaths }} {{ $filePath }} {{ end }} cat output {{ .ResultFilePath }}"
interpreter: "/bin/sh -c"
timeout: 30
# You may add more commands (or less, or even none).
conversions:
# The command template: you have access to FilePath and ResultFilePath variables.
- template: "markdown-pdf {{ .FilePath }} -o {{ .ResultFilePath }}"
# The binary which will call the command.
interpreter: "/bin/sh -c"
# Duration in seconds after which the command will be killed if it has not finished.
timeout: 30
# Files with the following extensions will be converted by the current command.
extensions:
- ".md"
- template: "xvfb-run -e /dev/stdout wkhtmltopdf {{ .FilePath }} {{ .ResultFilePath }}"
interpreter: "/bin/sh -c"
timeout: 30
extensions:
- ".html"
- ".htm"
- template: "unoconv --format pdf --output \"{{ .ResultFilePath }}\" \"{{ .FilePath }}\""
interpreter: "/bin/sh -c"
timeout: 30
extensions:
- ".doc"
- ".docx"
- ".odt"
- ".xls"
- ".xlsx"
- ".ods"
- ".ppt"
- ".pptx"
- ".odp"

View File

@@ -1,15 +1,16 @@
# Contributing
Hi! Thank your for considering contributing to Gotenberg. You'll
Hi! Thank you for considering contributing to Gotenberg. You'll
find below useful information about how to contribute to the Gotenberg project.
## Contributing code
### Install from sources
1. Fork this repository
2. Clone it to your local Go environment (requires *Go* >= 1.10)
3. Install the latest [Orbit](https://github.com/gulien/orbit) release
1. Install and run the latest version of Docker
2. Verify your Go version (>= 1.11)
3. Fork this repository
4. Clone it outside of your `GOPATH` (we're using Go modules)
### Working with git
@@ -18,6 +19,11 @@ find below useful information about how to contribute to the Gotenberg project.
3. Push to the branch (`git push origin my-new-feature`)
4. Create a new pull request
### Testing
1. Run all linters (`make lint`)
2. Run all tests (`make tests`)
## Reporting bugs and feature request
Your issue or feature request may already be reported!

View File

@@ -27,7 +27,7 @@ Explain the **motivation** for making this change. What existing problem does th
Demonstrate the code is solid. Example: The exact commands you ran and their output.
<!-- Make sure tests pass on both Travis and AppVeyor. -->
<!-- Make sure tests pass on Travis. -->
**Closing issues**
@@ -37,8 +37,9 @@ Fixes #
**Checklist**
- [ ] Have you followed the guidelines in our [CONTRIBUTING](CONTRIBUTING.md) guide?
- [ ] Have you lint your code locally prior to submission (`orbit run fmt`)?
- [ ] Have you lint your code locally prior to submission (`make lint`)?
- [ ] Have you written new tests for your core changes, as applicable?
- [ ] Have you successfully ran tests with your changes locally (`orbit run ci`)?
- [ ] Have you successfully ran tests with your changes locally (`make tests`)?
- [ ] Have you updated the documentation (`make doc`)?
- [ ] I have squashed any insignificant commits
- [ ] This change has comments for package types, values, functions, and non-obvious lines of code

2
.gitignore vendored
View File

@@ -1,2 +0,0 @@
.ci/coverage.txt
.ci/gotenberg

View File

@@ -1,28 +0,0 @@
---
project_name: Gotenberg
builds:
- main: ./main.go
binary: gotenberg
env:
- CGO_ENABLED=0
goos:
- linux
- darwin
- windows
goarch:
- 386
- amd64
- arm
- arm64
archive:
name_template: '{{ .Binary }}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}'
replacements:
darwin: Darwin
linux: Linux
windows: Windows
386: i386
amd64: x86_64
format: tar.gz
format_overrides:
- goos: windows
format: zip

View File

@@ -10,14 +10,16 @@ env:
- secure: d6pliM7iR7KJ3TQkr1C0tHi0HgU6Vdmc/ycw11XrXs794/dgj6g9UDrhgcvT2mTzOVIUNouHx7gyEWAlwJv7p8PPyguHJfIsyqMWfaXNPoytTx1ETUiygl1G/UsvXVdry6I5tZF/U+/PJ3hBXoAvo8EOFe0/MxxvP3ghCeeCpeEwEdoYP2jbADGffywVUNI7OEdmS29A28GusXXp2cWCIZYSq5Gn5iu1ZMPaJA6SkRTXpS+iVVLUvlbz5HW09ETFfurTvsdz5Ze4JICiuQt///CR1uwdxD8K7Ils/mkenaMtja9NHA9RdQBa51Azn8iCLsObCTYc52Zb6atbCi/X4ae9M8GA8qaaLigNz5G4v1BHneJylYfWKj5GwqUA3VmRu1mlEw7d2sN2uFRmgIl5kgsYYyOVOLBOKaJWhyBhNnR4U8KeqwUkOnlSdmNZnjfexXXBEcKHen1SozpXwMnzZh8LZuN6BNYd6TVpKmjulGI3vqNMM0c4JJZ9pSm4q5FjbNGQArwCsujDDwY4bCGZSJJTPMXjYOrx4yJ1RY+0TpyV9oDU3ogEqaWnhrkgrphfiYBlDY2bEvrxMTMukLCxJb0Nru6LGCpg4X1P9hu9r0gceqUSnBgdJI4rYTNddenxlEd4j/Thqv835stJWz6YoV+rfvERuSUpntl8xvrk3U8=
- secure: CmLh169xwt3OC7l19lDUwdZYfw+l/+XXKjTGxnuJiXHa9hAH+nc4mEUSmqHMHwjquyYE6LnSVGZ94LH8sCaGed/TMm0Lj3DZtZ0VsjYu3sKsNrVJn5WdYEah+GvNJ8i8uzvHnGk1L2fAagLpXFUB2K9UraA1CFiHs1fF5Fua7Ra0mE7lMz0MFvyvzcmc9u8nMz71K2YVsXqn+I/rv8onJCTtc/Ds0hVrBXwG5Fh0T3xHqjOhME9ZI2hRHnnQVdBojSY3USifQ//qAKowfBVc0pebEtgFpyXx/I17A6xCm0KVtnA2tNqu1+XFkjWaBzryaLE9ujhJcTRC/YK0JUZ+QxfYbD5C7jVy9Jlx1lOEjJ42B/ImNxPP+9p3wi2MWNbcbZJPxIe635fnT17lI7CLK2Qg1xjfTQ4WTEKW5IhnYMiyfRKiripFh4PCkDm8cJlTbpntsX1n0uheydbsgf/11vS7o1WGhVQ8+Jelc4DvIbBHCqienQEOLJDBwm7FiK5JbwZJ6ZAGeDM/IhGU1h9PNjVbHUyJhUxYoPYUE9QsZFT7ENCrRQpq49KOzXBRfffyJNJRh+Ep9X5YoW0dUp0MNu5B7z2+6w9X9n/C+loa11xF42bFzniCvvbSOQ37J+EngDinVxJkNPeyivR4fye2ENhIAjAEjouTJLOSXL9nAOU=
before_install:
- wget -qO- https://github.com/gulien/orbit/releases/download/v3.0.0/orbit_Linux_x86_64.tar.gz | tar xvz -C .
- sudo mv ./orbit /usr/local/bin && chmod +x /usr/local/bin/orbit
stages:
- tests
- publish
script:
- orbit run ci -p "Version,${TRAVIS_BRANCH}"
- orbit run build -p "Version,${TRAVIS_BRANCH}"
# Uploads coverage.
- bash <(curl -s https://codecov.io/bash)
# Let's push to Docker hub only if not in a pull request and the branch name matches the semver format.
- if [[ "$TRAVIS_PULL_REQUEST" = false && "$TRAVIS_BRANCH" =~ ^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(-(0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(\.(0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*)?(\+[0-9a-zA-Z-]+(\.[0-9a-zA-Z-]+)*)?$ ]]; then docker login -u $DOCKER_USER -p $DOCKER_PASS && docker push thecodingmachine/gotenberg:${TRAVIS_BRANCH}; fi
jobs:
include:
- stage: tests
script: make lint
- stage: tests
script: make tests
- stage: publish
if: tag IS present
script: make publish VERSION=$TRAVIS_TAG DOCKER_USER=$DOCKER_USER DOCKER_PASSWORD=$DOCKER_PASS

View File

@@ -1,46 +0,0 @@
FROM debian:stretch-slim
LABEL authors="Julien Neuhart <j.neuhart@thecodingmachine.com>"
# |--------------------------------------------------------------------------
# | Libraries
# |--------------------------------------------------------------------------
# |
# | Installs all required libraries.
# |
RUN echo "deb http://httpredir.debian.org/debian/ stretch main contrib non-free" > /etc/apt/sources.list &&\
apt-get update &&\
apt-get install -y xvfb wkhtmltopdf pdftk unoconv ttf-mscorefonts-installer &&\
ln -s /usr/bin/xvfb-run /usr/local/bin/xvfb-run &&\
ln -s /usr/bin/wkhtmltopdf /usr/local/bin/wkhtmltopdf &&\
ln -s /usr/bin/pdftk /usr/local/bin/pdftk &&\
ln -s /usr/bin/unoconv /usr/local/bin/unoconv
RUN apt-get install -y curl gnupg &&\
curl -sL https://deb.nodesource.com/setup_8.x | bash - &&\
apt-get update &&\
apt-get install -y nodejs
RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - &&\
echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list &&\
apt-get update &&\
apt-get install -y yarn bzip2 &&\
yarn global add markdown-pdf --prefix /usr/local
# |--------------------------------------------------------------------------
# | Gotenberg
# |--------------------------------------------------------------------------
# |
# | All Gotenberg related stuff.
# |
COPY .ci/gotenberg /usr/bin/gotenberg
RUN ln -s /usr/bin/gotenberg /usr/local/bin/gotenberg
COPY .ci/gotenberg.yml /gotenberg/gotenberg.yml
WORKDIR /gotenberg
EXPOSE 3000
CMD ["gotenberg"]

View File

@@ -1,50 +0,0 @@
FROM golang:1.10-stretch
LABEL authors="Julien Neuhart <j.neuhart@thecodingmachine.com>"
# |--------------------------------------------------------------------------
# | Libraries
# |--------------------------------------------------------------------------
# |
# | Installs all required libraries.
# |
RUN echo "deb http://httpredir.debian.org/debian/ stretch main contrib non-free" > /etc/apt/sources.list &&\
apt-get update &&\
apt-get install -y xvfb wkhtmltopdf pdftk unoconv ttf-mscorefonts-installer &&\
ln -s /usr/bin/xvfb-run /usr/local/bin/xvfb-run &&\
ln -s /usr/bin/wkhtmltopdf /usr/local/bin/wkhtmltopdf &&\
ln -s /usr/bin/pdftk /usr/local/bin/pdftk &&\
ln -s /usr/bin/unoconv /usr/local/bin/unoconv
RUN apt-get install -y curl gnupg &&\
curl -sL https://deb.nodesource.com/setup_8.x | bash - &&\
apt-get update &&\
apt-get install -y nodejs
RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - &&\
echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list &&\
apt-get update &&\
apt-get install -y yarn bzip2 &&\
yarn global add markdown-pdf --prefix /usr/local
# |--------------------------------------------------------------------------
# | Gotenberg
# |--------------------------------------------------------------------------
# |
# | All Gotenberg related stuff.
# |
WORKDIR /go/src/github.com/thecodingmachine/gotenberg
# Installs lint dependencies.
RUN go get -u gopkg.in/alecthomas/gometalinter.v2 &&\
gometalinter.v2 --install
# Copies our Go source.
COPY . .
# Installs project dependencies.
RUN go get -d -v ./...
ENTRYPOINT [".ci/docker-entrypoint.sh"]

66
Gopkg.lock generated
View File

@@ -1,66 +0,0 @@
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
[[projects]]
branch = "master"
name = "github.com/dustin/go-humanize"
packages = ["."]
revision = "02af3965c54e8cacf948b97fef38925c4120652c"
[[projects]]
name = "github.com/gorilla/context"
packages = ["."]
revision = "1ea25387ff6f684839d82767c1733ff4d4d15d0a"
version = "v1.1"
[[projects]]
name = "github.com/gorilla/mux"
packages = ["."]
revision = "53c1911da2b537f792e7cafcb446b05ffe33b996"
version = "v1.6.1"
[[projects]]
branch = "master"
name = "github.com/justinas/alice"
packages = ["."]
revision = "03f45bd4b7dad4734bc4620e46a35789349abb20"
[[projects]]
name = "github.com/satori/go.uuid"
packages = ["."]
revision = "f58768cc1a7a7e77a3bd49e98cdd21419399b6a3"
version = "v1.2.0"
[[projects]]
name = "github.com/sirupsen/logrus"
packages = ["."]
revision = "c155da19408a8799da419ed3eeb0cb5db0ad5dbc"
version = "v1.0.5"
[[projects]]
branch = "master"
name = "golang.org/x/crypto"
packages = ["ssh/terminal"]
revision = "88942b9c40a4c9d203b82b3731787b672d6e809b"
[[projects]]
branch = "master"
name = "golang.org/x/sys"
packages = [
"unix",
"windows"
]
revision = "13d03a9a82fba647c21a0ef8fba44a795d0f0835"
[[projects]]
name = "gopkg.in/yaml.v2"
packages = ["."]
revision = "86f5ed62f8a0ee96bd888d2efdfd6d4fb100a4eb"
version = "v2.2.0"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
inputs-digest = "97e0dbb20631792bc25ad19e6a85a90a9d24bb03f773cd36dee11eebae152091"
solver-name = "gps-cdcl"
solver-version = 1

View File

@@ -1,54 +0,0 @@
# Gopkg.toml example
#
# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md
# for detailed Gopkg.toml documentation.
#
# required = ["github.com/user/thing/cmd/thing"]
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
#
# [[constraint]]
# name = "github.com/user/project"
# version = "1.0.0"
#
# [[constraint]]
# name = "github.com/user/project2"
# branch = "dev"
# source = "github.com/myfork/project2"
#
# [[override]]
# name = "github.com/x/y"
# version = "2.4.0"
#
# [prune]
# non-go = false
# go-tests = true
# unused-packages = true
[prune]
go-tests = true
unused-packages = true
[[constraint]]
name = "github.com/satori/go.uuid"
version = "1.2.0"
[[constraint]]
name = "github.com/gorilla/mux"
version = "1.6.1"
[[constraint]]
branch = "master"
name = "github.com/justinas/alice"
[[constraint]]
name = "gopkg.in/yaml.v2"
version = "2.2.0"
[[constraint]]
name = "github.com/sirupsen/logrus"
version = "1.0.5"
[[constraint]]
branch = "master"
name = "github.com/dustin/go-humanize"

View File

@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2018 TheCodingMachine
Copyright (c) 2019 TheCodingMachine
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

37
Makefile Normal file
View File

@@ -0,0 +1,37 @@
GOLANG_VERSION=1.11.2
VERSION=snapshot
DOCKER_USER=
DOCKER_PASSWORD=
# generate documentation.
doc:
docker build --build-arg GOLANG_VERSION=$(GOLANG_VERSION) -t thecodingmachine/gotenberg:docs -f build/docs/Dockerfile .
docker run --rm -it -v "$(PWD):/docs" thecodingmachine/gotenberg:docs
# gofmt and goimports all go files.
fmt:
go fmt ./...
# run all linters.
lint:
docker build --build-arg GOLANG_VERSION=$(GOLANG_VERSION) -t thecodingmachine/gotenberg:lint -f build/lint/Dockerfile .
docker run --rm -it -v "$(PWD):/lint" thecodingmachine/gotenberg:lint
# run all tests.
tests:
docker build -t thecodingmachine/gotenberg:base -f build/base/Dockerfile .
docker build --build-arg GOLANG_VERSION=$(GOLANG_VERSION) -t thecodingmachine/gotenberg:tests -f build/tests/Dockerfile .
docker run --rm -it -v "$(PWD):/tests" thecodingmachine/gotenberg:tests
# build Docker image.
image:
docker build -t thecodingmachine/gotenberg:base -f build/base/Dockerfile .
docker build --build-arg GOLANG_VERSION=$(GOLANG_VERSION) --build-arg VERSION=$(VERSION) -t thecodingmachine/gotenberg:$(VERSION) -f build/package/Dockerfile .
# start the API using previously built Docker image.
gotenberg:
docker run -it --rm -p "3000:3000" thecodingmachine/gotenberg:$(VERSION)
# publish Gotenberg images according to version.
publish:
./scripts/publish.sh $(GOLANG_VERSION) $(VERSION) $(DOCKER_USER) $(DOCKER_PASSWORD)

205
README.md
View File

@@ -1,202 +1,43 @@
<p align="center">
<img src="https://user-images.githubusercontent.com/8983173/38133342-11df3bd8-340f-11e8-9fe4-50baecdceeca.png" alt="Gotenberg's logo" width="250" height="250" />
<img src="https://user-images.githubusercontent.com/8983173/50009948-84b01e00-ffb8-11e8-850b-fc240382c626.png" alt="Gotenberg logo" width="250" height="250" />
</p>
<h3 align="center">Gotenberg</h3>
<p align="center">A stateless API for converting Markdown files, HTML files and Office documents to PDF</p>
<p align="center">
<a href="https://microbadger.com/images/thecodingmachine/gotenberg:2.0.0">
<img src="https://images.microbadger.com/badges/image/thecodingmachine/gotenberg:2.0.0.svg" alt="MicroBadger layers">
</a>
<a href="https://travis-ci.org/thecodingmachine/gotenberg">
<img src="https://travis-ci.org/thecodingmachine/gotenberg.svg?branch=master" alt="Travis CI">
</a>
<a href="https://godoc.org/github.com/thecodingmachine/gotenberg">
<img src="https://godoc.org/github.com/thecodingmachine/gotenberg?status.svg" alt="GoDoc">
</a>
<a href="https://goreportcard.com/report/thecodingmachine/gotenberg">
<img src="https://goreportcard.com/badge/github.com/thecodingmachine/gotenberg" alt="Go Report Card">
</a>
<a href="https://codecov.io/gh/thecodingmachine/gotenberg/branch/master">
<img src="https://codecov.io/gh/thecodingmachine/gotenberg/branch/master/graph/badge.svg" alt="Codecov">
</a>
</p>
<p align="center">A Docker-powered stateless API for converting HTML, Markdown and Office documents to PDF.</p>
<p align="center"><a href="https://thecodingmachine.github.io/gotenberg">Documentation</a> &#183; <a href="/.github/CONTRIBUTING.md">Contributing</a></p>
---
At TheCodingMachine, we build a lot of web applications (intranets, extranets and so on) which require to generate PDF from
various sources. Each time, we ended up using some well known libraries like **wkhtmltopdf** or **unoconv** and kind of lost time by
reimplementing a solution from a project to another project. Meh.
At TheCodingMachine, we build a lot of web applications (intranets, extranets and so on) which require to generate PDF from various sources. Each time, we ended up using some well known libraries and kind of lost time by reimplementing a solution from a project to another project. Meh.
# Menu
## Features
* [Usage](#usage)
* [Security](#security)
* [Scalability](#scalability)
* [Custom implementation](#custom-implementation)
* [Clients](#clients)
* HTML and Markdown conversions using Google Chrome headless
* Office conversions (.txt, .rtf, .docx, .doc, .odt, .pptx, .ppt, .odp and so on) using [unoconv](https://github.com/dagwieers/unoconv)
* Performance :zap:: Google Chrome and Libreoffice (unoconv) started once in the background thanks to PM2
* Failure prevention :broken_heart:: PM2 automatically restarts previous processes if they fail
* Assets :package:: send your header, footer, images, fonts, stylesheets and so on for converting your HTML and Markdown to beaufitul PDFs!
* Easily interact with the API using our [Go](/pkg) and [PHP](https://github.com/thecodingmachine/gotenberg-php-client) libraries
## Usage
## Quick start
Let's say you're starting the API using this simple command:
```sh
$ docker run --rm -p 3000:3000 thecodingmachine/gotenberg:2.0.0
```
The API is now available on your host under `http://127.0.0.1:3000`.
It accepts `POST` requests with a `multipart/form-data` Content-Type. Your form data should provide one or more files to convert.
The default image accepts the following:
* Markdown files
* HTML files
* Office documents (.docx, .doc, .odt, .pptx, .ppt, .odp and so on)
* PDF files
**Heads up:** the API relies on the file extension to determine which library to use for conversion.
There are two use cases:
* If you send one file, it will convert it and return the resulting PDF
* If many files, it will convert them to PDF, merge the resulting PDFs into a single PDF and return it
### Examples:
* One file
```sh
$ curl --request POST \
--url http://127.0.0.1:3000 \
--header 'Content-Type: multipart/form-data' \
--form files=@file.docx \
> result.pdf
```
* Many files
```sh
$ curl --request POST \
--url http://127.0.0.1:3000 \
--header 'Content-Type: multipart/form-data' \
--form files=@file.md \
--form files=@file.html \
--form files=@file.pdf \
--form files=@file.docx \
> result.pdf
```
## Security
The API does not provide any authentication mechanisms. Make sure to not put it on a public facing port and your client(s) should always
controls what is sent to the API.
## Scalability
Some libraries like **unoconv** cannot perform concurrent conversions. That's why the API does only one conversion at a time.
If your API is under heavy load, a request will take time to be processed.
Fortunately, you may pass through this limitation by scaling the API.
In the following example, I'll demonstrate how to do some vertical scaling (= on the same machine) with Docker Compose, but of course horizontal scaling works too!
```yaml
version: '3'
services:
# your others services
gotenberg:
image: thecodingmachine/gotenberg:2.0.0
```
You may now launch your services using:
Open a terminal and run the following command:
```bash
docker-compose up --scale gotenberg=your_number_of_instances
$ docker run --rm -p 3000:3000 thecodingmachine/gotenberg:4
```
When requesting the Gotenberg service with your client(s), Docker will automatically redirect a request to a Gotenberg container
according to the round-robin strategy.
The API is now available on your host at `http://localhost:3000`.
## Custom implementation
Head to the [documentation](https://thecodingmachine.github.io/gotenberg)
to learn how to interact with it!
The API relies on a simple YAML configuration file called `gotenberg.yml`. It allows you to tweak some values and even provides you
a way to change the commands called for each kind of conversion.
## Badges
Below the default configuration file:
```yaml
# The port the application will listen to.
port: 3000
logs:
# Accepted values, in order of severity: DEBUG, INFO, WARN, ERROR, FATAL, PANIC.
# Messages at and above the selected level will be logged.
level: "INFO"
# Accepted values: text, json.
# When a TTY is not attached, the output will be in the defined format.
formatter: "text"
# You don't like a library which is used for a conversion? You want to handle a new file type?
# You may provide here your own implementation!
commands:
# Some libraries like unoconv cannot perform concurrent conversions. That's why the API does only one conversion at a time.
# If your current implementation uses libraries which are able to perform concurrent conversions, you may
# change this value to false.
lock: true
# Unlike others commands' templates, you have access to FilesPaths instead of FilePath: it gathers all PDF files which should be merged.
merge:
template: "pdftk {{ range $filePath := .FilesPaths }} {{ $filePath }} {{ end }} cat output {{ .ResultFilePath }}"
interpreter: "/bin/sh -c"
timeout: 30
# You may add more commands (or less, or even none).
conversions:
# The command template: you have access to FilePath and ResultFilePath variables.
- template: "markdown-pdf {{ .FilePath }} -o {{ .ResultFilePath }}"
# The binary which will call the command.
interpreter: "/bin/sh -c"
# Duration in seconds after which the command will be killed if it has not finished.
timeout: 30
# Files with the following extensions will be converted by the current command.
extensions:
- ".md"
- template: "xvfb-run -e /dev/stdout wkhtmltopdf {{ .FilePath }} {{ .ResultFilePath }}"
interpreter: "/bin/sh -c"
timeout: 30
extensions:
- ".html"
- ".htm"
- template: "unoconv --format pdf --output \"{{ .ResultFilePath }}\" \"{{ .FilePath }}\""
interpreter: "/bin/sh -c"
timeout: 30
extensions:
- ".doc"
- ".docx"
- ".odt"
- ".xls"
- ".xlsx"
- ".ods"
- ".ppt"
- ".pptx"
- ".odp"
```
We provide binaries for a wide range of OS and architecture in the [releases page](../../releases),
so feel free to create your own Docker image for your implementation of the Gotenberg API :metal:
## Clients
* https://github.com/thecodingmachine/gotenberg-php-client (PHP client)
* Add your own client by submitting a [pull request](../../pulls)!
[![Docker image layers](https://images.microbadger.com/badges/image/thecodingmachine/gotenberg:4.svg)](https://microbadger.com/images/thecodingmachine/gotenberg:4)
[![Travis CI](https://travis-ci.org/thecodingmachine/gotenberg.svg?branch=master)](https://travis-ci.org/thecodingmachine/gotenberg)
[![GoDoc](https://godoc.org/github.com/thecodingmachine/gotenberg?status.svg)](https://godoc.org/github.com/thecodingmachine/gotenberg)
[![Go Report Card](https://goreportcard.com/badge/github.com/thecodingmachine/gotenberg)](https://goreportcard.com/report/thecodingmachine/gotenberg)
---
Would you like to update this documentation ? Feel free to open an [issue](../../issues).
Psst: TheCodingMachine is always looking for [talented coders](https://coders.thecodingmachine.com).

View File

@@ -1,39 +0,0 @@
port: 3000
logs:
level: "DEBUG"
formatter: {
value: [
...
}
commands:
lock: true
merge:
template: "pdftk {{ range $filePath := .FilesPaths }} {{ $filePath }} {{ end }} cat output {{ .ResultFilePath }}"
interpreter: "/bin/sh -c"
timeout: 30
conversions:
- template: "markdown-pdf {{ .FilePath }} -o {{ .ResultFilePath }}"
interpreter: "/bin/sh -c"
timeout: 30
extensions:
- ".md"
- template: "xvfb-run -e /dev/stdout wkhtmltopdf {{ .FilePath }} {{ .ResultFilePath }}"
interpreter: "/bin/sh -c"
timeout: 30
extensions:
- ".html"
- ".htm"
- template: "unoconv --format pdf --output \"{{ .ResultFilePath }}\" \"{{ .FilePath }}\""
interpreter: "/bin/sh -c"
timeout: 30
extensions:
- ".doc"
- ".docx"
- ".odt"
- ".xls"
- ".xlsx"
- ".ods"
- ".ppt"
- ".pptx"
- ".odp"

View File

@@ -1,40 +0,0 @@
port: 3000
logs:
level: "DEBUG"
formatter: "text"
commands:
lock: true
merge:
template: "pdftk {{ range $filePath := .FilesPaths }} {{ $filePath }} {{ end }} cat output {{ .ResultFilePath }}"
interpreter: "/bin/sh -c"
timeout: 30
conversions:
- template: "markdown-pdf {{ .FilePath }} -o {{ .ResultFilePath }}"
interpreter: "/bin/sh -c"
timeout: 30
extensions:
- ".md"
- template: "markdown-pdf {{ .FilePath }} -o {{ .ResultFilePath }}"
interpreter: "/bin/sh -c"
timeout: 30
extensions:
- ".md"
- template: "xvfb-run -e /dev/stdout wkhtmltopdf {{ .FilePath }} {{ .ResultFilePath }}"
interpreter: "/bin/sh -c"
timeout: 30
extensions:
- ".html"
- ".htm"
- template: "unoconv --format pdf --output \"{{ .ResultFilePath }}\" \"{{ .FilePath }}\""
interpreter: "/bin/sh -c"
timeout: 30
extensions:
- ".doc"
- ".docx"
- ".odt"
- ".xls"
- ".xlsx"
- ".ods"
- ".ppt"
- ".pptx"
- ".odp"

View File

@@ -1,35 +0,0 @@
port: 3000
logs:
level: "DEBUG"
formatter: "text"
commands:
lock: true
merge:
template: "pdftk {{ range $filePath := .FilesPaths }} {{ $filePath }} {{ end }} cat output {{ .ResultFilePath }}"
interpreter: "/bin/sh -c"
timeout: 30
conversions:
- template: "markdown-pdf {{ .FilePath }} -o {{ .ResultFilePath }}"
interpreter: "/bin/sh -c"
timeout: 30
extensions:
- ".md"
- template: "xvfb-run -e /dev/stdout wkhtmltopdf {{ .FilePath }} {{ .ResultFilePath }}"
interpreter: "/bin/sh -c"
timeout: 30
extensions:
- ".html"
- ".htm"
- template: "unoconv --format pdf --output \"{{ .ResultFilePath }}\" \"{{ .FilePath }}\""
interpreter: "/bin/sh -c"
timeout: 30
extensions:
- ".doc"
- ".docx"
- ".odt"
- ".xls"
- ".xlsx"
- ".ods"
- ".ppt"
- ".pptx"
- ".odp"

View File

@@ -1,35 +0,0 @@
port: 3000
logs:
level: "DEBUG"
formatter: "text"
commands:
lock: true
merge:
template: "pdftk {{ range $filePath := .FilesPaths }} {{ $filePath }} {{ end }} cat output {{ .ResultFilePath }}"
interpreter: "/bin/sh -c"
timeout: 0
conversions:
- template: "markdown-pdf {{ .FilePath }} -o {{ .ResultFilePath }}"
interpreter: "/bin/sh -c"
timeout: 30
extensions:
- ".md"
- template: "xvfb-run -e /dev/stdout wkhtmltopdf {{ .FilePath }} {{ .ResultFilePath }}"
interpreter: "/bin/sh -c"
timeout: 30
extensions:
- ".html"
- ".htm"
- template: "unoconv --format pdf --output \"{{ .ResultFilePath }}\" \"{{ .FilePath }}\""
interpreter: "/bin/sh -c"
timeout: 30
extensions:
- ".doc"
- ".docx"
- ".odt"
- ".xls"
- ".xlsx"
- ".ods"
- ".ppt"
- ".pptx"
- ".odp"

View File

@@ -1,35 +0,0 @@
port: 3000
logs:
level: "DEBUG"
formatter: "text"
commands:
lock: false
merge:
template: "pdftk {{ range $filePath := .FilesPaths }} {{ $filePath }} {{ end }} cat output {{ .ResultFilePath }}"
interpreter: "/bin/sh -c"
timeout: 30
conversions:
- template: "markdown-pdf {{ .FilePath }} -o {{ .ResultFilePath }}"
interpreter: "/bin/sh -c"
timeout: 30
extensions:
- ".md"
- template: "xvfb-run -e /dev/stdout wkhtmltopdf {{ .FilePath }} {{ .ResultFilePath }}"
interpreter: "/bin/sh -c"
timeout: 30
extensions:
- ".html"
- ".htm"
- template: "unoconv --format pdf --output \"{{ .ResultFilePath }}\" \"{{ .FilePath }}\""
interpreter: "/bin/sh -c"
timeout: 30
extensions:
- ".doc"
- ".docx"
- ".odt"
- ".xls"
- ".xlsx"
- ".ods"
- ".ppt"
- ".pptx"
- ".odp"

View File

@@ -1,35 +0,0 @@
port: 3000
logs:
level: "DEBUG"
formatter: "text"
commands:
lock: true
merge:
template: "pdftk {{ range $filePath := .FilesPaths }} {{ $filePath }} {{ end }} cat output {{ .ResultFilePath }}"
interpreter: "/bin/sh -c"
timeout: 0
conversions:
- template: "markdown-pdf {{ .FilePath }} -o {{ .ResultFilePath }}"
interpreter: "/bin/sh -c"
timeout: 0
extensions:
- ".md"
- template: "xvfb-run -e /dev/stdout wkhtmltopdf {{ .FilePath }} {{ .ResultFilePath }}"
interpreter: "/bin/sh -c"
timeout: 0
extensions:
- ".html"
- ".htm"
- template: "unoconv --format pdf --output \"{{ .ResultFilePath }}\" \"{{ .FilePath }}\""
interpreter: "/bin/sh -c"
timeout: 0
extensions:
- ".doc"
- ".docx"
- ".odt"
- ".xls"
- ".xlsx"
- ".ods"
- ".ppt"
- ".pptx"
- ".odp"

View File

@@ -1,35 +0,0 @@
port: 3000
logs:
level: "DEBUG"
formatter: "text"
commands:
lock: true
merge:
template: "pdftk {{ range $filePath := .FilesPaths }} {{ $filePath }} {{ end }} cat output {{ .ResultFilePath }}"
interpreter: "/bin/sh -c"
timeout: 30
conversions:
- template: "markdown-pdf {{ FilePath }} -o {{ .ResultFilePath }}"
interpreter: "/bin/sh -c"
timeout: 30
extensions:
- ".md"
- template: "xvfb-run -e /dev/stdout wkhtmltopdf {{ .FilePath }} {{ .ResultFilePath }}"
interpreter: "/bin/sh -c"
timeout: 30
extensions:
- ".html"
- ".htm"
- template: "unoconv --format pdf --output \"{{ .ResultFilePath }}\" \"{{ .FilePath }}\""
interpreter: "/bin/sh -c"
timeout: 30
extensions:
- ".doc"
- ".docx"
- ".odt"
- ".xls"
- ".xlsx"
- ".ods"
- ".ppt"
- ".pptx"
- ".odp"

View File

@@ -1,35 +0,0 @@
port: 3000
logs:
level: "DEBUG"
formatter: "DEBUG"
commands:
lock: true
merge:
template: "pdftk {{ range $filePath := .FilesPaths }} {{ $filePath }} {{ end }} cat output {{ .ResultFilePath }}"
interpreter: "/bin/sh -c"
timeout: 30
conversions:
- template: "markdown-pdf {{ .FilePath }} -o {{ .ResultFilePath }}"
interpreter: "/bin/sh -c"
timeout: 30
extensions:
- ".md"
- template: "xvfb-run -e /dev/stdout wkhtmltopdf {{ .FilePath }} {{ .ResultFilePath }}"
interpreter: "/bin/sh -c"
timeout: 30
extensions:
- ".html"
- ".htm"
- template: "unoconv --format pdf --output \"{{ .ResultFilePath }}\" \"{{ .FilePath }}\""
interpreter: "/bin/sh -c"
timeout: 30
extensions:
- ".doc"
- ".docx"
- ".odt"
- ".xls"
- ".xlsx"
- ".ods"
- ".ppt"
- ".pptx"
- ".odp"

View File

@@ -1,35 +0,0 @@
port: 3000
logs:
level: "text"
formatter: "text"
commands:
lock: true
merge:
template: "pdftk {{ range $filePath := .FilesPaths }} {{ $filePath }} {{ end }} cat output {{ .ResultFilePath }}"
interpreter: "/bin/sh -c"
timeout: 30
conversions:
- template: "markdown-pdf {{ .FilePath }} -o {{ .ResultFilePath }}"
interpreter: "/bin/sh -c"
timeout: 30
extensions:
- ".md"
- template: "xvfb-run -e /dev/stdout wkhtmltopdf {{ .FilePath }} {{ .ResultFilePath }}"
interpreter: "/bin/sh -c"
timeout: 30
extensions:
- ".html"
- ".htm"
- template: "unoconv --format pdf --output \"{{ .ResultFilePath }}\" \"{{ .FilePath }}\""
interpreter: "/bin/sh -c"
timeout: 30
extensions:
- ".doc"
- ".docx"
- ".odt"
- ".xls"
- ".xlsx"
- ".ods"
- ".ppt"
- ".pptx"
- ".odp"

View File

@@ -1,35 +0,0 @@
port: 3000
logs:
level: "DEBUG"
formatter: "text"
commands:
lock: true
merge:
template: "pdftk {{ range $filePath := FilesPaths }} {{ $filePath }} {{ end }} cat output {{ .ResultFilePath }}"
interpreter: "/bin/sh -c"
timeout: 30
conversions:
- template: "markdown-pdf {{ .FilePath }} -o {{ .ResultFilePath }}"
interpreter: "/bin/sh -c"
timeout: 30
extensions:
- ".md"
- template: "xvfb-run -e /dev/stdout wkhtmltopdf {{ .FilePath }} {{ .ResultFilePath }}"
interpreter: "/bin/sh -c"
timeout: 30
extensions:
- ".html"
- ".htm"
- template: "unoconv --format pdf --output \"{{ .ResultFilePath }}\" \"{{ .FilePath }}\""
interpreter: "/bin/sh -c"
timeout: 30
extensions:
- ".doc"
- ".docx"
- ".odt"
- ".xls"
- ".xlsx"
- ".ods"
- ".ppt"
- ".pptx"
- ".odp"

Binary file not shown.

View File

@@ -1,16 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Some title</title>
<meta name="author" content="Julien Neuhart">
<meta name="description" content="Simple HTML page">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<p>Hi! I have been converted from an HTML page!</p>
</body>
</html>

View File

@@ -1 +0,0 @@
# Hi, I've been converted from a markdown file!

Binary file not shown.

View File

@@ -1,213 +0,0 @@
/*
Package config contains all the logic allowing us to instantiate the application's configuration.
The application's configuration is loaded from a YAML file named gotenberg.yml.
It should be located where the user starts the application from the CLI.
*/
package config
import (
"fmt"
"strings"
"text/template"
"github.com/sirupsen/logrus"
)
type (
// appConfig gathers all configuration data.
appConfig struct {
port string
logsLevel logrus.Level
logsFormatter logrus.Formatter
lock bool
// commands associates a file extension with a Command instance.
// Particular case: ".pdf" extension is used for the merge command.
commands map[string]*Command
}
// Command gathers information on how to launch an external binary used for converting
// a file to PDF.
Command struct {
// Template is the data-driven template of the command.
Template *template.Template
// The binary which will call the command.
Interpreter []string
// Timeout is the duration in seconds after which the command's process will be killed
// if it does not finish before.
Timeout int
}
)
// our default instance of appConfig.
var config = &appConfig{}
// Reset reinitializes our configuration.
func Reset() {
config = &appConfig{}
}
// WithPort sets the port which will be used by the application.
func WithPort(port string) {
config.port = port
}
// GetPort returns the current port.
func GetPort() string {
return config.port
}
// levels associates logging levels as defined in the configuration file gotenberg.yml
// with its counterpart from the logrus library.
var levels = map[string]logrus.Level{
"DEBUG": logrus.DebugLevel,
"INFO": logrus.InfoLevel,
"WARN": logrus.WarnLevel,
"ERROR": logrus.ErrorLevel,
"FATAL": logrus.FatalLevel,
"PANIC": logrus.PanicLevel,
}
type wrongLogsLevelError struct{}
const wrongLogsLevelErrorMessage = "accepted values for logs level: DEBUG, INFO, WARN, ERROR, FATAL, PANIC"
func (e *wrongLogsLevelError) Error() string {
return wrongLogsLevelErrorMessage
}
// WithLogsLevel sets the logs level.
// If the given string does not match with a logrus level,
// throws an error.
func WithLogsLevel(level string) error {
l, ok := levels[level]
if !ok {
return &wrongLogsLevelError{}
}
config.logsLevel = l
return nil
}
// GetLogsLevel returns the current logs level.
func GetLogsLevel() logrus.Level {
return config.logsLevel
}
// formatters associates logging formatter as defined in the configuration file gotenberg.yml
// with its counterpart from the logrus library.
var formatters = map[string]logrus.Formatter{
"text": &logrus.TextFormatter{},
"json": &logrus.JSONFormatter{},
}
type wrongLogsFormatterError struct{}
const wrongLogsFormatterErrorMessage = "accepted value for logs formatter: text, json"
func (e *wrongLogsFormatterError) Error() string {
return wrongLogsFormatterErrorMessage
}
// WithLogsFormatter sets the logs formatter.
// If the given string does not match with a logrus formatter,
// throws an error.
func WithLogsFormatter(formatter string) error {
f, ok := formatters[formatter]
if !ok {
return &wrongLogsFormatterError{}
}
config.logsFormatter = f
return nil
}
// GetLogsFormatter returns the current logs formatter.
func GetLogsFormatter() logrus.Formatter {
return config.logsFormatter
}
// WithLock sets the lock strategy.
func WithLock(lock bool) {
config.lock = lock
}
// HasLock returns the current lock strategy.
func HasLock() bool {
return config.lock
}
type interpreterEmptyError struct {
command string
}
const interpreterEmptyErrorMessage = "the interepreter for command %s should not be empty"
func (e *interpreterEmptyError) Error() string {
return fmt.Sprintf(interpreterEmptyErrorMessage, e.command)
}
// NewCommand instantiates a Command. If the given command string
// is not a valid template, throws an error.
func NewCommand(command string, interpreter string, timeout int) (*Command, error) {
t, err := template.New(command).Parse(command)
if err != nil {
return nil, err
}
if interpreter == "" {
return nil, &interpreterEmptyError{command}
}
return &Command{t, strings.Fields(interpreter), timeout}, nil
}
type fileExtensionAlreadyUsedError struct {
extension string
command *Command
existingCommand *Command
}
const fileExtensionAlreadyUsedErrorMessage = "file extension %s from command %s is already used by command %s"
func (e *fileExtensionAlreadyUsedError) Error() string {
return fmt.Sprintf(fileExtensionAlreadyUsedErrorMessage, e.extension, e.command.Template.Name(), e.existingCommand.Template.Name())
}
// WithCommand adds a Command instance and associates it with the given
// file extension. If the file extension is already used by another Command
// instance, throws an error.
func WithCommand(extension string, command *Command) error {
if config.commands == nil {
config.commands = make(map[string]*Command)
}
existingCommand, ok := config.commands[extension]
if ok {
return &fileExtensionAlreadyUsedError{extension, command, existingCommand}
}
config.commands[extension] = command
return nil
}
type noCommandFoundForFileExtensionError struct {
extension string
}
const noCommandFoundForFileExtensionErrorMessage = "no command found for file extension %s"
func (e *noCommandFoundForFileExtensionError) Error() string {
return fmt.Sprintf(noCommandFoundForFileExtensionErrorMessage, e.extension)
}
// GetCommand returns the Command instance associated with the given
// file extension. If no Command instance found, throws an error.
func GetCommand(extension string) (*Command, error) {
c, ok := config.commands[extension]
if !ok {
return nil, &noCommandFoundForFileExtensionError{extension}
}
return c, nil
}

View File

@@ -1,201 +0,0 @@
package config
import (
"fmt"
"testing"
"github.com/sirupsen/logrus"
)
func TestReset(t *testing.T) {
c := &appConfig{}
config.port = "3000"
Reset()
if c.port != config.port {
t.Error("Configuration should have been reset")
}
}
func TestWithPort(t *testing.T) {
port := "3000"
WithPort(port)
if config.port != port {
t.Errorf("Configuration populated with a wrong port: got '%s' want '%s'", config.port, port)
}
}
func TestGetPort(t *testing.T) {
port := "3000"
config.port = port
if GetPort() != port {
t.Errorf("Configuration returned a wrong port: got '%s' want '%s'", GetPort(), port)
}
}
func TestWrongLogsLevelError(t *testing.T) {
err := &wrongLogsLevelError{}
if err.Error() != wrongLogsLevelErrorMessage {
t.Errorf("Error returned a wrong message: got '%s' want '%s'", err.Error(), wrongLogsLevelErrorMessage)
}
}
func TestWithLogsLevel(t *testing.T) {
var lvl string
// case 1: uses a wrong logs level.
lvl = "text"
if err := WithLogsLevel(lvl); err == nil {
t.Errorf("Configuration should not have been populated by using '%s' as logs level", lvl)
}
// case 2: uses a correct logs level.
lvl = "DEBUG"
if err := WithLogsLevel(lvl); err != nil {
t.Errorf("Configuration should have been populated by using '%s' as logs level", lvl)
}
}
func TestGetLogsLevel(t *testing.T) {
lvl := logrus.DebugLevel
config.logsLevel = lvl
if GetLogsLevel() != lvl {
t.Errorf("Configuration returned a wrong logs level: got '%s' want '%s'", GetLogsLevel(), lvl)
}
}
func TestWrongLogsFormatterError(t *testing.T) {
err := &wrongLogsFormatterError{}
if err.Error() != wrongLogsFormatterErrorMessage {
t.Errorf("Error returned a wrong message: got '%s' want '%s'", err.Error(), wrongLogsFormatterErrorMessage)
}
}
func TestWithLogsFormatter(t *testing.T) {
var formatter string
// case 1: uses a wrong logs formatter.
formatter = "DEBUG"
if err := WithLogsFormatter(formatter); err == nil {
t.Errorf("Configuration should not have been populated by using '%s' as logs formatter", formatter)
}
// case 2: uses a correct logs formatter.
formatter = "text"
if err := WithLogsFormatter(formatter); err != nil {
t.Errorf("Configuration should have been populated by using '%s' as logs formatter", formatter)
}
}
func TestGetLogsFormatter(t *testing.T) {
formatter := &logrus.TextFormatter{}
config.logsFormatter = formatter
if GetLogsFormatter() != formatter {
t.Errorf("Configuration returned a wrong logs formatter: got '%v' want '%v'", GetLogsFormatter(), formatter)
}
}
func TestWithLock(t *testing.T) {
lock := true
WithLock(lock)
if config.lock != lock {
t.Errorf("Configuration populated with a wrong lock strategy: got '%t' want '%t'", config.lock, lock)
}
}
func TestHasLock(t *testing.T) {
lock := true
config.lock = true
if HasLock() != lock {
t.Errorf("Configuration returned a wrong port: got '%t' want '%t'", HasLock(), lock)
}
}
func TestInterpreterEmptyError(t *testing.T) {
err := &interpreterEmptyError{"echo hello world"}
expected := fmt.Sprintf(interpreterEmptyErrorMessage, err.command)
if err.Error() != expected {
t.Errorf("Error returned a wrong message: got '%s' want '%s'", err.Error(), expected)
}
}
func TestNewCommand(t *testing.T) {
var cmd string
// case 1: uses a wrong command template.
cmd = "pdftk {{ range $filePath := FilesPaths }} {{ $filePath }} {{ end }} cat output {{ .ResultFilePath }}"
if _, err := NewCommand(cmd, "/bin/sh -c", 0); err == nil {
t.Errorf("Command should not have been instantiated by using '%s' as command template", cmd)
}
// case 2: uses a correct command template.
cmd = "pdftk {{ range $filePath := .FilesPaths }} {{ $filePath }} {{ end }} cat output {{ .ResultFilePath }}"
if _, err := NewCommand(cmd, "/bin/sh -c", 0); err != nil {
t.Errorf("Command should have been instantiated by using '%s' as command template", cmd)
}
// case 3: uses an empty interpreter.
if _, err := NewCommand(cmd, "", 0); err == nil {
t.Error("Command should not have been instantiated by using an empty interpreter")
}
}
func TestFileExtensionAlreadyUsedError(t *testing.T) {
ext := ".pdf"
cmd1, _ := NewCommand("echo", "/bin/sh -c", 0)
cmd2, _ := NewCommand("echo", "/bin/sh -c", 0)
err := &fileExtensionAlreadyUsedError{ext, cmd1, cmd2}
expected := fmt.Sprintf(fileExtensionAlreadyUsedErrorMessage, err.extension, err.command.Template.Name(), err.existingCommand.Template.Name())
if err.Error() != expected {
t.Errorf("Error returned a wrong message: got '%s' want '%s'", err.Error(), expected)
}
}
func TestWithCommand(t *testing.T) {
ext := ".pdf"
cmd, _ := NewCommand("echo", "/bin/sh -c", 0)
// case 1: uses a command with a file extension not already referenced.
if err := WithCommand(ext, cmd); err != nil {
t.Errorf("Configuration should have been populated by using a command with the file extension '%s'", ext)
}
// case 2: uses a command with a file extension already referenced.
if err := WithCommand(ext, cmd); err == nil {
t.Errorf("Configuration should not have been populated by using a command with the file extension '%s'", ext)
}
}
func TestNoCommandFoundForFileExtensionError(t *testing.T) {
err := &noCommandFoundForFileExtensionError{".pdf"}
expected := fmt.Sprintf(noCommandFoundForFileExtensionErrorMessage, err.extension)
if err.Error() != expected {
t.Errorf("Error returned a wrong message: got '%s' want '%s'", err.Error(), expected)
}
}
func TestGetCommand(t *testing.T) {
Reset()
ext := ".pdf"
cmd, _ := NewCommand("echo", "/bin/sh -c", 0)
WithCommand(ext, cmd)
// case 1: uses a file extension which has a command associated.
if _, err := GetCommand(ext); err != nil {
t.Errorf("Configuration should have been able to return a command by using the file extension '%s'", ext)
}
// case 2: uses a file extension which has no command associated.
ext = ".docx"
if _, err := GetCommand(ext); err == nil {
t.Errorf("Configuration should not have been able to return a command by using the file extension '%s'", ext)
}
}

View File

@@ -1,99 +0,0 @@
package config
import (
"io/ioutil"
"gopkg.in/yaml.v2"
)
// ParseFile instantiates the application's configuration using the given YAML file.
func ParseFile(configurationFilePath string) error {
fileConfig, err := readFile(configurationFilePath)
if err != nil {
return err
}
WithPort(fileConfig.Port)
if err := WithLogsLevel(fileConfig.Logs.Level); err != nil {
return err
}
if err := WithLogsFormatter(fileConfig.Logs.Formatter); err != nil {
return err
}
WithLock(fileConfig.Commands.Lock)
// handles merge command first...
cmd, err := NewCommand(fileConfig.Commands.Merge.Template, fileConfig.Commands.Merge.Interpreter, fileConfig.Commands.Merge.Timeout)
if err != nil {
return err
}
WithCommand(".pdf", cmd)
// ...then conversion commands!
for _, command := range fileConfig.Commands.Conversions {
cmd, err := NewCommand(command.Template, command.Interpreter, command.Timeout)
if err != nil {
return err
}
for _, ext := range command.Extensions {
if err := WithCommand(ext, cmd); err != nil {
return err
}
}
}
return nil
}
type (
// fileConfig gathers all data coming from the configuration file gotenberg.yml.
fileConfig struct {
Port string `yaml:"port"`
Logs struct {
Level string `yaml:"level"`
Formatter string `yaml:"formatter"`
} `yaml:"logs"`
Commands struct {
Lock bool `yaml:"lock"`
Merge *mergeCommand `yaml:"merge"`
Conversions []*conversionCommand `yaml:"conversions,omitempty"`
} `yaml:"commands"`
}
// mergeCommand gathers all data regarding the... merge command.
mergeCommand struct {
Template string `yaml:"template"`
Interpreter string `yaml:"interpreter"`
Timeout int `yaml:"timeout"`
}
// conversionCommand gathers all data regarding a conversion command.
conversionCommand struct {
Template string `yaml:"template"`
Interpreter string `yaml:"interpreter"`
Timeout int `yaml:"timeout"`
Extensions []string `yaml:"extensions"`
}
)
// readFile instantiates a fileConfig instance by reading
// the given YAML file.
func readFile(configurationFilePath string) (*fileConfig, error) {
c := &fileConfig{}
data, err := ioutil.ReadFile(configurationFilePath)
if err != nil {
return nil, err
}
if err := yaml.Unmarshal(data, &c); err != nil {
return nil, err
}
return c, nil
}

View File

@@ -1,62 +0,0 @@
package config
import (
"path/filepath"
"testing"
)
func load(configurationFilePath string) error {
Reset()
return ParseFile(configurationFilePath)
}
func TestParseFile(t *testing.T) {
var path string
// case 1: uses an empty configuration file path.
if err := load(""); err == nil {
t.Error("Configuration should not have been populated by using an empty configuration file path")
}
// case 2: uses a broken configuration file.
path, _ = filepath.Abs("../../_tests/configurations/broken-gotenberg.yml")
if err := load(path); err == nil {
t.Errorf("Configuration should not have been populated with '%s'", path)
}
// case 3: uses a configuration file with a wrong logging level.
path, _ = filepath.Abs("../../_tests/configurations/wrong-logging-level-gotenberg.yml")
if err := load(path); err == nil {
t.Errorf("Configuration should not have been populated with '%s'", path)
}
// case 4: uses a configuration file with a wrong logging formatter.
path, _ = filepath.Abs("../../_tests/configurations/wrong-logging-formatter-gotenberg.yml")
if err := load(path); err == nil {
t.Errorf("Configuration should not have been populated with '%s'", path)
}
// case 5: uses a configuration file with a wrong merge command template.
path, _ = filepath.Abs("../../_tests/configurations/wrong-merge-command-template-gotenberg.yml")
if err := load(path); err == nil {
t.Errorf("Configuration should not have been populated with '%s'", path)
}
// case 6: uses a configuration file with a wrong command template.
path, _ = filepath.Abs("../../_tests/configurations/wrong-command-template-gotenberg.yml")
if err := load(path); err == nil {
t.Errorf("Configuration should not have been populated with '%s'", path)
}
// case 7: uses a configuration file with a duplicate command.
path, _ = filepath.Abs("../../_tests/configurations/duplicate-command-gotenberg.yml")
if err := load(path); err == nil {
t.Errorf("Configuration should not have been populated with '%s'", path)
}
// case 8: uses a correct configuration file.
path, _ = filepath.Abs("../../_tests/configurations/gotenberg.yml")
if err := load(path); err != nil {
t.Errorf("Configuration should have been populated with '%s'", path)
}
}

View File

@@ -1,105 +0,0 @@
// Package context provides functions for setting and getting values from
// a request's context.
package context
import (
"context"
"net/http"
"github.com/thecodingmachine/gotenberg/app/converter"
)
type key uint32
const (
requestIDKey key = iota
converterKey
resultFilePathKey
)
// WithRequestID populates a request's context with the given request ID
// and returns the updated request.
func WithRequestID(r *http.Request, requestID string) *http.Request {
ctx := r.Context()
ctx = context.WithValue(ctx, requestIDKey, requestID)
r = r.WithContext(ctx)
return r
}
type requestIDNotFoundError struct{}
const requestIDNotFoundErrorMessage = "the request ID was not found in request context"
func (e *requestIDNotFoundError) Error() string {
return requestIDNotFoundErrorMessage
}
// GetRequestID returns the request ID if found in
// the request's context. Otherwise throws an error.
func GetRequestID(r *http.Request) (string, error) {
ID, ok := r.Context().Value(requestIDKey).(string)
if !ok {
return "", &requestIDNotFoundError{}
}
return ID, nil
}
// WithConverter populates a request's context with the given converter
// and returns the updated request.
func WithConverter(r *http.Request, converter *converter.Converter) *http.Request {
ctx := r.Context()
ctx = context.WithValue(ctx, converterKey, converter)
r = r.WithContext(ctx)
return r
}
type converterNotFoundError struct{}
const converterNotFoundErrorMessage = "the converter was not found in request context"
func (e *converterNotFoundError) Error() string {
return converterNotFoundErrorMessage
}
// GetConverter returns the converter if found in
// the request's context. Otherwise throws an error.
func GetConverter(r *http.Request) (*converter.Converter, error) {
c, ok := r.Context().Value(converterKey).(*converter.Converter)
if !ok {
return nil, &converterNotFoundError{}
}
return c, nil
}
// WithResultFilePath populates a request's context with the given result file path
// and returns the updated request.
func WithResultFilePath(r *http.Request, resultFilePath string) *http.Request {
ctx := r.Context()
ctx = context.WithValue(ctx, resultFilePathKey, resultFilePath)
r = r.WithContext(ctx)
return r
}
type resultFilePathNotFoundError struct{}
const resultFilePathNotFoundErrorMessage = "the result file path was not found in request context"
func (e *resultFilePathNotFoundError) Error() string {
return resultFilePathNotFoundErrorMessage
}
// GetResultFilePath returns the result file path if found in
// the request's context. Otherwise throws an error.
func GetResultFilePath(r *http.Request) (string, error) {
path, ok := r.Context().Value(resultFilePathKey).(string)
if !ok {
return "", &resultFilePathNotFoundError{}
}
return path, nil
}

View File

@@ -1,100 +0,0 @@
package context
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/thecodingmachine/gotenberg/app/converter"
"github.com/satori/go.uuid"
)
func TestWithRequestID(t *testing.T) {
requestID := uuid.NewV4().String()
req := WithRequestID(httptest.NewRequest(http.MethodPost, "/", nil), requestID)
if ID, _ := req.Context().Value(requestIDKey).(string); ID != requestID {
t.Errorf("Context returned a wrong converter: got '%s' want '%s'", ID, requestID)
}
}
func TestRequestIDNotFoundError(t *testing.T) {
err := &requestIDNotFoundError{}
if err.Error() != requestIDNotFoundErrorMessage {
t.Errorf("Error returned a wrong message: got '%s' want '%s'", err.Error(), requestIDNotFoundErrorMessage)
}
}
func TestGetRequestID(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "/", nil)
// case 1: uses a request without a request ID entry in its context.
if _, err := GetRequestID(req); err == nil {
t.Error("Context should not have a request ID entry")
}
// case 2: uses a request with a request ID entry in its context.
req = WithRequestID(req, uuid.NewV4().String())
if _, err := GetRequestID(req); err != nil {
t.Error("Context should have a request ID entry")
}
}
func TestWithConverter(t *testing.T) {
req := WithConverter(httptest.NewRequest(http.MethodPost, "/", nil), &converter.Converter{})
if c, _ := req.Context().Value(converterKey).(*converter.Converter); c == nil {
t.Errorf("Context returned a wrong converter: got '%v' want not nil", c)
}
}
func TestConverterNotFoundError(t *testing.T) {
err := &converterNotFoundError{}
if err.Error() != converterNotFoundErrorMessage {
t.Errorf("Error returned a wrong message: got '%s' want '%s'", err.Error(), converterNotFoundErrorMessage)
}
}
func TestGetConverter(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "/", nil)
// case 1: uses a request without a converter entry in its context.
if _, err := GetConverter(req); err == nil {
t.Error("Context should not have a converter entry")
}
// case 2: uses a request with a converter entry in its context.
req = WithConverter(req, &converter.Converter{})
if _, err := GetConverter(req); err != nil {
t.Error("Context should have a converter entry")
}
}
func TestWithResultFilePath(t *testing.T) {
filePath := "file.pdf"
req := WithResultFilePath(httptest.NewRequest(http.MethodPost, "/", nil), filePath)
if path, _ := req.Context().Value(resultFilePathKey).(string); path != filePath {
t.Errorf("Context returned a wrong result file path: got '%s' want '%s'", path, filePath)
}
}
func TestResultFilePathNotFoundError(t *testing.T) {
err := &resultFilePathNotFoundError{}
if err.Error() != resultFilePathNotFoundErrorMessage {
t.Errorf("Error returned a wrong message: got '%s' want '%s'", err.Error(), resultFilePathNotFoundErrorMessage)
}
}
func TestGetResultFilePath(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "/", nil)
// case 1: uses a request without a result file path entry in its context.
if _, err := GetResultFilePath(req); err == nil {
t.Error("Context should not have a result file path entry")
}
// case 2: uses a request with a result file path entry in its context.
req = WithResultFilePath(req, "file.pdf")
if _, err := GetResultFilePath(req); err != nil {
t.Error("Context should have a result file path entry")
}
}

View File

@@ -1,114 +0,0 @@
// Package converter implements a solution for converting one or more files to PDF.
package converter
import (
"fmt"
"io"
"net/http"
"os"
gfile "github.com/thecodingmachine/gotenberg/app/converter/file"
"github.com/thecodingmachine/gotenberg/app/converter/process"
"github.com/thecodingmachine/gotenberg/app/logger"
"github.com/satori/go.uuid"
)
// Converter handles conversion into PDF of files coming from a request.
type Converter struct {
files []*gfile.File
workingDir string
}
// NoFileToConvertError is raided when a request has not file attached to it.
type NoFileToConvertError struct{}
const noFileToConvertErrorMessage = "no file to convert"
func (e *NoFileToConvertError) Error() string {
return noFileToConvertErrorMessage
}
// NewConverter instantiates a converter by parsing a request.
func NewConverter(r *http.Request) (*Converter, error) {
c := &Converter{
workingDir: fmt.Sprintf("./%s/", uuid.NewV4().String()),
}
if err := os.Mkdir(c.workingDir, 0666); err != nil {
return nil, err
}
logger.Debugf("created working directory %s", c.workingDir)
reader, err := r.MultipartReader()
if err != nil {
return c, err
}
for {
part, err := reader.NextPart()
if err == io.EOF {
break
}
fileName := part.FileName()
if fileName == "" {
continue
}
f, err := gfile.NewFile(c.workingDir, part, fileName)
if err != nil {
return c, err
}
c.files = append(c.files, f)
}
if len(c.files) == 0 {
return c, &NoFileToConvertError{}
}
return c, nil
}
// Convert converts its associated files to PDF. If more than one file,
// it will merge all of them into one unique PDF file.
// Returns the new file path or an error if something bad happened.
func (c *Converter) Convert() (string, error) {
var filesPaths []string
for _, f := range c.files {
if f.Extension != ".pdf" {
path, err := process.Unconv(c.workingDir, f)
if err != nil {
return "", err
}
filesPaths = append(filesPaths, path)
} else {
filesPaths = append(filesPaths, f.Path)
}
}
if len(filesPaths) == 1 {
return filesPaths[0], nil
}
path, err := process.Merge(c.workingDir, filesPaths)
if err != nil {
return "", err
}
return path, nil
}
// Clear removes all file inside its working directory.
func (c *Converter) Clear() error {
if err := os.RemoveAll(c.workingDir); err != nil {
logger.Error(fmt.Errorf("failed to remove working directory %s", c.workingDir))
return err
}
logger.Debugf("removed working directory %s", c.workingDir)
return nil
}

View File

@@ -1,150 +0,0 @@
package converter
import (
"io"
"mime/multipart"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
"github.com/thecodingmachine/gotenberg/app/config"
)
func makeRequest(filesPaths ...string) *http.Request {
r, w := io.Pipe()
mpw := multipart.NewWriter(w)
go func() {
var part io.Writer
defer w.Close()
if len(filesPaths) == 0 {
part, _ = mpw.CreateFormField("foo")
part.Write([]byte("bar"))
} else {
for _, filePath := range filesPaths {
file, _ := os.Open(filePath)
defer file.Close()
fileInfo, _ := file.Stat()
part, _ = mpw.CreateFormFile("files", fileInfo.Name())
io.Copy(part, file)
}
}
mpw.Close()
}()
req := httptest.NewRequest(http.MethodPost, "/", r)
req.Header.Set("Content-Type", mpw.FormDataContentType())
return req
}
func load(configurationFilePath string) {
config.Reset()
path, _ := filepath.Abs(configurationFilePath)
config.ParseFile(path)
}
func TestNoFileToConvertError(t *testing.T) {
err := &NoFileToConvertError{}
if err.Error() != noFileToConvertErrorMessage {
t.Errorf("Error returned a wrong message: got '%s' want '%s'", err.Error(), noFileToConvertErrorMessage)
}
}
func TestNewConverter(t *testing.T) {
var (
path string
oPath string
)
load("../../_tests/configurations/gotenberg.yml")
// case 1: uses a request with a single file.
path, _ = filepath.Abs("../../_tests/file.docx")
if _, err := NewConverter(makeRequest(path)); err != nil {
t.Errorf("Converter should have been instantiated with '%s'", path)
}
// case 2: uses a request with wrong file type.
path, _ = filepath.Abs("../../_tests/configurations/gotenberg.yml")
if _, err := NewConverter(makeRequest(path)); err == nil {
t.Errorf("Converter should not have been instantiated with '%s'", path)
}
// case 3: uses a request with two files.
path, _ = filepath.Abs("../../_tests/file.pdf")
oPath, _ = filepath.Abs("../../_tests/file.docx")
if _, err := NewConverter(makeRequest(path, oPath)); err != nil {
t.Errorf("Converter should have been instantiated with '%s' and '%s'", path, oPath)
}
// case 4: uses a request with one Office file type and one wrong file type.
path, _ = filepath.Abs("../../_tests/configurations/gotenberg.yml")
oPath, _ = filepath.Abs("../../_tests/file.docx")
if _, err := NewConverter(makeRequest(path, oPath)); err == nil {
t.Errorf("Converter should not have been instantiated with '%s' and '%s'", path, oPath)
}
// case 5: uses a request with no file.
if _, err := NewConverter(makeRequest()); err == nil {
t.Error("Converter should not have been instantiated with no file")
}
}
func TestConvert(t *testing.T) {
var (
path string
oPath string
c *Converter
)
load("../../_tests/configurations/gotenberg.yml")
// case 1: uses a request with a single file.
path, _ = filepath.Abs("../../_tests/file.docx")
c, _ = NewConverter(makeRequest(path))
if _, err := c.Convert(); err != nil {
t.Errorf("Converter should have been able to convert '%s' to PDF", path)
}
// case 2: uses a request with two files.
path, _ = filepath.Abs("../../_tests/file.pdf")
oPath, _ = filepath.Abs("../../_tests/file.docx")
c, _ = NewConverter(makeRequest(path, oPath))
if _, err := c.Convert(); err != nil {
t.Errorf("Converter should have been able to convert '%s' and '%s' to PDF", path, oPath)
}
load("../../_tests/configurations/timeout-gotenberg.yml")
// case 3: uses a request with a single file and a configuration with an unsuitable timeout for the conversion commands.
path, _ = filepath.Abs("../../_tests/file.docx")
c, _ = NewConverter(makeRequest(path))
if _, err := c.Convert(); err == nil {
t.Errorf("Converter should not have been able to convert '%s' to PDF", path)
}
load("../../_tests/configurations/merge-timeout-gotenberg.yml")
// case 4: uses a request with two files and a configuration with an unsuitable timeout for the merge command.
path, _ = filepath.Abs("../../_tests/file.pdf")
oPath, _ = filepath.Abs("../../_tests/file.docx")
c, _ = NewConverter(makeRequest(path, oPath))
if _, err := c.Convert(); err == nil {
t.Errorf("Converter should not have been able to merge '%s' and '%s' into PDF", path, oPath)
}
}
func TestClear(t *testing.T) {
load("../../_tests/configurations/gotenberg.yml")
path, _ := filepath.Abs("../../_tests/file.docx")
c, _ := NewConverter(makeRequest(path))
if err := c.Clear(); err != nil {
t.Error("Converter should have been able to clear itself")
}
}

View File

@@ -1,60 +0,0 @@
// Package file implements a solution for handling files coming from a request.
package file
import (
"fmt"
"io"
"os"
"path/filepath"
"github.com/thecodingmachine/gotenberg/app/config"
"github.com/thecodingmachine/gotenberg/app/logger"
"github.com/dustin/go-humanize"
"github.com/satori/go.uuid"
)
// File represents a file which has been created
// from a request.
type File struct {
// Extension is the extension of the file.
Extension string
// Path is the file path.
Path string
}
// NewFile creates a file in the considered directory.
// Returns a *File instance or an error if something bad happened.
func NewFile(workingDir string, r io.Reader, fileName string) (*File, error) {
ext := filepath.Ext(fileName)
if _, err := config.GetCommand(ext); err != nil {
return nil, err
}
f := &File{ext, MakeFilePath(workingDir, ext)}
file, err := os.Create(f.Path)
if err != nil {
return nil, err
}
defer file.Close()
n, err := io.Copy(file, r)
if err != nil {
return nil, err
}
// resets the read pointer.
file.Seek(0, 0)
logger.Debugf("working file %s has been created from %s (%s copied)", f.Path, fileName, humanize.Bytes(uint64(n)))
return f, nil
}
// MakeFilePath is a simple helper which generates a random file name
// and associates it with the considered directory to make a path.
func MakeFilePath(workingDir string, ext string) string {
return fmt.Sprintf("%s%s%s", workingDir, uuid.NewV4().String(), ext)
}

View File

@@ -1,38 +0,0 @@
package file
import (
"bytes"
"os"
"path/filepath"
"testing"
"github.com/thecodingmachine/gotenberg/app/config"
)
func load(configurationFilePath string) {
config.Reset()
path, _ := filepath.Abs(configurationFilePath)
config.ParseFile(path)
}
func TestNewFile(t *testing.T) {
load("../../../_tests/configurations/gotenberg.yml")
workingDir := "test"
os.Mkdir(workingDir, 0666)
// case 1: uses a wrong file name.
if _, err := NewFile(workingDir, new(bytes.Buffer), "file.yml"); err == nil {
t.Error("File should not have been instantiated with an empty buffer")
}
// case 2: uses a file name.
filePath, _ := filepath.Abs("../../../_tests/file.pdf")
r, _ := os.Open(filePath)
defer r.Close()
if _, err := NewFile(workingDir, r, "file.pdf"); err != nil {
t.Errorf("File should have been instantiated using a reader of '%s'", filePath)
}
os.RemoveAll(workingDir)
}

View File

@@ -1,130 +0,0 @@
// Package process handles all commands executions.
package process
import (
"bytes"
"fmt"
"os/exec"
"sync"
"time"
"github.com/thecodingmachine/gotenberg/app/config"
gfile "github.com/thecodingmachine/gotenberg/app/converter/file"
"github.com/thecodingmachine/gotenberg/app/logger"
)
type runner struct {
mu sync.Mutex
}
var forest = &runner{}
type commandTimeoutError struct {
command string
timeout int
}
const commandTimeoutErrorMessage = "the command %s has reached the %d second(s) timeout"
func (e *commandTimeoutError) Error() string {
return fmt.Sprintf(commandTimeoutErrorMessage, e.command, e.timeout)
}
// run runs the given command. If timeout is reached or
// something bad happened, returns an error.
func (r *runner) run(command string, interpreter []string, timeout int) error {
if config.HasLock() {
r.mu.Lock()
defer r.mu.Unlock()
} else {
logger.Warn("lock disabled")
}
binary := interpreter[0]
parameters := append(interpreter[1:], command)
cmd := exec.Command(binary, parameters...)
logger.Debugf("executing command %s", cmd.Args)
if err := cmd.Start(); err != nil {
return err
}
done := make(chan error, 1)
go func() {
done <- cmd.Wait()
}()
// wait for the process to finish or kill it after a timeout.
select {
case <-time.After(time.Duration(timeout) * time.Second):
if err := cmd.Process.Kill(); err != nil {
return err
}
return &commandTimeoutError{command, timeout}
case err := <-done:
if err != nil {
return err
}
return nil
}
}
// conversionData will be applied to the data-driven templates of conversions commands.
type conversionData struct {
FilePath string
ResultFilePath string
}
// Unconv converts a file to PDF and returns the new file path.
func Unconv(workingDir string, file *gfile.File) (string, error) {
cmdData := &conversionData{file.Path, gfile.MakeFilePath(workingDir, ".pdf")}
cmd, err := config.GetCommand(file.Extension)
if err != nil {
return "", err
}
var data bytes.Buffer
if err := cmd.Template.Execute(&data, cmdData); err != nil {
return "", err
}
err = forest.run(data.String(), cmd.Interpreter, cmd.Timeout)
if err != nil {
return "", err
}
logger.Debugf("created %s from %s", cmdData.ResultFilePath, cmdData.FilePath)
return cmdData.ResultFilePath, nil
}
// mergeData will be applied to the data-driven template of the merge command.
type mergeData struct {
FilesPaths []string
ResultFilePath string
}
// Merge merges many PDF files to one unique PDF file and returns the new file path.
func Merge(workingDir string, filesPaths []string) (string, error) {
cmdData := &mergeData{filesPaths, gfile.MakeFilePath(workingDir, ".pdf")}
cmd, err := config.GetCommand(".pdf")
if err != nil {
return "", err
}
var data bytes.Buffer
if err := cmd.Template.Execute(&data, cmdData); err != nil {
return "", err
}
err = forest.run(data.String(), cmd.Interpreter, cmd.Timeout)
if err != nil {
return "", err
}
logger.Debugf("created %s from %+v", cmdData.ResultFilePath, cmdData.FilesPaths)
return cmdData.ResultFilePath, nil
}

View File

@@ -1,140 +0,0 @@
package process
import (
"fmt"
"os"
"path/filepath"
"strings"
"testing"
"github.com/thecodingmachine/gotenberg/app/config"
gfile "github.com/thecodingmachine/gotenberg/app/converter/file"
)
func makeFile(workingDir string, fileName string) *gfile.File {
filePath := fmt.Sprintf("%s%s", "../../../_tests/", fileName)
absPath, _ := filepath.Abs(filePath)
r, _ := os.Open(absPath)
defer r.Close()
f, _ := gfile.NewFile(workingDir, r, fileName)
return f
}
func load(configurationFilePath string) {
config.Reset()
path, _ := filepath.Abs(configurationFilePath)
config.ParseFile(path)
}
func TestCommandTimeoutError(t *testing.T) {
err := &commandTimeoutError{"echo hello", 30}
expected := fmt.Sprintf(commandTimeoutErrorMessage, err.command, err.timeout)
if err.Error() != expected {
t.Errorf("Error returned a wrong message: got '%s' want '%s'", err.Error(), expected)
}
}
func TestRun(t *testing.T) {
var cmd string
load("../../../_tests/configurations/gotenberg.yml")
// case 1: uses a simple command.
cmd = "echo Hello world"
if err := forest.run(cmd, strings.Fields("/bin/sh -c"), 30); err != nil {
t.Errorf("Command '%s' should have worked", cmd)
}
// case 2: uses a simple command but with an unsuitable timeout.
cmd = "sleep 5"
if err := forest.run(cmd, strings.Fields("/bin/sh -c"), 0); err == nil {
t.Errorf("Command '%s' should not have worked", cmd)
}
// case 3: uses a broken command.
cmd = "helloworld"
if err := forest.run(cmd, strings.Fields("/bin/sh -c"), 30); err == nil {
t.Errorf("Command '%s' should not have worked", cmd)
}
load("../../../_tests/configurations/no-lock-gotenberg.yml")
// case 4: uses a configuration with a no lock strategy.
cmd = "echo Hello world"
if err := forest.run(cmd, strings.Fields("/bin/sh -c"), 30); err != nil {
t.Errorf("Command '%s' should have worked", cmd)
}
}
func TestUnconv(t *testing.T) {
var file *gfile.File
workingDir := "test"
os.Mkdir(workingDir, 0666)
load("../../../_tests/configurations/gotenberg.yml")
// case 1: uses an Markdown file type.
file = makeFile(workingDir, "file.md")
if _, err := Unconv(workingDir, file); err != nil {
t.Errorf("Converting '%s' to PDF should have worked", file.Path)
}
// case 2: uses an HTML file type.
file = makeFile(workingDir, "file.html")
if _, err := Unconv(workingDir, file); err != nil {
t.Errorf("Converting '%s' to PDF should have worked", file.Path)
}
// case 3: uses an Office file type.
file = makeFile(workingDir, "file.docx")
if _, err := Unconv(workingDir, file); err != nil {
t.Errorf("Converting '%s' to PDF should have worked", file.Path)
}
// case 4: uses a PDF file type.
file = makeFile(workingDir, "file.pdf")
if _, err := Unconv(workingDir, file); err == nil {
t.Errorf("Converting '%s' to PDF should not have worked", file.Path)
}
load("../../../_tests/configurations/timeout-gotenberg.yml")
// case 5: uses a command with an unsuitable timeout.
file = makeFile(workingDir, "file.docx")
if _, err := Unconv(workingDir, makeFile(workingDir, "file.docx")); err == nil {
t.Errorf("Converting '%s' to PDF should have reached timeout", file.Path)
}
os.RemoveAll(workingDir)
}
func TestMerge(t *testing.T) {
workingDir := "test"
os.Mkdir(workingDir, 0666)
load("../../../_tests/configurations/gotenberg.yml")
var filesPaths []string
path, _ := filepath.Abs("../../../_tests/file.pdf")
filesPaths = append(filesPaths, path)
filesPaths = append(filesPaths, path)
// case 1: simple merge.
if _, err := Merge(workingDir, filesPaths); err != nil {
t.Error("Merge should have worked")
}
load("../../../_tests/configurations/timeout-gotenberg.yml")
// case 2: uses a command with an unsuitable timeout.
if _, err := Merge(workingDir, filesPaths); err == nil {
t.Error("Merge should have reached timeout")
}
os.RemoveAll(workingDir)
}

View File

@@ -1,170 +0,0 @@
// Package app implements all functions on which a request will pass through.
package app
import (
"fmt"
"io"
"net/http"
"os"
"github.com/thecodingmachine/gotenberg/app/context"
"github.com/thecodingmachine/gotenberg/app/converter"
ghttp "github.com/thecodingmachine/gotenberg/app/http"
"github.com/thecodingmachine/gotenberg/app/logger"
"github.com/dustin/go-humanize"
"github.com/justinas/alice"
"github.com/satori/go.uuid"
)
// GetHandlersChain returns the handlers chaining
// thanks to the alice library.
func GetHandlersChain() http.Handler {
return alice.New(enforceContentLengthHandler, enforceContentTypeHandler, convertHandler).ThenFunc(serveHandler)
}
type requestHasNoContentError struct{}
const requestHasNoContentErrorMessage = "request has not content"
func (e *requestHasNoContentError) Error() string {
return requestHasNoContentErrorMessage
}
// enforeContentLengthHandler checks if the request has content.
func enforceContentLengthHandler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.ContentLength == 0 {
e := &requestHasNoContentError{}
http.Error(w, e.Error(), http.StatusBadRequest)
logger.Error(e)
return
}
requestID := uuid.NewV4().String()
r = context.WithRequestID(r, requestID)
logger.Infof("identified new request (%s) with %s", humanize.Bytes(uint64(r.ContentLength)), requestID)
next.ServeHTTP(w, r)
})
}
// enforceContentTypeHandler checks if the "Content-Type" entry
// from the request's header matches the allowed content type.
func enforceContentTypeHandler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if err := ghttp.CheckAuthorizedContentType(r.Header); err != nil {
http.Error(w, err.Error(), http.StatusUnsupportedMediaType)
logger.Error(err)
return
}
next.ServeHTTP(w, r)
})
}
// convertHandler is in charge of converting the file(s) from the request to PDF.
func convertHandler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
c, err := converter.NewConverter(r)
if err != nil {
if _, ok := err.(*converter.NoFileToConvertError); ok {
http.Error(w, err.Error(), http.StatusBadRequest)
} else {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
logger.Error(err)
if c != nil {
r = context.WithConverter(r, c)
cleanup(r)
}
return
}
r = context.WithConverter(r, c)
path, err := c.Convert()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
logger.Error(err)
cleanup(r)
return
}
r = context.WithResultFilePath(r, path)
next.ServeHTTP(w, r)
})
}
// serveHandler simply serves the created PDF.
func serveHandler(w http.ResponseWriter, r *http.Request) {
path, err := context.GetResultFilePath(r)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
logger.Error(err)
cleanup(r)
return
}
reader, err := os.Open(path)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
logger.Error(err)
cleanup(r)
return
}
defer reader.Close()
resultFileInfo, err := reader.Stat()
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
logger.Error(err)
cleanup(r)
return
}
requestID, err := context.GetRequestID(r)
if err != nil {
logger.Error(err)
}
logger.Debugf("serving result file %s for request %s...", path, requestID)
done := make(chan error, 1)
go func() {
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", resultFileInfo.Name()))
w.Header().Set("Content-Type", "application/pdf")
w.Header().Set("Content-Length", fmt.Sprintf("%d", resultFileInfo.Size()))
_, err := io.Copy(w, reader)
done <- err
}()
err = <-done
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
logger.Error(err)
} else {
logger.Infof("result file %s (%s) sent for request %s", path, humanize.Bytes(uint64(resultFileInfo.Size())), requestID)
}
cleanup(r)
}
// cleanup removes all files created during the conversion.
func cleanup(r *http.Request) {
c, err := context.GetConverter(r)
if err != nil {
logger.Error(err)
return
}
if err := c.Clear(); err != nil {
logger.Error(err)
}
}

View File

@@ -1,227 +0,0 @@
package app
import (
"io"
"mime/multipart"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
"github.com/thecodingmachine/gotenberg/app/config"
"github.com/thecodingmachine/gotenberg/app/context"
"github.com/justinas/alice"
)
func makeRequest(filesPaths ...string) *http.Request {
r, w := io.Pipe()
mpw := multipart.NewWriter(w)
go func() {
var part io.Writer
defer w.Close()
if len(filesPaths) == 0 {
part, _ = mpw.CreateFormField("foo")
part.Write([]byte("bar"))
} else {
for _, filePath := range filesPaths {
file, _ := os.Open(filePath)
defer file.Close()
fileInfo, _ := file.Stat()
part, _ = mpw.CreateFormFile("files", fileInfo.Name())
io.Copy(part, file)
}
}
mpw.Close()
}()
req := httptest.NewRequest(http.MethodPost, "/", r)
req.Header.Set("Content-Type", mpw.FormDataContentType())
return req
}
func load(configurationFilePath string) {
config.Reset()
path, _ := filepath.Abs(configurationFilePath)
config.ParseFile(path)
}
func fakeSuccessHandler(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
}
func TestGetHandlersChain(t *testing.T) {
// dumb test to improve code coverage...
if GetHandlersChain() == nil {
t.Errorf("Handler chains should not be nil")
}
}
func TestRequestHasNoContentError(t *testing.T) {
err := &requestHasNoContentError{}
if err.Error() != requestHasNoContentErrorMessage {
t.Errorf("Error returned a wrong message: got '%s' want '%s'", err.Error(), requestHasNoContentErrorMessage)
}
}
func TestEnforceContentLengthHandler(t *testing.T) {
var (
req *http.Request
rr *httptest.ResponseRecorder
)
h := alice.New(enforceContentLengthHandler).ThenFunc(fakeSuccessHandler)
// case 1: sends an empty request.
req = httptest.NewRequest(http.MethodPost, "/", nil)
rr = httptest.NewRecorder()
h.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusBadRequest {
t.Errorf("Handler returned a wrong status code: got '%v' want '%v'", status, http.StatusBadRequest)
}
// case 2: sends a real body.
path, _ := filepath.Abs("../_tests/file.docx")
rr = httptest.NewRecorder()
h.ServeHTTP(rr, makeRequest(path))
if status := rr.Code; status != http.StatusOK {
t.Errorf("Handler returned a wrong status code: got '%v' want '%v'", status, http.StatusOK)
}
}
func TestEnforceContentTypeHandler(t *testing.T) {
var (
req *http.Request
rr *httptest.ResponseRecorder
)
h := alice.New(enforceContentTypeHandler).ThenFunc(fakeSuccessHandler)
// case 1: sends a wrong content type.
req = httptest.NewRequest(http.MethodPost, "/", nil)
req.Header.Set("Content-Type", "application/pdf")
rr = httptest.NewRecorder()
h.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusUnsupportedMediaType {
t.Errorf("Handler returned a wrong status code: got '%v' want '%v'", status, http.StatusUnsupportedMediaType)
}
// case 2: sends a good content type.
path, _ := filepath.Abs("../_tests/file.docx")
rr = httptest.NewRecorder()
h.ServeHTTP(rr, makeRequest(path))
if status := rr.Code; status != http.StatusOK {
t.Errorf("Handler returned wrong a status code: got '%v' want '%v'", status, http.StatusOK)
}
}
func TestConvertHandler(t *testing.T) {
var (
req *http.Request
rr *httptest.ResponseRecorder
path string
oPath string
)
h := alice.New(convertHandler).ThenFunc(fakeSuccessHandler)
// case 1: sends a request without body.
req = httptest.NewRequest(http.MethodPost, "/", nil)
rr = httptest.NewRecorder()
h.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusInternalServerError {
t.Errorf("Handler returned a wrong status code: got '%v' want '%v'", status, http.StatusInternalServerError)
}
// case 2: sends a request with no file.
req = makeRequest()
rr = httptest.NewRecorder()
h.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusBadRequest {
t.Errorf("Handler returned a wrong status code: got '%v' want '%v'", status, http.StatusBadRequest)
}
load("../_tests/configurations/merge-timeout-gotenberg.yml")
// case 3: sends a request with two files and using an unsuitable timeout for merge commande.
path, _ = filepath.Abs("../_tests/file.pdf")
oPath, _ = filepath.Abs("../_tests/file.docx")
req = makeRequest(path, oPath)
rr = httptest.NewRecorder()
h.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusInternalServerError {
t.Errorf("Handler returned a wrong status code: got '%v' want '%v'", status, http.StatusInternalServerError)
}
load("../_tests/configurations/gotenberg.yml")
// case 4: sends a request with two files.
path, _ = filepath.Abs("../_tests/file.pdf")
oPath, _ = filepath.Abs("../_tests/file.docx")
req = makeRequest(path, oPath)
rr = httptest.NewRecorder()
h.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusOK {
t.Errorf("Handler returned a wrong status code: got '%v' want '%v'", status, http.StatusOK)
}
// case 5: sends five requests (almost) simultany.
path, _ = filepath.Abs("../_tests/file.docx")
filesPaths := []string{
path,
path,
path,
path,
path,
}
for i := 0; i < len(filesPaths); i++ {
go func(i int) {
req := makeRequest(filesPaths[i])
rr := httptest.NewRecorder()
h.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusOK {
t.Errorf("Handler returned a wrong status code: got '%v' want '%v'", status, http.StatusOK)
}
}(i)
}
}
func TestServeHandler(t *testing.T) {
var (
req *http.Request
rr *httptest.ResponseRecorder
)
h := alice.New().ThenFunc(serveHandler)
// case 1: sends a request without a result file path entry in its context.
req = httptest.NewRequest(http.MethodPost, "/", nil)
rr = httptest.NewRecorder()
h.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusInternalServerError {
t.Errorf("Handler returned a wrong status code: got '%v' want '%v'", status, http.StatusInternalServerError)
}
// case 2: sends a request with a wrong result file path entry in its context.
req = context.WithResultFilePath(httptest.NewRequest(http.MethodPost, "/", nil), "file")
rr = httptest.NewRecorder()
h.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusInternalServerError {
t.Errorf("Handler returned a wrong status code: got '%v' want '%v'", status, http.StatusInternalServerError)
}
// case 3: sends a request with a correct result file path entry in its context.
path, _ := filepath.Abs("../_tests/file.pdf")
req = context.WithResultFilePath(httptest.NewRequest(http.MethodPost, "/", nil), path)
rr = httptest.NewRecorder()
h.ServeHTTP(rr, req)
if status := rr.Code; status != http.StatusOK {
t.Errorf("Handler returned a wrong status code: got '%v' want '%v'", status, http.StatusOK)
}
}

View File

@@ -1,49 +0,0 @@
// Package http provides functions for detecting a request content type.
package http
import (
"fmt"
"net/http"
"strings"
)
// ContentType is a string which represents a content type.
type ContentType string
// MultipartFormDataContentType represents... the multipart form data content type.
const MultipartFormDataContentType ContentType = "multipart/form-data"
type notAuthorizedContentTypeError struct{}
const notAuthorizedContentTypeErrorMessage = "accepted value for 'Content-Type': %s"
func (e *notAuthorizedContentTypeError) Error() string {
return fmt.Sprintf(notAuthorizedContentTypeErrorMessage, MultipartFormDataContentType)
}
// CheckAuthorizedContentType checks if the request header header has an authorized content type.
// If no authorized content type found, throws an error.
func CheckAuthorizedContentType(h http.Header) error {
ct := findContentType(h.Get("Content-Type"), MultipartFormDataContentType)
if ct == "" {
return &notAuthorizedContentTypeError{}
}
return nil
}
// findContentType parses a string representing a content type and tries to find
// one of the given content types.
func findContentType(requestContentType string, contentTypes ...ContentType) ContentType {
for _, ct := range contentTypes {
if i := strings.IndexRune(requestContentType, ';'); i != -1 {
requestContentType = requestContentType[0:i]
}
if requestContentType == string(ct) {
return ct
}
}
return ""
}

View File

@@ -1,39 +0,0 @@
package http
import (
"fmt"
"net/http"
"net/http/httptest"
"testing"
)
func TestCheckAuthorizedContentType(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "/", nil)
// case 1: uses a request without a content type entry in its header.
if err := CheckAuthorizedContentType(req.Header); err == nil {
t.Error("Function should not have been able to retrieve an authorized content type from request's header")
}
// case 2: uses a request with a content type entry in its header.
req.Header.Set("Content-Type", string(MultipartFormDataContentType))
if err := CheckAuthorizedContentType(req.Header); err != nil {
t.Error("Function should have been able to retrieve an authorized content type from request's header")
}
// case 3: uses a request with a composed content type entry in its header.
req.Header.Set("Content-Type", "multipart/form-data; boundary=—-WebKitFormBoundary7MA4YWxkTrZu0gW")
if err := CheckAuthorizedContentType(req.Header); err != nil {
t.Error("Function should have been able to retrieve an authorized content type from request's header")
}
}
func TestNotAuthorizedContentTypeError(t *testing.T) {
err := &notAuthorizedContentTypeError{}
expected := fmt.Sprintf(notAuthorizedContentTypeErrorMessage, MultipartFormDataContentType)
if err.Error() != expected {
t.Errorf("Error returned a wrong message: got '%s' want '%s'", err.Error(), expected)
}
}

View File

@@ -1,81 +0,0 @@
// Package logger implements a simple wrapper of the logrus library.
package logger
import (
"os"
"github.com/sirupsen/logrus"
)
// logger wraps a logrus.Logger instance.
type logger struct {
logger *logrus.Logger
}
// log is our logger instance used across the application.
var log = newLogger()
// newLogger instantiates a logger instance with default values.
func newLogger() *logger {
l := &logger{logrus.New()}
l.logger.Out = os.Stdout
l.logger.Level = logrus.InfoLevel
return l
}
// SetLevel updates the level of messages which will be logged.
func SetLevel(level logrus.Level) {
log.logger.SetLevel(level)
}
// SetFormatter updates the output format.
// When a TTY is not attached, the output will be in the defined format.
func SetFormatter(formatter logrus.Formatter) {
log.logger.Formatter = formatter
}
// Debug is a wrapper of the logrus Debug function.
func Debug(message string) {
log.logger.Debug(message)
}
// Debugf is a wrapper of the logrus Debugf function.
func Debugf(format string, args ...interface{}) {
log.logger.Debugf(format, args...)
}
// Info is a wrapper of the logrus Info function.
func Info(message string) {
log.logger.Info(message)
}
// Infof is a wrapper of the logrus Infof function.
func Infof(format string, args ...interface{}) {
log.logger.Infof(format, args...)
}
// Warn is a wrapper of the logrus Warn function.
func Warn(message string) {
log.logger.Warn(message)
}
// Warnf is wrapper of the logrus Warnf function.
func Warnf(format string, args ...interface{}) {
log.logger.Warnf(format, args...)
}
// Error is a wrapper of the logrus Error function.
func Error(err error) {
log.logger.Error(err.Error())
}
// Fatal is a wrapper of the logrus Fatal function.
func Fatal(err error) {
log.logger.Fatal(err.Error())
}
// Panic is a wrapper of the logrus Panic function.
func Panic(err error) {
log.logger.Panic(err.Error())
}

104
build/base/Dockerfile Normal file
View File

@@ -0,0 +1,104 @@
FROM thecodingmachine/gotenberg:3.2.0 AS hack
FROM debian:9.5-slim
# |--------------------------------------------------------------------------
# | Common libraries
# |--------------------------------------------------------------------------
# |
# | Libraries used in the build process of this image.
# |
RUN echo "deb http://httpredir.debian.org/debian/ stretch main contrib non-free" > /etc/apt/sources.list &&\
apt-get update &&\
apt-get install -y curl wget python3-pip ttf-mscorefonts-installer
# |--------------------------------------------------------------------------
# | PM2
# |--------------------------------------------------------------------------
# |
# | Installs PM2 for launching programs in background and with failure
# | recovering. In our case: Chrome (headless) and Office (headless).
# |
# Yep, this is dirty. The following script does not work anymore (see https://github.com/Unitech/pm2/issues/4127).
#RUN curl -sL https://raw.githubusercontent.com/Unitech/pm2/master/packager/setup.deb.sh | bash -
# Installing PM2 with Node.js and npm breaks something which prevents Google Chrome to work as expected.
# So we just copy all PM2 related files from a previous version of Gotenberg.
RUN curl -sL https://deb.nodesource.com/setup_9.x | bash - &&\
apt-get install -y nodejs
COPY --from=hack /usr/bin/pm2 /usr/bin/pm2
COPY --from=hack /usr/share/pm2 /usr/share/pm2
COPY --from=hack /etc/default/pm2 /etc/default/pm2
COPY --from=hack /etc/systemd/system/pm2.service /etc/systemd/system/pm2.service
# |--------------------------------------------------------------------------
# | Chrome
# |--------------------------------------------------------------------------
# |
# | Installs Chrome for HTML and Markdown conversions.
# |
RUN wget -q -O - 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 &&\
apt-get -y install google-chrome-stable
# |--------------------------------------------------------------------------
# | Unoconv
# |--------------------------------------------------------------------------
# |
# | Installs unoconv and LibreOffice for Office documents conversions.
# |
RUN pip3 install unoconv &&\
# https://github.com/nextcloud/docker/issues/380
mkdir -p /usr/share/man/man1mkdir -p /usr/share/man/man1 &&\
apt-get -y install libreoffice
# |--------------------------------------------------------------------------
# | PDFtk
# |--------------------------------------------------------------------------
# |
# | Installs PDFtk as an alternative to pdfcpu for merging PDFs.
# | https://github.com/thecodingmachine/gotenberg/issues/29
# |
RUN apt-get -y install pdftk
# |--------------------------------------------------------------------------
# | Fonts
# |--------------------------------------------------------------------------
# |
# | Installs a handful of fonts.
# | Note: ttf-mscorefonts-installer are installed on top of this Dockerfile.
# |
# Credits:
# https://github.com/arachnys/athenapdf/blob/master/cli/Dockerfile
# https://help.accusoft.com/PrizmDoc/v12.1/HTML/Installing_Asian_Fonts_on_Ubuntu_and_Debian.html
RUN apt-get install -y \
culmus \
fonts-beng \
fonts-hosny-amiri \
fonts-lklug-sinhala \
fonts-lohit-guru \
fonts-lohit-knda \
fonts-samyak-gujr \
fonts-samyak-mlym \
fonts-samyak-taml \
fonts-sarai \
fonts-sil-abyssinica \
fonts-sil-padauk \
fonts-telu \
fonts-thai-tlwg \
ttf-liberation \
ttf-wqy-zenhei \
fonts-arphic-uming \
fonts-ipafont-mincho \
fonts-ipafont-gothic \
fonts-unfonts-core
COPY build/base/fonts.conf /etc/fonts/conf.d/100-gotenberg.conf

29
build/base/fonts.conf Normal file
View File

@@ -0,0 +1,29 @@
<?xml version='1.0'?>
<!DOCTYPE fontconfig SYSTEM 'fonts.dtd'>
<fontconfig>
<match target="font">
<edit mode="assign" name="rgba">
<const>rgb</const>
</edit>
</match>
<match target="font">
<edit mode="assign" name="hinting">
<bool>true</bool>
</edit>
</match>
<match target="font">
<edit mode="assign" name="hintstyle">
<const>hintslight</const>
</edit>
</match>
<match target="font">
<edit mode="assign" name="antialias">
<bool>true</bool>
</edit>
</match>
<match target="font">
<edit mode="assign" name="lcdfilter">
<const>lcddefault</const>
</edit>
</match>
</fontconfig>

24
build/docs/Dockerfile Normal file
View File

@@ -0,0 +1,24 @@
ARG GOLANG_VERSION
FROM golang:${GOLANG_VERSION}-stretch
# |--------------------------------------------------------------------------
# | static
# |--------------------------------------------------------------------------
# |
# | Installs the static site anti-framework general-purpose library,
# | purpose-built commands for various domains.
# |
RUN go get github.com/apex/static/cmd/static-docs
# |--------------------------------------------------------------------------
# | Final touch
# |--------------------------------------------------------------------------
# |
# | Last instructions of this build.
# |
WORKDIR /docs
CMD [ "static-docs", "--in", "build/docs/content", "--out", "docs", "--theme", "gotenberg", "--title", "Gotenberg", "--subtitle", "A Docker-powered stateless API for converting HTML, Markdown and Office documents to PDF." ]

View File

@@ -0,0 +1,12 @@
---
title: Introduction
---
[Gotenberg](https://github.com/thecodingmachine/gotenberg/) is a Docker-powered stateless API for converting HTML, Markdown and Office documents to PDF.
* HTML and Markdown conversions using Google Chrome headless
* Office conversions (.txt, .rtf, .docx, .doc, .odt, .pptx, .ppt, .odp and so on) using [unoconv](https://github.com/dagwieers/unoconv)
* Performance: Google Chrome and Libreoffice (unoconv) started once in the background thanks to PM2
* Failure prevention: PM2 automatically restarts previous processes if they fail
* Assets: send your header, footer, images, fonts, stylesheets and so on for converting your HTML and Markdown to beaufitul PDFs!
* Easily interact with the API using our [Go](https://github.com/thecodingmachine/gotenberg/pkg) and [PHP](https://github.com/thecodingmachine/gotenberg-php-client) libraries

View File

@@ -0,0 +1,53 @@
---
title: Install
---
Gotenberg is shipped within a Docker image.
You may start it with:
```bash
$ docker run --rm -p 3000:3000 thecodingmachine/gotenberg:4
```
> The API will be available at [http://localhost:3000](http://localhost:3000).
Or add it in your Docker Compose stack:
```yaml
version: '3'
services:
# your others services
gotenberg:
image: thecodingmachine/gotenberg:4
```
> The API will be available under `gotenberg:3000` in your Docker Compose network.
It may also be deployed with Kubernetes.
In the following examples, we will assume your
Gotenberg API is available at [http://localhost:3000](http://localhost:3000).
## Go client
```bash
$ go get -u github.com/thecodingmachine/gotenberg
```
## PHP client
Unless your project already has a PSR7 `HttpClient`, install `php-http/guzzle6-adapter`:
```bash
$ composer require php-http/guzzle6-adapter
```
Then the PHP client:
```bash
$ composer require thecodingmachine/gotenberg-php-client
```

View File

@@ -0,0 +1,298 @@
---
title: HTML
---
Gotenberg provides the endpoint `/convert/html` for HTML conversions.
It accepts `POST` requests with a `multipart/form-data` Content-Type.
## Basic
The only requirement is to send a file named `index.html`: it is the file
which will be converted to PDF.
For instance:
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>My PDF</title>
</head>
<body>
<h1>Hello world!</h1>
</body>
</html>
```
### cURL
```bash
$ curl --request POST \
--url http://localhost:3000/convert/html \
--header 'Content-Type: multipart/form-data' \
--form files=@index.html \
-o result.pdf
```
### Go
```golang
import "github.com/thecodingmachine/gotenberg/pkg"
func main() {
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
req, _ := gotenberg.NewHTMLRequest("index.html")
dest := "result.pdf"
c.Store(req, dest)
}
```
### PHP
```php
use TheCodingMachine\Gotenberg\Client;
use TheCodingMachine\Gotenberg\DocumentFactory;
use TheCodingMachine\Gotenberg\HTMLRequest;
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
$index = DocumentFactory::makeFromPath('index.html', 'index.html');
$request = new HTMLRequest($index);
$dirPath = "/foo";
$filename = $client->store($request, $dirPath);
```
## Header and footer
You may also add a header and/or a footer in the resulting PDF.
Respectively, a file named `header.html` and `footer.html`.
Each of them **has to be a complete HTML document**:
```html
<html>
<head>
<style>
body {
font-size: 8rem;
margin: 4rem auto;
}
</style>
</head>
<body>
<p>
<span class="pageNumber"></span> of <span class="totalPages"></span>
</p>
</body>
</html>
```
The following classes allow you to inject printing values:
* `date`: formatted print date
* `title`: document title
* `pageNumber`: current page number
* `totalPage`: total pages in the document
> **Attention:** the CSS properties are independant of the ones used in the `index.html` file.
> Also, `footer.html` CSS properties override the ones from `header.html`.
### cURL
```bash
$ curl --request POST \
--url http://localhost:3000/convert/html \
--header 'Content-Type: multipart/form-data' \
--form files=@index.html \
--form files=@header.html \
--form files=@footer.html \
-o result.pdf
```
### Go
```golang
import "github.com/thecodingmachine/gotenberg/pkg"
func main() {
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
req, _ := gotenberg.NewHTMLRequest("index.html")
req.SetHeader("header.html")
req.SetFooter("footer.html")
dest := "result.pdf"
c.Store(req, dest)
}
```
### PHP
```php
use TheCodingMachine\Gotenberg\Client;
use TheCodingMachine\Gotenberg\DocumentFactory;
use TheCodingMachine\Gotenberg\HTMLRequest;
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
$index = DocumentFactory::makeFromPath('index.html', 'index.html');
$header = DocumentFactory::makeFromPath('header.html', 'header.html');
$footer = DocumentFactory::makeFromPath('footer.html', 'footer.html');
$request = new HTMLRequest($index);
$request->setHeader($header);
$request->setFooter($footer);
$dirPath = "/foo";
$filename = $client->store($request, $dirPath);
```
## Assets
You may also send additional files. For instance: images, fonts, stylesheets and so on.
The only requirement is to make sure that their paths
are on the same level as the `index.html` file.
In others words, this will work:
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>My PDF</title>
</head>
<body>
<h1>Hello world!</h1>
<img src="img.png">
</body>
</html>
```
But this won't:
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>My PDF</title>
</head>
<body>
<h1>Hello world!</h1>
<img src="/foo/img.png">
</body>
</html>
```
You may also use *remote* paths for Google fonts, images and so on.
> If you want to install fonts directly in the Gotenberg Docker image,
> see to the [fonts section](#fonts).
> For web fonts, there is a timeout of 500ms by default. You may update
> this value thanks to the form field `webFontsTimeout`.
### cURL
```bash
$ curl --request POST \
--url http://localhost:3000/convert/html \
--header 'Content-Type: multipart/form-data' \
--form files=@index.html \
--form files=@style.css \
--form files=@img.png \
--form files=@font.woff \
-o result.pdf
```
### Go
```golang
import "github.com/thecodingmachine/gotenberg/pkg"
func main() {
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
req, _ := gotenberg.NewHTMLRequest("index.html")
req.SetAssets("font.woff", "img.gif", "style.css")
dest := "result.pdf"
c.Store(req, dest)
}
```
### PHP
```php
use TheCodingMachine\Gotenberg\Client;
use TheCodingMachine\Gotenberg\DocumentFactory;
use TheCodingMachine\Gotenberg\HTMLRequest;
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
$index = DocumentFactory::makeFromPath('index.html', 'index.html');
$assets = [
DocumentFactory::makeFromPath('style.css', 'style.css'),
DocumentFactory::makeFromPath('img.png', 'img.png'),
DocumentFactory::makeFromPath('font.woff', 'font.woff'),
];
$request = new HTMLRequest($index);
$request->setAssets($assets);
$dest = "result.pdf";
$client->store($request, $dest);
```
## Paper size, margins, orientation
You may also customize the resulting PDF format.
By default, it will be rendered with `A4` size, `1 inch` margins and `portrait` orientation.
> Paper size and margins have to be provided in `inches`.
> Also, you have to set both `paperWidth` and `paperHeight`. Same for margins.
### cURL
```bash
$ curl --request POST \
--url http://localhost:3000/convert/html \
--header 'Content-Type: multipart/form-data' \
--form files=@index.html \
--form paperWidth=8.27 \
--form paperHeight=11.27 \
--form marginTop=0 \
--form marginBottom=0 \
--form marginLeft=0 \
--form marginRight=0 \
--form landscape=true \
-o result.pdf
```
### Go
```golang
import "github.com/thecodingmachine/gotenberg/pkg"
func main() {
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
req, _ := gotenberg.NewHTMLRequest("index.html")
req.SetPaperSize(gotenberg.A4)
req.SetMargins(gotenberg.NoMargins)
req.SetLandscape(true)
dest := "result.pdf"
c.Store(req, dest)
}
```
### PHP
```php
use TheCodingMachine\Gotenberg\Client;
use TheCodingMachine\Gotenberg\DocumentFactory;
use TheCodingMachine\Gotenberg\HTMLRequest;
use TheCodingMachine\Gotenberg\Request;
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
$index = DocumentFactory::makeFromPath('index.html', 'index.html');
$request = new HTMLRequest($index);
$request->setPaperSize(Request::A4);
$request->setMargins(Request::NO_MARGINS);
$request->setLandscape(true);
$dest = "result.pdf";
$client->store($request, $dest);
```

View File

@@ -0,0 +1,56 @@
---
title: URL
---
Gotenberg provides the endpoint `/convert/url` for remote URL conversions.
It accepts `POST` requests with a `multipart/form-data` Content-Type.
## Basic
This endpoint does not accept an `index.html` file nor assets files but a form field
named `remoteURL` instead. Otherwise, URL conversions work the same as HTML conversions.
> **Attention:** when converting a website to PDF, you should remove all margins.
> If not, some of the content of the page might be hidden.
### cURL
```bash
$ curl --request POST \
--url http://localhost:3000/convert/url \
--header 'Content-Type: multipart/form-data' \
--form remoteURL=https://google.com
--form marginTop=0 \
--form marginBottom=0 \
--form marginLeft=0 \
--form marginRight=0 \
-o result.pdf
```
### Go
```golang
import "github.com/thecodingmachine/gotenberg/pkg"
func main() {
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
req := gotenberg.NewURLRequest("https://google.com")
req.SetMargins(gotenberg.NoMargins)
dest := "result.pdf"
c.Store(req, dest)
}
```
### PHP
```php
use TheCodingMachine\Gotenberg\Client;
use TheCodingMachine\Gotenberg\URLRequest;
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
$request = new URLRequest('https://google.com');
$request->setMargins(Request::NO_MARGINS);
$dest = "result.pdf";
$client->store($request, $dest);
```

View File

@@ -0,0 +1,70 @@
---
title: Markdown
---
Gotenberg provides the endpoint `/convert/markdown` for Markdown conversions.
It accepts `POST` requests with a `multipart/form-data` Content-Type.
## Basic
Markdown conversions work the same as HTML conversions.
Only difference is that you have access to the Go template function `toHTML`
in the file `index.html`. This function will convert a given markdown file to HTML.
For instance:
```html
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>My PDF</title>
</head>
<body>
{{ toHTML .DirPath "file.md" }}
</body>
</html>
```
### cURL
```bash
$ curl --request POST \
--url http://localhost:3000/convert/markdown \
--header 'Content-Type: multipart/form-data' \
--form files=@index.html \
--form files=@file.md \
-o result.pdf
```
### Go
```golang
import "github.com/thecodingmachine/gotenberg/pkg"
func main() {
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
req, _ := gotenberg.NewMarkdownRequest("index.html", "file.md")
dest := "result.pdf"
c.Store(req, dest)
}
```
### PHP
```php
use TheCodingMachine\Gotenberg\Client;
use TheCodingMachine\Gotenberg\DocumentFactory;
use TheCodingMachine\Gotenberg\MarkdownRequest;
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
$index = DocumentFactory::makeFromPath('index.html', 'index.html');
$markdowns = [
DocumentFactory::makeFromPath('file.md', 'file.md'),
];
$request = new MarkdownRequest($index, $markdowns);
$dest = "result.pdf";
$client->store($request, $dest);
```

View File

@@ -0,0 +1,118 @@
---
title: Office
---
Gotenberg provides the endpoint `/convert/office` for Office document conversions.
It accepts `POST` requests with a `multipart/form-data` Content-Type.
## Basic
You may send one or more Office documents. Following file extensions are accepted:
* `.txt`
* `.rtf`
* `.doc`
* `.docx`
* `.odt`
* `.xls`
* `.xlsx`
* `.ods`
* `.ppt`
* `.pptx`
* `.odp`
All files will be merged into a single resulting PDF.
> **Attention:** currently, `unoconv` cannot perform concurrent conversions.
> That's why for Office conversions, the API does only one conversion at a time.
> See the [scalability section](#scalability) to find how to mitigate this issue.
### cURL
```bash
$ curl --request POST \
--url http://localhost:3000/convert/office \
--header 'Content-Type: multipart/form-data' \
--form files=@document.docx \
--form files=@document2.docx \
-o result.pdf
```
### Go
```golang
import "github.com/thecodingmachine/gotenberg/pkg"
func main() {
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
req, _ := gotenberg.NewOfficeRequest("document.docx", "document2.docx")
dest := "result.pdf"
c.Store(req, dest)
}
```
### PHP
```php
use TheCodingMachine\Gotenberg\Client;
use TheCodingMachine\Gotenberg\DocumentFactory;
use TheCodingMachine\Gotenberg\OfficeRequest;
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
$files = [
DocumentFactory::makeFromPath('document.docx', 'document.docx'),
DocumentFactory::makeFromPath('document2.docx', 'document2.docx'),
];
$request = new OfficeRequest($files);
$dest = "result.pdf";
$client->store($request, $dest);
```
## Orientation
You may also customize the resulting PDF format.
By default, it will be rendered with `portrait` orientation.
### cURL
```bash
$ curl --request POST \
--url http://localhost:3000/convert/office \
--header 'Content-Type: multipart/form-data' \
--form files=@document.docx \
--form landscape=true \
-o result.pdf
```
### Go
```golang
import "github.com/thecodingmachine/gotenberg/pkg"
func main() {
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
req, _ := gotenberg.NewOfficeRequest("document.docx")
req.SetLandscape(true)
dest := "result.pdf"
c.Store(req, dest)
}
```
### PHP
```php
use TheCodingMachine\Gotenberg\Client;
use TheCodingMachine\Gotenberg\DocumentFactory;
use TheCodingMachine\Gotenberg\OfficeRequest;
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
$files = [
DocumentFactory::makeFromPath('document.docx', 'document.docx'),
];
$request = new OfficeRequest($files);
$request->setLandscape(true);
$dest = "result.pdf";
$client->store($request, $dest);
```

View File

@@ -0,0 +1,53 @@
---
title: Merge
---
Gotenberg provides the endpoint `/merge` for merging PDFs.
It accepts `POST` requests with a `multipart/form-data` Content-Type.
## Basic
Nothing special here: you may send one or more PDF files and the API
will merge them and return the resulting PDF file.
### cURL
```bash
$ curl --request POST \
--url http://localhost:3000/merge \
--header 'Content-Type: multipart/form-data' \
--form files=@file.pdf \
--form files=@file2.pdf \
-o result.pdf
```
### Go
```golang
import "github.com/thecodingmachine/gotenberg/pkg"
func main() {
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
req, _ := gotenberg.NewMergeRequest("file.pdf", "file2.pdf")
dest := "result.pdf"
c.Store(req, dest)
}
```
### PHP
```php
use TheCodingMachine\Gotenberg\Client;
use TheCodingMachine\Gotenberg\DocumentFactory;
use TheCodingMachine\Gotenberg\MergeRequest;
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
$files = [
DocumentFactory::makeFromPath('file.pdf', 'file.pdf'),
DocumentFactory::makeFromPath('file2.pdf', 'file2.pdf'),
];
$request = new MergeRequest($files);
$dest = "result.pdf";
$client->store($request, $dest);
```

View File

@@ -0,0 +1,50 @@
---
title: Webhook
---
All endpoints accept a form field named `webhookURL`.
If provided, the API will send the resulting PDF file in a `POST` request with the `application/pdf` Content-Type
to given URL.
By doing so, your requests to the API will be over before the conversions are actually done!
## Examples
### cURL
```bash
$ curl --request POST \
--url http://localhost:3000/convert/html \
--header 'Content-Type: multipart/form-data' \
--form files=@index.html \
--form webhookURL='http://myapp.com/webhook/'
```
### Go
```golang
import "github.com/thecodingmachine/gotenberg/pkg"
func main() {
c := &gotenberg.Client{Hostname: "http://localhost:3000"}
req, _ := gotenberg.NewHTMLRequest("index.html")
req.SetWebhookURL("http://myapp.com/webhook/")
dest := "result.pdf"
resp, _ := c.Post(req)
}
```
### PHP
```php
use TheCodingMachine\Gotenberg\Client;
use TheCodingMachine\Gotenberg\DocumentFactory;
use TheCodingMachine\Gotenberg\HTMLRequest;
$client = new Client('http://localhost:3000', new \Http\Adapter\Guzzle6\Client());
$index = DocumentFactory::makeFromPath('index.html', 'index.html');
$request = new HTMLRequest($index);
$request->setWebhookURL('http://myapp.com/webhook/');
$resp = $client->post($request);
```

View File

@@ -0,0 +1,27 @@
---
title: Scalability
---
The API being stateless, you may scale it as much as you want.
For instance, using the following Docker Compose file:
```yaml
version: '3'
services:
# your others services
gotenberg:
image: thecodingmachine/gotenberg:4
```
You may now launch your services using:
```bash
$ docker-compose up --scale gotenberg=your_number_of_instances
```
When requesting the Gotenberg service with your client(s), Docker will automatically
redirect a request to a Gotenberg container according to the round-robin strategy.

View File

@@ -0,0 +1,11 @@
---
title: Liveness
---
Gotenberg provides the endpoint `/ping` for checking the API availability with
a simple `GET` request.
This feature is especially useful for liveness/readiness probes in Kubernetes:
* [Pod lifecycle](https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#container-probes)
* [Configure Liveness and Readiness Probes](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-probes/)

View File

@@ -0,0 +1,13 @@
---
title: Fonts
---
By default, a handful of fonts are installed. Asian characters are also supported out of the box.
If you wish to use more fonts, you will have to create your own image:
```Dockerfile
FROM thecodingmachine/gotenberg:4
RUN apt-get -y install yourfonts
```

View File

@@ -0,0 +1,8 @@
---
title: Links
---
* Follow the progress on the [GitHub repository](https://github.com/thecodingmachine/gotenberg)
* Follow [@gulnap](https://twitter.com/gulnap) on Twitter
Psst: TheCodingMachine is always looking for [talented coders](https://coders.thecodingmachine.com).

35
build/lint/Dockerfile Normal file
View File

@@ -0,0 +1,35 @@
ARG GOLANG_VERSION
FROM golang:${GOLANG_VERSION}-stretch
# |--------------------------------------------------------------------------
# | GolangCI-Lint
# |--------------------------------------------------------------------------
# |
# | Installs GolangCI-Lint, a linters Runner for Go. 5x faster
# | than gometalinter.
# |
ENV GOLANGCI_LINT_VERSION 1.12.3
RUN curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b /usr/local/bin v${GOLANGCI_LINT_VERSION} &&\
golangci-lint --version
# |--------------------------------------------------------------------------
# | Final touch
# |--------------------------------------------------------------------------
# |
# | Last instructions of this build.
# |
# Define our workding outside of $GOPATH (we're using go modules).
WORKDIR /lint
# Copy our module dependencies definitions.
COPY go.mod .
COPY go.sum .
# Install module dependencies.
RUN go mod download
CMD ["golangci-lint", "run" ,"--tests=false", "--enable-all", "--disable=dupl", "--disable=lll", "--disable=errcheck", "--disable=gosec", "--disable=gochecknoglobals", "--disable=gochecknoinits" ]

43
build/package/Dockerfile Normal file
View File

@@ -0,0 +1,43 @@
ARG GOLANG_VERSION
# |--------------------------------------------------------------------------
# | Binary
# |--------------------------------------------------------------------------
# |
# | Buils Gotenberg binary.
# |
FROM golang:${GOLANG_VERSION}-stretch AS golang
ARG VERSION
ENV GOOS=linux \
GOARCH=amd64 \
CGO_ENABLED=0
# Define our workding outside of $GOPATH (we're using go modules).
WORKDIR /gotenberg
# Copy our source code.
COPY . .
# Build our binary.
RUN go build -o /gotenberg/gotenberg -ldflags "-X main.version=${VERSION}" cmd/gotenberg/main.go
# |--------------------------------------------------------------------------
# | Final touch
# |--------------------------------------------------------------------------
# |
# | Last instructions of this build.
# |
FROM thecodingmachine/gotenberg:base
LABEL authors="Julien Neuhart <j.neuhart@thecodingmachine.com>"
COPY --from=golang /gotenberg/gotenberg /usr/local/bin/
WORKDIR /gotenberg
EXPOSE 3000
CMD [ "gotenberg" ]

48
build/tests/Dockerfile Normal file
View File

@@ -0,0 +1,48 @@
ARG GOLANG_VERSION
FROM golang:${GOLANG_VERSION}-stretch AS golang
FROM thecodingmachine/gotenberg:base
# |--------------------------------------------------------------------------
# | Common libraries
# |--------------------------------------------------------------------------
# |
# | Libraries used in the build process of this image.
# |
RUN apt-get install -y git gcc
# |--------------------------------------------------------------------------
# | Golang
# |--------------------------------------------------------------------------
# |
# | Installs Golang.
# |
COPY --from=golang /usr/local/go /usr/local/go
RUN export PATH="/usr/local/go/bin:$PATH" &&\
go version
ENV GOPATH /go
ENV PATH $GOPATH/bin:/usr/local/go/bin:$PATH
# |--------------------------------------------------------------------------
# | Final touch
# |--------------------------------------------------------------------------
# |
# | Last instructions of this build.
# |
# Define our workding outside of $GOPATH (we're using go modules).
WORKDIR /tests
# Copy our module dependencies definitions.
COPY go.mod .
COPY go.sum .
# Install module dependencies.
RUN go mod download
ENTRYPOINT ["build/tests/docker-entrypoint.sh"]

View File

@@ -0,0 +1,25 @@
#!/bin/bash
set -xe
# Testing PM2 processes launch separatly for avoiding
# spending to much time on each tests depending on
# them.
go test github.com/thecodingmachine/gotenberg/internal/pkg/pm2 -run TestChromeLaunch
go test github.com/thecodingmachine/gotenberg/internal/pkg/pm2 -run TestUnoconvLaunch
# Running others tests.
go test -race -cover -covermode=atomic github.com/thecodingmachine/gotenberg/internal/app/api
go test -race -cover -covermode=atomic github.com/thecodingmachine/gotenberg/internal/pkg/printer
go test -race -cover -covermode=atomic github.com/thecodingmachine/gotenberg/internal/pkg/rand
# Finally testing processes shutdown.
go test github.com/thecodingmachine/gotenberg/internal/pkg/pm2 -run TestChromeShutdown
go test github.com/thecodingmachine/gotenberg/internal/pkg/pm2 -run TestUnoconvShutdown
# Testing Go client.
go build -o /usr/local/bin/gotenberg cmd/gotenberg/main.go
gotenberg &
sleep 10
go test -race -cover -covermode=atomic github.com/thecodingmachine/gotenberg/pkg
sleep 5 # allows Gotenberg to remove generated files (concurrent requests).

21
cmd/gotenberg/main.go Normal file
View File

@@ -0,0 +1,21 @@
package main
import (
"fmt"
"os"
"github.com/thecodingmachine/gotenberg/internal/app/api"
"github.com/thecodingmachine/gotenberg/internal/pkg/notify"
)
// version will be set on build time.
var version = "snapshot"
func main() {
notify.Println(fmt.Sprintf("Gotenberg %s", version))
if err := api.Start(); err != nil {
notify.ErrPrintln(err)
os.Exit(1)
}
os.Exit(0)
}

966
docs/index.html Executable file
View File

@@ -0,0 +1,966 @@
<!DOCTYPE html><html><head>
<meta charset="utf-8"/>
<meta http-equiv="x-ua-compatible" content="ie=edge"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>Gotenberg · A Docker-powered stateless API for converting HTML, Markdown and Office documents to PDF.</title>
<link rel="apple-touch-icon-precomposed" sizes="57x57" href="theme/gotenberg/img/apple-touch-icon-57x57.png"/>
<link rel="apple-touch-icon-precomposed" sizes="114x114" href="theme/gotenberg/img/apple-touch-icon-114x114.png"/>
<link rel="apple-touch-icon-precomposed" sizes="72x72" href="theme/gotenberg/img/apple-touch-icon-72x72.png"/>
<link rel="apple-touch-icon-precomposed" sizes="144x144" href="theme/gotenberg/img/apple-touch-icon-144x144.png"/>
<link rel="apple-touch-icon-precomposed" sizes="60x60" href="theme/gotenberg/img/apple-touch-icon-60x60.png"/>
<link rel="apple-touch-icon-precomposed" sizes="120x120" href="theme/gotenberg/img/apple-touch-icon-120x120.png"/>
<link rel="apple-touch-icon-precomposed" sizes="76x76" href="theme/gotenberg/img/apple-touch-icon-76x76.png"/>
<link rel="apple-touch-icon-precomposed" sizes="152x152" href="theme/gotenberg/img/apple-touch-icon-152x152.png"/>
<link rel="icon" type="image/png" href="theme/gotenberg/img/favicon-196x196.png" sizes="196x196"/>
<link rel="icon" type="image/png" href="theme/gotenberg/img/favicon-96x96.png" sizes="96x96"/>
<link rel="icon" type="image/png" href="theme/gotenberg/img/favicon-32x32.png" sizes="32x32"/>
<link rel="icon" type="image/png" href="theme/gotenberg/img/favicon-16x16.png" sizes="16x16"/>
<link rel="icon" type="image/png" href="theme/gotenberg/img/favicon-128.png" sizes="128x128"/>
<link rel="stylesheet" href="theme/gotenberg/css/index.css"/>
</head>
<body>
<div class="Wrapper">
<div class="Container">
<div class="Header">
<div class="Title center">
<img src="https://user-images.githubusercontent.com/8983173/50009948-84b01e00-ffb8-11e8-850b-fc240382c626.png" alt="Gotenberg logo" width="250" height="250"/>
<span class="text">Gotenberg</span>
<span class="subtext">A Docker-powered stateless API for converting HTML, Markdown and Office documents to PDF.</span>
</div>
</div>
<div class="Content-wrapper">
<div class="Sidebar">
<div class="Menu">
<div class="item">
<a href="#introduction">Introduction</a>
</div>
<div class="item">
<a href="#install">Install</a>
</div>
<div class="item">
<a href="#html">HTML</a>
</div>
<div class="item">
<a href="#url">URL</a>
</div>
<div class="item">
<a href="#markdown">Markdown</a>
</div>
<div class="item">
<a href="#office">Office</a>
</div>
<div class="item">
<a href="#merge">Merge</a>
</div>
<div class="item">
<a href="#webhook">Webhook</a>
</div>
<div class="item">
<a href="#scalability">Scalability</a>
</div>
<div class="item">
<a href="#liveness">Liveness</a>
</div>
<div class="item">
<a href="#fonts">Fonts</a>
</div>
<div class="item">
<a href="#links">Links</a>
</div>
</div>
</div>
<div class="Content">
<div class="Page" id="introduction">
<h1 class="Heading"><a class="Anchor" aria-hidden="true" id="introduction" href="#introduction">
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
</a>Introduction</h1>
<p><a href="https://github.com/thecodingmachine/gotenberg/">Gotenberg</a> is a Docker-powered stateless API for converting HTML, Markdown and Office documents to PDF.</p>
<ul>
<li>HTML and Markdown conversions using Google Chrome headless</li>
<li>Office conversions (.txt, .rtf, .docx, .doc, .odt, .pptx, .ppt, .odp and so on) using <a href="https://github.com/dagwieers/unoconv">unoconv</a></li>
<li>Performance: Google Chrome and Libreoffice (unoconv) started once in the background thanks to PM2</li>
<li>Failure prevention: PM2 automatically restarts previous processes if they fail</li>
<li>Assets: send your header, footer, images, fonts, stylesheets and so on for converting your HTML and Markdown to beaufitul PDFs!</li>
<li>Easily interact with the API using our <a href="https://github.com/thecodingmachine/gotenberg/pkg">Go</a> and <a href="https://github.com/thecodingmachine/gotenberg-php-client">PHP</a> libraries</li>
</ul>
</div>
<div class="Page" id="install">
<h1 class="Heading"><a class="Anchor" aria-hidden="true" id="install" href="#install">
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
</a>Install</h1>
<p>Gotenberg is shipped within a Docker image.</p>
<p>You may start it with:</p>
<pre class="chroma">$ docker run --rm -p <span class="m">3000</span>:3000 thecodingmachine/gotenberg:4
</pre>
<blockquote>
<p>The API will be available at <a href="http://localhost:3000">http://localhost:3000</a>.</p>
</blockquote>
<p>Or add it in your Docker Compose stack:</p>
<pre class="chroma">version<span class="p">:</span><span class="w"> </span><span class="s1">&#39;3&#39;</span><span class="w">
</span><span class="w">
</span><span class="w"></span>services<span class="p">:</span><span class="w">
</span><span class="w">
</span><span class="w"> </span><span class="c"># your others services</span><span class="w">
</span><span class="w">
</span><span class="w"> </span>gotenberg<span class="p">:</span><span class="w">
</span><span class="w"> </span>image<span class="p">:</span><span class="w"> </span>thecodingmachine/gotenberg<span class="p">:</span><span class="m">4</span><span class="w">
</span></pre>
<blockquote>
<p>The API will be available under <code>gotenberg:3000</code> in your Docker Compose network.</p>
</blockquote>
<p>It may also be deployed with Kubernetes.</p>
<p>In the following examples, we will assume your
Gotenberg API is available at <a href="http://localhost:3000">http://localhost:3000</a>.</p>
<h2 class="Heading"><a class="Anchor" aria-hidden="true" id="install.go_client" href="#install.go_client">
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
</a>Go client</h2>
<pre class="chroma">$ go get -u github.com/thecodingmachine/gotenberg
</pre>
<h2 class="Heading"><a class="Anchor" aria-hidden="true" id="install.php_client" href="#install.php_client">
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
</a>PHP client</h2>
<p>Unless your project already has a PSR7 <code>HttpClient</code>, install <code>php-http/guzzle6-adapter</code>:</p>
<pre class="chroma">$ composer require php-http/guzzle6-adapter
</pre>
<p>Then the PHP client:</p>
<pre class="chroma">$ composer require thecodingmachine/gotenberg-php-client
</pre>
</div>
<div class="Page" id="html">
<h1 class="Heading"><a class="Anchor" aria-hidden="true" id="html" href="#html">
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
</a>HTML</h1>
<p>Gotenberg provides the endpoint <code>/convert/html</code> for HTML conversions.</p>
<p>It accepts <code>POST</code> requests with a <code>multipart/form-data</code> Content-Type.</p>
<h2 class="Heading"><a class="Anchor" aria-hidden="true" id="html.basic" href="#html.basic">
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
</a>Basic</h2>
<p>The only requirement is to send a file named <code>index.html</code>: it is the file
which will be converted to PDF.</p>
<p>For instance:</p>
<pre class="chroma"><span class="cp">&lt;!doctype html&gt;</span>
<span class="p">&lt;</span><span class="nt">html</span> <span class="na">lang</span><span class="o">=</span><span class="s">&#34;en&#34;</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">head</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">meta</span> <span class="na">charset</span><span class="o">=</span><span class="s">&#34;utf-8&#34;</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">title</span><span class="p">&gt;</span>My PDF<span class="p">&lt;/</span><span class="nt">title</span><span class="p">&gt;</span>
<span class="p">&lt;/</span><span class="nt">head</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">body</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">h1</span><span class="p">&gt;</span>Hello world!<span class="p">&lt;/</span><span class="nt">h1</span><span class="p">&gt;</span>
<span class="p">&lt;/</span><span class="nt">body</span><span class="p">&gt;</span>
<span class="p">&lt;/</span><span class="nt">html</span><span class="p">&gt;</span>
</pre>
<h3 class="Heading"><a class="Anchor" aria-hidden="true" id="html.basic.c_url" href="#html.basic.c_url">
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
</a>cURL</h3>
<pre class="chroma">$ curl --request POST <span class="se">\
</span><span class="se"></span> --url http://localhost:3000/convert/html <span class="se">\
</span><span class="se"></span> --header <span class="s1">&#39;Content-Type: multipart/form-data&#39;</span> <span class="se">\
</span><span class="se"></span> --form <span class="nv">files</span><span class="o">=</span>@index.html <span class="se">\
</span><span class="se"></span> -o result.pdf
</pre>
<h3 class="Heading"><a class="Anchor" aria-hidden="true" id="html.basic.go" href="#html.basic.go">
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
</a>Go</h3>
<pre class="chroma"><span class="kn">import</span> <span class="s">&#34;github.com/thecodingmachine/gotenberg/pkg&#34;</span>
<span class="kd">func</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span>
<span class="nx">c</span> <span class="o">:=</span> <span class="o">&amp;</span><span class="nx">gotenberg</span><span class="p">.</span><span class="nx">Client</span><span class="p">{</span><span class="nx">Hostname</span><span class="p">:</span> <span class="s">&#34;http://localhost:3000&#34;</span><span class="p">}</span>
<span class="nx">req</span><span class="p">,</span> <span class="nx">_</span> <span class="o">:=</span> <span class="nx">gotenberg</span><span class="p">.</span><span class="nf">NewHTMLRequest</span><span class="p">(</span><span class="s">&#34;index.html&#34;</span><span class="p">)</span>
<span class="nx">dest</span> <span class="o">:=</span> <span class="s">&#34;result.pdf&#34;</span>
<span class="nx">c</span><span class="p">.</span><span class="nf">Store</span><span class="p">(</span><span class="nx">req</span><span class="p">,</span> <span class="nx">dest</span><span class="p">)</span>
<span class="p">}</span>
</pre>
<h3 class="Heading"><a class="Anchor" aria-hidden="true" id="html.basic.php" href="#html.basic.php">
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
</a>PHP</h3>
<pre class="chroma">use TheCodingMachine\Gotenberg\Client;
use TheCodingMachine\Gotenberg\DocumentFactory;
use TheCodingMachine\Gotenberg\HTMLRequest;
$client = new Client(&#39;http://localhost:3000&#39;, new \Http\Adapter\Guzzle6\Client());
$index = DocumentFactory::makeFromPath(&#39;index.html&#39;, &#39;index.html&#39;);
$request = new HTMLRequest($index);
$dirPath = &#34;/foo&#34;;
$filename = $client-&gt;store($request, $dirPath);
</pre>
<h2 class="Heading"><a class="Anchor" aria-hidden="true" id="html.header_and_footer" href="#html.header_and_footer">
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
</a>Header and footer</h2>
<p>You may also add a header and/or a footer in the resulting PDF.
Respectively, a file named <code>header.html</code> and <code>footer.html</code>.</p>
<p>Each of them <strong>has to be a complete HTML document</strong>:</p>
<pre class="chroma"><span class="p">&lt;</span><span class="nt">html</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">head</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">style</span><span class="p">&gt;</span>
<span class="nt">body</span> <span class="p">{</span>
<span class="k">font-size</span><span class="p">:</span> <span class="mi">8</span><span class="kt">rem</span><span class="p">;</span>
<span class="k">margin</span><span class="p">:</span> <span class="mi">4</span><span class="kt">rem</span> <span class="kc">auto</span><span class="p">;</span>
<span class="p">}</span>
<span class="p">&lt;/</span><span class="nt">style</span><span class="p">&gt;</span>
<span class="p">&lt;/</span><span class="nt">head</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">body</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">p</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">span</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;pageNumber&#34;</span><span class="p">&gt;&lt;/</span><span class="nt">span</span><span class="p">&gt;</span> of <span class="p">&lt;</span><span class="nt">span</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;totalPages&#34;</span><span class="p">&gt;&lt;/</span><span class="nt">span</span><span class="p">&gt;</span>
<span class="p">&lt;/</span><span class="nt">p</span><span class="p">&gt;</span>
<span class="p">&lt;/</span><span class="nt">body</span><span class="p">&gt;</span>
<span class="p">&lt;/</span><span class="nt">html</span><span class="p">&gt;</span>
</pre>
<p>The following classes allow you to inject printing values:</p>
<ul>
<li><code>date</code>: formatted print date</li>
<li><code>title</code>: document title</li>
<li><code>pageNumber</code>: current page number</li>
<li><code>totalPage</code>: total pages in the document</li>
</ul>
<blockquote>
<p><strong>Attention:</strong> the CSS properties are independant of the ones used in the <code>index.html</code> file.
Also, <code>footer.html</code> CSS properties override the ones from <code>header.html</code>.</p>
</blockquote>
<h3 class="Heading"><a class="Anchor" aria-hidden="true" id="html.header_and_footer.c_url" href="#html.header_and_footer.c_url">
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
</a>cURL</h3>
<pre class="chroma">$ curl --request POST <span class="se">\
</span><span class="se"></span> --url http://localhost:3000/convert/html <span class="se">\
</span><span class="se"></span> --header <span class="s1">&#39;Content-Type: multipart/form-data&#39;</span> <span class="se">\
</span><span class="se"></span> --form <span class="nv">files</span><span class="o">=</span>@index.html <span class="se">\
</span><span class="se"></span> --form <span class="nv">files</span><span class="o">=</span>@header.html <span class="se">\
</span><span class="se"></span> --form <span class="nv">files</span><span class="o">=</span>@footer.html <span class="se">\
</span><span class="se"></span> -o result.pdf
</pre>
<h3 class="Heading"><a class="Anchor" aria-hidden="true" id="html.header_and_footer.go" href="#html.header_and_footer.go">
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
</a>Go</h3>
<pre class="chroma"><span class="kn">import</span> <span class="s">&#34;github.com/thecodingmachine/gotenberg/pkg&#34;</span>
<span class="kd">func</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span>
<span class="nx">c</span> <span class="o">:=</span> <span class="o">&amp;</span><span class="nx">gotenberg</span><span class="p">.</span><span class="nx">Client</span><span class="p">{</span><span class="nx">Hostname</span><span class="p">:</span> <span class="s">&#34;http://localhost:3000&#34;</span><span class="p">}</span>
<span class="nx">req</span><span class="p">,</span> <span class="nx">_</span> <span class="o">:=</span> <span class="nx">gotenberg</span><span class="p">.</span><span class="nf">NewHTMLRequest</span><span class="p">(</span><span class="s">&#34;index.html&#34;</span><span class="p">)</span>
<span class="nx">req</span><span class="p">.</span><span class="nf">SetHeader</span><span class="p">(</span><span class="s">&#34;header.html&#34;</span><span class="p">)</span>
<span class="nx">req</span><span class="p">.</span><span class="nf">SetFooter</span><span class="p">(</span><span class="s">&#34;footer.html&#34;</span><span class="p">)</span>
<span class="nx">dest</span> <span class="o">:=</span> <span class="s">&#34;result.pdf&#34;</span>
<span class="nx">c</span><span class="p">.</span><span class="nf">Store</span><span class="p">(</span><span class="nx">req</span><span class="p">,</span> <span class="nx">dest</span><span class="p">)</span>
<span class="p">}</span>
</pre>
<h3 class="Heading"><a class="Anchor" aria-hidden="true" id="html.header_and_footer.php" href="#html.header_and_footer.php">
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
</a>PHP</h3>
<pre class="chroma">use TheCodingMachine\Gotenberg\Client;
use TheCodingMachine\Gotenberg\DocumentFactory;
use TheCodingMachine\Gotenberg\HTMLRequest;
$client = new Client(&#39;http://localhost:3000&#39;, new \Http\Adapter\Guzzle6\Client());
$index = DocumentFactory::makeFromPath(&#39;index.html&#39;, &#39;index.html&#39;);
$header = DocumentFactory::makeFromPath(&#39;header.html&#39;, &#39;header.html&#39;);
$footer = DocumentFactory::makeFromPath(&#39;footer.html&#39;, &#39;footer.html&#39;);
$request = new HTMLRequest($index);
$request-&gt;setHeader($header);
$request-&gt;setFooter($footer);
$dirPath = &#34;/foo&#34;;
$filename = $client-&gt;store($request, $dirPath);
</pre>
<h2 class="Heading"><a class="Anchor" aria-hidden="true" id="html.assets" href="#html.assets">
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
</a>Assets</h2>
<p>You may also send additional files. For instance: images, fonts, stylesheets and so on.</p>
<p>The only requirement is to make sure that their paths
are on the same level as the <code>index.html</code> file.</p>
<p>In others words, this will work:</p>
<pre class="chroma"><span class="cp">&lt;!doctype html&gt;</span>
<span class="p">&lt;</span><span class="nt">html</span> <span class="na">lang</span><span class="o">=</span><span class="s">&#34;en&#34;</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">head</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">meta</span> <span class="na">charset</span><span class="o">=</span><span class="s">&#34;utf-8&#34;</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">title</span><span class="p">&gt;</span>My PDF<span class="p">&lt;/</span><span class="nt">title</span><span class="p">&gt;</span>
<span class="p">&lt;/</span><span class="nt">head</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">body</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">h1</span><span class="p">&gt;</span>Hello world!<span class="p">&lt;/</span><span class="nt">h1</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">img</span> <span class="na">src</span><span class="o">=</span><span class="s">&#34;img.png&#34;</span><span class="p">&gt;</span>
<span class="p">&lt;/</span><span class="nt">body</span><span class="p">&gt;</span>
<span class="p">&lt;/</span><span class="nt">html</span><span class="p">&gt;</span>
</pre>
<p>But this wont:</p>
<pre class="chroma"><span class="cp">&lt;!doctype html&gt;</span>
<span class="p">&lt;</span><span class="nt">html</span> <span class="na">lang</span><span class="o">=</span><span class="s">&#34;en&#34;</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">head</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">meta</span> <span class="na">charset</span><span class="o">=</span><span class="s">&#34;utf-8&#34;</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">title</span><span class="p">&gt;</span>My PDF<span class="p">&lt;/</span><span class="nt">title</span><span class="p">&gt;</span>
<span class="p">&lt;/</span><span class="nt">head</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">body</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">h1</span><span class="p">&gt;</span>Hello world!<span class="p">&lt;/</span><span class="nt">h1</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">img</span> <span class="na">src</span><span class="o">=</span><span class="s">&#34;/foo/img.png&#34;</span><span class="p">&gt;</span>
<span class="p">&lt;/</span><span class="nt">body</span><span class="p">&gt;</span>
<span class="p">&lt;/</span><span class="nt">html</span><span class="p">&gt;</span>
</pre>
<p>You may also use <em>remote</em> paths for Google fonts, images and so on.</p>
<blockquote>
<p>If you want to install fonts directly in the Gotenberg Docker image,
see to the <a href="#fonts">fonts section</a>.</p>
<p>For web fonts, there is a timeout of 500ms by default. You may update
this value thanks to the form field <code>webFontsTimeout</code>.</p>
</blockquote>
<h3 class="Heading"><a class="Anchor" aria-hidden="true" id="html.assets.c_url" href="#html.assets.c_url">
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
</a>cURL</h3>
<pre class="chroma">$ curl --request POST <span class="se">\
</span><span class="se"></span> --url http://localhost:3000/convert/html <span class="se">\
</span><span class="se"></span> --header <span class="s1">&#39;Content-Type: multipart/form-data&#39;</span> <span class="se">\
</span><span class="se"></span> --form <span class="nv">files</span><span class="o">=</span>@index.html <span class="se">\
</span><span class="se"></span> --form <span class="nv">files</span><span class="o">=</span>@style.css <span class="se">\
</span><span class="se"></span> --form <span class="nv">files</span><span class="o">=</span>@img.png <span class="se">\
</span><span class="se"></span> --form <span class="nv">files</span><span class="o">=</span>@font.woff <span class="se">\
</span><span class="se"></span> -o result.pdf
</pre>
<h3 class="Heading"><a class="Anchor" aria-hidden="true" id="html.assets.go" href="#html.assets.go">
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
</a>Go</h3>
<pre class="chroma"><span class="kn">import</span> <span class="s">&#34;github.com/thecodingmachine/gotenberg/pkg&#34;</span>
<span class="kd">func</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span>
<span class="nx">c</span> <span class="o">:=</span> <span class="o">&amp;</span><span class="nx">gotenberg</span><span class="p">.</span><span class="nx">Client</span><span class="p">{</span><span class="nx">Hostname</span><span class="p">:</span> <span class="s">&#34;http://localhost:3000&#34;</span><span class="p">}</span>
<span class="nx">req</span><span class="p">,</span> <span class="nx">_</span> <span class="o">:=</span> <span class="nx">gotenberg</span><span class="p">.</span><span class="nf">NewHTMLRequest</span><span class="p">(</span><span class="s">&#34;index.html&#34;</span><span class="p">)</span>
<span class="nx">req</span><span class="p">.</span><span class="nf">SetAssets</span><span class="p">(</span><span class="s">&#34;font.woff&#34;</span><span class="p">,</span> <span class="s">&#34;img.gif&#34;</span><span class="p">,</span> <span class="s">&#34;style.css&#34;</span><span class="p">)</span>
<span class="nx">dest</span> <span class="o">:=</span> <span class="s">&#34;result.pdf&#34;</span>
<span class="nx">c</span><span class="p">.</span><span class="nf">Store</span><span class="p">(</span><span class="nx">req</span><span class="p">,</span> <span class="nx">dest</span><span class="p">)</span>
<span class="p">}</span>
</pre>
<h3 class="Heading"><a class="Anchor" aria-hidden="true" id="html.assets.php" href="#html.assets.php">
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
</a>PHP</h3>
<pre class="chroma">use TheCodingMachine\Gotenberg\Client;
use TheCodingMachine\Gotenberg\DocumentFactory;
use TheCodingMachine\Gotenberg\HTMLRequest;
$client = new Client(&#39;http://localhost:3000&#39;, new \Http\Adapter\Guzzle6\Client());
$index = DocumentFactory::makeFromPath(&#39;index.html&#39;, &#39;index.html&#39;);
$assets = [
DocumentFactory::makeFromPath(&#39;style.css&#39;, &#39;style.css&#39;),
DocumentFactory::makeFromPath(&#39;img.png&#39;, &#39;img.png&#39;),
DocumentFactory::makeFromPath(&#39;font.woff&#39;, &#39;font.woff&#39;),
];
$request = new HTMLRequest($index);
$request-&gt;setAssets($assets);
$dest = &#34;result.pdf&#34;;
$client-&gt;store($request, $dest);
</pre>
<h2 class="Heading"><a class="Anchor" aria-hidden="true" id="html.paper_size_margins_orientation" href="#html.paper_size_margins_orientation">
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
</a>Paper size, margins, orientation</h2>
<p>You may also customize the resulting PDF format.</p>
<p>By default, it will be rendered with <code>A4</code> size, <code>1 inch</code> margins and <code>portrait</code> orientation.</p>
<blockquote>
<p>Paper size and margins have to be provided in <code>inches</code>.
Also, you have to set both <code>paperWidth</code> and <code>paperHeight</code>. Same for margins.</p>
</blockquote>
<h3 class="Heading"><a class="Anchor" aria-hidden="true" id="html.paper_size_margins_orientation.c_url" href="#html.paper_size_margins_orientation.c_url">
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
</a>cURL</h3>
<pre class="chroma">$ curl --request POST <span class="se">\
</span><span class="se"></span> --url http://localhost:3000/convert/html <span class="se">\
</span><span class="se"></span> --header <span class="s1">&#39;Content-Type: multipart/form-data&#39;</span> <span class="se">\
</span><span class="se"></span> --form <span class="nv">files</span><span class="o">=</span>@index.html <span class="se">\
</span><span class="se"></span> --form <span class="nv">paperWidth</span><span class="o">=</span><span class="m">8</span>.27 <span class="se">\
</span><span class="se"></span> --form <span class="nv">paperHeight</span><span class="o">=</span><span class="m">11</span>.27 <span class="se">\
</span><span class="se"></span> --form <span class="nv">marginTop</span><span class="o">=</span><span class="m">0</span> <span class="se">\
</span><span class="se"></span> --form <span class="nv">marginBottom</span><span class="o">=</span><span class="m">0</span> <span class="se">\
</span><span class="se"></span> --form <span class="nv">marginLeft</span><span class="o">=</span><span class="m">0</span> <span class="se">\
</span><span class="se"></span> --form <span class="nv">marginRight</span><span class="o">=</span><span class="m">0</span> <span class="se">\
</span><span class="se"></span> --form <span class="nv">landscape</span><span class="o">=</span><span class="nb">true</span> <span class="se">\
</span><span class="se"></span> -o result.pdf
</pre>
<h3 class="Heading"><a class="Anchor" aria-hidden="true" id="html.paper_size_margins_orientation.go" href="#html.paper_size_margins_orientation.go">
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
</a>Go</h3>
<pre class="chroma"><span class="kn">import</span> <span class="s">&#34;github.com/thecodingmachine/gotenberg/pkg&#34;</span>
<span class="kd">func</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span>
<span class="nx">c</span> <span class="o">:=</span> <span class="o">&amp;</span><span class="nx">gotenberg</span><span class="p">.</span><span class="nx">Client</span><span class="p">{</span><span class="nx">Hostname</span><span class="p">:</span> <span class="s">&#34;http://localhost:3000&#34;</span><span class="p">}</span>
<span class="nx">req</span><span class="p">,</span> <span class="nx">_</span> <span class="o">:=</span> <span class="nx">gotenberg</span><span class="p">.</span><span class="nf">NewHTMLRequest</span><span class="p">(</span><span class="s">&#34;index.html&#34;</span><span class="p">)</span>
<span class="nx">req</span><span class="p">.</span><span class="nf">SetPaperSize</span><span class="p">(</span><span class="nx">gotenberg</span><span class="p">.</span><span class="nx">A4</span><span class="p">)</span>
<span class="nx">req</span><span class="p">.</span><span class="nf">SetMargins</span><span class="p">(</span><span class="nx">gotenberg</span><span class="p">.</span><span class="nx">NoMargins</span><span class="p">)</span>
<span class="nx">req</span><span class="p">.</span><span class="nf">SetLandscape</span><span class="p">(</span><span class="kc">true</span><span class="p">)</span>
<span class="nx">dest</span> <span class="o">:=</span> <span class="s">&#34;result.pdf&#34;</span>
<span class="nx">c</span><span class="p">.</span><span class="nf">Store</span><span class="p">(</span><span class="nx">req</span><span class="p">,</span> <span class="nx">dest</span><span class="p">)</span>
<span class="p">}</span>
</pre>
<h3 class="Heading"><a class="Anchor" aria-hidden="true" id="html.paper_size_margins_orientation.php" href="#html.paper_size_margins_orientation.php">
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
</a>PHP</h3>
<pre class="chroma">use TheCodingMachine\Gotenberg\Client;
use TheCodingMachine\Gotenberg\DocumentFactory;
use TheCodingMachine\Gotenberg\HTMLRequest;
use TheCodingMachine\Gotenberg\Request;
$client = new Client(&#39;http://localhost:3000&#39;, new \Http\Adapter\Guzzle6\Client());
$index = DocumentFactory::makeFromPath(&#39;index.html&#39;, &#39;index.html&#39;);
$request = new HTMLRequest($index);
$request-&gt;setPaperSize(Request::A4);
$request-&gt;setMargins(Request::NO_MARGINS);
$request-&gt;setLandscape(true);
$dest = &#34;result.pdf&#34;;
$client-&gt;store($request, $dest);
</pre>
</div>
<div class="Page" id="url">
<h1 class="Heading"><a class="Anchor" aria-hidden="true" id="url" href="#url">
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
</a>URL</h1>
<p>Gotenberg provides the endpoint <code>/convert/url</code> for remote URL conversions.</p>
<p>It accepts <code>POST</code> requests with a <code>multipart/form-data</code> Content-Type.</p>
<h2 class="Heading"><a class="Anchor" aria-hidden="true" id="url.basic" href="#url.basic">
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
</a>Basic</h2>
<p>This endpoint does not accept an <code>index.html</code> file nor assets files but a form field
named <code>remoteURL</code> instead. Otherwise, URL conversions work the same as HTML conversions.</p>
<blockquote>
<p><strong>Attention:</strong> when converting a website to PDF, you should remove all margins.
If not, some of the content of the page might be hidden.</p>
</blockquote>
<h3 class="Heading"><a class="Anchor" aria-hidden="true" id="url.basic.c_url" href="#url.basic.c_url">
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
</a>cURL</h3>
<pre class="chroma">$ curl --request POST <span class="se">\
</span><span class="se"></span> --url http://localhost:3000/convert/url <span class="se">\
</span><span class="se"></span> --header <span class="s1">&#39;Content-Type: multipart/form-data&#39;</span> <span class="se">\
</span><span class="se"></span> --form <span class="nv">remoteURL</span><span class="o">=</span>https://google.com
--form <span class="nv">marginTop</span><span class="o">=</span><span class="m">0</span> <span class="se">\
</span><span class="se"></span> --form <span class="nv">marginBottom</span><span class="o">=</span><span class="m">0</span> <span class="se">\
</span><span class="se"></span> --form <span class="nv">marginLeft</span><span class="o">=</span><span class="m">0</span> <span class="se">\
</span><span class="se"></span> --form <span class="nv">marginRight</span><span class="o">=</span><span class="m">0</span> <span class="se">\
</span><span class="se"></span> -o result.pdf
</pre>
<h3 class="Heading"><a class="Anchor" aria-hidden="true" id="url.basic.go" href="#url.basic.go">
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
</a>Go</h3>
<pre class="chroma"><span class="kn">import</span> <span class="s">&#34;github.com/thecodingmachine/gotenberg/pkg&#34;</span>
<span class="kd">func</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span>
<span class="nx">c</span> <span class="o">:=</span> <span class="o">&amp;</span><span class="nx">gotenberg</span><span class="p">.</span><span class="nx">Client</span><span class="p">{</span><span class="nx">Hostname</span><span class="p">:</span> <span class="s">&#34;http://localhost:3000&#34;</span><span class="p">}</span>
<span class="nx">req</span> <span class="o">:=</span> <span class="nx">gotenberg</span><span class="p">.</span><span class="nf">NewURLRequest</span><span class="p">(</span><span class="s">&#34;https://google.com&#34;</span><span class="p">)</span>
<span class="nx">req</span><span class="p">.</span><span class="nf">SetMargins</span><span class="p">(</span><span class="nx">gotenberg</span><span class="p">.</span><span class="nx">NoMargins</span><span class="p">)</span>
<span class="nx">dest</span> <span class="o">:=</span> <span class="s">&#34;result.pdf&#34;</span>
<span class="nx">c</span><span class="p">.</span><span class="nf">Store</span><span class="p">(</span><span class="nx">req</span><span class="p">,</span> <span class="nx">dest</span><span class="p">)</span>
<span class="p">}</span>
</pre>
<h3 class="Heading"><a class="Anchor" aria-hidden="true" id="url.basic.php" href="#url.basic.php">
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
</a>PHP</h3>
<pre class="chroma">use TheCodingMachine\Gotenberg\Client;
use TheCodingMachine\Gotenberg\URLRequest;
$client = new Client(&#39;http://localhost:3000&#39;, new \Http\Adapter\Guzzle6\Client());
$request = new URLRequest(&#39;https://google.com&#39;);
$request-&gt;setMargins(Request::NO_MARGINS);
$dest = &#34;result.pdf&#34;;
$client-&gt;store($request, $dest);
</pre>
</div>
<div class="Page" id="markdown">
<h1 class="Heading"><a class="Anchor" aria-hidden="true" id="markdown" href="#markdown">
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
</a>Markdown</h1>
<p>Gotenberg provides the endpoint <code>/convert/markdown</code> for Markdown conversions.</p>
<p>It accepts <code>POST</code> requests with a <code>multipart/form-data</code> Content-Type.</p>
<h2 class="Heading"><a class="Anchor" aria-hidden="true" id="markdown.basic" href="#markdown.basic">
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
</a>Basic</h2>
<p>Markdown conversions work the same as HTML conversions.</p>
<p>Only difference is that you have access to the Go template function <code>toHTML</code>
in the file <code>index.html</code>. This function will convert a given markdown file to HTML.</p>
<p>For instance:</p>
<pre class="chroma"><span class="cp">&lt;!doctype html&gt;</span>
<span class="p">&lt;</span><span class="nt">html</span> <span class="na">lang</span><span class="o">=</span><span class="s">&#34;en&#34;</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">head</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">meta</span> <span class="na">charset</span><span class="o">=</span><span class="s">&#34;utf-8&#34;</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">title</span><span class="p">&gt;</span>My PDF<span class="p">&lt;/</span><span class="nt">title</span><span class="p">&gt;</span>
<span class="p">&lt;/</span><span class="nt">head</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">body</span><span class="p">&gt;</span>
{{ toHTML .DirPath &#34;file.md&#34; }}
<span class="p">&lt;/</span><span class="nt">body</span><span class="p">&gt;</span>
<span class="p">&lt;/</span><span class="nt">html</span><span class="p">&gt;</span>
</pre>
<h3 class="Heading"><a class="Anchor" aria-hidden="true" id="markdown.basic.c_url" href="#markdown.basic.c_url">
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
</a>cURL</h3>
<pre class="chroma">$ curl --request POST <span class="se">\
</span><span class="se"></span> --url http://localhost:3000/convert/markdown <span class="se">\
</span><span class="se"></span> --header <span class="s1">&#39;Content-Type: multipart/form-data&#39;</span> <span class="se">\
</span><span class="se"></span> --form <span class="nv">files</span><span class="o">=</span>@index.html <span class="se">\
</span><span class="se"></span> --form <span class="nv">files</span><span class="o">=</span>@file.md <span class="se">\
</span><span class="se"></span> -o result.pdf
</pre>
<h3 class="Heading"><a class="Anchor" aria-hidden="true" id="markdown.basic.go" href="#markdown.basic.go">
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
</a>Go</h3>
<pre class="chroma"><span class="kn">import</span> <span class="s">&#34;github.com/thecodingmachine/gotenberg/pkg&#34;</span>
<span class="kd">func</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span>
<span class="nx">c</span> <span class="o">:=</span> <span class="o">&amp;</span><span class="nx">gotenberg</span><span class="p">.</span><span class="nx">Client</span><span class="p">{</span><span class="nx">Hostname</span><span class="p">:</span> <span class="s">&#34;http://localhost:3000&#34;</span><span class="p">}</span>
<span class="nx">req</span><span class="p">,</span> <span class="nx">_</span> <span class="o">:=</span> <span class="nx">gotenberg</span><span class="p">.</span><span class="nf">NewMarkdownRequest</span><span class="p">(</span><span class="s">&#34;index.html&#34;</span><span class="p">,</span> <span class="s">&#34;file.md&#34;</span><span class="p">)</span>
<span class="nx">dest</span> <span class="o">:=</span> <span class="s">&#34;result.pdf&#34;</span>
<span class="nx">c</span><span class="p">.</span><span class="nf">Store</span><span class="p">(</span><span class="nx">req</span><span class="p">,</span> <span class="nx">dest</span><span class="p">)</span>
<span class="p">}</span>
</pre>
<h3 class="Heading"><a class="Anchor" aria-hidden="true" id="markdown.basic.php" href="#markdown.basic.php">
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
</a>PHP</h3>
<pre class="chroma">use TheCodingMachine\Gotenberg\Client;
use TheCodingMachine\Gotenberg\DocumentFactory;
use TheCodingMachine\Gotenberg\MarkdownRequest;
$client = new Client(&#39;http://localhost:3000&#39;, new \Http\Adapter\Guzzle6\Client());
$index = DocumentFactory::makeFromPath(&#39;index.html&#39;, &#39;index.html&#39;);
$markdowns = [
DocumentFactory::makeFromPath(&#39;file.md&#39;, &#39;file.md&#39;),
];
$request = new MarkdownRequest($index, $markdowns);
$dest = &#34;result.pdf&#34;;
$client-&gt;store($request, $dest);
</pre>
</div>
<div class="Page" id="office">
<h1 class="Heading"><a class="Anchor" aria-hidden="true" id="office" href="#office">
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
</a>Office</h1>
<p>Gotenberg provides the endpoint <code>/convert/office</code> for Office document conversions.</p>
<p>It accepts <code>POST</code> requests with a <code>multipart/form-data</code> Content-Type.</p>
<h2 class="Heading"><a class="Anchor" aria-hidden="true" id="office.basic" href="#office.basic">
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
</a>Basic</h2>
<p>You may send one or more Office documents. Following file extensions are accepted:</p>
<ul>
<li><code>.txt</code></li>
<li><code>.rtf</code></li>
<li><code>.doc</code></li>
<li><code>.docx</code></li>
<li><code>.odt</code></li>
<li><code>.xls</code></li>
<li><code>.xlsx</code></li>
<li><code>.ods</code></li>
<li><code>.ppt</code></li>
<li><code>.pptx</code></li>
<li><code>.odp</code></li>
</ul>
<p>All files will be merged into a single resulting PDF.</p>
<blockquote>
<p><strong>Attention:</strong> currently, <code>unoconv</code> cannot perform concurrent conversions.
Thats why for Office conversions, the API does only one conversion at a time.
See the <a href="#scalability">scalability section</a> to find how to mitigate this issue.</p>
</blockquote>
<h3 class="Heading"><a class="Anchor" aria-hidden="true" id="office.basic.c_url" href="#office.basic.c_url">
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
</a>cURL</h3>
<pre class="chroma">$ curl --request POST <span class="se">\
</span><span class="se"></span> --url http://localhost:3000/convert/office <span class="se">\
</span><span class="se"></span> --header <span class="s1">&#39;Content-Type: multipart/form-data&#39;</span> <span class="se">\
</span><span class="se"></span> --form <span class="nv">files</span><span class="o">=</span>@document.docx <span class="se">\
</span><span class="se"></span> --form <span class="nv">files</span><span class="o">=</span>@document2.docx <span class="se">\
</span><span class="se"></span> -o result.pdf
</pre>
<h3 class="Heading"><a class="Anchor" aria-hidden="true" id="office.basic.go" href="#office.basic.go">
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
</a>Go</h3>
<pre class="chroma"><span class="kn">import</span> <span class="s">&#34;github.com/thecodingmachine/gotenberg/pkg&#34;</span>
<span class="kd">func</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span>
<span class="nx">c</span> <span class="o">:=</span> <span class="o">&amp;</span><span class="nx">gotenberg</span><span class="p">.</span><span class="nx">Client</span><span class="p">{</span><span class="nx">Hostname</span><span class="p">:</span> <span class="s">&#34;http://localhost:3000&#34;</span><span class="p">}</span>
<span class="nx">req</span><span class="p">,</span> <span class="nx">_</span> <span class="o">:=</span> <span class="nx">gotenberg</span><span class="p">.</span><span class="nf">NewOfficeRequest</span><span class="p">(</span><span class="s">&#34;document.docx&#34;</span><span class="p">,</span> <span class="s">&#34;document2.docx&#34;</span><span class="p">)</span>
<span class="nx">dest</span> <span class="o">:=</span> <span class="s">&#34;result.pdf&#34;</span>
<span class="nx">c</span><span class="p">.</span><span class="nf">Store</span><span class="p">(</span><span class="nx">req</span><span class="p">,</span> <span class="nx">dest</span><span class="p">)</span>
<span class="p">}</span>
</pre>
<h3 class="Heading"><a class="Anchor" aria-hidden="true" id="office.basic.php" href="#office.basic.php">
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
</a>PHP</h3>
<pre class="chroma">use TheCodingMachine\Gotenberg\Client;
use TheCodingMachine\Gotenberg\DocumentFactory;
use TheCodingMachine\Gotenberg\OfficeRequest;
$client = new Client(&#39;http://localhost:3000&#39;, new \Http\Adapter\Guzzle6\Client());
$files = [
DocumentFactory::makeFromPath(&#39;document.docx&#39;, &#39;document.docx&#39;),
DocumentFactory::makeFromPath(&#39;document2.docx&#39;, &#39;document2.docx&#39;),
];
$request = new OfficeRequest($files);
$dest = &#34;result.pdf&#34;;
$client-&gt;store($request, $dest);
</pre>
<h2 class="Heading"><a class="Anchor" aria-hidden="true" id="office.orientation" href="#office.orientation">
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
</a>Orientation</h2>
<p>You may also customize the resulting PDF format.</p>
<p>By default, it will be rendered with <code>portrait</code> orientation.</p>
<h3 class="Heading"><a class="Anchor" aria-hidden="true" id="office.orientation.c_url" href="#office.orientation.c_url">
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
</a>cURL</h3>
<pre class="chroma">$ curl --request POST <span class="se">\
</span><span class="se"></span> --url http://localhost:3000/convert/office <span class="se">\
</span><span class="se"></span> --header <span class="s1">&#39;Content-Type: multipart/form-data&#39;</span> <span class="se">\
</span><span class="se"></span> --form <span class="nv">files</span><span class="o">=</span>@document.docx <span class="se">\
</span><span class="se"></span> --form <span class="nv">landscape</span><span class="o">=</span><span class="nb">true</span> <span class="se">\
</span><span class="se"></span> -o result.pdf
</pre>
<h3 class="Heading"><a class="Anchor" aria-hidden="true" id="office.orientation.go" href="#office.orientation.go">
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
</a>Go</h3>
<pre class="chroma"><span class="kn">import</span> <span class="s">&#34;github.com/thecodingmachine/gotenberg/pkg&#34;</span>
<span class="kd">func</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span>
<span class="nx">c</span> <span class="o">:=</span> <span class="o">&amp;</span><span class="nx">gotenberg</span><span class="p">.</span><span class="nx">Client</span><span class="p">{</span><span class="nx">Hostname</span><span class="p">:</span> <span class="s">&#34;http://localhost:3000&#34;</span><span class="p">}</span>
<span class="nx">req</span><span class="p">,</span> <span class="nx">_</span> <span class="o">:=</span> <span class="nx">gotenberg</span><span class="p">.</span><span class="nf">NewOfficeRequest</span><span class="p">(</span><span class="s">&#34;document.docx&#34;</span><span class="p">)</span>
<span class="nx">req</span><span class="p">.</span><span class="nf">SetLandscape</span><span class="p">(</span><span class="kc">true</span><span class="p">)</span>
<span class="nx">dest</span> <span class="o">:=</span> <span class="s">&#34;result.pdf&#34;</span>
<span class="nx">c</span><span class="p">.</span><span class="nf">Store</span><span class="p">(</span><span class="nx">req</span><span class="p">,</span> <span class="nx">dest</span><span class="p">)</span>
<span class="p">}</span>
</pre>
<h3 class="Heading"><a class="Anchor" aria-hidden="true" id="office.orientation.php" href="#office.orientation.php">
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
</a>PHP</h3>
<pre class="chroma">use TheCodingMachine\Gotenberg\Client;
use TheCodingMachine\Gotenberg\DocumentFactory;
use TheCodingMachine\Gotenberg\OfficeRequest;
$client = new Client(&#39;http://localhost:3000&#39;, new \Http\Adapter\Guzzle6\Client());
$files = [
DocumentFactory::makeFromPath(&#39;document.docx&#39;, &#39;document.docx&#39;),
];
$request = new OfficeRequest($files);
$request-&gt;setLandscape(true);
$dest = &#34;result.pdf&#34;;
$client-&gt;store($request, $dest);
</pre>
</div>
<div class="Page" id="merge">
<h1 class="Heading"><a class="Anchor" aria-hidden="true" id="merge" href="#merge">
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
</a>Merge</h1>
<p>Gotenberg provides the endpoint <code>/merge</code> for merging PDFs.</p>
<p>It accepts <code>POST</code> requests with a <code>multipart/form-data</code> Content-Type.</p>
<h2 class="Heading"><a class="Anchor" aria-hidden="true" id="merge.basic" href="#merge.basic">
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
</a>Basic</h2>
<p>Nothing special here: you may send one or more PDF files and the API
will merge them and return the resulting PDF file.</p>
<h3 class="Heading"><a class="Anchor" aria-hidden="true" id="merge.basic.c_url" href="#merge.basic.c_url">
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
</a>cURL</h3>
<pre class="chroma">$ curl --request POST <span class="se">\
</span><span class="se"></span> --url http://localhost:3000/merge <span class="se">\
</span><span class="se"></span> --header <span class="s1">&#39;Content-Type: multipart/form-data&#39;</span> <span class="se">\
</span><span class="se"></span> --form <span class="nv">files</span><span class="o">=</span>@file.pdf <span class="se">\
</span><span class="se"></span> --form <span class="nv">files</span><span class="o">=</span>@file2.pdf <span class="se">\
</span><span class="se"></span> -o result.pdf
</pre>
<h3 class="Heading"><a class="Anchor" aria-hidden="true" id="merge.basic.go" href="#merge.basic.go">
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
</a>Go</h3>
<pre class="chroma"><span class="kn">import</span> <span class="s">&#34;github.com/thecodingmachine/gotenberg/pkg&#34;</span>
<span class="kd">func</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span>
<span class="nx">c</span> <span class="o">:=</span> <span class="o">&amp;</span><span class="nx">gotenberg</span><span class="p">.</span><span class="nx">Client</span><span class="p">{</span><span class="nx">Hostname</span><span class="p">:</span> <span class="s">&#34;http://localhost:3000&#34;</span><span class="p">}</span>
<span class="nx">req</span><span class="p">,</span> <span class="nx">_</span> <span class="o">:=</span> <span class="nx">gotenberg</span><span class="p">.</span><span class="nf">NewMergeRequest</span><span class="p">(</span><span class="s">&#34;file.pdf&#34;</span><span class="p">,</span> <span class="s">&#34;file2.pdf&#34;</span><span class="p">)</span>
<span class="nx">dest</span> <span class="o">:=</span> <span class="s">&#34;result.pdf&#34;</span>
<span class="nx">c</span><span class="p">.</span><span class="nf">Store</span><span class="p">(</span><span class="nx">req</span><span class="p">,</span> <span class="nx">dest</span><span class="p">)</span>
<span class="p">}</span>
</pre>
<h3 class="Heading"><a class="Anchor" aria-hidden="true" id="merge.basic.php" href="#merge.basic.php">
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
</a>PHP</h3>
<pre class="chroma">use TheCodingMachine\Gotenberg\Client;
use TheCodingMachine\Gotenberg\DocumentFactory;
use TheCodingMachine\Gotenberg\MergeRequest;
$client = new Client(&#39;http://localhost:3000&#39;, new \Http\Adapter\Guzzle6\Client());
$files = [
DocumentFactory::makeFromPath(&#39;file.pdf&#39;, &#39;file.pdf&#39;),
DocumentFactory::makeFromPath(&#39;file2.pdf&#39;, &#39;file2.pdf&#39;),
];
$request = new MergeRequest($files);
$dest = &#34;result.pdf&#34;;
$client-&gt;store($request, $dest);
</pre>
</div>
<div class="Page" id="webhook">
<h1 class="Heading"><a class="Anchor" aria-hidden="true" id="webhook" href="#webhook">
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
</a>Webhook</h1>
<p>All endpoints accept a form field named <code>webhookURL</code>.</p>
<p>If provided, the API will send the resulting PDF file in a <code>POST</code> request with the <code>application/pdf</code> Content-Type
to given URL.</p>
<p>By doing so, your requests to the API will be over before the conversions are actually done!</p>
<h2 class="Heading"><a class="Anchor" aria-hidden="true" id="webhook.examples" href="#webhook.examples">
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
</a>Examples</h2>
<h3 class="Heading"><a class="Anchor" aria-hidden="true" id="webhook.examples.c_url" href="#webhook.examples.c_url">
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
</a>cURL</h3>
<pre class="chroma">$ curl --request POST <span class="se">\
</span><span class="se"></span> --url http://localhost:3000/convert/html <span class="se">\
</span><span class="se"></span> --header <span class="s1">&#39;Content-Type: multipart/form-data&#39;</span> <span class="se">\
</span><span class="se"></span> --form <span class="nv">files</span><span class="o">=</span>@index.html <span class="se">\
</span><span class="se"></span> --form <span class="nv">webhookURL</span><span class="o">=</span><span class="s1">&#39;http://myapp.com/webhook/&#39;</span>
</pre>
<h3 class="Heading"><a class="Anchor" aria-hidden="true" id="webhook.examples.go" href="#webhook.examples.go">
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
</a>Go</h3>
<pre class="chroma"><span class="kn">import</span> <span class="s">&#34;github.com/thecodingmachine/gotenberg/pkg&#34;</span>
<span class="kd">func</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span>
<span class="nx">c</span> <span class="o">:=</span> <span class="o">&amp;</span><span class="nx">gotenberg</span><span class="p">.</span><span class="nx">Client</span><span class="p">{</span><span class="nx">Hostname</span><span class="p">:</span> <span class="s">&#34;http://localhost:3000&#34;</span><span class="p">}</span>
<span class="nx">req</span><span class="p">,</span> <span class="nx">_</span> <span class="o">:=</span> <span class="nx">gotenberg</span><span class="p">.</span><span class="nf">NewHTMLRequest</span><span class="p">(</span><span class="s">&#34;index.html&#34;</span><span class="p">)</span>
<span class="nx">req</span><span class="p">.</span><span class="nf">SetWebhookURL</span><span class="p">(</span><span class="s">&#34;http://myapp.com/webhook/&#34;</span><span class="p">)</span>
<span class="nx">dest</span> <span class="o">:=</span> <span class="s">&#34;result.pdf&#34;</span>
<span class="nx">resp</span><span class="p">,</span> <span class="nx">_</span> <span class="o">:=</span> <span class="nx">c</span><span class="p">.</span><span class="nf">Post</span><span class="p">(</span><span class="nx">req</span><span class="p">)</span>
<span class="p">}</span>
</pre>
<h3 class="Heading"><a class="Anchor" aria-hidden="true" id="webhook.examples.php" href="#webhook.examples.php">
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
</a>PHP</h3>
<pre class="chroma">use TheCodingMachine\Gotenberg\Client;
use TheCodingMachine\Gotenberg\DocumentFactory;
use TheCodingMachine\Gotenberg\HTMLRequest;
$client = new Client(&#39;http://localhost:3000&#39;, new \Http\Adapter\Guzzle6\Client());
$index = DocumentFactory::makeFromPath(&#39;index.html&#39;, &#39;index.html&#39;);
$request = new HTMLRequest($index);
$request-&gt;setWebhookURL(&#39;http://myapp.com/webhook/&#39;);
$resp = $client-&gt;post($request);
</pre>
</div>
<div class="Page" id="scalability">
<h1 class="Heading"><a class="Anchor" aria-hidden="true" id="scalability" href="#scalability">
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
</a>Scalability</h1>
<p>The API being stateless, you may scale it as much as you want.</p>
<p>For instance, using the following Docker Compose file:</p>
<pre class="chroma">version<span class="p">:</span><span class="w"> </span><span class="s1">&#39;3&#39;</span><span class="w">
</span><span class="w">
</span><span class="w"></span>services<span class="p">:</span><span class="w">
</span><span class="w">
</span><span class="w"> </span><span class="c"># your others services</span><span class="w">
</span><span class="w">
</span><span class="w"> </span>gotenberg<span class="p">:</span><span class="w">
</span><span class="w"> </span>image<span class="p">:</span><span class="w"> </span>thecodingmachine/gotenberg<span class="p">:</span><span class="m">4</span><span class="w">
</span></pre>
<p>You may now launch your services using:</p>
<pre class="chroma">$ docker-compose up --scale <span class="nv">gotenberg</span><span class="o">=</span>your_number_of_instances
</pre>
<p>When requesting the Gotenberg service with your client(s), Docker will automatically
redirect a request to a Gotenberg container according to the round-robin strategy.</p>
</div>
<div class="Page" id="liveness">
<h1 class="Heading"><a class="Anchor" aria-hidden="true" id="liveness" href="#liveness">
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
</a>Liveness</h1>
<p>Gotenberg provides the endpoint <code>/ping</code> for checking the API availability with
a simple <code>GET</code> request.</p>
<p>This feature is especially useful for liveness/readiness probes in Kubernetes:</p>
<ul>
<li><a href="https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#container-probes">Pod lifecycle</a></li>
<li><a href="https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-probes/">Configure Liveness and Readiness Probes</a></li>
</ul>
</div>
<div class="Page" id="fonts">
<h1 class="Heading"><a class="Anchor" aria-hidden="true" id="fonts" href="#fonts">
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
</a>Fonts</h1>
<p>By default, a handful of fonts are installed. Asian characters are also supported out of the box.</p>
<p>If you wish to use more fonts, you will have to create your own image:</p>
<pre class="chroma"><span class="k">FROM</span><span class="s"> thecodingmachine/gotenberg:4</span><span class="err">
</span><span class="err">
</span><span class="err"></span><span class="k">RUN</span> apt-get -y install yourfonts<span class="err">
</span></pre>
</div>
<div class="Page" id="links">
<h1 class="Heading"><a class="Anchor" aria-hidden="true" id="links" href="#links">
<svg xmlns="http://www.w3.org/2000/svg" aria-hidden="true" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-link"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"></path><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"></path></svg>
</a>Links</h1>
<ul>
<li>Follow the progress on the <a href="https://github.com/thecodingmachine/gotenberg">GitHub repository</a></li>
<li>Follow <a href="https://twitter.com/gulnap">@gulnap</a> on Twitter</li>
</ul>
<p>Psst: TheCodingMachine is always looking for <a href="https://coders.thecodingmachine.com">talented coders</a>.</p>
</div>
<div class="Footer"></div>
<script src="theme/gotenberg/js/index.js"></script>
</div>
</div>
</div>
</div></body></html>

342
docs/theme/gotenberg/css/index.css vendored Normal file
View File

@@ -0,0 +1,342 @@
:root {
--ease: cubic-bezier(.82, 0, .12, 1);
--width: 800px;
--header-height: 400px;
--tracking: 0.05rem;
--tracking-medium: 0.5rem;
--tracking-large: 0.8rem;
--dark: #000;
--blue: #33A9FF;
--light-gray: #fafafa;
--bg: #fff;
--fg: #868E96;
--fg-dark: #212529;
--selection-bg: var(--blue);
--selection-fg: white;
}
* {
box-sizing: border-box;
}
html, body {
font-family: -apple-system, BlinkMacSystemFont,
"Segoe UI", "Roboto", "Oxygen",
"Ubuntu", "Cantarell", "Fira Sans",
"Droid Sans", "Helvetica Neue", sans-serif;
-webkit-font-smoothing: antialiased;
font-size: 16px;
font-weight: 300;
background: var(--bg);
color: var(--fg-dark);
line-height: 1;
margin: 0;
padding: 0;
}
::selection {
background: var(--selection-bg);
color: var(--selection-fg);
}
h1, h2, h3, h4 {
margin-top: 75px;
margin-bottom: 0;
font-size: 1.2rem;
font-weight: 600;
line-height: 1.5rem;
color: var(--fg-dark);
}
h1 {
margin-top: 0;
font-size: 1.5rem;
}
h1 + p {
font-size: 1.5rem;
line-height: 1.6;
}
h3 {
font-size: 1.0rem;
font-style: italic;
}
a {
color: inherit;
text-decoration: none;
}
p {
margin: 25px 0;
line-height: 1.6;
color: var(--fg-color-light);
}
li strong,
p strong {
color: var(--fg-dark);
font-weight: 500;
}
li a,
p a {
color: var(--fg-dark);
font-weight: 400;
padding-bottom: 3px;
border-bottom: 1px dotted #ddd;
}
li a:hover,
p a:hover {
color: var(--blue);
border-bottom: none;
}
p a:hover {
border-bottom-color: var(--color);
}
ul {
margin: 50px 0 50px 30px;
padding: 0;
}
ul ul {
margin: 10px 0 10px 30px;
}
ul li {
margin: 5px 0;
color: var(--fg-color-light);
line-height: 1.5em;
}
ul li strong {
color: var(--fg-color);
font-weight: 500;
}
pre {
background: var(--light-gray);
color: var(--dark);
padding: 30px;
border-radius: 2px;
overflow: auto;
font: "Source Code Pro", Menlo, monospace;
font-size: .8em;
line-height: 1.5em;
}
blockquote {
margin: 0;
padding: 0 1em;
color: var(--fg-dark);
border-left: 0.25em solid var(--dark);
}
li > code,
p > code {
border: 1px solid #DEE2E6;
font-size: 0.75rem;
padding: 3px 10px;
border-radius: 3px;
white-space: nowrap;
font-weight: 600;
font-family: inherit;
}
details > summary {
cursor: pointer;
outline: none;
user-select: none;
}
details > p {
border-left: 3px solid var(--dark);
padding-left: 15px;
}
.Wrapper {
display: flex;
justify-content: center;
}
.Container {
width: var(--width);
display: flex;
flex-direction: column;
}
.Content-wrapper {
display: flex;
}
.Header {
display: flex;
justify-content: center;
align-items: center;
height: var(--header-height);
}
.Sidebar {
flex: 1 1 auto;
}
.Content {
width: 75%;
}
.Content img {
max-width: 100%;
}
.Menu {
position: -webkit-sticky;
position: sticky;
top: 50px;
color: var(--fg-dark);
}
.Menu > .item {
padding-bottom: 15px;
}
.Menu > .item > a {
position: relative;
user-select: none;
font-weight: 400;
transition: color 200ms;
color: var(--fg);
}
.Menu > .item > a.active {
color: var(--fg-dark);
}
.Menu > .item > a:before {
content: "";
position: absolute;
width: 100%;
height: 1px;
bottom: -5px;
left: 0;
background-color: var(--fg-dark);
visibility: hidden;
transform: scaleX(0);
transform-origin: left center;
transition: all 250ms var(--ease);
}
.Menu > .item > a:hover {
color: var(--fg-dark);
}
.Menu > .item > a:hover:before {
visibility: visible;
transform: scaleX(1);
}
.Page {
margin-top: 100px;
padding-top: 50px;
}
.Page:first-child {
margin-top: 0;
padding-top: 0;
}
.Title {
margin: 5px 0;
line-height: 2.2em;
}
.Title.center {
margin-left: auto;
margin-right: auto;
text-align: center;
max-width: 500px;
}
.Title.margin {
margin-bottom: 80px;
}
.Title > span {
display: block;
}
.Title .subtext {
color: var(--fg-color-light);
font-size: 0.8rem;
text-transform: uppercase;
display: none;
}
.Title .text {
letter-spacing: var(--tracking);
text-transform: uppercase;
font-weight: bold;
font-size: 16px;
}
.Title.small .text {
font-size: 14px;
text-transform: none;
letter-spacing: normal;
line-height: 2rem;
}
.Anchor {
position: relative;
margin-left: -14px;
opacity: 0.15;
display: inline-block;
width: 14px;
height: 14px;
}
.Anchor svg {
position: absolute;
right: 5px;
top: 0;
}
.Anchor svg path {
pointer-events: all
}
.Anchor:hover {
opacity: 1;
}
.Footer {
height: 100px;
}
@media screen and (max-width: 850px) {
html, body {
--header-height: 300px;
--width: 80%;
font-size: 14px;
}
.Content-wrapper {
display: flex;
flex-direction: column;
}
.Sidebar {
margin-bottom: 30px;
}
.Content {
width: 100%;
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
docs/theme/gotenberg/img/favicon-128.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 917 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
docs/theme/gotenberg/img/favicon.ico vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 372 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

46
docs/theme/gotenberg/js/index.js vendored Normal file
View File

@@ -0,0 +1,46 @@
/**
* Setup.
*/
const items = document.querySelectorAll('.Page')
const links = document.querySelectorAll('.Menu a')
/**
* Check if `el` is out out of view.
*/
function isBelowScroll(el) {
return el.getBoundingClientRect().bottom > 0
}
/**
* Activate item `i`.
*/
function activateItem(i) {
links.forEach(e => e.classList.remove('active'))
links[i].classList.add('active')
}
/**
* Activate the correct menu item for the
* contents in the viewport.
*/
function activate() {
let i = 0
for (; i < items.length; i++) {
if (isBelowScroll(items[i])) {
break
}
}
activateItem(i)
}
/**
* Activate scroll spy thingy.
*/
window.addEventListener('scroll', e => activate())

58
docs/theme/gotenberg/views/index.html vendored Normal file
View File

@@ -0,0 +1,58 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{.Title}} &#183; {{.Subtitle}}</title>
<link rel="apple-touch-icon-precomposed" sizes="57x57" href="theme/gotenberg/img/apple-touch-icon-57x57.png" />
<link rel="apple-touch-icon-precomposed" sizes="114x114" href="theme/gotenberg/img/apple-touch-icon-114x114.png" />
<link rel="apple-touch-icon-precomposed" sizes="72x72" href="theme/gotenberg/img/apple-touch-icon-72x72.png" />
<link rel="apple-touch-icon-precomposed" sizes="144x144" href="theme/gotenberg/img/apple-touch-icon-144x144.png" />
<link rel="apple-touch-icon-precomposed" sizes="60x60" href="theme/gotenberg/img/apple-touch-icon-60x60.png" />
<link rel="apple-touch-icon-precomposed" sizes="120x120" href="theme/gotenberg/img/apple-touch-icon-120x120.png" />
<link rel="apple-touch-icon-precomposed" sizes="76x76" href="theme/gotenberg/img/apple-touch-icon-76x76.png" />
<link rel="apple-touch-icon-precomposed" sizes="152x152" href="theme/gotenberg/img/apple-touch-icon-152x152.png" />
<link rel="icon" type="image/png" href="theme/gotenberg/img/favicon-196x196.png" sizes="196x196" />
<link rel="icon" type="image/png" href="theme/gotenberg/img/favicon-96x96.png" sizes="96x96" />
<link rel="icon" type="image/png" href="theme/gotenberg/img/favicon-32x32.png" sizes="32x32" />
<link rel="icon" type="image/png" href="theme/gotenberg/img/favicon-16x16.png" sizes="16x16" />
<link rel="icon" type="image/png" href="theme/gotenberg/img/favicon-128.png" sizes="128x128" />
<link rel="stylesheet" href="theme/gotenberg/css/index.css">
</head>
<body>
<div class="Wrapper">
<div class="Container">
<div class="Header">
<div class="Title center">
<img src="https://user-images.githubusercontent.com/8983173/50009948-84b01e00-ffb8-11e8-850b-fc240382c626.png" alt="Gotenberg logo" width="250" height="250" />
<span class="text">{{.Title}}</span>
<span class="subtext">{{.Subtitle}}</span>
</div>
</div>
<div class="Content-wrapper">
<div class="Sidebar">
<div class="Menu">
{{range .Pages}}
<div class="item">
<a href="#{{.Slug}}">{{.Title}}</a>
</div>
{{end}}
</div>
</div>
<div class="Content">
{{range .Pages}}
<div class="Page" id="{{.Slug}}">
<h1>{{.Title}}</h1>
{{.Content}}
</div>
{{end}}
<div class="Footer"></div>
<script src="theme/gotenberg/js/index.js"></script>
</div>
</div>
</div>
</body>
</html>

21
go.mod Normal file
View File

@@ -0,0 +1,21 @@
module github.com/thecodingmachine/gotenberg
require (
github.com/derekparker/delve v1.1.0
github.com/google/go-cmp v0.2.0 // indirect
github.com/gorilla/websocket v1.4.0 // indirect
github.com/hhrutter/pdfcpu v0.1.19
github.com/labstack/echo v0.0.0-20190126223945-739700bf4ef7
github.com/labstack/gommon v0.2.8
github.com/mafredri/cdp v0.20.0
github.com/microcosm-cc/bluemonday v1.0.1
github.com/pkg/errors v0.8.0 // indirect
github.com/russross/blackfriday/v2 v2.0.1
github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95 // indirect
github.com/stretchr/objx v0.1.1 // indirect
github.com/stretchr/testify v1.2.2
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9 // indirect
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f // indirect
golang.org/x/sys v0.0.0-20181128092732-4ed8d59d0b35 // indirect
)

57
go.sum Normal file
View File

@@ -0,0 +1,57 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/derekparker/delve v1.1.0 h1:icd65nMp7s2HiLz6y/6RCVXBdoED3xxYLwX09EMaRCc=
github.com/derekparker/delve v1.1.0/go.mod h1:pMSZMfp0Nhbm8qdZJkuE/yPGOkLpGXLS1I4poXQpuJU=
github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/hhrutter/pdfcpu v0.1.18 h1:GX3d6tZxmQlmB5X9VUyOtx7+HZZpR+8ISLqBineP6dc=
github.com/hhrutter/pdfcpu v0.1.18/go.mod h1:iaCmXGnOXPIvoGLELZZc4m/GpIopVsvLQ00thrbu8LU=
github.com/hhrutter/pdfcpu v0.1.19 h1:J6GjNFDPjurQWallPW9gmz3CcNqBfvgB7Dad9T3e8Hc=
github.com/hhrutter/pdfcpu v0.1.19/go.mod h1:iaCmXGnOXPIvoGLELZZc4m/GpIopVsvLQ00thrbu8LU=
github.com/labstack/echo v0.0.0-20181123063703-c7eb8da9ec73 h1:oQvA3Xy8274W5Ldgo6Nogy4lX8XO5AaIBsjF5Ggk0IU=
github.com/labstack/echo v0.0.0-20181123063703-c7eb8da9ec73/go.mod h1:rS0D1UPvC8/3sXjhSwEq+K1olh7ipbDhjDWATN2KSgA=
github.com/labstack/echo v0.0.0-20190126223945-739700bf4ef7 h1:kFheQLunLP9dCBrmg3D7y9FD44tlk/nvZQ7JCNvvWEA=
github.com/labstack/echo v0.0.0-20190126223945-739700bf4ef7/go.mod h1:rS0D1UPvC8/3sXjhSwEq+K1olh7ipbDhjDWATN2KSgA=
github.com/labstack/echo v3.3.5+incompatible h1:9PfxPUmasKzeJor9uQTaXLT6WUG/r+vSTmvXxvv3JO4=
github.com/labstack/echo v3.3.5+incompatible/go.mod h1:0INS7j/VjnFxD4E2wkz67b8cVwCLbBmJyDaka6Cmk1s=
github.com/labstack/gommon v0.2.7/go.mod h1:/tj9csK2iPSBvn+3NLM9e52usepMtrd5ilFYA+wQNJ4=
github.com/labstack/gommon v0.2.8 h1:JvRqmeZcfrHC5u6uVleB4NxxNbzx6gpbJiQknDbKQu0=
github.com/labstack/gommon v0.2.8/go.mod h1:/tj9csK2iPSBvn+3NLM9e52usepMtrd5ilFYA+wQNJ4=
github.com/mafredri/cdp v0.20.0 h1:dRq16FwXCAAbolR4W2iyRNsL14Bp6jFHLbRp2EkICVs=
github.com/mafredri/cdp v0.20.0/go.mod h1:hgdiA0yp1uqhSaDOHJWPgXpMbh+LAfUdD9vbN2AM8gE=
github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs=
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
github.com/microcosm-cc/bluemonday v1.0.1 h1:SIYunPjnlXcW+gVfvm0IlSeR5U3WZUOLfVmqg85Go44=
github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=
github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
github.com/pkg/errors v0.8.0/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/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95 h1:/vdW8Cb7EXrkqWGufVMES1OH2sU9gKVb2n9/1y5NMBY=
github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v0.0.0-20170224212429-dcecefd839c4 h1:gKMu1Bf6QINDnvyZuTaACm9ofY+PRh+5vFz4oxBZeF8=
github.com/valyala/fasttemplate v0.0.0-20170224212429-dcecefd839c4/go.mod h1:50wTf68f99/Zt14pr046Tgt3Lp2vLyFZKzbFXTOabXw=
golang.org/x/crypto v0.0.0-20181106171534-e4dc69e5b2fd/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9 h1:mKdxBk7AujPs8kU4m80U72y/zjbZ3UcXC7dClwKbUI0=
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc h1:a3CU5tJYVj92DY2LaA1kUkrsqD5/3mLDhx2NcNqyW+0=
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f h1:Bl/8QSvNqXvPGPGXa2z5xUTmV7VDcZyvRZ+QQXkXTZQ=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181128092732-4ed8d59d0b35 h1:YAFjXN64LMvktoUZH9zgY4lGc/msGN7HQfoSuKCgaDU=
golang.org/x/sys v0.0.0-20181128092732-4ed8d59d0b35/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=

136
internal/app/api/api.go Normal file
View File

@@ -0,0 +1,136 @@
package api
import (
"context"
"fmt"
"net/http"
"os"
"os/signal"
"time"
"github.com/labstack/echo"
"github.com/labstack/echo/middleware"
"github.com/thecodingmachine/gotenberg/internal/pkg/notify"
"github.com/thecodingmachine/gotenberg/internal/pkg/pm2"
"github.com/thecodingmachine/gotenberg/internal/pkg/printer"
"github.com/thecodingmachine/gotenberg/internal/pkg/rand"
)
// Start starts the API server on port 3000.
func Start() error {
e := setup()
// start Chrome headless and
// unoconv listener with PM2.
chrome := &pm2.Chrome{}
unoconv := &pm2.Unoconv{}
if err := chrome.Launch(); err != nil {
return err
}
if err := unoconv.Launch(); err != nil {
return err
}
// run our API in a goroutine so that it doesn't block.
go func() {
notify.Println("http server started on port 3000")
if err := e.Start(":3000"); err != nil {
e.Logger.Fatalf("%v", err)
os.Exit(1)
}
}()
quit := make(chan os.Signal, 1)
// we'll accept graceful shutdowns when quit via SIGINT (Ctrl+C)
// SIGKILL, SIGQUIT or SIGTERM (Ctrl+/) will not be caught.
signal.Notify(quit, os.Interrupt)
// block until we receive our signal.
<-quit
// create a deadline to wait for.
ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
defer cancel()
// doesn't block if no connections, but will otherwise wait
// until the timeout deadline.
notify.Println("shutting down http server... (Ctrl+C to force)")
return e.Shutdown(ctx)
}
func setup() *echo.Echo {
e := echo.New()
e.HideBanner = true
e.HidePort = true
e.Use(middleware.Logger())
e.Use(func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
if err := next(c); err != nil {
// TODO should return a better HTTP status code
// than 500 for some cases.
return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("%v", err))
}
return nil
}
})
e.GET("/ping", func(c echo.Context) error { return nil })
e.POST("/merge", merge)
g := e.Group("/convert")
g.POST("/html", convertHTML)
g.POST("/url", convertURL)
g.POST("/markdown", convertMarkdown)
g.POST("/office", convertOffice)
return e
}
func newContext(r *resource) (context.Context, context.CancelFunc) {
webhookURL := r.webhookURL()
if webhookURL == "" {
ctx, cancel := context.WithTimeout(context.Background(), 120*time.Second)
return ctx, cancel
}
return context.Background(), nil
}
func print(c echo.Context, p printer.Printer, r *resource) error {
baseFilename, err := rand.Get()
if err != nil {
return hijackErr(fmt.Errorf("getting result file name: %v", err), r)
}
filename := fmt.Sprintf("%s.pdf", baseFilename)
fpath := fmt.Sprintf("%s/%s", r.dirPath, filename)
if r.webhookURL() == "" {
defer r.removeAll()
// if no webhook URL given, run conversion
// and directly return the resulting PDF file
// or an error.
if err := p.Print(fpath); err != nil {
return err
}
return c.Attachment(fpath, filename)
}
// as a webhook URL has been given, we
// run the following lines in a goroutine so that
// it doesn't block.
go func() {
defer r.removeAll()
if err := p.Print(fpath); err != nil {
c.Logger().Errorf("%v", err)
return
}
f, err := os.Open(fpath)
if err != nil {
c.Logger().Errorf("%v", err)
return
}
defer f.Close()
resp, err := http.Post(r.webhookURL(), "application/pdf", f)
if err != nil {
c.Logger().Errorf("%v", err)
return
}
defer resp.Body.Close()
}()
return nil
}
func hijackErr(err error, r *resource) error {
if r != nil {
defer r.removeAll()
}
return err
}

View File

@@ -0,0 +1,18 @@
package api
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/labstack/echo"
"github.com/stretchr/testify/assert"
)
func TestPing(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/ping", nil)
rec := httptest.NewRecorder()
e := echo.New()
_ = e.NewContext(req, rec)
assert.Equal(t, http.StatusOK, rec.Code)
}

9
internal/app/api/doc.go Normal file
View File

@@ -0,0 +1,9 @@
/*
Package api starts a HTTP server on port 3000.
It accepts POST requests with a multipart/form-data Content-Type
for converting HTML, Markdown and Office documents to PDF.
It is also able to merge a list of PDF files.
*/
package api

56
internal/app/api/html.go Normal file
View File

@@ -0,0 +1,56 @@
package api
import (
"github.com/labstack/echo"
"github.com/thecodingmachine/gotenberg/internal/pkg/printer"
)
func convertHTML(c echo.Context) error {
r, err := newResource(c)
if err != nil {
return hijackErr(err, r)
}
ctx, cancel := newContext(r)
if cancel != nil {
defer cancel()
}
p := &printer.HTML{Context: ctx}
indexPath, err := r.filePath("index.html")
if err != nil {
return hijackErr(err, r)
}
p.WithLocalURL(indexPath)
headerPath, _ := r.filePath("header.html")
if err := p.WithHeaderFile(headerPath); err != nil {
return hijackErr(err, r)
}
footerPath, _ := r.filePath("footer.html")
if err := p.WithFooterFile(footerPath); err != nil {
return hijackErr(err, r)
}
paperSize, err := r.paperSize()
if err != nil {
return hijackErr(err, r)
}
p.PaperWidth = paperSize[0]
p.PaperHeight = paperSize[1]
paperMargins, err := r.paperMargins()
if err != nil {
return hijackErr(err, r)
}
p.MarginTop = paperMargins[0]
p.MarginBottom = paperMargins[1]
p.MarginLeft = paperMargins[2]
p.MarginRight = paperMargins[3]
landscape, err := r.landscape()
if err != nil {
return hijackErr(err, r)
}
p.Landscape = landscape
chromeWebFontsTimeout, err := r.webFontsTimeout()
if err != nil {
return hijackErr(err, r)
}
p.WebFontsTimeout = chromeWebFontsTimeout
return print(c, p, r)
}

View File

@@ -0,0 +1,24 @@
package api
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/labstack/echo"
"github.com/stretchr/testify/assert"
"github.com/thecodingmachine/gotenberg/test"
)
func TestHTML(t *testing.T) {
body, contentType := test.HTMLTestMultipartForm(t)
req := httptest.NewRequest(http.MethodPost, "/convert/html", body)
req.Header.Set(echo.HeaderContentType, contentType)
rec := httptest.NewRecorder()
e := echo.New()
c := e.NewContext(req, rec)
if assert.NoError(t, convertHTML(c)) {
assert.Equal(t, http.StatusOK, rec.Code)
assert.Equal(t, "application/pdf", rec.Header().Get(echo.HeaderContentType))
}
}

View File

@@ -0,0 +1,55 @@
package api
import (
"github.com/labstack/echo"
"github.com/thecodingmachine/gotenberg/internal/pkg/printer"
)
func convertMarkdown(c echo.Context) error {
r, err := newResource(c)
if err != nil {
return hijackErr(err, r)
}
ctx, cancel := newContext(r)
if cancel != nil {
defer cancel()
}
indexPath, err := r.filePath("index.html")
if err != nil {
return hijackErr(err, r)
}
p := &printer.Markdown{Context: ctx, TemplatePath: indexPath}
headerPath, _ := r.filePath("header.html")
if err := p.WithHeaderFile(headerPath); err != nil {
return hijackErr(err, r)
}
footerPath, _ := r.filePath("footer.html")
if err := p.WithFooterFile(footerPath); err != nil {
return hijackErr(err, r)
}
paperSize, err := r.paperSize()
if err != nil {
return hijackErr(err, r)
}
p.PaperWidth = paperSize[0]
p.PaperHeight = paperSize[1]
paperMargins, err := r.paperMargins()
if err != nil {
return hijackErr(err, r)
}
p.MarginTop = paperMargins[0]
p.MarginBottom = paperMargins[1]
p.MarginLeft = paperMargins[2]
p.MarginRight = paperMargins[3]
landscape, err := r.landscape()
if err != nil {
return hijackErr(err, r)
}
p.Landscape = landscape
chromeWebFontsTimeout, err := r.webFontsTimeout()
if err != nil {
return hijackErr(err, r)
}
p.WebFontsTimeout = chromeWebFontsTimeout
return print(c, p, r)
}

View File

@@ -0,0 +1,24 @@
package api
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/labstack/echo"
"github.com/stretchr/testify/assert"
"github.com/thecodingmachine/gotenberg/test"
)
func TestMarkdown(t *testing.T) {
body, contentType := test.MarkdownTestMultipartForm(t)
req := httptest.NewRequest(http.MethodPost, "/convert/markdown", body)
req.Header.Set(echo.HeaderContentType, contentType)
rec := httptest.NewRecorder()
e := echo.New()
c := e.NewContext(req, rec)
if assert.NoError(t, convertMarkdown(c)) {
assert.Equal(t, http.StatusOK, rec.Code)
assert.Equal(t, "application/pdf", rec.Header().Get(echo.HeaderContentType))
}
}

65
internal/app/api/merge.go Normal file
View File

@@ -0,0 +1,65 @@
package api
import (
"errors"
"fmt"
"net/http"
"os"
"github.com/labstack/echo"
"github.com/thecodingmachine/gotenberg/internal/pkg/printer"
"github.com/thecodingmachine/gotenberg/internal/pkg/rand"
)
func merge(c echo.Context) error {
r, err := newResource(c)
if err != nil {
return hijackErr(err, r)
}
fpaths, err := r.filePaths([]string{".pdf"})
if err != nil {
return hijackErr(err, r)
}
if len(fpaths) == 0 {
return hijackErr(errors.New("no suitable PDF files to merge"), r)
}
baseFilename, err := rand.Get()
if err != nil {
return hijackErr(fmt.Errorf("getting result file name: %v", err), r)
}
filename := fmt.Sprintf("%s.pdf", baseFilename)
fpath := fmt.Sprintf("%s/%s", r.dirPath, filename)
if r.webhookURL() == "" {
defer r.removeAll()
// if no webhook URL given, run merge
// and directly return the resulting PDF file
// or an error.
if err := printer.Merge(fpaths, fpath); err != nil {
return err
}
return c.Attachment(fpath, filename)
}
// as a webhook URL has been given, we
// run the following lines in a goroutine so that
// it doesn't block.
go func() {
defer r.removeAll()
if err := printer.Merge(fpaths, fpath); err != nil {
c.Logger().Errorf("%v", err)
return
}
f, err := os.Open(fpath)
if err != nil {
c.Logger().Errorf("%v", err)
return
}
defer f.Close()
resp, err := http.Post(r.webhookURL(), "application/pdf", f)
if err != nil {
c.Logger().Errorf("%v", err)
return
}
defer resp.Body.Close()
}()
return nil
}

View File

@@ -0,0 +1,24 @@
package api
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/labstack/echo"
"github.com/stretchr/testify/assert"
"github.com/thecodingmachine/gotenberg/test"
)
func TestMerge(t *testing.T) {
body, contentType := test.PDFTestMultipartForm(t)
req := httptest.NewRequest(http.MethodPost, "/merge", body)
req.Header.Set(echo.HeaderContentType, contentType)
rec := httptest.NewRecorder()
e := echo.New()
c := e.NewContext(req, rec)
if assert.NoError(t, merge(c)) {
assert.Equal(t, http.StatusOK, rec.Code)
assert.Equal(t, "application/pdf", rec.Header().Get(echo.HeaderContentType))
}
}

Some files were not shown because too many files have changed in this diff Show More