├── .github ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .travis.yml ├── LICENSE ├── Makefile ├── README.md ├── build ├── lint │ └── Dockerfile └── tests │ ├── Dockerfile │ └── docker-entrypoint.sh ├── chrome.go ├── client.go ├── doc.go ├── document.go ├── go.mod ├── go.sum ├── html.go ├── html_test.go ├── markdown.go ├── markdown_test.go ├── merge.go ├── merge_test.go ├── office.go ├── office_test.go ├── test ├── testdata │ ├── html │ │ ├── font.woff │ │ ├── footer.html │ │ ├── header.html │ │ ├── img.gif │ │ ├── index.html │ │ └── style.css │ ├── markdown │ │ ├── font.woff │ │ ├── footer.html │ │ ├── header.html │ │ ├── img.gif │ │ ├── index.html │ │ ├── paragraph1.md │ │ ├── paragraph2.md │ │ ├── paragraph3.md │ │ └── style.css │ ├── office │ │ ├── document.docx │ │ ├── document.rtf │ │ └── document.txt │ ├── pdf │ │ ├── gotenberg.pdf │ │ └── gotenberg_bis.pdf │ └── url │ │ ├── footer.html │ │ └── header.html └── testfunc.go ├── url.go └── url_test.go /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at neuhart.julien@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Hi! Thank you for considering contributing to Gotenberg Go client. You'll 4 | find below useful information about how to contribute to the Gotenberg project. 5 | 6 | ## Contributing code 7 | 8 | ### Install from sources 9 | 10 | 1. Install and run the latest version of Docker 11 | 2. Verify your Go version (>= 1.12) 12 | 3. Fork this repository 13 | 4. Clone it outside of your `GOPATH` (we're using Go modules) 14 | 15 | ### Working with git 16 | 17 | 1. Create your feature branch (`git checkout -b my-new-feature`) 18 | 2. Commit your changes (`git commit -am 'Add some feature'`) 19 | 3. Push to the branch (`git push origin my-new-feature`) 20 | 4. Create a new pull request 21 | 22 | ### Testing 23 | 24 | 1. Run all linters (`make lint`) 25 | 2. Run all tests (`make tests`) 26 | 27 | ## Reporting bugs and feature request 28 | 29 | Your issue or feature request may already be reported! 30 | Please search on the [issue tracker](../../../issues) before creating one. 31 | 32 | If you do not find any relevant issue or feature request, feel free to 33 | add a new one! 34 | 35 | ## Additional resources 36 | 37 | * [Code of conduct](CODE_OF_CONDUCT.md) 38 | * [Issue template](ISSUE_TEMPLATE.md) 39 | * [Pull request template](PULL_REQUEST_TEMPLATE.md) -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Your issue may already be reported! 2 | Please search on the [issue tracker](../../../issues) before creating one. 3 | 4 | ## Expected Behavior 5 | 6 | 7 | 8 | ## Current Behavior 9 | 10 | 11 | 12 | ## Possible Solution 13 | 14 | 15 | 16 | ## Steps to Reproduce (for bugs) 17 | 18 | 19 | 1. 20 | 2. 21 | 3. 22 | 4. 23 | 24 | ## Context 25 | 26 | 27 | 28 | ## Your Environment 29 | 30 | * Version used: 31 | * Operating System and version: 32 | * Link to your project: -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | A similar PR may already be submitted! 2 | Please search among the [pull requests](../../../pulls) before creating one. 3 | 4 | Thanks for submitting a pull request! Please provide enough information so that others can review your pull request: 5 | 6 | For more information, see the [CONTRIBUTING](CONTRIBUTING.md) guide. 7 | 8 | **Summary** 9 | 10 | 11 | 12 | This PR fixes/implements the following **bugs/features** 13 | 14 | * [ ] Bug 1 15 | * [ ] Bug 2 16 | * [ ] Feature 1 17 | * [ ] Feature 2 18 | * [ ] Breaking changes 19 | 20 | 21 | 22 | Explain the **motivation** for making this change. What existing problem does the pull request solve? 23 | 24 | 25 | 26 | **Test plan (required)** 27 | 28 | Demonstrate the code is solid. Example: The exact commands you ran and their output. 29 | 30 | 31 | 32 | **Closing issues** 33 | 34 | 35 | Fixes # 36 | 37 | **Checklist** 38 | 39 | - [ ] Have you followed the guidelines in our [CONTRIBUTING](CONTRIBUTING.md) guide? 40 | - [ ] Have you lint your code locally prior to submission (`make lint`)? 41 | - [ ] Have you written new tests for your core changes, as applicable? 42 | - [ ] Have you successfully ran tests with your changes locally (`make tests`)? 43 | - [ ] I have squashed any insignificant commits 44 | - [ ] This change has comments for package types, values, functions, and non-obvious lines of code -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | 3 | language: generic 4 | 5 | services: 6 | - docker 7 | 8 | stages: 9 | - tests 10 | 11 | jobs: 12 | include: 13 | - stage: tests 14 | script: make lint 15 | - stage: tests 16 | script: make tests -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 TheCodingMachine 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | GOLANG_VERSION=1.13 2 | GOTENBERG_VERSION=6 3 | GOTENBERG_LOG_LEVEL=ERROR 4 | VERSION=snapshot 5 | GOLANGCI_LINT_VERSION=1.20.1 6 | 7 | # gofmt and goimports all go files. 8 | fmt: 9 | go fmt ./... 10 | go mod tidy 11 | 12 | # run all linters. 13 | lint: 14 | docker build --build-arg GOLANG_VERSION=$(GOLANG_VERSION) --build-arg GOLANGCI_LINT_VERSION=$(GOLANGCI_LINT_VERSION) -t thecodingmachine/gotenberg-go-client:lint -f build/lint/Dockerfile . 15 | docker run --rm -it -v "$(PWD):/lint" thecodingmachine/gotenberg-go-client:lint 16 | 17 | # run all tests. 18 | tests: 19 | docker build --build-arg GOLANG_VERSION=$(GOLANG_VERSION) --build-arg GOTENBERG_VERSION=$(GOTENBERG_VERSION) --build-arg GOTENBERG_LOG_LEVEL=$(GOTENBERG_LOG_LEVEL) -t thecodingmachine/gotenberg-go-client:tests -f build/tests/Dockerfile . 20 | docker run --rm -it -v "$(PWD):/tests" thecodingmachine/gotenberg-go-client:tests -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | **⚠️ Not working for Gotenberg >= 7 ⚠️** 2 | 3 | # Gotenberg Go client 4 | 5 | A simple Go client for interacting with a Gotenberg API. 6 | 7 | ## Install 8 | 9 | ```bash 10 | $ go get -u github.com/thecodingmachine/gotenberg-go-client/v7 11 | ``` 12 | 13 | ## Usage 14 | 15 | ```golang 16 | import ( 17 | "time" 18 | "net/http" 19 | 20 | "github.com/thecodingmachine/gotenberg-go-client/v7" 21 | ) 22 | 23 | // create the client. 24 | client := &gotenberg.Client{Hostname: "http://localhost:3000"} 25 | // ... or use your own *http.Client. 26 | httpClient := &http.Client{ 27 | Timeout: time.Duration(5) * time.Second, 28 | } 29 | client := &gotenberg.Client{Hostname: "http://localhost:3000", HTTPClient: httpClient} 30 | 31 | // prepare the files required for your conversion. 32 | 33 | // from a path. 34 | index, _ := gotenberg.NewDocumentFromPath("index.html", "/path/to/file") 35 | // ... or from a string. 36 | index, _ := gotenberg.NewDocumentFromString("index.html", "Foo") 37 | // ... or from bytes. 38 | index, _ := gotenberg.NewDocumentFromBytes("index.html", []byte("Foo")) 39 | 40 | header, _ := gotenberg.NewDocumentFromPath("header.html", "/path/to/file") 41 | footer, _ := gotenberg.NewDocumentFromPath("footer.html", "/path/to/file") 42 | style, _ := gotenberg.NewDocumentFromPath("style.css", "/path/to/file") 43 | img, _ := gotenberg.NewDocumentFromPath("img.png", "/path/to/file") 44 | 45 | req := gotenberg.NewHTMLRequest(index) 46 | req.Header(header) 47 | req.Footer(footer) 48 | req.Assets(style, img) 49 | req.PaperSize(gotenberg.A4) 50 | req.Margins(gotenberg.NoMargins) 51 | req.Scale(0.75) 52 | 53 | // store method allows you to... store the resulting PDF in a particular destination. 54 | client.Store(req, "path/you/want/the/pdf/to/be/stored.pdf") 55 | 56 | // if you wish to redirect the response directly to the browser, you may also use: 57 | resp, _ := client.Post(req) 58 | ``` 59 | 60 | For more complete usages, head to the [documentation](https://gotenberg.dev/). 61 | 62 | ## Badges 63 | 64 | [](https://travis-ci.org/thecodingmachine/gotenberg-go-client) 65 | [](https://godoc.org/github.com/thecodingmachine/gotenberg-go-client) 66 | [](https://goreportcard.com/report/thecodingmachine/gotenberg-go-client) 67 | -------------------------------------------------------------------------------- /build/lint/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG GOLANG_VERSION 2 | 3 | FROM golang:${GOLANG_VERSION}-stretch 4 | 5 | # |-------------------------------------------------------------------------- 6 | # | GolangCI-Lint 7 | # |-------------------------------------------------------------------------- 8 | # | 9 | # | Installs GolangCI-Lint, a linters Runner for Go. 5x faster 10 | # | than gometalinter. 11 | # | 12 | 13 | ARG GOLANGCI_LINT_VERSION 14 | 15 | RUN curl -sfL https://install.goreleaser.com/github.com/golangci/golangci-lint.sh | sh -s -- -b /usr/local/bin v${GOLANGCI_LINT_VERSION} &&\ 16 | golangci-lint --version 17 | 18 | # |-------------------------------------------------------------------------- 19 | # | Final touch 20 | # |-------------------------------------------------------------------------- 21 | # | 22 | # | Last instructions of this build. 23 | # | 24 | 25 | # Define our workding outside of $GOPATH (we're using go modules). 26 | WORKDIR /lint 27 | 28 | # Copy our module dependencies definitions. 29 | COPY go.mod . 30 | COPY go.sum . 31 | 32 | # Install module dependencies. 33 | RUN go mod download 34 | 35 | CMD [ "golangci-lint", "run" ,"--tests=false", "--enable-all", "--disable=dupl", "--disable=wsl" ] -------------------------------------------------------------------------------- /build/tests/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG GOLANG_VERSION 2 | ARG GOTENBERG_VERSION 3 | 4 | FROM golang:${GOLANG_VERSION}-stretch AS golang 5 | 6 | FROM thecodingmachine/gotenberg:${GOTENBERG_VERSION} 7 | 8 | USER root 9 | 10 | # |-------------------------------------------------------------------------- 11 | # | Common libraries 12 | # |-------------------------------------------------------------------------- 13 | # | 14 | # | Libraries used in the build process of this image. 15 | # | 16 | 17 | RUN apt-get update &&\ 18 | apt-get install -y git gcc 19 | 20 | # |-------------------------------------------------------------------------- 21 | # | Golang 22 | # |-------------------------------------------------------------------------- 23 | # | 24 | # | Installs Golang. 25 | # | 26 | 27 | COPY --from=golang /usr/local/go /usr/local/go 28 | 29 | RUN export PATH="/usr/local/go/bin:$PATH" &&\ 30 | go version 31 | 32 | ENV GOPATH /go 33 | ENV PATH $GOPATH/bin:/usr/local/go/bin:$PATH 34 | 35 | # |-------------------------------------------------------------------------- 36 | # | Final touch 37 | # |-------------------------------------------------------------------------- 38 | # | 39 | # | Last instructions of this build. 40 | # | 41 | 42 | ARG GOTENBERG_LOG_LEVEL 43 | 44 | ENV LOG_LEVEL=${GOTENBERG_LOG_LEVEL} 45 | 46 | # Define our workding outside of $GOPATH (we're using go modules). 47 | WORKDIR /tests 48 | 49 | # Copy our module dependencies definitions. 50 | COPY go.mod . 51 | COPY go.sum . 52 | 53 | # Install module dependencies. 54 | RUN go mod download 55 | 56 | ENTRYPOINT [ "build/tests/docker-entrypoint.sh" ] -------------------------------------------------------------------------------- /build/tests/docker-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -xe 4 | 5 | # Testing Go client. 6 | gotenberg & 7 | sleep 10 8 | go test -race -cover -covermode=atomic github.com/thecodingmachine/gotenberg-go-client/v7 9 | sleep 10 # allows Gotenberg to remove generated files. -------------------------------------------------------------------------------- /chrome.go: -------------------------------------------------------------------------------- 1 | package gotenberg 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | ) 7 | 8 | const ( 9 | waitDelay string = "waitDelay" 10 | paperWidth string = "paperWidth" 11 | paperHeight string = "paperHeight" 12 | marginTop string = "marginTop" 13 | marginBottom string = "marginBottom" 14 | marginLeft string = "marginLeft" 15 | marginRight string = "marginRight" 16 | landscapeChrome string = "landscape" 17 | pageRanges string = "pageRanges" 18 | googleChromeRpccBufferSize string = "googleChromeRpccBufferSize" 19 | scale string = "scale" 20 | ) 21 | 22 | // nolint: gochecknoglobals 23 | var ( 24 | // A3 paper size. 25 | A3 = [2]float64{11.7, 16.5} 26 | // A4 paper size. 27 | A4 = [2]float64{8.27, 11.7} 28 | // A5 paper size. 29 | A5 = [2]float64{5.8, 8.3} 30 | // A6 paper size. 31 | A6 = [2]float64{4.1, 5.8} 32 | // Letter paper size. 33 | Letter = [2]float64{8.5, 11} 34 | // Legal paper size. 35 | Legal = [2]float64{8.5, 14} 36 | // Tabloid paper size. 37 | Tabloid = [2]float64{11, 17} 38 | ) 39 | 40 | // nolint: gochecknoglobals 41 | var ( 42 | // NoMargins removes margins. 43 | NoMargins = [4]float64{0, 0, 0, 0} 44 | // NormalMargins uses 1 inche margins. 45 | NormalMargins = [4]float64{1, 1, 1, 1} 46 | // LargeMargins uses 2 inche margins. 47 | LargeMargins = [4]float64{2, 2, 2, 2} 48 | ) 49 | 50 | type chromeRequest struct { 51 | header Document 52 | footer Document 53 | 54 | *request 55 | } 56 | 57 | func newChromeRequest() *chromeRequest { 58 | return &chromeRequest{nil, nil, newRequest()} 59 | } 60 | 61 | // WaitDelay sets waitDelay form field. 62 | func (req *chromeRequest) WaitDelay(delay float64) { 63 | req.values[waitDelay] = strconv.FormatFloat(delay, 'f', 2, 64) 64 | } 65 | 66 | // Header sets header form file. 67 | func (req *chromeRequest) Header(header Document) { 68 | req.header = header 69 | } 70 | 71 | // Footer sets footer form file. 72 | func (req *chromeRequest) Footer(footer Document) { 73 | req.footer = footer 74 | } 75 | 76 | // PaperSize sets paperWidth and paperHeight form fields. 77 | func (req *chromeRequest) PaperSize(size [2]float64) { 78 | req.values[paperWidth] = fmt.Sprintf("%f", size[0]) 79 | req.values[paperHeight] = fmt.Sprintf("%f", size[1]) 80 | } 81 | 82 | // Margins sets marginTop, marginBottom, 83 | // marginLeft and marginRight form fields. 84 | func (req *chromeRequest) Margins(margins [4]float64) { 85 | req.values[marginTop] = fmt.Sprintf("%f", margins[0]) 86 | req.values[marginBottom] = fmt.Sprintf("%f", margins[1]) 87 | req.values[marginLeft] = fmt.Sprintf("%f", margins[2]) 88 | req.values[marginRight] = fmt.Sprintf("%f", margins[3]) 89 | } 90 | 91 | // Landscape sets landscape form field. 92 | func (req *chromeRequest) Landscape(isLandscape bool) { 93 | req.values[landscapeChrome] = strconv.FormatBool(isLandscape) 94 | } 95 | 96 | // PageRanges sets pageRanges form field. 97 | func (req *chromeRequest) PageRanges(ranges string) { 98 | req.values[pageRanges] = ranges 99 | } 100 | 101 | // GoogleChromeRpccBufferSize sets googleChromeRpccBufferSize form field. 102 | func (req *chromeRequest) GoogleChromeRpccBufferSize(bufferSize int64) { 103 | req.values[googleChromeRpccBufferSize] = strconv.FormatInt(bufferSize, 10) 104 | } 105 | 106 | // Scale sets scale form field 107 | func (req *chromeRequest) Scale(scaleFactor float64) { 108 | req.values[scale] = fmt.Sprintf("%f", scaleFactor) 109 | } 110 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | package gotenberg 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "mime/multipart" 10 | "net/http" 11 | "os" 12 | "path/filepath" 13 | "runtime" 14 | "strconv" 15 | ) 16 | 17 | const ( 18 | resultFilename string = "resultFilename" 19 | waitTimeout string = "waitTimeout" 20 | webhookURL string = "webhookURL" 21 | webhookURLTimeout string = "webhookURLTimeout" 22 | webhookURLBaseHTTPHeaderKey string = "Gotenberg-Webhookurl-" 23 | ) 24 | 25 | // Client facilitates interacting with 26 | // the Gotenberg API. 27 | type Client struct { 28 | Hostname string 29 | HTTPClient *http.Client 30 | } 31 | 32 | // Request is a type for sending 33 | // form values and form files to 34 | // the Gotenberg API. 35 | type Request interface { 36 | postURL() string 37 | customHTTPHeaders() map[string]string 38 | formValues() map[string]string 39 | formFiles() map[string]Document 40 | } 41 | 42 | type request struct { 43 | httpHeaders map[string]string 44 | values map[string]string 45 | } 46 | 47 | func newRequest() *request { 48 | return &request{ 49 | httpHeaders: make(map[string]string), 50 | values: make(map[string]string), 51 | } 52 | } 53 | 54 | // ResultFilename sets resultFilename form field. 55 | func (req *request) ResultFilename(filename string) { 56 | req.values[resultFilename] = filename 57 | } 58 | 59 | // WaitTimeout sets waitTimeout form field. 60 | func (req *request) WaitTimeout(timeout float64) { 61 | req.values[waitTimeout] = strconv.FormatFloat(timeout, 'f', 2, 64) 62 | } 63 | 64 | // WebhookURL sets webhookURL form field. 65 | func (req *request) WebhookURL(url string) { 66 | req.values[webhookURL] = url 67 | } 68 | 69 | // WebhookURLTimeout sets webhookURLTimeout form field. 70 | func (req *request) WebhookURLTimeout(timeout float64) { 71 | req.values[webhookURLTimeout] = strconv.FormatFloat(timeout, 'f', 2, 64) 72 | } 73 | 74 | // AddWebhookURLHTTPHeader add a webhook custom HTTP header. 75 | func (req *request) AddWebhookURLHTTPHeader(key, value string) { 76 | key = fmt.Sprintf("%s%s", webhookURLBaseHTTPHeaderKey, key) 77 | req.httpHeaders[key] = value 78 | } 79 | 80 | func (req *request) customHTTPHeaders() map[string]string { 81 | return req.httpHeaders 82 | } 83 | 84 | func (req *request) formValues() map[string]string { 85 | return req.values 86 | } 87 | 88 | // Post sends a request to the Gotenberg API 89 | // and returns the response. 90 | func (c *Client) Post(req Request) (*http.Response, error) { 91 | return c.PostContext(context.Background(), req) 92 | } 93 | 94 | // PostContext sends a request to the Gotenberg API 95 | // and returns the response. 96 | // The created HTTP request can be canceled by the passed context. 97 | func (c *Client) PostContext(ctx context.Context, req Request) (*http.Response, error) { 98 | body, contentType, err := multipartForm(req) 99 | if err != nil { 100 | return nil, err 101 | } 102 | if c.HTTPClient == nil { 103 | c.HTTPClient = &http.Client{} 104 | } 105 | URL := fmt.Sprintf("%s%s", c.Hostname, req.postURL()) 106 | httpReq, err := http.NewRequestWithContext(ctx, http.MethodPost, URL, body) 107 | if err != nil { 108 | return nil, err 109 | } 110 | httpReq.Header.Set("Content-Type", contentType) 111 | for key, value := range req.customHTTPHeaders() { 112 | httpReq.Header.Set(key, value) 113 | } 114 | resp, err := c.HTTPClient.Do(httpReq) /* #nosec */ 115 | if err != nil { 116 | return nil, err 117 | } 118 | return resp, nil 119 | } 120 | 121 | // Store creates the resulting PDF to given destination. 122 | func (c *Client) Store(req Request, dest string) error { 123 | return c.StoreContext(context.Background(), req, dest) 124 | } 125 | 126 | // StoreContext creates the resulting PDF to given destination. 127 | // The created HTTP request can be canceled by the passed context. 128 | func (c *Client) StoreContext(ctx context.Context, req Request, dest string) error { 129 | if hasWebhook(req) { 130 | return errors.New("cannot use Store method with a webhook") 131 | } 132 | resp, err := c.PostContext(ctx, req) 133 | if err != nil { 134 | return err 135 | } 136 | defer resp.Body.Close() 137 | 138 | if resp.StatusCode != http.StatusOK { 139 | return errors.New("failed to generate the result PDF") 140 | } 141 | return writeNewFile(dest, resp.Body) 142 | } 143 | 144 | func hasWebhook(req Request) bool { 145 | webhookURL, ok := req.formValues()[webhookURL] 146 | if !ok { 147 | return false 148 | } 149 | return webhookURL != "" 150 | } 151 | 152 | func writeNewFile(fpath string, in io.Reader) error { 153 | if err := os.MkdirAll(filepath.Dir(fpath), 0755); err != nil { 154 | return fmt.Errorf("%s: making directory for file: %v", fpath, err) 155 | } 156 | out, err := os.Create(fpath) 157 | if err != nil { 158 | return fmt.Errorf("%s: creating new file: %v", fpath, err) 159 | } 160 | defer out.Close() // nolint: errcheck 161 | err = out.Chmod(0644) 162 | if err != nil && runtime.GOOS != "windows" { 163 | return fmt.Errorf("%s: changing file mode: %v", fpath, err) 164 | } 165 | _, err = io.Copy(out, in) 166 | if err != nil { 167 | return fmt.Errorf("%s: writing file: %v", fpath, err) 168 | } 169 | return nil 170 | } 171 | 172 | func fileExists(name string) bool { 173 | _, err := os.Stat(name) 174 | return !os.IsNotExist(err) 175 | } 176 | 177 | func multipartForm(req Request) (*bytes.Buffer, string, error) { 178 | body := &bytes.Buffer{} 179 | writer := multipart.NewWriter(body) 180 | defer writer.Close() // nolint: errcheck 181 | for filename, document := range req.formFiles() { 182 | in, err := document.Reader() 183 | if err != nil { 184 | return nil, "", fmt.Errorf("%s: creating reader: %v", filename, err) 185 | } 186 | defer in.Close() // nolint: errcheck 187 | part, err := writer.CreateFormFile("files", filename) 188 | if err != nil { 189 | return nil, "", fmt.Errorf("%s: creating form file: %v", filename, err) 190 | } 191 | _, err = io.Copy(part, in) 192 | if err != nil { 193 | return nil, "", fmt.Errorf("%s: copying data: %v", filename, err) 194 | } 195 | } 196 | for name, value := range req.formValues() { 197 | if err := writer.WriteField(name, value); err != nil { 198 | return nil, "", fmt.Errorf("%s: writing form field: %v", name, err) 199 | } 200 | } 201 | return body, writer.FormDataContentType(), nil 202 | } 203 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package gotenberg is a Go client for 3 | interacting with a Gotenberg API. 4 | 5 | For more complete usages, head to the documentation: 6 | https://thecodingmachine.github.io/gotenberg/ 7 | */ 8 | package gotenberg 9 | -------------------------------------------------------------------------------- /document.go: -------------------------------------------------------------------------------- 1 | package gotenberg 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "io" 7 | "io/ioutil" 8 | "os" 9 | "strings" 10 | ) 11 | 12 | // Document reprents a file which 13 | // will be send to the Gotenberg API. 14 | type Document interface { 15 | Filename() string 16 | Reader() (io.ReadCloser, error) 17 | } 18 | 19 | type document struct { 20 | filename string 21 | } 22 | 23 | func (doc *document) Filename() string { 24 | return doc.filename 25 | } 26 | 27 | type documentFromPath struct { 28 | fpath string 29 | 30 | *document 31 | } 32 | 33 | // NewDocumentFromPath creates a Document from 34 | // a file path. 35 | func NewDocumentFromPath(filename, fpath string) (Document, error) { 36 | if !fileExists(fpath) { 37 | return nil, fmt.Errorf("%s: file %s does not exist", fpath, filename) 38 | } 39 | return &documentFromPath{ 40 | fpath, 41 | &document{filename}, 42 | }, nil 43 | } 44 | 45 | func (doc *documentFromPath) Reader() (io.ReadCloser, error) { 46 | in, err := os.Open(doc.fpath) 47 | if err != nil { 48 | return nil, fmt.Errorf("%s: opening file: %v", doc.Filename(), err) 49 | } 50 | return in, nil 51 | } 52 | 53 | type documentFromString struct { 54 | data string 55 | 56 | *document 57 | } 58 | 59 | // NewDocumentFromString creates a Document from 60 | // a string. 61 | func NewDocumentFromString(filename, data string) (Document, error) { 62 | if len(data) == 0 { 63 | return nil, fmt.Errorf("%s: string is empty", filename) 64 | } 65 | return &documentFromString{ 66 | data, 67 | &document{filename}, 68 | }, nil 69 | } 70 | 71 | func (doc *documentFromString) Reader() (io.ReadCloser, error) { 72 | return ioutil.NopCloser(strings.NewReader(doc.data)), nil 73 | } 74 | 75 | type documentFromBytes struct { 76 | data []byte 77 | 78 | *document 79 | } 80 | 81 | // NewDocumentFromBytes creates a Document from 82 | // bytes. 83 | func NewDocumentFromBytes(filename string, data []byte) (Document, error) { 84 | if len(data) == 0 { 85 | return nil, fmt.Errorf("%s: bytes are empty", filename) 86 | } 87 | return &documentFromBytes{ 88 | data, 89 | &document{filename}, 90 | }, nil 91 | } 92 | 93 | func (doc *documentFromBytes) Reader() (io.ReadCloser, error) { 94 | return ioutil.NopCloser(bytes.NewReader(doc.data)), nil 95 | } 96 | 97 | // Compile-time checks to ensure type implements desired interfaces. 98 | var ( 99 | _ = Document(new(documentFromPath)) 100 | _ = Document(new(documentFromString)) 101 | _ = Document(new(documentFromBytes)) 102 | ) 103 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/thecodingmachine/gotenberg-go-client/v7 2 | 3 | go 1.13 4 | 5 | require github.com/stretchr/testify v1.3.0 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 4 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 5 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 6 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 7 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 8 | -------------------------------------------------------------------------------- /html.go: -------------------------------------------------------------------------------- 1 | package gotenberg 2 | 3 | // HTMLRequest facilitates HTML conversion 4 | // with the Gotenberg API. 5 | type HTMLRequest struct { 6 | index Document 7 | assets []Document 8 | 9 | *chromeRequest 10 | } 11 | 12 | // NewHTMLRequest create HTMLRequest. 13 | func NewHTMLRequest(index Document) *HTMLRequest { 14 | return &HTMLRequest{index, []Document{}, newChromeRequest()} 15 | } 16 | 17 | // Assets sets assets form files. 18 | func (req *HTMLRequest) Assets(assets ...Document) { 19 | req.assets = assets 20 | } 21 | 22 | func (req *HTMLRequest) postURL() string { 23 | return "/convert/html" 24 | } 25 | 26 | func (req *HTMLRequest) formFiles() map[string]Document { 27 | files := make(map[string]Document) 28 | files["index.html"] = req.index 29 | if req.header != nil { 30 | files["header.html"] = req.header 31 | } 32 | if req.footer != nil { 33 | files["footer.html"] = req.footer 34 | } 35 | for _, asset := range req.assets { 36 | files[asset.Filename()] = asset 37 | } 38 | return files 39 | } 40 | 41 | // Compile-time checks to ensure type implements desired interfaces. 42 | var ( 43 | _ = Request(new(HTMLRequest)) 44 | ) 45 | -------------------------------------------------------------------------------- /html_test.go: -------------------------------------------------------------------------------- 1 | package gotenberg 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | "github.com/thecodingmachine/gotenberg-go-client/v7/test" 11 | ) 12 | 13 | func TestHTML(t *testing.T) { 14 | c := &Client{Hostname: "http://localhost:3000"} 15 | index, err := NewDocumentFromPath("index.html", test.HTMLTestFilePath(t, "index.html")) 16 | require.Nil(t, err) 17 | req := NewHTMLRequest(index) 18 | dirPath, err := test.Rand() 19 | require.Nil(t, err) 20 | dest := fmt.Sprintf("%s/foo.pdf", dirPath) 21 | err = c.Store(req, dest) 22 | assert.Nil(t, err) 23 | assert.FileExists(t, dest) 24 | err = os.RemoveAll(dirPath) 25 | assert.Nil(t, err) 26 | } 27 | 28 | func TestHTMLFromString(t *testing.T) { 29 | c := &Client{Hostname: "http://localhost:3000"} 30 | index, err := NewDocumentFromString("index.html", "Foo") 31 | req := NewHTMLRequest(index) 32 | dirPath, err := test.Rand() 33 | require.Nil(t, err) 34 | dest := fmt.Sprintf("%s/foo.pdf", dirPath) 35 | err = c.Store(req, dest) 36 | assert.Nil(t, err) 37 | assert.FileExists(t, dest) 38 | err = os.RemoveAll(dirPath) 39 | assert.Nil(t, err) 40 | } 41 | 42 | func TestHTMLFromBytes(t *testing.T) { 43 | c := &Client{Hostname: "http://localhost:3000"} 44 | index, err := NewDocumentFromBytes("index.html", []byte("Foo")) 45 | req := NewHTMLRequest(index) 46 | dirPath, err := test.Rand() 47 | require.Nil(t, err) 48 | dest := fmt.Sprintf("%s/foo.pdf", dirPath) 49 | err = c.Store(req, dest) 50 | assert.Nil(t, err) 51 | assert.FileExists(t, dest) 52 | err = os.RemoveAll(dirPath) 53 | assert.Nil(t, err) 54 | } 55 | 56 | func TestHTMLComplete(t *testing.T) { 57 | c := &Client{Hostname: "http://localhost:3000"} 58 | index, err := NewDocumentFromPath("index.html", test.HTMLTestFilePath(t, "index.html")) 59 | require.Nil(t, err) 60 | req := NewHTMLRequest(index) 61 | header, err := NewDocumentFromPath("header.html", test.HTMLTestFilePath(t, "header.html")) 62 | require.Nil(t, err) 63 | req.Header(header) 64 | footer, err := NewDocumentFromPath("footer.html", test.HTMLTestFilePath(t, "footer.html")) 65 | require.Nil(t, err) 66 | req.Footer(footer) 67 | font, err := NewDocumentFromPath("font.woff", test.HTMLTestFilePath(t, "font.woff")) 68 | require.Nil(t, err) 69 | img, err := NewDocumentFromPath("img.gif", test.HTMLTestFilePath(t, "img.gif")) 70 | require.Nil(t, err) 71 | style, err := NewDocumentFromPath("style.css", test.HTMLTestFilePath(t, "style.css")) 72 | req.Assets(font, img, style) 73 | req.ResultFilename("foo.pdf") 74 | req.WaitTimeout(5) 75 | req.WaitDelay(1) 76 | req.PaperSize(A4) 77 | req.Margins(NormalMargins) 78 | req.Landscape(false) 79 | req.GoogleChromeRpccBufferSize(1048576) 80 | req.Scale(1.5) 81 | dirPath, err := test.Rand() 82 | require.Nil(t, err) 83 | dest := fmt.Sprintf("%s/foo.pdf", dirPath) 84 | err = c.Store(req, dest) 85 | assert.Nil(t, err) 86 | assert.FileExists(t, dest) 87 | err = os.RemoveAll(dirPath) 88 | assert.Nil(t, err) 89 | } 90 | 91 | func TestHTMLPageRanges(t *testing.T) { 92 | c := &Client{Hostname: "http://localhost:3000"} 93 | index, err := NewDocumentFromPath("index.html", test.HTMLTestFilePath(t, "index.html")) 94 | require.Nil(t, err) 95 | req := NewHTMLRequest(index) 96 | req.PageRanges("1-1") 97 | resp, err := c.Post(req) 98 | assert.Nil(t, err) 99 | assert.Equal(t, 200, resp.StatusCode) 100 | } 101 | 102 | func TestHTMLWebhook(t *testing.T) { 103 | c := &Client{Hostname: "http://localhost:3000"} 104 | index, err := NewDocumentFromPath("index.html", test.HTMLTestFilePath(t, "index.html")) 105 | require.Nil(t, err) 106 | req := NewHTMLRequest(index) 107 | req.WebhookURL("https://google.com") 108 | req.WebhookURLTimeout(5.0) 109 | req.AddWebhookURLHTTPHeader("A-Header", "Foo") 110 | resp, err := c.Post(req) 111 | assert.Nil(t, err) 112 | assert.Equal(t, 200, resp.StatusCode) 113 | } 114 | -------------------------------------------------------------------------------- /markdown.go: -------------------------------------------------------------------------------- 1 | package gotenberg 2 | 3 | // MarkdownRequest facilitates Markdown conversion 4 | // with the Gotenberg API. 5 | type MarkdownRequest struct { 6 | index Document 7 | markdowns []Document 8 | assets []Document 9 | 10 | *chromeRequest 11 | } 12 | 13 | // NewMarkdownRequest create MarkdownRequest. 14 | func NewMarkdownRequest(index Document, markdowns ...Document) *MarkdownRequest { 15 | return &MarkdownRequest{index, markdowns, []Document{}, newChromeRequest()} 16 | } 17 | 18 | // Assets sets assets form files. 19 | func (req *MarkdownRequest) Assets(assets ...Document) { 20 | req.assets = assets 21 | } 22 | 23 | func (req *MarkdownRequest) postURL() string { 24 | return "/convert/markdown" 25 | } 26 | 27 | func (req *MarkdownRequest) formFiles() map[string]Document { 28 | files := make(map[string]Document) 29 | files["index.html"] = req.index 30 | for _, markdown := range req.markdowns { 31 | files[markdown.Filename()] = markdown 32 | } 33 | if req.header != nil { 34 | files["header.html"] = req.header 35 | } 36 | if req.footer != nil { 37 | files["footer.html"] = req.footer 38 | } 39 | for _, asset := range req.assets { 40 | files[asset.Filename()] = asset 41 | } 42 | return files 43 | } 44 | 45 | // Compile-time checks to ensure type implements desired interfaces. 46 | var ( 47 | _ = Request(new(MarkdownRequest)) 48 | ) 49 | -------------------------------------------------------------------------------- /markdown_test.go: -------------------------------------------------------------------------------- 1 | package gotenberg 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | "github.com/thecodingmachine/gotenberg-go-client/v7/test" 11 | ) 12 | 13 | func TestMarkdown(t *testing.T) { 14 | c := &Client{Hostname: "http://localhost:3000"} 15 | index, err := NewDocumentFromPath("index.html", test.MarkdownTestFilePath(t, "index.html")) 16 | require.Nil(t, err) 17 | markdown1, err := NewDocumentFromPath("paragraph1.md", test.MarkdownTestFilePath(t, "paragraph1.md")) 18 | require.Nil(t, err) 19 | markdown2, err := NewDocumentFromPath("paragraph2.md", test.MarkdownTestFilePath(t, "paragraph2.md")) 20 | require.Nil(t, err) 21 | markdown3, err := NewDocumentFromPath("paragraph3.md", test.MarkdownTestFilePath(t, "paragraph3.md")) 22 | require.Nil(t, err) 23 | req := NewMarkdownRequest(index, markdown1, markdown2, markdown3) 24 | dirPath, err := test.Rand() 25 | require.Nil(t, err) 26 | dest := fmt.Sprintf("%s/foo.pdf", dirPath) 27 | err = c.Store(req, dest) 28 | assert.Nil(t, err) 29 | assert.FileExists(t, dest) 30 | err = os.RemoveAll(dirPath) 31 | assert.Nil(t, err) 32 | } 33 | 34 | func TestMarkdownComplete(t *testing.T) { 35 | c := &Client{Hostname: "http://localhost:3000"} 36 | index, err := NewDocumentFromPath("index.html", test.MarkdownTestFilePath(t, "index.html")) 37 | require.Nil(t, err) 38 | markdown1, err := NewDocumentFromPath("paragraph1.md", test.MarkdownTestFilePath(t, "paragraph1.md")) 39 | require.Nil(t, err) 40 | markdown2, err := NewDocumentFromPath("paragraph2.md", test.MarkdownTestFilePath(t, "paragraph2.md")) 41 | require.Nil(t, err) 42 | markdown3, err := NewDocumentFromPath("paragraph3.md", test.MarkdownTestFilePath(t, "paragraph3.md")) 43 | require.Nil(t, err) 44 | req := NewMarkdownRequest(index, markdown1, markdown2, markdown3) 45 | header, err := NewDocumentFromPath("header.html", test.MarkdownTestFilePath(t, "header.html")) 46 | require.Nil(t, err) 47 | req.Header(header) 48 | footer, err := NewDocumentFromPath("footer.html", test.MarkdownTestFilePath(t, "footer.html")) 49 | require.Nil(t, err) 50 | req.Footer(footer) 51 | font, err := NewDocumentFromPath("font.woff", test.MarkdownTestFilePath(t, "font.woff")) 52 | require.Nil(t, err) 53 | img, err := NewDocumentFromPath("img.gif", test.MarkdownTestFilePath(t, "img.gif")) 54 | require.Nil(t, err) 55 | style, err := NewDocumentFromPath("style.css", test.MarkdownTestFilePath(t, "style.css")) 56 | req.Assets(font, img, style) 57 | req.ResultFilename("foo.pdf") 58 | req.WaitTimeout(5) 59 | req.WaitDelay(1) 60 | req.PaperSize(A4) 61 | req.Margins(NormalMargins) 62 | req.Landscape(false) 63 | req.GoogleChromeRpccBufferSize(1048576) 64 | dirPath, err := test.Rand() 65 | require.Nil(t, err) 66 | dest := fmt.Sprintf("%s/foo.pdf", dirPath) 67 | err = c.Store(req, dest) 68 | assert.Nil(t, err) 69 | assert.FileExists(t, dest) 70 | err = os.RemoveAll(dirPath) 71 | assert.Nil(t, err) 72 | } 73 | 74 | func TestMarkdownPageRanges(t *testing.T) { 75 | c := &Client{Hostname: "http://localhost:3000"} 76 | index, err := NewDocumentFromPath("index.html", test.MarkdownTestFilePath(t, "index.html")) 77 | require.Nil(t, err) 78 | markdown1, err := NewDocumentFromPath("paragraph1.md", test.MarkdownTestFilePath(t, "paragraph1.md")) 79 | require.Nil(t, err) 80 | markdown2, err := NewDocumentFromPath("paragraph2.md", test.MarkdownTestFilePath(t, "paragraph2.md")) 81 | require.Nil(t, err) 82 | markdown3, err := NewDocumentFromPath("paragraph3.md", test.MarkdownTestFilePath(t, "paragraph3.md")) 83 | require.Nil(t, err) 84 | req := NewMarkdownRequest(index, markdown1, markdown2, markdown3) 85 | req.PageRanges("1-1") 86 | resp, err := c.Post(req) 87 | assert.Nil(t, err) 88 | assert.Equal(t, 200, resp.StatusCode) 89 | } 90 | 91 | func TestMarkdownWebhook(t *testing.T) { 92 | c := &Client{Hostname: "http://localhost:3000"} 93 | index, err := NewDocumentFromPath("index.html", test.MarkdownTestFilePath(t, "index.html")) 94 | require.Nil(t, err) 95 | markdown1, err := NewDocumentFromPath("paragraph1.md", test.MarkdownTestFilePath(t, "paragraph1.md")) 96 | require.Nil(t, err) 97 | markdown2, err := NewDocumentFromPath("paragraph2.md", test.MarkdownTestFilePath(t, "paragraph2.md")) 98 | require.Nil(t, err) 99 | markdown3, err := NewDocumentFromPath("paragraph3.md", test.MarkdownTestFilePath(t, "paragraph3.md")) 100 | require.Nil(t, err) 101 | req := NewMarkdownRequest(index, markdown1, markdown2, markdown3) 102 | req.WebhookURL("https://google.com") 103 | req.WebhookURLTimeout(5.0) 104 | req.AddWebhookURLHTTPHeader("A-Header", "Foo") 105 | resp, err := c.Post(req) 106 | assert.Nil(t, err) 107 | assert.Equal(t, 200, resp.StatusCode) 108 | } 109 | -------------------------------------------------------------------------------- /merge.go: -------------------------------------------------------------------------------- 1 | package gotenberg 2 | 3 | // MergeRequest facilitates merging PDF 4 | // with the Gotenberg API. 5 | type MergeRequest struct { 6 | pdfs []Document 7 | 8 | *request 9 | } 10 | 11 | // NewMergeRequest create MergeRequest. 12 | func NewMergeRequest(pdfs ...Document) *MergeRequest { 13 | return &MergeRequest{pdfs, newRequest()} 14 | } 15 | 16 | func (req *MergeRequest) postURL() string { 17 | return "/merge" 18 | } 19 | 20 | func (req *MergeRequest) formFiles() map[string]Document { 21 | files := make(map[string]Document) 22 | for _, pdf := range req.pdfs { 23 | files[pdf.Filename()] = pdf 24 | } 25 | return files 26 | } 27 | 28 | // Compile-time checks to ensure type implements desired interfaces. 29 | var ( 30 | _ = Request(new(MergeRequest)) 31 | ) 32 | -------------------------------------------------------------------------------- /merge_test.go: -------------------------------------------------------------------------------- 1 | package gotenberg 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | "github.com/thecodingmachine/gotenberg-go-client/v7/test" 11 | ) 12 | 13 | func TestMerge(t *testing.T) { 14 | c := &Client{Hostname: "http://localhost:3000"} 15 | pdf1, err := NewDocumentFromPath("gotenberg1.pdf", test.PDFTestFilePath(t, "gotenberg.pdf")) 16 | require.Nil(t, err) 17 | pdf2, err := NewDocumentFromPath("gotenberg2.pdf", test.PDFTestFilePath(t, "gotenberg.pdf")) 18 | require.Nil(t, err) 19 | req := NewMergeRequest(pdf1, pdf2) 20 | req.ResultFilename("foo.pdf") 21 | req.WaitTimeout(5) 22 | dirPath, err := test.Rand() 23 | require.Nil(t, err) 24 | dest := fmt.Sprintf("%s/foo.pdf", dirPath) 25 | err = c.Store(req, dest) 26 | assert.Nil(t, err) 27 | assert.FileExists(t, dest) 28 | err = os.RemoveAll(dirPath) 29 | assert.Nil(t, err) 30 | } 31 | 32 | func TestMergeWebhook(t *testing.T) { 33 | c := &Client{Hostname: "http://localhost:3000"} 34 | pdf1, err := NewDocumentFromPath("gotenberg1.pdf", test.PDFTestFilePath(t, "gotenberg.pdf")) 35 | require.Nil(t, err) 36 | pdf2, err := NewDocumentFromPath("gotenberg2.pdf", test.PDFTestFilePath(t, "gotenberg.pdf")) 37 | require.Nil(t, err) 38 | req := NewMergeRequest(pdf1, pdf2) 39 | req.WebhookURL("https://google.com") 40 | req.WebhookURLTimeout(5.0) 41 | req.AddWebhookURLHTTPHeader("A-Header", "Foo") 42 | resp, err := c.Post(req) 43 | assert.Nil(t, err) 44 | assert.Equal(t, 200, resp.StatusCode) 45 | } 46 | -------------------------------------------------------------------------------- /office.go: -------------------------------------------------------------------------------- 1 | package gotenberg 2 | 3 | import ( 4 | "strconv" 5 | ) 6 | 7 | const ( 8 | landscapeOffice string = "landscape" 9 | pageRangesOffice string = "pageRanges" 10 | ) 11 | 12 | // OfficeRequest facilitates Office documents 13 | // conversion with the Gotenberg API. 14 | type OfficeRequest struct { 15 | docs []Document 16 | 17 | *request 18 | } 19 | 20 | // NewOfficeRequest create OfficeRequest. 21 | func NewOfficeRequest(docs ...Document) *OfficeRequest { 22 | return &OfficeRequest{docs, newRequest()} 23 | } 24 | 25 | // Landscape sets landscape form field. 26 | func (req *OfficeRequest) Landscape(isLandscape bool) { 27 | req.values[landscapeOffice] = strconv.FormatBool(isLandscape) 28 | } 29 | 30 | // PageRanges sets pageRanges form field. 31 | func (req *OfficeRequest) PageRanges(ranges string) { 32 | req.values[pageRangesOffice] = ranges 33 | } 34 | 35 | func (req *OfficeRequest) postURL() string { 36 | return "/convert/office" 37 | } 38 | 39 | func (req *OfficeRequest) formFiles() map[string]Document { 40 | files := make(map[string]Document) 41 | for _, doc := range req.docs { 42 | files[doc.Filename()] = doc 43 | } 44 | return files 45 | } 46 | 47 | // Compile-time checks to ensure type implements desired interfaces. 48 | var ( 49 | _ = Request(new(OfficeRequest)) 50 | ) 51 | -------------------------------------------------------------------------------- /office_test.go: -------------------------------------------------------------------------------- 1 | package gotenberg 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | "github.com/thecodingmachine/gotenberg-go-client/v7/test" 11 | ) 12 | 13 | func TestOffice(t *testing.T) { 14 | c := &Client{Hostname: "http://localhost:3000"} 15 | doc, err := NewDocumentFromPath("document.docx", test.OfficeTestFilePath(t, "document.docx")) 16 | require.Nil(t, err) 17 | req := NewOfficeRequest(doc) 18 | req.ResultFilename("foo.pdf") 19 | req.WaitTimeout(5) 20 | req.Landscape(false) 21 | dirPath, err := test.Rand() 22 | require.Nil(t, err) 23 | dest := fmt.Sprintf("%s/foo.pdf", dirPath) 24 | err = c.Store(req, dest) 25 | assert.Nil(t, err) 26 | assert.FileExists(t, dest) 27 | err = os.RemoveAll(dirPath) 28 | assert.Nil(t, err) 29 | } 30 | 31 | func TestOfficePageRanges(t *testing.T) { 32 | c := &Client{Hostname: "http://localhost:3000"} 33 | doc, err := NewDocumentFromPath("document.docx", test.OfficeTestFilePath(t, "document.docx")) 34 | require.Nil(t, err) 35 | req := NewOfficeRequest(doc) 36 | req.PageRanges("1-1") 37 | resp, err := c.Post(req) 38 | assert.Nil(t, err) 39 | assert.Equal(t, 200, resp.StatusCode) 40 | } 41 | 42 | func TestOfficeWebhook(t *testing.T) { 43 | c := &Client{Hostname: "http://localhost:3000"} 44 | doc, err := NewDocumentFromPath("document.docx", test.OfficeTestFilePath(t, "document.docx")) 45 | require.Nil(t, err) 46 | req := NewOfficeRequest(doc) 47 | req.WebhookURL("https://google.com") 48 | req.WebhookURLTimeout(5.0) 49 | req.AddWebhookURLHTTPHeader("A-Header", "Foo") 50 | resp, err := c.Post(req) 51 | assert.Nil(t, err) 52 | assert.Equal(t, 200, resp.StatusCode) 53 | } 54 | -------------------------------------------------------------------------------- /test/testdata/html/font.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thecodingmachine/gotenberg-go-client/968713b31a038394f2c93edb5ff7621f9b052875/test/testdata/html/font.woff -------------------------------------------------------------------------------- /test/testdata/html/footer.html: -------------------------------------------------------------------------------- 1 | 2 |
3 | 9 | 10 | 11 |12 | of 13 |
14 | 15 | -------------------------------------------------------------------------------- /test/testdata/html/header.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /test/testdata/html/img.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thecodingmachine/gotenberg-go-client/968713b31a038394f2c93edb5ff7621f9b052875/test/testdata/html/img.gif -------------------------------------------------------------------------------- /test/testdata/html/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |17 |20 |It is a press, certainly, but a press from which shall flow in inexhaustible streams...Through it, God will spread His Word. A spring of truth shall flow from it: like a new star it shall scatter the darkness of ignorance, and cause a light heretofore unknown to shine amongst men.
18 | 19 |
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
25 | 26 |Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
28 | 29 |Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
31 |12 | of 13 |
14 | 15 | -------------------------------------------------------------------------------- /test/testdata/markdown/header.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /test/testdata/markdown/img.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thecodingmachine/gotenberg-go-client/968713b31a038394f2c93edb5ff7621f9b052875/test/testdata/markdown/img.gif -------------------------------------------------------------------------------- /test/testdata/markdown/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |17 |20 |It is a press, certainly, but a press from which shall flow in inexhaustible streams...Through it, God will spread His Word. A spring of truth shall flow from it: like a new star it shall scatter the darkness of ignorance, and cause a light heretofore unknown to shine amongst men.
18 | 19 |
12 | of 13 |
14 | 15 | -------------------------------------------------------------------------------- /test/testdata/url/header.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /test/testfunc.go: -------------------------------------------------------------------------------- 1 | // Package test contains useful functions used across tests. 2 | package test 3 | 4 | import ( 5 | "crypto/rand" 6 | "encoding/hex" 7 | "fmt" 8 | "path" 9 | "path/filepath" 10 | "runtime" 11 | "testing" 12 | 13 | "github.com/stretchr/testify/require" 14 | ) 15 | 16 | // Rand returns a random string. 17 | func Rand() (string, error) { 18 | randBytes := make([]byte, 16) 19 | _, err := rand.Read(randBytes) 20 | if err != nil { 21 | return "", fmt.Errorf("creating random string: %v", err) 22 | } 23 | return hex.EncodeToString(randBytes), nil 24 | } 25 | 26 | // HTMLTestFilePath returns the absolute file path 27 | // of a file in "html" folder in test/testdata. 28 | func HTMLTestFilePath(t *testing.T, filename string) string { 29 | return abs(t, "html", filename) 30 | } 31 | 32 | // URLTestFilePath returns the absolute file path 33 | // of a file in "url" folder in test/testdata. 34 | func URLTestFilePath(t *testing.T, filename string) string { 35 | return abs(t, "url", filename) 36 | } 37 | 38 | // MarkdownTestFilePath returns the absolute file path 39 | // of a file in "markdown" folder in test/testdata. 40 | func MarkdownTestFilePath(t *testing.T, filename string) string { 41 | return abs(t, "markdown", filename) 42 | } 43 | 44 | // OfficeTestFilePath returns the absolute file path 45 | // of a file in "office" folder in test/testdata. 46 | func OfficeTestFilePath(t *testing.T, filename string) string { 47 | return abs(t, "office", filename) 48 | } 49 | 50 | // PDFTestFilePath returns the absolute file path 51 | // of a file in "pdf" folder in test/testdata. 52 | func PDFTestFilePath(t *testing.T, filename string) string { 53 | return abs(t, "pdf", filename) 54 | } 55 | 56 | func abs(t *testing.T, kind, filename string) string { 57 | _, gofilename, _, ok := runtime.Caller(0) 58 | require.Equal(t, ok, true, "got no caller information") 59 | if filename == "" { 60 | path, err := filepath.Abs(fmt.Sprintf("%s/testdata/%s", path.Dir(gofilename), kind)) 61 | require.Nil(t, err, `getting the absolute path of "%s"`, kind) 62 | return path 63 | } 64 | path, err := filepath.Abs(fmt.Sprintf("%s/testdata/%s/%s", path.Dir(gofilename), kind, filename)) 65 | require.Nil(t, err, `getting the absolute path of "%s"`, filename) 66 | return path 67 | } 68 | -------------------------------------------------------------------------------- /url.go: -------------------------------------------------------------------------------- 1 | package gotenberg 2 | 3 | import "fmt" 4 | 5 | const ( 6 | remoteURL string = "remoteURL" 7 | remoteURLBaseHTTPHeaderKey string = "Gotenberg-Remoteurl-" 8 | ) 9 | 10 | // URLRequest facilitates remote URL conversion 11 | // with the Gotenberg API. 12 | type URLRequest struct { 13 | *chromeRequest 14 | } 15 | 16 | // NewURLRequest create URLRequest. 17 | func NewURLRequest(url string) *URLRequest { 18 | req := &URLRequest{newChromeRequest()} 19 | req.values[remoteURL] = url 20 | return req 21 | } 22 | 23 | func (req *URLRequest) postURL() string { 24 | return "/convert/url" 25 | } 26 | 27 | // AddRemoteURLHTTPHeader add a remote URL custom HTTP header. 28 | func (req *URLRequest) AddRemoteURLHTTPHeader(key, value string) { 29 | key = fmt.Sprintf("%s%s", remoteURLBaseHTTPHeaderKey, key) 30 | req.httpHeaders[key] = value 31 | } 32 | 33 | func (req *URLRequest) formFiles() map[string]Document { 34 | files := make(map[string]Document) 35 | if req.header != nil { 36 | files["header.html"] = req.header 37 | } 38 | if req.footer != nil { 39 | files["footer.html"] = req.footer 40 | } 41 | return files 42 | } 43 | 44 | // Compile-time checks to ensure type implements desired interfaces. 45 | var ( 46 | _ = Request(new(URLRequest)) 47 | ) 48 | -------------------------------------------------------------------------------- /url_test.go: -------------------------------------------------------------------------------- 1 | package gotenberg 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | "github.com/thecodingmachine/gotenberg-go-client/v7/test" 11 | ) 12 | 13 | func TestURL(t *testing.T) { 14 | c := &Client{Hostname: "http://localhost:3000"} 15 | req := NewURLRequest("http://google.com") 16 | dirPath, err := test.Rand() 17 | require.Nil(t, err) 18 | dest := fmt.Sprintf("%s/foo.pdf", dirPath) 19 | err = c.Store(req, dest) 20 | assert.Nil(t, err) 21 | assert.FileExists(t, dest) 22 | err = os.RemoveAll(dirPath) 23 | assert.Nil(t, err) 24 | } 25 | 26 | func TestURLComplete(t *testing.T) { 27 | c := &Client{Hostname: "http://localhost:3000"} 28 | req := NewURLRequest("http://google.com") 29 | header, err := NewDocumentFromPath("header.html", test.HTMLTestFilePath(t, "header.html")) 30 | require.Nil(t, err) 31 | req.Header(header) 32 | footer, err := NewDocumentFromPath("footer.html", test.HTMLTestFilePath(t, "footer.html")) 33 | require.Nil(t, err) 34 | req.Footer(footer) 35 | req.ResultFilename("foo.pdf") 36 | req.WaitTimeout(5) 37 | req.WaitDelay(1) 38 | req.PaperSize(A4) 39 | req.Margins(NormalMargins) 40 | req.Landscape(false) 41 | req.GoogleChromeRpccBufferSize(1048576) 42 | req.AddRemoteURLHTTPHeader("A-Header", "Foo") 43 | dirPath, err := test.Rand() 44 | require.Nil(t, err) 45 | dest := fmt.Sprintf("%s/foo.pdf", dirPath) 46 | err = c.Store(req, dest) 47 | assert.Nil(t, err) 48 | assert.FileExists(t, dest) 49 | err = os.RemoveAll(dirPath) 50 | assert.Nil(t, err) 51 | } 52 | 53 | func TestURLPageRanges(t *testing.T) { 54 | c := &Client{Hostname: "http://localhost:3000"} 55 | req := NewURLRequest("http://google.com") 56 | req.PageRanges("1-1") 57 | resp, err := c.Post(req) 58 | assert.Nil(t, err) 59 | assert.Equal(t, 200, resp.StatusCode) 60 | } 61 | 62 | func TestURLWebhook(t *testing.T) { 63 | c := &Client{Hostname: "http://localhost:3000"} 64 | req := NewURLRequest("http://google.com") 65 | req.WebhookURL("https://google.com") 66 | req.WebhookURLTimeout(5.0) 67 | req.AddWebhookURLHTTPHeader("A-Header", "Foo") 68 | resp, err := c.Post(req) 69 | assert.Nil(t, err) 70 | assert.Equal(t, 200, resp.StatusCode) 71 | } 72 | --------------------------------------------------------------------------------